@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
JavaScript
;
/**
* 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;