BaiFu
Committed by GitHub

Feat: 新增安思派AI搜索支持 (#457) (#461)

* :sparkling: 安思派搜索框架定义

* feat: 安思派搜索接入后端实现

* feat: MediaEngine中接入AnspireSearchAgent

* feat: 修改前端以适配新搜索agent

Co-authored-by: Zhang Yuxiang <51037789+NTFago@users.noreply.github.com>
@@ -66,6 +66,13 @@ KEYWORD_OPTIMIZER_MODEL_NAME= @@ -66,6 +66,13 @@ KEYWORD_OPTIMIZER_MODEL_NAME=
66 # Tavily API密钥,用于Tavily网络搜索,申请地址:https://www.tavily.com/ 66 # Tavily API密钥,用于Tavily网络搜索,申请地址:https://www.tavily.com/
67 TAVILY_API_KEY= 67 TAVILY_API_KEY=
68 68
  69 +# 网络搜索工具类型,支持BochaAPI或AnspireAPI两种,默认为AnspireAPI
  70 +SEARCH_TOOL_TYPE=AnspireAPI
  71 +
69 # Bocha AI Search BASEURL,用于Bocha多模态搜索,这里密钥名称虽然是Web Search,但其实是要AI Search的,申请地址:https://open.bochaai.com/ 72 # Bocha AI Search BASEURL,用于Bocha多模态搜索,这里密钥名称虽然是Web Search,但其实是要AI Search的,申请地址:https://open.bochaai.com/
70 BOCHA_BASE_URL=https://api.bocha.cn/v1/ai-search 73 BOCHA_BASE_URL=https://api.bocha.cn/v1/ai-search
71 -BOCHA_WEB_SEARCH_API_KEY=  
  74 +BOCHA_WEB_SEARCH_API_KEY=
  75 +
  76 +# Anspire AI Search API(申请地址:https://open.anspire.cn/)
  77 +ANSPIRE_BASE_URL=https://plugin.anspire.cn/api/ntsearch/search
  78 +ANSPIRE_API_KEY=
@@ -3,10 +3,10 @@ Deep Search Agent @@ -3,10 +3,10 @@ Deep Search Agent
3 一个无框架的深度搜索AI代理实现 3 一个无框架的深度搜索AI代理实现
4 """ 4 """
5 5
6 -from .agent import DeepSearchAgent, create_agent 6 +from .agent import DeepSearchAgent, AnspireSearchAgent, create_agent
7 from .utils.config import Settings 7 from .utils.config import Settings
8 8
9 __version__ = "1.0.0" 9 __version__ = "1.0.0"
10 __author__ = "Deep Search Agent Team" 10 __author__ = "Deep Search Agent Team"
11 11
12 -__all__ = ["DeepSearchAgent", "create_agent", "Settings"] 12 +__all__ = ["DeepSearchAgent", "AnspireSearchAgent", "create_agent", "Settings"]
@@ -19,7 +19,7 @@ from .nodes import ( @@ -19,7 +19,7 @@ from .nodes import (
19 ReportFormattingNode 19 ReportFormattingNode
20 ) 20 )
21 from .state import State 21 from .state import State
22 -from .tools import BochaMultimodalSearch, BochaResponse 22 +from .tools import BochaMultimodalSearch, BochaResponse, AnspireAISearch, AnspireResponse
23 from .utils import settings, Settings, format_search_results_for_prompt 23 from .utils import settings, Settings, format_search_results_for_prompt
24 24
25 25
@@ -50,7 +50,7 @@ class DeepSearchAgent: @@ -50,7 +50,7 @@ class DeepSearchAgent:
50 # 确保输出目录存在 50 # 确保输出目录存在
51 os.makedirs(self.config.OUTPUT_DIR, exist_ok=True) 51 os.makedirs(self.config.OUTPUT_DIR, exist_ok=True)
52 52
53 - logger.info(f"Meida Agent已初始化") 53 + logger.info(f"Media Agent已初始化")
54 logger.info(f"使用LLM: {self.llm_client.get_model_info()}") 54 logger.info(f"使用LLM: {self.llm_client.get_model_info()}")
55 logger.info(f"搜索工具集: BochaMultimodalSearch (支持5种多模态搜索工具)") 55 logger.info(f"搜索工具集: BochaMultimodalSearch (支持5种多模态搜索工具)")
56 56
@@ -436,6 +436,60 @@ class DeepSearchAgent: @@ -436,6 +436,60 @@ class DeepSearchAgent:
436 self.state.save_to_file(filepath) 436 self.state.save_to_file(filepath)
437 logger.info(f"状态已保存到 {filepath}") 437 logger.info(f"状态已保存到 {filepath}")
438 438
  439 +class AnspireSearchAgent(DeepSearchAgent):
  440 + """调用Anspire搜索引擎的Deep Search Agent"""
  441 +
  442 + def __init__(self, config: Settings | None = None):
  443 + self.config = config or settings
  444 +
  445 + # 初始化LLM客户端
  446 + self.llm_client = self._initialize_llm()
  447 +
  448 + # 初始化搜索工具集
  449 + self.search_agency = AnspireAISearch(api_key=self.config.ANSPIRE_API_KEY)
  450 +
  451 + # 初始化节点
  452 + self._initialize_nodes()
  453 +
  454 + # 状态
  455 + self.state = State()
  456 +
  457 + # 确保输出目录存在
  458 + os.makedirs(self.config.OUTPUT_DIR, exist_ok=True)
  459 +
  460 + logger.info(f"Media Agent已初始化")
  461 + logger.info(f"使用LLM: {self.llm_client.get_model_info()}")
  462 + logger.info(f"搜索工具集: AnspireSearch")
  463 +
  464 + def execute_search_tool(self, tool_name: str, query: str, **kwargs) -> AnspireResponse:
  465 + # TODO: 使用Anspire搜索工具执行搜索
  466 + """
  467 + 执行指定的搜索工具
  468 +
  469 + Args:
  470 + tool_name: 工具名称,可选值:
  471 + - "comprehensive_search": 全面综合搜索(默认)
  472 + - "search_last_24_hours": 24小时内最新信息
  473 + - "search_last_week": 本周信息
  474 + query: 搜索查询
  475 + **kwargs: 额外参数(如max_results)
  476 +
  477 + Returns:
  478 + AnspireResponse对象
  479 + """
  480 + logger.info(f" → 执行搜索工具: {tool_name}")
  481 +
  482 + if tool_name == "comprehensive_search":
  483 + max_results = kwargs.get("max_results", 10)
  484 + return self.search_agency.comprehensive_search(query, max_results)
  485 + elif tool_name == "search_last_24_hours":
  486 + return self.search_agency.search_last_24_hours(query)
  487 + elif tool_name == "search_last_week":
  488 + return self.search_agency.search_last_week(query)
  489 + else:
  490 + logger.info(f" ⚠️ 未知的搜索工具: {tool_name},使用默认综合搜索")
  491 + return self.search_agency.comprehensive_search(query)
  492 +
439 493
440 def create_agent(config_file: Optional[str] = None) -> DeepSearchAgent: 494 def create_agent(config_file: Optional[str] = None) -> DeepSearchAgent:
441 """ 495 """
@@ -448,4 +502,6 @@ def create_agent(config_file: Optional[str] = None) -> DeepSearchAgent: @@ -448,4 +502,6 @@ def create_agent(config_file: Optional[str] = None) -> DeepSearchAgent:
448 DeepSearchAgent实例 502 DeepSearchAgent实例
449 """ 503 """
450 settings = Settings() 504 settings = Settings()
  505 + if settings.SEARCH_TOOL_TYPE == "AnspireAPI":
  506 + return AnspireSearchAgent(settings)
451 return DeepSearchAgent(settings) 507 return DeepSearchAgent(settings)
@@ -5,18 +5,22 @@ @@ -5,18 +5,22 @@
5 5
6 from .search import ( 6 from .search import (
7 BochaMultimodalSearch, 7 BochaMultimodalSearch,
  8 + AnspireAISearch,
8 WebpageResult, 9 WebpageResult,
9 ImageResult, 10 ImageResult,
10 ModalCardResult, 11 ModalCardResult,
11 BochaResponse, 12 BochaResponse,
  13 + AnspireResponse,
12 print_response_summary 14 print_response_summary
13 ) 15 )
14 16
15 __all__ = [ 17 __all__ = [
16 "BochaMultimodalSearch", 18 "BochaMultimodalSearch",
  19 + "AnspireAISearch",
17 "WebpageResult", 20 "WebpageResult",
18 "ImageResult", 21 "ImageResult",
19 "ModalCardResult", 22 "ModalCardResult",
20 "BochaResponse", 23 "BochaResponse",
  24 + "AnspireResponse",
21 "print_response_summary" 25 "print_response_summary"
22 ] 26 ]
@@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
23 import os 23 import os
24 import json 24 import json
25 import sys 25 import sys
  26 +import datetime
26 from typing import List, Dict, Any, Optional, Literal 27 from typing import List, Dict, Any, Optional, Literal
27 28
28 from loguru import logger 29 from loguru import logger
@@ -85,6 +86,14 @@ class BochaResponse: @@ -85,6 +86,14 @@ class BochaResponse:
85 images: List[ImageResult] = field(default_factory=list) 86 images: List[ImageResult] = field(default_factory=list)
86 modal_cards: List[ModalCardResult] = field(default_factory=list) 87 modal_cards: List[ModalCardResult] = field(default_factory=list)
87 88
  89 +@dataclass
  90 +class AnspireResponse:
  91 + """封装 Anspire API 的完整返回结果,以便在工具间传递"""
  92 + query: str
  93 + conversation_id: Optional[str] = None
  94 + score: Optional[float] = None
  95 + webpages: List[WebpageResult] = field(default_factory=list)
  96 +
88 97
89 # --- 2. 核心客户端与专用工具集 --- 98 # --- 2. 核心客户端与专用工具集 ---
90 99
@@ -94,7 +103,7 @@ class BochaMultimodalSearch: @@ -94,7 +103,7 @@ class BochaMultimodalSearch:
94 每个公共方法都设计为供 AI Agent 独立调用的工具。 103 每个公共方法都设计为供 AI Agent 独立调用的工具。
95 """ 104 """
96 105
97 - BOCHA_BASE_URL = settings.BOCHA_BASE_URL or "https://api.bochaai.com/v1/ai-search" 106 + BOCHA_BASE_URL = settings.BOCHA_BASE_URL or "https://api.bocha.com/v1/ai-search"
98 107
99 def __init__(self, api_key: Optional[str] = None): 108 def __init__(self, api_key: Optional[str] = None):
100 """ 109 """
@@ -181,6 +190,7 @@ class BochaMultimodalSearch: @@ -181,6 +190,7 @@ class BochaMultimodalSearch:
181 payload.update(kwargs) 190 payload.update(kwargs)
182 191
183 try: 192 try:
  193 +
184 response = requests.post(self.BOCHA_BASE_URL, headers=self._headers, json=payload, timeout=30) 194 response = requests.post(self.BOCHA_BASE_URL, headers=self._headers, json=payload, timeout=30)
185 response.raise_for_status() # 如果HTTP状态码是4xx或5xx,则抛出异常 195 response.raise_for_status() # 如果HTTP状态码是4xx或5xx,则抛出异常
186 196
@@ -255,22 +265,138 @@ class BochaMultimodalSearch: @@ -255,22 +265,138 @@ class BochaMultimodalSearch:
255 logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---") 265 logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---")
256 return self._search_internal(query=query, freshness='oneWeek', answer=True) 266 return self._search_internal(query=query, freshness='oneWeek', answer=True)
257 267
  268 +class AnspireAISearch:
  269 + """
  270 + Anspire AI Search 客户端
  271 + """
  272 + ANSPIRE_BASE_URL = settings.ANSPIRE_BASE_URL or "https://plugin.anspire.cn/api/ntsearch/search"
