@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.mjs CHANGED
@@ -4029,6 +4029,38 @@ var PromptLoader = class {
4029
4029
  logger.warn(`Using default database rules for '${this.databaseType}' (file not found at ${rulesPath})`);
4030
4030
  return defaultRules;
4031
4031
  }
4032
+ /**
4033
+ * Load database-specific SQL rules for a given source type.
4034
+ * Used by the agent architecture where multiple source types can coexist.
4035
+ * @param sourceType - Source type from tool ID (e.g., 'postgres', 'mssql', 'mysql')
4036
+ * @returns Database rules as a string
4037
+ */
4038
+ async loadDatabaseRulesForType(sourceType) {
4039
+ const typeMap = {
4040
+ "postgres": "postgresql",
4041
+ "postgresql": "postgresql",
4042
+ "mssql": "mssql",
4043
+ "mysql": "postgresql"
4044
+ // MySQL uses similar rules to PostgreSQL
4045
+ };
4046
+ const dbType = typeMap[sourceType] || "postgresql";
4047
+ if (this.databaseRulesCache.has(dbType)) {
4048
+ return this.databaseRulesCache.get(dbType);
4049
+ }
4050
+ const rulesPath = path2.join(this.promptsDir, "database-rules", `${dbType}.md`);
4051
+ try {
4052
+ if (fs3.existsSync(rulesPath)) {
4053
+ const rules = fs3.readFileSync(rulesPath, "utf-8");
4054
+ this.databaseRulesCache.set(dbType, rules);
4055
+ return rules;
4056
+ }
4057
+ } catch (error) {
4058
+ logger.warn(`Could not load database rules for '${dbType}' from file system: ${error}`);
4059
+ }
4060
+ const defaultRules = this.getDefaultDatabaseRulesForType(dbType);
4061
+ this.databaseRulesCache.set(dbType, defaultRules);
4062
+ return defaultRules;
4063
+ }
4032
4064
  /**
4033
4065
  * Get default database rules as fallback
4034
4066
  * @returns Minimal database rules
@@ -4048,6 +4080,33 @@ var PromptLoader = class {
4048
4080
  }
4049
4081
  return `**Database Type: PostgreSQL**
4050
4082
 
4083
+ **SQL Query Rules:**
4084
+ - Use \`LIMIT N\` for row limiting (e.g., \`SELECT * FROM table LIMIT 32\`)
4085
+ - Use \`true\` / \`false\` for boolean values
4086
+ - Use \`||\` for string concatenation
4087
+ - Use \`NOW()\` for current timestamp
4088
+ - Use \`::TYPE\` or \`CAST()\` for type casting
4089
+ - Use \`RETURNING\` clause for mutations
4090
+ - NULL values: Use \`NULL\` keyword without quotes`;
4091
+ }
4092
+ /**
4093
+ * Get default database rules for a specific type
4094
+ */
4095
+ getDefaultDatabaseRulesForType(dbType) {
4096
+ if (dbType === "mssql") {
4097
+ return `**Database Type: Microsoft SQL Server**
4098
+
4099
+ **SQL Query Rules:**
4100
+ - Use \`TOP N\` for row limiting (e.g., \`SELECT TOP 32 * FROM table\`)
4101
+ - Use \`1\` for true, \`0\` for false (no native boolean)
4102
+ - Use \`+\` or \`CONCAT()\` for string concatenation
4103
+ - Use \`GETDATE()\` for current timestamp
4104
+ - Use \`CAST()\` or \`CONVERT()\` for type casting
4105
+ - Use \`OUTPUT INSERTED.*\` instead of \`RETURNING\`
4106
+ - NULL values: Use \`NULL\` keyword without quotes`;
4107
+ }
4108
+ return `**Database Type: PostgreSQL**
4109
+
4051
4110
  **SQL Query Rules:**
4052
4111
  - Use \`LIMIT N\` for row limiting (e.g., \`SELECT * FROM table LIMIT 32\`)
4053
4112
  - Use \`true\` / \`false\` for boolean values
@@ -6613,9 +6672,11 @@ var SourceAgent = class {
6613
6672
  const { intent, aggregation = "raw" } = input;
6614
6673
  logger.info(`[SourceAgent:${this.tool.name}] Starting | intent: "${intent}" | aggregation: ${aggregation}`);
6615
6674
  if (this.streamBuffer.hasCallback()) {
6675
+ const sourceType = this.extractSourceType();
6676
+ const sourceLabel = sourceType !== "unknown" ? ` (${sourceType})` : "";
6616
6677
  this.streamBuffer.write(`
6617
6678
 
6618
- \u{1F517} **Querying ${this.tool.name}...**
6679
+ \u{1F517} **Connecting to ${this.tool.name}**${sourceLabel}
6619
6680
 
6620
6681
  `);
6621
6682
  await streamDelay();
@@ -6635,7 +6696,7 @@ var SourceAgent = class {
6635
6696
  if (this.attempts > 1 && this.streamBuffer.hasCallback()) {
6636
6697
  this.streamBuffer.write(`
6637
6698
 
6638
- \u{1F504} **Retrying ${this.tool.name} (attempt ${this.attempts}/${this.config.maxRetries})...**
6699
+ \u{1F504} **Query failed, retrying with corrected query** (attempt ${this.attempts}/${this.config.maxRetries})
6639
6700
 
6640
6701
  `);
6641
6702
  await streamDelay();
@@ -6645,10 +6706,29 @@ var SourceAgent = class {
6645
6706
  cappedInput.limit = this.config.maxRowsPerSource;
6646
6707
  }
6647
6708
  queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
6709
+ if (this.streamBuffer.hasCallback() && queryExecuted) {
6710
+ const queryDisplay = cappedInput.sql || cappedInput.query;
6711
+ if (queryDisplay) {
6712
+ this.streamBuffer.write(`\u{1F4DD} **Generated SQL query:**
6713
+ \`\`\`sql
6714
+ ${queryDisplay}
6715
+ \`\`\`
6716
+
6717
+ `);
6718
+ } else {
6719
+ this.streamBuffer.write(`\u{1F4DD} **Query parameters:**
6720
+ \`\`\`json
6721
+ ${JSON.stringify(cappedInput, null, 2)}
6722
+ \`\`\`
6723
+
6724
+ `);
6725
+ }
6726
+ await streamDelay();
6727
+ }
6648
6728
  try {
6649
6729
  const result = await withProgressHeartbeat(
6650
6730
  () => this.tool.fn(cappedInput),
6651
- `Running ${this.tool.name}`,
6731
+ `Executing query`,
6652
6732
  this.streamBuffer
6653
6733
  );
6654
6734
  if (result && result.error) {
@@ -6660,6 +6740,12 @@ Analyze the error and try again with a corrected query.`;
6660
6740
  }
6661
6741
  resultData = result.data || [];
6662
6742
  totalRowsMatched = result.metadata?.totalCount || result.count || resultData.length;
6743
+ if (this.streamBuffer.hasCallback()) {
6744
+ const totalInfo = totalRowsMatched > resultData.length ? ` (${totalRowsMatched} total matched)` : "";
6745
+ this.streamBuffer.write(`\u2705 Retrieved ${resultData.length} rows${totalInfo}
6746
+
6747
+ `);
6748
+ }
6663
6749
  const formattedResult = formatToolResultForLLM(result, {
6664
6750
  toolName: this.tool.name,
6665
6751
  maxRows: 5,
@@ -6675,7 +6761,9 @@ Analyze the error and try again with a corrected query.`;
6675
6761
  _metadata: result.metadata,
6676
6762
  _sampleData: resultData.slice(0, 3)
6677
6763
  },
6678
- outputSchema: this.tool.outputSchema
6764
+ outputSchema: this.tool.outputSchema,
6765
+ sourceSchema: this.tool.description,
6766
+ sourceType: this.extractSourceType()
6679
6767
  };
