v0.12: stabilize search routing and profile-driven phase4 pipeline

This commit is contained in:
kai
2026-04-29 15:53:20 +08:00
parent 450ecebcff
commit 5342a26018
33 changed files with 1436 additions and 140 deletions
+24 -14
View File
@@ -1,11 +1,12 @@
"""通用搜索客户端(Exa 优先,Tavily fallback)。
"""通用搜索客户端(Serper / Exa / Tavily 路由)。
为 build_glossary.py 这类术语核查场景服务。
关键设计:
- `trust_env=False` 绕开系统 socks 代理(Clash on macOS 配 socks5 时 httpx 会 TLS EOF
- Exa 优先:LinkedIn / 官网 / 百度百科返回质量最高
- 遇到配额问题自动降级到 Tavily 或返回 empty
- 专利 / Scholar / News 优先 Serper,保证 Google Patents / Google Scholar 路径被真正调用
- 通用网页 Exa 优先,Tavily fallback
- 遇到配额问题自动降级或返回 empty
- 不做深度 crawl,只要摘要
"""
@@ -125,10 +126,11 @@ class SearchClient:
所有客户端都延迟导入 serper_client,避免没装 SERPAPI_KEY 时 import 炸。
"""
def __init__(self) -> None:
def __init__(self, *, strict_specialized: bool = True) -> None:
self._exa: ExaClient | None = None
self._tavily: TavilyClient | None = None
self._serper = None # 惰性实例化
self.strict_specialized = strict_specialized
try:
self._exa = ExaClient()
except SearchError:
@@ -137,10 +139,9 @@ class SearchClient:
self._tavily = TavilyClient()
except SearchError:
pass
if not (self._exa or self._tavily):
raise SearchError(
"neither EXA_API_KEY nor TAVILY_API_KEY available"
)
self._has_serper_key = bool(os.environ.get("SERPER_API_KEY") or os.environ.get("SERPAPI_KEY"))
if not (self._exa or self._tavily or self._has_serper_key):
raise SearchError("no search API key available: set SERPER_API_KEY, SERPAPI_KEY, EXA_API_KEY, or TAVILY_API_KEY")
def _get_serper(self):
"""惰性创建 SerperClient。没 key 时返回 None。"""
@@ -190,9 +191,12 @@ class SearchClient:
try:
hits = serper.patents(query, num_results=num_results)
return [SearchHit(h.title, h.url, h.snippet) for h in hits]
except Exception:
pass
except Exception as exc:
if self.strict_specialized:
raise SearchError(f"serper patents failed: {exc}") from exc
# 降级:通用搜索加 site 限定
if self.strict_specialized:
raise SearchError("serper unavailable for patents route; refusing silent fallback")
return self.search(f"site:patents.google.com {query}", num_results=num_results)
def scholar(
@@ -215,8 +219,11 @@ class SearchClient:
)
for h in hits
]
except Exception:
pass
except Exception as exc:
if self.strict_specialized:
raise SearchError(f"serper scholar failed: {exc}") from exc
if self.strict_specialized:
raise SearchError("serper unavailable for scholar route; refusing silent fallback")
return self.search(query, num_results=num_results)
def news(
@@ -239,8 +246,11 @@ class SearchClient:
)
for h in hits
]
except Exception:
pass
except Exception as exc:
if self.strict_specialized:
raise SearchError(f"serper news failed: {exc}") from exc
if self.strict_specialized:
raise SearchError("serper unavailable for news route; refusing silent fallback")
return self.search(query, num_results=num_results)