mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(search): read AgentCore structuredContent results
Web-search connector 1.1.0 and later return the machine-readable results in result.structuredContent and may leave the text block as prose, which the parser dropped. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
ed84e82428
commit
8ef522a2a0
2 changed files with 50 additions and 12 deletions
|
|
@ -104,6 +104,13 @@ def _to_search_result(item: Mapping[str, object]) -> SearchResult:
|
|||
)
|
||||
|
||||
|
||||
def _result_items(parsed: object) -> tuple[Mapping[str, object], ...]:
|
||||
items: Final = parsed.get("results", ()) if isinstance(parsed, Mapping) else parsed
|
||||
if not isinstance(items, Sequence) or isinstance(items, (str, bytes)):
|
||||
return ()
|
||||
return tuple(item for item in items if isinstance(item, Mapping))
|
||||
|
||||
|
||||
def _parse_result_items(raw_text: object) -> tuple[Mapping[str, object], ...]:
|
||||
"""
|
||||
Parse one MCP text block into the search result objects it carries.
|
||||
|
|
@ -117,10 +124,7 @@ def _parse_result_items(raw_text: object) -> tuple[Mapping[str, object], ...]:
|
|||
parsed: Final = json.loads(raw_text)
|
||||
except json.JSONDecodeError:
|
||||
return ()
|
||||
items: Final = parsed.get("results", ()) if isinstance(parsed, dict) else parsed
|
||||
if not isinstance(items, Sequence) or isinstance(items, (str, bytes)):
|
||||
return ()
|
||||
return tuple(item for item in items if isinstance(item, dict))
|
||||
return _result_items(parsed)
|
||||
|
||||
|
||||
def _iter_sse_events(text: str) -> Iterator[Mapping[str, object]]:
|
||||
|
|
@ -350,7 +354,9 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
|
|||
|
||||
The gateway returns JSON-RPC (as plain JSON or a single-message SSE
|
||||
stream) whose result.content[] text blocks contain a JSON list of
|
||||
{title, url, date/publishedDate, text} entries.
|
||||
{title, url, date/publishedDate, text} entries. Web-search connector
|
||||
1.1.0 and later repeat that list in result.structuredContent, which is
|
||||
the only machine-readable copy when the text block holds prose instead.
|
||||
"""
|
||||
response_json: Final = self._parse_mcp_body(raw_response)
|
||||
|
||||
|
|
@ -370,14 +376,15 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
|
|||
message=f"AgentCore web search tool error: {self._tool_error_message(response_json)}",
|
||||
)
|
||||
|
||||
return SearchResponse(
|
||||
results=[ # mutable-ok: SearchResponse.results is a pydantic list field
|
||||
_to_search_result(item)
|
||||
for block in self._text_blocks(response_json)
|
||||
for item in _parse_result_items(block.get("text"))
|
||||
],
|
||||
object="search",
|
||||
text_items: Final = tuple(
|
||||
item for block in self._text_blocks(response_json) for item in _parse_result_items(block.get("text"))
|
||||
)
|
||||
structured: Final = result.get("structuredContent") if isinstance(result, Mapping) else None
|
||||
items: Final = text_items or _result_items(structured)
|
||||
|
||||
results: Final = [_to_search_result(item) for item in items] # mutable-ok: pydantic list field
|
||||
|
||||
return SearchResponse(results=results, object="search")
|
||||
|
||||
def _tool_error_message(self, response_json: Mapping[str, object]) -> str:
|
||||
texts: Final = tuple(
|
||||
|
|
|
|||
|
|
@ -261,6 +261,37 @@ class TestAgentCoreSearch:
|
|||
with pytest.raises(Exception, match="AccessDeniedException"):
|
||||
config.transform_search_response(raw_response=mock_response, logging_obj=MagicMock())
|
||||
|
||||
def test_transform_search_response_reads_structured_content(self):
|
||||
"""Connector 1.1.0+ puts the machine-readable results in structuredContent and may
|
||||
leave the text block as prose, which must not come back as an empty result list."""
|
||||
config = AgentCoreSearchConfig()
|
||||
mock_response = _make_mock_response(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"content": [{"type": "text", "text": "Here is a prose summary of what I found."}],
|
||||
"structuredContent": {"id": "824f89d0", "results": MCP_RESULTS},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
response = config.transform_search_response(raw_response=mock_response, logging_obj=MagicMock())
|
||||
assert [result.title for result in response.results] == ["Test Result 1", "Test Result 2"]
|
||||
assert response.results[0].url == "https://example.com/1"
|
||||
assert response.results[0].snippet == "Snippet for result 1"
|
||||
assert response.results[0].date == "2026-06-16"
|
||||
|
||||
def test_transform_search_response_does_not_duplicate_structured_content(self):
|
||||
"""1.1.0+ repeats the same results in both places, so parsing both would double them."""
|
||||
config = AgentCoreSearchConfig()
|
||||
body = _mcp_response_body()
|
||||
body["result"]["structuredContent"] = {"results": MCP_RESULTS}
|
||||
mock_response = _make_mock_response(body)
|
||||
|
||||
response = config.transform_search_response(raw_response=mock_response, logging_obj=MagicMock())
|
||||
assert len(response.results) == 2
|
||||
|
||||
def test_transform_search_response_parses_crlf_framed_sse(self):
|
||||
"""SSE streams may be CRLF framed; events must still split into separate events."""
|
||||
config = AgentCoreSearchConfig()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue