@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.mjs CHANGED
@@ -3124,7 +3124,136 @@ var ThreadManager = class _ThreadManager {
3124
3124
  }
3125
3125
  };
3126
3126
 
3127
+ // src/utils/query-cache.ts
3128
+ init_logger();
3129
+ var QueryCache = class {
3130
+ constructor() {
3131
+ this.cache = /* @__PURE__ */ new Map();
3132
+ this.ttlMs = 5 * 60 * 1e3;
3133
+ // Default: 5 minutes
3134
+ this.cleanupInterval = null;
3135
+ this.startCleanup();
3136
+ }
3137
+ /**
3138
+ * Set the cache TTL (Time To Live)
3139
+ * @param minutes - TTL in minutes (default: 5)
3140
+ */
3141
+ setTTL(minutes) {
3142
+ this.ttlMs = minutes * 60 * 1e3;
3143
+ logger.info(`[QueryCache] TTL set to ${minutes} minutes`);
3144
+ }
3145
+ /**
3146
+ * Get the current TTL in minutes
3147
+ */
3148
+ getTTL() {
3149
+ return this.ttlMs / 60 / 1e3;
3150
+ }
3151
+ /**
3152
+ * Store query result in cache
3153
+ * Key is the exact query string (or JSON for parameterized queries)
3154
+ */
3155
+ set(query, data) {
3156
+ this.cache.set(query, {
3157
+ query,
3158
+ data,
3159
+ timestamp: Date.now()
3160
+ });
3161
+ logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
3162
+ }
3163
+ /**
3164
+ * Get cached result if exists and not expired
3165
+ */
3166
+ get(query) {
3167
+ const entry = this.cache.get(query);
3168
+ if (!entry) {
3169
+ return null;
3170
+ }
3171
+ if (Date.now() - entry.timestamp > this.ttlMs) {
3172
+ this.cache.delete(query);
3173
+ logger.debug(`[QueryCache] Entry expired for query (${query.substring(0, 50)}...)`);
3174
+ return null;
3175
+ }
3176
+ logger.info(`[QueryCache] Cache HIT for query (${query.substring(0, 50)}...)`);
3177
+ return entry.data;
3178
+ }
3179
+ /**
3180
+ * Check if query exists in cache (not expired)
3181
+ */
3182
+ has(query) {
3183
+ return this.get(query) !== null;
3184
+ }
3185
+ /**
3186
+ * Remove a specific query from cache
3187
+ */
3188
+ delete(query) {
3189
+ this.cache.delete(query);
3190
+ }
3191
+ /**
3192
+ * Clear all cached entries
3193
+ */
3194
+ clear() {
3195
+ this.cache.clear();
3196
+ logger.info("[QueryCache] Cache cleared");
3197
+ }
3198
+ /**
3199
+ * Get cache statistics
3200
+ */
3201
+ getStats() {
3202
+ let oldestTimestamp = null;
3203
+ for (const entry of this.cache.values()) {
3204
+ if (oldestTimestamp === null || entry.timestamp < oldestTimestamp) {
3205
+ oldestTimestamp = entry.timestamp;
3206
+ }
3207
+ }
3208
+ return {
3209
+ size: this.cache.size,
3210
+ oldestEntryAge: oldestTimestamp ? Date.now() - oldestTimestamp : null
3211
+ };
3212
+ }
3213
+ /**
3214
+ * Start periodic cleanup of expired entries
3215
+ */
3216
+ startCleanup() {
3217
+ this.cleanupInterval = setInterval(() => {
3218
+ const now = Date.now();
3219
+ let expiredCount = 0;
3220
+ for (const [key, entry] of this.cache.entries()) {
3221
+ if (now - entry.timestamp > this.ttlMs) {
3222
+ this.cache.delete(key);
3223
+ expiredCount++;
3224
+ }
3225
+ }
3226
+ if (expiredCount > 0) {
3227
+ logger.debug(`[QueryCache] Cleaned up ${expiredCount} expired entries`);
3228
+ }
3229
+ }, 2 * 60 * 1e3);
3230
+ }
3231
+ /**
3232
+ * Stop cleanup interval (for graceful shutdown)
3233
+ */
3234
+ destroy() {
3235
+ if (this.cleanupInterval) {
3236
+ clearInterval(this.cleanupInterval);
3237
+ this.cleanupInterval = null;
3238
+ }
3239
+ this.cache.clear();
3240
+ }
3241
+ };
3242
+ var queryCache = new QueryCache();
3243
+
3127
3244
  // src/handlers/data-request.ts