258 273
259 -# --- 3. 测试与使用示例 --- 274 + def __init__(self, api_key: Optional[str] = None):
  275 + """
  276 + 初始化客户端。
  277 + Args:
  278 + api_key: Anspire API密钥,若不提供则从环境变量 ANSPIRE_API_KEY 读取。
  279 + """
  280 + if api_key is None:
  281 + api_key = settings.ANSPIRE_API_KEY
  282 + if not api_key:
  283 + raise ValueError("Anspire API Key未找到!请设置 ANSPIRE_API_KEY 环境变量或在初始化时提供")
  284 +
  285 + self._headers = {
  286 + 'Authorization': f'Bearer {api_key}',
  287 + 'Content-Type': 'application/json',
  288 + 'Connection': 'keep-alive',
  289 + 'Accept': '*/*'
  290 + }
  291 +
  292 + def _parse_search_response(self, response_dict: Dict[str, Any], query: str) -> AnspireResponse:
  293 + final_response = AnspireResponse(query=query)
  294 + final_response.conversation_id = response_dict.get('Uuid')
  295 +
  296 + messages = response_dict.get("results", [])
  297 + for msg in messages:
  298 + final_response.score = msg.get("score")
  299 + final_response.webpages.append(WebpageResult(
  300 + name = msg.get("title", ""),
  301 + url = msg.get("url", ""),
  302 + snippet = msg.get("content", ""),
  303 + date_last_crawled = msg.get("date", None)
  304 + ))
  305 +
  306 + return final_response
  307 +
  308 + @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return=AnspireResponse(query="搜索失败"))
  309 + def _search_internal(self, **kwargs) -> AnspireResponse:
  310 + """内部通用的搜索执行器,所有工具最终都调用此方法"""
  311 + query = kwargs.get("query", "Unknown Query")
  312 + payload = {
  313 + "query": query,
  314 + "top_k": kwargs.get("top_k", 10),
  315 + "Insite": kwargs.get("Insite", ""),
  316 + "FromTime": kwargs.get("FromTime", ""),
  317 + "ToTime": kwargs.get("ToTime", "")
  318 + }
  319 +
  320 + try:
  321 + response = requests.get(self.ANSPIRE_BASE_URL, headers=self._headers, params=payload, timeout=30)
  322 + response.raise_for_status() # 如果HTTP状态码是4xx或5xx,则抛出异常
  323 +
  324 + response_dict = response.json()
  325 + return self._parse_search_response(response_dict, query)
  326 + except requests.exceptions.RequestException as e:
  327 + logger.exception(f"搜索时发生网络错误: {str(e)}")
  328 + raise e # 让重试机制捕获并处理
  329 + except Exception as e:
  330 + logger.exception(f"处理响应时发生未知错误: {str(e)}")
  331 + raise e # 让重试机制捕获并处理
  332 +
  333 + def comprehensive_search(self, query: str, max_results: int = 10) -> AnspireResponse:
  334 + """
  335 + 【工具】综合搜索: 获取关于某个主题的全面信息,包括网页。
  336 + 适用于需要多种信息来源的场景。
  337 + """
  338 + logger.info(f"--- TOOL: 综合搜索 (query: {query}) ---")
  339 + return self._search_internal(
  340 + query=query,
  341 + top_k=max_results
  342 + )
