@superatomai/sdk-node 0.0.4-mds → 0.0.5-mds

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
@@ -4086,6 +4086,38 @@ var PromptLoader = class {
4086
4086
  logger.warn(`Using default database rules for '${this.databaseType}' (file not found at ${rulesPath})`);
4087
4087
  return defaultRules;
4088
4088
  }
4089
+ /**
4090
+ * Load database-specific SQL rules for a given source type.
4091
+ * Used by the agent architecture where multiple source types can coexist.
4092
+ * @param sourceType - Source type from tool ID (e.g., 'postgres', 'mssql', 'mysql')
4093
+ * @returns Database rules as a string
4094
+ */
4095
+ async loadDatabaseRulesForType(sourceType) {
4096
+ const typeMap = {
4097
+ "postgres": "postgresql",
4098
+ "postgresql": "postgresql",
4099
+ "mssql": "mssql",
4100
+ "mysql": "postgresql"
4101
+ // MySQL uses similar rules to PostgreSQL
4102
+ };
4103
+ const dbType = typeMap[sourceType] || "postgresql";
4104
+ if (this.databaseRulesCache.has(dbType)) {
4105
+ return this.databaseRulesCache.get(dbType);
4106
+ }
4107
+ const rulesPath = import_path.default.join(this.promptsDir, "database-rules", `${dbType}.md`);
4108
+ try {
4109
+ if (import_fs2.default.existsSync(rulesPath)) {
4110
+ const rules = import_fs2.default.readFileSync(rulesPath, "utf-8");
4111
+ this.databaseRulesCache.set(dbType, rules);
4112
+ return rules;
4113
+ }
4114
+ } catch (error) {
4115
+ logger.warn(`Could not load database rules for '${dbType}' from file system: ${error}`);
4116
+ }
4117
+ const defaultRules = this.getDefaultDatabaseRulesForType(dbType);
4118
+ this.databaseRulesCache.set(dbType, defaultRules);
4119
+ return defaultRules;
4120
+ }
4089
4121
  /**
4090
4122
  * Get default database rules as fallback
4091
4123
  * @returns Minimal database rules
@@ -4105,6 +4137,33 @@ var PromptLoader = class {
4105
4137
  }
4106
4138
  return `**Database Type: PostgreSQL**
4107
4139
 
4140
+ **SQL Query Rules:**
4141
+ - Use \`LIMIT N\` for row limiting (e.g., \`SELECT * FROM table LIMIT 32\`)
4142
+ - Use \`true\` / \`false\` for boolean values
4143
+ - Use \`||\` for string concatenation
4144
+ - Use \`NOW()\` for current timestamp
4145
+ - Use \`::TYPE\` or \`CAST()\` for type casting
4146
+ - Use \`RETURNING\` clause for mutations
4147
+ - NULL values: Use \`NULL\` keyword without quotes`;
4148
+ }
4149
+ /**
4150
+ * Get default database rules for a specific type
4151
+ */
4152
+ getDefaultDatabaseRulesForType(dbType) {
4153
+ if (dbType === "mssql") {
4154
+ return `**Database Type: Microsoft SQL Server**
4155
+
4156
+ **SQL Query Rules:**
4157
+ - Use \`TOP N\` for row limiting (e.g., \`SELECT TOP 32 * FROM table\`)
4158
+ - Use \`1\` for true, \`0\` for false (no native boolean)
4159
+ - Use \`+\` or \`CONCAT()\` for string concatenation
4160
+ - Use \`GETDATE()\` for current timestamp
4161
+ - Use \`CAST()\` or \`CONVERT()\` for type casting
4162
+ - Use \`OUTPUT INSERTED.*\` instead of \`RETURNING\`
4163
+ - NULL values: Use \`NULL\` keyword without quotes`;
4164
+ }
4165
+ return `**Database Type: PostgreSQL**
4166
+
4108
4167
  **SQL Query Rules:**
4109
4168
  - Use \`LIMIT N\` for row limiting (e.g., \`SELECT * FROM table LIMIT 32\`)
4110
4169
  - Use \`true\` / \`false\` for boolean values
@@ -6670,9 +6729,11 @@ var SourceAgent = class {
6670
6729
  const { intent, aggregation = "raw" } = input;
6671
6730
  logger.info(`[SourceAgent:${this.tool.name}] Starting | intent: "${intent}" | aggregation: ${aggregation}`);
6672
6731
  if (this.streamBuffer.hasCallback()) {
6732
+ const sourceType = this.extractSourceType();
6733
+ const sourceLabel = sourceType !== "unknown" ? ` (${sourceType})` : "";
6673
6734
  this.streamBuffer.write(`
6674
6735
 
6675
- \u{1F517} **Querying ${this.tool.name}...**
6736
+ \u{1F517} **Connecting to ${this.tool.name}**${sourceLabel}
6676
6737
 
6677
6738
  `);
6678
6739
  await streamDelay();
@@ -6692,7 +6753,7 @@ var SourceAgent = class {
6692
6753
  if (this.attempts > 1 && this.streamBuffer.hasCallback()) {
6693
6754
  this.streamBuffer.write(`
6694
6755
 
6695
- \u{1F504} **Retrying ${this.tool.name} (attempt ${this.attempts}/${this.config.maxRetries})...**
6756
+ \u{1F504} **Query failed, retrying with corrected query** (attempt ${this.attempts}/${this.config.maxRetries})
6696
6757
 
6697
6758
  `);
6698
6759
  await streamDelay();
@@ -6702,10 +6763,29 @@ var SourceAgent = class {
6702
6763
  cappedInput.limit = this.config.maxRowsPerSource;
6703
6764
  }
6704
6765
  queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
6766
+ if (this.streamBuffer.hasCallback() && queryExecuted) {
6767
+ const queryDisplay = cappedInput.sql || cappedInput.query;
6768
+ if (queryDisplay) {
6769
+ this.streamBuffer.write(`\u{1F4DD} **Generated SQL query:**
6770
+ \`\`\`sql
6771
+ ${queryDisplay}
6772
+ \`\`\`
6773
+
6774
+ `);
6775
+ } else {
6776
+ this.streamBuffer.write(`\u{1F4DD} **Query parameters:**
6777
+ \`\`\`json
6778
+ ${JSON.stringify(cappedInput, null, 2)}
6779
+ \`\`\`
6780
+
6781
+ `);
6782
+ }
6783
+ await streamDelay();
6784
+ }
6705
6785
  try {
6706
6786
  const result = await withProgressHeartbeat(
6707
6787
  () => this.tool.fn(cappedInput),
6708
- `Running ${this.tool.name}`,
6788
+ `Executing query`,
6709
6789
  this.streamBuffer
6710
6790
  );
6711
6791
  if (result && result.error) {
@@ -6717,6 +6797,12 @@ Analyze the error and try again with a corrected query.`;
6717
6797
  }
6718
6798
  resultData = result.data || [];
6719
6799
  totalRowsMatched = result.metadata?.totalCount || result.count || resultData.length;
6800
+ if (this.streamBuffer.hasCallback()) {
6801
+ const totalInfo = totalRowsMatched > resultData.length ? ` (${totalRowsMatched} total matched)` : "";
6802
+ this.streamBuffer.write(`\u2705 Retrieved ${resultData.length} rows${totalInfo}
6803
+
6804
+ `);
6805
+ }
6720
6806
  const formattedResult = formatToolResultForLLM(result, {
6721
6807
  toolName: this.tool.name,
6722
6808
  maxRows: 5,
@@ -6732,7 +6818,9 @@ Analyze the error and try again with a corrected query.`;
6732
6818
  _metadata: result.metadata,
6733
6819
  _sampleData: resultData.slice(0, 3)
6734
6820
  },
6735
- outputSchema: this.tool.outputSchema
6821
+ outputSchema: this.tool.outputSchema,
6822
+ sourceSchema: this.tool.description,
6823
+ sourceType: this.extractSourceType()
6736
6824
  };
