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>
Showing
10 changed files
with
344 additions
and
43 deletions
| @@ -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 = {}; |
-
Please register or login to post a comment