Initial commit

This commit is contained in:
2026-07-18 15:51:01 +08:00
commit f2c80c5a9c
799 changed files with 133475 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
"""MCP 工具实现。
每个工具:接收自然语言查询 → DashScope 嵌入 → Qdrant 检索 → 格式化返回。
嵌入 provider 复用 M5,检索复用 M6。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from dotenv import load_dotenv
from loguru import logger
from mcp.server.fastmcp import FastMCP
from embedding import make_sync_provider
from vectorstore import SearchFilter, VectorStore, make_qdrant_client
# 加载 .env(API key 等)
load_dotenv()
# --------------------------------------------------------------------------- #
# 单例(模块加载时初始化,所有工具共用)
# --------------------------------------------------------------------------- #
@dataclass
class _Backend:
embedder: Any # EmbeddingProvider
vector_store: VectorStore
_backend: _Backend | None = None
def _get_backend() -> _Backend:
global _backend
if _backend is None:
emb = make_sync_provider() # 读取 EMBEDDING_PROVIDER 环境变量
logger.info("MCP embedder 就绪: dim={}", emb.dim)
client = make_qdrant_client()
store = VectorStore(client)
logger.info("MCP vector_store 就绪: count={}", store.count())
_backend = _Backend(embedder=emb, vector_store=store)
return _backend
# --------------------------------------------------------------------------- #
# 嵌入 + 检索
# --------------------------------------------------------------------------- #
def _search(
query: str,
top_k: int = 10,
filter: SearchFilter | None = None,
) -> list[dict[str, Any]]:
"""嵌入查询文本 → Qdrant 检索 → 返回 dict 列表。"""
be = _get_backend()
vec = be.embedder.embed_one(query)
results = be.vector_store.query(
query_vector=vec, top_k=top_k, filter=filter, score_threshold=0.3,
)
return [
{
"title": r.title,
"url": r.url,
"source": r.source_id,
"score": round(r.score, 4),
"publish_time": r.publish_time.isoformat() if r.publish_time else None,
"event": {
"sentiment": (r.event or {}).get("sentiment"),
"importance": (r.event or {}).get("importance"),
"event_type": (r.event or {}).get("event_type"),
"stock_codes": (r.event or {}).get("stock_codes", []),
"company_names": (r.event or {}).get("company_names", []),
"industries": (r.event or {}).get("industries", []),
"summary": (r.event or {}).get("summary"),
},
}
for r in results
]
# --------------------------------------------------------------------------- #
# MCP 服务器 & 工具
# --------------------------------------------------------------------------- #
mcp = FastMCP(
name="A股DeepResearch",
instructions="A 股 Deep Research 知识库——语义检索财经新闻与投资事件。",
)
def _fmt_results(hits: list[dict[str, Any]], query: str) -> str:
"""把检索结果格式化为 Markdown 文本。"""
if not hits:
return f"未找到与「{query}」相关的结果。"
lines = [f"# 检索结果: {query}", "", f"{len(hits)} 条:", ""]
for i, h in enumerate(hits, 1):
ev = h["event"]
sentiment = {"positive": "🟢利好", "neutral": "⚪中性", "negative": "🔴利空"}.get(
ev.get("sentiment"), ""
)
lines.append(f"### {i}. {h['title']}")
lines.append(f"- 来源: {h['source']} | 相似度: {h['score']} | {sentiment}")
lines.append(f"- 时间: {h['publish_time'] or '未知'}")
if ev.get("company_names"):
lines.append(f"- 公司: {', '.join(ev['company_names'][:5])}")
if ev.get("stock_codes"):
lines.append(f"- 代码: {', '.join(ev['stock_codes'][:5])}")
if ev.get("industries"):
lines.append(f"- 行业: {', '.join(ev['industries'][:3])}")
if ev.get("summary"):
lines.append(f"- 摘要: {ev['summary']}")
lines.append(f"- URL: {h['url']}")
lines.append("")
return "\n".join(lines)
# --------------------------------------------------------------------------- #
# Tool 1: search_news
# --------------------------------------------------------------------------- #
@mcp.tool()
def search_news(query: str, top_k: int = 10) -> str:
"""语义检索 A 股财经新闻知识库。
参数:
query: 自然语言查询(如 "宁德时代最新动态" "AI 行业政策")
top_k: 返回条数(默认 10)
返回: Markdown 格式的检索结果,含标题、来源、相似度、URL。
"""
logger.info("search_news query={!r} top_k={}", query, top_k)
hits = _search(query, top_k=top_k)
return _fmt_results(hits, query)
# --------------------------------------------------------------------------- #
# Tool 2: search_company_news
# --------------------------------------------------------------------------- #
@mcp.tool()
def search_company_news(query: str, company: str, top_k: int = 10) -> str:
"""检索指定公司的相关新闻。
参数:
query: 自然语言查询
company: 公司名称(如 "宁德时代" "贵州茅台")
top_k: 返回条数(默认 10)
返回: Markdown 格式的检索结果。
"""
logger.info("search_company_news query={!r} company={!r}", query, company)
hits = _search(
query, top_k=top_k,
filter=SearchFilter(company_names=[company]),
)
if not hits:
# 降级:无精确命中时做纯语义搜索
logger.info("company 精确命中 0 条,降级为纯语义搜索")
hits = _search(query, top_k=top_k)
return _fmt_results(hits, f"{query} (公司:{company})")
# --------------------------------------------------------------------------- #
# Tool 3: search_industry_news
# --------------------------------------------------------------------------- #
@mcp.tool()
def search_industry_news(query: str, industry: str, top_k: int = 10) -> str:
"""检索指定行业的相关新闻。
参数:
query: 自然语言查询
industry: 行业名(如 "动力电池" "白酒" "半导体")
top_k: 返回条数(默认 10)
返回: Markdown 格式的检索结果。
"""
logger.info("search_industry_news query={!r} industry={!r}", query, industry)
hits = _search(
query, top_k=top_k,
filter=SearchFilter(industries=[industry]),
)
if not hits:
logger.info("industry 精确命中 0 条,降级为纯语义搜索")
hits = _search(query, top_k=top_k)
return _fmt_results(hits, f"{query} (行业:{industry})")
# --------------------------------------------------------------------------- #
# Tool 4: search_stock_events
# --------------------------------------------------------------------------- #
@mcp.tool()
def search_stock_events(query: str, stock_code: str, top_k: int = 10) -> str:
"""检索指定股票代码相关的投资事件。
参数:
query: 自然语言查询
stock_code: 6 位 A 股代码(如 "300750" "000001",可带 .SH/.SZ 后缀)
top_k: 返回条数(默认 10)
返回: Markdown 格式的检索结果。
"""
# 标准化:去掉后缀,补一致性
code = stock_code.strip().split(".")[0].upper()
logger.info("search_stock_events query={!r} code={!r}", query, code)
hits = _search(
query, top_k=top_k,
filter=SearchFilter(stock_codes=[code]),
)
if not hits:
logger.info("stock_code 精确命中 0 条,降级为纯语义搜索")
hits = _search(query, top_k=top_k)
return _fmt_results(hits, f"{query} (代码:{code})")
# --------------------------------------------------------------------------- #
# Tool 5: search_sentiment_trend
# --------------------------------------------------------------------------- #
@mcp.tool()
def search_sentiment_trend(query: str, sentiment: str = "all", top_k: int = 20) -> str:
"""检索并统计特定情绪倾向的新闻。
参数:
query: 自然语言查询
sentiment: positive(利好) / negative(利空) / neutral(中性) / all(全部,默认)
top_k: 返回条数(默认 20)
返回: Markdown 格式的检索结果 + 情绪分布统计。
"""
logger.info("search_sentiment_trend query={!r} sentiment={!r}", query, sentiment)
filt = None
if sentiment in ("positive", "negative", "neutral"):
filt = SearchFilter(sentiment=sentiment)
hits = _search(query, top_k=top_k, filter=filt)
# 统计情绪分布
pos = sum(1 for h in hits if h["event"].get("sentiment") == "positive")
neg = sum(1 for h in hits if h["event"].get("sentiment") == "negative")
neu = sum(1 for h in hits if h["event"].get("sentiment") == "neutral")
header = (
f"# 情绪趋势: {query}\n\n"
f"{len(hits)} 条 | "
f"🟢利好 {pos} | 🔴利空 {neg} | ⚪中性 {neu}\n"
)
return header + "\n" + _fmt_results(hits, query)