Skip to content

Commit 265f824

Browse files
committed
Retry empty JSON results in auto search
1 parent 4daaade commit 265f824

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

src/googleaisearch2api/app.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@
5858
google_block_has_ip_mismatch,
5959
parse_google_block_ips,
6060
)
61-
from .quality import assess_search_answer_quality, normalize_answer_for_prompt
61+
from .quality import (
62+
answer_has_empty_json_results,
63+
assess_search_answer_quality,
64+
normalize_answer_for_prompt,
65+
)
6266
from .query_adapter import (
6367
build_prompt_from_query_request,
6468
build_query_response,
@@ -89,6 +93,19 @@ def _normalize_result_for_prompt(prompt: str, result: GoogleAiResult) -> GoogleA
8993
return result.model_copy(update={"answer_text": normalized_answer})
9094

9195

96+
def _auto_empty_json_result_error(
97+
engine_label: str,
98+
prompt: str,
99+
result: GoogleAiResult,
100+
config: ServiceConfig,
101+
) -> str | None:
102+
if config.search_engine != "auto":
103+
return None
104+
if not answer_has_empty_json_results(prompt, result.answer_text):
105+
return None
106+
return f"{engine_label} returned empty JSON results in auto mode"
107+
108+
92109
class DuckAiCircuitBreaker:
93110
def __init__(self, cooldown_seconds: int) -> None:
94111
self._cooldown_seconds = max(cooldown_seconds, 0)
@@ -330,6 +347,23 @@ def _run_google_ai(
330347
error_message=message,
331348
)
332349
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=message)
350+
empty_json_message = _auto_empty_json_result_error(
351+
"Google",
352+
prompt,
353+
result,
354+
config,
355+
)
356+
if empty_json_message is not None:
357+
services.store.finish_request_error(
358+
request_id,
359+
empty_json_message,
360+
duration_ms,
361+
result=result,
362+
)
363+
raise HTTPException(
364+
status_code=status.HTTP_502_BAD_GATEWAY,
365+
detail=empty_json_message,
366+
)
333367
services.store.finish_request_success(request_id, result, duration_ms)
334368
if selection is not None:
335369
services.proxy_session_store.finish_request_success(
@@ -520,6 +554,23 @@ def _run_duck_ai(
520554
result=result,
521555
)
522556
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=message)
557+
empty_json_message = _auto_empty_json_result_error(
558+
"Duck.ai",
559+
prompt,
560+
result,
561+
config,
562+
)
563+
if empty_json_message is not None:
564+
services.store.finish_request_error(
565+
request_id,
566+
empty_json_message,
567+
duration_ms,
568+
result=result,
569+
)
570+
raise HTTPException(
571+
status_code=status.HTTP_502_BAD_GATEWAY,
572+
detail=empty_json_message,
573+
)
523574
services.store.finish_request_success(request_id, result, duration_ms)
524575
services.duck_circuit.record_success()
525576
if selection is not None:

src/googleaisearch2api/quality.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,17 @@ def _extract_json_results_payload(answer: str) -> tuple[dict[str, Any], str] | N
427427
return None
428428

429429

430+
def answer_has_empty_json_results(prompt: str, answer: str) -> bool:
431+
prompt_text = _normalize(prompt)
432+
if not _prompt_requests_json_results(prompt_text):
433+
return False
434+
parsed = _extract_json_results_payload(answer)
435+
if parsed is None:
436+
return False
437+
payload, _ = parsed
438+
return not payload["results"]
439+
440+
430441
def _is_usable_result_url(value: object) -> bool:
431442
raw_url = str(value or "").strip()
432443
if _url_has_artifacts(raw_url):

tests/test_app.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,70 @@ def test_query_auto_falls_back_to_duck_when_google_list_answer_is_too_short(
416416
assert "too short for the requested list" in (recent[1].error_message or "")
417417

418418

419+
def test_query_auto_falls_back_to_duck_when_google_json_results_are_empty(
420+
test_app,
421+
) -> None:
422+
prompt = (
423+
"只返回一个 JSON 对象,输出格式固定为 "
424+
'{"results":[{"title":"","content":"","source":"","url":"","published_date":"YYYY-MM-DD"}]}。'
425+
'若找不到足够直接相关的结果,返回 {"results": []}。'
426+
"问题:PingAn 000001.SZ 最新公告 新闻 催化 风险 最多返回 5 条"
427+
)
428+
duck_answer = (
429+
'{"results":[{"title":"平安银行公告","content":"平安银行发布公告。",'
430+
'"source":"证券时报","url":"https://www.stcn.com/article/detail/1234567.html",'
431+
'"published_date":"2026-05-27"}]}'
432+
)
433+
with TestClient(test_app) as client:
434+
_set_search_engine(test_app, "auto")
435+
google_pool = _install_fake_pool(test_app, answer_text='{"results": []}')
436+
duck_pool = _install_fake_duck_pool(test_app, answer_text=duck_answer)
437+
response = client.post(
438+
"/query",
439+
headers=_auth_headers(),
440+
json={"model": "google-search", "query": prompt},
441+
)
442+
recent = test_app.state.services.store.list_recent_requests(limit=2)
443+
444+
assert response.status_code == 200
445+
assert response.json()["answer"] == duck_answer
446+
assert google_pool.prompts == [f"User request:\n{prompt}"]
447+
assert duck_pool.prompts == [f"User request:\n{prompt}"]
448+
assert [record.engine for record in recent] == ["duck", "google"]
449+
assert [record.status for record in recent] == ["ok", "error"]
450+
assert "empty JSON results" in (recent[1].error_message or "")
451+
452+
453+
def test_query_auto_rejects_when_both_engines_return_empty_json_results(
454+
test_app,
455+
) -> None:
456+
prompt = (
457+
"只返回一个 JSON 对象,输出格式固定为 "
458+
'{"results":[{"title":"","content":"","source":"","url":"","published_date":"YYYY-MM-DD"}]}。'
459+
'若找不到足够直接相关的结果,返回 {"results": []}。'
460+
"问题:PingAn 000001.SZ 最新公告 新闻 催化 风险 最多返回 5 条"
461+
)
462+
with TestClient(test_app) as client:
463+
_set_search_engine(test_app, "auto")
464+
google_pool = _install_fake_pool(test_app, answer_text='{"results": []}')
465+
duck_pool = _install_fake_duck_pool(test_app, answer_text='{"results": []}')
466+
response = client.post(
467+
"/query",
468+
headers=_auth_headers(),
469+
json={"model": "google-search", "query": prompt},
470+
)
471+
recent = test_app.state.services.store.list_recent_requests(limit=2)
472+
473+
assert response.status_code == 502
474+
assert "Both search engines failed" in response.json()["detail"]
475+
assert "Google returned empty JSON results in auto mode" in response.json()["detail"]
476+
assert "Duck.ai returned empty JSON results in auto mode" in response.json()["detail"]
477+
assert google_pool.prompts == [f"User request:\n{prompt}"]
478+
assert duck_pool.prompts == [f"User request:\n{prompt}"]
479+
assert [record.engine for record in recent] == ["duck", "google"]
480+
assert [record.status for record in recent] == ["error", "error"]
481+
482+
419483
def test_query_uses_active_sticky_proxy_session_when_enabled(test_app) -> None:
420484
with TestClient(test_app) as client:
421485
test_app.state.services.store.update_config(

0 commit comments

Comments
 (0)