Ali Zarinkolah b25c15fefa docs(claude): record plan 002 phase 2 in the project status
Why:
- The status section described plan 002 as Phase 1 only. The /v1/points read
  routes, the query service, and the points scopes had shipped but were listed
  under "not built yet", so a fresh session would start from a wrong map of the
  codebase.

Changes:
- Record the five read routes, the points:read gating, and the 404-not-403
  mapping.
- Call out the three route-level rules that fail quietly when broken: /count and
  /search must precede /{point_id}, file_id is required on the listing, and the
  search query is Persian-folded before matching.
- Point at tests/support/point_contract.py as the place new repository behaviour
  belongs, since it runs against both the fake and real Qdrant.
- Narrow the "not built yet" list to Phases 3-6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:04:59 +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%