Fix migration workflow: upload as artifact instead of scp to practice EC2

This commit is contained in:
2026-08-13 11:14:25 +07:00
parent 7ebbe1f309
commit a4819b8653
51 changed files with 6830 additions and 8 deletions
+126
View File
@@ -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.01.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.