UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

364 lines (355 loc) 14.5 kB
"use strict"; /** * Resource Tools for AI-Powered Cluster Intelligence * * Shared tool definitions and executor for resource vector DB operations. * Used by query and other cluster intelligence workflows. * * PRD #291: Cluster Query Tool - Natural Language Cluster Intelligence */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RESOURCE_TOOLS = exports.QUERY_RESOURCES_TOOL = exports.SEARCH_RESOURCES_TOOL = void 0; exports.getResourceService = getResourceService; exports.executeResourceTools = executeResourceTools; exports.resetResourceService = resetResourceService; exports.getResourceKinds = getResourceKinds; exports.listResources = listResources; exports.getNamespaces = getNamespaces; const resource_vector_service_1 = require("./resource-vector-service"); const validation_1 = require("./constants/validation"); /** * Tool: search_resources * Semantic search for cluster resources by name, kind, labels, and annotations */ exports.SEARCH_RESOURCES_TOOL = { name: 'search_resources', description: `Search for Kubernetes resources in the cluster inventory using semantic search. Searches resource names, kinds, labels, and annotations stored in Vector DB. This tool is useful for: - Finding resources by partial name match (e.g., "nginx" finds nginx-deployment, nginx-service) - Finding resources by kind (e.g., "deployments" finds all Deployment resources) - Finding resources by label values (e.g., "frontend" finds resources with tier=frontend label) - Finding resources by annotation content (e.g., "team platform" finds resources with team=platform annotation) You can optionally filter results by namespace, kind, or apiVersion to narrow the search scope: - Search "nginx" within namespace "production": query="nginx", namespace="production" - Search "database" within Deployments only: query="database", kind="Deployment", apiVersion="apps/v1" - Search across all resources: query="nginx" (no filters) Note: For conceptual queries like "what databases are running", use the semantic bridge pattern: 1. Use search_capabilities to find what KINDS relate to "database" 2. Then use query_resources to find instances of those kinds For live cluster status, use kubectl tools after finding resources.`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query matching resource names, kinds, labels, or annotations (e.g., "nginx", "frontend", "team platform")' }, namespace: { type: 'string', description: 'Optional: Filter results to this namespace only (exact match)' }, kind: { type: 'string', description: 'Optional: Filter results to this resource kind only (exact match, e.g., "Deployment", "Service")' }, apiVersion: { type: 'string', description: 'Optional: Filter results to this API version only (exact match, e.g., "apps/v1", "v1")' }, limit: { type: 'number', description: 'Maximum results to return (default: 10)' } }, required: ['query'] } }; /** * Tool: query_resources * Filter-based query for resources using Qdrant filter syntax */ exports.QUERY_RESOURCES_TOOL = { name: 'query_resources', description: `Query cluster resources using Qdrant filter syntax. Use this when you need to filter by specific fields like kind, namespace, or labels. This is the primary tool for finding resource instances after using search_capabilities to identify relevant kinds. Available payload fields for filtering: - id: string (format: namespace:apiVersion:kind:name) - namespace: string (e.g., "default", "kube-system", "_cluster" for cluster-scoped) - name: string (resource name) - kind: string (e.g., "Deployment", "Service", "Pod", "StatefulSet") - apiVersion: string (e.g., "apps/v1", "v1") - apiGroup: string (e.g., "apps", "", "postgresql.cnpg.io") - labels: object (e.g., { "app": "nginx", "env": "prod" }) - annotations: object (resource annotations) - createdAt: string (ISO timestamp) - updatedAt: string (ISO timestamp) Qdrant filter syntax examples: - Filter by kind: { "must": [{ "key": "kind", "match": { "value": "Deployment" } }] } - Filter by namespace: { "must": [{ "key": "namespace", "match": { "value": "default" } }] } - Filter by multiple kinds: { "must": [{ "key": "kind", "match": { "any": ["Deployment", "StatefulSet"] } }] } - Filter by label: { "must": [{ "key": "labels.app", "match": { "value": "nginx" } }] } - Combined filter: { "must": [{ "key": "kind", "match": { "value": "Deployment" } }, { "key": "namespace", "match": { "value": "production" } }] } Note: This queries the resource inventory in Vector DB, not live cluster state.`, inputSchema: { type: 'object', properties: { filter: { type: 'object', description: 'Qdrant filter object with must/should/must_not conditions' }, limit: { type: 'number', description: 'Maximum results to return (default: 100)' } }, required: ['filter'] } }; /** * All resource tools for cluster intelligence * Convenient array for passing to toolLoop() */ exports.RESOURCE_TOOLS = [ exports.SEARCH_RESOURCES_TOOL, exports.QUERY_RESOURCES_TOOL ]; /** * Shared ResourceVectorService instance for tool execution * Initialized lazily on first use */ let resourceService = null; /** * Get or create the resource vector service * Uses lazy initialization to avoid startup errors when Qdrant isn't ready * Respects QDRANT_RESOURCES_COLLECTION env var for collection name */ async function getResourceService() { if (!resourceService) { const collectionName = process.env.QDRANT_RESOURCES_COLLECTION || 'resources'; resourceService = new resource_vector_service_1.ResourceVectorService(collectionName); await resourceService.initialize(); } return resourceService; } /** * Tool executor for resource-based tools * Handles execution and error handling for all resource tool calls * * @param toolName - Name of the tool to execute * @param input - Tool input parameters * @returns Tool execution result */ async function executeResourceTools(toolName, input) { try { switch (toolName) { case 'search_resources': { const { query, namespace, kind, apiVersion, limit = 10 } = input; if (!query) { return { success: false, error: validation_1.VALIDATION_MESSAGES.MISSING_PARAMETER('query'), message: 'search_resources requires a query parameter' }; } const service = await getResourceService(); // Build filters from optional parameters const filters = {}; if (namespace) filters.namespace = namespace; if (kind) filters.kind = kind; if (apiVersion) filters.apiVersion = apiVersion; // Use searchResources with filters const results = await service.searchResources(query, Object.keys(filters).length > 0 ? filters : undefined, limit); // Transform results to a clean format for AI consumption const formattedResources = results.map(({ resource: r, score }) => ({ id: r.id, namespace: r.namespace, name: r.name, kind: r.kind, apiVersion: r.apiVersion, apiGroup: r.apiGroup, labels: r.labels, createdAt: r.createdAt, updatedAt: r.updatedAt, score })); // Build message with filter info const filterInfo = Object.keys(filters).length > 0 ? ` (filtered by ${Object.entries(filters).map(([k, v]) => `${k}=${v}`).join(', ')})` : ''; return { success: true, data: formattedResources, count: formattedResources.length, message: `Found ${formattedResources.length} resources matching "${query}"${filterInfo}` }; } case 'query_resources': { const { filter, limit = 100 } = input; if (!filter) { return { success: false, error: validation_1.VALIDATION_MESSAGES.MISSING_PARAMETER('filter'), message: 'query_resources requires a filter parameter with Qdrant filter syntax' }; } const service = await getResourceService(); const results = await service.queryWithFilter(filter, limit); // Transform results to a clean format for AI consumption const resources = results.map(r => ({ id: r.id, namespace: r.namespace, name: r.name, kind: r.kind, apiVersion: r.apiVersion, apiGroup: r.apiGroup, labels: r.labels, createdAt: r.createdAt, updatedAt: r.updatedAt })); return { success: true, data: resources, count: resources.length, message: `Found ${resources.length} resources matching filter` }; } default: return { success: false, error: `Unknown resource tool: ${toolName}`, message: `Tool '${toolName}' is not implemented` }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, error: errorMessage, message: `Failed to execute ${toolName}: ${errorMessage}` }; } } /** * Reset the resource service (useful for testing) */ function resetResourceService() { resourceService = null; } /** * Get all unique resource kinds with counts * Groups resources by kind+apiGroup+apiVersion and counts each group * * @param namespace - Optional namespace to filter by * @returns Array of resource kinds sorted by count descending */ async function getResourceKinds(namespace) { const service = await getResourceService(); const allResources = await service.getAllData(); // Group by kind+apiGroup+apiVersion const kindMap = new Map(); for (const resource of allResources) { // Filter by namespace if provided if (namespace !== undefined && resource.namespace !== namespace) { continue; } const apiGroup = resource.apiGroup || ''; const key = `${resource.kind}:${apiGroup}:${resource.apiVersion}`; const existing = kindMap.get(key); if (existing) { existing.count++; } else { kindMap.set(key, { kind: resource.kind, apiGroup, apiVersion: resource.apiVersion, count: 1 }); } } // Sort by count descending return Array.from(kindMap.values()).sort((a, b) => b.count - a.count); } /** * List resources with filtering and pagination * PRD #343: Status fetching moved to REST API layer via plugin * * @param options - Filter and pagination options * @returns Paginated list of resources */ async function listResources(options) { const { kind, apiGroup, apiVersion, namespace, limit = 100, offset = 0 } = options; // Clamp limit to max 1000 const effectiveLimit = Math.min(Math.max(1, limit), 1000); const effectiveOffset = Math.max(0, offset); const service = await getResourceService(); const allResources = await service.getAllData(); // Filter resources const filtered = allResources.filter(resource => { // Kind filter (required) if (resource.kind !== kind) { return false; } // API group filter (optional) if (apiGroup !== undefined) { const resourceApiGroup = resource.apiGroup || ''; if (resourceApiGroup !== apiGroup) { return false; } } // API version filter (optional) - exact match if (apiVersion !== undefined && resource.apiVersion !== apiVersion) { return false; } // Namespace filter (optional) if (namespace !== undefined && resource.namespace !== namespace) { return false; } return true; }); // Get total count before pagination const total = filtered.length; // Apply pagination const paginated = filtered.slice(effectiveOffset, effectiveOffset + effectiveLimit); // Transform to response format const resources = paginated.map(r => ({ name: r.name, namespace: r.namespace, kind: r.kind, apiGroup: r.apiGroup || '', apiVersion: r.apiVersion, labels: r.labels || {}, createdAt: r.createdAt, updatedAt: r.updatedAt })); return { resources, total, limit: effectiveLimit, offset: effectiveOffset }; } /** * Get all unique namespaces from resources * Filters out '_cluster' marker for cluster-scoped resources * * @returns Sorted array of namespace names */ async function getNamespaces() { const service = await getResourceService(); const allResources = await service.getAllData(); // Extract unique namespaces const namespaceSet = new Set(); for (const resource of allResources) { // Filter out '_cluster' marker for cluster-scoped resources if (resource.namespace && resource.namespace !== '_cluster') { namespaceSet.add(resource.namespace); } } // Sort alphabetically return Array.from(namespaceSet).sort(); }