feat(librarian): signal degraded search coverage

- parse source_counts into HybridRAGResponse and additively parse the
  shared-contract source_status/degraded fields when present (absence
  tolerated, so deploy order between tatlock and library-desk never
  matters)
- hybrid_search appends a one-line coverage note when a leg reported
  'failed' (or degraded is set), falling back to inferring silent legs
  from source_counts on older library-desk versions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:15:09 +02:00
co-authored by Claude Fable 5
parent f853db8ccc
commit 99e1fe33ca
4 changed files with 227 additions and 2 deletions
+11
View File
@@ -75,7 +75,14 @@ class HybridRAGResponse(BaseModel):
related_dossiers: list[str] = Field(default_factory=list)
formatted_context: str = ""
search_id: str | None = None
source_counts: dict[str, int] = Field(default_factory=dict)
timing: dict[str, float] = Field(default_factory=dict)
# Additive degradation contract - only newer library-desk versions
# send these; absence means "no status reported", not "healthy".
# Maps each leg (vector/graph/web/volatile/documents) to
# "ok" | "failed" | "disabled".
source_status: dict[str, str] = Field(default_factory=dict)
degraded: bool = False
class GraphNode(BaseModel):
@@ -327,7 +334,11 @@ class LibraryDeskClient:
related_dossiers=related_dossiers,
formatted_context=data.get("context", data.get("formatted_context", "")),
search_id=data.get("search_id"),
source_counts=data.get("source_counts", {}),
timing=data.get("timing", {}),
# Additive fields - tolerate absence on older library-desk
source_status=data.get("source_status") or {},
degraded=bool(data.get("degraded", False)),
)
# ========================================================================
+69 -1
View File
@@ -4,7 +4,7 @@ Librarian tools for PydanticAI agent.
These tools wrap the library-desk API and are registered with
The Librarian agent for research and knowledge management tasks.
"""
from src.agents.librarian.client import LibraryDeskClient
from src.agents.librarian.client import HybridRAGResponse, LibraryDeskClient
from src.core.logging_config import get_logger
logger = get_logger(__name__)
@@ -22,6 +22,62 @@ SOURCE_ICONS = {
}
def _coverage_note(
response: HybridRAGResponse,
include_web: bool,
include_documents: bool,
include_volatile: bool,
) -> str:
"""
Build a one-line coverage note when the search was degraded or an
enabled source leg contributed nothing, so outages stay visible to
the model and the user instead of silently narrowing results.
Prefers the additive source_status/degraded contract when present;
falls back to inferring silent legs from source_counts.
"""
if response.source_status:
failed = sorted(
leg
for leg, status in response.source_status.items()
if status == "failed"
)
if failed:
return (
"⚠️ *Coverage note: results are partial - "
f"these sources failed: {', '.join(failed)}.*"
)
if response.degraded:
return (
"⚠️ *Coverage note: results are partial - "
"one or more sources failed during this search.*"
)
return ""
if not response.source_counts:
# Older library-desk without per-source reporting - nothing to infer
return ""
expected = {"vector", "graph"}
if include_web:
expected.add("web")
if include_documents:
expected.add("documents")
if include_volatile:
expected.add("volatile")
# Normalize count keys to leg names (document/documents, wiki/vector)
aliases = {"document": "documents", "wiki": "vector"}
reported = {aliases.get(key, key) for key in response.source_counts}
missing = sorted(expected - reported)
if missing:
return (
"⚠️ *Coverage note: no results came from: "
f"{', '.join(missing)} (source unavailable or nothing found).*"
)
return ""
# ============================================================================
# HybridRAG Search
# ============================================================================
@@ -101,10 +157,22 @@ async def hybrid_search(
output_parts.append(f" {result.content[:300]}...")
output_parts.append("")
# Surface degraded coverage so outages are visible downstream
coverage_note = _coverage_note(
response,
include_web=include_web,
include_documents=include_documents,
include_volatile=include_volatile,
)
if coverage_note:
output_parts.append(coverage_note)
logger.info(
"librarian_hybrid_search",
query=query,
result_count=len(response.results),
degraded=response.degraded,
source_counts=response.source_counts,
)
return "\n".join(output_parts)