Add read-only production runtime audit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# 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_cache`d, 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](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](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.
|
||||
Reference in New Issue
Block a user