feat: 实现智能指令的格式转换和文档编辑功能

主要更新:
- 新增 transform 意图:支持 Word/Excel/Markdown 格式互转
- 新增 edit 意图:使用 LLM 润色编辑文档内容
- 智能指令接口增加异步执行模式(async_execute 参数)
- 修复 Word 模板导出文档损坏问题(改用临时文件方式)
- 优化 intent_parser 增加 transform/edit 关键词识别

新增文件:
- app/api/endpoints/instruction.py: 智能指令 API 端点
- app/services/multi_doc_reasoning_service.py: 多文档推理服务

其他优化:
- RAG 服务混合搜索(BM25 + 向量)融合
- 模板填充服务表头匹配增强
- Word AI 解析服务返回结构完善
- 前端 InstructionChat 组件对接真实 API
This commit is contained in:
dj
2026-04-14 20:39:37 +08:00
parent 51350e3002
commit ecad9ccd82
12 changed files with 2943 additions and 196 deletions

View File

@@ -1,15 +1,14 @@
"""
指令执行模块
注意: 此模块为可选功能,当前尚未实现。
如需启用,请实现 intent_parser.py 和 executor.py
支持文档智能操作交互,包括意图解析和指令执行
"""
from .intent_parser import IntentParser, DefaultIntentParser
from .executor import InstructionExecutor, DefaultInstructionExecutor
from .intent_parser import IntentParser, intent_parser
from .executor import InstructionExecutor, instruction_executor
__all__ = [
"IntentParser",
"DefaultIntentParser",
"intent_parser",
"InstructionExecutor",
"DefaultInstructionExecutor",
"instruction_executor",
]

View File

