|
@@ -247,6 +247,17 @@ _FLORA_GROUP_TO_KEY: dict[str, str] = {
|
|
|
_FLORA_EXTRA_GROUPS: list[str] = ['有害菌属', '其它重要菌属']
|
|
_FLORA_EXTRA_GROUPS: list[str] = ['有害菌属', '其它重要菌属']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _indicator_constraint(indicators) -> str:
|
|
|
|
|
+ """采集指标清单 → LLM 提示词约束块;清单为空时返回空串(保持现状)。"""
|
|
|
|
|
+ if not indicators:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ names = "、".join(str(x) for x in indicators if x)
|
|
|
|
|
+ if not names:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ return (f"\n\n【采集指标约束】请仅提取以下指标项,name 必须与清单一致;"
|
|
|
|
|
+ f"报告中缺失的项返回 null,不得臆造:{names}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class ReportParseAgent:
|
|
class ReportParseAgent:
|
|
|
"""报告解析 Agent"""
|
|
"""报告解析 Agent"""
|
|
|
|
|
|
|
@@ -366,7 +377,7 @@ class ReportParseAgent:
|
|
|
if foods:
|
|
if foods:
|
|
|
result['foods'] = foods
|
|
result['foods'] = foods
|
|
|
|
|
|
|
|
- async def parse(self, file_path: str) -> dict:
|
|
|
|
|
|
|
+ async def parse(self, file_path: str, indicators: Optional[list] = None) -> dict:
|
|
|
"""解析 PDF 报告,算法解析 + LLM 兜底"""
|
|
"""解析 PDF 报告,算法解析 + LLM 兜底"""
|
|
|
# 1. 算法解析
|
|
# 1. 算法解析
|
|
|
result = parse_report_pdf_with_fallback(file_path)
|
|
result = parse_report_pdf_with_fallback(file_path)
|
|
@@ -381,7 +392,7 @@ class ReportParseAgent:
|
|
|
# 2. 如果解析不完整,LLM 兜底
|
|
# 2. 如果解析不完整,LLM 兜底
|
|
|
if result.get('_parse_incomplete') or not result.get('disease_risks'):
|
|
if result.get('_parse_incomplete') or not result.get('disease_risks'):
|
|
|
logger.info("算法解析不完整,尝试 LLM 兜底")
|
|
logger.info("算法解析不完整,尝试 LLM 兜底")
|
|
|
- llm_result = await self._parse_with_llm(file_path)
|
|
|
|
|
|
|
+ llm_result = await self._parse_with_llm(file_path, indicators)
|
|
|
if llm_result:
|
|
if llm_result:
|
|
|
# 合并 LLM 结果到算法结果上(LLM 覆盖缺失字段)
|
|
# 合并 LLM 结果到算法结果上(LLM 覆盖缺失字段)
|
|
|
for key in ['disease_risks', 'nutrition', 'amino_acids',
|
|
for key in ['disease_risks', 'nutrition', 'amino_acids',
|
|
@@ -399,7 +410,8 @@ class ReportParseAgent:
|
|
|
result.pop('_parse_incomplete', None)
|
|
result.pop('_parse_incomplete', None)
|
|
|
return result
|
|
return result
|
|
|
|
|
|
|
|
- async def parse_generic(self, file_path: str, extra_context: Optional[dict] = None) -> dict:
|
|
|
|
|
|
|
+ async def parse_generic(self, file_path: str, extra_context: Optional[dict] = None,
|
|
|
|
|
+ indicators: Optional[list] = None) -> dict:
|
|
|
"""通用报告 LLM 解析(不经过算法解析,直接走 LLM)"""
|
|
"""通用报告 LLM 解析(不经过算法解析,直接走 LLM)"""
|
|
|
try:
|
|
try:
|
|
|
from PyPDF2 import PdfReader
|
|
from PyPDF2 import PdfReader
|
|
@@ -411,7 +423,7 @@ class ReportParseAgent:
|
|
|
ctx_str = f"\n额外上下文:{json.dumps(extra_context, ensure_ascii=False)}"
|
|
ctx_str = f"\n额外上下文:{json.dumps(extra_context, ensure_ascii=False)}"
|
|
|
|
|
|
|
|
template = await get_prompt("report_parse_generic") or REPORT_PARSE_GENERIC_TEMPLATE
|
|
template = await get_prompt("report_parse_generic") or REPORT_PARSE_GENERIC_TEMPLATE
|
|
|
- prompt = template.format(report_text=text[:12000] + ctx_str)
|
|
|
|
|
|
|
+ prompt = template.format(report_text=text[:12000] + ctx_str) + _indicator_constraint(indicators)
|
|
|
|
|
|
|
|
if self.llm_api_key:
|
|
if self.llm_api_key:
|
|
|
import httpx
|
|
import httpx
|
|
@@ -437,7 +449,7 @@ class ReportParseAgent:
|
|
|
logger.error("通用LLM解析失败: %s", e)
|
|
logger.error("通用LLM解析失败: %s", e)
|
|
|
return {"reportType": "unknown", "error": str(e), "summary": {}, "indicators": [], "sections": []}
|
|
return {"reportType": "unknown", "error": str(e), "summary": {}, "indicators": [], "sections": []}
|
|
|
|
|
|
|
|
- async def _parse_with_llm(self, file_path: str) -> Optional[dict]:
|
|
|
|
|
|
|
+ async def _parse_with_llm(self, file_path: str, indicators: Optional[list] = None) -> Optional[dict]:
|
|
|
"""LLM 兜底解析"""
|
|
"""LLM 兜底解析"""
|
|
|
try:
|
|
try:
|
|
|
from PyPDF2 import PdfReader
|
|
from PyPDF2 import PdfReader
|
|
@@ -445,7 +457,7 @@ class ReportParseAgent:
|
|
|
text = '\n'.join(page.extract_text() or '' for page in reader.pages)
|
|
text = '\n'.join(page.extract_text() or '' for page in reader.pages)
|
|
|
|
|
|
|
|
template = await get_prompt("report_parse_gut") or REPORT_PARSE_GUT_TEMPLATE
|
|
template = await get_prompt("report_parse_gut") or REPORT_PARSE_GUT_TEMPLATE
|
|
|
- prompt = template.format(report_text=text[:8000])
|
|
|
|
|
|
|
+ prompt = template.format(report_text=text[:8000]) + _indicator_constraint(indicators)
|
|
|
|
|
|
|
|
if self.llm_api_key:
|
|
if self.llm_api_key:
|
|
|
# 调 OpenAI 兼容 API
|
|
# 调 OpenAI 兼容 API
|
|
@@ -477,7 +489,8 @@ class ReportParseAgent:
|
|
|
# ================================================================
|
|
# ================================================================
|
|
|
|
|
|
|
|
async def parse_by_type(self, file_path: str, report_type: str,
|
|
async def parse_by_type(self, file_path: str, report_type: str,
|
|
|
- extra_context: Optional[dict] = None) -> dict:
|
|
|
|
|
|
|
+ extra_context: Optional[dict] = None,
|
|
|
|
|
+ indicators: Optional[list] = None) -> dict:
|
|
|
"""按报告类型路由到专用解析器。
|
|
"""按报告类型路由到专用解析器。
|
|
|
|
|
|
|
|
:param file_path: PDF 文件路径
|
|
:param file_path: PDF 文件路径
|
|
@@ -500,7 +513,7 @@ class ReportParseAgent:
|
|
|
return await self._parse_scanned_image(file_path)
|
|
return await self._parse_scanned_image(file_path)
|
|
|
else:
|
|
else:
|
|
|
logger.info("未识别的报告类型 %s,走通用解析", report_type)
|
|
logger.info("未识别的报告类型 %s,走通用解析", report_type)
|
|
|
- return await self.parse_generic(file_path, extra_context)
|
|
|
|
|
|
|
+ return await self.parse_generic(file_path, extra_context, indicators)
|
|
|
|
|
|
|
|
# ---- 格式检测 ----
|
|
# ---- 格式检测 ----
|
|
|
|
|
|