Skip to content

Commit 7965008

Browse files
committed
feat(search): add tavily/perplexity and default bing fallback
1 parent 568449e commit 7965008

5 files changed

Lines changed: 466 additions & 43 deletions

File tree

backend/config/settings.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@
2222

2323
SEARCH_PROVIDER_FIRECRAWL = "firecrawl"
2424
SEARCH_PROVIDER_EXA = "exa"
25+
SEARCH_PROVIDER_TAVILY = "tavily"
26+
SEARCH_PROVIDER_PERPLEXITY = "perplexity"
27+
SEARCH_PROVIDER_BING = "bing"
2528
DEFAULT_FIRECRAWL_BASE_URL = "https://api.firecrawl.dev"
2629
DEFAULT_EXA_BASE_URL = "https://api.exa.ai"
30+
DEFAULT_TAVILY_BASE_URL = "https://api.tavily.com"
31+
DEFAULT_PERPLEXITY_BASE_URL = "https://api.perplexity.ai"
32+
DEFAULT_BING_BASE_URL = "https://www.bing.com"
2733

2834

2935
def _get_project_root() -> Path:
@@ -346,7 +352,7 @@ def save_text_providers_config(cls, config: Dict[str, Any]) -> None:
346352
@classmethod
347353
def _default_search_providers_config(cls) -> Dict[str, Any]:
348354
return {
349-
"active_provider": SEARCH_PROVIDER_FIRECRAWL,
355+
"active_provider": SEARCH_PROVIDER_BING,
350356
"providers": {
351357
SEARCH_PROVIDER_FIRECRAWL: {
352358
"type": SEARCH_PROVIDER_FIRECRAWL,
@@ -360,6 +366,25 @@ def _default_search_providers_config(cls) -> Dict[str, Any]:
360366
"api_key": "",
361367
"base_url": "",
362368
},
369+
SEARCH_PROVIDER_TAVILY: {
370+
"type": SEARCH_PROVIDER_TAVILY,
371+
"enabled": False,
372+
"api_key": "",
373+
"base_url": "",
374+
},
375+
SEARCH_PROVIDER_PERPLEXITY: {
376+
"type": SEARCH_PROVIDER_PERPLEXITY,
377+
"enabled": False,
378+
"api_key": "",
379+
"base_url": "",
380+
"model": "sonar",
381+
},
382+
SEARCH_PROVIDER_BING: {
383+
"type": SEARCH_PROVIDER_BING,
384+
"enabled": True,
385+
"api_key": "",
386+
"base_url": "",
387+
},
363388
},
364389
}
365390