6737
6825
  const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
6738
6826
  return `\u2705 Query executed successfully. ${resultData.length} rows returned (${totalRowsMatched} total matched). Data is ready \u2014 do NOT call the tool again.
@@ -7085,158 +7173,134 @@ var DEFAULT_AGENT_CONFIG = {
7085
7173
  maxIterations: 10
7086
7174
  };
7087
7175
 
7088
- // src/userResponse/anthropic.ts
7089
- var import_dotenv = __toESM(require("dotenv"));
7090
-
7091
- // src/userResponse/schema.ts
7092
- var import_path4 = __toESM(require("path"));
7093
- var import_fs5 = __toESM(require("fs"));
7094
- var Schema = class {
7095
- constructor(schemaFilePath) {
7096
- this.cachedSchema = null;
7097
- this.schemaFilePath = schemaFilePath || import_path4.default.join(process.cwd(), "../analysis/data/schema.json");
7176
+ // src/userResponse/utils/component-props-processor.ts
7177
+ function validateExternalTool(externalTool, executedTools, providerName) {
7178
+ if (!externalTool) {
7179
+ return { valid: true };
7098
7180
  }
7099
- /**
7100
- * Gets the database schema from the schema file
7101
- * @returns Parsed schema object or null if error occurs
7102
- */
7103
- getDatabaseSchema() {
7104
- try {
7105
- const dir = import_path4.default.dirname(this.schemaFilePath);
7106
- if (!import_fs5.default.existsSync(dir)) {
7107
- logger.info(`Creating directory structure: ${dir}`);
7108
- import_fs5.default.mkdirSync(dir, { recursive: true });
7109
- }
7110
- if (!import_fs5.default.existsSync(this.schemaFilePath)) {
7111
- logger.info(`Schema file does not exist at ${this.schemaFilePath}, creating with empty schema`);
7112
- const initialSchema = {
7113
- database: "",
7114
- schema: "",
7115
- description: "",
7116
- tables: [],
7117
- relationships: []
7118
- };
7119
- import_fs5.default.writeFileSync(this.schemaFilePath, JSON.stringify(initialSchema, null, 4));
7120
- this.cachedSchema = initialSchema;
7121
- return initialSchema;
7122
- }
7123
- const fileContent = import_fs5.default.readFileSync(this.schemaFilePath, "utf-8");
7124
- const schema2 = JSON.parse(fileContent);
7125
- this.cachedSchema = schema2;
7126
- return schema2;
7127
- } catch (error) {
7128
- logger.error("Error parsing schema file:", error);
7129
- return null;
7130
- }
7181
+ const toolId = externalTool.toolId;
7182
+ const validToolIds = (executedTools || []).map((t) => t.id);
7183
+ const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
7184
+ if (!isValidTool) {
7185
+ logger.warn(`[${providerName}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
7186
+ return { valid: false };
7131
7187
  }
