Stop web_search from leaking upstream details into tool results
Failure messages were echoing the raw requests-exception text — for an empty query the model would see "API request failed: 400 Client Error: Bad Request for url: https://api.perplexity.ai/chat/completions" and learn the upstream URL, the HTTP status, and the literal word "API" none of which it has any use for or right to. Same pattern in every except branch: KeyError leaked internal field names, generic exceptions leaked library exception text, etc. Two fixes: - Pre-flight reject empty/whitespace queries so the trivial misuse case never hits the network at all and gets a "Query cannot be empty." result immediately. - Sanitize every failure path: split RequestException into HTTPError (4xx → "rejected the query — refine and retry", 5xx → "service unavailable"), Timeout, ConnectionError, response-shape (KeyError / IndexError / ValueError), and a generic catch-all. Each path returns a short actionable message and logs the full traceback via logger.exception so operator-side observability is preserved. The model sees no URLs, no status codes, no library exception text. While in here: the missing-API-key message keeps the env var name because that's operator-actionable, and the dead "results": [] field the failure paths used to carry is dropped (success path never had it either, so the shape was inconsistent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,14 +41,19 @@ Structure your response to be comprehensive yet concise, emphasizing the most cr
|
|||||||
security implications and details."""
|
security implications and details."""
|
||||||
|
|
||||||
|
|
||||||
def _do_search(query: str) -> dict[str, Any]:
|
def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error class needs its own sanitized return
|
||||||
|
if not query or not query.strip():
|
||||||
|
return {"success": False, "message": "Query cannot be empty."}
|
||||||
|
|
||||||
api_key = load_settings().integrations.perplexity_api_key
|
api_key = load_settings().integrations.perplexity_api_key
|
||||||
if not api_key:
|
if not api_key:
|
||||||
logger.warning("web_search invoked without PERPLEXITY_API_KEY configured")
|
logger.warning("web_search invoked without PERPLEXITY_API_KEY configured")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "PERPLEXITY_API_KEY environment variable not set",
|
"message": (
|
||||||
"results": [],
|
"Web search is not configured for this scan "
|
||||||
|
"(operator needs to set PERPLEXITY_API_KEY). Proceed without it."
|
||||||
|
),
|
||||||
}
|
}
|
||||||
logger.info("web_search query (len=%d): %s", len(query), query[:120])
|
logger.info("web_search query (len=%d): %s", len(query), query[:120])
|
||||||
|
|
||||||
@@ -62,32 +67,57 @@ def _do_search(query: str) -> dict[str, Any]:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Internal details (upstream URL, HTTP status, library exception text) stay
|
||||||
|
# in the logs; the model only ever sees a short actionable category so it
|
||||||
|
# can decide whether to retry, refine, or work around the gap.
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
content = response.json()["choices"][0]["message"]["content"]
|
content = response.json()["choices"][0]["message"]["content"]
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
logger.warning("web_search timed out")
|
logger.warning("web_search timed out")
|
||||||
return {"success": False, "message": "Request timed out", "results": []}
|
return {
|
||||||
except requests.exceptions.RequestException as e:
|
"success": False,
|
||||||
logger.exception("web_search API request failed")
|
"message": "Web search timed out. Try again or shorten the query.",
|
||||||
return {"success": False, "message": f"API request failed: {e!s}", "results": []}
|
}
|
||||||
except KeyError as e:
|
except requests.exceptions.HTTPError as exc:
|
||||||
|
status = exc.response.status_code if exc.response is not None else None
|
||||||
|
logger.exception("web_search HTTP error status=%s", status)
|
||||||
|
if status is not None and 400 <= status < 500:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": (
|
||||||
|
"Web search rejected the query. Refine it "
|
||||||
|
"(more specific, shorter, no unusual characters) and retry."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Web search service is unavailable. Try again later.",
|
||||||
|
}
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
logger.exception("web_search network error")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Web search network error. Try again later.",
|
||||||
|
}
|
||||||
|
except (KeyError, IndexError, ValueError):
|
||||||
logger.exception("web_search response shape unexpected")
|
logger.exception("web_search response shape unexpected")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Unexpected API response format: missing {e!s}",
|
"message": "Web search returned an unexpected response. Try again.",
|
||||||
"results": [],
|
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception("web_search failed")
|
logger.exception("web_search failed")
|
||||||
return {"success": False, "message": f"Web search failed: {e!s}", "results": []}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Web search failed unexpectedly.",
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"query": query,
|
"query": query,
|
||||||
"content": content,
|
"content": content,
|
||||||
"message": "Web search completed successfully",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user