260 343
261 -def print_response_summary(response: BochaResponse): 344 + def search_last_24_hours(self, query: str, max_results: int = 10) -> AnspireResponse:
  345 + """
  346 + 【工具】搜索24小时内信息: 获取关于某个主题的最新动态。
  347 + 此工具专门查找过去24小时内发布的内容。适用于追踪突发事件或最新进展。
  348 + """
  349 + logger.info(f"--- TOOL: 搜索24小时内信息 (query: {query}) ---")
  350 + to_time = datetime.datetime.now()
  351 + from_time = to_time - datetime.timedelta(days=1)
  352 + return self._search_internal(query=query,
  353 + top_k=max_results,
  354 + FromTime=from_time.strftime("%Y-%m-%d %H:%M:%S"),
  355 + ToTime=to_time.strftime("%Y-%m-%d %H:%M:%S"))
  356 +
  357 + def search_last_week(self, query: str, max_results: int = 10) -> AnspireResponse:
  358 + """
  359 + 【工具】搜索本周信息: 获取关于某个主题过去一周内的主要报道。
  360 + 适用于进行周度舆情总结或回顾。
  361 + """
  362 + logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---")
  363 + to_time = datetime.datetime.now()
  364 + from_time = to_time - datetime.timedelta(weeks=1)
  365 + return self._search_internal(query=query,
  366 + top_k=max_results,
  367 + FromTime=from_time.strftime("%Y-%m-%d %H:%M:%S"),
  368 + ToTime=to_time.strftime("%Y-%m-%d %H:%M:%S"))
  369 +
  370 +
  371 +# --- 3. 测试与使用示例 ---
  372 +def load_agent_from_config():
  373 + """根据配置文件选择并加载搜索Agent"""
  374 + if settings.BOCHA_WEB_SEARCH_API_KEY:
  375 + logger.info("加载 BochaMultimodalSearch Agent")
  376 + return BochaMultimodalSearch()
  377 + elif settings.ANSPIRE_API_KEY:
  378 + logger.info("加载 AnspireAISearch Agent")
  379 + return AnspireAISearch()
  380 + else:
  381 + raise ValueError("未配置有效的搜索Agent")
  382 +
  383 +def print_response_summary(response):
