docs / agent-framework / getting startedgithub.com/Fareground/agent-framework ↗

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:

  1. An explicit override: AgentLLM(api_keys={"openai": "sk-...", ...})
  2. 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_KEYanthropic:claude-sonnet-4-6, OPENAI_API_KEYopenai:gpt-5.2, GOOGLE_API_KEY/GEMINI_API_KEYgoogle: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:

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:

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.

Where to go next