fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:12:24 +02:00
|
|
|
"""Retrieval module -- module 5, phase 3, plan step 4 (kb/phases/kb-m5-faza3.md, §6).
|
|
|
|
|
Moved 1:1 into `packages/kb-retrieval` in phase 4 (kb/phases/kb-m5-faza4.md, §3,
|
2026-07-22 16:06:02 +02:00
|
|
|
decision 1) so both `documents-ingest` (venv job) and `kb-query` (Docker service) share one
|
|
|
|
|
tested module instead of the service image needing to pull in all of `jobs/documents-ingest`.
|
|
|
|
|
|
feat(kb-retrieval,kb-query): add hybrid retrieval mode (faza mailowa Krok 3)
Mail (gmail) envelopes never get a document_summary (Decyzja 6 -- a mail
"summary" would usually be longer than the mail itself), so they're invisible
to the cascade's stage-1 pre-filter. hybrid_retrieve runs the existing
cascade for summarized sources (paperless) and, in parallel, a direct chunk
scan restricted to summaryless_sources (gmail), merging both by dist -- same
embedder/cosine space, so the merge is a plain sort, no re-normalization.
hybrid_query mirrors cascade_query (one shared query embed).
kb-query: mode pattern extended to ^(cascade|flat|hybrid)$, /search routes
"hybrid" to hybrid_query. Default mode stays "cascade" until the quality gate
(plan §8) PASSes on the full mail corpus -- flipping the default, and
deploying this to the running kb-query container, are separate follow-ups
for the operator; this task only adds the code path + tests
(docs/kb/modules/05-faza-mailowa-plan.md, §6, Krok 3).
2026-07-22 19:01:21 +02:00
|
|
|
Three retrieval paths over the same corpus, sharing one query embedding (bge-m3, via Ollama):
|
2026-07-22 16:06:02 +02:00
|
|
|
|
|
|
|
|
- `flat_retrieve`: baseline -- ranks every active `document_chunk` row directly. This
|
fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:12:24 +02:00
|
|
|
formalizes the pilot's ad hoc `/tmp/kbq.sh` query (kb/phases/kb-m5-eval-retrieval-pilot.md)
|
2026-07-22 16:06:02 +02:00
|
|
|
into a tested, versioned module instead of a script living only in a session transcript.
|
|
|
|
|
- `cascade_retrieve`: pre-filters to the top-N `document_summary` matches for one configured
|
|
|
|
|
`model` (plan §2 decision 3, resolved 2026-07-17 as D3: `claude-haiku-4-5` is the compilation
|
|
|
|
|
track; `gemma3:12b` stays in reserve as the local track) before ranking `document_chunk`
|
|
|
|
|
within just those envelopes. At the 186-document pilot scale this doesn't speed anything up
|
|
|
|
|
-- it is an architecture test for the mail-scale corpus (225k envelopes, plan §1.1) where a
|
|
|
|
|
flat chunk scan stops being cheap. `eval/retrieval_eval.py` runs the quality gate (plan §6.2)
|
|
|
|
|
that decides whether it becomes the default path.
|
fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:12:24 +02:00
|
|
|
- `hybrid_retrieve` -- module 5, faza mailowa, plan Krok 3 (kb/phases/kb-m5-faza-mailowa.md,
|
feat(kb-retrieval,kb-query): add hybrid retrieval mode (faza mailowa Krok 3)
Mail (gmail) envelopes never get a document_summary (Decyzja 6 -- a mail
"summary" would usually be longer than the mail itself), so they're invisible
to the cascade's stage-1 pre-filter. hybrid_retrieve runs the existing
cascade for summarized sources (paperless) and, in parallel, a direct chunk
scan restricted to summaryless_sources (gmail), merging both by dist -- same
embedder/cosine space, so the merge is a plain sort, no re-normalization.
hybrid_query mirrors cascade_query (one shared query embed).
kb-query: mode pattern extended to ^(cascade|flat|hybrid)$, /search routes
"hybrid" to hybrid_query. Default mode stays "cascade" until the quality gate
(plan §8) PASSes on the full mail corpus -- flipping the default, and
deploying this to the running kb-query container, are separate follow-ups
for the operator; this task only adds the code path + tests
(docs/kb/modules/05-faza-mailowa-plan.md, §6, Krok 3).
2026-07-22 19:01:21 +02:00
|
|
|
§6, decision 6): mail (gmail) envelopes never get a `document_summary` (streszczenie maila
|
|
|
|
|
would usually be longer than the mail itself -- decision 6's rejected-summaries reasoning), so
|
|
|
|
|
they are invisible to the cascade's stage 1 pre-filter. Hybrid runs the cascade for sources that
|
|
|
|
|
DO have summaries (paperless) and, in parallel, a direct flat-style HNSW scan restricted to
|
|
|
|
|
`summaryless_sources` (gmail), then merges both result sets by `dist` (same embedder, same
|
|
|
|
|
cosine space -- merge is a plain sort, no re-normalization needed). This is the only path that
|
|
|
|
|
can surface mail content until/unless mail gets its own summaries later (decision 6, deferred).
|
|
|
|
|
|
|
|
|
|
`cascade_query` / `flat_query` / `hybrid_query` are the intended clean entry points for kb-query:
|
|
|
|
|
query_text -> chunk hits with `dist` (cosine distance) and `source` ("flat" | "cascade" |
|
|
|
|
|
"hybrid"). `envelope`, `document_chunk`, and `document_summary` are read-only here -- this module
|
|
|
|
|
only ever `SELECT`s.
|
2026-07-22 16:06:02 +02:00
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import aiohttp
|
|
|
|
|
import asyncpg
|
|
|
|
|
|
|
|
|
|
from kb_retrieval.embed import _vector_literal, embed_chunk
|
|
|
|
|
|
|
|
|
|
DEFAULT_SUMMARY_MODEL = "claude-haiku-4-5" # plan §2 D3 resolution 2026-07-17: compilation track
|
|
|
|
|
DEFAULT_EMBED_MODEL = "bge-m3"
|
|
|
|
|
DEFAULT_N = 10 # plan §6.1 start value
|
|
|
|
|
DEFAULT_K = 5 # plan §6.1 start value
|
feat(mail-imap-sync): job przyrostowki + wpiecie w tor body-ingest
Nowy job jobs/mail-imap-sync — jedyny wlasciwy nowy kod przyrostowki
(recon kb/audits/mail-sync-2026-08-06.md §6 poz. 1). Jeden tick, per konto
i folder: EXAMINE -> plan -> UID SEARCH -> FETCH BODY.PEEK[] -> save_eml ->
insert_envelope(entities=[headers, attachment...]) -> UPDATE mail_sync_state.
Wlasciwosci, ktore latwo zgubic po cichu:
- Job NIE embeduje i NIE chunkuje (recon §3.2). Pobieranie jest sieciowe i chodzi
na PIHA 24/7; chunk+embed potrzebuje Ollamy na SOLARII, wylaczanej ~16 h/dobe.
Spoiwem jest kolejka wynikajaca z danych: koperta bez chunkow JEST elementem
kolejki, ktora drenuje mail-body-ingest --only-unchunked.
- Koperta dostaje entities[type=headers] juz przy INSERCIE. Bez tego kazdy nowy
mail mialby prefiks "Temat: (brak tematu) | Od: ?" — bez bledu, tylko z gorszym
retrievalem (recon §2.5 i).
- Kolizja Message-ID miedzy kontami jest liczona (envelopes_conflict_other_source),
nie ukryta w zwyklych duplikatach (recon §2.4).
- Kursor przesuwa sie tylko po nieprzerwanym ciagu w pelni trwalych wiadomosci.
Bledna wiadomosc jest ponawiana (dedup czyni to darmowym), nie przeskakiwana;
trwale zatrucie widac jako niezerowy licznik bledow i stojacy kursor.
- Poswiadczenia wylacznie ze srodowiska — brak flagi --password/--user (Decyzja (c)).
- Tryb --measure (STATUS MESSAGES/UIDNEXT/UIDVALIDITY): pomiar, na ktorym operator
oprze decyzje o historii Fastmaila (Decyzja (e), celowo nieodgadywana).
- Metryki .prom per konto; last_success_timestamp przenoszony przez nieudany run.
Wpiecie w istniejacy tor (recon §6 poz. 4-6):
- mail_body_ingest.fetch_envelopes: --sources (domyslnie gmail,fastmail) +
--only-unchunked.
- fetch_existing_chunk_keys zawezone do zbioru roboczego — bez tego kazdy tick
czyta wszystkie 389 012 kluczy chunkow (~26 MB, ~1,0 s) na nodzie z 2,4 GB
available (recon §2.5 iv).
- DEFAULT_SUMMARYLESS_SOURCES += "fastmail" (Decyzja (g)) w TYM SAMYM commicie,
ktory wprowadza zrodlo: bez tego koperty fastmail zaindeksowalyby sie poprawnie
i byly niewidoczne w /search, bez zadnego bledu.
Testy: 80 dla nowego joba (mock IMAP na poziomie imaplib, wiec testowane jest
prawdziwe parsowanie protokolu) — nowe wiadomosci, uniewaznienie UIDVALIDITY,
dedup, wznowienie po przerwaniu, izolacja kont, dry-run bez sieci, metryki;
+ 47 mail-body-ingest/kb-retrieval. Smoke: --help, blad konfiguracji -> exit 2.
Zero polaczen z zywymi kontami — pierwszy zywy sync robi operator wg runbooka.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:37:32 +02:00
|
|
|
# faza mailowa plan §6 Krok 3: sources with no summaries. `fastmail` added 2026-08-06 in the
|
|
|
|
|
# same commit that introduced the source (recon Decyzja (g)) — mail reaches /search results
|
|
|
|
|
# ONLY through the hybrid path's summaryless leg, which filters `WHERE e.source = ANY($2)`.
|
|
|
|
|
# Left out, fastmail envelopes would archive, chunk and embed correctly and then be invisible
|
|
|
|
|
# to every query, with nothing anywhere reporting an error.
|
|
|
|
|
DEFAULT_SUMMARYLESS_SOURCES = ("gmail", "fastmail")
|
2026-07-22 16:06:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def flat_retrieve(conn: asyncpg.Connection, query_vector: str, k: int = DEFAULT_K) -> list[dict]:
|
|
|
|
|
"""Baseline: rank every active chunk directly against the query embedding, no pre-filter."""
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"SELECT envelope_id, chunk_index, text, embedding <=> $1::vector AS dist "
|
|
|
|
|
"FROM document_chunk WHERE excluded_reason IS NULL AND embedding IS NOT NULL "
|
|
|
|
|
"ORDER BY embedding <=> $1::vector LIMIT $2",
|
|
|
|
|
query_vector, k,
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"envelope_id": r["envelope_id"],
|
|
|
|
|
"chunk_index": r["chunk_index"],
|
|
|
|
|
"text": r["text"],
|
|
|
|
|
"dist": r["dist"],
|
|
|
|
|
"source": "flat",
|
|
|
|
|
}
|
|
|
|
|
for r in rows
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def cascade_retrieve(
|
|
|
|
|
conn: asyncpg.Connection,
|
|
|
|
|
query_vector: str,
|
|
|
|
|
summary_model: str = DEFAULT_SUMMARY_MODEL,
|
|
|
|
|
n: int = DEFAULT_N,
|
|
|
|
|
k: int = DEFAULT_K,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""Stage 1: top-N `document_summary` envelopes for `summary_model`. Stage 2: top-k
|
|
|
|
|
`document_chunk` ranked within just those envelopes.
|
|
|
|
|
|
|
|
|
|
`n` exceeding the number of summarized envelopes is not an error -- plain SQL `LIMIT`
|
|
|
|
|
semantics just return all of them. An empty stage 1 (no summaries for this model, or a
|
|
|
|
|
corpus that hasn't been summarized yet) short-circuits before stage 2 runs at all: a
|
|
|
|
|
cascade can never rank chunks in envelopes it didn't pre-filter into, so there is nothing
|
|
|
|
|
for stage 2 to query.
|
|
|
|
|
"""
|
|
|
|
|
stage1 = await conn.fetch(
|
|
|
|
|
"SELECT envelope_id, embedding <=> $1::vector AS dist FROM document_summary "
|
|
|
|
|
"WHERE model = $2 AND embedding IS NOT NULL ORDER BY embedding <=> $1::vector LIMIT $3",
|
|
|
|
|
query_vector, summary_model, n,
|
|
|
|
|
)
|
|
|
|
|
stage1_summaries = [{"envelope_id": r["envelope_id"], "dist": r["dist"]} for r in stage1]
|
|
|
|
|
|
|
|
|
|
envelope_ids = [s["envelope_id"] for s in stage1_summaries]
|
|
|
|
|
if not envelope_ids:
|
|
|
|
|
return {"stage1_summaries": stage1_summaries, "chunks": []}
|
|
|
|
|
|
|
|
|
|
stage2 = await conn.fetch(
|
|
|
|
|
"SELECT envelope_id, chunk_index, text, embedding <=> $1::vector AS dist "
|
|
|
|
|
"FROM document_chunk WHERE envelope_id = ANY($2::text[]) AND excluded_reason IS NULL "
|
|
|
|
|
"AND embedding IS NOT NULL ORDER BY embedding <=> $1::vector LIMIT $3",
|
|
|
|
|
query_vector, envelope_ids, k,
|
|
|
|
|
)
|
|
|
|
|
chunks = [
|
|
|
|
|
{
|
|
|
|
|
"envelope_id": r["envelope_id"],
|
|
|
|
|
"chunk_index": r["chunk_index"],
|
|
|
|
|
"text": r["text"],
|
|
|
|
|
"dist": r["dist"],
|
|
|
|
|
"source": "cascade",
|
|
|
|
|
}
|
|
|
|
|
for r in stage2
|
|
|
|
|
]
|
|
|
|
|
return {"stage1_summaries": stage1_summaries, "chunks": chunks}
|
|
|
|
|
|
|
|
|
|
|
feat(kb-retrieval,kb-query): add hybrid retrieval mode (faza mailowa Krok 3)
Mail (gmail) envelopes never get a document_summary (Decyzja 6 -- a mail
"summary" would usually be longer than the mail itself), so they're invisible
to the cascade's stage-1 pre-filter. hybrid_retrieve runs the existing
cascade for summarized sources (paperless) and, in parallel, a direct chunk
scan restricted to summaryless_sources (gmail), merging both by dist -- same
embedder/cosine space, so the merge is a plain sort, no re-normalization.
hybrid_query mirrors cascade_query (one shared query embed).
kb-query: mode pattern extended to ^(cascade|flat|hybrid)$, /search routes
"hybrid" to hybrid_query. Default mode stays "cascade" until the quality gate
(plan §8) PASSes on the full mail corpus -- flipping the default, and
deploying this to the running kb-query container, are separate follow-ups
for the operator; this task only adds the code path + tests
(docs/kb/modules/05-faza-mailowa-plan.md, §6, Krok 3).
2026-07-22 19:01:21 +02:00
|
|
|
async def hybrid_retrieve(
|
|
|
|
|
conn: asyncpg.Connection,
|
|
|
|
|
query_vector: str,
|
|
|
|
|
summary_model: str = DEFAULT_SUMMARY_MODEL,
|
|
|
|
|
summaryless_sources: tuple[str, ...] = DEFAULT_SUMMARYLESS_SOURCES,
|
|
|
|
|
n: int = DEFAULT_N,
|
|
|
|
|
k: int = DEFAULT_K,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""Cascade (for summarized sources) merged with a direct chunk scan over
|
|
|
|
|
`summaryless_sources` (plan §6 decision 6) -- mail envelopes never reach the cascade's
|
|
|
|
|
stage 1, so this is the only path today that can surface them.
|
|
|
|
|
|
|
|
|
|
Both branches share the same query embedding/cosine space, so the merge is a plain sort by
|
|
|
|
|
`dist`, then truncate to top-k -- no re-normalization needed (plan §6 decision 6 rationale).
|
|
|
|
|
Each returned chunk's `source` is overwritten to `"hybrid"` regardless of which branch it
|
|
|
|
|
came from, matching `flat`/`cascade`'s convention that `source` names the retrieval mode,
|
|
|
|
|
not an internal sub-path.
|
|
|
|
|
"""
|
|
|
|
|
cascade = await cascade_retrieve(conn, query_vector, summary_model, n=n, k=k)
|
|
|
|
|
|
|
|
|
|
mail_rows = await conn.fetch(
|
|
|
|
|
"SELECT c.envelope_id, c.chunk_index, c.text, c.embedding <=> $1::vector AS dist "
|
|
|
|
|
"FROM document_chunk c JOIN envelope e ON e.id = c.envelope_id "
|
|
|
|
|
"WHERE e.source = ANY($2::text[]) AND c.excluded_reason IS NULL "
|
|
|
|
|
"AND c.embedding IS NOT NULL ORDER BY c.embedding <=> $1::vector LIMIT $3",
|
|
|
|
|
query_vector, list(summaryless_sources), k,
|
|
|
|
|
)
|
|
|
|
|
mail_chunks = [
|
|
|
|
|
{
|
|
|
|
|
"envelope_id": r["envelope_id"],
|
|
|
|
|
"chunk_index": r["chunk_index"],
|
|
|
|
|
"text": r["text"],
|
|
|
|
|
"dist": r["dist"],
|
|
|
|
|
"source": "hybrid",
|
|
|
|
|
}
|
|
|
|
|
for r in mail_rows
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
merged = sorted(
|
|
|
|
|
[{**c, "source": "hybrid"} for c in cascade["chunks"]] + mail_chunks,
|
|
|
|
|
key=lambda c: c["dist"],
|
|
|
|
|
)[:k]
|
|
|
|
|
return {"stage1_summaries": cascade["stage1_summaries"], "chunks": merged}
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 16:06:02 +02:00
|
|
|
async def flat_query(
|
|
|
|
|
conn: asyncpg.Connection,
|
|
|
|
|
session: aiohttp.ClientSession,
|
|
|
|
|
ollama_url: str,
|
|
|
|
|
query_text: str,
|
|
|
|
|
embed_model: str = DEFAULT_EMBED_MODEL,
|
|
|
|
|
k: int = DEFAULT_K,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""query_text -> flat chunk hits. One Ollama embed call, one SQL query."""
|
|
|
|
|
embedding, _elapsed = await embed_chunk(session, ollama_url, embed_model, query_text)
|
|
|
|
|
chunks = await flat_retrieve(conn, _vector_literal(embedding), k=k)
|
|
|
|
|
return {"query": query_text, "k": k, "chunks": chunks}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def cascade_query(
|
|
|
|
|
conn: asyncpg.Connection,
|
|
|
|
|
session: aiohttp.ClientSession,
|
|
|
|
|
ollama_url: str,
|
|
|
|
|
query_text: str,
|
|
|
|
|
summary_model: str = DEFAULT_SUMMARY_MODEL,
|
|
|
|
|
embed_model: str = DEFAULT_EMBED_MODEL,
|
|
|
|
|
n: int = DEFAULT_N,
|
|
|
|
|
k: int = DEFAULT_K,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""query_text -> cascade chunk hits, plus the stage-1 envelope pre-filter (needed for the
|
|
|
|
|
gate's diagnosis order, plan §6.2: "N too small" is diagnosed by looking at stage 1).
|
|
|
|
|
|
|
|
|
|
Same query embedding as `flat_query`'s single Ollama call -- the cascade's only added
|
|
|
|
|
cost over the flat path is one extra SQL query (stage 1), never an extra embed.
|
|
|
|
|
"""
|
|
|
|
|
embedding, _elapsed = await embed_chunk(session, ollama_url, embed_model, query_text)
|
|
|
|
|
result = await cascade_retrieve(conn, _vector_literal(embedding), summary_model, n=n, k=k)
|
|
|
|
|
result["query"] = query_text
|
|
|
|
|
result["n"] = n
|
|
|
|
|
result["k"] = k
|
|
|
|
|
return result
|
feat(kb-retrieval,kb-query): add hybrid retrieval mode (faza mailowa Krok 3)
Mail (gmail) envelopes never get a document_summary (Decyzja 6 -- a mail
"summary" would usually be longer than the mail itself), so they're invisible
to the cascade's stage-1 pre-filter. hybrid_retrieve runs the existing
cascade for summarized sources (paperless) and, in parallel, a direct chunk
scan restricted to summaryless_sources (gmail), merging both by dist -- same
embedder/cosine space, so the merge is a plain sort, no re-normalization.
hybrid_query mirrors cascade_query (one shared query embed).
kb-query: mode pattern extended to ^(cascade|flat|hybrid)$, /search routes
"hybrid" to hybrid_query. Default mode stays "cascade" until the quality gate
(plan §8) PASSes on the full mail corpus -- flipping the default, and
deploying this to the running kb-query container, are separate follow-ups
for the operator; this task only adds the code path + tests
(docs/kb/modules/05-faza-mailowa-plan.md, §6, Krok 3).
2026-07-22 19:01:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def hybrid_query(
|
|
|
|
|
conn: asyncpg.Connection,
|
|
|
|
|
session: aiohttp.ClientSession,
|
|
|
|
|
ollama_url: str,
|
|
|
|
|
query_text: str,
|
|
|
|
|
summary_model: str = DEFAULT_SUMMARY_MODEL,
|
|
|
|
|
embed_model: str = DEFAULT_EMBED_MODEL,
|
|
|
|
|
summaryless_sources: tuple[str, ...] = DEFAULT_SUMMARYLESS_SOURCES,
|
|
|
|
|
n: int = DEFAULT_N,
|
|
|
|
|
k: int = DEFAULT_K,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""query_text -> hybrid chunk hits (cascade + direct mail scan, merged by dist).
|
|
|
|
|
|
|
|
|
|
Same single query embedding as `flat_query`/`cascade_query` -- hybrid's only added cost
|
|
|
|
|
over cascade is one extra SQL query (the mail branch), never an extra Ollama call.
|
|
|
|
|
"""
|
|
|
|
|
embedding, _elapsed = await embed_chunk(session, ollama_url, embed_model, query_text)
|
|
|
|
|
result = await hybrid_retrieve(
|
|
|
|
|
conn, _vector_literal(embedding), summary_model, summaryless_sources, n=n, k=k
|
|
|
|
|
)
|
|
|
|
|
result["query"] = query_text
|
|
|
|
|
result["n"] = n
|
|
|
|
|
result["k"] = k
|
|
|
|
|
return result
|