@@ -2,34 +2,571 @@
指令执行器模块
将自然语言指令转换为可执行操作
注意: 此模块为可选功能,当前尚未实现。
"""
from abc import ABC, abstractmethod
from typing import Any, Dict
import logging
import json
from typing import Any, Dict, List, Optional
from app.services.template_fill_service import template_fill_service
from app.services.rag_service import rag_service
from app.services.markdown_ai_service import markdown_ai_service
from app.core.database import mongodb
logger = logging.getLogger(__name__)
class InstructionExecutor(ABC):
"""指令执行器抽象基类"""
class InstructionExecutor:
"""指令执行器"""
@abstractmethod
async def execute(self, instruction: str, context: Dict[str, Any]) -> Dict[str, Any]:
def __init__(self):
self.intent_parser = None # 将通过 set_intent_parser 设置
def set_intent_parser(self, intent_parser):
"""设置意图解析器"""
self.intent_parser = intent_parser
async def execute(self, instruction: str, context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
执行指令
Args:
instruction: 解析后的指令
context: 执行上下文
instruction: 自然语言指令
context: 执行上下文(包含文档信息等)
Returns:
执行结果
"""
pass
if self.intent_parser is None:
from app.instruction.intent_parser import intent_parser
self.intent_parser = intent_parser
context = context or {}
# 解析意图
intent, params = await self.intent_parser.parse(instruction)
# 根据意图类型执行相应操作
if intent == "extract":
return await self._execute_extract(params, context)
elif intent == "fill_table":
return await self._execute_fill_table(params, context)
elif intent == "summarize":
return await self._execute_summarize(params, context)
elif intent == "question":
return await self._execute_question(params, context)
elif intent == "search":
return await self._execute_search(params, context)
elif intent == "compare":
return await self._execute_compare(params, context)
elif intent == "edit":
return await self._execute_edit(params, context)
elif intent == "transform":
return await self._execute_transform(params, context)
else:
return {
"success": False,
"error": f"未知意图类型: {intent}",
"message": "无法理解该指令,请尝试更明确的描述"
}
async def _execute_extract(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行信息提取"""
try:
target_fields = params.get("field_refs", [])
doc_ids = params.get("document_refs", [])
if not target_fields:
return {
"success": False,
"error": "未指定要提取的字段",
"message": "请明确说明要提取哪些字段,如:'提取医院数量和床位数'"
}
# 如果指定了文档,验证文档存在
if doc_ids and "all_docs" not in doc_ids:
valid_docs = []
for doc_ref in doc_ids:
doc_id = doc_ref.replace("doc_", "")
doc = await mongodb.get_document(doc_id)
if doc:
valid_docs.append(doc)
if not valid_docs:
return {
"success": False,
"error": "指定的文档不存在",
"message": "请检查文档编号是否正确"
}
context["source_docs"] = valid_docs
# 构建字段列表
fields = []
for i, field_name in enumerate(target_fields):
fields.append({
"name": field_name,
"cell": f"A{i+1}",
"field_type": "text",
"required": False
})
# 调用填表服务
result = await template_fill_service.fill_template(
template_fields=fields,
source_doc_ids=[doc.get("_id") for doc in context.get("source_docs", [])] if context.get("source_docs") else None,
user_hint=f"请提取字段: {', '.join(target_fields)}"
)
return {
"success": True,
"intent": "extract",
"extracted_data": result.get("filled_data", {}),
"fields": target_fields,
"message": f"成功提取 {len(result.get('filled_data', {}))} 个字段"
}
except Exception as e:
logger.error(f"提取执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"提取失败: {str(e)}"
}
async def _execute_fill_table(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行填表操作"""
try:
template_file = context.get("template_file")
if not template_file:
return {
"success": False,
"error": "未提供表格模板",
"message": "请先上传要填写的表格模板"
}
# 获取源文档
source_docs = context.get("source_docs", [])
source_doc_ids = [doc.get("_id") for doc in source_docs if doc.get("_id")]
# 获取字段
fields = context.get("template_fields", [])
# 调用填表服务
result = await template_fill_service.fill_template(
template_fields=fields,
source_doc_ids=source_doc_ids if source_doc_ids else None,
source_file_paths=context.get("source_file_paths"),
user_hint=params.get("user_hint"),
template_id=template_file if isinstance(template_file, str) else None,
template_file_type=params.get("template", {}).get("type", "xlsx")
)
return {
"success": True,
"intent": "fill_table",
"result": result,
"message": f"填表完成,成功填写 {len(result.get('filled_data', {}))} 个字段"
}
except Exception as e:
logger.error(f"填表执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"填表失败: {str(e)}"
}
async def _execute_summarize(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行摘要总结"""
try:
docs = context.get("source_docs", [])
if not docs:
return {
"success": False,
"error": "没有可用的文档",
"message": "请先上传要总结的文档"
}
summaries = []
for doc in docs[:5]: # 最多处理5个文档
content = doc.get("content", "")[:5000] # 限制内容长度
if content:
summaries.append({
"filename": doc.get("metadata", {}).get("original_filename", "未知"),
"content_preview": content[:500] + "..." if len(content) > 500 else content
})
return {
"success": True,
"intent": "summarize",
"summaries": summaries,
"message": f"找到 {len(summaries)} 个文档可供参考"
}
except Exception as e:
logger.error(f"摘要执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"摘要生成失败: {str(e)}"
}
async def _execute_question(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行问答"""
try:
question = params.get("question", "")
if not question:
return {
"success": False,
"error": "未提供问题",
"message": "请输入要回答的问题"
}
# 使用 RAG 检索相关文档
docs = context.get("source_docs", [])
rag_results = []
for doc in docs:
doc_id = doc.get("_id", "")
if doc_id:
results = rag_service.retrieve_by_doc_id(doc_id, top_k=3)
rag_results.extend(results)
# 构建上下文
context_text = "\n\n".join([
r.get("content", "") for r in rag_results[:5]
]) if rag_results else ""
# 如果没有 RAG 结果,使用文档内容
if not context_text:
context_text = "\n\n".join([
doc.get("content", "")[:3000] for doc in docs[:3] if doc.get("content")
])
return {
"success": True,
"intent": "question",
"question": question,
"context_preview": context_text[:500] + "..." if len(context_text) > 500 else context_text,
"message": "已找到相关上下文,可进行问答"
}
except Exception as e:
logger.error(f"问答执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"问答处理失败: {str(e)}"
}
async def _execute_search(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行搜索"""
try:
field_refs = params.get("field_refs", [])
query = " ".join(field_refs) if field_refs else params.get("question", "")
if not query:
return {
"success": False,
"error": "未提供搜索关键词",
"message": "请输入要搜索的关键词"
}
# 使用 RAG 检索
results = rag_service.retrieve(query, top_k=10, min_score=0.3)
return {
"success": True,
"intent": "search",
"query": query,
"results": [
{
"content": r.get("content", "")[:200],
"score": r.get("score", 0),
"doc_id": r.get("doc_id", "")
}
for r in results[:10]
],
"message": f"找到 {len(results)} 条相关结果"
}
except Exception as e:
logger.error(f"搜索执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"搜索失败: {str(e)}"
}
async def _execute_compare(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行对比分析"""
try:
docs = context.get("source_docs", [])
if len(docs) < 2:
return {
"success": False,
"error": "对比需要至少2个文档",
"message": "请上传至少2个文档进行对比"
}
# 提取文档基本信息
comparison = []
for i, doc in enumerate(docs[:5]):
comparison.append({
"index": i + 1,
"filename": doc.get("metadata", {}).get("original_filename", "未知"),
"doc_type": doc.get("doc_type", "未知"),
"content_length": len(doc.get("content", "")),
"has_tables": bool(doc.get("structured_data", {}).get("tables")),
})
return {
"success": True,
"intent": "compare",
"comparison": comparison,
"message": f"对比了 {len(comparison)} 个文档的基本信息"
}
except Exception as e:
logger.error(f"对比执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"对比分析失败: {str(e)}"
}
async def _execute_edit(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""执行文档编辑操作"""
try:
docs = context.get("source_docs", [])
if not docs:
return {
"success": False,
"error": "没有可用的文档",
"message": "请先上传要编辑的文档"
}
doc = docs[0] # 默认编辑第一个文档
content = doc.get("content", "")
original_filename = doc.get("metadata", {}).get("original_filename", "未知文档")
if not content:
return {
"success": False,
"error": "文档内容为空",
"message": "该文档没有可编辑的内容"
}
# 使用 LLM 进行文本润色/编辑
prompt = f"""请对以下文档内容进行编辑处理。
原文内容:
{content[:8000]}
编辑要求:
- 润色表述,使其更加专业流畅
- 修正明显的语法错误
- 保持原意不变
- 只返回编辑后的内容,不要解释
请直接输出编辑后的内容:"""
messages = [
{"role": "system", "content": "你是一个专业的文本编辑助手。请直接输出编辑后的内容。"},
{"role": "user", "content": prompt}
]
from app.services.llm_service import llm_service
response = await llm_service.chat(messages=messages, temperature=0.3, max_tokens=8000)
edited_content = llm_service.extract_message_content(response)
return {
"success": True,
"intent": "edit",
"edited_content": edited_content,
"original_filename": original_filename,
"message": "文档编辑完成,内容已返回"
}
except Exception as e:
logger.error(f"编辑执行失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"编辑处理失败: {str(e)}"
}
async def _execute_transform(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""
执行格式转换操作
支持:
- Word -> Excel
- Excel -> Word
- Markdown -> Word
- Word -> Markdown
"""
try:
docs = context.get("source_docs", [])
if not docs:
return {
"success": False,
"error": "没有可用的文档",
"message": "请先上传要转换的文档"
}
# 获取目标格式
template_info = params.get("template", {})
target_type = template_info.get("type", "")
if not target_type:
# 尝试从指令中推断
instruction = params.get("instruction", "")
if "excel" in instruction.lower() or "xlsx" in instruction.lower():
target_type = "xlsx"
elif "word" in instruction.lower() or "docx" in instruction.lower():
target_type = "docx"
elif "markdown" in instruction.lower() or "md" in instruction.lower():
target_type = "md"
if not target_type:
return {
"success": False,
"error": "未指定目标格式",
"message": "请说明要转换成什么格式转成Excel、转成Word"
}
doc = docs[0]
content = doc.get("content", "")
structured_data = doc.get("structured_data", {})
original_filename = doc.get("metadata", {}).get("original_filename", "未知文档")
# 构建转换内容
if structured_data.get("tables"):
# 有表格数据,生成表格格式的内容
tables = structured_data.get("tables", [])
table_content = []
for i, table in enumerate(tables[:3]): # 最多处理3个表格
headers = table.get("headers", [])
rows = table.get("rows", [])[:20] # 最多20行
if headers:
table_content.append(f"【表格 {i+1}")
table_content.append(" | ".join(str(h) for h in headers))
table_content.append(" | ".join(["---"] * len(headers)))
for row in rows:
if isinstance(row, list):
table_content.append(" | ".join(str(c) for c in row))
elif isinstance(row, dict):
table_content.append(" | ".join(str(row.get(h, "")) for h in headers))
table_content.append("")
if target_type == "xlsx":
# 生成 Excel 格式的数据JSON
excel_data = []
for table in tables[:1]: # 只处理第一个表格
headers = table.get("headers", [])
rows = table.get("rows", [])[:100]
for row in rows:
if isinstance(row, list):
excel_data.append(dict(zip(headers, row)))
elif isinstance(row, dict):
excel_data.append(row)
return {
"success": True,
"intent": "transform",
"transform_type": "to_excel",
"target_format": "xlsx",
"excel_data": excel_data,
"headers": headers,
"message": f"已转换为 Excel 格式,包含 {len(excel_data)} 行数据"
}
elif target_type in ["docx", "word"]:
# 生成 Word 格式的文本
word_content = f"# {original_filename}\n\n"
word_content += "\n".join(table_content)
return {
"success": True,
"intent": "transform",
"transform_type": "to_word",
"target_format": "docx",
"content": word_content,
"message": "已转换为 Word 格式"
}
elif target_type == "md":
# 生成 Markdown 格式
md_content = f"# {original_filename}\n\n"
md_content += "\n".join(table_content)
return {
"success": True,
"intent": "transform",
"transform_type": "to_markdown",
"target_format": "md",
"content": md_content,
"message": "已转换为 Markdown 格式"
}
# 无表格数据,使用纯文本内容转换
if target_type == "xlsx":
# 将文本内容转为 Excel 格式(每行作为一列)
lines = [line.strip() for line in content.split("\n") if line.strip()][:100]
excel_data = [{"行号": i+1, "内容": line} for i, line in enumerate(lines)]
return {
"success": True,
"intent": "transform",
"transform_type": "to_excel",
"target_format": "xlsx",
"excel_data": excel_data,
"headers": ["行号", "内容"],
"message": f"已将文本内容转换为 Excel包含 {len(excel_data)}"
}
elif target_type in ["docx", "word"]:
return {
"success": True,
"intent": "transform",
"transform_type": "to_word",
"target_format": "docx",
"content": content,
"message": "文档内容已准备好,可下载为 Word 格式"
}
elif target_type == "md":
# 简单的文本转 Markdown
md_lines = []
for line in content.split("\n"):
line = line.strip()
if line:
# 简单处理:如果行不长且不是列表格式,作为段落
if len(line) < 100 and not line.startswith(("-", "*", "1.", "2.", "3.")):
md_lines.append(line)
else:
md_lines.append(line)
else:
md_lines.append("")
return {
"success": True,
"intent": "transform",
"transform_type": "to_markdown",
"target_format": "md",
"content": "\n".join(md_lines),
"message": "已转换为 Markdown 格式"
}
return {
"success": False,
"error": "不支持的目标格式",
"message": f"暂不支持转换为 {target_type} 格式"
}
except Exception as e:
logger.error(f"格式转换失败: {e}")
return {
"success": False,
"error": str(e),
"message": f"格式转换失败: {str(e)}"
}
class DefaultInstructionExecutor(InstructionExecutor):
"""默认指令执行器"""
async def execute(self, instruction: str, context: Dict[str, Any]) -> Dict[str, Any]:
"""暂未实现"""
raise NotImplementedError("指令执行功能暂未实现")
# 全局单例
instruction_executor = InstructionExecutor()

View File

@@ -2,17 +2,51 @@
意图解析器模块
解析用户自然语言指令,识别意图和参数
注意: 此模块为可选功能,当前尚未实现。
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Tuple
import re
import logging
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
class IntentParser(ABC):
"""意图解析器抽象基类"""
class IntentParser:
"""意图解析器"""
# 意图类型定义
INTENT_EXTRACT = "extract" # 信息提取
INTENT_FILL_TABLE = "fill_table" # 填表
INTENT_SUMMARIZE = "summarize" # 摘要总结
INTENT_QUESTION = "question" # 问答
INTENT_SEARCH = "search" # 搜索
INTENT_COMPARE = "compare" # 对比分析
INTENT_TRANSFORM = "transform" # 格式转换
INTENT_EDIT = "edit" # 编辑文档
INTENT_UNKNOWN = "unknown" # 未知
# 意图关键词映射
INTENT_KEYWORDS = {
INTENT_EXTRACT: ["提取", "抽取", "获取", "找出", "查找", "识别", "找到"],
INTENT_FILL_TABLE: ["填表", "填写", "填充", "录入", "导入到表格", "填写到"],
INTENT_SUMMARIZE: ["总结", "摘要", "概括", "概述", "归纳", "提炼"],
INTENT_QUESTION: ["问答", "回答", "解释", "什么是", "为什么", "如何", "怎样", "多少", "几个"],
INTENT_SEARCH: ["搜索", "查找", "检索", "查询", ""],
INTENT_COMPARE: ["对比", "比较", "差异", "区别", "不同"],
INTENT_TRANSFORM: ["转换", "转化", "变成", "转为", "导出"],
INTENT_EDIT: ["修改", "编辑", "调整", "改写", "润色", "优化"],
}
# 实体模式定义
ENTITY_PATTERNS = {
"number": [r"\d+", r"[一二三四五六七八九十百千万]+"],
"date": [r"\d{4}", r"\d{1,2}月", r"\d{1,2}日"],
"percentage": [r"\d+(\.\d+)?%", r"\d+(\.\d+)?‰"],
"currency": [r"\d+(\.\d+)?万元", r"\d+(\.\d+)?亿元", r"\d+(\.\d+)?元"],
}
def __init__(self):
self.intent_history: List[Dict[str, Any]] = []
@abstractmethod
async def parse(self, text: str) -> Tuple[str, Dict[str, Any]]:
"""
解析自然语言指令
@@ -23,12 +57,186 @@ class IntentParser(ABC):
Returns:
(意图类型, 参数字典)
"""
pass
text = text.strip()
if not text:
return self.INTENT_UNKNOWN, {}
# 记录历史
self.intent_history.append({"text": text, "intent": None})
# 识别意图
intent = self._recognize_intent(text)
# 提取参数
params = self._extract_params(text, intent)
# 更新历史
if self.intent_history:
self.intent_history[-1]["intent"] = intent
logger.info(f"意图解析: text={text[:50]}..., intent={intent}, params={params}")
return intent, params
def _recognize_intent(self, text: str) -> str:
"""识别意图类型"""
intent_scores: Dict[str, float] = {}
for intent, keywords in self.INTENT_KEYWORDS.items():
score = 0
for keyword in keywords:
if keyword in text:
score += 1
if score > 0:
intent_scores[intent] = score
if not intent_scores:
return self.INTENT_UNKNOWN
# 返回得分最高的意图
return max(intent_scores, key=intent_scores.get)
def _extract_params(self, text: str, intent: str) -> Dict[str, Any]:
"""提取参数"""
params: Dict[str, Any] = {
"entities": self._extract_entities(text),
"document_refs": self._extract_document_refs(text),
"field_refs": self._extract_field_refs(text),
"template_refs": self._extract_template_refs(text),
}
# 根据意图类型提取特定参数
if intent == self.INTENT_QUESTION:
params["question"] = text
params["focus"] = self._extract_question_focus(text)
elif intent == self.INTENT_FILL_TABLE:
params["template"] = self._extract_template_info(text)
elif intent == self.INTENT_EXTRACT:
params["target_fields"] = self._extract_target_fields(text)
return params
def _extract_entities(self, text: str) -> Dict[str, List[str]]:
"""提取实体"""
entities: Dict[str, List[str]] = {}
for entity_type, patterns in self.ENTITY_PATTERNS.items():
matches = []
for pattern in patterns:
found = re.findall(pattern, text)
matches.extend(found)
if matches:
entities[entity_type] = list(set(matches))
return entities
def _extract_document_refs(self, text: str) -> List[str]:
"""提取文档引用"""
# 匹配 "文档1"、"doc1"、"第一个文档" 等
refs = []
# 数字索引: 文档1, doc1, 第1个文档
num_patterns = [
r"[文档doc]+(\d+)",
r"第(\d+)个文档",
r"第(\d+)份",
]
for pattern in num_patterns:
matches = re.findall(pattern, text.lower())
refs.extend([f"doc_{m}" for m in matches])
# "所有文档"、"全部文档"
if any(kw in text for kw in ["所有", "全部", "整个"]):
refs.append("all_docs")
return refs
def _extract_field_refs(self, text: str) -> List[str]:
"""提取字段引用"""
fields = []
# 匹配引号内的字段名
quoted = re.findall(r"['\"『「]([^'\"』」]+)['\"』」]", text)
fields.extend(quoted)
# 匹配 "xxx字段"、"xxx列" 等
field_patterns = [
r"([^\s]+)字段",
r"([^\s]+)列",
r"([^\s]+)数据",
]
for pattern in field_patterns:
matches = re.findall(pattern, text)
fields.extend(matches)
return list(set(fields))
def _extract_template_refs(self, text: str) -> List[str]:
"""提取模板引用"""
templates = []
# 匹配 "表格模板"、"Excel模板"、"表1" 等
template_patterns = [
r"([^\s]+模板)",
r"表(\d+)",
r"([^\s]+表格)",
]
for pattern in template_patterns:
matches = re.findall(pattern, text)
templates.extend(matches)
return list(set(templates))
def _extract_question_focus(self, text: str) -> Optional[str]:
"""提取问题焦点"""
# "什么是XXX"、"XXX是什么"
match = re.search(r"[什么是]([^?]+)", text)
if match:
return match.group(1).strip()
# "XXX有多少"
match = re.search(r"([^?]+)有多少", text)
if match:
return match.group(1).strip()
return None
def _extract_template_info(self, text: str) -> Optional[Dict[str, str]]:
"""提取模板信息"""
template_info: Dict[str, str] = {}
# 提取模板类型
if "excel" in text.lower() or "xlsx" in text.lower() or "电子表格" in text:
template_info["type"] = "xlsx"
elif "word" in text.lower() or "docx" in text.lower() or "文档" in text:
template_info["type"] = "docx"
return template_info if template_info else None
def _extract_target_fields(self, text: str) -> List[str]:
"""提取目标字段"""
fields = []
# 匹配 "提取XXX和YYY"、"抽取XXX、YYY"
patterns = [
r"提取([^(and|,|)+]+?)(?:和|与|、|,|plus)",
r"抽取([^(and|,|)+]+?)(?:和|与|、|,|plus)",
]
for pattern in patterns:
matches = re.findall(pattern, text)
fields.extend([m.strip() for m in matches if m.strip()])
return list(set(fields))
def get_intent_history(self) -> List[Dict[str, Any]]:
"""获取意图历史"""
return self.intent_history
def clear_history(self):
"""清空历史"""
self.intent_history = []
class DefaultIntentParser(IntentParser):
"""默认意图解析器"""
async def parse(self, text: str) -> Tuple[str, Dict[str, Any]]:
"""暂未实现"""
raise NotImplementedError("意图解析功能暂未实现")
# 全局单例
intent_parser = IntentParser()