from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Optional, Any from app.agents.report_parse_agent import ReportParseAgent import logging import os import json import tempfile logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1", tags=["report_parse"]) _agent = None def get_agent(): global _agent if _agent is None: _agent = ReportParseAgent() return _agent class ParseRequest(BaseModel): file_path: str family_id: Optional[int] = None user_id: Optional[int] = None class ParseResponse(BaseModel): code: int = 200 message: str = "ok" data: dict = {} async def resolve_local_path(file_path: str) -> str: """将 file_path 解析为本地文件路径。 支持:本地路径、/uploads 共享卷路径、http(s) 远程 OSS URL(下载到临时文件)。""" if file_path.startswith(('http://', 'https://')): import httpx logger.info("report_parse: 远程文件 %s,下载到临时文件", file_path) async with httpx.AsyncClient(timeout=120, follow_redirects=True) as client: resp = await client.get(file_path) resp.raise_for_status() ext = os.path.splitext(file_path.split('?')[0])[1] or '.pdf' fd, tmp = tempfile.mkstemp(suffix=ext) with os.fdopen(fd, 'wb') as f: f.write(resp.content) logger.info("report_parse: 下载完成 -> %s", tmp) return tmp return file_path @router.post("/report/parse", response_model=ParseResponse) async def parse_report(req: ParseRequest): local = await resolve_local_path(req.file_path) if not os.path.exists(local): raise HTTPException(status_code=400, detail=f"文件不存在: {req.file_path}") logger.info("report_parse: file_path=%s family_id=%s user_id=%s", local, req.family_id, req.user_id) agent = get_agent() try: result = await agent.parse(local) return ParseResponse(data=result) except Exception as e: logger.error("报告解析失败: %s", e, exc_info=True) return ParseResponse(code=500, message=f"解析失败: {str(e)}", data={}) class GenericParseRequest(BaseModel): file_path: str extra_context: Optional[dict] = None class GenericParseResponse(BaseModel): code: int = 200 message: str = "ok" data: dict = {} @router.post("/report/parse-generic", response_model=GenericParseResponse) async def parse_report_generic(req: GenericParseRequest): """通用报告 LLM 兜底解析。 适用于指纹检测未匹配的未知类型报告。 直接交给 LLM 提取结构化数据,不经过算法预解析。 """ if not os.path.exists(req.file_path): raise HTTPException(status_code=400, detail=f"文件不存在: {req.file_path}") logger.info("report_parse_generic: file_path=%s", req.file_path) agent = get_agent() try: result = await agent.parse_generic(req.file_path, req.extra_context) return GenericParseResponse(data=result) except Exception as e: logger.error("通用报告解析失败: %s", e, exc_info=True) return GenericParseResponse(code=500, message=f"解析失败: {str(e)}", data={})