Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abe3f690ec | ||
|
|
70d9328afc | ||
|
|
02279f9344 | ||
|
|
a115ee68b0 | ||
|
|
6bc7873bc3 | ||
|
|
e67aec11ef | ||
|
|
336f4c2e82 | ||
|
|
1e07e855d8 | ||
|
|
4fcc213565 | ||
|
|
1a99ba3ac1 |
+149
-92
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
Executable
+148
@@ -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\""
|
||||
@@ -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*
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user