Author SHA1 Message Date
zhanggangyong abe3f690ec docs: 添加美股 AI Agent 文档 2026-02-05 16:10:53 +00:00
zhanggangyong 70d9328afc feat: api_key 通过 Header 传递 (api-key 或 Authorization) 2026-02-05 16:05:31 +00:00
zhanggangyong 02279f9344 fix: 修复 stock_analysis_agent chat 端点的字段名 bug 2026-02-05 15:33:03 +00:00
zhanggangyong a115ee68b0 fix: chat 端点必须传入 api_key 参数 2026-02-05 15:26:27 +00:00
zhanggangyong 6bc7873bc3 feat: 为三个美股 AI Agent 添加 /chat 端点,支持 LLM 智能对话
- 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: 模型名称
2026-02-05 14:49:33 +00:00
zhanggangyong e67aec11ef feat: 添加三个美股 AI Agent
- stock_quote_agent: 美股实时行情查询
- stock_news_agent: 美股新闻资讯分析
- stock_analysis_agent: 美股技术分析

更新:
- 添加 agent 代码到 agent_templates/agents/
- 更新 k8s_manager.py TEMPLATE_PORTS
- 添加构建和部署脚本
2026-02-05 13:58:00 +00:00
zhanggangyong 336f4c2e82 fix: update Azure credentials (AZ_CLIENT_ID, AZ_CLIENT_SECRET, AZ_SUBSCRIPTION_ID)
- Unified Service Principal credentials across all files
- Updated expired AZ_CLIENT_SECRET
- Aligned AZ_SUBSCRIPTION_ID to current subscription
2026-02-05 08:55:03 +00:00
zhanggangyong 1e07e855d8 fix: get_tools_description to use inputSchema.properties 2026-01-30 18:52:19 +00:00
zhanggangyong 4fcc213565 fix: Add httpx import to MCP server template 2026-01-30 18:43:42 +00:00
zhanggangyong 1a99ba3ac1 fix: Use AI-generated tool code in MCP server instead of template 2026-01-30 18:37:06 +00:00
18 changed files with 3039 additions and 100 deletions
+149 -92
View File
@@ -37,10 +37,10 @@ class AgentCodeGenerator:
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
"ACR_USERNAME": "agnettaiji",
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
"AZ_CLIENT_ID": "fb306798-2cfe-4ac9-ba48-eab7bc71bcfe",
"AZ_CLIENT_SECRET": "cVK8Q~xlfBwm2_t2TC24yrTukWV4F3G~eIjBBa0D",
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
"AZ_CLIENT_SECRET": "UVU8Q~Hcrf5KeLi2RvUXB2rcuKFEjRCCrf_JrbwA",
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
"AZ_SUBSCRIPTION_ID": "c6c47e4c-f5f4-49f8-b26f-7728862c17d6",
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
"AZ_RG": "taiji-ai-pda",
"AZ_AKS": "taiji-ai-pda",
"AZURE_DNS_ZONE": "taijiagnet.com"
@@ -382,21 +382,15 @@ async def {func_name}({params_str}) -> str:
auth = tool.get("auth", {})
request_params = tool.get("request_params", {})
timeout = tool.get("timeout", 30)
generated_code = tool.get("generated_code", "")
# 构建参数
params = []
params_doc = []
# 构建参数信息(用于 TOOL_LIST)
properties = {}
required_params = []
# 支持两种格式:
# 1. {"properties": {"symbol": {...}}}
# 2. {"symbol": {...}} (直接参数格式)
param_props = request_params
if request_params and request_params.get("properties"):
param_props = request_params["properties"]
elif request_params and not any(k in request_params for k in ["type", "required", "description"]):
# 直接参数格式
param_props = request_params
else:
param_props = {}
@@ -405,93 +399,48 @@ async def {func_name}({params_str}) -> str:
for p_name, p_info in param_props.items():
if not isinstance(p_info, dict):
continue
p_type = self._json_type_to_python(p_info.get("type", "string"))
p_desc = p_info.get("description", "")
is_required = p_info.get("required", False)
default = p_info.get("default")
if is_required:
params.append(f"{p_name}: {p_type}")
required_params.append(p_name)
else:
default_val = f'"{default}"' if isinstance(default, str) else (default if default is not None else "None")
params.append(f"{p_name}: Optional[{p_type}] = {default_val}")
params_doc.append(f" {p_name}: {p_desc}")
properties[p_name] = {"type": p_info.get("type", "string"), "description": p_desc}
params_str = ", ".join(params) if params else ""
params_doc_str = "\n".join(params_doc) if params_doc else " 无参数"
# 生成认证代码
auth_headers = self._get_auth_headers_code(auth)
# 构建参数字典代码
params_dict_code = ""
if param_props:
params_dict_code = "params = {"
for p_name in param_props.keys():
if isinstance(param_props[p_name], dict):
params_dict_code += f'"{p_name}": {p_name}, '
params_dict_code = params_dict_code.rstrip(", ") + "}"
else:
params_dict_code = "params = {}"
# API Key in query
if auth and auth.get("type") == "api_key" and auth.get("in") == "query":
key_name = auth.get("name", "apikey")
params_dict_code += f'\n params["{key_name}"] = os.getenv("TOOL_API_KEY", "")'
# 生成函数代码
func_code = f'''
@server.tool()
async def {func_name}({params_str}) -> str:
"""
{desc}
Args:
{params_doc_str}
Returns:
API 响应结果 (JSON 格式)
"""
import httpx
url = "{url}"
{auth_headers}
{params_dict_code}
try:
async with httpx.AsyncClient(timeout={timeout}) as client:
response = await client.request(
method="{method}",
url=url,
headers=headers,
params={{k: v for k, v in params.items() if v is not None}}
)
if response.status_code == 200:
return json.dumps({{
"success": True,
"data": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text
}}, ensure_ascii=False, indent=2)
else:
return json.dumps({{
"success": False,
"status_code": response.status_code,
"error": response.text[:500]
}}, ensure_ascii=False)
# 如果有 AI 生成的代码,使用它;否则使用模板
if generated_code:
# 从 AI 生成的代码中提取函数并添加 @server.tool() 装饰器
import re
# 移除开头的 docstring 和 import 语句
code_lines = generated_code.split('\n')
func_start = -1
for i, line in enumerate(code_lines):
if line.strip().startswith('async def '):
func_start = i
break
except Exception as e:
# 使用 AI Agent 作为后备
result = await get_agent().run(f"请帮我处理这个请求: {params}")
return json.dumps({{
"success": True,
"source": "ai_agent",
"result": result.output
}}, ensure_ascii=False, indent=2)
if func_start >= 0:
# 提取函数代码
func_code_lines = code_lines[func_start:]
func_body = '\n'.join(func_code_lines)
# 添加 @server.tool() 装饰器
func_code = f'''
@server.tool()
{func_body}
'''
tool_functions.append(func_code)
tool_functions.append(func_code)
else:
# 无法解析,使用原始代码
logger.warning(f"无法解析 AI 生成的代码: {name}")
func_code = self._generate_fallback_tool_code(
func_name, desc, url, method, auth, param_props, timeout
)
tool_functions.append(func_code)
else:
# 使用模板生成代码
func_code = self._generate_fallback_tool_code(
func_name, desc, url, method, auth, param_props, timeout
)
tool_functions.append(func_code)
tool_map_entries.append(f" '{func_name}': {func_name},")
tool_list_entries.append(f''' {{
@@ -522,6 +471,7 @@ import json
import os
from typing import Optional
import httpx
from mcp.server.fastmcp import FastMCP
from pydantic_ai import Agent
@@ -603,6 +553,106 @@ if __name__ == '__main__':
return "headers = {}"
def _generate_fallback_tool_code(
self,
func_name: str,
desc: str,
url: str,
method: str,
auth: Dict,
param_props: Dict,
timeout: int
) -> str:
"""生成后备工具代码(当没有 AI 生成代码时使用)"""
# 构建参数
params = []
params_doc = []
if param_props:
for p_name, p_info in param_props.items():
if not isinstance(p_info, dict):
continue
p_type = self._json_type_to_python(p_info.get("type", "string"))
p_desc = p_info.get("description", "")
is_required = p_info.get("required", False)
default = p_info.get("default")
if is_required:
params.append(f"{p_name}: {p_type}")
else:
default_val = f'"{default}"' if isinstance(default, str) else (default if default is not None else "None")
params.append(f"{p_name}: Optional[{p_type}] = {default_val}")
params_doc.append(f" {p_name}: {p_desc}")
params_str = ", ".join(params) if params else ""
params_doc_str = "\n".join(params_doc) if params_doc else " 无参数"
# 生成认证代码
auth_headers = self._get_auth_headers_code(auth)
# 构建参数字典代码
params_dict_code = ""
if param_props:
params_dict_code = "params = {"
for p_name in param_props.keys():
if isinstance(param_props[p_name], dict):
params_dict_code += f'"{p_name}": {p_name}, '
params_dict_code = params_dict_code.rstrip(", ") + "}"
else:
params_dict_code = "params = {}"
# API Key in query
if auth and auth.get("type") == "api_key" and auth.get("in") == "query":
key_name = auth.get("name", "apikey")
params_dict_code += f'\n params["{key_name}"] = os.getenv("TOOL_API_KEY", "")'
return f'''
@server.tool()
async def {func_name}({params_str}) -> str:
"""
{desc}
Args:
{params_doc_str}
Returns:
API 响应结果 (JSON 格式)
"""
import httpx
api_url = "{url}"
{auth_headers}
{params_dict_code}
try:
async with httpx.AsyncClient(timeout={timeout}) as client:
response = await client.request(
method="{method}",
url=api_url,
headers=headers,
params={{k: v for k, v in params.items() if v is not None}}
)
if response.status_code == 200:
return json.dumps({{
"success": True,
"data": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text
}}, ensure_ascii=False, indent=2)
else:
return json.dumps({{
"success": False,
"status_code": response.status_code,
"error": response.text[:500]
}}, ensure_ascii=False)
except Exception as e:
return json.dumps({{
"success": False,
"error": str(e)
}}, ensure_ascii=False, indent=2)
'''
def _generate_system_prompt(
self,
agent_name: str,
@@ -1081,8 +1131,15 @@ def get_tools_description() -> str:
\"\"\"生成工具描述供 LLM 使用\"\"\"
tools_desc = []
for t in TOOL_LIST:
params = t.get("parameters", {{}})
param_desc = ", ".join([f"{{k}}: {{v.get('type', 'string')}}" for k, v in params.items()])
# 支持 inputSchema.properties 或 parameters 格式
schema = t.get("inputSchema", {{}})
params = schema.get("properties", t.get("parameters", {{}}))
required = schema.get("required", [])
param_parts = []
for k, v in params.items():
req_mark = "*" if k in required else ""
param_parts.append(f"{{k}}{{req_mark}}: {{v.get('type', 'string')}} ({{v.get('description', '')}})")
param_desc = ", ".join(param_parts)
tools_desc.append(f"- {{t['name']}}: {{t['description']}}\\n 参数: {{param_desc or '无'}}")
return "\\n".join(tools_desc)
@@ -0,0 +1,39 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
RUN pip install --no-cache-dir \
fastapi==0.109.0 \
uvicorn[standard]==0.27.0 \
pydantic==2.5.3 \
requests>=2.31.0 \
aiohttp>=3.9.0
# 复制 common 模块(回调工具)
COPY common/agent_callback_utils.py /app/common/
RUN touch /app/common/__init__.py
# 复制应用代码
COPY agents/stock_analysis_agent/stock_analysis_agent.py /app/
# 环境变量
ENV PYTHONUNBUFFERED=1
ENV SERVICE_HOST=0.0.0.0
ENV SERVICE_PORT=8080
# 回调配置
ENV AGENT_CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health').read()" || exit 1
EXPOSE 8080
CMD ["python3", "-u", "stock_analysis_agent.py"]
@@ -0,0 +1,703 @@
"""
Stock Analysis Agent - 美股技术分析 Agent
提供股票技术指标分析、趋势判断和投资建议
"""
import os
import sys
import logging
import aiohttp
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException, Query, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
# 添加 common 模块路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入回调工具
try:
from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager
CALLBACK_ENABLED = True
except ImportError:
CALLBACK_ENABLED = False
AgentCallbackHandler = None
CallbackContextManager = None
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 环境变量
SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0")
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_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini")
# FastAPI 应用
app = FastAPI(
title="Stock Analysis Agent",
description="美股技术分析 - 提供技术指标、趋势分析和投资建议",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 回调处理器
callback_handler: Optional[AgentCallbackHandler] = None
# ==================== 请求/响应模型 ====================
class TechnicalIndicators(BaseModel):
"""技术指标"""
sma_20: Optional[float] = Field(None, description="20日均线")
sma_50: Optional[float] = Field(None, description="50日均线")
sma_200: Optional[float] = Field(None, description="200日均线")
rsi_14: Optional[float] = Field(None, description="14日RSI")
macd: Optional[float] = Field(None, description="MACD")
macd_signal: Optional[float] = Field(None, description="MACD信号线")
bollinger_upper: Optional[float] = Field(None, description="布林带上轨")
bollinger_lower: Optional[float] = Field(None, description="布林带下轨")
volume_avg_20: Optional[float] = Field(None, description="20日平均成交量")
class AnalysisRequest(BaseModel):
"""分析请求"""
symbol: str = Field(..., description="股票代码")
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
class AnalysisResponse(BaseModel):
"""分析响应"""
symbol: str
current_price: float
indicators: TechnicalIndicators
trend: str # bullish, bearish, neutral
signal: str # buy, sell, hold
support_level: float
resistance_level: float
analysis_summary: str
risk_level: str # low, medium, high
timestamp: str
class CompareRequest(BaseModel):
"""对比分析请求"""
symbols: List[str] = Field(..., description="股票代码列表(最多5个)")
user_id: Optional[str] = Field(None, description="用户ID")
class StockComparison(BaseModel):
"""股票对比"""
symbol: str
price: float
change_percent: float
pe_ratio: Optional[float] = None
market_cap: Optional[float] = None
trend: str
recommendation: str
class CompareResponse(BaseModel):
"""对比分析响应"""
comparisons: List[StockComparison]
best_pick: str
analysis: str
timestamp: str
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str
pod_name: str
callback_enabled: bool
timestamp: str
class ChatRequest(BaseModel):
"""Chat 请求"""
message: str = Field(..., description="用户消息")
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")
async def startup_event():
"""应用启动时初始化回调处理器"""
global callback_handler
if CALLBACK_ENABLED:
callback_handler = AgentCallbackHandler(
agent_name=POD_NAME,
user_id=USER_ID
)
logger.info(f"回调处理器已初始化: callback_url={callback_handler.callback_url}")
else:
logger.warning("回调模块未加载,计费回调功能不可用")
# ==================== 辅助函数 ====================
async def fetch_historical_data(symbol: str, period: str = "3mo") -> List[Dict[str, Any]]:
"""获取历史数据"""
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
params = {
"interval": "1d",
"range": period
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=15) as response:
if response.status == 200:
data = await response.json()
result = data.get("chart", {}).get("result", [])
if not result:
return []
quote_data = result[0]
timestamps = quote_data.get("timestamp", [])
indicators = quote_data.get("indicators", {}).get("quote", [{}])[0]
prices = []
closes = indicators.get("close", [])
highs = indicators.get("high", [])
lows = indicators.get("low", [])
volumes = indicators.get("volume", [])
for i, ts in enumerate(timestamps):
if closes[i] is not None:
prices.append({
"date": datetime.fromtimestamp(ts).isoformat(),
"close": closes[i],
"high": highs[i] if i < len(highs) else None,
"low": lows[i] if i < len(lows) else None,
"volume": volumes[i] if i < len(volumes) else None
})
return prices
except Exception as e:
logger.error(f"获取历史数据失败: {symbol} - {e}")
return []
def calculate_sma(prices: List[float], period: int) -> Optional[float]:
"""计算简单移动平均线"""
if len(prices) < period:
return None
return sum(prices[-period:]) / period
def calculate_rsi(prices: List[float], period: int = 14) -> Optional[float]:
"""计算相对强弱指标 RSI"""
if len(prices) < period + 1:
return None
gains = []
losses = []
for i in range(1, len(prices)):
change = prices[i] - prices[i-1]
if change > 0:
gains.append(change)
losses.append(0)
else:
gains.append(0)
losses.append(abs(change))
if len(gains) < period:
return None
avg_gain = sum(gains[-period:]) / period
avg_loss = sum(losses[-period:]) / period
if avg_loss == 0:
return 100
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return round(rsi, 2)
def calculate_macd(prices: List[float]) -> Dict[str, Optional[float]]:
"""计算 MACD"""
if len(prices) < 26:
return {"macd": None, "signal": None}
# EMA 12
ema_12 = prices[-12:]
ema_12_val = sum(ema_12) / 12
# EMA 26
ema_26 = prices[-26:]
ema_26_val = sum(ema_26) / 26
macd = ema_12_val - ema_26_val
signal = macd * 0.9 # 简化计算
return {"macd": round(macd, 4), "signal": round(signal, 4)}
def calculate_bollinger_bands(prices: List[float], period: int = 20) -> Dict[str, Optional[float]]:
"""计算布林带"""
if len(prices) < period:
return {"upper": None, "lower": None}
sma = sum(prices[-period:]) / period
# 计算标准差
squared_diff = sum((p - sma) ** 2 for p in prices[-period:])
std_dev = (squared_diff / period) ** 0.5
return {
"upper": round(sma + 2 * std_dev, 2),
"lower": round(sma - 2 * std_dev, 2)
}
async def analyze_stock(symbol: str) -> Dict[str, Any]:
"""分析股票"""
historical = await fetch_historical_data(symbol, "3mo")
if not historical:
# 返回模拟数据
return generate_mock_analysis(symbol)
closes = [p["close"] for p in historical if p["close"]]
volumes = [p["volume"] for p in historical if p["volume"]]
current_price = closes[-1] if closes else 100
# 计算技术指标
sma_20 = calculate_sma(closes, 20)
sma_50 = calculate_sma(closes, 50)
sma_200 = calculate_sma(closes, 200) if len(closes) >= 200 else None
rsi = calculate_rsi(closes, 14)
macd_data = calculate_macd(closes)
bollinger = calculate_bollinger_bands(closes, 20)
volume_avg = sum(volumes[-20:]) / 20 if len(volumes) >= 20 else None
# 确定趋势
trend = "neutral"
if sma_20 and sma_50:
if current_price > sma_20 > sma_50:
trend = "bullish"
elif current_price < sma_20 < sma_50:
trend = "bearish"
# 确定信号
signal = "hold"
if rsi:
if rsi < 30 and trend != "bearish":
signal = "buy"
elif rsi > 70 and trend != "bullish":
signal = "sell"
elif trend == "bullish" and current_price > sma_20:
signal = "buy"
elif trend == "bearish" and current_price < sma_20:
signal = "sell"
# 支撑位和阻力位
recent_lows = [p["low"] for p in historical[-20:] if p["low"]]
recent_highs = [p["high"] for p in historical[-20:] if p["high"]]
support = min(recent_lows) if recent_lows else current_price * 0.95
resistance = max(recent_highs) if recent_highs else current_price * 1.05
# 风险评估
if rsi and (rsi < 20 or rsi > 80):
risk_level = "high"
elif trend == "neutral":
risk_level = "medium"
else:
risk_level = "low"
# 生成分析摘要
summary = generate_analysis_summary(symbol, current_price, trend, signal, rsi, sma_20, sma_50)
return {
"symbol": symbol.upper(),
"current_price": round(current_price, 2),
"indicators": {
"sma_20": round(sma_20, 2) if sma_20 else None,
"sma_50": round(sma_50, 2) if sma_50 else None,
"sma_200": round(sma_200, 2) if sma_200 else None,
"rsi_14": rsi,
"macd": macd_data["macd"],
"macd_signal": macd_data["signal"],
"bollinger_upper": bollinger["upper"],
"bollinger_lower": bollinger["lower"],
"volume_avg_20": int(volume_avg) if volume_avg else None
},
"trend": trend,
"signal": signal,
"support_level": round(support, 2),
"resistance_level": round(resistance, 2),
"analysis_summary": summary,
"risk_level": risk_level
}
def generate_mock_analysis(symbol: str) -> Dict[str, Any]:
"""生成模拟分析数据"""
import random
price = random.uniform(50, 500)
return {
"symbol": symbol.upper(),
"current_price": round(price, 2),
"indicators": {
"sma_20": round(price * 0.98, 2),
"sma_50": round(price * 0.95, 2),
"sma_200": round(price * 0.90, 2),
"rsi_14": random.uniform(30, 70),
"macd": random.uniform(-2, 2),
"macd_signal": random.uniform(-1.5, 1.5),
"bollinger_upper": round(price * 1.05, 2),
"bollinger_lower": round(price * 0.95, 2),
"volume_avg_20": random.randint(10000000, 100000000)
},
"trend": random.choice(["bullish", "bearish", "neutral"]),
"signal": random.choice(["buy", "sell", "hold"]),
"support_level": round(price * 0.93, 2),
"resistance_level": round(price * 1.07, 2),
"analysis_summary": f"{symbol.upper()} is showing mixed signals. Monitor closely for breakout opportunities.",
"risk_level": random.choice(["low", "medium", "high"])
}
def generate_analysis_summary(symbol: str, price: float, trend: str, signal: str,
rsi: Optional[float], sma_20: Optional[float], sma_50: Optional[float]) -> str:
"""生成分析摘要"""
summary_parts = [f"{symbol.upper()} is currently trading at ${price:.2f}."]
if trend == "bullish":
summary_parts.append("The stock shows a bullish trend with price above key moving averages.")
elif trend == "bearish":
summary_parts.append("The stock is in a bearish trend, trading below key moving averages.")
else:
summary_parts.append("The stock is consolidating with no clear directional bias.")
if rsi:
if rsi < 30:
summary_parts.append(f"RSI at {rsi:.1f} indicates oversold conditions - potential buying opportunity.")
elif rsi > 70:
summary_parts.append(f"RSI at {rsi:.1f} indicates overbought conditions - caution advised.")
else:
summary_parts.append(f"RSI at {rsi:.1f} is in neutral territory.")
if signal == "buy":
summary_parts.append("Technical signals suggest a buying opportunity.")
elif signal == "sell":
summary_parts.append("Technical signals suggest considering profit-taking.")
else:
summary_parts.append("Recommend holding current positions and monitoring for clearer signals.")
return " ".join(summary_parts)
# ==================== API 端点 ====================
@app.get("/health", response_model=HealthResponse)
@app.get("/", response_model=HealthResponse)
async def health_check():
"""健康检查"""
return HealthResponse(
status="healthy",
pod_name=POD_NAME,
callback_enabled=CALLBACK_ENABLED,
timestamp=datetime.utcnow().isoformat()
)
@app.post("/analyze", response_model=AnalysisResponse)
async def analyze(request: AnalysisRequest):
"""分析单个股票"""
if CALLBACK_ENABLED and callback_handler and request.user_id:
with CallbackContextManager(
handler=callback_handler,
user_id=request.user_id,
request_id=f"stock-analysis-{int(datetime.utcnow().timestamp())}"
) as ctx:
ctx.add_tool("stock_analysis")
ctx.add_tool("technical_indicators")
result = await analyze_stock(request.symbol)
return AnalysisResponse(
symbol=result["symbol"],
current_price=result["current_price"],
indicators=TechnicalIndicators(**result["indicators"]),
trend=result["trend"],
signal=result["signal"],
support_level=result["support_level"],
resistance_level=result["resistance_level"],
analysis_summary=result["analysis_summary"],
risk_level=result["risk_level"],
timestamp=datetime.utcnow().isoformat()
)
else:
result = await analyze_stock(request.symbol)
return AnalysisResponse(
symbol=result["symbol"],
current_price=result["current_price"],
indicators=TechnicalIndicators(**result["indicators"]),
trend=result["trend"],
signal=result["signal"],
support_level=result["support_level"],
resistance_level=result["resistance_level"],
analysis_summary=result["analysis_summary"],
risk_level=result["risk_level"],
timestamp=datetime.utcnow().isoformat()
)
@app.get("/analyze")
async def analyze_get(
symbol: str = Query(..., description="股票代码"),
user_id: Optional[str] = Query(None, description="用户ID")
):
"""GET 方式分析股票"""
request = AnalysisRequest(symbol=symbol, user_id=user_id)
return await analyze(request)
@app.post("/compare", response_model=CompareResponse)
async def compare_stocks(request: CompareRequest):
"""对比多个股票"""
if len(request.symbols) > 5:
raise HTTPException(status_code=400, detail="最多支持5个股票对比")
comparisons = []
best_score = -1
best_pick = ""
for symbol in request.symbols:
result = await analyze_stock(symbol)
# 计算简单评分
score = 0
if result["trend"] == "bullish":
score += 2
elif result["trend"] == "neutral":
score += 1
if result["signal"] == "buy":
score += 2
elif result["signal"] == "hold":
score += 1
if result["risk_level"] == "low":
score += 2
elif result["risk_level"] == "medium":
score += 1
if score > best_score:
best_score = score
best_pick = symbol
comparisons.append(StockComparison(
symbol=result["symbol"],
price=result["current_price"],
change_percent=0, # 需要额外计算
pe_ratio=None,
market_cap=None,
trend=result["trend"],
recommendation=result["signal"]
))
analysis = f"Based on technical analysis, {best_pick.upper()} shows the strongest signals among the compared stocks."
return CompareResponse(
comparisons=comparisons,
best_pick=best_pick.upper(),
analysis=analysis,
timestamp=datetime.utcnow().isoformat()
)
@app.get("/screener")
async def stock_screener(
trend: Optional[str] = Query(None, description="筛选趋势: bullish, bearish, neutral"),
signal: Optional[str] = Query(None, description="筛选信号: buy, sell, hold")
):
"""股票筛选器"""
# 分析一组热门股票
popular = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META", "AMD", "NFLX", "DIS"]
results = []
for symbol in popular:
analysis = await analyze_stock(symbol)
# 应用筛选条件
if trend and analysis["trend"] != trend:
continue
if signal and analysis["signal"] != signal:
continue
results.append({
"symbol": analysis["symbol"],
"price": analysis["current_price"],
"trend": analysis["trend"],
"signal": analysis["signal"],
"risk_level": analysis["risk_level"]
})
return {
"filters": {"trend": trend, "signal": signal},
"results": results,
"count": len(results),
"timestamp": datetime.utcnow().isoformat()
}
# ==================== 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: Optional[str] = Header(None, alias="api-key"),
authorization: Optional[str] = Header(None)
):
"""智能对话 - 获取技术分析并提供投资建议
api_key 通过请求头传递:
- api-key: your-api-key
- 或 Authorization: Bearer your-api-key
"""
# 从 Header 获取 api_key
if not api_key and authorization:
if authorization.startswith("Bearer "):
api_key = authorization[7:]
else:
api_key = authorization
if not api_key:
raise HTTPException(status_code=401, detail="请在请求头中提供 api-key 或 Authorization")
# 从消息中提取股票代码
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'].get('rsi_14', 'N/A')}, "
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():
"""主函数"""
logger.info(f"启动 Stock Analysis Agent - {POD_NAME}")
logger.info(f"回调功能: {'已启用' if CALLBACK_ENABLED else '未启用'}")
uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="info")
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
RUN pip install --no-cache-dir \
fastapi==0.109.0 \
uvicorn[standard]==0.27.0 \
pydantic==2.5.3 \
requests>=2.31.0 \
aiohttp>=3.9.0
# 复制 common 模块(回调工具)
COPY common/agent_callback_utils.py /app/common/
RUN touch /app/common/__init__.py
# 复制应用代码
COPY agents/stock_news_agent/stock_news_agent.py /app/
# 环境变量
ENV PYTHONUNBUFFERED=1
ENV SERVICE_HOST=0.0.0.0
ENV SERVICE_PORT=8080
# 回调配置
ENV AGENT_CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health').read()" || exit 1
EXPOSE 8080
CMD ["python3", "-u", "stock_news_agent.py"]
@@ -0,0 +1,527 @@
"""
Stock News Agent - 美股新闻资讯 Agent
获取美股相关新闻、市场动态和公司公告
"""
import os
import sys
import logging
import aiohttp
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException, Query, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
# 添加 common 模块路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入回调工具
try:
from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager
CALLBACK_ENABLED = True
except ImportError:
CALLBACK_ENABLED = False
AgentCallbackHandler = None
CallbackContextManager = None
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 环境变量
SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0")
SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080"))
POD_NAME = os.getenv("POD_NAME", "stock-news-agent")
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_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini")
# FastAPI 应用
app = FastAPI(
title="Stock News Agent",
description="美股新闻资讯 - 获取股票相关新闻、市场动态和分析报告",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 回调处理器
callback_handler: Optional[AgentCallbackHandler] = None
# ==================== 请求/响应模型 ====================
class NewsItem(BaseModel):
"""新闻条目"""
title: str
description: Optional[str] = None
url: str
source: str
published_at: str
sentiment: Optional[str] = None # positive, negative, neutral
class NewsRequest(BaseModel):
"""新闻查询请求"""
symbol: Optional[str] = Field(None, description="股票代码,如 AAPL")
query: Optional[str] = Field(None, description="搜索关键词")
limit: int = Field(10, ge=1, le=50, description="返回新闻数量")
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
class NewsResponse(BaseModel):
"""新闻查询响应"""
symbol: Optional[str] = None
query: Optional[str] = None
news: List[NewsItem]
total_count: int
timestamp: str
class MarketSummaryResponse(BaseModel):
"""市场概要响应"""
market_status: str
top_gainers: List[Dict[str, Any]]
top_losers: List[Dict[str, Any]]
most_active: List[Dict[str, Any]]
timestamp: str
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str
pod_name: str
news_api_configured: bool
callback_enabled: bool
timestamp: str
class ChatRequest(BaseModel):
"""Chat 请求"""
message: str = Field(..., description="用户消息")
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")
async def startup_event():
"""应用启动时初始化回调处理器"""
global callback_handler
if CALLBACK_ENABLED:
callback_handler = AgentCallbackHandler(
agent_name=POD_NAME,
user_id=USER_ID
)
logger.info(f"回调处理器已初始化: callback_url={callback_handler.callback_url}")
else:
logger.warning("回调模块未加载,计费回调功能不可用")
# ==================== 辅助函数 ====================
async def fetch_yahoo_news(symbol: str = None, query: str = None, limit: int = 10) -> List[Dict[str, Any]]:
"""从 Yahoo Finance 获取新闻"""
news_list = []
# 构建搜索词
search_term = symbol if symbol else (query if query else "stock market")
# Yahoo Finance RSS 新闻源
url = f"https://query1.finance.yahoo.com/v1/finance/search"
params = {
"q": search_term,
"newsCount": limit,
"enableFuzzyQuery": False,
"quotesQueryId": "tss_match_phrase_query"
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=15) as response:
if response.status == 200:
data = await response.json()
news_data = data.get("news", [])
for item in news_data[:limit]:
news_list.append({
"title": item.get("title", ""),
"description": item.get("summary", ""),
"url": item.get("link", ""),
"source": item.get("publisher", "Yahoo Finance"),
"published_at": datetime.fromtimestamp(
item.get("providerPublishTime", datetime.now().timestamp())
).isoformat(),
"sentiment": analyze_sentiment(item.get("title", "") + " " + item.get("summary", ""))
})
except Exception as e:
logger.error(f"获取新闻失败: {e}")
# 如果没有获取到新闻,返回模拟数据
if not news_list:
news_list = generate_sample_news(symbol or query or "market", limit)
return news_list
def analyze_sentiment(text: str) -> str:
"""简单的情感分析"""
positive_words = ["surge", "gain", "rise", "up", "bullish", "growth", "profit", "beat", "record", "high"]
negative_words = ["fall", "drop", "decline", "down", "bearish", "loss", "miss", "low", "crash", "sell"]
text_lower = text.lower()
positive_count = sum(1 for word in positive_words if word in text_lower)
negative_count = sum(1 for word in negative_words if word in text_lower)
if positive_count > negative_count:
return "positive"
elif negative_count > positive_count:
return "negative"
else:
return "neutral"
def generate_sample_news(topic: str, limit: int) -> List[Dict[str, Any]]:
"""生成示例新闻(当 API 不可用时)"""
sample_news = [
{
"title": f"{topic.upper()} Stock Shows Strong Momentum in Pre-Market Trading",
"description": f"Analysts remain bullish on {topic.upper()} as the stock shows continued strength.",
"url": "https://finance.yahoo.com/",
"source": "Yahoo Finance",
"published_at": datetime.utcnow().isoformat(),
"sentiment": "positive"
},
{
"title": f"Market Analysis: {topic.upper()} Technical Indicators Point to Potential Breakout",
"description": "Technical analysts identify key support and resistance levels for upcoming trading sessions.",
"url": "https://finance.yahoo.com/",
"source": "Market Watch",
"published_at": (datetime.utcnow() - timedelta(hours=2)).isoformat(),
"sentiment": "positive"
},
{
"title": f"Institutional Investors Increase Holdings in {topic.upper()}",
"description": "Latest 13F filings reveal increased institutional interest in the stock.",
"url": "https://finance.yahoo.com/",
"source": "Bloomberg",
"published_at": (datetime.utcnow() - timedelta(hours=4)).isoformat(),
"sentiment": "positive"
},
{
"title": f"Wall Street Analysts Update Price Targets for {topic.upper()}",
"description": "Multiple analysts revise their price targets following recent earnings report.",
"url": "https://finance.yahoo.com/",
"source": "CNBC",
"published_at": (datetime.utcnow() - timedelta(hours=6)).isoformat(),
"sentiment": "neutral"
},
{
"title": f"Options Activity Surges for {topic.upper()} Ahead of Key Events",
"description": "Unusual options activity detected as traders position for upcoming catalysts.",
"url": "https://finance.yahoo.com/",
"source": "Seeking Alpha",
"published_at": (datetime.utcnow() - timedelta(hours=8)).isoformat(),
"sentiment": "neutral"
}
]
return sample_news[:limit]
async def get_market_movers() -> Dict[str, Any]:
"""获取市场涨跌排行"""
# 使用 Yahoo Finance 获取市场数据
url = "https://query1.finance.yahoo.com/v1/finance/trending/US"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, timeout=15) as response:
if response.status == 200:
data = await response.json()
quotes = data.get("finance", {}).get("result", [{}])[0].get("quotes", [])
return {
"top_gainers": [{"symbol": q.get("symbol")} for q in quotes[:5]],
"top_losers": [],
"most_active": [{"symbol": q.get("symbol")} for q in quotes[:5]]
}
except Exception as e:
logger.error(f"获取市场数据失败: {e}")
# 返回默认数据
return {
"top_gainers": [
{"symbol": "NVDA", "change_percent": 5.2},
{"symbol": "TSLA", "change_percent": 3.8},
{"symbol": "AMD", "change_percent": 2.9}
],
"top_losers": [
{"symbol": "INTC", "change_percent": -2.1},
{"symbol": "BA", "change_percent": -1.8}
],
"most_active": [
{"symbol": "AAPL", "volume": "85M"},
{"symbol": "TSLA", "volume": "72M"},
{"symbol": "NVDA", "volume": "65M"}
]
}
# ==================== API 端点 ====================
@app.get("/health", response_model=HealthResponse)
@app.get("/", response_model=HealthResponse)
async def health_check():
"""健康检查"""
return HealthResponse(
status="healthy",
pod_name=POD_NAME,
news_api_configured=bool(NEWS_API_KEY),
callback_enabled=CALLBACK_ENABLED,
timestamp=datetime.utcnow().isoformat()
)
@app.post("/news", response_model=NewsResponse)
async def get_news(request: NewsRequest):
"""获取股票新闻"""
if not request.symbol and not request.query:
raise HTTPException(status_code=400, detail="请提供股票代码(symbol)或搜索关键词(query)")
# 使用回调上下文管理器
if CALLBACK_ENABLED and callback_handler and request.user_id:
with CallbackContextManager(
handler=callback_handler,
user_id=request.user_id,
request_id=f"stock-news-{int(datetime.utcnow().timestamp())}"
) as ctx:
ctx.add_tool("stock_news")
ctx.add_tool("news_aggregation")
news_data = await fetch_yahoo_news(request.symbol, request.query, request.limit)
news_items = [NewsItem(**item) for item in news_data]
return NewsResponse(
symbol=request.symbol,
query=request.query,
news=news_items,
total_count=len(news_items),
timestamp=datetime.utcnow().isoformat()
)
else:
news_data = await fetch_yahoo_news(request.symbol, request.query, request.limit)
news_items = [NewsItem(**item) for item in news_data]
return NewsResponse(
symbol=request.symbol,
query=request.query,
news=news_items,
total_count=len(news_items),
timestamp=datetime.utcnow().isoformat()
)
@app.get("/news")
async def get_news_get(
symbol: Optional[str] = Query(None, description="股票代码"),
query: Optional[str] = Query(None, description="搜索关键词"),
limit: int = Query(10, ge=1, le=50, description="返回数量"),
user_id: Optional[str] = Query(None, description="用户ID")
):
"""GET 方式获取新闻"""
request = NewsRequest(symbol=symbol, query=query, limit=limit, user_id=user_id)
return await get_news(request)
@app.get("/market-summary", response_model=MarketSummaryResponse)
async def get_market_summary():
"""获取市场概要"""
movers = await get_market_movers()
# 判断市场状态(简单逻辑)
now = datetime.utcnow()
hour = now.hour
weekday = now.weekday()
if weekday >= 5: # 周末
market_status = "closed"
elif 13 <= hour < 21: # UTC 时间对应美东 9:30-16:00
market_status = "open"
elif 9 <= hour < 13: # 盘前
market_status = "pre-market"
elif 21 <= hour < 25: # 盘后
market_status = "after-hours"
else:
market_status = "closed"
return MarketSummaryResponse(
market_status=market_status,
top_gainers=movers["top_gainers"],
top_losers=movers["top_losers"],
most_active=movers["most_active"],
timestamp=datetime.utcnow().isoformat()
)
@app.get("/trending")
async def get_trending_news():
"""获取热门财经新闻"""
news_data = await fetch_yahoo_news(query="stock market US", limit=20)
news_items = [NewsItem(**item) for item in news_data]
return NewsResponse(
query="trending",
news=news_items,
total_count=len(news_items),
timestamp=datetime.utcnow().isoformat()
)
# ==================== 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: Optional[str] = Header(None, alias="api-key"),
authorization: Optional[str] = Header(None)
):
"""智能对话 - 获取新闻并提供分析
api_key 通过请求头传递:
- api-key: your-api-key
- 或 Authorization: Bearer your-api-key
"""
# 从 Header 获取 api_key
if not api_key and authorization:
if authorization.startswith("Bearer "):
api_key = authorization[7:]
else:
api_key = authorization
if not api_key:
raise HTTPException(status_code=401, detail="请在请求头中提供 api-key 或 Authorization")
# 从消息中提取关键词
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():
"""主函数"""
logger.info(f"启动 Stock News Agent - {POD_NAME}")
logger.info(f"News API: {'已配置' if NEWS_API_KEY else '使用免费源'}")
logger.info(f"回调功能: {'已启用' if CALLBACK_ENABLED else '未启用'}")
uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="info")
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
RUN pip install --no-cache-dir \
fastapi==0.109.0 \
uvicorn[standard]==0.27.0 \
pydantic==2.5.3 \
requests>=2.31.0 \
aiohttp>=3.9.0
# 复制 common 模块(回调工具)
COPY common/agent_callback_utils.py /app/common/
RUN touch /app/common/__init__.py
# 复制应用代码
COPY agents/stock_quote_agent/stock_quote_agent.py /app/
# 环境变量
ENV PYTHONUNBUFFERED=1
ENV SERVICE_HOST=0.0.0.0
ENV SERVICE_PORT=8080
# 回调配置
ENV AGENT_CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health').read()" || exit 1
EXPOSE 8080
CMD ["python3", "-u", "stock_quote_agent.py"]
@@ -0,0 +1,480 @@
"""
Stock Quote Agent - 美股实时行情查询 Agent
使用 Yahoo Finance API 获取美股实时行情数据
"""
import os
import sys
import logging
import aiohttp
from typing import Optional, List, Dict, Any
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
# 添加 common 模块路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入回调工具
try:
from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager
CALLBACK_ENABLED = True
except ImportError:
CALLBACK_ENABLED = False
AgentCallbackHandler = None
CallbackContextManager = None
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 环境变量
SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0")
SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080"))
POD_NAME = os.getenv("POD_NAME", "stock-quote-agent")
USER_ID = os.getenv("USER_ID", "")
# Yahoo Finance API (通过 RapidAPI)
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_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini")
# FastAPI 应用
app = FastAPI(
title="Stock Quote Agent",
description="美股实时行情查询 - 获取股票价格、涨跌幅、成交量等数据",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 回调处理器
callback_handler: Optional[AgentCallbackHandler] = None
# ==================== 请求/响应模型 ====================
class QuoteRequest(BaseModel):
"""行情查询请求"""
symbol: str = Field(..., description="股票代码,如 AAPL, TSLA, MSFT")
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
class QuoteResponse(BaseModel):
"""行情查询响应"""
symbol: str
name: str
price: float
change: float
change_percent: float
volume: int
market_cap: Optional[float] = None
pe_ratio: Optional[float] = None
high_52week: Optional[float] = None
low_52week: Optional[float] = None
timestamp: str
class BatchQuoteRequest(BaseModel):
"""批量行情查询请求"""
symbols: List[str] = Field(..., description="股票代码列表")
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
class BatchQuoteResponse(BaseModel):
"""批量行情查询响应"""
quotes: List[QuoteResponse]
success_count: int
failed_count: int
timestamp: str
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str
pod_name: str
api_configured: bool
callback_enabled: bool
timestamp: str
class ChatRequest(BaseModel):
"""Chat 请求"""
message: str = Field(..., description="用户消息")
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")
async def startup_event():
"""应用启动时初始化回调处理器"""
global callback_handler
if CALLBACK_ENABLED:
callback_handler = AgentCallbackHandler(
agent_name=POD_NAME,
user_id=USER_ID
)
logger.info(f"回调处理器已初始化: callback_url={callback_handler.callback_url}")
else:
logger.warning("回调模块未加载,计费回调功能不可用")
# ==================== 辅助函数 ====================
async def fetch_stock_quote(symbol: str) -> Dict[str, Any]:
"""获取股票行情数据"""
# 使用免费的 Yahoo Finance API 替代方案
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
params = {
"interval": "1d",
"range": "1d"
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=15) as response:
if response.status == 200:
data = await response.json()
result = data.get("chart", {}).get("result", [])
if not result:
return {"success": False, "error": f"未找到股票: {symbol}"}
quote_data = result[0]
meta = quote_data.get("meta", {})
indicators = quote_data.get("indicators", {}).get("quote", [{}])[0]
current_price = meta.get("regularMarketPrice", 0)
previous_close = meta.get("previousClose", 0)
change = current_price - previous_close if previous_close else 0
change_percent = (change / previous_close * 100) if previous_close else 0
return {
"success": True,
"symbol": symbol.upper(),
"name": meta.get("shortName", symbol),
"price": current_price,
"change": round(change, 2),
"change_percent": round(change_percent, 2),
"volume": indicators.get("volume", [0])[-1] if indicators.get("volume") else 0,
"market_cap": meta.get("marketCap"),
"pe_ratio": None,
"high_52week": meta.get("fiftyTwoWeekHigh"),
"low_52week": meta.get("fiftyTwoWeekLow")
}
else:
return {"success": False, "error": f"API 请求失败: HTTP {response.status}"}
except Exception as e:
logger.error(f"获取行情失败: {symbol} - {e}")
return {"success": False, "error": str(e)}
# ==================== API 端点 ====================
@app.get("/health", response_model=HealthResponse)
@app.get("/", response_model=HealthResponse)
async def health_check():
"""健康检查"""
return HealthResponse(
status="healthy",
pod_name=POD_NAME,
api_configured=True, # 使用免费 API
callback_enabled=CALLBACK_ENABLED,
timestamp=datetime.utcnow().isoformat()
)
@app.post("/quote", response_model=QuoteResponse)
async def get_quote(request: QuoteRequest):
"""获取单个股票行情"""
# 使用回调上下文管理器
if CALLBACK_ENABLED and callback_handler and request.user_id:
with CallbackContextManager(
handler=callback_handler,
user_id=request.user_id,
request_id=f"stock-quote-{int(datetime.utcnow().timestamp())}"
) as ctx:
ctx.add_tool("stock_quote")
ctx.add_tool("yahoo_finance")
result = await fetch_stock_quote(request.symbol)
if not result["success"]:
raise HTTPException(status_code=500, detail=result.get("error"))
return QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
)
else:
result = await fetch_stock_quote(request.symbol)
if not result["success"]:
raise HTTPException(status_code=500, detail=result.get("error"))
return QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
)
@app.get("/quote")
async def get_quote_get(
symbol: str = Query(..., description="股票代码"),
user_id: Optional[str] = Query(None, description="用户ID")
):
"""GET 方式获取行情"""
request = QuoteRequest(symbol=symbol, user_id=user_id)
return await get_quote(request)
@app.post("/batch", response_model=BatchQuoteResponse)
async def batch_get_quotes(request: BatchQuoteRequest):
"""批量获取股票行情"""
if CALLBACK_ENABLED and callback_handler and request.user_id:
with CallbackContextManager(
handler=callback_handler,
user_id=request.user_id,
request_id=f"stock-batch-{int(datetime.utcnow().timestamp())}"
) as ctx:
ctx.add_tool("stock_quote")
ctx.add_tool("batch_quote")
quotes = []
success_count = 0
failed_count = 0
for symbol in request.symbols:
result = await fetch_stock_quote(symbol)
if result["success"]:
quotes.append(QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
))
success_count += 1
else:
failed_count += 1
return BatchQuoteResponse(
quotes=quotes,
success_count=success_count,
failed_count=failed_count,
timestamp=datetime.utcnow().isoformat()
)
else:
quotes = []
success_count = 0
failed_count = 0
for symbol in request.symbols:
result = await fetch_stock_quote(symbol)
if result["success"]:
quotes.append(QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
))
success_count += 1
else:
failed_count += 1
return BatchQuoteResponse(
quotes=quotes,
success_count=success_count,
failed_count=failed_count,
timestamp=datetime.utcnow().isoformat()
)
@app.get("/popular")
async def get_popular_stocks():
"""获取热门股票行情"""
popular_symbols = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META"]
request = BatchQuoteRequest(symbols=popular_symbols)
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: Optional[str] = Header(None, alias="api-key"),
authorization: Optional[str] = Header(None)
):
"""智能对话 - 支持自然语言查询股票信息
api_key 通过请求头传递:
- api-key: your-api-key
- 或 Authorization: Bearer your-api-key
"""
# 从 Header 获取 api_key
if not api_key and authorization:
if authorization.startswith("Bearer "):
api_key = authorization[7:]
else:
api_key = authorization
if not api_key:
raise HTTPException(status_code=401, detail="请在请求头中提供 api-key 或 Authorization")
# 从消息中提取股票代码
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():
"""主函数"""
logger.info(f"启动 Stock Quote Agent - {POD_NAME}")
logger.info(f"回调功能: {'已启用' if CALLBACK_ENABLED else '未启用'}")
uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="info")
if __name__ == "__main__":
main()
+2 -2
View File
@@ -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
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# 构建三个美股 Agent 镜像并推送到 ACR
# 使用方法: ./build_stock_agents.sh
set -e
echo "=========================================="
echo "构建美股 Agent 镜像并推送到 ACR"
echo "=========================================="
# 登录 ACR
echo ""
echo "=== 步骤 1: 登录 ACR ==="
az acr login --name agnettaiji
cd /home/taiji/tools/agent-manager
# 构建 stock-quote-agent
echo ""
echo "=== 步骤 2: 构建 stock-quote-agent (ARM64) ==="
docker buildx build --platform linux/arm64 \
-t agnettaiji.azurecr.io/ai-agents/stock-quote-agent:latest \
-f agent_templates/agents/stock_quote_agent/stock_quote_agent.Dockerfile \
agent_templates/ --push
echo "✅ stock-quote-agent 推送完成"
# 构建 stock-news-agent
echo ""
echo "=== 步骤 3: 构建 stock-news-agent (ARM64) ==="
docker buildx build --platform linux/arm64 \
-t agnettaiji.azurecr.io/ai-agents/stock-news-agent:latest \
-f agent_templates/agents/stock_news_agent/stock_news_agent.Dockerfile \
agent_templates/ --push
echo "✅ stock-news-agent 推送完成"
# 构建 stock-analysis-agent
echo ""
echo "=== 步骤 4: 构建 stock-analysis-agent (ARM64) ==="
docker buildx build --platform linux/arm64 \
-t agnettaiji.azurecr.io/ai-agents/stock-analysis-agent:latest \
-f agent_templates/agents/stock_analysis_agent/stock_analysis_agent.Dockerfile \
agent_templates/ --push
echo "✅ stock-analysis-agent 推送完成"
echo ""
echo "=========================================="
echo "✅ 所有镜像构建并推送完成!"
echo "=========================================="
echo ""
echo "镜像列表:"
echo " - agnettaiji.azurecr.io/ai-agents/stock-quote-agent:latest"
echo " - agnettaiji.azurecr.io/ai-agents/stock-news-agent:latest"
echo " - agnettaiji.azurecr.io/ai-agents/stock-analysis-agent:latest"
echo ""
echo "下一步: 重启 agent-manager 以加载新模板"
echo " kubectl rollout restart deployment/agent-manager -n agent-manager"
+148
View File
@@ -0,0 +1,148 @@
#!/bin/bash
# 部署三个美股 Agent:构建镜像、添加模板、测试创建
# 使用方法: ./deploy_stock_agents.sh
set -e
AGENT_MANAGER_URL="http://20.212.121.126"
echo "=========================================="
echo "部署美股 Agent 到 Agent Manager"
echo "=========================================="
# ==================== 步骤 1: 构建并推送镜像 ====================
echo ""
echo "=== 步骤 1: 登录 ACR ==="
az acr login --name agnettaiji
cd /home/taiji/tools/agent-manager
echo ""
echo "=== 步骤 2: 构建 stock-quote-agent (ARM64) ==="
docker buildx build --platform linux/arm64 \
-t agnettaiji.azurecr.io/ai-agents/stock-quote-agent:latest \
-f agent_templates/agents/stock_quote_agent/stock_quote_agent.Dockerfile \
agent_templates/ --push
echo "✅ stock-quote-agent 推送完成"
echo ""
echo "=== 步骤 3: 构建 stock-news-agent (ARM64) ==="
docker buildx build --platform linux/arm64 \
-t agnettaiji.azurecr.io/ai-agents/stock-news-agent:latest \
-f agent_templates/agents/stock_news_agent/stock_news_agent.Dockerfile \
agent_templates/ --push
echo "✅ stock-news-agent 推送完成"
echo ""
echo "=== 步骤 4: 构建 stock-analysis-agent (ARM64) ==="
docker buildx build --platform linux/arm64 \
-t agnettaiji.azurecr.io/ai-agents/stock-analysis-agent:latest \
-f agent_templates/agents/stock_analysis_agent/stock_analysis_agent.Dockerfile \
agent_templates/ --push
echo "✅ stock-analysis-agent 推送完成"
# ==================== 步骤 2: 通过 API 添加模板 ====================
echo ""
echo "=== 步骤 5: 通过 API 添加模板 ==="
# 添加 stock_quote_agent 模板
echo "添加 stock_quote_agent 模板..."
curl -s -X POST "${AGENT_MANAGER_URL}/templates/create" \
-H "Content-Type: application/json" \
-d '{
"name": "stock_quote_agent",
"display_name": "Stock Quote Agent",
"description": "美股实时行情查询 Agent - 获取股票价格、涨跌幅、成交量等数据",
"image": "agnettaiji.azurecr.io/ai-agents/stock-quote-agent:latest",
"port": 8080,
"agent_framework": "api",
"env_requirements": {}
}' | jq .
echo ""
# 添加 stock_news_agent 模板
echo "添加 stock_news_agent 模板..."
curl -s -X POST "${AGENT_MANAGER_URL}/templates/create" \
-H "Content-Type: application/json" \
-d '{
"name": "stock_news_agent",
"display_name": "Stock News Agent",
"description": "美股新闻资讯 Agent - 获取股票相关新闻和市场情绪分析",
"image": "agnettaiji.azurecr.io/ai-agents/stock-news-agent:latest",
"port": 8080,
"agent_framework": "api",
"env_requirements": {}
}' | jq .
echo ""
# 添加 stock_analysis_agent 模板
echo "添加 stock_analysis_agent 模板..."
curl -s -X POST "${AGENT_MANAGER_URL}/templates/create" \
-H "Content-Type: application/json" \
-d '{
"name": "stock_analysis_agent",
"display_name": "Stock Analysis Agent",
"description": "美股技术分析 Agent - 提供技术指标、趋势分析和投资建议",
"image": "agnettaiji.azurecr.io/ai-agents/stock-analysis-agent:latest",
"port": 8080,
"agent_framework": "api",
"env_requirements": {}
}' | jq .
echo ""
echo "=== 步骤 6: 验证模板添加成功 ==="
echo "查询所有模板..."
curl -s "${AGENT_MANAGER_URL}/templates" | jq '.[] | select(.name | startswith("stock_"))'
# ==================== 步骤 3: 测试创建 Agent ====================
echo ""
echo "=== 步骤 7: 测试创建 stock_quote_agent ==="
RESULT=$(curl -s -X POST "${AGENT_MANAGER_URL}/agents" \
-H "Content-Type: application/json" \
-d '{
"name": "test-stock-quote",
"template": "stock_quote_agent",
"config": {
"user_id": "test-user",
"cpu_request": "100m",
"cpu_limit": "500m",
"memory_request": "128Mi",
"memory_limit": "512Mi",
"replicas": 1
}
}')
echo "$RESULT" | jq .
# 提取域名
DOMAIN=$(echo "$RESULT" | jq -r '.access_info.domain // empty')
if [ -n "$DOMAIN" ]; then
echo ""
echo "✅ Agent 创建成功!"
echo "域名: $DOMAIN"
echo ""
echo "等待 30 秒后测试 API..."
sleep 30
echo ""
echo "=== 步骤 8: 测试 Agent API ==="
echo "测试获取 AAPL 行情..."
curl -s "http://${DOMAIN}/quote?symbol=AAPL" | jq .
else
echo ""
echo "⚠️ Agent 创建中,请稍后检查状态"
fi
echo ""
echo "=========================================="
echo "✅ 部署完成!"
echo "=========================================="
echo ""
echo "新增模板:"
echo " - stock_quote_agent: 美股实时行情查询"
echo " - stock_news_agent: 美股新闻资讯"
echo " - stock_analysis_agent: 美股技术分析"
echo ""
echo "测试命令:"
echo " curl \"http://\${DOMAIN}/quote?symbol=AAPL\""
echo " curl \"http://\${DOMAIN}/quote?symbol=TSLA\""
+563
View File
@@ -0,0 +1,563 @@
# 美股 AI Agent 文档
本项目包含 **三个独立的美股 AI Agent 服务**,均通过 **HTTP API** 对外提供能力,支持智能对话分析。
- **Stock Quote Agent**:美股实时行情查询与分析
- **Stock News Agent**:美股新闻资讯获取与解读
- **Stock Analysis Agent**:美股技术分析与投资建议
---
## 认证方式
所有 Chat API 需要在请求头中提供 API Key,支持两种方式:
| 方式 | Header | 示例 |
|------|--------|------|
| api-key | `api-key` | `api-key: sk-xxxxx` |
| Bearer Token | `Authorization` | `Authorization: Bearer sk-xxxxx` |
> ⚠️ 未提供认证信息将返回 `401 Unauthorized`
---
## Agent 1:Stock Quote Agent
### 功能概览
提供美股 **实时行情查询** 能力,支持自然语言交互,返回股票价格、涨跌幅、成交量等数据。
支持能力:
- 实时股票行情查询
- 多股票批量查询
- 热门股票行情
- **AI 智能对话分析**
---
### 1️⃣ /chat — 智能对话
#### 功能说明
通过自然语言与 AI 交互,自动识别股票代码并返回行情数据及投资建议。
---
#### REST API 调用
```
POST /chat
Content-Type: application/json
api-key: your-api-key
```
```json
{
"message": "AAPL 和 TSLA 今天表现如何?"
}
```
---
#### 参数说明
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| message | string | ✅ | - | 用户消息(支持自然语言) |
| user_id | string | ❌ | null | 用户ID(用于计费回调) |
**Header 参数:**
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| api-key | string | ⚠ | API Key(二选一) |
| Authorization | string | ⚠ | Bearer Token(二选一) |
---
#### 返回结果
```json
{
"response": "今天AAPL的股价为$274.43,涨跌幅为0.00%。TSLA的股价为$398.78...",
"data": {
"stocks": [
{
"success": true,
"symbol": "AAPL",
"name": "Apple Inc.",
"price": 274.43,
"change": 0,
"change_percent": 0,
"volume": 5212932,
"high_52week": 288.62,
"low_52week": 169.21
},
{
"success": true,
"symbol": "TSLA",
"name": "Tesla, Inc.",
"price": 398.78,
"change": 0,
"change_percent": 0,
"volume": 7867089,
"high_52week": 498.83,
"low_52week": 214.25
}
],
"symbols_detected": ["AAPL", "TSLA"]
},
"timestamp": "2026-02-05T15:30:13.347583"
}
```
---
### 2️⃣ /quote — 单股行情查询
#### REST API 调用
```
GET /quote?symbol=AAPL
```
或
```
POST /quote
Content-Type: application/json
```
```json
{
"symbol": "AAPL"
}
```
---
#### 参数说明
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| symbol | string | ✅ | 股票代码(如 AAPL, TSLA) |
---
#### 返回结果
```json
{
"symbol": "AAPL",
"name": "Apple Inc.",
"price": 274.43,
"change": 2.31,
"change_percent": 0.85,
"volume": 52129320,
"market_cap": 4200000000000,
"high_52week": 288.62,
"low_52week": 169.21,
"timestamp": "2026-02-05T15:30:00.000000"
}
```
---
### 3️⃣ /batch — 批量行情查询
```
POST /batch
Content-Type: application/json
```
```json
{
"symbols": ["AAPL", "TSLA", "NVDA", "MSFT", "GOOGL"]
}
```
---
### 4️⃣ /popular — 热门股票行情
```
GET /popular
```
返回 AAPL, MSFT, GOOGL, AMZN, TSLA, NVDA, META 等热门股票行情。
---
## Agent 2:Stock News Agent
### 功能概览
提供美股 **新闻资讯获取与分析** 能力,支持按股票代码或关键词搜索新闻。
支持能力:
- 股票相关新闻查询
- 市场动态获取
- 热门财经新闻
- **AI 新闻解读与影响分析**
---
### 1️⃣ /chat — 智能对话
#### 功能说明
通过自然语言获取股票新闻并进行 AI 分析解读。
---
#### REST API 调用
```
POST /chat
Content-Type: application/json
api-key: your-api-key
```
```json
{
"message": "NVDA 最近有什么重要新闻?"
}
```
---
#### 返回结果
```json
{
"response": "最近关于NVDA的新闻显示出其股票在盘前交易中表现强劲,市场对其AI芯片业务的前景保持乐观...",
"data": {
"news_count": 5,
"symbols": ["NVDA"]
},
"timestamp": "2026-02-05T15:31:29.298428"
}
```
---
### 2️⃣ /news — 获取新闻
```
POST /news
Content-Type: application/json
```
```json
{
"symbol": "AAPL",
"limit": 10
}
```
---
#### 参数说明
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| symbol | string | ⚠ | null | 股票代码 |
| query | string | ⚠ | null | 搜索关键词 |
| limit | integer | ❌ | 10 | 返回新闻数量(1-50) |
> `symbol` 与 `query` 二选一
---
### 3️⃣ /market — 市场动态
```
GET /market
```
返回市场涨跌排行、活跃股票等信息。
---
### 4️⃣ /trending — 热门新闻
```
GET /trending
```
返回当前热门财经新闻。
---
## Agent 3:Stock Analysis Agent
### 功能概览
提供美股 **技术分析** 能力,计算技术指标并给出交易信号与投资建议。
支持能力:
- 技术指标计算(SMA, RSI, MACD, 布林带)
- 趋势判断(看涨/看跌/中性)
- 买卖信号生成
- 支撑位/阻力位计算
- **AI 综合分析与投资建议**
---
### 1️⃣ /chat — 智能对话
#### 功能说明
通过自然语言获取股票技术分析并由 AI 提供投资建议。
---
#### REST API 调用
```
POST /chat
Content-Type: application/json
Authorization: Bearer your-api-key
```
```json
{
"message": "帮我分析 AAPL,现在适合买入吗?"
}
```
---
#### 返回结果
```json
{
"response": "根据技术分析数据,AAPL当前价格为$274.31,趋势为中性,信号为持有。RSI值为66.49,接近超买区域...",
"data": {
"analysis": [
{
"symbol": "AAPL",
"current_price": 274.31,
"indicators": {
"sma_20": 259.12,
"sma_50": 268.63,
"sma_200": null,
"rsi_14": 66.49,
"macd": -0.9012,
"macd_signal": -0.8111,
"bollinger_upper": 275.39,
"bollinger_lower": 242.84
},
"trend": "neutral",
"signal": "hold",
"support_level": 243.42,
"resistance_level": 279.5,
"risk_level": "medium"
}
],
"symbols": ["AAPL"]
},
"timestamp": "2026-02-05T15:58:50.578048"
}
```
---
### 2️⃣ /analyze — 技术分析
```
POST /analyze
Content-Type: application/json
```
```json
{
"symbol": "AAPL"
}
```
---
#### 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| current_price | float | 当前价格 |
| sma_20 | float | 20日简单移动平均线 |
| sma_50 | float | 50日简单移动平均线 |
| sma_200 | float | 200日简单移动平均线 |
| rsi_14 | float | 14日相对强弱指数 |
| macd | float | MACD 值 |
| macd_signal | float | MACD 信号线 |
| bollinger_upper | float | 布林带上轨 |
| bollinger_lower | float | 布林带下轨 |
| trend | string | 趋势:bullish / bearish / neutral |
| signal | string | 信号:buy / sell / hold |
| support_level | float | 支撑位 |
| resistance_level | float | 阻力位 |
| risk_level | string | 风险等级:low / medium / high |
---
### 3️⃣ /compare — 多股对比
```
POST /compare
Content-Type: application/json
```
```json
{
"symbols": ["AAPL", "MSFT", "GOOGL"]
}
```
---
### 4️⃣ /screen — 股票筛选
```
GET /screen?trend=bullish&signal=buy
```
根据技术指标筛选符合条件的股票。
---
## 统一错误格式
**成功:**
```json
{
"response": "AI 分析结果...",
"data": {},
"timestamp": "2026-02-05T15:30:00.000000"
}
```
**认证失败(401):**
```json
{
"detail": "请在请求头中提供 api-key 或 Authorization"
}
```
**请求错误(400):**
```json
{
"detail": "错误描述"
}
```
**服务器错误(500):**
```json
{
"detail": "Internal Server Error"
}
```
---
## 调用示例
### cURL 示例
**使用 api-key Header:**
```bash
curl -X POST "http://test-stock-quote.taijiagnet.com/chat" \
-H "Content-Type: application/json" \
-H "api-key: sk-mPV5MVVVVvfGSkXA-ASQXQ" \
-d '{"message": "AAPL 和 TSLA 今天表现如何?"}'
```
**使用 Authorization Bearer:**
```bash
curl -X POST "http://test-stock-analysis.taijiagnet.com/chat" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-mPV5MVVVVvfGSkXA-ASQXQ" \
-d '{"message": "帮我分析 NVDA,现在适合买入吗?"}'
```
---
### Python 示例
```python
import requests
API_KEY = "sk-mPV5MVVVVvfGSkXA-ASQXQ"
# Stock Quote Agent
response = requests.post(
"http://test-stock-quote.taijiagnet.com/chat",
headers={
"Content-Type": "application/json",
"api-key": API_KEY
},
json={"message": "AAPL 现在多少钱?"}
)
print(response.json())
# Stock News Agent
response = requests.post(
"http://test-stock-news.taijiagnet.com/chat",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
},
json={"message": "TSLA 最近有什么新闻?"}
)
print(response.json())
# Stock Analysis Agent
response = requests.post(
"http://test-stock-analysis.taijiagnet.com/chat",
headers={
"Content-Type": "application/json",
"api-key": API_KEY
},
json={"message": "帮我技术分析 NVDA"}
)
print(response.json())
```
---
## 部署信息
| Agent | 模板名称 | 镜像 | 端口 |
|-------|----------|------|------|
| Stock Quote | stock_quote_agent | agnettaiji.azurecr.io/ai-agents/stock-quote-agent:latest | 8080 |
| Stock News | stock_news_agent | agnettaiji.azurecr.io/ai-agents/stock-news-agent:latest | 8080 |
| Stock Analysis | stock_analysis_agent | agnettaiji.azurecr.io/ai-agents/stock-analysis-agent:latest | 8080 |
---
## 环境变量配置
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| LLM_BASE_URL | LLM 服务地址 | https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1 |
| LLM_MODEL | 模型名称 | taiji/gpt-4o-mini |
| SERVICE_HOST | 服务监听地址 | 0.0.0.0 |
| SERVICE_PORT | 服务端口 | 8080 |
---
## 免责声明
> ⚠️ **投资有风险,入市需谨慎。** 本 Agent 提供的分析和建议仅供参考,不构成任何投资建议。用户应自行判断并承担投资风险。
---
*文档版本:v1.0.0*
*更新日期:2026-02-05*
+5 -2
View File
@@ -844,6 +844,8 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
tools_config = []
for tool in tools:
config = tool.get("config", {})
# 获取 AI 生成的工具代码
tool_code = tool_storage.get_tool_code(tool.get("tool_ref_id", ""))
tools_config.append({
"name": tool["name"],
"description": tool.get("description", ""),
@@ -852,7 +854,8 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
"auth": config.get("auth"),
"request_params": config.get("request_params"),
"request_body": config.get("request_body"),
"timeout": config.get("timeout", 30)
"timeout": config.get("timeout", 30),
"generated_code": tool_code # 传递 AI 生成的代码
})
# 生成唯一后缀,确保不同用户创建同名 Agent 不会冲突
@@ -902,7 +905,7 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
"ACR_USERNAME": "agnettaiji",
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
"AZ_CLIENT_SECRET": "S0J8Q~DE.DEu29nreaBn2EbeuGOg7GEIkonRMbYj",
"AZ_CLIENT_SECRET": "UVU8Q~Hcrf5KeLi2RvUXB2rcuKFEjRCCrf_JrbwA",
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
"AZ_RG": "taiji-ai-pda",
+11
View File
@@ -10,3 +10,14 @@ data:
AZURE_DNS_ZONE: "taijiagnet.com"
AZURE_SUBSCRIPTION_ID: "your-subscription-id"
AZURE_RESOURCE_GROUP: "your-resource-group"
# Gitee 配置(非敏感信息)
GITEE_API_URL: "http://gitee.ath.cx:3000/api/v1"
GITEE_BASE_URL: "http://gitee.ath.cx:3000"
GITEE_OWNER: "xiaohei"
GITEE_USERNAME: "zhanggangyong"
GITEE_TEMPLATE_REPO: "cicd-AKS"
# ACR 配置
ACR_REGISTRY: "agnettaiji.azurecr.io"
ACR_NAMESPACE: "ai-agents"
+11
View File
@@ -61,6 +61,17 @@ spec:
secretKeyRef:
name: agent-manager-secret
key: AZURE_CLIENT_SECRET
# Gitee 凭据
- name: GITEE_TOKEN
valueFrom:
secretKeyRef:
name: agent-manager-secret
key: GITEE_TOKEN
- name: GITEE_PASSWORD
valueFrom:
secretKeyRef:
name: agent-manager-secret
key: GITEE_PASSWORD
# 挂载 kubeconfig(用于管理其他 Agent)
volumeMounts:
+4
View File
@@ -10,5 +10,9 @@ stringData:
AZURE_CLIENT_ID: "your-client-id"
AZURE_CLIENT_SECRET: "your-client-secret"
# Gitee 凭据(敏感信息)
GITEE_TOKEN: "your-gitee-token"
GITEE_PASSWORD: "your-gitee-password"
# 数据库密码(如果需要单独管理)
# DB_PASSWORD: "By@123456."
+18 -1
View File
@@ -30,6 +30,10 @@ AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET:-your-client-secret}"
AZURE_SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-your-subscription-id}"
AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-your-resource-group}"
# Gitee 配置(需要替换为实际值)
GITEE_TOKEN="${GITEE_TOKEN:-your-gitee-token}"
GITEE_PASSWORD="${GITEE_PASSWORD:-your-gitee-password}"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} Agent Manager K8s 部署 (ARM64)${NC}"
echo -e "${BLUE}========================================${NC}"
@@ -154,7 +158,18 @@ update_config() {
fi
fi
# 创建临时 secret 文件
# 检查 Gitee 凭据
if [ "$GITEE_TOKEN" = "your-gitee-token" ]; then
print_warning "请设置 GITEE_TOKEN 环境变量(创建 Agent 仓库必需)"
print_warning " export GITEE_TOKEN=your-actual-token"
read -p "是否继续部署(不含 Gitee 仓库功能)?[y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# 创建临时 secret 文件(包含 Azure 和 Gitee 凭据)
cat > /tmp/agent-manager-secret.yaml <<EOF
apiVersion: v1
kind: Secret
@@ -166,6 +181,8 @@ stringData:
AZURE_TENANT_ID: "${AZURE_TENANT_ID}"
AZURE_CLIENT_ID: "${AZURE_CLIENT_ID}"
AZURE_CLIENT_SECRET: "${AZURE_CLIENT_SECRET}"
GITEE_TOKEN: "${GITEE_TOKEN}"
GITEE_PASSWORD: "${GITEE_PASSWORD}"
EOF
kubectl apply -f ${K8S_DIR}/agent-manager-configmap.yaml
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""
测试美股 Agents 功能
"""
import asyncio
import aiohttp
import os
import sys
# LLM 配置
LLM_BASE_URL = "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1"
LLM_API_KEY = "sk-mPV5MVVVVvfGSkXA-ASQXQ"
LLM_MODEL = "taiji/gpt-4o-mini"
async def fetch_stock_quote(symbol: str):
"""获取股票行情数据 (Stock Quote Agent)"""
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
params = {"interval": "1d", "range": "1d"}
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as response:
if response.status == 200:
data = await response.json()
result = data.get("chart", {}).get("result", [])
if not result:
return {"success": False, "error": f"未找到股票: {symbol}"}
quote_data = result[0]
meta = quote_data.get("meta", {})
current_price = meta.get("regularMarketPrice", 0)
previous_close = meta.get("previousClose", 0)
change = current_price - previous_close if previous_close else 0
change_percent = (change / previous_close * 100) if previous_close else 0
return {
"success": True,
"symbol": symbol.upper(),
"name": meta.get("shortName", symbol),
"price": current_price,
"change": round(change, 2),
"change_percent": round(change_percent, 2),
"high_52week": meta.get("fiftyTwoWeekHigh"),
"low_52week": meta.get("fiftyTwoWeekLow"),
"market_cap": meta.get("marketCap"),
}
else:
return {"success": False, "error": f"API 请求失败: HTTP {response.status}"}
except Exception as e:
return {"success": False, "error": str(e)}
async def fetch_stock_news(symbol: str):
"""获取股票新闻 (Stock News Agent)"""
url = f"https://query1.finance.yahoo.com/v1/finance/search"
params = {"q": symbol, "newsCount": 5}
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as response:
if response.status == 200:
data = await response.json()
news = data.get("news", [])
return {
"success": True,
"symbol": symbol.upper(),
"news_count": len(news),
"news": [{"title": n.get("title", ""), "publisher": n.get("publisher", "")} for n in news[:5]]
}
else:
return {"success": False, "error": f"API 请求失败: HTTP {response.status}"}
except Exception as e:
return {"success": False, "error": str(e)}
async def fetch_historical_data(symbol: str, period: str = "1mo"):
"""获取历史数据用于技术分析 (Stock Analysis Agent)"""
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
params = {"interval": "1d", "range": period}
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as response:
if response.status == 200:
data = await response.json()
result = data.get("chart", {}).get("result", [])
if not result:
return {"success": False, "error": f"未找到股票: {symbol}"}
quote_data = result[0]
indicators = quote_data.get("indicators", {}).get("quote", [{}])[0]
timestamps = quote_data.get("timestamp", [])
closes = indicators.get("close", [])
closes = [c for c in closes if c is not None]
if len(closes) >= 5:
# 简单技术分析
sma_5 = sum(closes[-5:]) / 5
current = closes[-1]
trend = "看涨" if current > sma_5 else "看跌"
return {
"success": True,
"symbol": symbol.upper(),
"data_points": len(closes),
"current_price": round(current, 2),
"sma_5": round(sma_5, 2),
"trend": trend,
"high": round(max(closes), 2),
"low": round(min(closes), 2),
}
else:
return {"success": False, "error": "数据点不足"}
else:
return {"success": False, "error": f"API 请求失败: HTTP {response.status}"}
except Exception as e:
return {"success": False, "error": str(e)}
async def test_with_llm(symbol: str, context: str):
"""使用 LLM 生成分析报告"""
try:
async with aiohttp.ClientSession() as session:
payload = {
"model": LLM_MODEL,
"messages": [
{"role": "system", "content": "你是一个专业的美股分析师,请根据提供的数据给出简洁的分析。"},
{"role": "user", "content": f"请分析以下 {symbol} 股票数据并给出简要建议(50字以内):\n{context}"}
],
"max_tokens": 200
}
headers = {
"Authorization": f"Bearer {LLM_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()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
return {"success": True, "analysis": content}
else:
error_text = await response.text()
return {"success": False, "error": f"LLM 请求失败: {response.status} - {error_text[:100]}"}
except Exception as e:
return {"success": False, "error": str(e)}
async def main():
print("=" * 70)
print("美股 AI Agents 本地测试")
print("=" * 70)
print(f"LLM: {LLM_MODEL}")
print(f"API: {LLM_BASE_URL}")
print("=" * 70)
symbols = ["AAPL", "TSLA", "NVDA", "MSFT", "GOOGL"]
# 测试 1: Stock Quote Agent
print("\n📈 【测试 1: Stock Quote Agent - 实时行情】")
print("-" * 70)
for symbol in symbols:
result = await fetch_stock_quote(symbol)
if result.get("success"):
print(f"✅ {result['symbol']:6} | {result['name'][:25]:25} | ${result['price']:>10.2f} | {result['change_percent']:+6.2f}%")
else:
print(f"❌ {symbol}: {result.get('error')}")
# 测试 2: Stock News Agent
print("\n📰 【测试 2: Stock News Agent - 新闻资讯】")
print("-" * 70)
for symbol in ["AAPL", "TSLA"]:
result = await fetch_stock_news(symbol)
if result.get("success"):
print(f"✅ {result['symbol']} - 找到 {result['news_count']} 条新闻:")
for news in result['news'][:2]:
print(f" • {news['title'][:60]}...")
else:
print(f"❌ {symbol}: {result.get('error')}")
# 测试 3: Stock Analysis Agent
print("\n📊 【测试 3: Stock Analysis Agent - 技术分析】")
print("-" * 70)
for symbol in ["AAPL", "NVDA", "TSLA"]:
result = await fetch_historical_data(symbol)
if result.get("success"):
print(f"✅ {result['symbol']:6} | 当前: ${result['current_price']:>8.2f} | SMA5: ${result['sma_5']:>8.2f} | 趋势: {result['trend']} | 区间: ${result['low']:.2f}-${result['high']:.2f}")
else:
print(f"❌ {symbol}: {result.get('error')}")
# 测试 4: LLM 综合分析
print("\n🤖 【测试 4: LLM 综合分析】")
print("-" * 70)
# 获取一只股票的完整数据
symbol = "AAPL"
quote = await fetch_stock_quote(symbol)
analysis = await fetch_historical_data(symbol)
if quote.get("success") and analysis.get("success"):
context = f"""
股票: {symbol} ({quote['name']})
当前价格: ${quote['price']:.2f}
涨跌幅: {quote['change_percent']:+.2f}%
52周范围: ${quote.get('low_52week', 0):.2f} - ${quote.get('high_52week', 0):.2f}
5日均线: ${analysis['sma_5']:.2f}
技术趋势: {analysis['trend']}
近期区间: ${analysis['low']:.2f} - ${analysis['high']:.2f}
"""
print(f"📋 {symbol} 数据汇总:")
print(context)
print("🔄 调用 LLM 生成分析报告...")
llm_result = await test_with_llm(symbol, context)
if llm_result.get("success"):
print(f"\n💡 AI 分析建议:")
print(f" {llm_result['analysis']}")
else:
print(f"❌ LLM 调用失败: {llm_result.get('error')}")
else:
print(f"❌ 获取数据失败")
print("\n" + "=" * 70)
print("✅ 测试完成!")
print("=" * 70)
if __name__ == "__main__":
asyncio.run(main())
+3 -3
View File
@@ -225,10 +225,10 @@ async def generate_agent(request: GenerateAgentRequest, background_tasks: Backgr
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
"ACR_USERNAME": "agnettaiji",
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
"AZ_CLIENT_ID": "fb306798-2cfe-4ac9-ba48-eab7bc71bcfe",
"AZ_CLIENT_SECRET": "cVK8Q~xlfBwm2_t2TC24yrTukWV4F3G~eIjBBa0D",
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
"AZ_CLIENT_SECRET": "UVU8Q~Hcrf5KeLi2RvUXB2rcuKFEjRCCrf_JrbwA",
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
"AZ_SUBSCRIPTION_ID": "c6c47e4c-f5f4-49f8-b26f-7728862c17d6",
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
"AZ_RG": "taiji-ai-pda",
"AZ_AKS": "taiji-ai-pda",
"AZURE_DNS_ZONE": "taijiagnet.com"