diff --git a/backend/app/agent/skill_manager.py b/backend/app/agent/skill_manager.py index f277603..4a1174f 100644 --- a/backend/app/agent/skill_manager.py +++ b/backend/app/agent/skill_manager.py @@ -99,12 +99,14 @@ class SkillManager: skill = self.get_skill(skill_name) if not skill: + logger.error(f"❌ 技能不存在: {skill_name}") return { "success": False, "error": f"技能不存在: {skill_name}" } if not skill.enabled: + logger.warning(f"⚠️ 技能已禁用: {skill_name}") return { "success": False, "error": f"技能已禁用: {skill_name}" @@ -113,6 +115,7 @@ class SkillManager: # 验证参数 valid, error = skill.validate_params(**kwargs) if not valid: + logger.error(f"❌ 技能参数验证失败 {skill_name}: {error}") return { "success": False, "error": error @@ -120,13 +123,15 @@ class SkillManager: # 执行技能 try: + logger.info(f"🚀 开始执行技能: {skill_name}, 参数: {kwargs}") result = await skill.execute(**kwargs) + logger.info(f"✅ 技能执行成功: {skill_name}") return { "success": True, "data": result } except Exception as e: - logger.error(f"技能执行失败 {skill_name}: {e}") + logger.error(f"❌ 技能执行失败 {skill_name}: {e}") return { "success": False, "error": str(e) @@ -249,13 +254,14 @@ class SkillManager: # 按优先级顺序执行 for priority in sorted(priority_groups.keys()): skill_group = priority_groups[priority] - logger.info(f"执行优先级 {priority} 的技能: {[s['name'] for s in skill_group]}") + logger.info(f"📋 执行优先级 {priority} 的技能: {[s['name'] for s in skill_group]}") # 同一优先级的技能并行执行 tasks = [] for skill_info in skill_group: params = skill_info['params'].copy() params['stock_code'] = stock_code + logger.info(f" ➡️ 准备执行技能: {skill_info['name']}, 原因: {skill_info.get('reason', '未知')}") task = self.execute_skill(skill_info['name'], **params) tasks.append((skill_info['name'], task)) @@ -265,14 +271,15 @@ class SkillManager: # 处理结果 for (skill_name, _), result in zip(tasks, results): if isinstance(result, Exception): - logger.error(f"技能执行失败: {skill_name}, {result}") + logger.error(f"❌ 技能执行异常: {skill_name}, {result}") all_errors.append(f"{skill_name}: {str(result)}") all_results[skill_name] = {'error': str(result)} elif result.get('success'): + # 不在这里记录成功日志,因为 execute_skill 已经记录了 all_results[skill_name] = result.get('data', {}) else: error_msg = result.get('error', '未知错误') - logger.error(f"技能执行失败: {skill_name}, {error_msg}") + logger.error(f"❌ 技能执行失败: {skill_name}, {error_msg}") all_errors.append(f"{skill_name}: {error_msg}") all_results[skill_name] = {'error': error_msg} diff --git a/backend/app/agent/skill_planner.py b/backend/app/agent/skill_planner.py index cbdd7ee..07342ec 100644 --- a/backend/app/agent/skill_planner.py +++ b/backend/app/agent/skill_planner.py @@ -11,27 +11,31 @@ class SkillPlanner: # 维度到技能的映射 DIMENSION_SKILL_MAP = { 'price_trend': { - 'required': ['market_data'], + 'required': ['market_data', 'brave_search'], # brave_search 必需 'optional': [] }, 'technical': { - 'required': ['market_data', 'technical_analysis'], + 'required': ['market_data', 'technical_analysis', 'brave_search'], # brave_search 必需 'optional': ['visualization'] }, 'fundamental': { - 'required': ['fundamental'], + 'required': ['fundamental', 'brave_search'], # brave_search 必需 'optional': [] }, 'valuation': { - 'required': ['advanced_data'], + 'required': ['advanced_data', 'brave_search'], # brave_search 必需 'optional': [] }, 'money_flow': { - 'required': ['advanced_data'], + 'required': ['advanced_data', 'brave_search'], # brave_search 必需 'optional': [] }, 'risk': { - 'required': ['technical_analysis', 'advanced_data'], + 'required': ['technical_analysis', 'advanced_data', 'brave_search'], # brave_search 必需 + 'optional': [] + }, + 'news': { # 新闻维度 + 'required': ['brave_search'], 'optional': [] } } @@ -46,6 +50,7 @@ class SkillPlanner: SKILL_PRIORITY = { 'market_data': 1, # 最高优先级 'fundamental': 1, + 'brave_search': 1, # 新闻搜索也是高优先级 'technical_analysis': 2, 'advanced_data': 2, 'visualization': 3, # 最低优先级 @@ -141,7 +146,8 @@ class SkillPlanner: if enabled and dimension in self.DIMENSION_SKILL_MAP: mapping = self.DIMENSION_SKILL_MAP[dimension] skills.extend(mapping['required']) - # 可选技能稍后根据深度策略添加 + # 默认也添加可选技能(特别是 brave_search) + skills.extend(mapping['optional']) return skills @@ -217,6 +223,37 @@ class SkillPlanner: params['data_types'] = data_types + elif skill_name == 'brave_search': + # 构建搜索查询 + target = intent.get('target', {}) + stock_name = target.get('stock_name', '') + stock_code = target.get('stock_code', '') + dimensions = intent.get('dimensions', {}) + + # 根据维度构建搜索关键词 + search_keywords = [] + if stock_name: + search_keywords.append(stock_name) + elif stock_code: + search_keywords.append(stock_code) + + # 添加维度相关关键词 + if dimensions.get('fundamental'): + search_keywords.append('财报 业绩') + if dimensions.get('news'): + search_keywords.append('最新消息') + if dimensions.get('risk'): + search_keywords.append('风险 预警') + + # 如果没有特定维度,搜索一般新闻 + if not any(dimensions.values()): + search_keywords.append('最新动态') + + params['query'] = ' '.join(search_keywords) + params['search_type'] = 'news' # 默认搜索新闻 + params['count'] = 5 + params['freshness'] = 'pw' # 过去一周 + return params def _get_skill_reason(self, skill_name: str, intent: Dict[str, Any]) -> str: @@ -253,4 +290,14 @@ class SkillPlanner: elif skill_name == 'visualization': reasons.append('生成K线图表') + elif skill_name == 'brave_search': + if dimensions.get('news'): + reasons.append('用户关注最新新闻') + elif dimensions.get('fundamental'): + reasons.append('搜索公司最新动态和财报信息') + elif dimensions.get('risk'): + reasons.append('搜索风险预警信息') + else: + reasons.append('获取最新市场资讯和舆情') + return ', '.join(reasons) if reasons else '提供分析数据' diff --git a/backend/app/agent/smart_agent.py b/backend/app/agent/smart_agent.py index 8c9e9fb..114d54c 100644 --- a/backend/app/agent/smart_agent.py +++ b/backend/app/agent/smart_agent.py @@ -16,6 +16,7 @@ from app.skills.fundamental import FundamentalSkill from app.skills.visualization import VisualizationSkill from app.skills.advanced_data import AdvancedDataSkill from app.skills.us_stock_skill import USStockSkill +from app.skills.brave_search import BraveSearchSkill from app.services.llm_service import llm_service from app.services.tushare_service import tushare_service from app.utils.logger import logger @@ -60,7 +61,8 @@ class SmartStockAgent: skill_manager.register(VisualizationSkill()) skill_manager.register(AdvancedDataSkill()) skill_manager.register(USStockSkill()) - logger.info("技能注册完成(Tushare Pro高级数据 + 美股支持)") + skill_manager.register(BraveSearchSkill()) + logger.info("技能注册完成(Tushare Pro高级数据 + 美股支持 + Brave搜索)") async def process_message( self, @@ -112,7 +114,7 @@ class SmartStockAgent: message: str ) -> Dict[str, Any]: """ - 全面分析:整合多个数据源 + LLM深度分析 + 全面分析:使用 skill_planner 智能规划技能 + LLM深度分析 Args: stock_code: 股票代码 @@ -126,46 +128,48 @@ class SmartStockAgent: display_name = stock_name or stock_code - # 1. 并行获取所有数据 try: - # 获取实时行情 - quote_result = await skill_manager.execute_skill( - "market_data", - stock_code=stock_code, - data_type="quote" + # 1. 使用 QuestionAnalyzer 分析问题意图 + intent = await self.question_analyzer.analyze_question( + question=message, + context=[], + session_id="" ) - # 获取技术指标 - technical_result = await skill_manager.execute_skill( - "technical_analysis", - stock_code=stock_code, - indicators=["ma", "macd", "rsi", "kdj"] - ) + # 确保 intent 包含股票信息 + if 'target' not in intent: + intent['target'] = {} + intent['target']['stock_code'] = stock_code + intent['target']['stock_name'] = stock_name - # 获取基本面 - fundamental_result = await skill_manager.execute_skill( - "fundamental", + logger.info(f"问题意图分析: dimensions={intent.get('dimensions')}") + + # 2. 使用 SkillPlanner 规划技能(包括 brave_search) + plan = self.skill_planner.plan_skills(intent) + logger.info(f"技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + + # 3. 执行技能规划 + execution_results = await skill_manager.execute_plan( + plan=plan, stock_code=stock_code ) - # 获取高级数据(Tushare Pro 5000+积分) - advanced_result = await skill_manager.execute_skill( - "advanced_data", - stock_code=stock_code, - data_type="all" - ) + if execution_results['errors']: + logger.warning(f"技能执行有错误: {execution_results['errors']}") - # 整合数据 + # 4. 整合数据(兼容旧格式) + results = execution_results['results'] all_data = { "stock_code": stock_code, "stock_name": display_name, - "quote": quote_result.get("data") if quote_result.get("success") else None, - "technical": technical_result.get("data") if technical_result.get("success") else None, - "fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None, - "advanced": advanced_result.get("data") if advanced_result.get("success") else None + "quote": results.get("market_data"), + "technical": results.get("technical_analysis"), + "fundamental": results.get("fundamental"), + "advanced": results.get("advanced_data"), + "news": results.get("brave_search") # 新增:新闻数据 } - # 2. 使用LLM进行深度分析 + # 5. 使用LLM进行深度分析 if self.use_llm: analysis = await self._llm_comprehensive_analysis(all_data, message) else: @@ -175,12 +179,15 @@ class SmartStockAgent: "message": analysis, "metadata": { "type": "comprehensive", - "data": all_data + "data": all_data, + "plan": plan } } except Exception as e: logger.error(f"全面分析失败: {e}") + import traceback + logger.error(traceback.format_exc()) return { "message": f"分析{display_name}时出错:{str(e)}", "metadata": {"type": "error"} @@ -256,6 +263,31 @@ class SmartStockAgent: else: advanced_summary = "\n【高级财务数据】\n暂无高级数据\n" + # 格式化新闻数据 + news_section = "" + news_data = data.get('news', {}) + if news_data and not news_data.get('error'): + results = news_data.get('results', []) + if results: + news_section = "\n【最新新闻和舆情】(来源:Brave Search)\n" + for i, item in enumerate(results[:5], 1): + title = item.get('title', '无标题') + description = item.get('description', '') + source = item.get('source', '') + published = item.get('published', '') + news_section += f"{i}. {title}\n" + if description: + news_section += f" 摘要:{description[:100]}...\n" + if source: + news_section += f" 来源:{source}\n" + if published: + news_section += f" 发布时间:{published}\n" + news_section += "\n" + else: + news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n" + else: + news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n" + # 构建详细的分析提示 prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。 @@ -275,7 +307,7 @@ class SmartStockAgent: 数据来源:Tushare Pro API {json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'} {advanced_summary} - +{news_section} 请按以下结构进行分析,并在每个部分明确标注数据来源和时效性: ## 一、基本面分析 @@ -304,12 +336,13 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 **支撑与压力** 关键支撑位和压力位的具体价格区间。 -## 三、市场情绪分析 +## 三、市场情绪和新闻分析 分段分析市场情绪: - 第一段:资金流向分析(主力资金、大单资金流入/流出情况) - 第二段:融资融券情况(如有) -- 第三段:当前市场情绪(乐观/谨慎/悲观)及原因 -- 第四段:短期可能的催化因素 +- 第三段:**基于最新新闻分析市场情绪和舆情,识别可能影响股价的重要事件** +- 第四段:当前市场情绪(乐观/谨慎/悲观)及原因 +- 第五段:短期可能的催化因素 ## 四、投资建议 基于技术面分析,给出具体的操作建议和点位: @@ -349,6 +382,7 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 - 资金流向:Tushare Pro(主力资金、大单资金) - 融资融券:Tushare Pro(如有) - 公告数据:Tushare Pro(重大公告) +- 新闻舆情:Brave Search(最新市场资讯) 写作要求: 1. 语言简洁专业,避免过度修饰和比喻 @@ -1438,7 +1472,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} async def _handle_us_stock(self, keyword: str, message: str) -> Dict[str, Any]: """ - 处理美股查询 + 处理美股查询(使用 skill_planner) Args: keyword: 股票关键词 @@ -1453,30 +1487,75 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} logger.info(f"处理美股查询: {keyword} -> {symbol}") try: - # 调用美股分析技能 - result = await skill_manager.execute_skill( - "us_stock_analysis", - symbol=symbol, - analysis_type="comprehensive" + # 1. 使用 QuestionAnalyzer 分析问题意图 + intent = await self.question_analyzer.analyze_question( + question=message, + context=[], + session_id="" ) - if not result.get("success"): + # 确保 intent 包含股票信息 + if 'target' not in intent: + intent['target'] = {} + intent['target']['stock_code'] = symbol + intent['target']['stock_name'] = keyword + intent['target']['market'] = '美股' + + logger.info(f"美股问题意图分析: dimensions={intent.get('dimensions')}") + + # 2. 使用 SkillPlanner 规划技能(包括 brave_search) + plan = self.skill_planner.plan_skills(intent) + + # 3. 将 us_stock_analysis 添加到技能列表(如果不存在) + skill_names = [s['name'] for s in plan['skills']] + if 'us_stock_analysis' not in skill_names: + plan['skills'].insert(0, { + 'name': 'us_stock_analysis', + 'params': {'symbol': symbol, 'analysis_type': 'comprehensive'}, + 'priority': 1, + 'required': True, + 'reason': '获取美股基础数据' + }) + + logger.info(f"美股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + + # 4. 执行技能规划 + execution_results = await skill_manager.execute_plan( + plan=plan, + stock_code=symbol + ) + + if execution_results['errors']: + logger.warning(f"美股技能执行有错误: {execution_results['errors']}") + + # 5. 检查 us_stock_analysis 是否成功 + us_stock_data = execution_results['results'].get('us_stock_analysis') + if not us_stock_data or 'error' in us_stock_data: return { "message": f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。\n\n提示:美股代码通常为大写字母,如 AAPL(苹果)、TSLA(特斯拉)、MSFT(微软)等。", "metadata": {"type": "error"} } - # 使用LLM分析美股数据 + # 6. 整合数据 + all_data = { + "symbol": symbol, + "name": keyword, + **us_stock_data, + "news": execution_results['results'].get("brave_search") # 新增:新闻数据 + } + + # 7. 使用LLM分析美股数据 if self.use_llm: - analysis = await self._llm_us_stock_analysis(result["data"], message) + analysis = await self._llm_us_stock_analysis(all_data, message) else: - analysis = self._format_us_stock_data(result["data"]) + analysis = self._format_us_stock_data(all_data) return { "message": analysis, "metadata": { "type": "us_stock_analysis", - "data": result["data"] + "data": all_data, + "plan": plan } } @@ -1509,10 +1588,35 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} week_52_low = data.get("52_week_low", 0) technical = data.get("technical_indicators", {}) description = data.get("description", "") + news_data = data.get("news", {}) # 新增:获取新闻数据 # 格式化市值 market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M" + # 格式化新闻数据 + news_section = "" + if news_data and not news_data.get('error'): + results = news_data.get('results', []) + if results: + news_section = "\n【最新新闻和舆情】(来源:Brave Search)\n" + for i, item in enumerate(results[:5], 1): + title = item.get('title', '无标题') + description = item.get('description', '') + source = item.get('source', '') + published = item.get('published', '') + news_section += f"{i}. {title}\n" + if description: + news_section += f" 摘要:{description[:100]}...\n" + if source: + news_section += f" 来源:{source}\n" + if published: + news_section += f" 发布时间:{published}\n" + news_section += "\n" + else: + news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n" + else: + news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n" + # 构建分析提示 prompt = f"""你是一位专业的美股分析师。请基于以下数据对 {name} ({symbol}) 进行全面分析。 @@ -1545,7 +1649,7 @@ MA20:{f"${technical.get('ma20'):.2f}" if technical.get('ma20') else '计算中 MA60:{f"${technical.get('ma60'):.2f}" if technical.get('ma60') else '计算中'} RSI:{f"{technical.get('rsi'):.2f}" if technical.get('rsi') else '计算中'} MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中'} - +{news_section} 用户问题:{user_message} 请提供专业的分析报告,包括: @@ -1563,6 +1667,11 @@ MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中' - 关键支撑位和压力位 - RSI和MACD信号解读 +## 📰 市场情绪和新闻分析 +- 基于最新新闻分析市场情绪和舆情 +- 识别可能影响股价的重要事件或消息 +- 评估新闻对短期和中期走势的影响 + ## 💡 投资建议 - 短期操作建议(1-2周) - 中期投资价值(1-3个月) @@ -1572,16 +1681,17 @@ MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中' 1. 语言专业但易懂,避免过度修饰 2. 分析客观理性,基于数据和事实 3. 每个部分独立成段,段落间用空行分隔 -4. 控制在500-600字 -5. **不要在报告中添加日期标题,直接开始分析内容** -6. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" +4. **充分利用新闻数据进行市场情绪分析** +5. 控制在600-800字 +6. **不要在报告中添加日期标题,直接开始分析内容** +7. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" """ try: analysis = await self._call_llm_async( messages=[{"role": "user", "content": prompt}], temperature=0.7, - max_tokens=2000 + max_tokens=2500 # 增加 token 数量以容纳新闻分析 ) if analysis: @@ -1732,7 +1842,7 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} yield chunk async def _handle_a_stock_stream(self, stock_keyword: str, message: str): - """流式处理A股分析""" + """流式处理A股分析(使用 skill_planner)""" # 使用LLM进行智能匹配 stock_info = await self._match_stock_with_llm(stock_keyword) @@ -1744,23 +1854,50 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} stock_name = stock_info['name'] is_index = stock_info['is_index'] - # 获取数据(非流式) - try: - quote_result = await skill_manager.execute_skill("market_data", stock_code=stock_code, data_type="quote") - technical_result = await skill_manager.execute_skill("technical_analysis", stock_code=stock_code, indicators=["ma", "macd", "rsi", "kdj"]) - fundamental_result = await skill_manager.execute_skill("fundamental", stock_code=stock_code) - advanced_result = await skill_manager.execute_skill("advanced_data", stock_code=stock_code, data_type="all") + logger.info(f"[流式] A股分析: {stock_name}({stock_code})") + try: + # 1. 使用 QuestionAnalyzer 分析问题意图 + intent = await self.question_analyzer.analyze_question( + question=message, + context=[], + session_id="" + ) + + # 确保 intent 包含股票信息 + if 'target' not in intent: + intent['target'] = {} + intent['target']['stock_code'] = stock_code + intent['target']['stock_name'] = stock_name + + logger.info(f"[流式] A股问题意图分析: dimensions={intent.get('dimensions')}") + + # 2. 使用 SkillPlanner 规划技能(包括 brave_search) + plan = self.skill_planner.plan_skills(intent) + logger.info(f"[流式] A股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + + # 3. 执行技能规划 + execution_results = await skill_manager.execute_plan( + plan=plan, + stock_code=stock_code + ) + + if execution_results['errors']: + logger.warning(f"[流式] A股技能执行有错误: {execution_results['errors']}") + + # 4. 整合数据(兼容旧格式) + results = execution_results['results'] all_data = { "stock_code": stock_code, "stock_name": stock_name, - "quote": quote_result.get("data") if quote_result.get("success") else None, - "technical": technical_result.get("data") if technical_result.get("success") else None, - "fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None, - "advanced": advanced_result.get("data") if advanced_result.get("success") else None + "quote": results.get("market_data"), + "technical": results.get("technical_analysis"), + "fundamental": results.get("fundamental"), + "advanced": results.get("advanced_data"), + "news": results.get("brave_search") # 新增:新闻数据 } - # 使用LLM流式分析 + # 5. 使用LLM流式分析 if self.use_llm: async for chunk in self._llm_comprehensive_analysis_stream(all_data, message, is_index): yield chunk @@ -1769,42 +1906,282 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} except Exception as e: logger.error(f"A股分析失败: {e}") + import traceback + logger.error(traceback.format_exc()) yield f"分析{stock_name}时出错:{str(e)}" async def _handle_us_stock_stream(self, keyword: str, message: str): - """流式处理美股分析(智能模式)""" + """流式处理美股分析(使用 skill_planner)""" symbol = self._get_us_stock_symbol(keyword) logger.info(f"[智能模式-流式] 美股查询: {keyword} -> {symbol}") try: - result = await skill_manager.execute_skill("us_stock_analysis", symbol=symbol, analysis_type="comprehensive") + # 1. 使用 QuestionAnalyzer 分析问题意图 + intent = await self.question_analyzer.analyze_question( + question=message, + context=[], + session_id="" + ) - if not result.get("success"): + # 确保 intent 包含股票信息 + if 'target' not in intent: + intent['target'] = {} + intent['target']['stock_code'] = symbol + intent['target']['stock_name'] = keyword + intent['target']['market'] = '美股' + + logger.info(f"[流式] 美股问题意图分析: dimensions={intent.get('dimensions')}") + + # 2. 使用 SkillPlanner 规划技能(包括 brave_search) + plan = self.skill_planner.plan_skills(intent) + + # 3. 将 us_stock_analysis 添加到技能列表(如果不存在) + skill_names = [s['name'] for s in plan['skills']] + if 'us_stock_analysis' not in skill_names: + plan['skills'].insert(0, { + 'name': 'us_stock_analysis', + 'params': {'symbol': symbol, 'analysis_type': 'comprehensive'}, + 'priority': 1, + 'required': True, + 'reason': '获取美股基础数据' + }) + + logger.info(f"[流式] 美股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + + # 4. 执行技能规划 + execution_results = await skill_manager.execute_plan( + plan=plan, + stock_code=symbol + ) + + if execution_results['errors']: + logger.warning(f"[流式] 美股技能执行有错误: {execution_results['errors']}") + + # 5. 检查 us_stock_analysis 是否成功 + us_stock_data = execution_results['results'].get('us_stock_analysis') + if not us_stock_data or 'error' in us_stock_data: yield f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。" return - # 使用智能模式的动态prompt生成 + # 6. 整合数据 + all_data = { + "symbol": symbol, + "name": keyword, + **us_stock_data, + "news": execution_results['results'].get("brave_search") # 新增:新闻数据 + } + + # 7. 使用智能模式的动态prompt生成 if self.use_llm: # 构建美股数据的动态prompt - us_data = result["data"] - prompt = self._build_us_stock_dynamic_prompt(us_data, symbol, message) + prompt = self._build_us_stock_dynamic_prompt(all_data, symbol, message) # 流式生成 stream = llm_service.chat_stream( messages=[{"role": "user", "content": prompt}], temperature=0.7, - max_tokens=2000 + max_tokens=2500 # 增加 token 数量以容纳新闻分析 ) for chunk in stream: yield chunk else: - yield self._format_us_stock_data(result["data"]) + yield self._format_us_stock_data(all_data) except Exception as e: logger.error(f"美股查询失败: {e}") + import traceback + logger.error(traceback.format_exc()) yield f"查询美股 {symbol} 时出错:{str(e)}" + def _format_news_section(self, news_data: Dict[str, Any]) -> str: + """ + 格式化新闻数据为统一格式 + + Args: + news_data: 新闻数据字典 + + Returns: + 格式化后的新闻文本 + """ + if not news_data or news_data.get('error'): + return "\n**最新新闻和舆情**: 暂无相关新闻\n" + + results = news_data.get('results', []) + if not results: + return "\n**最新新闻和舆情**: 暂无相关新闻\n" + + news_section = "\n**最新新闻和舆情**(来源:Brave Search):\n" + for i, item in enumerate(results[:5], 1): + title = item.get('title', '无标题') + description = item.get('description', '') + source = item.get('source', '') + published = item.get('published', '') + news_section += f"{i}. {title}\n" + if description: + news_section += f" 摘要:{description[:100]}...\n" + if source: + news_section += f" 来源:{source}\n" + if published: + news_section += f" 发布时间:{published}\n" + news_section += "\n" + + return news_section + + def _build_unified_analysis_prompt( + self, + market: str, + stock_info: Dict[str, Any], + data: Dict[str, Any], + user_message: str + ) -> str: + """ + 统一的分析提示词构建函数(自然语言风格) + + Args: + market: 市场类型('A股' 或 '美股') + stock_info: 股票基本信息 {'code': ..., 'name': ...} + data: 市场数据(行情、技术、基本面、新闻等) + user_message: 用户问题 + + Returns: + 构建好的prompt字符串 + """ + from datetime import datetime + + current_time = datetime.now().strftime("%Y-%m-%d %H:%M") + stock_code = stock_info.get('code', '') + stock_name = stock_info.get('name', '') + + # 格式化新闻数据(统一格式) + news_section = self._format_news_section(data.get('news', {})) + + if market == '美股': + # 美股数据格式 + current_price = data.get('current_price', 0) + change = data.get('change', 0) + change_percent = data.get('change_percent', 0) + volume = data.get('volume', 0) + market_cap = data.get('market_cap', 0) + pe_ratio = data.get('pe_ratio', 0) + pb_ratio = data.get('pb_ratio', 0) + + technical = data.get('technical_indicators', {}) + ma5 = technical.get('ma5', 0) + ma10 = technical.get('ma10', 0) + ma20 = technical.get('ma20', 0) + rsi = technical.get('rsi', 0) + macd = technical.get('macd', 0) + + market_data_section = f""" +**行情数据**: +- 最新价: ${current_price:.2f} +- 涨跌: ${change:+.2f} ({change_percent:+.2f}%) +- 成交量: {volume:,.0f} +- 市值: ${market_cap:,.0f} +- 市盈率(PE): {pe_ratio:.2f} +- 市净率(PB): {pb_ratio:.2f} + +**技术指标**: +- 均线: MA5=${ma5:.2f}, MA10=${ma10:.2f}, MA20=${ma20:.2f} +- RSI: {rsi:.2f} +- MACD: {macd:.4f} +""" + + analyst_role = "美股分析师" + risk_disclaimer = "以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" + + else: # A股 + # A股数据格式 + quote = data.get('quote', {}) + technical = data.get('technical', {}) + fundamental = data.get('fundamental', {}) + advanced = data.get('advanced', {}) + + # 获取交易日期 + quote_date = quote.get('trade_date', '未知') if quote else '未知' + + # 构建行情数据部分 + if quote: + market_data_section = f""" +**行情数据**(截止:{quote_date}): +- 最新价: ¥{quote.get('close', 0):.2f} +- 涨跌幅: {quote.get('pct_chg', 0):+.2f}% +- 成交量: {quote.get('vol', 0):,.0f}手 +- 成交额: {quote.get('amount', 0):,.0f}千元 +- 换手率: {quote.get('turnover_rate', 0):.2f}% +""" + else: + market_data_section = "\n**行情数据**: 数据获取失败\n" + + # 添加技术指标 + if technical: + market_data_section += f""" +**技术指标**(截止:{quote_date}): +- 均线: MA5=¥{technical.get('ma5', 0):.2f}, MA10=¥{technical.get('ma10', 0):.2f}, MA20=¥{technical.get('ma20', 0):.2f} +- RSI: {technical.get('rsi', 0):.2f} +- MACD: {technical.get('macd', 0):.4f} +""" + + # 添加基本面数据 + if fundamental: + market_data_section += f""" +**基本面数据**: +- 市盈率(PE): {fundamental.get('pe', 0):.2f} +- 市净率(PB): {fundamental.get('pb', 0):.2f} +- 总市值: {fundamental.get('total_mv', 0):,.0f}亿元 +- ROE: {fundamental.get('roe', 0):.2f}% +""" + + # 添加高级数据(资金流向、融资融券等) + if advanced: + if advanced.get('money_flow'): + money_flow = advanced['money_flow'][0] if advanced['money_flow'] else {} + market_data_section += f""" +**资金流向**: +- 主力净流入: {money_flow.get('net_mf_amount', 0):,.0f}万元 +- 大单净流入: {money_flow.get('buy_lg_amount', 0) - money_flow.get('sell_lg_amount', 0):,.0f}万元 +""" + + if advanced.get('margin'): + margin = advanced['margin'][0] if advanced['margin'] else {} + market_data_section += f""" +**融资融券**: +- 融资余额: {margin.get('rzye', 0):,.0f}元 +- 融券余额: {margin.get('rqye', 0):,.0f}元 +""" + + analyst_role = "A股分析师" + risk_disclaimer = "以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" + + # 构建统一的prompt + prompt = f"""你是一位专业的{analyst_role}。请根据以下数据分析【{stock_name}({stock_code})】。 + +**用户问题**: {user_message} + +**【重要】最新新闻和舆情(必须分析)** +{news_section} + +## 数据信息 +{market_data_section} + +## 分析要求 + +请根据用户的问题,提供自然、有针对性的分析。不要使用固定格式,而是像专业分析师一样,用自然的语言回答用户的问题。 + +**重要:必须在分析中包含对上面提供的最新新闻的分析,评估新闻对股价的影响。** + +分析时请注意: +- 如果用户关注价格走势,重点分析价格和趋势,并结合新闻分析市场情绪 +- 如果用户关注技术指标,重点分析技术面,并结合新闻判断短期走势 +- 如果用户关注基本面,重点分析公司情况和估值,并结合新闻评估投资价值 +- **无论用户问什么,都要在分析中提及最新新闻对股价的潜在影响** + +请直接开始分析,不要添加日期标题。控制在600-800字。最后声明:"{risk_disclaimer}" +""" + + return prompt + def _build_us_stock_dynamic_prompt( self, data: Dict[str, Any], @@ -1812,7 +2189,7 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} user_message: str ) -> str: """ - 为美股构建动态prompt + 为美股构建动态prompt(调用统一函数) Args: data: 美股数据 @@ -1822,165 +2199,42 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} Returns: prompt字符串 """ - # 提取数据 - name = data.get('name', symbol) - current_price = data.get('current_price', 0) - change = data.get('change', 0) - change_percent = data.get('change_percent', 0) - volume = data.get('volume', 0) - market_cap = data.get('market_cap', 0) - pe_ratio = data.get('pe_ratio', 0) + stock_info = { + 'code': symbol, + 'name': data.get('name', symbol) + } - # 技术指标 - technical = data.get('technical_indicators', {}) - ma5 = technical.get('ma5', 0) - ma10 = technical.get('ma10', 0) - ma20 = technical.get('ma20', 0) - rsi = technical.get('rsi', 0) - macd = technical.get('macd', 0) - - prompt = f"""你是一个专业的美股分析师。请根据以下数据分析【{name}({symbol})】。 - -**用户问题**: {user_message} - -## 数据信息 - -**行情数据**: -- 最新价: ${current_price:.2f} -- 涨跌: ${change:+.2f} ({change_percent:+.2f}%) -- 成交量: {volume:,.0f} -- 市值: ${market_cap:,.0f} -- 市盈率(PE): {pe_ratio:.2f} - -**技术指标**: -- 均线: MA5=${ma5:.2f}, MA10=${ma10:.2f}, MA20=${ma20:.2f} -- RSI: {rsi:.2f} -- MACD: {macd:.4f} - -## 分析要求 - -请根据用户的问题,提供自然、有针对性的分析。不要使用固定格式,而是像专业分析师一样,用自然的语言回答用户的问题。 - -- 如果用户关注价格走势,重点分析价格和趋势 -- 如果用户关注技术指标,重点分析技术面 -- 如果用户关注基本面,重点分析公司情况和估值 - -请直接开始分析,不要添加日期标题。最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" -""" - - return prompt + return self._build_unified_analysis_prompt( + market='美股', + stock_info=stock_info, + data=data, + user_message=user_message + ) async def _llm_comprehensive_analysis_stream(self, data: Dict[str, Any], user_message: str, is_index: bool = False): - """使用LLM流式进行综合分析""" - from datetime import datetime - import json + """使用LLM流式进行综合分析(调用统一函数)""" + stock_info = { + 'code': data.get('stock_code', ''), + 'name': data.get('stock_name', '') + } - current_time = datetime.now().strftime("%Y-%m-%d %H:%M") + # 使用统一的prompt构建函数 + prompt = self._build_unified_analysis_prompt( + market='A股', + stock_info=stock_info, + data=data, + user_message=user_message + ) - # 获取行情数据的交易日期 - quote_date = "未知" - if data.get('quote') and data['quote'].get('trade_date'): - quote_date = data['quote']['trade_date'] - - # 构建高级数据摘要 - advanced_summary = "" - if data.get('advanced'): - advanced_data = data['advanced'] - advanced_summary = "\n【高级财务数据】(Tushare Pro 5000+积分)\n" - - # 财务指标 - if advanced_data.get('financial'): - financial = advanced_data['financial'] - if financial.get('indicators'): - indicators = financial['indicators'].get('indicators', {}) - advanced_summary += f"财务指标(截止:{financial['indicators'].get('end_date', '未知')}):\n" - advanced_summary += f" ROE: {indicators.get('roe', 'N/A')}%\n" - advanced_summary += f" ROA: {indicators.get('roa', 'N/A')}%\n" - advanced_summary += f" 毛利率: {indicators.get('gross_margin', 'N/A')}%\n" - advanced_summary += f" 资产负债率: {indicators.get('debt_to_assets', 'N/A')}%\n" - advanced_summary += f" 流动比率: {indicators.get('current_ratio', 'N/A')}\n\n" - - # 估值数据 - if advanced_data.get('valuation'): - valuation = advanced_data['valuation'] - advanced_summary += f"估值指标:\n" - advanced_summary += f" PE(市盈率): {valuation.get('pe', 'N/A')}\n" - advanced_summary += f" PB(市净率): {valuation.get('pb', 'N/A')}\n" - advanced_summary += f" PS(市销率): {valuation.get('ps', 'N/A')}\n" - advanced_summary += f" 总市值: {valuation.get('total_mv', 'N/A')}万元\n" - advanced_summary += f" 流通市值: {valuation.get('circ_mv', 'N/A')}万元\n" - advanced_summary += f" 换手率: {valuation.get('turnover_rate', 'N/A')}%\n\n" - - # 资金流向(最近一天) - if advanced_data.get('money_flow') and len(advanced_data['money_flow']) > 0: - latest_flow = advanced_data['money_flow'][0] - advanced_summary += f"资金流向({latest_flow.get('trade_date', '最近')}):\n" - advanced_summary += f" 主力净流入: {latest_flow.get('net_mf_amount', 'N/A')}万元\n" - advanced_summary += f" 超大单净流入: {latest_flow.get('buy_elg_amount', 0) - latest_flow.get('sell_elg_amount', 0):.2f}万元\n" - advanced_summary += f" 大单净流入: {latest_flow.get('buy_lg_amount', 0) - latest_flow.get('sell_lg_amount', 0):.2f}万元\n\n" - - # 融资融券 - if advanced_data.get('margin') and len(advanced_data['margin']) > 0: - latest_margin = advanced_data['margin'][0] - advanced_summary += f"融资融券({latest_margin.get('trade_date', '最近')}):\n" - advanced_summary += f" 融资余额: {latest_margin.get('rzye', 'N/A')}元\n" - advanced_summary += f" 融券余额: {latest_margin.get('rqye', 'N/A')}元\n\n" - - else: - advanced_summary = "\n【高级财务数据】\n暂无高级数据\n" - - # 构建详细的分析提示 - prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。 - -用户问题:{user_message} - -【实时行情数据】 -数据来源:Tushare Pro API -交易日期:{quote_date} -{json.dumps(data.get('quote'), ensure_ascii=False, indent=2) if data.get('quote') else '数据获取失败'} - -【技术指标数据】 -数据来源:Tushare Pro API(基于历史K线数据计算) -计算截止日期:{quote_date} -{json.dumps(data.get('technical'), ensure_ascii=False, indent=2) if data.get('technical') else '数据获取失败'} - -【基本面数据】 -数据来源:Tushare Pro API -{json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'} -{advanced_summary} - -请按以下结构进行分析: - -## 一、基本面分析 -分段说明公司情况,每个要点独立成段。 - -## 二、技术面分析(数据截止:{quote_date}) -使用清晰的分段结构,每个技术指标独立成段。 - -## 三、市场情绪分析 -分段分析市场情绪和资金流向。 - -## 四、投资建议 -基于分析给出具体的操作建议和点位。 - -写作要求: -1. 语言简洁专业但易懂 -2. 分析客观理性,基于数据 -3. 每个分析点独立成段 -4. 控制在600-800字 -5. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - # 流式调用LLM(同步生成器,使用线程避免阻塞) + # 流式调用LLM import asyncio stream = llm_service.chat_stream( messages=[{"role": "user", "content": prompt}], temperature=0.7, - max_tokens=2000 + max_tokens=2500 ) # 在线程中迭代同步生成器,避免阻塞事件循环 - loop = asyncio.get_event_loop() for chunk in stream: # 每次yield后让出控制权 await asyncio.sleep(0) @@ -2078,7 +2332,7 @@ MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中' stream = llm_service.chat_stream( messages=[{"role": "user", "content": prompt}], temperature=0.7, - max_tokens=2000 + max_tokens=2500 # 增加 token 数量以容纳新闻分析 ) # 在线程中迭代同步生成器,避免阻塞事件循环 diff --git a/backend/app/main.py b/backend/app/main.py index cfbf93e..858b8df 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -55,9 +55,11 @@ if os.path.exists(frontend_path): @app.get("/") async def root(): - """根路径,重定向到登录页""" - from fastapi.responses import RedirectResponse - return RedirectResponse(url="/static/login.html") + """根路径,返回主应用页面""" + index_path = os.path.join(frontend_path, "index.html") + if os.path.exists(index_path): + return FileResponse(index_path) + return {"message": "页面不存在"} @app.get("/app") async def app_page():