haiku.rag/haiku_rag_slim/haiku/rag/capabilities/_tools.py
Yiorgis Gozadinos d9ac221ca0
Refuse a chunk id that names a chunk in two databases
A chunk id is unique within a database and says nothing across them, so a
database copied from another holds the same ids. `qualified_id` keys the
two in-memory identity sites on the database and the id together:
`merge_results` was dropping the second database's result when a query
repeated, and the arrival map that breaks fused score ties was ranking one
of the pair as the other.

Everything serialized records the id alone, so there ambiguity is refused
rather than qualified. `resolve_citations` raises `AmbiguousCitationError`
for a cited id held by two of the databases searched, where it used to
resolve to whichever result came last; `_register_citations` raises for one
already cited from another database in an earlier question. `_cite` turns
both into a `ModelRetry` asking for other evidence. The direct-id fallback
asks every database the question covers instead of taking the first that
answers, so an id no search returned is refused on the same terms.
`all_found` collects them and `first_found` reads its first, which document
reads keep doing on purpose.

Also drop a duplicated 0.77.0 heading from the changelog.
2026-08-25 17:38:25 +03:00

55 lines
1.5 KiB
Python

from collections.abc import Iterable
from pydantic import BaseModel
from haiku.rag.client import HaikuRAG
from haiku.rag.store.models.chunk import SearchResult, qualified_id
class CodeExecutionEntry(BaseModel):
code: str
stdout: str
stderr: str = ""
success: bool = True
async def search_corpus(
rag: HaikuRAG,
query: str,
limit: int | None = None,
document_filter: str | None = None,
sources: list[str] | None = None,
) -> tuple[str, list[SearchResult]]:
"""Search and context-expand results for a capability tool."""
results = await rag.search(
query, limit=limit, filter=document_filter, sources=sources
)
results = await rag.expand_context(results)
formatted = "\n\n---\n\n".join(
result.format_for_agent(rank=index + 1, total=len(results))
for index, result in enumerate(results)
)
return formatted or "No results found.", list(results)
def merge_results(
existing: list[SearchResult], incoming: Iterable[SearchResult]
) -> None:
"""Add the results not already held.
Identity is the database and the chunk id: results built by hand carry
neither and cannot be told apart, so they collapse to the first.
"""
seen = {qualified_id(result.source, result.chunk_id) for result in existing}
for result in incoming:
key = qualified_id(result.source, result.chunk_id)
if key not in seen:
existing.append(result)
seen.add(key)
__all__ = [
"CodeExecutionEntry",
"merge_results",
"search_corpus",
]