feat(rag): lite-generated KB overview in the system prompt — stored single row, regenerated on import, <knowledge_base> section in HIGH+LOW prompts
This commit is contained in:
@@ -19,13 +19,16 @@ from fastapi.testclient import TestClient
|
||||
from app.api import chat as chat_api
|
||||
from app.config import Settings
|
||||
from app.main import app as fastapi_app
|
||||
from app.models import Document, QueryLog
|
||||
from app.models import Document, KbOverview, QueryLog
|
||||
from app.rag.llm import StreamPiece
|
||||
from app.rag.retriever import RetrievedChunk, weak_hit_titles
|
||||
from app.rag.suggestions import MAX_SUGGESTIONS, derive_suggestions
|
||||
|
||||
ANSWER = "I haven't done anything like that — try one of these instead!"
|
||||
|
||||
#: A small KB outline standing in for the lite-generated one (phase 31).
|
||||
KB_OVERVIEW = "- Homelab\n - Kubernetes (k3s)\n- Deployments\n - Borg backups"
|
||||
|
||||
|
||||
def _settings(threshold: float = 0.30) -> Settings:
|
||||
return Settings(
|
||||
@@ -236,6 +239,87 @@ def test_no_summary_chunks_yields_zero_summary_hits() -> None:
|
||||
assert plan_low.summary_hits == 0
|
||||
|
||||
|
||||
# ---------- KB overview (phase 31: <knowledge_base> section + kb_chars) ----------
|
||||
|
||||
|
||||
def test_plan_turn_high_injects_kb_overview() -> None:
|
||||
"""HIGH branch: the stored outline lands in the prompt between
|
||||
``<relevance>`` and ``<tuning>`` (or the ``<documents>`` body with no
|
||||
notes), and ``kb_chars`` records the outline's length."""
|
||||
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_CONTENT")
|
||||
plan = chat_api.plan_turn(
|
||||
[_chunk(doc, 0.90)], _settings(threshold=0.30), kb_overview=KB_OVERVIEW
|
||||
)
|
||||
assert plan.deflected is False
|
||||
assert plan.kb_chars == len(KB_OVERVIEW)
|
||||
prompt = plan.system_prompt
|
||||
assert "<knowledge_base>" in prompt
|
||||
assert KB_OVERVIEW in prompt
|
||||
i_rel = prompt.index("<relevance>HIGH</relevance>")
|
||||
i_kb = prompt.index("<knowledge_base>")
|
||||
i_docs = prompt.index("<documents>")
|
||||
assert i_rel < i_kb < i_docs
|
||||
|
||||
|
||||
def test_plan_turn_high_kb_section_ordered_before_tuning() -> None:
|
||||
"""Both sections present: ``<relevance>`` → ``<knowledge_base>`` →
|
||||
``<tuning>`` → ``<documents>`` (the locked phase-31 order)."""
|
||||
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_CONTENT")
|
||||
plan = chat_api.plan_turn(
|
||||
[_chunk(doc, 0.90)],
|
||||
_settings(threshold=0.30),
|
||||
notes=["be concise"],
|
||||
kb_overview=KB_OVERVIEW,
|
||||
)
|
||||
prompt = plan.system_prompt
|
||||
i_rel = prompt.index("<relevance>HIGH</relevance>")
|
||||
i_kb = prompt.index("<knowledge_base>")
|
||||
i_kb_close = prompt.index("</knowledge_base>")
|
||||
i_tuning = prompt.index("<tuning>")
|
||||
i_docs = prompt.index("<documents>")
|
||||
assert i_rel < i_kb < i_kb_close < i_tuning < i_docs
|
||||
assert plan.kb_chars == len(KB_OVERVIEW)
|
||||
|
||||
|
||||
def test_plan_turn_low_injects_kb_overview() -> None:
|
||||
"""LOW (deflected) branch: the outline is injected there too, ahead
|
||||
of the DEFLECT_MODE body, and document content stays excluded."""
|
||||
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_NEVER_SENT")
|
||||
plan = chat_api.plan_turn(
|
||||
[_chunk(doc, 0.10)], _settings(threshold=0.30), kb_overview=KB_OVERVIEW
|
||||
)
|
||||
assert plan.deflected is True
|
||||
assert plan.kb_chars == len(KB_OVERVIEW)
|
||||
prompt = plan.system_prompt
|
||||
assert "<knowledge_base>" in prompt
|
||||
assert KB_OVERVIEW in prompt
|
||||
i_rel = prompt.index("<relevance>LOW</relevance>")
|
||||
i_kb = prompt.index("<knowledge_base>")
|
||||
i_mode = prompt.index("DEFLECT_MODE")
|
||||
assert i_rel < i_kb < i_mode
|
||||
assert "TALOS_DOC_NEVER_SENT" not in prompt # titles only, still
|
||||
|
||||
|
||||
def test_plan_turn_empty_overview_keeps_prompt_and_zero_kb_chars() -> None:
|
||||
"""No outline (None/empty/blank) → ``kb_chars == 0`` and a prompt
|
||||
byte-identical to the no-overview build in both branches."""
|
||||
high_doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_CONTENT")
|
||||
low_doc = _doc("Backup Strategy", "BACKUP_DOC_CONTENT")
|
||||
for chunks, kb in (
|
||||
([_chunk(high_doc, 0.90)], None),
|
||||
([_chunk(high_doc, 0.90)], ""),
|
||||
([_chunk(high_doc, 0.90)], " \n\t "),
|
||||
([_chunk(low_doc, 0.10)], None),
|
||||
([_chunk(low_doc, 0.10)], ""),
|
||||
):
|
||||
plan = chat_api.plan_turn(chunks, _settings(threshold=0.30), kb_overview=kb)
|
||||
baseline = chat_api.plan_turn(chunks, _settings(threshold=0.30))
|
||||
assert plan.kb_chars == 0
|
||||
assert baseline.kb_chars == 0
|
||||
assert plan.system_prompt == baseline.system_prompt # byte-identical
|
||||
assert "<knowledge_base>" not in plan.system_prompt
|
||||
|
||||
|
||||
# ---------- prompt content (LOW vs HIGH) ----------
|
||||
|
||||
|
||||
@@ -360,12 +444,15 @@ class _FakeSession:
|
||||
"""Stands in for the DB session: records the QueryLog row it is given.
|
||||
|
||||
``scalars`` always yields no steering notes (phase 15) so the chat
|
||||
turn's ``load_steering_notes`` call stays a no-op here.
|
||||
turn's ``load_steering_notes`` call stays a no-op here, and ``get``
|
||||
returns the single ``kb_overview`` row when one is configured
|
||||
(phase 31) — ``None`` by default, i.e. no stored outline.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, kb_overview: str = "") -> None:
|
||||
self.added: list[Any] = []
|
||||
self.commits = 0
|
||||
self.kb_overview = kb_overview
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
self.added.append(obj)
|
||||
@@ -376,6 +463,11 @@ class _FakeSession:
|
||||
def scalars(self, _stmt: Any) -> _FakeSteeringResult:
|
||||
return _FakeSteeringResult()
|
||||
|
||||
def get(self, model: Any, pk: Any) -> Any:
|
||||
if model is KbOverview and self.kb_overview:
|
||||
return KbOverview(id=1, content=self.kb_overview)
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gate_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[tuple[_FakeSession, _CannedLLM]]:
|
||||
@@ -479,3 +571,55 @@ def test_endpoint_score_at_threshold_answers(
|
||||
assert isinstance(row, QueryLog)
|
||||
assert row.deflected is False
|
||||
assert row.top_score == pytest.approx(0.30)
|
||||
|
||||
|
||||
# ---------- endpoint: KB overview row (phase 31) ----------
|
||||
|
||||
|
||||
def test_endpoint_stored_kb_row_injected_into_system_prompt(
|
||||
client: TestClient,
|
||||
gate_env: tuple[_FakeSession, _CannedLLM],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A non-empty ``kb_overview`` row (read via one PK lookup) reaches
|
||||
the LLM's system prompt in both modes, and the per-turn log line
|
||||
records ``kb_chars=N`` (PLAN §9)."""
|
||||
session, llm = gate_env
|
||||
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_SENT")
|
||||
monkeypatch.setattr(chat_api, "retrieve", _fake_retriever([_chunk(doc, 0.30)]))
|
||||
|
||||
session.kb_overview = f" {KB_OVERVIEW} " # the loader trims it
|
||||
with caplog.at_level("INFO", logger="app.chat"):
|
||||
_ask(client, "How is my Kubernetes cluster set up?")
|
||||
|
||||
(system, _user) = llm.seen[0][0], llm.seen[0][1]
|
||||
assert "<knowledge_base>" in system["content"]
|
||||
assert KB_OVERVIEW in system["content"]
|
||||
assert system["content"].index("<relevance>HIGH</relevance>") < system["content"].index(
|
||||
"<knowledge_base>"
|
||||
) < system["content"].index("<documents>")
|
||||
log_lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
|
||||
assert log_lines and f"kb_chars={len(KB_OVERVIEW)}" in log_lines[-1]
|
||||
|
||||
|
||||
def test_endpoint_no_kb_row_prompt_unchanged(
|
||||
client: TestClient,
|
||||
gate_env: tuple[_FakeSession, _CannedLLM],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""No ``kb_overview`` row → the section is absent (byte-identical to
|
||||
the pre-phase prompt) and the log line records ``kb_chars=0``."""
|
||||
session, llm = gate_env
|
||||
assert session.kb_overview == "" # fixture default: no stored row
|
||||
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_SENT")
|
||||
monkeypatch.setattr(chat_api, "retrieve", _fake_retriever([_chunk(doc, 0.30)]))
|
||||
|
||||
with caplog.at_level("INFO", logger="app.chat"):
|
||||
_ask(client, "How is my Kubernetes cluster set up?")
|
||||
|
||||
(system, _user) = llm.seen[0][0], llm.seen[0][1]
|
||||
assert "<knowledge_base>" not in system["content"]
|
||||
log_lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
|
||||
assert log_lines and "kb_chars=0" in log_lines[-1]
|
||||
|
||||
Reference in New Issue
Block a user