UNPKG

@vfarcic/dot-ai

Version:

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

1,061 lines (1,058 loc) 58.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 = exports.OUTPUT_PATH_QUESTION = exports.OUTPUT_FORMAT_QUESTION = void 0; const plugin_registry_1 = require("./plugin-registry"); const pattern_vector_service_1 = require("./pattern-vector-service"); const capability_vector_service_1 = require("./capability-vector-service"); const policy_vector_service_1 = require("./policy-vector-service"); const shared_prompt_loader_1 = require("./shared-prompt-loader"); const platform_utils_1 = require("./platform-utils"); const constants_1 = require("./constants"); // PRD #343: Inline sanitization (helm-utils.ts removed) function sanitizeShellArg(arg, fieldName = 'argument') { if (!/^[a-zA-Z0-9\-_./:\\@]+$/.test(arg)) { throw new Error(`Invalid characters in ${fieldName}: "${arg}". Only alphanumeric characters, dashes, underscores, dots, forward slashes, colons, and @ are allowed.`); } return arg; } function sanitizeChartInfo(chart) { return { repositoryName: sanitizeShellArg(chart.repositoryName, 'repository name'), repository: sanitizeShellArg(chart.repository, 'repository URL'), chartName: sanitizeShellArg(chart.chartName, 'chart name'), version: chart.version ? sanitizeShellArg(chart.version, 'version') : undefined, }; } /** * Packaging questions for capability-based solutions (not Helm charts) * These are injected programmatically after AI generates questions */ exports.OUTPUT_FORMAT_QUESTION = { id: 'outputFormat', question: 'How would you like the manifests packaged?', type: 'select', options: ['raw', 'helm', 'kustomize'], placeholder: 'Select output format', suggestedAnswer: 'kustomize', validation: { required: true }, }; exports.OUTPUT_PATH_QUESTION = { id: 'outputPath', question: 'Where would you like to save the output?', type: 'text', placeholder: 'e.g., ./manifests or ./my-app', suggestedAnswer: './manifests', validation: { required: true }, }; /** * Inject packaging questions into a QuestionGroup for capability-based solutions * Adds outputFormat and outputPath to required questions if not already present */ function injectPackagingQuestions(questions) { const hasOutputFormat = questions.required.some(q => q.id === 'outputFormat'); const hasOutputPath = questions.required.some(q => q.id === 'outputPath'); const packagingQuestions = []; if (!hasOutputFormat) { packagingQuestions.push({ ...exports.OUTPUT_FORMAT_QUESTION }); } if (!hasOutputPath) { packagingQuestions.push({ ...exports.OUTPUT_PATH_QUESTION }); } return { ...questions, required: [...questions.required, ...packagingQuestions], }; } /** * 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 * PRD #343: Supports plugin system for kubectl operations */ class ManifestValidator { /** * PRD #359: Uses unified plugin registry for kubectl operations */ constructor() { // Plugin registry will be checked at operation time } /** * Execute kubectl via plugin system * PRD #359: ALL Kubernetes operations go through unified plugin registry */ async executeKubectlViaPlugin(args) { if (!(0, plugin_registry_1.isPluginInitialized)()) { throw new Error('Plugin system not available. ManifestValidator requires agentic-tools plugin for kubectl operations.'); } const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_exec_command', { args }); if (response.success) { if (typeof response.result === 'object' && response.result !== null) { const result = response.result; // Check for nested error - plugin wraps kubectl errors in { success: false, error: "..." } if (result.success === false) { throw new Error(result.error || result.message || 'kubectl command failed'); } // Return only the data field - never pass JSON wrapper to consumers if (result.data !== undefined) { return String(result.data); } if (typeof result === 'string') { return result; } throw new Error('Plugin returned unexpected response format - missing data field'); } return String(response.result || ''); } else { throw new Error(response.error?.message || 'kubectl command failed via plugin'); } } /** * Validate a manifest using kubectl dry-run * This uses the actual Kubernetes API server validation for accuracy * PRD #359: Routes through unified plugin registry */ async validateManifest(manifestPath, config) { const errors = []; const warnings = []; try { const dryRunMode = config?.dryRunMode || 'server'; // PRD #359: Read manifest content and use kubectl_apply_dryrun tool via unified registry // File paths don't work across containers, so we pass content via plugin tool const fs = await Promise.resolve().then(() => __importStar(require('fs'))); const yaml = await Promise.resolve().then(() => __importStar(require('yaml'))); const manifestContent = fs.readFileSync(manifestPath, 'utf8'); if (!(0, plugin_registry_1.isPluginInitialized)()) { throw new Error('Plugin system not available. ManifestValidator requires agentic-tools plugin for kubectl operations.'); } // Use kubectl_apply_dryrun tool which accepts manifest content const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_apply_dryrun', { manifest: manifestContent, dryRunMode: dryRunMode, }); if (!response.success) { throw new Error(response.error?.message || 'kubectl dry-run validation failed'); } // Check for nested error if (typeof response.result === 'object' && response.result !== null) { const result = response.result; if (result.success === false) { throw new Error(result.error || result.message || 'kubectl dry-run validation failed'); } } // If we get here, validation passed // Add best practice warnings by parsing the manifest const documents = yaml.parseAllDocuments(manifestContent); // Process all documents for best practice warnings documents.forEach(doc => { if (doc.contents) { this.addBestPracticeWarnings(doc.toJS(), warnings); } }); return { valid: true, errors, warnings, }; } catch (error) { // Parse kubectl error output for validation issues const errorMessage = error instanceof Error ? error.message : String(error); 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 * PRD #359: Uses unified plugin registry for kubectl operations */ class ResourceRecommender { aiProvider; patternService; capabilityService; policyService; constructor(aiProvider) { // Use provided AI provider or create from environment this.aiProvider = aiProvider || (() => { // Lazy import to avoid circular dependencies // eslint-disable-next-line @typescript-eslint/no-require-imports -- Dynamic require to avoid circular dependency const { createAIProvider } = require('./ai-provider-factory'); return createAIProvider(); })(); // Initialize capability service - fail gracefully if plugin unavailable try { // Use environment variable for collection name (allows using test data collection) const collectionName = process.env.QDRANT_CAPABILITIES_COLLECTION || 'capabilities'; this.capabilityService = new capability_vector_service_1.CapabilityVectorService(collectionName); console.log(`✅ Capability service initialized (collection: ${collectionName})`); } catch (error) { console.warn('⚠️ Vector service initialization failed, capabilities disabled:', error); this.capabilityService = undefined; } // Initialize pattern service try { this.patternService = new pattern_vector_service_1.PatternVectorService('patterns'); console.log('✅ Pattern service initialized'); } catch (error) { console.warn('⚠️ Vector service initialization failed, patterns disabled:', error); this.patternService = undefined; } // Initialize policy service try { this.policyService = new policy_vector_service_1.PolicyVectorService(); console.log('✅ Policy service initialized'); } catch (error) { console.warn('⚠️ Vector service initialization failed, policies disabled:', error); this.policyService = undefined; } } /** * Execute kubectl via plugin system * PRD #359: ALL Kubernetes operations go through unified plugin registry */ async executeKubectlViaPlugin(args) { if (!(0, plugin_registry_1.isPluginInitialized)()) { throw new Error('Plugin system not available. ResourceRecommender requires agentic-tools plugin for kubectl operations.'); } const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_exec_command', { args }); if (response.success) { if (typeof response.result === 'object' && response.result !== null) { const result = response.result; // Check for nested error - plugin wraps kubectl errors in { success: false, error: "..." } if (result.success === false) { throw new Error(result.error || result.message || 'kubectl command failed'); } // Return only the data field - never pass JSON wrapper to consumers if (result.data !== undefined) { return String(result.data); } if (typeof result === 'string') { return result; } throw new Error('Plugin returned unexpected response format - missing data field'); } return String(response.result || ''); } else { throw new Error(response.error?.message || 'kubectl command failed via plugin'); } } /** * Find the best resource solution(s) for user intent using two-phase analysis */ async findBestSolutions(intent, _explainResource, interaction_id) { if (!this.aiProvider.isInitialized()) { throw new Error(constants_1.AI_SERVICE_ERROR_TEMPLATES.PROVIDER_NOT_INITIALIZED('AI-powered resource ranking')); } try { // Phase 0: Search for relevant organizational patterns const relevantPatterns = await this.searchRelevantPatterns(intent); // Phase 1a: Replace mass resource discovery with capability-based pre-filtering if (!this.capabilityService) { // Capability service not available - fail fast with clear guidance throw new Error(`Capability service not available for intent "${intent}". Please scan your cluster first:\n` + `Run: manageOrgData({ dataType: "capabilities", operation: "scan" })\n` + `Note: Vector DB is required for capability-based recommendations.`); } let relevantCapabilities = []; if (this.capabilityService) { try { relevantCapabilities = await this.capabilityService.searchCapabilities(intent, { limit: 50, }); } catch (error) { // Capability search failed - fail fast with clear guidance throw new Error(`Capability search failed for intent "${intent}". Please scan your cluster first:\n` + `Run: manageOrgData({ dataType: "capabilities", operation: "scan" })\n` + `Error: ${error}`, { cause: error }); } } else { console.warn('⚠️ Capability service not available (Vector DB not reachable), proceeding without capabilities'); } if (relevantCapabilities.length === 0) { // Fail fast with clear user guidance if no capabilities found throw new Error(`No capabilities found for "${intent}". Please scan your cluster first:\n` + `Run: manageOrgData({ dataType: "capabilities", operation: "scan" })`); } console.log(`🎯 Found ${relevantCapabilities.length} relevant capabilities (vs 415+ mass discovery)`); // Create normalized resource objects from capability matches const capabilityFilteredResources = relevantCapabilities.map(cap => ({ kind: this.extractKindFromResourceName(cap.data.resourceName), group: cap.data.group || this.extractGroupFromResourceName(cap.data.resourceName), apiVersion: cap.data.apiVersion, // Use stored apiVersion from capability scan version: cap.data.version, // Just the version part (e.g., "v1beta1") resourceName: cap.data.resourceName, capabilities: cap.data, // Include capability data for AI decision-making (includes namespaced, etc.) })); // Phase 1: Add missing pattern-suggested resources to available resources list const enhancedResources = await this.addMissingPatternResources(capabilityFilteredResources, relevantPatterns); // Phase 2: AI assembles and ranks complete solutions (replaces separate selection + ranking phases) const solutionResult = await this.assembleAndRankSolutions(intent, enhancedResources, relevantPatterns, interaction_id); // If Helm is recommended, return early - questions will be generated from Helm chart values later if (solutionResult.helmRecommendation) { console.log(`🎯 Helm installation recommended for "${intent}": ${solutionResult.helmRecommendation.suggestedTool}`); return solutionResult; } // Phase 3: Generate questions for each capability-based solution for (const solution of solutionResult.solutions) { solution.questions = await this.generateQuestionsWithAI(intent, solution, _explainResource, interaction_id); } return solutionResult; } catch (error) { throw new Error(`AI-powered resource solution analysis failed: ${error}`, { cause: error }); } } /** * Phase 2: AI assembles and ranks complete solutions (replaces separate selection + ranking) */ async assembleAndRankSolutions(intent, availableResources, patterns, interaction_id) { const prompt = await this.loadSolutionAssemblyPrompt(intent, availableResources, patterns); const response = await this.aiProvider.sendMessage(prompt, 'recommend-solution-assembly', { user_intent: intent ? `Kubernetes solution assembly for: ${intent}` : 'Kubernetes solution assembly', interaction_id: interaction_id || 'recommend_solution_assembly', }); return this.parseSimpleSolutionResponse(response.content); } /** * Parse AI response for simple solution structure (no schema matching needed) */ parseSimpleSolutionResponse(aiResponse) { try { // Use robust JSON extraction const parsed = (0, platform_utils_1.extractJsonFromAIResponse)(aiResponse); // Handle Helm recommendation case (presence of helmRecommendation means Helm is needed) const helmRecommendation = parsed.helmRecommendation || null; // If Helm is recommended (empty solutions + helmRecommendation present), return early if (helmRecommendation && (!parsed.solutions || parsed.solutions.length === 0)) { return { solutions: [], helmRecommendation, }; } 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)); } // Convert resource references to ResourceSchema format for compatibility const resources = (solution.resources || []).map(resource => ({ kind: resource.kind, apiVersion: resource.apiVersion, group: resource.group || '', resourceName: resource.resourceName, // Preserve resourceName from AI response description: `${resource.kind} resource from ${resource.group || 'core'} group`, properties: new Map(), namespace: true, // Default assumption for new architecture })); return { type: solution.type, resources, score: solution.score, description: solution.description, reasons: solution.reasons || [], questions: { required: [], basic: [], advanced: [], open: { question: '', placeholder: '' }, }, appliedPatterns: solution.appliedPatterns || [], }; }); // Sort by score descending const sortedSolutions = solutions.sort((a, b) => b.score - a.score); return { solutions: sortedSolutions, helmRecommendation, }; } 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)}..."`; throw new Error(errorMsg + contextMsg, { cause: error }); } } /** * Load and format solution assembly prompt from file */ async loadSolutionAssemblyPrompt(intent, resources, patterns) { // Format resources for the prompt with capability information const resourcesText = resources .map((resource, index) => { return `${index}: ${resource.kind.toUpperCase()} Group: ${resource.group || 'core'} API Version: ${resource.apiVersion || 'unknown'} Resource Name: ${resource.resourceName} Capabilities: ${Array.isArray(resource.capabilities.capabilities) ? resource.capabilities.capabilities.join(', ') : 'Not specified'} Providers: ${Array.isArray(resource.capabilities.providers) ? resource.capabilities.providers.join(', ') : resource.capabilities.providers || 'kubernetes'} Complexity: ${resource.capabilities.complexity || 'medium'} Use Case: ${resource.capabilities.useCase || resource.capabilities.description || 'General purpose'} Description: ${resource.capabilities.description || 'Kubernetes resource'} Confidence: ${resource.capabilities.confidence || 1.0}`; }) .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 (0, shared_prompt_loader_1.loadPrompt)('resource-selection', { intent, resources: resourcesText, patterns: patternsContext, }); } /** * Add pattern-suggested resources that are missing from capability search results */ async addMissingPatternResources(capabilityResources, patterns) { if (!patterns.length) { return capabilityResources; } // Extract all resource names already in capability results const existingResourceNames = new Set(capabilityResources.map(r => r.resourceName)); // Collect missing pattern resources const missingPatternResources = []; for (const pattern of patterns) { if (pattern.suggestedResources) { for (const suggestedResource of pattern.suggestedResources) { // Skip null/undefined resources if (!suggestedResource || typeof suggestedResource !== 'string') { continue; } // Convert pattern resource format to resource name (e.g., "resourcegroups.azure.upbound.io" -> resourceName) const resourceName = suggestedResource.includes('.') ? suggestedResource : `${suggestedResource}.core`; // Only add if not already present in capability results if (!existingResourceNames.has(resourceName)) { try { // Parse resource components const parts = suggestedResource.split('.'); const kind = parts[0]; // Use resource name as-is: resourcegroups, servicemonitors, etc. const group = parts.length > 1 ? parts.slice(1).join('.') : ''; missingPatternResources.push({ kind, group, resourceName, capabilities: { resourceName, description: `Resource suggested by organizational pattern: ${pattern.description}`, capabilities: [ `organizational pattern`, pattern.description.toLowerCase(), ], providers: this.inferProvidersFromResourceName(suggestedResource), complexity: 'medium', useCase: `Pattern-suggested resource for: ${pattern.rationale}`, confidence: 1.0, // High confidence since it's from organizational pattern source: 'organizational-pattern', patternId: pattern.id, }, }); existingResourceNames.add(resourceName); } catch (error) { console.warn(`Failed to parse pattern resource ${suggestedResource}:`, error); } } } } } return [...capabilityResources, ...missingPatternResources]; } /** * Infer cloud providers from resource name */ inferProvidersFromResourceName(resourceName) { if (resourceName.includes('azure')) return ['azure']; if (resourceName.includes('aws')) return ['aws']; if (resourceName.includes('gcp') || resourceName.includes('google')) return ['gcp']; return ['kubernetes']; } /** * Extract Kubernetes kind from resource name (e.g., "sqls.devopstoolkit.live" -> "SQL") */ extractKindFromResourceName(resourceName) { // For CRDs like "sqls.devopstoolkit.live", the kind is usually the singular of the plural // For core resources like "pods", return as-is if (!resourceName.includes('.')) { return resourceName; // Core resources like "pods", "services" } // For CRDs, extract the resource part (before first dot) const resourcePart = resourceName.split('.')[0]; // Convert plural to singular and capitalize (sqls -> SQL) return resourcePart.toUpperCase(); } /** * Extract group from resource name (e.g., "sqls.devopstoolkit.live" -> "devopstoolkit.live") */ extractGroupFromResourceName(resourceName) { if (!resourceName.includes('.')) { return 'core'; // Core resources have no group } // Return everything after the first dot return resourceName.substring(resourceName.indexOf('.') + 1); } // Note: constructApiVersionFromResourceName method removed - no longer needed // API versions are extracted from kubectl explain schema content during manifest generation /** * 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 { // Search patterns directly with user intent (vector search handles semantic concepts) const patternResults = await this.patternService.searchPatterns(intent, { limit: 5, }); return patternResults.map(result => result.data); } catch (error) { // Pattern search is non-blocking - if it fails, continue without patterns console.warn('❌ Pattern search failed, continuing without patterns:', error); return []; } } // REMOVED: selectResourceCandidates - replaced by single-phase assembleAndRankSolutions // REMOVED: fetchDetailedSchemas - no longer needed in single-phase architecture /** * 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; } /** * Discover cluster options for dynamic question generation * PRD #343: ALL kubectl operations go through plugin */ async discoverClusterOptions() { try { // Discover namespaces via plugin const namespacesResult = await this.executeKubectlViaPlugin([ 'get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}', ]); const namespaces = namespacesResult.split(/\s+/).filter(Boolean); // Discover storage classes with default marking let storageClasses = []; try { const storageResult = await this.executeKubectlViaPlugin([ 'get', 'storageclass', '-o', 'json', ]); const storageData = JSON.parse(storageResult); storageClasses = (storageData.items || []).map((item) => ({ name: item.metadata?.name || '', isDefault: item.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true', })); } catch { // Storage classes might not be available in all clusters storageClasses = []; } // Discover ingress classes with default marking let ingressClasses = []; try { const ingressResult = await this.executeKubectlViaPlugin([ 'get', 'ingressclass', '-o', 'json', ]); const ingressData = JSON.parse(ingressResult); ingressClasses = (ingressData.items || []).map((item) => ({ name: item.metadata?.name || '', isDefault: item.metadata?.annotations?.['ingressclass.kubernetes.io/is-default-class'] === 'true', })); } catch { // Ingress classes might not be available ingressClasses = []; } // Get common node labels let nodeLabels = []; try { const nodesResult = await this.executeKubectlViaPlugin([ '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: [], }; } } /** * Format cluster options for inclusion in prompts */ formatClusterOptionsText(clusterOptions) { const formatResourceList = (items) => { if (items.length === 0) return 'None discovered'; return items .map(item => (item.isDefault ? `${item.name} (default)` : item.name)) .join(', '); }; return `Available Namespaces: ${clusterOptions.namespaces.join(', ')} Available Storage Classes: ${formatResourceList(clusterOptions.storageClasses)} Available Ingress Classes: ${formatResourceList(clusterOptions.ingressClasses)} Available Node Labels: ${clusterOptions.nodeLabels.length > 0 ? clusterOptions.nodeLabels.slice(0, 10).join(', ') : 'None discovered'}`; } /** * Generate contextual questions using AI based on user intent and solution resources */ async generateQuestionsWithAI(intent, solution, _explainResource, interaction_id) { try { // Discover cluster options for dynamic questions const clusterOptions = await this.discoverClusterOptions(); // Search for relevant policy intents based on the selected resources let relevantPolicyResults = []; if (this.policyService) { try { const resourceContext = solution.resources .map(r => `${r.kind} ${r.description}`) .join(' '); const policyResults = await this.policyService.searchPolicyIntents(`${intent} ${resourceContext}`, { limit: 50 }); relevantPolicyResults = policyResults.map(result => ({ policy: result.data, score: result.score, matchType: result.matchType, })); console.log(`🛡️ Found ${relevantPolicyResults.length} relevant policy intents for question generation`); } catch (error) { console.warn('⚠️ Policy search failed during question generation, proceeding without policies:', error); } } else { console.log('🛡️ Policy service unavailable, skipping policy search - proceeding without policy guidance'); } // Fetch resource schemas for each resource in the solution const resourcesWithSchemas = await Promise.all(solution.resources.map(async (resource) => { // Validate that resource has resourceName field for kubectl explain if (!resource.resourceName) { throw new Error(`Resource ${resource.kind} is missing resourceName field. This indicates a bug in solution construction.`); } try { // Use resourceName for kubectl explain - this should be the plural form like 'pods', 'services', etc. const schemaExplanation = await _explainResource(resource.resourceName); return { ...resource, rawExplanation: schemaExplanation, }; } catch (error) { console.warn(`Failed to fetch schema for ${resource.kind}: ${error}`); return resource; } })); // Format resource details for the prompt using raw explanation when available const resourceDetails = resourcesWithSchemas .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 = this.formatClusterOptionsText(clusterOptions); // Format organizational policies for AI context with relevance scores const policyContextText = relevantPolicyResults.length > 0 ? relevantPolicyResults .map(result => `- ID: ${result.policy.id} Description: ${result.policy.description} Rationale: ${result.policy.rationale} Triggers: ${result.policy.triggers?.join(', ') || 'None'} Score: ${result.score.toFixed(3)} (${result.matchType})`) .join('\n') : 'No organizational policies found for this request.'; // Build source_material for capabilities (Kubernetes resource-based solutions) const sourceMaterial = `## Source Material You are generating questions for Kubernetes resources. The schemas below define the available configuration options. ## Resources in Solution ${resourceDetails}`; // Generate question prompt with variables const questionPrompt = (0, shared_prompt_loader_1.loadPrompt)('question-generation', { intent, solution_description: solution.description, source_material: sourceMaterial, cluster_options: clusterOptionsText, policy_context: policyContextText, }); const response = await this.aiProvider.sendMessage(questionPrompt, 'recommend-question-generation', { user_intent: `Generate deployment questions for: ${intent}`, interaction_id: interaction_id || 'recommend_question_generation', }); // Use robust JSON extraction const questions = (0, platform_utils_1.extractJsonFromAIResponse)(response.content); // Validate the response structure if (!questions.required || !questions.basic || !questions.advanced || !questions.open) { throw new Error('Invalid question structure from AI'); } // Sanitize questions: ensure suggestedAnswer passes its own validation constraints const sanitizeQuestions = (qs) => { for (const q of qs) { if ((q.type === 'select' || q.type === 'multiselect') && q.options && q.options.length > 0 && q.suggestedAnswer !== undefined) { if (q.type === 'select' && !q.options.includes(q.suggestedAnswer)) { q.suggestedAnswer = q.options[0]; } else if (q.type === 'multiselect') { if (!Array.isArray(q.suggestedAnswer)) { q.suggestedAnswer = [q.options[0]]; } else { q.suggestedAnswer = q.suggestedAnswer.filter(a => q.options.includes(a)); if (q.suggestedAnswer.length === 0) { q.suggestedAnswer = [q.options[0]]; } } } } // Clamp number suggestedAnswer to validation.min/max bounds