@superatomai/sdk-node 0.0.62 → 0.0.64

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/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
- const handler = collections[collection][op];
3197
- const result = await handler(params || {});
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;
@@ -6089,6 +6246,96 @@ var BaseLLM = class {
6089
6246
  }
6090
6247
  return false;
6091
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
+ }
6092
6339
  /**
6093
6340
  * Match components from text response suggestions and generate follow-up questions
6094
6341
  * Takes a text response with component suggestions (c1:type format) and matches with available components
@@ -6125,11 +6372,11 @@ var BaseLLM = class {
6125
6372
  logger.info(`[${this.getProviderName()}] Passing ${deferredTools.length} deferred tools to component matching`);
6126
6373
  deferredToolsText = "The following external tools need user input via a Form component.\n**IMPORTANT: Use these EXACT values when generating Form externalTool prop.**\n\n" + deferredTools.map((tool, idx) => {
6127
6374
  return `${idx + 1}. **${tool.name}**
6128
- toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
6129
- toolName: "${tool.name}"
6130
- parameters: ${JSON.stringify(tool.params || {})}
6131
- requiredFields:
6132
- ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6375
+ toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
6376
+ toolName: "${tool.name}"
6377
+ parameters: ${JSON.stringify(tool.params || {})}
6378
+ requiredFields:
6379
+ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6133
6380
  }).join("\n\n");
6134
6381
  }
6135
6382
  let executedToolsText = "No external tools were executed for data fetching.";
@@ -6145,8 +6392,8 @@ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6145
6392
  const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
6146
6393
  const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
6147
6394
  fieldNamesList = `
6148
- \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
6149
- \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
6395
+ \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
6396
+ \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
6150
6397
  const fieldsText = fields.map(
6151
6398
  (f) => ` "${f.name}" (${f.type}): ${f.description}`
6152
6399
  ).join("\n");
@@ -6155,11 +6402,11 @@ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6155
6402
  ${fieldsText}`;
6156
6403
  }
6157
6404
  return `${idx + 1}. **${tool.name}**
6158
- toolId: "${tool.id}"
6159
- toolName: "${tool.name}"
6160
- parameters: ${JSON.stringify(tool.params || {})}
6161
- recordCount: ${recordCount} rows returned
6162
- outputSchema: ${outputSchemaText}${fieldNamesList}`;
6405
+ toolId: "${tool.id}"
6406
+ toolName: "${tool.name}"
6407
+ parameters: ${JSON.stringify(tool.params || {})}
6408
+ recordCount: ${recordCount} rows returned
6409
+ outputSchema: ${outputSchemaText}${fieldNamesList}`;
6163
6410
  }).join("\n\n");
6164
6411
  }
6165
6412
  const schemaDoc = schema.generateSchemaDocumentation();
@@ -6210,6 +6457,7 @@ ${executedToolsText}`);
6210
6457
  logCollector?.info("Matching components from text response...");
6211
6458
  let fullResponseText = "";
6212
6459
  let answerComponentExtracted = false;
6460
+ let validatedAnswerComponent = null;
6213
6461
  const answerCallback = componentStreamCallback;
6214
6462
  const partialCallback = answerCallback ? (chunk) => {
6215
6463
  fullResponseText += chunk;
@@ -6268,8 +6516,8 @@ ${executedToolsText}`);
6268
6516
  }
6269
6517
  };
6270
6518
  const streamTime = (/* @__PURE__ */ new Date()).toISOString();
