Ali Zarinkolah 43932b6562 docs(adr): record query normalization and file-scoped pagination
Why:
- Both are contracts callers depend on, not implementation details, and neither
  was written down. This repo treats the ADR as the source of truth rather than
  letting code diverge from it silently.

Changes:
- Record that the search query is folded the same way ingested content was, and
  why the alternative fails in the worst available way: an exact-looking query
  returning nothing, with no error and nothing in the logs to distinguish it
  from a genuine miss.
- Record that listing requires file_id and paginates by order_id value, and why
  an offset cursor repeats an already-served row under a concurrent insert.
- State that results carry no relevance score and no ranked order, so callers
  cannot read array position as relevance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:09:07 +03:30
2026-08-02 15:52:46 +03:30

Talie chatbot service

Architecture decisions live in docs/adr. The first implementation milestone is documented in the ingestion vertical-slice plan. Day-to-day operation — tuning the ingestion bounds, the proxy timeout requirement, and how to investigate or retry a failed upload — is the operator runbook.

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").

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

Nothing over HTTP can create the first tenant — every /v1 route needs an API key, and a key cannot exist before its tenant. One command issues both, plus any domains, printing the key once (only its hash is stored):

uv run python -m src.cli.provision_tenant --slug acme --domain fire

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:

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.

./scripts/smoke.sh verifies the whole path — Compose up, both deployment steps, provisioning, an upload through the running web process to indexed Qdrant points. See the runbook.

Local Langfuse

This repo includes a root-level development Compose file for Langfuse:

Start Langfuse locally:

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:

http://localhost:3000

If the chatbot app runs on your host machine, configure it with:

LANGFUSE_HOST=http://localhost:3000

If the chatbot app later runs inside the same Compose project/network as Langfuse, configure it with:

LANGFUSE_HOST=http://langfuse-web:3000

A future app stack can be launched together with Langfuse using multiple Compose files:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.langfuse.yml \
  --env-file .env \
  --env-file .env.langfuse \
  up -d
Description
No description provided
Readme 1.4 MiB
Languages
Python 99.3%
Shell 0.6%
Mako 0.1%