Skip to main content

lovia

English | 简体中文

lovia is an elegant, restrained Python framework for developers who want to own the agent loop without rebuilding every supporting primitive from scratch. It covers the recurring hard parts of agent applications — tools, sessions, events, context compaction, serving — without turning into a platform.

pip install lovia
from lovia import Agent, tool


@tool
def get_order(order_id: str) -> str:
    """Look up an order's status by id."""
    return f"Order {order_id}: shipped 2 days ago, arriving Thursday."


# Configure OPENAI_BASE_URL and OPENAI_API_KEY in your environment.
agent = Agent(
    name="support",
    instructions="You are a customer-support agent. Look the order up before "
    "answering, and reply in one or two concrete sentences.",
    model="glm-5.2",
    tools=[get_order],
)

# run_sync() suits scripts and notebooks; from async code,
# use `await Runner.run(agent, ...)` instead.
result = agent.run_sync("Where is my order A-1042?")
print(result.output)

Or serve a full chat UI — memory, skills, scheduling, and a workspace on the current directory included — in one line:

pip install "lovia[web]" && lovia web

Anthropic is built in too: configure ANTHROPIC_API_KEY, set ANTHROPIC_BASE_URL for non-default endpoints, and use the anthropic: model prefix. Everything else about models lives in Providers & models.

Documentation

This README is a quick tour. For the full guide, start with the quickstart and core concepts in the documentation. The examples are a numbered, runnable learning path.

Why lovia

Composable primitives, ordinary Python — no new universe of abstractions:

  • Minimum dependencies. The core depends only on httpx, pydantic, and pyyaml; install everything else only when needed.
  • Few abstractions. An Agent is treated as immutable configuration and normally varied with clone(); a Runner executes one run, and @tool wraps a typed callable as a Tool; handoff and agent-as-tool compose agents; plugins package the rest.
  • Readable. The critical path is concentrated and explicit: model calls, tool execution, retries, and persistence happen in a clear order. When something surprises you, there is one chain to follow.
  • Lightweight model integration. OpenAI, Anthropic, and compatible endpoints are built in. There is no adapter stack to fight; a new provider is just a small Protocol.
  • Cache-friendly context management. Compaction changes only the next model view, without rewriting the conversation body, and keeps prompt prefixes stable. Explicit output limits and persistence maintenance still apply.
  • Production seams, not a production costume. Approvals, budgets, cancellation, mid-run steering, retries, checkpoint/resume — explicit knobs you wire into your own app.
  • One extension axis. Skills, MCP, Todo, and Memory are all plugins on the same seam you get for your own capabilities.

The design pressure throughout is restraint: if a feature can be a short user-side recipe, it stays out of the framework.

The tour

Every stop below has a full guide; each snippet runs as written.

Agents

An Agent is declarative configuration — no conversation state, safe to share, cheap to clone(). Tools, a workspace, and plugins compose directly on that configuration:

from lovia import Agent, Memory, Skills, tool
from lovia.workspace import Workspace


@tool
def light_travel_time(distance_km: float) -> str:
    """Calculate one-way light-signal delay for a distance in kilometers."""
    return f"{distance_km / 299_792.458:.2f} seconds"


agent = Agent(
    name="science-writer",
    instructions="Explain complex science with vivid, everyday analogies.",
    model="gpt-5.5",
    tools=[light_travel_time],
    workspace=Workspace.local(".", mode="readonly"),
    plugins=[
        Skills("./skills"),
        Memory(),
    ],
)

→ Agents

Running and streaming

One run, three consumption styles — and the stream handle is both async-iterable and awaitable. Iteration never raises: every stream ends with exactly one terminal event.

from lovia import Runner, events

handle = Runner.stream(
    agent,
    "How long does a signal take to reach Mars at 225 million km?",
)

async for ev in handle:
    if isinstance(ev, events.TextDelta):
        print(ev.delta, end="", flush=True)

result = await handle.result()

→ Running · Streaming

Tools

@tool wraps a typed callable as a Tool; the schema comes from signatures, docstrings, Annotated, and Pydantic Fields. One turn's calls execute concurrently by default — tools with non-reentrant side effects opt out and become execution barriers:

from typing import Annotated
from pydantic import Field
from lovia import tool


@tool(strict=True)
def search_docs(
    query: Annotated[str, "Search terms"],
    limit: Annotated[int, Field(ge=1, le=10)] = 5,
) -> list[str]:
    """Search internal documentation."""
    return []


@tool(parallel=False)
async def apply_migration(name: str) -> str:
    """Apply a database migration (never concurrently with other tools)."""
    return "applied"

The built-in web tools (read_page, http_request, web_search) fence what they fetch, so the model reads it as material rather than instructions; fence() does the same for your own tools.

→ Tools · Built-in tools

Structured output

Pass a Pydantic model, dataclass, TypedDict, or plain type; the final answer is validated — and repaired once, by default, when it doesn't parse:

from pydantic import BaseModel
from lovia import Agent, Runner


class Brief(BaseModel):
    title: str
    bullets: list[str]


agent = Agent(name="summarizer", model="glm-5.2", output_type=Brief)
result = await Runner.run(agent, "Summarize Transformer for a Python developer.")
print(result.output.title)

→ Structured output

Providers

Use model strings or provider instances. OpenAI-compatible endpoints read OPENAI_BASE_URL / OPENAI_API_KEY; Anthropic defaults to the official endpoint, reads ANTHROPIC_API_KEY, and uses ANTHROPIC_BASE_URL for non-default endpoints. Prompt caching and reasoning models are handled per endpoint, and a custom provider is a small Protocol:

from lovia import Agent, ModelSettings

agent = Agent(
    name="assistant",
    model="anthropic:<model>",
    settings=ModelSettings(temperature=0.2, max_tokens=800),
)

→ Providers & models

Multi-agent

Your task Use Runtime behavior
Let a specialist take over the conversation Handoff The specialist receives full history and continues in the same Run
Get a subtask result before the parent continues Agent-as-tool The child receives only the delegated prompt and returns a Tool result
Run independent work in the background Subagents The child runs independently and reports back later

The three alternatives below show the same researcher in each mode:

from lovia import Agent, Subagents

researcher = Agent(
    name="researcher",
    instructions="Research the given topic and return a concise report.",
    model="glm-5.2",
)

# Handoff: researcher takes over and receives the full conversation
router = Agent(
    name="router",
    instructions="Transfer to researcher when in-depth research is needed.",
    model="deepseek-v4-flash",
    handoffs=[researcher],
)

# Agent-as-tool: manager waits for the research, then answers the user itself
manager = Agent(
    name="manager",
    instructions="Ask researcher for any research needed before answering.",
    model="deepseek-v4-flash",
    tools=[researcher.as_tool(description="Research a topic and return a report.")],
)

# Subagents: assistant starts background research and keeps working
assistant = Agent(
    name="assistant",
    instructions="Run independent research in the background while drafting the answer.",
    model="deepseek-v4-flash",
    plugins=[Subagents([researcher])],
)

lovia web enables Subagents by default and reports completed work back to the original conversation.

→ Multi-agent

Human in the loop

Gate sensitive tools; resolve from your UI, a server-side policy, or an out-of-band channel — unresolved approvals deny, so runs never hang:

from lovia import Runner, events, tool


@tool(needs_approval=True)
async def refund(order_id: str, amount_cents: int) -> str:
    """Issue a refund."""
    return "refunded"


async for ev in Runner.stream(agent, "Refund order A123."):
    if isinstance(ev, events.ApprovalRequired):
        ev.approve()          # or ev.reject()

→ Tool approval

Sessions and checkpoints

Sessions persist conversations across runs; checkpoints make single runs crash-safe and idempotent — re-issuing a completed run_id replays its result without calling the model:

from lovia import CheckpointOptions, Runner, SQLiteCheckpointer, SQLiteSession

session = SQLiteSession("chat.db")
await Runner.run(agent, "My project is called Atlas.", session=session, session_id="u1")

cp = SQLiteCheckpointer("runs.db")
result = await Runner.run(
    agent,
    "Migrate the report format.",
    checkpoint=CheckpointOptions(cp, "report-migration-42"),
)

