Skip to content

Commit 74eddab

Browse files
authored
fix: fail close topic brief provider shells (#129)
1 parent ef91447 commit 74eddab

3 files changed

Lines changed: 165 additions & 4 deletions

File tree

apps/orchestrator/src/openvibecoding_orch/scheduler/tool_execution_pipeline.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from tooling.page_brief_pipeline import write_page_brief_evidence_bundle, write_page_brief_result
1414
from tooling.search.ai_verifier import verify_search_results_ai
1515
from tooling.search_pipeline import (
16+
_has_public_source_hits,
17+
_summarize_public_source_failure,
1618
write_ai_verification,
1719
write_evidence_bundle,
1820
write_news_digest_result,
@@ -115,6 +117,10 @@ def _summarize_page_brief_failure(result: dict[str, Any]) -> str:
115117
return "页面抓取失败:浏览器任务未返回可用结果。"
116118

117119

120+
def _summarize_public_source_receipt_failure(results: list[dict[str, Any]]) -> str:
121+
return _summarize_public_source_failure(results)
122+
123+
118124
def _write_public_task_result(
119125
run_id: str,
120126
request: dict[str, Any],
@@ -287,13 +293,15 @@ def _run_search_task(job: tuple[str, str, dict[str, Any] | None, str]) -> dict[s
287293
domain_counts[domain] = domain_counts.get(domain, 0) + 1
288294
consensus_domains = [domain for domain, count in domain_counts.items() if count >= 2]
289295

296+
public_source_receipt_missing = not _has_public_source_hits(results)
290297
verification = {
291298
"queries": queries,
292299
"runs": len(results),
293300
"providers": provider_counts,
294-
"consensus_domains": consensus_domains,
301+
"consensus_domains": [] if public_source_receipt_missing else consensus_domains,
295302
"verification_runs": len(verify_results),
296-
"all_consistent": all(r.get("verification", {}).get("consistent") for r in results),
303+
"all_consistent": (not public_source_receipt_missing) and all(r.get("verification", {}).get("consistent") for r in results),
304+
"public_source_receipt_missing": public_source_receipt_missing,
297305
}
298306
if policy_adjustments:
299307
verification["policy_adjustments"] = policy_adjustments
@@ -339,21 +347,26 @@ def _run_search_task(job: tuple[str, str, dict[str, Any] | None, str]) -> dict[s
339347
verify_failures = [item for item in verify_results if isinstance(item, dict) and not item.get("ok", True)]
340348
verification["failure_count"] = len(failures)
341349
verification["verify_failure_count"] = len(verify_failures)
342-
if failures or verify_failures:
350+
if failures or verify_failures or public_source_receipt_missing:
343351
_write_public_task_result(
344352
run_id,
345353
request,
346354
results,
347355
store=store,
348356
status_override="FAILED",
349-
failure_reason_zh=_summarize_news_digest_failure(failures, verify_failures),
357+
failure_reason_zh=(
358+
_summarize_news_digest_failure(failures, verify_failures)
359+
if (failures or verify_failures)
360+
else _summarize_public_source_receipt_failure(results)
361+
),
350362
)
351363
return {
352364
"ok": False,
353365
"runs": len(results),
354366
"verification_runs": len(verify_results),
355367
"failures": failures,
356368
"verify_failures": verify_failures,
369+
"public_source_receipt_missing": public_source_receipt_missing,
357370
}
358371
_write_public_task_result(run_id, request, results, store=store)
359372
return {"ok": True, "runs": len(results), "verification_runs": len(verify_results)}

apps/orchestrator/tests/test_news_digest_template.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,44 @@ def test_topic_brief_intake_and_result_builder() -> None:
153153
assert search_payload["topic_brief_result"] == {"name": "topic_brief_result.json"}
154154

155155

156+
def test_topic_brief_fail_closes_when_only_provider_homepages_are_captured() -> None:
157+
search_request = {
158+
"task_template": "topic_brief",
159+
"template_payload": {
160+
"topic": "Seattle AI",
161+
"time_range": "24h",
162+
"max_results": 3,
163+
},
164+
}
165+
results = [
166+
{
167+
"provider": "gemini_web",
168+
"results": [
169+
{
170+
"title": "gemini_web response",
171+
"href": "https://gemini.google.com/",
172+
"snippet": "Gemini 与 Gemini 对话 你说 Seattle AI",
173+
}
174+
],
175+
},
176+
{
177+
"provider": "grok_web",
178+
"results": [
179+
{
180+
"title": "grok_web response",
181+
"href": "https://grok.com/",
182+
"snippet": "登录 注册 Seattle AI",
183+
}
184+
],
185+
},
186+
]
187+
brief = search_pipeline.build_topic_brief_result(search_request, results)
188+
assert brief is not None
189+
assert brief["status"] == "FAILED"
190+
assert "provider outputs stayed on provider shell pages" in brief["summary"]
191+
assert "provider 壳页" in brief["failure_reason_zh"]
192+
193+
156194
def test_page_brief_intake_builds_browser_contract_artifact(monkeypatch, tmp_path: Path) -> None:
157195
runtime_root = tmp_path / "runtime"
158196
monkeypatch.setenv("OPENVIBECODING_RUNTIME_ROOT", str(runtime_root))
@@ -370,3 +408,53 @@ def run_search(self, query: str, provider: str | None = None, browser_policy=Non
370408
assert digest_payload["status"] == "FAILED"
371409
assert digest_payload["summary"].startswith("The news digest for 'Seattle AI' did not complete successfully.")
372410
assert "来源链路失败" in digest_payload["failure_reason_zh"]
411+
412+
413+
def test_topic_brief_provider_homepage_only_results_write_failed_report(monkeypatch, tmp_path: Path) -> None:
414+
runtime_root = tmp_path / "runtime"
415+
monkeypatch.setenv("OPENVIBECODING_RUNTIME_ROOT", str(runtime_root))
416+
monkeypatch.setenv("OPENVIBECODING_RUNS_ROOT", str(runtime_root / "runs"))
417+
418+
store = RunStore()
419+
run_id = store.create_run("topic-brief-provider-homepage-only")
420+
421+
class ProviderHomepageOnlyToolRunner(ToolRunner):
422+
def __init__(self) -> None:
423+
super().__init__(run_id=run_id, store=store)
424+
425+
def run_search(self, query: str, provider: str | None = None, browser_policy=None, policy_audit=None) -> dict:
426+
normalized_provider = "gemini_web" if provider == "chatgpt_web" else str(provider or "")
427+
href = "https://gemini.google.com/" if normalized_provider == "gemini_web" else "https://grok.com/"
428+
return {
429+
"ok": True,
430+
"provider": normalized_provider,
431+
"results": [{"title": f"{normalized_provider} response", "href": href, "snippet": f"{query} provider shell"}],
432+
"verification": {"consistent": True},
433+
}
434+
435+
request = {
436+
"queries": ["Seattle AI"],
437+
"providers": ["chatgpt_web", "grok_web"],
438+
"verify": {"providers": ["chatgpt_web"], "repeat": 1},
439+
"task_template": "topic_brief",
440+
"template_payload": {
441+
"topic": "Seattle AI",
442+
"time_range": "24h",
443+
"max_results": 3,
444+
},
445+
}
446+
447+
result = run_search_pipeline(
448+
run_id,
449+
ProviderHomepageOnlyToolRunner(),
450+
store,
451+
request,
452+
requested_by={"role": "PM", "agent_id": "pm-1"},
453+
)
454+
assert result["ok"] is False
455+
assert result["public_source_receipt_missing"] is True
456+
run_dir = runtime_root / "runs" / run_id / "reports"
457+
digest_payload = json.loads((run_dir / "topic_brief_result.json").read_text(encoding="utf-8"))
458+
assert digest_payload["status"] == "FAILED"
459+
assert digest_payload["summary"].startswith("The topic brief for 'Seattle AI' did not complete successfully.")
460+
assert "provider 壳页" in digest_payload["failure_reason_zh"]

tooling/search_pipeline.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
from openvibecoding_orch.store.run_store import RunStore
1111
from openvibecoding_orch.contract.validator import ContractValidator
1212

13+
_CHAT_PROVIDER_HOSTS: dict[str, set[str]] = {
14+
"gemini_web": {"gemini.google.com"},
15+
"grok_web": {"grok.com"},
16+
"chatgpt_web": {"chatgpt.com", "chat.openai.com"},
17+
}
18+
1319

1420
def _now_ts() -> str:
1521
return datetime.now(timezone.utc).isoformat()
@@ -42,6 +48,47 @@ def _domain_from_href(href: str) -> str:
4248
return ""
4349

4450

51+
def _is_chat_provider_homepage(provider: Any, href: Any) -> bool:
52+
provider_name = str(provider or "").strip().lower()
53+
domain = _domain_from_href(str(href or "")).strip().lower()
54+
return bool(domain) and domain in _CHAT_PROVIDER_HOSTS.get(provider_name, set())
55+
56+
57+
def _has_public_source_hits(results: list[dict[str, Any]]) -> bool:
58+
for item in results:
59+
if not isinstance(item, dict):
60+
continue
61+
provider = item.get("provider") or item.get("resolved_provider") or item.get("mode") or "unknown"
62+
hits = item.get("results") if isinstance(item.get("results"), list) else []
63+
for hit in hits:
64+
if not isinstance(hit, dict):
65+
continue
66+
href = str(hit.get("href") or "").strip()
67+
if href and not _is_chat_provider_homepage(provider, href):
68+
return True
69+
return False
70+
71+
72+
def _summarize_public_source_failure(results: list[dict[str, Any]]) -> str:
73+
offenders: list[str] = []
74+
for item in results:
75+
if not isinstance(item, dict):
76+
continue
77+
provider = item.get("provider") or item.get("resolved_provider") or item.get("mode") or "unknown"
78+
hits = item.get("results") if isinstance(item.get("results"), list) else []
79+
for hit in hits:
80+
if not isinstance(hit, dict):
81+
continue
82+
href = str(hit.get("href") or "").strip()
83+
if href and _is_chat_provider_homepage(provider, href):
84+
offender = f"{str(provider).strip()} -> {_domain_from_href(href)}"
85+
if offender not in offenders:
86+
offenders.append(offender)
87+
if offenders:
88+
return f"来源链路失败:当前结果仍停在 provider 壳页而不是公开来源页面({', '.join(offenders)})。"
89+
return "来源链路失败:当前结果没有产出可公开审计的来源页面。"
90+
91+
4592
def _build_sources(results: list[dict]) -> list[dict]:
4693
sources: list[dict] = []
4794
retrieved_at = _now_ts()
@@ -117,6 +164,8 @@ def _purify_results(results: list[dict], verification: dict | None = None) -> di
117164
if not href:
118165
missing_href += 1
119166
continue
167+
if _is_chat_provider_homepage(provider, href):
168+
continue
120169
domain = _domain_from_href(str(href))
121170
if domain:
122171
domain_counts[domain] = domain_counts.get(domain, 0) + 1
@@ -242,6 +291,7 @@ def _build_digest_result(
242291
) -> dict[str, Any]:
243292
digest_sources: list[dict[str, Any]] = []
244293
seen_urls: set[str] = set()
294+
public_source_hit_found = False
245295
for provider_entry in results:
246296
if not isinstance(provider_entry, dict):
247297
continue
@@ -256,10 +306,13 @@ def _build_digest_result(
256306
if not isinstance(hit, dict):
257307
continue
258308
href = str(hit.get("href") or "").strip()
309+
if href and _is_chat_provider_homepage(provider, href):
310+
continue
259311
if href and href in seen_urls:
260312
continue
261313
if href:
262314
seen_urls.add(href)
315+
public_source_hit_found = True
263316
digest_sources.append(
264317
{
265318
"title": str(hit.get("title") or hit.get("name") or href or "result").strip() or "result",
@@ -283,6 +336,13 @@ def _build_digest_result(
283336
" Review failure_reason_zh and the evidence bundle for the detailed provider failure context."
284337
).strip()
285338
status = "FAILED"
339+
elif not public_source_hit_found and results:
340+
summary = (
341+
f"The {template_label} for '{topic}' did not produce a trustworthy public-source receipt."
342+
" The current provider outputs stayed on provider shell pages instead of auditable source URLs."
343+
)
344+
status = "FAILED"
345+
failure_reason_zh = failure_reason_zh or _summarize_public_source_failure(results)
286346
elif digest_sources:
287347
preview = ", ".join(item["title"] for item in digest_sources[:3])
288348
summary = (

0 commit comments

Comments
 (0)