262 """简化的打印函数,用于展示测试结果""" 384 """简化的打印函数,用于展示测试结果"""
263 if not response or not response.query: 385 if not response or not response.query:
264 logger.error("未能获取有效响应。") 386 logger.error("未能获取有效响应。")
265 return 387 return
266 388
267 logger.info(f"\n查询: '{response.query}' | 会话ID: {response.conversation_id}") 389 logger.info(f"\n查询: '{response.query}' | 会话ID: {response.conversation_id}")
268 - if response.answer: 390 + if hasattr(response, 'answer') and response.answer:
269 logger.info(f"AI摘要: {response.answer[:150]}...") 391 logger.info(f"AI摘要: {response.answer[:150]}...")
270 392
271 - logger.info(f"找到 {len(response.webpages)} 个网页, {len(response.images)} 张图片, {len(response.modal_cards)} 个模态卡。") 393 + logger.info(f"找到 {len(response.webpages)} 个网页")
  394 + if hasattr(response, 'images'):
  395 + logger.info(f"找到 {len(response.images)} 张图片")
  396 + if hasattr(response, 'modal_cards'):
  397 + logger.info(f"找到 {len(response.modal_cards)} 个模态卡")
272 398
273 - if response.modal_cards: 399 + if hasattr(response, 'modal_cards') and response.modal_cards:
274 first_card = response.modal_cards[0] 400 first_card = response.modal_cards[0]
275 logger.info(f"第一个模态卡类型: {first_card.card_type}") 401 logger.info(f"第一个模态卡类型: {first_card.card_type}")
276 402
@@ -278,7 +404,7 @@ def print_response_summary(response: BochaResponse): @@ -278,7 +404,7 @@ def print_response_summary(response: BochaResponse):
278 first_result = response.webpages[0] 404 first_result = response.webpages[0]
279 logger.info(f"第一条网页结果: {first_result.name}") 405 logger.info(f"第一条网页结果: {first_result.name}")
280 406
281 - if response.follow_ups: 407 + if hasattr(response, 'follow_ups') and response.follow_ups:
282 logger.info(f"建议追问: {response.follow_ups}") 408 logger.info(f"建议追问: {response.follow_ups}")
283 409
284 logger.info("-" * 60) 410 logger.info("-" * 60)
@@ -289,31 +415,34 @@ if __name__ == "__main__": @@ -289,31 +415,34 @@ if __name__ == "__main__":
289 415
290 try: 416 try:
291 # 初始化多模态搜索客户端,它内部包含了所有工具 417 # 初始化多模态搜索客户端,它内部包含了所有工具
292 - search_client = BochaMultimodalSearch() 418 + search_client = load_agent_from_config()
293 419
294 # 场景1: Agent进行一次常规的、需要AI总结的综合搜索 420 # 场景1: Agent进行一次常规的、需要AI总结的综合搜索
295 response1 = search_client.comprehensive_search(query="人工智能对未来教育的影响") 421 response1 = search_client.comprehensive_search(query="人工智能对未来教育的影响")
296 print_response_summary(response1) 422 print_response_summary(response1)
297 423
298 # 场景2: Agent需要查询特定结构化信息 - 天气 424 # 场景2: Agent需要查询特定结构化信息 - 天气
299 - response2 = search_client.search_for_structured_data(query="上海明天天气怎么样")  
300 - print_response_summary(response2)  
301 - # 深度解析第一个模态卡  
302 - if response2.modal_cards and response2.modal_cards[0].card_type == 'weather_china':  
303 - logger.info("天气模态卡详情:", json.dumps(response2.modal_cards[0].content, indent=2, ensure_ascii=False)) 425 + if isinstance(search_client, BochaMultimodalSearch):
  426 + response2 = search_client.search_for_structured_data(query="上海明天天气怎么样")
  427 + print_response_summary(response2)
  428 + # 深度解析第一个模态卡
  429 + if response2.modal_cards and response2.modal_cards[0].card_type == 'weather_china':
  430 + logger.info("天气模态卡详情:", json.dumps(response2.modal_cards[0].content, indent=2, ensure_ascii=False))
