Jelajahi Sumber

feat: 创建 ReportParseAgent(算法解析 + LLM 兜底)

Xiaogang Liao 1 bulan lalu
induk
melakukan
33862d95e3
1 mengubah file dengan 95 tambahan dan 0 penghapusan
  1. 95 0
      cfc-langgraph/app/agents/report_parse_agent.py

+ 95 - 0
cfc-langgraph/app/agents/report_parse_agent.py

@@ -0,0 +1,95 @@
+"""
+报告解析 Agent:算法解析 + LLM 兜底
+"""
+import json
+import logging
+from typing import Optional
+
+from app.config import settings
+from app.parsers.pdf_parser import parse_report_pdf_with_fallback
+
+logger = logging.getLogger(__name__)
+
+
+class ReportParseAgent:
+    """报告解析 Agent"""
+
+    def __init__(self):
+        self.llm_api_key = getattr(settings, 'llm_api_key', '')
+
+    async def parse(self, file_path: str) -> dict:
+        """解析 PDF 报告,算法解析 + LLM 兜底"""
+        # 1. 算法解析
+        result = parse_report_pdf_with_fallback(file_path)
+        logger.info("算法解析完成: format=%s, overview_keys=%d",
+                     result.get('format'), len(result.get('overview', {})))
+
+        # 2. 如果解析不完整,LLM 兜底
+        if result.get('_parse_incomplete') or not result.get('disease_risks'):
+            logger.info("算法解析不完整,尝试 LLM 兜底")
+            llm_result = await self._parse_with_llm(file_path)
+            if llm_result:
+                # 合并 LLM 结果到算法结果上(LLM 覆盖缺失字段)
+                for key in ['disease_risks', 'nutrition', 'amino_acids',
+                            'vitamins', 'trace_elements', 'indicators']:
+                    if key in llm_result and not result.get(key):
+                        result[key] = llm_result[key]
+                if llm_result.get('overview'):
+                    for k, v in llm_result['overview'].items():
+                        if k not in result.get('overview', {}):
+                            result.setdefault('overview', {})[k] = v
+
+        # 清理内部标记
+        result.pop('_parse_incomplete', None)
+        return result
+
+    async def _parse_with_llm(self, file_path: str) -> Optional[dict]:
+        """LLM 兜底解析"""
+        try:
+            from PyPDF2 import PdfReader
+            reader = PdfReader(file_path)
+            text = '\n'.join(page.extract_text() or '' for page in reader.pages)
+
+            # 构造 prompt
+            prompt = f"""你是一个肠道菌群检测报告解析专家。请从以下PDF文本中提取结构化数据,返回JSON格式。
+
+文本内容:
+{text[:8000]}
+
+请按以下JSON Schema返回:
+{{
+    "overview": {{ "person_name": "", "report_number": "", "age": 0, "gender": "male/female",
+        "overallScore": 0, "gutHealthScore": 0, "chronicDiseaseScore": 0, "nutritionScore": 0,
+        "gutAge": "", "gutType": "" }},
+    "disease_risks": [{{"name": "", "value": "", "status": ""}}],
+    "nutrition": [{{"name": "", "value": "", "status": ""}}],
+    "amino_acids": [{{"name": "", "value": "", "status": ""}}],
+    "vitamins": [{{"name": "", "value": "", "status": ""}}],
+    "trace_elements": [{{"name": "", "value": "", "status": ""}}]
+}}
+
+只返回JSON,不要其他文字。"""
+
+            if self.llm_api_key:
+                # 调 OpenAI 兼容 API
+                import httpx
+                async with httpx.AsyncClient(timeout=60) as client:
+                    resp = await client.post(
+                        f"{settings.llm_base_url}/chat/completions",
+                        json={
+                            "model": settings.llm_model or "gpt-4o",
+                            "messages": [{"role": "user", "content": prompt}],
+                            "temperature": 0.1,
+                        },
+                        headers={"Authorization": f"Bearer {self.llm_api_key}"},
+                    )
+                    data = resp.json()
+                    content = data['choices'][0]['message']['content']
+                    content = content.replace('```json', '').replace('```', '').strip()
+                    return json.loads(content)
+            else:
+                logger.warning("LLM 未配置,跳过 LLM 兜底")
+                return None
+        except Exception as e:
+            logger.warning("LLM 解析失败: %s", e)
+            return None