6271
- logger.info(`[${this.getProviderName()}] \u2713 [${streamTime}] Answer component detected in stream: ${answerComponent.name} (${answerComponent.type}) - STREAMING TO FRONTEND NOW`);
6272
- logCollector?.info(`\u2713 Answer component: ${answerComponent.name} (${answerComponent.type}) - streaming to frontend at ${streamTime}`);
6519
+ logger.info(`[${this.getProviderName()}] \u2713 [${streamTime}] Answer component detected in stream: ${answerComponent.name} (${answerComponent.type})`);
6520
+ logCollector?.info(`\u2713 Answer component: ${answerComponent.name} (${answerComponent.type}) - detected at ${streamTime}`);
6273
6521
  if (answerComponentData.props?.query) {
6274
6522
  logCollector?.logQuery(
6275
6523
  "Answer component query",
@@ -6277,7 +6525,79 @@ ${executedToolsText}`);
6277
6525
  { componentName: answerComponent.name, componentType: answerComponent.type, reasoning: answerComponentData.reasoning }
6278
6526
  );
6279
6527
  }
6280
- answerCallback(answerComponent);
6528
+ const answerQuery = answerComponent.props?.query;
6529
+ logger.info(`[${this.getProviderName()}] Answer component detected: ${answerComponent.name} (${answerComponent.type}), hasQuery: ${!!answerQuery}, hasDbExecute: ${!!collections?.["database"]?.["execute"]}`);
6530
+ if (answerQuery && collections?.["database"]?.["execute"]) {
6531
+ (async () => {
6532
+ const MAX_RETRIES = 3;
6533
+ let attempts = 0;
6534
+ let validated = false;
6535
+ let currentQuery = answerQuery;
6536
+ let currentQueryStr = typeof answerQuery === "string" ? answerQuery : answerQuery?.sql || "";
6537
+ let lastError = "";
6538
+ logger.info(`[${this.getProviderName()}] Validating answer component query before streaming...`);
6539
+ while (attempts < MAX_RETRIES && !validated) {
6540
+ attempts++;
6541
+ try {
6542
+ const cacheKey = this.getQueryCacheKey(currentQuery);
6543
+ if (cacheKey) {
6544
+ logger.debug(`[${this.getProviderName()}] Answer component query validation attempt ${attempts}/${MAX_RETRIES}`);
6545
+ const result2 = await collections["database"]["execute"]({ sql: cacheKey });
6546
+ queryCache.set(cacheKey, result2);
6547
+ validated = true;
6548
+ if (currentQuery !== answerQuery) {
6549
+ answerComponent.props.query = currentQuery;
6550
+ }
6551
+ validatedAnswerComponent = {
6552
+ componentId: answerComponentData.componentId,
6553
+ validatedQuery: currentQuery
6554
+ };
6555
+ logger.info(`[${this.getProviderName()}] \u2713 Answer component query validated (attempt ${attempts}) - STREAMING TO FRONTEND NOW`);
6556
+ logCollector?.info(`\u2713 Answer component query validated - streaming to frontend`);
6557
+ logger.info(`[${this.getProviderName()}] Calling answerCallback for: ${answerComponent.name}`);
6558
+ answerCallback(answerComponent);
6559
+ logger.info(`[${this.getProviderName()}] answerCallback completed for: ${answerComponent.name}`);
6560
+ }
6561
+ } catch (validationError) {
6562
+ lastError = validationError instanceof Error ? validationError.message : String(validationError);
6563
+ logger.warn(`[${this.getProviderName()}] Answer component query validation failed (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6564
+ if (attempts < MAX_RETRIES) {
6565
+ try {
6566
+ logger.info(`[${this.getProviderName()}] Requesting LLM to fix answer component query...`);
6567
+ const fixedQueryStr = await this.requestQueryFix(
6568
+ currentQueryStr,
6569
+ lastError,
6570
+ {
6571
+ name: answerComponent.name,
6572
+ type: answerComponent.type,
6573
+ title: answerComponent.props?.title
6574
+ },
6575
+ apiKey
6576
+ );
6577
+ if (typeof currentQuery === "string") {
6578
+ currentQuery = fixedQueryStr;
6579
+ } else {
6580
+ currentQuery = { ...currentQuery, sql: fixedQueryStr };
6581
+ }
6582
+ currentQueryStr = fixedQueryStr;
6583
+ logger.info(`[${this.getProviderName()}] LLM provided fixed query for answer component, retrying...`);
6584
+ } catch (fixError) {
6585
+ const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6586
+ logger.error(`[${this.getProviderName()}] Failed to get LLM query fix for answer component: ${fixErrorMsg}`);
6587
+ break;
6588
+ }
6589
+ }
6590
+ }
6591
+ }
6592
+ if (!validated) {
6593
+ logger.warn(`[${this.getProviderName()}] Answer component query validation failed after ${attempts} attempts - skipping early stream`);
6594
+ logCollector?.warn(`Answer component query validation failed: ${lastError} - will be handled in batch validation`);
6595
+ }
6596
+ })();
6597
+ } else {
6598
+ logger.info(`[${this.getProviderName()}] Answer component has no query - STREAMING TO FRONTEND NOW`);
6599
+ answerCallback(answerComponent);
6600
+ }
6281
6601
  answerComponentExtracted = true;
6282
6602
  }
6283
6603
  }
