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

@@ -1459,4 +1459,131 @@ export const aiApi = {
throw error;
}
},
// ==================== 智能指令 ====================
/**
* 识别自然语言指令的意图
*/
async recognizeIntent(
instruction: string,
docIds?: string[]
): Promise<{
success: boolean;
intent: string;
params: Record<string, any>;
message: string;
}> {
const url = `${BACKEND_BASE_URL}/instruction/recognize`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instruction, doc_ids: docIds }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || '意图识别失败');
}
return await response.json();
} catch (error) {
console.error('意图识别失败:', error);
throw error;
}
},
/**
* 执行自然语言指令
*/
async executeInstruction(
instruction: string,
docIds?: string[],
context?: Record<string, any>
): Promise<{
success: boolean;
intent: string;
result: Record<string, any>;
message: string;
}> {
const url = `${BACKEND_BASE_URL}/instruction/execute`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instruction, doc_ids: docIds, context }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || '指令执行失败');
}
return await response.json();
} catch (error) {
console.error('指令执行失败:', error);
throw error;
}
},
/**
* 智能对话(支持多轮对话的指令执行)
*/
async instructionChat(
instruction: string,
docIds?: string[],
context?: Record<string, any>
): Promise<{
success: boolean;
intent: string;
result: Record<string, any>;
message: string;
hint?: string;
}> {
const url = `${BACKEND_BASE_URL}/instruction/chat`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instruction, doc_ids: docIds, context }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || '对话处理失败');
}
return await response.json();
} catch (error) {
console.error('对话处理失败:', error);
throw error;
}
},
/**
* 获取支持的指令类型列表
*/
async getSupportedIntents(): Promise<{
intents: Array<{
intent: string;
name: string;
examples: string[];
params: string[];
}>;
}> {
const url = `${BACKEND_BASE_URL}/instruction/intents`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('获取指令列表失败');
return await response.json();
} catch (error) {
console.error('获取指令列表失败:', error);
throw error;
}
},
};

View File

