API Test-Drive Runbook¶
A hands-on, ~30-minute path from zero to querying a real knowledge graph over the
API — via the graffold CLI and raw curl. Run it top to bottom.
Two paths, pick one
- Path A — Local stack: you run the API on your machine. Simplest auth (a static token). Best for learning the surface.
- Path B — Live API (Hetzner): hit the deployed
api.graffold.com. Uses JWT auth (no static token in prod). Best for querying the real graphs (etec,alltech, …).
Start with Path A to learn the shape, then do Path B to touch real data.
0. Prerequisites (2 min)¶
The CLI is graffold (entry point) or uv run python -m cli.cli. It reads
config from .env and these env vars:
| Variable | Purpose |
|---|---|
GRAFFOLD_API_URL |
API base URL (default http://localhost:8000) |
API_AUTH_TOKEN |
Bearer token the CLI sends (local/legacy auth) |
Just run it bare to see the banner + live config detection:
Path A — Local stack¶
A1. Start the API (3 min)¶
# Minimal: API + in-memory session store, no external DB required for a smoke test
cp .env.example .env
# edit .env: set API_AUTH_TOKEN=dev-token-123 (any string; the CLI will send it)
graffold serve # → http://localhost:8000
# or: uv run granian --interface asgi api.app:app --host 127.0.0.1 --port 8000
For a full local stack (Neo4j + Redis + API) use Docker:
A2. Health check (1 min)¶
Expected:
If API fails → the server isn't up. If database fails → no graph backend
(fine for a pure API smoke test; queries just return empty).
A3. Explore the API surface (2 min)¶
# What models / databases / search modes does this instance expose?
curl -s -H "Authorization: Bearer $API_AUTH_TOKEN" \
http://localhost:8000/v1/config/options | jq
# Full interactive docs in a browser:
open http://localhost:8000/docs # Swagger UI
open http://localhost:8000/redoc # ReDoc
A4. Create a session, then query (5 min)¶
The CLI's query command needs a session ID. There's no CLI command to
create one yet, so create it with curl, then query with the CLI:
# 1. Create a session (all of llm_service, database_name, database_type required)
SESSION=$(curl -s -X POST http://localhost:8000/v1/sessions \
-H "Authorization: Bearer $API_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"llm_service":"bedrock","database_name":"neo4j","database_type":"neo4j","agent_type":"Agentic"}' \
| jq -r .session_id)
echo "session = $SESSION"
agent_type
Agentic selects the TwoPhaseAgent (deterministic tools → LLM synthesis).
Standard (the default) uses GraphQueryAgent (NL→Cypher). Use Agentic
to exercise the query brain this runbook describes.
# 2. Query it (streams token-by-token by default)
graffold query "What proteins are associated with heart disease?" -s "$SESSION"
# Non-streaming, pick a mode + depth:
graffold query "Overview of IL-6" -s "$SESSION" --mode hybrid --depth deep --no-stream
--depth maps to the query brain's aggressiveness:
| depth | behavior |
|---|---|
fast |
discovery only (no graph expansion) |
balanced |
discovery + 1 expansion round (default) |
deep |
full iterative expansion + Cypher fallback |
A5. Benchmark (2 min)¶
graffold benchmark -s "$SESSION" # built-in queries
graffold benchmark -s "$SESSION" -n 10 -o results.json # custom iterations, save JSON
Path B — Live API (Hetzner / api.graffold.com)¶
The deployed API is JWT-authenticated — there is no static API_AUTH_TOKEN
in prod. You mint a short-lived JWT signed with the prod JWT_SECRET, then send
it as a bearer token. TLS is terminated by Cloudflare, so just use https://.
B1. Mint a JWT (2 min)¶
Ask an admin for a token, or mint one on the box (requires SSH access):
# On the Hetzner box — mint a 1-hour enterprise-tier token
ssh hetzner
docker exec prod-fastapi /app/.venv/bin/python - <<'PY'
import os, jwt, time
now = int(time.time())
claims = {"sub": "onboarding", "tier": "enterprise",
"iat": now, "exp": now + 3600, "allowed_connectors": []}
print(jwt.encode(claims, os.environ["JWT_SECRET"], algorithm="HS256"))
PY
Copy the printed token and, back on your machine:
export GRAFFOLD_API_URL=https://api.graffold.com
export API_AUTH_TOKEN=<paste-jwt-here> # the CLI sends whatever's here as Bearer
JWT claims matter
The token must include sub, tier, iat, and exp. Missing iat
→ 401 Invalid token. A free tier → 403 on /v1/atlas/* and /v1/kg/*
(those require a paid tier via require_api_access).
B2. Confirm auth + list the real graphs (2 min)¶
# Health (no auth needed)
curl -s https://api.graffold.com/health | jq
# What graphs are actually served? (dynamic discovery)
curl -s -H "Authorization: Bearer $API_AUTH_TOKEN" \
https://api.graffold.com/v1/config/options | jq '.databases[] | {name, display_name}'
You should see the FalkorDB named graphs: etec, alltech, elanco,
zoetis, master.
B3. Query a real graph via the Atlas endpoints (5 min)¶
These are the endpoints the Atlas pipeline uses. kg_id selects the graph.
# Coverage check — how many of these entities exist in the etec graph?
curl -s -H "Authorization: Bearer $API_AUTH_TOKEN" \
"https://api.graffold.com/v1/atlas/entity-coverage?entities=FedF,fimbriae,mucin&kg_id=etec" | jq
# Same query, different graph — proves isolation:
curl -s -H "Authorization: Bearer $API_AUTH_TOKEN" \
"https://api.graffold.com/v1/atlas/entity-coverage?entities=FedF,fimbriae,mucin&kg_id=alltech" | jq
Expected contrast (this is the isolation proof):
# Evidence for a specific claim
curl -s -H "Authorization: Bearer $API_AUTH_TOKEN" \
"https://api.graffold.com/v1/atlas/evidence-for-claim?subject=FedF&predicate=BINDS&object=mucin&kg_id=etec" | jq
B4. Natural-language query against a real graph (3 min)¶
# Create a session bound to the etec graph
SESSION=$(curl -s -X POST https://api.graffold.com/v1/sessions \
-H "Authorization: Bearer $API_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"llm_service":"bedrock","database_name":"etec","database_type":"falkordb","agent_type":"Agentic"}' \
| jq -r .session_id)
graffold query "What adhesins does ETEC use to colonize the piglet gut?" -s "$SESSION" --depth balanced
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
401 Missing bearer token |
No API_AUTH_TOKEN set |
export API_AUTH_TOKEN=... |
401 Invalid token (prod) |
JWT missing iat/exp, or bad secret |
Re-mint with all claims (B1) |
403 Upgrade to Pro… |
JWT tier is free/demo |
Mint with "tier":"enterprise" |
graffold query → session not found |
Session expired or wrong ID | Re-create the session (A4/B4) |
database health fails |
No graph backend reachable | OK for API smoke test; queries return empty |
| Empty query results | Graph has no matching data, or wrong database_name |
Check /v1/config/options for valid graph names |
What to try next¶
- API Explorer (
development/api-explorer.md) — every endpoint, live Redoc. - SDK Reference (
development/sdk-reference.md) — the Python client if you'd rather script than curl. - The query brain — read
src/agents/two_phase_agent.py: tools run deterministically first (no LLM in the retrieval loop), LLM only synthesizes and fact-checks at the end.
Quick reference — CLI commands¶
| Command | What it does |
|---|---|
graffold |
Banner + live config detection |
graffold health |
API / DB / LLM / liveness status |
graffold query "<q>" -s <session> |
NL query (streams; --mode, --depth, --no-stream) |
graffold benchmark -s <session> |
Latency benchmark (avg/p50/p95/p99) |
graffold serve |
Start the API locally |
graffold ingest -q "<q>" --source pubmed |
Submit an ingestion job |
graffold pipeline … |
Full KG creation pipeline |
graffold enrich -f data.csv -db <db> |
CSV/Excel enrichment |