UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

270 lines (269 loc) 13.2 kB
"use strict"; /** * Query Tool - Natural Language Cluster Intelligence * * Provides natural language query interface to discover and understand * cluster capabilities and resources. * * PRD #291: Cluster Query Tool - Natural Language Cluster Intelligence */ Object.defineProperty(exports, "__esModule", { value: true }); exports.QUERY_TOOL_INPUT_SCHEMA = exports.QUERY_TOOL_DESCRIPTION = exports.QUERY_TOOL_NAME = void 0; exports.handleQueryTool = handleQueryTool; const zod_1 = require("zod"); const error_handling_1 = require("../core/error-handling"); const ai_provider_factory_1 = require("../core/ai-provider-factory"); const capability_tools_1 = require("../core/capability-tools"); const resource_tools_1 = require("../core/resource-tools"); const mcp_client_registry_1 = require("../core/mcp-client-registry"); const generic_session_manager_1 = require("../core/generic-session-manager"); const visualization_1 = require("../core/visualization"); const mermaid_tools_1 = require("../core/mermaid-tools"); const shared_prompt_loader_1 = require("../core/shared-prompt-loader"); // Tool metadata for MCP registration exports.QUERY_TOOL_NAME = 'query'; exports.QUERY_TOOL_DESCRIPTION = 'Natural language query interface for Kubernetes cluster intelligence. Ask any questions about your cluster resources, capabilities, and status in plain English. Examples: "What databases are running?", "Describe the nginx deployment", "Show me pods in the kube-system namespace", "What operators are installed?", "Is my-postgres healthy?"'; // Zod schema for MCP registration exports.QUERY_TOOL_INPUT_SCHEMA = { intent: zod_1.z.string().min(1).max(1000).describe('Natural language query about the cluster'), interaction_id: zod_1.z.string().optional().describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.') }; /** * Parse the AI's final JSON response for summary only */ function parseSummary(aiResponse) { try { // Find JSON in the response const firstBraceIndex = aiResponse.indexOf('{'); if (firstBraceIndex === -1) { // No JSON found, use the response as summary return aiResponse.trim() || 'No summary provided'; } // Track brace depth to find complete JSON object let braceCount = 0; let inString = false; let escapeNext = false; let jsonEndIndex = -1; for (let i = firstBraceIndex; i < aiResponse.length; i++) { const char = aiResponse[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } if (inString) continue; if (char === '{') braceCount++; if (char === '}') { braceCount--; if (braceCount === 0) { jsonEndIndex = i + 1; break; } } } if (jsonEndIndex === -1) { return aiResponse.trim() || 'No summary provided'; } const jsonString = aiResponse.substring(firstBraceIndex, jsonEndIndex); const parsed = JSON.parse(jsonString); return parsed.summary || 'No summary provided'; } catch { // If parsing fails, use the raw response as summary return aiResponse.trim() || 'No summary provided'; } } async function handleQueryTool(args, pluginManager) { const requestId = `query_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; const logger = new error_handling_1.ConsoleLogger('QueryTool'); try { // Validate input let intent = args.intent; if (!intent || typeof intent !== 'string') { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, 'Intent is required and must be a string', { operation: 'input_validation', component: 'QueryTool' }); } // Detect visualization mode and strip prefix const visualizationMode = intent.startsWith(visualization_1.VISUALIZATION_PREFIX); if (visualizationMode) { intent = intent.slice(visualization_1.VISUALIZATION_PREFIX.length).trim(); } logger.info('Processing query', { requestId, intent, visualizationMode }); // Initialize AI provider const aiProvider = (0, ai_provider_factory_1.createAIProvider)(); // Load system prompt with appropriate output instructions const systemPrompt = (0, shared_prompt_loader_1.loadPrompt)('query-system', { outputInstructions: visualizationMode ? (0, shared_prompt_loader_1.loadPrompt)('partials/visualization-output') : (0, shared_prompt_loader_1.loadPrompt)('partials/query-simple-output') }); // Local executor for non-plugin tools (capability, resource, mermaid) const localToolExecutor = async (toolName, input) => { if (toolName.startsWith('search_capabilities') || toolName.startsWith('query_capabilities')) { return (0, capability_tools_1.executeCapabilityTools)(toolName, input); } if (toolName.startsWith('search_resources') || toolName.startsWith('query_resources')) { return (0, resource_tools_1.executeResourceTools)(toolName, input); } if (toolName === 'validate_mermaid') { return (0, mermaid_tools_1.executeMermaidTools)(toolName, input); } return { success: false, error: `Unknown tool: ${toolName}`, message: `Tool '${toolName}' is not implemented in query tool` }; }; // PRD #343: Use plugin executor when pluginManager is available // kubectl tools route through plugin HTTP, others use local executor // PRD #358: Chain MCP executor with plugin executor as fallback const pluginExecutor = pluginManager ? pluginManager.createToolExecutor(localToolExecutor) : localToolExecutor; const executeQueryTools = (0, mcp_client_registry_1.isMcpClientInitialized)() ? (0, mcp_client_registry_1.getMcpClientManager)().createToolExecutor(pluginExecutor) : pluginExecutor; // PRD #343: Get kubectl tools from plugin (read-only tools for query) // Only include kubectl tools when plugin provides them const KUBECTL_READONLY_TOOL_NAMES = [ 'kubectl_api_resources', 'kubectl_get', 'kubectl_describe', 'kubectl_logs', 'kubectl_events', 'kubectl_get_crd_schema' ]; const pluginKubectlTools = pluginManager ? pluginManager.getDiscoveredTools().filter(t => KUBECTL_READONLY_TOOL_NAMES.includes(t.name)) : []; // PRD #358: Get MCP server tools attached to query const mcpTools = (0, mcp_client_registry_1.isMcpClientInitialized)() ? (0, mcp_client_registry_1.getMcpClientManager)().getToolsForOperation('query') : []; // Build tool list - add mermaid tools when in visualization mode // kubectl tools only available when plugin is configured // MCP tools added when MCP servers are configured const tools = visualizationMode ? [...capability_tools_1.CAPABILITY_TOOLS, ...resource_tools_1.RESOURCE_TOOLS, ...pluginKubectlTools, ...mcpTools, ...mermaid_tools_1.MERMAID_TOOLS] : [...capability_tools_1.CAPABILITY_TOOLS, ...resource_tools_1.RESOURCE_TOOLS, ...pluginKubectlTools, ...mcpTools]; // Execute tool loop with capability, resource, kubectl, and MCP tools const result = await aiProvider.toolLoop({ systemPrompt, userMessage: intent, tools, toolExecutor: executeQueryTools, maxIterations: 30, operation: 'query', evaluationContext: { user_intent: intent }, interaction_id: args.interaction_id }); // Extract data from execution record (reliable, not AI self-reporting) const toolsUsed = [...new Set(result.toolCallsExecuted.map(tc => tc.tool))]; logger.info('Query completed', { requestId, iterations: result.iterations, toolsUsed, visualizationMode }); // Guard: if the AI call did not succeed, surface the real error instead of trying to parse if (result.status && result.status !== 'success') { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.AI_SERVICE, error_handling_1.ErrorSeverity.HIGH, `Query ${result.status}: ${result.finalMessage}`, { operation: 'query_tool_execution', component: 'QueryTool', isRetryable: true, requestId, input: { intent } }); } // Handle visualization mode - return visualization response with sessionId for caching if (visualizationMode) { const visualizationResponse = (0, visualization_1.parseVisualizationResponse)(result.finalMessage, toolsUsed); // Create session with cached visualization for URL caching/bookmarking (PRD #328) const sessionManager = new generic_session_manager_1.GenericSessionManager('qry'); const session = sessionManager.createSession({ toolName: 'query', intent, summary: visualizationResponse.title, // Use title as summary for visualization sessions toolsUsed, iterations: result.iterations, toolCallsExecuted: result.toolCallsExecuted, cachedVisualization: { title: visualizationResponse.title, visualizations: visualizationResponse.visualizations, insights: visualizationResponse.insights, generatedAt: new Date().toISOString() } }); logger.info('Visualization session created', { requestId, sessionId: session.sessionId }); return { content: [ { type: 'text', text: JSON.stringify({ sessionId: session.sessionId, ...visualizationResponse }, null, 2) } ] }; } // Normal mode - return summary with session for later visualization const summary = parseSummary(result.finalMessage); // Store session for visualization (PRD #317, PRD #320) const sessionManager = new generic_session_manager_1.GenericSessionManager('qry'); const session = sessionManager.createSession({ toolName: 'query', // PRD #320: Tool identifier for visualization endpoint intent, summary, toolsUsed, iterations: result.iterations, toolCallsExecuted: result.toolCallsExecuted }); // PRD #317: Include visualization URL when WEB_UI_BASE_URL is configured const visualizationUrl = (0, visualization_1.getVisualizationUrl)(session.sessionId); logger.info('Session created for visualization', { requestId, sessionId: session.sessionId, ...(visualizationUrl && { visualizationUrl }) }); const agentInstructions = visualizationUrl ? 'Present the summary to the user. Include the visualizationUrl at the end of your response. For dependency or impact questions, suggest the user run impact_analysis to get detailed blast radius analysis.' : 'Present the summary to the user. For dependency or impact questions, suggest the user run impact_analysis to get detailed blast radius analysis.'; const output = { success: true, summary, toolsUsed, iterations: result.iterations, sessionId: session.sessionId, ...(visualizationUrl && { visualizationUrl }), agentInstructions }; return { content: [ { type: 'text', text: JSON.stringify(output, null, 2) } ] }; } catch (error) { logger.error('Query failed', error, { requestId }); if (error instanceof Error && 'category' in error) { throw error; } throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.UNKNOWN, error_handling_1.ErrorSeverity.HIGH, `Query tool failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { operation: 'query_tool_execution', component: 'QueryTool', requestId, input: { intent: args.intent } }); } }