304 431
305 432
306 # 场景3: Agent需要查询特定结构化信息 - 股票 433 # 场景3: Agent需要查询特定结构化信息 - 股票
307 - response3 = search_client.search_for_structured_data(query="东方财富股票")  
308 - print_response_summary(response3) 434 + if isinstance(search_client, BochaMultimodalSearch):
  435 + response3 = search_client.search_for_structured_data(query="东方财富股票")
  436 + print_response_summary(response3)
309 437
310 # 场景4: Agent需要追踪某个事件的最新进展 438 # 场景4: Agent需要追踪某个事件的最新进展
311 response4 = search_client.search_last_24_hours(query="C929大飞机最新消息") 439 response4 = search_client.search_last_24_hours(query="C929大飞机最新消息")
312 print_response_summary(response4) 440 print_response_summary(response4)
313 441
314 # 场景5: Agent只需要快速获取网页信息,不需要AI总结 442 # 场景5: Agent只需要快速获取网页信息,不需要AI总结
315 - response5 = search_client.web_search_only(query="Python dataclasses用法")  
316 - print_response_summary(response5) 443 + if isinstance(search_client, BochaMultimodalSearch):
  444 + response5 = search_client.web_search_only(query="Python dataclasses用法")
  445 + print_response_summary(response5)
317 446
318 # 场景6: Agent需要回顾一周内关于某项技术的新闻 447 # 场景6: Agent需要回顾一周内关于某项技术的新闻
319 response6 = search_client.search_last_week(query="量子计算商业化") 448 response6 = search_client.search_last_week(query="量子计算商业化")
@@ -5,7 +5,7 @@ Configuration management module for the Media Engine (pydantic_settings style). @@ -5,7 +5,7 @@ Configuration management module for the Media Engine (pydantic_settings style).
5 from pathlib import Path 5 from pathlib import Path
6 from pydantic_settings import BaseSettings 6 from pydantic_settings import BaseSettings
7 from pydantic import Field 7 from pydantic import Field
8 -from typing import Optional 8 +from typing import Optional, Literal
9 9
10 10
11 # 计算 .env 优先级:优先当前工作目录,其次项目根目录 11 # 计算 .env 优先级:优先当前工作目录,其次项目根目录
@@ -70,8 +70,13 @@ class Settings(BaseSettings): @@ -70,8 +70,13 @@ class Settings(BaseSettings):
70 70
71 # ================== 网络工具配置 ==================== 71 # ================== 网络工具配置 ====================
72 TAVILY_API_KEY: str = Field(None, description="Tavily API(申请地址:https://www.tavily.com/)API密钥,用于Tavily网络搜索") 72 TAVILY_API_KEY: str = Field(None, description="Tavily API(申请地址:https://www.tavily.com/)API密钥,用于Tavily网络搜索")
  73 +
  74 + SEARCH_TOOL_TYPE: Literal["AnspireAPI", "BochaAPI"] = Field("AnspireAPI", description="网络搜索工具类型,支持BochaAPI或AnspireAPI两种,默认为AnspireAPI")
73 BOCHA_BASE_URL: Optional[str] = Field("https://api.bochaai.com/v1/ai-search", description="Bocha AI 搜索BaseUrl或博查网页搜索BaseUrl") 75 BOCHA_BASE_URL: Optional[str] = Field("https://api.bochaai.com/v1/ai-search", description="Bocha AI 搜索BaseUrl或博查网页搜索BaseUrl")
74 - BOCHA_WEB_SEARCH_API_KEY: str = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索") 76 + BOCHA_WEB_SEARCH_API_KEY: Optional[str] = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索")
  77 + # Anspire AI Search API(申请地址:https://open.anspire.cn/)
  78 + ANSPIRE_BASE_URL: Optional[str] = Field("https://plugin.anspire.cn/api/ntsearch/search", description="Anspire AI 搜索BaseUrl")
  79 + ANSPIRE_API_KEY: Optional[str] = Field(None, description="Anspire AI Search API(申请地址:https://open.anspire.cn/)API密钥,用于Anspire搜索")
