UNPKG

@vfarcic/dot-ai

Version:

Universal Kubernetes application deployment agent with CLI and MCP interfaces

347 lines (346 loc) 17.1 kB
"use strict"; /** * Recommend Tool - AI-powered Kubernetes resource recommendations */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.RECOMMEND_TOOL_INPUT_SCHEMA = exports.RECOMMEND_TOOL_DESCRIPTION = exports.RECOMMEND_TOOL_NAME = void 0; exports.handleRecommendTool = handleRecommendTool; const zod_1 = require("zod"); const error_handling_1 = require("../core/error-handling"); const schema_1 = require("../core/schema"); const claude_1 = require("../core/claude"); const cluster_utils_1 = require("../core/cluster-utils"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); const session_utils_1 = require("../core/session-utils"); // Tool metadata for direct MCP registration exports.RECOMMEND_TOOL_NAME = 'recommend'; exports.RECOMMEND_TOOL_DESCRIPTION = 'Deploy, create, setup, install, or run applications, infrastructure, and services on Kubernetes with AI recommendations. Describe what you want to deploy.'; // Zod schema for MCP registration exports.RECOMMEND_TOOL_INPUT_SCHEMA = { intent: zod_1.z.string().min(1).max(1000).describe('What the user wants to deploy, create, setup, install, or run on Kubernetes. Examples: "deploy web application", "create PostgreSQL database", "setup Redis cache", "install Prometheus monitoring", "configure Ingress controller", "provision storage volumes", "launch MongoDB operator", "run Node.js API", "setup CI/CD pipeline", "create load balancer", "install Grafana dashboard", "deploy React frontend"') }; /** * Validate intent meaningfulness using AI */ async function validateIntentWithAI(intent, claudeIntegration) { try { // Load prompt template const promptPath = path.join(__dirname, '..', '..', 'prompts', 'intent-validation.md'); const template = fs.readFileSync(promptPath, 'utf8'); // Replace template variables const validationPrompt = template.replace('{intent}', intent); // Send to Claude for validation const response = await claudeIntegration.sendMessage(validationPrompt); // Parse JSON response with robust error handling let jsonContent = response.content; // Try to find JSON object wrapped in code blocks const codeBlockMatch = response.content.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); if (codeBlockMatch) { jsonContent = codeBlockMatch[1]; } else { // Try to find JSON object that starts with { and find the matching closing } const startIndex = response.content.indexOf('{'); if (startIndex !== -1) { let braceCount = 0; let endIndex = startIndex; for (let i = startIndex; i < response.content.length; i++) { if (response.content[i] === '{') braceCount++; if (response.content[i] === '}') braceCount--; if (braceCount === 0) { endIndex = i; break; } } if (braceCount === 0) { jsonContent = response.content.substring(startIndex, endIndex + 1); } } } const validation = JSON.parse(jsonContent.trim()); // Validate response structure if (typeof validation.isSpecific !== 'boolean' || typeof validation.reason !== 'string' || !Array.isArray(validation.suggestions)) { throw new Error('AI response has invalid structure'); } // If intent is not specific enough, throw error with suggestions if (!validation.isSpecific) { const suggestions = validation.suggestions.length ? validation.suggestions.map((s) => `• ${s}`).join('\n') : '• Include specific technology (Node.js, PostgreSQL, React, etc.)\n• Describe the purpose or function\n• Add context about requirements'; throw new Error(`Intent needs more specificity: ${validation.reason}\n\n` + `Suggestions to improve your intent:\n${suggestions}\n\n` + `Original intent: "${intent}"`); } } catch (error) { // If it's our validation error, re-throw it if (error instanceof Error && error.message.includes('Intent needs more specificity')) { throw error; } // For other errors (AI service issues, JSON parsing, etc.), // continue without blocking the user - log the issue but don't fail console.warn('Intent validation failed, continuing with original intent:', error); return; } } /** * Generate unique solution ID with timestamp and random component */ function generateSolutionId() { const timestamp = new Date().toISOString().replace(/[:.]/g, '').split('T'); const dateTime = timestamp[0] + 'T' + timestamp[1].substring(0, 6); const randomHex = crypto.randomBytes(6).toString('hex'); return `sol_${dateTime}_${randomHex}`; } /** * Write solution data to file atomically (temp file + rename) */ function writeSolutionFile(sessionDir, solutionId, solutionData) { const fileName = `${solutionId}.json`; const filePath = path.join(sessionDir, fileName); const tempPath = filePath + '.tmp'; try { // Write to temporary file first fs.writeFileSync(tempPath, JSON.stringify(solutionData, null, 2)); // Atomically rename to final location fs.renameSync(tempPath, filePath); } catch (error) { // Clean up temp file if it exists try { if (fs.existsSync(tempPath)) { fs.unlinkSync(tempPath); } } catch (cleanupError) { // Ignore cleanup errors } throw new Error(`Failed to write solution file ${fileName}: ${error}`); } } /** * Direct MCP tool handler for recommend functionality */ async function handleRecommendTool(args, dotAI, logger, requestId) { return await error_handling_1.ErrorHandler.withErrorHandling(async () => { logger.debug('Handling recommend request', { requestId, intent: args?.intent }); // Input validation is handled automatically by MCP SDK with Zod schema // args are already validated and typed when we reach this point // Check for Claude API key const claudeApiKey = dotAI.getAnthropicApiKey(); if (!claudeApiKey) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.AI_SERVICE, error_handling_1.ErrorSeverity.HIGH, 'ANTHROPIC_API_KEY environment variable must be set for AI-powered resource recommendations', { operation: 'api_key_check', component: 'RecommendTool', requestId, suggestedActions: [ 'Set ANTHROPIC_API_KEY environment variable', 'Verify the API key is valid and active', 'Check that the API key has sufficient credits' ] }); } // Validate session directory configuration let sessionDir; try { sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, true); // requireWrite=true logger.debug('Session directory validated', { requestId, sessionDir }); } catch (error) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `Session directory validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { operation: 'session_directory_validation', component: 'RecommendTool', requestId, suggestedActions: [ 'Ensure session directory exists and is writable', 'Set --session-dir parameter or DOT_AI_SESSION_DIR environment variable', 'Check directory permissions' ] }, error instanceof Error ? error : new Error(String(error))); } // Ensure cluster connectivity before proceeding await (0, cluster_utils_1.ensureClusterConnection)(dotAI, logger, requestId, 'RecommendTool'); logger.info('Starting resource recommendation process', { requestId, intent: args.intent, hasApiKey: !!claudeApiKey }); // Validate intent specificity with AI before expensive resource discovery logger.debug('Validating intent specificity', { requestId, intent: args.intent }); try { const claudeIntegration = new claude_1.ClaudeIntegration(claudeApiKey); await validateIntentWithAI(args.intent, claudeIntegration); logger.debug('Intent validation passed', { requestId }); } catch (error) { if (error instanceof Error && error.message.includes('Intent needs more specificity')) { // This is a validation error that should be returned to the user throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, error.message, { operation: 'intent_validation', component: 'RecommendTool', requestId, input: { intent: args.intent }, suggestedActions: [ 'Provide more specific details about your deployment', 'Include technology stack information', 'Describe the purpose or function of what you want to deploy' ] }, error); } // For other errors, log but continue (don't block user due to AI service issues) logger.warn('Intent validation failed, continuing with recommendation', { requestId, error: error instanceof Error ? error.message : 'Unknown error' }); } // Initialize AI-powered ResourceRecommender const rankingConfig = { claudeApiKey }; const recommender = new schema_1.ResourceRecommender(rankingConfig); // Create discovery functions const discoverResourcesFn = async () => { logger.debug('Discovering cluster resources', { requestId }); return await dotAI.discovery.discoverResources(); }; const explainResourceFn = async (resource) => { logger.debug(`Explaining resource: ${resource}`, { requestId }); return await dotAI.discovery.explainResource(resource); }; // Find best solutions for the user intent logger.debug('Generating recommendations with AI', { requestId }); const solutions = await recommender.findBestSolutions(args.intent, discoverResourcesFn, explainResourceFn); logger.info('Recommendation process completed', { requestId, solutionCount: solutions.length, topScore: solutions[0]?.score }); // Create solution files and build response const solutionSummaries = []; const timestamp = new Date().toISOString(); // Limit to top 5 solutions (respecting quality thresholds from AI ranking) const topSolutions = solutions.slice(0, 5); for (const solution of topSolutions) { const solutionId = generateSolutionId(); // Create complete solution file with all data const solutionFileData = { solutionId, intent: args.intent, type: solution.type, score: solution.score, description: solution.description, reasons: solution.reasons, analysis: solution.analysis, resources: solution.resources.map(r => ({ kind: r.kind, apiVersion: r.apiVersion, group: r.group, description: r.description })), questions: solution.questions, answers: {}, // Empty initially - will be filled by answerQuestion tool timestamp }; // Write solution to file try { writeSolutionFile(sessionDir, solutionId, solutionFileData); logger.debug('Solution file created', { requestId, solutionId, fileName: `${solutionId}.json` }); } catch (error) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.STORAGE, error_handling_1.ErrorSeverity.HIGH, `Failed to store solution file: ${error instanceof Error ? error.message : 'Unknown error'}`, { operation: 'solution_file_creation', component: 'RecommendTool', requestId, input: { solutionId }, suggestedActions: [ 'Check session directory write permissions', 'Ensure sufficient disk space', 'Verify session directory is accessible' ] }, error instanceof Error ? error : new Error(String(error))); } // Add to response summary (decision-making data only) solutionSummaries.push({ solutionId, type: solution.type, score: solution.score, description: solution.description, primaryResources: solution.resources.slice(0, 3).map(r => r.kind), reasons: solution.reasons, analysis: solution.analysis, usedPatterns: solution.usedPatterns || false, patternInfluences: solution.patternInfluences || [] }); } // Analyze pattern usage across all solutions const patternsUsedCount = solutionSummaries.filter(s => s.usedPatterns).length; const totalPatternInfluences = solutionSummaries.reduce((count, s) => count + (s.patternInfluences?.length || 0), 0); // Build new response format const response = { intent: args.intent, solutions: solutionSummaries, patternSummary: { solutionsUsingPatterns: patternsUsedCount, totalSolutions: solutionSummaries.length, totalPatternInfluences: totalPatternInfluences, patternsAvailable: totalPatternInfluences > 0 ? "Yes" : "None found or pattern search failed" }, nextAction: "Call chooseSolution with your preferred solutionId", guidance: "🔴 CRITICAL: You MUST present these solutions to the user and ask them to choose. DO NOT automatically call chooseSolution() without user input. Stop here and wait for user selection. ALSO: Include pattern usage information in your response - show which solutions used organizational patterns and which did not.", timestamp }; logger.info('Solution files created and response prepared', { requestId, solutionCount: solutionSummaries.length, sessionDir }); return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; }, { operation: 'recommend_tool', component: 'RecommendTool', requestId, input: args }, { convertToMcp: true, retryCount: 1 }); }