Přeskočit obsah

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:

  1. /chat interní volání retrieval pipeline (Phase C /retrieve) místo full markdown dump
  2. Top-K chunks (default 6) s denormalized metadaty pro textové citace v odpovědi
  3. History-aware standalone rewrite (follow-up dotazy se rozšíří o kontext z předchozích turn)
  4. Pydantic AI message_history continuity přes Pydantic AI 1.81 ModelRequest/ModelResponse
  5. Soft-fail na full markdown pro reporty bez chunks (žádný 4xx pro user)
  6. 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:

  1. rewrite_standalone(query, history) — LLM přepíše konverzační dotaz na samostatný (zájmena „to“, „ten dům“ → konkrétní entity z history)
  2. Pydantic AI message_historyagent.run_stream(message_history=...) projde list ModelRequest/ModelResponse parts → 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-exports format_chunks_as_context, load_recent_history, history_to_pydantic_messages
  • app/chat/rag.py — všechny tři helpery + citation preamble + DEFAULT_HISTORY_LIMIT_PAIRS=3 konstanta

Upravené soubory

  • app/retrieval/rewrite.pyrewrite_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 query
  • app/retrieval/schemas.pyRetrieveResponse.retrieval_log_id: UUID | None přidán
  • app/retrieval/service.py — capture db.insert_retrieval_log() return ID + populate response
  • app/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=true v 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.messagesrich 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_history pattern 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 RAGcompare_report_ids ≥ 2 zatím na full-text concat
  • Section scope retrieval/retrieve nepodporuje scope.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: retrieval SSE — 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: fe47e93 feat(phase-d): D-core — chat /retrieve injection + history-aware rewrite
  • Architectural pipeline doc: Pipeline § Krok 7