| """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 |
|
|
|
|
| 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()} |
|
|