report_parse_agent.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """
  2. 报告解析 Agent:算法解析 + LLM 兜底
  3. """
  4. import json
  5. import logging
  6. from typing import Optional
  7. from app.config import settings
  8. from app.parsers.pdf_parser import parse_report_pdf_with_fallback
  9. logger = logging.getLogger(__name__)
  10. class ReportParseAgent:
  11. """报告解析 Agent"""
  12. def __init__(self):
  13. self.llm_api_key = getattr(settings, 'llm_api_key', '')
  14. async def parse(self, file_path: str) -> dict:
  15. """解析 PDF 报告,算法解析 + LLM 兜底"""
  16. # 1. 算法解析
  17. result = parse_report_pdf_with_fallback(file_path)
  18. logger.info("算法解析完成: format=%s, overview_keys=%d",
  19. result.get('format'), len(result.get('overview', {})))
  20. # 2. 如果解析不完整,LLM 兜底
  21. if result.get('_parse_incomplete') or not result.get('disease_risks'):
  22. logger.info("算法解析不完整,尝试 LLM 兜底")
  23. llm_result = await self._parse_with_llm(file_path)
  24. if llm_result:
  25. # 合并 LLM 结果到算法结果上(LLM 覆盖缺失字段)
  26. for key in ['disease_risks', 'nutrition', 'amino_acids',
  27. 'vitamins', 'trace_elements', 'indicators']:
  28. if key in llm_result and not result.get(key):
  29. result[key] = llm_result[key]
  30. if llm_result.get('overview'):
  31. for k, v in llm_result['overview'].items():
  32. if k not in result.get('overview', {}):
  33. result.setdefault('overview', {})[k] = v
  34. # 清理内部标记
  35. result.pop('_parse_incomplete', None)
  36. return result
  37. async def _parse_with_llm(self, file_path: str) -> Optional[dict]:
  38. """LLM 兜底解析"""
  39. try:
  40. from PyPDF2 import PdfReader
  41. reader = PdfReader(file_path)
  42. text = '\n'.join(page.extract_text() or '' for page in reader.pages)
  43. # 构造 prompt
  44. prompt = f"""你是一个肠道菌群检测报告解析专家。请从以下PDF文本中提取结构化数据,返回JSON格式。
  45. 文本内容:
  46. {text[:8000]}
  47. 请按以下JSON Schema返回:
  48. {{
  49. "overview": {{ "person_name": "", "report_number": "", "age": 0, "gender": "male/female",
  50. "overallScore": 0, "gutHealthScore": 0, "chronicDiseaseScore": 0, "nutritionScore": 0,
  51. "gutAge": "", "gutType": "" }},
  52. "disease_risks": [{{"name": "", "value": "", "status": ""}}],
  53. "nutrition": [{{"name": "", "value": "", "status": ""}}],
  54. "amino_acids": [{{"name": "", "value": "", "status": ""}}],
  55. "vitamins": [{{"name": "", "value": "", "status": ""}}],
  56. "trace_elements": [{{"name": "", "value": "", "status": ""}}]
  57. }}
  58. 只返回JSON,不要其他文字。"""
  59. if self.llm_api_key:
  60. # 调 OpenAI 兼容 API
  61. import httpx
  62. async with httpx.AsyncClient(timeout=60) as client:
  63. resp = await client.post(
  64. f"{settings.llm_base_url}/chat/completions",
  65. json={
  66. "model": settings.llm_model or "gpt-4o",
  67. "messages": [{"role": "user", "content": prompt}],
  68. "temperature": 0.1,
  69. },
  70. headers={"Authorization": f"Bearer {self.llm_api_key}"},
  71. )
  72. resp.raise_for_status()
  73. data = resp.json()
  74. content = data['choices'][0]['message']['content']
  75. content = content.replace('```json', '').replace('```', '').strip()
  76. return json.loads(content)
  77. else:
  78. logger.warning("LLM 未配置,跳过 LLM 兜底")
  79. return None
  80. except Exception as e:
  81. logger.warning("LLM 解析失败: %s", e)
  82. return None