phase: 94_ls_tree_drilldown
All green. Verification complete. **Phase 94 — `ls` drill-down tree: final verification pass (all 5 tasks were already complete; verified, nothing to fix)** - Verified `ls` 3-level tree (`app/rag/agent.py`): `ls()` sources + summaries, `ls(source)`/`ls(source/folder)` drill-down, 50-line file cap + grep-pointer note, NOT-A-FOLDER teaching refusal - Verified `folder_summaries` (migration 0017, model, `app/rag/folder_summaries.py` generator: `FOLDER_SUMMARY_MODE` marker, fail-soft per folder, ≥2-doc scope + prune) wired change-gated in both sync paths - Verified 10-turn fixture battery verdict recorded in `TOOL_CALLING_TESTING.md` §9 (2026-09-11): turbo PASS 19/19 contract, 98.7 s (−12.5…−13.2 % vs baseline); lite PASS 18/18, 43.6 s (+7.7 %) — accuracy at/above baseline, gate met - `uv run pytest --cov=app --cov-report=term-missing` → 1939 passed, 0 failed; TOTAL coverage **99 %** (folder_summaries.py 100 %) - `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings - E2E in isolation: `test_ls_tree_drilldown.py` 3 passed; `test_agent_document_tools` 4, `test_agent_unlimited_tools` 4, `test_harness_aligned_tools` 3, `test_search_tool` 3, `test_grep_regex_teaching` 2, `test_response_to_docs` 4 — all passed (read/grep contracts untouched) - Dedicated folder-summary tests (fail-soft, prune, both sync paths, migration): 46 passed - Completion criteria: all 6 met; working tree holds only phase-94 changes (commit left to harness per protocol) **Next pending phase:** `95_read_truncation_cap`
This commit is contained in:
@@ -82,17 +82,18 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.chat import plan_turn
|
||||
from app.api.steering import load_steering_notes
|
||||
from app.config import Settings, get_settings
|
||||
from app.db import SessionLocal, db_available
|
||||
from app.models import Document
|
||||
from app.rag.agent import (
|
||||
CORRECTION_INSTRUCTION,
|
||||
AgentHolder,
|
||||
MalformedReplyError,
|
||||
find_document,
|
||||
list_catalog,
|
||||
list_source_names,
|
||||
run_agent,
|
||||
)
|
||||
@@ -113,6 +114,19 @@ logger = logging.getLogger("agent_realmodel_check")
|
||||
#: Repo-relative fixture dump (written by scripts.load_test_kb).
|
||||
DEFAULT_DUMP_PATH = Path("tests/fixtures/test_kb.dump.sql")
|
||||
|
||||
|
||||
def _catalog(db) -> list[tuple[str, str, str]]:
|
||||
"""The full catalog as ``(source, path, title)`` in ``(source, path)``
|
||||
order — the battery builder's pre-phase-94 ``list_catalog`` contract,
|
||||
kept local to this script (phase 94 deleted the agent accessor: the
|
||||
drill-down ``ls`` lists one bounded source level at a time)."""
|
||||
rows = db.execute(
|
||||
select(Document.source, Document.path, Document.title).order_by(
|
||||
Document.source, Document.path
|
||||
)
|
||||
).all()
|
||||
return [(source, path, title) for source, path, title in rows]
|
||||
|
||||
#: The per-turn line truncates the question at this width (the locked
|
||||
#: format prints ``turn NN | emitted=E executed=X cap=Y|N | <question>``).
|
||||
QUESTION_DISPLAY_WIDTH = 40
|
||||
@@ -263,7 +277,7 @@ def check_preconditions(
|
||||
return 2
|
||||
return None
|
||||
with SessionLocal() as db:
|
||||
catalog = list_catalog(db)
|
||||
catalog = _catalog(db)
|
||||
if len(catalog) < 2:
|
||||
print(
|
||||
f"precondition failed: catalog holds {len(catalog)} document(s) "
|
||||
@@ -443,8 +457,33 @@ async def _run_deflected(
|
||||
)
|
||||
|
||||
|
||||
def _ls_folder_prefixes(catalog: set[tuple[str, str]]) -> dict[str, set[str]]:
|
||||
"""The existing ``ls`` folders per source (the phase-94 drill-down
|
||||
contract): for each source, the set of source-relative folder
|
||||
prefixes — every slash-boundary prefix of its indexed paths.
|
||||
|
||||
Mirrors the ``00_phase.md`` existence rule that
|
||||
``app.rag.agent._execute_tool`` applies: a folder ``F`` exists ⟺
|
||||
some indexed path of the source starts with ``F + "/"`` — a
|
||||
document's OWN path is never a folder, so the document's path itself
|
||||
is not in the set (``ls`` of a file path is a refusal, as is a bare
|
||||
folder name missing its source prefix).
|
||||
"""
|
||||
folders: dict[str, set[str]] = {}
|
||||
for source, path in catalog:
|
||||
folder = path[: path.rfind("/")] if "/" in path else ""
|
||||
while folder:
|
||||
folders.setdefault(source, set()).add(folder)
|
||||
folder = folder[: folder.rfind("/")] if "/" in folder else ""
|
||||
return folders
|
||||
|
||||
|
||||
def classify_call(
|
||||
name: str, args: dict[str, Any], catalog: set[tuple[str, str]], sources: set[str]
|
||||
name: str,
|
||||
args: dict[str, Any],
|
||||
catalog: set[tuple[str, str]],
|
||||
sources: set[str],
|
||||
ls_folders: dict[str, set[str]],
|
||||
) -> bool:
|
||||
"""Contract correctness of ONE emitted call (the tool-calling
|
||||
accuracy metric, 2026-09-04 controlled methodology).
|
||||
@@ -460,12 +499,25 @@ def classify_call(
|
||||
re-read 15/15 across copy variants — is documented in
|
||||
``TOOL_CALLING_TESTING.md``). The classification mirrors
|
||||
``app.rag.agent._execute_tool``'s resolution rules gate-side (no
|
||||
app-code changes for measurement).
|
||||
app-code changes for measurement) — including the phase-94 ``ls``
|
||||
revision (owner-permitted tool-surface change, ``00_phase.md``):
|
||||
``ls(path)`` is contract-correct for ``""`` (the synced sources),
|
||||
a registered source name (its root folder), or a ``source/folder``
|
||||
path naming an EXISTING folder (``ls_folders`` — the drill-down);
|
||||
an unknown first segment, a bare folder name (no source prefix), or
|
||||
a folder matching no indexed prefix is the violation.
|
||||
"""
|
||||
if name == "ls":
|
||||
raw = args.get("path")
|
||||
scope = raw.strip() if isinstance(raw, str) else ""
|
||||
return scope == "" or scope in sources
|
||||
if not scope:
|
||||
return True # the top level (the synced sources)
|
||||
source, _, rest = scope.partition("/")
|
||||
if source not in sources:
|
||||
return False # unknown first segment (the phase-72 incident class)
|
||||
if not rest:
|
||||
return True # a registered source's root folder
|
||||
return rest in ls_folders.get(source, ())
|
||||
if name == "read":
|
||||
raw = args.get("path")
|
||||
arg = raw.strip() if isinstance(raw, str) else ""
|
||||
@@ -498,10 +550,13 @@ def score_contract(
|
||||
and source names. Per-turn counts are attached on each
|
||||
:class:`TurnResult` as ``_contract_ok`` (measurement state, not a
|
||||
dataclass field — the display line stays the locked format)."""
|
||||
ls_folders = _ls_folder_prefixes(catalog)
|
||||
ok = 0
|
||||
for turn in turns:
|
||||
turn_ok = sum(
|
||||
1 for name, args in turn.calls if classify_call(name, args, catalog, sources)
|
||||
1
|
||||
for name, args in turn.calls
|
||||
if classify_call(name, args, catalog, sources, ls_folders)
|
||||
)
|
||||
turn._contract_ok = turn_ok # type: ignore[attr-defined]
|
||||
ok += turn_ok
|
||||
@@ -763,7 +818,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
else:
|
||||
with SessionLocal() as db:
|
||||
catalog = list_catalog(db)
|
||||
catalog = _catalog(db)
|
||||
s2, p2, _t2 = catalog[1]
|
||||
d2 = find_document(db, s2, p2)
|
||||
d2_content = d2.content if d2 is not None else ""
|
||||
@@ -794,7 +849,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
# source names (the KB is static across the run — the restore, when
|
||||
# any, happened before the battery).
|
||||
with SessionLocal() as db:
|
||||
catalog_set = set((s, p) for s, p, _t in list_catalog(db))
|
||||
catalog_set = set((s, p) for s, p, _t in _catalog(db))
|
||||
sources_set = set(list_source_names(db))
|
||||
score_contract(turns, catalog_set, sources_set)
|
||||
passed, conditions = evaluate(
|
||||
|
||||
+93
-15
@@ -54,6 +54,20 @@ 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).
|
||||
|
||||
The stored folder summaries (phase 94 — the drill-down ``ls``'s
|
||||
per-level descriptions, the ``folder_summaries`` table) regenerate in
|
||||
the same run under the same gate: a changed KB (added + updated > 0),
|
||||
or an empty table after an unchanged walk (the first full run after
|
||||
migration 0017, or after a ``--limit`` first walk that skipped them).
|
||||
Same contract — **best-effort, per-folder fail-soft**: a ``lite``
|
||||
failure keeps the failed folders' previous rows and only counts into
|
||||
the stats; ``--limit`` debug runs skip them entirely (never burning a
|
||||
``lite`` call). The stats land on the summary line as
|
||||
``folder_summaries=<generated>/<failed>/<pruned>`` (or
|
||||
``folder_summaries=skipped`` when the gate did not fire), and the rows
|
||||
commit in the run's own short-lived session (the phase-53 convention —
|
||||
a failed commit rolls them back with it).
|
||||
|
||||
A run that **changed** the knowledge base (added + updated + pruned > 0
|
||||
— phase 53, task 02) also advances the single-row ``sources_meta``
|
||||
version exactly once (``sources_version=<n>`` on the summary line): the
|
||||
@@ -78,6 +92,7 @@ from app.core.debugging import configure_debugging
|
||||
from app.core.logging import configure_logging
|
||||
from app.db import SessionLocal
|
||||
from app.models import KbOverview
|
||||
from app.rag.folder_summaries import folder_summary_table_empty, generate_folder_summaries
|
||||
from app.rag.git_sources import effective_sources
|
||||
from app.rag.importer import ImportSummary, import_sources
|
||||
from app.rag.llm import LLMClient
|
||||
@@ -216,6 +231,20 @@ def _overview_row_exists() -> bool:
|
||||
return session.get(KbOverview, 1) is not None
|
||||
|
||||
|
||||
def _folder_summaries_table_empty() -> bool:
|
||||
"""Whether the ``folder_summaries`` table holds any row (phase 94).
|
||||
|
||||
The ``_overview_row_exists`` pattern extended to a table-emptiness
|
||||
check (one bounded ``LIMIT 1`` probe): an empty table after an
|
||||
unchanged re-import — e.g. the first full run after migration 0017,
|
||||
or after a ``--limit`` first walk that skipped generation — still
|
||||
gets a fresh batch of folder summaries, while a populated table is
|
||||
left untouched until the KB actually changes.
|
||||
"""
|
||||
with SessionLocal() as session:
|
||||
return folder_summary_table_empty(session)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
settings = get_settings()
|
||||
@@ -246,9 +275,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
llm = LLMClient()
|
||||
|
||||
async def _run() -> tuple[ImportSummary, str, str]:
|
||||
"""Import, then (change-gated) advance the sources version and
|
||||
refresh the stored KB overview.
|
||||
async def _run() -> tuple[ImportSummary, str, str, dict[str, int] | None]:
|
||||
"""Import, then (change-gated) advance the sources version,
|
||||
refresh the stored KB overview, and regenerate the stored folder
|
||||
summaries.
|
||||
|
||||
One event loop, one ``LLMClient`` (phase 31, task 04): the
|
||||
outline that every chat prompt injects as ``<knowledge_base>`` is
|
||||
@@ -268,6 +298,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
deliberately broader than the overview's). ``--limit`` debug
|
||||
runs and unchanged re-runs never bump. The returned token is
|
||||
the new version, or ``"skipped"``.
|
||||
|
||||
The folder summaries (phase 94, task 02) follow the same gate
|
||||
— a changed KB, or an empty ``folder_summaries`` table after an
|
||||
unchanged walk (the first full run after migration 0017, or
|
||||
after a ``--limit`` first walk) — with per-folder fail-soft
|
||||
inside the generator (a ``lite`` failure keeps the failed
|
||||
folders' previous rows). The generator only flushes: this
|
||||
run's own short-lived session commits (the phase-53
|
||||
convention), and the stats land on the summary line as
|
||||
``folder_summaries=<generated>/<failed>/<pruned>``
|
||||
(``None`` — rendered ``skipped`` — when the gate did not fire).
|
||||
"""
|
||||
summary = await import_sources(
|
||||
sources, llm, prune=args.prune, limit=args.limit,
|
||||
@@ -294,32 +335,69 @@ def main(argv: list[str] | None = None) -> int:
|
||||
sources_version = "skipped"
|
||||
logger.info("sources: version bump skipped (KB unchanged)")
|
||||
if args.limit is not None:
|
||||
# An incomplete walk must never rewrite the outline or the
|
||||
# folder summaries (the --limit skip, mirrored above for the
|
||||
# sources version).
|
||||
logger.info("overview: skipped (--limit)")
|
||||
return summary, "skipped", sources_version
|
||||
if summary.added + summary.updated == 0:
|
||||
return summary, "skipped", sources_version, None
|
||||
changed = summary.added + summary.updated > 0
|
||||
overview_due = changed
|
||||
folders_due = changed
|
||||
if not changed:
|
||||
if summary.files == 0:
|
||||
logger.info("overview: skipped (nothing imported)")
|
||||
return summary, "skipped", sources_version
|
||||
if _overview_row_exists():
|
||||
return summary, "skipped", sources_version, None
|
||||
# The unchanged-walk first-run triggers: the overview when
|
||||
# no row exists yet (the first run after migration 0005),
|
||||
# the folder summaries when the table is empty (the first
|
||||
# full run after migration 0017, or after a --limit first
|
||||
# walk that skipped them).
|
||||
overview_due = not _overview_row_exists()
|
||||
folders_due = _folder_summaries_table_empty()
|
||||
if not overview_due and not folders_due:
|
||||
logger.info("overview: skipped (KB unchanged)")
|
||||
return summary, "skipped", sources_version
|
||||
# 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", sources_version
|
||||
return summary, "skipped", sources_version, None
|
||||
overview_status = "skipped"
|
||||
if overview_due:
|
||||
ok = await regenerate_overview(llm)
|
||||
overview_status = "updated" if ok else "failed"
|
||||
else:
|
||||
logger.info("overview: skipped (KB unchanged)")
|
||||
# Phase 94 (task 02): the folder summaries — same event loop +
|
||||
# LLMClient (phase 31 convention), per-folder fail-soft inside
|
||||
# the generator (a ``lite`` failure never flips the exit code).
|
||||
# It only flushes — this run's own short-lived session commits
|
||||
# (the phase-53 convention), so a failed commit rolls the
|
||||
# summaries back with it.
|
||||
folder_stats: dict[str, int] | None = None
|
||||
if folders_due:
|
||||
session = SessionLocal()
|
||||
try:
|
||||
folder_stats = await generate_folder_summaries(session, llm)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
return summary, overview_status, sources_version, folder_stats
|
||||
|
||||
summary, overview_status, sources_version = asyncio.run(_run())
|
||||
summary, overview_status, sources_version, folder_stats = asyncio.run(_run())
|
||||
folder_token = (
|
||||
"skipped"
|
||||
if folder_stats is None
|
||||
else f"{folder_stats['generated']}/{folder_stats['failed']}/{folder_stats['pruned']}"
|
||||
)
|
||||
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"overview={overview_status} sources_version={sources_version}"
|
||||
f"overview={overview_status} sources_version={sources_version} "
|
||||
f"folder_summaries={folder_token}"
|
||||
)
|
||||
# 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. The
|
||||
# overview is best-effort: a failed outline never changes the exit code.
|
||||
# overview and the folder summaries are best-effort: a failed outline
|
||||
# or a failed folder batch never changes the exit code.
|
||||
return 1 if summary.errors else 0
|
||||
|
||||
|
||||
|
||||
+51
-11
@@ -15,23 +15,30 @@ guessable by any model. This script:
|
||||
``BOR_GIT_SOURCES`` env var),
|
||||
3. imports the fixture documents through the real pipeline
|
||||
(``import_sources`` — real chunking + real ``embed``-model
|
||||
embeddings; this is the ONLY step that burns model calls, and only
|
||||
at build time),
|
||||
embeddings; this is the only step that burns ``embed`` calls, and
|
||||
only at build time),
|
||||
4. stores the static KB overview + the sources-version row,
|
||||
5. prints a **retrieval report** for every fixture-battery question
|
||||
5. generates the sync-time **folder summaries** through the real
|
||||
generator (``app.rag.folder_summaries``) against the live ``lite``
|
||||
endpoint — the drill-down ``ls`` (phase 94) shows the stored rows,
|
||||
so the dump must carry them (the build always truncates first, so
|
||||
the sync paths' table-empty first-run trigger holds; a failed batch
|
||||
aborts the build — a summary-less dump would be a broken fixture),
|
||||
6. prints a **retrieval report** for every fixture-battery question
|
||||
(grounded or deflected, which documents would seed) — the battery
|
||||
must be all-grounded for the gate to exercise the tools,
|
||||
6. snapshots the resulting database state into
|
||||
7. snapshots the resulting database state into
|
||||
``tests/fixtures/test_kb.dump.sql`` — a data-only SQL script
|
||||
(TRUNCATE + one multi-row ``INSERT`` per app table, generated
|
||||
in-process — the same file runs in psql or psycopg, in one
|
||||
transaction) — and **verifies the snapshot by restoring it and
|
||||
comparing a per-table checksum**.
|
||||
|
||||
Re-run it only when the fixture documents, the chunker, or the
|
||||
embedding model change — everyday iterations restore the dump in
|
||||
sub-second time (``scripts/restore_test_kb`` / the gate's
|
||||
``--restore``), never re-embedding (see ``TOOL_CALLING_TESTING.md``).
|
||||
Re-run it only when the fixture documents, the chunker, the embedding
|
||||
model, or the folder-summary prompt (its ``lite`` output is baked into
|
||||
the dump) change — everyday iterations restore the dump in sub-second
|
||||
time (``scripts/restore_test_kb`` / the gate's ``--restore``), never
|
||||
re-embedding (see ``TOOL_CALLING_TESTING.md``).
|
||||
|
||||
Exit codes: **0** built + verified, **1** build/verification failure,
|
||||
**2** precondition failure.
|
||||
@@ -59,6 +66,7 @@ from app.models import (
|
||||
Chunk,
|
||||
DocDraft,
|
||||
Document,
|
||||
FolderSummary,
|
||||
GitSource,
|
||||
KbOverview,
|
||||
QueryLog,
|
||||
@@ -66,6 +74,7 @@ from app.models import (
|
||||
SourcesMeta,
|
||||
SteeringNote,
|
||||
)
|
||||
from app.rag.folder_summaries import generate_folder_summaries
|
||||
from app.rag.importer import import_sources
|
||||
from app.rag.llm import LLMClient
|
||||
from app.rag.retriever import retrieve
|
||||
@@ -111,6 +120,12 @@ _TABLES: tuple[tuple[str, type, tuple[str, ...]], ...] = (
|
||||
("doc_drafts", DocDraft, ("id", "token", "title", "path", "body",
|
||||
"status", "branch", "commit_sha", "created_at",
|
||||
"updated_at")),
|
||||
# Phase 94 (task 05): the sync-time folder summaries the drill-down
|
||||
# ``ls`` shows — generated against the live ``lite`` endpoint in
|
||||
# step 5 of :func:`_build` (the build truncates first, so the sync
|
||||
# paths' table-empty first-run trigger holds).
|
||||
("folder_summaries", FolderSummary, ("source", "folder_path",
|
||||
"summary", "updated_at")),
|
||||
)
|
||||
|
||||
|
||||
@@ -257,7 +272,31 @@ async def _build(kb_dir: Path, dump_path: Path) -> int:
|
||||
db.add(meta)
|
||||
db.commit()
|
||||
|
||||
# 4. Retrieval report (all battery questions must stay grounded).
|
||||
# 4. The sync-time folder summaries (phase 94, task 05): the
|
||||
# drill-down ``ls`` shows the stored rows, so the dump carries
|
||||
# them. Generated against the live ``lite`` endpoint through the
|
||||
# real sync-time generator — the build just truncated the table,
|
||||
# so the sync paths' table-empty first-run trigger holds (no
|
||||
# change-gate bookkeeping needed on a fresh build). The generator
|
||||
# only flushes; this build commits (the phase-53 convention the
|
||||
# sync paths follow). A failed batch aborts: the dump's folder
|
||||
# lines are part of the controlled fixture.
|
||||
with SessionLocal() as db:
|
||||
folder_stats = await generate_folder_summaries(db, llm)
|
||||
db.commit()
|
||||
logger.info(
|
||||
"load_test_kb: folder summaries generated=%d failed=%d pruned=%d",
|
||||
folder_stats["generated"], folder_stats["failed"], folder_stats["pruned"],
|
||||
)
|
||||
if folder_stats["failed"]:
|
||||
print(
|
||||
f"load_test_kb: {folder_stats['failed']} folder summary call(s) "
|
||||
"failed — the fixture dump needs the stored rows; check the LLM "
|
||||
"endpoint (BOR_LLM_SUMMARY_MODEL) and re-run"
|
||||
)
|
||||
return 1
|
||||
|
||||
# 5. Retrieval report (all battery questions must stay grounded).
|
||||
n_deflected = await _retrieval_report(llm, list(FIXTURE_BATTERY))
|
||||
if n_deflected:
|
||||
print(
|
||||
@@ -267,7 +306,7 @@ async def _build(kb_dir: Path, dump_path: Path) -> int:
|
||||
"question before running the gate."
|
||||
)
|
||||
|
||||
# 5. Snapshot (data-only) + round-trip verification.
|
||||
# 6. Snapshot (data-only) + round-trip verification.
|
||||
with SessionLocal() as db:
|
||||
before = {table: _table_checksum(db, table) for table, _m, _c in _TABLES}
|
||||
parts = [
|
||||
@@ -309,7 +348,8 @@ async def _build(kb_dir: Path, dump_path: Path) -> int:
|
||||
wall = time.monotonic() - started
|
||||
print(
|
||||
f"load_test_kb: ok — docs={summary.added} chunks={summary.chunks} "
|
||||
f"sources={len(FIXTURE_SOURCES)} dump={dump_path} "
|
||||
f"sources={len(FIXTURE_SOURCES)} folder_summaries={folder_stats['generated']} "
|
||||
f"dump={dump_path} "
|
||||
f"({dump_path.stat().st_size // 1024} KB, verified by round-trip) "
|
||||
f"in {wall:.1f}s"
|
||||
)
|
||||
|
||||
@@ -11,11 +11,13 @@ snapshot in **one transaction** through the app's own database URL
|
||||
(``BOR_DATABASE_URL``): no git clone of the homelab repo, no re-embedding,
|
||||
no ``lite``-model calls — the whole known state (documents, chunks +
|
||||
embeddings, the source registry rows, the KB overview, the sources
|
||||
version) lands in a fraction of a second, which is what makes a
|
||||
tool-calling iteration loop fast (see ``TOOL_CALLING_TESTING.md``):
|
||||
version, the stored folder summaries — the drill-down ``ls``'s rows,
|
||||
phase 94, task 05) lands in a fraction of a second, which is what makes
|
||||
a tool-calling iteration loop fast (see ``TOOL_CALLING_TESTING.md``):
|
||||
|
||||
uv run python -m scripts.restore_test_kb
|
||||
# restore_test_kb: ok in 0.41s (8 docs, 2 sources, 16 chunks)
|
||||
# restore_test_kb: ok in 0.41s (8 docs, 2 sources, 9 chunks,
|
||||
# 4 folder summaries, dump … KB)
|
||||
|
||||
The gate runs the same restore inline:
|
||||
``uv run python -m scripts.agent_realmodel_check --restore``.
|
||||
@@ -57,6 +59,10 @@ APP_TABLES: tuple[str, ...] = (
|
||||
"query_log",
|
||||
"saved_chats",
|
||||
"doc_drafts",
|
||||
# Phase 94 (task 05): the sync-time folder summaries the drill-down
|
||||
# ``ls`` shows — the dump carries the rows (built by
|
||||
# ``scripts.load_test_kb`` against the live ``lite`` endpoint).
|
||||
"folder_summaries",
|
||||
)
|
||||
|
||||
#: Repo-relative default dump location (the load script writes it there).
|
||||
@@ -71,6 +77,7 @@ class RestoreResult:
|
||||
docs: int
|
||||
sources: tuple[str, ...]
|
||||
chunks: int
|
||||
folders: int # stored folder summaries (phase 94)
|
||||
dump_bytes: int
|
||||
|
||||
|
||||
@@ -127,11 +134,13 @@ def restore_dump(dump: Path) -> RestoreResult:
|
||||
)
|
||||
)
|
||||
chunks = db.execute(text("select count(*) from chunks")).scalar_one()
|
||||
folders = db.execute(text("select count(*) from folder_summaries")).scalar_one()
|
||||
return RestoreResult(
|
||||
seconds=seconds,
|
||||
docs=docs,
|
||||
sources=sources,
|
||||
chunks=chunks,
|
||||
folders=folders,
|
||||
dump_bytes=dump.stat().st_size,
|
||||
)
|
||||
|
||||
@@ -169,7 +178,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(
|
||||
f"restore_test_kb: ok in {result.seconds:.2f}s "
|
||||
f"({result.docs} docs, {len(result.sources)} sources, "
|
||||
f"{result.chunks} chunks, dump {result.dump_bytes // 1024} KB)"
|
||||
f"{result.chunks} chunks, {result.folders} folder summaries, "
|
||||
f"dump {result.dump_bytes // 1024} KB)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user