@@ -371,6 +396,8 @@ def _normalize_search_provider_item(cls, provider_name: str, item: Any) -> Dict[
371396
normalized["enabled"] = bool(normalized.get("enabled", False))
372397
normalized["api_key"] = (normalized.get("api_key") or "").strip()
373398
normalized["base_url"] = (normalized.get("base_url") or "").strip()
399+
if normalized.get("model") is not None:
400+
normalized["model"] = str(normalized.get("model") or "").strip()
374401
return normalized
375402

376403
@classmethod
@@ -387,23 +414,17 @@ def _normalize_search_providers_config(cls, config: Dict[str, Any]) -> Dict[str,
387414
else:
388415
providers = deepcopy(cls._default_search_providers_config()["providers"])
389416

390-
# 保证 firecrawl/exa 至少存在
391-
if SEARCH_PROVIDER_FIRECRAWL not in providers:
392-
providers[SEARCH_PROVIDER_FIRECRAWL] = cls._normalize_search_provider_item(
393-
SEARCH_PROVIDER_FIRECRAWL,
394-
cls._default_search_providers_config()["providers"][SEARCH_PROVIDER_FIRECRAWL],
395-
)
396-
if SEARCH_PROVIDER_EXA not in providers:
397-
providers[SEARCH_PROVIDER_EXA] = cls._normalize_search_provider_item(
398-
SEARCH_PROVIDER_EXA,
399-
cls._default_search_providers_config()["providers"][SEARCH_PROVIDER_EXA],
400-
)
417+
# 保证默认搜索服务商都存在
418+
default_providers = cls._default_search_providers_config()["providers"]
419+
for provider_name, provider_item in default_providers.items():
420+
if provider_name not in providers:
421+
providers[provider_name] = cls._normalize_search_provider_item(provider_name, provider_item)
401422

402423
active_provider = str(config.get("active_provider") or "").strip()
403424
if not active_provider or active_provider not in providers:
404425
active_provider = cls._resolve_active_provider(
405426
{"active_provider": active_provider, "providers": providers},
406-
SEARCH_PROVIDER_FIRECRAWL,
427+
SEARCH_PROVIDER_BING,
407428
)
408429

409430
return {
@@ -430,7 +451,7 @@ def save_search_providers_config(cls, config: Dict[str, Any]) -> None:
430451
@classmethod
431452
def get_active_search_provider(cls) -> str:
432453
config = cls.load_search_providers_config()
433-
return cls._resolve_active_provider(config, SEARCH_PROVIDER_FIRECRAWL)
454+
return cls._resolve_active_provider(config, SEARCH_PROVIDER_BING)
434455

435456
@classmethod
436457
def get_search_provider_config(
@@ -443,6 +464,7 @@ def get_search_provider_config(
443464
if not providers:
444465
raise ValueError("未找到任何搜索服务商配置")
445466

467+
requested_provider_name = provider_name
446468
provider_name = provider_name or cls.get_active_search_provider()
447469
if provider_name not in providers:
448470
available = ", ".join(providers.keys())
@@ -452,17 +474,40 @@ def get_search_provider_config(
452474
provider_type = (provider_config.get("type") or provider_name).strip().lower()
453475
provider_config["type"] = provider_type
454476

477+
if require_enabled and not bool(provider_config.get("enabled", False)):
478+
# 未显式指定 provider 时,自动回退到默认 bing
479+
if requested_provider_name is None and provider_name != SEARCH_PROVIDER_BING:
480+
fallback = providers.get(SEARCH_PROVIDER_BING) or {}
481+
if bool(fallback.get("enabled", False)):
482+
provider_name = SEARCH_PROVIDER_BING
483+
provider_config = fallback.copy()
484+
provider_type = (provider_config.get("type") or provider_name).strip().lower()
485+
provider_config["type"] = provider_type
486+
else:
487+
raise ValueError(f"搜索服务商 [{provider_name}] 未启用")
488+
else:
489+
raise ValueError(f"搜索服务商 [{provider_name}] 未启用")
490+
455491
if require_enabled and not bool(provider_config.get("enabled", False)):
456492
raise ValueError(f"搜索服务商 [{provider_name}] 未启用")
457493

458-
if provider_type == SEARCH_PROVIDER_EXA and not (provider_config.get("api_key") or "").strip():
494+
requires_api_key = {
495+
SEARCH_PROVIDER_EXA,
496+
SEARCH_PROVIDER_TAVILY,
497+
SEARCH_PROVIDER_PERPLEXITY,
498+
}
499+
if provider_type in requires_api_key and not (provider_config.get("api_key") or "").strip():
459500
raise ValueError(f"搜索服务商 [{provider_name}] 未配置 API Key")
460501

502+
default_base_urls = {
503+
SEARCH_PROVIDER_FIRECRAWL: DEFAULT_FIRECRAWL_BASE_URL,
504+
SEARCH_PROVIDER_EXA: DEFAULT_EXA_BASE_URL,
505+
SEARCH_PROVIDER_TAVILY: DEFAULT_TAVILY_BASE_URL,
506+
SEARCH_PROVIDER_PERPLEXITY: DEFAULT_PERPLEXITY_BASE_URL,
507+
SEARCH_PROVIDER_BING: DEFAULT_BING_BASE_URL,
508+
}
461509
if not (provider_config.get("base_url") or "").strip():
462-
if provider_type == SEARCH_PROVIDER_FIRECRAWL:
463-
provider_config["base_url"] = DEFAULT_FIRECRAWL_BASE_URL
464-
elif provider_type == SEARCH_PROVIDER_EXA:
465-
provider_config["base_url"] = DEFAULT_EXA_BASE_URL
510+
provider_config["base_url"] = default_base_urls.get(provider_type, "")
466511

467512
return provider_config
468513

backend/routes/config_routes.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def get_config():
6565
search_config = Config.load_search_providers_config()
6666
search_providers = search_config.get("providers", {})
6767
search_response = {
68-
"active_provider": search_config.get("active_provider", "firecrawl"),
68+
"active_provider": search_config.get("active_provider", "bing"),
6969
"providers": _prepare_search_providers_for_response(search_providers),
7070
}
7171

@@ -157,7 +157,7 @@ def test_connection():
157157
测试服务商连接
158158
159159
请求体:
160-
- type: 服务商类型(google_genai/google_gemini/openai_compatible/image_api/firecrawl/exa)
160+
- type: 服务商类型(google_genai/google_gemini/openai_compatible/image_api/firecrawl/exa/tavily/perplexity/bing
161161
- provider_name: 服务商名称(用于从配置读取 API Key)
162162
- api_key: API Key(可选,若不提供则从配置读取)
163163
- base_url: Base URL(可选)
@@ -189,7 +189,7 @@ def test_connection():
189189
if not config['api_key'] and provider_name:
190190
config = _load_provider_config(provider_type, provider_name, config)
191191

192-
if not config['api_key'] and provider_type != 'firecrawl':
192+
if not config['api_key'] and provider_type not in {'firecrawl', 'bing'}:
193193
return jsonify({"success": False, "error": "API Key 未配置"}), 400
194194

195195
# 根据类型执行测试
@@ -381,8 +381,8 @@ def _load_provider_config(provider_type: str, provider_name: str, config: dict)
381381
Returns:
382382
dict: 合并后的配置
383383
"""
384-
# 搜索服务商配置独立保存(firecrawl/exa)
385-
if provider_type in ['firecrawl', 'exa']:
384+
# 搜索服务商配置独立保存(firecrawl/exa/tavily/perplexity/bing
385+
if provider_type in ['firecrawl', 'exa', 'tavily', 'perplexity', 'bing']:
386386
from backend.config import Config
387387
search_config = Config.load_search_providers_config()
388388
providers = search_config.get("providers", {})
@@ -396,6 +396,8 @@ def _load_provider_config(provider_type: str, provider_name: str, config: dict)
396396
config['enabled'] = saved.get('enabled')
397397
if not config.get('type'):
398398
config['type'] = saved.get('type') or provider_type
399+
if not config.get('model'):
400+
config['model'] = saved.get('model')
399401
return config
400402

401403
# 确定配置文件路径
@@ -461,6 +463,15 @@ def _test_provider_connection(provider_type: str, config: dict) -> dict:
461463
elif provider_type == 'exa':
462464
return _test_exa(config)
463465

466+
elif provider_type == 'tavily':
467+
return _test_tavily(config)
468+
469+
elif provider_type == 'perplexity':
470+
return _test_perplexity(config)
471+
472+
elif provider_type == 'bing':
473+
return _test_bing(config)
474+
464475
else:
465476
raise ValueError(f"不支持的类型: {provider_type}")
466477

@@ -719,6 +730,48 @@ def _test_exa(config: dict) -> dict:
719730
return result
720731

721732

733+
def _test_tavily(config: dict) -> dict:
734+
"""测试 Tavily 连接。"""
735+
from backend.services.search_service import test_provider
736+
737+
result = test_provider("tavily", {
738+
"type": "tavily",
739+
"api_key": config.get("api_key"),
740+
"base_url": config.get("base_url"),
741+
})
742+
if not result.get("success"):
743+
raise Exception(result.get("message") or "Tavily 连接失败")
744+
return result
745+
746+
747+
def _test_perplexity(config: dict) -> dict:
748+
"""测试 Perplexity 连接。"""
749+
from backend.services.search_service import test_provider
750+
751+
result = test_provider("perplexity", {
752+
"type": "perplexity",
753+
"api_key": config.get("api_key"),
754+
"base_url": config.get("base_url"),
755+
"model": config.get("model"),
756+
})
757+
if not result.get("success"):
758+
raise Exception(result.get("message") or "Perplexity 连接失败")
759+
return result
760+
761+
762+
def _test_bing(config: dict) -> dict:
763+
"""测试 Bing 抓取能力。"""
764+
from backend.services.search_service import test_provider
765+
766+
result = test_provider("bing", {
767+
"type": "bing",
768+
"base_url": config.get("base_url"),
769+
})
770+
if not result.get("success"):
771+
raise Exception(result.get("message") or "Bing 连接失败")
772+
return result
773+
774+
722775
def _check_response(result_text: str) -> dict:
723776
"""检查响应是否符合预期"""
724777
if "你好" in result_text and "渲染AI" in result_text:

0 commit comments

Comments
 (0)