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(