UNPKG

@vfarcic/dot-ai

Version:

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

350 lines (349 loc) 12.8 kB
"use strict"; /** * Resource Vector Service * * Vector-based storage and retrieval for Kubernetes cluster resources. * Extends BaseVectorService to provide resource-specific operations. * * This service receives resource data from the dot-ai-controller and stores * it in Qdrant for semantic search capabilities. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ResourceVectorService = void 0; exports.extractApiGroup = extractApiGroup; exports.buildEmbeddingText = buildEmbeddingText; exports.generateResourceId = generateResourceId; exports.generateResourceUuid = generateResourceUuid; exports.hasResourceChanged = hasResourceChanged; const crypto_1 = require("crypto"); const base_vector_service_1 = require("./base-vector-service"); /** * Extract API group from apiVersion * e.g., 'apps/v1' -> 'apps', 'v1' -> '' */ function extractApiGroup(apiVersion) { const parts = apiVersion.split('/'); return parts.length > 1 ? parts[0] : ''; } /** * Build embedding text from resource data * Creates a semantic representation for vector search */ function buildEmbeddingText(resource) { const parts = [ `${resource.kind} ${resource.name}`, `namespace: ${resource.namespace}`, `apiVersion: ${resource.apiVersion}`, ]; // Add API group if present const apiGroup = resource.apiGroup || extractApiGroup(resource.apiVersion); if (apiGroup) { parts.push(`group: ${apiGroup}`); } // Add meaningful labels (skip standard Kubernetes labels) if (resource.labels && Object.keys(resource.labels).length > 0) { const meaningfulLabels = Object.entries(resource.labels) .filter(([k]) => { // Skip standard Kubernetes labels that don't add semantic value const skipPrefixes = [ 'app.kubernetes.io/', 'helm.sh/', 'kubernetes.io/', 'k8s.io/', ]; return !skipPrefixes.some(prefix => k.startsWith(prefix)); }) .map(([k, v]) => `${k}=${v}`); if (meaningfulLabels.length > 0) { parts.push(`labels: ${meaningfulLabels.join(', ')}`); } // Also include app name from standard labels if present const appName = resource.labels['app.kubernetes.io/name'] || resource.labels['app'] || resource.labels['name']; if (appName) { parts.push(`app: ${appName}`); } } // Add meaningful annotations (skip standard Kubernetes/system annotations) if (resource.annotations && Object.keys(resource.annotations).length > 0) { const meaningfulAnnotations = Object.entries(resource.annotations) .filter(([k, v]) => { // Skip system annotations that don't add semantic value const skipPrefixes = [ 'kubectl.kubernetes.io/', 'kubernetes.io/', 'k8s.io/', 'helm.sh/', 'deployment.kubernetes.io/', 'meta.helm.sh/', 'argocd.argoproj.io/', 'checksum/', ]; // Also skip very long values (likely JSON blobs) return !skipPrefixes.some(prefix => k.startsWith(prefix)) && v.length < 500; }) .map(([k, v]) => `${k}=${v}`); if (meaningfulAnnotations.length > 0) { parts.push(`annotations: ${meaningfulAnnotations.join(', ')}`); } } return parts.join(' | '); } /** * Generate resource ID from components * Format: namespace:apiVersion:kind:name */ function generateResourceId(namespace, apiVersion, kind, name) { return `${namespace}:${apiVersion}:${kind}:${name}`; } /** * Generate a deterministic UUID from resource ID for Qdrant storage * Qdrant requires UUIDs or positive integers as point IDs * The hash is deterministic so the same resource ID always maps to the same UUID */ function generateResourceUuid(resourceId) { const hash = (0, crypto_1.createHash)('sha256').update(`resource-${resourceId}`).digest('hex'); // Convert to UUID format: 8-4-4-4-12 return `${hash.substring(0, 8)}-${hash.substring(8, 12)}-${hash.substring(12, 16)}-${hash.substring(16, 20)}-${hash.substring(20, 32)}`; } /** * Stringify an object with sorted keys for reliable comparison * Ensures consistent ordering regardless of object creation order */ function sortedStringify(obj) { if (!obj) return '{}'; const sorted = Object.keys(obj).sort().reduce((acc, key) => { acc[key] = obj[key]; return acc; }, {}); return JSON.stringify(sorted); } /** * Check if two resources have meaningful differences * Used for resync diff logic */ function hasResourceChanged(existing, incoming) { // Compare updatedAt timestamps if (existing.updatedAt !== incoming.updatedAt) { return true; } // Compare labels (with sorted keys for reliable comparison) if (sortedStringify(existing.labels) !== sortedStringify(incoming.labels)) { return true; } // Compare annotations (with sorted keys for reliable comparison) if (sortedStringify(existing.annotations) !== sortedStringify(incoming.annotations)) { return true; } return false; } /** * Vector service for storing and searching Kubernetes cluster resources */ class ResourceVectorService extends base_vector_service_1.BaseVectorService { constructor(collectionName = 'resources', embeddingService) { super(collectionName, embeddingService); } /** * Create searchable text from resource data for embedding generation */ createSearchText(resource) { return buildEmbeddingText(resource); } /** * Extract unique ID from resource data * Always constructs from components and hashes to UUID for Qdrant */ extractId(resource) { // Always construct ID from components (ignore any provided id) const resourceId = generateResourceId(resource.namespace, resource.apiVersion, resource.kind, resource.name); return generateResourceUuid(resourceId); } /** * Convert resource to storage payload format */ createPayload(resource) { return { id: generateResourceId(resource.namespace, resource.apiVersion, resource.kind, resource.name), namespace: resource.namespace, name: resource.name, kind: resource.kind, apiVersion: resource.apiVersion, apiGroup: resource.apiGroup || extractApiGroup(resource.apiVersion), labels: resource.labels || {}, annotations: resource.annotations || {}, createdAt: resource.createdAt, updatedAt: resource.updatedAt }; } /** * Convert storage payload back to resource object */ payloadToData(payload) { return { namespace: payload.namespace || '', name: payload.name || '', kind: payload.kind || '', apiVersion: payload.apiVersion || '', apiGroup: payload.apiGroup || '', labels: payload.labels || {}, annotations: payload.annotations || {}, createdAt: payload.createdAt || new Date().toISOString(), updatedAt: payload.updatedAt || new Date().toISOString() }; } /** * Store a resource in the vector database */ async storeResource(resource) { await this.storeData(resource); } /** * Upsert a resource (alias for storeResource for API consistency) */ async upsertResource(resource) { await this.storeResource(resource); } /** * Get a resource by ID * Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID */ async getResource(id) { const uuid = generateResourceUuid(id); return await this.getData(uuid); } /** * Delete a resource by ID (idempotent - ignores not found) * Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID */ async deleteResource(id) { try { // Convert human-readable ID to UUID for Qdrant const uuid = generateResourceUuid(id); await this.deleteData(uuid); } catch (error) { // Idempotent delete - ignore "not found" errors const errorMessage = error instanceof Error ? error.message : String(error); if (!errorMessage.toLowerCase().includes('not found')) { throw error; } // Resource already deleted or never existed - this is fine } } /** * Delete all resources (for testing/reset) */ async deleteAllResources() { await this.deleteAllData(); } /** * List all resources */ async listResources() { return await this.getAllData(); } /** * Semantic search for resources with optional exact filters * Combines semantic/keyword search with exact field filtering * Returns resources with their similarity scores for relevance ranking */ async searchResources(query, filters, limit = 10, minScore) { // Build Qdrant filter from simple parameters const qdrantFilter = this.buildQdrantFilter(filters); // Perform semantic search with filter and optional score threshold const results = await this.searchData(query, { limit, filter: qdrantFilter, scoreThreshold: minScore }); return results.map(r => ({ resource: r.data, score: r.score })); } /** * Build Qdrant filter object from simple filter parameters */ buildQdrantFilter(filters) { if (!filters) return undefined; const conditions = []; if (filters.namespace) { conditions.push({ key: 'namespace', match: { value: filters.namespace } }); } if (filters.kind) { conditions.push({ key: 'kind', match: { value: filters.kind } }); } if (filters.apiVersion) { conditions.push({ key: 'apiVersion', match: { value: filters.apiVersion } }); } if (conditions.length === 0) return undefined; return { must: conditions }; } /** * Diff incoming resources against Qdrant and sync changes * Used for periodic resync operations */ async diffAndSync(incoming) { // Helper to get human-readable ID from resource const getResourceKey = (r) => generateResourceId(r.namespace, r.apiVersion, r.kind, r.name); // Helper to extract resource identifier const toResourceIdentifier = (r) => ({ namespace: r.namespace, kind: r.kind, name: r.name, apiVersion: r.apiVersion }); // Get all existing resources from Qdrant const existing = await this.listResources(); const existingMap = new Map(existing.map(r => [getResourceKey(r), r])); const incomingMap = new Map(incoming.map(r => [getResourceKey(r), r])); const toInsert = []; const toUpdate = []; const toDeleteResources = []; // Find new and changed resources for (const resource of incoming) { const resourceId = getResourceKey(resource); const existingResource = existingMap.get(resourceId); if (!existingResource) { toInsert.push(resource); } else if (hasResourceChanged(existingResource, resource)) { toUpdate.push(resource); } } // Find deleted resources (in Qdrant but not in incoming) for (const [id, resource] of existingMap.entries()) { if (!incomingMap.has(id)) { toDeleteResources.push(resource); } } // Apply changes for (const resource of [...toInsert, ...toUpdate]) { await this.storeResource(resource); } for (const resource of toDeleteResources) { const id = getResourceKey(resource); await this.deleteResource(id); } return { inserted: toInsert.length, updated: toUpdate.length, deleted: toDeleteResources.length, insertedResources: toInsert.map(toResourceIdentifier), updatedResources: toUpdate.map(toResourceIdentifier), deletedResources: toDeleteResources.map(toResourceIdentifier) }; } } exports.ResourceVectorService = ResourceVectorService;