refactor(web_search): drop STRIX_EXA_CONTENT_MODE, Exa always returns summaries

This commit is contained in:
Ahmed Allam 2026-09-04 16:51:54 +00:00
parent a4fa849f59
commit 34d53db041
4 changed files with 18 additions and 95 deletions

View file

@ -93,11 +93,7 @@ affecting the agents that do the actual testing.
</ParamField>
<ParamField path="STRIX_EXA_NUM_RESULTS" default="5" type="integer">
Number of Exa results to return, from `1` to `100`. This setting applies only to the Exa provider.
</ParamField>
<ParamField path="STRIX_EXA_CONTENT_MODE" default="summary" type="string">
Content that each Exa result includes: `summary` or `highlights`. With `summary`, each result includes a short security-focused summary of the page. With `highlights`, each result includes verbatim query-relevant excerpts. To read a full page, the agent calls `web_get_contents` with the result URL. This setting applies only to the Exa provider.
Number of Exa results to return, from `1` to `100`. Each result includes a title, a URL, and a short security-focused summary. To read a full page, the agent calls `web_get_contents` with the result URL. This setting applies only to the Exa provider.
</ParamField>
<ParamField path="POSTMAN_API_KEY" type="string">

View file

@ -122,7 +122,6 @@ class TelemetrySettings(BaseSettings):
WebSearchProvider = Literal["auto", "perplexity", "exa"]
ExaSearchType = Literal["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"]
ExaContentMode = Literal["summary", "highlights"]
class IntegrationSettings(BaseSettings):
@ -152,10 +151,6 @@ class IntegrationSettings(BaseSettings):
le=100,
alias="STRIX_EXA_NUM_RESULTS",
)
exa_content_mode: ExaContentMode = Field(
default="summary",
alias="STRIX_EXA_CONTENT_MODE",
)
postman_api_key: str | None = Field(
default=None,
alias="POSTMAN_API_KEY",

View file

@ -60,7 +60,6 @@ def _perplexity_content(api_key: str, query: str) -> str:
return str(response.json()["choices"][0]["message"]["content"])
_EXA_HIGHLIGHT_MAX_CHARS = 750
_EXA_PAGE_MAX_CHARS = 20000
_EXA_MAX_CONTENT_URLS = 10
_EXA_SUMMARY_PROMPT = (
@ -71,12 +70,6 @@ _EXA_SUMMARY_PROMPT = (
)
def _exa_search_contents(content_mode: str) -> dict[str, Any]:
if content_mode == "highlights":
return {"highlights": {"maxCharacters": _EXA_HIGHLIGHT_MAX_CHARS}}
return {"summary": {"query": _EXA_SUMMARY_PROMPT}}
def _exa_result_block(result: dict[str, Any]) -> str | None:
result_url = str(result.get("url") or result.get("id") or "")
if not result_url:
@ -86,11 +79,6 @@ def _exa_result_block(result: dict[str, Any]) -> str | None:
summary = str(result.get("summary") or "").strip()
if summary:
parts.append(summary)
highlights = result.get("highlights") or []
if isinstance(highlights, list):
excerpts = [str(item).strip() for item in highlights if str(item).strip()]
if excerpts:
parts.append("\n".join(f"> {excerpt}" for excerpt in excerpts))
return "\n".join(parts)
@ -127,13 +115,7 @@ def _exa_post(api_key: str, endpoint: str, payload: dict[str, Any]) -> dict[str,
return body
def _exa_content(
api_key: str,
query: str,
search_type: str,
num_results: int,
content_mode: str,
) -> str:
def _exa_content(api_key: str, query: str, search_type: str, num_results: int) -> str:
body = _exa_post(
api_key,
"https://api.exa.ai/search",
@ -141,7 +123,7 @@ def _exa_content(
"query": f"{_SYSTEM_PROMPT}\n\n{query}",
"type": search_type,
"numResults": num_results,
"contents": _exa_search_contents(content_mode),
"contents": {"summary": {"query": _EXA_SUMMARY_PROMPT}},
},
)
blocks = _exa_blocks(body.get("results") or [], _exa_result_block)
@ -239,7 +221,6 @@ def _do_search(query: str) -> dict[str, Any]:
query,
integrations.exa_search_type,
integrations.exa_num_results,
integrations.exa_content_mode,
)
return _perplexity_content(api_key, query)
@ -335,11 +316,10 @@ async def web_search(ctx: RunContextWrapper, query: str) -> str:
examples.
With the Exa provider you get a ranked list of results, each with a
title, URL, and a short security-focused summary (or verbatim
highlights when ``STRIX_EXA_CONTENT_MODE=highlights``). Read the
result you need, then call ``web_get_contents`` with its URL to pull
the full page text when a summary is not enough. With Perplexity you
get a single synthesized cited answer.
title, URL, and a short security-focused summary. Read the result
you need, then call ``web_get_contents`` with its URL to pull the
full page text when a summary is not enough. With Perplexity you get
a single synthesized cited answer.
**Good example queries** (each is a full sentence, names a
version/product, and asks one concrete thing):
@ -375,8 +355,8 @@ async def web_get_contents(ctx: RunContextWrapper, urls: list[str]) -> str:
"""Fetch the full, cleaned text of specific web pages (Exa only).
Use this as the drill-down step after ``web_search``: when a result's
summary or highlights are not enough, pass that result's URL here to
read the whole page. Good for reading a full advisory, a CVE writeup,
summary is not enough, pass that result's URL here to read the whole
page. Good for reading a full advisory, a CVE writeup,
an exploit proof-of-concept, or vendor documentation end to end.
Prefer ``web_search`` first to find the right pages, then fetch only

View file

@ -80,7 +80,9 @@ def test_no_keys_names_both_providers() -> None:
assert "EXA_API_KEY or PERPLEXITY_API_KEY" in resolved["error"]
def test_exa_content_renders_results_with_highlights(monkeypatch: pytest.MonkeyPatch) -> None:
def test_exa_content_requests_summaries_and_renders_results(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
@ -93,7 +95,7 @@ def test_exa_content_renders_results_with_highlights(monkeypatch: pytest.MonkeyP
{
"url": "https://nvd.example/cve",
"title": "NVD entry",
"highlights": ["CVE-2024-0001 affects it.", " "],
"summary": " CVE-2024-0001 is a heap overflow. ",
},
{"id": "https://blog.example/post"},
"not-a-dict",
@ -104,49 +106,20 @@ def test_exa_content_renders_results_with_highlights(monkeypatch: pytest.MonkeyP
monkeypatch.setattr(requests, "post", fake_post)
content = tool._exa_content("ek", "OpenSSH 7.4 RCE?", "auto", 5, "highlights")
content = tool._exa_content("ek", "OpenSSH 7.4 RCE?", "auto", 5)
assert captured["url"] == "https://api.exa.ai/search"
assert captured["headers"]["x-api-key"] == "ek"
assert "OpenSSH 7.4 RCE?" in captured["json"]["query"]
assert captured["json"]["type"] == "auto"
assert captured["json"]["numResults"] == 5
assert captured["json"]["contents"]["highlights"]["maxCharacters"] > 0
assert "summary" not in captured["json"]["contents"]
assert captured["json"]["contents"] == {"summary": {"query": tool._EXA_SUMMARY_PROMPT}}
assert content == (
"### NVD entry\nhttps://nvd.example/cve\n> CVE-2024-0001 affects it.\n\n"
"### NVD entry\nhttps://nvd.example/cve\nCVE-2024-0001 is a heap overflow.\n\n"
"### https://blog.example/post\nhttps://blog.example/post"
)
def test_exa_content_defaults_to_summary_and_renders_it(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def fake_post(_url: str, **kwargs: Any) -> _FakeResponse:
captured["json"] = kwargs["json"]
return _FakeResponse(
{
"results": [
{
"url": "https://nvd.example/cve",
"title": "NVD entry",
"summary": " CVE-2024-0001 is a heap overflow. ",
},
],
}
)
monkeypatch.setattr(requests, "post", fake_post)
content = tool._exa_content("ek", "q", "auto", 5, "summary")
assert captured["json"]["contents"]["summary"]["query"]
assert "highlights" not in captured["json"]["contents"]
assert content == "### NVD entry\nhttps://nvd.example/cve\nCVE-2024-0001 is a heap overflow."
def test_exa_content_renders_a_result_without_contents(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@ -157,7 +130,7 @@ def test_exa_content_renders_a_result_without_contents(
{"results": [{"url": "https://ex.example", "title": "Ex"}]}
),
)
assert tool._exa_content("ek", "q", "auto", 5, "summary") == "### Ex\nhttps://ex.example"
assert tool._exa_content("ek", "q", "auto", 5) == "### Ex\nhttps://ex.example"
@pytest.mark.parametrize("body", [{}, {"results": None}, {"results": []}, {"results": ["x"]}])
@ -166,28 +139,7 @@ def test_exa_content_rejects_empty_results(
) -> None:
monkeypatch.setattr(requests, "post", lambda *_a, **_kw: _FakeResponse(body))
with pytest.raises(ValueError, match="no results"):
tool._exa_content("ek", "q", "auto", 5, "summary")
def test_exa_content_mode_is_configurable(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
class _Settings:
integrations = IntegrationSettings(
EXA_API_KEY="ek",
STRIX_EXA_CONTENT_MODE="highlights",
)
def fake_post(_url: str, **kwargs: Any) -> _FakeResponse:
captured["json"] = kwargs["json"]
return _FakeResponse({"results": [{"url": "https://ex.example", "title": "Ex"}]})
monkeypatch.setattr(tool, "load_settings", _Settings)
monkeypatch.setattr(requests, "post", fake_post)
assert tool._do_search("q")["success"] is True
assert "highlights" in captured["json"]["contents"]
assert "summary" not in captured["json"]["contents"]
tool._exa_content("ek", "q", "auto", 5)
def test_do_search_reports_empty_exa_results_as_unexpected(