UNPKG

@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
"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; } }