Files
GoClaw/server/tool-builder.ts
2026-03-20 17:34:20 -04:00

283 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tool Builder Agent — генерирует новые инструменты через LLM и добавляет их в реестр
*/
import { invokeLLM } from "./_core/llm";
import { getDb } from "./db";
import { toolDefinitions } from "../drizzle/schema";
import { eq } from "drizzle-orm";
import { TOOL_REGISTRY, type ToolDefinition } from "./tools";
export interface GenerateToolRequest {
name: string;
description: string;
category?: string;
exampleInput?: string;
exampleOutput?: string;
dangerous?: boolean;
}
export interface GeneratedToolData {
toolId: string;
name: string;
description: string;
category: string;
dangerous: boolean;
parameters: Record<string, { type: string; description: string; required?: boolean }>;
implementation: string;
warnings?: string[];
}
export interface GenerateToolResult {
success: boolean;
tool?: GeneratedToolData;
error?: string;
}
/**
* Генерирует новый инструмент через LLM на основе описания
*/
export async function generateTool(request: GenerateToolRequest): Promise<GenerateToolResult> {
const systemPrompt = `You are an expert JavaScript developer specializing in creating tool functions for AI agents.
Your task is to generate a complete, working JavaScript tool implementation.
Rules:
1. The tool must be a single async function named "execute" that accepts a params object
2. Use only built-in Node.js modules (fs, path, crypto, http, https, url) or global fetch API
3. Always handle errors gracefully and return meaningful error messages
4. The function must return a JSON-serializable result object
5. Add input validation for all required parameters
6. Keep the implementation concise but complete
Return ONLY valid JSON with this exact structure:
{
"toolId": "snake_case_id",
"name": "Human Readable Name",
"description": "What this tool does",
"category": "web|data|file|system|ai|custom",
"dangerous": false,
"parameters": {
"paramName": {
"type": "string|number|boolean|array|object",
"description": "What this parameter does",
"required": true
}
},
"implementation": "async function execute(params) { /* your code here */ return { result: 'value' }; }"
}`;
const userPrompt = `Create a tool with the following specification:
Name: ${request.name}
Description: ${request.description}
Category: ${request.category || "custom"}
${request.exampleInput ? `Example Input: ${request.exampleInput}` : ""}
${request.exampleOutput ? `Example Output: ${request.exampleOutput}` : ""}
${request.dangerous ? "Note: This tool may perform dangerous operations, add appropriate safety checks." : ""}
Generate a complete, production-ready implementation.`;
try {
const response = await invokeLLM({
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
response_format: {
type: "json_schema",
json_schema: {
name: "tool_definition",
strict: true,
schema: {
type: "object",
properties: {
toolId: { type: "string" },
name: { type: "string" },
description: { type: "string" },
category: { type: "string" },
dangerous: { type: "boolean" },
parameters: { type: "object", additionalProperties: true },
implementation: { type: "string" },
},
required: ["toolId", "name", "description", "category", "dangerous", "parameters", "implementation"],
additionalProperties: false,
},
},
},
});
const content = response.choices[0].message.content;
const toolData = typeof content === "string" ? JSON.parse(content) : content;
// Validate the implementation is safe
const warnings: string[] = [];
const dangerousPatterns = ["child_process", "exec(", "spawn(", "eval(", "new Function("];
for (const pattern of dangerousPatterns) {
if (toolData.implementation.includes(pattern) && !request.dangerous) {
warnings.push(`Warning: Implementation contains potentially dangerous pattern: ${pattern}`);
}
}
return {
success: true,
tool: { ...toolData, warnings },
};
} catch (error: any) {
return {
success: false,
error: `Failed to generate tool: ${error.message}`,
};
}
}
/**
* Сохраняет инструмент в БД и регистрирует в реестре
*/
export async function installTool(
toolData: GeneratedToolData,
createdBy?: number
): Promise<{ success: boolean; toolId?: string; error?: string }> {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
try {
// Save to DB
await db.insert(toolDefinitions).values({
toolId: toolData.toolId,
name: toolData.name,
description: toolData.description,
category: toolData.category,
dangerous: toolData.dangerous,
parameters: toolData.parameters,
implementation: toolData.implementation,
isActive: true,
createdBy: createdBy || null,
}).onDuplicateKeyUpdate({
set: {
name: toolData.name,
description: toolData.description,
implementation: toolData.implementation,
parameters: toolData.parameters,
updatedAt: new Date(),
},
});
// Register in runtime TOOL_REGISTRY
registerToolInRegistry(toolData);
return { success: true, toolId: toolData.toolId };
} catch (error: any) {
return { success: false, error: error.message };
}
}
/**
* Динамически регистрирует инструмент в TOOL_REGISTRY (массив)
*/
function registerToolInRegistry(toolData: GeneratedToolData): void {
try {
// Create the execute function from implementation string
const executeFunc = new Function("return " + toolData.implementation)() as (params: any) => Promise<any>;
const toolEntry: ToolDefinition = {
id: toolData.toolId,
name: toolData.name,
description: toolData.description,
category: toolData.category as ToolDefinition["category"],
dangerous: toolData.dangerous,
parameters: toolData.parameters,
execute: executeFunc,
};
// Remove existing entry if present, then add new one
const existingIdx = TOOL_REGISTRY.findIndex(t => t.id === toolData.toolId);
if (existingIdx >= 0) {
TOOL_REGISTRY.splice(existingIdx, 1, toolEntry);
} else {
TOOL_REGISTRY.push(toolEntry);
}
} catch (error: any) {
console.warn(`[ToolBuilder] Failed to register tool ${toolData.toolId} in runtime:`, error.message);
}
}
/**
* Загружает все кастомные инструменты из БД в реестр при старте
*/
export async function loadCustomToolsFromDb(): Promise<void> {
const db = await getDb();
if (!db) return;
try {
const tools = await db.select().from(toolDefinitions)
.where(eq(toolDefinitions.isActive, true));
for (const tool of tools) {
registerToolInRegistry({
toolId: tool.toolId,
name: tool.name,
description: tool.description,
category: tool.category,
dangerous: tool.dangerous || false,
parameters: (tool.parameters as any) || {},
implementation: tool.implementation,
});
}
console.log(`[ToolBuilder] Loaded ${tools.length} custom tools from DB`);
} catch (error: any) {
console.warn("[ToolBuilder] Failed to load custom tools:", error.message);
}
}
/**
* Получает все инструменты из БД
*/
export async function getCustomTools() {
const db = await getDb();
if (!db) return [];
return db.select().from(toolDefinitions);
}
/**
* Удаляет инструмент из БД и реестра
*/
export async function deleteTool(toolId: string): Promise<{ success: boolean; error?: string }> {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
try {
await db.delete(toolDefinitions).where(eq(toolDefinitions.toolId, toolId));
// Remove from TOOL_REGISTRY
const idx = TOOL_REGISTRY.findIndex(t => t.id === toolId);
if (idx >= 0) TOOL_REGISTRY.splice(idx, 1);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}
/**
* Тестирует инструмент с заданными параметрами
*/
export async function testTool(
toolId: string,
params: Record<string, any>
): Promise<{ success: boolean; result?: any; error?: string; executionTimeMs: number }> {
const start = Date.now();
const tool = TOOL_REGISTRY.find(t => t.id === toolId);
if (!tool) {
return { success: false, error: `Tool '${toolId}' not found in registry`, executionTimeMs: 0 };
}
if (!tool.execute) {
return { success: false, error: `Tool '${toolId}' has no execute function (built-in tools use executeToolImpl)`, executionTimeMs: 0 };
}
try {
const result = await tool.execute(params);
return { success: true, result, executionTimeMs: Date.now() - start };
} catch (error: any) {
return { success: false, error: error.message, executionTimeMs: Date.now() - start };
}
}