@superatomai/sdk-node 0.0.70 → 0.0.72

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
@@ -1516,6 +1516,351 @@ var QueryCache = class {
1516
1516
  };
1517
1517
  var queryCache = new QueryCache();
1518
1518
 
1519
+ // src/userResponse/llm-result-truncator.ts
1520
+ var DEFAULT_MAX_ROWS = 10;
1521
+ var DEFAULT_MAX_CHARS_PER_FIELD = 500;
1522
+ function inferFieldType(value) {
1523
+ if (value === null || value === void 0) {
1524
+ return "null";
1525
+ }
1526
+ if (typeof value === "string") {
1527
+ if (isDateString(value)) {
1528
+ return "date";
1529
+ }
1530
+ return "string";
1531
+ }
1532
+ if (typeof value === "number") {
1533
+ return "number";
1534
+ }
1535
+ if (typeof value === "boolean") {
1536
+ return "boolean";
1537
+ }
1538
+ if (Array.isArray(value)) {
1539
+ return "array";
1540
+ }
1541
+ if (typeof value === "object") {
1542
+ return "object";
1543
+ }
1544
+ return "unknown";
1545
+ }
1546
+ function isDateString(value) {
1547
+ const datePatterns = [
1548
+ /^\d{4}-\d{2}-\d{2}$/,
1549
+ // YYYY-MM-DD
1550
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
1551
+ // ISO 8601
1552
+ /^\d{2}\/\d{2}\/\d{4}$/,
1553
+ // MM/DD/YYYY
1554
+ /^\d{4}\/\d{2}\/\d{2}$/
1555
+ // YYYY/MM/DD
1556
+ ];
1557
+ return datePatterns.some((pattern) => pattern.test(value));
1558
+ }
1559
+ function truncateTextField(value, maxLength) {
1560
+ if (value.length <= maxLength) {
1561
+ return { text: value, wasTruncated: false };
1562
+ }
1563
+ const truncated = value.substring(0, maxLength);
1564
+ const remaining = value.length - maxLength;
1565
+ return {
1566
+ text: `${truncated}... (${remaining} more chars)`,
1567
+ wasTruncated: true
1568
+ };
1569
+ }
1570
+ function truncateFieldValue(value, maxCharsPerField) {
1571
+ if (value === null || value === void 0) {
1572
+ return { value, wasTruncated: false };
1573
+ }
1574
+ if (typeof value === "number" || typeof value === "boolean") {
1575
+ return { value, wasTruncated: false };
1576
+ }
1577
+ if (typeof value === "string") {
1578
+ const result2 = truncateTextField(value, maxCharsPerField);
1579
+ return { value: result2.text, wasTruncated: result2.wasTruncated };
1580
+ }
1581
+ if (Array.isArray(value)) {
1582
+ if (value.length === 0) {
1583
+ return { value: [], wasTruncated: false };
1584
+ }
1585
+ const preview = value.slice(0, 3);
1586
+ const hasMore = value.length > 3;
1587
+ return {
1588
+ value: hasMore ? `[${preview.join(", ")}... (${value.length} items)]` : value,
1589
+ wasTruncated: hasMore
1590
+ };
1591
+ }
1592
+ if (typeof value === "object") {
1593
+ const jsonStr = JSON.stringify(value);
1594
+ const result2 = truncateTextField(jsonStr, maxCharsPerField);
1595
+ return { value: result2.text, wasTruncated: result2.wasTruncated };
1596
+ }
1597
+ const strValue = String(value);
1598
+ const result = truncateTextField(strValue, maxCharsPerField);
1599
+ return { value: result.text, wasTruncated: result.wasTruncated };
1600
+ }
1601
+ function truncateRow(row, maxCharsPerField) {
1602
+ const truncatedRow = {};
1603
+ const truncatedFields = /* @__PURE__ */ new Set();
1604
+ for (const [key, value] of Object.entries(row)) {
1605
+ const result = truncateFieldValue(value, maxCharsPerField);
1606
+ truncatedRow[key] = result.value;
1607
+ if (result.wasTruncated) {
1608
+ truncatedFields.add(key);
1609
+ }
1610
+ }
1611
+ return { row: truncatedRow, truncatedFields };
1612
+ }
1613
+ function extractMetadataFromObject(obj, dataKey, maxCharsPerField) {
1614
+ const metadata = {};
1615
+ const truncatedFields = /* @__PURE__ */ new Set();
1616
+ for (const [key, value] of Object.entries(obj)) {
1617
+ if (key === dataKey) {
1618
+ continue;
1619
+ }
1620
+ if (Array.isArray(value)) {
1621
+ continue;
1622
+ }
1623
+ const result = truncateFieldValue(value, maxCharsPerField);
1624
+ metadata[key] = result.value;
1625
+ if (result.wasTruncated) {
1626
+ truncatedFields.add(key);
1627
+ }
1628
+ }
1629
+ return { metadata, truncatedFields };
1630
+ }
1631
+ function extractSchema(data, truncatedFields = /* @__PURE__ */ new Set()) {
1632
+ if (!data || data.length === 0) {
1633
+ return [];
1634
+ }
1635
+ const firstRow = data[0];
1636
+ const schema2 = [];
1637
+ for (const [name, value] of Object.entries(firstRow)) {
1638
+ schema2.push({
1639
+ name,
1640
+ type: inferFieldType(value),
1641
+ truncated: truncatedFields.has(name) ? true : void 0
1642
+ });
1643
+ }
1644
+ return schema2;
1645
+ }
1646
+ function truncateDataArray(data, options = {}) {
1647
+ const maxRows = options.maxRows ?? DEFAULT_MAX_ROWS;
1648
+ const maxCharsPerField = options.maxCharsPerField ?? DEFAULT_MAX_CHARS_PER_FIELD;
1649
+ if (!data || !Array.isArray(data)) {
1650
+ return {
1651
+ data: [],
1652
+ totalRecords: 0,
1653
+ recordsShown: 0,
1654
+ truncatedFields: /* @__PURE__ */ new Set()
1655
+ };
1656
+ }
1657
+ const totalRecords = data.length;
1658
+ const rowsToProcess = data.slice(0, maxRows);
1659
+ const truncatedData = [];
1660
+ const allTruncatedFields = /* @__PURE__ */ new Set();
1661
+ for (const row of rowsToProcess) {
1662
+ const { row: truncatedRow, truncatedFields } = truncateRow(row, maxCharsPerField);
1663
+ truncatedData.push(truncatedRow);
1664
+ for (const field of truncatedFields) {
1665
+ allTruncatedFields.add(field);
1666
+ }
1667
+ }
1668
+ return {
1669
+ data: truncatedData,
1670
+ totalRecords,
1671
+ recordsShown: truncatedData.length,
1672
+ truncatedFields: allTruncatedFields
1673
+ };
1674
+ }
1675
+ function buildTruncationNote(totalRecords, recordsShown, truncatedFields, maxCharsPerField, sourceName) {
1676
+ const parts = [];
1677
+ if (totalRecords > recordsShown) {
1678
+ const source = sourceName ? ` from ${sourceName}` : "";
1679
+ parts.push(`Showing ${recordsShown} of ${totalRecords} total records${source}`);
1680
+ }
1681
+ if (truncatedFields.size > 0) {
1682
+ const fieldList = Array.from(truncatedFields).join(", ");
1683
+ parts.push(`Fields truncated to ${maxCharsPerField} chars: ${fieldList}`);
1684
+ }
1685
+ return parts.length > 0 ? parts.join(". ") + "." : null;
1686
+ }
1687
+ function formatQueryResultForLLM(data, options = {}) {
1688
+ const maxCharsPerField = options.maxCharsPerField ?? DEFAULT_MAX_CHARS_PER_FIELD;
1689
+ if (!Array.isArray(data)) {
1690
+ if (data !== null && data !== void 0) {
1691
+ return {
1692
+ summary: {
1693
+ totalRecords: 1,
1694
+ recordsShown: 1,
1695
+ schema: [{ name: "result", type: inferFieldType(data) }]
1696
+ },
1697
+ data: [{ result: data }],
1698
+ truncationNote: null
1699
+ };
1700
+ }
1701
+ return {
1702
+ summary: {
1703
+ totalRecords: 0,
1704
+ recordsShown: 0,
1705
+ schema: []
1706
+ },
1707
+ data: [],
1708
+ truncationNote: null
1709
+ };
1710
+ }
1711
+ const {
1712
+ data: truncatedData,
1713
+ totalRecords,
1714
+ recordsShown,
1715
+ truncatedFields
1716
+ } = truncateDataArray(data, options);
1717
+ const schema2 = extractSchema(truncatedData, truncatedFields);
1718
+ const truncationNote = buildTruncationNote(
1719
+ totalRecords,
1720
+ recordsShown,
1721
+ truncatedFields,
1722
+ maxCharsPerField,
1723
+ "query"
1724
+ );
1725
+ return {
1726
+ summary: {
1727
+ totalRecords,
1728
+ recordsShown,
1729
+ schema: schema2
1730
+ },
1731
+ data: truncatedData,
1732
+ truncationNote
1733
+ };
1734
+ }
1735
+ function formatToolResultForLLM(result, options = {}) {
1736
+ const { toolName, toolLimit } = options;
1737
+ const effectiveMaxRows = toolLimit ?? options.maxRows ?? DEFAULT_MAX_ROWS;
1738
+ const maxCharsPerField = options.maxCharsPerField ?? DEFAULT_MAX_CHARS_PER_FIELD;
1739
+ if (result === null || result === void 0) {
1740
+ return {
1741
+ toolName,
1742
+ summary: {
1743
+ totalRecords: 0,
1744
+ recordsShown: 0,
1745
+ schema: []
1746
+ },
1747
+ data: [],
1748
+ truncationNote: null
1749
+ };
1750
+ }
1751
+ if (typeof result === "string") {
1752
+ const { text, wasTruncated } = truncateTextField(result, maxCharsPerField);
1753
+ return {
1754
+ toolName,
1755
+ summary: {
1756
+ totalRecords: 1,
1757
+ recordsShown: 1,
1758
+ schema: [{ name: "result", type: "string", truncated: wasTruncated || void 0 }]
1759
+ },
1760
+ data: [{ result: text }],
1761
+ truncationNote: wasTruncated ? `Result truncated to ${maxCharsPerField} chars.` : null
1762
+ };
1763
+ }
1764
+ if (Array.isArray(result)) {
1765
+ const {
1766
+ data: truncatedData,
1767
+ totalRecords,
1768
+ recordsShown,
1769
+ truncatedFields
1770
+ } = truncateDataArray(result, {
1771
+ maxRows: effectiveMaxRows,
1772
+ maxCharsPerField
1773
+ });
1774
+ const schema2 = extractSchema(truncatedData, truncatedFields);
1775
+ const truncationNote = buildTruncationNote(
1776
+ totalRecords,
1777
+ recordsShown,
1778
+ truncatedFields,
1779
+ maxCharsPerField,
1780
+ toolName
1781
+ );
1782
+ return {
1783
+ toolName,
1784
+ summary: {
1785
+ totalRecords,
1786
+ recordsShown,
1787
+ schema: schema2
1788
+ },
1789
+ data: truncatedData,
1790
+ truncationNote
1791
+ };
1792
+ }
1793
+ if (typeof result === "object") {
1794
+ const objResult = result;
1795
+ const dataWrapperKeys = ["data", "results", "items", "records", "rows", "list"];
1796
+ for (const key of dataWrapperKeys) {
1797
+ if (Array.isArray(objResult[key])) {
1798
+ const innerData = objResult[key];
1799
+ const {
1800
+ data: truncatedData,
1801
+ totalRecords,
1802
+ recordsShown,
1803
+ truncatedFields: dataTruncatedFields
1804
+ } = truncateDataArray(innerData, {
1805
+ maxRows: effectiveMaxRows,
1806
+ maxCharsPerField
1807
+ });
1808
+ const {
1809
+ metadata,
1810
+ truncatedFields: metadataTruncatedFields
1811
+ } = extractMetadataFromObject(objResult, key, maxCharsPerField);
1812
+ const allTruncatedFields = /* @__PURE__ */ new Set([...dataTruncatedFields, ...metadataTruncatedFields]);
1813
+ const schema3 = extractSchema(truncatedData, dataTruncatedFields);
1814
+ const truncationNote2 = buildTruncationNote(
1815
+ totalRecords,
1816
+ recordsShown,
1817
+ allTruncatedFields,
1818
+ maxCharsPerField,
1819
+ toolName
1820
+ );
1821
+ const hasMetadata = Object.keys(metadata).length > 0;
1822
+ return {
1823
+ toolName,
1824
+ summary: {
1825
+ totalRecords,
1826
+ recordsShown,
1827
+ schema: schema3
1828
+ },
1829
+ ...hasMetadata && { metadata },
1830
+ data: truncatedData,
1831
+ truncationNote: truncationNote2
1832
+ };
1833
+ }
1834
+ }
1835
+ const { row: truncatedRow, truncatedFields } = truncateRow(objResult, maxCharsPerField);
1836
+ const schema2 = extractSchema([truncatedRow], truncatedFields);
1837
+ const truncationNote = truncatedFields.size > 0 ? `Fields truncated to ${maxCharsPerField} chars: ${Array.from(truncatedFields).join(", ")}.` : null;
1838
+ return {
1839
+ toolName,
1840
+ summary: {
1841
+ totalRecords: 1,
1842
+ recordsShown: 1,
1843
+ schema: schema2
1844
+ },
1845
+ data: [truncatedRow],
1846
+ truncationNote
1847
+ };
1848
+ }
1849
+ return {
1850
+ toolName,
1851
+ summary: {
1852
+ totalRecords: 1,
1853
+ recordsShown: 1,
1854
+ schema: [{ name: "result", type: inferFieldType(result) }]
1855
+ },
1856
+ data: [{ result }],
1857
+ truncationNote: null
1858
+ };
1859
+ }
1860
+ function formatResultAsString(formattedResult) {
1861
+ return JSON.stringify(formattedResult, null, 2);
1862
+ }
1863
+
1519
1864
  // src/handlers/data-request.ts
