6.8 KiB
15 — Configuration
Where configuration is defined
apps/ai-service/config.py is the single authority for the Python service:
every setting is a field on the Pydantic Settings class, loaded from the
environment or from a .env file next to the process, with extra="ignore".
get_settings() is @lru_cached, so values are read once per process.
There is no .env.example anywhere in the repository. The only env file is
apps/ai-service/.env, which is gitignored and local; production uses
apps/ai-service/.env.prod, which is also gitignored and lives only on the EC2
host. A new engineer therefore has no committed template to copy — see
27-technical-debt.md.
ai-service settings
| Variable | Required | Default | Purpose | Secret |
|---|---|---|---|---|
APP_NAME |
no | vsf-duoc-thu-ai-service |
FastAPI title | no |
ENVIRONMENT |
no | local |
Label; sent as deployment.environment on OTel resource |
no |
QDRANT_URL |
effectively yes | http://localhost:6333 |
Vector store | no |
QDRANT_COLLECTION |
no | duocthu_v1 |
Collection name; the manifest sidecar is <name>__manifest |
no |
QDRANT_API_KEY |
no | None |
Qdrant auth | yes |
POSTGRES_DSN |
no | postgresql://duoc_thu:duoc_thu@localhost:5432/duoc_thu |
Traces, turns, feedback. Declared repr=False so it is not echoed |
yes |
EMBEDDING_PROVIDER |
no | cohere-v4 |
cohere-v4 or disabled. Any other value raises at startup |
no |
EMBEDDING_DIMENSIONS |
no | 1024 |
Must match the corpus manifest or startup fails | no |
EVIDENCE_MINIMUM_SCORE |
no | 0.12 |
Dense-route score floor | no |
AWS_REGION |
no | us-east-1 |
Bedrock region | no |
ANSWER_PROVIDER |
no | disabled |
disabled | stub | bedrock-converse | bedrock-claude. Chooses the operating mode |
no |
ANSWER_MODEL_ID |
no | deepseek.v3.2 |
Bedrock model id | no |
RERANK_ENABLED |
no | false |
Enables cohere.rerank-v3-5:0 on the fallback route |
no |
METRICS_ENABLED |
no | true |
Builds the Prometheus exporter | no |
METRICS_TOKEN |
no | "" |
Bearer token for GET /metrics; empty = unauthenticated |
yes |
OTEL_ENABLED |
no | false |
Turns on OTLP export | no |
OTEL_SERVICE_NAME |
no | ai-service |
no | |
OTEL_EXPORTER_OTLP_ENDPOINT |
no | http://localhost:4318/v1/traces |
OTLP/HTTP traces endpoint | no |
OTEL_SAMPLE_RATIO |
no | 1.0 (0.0–1.0) |
TraceIdRatioBased sampler |
no |
ENTITIES_PATH |
in the container | repo-relative ingestion/data/verified/drug_entities.json |
Drug alias catalog | no |
MAX_WALL_CLOCK_MS |
no | 40000 |
Per-turn budget | no |
MAX_LLM_CALLS_PER_TURN |
no | 8 |
Per-turn budget | no |
ENTITIES_PATH needs an explicit value in the container: config.py's default
resolves two parents up from apps/ai-service/config.py, and the image
flattens apps/ai-service/ into /app, so the depth is wrong. The Dockerfile
bakes the file to ./ingestion_data/drug_entities.json and .env.prod points
at it.
Settings that change behaviour, not just tuning
Three values are mode switches rather than knobs:
| Setting | Effect |
|---|---|
EMBEDDING_PROVIDER=disabled |
build_runtime returns no answer service and no agent. /v1/rag/query answers 503, while /ready still answers 200. |
ANSWER_PROVIDER=disabled |
No RagAgent, no understanding, no multi-turn. Retrieval-only, single-turn, verbatim quotes. |
EMBEDDING_DIMENSIONS ≠ manifest |
Startup raises ManifestMismatch and the process does not come up. |
web settings
| Variable | Required | Default | Purpose |
|---|---|---|---|
API_GATEWAY_URL |
no | — | Preferred upstream; accepts a base URL or a full /v1/rag/... URL |
AI_SERVICE_URL |
no | http://localhost:8000 |
Fallback; set to http://ai-service:8000 in prod Compose |
Rate-limit rules are hard-coded constants in middleware.ts, not
configuration: /api/chat 12/min and 120/hour; /api/suggest 120/min.
ingestion settings
ingestion takes no environment variables. Everything is a CLI flag
(--pdf, --out, --tables, --monographs, --chunks, --provider,
--collection, --region, --qdrant-url, --slice-size, --attempts,
--embed-only). AWS credentials come from the standard boto3 chain.
Deployment-layer configuration
| Layer | File | Notes |
|---|---|---|
| Production Compose | infra/docker/docker-compose.prod.yml |
ai-service reads env_file: ../../apps/ai-service/.env.prod (not in the repo) |
| Observability overlay | infra/docker/docker-compose.observability.yml |
Sets OTEL_ENABLED=true, OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318/v1/traces, ENVIRONMENT=compose; reads GRAFANA_ADMIN_USER / GRAFANA_ADMIN_PASSWORD from the shell |
| Helm | values.yaml + values-{dev,staging,prod}.yaml |
Maps to a ConfigMap of the same env vars; POSTGRES_DSN comes from a Secret |
| CI | .github/workflows/deploy.yml |
Uses EC2_HOST, EC2_SSH_KEY, GRAFANA_ADMIN_PASSWORD GitHub secrets |
Helm chart defaults are not production defaults
infra/helm/medical-chatbot/values.yaml ships
aiService.config.embeddingProvider: disabled and answerProvider: disabled,
i.e. a deployment of the chart as-is answers 503 on /v1/rag/query. It also
ships secret.postgresPassword: duoc_thu and
secret.grafanaAdminPassword: change-me as literal defaults.
Secrets inventory
| Secret | Where it lives | Committed? |
|---|---|---|
| PostgreSQL password | docker-compose.prod.yml env (duoc_thu/duoc_thu), Helm secret.postgresPassword |
Yes — a default credential is in the repository |
| Grafana admin password | GRAFANA_ADMIN_PASSWORD GitHub secret → shell env; Helm default change-me |
Secret value not committed; the placeholder default is |
| AWS credentials | EC2 instance IAM role | No — deliberately; the Compose header comment says so |
QDRANT_API_KEY |
Unset (Qdrant is not exposed) | No |
METRICS_TOKEN |
Unset | No |
| EC2 host + SSH key | GitHub Actions secrets | No |
git ls-files shows no .env file tracked, and the two IAM documents under
infra/aws/iam/ are policy JSON, not credentials. The one real issue is the
PostgreSQL default credential, which is committed in two places — see
16-security.md.
Configuration verified this session
apps/ai-service/.env (local, gitignored) contains:
EMBEDDING_PROVIDER=cohere-v4
ANSWER_PROVIDER=bedrock-converse
ANSWER_MODEL_ID=qwen.qwen3-next-80b-a3b
RERANK_ENABLED=true
AWS_REGION=us-east-1
QDRANT_COLLECTION=duocthu_v1
QDRANT_URL=<local>
Note the drift: the code default for ANSWER_MODEL_ID is deepseek.v3.2,
the local .env uses qwen.qwen3-next-80b-a3b, and the production value
is unverifiable from the repository because .env.prod is not committed. Any
statement about which model production runs would be a guess.