→ Sessions & checkpoints

Context compaction

Long conversations survive the window without rewriting history: compaction is view-only, keeps the prompt prefix stable for provider caches:

from lovia import Agent, Compaction

agent = Agent(
    name="companion",
    model="glm-5.2",
    context_policy=Compaction(context_window=200_000, compact_at=0.85, compact_to=0.60),
)

→ Context management

Guardrails and reliability

Guardrails veto runs at the input/output boundary. Reliability follows one placement rule — retry posture on the agent, per-run limits on the run:

from lovia import Agent, RetryPolicy, RunBudget, Runner
from lovia.exceptions import GuardrailTripped


async def must_cite(output, ctx):
    if "source:" not in str(output).lower():
        return "Missing source citation."


agent = Agent(
    name="researcher",
    model="glm-5.2",
    output_guardrails=[must_cite],
    retry=RetryPolicy(max_attempts=2)
)

result = await Runner.run(
    agent,
    "Analyze these logs.",
    budget=RunBudget(max_tool_calls=20, max_seconds=60)
)

→ Guardrails · Provider retries · Budgets & limits

Hooks and steering

Hooks observe every run event (fail-open, same types as streaming); the mailbox is the inbound dual of cancellation — push a message into a live run and the model sees it next turn. A run can even steer itself:

from lovia import Mailbox, RunContext, Runner, events
from lovia.hooks import AgentHooks

hooks = AgentHooks()


@hooks.on(events.TurnStarted)
def deadline(ev, ctx: RunContext):
    if ev.turn == 9:
        ctx.mailbox.push("Last turn: answer with what you have.")


mailbox = Mailbox()
handle = Runner.stream(agent.clone(hooks=hooks), "Analyze these logs.", mailbox=mailbox)
mailbox.push("Focus on the 5xx spike around 14:00.")  # seen next turn

→ Observability · Cancellation & steering

Workspace

File and shell tools scoped to a root, governed by one allow/ask/deny policy over paths and commands — ask decisions ride the normal approval channel. shell(background=true) runs dev servers, watchers, and long builds as session-owned background processes (poll read_process_output, stop with kill_process):

from lovia import Agent
from lovia.workspace import CommandRule, Workspace

agent = Agent(
    name="coder",
    instructions="Make small, targeted code changes.",
    model="glm-5.2",
    workspace=Workspace.local(
        ".",
        mode="coding",
        denied_paths=(".env*",),
        command_rules=(CommandRule("pytest", "allow"), CommandRule("rm -rf", "deny")),
    ),
)

→ Workspace

Plugins

One extension axis: a plugin contributes tools, prompt text, per-turn view injectors, hooks, and guardrails — never control flow:

from lovia import Agent, Skills, Todo
from lovia.plugins.mcp import MCP, MCPServerStdio

agent = Agent(
    name="builder",
    model="glm-5.2",
    plugins=[
        Todo(),
        Skills("./skills"),
        MCP(MCPServerStdio(name="web", command="uvx", args=["mcp-server-fetch"])),
    ],
)
  • Todo() — gives the model a checklist for multi-step work; the current list is re-shown every turn without growing the transcript.
  • Skills(dir) — reusable instruction bundles (SKILL.md + files): a one-line index stays in the prompt, full content loads on demand.
  • MCP(server) — tools from Model Context Protocol servers, stdio or HTTP, with per-server name prefixes and approval gates.

Writing your own is a name plus one async setup() returning the contributions.

→ Plugins · Skills · MCP

Memory

