Phase D — Chat s RAG injection¶
Stav D-core: ✅ Dokončeno 30.4.2026 (6 sub-tasků, spine + expand + smoke test + docs)
Stav D-memory: ⏳ Plánováno na separátní session (Cognee + Neo4j + chat message vector index)
Trvání: ~1 den (30.4)
Klíčový artefakt: app/chat/ modul, /chat rewrite na RAG-first, history-aware rewrite_standalone()
Cíl D-core¶
Postavit production-ready chat s RAG kontextem:
/chatinterní volání retrieval pipeline (Phase C/retrieve) místo full markdown dump- Top-K chunks (default 6) s denormalized metadaty pro textové citace v odpovědi
- History-aware standalone rewrite (follow-up dotazy se rozšíří o kontext z předchozích turn)
- Pydantic AI
message_historycontinuity přes Pydantic AI 1.81 ModelRequest/ModelResponse - Soft-fail na full markdown pro reporty bez chunks (žádný 4xx pro user)
- Per-turn audit trail v
messages.meta(retrieval_log_id, chunks_used, mode, rewritten_query)
D-memory bude separátní fáze — vector indexace chat zpráv + Cognee orchestrator + Neo4j graph entit. Důvod staged přístupu: bez real chat dat nelze rozumně designovat entity extraction patterns; D-core data poslouží jako training input pro D-memory tasks.
Architektonická rozhodnutí¶
D1: Just-ship + iterate (žádný formal eval pro D-core)¶
User decision 30.4: golden set v1 + recall@10 quality gate (plánováno v Phase C C.12) deferováno. Důvod: real chat queries odhalí relevant problems rychleji než kalibrovaná metrika nad mock queries. Pokud potřeba bude, golden set v2 později.
D2: Last-N v promptu (transitional, D-memory ho nahradí)¶
3 páry user/assistant = posledních 6 zpráv předané jako:
rewrite_standalone(query, history)— LLM přepíše konverzační dotaz na samostatný (zájmena „to“, „ten dům“ → konkrétní entity z history)- Pydantic AI
message_history—agent.run_stream(message_history=...)projde listModelRequest/ModelResponseparts → LLM má přirozenou continuity bez stuffingu do user_prompt
Proč nejde dál v D-core: full conversation memory vyžaduje vector index nad messages tabulkou + entity-aware retrieval. To je D-memory práce přes Cognee — bez Cognee patterns by to byl throwaway kód.
D3: Textové citace v odpovědi (žádná UI)¶
Phase C chunks už mají denormalized source_label / attachment_filename / section_name (žádný JOIN per chunk). format_chunks_as_context() je sestaví do markdown bloku, který obsahuje preamble s citation rules:
Pravidla pro práci s úryvky:
- **Cituj zdroj v odpovědi** (např. „Z přílohy „Vyjádření ČEZ" vyplývá…",
„Sekce *Povodně* hlavního reportu uvádí…").
- **Pokud info v úryvcích chybí, řekni to** (nevymýšlej, nehádej).
- **Více úryvků v rozporu** → uveď oba a vysvětli rozdíl.
LLM přepíše citace na přirozený jazyk. Frontend nemá speciální source-badges UI — citace jsou inline v textu, čitelné jako součást odpovědi.
D4: Soft-fail na full markdown¶
Pokud retrieve vrátí 0 chunks nebo selže (timeout, RetrieveError, embedding_status='failed', infra error) → automatický fallback na _report_body() cestu (parsed_markdown z Phase B nebo clean_text z v1-import). User dostane funkční odpověď, mode tracking pomocí meta.mode='full_fallback' + meta.fallback_reason.
Bez hardcoded blacklistu: chat router nečte parsed_metadata.embedding_status jako pre-flight gate — reactive fallback po neúspěšném retrieve. Robustnější (kryje i případy kdy je status='ok' ale chunks prázdné z jiných důvodů).
D5: Single-report folder scope only¶
D-core implementuje jen single-report path. Multi-report (compare_report_ids ≥ 2) zachovává current full-text concat (jako Phase A.1). Folder scope = main report + attachments + figures pokrývá 95 % use cases. Cross-folder multi-report je D-memory práce (Cognee s graph hybrid pro entity-level srovnání).
Implementace¶
Sub-tasky (hybrid spine, jako Phase B + C)¶
| # | Task | Status |
|---|---|---|
| D.1 | /chat rewrite na RAG-first (single-report) + soft-fail base |
✅ |
| D.2 | History-aware rewrite_standalone() + Pydantic AI message_history |
✅ |
| D.3 | Soft-fail formalizace (mode tracking, fallback_reason) | ✅ pokryto D.1 |
| D.4 | Citation rules + messages.meta enrichment + RetrieveResponse.retrieval_log_id |
✅ |
| D.5 | Manuální smoke test 5 klasifikovaných queries | ✅ |
| D.6 | MkDocs phase-d.md + pipeline § Krok 7 | ✅ tato stránka |
Nové soubory¶
app/chat/__init__.py— re-exportsformat_chunks_as_context,load_recent_history,history_to_pydantic_messagesapp/chat/rag.py— všechny tři helpery + citation preamble +DEFAULT_HISTORY_LIMIT_PAIRS=3konstanta
Upravené soubory¶
app/retrieval/rewrite.py—rewrite_standalone(query, history)aktivuje real LLM rewrite (gemini-3-flash-preview) když history má obsah; sanity guard proti suspicious-length output (LLM hallucinated answer místo rewrite); failure mode = fallback na original queryapp/retrieval/schemas.py—RetrieveResponse.retrieval_log_id: UUID | Nonepřidánapp/retrieval/service.py— capturedb.insert_retrieval_log()return ID + populate responseapp/routers/chat.py— major refactor: ServiceDB dep + reorder (conversation init před retrieve) + history flow + soft-fail + meta enrichment + headers
Smoke test (D.5)¶
5 klasifikovaných queries proti production tenant jiri@slimarik.cz (21 reportů, 290 chunks):
TEST 1 — simple lookup¶
Query: „Jaká je adresa nemovitosti?“ (KOMPLET PDF, 33 chunks)
- mode=rag, retrieval_log_id present, 8.2 s end-to-end
- LLM odpovědělo: „Adresa: Dolanská 120, 273 51 Velké Přítočno (okres Kladno). Tato informace je uvedena v úvodní části hlavního reportu (Sekce 1)…“
- Citation funkční ✅ — explicit reference na sekci
TEST 2 — vague short (HyDE expected)¶
Query: „povodně“ (1 slovo → < 4 → HyDE aktivuje)
- mode=rag, 27.4 s (HyDE LLM call adds ~700 ms + LLM stream pomalejší kvůli rich context)
used_hyde=truev retrieval_log- LLM odpovědělo: „K problematice povodní u nemovitosti na adrese Dolanská 120, Velké Přítočno jsem v dostupných částech vašeho reportu (konkrétně v Technické části) nalezl… Sekce 2 reportu (Podklady z územního plánu obce)…“
- Citation funkční ✅
TEST 3 — follow-up rewrite (multi-turn)¶
Turn 1: „Co říká report o povodňovém riziku?“ - mode=rag, conv_id zachycen pro turn 2
Turn 2: „a co to znamená pro hypotéku?“
- mode=rag, stejný conv_id, 12.6 s
- meta.rewritten_query = "Jaký vliv má zjištěné riziko povodní pro nemovitost na adrese Dolanská 120, Velké Přítočno na získání a podmínky hypotéky?"
- LLM rozbalil zájmeno „to“ → konkrétní entity z předchozí konverzace ✅
- Odpověď: konkrétní content o vinkulaci pojistky, povodňových zónách 1–4, podmínkách bank
- Confirmuje že message_history continuity + history-aware rewrite oba fungují ✅
TEST 4 — out-of-scope¶
Query: „Jaká je sazba DPH v České republice v roce 2026?“
- mode=rag, 18.8 s
- LLM správně řekl: „Informace o konkrétních sazbách DPH nejsou součástí technického reportu pro nemovitost…“
- Pak doplnil obecnou znalost (21 % / 12 %) — hraniční case (LLM uznal limit reportu, pak přidal value-add z general knowledge). Pro user OK; striktnější citation rule by zastavila u „v reportu to není“.
TEST 5 — soft-fail fallback¶
Query: „Co je v tomto reportu?“ (test report 1a004adb…855, 0 chunks, embed_status='-')
- mode=full_fallback, log_id=None, 15.6 s
meta.fallback_reason = "no_chunks"✅- LLM odpovědělo z
clean_text(testovací data seedovaná SQLem) — full text path projel správně - Soft-fail = transparent pro user, audit trail v meta
Telemetrie (retrieval_log za 15 minut)¶
| Metric | Value |
|---|---|
| Total log rows | 6 (5 testů + 1 setup turn pro T3) |
| HyDE aktivace | 1 (T2 short query) |
| Reranked | 5 z 6 (T2 měl 1 chunk po hybrid → rerank no-op guard) |
| Had rewrite | 1 (T3b follow-up) |
| avg embed_ms | 280 |
| avg retrieval_ms (PG RPC) | 62 |
| avg rerank_ms (Cohere) | 597 |
E2E retrieval overhead (embed + RPC + rerank): ~939 ms. Plus LLM stream čas (~5–25 s podle output length).
Co Phase D-core zanechala pro Phase D-memory¶
nemoreport.messagesmá rich meta payload (retrieval_log_id, chunks_used, rewritten_query, fallback_reason, mode) → Cognee může retrospektivně backfill embed + entity extract- 21 reportů × průměrně 14 chunks + 6 produkčních konverzací = baseline data set pro entity extraction tuning
rewrite_standalone()má real LLM impl — D-memory ji rozšíří o entity-aware rewrite („nemovitost“ → konkrétníreport_id, „povodně“ → graph node id)- Pydantic AI
message_historypattern v chat.py je channel kterým Cognee bude injektovat entity-aware kontext (místo raw last-N replay)
Co Phase D-core deferovala do D-memory¶
- Vector index nad chat messages — full historie dohledatelná přes embedding (user request: „aby byla kompletní historie chatu uživatele dohledatelná přes vektory“)
- Cross-session memory — uživatel se vrací po týdnu, chat „pamatuje“ co řešil
- Entity extraction tasks — parcely, adresy, rizika z chat → graph nodes
- Episode model — 1 conversation = 1 episode? per-turn? mix?
- Hybrid retrieval over messages — vector (sémantika) + graph (vztahy entit) podle dotazu
- Cognee orchestrator integration — Python lib decidne kdy vector vs graph vs hybrid
- Neo4j Docker service jako 4. service v Sliplane
Co Phase D-core deferovala do dalšího iteration cyklu (mimo D-memory)¶
- Multi-report cross-folder RAG —
compare_report_ids ≥ 2zatím na full-text concat - Section scope retrieval —
/retrievenepodporujescope.type='section'(501 v Phase C.5) - Figure binary delivery v RAG mode — chunks figures mají AI annotation v
content, binary obrázky až po validaci value-vs-cost - Pre-stream
event: retrievalSSE — frontend UX progress indicator - Sources UI badge / modal — citation jsou inline text, badge UI je nice-to-have
- Debug mode
?debug=1— debug payload v response (chunks scores, rewrite, latence per stage) pro tester troubleshooting - Admin retrieval quality dashboard — top queries, latency histogram, feedback correlation, golden set results
Známé limity D-core¶
- Out-of-scope queries: LLM uzná limit reportu, ale doplní general knowledge (T4 DPH). Striktnější citation rule by zastavila po „v reportu to není“ — trade-off mezi user value a strict scope adherence.
- Latence vague queries: HyDE adds ~700 ms LLM call, plus rich context = pomalejší stream output. T2 trvalo 27 s vs 8 s pro simple lookup. Optimization v expand session.
- No section retrieve scope: user kliknutí na sekci v ZoomBar → backend ignoruje scope a vrací folder retrieve, jen prolije section name jako focus hint do system promptu.
- No multi-report RAG: cross-folder srovnání zatím na full-text concat (může být 40K+ tokens při 5 reportech, drahý LLM call).
Reference¶
- Plán:
plan/04-phase-d-chat-integration.md(Draft 1 — 22.4) → reconciliace 30.4 v conversation - Implementace commit:
fe47e93feat(phase-d): D-core — chat /retrieve injection + history-aware rewrite - Architectural pipeline doc: Pipeline § Krok 7