3245
+ function getQueryCacheKey(query) {
3246
+ if (typeof query === "string") {
3247
+ return query;
3248
+ } else if (query?.sql) {
3249
+ const values = query.values || query.params;
3250
+ if (values && Object.keys(values).length > 0) {
3251
+ return JSON.stringify({ sql: query.sql, values });
3252
+ }
3253
+ return query.sql;
3254
+ }
3255
+ return "";
3256
+ }
3128
3257
  async function handleDataRequest(data, collections, sendMessage) {
3129
3258
  try {
3130
3259
  const dataRequest = DataRequestMessageSchema.parse(data);
@@ -3143,10 +3272,37 @@ async function handleDataRequest(data, collections, sendMessage) {
3143
3272
  return;
3144
3273
  }
3145
3274
  const startTime = performance.now();
3146
- const handler = collections[collection][op];
3147
- const result = await handler(params || {});
3275
+ let result;
3276
+ let fromCache = false;
3277
+ if (collection === "database" && op === "execute" && params?.sql) {
3278
+ const cacheKey = getQueryCacheKey(params.sql);
3279
+ if (cacheKey) {
3280
+ const cachedResult = queryCache.get(cacheKey);
3281
+ if (cachedResult !== null) {
3282
+ result = cachedResult;
3283
+ fromCache = true;
3284
+ logger.info(`[QueryCache] Returning cached result for database.execute`);
3285
+ }
3286
+ }
3287
+ }
3288
+ if (!fromCache) {
3289
+ const handler = collections[collection][op];
3290
+ let handlerParams = params || {};
3291
+ if (collection === "database" && op === "execute" && params?.sql && typeof params.sql !== "string") {
3292
+ const cacheKey = getQueryCacheKey(params.sql);
3293
+ handlerParams = { ...params, sql: cacheKey };
3294
+ logger.debug(`[data-request] Converted object query to JSON string for database handler`);
3295
+ }
3296
+ result = await handler(handlerParams);
3297
+ if (collection === "database" && op === "execute" && params?.sql && result) {
3298
+ const cacheKey = getQueryCacheKey(params.sql);
3299
+ if (cacheKey) {
3300
+ queryCache.set(cacheKey, result);
3301
+ }
3302
+ }
3303
+ }
3148
3304
  const executionMs = Math.round(performance.now() - startTime);
3149
- logger.info(`Executed ${collection}.${op} in ${executionMs}ms`);
3305
+ logger.info(`Executed ${collection}.${op} in ${executionMs}ms${fromCache ? " (from cache)" : ""}`);
3150
3306
  if (SA_RUNTIME && typeof SA_RUNTIME === "object" && "uiBlockId" in SA_RUNTIME) {
3151
3307
  const uiBlockId = SA_RUNTIME.uiBlockId;
3152
3308
  const threadId = SA_RUNTIME.threadId;
@@ -6039,6 +6195,96 @@ var BaseLLM = class {
6039
6195
  }
6040
6196
  return false;
6041
6197
  }
6198
+ /**
6199
+ * Get the cache key for a query (the exact sql param that would be sent to execute)
6200
+ * This ensures the cache key matches what the frontend will send
6201
+ * Used for both caching and internal deduplication
6202
+ */
6203
+ getQueryCacheKey(query) {
6204
+ if (typeof query === "string") {
6205
+ return query;
6206
+ } else if (query?.sql) {
6207
+ const values = query.values || query.params;
6208
+ if (values && Object.keys(values).length > 0) {
6209
+ return JSON.stringify({ sql: query.sql, values });
6210
+ } else {
6211
+ return query.sql;
6212
+ }
6213
+ }
6214
+ return "";
6215
+ }
6216
+ /**
6217
+ * Execute a query against the database for validation and caching
6218
+ * @param query - The SQL query to execute (string or object with sql/values)
6219
+ * @param collections - Collections object containing database execute function
6220
+ * @returns Object with result data and cache key
6221
+ * @throws Error if query execution fails
6222
+ */
6223
+ async executeQueryForValidation(query, collections) {
6224
+ const cacheKey = this.getQueryCacheKey(query);
6225
+ if (!cacheKey) {
6226
+ throw new Error("Invalid query format: expected string or object with sql property");
6227
+ }
6228
+ const result = await collections["database"]["execute"]({ sql: cacheKey });
6229
+ return { result, cacheKey };
6230
+ }
6231
+ /**
6232
+ * Request the LLM to fix a failed SQL query
6233
+ * @param failedQuery - The query that failed execution
6234
+ * @param errorMessage - The error message from the failed execution
6235
+ * @param componentContext - Context about the component (name, type, title)
6236
+ * @param apiKey - Optional API key
6237
+ * @returns Fixed query string
6238
+ */
6239
+ async requestQueryFix(failedQuery, errorMessage, componentContext, apiKey) {
6240
+ const schemaDoc = schema.generateSchemaDocumentation();
6241
+ const databaseRules = await promptLoader.loadDatabaseRules();
6242
+ const prompt = `You are a SQL expert. Fix the following SQL query that failed execution.
6243
+
6244
+ ## Database Schema
6245
+ ${schemaDoc}
6246
+
6247
+ ## Database-Specific SQL Rules
6248
+ ${databaseRules}
6249
+
6250
+ ## Component Context
6251
+ - Component Name: ${componentContext.name}
6252
+ - Component Type: ${componentContext.type}
6253
+ - Title: ${componentContext.title || "N/A"}
6254
+
6255
+ ## Failed Query
6256
+ \`\`\`sql
6257
+ ${failedQuery}
6258
+ \`\`\`
6259
+
6260
+ ## Error Message
6261
+ ${errorMessage}
6262
+
6263
+ ## Instructions
6264
+ 1. Analyze the error message and identify what caused the query to fail
6265
+ 2. Fix the query to resolve the error while preserving the original intent
6266
+ 3. Ensure the fixed query follows the database-specific SQL rules above
6267
+ 4. Return ONLY the fixed SQL query, no explanations or markdown
6268
+
6269
+ Fixed SQL query:`;
6270
+ const response = await LLM.text(
6271
+ {
6272
+ sys: "You are a SQL expert. Return only the fixed SQL query with no additional text, explanations, or markdown formatting.",
6273
+ user: prompt
6274
+ },
6275
+ {
6276
+ model: this.getModelForTask("simple"),
6277
+ maxTokens: 2048,
6278
+ temperature: 0.1,
6279
+ apiKey: this.getApiKey(apiKey)
6280
+ }
6281
+ );
6282
+ let fixedQuery = response.trim();
6283
+ fixedQuery = fixedQuery.replace(/^```sql\s*/i, "").replace(/\s*```$/i, "");
6284
+ fixedQuery = fixedQuery.replace(/^```\s*/i, "").replace(/\s*```$/i, "");
6285
+ const { query: validatedQuery } = validateAndFixSqlQuery(fixedQuery);
6286
+ return validatedQuery;
6287
+ }
6042
6288
  /**
6043
6289
  * Match components from text response suggestions and generate follow-up questions
6044
6290
  * Takes a text response with component suggestions (c1:type format) and matches with available components
@@ -6075,11 +6321,11 @@ var BaseLLM = class {
6075
6321
  logger.info(`[${this.getProviderName()}] Passing ${deferredTools.length} deferred tools to component matching`);
6076
6322
  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) => {
6077
6323
  return `${idx + 1}. **${tool.name}**
6078
- toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
6079
- toolName: "${tool.name}"
6080
- parameters: ${JSON.stringify(tool.params || {})}
6081
- requiredFields:
6082
- ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6324
+ toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
6325
+ toolName: "${tool.name}"
6326
+ parameters: ${JSON.stringify(tool.params || {})}
6327
+ requiredFields:
6328
+ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6083
6329
  }).join("\n\n");
6084
6330
  }
6085
6331
  let executedToolsText = "No external tools were executed for data fetching.";
@@ -6095,8 +6341,8 @@ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6095
6341
  const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
6096
6342
  const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
6097
6343
  fieldNamesList = `
6098
- \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
6099
- \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
6344
+ \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
6345
+ \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
6100
6346
  const fieldsText = fields.map(
6101
6347
  (f) => ` "${f.name}" (${f.type}): ${f.description}`
6102
6348
  ).join("\n");
@@ -6105,11 +6351,11 @@ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6105
6351
  ${fieldsText}`;
6106
6352
  }
6107
6353
  return `${idx + 1}. **${tool.name}**
6108
- toolId: "${tool.id}"
6109
- toolName: "${tool.name}"
6110
- parameters: ${JSON.stringify(tool.params || {})}
6111
- recordCount: ${recordCount} rows returned
6112
- outputSchema: ${outputSchemaText}${fieldNamesList}`;
6354
+ toolId: "${tool.id}"
6355
+ toolName: "${tool.name}"
6356
+ parameters: ${JSON.stringify(tool.params || {})}
6357
+ recordCount: ${recordCount} rows returned
6358
+ outputSchema: ${outputSchemaText}${fieldNamesList}`;
6113
6359
  }).join("\n\n");
6114
6360
  }
6115
6361
  const schemaDoc = schema.generateSchemaDocumentation();
@@ -6160,6 +6406,7 @@ ${executedToolsText}`);
6160
6406
  logCollector?.info("Matching components from text response...");
6161
6407
  let fullResponseText = "";
6162
6408
  let answerComponentExtracted = false;
6409
+ let validatedAnswerComponent = null;
6163
6410
  const answerCallback = componentStreamCallback;
6164
6411
  const partialCallback = answerCallback ? (chunk) => {
6165
6412
  fullResponseText += chunk;
@@ -6218,8 +6465,8 @@ ${executedToolsText}`);
6218
6465
  }
6219
6466
  };
6220
6467
  const streamTime = (/* @__PURE__ */ new Date()).toISOString();
6221
- logger.info(`[${this.getProviderName()}] \u2713 [${streamTime}] Answer component detected in stream: ${answerComponent.name} (${answerComponent.type}) - STREAMING TO FRONTEND NOW`);
6222
- logCollector?.info(`\u2713 Answer component: ${answerComponent.name} (${answerComponent.type}) - streaming to frontend at ${streamTime}`);
6468
+ logger.info(`[${this.getProviderName()}] \u2713 [${streamTime}] Answer component detected in stream: ${answerComponent.name} (${answerComponent.type})`);
6469
+ logCollector?.info(`\u2713 Answer component: ${answerComponent.name} (${answerComponent.type}) - detected at ${streamTime}`);
6223
6470
  if (answerComponentData.props?.query) {
6224
6471
  logCollector?.logQuery(
6225
6472
  "Answer component query",
@@ -6227,7 +6474,79 @@ ${executedToolsText}`);
6227
6474
  { componentName: answerComponent.name, componentType: answerComponent.type, reasoning: answerComponentData.reasoning }
6228
6475
  );
6229
6476
  }
6230
- answerCallback(answerComponent);
6477
+ const answerQuery = answerComponent.props?.query;
6478
+ logger.info(`[${this.getProviderName()}] Answer component detected: ${answerComponent.name} (${answerComponent.type}), hasQuery: ${!!answerQuery}, hasDbExecute: ${!!collections?.["database"]?.["execute"]}`);
6479
+ if (answerQuery && collections?.["database"]?.["execute"]) {
6480
+ (async () => {
6481
+ const MAX_RETRIES = 3;
6482
+ let attempts = 0;
6483
+ let validated = false;
6484
+ let currentQuery = answerQuery;
6485
+ let currentQueryStr = typeof answerQuery === "string" ? answerQuery : answerQuery?.sql || "";
6486
+ let lastError = "";
6487
+ logger.info(`[${this.getProviderName()}] Validating answer component query before streaming...`);
6488
+ while (attempts < MAX_RETRIES && !validated) {
6489
+ attempts++;
6490
+ try {
6491
+ const cacheKey = this.getQueryCacheKey(currentQuery);
6492
+ if (cacheKey) {
6493
+ logger.debug(`[${this.getProviderName()}] Answer component query validation attempt ${attempts}/${MAX_RETRIES}`);
6494
+ const result2 = await collections["database"]["execute"]({ sql: cacheKey });
6495
+ queryCache.set(cacheKey, result2);
6496
+ validated = true;
6497
+ if (currentQuery !== answerQuery) {
6498
+ answerComponent.props.query = currentQuery;
6499
+ }
6500
+ validatedAnswerComponent = {
6501
+ componentId: answerComponentData.componentId,
6502
+ validatedQuery: currentQuery
6503
+ };
6504
+ logger.info(`[${this.getProviderName()}] \u2713 Answer component query validated (attempt ${attempts}) - STREAMING TO FRONTEND NOW`);
6505
+ logCollector?.info(`\u2713 Answer component query validated - streaming to frontend`);
6506
+ logger.info(`[${this.getProviderName()}] Calling answerCallback for: ${answerComponent.name}`);
6507
+ answerCallback(answerComponent);
6508
+ logger.info(`[${this.getProviderName()}] answerCallback completed for: ${answerComponent.name}`);
6509
+ }
6510
+ } catch (validationError) {
6511
+ lastError = validationError instanceof Error ? validationError.message : String(validationError);
6512
+ logger.warn(`[${this.getProviderName()}] Answer component query validation failed (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6513
+ if (attempts < MAX_RETRIES) {
6514
+ try {
6515
+ logger.info(`[${this.getProviderName()}] Requesting LLM to fix answer component query...`);
6516
+ const fixedQueryStr = await this.requestQueryFix(
6517
+ currentQueryStr,
6518
+ lastError,
6519
+ {
6520
+ name: answerComponent.name,
6521
+ type: answerComponent.type,
6522
+ title: answerComponent.props?.title
6523
+ },
6524
+ apiKey
6525
+ );
6526
+ if (typeof currentQuery === "string") {
6527
+ currentQuery = fixedQueryStr;
6528
+ } else {
6529
+ currentQuery = { ...currentQuery, sql: fixedQueryStr };
6530
+ }
6531
+ currentQueryStr = fixedQueryStr;
6532
+ logger.info(`[${this.getProviderName()}] LLM provided fixed query for answer component, retrying...`);
6533
+ } catch (fixError) {
6534
+ const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6535
+ logger.error(`[${this.getProviderName()}] Failed to get LLM query fix for answer component: ${fixErrorMsg}`);
6536
+ break;
6537
+ }
6538
+ }
6539
+ }
6540
+ }
6541
+ if (!validated) {
6542
+ logger.warn(`[${this.getProviderName()}] Answer component query validation failed after ${attempts} attempts - skipping early stream`);
6543
+ logCollector?.warn(`Answer component query validation failed: ${lastError} - will be handled in batch validation`);
6544
+ }
6545
+ })();
6546
+ } else {
6547
+ logger.info(`[${this.getProviderName()}] Answer component has no query - STREAMING TO FRONTEND NOW`);
6548
+ answerCallback(answerComponent);
6549
+ }
6231
6550
  answerComponentExtracted = true;
