diff --git a/README.md b/README.md index 3885e6d..05cb3ef 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ response = client.retrieval.query( print(response.router_used) print(response.answer_text) +print(response.evidence) # composed parts to consume print(response.evidence_text) print(response.stop_reason) print(response.failure_reason) @@ -84,7 +85,7 @@ for reference in response.referenced_chunks: print(reference.chunk_id, reference.chunk_type, reference.content_source) print(reference.metadata, reference.asset_url) -for result in response.results: +for result in response.results: # raw path chunks for debug print(result.chunk_id, result.chunk_type, result.content_source) print(result.content) print(result.score) diff --git a/docs/usage.md b/docs/usage.md index d78e8a2..e8f7e00 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -541,14 +541,15 @@ response = client.retrieval.query( ) print(response.answer_text) # LLM-generated natural-language answer print(response.router_used) # "workflow_single_step", "small_kb_all", etc. -print(response.evidence_text) # rendered evidence context, when returned +print(response.evidence) # composed parts to consume +print(response.evidence_text) # text projection of those parts print(response.stop_reason) # agentic termination reason, when returned print(response.failure_reason) # no-answer reason, when returned for ref in response.referenced_chunks: print(ref.chunk_id, ref.document_id, ref.chunk_type, ref.content_source) print(ref.section_path, ref.file_path, ref.job_id, ref.asset_url, ref.metadata) -# Legacy results are always available +# results are raw path chunks for debug for result in response.results: print(result.chunk_id) print(result.content) diff --git a/src/knowhere/__init__.py b/src/knowhere/__init__.py index f865051..7a766f5 100644 --- a/src/knowhere/__init__.py +++ b/src/knowhere/__init__.py @@ -87,6 +87,7 @@ from knowhere.types.retrieval import ( RetrievalChannel, RetrievalChunkType, + RetrievalEvidencePart, RetrievalFilterMode, RetrievalQueryResponse, RetrievalReferencedChunk, @@ -149,6 +150,7 @@ "RetrievalReferencedChunk", "RetrievalSectionExclusion", "RetrievalSource", + "RetrievalEvidencePart", "RetrievalQueryResponse", "RetrievalResult", # Result types diff --git a/src/knowhere/types/__init__.py b/src/knowhere/types/__init__.py index 7dba489..f0bda89 100644 --- a/src/knowhere/types/__init__.py +++ b/src/knowhere/types/__init__.py @@ -50,6 +50,7 @@ from knowhere.types.retrieval import ( RetrievalChannel, RetrievalChunkType, + RetrievalEvidencePart, RetrievalFilterMode, RetrievalQueryResponse, RetrievalReferencedChunk, @@ -84,6 +85,7 @@ "RetrievalReferencedChunk", "RetrievalSectionExclusion", "RetrievalSource", + "RetrievalEvidencePart", "RetrievalQueryResponse", "RetrievalResult", # params diff --git a/src/knowhere/types/retrieval.py b/src/knowhere/types/retrieval.py index 5e8fbb6..c67f60f 100644 --- a/src/knowhere/types/retrieval.py +++ b/src/knowhere/types/retrieval.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Literal, Optional, TypedDict +from typing import Annotated, Any, Literal, Optional, TypedDict, Union from pydantic import BaseModel, Field @@ -27,6 +27,23 @@ class RetrievalSource(BaseModel): section_path: Optional[str] = None +class RetrievalTextPart(BaseModel): + type: Literal["text"] + text: str + + +class RetrievalImagePart(BaseModel): + type: Literal["image"] + media_type: str + data: str + + +RetrievalEvidencePart = Annotated[ + Union[RetrievalTextPart, RetrievalImagePart], + Field(discriminator="type"), +] + + class RetrievalResult(BaseModel): """Canonical chunk result returned by ``POST /v2/retrieval/query``.""" @@ -60,9 +77,11 @@ class RetrievalReferencedChunk(BaseModel): class RetrievalQueryResponse(BaseModel): """Response from ``POST /v2/retrieval/query``. - Three PRIMARY output fields for downstream agent consumption: + Downstream agents consume: - - ``evidence_text``: hierarchical evidence tree for LLM context + - ``evidence``: composed parts (text/HTML and inline images) + - ``evidence_text``: text projection of those parts + - ``results``: raw path chunks for debug - ``decision_trace``: per-step navigation decisions (includes stop/failure) - ``referenced_chunks``: structured chunk citations for follow-up queries """ @@ -70,6 +89,7 @@ class RetrievalQueryResponse(BaseModel): namespace: str query: str router_used: str + evidence: list[RetrievalEvidencePart] = Field(default_factory=list) answer_text: Optional[str] = None referenced_chunks: list[RetrievalReferencedChunk] = Field(default_factory=list) evidence_text: Optional[str] = None diff --git a/tests/test_retrieval.py b/tests/test_retrieval.py index 515fb3b..6fc1fb2 100644 --- a/tests/test_retrieval.py +++ b/tests/test_retrieval.py @@ -21,6 +21,17 @@ def _make_retrieval_response() -> Dict[str, Any]: "query": "refund policy", "router_used": "discovery+agent", "answer_text": "Annual plans may be refunded within 30 days of purchase.", + "evidence": [ + { + "type": "text", + "text": "Annual plans may be refunded within 30 days.", + }, + { + "type": "image", + "media_type": "image/png", + "data": "abc", + }, + ], "evidence_text": "Rendered retrieval evidence", "stop_reason": "answer_done", "failure_reason": "insufficient evidence", @@ -147,6 +158,11 @@ def test_query_sends_request_and_returns_results(self, sync_client: Any) -> None assert response.answer_text == ("Annual plans may be refunded within 30 days of purchase.") assert len(response.referenced_chunks) == 1 assert response.evidence_text == "Rendered retrieval evidence" + assert response.evidence[0].type == "text" + assert response.evidence[0].text == "Annual plans may be refunded within 30 days." + assert response.evidence[1].type == "image" + assert response.evidence[1].media_type == "image/png" + assert not hasattr(response.results[0], "composed") assert response.stop_reason == "answer_done" assert response.failure_reason == "insufficient evidence" assert response.referenced_chunks[0].chunk_id == "chunk_001" @@ -329,6 +345,8 @@ def test_agentic_response_fields(self, sync_client: Any) -> None: assert response.referenced_chunks[0].job_id == "job_123" assert response.referenced_chunks[0].asset_url == ("https://example.com/assets/chunk_001") assert response.evidence_text == "Rendered retrieval evidence" + assert response.evidence[1].media_type == "image/png" + assert not hasattr(response.results[0], "composed") assert response.stop_reason == "answer_done" assert response.failure_reason == "insufficient evidence" assert response.decision_trace is not None @@ -346,5 +364,7 @@ def test_legacy_response_without_agentic_fields(self, sync_client: Any) -> None: response = sync_client.retrieval.query(query="refund policy") assert response.answer_text is None + assert response.evidence == [] + assert not hasattr(response.results[0], "composed") assert response.referenced_chunks == [] assert response.decision_trace is None