Skip to content

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)

git clone https://github.com/graffold/graffold-api.git
cd graffold-api
pip install uv && uv sync

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:

graffold

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:

docker compose --profile full up -d

A2. Health check (1 min)

export API_AUTH_TOKEN=dev-token-123      # same value you put in .env
graffold health

Expected:

✓ API        status=ok  version=0.2.0  uptime=...
✓ database   ok
✓ llm        ok
✓ Liveness   alive

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 iat401 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):

etec    → found 2/3 (FedF + Mucin), coverage 0.667
alltech → found 1/3 (Mucin only),   coverage 0.333
# 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 querysession 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