6232
6551
  }
6233
6552
  }
@@ -6281,6 +6600,10 @@ ${executedToolsText}`);
6281
6600
  return null;
6282
6601
  }
6283
6602
  let cleanedProps = { ...mc.props };
6603
+ if (validatedAnswerComponent && mc.componentId === validatedAnswerComponent.componentId) {
6604
+ logger.info(`[${this.getProviderName()}] Using pre-validated query for answer component: ${mc.componentId}`);
6605
+ cleanedProps.query = validatedAnswerComponent.validatedQuery;
6606
+ }
6284
6607
  if (cleanedProps.externalTool) {
6285
6608
  const toolId = cleanedProps.externalTool.toolId;
6286
6609
  const validToolIds = (executedTools || []).map((t) => t.id);
@@ -6399,10 +6722,34 @@ ${executedToolsText}`);
6399
6722
  }
6400
6723
  };
6401
6724
  }).filter(Boolean);
6725
+ let validatedComponents = finalComponents;
6726
+ if (collections?.["database"]?.["execute"]) {
6727
+ logger.info(`[${this.getProviderName()}] Starting query validation for ${finalComponents.length} components...`);
6728
+ logCollector?.info(`Validating queries for ${finalComponents.length} components...`);
6729
+ try {
6730
+ const validationResult = await this.validateAndRetryComponentQueries(
6731
+ finalComponents,
6732
+ collections,
6733
+ apiKey,
6734
+ logCollector
6735
+ );
6736
+ validatedComponents = validationResult.components;
6737
+ const queriedComponents = finalComponents.filter((c) => c.props?.query);
6738
+ const validatedQueries = validatedComponents.filter((c) => c.props?.query);
6739
+ logger.info(`[${this.getProviderName()}] Query validation complete: ${validatedQueries.length}/${queriedComponents.length} queries validated`);
6740
+ logCollector?.info(`Query validation complete: ${validatedQueries.length}/${queriedComponents.length} queries validated`);
6741
+ } catch (validationError) {
6742
+ const validationErrorMsg = validationError instanceof Error ? validationError.message : String(validationError);
6743
+ logger.error(`[${this.getProviderName()}] Query validation error: ${validationErrorMsg}`);
6744
+ logCollector?.error(`Query validation error: ${validationErrorMsg}`);
6745
+ }
6746
+ } else {
6747
+ logger.debug(`[${this.getProviderName()}] Skipping query validation - database execute function not available`);
6748
+ }
6402
6749
  const methodDuration = Date.now() - methodStartTime;