@@ -6331,6 +6651,10 @@ ${executedToolsText}`);
6331
6651
  return null;
6332
6652
  }
6333
6653
  let cleanedProps = { ...mc.props };
6654
+ if (validatedAnswerComponent && mc.componentId === validatedAnswerComponent.componentId) {
6655
+ logger.info(`[${this.getProviderName()}] Using pre-validated query for answer component: ${mc.componentId}`);
6656
+ cleanedProps.query = validatedAnswerComponent.validatedQuery;
6657
+ }
6334
6658
  if (cleanedProps.externalTool) {
6335
6659
  const toolId = cleanedProps.externalTool.toolId;
6336
6660
  const validToolIds = (executedTools || []).map((t) => t.id);
@@ -6449,10 +6773,34 @@ ${executedToolsText}`);
6449
6773
  }
6450
6774
  };
6451
6775
  }).filter(Boolean);
6776
+ let validatedComponents = finalComponents;
6777
+ if (collections?.["database"]?.["execute"]) {
6778
+ logger.info(`[${this.getProviderName()}] Starting query validation for ${finalComponents.length} components...`);
6779
+ logCollector?.info(`Validating queries for ${finalComponents.length} components...`);
6780
+ try {
6781
+ const validationResult = await this.validateAndRetryComponentQueries(
6782
+ finalComponents,
6783
+ collections,
6784
+ apiKey,
6785
+ logCollector
6786
+ );
6787
+ validatedComponents = validationResult.components;
6788
+ const queriedComponents = finalComponents.filter((c) => c.props?.query);
6789
+ const validatedQueries = validatedComponents.filter((c) => c.props?.query);
6790
+ logger.info(`[${this.getProviderName()}] Query validation complete: ${validatedQueries.length}/${queriedComponents.length} queries validated`);
6791
+ logCollector?.info(`Query validation complete: ${validatedQueries.length}/${queriedComponents.length} queries validated`);
6792
+ } catch (validationError) {
6793
+ const validationErrorMsg = validationError instanceof Error ? validationError.message : String(validationError);
6794
+ logger.error(`[${this.getProviderName()}] Query validation error: ${validationErrorMsg}`);
6795
+ logCollector?.error(`Query validation error: ${validationErrorMsg}`);
6796
+ }
6797
+ } else {
6798
+ logger.debug(`[${this.getProviderName()}] Skipping query validation - database execute function not available`);
6799
+ }
6452
6800
  const methodDuration = Date.now() - methodStartTime;
