diff --git a/backend/app/agent/smart_agent.py b/backend/app/agent/smart_agent.py index 53c4ad6..2f57152 100644 --- a/backend/app/agent/smart_agent.py +++ b/backend/app/agent/smart_agent.py @@ -1522,6 +1522,8 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'} # 构建分析提示 prompt = f"""你是一位专业的美股分析师。请基于以下数据对 {name} ({symbol}) 进行全面分析。 +**重要提示:当前日期是 {current_time},请在分析中使用这个日期,不要使用其他日期。** + 【基本信息】 股票代码:{symbol} 公司名称:{name} @@ -1577,7 +1579,8 @@ MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中' 2. 分析客观理性,基于数据和事实 3. 每个部分独立成段,段落间用空行分隔 4. 控制在500-600字 -5. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" +5. **不要在报告中添加日期标题,直接开始分析内容** +6. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。" """ try: @@ -1625,6 +1628,409 @@ 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股分析""" + # 指数映射 + index_mapping = { + "上证指数": "000001.SH", "上证": "000001.SH", "大盘": "000001.SH", "沪指": "000001.SH", + "深证成指": "399001.SZ", "深证": "399001.SZ", "深指": "399001.SZ", + "创业板指": "399006.SZ", "创业板": "399006.SZ", + "科创50": "000688.SH", "沪深300": "000300.SH", "中证500": "000905.SH", + "A股": "000001.SH" + } + + stock_code = None + stock_name = None + is_index = False + + for key, code in index_mapping.items(): + if key in stock_keyword or stock_keyword in key: + stock_code = code + stock_name = key if key in stock_keyword else stock_keyword + is_index = True + break + + if not is_index: + search_results = tushare_service.search_stock(stock_keyword) + if not search_results: + yield f"抱歉,未找到股票\"{stock_keyword}\"。请确认股票名称或代码是否正确。" + return + stock = search_results[0] + stock_code = stock['symbol'] + stock_name = stock['name'] + + # 获取数据(非流式) + 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(同步生成器) + stream = llm_service.chat_stream( + messages=[{"role": "user", "content": prompt}], + temperature=0.7, + max_tokens=2000 + ) + for chunk in stream: + 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(同步生成器) + stream = llm_service.chat_stream( + messages=[{"role": "user", "content": prompt}], + temperature=0.7, + max_tokens=2000 + ) + for chunk in stream: + yield chunk + # 创建全局实例 smart_agent = SmartStockAgent() diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py index 3ab1c9e..5a6da66 100644 --- a/backend/app/api/chat.py +++ b/backend/app/api/chat.py @@ -2,8 +2,11 @@ 对话API路由 """ from fastapi import APIRouter, HTTPException +from fastapi.responses import StreamingResponse from typing import Optional import uuid +import json +import asyncio from app.models.chat import ChatRequest, ChatResponse from app.agent.smart_agent import smart_agent # 使用智能Agent from app.utils.logger import logger @@ -65,3 +68,60 @@ async def get_history(session_id: str, limit: int = 50): except Exception as e: logger.error(f"获取历史失败: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/message/stream") +async def send_message_stream(request: ChatRequest): + """ + 流式发送消息给Agent + + Args: + request: 聊天请求 + + Returns: + Server-Sent Events 流式响应 + """ + try: + # 生成或使用现有session_id + session_id = request.session_id or str(uuid.uuid4()) + + async def event_generator(): + """生成SSE事件流""" + try: + # 发送session_id + yield f"data: {json.dumps({'type': 'session_id', 'session_id': session_id})}\n\n" + + # 添加小延迟确保数据被发送 + await asyncio.sleep(0.01) + + # 处理消息并流式返回 + async for chunk in smart_agent.process_message_stream( + message=request.message, + session_id=session_id, + user_id=request.user_id + ): + yield f"data: {json.dumps({'type': 'content', 'content': chunk})}\n\n" + # 添加小延迟,让浏览器有机会接收数据 + await asyncio.sleep(0.001) + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'done'})}\n\n" + + except Exception as e: + logger.error(f"流式处理消息失败: {e}") + yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + "Transfer-Encoding": "chunked" + } + ) + + except Exception as e: + logger.error(f"创建流式响应失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/services/llm_service.py b/backend/app/services/llm_service.py index 35da9fb..e8c7265 100644 --- a/backend/app/services/llm_service.py +++ b/backend/app/services/llm_service.py @@ -43,6 +43,29 @@ class LLMService: """使用LLM分析用户意图""" return self.multi_service.analyze_intent(user_message) + def chat_stream( + self, + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 2000 + ): + """ + 流式调用LLM进行对话 + + Args: + messages: 消息列表 + temperature: 温度参数 + max_tokens: 最大token数 + + Yields: + LLM响应的文本片段 + """ + return self.multi_service.chat_stream( + messages=messages, + temperature=temperature, + max_tokens=max_tokens + ) + # 创建全局实例 llm_service = LLMService() diff --git a/backend/app/services/multi_llm_service.py b/backend/app/services/multi_llm_service.py index eba4c58..ce5745c 100644 --- a/backend/app/services/multi_llm_service.py +++ b/backend/app/services/multi_llm_service.py @@ -170,6 +170,75 @@ class MultiLLMService: logger.error(f"详细错误: {traceback.format_exc()}") return None + def chat_stream( + self, + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 2000, + model_override: Optional[str] = None + ): + """ + 流式调用LLM进行对话 + + Args: + messages: 消息列表 + temperature: 温度参数 + max_tokens: 最大token数 + model_override: 临时覆盖使用的模型 + + Yields: + LLM响应的文本片段 + """ + provider = model_override or self.current_model + + if not provider or provider not in self.clients: + logger.error("没有可用的LLM客户端") + return + + try: + client = self.clients[provider] + model_id = self.model_info[provider]['model_id'] + + logger.info(f"流式调用LLM: provider={provider}, model={model_id}, messages={len(messages)}条") + + if provider == 'zhipu': + # 智谱AI流式调用 + response = client.chat.completions.create( + model=model_id, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + stream=True + ) + for chunk in response: + if chunk.choices and chunk.choices[0].delta.content: + yield chunk.choices[0].delta.content + + elif provider == 'deepseek': + # DeepSeek流式调用(OpenAI兼容) + response = client.chat.completions.create( + model=model_id, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + stream=True + ) + for chunk in response: + if chunk.choices and chunk.choices[0].delta.content: + yield chunk.choices[0].delta.content + + else: + logger.error(f"未知的模型提供商: {provider}") + return + + logger.info("LLM流式响应完成") + + except Exception as e: + logger.error(f"LLM流式调用失败: {type(e).__name__}: {e}") + import traceback + logger.error(f"详细错误: {traceback.format_exc()}") + return + def analyze_intent(self, user_message: str) -> Dict[str, Any]: """使用LLM分析用户意图""" if not self.current_model: diff --git a/frontend/css/style.css b/frontend/css/style.css index e402cf2..9ff75b9 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -314,6 +314,42 @@ html, body { flex-shrink: 0; } +/* Streaming Indicator */ +.streaming-indicator { + display: flex; + gap: 6px; + align-items: center; + margin-top: 12px; + padding: 8px 0; +} + +.streaming-indicator .dot { + width: 6px; + height: 6px; + background: var(--accent); + border-radius: 50%; + animation: pulse 1.4s infinite ease-in-out; +} + +.streaming-indicator .dot:nth-child(1) { + animation-delay: -0.32s; +} + +.streaming-indicator .dot:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes pulse { + 0%, 80%, 100% { + opacity: 0.3; + transform: scale(0.8); + } + 40% { + opacity: 1; + transform: scale(1); + } +} + /* Share Image Container (hidden, used for rendering) */ .share-image-container { position: fixed; diff --git a/frontend/index.html b/frontend/index.html index 5670bd0..4157710 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,7 +14,7 @@ - +