@vfarcic/dot-ai
Version:
Universal Kubernetes application deployment agent with CLI and MCP interfaces
836 lines (834 loc) • 38.6 kB
JavaScript
;
/**
* 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;