6453
- logger.info(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration}ms | components: ${finalComponents.length} | actions: ${actions.length}`);
6801
+ logger.info(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration}ms | components: ${validatedComponents.length} | actions: ${actions.length}`);
6454
6802
  return {
6455
- components: finalComponents,
6803
+ components: validatedComponents,
6456
6804
  layoutTitle,
6457
6805
  layoutDescription,
6458
6806
  actions
@@ -6470,6 +6818,117 @@ ${executedToolsText}`);
6470
6818
  };
6471
6819
  }
6472
6820
  }
6821
+ /**
6822
+ * Validate component queries against the database and retry with LLM fixes if they fail
6823
+ * @param components - Array of components with potential queries
6824
+ * @param collections - Collections object containing database execute function
6825
+ * @param apiKey - Optional API key for LLM calls
6826
+ * @param logCollector - Optional log collector for logging
6827
+ * @returns Object with validated components and a map of query results
6828
+ */
6829
+ async validateAndRetryComponentQueries(components, collections, apiKey, logCollector) {
6830
+ const MAX_RETRIES = 3;
6831
+ const queryResults = /* @__PURE__ */ new Map();
6832
+ const validatedComponents = [];
6833
+ const queryAttempts = /* @__PURE__ */ new Map();
6834
+ const queryValidationStatus = /* @__PURE__ */ new Map();
6835
+ for (const component of components) {
6836
+ const query = component.props?.query;
6837
+ if (!query) {
6838
+ validatedComponents.push(component);
6839
+ continue;
6840
+ }
6841
+ const queryKey = this.getQueryCacheKey(query);
6842
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
6843
+ if (queryValidationStatus.has(queryKey)) {
6844
+ if (queryValidationStatus.get(queryKey)) {
6845
+ validatedComponents.push(component);
6846
+ if (queryResults.has(queryKey)) {
6847
+ queryResults.set(`${component.id}:${queryKey}`, queryResults.get(queryKey));
6848
+ }
6849
+ } else {
6850
+ logger.warn(`[${this.getProviderName()}] Component ${component.name} has previously failed query, keeping as-is`);
6851
+ validatedComponents.push(component);
6852
+ }
6853
+ continue;
6854
+ }
6855
+ let attempts = queryAttempts.get(queryKey) || 0;
6856
+ let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
6857
+ let currentQueryStr = queryStr;
6858
+ let validated = false;
6859
+ let lastError = "";
6860
+ logger.info(`[${this.getProviderName()}] Validating query for component: ${component.name} (${component.type})`);
6861
+ while (attempts < MAX_RETRIES && !validated) {
6862
+ attempts++;
6863
+ queryAttempts.set(queryKey, attempts);
6864
+ try {
6865
+ logger.debug(`[${this.getProviderName()}] Query validation attempt ${attempts}/${MAX_RETRIES} for ${component.name}`);
6866
+ const { result, cacheKey } = await this.executeQueryForValidation(currentQuery, collections);
6867
+ validated = true;
6868
+ queryValidationStatus.set(queryKey, true);
6869
+ queryResults.set(queryKey, result);
6870
+ queryResults.set(`${component.id}:${queryKey}`, result);
6871
+ queryCache.set(cacheKey, result);
6872
+ logger.info(`[${this.getProviderName()}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
6873
+ logCollector?.info(`\u2713 Query validated for ${component.name}`);
6874
+ if (currentQueryStr !== queryStr) {
6875
+ component.props = {
6876
+ ...component.props,
6877
+ query: typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr }
6878
+ };
6879
+ logger.info(`[${this.getProviderName()}] Updated ${component.name} with fixed query`);
6880
+ }
6881
+ } catch (error) {
6882
+ lastError = error instanceof Error ? error.message : String(error);
6883
+ logger.warn(`[${this.getProviderName()}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6884
+ logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
6885
+ if (attempts >= MAX_RETRIES) {
6886
+ logger.error(`[${this.getProviderName()}] \u2717 Max retries reached for ${component.name}, keeping original component`);
6887
+ logCollector?.error(`Max retries reached for ${component.name}, query may fail at runtime`);
6888
+ queryValidationStatus.set(queryKey, false);
6889
+ break;
6890
+ }
6891
+ logger.info(`[${this.getProviderName()}] Requesting query fix from LLM for ${component.name}...`);
6892
+ logCollector?.info(`Requesting query fix for ${component.name}...`);
6893
+ try {
6894
+ const fixedQueryStr = await this.requestQueryFix(
6895
+ currentQueryStr,
6896
+ lastError,
6897
+ {
6898
+ name: component.name,
6899
+ type: component.type,
6900
+ title: component.props?.title
6901
+ },
6902
+ apiKey
6903
+ );
6904
+ if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
6905
+ logger.info(`[${this.getProviderName()}] Received fixed query for ${component.name}, retrying...`);
6906
+ currentQueryStr = fixedQueryStr;
6907
+ if (typeof currentQuery === "string") {
6908
+ currentQuery = fixedQueryStr;
6909
+ } else {
6910
+ currentQuery = { ...currentQuery, sql: fixedQueryStr };
6911
+ }
6912
+ } else {
6913
+ logger.warn(`[${this.getProviderName()}] LLM returned same or empty query, stopping retries`);
6914
+ queryValidationStatus.set(queryKey, false);
6915
+ break;
6916
+ }
6917
+ } catch (fixError) {
6918
+ const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6919
+ logger.error(`[${this.getProviderName()}] Failed to get query fix from LLM: ${fixErrorMsg}`);
6920
+ queryValidationStatus.set(queryKey, false);
6921
+ break;
6922
+ }
6923
+ }
6924
+ }
6925
+ validatedComponents.push(component);
6926
+ }
6927
+ return {
6928
+ components: validatedComponents,
6929
+ queryResults
6930
+ };
6931
+ }
6473
6932
  /**
6474
6933
  * Classify user question into category and detect external tools needed
6475
6934
  * Determines if question is for data analysis, requires external tools, or needs text response
@@ -6651,12 +7110,6 @@ ${executedToolsText}`);
6651
7110
  * This provides conversational text responses instead of component generation
6652
7111
  * Supports tool calling for query execution with automatic retry on errors (max 3 attempts)
6653
7112
  * After generating text response, if components are provided, matches suggested components
6654
- * @param streamCallback - Optional callback function to receive text chunks as they stream
6655
- * @param collections - Collection registry for executing database queries via database.execute
6656
- * @param components - Optional list of available components for matching suggestions
6657
- * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called
6658
- * @param category - Question category ('data_analysis' | 'data_modification' | 'general'). For data_modification, answer component streaming is skipped. For general, component generation is skipped entirely.
6659
- * @param userId - Optional user ID for fetching user-specific knowledge base nodes
6660
7113
  */
6661
7114
  async generateTextResponse(userPrompt, apiKey, logCollector, conversationHistory, streamCallback, collections, components, externalTools, category, userId) {
6662
7115
  const methodStartTime = Date.now();
@@ -7247,11 +7700,14 @@ ${errorMsg}
7247
7700
  logger.info(`[${this.getProviderName()}] Generated ${actions.length} follow-up actions for general question`);
7248
7701
  } else if (components && components.length > 0) {
7249
7702
  logger.info(`[${this.getProviderName()}] Matching components from text response...`);
7703
+ logger.info(`[${this.getProviderName()}] componentStreamCallback setup: wrappedStreamCallback=${!!wrappedStreamCallback}, category=${category}`);
7250
7704
  const componentStreamCallback = wrappedStreamCallback && category !== "data_modification" ? (component) => {
7705
+ logger.info(`[${this.getProviderName()}] componentStreamCallback INVOKED for: ${component.name} (${component.type})`);
7251
7706
  const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
7252
7707
  wrappedStreamCallback(answerMarker);
7253
7708
  logger.info(`[${this.getProviderName()}] Streamed answer component to frontend: ${component.name} (${component.type})`);
7254
7709
  } : void 0;
7710
+ logger.info(`[${this.getProviderName()}] componentStreamCallback created: ${!!componentStreamCallback}`);
7255
7711
  const deferredTools = externalTools?.filter((t) => {
7256
7712
  if (t.executionType === "deferred" && !t.userProvidedData) return true;
7257
7713
  if (category === "data_modification" && !t.userProvidedData) {
@@ -7346,11 +7802,6 @@ ${errorMsg}
7346
7802
  * - If match found → Adapt UI block parameters and return
7347
7803
  * 2. Category classification: Determine if data_analysis, requires_external_tools, or text_response
7348
7804
  * 3. Route appropriately based on category and response mode
7349
- *
7350
- * @param responseMode - 'component' for component generation (default), 'text' for text responses
7351
- * @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
7352
- * @param collections - Collection registry for executing database queries (required for text mode)
7353
- * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
7354
7805
  */
7355
7806
  async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
7356
7807
  const startTime = Date.now();
@@ -7364,7 +7815,7 @@ ${errorMsg}
7364
7815
  userPrompt,
7365
7816
  collections,
7366
7817
  userId,
7367
- similarityThreshold: this.conversationSimilarityThreshold
7818
+ similarityThreshold: 0.99
7368
7819
  });
7369
7820
  if (conversationMatch) {
7370
7821
  logger.info(`[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`);
@@ -7515,16 +7966,6 @@ ${errorMsg}
7515
7966
  };
7516
7967
  }) || [];
7517
7968
  }
