fix(librarian): map live HybridRAG response fields correctly
The client parsed field names the live library-desk service never
returns, so every result rendered as "unknown (score: 0.00)":
- source_type/sources -> source + sources (icons key off sources values)
- rrf_score -> score
- context -> formatted_context
- related_dossiers are per-result; top level aggregates unique titles
- synonyms live inside the keywords dict as a {term: [synonyms]} map
Also stop sending zero limits (service 422s on limit < 1); disabled
legs now rely on the enable_* flags with limits clamped to >= 1.
Adds a recorded live response as a fixture plus contract tests that
pin the mapping (non-unknown sources, non-zero scores, icon coverage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,12 +56,14 @@ class VectorSearchResult(BaseModel):
|
||||
|
||||
class HybridSearchResult(BaseModel):
|
||||
"""Result from HybridRAG search."""
|
||||
source: str # "vector", "graph", "web"
|
||||
source: str # source_type: "wiki", "web", "volatile", "document"
|
||||
sources: list[str] = Field(default_factory=list) # legs that found it: "vector", "graph", "web", ...
|
||||
title: str
|
||||
content: str
|
||||
url: str | None = None
|
||||
score: float
|
||||
score: float # rrf_score from the live service
|
||||
page_id: int | None = None
|
||||
related_dossiers: list[dict[str, Any]] = Field(default_factory=list)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -250,14 +252,16 @@ class LibraryDeskClient:
|
||||
user = user or get_user()
|
||||
client = self._ensure_client()
|
||||
|
||||
# The live service requires all limits >= 1 (422 otherwise);
|
||||
# legs are disabled via the enable_* flags, not a zero limit.
|
||||
payload = {
|
||||
"query": query,
|
||||
"config": {
|
||||
"vector_limit": vector_limit,
|
||||
"graph_limit": graph_limit,
|
||||
"web_limit": web_limit,
|
||||
"document_limit": document_limit,
|
||||
"volatile_limit": volatile_limit,
|
||||
"vector_limit": max(vector_limit, 1),
|
||||
"graph_limit": max(graph_limit, 1),
|
||||
"web_limit": max(web_limit, 1),
|
||||
"document_limit": max(document_limit, 1),
|
||||
"volatile_limit": max(volatile_limit, 1),
|
||||
"enable_documents": document_limit > 0,
|
||||
"enable_volatile": volatile_limit > 0,
|
||||
"enable_web": web_limit > 0,
|
||||
@@ -277,32 +281,51 @@ class LibraryDeskClient:
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Parse results
|
||||
# Parse results (live field names: source_type, sources, rrf_score,
|
||||
# related_dossiers; older names kept as fallbacks)
|
||||
results = []
|
||||
for r in data.get("results", []):
|
||||
results.append(HybridSearchResult(
|
||||
source=r.get("source", "unknown"),
|
||||
source=r.get("source_type") or r.get("source", "unknown"),
|
||||
sources=r.get("sources", []),
|
||||
title=r.get("title", ""),
|
||||
content=r.get("content", ""),
|
||||
url=r.get("url"),
|
||||
score=r.get("score", 0.0),
|
||||
score=r.get("rrf_score", r.get("score", 0.0)),
|
||||
page_id=r.get("page_id"),
|
||||
related_dossiers=r.get("related_dossiers", []),
|
||||
metadata=r.get("metadata", {}),
|
||||
))
|
||||
|
||||
# Handle keywords being either a list or a dict with core_keywords
|
||||
# Handle keywords being either a list or a dict with core_keywords;
|
||||
# the live service nests synonyms inside the keywords dict as a
|
||||
# {term: [synonyms]} map.
|
||||
raw_keywords = data.get("keywords", [])
|
||||
raw_synonyms: Any = data.get("synonyms", [])
|
||||
if isinstance(raw_keywords, dict):
|
||||
keywords = raw_keywords.get("core_keywords", [])
|
||||
raw_synonyms = raw_keywords.get("synonyms", {})
|
||||
else:
|
||||
keywords = raw_keywords
|
||||
if isinstance(raw_synonyms, dict):
|
||||
synonyms = [s for values in raw_synonyms.values() for s in values]
|
||||
else:
|
||||
synonyms = raw_synonyms
|
||||
|
||||
# Aggregate per-result related dossiers into unique top-level titles
|
||||
related_dossiers: list[str] = []
|
||||
for result in results:
|
||||
for dossier in result.related_dossiers:
|
||||
title = dossier.get("title", "")
|
||||
if title and title not in related_dossiers:
|
||||
related_dossiers.append(title)
|
||||
|
||||
return HybridRAGResponse(
|
||||
results=results,
|
||||
keywords=keywords,
|
||||
synonyms=data.get("synonyms", []),
|
||||
related_dossiers=data.get("related_dossiers", []),
|
||||
formatted_context=data.get("formatted_context", ""),
|
||||
synonyms=synonyms,
|
||||
related_dossiers=related_dossiers,
|
||||
formatted_context=data.get("context", data.get("formatted_context", "")),
|
||||
search_id=data.get("search_id"),
|
||||
timing=data.get("timing", {}),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,18 @@ from src.core.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Icons keyed by the values library-desk emits in each result's `sources`
|
||||
# list (search legs) and `source_type` (result origin).
|
||||
SOURCE_ICONS = {
|
||||
"vector": "📄",
|
||||
"graph": "🔗",
|
||||
"web": "🌐",
|
||||
"document": "📑",
|
||||
"documents": "📑",
|
||||
"volatile": "⚡",
|
||||
"wiki": "📄",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HybridRAG Search
|
||||
@@ -76,13 +88,10 @@ async def hybrid_search(
|
||||
|
||||
# Add results
|
||||
for i, result in enumerate(response.results, 1):
|
||||
source_icon = {
|
||||
"vector": "📄",
|
||||
"graph": "🔗",
|
||||
"web": "🌐",
|
||||
"document": "📑",
|
||||
"volatile": "⚡",
|
||||
}.get(result.source, "•")
|
||||
source_keys = result.sources or [result.source]
|
||||
source_icon = "".join(
|
||||
dict.fromkeys(SOURCE_ICONS.get(key, "•") for key in source_keys)
|
||||
)
|
||||
|
||||
output_parts.append(
|
||||
f"{i}. {source_icon} **{result.title}** (score: {result.score:.2f})"
|
||||
|
||||
Reference in New Issue
Block a user