Tutorial: two applications in Python
The two examples of the catalogue written in Python are this tutorial:
Customer Interview, a chat, and Report from a File, a widget. Each is
an app.py beside the Appspec it builds, in agentspecs
(agentspecs/apps/<id>/app.py); a test of agent-runtimes builds each again
and compares, so the two never drift. Start one of them as your own, read
what each part does, change it, then save it on Datalayer.
The reference for every call is Applications in Python.
Both run in process in agent-runtimes' tests, with a scripted model
(test_apps_examples.py; test_app_sessions.py for Report from a File,
which takes its file from the page through the Session API),
and test_apps_init.py starts an application from each. Both are switched off in the
catalogue: neither has run with a real model, and each says so in its spec
(tests.verified).
1. Start from an example
loop apps init interview --from customer-interview
cd interview
init writes a folder named by the new id: app.py, the source, under the
new id; app.yaml, the Appspec it builds; and tests/test_app.py, which
checks that the two agree and that the spec passes the instant checks:
pytest tests runs it.
2. Customer Interview: a chat
What it declares
app = Application.from_spec({...}) # its spec: name, agent, instructions, ui, page, tests, record
app.starter("Trial churn", "Interview me about why I stopped after the trial.")
app.setting(
"language",
{"type": "string", "title": "Language", "enum": ["English", "French"], "default": "English"},
)
app.setting(
"length", {"type": "integer", "title": "Questions", "minimum": 3, "maximum": 15, "default": 8}
)
app.rule("Send the summary by email", applies_to="send", behaviour="ask_first")
Application.from_spec attaches code to a spec; the plain calls after it add
to that spec what the Canvas would: suggested questions, settings, a rule.
Everything declared is the Appspec loop apps build writes.
What it reacts to
@app.start
async def opening(session: Session) -> None:
consent = await session.ask(
ChoiceQuestion("May this interview be recorded, to be read by the team?", ("Yes", "No"))
)
session.state["consent"] = consent == "Yes"
if not session.state["consent"]:
await session.send("Understood: I ask nothing more. Thank you.")
return
session.state["goal"] = await session.ask("What do you want to learn from this interview?")
session.state["insights"] = []
@app.startruns when a session opens.session.askstops and asks the person — here a choice, then text — and goes on with the answer.session.stateis what the application keeps across the turns of one session.
@app.message
async def reply(session: Session, text: str) -> None:
...
async with session.step("Choosing the next question", kind="model", input=text) as step:
answer = await session.agent.run(
text,
goal=session.state["goal"],
language=session.settings["language"],
questions=session.settings["length"],
)
step.output = answer.text
await session.send(answer.text)
@app.messageanswers each message.session.agentis the application's agent with its rules, checks and record on; what is given after the text goes with the request.session.stepshows what it is doing while it does it: a row in the conversation, and astepentry in the record (see Steps).
class Insight(BaseModel):
insight: str = Field(..., min_length=1)
quote: str = Field(..., min_length=1)
@app.action("save", payload=Insight)
async def save(session: Session, saved: Insight) -> None:
...
await session.record(insight, summary="An insight", kind="feedback")
@app.action("finish")
async def finish(session: Session) -> None:
...
await session.record(result, summary="The interview's result")
await session.send(answer.text)
@app.action(name, payload=Model) is what a button named so does — in what
an answer shows, or pressed with the token its session offers. It is typed:
the model's JSON Schema is written in the Appspec (interface.actions), a
press is checked against it before the handler runs and the handler is given
the model; an action without a model takes nothing (RU-5). session.record writes to the application's record, kept
as its spec's record.include says.
Try it
loop apps run app.py --local --watch
The terminal is the application's host: start runs as the session opens,
session.ask asks at the prompt, /action save {"insight": "…", "quote": "…"} presses a button, and --watch builds the file again when you change
it, the session's state kept.
Change it: show the insights
At the end of an interview, show the insights as a table under the answer (see What an answer shows):
await session.send(
answer.text,
show=[session.ui.table("insights", columns=["insight", "quote"], rows={"path": "/insights"})],
data={"insights": result["insights"]},
)
3. Report from a File: a widget
loop apps init report --from report-from-a-file
A widget is a page with inputs and an output. Its settings are what the page asks before it runs — a report's kind and what to look at:
app.setting("report", {"type": "string", "title": "Report", "enum": ["Summary", "Full"], "default": "Summary"})
app.setting("question", {"type": "string", "title": "What to look at", "default": ""})
app.rule("Send the report by email", applies_to="send", behaviour="leave_to_me")
Its one reaction asks for the file, has its agent analyse it in a step, and keeps the report in its record:
@app.message
async def run(session: Session, text: str) -> None:
file: UploadedFile = await session.ask(
FileQuestion("Which file?", accept=(".csv",), max_bytes=25 * 1024 * 1024)
)
async with session.step("Analysing the file in the sandbox", kind="run", input=file.name) as step:
answer = await session.agent.run(text or "Report on this file.", file=file.name, ...)
step.output = answer.text
await session.record({"file": file.name, ...}, summary=f"The report on {file.name}")
await session.send(answer.text)
FileQuestionasks for a file of the kinds it accepts, up to a size; what is not one of them is refused in a sentence. On its page, the file chosen answers the question — given before it is asked, or after.- Its instructions have its agent read the file in code, in the sandbox, rather than estimate from it: every number in the report is computed.
4. Check it, and save it on Datalayer
loop apps validate app.py # the instant checks, on the spec it builds
loop apps build app.py --out app.yaml # its Appspec, its code marked at the top
loop apps push app.py --app <its id> # saved as the application's next version
push keeps the file itself in the application's item, beside its spec, and
versions it with it; the Studio shows it in the Python tab, and the
Canvas links what the code decides to its line. loop apps pull <its id>
writes both back. See Where the file lives.
An application built by spec or on the Canvas comes to Python the other way:
loop apps eject app.yaml writes the app.py that builds it — one way, and
said before it is done.