7518
- if (categoryClassification.category === "general") {
7519
- logger.info(`[${this.getProviderName()}] Routing to general conversation (no database operations)`);
7520
- logCollector?.info("Routing to general conversation...");
7521
- } else if (categoryClassification.category === "data_analysis") {
7522
- logger.info(`[${this.getProviderName()}] Routing to data analysis (SELECT operations)`);
7523
- logCollector?.info("Routing to data analysis...");
7524
- } else if (categoryClassification.category === "data_modification") {
7525
- logger.info(`[${this.getProviderName()}] Routing to data modification (INSERT/UPDATE/DELETE operations)`);
7526
- logCollector?.info("Routing to data modification...");
7527
- }
7528
7969
  const textResponse = await this.generateTextResponse(
7529
7970
  userPrompt,
7530
7971
  apiKey,
@@ -12947,7 +13388,10 @@ var SuperatomSDK = class {
12947
13388
  this.conversationSimilarityThreshold = config.conversationSimilarityThreshold ?? 0.8;
12948
13389
  this.applyModelStrategy(this.modelStrategy);
12949
13390
  this.applyConversationSimilarityThreshold(this.conversationSimilarityThreshold);
12950
- 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}`);
13391
+ if (config.queryCacheTTL !== void 0) {
13392
+ queryCache.setTTL(config.queryCacheTTL);
13393
+ }
13394
+ 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`);
12951
13395
  this.userManager = new UserManager(this.projectId, 5e3);
12952
13396
  this.dashboardManager = new DashboardManager(this.projectId);
12953
13397
  this.reportManager = new ReportManager(this.projectId);
@@ -13407,6 +13851,7 @@ var SuperatomSDK = class {
13407
13851
  llmUsageLogger,
13408
13852
  logger,
13409
13853
  openaiLLM,
13854
+ queryCache,
13410
13855
  rerankChromaResults,
13411
13856
  rerankConversationResults,
13412
13857
  userPromptErrorLogger