AgentDBDocs

Python and FastAPI

Build a small Python service that lists, searches and creates contacts. FastAPI serves the routes. httpx calls AgentDB. The key stays in the server process.

This page uses the python-fastapi starter. The smoke test runs app.py when FastAPI, uvicorn and httpx are installed. Otherwise it runs server_stdlib.py, which has the same routes and uses only the standard library.

#Where the key lives

In the environment of the server process. uvicorn --env-file .env loads it (with python-dotenv installed).

Pythonexamples/starters/python-fastapi/app.py
API = os.environ.get("AGENTDB_API_URL", "https://app.agentdb.link").rstrip("/")
WORKSPACE = os.environ["AGENTDB_WORKSPACE"]
TOKEN = os.environ["AGENTDB_TOKEN"]  # an agent key with role editor, never an owner key

#The AgentDB client

One httpx.AsyncClient for the whole app. It carries the key and the workspace URL.

Pythonexamples/starters/python-fastapi/app.py
@asynccontextmanager
async def lifespan(app: FastAPI):
    # One connection pool for the whole app.
    app.state.agentdb = httpx.AsyncClient(
        base_url=f"{API}/v1/workspaces/{WORKSPACE}",
        headers={"Authorization": f"Bearer {TOKEN}"},
        timeout=15,
    )
    yield
    await app.state.agentdb.aclose()


async def agentdb(request: Request, method: str, path: str, body=None, idempotency_key: str | None = None):
    headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
    try:
        res = await request.app.state.agentdb.request(method, path, json=body, headers=headers)
    except httpx.HTTPError as e:
        raise AgentDBError(0, None, "Could not reach AgentDB.") from e
    payload = res.json()
    if res.is_error:
        err = payload.get("error", {})
        raise AgentDBError(res.status_code, err.get("code"), err.get("message", f"AgentDB answered {res.status_code}"))
    return payload["data"]

Without FastAPI, urllib.request does the same job:

Pythonexamples/starters/python-fastapi/server_stdlib.py
def agentdb(method, path, body=None, idempotency_key=None):
    req = urllib.request.Request(
        f"{API}/v1/workspaces/{WORKSPACE}{path}",
        method=method,
        data=None if body is None else json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json",
                 **({"Idempotency-Key": idempotency_key} if idempotency_key else {})},
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as res:
            return json.load(res)["data"]
    except urllib.error.HTTPError as e:
        err = json.load(e).get("error", {})
        raise AgentDBError(e.code, err.get("code"), err.get("message", f"AgentDB answered {e.code}")) from None
    except urllib.error.URLError:
        raise AgentDBError(0, None, "Could not reach AgentDB.") from None

#The backend routes

Pythonexamples/starters/python-fastapi/app.py
@app.get("/api/contacts")
async def list_contacts(request: Request, q: str = ""):
    q = q.strip()[:100]
    data = await agentdb(request, "POST", "/query", {
        "object": "contact",
        "where": {"all": [{"field": "name", "op": "contains", "value": q}] if q else []},
        "sort": [{"field": "name", "order": "asc"}],
        "limit": 50,
    })
    return {"contacts": [contact_out(r) for r in data["records"]]}


@app.post("/api/contacts", status_code=201)
async def create_contact(request: Request):
    if not request.headers.get("content-type", "").startswith("application/json"):
        raise HTTPException(415, "Send JSON.")
    body = await request.json()
    if not isinstance(body, dict):
        raise HTTPException(400, "Send a JSON object.")
    name = str(body.get("name") or "").strip()[:200]
    email = str(body.get("email") or "").strip()[:254]
    if not name:
        raise HTTPException(400, "A contact needs a name.")
    submit = "".join(ch for ch in request.headers.get("x-submit-id", str(uuid.uuid4())) if ch.isalnum() or ch == "-")[:64]
    rec = await agentdb(request, "POST", "/records",
                        {"object": "contact", "data": {"name": name, **({"email": email} if email else {})}},
                        idempotency_key=f"fastapi.{submit}")
    return {"contact": contact_out(rec)}

The name check returns 400 with a clear message. The submit id becomes an Idempotency-Key, so a retried request is written once.

#The frontend

The page in static/ is the same plain HTML page as in Plain HTML and JavaScript. It calls /api/contacts on the same origin.

JavaScriptexamples/starters/python-fastapi/static/app.js
async function load() {
  status.textContent = "Loading…";
  try {
    const res = await fetch("/api/contacts?q=" + encodeURIComponent(q.value.trim()));
    const body = await res.json();
    if (!res.ok) throw new Error(body.error ?? "Could not load contacts.");
    // Set text, never HTML: names come from your data and may contain anything.
    list.replaceChildren(
      ...body.contacts.map((c) => {
        const li = document.createElement("li");
        li.textContent = c.email ? `${c.name} (${c.email})` : c.name;
        return li;
      }),
    );
    status.textContent = body.contacts.length ? "" : q.value ? "No contacts match your search." : "No contacts yet. Add the first one below.";
  } catch (e) {
    list.replaceChildren();
    status.textContent = `Error: ${e.message}`;
  }
}

#Loading, empty and error states

The service turns AgentDB errors into short messages. Validation errors are shown. Anything else is logged, and the page gets a general message.

Pythonexamples/starters/python-fastapi/app.py
@app.exception_handler(AgentDBError)
async def agentdb_error(_request: Request, e: AgentDBError):
    if e.code == "VERSION_CONFLICT":
        return JSONResponse({"error": "Someone changed it first. Reload and try again."}, status_code=409)
    if e.status in (400, 422):
        return JSONResponse({"error": e.message}, status_code=400)  # validation: safe to show
    print(f"AgentDB call failed: {e.status} {e.code} {e.message}", flush=True)
    return JSONResponse({"error": "The data service is not available. Try again in a moment."}, status_code=502)


@app.exception_handler(HTTPException)
async def http_error(_request: Request, e: HTTPException):
    return JSONResponse({"error": e.detail}, status_code=e.status_code)

#Live updates

FastAPI can stream server-sent events with a StreamingResponse and media_type="text/event-stream". Run one background task that follows the change feed (GET /changes?after=<seq>&wait=1) and fans out to all open streams, like the live dashboard starter. See Live data.

#Run it

Shell
cd examples/starters/python-fastapi
cp .env.example .env    # then fill in your workspace and agent key
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt python-dotenv
uvicorn app:app --env-file .env --port 8000

Or, with no packages: set -a; . ./.env; set +a; python3 server_stdlib.py.