feat: 为三个美股 AI Agent 添加 /chat 端点,支持 LLM 智能对话

- stock_quote_agent: 添加 chat 功能,可分析行情数据并给出投资建议
- stock_news_agent: 添加 chat 功能,可解读新闻并分析市场影响
- stock_analysis_agent: 添加 chat 功能,可进行技术分析并提供交易信号

Chat 端点使用方法:
POST /chat
{
  "message": "请帮我分析 AAPL 行情",
  "api_key": "your-llm-api-key"
}

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