@@ -10,7 +10,11 @@ import {
TableProperties,
ChevronRight,
ArrowRight,
Loader2
Loader2,
Download,
Search,
MessageSquare,
CheckCircle
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -26,12 +30,15 @@ type ChatMessage = {
role: 'user' | 'assistant';
content: string;
created_at: string;
intent?: string;
result?: any;
};
const InstructionChat: React.FC = () => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [currentDocIds, setCurrentDocIds] = useState<string[]>([]);
const scrollAreaRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -43,27 +50,47 @@ const InstructionChat: React.FC = () => {
role: 'assistant',
content: `您好!我是智联文档 AI 助手。
我可以帮您完成以下操作:
**📄 文档智能操作**
- "提取文档中的医院数量和床位数"
- "帮我找出所有机构的名称"
📄 **文档管理**
- "帮我列出最近上传的所有文档"
- "删除三天前的 docx 文档"
**📊 数据填表**
- "根据这些数据填表"
- "将提取的信息填写到Excel模板"
📊 **Excel 分析**
- "分析一下最近上传的 Excel 文件"
- "帮我统计销售报表中的数据"
**📝 内容处理**
- "总结一下这份文档"
- "对比这两个文档的差异"
📝 **智能填表**
- "根据员工信息表创建一个考勤汇总表"
- "用财务文档填充报销模板"
**🔍 智能问答**
- "文档里说了些什么?"
- "有多少家医院?"
请告诉我您想做什么?`,
created_at: new Date().toISOString()
}
]);
// 获取已上传的文档ID列表
loadDocuments();
}
}, []);
const loadDocuments = async () => {
try {
const result = await backendApi.getDocuments(undefined, 50);
if (result.success && result.documents) {
const docIds = result.documents.map((d: any) => d.doc_id);
setCurrentDocIds(docIds);
if (docIds.length > 0) {
console.log(`已加载 ${docIds.length} 个文档`);
}
}
} catch (err) {
console.error('获取文档列表失败:', err);
}
};
useEffect(() => {
// Scroll to bottom
if (scrollAreaRef.current) {
@@ -89,95 +116,126 @@ const InstructionChat: React.FC = () => {
setLoading(true);
try {
// TODO: 后端对话接口,暂用模拟响应
await new Promise(resolve => setTimeout(resolve, 1500));
// 使用真实的智能指令 API
const response = await backendApi.instructionChat(
input.trim(),
currentDocIds.length > 0 ? currentDocIds : undefined
);
// 简单的命令解析演示
const userInput = userMessage.content.toLowerCase();
let response = '';
// 根据意图类型生成友好响应
let responseContent = '';
const resultData = response.result;
if (userInput.includes('列出') || userInput.includes('列表')) {
const result = await backendApi.getDocuments(undefined, 10);
if (result.success && result.documents && result.documents.length > 0) {
response = `已为您找到 ${result.documents.length} 个文档:\n\n`;
result.documents.slice(0, 5).forEach((doc: any, idx: number) => {
response += `${idx + 1}. **${doc.original_filename}** (${doc.doc_type.toUpperCase()})\n`;
response += ` - 大小: ${(doc.file_size / 1024).toFixed(1)} KB\n`;
response += ` - 时间: ${new Date(doc.created_at).toLocaleDateString()}\n\n`;
});
if (result.documents.length > 5) {
response += `...还有 ${result.documents.length - 5} 个文档`;
switch (response.intent) {
case 'extract':
// 信息提取结果
const extracted = resultData?.extracted_data || {};
const keys = Object.keys(extracted);
if (keys.length > 0) {
responseContent = `✅ 已提取到 ${keys.length} 个字段的数据:\n\n`;
for (const [key, value] of Object.entries(extracted)) {
const values = Array.isArray(value) ? value : [value];
responseContent += `**${key}**: ${values.slice(0, 3).join(', ')}${values.length > 3 ? '...' : ''}\n`;
}
responseContent += `\n💡 您可以将这些数据填入表格。`;
} else {
responseContent = '未能从文档中提取到相关数据。请尝试更明确的字段名称。';
}
} else {
response = '暂未找到已上传的文档,您可以先上传一些文档试试。';
}
} else if (userInput.includes('分析') || userInput.includes('excel') || userInput.includes('报表')) {
response = `好的,我可以帮您分析 Excel 文件。
break;
请告诉我:
1. 您想分析哪个 Excel 文件?
2. 需要什么样的分析?(数据摘要/统计分析/图表生成)
case 'fill_table':
// 填表结果
const filled = resultData?.result?.filled_data || {};
const filledKeys = Object.keys(filled);
if (filledKeys.length > 0) {
responseContent = `✅ 填表完成!成功填写 ${filledKeys.length} 个字段:\n\n`;
for (const [key, value] of Object.entries(filled)) {
const values = Array.isArray(value) ? value : [value];
responseContent += `**${key}**: ${values.slice(0, 3).join(', ')}\n`;
}
responseContent += `\n📋 请到【智能填表】页面查看或导出结果。`;
} else {
responseContent = '填表未能提取到数据。请检查模板表头和数据源内容。';
}
break;
或者您可以直接告诉我您想从数据中了解什么,我来为您生成分析。`;
} else if (userInput.includes('填表') || userInput.includes('模板')) {
response = `好的,要进行智能填表,我需要:
case 'summarize':
// 摘要结果
const summaries = resultData?.summaries || [];
if (summaries.length > 0) {
responseContent = `📄 找到 ${summaries.length} 个文档的摘要:\n\n`;
summaries.forEach((s: any, idx: number) => {
responseContent += `**${idx + 1}. ${s.filename}**\n${s.content_preview}\n\n`;
});
} else {
responseContent = '未能生成摘要。请确保已上传文档。';
}
break;
1. **上传表格模板** - 您要填写的表格模板文件Excel 或 Word 格式)
2. **选择数据源** - 包含要填写内容的源文档
case 'question':
// 问答结果
if (resultData?.answer) {
responseContent = `**问题**: ${resultData.question}\n\n**答案**: ${resultData.answer}`;
} else {
responseContent = resultData?.message || '我找到了相关信息,请查看上文。';
}
break;
您可以去【智能填表】页面完成这些操作,或者告诉我您具体想填什么类型的表格,我来指导您操作。`;
} else if (userInput.includes('删除')) {
response = `要删除文档,请告诉我:
case 'search':
// 搜索结果
const searchResults = resultData?.results || [];
if (searchResults.length > 0) {
responseContent = `🔍 找到 ${searchResults.length} 条相关内容:\n\n`;
searchResults.slice(0, 5).forEach((r: any, idx: number) => {
responseContent += `**${idx + 1}.** ${r.content?.substring(0, 100)}...\n\n`;
});
} else {
responseContent = '未找到相关内容。请尝试其他关键词。';
}
break;
- 要删除的文件名是什么?
- 或者您可以到【文档中心】页面手动选择并删除文档
case 'compare':
// 对比结果
const comparison = resultData?.comparison || [];
if (comparison.length > 0) {
responseContent = `📊 对比了 ${comparison.length} 个文档:\n\n`;
comparison.forEach((c: any) => {
responseContent += `- **${c.filename}**: ${c.doc_type}, ${c.content_length}\n`;
});
} else {
responseContent = '需要至少2个文档才能进行对比。';
}
break;
⚠️ 删除操作不可恢复,请确认后再操作。`;
} else if (userInput.includes('帮助') || userInput.includes('help')) {
response = `**我可以帮您完成以下操作:**
case 'unknown':
responseContent = `我理解您想要: "${input.trim()}"\n\n但我目前无法完成此操作。您可以尝试\n\n1. **提取数据**: "提取医院数量和床位数"\n2. **填表**: "根据这些数据填表"\n3. **总结**: "总结这份文档"\n4. **问答**: "文档里说了什么?"\n5. **搜索**: "搜索相关内容"`;
break;
📄 **文档管理**
- 列出/搜索已上传的文档
- 查看文档详情和元数据
- 删除不需要的文档
📊 **Excel 处理**
- 分析 Excel 文件内容
- 生成数据统计和图表
- 导出处理后的数据
📝 **智能填表**
- 上传表格模板
- 从文档中提取信息填入模板
- 导出填写完成的表格
📋 **任务历史**
- 查看历史处理任务
- 重新执行或导出结果
请直接告诉我您想做什么!`;
} else {
response = `我理解您想要: "${input.trim()}"
目前我还在学习如何更好地理解您的需求。您可以尝试:
1. **上传文档** - 去【文档中心】上传 docx/md/txt 文件
2. **分析 Excel** - 去【Excel解析】上传并分析 Excel 文件
3. **智能填表** - 去【智能填表】创建填表任务
或者您可以更具体地描述您想做的事情,我会尽力帮助您!`;
default:
responseContent = response.message || resultData?.message || '已完成您的请求。';
}
const assistantMessage: ChatMessage = {
id: Math.random().toString(36).substring(7),
role: 'assistant',
content: response,
created_at: new Date().toISOString()
content: responseContent,
created_at: new Date().toISOString(),
intent: response.intent,
result: resultData
};
setMessages(prev => [...prev, assistantMessage]);
} catch (err: any) {
toast.error('请求失败,请重试');
console.error('指令执行失败:', err);
toast.error(err.message || '请求失败,请重试');
const errorMessage: ChatMessage = {
id: Math.random().toString(36).substring(7),
role: 'assistant',
content: `抱歉,处理您的请求时遇到了问题:${err.message}\n\n请稍后重试或尝试更简单的指令。`,
created_at: new Date().toISOString()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setLoading(false);
}
@@ -189,10 +247,10 @@ const InstructionChat: React.FC = () => {
};
const quickActions = [
{ label: '列出所有文档', icon: FileText, action: () => setInput('列出所有已上传的文档') },
{ label: '分析 Excel 数据', icon: TableProperties, action: () => setInput('分析一下 Excel 文件') },
{ label: '智能填表', icon: Sparkles, action: () => setInput('我想进行智能填表') },
{ label: '帮助', icon: Sparkles, action: () => setInput('帮助') }
{ label: '提取医院数量', icon: Search, action: () => setInput('提取文档中的医院数量和床位数') },
{ label: '智能填表', icon: TableProperties, action: () => setInput('根据这些数据填表') },
{ label: '总结文档', icon: MessageSquare, action: () => setInput('总结一下这份文档') },
{ label: '智能问答', icon: Bot, action: () => setInput('文档里说了些什么?') }
];
return (