UNPKG

@vfarcic/dot-ai

Version:

Universal Kubernetes application deployment agent with CLI and MCP interfaces

836 lines (834 loc) 38.6 kB
"use strict"; /** * Resource Schema Parser and Validator * * Implements comprehensive schema parsing and validation for Kubernetes resources * Fetches structured OpenAPI schemas from Kubernetes API server and validates manifests */ 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.ResourceRecommender = exports.ManifestValidator = exports.SchemaParser = void 0; const kubernetes_utils_1 = require("./kubernetes-utils"); const claude_1 = require("./claude"); const pattern_vector_service_1 = require("./pattern-vector-service"); const vector_db_service_1 = require("./vector-db-service"); /** * SchemaParser converts kubectl explain output to structured ResourceSchema */ class SchemaParser { /** * Parse ResourceExplanation from discovery engine into structured schema */ parseResourceExplanation(explanation) { const apiVersion = explanation.group ? `${explanation.group}/${explanation.version}` : explanation.version; const properties = new Map(); const required = []; // Process all fields from the explanation for (const field of explanation.fields) { const parts = field.name.split('.'); const topLevelField = parts[0]; // Add to required if marked as required if (field.required && !required.includes(topLevelField)) { required.push(topLevelField); } // Create or get the top-level field if (!properties.has(topLevelField)) { properties.set(topLevelField, { name: topLevelField, type: this.normalizeType(field.type), description: field.description, required: field.required, constraints: this.parseFieldConstraints(field.type, field.description), nested: new Map() }); } // Handle nested fields if (parts.length > 1) { this.addNestedField(properties.get(topLevelField), parts.slice(1), field); } } return { apiVersion, kind: explanation.kind, group: explanation.group, version: explanation.version, description: explanation.description, properties, required, namespace: true // Default to namespaced, could be enhanced with discovery data }; } /** * Add nested field to the schema structure */ addNestedField(parentField, fieldParts, field) { const currentPart = fieldParts[0]; if (!parentField.nested.has(currentPart)) { parentField.nested.set(currentPart, { name: `${parentField.name}.${currentPart}`, type: this.normalizeType(field.type), description: field.description, required: field.required, constraints: this.parseFieldConstraints(field.type, field.description), nested: new Map() }); } // Continue recursively if there are more field parts if (fieldParts.length > 1) { this.addNestedField(parentField.nested.get(currentPart), fieldParts.slice(1), field); } } /** * Normalize field types from kubectl explain output */ normalizeType(type) { const lowerType = type.toLowerCase(); // Map kubectl types to standard types const typeMap = { 'object': 'object', 'string': 'string', 'integer': 'integer', 'int32': 'integer', 'int64': 'integer', 'boolean': 'boolean', 'array': 'array', '[]string': 'array', '[]object': 'array', 'map[string]string': 'object', 'map[string]object': 'object' }; return typeMap[lowerType] || 'string'; } /** * Parse field constraints from description text */ parseFieldConstraints(type, description) { const constraints = {}; // Extract minimum/maximum values const minMatch = description.match(/(?:minimum|min):\s*(\d+)/i); if (minMatch) { constraints.minimum = parseInt(minMatch[1]); } const maxMatch = description.match(/(?:maximum|max):\s*(\d+)/i); if (maxMatch) { constraints.maximum = parseInt(maxMatch[1]); } // Extract enum values - Fixed: Avoid catastrophic backtracking const enumMatch = description.match(/(possible values|valid values|values)\s*(?:are)?:\s*([^.]+)/i); if (enumMatch) { const values = enumMatch[2] .split(/,|\s+and\s+/) .map(v => v.trim()) .filter(v => v.length > 0); constraints.enum = values; } // Extract default values - Fixed: Use simpler non-catastrophic patterns let defaultMatch = description.match(/\(default:\s*([^)]+)\)/i); if (!defaultMatch) { defaultMatch = description.match(/defaults?\s+to\s+(\w+)/i); } if (!defaultMatch) { defaultMatch = description.match(/\.\s+default:\s*(\w+)/i); } if (defaultMatch) { const defaultValue = defaultMatch[1].trim(); if (type === 'integer') { const parsed = parseInt(defaultValue); if (!isNaN(parsed)) { constraints.default = parsed; } } else { constraints.default = defaultValue; } } // Extract string length constraints const minLengthMatch = description.match(/min length:\s*(\d+)/i); if (minLengthMatch) { constraints.minLength = parseInt(minLengthMatch[1]); } const maxLengthMatch = description.match(/max length:\s*(\d+)/i); if (maxLengthMatch) { constraints.maxLength = parseInt(maxLengthMatch[1]); } return constraints; } } exports.SchemaParser = SchemaParser; /** * ManifestValidator validates Kubernetes manifests using kubectl dry-run */ class ManifestValidator { /** * Validate a manifest using kubectl dry-run * This uses the actual Kubernetes API server validation for accuracy */ async validateManifest(manifestPath, config) { const errors = []; const warnings = []; try { const dryRunMode = config?.dryRunMode || 'server'; const args = ['apply', '--dry-run=' + dryRunMode, '-f', manifestPath]; await (0, kubernetes_utils_1.executeKubectl)(args, { kubeconfig: config?.kubeconfig }); // If we get here, validation passed // kubectl dry-run will throw an error if validation fails // Add best practice warnings by reading the manifest const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const yaml = await Promise.resolve().then(() => __importStar(require('yaml'))); const manifestContent = yaml.parse(fs.readFileSync(manifestPath, 'utf8')); this.addBestPracticeWarnings(manifestContent, warnings); return { valid: true, errors, warnings }; } catch (error) { // Parse kubectl error output for validation issues const errorMessage = error.message || ''; if (errorMessage.includes('validation failed')) { errors.push('Kubernetes validation failed: ' + errorMessage); } else if (errorMessage.includes('unknown field')) { errors.push('Unknown field in manifest: ' + errorMessage); } else if (errorMessage.includes('required field')) { errors.push('Missing required field: ' + errorMessage); } else { errors.push('Validation error: ' + errorMessage); } return { valid: false, errors, warnings }; } } /** * Add best practice warnings */ addBestPracticeWarnings(manifest, warnings) { // Check for missing labels if (!manifest.metadata?.labels) { warnings.push('Consider adding labels to metadata for better resource organization'); } // Check for missing namespace in namespaced resources if (!manifest.metadata?.namespace && manifest.kind !== 'Namespace') { warnings.push('Consider specifying a namespace for better resource isolation'); } } } exports.ManifestValidator = ManifestValidator; /** * ResourceRecommender determines which resources best meet user needs using AI */ class ResourceRecommender { claudeIntegration; config; patternService; constructor(config) { this.config = config; this.claudeIntegration = new claude_1.ClaudeIntegration(config.claudeApiKey); // Initialize pattern service only if Vector DB is available try { const vectorDB = new vector_db_service_1.VectorDBService({ collectionName: 'patterns' }); this.patternService = new pattern_vector_service_1.PatternVectorService(vectorDB); console.log('✅ Pattern service initialized with Vector DB'); } catch (error) { console.warn('⚠️ Vector DB not available, patterns disabled:', error); this.patternService = undefined; } } /** * Find the best resource solution(s) for user intent using two-phase analysis */ async findBestSolutions(intent, discoverResources, explainResource) { if (!this.claudeIntegration.isInitialized()) { throw new Error('Claude integration not initialized. API key required for AI-powered resource ranking.'); } try { // Phase 0: Search for relevant organizational patterns const relevantPatterns = await this.searchRelevantPatterns(intent); // Phase 1: Get lightweight resource list and let AI select candidates const resourceMap = await discoverResources(); const allResources = [...resourceMap.resources, ...resourceMap.custom]; const candidates = await this.selectResourceCandidates(intent, allResources, relevantPatterns); // Phase 2: Fetch detailed schemas for selected candidates and rank const schemas = await this.fetchDetailedSchemas(candidates, explainResource); return await this.rankWithDetailedSchemas(intent, schemas, relevantPatterns); } catch (error) { throw new Error(`AI-powered resource solution analysis failed: ${error}`); } } /** * Phase 0: Search for relevant organizational patterns using multi-concept approach * Returns empty array if Vector DB is not available - this is completely optional */ async searchRelevantPatterns(intent) { // If pattern service is not available, skip pattern search entirely if (!this.patternService) { console.log('📋 Pattern service unavailable, skipping pattern search - using pure AI recommendations'); return []; } try { // Step 1: Extract deployment concepts from user intent const concepts = await this.extractDeploymentConcepts(intent); console.log(`🔍 Extracted ${concepts.length} deployment concepts from intent`); // If concept extraction fails, fall back to simple search if (concepts.length === 0) { console.warn('⚠️ No concepts extracted, falling back to simple pattern search'); const fallbackResults = await this.patternService.searchPatterns(intent, { limit: 5 }); return fallbackResults.map(result => result.data); } // Step 2: Find patterns for each concept const allPatternMatches = []; for (const concept of concepts) { try { // Search using concept keywords const conceptKeywords = concept.keywords.join(' '); const searchResults = await this.patternService.searchPatterns(conceptKeywords, { limit: 10 }); // Convert to PatternMatch with concept context const matches = searchResults.map(result => ({ pattern: result.data, score: result.score * this.getConceptImportanceWeight(concept.importance), matchedConcept: concept, matchType: result.matchType })); allPatternMatches.push(...matches); console.log(` 📋 Found ${matches.length} patterns for concept: ${concept.concept}`); } catch (error) { console.warn(`⚠️ Pattern search failed for concept "${concept.concept}":`, error); } } // Step 3: Deduplicate and rank patterns const uniquePatterns = this.deduplicateAndRankPatterns(allPatternMatches); console.log(`✅ Final pattern selection: ${uniquePatterns.length} unique patterns`); return uniquePatterns; } catch (error) { // Pattern search is non-blocking - if it fails, continue without patterns console.warn('❌ Multi-concept pattern search failed, continuing without patterns:', error); return []; } } /** * Extract deployment concepts from user intent using AI */ async extractDeploymentConcepts(intent) { const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); const promptPath = path.join(__dirname, '..', '..', 'prompts', 'concept-extraction.md'); const template = fs.readFileSync(promptPath, 'utf8'); const conceptPrompt = template.replace('{intent}', intent); const response = await this.claudeIntegration.sendMessage(conceptPrompt); try { // Extract JSON from response let jsonContent = response.content; const codeBlockMatch = response.content.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); if (codeBlockMatch) { jsonContent = codeBlockMatch[1]; } const result = JSON.parse(jsonContent); return result.concepts || []; } catch (error) { console.warn('Failed to parse concept extraction response:', error); // Fallback: create a single concept from the original intent return [{ category: 'application_architecture', concept: 'generic application', importance: 'medium', keywords: [intent] }]; } } /** * Get weight multiplier based on concept importance */ getConceptImportanceWeight(importance) { switch (importance) { case 'high': return 1.2; case 'medium': return 1.0; case 'low': return 0.8; default: return 1.0; } } /** * Deduplicate patterns and rank by combined score */ deduplicateAndRankPatterns(matches) { // Group by pattern ID and combine scores const patternScores = new Map(); for (const match of matches) { const existing = patternScores.get(match.pattern.id); if (existing) { existing.totalScore += match.score; existing.matchCount += 1; } else { patternScores.set(match.pattern.id, { pattern: match.pattern, totalScore: match.score, matchCount: 1 }); } } // Calculate average scores and sort const rankedPatterns = Array.from(patternScores.values()) .map(item => ({ pattern: item.pattern, avgScore: item.totalScore / item.matchCount, matchCount: item.matchCount })) .sort((a, b) => { // Prioritize patterns that match multiple concepts if (a.matchCount !== b.matchCount) { return b.matchCount - a.matchCount; } // Then sort by average score return b.avgScore - a.avgScore; }) .slice(0, 8); // Increased limit for multi-concept matching return rankedPatterns.map(item => item.pattern); } /** * Phase 1: AI selects promising resource candidates from lightweight list */ async selectResourceCandidates(intent, resources, patterns = []) { // Normalize resource structures between standard resources and CRDs const normalizedResources = resources.map(resource => { // Handle both standard resources and CRDs const apiVersion = resource.apiVersion || (resource.group ? `${resource.group}/${resource.version}` : resource.version); const isNamespaced = resource.namespaced !== undefined ? resource.namespaced : resource.scope === 'Namespaced'; return { ...resource, apiVersion, namespaced: isNamespaced }; }); const resourceSummary = normalizedResources.map((resource, index) => `${index}: ${resource.kind} (${resource.apiVersion}) Group: ${resource.group || 'core'} Namespaced: ${resource.namespaced}`).join('\n\n'); // Format organizational patterns for AI context const patternsContext = patterns.length > 0 ? patterns.map(pattern => `- ID: ${pattern.id} Description: ${pattern.description} Suggested Resources: ${pattern.suggestedResources?.join(', ') || 'Not specified'} Rationale: ${pattern.rationale} Triggers: ${pattern.triggers?.join(', ') || 'None'}`).join('\n') : 'No organizational patterns found for this request.'; const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); const promptPath = path.join(__dirname, '..', '..', 'prompts', 'resource-selection.md'); const template = fs.readFileSync(promptPath, 'utf8'); const selectionPrompt = template .replace('{intent}', intent) .replace('{resources}', resourceSummary) .replace('{patterns}', patternsContext); const response = await this.claudeIntegration.sendMessage(selectionPrompt); try { // Extract JSON from response with robust parsing let jsonContent = response.content; // First try to find JSON array wrapped in code blocks const codeBlockMatch = response.content.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/); if (codeBlockMatch) { jsonContent = codeBlockMatch[1]; } else { // Try to find JSON array that starts with [ and find the matching closing ] const startIndex = response.content.indexOf('['); if (startIndex !== -1) { let bracketCount = 0; let endIndex = startIndex; for (let i = startIndex; i < response.content.length; i++) { if (response.content[i] === '[') bracketCount++; if (response.content[i] === ']') bracketCount--; if (bracketCount === 0) { endIndex = i; break; } } if (bracketCount === 0) { jsonContent = response.content.substring(startIndex, endIndex + 1); } } } const selectedResources = JSON.parse(jsonContent.trim()); if (!Array.isArray(selectedResources)) { throw new Error('AI response is not an array'); } // Validate that each resource has required fields for (const resource of selectedResources) { if (!resource.kind || !resource.apiVersion) { throw new Error(`AI selected invalid resource: ${JSON.stringify(resource)}`); } } return selectedResources; } catch (error) { throw new Error(`AI failed to select resources in valid JSON format. Error: ${error.message}. AI response: "${response.content.substring(0, 200)}..."`); } } /** * Phase 2: Fetch detailed schemas for selected candidates */ async fetchDetailedSchemas(candidates, explainResource) { const schemas = []; const errors = []; for (const resource of candidates) { try { const explanation = await explainResource(resource.kind); // Parse GROUP, KIND, VERSION from kubectl explain output const lines = explanation.split('\n'); const groupLine = lines.find((line) => line.startsWith('GROUP:')); const kindLine = lines.find((line) => line.startsWith('KIND:')); const versionLine = lines.find((line) => line.startsWith('VERSION:')); const group = groupLine ? groupLine.replace('GROUP:', '').trim() : ''; const kind = kindLine ? kindLine.replace('KIND:', '').trim() : resource.kind; const version = versionLine ? versionLine.replace('VERSION:', '').trim() : 'v1'; // Build apiVersion from group and version const apiVersion = group ? `${group}/${version}` : version; // Create a simple schema with raw explanation for AI processing const schema = { kind: kind, apiVersion: apiVersion, group: group, description: explanation.split('\n').find((line) => line.startsWith('DESCRIPTION:'))?.replace('DESCRIPTION:', '').trim() || '', properties: new Map(), rawExplanation: explanation // Include raw explanation for AI }; schemas.push(schema); } catch (error) { errors.push(`${resource.kind}: ${error.message}`); } } if (schemas.length === 0) { throw new Error(`Could not fetch schemas for any selected resources. Candidates: ${candidates.map(c => c.kind).join(', ')}. Errors: ${errors.join(', ')}`); } if (errors.length > 0) { console.warn(`Some resources could not be analyzed: ${errors.join(', ')}`); console.warn(`Successfully fetched schemas for: ${schemas.map(s => s.kind).join(', ')}`); } return schemas; } /** * Phase 3: Rank resources with detailed schema information */ async rankWithDetailedSchemas(intent, schemas, patterns = []) { const prompt = await this.loadPromptTemplate(intent, schemas, patterns); const response = await this.claudeIntegration.sendMessage(prompt); const solutions = this.parseAISolutionResponse(response.content, schemas); // Generate AI-powered questions for each solution for (const solution of solutions) { solution.questions = await this.generateQuestionsWithAI(intent, solution); } return solutions; } /** * Load and format prompt template from file */ async loadPromptTemplate(intent, schemas, patterns = []) { const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); const promptPath = path.join(__dirname, '..', '..', 'prompts', 'resource-solution-ranking.md'); const template = fs.readFileSync(promptPath, 'utf8'); // Format resources for the prompt with detailed schema information const resourcesText = schemas.map((schema, index) => { let resourceInfo = `${index}: ${schema.kind} (${schema.apiVersion}) Group: ${schema.group || 'core'} Description: ${schema.description} Namespaced: ${schema.namespace}`; // Include detailed schema information for capability analysis if (schema.rawExplanation) { resourceInfo += `\n\n Complete Schema Information:\n${schema.rawExplanation}`; } return resourceInfo; }).join('\n\n'); // Format organizational patterns for AI context const patternsContext = patterns.length > 0 ? patterns.map(pattern => `- ID: ${pattern.id} Description: ${pattern.description} Suggested Resources: ${pattern.suggestedResources?.join(', ') || 'Not specified'} Rationale: ${pattern.rationale} Triggers: ${pattern.triggers?.join(', ') || 'None'}`).join('\n') : 'No organizational patterns found for this request.'; return template .replace('{intent}', intent) .replace('{resources}', resourcesText) .replace('{patterns}', patternsContext); } /** * Parse AI response into solution results */ parseAISolutionResponse(aiResponse, schemas) { try { // Use robust JSON extraction const parsed = this.extractJsonFromAIResponse(aiResponse); const solutions = parsed.solutions.map((solution) => { const isDebugMode = process.env.DOT_AI_DEBUG === 'true'; if (isDebugMode) { console.debug('DEBUG: solution object:', JSON.stringify(solution, null, 2)); } // Find matching schemas for the requested resources const resources = []; const notFound = []; for (const requestedResource of solution.resources || []) { const matchingSchema = schemas.find(schema => schema.kind === requestedResource.kind && schema.apiVersion === requestedResource.apiVersion && schema.group === requestedResource.group); if (matchingSchema) { resources.push(matchingSchema); } else { notFound.push(requestedResource); } } if (resources.length === 0) { if (isDebugMode) { console.debug('DEBUG: No matching resources found'); console.debug('DEBUG: Requested resources:', solution.resources); console.debug('DEBUG: Available schemas:', schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group }))); } const debugInfo = { requestedResources: solution.resources || [], notFoundResources: notFound, availableSchemas: schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group })) }; throw new Error(`No matching resources found: ${JSON.stringify(debugInfo, null, 2)}`); } if (notFound.length > 0 && isDebugMode) { console.debug('DEBUG: Some resources not found:', notFound); } return { type: solution.type, resources, score: solution.score, description: solution.description, reasons: solution.reasons || [], analysis: solution.analysis || '', questions: { required: [], basic: [], advanced: [], open: { question: '', placeholder: '' } }, patternInfluences: solution.patternInfluences || [], usedPatterns: solution.usedPatterns || false }; }); // Sort by score descending return solutions.sort((a, b) => b.score - a.score); } catch (error) { // Enhanced error message with more context const errorMsg = `Failed to parse AI solution response: ${error.message}`; const contextMsg = `\nAI Response (first 500 chars): "${aiResponse.substring(0, 500)}..."`; const schemasMsg = `\nAvailable schemas: ${schemas.map(s => s.kind).join(', ')} (total: ${schemas.length})`; throw new Error(errorMsg + contextMsg + schemasMsg); } } /** * Discover cluster options for dynamic question generation */ async discoverClusterOptions() { try { const { executeKubectl } = await Promise.resolve().then(() => __importStar(require('./kubernetes-utils'))); // Discover namespaces const namespacesResult = await executeKubectl(['get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}']); const namespaces = namespacesResult.split(/\s+/).filter(Boolean); // Discover storage classes let storageClasses = []; try { const storageResult = await executeKubectl(['get', 'storageclass', '-o', 'jsonpath={.items[*].metadata.name}']); storageClasses = storageResult.split(/\s+/).filter(Boolean); } catch { // Storage classes might not be available in all clusters storageClasses = []; } // Discover ingress classes let ingressClasses = []; try { const ingressResult = await executeKubectl(['get', 'ingressclass', '-o', 'jsonpath={.items[*].metadata.name}']); ingressClasses = ingressResult.split(/\s+/).filter(Boolean); } catch { // Ingress classes might not be available ingressClasses = []; } // Get common node labels let nodeLabels = []; try { const nodesResult = await executeKubectl(['get', 'nodes', '-o', 'json']); const nodes = JSON.parse(nodesResult); const labelSet = new Set(); nodes.items?.forEach((node) => { Object.keys(node.metadata?.labels || {}).forEach(label => { if (!label.startsWith('kubernetes.io/') && !label.startsWith('node.kubernetes.io/')) { labelSet.add(label); } }); }); nodeLabels = Array.from(labelSet); } catch { nodeLabels = []; } return { namespaces, storageClasses, ingressClasses, nodeLabels }; } catch (error) { console.warn('Failed to discover cluster options, using defaults:', error); return { namespaces: ['default'], storageClasses: [], ingressClasses: [], nodeLabels: [] }; } } /** * Extract JSON object from AI response with robust parsing */ extractJsonFromAIResponse(aiResponse) { let jsonContent = aiResponse; // First try to find JSON wrapped in code blocks const codeBlockMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); if (codeBlockMatch) { jsonContent = codeBlockMatch[1]; } else { // Try to find JSON that starts with { and find the matching closing } const startIndex = aiResponse.indexOf('{'); if (startIndex !== -1) { let braceCount = 0; let endIndex = startIndex; for (let i = startIndex; i < aiResponse.length; i++) { if (aiResponse[i] === '{') braceCount++; if (aiResponse[i] === '}') braceCount--; if (braceCount === 0) { endIndex = i; break; } } if (braceCount === 0) { jsonContent = aiResponse.substring(startIndex, endIndex + 1); } } } return JSON.parse(jsonContent.trim()); } /** * Generate contextual questions using AI based on user intent and solution resources */ async generateQuestionsWithAI(intent, solution) { try { // Discover cluster options for dynamic questions const clusterOptions = await this.discoverClusterOptions(); // Format resource details for the prompt using raw explanation when available const resourceDetails = solution.resources.map(resource => { if (resource.rawExplanation) { // Use raw kubectl explain output for comprehensive field information return `${resource.kind} (${resource.apiVersion}): Description: ${resource.description} Complete Schema Information: ${resource.rawExplanation}`; } else { // Fallback to properties map if raw explanation is not available const properties = Array.from(resource.properties.entries()).map(([key, field]) => { const nestedFields = Array.from(field.nested.entries()).map(([nestedKey, nestedField]) => ` ${nestedKey}: ${nestedField.type} - ${nestedField.description}`).join('\n'); return ` ${key}: ${field.type} - ${field.description}${field.required ? ' (required)' : ''}${nestedFields ? '\n' + nestedFields : ''}`; }).join('\n'); return `${resource.kind} (${resource.apiVersion}): Description: ${resource.description} Required fields: ${resource.required?.join(', ') || 'none specified'} Properties: ${properties}`; } }).join('\n\n'); // Format cluster options for the prompt const clusterOptionsText = `Available Namespaces: ${clusterOptions.namespaces.join(', ')} Available Storage Classes: ${clusterOptions.storageClasses.length > 0 ? clusterOptions.storageClasses.join(', ') : 'None discovered'} Available Ingress Classes: ${clusterOptions.ingressClasses.length > 0 ? clusterOptions.ingressClasses.join(', ') : 'None discovered'} Available Node Labels: ${clusterOptions.nodeLabels.length > 0 ? clusterOptions.nodeLabels.slice(0, 10).join(', ') : 'None discovered'}`; // Load and format the question generation prompt const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); const promptPath = path.join(__dirname, '..', '..', 'prompts', 'question-generation.md'); const template = fs.readFileSync(promptPath, 'utf8'); const questionPrompt = template .replace('{intent}', intent) .replace('{solution_description}', solution.description) .replace('{resource_details}', resourceDetails) .replace('{cluster_options}', clusterOptionsText); const response = await this.claudeIntegration.sendMessage(questionPrompt); // Use robust JSON extraction const questions = this.extractJsonFromAIResponse(response.content); // Validate the response structure if (!questions.required || !questions.basic || !questions.advanced || !questions.open) { throw new Error('Invalid question structure from AI'); } return questions; } catch (error) { console.warn(`Failed to generate AI questions for solution: ${error}`); // Fallback to basic open question return { required: [], basic: [], advanced: [], open: { question: "Is there anything else about your requirements or constraints that would help us provide better recommendations?", placeholder: "e.g., specific security requirements, performance needs, existing infrastructure constraints..." } }; } } } exports.ResourceRecommender = ResourceRecommender;