UNPKG

@dev-fastn-ai/ucl-sdk

Version:

Fastn UCL SDK - A robust TypeScript SDK for integrating AI agents with Fastn UCL

227 lines 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Analyzer = void 0; class Analyzer { constructor(tools = [], connectors = [], embeddingService) { this.tools = tools; this.connectors = connectors; this.toolEmbeddings = []; this.connectorEmbeddings = []; this.tenantConnectorsCache = {}; this.embeddingService = embeddingService; this.toolCache = new (require('../services/embedding-service').EmbeddingCache)(Analyzer.TOOL_EMBEDDINGS_KEY); this.connectorCache = new (require('../services/embedding-service').EmbeddingCache)(Analyzer.CONNECTOR_EMBEDDINGS_KEY); } async initialize() { await this.updateTools(this.tools); await this.updateConnectors(this.connectors); } /** * Checks if two arrays of tools/connectors are different by comparing their unique IDs and count. */ isDifferentList(a, b, keySelector) { if (a.length !== b.length) return true; const aIds = a.map(keySelector).sort(); const bIds = b.map(keySelector).sort(); for (let i = 0; i < aIds.length; i++) { if (aIds[i] !== bIds[i]) return true; } return false; } async updateTenantConnectors(tenantId, connectors) { this.tenantConnectorsCache[tenantId] = { connectors, expiresAt: Date.now() + 1000 * 60 * 60 * 24 // 24 hours }; } getTenantConnectors(tenantId) { if (this.tenantConnectorsCache[tenantId] && this.tenantConnectorsCache[tenantId].expiresAt > Date.now()) { return this.tenantConnectorsCache[tenantId].connectors; } return []; } doesTenantConnectorsExist(tenantId) { return this.tenantConnectorsCache[tenantId] !== undefined && this.tenantConnectorsCache[tenantId].expiresAt > Date.now(); } async updateTools(tools, clearCache = false) { // Only update embeddings if tools list is different if (!this.isDifferentList(tools, this.tools, t => t.actionId) && !clearCache) { console.log("Tools list is the same, skipping embedding"); this.tools = tools; return; } console.log("Tools list is different, embedding"); this.tools = tools; // --- Tools Embedding (name + description) --- const toolTexts = tools.map(tool => `${tool.function.name}: ${tool.function.description}`); const toolKeys = tools.map(tool => `${tool.function.name || ''}::${tool.function.description || ''}`); let toolEmbeddingsCache = this.toolCache.getAll(); let toEmbed = []; let toEmbedIndices = []; // Find which tools need embedding for (let i = 0; i < toolKeys.length; i++) { const key = toolKeys[i] || ''; if (!Array.isArray(toolEmbeddingsCache[key])) { toEmbed.push(toolTexts[i] || ''); toEmbedIndices.push(i); } } if (toEmbed.length > 0) { let newEmbeddings = []; if (typeof this.embeddingService.embedBatch === 'function' && this.embeddingService['provider'] === 'openai') { newEmbeddings = await this.embeddingService.embedBatch(toEmbed); } else { for (const text of toEmbed) { newEmbeddings.push(await this.embeddingService.embed(text)); } } for (let i = 0; i < toEmbed.length; i++) { const idx = toEmbedIndices[i]; if (typeof idx === 'number' && typeof toolKeys[idx] === 'string') { const key = toolKeys[idx] || ''; toolEmbeddingsCache[key] = newEmbeddings[i] || []; } } this.toolCache.clear(); for (const key in toolEmbeddingsCache) { this.toolCache.set(key, toolEmbeddingsCache[key] || []); } } this.toolEmbeddings = tools.map((tool, i) => { const key = toolKeys[i] || ''; return { tool, embedding: Array.isArray(toolEmbeddingsCache[key]) ? toolEmbeddingsCache[key] : [] }; }); } async updateConnectors(connectors, clearCache = false) { // Only update embeddings if connectors list is different if (!this.isDifferentList(connectors, this.connectors, c => c.id) && !clearCache) { this.connectors = connectors; return; } this.connectors = connectors; const connectorTexts = connectors.map(connector => `${connector.name || ''}: ${connector.description || ''}`); const connectorKeys = connectors.map(connector => `${connector.name || ''}::${connector.description || ''}`); let connectorEmbeddingsCache = this.connectorCache.getAll(); let toEmbed = []; let toEmbedIndices = []; for (let i = 0; i < connectorKeys.length; i++) { const key = connectorKeys[i] || ''; if (!Array.isArray(connectorEmbeddingsCache[key])) { toEmbed.push(connectorTexts[i] || ''); toEmbedIndices.push(i); } } if (toEmbed.length > 0) { let newEmbeddings = []; if (typeof this.embeddingService.embedBatch === 'function' && this.embeddingService['provider'] === 'openai') { newEmbeddings = await this.embeddingService.embedBatch(toEmbed); } else { for (const text of toEmbed) { newEmbeddings.push(await this.embeddingService.embed(text)); } } for (let i = 0; i < toEmbed.length; i++) { const idx = toEmbedIndices[i]; if (typeof idx === 'number' && typeof connectorKeys[idx] === 'string') { const key = connectorKeys[idx] || ''; connectorEmbeddingsCache[key] = newEmbeddings[i] || []; } } this.connectorCache.clear(); for (const key in connectorEmbeddingsCache) { this.connectorCache.set(key, connectorEmbeddingsCache[key] || []); } } this.connectorEmbeddings = connectors.map((connector, i) => { const key = connectorKeys[i] || ''; return { connector, embedding: Array.isArray(connectorEmbeddingsCache[key]) ? connectorEmbeddingsCache[key] : [] }; }); } /** * Analyze a message for tool or connector requirements. * - If a tool is available, return { requiredTool: true, toolName, tool } * - If no tool, but a matching connector is found and is INACTIVE, return { requireConnection: true, connectorName, connector } * - Else, return { requiredTool: false } */ async analyzeMessage(message, tenantId) { // 1. Embed the message const messageEmbedding = await this.embeddingService.embed(message); // 2. Initialize the analysis let analysis = { requiresTool: false, }; // 2. Find best-matching tool (name+description) let bestMatch = null; let bestScore = -1; for (const toolEmbedding of this.toolEmbeddings) { const score = this.cosineSimilarity(messageEmbedding, toolEmbedding.embedding); if (score > bestScore) { bestScore = score; bestMatch = toolEmbedding; } } // 3. If a tool is found, then proceed to check for a connector otherwise return false if (bestMatch && bestScore >= Analyzer.SIMILARITY_THRESHOLD) { analysis.requiresTool = true; analysis.tool = bestMatch.tool; } else { analysis.requiresTool = false; analysis.requiresConnection = false; analysis.message = "No tool found for this action"; return analysis; } // 3. If there is a tool, check for matching connector let bestConnector = null; let bestConnectorScore = -1; for (const connectorEmbedding of this.connectorEmbeddings) { const score = this.cosineSimilarity(messageEmbedding, connectorEmbedding.embedding); if (score > bestConnectorScore) { bestConnectorScore = score; bestConnector = connectorEmbedding; } } // 4. If a connector is found, check if it is active if (bestConnector && bestConnectorScore >= Analyzer.SIMILARITY_THRESHOLD) { const tenantConnector = this.getTenantConnectors(tenantId).find(c => c.id === bestConnector.connector.id); // if the connector is active, then we can proceed to execute the tool if (tenantConnector && tenantConnector.status === 'ACTIVE') { analysis.requiresConnection = false; analysis.connector = bestConnector.connector; analysis.message = "Connector is active"; } else { // if the connector is not active, then we need to connect to it analysis.requiresConnection = true; analysis.connector = bestConnector.connector; analysis.message = `Connect to ${bestConnector.connector.name} to continue`; } } analysis.message = "No connector found for this action"; return analysis; } cosineSimilarity(a, b) { const len = Math.min(a.length, b.length); let dot = 0, normA = 0, normB = 0; for (let i = 0; i < len; i++) { const ai = a[i] ?? 0; const bi = b[i] ?? 0; dot += ai * bi; normA += ai * ai; normB += bi * bi; } if (normA === 0 || normB === 0) return 0; return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } } exports.Analyzer = Analyzer; // --- Embedding cache keys --- Analyzer.TOOL_EMBEDDINGS_KEY = 'tool-embeddings-v2'; Analyzer.CONNECTOR_EMBEDDINGS_KEY = 'connector-embeddings-v2'; Analyzer.SIMILARITY_THRESHOLD = 0.25; //# sourceMappingURL=analyzer.js.map