@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
309 lines (308 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeIntent = analyzeIntent;
const mcp_client_registry_1 = require("../core/mcp-client-registry");
const ai_provider_factory_1 = require("../core/ai-provider-factory");
const shared_prompt_loader_1 = require("../core/shared-prompt-loader");
const visualization_1 = require("../core/visualization");
const operate_1 = require("./operate");
/**
* Analyzes user intent and generates operational proposal using AI tool loop
*
* PRD #343: pluginManager is required - all kubectl operations go through plugin.
*
* @param intent - User's operational intent (e.g., "update my-api to v2.0")
* @param logger - Logger instance
* @param sessionManager - Session manager instance
* @param pluginManager - Plugin manager for kubectl operations
* @param sessionId - Optional session ID for refinement
* @param interaction_id - Optional interaction ID for eval datasets
* @returns Operation output with proposed changes
*/
async function analyzeIntent(intent, logger, sessionManager, pluginManager, sessionId, interaction_id) {
logger.info('Starting operate analysis', { intent, sessionId });
// 1. Embed context (patterns, policies, capabilities)
const context = await (0, operate_1.embedContext)(intent, logger);
// 2. Load prompts (static system + dynamic user message)
const systemPrompt = loadSystemPrompt();
const userMessage = buildUserMessage(intent, context);
// 3. Execute AI tool loop with kubectl tools (PRD #343: via plugin)
const aiResult = await executeToolLoop(systemPrompt, userMessage, logger, pluginManager, interaction_id);
// 4. Parse AI response into structured format
const proposedChanges = parseAIResponse(aiResult, logger);
// 5. Create and save session
const session = await saveAnalysisSession(intent, context, proposedChanges, sessionManager, sessionId, interaction_id, logger);
logger.info('Operate analysis complete', { sessionId: session.sessionId });
// PRD #320: Generate visualization URL for analysis response
const visualizationUrl = (0, visualization_1.getVisualizationUrl)(session.sessionId);
// 6. Return formatted output for user
return {
status: 'awaiting_user_approval',
sessionId: session.sessionId,
...(visualizationUrl && { visualizationUrl }), // PRD #320: Include visualization URL if WEB_UI_BASE_URL is set
analysis: {
summary: proposedChanges.analysis,
currentState: proposedChanges.currentState,
proposedChanges: session.data.proposedChanges,
commands: session.data.commands,
dryRunValidation: session.data.dryRunValidation,
patternsApplied: session.data.patternsApplied,
capabilitiesUsed: session.data.capabilitiesUsed,
policiesChecked: session.data.policiesChecked,
risks: session.data.risks,
validationIntent: session.data.validationIntent,
},
message: 'Operational proposal generated successfully. Review changes and execute with operate(sessionId, executeChoice=1).',
agentInstructions: `Review the proposed changes. You can call operate({ sessionId: "${session.sessionId}", executeChoice: 1 }) to execute directly, or run impact_analysis first to understand the blast radius and downstream dependencies before executing.`,
};
}
/**
* Loads static system prompt from prompts/operate-system.md
* This prompt is cacheable across all operate calls
*/
function loadSystemPrompt() {
return (0, shared_prompt_loader_1.loadPrompt)('operate-system');
}
/**
* Builds dynamic user message with intent and embedded context
* Uses template from prompts/operate-user.md and formatting functions from operate.ts
*/
function buildUserMessage(intent, context) {
// Format context sections using shared formatting functions
const patternsText = (0, operate_1.formatPatterns)(context.patterns);
const policiesText = (0, operate_1.formatPolicies)(context.policies);
const capabilitiesText = (0, operate_1.formatCapabilities)(context.capabilities);
// Use loadPrompt with Handlebars template variables
return (0, shared_prompt_loader_1.loadPrompt)('operate-user', {
intent,
patterns: patternsText,
policies: policiesText,
capabilities: capabilitiesText,
});
}
/** Kubectl and Helm tool names for investigation and dry-run validation */
const KUBECTL_INVESTIGATION_TOOL_NAMES = [
'kubectl_get',
'kubectl_describe',
'kubectl_logs',
'kubectl_events',
'kubectl_api_resources',
'kubectl_get_crd_schema',
'kubectl_get_resource_json',
// Dry-run tools for validation
'kubectl_patch_dryrun',
'kubectl_apply_dryrun',
'kubectl_delete_dryrun',
// Helm investigation tools (PRD #251: Helm Day-2 operations)
'helm_list',
'helm_status',
'helm_history',
'helm_get_values',
// Helm dry-run validation (PRD #251)
'helm_install_dryrun',
];
/**
* Executes AI tool loop with kubectl investigation tools
* AI autonomously inspects cluster and validates changes with dry-run
*
* PRD #343: Kubectl tools are routed through the plugin system.
*
* @param systemPrompt - Static instructions (cacheable)
* @param userMessage - Dynamic content with intent and context
* @param logger - Logger instance
* @param pluginManager - Plugin manager for kubectl operations
* @param interaction_id - Optional interaction ID for eval datasets
* @returns AI's final response
* @throws Error if AI fails to converge within 30 iterations
*/
async function executeToolLoop(systemPrompt, userMessage, logger, pluginManager, interaction_id) {
logger.debug('Starting AI tool loop for operate analysis');
// PRD #343: Get kubectl tools from plugin
const kubectlTools = pluginManager
.getDiscoveredTools()
.filter(t => KUBECTL_INVESTIGATION_TOOL_NAMES.includes(t.name));
if (kubectlTools.length === 0) {
throw new Error('No kubectl tools available from plugin. Ensure agentic-tools plugin is running.');
}
// PRD #358: Get MCP server tools attached to operate
const mcpTools = (0, mcp_client_registry_1.isMcpClientInitialized)()
? (0, mcp_client_registry_1.getMcpClientManager)().getToolsForOperation('operate')
: [];
const allTools = [...kubectlTools, ...mcpTools];
logger.debug('Using investigation tools', {
kubectlToolCount: kubectlTools.length,
mcpToolCount: mcpTools.length,
tools: allTools.map(t => t.name),
});
// PRD #343: Create tool executor that routes through plugin
// PRD #358: Chain MCP executor with plugin executor as fallback
const pluginExecutor = pluginManager.createToolExecutor();
const toolExecutor = (0, mcp_client_registry_1.isMcpClientInitialized)()
? (0, mcp_client_registry_1.getMcpClientManager)().createToolExecutor(pluginExecutor)
: pluginExecutor;
const aiProvider = (0, ai_provider_factory_1.createAIProvider)();
const result = await aiProvider.toolLoop({
systemPrompt,
userMessage,
tools: allTools,
toolExecutor: toolExecutor,
maxIterations: 30,
operation: 'operate-analysis',
evaluationContext: {
user_intent: userMessage.substring(0, 200), // First 200 chars as context
},
interaction_id,
});
logger.debug('AI tool loop completed', {
iterations: result.iterations,
toolCallsExecuted: result.toolCallsExecuted.length,
responseLength: result.finalMessage.length,
});
return result.finalMessage;
}
/**
* Parses AI response into structured ProposedChanges format
* Enforces strict JSON parsing with validation
*
* @param response - AI's final response
* @param logger - Logger instance
* @returns Parsed proposed changes
* @throws Error if response is not valid JSON or missing required fields
*/
function parseAIResponse(response, logger) {
logger.debug('Parsing AI response');
// Try to extract JSON from code block first (Claude format)
const jsonMatch = response.match(/```json\n([\s\S]+?)\n```/);
let jsonContent;
if (jsonMatch) {
jsonContent = jsonMatch[1];
}
else {
// Fallback: try to parse raw JSON response (Gemini format)
// Look for JSON object starting with { and ending with }
const rawJsonMatch = response.match(/^\s*(\{[\s\S]*\})\s*$/);
if (rawJsonMatch) {
jsonContent = rawJsonMatch[1];
logger.debug('Parsing raw JSON response (no code block wrapper)');
}
else {
const truncatedResponse = response.substring(0, 500);
logger.error(`AI response not valid JSON. Response: ${truncatedResponse}`);
throw new Error('AI did not return structured JSON response. Expected JSON object or ```json code block.');
}
}
try {
const parsed = JSON.parse(jsonContent);
// Validate required fields
if (!parsed.analysis || typeof parsed.analysis !== 'string') {
throw new Error('AI response missing required "analysis" field (string)');
}
if (!parsed.commands || !Array.isArray(parsed.commands)) {
throw new Error('AI response missing required "commands" array');
}
if (parsed.commands.length === 0) {
throw new Error('AI response has empty "commands" array - no operations proposed');
}
if (!parsed.dryRunValidation ||
typeof parsed.dryRunValidation !== 'object') {
throw new Error('AI response missing required "dryRunValidation" object');
}
// Trust AI's claim but log for audit trail
logger.info('AI dry-run validation status', {
validation: parsed.dryRunValidation,
status: parsed.dryRunValidation.status,
});
// Ensure proposedChanges structure exists
if (!parsed.proposedChanges) {
parsed.proposedChanges = { create: [], update: [], delete: [] };
}
// Validate proposedChanges structure
const changes = parsed.proposedChanges;
if (!Array.isArray(changes.create))
changes.create = [];
if (!Array.isArray(changes.update))
changes.update = [];
if (!Array.isArray(changes.delete))
changes.delete = [];
// Ensure metadata arrays exist
if (!Array.isArray(parsed.patternsApplied))
parsed.patternsApplied = [];
if (!Array.isArray(parsed.capabilitiesUsed))
parsed.capabilitiesUsed = [];
if (!Array.isArray(parsed.policiesChecked))
parsed.policiesChecked = [];
// Ensure risks object exists
if (!parsed.risks) {
parsed.risks = {
level: 'low',
description: 'No specific risks identified',
};
}
// Ensure validationIntent exists
if (!parsed.validationIntent ||
typeof parsed.validationIntent !== 'string') {
parsed.validationIntent =
'Validate that the operation completed successfully';
}
logger.debug('AI response parsed successfully', {
commandCount: parsed.commands.length,
createCount: changes.create.length,
updateCount: changes.update.length,
deleteCount: changes.delete.length,
});
return parsed;
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error(`Failed to parse AI response: ${errorMsg}`);
throw new Error(`Invalid AI response format: ${errorMsg}`, {
cause: error,
});
}
}
/**
* Saves analysis session to disk using GenericSessionManager
*
* @param intent - User's operational intent
* @param context - Embedded context
* @param proposedChanges - Parsed AI proposal
* @param sessionManager - Session manager instance
* @param sessionId - Optional existing session ID for updates
* @param interaction_id - Optional interaction ID for eval datasets
* @param logger - Logger instance
* @returns Saved session
*/
async function saveAnalysisSession(intent, context, proposedChanges, sessionManager, sessionId, interaction_id, logger) {
const sessionData = {
toolName: 'operate', // PRD #320: Tool identifier for visualization prompt selection
intent,
interaction_id,
context,
proposedChanges: proposedChanges.proposedChanges,
commands: proposedChanges.commands,
dryRunValidation: proposedChanges.dryRunValidation,
patternsApplied: proposedChanges.patternsApplied,
capabilitiesUsed: proposedChanges.capabilitiesUsed,
policiesChecked: proposedChanges.policiesChecked,
risks: proposedChanges.risks,
validationIntent: proposedChanges.validationIntent,
status: 'analysis_complete',
};
if (sessionId) {
// Update existing session (refinement case)
logger.debug('Updating existing operate session', { sessionId });
await sessionManager.replaceSession(sessionId, sessionData);
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Failed to retrieve session ${sessionId} after update`);
}
return session;
}
else {
// Create new session
logger.debug('Creating new operate session');
const session = await sessionManager.createSession(sessionData);
logger.info('Operate session created', { sessionId: session.sessionId });
return session;
}
}