Coverage for web_search / runtime.py: 23%
52 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
1import os
2import time
3from typing import Optional, Dict, Any, List
4from qrclaw.web_search.types import WebSearchResponse, SearchResult, WebSearchProvider
5from qrclaw.web_search.provider_registry import list_providers, get_provider
6from qrclaw.config import TAVILY_API_KEY
7from qrclaw.logger import get_logger
9logger = get_logger("qrclaw.web_search.runtime")
11class WebSearchError(Exception):
12 """Web Search Runtime Error"""
13 pass
15def resolve_web_search_provider_id(prefer_provider: Optional[str] = None) -> Optional[str]:
16 """
17 确定使用哪个 Provider。
18 优先级逻辑:
19 1. 用户显式指定 (prefer_provider)
20 2. Tavily (最强,如果有 Key)
21 3. Google (次强,如果有 Key)
22 4. DuckDuckGo (免费保底)
23 """
24 if prefer_provider:
25 p_cls = get_provider(prefer_provider)
26 if p_cls and p_cls().is_available():
27 return prefer_provider
29 # 按优先级列表自动探测
30 # 这里我们硬编码一个优先级顺序,而不是简单遍历注册表
31 priority_order = ["tavily", "google", "duckduckgo"]
33 for pid in priority_order:
34 p_cls = get_provider(pid)
35 if p_cls:
36 provider = p_cls()
37 if provider.is_available():
38 logger.info(f"Auto-detected web search provider: {provider.name}")
39 return provider.id
41 # 如果上面的都没选中(理论上 DuckDuckGo 总是可用),再尝试剩下的
42 for p_cls in list_providers():
43 provider = p_cls()
44 if provider.is_available():
45 # 避免重复选
46 if provider.id not in priority_order:
47 logger.info(f"Auto-detected fallback provider: {provider.name}")
48 return provider.id
50 return None
52def run_web_search(query: str, max_results: int = 5, provider_id: Optional[str] = None, **kwargs) -> WebSearchResponse:
53 """
54 执行 Web 搜索
55 """
56 selected_id = provider_id or resolve_web_search_provider_id()
58 if not selected_id:
59 error_msg = "No web search provider is available."
60 logger.error(error_msg)
61 raise WebSearchError(error_msg)
63 provider_cls = get_provider(selected_id)
64 if not provider_cls:
65 raise WebSearchError(f"Provider '{selected_id}' not found.")
67 provider = provider_cls()
68 logger.info(f"Executing web search with provider: {provider.name}")
70 try:
71 return provider.search(query=query, max_results=max_results, **kwargs)
72 except Exception as e:
73 logger.error(f"Web search failed with {provider.name}: {e}")
75 # 简单的故障转移逻辑:如果首选失败,尝试降级到 DuckDuckGo
76 if provider.id != "duckduckgo":
77 logger.warning("Attempting fallback to DuckDuckGo...")
78 fallback_cls = get_provider("duckduckgo")
79 if fallback_cls:
80 fallback = fallback_cls()
81 return fallback.search(query=query, max_results=max_results, **kwargs)
83 raise