From 34d53db041d4e100debc2e50632fa161c7768fa3 Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Fri, 4 Sep 2026 16:51:54 +0000 Subject: [PATCH] refactor(web_search): drop STRIX_EXA_CONTENT_MODE, Exa always returns summaries --- docs/advanced/configuration.mdx | 6 +-- strix/config/settings.py | 5 --- strix/tools/web_search/tool.py | 36 ++++-------------- tests/test_web_search.py | 66 +++++---------------------------- 4 files changed, 18 insertions(+), 95 deletions(-) diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index 105fb727..98a9d690 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -93,11 +93,7 @@ affecting the agents that do the actual testing. - Number of Exa results to return, from `1` to `100`. This setting applies only to the Exa provider. - - - - 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. diff --git a/strix/config/settings.py b/strix/config/settings.py index cc6847bc..9309ac39 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -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", diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py index 7dc33e15..8ad06eea 100644 --- a/strix/tools/web_search/tool.py +++ b/strix/tools/web_search/tool.py @@ -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 diff --git a/tests/test_web_search.py b/tests/test_web_search.py index f2e821d1..f9bd6fc3 100644 --- a/tests/test_web_search.py +++ b/tests/test_web_search.py @@ -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(