Skip to content

Commit d439490

Browse files
authored
Merge pull request #25 from Ontos-AI/fix/wangbinqi/retrieval-response-contract
fix: sync retrieval response SDK contract
2 parents 3fd5cef + 7827ea6 commit d439490

6 files changed

Lines changed: 66 additions & 15 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ response = client.retrieval.query(
6666
)
6767

6868
print(response.router_used)
69+
print(response.answer_text)
70+
print(response.evidence_text)
71+
print(response.stop_reason)
72+
print(response.failure_reason)
73+
74+
for reference in response.referenced_chunks:
75+
print(reference.chunk_id, reference.document_id, reference.asset_url)
6976

7077
for result in response.results:
7178
print(result.content)

docs/usage.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,12 @@ response = client.retrieval.query(
482482
)
483483
print(response.answer_text) # LLM-generated natural-language answer
484484
print(response.router_used) # "workflow_single_step", "small_kb_all", etc.
485+
print(response.evidence_text) # rendered evidence context, when returned
486+
print(response.stop_reason) # agentic termination reason, when returned
487+
print(response.failure_reason) # no-answer reason, when returned
485488
for ref in response.referenced_chunks:
486-
print(ref.get("chunk_id"), ref.get("asset_url"))
489+
print(ref.chunk_id, ref.document_id, ref.chunk_type)
490+
print(ref.section_path, ref.file_path, ref.job_id, ref.asset_url)
487491

488492
# Legacy results are always available
489493
for result in response.results:

src/knowhere/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from knowhere.types.retrieval import (
5050
RetrievalChannel,
5151
RetrievalFilterMode,
52+
RetrievalReferencedChunk,
5253
RetrievalSectionExclusion,
5354
RetrievalSource,
5455
RetrievalQueryResponse,
@@ -115,6 +116,7 @@
115116
# Retrieval types
116117
"RetrievalChannel",
117118
"RetrievalFilterMode",
119+
"RetrievalReferencedChunk",
118120
"RetrievalSectionExclusion",
119121
"RetrievalSource",
120122
"RetrievalQueryResponse",

src/knowhere/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from knowhere.types.retrieval import (
1717
RetrievalChannel,
1818
RetrievalFilterMode,
19+
RetrievalReferencedChunk,
1920
RetrievalSectionExclusion,
2021
RetrievalSource,
2122
RetrievalQueryResponse,
@@ -56,6 +57,7 @@
5657
# retrieval
5758
"RetrievalChannel",
5859
"RetrievalFilterMode",
60+
"RetrievalReferencedChunk",
5961
"RetrievalSectionExclusion",
6062
"RetrievalSource",
6163
"RetrievalQueryResponse",

src/knowhere/types/retrieval.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import Any, Dict, List, Literal, Optional, TypedDict
5+
from typing import Literal, Optional, TypedDict
66

77
from pydantic import BaseModel, Field
88

@@ -36,17 +36,31 @@ class RetrievalResult(BaseModel):
3636
source: RetrievalSource
3737

3838

39+
class RetrievalReferencedChunk(BaseModel):
40+
"""Cited evidence chunk returned by agentic retrieval."""
41+
42+
chunk_id: str
43+
document_id: str
44+
chunk_type: str
45+
section_path: str
46+
file_path: Optional[str] = None
47+
job_id: Optional[str] = None
48+
asset_url: Optional[str] = None
49+
50+
3951
class RetrievalQueryResponse(BaseModel):
4052
"""Response from ``POST /v1/retrieval/query``.
4153
42-
Agentic fields (``answer_text``, ``referenced_chunks``) are only
43-
populated when ``use_agentic=True``. In legacy retrieval mode they
44-
default to ``None`` and ``[]`` respectively.
54+
Agentic retrieval may also include ``evidence_text``, ``stop_reason``,
55+
and ``failure_reason`` when the server returns workflow diagnostics.
4556
"""
4657

4758
namespace: str
4859
query: str
49-
router_used: Optional[str] = None
60+
router_used: str
5061
answer_text: Optional[str] = None
51-
referenced_chunks: List[Dict[str, Any]] = Field(default_factory=list)
62+
referenced_chunks: list[RetrievalReferencedChunk] = Field(default_factory=list)
63+
evidence_text: Optional[str] = None
64+
stop_reason: Optional[str] = None
65+
failure_reason: Optional[str] = None
5266
results: list[RetrievalResult]

tests/test_retrieval.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ def _make_retrieval_response() -> Dict[str, Any]:
2121
"query": "refund policy",
2222
"router_used": "discovery+agent",
2323
"answer_text": "Annual plans may be refunded within 30 days of purchase.",
24+
"evidence_text": "Rendered retrieval evidence",
25+
"stop_reason": "answer_done",
26+
"failure_reason": "insufficient evidence",
2427
"referenced_chunks": [
2528
{
2629
"chunk_id": "chunk_001",
2730
"document_id": "doc_123",
31+
"chunk_type": "text",
32+
"section_path": "Policies / Billing / Refunds",
33+
"file_path": None,
34+
"job_id": "job_123",
2835
"asset_url": "https://example.com/assets/chunk_001",
2936
}
3037
],
@@ -44,11 +51,13 @@ def _make_retrieval_response() -> Dict[str, Any]:
4451

4552

4653
def _make_legacy_retrieval_response() -> Dict[str, Any]:
47-
"""Legacy-mode response without agentic fields (backward compatibility)."""
54+
"""Legacy-mode response with server-default agentic fields."""
4855
return {
4956
"namespace": "support-center",
5057
"query": "refund policy",
5158
"router_used": "discovery+legacy",
59+
"answer_text": None,
60+
"referenced_chunks": [],
5261
"results": [
5362
{
5463
"chunk_type": "text",
@@ -126,7 +135,12 @@ def test_query_sends_request_and_returns_results(self, sync_client: Any) -> None
126135
"Annual plans may be refunded within 30 days of purchase."
127136
)
128137
assert len(response.referenced_chunks) == 1
129-
assert response.referenced_chunks[0]["chunk_id"] == "chunk_001"
138+
assert response.evidence_text == "Rendered retrieval evidence"
139+
assert response.stop_reason == "answer_done"
140+
assert response.failure_reason == "insufficient evidence"
141+
assert response.referenced_chunks[0].chunk_id == "chunk_001"
142+
assert response.referenced_chunks[0].chunk_type == "text"
143+
assert response.referenced_chunks[0].file_path is None
130144
assert not hasattr(response.results[0], "citation")
131145
assert not hasattr(response.results[0], "chunk_id")
132146
assert not hasattr(response.results[0], "section_id")
@@ -188,8 +202,8 @@ def test_use_agentic_omitted_when_none(self, sync_client: Any) -> None:
188202

189203
@respx.mock
190204
def test_agentic_response_fields(self, sync_client: Any) -> None:
191-
"""Agentic response exposes answer_text and referenced_chunks."""
192-
route = respx.post(RETRIEVAL_QUERY_URL).mock(
205+
"""Agentic response exposes answer, evidence, and typed references."""
206+
respx.post(RETRIEVAL_QUERY_URL).mock(
193207
return_value=httpx.Response(200, json=_make_retrieval_response())
194208
)
195209

@@ -202,15 +216,23 @@ def test_agentic_response_fields(self, sync_client: Any) -> None:
202216
"Annual plans may be refunded within 30 days of purchase."
203217
)
204218
assert len(response.referenced_chunks) == 1
205-
assert response.referenced_chunks[0]["chunk_id"] == "chunk_001"
206-
assert response.referenced_chunks[0]["asset_url"] == (
219+
assert response.referenced_chunks[0].chunk_id == "chunk_001"
220+
assert response.referenced_chunks[0].document_id == "doc_123"
221+
assert response.referenced_chunks[0].chunk_type == "text"
222+
assert response.referenced_chunks[0].section_path == "Policies / Billing / Refunds"
223+
assert response.referenced_chunks[0].file_path is None
224+
assert response.referenced_chunks[0].job_id == "job_123"
225+
assert response.referenced_chunks[0].asset_url == (
207226
"https://example.com/assets/chunk_001"
208227
)
228+
assert response.evidence_text == "Rendered retrieval evidence"
229+
assert response.stop_reason == "answer_done"
230+
assert response.failure_reason == "insufficient evidence"
209231

210232
@respx.mock
211233
def test_legacy_response_without_agentic_fields(self, sync_client: Any) -> None:
212-
"""Legacy-mode response (no agentic fields) parses without error."""
213-
route = respx.post(RETRIEVAL_QUERY_URL).mock(
234+
"""Legacy-mode response defaults agentic fields to null and empty references."""
235+
respx.post(RETRIEVAL_QUERY_URL).mock(
214236
return_value=httpx.Response(
215237
200, json=_make_legacy_retrieval_response()
216238
)

0 commit comments

Comments
 (0)