@vfarcic/dot-ai 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/context-load.md +11 -0
- package/.claude/commands/context-save.md +16 -0
- package/.claude/commands/prd-done.md +115 -0
- package/.claude/commands/prd-get.md +25 -0
- package/.claude/commands/prd-start.md +87 -0
- package/.claude/commands/task-done.md +77 -0
- package/.claude/commands/tests-reminder.md +32 -0
- package/.claude/settings.local.json +20 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/ci.yml +170 -0
- package/.prettierrc.json +10 -0
- package/.teller.yml +8 -0
- package/CLAUDE.md +162 -0
- package/assets/images/logo.png +0 -0
- package/bin/dot-ai.ts +47 -0
- package/destroy.sh +45 -0
- package/devbox.json +13 -0
- package/devbox.lock +225 -0
- package/docs/API.md +449 -0
- package/docs/CONTEXT.md +49 -0
- package/docs/DEVELOPMENT.md +203 -0
- package/docs/NEXT_STEPS.md +97 -0
- package/docs/STAGE_BASED_API.md +97 -0
- package/docs/cli-guide.md +798 -0
- package/docs/design.md +750 -0
- package/docs/discovery-engine.md +515 -0
- package/docs/error-handling.md +429 -0
- package/docs/function-registration.md +157 -0
- package/docs/mcp-guide.md +416 -0
- package/package.json +2 -121
- package/renovate.json +51 -0
- package/setup.sh +111 -0
- package/{dist/cli.js → src/cli.ts} +26 -19
- package/src/core/claude.ts +280 -0
- package/src/core/deploy-operation.ts +127 -0
- package/src/core/discovery.ts +900 -0
- package/src/core/error-handling.ts +562 -0
- package/src/core/index.ts +143 -0
- package/src/core/kubernetes-utils.ts +218 -0
- package/src/core/memory.ts +148 -0
- package/src/core/schema.ts +830 -0
- package/src/core/session-utils.ts +97 -0
- package/src/core/workflow.ts +234 -0
- package/src/index.ts +18 -0
- package/src/interfaces/cli.ts +872 -0
- package/src/interfaces/mcp.ts +183 -0
- package/src/mcp/server.ts +131 -0
- package/src/tools/answer-question.ts +807 -0
- package/src/tools/choose-solution.ts +169 -0
- package/src/tools/deploy-manifests.ts +94 -0
- package/src/tools/generate-manifests.ts +502 -0
- package/src/tools/index.ts +41 -0
- package/src/tools/recommend.ts +370 -0
- package/tests/__mocks__/@kubernetes/client-node.ts +106 -0
- package/tests/build-system.test.ts +345 -0
- package/tests/configuration.test.ts +226 -0
- package/tests/core/deploy-operation.test.ts +38 -0
- package/tests/core/discovery.test.ts +1648 -0
- package/tests/core/error-handling.test.ts +632 -0
- package/tests/core/schema.test.ts +1658 -0
- package/tests/core/session-utils.test.ts +245 -0
- package/tests/core.test.ts +439 -0
- package/tests/fixtures/configmap-no-labels.yaml +8 -0
- package/tests/fixtures/crossplane-app-configuration.yaml +6 -0
- package/tests/fixtures/crossplane-providers.yaml +45 -0
- package/tests/fixtures/crossplane-rbac.yaml +48 -0
- package/tests/fixtures/invalid-configmap.yaml +8 -0
- package/tests/fixtures/invalid-deployment.yaml +17 -0
- package/tests/fixtures/test-deployment.yaml +28 -0
- package/tests/fixtures/valid-configmap.yaml +15 -0
- package/tests/infrastructure.test.ts +426 -0
- package/tests/interfaces/cli.test.ts +1036 -0
- package/tests/interfaces/mcp.test.ts +139 -0
- package/tests/kubernetes-utils.test.ts +200 -0
- package/tests/mcp/server.test.ts +126 -0
- package/tests/setup.ts +31 -0
- package/tests/tools/answer-question.test.ts +367 -0
- package/tests/tools/choose-solution.test.ts +481 -0
- package/tests/tools/deploy-manifests.test.ts +185 -0
- package/tests/tools/generate-manifests.test.ts +441 -0
- package/tests/tools/index.test.ts +111 -0
- package/tests/tools/recommend.test.ts +180 -0
- package/tsconfig.json +34 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/core/claude.d.ts +0 -42
- package/dist/core/claude.d.ts.map +0 -1
- package/dist/core/claude.js +0 -229
- package/dist/core/deploy-operation.d.ts +0 -38
- package/dist/core/deploy-operation.d.ts.map +0 -1
- package/dist/core/deploy-operation.js +0 -101
- package/dist/core/discovery.d.ts +0 -162
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -758
- package/dist/core/error-handling.d.ts +0 -167
- package/dist/core/error-handling.d.ts.map +0 -1
- package/dist/core/error-handling.js +0 -399
- package/dist/core/index.d.ts +0 -42
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -123
- package/dist/core/kubernetes-utils.d.ts +0 -38
- package/dist/core/kubernetes-utils.d.ts.map +0 -1
- package/dist/core/kubernetes-utils.js +0 -177
- package/dist/core/memory.d.ts +0 -45
- package/dist/core/memory.d.ts.map +0 -1
- package/dist/core/memory.js +0 -113
- package/dist/core/schema.d.ts +0 -187
- package/dist/core/schema.d.ts.map +0 -1
- package/dist/core/schema.js +0 -655
- package/dist/core/session-utils.d.ts +0 -29
- package/dist/core/session-utils.d.ts.map +0 -1
- package/dist/core/session-utils.js +0 -121
- package/dist/core/workflow.d.ts +0 -70
- package/dist/core/workflow.d.ts.map +0 -1
- package/dist/core/workflow.js +0 -161
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -32
- package/dist/interfaces/cli.d.ts +0 -74
- package/dist/interfaces/cli.d.ts.map +0 -1
- package/dist/interfaces/cli.js +0 -769
- package/dist/interfaces/mcp.d.ts +0 -30
- package/dist/interfaces/mcp.d.ts.map +0 -1
- package/dist/interfaces/mcp.js +0 -105
- package/dist/mcp/server.d.ts +0 -9
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -151
- package/dist/tools/answer-question.d.ts +0 -27
- package/dist/tools/answer-question.d.ts.map +0 -1
- package/dist/tools/answer-question.js +0 -696
- package/dist/tools/choose-solution.d.ts +0 -23
- package/dist/tools/choose-solution.d.ts.map +0 -1
- package/dist/tools/choose-solution.js +0 -171
- package/dist/tools/deploy-manifests.d.ts +0 -25
- package/dist/tools/deploy-manifests.d.ts.map +0 -1
- package/dist/tools/deploy-manifests.js +0 -74
- package/dist/tools/generate-manifests.d.ts +0 -23
- package/dist/tools/generate-manifests.d.ts.map +0 -1
- package/dist/tools/generate-manifests.js +0 -424
- package/dist/tools/index.d.ts +0 -11
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -34
- package/dist/tools/recommend.d.ts +0 -23
- package/dist/tools/recommend.d.ts.map +0 -1
- package/dist/tools/recommend.js +0 -332
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Schema Parser and Validator
|
|
3
|
+
*
|
|
4
|
+
* Implements comprehensive schema parsing and validation for Kubernetes resources
|
|
5
|
+
* Fetches structured OpenAPI schemas from Kubernetes API server and validates manifests
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ResourceExplanation } from './discovery';
|
|
9
|
+
import {
|
|
10
|
+
executeKubectl
|
|
11
|
+
} from './kubernetes-utils';
|
|
12
|
+
import { ClaudeIntegration } from './claude';
|
|
13
|
+
|
|
14
|
+
// Core type definitions for schema structure
|
|
15
|
+
export interface FieldConstraints {
|
|
16
|
+
minimum?: number;
|
|
17
|
+
maximum?: number;
|
|
18
|
+
minLength?: number;
|
|
19
|
+
maxLength?: number;
|
|
20
|
+
enum?: string[];
|
|
21
|
+
default?: any;
|
|
22
|
+
pattern?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SchemaField {
|
|
26
|
+
name: string;
|
|
27
|
+
type: string;
|
|
28
|
+
description: string;
|
|
29
|
+
required: boolean;
|
|
30
|
+
default?: any;
|
|
31
|
+
constraints?: FieldConstraints;
|
|
32
|
+
nested: Map<string, SchemaField>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResourceSchema {
|
|
36
|
+
apiVersion: string;
|
|
37
|
+
kind: string;
|
|
38
|
+
group: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
description: string;
|
|
41
|
+
properties: Map<string, SchemaField>;
|
|
42
|
+
required?: string[];
|
|
43
|
+
namespace?: boolean;
|
|
44
|
+
rawExplanation?: string; // Raw kubectl explain output for AI processing
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ValidationResult {
|
|
48
|
+
valid: boolean;
|
|
49
|
+
errors: string[];
|
|
50
|
+
warnings: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ResourceMapping {
|
|
54
|
+
resourceKind: string;
|
|
55
|
+
apiVersion: string;
|
|
56
|
+
group?: string;
|
|
57
|
+
fieldPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Simple answer structure for manifest generation
|
|
61
|
+
export interface AnswerSet {
|
|
62
|
+
[questionId: string]: any;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Enhanced solution for single-pass workflow
|
|
66
|
+
export interface EnhancedSolution extends ResourceSolution {
|
|
67
|
+
answers: AnswerSet;
|
|
68
|
+
openAnswer: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Question {
|
|
72
|
+
id: string;
|
|
73
|
+
question: string;
|
|
74
|
+
type: 'text' | 'select' | 'multiselect' | 'boolean' | 'number';
|
|
75
|
+
options?: string[];
|
|
76
|
+
placeholder?: string;
|
|
77
|
+
validation?: {
|
|
78
|
+
required?: boolean;
|
|
79
|
+
min?: number;
|
|
80
|
+
max?: number;
|
|
81
|
+
pattern?: string;
|
|
82
|
+
message?: string;
|
|
83
|
+
};
|
|
84
|
+
answer?: any;
|
|
85
|
+
// Note: resourceMapping removed - manifest generator handles field mapping
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface QuestionGroup {
|
|
89
|
+
required: Question[];
|
|
90
|
+
basic: Question[];
|
|
91
|
+
advanced: Question[];
|
|
92
|
+
open: {
|
|
93
|
+
question: string;
|
|
94
|
+
placeholder: string;
|
|
95
|
+
answer?: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ResourceSolution {
|
|
100
|
+
type: 'single' | 'combination';
|
|
101
|
+
resources: ResourceSchema[];
|
|
102
|
+
score: number;
|
|
103
|
+
description: string;
|
|
104
|
+
reasons: string[];
|
|
105
|
+
analysis: string;
|
|
106
|
+
questions: QuestionGroup;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface AIRankingConfig {
|
|
110
|
+
claudeApiKey: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface DiscoveryFunctions {
|
|
114
|
+
discoverResources: () => Promise<any>;
|
|
115
|
+
explainResource: (resource: string) => Promise<any>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ClusterOptions {
|
|
119
|
+
namespaces: string[];
|
|
120
|
+
storageClasses: string[];
|
|
121
|
+
ingressClasses: string[];
|
|
122
|
+
nodeLabels: string[];
|
|
123
|
+
serviceAccounts?: { [namespace: string]: string[] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* SchemaParser converts kubectl explain output to structured ResourceSchema
|
|
128
|
+
*/
|
|
129
|
+
export class SchemaParser {
|
|
130
|
+
/**
|
|
131
|
+
* Parse ResourceExplanation from discovery engine into structured schema
|
|
132
|
+
*/
|
|
133
|
+
parseResourceExplanation(explanation: ResourceExplanation): ResourceSchema {
|
|
134
|
+
const apiVersion = explanation.group
|
|
135
|
+
? `${explanation.group}/${explanation.version}`
|
|
136
|
+
: explanation.version;
|
|
137
|
+
|
|
138
|
+
const properties = new Map<string, SchemaField>();
|
|
139
|
+
const required: string[] = [];
|
|
140
|
+
|
|
141
|
+
// Process all fields from the explanation
|
|
142
|
+
for (const field of explanation.fields) {
|
|
143
|
+
const parts = field.name.split('.');
|
|
144
|
+
const topLevelField = parts[0];
|
|
145
|
+
|
|
146
|
+
// Add to required if marked as required
|
|
147
|
+
if (field.required && !required.includes(topLevelField)) {
|
|
148
|
+
required.push(topLevelField);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create or get the top-level field
|
|
152
|
+
if (!properties.has(topLevelField)) {
|
|
153
|
+
properties.set(topLevelField, {
|
|
154
|
+
name: topLevelField,
|
|
155
|
+
type: this.normalizeType(field.type),
|
|
156
|
+
description: field.description,
|
|
157
|
+
required: field.required,
|
|
158
|
+
constraints: this.parseFieldConstraints(field.type, field.description),
|
|
159
|
+
nested: new Map()
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle nested fields
|
|
164
|
+
if (parts.length > 1) {
|
|
165
|
+
this.addNestedField(properties.get(topLevelField)!, parts.slice(1), field);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
apiVersion,
|
|
171
|
+
kind: explanation.kind,
|
|
172
|
+
group: explanation.group,
|
|
173
|
+
version: explanation.version,
|
|
174
|
+
description: explanation.description,
|
|
175
|
+
properties,
|
|
176
|
+
required,
|
|
177
|
+
namespace: true // Default to namespaced, could be enhanced with discovery data
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Add nested field to the schema structure
|
|
183
|
+
*/
|
|
184
|
+
private addNestedField(parentField: SchemaField, fieldParts: string[], field: any): void {
|
|
185
|
+
const currentPart = fieldParts[0];
|
|
186
|
+
|
|
187
|
+
if (!parentField.nested.has(currentPart)) {
|
|
188
|
+
parentField.nested.set(currentPart, {
|
|
189
|
+
name: `${parentField.name}.${currentPart}`,
|
|
190
|
+
type: this.normalizeType(field.type),
|
|
191
|
+
description: field.description,
|
|
192
|
+
required: field.required,
|
|
193
|
+
constraints: this.parseFieldConstraints(field.type, field.description),
|
|
194
|
+
nested: new Map()
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Continue recursively if there are more field parts
|
|
199
|
+
if (fieldParts.length > 1) {
|
|
200
|
+
this.addNestedField(parentField.nested.get(currentPart)!, fieldParts.slice(1), field);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Normalize field types from kubectl explain output
|
|
206
|
+
*/
|
|
207
|
+
private normalizeType(type: string): string {
|
|
208
|
+
const lowerType = type.toLowerCase();
|
|
209
|
+
|
|
210
|
+
// Map kubectl types to standard types
|
|
211
|
+
const typeMap: { [key: string]: string } = {
|
|
212
|
+
'object': 'object',
|
|
213
|
+
'string': 'string',
|
|
214
|
+
'integer': 'integer',
|
|
215
|
+
'int32': 'integer',
|
|
216
|
+
'int64': 'integer',
|
|
217
|
+
'boolean': 'boolean',
|
|
218
|
+
'array': 'array',
|
|
219
|
+
'[]string': 'array',
|
|
220
|
+
'[]object': 'array',
|
|
221
|
+
'map[string]string': 'object',
|
|
222
|
+
'map[string]object': 'object'
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return typeMap[lowerType] || 'string';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Parse field constraints from description text
|
|
230
|
+
*/
|
|
231
|
+
parseFieldConstraints(type: string, description: string): FieldConstraints {
|
|
232
|
+
const constraints: FieldConstraints = {};
|
|
233
|
+
|
|
234
|
+
// Extract minimum/maximum values
|
|
235
|
+
const minMatch = description.match(/(?:minimum|min):\s*(\d+)/i);
|
|
236
|
+
if (minMatch) {
|
|
237
|
+
constraints.minimum = parseInt(minMatch[1]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const maxMatch = description.match(/(?:maximum|max):\s*(\d+)/i);
|
|
241
|
+
if (maxMatch) {
|
|
242
|
+
constraints.maximum = parseInt(maxMatch[1]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Extract enum values
|
|
246
|
+
const enumMatch = description.match(/(?:possible values|valid values|values)\s*(?:are)?:\s*([^.]+)/i);
|
|
247
|
+
if (enumMatch) {
|
|
248
|
+
const values = enumMatch[1]
|
|
249
|
+
.split(/,|\s+and\s+/)
|
|
250
|
+
.map(v => v.trim())
|
|
251
|
+
.filter(v => v.length > 0);
|
|
252
|
+
constraints.enum = values;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Extract default values - handle multiple patterns: "(default: value)", "defaults to value", ". Default: value"
|
|
256
|
+
const defaultMatch = description.match(/(?:\(default:\s*([^)]+)\)|(?:defaults?\s+to\s+(\w+))|(?:\.\s+default:\s*(\w+)))/i);
|
|
257
|
+
if (defaultMatch) {
|
|
258
|
+
const defaultValue = (defaultMatch[1] || defaultMatch[2] || defaultMatch[3]).trim();
|
|
259
|
+
if (type === 'integer') {
|
|
260
|
+
const parsed = parseInt(defaultValue);
|
|
261
|
+
if (!isNaN(parsed)) {
|
|
262
|
+
constraints.default = parsed;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
constraints.default = defaultValue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Extract string length constraints
|
|
270
|
+
const minLengthMatch = description.match(/min length:\s*(\d+)/i);
|
|
271
|
+
if (minLengthMatch) {
|
|
272
|
+
constraints.minLength = parseInt(minLengthMatch[1]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const maxLengthMatch = description.match(/max length:\s*(\d+)/i);
|
|
276
|
+
if (maxLengthMatch) {
|
|
277
|
+
constraints.maxLength = parseInt(maxLengthMatch[1]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return constraints;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* ManifestValidator validates Kubernetes manifests using kubectl dry-run
|
|
286
|
+
*/
|
|
287
|
+
export class ManifestValidator {
|
|
288
|
+
/**
|
|
289
|
+
* Validate a manifest using kubectl dry-run
|
|
290
|
+
* This uses the actual Kubernetes API server validation for accuracy
|
|
291
|
+
*/
|
|
292
|
+
async validateManifest(manifestPath: string, config?: { kubeconfig?: string; dryRunMode?: 'client' | 'server' }): Promise<ValidationResult> {
|
|
293
|
+
const errors: string[] = [];
|
|
294
|
+
const warnings: string[] = [];
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const dryRunMode = config?.dryRunMode || 'server';
|
|
298
|
+
const args = ['apply', '--dry-run=' + dryRunMode, '-f', manifestPath];
|
|
299
|
+
|
|
300
|
+
await executeKubectl(args, { kubeconfig: config?.kubeconfig });
|
|
301
|
+
|
|
302
|
+
// If we get here, validation passed
|
|
303
|
+
// kubectl dry-run will throw an error if validation fails
|
|
304
|
+
|
|
305
|
+
// Add best practice warnings by reading the manifest
|
|
306
|
+
const fs = await import('fs');
|
|
307
|
+
const yaml = await import('yaml');
|
|
308
|
+
const manifestContent = yaml.parse(fs.readFileSync(manifestPath, 'utf8')) as any;
|
|
309
|
+
this.addBestPracticeWarnings(manifestContent, warnings);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
valid: true,
|
|
313
|
+
errors,
|
|
314
|
+
warnings
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
} catch (error: any) {
|
|
318
|
+
// Parse kubectl error output for validation issues
|
|
319
|
+
const errorMessage = error.message || '';
|
|
320
|
+
|
|
321
|
+
if (errorMessage.includes('validation failed')) {
|
|
322
|
+
errors.push('Kubernetes validation failed: ' + errorMessage);
|
|
323
|
+
} else if (errorMessage.includes('unknown field')) {
|
|
324
|
+
errors.push('Unknown field in manifest: ' + errorMessage);
|
|
325
|
+
} else if (errorMessage.includes('required field')) {
|
|
326
|
+
errors.push('Missing required field: ' + errorMessage);
|
|
327
|
+
} else {
|
|
328
|
+
errors.push('Validation error: ' + errorMessage);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
valid: false,
|
|
333
|
+
errors,
|
|
334
|
+
warnings
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Add best practice warnings
|
|
341
|
+
*/
|
|
342
|
+
private addBestPracticeWarnings(manifest: any, warnings: string[]): void {
|
|
343
|
+
// Check for missing labels
|
|
344
|
+
if (!manifest.metadata?.labels) {
|
|
345
|
+
warnings.push('Consider adding labels to metadata for better resource organization');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check for missing namespace in namespaced resources
|
|
349
|
+
if (!manifest.metadata?.namespace && manifest.kind !== 'Namespace') {
|
|
350
|
+
warnings.push('Consider specifying a namespace for better resource isolation');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* ResourceRecommender determines which resources best meet user needs using AI
|
|
357
|
+
*/
|
|
358
|
+
export class ResourceRecommender {
|
|
359
|
+
private claudeIntegration: ClaudeIntegration;
|
|
360
|
+
private config: AIRankingConfig;
|
|
361
|
+
|
|
362
|
+
constructor(config: AIRankingConfig) {
|
|
363
|
+
this.config = config;
|
|
364
|
+
this.claudeIntegration = new ClaudeIntegration(config.claudeApiKey);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Find the best resource solution(s) for user intent using two-phase analysis
|
|
369
|
+
*/
|
|
370
|
+
async findBestSolutions(
|
|
371
|
+
intent: string,
|
|
372
|
+
discoverResources: () => Promise<any>,
|
|
373
|
+
explainResource: (resource: string) => Promise<any>
|
|
374
|
+
): Promise<ResourceSolution[]> {
|
|
375
|
+
if (!this.claudeIntegration.isInitialized()) {
|
|
376
|
+
throw new Error('Claude integration not initialized. API key required for AI-powered resource ranking.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
// Phase 1: Get lightweight resource list and let AI select candidates
|
|
381
|
+
const resourceMap = await discoverResources();
|
|
382
|
+
const allResources = [...resourceMap.resources, ...resourceMap.custom];
|
|
383
|
+
const candidates = await this.selectResourceCandidates(intent, allResources);
|
|
384
|
+
|
|
385
|
+
// Phase 2: Fetch detailed schemas for selected candidates and rank
|
|
386
|
+
const schemas = await this.fetchDetailedSchemas(candidates, explainResource);
|
|
387
|
+
return await this.rankWithDetailedSchemas(intent, schemas);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
throw new Error(`AI-powered resource solution analysis failed: ${error}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Phase 1: AI selects promising resource candidates from lightweight list
|
|
395
|
+
*/
|
|
396
|
+
private async selectResourceCandidates(intent: string, resources: any[]): Promise<any[]> {
|
|
397
|
+
// Normalize resource structures between standard resources and CRDs
|
|
398
|
+
const normalizedResources = resources.map(resource => {
|
|
399
|
+
// Handle both standard resources and CRDs
|
|
400
|
+
const apiVersion = resource.apiVersion ||
|
|
401
|
+
(resource.group ? `${resource.group}/${resource.version}` : resource.version);
|
|
402
|
+
const isNamespaced = resource.namespaced !== undefined ?
|
|
403
|
+
resource.namespaced :
|
|
404
|
+
resource.scope === 'Namespaced';
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
...resource,
|
|
408
|
+
apiVersion,
|
|
409
|
+
namespaced: isNamespaced
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const resourceSummary = normalizedResources.map((resource, index) =>
|
|
414
|
+
`${index}: ${resource.kind} (${resource.apiVersion})
|
|
415
|
+
Group: ${resource.group || 'core'}
|
|
416
|
+
Namespaced: ${resource.namespaced}`
|
|
417
|
+
).join('\n\n');
|
|
418
|
+
|
|
419
|
+
const fs = await import('fs');
|
|
420
|
+
const path = await import('path');
|
|
421
|
+
|
|
422
|
+
const promptPath = path.join(process.cwd(), 'prompts', 'resource-selection.md');
|
|
423
|
+
const template = fs.readFileSync(promptPath, 'utf8');
|
|
424
|
+
|
|
425
|
+
const selectionPrompt = template
|
|
426
|
+
.replace('{intent}', intent)
|
|
427
|
+
.replace('{resources}', resourceSummary);
|
|
428
|
+
|
|
429
|
+
const response = await this.claudeIntegration.sendMessage(selectionPrompt);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Extract JSON from response with robust parsing
|
|
433
|
+
let jsonContent = response.content;
|
|
434
|
+
|
|
435
|
+
// First try to find JSON array wrapped in code blocks
|
|
436
|
+
const codeBlockMatch = response.content.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
437
|
+
if (codeBlockMatch) {
|
|
438
|
+
jsonContent = codeBlockMatch[1];
|
|
439
|
+
} else {
|
|
440
|
+
// Try to find JSON array that starts with [ and find the matching closing ]
|
|
441
|
+
const startIndex = response.content.indexOf('[');
|
|
442
|
+
if (startIndex !== -1) {
|
|
443
|
+
let bracketCount = 0;
|
|
444
|
+
let endIndex = startIndex;
|
|
445
|
+
|
|
446
|
+
for (let i = startIndex; i < response.content.length; i++) {
|
|
447
|
+
if (response.content[i] === '[') bracketCount++;
|
|
448
|
+
if (response.content[i] === ']') bracketCount--;
|
|
449
|
+
if (bracketCount === 0) {
|
|
450
|
+
endIndex = i;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (bracketCount === 0) {
|
|
456
|
+
jsonContent = response.content.substring(startIndex, endIndex + 1);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const selectedResources = JSON.parse(jsonContent.trim());
|
|
462
|
+
|
|
463
|
+
if (!Array.isArray(selectedResources)) {
|
|
464
|
+
throw new Error('AI response is not an array');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Validate that each resource has required fields
|
|
468
|
+
for (const resource of selectedResources) {
|
|
469
|
+
if (!resource.kind || !resource.apiVersion) {
|
|
470
|
+
throw new Error(`AI selected invalid resource: ${JSON.stringify(resource)}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return selectedResources;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
throw new Error(`AI failed to select resources in valid JSON format. Error: ${(error as Error).message}. AI response: "${response.content.substring(0, 200)}..."`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Phase 2: Fetch detailed schemas for selected candidates
|
|
482
|
+
*/
|
|
483
|
+
private async fetchDetailedSchemas(candidates: any[], explainResource: (resource: string) => Promise<any>): Promise<ResourceSchema[]> {
|
|
484
|
+
const schemas: ResourceSchema[] = [];
|
|
485
|
+
const errors: string[] = [];
|
|
486
|
+
|
|
487
|
+
for (const resource of candidates) {
|
|
488
|
+
try {
|
|
489
|
+
const explanation = await explainResource(resource.kind);
|
|
490
|
+
|
|
491
|
+
// Parse GROUP, KIND, VERSION from kubectl explain output
|
|
492
|
+
const lines = explanation.split('\n');
|
|
493
|
+
const groupLine = lines.find((line: string) => line.startsWith('GROUP:'));
|
|
494
|
+
const kindLine = lines.find((line: string) => line.startsWith('KIND:'));
|
|
495
|
+
const versionLine = lines.find((line: string) => line.startsWith('VERSION:'));
|
|
496
|
+
|
|
497
|
+
const group = groupLine ? groupLine.replace('GROUP:', '').trim() : '';
|
|
498
|
+
const kind = kindLine ? kindLine.replace('KIND:', '').trim() : resource.kind;
|
|
499
|
+
const version = versionLine ? versionLine.replace('VERSION:', '').trim() : 'v1';
|
|
500
|
+
|
|
501
|
+
// Build apiVersion from group and version
|
|
502
|
+
const apiVersion = group ? `${group}/${version}` : version;
|
|
503
|
+
|
|
504
|
+
// Create a simple schema with raw explanation for AI processing
|
|
505
|
+
const schema: ResourceSchema = {
|
|
506
|
+
kind: kind,
|
|
507
|
+
apiVersion: apiVersion,
|
|
508
|
+
group: group,
|
|
509
|
+
description: explanation.split('\n').find((line: string) => line.startsWith('DESCRIPTION:'))?.replace('DESCRIPTION:', '').trim() || '',
|
|
510
|
+
properties: new Map<string, SchemaField>(),
|
|
511
|
+
rawExplanation: explanation // Include raw explanation for AI
|
|
512
|
+
};
|
|
513
|
+
schemas.push(schema);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
errors.push(`${resource.kind}: ${(error as Error).message}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (schemas.length === 0) {
|
|
520
|
+
throw new Error(`Could not fetch schemas for any selected resources. Candidates: ${candidates.map(c => c.kind).join(', ')}. Errors: ${errors.join(', ')}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (errors.length > 0) {
|
|
524
|
+
console.warn(`Some resources could not be analyzed: ${errors.join(', ')}`);
|
|
525
|
+
console.warn(`Successfully fetched schemas for: ${schemas.map(s => s.kind).join(', ')}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return schemas;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Phase 3: Rank resources with detailed schema information
|
|
533
|
+
*/
|
|
534
|
+
private async rankWithDetailedSchemas(intent: string, schemas: ResourceSchema[]): Promise<ResourceSolution[]> {
|
|
535
|
+
const prompt = await this.loadPromptTemplate(intent, schemas);
|
|
536
|
+
const response = await this.claudeIntegration.sendMessage(prompt);
|
|
537
|
+
const solutions = this.parseAISolutionResponse(response.content, schemas);
|
|
538
|
+
|
|
539
|
+
// Generate AI-powered questions for each solution
|
|
540
|
+
for (const solution of solutions) {
|
|
541
|
+
solution.questions = await this.generateQuestionsWithAI(intent, solution);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return solutions;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Load and format prompt template from file
|
|
549
|
+
*/
|
|
550
|
+
private async loadPromptTemplate(intent: string, schemas: ResourceSchema[]): Promise<string> {
|
|
551
|
+
const fs = await import('fs');
|
|
552
|
+
const path = await import('path');
|
|
553
|
+
|
|
554
|
+
const promptPath = path.join(process.cwd(), 'prompts', 'resource-solution-ranking.md');
|
|
555
|
+
const template = fs.readFileSync(promptPath, 'utf8');
|
|
556
|
+
|
|
557
|
+
// Format resources for the prompt
|
|
558
|
+
const resourcesText = schemas.map((schema, index) =>
|
|
559
|
+
`${index}: ${schema.kind} (${schema.apiVersion})
|
|
560
|
+
Group: ${schema.group || 'core'}
|
|
561
|
+
Description: ${schema.description}
|
|
562
|
+
Namespaced: ${schema.namespace}`
|
|
563
|
+
).join('\n\n');
|
|
564
|
+
|
|
565
|
+
return template
|
|
566
|
+
.replace('{intent}', intent)
|
|
567
|
+
.replace('{resources}', resourcesText);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Parse AI response into solution results
|
|
572
|
+
*/
|
|
573
|
+
private parseAISolutionResponse(aiResponse: string, schemas: ResourceSchema[]): ResourceSolution[] {
|
|
574
|
+
try {
|
|
575
|
+
// Use robust JSON extraction
|
|
576
|
+
const parsed = this.extractJsonFromAIResponse(aiResponse);
|
|
577
|
+
|
|
578
|
+
const solutions: ResourceSolution[] = parsed.solutions.map((solution: any) => {
|
|
579
|
+
const isDebugMode = process.env.DOT_AI_DEBUG === 'true';
|
|
580
|
+
|
|
581
|
+
if (isDebugMode) {
|
|
582
|
+
console.debug('DEBUG: solution object:', JSON.stringify(solution, null, 2));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Find matching schemas for the requested resources
|
|
586
|
+
const resources: ResourceSchema[] = [];
|
|
587
|
+
const notFound: any[] = [];
|
|
588
|
+
|
|
589
|
+
for (const requestedResource of solution.resources || []) {
|
|
590
|
+
const matchingSchema = schemas.find(schema =>
|
|
591
|
+
schema.kind === requestedResource.kind &&
|
|
592
|
+
schema.apiVersion === requestedResource.apiVersion &&
|
|
593
|
+
schema.group === requestedResource.group
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
if (matchingSchema) {
|
|
597
|
+
resources.push(matchingSchema);
|
|
598
|
+
} else {
|
|
599
|
+
notFound.push(requestedResource);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (resources.length === 0) {
|
|
604
|
+
if (isDebugMode) {
|
|
605
|
+
console.debug('DEBUG: No matching resources found');
|
|
606
|
+
console.debug('DEBUG: Requested resources:', solution.resources);
|
|
607
|
+
console.debug('DEBUG: Available schemas:', schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group })));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const debugInfo = {
|
|
611
|
+
requestedResources: solution.resources || [],
|
|
612
|
+
notFoundResources: notFound,
|
|
613
|
+
availableSchemas: schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group }))
|
|
614
|
+
};
|
|
615
|
+
throw new Error(`No matching resources found: ${JSON.stringify(debugInfo, null, 2)}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (notFound.length > 0 && isDebugMode) {
|
|
619
|
+
console.debug('DEBUG: Some resources not found:', notFound);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
type: solution.type,
|
|
624
|
+
resources,
|
|
625
|
+
score: solution.score,
|
|
626
|
+
description: solution.description,
|
|
627
|
+
reasons: solution.reasons || [],
|
|
628
|
+
analysis: solution.analysis || '',
|
|
629
|
+
questions: { required: [], basic: [], advanced: [], open: { question: '', placeholder: '' } }
|
|
630
|
+
};
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Sort by score descending
|
|
634
|
+
return solutions.sort((a, b) => b.score - a.score);
|
|
635
|
+
|
|
636
|
+
} catch (error) {
|
|
637
|
+
// Enhanced error message with more context
|
|
638
|
+
const errorMsg = `Failed to parse AI solution response: ${(error as Error).message}`;
|
|
639
|
+
const contextMsg = `\nAI Response (first 500 chars): "${aiResponse.substring(0, 500)}..."`;
|
|
640
|
+
const schemasMsg = `\nAvailable schemas: ${schemas.map(s => s.kind).join(', ')} (total: ${schemas.length})`;
|
|
641
|
+
throw new Error(errorMsg + contextMsg + schemasMsg);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Discover cluster options for dynamic question generation
|
|
647
|
+
*/
|
|
648
|
+
private async discoverClusterOptions(): Promise<ClusterOptions> {
|
|
649
|
+
try {
|
|
650
|
+
const { executeKubectl } = await import('./kubernetes-utils');
|
|
651
|
+
|
|
652
|
+
// Discover namespaces
|
|
653
|
+
const namespacesResult = await executeKubectl(['get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}']);
|
|
654
|
+
const namespaces = namespacesResult.split(/\s+/).filter(Boolean);
|
|
655
|
+
|
|
656
|
+
// Discover storage classes
|
|
657
|
+
let storageClasses: string[] = [];
|
|
658
|
+
try {
|
|
659
|
+
const storageResult = await executeKubectl(['get', 'storageclass', '-o', 'jsonpath={.items[*].metadata.name}']);
|
|
660
|
+
storageClasses = storageResult.split(/\s+/).filter(Boolean);
|
|
661
|
+
} catch {
|
|
662
|
+
// Storage classes might not be available in all clusters
|
|
663
|
+
storageClasses = [];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Discover ingress classes
|
|
667
|
+
let ingressClasses: string[] = [];
|
|
668
|
+
try {
|
|
669
|
+
const ingressResult = await executeKubectl(['get', 'ingressclass', '-o', 'jsonpath={.items[*].metadata.name}']);
|
|
670
|
+
ingressClasses = ingressResult.split(/\s+/).filter(Boolean);
|
|
671
|
+
} catch {
|
|
672
|
+
// Ingress classes might not be available
|
|
673
|
+
ingressClasses = [];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Get common node labels
|
|
677
|
+
let nodeLabels: string[] = [];
|
|
678
|
+
try {
|
|
679
|
+
const nodesResult = await executeKubectl(['get', 'nodes', '-o', 'json']);
|
|
680
|
+
const nodes = JSON.parse(nodesResult);
|
|
681
|
+
const labelSet = new Set<string>();
|
|
682
|
+
|
|
683
|
+
nodes.items?.forEach((node: any) => {
|
|
684
|
+
Object.keys(node.metadata?.labels || {}).forEach(label => {
|
|
685
|
+
if (!label.startsWith('kubernetes.io/') && !label.startsWith('node.kubernetes.io/')) {
|
|
686
|
+
labelSet.add(label);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
nodeLabels = Array.from(labelSet);
|
|
692
|
+
} catch {
|
|
693
|
+
nodeLabels = [];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
namespaces,
|
|
698
|
+
storageClasses,
|
|
699
|
+
ingressClasses,
|
|
700
|
+
nodeLabels
|
|
701
|
+
};
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.warn('Failed to discover cluster options, using defaults:', error);
|
|
704
|
+
return {
|
|
705
|
+
namespaces: ['default'],
|
|
706
|
+
storageClasses: [],
|
|
707
|
+
ingressClasses: [],
|
|
708
|
+
nodeLabels: []
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Extract JSON object from AI response with robust parsing
|
|
715
|
+
*/
|
|
716
|
+
private extractJsonFromAIResponse(aiResponse: string): any {
|
|
717
|
+
let jsonContent = aiResponse;
|
|
718
|
+
|
|
719
|
+
// First try to find JSON wrapped in code blocks
|
|
720
|
+
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
721
|
+
if (codeBlockMatch) {
|
|
722
|
+
jsonContent = codeBlockMatch[1];
|
|
723
|
+
} else {
|
|
724
|
+
// Try to find JSON that starts with { and find the matching closing }
|
|
725
|
+
const startIndex = aiResponse.indexOf('{');
|
|
726
|
+
if (startIndex !== -1) {
|
|
727
|
+
let braceCount = 0;
|
|
728
|
+
let endIndex = startIndex;
|
|
729
|
+
|
|
730
|
+
for (let i = startIndex; i < aiResponse.length; i++) {
|
|
731
|
+
if (aiResponse[i] === '{') braceCount++;
|
|
732
|
+
if (aiResponse[i] === '}') braceCount--;
|
|
733
|
+
if (braceCount === 0) {
|
|
734
|
+
endIndex = i;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (braceCount === 0) {
|
|
740
|
+
jsonContent = aiResponse.substring(startIndex, endIndex + 1);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return JSON.parse(jsonContent.trim());
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Generate contextual questions using AI based on user intent and solution resources
|
|
750
|
+
*/
|
|
751
|
+
private async generateQuestionsWithAI(intent: string, solution: ResourceSolution): Promise<QuestionGroup> {
|
|
752
|
+
try {
|
|
753
|
+
// Discover cluster options for dynamic questions
|
|
754
|
+
const clusterOptions = await this.discoverClusterOptions();
|
|
755
|
+
|
|
756
|
+
// Format resource details for the prompt using raw explanation when available
|
|
757
|
+
const resourceDetails = solution.resources.map(resource => {
|
|
758
|
+
if (resource.rawExplanation) {
|
|
759
|
+
// Use raw kubectl explain output for comprehensive field information
|
|
760
|
+
return `${resource.kind} (${resource.apiVersion}):
|
|
761
|
+
Description: ${resource.description}
|
|
762
|
+
|
|
763
|
+
Complete Schema Information:
|
|
764
|
+
${resource.rawExplanation}`;
|
|
765
|
+
} else {
|
|
766
|
+
// Fallback to properties map if raw explanation is not available
|
|
767
|
+
const properties = Array.from(resource.properties.entries()).map(([key, field]) => {
|
|
768
|
+
const nestedFields = Array.from(field.nested.entries()).map(([nestedKey, nestedField]) =>
|
|
769
|
+
` ${nestedKey}: ${nestedField.type} - ${nestedField.description}`
|
|
770
|
+
).join('\n');
|
|
771
|
+
|
|
772
|
+
return ` ${key}: ${field.type} - ${field.description}${field.required ? ' (required)' : ''}${nestedFields ? '\n' + nestedFields : ''}`;
|
|
773
|
+
}).join('\n');
|
|
774
|
+
|
|
775
|
+
return `${resource.kind} (${resource.apiVersion}):
|
|
776
|
+
Description: ${resource.description}
|
|
777
|
+
Required fields: ${resource.required?.join(', ') || 'none specified'}
|
|
778
|
+
Properties:
|
|
779
|
+
${properties}`;
|
|
780
|
+
}
|
|
781
|
+
}).join('\n\n');
|
|
782
|
+
|
|
783
|
+
// Format cluster options for the prompt
|
|
784
|
+
const clusterOptionsText = `Available Namespaces: ${clusterOptions.namespaces.join(', ')}
|
|
785
|
+
Available Storage Classes: ${clusterOptions.storageClasses.length > 0 ? clusterOptions.storageClasses.join(', ') : 'None discovered'}
|
|
786
|
+
Available Ingress Classes: ${clusterOptions.ingressClasses.length > 0 ? clusterOptions.ingressClasses.join(', ') : 'None discovered'}
|
|
787
|
+
Available Node Labels: ${clusterOptions.nodeLabels.length > 0 ? clusterOptions.nodeLabels.slice(0, 10).join(', ') : 'None discovered'}`;
|
|
788
|
+
|
|
789
|
+
// Load and format the question generation prompt
|
|
790
|
+
const fs = await import('fs');
|
|
791
|
+
const path = await import('path');
|
|
792
|
+
|
|
793
|
+
const promptPath = path.join(process.cwd(), 'prompts', 'question-generation.md');
|
|
794
|
+
const template = fs.readFileSync(promptPath, 'utf8');
|
|
795
|
+
|
|
796
|
+
const questionPrompt = template
|
|
797
|
+
.replace('{intent}', intent)
|
|
798
|
+
.replace('{solution_description}', solution.description)
|
|
799
|
+
.replace('{resource_details}', resourceDetails)
|
|
800
|
+
.replace('{cluster_options}', clusterOptionsText);
|
|
801
|
+
|
|
802
|
+
const response = await this.claudeIntegration.sendMessage(questionPrompt);
|
|
803
|
+
|
|
804
|
+
// Use robust JSON extraction
|
|
805
|
+
const questions = this.extractJsonFromAIResponse(response.content);
|
|
806
|
+
|
|
807
|
+
// Validate the response structure
|
|
808
|
+
if (!questions.required || !questions.basic || !questions.advanced || !questions.open) {
|
|
809
|
+
throw new Error('Invalid question structure from AI');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return questions as QuestionGroup;
|
|
813
|
+
} catch (error) {
|
|
814
|
+
console.warn(`Failed to generate AI questions for solution: ${error}`);
|
|
815
|
+
|
|
816
|
+
// Fallback to basic open question
|
|
817
|
+
return {
|
|
818
|
+
required: [],
|
|
819
|
+
basic: [],
|
|
820
|
+
advanced: [],
|
|
821
|
+
open: {
|
|
822
|
+
question: "Is there anything else about your requirements or constraints that would help us provide better recommendations?",
|
|
823
|
+
placeholder: "e.g., specific security requirements, performance needs, existing infrastructure constraints..."
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|