Web server¶
The optional Web package provides a small FastAPI server for one or more
Agents. Use serve() for a standalone process or create_app() when your
deployment already owns the ASGI lifecycle.
from lovia import Agent
from lovia.web import serve
agent = Agent(name="assistant", model="<model>")
serve(agent, host="127.0.0.1", port=8000, db_path="lovia.db")
serve() and create_app()¶
serve(agent_or_agents, *, host="127.0.0.1", port=8000, ...) creates the
application and runs uvicorn. Extra server options such as log_level,
ssl_certfile, and workers pass through. create_app(...) returns the ASGI
application without starting a process.
| Option | Default | Description |
|---|---|---|
agent_or_agents |
required | One Agent or a {name: agent} mapping |
db_path / store / session |
./.lovia/<agent>.db |
Transcript and chat metadata storage |
max_turns / budget / retry / context_policy |
— | Settings applied to every served Run |
tracer |
None |
Span recorder for served Runs |
generate_titles / title_model |
True / Agent model |
Generate conversation titles in the background |
followups / followup_model |
False / Agent model |
Suggest follow-up questions after a reply (see below) |
approval_timeout |
None |
Auto-deny unresolved approvals after N seconds |
max_background_runs (create_app() only) |
8 |
Concurrent supervised Runs; excess starts return 429 |
ui |
True |
Set False for API-only serving |
cors_origins |
None |
Allowed browser origins; unset sends no CORS headers |
token / auth |
None |
Bearer-token guard for business API routes, or your own FastAPI dependency (see below) |
title / empty_title / empty_description |
lovia defaults | UI copy and branding |
empty_examples |
None |
Clickable starter prompts on the blank chat state (clicking fills the composer) |
serve() always uses max_background_runs=8. To change it, build the app with
create_app() and run it with an ASGI server.
For endpoint contracts and the ChatStore interface, see
HTTP API.
Follow-up suggestions¶
Once a turn finishes, the UI can offer a few questions the user might want to ask next, rendered as clickable chips (clicking one sends it). They come from a separate, tiny model call — the main Transcript never sees the request — made only after a run that actually answered.
Off by default: unlike a title, this costs one model call per run, so the
serving layer doesn't presume it. followup_model points that call at a
cheaper model than the Agent's own. The bundled lovia web CLI opts in on its
own behalf — disable it with --no-followups or LOVIA_FOLLOWUPS=0, and point
it at a small model with LOVIA_FOLLOWUP_MODEL.
Pass a callable to replace the built-in suggester entirely — back it with a curated FAQ, a vector store, or the same generator under a different prompt:
from lovia.web import FollowupRequest, create_app, generate_followups
async def pricing_only(request: FollowupRequest) -> list[str]:
return await generate_followups(
request, model="<model>", instructions="Ask only about pricing."
)
create_app(agent, followups=pricing_only)
A suggester receives the session's conversation (session_id, agent, and the
chat-shaped messages) and returns any number of strings. Returning [] shows
nothing, which is also what a raising suggester degrades to — the chips are
never load-bearing.
Authentication¶
Loopback binds need no credentials. serve() is safe by default beyond that:
binding a non-loopback host with neither token nor auth generates a token
and prints it once, together with a ready /?token=... UI link. This prevents
the business API from being exposed anonymously on a non-loopback address.
serve(agent, host="0.0.0.0", token="s3cret") # fixed token
serve(agent, host="0.0.0.0") # generated + printed
The token guards the business routes registered by build_api_router.
/healthz, /api/docs, /api/openapi.json, the UI shell, and static assets
remain public. Clients can supply the token in two ways:
- Plain API requests and chat SSE send
Authorization: Bearer <token>. Chat streams usefetch, so headers work. - The bundled UI stores the token in a cookie.
/api/eventsusesEventSource, which cannot set custom headers, so it authenticates with the cookie;<img>previews and download links use it too. The UI reads the token from a/?token=...link or asks for it after a 401.
For sessions, OAuth, or per-user identity, replace the built-in check with any FastAPI dependency — it guards the same routes:
async def my_auth(request: Request) -> None:
if not valid(request):
raise HTTPException(status_code=401)
serve(agent, host="0.0.0.0", auth=my_auth)
create_app() accepts the same two parameters but enables no authentication
by default. Pass either token or auth explicitly when you own the app.
Supervised Run lifecycle¶
Streaming Runs are server-owned tasks. An SSE subscriber may disconnect and reattach without cancelling work.
- User cancellation finalizes completed turns into the Session, removes dangling Tool calls, and clears the checkpoint.
- Server shutdown cancels Runs cooperatively but keeps checkpoints, so a client can reconnect and resume after deployment.
- Capacity is bounded by
max_background_runs; new starts receive HTTP 429 while full. - Blocking
/api/chatis not supervised. Front ends should use/api/chat/stream.
Live Runs, approvals, and SSE hubs are process-local. Run one worker. SQLite data uses WAL and survives restarts, but it does not make in-memory supervision safe across multiple workers.
Scheduling¶
The Web package stores durable schedules and supports three trigger forms:
| Trigger | Value |
|---|---|
at |
One ISO-8601 timestamp or epoch time |
every |
Interval in seconds |
cron |
Cron expression; croniter ships with lovia[web] |
Scheduling(store) contributes the approval-gated schedule_run Tool. The
model can propose a future run, but it is not created until a user approves the
Tool call. continue_session=True appends results to the same chat; otherwise
each fire starts a new Session. Delivery is at-most-once and coalesced: a fire
is skipped while the previous one is still running.
A repeating schedule can carry a stop condition (until, natural language):
each fire is then told to evaluate it after doing the task and cancel the
schedule once it is met — "check the log every minute until it says ready".
Deterministic safety nets (max_fires, expires_at) deactivate the schedule
even if the condition is never met; the tool requires one when until is set.
The plugin also contributes list_schedules and cancel_schedule, which need
no approval: cancelling only deactivates (resume or delete from the panel),
and the self-cancel must work inside a clientless scheduled run, where an
approval request would be auto-denied.
Security checklist¶
- Keep
host="127.0.0.1"for personal use; non-loopback binds are token-guarded automatically, but the token then protects everything — treat it like a password. - Restrict or disable writable Workspace access for untrusted users: anyone holding the token can make the agent edit files or run shell commands.
- Configure
approval_timeoutso abandoned dialogs do not occupy capacity. - Use one worker and back up the SQLite database.
- For real multi-user exposure add TLS, per-user auth (
auth=), and rate limiting — a shared token is single-user security.
See the complete Deployment guide before production use.
See also¶
- Web UI — built-in browser experience and CLI
- HTTP API — endpoints, SSE wire format, and
ChatStore - Tools: approval — server-side approval flow