Long-term memory across conversations — always-in-prompt Notes (facts the user stated or confirmed, never the assistant's own claims) plus a searchable Archive, zero-config, self-tidying (a periodic dream pass merges duplicates and prunes stale facts), upgradeable one argument at a time:

from lovia import Agent, Memory
from lovia.plugins import OpenAIEmbedder

agent = Agent(name="assistant", model="glm-5.2",
              plugins=[Memory("./.lovia/memory")])

Memory("./memory")                             # stdlib keyword search (FTS5 bm25)
Memory("./memory", embedder=OpenAIEmbedder())  # + semantic arm -> hybrid recall
Memory("./memory", index=None)                 # notes only, no archive

→ Memory

Web UI

A lightweight FastAPI app — SSE streaming, sessions with titles, follow-up suggestions, approvals, schedules, a memory editor, image & file attachments — whose runs survive browser disconnects:

from lovia.web import create_app, serve

serve(create_app(agent, db_path="lovia.db"), host="127.0.0.1", port=8000)
lovia web --port 9000                      # python -m lovia.web works too
lovia web --check                          # inspect the saved connection

The model connection (model name, Base URL, API key) is configured once, in the browser — Settings → Models: multiple profiles, a vision/aux role per task, extra request fields per endpoint (reasoning effort, thinking on/off, in the endpoint's own dialect), connection testing, live switching from the composer — and saved to ~/.lovia/config.json (owner-only, git-ignored). A project-local ./.lovia/config.json overrides it wholesale; changes apply live, no restart. Diagnose with lovia web --check; server and agent options stay flags/env vars.

The bundled page is optional: everything is exposed as a JSON + SSE REST API (browse it at /api/docs), so create_app(agent, ui=False) — or mounting the router into your own FastAPI app — lets you build a custom front-end on the same endpoints. Errors carry a stable code to branch on, in HTTP responses and stream error events alike. Keeping the page? ui=ChatUI(...) sets its blank-state copy, a login_url for your own auth=, and template overrides for your own head, sidebar, and scripts; it works under a path prefix — behind a proxy (--root-path) or mounted in your own app. A tool can return ui_result(kind, props) to render as UI, drawn by a renderer you load with ChatUI(scripts=[...]) (lovia.getRenderer lets one wrap another); put_ui_data keeps a large result out of the model's context, which reads a preview while the page draws it all. Three kinds come built in: tables — sort, filter, pages, copy / CSV (show_table, or ui_result("table", ...) straight from your data); charts (show_chart, Apache ECharts), with PNG / SVG / CSV download and full screen; and pages (show_html), a self-contained interactive page run in a sandbox with no network access. Tables and charts also draw in the chat's HTML export, and lovia web's default agent has all three tools. Your own resources — a DB pool your tools share — open and close with the app via create_app(lifespan=...).

→ Web UI · Web server · HTTP API

Evals

Behavioral test suites: a Case pairs input with checks, any function is a check, the LLM judge is just another check, and reports diff against a baseline in CI:

from lovia.eval import Case, contains, evaluate, llm_judge, tool_called

report = await evaluate(agent, [
    Case("What is the capital of France?", checks=[contains("Paris")]),
    Case("What's 23.4 * 91?", checks=[tool_called("calculator")]),
    Case("Write a haiku about spring",
         checks=[llm_judge("A 5-7-5 haiku that evokes spring")],
         samples=4, pass_threshold=0.75),
])
print(report)
assert report.passed

→ Evals

Testing

Everything runs offline against a scripted provider — real tools, real loop, canned model:

from lovia.testing import ScriptedProvider, call, text

provider = ScriptedProvider([
    call("add", {"a": 2, "b": 3}, call_id="c1"),
    text("The answer is 5."),
])

→ Testing

Examples

The examples/ directory is a numbered learning path of thirty self-contained, runnable scripts — from 01_hello.py to a terminal support bot — plus one script per built-in tool family (tools/) and the classic agentic patterns in plain Python (workflows/). Setup and the full index: examples/README.md.

Install extras

Need Install
Core framework pip install lovia
DuckDuckGo search pip install "lovia[ddg]"
Tavily search no extra — set TAVILY_API_KEY
MCP integration pip install "lovia[mcp]"
Web UI pip install "lovia[web]"
Runnable examples pip install "lovia[examples,web]"
Development pip install -e ".[dev]"

Development

pip install -e ".[dev]"
.venv/bin/python -m pytest
.venv/bin/python -m ruff check .
.venv/bin/python -m ruff format .
.venv/bin/python -m mypy lovia

Live provider tests are marked live_provider and stay skipped unless enabled explicitly. Contributor-facing internals are documented in docs/architecture.md.

Metadata

Release files for lovia 0.9.86

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lovia 0.9.86
File Size Uploaded
lovia-0.9.86.tar.gz 3.0 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for lovia 0.9.86
File Interpreter ABI Platform
lovia-0.9.86-py3-none-any.whl Python 3 none any Details

Total release size: 5.0 MB

Release files / lovia-0.9.86.tar.gz

Download URL lovia-0.9.86.tar.gz
Size 3.0 MB
Tags Source
SHA-256 checksum
How to use checksums
ebce6b87ed7cd9461d9bbb183a6f869e33d0dc087b69ecc2cad33f2bab18544e
BLAKE2b-256 checksum
How to use checksums
cd48652345b8bdd2ae292b77ee8d5cdc8521cb9733853b6f03f6e3abdf38d4f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / lovia-0.9.86-py3-none-any.whl

Download URL lovia-0.9.86-py3-none-any.whl
Size 2.0 MB
Tags Python 3
SHA-256 checksum
How to use checksums
a67b92efc90c58aa33fc53d30748db7ac812927ca9c0766a097150c799be0aa1
BLAKE2b-256 checksum
How to use checksums
548f6dcc95231f56cf5c8fa009f14d78e5836ef921d8fe9a483555b1ac881f19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.89

2 release files

0.9.88

2 release files

0.9.87

2 release files

This release

0.9.86 This release

2 release files

0.9.85

2 release files

0.9.84

2 release files

0.9.83

2 release files

0.9.82

2 release files

0.9.81

2 release files

0.9.80

2 release files

0.9.79

2 release files

0.9.78

2 release files

0.9.77

2 release files

0.9.76

2 release files

0.9.75

2 release files

0.9.74

2 release files

0.9.73

2 release files

0.9.72

2 release files

0.9.71

2 release files

0.9.70

2 release files

0.9.69

2 release files

0.9.68

2 release files

0.9.67

2 release files

0.9.66

2 release files

0.9.65

2 release files

0.9.64

2 release files

0.9.63

2 release files

0.9.62

2 release files

0.9.61

2 release files

0.9.60

2 release files

0.9.59

2 release files

0.9.58

2 release files

0.9.57

2 release files

0.9.56

2 release files

0.9.55

2 release files

0.9.54

2 release files

0.9.53

2 release files

0.9.52

2 release files

0.9.51

2 release files

0.9.50

2 release files

0.9.49

2 release files

0.9.48

2 release files

0.9.23

2 release files

0.9.22

2 release files

0.9.21

2 release files

0.9.20

2 release files

0.9.19

2 release files

0.9.17

2 release files

0.9.15

2 release files

0.9.13

2 release files

0.9.12

2 release files

0.9.11

2 release files

0.9.10

2 release files

0.9.8

2 release files

0.9.7

2 release files

0.9.6

2 release files

0.9.5

2 release files

0.9.4

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.36

2 release files

0.8.33

2 release files

0.8.32

2 release files

0.8.31

2 release files

0.8.30

2 release files

0.8.29

2 release files

0.8.27

2 release files

0.8.26

2 release files

0.8.25

2 release files

0.8.24

2 release files

0.8.23

2 release files

0.8.22

2 release files

0.8.21

2 release files

0.8.20

2 release files

0.8.19

2 release files

0.8.18

2 release files

0.8.17

2 release files

0.8.16

2 release files

0.8.15

2 release files

0.8.8

2 release files

0.8.7

2 release files

0.8.6

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.11

2 release files

0.7.10

2 release files

0.7.9

2 release files

0.7.8

2 release files

0.7.7

2 release files

0.7.6

2 release files

0.7.5

2 release files

0.7.4

2 release files

0.7.3

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.14

2 release files

0.6.13

2 release files

0.6.12

2 release files

0.6.11

2 release files

0.6.10

2 release files

0.6.9

2 release files

0.6.8

2 release files

0.6.7

1 release file

0.6.6

2 release files

0.6.5

2 release files

0.6.4

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.14

2 release files

0.5.9

2 release files

0.5.8

2 release files

0.5.7

2 release files

0.5.6

2 release files

0.5.5

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page