6680
6768
  const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
6681
6769
  return `\u2705 Query executed successfully. ${resultData.length} rows returned (${totalRowsMatched} total matched). Data is ready \u2014 do NOT call the tool again.
@@ -7028,158 +7116,134 @@ var DEFAULT_AGENT_CONFIG = {
7028
7116
  maxIterations: 10
7029
7117
  };
7030
7118
 
7031
- // src/userResponse/anthropic.ts
7032
- import dotenv from "dotenv";
7033
-
7034
- // src/userResponse/schema.ts
7035
- import path5 from "path";
7036
- import fs6 from "fs";
7037
- var Schema = class {
7038
- constructor(schemaFilePath) {
7039
- this.cachedSchema = null;
7040
- this.schemaFilePath = schemaFilePath || path5.join(process.cwd(), "../analysis/data/schema.json");
7119
+ // src/userResponse/utils/component-props-processor.ts
7120
+ function validateExternalTool(externalTool, executedTools, providerName) {
7121
+ if (!externalTool) {
7122
+ return { valid: true };
7041
7123
  }
7042
- /**
7043
- * Gets the database schema from the schema file
7044
- * @returns Parsed schema object or null if error occurs
7045
- */
7046
- getDatabaseSchema() {
7047
- try {
7048
- const dir = path5.dirname(this.schemaFilePath);
7049
- if (!fs6.existsSync(dir)) {
7050
- logger.info(`Creating directory structure: ${dir}`);
7051
- fs6.mkdirSync(dir, { recursive: true });
7052
- }
7053
- if (!fs6.existsSync(this.schemaFilePath)) {
7054
- logger.info(`Schema file does not exist at ${this.schemaFilePath}, creating with empty schema`);
7055
- const initialSchema = {
7056
- database: "",
7057
- schema: "",
7058
- description: "",
7059
- tables: [],
7060
- relationships: []
7061
- };
7062
- fs6.writeFileSync(this.schemaFilePath, JSON.stringify(initialSchema, null, 4));
7063
- this.cachedSchema = initialSchema;
7064
- return initialSchema;
7065
- }
7066
- const fileContent = fs6.readFileSync(this.schemaFilePath, "utf-8");
7067
- const schema2 = JSON.parse(fileContent);
7068
- this.cachedSchema = schema2;
7069
- return schema2;
7070
- } catch (error) {
7071
- logger.error("Error parsing schema file:", error);
7072
- return null;
7073
- }
7124
+ const toolId = externalTool.toolId;
7125
+ const validToolIds = (executedTools || []).map((t) => t.id);
7126
+ const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
7127
+ if (!isValidTool) {
7128
+ logger.warn(`[${providerName}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
7129
+ return { valid: false };
7074
7130
  }