1520
1865
  function getQueryCacheKey(query) {
1521
1866
  if (typeof query === "string") {
@@ -1597,15 +1942,25 @@ async function handleDataRequest(data, collections, sendMessage) {
1597
1942
  }
1598
1943
  }
1599
1944
  if (uiBlock) {
1945
+ const formattedResult = formatToolResultForLLM(result, {
1946
+ maxRows: 3,
1947
+ // Only need a few sample rows for summary
1948
+ maxCharsPerField: 100
1949
+ // Short truncation for summary
1950
+ });
1600
1951
  const dataSummary = {
1601
1952
  _dataReceived: true,
1602
1953
  _timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1603
1954
  _collection: collection,
1604
1955
  _operation: op,
1605
- _recordCount: Array.isArray(result) ? result.length : result?.data?.length || result?.contacts?.length || result?.salesorders?.length || "unknown"
1956
+ _totalRecords: formattedResult.summary.totalRecords,
1957
+ _recordsShown: formattedResult.summary.recordsShown,
1958
+ _metadata: formattedResult.metadata,
1959
+ // Preserve totalItems, totalDeadstockItems, etc.
1960
+ _schema: formattedResult.summary.schema
1606
1961
  };
1607
1962
  uiBlock.setComponentData(dataSummary);
1608
- logger.info(`Updated UIBlock ${uiBlockId} with data summary from ${collection}.${op} (full data not stored to save memory)`);
1963
+ logger.info(`Updated UIBlock ${uiBlockId} with data summary from ${collection}.${op} (${formattedResult.summary.totalRecords} total records)`);
1609
1964
  } else {
1610
1965
  logger.warn(`UIBlock ${uiBlockId} not found in threads`);
1611
1966
  }
@@ -6022,97 +6377,335 @@ var ConversationSearch = {
6022
6377
  };
6023
6378
  var conversation_search_default = ConversationSearch;
6024
6379
 
6025
- // src/userResponse/base-llm.ts
6026
- var BaseLLM = class {
6027
- constructor(config) {
6028
- this.model = config?.model || this.getDefaultModel();
6029
- this.fastModel = config?.fastModel || this.getDefaultFastModel();
6030
- this.defaultLimit = config?.defaultLimit || 10;
6031
- this.apiKey = config?.apiKey;
6032
- this.modelStrategy = config?.modelStrategy || "fast";
6033
- this.conversationSimilarityThreshold = config?.conversationSimilarityThreshold || 0.8;
6380
+ // src/userResponse/prompt-extractor.ts
6381
+ function extractPromptText(content) {
6382
+ if (content === null || content === void 0) {
6383
+ return "";
6034
6384
  }
6035
- /**
6036
- * Get the appropriate model based on task type and model strategy
6037
- * @param taskType - 'complex' for text generation/matching, 'simple' for classification/actions
6038
- * @returns The model string to use for this task
6039
- */
6040
- getModelForTask(taskType) {
6041
- switch (this.modelStrategy) {
6042
- case "best":
6043
- return this.model;
6044
- case "fast":
6045
- return this.fastModel;
6046
- case "balanced":
6047
- default:
6048
- return taskType === "complex" ? this.model : this.fastModel;
6385
+ if (typeof content === "string") {
6386
+ return content;
6387
+ }
6388
+ if (Array.isArray(content)) {
6389
+ return content.map((item) => extractContentBlockText(item)).filter((text) => text.length > 0).join("\n\n---\n\n");
6390
+ }
6391
+ if (content && typeof content === "object") {
6392
+ return extractObjectText(content);
6393
+ }
6394
+ return String(content);
6395
+ }
6396
+ function extractContentBlockText(item) {
6397
+ if (typeof item === "string") {
6398
+ return item;
6399
+ }
6400
+ if (item && typeof item === "object") {
6401
+ const obj = item;
6402
+ if (typeof obj.text === "string") {
6403
+ return obj.text;
6049
6404
  }
6405
+ if (typeof obj.content === "string") {
6406
+ return obj.content;
6407
+ }
6408
+ return JSON.stringify(item, null, 2);
6409
+ }
6410
+ return String(item);
6411
+ }
6412
+ function extractObjectText(obj) {
6413
+ if (typeof obj.text === "string") {
6414
+ return obj.text;
6415
+ }
6416
+ if (typeof obj.content === "string") {
6417
+ return obj.content;
6418
+ }
6419
+ return JSON.stringify(obj, null, 2);
6420
+ }
6421
+
6422
+ // src/userResponse/constants.ts
6423
+ var MAX_QUERY_VALIDATION_RETRIES = 3;
6424
+ var MAX_QUERY_ATTEMPTS = 6;
6425
+ var MAX_TOOL_ATTEMPTS = 3;
6426
+ var STREAM_FLUSH_INTERVAL_MS = 50;
6427
+ var PROGRESS_HEARTBEAT_INTERVAL_MS = 800;
6428
+ var STREAM_DELAY_MS = 50;
6429
+ var STREAM_IMMEDIATE_FLUSH_THRESHOLD = 100;
6430
+ var MAX_TOKENS_QUERY_FIX = 2048;
6431
+ var MAX_TOKENS_COMPONENT_MATCHING = 8192;
6432
+ var MAX_TOKENS_CLASSIFICATION = 1500;
6433
+ var MAX_TOKENS_ADAPTATION = 8192;
6434
+ var MAX_TOKENS_TEXT_RESPONSE = 4e3;
6435
+ var MAX_TOKENS_NEXT_QUESTIONS = 1200;
6436
+ var DEFAULT_MAX_ROWS_FOR_LLM = 10;
6437
+ var DEFAULT_MAX_CHARS_PER_FIELD2 = 500;
6438
+ var STREAM_PREVIEW_MAX_ROWS = 10;
6439
+ var STREAM_PREVIEW_MAX_CHARS = 200;
6440
+ var TOOL_TRACKING_MAX_ROWS = 5;
6441
+ var TOOL_TRACKING_MAX_CHARS = 200;
6442
+ var TOOL_TRACKING_SAMPLE_ROWS = 3;
6443
+ var MAX_COMPONENT_QUERY_LIMIT = 10;
6444
+ var EXACT_MATCH_SIMILARITY_THRESHOLD = 0.99;
6445
+ var DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD = 0.8;
6446
+ var MAX_TOOL_CALLING_ITERATIONS = 20;
6447
+ var KNOWLEDGE_BASE_TOP_K = 3;
6448
+
6449
+ // src/userResponse/stream-buffer.ts
6450
+ var StreamBuffer = class {
6451
+ constructor(callback) {
6452
+ this.buffer = "";
6453
+ this.flushTimer = null;
6454
+ this.fullText = "";
6455
+ this.callback = callback;
6050
6456
  }
6051
6457
  /**
6052
- * Set the model strategy at runtime
6053
- * @param strategy - 'best', 'fast', or 'balanced'
6458
+ * Check if the buffer has a callback configured
6054
6459
  */
6055
- setModelStrategy(strategy) {
6056
- this.modelStrategy = strategy;
6057
- logger.info(`[${this.getProviderName()}] Model strategy set to: ${strategy}`);
6460
+ hasCallback() {
6461
+ return !!this.callback;
6058
6462
  }
6059
6463
  /**
6060
- * Get the current model strategy
6061
- * @returns The current model strategy
6464
+ * Get all text that has been written (including already flushed)
6062
6465
  */
6063
- getModelStrategy() {
6064
- return this.modelStrategy;
6466
+ getFullText() {
6467
+ return this.fullText;
6065
6468
  }
6066
6469
  /**
6067
- * Set the conversation similarity threshold at runtime
6068
- * @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
6470
+ * Write a chunk to the buffer
6471
+ * Large chunks or chunks with newlines are flushed immediately
6472
+ * Small chunks are batched and flushed after a short interval
6473
+ *
6474
+ * @param chunk - Text chunk to write
6069
6475
  */
6070
- setConversationSimilarityThreshold(threshold) {
6071
- if (threshold < 0 || threshold > 1) {
6072
- logger.warn(`[${this.getProviderName()}] Invalid threshold ${threshold}, must be between 0 and 1. Using default 0.8`);
6073
- this.conversationSimilarityThreshold = 0.8;
6476
+ write(chunk) {
6477
+ this.fullText += chunk;
6478
+ if (!this.callback) {
6074
6479
  return;
6075
6480
  }
6076
- this.conversationSimilarityThreshold = threshold;
6077
- logger.info(`[${this.getProviderName()}] Conversation similarity threshold set to: ${threshold}`);
6481
+ this.buffer += chunk;
6482
+ if (chunk.includes("\n") || chunk.length > STREAM_IMMEDIATE_FLUSH_THRESHOLD) {
6483
+ this.flushNow();
6484
+ } else if (!this.flushTimer) {
6485
+ this.flushTimer = setTimeout(() => this.flushNow(), STREAM_FLUSH_INTERVAL_MS);
6486
+ }
6078
6487
  }
6079
6488
  /**
6080
- * Get the current conversation similarity threshold
6081
- * @returns The current threshold value
6489
+ * Flush the buffer immediately
6490
+ * Call this before tool execution or other operations that need clean output
6082
6491
  */
6083
- getConversationSimilarityThreshold() {
6084
- return this.conversationSimilarityThreshold;
6492
+ flush() {
6493
+ this.flushNow();
6085
6494
  }
6086
6495
  /**
6087
- * Get the API key (from instance, parameter, or environment)
6496
+ * Internal flush implementation
6088
6497
  */
6089
- getApiKey(apiKey) {
6090
- return apiKey || this.apiKey || this.getDefaultApiKey();
6498
+ flushNow() {
6499
+ if (this.flushTimer) {
6500
+ clearTimeout(this.flushTimer);
6501
+ this.flushTimer = null;
6502
+ }
6503
+ if (this.buffer && this.callback) {
6504
+ this.callback(this.buffer);
6505
+ this.buffer = "";
6506
+ }
6091
6507
  }
6092
6508
  /**
6093
- * Check if a component contains a Form (data_modification component)
6094
- * Forms have hardcoded defaultValues that become stale when cached
6095
- * This checks both single Form components and Forms inside MultiComponentContainer
6509
+ * Clean up resources
6510
+ * Call this when done with the buffer
6096
6511
  */
6097
- containsFormComponent(component) {
6098
- if (!component) return false;
6099
- if (component.type === "Form" || component.name === "DynamicForm") {
6100
- return true;
6512
+ dispose() {
6513
+ this.flush();
6514
+ this.callback = void 0;
6515
+ }
6516
+ };
6517
+ function streamDelay(ms = STREAM_DELAY_MS) {
6518
+ return new Promise((resolve) => setTimeout(resolve, ms));
6519
+ }
6520
+ async function withProgressHeartbeat(operation, progressMessage, streamBuffer, intervalMs = PROGRESS_HEARTBEAT_INTERVAL_MS) {
6521
+ if (!streamBuffer.hasCallback()) {
6522
+ return operation();
6523
+ }
6524
+ const startTime = Date.now();
6525
+ await streamDelay(30);
6526
+ streamBuffer.write(`\u23F3 ${progressMessage}`);
6527
+ const heartbeatInterval = setInterval(() => {
6528
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1e3);
6529
+ if (elapsedSeconds >= 1) {
6530
+ streamBuffer.write(` (${elapsedSeconds}s)`);
6531
+ }
6532
+ }, intervalMs);
6533
+ try {
6534
+ const result = await operation();
6535
+ return result;
6536
+ } finally {
6537
+ clearInterval(heartbeatInterval);
6538
+ streamBuffer.write("\n\n");
6539
+ }
6540
+ }
6541
+
6542
+ // src/userResponse/utils/component-props-processor.ts
6543
+ var NUMERIC_CONFIG_KEYS = ["yAxisKey", "valueKey", "aggregationField", "sizeKey"];
6544
+ var STRING_CONFIG_KEYS = ["xAxisKey", "nameKey", "labelKey", "groupBy"];
6545
+ var CONFIG_FIELDS_TO_VALIDATE = [
6546
+ "xAxisKey",
6547
+ "yAxisKey",
6548
+ "valueKey",
6549
+ "nameKey",
6550
+ "labelKey",
6551
+ "groupBy",
6552
+ "aggregationField",
6553
+ "seriesKey",
6554
+ "sizeKey",
6555
+ "xAggregationField",
6556
+ "yAggregationField"
6557
+ ];
6558
+ function findMatchingField(fieldName, configKey, validFieldNames, fieldTypes, providerName) {
6559
+ if (!fieldName) return null;
6560
+ const lowerField = fieldName.toLowerCase();
6561
+ const validFieldNamesLower = validFieldNames.map((n) => n.toLowerCase());
6562
+ const exactIdx = validFieldNamesLower.indexOf(lowerField);
6563
+ if (exactIdx !== -1) return validFieldNames[exactIdx];
6564
+ const containsMatches = validFieldNames.filter(
6565
+ (_, i) => validFieldNamesLower[i].includes(lowerField) || lowerField.includes(validFieldNamesLower[i])
6566
+ );
6567
+ if (containsMatches.length === 1) return containsMatches[0];
6568
+ if (NUMERIC_CONFIG_KEYS.includes(configKey)) {
6569
+ const numericFields = validFieldNames.filter((f) => fieldTypes[f] === "number");
6570
+ const match = numericFields.find(
6571
+ (f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase())
6572
+ );
6573
+ if (match) return match;
6574
+ if (numericFields.length > 0) {
6575
+ logger.warn(`[${providerName}] No match for "${fieldName}", using first numeric field: ${numericFields[0]}`);
6576
+ return numericFields[0];
6101
6577
  }
6102
- if (component.type === "Container" || component.name === "MultiComponentContainer") {
6103
- const nestedComponents = component.props?.config?.components || [];
6104
- for (const nested of nestedComponents) {
6105
- if (nested.type === "Form" || nested.name === "DynamicForm") {
6106
- return true;
6578
+ }
6579
+ if (STRING_CONFIG_KEYS.includes(configKey)) {
6580
+ const stringFields = validFieldNames.filter((f) => fieldTypes[f] === "string");
6581
+ const match = stringFields.find(
6582
+ (f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase())
6583
+ );
6584
+ if (match) return match;
6585
+ if (stringFields.length > 0) {
6586
+ logger.warn(`[${providerName}] No match for "${fieldName}", using first string field: ${stringFields[0]}`);
6587
+ return stringFields[0];
6588
+ }
6589
+ }
6590
+ logger.warn(`[${providerName}] No match for "${fieldName}", using first field: ${validFieldNames[0]}`);
6591
+ return validFieldNames[0];
6592
+ }
6593
+ function validateConfigFieldNames(config, outputSchema, providerName) {
6594
+ if (!outputSchema?.fields || !config) return config;
6595
+ const validFieldNames = outputSchema.fields.map((f) => f.name);
6596
+ const fieldTypes = outputSchema.fields.reduce((acc, f) => {
6597
+ acc[f.name] = f.type;
6598
+ return acc;
6599
+ }, {});
6600
+ const correctedConfig = { ...config };
6601
+ for (const configKey of CONFIG_FIELDS_TO_VALIDATE) {
6602
+ const fieldValue = correctedConfig[configKey];
6603
+ if (fieldValue && typeof fieldValue === "string") {
6604
+ if (!validFieldNames.includes(fieldValue)) {
6605
+ const correctedField = findMatchingField(fieldValue, configKey, validFieldNames, fieldTypes, providerName);
6606
+ if (correctedField) {
6607
+ logger.warn(`[${providerName}] Correcting config.${configKey}: "${fieldValue}" \u2192 "${correctedField}"`);
6608
+ correctedConfig[configKey] = correctedField;
6107
6609
  }
6108
6610
  }
6109
6611
  }
6110
- return false;
6612
+ }
6613
+ if (Array.isArray(correctedConfig.series)) {
6614
+ correctedConfig.series = correctedConfig.series.map((s) => {
6615
+ if (s.dataKey && typeof s.dataKey === "string" && !validFieldNames.includes(s.dataKey)) {
6616
+ const correctedField = findMatchingField(s.dataKey, "yAxisKey", validFieldNames, fieldTypes, providerName);
6617
+ if (correctedField) {
6618
+ logger.warn(`[${providerName}] Correcting series.dataKey: "${s.dataKey}" \u2192 "${correctedField}"`);
6619
+ return { ...s, dataKey: correctedField };
6620
+ }
6621
+ }
6622
+ return s;
6623
+ });
6624
+ }
6625
+ return correctedConfig;
6626
+ }
6627
+ function validateExternalTool(externalTool, executedTools, providerName) {
6628
+ if (!externalTool) {
6629
+ return { valid: true };
6630
+ }
6631
+ const toolId = externalTool.toolId;
6632
+ const validToolIds = (executedTools || []).map((t) => t.id);
6633
+ const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
6634
+ if (!isValidTool) {
6635
+ logger.warn(`[${providerName}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
6636
+ return { valid: false };
6637
+ }
6638
+ const executedTool = executedTools?.find((t) => t.id === toolId);
6639
+ return { valid: true, executedTool };
6640
+ }
6641
+ function validateAndCleanQuery(query, config) {
6642
+ if (!query) {
6643
+ return { query: null, wasModified: false };
6644
+ }
6645
+ let wasModified = false;
6646
+ let cleanedQuery = query;
6647
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
6648
+ if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
6649
+ logger.warn(`[${config.providerName}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
6650
+ return { query: null, wasModified: true };
6651
+ }
6652
+ const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
6653
+ if (fixed) {
6654
+ logger.warn(`[${config.providerName}] SQL fixes applied to component query: ${fixes.join("; ")}`);
6655
+ wasModified = true;
6656
+ if (typeof cleanedQuery === "string") {
6657
+ cleanedQuery = fixedQuery;
6658
+ } else if (cleanedQuery?.sql) {
6659
+ cleanedQuery = { ...cleanedQuery, sql: fixedQuery };
6660
+ }
6661
+ }
6662
+ if (typeof cleanedQuery === "string") {
6663
+ const limitedQuery = ensureQueryLimit(cleanedQuery, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6664
+ if (limitedQuery !== cleanedQuery) wasModified = true;
6665
+ cleanedQuery = limitedQuery;
6666
+ } else if (cleanedQuery?.sql) {
6667
+ const limitedSql = ensureQueryLimit(cleanedQuery.sql, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6668
+ if (limitedSql !== cleanedQuery.sql) wasModified = true;
6669
+ cleanedQuery = { ...cleanedQuery, sql: limitedSql };
6670
+ }
6671
+ return { query: cleanedQuery, wasModified };
6672
+ }
6673
+ function processComponentProps(props, executedTools, config) {
6674
+ let cleanedProps = { ...props };
6675
+ if (cleanedProps.externalTool) {
6676
+ const { valid, executedTool } = validateExternalTool(
6677
+ cleanedProps.externalTool,
6678
+ executedTools,
6679
+ config.providerName
6680
+ );
6681
+ if (!valid) {
6682
+ cleanedProps.externalTool = null;
6683
+ } else if (executedTool?.outputSchema?.fields && cleanedProps.config) {
6684
+ cleanedProps.config = validateConfigFieldNames(
6685
+ cleanedProps.config,
6686
+ executedTool.outputSchema,
6687
+ config.providerName
6688
+ );
6689
+ }
6690
+ }
6691
+ if (cleanedProps.query) {
6692
+ const { query } = validateAndCleanQuery(cleanedProps.query, config);
6693
+ cleanedProps.query = query;
6694
+ }
6695
+ if (cleanedProps.query && cleanedProps.externalTool) {
6696
+ logger.info(`[${config.providerName}] Both query and externalTool exist, keeping both - frontend will decide`);
6697
+ }
6698
+ return cleanedProps;
6699
+ }
6700
+
6701
+ // src/userResponse/services/query-execution-service.ts
6702
+ var QueryExecutionService = class {
6703
+ constructor(config) {
6704
+ this.config = config;
6111
6705
  }
6112
6706
  /**
6113
- * Get the cache key for a query (the exact sql param that would be sent to execute)
6707
+ * Get the cache key for a query
6114
6708
  * This ensures the cache key matches what the frontend will send
6115
- * Used for both caching and internal deduplication
6116
6709
  */
6117
6710
  getQueryCacheKey(query) {
6118
6711
  if (typeof query === "string") {
@@ -6128,17 +6721,19 @@ var BaseLLM = class {
6128
6721
  return "";
6129
6722
  }
6130
6723
  /**
6131
- * Execute a query against the database for validation and caching
6724
+ * Execute a query against the database
6132
6725
  * @param query - The SQL query to execute (string or object with sql/values)
6133
6726
  * @param collections - Collections object containing database execute function
6134
6727
  * @returns Object with result data and cache key
6135
- * @throws Error if query execution fails
6136
6728
  */
6137
- async executeQueryForValidation(query, collections) {
6729
+ async executeQuery(query, collections) {
6138
6730
  const cacheKey = this.getQueryCacheKey(query);
6139
6731
  if (!cacheKey) {
6140
6732
  throw new Error("Invalid query format: expected string or object with sql property");
6141
6733
  }
6734
+ if (!collections?.["database"]?.["execute"]) {
6735
+ throw new Error("Database collection not registered. Please register database.execute collection to execute queries.");
6736
+ }
6142
6737
  const result = await collections["database"]["execute"]({ sql: cacheKey });
6143
6738
  return { result, cacheKey };
6144
6739
  }
@@ -6146,7 +6741,7 @@ var BaseLLM = class {
6146
6741
  * Request the LLM to fix a failed SQL query
6147
6742
  * @param failedQuery - The query that failed execution
6148
6743
  * @param errorMessage - The error message from the failed execution
6149
- * @param componentContext - Context about the component (name, type, title)
6744
+ * @param componentContext - Context about the component
6150
6745
  * @param apiKey - Optional API key
6151
6746
  * @returns Fixed query string
6152
6747
  */
@@ -6187,10 +6782,10 @@ Fixed SQL query:`;
6187
6782
  user: prompt
6188
6783
  },
6189
6784
  {
6190
- model: this.getModelForTask("simple"),
6191
- maxTokens: 2048,
6785
+ model: this.config.getModelForTask("simple"),
6786
+ maxTokens: MAX_TOKENS_QUERY_FIX,
6192
6787
  temperature: 0,
6193
- apiKey: this.getApiKey(apiKey)
6788
+ apiKey: this.config.getApiKey(apiKey)
6194
6789
  }
6195
6790
  );
6196
6791
  let fixedQuery = response.trim();
@@ -6200,81 +6795,656 @@ Fixed SQL query:`;
6200
6795
  return validatedQuery;
6201
6796
  }
6202
6797
  /**
6203
- * Match components from text response suggestions and generate follow-up questions
6204
- * Takes a text response with component suggestions (c1:type format) and matches with available components
6205
- * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
6206
- * All components are placed in a default MultiComponentContainer layout
6207
- * @param analysisContent - The text response containing component suggestions
6208
- * @param components - List of available components
6209
- * @param apiKey - Optional API key
6210
- * @param logCollector - Optional log collector
6211
- * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
6212
- * @returns Object containing matched components, layout title/description, and follow-up actions
6798
+ * Validate a single component's query with retry logic
6799
+ * @param component - The component to validate
6800
+ * @param collections - Collections object containing database execute function
6801
+ * @param apiKey - Optional API key for LLM calls
6802
+ * @param logCollector - Optional log collector for logging
6803
+ * @returns Validation result with component, query key, and result
6213
6804
  */
6214
- async matchComponentsFromAnalysis(analysisContent, components, userPrompt, apiKey, logCollector, componentStreamCallback, deferredTools, executedTools, collections, userId) {
6215
- const methodStartTime = Date.now();
6216
- const methodName = "matchComponentsFromAnalysis";
6217
- logger.info(`[${this.getProviderName()}] [TIMING] START ${methodName} | model: ${this.getModelForTask("complex")}`);
6218
- try {
6219
- logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
6220
- let availableComponentsText = "No components available";
6221
- if (components && components.length > 0) {
6222
- availableComponentsText = components.map((comp, idx) => {
6223
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
6224
- const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
6225
- return `${idx + 1}. ID: ${comp.id}
6226
- Name: ${comp.name}
6227
- Type: ${comp.type}
6228
- Description: ${comp.description || "No description"}
6229
- Keywords: ${keywords}
6230
- Props Structure: ${propsPreview}`;
6231
- }).join("\n\n");
6232
- }
6233
- let deferredToolsText = "No deferred external tools for this request.";
6234
- if (deferredTools && deferredTools.length > 0) {
6235
- logger.info(`[${this.getProviderName()}] Passing ${deferredTools.length} deferred tools to component matching`);
6236
- deferredToolsText = "The following external tools need user input via a Form component.\n**IMPORTANT: Use these EXACT values when generating Form externalTool prop.**\n\n" + deferredTools.map((tool, idx) => {
6237
- return `${idx + 1}. **${tool.name}**
6238
- toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
6239
- toolName: "${tool.name}"
6240
- parameters: ${JSON.stringify(tool.params || {})}
6241
- requiredFields:
6242
- ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6243
- }).join("\n\n");
6244
- }
6245
- let executedToolsText = "No external tools were executed for data fetching.";
6246
- if (executedTools && executedTools.length > 0) {
6247
- logger.info(`[${this.getProviderName()}] Passing ${executedTools.length} executed tools to component matching`);
6248
- executedToolsText = "The following external tools were executed to fetch data.\n" + // '**IMPORTANT: For components displaying this data, use externalTool prop instead of query.**\n' +
6249
- // '**IMPORTANT: Use ONLY the field names listed in outputSchema for config keys.**\n\n' +
6250
- executedTools.map((tool, idx) => {
6251
- let outputSchemaText = "Not available";
6252
- let fieldNamesList = "";
6253
- let recordCount = "unknown";
6254
- if (tool.outputSchema) {
6255
- const fields = tool.outputSchema.fields || [];
6256
- recordCount = tool.result?._recordCount || (Array.isArray(tool.result) ? tool.result.length : "unknown");
6257
- const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
6258
- const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
6259
- fieldNamesList = `
6260
- \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
6261
- \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
6262
- const fieldsText = fields.map(
6263
- (f) => ` "${f.name}" (${f.type}): ${f.description}`
6264
- ).join("\n");
6265
- outputSchemaText = `${tool.outputSchema.description}
6266
- Fields:
6267
- ${fieldsText}`;
6268
- }
6269
- return `${idx + 1}. **${tool.name}**
6270
- toolId: "${tool.id}"
6271
- toolName: "${tool.name}"
6272
- parameters: ${JSON.stringify(tool.params || {})}
6273
- recordCount: ${recordCount} rows returned
6274
- outputSchema: ${outputSchemaText}${fieldNamesList}`;
6275
- }).join("\n\n");
6276
- }
6277
- const schemaDoc = schema.generateSchemaDocumentation();
6805
+ async validateSingleQuery(component, collections, apiKey, logCollector) {
6806
+ const query = component.props?.query;
6807
+ const originalQueryKey = this.getQueryCacheKey(query);
6808
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
6809
+ let finalQueryKey = originalQueryKey;
6810
+ let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
6811
+ let currentQueryStr = queryStr;
6812
+ let validated = false;
6813
+ let lastError = "";
6814
+ let result = null;
6815
+ let attempts = 0;
6816
+ logger.info(`[${this.config.providerName}] Validating query for component: ${component.name} (${component.type})`);
6817
+ while (attempts < MAX_QUERY_VALIDATION_RETRIES && !validated) {
6818
+ attempts++;
6819
+ try {
6820
+ logger.debug(`[${this.config.providerName}] Query validation attempt ${attempts}/${MAX_QUERY_VALIDATION_RETRIES} for ${component.name}`);
6821
+ const validationResult = await this.executeQuery(currentQuery, collections);
6822
+ result = validationResult.result;
6823
+ validated = true;
6824
+ queryCache.set(validationResult.cacheKey, result);
6825
+ logger.info(`[${this.config.providerName}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
6826
+ logCollector?.info(`\u2713 Query validated for ${component.name}`);
6827
+ if (currentQueryStr !== queryStr) {
6828
+ const fixedQuery = typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr };
6829
+ component.props = {
6830
+ ...component.props,
6831
+ query: fixedQuery
6832
+ };
6833
+ finalQueryKey = this.getQueryCacheKey(fixedQuery);
6834
+ logger.info(`[${this.config.providerName}] Updated ${component.name} with fixed query`);
6835
+ }
6836
+ } catch (error) {
6837
+ lastError = error instanceof Error ? error.message : String(error);
6838
+ logger.warn(`[${this.config.providerName}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_QUERY_VALIDATION_RETRIES}): ${lastError}`);
6839
+ logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
6840
+ if (attempts >= MAX_QUERY_VALIDATION_RETRIES) {
6841
+ logger.error(`[${this.config.providerName}] \u2717 Max retries reached for ${component.name}, excluding from response`);
6842
+ logCollector?.error(`Max retries reached for ${component.name}, component excluded from response`);
6843
+ break;
6844
+ }
6845
+ logger.info(`[${this.config.providerName}] Requesting query fix from LLM for ${component.name}...`);
6846
+ logCollector?.info(`Requesting query fix for ${component.name}...`);
6847
+ try {
6848
+ const fixedQueryStr = await this.requestQueryFix(
6849
+ currentQueryStr,
6850
+ lastError,
6851
+ {
6852
+ name: component.name,
6853
+ type: component.type,
6854
+ title: component.props?.title
6855
+ },
6856
+ apiKey
6857
+ );
6858
+ if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
6859
+ logger.info(`[${this.config.providerName}] Received fixed query for ${component.name}, retrying...`);
6860
+ const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6861
+ currentQueryStr = limitedFixedQuery;
6862
+ if (typeof currentQuery === "string") {
6863
+ currentQuery = limitedFixedQuery;
6864
+ } else {
6865
+ currentQuery = { ...currentQuery, sql: limitedFixedQuery };
6866
+ }
6867
+ } else {
6868
+ logger.warn(`[${this.config.providerName}] LLM returned same or empty query, stopping retries`);
6869
+ break;
6870
+ }
6871
+ } catch (fixError) {
6872
+ const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6873
+ logger.error(`[${this.config.providerName}] Failed to get query fix from LLM: ${fixErrorMsg}`);
6874
+ break;
6875
+ }
6876
+ }
6877
+ }
6878
+ if (!validated) {
6879
+ logger.warn(`[${this.config.providerName}] Component ${component.name} excluded from response due to failed query validation`);
6880
+ logCollector?.warn(`Component ${component.name} excluded from response`);
6881
+ }
6882
+ return {
6883
+ component: validated ? component : null,
6884
+ queryKey: finalQueryKey,
6885
+ result,
6886
+ validated
6887
+ };
6888
+ }
6889
+ /**
6890
+ * Validate multiple component queries in parallel
6891
+ * @param components - Array of components with potential queries
6892
+ * @param collections - Collections object containing database execute function
6893
+ * @param apiKey - Optional API key for LLM calls
6894
+ * @param logCollector - Optional log collector for logging
6895
+ * @returns Object with validated components and query results map
6896
+ */
6897
+ async validateComponentQueries(components, collections, apiKey, logCollector) {
6898
+ const queryResults = /* @__PURE__ */ new Map();
6899
+ const validatedComponents = [];
6900
+ const componentsWithoutQuery = [];
6901
+ const componentsWithQuery = [];
6902
+ for (const component of components) {
6903
+ if (!component.props?.query) {
6904
+ componentsWithoutQuery.push(component);
6905
+ } else {
6906
+ componentsWithQuery.push(component);
6907
+ }
6908
+ }
6909
+ validatedComponents.push(...componentsWithoutQuery);
6910
+ if (componentsWithQuery.length === 0) {
6911
+ return { components: validatedComponents, queryResults };
6912
+ }
6913
+ logger.info(`[${this.config.providerName}] Validating ${componentsWithQuery.length} component queries in parallel...`);
6914
+ logCollector?.info(`Validating ${componentsWithQuery.length} component queries in parallel...`);
6915
+ const validationPromises = componentsWithQuery.map(
6916
+ (component) => this.validateSingleQuery(component, collections, apiKey, logCollector)
6917
+ );
6918
+ const results = await Promise.allSettled(validationPromises);
6919
+ for (let i = 0; i < results.length; i++) {
6920
+ const result = results[i];
6921
+ const component = componentsWithQuery[i];
6922
+ if (result.status === "fulfilled") {
6923
+ const { component: validatedComponent, queryKey, result: queryResult, validated } = result.value;
6924
+ if (validated && validatedComponent) {
6925
+ validatedComponents.push(validatedComponent);
6926
+ if (queryResult) {
6927
+ queryResults.set(queryKey, queryResult);
6928
+ queryResults.set(`${component.id}:${queryKey}`, queryResult);
6929
+ }
6930
+ }
6931
+ } else {
6932
+ logger.error(`[${this.config.providerName}] Unexpected error validating ${component.name}: ${result.reason}`);
6933
+ logCollector?.error(`Unexpected error validating ${component.name}: ${result.reason}`);
6934
+ }
6935
+ }
6936
+ logger.info(`[${this.config.providerName}] Parallel validation complete: ${validatedComponents.length}/${components.length} components validated`);
6937
+ return {
6938
+ components: validatedComponents,
6939
+ queryResults
6940
+ };
6941
+ }
6942
+ };
6943
+
6944
+ // src/userResponse/services/tool-executor-service.ts
6945
+ var ToolExecutorService = class {
6946
+ constructor(config) {
6947
+ this.queryAttempts = /* @__PURE__ */ new Map();
6948
+ this.toolAttempts = /* @__PURE__ */ new Map();
6949
+ this.executedToolsList = [];
6950
+ this.maxAttemptsReached = false;
6951
+ this.config = config;
6952
+ }
6953
+ /**
6954
+ * Reset state for a new execution
6955
+ */
6956
+ reset() {
6957
+ this.queryAttempts.clear();
6958
+ this.toolAttempts.clear();
6959
+ this.executedToolsList = [];
6960
+ this.maxAttemptsReached = false;
6961
+ }
6962
+ /**
6963
+ * Get list of successfully executed tools
6964
+ */
6965
+ getExecutedTools() {
6966
+ return this.executedToolsList;
6967
+ }
6968
+ /**
6969
+ * Check if max attempts were reached
6970
+ */
6971
+ isMaxAttemptsReached() {
6972
+ return this.maxAttemptsReached;
6973
+ }
6974
+ /**
6975
+ * Create a tool handler function for LLM.streamWithTools
6976
+ * @param externalTools - List of available external tools
6977
+ * @returns Tool handler function
6978
+ */
6979
+ createToolHandler(externalTools) {
6980
+ return async (toolName, toolInput) => {
6981
+ if (toolName === "execute_query") {
6982
+ return this.executeQuery(toolInput);
6983
+ } else {
6984
+ return this.executeExternalTool(toolName, toolInput, externalTools);
6985
+ }
6986
+ };
6987
+ }
6988
+ /**
6989
+ * Execute a SQL query with retry tracking and streaming feedback
6990
+ */
6991
+ async executeQuery(toolInput) {
6992
+ let sql = toolInput.sql;
6993
+ const params = toolInput.params || {};
6994
+ const reasoning = toolInput.reasoning;
6995
+ const { streamBuffer, collections, logCollector, providerName } = this.config;
6996
+ sql = ensureQueryLimit(sql, MAX_COMPONENT_QUERY_LIMIT, MAX_COMPONENT_QUERY_LIMIT);
6997
+ const queryKey = sql.toLowerCase().replace(/\s+/g, " ").trim();
6998
+ const attempts = (this.queryAttempts.get(queryKey) || 0) + 1;
6999
+ this.queryAttempts.set(queryKey, attempts);
7000
+ logger.info(`[${providerName}] Executing query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${sql.substring(0, 100)}...`);
7001
+ if (Object.keys(params).length > 0) {
7002
+ logger.info(`[${providerName}] Query params: ${JSON.stringify(params)}`);
7003
+ }
7004
+ if (reasoning) {
7005
+ logCollector?.info(`Query reasoning: ${reasoning}`);
7006
+ }
7007
+ if (attempts > MAX_QUERY_ATTEMPTS) {
7008
+ const errorMsg = `Maximum query attempts (${MAX_QUERY_ATTEMPTS}) reached. Unable to generate a valid query for your question.`;
7009
+ logger.error(`[${providerName}] ${errorMsg}`);
7010
+ logCollector?.error(errorMsg);
7011
+ this.maxAttemptsReached = true;
7012
+ if (streamBuffer.hasCallback()) {
7013
+ streamBuffer.write(`
7014
+
7015
+ \u274C ${errorMsg}
7016
+
7017
+ Please try rephrasing your question or simplifying your request.
7018
+
7019
+ `);
7020
+ }
7021
+ throw new Error(errorMsg);
7022
+ }
7023
+ try {
7024
+ streamBuffer.flush();
7025
+ if (streamBuffer.hasCallback()) {
7026
+ const paramsDisplay = Object.keys(params).length > 0 ? `
7027
+ **Parameters:** ${JSON.stringify(params)}` : "";
7028
+ if (attempts === 1) {
7029
+ streamBuffer.write(`
7030
+
7031
+ \u{1F50D} **Analyzing your question...**
7032
+
7033
+ `);
7034
+ await streamDelay();
7035
+ if (reasoning) {
7036
+ streamBuffer.write(`\u{1F4AD} ${reasoning}
7037
+
7038
+ `);
7039
+ await streamDelay();
7040
+ }
7041
+ streamBuffer.write(`\u{1F4DD} **Generated SQL Query:**
7042
+ \`\`\`sql
7043
+ ${sql}
7044
+ \`\`\`${paramsDisplay}
7045
+
7046
+ `);
7047
+ await streamDelay();
7048
+ } else {
7049
+ streamBuffer.write(`
7050
+
7051
+ \u{1F504} **Retrying with corrected query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS})...**
7052
+
7053
+ `);
7054
+ await streamDelay();
7055
+ if (reasoning) {
7056
+ streamBuffer.write(`\u{1F4AD} ${reasoning}
7057
+
7058
+ `);
7059
+ await streamDelay();
7060
+ }
7061
+ streamBuffer.write(`\u{1F4DD} **Corrected SQL Query:**
7062
+ \`\`\`sql
7063
+ ${sql}
7064
+ \`\`\`${paramsDisplay}
7065
+
7066
+ `);
7067
+ await streamDelay();
7068
+ }
7069
+ }
7070
+ logCollector?.logQuery?.(
7071
+ `Executing SQL query (attempt ${attempts})`,
7072
+ { sql, params },
7073
+ { reasoning, attempt: attempts }
7074
+ );
7075
+ if (!collections?.["database"]?.["execute"]) {
7076
+ throw new Error("Database collection not registered. Please register database.execute collection to execute queries.");
7077
+ }
7078
+ const queryPayload = Object.keys(params).length > 0 ? { sql: JSON.stringify({ sql, values: params }) } : { sql };
7079
+ const result = await withProgressHeartbeat(
7080
+ () => collections["database"]["execute"](queryPayload),
7081
+ "Executing database query",
7082
+ streamBuffer
7083
+ );
7084
+ const data = result?.data || result;
7085
+ const rowCount = result?.count ?? (Array.isArray(data) ? data.length : "N/A");
7086
+ logger.info(`[${providerName}] Query executed successfully, rows returned: ${rowCount}`);
7087
+ logCollector?.info(`Query successful, returned ${rowCount} rows`);
7088
+ if (streamBuffer.hasCallback()) {
7089
+ streamBuffer.write(`
7090
+ \u2705 **Query executed successfully!**
7091
+
7092
+ `);
7093
+ await streamDelay();
7094
+ if (Array.isArray(data) && data.length > 0) {
7095
+ const firstRow = data[0];
7096
+ const columns = Object.keys(firstRow);
7097
+ if (data.length === 1 && columns.length === 1) {
7098
+ const value = firstRow[columns[0]];
7099
+ streamBuffer.write(`**Result:** ${value}
7100
+
7101
+ `);
7102
+ await streamDelay();
7103
+ } else if (data.length > 0) {
7104
+ streamBuffer.write(`**Retrieved ${rowCount} rows**
7105
+
7106
+ `);
7107
+ await streamDelay();
7108
+ const streamPreview = formatQueryResultForLLM(data, {
7109
+ maxRows: STREAM_PREVIEW_MAX_ROWS,
7110
+ maxCharsPerField: STREAM_PREVIEW_MAX_CHARS
7111
+ });
7112
+ streamBuffer.write(`<DataTable>${JSON.stringify(streamPreview.data)}</DataTable>
7113
+
7114
+ `);
7115
+ if (streamPreview.truncationNote) {
7116
+ streamBuffer.write(`*${streamPreview.truncationNote}*
7117
+
7118
+ `);
7119
+ }
7120
+ await streamDelay();
7121
+ }
7122
+ } else if (Array.isArray(data) && data.length === 0) {
7123
+ streamBuffer.write(`**No rows returned.**
7124
+
7125
+ `);
7126
+ await streamDelay();
7127
+ }
7128
+ streamBuffer.write(`\u{1F4CA} **Analyzing results...**
7129
+
7130
+ `);
7131
+ }
7132
+ const formattedResult = formatQueryResultForLLM(data, {
7133
+ maxRows: DEFAULT_MAX_ROWS_FOR_LLM,
7134
+ maxCharsPerField: DEFAULT_MAX_CHARS_PER_FIELD2
7135
+ });
7136
+ logger.info(`[${providerName}] Query result formatted: ${formattedResult.summary.recordsShown}/${formattedResult.summary.totalRecords} records`);
7137
+ if (formattedResult.truncationNote) {
7138
+ logger.info(`[${providerName}] Truncation: ${formattedResult.truncationNote}`);
7139
+ }
7140
+ return formatResultAsString(formattedResult);
7141
+ } catch (error) {
7142
+ const errorMsg = error instanceof Error ? error.message : String(error);
7143
+ logger.error(`[${providerName}] Query execution failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7144
+ logCollector?.error(`Query failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7145
+ userPromptErrorLogger.logSqlError(sql, error instanceof Error ? error : new Error(errorMsg), Object.keys(params).length > 0 ? Object.values(params) : void 0);
7146
+ if (streamBuffer.hasCallback()) {
7147
+ streamBuffer.write(`\u274C **Query execution failed:**
7148
+ \`\`\`
7149
+ ${errorMsg}
7150
+ \`\`\`
7151
+
7152
+ `);
7153
+ if (attempts < MAX_QUERY_ATTEMPTS) {
7154
+ streamBuffer.write(`\u{1F527} **Generating corrected query...**
7155
+
7156
+ `);
7157
+ }
7158
+ }
7159
+ throw new Error(`Query execution failed: ${errorMsg}`);
7160
+ }
7161
+ }
7162
+ /**
7163
+ * Execute an external tool with retry tracking and streaming feedback
7164
+ */
7165
+ async executeExternalTool(toolName, toolInput, externalTools) {
7166
+ const { streamBuffer, logCollector, providerName } = this.config;
7167
+ const externalTool = externalTools?.find((t) => t.id === toolName);
7168
+ if (!externalTool) {
7169
+ throw new Error(`Unknown tool: ${toolName}`);
7170
+ }
7171
+ const attempts = (this.toolAttempts.get(toolName) || 0) + 1;
7172
+ this.toolAttempts.set(toolName, attempts);
7173
+ logger.info(`[${providerName}] Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
7174
+ logCollector?.info(`Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
7175
+ if (attempts > MAX_TOOL_ATTEMPTS) {
7176
+ const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${externalTool.name}`;
7177
+ logger.error(`[${providerName}] ${errorMsg}`);
7178
+ logCollector?.error(errorMsg);
7179
+ if (streamBuffer.hasCallback()) {
7180
+ streamBuffer.write(`
7181
+
7182
+ \u274C ${errorMsg}
7183
+
7184
+ Please try rephrasing your request or contact support.
7185
+
7186
+ `);
7187
+ }
7188
+ throw new Error(errorMsg);
7189
+ }
7190
+ try {
7191
+ streamBuffer.flush();
7192
+ if (streamBuffer.hasCallback()) {
7193
+ if (attempts === 1) {
7194
+ streamBuffer.write(`
7195
+
7196
+ \u{1F517} **Executing ${externalTool.name}...**
7197
+
7198
+ `);
7199
+ } else {
7200
+ streamBuffer.write(`
7201
+
7202
+ \u{1F504} **Retrying ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...**
7203
+
7204
+ `);
7205
+ }
7206
+ await streamDelay();
7207
+ }
7208
+ const result = await withProgressHeartbeat(
7209
+ () => externalTool.fn(toolInput),
7210
+ `Running ${externalTool.name}`,
7211
+ streamBuffer
7212
+ );
7213
+ logger.info(`[${providerName}] External tool ${externalTool.name} executed successfully`);
7214
+ logCollector?.info(`\u2713 ${externalTool.name} executed successfully`);
7215
+ if (!this.executedToolsList.find((t) => t.id === externalTool.id)) {
7216
+ const formattedForTracking = formatToolResultForLLM(result, {
7217
+ toolName: externalTool.name,
7218
+ toolLimit: externalTool.limit,
7219
+ maxRows: TOOL_TRACKING_MAX_ROWS,
7220
+ maxCharsPerField: TOOL_TRACKING_MAX_CHARS
7221
+ });
7222
+ this.executedToolsList.push({
7223
+ id: externalTool.id,
7224
+ name: externalTool.name,
7225
+ params: toolInput,
7226
+ result: {
7227
+ _totalRecords: formattedForTracking.summary.totalRecords,
7228
+ _recordsShown: formattedForTracking.summary.recordsShown,
7229
+ _metadata: formattedForTracking.metadata,
7230
+ _sampleData: formattedForTracking.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS)
7231
+ },
7232
+ outputSchema: externalTool.outputSchema
7233
+ });
7234
+ logger.info(`[${providerName}] Tracked executed tool: ${externalTool.name} with ${formattedForTracking.summary.totalRecords} total records`);
7235
+ }
7236
+ if (streamBuffer.hasCallback()) {
7237
+ streamBuffer.write(`\u2705 **${externalTool.name} completed successfully**
7238
+
7239
+ `);
7240
+ await streamDelay();
7241
+ }
7242
+ const formattedToolResult = formatToolResultForLLM(result, {
7243
+ toolName: externalTool.name,
7244
+ toolLimit: externalTool.limit,
7245
+ maxRows: DEFAULT_MAX_ROWS_FOR_LLM,
7246
+ maxCharsPerField: DEFAULT_MAX_CHARS_PER_FIELD2
7247
+ });
7248
+ logger.info(`[${providerName}] Tool result formatted: ${formattedToolResult.summary.recordsShown}/${formattedToolResult.summary.totalRecords} records`);
7249
+ if (formattedToolResult.truncationNote) {
7250
+ logger.info(`[${providerName}] Truncation: ${formattedToolResult.truncationNote}`);
7251
+ }
7252
+ return formatResultAsString(formattedToolResult);
7253
+ } catch (error) {
7254
+ const errorMsg = error instanceof Error ? error.message : String(error);
7255
+ logger.error(`[${providerName}] External tool ${externalTool.name} failed (attempt ${attempts}/${MAX_TOOL_ATTEMPTS}): ${errorMsg}`);
7256
+ logCollector?.error(`\u2717 ${externalTool.name} failed: ${errorMsg}`);
7257
+ userPromptErrorLogger.logToolError(externalTool.name, toolInput, error instanceof Error ? error : new Error(errorMsg));
7258
+ if (streamBuffer.hasCallback()) {
7259
+ streamBuffer.write(`\u274C **${externalTool.name} failed:**
7260
+ \`\`\`
7261
+ ${errorMsg}
7262
+ \`\`\`
7263
+
7264
+ `);
7265
+ if (attempts < MAX_TOOL_ATTEMPTS) {
7266
+ streamBuffer.write(`\u{1F527} **Retrying with adjusted parameters...**
7267
+
7268
+ `);
7269
+ }
7270
+ }
7271
+ throw new Error(`Tool execution failed: ${errorMsg}`);
7272
+ }
7273
+ }
7274
+ };
7275
+
7276
+ // src/userResponse/base-llm.ts
7277
+ var BaseLLM = class {
7278
+ constructor(config) {
7279
+ this.model = config?.model || this.getDefaultModel();
7280
+ this.fastModel = config?.fastModel || this.getDefaultFastModel();
7281
+ this.defaultLimit = config?.defaultLimit || 10;
7282
+ this.apiKey = config?.apiKey;
7283
+ this.modelStrategy = config?.modelStrategy || "fast";
7284
+ this.conversationSimilarityThreshold = config?.conversationSimilarityThreshold || DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD;
7285
+ this.queryService = new QueryExecutionService({
7286
+ defaultLimit: this.defaultLimit,
7287
+ getModelForTask: (taskType) => this.getModelForTask(taskType),
7288
+ getApiKey: (apiKey) => this.getApiKey(apiKey),
7289
+ providerName: this.getProviderName()
7290
+ });
7291
+ }
7292
+ /**
7293
+ * Get the appropriate model based on task type and model strategy
7294
+ * @param taskType - 'complex' for text generation/matching, 'simple' for classification/actions
7295
+ * @returns The model string to use for this task
7296
+ */
7297
+ getModelForTask(taskType) {
7298
+ switch (this.modelStrategy) {
7299
+ case "best":
7300
+ return this.model;
7301
+ case "fast":
7302
+ return this.fastModel;
7303
+ case "balanced":
7304
+ default:
7305
+ return taskType === "complex" ? this.model : this.fastModel;
7306
+ }
7307
+ }
7308
+ /**
7309
+ * Set the model strategy at runtime
7310
+ * @param strategy - 'best', 'fast', or 'balanced'
7311
+ */
7312
+ setModelStrategy(strategy) {
7313
+ this.modelStrategy = strategy;
7314
+ logger.info(`[${this.getProviderName()}] Model strategy set to: ${strategy}`);
7315
+ }
7316
+ /**
7317
+ * Get the current model strategy
7318
+ * @returns The current model strategy
7319
+ */
7320
+ getModelStrategy() {
7321
+ return this.modelStrategy;
7322
+ }
7323
+ /**
7324
+ * Set the conversation similarity threshold at runtime
7325
+ * @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
7326
+ */
7327
+ setConversationSimilarityThreshold(threshold) {
7328
+ if (threshold < 0 || threshold > 1) {
7329
+ logger.warn(`[${this.getProviderName()}] Invalid threshold ${threshold}, must be between 0 and 1. Using default ${DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD}`);
7330
+ this.conversationSimilarityThreshold = DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD;
7331
+ return;
7332
+ }
7333
+ this.conversationSimilarityThreshold = threshold;
7334
+ logger.info(`[${this.getProviderName()}] Conversation similarity threshold set to: ${threshold}`);
7335
+ }
7336
+ /**
7337
+ * Get the current conversation similarity threshold
7338
+ * @returns The current threshold value
7339
+ */
7340
+ getConversationSimilarityThreshold() {
7341
+ return this.conversationSimilarityThreshold;
7342
+ }
7343
+ /**
7344
+ * Get the API key (from instance, parameter, or environment)
7345
+ */
7346
+ getApiKey(apiKey) {
7347
+ return apiKey || this.apiKey || this.getDefaultApiKey();
7348
+ }
7349
+ /**
7350
+ * Check if a component contains a Form (data_modification component)
7351
+ * Forms have hardcoded defaultValues that become stale when cached
7352
+ * This checks both single Form components and Forms inside MultiComponentContainer
7353
+ */
7354
+ containsFormComponent(component) {
7355
+ if (!component) return false;
7356
+ if (component.type === "Form" || component.name === "DynamicForm") {
7357
+ return true;
7358
+ }
7359
+ if (component.type === "Container" || component.name === "MultiComponentContainer") {
7360
+ const nestedComponents = component.props?.config?.components || [];
7361
+ for (const nested of nestedComponents) {
7362
+ if (nested.type === "Form" || nested.name === "DynamicForm") {
7363
+ return true;
7364
+ }
7365
+ }
7366
+ }
7367
+ return false;
7368
+ }
7369
+ /**
7370
+ * Match components from text response suggestions and generate follow-up questions
7371
+ * Takes a text response with component suggestions (c1:type format) and matches with available components
7372
+ * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
7373
+ * All components are placed in a default MultiComponentContainer layout
7374
+ * @param analysisContent - The text response containing component suggestions
7375
+ * @param components - List of available components
7376
+ * @param apiKey - Optional API key
7377
+ * @param logCollector - Optional log collector
7378
+ * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
7379
+ * @returns Object containing matched components, layout title/description, and follow-up actions
7380
+ */
7381
+ async matchComponentsFromAnalysis(analysisContent, components, userPrompt, apiKey, logCollector, componentStreamCallback, deferredTools, executedTools, collections, userId) {
7382
+ const methodStartTime = Date.now();
7383
+ const methodName = "matchComponentsFromAnalysis";
7384
+ logger.info(`[${this.getProviderName()}] [TIMING] START ${methodName} | model: ${this.getModelForTask("complex")}`);
7385
+ try {
7386
+ logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
7387
+ let availableComponentsText = "No components available";
7388
+ if (components && components.length > 0) {
7389
+ availableComponentsText = components.map((comp, idx) => {
7390
+ const keywords = comp.keywords ? comp.keywords.join(", ") : "";
7391
+ const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
7392
+ return `${idx + 1}. ID: ${comp.id}
7393
+ Name: ${comp.name}
7394
+ Type: ${comp.type}
7395
+ Description: ${comp.description || "No description"}
7396
+ Keywords: ${keywords}
7397
+ Props Structure: ${propsPreview}`;
7398
+ }).join("\n\n");
7399
+ }
7400
+ let deferredToolsText = "No deferred external tools for this request.";
7401
+ if (deferredTools && deferredTools.length > 0) {
7402
+ logger.info(`[${this.getProviderName()}] Passing ${deferredTools.length} deferred tools to component matching`);
7403
+ deferredToolsText = "The following external tools need user input via a Form component.\n**IMPORTANT: Use these EXACT values when generating Form externalTool prop.**\n\n" + deferredTools.map((tool, idx) => {
7404
+ return `${idx + 1}. **${tool.name}**
7405
+ toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
7406
+ toolName: "${tool.name}"
7407
+ parameters: ${JSON.stringify(tool.params || {})}
7408
+ requiredFields:
7409
+ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
7410
+ }).join("\n\n");
7411
+ }
7412
+ let executedToolsText = "No external tools were executed for data fetching.";
7413
+ if (executedTools && executedTools.length > 0) {
7414
+ logger.info(`[${this.getProviderName()}] Passing ${executedTools.length} executed tools to component matching`);
7415
+ executedToolsText = "The following external tools were executed to fetch data.\n" + executedTools.map((tool, idx) => {
7416
+ let outputSchemaText = "Not available";
7417
+ let fieldNamesList = "";
7418
+ const recordCount = tool.result?._totalRecords ?? "unknown";
7419
+ let metadataText = "";
7420
+ if (tool.result?._metadata && Object.keys(tool.result._metadata).length > 0) {
7421
+ const metadataEntries = Object.entries(tool.result._metadata).map(([key, value]) => `${key}: ${value}`).join(", ");
7422
+ metadataText = `
7423
+ \u{1F4CB} METADATA: ${metadataEntries}`;
7424
+ }
7425
+ if (tool.outputSchema) {
7426
+ const fields = tool.outputSchema.fields || [];
7427
+ const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
7428
+ const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
7429
+ fieldNamesList = `
7430
+ \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
7431
+ \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
7432
+ const fieldsText = fields.map(
7433
+ (f) => ` "${f.name}" (${f.type}): ${f.description}`
7434
+ ).join("\n");
7435
+ outputSchemaText = `${tool.outputSchema.description}
7436
+ Fields:
7437
+ ${fieldsText}`;
7438
+ }
7439
+ return `${idx + 1}. **${tool.name}**
7440
+ toolId: "${tool.id}"
7441
+ toolName: "${tool.name}"
7442
+ parameters: ${JSON.stringify(tool.params || {})}
7443
+ recordCount: ${recordCount} rows returned${metadataText}
7444
+ outputSchema: ${outputSchemaText}${fieldNamesList}`;
7445
+ }).join("\n\n");
7446
+ }
7447
+ const schemaDoc = schema.generateSchemaDocumentation();
6278
7448
  const databaseRules = await promptLoader.loadDatabaseRules();
6279
7449
  let knowledgeBaseContext = "No additional knowledge base context available.";
6280
7450
  if (collections) {
@@ -6282,7 +7452,7 @@ ${fieldsText}`;
6282
7452
  prompt: userPrompt || analysisContent,
6283
7453
  collections,
6284
7454
  userId,
6285
- topK: 3
7455
+ topK: KNOWLEDGE_BASE_TOP_K
6286
7456
  });
6287
7457
  knowledgeBaseContext = kbResult.combinedContext || knowledgeBaseContext;
6288
7458
  }
@@ -6298,22 +7468,6 @@ ${fieldsText}`;
6298
7468
  CURRENT_DATETIME: getCurrentDateTimeForPrompt()
6299
7469
  });
6300
7470
  logger.debug(`[${this.getProviderName()}] Loaded match-text-components prompts`);
6301
- const extractPromptText = (content) => {
6302
- if (typeof content === "string") return content;
6303
- if (Array.isArray(content)) {
6304
- return content.map((item) => {
6305
- if (typeof item === "string") return item;
6306
- if (item && typeof item.text === "string") return item.text;
6307
- if (item && item.content && typeof item.content === "string") return item.content;
6308
- return JSON.stringify(item, null, 2);
6309
- }).join("\n\n---\n\n");
6310
- }
6311
- if (content && typeof content === "object") {
6312
- if (typeof content.text === "string") return content.text;
6313
- return JSON.stringify(content, null, 2);
6314
- }
6315
- return String(content);
6316
- };
6317
7471
  logger.logLLMPrompt("matchComponentsFromAnalysis", "system", extractPromptText(prompts.system));
6318
7472
  logger.logLLMPrompt("matchComponentsFromAnalysis", "user", `Text Analysis:
6319
7473
  ${analysisContent}
@@ -6390,23 +7544,32 @@ ${executedToolsText}`);
6390
7544
  { componentName: answerComponent.name, componentType: answerComponent.type, reasoning: answerComponentData.reasoning }
6391
7545
  );
6392
7546
  }
6393
- const answerQuery = answerComponent.props?.query;
7547
+ let answerQuery = answerComponent.props?.query;
6394
7548
  logger.info(`[${this.getProviderName()}] Answer component detected: ${answerComponent.name} (${answerComponent.type}), hasQuery: ${!!answerQuery}, hasDbExecute: ${!!collections?.["database"]?.["execute"]}`);
7549
+ if (answerQuery) {
7550
+ if (typeof answerQuery === "string") {
7551
+ answerQuery = ensureQueryLimit(answerQuery, this.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7552
+ } else if (answerQuery?.sql) {
7553
+ const queryObj = answerQuery;
7554
+ answerQuery = { ...queryObj, sql: ensureQueryLimit(queryObj.sql, this.defaultLimit, MAX_COMPONENT_QUERY_LIMIT) };
7555
+ }
7556
+ answerComponent.props.query = answerQuery;
7557
+ }
6395
7558
  if (answerQuery && collections?.["database"]?.["execute"]) {
6396
7559
  (async () => {
6397
- const MAX_RETRIES = 3;
7560
+ const maxRetries = MAX_QUERY_VALIDATION_RETRIES;
6398
7561
  let attempts = 0;
6399
7562
  let validated = false;
6400
7563
  let currentQuery = answerQuery;
6401
7564
  let currentQueryStr = typeof answerQuery === "string" ? answerQuery : answerQuery?.sql || "";
6402
7565
  let lastError = "";
6403
7566
  logger.info(`[${this.getProviderName()}] Validating answer component query before streaming...`);
6404
- while (attempts < MAX_RETRIES && !validated) {
7567
+ while (attempts < maxRetries && !validated) {
6405
7568
  attempts++;
6406
7569
  try {
6407
- const cacheKey = this.getQueryCacheKey(currentQuery);
7570
+ const cacheKey = this.queryService.getQueryCacheKey(currentQuery);
6408
7571
  if (cacheKey) {
6409
- logger.debug(`[${this.getProviderName()}] Answer component query validation attempt ${attempts}/${MAX_RETRIES}`);
7572
+ logger.debug(`[${this.getProviderName()}] Answer component query validation attempt ${attempts}/${maxRetries}`);
6410
7573
  const result2 = await collections["database"]["execute"]({ sql: cacheKey });
6411
7574
  queryCache.set(cacheKey, result2);
6412
7575
  validated = true;
@@ -6421,11 +7584,11 @@ ${executedToolsText}`);
6421
7584
  }
6422
7585
  } catch (validationError) {
6423
7586
  lastError = validationError instanceof Error ? validationError.message : String(validationError);
6424
- logger.warn(`[${this.getProviderName()}] Answer component query validation failed (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6425
- if (attempts < MAX_RETRIES) {
7587
+ logger.warn(`[${this.getProviderName()}] Answer component query validation failed (attempt ${attempts}/${maxRetries}): ${lastError}`);
7588
+ if (attempts < maxRetries) {
6426
7589
  try {
6427
7590
  logger.info(`[${this.getProviderName()}] Requesting LLM to fix answer component query...`);
6428
- const fixedQueryStr = await this.requestQueryFix(
7591
+ const fixedQueryStr = await this.queryService.requestQueryFix(
6429
7592
  currentQueryStr,
6430
7593
  lastError,
6431
7594
  {
@@ -6435,7 +7598,7 @@ ${executedToolsText}`);
6435
7598
  },
6436
7599
  apiKey
6437
7600
  );
6438
- const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.defaultLimit, 10);
7601
+ const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6439
7602
  if (typeof currentQuery === "string") {
6440
7603
  currentQuery = limitedFixedQuery;
6441
7604
  } else {
@@ -6476,7 +7639,7 @@ ${executedToolsText}`);
6476
7639
  },
6477
7640
  {
6478
7641
  model: this.getModelForTask("complex"),
6479
- maxTokens: 8192,
7642
+ maxTokens: MAX_TOKENS_COMPONENT_MATCHING,
6480
7643
  temperature: 0,
6481
7644
  apiKey: this.getApiKey(apiKey),
6482
7645
  partial: partialCallback
@@ -6512,137 +7675,14 @@ ${executedToolsText}`);
6512
7675
  logger.warn(`[${this.getProviderName()}] Component ${mc.componentId} not found in available components`);
6513
7676
  return null;
6514
7677
  }
6515
- let cleanedProps = { ...mc.props };
6516
- if (cleanedProps.externalTool) {
6517
- const toolId = cleanedProps.externalTool.toolId;
6518
- const validToolIds = (executedTools || []).map((t) => t.id);
6519
- const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
6520
- if (!isValidTool) {
6521
- logger.warn(`[${this.getProviderName()}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
6522
- cleanedProps.externalTool = null;
6523
- } else {
6524
- const executedTool = executedTools?.find((t) => t.id === toolId);
6525
- if (executedTool?.outputSchema?.fields && cleanedProps.config) {
6526
- const validFieldNames = executedTool.outputSchema.fields.map((f) => f.name);
6527
- const validFieldNamesLower = validFieldNames.map((n) => n.toLowerCase());
6528
- const findMatchingField = (fieldName, configKey) => {
6529
- if (!fieldName) return null;
6530
- const lowerField = fieldName.toLowerCase();
6531
- const exactIdx = validFieldNamesLower.indexOf(lowerField);
6532
- if (exactIdx !== -1) return validFieldNames[exactIdx];
6533
- const containsMatches = validFieldNames.filter(
6534
- (_, i) => validFieldNamesLower[i].includes(lowerField) || lowerField.includes(validFieldNamesLower[i])
6535
- );
6536
- if (containsMatches.length === 1) return containsMatches[0];
6537
- const fieldTypes = executedTool.outputSchema.fields.reduce((acc, f) => {
6538
- acc[f.name] = f.type;
6539
- return acc;
6540
- }, {});
6541
- const numericConfigKeys = ["yAxisKey", "valueKey", "aggregationField", "sizeKey"];
6542
- const stringConfigKeys = ["xAxisKey", "nameKey", "labelKey", "groupBy"];
6543
- if (numericConfigKeys.includes(configKey)) {
6544
- const numericFields = validFieldNames.filter((f) => fieldTypes[f] === "number");
6545
- const match = numericFields.find((f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase()));
6546
- if (match) return match;
6547
- if (numericFields.length > 0) {
6548
- logger.warn(`[${this.getProviderName()}] No match for "${fieldName}", using first numeric field: ${numericFields[0]}`);
6549
- return numericFields[0];
6550
- }
6551
- }
6552
- if (stringConfigKeys.includes(configKey)) {
6553
- const stringFields = validFieldNames.filter((f) => fieldTypes[f] === "string");
6554
- const match = stringFields.find((f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase()));
6555
- if (match) return match;
6556
- if (stringFields.length > 0) {
6557
- logger.warn(`[${this.getProviderName()}] No match for "${fieldName}", using first string field: ${stringFields[0]}`);
6558
- return stringFields[0];
6559
- }
6560
- }
6561
- logger.warn(`[${this.getProviderName()}] No match for "${fieldName}", using first field: ${validFieldNames[0]}`);
6562
- return validFieldNames[0];
6563
- };
6564
- const configFieldsToValidate = [
6565
- "xAxisKey",
6566
- "yAxisKey",
6567
- "valueKey",
6568
- "nameKey",
6569
- "labelKey",
6570
- "groupBy",
6571
- "aggregationField",
6572
- "seriesKey",
6573
- "sizeKey",
6574
- "xAggregationField",
6575
- "yAggregationField"
6576
- ];
6577
- for (const configKey of configFieldsToValidate) {
6578
- const fieldValue = cleanedProps.config[configKey];
6579
- if (fieldValue && typeof fieldValue === "string") {
6580
- if (!validFieldNames.includes(fieldValue)) {
6581
- const correctedField = findMatchingField(fieldValue, configKey);
6582
- if (correctedField) {
6583
- logger.warn(`[${this.getProviderName()}] Correcting config.${configKey}: "${fieldValue}" \u2192 "${correctedField}"`);
6584
- cleanedProps.config[configKey] = correctedField;
6585
- }
6586
- }
6587
- }
6588
- }
6589
- if (Array.isArray(cleanedProps.config.series)) {
6590
- cleanedProps.config.series = cleanedProps.config.series.map((s) => {
6591
- if (s.dataKey && typeof s.dataKey === "string" && !validFieldNames.includes(s.dataKey)) {
6592
- const correctedField = findMatchingField(s.dataKey, "yAxisKey");
6593
- if (correctedField) {
6594
- logger.warn(`[${this.getProviderName()}] Correcting series.dataKey: "${s.dataKey}" \u2192 "${correctedField}"`);
6595
- return { ...s, dataKey: correctedField };
6596
- }
6597
- }
6598
- return s;
6599
- });
6600
- }
6601
- }
6602
- }
6603
- }
6604
- if (cleanedProps.query) {
6605
- const queryStr = typeof cleanedProps.query === "string" ? cleanedProps.query : cleanedProps.query?.sql || "";
6606
- if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
6607
- logger.warn(`[${this.getProviderName()}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
6608
- cleanedProps.query = null;
6609
- }
6610
- }
6611
- if (cleanedProps.query) {
6612
- const queryStr = typeof cleanedProps.query === "string" ? cleanedProps.query : cleanedProps.query?.sql || "";
6613
- const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
6614
- if (fixed) {
6615
- logger.warn(`[${this.getProviderName()}] SQL fixes applied to component query: ${fixes.join("; ")}`);
6616
- if (typeof cleanedProps.query === "string") {
6617
- cleanedProps.query = fixedQuery;
6618
- } else if (cleanedProps.query?.sql) {
6619
- cleanedProps.query.sql = fixedQuery;
6620
- }
6621
- }
6622
- }
6623
- if (cleanedProps.query) {
6624
- if (typeof cleanedProps.query === "string") {
6625
- cleanedProps.query = ensureQueryLimit(
6626
- cleanedProps.query,
6627
- this.defaultLimit,
6628
- 10
6629
- // maxLimit - enforce maximum of 10 rows for component queries
6630
- );
6631
- } else if (cleanedProps.query?.sql) {
6632
- cleanedProps.query = {
6633
- ...cleanedProps.query,
6634
- sql: ensureQueryLimit(
6635
- cleanedProps.query.sql,
6636
- this.defaultLimit,
6637
- 10
6638
- // maxLimit - enforce maximum of 10 rows for component queries
6639
- )
6640
- };
7678
+ const cleanedProps = processComponentProps(
7679
+ mc.props,
7680
+ executedTools,
7681
+ {
7682
+ providerName: this.getProviderName(),
7683
+ defaultLimit: this.defaultLimit
6641
7684
  }
6642
- }
6643
- if (cleanedProps.query && cleanedProps.externalTool) {
6644
- logger.info(`[${this.getProviderName()}] Both query and externalTool exist, keeping both - frontend will decide`);
6645
- }
7685
+ );
6646
7686
  return {
6647
7687
  ...originalComponent,
6648
7688
  props: {
@@ -6656,7 +7696,7 @@ ${executedToolsText}`);
6656
7696
  logger.info(`[${this.getProviderName()}] Starting query validation for ${finalComponents.length} components...`);
6657
7697
  logCollector?.info(`Validating queries for ${finalComponents.length} components...`);
6658
7698
  try {
6659
- const validationResult = await this.validateAndRetryComponentQueries(
7699
+ const validationResult = await this.queryService.validateComponentQueries(
6660
7700
  finalComponents,
6661
7701
  collections,
6662
7702
  apiKey,
@@ -6696,153 +7736,6 @@ ${executedToolsText}`);
6696
7736
  };
6697
7737
  }
6698
7738
  }
6699
- /**
6700
- * Validate a single component's query with retry logic
6701
- * @param component - The component to validate
6702
- * @param collections - Collections object containing database execute function
6703
- * @param apiKey - Optional API key for LLM calls
6704
- * @param logCollector - Optional log collector for logging
6705
- * @returns Object with validated component (or null if failed) and query result
6706
- */
6707
- async validateSingleComponentQuery(component, collections, apiKey, logCollector) {
6708
- const MAX_RETRIES = 3;
6709
- const query = component.props?.query;
6710
- const originalQueryKey = this.getQueryCacheKey(query);
6711
- const queryStr = typeof query === "string" ? query : query?.sql || "";
6712
- let finalQueryKey = originalQueryKey;
6713
- let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
6714
- let currentQueryStr = queryStr;
6715
- let validated = false;
6716
- let lastError = "";
6717
- let result = null;
6718
- let attempts = 0;
6719
- logger.info(`[${this.getProviderName()}] Validating query for component: ${component.name} (${component.type})`);
6720
- while (attempts < MAX_RETRIES && !validated) {
6721
- attempts++;
6722
- try {
6723
- logger.debug(`[${this.getProviderName()}] Query validation attempt ${attempts}/${MAX_RETRIES} for ${component.name}`);
6724
- const validationResult = await this.executeQueryForValidation(currentQuery, collections);
6725
- result = validationResult.result;
6726
- validated = true;
6727
- queryCache.set(validationResult.cacheKey, result);
6728
- logger.info(`[${this.getProviderName()}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
6729
- logCollector?.info(`\u2713 Query validated for ${component.name}`);
6730
- if (currentQueryStr !== queryStr) {
6731
- const fixedQuery = typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr };
6732
- component.props = {
6733
- ...component.props,
6734
- query: fixedQuery
6735
- };
6736
- finalQueryKey = this.getQueryCacheKey(fixedQuery);
6737
- logger.info(`[${this.getProviderName()}] Updated ${component.name} with fixed query`);
6738
- }
6739
- } catch (error) {
6740
- lastError = error instanceof Error ? error.message : String(error);
6741
- logger.warn(`[${this.getProviderName()}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6742
- logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
6743
- if (attempts >= MAX_RETRIES) {
6744
- logger.error(`[${this.getProviderName()}] \u2717 Max retries reached for ${component.name}, excluding from response`);
6745
- logCollector?.error(`Max retries reached for ${component.name}, component excluded from response`);
6746
- break;
6747
- }
6748
- logger.info(`[${this.getProviderName()}] Requesting query fix from LLM for ${component.name}...`);
6749
- logCollector?.info(`Requesting query fix for ${component.name}...`);
6750
- try {
6751
- const fixedQueryStr = await this.requestQueryFix(
6752
- currentQueryStr,
6753
- lastError,
6754
- {
6755
- name: component.name,
6756
- type: component.type,
6757
- title: component.props?.title
6758
- },
6759
- apiKey
6760
- );
6761
- if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
6762
- logger.info(`[${this.getProviderName()}] Received fixed query for ${component.name}, retrying...`);
6763
- const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.defaultLimit, 10);
6764
- currentQueryStr = limitedFixedQuery;
6765
- if (typeof currentQuery === "string") {
6766
- currentQuery = limitedFixedQuery;
6767
- } else {
6768
- currentQuery = { ...currentQuery, sql: limitedFixedQuery };
6769
- }
6770
- } else {
6771
- logger.warn(`[${this.getProviderName()}] LLM returned same or empty query, stopping retries`);
6772
- break;
6773
- }
6774
- } catch (fixError) {
6775
- const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6776
- logger.error(`[${this.getProviderName()}] Failed to get query fix from LLM: ${fixErrorMsg}`);
6777
- break;
6778
- }
6779
- }
6780
- }
6781
- if (!validated) {
6782
- logger.warn(`[${this.getProviderName()}] Component ${component.name} excluded from response due to failed query validation`);
6783
- logCollector?.warn(`Component ${component.name} excluded from response`);
6784
- }
6785
- return {
6786
- component: validated ? component : null,
6787
- queryKey: finalQueryKey,
6788
- result,
6789
- validated
6790
- };
6791
- }
6792
- /**
6793
- * Validate component queries against the database and retry with LLM fixes if they fail
6794
- * Uses parallel execution for faster validation
6795
- * @param components - Array of components with potential queries
6796
- * @param collections - Collections object containing database execute function
6797
- * @param apiKey - Optional API key for LLM calls
6798
- * @param logCollector - Optional log collector for logging
6799
- * @returns Object with validated components and a map of query results
6800
- */
6801
- async validateAndRetryComponentQueries(components, collections, apiKey, logCollector) {
6802
- const queryResults = /* @__PURE__ */ new Map();
6803
- const validatedComponents = [];
6804
- const componentsWithoutQuery = [];
6805
- const componentsWithQuery = [];
6806
- for (const component of components) {
6807
- if (!component.props?.query) {
6808
- componentsWithoutQuery.push(component);
6809
- } else {
6810
- componentsWithQuery.push(component);
6811
- }
6812
- }
6813
- validatedComponents.push(...componentsWithoutQuery);
6814
- if (componentsWithQuery.length === 0) {
6815
- return { components: validatedComponents, queryResults };
6816
- }
6817
- logger.info(`[${this.getProviderName()}] Validating ${componentsWithQuery.length} component queries in parallel...`);
6818
- logCollector?.info(`Validating ${componentsWithQuery.length} component queries in parallel...`);
6819
- const validationPromises = componentsWithQuery.map(
6820
- (component) => this.validateSingleComponentQuery(component, collections, apiKey, logCollector)
6821
- );
6822
- const results = await Promise.allSettled(validationPromises);
6823
- for (let i = 0; i < results.length; i++) {
6824
- const result = results[i];
6825
- const component = componentsWithQuery[i];
6826
- if (result.status === "fulfilled") {
6827
- const { component: validatedComponent, queryKey, result: queryResult, validated } = result.value;
6828
- if (validated && validatedComponent) {
6829
- validatedComponents.push(validatedComponent);
6830
- if (queryResult) {
6831
- queryResults.set(queryKey, queryResult);
6832
- queryResults.set(`${component.id}:${queryKey}`, queryResult);
6833
- }
6834
- }
6835
- } else {
6836
- logger.error(`[${this.getProviderName()}] Unexpected error validating ${component.name}: ${result.reason}`);
6837
- logCollector?.error(`Unexpected error validating ${component.name}: ${result.reason}`);
6838
- }
6839
- }
6840
- logger.info(`[${this.getProviderName()}] Parallel validation complete: ${validatedComponents.length}/${components.length} components validated`);
6841
- return {
6842
- components: validatedComponents,
6843
- queryResults
6844
- };
6845
- }
6846
7739
  /**
6847
7740
  * Classify user question into category and detect external tools needed
6848
7741
  * Determines if question is for data analysis, requires external tools, or needs text response
@@ -6867,24 +7760,8 @@ ${executedToolsText}`);
6867
7760
  SCHEMA_DOC: schemaDoc || "No database schema available",
6868
7761
  CURRENT_DATETIME: getCurrentDateTimeForPrompt()
6869
7762
  });
6870
- const extractTextContent = (content) => {
6871
- if (typeof content === "string") return content;
6872
- if (Array.isArray(content)) {
6873
- return content.map((item) => {
6874
- if (typeof item === "string") return item;
6875
- if (item && typeof item.text === "string") return item.text;
6876
- if (item && item.content && typeof item.content === "string") return item.content;
6877
- return JSON.stringify(item, null, 2);
6878
- }).join("\n\n---\n\n");
6879
- }
6880
- if (content && typeof content === "object") {
6881
- if (typeof content.text === "string") return content.text;
6882
- return JSON.stringify(content, null, 2);
6883
- }
6884
- return String(content);
6885
- };
6886
- logger.logLLMPrompt("classifyQuestionCategory", "system", extractTextContent(prompts.system));
6887
- logger.logLLMPrompt("classifyQuestionCategory", "user", extractTextContent(prompts.user));
7763
+ logger.logLLMPrompt("classifyQuestionCategory", "system", extractPromptText(prompts.system));
7764
+ logger.logLLMPrompt("classifyQuestionCategory", "user", extractPromptText(prompts.user));
6888
7765
  const result = await LLM.stream(
6889
7766
  {
6890
7767
  sys: prompts.system,
@@ -6892,7 +7769,7 @@ ${executedToolsText}`);
6892
7769
  },
6893
7770
  {
6894
7771
  model: this.getModelForTask("simple"),
6895
- maxTokens: 1500,
7772
+ maxTokens: MAX_TOKENS_CLASSIFICATION,
6896
7773
  temperature: 0,
6897
7774
  apiKey: this.getApiKey(apiKey)
6898
7775
  },
@@ -6965,7 +7842,7 @@ ${executedToolsText}`);
6965
7842
  },
6966
7843
  {
6967
7844
  model: this.getModelForTask("complex"),
6968
- maxTokens: 8192,
7845
+ maxTokens: MAX_TOKENS_ADAPTATION,
6969
7846
  temperature: 0,
6970
7847
  apiKey: this.getApiKey(apiKey)
6971
7848
  },
@@ -7082,7 +7959,7 @@ ${executedToolsText}`);
7082
7959
  prompt: userPrompt,
7083
7960
  collections,
7084
7961
  userId,
7085
- topK: 3
7962
+ topK: KNOWLEDGE_BASE_TOP_K
7086
7963
  });
7087
7964
  const knowledgeBaseContext = kbResult.combinedContext;
7088
7965
  const prompts = await promptLoader.loadPrompts("text-response", {
@@ -7094,24 +7971,8 @@ ${executedToolsText}`);
7094
7971
  AVAILABLE_EXTERNAL_TOOLS: availableToolsDoc,
7095
7972
  CURRENT_DATETIME: getCurrentDateTimeForPrompt()
7096
7973
  });
7097
- const extractText = (content) => {
7098
- if (typeof content === "string") return content;
7099
- if (Array.isArray(content)) {
7100
- return content.map((item) => {
7101
- if (typeof item === "string") return item;
7102
- if (item && typeof item.text === "string") return item.text;
7103
- if (item && item.content && typeof item.content === "string") return item.content;
7104
- return JSON.stringify(item, null, 2);
7105
- }).join("\n\n---\n\n");
7106
- }
7107
- if (content && typeof content === "object") {
7108
- if (typeof content.text === "string") return content.text;
7109
- return JSON.stringify(content, null, 2);
7110
- }
7111
- return String(content);
7112
- };
7113
- logger.logLLMPrompt("generateTextResponse", "system", extractText(prompts.system));
7114
- logger.logLLMPrompt("generateTextResponse", "user", extractText(prompts.user));
7974
+ logger.logLLMPrompt("generateTextResponse", "system", extractPromptText(prompts.system));
7975
+ logger.logLLMPrompt("generateTextResponse", "user", extractPromptText(prompts.user));
7115
7976
  logger.debug(`[${this.getProviderName()}] Loaded text-response prompts with schema`);
7116
7977
  logger.debug(`[${this.getProviderName()}] System prompt length: ${prompts.system.length}, User prompt length: ${prompts.user.length}`);
7117
7978
  logCollector?.info("Generating text response with query execution capability...");
@@ -7223,324 +8084,24 @@ ${executedToolsText}`);
7223
8084
  logger.info(`[${this.getProviderName()}] Added ${addedToolIds.size} unique tool definitions from ${executableTools.length} tool calls (${externalTools.length - executableTools.length} deferred tools await form input)`);
7224
8085
  logger.info(`[${this.getProviderName()}] Complete tools array:`, JSON.stringify(tools, null, 2));
7225
8086
  }
7226
- const queryAttempts = /* @__PURE__ */ new Map();
7227
- const MAX_QUERY_ATTEMPTS = 6;
7228
- const toolAttempts = /* @__PURE__ */ new Map();
7229
- const MAX_TOOL_ATTEMPTS = 3;
7230
- const executedToolsList = [];
7231
- let maxAttemptsReached = false;
7232
- let fullStreamedText = "";
7233
- let streamBuffer = "";
7234
- let flushTimer = null;
7235
- const FLUSH_INTERVAL = 50;
7236
- const flushStreamBuffer = () => {
7237
- if (streamBuffer && streamCallback) {
7238
- streamCallback(streamBuffer);
7239
- streamBuffer = "";
7240
- }
7241
- flushTimer = null;
7242
- };
7243
- const wrappedStreamCallback = streamCallback ? (chunk) => {
7244
- fullStreamedText += chunk;
7245
- streamBuffer += chunk;
7246
- if (chunk.includes("\n") || chunk.length > 100) {
7247
- if (flushTimer) {
7248
- clearTimeout(flushTimer);
7249
- flushTimer = null;
7250
- }
7251
- flushStreamBuffer();
7252
- } else if (!flushTimer) {
7253
- flushTimer = setTimeout(flushStreamBuffer, FLUSH_INTERVAL);
7254
- }
7255
- } : void 0;
7256
- const flushStream = () => {
7257
- if (flushTimer) {
7258
- clearTimeout(flushTimer);
7259
- flushTimer = null;
7260
- }
7261
- flushStreamBuffer();
7262
- };
7263
- const streamDelay = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
7264
- const withProgressHeartbeat = async (operation, progressMessage, intervalMs = 1e3) => {
7265
- if (!wrappedStreamCallback) {
7266
- return operation();
7267
- }
7268
- const startTime = Date.now();
7269
- await streamDelay(30);
7270
- wrappedStreamCallback(`\u23F3 ${progressMessage}`);
7271
- const heartbeatInterval = setInterval(() => {
7272
- const elapsedSeconds = Math.floor((Date.now() - startTime) / 1e3);
7273
- if (elapsedSeconds >= 1) {
7274
- wrappedStreamCallback(` (${elapsedSeconds}s)`);
7275
- }
7276
- }, intervalMs);
7277
- try {
7278
- const result2 = await operation();
7279
- return result2;
7280
- } finally {
7281
- clearInterval(heartbeatInterval);
7282
- wrappedStreamCallback("\n\n");
7283
- }
7284
- };
7285
- const toolHandler = async (toolName, toolInput) => {
7286
- if (toolName === "execute_query") {
7287
- let sql = toolInput.sql;
7288
- const params = toolInput.params || {};
7289
- const reasoning = toolInput.reasoning;
7290
- sql = ensureQueryLimit(sql, 10, 10);
7291
- const queryKey = sql.toLowerCase().replace(/\s+/g, " ").trim();
7292
- const attempts = (queryAttempts.get(queryKey) || 0) + 1;
7293
- queryAttempts.set(queryKey, attempts);
7294
- logger.info(`[${this.getProviderName()}] Executing query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${sql.substring(0, 100)}...`);
7295
- if (Object.keys(params).length > 0) {
7296
- logger.info(`[${this.getProviderName()}] Query params: ${JSON.stringify(params)}`);
7297
- }
7298
- if (reasoning) {
7299
- logCollector?.info(`Query reasoning: ${reasoning}`);
7300
- }
7301
- if (attempts > MAX_QUERY_ATTEMPTS) {
7302
- const errorMsg = `Maximum query attempts (${MAX_QUERY_ATTEMPTS}) reached. Unable to generate a valid query for your question.`;
7303
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
7304
- logCollector?.error(errorMsg);
7305
- maxAttemptsReached = true;
7306
- if (wrappedStreamCallback) {
7307
- wrappedStreamCallback(`
7308
-
7309
- \u274C ${errorMsg}
7310
-
7311
- Please try rephrasing your question or simplifying your request.
7312
-
7313
- `);
7314
- }
7315
- throw new Error(errorMsg);
7316
- }
7317
- try {
7318
- flushStream();
7319
- if (wrappedStreamCallback) {
7320
- const paramsDisplay = Object.keys(params).length > 0 ? `
7321
- **Parameters:** ${JSON.stringify(params)}` : "";
7322
- if (attempts === 1) {
7323
- wrappedStreamCallback(`
7324
-
7325
- \u{1F50D} **Analyzing your question...**
7326
-
7327
- `);
7328
- await streamDelay(50);
7329
- if (reasoning) {
7330
- wrappedStreamCallback(`\u{1F4AD} ${reasoning}
7331
-
7332
- `);
7333
- await streamDelay(50);
7334
- }
7335
- wrappedStreamCallback(`\u{1F4DD} **Generated SQL Query:**
7336
- \`\`\`sql
7337
- ${sql}
7338
- \`\`\`${paramsDisplay}
7339
-
7340
- `);
7341
- await streamDelay(50);
7342
- } else {
7343
- wrappedStreamCallback(`
7344
-
7345
- \u{1F504} **Retrying with corrected query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS})...**
7346
-
7347
- `);
7348
- await streamDelay(50);
7349
- if (reasoning) {
7350
- wrappedStreamCallback(`\u{1F4AD} ${reasoning}
7351
-
7352
- `);
7353
- await streamDelay(50);
7354
- }
7355
- wrappedStreamCallback(`\u{1F4DD} **Corrected SQL Query:**
7356
- \`\`\`sql
7357
- ${sql}
7358
- \`\`\`${paramsDisplay}
7359
-
7360
- `);
7361
- await streamDelay(50);
7362
- }
7363
- }
7364
- logCollector?.logQuery(
7365
- `Executing SQL query (attempt ${attempts})`,
7366
- { sql, params },
7367
- { reasoning, attempt: attempts }
7368
- );
7369
- if (!collections || !collections["database"] || !collections["database"]["execute"]) {
7370
- throw new Error("Database collection not registered. Please register database.execute collection to execute queries.");
7371
- }
7372
- const queryPayload = Object.keys(params).length > 0 ? { sql: JSON.stringify({ sql, values: params }) } : { sql };
7373
- const result2 = await withProgressHeartbeat(
7374
- () => collections["database"]["execute"](queryPayload),
7375
- "Executing database query",
7376
- 800
7377
- // Send heartbeat every 800ms for responsive feedback
7378
- );
7379
- const data = result2?.data || result2;
7380
- const rowCount = result2?.count ?? (Array.isArray(data) ? data.length : "N/A");
7381
- logger.info(`[${this.getProviderName()}] Query executed successfully, rows returned: ${rowCount}`);
7382
- logCollector?.info(`Query successful, returned ${rowCount} rows`);
7383
- if (wrappedStreamCallback) {
7384
- wrappedStreamCallback(`
7385
- \u2705 **Query executed successfully!**
7386
-
7387
- `);
7388
- await streamDelay(50);
7389
- if (Array.isArray(data) && data.length > 0) {
7390
- const firstRow = data[0];
7391
- const columns = Object.keys(firstRow);
7392
- if (data.length === 1 && columns.length === 1) {
7393
- const value = firstRow[columns[0]];
7394
- wrappedStreamCallback(`**Result:** ${value}
7395
-
7396
- `);
7397
- await streamDelay(50);
7398
- } else if (data.length > 0) {
7399
- wrappedStreamCallback(`**Retrieved ${rowCount} rows**
7400
-
7401
- `);
7402
- await streamDelay(50);
7403
- wrappedStreamCallback(`<DataTable>${JSON.stringify(data)}</DataTable>
7404
-
7405
- `);
7406
- await streamDelay(50);
7407
- }
7408
- } else if (Array.isArray(data) && data.length === 0) {
7409
- wrappedStreamCallback(`**No rows returned.**
7410
-
7411
- `);
7412
- await streamDelay(50);
7413
- }
7414
- wrappedStreamCallback(`\u{1F4CA} **Analyzing results...**
7415
-
7416
- `);
7417
- }
7418
- return JSON.stringify(data, null, 2);
7419
- } catch (error) {
7420
- const errorMsg = error instanceof Error ? error.message : String(error);
7421
- logger.error(`[${this.getProviderName()}] Query execution failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7422
- logCollector?.error(`Query failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7423
- userPromptErrorLogger.logSqlError(sql, error instanceof Error ? error : new Error(errorMsg), Object.keys(params).length > 0 ? Object.values(params) : void 0);
7424
- if (wrappedStreamCallback) {
7425
- wrappedStreamCallback(`\u274C **Query execution failed:**
7426
- \`\`\`
7427
- ${errorMsg}
7428
- \`\`\`
7429
-
7430
- `);
7431
- if (attempts < MAX_QUERY_ATTEMPTS) {
7432
- wrappedStreamCallback(`\u{1F527} **Generating corrected query...**
7433
-
7434
- `);
7435
- }
7436
- }
7437
- throw new Error(`Query execution failed: ${errorMsg}`);
7438
- }
7439
- } else {
7440
- const externalTool = externalTools?.find((t) => t.id === toolName);
7441
- if (externalTool) {
7442
- const attempts = (toolAttempts.get(toolName) || 0) + 1;
7443
- toolAttempts.set(toolName, attempts);
7444
- logger.info(`[${this.getProviderName()}] Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
7445
- logCollector?.info(`Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
7446
- if (attempts > MAX_TOOL_ATTEMPTS) {
7447
- const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${externalTool.name}`;
7448
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
7449
- logCollector?.error(errorMsg);
7450
- if (wrappedStreamCallback) {
7451
- wrappedStreamCallback(`
7452
-
7453
- \u274C ${errorMsg}
7454
-
7455
- Please try rephrasing your request or contact support.
7456
-
7457
- `);
7458
- }
7459
- throw new Error(errorMsg);
7460
- }
7461
- try {
7462
- flushStream();
7463
- if (wrappedStreamCallback) {
7464
- if (attempts === 1) {
7465
- wrappedStreamCallback(`
7466
-
7467
- \u{1F517} **Executing ${externalTool.name}...**
7468
-
7469
- `);
7470
- } else {
7471
- wrappedStreamCallback(`
7472
-
7473
- \u{1F504} **Retrying ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...**
7474
-
7475
- `);
7476
- }
7477
- await streamDelay(50);
7478
- }
7479
- const result2 = await withProgressHeartbeat(
7480
- () => externalTool.fn(toolInput),
7481
- `Running ${externalTool.name}`,
7482
- 800
7483
- // Send heartbeat every 800ms
7484
- );
7485
- logger.info(`[${this.getProviderName()}] External tool ${externalTool.name} executed successfully`);
7486
- logCollector?.info(`\u2713 ${externalTool.name} executed successfully`);
7487
- if (!executedToolsList.find((t) => t.id === externalTool.id)) {
7488
- let resultSummary = null;
7489
- if (result2) {
7490
- const resultStr = typeof result2 === "string" ? result2 : JSON.stringify(result2);
7491
- if (resultStr.length > 1e3) {
7492
- resultSummary = {
7493
- _preview: resultStr.substring(0, 1e3) + "... (truncated)",
7494
- _totalLength: resultStr.length,
7495
- _recordCount: Array.isArray(result2) ? result2.length : result2?.data?.length || result2?.contacts?.length || result2?.salesorders?.length || "unknown"
7496
- };
7497
- } else {
7498
- resultSummary = result2;
7499
- }
7500
- }
7501
- executedToolsList.push({
7502
- id: externalTool.id,
7503
- name: externalTool.name,
7504
- params: toolInput,
7505
- // The actual parameters used in this execution
7506
- result: resultSummary,
7507
- // Store summary instead of full result to save memory
7508
- outputSchema: externalTool.outputSchema
7509
- // Include output schema for component config generation
7510
- });
7511
- logger.info(`[${this.getProviderName()}] Tracked executed tool: ${externalTool.name} with params: ${JSON.stringify(toolInput)}`);
7512
- }
7513
- if (wrappedStreamCallback) {
7514
- wrappedStreamCallback(`\u2705 **${externalTool.name} completed successfully**
7515
-
7516
- `);
7517
- await streamDelay(50);
7518
- }
7519
- return JSON.stringify(result2, null, 2);
7520
- } catch (error) {
7521
- const errorMsg = error instanceof Error ? error.message : String(error);
7522
- logger.error(`[${this.getProviderName()}] External tool ${externalTool.name} failed (attempt ${attempts}/${MAX_TOOL_ATTEMPTS}): ${errorMsg}`);
7523
- logCollector?.error(`\u2717 ${externalTool.name} failed: ${errorMsg}`);
7524
- userPromptErrorLogger.logToolError(externalTool.name, toolInput, error instanceof Error ? error : new Error(errorMsg));
7525
- if (wrappedStreamCallback) {
7526
- wrappedStreamCallback(`\u274C **${externalTool.name} failed:**
7527
- \`\`\`
7528
- ${errorMsg}
7529
- \`\`\`
7530
-
7531
- `);
7532
- if (attempts < MAX_TOOL_ATTEMPTS) {
7533
- wrappedStreamCallback(`\u{1F527} **Retrying with adjusted parameters...**
7534
-
7535
- `);
7536
- }
7537
- }
7538
- throw new Error(`Tool execution failed: ${errorMsg}`);
7539
- }
7540
- }
7541
- throw new Error(`Unknown tool: ${toolName}`);
7542
- }
7543
- };
8087
+ const streamBuffer = new StreamBuffer(streamCallback);
8088
+ const toolExecutor = new ToolExecutorService({
8089
+ providerName: this.getProviderName(),
8090
+ collections,
8091
+ streamBuffer,
8092
+ logCollector
8093
+ });
8094
+ const executableExternalTools = externalTools?.map((t) => ({
8095
+ id: t.id,
8096
+ name: t.name,
8097
+ description: t.description,
8098
+ fn: t.fn,
8099
+ limit: t.limit,
8100
+ outputSchema: t.outputSchema,
8101
+ executionType: t.executionType,
8102
+ userProvidedData: t.userProvidedData
8103
+ })) || [];
8104
+ const toolHandler = toolExecutor.createToolHandler(executableExternalTools);
7544
8105
  const result = await LLM.streamWithTools(
7545
8106
  {
7546
8107
  sys: prompts.system,
@@ -7550,18 +8111,16 @@ ${errorMsg}
7550
8111
  toolHandler,
7551
8112
  {
7552
8113
  model: this.getModelForTask("complex"),
7553
- maxTokens: 4e3,
8114
+ maxTokens: MAX_TOKENS_TEXT_RESPONSE,
7554
8115
  temperature: 0,
7555
8116
  apiKey: this.getApiKey(apiKey),
7556
- partial: wrappedStreamCallback
7557
- // Pass the wrapped streaming callback to LLM
8117
+ partial: streamBuffer.hasCallback() ? (chunk) => streamBuffer.write(chunk) : void 0
7558
8118
  },
7559
- 20
7560
- // max iterations: allows for 6 query retries + 3 tool retries + final response + buffer
8119
+ MAX_TOOL_CALLING_ITERATIONS
7561
8120
  );
7562
8121
  logger.info(`[${this.getProviderName()}] Text response stream completed`);
7563
- const textResponse = fullStreamedText || result || "I apologize, but I was unable to generate a response.";
7564
- if (maxAttemptsReached) {
8122
+ const textResponse = streamBuffer.getFullText() || result || "I apologize, but I was unable to generate a response.";
8123
+ if (toolExecutor.isMaxAttemptsReached()) {
7565
8124
  const methodDuration2 = Date.now() - methodStartTime;
7566
8125
  logger.warn(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration2}ms | result: max attempts reached`);
7567
8126
  logCollector?.error("Failed to generate valid query after maximum attempts");
@@ -7584,10 +8143,10 @@ ${errorMsg}
7584
8143
  textLength: textResponse.length
7585
8144
  }
7586
8145
  );
7587
- flushStream();
7588
- if (wrappedStreamCallback && components && components.length > 0 && category !== "general") {
7589
- wrappedStreamCallback("\n\n\u{1F4CA} **Generating visualization components...**\n\n");
7590
- wrappedStreamCallback("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
8146
+ streamBuffer.flush();
8147
+ if (streamBuffer.hasCallback() && components && components.length > 0 && category !== "general") {
8148
+ streamBuffer.write("\n\n\u{1F4CA} **Generating visualization components...**\n\n");
8149
+ streamBuffer.write("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
7591
8150
  }
7592
8151
  let matchedComponents = [];
7593
8152
  let layoutTitle = "Dashboard";
@@ -7613,11 +8172,11 @@ ${errorMsg}
7613
8172
  logger.info(`[${this.getProviderName()}] Generated ${actions.length} follow-up actions for general question`);
7614
8173
  } else if (components && components.length > 0) {
7615
8174
  logger.info(`[${this.getProviderName()}] Matching components from text response...`);
7616
- logger.info(`[${this.getProviderName()}] componentStreamCallback setup: wrappedStreamCallback=${!!wrappedStreamCallback}, category=${category}`);
7617
- const componentStreamCallback = wrappedStreamCallback && category !== "data_modification" ? (component) => {
8175
+ logger.info(`[${this.getProviderName()}] componentStreamCallback setup: hasCallback=${streamBuffer.hasCallback()}, category=${category}`);
8176
+ const componentStreamCallback = streamBuffer.hasCallback() && category !== "data_modification" ? (component) => {
7618
8177
  logger.info(`[${this.getProviderName()}] componentStreamCallback INVOKED for: ${component.name} (${component.type})`);
7619
8178
  const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
7620
- wrappedStreamCallback(answerMarker);
8179
+ streamBuffer.write(answerMarker);
7621
8180
  logger.info(`[${this.getProviderName()}] Streamed answer component to frontend: ${component.name} (${component.type})`);
7622
8181
  } : void 0;
7623
8182
  logger.info(`[${this.getProviderName()}] componentStreamCallback created: ${!!componentStreamCallback}`);
@@ -7644,7 +8203,7 @@ ${errorMsg}
7644
8203
  logCollector,
7645
8204
  componentStreamCallback,
7646
8205
  deferredTools,
7647
- executedToolsList,
8206
+ toolExecutor.getExecutedTools(),
7648
8207
  collections,
7649
8208
  userId
7650
8209
  );
@@ -7728,7 +8287,7 @@ ${errorMsg}
7728
8287
  userPrompt,
7729
8288
  collections,
7730
8289
  userId,
7731
- similarityThreshold: 0.99
8290
+ similarityThreshold: EXACT_MATCH_SIMILARITY_THRESHOLD
7732
8291
  });
7733
8292
  if (conversationMatch) {
7734
8293
  logger.info(`[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`);
@@ -7743,7 +8302,7 @@ ${errorMsg}
7743
8302
  logger.info(`[${this.getProviderName()}] Skipping cached result - Form components contain stale defaultValues, fetching fresh data`);
7744
8303
  logCollector?.info("Skipping cache for form - fetching current values from database...");
7745
8304
  } else if (!component) {
7746
- if (conversationMatch.similarity >= 0.99) {
8305
+ if (conversationMatch.similarity >= EXACT_MATCH_SIMILARITY_THRESHOLD) {
7747
8306
  const elapsedTime2 = Date.now() - startTime;
7748
8307
  logger.info(`[${this.getProviderName()}] \u2713 Exact match for general question - returning cached text response`);
7749
8308
  logCollector?.info(`\u2713 Exact match for general question - returning cached response`);
@@ -7765,7 +8324,7 @@ ${errorMsg}
7765
8324
  logCollector?.info("Similar match found but was a general conversation - processing as new question");
7766
8325
  }
7767
8326
  } else {
7768
- if (conversationMatch.similarity >= 0.99) {
8327
+ if (conversationMatch.similarity >= EXACT_MATCH_SIMILARITY_THRESHOLD) {
7769
8328
  const elapsedTime2 = Date.now() - startTime;
7770
8329
  logger.info(`[${this.getProviderName()}] \u2713 100% match - returning UI block directly without adaptation`);
7771
8330
  logCollector?.info(`\u2713 Exact match (${(conversationMatch.similarity * 100).toFixed(2)}%) - returning cached result`);
@@ -7960,7 +8519,7 @@ ${errorMsg}
7960
8519
  },
7961
8520
  {
7962
8521
  model: this.getModelForTask("simple"),
7963
- maxTokens: 1200,
8522
+ maxTokens: MAX_TOKENS_NEXT_QUESTIONS,
7964
8523
  temperature: 0,
7965
8524
  apiKey: this.getApiKey(apiKey)
7966
8525
  },
@@ -8741,6 +9300,12 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
8741
9300
  };
8742
9301
  async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools) {
8743
9302
  const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools);
9303
+ if (response.data?.component?.props?.config?.components) {
9304
+ response.data.component.props.config.components = response.data.component.props.config.components.map((comp) => ({
9305
+ ...comp,
9306
+ id: `comp_${Math.random().toString(36).substring(2, 8)}`
9307
+ }));
9308
+ }
8744
9309
  sendDataResponse4(
8745
9310
  response.id || data.id,
8746
9311
  {