From 6bc7873bc3799a6d1c8a8c10a33e324739f57aa7 Mon Sep 17 00:00:00 2001 From: zhanggangyong Date: Thu, 5 Feb 2026 14:49:33 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E4=B8=89=E4=B8=AA=E7=BE=8E?= =?UTF-8?q?=E8=82=A1=20AI=20Agent=20=E6=B7=BB=E5=8A=A0=20/chat=20=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=EF=BC=8C=E6=94=AF=E6=8C=81=20LLM=20=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stock_quote_agent: 添加 chat 功能,可分析行情数据并给出投资建议 - stock_news_agent: 添加 chat 功能,可解读新闻并分析市场影响 - stock_analysis_agent: 添加 chat 功能,可进行技术分析并提供交易信号 Chat 端点使用方法: POST /chat { "message": "请帮我分析 AAPL 行情", "api_key": "your-llm-api-key" } LLM 配置支持环境变量: - LLM_BASE_URL: LLM 服务地址 - LLM_API_KEY: API 密钥 - LLM_MODEL: 模型名称 --- .../stock_analysis_agent.py | 113 +++++++++++++++++ .../stock_news_agent/stock_news_agent.py | 109 ++++++++++++++++ .../stock_quote_agent/stock_quote_agent.py | 117 ++++++++++++++++++ app.py | 4 +- k8s_manager.py | 4 - 5 files changed, 341 insertions(+), 6 deletions(-) diff --git a/agent_templates/agents/stock_analysis_agent/stock_analysis_agent.py b/agent_templates/agents/stock_analysis_agent/stock_analysis_agent.py index 171c139..f5f4ba6 100644 --- a/agent_templates/agents/stock_analysis_agent/stock_analysis_agent.py +++ b/agent_templates/agents/stock_analysis_agent/stock_analysis_agent.py @@ -36,6 +36,11 @@ SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) POD_NAME = os.getenv("POD_NAME", "stock-analysis-agent") USER_ID = os.getenv("USER_ID", "") +# LLM 配置 +LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1") +LLM_API_KEY = os.getenv("LLM_API_KEY", "") +LLM_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini") + # FastAPI 应用 app = FastAPI( title="Stock Analysis Agent", @@ -123,6 +128,20 @@ class HealthResponse(BaseModel): timestamp: str +class ChatRequest(BaseModel): + """Chat 请求""" + message: str = Field(..., description="用户消息") + api_key: Optional[str] = Field(None, description="LLM API Key") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatResponse(BaseModel): + """Chat 响应""" + response: str + data: Optional[Dict[str, Any]] = None + timestamp: str + + # ==================== 生命周期 ==================== @app.on_event("startup") @@ -563,6 +582,100 @@ async def stock_screener( } +# ==================== Chat 功能 ==================== + +async def chat_with_llm(message: str, context: str, api_key: str) -> str: + """调用 LLM 生成响应""" + try: + async with aiohttp.ClientSession() as session: + payload = { + "model": LLM_MODEL, + "messages": [ + { + "role": "system", + "content": """你是一个专业的美股技术分析师。你可以: +1. 分析股票技术指标(SMA, RSI, MACD, 布林带等) +2. 判断股票趋势(看涨/看跌/中性) +3. 提供买卖信号和投资建议 +4. 评估风险等级 + +请根据提供的技术分析数据,用简洁专业的语言回答用户问题。注意:投资有风险,建议仅供参考。""" + }, + { + "role": "user", + "content": f"技术分析数据:\n{context}\n\n用户问题: {message}" + } + ], + "max_tokens": 600, + "temperature": 0.7 + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + async with session.post( + f"{LLM_BASE_URL}/chat/completions", + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + if response.status == 200: + data = await response.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "抱歉,无法生成回复") + else: + error = await response.text() + logger.error(f"LLM 请求失败: {response.status} - {error}") + return f"LLM 服务错误: {response.status}" + except Exception as e: + logger.error(f"LLM 调用失败: {e}") + return f"调用失败: {str(e)}" + + +@app.post("/chat", response_model=ChatResponse) +async def chat(request: ChatRequest): + """智能对话 - 获取技术分析并提供投资建议""" + api_key = request.api_key or LLM_API_KEY + if not api_key: + raise HTTPException(status_code=400, detail="请提供 api_key 参数或设置 LLM_API_KEY 环境变量") + + # 从消息中提取股票代码 + import re + symbols = re.findall(r'\b([A-Z]{1,5})\b', request.message.upper()) + common_words = {"I", "A", "THE", "IS", "IT", "TO", "OF", "AND", "FOR", "IN", "ON", "AT", "BY", "BUY", "SELL"} + symbols = [s for s in symbols if s not in common_words][:3] + + if not symbols: + symbols = ["AAPL"] # 默认分析苹果 + + # 获取技术分析数据 + analysis_data = [] + for symbol in symbols: + analysis = await analyze_stock(symbol) + if analysis: + analysis_data.append(analysis) + + # 构建上下文 + if analysis_data: + context = "\n".join([ + f"{a['symbol']}: 价格${a['current_price']:.2f}, 趋势:{a['trend']}, " + f"信号:{a['signal']}, RSI:{a['indicators']['rsi']:.1f}, " + f"风险:{a['risk_level']}" + for a in analysis_data + ]) + else: + context = "暂无技术分析数据" + + # 调用 LLM 生成回复 + llm_response = await chat_with_llm(request.message, context, api_key) + + return ChatResponse( + response=llm_response, + data={"analysis": analysis_data, "symbols": symbols}, + timestamp=datetime.utcnow().isoformat() + ) + + # ==================== 主入口 ==================== def main(): diff --git a/agent_templates/agents/stock_news_agent/stock_news_agent.py b/agent_templates/agents/stock_news_agent/stock_news_agent.py index bd486f7..b8dfa43 100644 --- a/agent_templates/agents/stock_news_agent/stock_news_agent.py +++ b/agent_templates/agents/stock_news_agent/stock_news_agent.py @@ -39,6 +39,11 @@ USER_ID = os.getenv("USER_ID", "") # News API (可选) NEWS_API_KEY = os.getenv("NEWS_API_KEY", "") +# LLM 配置 +LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1") +LLM_API_KEY = os.getenv("LLM_API_KEY", "") +LLM_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini") + # FastAPI 应用 app = FastAPI( title="Stock News Agent", @@ -105,6 +110,20 @@ class HealthResponse(BaseModel): timestamp: str +class ChatRequest(BaseModel): + """Chat 请求""" + message: str = Field(..., description="用户消息") + api_key: Optional[str] = Field(None, description="LLM API Key") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatResponse(BaseModel): + """Chat 响应""" + response: str + data: Optional[Dict[str, Any]] = None + timestamp: str + + # ==================== 生命周期 ==================== @app.on_event("startup") @@ -390,6 +409,96 @@ async def get_trending_news(): ) +# ==================== Chat 功能 ==================== + +async def chat_with_llm(message: str, context: str, api_key: str) -> str: + """调用 LLM 生成响应""" + try: + async with aiohttp.ClientSession() as session: + payload = { + "model": LLM_MODEL, + "messages": [ + { + "role": "system", + "content": """你是一个专业的美股新闻分析师。你可以: +1. 获取和分析股票相关新闻 +2. 解读市场动态和公司公告 +3. 提供新闻情感分析和市场影响评估 + +请根据提供的新闻数据,用简洁专业的语言回答用户问题。""" + }, + { + "role": "user", + "content": f"最新新闻:\n{context}\n\n用户问题: {message}" + } + ], + "max_tokens": 500, + "temperature": 0.7 + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + async with session.post( + f"{LLM_BASE_URL}/chat/completions", + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + if response.status == 200: + data = await response.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "抱歉,无法生成回复") + else: + error = await response.text() + logger.error(f"LLM 请求失败: {response.status} - {error}") + return f"LLM 服务错误: {response.status}" + except Exception as e: + logger.error(f"LLM 调用失败: {e}") + return f"调用失败: {str(e)}" + + +@app.post("/chat", response_model=ChatResponse) +async def chat(request: ChatRequest): + """智能对话 - 获取新闻并提供分析""" + api_key = request.api_key or LLM_API_KEY + if not api_key: + raise HTTPException(status_code=400, detail="请提供 api_key 参数或设置 LLM_API_KEY 环境变量") + + # 从消息中提取关键词 + import re + symbols = re.findall(r'\b([A-Z]{1,5})\b', request.message.upper()) + common_words = {"I", "A", "THE", "IS", "IT", "TO", "OF", "AND", "FOR", "IN", "ON", "AT", "BY", "NEWS", "WHAT"} + symbols = [s for s in symbols if s not in common_words][:3] + + # 获取相关新闻 + news_data = [] + if symbols: + for symbol in symbols: + data = await fetch_yahoo_news(symbol=symbol, limit=3) + news_data.extend(data) + else: + news_data = await fetch_yahoo_news(query="stock market", limit=5) + + # 构建上下文 + if news_data: + context = "\n".join([ + f"- {item['title']} ({item['source']}, {item['published_at'][:10]})" + for item in news_data[:5] + ]) + else: + context = "暂无相关新闻" + + # 调用 LLM 生成回复 + llm_response = await chat_with_llm(request.message, context, api_key) + + return ChatResponse( + response=llm_response, + data={"news_count": len(news_data), "symbols": symbols}, + timestamp=datetime.utcnow().isoformat() + ) + + # ==================== 主入口 ==================== def main(): diff --git a/agent_templates/agents/stock_quote_agent/stock_quote_agent.py b/agent_templates/agents/stock_quote_agent/stock_quote_agent.py index 8f7d96c..0ab34be 100644 --- a/agent_templates/agents/stock_quote_agent/stock_quote_agent.py +++ b/agent_templates/agents/stock_quote_agent/stock_quote_agent.py @@ -40,6 +40,11 @@ USER_ID = os.getenv("USER_ID", "") RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY", "") YAHOO_FINANCE_HOST = "yahoo-finance15.p.rapidapi.com" +# LLM 配置 +LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1") +LLM_API_KEY = os.getenv("LLM_API_KEY", "") +LLM_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini") + # FastAPI 应用 app = FastAPI( title="Stock Quote Agent", @@ -105,6 +110,20 @@ class HealthResponse(BaseModel): timestamp: str +class ChatRequest(BaseModel): + """Chat 请求""" + message: str = Field(..., description="用户消息") + api_key: Optional[str] = Field(None, description="LLM API Key") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatResponse(BaseModel): + """Chat 响应""" + response: str + data: Optional[Dict[str, Any]] = None + timestamp: str + + # ==================== 生命周期 ==================== @app.on_event("startup") @@ -336,6 +355,104 @@ async def get_popular_stocks(): return await batch_get_quotes(request) +async def chat_with_llm(message: str, context: str, api_key: str) -> str: + """调用 LLM 生成响应""" + try: + async with aiohttp.ClientSession() as session: + payload = { + "model": LLM_MODEL, + "messages": [ + { + "role": "system", + "content": """你是一个专业的美股分析助手。你可以: +1. 查询股票实时行情(价格、涨跌幅、成交量) +2. 分析股票数据并给出建议 +3. 解答关于美股市场的问题 + +请根据提供的股票数据,用简洁专业的语言回答用户问题。""" + }, + { + "role": "user", + "content": f"当前股票数据:\n{context}\n\n用户问题: {message}" + } + ], + "max_tokens": 500, + "temperature": 0.7 + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + async with session.post( + f"{LLM_BASE_URL}/chat/completions", + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + if response.status == 200: + data = await response.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "抱歉,无法生成回复") + else: + error = await response.text() + logger.error(f"LLM 请求失败: {response.status} - {error}") + return f"LLM 服务错误: {response.status}" + except Exception as e: + logger.error(f"LLM 调用失败: {e}") + return f"调用失败: {str(e)}" + + +def extract_symbols_from_message(message: str) -> List[str]: + """从消息中提取股票代码""" + import re + # 匹配常见美股代码格式(1-5个大写字母) + symbols = re.findall(r'\b([A-Z]{1,5})\b', message.upper()) + # 过滤常见词汇 + common_words = {"I", "A", "THE", "IS", "IT", "TO", "OF", "AND", "FOR", "IN", "ON", "AT", "BY"} + return [s for s in symbols if s not in common_words][:5] # 最多5个 + + +@app.post("/chat", response_model=ChatResponse) +async def chat(request: ChatRequest): + """智能对话 - 支持自然语言查询股票信息""" + api_key = request.api_key or LLM_API_KEY + if not api_key: + raise HTTPException(status_code=400, detail="请提供 api_key 参数或设置 LLM_API_KEY 环境变量") + + # 从消息中提取股票代码 + symbols = extract_symbols_from_message(request.message) + + # 如果没有提取到,默认查询热门股票 + if not symbols: + symbols = ["AAPL", "TSLA", "NVDA"] + + # 获取股票数据 + stock_data = [] + for symbol in symbols: + result = await fetch_stock_quote(symbol) + if result.get("success"): + stock_data.append(result) + + # 构建上下文 + if stock_data: + context = "\n".join([ + f"{d['symbol']} ({d['name']}): ${d['price']:.2f}, 涨跌: {d['change_percent']:+.2f}%, " + f"成交量: {d['volume']:,}, 52周范围: ${d.get('low_52week', 0):.2f}-${d.get('high_52week', 0):.2f}" + for d in stock_data + ]) + else: + context = "暂无股票数据" + + # 调用 LLM 生成回复 + llm_response = await chat_with_llm(request.message, context, api_key) + + return ChatResponse( + response=llm_response, + data={"stocks": stock_data, "symbols_detected": symbols}, + timestamp=datetime.utcnow().isoformat() + ) + + # ==================== 主入口 ==================== def main(): diff --git a/app.py b/app.py index 5860da6..9db29dc 100644 --- a/app.py +++ b/app.py @@ -389,8 +389,8 @@ async def create_agent(request: CreateAgentRequest, db: Session = Depends(get_db config_data=config_data ) - # 步骤3: 获取服务端口 - service_port = k8s_manager.TEMPLATE_PORTS.get(request.template) + # 步骤3: 获取服务端口(从数据库动态获取) + service_port = template_manager.get_port(request.template) # 步骤4: 创建 LoadBalancer Service(AKS 会自动分配外网 IP) service_info = None diff --git a/k8s_manager.py b/k8s_manager.py index a3d3962..5f77f31 100644 --- a/k8s_manager.py +++ b/k8s_manager.py @@ -331,10 +331,6 @@ class K8sManager: "microsoft_learn_agent": 8000, "aws_docs_mcp": 8000, "google_mcp": 8000, - # 美股 Agent - "stock_quote_agent": 8080, - "stock_news_agent": 8080, - "stock_analysis_agent": 8080, } # 模板所需环境变量说明