mx_search_tool.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. """
  2. 智能股票分析助手 — HelloAgents 资讯搜索工具封装
  3. 将东方财富 mx-search Skill 封装为符合 HelloAgents 标准 Tool 接口的工具类。
  4. Agent可通过此工具调用自然语言搜索金融资讯(新闻、研报、公告)。
  5. """
  6. import sys
  7. from pathlib import Path
  8. # 将HelloAgents框架和skills路径加入sys.path
  9. _PROJECT_ROOT = Path(__file__).parent.parent.parent
  10. _HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
  11. _SKILLS_PATH = _PROJECT_ROOT / "skills" / "资讯搜索" / "mx-search"
  12. for p in [_PROJECT_ROOT, _HELLO_PATH, _SKILLS_PATH]:
  13. if str(p) not in sys.path:
  14. sys.path.insert(0, str(p))
  15. from hello_agents.tools import Tool, ToolParameter
  16. from ..text_truncation import truncate_at_natural_boundary
  17. # 单条资讯正文展示上限(字符);放宽可减少摘要中途截断
  18. MX_SEARCH_CONTENT_MAX_CHARS = 2500
  19. class MXSearchTool(Tool):
  20. """金融资讯搜索工具 — 封装东方财富妙想mx-search Skill
  21. 支持通过自然语言搜索金融资讯,包括:
  22. - 个股相关:研报、公告、机构观点
  23. - 行业/板块:产业新闻、政策解读
  24. - 宏观/市场:经济分析、资金流向
  25. - 事件/规则:分红公告、交易规则等
  26. 使用示例:
  27. tool = MXSearchTool(api_key="your_mx_apikey")
  28. result = tool.run({"query": "贵州茅台最新研报"})
  29. """
  30. def __init__(self, api_key: str = None):
  31. super().__init__(
  32. name="mx_search",
  33. description=(
  34. "东方财富金融资讯搜索工具。支持搜索A股相关的新闻、研报、公告、"
  35. "政策解读、行业分析等金融资讯。适用于获取时效性信息和特定事件信息。"
  36. "支持自然语言查询,如'贵州茅台最新研报'、'人工智能板块近期新闻'、"
  37. "'美联储加息对A股影响分析'、'新能源汽车产业政策最新解读'。"
  38. ),
  39. )
  40. # 获取API密钥:优先参数 > 环境变量
  41. import os
  42. self.api_key = api_key or os.getenv("MX_APIKEY", "")
  43. # 延迟导入mx_search模块
  44. self._mx_module = None
  45. def _get_mx_module(self):
  46. """延迟导入mx_search模块(避免初始化时的导入错误)"""
  47. if self._mx_module is None:
  48. import mx_search as _mx_search
  49. self._mx_module = _mx_search
  50. return self._mx_module
  51. def get_parameters(self) -> list:
  52. return [
  53. ToolParameter(
  54. name="query",
  55. type="string",
  56. description=(
  57. "自然语言查询语句。支持中文查询,例如:\n"
  58. "- 个股资讯: '贵州茅台最新研报', '比亚迪机构观点汇总'\n"
  59. "- 行业新闻: '人工智能板块近期新闻', '新能源汽车产业政策'\n"
  60. "- 宏观分析: '美联储加息对A股影响分析', '北向资金最新流向'\n"
  61. "- 事件公告: '贵州茅台分红派息实施公告', '宁德时代定增预案'\n"
  62. "- 交易规则: '科创板涨跌幅限制', '新股申购规则'"
  63. ),
  64. required=True,
  65. ),
  66. ]
  67. def run(self, parameters: dict) -> str:
  68. """执行金融资讯搜索
  69. Args:
  70. parameters: {"query": "自然语言查询"}
  71. Returns:
  72. 格式化的搜索结果文本
  73. """
  74. query = parameters.get("query", "")
  75. if not query:
  76. return "错误:查询内容不能为空"
  77. if not self.api_key:
  78. return "错误:MX_APIKEY 未配置,无法搜索资讯。请设置环境变量 MX_APIKEY"
  79. try:
  80. mx = self._get_mx_module()
  81. # 创建MXSearch实例并查询
  82. search_client = mx.MXSearch(api_key=self.api_key)
  83. result = search_client.search(query)
  84. # 格式化输出为可读文本
  85. return self._format_result(result, query)
  86. except Exception as e:
  87. return f"资讯搜索异常: {str(e)}"
  88. def _format_result(self, result: dict, query: str) -> str:
  89. """将搜索结果格式化为可读文本"""
  90. lines = []
  91. # 检查API状态
  92. status = result.get("status")
  93. message = result.get("message", "")
  94. if status != 0:
  95. lines.append(f"## 资讯搜索结果")
  96. lines.append(f"查询: {query}")
  97. lines.append(f"错误: 状态码 {status} - {message}")
  98. return "\n".join(lines)
  99. # 解析搜索结果
  100. data = result.get("data", {})
  101. inner_data = data.get("data", {})
  102. search_response = inner_data.get("llmSearchResponse", {})
  103. items = search_response.get("data", [])
  104. if not items:
  105. return f"未找到与'{query}'相关的最新金融资讯"
  106. # 类型映射
  107. type_map = {
  108. "REPORT": "研报",
  109. "NEWS": "新闻",
  110. "ANNOUNCEMENT": "公告"
  111. }
  112. # 限制输出条数
  113. max_items = 15
  114. display_items = items[:max_items]
  115. lines.append(f"## 资讯搜索结果")
  116. lines.append(f"查询: {query}")
  117. lines.append(f"共找到 {len(items)} 条相关资讯\n")
  118. for i, item in enumerate(display_items):
  119. title = item.get("title", "无标题")
  120. content = item.get("content", "")
  121. date = item.get("date", "")
  122. ins_name = item.get("insName", "")
  123. info_type = item.get("informationType", "")
  124. rating = item.get("rating", "")
  125. entity_name = item.get("entityFullName", "")
  126. type_cn = type_map.get(info_type, info_type or "资讯")
  127. lines.append(f"### {i+1}. {title}")
  128. meta_parts = []
  129. if entity_name:
  130. meta_parts.append(f"证券: {entity_name}")
  131. if ins_name:
  132. meta_parts.append(f"机构: {ins_name}")
  133. if date:
  134. meta_parts.append(f"日期: {date.split()[0]}")
  135. lines.append(f"类型: {type_cn} | {' | '.join(meta_parts)}")
  136. if rating:
  137. lines.append(f"评级: {rating}")
  138. if content:
  139. # 截断过长内容(优先段落/句号)
  140. if len(content) > MX_SEARCH_CONTENT_MAX_CHARS:
  141. content = truncate_at_natural_boundary(
  142. content, MX_SEARCH_CONTENT_MAX_CHARS, "..."
  143. )
  144. lines.append("")
  145. lines.append(content)
  146. lines.append("")
  147. if len(items) > max_items:
  148. lines.append(f"*(仅显示前{max_items}条,共{len(items)}条)*")
  149. return "\n".join(lines)