Merge branch 'main' of https://gitea.kronecker.cc/OurCodesAreAllRight/FilesReadSystem
This commit is contained in:
24
.gitignore
vendored
24
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/.git/
|
||||
/.gitignore
|
||||
/.idea/
|
||||
/.vscode/
|
||||
/backend/venv/
|
||||
@@ -18,11 +19,7 @@
|
||||
/frontend/.idea/
|
||||
/frontend/.env
|
||||
/frontend/*.log
|
||||
/技术路线.md
|
||||
/开发路径.md
|
||||
/开发日志_2026-03-16.md
|
||||
/frontendTest/
|
||||
/docs/
|
||||
|
||||
/frontend/src/api/
|
||||
/frontend/src/api/index.js
|
||||
/frontend/src/api/index.ts
|
||||
@@ -30,9 +27,22 @@
|
||||
/frontend/src/api/index.py
|
||||
/frontend/src/api/index.go
|
||||
/frontend/src/api/index.java
|
||||
|
||||
/frontend - 副本/
|
||||
|
||||
/docs/
|
||||
/frontend - 副本/*
|
||||
/frontendTest/
|
||||
/supabase.txt
|
||||
|
||||
**/__pycache__/*
|
||||
# 取消跟踪的文件 / Untracked files
|
||||
比赛备赛规划.md
|
||||
Q&A.xlsx
|
||||
package.json
|
||||
技术路线.md
|
||||
开发路径.md
|
||||
开发日志_2026-03-16.md
|
||||
/logs/
|
||||
|
||||
# Python cache
|
||||
**/__pycache__/**
|
||||
**.pyc
|
||||
|
||||
238
README.md
Normal file
238
README.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# FilesReadSystem
|
||||
|
||||
## 项目介绍 / Project Introduction
|
||||
|
||||
基于大语言模型的文档理解与多源数据融合系统,专为第十七届中国大学生服务外包创新创业大赛(A23赛题)开发。本系统利用大语言模型(LLM)解析、分析各类文档格式并提取结构化数据,支持通过自然语言指令自动填写模板表格。
|
||||
|
||||
A document understanding and multi-source data fusion system based on Large Language Models (LLM), developed for the 17th China University Student Service Outsourcing Innovation and Entrepreneurship Competition (Topic A23). This system uses LLMs to parse, analyze, and extract structured data from various document formats, supporting automatic template table filling through natural language instructions.
|
||||
|
||||
---
|
||||
|
||||
## 技术栈 / Technology Stack
|
||||
|
||||
| 层次 / Layer | 组件 / Component | 说明 / Description |
|
||||
|:---|:---|:---|
|
||||
| 后端 / Backend | FastAPI + Uvicorn | RESTful API,异步任务调度 / API & async task scheduling |
|
||||
| 前端 / Frontend | React + TypeScript + Vite | 文件上传、表格配置、聊天界面 / Upload, table config, chat UI |
|
||||
| 异步任务 / Async Tasks | Celery + Redis | 处理耗时的解析与AI提取 / Heavy parsing & AI extraction |
|
||||
| 文档数据库 / Document DB | MongoDB (Motor) | 元数据、提取结果、文档块存储 / Metadata, results, chunk storage |
|
||||
| 关系数据库 / Relational DB | MySQL (SQLAlchemy) | 结构化数据存储 / Structured data storage |
|
||||
| 缓存 / Cache | Redis | 缓存与任务队列 / Caching & task queue |
|
||||
| 向量检索 / Vector Search | FAISS | 高效相似性搜索 / Efficient similarity search |
|
||||
| AI集成 / AI Integration | LangChain-style + MiniMax API | RAG流水线、提示词管理 / RAG pipeline, prompt management |
|
||||
| 文档解析 / Document Parsing | python-docx, pandas, openpyxl, markdown-it | 多格式支持 / Multi-format support |
|
||||
|
||||
---
|
||||
|
||||
## 项目架构 / Project Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ User Interface │
|
||||
│ (React + TypeScript + shadcn/ui) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FastAPI Backend │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ Upload API │ │ RAG Search │ │ Natural Language │ │
|
||||
│ │ /documents │ │ /rag/search │ │ /instruction/execute │ │
|
||||
│ └─────────────┘ └──────────────┘ └─────────────────────────┘ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ AI Analyze │ │ Template Fill│ │ Visualization │ │
|
||||
│ │ /ai/analyze │ │ /templates │ │ /visualization │ │
|
||||
│ └─────────────┘ └──────────────┘ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ MongoDB │ │ MySQL │ │ Redis │
|
||||
│ (Documents) │ │ (Structured) │ │ (Cache/Queue) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ FAISS │
|
||||
│ (Vector Index) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录结构 / Directory Structure
|
||||
|
||||
```
|
||||
FilesReadSystem/
|
||||
├── backend/ # 后端服务(Python + FastAPI)
|
||||
│ ├── app/
|
||||
│ │ ├── api/endpoints/ # API路由层 / API endpoints
|
||||
│ │ │ ├── ai_analyze.py # AI分析接口 / AI analysis
|
||||
│ │ │ ├── documents.py # 文档管理 / Document management
|
||||
│ │ │ ├── instruction.py # 自然语言指令 / Natural language instruction
|
||||
│ │ │ ├── rag.py # RAG检索 / RAG retrieval
|
||||
│ │ │ ├── tasks.py # 任务管理 / Task management
|
||||
│ │ │ ├── templates.py # 模板管理 / Template management
|
||||
│ │ │ ├── upload.py # 文件上传 / File upload
|
||||
│ │ │ └── visualization.py # 可视化 / Visualization
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── database/ # 数据库连接 / Database connections
|
||||
│ │ │ └── document_parser/ # 文档解析器 / Document parsers
|
||||
│ │ ├── services/ # 业务逻辑服务 / Business logic services
|
||||
│ │ │ ├── llm_service.py # LLM调用 / LLM service
|
||||
│ │ │ ├── rag_service.py # RAG流水线 / RAG pipeline
|
||||
│ │ │ ├── template_fill_service.py # 模板填充 / Template filling
|
||||
│ │ │ ├── excel_ai_service.py # Excel AI分析 / Excel AI analysis
|
||||
│ │ │ ├── word_ai_service.py # Word AI分析 / Word AI analysis
|
||||
│ │ │ └── table_rag_service.py # 表格RAG / Table RAG
|
||||
│ │ └── instruction/ # 指令解析与执行 / Instruction parsing & execution
|
||||
│ ├── requirements.txt # Python依赖 / Python dependencies
|
||||
│ └── README.md
|
||||
│
|
||||
├── frontend/ # 前端项目(React + TypeScript)
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # 页面组件 / Page components
|
||||
│ │ │ ├── Dashboard.tsx # 仪表板 / Dashboard
|
||||
│ │ │ ├── Documents.tsx # 文档管理 / Document management
|
||||
│ │ │ ├── TemplateFill.tsx # 模板填充 / Template fill
|
||||
│ │ │ └── InstructionChat.tsx # 指令聊天 / Instruction chat
|
||||
│ │ ├── components/ui/ # shadcn/ui组件库 / shadcn/ui components
|
||||
│ │ ├── contexts/ # React上下文 / React contexts
|
||||
│ │ ├── db/ # API调用封装 / API call wrappers
|
||||
│ │ └── supabase/functions/ # Edge函数 / Edge functions
|
||||
│ ├── package.json
|
||||
│ └── README.md
|
||||
│
|
||||
├── docs/ # 文档与测试数据 / Documentation & test data
|
||||
├── logs/ # 应用日志 / Application logs
|
||||
└── README.md # 本文件 / This file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 主要功能 / Key Features
|
||||
|
||||
- **多格式文档解析** / Multi-format Document Parsing
|
||||
- Excel (.xlsx)
|
||||
- Word (.docx)
|
||||
- Markdown (.md)
|
||||
- Plain Text (.txt)
|
||||
|
||||
- **AI智能分析** / AI-Powered Analysis
|
||||
- 文档内容理解与摘要
|
||||
- 表格数据自动提取
|
||||
- 多文档联合推理
|
||||
|
||||
- **RAG检索增强** / RAG (Retrieval Augmented Generation)
|
||||
- 语义向量相似度搜索
|
||||
- 上下文感知的答案生成
|
||||
|
||||
- **模板自动填充** / Template Auto-fill
|
||||
- 智能表格模板识别
|
||||
- 自然语言指令驱动填写
|
||||
- 批量数据导入导出
|
||||
|
||||
- **自然语言指令** / Natural Language Instructions
|
||||
- 意图识别与解析
|
||||
- 多步骤任务自动执行
|
||||
|
||||
---
|
||||
|
||||
## API接口 / API Endpoints
|
||||
|
||||
| 方法 / Method | 路径 / Path | 说明 / Description |
|
||||
|:---|:---|:---|
|
||||
| GET | `/health` | 健康检查 / Health check |
|
||||
| POST | `/upload/document` | 单文件上传 / Single file upload |
|
||||
| POST | `/upload/documents` | 批量上传 / Batch upload |
|
||||
| GET | `/documents` | 文档库 / Document library |
|
||||
| GET | `/tasks/{task_id}` | 任务状态 / Task status |
|
||||
| POST | `/rag/search` | RAG语义搜索 / RAG search |
|
||||
| POST | `/templates/upload` | 模板上传 / Template upload |
|
||||
| POST | `/templates/fill` | 执行模板填充 / Execute template fill |
|
||||
| POST | `/ai/analyze/excel` | Excel AI分析 / Excel AI analysis |
|
||||
| POST | `/ai/analyze/word` | Word AI分析 / Word AI analysis |
|
||||
| POST | `/instruction/recognize` | 意图识别 / Intent recognition |
|
||||
| POST | `/instruction/execute` | 执行指令 / Execute instruction |
|
||||
| GET | `/visualization/statistics` | 统计图表 / Statistics charts |
|
||||
|
||||
---
|
||||
|
||||
## 环境配置 / Environment Setup
|
||||
|
||||
### 后端 / Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 创建虚拟环境 / Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# 激活虚拟环境 / Activate virtual environment
|
||||
# Windows PowerShell:
|
||||
.\venv\Scripts\Activate.ps1
|
||||
# Windows CMD:
|
||||
.\venv\Scripts\Activate.bat
|
||||
|
||||
# 安装依赖 / Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 复制环境变量模板 / Copy environment template
|
||||
copy .env.example .env
|
||||
# 编辑 .env 填入API密钥 / Edit .env with your API keys
|
||||
```
|
||||
|
||||
### 前端 / Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# 安装依赖 / Install dependencies
|
||||
npm install
|
||||
|
||||
# 或使用 pnpm / Or using pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 启动项目 / Starting the Project
|
||||
|
||||
### 后端启动 / Backend Startup
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./venv/Scripts/python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
```
|
||||
|
||||
### 前端启动 / Frontend Startup
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev
|
||||
# 或 / or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
前端地址 / Frontend URL: http://localhost:5173
|
||||
|
||||
---
|
||||
|
||||
## 配置说明 / Configuration
|
||||
|
||||
### 环境变量 / Environment Variables
|
||||
|
||||
| 变量 / Variable | 说明 / Description |
|
||||
|:---|:---|
|
||||
| `MONGODB_URL` | MongoDB连接地址 / MongoDB connection URL |
|
||||
| `MYSQL_HOST` | MySQL主机 / MySQL host |
|
||||
| `REDIS_URL` | Redis连接地址 / Redis connection URL |
|
||||
| `MINIMAX_API_KEY` | MiniMax API密钥 / MiniMax API key |
|
||||
| `MINIMAX_API_URL` | MiniMax API地址 / MiniMax API URL |
|
||||
|
||||
---
|
||||
|
||||
## 许可证 / License
|
||||
|
||||
ISC
|
||||
@@ -1,854 +0,0 @@
|
||||
diff --git a/backend/app/api/endpoints/templates.py b/backend/app/api/endpoints/templates.py
|
||||
index 572d56e..706f281 100644
|
||||
--- a/backend/app/api/endpoints/templates.py
|
||||
+++ b/backend/app/api/endpoints/templates.py
|
||||
@@ -13,7 +13,7 @@ import pandas as pd
|
||||
from pydantic import BaseModel
|
||||
|
||||
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__)
|
||||
|
||||
@@ -28,13 +28,15 @@ class TemplateFieldRequest(BaseModel):
|
||||
name: str
|
||||
field_type: str = "text"
|
||||
required: bool = True
|
||||
+ hint: str = ""
|
||||
|
||||
|
||||
class FillRequest(BaseModel):
|
||||
"""填写请求"""
|
||||
template_id: str
|
||||
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
|
||||
|
||||
|
||||
@@ -71,7 +73,6 @@ async def upload_template(
|
||||
|
||||
try:
|
||||
# 保存文件
|
||||
- from app.services.file_service import file_service
|
||||
content = await file.read()
|
||||
saved_path = file_service.save_uploaded_file(
|
||||
content,
|
||||
@@ -87,7 +88,7 @@ async def upload_template(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
- "template_id": saved_path, # 使用文件路径作为ID
|
||||
+ "template_id": saved_path,
|
||||
"filename": file.filename,
|
||||
"file_type": file_ext,
|
||||
"fields": [
|
||||
@@ -95,7 +96,8 @@ async def upload_template(
|
||||
"cell": f.cell,
|
||||
"name": f.name,
|
||||
"field_type": f.field_type,
|
||||
- "required": f.required
|
||||
+ "required": f.required,
|
||||
+ "hint": f.hint
|
||||
}
|
||||
for f in template_fields
|
||||
],
|
||||
@@ -135,7 +137,8 @@ async def extract_template_fields(
|
||||
"cell": f.cell,
|
||||
"name": f.name,
|
||||
"field_type": f.field_type,
|
||||
- "required": f.required
|
||||
+ "required": f.required,
|
||||
+ "hint": f.hint
|
||||
}
|
||||
for f in fields
|
||||
]
|
||||
@@ -153,7 +156,7 @@ async def fill_template(
|
||||
"""
|
||||
执行表格填写
|
||||
|
||||
- 根据提供的字段定义,从已上传的文档中检索信息并填写
|
||||
+ 根据提供的字段定义,从源文档中检索信息并填写
|
||||
|
||||
Args:
|
||||
request: 填写请求
|
||||
@@ -168,7 +171,8 @@ async def fill_template(
|
||||
cell=f.cell,
|
||||
name=f.name,
|
||||
field_type=f.field_type,
|
||||
- required=f.required
|
||||
+ required=f.required,
|
||||
+ hint=f.hint
|
||||
)
|
||||
for f in request.template_fields
|
||||
]
|
||||
@@ -177,6 +181,7 @@ async def fill_template(
|
||||
result = await template_fill_service.fill_template(
|
||||
template_fields=fields,
|
||||
source_doc_ids=request.source_doc_ids,
|
||||
+ source_file_paths=request.source_file_paths,
|
||||
user_hint=request.user_hint
|
||||
)
|
||||
|
||||
@@ -194,6 +199,8 @@ async def export_filled_template(
|
||||
"""
|
||||
导出填写后的表格
|
||||
|
||||
+ 支持 Excel (.xlsx) 和 Word (.docx) 格式
|
||||
+
|
||||
Args:
|
||||
request: 导出请求
|
||||
|
||||
@@ -201,25 +208,124 @@ async def export_filled_template(
|
||||
文件流
|
||||
"""
|
||||
try:
|
||||
- # 创建 DataFrame
|
||||
- df = pd.DataFrame([request.filled_data])
|
||||
+ if request.format == "xlsx":
|
||||
+ return await _export_to_excel(request.filled_data, request.template_id)
|
||||
+ elif request.format == "docx":
|
||||
+ return await _export_to_word(request.filled_data, request.template_id)
|
||||
+ else:
|
||||
+ raise HTTPException(
|
||||
+ status_code=400,
|
||||
+ detail=f"不支持的导出格式: {request.format},仅支持 xlsx/docx"
|
||||
+ )
|
||||
|
||||
- # 导出为 Excel
|
||||
- output = io.BytesIO()
|
||||
- with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
- df.to_excel(writer, index=False, sheet_name='填写结果')
|
||||
+ except HTTPException:
|
||||
+ raise
|
||||
+ except Exception as e:
|
||||
+ logger.error(f"导出失败: {str(e)}")
|
||||
+ raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
||||
|
||||
- output.seek(0)
|
||||
|
||||
- # 生成文件名
|
||||
- filename = f"filled_template.{request.format}"
|
||||
+async def _export_to_excel(filled_data: dict, template_id: str) -> StreamingResponse:
|
||||
+ """导出为 Excel 格式"""
|
||||
+ # 将字典转换为单行 DataFrame
|
||||
+ df = pd.DataFrame([filled_data])
|
||||
|
||||
- return StreamingResponse(
|
||||
- io.BytesIO(output.getvalue()),
|
||||
- media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
- headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
- )
|
||||
+ output = io.BytesIO()
|
||||
+ with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
+ df.to_excel(writer, index=False, sheet_name='填写结果')
|
||||
|
||||
- except Exception as e:
|
||||
- logger.error(f"导出失败: {str(e)}")
|
||||
- raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
||||
+ 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)
|
||||
diff --git a/backend/app/core/document_parser/docx_parser.py b/backend/app/core/document_parser/docx_parser.py
|
||||
index 75e79da..03c341d 100644
|
||||
--- a/backend/app/core/document_parser/docx_parser.py
|
||||
+++ b/backend/app/core/document_parser/docx_parser.py
|
||||
@@ -161,3 +161,133 @@ class DocxParser(BaseParser):
|
||||
fields[field_name] = match.group(1)
|
||||
|
||||
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"
|
||||
diff --git a/backend/app/services/template_fill_service.py b/backend/app/services/template_fill_service.py
|
||||
index 2612354..94930fb 100644
|
||||
--- a/backend/app/services/template_fill_service.py
|
||||
+++ b/backend/app/services/template_fill_service.py
|
||||
@@ -4,13 +4,12 @@
|
||||
从非结构化文档中检索信息并填写到表格模板
|
||||
"""
|
||||
import logging
|
||||
-from dataclasses import dataclass
|
||||
+from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
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.excel_storage_service import excel_storage_service
|
||||
+from app.core.document_parser import ParserFactory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,6 +21,17 @@ class TemplateField:
|
||||
name: str # 字段名称
|
||||
field_type: str = "text" # 字段类型: text/number/date
|
||||
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
|
||||
@@ -38,12 +48,12 @@ class TemplateFillService:
|
||||
|
||||
def __init__(self):
|
||||
self.llm = llm_service
|
||||
- self.rag = rag_service
|
||||
|
||||
async def fill_template(
|
||||
self,
|
||||
template_fields: List[TemplateField],
|
||||
source_doc_ids: Optional[List[str]] = None,
|
||||
+ source_file_paths: Optional[List[str]] = None,
|
||||
user_hint: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -51,7 +61,8 @@ class TemplateFillService:
|
||||
|
||||
Args:
|
||||
template_fields: 模板字段列表
|
||||
- source_doc_ids: 源文档ID列表,不指定则从所有文档检索
|
||||
+ source_doc_ids: 源文档 MongoDB ID 列表
|
||||
+ source_file_paths: 源文档文件路径列表
|
||||
user_hint: 用户提示(如"请从合同文档中提取")
|
||||
|
||||
Returns:
|
||||
@@ -60,28 +71,23 @@ class TemplateFillService:
|
||||
filled_data = {}
|
||||
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:
|
||||
try:
|
||||
- # 1. 从 RAG 检索相关上下文
|
||||
- rag_results = await self._retrieve_context(field.name, 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. 存储结果
|
||||
+ # 从源文档中提取字段值
|
||||
+ result = await self._extract_field_value(
|
||||
+ field=field,
|
||||
+ source_docs=source_docs,
|
||||
+ user_hint=user_hint
|
||||
+ )
|
||||
+
|
||||
+ # 存储结果
|
||||
filled_data[field.name] = result.value
|
||||
fill_details.append({
|
||||
"field": field.name,
|
||||
@@ -107,75 +113,113 @@ class TemplateFillService:
|
||||
return {
|
||||
"success": True,
|
||||
"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,
|
||||
- field_name: str,
|
||||
- user_hint: Optional[str] = None
|
||||
- ) -> List[Dict[str, Any]]:
|
||||
+ source_doc_ids: Optional[List[str]] = None,
|
||||
+ source_file_paths: Optional[List[str]] = None
|
||||
+ ) -> List[SourceDocument]:
|
||||
"""
|
||||
- 从 RAG 检索相关上下文
|
||||
+ 加载源文档内容
|
||||
|
||||
Args:
|
||||
- field_name: 字段名称
|
||||
- user_hint: 用户提示
|
||||
+ source_doc_ids: MongoDB 文档 ID 列表
|
||||
+ source_file_paths: 源文档文件路径列表
|
||||
|
||||
Returns:
|
||||
- 检索结果列表
|
||||
+ 源文档列表
|
||||
"""
|
||||
- # 构建查询文本
|
||||
- query = field_name
|
||||
- if user_hint:
|
||||
- query = f"{user_hint} {field_name}"
|
||||
-
|
||||
- # 检索相关文档片段
|
||||
- results = self.rag.retrieve(query=query, top_k=5)
|
||||
-
|
||||
- return results
|
||||
+ source_docs = []
|
||||
+
|
||||
+ # 1. 从 MongoDB 加载文档
|
||||
+ 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)}")
|
||||
+
|
||||
+ # 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(
|
||||
self,
|
||||
field: TemplateField,
|
||||
- rag_context: List[Dict[str, Any]],
|
||||
+ source_docs: List[SourceDocument],
|
||||
user_hint: Optional[str] = None
|
||||
) -> FillResult:
|
||||
"""
|
||||
- 使用 LLM 从上下文中提取字段值
|
||||
+ 使用 LLM 从源文档中提取字段值
|
||||
|
||||
Args:
|
||||
field: 字段定义
|
||||
- rag_context: RAG 检索到的上下文
|
||||
+ source_docs: 源文档列表
|
||||
user_hint: 用户提示
|
||||
|
||||
Returns:
|
||||
提取结果
|
||||
"""
|
||||
+ if not source_docs:
|
||||
+ return FillResult(
|
||||
+ field=field.name,
|
||||
+ value="",
|
||||
+ source="无源文档",
|
||||
+ confidence=0.0
|
||||
+ )
|
||||
+
|
||||
# 构建上下文文本
|
||||
- context_text = "\n\n".join([
|
||||
- f"【文档 {i+1}】\n{doc['content']}"
|
||||
- for i, doc in enumerate(rag_context)
|
||||
- ])
|
||||
+ 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
|
||||
- prompt = f"""你是一个数据提取专家。请根据以下文档内容,提取指定字段的信息。
|
||||
+ prompt = f"""你是一个专业的数据提取专家。请根据以下文档内容,提取指定字段的信息。
|
||||
|
||||
需要提取的字段:
|
||||
- 字段名称:{field.name}
|
||||
- 字段类型:{field.field_type}
|
||||
+- 填写提示:{hint_text}
|
||||
- 是否必填:{'是' if field.required else '否'}
|
||||
|
||||
-{'用户提示:' + user_hint if user_hint else ''}
|
||||
-
|
||||
参考文档内容:
|
||||
{context_text}
|
||||
|
||||
请严格按照以下 JSON 格式输出,不要添加任何解释:
|
||||
{{
|
||||
"value": "提取到的值,如果没有找到则填写空字符串",
|
||||
- "source": "数据来源的文档描述",
|
||||
- "confidence": 0.0到1.0之间的置信度
|
||||
+ "source": "数据来源的文档描述(如:来自xxx文档)",
|
||||
+ "confidence": 0.0到1.0之间的置信度,表示对提取结果的信心程度"
|
||||
}}
|
||||
"""
|
||||
|
||||
@@ -226,6 +270,54 @@ class TemplateFillService:
|
||||
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(
|
||||
self,
|
||||
file_path: str,
|
||||
@@ -236,7 +328,7 @@ class TemplateFillService:
|
||||
|
||||
Args:
|
||||
file_path: 模板文件路径
|
||||
- file_type: 文件类型
|
||||
+ file_type: 文件类型 (xlsx/xls/docx)
|
||||
|
||||
Returns:
|
||||
字段列表
|
||||
@@ -245,43 +337,108 @@ class TemplateFillService:
|
||||
|
||||
try:
|
||||
if file_type in ["xlsx", "xls"]:
|
||||
- # 从 Excel 读取表头
|
||||
- import pandas as pd
|
||||
- df = pd.read_excel(file_path, nrows=5)
|
||||
+ fields = await self._get_template_fields_from_excel(file_path)
|
||||
+ elif file_type == "docx":
|
||||
+ fields = await self._get_template_fields_from_docx(file_path)
|
||||
|
||||
- for idx, col in enumerate(df.columns):
|
||||
- # 获取单元格位置 (A, B, C, ...)
|
||||
- cell = self._column_to_cell(idx)
|
||||
+ except Exception as e:
|
||||
+ logger.error(f"提取模板字段失败: {str(e)}")
|
||||
|
||||
- fields.append(TemplateField(
|
||||
- cell=cell,
|
||||
- name=str(col),
|
||||
- field_type=self._infer_field_type(df[col]),
|
||||
- required=True
|
||||
- ))
|
||||
+ return fields
|
||||
|
||||
- elif file_type == "docx":
|
||||
- # 从 Word 表格读取
|
||||
- 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
|
||||
- ))
|
||||
+ 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"提取模板字段失败: {str(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:
|
||||
"""将列索引转换为单元格列名 (0 -> A, 1 -> B, ...)"""
|
||||
result = ""
|
||||
@@ -290,17 +447,6 @@ class TemplateFillService:
|
||||
col_idx = col_idx // 26 - 1
|
||||
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"
|
||||
-
|
||||
|
||||
# ==================== 全局单例 ====================
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
diff --git a/frontend/src/db/backend-api.ts b/frontend/src/db/backend-api.ts
|
||||
index 8944353..94ac852 100644
|
||||
--- a/frontend/src/db/backend-api.ts
|
||||
+++ b/frontend/src/db/backend-api.ts
|
||||
@@ -92,6 +92,7 @@ export interface TemplateField {
|
||||
name: string;
|
||||
field_type: string;
|
||||
required: boolean;
|
||||
+ hint?: string;
|
||||
}
|
||||
|
||||
// 表格填写结果
|
||||
@@ -625,7 +626,10 @@ export const backendApi = {
|
||||
*/
|
||||
async fillTemplate(
|
||||
templateId: string,
|
||||
- templateFields: TemplateField[]
|
||||
+ templateFields: TemplateField[],
|
||||
+ sourceDocIds?: string[],
|
||||
+ sourceFilePaths?: string[],
|
||||
+ userHint?: string
|
||||
): Promise<FillResult> {
|
||||
const url = `${BACKEND_BASE_URL}/templates/fill`;
|
||||
|
||||
@@ -636,6 +640,9 @@ export const backendApi = {
|
||||
body: JSON.stringify({
|
||||
template_id: templateId,
|
||||
template_fields: templateFields,
|
||||
+ source_doc_ids: sourceDocIds || [],
|
||||
+ source_file_paths: sourceFilePaths || [],
|
||||
+ user_hint: userHint || null,
|
||||
}),
|
||||
});
|
||||
|
||||
diff --git a/frontend/src/pages/TemplateFill.tsx b/frontend/src/pages/TemplateFill.tsx
|
||||
index 8c330a9..f9a4a39 100644
|
||||
--- a/frontend/src/pages/TemplateFill.tsx
|
||||
+++ b/frontend/src/pages/TemplateFill.tsx
|
||||
@@ -128,8 +128,12 @@ const TemplateFill: React.FC = () => {
|
||||
setStep('filling');
|
||||
|
||||
try {
|
||||
- // 调用后端填表接口
|
||||
- const result = await backendApi.fillTemplate('temp-template-id', templateFields);
|
||||
+ // 调用后端填表接口,传递选中的文档ID
|
||||
+ const result = await backendApi.fillTemplate(
|
||||
+ 'temp-template-id',
|
||||
+ templateFields,
|
||||
+ selectedDocs // 传递源文档ID列表
|
||||
+ );
|
||||
setFilledResult(result);
|
||||
setStep('preview');
|
||||
toast.success('表格填写完成');
|
||||
@@ -1,221 +0,0 @@
|
||||
diff --git "a/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md" "b/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md"
|
||||
index bcb48fd..440a12d 100644
|
||||
--- "a/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md"
|
||||
+++ "b/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md"
|
||||
@@ -50,7 +50,7 @@
|
||||
| `prompt_service.py` | ✅ 已完成 | Prompt 模板管理 |
|
||||
| `text_analysis_service.py` | ✅ 已完成 | 文本分析 |
|
||||
| `chart_generator_service.py` | ✅ 已完成 | 图表生成服务 |
|
||||
-| `template_fill_service.py` | ❌ 未完成 | 模板填写服务 |
|
||||
+| `template_fill_service.py` | ✅ 已完成 | 模板填写服务,支持直接读取源文档进行填表 |
|
||||
|
||||
### 2.2 API 接口 (`backend/app/api/endpoints/`)
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
| `ai_analyze.py` | `/api/v1/analyze/*` | ✅ AI 分析(Excel、Markdown、流式) |
|
||||
| `rag.py` | `/api/v1/rag/*` | ⚠️ RAG 检索(当前返回空) |
|
||||
| `tasks.py` | `/api/v1/tasks/*` | ✅ 异步任务状态查询 |
|
||||
-| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理 |
|
||||
+| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理 (含 Word 导出) |
|
||||
| `visualization.py` | `/api/v1/visualization/*` | ✅ 可视化图表 |
|
||||
| `health.py` | `/api/v1/health` | ✅ 健康检查 |
|
||||
|
||||
@@ -78,8 +78,8 @@
|
||||
|------|----------|------|
|
||||
| Excel (.xlsx/.xls) | ✅ 已完成 | pandas + XML 回退解析 |
|
||||
| Markdown (.md) | ✅ 已完成 | 正则 + AI 分章节 |
|
||||
-| Word (.docx) | ❌ 未完成 | 尚未实现 |
|
||||
-| Text (.txt) | ❌ 未完成 | 尚未实现 |
|
||||
+| Word (.docx) | ✅ 已完成 | python-docx 解析,支持表格提取和字段识别 |
|
||||
+| Text (.txt) | ✅ 已完成 | chardet 编码检测,支持文本清洗和结构化提取 |
|
||||
|
||||
---
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
### 3.1 模板填写模块(最优先)
|
||||
|
||||
-**这是比赛的核心评测功能,必须完成。**
|
||||
+**当前状态**:✅ 已完成
|
||||
|
||||
```
|
||||
用户上传模板表格(Word/Excel)
|
||||
@@ -103,30 +103,34 @@ AI 根据字段提示词从源数据中提取信息
|
||||
返回填写完成的表格
|
||||
```
|
||||
|
||||
-**需要实现**:
|
||||
-- [ ] `template_fill_service.py` - 模板填写核心服务
|
||||
-- [ ] Word 模板解析 (`docx_parser.py` 需新建)
|
||||
-- [ ] Text 模板解析 (`txt_parser.py` 需新建)
|
||||
-- [ ] 模板字段识别与提示词提取
|
||||
-- [ ] 多文档数据聚合与冲突处理
|
||||
-- [ ] 结果导出为 Word/Excel
|
||||
+**已完成实现**:
|
||||
+- [x] `template_fill_service.py` - 模板填写核心服务
|
||||
+- [x] Word 模板解析 (`docx_parser.py` - parse_tables_for_template, extract_template_fields_from_docx)
|
||||
+- [x] Text 模板解析 (`txt_parser.py` - 已完成)
|
||||
+- [x] 模板字段识别与提示词提取
|
||||
+- [x] 多文档数据聚合与冲突处理
|
||||
+- [x] 结果导出为 Word/Excel
|
||||
|
||||
### 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 文档解析
|
||||
|
||||
-**需要实现**:
|
||||
-- [ ] `txt_parser.py` - 文本文件解析器
|
||||
-- [ ] 编码自动检测
|
||||
-- [ ] 文本清洗
|
||||
+**当前状态**:✅ 已完成
|
||||
+
|
||||
+**已实现功能**:
|
||||
+- [x] `txt_parser.py` - 文本文件解析器
|
||||
+- [x] 编码自动检测 (chardet)
|
||||
+- [x] 文本清洗
|
||||
|
||||
### 3.4 文档模板匹配(已有框架)
|
||||
|
||||
@@ -215,5 +219,122 @@ docs/test/
|
||||
|
||||
---
|
||||
|
||||
-*文档版本: v1.0*
|
||||
-*最后更新: 2026-04-08*
|
||||
\ No newline at end of file
|
||||
+*文档版本: v1.1*
|
||||
+*最后更新: 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"
|
||||
+}
|
||||
+```
|
||||
\ No newline at end of file
|
||||
@@ -1,59 +0,0 @@
|
||||
RAG 服务临时禁用说明
|
||||
========================
|
||||
日期: 2026-04-08
|
||||
|
||||
修改内容:
|
||||
----------
|
||||
应需求,RAG 向量检索功能已临时禁用,具体如下:
|
||||
|
||||
1. 修改文件: backend/app/services/rag_service.py
|
||||
|
||||
2. 关键变更:
|
||||
- 在 RAGService.__init__ 中添加 self._disabled = True 标志
|
||||
- index_field() - 添加 _disabled 检查,跳过实际索引操作并记录日志
|
||||
- index_document_content() - 添加 _disabled 检查,跳过实际索引操作并记录日志
|
||||
- retrieve() - 添加 _disabled 检查,返回空列表并记录日志
|
||||
- get_vector_count() - 添加 _disabled 检查,返回 0 并记录日志
|
||||
- clear() - 添加 _disabled 检查,跳过实际清空操作并记录日志
|
||||
|
||||
3. 行为变更:
|
||||
- 所有 RAG 索引构建操作会被记录到日志 ([RAG DISABLED] 前缀)
|
||||
- 所有 RAG 检索操作返回空结果
|
||||
- 向量计数始终返回 0
|
||||
- 实际向量数据库操作被跳过
|
||||
|
||||
4. 恢复方式:
|
||||
- 将 RAGService.__init__ 中的 self._disabled = True 改为 self._disabled = False
|
||||
- 重新启动服务即可恢复 RAG 功能
|
||||
|
||||
目的:
|
||||
------
|
||||
保留 RAG 索引构建功能的前端界面和代码结构,暂不实际调用向量数据库 API,
|
||||
待后续需要时再启用。
|
||||
|
||||
影响范围:
|
||||
---------
|
||||
- /api/v1/rag/search - RAG 搜索接口 (返回空结果)
|
||||
- /api/v1/rag/status - RAG 状态接口 (返回 vector_count=0)
|
||||
- /api/v1/rag/rebuild - RAG 重建接口 (仅记录日志)
|
||||
- Excel/文档上传时的 RAG 索引构建 (仅记录日志)
|
||||
|
||||
========================
|
||||
后续补充 (2026-04-08):
|
||||
========================
|
||||
修改文件: backend/app/services/table_rag_service.py
|
||||
|
||||
关键变更:
|
||||
- 在 TableRAGService.__init__ 中添加 self._disabled = True 标志
|
||||
- build_table_rag_index() - RAG 索引部分被跳过,仅记录日志
|
||||
- index_document_table() - RAG 索引部分被跳过,仅记录日志
|
||||
|
||||
行为变更:
|
||||
- Excel 上传时,MySQL 存储仍然正常进行
|
||||
- AI 字段描述仍然正常生成(调用 LLM)
|
||||
- 只有向量数据库索引操作被跳过
|
||||
|
||||
恢复方式:
|
||||
- 将 TableRAGService.__init__ 中的 self._disabled = True 改为 self._disabled = False
|
||||
- 或将 rag_service.py 中的 self._disabled = True 改为 self._disabled = False
|
||||
- 两者需同时改为 False 才能完全恢复 RAG 功能
|
||||
@@ -1,144 +0,0 @@
|
||||
# 模板填表功能变更日志
|
||||
|
||||
**变更日期**: 2026-04-08
|
||||
**变更类型**: 功能完善
|
||||
**变更内容**: Word 表格解析和模板填表功能
|
||||
|
||||
---
|
||||
|
||||
## 变更概述
|
||||
|
||||
本次变更完善了 Word 表格解析、表格模板构建和填写功能,实现了从源文档(MongoDB/文件)读取数据并智能填表的核心流程。
|
||||
|
||||
### 涉及文件
|
||||
|
||||
| 文件 | 变更行数 | 说明 |
|
||||
|------|----------|------|
|
||||
| backend/app/api/endpoints/templates.py | +156 | API 端点完善,添加 Word 导出 |
|
||||
| backend/app/core/document_parser/docx_parser.py | +130 | Word 表格解析增强 |
|
||||
| backend/app/services/template_fill_service.py | +340 | 核心填表服务重写 |
|
||||
| frontend/src/db/backend-api.ts | +9 | 前端 API 更新 |
|
||||
| frontend/src/pages/TemplateFill.tsx | +8 | 前端页面更新 |
|
||||
| 比赛备赛规划.md | +169 | 文档更新 |
|
||||
|
||||
---
|
||||
|
||||
## 详细变更
|
||||
|
||||
### 1. backend/app/core/document_parser/docx_parser.py
|
||||
|
||||
**新增方法**:
|
||||
|
||||
- `parse_tables_for_template(file_path)` - 解析 Word 文档中的表格,提取模板字段
|
||||
- `extract_template_fields_from_docx(file_path)` - 从 Word 文档提取模板字段定义
|
||||
- `_infer_field_type_from_hint(hint)` - 从提示词推断字段类型
|
||||
|
||||
**功能说明**:
|
||||
- 专门用于比赛场景:解析表格模板,识别需要填写的字段
|
||||
- 支持从表格第一列提取字段名,第二列提取提示词/描述
|
||||
- 自动推断字段类型(text/number/date)
|
||||
|
||||
### 2. backend/app/services/template_fill_service.py
|
||||
|
||||
**重构内容**:
|
||||
|
||||
- 不再依赖 RAG 服务,直接从 MongoDB 或文件读取源文档
|
||||
- 新增 `SourceDocument` 数据类
|
||||
- 完善 `fill_template()` 方法,支持 `source_doc_ids` 和 `source_file_paths`
|
||||
- 新增 `_load_source_documents()` - 加载源文档内容
|
||||
- 新增 `_extract_field_value()` - 使用 LLM 提取字段值
|
||||
- 新增 `_build_context_text()` - 构建上下文(优先使用表格数据)
|
||||
- 完善 `_get_template_fields_from_docx()` - Word 模板字段提取
|
||||
|
||||
**核心流程**:
|
||||
```
|
||||
1. 加载源文档(MongoDB 或文件)
|
||||
2. 对每个字段调用 LLM 提取值
|
||||
3. 返回填写结果
|
||||
```
|
||||
|
||||
### 3. backend/app/api/endpoints/templates.py
|
||||
|
||||
**新增内容**:
|
||||
|
||||
- `FillRequest` 添加 `source_doc_ids`, `source_file_paths`, `user_hint` 字段
|
||||
- `ExportRequest` 添加 `format` 字段
|
||||
- `_export_to_word()` - 导出为 Word 格式
|
||||
- `/templates/export/excel` - 专门导出 Excel
|
||||
- `/templates/export/word` - 专门导出 Word
|
||||
|
||||
### 4. frontend/src/db/backend-api.ts
|
||||
|
||||
**更新内容**:
|
||||
|
||||
- `TemplateField` 接口添加 `hint` 字段
|
||||
- `fillTemplate()` 方法添加 `sourceDocIds`, `sourceFilePaths`, `userHint` 参数
|
||||
|
||||
### 5. frontend/src/pages/TemplateFill.tsx
|
||||
|
||||
**更新内容**:
|
||||
|
||||
- `handleFillTemplate()` 传递 `selectedDocs` 作为 `sourceDocIds` 参数
|
||||
|
||||
---
|
||||
|
||||
## 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"],
|
||||
"source_file_paths": [],
|
||||
"user_hint": "请从xxx文档中提取"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"filled_data": {"姓名": "张三"},
|
||||
"fill_details": [...],
|
||||
"source_doc_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/templates/export
|
||||
|
||||
**新增支持 format=dicx**,可导出为 Word 格式
|
||||
|
||||
---
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 字段类型推断
|
||||
|
||||
| 关键词 | 推断类型 |
|
||||
|--------|----------|
|
||||
| 年、月、日、日期、时间、出生 | date |
|
||||
| 数量、金额、比率、%、率、合计 | number |
|
||||
| 其他 | text |
|
||||
|
||||
### 上下文构建
|
||||
|
||||
源文档内容构建优先级:
|
||||
1. 结构化数据(表格数据)
|
||||
2. 原始文本内容(限制 5000 字符)
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [比赛备赛规划.md](../比赛备赛规划.md) - 已更新功能状态和技术实现细节
|
||||
20
package.json
20
package.json
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "filesreadsystem",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.kronecker.cc/OurCodesAreAllRight/FilesReadSystem.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs"
|
||||
}
|
||||
558
比赛备赛规划.md
558
比赛备赛规划.md
@@ -1,558 +0,0 @@
|
||||
# 比赛备赛规划文档
|
||||
|
||||
## 一、赛题核心理解
|
||||
|
||||
### 1.1 赛题名称
|
||||
**A23 - 基于大语言模型的文档理解与多源数据融合**
|
||||
参赛院校:金陵科技学院
|
||||
|
||||
### 1.2 核心任务
|
||||
1. **文档解析**:解析 docx/md/xlsx/txt 四种格式的源数据文档
|
||||
2. **模板填写**:根据模板表格要求,从源文档中提取数据填写到 Word/Excel 模板
|
||||
3. **准确率与速度**:准确率优先,速度作为辅助评分因素
|
||||
|
||||
### 1.3 评分规则
|
||||
| 要素 | 说明 |
|
||||
|------|------|
|
||||
| 准确率 | 填写结果与样例表格对比的正确率 |
|
||||
| 响应时间 | 从导入文档到得到结果的时间 ≤ 90s × 文档数量 |
|
||||
| 评测方式 | 赛方提供空表格模板 + 样例表格(人工填写),系统自动填写后对比 |
|
||||
|
||||
### 1.4 关键Q&A摘录
|
||||
|
||||
| 问题 | 解答要点 |
|
||||
|------|----------|
|
||||
| Q2: 模板与文档的关系 | 前2个表格只涉及1份文档;第3-4个涉及多份文档;第5个涉及大部分文档(从易到难) |
|
||||
| Q5: 响应时间定义 | 从导入文档到最终得到结果的时间 ≤ 90s × 文档数量 |
|
||||
| Q7: 需要读取哪些文件 | 每个模板只读取指定的数据文件,不需要读取全部 |
|
||||
| Q10: 部署方式 | 不要求部署到服务器,本地部署即可 |
|
||||
| Q14: 模板匹配 | 模板已指定数据文件,不需要算法匹配 |
|
||||
| Q16: 数据库存储 | 可跳过,不强制要求 |
|
||||
| Q20: 创新点 | 不用管,随意发挥 |
|
||||
| Q21: 填写依据 | 按照测试表格模板给的提示词进行填写 |
|
||||
|
||||
---
|
||||
|
||||
## 二、已完成功能清单
|
||||
|
||||
### 2.1 后端服务 (`backend/app/services/`)
|
||||
|
||||
| 服务文件 | 功能状态 | 说明 |
|
||||
|----------|----------|------|
|
||||
| `file_service.py` | ✅ 已完成 | 文件上传、保存、类型识别 |
|
||||
| `excel_storage_service.py` | ✅ 已完成 | Excel 存储到 MySQL,支持 XML 回退解析 |
|
||||
| `table_rag_service.py` | ⚠️ 已禁用 | RAG 索引构建(当前禁用,仅记录日志) |
|
||||
| `llm_service.py` | ✅ 已完成 | LLM 调用、流式输出、多模型支持 |
|
||||
| `markdown_ai_service.py` | ✅ 已完成 | Markdown AI 分析、分章节提取、流式输出、图表生成 |
|
||||
| `excel_ai_service.py` | ✅ 已完成 | Excel AI 分析 |
|
||||
| `visualization_service.py` | ✅ 已完成 | 图表生成(matplotlib) |
|
||||
| `rag_service.py` | ⚠️ 已禁用 | FAISS 向量检索(当前禁用) |
|
||||
| `prompt_service.py` | ✅ 已完成 | Prompt 模板管理 |
|
||||
| `text_analysis_service.py` | ✅ 已完成 | 文本分析 |
|
||||
| `chart_generator_service.py` | ✅ 已完成 | 图表生成服务 |
|
||||
| `template_fill_service.py` | ✅ 已完成 | 模板填写服务,支持多行提取、直接从结构化数据提取、JSON容错、Word文档表格处理 |
|
||||
|
||||
### 2.2 API 接口 (`backend/app/api/endpoints/`)
|
||||
|
||||
| 接口文件 | 路由 | 功能状态 |
|
||||
|----------|------|----------|
|
||||
| `upload.py` | `/api/v1/upload/document` | ✅ 文档上传与解析 |
|
||||
| `documents.py` | `/api/v1/documents/*` | ✅ 文档管理(列表、删除、搜索) |
|
||||
| `ai_analyze.py` | `/api/v1/analyze/*` | ✅ AI 分析(Excel、Markdown、流式) |
|
||||
| `rag.py` | `/api/v1/rag/*` | ⚠️ RAG 检索(当前返回空) |
|
||||
| `tasks.py` | `/api/v1/tasks/*` | ✅ 异步任务状态查询 |
|
||||
| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理(含多行导出、Word导出、Word结构化字段解析) |
|
||||
| `visualization.py` | `/api/v1/visualization/*` | ✅ 可视化图表 |
|
||||
| `health.py` | `/api/v1/health` | ✅ 健康检查 |
|
||||
|
||||
### 2.3 前端页面 (`frontend/src/pages/`)
|
||||
|
||||
| 页面文件 | 功能 | 状态 |
|
||||
|----------|------|------|
|
||||
| `Documents.tsx` | 主文档管理页面 | ✅ 已完成 |
|
||||
| `TemplateFill.tsx` | 智能填表页面 | ✅ 已完成 |
|
||||
| `ExcelParse.tsx` | Excel 解析页面 | ✅ 已完成 |
|
||||
|
||||
### 2.4 文档解析能力
|
||||
|
||||
| 格式 | 解析状态 | 说明 |
|
||||
|------|----------|------|
|
||||
| Excel (.xlsx/.xls) | ✅ 已完成 | pandas + XML 回退解析,支持多sheet |
|
||||
| Markdown (.md) | ✅ 已完成 | 正则 + AI 分章节 |
|
||||
| Word (.docx) | ✅ 已完成 | python-docx 解析,支持表格提取和字段识别 |
|
||||
| Text (.txt) | ✅ 已完成 | chardet 编码检测,支持文本清洗和结构化提取 |
|
||||
|
||||
---
|
||||
|
||||
## 三、核心功能实现详情
|
||||
|
||||
### 3.1 模板填写模块(✅ 已完成)
|
||||
|
||||
**核心流程**:
|
||||
```
|
||||
上传模板表格(Word/Excel)
|
||||
↓
|
||||
解析模板,提取需要填写的字段和提示词
|
||||
↓
|
||||
根据源文档ID列表读取源数据(MongoDB或文件)
|
||||
↓
|
||||
优先从结构化数据直接提取(Excel rows)
|
||||
↓
|
||||
无法直接提取时使用 LLM 从文本中提取
|
||||
↓
|
||||
将提取的数据填入原始模板对应位置(保持模板格式)
|
||||
↓
|
||||
导出填写完成的表格(Excel/Word)
|
||||
```
|
||||
|
||||
**关键特性**:
|
||||
- **原始模板填充**:直接打开原始模板文件,填充数据到原表格/单元格
|
||||
- **多行数据支持**:每个字段可提取多个值,导出时自动扩展行数
|
||||
- **结构化数据优先**:直接从 Excel rows 提取,无需 LLM
|
||||
- **JSON 容错**:支持 LLM 返回的损坏/截断 JSON
|
||||
- **Markdown 清理**:自动清理 LLM 返回的 markdown 格式
|
||||
|
||||
### 3.2 Word 文档解析(✅ 已完成)
|
||||
|
||||
**已实现功能**:
|
||||
- `docx_parser.py` - Word 文档解析器
|
||||
- 提取段落文本
|
||||
- 提取表格内容(支持比赛表格格式:字段名 | 提示词 | 填写值)
|
||||
- `parse_tables_for_template()` - 解析表格模板,提取字段
|
||||
- `extract_template_fields_from_docx()` - 提取模板字段定义
|
||||
- `_infer_field_type_from_hint()` - 从提示词推断字段类型
|
||||
- **API 端点**:`/api/v1/templates/parse-word-structure` - 上传 Word 文档,提取结构化字段并存入 MongoDB
|
||||
- **API 端点**:`/api/v1/templates/word-fields/{doc_id}` - 获取已存文档的模板字段信息
|
||||
|
||||
### 3.3 Text 文档解析(✅ 已完成)
|
||||
|
||||
**已实现功能**:
|
||||
- `txt_parser.py` - 文本文件解析器
|
||||
- 编码自动检测 (chardet)
|
||||
- 文本清洗(去除控制字符、规范化空白)
|
||||
- 结构化数据提取(邮箱、URL、电话、日期、金额)
|
||||
|
||||
---
|
||||
|
||||
## 四、参赛材料准备
|
||||
|
||||
### 4.1 必交材料
|
||||
|
||||
| 材料 | 要求 | 当前状态 | 行动项 |
|
||||
|------|------|----------|--------|
|
||||
| 项目概要介绍 | PPT 格式 | ❌ 待制作 | 制作 PPT |
|
||||
| 项目简介 PPT | - | ❌ 待制作 | 制作 PPT |
|
||||
| 项目详细方案 | 文档 | ⚠️ 部分完成 | 完善文档 |
|
||||
| 项目演示视频 | - | ❌ 待制作 | 录制演示视频 |
|
||||
| 训练素材说明 | 来源说明 | ⚠️ 已有素材 | 整理素材文档 |
|
||||
| 关键模块设计文档 | 概要设计 | ⚠️ 已有部分 | 完善文档 |
|
||||
| 可运行 Demo | 核心代码 | ✅ 已完成 | 打包可运行版本 |
|
||||
|
||||
### 4.2 Demo 提交要求
|
||||
|
||||
根据 Q&A:
|
||||
- 可以只提交核心代码,不需要完整运行环境
|
||||
- 现场答辩可使用自带笔记本电脑
|
||||
- 需要提供部署和运行说明(README)
|
||||
|
||||
---
|
||||
|
||||
## 五、测试验证计划
|
||||
|
||||
### 5.1 使用现有测试数据
|
||||
|
||||
```
|
||||
docs/test/
|
||||
├── 2023年文化和旅游发展统计公报.md
|
||||
├── 2024年卫生健康事业发展统计公报.md
|
||||
├── 第三次全国工业普查主要数据公报.md
|
||||
```
|
||||
|
||||
### 5.2 模板填写测试流程
|
||||
|
||||
1. 准备一个 Word/Excel 模板表格
|
||||
2. 指定源数据文档
|
||||
3. 上传模板和文档
|
||||
4. 执行模板填写
|
||||
5. 检查填写结果准确率
|
||||
6. 记录响应时间
|
||||
|
||||
### 5.3 性能目标
|
||||
|
||||
| 指标 | 目标 | 当前状态 |
|
||||
|------|------|----------|
|
||||
| 信息提取准确率 | ≥80% | 需测试验证 |
|
||||
| 单次响应时间 | ≤90s × 文档数 | 需测试验证 |
|
||||
|
||||
---
|
||||
|
||||
## 六、工作计划(建议)
|
||||
|
||||
### 第一优先级:端到端测试
|
||||
- 使用真实测试数据进行准确率测试
|
||||
- 验证多行数据导出是否正确
|
||||
- 测试 Word 模板解析是否正常
|
||||
|
||||
### 第二优先级:Demo 打包与文档
|
||||
- 制作项目演示 PPT
|
||||
- 录制演示视频
|
||||
- 完善 README 部署文档
|
||||
|
||||
### 第三优先级:优化
|
||||
- 优化响应时间
|
||||
- 完善错误处理
|
||||
- 增加更多测试用例
|
||||
|
||||
---
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
1. **创新点**:根据 Q&A,不必纠结创新点数量限制
|
||||
2. **数据库**:不强制要求数据库存储,可跳过
|
||||
3. **部署**:本地部署即可,不需要公网服务器
|
||||
4. **评测数据**:初赛仅使用目前提供的数据
|
||||
5. **RAG 功能**:当前已临时禁用,不影响核心评测功能(因为使用直接文件读取)
|
||||
|
||||
---
|
||||
|
||||
*文档版本: v1.5*
|
||||
*最后更新: 2026-04-09*
|
||||
|
||||
---
|
||||
|
||||
## 八、技术实现细节
|
||||
|
||||
### 8.1 模板填表流程
|
||||
|
||||
#### 流程图
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 上传模板 │ ──► │ 选择数据源 │ ──► │ 智能填表 │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
┌─────────────────────────┼─────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ 结构化数据提取 │ │ LLM 提取 │ │ 导出结果 │
|
||||
│ (直接读rows) │ │ (文本理解) │ │ (Excel/Word) │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
#### 核心组件
|
||||
|
||||
| 组件 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 模板上传 | `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 提取 |
|
||||
| 多行支持 | `template_fill_service.py` `FillResult` | values 数组支持 |
|
||||
| JSON 容错 | `template_fill_service.py` `_fix_json()` | 修复损坏的 JSON |
|
||||
| 结果导出 | `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
|
||||
# 提取表格模板字段
|
||||
from docx_parser import DocxParser
|
||||
parser = DocxParser()
|
||||
fields = 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/upload`
|
||||
|
||||
上传模板文件,提取字段定义。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"template_id": "/path/to/saved/template.docx",
|
||||
"filename": "模板.docx",
|
||||
"file_type": "docx",
|
||||
"fields": [
|
||||
{"cell": "A1", "name": "姓名", "field_type": "text", "required": true, "hint": "提取人员姓名"}
|
||||
],
|
||||
"field_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 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": "请从xxx文档中提取"
|
||||
}
|
||||
```
|
||||
|
||||
**响应(含多行支持)**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"filled_data": {
|
||||
"姓名": ["张三", "李四", "王五"],
|
||||
"年龄": ["25", "30", "28"]
|
||||
},
|
||||
"fill_details": [
|
||||
{
|
||||
"field": "姓名",
|
||||
"cell": "A1",
|
||||
"values": ["张三", "李四", "王五"],
|
||||
"value": "张三",
|
||||
"source": "结构化数据直接提取",
|
||||
"confidence": 1.0
|
||||
}
|
||||
],
|
||||
"source_doc_count": 2,
|
||||
"max_rows": 3
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/v1/templates/export`
|
||||
|
||||
导出请求(创建新文件):
|
||||
```json
|
||||
{
|
||||
"template_id": "模板ID",
|
||||
"filled_data": {"姓名": ["张三", "李四"], "金额": ["10000", "20000"]},
|
||||
"format": "xlsx"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/v1/templates/fill-and-export`
|
||||
|
||||
**填充原始模板并导出**(推荐用于比赛)
|
||||
|
||||
直接打开原始模板文件,将数据填入模板的表格/单元格中,然后导出。**保持原始模板格式不变**。
|
||||
|
||||
**请求**:
|
||||
```json
|
||||
{
|
||||
"template_path": "/path/to/original/template.docx",
|
||||
"filled_data": {
|
||||
"姓名": ["张三", "李四", "王五"],
|
||||
"年龄": ["25", "30", "28"]
|
||||
},
|
||||
"format": "docx"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:填充后的 Word/Excel 文件(文件流)
|
||||
|
||||
**特点**:
|
||||
- 打开原始模板文件
|
||||
- 根据表头行匹配字段名到列索引
|
||||
- 将数据填入对应列的单元格
|
||||
- 多行数据自动扩展表格行数
|
||||
- 保持原始模板格式和样式
|
||||
|
||||
#### POST `/api/v1/templates/parse-word-structure`
|
||||
|
||||
**上传 Word 文档并提取结构化字段**(比赛专用)
|
||||
|
||||
上传 Word 文档,从表格模板中提取字段定义(字段名、提示词、字段类型)并存入 MongoDB。
|
||||
|
||||
**请求**:multipart/form-data
|
||||
- file: Word 文件
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"doc_id": "mongodb_doc_id",
|
||||
"filename": "模板.docx",
|
||||
"file_path": "/path/to/saved/template.docx",
|
||||
"field_count": 5,
|
||||
"fields": [
|
||||
{
|
||||
"cell": "T0R1",
|
||||
"name": "字段名",
|
||||
"hint": "提示词",
|
||||
"field_type": "text",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tables": [...],
|
||||
"metadata": {
|
||||
"paragraph_count": 10,
|
||||
"table_count": 1,
|
||||
"word_count": 500,
|
||||
"has_tables": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/v1/templates/word-fields/{doc_id}`
|
||||
|
||||
**获取 Word 文档模板字段信息**
|
||||
|
||||
根据 doc_id 获取已上传的 Word 文档的模板字段信息。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"doc_id": "mongodb_doc_id",
|
||||
"filename": "模板.docx",
|
||||
"fields": [...],
|
||||
"tables": [...],
|
||||
"field_count": 5,
|
||||
"metadata": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 多行数据处理
|
||||
|
||||
**FillResult 数据结构**:
|
||||
```python
|
||||
@dataclass
|
||||
class FillResult:
|
||||
field: str
|
||||
values: List[Any] = None # 支持多个值(数组)
|
||||
value: Any = "" # 保留兼容(第一个值)
|
||||
source: str = "" # 来源文档
|
||||
confidence: float = 1.0 # 置信度
|
||||
```
|
||||
|
||||
**导出逻辑**:
|
||||
- 计算所有字段的最大行数
|
||||
- 遍历每一行,取对应索引的值
|
||||
- 不足的行填空字符串
|
||||
|
||||
### 8.7 JSON 容错处理
|
||||
|
||||
当 LLM 返回的 JSON 损坏或被截断时,系统会:
|
||||
|
||||
1. 清理 markdown 代码块标记(```json, ```)
|
||||
2. 尝试配对括号找到完整的 JSON
|
||||
3. 移除末尾多余的逗号
|
||||
4. 使用正则表达式提取 values 数组
|
||||
5. 备选方案:直接提取所有引号内的字符串
|
||||
|
||||
### 8.8 结构化数据优先提取
|
||||
|
||||
对于 Excel 等有 `rows` 结构的文档,系统会:
|
||||
|
||||
1. 直接从 `structured_data.rows` 中查找匹配列
|
||||
2. 使用模糊匹配(字段名包含或被包含)
|
||||
3. 提取该列的所有行值
|
||||
4. 无需调用 LLM,速度更快,准确率更高
|
||||
|
||||
```python
|
||||
# 内部逻辑
|
||||
if structured.get("rows"):
|
||||
columns = structured.get("columns", [])
|
||||
values = _extract_column_values(rows, columns, field_name)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、依赖说明
|
||||
|
||||
### Python 依赖
|
||||
|
||||
```
|
||||
# requirements.txt 中需要包含
|
||||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
motor>=3.3.0 # MongoDB 异步驱动
|
||||
sqlalchemy>=2.0.0 # MySQL ORM
|
||||
pandas>=2.0.0 # Excel 处理
|
||||
openpyxl>=3.1.0 # Excel 写入
|
||||
python-docx>=0.8.0 # Word 处理
|
||||
chardet>=4.0.0 # 编码检测
|
||||
httpx>=0.25.0 # HTTP 客户端
|
||||
```
|
||||
|
||||
### 前端依赖
|
||||
|
||||
```
|
||||
# package.json 中需要包含
|
||||
react>=18.0.0
|
||||
react-dropzone>=14.0.0
|
||||
lucide-react>=0.300.0
|
||||
sonner>=1.0.0 # toast 通知
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、启动说明
|
||||
|
||||
### 后端启动
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
.\venv\Scripts\Activate.ps1 # 或 Activate.bat
|
||||
pip install -r requirements.txt # 确保依赖完整
|
||||
.\venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
```
|
||||
|
||||
### 前端启动
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
在 `backend/.env` 中配置:
|
||||
```
|
||||
MONGODB_URL=mongodb://localhost:27017
|
||||
MONGODB_DB_NAME=document_system
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=your_password
|
||||
MYSQL_DATABASE=document_system
|
||||
LLM_API_KEY=your_api_key
|
||||
LLM_BASE_URL=https://api.minimax.chat
|
||||
LLM_MODEL_NAME=MiniMax-Text-01
|
||||
```
|
||||
Reference in New Issue
Block a user