6403
- logger.info(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration}ms | components: ${finalComponents.length} | actions: ${actions.length}`);
6750
+ logger.info(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration}ms | components: ${validatedComponents.length} | actions: ${actions.length}`);
6404
6751
  return {
6405
- components: finalComponents,
6752
+ components: validatedComponents,
6406
6753
  layoutTitle,
6407
6754
  layoutDescription,
6408
6755
  actions
@@ -6420,6 +6767,117 @@ ${executedToolsText}`);
6420
6767
  };
6421
6768
  }
6422
6769
  }
6770
+ /**
6771
+ * Validate component queries against the database and retry with LLM fixes if they fail
6772
+ * @param components - Array of components with potential queries
6773
+ * @param collections - Collections object containing database execute function
6774
+ * @param apiKey - Optional API key for LLM calls
6775
+ * @param logCollector - Optional log collector for logging
6776
+ * @returns Object with validated components and a map of query results
6777
+ */
6778
+ async validateAndRetryComponentQueries(components, collections, apiKey, logCollector) {
6779
+ const MAX_RETRIES = 3;
6780
+ const queryResults = /* @__PURE__ */ new Map();
6781
+ const validatedComponents = [];
6782
+ const queryAttempts = /* @__PURE__ */ new Map();
6783
+ const queryValidationStatus = /* @__PURE__ */ new Map();
6784
+ for (const component of components) {
6785
+ const query = component.props?.query;
6786
+ if (!query) {
6787
+ validatedComponents.push(component);
6788
+ continue;
6789
+ }
6790
+ const queryKey = this.getQueryCacheKey(query);
6791
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
6792
+ if (queryValidationStatus.has(queryKey)) {
6793
+ if (queryValidationStatus.get(queryKey)) {
6794
+ validatedComponents.push(component);
6795
+ if (queryResults.has(queryKey)) {
6796
+ queryResults.set(`${component.id}:${queryKey}`, queryResults.get(queryKey));
6797
+ }
6798
+ } else {
6799
+ logger.warn(`[${this.getProviderName()}] Component ${component.name} has previously failed query, keeping as-is`);
6800
+ validatedComponents.push(component);
6801
+ }
6802
+ continue;
6803
+ }
6804
+ let attempts = queryAttempts.get(queryKey) || 0;
6805
+ let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
6806
+ let currentQueryStr = queryStr;
6807
+ let validated = false;
6808
+ let lastError = "";
6809
+ logger.info(`[${this.getProviderName()}] Validating query for component: ${component.name} (${component.type})`);
6810
+ while (attempts < MAX_RETRIES && !validated) {
6811
+ attempts++;
6812
+ queryAttempts.set(queryKey, attempts);
6813
+ try {
6814
+ logger.debug(`[${this.getProviderName()}] Query validation attempt ${attempts}/${MAX_RETRIES} for ${component.name}`);
6815
+ const { result, cacheKey } = await this.executeQueryForValidation(currentQuery, collections);
6816
+ validated = true;
6817
+ queryValidationStatus.set(queryKey, true);
6818
+ queryResults.set(queryKey, result);
6819
+ queryResults.set(`${component.id}:${queryKey}`, result);
6820
+ queryCache.set(cacheKey, result);
6821
+ logger.info(`[${this.getProviderName()}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
6822
+ logCollector?.info(`\u2713 Query validated for ${component.name}`);
6823
+ if (currentQueryStr !== queryStr) {
6824
+ component.props = {
6825
+ ...component.props,
6826
+ query: typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr }
6827
+ };
6828
+ logger.info(`[${this.getProviderName()}] Updated ${component.name} with fixed query`);
6829
+ }
6830
+ } catch (error) {
6831
+ lastError = error instanceof Error ? error.message : String(error);
6832
+ logger.warn(`[${this.getProviderName()}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6833
+ logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
6834
+ if (attempts >= MAX_RETRIES) {
6835
+ logger.error(`[${this.getProviderName()}] \u2717 Max retries reached for ${component.name}, keeping original component`);
6836
+ logCollector?.error(`Max retries reached for ${component.name}, query may fail at runtime`);
6837
+ queryValidationStatus.set(queryKey, false);
6838
+ break;
6839
+ }
6840
+ logger.info(`[${this.getProviderName()}] Requesting query fix from LLM for ${component.name}...`);
6841
+ logCollector?.info(`Requesting query fix for ${component.name}...`);
6842
+ try {
6843
+ const fixedQueryStr = await this.requestQueryFix(
6844
+ currentQueryStr,
6845
+ lastError,
6846
+ {
6847
+ name: component.name,
6848
+ type: component.type,
6849
+ title: component.props?.title
6850
+ },
6851
+ apiKey
6852
+ );
6853
+ if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
6854
+ logger.info(`[${this.getProviderName()}] Received fixed query for ${component.name}, retrying...`);
6855
+ currentQueryStr = fixedQueryStr;
6856
+ if (typeof currentQuery === "string") {
6857
+ currentQuery = fixedQueryStr;
6858
+ } else {
6859
+ currentQuery = { ...currentQuery, sql: fixedQueryStr };
6860
+ }
6861
+ } else {
6862
+ logger.warn(`[${this.getProviderName()}] LLM returned same or empty query, stopping retries`);
6863
+ queryValidationStatus.set(queryKey, false);
6864
+ break;
6865
+ }
6866
+ } catch (fixError) {
6867
+ const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6868
+ logger.error(`[${this.getProviderName()}] Failed to get query fix from LLM: ${fixErrorMsg}`);
6869
+ queryValidationStatus.set(queryKey, false);
6870
+ break;
6871
+ }
6872
+ }
6873
+ }
6874
+ validatedComponents.push(component);
6875
+ }
6876
+ return {
6877
+ components: validatedComponents,
6878
+ queryResults
6879
+ };
6880
+ }
6423
6881
  /**
6424
6882
  * Classify user question into category and detect external tools needed
6425
6883
  * Determines if question is for data analysis, requires external tools, or needs text response
@@ -6601,12 +7059,6 @@ ${executedToolsText}`);
6601
7059
  * This provides conversational text responses instead of component generation
6602
7060
  * Supports tool calling for query execution with automatic retry on errors (max 3 attempts)
6603
7061
  * After generating text response, if components are provided, matches suggested components
6604
- * @param streamCallback - Optional callback function to receive text chunks as they stream
6605
- * @param collections - Collection registry for executing database queries via database.execute
6606
- * @param components - Optional list of available components for matching suggestions
6607
- * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called
6608
- * @param category - Question category ('data_analysis' | 'data_modification' | 'general'). For data_modification, answer component streaming is skipped. For general, component generation is skipped entirely.
6609
- * @param userId - Optional user ID for fetching user-specific knowledge base nodes
6610
7062
  */
6611
7063
  async generateTextResponse(userPrompt, apiKey, logCollector, conversationHistory, streamCallback, collections, components, externalTools, category, userId) {
6612
7064
  const methodStartTime = Date.now();
@@ -7197,11 +7649,14 @@ ${errorMsg}
7197
7649
  logger.info(`[${this.getProviderName()}] Generated ${actions.length} follow-up actions for general question`);
7198
7650
  } else if (components && components.length > 0) {
7199
7651
  logger.info(`[${this.getProviderName()}] Matching components from text response...`);
7652
+ logger.info(`[${this.getProviderName()}] componentStreamCallback setup: wrappedStreamCallback=${!!wrappedStreamCallback}, category=${category}`);
7200
7653
  const componentStreamCallback = wrappedStreamCallback && category !== "data_modification" ? (component) => {
7654
+ logger.info(`[${this.getProviderName()}] componentStreamCallback INVOKED for: ${component.name} (${component.type})`);
7201
7655
  const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
7202
7656
  wrappedStreamCallback(answerMarker);
7203
7657
  logger.info(`[${this.getProviderName()}] Streamed answer component to frontend: ${component.name} (${component.type})`);
7204
7658
  } : void 0;
7659
+ logger.info(`[${this.getProviderName()}] componentStreamCallback created: ${!!componentStreamCallback}`);
7205
7660
  const deferredTools = externalTools?.filter((t) => {
7206
7661
  if (t.executionType === "deferred" && !t.userProvidedData) return true;
7207
7662
  if (category === "data_modification" && !t.userProvidedData) {
@@ -7296,11 +7751,6 @@ ${errorMsg}
7296
7751
  * - If match found → Adapt UI block parameters and return
7297
7752
  * 2. Category classification: Determine if data_analysis, requires_external_tools, or text_response
7298
7753
  * 3. Route appropriately based on category and response mode
7299
- *
7300
- * @param responseMode - 'component' for component generation (default), 'text' for text responses
7301
- * @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
7302
- * @param collections - Collection registry for executing database queries (required for text mode)
7303
- * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
7304
7754
  */
7305
7755
  async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
7306
7756
  const startTime = Date.now();
@@ -7314,7 +7764,7 @@ ${errorMsg}
7314
7764
  userPrompt,
7315
7765
  collections,
7316
7766
  userId,
7317
- similarityThreshold: this.conversationSimilarityThreshold
7767
+ similarityThreshold: 0.99
7318
7768
  });
7319
7769
  if (conversationMatch) {
7320
7770
  logger.info(`[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`);
@@ -7465,16 +7915,6 @@ ${errorMsg}
7465
7915
  };
7466
7916
  }) || [];
7467
7917
  }
