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
cp .env.example .env # once: a local sender id, which the API needs to start
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 |
Every turn goes to the DSS at EXPERIENCE_API_DSS_BASE_URL, so run a DSS
first. There is no stand-in: with no DSS, a turn fails.
| Variable | Default | What |
|---|---|---|
EXPERIENCE_API_SENDER_ID |
none, required | The network participant this deployment serves. Sent to the DSS as senderId |
EXPERIENCE_API_DSS_BASE_URL |
http://localhost:8077 |
Where the DSS listens |
EXPERIENCE_API_CHANNEL |
web |
The channel for a chat whose client sends none |
EXPERIENCE_API_MAX_CHARACTERS |
1200 |
The longest answer to ask the DSS for |
Each can also be set in .env; a real environment variable wins over it.
A missing sender id, or a bad 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, and the sender id from .env. It calls a DSS on the host's port
8077; set EXPERIENCE_API_DSS_BASE_URL for another. 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