report_parse.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. from fastapi import APIRouter, HTTPException
  2. from pydantic import BaseModel
  3. from typing import Optional, Any
  4. from app.agents.report_parse_agent import ReportParseAgent
  5. import logging
  6. import os
  7. import json
  8. import tempfile
  9. logger = logging.getLogger(__name__)
  10. router = APIRouter(prefix="/api/v1", tags=["report_parse"])
  11. _agent = None
  12. def get_agent():
  13. global _agent
  14. if _agent is None:
  15. _agent = ReportParseAgent()
  16. return _agent
  17. class ParseRequest(BaseModel):
  18. file_path: str
  19. family_id: Optional[int] = None
  20. user_id: Optional[int] = None
  21. class ParseResponse(BaseModel):
  22. code: int = 200
  23. message: str = "ok"
  24. data: dict = {}
  25. async def resolve_local_path(file_path: str) -> str:
  26. """将 file_path 解析为本地文件路径。
  27. 支持:本地路径、/uploads 共享卷路径、http(s) 远程 OSS URL(下载到临时文件)。"""
  28. if file_path.startswith(('http://', 'https://')):
  29. import httpx
  30. logger.info("report_parse: 远程文件 %s,下载到临时文件", file_path)
  31. async with httpx.AsyncClient(timeout=120, follow_redirects=True) as client:
  32. resp = await client.get(file_path)
  33. resp.raise_for_status()
  34. ext = os.path.splitext(file_path.split('?')[0])[1] or '.pdf'
  35. fd, tmp = tempfile.mkstemp(suffix=ext)
  36. with os.fdopen(fd, 'wb') as f:
  37. f.write(resp.content)
  38. logger.info("report_parse: 下载完成 -> %s", tmp)
  39. return tmp
  40. return file_path
  41. @router.post("/report/parse", response_model=ParseResponse)
  42. async def parse_report(req: ParseRequest):
  43. local = await resolve_local_path(req.file_path)
  44. if not os.path.exists(local):
  45. raise HTTPException(status_code=400, detail=f"文件不存在: {req.file_path}")
  46. logger.info("report_parse: file_path=%s family_id=%s user_id=%s", local, req.family_id, req.user_id)
  47. agent = get_agent()
  48. try:
  49. result = await agent.parse(local)
  50. return ParseResponse(data=result)
  51. except Exception as e:
  52. logger.error("报告解析失败: %s", e, exc_info=True)
  53. return ParseResponse(code=500, message=f"解析失败: {str(e)}", data={})
  54. class GenericParseRequest(BaseModel):
  55. file_path: str
  56. extra_context: Optional[dict] = None
  57. class GenericParseResponse(BaseModel):
  58. code: int = 200
  59. message: str = "ok"
  60. data: dict = {}
  61. @router.post("/report/parse-generic", response_model=GenericParseResponse)
  62. async def parse_report_generic(req: GenericParseRequest):
  63. """通用报告 LLM 兜底解析。
  64. 适用于指纹检测未匹配的未知类型报告。
  65. 直接交给 LLM 提取结构化数据,不经过算法预解析。
  66. """
  67. if not os.path.exists(req.file_path):
  68. raise HTTPException(status_code=400, detail=f"文件不存在: {req.file_path}")
  69. logger.info("report_parse_generic: file_path=%s", req.file_path)
  70. agent = get_agent()
  71. try:
  72. result = await agent.parse_generic(req.file_path, req.extra_context)
  73. return GenericParseResponse(data=result)
  74. except Exception as e:
  75. logger.error("通用报告解析失败: %s", e, exc_info=True)
  76. return GenericParseResponse(code=500, message=f"解析失败: {str(e)}", data={})