T4: AnySearch 联网检索接入 (anysearch_client trust_env直连 NO_PROXY, kb_router use_web RRF融合, chat_context 自动注入联网上下文, admin 支持 anysearch 服务类型, 新增 POST /api/knowledge/web-search 端点)
This commit is contained in:
1 parent
be7d792dc2
commit
3c9d9518b1
5 files changed
+6161
-2993
No files matched your search
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
AnySearch 联网检索客户端
|
||||
======================
|
||||
调用 AnySearch 网页/领域搜索 API (https://api.anysearch.com/v1/search),
|
||||
为 AI 聊天注入实时联网检索结果,亦可被后端端点 / Agent 工具复用。
|
||||
|
||||
约束:平台容器存在智能路由代理,对外部 API 偶发重置;本客户端使用
|
||||
trust_env=False 直连 (等价于 NO_PROXY=api.anysearch.com),规避代理重置。
|
||||
失败一律返回空/降级结果,保证聊天链路不中断。
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("aiflow.anysearch")
|
||||
|
||||
DEFAULT_API_URL = "https://api.anysearch.com/v1/search"
|
||||
DEFAULT_ZONE = "cn"
|
||||
REQUEST_TIMEOUT = 8.0
|
||||
|
||||
|
||||
async def anysearch_search(
|
||||
api_key: str,
|
||||
api_url: str = DEFAULT_API_URL,
|
||||
query: str = "",
|
||||
top_k: int = 5,
|
||||
zone: str = DEFAULT_ZONE,
|
||||
freshness: Optional[str] = None,
|
||||
timeout: float = REQUEST_TIMEOUT,
|
||||
) -> List[Dict]:
|
||||
"""调用 AnySearch 搜索,返回结构化结果列表。
|
||||
|
||||
每个结果:
|
||||
{
|
||||
"content": snippet 摘要文本,
|
||||
"score": 0.9..0.5 递减,
|
||||
"source": "web_anysearch",
|
||||
"metadata": {"url", "title", "source_domain", "zone"}
|
||||
}
|
||||
失败时返回 [] (不抛异常)。
|
||||
"""
|
||||
if not api_key:
|
||||
return []
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
logger.warning("httpx not available for AnySearch")
|
||||
return []
|
||||
|
||||
payload = {
|
||||
"query": query,
|
||||
"zone": zone,
|
||||
"top_k": max(1, min(int(top_k), 20)),
|
||||
}
|
||||
if freshness:
|
||||
payload["freshness"] = freshness
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
try:
|
||||
# trust_env=False -> 忽略容器内 HTTP(S)_PROXY,直连 api.anysearch.com
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=timeout) as client:
|
||||
resp = await client.post(api_url, json=payload, headers=headers)
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"AnySearch HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
return []
|
||||
data = resp.json()
|
||||
if data.get("code") != 0:
|
||||
logger.warning(f"AnySearch biz error: {data.get('message')}")
|
||||
return []
|
||||
results = (data.get("data") or {}).get("results") or []
|
||||
hits = []
|
||||
for idx, r in enumerate(results[:top_k]):
|
||||
title = r.get("title", "") or ""
|
||||
url = r.get("url", "") or ""
|
||||
snippet = r.get("snippet") or r.get("content") or ""
|
||||
if not snippet and not title:
|
||||
continue
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
domain = urlparse(url).netloc
|
||||
except Exception:
|
||||
domain = ""
|
||||
hits.append({
|
||||
"content": snippet,
|
||||
"score": max(0.4, 0.9 - idx * 0.08),
|
||||
"source": "web_anysearch",
|
||||
"metadata": {
|
||||
"url": url,
|
||||
"title": title,
|
||||
"source_domain": domain,
|
||||
"zone": zone,
|
||||
},
|
||||
})
|
||||
return hits
|
||||
except Exception as e:
|
||||
logger.warning(f"AnySearch search failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def anysearch_health(api_key: str, api_url: str = DEFAULT_API_URL, timeout: float = REQUEST_TIMEOUT) -> Dict:
|
||||
"""连通性探测:返回 {"status": "connected"/"disconnected", "detail": ...}。"""
|
||||
if not api_key:
|
||||
return {"status": "disconnected", "detail": "未配置 API Key", "connected": False, "usable": False}
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=timeout) as client:
|
||||
resp = await client.post(
|
||||
api_url,
|
||||
json={"query": "ping", "zone": DEFAULT_ZONE, "top_k": 1},
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return {"status": "connected", "detail": "AnySearch 连接正常", "connected": True, "usable": True}
|
||||
return {"status": "error", "detail": f"HTTP {resp.status_code}", "connected": False, "usable": False}
|
||||
except Exception as e:
|
||||
return {"status": "error", "detail": str(e)[:200], "connected": False, "usable": False}
|
||||
Reference in new issue
Block a user