75 80
76 class Config: 81 class Config:
77 env_file = ENV_FILE 82 env_file = ENV_FILE
@@ -27,7 +27,7 @@ except locale.Error: @@ -27,7 +27,7 @@ except locale.Error:
27 # 添加src目录到Python路径 27 # 添加src目录到Python路径
28 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 28 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
29 29
30 -from MediaEngine import DeepSearchAgent, Settings 30 +from MediaEngine import DeepSearchAgent, AnspireSearchAgent, Settings
31 from config import settings 31 from config import settings
32 from utils.github_issues import error_with_issue_link 32 from utils.github_issues import error_with_issue_link
33 33
@@ -101,25 +101,39 @@ def main(): @@ -101,25 +101,39 @@ def main():
101 st.error("请在您的环境变量中设置MEDIA_ENGINE_API_KEY") 101 st.error("请在您的环境变量中设置MEDIA_ENGINE_API_KEY")
102 logger.error("请在您的环境变量中设置MEDIA_ENGINE_API_KEY") 102 logger.error("请在您的环境变量中设置MEDIA_ENGINE_API_KEY")
103 return 103 return
104 - if not settings.BOCHA_WEB_SEARCH_API_KEY:  
105 - st.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY")  
106 - logger.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY") 104 + if (not settings.BOCHA_WEB_SEARCH_API_KEY) and (not settings.ANSPIRE_API_KEY):
  105 + st.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY或ANSPIRE_API_KEY")
  106 + logger.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY或ANSPIRE_API_KEY")
107 return 107 return
108 108
109 # 自动使用配置文件中的API密钥 109 # 自动使用配置文件中的API密钥
110 engine_key = settings.MEDIA_ENGINE_API_KEY 110 engine_key = settings.MEDIA_ENGINE_API_KEY
111 bocha_key = settings.BOCHA_WEB_SEARCH_API_KEY 111 bocha_key = settings.BOCHA_WEB_SEARCH_API_KEY
  112 + ansire_key = settings.ANSPIRE_API_KEY
112 113
113 # 构建 Settings(pydantic_settings风格,优先大写环境变量) 114 # 构建 Settings(pydantic_settings风格,优先大写环境变量)
114 - config = Settings(  
115 - MEDIA_ENGINE_API_KEY=engine_key,  
116 - MEDIA_ENGINE_BASE_URL=settings.MEDIA_ENGINE_BASE_URL,  
117 - MEDIA_ENGINE_MODEL_NAME=model_name,  
118 - BOCHA_WEB_SEARCH_API_KEY=bocha_key,  
119 - MAX_REFLECTIONS=max_reflections,  
120 - SEARCH_CONTENT_MAX_LENGTH=max_content_length,  
121 - OUTPUT_DIR="media_engine_streamlit_reports",  
122 - ) 115 + if bocha_key:
  116 + logger.info("使用Bocha搜索API密钥")
  117 + config = Settings(
  118 + MEDIA_ENGINE_API_KEY=engine_key,
  119 + MEDIA_ENGINE_BASE_URL=settings.MEDIA_ENGINE_BASE_URL,
  120 + MEDIA_ENGINE_MODEL_NAME=model_name,
  121 + BOCHA_WEB_SEARCH_API_KEY=bocha_key,
  122 + MAX_REFLECTIONS=max_reflections,
  123 + SEARCH_CONTENT_MAX_LENGTH=max_content_length,
  124 + OUTPUT_DIR="media_engine_streamlit_reports",
  125 + )
  126 + elif ansire_key:
  127 + logger.info("使用Anspire搜索API密钥")
  128 + config = Settings(
  129 + MEDIA_ENGINE_API_KEY=engine_key,
  130 + MEDIA_ENGINE_BASE_URL=settings.MEDIA_ENGINE_BASE_URL,
  131 + MEDIA_ENGINE_MODEL_NAME=model_name,
  132 + ANSPIRE_API_KEY=ansire_key,
  133 + MAX_REFLECTIONS=max_reflections,
  134 + SEARCH_CONTENT_MAX_LENGTH=max_content_length,
  135 + OUTPUT_DIR="media_engine_streamlit_reports",
  136 + )
123 137
124 # 执行研究 138 # 执行研究
125 execute_research(query, config) 139 execute_research(query, config)
@@ -134,7 +148,10 @@ def execute_research(query: str, config: Settings): @@ -134,7 +148,10 @@ def execute_research(query: str, config: Settings):
134 148
135 # 初始化Agent 149 # 初始化Agent
136 status_text.text("正在初始化Agent...") 150 status_text.text("正在初始化Agent...")
137 - agent = DeepSearchAgent(config) 151 + if config.SEARCH_TOOL_TYPE == "BochaAPI":
  152 + agent = DeepSearchAgent(config)
  153 + else:
  154 + agent = AnspireSearchAgent(config)
