- 新增对话历史管理:MongoDB新增conversations集合,存储用户与AI的对话上下文,支持多轮对话意图延续
- 新增对话历史API(conversation.py):GET/DELETE conversation历史、列出所有会话
- 意图解析增强:支持基于对话历史的意图识别,上下文理解更准确
- 字段提取优化:支持"提取文档中的医院数量"等自然语言模式,智能去除"文档中的"前缀
- 文档对比优化:从指令中提取文件名并精确匹配source_docs,支持"对比A和B两个文档"
- 文档摘要优化:使用LLM生成真实AI摘要而非返回原始文档预览
【Word模板填表核心功能】
- Word模板字段生成:空白Word上传后,自动从源文档(Excel/Word/TXT/MD)内容AI生成字段名
- Word模板填表(_fill_docx):将提取数据写入Word模板表格,支持精确匹配、模糊匹配、追加新行
- 数据润色(_polish_word_filled_data):LLM对多行Excel数据进行统计归纳(合计/平均/极值),转化为专业自然语言描述
- 段落格式输出:使用📌字段名+值段落+分隔线(灰色横线)格式,提升可读性
- 导出链打通:fill_template返回filled_file_path,export直接返回已填好的Word文件
【其他修复】
- 修复Word导出Windows文件锁问题:NamedTemporaryFile改为mkstemp+close
- 修复Word方框非法字符:扩展clean_text移除\uFFFD、□等Unicode替代符和零宽字符
- 修复文档对比"需要至少2个文档":从指令提取具体文件名优先匹配而非取前2个
- 修复导出format硬编码:自动识别docx/xlsx格式
- Docx解析器增加备用解析方法和更完整的段落/表格/标题提取
- RAG服务新增MySQL数据源支持
473 lines
14 KiB
Python
473 lines
14 KiB
Python
"""
|
||
智能指令 API 接口
|
||
|
||
支持自然语言指令解析和执行
|
||
"""
|
||
import logging
|
||
import uuid
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
|
||
from pydantic import BaseModel
|
||
|
||
from app.instruction.intent_parser import intent_parser
|
||
from app.instruction.executor import instruction_executor
|
||
from app.core.database import mongodb
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/instruction", tags=["智能指令"])
|
||
|
||
|
||
# ==================== 请求/响应模型 ====================
|
||
|
||
class InstructionRequest(BaseModel):
|
||
instruction: str
|
||
doc_ids: Optional[List[str]] = None # 关联的文档 ID 列表
|
||
context: Optional[Dict[str, Any]] = None # 额外上下文
|
||
conversation_id: Optional[str] = None # 对话会话ID,用于关联历史记录
|
||
|
||
|
||
class IntentRecognitionResponse(BaseModel):
|
||
success: bool
|
||
intent: str
|
||
params: Dict[str, Any]
|
||
message: str
|
||
|
||
|
||
class InstructionExecutionResponse(BaseModel):
|
||
success: bool
|
||
intent: str
|
||
result: Dict[str, Any]
|
||
message: str
|
||
|
||
|
||
# ==================== 接口 ====================
|
||
|
||
@router.post("/recognize", response_model=IntentRecognitionResponse)
|
||
async def recognize_intent(request: InstructionRequest):
|
||
"""
|
||
意图识别接口
|
||
|
||
将自然语言指令解析为结构化的意图和参数
|
||
|
||
示例指令:
|
||
- "提取文档中的医院数量和床位数"
|
||
- "根据这些数据填表"
|
||
- "总结一下这份文档"
|
||
- "对比这两个文档的差异"
|
||
"""
|
||
try:
|
||
intent, params = await intent_parser.parse(request.instruction)
|
||
|
||
# 添加文档关联信息
|
||
if request.doc_ids:
|
||
params["document_refs"] = [f"doc_{doc_id}" for doc_id in request.doc_ids]
|
||
|
||
intent_names = {
|
||
"extract": "信息提取",
|
||
"fill_table": "表格填写",
|
||
"summarize": "摘要总结",
|
||
"question": "智能问答",
|
||
"search": "文档搜索",
|
||
"compare": "对比分析",
|
||
"transform": "格式转换",
|
||
"edit": "文档编辑",
|
||
"unknown": "未知"
|
||
}
|
||
|
||
return IntentRecognitionResponse(
|
||
success=True,
|
||
intent=intent,
|
||
params=params,
|
||
message=f"识别到意图: {intent_names.get(intent, intent)}"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"意图识别失败: {e}")
|
||
return IntentRecognitionResponse(
|
||
success=False,
|
||
intent="error",
|
||
params={},
|
||
message=f"意图识别失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/execute")
|
||
async def execute_instruction(
|
||
background_tasks: BackgroundTasks,
|
||
request: InstructionRequest,
|
||
async_execute: bool = Query(False, description="是否异步执行(仅返回任务ID)")
|
||
):
|
||
"""
|
||
指令执行接口
|
||
|
||
解析并执行自然语言指令
|
||
|
||
示例:
|
||
- 指令: "提取文档1中的医院数量"
|
||
返回: {"extracted_data": {"医院数量": ["38710个"]}}
|
||
|
||
- 指令: "填表"
|
||
返回: {"filled_data": {...}}
|
||
|
||
设置 async_execute=true 可异步执行,返回任务ID用于查询进度
|
||
"""
|
||
task_id = str(uuid.uuid4())
|
||
|
||
if async_execute:
|
||
# 异步模式:立即返回任务ID,后台执行
|
||
background_tasks.add_task(
|
||
_execute_instruction_task,
|
||
task_id=task_id,
|
||
instruction=request.instruction,
|
||
doc_ids=request.doc_ids,
|
||
context=request.context
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"task_id": task_id,
|
||
"message": "指令已提交执行",
|
||
"status_url": f"/api/v1/tasks/{task_id}"
|
||
}
|
||
|
||
# 同步模式:等待执行完成
|
||
return await _execute_instruction_task(task_id, request.instruction, request.doc_ids, request.context)
|
||
|
||
|
||
async def _execute_instruction_task(
|
||
task_id: str,
|
||
instruction: str,
|
||
doc_ids: Optional[List[str]],
|
||
context: Optional[Dict[str, Any]]
|
||
) -> InstructionExecutionResponse:
|
||
"""执行指令的后台任务"""
|
||
from app.core.database import redis_db, mongodb as mongo_client
|
||
|
||
try:
|
||
# 记录任务
|
||
try:
|
||
await mongo_client.insert_task(
|
||
task_id=task_id,
|
||
task_type="instruction_execute",
|
||
status="processing",
|
||
message="正在执行指令"
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# 构建执行上下文
|
||
ctx: Dict[str, Any] = context or {}
|
||
|
||
# 如果提供了文档 ID,获取文档内容
|
||
if doc_ids:
|
||
docs = []
|
||
for doc_id in doc_ids:
|
||
doc = await mongo_client.get_document(doc_id)
|
||
if doc:
|
||
docs.append(doc)
|
||
|
||
if docs:
|
||
ctx["source_docs"] = docs
|
||
logger.info(f"指令执行上下文: 关联了 {len(docs)} 个文档")
|
||
|
||
# 执行指令
|
||
result = await instruction_executor.execute(instruction, ctx)
|
||
|
||
# 更新任务状态
|
||
try:
|
||
await mongo_client.update_task(
|
||
task_id=task_id,
|
||
status="success",
|
||
message="执行完成",
|
||
result=result
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return InstructionExecutionResponse(
|
||
success=result.get("success", False),
|
||
intent=result.get("intent", "unknown"),
|
||
result=result,
|
||
message=result.get("message", "执行完成")
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"指令执行失败: {e}")
|
||
try:
|
||
await mongo_client.update_task(
|
||
task_id=task_id,
|
||
status="failure",
|
||
message="执行失败",
|
||
error=str(e)
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return InstructionExecutionResponse(
|
||
success=False,
|
||
intent="error",
|
||
result={"error": str(e)},
|
||
message=f"指令执行失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/chat")
|
||
async def instruction_chat(
|
||
background_tasks: BackgroundTasks,
|
||
request: InstructionRequest,
|
||
async_execute: bool = Query(False, description="是否异步执行(仅返回任务ID)")
|
||
):
|
||
"""
|
||
指令对话接口
|
||
|
||
支持多轮对话的指令执行
|
||
|
||
示例对话流程:
|
||
1. 用户: "上传一些文档"
|
||
2. 系统: "请上传文档"
|
||
3. 用户: "提取其中的医院数量"
|
||
4. 系统: 返回提取结果
|
||
|
||
设置 async_execute=true 可异步执行,返回任务ID用于查询进度
|
||
"""
|
||
task_id = str(uuid.uuid4())
|
||
|
||
if async_execute:
|
||
# 异步模式:立即返回任务ID,后台执行
|
||
background_tasks.add_task(
|
||
_execute_chat_task,
|
||
task_id=task_id,
|
||
instruction=request.instruction,
|
||
doc_ids=request.doc_ids,
|
||
context=request.context,
|
||
conversation_id=request.conversation_id
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"task_id": task_id,
|
||
"message": "指令已提交执行",
|
||
"status_url": f"/api/v1/tasks/{task_id}"
|
||
}
|
||
|
||
# 同步模式:等待执行完成
|
||
return await _execute_chat_task(task_id, request.instruction, request.doc_ids, request.context, request.conversation_id)
|
||
|
||
|
||
async def _execute_chat_task(
|
||
task_id: str,
|
||
instruction: str,
|
||
doc_ids: Optional[List[str]],
|
||
context: Optional[Dict[str, Any]],
|
||
conversation_id: Optional[str] = None
|
||
):
|
||
"""执行指令对话的后台任务"""
|
||
from app.core.database import mongodb as mongo_client
|
||
|
||
try:
|
||
# 记录任务
|
||
try:
|
||
await mongo_client.insert_task(
|
||
task_id=task_id,
|
||
task_type="instruction_chat",
|
||
status="processing",
|
||
message="正在处理对话"
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# 构建上下文
|
||
ctx: Dict[str, Any] = context or {}
|
||
|
||
# 获取对话历史
|
||
if conversation_id:
|
||
history = await mongo_client.get_conversation_history(conversation_id, limit=20)
|
||
if history:
|
||
ctx["conversation_history"] = history
|
||
logger.info(f"加载对话历史: conversation_id={conversation_id}, 消息数={len(history)}")
|
||
|
||
# 获取关联文档
|
||
if doc_ids:
|
||
docs = []
|
||
for doc_id in doc_ids:
|
||
doc = await mongo_client.get_document(doc_id)
|
||
if doc:
|
||
docs.append(doc)
|
||
if docs:
|
||
ctx["source_docs"] = docs
|
||
|
||
# 执行指令
|
||
result = await instruction_executor.execute(instruction, ctx)
|
||
|
||
# 存储对话历史
|
||
if conversation_id:
|
||
try:
|
||
# 存储用户消息
|
||
await mongo_client.insert_conversation(
|
||
conversation_id=conversation_id,
|
||
role="user",
|
||
content=instruction,
|
||
intent=result.get("intent", "unknown")
|
||
)
|
||
# 存储助手回复
|
||
response_content = result.get("message", "")
|
||
if response_content:
|
||
await mongo_client.insert_conversation(
|
||
conversation_id=conversation_id,
|
||
role="assistant",
|
||
content=response_content,
|
||
intent=result.get("intent", "unknown")
|
||
)
|
||
logger.info(f"已存储对话历史: conversation_id={conversation_id}")
|
||
except Exception as e:
|
||
logger.error(f"存储对话历史失败: {e}")
|
||
|
||
# 根据意图类型添加友好的响应消息
|
||
response_messages = {
|
||
"extract": f"已提取 {len(result.get('extracted_data', {}))} 个字段的数据",
|
||
"fill_table": f"填表完成,填写了 {len(result.get('result', {}).get('filled_data', {}))} 个字段",
|
||
"summarize": "已生成文档摘要",
|
||
"question": "已找到相关答案",
|
||
"search": f"找到 {len(result.get('results', []))} 条相关内容",
|
||
"compare": f"对比了 {len(result.get('comparison', []))} 个文档",
|
||
"edit": "编辑操作已完成",
|
||
"transform": "格式转换已完成",
|
||
"unknown": "无法理解该指令,请尝试更明确的描述"
|
||
}
|
||
|
||
response = {
|
||
"success": result.get("success", False),
|
||
"intent": result.get("intent", "unknown"),
|
||
"result": result,
|
||
"message": response_messages.get(result.get("intent", ""), result.get("message", "")),
|
||
"hint": _get_intent_hint(result.get("intent", ""))
|
||
}
|
||
|
||
# 更新任务状态
|
||
try:
|
||
await mongo_client.update_task(
|
||
task_id=task_id,
|
||
status="success",
|
||
message="处理完成",
|
||
result=response
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return response
|
||
|
||
except Exception as e:
|
||
logger.error(f"指令对话失败: {e}")
|
||
try:
|
||
await mongo_client.update_task(
|
||
task_id=task_id,
|
||
status="failure",
|
||
message="处理失败",
|
||
error=str(e)
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"message": f"处理失败: {str(e)}"
|
||
}
|
||
|
||
|
||
def _get_intent_hint(intent: str) -> Optional[str]:
|
||
"""根据意图返回下一步提示"""
|
||
hints = {
|
||
"extract": "您可以继续说 '提取更多字段' 或 '将数据填入表格'",
|
||
"fill_table": "您可以提供表格模板或说 '帮我创建一个表格'",
|
||
"question": "您可以继续提问或说 '总结一下这些内容'",
|
||
"search": "您可以查看搜索结果或说 '对比这些内容'",
|
||
"unknown": "您可以尝试: '提取数据'、'填表'、'总结'、'问答' 等指令"
|
||
}
|
||
return hints.get(intent)
|
||
|
||
|
||
@router.get("/intents")
|
||
async def list_supported_intents():
|
||
"""
|
||
获取支持的意图类型列表
|
||
|
||
返回所有可用的自然语言指令类型
|
||
"""
|
||
return {
|
||
"intents": [
|
||
{
|
||
"intent": "extract",
|
||
"name": "信息提取",
|
||
"examples": [
|
||
"提取文档中的医院数量",
|
||
"抽取所有机构的名称",
|
||
"找出表格中的数据"
|
||
],
|
||
"params": ["field_refs", "document_refs"]
|
||
},
|
||
{
|
||
"intent": "fill_table",
|
||
"name": "表格填写",
|
||
"examples": [
|
||
"填表",
|
||
"根据这些数据填写表格",
|
||
"帮我填到Excel里"
|
||
],
|
||
"params": ["template", "document_refs"]
|
||
},
|
||
{
|
||
"intent": "summarize",
|
||
"name": "摘要总结",
|
||
"examples": [
|
||
"总结一下这份文档",
|
||
"生成摘要",
|
||
"概括主要内容"
|
||
],
|
||
"params": ["document_refs"]
|
||
},
|
||
{
|
||
"intent": "question",
|
||
"name": "智能问答",
|
||
"examples": [
|
||
"这段话说的是什么?",
|
||
"有多少家医院?",
|
||
"解释一下这个概念"
|
||
],
|
||
"params": ["question", "focus"]
|
||
},
|
||
{
|
||
"intent": "search",
|
||
"name": "文档搜索",
|
||
"examples": [
|
||
"搜索相关内容",
|
||
"找找看有哪些机构",
|
||
"查询医院相关的数据"
|
||
],
|
||
"params": ["field_refs", "question"]
|
||
},
|
||
{
|
||
"intent": "compare",
|
||
"name": "对比分析",
|
||
"examples": [
|
||
"对比这两个文档",
|
||
"比较一下差异",
|
||
"找出不同点"
|
||
],
|
||
"params": ["document_refs"]
|
||
},
|
||
{
|
||
"intent": "edit",
|
||
"name": "文档编辑",
|
||
"examples": [
|
||
"润色这段文字",
|
||
"修改格式",
|
||
"添加注释"
|
||
],
|
||
"params": []
|
||
}
|
||
]
|
||
}
|