283 lines
9.1 KiB
TypeScript
283 lines
9.1 KiB
TypeScript
/**
|
||
* 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 };
|
||
}
|
||
}
|