agent_system.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. """
  2. 智能股票分析助手 — 智能体系统(统一Agent管理与流式调度)
  3. 基于 HelloAgents Optimized 框架,管理所有专业Agent的生命周期,
  4. 提供统一的流式分析接口供后端API调用。
  5. """
  6. import sys
  7. import os
  8. import threading
  9. from pathlib import Path
  10. from typing import AsyncIterator, Optional
  11. def _coord_answer_cap_hint(max_chars: int) -> str:
  12. return (
  13. f"\n\n【硬性要求】最终答案全文不得超过约 {max_chars} 个汉字(含标点),"
  14. "分条简练,禁止复述工具返回的全文或大段粘贴。"
  15. )
  16. def _apply_answer_cap(text: str, max_chars: Optional[int]) -> str:
  17. if max_chars is None or max_chars <= 0:
  18. return text or ""
  19. from .text_truncation import truncate_at_natural_boundary
  20. t = (text or "").strip()
  21. if len(t) <= max_chars:
  22. return t
  23. return truncate_at_natural_boundary(t, max_chars, "\n\n…(已达字数上限)")
  24. _HELLO_AGENTS_PATH = Path(__file__).parent.parent / "HelloAgents Optimized"
  25. if str(_HELLO_AGENTS_PATH) not in sys.path:
  26. sys.path.insert(0, str(_HELLO_AGENTS_PATH))
  27. _SKILLS_PATH = Path(__file__).parent.parent / "skills"
  28. if str(_SKILLS_PATH) not in sys.path:
  29. sys.path.insert(0, str(_SKILLS_PATH))
  30. _BACKEND_PATH = Path(__file__).parent.parent / "backend"
  31. if str(_BACKEND_PATH) not in sys.path:
  32. sys.path.insert(0, str(_BACKEND_PATH))
  33. from hello_agents.core.llm import HelloAgentsLLM
  34. from hello_agents.core.config import Config
  35. _agent_lock = threading.Lock()
  36. _agent_system_instance: Optional["AgentSystem"] = None
  37. class AgentSystem:
  38. """智能体系统 — 统一管理所有Agent并提供流式分析接口"""
  39. def __init__(self):
  40. self._llm: Optional[HelloAgentsLLM] = None
  41. self._advisor = None # 巴菲特评估 Agent (Reflection)
  42. self._sentiment = None # 舆情分析 Agent (ReAct)
  43. self._data_analysis = None # 数据分析 Agent (ReAct)
  44. self._general_advisor = None # 普通投资顾问 Agent
  45. self._initialized = False
  46. def _ensure_llm(self) -> HelloAgentsLLM:
  47. if self._llm is None:
  48. self._llm = _create_default_llm()
  49. return self._llm
  50. def _get_api_key(self) -> Optional[str]:
  51. key = os.getenv("MX_APIKEY", "").strip()
  52. if key and key != "your-mx-apikey-here":
  53. return key
  54. try:
  55. from app.config import settings
  56. return settings.MX_APIKEY or None
  57. except Exception:
  58. return None
  59. # ---- 巴菲特评估 Agent ----
  60. def get_advisor_agent(self):
  61. """获取巴菲特评估 Agent(仅限巴菲特评估界面调用)"""
  62. if self._advisor is None:
  63. from agents.advisor_agent import create_advisor_agent
  64. self._advisor = create_advisor_agent(llm=self._ensure_llm())
  65. return self._advisor
  66. def evaluate_buffett_stream(self, stock_code: str, stock_name: str = ""):
  67. """流式巴菲特评估 - 通过 advisor_agent 生成评估报告"""
  68. from agents.advisor_agent import evaluate_buffett_stream
  69. yield from evaluate_buffett_stream(
  70. llm=self._ensure_llm(),
  71. stock_code=stock_code,
  72. stock_name=stock_name,
  73. )
  74. # ---- 舆情分析 Agent ----
  75. def get_sentiment_agent(self):
  76. """获取舆情分析 Agent"""
  77. if self._sentiment is None:
  78. from agents.sentiment_agent import create_sentiment_agent
  79. self._sentiment = create_sentiment_agent(
  80. api_key=self._get_api_key(),
  81. llm=self._ensure_llm(),
  82. )
  83. return self._sentiment
  84. def run_sentiment(
  85. self,
  86. stock_code: str,
  87. stock_name: str = "",
  88. *,
  89. max_answer_chars: Optional[int] = None,
  90. ) -> str:
  91. """非流式舆情分析 — 返回完整文本,供协调者Agent内部调用"""
  92. agent = self.get_sentiment_agent()
  93. stock_label = f"{stock_name}({stock_code})" if stock_name else stock_code
  94. task = f"请搜索并分析股票 {stock_label} 的最新金融资讯、研究报告和公告,判断市场舆情趋势。"
  95. if max_answer_chars:
  96. task += _coord_answer_cap_hint(max_answer_chars)
  97. try:
  98. out = (agent.run(task) or "").strip()
  99. if not out:
  100. return (
  101. "[舆情分析未生成有效正文:可能因网络/超时或模型提前结束。"
  102. "建议在个股页使用「AI舆情分析」流式重试,或在 .env 将 LLM_TIMEOUT 调至 300 后重启后端。]"
  103. )
  104. return _apply_answer_cap(out, max_answer_chars)
  105. except Exception as e:
  106. return f"[舆情分析失败: {e}]"
  107. def analyze_sentiment_stream(self, stock_code: str, stock_name: str = ""):
  108. """流式舆情分析"""
  109. from agents.sentiment_agent import analyze_sentiment_stream
  110. yield from analyze_sentiment_stream(
  111. agent=self.get_sentiment_agent(),
  112. stock_code=stock_code,
  113. stock_name=stock_name,
  114. )
  115. # ---- 数据分析 Agent ----
  116. def get_data_analysis_agent(self):
  117. """获取数据分析 Agent"""
  118. if self._data_analysis is None:
  119. from agents.data_analysis_agent import create_data_analysis_agent
  120. self._data_analysis = create_data_analysis_agent(
  121. api_key=self._get_api_key(),
  122. llm=self._ensure_llm(),
  123. )
  124. return self._data_analysis
  125. def run_data_analysis(
  126. self,
  127. stock_code: str,
  128. stock_name: str = "",
  129. *,
  130. max_answer_chars: Optional[int] = None,
  131. ) -> str:
  132. """非流式数据分析 — 返回完整文本,供协调者Agent内部调用"""
  133. agent = self.get_data_analysis_agent()
  134. stock_label = f"{stock_name}({stock_code})" if stock_name else stock_code
  135. task = f"""请查询股票 {stock_label} 的以下数据并进行综合分析:
  136. 1. 实时行情(价格、涨跌幅、成交量、换手率等)
  137. 2. 核心财务指标(ROE、净利润、营收增长率、毛利率等)
  138. 3. 估值水平(市盈率、市净率、股息率等)
  139. 4. 公司基本概况
  140. 请给出专业的数据分析报告。"""
  141. if max_answer_chars:
  142. task += _coord_answer_cap_hint(max_answer_chars)
  143. try:
  144. out = (agent.run(task) or "").strip()
  145. if not out:
  146. return (
  147. "[数据分析未生成有效正文:可能因网络/超时或模型提前结束。"
  148. "建议使用个股页「AI数据分析」流式重试,或在 .env 将 LLM_TIMEOUT 调至 300 后重启后端。]"
  149. )
  150. return _apply_answer_cap(out, max_answer_chars)
  151. except Exception as e:
  152. return f"[数据分析失败: {e}]"
  153. def analyze_data_stream(self, stock_code: str, stock_name: str = ""):
  154. """流式数据分析"""
  155. from agents.data_analysis_agent import analyze_data_stream
  156. yield from analyze_data_stream(
  157. agent=self.get_data_analysis_agent(),
  158. stock_code=stock_code,
  159. stock_name=stock_name,
  160. )
  161. # ---- 普通投资顾问 Agent ----
  162. def get_general_advisor_agent(self):
  163. """获取普通投资顾问 Agent"""
  164. if self._general_advisor is None:
  165. from agents.general_advisor_agent import create_general_advisor_agent
  166. self._general_advisor = create_general_advisor_agent(
  167. llm=self._ensure_llm(),
  168. )
  169. return self._general_advisor
  170. def run_advisor(
  171. self,
  172. task: str,
  173. *,
  174. max_answer_chars: Optional[int] = None,
  175. ) -> str:
  176. """非流式投资建议 — 返回完整文本,供协调者Agent内部调用"""
  177. agent = self.get_general_advisor_agent()
  178. if max_answer_chars:
  179. task = task + _coord_answer_cap_hint(max_answer_chars)
  180. try:
  181. out = (agent.run(task) or "").strip()
  182. return _apply_answer_cap(out, max_answer_chars)
  183. except Exception as e:
  184. return f"[投资分析失败: {e}]"
  185. # ---- AI 对话助手(协调者)----
  186. def chat_stream(self, user_message: str, history: list = None):
  187. """AI对话助手流式接口 - 协调者Agent解析用户需求并调度子Agent"""
  188. from agents.coordinator_agent import coordinator_chat_stream
  189. yield from coordinator_chat_stream(
  190. llm=self._ensure_llm(),
  191. user_message=user_message,
  192. history=history or [],
  193. agent_system=self,
  194. )
  195. # ---- 健康检查 ----
  196. def is_ready(self) -> bool:
  197. try:
  198. self._ensure_llm()
  199. return True
  200. except Exception:
  201. return False
  202. def _create_default_llm() -> HelloAgentsLLM:
  203. model = os.getenv("LLM_MODEL_ID")
  204. api_key = os.getenv("LLM_API_KEY")
  205. base_url = os.getenv("LLM_BASE_URL")
  206. provider = os.getenv("LLM_PROVIDER", "auto")
  207. if not api_key:
  208. raise RuntimeError("LLM_API_KEY 环境变量未设置")
  209. try:
  210. from app.config import settings
  211. raw_timeout = int(settings.LLM_TIMEOUT)
  212. except Exception:
  213. raw_timeout = int(os.getenv("LLM_TIMEOUT", "60"))
  214. # ReAct 多轮 + 工具调用 + 协调者多 Agent 串联,默认 60s 极易中途超时
  215. timeout = max(raw_timeout, 180)
  216. return HelloAgentsLLM(
  217. model=model,
  218. api_key=api_key,
  219. base_url=base_url,
  220. provider=provider,
  221. temperature=0.3,
  222. max_tokens=8192,
  223. timeout=timeout,
  224. )
  225. def get_agent_system() -> AgentSystem:
  226. """获取 AgentSystem 全局单例"""
  227. global _agent_system_instance
  228. if _agent_system_instance is None:
  229. with _agent_lock:
  230. if _agent_system_instance is None:
  231. _agent_system_instance = AgentSystem()
  232. return _agent_system_instance