Foundation (phase 01, verified): - FastAPI app: /api/health, /api/suggestions, /api/chat (placeholder), static frontend served locally (no CDN) - Postgres 17 + pgvector via db/Containerfile + compose.yaml (podman compose up -d db), Alembic initial migration (documents, chunks with vector(768), query_log) - LLM client targeting https://aipi.reeseapps.com/v1 (turbo/embed); scripts/llm_probe.py verified models + 768-dim embeddings live - Conditional debugpy: imported only when DEBUGPY=1 (attach on demand, :5678); logging config for clean single-line logs - Frontend shell: mobile-first chat + Sources pages, tokens, a11y baselines - Tests: 24 unit+integration (99% coverage on app/), ruff + pyright clean, Playwright smoke E2E (3 tests) against a deterministic mock LLM - Planning: .agent/PLAN.md (architecture + LOCKED decisions), AGENTS.md, 6 user stories, 7 phase files (one story / one phase / one Playwright suite each)
3.5 KiB
Phase 03 — Story: Chat RAG Answer (happy path)
Story: .agent/user_stories/chat-rag-answer.md
Context: .agent/PLAN.md §3 (data flow), §4 (SSE contract), §6 (persona), §9 (logging)
Goal
The core product loop: question → embed → cosine top-4 → full top-2
documents → turbo (streamed) → chippy grounded answer with source chips.
Implementation steps
app/rag/retriever.py—retrieve(db, question_embedding) -> list[RetrievedChunk](score = 1 − distance,ORDER BY embedding <=> $1 LIMIT BOR_TOP_K_CHUNKS) +select_documents(chunks, n) -> list[Document](distinct bydocument_id, ranked by best chunk score, cap content atBOR_MAX_CONTEXT_CHARSwith[…truncated…]).app/rag/prompts.py— locked persona + HONESTY GATE prompt builder (PLAN §6 verbatim,<relevance>HIGH|LOW</relevance>,<documents>block; LOW mode includes theDEFLECT_MODEmarker + weak-hit titles).app/rag/llm.py— addchat_stream(messages) -> AsyncIterator[str](openai async,stream=True,model=turbo, temperature 0.4, max_tokens ~700).app/api/chat.py—POST /api/chat(ChatRequest) →StreamingResponse(SSE): emitdeltaevents from the stream, then thedoneevent (deflected, sources, suggestions); insertquery_logrow (deflected= false this phase); per-turn log line (PLAN §9); structured error events ({"type":"error","detail":…}) on LLM/DB failure.frontend/assets/app.js— replace the placeholder handler:fetch+ReadableStreamSSE parser; render deltas live into a brain bubble (reuse the typing-indicator → streaming handoff); ondone, append.source-chips under the bubble; on error, show the banner (full state machine is Phase 06 — keep it simple-correct here).- Tune
settings.suggestionsif the real Homelab import revealed better defaults (optional here; Phase 05 owns the chips).
UI Verification
Against the story's "UI Visualization & Structure": bubbles right/left
(brand vs surface, ≥4.5:1 text), avatar 🧠, source chips mono/brand-soft
with source/path and ellipsis, safe markdown (paste an answer containing
<script>alert(1)</script> from the mock to prove it's escaped). Chat
column 46rem centered. 1280px + 375px screenshot pass.
Testing & Quality
- Unit: retriever ordering/dedup/cap (fake rows), prompt builder (HIGH
contains documents +
HIGH, LOW containsDEFLECT_MODE+ titles only, persona rules present verbatim), SSE event serialization. - Integration:
/api/chatagainst the mock LLM with a seeded temp schema — assert SSE delta sequence,donepayload (sources non-empty, deflected false),query_logrow, error event when LLM unreachable. - Coverage:
uv run pytest --cov=app --cov-report=term-missing— >90%.
Playwright Execution Phase
Run ONLY this story's suite:
uv run pytest tests/e2e/test_chat_rag.py -v --no-cov
Implements the story mapping: streamed grounded answer + kubernetes.md
source chip + button recovery; DB query_log assertion; raw SSE shape
check via httpx.
Success criteria
- end-to-end: question → streamed chippy answer citing
kubernetes.md query_logrow per turn; per-turn log line in stdout- LLM-down path shows error banner, no stuck button
- unit + integration green, coverage >90%
- UI verification passed
- story E2E green in isolation
- committed
Commit
git add -A && git commit --no-gpg-sign -m "feat(rag): stream grounded chat answers via pgvector cosine retrieval with source citations"