The Experience API sits between the OpenAgriNet web client and the Decision Support System (DSS). The client sends a chat turn; the API builds a DSS request, calls the DSS, and streams the answer back.
Needs uv. It installs Python 3.13 itself.
uv sync
uv run uvicorn --factory experience_api.app:create_app --port 8078The API listens on http://localhost:8078.
| Path | What |
|---|---|
POST /v1/chat |
One chat turn, contract §4–§6 |
GET /healthz |
{"status": "ok"} when the process is up. For Docker and the proxy |
GET /docs |
The interactive OpenAPI page |
By default a fake DSS answers every turn, so the API runs with nothing else installed. To call a real one:
EXPERIENCE_API_DSS_MODE=http uv run uvicorn --factory experience_api.app:create_app --port 8078| Variable | Default | What |
|---|---|---|
EXPERIENCE_API_DSS_MODE |
fake |
fake, or http to call the DSS |
EXPERIENCE_API_DSS_BASE_URL |
http://localhost:8077 |
Where the DSS listens |
EXPERIENCE_API_CHANNEL |
web |
Sent to the DSS as the channel |
EXPERIENCE_API_MAX_CHARACTERS |
1200 |
The longest answer to ask the DSS for |
A bad mode, channel or length stops the API from starting; a wrong DSS URL
fails the first turn, like a DSS that is down. Errors from the DSS itself (it being
down, busy, or refusing the request) come back as a plain 500 for now.
curl -i http://localhost:8078/healthzAdd --reload to restart on every file change while developing.
docker compose up --build # add -d to run it in the background
docker compose downSame port, same fake DSS. EXPERIENCE_API_DSS_MODE=http docker compose up --build calls a DSS on the host's port 8077; the other variables pass through
too. Set EXPERIENCE_API_HOST_PORT to publish on another port. The container
reports healthy once /healthz answers.
CI publishes an image on every push to main and every release tag, once
every check passes. It can also be run by hand from the Actions tab, for any
branch; only main moves latest.
docker pull ghcr.io/openagrinet/experience-api:latest # or :main, :v1.2.0, :<commit>cat > request.json <<'JSON'
{
"sessionId": "68a3872f-3f0d-4cf6-99a3-a350132a0080",
"messageId": "1ab38d6c-6fdb-4849-8ea1-da5e80a8687c",
"query": "And what about tomorrow?",
"history": [],
"language": { "source": "en", "target": "en" }
}
JSON
# Streamed: started, delta..., completed
curl -N -H 'Accept: text/event-stream' -H 'Content-Type: application/json' \
-d @request.json http://localhost:8078/v1/chat
# One JSON answer
curl -H 'Content-Type: application/json' -d @request.json http://localhost:8078/v1/chatA body that breaks the contract's rules gets FastAPI's default 422 for now
(contract §6.1).
uv run ruff check . && uv run ruff format --check . # lint, format
uv run pyright # types
uv run vulture # dead code
uv run pytest # tests, with coverageOnce per clone, install the git hooks. Lint, types and dead code run on every
commit; the test suite runs before every push. CI runs the same checks, so
--no-verify only moves a failure later.
uv run pre-commit install --hook-type pre-commit --hook-type pre-pushdocs/api-contracts/api-contract.md: the contract the web client builds againstdocs/ADR/: accepted decisions; ADR-0001 is the architectureCLAUDE.md: layout, stack, test tiersCONVENTIONS.md: naming, commits, PRs