feat: v3.0 Tool Calling — AI 本地文件操作系统
新增文件: - src/main/tool-security.ts: 路径白名单/黑名单、命令安全检查 - src/main/tool-handlers.ts: 7个工具实现(read/write/list/search/create/delete/run) - src/renderer/services/tool-registry.ts: 工具注册调度中心 - src/renderer/services/agent-engine.ts: Agent Loop 流式多轮工具调用引擎 - src/renderer/components/tool-confirm-modal.ts: 高风险操作确认对话框 修改文件: - types.d.ts: 新增 ToolCall/ToolResult/ToolCallRecord 等类型 - ollama.ts: chatStream 支持 tools 参数 - ipc.ts: 新增 tool:execute/getConfig/setAllowedDirs IPC - preload.ts: 暴露 tool API - chat-area.ts: 渲染工具调用卡片 - input-area.ts: 集成 Agent Loop 引擎 - settings-modal.ts: 工具调用开关设置 - index.html: 工具调用设置面板 + 确认对话框 HTML - style.css: 工具调用卡片、确认对话框样式 - main.ts: 初始化工具调用配置
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Agent Engine - Agent Loop 核心引擎
|
||||
* 管理带工具调用的流式对话循环
|
||||
*/
|
||||
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import {
|
||||
executeTool,
|
||||
getEnabledToolDefinitions,
|
||||
needsConfirmation
|
||||
} from './tool-registry.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
import type {
|
||||
OllamaMessage,
|
||||
OllamaStreamChunk,
|
||||
ToolCall,
|
||||
ToolResult,
|
||||
ToolCallRecord
|
||||
} from '../types.js';
|
||||
|
||||
export interface AgentCallbacks {
|
||||
onThinking: (text: string) => void;
|
||||
onContent: (text: string) => void;
|
||||
onToolCallStart: (call: ToolCall) => void;
|
||||
onToolCallResult: (name: string, result: ToolResult, call: ToolCall) => void;
|
||||
onToolCallError: (name: string, error: string, call: ToolCall) => void;
|
||||
onDone: (finalContent: string, toolRecords?: ToolCallRecord[]) => void;
|
||||
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export async function runAgentLoop(
|
||||
userContent: string,
|
||||
images: string[],
|
||||
historyMessages: Array<{ role: string; content: string; images?: string[] }>,
|
||||
callbacks: AgentCallbacks
|
||||
): Promise<void> {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = state.get<string>('_defaultModel', '');
|
||||
|
||||
if (!api || !model) {
|
||||
showToast('请先选择模型', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const messages: OllamaMessage[] = [];
|
||||
|
||||
if (state.get<boolean>(KEYS.SYSTEM_PROMPT_ENABLED)) {
|
||||
const systemPrompt = state.get<string>(KEYS.SYSTEM_PROMPT, '');
|
||||
if (systemPrompt) {
|
||||
messages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
}
|
||||
|
||||
for (const msg of historyMessages) {
|
||||
messages.push({
|
||||
role: msg.role as 'user' | 'assistant',
|
||||
content: msg.content,
|
||||
...(msg.images?.length && { images: msg.images })
|
||||
});
|
||||
}
|
||||
|
||||
const userMsg: OllamaMessage = { role: 'user', content: userContent };
|
||||
if (images?.length) userMsg.images = images;
|
||||
messages.push(userMsg);
|
||||
|
||||
const tools = getEnabledToolDefinitions();
|
||||
const useTools = tools.length > 0;
|
||||
|
||||
let loopCount = 0;
|
||||
const maxLoops = 10;
|
||||
const allToolRecords: ToolCallRecord[] = [];
|
||||
|
||||
while (loopCount < maxLoops) {
|
||||
loopCount++;
|
||||
|
||||
let thinking = '';
|
||||
let content = '';
|
||||
const toolCalls: ToolCall[] = [];
|
||||
|
||||
const abortController = new AbortController();
|
||||
state.set(KEYS.ABORT_CONTROLLER, abortController);
|
||||
|
||||
try {
|
||||
await api.chatStream(
|
||||
{
|
||||
model,
|
||||
messages,
|
||||
stream: true,
|
||||
think: state.get<boolean>('thinkEnabled', false),
|
||||
options: {
|
||||
num_ctx: state.get<number>(KEYS.NUM_CTX, 24576),
|
||||
temperature: state.get<number>('temperature', 0.7)
|
||||
},
|
||||
...(useTools && { tools })
|
||||
},
|
||||
(chunk: OllamaStreamChunk) => {
|
||||
if (chunk.message?.thinking) {
|
||||
thinking += chunk.message.thinking;
|
||||
callbacks.onThinking(thinking);
|
||||
}
|
||||
if (chunk.message?.content) {
|
||||
content += chunk.message.content;
|
||||
callbacks.onContent(content);
|
||||
}
|
||||
if (chunk.message?.tool_calls?.length) {
|
||||
for (const tc of chunk.message.tool_calls) {
|
||||
if (tc.function?.name) {
|
||||
toolCalls.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments || {}
|
||||
}
|
||||
});
|
||||
} else if (toolCalls.length > 0) {
|
||||
const last = toolCalls[toolCalls.length - 1];
|
||||
if (tc.function?.arguments && typeof tc.function.arguments === 'object') {
|
||||
Object.assign(last.function.arguments, tc.function.arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
abortController
|
||||
);
|
||||
} catch (err) {
|
||||
if (abortController.signal.aborted) {
|
||||
if (content || thinking) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
...(thinking && { thinking })
|
||||
});
|
||||
}
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const assistantMsg: OllamaMessage = {
|
||||
role: 'assistant',
|
||||
content,
|
||||
...(thinking && { thinking })
|
||||
};
|
||||
if (toolCalls.length > 0) {
|
||||
assistantMsg.tool_calls = toolCalls;
|
||||
}
|
||||
messages.push(assistantMsg);
|
||||
|
||||
if (toolCalls.length === 0) {
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const call of toolCalls) {
|
||||
callbacks.onToolCallStart(call);
|
||||
|
||||
if (needsConfirmation(call.function.name)) {
|
||||
const confirmed = await callbacks.onConfirmTool(call);
|
||||
if (!confirmed) {
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result: { success: false, error: '用户取消了操作' },
|
||||
status: 'cancelled',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify({ success: false, error: '用户取消了操作' })
|
||||
});
|
||||
callbacks.onToolCallError(call.function.name, '用户取消', call);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeTool(call.function.name, call.function.arguments);
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result,
|
||||
status: result.success ? 'success' : 'error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify(result)
|
||||
});
|
||||
callbacks.onToolCallResult(call.function.name, result, call);
|
||||
} catch (err) {
|
||||
const errorMsg = (err as Error).message;
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result: { success: false, error: errorMsg },
|
||||
status: 'error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify({ success: false, error: errorMsg })
|
||||
});
|
||||
callbacks.onToolCallError(call.function.name, errorMsg, call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords);
|
||||
}
|
||||
Reference in New Issue
Block a user