advisor_agent.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. """
  2. 智能股票分析助手 — 巴菲特评估Agent(投资顾问Agent)
  3. 基于 HelloAgents ReflectionAgent(反思范式),结合巴菲特价值投资思维,
  4. 对股票进行深度评估分析。**仅允许巴菲特评估界面调用,不允许协调者Agent调用。**
  5. 支持流式输出评估报告。
  6. """
  7. import sys
  8. import os
  9. from pathlib import Path
  10. from typing import Iterator, Optional
  11. _PROJECT_ROOT = Path(__file__).parent.parent
  12. _HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
  13. _BACKEND_DIR = _PROJECT_ROOT / "backend"
  14. for p in [_HELLO_PATH, _BACKEND_DIR]:
  15. if str(p) not in sys.path:
  16. sys.path.insert(0, str(p))
  17. from hello_agents.agents.reflection_agent import ReflectionAgent
  18. from hello_agents.core.llm import HelloAgentsLLM
  19. from hello_agents.core.config import Config
  20. from hello_agents.core.stream import StreamEvent
  21. from .text_truncation import truncate_at_natural_boundary
  22. BUFFETT_INITIAL_PROMPT = """
  23. 你是一位深谙巴菲特价值投资理念的资深投资顾问。请根据以下数据,对股票进行专业的巴菲特式投资分析。
  24. ## 股票信息
  25. - 股票代码: {stock_code}
  26. - 股票名称: {stock_name}
  27. ## 分析数据:
  28. {data_context}
  29. ## 评估维度(巴菲特价值投资框架):
  30. 1. **能力圈评估**: 该公司的业务你是否能理解?商业模式是否简单清晰?
  31. 2. **护城河分析**: 公司是否有持久的竞争优势?(品牌、技术、规模、网络效应、成本优势等)
  32. 3. **管理层评估**: 管理层是否诚信、有能力?(经营历史、资本配置记录)
  33. 4. **财务健康**: 资产负债表是否稳健?(负债率、现金流、ROE稳定性、毛利率)
  34. 5. **估值分析**: 当前股价是否低于内在价值?安全边际是否充足?
  35. 6. **长期前景**: 公司未来5-10年是否能持续增长?(行业趋势、市场份额)
  36. 请提供一个完整、专业的巴菲特式投资分析报告。
  37. 文末必须标注:⚠️ 以上分析仅供参考,不构成投资建议。投资有风险,入市需谨慎。
  38. """
  39. BUFFETT_REFLECT_PROMPT = """
  40. 请以严格的投资委员会视角,审查以下巴菲特式投资分析报告的准确性和完整性:
  41. # 原始分析数据:
  42. {task}
  43. # 当前分析报告:
  44. {content}
  45. 请检查以下方面并提供改进建议:
  46. 1. 数据引用是否准确?有无断章取义?
  47. 2. 护城河分析是否有充分论据支撑?
  48. 3. 估值逻辑是否自洽?安全边际计算是否合理?
  49. 4. 是否遗漏了重要的风险因素?
  50. 5. 结论是否过于乐观或悲观?
  51. 6. 是否符合巴菲特的价值投资哲学?
  52. 如果你的回答已经全面、客观、准确,请回复"无需改进"。
  53. """
  54. BUFFETT_REFINE_PROMPT = """
  55. 请根据投资委员会的反馈意见,改进你的巴菲特式投资分析报告:
  56. # 原始分析数据:
  57. {task}
  58. # 上一轮分析报告:
  59. {last_attempt}
  60. # 委员会反馈:
  61. {feedback}
  62. 请提供一个改进后的、更加严谨和完整的投资分析报告。
  63. 末尾必须标注:⚠️ 以上分析仅供参考,不构成投资建议。投资有风险,入市需谨慎。
  64. """
  65. def _max_reflections_from_env() -> int:
  66. """环境变量 BUFFETT_MAX_REFLECTIONS,默认 0(初稿后即结束,避免「报告已完却仍在调 LLM」)。"""
  67. raw = os.getenv("BUFFETT_MAX_REFLECTIONS", "0").strip()
  68. try:
  69. return max(0, int(raw))
  70. except ValueError:
  71. return 0
  72. def create_advisor_agent(
  73. llm: HelloAgentsLLM = None,
  74. custom_prompts: dict = None,
  75. max_reflections: Optional[int] = None,
  76. ) -> ReflectionAgent:
  77. """创建巴菲特评估Agent(仅限巴菲特评估界面调用)
  78. Args:
  79. llm: HelloAgentsLLM实例
  80. custom_prompts: 自定义三阶段提示词
  81. max_reflections: 最大反思迭代次数;为 None 时读取环境变量 BUFFETT_MAX_REFLECTIONS(默认 0)
  82. Returns:
  83. 配置好的ReflectionAgent实例
  84. """
  85. if llm is None:
  86. llm = _create_default_llm()
  87. if max_reflections is None:
  88. max_reflections = _max_reflections_from_env()
  89. prompts = custom_prompts or {
  90. "initial": BUFFETT_INITIAL_PROMPT,
  91. "reflect": BUFFETT_REFLECT_PROMPT,
  92. "refine": BUFFETT_REFINE_PROMPT,
  93. }
  94. agent = ReflectionAgent(
  95. name="巴菲特评估Agent",
  96. llm=llm,
  97. system_prompt="你是一位精通巴菲特价值投资理念的资深投资顾问,擅长护城河分析和安全边际评估。",
  98. config=Config(temperature=0.4, max_tokens=4096),
  99. max_iterations=max_reflections,
  100. custom_prompts=prompts,
  101. )
  102. return agent
  103. def evaluate_buffett_stream(
  104. llm: HelloAgentsLLM = None,
  105. stock_code: str = "",
  106. stock_name: str = "",
  107. ) -> Iterator[dict]:
  108. """流式巴菲特评估 - 收集数据并通过ReflectionAgent生成评估报告
  109. Args:
  110. llm: HelloAgentsLLM实例
  111. stock_code: 股票代码
  112. stock_name: 股票名称
  113. Yields:
  114. dict: {"type": "meta"|"status"|"delta"|"done"|"error", "content": str}
  115. """
  116. if llm is None:
  117. llm = _create_default_llm()
  118. yield {"type": "meta", "stock_code": stock_code, "stock_name": stock_name}
  119. # 收集分析所需数据
  120. yield {"type": "status", "content": "正在获取分析数据..."}
  121. try:
  122. data_context = _collect_stock_data(stock_code, stock_name)
  123. except Exception as e:
  124. msg = f"数据获取失败: {e}"
  125. yield {"type": "error", "message": msg, "content": msg}
  126. return
  127. yield {"type": "status", "content": f"数据获取完成,开始巴菲特式评估分析..."}
  128. # 构建评估任务
  129. task = f"""
  130. ## 分析数据:
  131. {data_context}
  132. 请对股票 {stock_name}({stock_code}) 进行巴菲特式价值投资分析。
  133. """
  134. # 创建Agent并使用流式运行
  135. agent = create_advisor_agent(llm=llm)
  136. agent.prompts["initial"] = BUFFETT_INITIAL_PROMPT.replace(
  137. "{stock_code}", stock_code
  138. ).replace("{stock_name}", stock_name).replace("{data_context}", data_context)
  139. try:
  140. for event in agent.stream_run(task, conversation_id=None):
  141. if event.event_type == "status":
  142. yield {"type": "status", "content": event.content}
  143. elif event.event_type == "text":
  144. chunk = event.content or ""
  145. yield {"type": "delta", "text": chunk, "content": chunk}
  146. elif event.event_type == "thought":
  147. yield {"type": "thought", "content": event.content}
  148. elif event.event_type == "done":
  149. yield {"type": "done"}
  150. elif event.event_type == "error":
  151. msg = event.content or ""
  152. yield {"type": "error", "message": msg, "content": msg}
  153. except Exception as e:
  154. msg = f"分析过程出错: {e}"
  155. yield {"type": "error", "message": msg, "content": msg}
  156. def _collect_stock_data(stock_code: str, stock_name: str = "") -> str:
  157. """收集股票分析所需数据"""
  158. parts = []
  159. try:
  160. from app.services import market_service, news_service
  161. # 行情数据
  162. try:
  163. quote = market_service.get_stock_quote(stock_code)
  164. if quote and quote.get("success"):
  165. parts.append(f"## 行情数据\n```json\n{_truncate(str(quote), 3000)}\n```")
  166. except Exception:
  167. parts.append("## 行情数据\n获取失败")
  168. # 财务数据
  169. try:
  170. financial = market_service.get_stock_financial(stock_code)
  171. if financial and financial.get("success"):
  172. parts.append(f"## 财务数据\n```json\n{_truncate(str(financial), 4000)}\n```")
  173. except Exception:
  174. parts.append("## 财务数据\n获取失败")
  175. # 公司概况
  176. try:
  177. profile = market_service.get_stock_profile(stock_code)
  178. if profile and profile.get("success"):
  179. parts.append(f"## 公司概况\n```json\n{_truncate(str(profile), 3000)}\n```")
  180. except Exception:
  181. parts.append("## 公司概况\n获取失败")
  182. # 舆情数据
  183. try:
  184. sentiment = news_service.analyze_sentiment(stock_code)
  185. if sentiment and sentiment.get("success"):
  186. parts.append(f"## 舆情数据\n```json\n{_truncate(str(sentiment), 3000)}\n```")
  187. except Exception:
  188. parts.append("## 舆情数据\n获取失败")
  189. except Exception as e:
  190. parts.append(f"## 数据收集错误\n{str(e)}")
  191. return "\n\n".join(parts) if parts else "暂无可用数据"
  192. def _truncate(text: str, max_len: int) -> str:
  193. return truncate_at_natural_boundary(text or "", max_len, "...[已截断]")
  194. def _create_default_llm() -> HelloAgentsLLM:
  195. model = os.getenv("LLM_MODEL_ID")
  196. api_key = os.getenv("LLM_API_KEY")
  197. base_url = os.getenv("LLM_BASE_URL")
  198. provider = os.getenv("LLM_PROVIDER", "auto")
  199. if not api_key:
  200. raise RuntimeError("LLM_API_KEY 环境变量未设置")
  201. raw_timeout = int(os.getenv("LLM_TIMEOUT", "60"))
  202. buffett_timeout = max(raw_timeout, 180)
  203. return HelloAgentsLLM(
  204. model=model,
  205. api_key=api_key,
  206. base_url=base_url,
  207. provider=provider,
  208. temperature=0.4,
  209. max_tokens=6144,
  210. timeout=buffett_timeout,
  211. )