Log Brave search responses
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -8,8 +9,11 @@ import httpx
|
|||||||
|
|
||||||
from app.config import AIWebSearchConfig
|
from app.config import AIWebSearchConfig
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
SAFESEARCH_VALUES = {"off", "moderate", "strict"}
|
SAFESEARCH_VALUES = {"off", "moderate", "strict"}
|
||||||
FRESHNESS_RE = re.compile(r"^(pd|pw|pm|py|\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2})$")
|
FRESHNESS_RE = re.compile(r"^(pd|pw|pm|py|\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2})$")
|
||||||
|
MAX_LOG_BODY_CHARS = 1000
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -231,16 +235,59 @@ class BraveWebSearchClient:
|
|||||||
raise ValueError(f"Brave web search request failed: {exc}") from exc
|
raise ValueError(f"Brave web search request failed: {exc}") from exc
|
||||||
|
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
|
_log_error_response(response)
|
||||||
raise ValueError(_format_error_response(response))
|
raise ValueError(_format_error_response(response))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
_log_error_response(response)
|
||||||
raise ValueError("Brave web search returned invalid JSON") from exc
|
raise ValueError("Brave web search returned invalid JSON") from exc
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
|
_log_response_summary(response, query.query, results_count=0)
|
||||||
raise ValueError("Brave web search returned an unexpected response")
|
raise ValueError("Brave web search returned an unexpected response")
|
||||||
|
|
||||||
return parse_response(query.query, payload)
|
parsed = parse_response(query.query, payload)
|
||||||
|
_log_response_summary(response, query.query, results_count=len(parsed.results))
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _response_log_body(response: httpx.Response) -> str:
|
||||||
|
body = response.text.replace("\n", "\\n").replace("\r", "\\r")
|
||||||
|
if len(body) > MAX_LOG_BODY_CHARS:
|
||||||
|
return body[:MAX_LOG_BODY_CHARS] + "...(truncated)"
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def _response_url(response: httpx.Response) -> str:
|
||||||
|
try:
|
||||||
|
return str(response.request.url)
|
||||||
|
except RuntimeError:
|
||||||
|
return str(response.url)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_response_summary(
|
||||||
|
response: httpx.Response,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
results_count: int,
|
||||||
|
) -> None:
|
||||||
|
log.info(
|
||||||
|
"Brave web search response status=%s query=%r results=%s url=%s",
|
||||||
|
response.status_code,
|
||||||
|
query,
|
||||||
|
results_count,
|
||||||
|
_response_url(response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_error_response(response: httpx.Response) -> None:
|
||||||
|
log.warning(
|
||||||
|
"Brave web search error status=%s url=%s body=%s",
|
||||||
|
response.status_code,
|
||||||
|
_response_url(response),
|
||||||
|
_response_log_body(response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_error_response(response: httpx.Response) -> str:
|
def _format_error_response(response: httpx.Response) -> str:
|
||||||
|
|||||||
@@ -105,6 +105,62 @@ class WebSearchTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
assert seen_headers is not None
|
assert seen_headers is not None
|
||||||
self.assertEqual(seen_headers["x-subscription-token"], "test-key")
|
self.assertEqual(seen_headers["x-subscription-token"], "test-key")
|
||||||
|
|
||||||
|
async def test_search_logs_success_response_summary(self) -> None:
|
||||||
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
request=request,
|
||||||
|
json={
|
||||||
|
"query": {"original": "python"},
|
||||||
|
"web": {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Python",
|
||||||
|
"url": "https://www.python.org",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client = BraveWebSearchClient(
|
||||||
|
AIWebSearchConfig(api_key="test-key"),
|
||||||
|
transport=httpx.MockTransport(handler),
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertLogs("app.ai.web_search", level="INFO") as captured:
|
||||||
|
await client.search({"query": "python"})
|
||||||
|
|
||||||
|
logs = "\n".join(captured.output)
|
||||||
|
self.assertIn("Brave web search response status=200", logs)
|
||||||
|
self.assertIn("results=1", logs)
|
||||||
|
|
||||||
|
async def test_search_logs_error_response_body(self) -> None:
|
||||||
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
422,
|
||||||
|
request=request,
|
||||||
|
json={
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION",
|
||||||
|
"detail": "Unable to validate request parameter(s)",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client = BraveWebSearchClient(
|
||||||
|
AIWebSearchConfig(api_key="test-key"),
|
||||||
|
transport=httpx.MockTransport(handler),
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertLogs("app.ai.web_search", level="WARNING") as captured:
|
||||||
|
with self.assertRaisesRegex(ValueError, "Unable to validate"):
|
||||||
|
await client.search({"query": "python"})
|
||||||
|
|
||||||
|
logs = "\n".join(captured.output)
|
||||||
|
self.assertIn("Brave web search error status=422", logs)
|
||||||
|
self.assertIn("VALIDATION", logs)
|
||||||
|
|
||||||
async def test_search_rejects_missing_subscription_token_before_request(self) -> None:
|
async def test_search_rejects_missing_subscription_token_before_request(self) -> None:
|
||||||
async def handler(request: httpx.Request) -> httpx.Response:
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
self.fail("request should not be sent without a subscription token")
|
self.fail("request should not be sent without a subscription token")
|
||||||
|
|||||||
Reference in New Issue
Block a user