This commit is contained in:
Waqas Ahmed 2026-08-26 14:34:25 +08:00 committed by GitHub
commit b8e5a812d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 1 deletions

View file

@ -165,9 +165,16 @@ class ExaAISearchConfig(BaseSearchConfig):
- results[].title SearchResult.title
- results[].url SearchResult.url
- results[].text SearchResult.snippet
- results[].highlights SearchResult.snippet (fallback when "text" is absent)
- results[].summary SearchResult.snippet (fallback when "text" and "highlights" are absent)
- results[].publishedDate SearchResult.date
- No last_updated field in Exa AI response (set to None)
Exa only returns "text" when `contents.text` is requested. Requesting
`contents.highlights` and/or `contents.summary` instead returns those fields
with no "text" field at all, so we fall back to them to avoid silently
dropping content the caller already paid for.
Args:
raw_response: Raw httpx response from Exa AI API
logging_obj: Logging object for tracking
@ -180,10 +187,11 @@ class ExaAISearchConfig(BaseSearchConfig):
# Transform results to SearchResult objects
results: Final = []
for result in response_json.get("results", []):
snippet = result.get("text") or "\n\n".join(result.get("highlights") or []) or result.get("summary") or ""
search_result = SearchResult(
title=result.get("title", ""),
url=result.get("url", ""),
snippet=result.get("text", ""), # Exa AI uses "text" for content
snippet=snippet,
date=result.get("publishedDate"), # ISO 8601 datetime string
last_updated=None, # Exa AI doesn't provide last_updated in response
)

View file

@ -0,0 +1,115 @@
from typing import Final, TypedDict
from unittest.mock import Mock
from litellm.llms.exa_ai.search.transformation import ExaAISearchConfig
class _ExaSearchResult(TypedDict, total=False):
title: str
url: str
publishedDate: str
text: str
highlights: list[str]
summary: str
class _ExaSearchPayload(TypedDict):
results: list[_ExaSearchResult]
def _config() -> ExaAISearchConfig:
return ExaAISearchConfig()
def _resp(payload: _ExaSearchPayload) -> Mock:
r: Final = Mock()
r.json.return_value = payload
return r
def _result(**overrides: str | list[str]) -> _ExaSearchResult:
base: Final[_ExaSearchResult] = {
"title": "Lion Finance Group (LON:BGEO) Q1 2026 Earnings Call Transcript & Audio",
"url": "https://stockanalysis.com/quote/lon/BGEO/transcripts/557900-q1-2026/",
"publishedDate": "2026-05-01T00:00:00.000Z",
}
return {**base, **overrides} # type: ignore[return-value]
def test_transform_search_response_uses_text_when_present():
resp = _config().transform_search_response(
_resp({"results": [_result(text="net interest margin is expected to stabilize")]}),
logging_obj=Mock(),
)
assert resp.results[0].snippet == "net interest margin is expected to stabilize"
def test_transform_search_response_falls_back_to_highlights_when_text_missing():
"""Exa returns no "text" field when only contents.highlights is requested."""
resp = _config().transform_search_response(
_resp(
{
"results": [
_result(
highlights=[
"Net interest margin outlook remains stable for the quarter.",
"Management expects continued deposit growth.",
]
)
]
}
),
logging_obj=Mock(),
)
assert resp.results[0].snippet == (
"Net interest margin outlook remains stable for the quarter.\n\n"
"Management expects continued deposit growth."
)
def test_transform_search_response_falls_back_to_summary_when_text_and_highlights_missing():
"""Exa returns no "text" field when only contents.summary is requested."""
resp = _config().transform_search_response(
_resp({"results": [_result(summary="The outlook for net interest margin is positive.")]}),
logging_obj=Mock(),
)
assert resp.results[0].snippet == "The outlook for net interest margin is positive."
def test_transform_search_response_text_takes_priority_over_highlights_and_summary():
resp = _config().transform_search_response(
_resp(
{
"results": [
_result(
text="full text content",
highlights=["a highlight"],
summary="a summary",
)
]
}
),
logging_obj=Mock(),
)
assert resp.results[0].snippet == "full text content"
def test_transform_search_response_highlights_take_priority_over_summary():
resp = _config().transform_search_response(
_resp({"results": [_result(highlights=["a highlight"], summary="a summary")]}),
logging_obj=Mock(),
)
assert resp.results[0].snippet == "a highlight"
def test_transform_search_response_empty_string_when_no_content_fields_present():
resp = _config().transform_search_response(_resp({"results": [_result()]}), logging_obj=Mock())
assert resp.results[0].snippet == ""
def test_transform_search_response_empty_highlights_list_falls_back_to_summary():
resp = _config().transform_search_response(
_resp({"results": [_result(highlights=[], summary="a summary")]}),
logging_obj=Mock(),
)
assert resp.results[0].snippet == "a summary"