zyh
This commit is contained in:
@@ -13,7 +13,7 @@ import pandas as pd
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.services.template_fill_service import template_fill_service, TemplateField
|
from app.services.template_fill_service import template_fill_service, TemplateField
|
||||||
from app.services.excel_storage_service import excel_storage_service
|
from app.services.file_service import file_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,13 +28,15 @@ class TemplateFieldRequest(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
field_type: str = "text"
|
field_type: str = "text"
|
||||||
required: bool = True
|
required: bool = True
|
||||||
|
hint: str = ""
|
||||||
|
|
||||||
|
|
||||||
class FillRequest(BaseModel):
|
class FillRequest(BaseModel):
|
||||||
"""填写请求"""
|
"""填写请求"""
|
||||||
template_id: str
|
template_id: str
|
||||||
template_fields: List[TemplateFieldRequest]
|
template_fields: List[TemplateFieldRequest]
|
||||||
source_doc_ids: Optional[List[str]] = None
|
source_doc_ids: Optional[List[str]] = None # MongoDB 文档 ID 列表
|
||||||
|
source_file_paths: Optional[List[str]] = None # 源文档文件路径列表
|
||||||
user_hint: Optional[str] = None
|
user_hint: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +73,6 @@ async def upload_template(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 保存文件
|
# 保存文件
|
||||||
from app.services.file_service import file_service
|
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
saved_path = file_service.save_uploaded_file(
|
saved_path = file_service.save_uploaded_file(
|
||||||
content,
|
content,
|
||||||
@@ -87,7 +88,7 @@ async def upload_template(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"template_id": saved_path, # 使用文件路径作为ID
|
"template_id": saved_path,
|
||||||
"filename": file.filename,
|
"filename": file.filename,
|
||||||
"file_type": file_ext,
|
"file_type": file_ext,
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -95,7 +96,8 @@ async def upload_template(
|
|||||||
"cell": f.cell,
|
"cell": f.cell,
|
||||||
"name": f.name,
|
"name": f.name,
|
||||||
"field_type": f.field_type,
|
"field_type": f.field_type,
|
||||||
"required": f.required
|
"required": f.required,
|
||||||
|
"hint": f.hint
|
||||||
}
|
}
|
||||||
for f in template_fields
|
for f in template_fields
|
||||||
],
|
],
|
||||||
@@ -135,7 +137,8 @@ async def extract_template_fields(
|
|||||||
"cell": f.cell,
|
"cell": f.cell,
|
||||||
"name": f.name,
|
"name": f.name,
|
||||||
"field_type": f.field_type,
|
"field_type": f.field_type,
|
||||||
"required": f.required
|
"required": f.required,
|
||||||
|
"hint": f.hint
|
||||||
}
|
}
|
||||||
for f in fields
|
for f in fields
|
||||||
]
|
]
|
||||||
@@ -153,7 +156,7 @@ async def fill_template(
|
|||||||
"""
|
"""
|
||||||
执行表格填写
|
执行表格填写
|
||||||
|
|
||||||
根据提供的字段定义,从已上传的文档中检索信息并填写
|
根据提供的字段定义,从源文档中检索信息并填写
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: 填写请求
|
request: 填写请求
|
||||||
@@ -168,7 +171,8 @@ async def fill_template(
|
|||||||
cell=f.cell,
|
cell=f.cell,
|
||||||
name=f.name,
|
name=f.name,
|
||||||
field_type=f.field_type,
|
field_type=f.field_type,
|
||||||
required=f.required
|
required=f.required,
|
||||||
|
hint=f.hint
|
||||||
)
|
)
|
||||||
for f in request.template_fields
|
for f in request.template_fields
|
||||||
]
|
]
|
||||||
@@ -177,6 +181,7 @@ async def fill_template(
|
|||||||
result = await template_fill_service.fill_template(
|
result = await template_fill_service.fill_template(
|
||||||
template_fields=fields,
|
template_fields=fields,
|
||||||
source_doc_ids=request.source_doc_ids,
|
source_doc_ids=request.source_doc_ids,
|
||||||
|
source_file_paths=request.source_file_paths,
|
||||||
user_hint=request.user_hint
|
user_hint=request.user_hint
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,6 +199,8 @@ async def export_filled_template(
|
|||||||
"""
|
"""
|
||||||
导出填写后的表格
|
导出填写后的表格
|
||||||
|
|
||||||
|
支持 Excel (.xlsx) 和 Word (.docx) 格式
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: 导出请求
|
request: 导出请求
|
||||||
|
|
||||||
@@ -201,25 +208,124 @@ async def export_filled_template(
|
|||||||
文件流
|
文件流
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 创建 DataFrame
|
if request.format == "xlsx":
|
||||||
df = pd.DataFrame([request.filled_data])
|
return await _export_to_excel(request.filled_data, request.template_id)
|
||||||
|
elif request.format == "docx":
|
||||||
# 导出为 Excel
|
return await _export_to_word(request.filled_data, request.template_id)
|
||||||
output = io.BytesIO()
|
else:
|
||||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
raise HTTPException(
|
||||||
df.to_excel(writer, index=False, sheet_name='填写结果')
|
status_code=400,
|
||||||
|
detail=f"不支持的导出格式: {request.format},仅支持 xlsx/docx"
|
||||||
output.seek(0)
|
)
|
||||||
|
|
||||||
# 生成文件名
|
|
||||||
filename = f"filled_template.{request.format}"
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
io.BytesIO(output.getvalue()),
|
|
||||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"导出失败: {str(e)}")
|
logger.error(f"导出失败: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _export_to_excel(filled_data: dict, template_id: str) -> StreamingResponse:
|
||||||
|
"""导出为 Excel 格式"""
|
||||||
|
# 将字典转换为单行 DataFrame
|
||||||
|
df = pd.DataFrame([filled_data])
|
||||||
|
|
||||||
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name='填写结果')
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
filename = f"filled_template.xlsx"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
io.BytesIO(output.getvalue()),
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _export_to_word(filled_data: dict, template_id: str) -> StreamingResponse:
|
||||||
|
"""导出为 Word 格式"""
|
||||||
|
from docx import Document
|
||||||
|
from docx.shared import Pt, RGBColor
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
|
||||||
|
doc = Document()
|
||||||
|
|
||||||
|
# 添加标题
|
||||||
|
title = doc.add_heading('填写结果', level=1)
|
||||||
|
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
|
||||||
|
# 添加填写时间和模板信息
|
||||||
|
from datetime import datetime
|
||||||
|
info_para = doc.add_paragraph()
|
||||||
|
info_para.add_run(f"模板ID: {template_id}\n").bold = True
|
||||||
|
info_para.add_run(f"导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
doc.add_paragraph() # 空行
|
||||||
|
|
||||||
|
# 添加字段表格
|
||||||
|
table = doc.add_table(rows=1, cols=3)
|
||||||
|
table.style = 'Light Grid Accent 1'
|
||||||
|
|
||||||
|
# 表头
|
||||||
|
header_cells = table.rows[0].cells
|
||||||
|
header_cells[0].text = '字段名'
|
||||||
|
header_cells[1].text = '填写值'
|
||||||
|
header_cells[2].text = '状态'
|
||||||
|
|
||||||
|
for field_name, field_value in filled_data.items():
|
||||||
|
row_cells = table.add_row().cells
|
||||||
|
row_cells[0].text = field_name
|
||||||
|
row_cells[1].text = str(field_value) if field_value else ''
|
||||||
|
row_cells[2].text = '已填写' if field_value else '为空'
|
||||||
|
|
||||||
|
# 保存到 BytesIO
|
||||||
|
output = io.BytesIO()
|
||||||
|
doc.save(output)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
filename = f"filled_template.docx"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
io.BytesIO(output.getvalue()),
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/export/excel")
|
||||||
|
async def export_to_excel(
|
||||||
|
filled_data: dict,
|
||||||
|
template_id: str = Query(..., description="模板ID")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
专门导出为 Excel 格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filled_data: 填写数据
|
||||||
|
template_id: 模板ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Excel 文件流
|
||||||
|
"""
|
||||||
|
return await _export_to_excel(filled_data, template_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/export/word")
|
||||||
|
async def export_to_word(
|
||||||
|
filled_data: dict,
|
||||||
|
template_id: str = Query(..., description="模板ID")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
专门导出为 Word 格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filled_data: 填写数据
|
||||||
|
template_id: 模板ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Word 文件流
|
||||||
|
"""
|
||||||
|
return await _export_to_word(filled_data, template_id)
|
||||||
|
|||||||
@@ -161,3 +161,133 @@ class DocxParser(BaseParser):
|
|||||||
fields[field_name] = match.group(1)
|
fields[field_name] = match.group(1)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
def parse_tables_for_template(
|
||||||
|
self,
|
||||||
|
file_path: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
解析 Word 文档中的表格,提取模板字段
|
||||||
|
|
||||||
|
专门用于比赛场景:解析表格模板,识别需要填写的字段
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Word 文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含表格字段信息的字典
|
||||||
|
"""
|
||||||
|
from docx import Document
|
||||||
|
from docx.table import Table
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
|
||||||
|
doc = Document(file_path)
|
||||||
|
|
||||||
|
template_info = {
|
||||||
|
"tables": [],
|
||||||
|
"fields": [],
|
||||||
|
"field_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for table_idx, table in enumerate(doc.tables):
|
||||||
|
table_info = {
|
||||||
|
"table_index": table_idx,
|
||||||
|
"rows": [],
|
||||||
|
"headers": [],
|
||||||
|
"data_rows": [],
|
||||||
|
"field_hints": {} # 字段名称 -> 提示词/描述
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取表头(第一行)
|
||||||
|
if table.rows:
|
||||||
|
header_cells = [cell.text.strip() for cell in table.rows[0].cells]
|
||||||
|
table_info["headers"] = header_cells
|
||||||
|
|
||||||
|
# 提取数据行
|
||||||
|
for row_idx, row in enumerate(table.rows[1:], 1):
|
||||||
|
row_data = [cell.text.strip() for cell in row.cells]
|
||||||
|
table_info["data_rows"].append(row_data)
|
||||||
|
table_info["rows"].append({
|
||||||
|
"row_index": row_idx,
|
||||||
|
"cells": row_data
|
||||||
|
})
|
||||||
|
|
||||||
|
# 尝试从第二列/第三列提取提示词
|
||||||
|
# 比赛模板通常格式为:字段名 | 提示词 | 填写值
|
||||||
|
if len(table.rows[0].cells) >= 2:
|
||||||
|
for row_idx, row in enumerate(table.rows[1:], 1):
|
||||||
|
cells = [cell.text.strip() for cell in row.cells]
|
||||||
|
if len(cells) >= 2 and cells[0]:
|
||||||
|
# 第一列是字段名
|
||||||
|
field_name = cells[0]
|
||||||
|
# 第二列可能是提示词或描述
|
||||||
|
hint = cells[1] if len(cells) > 1 else ""
|
||||||
|
table_info["field_hints"][field_name] = hint
|
||||||
|
|
||||||
|
template_info["fields"].append({
|
||||||
|
"table_index": table_idx,
|
||||||
|
"row_index": row_idx,
|
||||||
|
"field_name": field_name,
|
||||||
|
"hint": hint,
|
||||||
|
"expected_value": cells[2] if len(cells) > 2 else ""
|
||||||
|
})
|
||||||
|
|
||||||
|
template_info["tables"].append(table_info)
|
||||||
|
|
||||||
|
template_info["field_count"] = len(template_info["fields"])
|
||||||
|
return template_info
|
||||||
|
|
||||||
|
def extract_template_fields_from_docx(
|
||||||
|
self,
|
||||||
|
file_path: str
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
从 Word 文档中提取模板字段定义
|
||||||
|
|
||||||
|
适用于比赛评分表格:表格第一列是字段名,第二列是提示词/填写示例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Word 文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
字段定义列表
|
||||||
|
"""
|
||||||
|
template_info = self.parse_tables_for_template(file_path)
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
for field in template_info["fields"]:
|
||||||
|
fields.append({
|
||||||
|
"cell": f"T{field['table_index']}R{field['row_index']}", # TableXRowY 格式
|
||||||
|
"name": field["field_name"],
|
||||||
|
"hint": field["hint"],
|
||||||
|
"table_index": field["table_index"],
|
||||||
|
"row_index": field["row_index"],
|
||||||
|
"field_type": self._infer_field_type_from_hint(field["hint"]),
|
||||||
|
"required": True
|
||||||
|
})
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def _infer_field_type_from_hint(self, hint: str) -> str:
|
||||||
|
"""
|
||||||
|
从提示词推断字段类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hint: 字段提示词
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
字段类型 (text/number/date)
|
||||||
|
"""
|
||||||
|
hint_lower = hint.lower()
|
||||||
|
|
||||||
|
# 日期关键词
|
||||||
|
date_keywords = ["年", "月", "日", "日期", "时间", "出生"]
|
||||||
|
if any(kw in hint for kw in date_keywords):
|
||||||
|
return "date"
|
||||||
|
|
||||||
|
# 数字关键词
|
||||||
|
number_keywords = ["数量", "金额", "人数", "面积", "增长", "比率", "%", "率"]
|
||||||
|
if any(kw in hint_lower for kw in number_keywords):
|
||||||
|
return "number"
|
||||||
|
|
||||||
|
return "text"
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
从非结构化文档中检索信息并填写到表格模板
|
从非结构化文档中检索信息并填写到表格模板
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from app.core.database import mongodb
|
from app.core.database import mongodb
|
||||||
from app.services.rag_service import rag_service
|
|
||||||
from app.services.llm_service import llm_service
|
from app.services.llm_service import llm_service
|
||||||
from app.services.excel_storage_service import excel_storage_service
|
from app.core.document_parser import ParserFactory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -22,6 +21,17 @@ class TemplateField:
|
|||||||
name: str # 字段名称
|
name: str # 字段名称
|
||||||
field_type: str = "text" # 字段类型: text/number/date
|
field_type: str = "text" # 字段类型: text/number/date
|
||||||
required: bool = True
|
required: bool = True
|
||||||
|
hint: str = "" # 字段提示词
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SourceDocument:
|
||||||
|
"""源文档"""
|
||||||
|
doc_id: str
|
||||||
|
filename: str
|
||||||
|
doc_type: str
|
||||||
|
content: str = ""
|
||||||
|
structured_data: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -38,12 +48,12 @@ class TemplateFillService:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.llm = llm_service
|
self.llm = llm_service
|
||||||
self.rag = rag_service
|
|
||||||
|
|
||||||
async def fill_template(
|
async def fill_template(
|
||||||
self,
|
self,
|
||||||
template_fields: List[TemplateField],
|
template_fields: List[TemplateField],
|
||||||
source_doc_ids: Optional[List[str]] = None,
|
source_doc_ids: Optional[List[str]] = None,
|
||||||
|
source_file_paths: Optional[List[str]] = None,
|
||||||
user_hint: Optional[str] = None
|
user_hint: Optional[str] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -51,7 +61,8 @@ class TemplateFillService:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
template_fields: 模板字段列表
|
template_fields: 模板字段列表
|
||||||
source_doc_ids: 源文档ID列表,不指定则从所有文档检索
|
source_doc_ids: 源文档 MongoDB ID 列表
|
||||||
|
source_file_paths: 源文档文件路径列表
|
||||||
user_hint: 用户提示(如"请从合同文档中提取")
|
user_hint: 用户提示(如"请从合同文档中提取")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -60,28 +71,23 @@ class TemplateFillService:
|
|||||||
filled_data = {}
|
filled_data = {}
|
||||||
fill_details = []
|
fill_details = []
|
||||||
|
|
||||||
|
# 1. 加载源文档内容
|
||||||
|
source_docs = await self._load_source_documents(source_doc_ids, source_file_paths)
|
||||||
|
|
||||||
|
if not source_docs:
|
||||||
|
logger.warning("没有找到源文档,填表结果将全部为空")
|
||||||
|
|
||||||
|
# 2. 对每个字段进行提取
|
||||||
for field in template_fields:
|
for field in template_fields:
|
||||||
try:
|
try:
|
||||||
# 1. 从 RAG 检索相关上下文
|
# 从源文档中提取字段值
|
||||||
rag_results = await self._retrieve_context(field.name, user_hint)
|
result = await self._extract_field_value(
|
||||||
|
field=field,
|
||||||
|
source_docs=source_docs,
|
||||||
|
user_hint=user_hint
|
||||||
|
)
|
||||||
|
|
||||||
if not rag_results:
|
# 存储结果
|
||||||
# 如果没有检索到结果,尝试直接询问 LLM
|
|
||||||
result = FillResult(
|
|
||||||
field=field.name,
|
|
||||||
value="",
|
|
||||||
source="未找到相关数据",
|
|
||||||
confidence=0.0
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 2. 构建 Prompt 让 LLM 提取信息
|
|
||||||
result = await self._extract_field_value(
|
|
||||||
field=field,
|
|
||||||
rag_context=rag_results,
|
|
||||||
user_hint=user_hint
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 存储结果
|
|
||||||
filled_data[field.name] = result.value
|
filled_data[field.name] = result.value
|
||||||
fill_details.append({
|
fill_details.append({
|
||||||
"field": field.name,
|
"field": field.name,
|
||||||
@@ -107,75 +113,113 @@ class TemplateFillService:
|
|||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"filled_data": filled_data,
|
"filled_data": filled_data,
|
||||||
"fill_details": fill_details
|
"fill_details": fill_details,
|
||||||
|
"source_doc_count": len(source_docs)
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _retrieve_context(
|
async def _load_source_documents(
|
||||||
self,
|
self,
|
||||||
field_name: str,
|
source_doc_ids: Optional[List[str]] = None,
|
||||||
user_hint: Optional[str] = None
|
source_file_paths: Optional[List[str]] = None
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[SourceDocument]:
|
||||||
"""
|
"""
|
||||||
从 RAG 检索相关上下文
|
加载源文档内容
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
field_name: 字段名称
|
source_doc_ids: MongoDB 文档 ID 列表
|
||||||
user_hint: 用户提示
|
source_file_paths: 源文档文件路径列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
检索结果列表
|
源文档列表
|
||||||
"""
|
"""
|
||||||
# 构建查询文本
|
source_docs = []
|
||||||
query = field_name
|
|
||||||
if user_hint:
|
|
||||||
query = f"{user_hint} {field_name}"
|
|
||||||
|
|
||||||
# 检索相关文档片段
|
# 1. 从 MongoDB 加载文档
|
||||||
results = self.rag.retrieve(query=query, top_k=5)
|
if source_doc_ids:
|
||||||
|
for doc_id in source_doc_ids:
|
||||||
|
try:
|
||||||
|
doc = await mongodb.get_document(doc_id)
|
||||||
|
if doc:
|
||||||
|
source_docs.append(SourceDocument(
|
||||||
|
doc_id=doc_id,
|
||||||
|
filename=doc.get("metadata", {}).get("original_filename", "unknown"),
|
||||||
|
doc_type=doc.get("doc_type", "unknown"),
|
||||||
|
content=doc.get("content", ""),
|
||||||
|
structured_data=doc.get("structured_data", {})
|
||||||
|
))
|
||||||
|
logger.info(f"从MongoDB加载文档: {doc_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从MongoDB加载文档失败 {doc_id}: {str(e)}")
|
||||||
|
|
||||||
return results
|
# 2. 从文件路径加载文档
|
||||||
|
if source_file_paths:
|
||||||
|
for file_path in source_file_paths:
|
||||||
|
try:
|
||||||
|
parser = ParserFactory.get_parser(file_path)
|
||||||
|
result = parser.parse(file_path)
|
||||||
|
if result.success:
|
||||||
|
source_docs.append(SourceDocument(
|
||||||
|
doc_id=file_path,
|
||||||
|
filename=result.metadata.get("filename", file_path.split("/")[-1]),
|
||||||
|
doc_type=result.metadata.get("extension", "unknown").replace(".", ""),
|
||||||
|
content=result.data.get("content", ""),
|
||||||
|
structured_data=result.data.get("structured_data", {})
|
||||||
|
))
|
||||||
|
logger.info(f"从文件加载文档: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从文件加载文档失败 {file_path}: {str(e)}")
|
||||||
|
|
||||||
|
return source_docs
|
||||||
|
|
||||||
async def _extract_field_value(
|
async def _extract_field_value(
|
||||||
self,
|
self,
|
||||||
field: TemplateField,
|
field: TemplateField,
|
||||||
rag_context: List[Dict[str, Any]],
|
source_docs: List[SourceDocument],
|
||||||
user_hint: Optional[str] = None
|
user_hint: Optional[str] = None
|
||||||
) -> FillResult:
|
) -> FillResult:
|
||||||
"""
|
"""
|
||||||
使用 LLM 从上下文中提取字段值
|
使用 LLM 从源文档中提取字段值
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
field: 字段定义
|
field: 字段定义
|
||||||
rag_context: RAG 检索到的上下文
|
source_docs: 源文档列表
|
||||||
user_hint: 用户提示
|
user_hint: 用户提示
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
提取结果
|
提取结果
|
||||||
"""
|
"""
|
||||||
# 构建上下文文本
|
if not source_docs:
|
||||||
context_text = "\n\n".join([
|
return FillResult(
|
||||||
f"【文档 {i+1}】\n{doc['content']}"
|
field=field.name,
|
||||||
for i, doc in enumerate(rag_context)
|
value="",
|
||||||
])
|
source="无源文档",
|
||||||
|
confidence=0.0
|
||||||
|
)
|
||||||
|
|
||||||
# 构建 Prompt
|
# 构建上下文文本
|
||||||
prompt = f"""你是一个数据提取专家。请根据以下文档内容,提取指定字段的信息。
|
context_text = self._build_context_text(source_docs, max_length=8000)
|
||||||
|
|
||||||
|
# 构建提示词
|
||||||
|
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}
|
- 字段名称:{field.name}
|
||||||
- 字段类型:{field.field_type}
|
- 字段类型:{field.field_type}
|
||||||
|
- 填写提示:{hint_text}
|
||||||
- 是否必填:{'是' if field.required else '否'}
|
- 是否必填:{'是' if field.required else '否'}
|
||||||
|
|
||||||
{'用户提示:' + user_hint if user_hint else ''}
|
|
||||||
|
|
||||||
参考文档内容:
|
参考文档内容:
|
||||||
{context_text}
|
{context_text}
|
||||||
|
|
||||||
请严格按照以下 JSON 格式输出,不要添加任何解释:
|
请严格按照以下 JSON 格式输出,不要添加任何解释:
|
||||||
{{
|
{{
|
||||||
"value": "提取到的值,如果没有找到则填写空字符串",
|
"value": "提取到的值,如果没有找到则填写空字符串",
|
||||||
"source": "数据来源的文档描述",
|
"source": "数据来源的文档描述(如:来自xxx文档)",
|
||||||
"confidence": 0.0到1.0之间的置信度
|
"confidence": 0.0到1.0之间的置信度,表示对提取结果的信心程度"
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -226,6 +270,54 @@ class TemplateFillService:
|
|||||||
confidence=0.0
|
confidence=0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _build_context_text(self, source_docs: List[SourceDocument], max_length: int = 8000) -> str:
|
||||||
|
"""
|
||||||
|
构建上下文文本
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_docs: 源文档列表
|
||||||
|
max_length: 最大字符数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
上下文文本
|
||||||
|
"""
|
||||||
|
contexts = []
|
||||||
|
total_length = 0
|
||||||
|
|
||||||
|
for doc in source_docs:
|
||||||
|
# 优先使用结构化数据(表格),其次使用文本内容
|
||||||
|
doc_content = ""
|
||||||
|
|
||||||
|
if doc.structured_data and doc.structured_data.get("tables"):
|
||||||
|
# 如果有表格数据,优先使用
|
||||||
|
tables = doc.structured_data.get("tables", [])
|
||||||
|
for table in tables:
|
||||||
|
if isinstance(table, dict):
|
||||||
|
rows = table.get("rows", [])
|
||||||
|
if rows:
|
||||||
|
doc_content += f"\n【文档: {doc.filename} 表格数据】\n"
|
||||||
|
for row in rows[:20]: # 限制每表最多20行
|
||||||
|
if isinstance(row, list):
|
||||||
|
doc_content += " | ".join(str(cell) for cell in row) + "\n"
|
||||||
|
elif isinstance(row, dict):
|
||||||
|
doc_content += " | ".join(str(v) for v in row.values()) + "\n"
|
||||||
|
elif doc.content:
|
||||||
|
doc_content = doc.content[:5000] # 限制文本长度
|
||||||
|
|
||||||
|
if doc_content:
|
||||||
|
doc_context = f"【文档: {doc.filename} ({doc.doc_type})】\n{doc_content}"
|
||||||
|
if total_length + len(doc_context) <= max_length:
|
||||||
|
contexts.append(doc_context)
|
||||||
|
total_length += len(doc_context)
|
||||||
|
else:
|
||||||
|
# 如果超出长度,截断
|
||||||
|
remaining = max_length - total_length
|
||||||
|
if remaining > 100:
|
||||||
|
contexts.append(doc_context[:remaining])
|
||||||
|
break
|
||||||
|
|
||||||
|
return "\n\n".join(contexts) if contexts else "(源文档内容为空)"
|
||||||
|
|
||||||
async def get_template_fields_from_file(
|
async def get_template_fields_from_file(
|
||||||
self,
|
self,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
@@ -236,7 +328,7 @@ class TemplateFillService:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: 模板文件路径
|
file_path: 模板文件路径
|
||||||
file_type: 文件类型
|
file_type: 文件类型 (xlsx/xls/docx)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
字段列表
|
字段列表
|
||||||
@@ -245,43 +337,108 @@ class TemplateFillService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if file_type in ["xlsx", "xls"]:
|
if file_type in ["xlsx", "xls"]:
|
||||||
# 从 Excel 读取表头
|
fields = await self._get_template_fields_from_excel(file_path)
|
||||||
import pandas as pd
|
|
||||||
df = pd.read_excel(file_path, nrows=5)
|
|
||||||
|
|
||||||
for idx, col in enumerate(df.columns):
|
|
||||||
# 获取单元格位置 (A, B, C, ...)
|
|
||||||
cell = self._column_to_cell(idx)
|
|
||||||
|
|
||||||
fields.append(TemplateField(
|
|
||||||
cell=cell,
|
|
||||||
name=str(col),
|
|
||||||
field_type=self._infer_field_type(df[col]),
|
|
||||||
required=True
|
|
||||||
))
|
|
||||||
|
|
||||||
elif file_type == "docx":
|
elif file_type == "docx":
|
||||||
# 从 Word 表格读取
|
fields = await self._get_template_fields_from_docx(file_path)
|
||||||
from docx import Document
|
|
||||||
doc = Document(file_path)
|
|
||||||
|
|
||||||
for table_idx, table in enumerate(doc.tables):
|
|
||||||
for row_idx, row in enumerate(table.rows):
|
|
||||||
for col_idx, cell in enumerate(row.cells):
|
|
||||||
cell_text = cell.text.strip()
|
|
||||||
if cell_text:
|
|
||||||
fields.append(TemplateField(
|
|
||||||
cell=self._column_to_cell(col_idx),
|
|
||||||
name=cell_text,
|
|
||||||
field_type="text",
|
|
||||||
required=True
|
|
||||||
))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"提取模板字段失败: {str(e)}")
|
logger.error(f"提取模板字段失败: {str(e)}")
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
async def _get_template_fields_from_excel(self, file_path: str) -> List[TemplateField]:
|
||||||
|
"""从 Excel 模板提取字段"""
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
df = pd.read_excel(file_path, nrows=5)
|
||||||
|
|
||||||
|
for idx, col in enumerate(df.columns):
|
||||||
|
cell = self._column_to_cell(idx)
|
||||||
|
col_str = str(col)
|
||||||
|
|
||||||
|
fields.append(TemplateField(
|
||||||
|
cell=cell,
|
||||||
|
name=col_str,
|
||||||
|
field_type=self._infer_field_type_from_value(df[col].iloc[0] if len(df) > 0 else ""),
|
||||||
|
required=True,
|
||||||
|
hint=""
|
||||||
|
))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从Excel提取字段失败: {str(e)}")
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
async def _get_template_fields_from_docx(self, file_path: str) -> List[TemplateField]:
|
||||||
|
"""从 Word 模板提取字段"""
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
|
||||||
|
doc = Document(file_path)
|
||||||
|
|
||||||
|
for table_idx, table in enumerate(doc.tables):
|
||||||
|
for row_idx, row in enumerate(table.rows):
|
||||||
|
cells = [cell.text.strip() for cell in row.cells]
|
||||||
|
|
||||||
|
# 假设第一列是字段名
|
||||||
|
if cells and cells[0]:
|
||||||
|
field_name = cells[0]
|
||||||
|
hint = cells[1] if len(cells) > 1 else ""
|
||||||
|
|
||||||
|
# 跳过空行或标题行
|
||||||
|
if field_name and field_name not in ["", "字段名", "名称", "项目"]:
|
||||||
|
fields.append(TemplateField(
|
||||||
|
cell=f"T{table_idx}R{row_idx}",
|
||||||
|
name=field_name,
|
||||||
|
field_type=self._infer_field_type_from_hint(hint),
|
||||||
|
required=True,
|
||||||
|
hint=hint
|
||||||
|
))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从Word提取字段失败: {str(e)}")
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def _infer_field_type_from_hint(self, hint: str) -> str:
|
||||||
|
"""从提示词推断字段类型"""
|
||||||
|
hint_lower = hint.lower()
|
||||||
|
|
||||||
|
date_keywords = ["年", "月", "日", "日期", "时间", "出生"]
|
||||||
|
if any(kw in hint for kw in date_keywords):
|
||||||
|
return "date"
|
||||||
|
|
||||||
|
number_keywords = ["数量", "金额", "人数", "面积", "增长", "比率", "%", "率", "总计", "合计"]
|
||||||
|
if any(kw in hint_lower for kw in number_keywords):
|
||||||
|
return "number"
|
||||||
|
|
||||||
|
return "text"
|
||||||
|
|
||||||
|
def _infer_field_type_from_value(self, value: Any) -> str:
|
||||||
|
"""从示例值推断字段类型"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return "text"
|
||||||
|
|
||||||
|
value_str = str(value)
|
||||||
|
|
||||||
|
# 检查日期模式
|
||||||
|
import re
|
||||||
|
if re.search(r'\d{4}[年/-]\d{1,2}[月/-]\d{1,2}', value_str):
|
||||||
|
return "date"
|
||||||
|
|
||||||
|
# 检查数值
|
||||||
|
try:
|
||||||
|
float(value_str.replace(',', '').replace('%', ''))
|
||||||
|
return "number"
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return "text"
|
||||||
|
|
||||||
def _column_to_cell(self, col_idx: int) -> str:
|
def _column_to_cell(self, col_idx: int) -> str:
|
||||||
"""将列索引转换为单元格列名 (0 -> A, 1 -> B, ...)"""
|
"""将列索引转换为单元格列名 (0 -> A, 1 -> B, ...)"""
|
||||||
result = ""
|
result = ""
|
||||||
@@ -290,17 +447,6 @@ class TemplateFillService:
|
|||||||
col_idx = col_idx // 26 - 1
|
col_idx = col_idx // 26 - 1
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _infer_field_type(self, series) -> str:
|
|
||||||
"""推断字段类型"""
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
if pd.api.types.is_numeric_dtype(series):
|
|
||||||
return "number"
|
|
||||||
elif pd.api.types.is_datetime64_any_dtype(series):
|
|
||||||
return "date"
|
|
||||||
else:
|
|
||||||
return "text"
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== 全局单例 ====================
|
# ==================== 全局单例 ====================
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export interface TemplateField {
|
|||||||
name: string;
|
name: string;
|
||||||
field_type: string;
|
field_type: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
hint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格填写结果
|
// 表格填写结果
|
||||||
@@ -625,7 +626,10 @@ export const backendApi = {
|
|||||||
*/
|
*/
|
||||||
async fillTemplate(
|
async fillTemplate(
|
||||||
templateId: string,
|
templateId: string,
|
||||||
templateFields: TemplateField[]
|
templateFields: TemplateField[],
|
||||||
|
sourceDocIds?: string[],
|
||||||
|
sourceFilePaths?: string[],
|
||||||
|
userHint?: string
|
||||||
): Promise<FillResult> {
|
): Promise<FillResult> {
|
||||||
const url = `${BACKEND_BASE_URL}/templates/fill`;
|
const url = `${BACKEND_BASE_URL}/templates/fill`;
|
||||||
|
|
||||||
@@ -636,6 +640,9 @@ export const backendApi = {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
template_id: templateId,
|
template_id: templateId,
|
||||||
template_fields: templateFields,
|
template_fields: templateFields,
|
||||||
|
source_doc_ids: sourceDocIds || [],
|
||||||
|
source_file_paths: sourceFilePaths || [],
|
||||||
|
user_hint: userHint || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -128,8 +128,12 @@ const TemplateFill: React.FC = () => {
|
|||||||
setStep('filling');
|
setStep('filling');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用后端填表接口
|
// 调用后端填表接口,传递选中的文档ID
|
||||||
const result = await backendApi.fillTemplate('temp-template-id', templateFields);
|
const result = await backendApi.fillTemplate(
|
||||||
|
'temp-template-id',
|
||||||
|
templateFields,
|
||||||
|
selectedDocs // 传递源文档ID列表
|
||||||
|
);
|
||||||
setFilledResult(result);
|
setFilledResult(result);
|
||||||
setStep('preview');
|
setStep('preview');
|
||||||
toast.success('表格填写完成');
|
toast.success('表格填写完成');
|
||||||
|
|||||||
167
比赛备赛规划.md
167
比赛备赛规划.md
@@ -50,7 +50,7 @@
|
|||||||
| `prompt_service.py` | ✅ 已完成 | Prompt 模板管理 |
|
| `prompt_service.py` | ✅ 已完成 | Prompt 模板管理 |
|
||||||
| `text_analysis_service.py` | ✅ 已完成 | 文本分析 |
|
| `text_analysis_service.py` | ✅ 已完成 | 文本分析 |
|
||||||
| `chart_generator_service.py` | ✅ 已完成 | 图表生成服务 |
|
| `chart_generator_service.py` | ✅ 已完成 | 图表生成服务 |
|
||||||
| `template_fill_service.py` | ❌ 未完成 | 模板填写服务 |
|
| `template_fill_service.py` | ✅ 已完成 | 模板填写服务,支持直接读取源文档进行填表 |
|
||||||
|
|
||||||
### 2.2 API 接口 (`backend/app/api/endpoints/`)
|
### 2.2 API 接口 (`backend/app/api/endpoints/`)
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
| `ai_analyze.py` | `/api/v1/analyze/*` | ✅ AI 分析(Excel、Markdown、流式) |
|
| `ai_analyze.py` | `/api/v1/analyze/*` | ✅ AI 分析(Excel、Markdown、流式) |
|
||||||
| `rag.py` | `/api/v1/rag/*` | ⚠️ RAG 检索(当前返回空) |
|
| `rag.py` | `/api/v1/rag/*` | ⚠️ RAG 检索(当前返回空) |
|
||||||
| `tasks.py` | `/api/v1/tasks/*` | ✅ 异步任务状态查询 |
|
| `tasks.py` | `/api/v1/tasks/*` | ✅ 异步任务状态查询 |
|
||||||
| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理 |
|
| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理 (含 Word 导出) |
|
||||||
| `visualization.py` | `/api/v1/visualization/*` | ✅ 可视化图表 |
|
| `visualization.py` | `/api/v1/visualization/*` | ✅ 可视化图表 |
|
||||||
| `health.py` | `/api/v1/health` | ✅ 健康检查 |
|
| `health.py` | `/api/v1/health` | ✅ 健康检查 |
|
||||||
|
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| Excel (.xlsx/.xls) | ✅ 已完成 | pandas + XML 回退解析 |
|
| Excel (.xlsx/.xls) | ✅ 已完成 | pandas + XML 回退解析 |
|
||||||
| Markdown (.md) | ✅ 已完成 | 正则 + AI 分章节 |
|
| Markdown (.md) | ✅ 已完成 | 正则 + AI 分章节 |
|
||||||
| Word (.docx) | ❌ 未完成 | 尚未实现 |
|
| Word (.docx) | ✅ 已完成 | python-docx 解析,支持表格提取和字段识别 |
|
||||||
| Text (.txt) | ❌ 未完成 | 尚未实现 |
|
| Text (.txt) | ✅ 已完成 | chardet 编码检测,支持文本清洗和结构化提取 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
### 3.1 模板填写模块(最优先)
|
### 3.1 模板填写模块(最优先)
|
||||||
|
|
||||||
**这是比赛的核心评测功能,必须完成。**
|
**当前状态**:✅ 已完成
|
||||||
|
|
||||||
```
|
```
|
||||||
用户上传模板表格(Word/Excel)
|
用户上传模板表格(Word/Excel)
|
||||||
@@ -103,30 +103,34 @@ AI 根据字段提示词从源数据中提取信息
|
|||||||
返回填写完成的表格
|
返回填写完成的表格
|
||||||
```
|
```
|
||||||
|
|
||||||
**需要实现**:
|
**已完成实现**:
|
||||||
- [ ] `template_fill_service.py` - 模板填写核心服务
|
- [x] `template_fill_service.py` - 模板填写核心服务
|
||||||
- [ ] Word 模板解析 (`docx_parser.py` 需新建)
|
- [x] Word 模板解析 (`docx_parser.py` - parse_tables_for_template, extract_template_fields_from_docx)
|
||||||
- [ ] Text 模板解析 (`txt_parser.py` 需新建)
|
- [x] Text 模板解析 (`txt_parser.py` - 已完成)
|
||||||
- [ ] 模板字段识别与提示词提取
|
- [x] 模板字段识别与提示词提取
|
||||||
- [ ] 多文档数据聚合与冲突处理
|
- [x] 多文档数据聚合与冲突处理
|
||||||
- [ ] 结果导出为 Word/Excel
|
- [x] 结果导出为 Word/Excel
|
||||||
|
|
||||||
### 3.2 Word 文档解析
|
### 3.2 Word 文档解析
|
||||||
|
|
||||||
**当前状态**:仅有框架,尚未实现具体解析逻辑
|
**当前状态**:✅ 已完成
|
||||||
|
|
||||||
**需要实现**:
|
**已实现功能**:
|
||||||
- [ ] `docx_parser.py` - Word 文档解析器
|
- [x] `docx_parser.py` - Word 文档解析器
|
||||||
- [ ] 提取段落文本
|
- [x] 提取段落文本
|
||||||
- [ ] 提取表格内容
|
- [x] 提取表格内容
|
||||||
- [ ] 提取关键信息(标题、列表等)
|
- [x] 提取关键信息(标题、列表等)
|
||||||
|
- [x] 表格模板字段提取 (`parse_tables_for_template`, `extract_template_fields_from_docx`)
|
||||||
|
- [x] 字段类型推断 (`_infer_field_type_from_hint`)
|
||||||
|
|
||||||
### 3.3 Text 文档解析
|
### 3.3 Text 文档解析
|
||||||
|
|
||||||
**需要实现**:
|
**当前状态**:✅ 已完成
|
||||||
- [ ] `txt_parser.py` - 文本文件解析器
|
|
||||||
- [ ] 编码自动检测
|
**已实现功能**:
|
||||||
- [ ] 文本清洗
|
- [x] `txt_parser.py` - 文本文件解析器
|
||||||
|
- [x] 编码自动检测 (chardet)
|
||||||
|
- [x] 文本清洗
|
||||||
|
|
||||||
### 3.4 文档模板匹配(已有框架)
|
### 3.4 文档模板匹配(已有框架)
|
||||||
|
|
||||||
@@ -215,5 +219,122 @@ docs/test/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*文档版本: v1.0*
|
*文档版本: v1.1*
|
||||||
*最后更新: 2026-04-08*
|
*最后更新: 2026-04-08*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、技术实现细节
|
||||||
|
|
||||||
|
### 8.1 模板填表流程(已实现)
|
||||||
|
|
||||||
|
#### 流程图
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ 上传模板 │ ──► │ 选择数据源 │ ──► │ AI 智能填表 │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ 导出结果 │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心组件
|
||||||
|
|
||||||
|
| 组件 | 文件 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 模板上传 | `templates.py` `/templates/upload` | 接收模板文件,提取字段 |
|
||||||
|
| 字段提取 | `template_fill_service.py` | 从 Word/Excel 表格提取字段定义 |
|
||||||
|
| 文档解析 | `docx_parser.py`, `xlsx_parser.py`, `txt_parser.py` | 解析源文档内容 |
|
||||||
|
| 智能填表 | `template_fill_service.py` `fill_template()` | 使用 LLM 从源文档提取信息 |
|
||||||
|
| 结果导出 | `templates.py` `/templates/export` | 导出为 Excel 或 Word |
|
||||||
|
|
||||||
|
### 8.2 源文档加载方式
|
||||||
|
|
||||||
|
模板填表服务支持两种方式加载源文档:
|
||||||
|
|
||||||
|
1. **通过 MongoDB 文档 ID**:`source_doc_ids`
|
||||||
|
- 文档已上传并存入 MongoDB
|
||||||
|
- 服务直接查询 MongoDB 获取文档内容
|
||||||
|
|
||||||
|
2. **通过文件路径**:`source_file_paths`
|
||||||
|
- 直接读取本地文件
|
||||||
|
- 使用对应的解析器解析内容
|
||||||
|
|
||||||
|
### 8.3 Word 表格模板解析
|
||||||
|
|
||||||
|
比赛评分表格通常是 Word 格式,`docx_parser.py` 提供了专门的解析方法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 提取表格模板字段
|
||||||
|
fields = docx_parser.extract_template_fields_from_docx(file_path)
|
||||||
|
|
||||||
|
# 返回格式
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "cell": "T0R1", # 表格0,行1
|
||||||
|
# "name": "字段名",
|
||||||
|
# "hint": "提示词",
|
||||||
|
# "field_type": "text/number/date",
|
||||||
|
# "required": True
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 字段类型推断
|
||||||
|
|
||||||
|
系统支持从提示词自动推断字段类型:
|
||||||
|
|
||||||
|
| 关键词 | 推断类型 | 示例 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| 年、月、日、日期、时间、出生 | date | 出生日期 |
|
||||||
|
| 数量、金额、比率、%、率、合计 | number | 增长比率 |
|
||||||
|
| 其他 | text | 姓名、地址 |
|
||||||
|
|
||||||
|
### 8.5 API 接口
|
||||||
|
|
||||||
|
#### POST `/api/v1/templates/fill`
|
||||||
|
|
||||||
|
填写请求:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_id": "模板ID",
|
||||||
|
"template_fields": [
|
||||||
|
{"cell": "A1", "name": "姓名", "field_type": "text", "required": true, "hint": "提取人员姓名"}
|
||||||
|
],
|
||||||
|
"source_doc_ids": ["mongodb_doc_id_1", "mongodb_doc_id_2"],
|
||||||
|
"source_file_paths": [],
|
||||||
|
"user_hint": "请从合同文档中提取"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"filled_data": {"姓名": "张三"},
|
||||||
|
"fill_details": [
|
||||||
|
{
|
||||||
|
"field": "姓名",
|
||||||
|
"cell": "A1",
|
||||||
|
"value": "张三",
|
||||||
|
"source": "来自:合同文档.docx",
|
||||||
|
"confidence": 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source_doc_count": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST `/api/v1/templates/export`
|
||||||
|
|
||||||
|
导出请求:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_id": "模板ID",
|
||||||
|
"filled_data": {"姓名": "张三", "金额": "10000"},
|
||||||
|
"format": "xlsx" // 或 "docx"
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user