Why: - Domain values are denormalized into every Qdrant point payload. Without validation, an unregistered or typo'd domain (e.g. "fier" for "fire") silently creates a new partition that retrieval never queries — the file ends up invisible rather than rejected. Tenants also need independently sized domain sets (one may run 14 insurance lines, another 6), which rules out an enum. Changes: - tenant_domains table (migration 41335d162de8) + repository, unique on (tenant_id, domain). - src/application/domains/: ensure_domain_allowed() is the strict-allowlist check now run inside upload_source_file()'s first transaction, before any MinIO object, job row, or Qdrant point is written. - /v1/domains (list/create/patch/disable/enable) gated on its own domains:read/domains:write scopes, deliberately separate from files:write so an upload key cannot create partitions. domain itself is immutable (denormalized into every point payload); only display_name is editable. Disable blocks new uploads without touching already-indexed points. Impact: - BREAKING: POST /v1/files now rejects any domain without an active tenant_domains row (400, unknown_domain). A domain must be created via POST /v1/domains before the first upload to it.
80 lines
2.3 KiB
Markdown
80 lines
2.3 KiB
Markdown
# Talie chatbot service
|
|
|
|
Architecture decisions live in [`docs/adr`](docs/adr). The first implementation
|
|
milestone is documented in the [ingestion vertical-slice plan](docs/plans/001-ingestion-vertical-slice.md).
|
|
|
|
## Provisioning the datastores
|
|
|
|
Both schema steps run as explicit deployment steps. The application performs no
|
|
DDL at startup — not for Postgres (ADR-0009) and not for Qdrant (ADR-0001,
|
|
"Collection provisioning").
|
|
|
|
```bash
|
|
docker compose up -d # Postgres, MinIO, Qdrant
|
|
uv run alembic upgrade head # Postgres schema
|
|
uv run python -m src.cli.qdrant_bootstrap # the `chunks` collection
|
|
uv run fastapi dev src/main.py
|
|
```
|
|
|
|
Before a tenant can upload, its domains must be registered — `POST /v1/files`
|
|
rejects an unregistered or disabled `domain` with `400`. The calling backend
|
|
manages them over `/v1/domains` using a key with the `domains:write` scope:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8000/v1/domains \
|
|
-H "Authorization: Bearer $API_KEY" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"domain": "fire", "display_name": "Fire insurance"}'
|
|
```
|
|
|
|
Both bootstrap commands are idempotent and safe to re-run. `qdrant_bootstrap` verifies an
|
|
existing collection against the pinned schema and exits non-zero on a mismatch,
|
|
rather than leaving a silently degraded sparse index in place.
|
|
|
|
## Local Langfuse
|
|
|
|
This repo includes a root-level development Compose file for Langfuse:
|
|
|
|
- [`docker-compose.langfuse.yml`](docker-compose.langfuse.yml)
|
|
- [`.env.langfuse.example`](.env.langfuse.example)
|
|
|
|
Start Langfuse locally:
|
|
|
|
```bash
|
|
cp .env.langfuse.example .env.langfuse
|
|
# edit .env.langfuse and replace CHANGE_ME values
|
|
|
|
docker compose --env-file .env.langfuse -f docker-compose.langfuse.yml up -d
|
|
```
|
|
|
|
Open:
|
|
|
|
```text
|
|
http://localhost:3000
|
|
```
|
|
|
|
If the chatbot app runs on your host machine, configure it with:
|
|
|
|
```env
|
|
LANGFUSE_HOST=http://localhost:3000
|
|
```
|
|
|
|
If the chatbot app later runs inside the same Compose project/network as
|
|
Langfuse, configure it with:
|
|
|
|
```env
|
|
LANGFUSE_HOST=http://langfuse-web:3000
|
|
```
|
|
|
|
A future app stack can be launched together with Langfuse using multiple Compose
|
|
files:
|
|
|
|
```bash
|
|
docker compose \
|
|
-f docker-compose.yml \
|
|
-f docker-compose.langfuse.yml \
|
|
--env-file .env \
|
|
--env-file .env.langfuse \
|
|
up -d
|
|
```
|