Getting started
Install
Requires Python 3.11+. On PyPI; the core install is deliberately slim — engine,
tools, ask/Agent, and in-memory persistence — and everything else is an extra:
pip install fg-agents # core (engine + tools + in-memory persistence)
pip install "fg-agents[anthropic]" # + Anthropic provider
pip install "fg-agents[web]" # + FastAPI app/router (create_app)
| Extra | Adds | You need it for |
|---|---|---|
| (none) | — | ask/Agent, tools, engine, in-memory persistence |
web |
fastapi, uvicorn | create_app, create_agent_router, the HTTP/SSE API |
postgres |
sqlalchemy, asyncpg | PostgresRepository, memory="postgres:<url>" |
sqlite |
aiosqlite | SQLiteRepository, memory="sqlite" |
anthropic / openai / google |
provider SDK | that provider (openai also covers Ollama and every other OpenAI-compatible provider) |
templates |
jinja2 | Jinja2 prompt templating |
scheduler |
croniter, sqlalchemy | AgentScheduler |
all |
everything above | — |
Imports guarded by optional dependencies (SQLiteRepository, PostgresRepository,
AgentScheduler, the web/API symbols, …) resolve to None or raise a clear error
until the matching extra is installed.
API keys
Keys are resolved in two ways, in order:
- An explicit override:
AgentLLM(api_keys={"openai": "sk-...", ...}) - Environment variables named
{PROVIDER}_API_KEY— e.g.OPENAI_API_KEY,ANTHROPIC_API_KEY.
Local providers (Ollama, LM Studio, vLLM, …) need no key. One environment gate matters:
the CODE tool type (one of the typed tools — see the API reference)
is disabled unless FG_ALLOW_CODE_EXECUTION=true is set — and even then it is
not a security sandbox.
Hello world — ask()
The fastest way in is no object at all. With one provider key in the environment (and its extra installed), three lines is the whole program — the model is detected for you:
import asyncio
from fg_agents import ask
print(asyncio.run(ask("What's 2+2?")))
More usefully, inside your own async code — tools and a system prompt work here
too, still with no object to manage (greet is the plain function defined in the
next section):
print(await ask("What's 2+2?"))
print(await ask("Please greet Ada.", tools=[greet],
system_prompt="You are a friendly greeter."))
Detection order when no model= is given: ANTHROPIC_API_KEY →
anthropic:claude-sonnet-4-6, OPENAI_API_KEY → openai:gpt-5.2,
GOOGLE_API_KEY/GEMINI_API_KEY → google:gemini-2.5-flash, then a probe for a
local Ollama server → ollama:qwen3:8b. Nothing found → a ModelDetectionError
that tells you exactly what to set. An explicit model= always wins.
ask() returns an AgentRunResult whose str() is the answer text — hence
print(await ask(...)). Its streaming sibling is the free function stream(),
which yields StreamEvents; if you might exit the loop early, wrap it in
contextlib.aclosing(...) so cleanup is deterministic (pattern in the
API reference).
Under the hood each call builds an ephemeral Agent, runs, and cleans up. The
usage ladder is ask() → Agent → engine: graduate one rung whenever you
need more.
Hello agent — the Agent facade
One object, no wiring — the facade sets up the LLM client, tool registry,
persistence, and engine for you, and adds what ask() can't give you: a
conversation. Agent() also auto-detects the model when you omit it, and works
as an async context manager (this is the repo's examples/00_quickstart.py):
async with Agent(tools=[greet], system_prompt="You are a friendly greeter.") as agent:
print(await agent.run("Greet Grace."))
print(await agent.run("Who did you just greet?")) # same conversation
Prefer an explicit model? Pass one — as written below it needs the [anthropic]
extra installed and ANTHROPIC_API_KEY set, or swap in a keyless local one like
"ollama:qwen3:8b":
import asyncio
from fg_agents import Agent
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}! Welcome to Fareground."
async def main():
agent = Agent(
model="anthropic:claude-sonnet-4-6", # any provider:model you have a key for
tools=[greet],
system_prompt="You are a friendly greeter.",
)
result = await agent.run("Please greet Ada.")
print(result.text)
asyncio.run(main())
What the facade gives you:
tools=[...]accepts@tool-decorated functions, plain callables (auto-wrapped — schema from the signature, description from the docstring), orRegisteredToolinstances. Duplicate tool names raiseValueErrorat construction.memory=picks persistence:"memory"(default),"sqlite","sqlite:<path>","postgres:<url>", or a readyBaseRepository. Initialization is lazy — the repository is initialized on the firstrun()/stream()call, so forgetting to initialize is never an error.await agent.run(...)returns anAgentRunResult(text,session_id,events); any failure raisesAgentRunErrorcarrying thesession_idand the partial events.agent.stream(...)yields rawStreamEvents instead.- Every run continues the instance's conversation unless you pass a per-call
session_id.await agent.close()closes the repository (theasync withform does it for you); the Agent is unusable afterwards. - Graduate to the low-level tier at any point via
agent.engine,agent.llm,agent.tools,agent.repository,agent.definition.
The same agent, wired by hand
The four objects the facade collapses — worth knowing when you need middleware, skills, or a shared registry:
import asyncio
from fg_agents import (
AgentDefinition, AgentEngine, AgentLLM, ToolRegistry, create_repository, tool,
)
@tool(description="Greet someone by name")
def greet(name: str) -> str:
return f"Hello, {name}! Welcome to Fareground."
async def main():
llm = AgentLLM()
repo = create_repository("memory") # or "sqlite", "postgres"
await repo.initialize()
tools = ToolRegistry()
tools.register_function(greet)
agent_def = AgentDefinition(
name="greeter",
model="ollama:qwen3:8b", # required — there is no default
system_prompt="You are a friendly greeter. Use the greet tool when asked.",
tools=["greet"],
)
engine = AgentEngine(llm=llm, tool_registry=tools, repository=repo)
async for event in engine.run("session-1", "Say hello to Alice", agent_def):
if event.data.get("text"):
print(event.data["text"], end="", flush=True)
elif event.type.value == "tool.result":
print(f"\n [tool] {event.data['tool_name']}: {event.data['status']}")
asyncio.run(main())
What each piece does:
@toolturns a plain function into a registered tool. The JSON schema is derived from the signature; sync functions run in a thread; an optionalctx: ExecutionContextparameter is auto-injected and excluded from the schema.create_repository("memory")picks the persistence backend."sqlite"and"postgres"are drop-in swaps — the engine only speaksBaseRepository.AgentDefinitionis declarative config.modelhas no default and must be"provider:model";toolsnames must exist in the registry.engine.run(...)is the ReAct loop: it persists the user message, calls the LLM, executes tool calls, and yields typedStreamEvents until the agent finishes.
A full web backend in one call
create_app() wires the whole stack — orchestrator, repository, router, SSE — into a
FastAPI application. It lives behind the web extra
(pip install "fg-agents[web]"); a PostgreSQL db_url additionally needs
[postgres]:
from fg_agents import AgentDefinition, ToolRegistry, create_app, tool
@tool(description="Search the knowledge base")
async def search(query: str) -> str: ...
assistant = AgentDefinition(
name="assistant",
model="anthropic:claude-sonnet-4-6",
system_prompt="You are the product assistant.",
tools=["search"],
)
tools = ToolRegistry()
tools.register_function(search)
app = create_app(
agents={"assistant": assistant},
tool_registry=tools,
# db_url="postgresql+asyncpg://user:pass@localhost:5432/mydb", omitting db_url falls back to a SQLite file (fg_agents.db), not in-memory
)
# run with: uvicorn myapp:app
The HTTP surface (default prefix /api/agent):
| Method | Path | Purpose |
|---|---|---|
POST |
/sessions |
Create a session → { session_id } |
GET |
/sessions/{id} |
Session status and metadata |
DELETE |
/sessions/{id} |
Delete a session |
POST |
/sessions/{id}/messages |
Send a message → SSE stream (or JSON with stream=false) |
POST |
/sessions/{id}/cancel |
Nuclear stop (cascades to sub-agents) |
POST |
/sessions/{id}/resume |
Resume a waiting_input / failed session |
GET |
/sessions/{id}/stream |
Re-attach to an in-flight run (Last-Event-ID replay) |
GET |
/sessions/{id}/messages |
Paginated history (limit / offset) |
GET |
/sessions/{id}/artifacts |
Session artifacts |
GET |
/sessions/{id}/audit |
Audit log |
GET / POST |
/tools |
List tools / register tool (POST admin-gated) |
GET / POST |
/agents |
List agents / register agent (POST admin-gated) |
GET |
/memory/{agent_id} |
Agent persistent memory |
PUT |
/memory/{agent_id}/{key} |
Set a memory key |
GET |
/health |
Health check |
Two failure modes are deliberately loud and early: omitting db_url (the SQLite-file
fallback) or passing a sqlite URL fails at create_app() call time — not at app
startup — when aiosqlite isn't installed, telling you to pip install
"fg-agents[sqlite]" or pass db_url="memory"; and an unrecognized db_url raises
ValueError (use a postgres/sqlite URL, "memory", or omit it).
Runs are owned by a process-wide run registry and detached from the HTTP request — a
client disconnect does not cancel the agent; the browser re-attaches via
GET /sessions/{id}/stream.
Without an authenticator, the app runs as a single shared admin tenant and logs a loud
one-time warning — fine for local development, never for production. See the
multi-tenant security model.