A lightweight and elegant Agent framework
Project description
lovia
A lightweight, provider-neutral agent framework for Python.
import asyncio
from lovia import Agent, Runner
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
model="openai:gpt-4o-mini",
)
result = asyncio.run(Runner.run(agent, "What is the capital of France?"))
print(result.output) # Paris
Two hard dependencies (httpx, pydantic). No DSL, no graph, no global state.
Every advanced feature — tools, sessions, handoffs, structured output, MCP, streaming — is opt-in.
Install
pip install lovia
Optional extras:
pip install "lovia[mcp]" # Model Context Protocol client
pip install "lovia[tools]" # web_search with DuckDuckGo backend
pip install "lovia[web]" # FastAPI + SSE chat server
Tools
Any typed Python function becomes a tool with @tool. Sync and async both work.
from lovia import Agent, Runner, tool
@tool
def calculate(expression: str) -> float:
"""Evaluate a simple math expression."""
return eval(expression, {"__builtins__": {}})
agent = Agent(
name="Calc",
instructions="Use calculate() for arithmetic.",
model="openai:gpt-4o-mini",
tools=[calculate],
)
result = asyncio.run(Runner.run(agent, "What is 1337 * 42?"))
Use Annotated to add per-parameter descriptions to the JSON schema:
from typing import Annotated
@tool
def search(
query: Annotated[str, "Keywords to search for."],
limit: Annotated[int, "Max results, 1-20."] = 5,
) -> list[str]: ...
Simple execution policies stay as decorator kwargs:
@tool(timeout=5, retries=2, needs_approval=True)
async def send_email(to: str, body: str) -> str: ...
For advanced cases, pass composable policies; simple kwargs still work.
from lovia import RunContext
async def redact(next_tool, args, ctx):
result = await next_tool(args, ctx)
return str(result).replace(ctx.context.api_key, "[redacted]")
@tool(policies=[redact])
async def call_api(ctx: RunContext, path: str) -> str: ...
Structured output
Pass any Pydantic model as output_type and the result is validated automatically.
output_repair=True lets the model self-correct if the first parse fails.
from pydantic import BaseModel
from lovia import Agent, Runner
class Review(BaseModel):
rating: int # 1-5
summary: str
pros: list[str]
cons: list[str]
agent = Agent(
name="Reviewer",
instructions="Extract a structured review from the user text.",
model="openai:gpt-4o-mini",
output_type=Review,
output_repair=True,
)
result = asyncio.run(Runner.run(agent, "The battery lasts all day but the screen is dim."))
print(result.output.rating) # -> int
Override output_type for a single call without touching the agent:
result = await Runner.run(agent, "Summarize in plain text.", output_type=str)
Streaming
async for event in Runner.stream(agent, "Tell me a joke"):
print(event)
Or directly from the agent instance:
async for event in agent.stream("Tell me a joke"):
print(event)
Dynamic instructions
Inject context-aware content at runtime with @agent.system_prompt.
Multiple fragments compose with the base instructions.
agent = Agent(name="Support", instructions="You are a support bot.", model="openai:gpt-4o-mini")
@agent.system_prompt
async def inject_user(ctx) -> str:
user = await db.get_user(ctx.context.user_id)
return f"The user's name is {user.name}. Their plan is {user.plan}."
# Append one-off context at call time:
result = await Runner.run(agent, "I need help.", append_instructions="Reply in Spanish.")
Prefer functional configuration when cloning reusable agents:
agent = agent.with_system_prompt(inject_user)
Handoffs
An agent can delegate to another agent mid-conversation. The Runner follows the chain automatically.
billing = Agent(name="Billing", instructions="Handle billing questions.", model="openai:gpt-4o-mini")
support = Agent(name="Support", instructions="Answer support questions. Hand off billing questions.", model="openai:gpt-4o-mini", handoffs=[billing])
result = await Runner.run(support, "Can I get a refund?")
Sessions
Persist conversation history across calls with a session= argument.
The default in-memory store is a good starting point; swap in Redis or SQL as needed.
from lovia.stores import InMemorySessionStore
session_store = InMemorySessionStore()
result1 = await Runner.run(agent, "My name is Alice.", session=session_store.session("u42"))
result2 = await Runner.run(agent, "What is my name?", session=session_store.session("u42"))
# → "Your name is Alice."
Approval (human in the loop)
Mark sensitive tools with needs_approval=True to require human sign-off.
from lovia import ApprovalChannel
channel = ApprovalChannel()
@tool(needs_approval=True)
def send_email(to: str, body: str) -> str:
...
# In your UI, call channel.approve(request_id) or channel.deny(request_id, reason)
result = await Runner.run(agent, "Send a welcome email to alice@example.com", approval_channel=channel)
Sync helpers
Runner.run_sync and agent.run_sync are convenience wrappers around
asyncio.run. Use them in scripts or wherever you can't await.
result = Runner.run_sync(agent, "What is 2+2?")
print(result.output)
Built-in tools
lovia.builtins ships practical, framework-agnostic tools you can drop straight into any agent.
Nothing is imported automatically — grab only what you need.
from lovia.builtins.http import http_fetch
from lovia.builtins.search import duckduckgo_search_tool
from lovia.builtins.todo import TodoList, todo_tools
from lovia.builtins.human import HumanChannel, ask_human
from lovia.builtins.think import think
from lovia.builtins.time import now
todos = TodoList()
channel = HumanChannel()
agent = Agent(
name="Worker",
instructions="Plan, reason, act.",
model="openai:gpt-4o-mini",
tools=[
http_fetch, now, think,
duckduckgo_search_tool(), # requires lovia[tools]
*todo_tools(todos),
ask_human(channel),
],
)
Builtin convention: stateless helpers export ready-to-use Tool instances,
pluggable backends use factories, stateful single-tool helpers expose .tool(),
and stateful multi-tool helpers expose .tools().
Filesystem and shell tools live in lovia.sandbox (see next section) —
they need a proper sandbox abstraction, not a thin one-off helper.
Runnable demos live in examples/builtins/.
Sandbox
lovia.sandbox is the concise filesystem + process layer. One
Protocol (Sandbox), one pool (SandboxProvider), one wiring call
(attach_sandbox). Ships with a default in-process backend
(LocalSandbox); swap in Docker / Firecracker by implementing the
Protocol — agent code stays the same.
from lovia import (
Agent, Runner,
LocalSandboxProvider, attach_sandbox, AuditStream,
)
from lovia.stores import InMemorySession
base = Agent(name="coder", instructions="…", model="openai:gpt-4o-mini")
async with LocalSandboxProvider() as provider:
audit = AuditStream() # optional pub/sub for a UI
agent = attach_sandbox(base, provider, audit_stream=audit)
session = InMemorySession() # from lovia.stores
await Runner.run(agent, "Create app.py and run it.", session=session, session_id="s1")
# Same session_id → same workspace on the next turn.
await Runner.run(agent, "Now add tests.", session=session, session_id="s1")
What you get for free:
- Path traversal guard — symlink-aware, blocks
..,/etc/..., etc. - Dependency isolation by PATH/HOME — each sandbox redirects
HOMEandTMPDIRto a private subdir and prepends<root>/.venv/bintoPATH. The framework does not manage that venv. When the LLM needs Python deps it bootstraps one itself:python -m venv .venv && .venv/bin/pip install pandas
From the next command onwardspythonandpipresolve to the venv automatically — no special API, no auto-bootstrap, zero pollution of the host environment. - Audit policy —
default_audit_policy()blocks the obvious foot-guns (rm -rf /,mkfs,curl|sh, fork bombs, …) and warns on barepip install/npm install -gso the LLM is nudged toward a venv. Three-valued (pass/warn/block): warnings annotate stderr without blocking, giving the model a chance to self-correct. - Per-session lifecycle — sandboxes are refcounted by
session_id; multi-turn runs reuse the same workspace (including any.venvthe model created) until the provider shuts down. - Hidden-file filtering —
ls/globskip dotfiles by default so**/*.pydoesn't drown in the LLM's own.venv/. Passinclude_hidden=Trueto look. - Live audit stream — subscribe via
AuditStream.subscribe()for a UI; history is kept for late-joining subscribers. - Apply-patch tool — tolerant unified-diff editor on top of read+write, the cheapest way to let a model edit files.
LocalSandbox is not a security boundary — HOME/PATH redirection
keeps things tidy, not safe. For untrusted code use a container-backed
Sandbox implementation.
Runnable demos: examples/22_sandbox.py,
examples/23_sandbox_session.py,
examples/24_custom_sandbox.py.
Web UI
lovia.web ships a small FastAPI app + bundled vanilla-JS chat UI. It's
the same wiring you'd build yourself, but pre-assembled so you can ship a
demo in three lines:
from lovia import Agent
from lovia.web import serve
agent = Agent(name="assistant", instructions="…", model="openai:gpt-4o-mini")
serve(agent, db_path="lovia.db") # http://127.0.0.1:8000
What you get out of the box:
- Sidebar of chats — every session lives in SQLite (
db_path), so it survives restarts, can be renamed, deleted, switched. - Auto-generated titles — after the first turn a tiny background call asks the same model for a 3-6 word headline.
- Streaming transcript with tool-call cards and approval prompts.
- Workspace panel + audit feed — pass
sandbox_provider=andaudit_stream=and the right-hand panel shows the per-session files and every shell command's pass/warn/block verdict in real time.
from lovia import LocalSandboxProvider, AuditStream, attach_sandbox
provider = LocalSandboxProvider()
audit = AuditStream()
agent = attach_sandbox(base_agent, provider, audit_stream=audit)
serve(agent, db_path="lovia.db", sandbox_provider=provider, audit_stream=audit)
See examples/25_web_sandbox.py for the
full wiring.
Skills
Skills are Markdown-driven instruction packs stored in a directory tree. They let you compose domain knowledge without bloating the system prompt.
skills/
translation/
SKILL.md # name, description, usage instructions
references/ # reference files the agent can read
from lovia.skills import SkillCatalog
catalog = SkillCatalog.from_dir("./skills") # lazy by default
agent = Agent(
name="Expert",
instructions=catalog.render_catalog(),
model="openai:gpt-4o-mini",
tools=catalog.tools(),
)
In lazy mode the catalog renders as a compact index; the model calls
load_skill to pull in a full skill body on demand. Switch to
mode="eager" to inline all bodies up front.
Multiple providers
The model= field accepts any "provider:model" string or a Provider instance.
# OpenAI
agent = Agent(model="openai:gpt-4o-mini", ...)
# Anthropic
agent = Agent(model="anthropic:claude-3-5-haiku-20241022", ...)
# Any OpenAI-compatible endpoint
from lovia import OpenAIChatProvider
provider = OpenAIChatProvider(model="deepseek-chat", base_url="https://api.deepseek.com/v1", api_key="...")
agent = Agent(model=provider, ...)
Examples
examples/
01_hello.py Minimal agent
02_tools.py Tool calling
03_streaming.py Streaming tokens
04_structured_output.py Pydantic output
05_handoff.py Agent-to-agent delegation
06_agent_as_tool.py Sub-agent as a tool
07_session.py Persistent sessions
08_skills.py SkillCatalog
09_compat_provider.py Custom OpenAI-compatible provider
10_hooks.py Lifecycle hooks / tracing
11_approval.py Human-in-the-loop approval
12_multimodal.py Image input
13_budget_and_cancel.py Token budget & cancellation
14_guardrails.py Input/output guards
15_resume.py Resume interrupted runs
16_web_serve.py FastAPI + SSE server
17_responses_reasoning.py OpenAI Responses API + reasoning
18_context_policy.py Auto-summarize long history
19_dynamic_instructions.py Dynamic system prompt
20_builtins.py Several builtins together
21_dx.py Annotated schemas, run_sync
22_sandbox.py Per-run LocalSandbox + sandbox_tools
23_sandbox_session.py Multi-turn with LocalSandboxProvider
24_custom_sandbox.py Implement Sandbox to plug Docker / firecracker
25_web_sandbox.py Full web stack: persistent + sandbox + UI
builtins/ One focused demo per builtin
workflows/ Multi-agent workflow patterns
Development
git clone https://github.com/cymoo/lovia
pip install -e ".[dev]"
pytest # run tests
ruff check . # lint
mypy lovia # type-check
See AGENTS.md for architecture notes, design philosophy,
and commit conventions.
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lovia-0.4.0.tar.gz.
File metadata
- Download URL: lovia-0.4.0.tar.gz
- Upload date:
- Size: 301.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74b03de768cf67507fb71ca7cb2071738d1747e356764c74e79f46988d0afe13
|
|
| MD5 |
cc4569bde70aab258fd831596fe8239a
|
|
| BLAKE2b-256 |
ec78bfe1b8f0ff23bb6a3ed39eadb77035c0a7853221064af06b5bccf73e4709
|
File details
Details for the file lovia-0.4.0-py3-none-any.whl.
File metadata
- Download URL: lovia-0.4.0-py3-none-any.whl
- Upload date:
- Size: 149.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a1cef76a5d0404f6736c1f7e7091758f50dfbedb233cf286e53735d64b03cc9
|
|
| MD5 |
8d32b07602ff9aa92975e38c188cad02
|
|
| BLAKE2b-256 |
09e9dbf0df2b882969c1f0d6d7bfc66cdc9f82e6073023ae3f84e762b78b4ef9
|