@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
270 lines (269 loc) • 13.2 kB
JavaScript
"use strict";
/**
* Query Tool - Natural Language Cluster Intelligence
*
* Provides natural language query interface to discover and understand
* cluster capabilities and resources.
*
* PRD #291: Cluster Query Tool - Natural Language Cluster Intelligence
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.QUERY_TOOL_INPUT_SCHEMA = exports.QUERY_TOOL_DESCRIPTION = exports.QUERY_TOOL_NAME = void 0;
exports.handleQueryTool = handleQueryTool;
const zod_1 = require("zod");
const error_handling_1 = require("../core/error-handling");
const ai_provider_factory_1 = require("../core/ai-provider-factory");
const capability_tools_1 = require("../core/capability-tools");
const resource_tools_1 = require("../core/resource-tools");
const mcp_client_registry_1 = require("../core/mcp-client-registry");
const generic_session_manager_1 = require("../core/generic-session-manager");
const visualization_1 = require("../core/visualization");
const mermaid_tools_1 = require("../core/mermaid-tools");
const shared_prompt_loader_1 = require("../core/shared-prompt-loader");
// Tool metadata for MCP registration
exports.QUERY_TOOL_NAME = 'query';
exports.QUERY_TOOL_DESCRIPTION = 'Natural language query interface for Kubernetes cluster intelligence. Ask any questions about your cluster resources, capabilities, and status in plain English. Examples: "What databases are running?", "Describe the nginx deployment", "Show me pods in the kube-system namespace", "What operators are installed?", "Is my-postgres healthy?"';
// Zod schema for MCP registration
exports.QUERY_TOOL_INPUT_SCHEMA = {
intent: zod_1.z.string().min(1).max(1000).describe('Natural language query about the cluster'),
interaction_id: zod_1.z.string().optional().describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.')
};
/**
* Parse the AI's final JSON response for summary only
*/
function parseSummary(aiResponse) {
try {
// Find JSON in the response
const firstBraceIndex = aiResponse.indexOf('{');
if (firstBraceIndex === -1) {
// No JSON found, use the response as summary
return aiResponse.trim() || 'No summary provided';
}
// Track brace depth to find complete JSON object
let braceCount = 0;
let inString = false;
let escapeNext = false;
let jsonEndIndex = -1;
for (let i = firstBraceIndex; i < aiResponse.length; i++) {
const char = aiResponse[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
if (char === '"') {
inString = !inString;
continue;
}
if (inString)
continue;
if (char === '{')
braceCount++;
if (char === '}') {
braceCount--;
if (braceCount === 0) {
jsonEndIndex = i + 1;
break;
}
}
}
if (jsonEndIndex === -1) {
return aiResponse.trim() || 'No summary provided';
}
const jsonString = aiResponse.substring(firstBraceIndex, jsonEndIndex);
const parsed = JSON.parse(jsonString);
return parsed.summary || 'No summary provided';
}
catch {
// If parsing fails, use the raw response as summary
return aiResponse.trim() || 'No summary provided';
}
}
async function handleQueryTool(args, pluginManager) {
const requestId = `query_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
const logger = new error_handling_1.ConsoleLogger('QueryTool');
try {
// Validate input
let intent = args.intent;
if (!intent || typeof intent !== 'string') {
throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, 'Intent is required and must be a string', { operation: 'input_validation', component: 'QueryTool' });
}
// Detect visualization mode and strip prefix
const visualizationMode = intent.startsWith(visualization_1.VISUALIZATION_PREFIX);
if (visualizationMode) {
intent = intent.slice(visualization_1.VISUALIZATION_PREFIX.length).trim();
}
logger.info('Processing query', { requestId, intent, visualizationMode });
// Initialize AI provider
const aiProvider = (0, ai_provider_factory_1.createAIProvider)();
// Load system prompt with appropriate output instructions
const systemPrompt = (0, shared_prompt_loader_1.loadPrompt)('query-system', {
outputInstructions: visualizationMode
? (0, shared_prompt_loader_1.loadPrompt)('partials/visualization-output')
: (0, shared_prompt_loader_1.loadPrompt)('partials/query-simple-output')
});
// Local executor for non-plugin tools (capability, resource, mermaid)
const localToolExecutor = async (toolName, input) => {
if (toolName.startsWith('search_capabilities') || toolName.startsWith('query_capabilities')) {
return (0, capability_tools_1.executeCapabilityTools)(toolName, input);
}
if (toolName.startsWith('search_resources') || toolName.startsWith('query_resources')) {
return (0, resource_tools_1.executeResourceTools)(toolName, input);
}
if (toolName === 'validate_mermaid') {
return (0, mermaid_tools_1.executeMermaidTools)(toolName, input);
}
return {
success: false,
error: `Unknown tool: ${toolName}`,
message: `Tool '${toolName}' is not implemented in query tool`
};
};
// PRD #343: Use plugin executor when pluginManager is available
// kubectl tools route through plugin HTTP, others use local executor
// PRD #358: Chain MCP executor with plugin executor as fallback
const pluginExecutor = pluginManager
? pluginManager.createToolExecutor(localToolExecutor)
: localToolExecutor;
const executeQueryTools = (0, mcp_client_registry_1.isMcpClientInitialized)()
? (0, mcp_client_registry_1.getMcpClientManager)().createToolExecutor(pluginExecutor)
: pluginExecutor;
// PRD #343: Get kubectl tools from plugin (read-only tools for query)
// Only include kubectl tools when plugin provides them
const KUBECTL_READONLY_TOOL_NAMES = [
'kubectl_api_resources',
'kubectl_get',
'kubectl_describe',
'kubectl_logs',
'kubectl_events',
'kubectl_get_crd_schema'
];
const pluginKubectlTools = pluginManager
? pluginManager.getDiscoveredTools().filter(t => KUBECTL_READONLY_TOOL_NAMES.includes(t.name))
: [];
// PRD #358: Get MCP server tools attached to query
const mcpTools = (0, mcp_client_registry_1.isMcpClientInitialized)()
? (0, mcp_client_registry_1.getMcpClientManager)().getToolsForOperation('query')
: [];
// Build tool list - add mermaid tools when in visualization mode
// kubectl tools only available when plugin is configured
// MCP tools added when MCP servers are configured
const tools = visualizationMode
? [...capability_tools_1.CAPABILITY_TOOLS, ...resource_tools_1.RESOURCE_TOOLS, ...pluginKubectlTools, ...mcpTools, ...mermaid_tools_1.MERMAID_TOOLS]
: [...capability_tools_1.CAPABILITY_TOOLS, ...resource_tools_1.RESOURCE_TOOLS, ...pluginKubectlTools, ...mcpTools];
// Execute tool loop with capability, resource, kubectl, and MCP tools
const result = await aiProvider.toolLoop({
systemPrompt,
userMessage: intent,
tools,
toolExecutor: executeQueryTools,
maxIterations: 30,
operation: 'query',
evaluationContext: {
user_intent: intent
},
interaction_id: args.interaction_id
});
// Extract data from execution record (reliable, not AI self-reporting)
const toolsUsed = [...new Set(result.toolCallsExecuted.map(tc => tc.tool))];
logger.info('Query completed', {
requestId,
iterations: result.iterations,
toolsUsed,
visualizationMode
});
// Guard: if the AI call did not succeed, surface the real error instead of trying to parse
if (result.status && result.status !== 'success') {
throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.AI_SERVICE, error_handling_1.ErrorSeverity.HIGH, `Query ${result.status}: ${result.finalMessage}`, { operation: 'query_tool_execution', component: 'QueryTool', isRetryable: true, requestId, input: { intent } });
}
// Handle visualization mode - return visualization response with sessionId for caching
if (visualizationMode) {
const visualizationResponse = (0, visualization_1.parseVisualizationResponse)(result.finalMessage, toolsUsed);
// Create session with cached visualization for URL caching/bookmarking (PRD #328)
const sessionManager = new generic_session_manager_1.GenericSessionManager('qry');
const session = sessionManager.createSession({
toolName: 'query',
intent,
summary: visualizationResponse.title, // Use title as summary for visualization sessions
toolsUsed,
iterations: result.iterations,
toolCallsExecuted: result.toolCallsExecuted,
cachedVisualization: {
title: visualizationResponse.title,
visualizations: visualizationResponse.visualizations,
insights: visualizationResponse.insights,
generatedAt: new Date().toISOString()
}
});
logger.info('Visualization session created', {
requestId,
sessionId: session.sessionId
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
sessionId: session.sessionId,
...visualizationResponse
}, null, 2)
}
]
};
}
// Normal mode - return summary with session for later visualization
const summary = parseSummary(result.finalMessage);
// Store session for visualization (PRD #317, PRD #320)
const sessionManager = new generic_session_manager_1.GenericSessionManager('qry');
const session = sessionManager.createSession({
toolName: 'query', // PRD #320: Tool identifier for visualization endpoint
intent,
summary,
toolsUsed,
iterations: result.iterations,
toolCallsExecuted: result.toolCallsExecuted
});
// PRD #317: Include visualization URL when WEB_UI_BASE_URL is configured
const visualizationUrl = (0, visualization_1.getVisualizationUrl)(session.sessionId);
logger.info('Session created for visualization', {
requestId,
sessionId: session.sessionId,
...(visualizationUrl && { visualizationUrl })
});
const agentInstructions = visualizationUrl
? 'Present the summary to the user. Include the visualizationUrl at the end of your response. For dependency or impact questions, suggest the user run impact_analysis to get detailed blast radius analysis.'
: 'Present the summary to the user. For dependency or impact questions, suggest the user run impact_analysis to get detailed blast radius analysis.';
const output = {
success: true,
summary,
toolsUsed,
iterations: result.iterations,
sessionId: session.sessionId,
...(visualizationUrl && { visualizationUrl }),
agentInstructions
};
return {
content: [
{
type: 'text',
text: JSON.stringify(output, null, 2)
}
]
};
}
catch (error) {
logger.error('Query failed', error, { requestId });
if (error instanceof Error && 'category' in error) {
throw error;
}
throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.UNKNOWN, error_handling_1.ErrorSeverity.HIGH, `Query tool failed: ${error instanceof Error ? error.message : 'Unknown error'}`, {
operation: 'query_tool_execution',
component: 'QueryTool',
requestId,
input: { intent: args.intent }
});
}
}