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:
@@ -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)),
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user