""" 报告解析 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__) # 算法解析器使用中文键名(与 extract_full_report_v5.py 一致), # Java 消费端期望英文键名;在此做双向映射,Java 读取英文键即可。 _CN_TO_EN_OVERVIEW: dict[str, str] = { '健康总分': 'overallScore', '菌群健康': 'gutHealthScore', '慢病控制': 'chronicDiseaseScore', '营养均衡': 'nutritionScore', '肠道菌群平衡': 'balanceScore', '菌群多样性': 'diversityScore', '有益菌': 'beneficialScore', '有害菌': 'harmfulScore', '核心菌属': 'coreGenusScore', } class ReportParseAgent: """报告解析 Agent""" def __init__(self): self.llm_api_key = getattr(settings, 'llm_api_key', '') @staticmethod def _normalize_overview_keys(result: dict) -> None: """将 overview 中的中文键名映射为英文键名(双向写入),兼容 Java 消费端。""" overview = result.get('overview') if not overview: return for _cn, _en in _CN_TO_EN_OVERVIEW.items(): if _cn in overview and _en not in overview: overview[_en] = overview[_cn] 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', {}))) # 1.5 归一化 overview 键名:中文 → 英文(Java 消费端兼容) self._normalize_overview_keys(result) # 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}"}, ) resp.raise_for_status() 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