7075
- /**
7076
- * Gets the cached schema or loads it if not cached
7077
- * @returns Cached schema or freshly loaded schema
7078
- */
7079
- getSchema() {
7080
- if (this.cachedSchema) {
7081
- return this.cachedSchema;
7082
- }
7083
- return this.getDatabaseSchema();
7131
+ const executedTool = executedTools?.find((t) => t.id === toolId);
7132
+ return { valid: true, executedTool };
7133
+ }
7134
+ function validateAndCleanQuery(query, config) {
7135
+ if (!query) {
7136
+ return { query: null, wasModified: false };
7084
7137
  }
7085
- /**
7086
- * Generates database schema documentation for LLM from Snowflake JSON schema
7087
- * @returns Formatted schema documentation string
7088
- */
7089
- generateSchemaDocumentation() {
7090
- const schema2 = this.getSchema();
7091
- if (!schema2) {
7092
- logger.warn("No database schema found.");
7093
- return "No database schema available.";
7094
- }
7095
- const tables = [];
7096
- tables.push(`Database: ${schema2.database}`);
7097
- tables.push(`Schema: ${schema2.schema}`);
7098
- tables.push(`Description: ${schema2.description}`);
7099
- tables.push("");
7100
- tables.push("=".repeat(80));
7101
- tables.push("");
7102
- for (const table of schema2.tables) {
7103
- const tableInfo = [];
7104
- tableInfo.push(`TABLE: ${table.fullName}`);
7105
- tableInfo.push(`Description: ${table.description}`);
7106
- tableInfo.push(`Row Count: ~${table.rowCount.toLocaleString()}`);
7107
- tableInfo.push("");
7108
- tableInfo.push("Columns:");
7109
- for (const column of table.columns) {
7110
- let columnLine = ` - ${column.name}: ${column.type}`;
7111
- if (column.isPrimaryKey) {
7112
- columnLine += " (PRIMARY KEY)";
7113
- }
7114
- if (column.isForeignKey && column.references) {
7115
- columnLine += ` (FK -> ${column.references.table}.${column.references.column})`;
7116
- }
7117
- if (!column.nullable) {
7118
- columnLine += " NOT NULL";
7119
- }
7120
- if (column.description) {
7121
- columnLine += ` - ${column.description}`;
7122
- }
7123
- tableInfo.push(columnLine);
7124
- if (column.sampleValues && column.sampleValues.length > 0) {
7125
- tableInfo.push(` Sample values: [${column.sampleValues.join(", ")}]`);
7126
- }
7127
- if (column.statistics) {
7128
- const stats = column.statistics;
7129
- if (stats.min !== void 0 && stats.max !== void 0) {
7130
- tableInfo.push(` Range: ${stats.min} to ${stats.max}`);
7131
- }
7132
- if (stats.distinct !== void 0) {
7133
- tableInfo.push(` Distinct values: ${stats.distinct.toLocaleString()}`);
7134
- }
7135
- }
7136
- }
7137
- tableInfo.push("");
7138
- tables.push(tableInfo.join("\n"));
7138
+ let wasModified = false;
7139
+ let cleanedQuery = query;
7140
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
7141
+ if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
7142
+ logger.warn(`[${config.providerName}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
7143
+ return { query: null, wasModified: true };
7144
+ }
7145
+ const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
7146
+ if (fixed) {
7147
+ logger.warn(`[${config.providerName}] SQL fixes applied to component query: ${fixes.join("; ")}`);
7148
+ wasModified = true;
7149
+ if (typeof cleanedQuery === "string") {
7150
+ cleanedQuery = fixedQuery;
7151
+ } else if (cleanedQuery?.sql) {
7152
+ cleanedQuery = { ...cleanedQuery, sql: fixedQuery };
7139
7153
  }
7140
- tables.push("=".repeat(80));
7141
- tables.push("");
7142
- tables.push("TABLE RELATIONSHIPS:");
7143
- tables.push("");
7144
- for (const rel of schema2.relationships) {
7145
- tables.push(`${rel.from} -> ${rel.to} (${rel.type}): ${rel.keys.join(" = ")}`);
7154
+ }
7155
+ if (typeof cleanedQuery === "string") {
7156
+ const limitedQuery = ensureQueryLimit(cleanedQuery, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7157
+ if (limitedQuery !== cleanedQuery) wasModified = true;
7158
+ cleanedQuery = limitedQuery;
7159
+ } else if (cleanedQuery?.sql) {
7160
+ const limitedSql = ensureQueryLimit(cleanedQuery.sql, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7161
+ if (limitedSql !== cleanedQuery.sql) wasModified = true;
7162
+ cleanedQuery = { ...cleanedQuery, sql: limitedSql };
7163
+ }
7164
+ return { query: cleanedQuery, wasModified };
7165
+ }
7166
+ function processComponentProps(props, executedTools, config) {
7167
+ let cleanedProps = { ...props };
7168
+ if (cleanedProps.externalTool) {
7169
+ const { valid } = validateExternalTool(
7170
+ cleanedProps.externalTool,
7171
+ executedTools,
7172
+ config.providerName
7173
+ );
7174
+ if (!valid) {
7175
+ cleanedProps.externalTool = null;
7146
7176
  }
7147
- return tables.join("\n");
7148
7177
  }
7149
- /**
7150
- * Clears the cached schema, forcing a reload on next access
7151
- */
7152
- clearCache() {
7153
- this.cachedSchema = null;
7178
+ if (cleanedProps.query) {
7179
+ const { query } = validateAndCleanQuery(cleanedProps.query, config);
7180
+ cleanedProps.query = query;
7154
7181
  }
7155
- /**
7156
- * Sets a custom schema file path
7157
- * @param filePath - Path to the schema file
7158
- */
7159
- setSchemaPath(filePath) {
7160
- this.schemaFilePath = filePath;
7161
- this.clearCache();
7182
+ if (cleanedProps.query && cleanedProps.externalTool) {
7183
+ logger.info(`[${config.providerName}] Both query and externalTool exist, keeping both - frontend will decide`);
7162
7184
  }
7163
- };
7164
- var schema = new Schema();
7185
+ return cleanedProps;
7186
+ }
7165
7187
 
7166
- // src/userResponse/knowledge-base.ts
7167
- var getKnowledgeBase = async ({
7168
- prompt,
7169
- collections,
7170
- topK = 1
7171
- }) => {
7172
- try {
7173
- if (!collections || !collections["knowledge-base"] || !collections["knowledge-base"]["query"]) {
7174
- logger.warn("[KnowledgeBase] knowledge-base.query collection not registered, skipping");
7175
- return "";
7176
- }
7177
- const result = await collections["knowledge-base"]["query"]({
7178
- prompt,
7179
- topK
7180
- });
7181
- if (!result || !result.content) {
7182
- logger.warn("[KnowledgeBase] No knowledge base results returned");
7188
+ // src/userResponse/prompt-extractor.ts
7189
+ function extractPromptText(content) {
7190
+ if (content === null || content === void 0) {
7191
+ return "";
7192
+ }
7193
+ if (typeof content === "string") {
7194
+ return content;
7195
+ }
7196
+ if (Array.isArray(content)) {
7197
+ return content.map((item) => extractContentBlockText(item)).filter((text) => text.length > 0).join("\n\n---\n\n");
7198
+ }
7199
+ if (content && typeof content === "object") {
7200
+ return extractObjectText(content);
7201
+ }
7202
+ return String(content);
7203
+ }
7204
+ function extractContentBlockText(item) {
7205
+ if (typeof item === "string") {
7206
+ return item;
7207
+ }
7208
+ if (item && typeof item === "object") {
7209
+ const obj = item;
7210
+ if (typeof obj.text === "string") {
7211
+ return obj.text;
7212
+ }
7213
+ if (typeof obj.content === "string") {
7214
+ return obj.content;
7215
+ }
7216
+ return JSON.stringify(item, null, 2);
7217
+ }
7218
+ return String(item);
7219
+ }
7220
+ function extractObjectText(obj) {
7221
+ if (typeof obj.text === "string") {
7222
+ return obj.text;
7223
+ }
7224
+ if (typeof obj.content === "string") {
7225
+ return obj.content;
7226
+ }
7227
+ return JSON.stringify(obj, null, 2);
7228
+ }
7229
+
7230
+ // src/userResponse/knowledge-base.ts
7231
+ var getKnowledgeBase = async ({
7232
+ prompt,
7233
+ collections,
7234
+ topK = 1
7235
+ }) => {
7236
+ try {
7237
+ if (!collections || !collections["knowledge-base"] || !collections["knowledge-base"]["query"]) {
7238
+ logger.warn("[KnowledgeBase] knowledge-base.query collection not registered, skipping");
7239
+ return "";
7240
+ }
7241
+ const result = await collections["knowledge-base"]["query"]({
7242
+ prompt,
7243
+ topK
7244
+ });
7245
+ if (!result || !result.content) {
7246
+ logger.warn("[KnowledgeBase] No knowledge base results returned");
7183
7247
  return "";
7184
7248
  }
7185
7249
  logger.info(`[KnowledgeBase] Retrieved knowledge base context (${result.content.length} chars)`);
@@ -7287,116 +7351,601 @@ var KB = {
7287
7351
  };
7288
7352
  var knowledge_base_default = KB;
7289
7353
 
7290
- // src/userResponse/prompt-extractor.ts
7291
- function extractPromptText(content) {
7292
- if (content === null || content === void 0) {
7293
- return "";
7294
- }
7295
- if (typeof content === "string") {
7296
- return content;
7354
+ // src/userResponse/agents/agent-component-generator.ts
7355
+ async function generateAgentComponents(params) {
7356
+ const startTime = Date.now();
7357
+ const {
7358
+ analysisContent,
7359
+ components,
7360
+ userPrompt,
7361
+ executedTools,
7362
+ collections,
7363
+ apiKey,
7364
+ componentStreamCallback,
7365
+ userId
7366
+ } = params;
7367
+ logger.info(`[AgentComponentGen] Starting | ${executedTools.length} executed tools | ${components.length} available components`);
7368
+ try {
7369
+ const availableComponentsText = formatAvailableComponents(components);
7370
+ const executedToolsText = formatExecutedTools(executedTools);
7371
+ const sourceTypes = [...new Set(executedTools.map((t) => t.sourceType).filter(Boolean))];
7372
+ let databaseRules = "";
7373
+ if (sourceTypes.length > 0) {
7374
+ const rulesArr = await Promise.all(
7375
+ sourceTypes.map((st) => promptLoader.loadDatabaseRulesForType(st))
7376
+ );
7377
+ databaseRules = rulesArr.join("\n\n");
7378
+ } else {
7379
+ databaseRules = await promptLoader.loadDatabaseRules();
7380
+ }
7381
+ let knowledgeBaseContext = "No additional knowledge base context available.";
7382
+ if (collections) {
7383
+ const kbResult = await knowledge_base_default.getAllKnowledgeBase({
7384
+ prompt: userPrompt || analysisContent,
7385
+ collections,
7386
+ userId,
7387
+ topK: KNOWLEDGE_BASE_TOP_K
7388
+ });
7389
+ knowledgeBaseContext = kbResult.combinedContext || knowledgeBaseContext;
7390
+ }
7391
+ const prompts = await promptLoader.loadPrompts("match-text-components", {
7392
+ USER_PROMPT: userPrompt || "",
7393
+ ANALYSIS_CONTENT: analysisContent,
7394
+ AVAILABLE_COMPONENTS: availableComponentsText,
7395
+ SCHEMA_DOC: "Use column names from executed query results in EXECUTED_TOOLS.",
7396
+ DATABASE_RULES: databaseRules,
7397
+ DEFERRED_TOOLS: "No deferred external tools for this request.",
7398
+ EXECUTED_TOOLS: executedToolsText,
7399
+ KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext,
7400
+ CURRENT_DATETIME: getCurrentDateTimeForPrompt()
7401
+ });
7402
+ logger.logLLMPrompt("agentComponentGen", "system", extractPromptText(prompts.system));
7403
+ logger.logLLMPrompt("agentComponentGen", "user", `Text Analysis:
7404
+ ${analysisContent}
7405
+
7406
+ Executed Tools:
7407
+ ${executedToolsText}`);
7408
+ let fullResponseText = "";
7409
+ let answerComponentStreamed = false;
7410
+ const partialCallback = componentStreamCallback ? (chunk) => {
7411
+ fullResponseText += chunk;
7412
+ if (!answerComponentStreamed && componentStreamCallback) {
7413
+ const streamed = tryStreamAnswerComponent(
7414
+ fullResponseText,
7415
+ components,
7416
+ executedTools,
7417
+ collections,
7418
+ apiKey,
7419
+ componentStreamCallback
7420
+ );
7421
+ if (streamed) answerComponentStreamed = true;
7422
+ }
7423
+ } : void 0;
7424
+ const result = await LLM.stream(
7425
+ { sys: prompts.system, user: prompts.user },
7426
+ {
7427
+ model: "anthropic/claude-haiku-4-5-20251001",
7428
+ maxTokens: MAX_TOKENS_COMPONENT_MATCHING,
7429
+ temperature: 0,
7430
+ apiKey,
7431
+ partial: partialCallback
7432
+ },
7433
+ true
7434
+ // Parse as JSON
7435
+ );
7436
+ const matchedComponents = result.matchedComponents || [];
7437
+ const layoutTitle = result.layoutTitle || "Dashboard";
7438
+ const layoutDescription = result.layoutDescription || "Multi-component dashboard";
7439
+ if (result.hasAnswerComponent && result.answerComponent?.componentId) {
7440
+ const answer = result.answerComponent;
7441
+ const answerSql = answer.props?.externalTool?.parameters?.sql || "";
7442
+ const answerTitle = answer.props?.title || "";
7443
+ const answerType = answer.componentName || "";
7444
+ const isDuplicate = matchedComponents.some((mc) => {
7445
+ const mcSql = mc.props?.externalTool?.parameters?.sql || "";
7446
+ const mcTitle = mc.props?.title || "";
7447
+ const mcType = mc.componentName || "";
7448
+ return mcType === answerType && (mcTitle === answerTitle || mcSql === answerSql);
7449
+ });
7450
+ if (!isDuplicate) {
7451
+ matchedComponents.unshift(answer);
7452
+ }
7453
+ }
7454
+ logger.info(`[AgentComponentGen] LLM returned ${matchedComponents.length} components`);
7455
+ matchedComponents.forEach((comp, idx) => {
7456
+ logger.info(`[AgentComponentGen] ${idx + 1}. ${comp.componentType} (${comp.componentName})`);
7457
+ });
7458
+ logger.file("\n=============================\nFull LLM response:", JSON.stringify(result, null, 2));
7459
+ const rawActions = result.actions || [];
7460
+ const actions = convertQuestionsToActions(rawActions);
7461
+ const finalComponents = matchedComponents.map((mc) => {
7462
+ const originalComponent = components.find((c) => c.name === mc.componentName);
7463
+ if (!originalComponent) {
7464
+ logger.warn(`[AgentComponentGen] Component "${mc.componentName}" not found in available components`);
7465
+ return null;
7466
+ }
7467
+ const cleanedProps = processComponentProps(
7468
+ mc.props,
7469
+ executedTools,
7470
+ { providerName: "AgentComponentGen", defaultLimit: 10 }
7471
+ );
7472
+ return {
7473
+ ...originalComponent,
7474
+ props: { ...originalComponent.props, ...cleanedProps }
7475
+ };
7476
+ }).filter(Boolean);
7477
+ const validatedComponents = await validateExternalToolQueries(
7478
+ finalComponents,
7479
+ collections,
7480
+ executedTools,
7481
+ apiKey
7482
+ );
7483
+ const elapsed = Date.now() - startTime;
7484
+ logger.info(`[AgentComponentGen] Complete | ${validatedComponents.length}/${finalComponents.length} validated | ${elapsed}ms`);
7485
+ return {
7486
+ components: validatedComponents,
7487
+ layoutTitle,
7488
+ layoutDescription,
7489
+ actions
7490
+ };
7491
+ } catch (error) {
7492
+ const errorMsg = error instanceof Error ? error.message : String(error);
7493
+ logger.error(`[AgentComponentGen] Error: ${errorMsg}`);
7494
+ return {
7495
+ components: [],
7496
+ layoutTitle: "Dashboard",
7497
+ layoutDescription: "",
7498
+ actions: []
7499
+ };
7297
7500
  }
7298
- if (Array.isArray(content)) {
7299
- return content.map((item) => extractContentBlockText(item)).filter((text) => text.length > 0).join("\n\n---\n\n");
7501
+ }
7502
+ function formatAvailableComponents(components) {
7503
+ if (!components || components.length === 0) return "No components available";
7504
+ return components.map((comp, idx) => {
7505
+ const keywords = comp.keywords ? comp.keywords.join(", ") : "";
7506
+ const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
7507
+ return `${idx + 1}. ID: ${comp.id}
7508
+ Name: ${comp.name}
7509
+ Type: ${comp.type}
7510
+ Description: ${comp.description || "No description"}
7511
+ Keywords: ${keywords}
7512
+ Props Structure: ${propsPreview}`;
7513
+ }).join("\n\n");
7514
+ }
7515
+ function formatExecutedTools(executedTools) {
7516
+ if (!executedTools || executedTools.length === 0) {
7517
+ return "No external tools were executed for data fetching.";
7518
+ }
7519
+ return "The following external tools were executed to fetch data.\n" + executedTools.map((tool, idx) => {
7520
+ let outputSchemaText = "Not available";
7521
+ let fieldNamesList = "";
7522
+ const recordCount = tool.result?._totalRecords ?? "unknown";
7523
+ let metadataText = "";
7524
+ if (tool.result?._metadata && Object.keys(tool.result._metadata).length > 0) {
7525
+ const metadataEntries = Object.entries(tool.result._metadata).map(([key, value]) => `${key}: ${value}`).join(", ");
7526
+ metadataText = `
7527
+ \u{1F4CB} METADATA: ${metadataEntries}`;
7528
+ }
7529
+ if (tool.outputSchema) {
7530
+ const fields = tool.outputSchema.fields || [];
7531
+ const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
7532
+ const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
7533
+ fieldNamesList = `
7534
+ \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
7535
+ \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
7536
+ const fieldsText = fields.map(
7537
+ (f) => ` "${f.name}" (${f.type}): ${f.description}`
7538
+ ).join("\n");
7539
+ outputSchemaText = `${tool.outputSchema.description}
7540
+ Fields:
7541
+ ${fieldsText}`;
7542
+ }
7543
+ return `${idx + 1}. **${tool.name}**
7544
+ toolId: "${tool.id}"
7545
+ toolName: "${tool.name}"
7546
+ parameters: ${JSON.stringify(tool.params || {})}
7547
+ recordCount: ${recordCount} rows returned${metadataText}
7548
+ outputSchema: ${outputSchemaText}${fieldNamesList}`;
7549
+ }).join("\n\n");
7550
+ }
7551
+ function tryStreamAnswerComponent(text, components, executedTools, collections, apiKey, callback) {
7552
+ const hasMatch = text.match(/"hasAnswerComponent"\s*:\s*(true|false)/);
7553
+ if (!hasMatch || hasMatch[1] !== "true") return false;
7554
+ const startMatch = text.match(/"answerComponent"\s*:\s*\{/);
7555
+ if (!startMatch) return false;
7556
+ const startPos = startMatch.index + startMatch[0].length - 1;
7557
+ let depth = 0;
7558
+ let inString = false;
7559
+ let escapeNext = false;
7560
+ let endPos = -1;
7561
+ for (let i = startPos; i < text.length; i++) {
7562
+ const char = text[i];
7563
+ if (escapeNext) {
7564
+ escapeNext = false;
7565
+ continue;
7566
+ }
7567
+ if (char === "\\") {
7568
+ escapeNext = true;
7569
+ continue;
7570
+ }
7571
+ if (char === '"') {
7572
+ inString = !inString;
7573
+ continue;
7574
+ }
7575
+ if (!inString) {
7576
+ if (char === "{") depth++;
7577
+ else if (char === "}") {
7578
+ depth--;
7579
+ if (depth === 0) {
7580
+ endPos = i + 1;
7581
+ break;
7582
+ }
7583
+ }
7584
+ }
7300
7585
  }
7301
- if (content && typeof content === "object") {
7302
- return extractObjectText(content);
7586
+ if (endPos <= startPos) return false;
7587
+ try {
7588
+ const answerData = JSON.parse(text.substring(startPos, endPos));
7589
+ if (!answerData?.componentId) return false;
7590
+ const original = components.find((c) => c.id === answerData.componentId);
7591
+ if (!original) return false;
7592
+ const answerComponent = {
7593
+ ...original,
7594
+ props: { ...original.props, ...answerData.props }
7595
+ };
7596
+ const answerProps = answerComponent.props;
7597
+ const sql = answerProps?.externalTool?.parameters?.sql;
7598
+ if (sql && collections?.["external-tools"]?.["execute"]) {
7599
+ const toolId = answerProps.externalTool.toolId;
7600
+ const toolName = answerProps.externalTool.toolName;
7601
+ (async () => {
7602
+ try {
7603
+ const result = await collections["external-tools"]["execute"]({
7604
+ toolId,
7605
+ toolName,
7606
+ sql,
7607
+ data: {}
7608
+ });
7609
+ if (result?.success === false || result?.error) {
7610
+ logger.warn(`[AgentComponentGen] Answer component query failed: ${result?.error}`);
7611
+ return;
7612
+ }
7613
+ queryCache.set(sql, result?.data ?? result);
7614
+ callback(answerComponent);
7615
+ } catch (err) {
7616
+ const msg = err instanceof Error ? err.message : String(err);
7617
+ logger.warn(`[AgentComponentGen] Answer component validation failed: ${msg}`);
7618
+ }
7619
+ })();
7620
+ } else {
7621
+ callback(answerComponent);
7622
+ }
7623
+ return true;
7624
+ } catch {
7625
+ return false;
7303
7626
  }
7304
- return String(content);
7305
7627
  }
7306
- function extractContentBlockText(item) {
7307
- if (typeof item === "string") {
7308
- return item;
7628
+ async function validateExternalToolQueries(components, collections, executedTools, apiKey) {
7629
+ if (!collections?.["external-tools"]?.["execute"]) {
7630
+ logger.warn(`[AgentComponentGen] external-tools.execute not available, skipping validation`);
7631
+ return components;
7309
7632
  }
7310
- if (item && typeof item === "object") {
7311
- const obj = item;
7312
- if (typeof obj.text === "string") {
7313
- return obj.text;
7633
+ const validated = [];
7634
+ const withSql = [];
7635
+ const withoutSql = [];
7636
+ for (const comp of components) {
7637
+ const sql = comp.props?.externalTool?.parameters?.sql;
7638
+ if (sql) {
7639
+ withSql.push(comp);
7640
+ } else {
7641
+ withoutSql.push(comp);
7314
7642
  }
7315
- if (typeof obj.content === "string") {
7316
- return obj.content;
7643
+ }
7644
+ validated.push(...withoutSql);
7645
+ if (withSql.length === 0) return validated;
7646
+ const sqlGroups = /* @__PURE__ */ new Map();
7647
+ for (const comp of withSql) {
7648
+ const sql = comp.props.externalTool.parameters.sql;
7649
+ const normalized = sql.replace(/\s+/g, " ").trim();
7650
+ if (!sqlGroups.has(normalized)) {
7651
+ sqlGroups.set(normalized, []);
7317
7652
  }
7318
- return JSON.stringify(item, null, 2);
7653
+ sqlGroups.get(normalized).push(comp);
7319
7654
  }
7320
- return String(item);
7321
- }
7322
- function extractObjectText(obj) {
7323
- if (typeof obj.text === "string") {
7324
- return obj.text;
7655
+ const uniqueQueries = Array.from(sqlGroups.entries());
7656
+ logger.info(`[AgentComponentGen] Validating ${uniqueQueries.length} unique queries (${withSql.length} components)...`);
7657
+ const results = await Promise.allSettled(
7658
+ uniqueQueries.map(([_, comps]) => validateSingleExternalToolQuery(comps[0], collections, executedTools, apiKey))
7659
+ );
7660
+ for (let i = 0; i < results.length; i++) {
7661
+ const result = results[i];
7662
+ const [_, groupComps] = uniqueQueries[i];
7663
+ if (result.status === "fulfilled" && result.value) {
7664
+ validated.push(result.value);
7665
+ for (let j = 1; j < groupComps.length; j++) {
7666
+ const fixedSql = result.value.props?.externalTool?.parameters?.sql;
7667
+ if (fixedSql) {
7668
+ const siblingProps = groupComps[j].props;
7669
+ validated.push({
7670
+ ...groupComps[j],
7671
+ props: {
7672
+ ...groupComps[j].props,
7673
+ externalTool: {
7674
+ ...siblingProps.externalTool,
7675
+ parameters: { ...siblingProps.externalTool.parameters, sql: fixedSql }
7676
+ }
7677
+ }
7678
+ });
7679
+ } else {
7680
+ validated.push(groupComps[j]);
7681
+ }
7682
+ }
7683
+ } else {
7684
+ const reason = result.status === "rejected" ? result.reason : "validation failed";
7685
+ for (const comp of groupComps) {
7686
+ logger.warn(`[AgentComponentGen] Excluded ${comp.name}: ${reason}`);
7687
+ }
7688
+ }
7325
7689
  }
7326
- if (typeof obj.content === "string") {
7327
- return obj.content;
7690
+ logger.info(`[AgentComponentGen] Validation complete: ${validated.length}/${components.length} components passed`);
7691
+ return validated;
7692
+ }
7693
+ async function validateSingleExternalToolQuery(component, collections, executedTools, apiKey) {
7694
+ const compProps = component.props;
7695
+ const toolId = compProps?.externalTool?.toolId;
7696
+ const toolName = compProps?.externalTool?.toolName;
7697
+ let currentSql = compProps?.externalTool?.parameters?.sql;
7698
+ if (!toolId || !currentSql) return component;
7699
+ currentSql = ensureQueryLimit(currentSql, 10, MAX_COMPONENT_QUERY_LIMIT);
7700
+ let attempts = 0;
7701
+ while (attempts < MAX_QUERY_VALIDATION_RETRIES) {
7702
+ attempts++;
7703
+ try {
7704
+ logger.info(`[AgentComponentGen] Validating ${component.name} (attempt ${attempts}/${MAX_QUERY_VALIDATION_RETRIES})`);
7705
+ const result = await collections["external-tools"]["execute"]({
7706
+ toolId,
7707
+ toolName,
7708
+ sql: currentSql,
7709
+ data: {}
7710
+ });
7711
+ if (result?.success === false || result?.error) {
7712
+ const errorMsg = result?.error || "Unknown error";
7713
+ throw new Error(typeof errorMsg === "string" ? errorMsg : JSON.stringify(errorMsg));
7714
+ }
7715
+ const rawToolData = result?.data ?? result;
7716
+ queryCache.set(currentSql, rawToolData);
7717
+ logger.info(`[AgentComponentGen] \u2713 ${component.name} validated (attempt ${attempts})`);
7718
+ return {
7719
+ ...component,
7720
+ props: {
7721
+ ...component.props,
7722
+ externalTool: {
7723
+ ...compProps.externalTool,
7724
+ parameters: {
7725
+ ...compProps.externalTool.parameters,
7726
+ sql: currentSql
7727
+ }
7728
+ }
7729
+ }
7730
+ };
7731
+ } catch (error) {
7732
+ const errorMsg = error instanceof Error ? error.message : String(error);
7733
+ logger.warn(`[AgentComponentGen] \u2717 ${component.name} failed (attempt ${attempts}): ${errorMsg}`);
7734
+ if (attempts >= MAX_QUERY_VALIDATION_RETRIES) {
7735
+ logger.error(`[AgentComponentGen] Max retries reached for ${component.name}, excluding`);
7736
+ return null;
7737
+ }
7738
+ try {
7739
+ const fixedSql = await requestExternalToolQueryFix(
7740
+ currentSql,
7741
+ errorMsg,
7742
+ component,
7743
+ executedTools,
7744
+ toolId,
7745
+ apiKey
7746
+ );
7747
+ if (fixedSql && fixedSql !== currentSql) {
7748
+ currentSql = ensureQueryLimit(fixedSql, 10, MAX_COMPONENT_QUERY_LIMIT);
7749
+ logger.info(`[AgentComponentGen] LLM provided fix for ${component.name}, retrying...`);
7750
+ } else {
7751
+ logger.warn(`[AgentComponentGen] LLM returned same or empty query, stopping retries`);
7752
+ return null;
7753
+ }
7754
+ } catch (fixError) {
7755
+ const fixMsg = fixError instanceof Error ? fixError.message : String(fixError);
7756
+ logger.error(`[AgentComponentGen] Failed to get LLM fix: ${fixMsg}`);
7757
+ return null;
7758
+ }
7759
+ }
7328
7760
  }
7329
- return JSON.stringify(obj, null, 2);
7761
+ return null;
7330
7762
  }
7763
+ async function requestExternalToolQueryFix(failedSql, errorMessage, component, executedTools, toolId, apiKey) {
7764
+ const executedTool = executedTools.find((t) => t.id === toolId);
7765
+ const sourceSchema = executedTool?.sourceSchema || "Schema not available";
7766
+ const sourceType = executedTool?.sourceType || "postgresql";
7767
+ const databaseRules = await promptLoader.loadDatabaseRulesForType(sourceType);
7768
+ const prompt = `You are a SQL expert. Fix the following SQL query that failed execution.
7331
7769
 
7332
- // src/userResponse/utils/component-props-processor.ts
7333
- function validateExternalTool(externalTool, executedTools, providerName) {
7334
- if (!externalTool) {
7335
- return { valid: true };
7336
- }
7337
- const toolId = externalTool.toolId;
7338
- const validToolIds = (executedTools || []).map((t) => t.id);
7339
- const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
7340
- if (!isValidTool) {
7341
- logger.warn(`[${providerName}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
7342
- return { valid: false };
7343
- }
7344
- const executedTool = executedTools?.find((t) => t.id === toolId);
7345
- return { valid: true, executedTool };
7770
+ ## Database Schema
7771
+ ${sourceSchema}
7772
+
7773
+ ## Database-Specific SQL Rules
7774
+ ${databaseRules}
7775
+
7776
+ ## Component Context
7777
+ - Component Name: ${component.name}
7778
+ - Component Type: ${component.type}
7779
+ - Title: ${component.props?.title || "N/A"}
7780
+
7781
+ ## Failed Query
7782
+ \`\`\`sql
7783
+ ${failedSql}
7784
+ \`\`\`
7785
+
7786
+ ## Error Message
7787
+ ${errorMessage}
7788
+
7789
+ ## Instructions
7790
+ 1. Analyze the error message and identify what caused the query to fail
7791
+ 2. Fix the query to resolve the error while preserving the original intent
7792
+ 3. Ensure the fixed query follows the database-specific SQL rules above
7793
+ 4. Return ONLY the fixed SQL query, no explanations or markdown
7794
+
7795
+ Fixed SQL query:`;
7796
+ const response = await LLM.text(
7797
+ {
7798
+ sys: "You are a SQL expert. Return only the fixed SQL query with no additional text, explanations, or markdown formatting.",
7799
+ user: prompt
7800
+ },
7801
+ {
7802
+ model: "anthropic/claude-haiku-4-5-20251001",
7803
+ maxTokens: 2048,
7804
+ temperature: 0,
7805
+ apiKey
7806
+ }
7807
+ );
7808
+ let fixedQuery = response.trim();
7809
+ fixedQuery = fixedQuery.replace(/^```sql\s*/i, "").replace(/\s*```$/i, "");
7810
+ fixedQuery = fixedQuery.replace(/^```\s*/i, "").replace(/\s*```$/i, "");
7811
+ const { query: validatedQuery } = validateAndFixSqlQuery(fixedQuery);
7812
+ return validatedQuery;
7346
7813
  }
7347
- function validateAndCleanQuery(query, config) {
7348
- if (!query) {
7349
- return { query: null, wasModified: false };
7350
- }
7351
- let wasModified = false;
7352
- let cleanedQuery = query;
7353
- const queryStr = typeof query === "string" ? query : query?.sql || "";
7354
- if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
7355
- logger.warn(`[${config.providerName}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
7356
- return { query: null, wasModified: true };
7814
+
7815
+ // src/userResponse/anthropic.ts
7816
+ import dotenv from "dotenv";
7817
+
7818
+ // src/userResponse/schema.ts
7819
+ import path5 from "path";
7820
+ import fs6 from "fs";
7821
+ var Schema = class {
7822
+ constructor(schemaFilePath) {
7823
+ this.cachedSchema = null;
7824
+ this.schemaFilePath = schemaFilePath || path5.join(process.cwd(), "../analysis/data/schema.json");
7357
7825
  }
7358
- const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
7359
- if (fixed) {
7360
- logger.warn(`[${config.providerName}] SQL fixes applied to component query: ${fixes.join("; ")}`);
7361
- wasModified = true;
7362
- if (typeof cleanedQuery === "string") {
7363
- cleanedQuery = fixedQuery;
7364
- } else if (cleanedQuery?.sql) {
7365
- cleanedQuery = { ...cleanedQuery, sql: fixedQuery };
7826
+ /**
7827
+ * Gets the database schema from the schema file
7828
+ * @returns Parsed schema object or null if error occurs
7829
+ */
7830
+ getDatabaseSchema() {
7831
+ try {
7832
+ const dir = path5.dirname(this.schemaFilePath);
7833
+ if (!fs6.existsSync(dir)) {
7834
+ logger.info(`Creating directory structure: ${dir}`);
7835
+ fs6.mkdirSync(dir, { recursive: true });
7836
+ }
7837
+ if (!fs6.existsSync(this.schemaFilePath)) {
7838
+ logger.info(`Schema file does not exist at ${this.schemaFilePath}, creating with empty schema`);
7839
+ const initialSchema = {
7840
+ database: "",
7841
+ schema: "",
7842
+ description: "",
7843
+ tables: [],
7844
+ relationships: []
7845
+ };
7846
+ fs6.writeFileSync(this.schemaFilePath, JSON.stringify(initialSchema, null, 4));
7847
+ this.cachedSchema = initialSchema;
7848
+ return initialSchema;
7849
+ }
7850
+ const fileContent = fs6.readFileSync(this.schemaFilePath, "utf-8");
7851
+ const schema2 = JSON.parse(fileContent);
7852
+ this.cachedSchema = schema2;
7853
+ return schema2;
7854
+ } catch (error) {
7855
+ logger.error("Error parsing schema file:", error);
7856
+ return null;
7366
7857
  }
7367
7858
  }
7368
- if (typeof cleanedQuery === "string") {
7369
- const limitedQuery = ensureQueryLimit(cleanedQuery, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7370
- if (limitedQuery !== cleanedQuery) wasModified = true;
7371
- cleanedQuery = limitedQuery;
7372
- } else if (cleanedQuery?.sql) {
7373
- const limitedSql = ensureQueryLimit(cleanedQuery.sql, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7374
- if (limitedSql !== cleanedQuery.sql) wasModified = true;
7375
- cleanedQuery = { ...cleanedQuery, sql: limitedSql };
7859
+ /**
7860
+ * Gets the cached schema or loads it if not cached
7861
+ * @returns Cached schema or freshly loaded schema
7862
+ */
7863
+ getSchema() {
7864
+ if (this.cachedSchema) {
7865
+ return this.cachedSchema;
7866
+ }
7867
+ return this.getDatabaseSchema();
7376
7868
  }
7377
- return { query: cleanedQuery, wasModified };
7378
- }
7379
- function processComponentProps(props, executedTools, config) {
7380
- let cleanedProps = { ...props };
7381
- if (cleanedProps.externalTool) {
7382
- const { valid } = validateExternalTool(
7383
- cleanedProps.externalTool,
7384
- executedTools,
7385
- config.providerName
7386
- );
7387
- if (!valid) {
7388
- cleanedProps.externalTool = null;
7869
+ /**
7870
+ * Generates database schema documentation for LLM from Snowflake JSON schema
7871
+ * @returns Formatted schema documentation string
7872
+ */
7873
+ generateSchemaDocumentation() {
7874
+ const schema2 = this.getSchema();
7875
+ if (!schema2) {
7876
+ logger.warn("No database schema found.");
7877
+ return "No database schema available.";
7878
+ }
7879
+ const tables = [];
7880
+ tables.push(`Database: ${schema2.database}`);
7881
+ tables.push(`Schema: ${schema2.schema}`);
7882
+ tables.push(`Description: ${schema2.description}`);
7883
+ tables.push("");
7884
+ tables.push("=".repeat(80));
7885
+ tables.push("");
7886
+ for (const table of schema2.tables) {
7887
+ const tableInfo = [];
7888
+ tableInfo.push(`TABLE: ${table.fullName}`);
7889
+ tableInfo.push(`Description: ${table.description}`);
7890
+ tableInfo.push(`Row Count: ~${table.rowCount.toLocaleString()}`);
7891
+ tableInfo.push("");
7892
+ tableInfo.push("Columns:");
7893
+ for (const column of table.columns) {
7894
+ let columnLine = ` - ${column.name}: ${column.type}`;
7895
+ if (column.isPrimaryKey) {
7896
+ columnLine += " (PRIMARY KEY)";
7897
+ }
7898
+ if (column.isForeignKey && column.references) {
7899
+ columnLine += ` (FK -> ${column.references.table}.${column.references.column})`;
7900
+ }
7901
+ if (!column.nullable) {
7902
+ columnLine += " NOT NULL";
7903
+ }
7904
+ if (column.description) {
7905
+ columnLine += ` - ${column.description}`;
7906
+ }
7907
+ tableInfo.push(columnLine);
7908
+ if (column.sampleValues && column.sampleValues.length > 0) {
7909
+ tableInfo.push(` Sample values: [${column.sampleValues.join(", ")}]`);
7910
+ }
7911
+ if (column.statistics) {
7912
+ const stats = column.statistics;
7913
+ if (stats.min !== void 0 && stats.max !== void 0) {
7914
+ tableInfo.push(` Range: ${stats.min} to ${stats.max}`);
7915
+ }
7916
+ if (stats.distinct !== void 0) {
7917
+ tableInfo.push(` Distinct values: ${stats.distinct.toLocaleString()}`);
7918
+ }
7919
+ }
7920
+ }
7921
+ tableInfo.push("");
7922
+ tables.push(tableInfo.join("\n"));
7389
7923
  }
7924
+ tables.push("=".repeat(80));
7925
+ tables.push("");
7926
+ tables.push("TABLE RELATIONSHIPS:");
7927
+ tables.push("");
7928
+ for (const rel of schema2.relationships) {
7929
+ tables.push(`${rel.from} -> ${rel.to} (${rel.type}): ${rel.keys.join(" = ")}`);
7930
+ }
7931
+ return tables.join("\n");
7390
7932
  }
7391
- if (cleanedProps.query) {
7392
- const { query } = validateAndCleanQuery(cleanedProps.query, config);
7393
- cleanedProps.query = query;
7933
+ /**
7934
+ * Clears the cached schema, forcing a reload on next access
7935
+ */
7936
+ clearCache() {
7937
+ this.cachedSchema = null;
7394
7938
  }
7395
- if (cleanedProps.query && cleanedProps.externalTool) {
7396
- logger.info(`[${config.providerName}] Both query and externalTool exist, keeping both - frontend will decide`);
7939
+ /**
7940
+ * Sets a custom schema file path
7941
+ * @param filePath - Path to the schema file
7942
+ */
7943
+ setSchemaPath(filePath) {
7944
+ this.schemaFilePath = filePath;
7945
+ this.clearCache();
7397
7946
  }
7398
- return cleanedProps;
7399
- }
7947
+ };
7948
+ var schema = new Schema();
7400
7949
 
7401
7950
  // src/userResponse/services/query-execution-service.ts
7402
7951
  var QueryExecutionService = class {
@@ -9329,7 +9878,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9329
9878
  let layoutDescription = "Multi-component dashboard";
9330
9879
  let actions = [];
9331
9880
  if (!hasExecutedTools) {
9332
- logger.info(`[AgentFlow] No tools executed \u2014 general question, skipping component generation`);
9881
+ logger.info(`[AgentFlow] No tools executed \u2014 general question, wrapping in DynamicMarkdownBlock`);
9333
9882
  const nextQuestions = await llmInstance.generateNextQuestions(
9334
9883
  prompt,
9335
9884
  null,
@@ -9339,6 +9888,18 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9339
9888
  textResponse
9340
9889
  );
9341
9890
  actions = convertQuestionsToActions(nextQuestions);
9891
+ const markdownContent = textResponse.replace(/<DashboardComponents>[\s\S]*?<\/DashboardComponents>/g, "").trim();
9892
+ matchedComponents = [{
9893
+ id: "dynamic-markdown-block",
9894
+ name: "DynamicMarkdownBlock",
9895
+ type: "MarkdownBlock",
9896
+ description: "Text response rendered as markdown",
9897
+ props: {
9898
+ content: markdownContent
9899
+ }
9900
+ }];
9901
+ layoutTitle = "";
9902
+ layoutDescription = "";
9342
9903
  } else if (components && components.length > 0) {
9343
9904
  logger.info(`[AgentFlow] ${agentResponse.executedTools.length} tools executed \u2014 generating components`);
9344
9905
  if (streamBuffer.hasCallback()) {
@@ -9353,18 +9914,16 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9353
9914
  /<DataTable>[\s\S]*?<\/DataTable>/g,
9354
9915
  "<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>"
9355
9916
  );
9356
- const matchResult = await llmInstance.matchComponentsFromAnalysis(
9357
- sanitizedTextResponse,
9917
+ const matchResult = await generateAgentComponents({
9918
+ analysisContent: sanitizedTextResponse,
9358
9919
  components,
9359
- prompt,
9920
+ userPrompt: prompt,
9921
+ executedTools: agentResponse.executedTools,
9922
+ collections,
9360
9923
  apiKey,
9361
9924
  componentStreamCallback,
9362
- [],
9363
- // deferredTools — MainAgent handles tool execution
9364
- agentResponse.executedTools,
9365
- collections,
9366
9925
  userId
9367
- );
9926
+ });
9368
9927
  matchedComponents = matchResult.components;
9369
9928
  layoutTitle = matchResult.layoutTitle;
9370
9929
  layoutDescription = matchResult.layoutDescription;
@@ -9372,9 +9931,11 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
9372
9931
  }
9373
9932
  const securedComponents = matchedComponents.map((comp) => {
9374
9933
  const props = { ...comp.props };
9375
- if (props.externalTool?.parameters?.sql) {
9376
- const { sql, ...restParams } = props.externalTool.parameters;
9377
- const queryId = queryCache.storeQuery(sql);
9934
+ const sqlValue = props.externalTool?.parameters?.sql || props.externalTool?.parameters?.query;
9935
+ if (sqlValue) {
9936
+ const { sql, query, ...restParams } = props.externalTool.parameters;
9937
+ const cachedData = queryCache.get(sqlValue);
9938
+ const queryId = queryCache.storeQuery(sqlValue, cachedData);
9378
9939
  props.externalTool = {
9379
9940
  ...props.externalTool,
9380
9941
  parameters: { queryId, ...restParams }
@@ -9493,18 +10054,10 @@ async function saveConversation(params) {
9493
10054
  }
9494
10055
  try {
9495
10056
  logger.info(`[CONVERSATION_SAVER] Saving conversation for userId: ${userId}, uiBlockId: ${uiBlockId}, threadId: ${threadId}`);
9496
- const userIdNumber = Number(userId);
9497
- if (isNaN(userIdNumber)) {
9498
- logger.warn(`[CONVERSATION_SAVER] Invalid userId: ${userId} (not a valid number)`);
9499
- return {
9500
- success: false,
9501
- error: `Invalid userId: ${userId} (not a valid number)`
9502
- };
9503
- }
9504
10057
  const dbUIBlock = transformUIBlockForDB(uiblock, userPrompt, uiBlockId);
9505
10058
  logger.debug(`[CONVERSATION_SAVER] Transformed UIBlock for DB: ${JSON.stringify(dbUIBlock)}`);
9506
10059
  const saveResult = await collections["user-conversations"]["create"]({
9507
- userId: userIdNumber,
10060
+ userId,
9508
10061
  userPrompt,
9509
10062
  uiblock: dbUIBlock,
9510
10063
  threadId
@@ -9525,7 +10078,7 @@ async function saveConversation(params) {
9525
10078
  userPrompt,
9526
10079
  uiBlock: dbUIBlock,
9527
10080
  // Use the transformed UIBlock
9528
- userId: userIdNumber
10081
+ userId
9529
10082
  });
9530
10083
  if (embedResult?.success) {
9531
10084
  logger.info("[CONVERSATION_SAVER] Successfully created embedding");