search-kb: on-demand KB tool on the [n] spine; drop kb_matched_chunk_ids

The main agent's search_knowledge_base tool runs the hybrid spine, renders
a <retrieved_context> of numbered [n] passages, and persists the registry.
KB subagent prompts teach citing [n] from <document view="full"> reads
(evidence.chunk_ids -> evidence.citations). Delete the now-unused
search->read highlighting hand-off: the kb_matched_chunk_ids state field,
its reducer default, the tool's _matched_chunk_ids writer, and the dead
KnowledgePriorityMiddleware writes.
This commit is contained in:
CREDO23 2026-06-25 15:26:39 +02:00
parent 04a76b163b
commit c98bdea5cf
16 changed files with 518 additions and 325 deletions

View file

@ -0,0 +1,13 @@
"""Resolve ``@document`` references.
Two concerns, one subject: ``resolver`` turns document ids into pointer
references for the model, ``referenced`` turns ``@document`` / ``@folder``
mentions into the document ids a retrieval is confined to.
"""
from __future__ import annotations
from .referenced import referenced_document_ids
from .resolver import resolve_document_references
__all__ = ["referenced_document_ids", "resolve_document_references"]

View file

@ -0,0 +1,39 @@
"""Resolve ``@document`` / ``@folder`` mentions to the documents they point at.
Reference resolution, not retrieval: this answers "which knowledge-base
documents did the user point at this turn?". ``@document`` ids pass through;
``@folder`` ids expand to the documents directly inside each folder within this
search space (direct children only, not nested subfolders). The caller turns the
returned ids into a retrieval ``SearchScope``.
"""
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import Document
async def referenced_document_ids(
session: AsyncSession,
*,
search_space_id: int,
document_ids: list[int] | None = None,
folder_ids: list[int] | None = None,
) -> tuple[int, ...]:
"""Sorted document ids the user pointed at (empty = nothing referenced)."""
doc_ids = set(document_ids or [])
folders = list(folder_ids or [])
if folders:
rows = await session.execute(
select(Document.id).where(
Document.search_space_id == search_space_id,
Document.folder_id.in_(folders),
)
)
doc_ids.update(rows.scalars().all())
return tuple(sorted(doc_ids))
__all__ = ["referenced_document_ids"]

View file

@ -0,0 +1,58 @@
"""Resolve ``@document`` ids into references for the pointer block."""
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.chat.runtime.path_resolver import PathIndex, doc_to_virtual_path
from app.db import Document
from ..models import DocumentReference
async def resolve_document_references(
session: AsyncSession,
*,
search_space_id: int,
document_ids: list[int],
index: PathIndex,
) -> list[DocumentReference]:
"""Map document ids to references in input order; unknown ids are dropped.
Best-effort and fail-closed: an id outside ``search_space_id`` (deleted or
foreign) simply does not produce a reference.
"""
if not document_ids:
return []
rows = await session.execute(
select(Document).where(
Document.search_space_id == search_space_id,
Document.id.in_(document_ids),
)
)
documents_by_id = {row.id: row for row in rows.scalars().all()}
references: list[DocumentReference] = []
for document_id in dict.fromkeys(document_ids):
document = documents_by_id.get(document_id)
if document is None:
continue
title = str(document.title or "untitled")
references.append(
DocumentReference(
entity_id=document.id,
label=title,
path=doc_to_virtual_path(
doc_id=document.id,
title=title,
folder_id=document.folder_id,
index=index,
),
)
)
return references
__all__ = ["resolve_document_references"]