138 st.session_state.agent = agent 155 st.session_state.agent = agent
139 156
140 progress_bar.progress(10) 157 progress_bar.progress(10)
@@ -111,7 +111,9 @@ CONFIG_KEYS = [ @@ -111,7 +111,9 @@ CONFIG_KEYS = [
111 'KEYWORD_OPTIMIZER_BASE_URL', 111 'KEYWORD_OPTIMIZER_BASE_URL',
112 'KEYWORD_OPTIMIZER_MODEL_NAME', 112 'KEYWORD_OPTIMIZER_MODEL_NAME',
113 'TAVILY_API_KEY', 113 'TAVILY_API_KEY',
114 - 'BOCHA_WEB_SEARCH_API_KEY' 114 + 'SEARCH_TOOL_TYPE',
  115 + 'BOCHA_WEB_SEARCH_API_KEY',
  116 + 'ANSPIRE_API_KEY'
115 ] 117 ]
116 118
117 119
@@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
10 from pathlib import Path 10 from pathlib import Path
11 from pydantic_settings import BaseSettings 11 from pydantic_settings import BaseSettings
12 from pydantic import Field, ConfigDict 12 from pydantic import Field, ConfigDict
13 -from typing import Optional 13 +from typing import Optional, Literal
14 from loguru import logger 14 from loguru import logger
15 15
16 16
@@ -79,10 +79,16 @@ class Settings(BaseSettings): @@ -79,10 +79,16 @@ class Settings(BaseSettings):
79 # ================== 网络工具配置 ==================== 79 # ================== 网络工具配置 ====================
80 # Tavily API(申请地址:https://www.tavily.com/) 80 # Tavily API(申请地址:https://www.tavily.com/)
81 TAVILY_API_KEY: Optional[str] = Field(None, description="Tavily API(申请地址:https://www.tavily.com/)API密钥,用于Tavily网络搜索") 81 TAVILY_API_KEY: Optional[str] = Field(None, description="Tavily API(申请地址:https://www.tavily.com/)API密钥,用于Tavily网络搜索")
82 - 82 +
  83 + SEARCH_TOOL_TYPE: Literal["AnspireAPI", "BochaAPI"] = Field("AnspireAPI", description="网络搜索工具类型,支持BochaAPI或AnspireAPI两种,默认为AnspireAPI")
83 # Bocha API(申请地址:https://open.bochaai.com/) 84 # Bocha API(申请地址:https://open.bochaai.com/)
84 BOCHA_BASE_URL: Optional[str] = Field("https://api.bocha.cn/v1/ai-search", description="Bocha AI 搜索BaseUrl或博查网页搜索BaseUrl") 85 BOCHA_BASE_URL: Optional[str] = Field("https://api.bocha.cn/v1/ai-search", description="Bocha AI 搜索BaseUrl或博查网页搜索BaseUrl")
85 BOCHA_WEB_SEARCH_API_KEY: Optional[str] = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索") 86 BOCHA_WEB_SEARCH_API_KEY: Optional[str] = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索")
  87 +
  88 + # Anspire AI Search API(申请地址:https://open.anspire.cn/)
  89 + ANSPIRE_BASE_URL: Optional[str] = Field("https://plugin.anspire.cn/api/ntsearch/search", description="Anspire AI 搜索BaseUrl")
  90 + ANSPIRE_API_KEY: Optional[str] = Field(None, description="Anspire AI Search API(申请地址:https://open.anspire.cn/)API密钥,用于Anspire搜索")
  91 +
86 92
87 # ================== Insight Engine 搜索配置 ==================== 93 # ================== Insight Engine 搜索配置 ====================
88 DEFAULT_SEARCH_HOT_CONTENT_LIMIT: int = Field(100, description="热榜内容默认最大数") 94 DEFAULT_SEARCH_HOT_CONTENT_LIMIT: int = Field(100, description="热榜内容默认最大数")
@@ -605,6 +605,15 @@ @@ -605,6 +605,15 @@
605 display: flex; 605 display: flex;
606 flex-direction: column; 606 flex-direction: column;
607 margin-bottom: 12px; 607 margin-bottom: 12px;
  608 + transition: all 0.3s ease;
  609 + }
  610 +
  611 + .config-field.hidden {
  612 + display: none;
  613 + opacity: 0;
  614 + height: 0;
  615 + margin-bottom: 0;
  616 + overflow: hidden;
608 } 617 }
609 618
610 .config-field-label { 619 .config-field-label {
@@ -1821,8 +1830,15 @@ @@ -1821,8 +1830,15 @@
1821 title: '外部检索工具', 1830 title: '外部检索工具',
1822 subtitle: '联动搜索引擎、网站抓取等在线服务,两个都需配置', 1831 subtitle: '联动搜索引擎、网站抓取等在线服务,两个都需配置',
1823 fields: [ 1832 fields: [
  1833 + {
  1834 + key: 'SEARCH_TOOL_TYPE',
  1835 + label: '选择检索工具',
  1836 + type: 'select',
  1837 + options: ['BochaAPI', 'AnspireAPI']
  1838 + },
1824 { key: 'TAVILY_API_KEY', label: 'Tavily API Key', type: 'password' }, 1839 { key: 'TAVILY_API_KEY', label: 'Tavily API Key', type: 'password' },
1825 - { key: 'BOCHA_WEB_SEARCH_API_KEY', label: 'Bocha API Key', type: 'password' } 1840 + { key: 'BOCHA_WEB_SEARCH_API_KEY', label: 'Bocha API Key', type: 'password', condition: { key: 'SEARCH_TOOL_TYPE', value: 'BochaAPI' } },
  1841 + { key: 'ANSPIRE_API_KEY', label: 'Anspire API Key', type: 'password', condition: { key: 'SEARCH_TOOL_TYPE', value: 'AnspireAPI' } }
1826 ] 1842 ]
1827 } 1843 }
1828 ]; 1844 ];
@@ -2117,6 +2133,17 @@ @@ -2117,6 +2133,17 @@
2117 const value = values[field.key] !== undefined ? values[field.key] : ''; 2133 const value = values[field.key] !== undefined ? values[field.key] : '';
2118 const safeValue = escapeHtml(String(value || '')); 2134 const safeValue = escapeHtml(String(value || ''));
2119 2135
  2136 + // 检查条件是否满足
  2137 + let isVisible = true;
  2138 + let hiddenClass = '';
  2139 + if (field.condition) {
  2140 + const conditionKey = field.condition.key;
  2141 + const conditionValue = field.condition.value;
  2142 + const currentValue = values[conditionKey];
  2143 + isVisible = currentValue === conditionValue;
  2144 + hiddenClass = isVisible ? '' : 'hidden';
  2145 + }
  2146 +
