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