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:
2026-08-25 20:22:51 -04:00
parent 572a4190a6
commit 0654b304e1
23 changed files with 2276 additions and 34 deletions
+63 -4
View File
@@ -27,6 +27,17 @@ caches) is skipped, along with non-content dirs (``.venv``,
``build``). Re-runs are cheap: files are diffed by sha256 and unchanged
ones are not re-embedded; ``--prune`` also drops documents whose files no
longer match the format filter.
After a run that **changed** the knowledge base (at least one document
added or updated — or no outline stored yet), the single ``kb_overview``
row is regenerated with the ``lite`` model (phase 31): the plain-text
outline of the KB's basic categories that every chat turn injects into
the system prompt as ``<knowledge_base>``. The regeneration is
**best-effort and change-gated** — unchanged re-imports and ``--limit``
debug runs never burn a ``lite`` call, and a ``lite`` failure only
reports ``overview=failed`` on the summary line: the import's exit code
is about files, and the previous outline stays (an old outline is better
than none).
"""
from __future__ import annotations
@@ -40,8 +51,11 @@ from pathlib import Path
from app.config import Settings, get_settings
from app.core.debugging import configure_debugging
from app.core.logging import configure_logging
from app.rag.importer import import_sources
from app.db import SessionLocal
from app.models import KbOverview
from app.rag.importer import ImportSummary, import_sources
from app.rag.llm import LLMClient
from app.rag.overview import regenerate_overview
from scripts.git_sync import GitSyncError, clone_or_pull
logger = logging.getLogger("scripts.import_docs")
@@ -116,6 +130,18 @@ def _resolve_sources(cli_sources: list[Path] | None, settings: Settings) -> list
return [path.expanduser() for path in DEFAULT_SOURCES]
def _overview_row_exists() -> bool:
"""Whether the single ``kb_overview`` row (id = 1) is present.
One indexed PK lookup (phase 31, task 04): a missing outline after an
unchanged re-import — e.g. the first run after migration 0005 — still
gets a fresh outline, while a present one is left untouched until the
KB actually changes.
"""
with SessionLocal() as session:
return session.get(KbOverview, 1) is not None
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
settings = get_settings()
@@ -144,16 +170,49 @@ def main(argv: list[str] | None = None) -> int:
return 1
llm = LLMClient()
summary = asyncio.run(import_sources(sources, llm, prune=args.prune, limit=args.limit))
async def _run() -> tuple[ImportSummary, str]:
"""Import, then (change-gated) refresh the stored KB overview.
One event loop, one ``LLMClient`` (phase 31, task 04): the
outline that every chat prompt injects as ``<knowledge_base>`` is
regenerated only when this run added/updated at least one
document — or when no outline exists yet after a walk that
actually saw files (e.g. the first run after migration 0005).
``--limit`` debug runs (an incomplete walk must not rewrite the
outline — mirrors the ``--prune``-with-``--limit`` guard) and
unchanged re-imports never burn a ``lite`` call, and a ``lite``
failure only flips the status token (``failed``) — the import's
exit code is unchanged.
"""
summary = await import_sources(sources, llm, prune=args.prune, limit=args.limit)
if args.limit is not None:
logger.info("overview: skipped (--limit)")
return summary, "skipped"
if summary.added + summary.updated == 0:
if summary.files == 0:
logger.info("overview: skipped (nothing imported)")
return summary, "skipped"
if _overview_row_exists():
logger.info("overview: skipped (KB unchanged)")
return summary, "skipped"
# No outline yet after an unchanged re-import (e.g. the first
# run after migration 0005) — fall through and generate one.
ok = await regenerate_overview(llm)
return summary, "updated" if ok else "failed"
summary, overview_status = asyncio.run(_run())
print(
f"import_docs: files={summary.files} added={summary.added} "
f"updated={summary.updated} unchanged={summary.unchanged} "
f"pruned={summary.pruned} errors={summary.errors} chunks={summary.chunks} "
f"embed_batches={summary.embed_batches} summaries={summary.summaries} "
f"summary_errors={summary.summary_errors} formats={summary.format_counts()}"
f"summary_errors={summary.summary_errors} formats={summary.format_counts()} "
f"overview={overview_status}"
)
# Non-zero if any file failed, so cron/CI notice — the rest of the KB
# was imported and the failed files are retried on the next run.
# was imported and the failed files are retried on the next run. The
# overview is best-effort: a failed outline never changes the exit code.
return 1 if summary.errors else 0