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"