@superatomai/sdk-node 0.0.61 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +942 -942
- package/dist/index.d.mts +130 -12
- package/dist/index.d.ts +130 -12
- package/dist/index.js +441 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +440 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +48 -49
package/dist/index.js
CHANGED
|
@@ -1963,6 +1963,7 @@ __export(index_exports, {
|
|
|
1963
1963
|
llmUsageLogger: () => llmUsageLogger,
|
|
1964
1964
|
logger: () => logger,
|
|
1965
1965
|
openaiLLM: () => openaiLLM,
|
|
1966
|
+
queryCache: () => queryCache,
|
|
1966
1967
|
rerankChromaResults: () => rerankChromaResults,
|
|
1967
1968
|
rerankConversationResults: () => rerankConversationResults,
|
|
1968
1969
|
userPromptErrorLogger: () => userPromptErrorLogger
|
|
@@ -3174,7 +3175,136 @@ var ThreadManager = class _ThreadManager {
|
|
|
3174
3175
|
}
|
|
3175
3176
|
};
|
|
3176
3177
|
|
|
3178
|
+
// src/utils/query-cache.ts
|
|
3179
|
+
init_logger();
|
|
3180
|
+
var QueryCache = class {
|
|
3181
|
+
constructor() {
|
|
3182
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
3183
|
+
this.ttlMs = 5 * 60 * 1e3;
|
|
3184
|
+
// Default: 5 minutes
|
|
3185
|
+
this.cleanupInterval = null;
|
|
3186
|
+
this.startCleanup();
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Set the cache TTL (Time To Live)
|
|
3190
|
+
* @param minutes - TTL in minutes (default: 5)
|
|
3191
|
+
*/
|
|
3192
|
+
setTTL(minutes) {
|
|
3193
|
+
this.ttlMs = minutes * 60 * 1e3;
|
|
3194
|
+
logger.info(`[QueryCache] TTL set to ${minutes} minutes`);
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Get the current TTL in minutes
|
|
3198
|
+
*/
|
|
3199
|
+
getTTL() {
|
|
3200
|
+
return this.ttlMs / 60 / 1e3;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Store query result in cache
|
|
3204
|
+
* Key is the exact query string (or JSON for parameterized queries)
|
|
3205
|
+
*/
|
|
3206
|
+
set(query, data) {
|
|
3207
|
+
this.cache.set(query, {
|
|
3208
|
+
query,
|
|
3209
|
+
data,
|
|
3210
|
+
timestamp: Date.now()
|
|
3211
|
+
});
|
|
3212
|
+
logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Get cached result if exists and not expired
|
|
3216
|
+
*/
|
|
3217
|
+
get(query) {
|
|
3218
|
+
const entry = this.cache.get(query);
|
|
3219
|
+
if (!entry) {
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
3223
|
+
this.cache.delete(query);
|
|
3224
|
+
logger.debug(`[QueryCache] Entry expired for query (${query.substring(0, 50)}...)`);
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
logger.info(`[QueryCache] Cache HIT for query (${query.substring(0, 50)}...)`);
|
|
3228
|
+
return entry.data;
|
|
3229
|
+
}
|
|
3230
|
+
/**
|
|
3231
|
+
* Check if query exists in cache (not expired)
|
|
3232
|
+
*/
|
|
3233
|
+
has(query) {
|
|
3234
|
+
return this.get(query) !== null;
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* Remove a specific query from cache
|
|
3238
|
+
*/
|
|
3239
|
+
delete(query) {
|
|
3240
|
+
this.cache.delete(query);
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Clear all cached entries
|
|
3244
|
+
*/
|
|
3245
|
+
clear() {
|
|
3246
|
+
this.cache.clear();
|
|
3247
|
+
logger.info("[QueryCache] Cache cleared");
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Get cache statistics
|
|
3251
|
+
*/
|
|
3252
|
+
getStats() {
|
|
3253
|
+
let oldestTimestamp = null;
|
|
3254
|
+
for (const entry of this.cache.values()) {
|
|
3255
|
+
if (oldestTimestamp === null || entry.timestamp < oldestTimestamp) {
|
|
3256
|
+
oldestTimestamp = entry.timestamp;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
return {
|
|
3260
|
+
size: this.cache.size,
|
|
3261
|
+
oldestEntryAge: oldestTimestamp ? Date.now() - oldestTimestamp : null
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
/**
|
|
3265
|
+
* Start periodic cleanup of expired entries
|
|
3266
|
+
*/
|
|
3267
|
+
startCleanup() {
|
|
3268
|
+
this.cleanupInterval = setInterval(() => {
|
|
3269
|
+
const now = Date.now();
|
|
3270
|
+
let expiredCount = 0;
|
|
3271
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
3272
|
+
if (now - entry.timestamp > this.ttlMs) {
|
|
3273
|
+
this.cache.delete(key);
|
|
3274
|
+
expiredCount++;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
if (expiredCount > 0) {
|
|
3278
|
+
logger.debug(`[QueryCache] Cleaned up ${expiredCount} expired entries`);
|
|
3279
|
+
}
|
|
3280
|
+
}, 2 * 60 * 1e3);
|
|
3281
|
+
}
|
|
3282
|
+
/**
|
|
3283
|
+
* Stop cleanup interval (for graceful shutdown)
|
|
3284
|
+
*/
|
|
3285
|
+
destroy() {
|
|
3286
|
+
if (this.cleanupInterval) {
|
|
3287
|
+
clearInterval(this.cleanupInterval);
|
|
3288
|
+
this.cleanupInterval = null;
|
|
3289
|
+
}
|
|
3290
|
+
this.cache.clear();
|
|
3291
|
+
}
|
|
3292
|
+
};
|
|
3293
|
+
var queryCache = new QueryCache();
|
|
3294
|
+
|
|
3177
3295
|
// src/handlers/data-request.ts
|
|
3296
|
+
function getQueryCacheKey(query) {
|
|
3297
|
+
if (typeof query === "string") {
|
|
3298
|
+
return query;
|
|
3299
|
+
} else if (query?.sql) {
|
|
3300
|
+
const values = query.values || query.params;
|
|
3301
|
+
if (values && Object.keys(values).length > 0) {
|
|
3302
|
+
return JSON.stringify({ sql: query.sql, values });
|
|
3303
|
+
}
|
|
3304
|
+
return query.sql;
|
|
3305
|
+
}
|
|
3306
|
+
return "";
|
|
3307
|
+
}
|
|
3178
3308
|
async function handleDataRequest(data, collections, sendMessage) {
|
|
3179
3309
|
try {
|
|
3180
3310
|
const dataRequest = DataRequestMessageSchema.parse(data);
|
|
@@ -3193,10 +3323,37 @@ async function handleDataRequest(data, collections, sendMessage) {
|
|
|
3193
3323
|
return;
|
|
3194
3324
|
}
|
|
3195
3325
|
const startTime = performance.now();
|
|
3196
|
-
|
|
3197
|
-
|
|
3326
|
+
let result;
|
|
3327
|
+
let fromCache = false;
|
|
3328
|
+
if (collection === "database" && op === "execute" && params?.sql) {
|
|
3329
|
+
const cacheKey = getQueryCacheKey(params.sql);
|
|
3330
|
+
if (cacheKey) {
|
|
3331
|
+
const cachedResult = queryCache.get(cacheKey);
|
|
3332
|
+
if (cachedResult !== null) {
|
|
3333
|
+
result = cachedResult;
|
|
3334
|
+
fromCache = true;
|
|
3335
|
+
logger.info(`[QueryCache] Returning cached result for database.execute`);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (!fromCache) {
|
|
3340
|
+
const handler = collections[collection][op];
|
|
3341
|
+
let handlerParams = params || {};
|
|
3342
|
+
if (collection === "database" && op === "execute" && params?.sql && typeof params.sql !== "string") {
|
|
3343
|
+
const cacheKey = getQueryCacheKey(params.sql);
|
|
3344
|
+
handlerParams = { ...params, sql: cacheKey };
|
|
3345
|
+
logger.debug(`[data-request] Converted object query to JSON string for database handler`);
|
|
3346
|
+
}
|
|
3347
|
+
result = await handler(handlerParams);
|
|
3348
|
+
if (collection === "database" && op === "execute" && params?.sql && result) {
|
|
3349
|
+
const cacheKey = getQueryCacheKey(params.sql);
|
|
3350
|
+
if (cacheKey) {
|
|
3351
|
+
queryCache.set(cacheKey, result);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3198
3355
|
const executionMs = Math.round(performance.now() - startTime);
|
|
3199
|
-
logger.info(`Executed ${collection}.${op} in ${executionMs}ms`);
|
|
3356
|
+
logger.info(`Executed ${collection}.${op} in ${executionMs}ms${fromCache ? " (from cache)" : ""}`);
|
|
3200
3357
|
if (SA_RUNTIME && typeof SA_RUNTIME === "object" && "uiBlockId" in SA_RUNTIME) {
|
|
3201
3358
|
const uiBlockId = SA_RUNTIME.uiBlockId;
|
|
3202
3359
|
const threadId = SA_RUNTIME.threadId;
|
|
@@ -6010,6 +6167,7 @@ var BaseLLM = class {
|
|
|
6010
6167
|
this.defaultLimit = config?.defaultLimit || 50;
|
|
6011
6168
|
this.apiKey = config?.apiKey;
|
|
6012
6169
|
this.modelStrategy = config?.modelStrategy || "fast";
|
|
6170
|
+
this.conversationSimilarityThreshold = config?.conversationSimilarityThreshold || 0.8;
|
|
6013
6171
|
}
|
|
6014
6172
|
/**
|
|
6015
6173
|
* Get the appropriate model based on task type and model strategy
|
|
@@ -6042,6 +6200,26 @@ var BaseLLM = class {
|
|
|
6042
6200
|
getModelStrategy() {
|
|
6043
6201
|
return this.modelStrategy;
|
|
6044
6202
|
}
|
|
6203
|
+
/**
|
|
6204
|
+
* Set the conversation similarity threshold at runtime
|
|
6205
|
+
* @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
|
|
6206
|
+
*/
|
|
6207
|
+
setConversationSimilarityThreshold(threshold) {
|
|
6208
|
+
if (threshold < 0 || threshold > 1) {
|
|
6209
|
+
logger.warn(`[${this.getProviderName()}] Invalid threshold ${threshold}, must be between 0 and 1. Using default 0.8`);
|
|
6210
|
+
this.conversationSimilarityThreshold = 0.8;
|
|
6211
|
+
return;
|
|
6212
|
+
}
|
|
6213
|
+
this.conversationSimilarityThreshold = threshold;
|
|
6214
|
+
logger.info(`[${this.getProviderName()}] Conversation similarity threshold set to: ${threshold}`);
|
|
6215
|
+
}
|
|
6216
|
+
/**
|
|
6217
|
+
* Get the current conversation similarity threshold
|
|
6218
|
+
* @returns The current threshold value
|
|
6219
|
+
*/
|
|
6220
|
+
getConversationSimilarityThreshold() {
|
|
6221
|
+
return this.conversationSimilarityThreshold;
|
|
6222
|
+
}
|
|
6045
6223
|
/**
|
|
6046
6224
|
* Get the API key (from instance, parameter, or environment)
|
|
6047
6225
|
*/
|
|
@@ -6068,6 +6246,96 @@ var BaseLLM = class {
|
|
|
6068
6246
|
}
|
|
6069
6247
|
return false;
|
|
6070
6248
|
}
|
|
6249
|
+
/**
|
|
6250
|
+
* Get the cache key for a query (the exact sql param that would be sent to execute)
|
|
6251
|
+
* This ensures the cache key matches what the frontend will send
|
|
6252
|
+
* Used for both caching and internal deduplication
|
|
6253
|
+
*/
|
|
6254
|
+
getQueryCacheKey(query) {
|
|
6255
|
+
if (typeof query === "string") {
|
|
6256
|
+
return query;
|
|
6257
|
+
} else if (query?.sql) {
|
|
6258
|
+
const values = query.values || query.params;
|
|
6259
|
+
if (values && Object.keys(values).length > 0) {
|
|
6260
|
+
return JSON.stringify({ sql: query.sql, values });
|
|
6261
|
+
} else {
|
|
6262
|
+
return query.sql;
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
return "";
|
|
6266
|
+
}
|
|
6267
|
+
/**
|
|
6268
|
+
* Execute a query against the database for validation and caching
|
|
6269
|
+
* @param query - The SQL query to execute (string or object with sql/values)
|
|
6270
|
+
* @param collections - Collections object containing database execute function
|
|
6271
|
+
* @returns Object with result data and cache key
|
|
6272
|
+
* @throws Error if query execution fails
|
|
6273
|
+
*/
|
|
6274
|
+
async executeQueryForValidation(query, collections) {
|
|
6275
|
+
const cacheKey = this.getQueryCacheKey(query);
|
|
6276
|
+
if (!cacheKey) {
|
|
6277
|
+
throw new Error("Invalid query format: expected string or object with sql property");
|
|
6278
|
+
}
|
|
6279
|
+
const result = await collections["database"]["execute"]({ sql: cacheKey });
|
|
6280
|
+
return { result, cacheKey };
|
|
6281
|
+
}
|
|
6282
|
+
/**
|
|
6283
|
+
* Request the LLM to fix a failed SQL query
|
|
6284
|
+
* @param failedQuery - The query that failed execution
|
|
6285
|
+
* @param errorMessage - The error message from the failed execution
|
|
6286
|
+
* @param componentContext - Context about the component (name, type, title)
|
|
6287
|
+
* @param apiKey - Optional API key
|
|
6288
|
+
* @returns Fixed query string
|
|
6289
|
+
*/
|
|
6290
|
+
async requestQueryFix(failedQuery, errorMessage, componentContext, apiKey) {
|
|
6291
|
+
const schemaDoc = schema.generateSchemaDocumentation();
|
|
6292
|
+
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
6293
|
+
const prompt = `You are a SQL expert. Fix the following SQL query that failed execution.
|
|
6294
|
+
|
|
6295
|
+
## Database Schema
|
|
6296
|
+
${schemaDoc}
|
|
6297
|
+
|
|
6298
|
+
## Database-Specific SQL Rules
|
|
6299
|
+
${databaseRules}
|
|
6300
|
+
|
|
6301
|
+
## Component Context
|
|
6302
|
+
- Component Name: ${componentContext.name}
|
|
6303
|
+
- Component Type: ${componentContext.type}
|
|
6304
|
+
- Title: ${componentContext.title || "N/A"}
|
|
6305
|
+
|
|
6306
|
+
## Failed Query
|
|
6307
|
+
\`\`\`sql
|
|
6308
|
+
${failedQuery}
|
|
6309
|
+
\`\`\`
|
|
6310
|
+
|
|
6311
|
+
## Error Message
|
|
6312
|
+
${errorMessage}
|
|
6313
|
+
|
|
6314
|
+
## Instructions
|
|
6315
|
+
1. Analyze the error message and identify what caused the query to fail
|
|
6316
|
+
2. Fix the query to resolve the error while preserving the original intent
|
|
6317
|
+
3. Ensure the fixed query follows the database-specific SQL rules above
|
|
6318
|
+
4. Return ONLY the fixed SQL query, no explanations or markdown
|
|
6319
|
+
|
|
6320
|
+
Fixed SQL query:`;
|
|
6321
|
+
const response = await LLM.text(
|
|
6322
|
+
{
|
|
6323
|
+
sys: "You are a SQL expert. Return only the fixed SQL query with no additional text, explanations, or markdown formatting.",
|
|
6324
|
+
user: prompt
|
|
6325
|
+
},
|
|
6326
|
+
{
|
|
6327
|
+
model: this.getModelForTask("simple"),
|
|
6328
|
+
maxTokens: 2048,
|
|
6329
|
+
temperature: 0.1,
|
|
6330
|
+
apiKey: this.getApiKey(apiKey)
|
|
6331
|
+
}
|
|
6332
|
+
);
|
|
6333
|
+
let fixedQuery = response.trim();
|
|
6334
|
+
fixedQuery = fixedQuery.replace(/^```sql\s*/i, "").replace(/\s*```$/i, "");
|
|
6335
|
+
fixedQuery = fixedQuery.replace(/^```\s*/i, "").replace(/\s*```$/i, "");
|
|
6336
|
+
const { query: validatedQuery } = validateAndFixSqlQuery(fixedQuery);
|
|
6337
|
+
return validatedQuery;
|
|
6338
|
+
}
|
|
6071
6339
|
/**
|
|
6072
6340
|
* Match components from text response suggestions and generate follow-up questions
|
|
6073
6341
|
* Takes a text response with component suggestions (c1:type format) and matches with available components
|
|
@@ -6428,10 +6696,34 @@ ${executedToolsText}`);
|
|
|
6428
6696
|
}
|
|
6429
6697
|
};
|
|
6430
6698
|
}).filter(Boolean);
|
|
6699
|
+
let validatedComponents = finalComponents;
|
|
6700
|
+
if (collections?.["database"]?.["execute"]) {
|
|
6701
|
+
logger.info(`[${this.getProviderName()}] Starting query validation for ${finalComponents.length} components...`);
|
|
6702
|
+
logCollector?.info(`Validating queries for ${finalComponents.length} components...`);
|
|
6703
|
+
try {
|
|
6704
|
+
const validationResult = await this.validateAndRetryComponentQueries(
|
|
6705
|
+
finalComponents,
|
|
6706
|
+
collections,
|
|
6707
|
+
apiKey,
|
|
6708
|
+
logCollector
|
|
6709
|
+
);
|
|
6710
|
+
validatedComponents = validationResult.components;
|
|
6711
|
+
const queriedComponents = finalComponents.filter((c) => c.props?.query);
|
|
6712
|
+
const validatedQueries = validatedComponents.filter((c) => c.props?.query);
|
|
6713
|
+
logger.info(`[${this.getProviderName()}] Query validation complete: ${validatedQueries.length}/${queriedComponents.length} queries validated`);
|
|
6714
|
+
logCollector?.info(`Query validation complete: ${validatedQueries.length}/${queriedComponents.length} queries validated`);
|
|
6715
|
+
} catch (validationError) {
|
|
6716
|
+
const validationErrorMsg = validationError instanceof Error ? validationError.message : String(validationError);
|
|
6717
|
+
logger.error(`[${this.getProviderName()}] Query validation error: ${validationErrorMsg}`);
|
|
6718
|
+
logCollector?.error(`Query validation error: ${validationErrorMsg}`);
|
|
6719
|
+
}
|
|
6720
|
+
} else {
|
|
6721
|
+
logger.debug(`[${this.getProviderName()}] Skipping query validation - database execute function not available`);
|
|
6722
|
+
}
|
|
6431
6723
|
const methodDuration = Date.now() - methodStartTime;
|
|
6432
|
-
logger.info(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration}ms | components: ${
|
|
6724
|
+
logger.info(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration}ms | components: ${validatedComponents.length} | actions: ${actions.length}`);
|
|
6433
6725
|
return {
|
|
6434
|
-
components:
|
|
6726
|
+
components: validatedComponents,
|
|
6435
6727
|
layoutTitle,
|
|
6436
6728
|
layoutDescription,
|
|
6437
6729
|
actions
|
|
@@ -6449,6 +6741,117 @@ ${executedToolsText}`);
|
|
|
6449
6741
|
};
|
|
6450
6742
|
}
|
|
6451
6743
|
}
|
|
6744
|
+
/**
|
|
6745
|
+
* Validate component queries against the database and retry with LLM fixes if they fail
|
|
6746
|
+
* @param components - Array of components with potential queries
|
|
6747
|
+
* @param collections - Collections object containing database execute function
|
|
6748
|
+
* @param apiKey - Optional API key for LLM calls
|
|
6749
|
+
* @param logCollector - Optional log collector for logging
|
|
6750
|
+
* @returns Object with validated components and a map of query results
|
|
6751
|
+
*/
|
|
6752
|
+
async validateAndRetryComponentQueries(components, collections, apiKey, logCollector) {
|
|
6753
|
+
const MAX_RETRIES = 3;
|
|
6754
|
+
const queryResults = /* @__PURE__ */ new Map();
|
|
6755
|
+
const validatedComponents = [];
|
|
6756
|
+
const queryAttempts = /* @__PURE__ */ new Map();
|
|
6757
|
+
const queryValidationStatus = /* @__PURE__ */ new Map();
|
|
6758
|
+
for (const component of components) {
|
|
6759
|
+
const query = component.props?.query;
|
|
6760
|
+
if (!query) {
|
|
6761
|
+
validatedComponents.push(component);
|
|
6762
|
+
continue;
|
|
6763
|
+
}
|
|
6764
|
+
const queryKey = this.getQueryCacheKey(query);
|
|
6765
|
+
const queryStr = typeof query === "string" ? query : query?.sql || "";
|
|
6766
|
+
if (queryValidationStatus.has(queryKey)) {
|
|
6767
|
+
if (queryValidationStatus.get(queryKey)) {
|
|
6768
|
+
validatedComponents.push(component);
|
|
6769
|
+
if (queryResults.has(queryKey)) {
|
|
6770
|
+
queryResults.set(`${component.id}:${queryKey}`, queryResults.get(queryKey));
|
|
6771
|
+
}
|
|
6772
|
+
} else {
|
|
6773
|
+
logger.warn(`[${this.getProviderName()}] Component ${component.name} has previously failed query, keeping as-is`);
|
|
6774
|
+
validatedComponents.push(component);
|
|
6775
|
+
}
|
|
6776
|
+
continue;
|
|
6777
|
+
}
|
|
6778
|
+
let attempts = queryAttempts.get(queryKey) || 0;
|
|
6779
|
+
let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
|
|
6780
|
+
let currentQueryStr = queryStr;
|
|
6781
|
+
let validated = false;
|
|
6782
|
+
let lastError = "";
|
|
6783
|
+
logger.info(`[${this.getProviderName()}] Validating query for component: ${component.name} (${component.type})`);
|
|
6784
|
+
while (attempts < MAX_RETRIES && !validated) {
|
|
6785
|
+
attempts++;
|
|
6786
|
+
queryAttempts.set(queryKey, attempts);
|
|
6787
|
+
try {
|
|
6788
|
+
logger.debug(`[${this.getProviderName()}] Query validation attempt ${attempts}/${MAX_RETRIES} for ${component.name}`);
|
|
6789
|
+
const { result, cacheKey } = await this.executeQueryForValidation(currentQuery, collections);
|
|
6790
|
+
validated = true;
|
|
6791
|
+
queryValidationStatus.set(queryKey, true);
|
|
6792
|
+
queryResults.set(queryKey, result);
|
|
6793
|
+
queryResults.set(`${component.id}:${queryKey}`, result);
|
|
6794
|
+
queryCache.set(cacheKey, result);
|
|
6795
|
+
logger.info(`[${this.getProviderName()}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
|
|
6796
|
+
logCollector?.info(`\u2713 Query validated for ${component.name}`);
|
|
6797
|
+
if (currentQueryStr !== queryStr) {
|
|
6798
|
+
component.props = {
|
|
6799
|
+
...component.props,
|
|
6800
|
+
query: typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr }
|
|
6801
|
+
};
|
|
6802
|
+
logger.info(`[${this.getProviderName()}] Updated ${component.name} with fixed query`);
|
|
6803
|
+
}
|
|
6804
|
+
} catch (error) {
|
|
6805
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
6806
|
+
logger.warn(`[${this.getProviderName()}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
|
|
6807
|
+
logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
|
|
6808
|
+
if (attempts >= MAX_RETRIES) {
|
|
6809
|
+
logger.error(`[${this.getProviderName()}] \u2717 Max retries reached for ${component.name}, keeping original component`);
|
|
6810
|
+
logCollector?.error(`Max retries reached for ${component.name}, query may fail at runtime`);
|
|
6811
|
+
queryValidationStatus.set(queryKey, false);
|
|
6812
|
+
break;
|
|
6813
|
+
}
|
|
6814
|
+
logger.info(`[${this.getProviderName()}] Requesting query fix from LLM for ${component.name}...`);
|
|
6815
|
+
logCollector?.info(`Requesting query fix for ${component.name}...`);
|
|
6816
|
+
try {
|
|
6817
|
+
const fixedQueryStr = await this.requestQueryFix(
|
|
6818
|
+
currentQueryStr,
|
|
6819
|
+
lastError,
|
|
6820
|
+
{
|
|
6821
|
+
name: component.name,
|
|
6822
|
+
type: component.type,
|
|
6823
|
+
title: component.props?.title
|
|
6824
|
+
},
|
|
6825
|
+
apiKey
|
|
6826
|
+
);
|
|
6827
|
+
if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
|
|
6828
|
+
logger.info(`[${this.getProviderName()}] Received fixed query for ${component.name}, retrying...`);
|
|
6829
|
+
currentQueryStr = fixedQueryStr;
|
|
6830
|
+
if (typeof currentQuery === "string") {
|
|
6831
|
+
currentQuery = fixedQueryStr;
|
|
6832
|
+
} else {
|
|
6833
|
+
currentQuery = { ...currentQuery, sql: fixedQueryStr };
|
|
6834
|
+
}
|
|
6835
|
+
} else {
|
|
6836
|
+
logger.warn(`[${this.getProviderName()}] LLM returned same or empty query, stopping retries`);
|
|
6837
|
+
queryValidationStatus.set(queryKey, false);
|
|
6838
|
+
break;
|
|
6839
|
+
}
|
|
6840
|
+
} catch (fixError) {
|
|
6841
|
+
const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
|
|
6842
|
+
logger.error(`[${this.getProviderName()}] Failed to get query fix from LLM: ${fixErrorMsg}`);
|
|
6843
|
+
queryValidationStatus.set(queryKey, false);
|
|
6844
|
+
break;
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
validatedComponents.push(component);
|
|
6849
|
+
}
|
|
6850
|
+
return {
|
|
6851
|
+
components: validatedComponents,
|
|
6852
|
+
queryResults
|
|
6853
|
+
};
|
|
6854
|
+
}
|
|
6452
6855
|
/**
|
|
6453
6856
|
* Classify user question into category and detect external tools needed
|
|
6454
6857
|
* Determines if question is for data analysis, requires external tools, or needs text response
|
|
@@ -6630,12 +7033,6 @@ ${executedToolsText}`);
|
|
|
6630
7033
|
* This provides conversational text responses instead of component generation
|
|
6631
7034
|
* Supports tool calling for query execution with automatic retry on errors (max 3 attempts)
|
|
6632
7035
|
* After generating text response, if components are provided, matches suggested components
|
|
6633
|
-
* @param streamCallback - Optional callback function to receive text chunks as they stream
|
|
6634
|
-
* @param collections - Collection registry for executing database queries via database.execute
|
|
6635
|
-
* @param components - Optional list of available components for matching suggestions
|
|
6636
|
-
* @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called
|
|
6637
|
-
* @param category - Question category ('data_analysis' | 'data_modification' | 'general'). For data_modification, answer component streaming is skipped. For general, component generation is skipped entirely.
|
|
6638
|
-
* @param userId - Optional user ID for fetching user-specific knowledge base nodes
|
|
6639
7036
|
*/
|
|
6640
7037
|
async generateTextResponse(userPrompt, apiKey, logCollector, conversationHistory, streamCallback, collections, components, externalTools, category, userId) {
|
|
6641
7038
|
const methodStartTime = Date.now();
|
|
@@ -7325,11 +7722,6 @@ ${errorMsg}
|
|
|
7325
7722
|
* - If match found → Adapt UI block parameters and return
|
|
7326
7723
|
* 2. Category classification: Determine if data_analysis, requires_external_tools, or text_response
|
|
7327
7724
|
* 3. Route appropriately based on category and response mode
|
|
7328
|
-
*
|
|
7329
|
-
* @param responseMode - 'component' for component generation (default), 'text' for text responses
|
|
7330
|
-
* @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
|
|
7331
|
-
* @param collections - Collection registry for executing database queries (required for text mode)
|
|
7332
|
-
* @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
|
|
7333
7725
|
*/
|
|
7334
7726
|
async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
|
|
7335
7727
|
const startTime = Date.now();
|
|
@@ -7343,8 +7735,7 @@ ${errorMsg}
|
|
|
7343
7735
|
userPrompt,
|
|
7344
7736
|
collections,
|
|
7345
7737
|
userId,
|
|
7346
|
-
similarityThreshold: 0.
|
|
7347
|
-
// 80% threshold
|
|
7738
|
+
similarityThreshold: 0.99
|
|
7348
7739
|
});
|
|
7349
7740
|
if (conversationMatch) {
|
|
7350
7741
|
logger.info(`[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`);
|
|
@@ -7495,16 +7886,6 @@ ${errorMsg}
|
|
|
7495
7886
|
};
|
|
7496
7887
|
}) || [];
|
|
7497
7888
|
}
|
|
7498
|
-
if (categoryClassification.category === "general") {
|
|
7499
|
-
logger.info(`[${this.getProviderName()}] Routing to general conversation (no database operations)`);
|
|
7500
|
-
logCollector?.info("Routing to general conversation...");
|
|
7501
|
-
} else if (categoryClassification.category === "data_analysis") {
|
|
7502
|
-
logger.info(`[${this.getProviderName()}] Routing to data analysis (SELECT operations)`);
|
|
7503
|
-
logCollector?.info("Routing to data analysis...");
|
|
7504
|
-
} else if (categoryClassification.category === "data_modification") {
|
|
7505
|
-
logger.info(`[${this.getProviderName()}] Routing to data modification (INSERT/UPDATE/DELETE operations)`);
|
|
7506
|
-
logCollector?.info("Routing to data modification...");
|
|
7507
|
-
}
|
|
7508
7889
|
const textResponse = await this.generateTextResponse(
|
|
7509
7890
|
userPrompt,
|
|
7510
7891
|
apiKey,
|
|
@@ -12924,8 +13305,13 @@ var SuperatomSDK = class {
|
|
|
12924
13305
|
this.llmProviders = config.LLM_PROVIDERS || getLLMProviders();
|
|
12925
13306
|
this.databaseType = config.databaseType || "postgresql";
|
|
12926
13307
|
this.modelStrategy = config.modelStrategy || "fast";
|
|
13308
|
+
this.conversationSimilarityThreshold = config.conversationSimilarityThreshold ?? 0.8;
|
|
12927
13309
|
this.applyModelStrategy(this.modelStrategy);
|
|
12928
|
-
|
|
13310
|
+
this.applyConversationSimilarityThreshold(this.conversationSimilarityThreshold);
|
|
13311
|
+
if (config.queryCacheTTL !== void 0) {
|
|
13312
|
+
queryCache.setTTL(config.queryCacheTTL);
|
|
13313
|
+
}
|
|
13314
|
+
logger.info(`Initializing Superatom SDK v${SDK_VERSION} for project ${this.projectId}, llm providers: ${this.llmProviders.join(", ")}, database type: ${this.databaseType}, model strategy: ${this.modelStrategy}, conversation similarity threshold: ${this.conversationSimilarityThreshold}, query cache TTL: ${queryCache.getTTL()} minutes`);
|
|
12929
13315
|
this.userManager = new UserManager(this.projectId, 5e3);
|
|
12930
13316
|
this.dashboardManager = new DashboardManager(this.projectId);
|
|
12931
13317
|
this.reportManager = new ReportManager(this.projectId);
|
|
@@ -13338,6 +13724,31 @@ var SuperatomSDK = class {
|
|
|
13338
13724
|
getModelStrategy() {
|
|
13339
13725
|
return this.modelStrategy;
|
|
13340
13726
|
}
|
|
13727
|
+
/**
|
|
13728
|
+
* Apply conversation similarity threshold to all LLM provider singletons
|
|
13729
|
+
* @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
|
|
13730
|
+
*/
|
|
13731
|
+
applyConversationSimilarityThreshold(threshold) {
|
|
13732
|
+
anthropicLLM.setConversationSimilarityThreshold(threshold);
|
|
13733
|
+
groqLLM.setConversationSimilarityThreshold(threshold);
|
|
13734
|
+
geminiLLM.setConversationSimilarityThreshold(threshold);
|
|
13735
|
+
openaiLLM.setConversationSimilarityThreshold(threshold);
|
|
13736
|
+
logger.info(`Conversation similarity threshold '${threshold}' applied to all LLM providers`);
|
|
13737
|
+
}
|
|
13738
|
+
/**
|
|
13739
|
+
* Set conversation similarity threshold at runtime
|
|
13740
|
+
* @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
|
|
13741
|
+
*/
|
|
13742
|
+
setConversationSimilarityThreshold(threshold) {
|
|
13743
|
+
this.conversationSimilarityThreshold = threshold;
|
|
13744
|
+
this.applyConversationSimilarityThreshold(threshold);
|
|
13745
|
+
}
|
|
13746
|
+
/**
|
|
13747
|
+
* Get current conversation similarity threshold
|
|
13748
|
+
*/
|
|
13749
|
+
getConversationSimilarityThreshold() {
|
|
13750
|
+
return this.conversationSimilarityThreshold;
|
|
13751
|
+
}
|
|
13341
13752
|
};
|
|
13342
13753
|
// Annotate the CommonJS export names for ESM import in node:
|
|
13343
13754
|
0 && (module.exports = {
|
|
@@ -13360,6 +13771,7 @@ var SuperatomSDK = class {
|
|
|
13360
13771
|
llmUsageLogger,
|
|
13361
13772
|
logger,
|
|
13362
13773
|
openaiLLM,
|
|
13774
|
+
queryCache,
|
|
13363
13775
|
rerankChromaResults,
|
|
13364
13776
|
rerankConversationResults,
|
|
13365
13777
|
userPromptErrorLogger
|