finsight / api /main.py
Maggei's picture
Week4: Gradio 前端 + PDF 入库 + HF Spaces 部署件
1f4b3f7
Raw
History Blame Contribute Delete
4.05 kB
"""FinSight FastAPI 接口层。
接口:
GET /health 健康检查 + 知识库规模
POST /analyze 文档分析(走 LangGraph:router → 4Agent并行 → synthesis)
POST /chat 问答(自动 RAG 混合检索;返回答案 + 来源)
POST /chat/stream 问答流式输出(text/plain 逐块)
POST /upload 上传文本入知识库
所有请求统一经 LangGraph 状态机执行(router 决定路径)。同步图调用由 FastAPI
自动放到线程池,天然支持并发。换微调/推理服务只动 agents/llm.py,本层不变。
启动:.venv/bin/uvicorn api.main:app --port 8000
"""
from fastapi import FastAPI, UploadFile
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from agents.analysts import QA
from agents.llm import chat_stream
from graph import analyze
from graph.workflow import get_retriever, refresh_retriever
from rag import KnowledgeBase, read_pdf
app = FastAPI(title="FinSight", description="金融研报多Agent分析系统", version="0.1")
class AnalyzeReq(BaseModel):
text: str
class ChatReq(BaseModel):
question: str
context: str | None = None # 显式给则跳过 RAG 检索
class UploadReq(BaseModel):
doc_id: str
text: str
@app.get("/health")
def health():
return {"status": "ok", "kb_chunks": KnowledgeBase().count(),
"models": {"analysts": "finsight-qwen", "qa/router": "qwen2.5:7b-instruct"}}
@app.post("/analyze")
def analyze_doc(req: AnalyzeReq):
"""文档分析:返回 intent + 结构化报告(若路由判为问答则返回 answer)。"""
r = analyze(req.text)
out = {"intent": r["intent"], "router": r.get("router")}
if r["intent"] == "doc_analysis":
out["report"] = r["report"]
else:
out["answer"] = r["answer"].get("result")
out["sources"] = r.get("sources", [])
return out
@app.post("/chat")
def chat_qa(req: ChatReq):
"""问答:自动 RAG 检索注入,返回答案 + 来源。"""
r = analyze(req.question, context=req.context)
if r["intent"] == "doc_analysis": # 用户把文档发进了问答口,如实回路由结果
return {"intent": "doc_analysis", "report": r["report"]}
return {"intent": "qa", "answer": r["answer"].get("result"),
"sources": r.get("sources", [])}
@app.post("/chat/stream")
def chat_stream_qa(req: ChatReq):
"""流式问答:先检索注入上下文,再逐块流式输出纯文本答案。"""
ctx = req.context
if ctx is None:
try:
ctx = get_retriever().context(req.question, k=4) or None
except Exception:
ctx = None
system = ("你是专业的金融问答助手。基于问题(如提供【参考资料】则优先依据资料)"
"给出准确、客观的回答,不编造数据,涉及投资建议时提示风险。")
user = req.question if ctx is None else f"【参考资料】\n{ctx}\n\n【问题】\n{req.question}"
return StreamingResponse(
chat_stream(system, user, model=QA.model),
media_type="text/plain; charset=utf-8",
)
@app.post("/upload")
def upload(req: UploadReq):
"""把一段文本切块入库,并刷新检索器。"""
kb = KnowledgeBase()
n = kb.add(req.doc_id, req.text, meta={"source": "upload"})
refresh_retriever()
return {"doc_id": req.doc_id, "chunks_added": n, "kb_total": kb.count()}
@app.post("/upload/pdf")
async def upload_pdf(file: UploadFile):
"""上传研报 PDF 入库:解析文本层 → 切块 → 入库 → 刷新检索器。doc_id 取文件名。"""
doc_id = (file.filename or "uploaded").rsplit(".", 1)[0]
text = read_pdf(file.file) # 直接读上传流,不落盘
if not text.strip():
return {"doc_id": doc_id, "chunks_added": 0,
"error": "无文本层(可能是扫描件),未入库"}
kb = KnowledgeBase()
n = kb.add(doc_id, text, meta={"source": "pdf", "file": file.filename})
refresh_retriever()
return {"doc_id": doc_id, "chunks_added": n, "kb_total": kb.count()}