7132
- /**
7133
- * Gets the cached schema or loads it if not cached
7134
- * @returns Cached schema or freshly loaded schema
7135
- */
7136
- getSchema() {
7137
- if (this.cachedSchema) {
7138
- return this.cachedSchema;
7139
- }
7140
- return this.getDatabaseSchema();
7188
+ const executedTool = executedTools?.find((t) => t.id === toolId);
7189
+ return { valid: true, executedTool };
7190
+ }
7191
+ function validateAndCleanQuery(query, config) {
7192
+ if (!query) {
7193
+ return { query: null, wasModified: false };
7141
7194
  }
7142
- /**
7143
- * Generates database schema documentation for LLM from Snowflake JSON schema
7144
- * @returns Formatted schema documentation string
7145
- */
7146
- generateSchemaDocumentation() {
7147
- const schema2 = this.getSchema();
7148
- if (!schema2) {
7149
- logger.warn("No database schema found.");
7150
- return "No database schema available.";
7151
- }
7152
- const tables = [];
7153
- tables.push(`Database: ${schema2.database}`);
7154
- tables.push(`Schema: ${schema2.schema}`);
7155
- tables.push(`Description: ${schema2.description}`);
7156
- tables.push("");
7157
- tables.push("=".repeat(80));
7158
- tables.push("");
7159
- for (const table of schema2.tables) {
7160
- const tableInfo = [];
7161
- tableInfo.push(`TABLE: ${table.fullName}`);
7162
- tableInfo.push(`Description: ${table.description}`);
7163
- tableInfo.push(`Row Count: ~${table.rowCount.toLocaleString()}`);
7164
- tableInfo.push("");
7165
- tableInfo.push("Columns:");
7166
- for (const column of table.columns) {
7167
- let columnLine = ` - ${column.name}: ${column.type}`;
7168
- if (column.isPrimaryKey) {
7169
- columnLine += " (PRIMARY KEY)";
7170
- }
7171
- if (column.isForeignKey && column.references) {
7172
- columnLine += ` (FK -> ${column.references.table}.${column.references.column})`;
7173
- }
7174
- if (!column.nullable) {
7175
- columnLine += " NOT NULL";
7176
- }
7177
- if (column.description) {
7178
- columnLine += ` - ${column.description}`;
7179
- }
7180
- tableInfo.push(columnLine);
7181
- if (column.sampleValues && column.sampleValues.length > 0) {
7182
- tableInfo.push(` Sample values: [${column.sampleValues.join(", ")}]`);
7183
- }
7184
- if (column.statistics) {
7185
- const stats = column.statistics;
7186
- if (stats.min !== void 0 && stats.max !== void 0) {
7187
- tableInfo.push(` Range: ${stats.min} to ${stats.max}`);
7188
- }
7189
- if (stats.distinct !== void 0) {
7190
- tableInfo.push(` Distinct values: ${stats.distinct.toLocaleString()}`);
7191
- }
7192
- }
7193
- }
7194
- tableInfo.push("");
7195
- tables.push(tableInfo.join("\n"));
7195
+ let wasModified = false;
7196
+ let cleanedQuery = query;
7197
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
7198
+ if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
7199
+ logger.warn(`[${config.providerName}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
7200
+ return { query: null, wasModified: true };
7201
+ }
7202
+ const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
7203
+ if (fixed) {
7204
+ logger.warn(`[${config.providerName}] SQL fixes applied to component query: ${fixes.join("; ")}`);
7205
+ wasModified = true;
7206
+ if (typeof cleanedQuery === "string") {
7207
+ cleanedQuery = fixedQuery;
7208
+ } else if (cleanedQuery?.sql) {
7209
+ cleanedQuery = { ...cleanedQuery, sql: fixedQuery };
7196
7210
  }
7197
- tables.push("=".repeat(80));
7198
- tables.push("");
7199
- tables.push("TABLE RELATIONSHIPS:");
7200
- tables.push("");
7201
- for (const rel of schema2.relationships) {
7202
- tables.push(`${rel.from} -> ${rel.to} (${rel.type}): ${rel.keys.join(" = ")}`);
7211
+ }
7212
+ if (typeof cleanedQuery === "string") {
7213
+ const limitedQuery = ensureQueryLimit(cleanedQuery, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7214
+ if (limitedQuery !== cleanedQuery) wasModified = true;
7215
+ cleanedQuery = limitedQuery;
7216
+ } else if (cleanedQuery?.sql) {
7217
+ const limitedSql = ensureQueryLimit(cleanedQuery.sql, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7218
+ if (limitedSql !== cleanedQuery.sql) wasModified = true;
7219
+ cleanedQuery = { ...cleanedQuery, sql: limitedSql };
7220
+ }
7221
+ return { query: cleanedQuery, wasModified };
7222
+ }
7223
+ function processComponentProps(props, executedTools, config) {
7224
+ let cleanedProps = { ...props };
7225
+ if (cleanedProps.externalTool) {
7226
+ const { valid } = validateExternalTool(
7227
+ cleanedProps.externalTool,
7228
+ executedTools,
7229
+ config.providerName
7230
+ );
7231
+ if (!valid) {
7232
+ cleanedProps.externalTool = null;
7203
7233
  }
7204
- return tables.join("\n");
7205
7234
  }
7206
- /**
7207
- * Clears the cached schema, forcing a reload on next access
7208
- */
7209
- clearCache() {
7210
- this.cachedSchema = null;
7235
+ if (cleanedProps.query) {
7236
+ const { query } = validateAndCleanQuery(cleanedProps.query, config);
7237
+ cleanedProps.query = query;
7211
7238
  }
7212
- /**
7213
- * Sets a custom schema file path
7214
- * @param filePath - Path to the schema file
7215
- */
7216
- setSchemaPath(filePath) {
7217
- this.schemaFilePath = filePath;
7218
- this.clearCache();
7239
+ if (cleanedProps.query && cleanedProps.externalTool) {
7240
+ logger.info(`[${config.providerName}] Both query and externalTool exist, keeping both - frontend will decide`);
7219
7241
  }
7220
- };
7221
- var schema = new Schema();
7242
+ return cleanedProps;
7243
+ }
7222
7244
 
7223
- // src/userResponse/knowledge-base.ts
7224
- var getKnowledgeBase = async ({
7225
- prompt,
7226
- collections,
7227
- topK = 1
7228
- }) => {
7229
- try {
7230
- if (!collections || !collections["knowledge-base"] || !collections["knowledge-base"]["query"]) {
7231
- logger.warn("[KnowledgeBase] knowledge-base.query collection not registered, skipping");
7232
- return "";
7233
- }
7234
- const result = await collections["knowledge-base"]["query"]({
7235
- prompt,
7236
- topK
7237
- });
7238
- if (!result || !result.content) {
7239
- logger.warn("[KnowledgeBase] No knowledge base results returned");
7245
+ // src/userResponse/prompt-extractor.ts
7246
+ function extractPromptText(content) {
7247
+ if (content === null || content === void 0) {
7248
+ return "";
7249
+ }
7250
+ if (typeof content === "string") {
7251
+ return content;
7252
+ }
7253
+ if (Array.isArray(content)) {
7254
+ return content.map((item) => extractContentBlockText(item)).filter((text) => text.length > 0).join("\n\n---\n\n");
7255
+ }
7256
+ if (content && typeof content === "object") {
7257
+ return extractObjectText(content);
7258
+ }
7259
+ return String(content);
7260
+ }
7261
+ function extractContentBlockText(item) {
7262
+ if (typeof item === "string") {
7263
+ return item;
7264
+ }
7265
+ if (item && typeof item === "object") {
7266
+ const obj = item;
7267
+ if (typeof obj.text === "string") {
7268
+ return obj.text;
7269
+ }
7270
+ if (typeof obj.content === "string") {
7271
+ return obj.content;
7272
+ }
7273
+ return JSON.stringify(item, null, 2);
7274
+ }
7275
+ return String(item);
7276
+ }
7277
+ function extractObjectText(obj) {
7278
+ if (typeof obj.text === "string") {
7279
+ return obj.text;
7280
+ }
7281
+ if (typeof obj.content === "string") {
7282
+ return obj.content;
7283
+ }
7284
+ return JSON.stringify(obj, null, 2);
7285
+ }
7286
+
7287
+ // src/userResponse/knowledge-base.ts
7288
+ var getKnowledgeBase = async ({
7289
+ prompt,
7290
+ collections,
7291
+ topK = 1
7292
+ }) => {
7293
+ try {
7294
+ if (!collections || !collections["knowledge-base"] || !collections["knowledge-base"]["query"]) {
7295
+ logger.warn("[KnowledgeBase] knowledge-base.query collection not registered, skipping");
7296
+ return "";
7297
+ }
7298
+ const result = await collections["knowledge-base"]["query"]({
7299
+ prompt,
7300
+ topK
7301
+ });
7302
+ if (!result || !result.content) {
7303
+ logger.warn("[KnowledgeBase] No knowledge base results returned");
7240
7304
  return "";
7241
7305
  }
7242
7306
  logger.info(`[KnowledgeBase] Retrieved knowledge base context (${result.content.length} chars)`);
@@ -7344,116 +7408,601 @@ var KB = {
7344
7408
  };
7345
7409
  var knowledge_base_default = KB;
7346
7410
 
7347
- // src/userResponse/prompt-extractor.ts
7348
- function extractPromptText(content) {
7349
- if (content === null || content === void 0) {
7350
- return "";
7351
- }
7352
- if (typeof content === "string") {
7353
- return content;
7411
+ // src/userResponse/agents/agent-component-generator.ts
7412
+ async function generateAgentComponents(params) {
7413
+ const startTime = Date.now();
7414
+ const {
7415
+ analysisContent,
7416
+ components,
7417
+ userPrompt,
7418
+ executedTools,
7419
+ collections,
7420
+ apiKey,
7421
+ componentStreamCallback,
7422
+ userId
7423
+ } = params;
7424
+ logger.info(`[AgentComponentGen] Starting | ${executedTools.length} executed tools | ${components.length} available components`);
7425
+ try {
7426
+ const availableComponentsText = formatAvailableComponents(components);
7427
+ const executedToolsText = formatExecutedTools(executedTools);
7428
+ const sourceTypes = [...new Set(executedTools.map((t) => t.sourceType).filter(Boolean))];
7429
+ let databaseRules = "";
7430
+ if (sourceTypes.length > 0) {
7431
+ const rulesArr = await Promise.all(
7432
+ sourceTypes.map((st) => promptLoader.loadDatabaseRulesForType(st))
7433
+ );
7434
+ databaseRules = rulesArr.join("\n\n");
7435
+ } else {
7436
+ databaseRules = await promptLoader.loadDatabaseRules();
7437
+ }
7438
+ let knowledgeBaseContext = "No additional knowledge base context available.";
7439
+ if (collections) {
7440
+ const kbResult = await knowledge_base_default.getAllKnowledgeBase({
7441
+ prompt: userPrompt || analysisContent,
7442
+ collections,
7443
+ userId,
7444
+ topK: KNOWLEDGE_BASE_TOP_K
7445
+ });
7446
+ knowledgeBaseContext = kbResult.combinedContext || knowledgeBaseContext;
7447
+ }
7448
+ const prompts = await promptLoader.loadPrompts("match-text-components", {
7449
+ USER_PROMPT: userPrompt || "",
7450
+ ANALYSIS_CONTENT: analysisContent,
7451
+ AVAILABLE_COMPONENTS: availableComponentsText,
7452
+ SCHEMA_DOC: "Use column names from executed query results in EXECUTED_TOOLS.",
7453
+ DATABASE_RULES: databaseRules,
7454
+ DEFERRED_TOOLS: "No deferred external tools for this request.",
7455
+ EXECUTED_TOOLS: executedToolsText,
7456
+ KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext,
7457
+ CURRENT_DATETIME: getCurrentDateTimeForPrompt()
7458
+ });
7459
+ logger.logLLMPrompt("agentComponentGen", "system", extractPromptText(prompts.system));
7460
+ logger.logLLMPrompt("agentComponentGen", "user", `Text Analysis:
7461
+ ${analysisContent}
7462
+
7463
+ Executed Tools:
7464
+ ${executedToolsText}`);
7465
+ let fullResponseText = "";
7466
+ let answerComponentStreamed = false;
7467
+ const partialCallback = componentStreamCallback ? (chunk) => {
7468
+ fullResponseText += chunk;
7469
+ if (!answerComponentStreamed && componentStreamCallback) {
7470
+ const streamed = tryStreamAnswerComponent(
7471
+ fullResponseText,
7472
+ components,
7473
+ executedTools,
7474
+ collections,
7475
+ apiKey,
7476
+ componentStreamCallback
7477
+ );
7478
+ if (streamed) answerComponentStreamed = true;
7479
+ }
7480
+ } : void 0;
7481
+ const result = await LLM.stream(
7482
+ { sys: prompts.system, user: prompts.user },
7483
+ {
7484
+ model: "anthropic/claude-haiku-4-5-20251001",
7485
+ maxTokens: MAX_TOKENS_COMPONENT_MATCHING,
7486
+ temperature: 0,
7487
+ apiKey,
7488
+ partial: partialCallback
7489
+ },
7490
+ true
7491
+ // Parse as JSON
7492
+ );
7493
+ const matchedComponents = result.matchedComponents || [];
7494
+ const layoutTitle = result.layoutTitle || "Dashboard";
7495
+ const layoutDescription = result.layoutDescription || "Multi-component dashboard";
7496
+ if (result.hasAnswerComponent && result.answerComponent?.componentId) {
7497
+ const answer = result.answerComponent;
7498
+ const answerSql = answer.props?.externalTool?.parameters?.sql || "";
7499
+ const answerTitle = answer.props?.title || "";
7500
+ const answerType = answer.componentName || "";
7501
+ const isDuplicate = matchedComponents.some((mc) => {
7502
+ const mcSql = mc.props?.externalTool?.parameters?.sql || "";
7503
+ const mcTitle = mc.props?.title || "";
7504
+ const mcType = mc.componentName || "";
7505
+ return mcType === answerType && (mcTitle === answerTitle || mcSql === answerSql);
7506
+ });
7507
+ if (!isDuplicate) {
7508
+ matchedComponents.unshift(answer);
7509
+ }
7510
+ }
7511
+ logger.info(`[AgentComponentGen] LLM returned ${matchedComponents.length} components`);
7512
+ matchedComponents.forEach((comp, idx) => {
7513
+ logger.info(`[AgentComponentGen] ${idx + 1}. ${comp.componentType} (${comp.componentName})`);
7514
+ });
7515
+ logger.file("\n=============================\nFull LLM response:", JSON.stringify(result, null, 2));
7516
+ const rawActions = result.actions || [];
7517
+ const actions = convertQuestionsToActions(rawActions);
7518
+ const finalComponents = matchedComponents.map((mc) => {
7519
+ const originalComponent = components.find((c) => c.name === mc.componentName);
7520
+ if (!originalComponent) {
7521
+ logger.warn(`[AgentComponentGen] Component "${mc.componentName}" not found in available components`);
7522
+ return null;
7523
+ }
7524
+ const cleanedProps = processComponentProps(
7525
+ mc.props,
7526
+ executedTools,
7527
+ { providerName: "AgentComponentGen", defaultLimit: 10 }
7528
+ );
7529
+ return {
7530
+ ...originalComponent,
7531
+ props: { ...originalComponent.props, ...cleanedProps }
7532
+ };
7533
+ }).filter(Boolean);
7534
+ const validatedComponents = await validateExternalToolQueries(
7535
+ finalComponents,
7536
+ collections,
7537
+ executedTools,
7538
+ apiKey
7539
+ );
7540
+ const elapsed = Date.now() - startTime;
7541
+ logger.info(`[AgentComponentGen] Complete | ${validatedComponents.length}/${finalComponents.length} validated | ${elapsed}ms`);
7542
+ return {
7543
+ components: validatedComponents,
7544
+ layoutTitle,
7545
+ layoutDescription,
7546
+ actions
7547
+ };
7548
+ } catch (error) {
7549
+ const errorMsg = error instanceof Error ? error.message : String(error);
7550
+ logger.error(`[AgentComponentGen] Error: ${errorMsg}`);
7551
+ return {
7552
+ components: [],
7553
+ layoutTitle: "Dashboard",
7554
+ layoutDescription: "",
7555
+ actions: []
7556
+ };
7354
7557
  }
7355
- if (Array.isArray(content)) {
7356
- return content.map((item) => extractContentBlockText(item)).filter((text) => text.length > 0).join("\n\n---\n\n");
7558
+ }
7559
+ function formatAvailableComponents(components) {
7560
+ if (!components || components.length === 0) return "No components available";
7561
+ return components.map((comp, idx) => {
7562
+ const keywords = comp.keywords ? comp.keywords.join(", ") : "";
7563
+ const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
7564
+ return `${idx + 1}. ID: ${comp.id}
7565
+ Name: ${comp.name}
7566
+ Type: ${comp.type}
7567
+ Description: ${comp.description || "No description"}
7568
+ Keywords: ${keywords}
7569
+ Props Structure: ${propsPreview}`;
7570
+ }).join("\n\n");
7571
+ }
7572
+ function formatExecutedTools(executedTools) {
7573
+ if (!executedTools || executedTools.length === 0) {
7574
+ return "No external tools were executed for data fetching.";
7575
+ }
7576
+ return "The following external tools were executed to fetch data.\n" + executedTools.map((tool, idx) => {
7577
+ let outputSchemaText = "Not available";
7578
+ let fieldNamesList = "";
7579
+ const recordCount = tool.result?._totalRecords ?? "unknown";
7580
+ let metadataText = "";
7581
+ if (tool.result?._metadata && Object.keys(tool.result._metadata).length > 0) {
7582
+ const metadataEntries = Object.entries(tool.result._metadata).map(([key, value]) => `${key}: ${value}`).join(", ");
7583
+ metadataText = `
7584
+ \u{1F4CB} METADATA: ${metadataEntries}`;
7585
+ }
7586
+ if (tool.outputSchema) {
7587
+ const fields = tool.outputSchema.fields || [];
7588
+ const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
7589
+ const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
7590
+ fieldNamesList = `
7591
+ \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
7592
+ \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
7593
+ const fieldsText = fields.map(
7594
+ (f) => ` "${f.name}" (${f.type}): ${f.description}`
7595
+ ).join("\n");
7596
+ outputSchemaText = `${tool.outputSchema.description}
7597
+ Fields:
7598
+ ${fieldsText}`;
7599
+ }
7600
+ return `${idx + 1}. **${tool.name}**
7601
+ toolId: "${tool.id}"
7602
+ toolName: "${tool.name}"
7603
+ parameters: ${JSON.stringify(tool.params || {})}
7604
+ recordCount: ${recordCount} rows returned${metadataText}
7605
+ outputSchema: ${outputSchemaText}${fieldNamesList}`;
7606
+ }).join("\n\n");
7607
+ }
7608
+ function tryStreamAnswerComponent(text, components, executedTools, collections, apiKey, callback) {
7609
+ const hasMatch = text.match(/"hasAnswerComponent"\s*:\s*(true|false)/);
7610
+ if (!hasMatch || hasMatch[1] !== "true") return false;
7611
+ const startMatch = text.match(/"answerComponent"\s*:\s*\{/);
7612
+ if (!startMatch) return false;
7613
+ const startPos = startMatch.index + startMatch[0].length - 1;
7614
+ let depth = 0;
7615
+ let inString = false;
7616
+ let escapeNext = false;
7617
+ let endPos = -1;
7618
+ for (let i = startPos; i < text.length; i++) {
7619
+ const char = text[i];
7620
+ if (escapeNext) {
7621
+ escapeNext = false;
7622
+ continue;
7623
+ }
7624
+ if (char === "\\") {
7625
+ escapeNext = true;
7626
+ continue;
7627
+ }
7628
+ if (char === '"') {
7629
+ inString = !inString;
7630
+ continue;
7631
+ }
7632
+ if (!inString) {
7633
+ if (char === "{") depth++;
7634
+ else if (char === "}") {
7635
+ depth--;
7636
+ if (depth === 0) {
7637
+ endPos = i + 1;
7638
+ break;
7639
+ }
7640
+ }
7641
+ }
7357
7642
  }
7358
- if (content && typeof content === "object") {
7359
- return extractObjectText(content);
7643
+ if (endPos <= startPos) return false;
7644
+ try {
7645
+ const answerData = JSON.parse(text.substring(startPos, endPos));
7646
+ if (!answerData?.componentId) return false;
7647
+ const original = components.find((c) => c.id === answerData.componentId);
7648
+ if (!original) return false;
7649
+ const answerComponent = {
7650
+ ...original,
7651
+ props: { ...original.props, ...answerData.props }
7652
+ };
7653
+ const answerProps = answerComponent.props;
7654
+ const sql = answerProps?.externalTool?.parameters?.sql;
7655
+ if (sql && collections?.["external-tools"]?.["execute"]) {
7656
+ const toolId = answerProps.externalTool.toolId;
7657
+ const toolName = answerProps.externalTool.toolName;
7658
+ (async () => {
7659
+ try {
7660
+ const result = await collections["external-tools"]["execute"]({
7661
+ toolId,
7662
+ toolName,
7663
+ sql,
7664
+ data: {}
7665
+ });
7666
+ if (result?.success === false || result?.error) {
7667
+ logger.warn(`[AgentComponentGen] Answer component query failed: ${result?.error}`);
7668
+ return;
7669
+ }
7670
+ queryCache.set(sql, result?.data ?? result);
7671
+ callback(answerComponent);
7672
+ } catch (err) {
7673
+ const msg = err instanceof Error ? err.message : String(err);
7674
+ logger.warn(`[AgentComponentGen] Answer component validation failed: ${msg}`);
7675
+ }
7676
+ })();
7677
+ } else {
7678
+ callback(answerComponent);
7679
+ }
7680
+ return true;
7681
+ } catch {
7682
+ return false;
7360
7683
  }
7361
- return String(content);
7362
7684
  }
7363
- function extractContentBlockText(item) {
7364
- if (typeof item === "string") {
7365
- return item;
7685
+ async function validateExternalToolQueries(components, collections, executedTools, apiKey) {
7686
+ if (!collections?.["external-tools"]?.["execute"]) {
7687
+ logger.warn(`[AgentComponentGen] external-tools.execute not available, skipping validation`);
7688
+ return components;
7366
7689
  }
7367
- if (item && typeof item === "object") {
7368
- const obj = item;
7369
- if (typeof obj.text === "string") {
7370
- return obj.text;
7690
+ const validated = [];
7691
+ const withSql = [];
7692
+ const withoutSql = [];
7693
+ for (const comp of components) {
7694
+ const sql = comp.props?.externalTool?.parameters?.sql;
7695
+ if (sql) {
7696
+ withSql.push(comp);
7697
+ } else {
7698
+ withoutSql.push(comp);
7371
7699
  }
7372
- if (typeof obj.content === "string") {
7373
- return obj.content;
7700
+ }
7701
+ validated.push(...withoutSql);
7702
+ if (withSql.length === 0) return validated;
7703
+ const sqlGroups = /* @__PURE__ */ new Map();
7704
+ for (const comp of withSql) {
7705
+ const sql = comp.props.externalTool.parameters.sql;
7706
+ const normalized = sql.replace(/\s+/g, " ").trim();
7707
+ if (!sqlGroups.has(normalized)) {
7708
+ sqlGroups.set(normalized, []);
7374
7709
  }
7375
- return JSON.stringify(item, null, 2);
7710
+ sqlGroups.get(normalized).push(comp);
7376
7711
  }
7377
- return String(item);
7378
- }
7379
- function extractObjectText(obj) {
7380
- if (typeof obj.text === "string") {
7381
- return obj.text;
7712
+ const uniqueQueries = Array.from(sqlGroups.entries());
7713
+ logger.info(`[AgentComponentGen] Validating ${uniqueQueries.length} unique queries (${withSql.length} components)...`);
7714
+ const results = await Promise.allSettled(
7715
+ uniqueQueries.map(([_, comps]) => validateSingleExternalToolQuery(comps[0], collections, executedTools, apiKey))
7716
+ );
7717
+ for (let i = 0; i < results.length; i++) {
7718
+ const result = results[i];
7719
+ const [_, groupComps] = uniqueQueries[i];
7720
+ if (result.status === "fulfilled" && result.value) {
7721
+ validated.push(result.value);
7722
+ for (let j = 1; j < groupComps.length; j++) {
7723
+ const fixedSql = result.value.props?.externalTool?.parameters?.sql;
7724
+ if (fixedSql) {
7725
+ const siblingProps = groupComps[j].props;
7726
+ validated.push({
7727
+ ...groupComps[j],
7728
+ props: {
7729
+ ...groupComps[j].props,
7730
+ externalTool: {
7731
+ ...siblingProps.externalTool,
7732
+ parameters: { ...siblingProps.externalTool.parameters, sql: fixedSql }
7733
+ }
7734
+ }
7735
+ });
7736
+ } else {
7737
+ validated.push(groupComps[j]);
7738
+ }
7739
+ }
7740
+ } else {
7741
+ const reason = result.status === "rejected" ? result.reason : "validation failed";
7742
+ for (const comp of groupComps) {
7743
+ logger.warn(`[AgentComponentGen] Excluded ${comp.name}: ${reason}`);
7744
+ }
7745
+ }
7382
7746
  }
7383
- if (typeof obj.content === "string") {
7384
- return obj.content;
7747
+ logger.info(`[AgentComponentGen] Validation complete: ${validated.length}/${components.length} components passed`);
7748
+ return validated;
7749
+ }
7750
+ async function validateSingleExternalToolQuery(component, collections, executedTools, apiKey) {
7751
+ const compProps = component.props;
7752
+ const toolId = compProps?.externalTool?.toolId;
7753
+ const toolName = compProps?.externalTool?.toolName;
7754
+ let currentSql = compProps?.externalTool?.parameters?.sql;
7755
+ if (!toolId || !currentSql) return component;
7756
+ currentSql = ensureQueryLimit(currentSql, 10, MAX_COMPONENT_QUERY_LIMIT);
7757
+ let attempts = 0;
7758
+ while (attempts < MAX_QUERY_VALIDATION_RETRIES) {
7759
+ attempts++;
7760
+ try {
7761
+ logger.info(`[AgentComponentGen] Validating ${component.name} (attempt ${attempts}/${MAX_QUERY_VALIDATION_RETRIES})`);
7762
+ const result = await collections["external-tools"]["execute"]({
7763
+ toolId,
7764
+ toolName,
7765
+ sql: currentSql,
7766
+ data: {}
7767
+ });
7768
+ if (result?.success === false || result?.error) {
7769
+ const errorMsg = result?.error || "Unknown error";
7770
+ throw new Error(typeof errorMsg === "string" ? errorMsg : JSON.stringify(errorMsg));
7771
+ }
7772
+ const rawToolData = result?.data ?? result;
7773
+ queryCache.set(currentSql, rawToolData);
7774
+ logger.info(`[AgentComponentGen] \u2713 ${component.name} validated (attempt ${attempts})`);
7775
+ return {
7776
+ ...component,
7777
+ props: {
7778
+ ...component.props,
7779
+ externalTool: {
7780
+ ...compProps.externalTool,
7781
+ parameters: {
7782
+ ...compProps.externalTool.parameters,
7783
+ sql: currentSql
7784
+ }
7785
+ }
7786
+ }
7787
+ };
7788
+ } catch (error) {
7789
+ const errorMsg = error instanceof Error ? error.message : String(error);
7790
+ logger.warn(`[AgentComponentGen] \u2717 ${component.name} failed (attempt ${attempts}): ${errorMsg}`);
7791
+ if (attempts >= MAX_QUERY_VALIDATION_RETRIES) {
7792
+ logger.error(`[AgentComponentGen] Max retries reached for ${component.name}, excluding`);
7793
+ return null;
7794
+ }
7795
+ try {
7796
+ const fixedSql = await requestExternalToolQueryFix(
7797
+ currentSql,
7798
+ errorMsg,
7799
+ component,
7800
+ executedTools,
7801
+ toolId,
7802
+ apiKey
7803
+ );
7804
+ if (fixedSql && fixedSql !== currentSql) {
7805
+ currentSql = ensureQueryLimit(fixedSql, 10, MAX_COMPONENT_QUERY_LIMIT);
7806
+ logger.info(`[AgentComponentGen] LLM provided fix for ${component.name}, retrying...`);
7807
+ } else {
7808
+ logger.warn(`[AgentComponentGen] LLM returned same or empty query, stopping retries`);
7809
+ return null;
7810
+ }
7811
+ } catch (fixError) {
7812
+ const fixMsg = fixError instanceof Error ? fixError.message : String(fixError);
7813
+ logger.error(`[AgentComponentGen] Failed to get LLM fix: ${fixMsg}`);
7814
+ return null;
7815
+ }
7816
+ }
7385
7817
  }
7386
- return JSON.stringify(obj, null, 2);
7818
+ return null;
7387
7819
  }
7820
+ async function requestExternalToolQueryFix(failedSql, errorMessage, component, executedTools, toolId, apiKey) {
7821
+ const executedTool = executedTools.find((t) => t.id === toolId);
7822
+ const sourceSchema = executedTool?.sourceSchema || "Schema not available";
7823
+ const sourceType = executedTool?.sourceType || "postgresql";
7824
+ const databaseRules = await promptLoader.loadDatabaseRulesForType(sourceType);
7825
+ const prompt = `You are a SQL expert. Fix the following SQL query that failed execution.
7388
7826
 
7389
- // src/userResponse/utils/component-props-processor.ts
7390
- function validateExternalTool(externalTool, executedTools, providerName) {
7391
- if (!externalTool) {
7392
- return { valid: true };
7393
- }
7394
- const toolId = externalTool.toolId;
7395
- const validToolIds = (executedTools || []).map((t) => t.id);
7396
- const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
7397
- if (!isValidTool) {
7398
- logger.warn(`[${providerName}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
7399
- return { valid: false };
7400
- }
7401
- const executedTool = executedTools?.find((t) => t.id === toolId);
7402
- return { valid: true, executedTool };
7827
+ ## Database Schema
7828
+ ${sourceSchema}
7829
+
7830
+ ## Database-Specific SQL Rules
7831
+ ${databaseRules}
7832
+
7833
+ ## Component Context
7834
+ - Component Name: ${component.name}
7835
+ - Component Type: ${component.type}
7836
+ - Title: ${component.props?.title || "N/A"}
7837
+
7838
+ ## Failed Query
7839
+ \`\`\`sql
7840
+ ${failedSql}
7841
+ \`\`\`
7842
+
7843
+ ## Error Message
7844
+ ${errorMessage}
7845
+
7846
+ ## Instructions
7847
+ 1. Analyze the error message and identify what caused the query to fail
7848
+ 2. Fix the query to resolve the error while preserving the original intent
7849
+ 3. Ensure the fixed query follows the database-specific SQL rules above
7850
+ 4. Return ONLY the fixed SQL query, no explanations or markdown
7851
+
7852
+ Fixed SQL query:`;
7853
+ const response = await LLM.text(
7854
+ {
7855
+ sys: "You are a SQL expert. Return only the fixed SQL query with no additional text, explanations, or markdown formatting.",
7856
+ user: prompt
7857
+ },
7858
+ {
7859
+ model: "anthropic/claude-haiku-4-5-20251001",
7860
+ maxTokens: 2048,
7861
+ temperature: 0,
7862
+ apiKey
7863
+ }
7864
+ );
7865
+ let fixedQuery = response.trim();
7866
+ fixedQuery = fixedQuery.replace(/^```sql\s*/i, "").replace(/\s*```$/i, "");
7867
+ fixedQuery = fixedQuery.replace(/^```\s*/i, "").replace(/\s*```$/i, "");
7868
+ const { query: validatedQuery } = validateAndFixSqlQuery(fixedQuery);
7869
+ return validatedQuery;
7403
7870
  }
7404
- function validateAndCleanQuery(query, config) {
7405
- if (!query) {
7406
- return { query: null, wasModified: false };
7407
- }
7408
- let wasModified = false;
7409
- let cleanedQuery = query;
7410
- const queryStr = typeof query === "string" ? query : query?.sql || "";
7411
- if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
7412
- logger.warn(`[${config.providerName}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
7413
- return { query: null, wasModified: true };
7871
+
7872
+ // src/userResponse/anthropic.ts
7873
+ var import_dotenv = __toESM(require("dotenv"));
7874
+
7875
+ // src/userResponse/schema.ts
7876
+ var import_path4 = __toESM(require("path"));
7877
+ var import_fs5 = __toESM(require("fs"));
7878
+ var Schema = class {
7879
+ constructor(schemaFilePath) {
7880
+ this.cachedSchema = null;
7881
+ this.schemaFilePath = schemaFilePath || import_path4.default.join(process.cwd(), "../analysis/data/schema.json");
7414
7882
  }
7415
- const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
7416
- if (fixed) {
7417
- logger.warn(`[${config.providerName}] SQL fixes applied to component query: ${fixes.join("; ")}`);
7418
- wasModified = true;
7419
- if (typeof cleanedQuery === "string") {
7420
- cleanedQuery = fixedQuery;
7421
- } else if (cleanedQuery?.sql) {
7422
- cleanedQuery = { ...cleanedQuery, sql: fixedQuery };
7883
+ /**
7884
+ * Gets the database schema from the schema file
7885
+ * @returns Parsed schema object or null if error occurs
7886
+ */
7887
+ getDatabaseSchema() {
7888
+ try {
7889
+ const dir = import_path4.default.dirname(this.schemaFilePath);
7890
+ if (!import_fs5.default.existsSync(dir)) {
7891
+ logger.info(`Creating directory structure: ${dir}`);
7892
+ import_fs5.default.mkdirSync(dir, { recursive: true });
7893
+ }
7894
+ if (!import_fs5.default.existsSync(this.schemaFilePath)) {
7895
+ logger.info(`Schema file does not exist at ${this.schemaFilePath}, creating with empty schema`);
7896
+ const initialSchema = {
7897
+ database: "",
7898
+ schema: "",
7899
+ description: "",
7900
+ tables: [],
7901
+ relationships: []
7902
+ };
7903
+ import_fs5.default.writeFileSync(this.schemaFilePath, JSON.stringify(initialSchema, null, 4));
7904
+ this.cachedSchema = initialSchema;
7905
+ return initialSchema;
7906
+ }
7907
+ const fileContent = import_fs5.default.readFileSync(this.schemaFilePath, "utf-8");
7908
+ const schema2 = JSON.parse(fileContent);
7909
+ this.cachedSchema = schema2;
7910
+ return schema2;
7911
+ } catch (error) {
7912
+ logger.error("Error parsing schema file:", error);
7913
+ return null;
7423
7914
  }
7424
7915
  }
7425
- if (typeof cleanedQuery === "string") {
7426
- const limitedQuery = ensureQueryLimit(cleanedQuery, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7427
- if (limitedQuery !== cleanedQuery) wasModified = true;
7428
- cleanedQuery = limitedQuery;
7429
- } else if (cleanedQuery?.sql) {
7430
- const limitedSql = ensureQueryLimit(cleanedQuery.sql, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7431
- if (limitedSql !== cleanedQuery.sql) wasModified = true;
7432
- cleanedQuery = { ...cleanedQuery, sql: limitedSql };
7916
+ /**
7917
+ * Gets the cached schema or loads it if not cached
7918
+ * @returns Cached schema or freshly loaded schema
7919
+ */
7920
+ getSchema() {
7921
+ if (this.cachedSchema) {
7922
+ return this.cachedSchema;
7923
+ }
7924
+ return this.getDatabaseSchema();
7433
7925
  }
7434
- return { query: cleanedQuery, wasModified };
7435
- }
7436
- function processComponentProps(props, executedTools, config) {
7437
- let cleanedProps = { ...props };
7438
- if (cleanedProps.externalTool) {
7439
- const { valid } = validateExternalTool(
7440
- cleanedProps.externalTool,
7441
- executedTools,
7442
- config.providerName
7443
- );
7444
- if (!valid) {
7445
- cleanedProps.externalTool = null;
7926
+ /**
7927
+ * Generates database schema documentation for LLM from Snowflake JSON schema
7928
+ * @returns Formatted schema documentation string
7929
+ */
7930
+ generateSchemaDocumentation() {
7931
+ const schema2 = this.getSchema();
7932
+ if (!schema2) {
7933
+ logger.warn("No database schema found.");
7934
+ return "No database schema available.";
7935
+ }
7936
+ const tables = [];
7937
+ tables.push(`Database: ${schema2.database}`);
7938
+ tables.push(`Schema: ${schema2.schema}`);
7939
+ tables.push(`Description: ${schema2.description}`);
7940
+ tables.push("");
7941
+ tables.push("=".repeat(80));
7942
+ tables.push("");
7943
+ for (const table of schema2.tables) {
7944
+ const tableInfo = [];
7945
+ tableInfo.push(`TABLE: ${table.fullName}`);
7946
+ tableInfo.push(`Description: ${table.description}`);
7947
+ tableInfo.push(`Row Count: ~${table.rowCount.toLocaleString()}`);
7948
+ tableInfo.push("");
7949
+ tableInfo.push("Columns:");
7950
+ for (const column of table.columns) {
7951
+ let columnLine = ` - ${column.name}: ${column.type}`;
7952
+ if (column.isPrimaryKey) {
7953
+ columnLine += " (PRIMARY KEY)";
7954
+ }
7955
+ if (column.isForeignKey && column.references) {
7956
+ columnLine += ` (FK -> ${column.references.table}.${column.references.column})`;
7957
+ }
7958
+ if (!column.nullable) {
7959
+ columnLine += " NOT NULL";
7960
+ }
7961
+ if (column.description) {
7962
+ columnLine += ` - ${column.description}`;
7963
+ }
7964
+ tableInfo.push(columnLine);
7965
+ if (column.sampleValues && column.sampleValues.length > 0) {
7966
+ tableInfo.push(` Sample values: [${column.sampleValues.join(", ")}]`);
7967
+ }
7968
+ if (column.statistics) {
7969
+ const stats = column.statistics;
7970
+ if (stats.min !== void 0 && stats.max !== void 0) {
7971
+ tableInfo.push(` Range: ${stats.min} to ${stats.max}`);
7972
+ }
7973
+ if (stats.distinct !== void 0) {
7974
+ tableInfo.push(` Distinct values: ${stats.distinct.toLocaleString()}`);
7975
+ }
7976
+ }
7977
+ }
7978
+ tableInfo.push("");
7979
+ tables.push(tableInfo.join("\n"));
7446
7980
  }
7981
+ tables.push("=".repeat(80));
7982
+ tables.push("");
7983
+ tables.push("TABLE RELATIONSHIPS:");
7984
+ tables.push("");
7985
+ for (const rel of schema2.relationships) {
7986
+ tables.push(`${rel.from} -> ${rel.to} (${rel.type}): ${rel.keys.join(" = ")}`);
7987
+ }
7988
+ return tables.join("\n");
7447
7989
  }
7448
- if (cleanedProps.query) {
7449
- const { query } = validateAndCleanQuery(cleanedProps.query, config);
7450
- cleanedProps.query = query;
7990
+ /**
7991
+ * Clears the cached schema, forcing a reload on next access
7992
+ */
7993
+ clearCache() {
7994
+ this.cachedSchema = null;
7451
7995
  }
7452
- if (cleanedProps.query && cleanedProps.externalTool) {
7453
- logger.info(`[${config.providerName}] Both query and externalTool exist, keeping both - frontend will decide`);
7996
+ /**
7997
+ * Sets a custom schema file path
7998
+ * @param filePath - Path to the schema file
7999
+ */
8000
+ setSchemaPath(filePath) {
8001
+ this.schemaFilePath = filePath;
8002
+ this.clearCache();
7454
8003
  }
7455
- return cleanedProps;
7456
- }
8004
+ };
8005
+ var schema = new Schema();
7457
8006
 
7458
8007
  // src/userResponse/services/query-execution-service.ts
7459
8008
  var QueryExecutionService = class {
@@ -9386,7 +9935,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9386
9935
  let layoutDescription = "Multi-component dashboard";
9387
9936
  let actions = [];
9388
9937
  if (!hasExecutedTools) {
9389
- logger.info(`[AgentFlow] No tools executed \u2014 general question, skipping component generation`);
9938
+ logger.info(`[AgentFlow] No tools executed \u2014 general question, wrapping in DynamicMarkdownBlock`);
9390
9939
  const nextQuestions = await llmInstance.generateNextQuestions(
9391
9940
  prompt,
9392
9941
  null,
@@ -9396,6 +9945,18 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9396
9945
  textResponse
9397
9946
  );
9398
9947
  actions = convertQuestionsToActions(nextQuestions);
9948
+ const markdownContent = textResponse.replace(/<DashboardComponents>[\s\S]*?<\/DashboardComponents>/g, "").trim();
9949
+ matchedComponents = [{
9950
+ id: "dynamic-markdown-block",
9951
+ name: "DynamicMarkdownBlock",
9952
+ type: "MarkdownBlock",
9953
+ description: "Text response rendered as markdown",
9954
+ props: {
9955
+ content: markdownContent
9956
+ }
9957
+ }];
9958
+ layoutTitle = "";
9959
+ layoutDescription = "";
9399
9960
  } else if (components && components.length > 0) {
9400
9961
  logger.info(`[AgentFlow] ${agentResponse.executedTools.length} tools executed \u2014 generating components`);
9401
9962
  if (streamBuffer.hasCallback()) {
@@ -9410,18 +9971,16 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9410
9971
  /<DataTable>[\s\S]*?<\/DataTable>/g,
9411
9972
  "<DataTable>[Data preview removed - for table components, REUSE the exact SQL query shown above (the one that returned these results). Do NOT write a new query or embed data in props.]</DataTable>"
9412
9973
  );
9413
- const matchResult = await llmInstance.matchComponentsFromAnalysis(
9414
- sanitizedTextResponse,
9974
+ const matchResult = await generateAgentComponents({
9975
+ analysisContent: sanitizedTextResponse,
9415
9976
  components,
9416
- prompt,
9977
+ userPrompt: prompt,
9978
+ executedTools: agentResponse.executedTools,
9979
+ collections,
9417
9980
  apiKey,
9418
9981
  componentStreamCallback,
9419
- [],
9420
- // deferredTools — MainAgent handles tool execution
9421
- agentResponse.executedTools,
9422
- collections,
9423
9982
  userId
9424
- );
9983
+ });
9425
9984
  matchedComponents = matchResult.components;
9426
9985
  layoutTitle = matchResult.layoutTitle;
9427
9986
  layoutDescription = matchResult.layoutDescription;
@@ -9429,9 +9988,11 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9429
9988
  }
9430
9989
  const securedComponents = matchedComponents.map((comp) => {
9431
9990
  const props = { ...comp.props };
9432
- if (props.externalTool?.parameters?.sql) {
9433
- const { sql, ...restParams } = props.externalTool.parameters;
9434
- const queryId = queryCache.storeQuery(sql);
9991
+ const sqlValue = props.externalTool?.parameters?.sql || props.externalTool?.parameters?.query;
9992
+ if (sqlValue) {
9993
+ const { sql, query, ...restParams } = props.externalTool.parameters;
9994
+ const cachedData = queryCache.get(sqlValue);
9995
+ const queryId = queryCache.storeQuery(sqlValue, cachedData);
9435
9996
  props.externalTool = {
9436
9997
  ...props.externalTool,
9437
9998
  parameters: { queryId, ...restParams }
@@ -9550,18 +10111,10 @@ async function saveConversation(params) {
9550
10111
  }
9551
10112
  try {
9552
10113
  logger.info(`[CONVERSATION_SAVER] Saving conversation for userId: ${userId}, uiBlockId: ${uiBlockId}, threadId: ${threadId}`);
9553
- const userIdNumber = Number(userId);
9554
- if (isNaN(userIdNumber)) {
9555
- logger.warn(`[CONVERSATION_SAVER] Invalid userId: ${userId} (not a valid number)`);
9556
- return {
9557
- success: false,
9558
- error: `Invalid userId: ${userId} (not a valid number)`
9559
- };
9560
- }
9561
10114
  const dbUIBlock = transformUIBlockForDB(uiblock, userPrompt, uiBlockId);
9562
10115
  logger.debug(`[CONVERSATION_SAVER] Transformed UIBlock for DB: ${JSON.stringify(dbUIBlock)}`);
9563
10116
  const saveResult = await collections["user-conversations"]["create"]({
9564
- userId: userIdNumber,
10117
+ userId,
9565
10118
  userPrompt,
9566
10119
  uiblock: dbUIBlock,
9567
10120
  threadId
@@ -9582,7 +10135,7 @@ async function saveConversation(params) {
9582
10135
  userPrompt,
9583
10136
  uiBlock: dbUIBlock,
9584
10137
  // Use the transformed UIBlock
9585
- userId: userIdNumber
10138
+ userId
9586
10139
  });
9587
10140
  if (embedResult?.success) {
9588
10141
  logger.info("[CONVERSATION_SAVER] Successfully created embedding");