增强模板填充服务支持非结构化文档AI分析
- 引入markdown_ai_service服务支持Markdown文档处理
- 实现_nonstructured_docs_for_fields方法对非结构化文档进行AI分析
- 优化LLM提示词,改进数据提取的准确性和格式规范
- 支持从Markdown表格格式{tables: [{headers: [...], rows: [...]}]}中提取数据
- 添加文档章节结构解析,提升上下文理解能力
- 增加JSON响应格式修复功能,提高数据解析成功率
This commit is contained in:
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
from app.core.database import mongodb
|
from app.core.database import mongodb
|
||||||
from app.services.llm_service import llm_service
|
from app.services.llm_service import llm_service
|
||||||
from app.core.document_parser import ParserFactory
|
from app.core.document_parser import ParserFactory
|
||||||
|
from app.services.markdown_ai_service import markdown_ai_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -233,6 +234,12 @@ class TemplateFillService:
|
|||||||
confidence=1.0
|
confidence=1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 无法直接从结构化数据提取,尝试 AI 分析非结构化文档
|
||||||
|
ai_structured = await self._analyze_unstructured_docs_for_fields(source_docs, field, user_hint)
|
||||||
|
if ai_structured:
|
||||||
|
logger.info(f"✅ 字段 {field.name} 通过 AI 分析结构化提取到数据")
|
||||||
|
return ai_structured
|
||||||
|
|
||||||
# 无法从结构化数据提取,使用 LLM
|
# 无法从结构化数据提取,使用 LLM
|
||||||
logger.info(f"字段 {field.name} 无法直接从结构化数据提取,使用 LLM...")
|
logger.info(f"字段 {field.name} 无法直接从结构化数据提取,使用 LLM...")
|
||||||
|
|
||||||
@@ -244,18 +251,20 @@ class TemplateFillService:
|
|||||||
if user_hint:
|
if user_hint:
|
||||||
hint_text = f"{user_hint}。{hint_text}"
|
hint_text = f"{user_hint}。{hint_text}"
|
||||||
|
|
||||||
prompt = f"""你是一个专业的数据提取专家。请从以下文档内容中提取"{field.name}"字段的所有行数据。
|
prompt = f"""你是一个专业的数据提取专家。请从以下文档内容中提取与"{field.name}"相关的所有信息。
|
||||||
|
|
||||||
参考文档内容(已提取" {field.name}"列的数据):
|
提示词: {hint_text}
|
||||||
|
|
||||||
|
文档内容:
|
||||||
{context_text}
|
{context_text}
|
||||||
|
|
||||||
请提取上述所有行的" {field.name}"值,存入数组。每一行对应数组中的一个元素。
|
请分析文档结构(可能包含表格、标题段落等),找出所有与"{field.name}"相关的数据。
|
||||||
如果某行该字段为空,请用空字符串""占位。
|
如果找到表格数据,返回多行值;如果是非表格段落,提取关键信息。
|
||||||
|
|
||||||
请严格按照以下 JSON 格式输出,不要添加任何解释:
|
请严格按照以下 JSON 格式输出:
|
||||||
{{
|
{{
|
||||||
"values": ["第1行的值", "第2行的值", "第3行的值", ...],
|
"values": ["第1行的值", "第2行的值", ...],
|
||||||
"source": "数据来源的文档描述",
|
"source": "数据来源描述",
|
||||||
"confidence": 0.0到1.0之间的置信度
|
"confidence": 0.0到1.0之间的置信度
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
@@ -473,6 +482,29 @@ class TemplateFillService:
|
|||||||
elif isinstance(row, list):
|
elif isinstance(row, list):
|
||||||
doc_content += " | ".join(str(cell) for cell in row) + "\n"
|
doc_content += " | ".join(str(cell) for cell in row) + "\n"
|
||||||
row_count += 1
|
row_count += 1
|
||||||
|
elif doc.structured_data and doc.structured_data.get("tables"):
|
||||||
|
# Markdown 表格格式: {tables: [{headers: [...], rows: [...]}]}
|
||||||
|
tables = doc.structured_data.get("tables", [])
|
||||||
|
for table in tables:
|
||||||
|
if isinstance(table, dict):
|
||||||
|
headers = table.get("headers", [])
|
||||||
|
rows = table.get("rows", [])
|
||||||
|
if rows and headers:
|
||||||
|
doc_content += f"\n【文档: {doc.filename} - 表格】\n"
|
||||||
|
doc_content += " | ".join(str(h) for h in headers) + "\n"
|
||||||
|
for row in rows:
|
||||||
|
if isinstance(row, list):
|
||||||
|
doc_content += " | ".join(str(cell) for cell in row) + "\n"
|
||||||
|
row_count += 1
|
||||||
|
# 如果有标题结构,也添加上下文
|
||||||
|
if doc.structured_data.get("titles"):
|
||||||
|
titles = doc.structured_data.get("titles", [])
|
||||||
|
doc_content += f"\n【文档章节结构】\n"
|
||||||
|
for title in titles[:20]: # 限制前20个标题
|
||||||
|
doc_content += f"{'#' * title.get('level', 1)} {title.get('text', '')}\n"
|
||||||
|
# 如果没有提取到表格内容,使用纯文本
|
||||||
|
if not doc_content.strip():
|
||||||
|
doc_content = doc.content[:5000] if doc.content else ""
|
||||||
elif doc.content:
|
elif doc.content:
|
||||||
doc_content = doc.content[:5000]
|
doc_content = doc.content[:5000]
|
||||||
|
|
||||||
@@ -720,6 +752,21 @@ class TemplateFillService:
|
|||||||
logger.info(f"从文档 {doc.filename} 提取到 {len(values)} 个值")
|
logger.info(f"从文档 {doc.filename} 提取到 {len(values)} 个值")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# 处理 Markdown 表格格式: {tables: [{headers: [...], rows: [...]}]}
|
||||||
|
elif structured.get("tables"):
|
||||||
|
tables = structured.get("tables", [])
|
||||||
|
for table in tables:
|
||||||
|
if isinstance(table, dict):
|
||||||
|
headers = table.get("headers", [])
|
||||||
|
rows = table.get("rows", [])
|
||||||
|
values = self._extract_column_values(rows, headers, field_name)
|
||||||
|
if values:
|
||||||
|
all_values.extend(values)
|
||||||
|
logger.info(f"从 Markdown 表格提取到 {len(values)} 个值")
|
||||||
|
break
|
||||||
|
if all_values:
|
||||||
|
break
|
||||||
|
|
||||||
return all_values
|
return all_values
|
||||||
|
|
||||||
def _extract_column_values(self, rows: List, columns: List, field_name: str) -> List[str]:
|
def _extract_column_values(self, rows: List, columns: List, field_name: str) -> List[str]:
|
||||||
@@ -1005,6 +1052,145 @@ class TemplateFillService:
|
|||||||
content = text.strip()[:500] if text.strip() else ""
|
content = text.strip()[:500] if text.strip() else ""
|
||||||
return [content] if content else []
|
return [content] if content else []
|
||||||
|
|
||||||
|
async def _analyze_unstructured_docs_for_fields(
|
||||||
|
self,
|
||||||
|
source_docs: List[SourceDocument],
|
||||||
|
field: TemplateField,
|
||||||
|
user_hint: Optional[str] = None
|
||||||
|
) -> Optional[FillResult]:
|
||||||
|
"""
|
||||||
|
对非结构化文档进行 AI 分析,尝试提取结构化数据
|
||||||
|
|
||||||
|
适用于 Markdown 等没有表格格式的文档,通过 AI 分析提取结构化信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_docs: 源文档列表
|
||||||
|
field: 字段定义
|
||||||
|
user_hint: 用户提示
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FillResult 如果提取成功,否则返回 None
|
||||||
|
"""
|
||||||
|
# 找出非结构化的 Markdown/TXT 文档(没有表格的)
|
||||||
|
unstructured_docs = []
|
||||||
|
for doc in source_docs:
|
||||||
|
if doc.doc_type in ["md", "txt", "markdown"]:
|
||||||
|
# 检查是否有表格
|
||||||
|
has_tables = (
|
||||||
|
doc.structured_data and
|
||||||
|
doc.structured_data.get("tables") and
|
||||||
|
len(doc.structured_data.get("tables", [])) > 0
|
||||||
|
)
|
||||||
|
if not has_tables:
|
||||||
|
unstructured_docs.append(doc)
|
||||||
|
|
||||||
|
if not unstructured_docs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f"发现 {len(unstructured_docs)} 个非结构化文档,尝试 AI 分析...")
|
||||||
|
|
||||||
|
# 对每个非结构化文档进行 AI 分析
|
||||||
|
for doc in unstructured_docs:
|
||||||
|
try:
|
||||||
|
# 使用 markdown_ai_service 的 statistics 分析类型
|
||||||
|
# 这种类型专门用于政府统计公报等包含数据的文档
|
||||||
|
hint_text = field.hint if field.hint else f"请提取{field.name}的信息"
|
||||||
|
if user_hint:
|
||||||
|
hint_text = f"{user_hint}。{hint_text}"
|
||||||
|
|
||||||
|
# 构建针对字段提取的提示词
|
||||||
|
prompt = f"""你是一个专业的数据提取专家。请从以下文档内容中提取与"{field.name}"相关的所有数据。
|
||||||
|
|
||||||
|
字段提示: {hint_text}
|
||||||
|
|
||||||
|
文档内容:
|
||||||
|
{doc.content[:8000] if doc.content else ""}
|
||||||
|
|
||||||
|
请完成以下任务:
|
||||||
|
1. 仔细阅读文档,找出所有与"{field.name}"相关的数据
|
||||||
|
2. 如果文档中有表格数据,提取表格中的对应列值
|
||||||
|
3. 如果文档中是段落描述,提取其中的关键数值或结论
|
||||||
|
4. 返回提取的所有值(可能多个,用数组存储)
|
||||||
|
|
||||||
|
请用严格的 JSON 格式返回:
|
||||||
|
{{
|
||||||
|
"values": ["值1", "值2", ...],
|
||||||
|
"source": "数据来源说明",
|
||||||
|
"confidence": 0.0到1.0之间的置信度
|
||||||
|
}}
|
||||||
|
|
||||||
|
如果没有找到相关数据,返回空数组 values: []"""
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "你是一个专业的数据提取助手,擅长从政府统计公报等文档中提取数据。请严格按JSON格式输出。"},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
]
|
||||||
|
|
||||||
|
response = await self.llm.chat(
|
||||||
|
messages=messages,
|
||||||
|
temperature=0.1,
|
||||||
|
max_tokens=5000
|
||||||
|
)
|
||||||
|
|
||||||
|
content = self.llm.extract_message_content(response)
|
||||||
|
logger.info(f"AI 分析返回: {content[:500]}")
|
||||||
|
|
||||||
|
# 解析 JSON
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
# 清理 markdown 格式
|
||||||
|
cleaned = content.strip()
|
||||||
|
cleaned = re.sub(r'^```json\s*', '', cleaned, flags=re.MULTILINE)
|
||||||
|
cleaned = re.sub(r'^```\s*', '', cleaned, flags=re.MULTILINE)
|
||||||
|
cleaned = cleaned.strip()
|
||||||
|
|
||||||
|
# 查找 JSON
|
||||||
|
json_start = -1
|
||||||
|
for i, c in enumerate(cleaned):
|
||||||
|
if c == '{' or c == '[':
|
||||||
|
json_start = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if json_start == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
json_text = cleaned[json_start:]
|
||||||
|
try:
|
||||||
|
result = json.loads(json_text)
|
||||||
|
values = self._extract_values_from_json(result)
|
||||||
|
if values:
|
||||||
|
return FillResult(
|
||||||
|
field=field.name,
|
||||||
|
values=values,
|
||||||
|
value=values[0] if values else "",
|
||||||
|
source=f"AI分析: {doc.filename}",
|
||||||
|
confidence=result.get("confidence", 0.8)
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 尝试修复 JSON
|
||||||
|
fixed = self._fix_json(json_text)
|
||||||
|
if fixed:
|
||||||
|
try:
|
||||||
|
result = json.loads(fixed)
|
||||||
|
values = self._extract_values_from_json(result)
|
||||||
|
if values:
|
||||||
|
return FillResult(
|
||||||
|
field=field.name,
|
||||||
|
values=values,
|
||||||
|
value=values[0] if values else "",
|
||||||
|
source=f"AI分析: {doc.filename}",
|
||||||
|
confidence=result.get("confidence", 0.8)
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"AI 分析文档 {doc.filename} 失败: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ==================== 全局单例 ====================
|
# ==================== 全局单例 ====================
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user