diff --git a/backend/app/agent/core.py b/backend/app/agent/core.py deleted file mode 100644 index d301468..0000000 --- a/backend/app/agent/core.py +++ /dev/null @@ -1,378 +0,0 @@ -""" -AI Agent核心 -基于LangChain的股票分析Agent -""" -import re -import json -from typing import Dict, Any, Optional -from app.config import get_settings -from app.agent.context import ContextManager -from app.agent.skill_manager import skill_manager -from app.skills.market_data import MarketDataSkill -from app.skills.technical_analysis import TechnicalAnalysisSkill -from app.skills.fundamental import FundamentalSkill -from app.skills.visualization import VisualizationSkill -from app.utils.logger import logger - - -class StockAnalysisAgent: - """股票分析Agent""" - - def __init__(self): - """初始化Agent""" - self.context_manager = ContextManager() - self.settings = get_settings() - - # 注册技能 - self._register_skills() - - # 初始化LLM(简化版,使用规则匹配) - # 在实际部署时,这里应该集成智谱AI GLM-4 - self.use_llm = bool(self.settings.zhipuai_api_key) - - logger.info("Stock Analysis Agent初始化完成") - - def _register_skills(self): - """注册所有技能""" - skill_manager.register(MarketDataSkill()) - skill_manager.register(TechnicalAnalysisSkill()) - skill_manager.register(FundamentalSkill()) - skill_manager.register(VisualizationSkill()) - logger.info("技能注册完成") - - async def process_message( - self, - message: str, - session_id: str, - user_id: Optional[str] = None - ) -> Dict[str, Any]: - """ - 处理用户消息 - - Args: - message: 用户消息 - session_id: 会话ID - user_id: 用户ID - - Returns: - 响应结果 - """ - logger.info(f"处理消息: {message[:50]}...") - - # 保存用户消息 - self.context_manager.add_message(session_id, "user", message) - - # 意图识别和技能调用 - intent = self._recognize_intent(message) - logger.info(f"识别意图: {intent}") - - # 执行技能 - result = await self._execute_intent(intent, message) - - # 生成响应 - response = self._generate_response(intent, result) - - # 保存助手响应 - self.context_manager.add_message( - session_id, - "assistant", - response["message"], - metadata=response.get("metadata") - ) - - return response - - def _recognize_intent(self, message: str) -> Dict[str, Any]: - """ - 识别用户意图(简化版规则匹配) - - Args: - message: 用户消息 - - Returns: - 意图字典 - """ - message_lower = message.lower() - - # 提取股票代码 - stock_code = self._extract_stock_code(message) - - # 行情查询 - if any(keyword in message_lower for keyword in ["行情", "价格", "涨跌", "实时", "quote"]): - return { - "type": "market_data", - "skill": "market_data", - "params": { - "stock_code": stock_code, - "data_type": "quote" - } - } - - # K线查询 - if any(keyword in message_lower for keyword in ["k线", "kline", "走势", "图表"]): - return { - "type": "visualization", - "skill": "visualization", - "params": { - "stock_code": stock_code, - "chart_type": "candlestick" - } - } - - # 技术分析 - if any(keyword in message_lower for keyword in ["技术", "指标", "macd", "rsi", "kdj", "均线", "ma"]): - return { - "type": "technical_analysis", - "skill": "technical_analysis", - "params": { - "stock_code": stock_code, - "indicators": ["ma", "macd", "rsi"] - } - } - - # 基本面 - if any(keyword in message_lower for keyword in ["基本面", "公司", "行业", "信息"]): - return { - "type": "fundamental", - "skill": "fundamental", - "params": { - "stock_code": stock_code - } - } - - # 默认:行情查询 - if stock_code: - return { - "type": "market_data", - "skill": "market_data", - "params": { - "stock_code": stock_code, - "data_type": "quote" - } - } - - # 无法识别 - return { - "type": "unknown", - "skill": None, - "params": {} - } - - def _extract_stock_code(self, message: str) -> Optional[str]: - """ - 从消息中提取股票代码 - - Args: - message: 用户消息 - - Returns: - 股票代码或None - """ - from app.utils.stock_names import search_stock_by_name - - # 匹配6位数字 - pattern = r'\b\d{6}\b' - matches = re.findall(pattern, message) - - if matches: - return matches[0] - - # 使用股票名称数据库搜索 - # 提取可能的股票名称(2-6个汉字) - chinese_pattern = r'[\u4e00-\u9fa5]{2,6}' - chinese_words = re.findall(chinese_pattern, message) - - for word in chinese_words: - code = search_stock_by_name(word) - if code: - logger.info(f"识别股票名称: {word} -> {code}") - return code - - return None - - async def _execute_intent(self, intent: Dict[str, Any], message: str) -> Dict[str, Any]: - """ - 执行意图对应的技能 - - Args: - intent: 意图字典 - message: 原始消息 - - Returns: - 执行结果 - """ - if intent["type"] == "unknown": - return { - "success": False, - "error": "无法理解您的问题,请提供股票代码或明确的查询意图" - } - - skill_name = intent["skill"] - params = intent["params"] - - if not params.get("stock_code"): - return { - "success": False, - "error": "请提供股票代码(6位数字)" - } - - # 执行技能 - result = await skill_manager.execute_skill(skill_name, **params) - - return result - - def _generate_response(self, intent: Dict[str, Any], result: Dict[str, Any]) -> Dict[str, Any]: - """ - 生成响应消息 - - Args: - intent: 意图 - result: 执行结果 - - Returns: - 响应字典 - """ - if not result.get("success", True): - return { - "message": f"抱歉,{result.get('error', '处理失败')}", - "metadata": { - "type": "error" - } - } - - data = result.get("data", result) - - # 根据意图类型生成不同响应 - if intent["type"] == "market_data": - return self._format_market_data_response(data) - elif intent["type"] == "technical_analysis": - return self._format_technical_response(data) - elif intent["type"] == "fundamental": - return self._format_fundamental_response(data) - elif intent["type"] == "visualization": - return self._format_visualization_response(data) - else: - return { - "message": "查询完成", - "metadata": { - "type": "data", - "data": data - } - } - - def _format_market_data_response(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化行情数据响应""" - if "error" in data: - return { - "message": f"查询失败:{data['error']}", - "metadata": {"type": "error"} - } - - if "kline_data" in data: - kline_data = data["kline_data"] - message = f"已获取K线数据,共{len(kline_data)}条记录" - return { - "message": message, - "metadata": { - "type": "kline", - "data": kline_data - } - } - - # 实时行情 - message = f""" -【{data.get('name', '股票')}】({data.get('ts_code', '')}) -交易日期:{data.get('trade_date', '')} -最新价:{data.get('close', 0):.2f} -涨跌额:{data.get('change', 0):.2f} -涨跌幅:{data.get('pct_chg', 0):.2f}% -开盘价:{data.get('open', 0):.2f} -最高价:{data.get('high', 0):.2f} -最低价:{data.get('low', 0):.2f} -成交量:{data.get('vol', 0):.0f}手 -成交额:{data.get('amount', 0):.0f}千元 - """.strip() - - return { - "message": message, - "metadata": { - "type": "quote", - "data": data - } - } - - def _format_technical_response(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化技术分析响应""" - if "error" in data: - return { - "message": f"分析失败:{data['error']}", - "metadata": {"type": "error"} - } - - indicators = data.get("indicators", {}) - message_parts = [f"【{data.get('stock_code', '')}】技术指标:\n"] - - if "ma" in indicators: - ma = indicators["ma"] - message_parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}") - - if "macd" in indicators: - macd = indicators["macd"] - message_parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}") - - if "rsi" in indicators: - rsi = indicators["rsi"] - message_parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}, RSI24={rsi.get('rsi24')}") - - return { - "message": "\n".join(message_parts), - "metadata": { - "type": "technical", - "data": data - } - } - - def _format_fundamental_response(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化基本面响应""" - if "error" in data: - return { - "message": f"查询失败:{data['error']}", - "metadata": {"type": "error"} - } - - message = f""" -【{data.get('name', '股票')}】基本信息 -股票代码:{data.get('ts_code', '')} -所属地域:{data.get('area', '')} -所属行业:{data.get('industry', '')} -上市市场:{data.get('market', '')} -上市日期:{data.get('list_date', '')} - """.strip() - - return { - "message": message, - "metadata": { - "type": "fundamental", - "data": data - } - } - - def _format_visualization_response(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化可视化响应""" - if "error" in data: - return { - "message": f"生成图表失败:{data['error']}", - "metadata": {"type": "error"} - } - - return { - "message": f"已生成{data.get('stock_code', '')}的K线图", - "metadata": { - "type": "chart", - "data": data - } - } - - -# 创建全局Agent实例 -stock_agent = StockAnalysisAgent() diff --git a/backend/app/agent/enhanced_agent.py b/backend/app/agent/enhanced_agent.py deleted file mode 100644 index bbd90f0..0000000 --- a/backend/app/agent/enhanced_agent.py +++ /dev/null @@ -1,377 +0,0 @@ -""" -增强版Agent - 集成LLM智能分析 -""" -import re -import json -from typing import Dict, Any, Optional -from app.config import get_settings -from app.agent.context import ContextManager -from app.agent.skill_manager import skill_manager -from app.skills.market_data import MarketDataSkill -from app.skills.technical_analysis import TechnicalAnalysisSkill -from app.skills.fundamental import FundamentalSkill -from app.skills.visualization import VisualizationSkill -from app.services.llm_service import llm_service -from app.utils.logger import logger -from app.utils.stock_names import search_stock_by_name, get_stock_name - - -class EnhancedStockAgent: - """增强版股票分析Agent(集成LLM)""" - - def __init__(self): - """初始化Agent""" - self.context_manager = ContextManager() - self.settings = get_settings() - - # 注册技能 - self._register_skills() - - # 检查LLM是否可用 - self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None - - if self.use_llm: - logger.info("Enhanced Agent初始化完成(LLM模式)") - else: - logger.info("Enhanced Agent初始化完成(规则模式)") - - def _register_skills(self): - """注册所有技能""" - skill_manager.register(MarketDataSkill()) - skill_manager.register(TechnicalAnalysisSkill()) - skill_manager.register(FundamentalSkill()) - skill_manager.register(VisualizationSkill()) - logger.info("技能注册完成") - - async def process_message( - self, - message: str, - session_id: str, - user_id: Optional[str] = None - ) -> Dict[str, Any]: - """ - 处理用户消息(增强版) - - Args: - message: 用户消息 - session_id: 会话ID - user_id: 用户ID - - Returns: - 响应结果 - """ - logger.info(f"处理消息: {message[:50]}...") - - # 保存用户消息 - self.context_manager.add_message(session_id, "user", message) - - # 提取股票代码 - stock_code = self._extract_stock_code(message) - - # 使用LLM或规则识别意图 - if self.use_llm: - intent = await self._recognize_intent_with_llm(message, stock_code) - else: - intent = self._recognize_intent_with_rules(message, stock_code) - - logger.info(f"识别意图: {intent}") - - # 执行技能 - result = await self._execute_intent(intent, message) - - # 生成响应(使用LLM增强) - response = await self._generate_response(intent, result, stock_code) - - # 保存助手响应 - self.context_manager.add_message( - session_id, - "assistant", - response["message"], - metadata=response.get("metadata") - ) - - return response - - async def _recognize_intent_with_llm( - self, - message: str, - stock_code: Optional[str] - ) -> Dict[str, Any]: - """使用LLM识别意图""" - try: - llm_result = llm_service.analyze_intent(message) - - intent_type = llm_result.get("type", "unknown") - confidence = llm_result.get("confidence", 0) - - # 如果置信度太低,回退到规则模式 - if confidence < 0.5: - logger.info("LLM置信度低,回退到规则模式") - return self._recognize_intent_with_rules(message, stock_code) - - # 构建意图 - intent = { - "type": intent_type, - "confidence": confidence, - "skill": self._map_intent_to_skill(intent_type), - "params": {"stock_code": stock_code} if stock_code else {} - } - - return intent - - except Exception as e: - logger.error(f"LLM意图识别失败: {e}") - return self._recognize_intent_with_rules(message, stock_code) - - def _recognize_intent_with_rules( - self, - message: str, - stock_code: Optional[str] - ) -> Dict[str, Any]: - """使用规则识别意图(原有逻辑)""" - message_lower = message.lower() - - # 行情查询 - if any(keyword in message_lower for keyword in ["行情", "价格", "涨跌", "实时", "quote"]): - return { - "type": "market_data", - "skill": "market_data", - "params": { - "stock_code": stock_code, - "data_type": "quote" - } - } - - # K线查询 - if any(keyword in message_lower for keyword in ["k线", "kline", "走势", "图表"]): - return { - "type": "visualization", - "skill": "visualization", - "params": { - "stock_code": stock_code, - "chart_type": "candlestick" - } - } - - # 技术分析 - if any(keyword in message_lower for keyword in ["技术", "指标", "macd", "rsi", "kdj", "均线", "ma"]): - return { - "type": "technical_analysis", - "skill": "technical_analysis", - "params": { - "stock_code": stock_code, - "indicators": ["ma", "macd", "rsi"] - } - } - - # 基本面 - if any(keyword in message_lower for keyword in ["基本面", "公司", "行业", "信息"]): - return { - "type": "fundamental", - "skill": "fundamental", - "params": { - "stock_code": stock_code - } - } - - # 默认:行情查询 - if stock_code: - return { - "type": "market_data", - "skill": "market_data", - "params": { - "stock_code": stock_code, - "data_type": "quote" - } - } - - # 无法识别 - return { - "type": "unknown", - "skill": None, - "params": {} - } - - def _map_intent_to_skill(self, intent_type: str) -> Optional[str]: - """将意图类型映射到技能名称""" - mapping = { - "market_data": "market_data", - "technical_analysis": "technical_analysis", - "fundamental": "fundamental", - "visualization": "visualization" - } - return mapping.get(intent_type) - - def _extract_stock_code(self, message: str) -> Optional[str]: - """从消息中提取股票代码""" - # 匹配6位数字 - pattern = r'\b\d{6}\b' - matches = re.findall(pattern, message) - - if matches: - return matches[0] - - # 使用股票名称数据库搜索 - chinese_pattern = r'[\u4e00-\u9fa5]{2,6}' - chinese_words = re.findall(chinese_pattern, message) - - for word in chinese_words: - code = search_stock_by_name(word) - if code: - logger.info(f"识别股票名称: {word} -> {code}") - return code - - return None - - async def _execute_intent(self, intent: Dict[str, Any], message: str) -> Dict[str, Any]: - """执行意图对应的技能""" - if intent["type"] == "unknown": - return { - "success": False, - "error": "无法理解您的问题,请提供股票代码或明确的查询意图" - } - - skill_name = intent["skill"] - params = intent["params"] - - if not params.get("stock_code"): - return { - "success": False, - "error": "请提供股票代码或股票名称" - } - - # 执行技能 - result = await skill_manager.execute_skill(skill_name, **params) - return result - - async def _generate_response( - self, - intent: Dict[str, Any], - result: Dict[str, Any], - stock_code: Optional[str] - ) -> Dict[str, Any]: - """生成响应消息(使用LLM增强)""" - if not result.get("success", True): - return { - "message": f"抱歉,{result.get('error', '处理失败')}", - "metadata": {"type": "error"} - } - - data = result.get("data", result) - - # 基础格式化 - base_response = self._format_response_basic(intent, data) - - # 如果启用LLM,添加智能分析 - if self.use_llm and stock_code and intent["type"] == "technical_analysis": - try: - stock_name = get_stock_name(stock_code) or stock_code - llm_summary = llm_service.generate_analysis_summary( - stock_code, stock_name, data - ) - base_response["message"] += f"\n\n【AI分析】\n{llm_summary}" - except Exception as e: - logger.error(f"LLM分析生成失败: {e}") - - return base_response - - def _format_response_basic(self, intent: Dict[str, Any], data: Dict[str, Any]) -> Dict[str, Any]: - """基础响应格式化(原有逻辑)""" - if "error" in data: - return { - "message": f"查询失败:{data['error']}", - "metadata": {"type": "error"} - } - - intent_type = intent["type"] - - if intent_type == "market_data": - return self._format_market_data(data) - elif intent_type == "technical_analysis": - return self._format_technical(data) - elif intent_type == "fundamental": - return self._format_fundamental(data) - elif intent_type == "visualization": - return self._format_visualization(data) - else: - return { - "message": "查询完成", - "metadata": {"type": "data", "data": data} - } - - def _format_market_data(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化行情数据""" - if "kline_data" in data: - kline_data = data["kline_data"] - message = f"已获取K线数据,共{len(kline_data)}条记录" - return { - "message": message, - "metadata": {"type": "kline", "data": kline_data} - } - - message = f""" -【{data.get('name', '股票')}】({data.get('ts_code', '')}) -交易日期:{data.get('trade_date', '')} -最新价:{data.get('close', 0):.2f} -涨跌额:{data.get('change', 0):.2f} -涨跌幅:{data.get('pct_chg', 0):.2f}% -开盘价:{data.get('open', 0):.2f} -最高价:{data.get('high', 0):.2f} -最低价:{data.get('low', 0):.2f} -成交量:{data.get('vol', 0):.0f}手 -成交额:{data.get('amount', 0):.0f}千元 - """.strip() - - return { - "message": message, - "metadata": {"type": "quote", "data": data} - } - - def _format_technical(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化技术分析""" - indicators = data.get("indicators", {}) - message_parts = [f"【{data.get('stock_code', '')}】技术指标:\n"] - - if "ma" in indicators: - ma = indicators["ma"] - message_parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}") - - if "macd" in indicators: - macd = indicators["macd"] - message_parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}") - - if "rsi" in indicators: - rsi = indicators["rsi"] - message_parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}, RSI24={rsi.get('rsi24')}") - - return { - "message": "\n".join(message_parts), - "metadata": {"type": "technical", "data": data} - } - - def _format_fundamental(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化基本面""" - message = f""" -【{data.get('name', '股票')}】基本信息 -股票代码:{data.get('ts_code', '')} -所属地域:{data.get('area', '')} -所属行业:{data.get('industry', '')} -上市市场:{data.get('market', '')} -上市日期:{data.get('list_date', '')} - """.strip() - - return { - "message": message, - "metadata": {"type": "fundamental", "data": data} - } - - def _format_visualization(self, data: Dict[str, Any]) -> Dict[str, Any]: - """格式化可视化""" - return { - "message": f"已生成{data.get('stock_code', '')}的K线图", - "metadata": {"type": "chart", "data": data} - } - - -# 创建全局Agent实例 -enhanced_agent = EnhancedStockAgent() diff --git a/backend/app/agent/question_analyzer.py b/backend/app/agent/question_analyzer.py index e93ccf0..e8bf1ee 100644 --- a/backend/app/agent/question_analyzer.py +++ b/backend/app/agent/question_analyzer.py @@ -139,7 +139,7 @@ class QuestionAnalyzer: 请分析以下维度: 1. **问题类型** - - stock_analysis: 针对**特定单只股票**的分析(如"贵州茅台怎么样"、"分析比亚迪"、"AAPL走势") + - stock_analysis: 针对**特定单只股票**的分析(如"贵州茅台怎么样"、"分析比亚迪"、"AAPL走势"、"阿里巴巴美股") **注意**:如果用户问的是"板块"、"行业"、"概念股"等,这不是stock_analysis,而是market_overview - market_overview: 市场整体分析、行业板块分析、投资机会(如"最近有什么投资机会"、"商业航天板块怎么样"、"新能源行业走势"、"现在适合买股票吗") - knowledge: 金融知识问答(如"什么是MACD"、"如何看K线图") @@ -150,7 +150,34 @@ class QuestionAnalyzer: - 如果提到"板块"、"行业"、"概念"、"赛道"、"领域" → market_overview - 如果问"哪些股票"、"什么机会" → market_overview -2. **用户关注维度**(如果是stock_analysis) +2. **股票识别**(如果是stock_analysis,这是最重要的部分) + 请识别用户提到的股票,并返回准确的股票代码: + + **A股代码格式**:6位数字 + - 上海主板:600xxx、601xxx、603xxx、605xxx + - 深圳主板:000xxx、001xxx + - 创业板:300xxx、301xxx + - 科创板:688xxx + - 常见示例:贵州茅台→600519,比亚迪→002594,宁德时代→300750 + + **美股代码格式**:1-5位大写字母 + - 常见示例:苹果→AAPL,特斯拉→TSLA,微软→MSFT,谷歌→GOOGL + - 中概股美股:阿里巴巴美股→BABA,京东美股→JD,拼多多→PDD,百度美股→BIDU,网易美股→NTES,哔哩哔哩美股→BILI + + **港股代码格式**:4-5位数字加.HK后缀 + - 常见示例:腾讯→0700.HK,阿里巴巴港股→9988.HK,美团→3690.HK,小米→1810.HK,京东港股→9618.HK,百度港股→9888.HK,网易港股→9999.HK,哔哩哔哩港股→9626.HK + - 注意:港股代码需要包含.HK后缀 + + **市场判断**: + - 如果用户明确说"美股"、"纳斯达克"、"纽交所" → 美股 + - 如果用户明确说"港股"、"香港"、"恒生" → 港股 + - 对于同时在多地上市的公司(如阿里巴巴、京东、百度等): + - 用户说"美股"或没有明确指定 → 返回美股代码(如BABA) + - 用户说"港股" → 返回港股代码(如9988.HK) + - 纯港股公司(如腾讯、美团、小米)→ 港股 + - 默认情况下,中国公司优先考虑A股市场 + +3. **用户关注维度**(如果是stock_analysis) 分析用户想了解哪些方面: - price_trend: 价格走势、涨跌情况、最新价格 - technical: 技术指标(MACD、RSI、均线、KDJ等) @@ -159,28 +186,28 @@ class QuestionAnalyzer: - money_flow: 资金流向、主力动向、大单流入流出 - risk: 风险分析、风险提示、投资风险 -3. **时间范围** +4. **时间范围** - short_term: 短期(1-2周)- 如"短期走势"、"近期表现" - medium_term: 中期(1-3月)- 如"中期趋势"、"未来一个月" - long_term: 长期(半年以上)- 如"长期投资"、"适合长期持有吗" -4. **分析深度** +5. **分析深度** - quick: 快速查看(只需要基本信息,如"价格多少") - standard: 标准分析(常规分析,如"怎么样"、"分析一下") - deep: 深度分析(全面详细,如"全面分析"、"深度研究") -5. **特定关注点** +6. **特定关注点** 提取用户明确提到的关注点,如: - "支撑位在哪" - "盈利能力如何" - "适合长期持有吗" - "有没有金叉" -6. **上下文引用** +7. **上下文引用** - 是否引用了之前的对话("这只股票"、"它"、"那技术面呢") - 是否要求对比分析("和上次相比"、"对比一下") -7. **用户风格** +8. **用户风格** - tone: professional(专业,使用专业术语)/ casual(随意,通俗易懂) - detail_level: brief(简洁,简短回答)/ detailed(详细,详细分析) @@ -188,9 +215,9 @@ class QuestionAnalyzer: {{ "type": "问题类型", "target": {{ - "stock_code": "股票代码(如有,只返回纯数字代码,如600519或002594,不要包含市场标识)", - "stock_name": "股票名称(如有,只返回公司名称,如贵州茅台或比亚迪)", - "market": "A股/美股" + "stock_code": "股票代码(A股返回6位数字如600519,美股返回大写字母如BABA,港股返回带.HK后缀如0700.HK)", + "stock_name": "股票/公司名称(如贵州茅台、阿里巴巴、腾讯)", + "market": "A股/美股/港股" }}, "dimensions": {{ "price_trend": true/false, diff --git a/backend/app/agent/skill_planner.py b/backend/app/agent/skill_planner.py index 07342ec..bf7e63c 100644 --- a/backend/app/agent/skill_planner.py +++ b/backend/app/agent/skill_planner.py @@ -8,53 +8,85 @@ from app.utils.logger import logger class SkillPlanner: """智能技能规划器 - 根据问题意图动态选择技能""" - # 维度到技能的映射 - DIMENSION_SKILL_MAP = { + # A股维度到技能的映射 + A_STOCK_DIMENSION_SKILL_MAP = { 'price_trend': { - 'required': ['market_data', 'brave_search'], # brave_search 必需 + 'required': ['market_data', 'brave_search'], 'optional': [] }, 'technical': { - 'required': ['market_data', 'technical_analysis', 'brave_search'], # brave_search 必需 + 'required': ['market_data', 'technical_analysis', 'brave_search'], 'optional': ['visualization'] }, 'fundamental': { - 'required': ['fundamental', 'brave_search'], # brave_search 必需 + 'required': ['fundamental', 'brave_search'], 'optional': [] }, 'valuation': { - 'required': ['advanced_data', 'brave_search'], # brave_search 必需 + 'required': ['advanced_data', 'brave_search'], 'optional': [] }, 'money_flow': { - 'required': ['advanced_data', 'brave_search'], # brave_search 必需 + 'required': ['advanced_data', 'brave_search'], 'optional': [] }, 'risk': { - 'required': ['technical_analysis', 'advanced_data', 'brave_search'], # brave_search 必需 + 'required': ['technical_analysis', 'advanced_data', 'brave_search'], 'optional': [] }, - 'news': { # 新闻维度 + 'news': { 'required': ['brave_search'], 'optional': [] } } - # 技能依赖关系 + # 美股/港股维度到技能的映射(使用 yfinance) + INTL_STOCK_DIMENSION_SKILL_MAP = { + 'price_trend': { + 'required': ['us_stock_analysis', 'brave_search'], + 'optional': [] + }, + 'technical': { + 'required': ['us_stock_analysis', 'brave_search'], + 'optional': [] + }, + 'fundamental': { + 'required': ['us_stock_analysis', 'brave_search'], + 'optional': [] + }, + 'valuation': { + 'required': ['us_stock_analysis', 'brave_search'], + 'optional': [] + }, + 'money_flow': { + 'required': ['us_stock_analysis', 'brave_search'], + 'optional': [] + }, + 'risk': { + 'required': ['us_stock_analysis', 'brave_search'], + 'optional': [] + }, + 'news': { + 'required': ['brave_search'], + 'optional': [] + } + } + + # 技能依赖关系(仅 A 股) SKILL_DEPENDENCIES = { - 'technical_analysis': ['market_data'], # 技术分析依赖行情数据 - 'visualization': ['market_data'], # 可视化依赖行情数据 + 'technical_analysis': ['market_data'], + 'visualization': ['market_data'], } # 技能优先级(数字越小优先级越高) SKILL_PRIORITY = { - 'market_data': 1, # 最高优先级 + 'market_data': 1, 'fundamental': 1, - 'brave_search': 1, # 新闻搜索也是高优先级 + 'brave_search': 1, + 'us_stock_analysis': 1, 'technical_analysis': 2, 'advanced_data': 2, - 'visualization': 3, # 最低优先级 - 'us_stock_analysis': 1 + 'visualization': 3, } # 分析深度策略 @@ -70,7 +102,7 @@ class SkillPlanner: 'use_cache': True }, 'deep': { - 'max_skills': None, # 无限制 + 'max_skills': None, 'include_optional': True, 'use_cache': False } @@ -88,23 +120,27 @@ class SkillPlanner: intent: 问题意图(来自QuestionAnalyzer) Returns: - SkillExecutionPlan: { - 'skills': [ - { - 'name': 'market_data', - 'params': {...}, - 'priority': 1, - 'required': True, - 'reason': '用户关注价格走势' - }, - ... - ], - 'execution_strategy': 'parallel' | 'sequential', - 'cache_strategy': 'use' | 'bypass' - } + SkillExecutionPlan """ + # 获取市场类型 + target = intent.get('target', {}) + market = target.get('market', 'A股') + stock_code = target.get('stock_code', '') + stock_name = target.get('stock_name', '') + + # 根据市场类型选择不同的技能映射 + if market in ('美股', '港股'): + return self._plan_intl_stock_skills(intent, market, stock_code, stock_name) + else: + return self._plan_a_stock_skills(intent) + + def _plan_a_stock_skills(self, intent: Dict[str, Any]) -> Dict[str, Any]: + """规划 A 股技能""" # 1. 根据维度映射技能 - skills = self._map_dimensions_to_skills(intent.get('dimensions', {})) + skills = self._map_dimensions_to_skills( + intent.get('dimensions', {}), + self.A_STOCK_DIMENSION_SKILL_MAP + ) # 2. 根据分析深度调整 depth = intent.get('analysis_depth', 'standard') @@ -113,13 +149,11 @@ class SkillPlanner: # 3. 解析依赖关系 skills = self._resolve_dependencies(skills) - # 4. 去重 + # 4. 去重并排序 skills = list(set(skills)) - - # 5. 排序(按优先级) sorted_skills = self._sort_by_priority(skills) - # 6. 构建执行计划 + # 5. 构建执行计划 plan = { 'skills': [ { @@ -135,18 +169,77 @@ class SkillPlanner: 'cache_strategy': 'use' if self.DEPTH_STRATEGY[depth]['use_cache'] else 'bypass' } - logger.info(f"技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + logger.info(f"[A股] 技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") return plan - def _map_dimensions_to_skills(self, dimensions: Dict[str, bool]) -> List[str]: + def _plan_intl_stock_skills(self, intent: Dict[str, Any], market: str, stock_code: str, stock_name: str) -> Dict[str, Any]: + """规划美股/港股技能""" + # 1. 根据维度映射技能 + skills = self._map_dimensions_to_skills( + intent.get('dimensions', {}), + self.INTL_STOCK_DIMENSION_SKILL_MAP + ) + + # 2. 确保至少有 us_stock_analysis + if 'us_stock_analysis' not in skills: + skills.append('us_stock_analysis') + + # 3. 去重并排序 + skills = list(set(skills)) + sorted_skills = self._sort_by_priority(skills) + + # 4. 构建执行计划 + depth = intent.get('analysis_depth', 'standard') + plan = { + 'skills': [ + { + 'name': skill, + 'params': self._get_intl_skill_params(skill, stock_code, stock_name), + 'priority': self.SKILL_PRIORITY.get(skill, 5), + 'required': skill == 'us_stock_analysis', + 'reason': self._get_intl_skill_reason(skill, market) + } + for skill in sorted_skills + ], + 'execution_strategy': 'parallel', + 'cache_strategy': 'use' if self.DEPTH_STRATEGY[depth]['use_cache'] else 'bypass' + } + + logger.info(f"[{market}] 技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + return plan + + def _get_intl_skill_params(self, skill_name: str, stock_code: str, stock_name: str) -> Dict[str, Any]: + """获取美股/港股技能参数""" + if skill_name == 'us_stock_analysis': + return { + 'symbol': stock_code, + 'analysis_type': 'comprehensive' + } + elif skill_name == 'brave_search': + return { + 'query': f'{stock_name} 最新动态 财报', + 'search_type': 'news', + 'count': 5, + 'freshness': 'pw' + } + return {} + + def _get_intl_skill_reason(self, skill_name: str, market: str) -> str: + """获取美股/港股技能调用原因""" + if skill_name == 'us_stock_analysis': + return f'获取{market}基础数据和技术指标' + elif skill_name == 'brave_search': + return '获取最新市场资讯和舆情' + return '提供分析数据' + + def _map_dimensions_to_skills(self, dimensions: Dict[str, bool], skill_map: Dict) -> List[str]: """将用户关注维度映射到技能""" skills = [] for dimension, enabled in dimensions.items(): - if enabled and dimension in self.DIMENSION_SKILL_MAP: - mapping = self.DIMENSION_SKILL_MAP[dimension] + if enabled and dimension in skill_map: + mapping = skill_map[dimension] skills.extend(mapping['required']) - # 默认也添加可选技能(特别是 brave_search) skills.extend(mapping['optional']) return skills diff --git a/backend/app/agent/smart_agent.py b/backend/app/agent/smart_agent.py index 114d54c..9062f3c 100644 --- a/backend/app/agent/smart_agent.py +++ b/backend/app/agent/smart_agent.py @@ -1393,42 +1393,11 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} if keyword.isupper() and keyword.isalpha() and len(keyword) <= 5: return True - # 常见美股公司名称映射 - us_stock_names = { - "苹果": "AAPL", - "特斯拉": "TSLA", - "微软": "MSFT", - "谷歌": "GOOGL", - "亚马逊": "AMZN", - "Meta": "META", - "脸书": "META", - "英伟达": "NVDA", - "奈飞": "NFLX", - "网飞": "NFLX", - "迪士尼": "DIS", - "可口可乐": "KO", - "麦当劳": "MCD", - "星巴克": "SBUX", - "耐克": "NKE", - "波音": "BA", - "英特尔": "INTC", - "AMD": "AMD", - "高通": "QCOM", - "推特": "TWTR", - "优步": "UBER", - "Uber": "UBER", - "Airbnb": "ABNB", - "爱彼迎": "ABNB", - } - - if keyword in us_stock_names: - return True - return False def _get_us_stock_symbol(self, keyword: str) -> str: """ - 获取美股代码 + 获取美股代码(同步版本,用于兼容) Args: keyword: 股票关键词 @@ -1440,51 +1409,45 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} if keyword.isupper() and keyword.isalpha(): return keyword - # 中文名称映射 - us_stock_names = { - "苹果": "AAPL", - "特斯拉": "TSLA", - "微软": "MSFT", - "谷歌": "GOOGL", - "亚马逊": "AMZN", - "Meta": "META", - "脸书": "META", - "英伟达": "NVDA", - "奈飞": "NFLX", - "网飞": "NFLX", - "迪士尼": "DIS", - "可口可乐": "KO", - "麦当劳": "MCD", - "星巴克": "SBUX", - "耐克": "NKE", - "波音": "BA", - "英特尔": "INTC", - "AMD": "AMD", - "高通": "QCOM", - "推特": "TWTR", - "优步": "UBER", - "Uber": "UBER", - "Airbnb": "ABNB", - "爱彼迎": "ABNB", - } - - return us_stock_names.get(keyword, keyword.upper()) + # 默认返回大写形式 + return keyword.upper() async def _handle_us_stock(self, keyword: str, message: str) -> Dict[str, Any]: """ - 处理美股查询(使用 skill_planner) + 处理美股查询(兼容旧接口,内部调用 _handle_us_stock_with_code) Args: - keyword: 股票关键词 + keyword: 股票关键词(可能是代码或名称) message: 用户消息 Returns: 分析结果 """ - # 获取美股代码 - symbol = self._get_us_stock_symbol(keyword) + # 如果是大写字母,直接作为代码使用 + if keyword.isupper() and keyword.isalpha() and len(keyword) <= 5: + return await self._handle_us_stock_with_code(keyword, keyword, message) - logger.info(f"处理美股查询: {keyword} -> {symbol}") + # 否则需要通过 QuestionAnalyzer 重新分析获取代码 + # 这种情况理论上不应该发生,因为 QuestionAnalyzer 应该已经返回了代码 + logger.warning(f"_handle_us_stock 收到非代码格式的关键词: {keyword}") + return { + "message": f"抱歉,无法识别美股 \"{keyword}\"。请直接输入美股代码(如 BABA、AAPL、TSLA)进行查询。", + "metadata": {"type": "error"} + } + + async def _handle_us_stock_with_code(self, symbol: str, stock_name: str, message: str) -> Dict[str, Any]: + """ + 处理美股查询(使用已知的股票代码) + + Args: + symbol: 美股代码(如 BABA、AAPL) + stock_name: 股票名称 + message: 用户消息 + + Returns: + 分析结果 + """ + logger.info(f"处理美股查询: {stock_name} -> {symbol}") try: # 1. 使用 QuestionAnalyzer 分析问题意图 @@ -1498,28 +1461,17 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} if 'target' not in intent: intent['target'] = {} intent['target']['stock_code'] = symbol - intent['target']['stock_name'] = keyword + intent['target']['stock_name'] = stock_name intent['target']['market'] = '美股' logger.info(f"美股问题意图分析: dimensions={intent.get('dimensions')}") - # 2. 使用 SkillPlanner 规划技能(包括 brave_search) + # 2. 使用 SkillPlanner 规划技能(会自动识别美股并使用正确的技能) 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. 执行技能规划 + # 3. 执行技能规划 execution_results = await skill_manager.execute_plan( plan=plan, stock_code=symbol @@ -1539,7 +1491,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} # 6. 整合数据 all_data = { "symbol": symbol, - "name": keyword, + "name": stock_name, **us_stock_data, "news": execution_results['results'].get("brave_search") # 新增:新闻数据 } @@ -1566,6 +1518,162 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} "metadata": {"type": "error"} } + async def _handle_hk_stock_with_code(self, symbol: str, stock_name: str, message: str) -> Dict[str, Any]: + """ + 处理港股查询(使用已知的股票代码) + + Args: + symbol: 港股代码(如 0700.HK、9988.HK) + stock_name: 股票名称 + message: 用户消息 + + Returns: + 分析结果 + """ + logger.info(f"处理港股查询: {stock_name} -> {symbol}") + + 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'] = symbol + intent['target']['stock_name'] = stock_name + intent['target']['market'] = '港股' + + logger.info(f"港股问题意图分析: dimensions={intent.get('dimensions')}") + + # 2. 使用 SkillPlanner 规划技能(会自动识别港股并使用正确的技能) + 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=symbol + ) + + if execution_results['errors']: + logger.warning(f"港股技能执行有错误: {execution_results['errors']}") + + # 5. 检查 us_stock_analysis 是否成功 + hk_stock_data = execution_results['results'].get('us_stock_analysis') + if not hk_stock_data or 'error' in hk_stock_data: + return { + "message": f"抱歉,未找到港股 {symbol}。请确认股票代码是否正确。\n\n提示:港股代码格式为数字加.HK后缀,如 0700.HK(腾讯)、9988.HK(阿里巴巴)等。", + "metadata": {"type": "error"} + } + + # 6. 整合数据 + all_data = { + "symbol": symbol, + "name": stock_name, + "market": "港股", + **hk_stock_data, + "news": execution_results['results'].get("brave_search") + } + + # 7. 使用LLM分析港股数据 + if self.use_llm: + analysis = await self._llm_hk_stock_analysis(all_data, message) + else: + analysis = self._format_us_stock_data(all_data) + + return { + "message": analysis, + "metadata": { + "type": "hk_stock_analysis", + "data": all_data, + "plan": plan + } + } + + except Exception as e: + logger.error(f"港股查询失败: {e}") + return { + "message": f"查询港股 {symbol} 时出错:{str(e)}", + "metadata": {"type": "error"} + } + + async def _llm_hk_stock_analysis(self, data: Dict[str, Any], user_message: str) -> str: + """使用LLM分析港股数据""" + from datetime import datetime + current_time = datetime.now().strftime("%Y-%m-%d %H:%M") + + # 提取关键数据(使用 us_stock_service 返回的扁平结构) + symbol = data.get('symbol', '') + name = data.get('name', symbol) + technical = data.get('technical_indicators', {}) + news = data.get('news', []) + + # 构建数据摘要 + data_summary = f""" +【港股数据】{name}({symbol}) +查询时间:{current_time} + +【基本信息】 +- 公司名称:{name} +- 行业:{data.get('industry', '未知')} +- 板块:{data.get('sector', '未知')} +- 市值:{data.get('market_cap', '未知')} + +【最新行情】 +- 当前价格:{data.get('current_price', '未知')} +- 今日涨跌:{data.get('change_percent', '未知')}% +- 52周最高:{data.get('52_week_high', '未知')} +- 52周最低:{data.get('52_week_low', '未知')} + +【估值指标】 +- 市盈率(PE):{data.get('pe_ratio', '未知')} +- 市净率(PB):{data.get('pb_ratio', '未知')} +- 股息率:{data.get('dividend_yield', '未知')} + +【技术指标】 +- MA5:{technical.get('ma5', '未知')} +- MA20:{technical.get('ma20', '未知')} +- RSI:{technical.get('rsi', '未知')} +- MACD:{technical.get('macd', '未知')} +""" + + # 添加新闻摘要 + if news: + data_summary += "\n【相关新闻】\n" + for i, item in enumerate(news[:3], 1): + if isinstance(item, dict): + title = item.get('title', '') + data_summary += f"{i}. {title}\n" + + prompt = f"""你是一个专业的港股分析师。请根据以下数据,回答用户的问题。 + +{data_summary} + +用户问题:{user_message} + +请提供专业、客观的分析,包括: +1. 直接回答用户的问题 +2. 基于数据的分析和判断 +3. 潜在的风险提示 + +注意:港股以港币计价,交易时间为港交所交易时段。""" + + try: + result = await self._call_llm_async( + messages=[{"role": "user", "content": prompt}], + temperature=0.7, + max_tokens=2000 + ) + return result or self._format_us_stock_data(data) + except Exception as e: + logger.error(f"LLM港股分析失败: {e}") + return self._format_us_stock_data(data) + async def _llm_us_stock_analysis(self, data: Dict[str, Any], user_message: str) -> str: """使用LLM分析美股数据""" from datetime import datetime @@ -1911,9 +2019,20 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} 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}") + """流式处理美股分析(兼容旧接口)""" + # 如果是大写字母,直接作为代码使用 + if keyword.isupper() and keyword.isalpha() and len(keyword) <= 5: + async for chunk in self._handle_us_stock_stream_with_code(keyword, keyword, message): + yield chunk + return + + # 否则报错 + logger.warning(f"_handle_us_stock_stream 收到非代码格式的关键词: {keyword}") + yield f"抱歉,无法识别美股 \"{keyword}\"。请直接输入美股代码(如 BABA、AAPL、TSLA)进行查询。" + + async def _handle_us_stock_stream_with_code(self, symbol: str, stock_name: str, message: str): + """流式处理美股分析(使用已知的股票代码)""" + logger.info(f"[智能模式-流式] 美股查询: {stock_name} -> {symbol}") try: # 1. 使用 QuestionAnalyzer 分析问题意图 @@ -1927,28 +2046,17 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} if 'target' not in intent: intent['target'] = {} intent['target']['stock_code'] = symbol - intent['target']['stock_name'] = keyword + intent['target']['stock_name'] = stock_name intent['target']['market'] = '美股' logger.info(f"[流式] 美股问题意图分析: dimensions={intent.get('dimensions')}") - # 2. 使用 SkillPlanner 规划技能(包括 brave_search) + # 2. 使用 SkillPlanner 规划技能(会自动识别美股并使用正确的技能) 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. 执行技能规划 + # 3. 执行技能规划 execution_results = await skill_manager.execute_plan( plan=plan, stock_code=symbol @@ -1966,7 +2074,7 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} # 6. 整合数据 all_data = { "symbol": symbol, - "name": keyword, + "name": stock_name, **us_stock_data, "news": execution_results['results'].get("brave_search") # 新增:新闻数据 } @@ -1994,6 +2102,147 @@ RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} logger.error(traceback.format_exc()) yield f"查询美股 {symbol} 时出错:{str(e)}" + async def _handle_hk_stock_stream_with_code(self, symbol: str, stock_name: str, message: str): + """流式处理港股分析(使用已知的股票代码)""" + logger.info(f"[智能模式-流式] 港股查询: {stock_name} -> {symbol}") + + 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'] = symbol + intent['target']['stock_name'] = stock_name + intent['target']['market'] = '港股' + + logger.info(f"[流式] 港股问题意图分析: dimensions={intent.get('dimensions')}") + + # 2. 使用 SkillPlanner 规划技能(会自动识别港股并使用正确的技能) + plan = self.skill_planner.plan_skills(intent) + + logger.info(f"[流式] 港股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}") + + 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 是否成功 + hk_stock_data = execution_results['results'].get('us_stock_analysis') + if not hk_stock_data or 'error' in hk_stock_data: + yield f"抱歉,未找到港股 {symbol}。请确认股票代码是否正确。\n\n提示:港股代码格式为数字加.HK后缀,如 0700.HK(腾讯)、9988.HK(阿里巴巴)等。" + return + + # 6. 整合数据 + all_data = { + "symbol": symbol, + "name": stock_name, + "market": "港股", + **hk_stock_data, + "news": execution_results['results'].get("brave_search") + } + + # 7. 使用智能模式的动态prompt生成 + if self.use_llm: + # 构建港股数据的动态prompt + prompt = self._build_hk_stock_dynamic_prompt(all_data, symbol, message) + + # 流式生成 + stream = llm_service.chat_stream( + messages=[{"role": "user", "content": prompt}], + temperature=0.7, + max_tokens=2500 + ) + + for chunk in stream: + yield chunk + else: + 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 _build_hk_stock_dynamic_prompt(self, data: Dict[str, Any], symbol: str, user_message: str) -> str: + """构建港股分析的动态prompt""" + from datetime import datetime + current_time = datetime.now().strftime("%Y-%m-%d %H:%M") + + # 提取关键数据(使用 us_stock_service 返回的扁平结构) + name = data.get('name', symbol) + technical = data.get('technical_indicators', {}) + news = data.get('news', []) + + # 构建数据摘要 + data_summary = f""" +【港股数据】{name}({symbol}) +查询时间:{current_time} + +【基本信息】 +- 公司名称:{name} +- 行业:{data.get('industry', '未知')} +- 板块:{data.get('sector', '未知')} +- 市值:{data.get('market_cap', '未知')} + +【最新行情】 +- 当前价格:{data.get('current_price', '未知')} +- 今日涨跌:{data.get('change_percent', '未知')}% +- 52周最高:{data.get('52_week_high', '未知')} +- 52周最低:{data.get('52_week_low', '未知')} + +【估值指标】 +- 市盈率(PE):{data.get('pe_ratio', '未知')} +- 市净率(PB):{data.get('pb_ratio', '未知')} +- 股息率:{data.get('dividend_yield', '未知')} + +【技术指标】 +- MA5:{technical.get('ma5', '未知')} +- MA20:{technical.get('ma20', '未知')} +- RSI:{technical.get('rsi', '未知')} +- MACD:{technical.get('macd', '未知')} +""" + + # 添加新闻摘要 + if news: + data_summary += "\n【相关新闻】\n" + if isinstance(news, dict) and 'results' in news: + news_list = news.get('results', []) + else: + news_list = news if isinstance(news, list) else [] + for i, item in enumerate(news_list[:3], 1): + if isinstance(item, dict): + title = item.get('title', '') + data_summary += f"{i}. {title}\n" + + prompt = f"""你是一个专业的港股分析师。请根据以下数据,回答用户的问题。 + +{data_summary} + +用户问题:{user_message} + +请提供专业、客观的分析,包括: +1. 直接回答用户的问题 +2. 基于数据的分析和判断 +3. 潜在的风险提示 + +注意:港股以港币(HKD)计价,交易时间为港交所交易时段(北京时间9:30-12:00, 13:00-16:00)。""" + + return prompt + def _format_news_section(self, news_data: Dict[str, Any]) -> str: """ 格式化新闻数据为统一格式 @@ -2364,31 +2613,22 @@ MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中' stock_name = target.get('stock_name') market = target.get('market', 'A股') - # 如果没有股票代码,尝试匹配 - if not stock_code and stock_name: - # 检测是否为美股 - is_us_stock = self._is_us_stock(stock_name, market) - - if is_us_stock: - return await self._handle_us_stock(stock_name, message) - - # A股匹配 - stock_info = await self._match_stock_with_llm(stock_name) - if not stock_info: - return { - "message": f"抱歉,未找到股票\"{stock_name}\"。请确认名称或代码是否正确。", - "metadata": {"type": "error"} - } - - stock_code = stock_info['code'] - stock_name = stock_info['name'] - + # QuestionAnalyzer 应该已经返回了股票代码 if not stock_code: return { - "message": "抱歉,我没有识别到您提到的股票。请提供更明确的股票代码或名称。", + "message": f"抱歉,我没有识别到您提到的股票「{stock_name or ''}」。请提供更明确的股票代码或名称。", "metadata": {"type": "error"} } + # 根据市场类型处理 + if market == '美股': + # 美股处理 + return await self._handle_us_stock_with_code(stock_code, stock_name or stock_code, message) + + if market == '港股': + # 港股处理(使用 yfinance,与美股类似) + return await self._handle_hk_stock_with_code(stock_code, stock_name or stock_code, message) + logger.info(f"[智能模式] 分析股票: {stock_name}({stock_code})") # 1. 技能规划 @@ -2665,30 +2905,25 @@ MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中' stock_name = target.get('stock_name') market = target.get('market', 'A股') - # 检测是否为美股 - is_us_stock = market == '美股' or self._is_us_stock(stock_name or stock_code or '', market) + # QuestionAnalyzer 应该已经返回了股票代码 + if not stock_code: + yield f"抱歉,我没有识别到您提到的股票「{stock_name or ''}」。请提供更明确的股票代码或名称。" + return - # 如果是美股,直接使用美股处理流程 - if is_us_stock: - async for chunk in self._handle_us_stock_stream(stock_name or stock_code, message): + # 根据市场类型处理 + if market == '美股': + # 美股处理流程 + async for chunk in self._handle_us_stock_stream_with_code(stock_code, stock_name or stock_code, message): + yield chunk + return + + if market == '港股': + # 港股处理流程(使用 yfinance,与美股类似) + async for chunk in self._handle_hk_stock_stream_with_code(stock_code, stock_name or stock_code, message): yield chunk return # A股处理流程 - # 如果没有股票代码,尝试匹配 - if not stock_code and stock_name: - stock_info = await self._match_stock_with_llm(stock_name) - if not stock_info: - yield f"抱歉,未找到股票\"{stock_name}\"。请确认名称或代码是否正确。" - return - - stock_code = stock_info['code'] - stock_name = stock_info['name'] - - if not stock_code: - yield "抱歉,我没有识别到您提到的股票。请提供更明确的股票代码或名称。" - return - logger.info(f"[智能模式-流式] 分析股票: {stock_name}({stock_code})") # 1. 技能规划 diff --git a/backend/app/agent/smart_agent.py.bak b/backend/app/agent/smart_agent.py.bak deleted file mode 100644 index de9b0dc..0000000 --- a/backend/app/agent/smart_agent.py.bak +++ /dev/null @@ -1,2126 +0,0 @@ -""" -智能Agent - 真正使用LLM进行全面分析 -""" -import re -import json -import asyncio -from typing import Dict, Any, Optional, List -from app.config import get_settings -from app.agent.context import ContextManager -from app.agent.skill_manager import skill_manager -from app.skills.market_data import MarketDataSkill -from app.skills.technical_analysis import TechnicalAnalysisSkill -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.services.llm_service import llm_service -from app.services.tushare_service import tushare_service -from app.utils.logger import logger - - -class SmartStockAgent: - """智能股票分析Agent - 深度集成LLM""" - - def __init__(self): - """初始化Agent""" - self.context_manager = ContextManager() - self.settings = get_settings() - - # 注册技能 - self._register_skills() - - # 检查LLM是否可用 - self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None - - if self.use_llm: - logger.info("Smart Agent初始化完成(LLM深度集成模式 + Tushare Pro高级数据)") - else: - logger.warning("Smart Agent初始化完成(规则模式,建议配置LLM)") - - async def _call_llm_async(self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 2000) -> Optional[str]: - """异步调用LLM,避免阻塞事件循环""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor( - None, - lambda: llm_service.chat(messages, temperature, max_tokens) - ) - - def _register_skills(self): - """注册所有技能""" - skill_manager.register(MarketDataSkill()) - skill_manager.register(TechnicalAnalysisSkill()) - skill_manager.register(FundamentalSkill()) - skill_manager.register(VisualizationSkill()) - skill_manager.register(AdvancedDataSkill()) - skill_manager.register(USStockSkill()) - logger.info("技能注册完成(Tushare Pro高级数据 + 美股支持)") - - async def process_message( - self, - message: str, - session_id: str, - user_id: Optional[str] = None - ) -> Dict[str, Any]: - """ - 处理用户消息(智能版) - - Args: - message: 用户消息 - session_id: 会话ID - user_id: 用户ID - - Returns: - 响应结果 - """ - logger.info(f"处理消息: {message[:50]}...") - - # 保存用户消息 - self.context_manager.add_message(session_id, "user", message) - - # 第一步:使用LLM理解问题意图 - intent_analysis = await self._analyze_question_intent(message, session_id) - - if not intent_analysis: - # 不直接说"无法理解",而是引导用户 - response = { - "message": """我是您的金融智能助手,可以帮您: - -📊 **股票分析** - 分析个股走势、技术指标、基本面 -📈 **市场观察** - 解读大盘走势、行业热点 -📚 **知识问答** - 解答金融投资相关问题 - -您可以这样问我: -• "分析一下贵州茅台" -• "现在A股市场怎么样" -• "什么是MACD指标" - -请告诉我您想了解什么?""", - "metadata": {"type": "guide"} - } - self.context_manager.add_message(session_id, "assistant", response["message"]) - return response - - # 第二步:根据意图类型处理 - question_type = intent_analysis['type'] - - if question_type == 'stock_specific': - # 针对特定股票的问题 - response = await self._handle_stock_question(intent_analysis, message) - elif question_type == 'macro_finance': - # 宏观金融问题 - response = await self._handle_macro_question(intent_analysis, message) - elif question_type == 'knowledge': - # 金融知识问答 - response = await self._handle_knowledge_question(intent_analysis, message) - elif question_type == 'general_chat': - # 一般对话,引导用户 - response = await self._handle_general_chat(intent_analysis, message) - else: - # 未知类型,智能引导 - response = { - "message": f"""我理解您想了解:{intent_analysis.get('description', '相关信息')} - -作为金融智能助手,我擅长: -• 📊 分析具体股票(如"分析比亚迪") -• 📈 解读市场走势(如"现在大盘怎么样") -• 📚 解答金融知识(如"什么是市盈率") - -能否更具体地告诉我您想了解什么?""", - "metadata": {"type": "guide"} - } - - # 保存助手响应 - self.context_manager.add_message( - session_id, - "assistant", - response["message"], - metadata=response.get("metadata") - ) - - return response - - def _is_comprehensive_analysis(self, message: str) -> bool: - """ - 判断是否需要全面分析 - - 默认情况下,如果用户只是简单提到股票名称或代码,就进行全面分析 - 只有明确要求特定信息时(如"技术指标"、"K线图"等),才做单一查询 - """ - # 明确要求单一查询的关键词 - single_query_keywords = [ - "k线", "图表", "走势图", "kline", - "技术指标", "macd", "rsi", "均线", "kdj", - "基本面", "公司信息", "行业", - "实时行情", "价格", "涨跌" - ] - - # 如果明确要求单一查询,返回False - if any(keyword in message.lower() for keyword in single_query_keywords): - return False - - # 默认进行全面分析 - return True - - async def _comprehensive_analysis( - self, - stock_code: str, - stock_name: Optional[str], - message: str - ) -> Dict[str, Any]: - """ - 全面分析:整合多个数据源 + LLM深度分析 - - Args: - stock_code: 股票代码 - stock_name: 股票名称 - message: 用户消息 - - Returns: - 综合分析结果 - """ - logger.info(f"执行全面分析: {stock_code}") - - 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" - ) - - # 获取技术指标 - 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 - ) - - # 获取高级数据(Tushare Pro 5000+积分) - advanced_result = await skill_manager.execute_skill( - "advanced_data", - stock_code=stock_code, - data_type="all" - ) - - # 整合数据 - 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 - } - - # 2. 使用LLM进行深度分析 - if self.use_llm: - analysis = await self._llm_comprehensive_analysis(all_data, message) - else: - analysis = self._rule_based_analysis(all_data) - - return { - "message": analysis, - "metadata": { - "type": "comprehensive", - "data": all_data - } - } - - except Exception as e: - logger.error(f"全面分析失败: {e}") - return { - "message": f"分析{display_name}时出错:{str(e)}", - "metadata": {"type": "error"} - } - - async def _llm_comprehensive_analysis( - self, - data: Dict[str, Any], - user_message: str - ) -> str: - """使用LLM进行深度综合分析""" - - # 获取当前时间 - from datetime import datetime - current_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 获取行情数据的交易日期 - 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" - - # 重大公告 - if advanced_data.get('announcements') and len(advanced_data['announcements']) > 0: - advanced_summary += "最近公告:\n" - for ann in advanced_data['announcements'][:3]: - advanced_summary += f" · {ann.get('title', '无标题')} ({ann.get('ann_date', '')})\n" - advanced_summary += "\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} - -请按以下结构进行分析,并在每个部分明确标注数据来源和时效性: - -## 一、基本面分析 -分段说明公司情况,每个要点独立成段: -- 第一段:公司主营业务和行业地位 -- 第二段:财务健康度分析(基于ROE、资产负债率、流动比率等财务指标) -- 第三段:估值水平分析(PE、PB、PS是否合理) -- 第四段:所属行业发展前景 -- 第五段:如果有公告,简要分析对公司的影响 - -## 二、技术面分析(数据截止:{quote_date}) -使用清晰的分段结构,每个技术指标独立成段: - -**价格走势** -当前价格走势特征(上涨/下跌/震荡),结合成交量分析。 - -**均线系统** -短期均线(MA5、MA10)与长期均线(MA20、MA60)的位置关系,判断当前趋势(多头/空头/震荡)。 - -**MACD指标** -DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信号。 - -**RSI指标** -当前RSI值的位置,是否超买(>70)或超卖(<30),短期走势预判。 - -**支撑与压力** -关键支撑位和压力位的具体价格区间。 - -## 三、市场情绪分析 -分段分析市场情绪: -- 第一段:资金流向分析(主力资金、大单资金流入/流出情况) -- 第二段:融资融券情况(如有) -- 第三段:当前市场情绪(乐观/谨慎/悲观)及原因 -- 第四段:短期可能的催化因素 - -## 四、投资建议 -基于技术面分析,给出具体的操作建议和点位: - -**短期(1-2周)操作建议** -- 明确的操作建议:买入/持有/观望/减仓 -- **具体点位建议**: - - 如果建议买入:给出建议买入价格区间(基于支撑位) - - 如果建议卖出:给出建议卖出价格区间(基于压力位) - - 止损位:明确的止损价格点位 - - 止盈位:明确的止盈价格点位 -- 操作理由:基于技术指标的具体分析 - -**中期(1-3个月)策略** -- 趋势判断(上涨/下跌/震荡) -- 关键价格区间: - - 上方目标位:具体价格 - - 下方支撑位:具体价格 -- 策略建议 - -**长期(半年以上)** -投资价值评估和长期持有建议。 - -**风险提示** -- 主要风险点和注意事项 -- 需要关注的关键价格位 - -## 五、总结 -用一句话概括核心观点。 - ---- -**数据说明** -- 行情数据来源:Tushare Pro(截止{quote_date}) -- 技术指标:基于历史K线数据计算(截止{quote_date}) -- 财务数据:Tushare Pro 5000+积分接口(利润表、资产负债表、财务指标) -- 估值数据:Tushare Pro(PE、PB、PS、市值等) -- 资金流向:Tushare Pro(主力资金、大单资金) -- 融资融券:Tushare Pro(如有) -- 公告数据:Tushare Pro(重大公告) - -写作要求: -1. 语言简洁专业,避免过度修饰和比喻 -2. 专业术语后用括号简单解释,例如"RSI超买(指标>70,股价可能回调)" -3. **重要:每个分析点必须独立成段,段落之间用空行分隔** -4. **技术面分析部分,每个指标必须使用加粗标题(**标题**)并独立成段** -5. 分析要客观理性,基于数据而非情绪 -6. 充分利用提供的财务数据、估值数据、资金流向等高级数据进行分析 -7. 结论要明确,不要模棱两可 -8. 控制在800-1000字(由于数据更丰富,可以写得更详细) -9. 最后必须声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - try: - analysis = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=3000 # 增加到3000,因为分析更详细了 - ) - - if analysis: - return f"【{data['stock_name']}({data['stock_code']}) - AI深度分析】\n\n{analysis}" - else: - return self._rule_based_analysis(data) - - except Exception as e: - logger.error(f"LLM分析失败: {e}") - return self._rule_based_analysis(data) - - async def _llm_single_analysis( - self, - intent: Dict[str, Any], - result: Dict[str, Any], - stock_code: str, - stock_name: Optional[str], - user_message: str - ) -> Dict[str, Any]: - """使用LLM对单一查询进行分析""" - data = result.get("data", result) - display_name = stock_name or stock_code - - # 根据查询类型构建不同的prompt - if intent["type"] == "technical": - prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的技术指标。 - -用户问题:{user_message} - -【技术指标数据】 -{json.dumps(data, ensure_ascii=False, indent=2)} - -请进行专业的技术分析: - -## 技术指标解读 -1. 均线系统分析: - - 短期均线(MA5、MA10)与长期均线(MA20、MA60)的位置关系 - - 判断当前趋势(多头/空头/震荡) - -2. MACD指标分析: - - DIF和DEA的位置关系 - - MACD柱状图的变化趋势 - - 判断动能强弱 - -3. RSI指标分析: - - 当前RSI值的位置(超买/超卖/中性) - - 短期可能的走势 - -4. KDJ指标分析(如有): - - K、D、J值的位置关系 - - 金叉/死叉信号 - -## 综合判断 -- 短期走势预判(1-2周) -- 关键支撑位和压力位 -- 操作建议(买入/持有/观望/减仓) - -## 风险提示 -- 主要技术风险点 - -写作要求: -1. 语言简洁专业,直接给出分析结论 -2. 基于数据进行分析,不要编造 -3. 控制在300-400字 -4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - elif intent["type"] == "quote": - prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的实时行情。 - -用户问题:{user_message} - -【实时行情数据】 -{json.dumps(data, ensure_ascii=False, indent=2)} - -请进行专业的行情分析: - -## 行情解读 -1. 当日表现: - - 涨跌幅分析 - - 成交量分析 - - 振幅分析 - -2. 价格位置: - - 当前价格相对开盘价、最高价、最低价的位置 - - 判断多空力量对比 - -3. 短期判断: - - 当日走势特征 - - 短期可能的走势 - - 操作建议 - -写作要求: -1. 语言简洁专业,直接给出分析结论 -2. 基于数据进行分析,不要编造 -3. 控制在200-300字 -4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - elif intent["type"] == "fundamental": - prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的基本面信息。 - -用户问题:{user_message} - -【基本面数据】 -{json.dumps(data, ensure_ascii=False, indent=2)} - -请进行专业的基本面分析: - -## 公司概况 -- 公司主营业务 -- 所属行业和地域 -- 上市时间和市场 - -## 行业分析 -- 所属行业的发展前景 -- 行业地位和竞争优势 - -## 投资价值 -- 基本面评估 -- 长期投资价值 -- 关注要点 - -写作要求: -1. 语言简洁专业,直接给出分析结论 -2. 基于数据进行分析,不要编造 -3. 控制在200-300字 -4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - else: - # 其他类型,使用通用分析 - prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的相关信息。 - -用户问题:{user_message} - -【数据】 -{json.dumps(data, ensure_ascii=False, indent=2)} - -请基于提供的数据进行专业分析,给出有价值的见解和建议。 - -写作要求: -1. 语言简洁专业,直接给出分析结论 -2. 基于数据进行分析,不要编造 -3. 控制在200-300字 -4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - try: - analysis = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=1500 - ) - - if analysis: - return { - "message": f"【{display_name}({stock_code}) - AI分析】\n\n{analysis}", - "metadata": {"type": intent["type"], "data": data} - } - else: - # LLM失败,使用原始格式化 - return self._format_response(intent, result, stock_code, stock_name) - - except Exception as e: - logger.error(f"LLM单一分析失败: {e}") - return self._format_response(intent, result, stock_code, stock_name) - - async def _comprehensive_index_analysis( - self, - index_code: str, - index_name: str, - message: str - ) -> Dict[str, Any]: - """ - 指数全面分析:使用Tushare获取指数数据 + LLM深度分析 - - Args: - index_code: 指数代码(如000001.SH) - index_name: 指数名称 - message: 用户消息 - - Returns: - 综合分析结果 - """ - logger.info(f"执行指数全面分析: {index_code}") - - try: - # 从tushare_advanced_service获取指数数据 - from app.services.tushare_advanced_service import tushare_advanced_service - - # 获取指数日线数据 - index_data = tushare_advanced_service.get_index_daily( - ts_code=index_code, - start_date=None, # 默认180天 - end_date=None - ) - - if not index_data or len(index_data) == 0: - return { - "message": f"抱歉,未能获取{index_name}的数据。", - "metadata": {"type": "error"} - } - - # 获取最新数据 - latest = index_data[-1] - - # 计算技术指标(简单版) - closes = [d['close'] for d in index_data] - - # 计算均线 - ma5 = sum(closes[-5:]) / 5 if len(closes) >= 5 else None - ma10 = sum(closes[-10:]) / 10 if len(closes) >= 10 else None - ma20 = sum(closes[-20:]) / 20 if len(closes) >= 20 else None - ma60 = sum(closes[-60:]) / 60 if len(closes) >= 60 else None - - # 整合数据 - all_data = { - "index_code": index_code, - "index_name": index_name, - "latest": latest, - "ma": { - "ma5": ma5, - "ma10": ma10, - "ma20": ma20, - "ma60": ma60 - }, - "history_days": len(index_data) - } - - # 使用LLM进行深度分析 - if self.use_llm: - analysis = await self._llm_index_analysis(all_data, message) - else: - analysis = f"【{index_name}】最新数据\n\n" \ - f"日期:{latest['trade_date']}\n" \ - f"收盘:{latest['close']:.2f}\n" \ - f"涨跌幅:{latest['pct_chg']:+.2f}%\n" \ - f"成交量:{latest['vol']:.0f}手" - - return { - "message": analysis, - "metadata": { - "type": "index_analysis", - "data": all_data - } - } - - except Exception as e: - logger.error(f"指数分析失败: {e}") - import traceback - logger.error(traceback.format_exc()) - return { - "message": f"分析{index_name}时出错:{str(e)}", - "metadata": {"type": "error"} - } - - async def _llm_index_analysis( - self, - data: Dict[str, Any], - user_message: str - ) -> str: - """使用LLM进行指数深度分析""" - - latest = data['latest'] - ma = data['ma'] - - prompt = f"""你是一位专业的股票分析师。请对{data['index_name']}({data['index_code']})进行全面分析,用简洁专业但易懂的语言回答。 - -用户问题:{user_message} - -【指数最新数据】 -数据来源:Tushare Pro API -交易日期:{latest['trade_date']} -收盘点位:{latest['close']:.2f} -开盘点位:{latest['open']:.2f} -最高点位:{latest['high']:.2f} -最低点位:{latest['low']:.2f} -涨跌幅:{latest['pct_chg']:+.2f}% -涨跌点数:{latest['change']:+.2f} -成交量:{latest['vol']:.0f}手 -成交额:{latest['amount']:.0f}千元 - -【技术指标】 -MA5:{f"{ma['ma5']:.2f}" if ma['ma5'] else '计算中'} -MA10:{f"{ma['ma10']:.2f}" if ma['ma10'] else '计算中'} -MA20:{f"{ma['ma20']:.2f}" if ma['ma20'] else '计算中'} -MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} -当前点位:{latest['close']:.2f} - -请按以下结构进行分析: - -## 一、市场现状 -- 当前点位分析(相对历史位置) -- 当日表现(涨跌幅、成交量) - -## 二、技术面分析 -**均线系统** -分析当前点位与各均线的关系,判断趋势(多头/空头/震荡)。 - -**支撑与压力** -基于近期走势,给出关键支撑位和压力位。 - -## 三、市场情绪 -- 当前市场情绪(乐观/谨慎/悲观) -- 成交量分析 - -## 四、投资建议 -**短期(1-2周)** -- 趋势判断 -- 关键点位(支撑位、压力位) -- 操作建议 - -**中期(1-3个月)** -- 趋势展望 -- 策略建议 - -**风险提示** -主要风险点和注意事项 - -## 五、总结 -用一句话概括核心观点。 - ---- -**数据说明** -- 数据来源:Tushare Pro(截止{latest['trade_date']}) -- 分析周期:基于近{data['history_days']}个交易日数据 - -写作要求: -1. 语言简洁专业但易懂 -2. 每个分析点独立成段 -3. 控制在600-800字 -4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - try: - analysis = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=2500 - ) - - if analysis: - return f"【{data['index_name']}({data['index_code']}) - AI深度分析】\n\n{analysis}" - else: - return f"【{data['index_name']}】分析生成失败" - - except Exception as e: - logger.error(f"LLM指数分析失败: {e}") - return f"【{data['index_name']}】分析生成失败" - - def _rule_based_analysis(self, data: Dict[str, Any]) -> str: - """基于规则的分析(LLM不可用时的备选方案)""" - parts = [f"【{data['stock_name']}({data['stock_code']}) - 综合分析】\n"] - - # 行情信息 - if data.get('quote'): - quote = data['quote'] - parts.append("## 一、实时行情") - parts.append(f"最新价:{quote.get('close', 0):.2f}元") - parts.append(f"涨跌幅:{quote.get('pct_chg', 0):.2f}%") - parts.append(f"成交量:{quote.get('vol', 0):.0f}手") - parts.append("") - - # 技术分析 - if data.get('technical'): - tech = data['technical'].get('indicators', {}) - parts.append("## 二、技术指标") - - if 'ma' in tech: - ma = tech['ma'] - parts.append(f"均线系统:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}") - - if 'macd' in tech: - macd = tech['macd'] - parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}") - - if 'rsi' in tech: - rsi = tech['rsi'] - rsi6 = rsi.get('rsi6', 50) - if rsi6 > 70: - parts.append(f"RSI:{rsi6:.1f}(超买区域,注意回调风险)") - elif rsi6 < 30: - parts.append(f"RSI:{rsi6:.1f}(超卖区域,可能存在反弹机会)") - else: - parts.append(f"RSI:{rsi6:.1f}(中性区域)") - - parts.append("") - - # 基本面 - if data.get('fundamental'): - fund = data['fundamental'] - parts.append("## 三、基本信息") - parts.append(f"所属行业:{fund.get('industry', '未知')}") - parts.append(f"上市日期:{fund.get('list_date', '未知')}") - parts.append("") - - # 简单建议 - parts.append("## 四、参考建议") - parts.append("建议结合更多信息进行综合判断。") - parts.append("") - parts.append("⚠️ 以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。") - - return "\n".join(parts) - - async def _single_query( - self, - stock_code: str, - stock_name: Optional[str], - message: str - ) -> Dict[str, Any]: - """单一查询处理 - 使用LLM进行分析""" - # 识别意图 - intent = self._recognize_intent(message, stock_code) - - # 执行技能 - result = await skill_manager.execute_skill( - intent["skill"], - **intent["params"] - ) - - # 格式化响应 - if not result.get("success", True): - return { - "message": f"查询失败:{result.get('error', '未知错误')}", - "metadata": {"type": "error"} - } - - # 所有查询都使用LLM进行分析(除了可视化) - if intent["type"] != "visualization" and self.use_llm: - return await self._llm_single_analysis(intent, result, stock_code, stock_name, message) - else: - return self._format_response(intent, result, stock_code, stock_name) - - def _recognize_intent(self, message: str, stock_code: str) -> Dict[str, Any]: - """识别查询意图""" - message_lower = message.lower() - - # K线图 - if any(kw in message_lower for kw in ["k线", "图表", "走势图", "kline"]): - return { - "type": "visualization", - "skill": "visualization", - "params": {"stock_code": stock_code} - } - - # 技术分析 - if any(kw in message_lower for kw in ["技术", "指标", "macd", "rsi", "均线"]): - return { - "type": "technical", - "skill": "technical_analysis", - "params": {"stock_code": stock_code, "indicators": ["ma", "macd", "rsi"]} - } - - # 基本面 - if any(kw in message_lower for kw in ["基本面", "公司", "行业", "信息"]): - return { - "type": "fundamental", - "skill": "fundamental", - "params": {"stock_code": stock_code} - } - - # 默认:实时行情 - return { - "type": "quote", - "skill": "market_data", - "params": {"stock_code": stock_code, "data_type": "quote"} - } - - def _format_response( - self, - intent: Dict[str, Any], - result: Dict[str, Any], - stock_code: str, - stock_name: Optional[str] - ) -> Dict[str, Any]: - """格式化响应""" - data = result.get("data", result) - display_name = stock_name or stock_code - - if intent["type"] == "quote": - message = f"""【{display_name}】实时行情 - -交易日期:{data.get('trade_date', '')} -最新价:{data.get('close', 0):.2f}元 -涨跌幅:{data.get('pct_chg', 0):+.2f}% -涨跌额:{data.get('change', 0):+.2f}元 -开盘价:{data.get('open', 0):.2f}元 -最高价:{data.get('high', 0):.2f}元 -最低价:{data.get('low', 0):.2f}元 -成交量:{data.get('vol', 0):.0f}手 -成交额:{data.get('amount', 0):.0f}千元""" - - return { - "message": message, - "metadata": {"type": "quote", "data": data} - } - - elif intent["type"] == "technical": - indicators = data.get("indicators", {}) - parts = [f"【{display_name}】技术指标\n"] - - if "ma" in indicators: - ma = indicators["ma"] - parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}") - - if "macd" in indicators: - macd = indicators["macd"] - parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}") - - if "rsi" in indicators: - rsi = indicators["rsi"] - parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}") - - return { - "message": "\n".join(parts), - "metadata": {"type": "technical", "data": data} - } - - elif intent["type"] == "visualization": - return { - "message": f"已生成【{display_name}】的K线图", - "metadata": {"type": "chart", "data": data} - } - - elif intent["type"] == "fundamental": - message = f"""【{display_name}】基本信息 - -股票代码:{data.get('ts_code', '')} -所属地域:{data.get('area', '')} -所属行业:{data.get('industry', '')} -上市市场:{data.get('market', '')} -上市日期:{data.get('list_date', '')}""" - - return { - "message": message, - "metadata": {"type": "fundamental", "data": data} - } - - return { - "message": "查询完成", - "metadata": {"type": "data", "data": data} - } - - async def _analyze_question_intent(self, message: str, session_id: str) -> Optional[Dict[str, Any]]: - """ - 使用LLM分析问题意图(支持上下文) - - Args: - message: 用户消息 - session_id: 会话ID - - Returns: - 意图分析结果: { - 'type': 'stock_specific' | 'macro_finance' | 'knowledge' | 'general_chat', - 'description': '问题描述', - 'keywords': ['关键词列表'], - 'stock_names': ['股票名称'] (如果是stock_specific类型) - } - """ - if not self.use_llm: - logger.warning("LLM未配置,无法分析意图") - return None - - # 获取历史对话上下文 - history = self.context_manager.get_context(session_id) - context_str = "" - if history: - context_str = "\n\n【对话历史】\n" - # 只取最近4条消息 - for msg in history[-4:]: - role = "用户" if msg["role"] == "user" else "助手" - content = msg['content'][:100] # 限制长度 - context_str += f"{role}: {content}\n" - - prompt = f"""你是一个专业的金融智能助手。分析用户的问题,理解用户意图。 - -{context_str} - -【当前问题】 -用户: {message} - -请分析这个问题属于以下哪一类: - -1. **stock_specific** - 针对特定股票或指数的问题 - 例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"、"帮我看看这只股票" - **重要**:指数查询也属于此类,例如:"上证指数怎么样"、"分析大盘"、"A股指数走势"、"深证成指" - **美股支持**:美股查询也属于此类,例如:"苹果股票怎么样"、"AAPL分析"、"特斯拉走势"、"TSLA技术指标" - -2. **macro_finance** - 宏观金融/市场问题(不针对特定股票或指数) - 例如:"最近有什么投资机会"、"现在适合买股票吗"、"市场情绪如何" - -3. **knowledge** - 金融知识问答 - 例如:"什么是MACD"、"如何看K线图"、"价值投资是什么"、"市盈率怎么算" - -4. **general_chat** - 一般对话/问候/不明确的问题 - 例如:"你好"、"在吗"、"你能做什么"、"帮我"(没有具体说明) - -**重要提示**: -- 如果用户问题不明确,但可能与金融相关,优先归类为 general_chat,以便引导用户 -- 如果用户提到"这只股票"、"它"等代词,查看对话历史判断是否指特定股票 -- **如果用户提到"大盘"、"上证"、"深证"、"A股指数"等,归类为 stock_specific,并在stock_names中填入对应的指数名称** -- **如果用户提到美股公司名称(如苹果、特斯拉、微软)或美股代码(如AAPL、TSLA、MSFT),归类为 stock_specific,并在stock_names中填入对应的股票名称或代码** -- 对于模糊的问题,不要强行归类,使用 general_chat 类型 - -请以JSON格式返回分析结果: -{{ - "type": "问题类型", - "description": "问题的简要描述(用一句话概括用户想了解什么)", - "keywords": ["关键词1", "关键词2"], - "stock_names": ["股票名称或指数名称或美股代码"] (仅当type为stock_specific时,如果有的话), - "market": "A股" 或 "美股" (仅当type为stock_specific时,根据股票类型判断) -}} - -只返回JSON,不要有任何其他内容。""" - - try: - result = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.3, - max_tokens=300 - ) - - if not result: - logger.warning("LLM返回空结果") - return None - - # 清理结果,移除可能的markdown代码块标记 - result = result.strip() - if result.startswith("```json"): - result = result[7:] - if result.startswith("```"): - result = result[3:] - if result.endswith("```"): - result = result[:-3] - result = result.strip() - - # 检查是否为空 - if not result: - logger.warning("LLM返回内容为空") - return None - - # 解析JSON - intent = json.loads(result) - logger.info(f"意图分析结果: {intent}") - return intent - - except json.JSONDecodeError as e: - logger.error(f"意图分析JSON解析失败: {e}, 原始响应: {result[:200] if result else 'None'}") - return None - except Exception as e: - logger.error(f"意图分析失败: {e}") - return None - - async def _handle_stock_question( - self, - intent_analysis: Dict[str, Any], - message: str - ) -> Dict[str, Any]: - """处理针对特定股票或指数的问题""" - stock_names = intent_analysis.get('stock_names', []) - market = intent_analysis.get('market', 'A股') # 默认A股 - - if not stock_names: - return { - "message": "抱歉,我没有识别到您提到的股票或指数。请提供更明确的股票代码、名称或指数名称。", - "metadata": {"type": "error"} - } - - # 提取第一个股票或指数 - stock_keyword = stock_names[0] - - # 检测是否为美股 - is_us_stock = self._is_us_stock(stock_keyword, market) - - if is_us_stock: - # 处理美股 - return await self._handle_us_stock(stock_keyword, message) - - # 处理A股和指数 - 使用LLM进行智能匹配 - stock_info = await self._match_stock_with_llm(stock_keyword) - - if not stock_info: - return { - "message": f"抱歉,未找到股票或指数\"{stock_keyword}\"。请确认名称或代码是否正确。", - "metadata": {"type": "error"} - } - - stock_code = stock_info['code'] - stock_name = stock_info['name'] - is_index = stock_info['is_index'] - - logger.info(f"处理{'指数' if is_index else '股票'}问题: {stock_name}({stock_code})") - - # 判断是否需要全面分析 - is_comprehensive = self._is_comprehensive_analysis(message) - - if is_comprehensive: - if is_index: - return await self._comprehensive_index_analysis(stock_code, stock_name, message) - else: - return await self._comprehensive_analysis(stock_code, stock_name, message) - else: - return await self._single_query(stock_code, stock_name, message) - - async def _match_stock_with_llm(self, keyword: str) -> Optional[Dict[str, Any]]: - """ - 使用LLM智能匹配股票或指数 - - Args: - keyword: 用户输入的关键词 - - Returns: - 匹配结果: {'code': '股票代码', 'name': '股票名称', 'is_index': bool} - """ - if not self.use_llm: - # 降级方案:使用Tushare搜索 - search_results = tushare_service.search_stock(keyword) - if search_results: - return { - 'code': search_results[0]['symbol'], - 'name': search_results[0]['name'], - 'is_index': False - } - return None - - prompt = f"""你是一个专业的A股市场专家。请根据用户输入的关键词,识别对应的股票代码或指数代码。 - -用户输入:{keyword} - -常见指数代码: -- 上证指数/大盘/沪指/A股 → 000001.SH -- 深证成指/深证/深指 → 399001.SZ -- 创业板指/创业板 → 399006.SZ -- 科创50 → 000688.SH -- 沪深300 → 000300.SH -- 中证500 → 000905.SH - -如果是指数,请直接返回对应的指数代码。 -如果是股票名称或代码,请使用Tushare数据库进行搜索匹配。 - -请以JSON格式返回: -{{ - "is_index": true/false, - "code": "股票或指数代码(如000001.SH)", - "name": "股票或指数名称", - "confidence": 0.0-1.0 -}} - -如果无法匹配,返回: -{{ - "is_index": false, - "code": null, - "name": null, - "confidence": 0.0 -}} - -只返回JSON,不要有任何其他内容。""" - - try: - result = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.3, - max_tokens=200 - ) - - if not result: - logger.warning("LLM匹配返回空结果") - return None - - # 清理结果 - result = result.strip() - if result.startswith("```json"): - result = result[7:] - if result.startswith("```"): - result = result[3:] - if result.endswith("```"): - result = result[:-3] - result = result.strip() - - # 解析JSON - match_result = json.loads(result) - - # 如果LLM无法匹配或置信度太低,使用Tushare搜索 - if not match_result.get('code') or match_result.get('confidence', 0) < 0.5: - logger.info(f"LLM匹配置信度低,使用Tushare搜索: {keyword}") - search_results = tushare_service.search_stock(keyword) - if search_results: - return { - 'code': search_results[0]['symbol'], - 'name': search_results[0]['name'], - 'is_index': False - } - return None - - logger.info(f"LLM匹配成功: {keyword} -> {match_result['name']}({match_result['code']})") - return { - 'code': match_result['code'], - 'name': match_result['name'], - 'is_index': match_result['is_index'] - } - - except json.JSONDecodeError as e: - logger.error(f"LLM匹配JSON解析失败: {e}, 原始响应: {result[:200] if result else 'None'}") - # 降级方案 - search_results = tushare_service.search_stock(keyword) - if search_results: - return { - 'code': search_results[0]['symbol'], - 'name': search_results[0]['name'], - 'is_index': False - } - return None - except Exception as e: - logger.error(f"LLM匹配失败: {e}") - # 降级方案 - search_results = tushare_service.search_stock(keyword) - if search_results: - return { - 'code': search_results[0]['symbol'], - 'name': search_results[0]['name'], - 'is_index': False - } - return None - - async def _handle_macro_question( - self, - intent_analysis: Dict[str, Any], - message: str - ) -> Dict[str, Any]: - """处理宏观金融问题""" - keywords = intent_analysis.get('keywords', []) - description = intent_analysis.get('description', '') - - logger.info(f"处理宏观问题: {description}") - - try: - # 使用LLM进行分析(基于Tushare数据和公开信息) - prompt = f"""你是一位专业的金融分析师。用户询问了宏观金融问题。 - -用户问题:{message} - -问题分析:{description} -关键词:{', '.join(keywords)} - -请基于你的金融知识和市场经验,给出专业且易懂的分析: - -## 市场现状分析 -简要说明当前市场整体情况和主要影响因素(分段说明,每个要点独立成段) - -## 趋势判断 -- 短期趋势(1-2周) -- 中期展望(1-3个月) - -## 投资建议 -- 具体的投资策略建议 -- 需要关注的风险点 - -写作要求: -1. 语言简洁专业但易懂,避免过度修饰 -2. 分析要客观理性,基于事实和数据 -3. 每个分析点独立成段,段落之间用空行分隔 -4. 控制在400-500字 -5. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" -""" - - analysis = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=1500 - ) - - if analysis: - return { - "message": f"【宏观市场分析】\n\n{analysis}", - "metadata": {"type": "macro_analysis"} - } - - except Exception as e: - logger.error(f"宏观问题处理失败: {e}") - - # 降级方案:提供友好的引导 - return { - "message": f"""我理解您想了解:{description} - -目前我可以帮您: -• 📊 分析具体股票的走势和投资价值 -• 📈 查看大盘指数(如上证指数、深证成指) -• 📚 解答金融投资相关问题 - -您可以更具体地问我,比如: -• "现在上证指数怎么样" -• "分析一下创业板的走势" -• "最近哪些行业比较热门" - -请告诉我您想了解什么?""", - "metadata": {"type": "guide"} - } - - async def _handle_knowledge_question( - self, - intent_analysis: Dict[str, Any], - message: str - ) -> Dict[str, Any]: - """处理金融知识问答""" - description = intent_analysis.get('description', '') - keywords = intent_analysis.get('keywords', []) - - logger.info(f"处理知识问答: {description}") - - # 直接使用LLM回答 - prompt = f"""你是一位专业的金融教育专家。用户询问了金融知识问题。 - -用户问题:{message} - -请用通俗易懂的语言解释这个概念或回答这个问题: - -## 核心概念 -- 清晰定义和解释 - -## 实际应用 -- 如何在投资中应用 -- 注意事项 - -## 举例说明 -- 用简单的例子帮助理解 - -写作要求: -1. 语言通俗易懂,避免过多专业术语 -2. 如果使用专业术语,要简单解释 -3. 控制在300-400字 -4. 重点是帮助用户理解,而不是炫耀知识 -""" - - try: - answer = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=1200 - ) - - if answer: - return { - "message": f"【金融知识解答】\n\n{answer}", - "metadata": {"type": "knowledge"} - } - - except Exception as e: - logger.error(f"知识问答处理失败: {e}") - - return { - "message": "抱歉,暂时无法回答您的问题。请稍后再试。", - "metadata": {"type": "error"} - } - - async def _handle_general_chat( - self, - intent_analysis: Dict[str, Any], - message: str - ) -> Dict[str, Any]: - """处理一般对话,智能引导用户""" - description = intent_analysis.get('description', '') - - logger.info(f"处理一般对话: {description}") - - # 使用LLM生成友好的引导回复 - prompt = f"""你是一位专业且友好的金融智能助手。用户发来了一条消息,但不够具体。 - -用户消息:{message} -问题分析:{description} - -你的任务是: -1. 如果是问候(如"你好"、"在吗"),友好回应并介绍你的能力 -2. 如果问题不明确(如"帮我"、"看看"),礼貌地询问用户想了解什么 -3. 如果可能与金融相关但不具体,给出具体的提问示例引导用户 - -你可以提供的服务: -• 📊 股票分析 - 分析个股走势、技术指标、基本面 -• 📈 市场观察 - 解读大盘走势、行业热点、投资机会 -• 📚 知识问答 - 解答金融投资相关问题 - -回复要求: -1. 语气友好、专业但不生硬 -2. 简洁明了,不要太长(150字以内) -3. 给出2-3个具体的提问示例 -4. 不要使用"抱歉"、"无法理解"等负面词汇 -5. 用emoji让回复更友好(但不要过度使用) - -直接返回回复内容,不要有其他格式。""" - - try: - reply = llm_service.chat( - messages=[{"role": "user", "content": prompt}], - temperature=0.8, - max_tokens=400 - ) - - if reply: - return { - "message": reply, - "metadata": {"type": "chat"} - } - - except Exception as e: - logger.error(f"一般对话处理失败: {e}") - - # 降级方案 - return { - "message": """您好!我是您的金融智能助手 👋 - -我可以帮您: -• 📊 分析具体股票(如"分析比亚迪") -• 📈 解读市场走势(如"现在大盘怎么样") -• 📚 解答金融知识(如"什么是市盈率") - -请告诉我您想了解什么?""", - "metadata": {"type": "chat"} - } - - def _is_us_stock(self, keyword: str, market: str) -> bool: - """ - 判断是否为美股 - - Args: - keyword: 股票关键词 - market: 市场类型(从LLM意图分析中获取) - - Returns: - 是否为美股 - """ - # 如果LLM已经判断为美股 - if market == "美股": - return True - - # 检查是否为全大写字母(美股代码特征) - if keyword.isupper() and keyword.isalpha() and len(keyword) <= 5: - return True - - # 常见美股公司名称映射 - us_stock_names = { - "苹果": "AAPL", - "特斯拉": "TSLA", - "微软": "MSFT", - "谷歌": "GOOGL", - "亚马逊": "AMZN", - "Meta": "META", - "脸书": "META", - "英伟达": "NVDA", - "奈飞": "NFLX", - "网飞": "NFLX", - "迪士尼": "DIS", - "可口可乐": "KO", - "麦当劳": "MCD", - "星巴克": "SBUX", - "耐克": "NKE", - "波音": "BA", - "英特尔": "INTC", - "AMD": "AMD", - "高通": "QCOM", - "推特": "TWTR", - "优步": "UBER", - "Uber": "UBER", - "Airbnb": "ABNB", - "爱彼迎": "ABNB", - } - - if keyword in us_stock_names: - return True - - return False - - def _get_us_stock_symbol(self, keyword: str) -> str: - """ - 获取美股代码 - - Args: - keyword: 股票关键词 - - Returns: - 美股代码 - """ - # 如果已经是代码格式,直接返回 - if keyword.isupper() and keyword.isalpha(): - return keyword - - # 中文名称映射 - us_stock_names = { - "苹果": "AAPL", - "特斯拉": "TSLA", - "微软": "MSFT", - "谷歌": "GOOGL", - "亚马逊": "AMZN", - "Meta": "META", - "脸书": "META", - "英伟达": "NVDA", - "奈飞": "NFLX", - "网飞": "NFLX", - "迪士尼": "DIS", - "可口可乐": "KO", - "麦当劳": "MCD", - "星巴克": "SBUX", - "耐克": "NKE", - "波音": "BA", - "英特尔": "INTC", - "AMD": "AMD", - "高通": "QCOM", - "推特": "TWTR", - "优步": "UBER", - "Uber": "UBER", - "Airbnb": "ABNB", - "爱彼迎": "ABNB", - } - - return us_stock_names.get(keyword, keyword.upper()) - - async def _handle_us_stock(self, keyword: str, message: str) -> Dict[str, Any]: - """ - 处理美股查询 - - Args: - keyword: 股票关键词 - message: 用户消息 - - Returns: - 分析结果 - """ - # 获取美股代码 - 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" - ) - - if not result.get("success"): - return { - "message": f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。\n\n提示:美股代码通常为大写字母,如 AAPL(苹果)、TSLA(特斯拉)、MSFT(微软)等。", - "metadata": {"type": "error"} - } - - # 使用LLM分析美股数据 - if self.use_llm: - analysis = await self._llm_us_stock_analysis(result["data"], message) - else: - analysis = self._format_us_stock_data(result["data"]) - - return { - "message": analysis, - "metadata": { - "type": "us_stock_analysis", - "data": result["data"] - } - } - - except Exception as e: - logger.error(f"美股查询失败: {e}") - return { - "message": f"查询美股 {symbol} 时出错:{str(e)}", - "metadata": {"type": "error"} - } - - async def _llm_us_stock_analysis(self, data: Dict[str, Any], user_message: str) -> str: - """使用LLM分析美股数据""" - from datetime import datetime - current_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 提取关键数据 - symbol = data.get("symbol", "") - name = data.get("name", "") - sector = data.get("sector", "") - industry = data.get("industry", "") - current_price = data.get("current_price", 0) - change = data.get("change", 0) - change_pct = 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) - dividend_yield = data.get("dividend_yield", 0) - week_52_high = data.get("52_week_high", 0) - week_52_low = data.get("52_week_low", 0) - technical = data.get("technical_indicators", {}) - description = data.get("description", "") - - # 格式化市值 - market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M" - - # 构建分析提示 - prompt = f"""你是一位专业的美股分析师。请基于以下数据对 {name} ({symbol}) 进行全面分析。 - -**重要提示:当前日期是 {current_time},请在分析中使用这个日期,不要使用其他日期。** - -【基本信息】 -股票代码:{symbol} -公司名称:{name} -所属行业:{sector} - {industry} -公司简介:{description[:300] if description else '暂无'} - -【实时行情】(数据时间:{current_time}) -当前价格:${current_price:.2f} -涨跌额:${change:.2f} -涨跌幅:{change_pct:.2f}% -成交量:{volume:,} -市值:{market_cap_str} - -【估值指标】 -市盈率(PE):{f"{pe_ratio:.2f}" if pe_ratio else '暂无'} -市净率(PB):{f"{pb_ratio:.2f}" if pb_ratio else '暂无'} -股息率:{f"{dividend_yield * 100:.2f}%" if dividend_yield else '暂无'} -52周最高:${week_52_high:.2f} -52周最低:${week_52_low:.2f} - -【技术指标】 -MA5:{f"${technical.get('ma5'):.2f}" if technical.get('ma5') else '计算中'} -MA10:{f"${technical.get('ma10'):.2f}" if technical.get('ma10') else '计算中'} -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 '计算中'} - -用户问题:{user_message} - -请提供专业的分析报告,包括: - -## 📊 行情概览 -简要总结当前股价表现和市场表现(2-3句话) - -## 💼 公司基本面 -- 行业地位和竞争优势 -- 估值水平分析(PE、PB是否合理) -- 盈利能力和成长性 - -## 📈 技术面分析 -- 当前趋势判断(基于均线系统) -- 关键支撑位和压力位 -- RSI和MACD信号解读 - -## 💡 投资建议 -- 短期操作建议(1-2周) -- 中期投资价值(1-3个月) -- 风险提示 - -写作要求: -1. 语言专业但易懂,避免过度修饰 -2. 分析客观理性,基于数据和事实 -3. 每个部分独立成段,段落间用空行分隔 -4. 控制在500-600字 -5. **不要在报告中添加日期标题,直接开始分析内容** -6. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" -""" - - try: - analysis = await self._call_llm_async( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=2000 - ) - - if analysis: - return f"【美股分析】{name} ({symbol})\n\n{analysis}" - else: - return self._format_us_stock_data(data) - - except Exception as e: - logger.error(f"LLM美股分析失败: {e}") - return self._format_us_stock_data(data) - - def _format_us_stock_data(self, data: Dict[str, Any]) -> str: - """格式化美股数据(降级方案)""" - symbol = data.get("symbol", "") - name = data.get("name", "") - current_price = data.get("current_price", 0) - change = data.get("change", 0) - change_pct = data.get("change_percent", 0) - market_cap = data.get("market_cap", 0) - pe_ratio = data.get("pe_ratio", 0) - technical = data.get("technical_indicators", {}) - - market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M" - - change_emoji = "📈" if change >= 0 else "📉" - - return f"""【美股行情】{name} ({symbol}) - -{change_emoji} 当前价格:${current_price:.2f} -涨跌:${change:.2f} ({change_pct:+.2f}%) -市值:{market_cap_str} -市盈率:{pe_ratio:.2f if pe_ratio else '暂无'} - -【技术指标】 -MA5:${technical.get('ma5', 0):.2f if technical.get('ma5') else '计算中'} -MA20:${technical.get('ma20', 0):.2f if technical.get('ma20') else '计算中'} -RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'} - -以上数据仅供参考,不构成投资建议。""" - - async def process_message_stream( - self, - message: str, - session_id: str, - user_id: Optional[str] = None - ): - """ - 流式处理用户消息 - - Args: - message: 用户消息 - session_id: 会话ID - user_id: 用户ID - - Yields: - 响应文本片段 - """ - logger.info(f"流式处理消息: {message[:50]}...") - - # 保存用户消息 - self.context_manager.add_message(session_id, "user", message) - - # 第一步:使用LLM理解问题意图 - intent_analysis = await self._analyze_question_intent(message, session_id) - - if not intent_analysis: - # 引导用户 - guide_message = """我是您的金融智能助手,可以帮您: - -📊 **股票分析** - 分析个股走势、技术指标、基本面 -📈 **市场观察** - 解读大盘走势、行业热点 -📚 **知识问答** - 解答金融投资相关问题 - -您可以这样问我: -• "分析一下贵州茅台" -• "现在A股市场怎么样" -• "什么是MACD指标" - -请告诉我您想了解什么?""" - self.context_manager.add_message(session_id, "assistant", guide_message) - yield guide_message - return - - # 第二步:根据意图类型处理(流式) - question_type = intent_analysis['type'] - - full_response = "" - if question_type == 'stock_specific': - # 针对特定股票的问题 - 流式输出 - async for chunk in self._handle_stock_question_stream(intent_analysis, message): - full_response += chunk - yield chunk - elif question_type in ['macro_finance', 'knowledge', 'general_chat']: - # 其他类型 - 直接流式输出(不需要特殊处理) - response = await self._handle_other_question(question_type, intent_analysis, message) - full_response = response["message"] - # 逐字yield - for char in full_response: - yield char - else: - # 未知类型 - guide_message = f"""我理解您想了解:{intent_analysis.get('description', '相关信息')} - -作为金融智能助手,我擅长: -• 📊 分析具体股票(如"分析比亚迪") -• 📈 解读市场走势(如"现在大盘怎么样") -• 📚 解答金融知识(如"什么是市盈率") - -能否更具体地告诉我您想了解什么?""" - full_response = guide_message - yield guide_message - - # 保存助手响应 - self.context_manager.add_message(session_id, "assistant", full_response) - - async def _handle_other_question( - self, - question_type: str, - intent_analysis: Dict[str, Any], - message: str - ) -> Dict[str, Any]: - """处理非股票分析的其他问题""" - if question_type == 'macro_finance': - return await self._handle_macro_question(intent_analysis, message) - elif question_type == 'knowledge': - return await self._handle_knowledge_question(intent_analysis, message) - elif question_type == 'general_chat': - return await self._handle_general_chat(intent_analysis, message) - else: - return {"message": "抱歉,我无法理解您的问题。"} - - async def _handle_stock_question_stream( - self, - intent_analysis: Dict[str, Any], - message: str - ): - """流式处理股票问题""" - stock_names = intent_analysis.get('stock_names', []) - market = intent_analysis.get('market', 'A股') - - if not stock_names: - yield "抱歉,我没有识别到您提到的股票或指数。请提供更明确的股票代码、名称或指数名称。" - return - - stock_keyword = stock_names[0] - is_us_stock = self._is_us_stock(stock_keyword, market) - - if is_us_stock: - # 美股分析 - 流式 - async for chunk in self._handle_us_stock_stream(stock_keyword, message): - yield chunk - else: - # A股分析 - 流式 - async for chunk in self._handle_a_stock_stream(stock_keyword, message): - yield chunk - - async def _handle_a_stock_stream(self, stock_keyword: str, message: str): - """流式处理A股分析""" - # 使用LLM进行智能匹配 - stock_info = await self._match_stock_with_llm(stock_keyword) - - if not stock_info: - yield f"抱歉,未找到股票或指数\"{stock_keyword}\"。请确认名称或代码是否正确。" - return - - stock_code = stock_info['code'] - 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") - - 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 - } - - # 使用LLM流式分析 - if self.use_llm: - async for chunk in self._llm_comprehensive_analysis_stream(all_data, message, is_index): - yield chunk - else: - yield self._rule_based_analysis(all_data) - - except Exception as e: - logger.error(f"A股分析失败: {e}") - yield f"分析{stock_name}时出错:{str(e)}" - - async def _handle_us_stock_stream(self, keyword: str, message: str): - """流式处理美股分析""" - 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") - - if not result.get("success"): - yield f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。" - return - - # 使用LLM流式分析 - if self.use_llm: - async for chunk in self._llm_us_stock_analysis_stream(result["data"], message): - yield chunk - else: - yield self._format_us_stock_data(result["data"]) - - except Exception as e: - logger.error(f"美股查询失败: {e}") - yield f"查询美股 {symbol} 时出错:{str(e)}" - - 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 - - current_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 获取行情数据的交易日期 - 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(同步生成器,使用线程避免阻塞) - import asyncio - stream = llm_service.chat_stream( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=2000 - ) - - # 在线程中迭代同步生成器,避免阻塞事件循环 - loop = asyncio.get_event_loop() - for chunk in stream: - # 每次yield后让出控制权 - await asyncio.sleep(0) - yield chunk - - async def _llm_us_stock_analysis_stream(self, data: Dict[str, Any], user_message: str): - """使用LLM流式分析美股""" - from datetime import datetime - current_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - symbol = data.get("symbol", "") - name = data.get("name", "") - sector = data.get("sector", "") - industry = data.get("industry", "") - current_price = data.get("current_price", 0) - change = data.get("change", 0) - change_pct = 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) - dividend_yield = data.get("dividend_yield", 0) - week_52_high = data.get("52_week_high", 0) - week_52_low = data.get("52_week_low", 0) - technical = data.get("technical_indicators", {}) - description = data.get("description", "") - - market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M" - - prompt = f"""你是一位专业的美股分析师。请基于以下数据对 {name} ({symbol}) 进行全面分析。 - -**重要提示:当前日期是 {current_time},请在分析中使用这个日期,不要使用其他日期。** - -【基本信息】 -股票代码:{symbol} -公司名称:{name} -所属行业:{sector} - {industry} -公司简介:{description[:300] if description else '暂无'} - -【实时行情】(数据时间:{current_time}) -当前价格:${current_price:.2f} -涨跌额:${change:.2f} -涨跌幅:{change_pct:.2f}% -成交量:{volume:,} -市值:{market_cap_str} - -【估值指标】 -市盈率(PE):{f"{pe_ratio:.2f}" if pe_ratio else '暂无'} -市净率(PB):{f"{pb_ratio:.2f}" if pb_ratio else '暂无'} -股息率:{f"{dividend_yield * 100:.2f}%" if dividend_yield else '暂无'} -52周最高:${week_52_high:.2f} -52周最低:${week_52_low:.2f} - -【技术指标】 -MA5:{f"${technical.get('ma5'):.2f}" if technical.get('ma5') else '计算中'} -MA10:{f"${technical.get('ma10'):.2f}" if technical.get('ma10') else '计算中'} -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 '计算中'} - -用户问题:{user_message} - -请提供专业的分析报告,包括: - -## 📊 行情概览 -简要总结当前股价表现和市场表现(2-3句话) - -## 💼 公司基本面 -- 行业地位和竞争优势 -- 估值水平分析(PE、PB是否合理) -- 盈利能力和成长性 - -## 📈 技术面分析 -- 当前趋势判断(基于均线系统) -- 关键支撑位和压力位 -- RSI和MACD信号解读 - -## 💡 投资建议 -- 短期操作建议(1-2周) -- 中期投资价值(1-3个月) -- 风险提示 - -写作要求: -1. 语言专业但易懂,避免过度修饰 -2. 分析客观理性,基于数据和事实 -3. 每个部分独立成段,段落间用空行分隔 -4. 控制在500-600字 -5. **不要在报告中添加日期标题,直接开始分析内容** -6. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" -""" - - # 流式调用LLM(同步生成器,使用线程避免阻塞) - import asyncio - stream = llm_service.chat_stream( - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=2000 - ) - - # 在线程中迭代同步生成器,避免阻塞事件循环 - for chunk in stream: - # 每次yield后让出控制权 - await asyncio.sleep(0) - yield chunk - - -# 创建全局实例 -smart_agent = SmartStockAgent() diff --git a/backend/app/skills/us_stock_skill.py b/backend/app/skills/us_stock_skill.py index e8696f7..bb75547 100644 --- a/backend/app/skills/us_stock_skill.py +++ b/backend/app/skills/us_stock_skill.py @@ -1,5 +1,5 @@ """ -美股分析技能 +美股/港股分析技能 """ from typing import Dict, Any from app.skills.base import BaseSkill, SkillParameter @@ -8,17 +8,17 @@ from app.utils.logger import logger class USStockSkill(BaseSkill): - """美股分析技能""" + """美股/港股分析技能(使用 yfinance)""" def __init__(self): super().__init__() self.name = "us_stock_analysis" - self.description = "分析美股(如 AAPL, TSLA, MSFT 等),获取实时行情、技术指标、基本面数据" + self.description = "分析美股(如 AAPL, TSLA)和港股(如 0700.HK, 9988.HK),获取实时行情、技术指标、基本面数据" self.parameters = [ SkillParameter( name="symbol", type="string", - description="美股代码(如 AAPL, TSLA, MSFT)", + description="股票代码(美股如 AAPL, TSLA;港股如 0700.HK, 9988.HK)", required=True ), SkillParameter( @@ -51,7 +51,7 @@ class USStockSkill(BaseSkill): "error": "请提供美股代码" } - logger.info(f"开始分析美股: {symbol}, 类型: {analysis_type}") + logger.info(f"开始分析股票: {symbol}, 类型: {analysis_type}") if analysis_type == "basic": # 基本信息 diff --git a/backend/app/utils/stock_names.py b/backend/app/utils/stock_names.py deleted file mode 100644 index 81a2963..0000000 --- a/backend/app/utils/stock_names.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -股票名称映射数据库 -包含常见A股股票的名称到代码的映射 -""" -from typing import Optional - -# 常见A股股票名称映射(按行业分类) -STOCK_NAME_MAP = { - # 白酒 - "贵州茅台": "600519", - "茅台": "600519", - "五粮液": "000858", - "泸州老窖": "000568", - "山西汾酒": "600809", - "洋河股份": "002304", - - # 银行 - "工商银行": "601398", - "工行": "601398", - "建设银行": "601939", - "建行": "601939", - "农业银行": "601288", - "农行": "601288", - "中国银行": "601988", - "中行": "601988", - "交通银行": "601328", - "交行": "601328", - "招商银行": "600036", - "招行": "600036", - "兴业银行": "601166", - "浦发银行": "600000", - "民生银行": "600016", - "光大银行": "601818", - "平安银行": "000001", - "宁波银行": "002142", - - # 保险 - "中国平安": "601318", - "平安": "601318", - "中国人寿": "601628", - "中国太保": "601601", - "新华保险": "601336", - - # 证券 - "中信证券": "600030", - "中信": "600030", - "海通证券": "600837", - "国泰君安": "601211", - "华泰证券": "601688", - "广发证券": "000776", - "招商证券": "600999", - "东方证券": "600958", - - # 科技 - "中兴通讯": "000063", - "中兴": "000063", - "立讯精密": "002475", - "京东方A": "000725", - "京东方": "000725", - "TCL科技": "000100", - "海康威视": "002415", - "大华股份": "002236", - "科大讯飞": "002230", - "讯飞": "002230", - "紫光国微": "002049", - "中芯国际": "688981", - "韦尔股份": "603501", - - # 新能源汽车 - "比亚迪": "002594", - "宁德时代": "300750", - "宁德": "300750", - "长城汽车": "601633", - "长城": "601633", - "上汽集团": "600104", - "上汽": "600104", - "广汽集团": "601238", - "广汽": "601238", - "吉利汽车": "00175", # 港股 - "理想汽车": "02015", # 港股 - "小鹏汽车": "09868", # 港股 - "蔚来": "09866", # 港股 - - # 医药 - "恒瑞医药": "600276", - "恒瑞": "600276", - "药明康德": "603259", - "迈瑞医疗": "300760", - "迈瑞": "300760", - "片仔癀": "600436", - "云南白药": "000538", - "白药": "000538", - "爱尔眼科": "300015", - "智飞生物": "300122", - - # 消费 - "伊利股份": "600887", - "伊利": "600887", - "海天味业": "603288", - "海天": "603288", - "格力电器": "000651", - "格力": "000651", - "美的集团": "000333", - "美的": "000333", - "海尔智家": "600690", - "海尔": "600690", - "老板电器": "002508", - - # 地产 - "万科A": "000002", - "万科": "000002", - "保利发展": "600048", - "保利": "600048", - "招商蛇口": "001979", - "金地集团": "600383", - "金地": "600383", - - # 能源 - "中国石油": "601857", - "中石油": "601857", - "中国石化": "600028", - "中石化": "600028", - "中国神华": "601088", - "神华": "601088", - "陕西煤业": "601225", - "长江电力": "600900", - "三峡能源": "600905", - - # 通信 - "中国移动": "600941", - "移动": "600941", - "中国电信": "601728", - "电信": "601728", - "中国联通": "600050", - "联通": "600050", - "中国卫通": "601698", - "卫通": "601698", - - # 航空航天 - "中国国航": "601111", - "国航": "601111", - "南方航空": "600029", - "南航": "600029", - "东方航空": "600115", - "东航": "600115", - "中国卫星": "600118", - "航天科技": "000901", - - # 钢铁 - "宝钢股份": "600019", - "宝钢": "600019", - "河钢股份": "000709", - "河钢": "000709", - "鞍钢股份": "000898", - "鞍钢": "000898", - - # 有色金属 - "紫金矿业": "601899", - "紫金": "601899", - "中国铝业": "601600", - "中铝": "601600", - "江西铜业": "600362", - "江铜": "600362", - "洛阳钼业": "603993", - - # 化工 - "万华化学": "600309", - "万华": "600309", - "华鲁恒升": "600426", - "恒力石化": "600346", - "荣盛石化": "002493", - - # 电力设备 - "隆基绿能": "601012", - "隆基": "601012", - "阳光电源": "300274", - "通威股份": "600438", - "通威": "600438", - "特变电工": "600089", - - # 军工 - "中航沈飞": "600760", - "沈飞": "600760", - "中航西飞": "000768", - "西飞": "000768", - "中国船舶": "600150", - "中船": "600150", - "航发动力": "600893", - "航天发展": "000547", - - # 互联网 - "腾讯控股": "00700", # 港股 - "腾讯": "00700", - "阿里巴巴": "09988", # 港股 - "阿里": "09988", - "美团": "03690", # 港股 - "京东": "09618", # 港股 - "拼多多": "PDD", # 美股 - "百度": "09888", # 港股 - "网易": "09999", # 港股 - "小米集团": "01810", # 港股 - "小米": "01810", - - # 指数 - "上证指数": "000001", - "上证": "000001", - "沪指": "000001", - "深证成指": "399001", - "深成指": "399001", - "创业板指": "399006", - "创业板": "399006", - "科创50": "000688", - "沪深300": "000300", - "中证500": "000905", - "中证1000": "000852", -} - - -def search_stock_by_name(name: str) -> Optional[str]: - """ - 根据股票名称搜索代码 - - Args: - name: 股票名称或简称 - - Returns: - 股票代码或None - """ - # 精确匹配 - if name in STOCK_NAME_MAP: - return STOCK_NAME_MAP[name] - - # 模糊匹配(包含关系) - for stock_name, code in STOCK_NAME_MAP.items(): - if name in stock_name or stock_name in name: - return code - - return None - - -def get_stock_name(code: str) -> Optional[str]: - """ - 根据代码获取股票名称 - - Args: - code: 股票代码 - - Returns: - 股票名称或None - """ - for name, stock_code in STOCK_NAME_MAP.items(): - if stock_code == code: - return name - return None diff --git a/backend/run.sh b/backend/run.sh index 7ed4eb0..e21311d 100755 --- a/backend/run.sh +++ b/backend/run.sh @@ -34,14 +34,11 @@ try: from app.services.tushare_service import tushare_service print(" ✓ Tushare服务") - from app.utils.stock_names import search_stock_by_name - print(" ✓ 股票名称库") - from app.services.llm_service import llm_service print(" ✓ LLM服务") - from app.agent.enhanced_agent import enhanced_agent - print(" ✓ 增强版Agent") + from app.agent.smart_agent import smart_agent + print(" ✓ 智能Agent") print("\n所有模块导入成功!") @@ -76,24 +73,6 @@ if not settings.zhipuai_api_key: print("⚠️ 警告: 智谱AI Key未配置,将使用规则模式(无AI分析)") EOF -echo "" -echo "3. 测试股票名称识别..." -python3 << 'EOF' -from app.utils.stock_names import search_stock_by_name - -test_cases = [ - ("中国卫通", "601698"), - ("贵州茅台", "600519"), - ("比亚迪", "002594"), - ("宁德时代", "300750") -] - -for name, expected in test_cases: - result = search_stock_by_name(name) - status = "✓" if result == expected else "❌" - print(f" {status} {name} -> {result}") -EOF - echo "" echo "================================" echo "检查完成!准备启动..." diff --git a/backend/test_import.sh b/backend/test_import.sh index f3cbaf5..cf9a085 100755 --- a/backend/test_import.sh +++ b/backend/test_import.sh @@ -20,7 +20,7 @@ python3 -c "from app.services.cache_service import cache_service; print('✓ 缓 echo "" echo "测试Agent..." -python3 -c "from app.agent.core import stock_agent; print('✓ Agent初始化成功')" +python3 -c "from app.agent.smart_agent import smart_agent; print('✓ Agent初始化成功')" echo "" echo "所有测试通过!可以启动应用了。" diff --git a/frontend/css/style.css b/frontend/css/style.css index 5df3595..0d4abac 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -415,6 +415,45 @@ html, body { color: var(--text-tertiary); } +/* Share Image Question Section */ +.share-image-question { + margin-bottom: 20px; + padding: 16px 20px; + background: rgba(0, 255, 65, 0.05); + border: 1px solid rgba(0, 255, 65, 0.2); + border-radius: 6px; +} + +.share-image-question-label { + font-size: 12px; + font-weight: 500; + color: var(--accent); + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.share-image-question-text { + font-size: 16px; + font-weight: 500; + color: var(--text-primary); + line-height: 1.5; +} + +/* Share Image Answer Section */ +.share-image-answer { + margin-bottom: 0; +} + +.share-image-answer-label { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 1px; +} + /* Markdown Styles */ .markdown h1, .markdown h2, diff --git a/frontend/index.html b/frontend/index.html index b3b2f92..507a1e0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -62,7 +62,7 @@

AI 金融智能体

-

支持 A股 + 美股双市场分析

+

支持 A股 · 美股 · 港股 三大市场分析

@@ -70,12 +70,13 @@ - + +
@@ -130,7 +131,7 @@