2120 let control; 2147 let control;
2121 2148
2122 if (field.type === 'select' && field.options) { 2149 if (field.type === 'select' && field.options) {
@@ -2180,7 +2207,7 @@ @@ -2180,7 +2207,7 @@
2180 } 2207 }
2181 2208
2182 return ` 2209 return `
2183 - <label class="config-field"> 2210 + <label class="config-field ${hiddenClass}" data-condition-key="${field.condition ? field.condition.key : ''}" data-condition-value="${field.condition ? field.condition.value : ''}">
2184 <span class="config-field-label">${field.label}</span> 2211 <span class="config-field-label">${field.label}</span>
2185 ${control} 2212 ${control}
2186 </label> 2213 </label>
@@ -2201,6 +2228,9 @@ @@ -2201,6 +2228,9 @@
2201 container.innerHTML = sections; 2228 container.innerHTML = sections;
2202 // 不再需要每次调用 attachConfigPasswordToggles 2229 // 不再需要每次调用 attachConfigPasswordToggles
2203 // 事件委托已在页面初始化时设置 2230 // 事件委托已在页面初始化时设置
  2231 +
  2232 + // 为所有 select 下拉框绑定事件,监听值变化并动态显示/隐藏条件字段
  2233 + attachConfigConditionalLogic();
2204 } 2234 }
2205 2235
2206 function attachConfigPasswordToggles() { 2236 function attachConfigPasswordToggles() {
@@ -2252,6 +2282,51 @@ @@ -2252,6 +2282,51 @@
2252 container.dataset.passwordToggleAttached = 'true'; 2282 container.dataset.passwordToggleAttached = 'true';
2253 } 2283 }
2254 2284
  2285 + // 【新增】条件字段动态显示逻辑
  2286 + function attachConfigConditionalLogic() {
  2287 + const container = document.getElementById('configFormContainer');
  2288 + if (!container) {
  2289 + return;
  2290 + }
  2291 +
  2292 + // 防止重复绑定
  2293 + if (container.dataset.conditionalLogicAttached === 'true') {
  2294 + return;
  2295 + }
  2296 +
  2297 + // 监听所有 select 下拉框的变化
  2298 + container.addEventListener('change', (event) => {
  2299 + const select = event.target.closest('select.config-field-input');
  2300 + if (!select) {
  2301 + return;
  2302 + }
  2303 +
  2304 + const triggerKey = select.dataset.configKey;
  2305 + const triggerValue = select.value;
  2306 +
  2307 + // 更新所有依赖于这个字段的条件字段的显示状态
  2308 + const conditionalFields = container.querySelectorAll('.config-field[data-condition-key]');
  2309 + conditionalFields.forEach(field => {
  2310 + const conditionKey = field.dataset.conditionKey;
  2311 + const conditionValue = field.dataset.conditionValue;
  2312 +
  2313 + // 检查这个条件字段是否依赖于当前改变的字段
  2314 + if (conditionKey === triggerKey) {
  2315 + if (triggerValue === conditionValue) {
  2316 + // 显示字段
  2317 + field.classList.remove('hidden');
  2318 + } else {
  2319 + // 隐藏字段
  2320 + field.classList.add('hidden');
  2321 + }
  2322 + }
  2323 + });
  2324 + });
  2325 +
  2326 + // 标记已绑定,防止重复
  2327 + container.dataset.conditionalLogicAttached = 'true';
  2328 + }
  2329 +
2255 function collectConfigUpdates() { 2330 function collectConfigUpdates() {
2256 const inputs = document.querySelectorAll('#configFormContainer [data-config-key]'); 2331 const inputs = document.querySelectorAll('#configFormContainer [data-config-key]');
2257 const updates = {}; 2332 const updates = {};