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

@@ -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()