Files
auv/backend/services/ai_tools.py
T

262 lines
9.8 KiB
Python

"""Function Calling 工具定义和执行器
让 AI 可以主动调用现有 API 获取市场数据:
- get_market_dashboard: 市场看板
- get_theme_history: 题材热点历史
- get_active_core_stocks: 核心股追踪
- get_stock_quote: 个股实时行情
- get_fund_flow: 资金流向
"""
import json
from services.ai_config import AI_API_BASE, AI_API_KEY, AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE
TOOLS = [
{
"type": "function",
"function": {
"name": "get_market_dashboard",
"description": "获取A股市场看板数据,包含主要指数行情、全市场涨跌统计(涨跌家数/涨停/跌停/炸板率/成交额)、市场温度评分与竞价信号、行业强度榜、概念热度、板块主力资金流、两融余额、海外主要指数与国内期货主力合约(globalMarkets字段)、完整连板梯队(limitLadder字段,含涨停原因与封单金额)、事件情报(热门股/龙虎榜/飙升/异动)",
"parameters": {"type": "object", "properties": {}, "required": []}
}
},
{
"type": "function",
"function": {
"name": "get_theme_history",
"description": "获取指定交易日的题材涨幅排行前20",
"parameters": {
"type": "object",
"properties": {
"date": {"type": "string", "description": "交易日 YYYY-MM-DD"}
},
"required": ["date"]
}
}
},
{
"type": "function",
"function": {
"name": "get_active_core_stocks",
"description": "获取活跃核心股列表,包含最近10日涨幅矩阵和所属题材",
"parameters": {"type": "object", "properties": {}, "required": []}
}
},
{
"type": "function",
"function": {
"name": "get_stock_quote",
"description": "获取单只股票实时行情(价格、涨跌幅、成交量、换手率等)",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "6位股票代码,如 600519"}
},
"required": ["code"]
}
}
},
{
"type": "function",
"function": {
"name": "get_fund_flow",
"description": "获取个股资金流向数据(主力净流入、超大单/大单/中单/小单流入流出)",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "6位股票代码"},
"name": {"type": "string", "description": "股票名称"},
"days": {"type": "integer", "description": "获取天数,默认30"}
},
"required": ["code", "name"]
}
}
},
{
"type": "function",
"function": {
"name": "get_news",
"description": "获取最近的财经快讯(新浪7x24,含宏观/行业/公司/海外动态),用于重要消息面梳理",
"parameters": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "获取条数,默认30,最大50"}
},
"required": []
}
}
}
]
async def execute_tool(tool_name: str, arguments: dict) -> str:
"""执行工具调用,返回 JSON 字符串"""
try:
if tool_name == "get_market_dashboard":
return await _get_market_dashboard()
elif tool_name == "get_theme_history":
return await _get_theme_history(arguments.get("date", ""))
elif tool_name == "get_active_core_stocks":
return await _get_active_core_stocks()
elif tool_name == "get_stock_quote":
return await _get_stock_quote(arguments.get("code", ""))
elif tool_name == "get_fund_flow":
return await _get_fund_flow(
arguments.get("code", ""),
arguments.get("name", ""),
arguments.get("days", 30)
)
elif tool_name == "get_news":
return await _get_news(arguments.get("limit", 30))
else:
return json.dumps({"error": f"未知工具: {tool_name}"})
except Exception as e:
return json.dumps({"error": str(e)})
async def _get_news(limit: int = 30) -> str:
"""获取财经快讯"""
from services.market_extra import fetch_news
news = await fetch_news(limit)
if not news:
return json.dumps({"error": "快讯获取失败或暂无数据"}, ensure_ascii=False)
return json.dumps({"count": len(news), "items": news}, ensure_ascii=False)
async def _get_market_dashboard() -> str:
"""获取市场看板数据"""
from routes.market_dashboard import _build_dashboard
data = await _build_dashboard()
return json.dumps(data, ensure_ascii=False, default=str)
async def _get_theme_history(date: str) -> str:
"""获取指定交易日题材涨幅前20"""
from database import get_connection
conn = get_connection()
try:
rows = conn.execute(
"SELECT * FROM daily_top_themes WHERE trade_date = ? ORDER BY rank ASC",
(date,)
).fetchall()
items = [dict(r) for r in rows]
return json.dumps({"date": date, "items": items}, ensure_ascii=False)
finally:
conn.close()
async def _get_active_core_stocks() -> str:
"""获取活跃核心股列表"""
from database import get_connection
from datetime import date as date_cls
conn = get_connection()
try:
# 最近10个有数据的交易日
rows = conn.execute(
"SELECT DISTINCT trade_date FROM daily_core_stocks ORDER BY trade_date DESC LIMIT 10"
).fetchall()
dates = [r["trade_date"] for r in reversed(rows)]
if not dates:
return json.dumps({"dates": [], "stocks": []}, ensure_ascii=False)
placeholders = ",".join("?" * len(dates))
rows = conn.execute(
f"""SELECT trade_date, stock_code, stock_name, f3, cover_count FROM daily_core_stocks
WHERE trade_date IN ({placeholders})
ORDER BY trade_date DESC, rank ASC""",
dates,
).fetchall()
stock_days = {}
for r in rows:
code = r["stock_code"]
s = stock_days.setdefault(code, {
"stockCode": code,
"stockName": r["stock_name"],
"coverCount": r["cover_count"],
"dailyGains": {},
"appearCount": 0,
"lastAppear": None,
})
s["dailyGains"][r["trade_date"]] = r["f3"]
s["appearCount"] += 1
if s["lastAppear"] is None or r["trade_date"] > s["lastAppear"]:
s["lastAppear"] = r["trade_date"]
# 只保留最近10日中出现次数最多的前25只(全量可达145KB,会把上下文撑爆)
stocks = sorted(stock_days.values(), key=lambda x: -x["appearCount"])[:25]
keep_codes = {s["stockCode"] for s in stocks}
themes_rows = conn.execute(
f"""SELECT stock_code, theme_code, theme_name FROM daily_core_stock_themes
WHERE trade_date IN ({placeholders})""",
dates,
).fetchall()
themes_by_stock = {}
for t in themes_rows:
if t["stock_code"] not in keep_codes:
continue
per = themes_by_stock.setdefault(t["stock_code"], {})
per.setdefault(t["theme_code"], {"theme_code": t["theme_code"], "theme_name": t["theme_name"]})
for s in stocks:
s["themes"] = list(themes_by_stock.get(s["stockCode"], {}).values())[:5]
latest = dates[-1] if dates else None
for s in stocks:
if s.get("lastAppear") and latest:
try:
d1 = date_cls.fromisoformat(latest)
d2 = date_cls.fromisoformat(s["lastAppear"])
s["daysSinceLastAppear"] = (d1 - d2).days
except ValueError:
s["daysSinceLastAppear"] = 0
else:
s["daysSinceLastAppear"] = 0
return json.dumps({"dates": dates, "stocks": stocks}, ensure_ascii=False)
finally:
conn.close()
async def _get_stock_quote(code: str) -> str:
"""获取个股实时行情"""
from services.tencent import fetch_quote
data = await fetch_quote(code)
if not data:
return json.dumps({"error": f"未找到股票 {code} 的数据"})
return json.dumps(data, ensure_ascii=False)
async def _get_fund_flow(code: str, name: str, days: int) -> str:
"""获取个股资金流向"""
from services.tencent import fetch_quote, get_market_prefix
from database import get_connection
# 先尝试从本地数据库获取历史资金流向
conn = get_connection()
try:
rows = conn.execute(
"SELECT * FROM cache WHERE key LIKE ? AND expires_at > datetime('now','localtime')",
(f"fund_flow:{code}%",)
).fetchall()
if rows:
return rows[0]["value"]
finally:
conn.close()
# 降级:通过腾讯获取基础行情数据
quote = await fetch_quote(code)
if not quote:
return json.dumps({"error": f"未找到股票 {code} 的数据"})
return json.dumps({
"code": code,
"name": name or quote.get("name", ""),
"currentPrice": quote.get("currentPrice", 0),
"changePercent": quote.get("changePercent", 0),
"volume": quote.get("volume", 0),
"amount": quote.get("amount", 0),
"note": "资金流向详细数据需通过东方财富API获取"
}, ensure_ascii=False)