@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
JavaScript
"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