# ai-service FastAPI service holding the entire live RAG path: query understanding, drug resolution, deterministic section routing, guarded retrieval, grounded generation, claim/citation verification, printed-page citations, and PostgreSQL conversation + retrieval traces. In production this is what `apps/web` calls — there is no gateway in front of it. ## Local infrastructure ```powershell docker compose -f ..\..\infra\docker\docker-compose.yml up -d postgres qdrant python -m migrate python -m uvicorn main:app --port 8079 ``` > **Use a plain restart rather than `--reload` on Windows.** The reloader has > been observed serving the previous code after an edit on this project, so a > change can appear to have no effect (or appear to work when it has not been > loaded). Restarting the process avoids the ambiguity. `GET /health` is always available. `POST /v1/rag/query` is the live agent endpoint. Structured `subject_scope`/`intent` requests fail closed on unknown/non-human input. ## Bedrock is required for real answers Since 2026-08-05 the live path calls AWS Bedrock and needs credentials with invoke permission: | Role | Default | Setting | |---|---|---| | Query embedding | `cohere.embed-v4:0` | `embedding_provider` (default `cohere-v4`) | | Answer generation | Bedrock Converse | `generation_provider` (`bedrock-converse` / `bedrock-claude`) | | Rerank | Cohere rerank | `rerank_enabled` — **off** by default; only affects the similarity fallback, never the deterministic section route | Credentials come from the environment (an IAM instance role in production, no long-lived keys). With no working generator configured, a rejected generation **abstains** — it must never degrade into dumping raw source text at a clinician. `EMBEDDING_PROVIDER=local-smoke` is only for local plumbing checks. Its hashing vectors are deterministic but not semantic and must not be used for any retrieval-quality claim. ## Corpus coupling Startup verifies the `duocthu_v1__manifest` sidecar and **refuses to run** against a collection whose corpus sha / model / dimensions do not match (F-05). A machine with an empty Qdrant cannot serve answers until a snapshot is restored or `ingestion/` is re-run — the latter costs real Bedrock spend. ## Request budget and timeouts A turn makes several sequential Bedrock calls, bounded by `max_wall_clock_ms` (40s) and `max_llm_calls_per_turn` (8) in `config.py`. The budget is checked **between** calls and cannot cancel one already in flight, which is separately bounded by `read_timeout` in `adapters/bedrock_converse.py`. Real ceiling ≈ 40s + one in-flight call, so any client calling this service must allow more than that — `apps/web`'s `ChatPanel.tsx` uses 65s for exactly this reason. ## Tests ```powershell python -m pytest -q # needs Postgres + Qdrant up python -m pytest -q --ignore=tests/test_api.py --ignore=tests/test_live_datastores.py ``` 230 pass without live datastores. Coverage is uneven and worth checking before relying on it: `rag/agent.py`'s dosing state machine has been covered only since 2026-08-11, and the repo has no frontend test setup, so a green suite does not by itself confirm a user-visible change works.