From 6a9571516f6e91004ffc7010562337e4bb1746f2 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:17:34 -0700 Subject: [PATCH 1/2] feat(retrieve): add provenance metadata to search results Adds an opt-in `include_provenance` parameter to the search/find API endpoints. When enabled, the response includes a `provenance` array with per-query retrieval details: which directories were traversed, which tier (L0/L1/L2) each result came from, match reasons, and the full thinking trace. The internal data was already being collected in MatchedContext.level, MatchedContext.context_type, and QueryResult.thinking_trace. This change surfaces it through the API for retrieval observability, which the README lists as a core design goal ("Visualized Retrieval Trajectory"). Backward compatible: defaults to false, existing clients see no change. Co-Authored-By: Claude Opus 4.6 --- openviking/server/routers/search.py | 8 ++- openviking_cli/retrieve/types.py | 30 ++++++++- tests/retrieve/test_provenance.py | 94 +++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 tests/retrieve/test_provenance.py diff --git a/openviking/server/routers/search.py b/openviking/server/routers/search.py index 8b10cdc30..6f6c2054d 100644 --- a/openviking/server/routers/search.py +++ b/openviking/server/routers/search.py @@ -16,7 +16,6 @@ from openviking.telemetry import TelemetryRequest - def _sanitize_floats(obj: Any) -> Any: """Recursively replace inf/nan with 0.0 to ensure JSON compliance.""" if isinstance(obj, float): @@ -29,6 +28,7 @@ def _sanitize_floats(obj: Any) -> Any: return [_sanitize_floats(v) for v in obj] return obj + router = APIRouter(prefix="/api/v1/search", tags=["search"]) @@ -41,6 +41,7 @@ class FindRequest(BaseModel): node_limit: Optional[int] = None score_threshold: Optional[float] = None filter: Optional[Dict[str, Any]] = None + include_provenance: bool = False telemetry: TelemetryRequest = False @@ -54,6 +55,7 @@ class SearchRequest(BaseModel): node_limit: Optional[int] = None score_threshold: Optional[float] = None filter: Optional[Dict[str, Any]] = None + include_provenance: bool = False telemetry: TelemetryRequest = False @@ -96,7 +98,7 @@ async def find( ) result = execution.result if hasattr(result, "to_dict"): - result = result.to_dict() + result = result.to_dict(include_provenance=request.include_provenance) result = _sanitize_floats(result) return Response( status="ok", @@ -136,7 +138,7 @@ async def _search(): ) result = execution.result if hasattr(result, "to_dict"): - result = result.to_dict() + result = result.to_dict(include_provenance=request.include_provenance) result = _sanitize_floats(result) return Response( status="ok", diff --git a/openviking_cli/retrieve/types.py b/openviking_cli/retrieve/types.py index f1ca51060..620595840 100644 --- a/openviking_cli/retrieve/types.py +++ b/openviking_cli/retrieve/types.py @@ -345,8 +345,13 @@ def __iter__(self): def __post_init__(self): self.total = len(self.memories) + len(self.resources) + len(self.skills) - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary format.""" + def to_dict(self, include_provenance: bool = False) -> Dict[str, Any]: + """Convert to dictionary format. + + Args: + include_provenance: If True, include query_results with thinking + trace and searched_directories for retrieval observability. + """ result = { "memories": [self._context_to_dict(m) for m in self.memories], "resources": [self._context_to_dict(r) for r in self.resources], @@ -360,6 +365,9 @@ def to_dict(self) -> Dict[str, Any]: "queries": [self._query_to_dict(q) for q in self.query_plan.queries], } + if include_provenance and self.query_results: + result["provenance"] = [self._query_result_to_dict(qr) for qr in self.query_results] + return result def _context_to_dict(self, ctx: MatchedContext) -> Dict[str, Any]: @@ -385,6 +393,24 @@ def _query_to_dict(self, q: TypedQuery) -> Dict[str, Any]: "priority": q.priority, } + def _query_result_to_dict(self, qr: "QueryResult") -> Dict[str, Any]: + """Convert QueryResult to dict with provenance data.""" + return { + "query": qr.query.query, + "searched_directories": qr.searched_directories, + "matched_contexts": [ + { + "uri": ctx.uri, + "tier": f"L{ctx.level}", + "context_type": ctx.context_type.value, + "score": ctx.score, + "match_reason": ctx.match_reason, + } + for ctx in qr.matched_contexts + ], + "thinking_trace": qr.thinking_trace.to_dict(), + } + @classmethod def from_dict(cls, data: Dict[str, Any]) -> "FindResult": """Construct FindResult from a dictionary (e.g. HTTP JSON response).""" diff --git a/tests/retrieve/test_provenance.py b/tests/retrieve/test_provenance.py new file mode 100644 index 000000000..7b36c6804 --- /dev/null +++ b/tests/retrieve/test_provenance.py @@ -0,0 +1,94 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 +"""Tests for search result provenance metadata.""" + +from __future__ import annotations + +from openviking_cli.retrieve.types import ( + ContextType, + FindResult, + MatchedContext, + QueryResult, + ThinkingTrace, + TypedQuery, +) + + +class TestFindResultProvenance: + def _make_find_result(self) -> FindResult: + """Build a FindResult with query_results for testing.""" + ctx = MatchedContext( + uri="viking://resources/docs/arch.md", + context_type=ContextType.RESOURCE, + level=2, + abstract="Architecture doc", + score=0.87, + match_reason="semantic_match", + ) + query = TypedQuery( + query="architecture", + context_type=ContextType.RESOURCE, + intent="find architecture docs", + ) + trace = ThinkingTrace() + qr = QueryResult( + query=query, + matched_contexts=[ctx], + searched_directories=["resources/", "resources/docs/"], + thinking_trace=trace, + ) + return FindResult( + memories=[], + resources=[ctx], + skills=[], + query_results=[qr], + ) + + def test_to_dict_without_provenance(self): + result = self._make_find_result() + d = result.to_dict(include_provenance=False) + assert "provenance" not in d + assert d["total"] == 1 + assert len(d["resources"]) == 1 + + def test_to_dict_with_provenance(self): + result = self._make_find_result() + d = result.to_dict(include_provenance=True) + assert "provenance" in d + assert len(d["provenance"]) == 1 + + prov = d["provenance"][0] + assert prov["query"] == "architecture" + assert prov["searched_directories"] == ["resources/", "resources/docs/"] + assert len(prov["matched_contexts"]) == 1 + + ctx = prov["matched_contexts"][0] + assert ctx["uri"] == "viking://resources/docs/arch.md" + assert ctx["tier"] == "L2" + assert ctx["context_type"] == "resource" + assert ctx["score"] == 0.87 + assert ctx["match_reason"] == "semantic_match" + + assert "thinking_trace" in prov + assert "statistics" in prov["thinking_trace"] + + def test_to_dict_default_no_provenance(self): + result = self._make_find_result() + d = result.to_dict() + assert "provenance" not in d + + def test_provenance_without_query_results(self): + result = FindResult(memories=[], resources=[], skills=[]) + d = result.to_dict(include_provenance=True) + assert "provenance" not in d + + def test_existing_fields_unchanged_with_provenance(self): + result = self._make_find_result() + d_without = result.to_dict(include_provenance=False) + d_with = result.to_dict(include_provenance=True) + + # All existing fields should be identical + assert d_without["memories"] == d_with["memories"] + assert d_without["resources"] == d_with["resources"] + assert d_without["skills"] == d_with["skills"] + assert d_without["total"] == d_with["total"] From 1a57f81f3048bdf76a92a4dce5b3084e46154a83 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:25:45 -0700 Subject: [PATCH 2/2] docs: add provenance feature screenshot Co-Authored-By: Claude Opus 4.6 --- docs/images/ov-provenance-example.png | Bin 0 -> 26330 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/ov-provenance-example.png diff --git a/docs/images/ov-provenance-example.png b/docs/images/ov-provenance-example.png new file mode 100644 index 0000000000000000000000000000000000000000..dd961ad90af42a8b8cc4a31ae20e2220cb1a3271 GIT binary patch literal 26330 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 literal 0 HcmV?d00001