7468
- if (categoryClassification.category === "general") {
7469
- logger.info(`[${this.getProviderName()}] Routing to general conversation (no database operations)`);
7470
- logCollector?.info("Routing to general conversation...");
7471
- } else if (categoryClassification.category === "data_analysis") {
7472
- logger.info(`[${this.getProviderName()}] Routing to data analysis (SELECT operations)`);
7473
- logCollector?.info("Routing to data analysis...");
7474
- } else if (categoryClassification.category === "data_modification") {
7475
- logger.info(`[${this.getProviderName()}] Routing to data modification (INSERT/UPDATE/DELETE operations)`);
7476
- logCollector?.info("Routing to data modification...");
7477
- }
7478
7918
  const textResponse = await this.generateTextResponse(
7479
7919
  userPrompt,
7480
7920
  apiKey,
@@ -12897,7 +13337,10 @@ var SuperatomSDK = class {
12897
13337
  this.conversationSimilarityThreshold = config.conversationSimilarityThreshold ?? 0.8;
12898
13338
  this.applyModelStrategy(this.modelStrategy);
12899
13339
  this.applyConversationSimilarityThreshold(this.conversationSimilarityThreshold);
12900
- 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}`);
13340
+ if (config.queryCacheTTL !== void 0) {
13341
+ queryCache.setTTL(config.queryCacheTTL);
13342
+ }
13343
+ 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`);
12901
13344
  this.userManager = new UserManager(this.projectId, 5e3);
12902
13345
  this.dashboardManager = new DashboardManager(this.projectId);
12903
13346
  this.reportManager = new ReportManager(this.projectId);
@@ -13356,6 +13799,7 @@ export {
13356
13799
  llmUsageLogger,
13357
13800
  logger,
13358
13801
  openaiLLM,
13802
+ queryCache,
13359
13803
  rerankChromaResults,
13360
13804
  rerankConversationResults,
13361
13805
  userPromptErrorLogger