mirror of
https://github.com/usestrix/strix.git
synced 2026-10-13 03:57:59 +00:00
refactor(web_search): drop STRIX_EXA_CONTENT_MODE, Exa always returns summaries
This commit is contained in:
parent
a4fa849f59
commit
34d53db041
4 changed files with 18 additions and 95 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue