@superatomai/sdk-node 0.0.71 → 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.mjs CHANGED
@@ -1458,6 +1458,351 @@ var QueryCache = class {
1458
1458
  };
1459
1459
  var queryCache = new QueryCache();
1460
1460
 
1461
+ // src/userResponse/llm-result-truncator.ts
1462
+ var DEFAULT_MAX_ROWS = 10;
1463
+ var DEFAULT_MAX_CHARS_PER_FIELD = 500;
1464
+ function inferFieldType(value) {
1465
+ if (value === null || value === void 0) {
1466
+ return "null";
1467
+ }
1468
+ if (typeof value === "string") {
1469
+ if (isDateString(value)) {
1470
+ return "date";
1471
+ }
1472
+ return "string";
1473
+ }
1474
+ if (typeof value === "number") {
1475
+ return "number";
1476
+ }
1477
+ if (typeof value === "boolean") {
1478
+ return "boolean";
1479
+ }
1480
+ if (Array.isArray(value)) {
1481
+ return "array";
1482
+ }
1483
+ if (typeof value === "object") {
1484
+ return "object";
1485
+ }
1486
+ return "unknown";
1487
+ }
1488
+ function isDateString(value) {
1489
+ const datePatterns = [
1490
+ /^\d{4}-\d{2}-\d{2}$/,
1491
+ // YYYY-MM-DD
1492
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
1493
+ // ISO 8601
1494
+ /^\d{2}\/\d{2}\/\d{4}$/,
1495
+ // MM/DD/YYYY
1496
+ /^\d{4}\/\d{2}\/\d{2}$/
1497
+ // YYYY/MM/DD
1498
+ ];
1499
+ return datePatterns.some((pattern) => pattern.test(value));
1500
+ }
1501
+ function truncateTextField(value, maxLength) {
1502
+ if (value.length <= maxLength) {
1503
+ return { text: value, wasTruncated: false };
1504
+ }
1505
+ const truncated = value.substring(0, maxLength);
1506
+ const remaining = value.length - maxLength;
1507
+ return {
1508
+ text: `${truncated}... (${remaining} more chars)`,
1509
+ wasTruncated: true
1510
+ };
1511
+ }
1512
+ function truncateFieldValue(value, maxCharsPerField) {
1513
+ if (value === null || value === void 0) {
1514
+ return { value, wasTruncated: false };
1515
+ }
1516
+ if (typeof value === "number" || typeof value === "boolean") {
1517
+ return { value, wasTruncated: false };
1518
+ }
1519
+ if (typeof value === "string") {
1520
+ const result2 = truncateTextField(value, maxCharsPerField);
1521
+ return { value: result2.text, wasTruncated: result2.wasTruncated };
1522
+ }
1523
+ if (Array.isArray(value)) {
1524
+ if (value.length === 0) {
1525
+ return { value: [], wasTruncated: false };
1526
+ }
1527
+ const preview = value.slice(0, 3);
1528
+ const hasMore = value.length > 3;
1529
+ return {
1530
+ value: hasMore ? `[${preview.join(", ")}... (${value.length} items)]` : value,
1531
+ wasTruncated: hasMore
1532
+ };
1533
+ }
1534
+ if (typeof value === "object") {
1535
+ const jsonStr = JSON.stringify(value);
1536
+ const result2 = truncateTextField(jsonStr, maxCharsPerField);
1537
+ return { value: result2.text, wasTruncated: result2.wasTruncated };
1538
+ }
1539
+ const strValue = String(value);
1540
+ const result = truncateTextField(strValue, maxCharsPerField);
1541
+ return { value: result.text, wasTruncated: result.wasTruncated };
1542
+ }
1543
+ function truncateRow(row, maxCharsPerField) {
1544
+ const truncatedRow = {};
1545
+ const truncatedFields = /* @__PURE__ */ new Set();
1546
+ for (const [key, value] of Object.entries(row)) {
1547
+ const result = truncateFieldValue(value, maxCharsPerField);
1548
+ truncatedRow[key] = result.value;
1549
+ if (result.wasTruncated) {
1550
+ truncatedFields.add(key);
1551
+ }
1552
+ }
1553
+ return { row: truncatedRow, truncatedFields };
1554
+ }
1555
+ function extractMetadataFromObject(obj, dataKey, maxCharsPerField) {
1556
+ const metadata = {};
1557
+ const truncatedFields = /* @__PURE__ */ new Set();
1558
+ for (const [key, value] of Object.entries(obj)) {
1559
+ if (key === dataKey) {
1560
+ continue;
1561
+ }
1562
+ if (Array.isArray(value)) {
1563
+ continue;
1564
+ }
1565
+ const result = truncateFieldValue(value, maxCharsPerField);
1566
+ metadata[key] = result.value;
1567
+ if (result.wasTruncated) {
1568
+ truncatedFields.add(key);
1569
+ }
1570
+ }
1571
+ return { metadata, truncatedFields };
1572
+ }
1573
+ function extractSchema(data, truncatedFields = /* @__PURE__ */ new Set()) {
1574
+ if (!data || data.length === 0) {
1575
+ return [];
1576
+ }
1577
+ const firstRow = data[0];
1578
+ const schema2 = [];
1579
+ for (const [name, value] of Object.entries(firstRow)) {
1580
+ schema2.push({
1581
+ name,
1582
+ type: inferFieldType(value),
1583
+ truncated: truncatedFields.has(name) ? true : void 0
1584
+ });
1585
+ }
1586
+ return schema2;
1587
+ }
1588
+ function truncateDataArray(data, options = {}) {
1589
+ const maxRows = options.maxRows ?? DEFAULT_MAX_ROWS;
1590
+ const maxCharsPerField = options.maxCharsPerField ?? DEFAULT_MAX_CHARS_PER_FIELD;
1591
+ if (!data || !Array.isArray(data)) {
1592
+ return {
1593
+ data: [],
1594
+ totalRecords: 0,
1595
+ recordsShown: 0,
1596
+ truncatedFields: /* @__PURE__ */ new Set()
1597
+ };
1598
+ }
1599
+ const totalRecords = data.length;
1600
+ const rowsToProcess = data.slice(0, maxRows);
1601
+ const truncatedData = [];
1602
+ const allTruncatedFields = /* @__PURE__ */ new Set();
1603
+ for (const row of rowsToProcess) {
1604
+ const { row: truncatedRow, truncatedFields } = truncateRow(row, maxCharsPerField);
1605
+ truncatedData.push(truncatedRow);
1606
+ for (const field of truncatedFields) {
1607
+ allTruncatedFields.add(field);
1608
+ }
1609
+ }
1610
+ return {
1611
+ data: truncatedData,
1612
+ totalRecords,
1613
+ recordsShown: truncatedData.length,
1614
+ truncatedFields: allTruncatedFields
1615
+ };
1616
+ }
1617
+ function buildTruncationNote(totalRecords, recordsShown, truncatedFields, maxCharsPerField, sourceName) {
1618
+ const parts = [];
1619
+ if (totalRecords > recordsShown) {
1620
+ const source = sourceName ? ` from ${sourceName}` : "";
1621
+ parts.push(`Showing ${recordsShown} of ${totalRecords} total records${source}`);
1622
+ }
1623
+ if (truncatedFields.size > 0) {
1624
+ const fieldList = Array.from(truncatedFields).join(", ");
1625
+ parts.push(`Fields truncated to ${maxCharsPerField} chars: ${fieldList}`);
1626
+ }
1627
+ return parts.length > 0 ? parts.join(". ") + "." : null;
1628
+ }
1629
+ function formatQueryResultForLLM(data, options = {}) {
1630
+ const maxCharsPerField = options.maxCharsPerField ?? DEFAULT_MAX_CHARS_PER_FIELD;
1631
+ if (!Array.isArray(data)) {
1632
+ if (data !== null && data !== void 0) {
1633
+ return {
1634
+ summary: {
1635
+ totalRecords: 1,
1636
+ recordsShown: 1,
1637
+ schema: [{ name: "result", type: inferFieldType(data) }]
1638
+ },
1639
+ data: [{ result: data }],
1640
+ truncationNote: null
1641
+ };
1642
+ }
1643
+ return {
1644
+ summary: {
1645
+ totalRecords: 0,
1646
+ recordsShown: 0,
1647
+ schema: []
1648
+ },
1649
+ data: [],
1650
+ truncationNote: null
1651
+ };
1652
+ }
1653
+ const {
1654
+ data: truncatedData,
1655
+ totalRecords,
1656
+ recordsShown,
1657
+ truncatedFields
1658
+ } = truncateDataArray(data, options);
1659
+ const schema2 = extractSchema(truncatedData, truncatedFields);
1660
+ const truncationNote = buildTruncationNote(
1661
+ totalRecords,
1662
+ recordsShown,
1663
+ truncatedFields,
1664
+ maxCharsPerField,
1665
+ "query"
1666
+ );
1667
+ return {
1668
+ summary: {
1669
+ totalRecords,
1670
+ recordsShown,
1671
+ schema: schema2
1672
+ },
1673
+ data: truncatedData,
1674
+ truncationNote
1675
+ };
1676
+ }
1677
+ function formatToolResultForLLM(result, options = {}) {
1678
+ const { toolName, toolLimit } = options;
1679
+ const effectiveMaxRows = toolLimit ?? options.maxRows ?? DEFAULT_MAX_ROWS;
1680
+ const maxCharsPerField = options.maxCharsPerField ?? DEFAULT_MAX_CHARS_PER_FIELD;
1681
+ if (result === null || result === void 0) {
1682
+ return {
1683
+ toolName,
1684
+ summary: {
1685
+ totalRecords: 0,
1686
+ recordsShown: 0,
1687
+ schema: []
1688
+ },
1689
+ data: [],
1690
+ truncationNote: null
1691
+ };
1692
+ }
1693
+ if (typeof result === "string") {
1694
+ const { text, wasTruncated } = truncateTextField(result, maxCharsPerField);
1695
+ return {
1696
+ toolName,
1697
+ summary: {
1698
+ totalRecords: 1,
1699
+ recordsShown: 1,
1700
+ schema: [{ name: "result", type: "string", truncated: wasTruncated || void 0 }]
1701
+ },
1702
+ data: [{ result: text }],
1703
+ truncationNote: wasTruncated ? `Result truncated to ${maxCharsPerField} chars.` : null
1704
+ };
1705
+ }
1706
+ if (Array.isArray(result)) {
1707
+ const {
1708
+ data: truncatedData,
1709
+ totalRecords,
1710
+ recordsShown,
1711
+ truncatedFields
1712
+ } = truncateDataArray(result, {
1713
+ maxRows: effectiveMaxRows,
1714
+ maxCharsPerField
1715
+ });
1716
+ const schema2 = extractSchema(truncatedData, truncatedFields);
1717
+ const truncationNote = buildTruncationNote(
1718
+ totalRecords,
1719
+ recordsShown,
1720
+ truncatedFields,
1721
+ maxCharsPerField,
1722
+ toolName
1723
+ );
1724
+ return {
1725
+ toolName,
1726
+ summary: {
1727
+ totalRecords,
1728
+ recordsShown,
1729
+ schema: schema2
1730
+ },
1731
+ data: truncatedData,
1732
+ truncationNote
1733
+ };
1734
+ }
1735
+ if (typeof result === "object") {
1736
+ const objResult = result;
1737
+ const dataWrapperKeys = ["data", "results", "items", "records", "rows", "list"];
1738
+ for (const key of dataWrapperKeys) {
1739
+ if (Array.isArray(objResult[key])) {
1740
+ const innerData = objResult[key];
1741
+ const {
1742
+ data: truncatedData,
1743
+ totalRecords,
1744
+ recordsShown,
1745
+ truncatedFields: dataTruncatedFields
1746
+ } = truncateDataArray(innerData, {
1747
+ maxRows: effectiveMaxRows,
1748
+ maxCharsPerField
1749
+ });
1750
+ const {
1751
+ metadata,
1752
+ truncatedFields: metadataTruncatedFields
1753
+ } = extractMetadataFromObject(objResult, key, maxCharsPerField);
1754
+ const allTruncatedFields = /* @__PURE__ */ new Set([...dataTruncatedFields, ...metadataTruncatedFields]);
1755
+ const schema3 = extractSchema(truncatedData, dataTruncatedFields);
1756
+ const truncationNote2 = buildTruncationNote(
1757
+ totalRecords,
1758
+ recordsShown,
1759
+ allTruncatedFields,
1760
+ maxCharsPerField,
1761
+ toolName
1762
+ );
1763
+ const hasMetadata = Object.keys(metadata).length > 0;
1764
+ return {
1765
+ toolName,
1766
+ summary: {
1767
+ totalRecords,
1768
+ recordsShown,
1769
+ schema: schema3
1770
+ },
1771
+ ...hasMetadata && { metadata },
1772
+ data: truncatedData,
1773
+ truncationNote: truncationNote2
1774
+ };
1775
+ }
1776
+ }
1777
+ const { row: truncatedRow, truncatedFields } = truncateRow(objResult, maxCharsPerField);
1778
+ const schema2 = extractSchema([truncatedRow], truncatedFields);
1779
+ const truncationNote = truncatedFields.size > 0 ? `Fields truncated to ${maxCharsPerField} chars: ${Array.from(truncatedFields).join(", ")}.` : null;
1780
+ return {
1781
+ toolName,
1782
+ summary: {
1783
+ totalRecords: 1,
1784
+ recordsShown: 1,
1785
+ schema: schema2
1786
+ },
1787
+ data: [truncatedRow],
1788
+ truncationNote
1789
+ };
1790
+ }
1791
+ return {
1792
+ toolName,
1793
+ summary: {
1794
+ totalRecords: 1,
1795
+ recordsShown: 1,
1796
+ schema: [{ name: "result", type: inferFieldType(result) }]
1797
+ },
1798
+ data: [{ result }],
1799
+ truncationNote: null
1800
+ };
1801
+ }
1802
+ function formatResultAsString(formattedResult) {
1803
+ return JSON.stringify(formattedResult, null, 2);
1804
+ }
1805
+
1461
1806
  // src/handlers/data-request.ts
1462
1807
  function getQueryCacheKey(query) {
1463
1808
  if (typeof query === "string") {
@@ -1539,15 +1884,25 @@ async function handleDataRequest(data, collections, sendMessage) {
1539
1884
  }
1540
1885
  }
1541
1886
  if (uiBlock) {
1887
+ const formattedResult = formatToolResultForLLM(result, {
1888
+ maxRows: 3,
1889
+ // Only need a few sample rows for summary
1890
+ maxCharsPerField: 100
1891
+ // Short truncation for summary
1892
+ });
1542
1893
  const dataSummary = {
1543
1894
  _dataReceived: true,
1544
1895
  _timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1545
1896
  _collection: collection,
1546
1897
  _operation: op,
1547
- _recordCount: Array.isArray(result) ? result.length : result?.data?.length || result?.contacts?.length || result?.salesorders?.length || "unknown"
1898
+ _totalRecords: formattedResult.summary.totalRecords,
1899
+ _recordsShown: formattedResult.summary.recordsShown,
1900
+ _metadata: formattedResult.metadata,
1901
+ // Preserve totalItems, totalDeadstockItems, etc.
1902
+ _schema: formattedResult.summary.schema
1548
1903
  };
1549
1904
  uiBlock.setComponentData(dataSummary);
1550
- logger.info(`Updated UIBlock ${uiBlockId} with data summary from ${collection}.${op} (full data not stored to save memory)`);
1905
+ logger.info(`Updated UIBlock ${uiBlockId} with data summary from ${collection}.${op} (${formattedResult.summary.totalRecords} total records)`);
1551
1906
  } else {
1552
1907
  logger.warn(`UIBlock ${uiBlockId} not found in threads`);
1553
1908
  }
@@ -5964,97 +6319,335 @@ var ConversationSearch = {
5964
6319
  };
5965
6320
  var conversation_search_default = ConversationSearch;
5966
6321
 
5967
- // src/userResponse/base-llm.ts
5968
- var BaseLLM = class {
5969
- constructor(config) {
5970
- this.model = config?.model || this.getDefaultModel();
5971
- this.fastModel = config?.fastModel || this.getDefaultFastModel();
5972
- this.defaultLimit = config?.defaultLimit || 10;
5973
- this.apiKey = config?.apiKey;
5974
- this.modelStrategy = config?.modelStrategy || "fast";
5975
- this.conversationSimilarityThreshold = config?.conversationSimilarityThreshold || 0.8;
6322
+ // src/userResponse/prompt-extractor.ts
6323
+ function extractPromptText(content) {
6324
+ if (content === null || content === void 0) {
6325
+ return "";
5976
6326
  }
5977
- /**
5978
- * Get the appropriate model based on task type and model strategy
5979
- * @param taskType - 'complex' for text generation/matching, 'simple' for classification/actions
5980
- * @returns The model string to use for this task
5981
- */
5982
- getModelForTask(taskType) {
5983
- switch (this.modelStrategy) {
5984
- case "best":
5985
- return this.model;
5986
- case "fast":
5987
- return this.fastModel;
5988
- case "balanced":
5989
- default:
5990
- return taskType === "complex" ? this.model : this.fastModel;
6327
+ if (typeof content === "string") {
6328
+ return content;
6329
+ }
6330
+ if (Array.isArray(content)) {
6331
+ return content.map((item) => extractContentBlockText(item)).filter((text) => text.length > 0).join("\n\n---\n\n");
6332
+ }
6333
+ if (content && typeof content === "object") {
6334
+ return extractObjectText(content);
6335
+ }
6336
+ return String(content);
6337
+ }
6338
+ function extractContentBlockText(item) {
6339
+ if (typeof item === "string") {
6340
+ return item;
6341
+ }
6342
+ if (item && typeof item === "object") {
6343
+ const obj = item;
6344
+ if (typeof obj.text === "string") {
6345
+ return obj.text;
5991
6346
  }
6347
+ if (typeof obj.content === "string") {
6348
+ return obj.content;
6349
+ }
6350
+ return JSON.stringify(item, null, 2);
6351
+ }
6352
+ return String(item);
6353
+ }
6354
+ function extractObjectText(obj) {
6355
+ if (typeof obj.text === "string") {
6356
+ return obj.text;
6357
+ }
6358
+ if (typeof obj.content === "string") {
6359
+ return obj.content;
6360
+ }
6361
+ return JSON.stringify(obj, null, 2);
6362
+ }
6363
+
6364
+ // src/userResponse/constants.ts
6365
+ var MAX_QUERY_VALIDATION_RETRIES = 3;
6366
+ var MAX_QUERY_ATTEMPTS = 6;
6367
+ var MAX_TOOL_ATTEMPTS = 3;
6368
+ var STREAM_FLUSH_INTERVAL_MS = 50;
6369
+ var PROGRESS_HEARTBEAT_INTERVAL_MS = 800;
6370
+ var STREAM_DELAY_MS = 50;
6371
+ var STREAM_IMMEDIATE_FLUSH_THRESHOLD = 100;
6372
+ var MAX_TOKENS_QUERY_FIX = 2048;
6373
+ var MAX_TOKENS_COMPONENT_MATCHING = 8192;
6374
+ var MAX_TOKENS_CLASSIFICATION = 1500;
6375
+ var MAX_TOKENS_ADAPTATION = 8192;
6376
+ var MAX_TOKENS_TEXT_RESPONSE = 4e3;
6377
+ var MAX_TOKENS_NEXT_QUESTIONS = 1200;
6378
+ var DEFAULT_MAX_ROWS_FOR_LLM = 10;
6379
+ var DEFAULT_MAX_CHARS_PER_FIELD2 = 500;
6380
+ var STREAM_PREVIEW_MAX_ROWS = 10;
6381
+ var STREAM_PREVIEW_MAX_CHARS = 200;
6382
+ var TOOL_TRACKING_MAX_ROWS = 5;
6383
+ var TOOL_TRACKING_MAX_CHARS = 200;
6384
+ var TOOL_TRACKING_SAMPLE_ROWS = 3;
6385
+ var MAX_COMPONENT_QUERY_LIMIT = 10;
6386
+ var EXACT_MATCH_SIMILARITY_THRESHOLD = 0.99;
6387
+ var DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD = 0.8;
6388
+ var MAX_TOOL_CALLING_ITERATIONS = 20;
6389
+ var KNOWLEDGE_BASE_TOP_K = 3;
6390
+
6391
+ // src/userResponse/stream-buffer.ts
6392
+ var StreamBuffer = class {
6393
+ constructor(callback) {
6394
+ this.buffer = "";
6395
+ this.flushTimer = null;
6396
+ this.fullText = "";
6397
+ this.callback = callback;
5992
6398
  }
5993
6399
  /**
5994
- * Set the model strategy at runtime
5995
- * @param strategy - 'best', 'fast', or 'balanced'
6400
+ * Check if the buffer has a callback configured
5996
6401
  */
5997
- setModelStrategy(strategy) {
5998
- this.modelStrategy = strategy;
5999
- logger.info(`[${this.getProviderName()}] Model strategy set to: ${strategy}`);
6402
+ hasCallback() {
6403
+ return !!this.callback;
6000
6404
  }
6001
6405
  /**
6002
- * Get the current model strategy
6003
- * @returns The current model strategy
6406
+ * Get all text that has been written (including already flushed)
6004
6407
  */
6005
- getModelStrategy() {
6006
- return this.modelStrategy;
6408
+ getFullText() {
6409
+ return this.fullText;
6007
6410
  }
6008
6411
  /**
6009
- * Set the conversation similarity threshold at runtime
6010
- * @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
6412
+ * Write a chunk to the buffer
6413
+ * Large chunks or chunks with newlines are flushed immediately
6414
+ * Small chunks are batched and flushed after a short interval
6415
+ *
6416
+ * @param chunk - Text chunk to write
6011
6417
  */
6012
- setConversationSimilarityThreshold(threshold) {
6013
- if (threshold < 0 || threshold > 1) {
6014
- logger.warn(`[${this.getProviderName()}] Invalid threshold ${threshold}, must be between 0 and 1. Using default 0.8`);
6015
- this.conversationSimilarityThreshold = 0.8;
6418
+ write(chunk) {
6419
+ this.fullText += chunk;
6420
+ if (!this.callback) {
6016
6421
  return;
6017
6422
  }
6018
- this.conversationSimilarityThreshold = threshold;
6019
- logger.info(`[${this.getProviderName()}] Conversation similarity threshold set to: ${threshold}`);
6423
+ this.buffer += chunk;
6424
+ if (chunk.includes("\n") || chunk.length > STREAM_IMMEDIATE_FLUSH_THRESHOLD) {
6425
+ this.flushNow();
6426
+ } else if (!this.flushTimer) {
6427
+ this.flushTimer = setTimeout(() => this.flushNow(), STREAM_FLUSH_INTERVAL_MS);
6428
+ }
6020
6429
  }
6021
6430
  /**
6022
- * Get the current conversation similarity threshold
6023
- * @returns The current threshold value
6431
+ * Flush the buffer immediately
6432
+ * Call this before tool execution or other operations that need clean output
6024
6433
  */
6025
- getConversationSimilarityThreshold() {
6026
- return this.conversationSimilarityThreshold;
6434
+ flush() {
6435
+ this.flushNow();
6027
6436
  }
6028
6437
  /**
6029
- * Get the API key (from instance, parameter, or environment)
6438
+ * Internal flush implementation
6030
6439
  */
6031
- getApiKey(apiKey) {
6032
- return apiKey || this.apiKey || this.getDefaultApiKey();
6440
+ flushNow() {
6441
+ if (this.flushTimer) {
6442
+ clearTimeout(this.flushTimer);
6443
+ this.flushTimer = null;
6444
+ }
6445
+ if (this.buffer && this.callback) {
6446
+ this.callback(this.buffer);
6447
+ this.buffer = "";
6448
+ }
6033
6449
  }
6034
6450
  /**
6035
- * Check if a component contains a Form (data_modification component)
6036
- * Forms have hardcoded defaultValues that become stale when cached
6037
- * This checks both single Form components and Forms inside MultiComponentContainer
6451
+ * Clean up resources
6452
+ * Call this when done with the buffer
6038
6453
  */
6039
- containsFormComponent(component) {
6040
- if (!component) return false;
6041
- if (component.type === "Form" || component.name === "DynamicForm") {
6042
- return true;
6454
+ dispose() {
6455
+ this.flush();
6456
+ this.callback = void 0;
6457
+ }
6458
+ };
6459
+ function streamDelay(ms = STREAM_DELAY_MS) {
6460
+ return new Promise((resolve) => setTimeout(resolve, ms));
6461
+ }
6462
+ async function withProgressHeartbeat(operation, progressMessage, streamBuffer, intervalMs = PROGRESS_HEARTBEAT_INTERVAL_MS) {
6463
+ if (!streamBuffer.hasCallback()) {
6464
+ return operation();
6465
+ }
6466
+ const startTime = Date.now();
6467
+ await streamDelay(30);
6468
+ streamBuffer.write(`\u23F3 ${progressMessage}`);
6469
+ const heartbeatInterval = setInterval(() => {
6470
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1e3);
6471
+ if (elapsedSeconds >= 1) {
6472
+ streamBuffer.write(` (${elapsedSeconds}s)`);
6473
+ }
6474
+ }, intervalMs);
6475
+ try {
6476
+ const result = await operation();
6477
+ return result;
6478
+ } finally {
6479
+ clearInterval(heartbeatInterval);
6480
+ streamBuffer.write("\n\n");
6481
+ }
6482
+ }
6483
+
6484
+ // src/userResponse/utils/component-props-processor.ts
6485
+ var NUMERIC_CONFIG_KEYS = ["yAxisKey", "valueKey", "aggregationField", "sizeKey"];
6486
+ var STRING_CONFIG_KEYS = ["xAxisKey", "nameKey", "labelKey", "groupBy"];
6487
+ var CONFIG_FIELDS_TO_VALIDATE = [
6488
+ "xAxisKey",
6489
+ "yAxisKey",
6490
+ "valueKey",
6491
+ "nameKey",
6492
+ "labelKey",
6493
+ "groupBy",
6494
+ "aggregationField",
6495
+ "seriesKey",
6496
+ "sizeKey",
6497
+ "xAggregationField",
6498
+ "yAggregationField"
6499
+ ];
6500
+ function findMatchingField(fieldName, configKey, validFieldNames, fieldTypes, providerName) {
6501
+ if (!fieldName) return null;
6502
+ const lowerField = fieldName.toLowerCase();
6503
+ const validFieldNamesLower = validFieldNames.map((n) => n.toLowerCase());
6504
+ const exactIdx = validFieldNamesLower.indexOf(lowerField);
6505
+ if (exactIdx !== -1) return validFieldNames[exactIdx];
6506
+ const containsMatches = validFieldNames.filter(
6507
+ (_, i) => validFieldNamesLower[i].includes(lowerField) || lowerField.includes(validFieldNamesLower[i])
6508
+ );
6509
+ if (containsMatches.length === 1) return containsMatches[0];
6510
+ if (NUMERIC_CONFIG_KEYS.includes(configKey)) {
6511
+ const numericFields = validFieldNames.filter((f) => fieldTypes[f] === "number");
6512
+ const match = numericFields.find(
6513
+ (f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase())
6514
+ );
6515
+ if (match) return match;
6516
+ if (numericFields.length > 0) {
6517
+ logger.warn(`[${providerName}] No match for "${fieldName}", using first numeric field: ${numericFields[0]}`);
6518
+ return numericFields[0];
6043
6519
  }
6044
- if (component.type === "Container" || component.name === "MultiComponentContainer") {
6045
- const nestedComponents = component.props?.config?.components || [];
6046
- for (const nested of nestedComponents) {
6047
- if (nested.type === "Form" || nested.name === "DynamicForm") {
6048
- return true;
6520
+ }
6521
+ if (STRING_CONFIG_KEYS.includes(configKey)) {
6522
+ const stringFields = validFieldNames.filter((f) => fieldTypes[f] === "string");
6523
+ const match = stringFields.find(
6524
+ (f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase())
6525
+ );
6526
+ if (match) return match;
6527
+ if (stringFields.length > 0) {
6528
+ logger.warn(`[${providerName}] No match for "${fieldName}", using first string field: ${stringFields[0]}`);
6529
+ return stringFields[0];
6530
+ }
6531
+ }
6532
+ logger.warn(`[${providerName}] No match for "${fieldName}", using first field: ${validFieldNames[0]}`);
6533
+ return validFieldNames[0];
6534
+ }
6535
+ function validateConfigFieldNames(config, outputSchema, providerName) {
6536
+ if (!outputSchema?.fields || !config) return config;
6537
+ const validFieldNames = outputSchema.fields.map((f) => f.name);
6538
+ const fieldTypes = outputSchema.fields.reduce((acc, f) => {
6539
+ acc[f.name] = f.type;
6540
+ return acc;
6541
+ }, {});
6542
+ const correctedConfig = { ...config };
6543
+ for (const configKey of CONFIG_FIELDS_TO_VALIDATE) {
6544
+ const fieldValue = correctedConfig[configKey];
6545
+ if (fieldValue && typeof fieldValue === "string") {
6546
+ if (!validFieldNames.includes(fieldValue)) {
6547
+ const correctedField = findMatchingField(fieldValue, configKey, validFieldNames, fieldTypes, providerName);
6548
+ if (correctedField) {
6549
+ logger.warn(`[${providerName}] Correcting config.${configKey}: "${fieldValue}" \u2192 "${correctedField}"`);
6550
+ correctedConfig[configKey] = correctedField;
6049
6551
  }
6050
6552
  }
6051
6553
  }
6052
- return false;
6554
+ }
6555
+ if (Array.isArray(correctedConfig.series)) {
6556
+ correctedConfig.series = correctedConfig.series.map((s) => {
6557
+ if (s.dataKey && typeof s.dataKey === "string" && !validFieldNames.includes(s.dataKey)) {
6558
+ const correctedField = findMatchingField(s.dataKey, "yAxisKey", validFieldNames, fieldTypes, providerName);
6559
+ if (correctedField) {
6560
+ logger.warn(`[${providerName}] Correcting series.dataKey: "${s.dataKey}" \u2192 "${correctedField}"`);
6561
+ return { ...s, dataKey: correctedField };
6562
+ }
6563
+ }
6564
+ return s;
6565
+ });
6566
+ }
6567
+ return correctedConfig;
6568
+ }
6569
+ function validateExternalTool(externalTool, executedTools, providerName) {
6570
+ if (!externalTool) {
6571
+ return { valid: true };
6572
+ }
6573
+ const toolId = externalTool.toolId;
6574
+ const validToolIds = (executedTools || []).map((t) => t.id);
6575
+ const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
6576
+ if (!isValidTool) {
6577
+ logger.warn(`[${providerName}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
6578
+ return { valid: false };
6579
+ }
6580
+ const executedTool = executedTools?.find((t) => t.id === toolId);
6581
+ return { valid: true, executedTool };
6582
+ }
6583
+ function validateAndCleanQuery(query, config) {
6584
+ if (!query) {
6585
+ return { query: null, wasModified: false };
6586
+ }
6587
+ let wasModified = false;
6588
+ let cleanedQuery = query;
6589
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
6590
+ if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
6591
+ logger.warn(`[${config.providerName}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
6592
+ return { query: null, wasModified: true };
6593
+ }
6594
+ const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
6595
+ if (fixed) {
6596
+ logger.warn(`[${config.providerName}] SQL fixes applied to component query: ${fixes.join("; ")}`);
6597
+ wasModified = true;
6598
+ if (typeof cleanedQuery === "string") {
6599
+ cleanedQuery = fixedQuery;
6600
+ } else if (cleanedQuery?.sql) {
6601
+ cleanedQuery = { ...cleanedQuery, sql: fixedQuery };
6602
+ }
6603
+ }
6604
+ if (typeof cleanedQuery === "string") {
6605
+ const limitedQuery = ensureQueryLimit(cleanedQuery, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6606
+ if (limitedQuery !== cleanedQuery) wasModified = true;
6607
+ cleanedQuery = limitedQuery;
6608
+ } else if (cleanedQuery?.sql) {
6609
+ const limitedSql = ensureQueryLimit(cleanedQuery.sql, config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6610
+ if (limitedSql !== cleanedQuery.sql) wasModified = true;
6611
+ cleanedQuery = { ...cleanedQuery, sql: limitedSql };
6612
+ }
6613
+ return { query: cleanedQuery, wasModified };
6614
+ }
6615
+ function processComponentProps(props, executedTools, config) {
6616
+ let cleanedProps = { ...props };
6617
+ if (cleanedProps.externalTool) {
6618
+ const { valid, executedTool } = validateExternalTool(
6619
+ cleanedProps.externalTool,
6620
+ executedTools,
6621
+ config.providerName
6622
+ );
6623
+ if (!valid) {
6624
+ cleanedProps.externalTool = null;
6625
+ } else if (executedTool?.outputSchema?.fields && cleanedProps.config) {
6626
+ cleanedProps.config = validateConfigFieldNames(
6627
+ cleanedProps.config,
6628
+ executedTool.outputSchema,
6629
+ config.providerName
6630
+ );
6631
+ }
6632
+ }
6633
+ if (cleanedProps.query) {
6634
+ const { query } = validateAndCleanQuery(cleanedProps.query, config);
6635
+ cleanedProps.query = query;
6636
+ }
6637
+ if (cleanedProps.query && cleanedProps.externalTool) {
6638
+ logger.info(`[${config.providerName}] Both query and externalTool exist, keeping both - frontend will decide`);
6639
+ }
6640
+ return cleanedProps;
6641
+ }
6642
+
6643
+ // src/userResponse/services/query-execution-service.ts
6644
+ var QueryExecutionService = class {
6645
+ constructor(config) {
6646
+ this.config = config;
6053
6647
  }
6054
6648
  /**
6055
- * Get the cache key for a query (the exact sql param that would be sent to execute)
6649
+ * Get the cache key for a query
6056
6650
  * This ensures the cache key matches what the frontend will send
6057
- * Used for both caching and internal deduplication
6058
6651
  */
6059
6652
  getQueryCacheKey(query) {
6060
6653
  if (typeof query === "string") {
@@ -6070,17 +6663,19 @@ var BaseLLM = class {
6070
6663
  return "";
6071
6664
  }
6072
6665
  /**
6073
- * Execute a query against the database for validation and caching
6666
+ * Execute a query against the database
6074
6667
  * @param query - The SQL query to execute (string or object with sql/values)
6075
6668
  * @param collections - Collections object containing database execute function
6076
6669
  * @returns Object with result data and cache key
6077
- * @throws Error if query execution fails
6078
6670
  */
6079
- async executeQueryForValidation(query, collections) {
6671
+ async executeQuery(query, collections) {
6080
6672
  const cacheKey = this.getQueryCacheKey(query);
6081
6673
  if (!cacheKey) {
6082
6674
  throw new Error("Invalid query format: expected string or object with sql property");
6083
6675
  }
6676
+ if (!collections?.["database"]?.["execute"]) {
6677
+ throw new Error("Database collection not registered. Please register database.execute collection to execute queries.");
6678
+ }
6084
6679
  const result = await collections["database"]["execute"]({ sql: cacheKey });
6085
6680
  return { result, cacheKey };
6086
6681
  }
@@ -6088,7 +6683,7 @@ var BaseLLM = class {
6088
6683
  * Request the LLM to fix a failed SQL query
6089
6684
  * @param failedQuery - The query that failed execution
6090
6685
  * @param errorMessage - The error message from the failed execution
6091
- * @param componentContext - Context about the component (name, type, title)
6686
+ * @param componentContext - Context about the component
6092
6687
  * @param apiKey - Optional API key
6093
6688
  * @returns Fixed query string
6094
6689
  */
@@ -6129,10 +6724,10 @@ Fixed SQL query:`;
6129
6724
  user: prompt
6130
6725
  },
6131
6726
  {
6132
- model: this.getModelForTask("simple"),
6133
- maxTokens: 2048,
6727
+ model: this.config.getModelForTask("simple"),
6728
+ maxTokens: MAX_TOKENS_QUERY_FIX,
6134
6729
  temperature: 0,
6135
- apiKey: this.getApiKey(apiKey)
6730
+ apiKey: this.config.getApiKey(apiKey)
6136
6731
  }
6137
6732
  );
6138
6733
  let fixedQuery = response.trim();
@@ -6142,81 +6737,656 @@ Fixed SQL query:`;
6142
6737
  return validatedQuery;
6143
6738
  }
6144
6739
  /**
6145
- * Match components from text response suggestions and generate follow-up questions
6146
- * Takes a text response with component suggestions (c1:type format) and matches with available components
6147
- * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
6148
- * All components are placed in a default MultiComponentContainer layout
6149
- * @param analysisContent - The text response containing component suggestions
6150
- * @param components - List of available components
6151
- * @param apiKey - Optional API key
6152
- * @param logCollector - Optional log collector
6153
- * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
6154
- * @returns Object containing matched components, layout title/description, and follow-up actions
6740
+ * Validate a single component's query with retry logic
6741
+ * @param component - The component to validate
6742
+ * @param collections - Collections object containing database execute function
6743
+ * @param apiKey - Optional API key for LLM calls
6744
+ * @param logCollector - Optional log collector for logging
6745
+ * @returns Validation result with component, query key, and result
6155
6746
  */
6156
- async matchComponentsFromAnalysis(analysisContent, components, userPrompt, apiKey, logCollector, componentStreamCallback, deferredTools, executedTools, collections, userId) {
6157
- const methodStartTime = Date.now();
6158
- const methodName = "matchComponentsFromAnalysis";
6159
- logger.info(`[${this.getProviderName()}] [TIMING] START ${methodName} | model: ${this.getModelForTask("complex")}`);
6160
- try {
6161
- logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
6162
- let availableComponentsText = "No components available";
6163
- if (components && components.length > 0) {
6164
- availableComponentsText = components.map((comp, idx) => {
6165
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
6166
- const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
6167
- return `${idx + 1}. ID: ${comp.id}
6168
- Name: ${comp.name}
6169
- Type: ${comp.type}
6170
- Description: ${comp.description || "No description"}
6171
- Keywords: ${keywords}
6172
- Props Structure: ${propsPreview}`;
6173
- }).join("\n\n");
6174
- }
6175
- let deferredToolsText = "No deferred external tools for this request.";
6176
- if (deferredTools && deferredTools.length > 0) {
6177
- logger.info(`[${this.getProviderName()}] Passing ${deferredTools.length} deferred tools to component matching`);
6178
- 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) => {
6179
- return `${idx + 1}. **${tool.name}**
6180
- toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
6181
- toolName: "${tool.name}"
6182
- parameters: ${JSON.stringify(tool.params || {})}
6183
- requiredFields:
6184
- ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
6185
- }).join("\n\n");
6186
- }
6187
- let executedToolsText = "No external tools were executed for data fetching.";
6188
- if (executedTools && executedTools.length > 0) {
6189
- logger.info(`[${this.getProviderName()}] Passing ${executedTools.length} executed tools to component matching`);
6190
- executedToolsText = "The following external tools were executed to fetch data.\n" + // '**IMPORTANT: For components displaying this data, use externalTool prop instead of query.**\n' +
6191
- // '**IMPORTANT: Use ONLY the field names listed in outputSchema for config keys.**\n\n' +
6192
- executedTools.map((tool, idx) => {
6193
- let outputSchemaText = "Not available";
6194
- let fieldNamesList = "";
6195
- let recordCount = "unknown";
6196
- if (tool.outputSchema) {
6197
- const fields = tool.outputSchema.fields || [];
6198
- recordCount = tool.result?._recordCount || (Array.isArray(tool.result) ? tool.result.length : "unknown");
6199
- const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
6200
- const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
6201
- fieldNamesList = `
6202
- \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
6203
- \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
6204
- const fieldsText = fields.map(
6205
- (f) => ` "${f.name}" (${f.type}): ${f.description}`
6206
- ).join("\n");
6207
- outputSchemaText = `${tool.outputSchema.description}
6208
- Fields:
6209
- ${fieldsText}`;
6210
- }
6211
- return `${idx + 1}. **${tool.name}**
6212
- toolId: "${tool.id}"
6213
- toolName: "${tool.name}"
6214
- parameters: ${JSON.stringify(tool.params || {})}
6215
- recordCount: ${recordCount} rows returned
6216
- outputSchema: ${outputSchemaText}${fieldNamesList}`;
6217
- }).join("\n\n");
6218
- }
6219
- const schemaDoc = schema.generateSchemaDocumentation();
6747
+ async validateSingleQuery(component, collections, apiKey, logCollector) {
6748
+ const query = component.props?.query;
6749
+ const originalQueryKey = this.getQueryCacheKey(query);
6750
+ const queryStr = typeof query === "string" ? query : query?.sql || "";
6751
+ let finalQueryKey = originalQueryKey;
6752
+ let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
6753
+ let currentQueryStr = queryStr;
6754
+ let validated = false;
6755
+ let lastError = "";
6756
+ let result = null;
6757
+ let attempts = 0;
6758
+ logger.info(`[${this.config.providerName}] Validating query for component: ${component.name} (${component.type})`);
6759
+ while (attempts < MAX_QUERY_VALIDATION_RETRIES && !validated) {
6760
+ attempts++;
6761
+ try {
6762
+ logger.debug(`[${this.config.providerName}] Query validation attempt ${attempts}/${MAX_QUERY_VALIDATION_RETRIES} for ${component.name}`);
6763
+ const validationResult = await this.executeQuery(currentQuery, collections);
6764
+ result = validationResult.result;
6765
+ validated = true;
6766
+ queryCache.set(validationResult.cacheKey, result);
6767
+ logger.info(`[${this.config.providerName}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
6768
+ logCollector?.info(`\u2713 Query validated for ${component.name}`);
6769
+ if (currentQueryStr !== queryStr) {
6770
+ const fixedQuery = typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr };
6771
+ component.props = {
6772
+ ...component.props,
6773
+ query: fixedQuery
6774
+ };
6775
+ finalQueryKey = this.getQueryCacheKey(fixedQuery);
6776
+ logger.info(`[${this.config.providerName}] Updated ${component.name} with fixed query`);
6777
+ }
6778
+ } catch (error) {
6779
+ lastError = error instanceof Error ? error.message : String(error);
6780
+ logger.warn(`[${this.config.providerName}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_QUERY_VALIDATION_RETRIES}): ${lastError}`);
6781
+ logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
6782
+ if (attempts >= MAX_QUERY_VALIDATION_RETRIES) {
6783
+ logger.error(`[${this.config.providerName}] \u2717 Max retries reached for ${component.name}, excluding from response`);
6784
+ logCollector?.error(`Max retries reached for ${component.name}, component excluded from response`);
6785
+ break;
6786
+ }
6787
+ logger.info(`[${this.config.providerName}] Requesting query fix from LLM for ${component.name}...`);
6788
+ logCollector?.info(`Requesting query fix for ${component.name}...`);
6789
+ try {
6790
+ const fixedQueryStr = await this.requestQueryFix(
6791
+ currentQueryStr,
6792
+ lastError,
6793
+ {
6794
+ name: component.name,
6795
+ type: component.type,
6796
+ title: component.props?.title
6797
+ },
6798
+ apiKey
6799
+ );
6800
+ if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
6801
+ logger.info(`[${this.config.providerName}] Received fixed query for ${component.name}, retrying...`);
6802
+ const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.config.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6803
+ currentQueryStr = limitedFixedQuery;
6804
+ if (typeof currentQuery === "string") {
6805
+ currentQuery = limitedFixedQuery;
6806
+ } else {
6807
+ currentQuery = { ...currentQuery, sql: limitedFixedQuery };
6808
+ }
6809
+ } else {
6810
+ logger.warn(`[${this.config.providerName}] LLM returned same or empty query, stopping retries`);
6811
+ break;
6812
+ }
6813
+ } catch (fixError) {
6814
+ const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6815
+ logger.error(`[${this.config.providerName}] Failed to get query fix from LLM: ${fixErrorMsg}`);
6816
+ break;
6817
+ }
6818
+ }
6819
+ }
6820
+ if (!validated) {
6821
+ logger.warn(`[${this.config.providerName}] Component ${component.name} excluded from response due to failed query validation`);
6822
+ logCollector?.warn(`Component ${component.name} excluded from response`);
6823
+ }
6824
+ return {
6825
+ component: validated ? component : null,
6826
+ queryKey: finalQueryKey,
6827
+ result,
6828
+ validated
6829
+ };
6830
+ }
6831
+ /**
6832
+ * Validate multiple component queries in parallel
6833
+ * @param components - Array of components with potential queries
6834
+ * @param collections - Collections object containing database execute function
6835
+ * @param apiKey - Optional API key for LLM calls
6836
+ * @param logCollector - Optional log collector for logging
6837
+ * @returns Object with validated components and query results map
6838
+ */
6839
+ async validateComponentQueries(components, collections, apiKey, logCollector) {
6840
+ const queryResults = /* @__PURE__ */ new Map();
6841
+ const validatedComponents = [];
6842
+ const componentsWithoutQuery = [];
6843
+ const componentsWithQuery = [];
6844
+ for (const component of components) {
6845
+ if (!component.props?.query) {
6846
+ componentsWithoutQuery.push(component);
6847
+ } else {
6848
+ componentsWithQuery.push(component);
6849
+ }
6850
+ }
6851
+ validatedComponents.push(...componentsWithoutQuery);
6852
+ if (componentsWithQuery.length === 0) {
6853
+ return { components: validatedComponents, queryResults };
6854
+ }
6855
+ logger.info(`[${this.config.providerName}] Validating ${componentsWithQuery.length} component queries in parallel...`);
6856
+ logCollector?.info(`Validating ${componentsWithQuery.length} component queries in parallel...`);
6857
+ const validationPromises = componentsWithQuery.map(
6858
+ (component) => this.validateSingleQuery(component, collections, apiKey, logCollector)
6859
+ );
6860
+ const results = await Promise.allSettled(validationPromises);
6861
+ for (let i = 0; i < results.length; i++) {
6862
+ const result = results[i];
6863
+ const component = componentsWithQuery[i];
6864
+ if (result.status === "fulfilled") {
6865
+ const { component: validatedComponent, queryKey, result: queryResult, validated } = result.value;
6866
+ if (validated && validatedComponent) {
6867
+ validatedComponents.push(validatedComponent);
6868
+ if (queryResult) {
6869
+ queryResults.set(queryKey, queryResult);
6870
+ queryResults.set(`${component.id}:${queryKey}`, queryResult);
6871
+ }
6872
+ }
6873
+ } else {
6874
+ logger.error(`[${this.config.providerName}] Unexpected error validating ${component.name}: ${result.reason}`);
6875
+ logCollector?.error(`Unexpected error validating ${component.name}: ${result.reason}`);
6876
+ }
6877
+ }
6878
+ logger.info(`[${this.config.providerName}] Parallel validation complete: ${validatedComponents.length}/${components.length} components validated`);
6879
+ return {
6880
+ components: validatedComponents,
6881
+ queryResults
6882
+ };
6883
+ }
6884
+ };
6885
+
6886
+ // src/userResponse/services/tool-executor-service.ts
6887
+ var ToolExecutorService = class {
6888
+ constructor(config) {
6889
+ this.queryAttempts = /* @__PURE__ */ new Map();
6890
+ this.toolAttempts = /* @__PURE__ */ new Map();
6891
+ this.executedToolsList = [];
6892
+ this.maxAttemptsReached = false;
6893
+ this.config = config;
6894
+ }
6895
+ /**
6896
+ * Reset state for a new execution
6897
+ */
6898
+ reset() {
6899
+ this.queryAttempts.clear();
6900
+ this.toolAttempts.clear();
6901
+ this.executedToolsList = [];
6902
+ this.maxAttemptsReached = false;
6903
+ }
6904
+ /**
6905
+ * Get list of successfully executed tools
6906
+ */
6907
+ getExecutedTools() {
6908
+ return this.executedToolsList;
6909
+ }
6910
+ /**
6911
+ * Check if max attempts were reached
6912
+ */
6913
+ isMaxAttemptsReached() {
6914
+ return this.maxAttemptsReached;
6915
+ }
6916
+ /**
6917
+ * Create a tool handler function for LLM.streamWithTools
6918
+ * @param externalTools - List of available external tools
6919
+ * @returns Tool handler function
6920
+ */
6921
+ createToolHandler(externalTools) {
6922
+ return async (toolName, toolInput) => {
6923
+ if (toolName === "execute_query") {
6924
+ return this.executeQuery(toolInput);
6925
+ } else {
6926
+ return this.executeExternalTool(toolName, toolInput, externalTools);
6927
+ }
6928
+ };
6929
+ }
6930
+ /**
6931
+ * Execute a SQL query with retry tracking and streaming feedback
6932
+ */
6933
+ async executeQuery(toolInput) {
6934
+ let sql = toolInput.sql;
6935
+ const params = toolInput.params || {};
6936
+ const reasoning = toolInput.reasoning;
6937
+ const { streamBuffer, collections, logCollector, providerName } = this.config;
6938
+ sql = ensureQueryLimit(sql, MAX_COMPONENT_QUERY_LIMIT, MAX_COMPONENT_QUERY_LIMIT);
6939
+ const queryKey = sql.toLowerCase().replace(/\s+/g, " ").trim();
6940
+ const attempts = (this.queryAttempts.get(queryKey) || 0) + 1;
6941
+ this.queryAttempts.set(queryKey, attempts);
6942
+ logger.info(`[${providerName}] Executing query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${sql.substring(0, 100)}...`);
6943
+ if (Object.keys(params).length > 0) {
6944
+ logger.info(`[${providerName}] Query params: ${JSON.stringify(params)}`);
6945
+ }
6946
+ if (reasoning) {
6947
+ logCollector?.info(`Query reasoning: ${reasoning}`);
6948
+ }
6949
+ if (attempts > MAX_QUERY_ATTEMPTS) {
6950
+ const errorMsg = `Maximum query attempts (${MAX_QUERY_ATTEMPTS}) reached. Unable to generate a valid query for your question.`;
6951
+ logger.error(`[${providerName}] ${errorMsg}`);
6952
+ logCollector?.error(errorMsg);
6953
+ this.maxAttemptsReached = true;
6954
+ if (streamBuffer.hasCallback()) {
6955
+ streamBuffer.write(`
6956
+
6957
+ \u274C ${errorMsg}
6958
+
6959
+ Please try rephrasing your question or simplifying your request.
6960
+
6961
+ `);
6962
+ }
6963
+ throw new Error(errorMsg);
6964
+ }
6965
+ try {
6966
+ streamBuffer.flush();
6967
+ if (streamBuffer.hasCallback()) {
6968
+ const paramsDisplay = Object.keys(params).length > 0 ? `
6969
+ **Parameters:** ${JSON.stringify(params)}` : "";
6970
+ if (attempts === 1) {
6971
+ streamBuffer.write(`
6972
+
6973
+ \u{1F50D} **Analyzing your question...**
6974
+
6975
+ `);
6976
+ await streamDelay();
6977
+ if (reasoning) {
6978
+ streamBuffer.write(`\u{1F4AD} ${reasoning}
6979
+
6980
+ `);
6981
+ await streamDelay();
6982
+ }
6983
+ streamBuffer.write(`\u{1F4DD} **Generated SQL Query:**
6984
+ \`\`\`sql
6985
+ ${sql}
6986
+ \`\`\`${paramsDisplay}
6987
+
6988
+ `);
6989
+ await streamDelay();
6990
+ } else {
6991
+ streamBuffer.write(`
6992
+
6993
+ \u{1F504} **Retrying with corrected query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS})...**
6994
+
6995
+ `);
6996
+ await streamDelay();
6997
+ if (reasoning) {
6998
+ streamBuffer.write(`\u{1F4AD} ${reasoning}
6999
+
7000
+ `);
7001
+ await streamDelay();
7002
+ }
7003
+ streamBuffer.write(`\u{1F4DD} **Corrected SQL Query:**
7004
+ \`\`\`sql
7005
+ ${sql}
7006
+ \`\`\`${paramsDisplay}
7007
+
7008
+ `);
7009
+ await streamDelay();
7010
+ }
7011
+ }
7012
+ logCollector?.logQuery?.(
7013
+ `Executing SQL query (attempt ${attempts})`,
7014
+ { sql, params },
7015
+ { reasoning, attempt: attempts }
7016
+ );
7017
+ if (!collections?.["database"]?.["execute"]) {
7018
+ throw new Error("Database collection not registered. Please register database.execute collection to execute queries.");
7019
+ }
7020
+ const queryPayload = Object.keys(params).length > 0 ? { sql: JSON.stringify({ sql, values: params }) } : { sql };
7021
+ const result = await withProgressHeartbeat(
7022
+ () => collections["database"]["execute"](queryPayload),
7023
+ "Executing database query",
7024
+ streamBuffer
7025
+ );
7026
+ const data = result?.data || result;
7027
+ const rowCount = result?.count ?? (Array.isArray(data) ? data.length : "N/A");
7028
+ logger.info(`[${providerName}] Query executed successfully, rows returned: ${rowCount}`);
7029
+ logCollector?.info(`Query successful, returned ${rowCount} rows`);
7030
+ if (streamBuffer.hasCallback()) {
7031
+ streamBuffer.write(`
7032
+ \u2705 **Query executed successfully!**
7033
+
7034
+ `);
7035
+ await streamDelay();
7036
+ if (Array.isArray(data) && data.length > 0) {
7037
+ const firstRow = data[0];
7038
+ const columns = Object.keys(firstRow);
7039
+ if (data.length === 1 && columns.length === 1) {
7040
+ const value = firstRow[columns[0]];
7041
+ streamBuffer.write(`**Result:** ${value}
7042
+
7043
+ `);
7044
+ await streamDelay();
7045
+ } else if (data.length > 0) {
7046
+ streamBuffer.write(`**Retrieved ${rowCount} rows**
7047
+
7048
+ `);
7049
+ await streamDelay();
7050
+ const streamPreview = formatQueryResultForLLM(data, {
7051
+ maxRows: STREAM_PREVIEW_MAX_ROWS,
7052
+ maxCharsPerField: STREAM_PREVIEW_MAX_CHARS
7053
+ });
7054
+ streamBuffer.write(`<DataTable>${JSON.stringify(streamPreview.data)}</DataTable>
7055
+
7056
+ `);
7057
+ if (streamPreview.truncationNote) {
7058
+ streamBuffer.write(`*${streamPreview.truncationNote}*
7059
+
7060
+ `);
7061
+ }
7062
+ await streamDelay();
7063
+ }
7064
+ } else if (Array.isArray(data) && data.length === 0) {
7065
+ streamBuffer.write(`**No rows returned.**
7066
+
7067
+ `);
7068
+ await streamDelay();
7069
+ }
7070
+ streamBuffer.write(`\u{1F4CA} **Analyzing results...**
7071
+
7072
+ `);
7073
+ }
7074
+ const formattedResult = formatQueryResultForLLM(data, {
7075
+ maxRows: DEFAULT_MAX_ROWS_FOR_LLM,
7076
+ maxCharsPerField: DEFAULT_MAX_CHARS_PER_FIELD2
7077
+ });
7078
+ logger.info(`[${providerName}] Query result formatted: ${formattedResult.summary.recordsShown}/${formattedResult.summary.totalRecords} records`);
7079
+ if (formattedResult.truncationNote) {
7080
+ logger.info(`[${providerName}] Truncation: ${formattedResult.truncationNote}`);
7081
+ }
7082
+ return formatResultAsString(formattedResult);
7083
+ } catch (error) {
7084
+ const errorMsg = error instanceof Error ? error.message : String(error);
7085
+ logger.error(`[${providerName}] Query execution failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7086
+ logCollector?.error(`Query failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7087
+ userPromptErrorLogger.logSqlError(sql, error instanceof Error ? error : new Error(errorMsg), Object.keys(params).length > 0 ? Object.values(params) : void 0);
7088
+ if (streamBuffer.hasCallback()) {
7089
+ streamBuffer.write(`\u274C **Query execution failed:**
7090
+ \`\`\`
7091
+ ${errorMsg}
7092
+ \`\`\`
7093
+
7094
+ `);
7095
+ if (attempts < MAX_QUERY_ATTEMPTS) {
7096
+ streamBuffer.write(`\u{1F527} **Generating corrected query...**
7097
+
7098
+ `);
7099
+ }
7100
+ }
7101
+ throw new Error(`Query execution failed: ${errorMsg}`);
7102
+ }
7103
+ }
7104
+ /**
7105
+ * Execute an external tool with retry tracking and streaming feedback
7106
+ */
7107
+ async executeExternalTool(toolName, toolInput, externalTools) {
7108
+ const { streamBuffer, logCollector, providerName } = this.config;
7109
+ const externalTool = externalTools?.find((t) => t.id === toolName);
7110
+ if (!externalTool) {
7111
+ throw new Error(`Unknown tool: ${toolName}`);
7112
+ }
7113
+ const attempts = (this.toolAttempts.get(toolName) || 0) + 1;
7114
+ this.toolAttempts.set(toolName, attempts);
7115
+ logger.info(`[${providerName}] Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
7116
+ logCollector?.info(`Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
7117
+ if (attempts > MAX_TOOL_ATTEMPTS) {
7118
+ const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${externalTool.name}`;
7119
+ logger.error(`[${providerName}] ${errorMsg}`);
7120
+ logCollector?.error(errorMsg);
7121
+ if (streamBuffer.hasCallback()) {
7122
+ streamBuffer.write(`
7123
+
7124
+ \u274C ${errorMsg}
7125
+
7126
+ Please try rephrasing your request or contact support.
7127
+
7128
+ `);
7129
+ }
7130
+ throw new Error(errorMsg);
7131
+ }
7132
+ try {
7133
+ streamBuffer.flush();
7134
+ if (streamBuffer.hasCallback()) {
7135
+ if (attempts === 1) {
7136
+ streamBuffer.write(`
7137
+
7138
+ \u{1F517} **Executing ${externalTool.name}...**
7139
+
7140
+ `);
7141
+ } else {
7142
+ streamBuffer.write(`
7143
+
7144
+ \u{1F504} **Retrying ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...**
7145
+
7146
+ `);
7147
+ }
7148
+ await streamDelay();
7149
+ }
7150
+ const result = await withProgressHeartbeat(
7151
+ () => externalTool.fn(toolInput),
7152
+ `Running ${externalTool.name}`,
7153
+ streamBuffer
7154
+ );
7155
+ logger.info(`[${providerName}] External tool ${externalTool.name} executed successfully`);
7156
+ logCollector?.info(`\u2713 ${externalTool.name} executed successfully`);
7157
+ if (!this.executedToolsList.find((t) => t.id === externalTool.id)) {
7158
+ const formattedForTracking = formatToolResultForLLM(result, {
7159
+ toolName: externalTool.name,
7160
+ toolLimit: externalTool.limit,
7161
+ maxRows: TOOL_TRACKING_MAX_ROWS,
7162
+ maxCharsPerField: TOOL_TRACKING_MAX_CHARS
7163
+ });
7164
+ this.executedToolsList.push({
7165
+ id: externalTool.id,
7166
+ name: externalTool.name,
7167
+ params: toolInput,
7168
+ result: {
7169
+ _totalRecords: formattedForTracking.summary.totalRecords,
7170
+ _recordsShown: formattedForTracking.summary.recordsShown,
7171
+ _metadata: formattedForTracking.metadata,
7172
+ _sampleData: formattedForTracking.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS)
7173
+ },
7174
+ outputSchema: externalTool.outputSchema
7175
+ });
7176
+ logger.info(`[${providerName}] Tracked executed tool: ${externalTool.name} with ${formattedForTracking.summary.totalRecords} total records`);
7177
+ }
7178
+ if (streamBuffer.hasCallback()) {
7179
+ streamBuffer.write(`\u2705 **${externalTool.name} completed successfully**
7180
+
7181
+ `);
7182
+ await streamDelay();
7183
+ }
7184
+ const formattedToolResult = formatToolResultForLLM(result, {
7185
+ toolName: externalTool.name,
7186
+ toolLimit: externalTool.limit,
7187
+ maxRows: DEFAULT_MAX_ROWS_FOR_LLM,
7188
+ maxCharsPerField: DEFAULT_MAX_CHARS_PER_FIELD2
7189
+ });
7190
+ logger.info(`[${providerName}] Tool result formatted: ${formattedToolResult.summary.recordsShown}/${formattedToolResult.summary.totalRecords} records`);
7191
+ if (formattedToolResult.truncationNote) {
7192
+ logger.info(`[${providerName}] Truncation: ${formattedToolResult.truncationNote}`);
7193
+ }
7194
+ return formatResultAsString(formattedToolResult);
7195
+ } catch (error) {
7196
+ const errorMsg = error instanceof Error ? error.message : String(error);
7197
+ logger.error(`[${providerName}] External tool ${externalTool.name} failed (attempt ${attempts}/${MAX_TOOL_ATTEMPTS}): ${errorMsg}`);
7198
+ logCollector?.error(`\u2717 ${externalTool.name} failed: ${errorMsg}`);
7199
+ userPromptErrorLogger.logToolError(externalTool.name, toolInput, error instanceof Error ? error : new Error(errorMsg));
7200
+ if (streamBuffer.hasCallback()) {
7201
+ streamBuffer.write(`\u274C **${externalTool.name} failed:**
7202
+ \`\`\`
7203
+ ${errorMsg}
7204
+ \`\`\`
7205
+
7206
+ `);
7207
+ if (attempts < MAX_TOOL_ATTEMPTS) {
7208
+ streamBuffer.write(`\u{1F527} **Retrying with adjusted parameters...**
7209
+
7210
+ `);
7211
+ }
7212
+ }
7213
+ throw new Error(`Tool execution failed: ${errorMsg}`);
7214
+ }
7215
+ }
7216
+ };
7217
+
7218
+ // src/userResponse/base-llm.ts
7219
+ var BaseLLM = class {
7220
+ constructor(config) {
7221
+ this.model = config?.model || this.getDefaultModel();
7222
+ this.fastModel = config?.fastModel || this.getDefaultFastModel();
7223
+ this.defaultLimit = config?.defaultLimit || 10;
7224
+ this.apiKey = config?.apiKey;
7225
+ this.modelStrategy = config?.modelStrategy || "fast";
7226
+ this.conversationSimilarityThreshold = config?.conversationSimilarityThreshold || DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD;
7227
+ this.queryService = new QueryExecutionService({
7228
+ defaultLimit: this.defaultLimit,
7229
+ getModelForTask: (taskType) => this.getModelForTask(taskType),
7230
+ getApiKey: (apiKey) => this.getApiKey(apiKey),
7231
+ providerName: this.getProviderName()
7232
+ });
7233
+ }
7234
+ /**
7235
+ * Get the appropriate model based on task type and model strategy
7236
+ * @param taskType - 'complex' for text generation/matching, 'simple' for classification/actions
7237
+ * @returns The model string to use for this task
7238
+ */
7239
+ getModelForTask(taskType) {
7240
+ switch (this.modelStrategy) {
7241
+ case "best":
7242
+ return this.model;
7243
+ case "fast":
7244
+ return this.fastModel;
7245
+ case "balanced":
7246
+ default:
7247
+ return taskType === "complex" ? this.model : this.fastModel;
7248
+ }
7249
+ }
7250
+ /**
7251
+ * Set the model strategy at runtime
7252
+ * @param strategy - 'best', 'fast', or 'balanced'
7253
+ */
7254
+ setModelStrategy(strategy) {
7255
+ this.modelStrategy = strategy;
7256
+ logger.info(`[${this.getProviderName()}] Model strategy set to: ${strategy}`);
7257
+ }
7258
+ /**
7259
+ * Get the current model strategy
7260
+ * @returns The current model strategy
7261
+ */
7262
+ getModelStrategy() {
7263
+ return this.modelStrategy;
7264
+ }
7265
+ /**
7266
+ * Set the conversation similarity threshold at runtime
7267
+ * @param threshold - Value between 0 and 1 (e.g., 0.8 = 80% similarity required)
7268
+ */
7269
+ setConversationSimilarityThreshold(threshold) {
7270
+ if (threshold < 0 || threshold > 1) {
7271
+ logger.warn(`[${this.getProviderName()}] Invalid threshold ${threshold}, must be between 0 and 1. Using default ${DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD}`);
7272
+ this.conversationSimilarityThreshold = DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD;
7273
+ return;
7274
+ }
7275
+ this.conversationSimilarityThreshold = threshold;
7276
+ logger.info(`[${this.getProviderName()}] Conversation similarity threshold set to: ${threshold}`);
7277
+ }
7278
+ /**
7279
+ * Get the current conversation similarity threshold
7280
+ * @returns The current threshold value
7281
+ */
7282
+ getConversationSimilarityThreshold() {
7283
+ return this.conversationSimilarityThreshold;
7284
+ }
7285
+ /**
7286
+ * Get the API key (from instance, parameter, or environment)
7287
+ */
7288
+ getApiKey(apiKey) {
7289
+ return apiKey || this.apiKey || this.getDefaultApiKey();
7290
+ }
7291
+ /**
7292
+ * Check if a component contains a Form (data_modification component)
7293
+ * Forms have hardcoded defaultValues that become stale when cached
7294
+ * This checks both single Form components and Forms inside MultiComponentContainer
7295
+ */
7296
+ containsFormComponent(component) {
7297
+ if (!component) return false;
7298
+ if (component.type === "Form" || component.name === "DynamicForm") {
7299
+ return true;
7300
+ }
7301
+ if (component.type === "Container" || component.name === "MultiComponentContainer") {
7302
+ const nestedComponents = component.props?.config?.components || [];
7303
+ for (const nested of nestedComponents) {
7304
+ if (nested.type === "Form" || nested.name === "DynamicForm") {
7305
+ return true;
7306
+ }
7307
+ }
7308
+ }
7309
+ return false;
7310
+ }
7311
+ /**
7312
+ * Match components from text response suggestions and generate follow-up questions
7313
+ * Takes a text response with component suggestions (c1:type format) and matches with available components
7314
+ * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
7315
+ * All components are placed in a default MultiComponentContainer layout
7316
+ * @param analysisContent - The text response containing component suggestions
7317
+ * @param components - List of available components
7318
+ * @param apiKey - Optional API key
7319
+ * @param logCollector - Optional log collector
7320
+ * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
7321
+ * @returns Object containing matched components, layout title/description, and follow-up actions
7322
+ */
7323
+ async matchComponentsFromAnalysis(analysisContent, components, userPrompt, apiKey, logCollector, componentStreamCallback, deferredTools, executedTools, collections, userId) {
7324
+ const methodStartTime = Date.now();
7325
+ const methodName = "matchComponentsFromAnalysis";
7326
+ logger.info(`[${this.getProviderName()}] [TIMING] START ${methodName} | model: ${this.getModelForTask("complex")}`);
7327
+ try {
7328
+ logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
7329
+ let availableComponentsText = "No components available";
7330
+ if (components && components.length > 0) {
7331
+ availableComponentsText = components.map((comp, idx) => {
7332
+ const keywords = comp.keywords ? comp.keywords.join(", ") : "";
7333
+ const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
7334
+ return `${idx + 1}. ID: ${comp.id}
7335
+ Name: ${comp.name}
7336
+ Type: ${comp.type}
7337
+ Description: ${comp.description || "No description"}
7338
+ Keywords: ${keywords}
7339
+ Props Structure: ${propsPreview}`;
7340
+ }).join("\n\n");
7341
+ }
7342
+ let deferredToolsText = "No deferred external tools for this request.";
7343
+ if (deferredTools && deferredTools.length > 0) {
7344
+ logger.info(`[${this.getProviderName()}] Passing ${deferredTools.length} deferred tools to component matching`);
7345
+ 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) => {
7346
+ return `${idx + 1}. **${tool.name}**
7347
+ toolId: "${tool.id}" (USE THIS EXACT VALUE - do not modify!)
7348
+ toolName: "${tool.name}"
7349
+ parameters: ${JSON.stringify(tool.params || {})}
7350
+ requiredFields:
7351
+ ${JSON.stringify(tool.requiredFields || [], null, 2)}`;
7352
+ }).join("\n\n");
7353
+ }
7354
+ let executedToolsText = "No external tools were executed for data fetching.";
7355
+ if (executedTools && executedTools.length > 0) {
7356
+ logger.info(`[${this.getProviderName()}] Passing ${executedTools.length} executed tools to component matching`);
7357
+ executedToolsText = "The following external tools were executed to fetch data.\n" + executedTools.map((tool, idx) => {
7358
+ let outputSchemaText = "Not available";
7359
+ let fieldNamesList = "";
7360
+ const recordCount = tool.result?._totalRecords ?? "unknown";
7361
+ let metadataText = "";
7362
+ if (tool.result?._metadata && Object.keys(tool.result._metadata).length > 0) {
7363
+ const metadataEntries = Object.entries(tool.result._metadata).map(([key, value]) => `${key}: ${value}`).join(", ");
7364
+ metadataText = `
7365
+ \u{1F4CB} METADATA: ${metadataEntries}`;
7366
+ }
7367
+ if (tool.outputSchema) {
7368
+ const fields = tool.outputSchema.fields || [];
7369
+ const numericFields = fields.filter((f) => f.type === "number").map((f) => f.name);
7370
+ const stringFields = fields.filter((f) => f.type === "string").map((f) => f.name);
7371
+ fieldNamesList = `
7372
+ \u{1F4CA} NUMERIC FIELDS (use for yAxisKey, valueKey, aggregationField): ${numericFields.join(", ") || "none"}
7373
+ \u{1F4DD} STRING FIELDS (use for xAxisKey, groupBy, nameKey): ${stringFields.join(", ") || "none"}`;
7374
+ const fieldsText = fields.map(
7375
+ (f) => ` "${f.name}" (${f.type}): ${f.description}`
7376
+ ).join("\n");
7377
+ outputSchemaText = `${tool.outputSchema.description}
7378
+ Fields:
7379
+ ${fieldsText}`;
7380
+ }
7381
+ return `${idx + 1}. **${tool.name}**
7382
+ toolId: "${tool.id}"
7383
+ toolName: "${tool.name}"
7384
+ parameters: ${JSON.stringify(tool.params || {})}
7385
+ recordCount: ${recordCount} rows returned${metadataText}
7386
+ outputSchema: ${outputSchemaText}${fieldNamesList}`;
7387
+ }).join("\n\n");
7388
+ }
7389
+ const schemaDoc = schema.generateSchemaDocumentation();
6220
7390
  const databaseRules = await promptLoader.loadDatabaseRules();
6221
7391
  let knowledgeBaseContext = "No additional knowledge base context available.";
6222
7392
  if (collections) {
@@ -6224,7 +7394,7 @@ ${fieldsText}`;
6224
7394
  prompt: userPrompt || analysisContent,
6225
7395
  collections,
6226
7396
  userId,
6227
- topK: 3
7397
+ topK: KNOWLEDGE_BASE_TOP_K
6228
7398
  });
6229
7399
  knowledgeBaseContext = kbResult.combinedContext || knowledgeBaseContext;
6230
7400
  }
@@ -6240,22 +7410,6 @@ ${fieldsText}`;
6240
7410
  CURRENT_DATETIME: getCurrentDateTimeForPrompt()
6241
7411
  });
6242
7412
  logger.debug(`[${this.getProviderName()}] Loaded match-text-components prompts`);
6243
- const extractPromptText = (content) => {
6244
- if (typeof content === "string") return content;
6245
- if (Array.isArray(content)) {
6246
- return content.map((item) => {
6247
- if (typeof item === "string") return item;
6248
- if (item && typeof item.text === "string") return item.text;
6249
- if (item && item.content && typeof item.content === "string") return item.content;
6250
- return JSON.stringify(item, null, 2);
6251
- }).join("\n\n---\n\n");
6252
- }
6253
- if (content && typeof content === "object") {
6254
- if (typeof content.text === "string") return content.text;
6255
- return JSON.stringify(content, null, 2);
6256
- }
6257
- return String(content);
6258
- };
6259
7413
  logger.logLLMPrompt("matchComponentsFromAnalysis", "system", extractPromptText(prompts.system));
6260
7414
  logger.logLLMPrompt("matchComponentsFromAnalysis", "user", `Text Analysis:
6261
7415
  ${analysisContent}
@@ -6332,23 +7486,32 @@ ${executedToolsText}`);
6332
7486
  { componentName: answerComponent.name, componentType: answerComponent.type, reasoning: answerComponentData.reasoning }
6333
7487
  );
6334
7488
  }
6335
- const answerQuery = answerComponent.props?.query;
7489
+ let answerQuery = answerComponent.props?.query;
6336
7490
  logger.info(`[${this.getProviderName()}] Answer component detected: ${answerComponent.name} (${answerComponent.type}), hasQuery: ${!!answerQuery}, hasDbExecute: ${!!collections?.["database"]?.["execute"]}`);
7491
+ if (answerQuery) {
7492
+ if (typeof answerQuery === "string") {
7493
+ answerQuery = ensureQueryLimit(answerQuery, this.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
7494
+ } else if (answerQuery?.sql) {
7495
+ const queryObj = answerQuery;
7496
+ answerQuery = { ...queryObj, sql: ensureQueryLimit(queryObj.sql, this.defaultLimit, MAX_COMPONENT_QUERY_LIMIT) };
7497
+ }
7498
+ answerComponent.props.query = answerQuery;
7499
+ }
6337
7500
  if (answerQuery && collections?.["database"]?.["execute"]) {
6338
7501
  (async () => {
6339
- const MAX_RETRIES = 3;
7502
+ const maxRetries = MAX_QUERY_VALIDATION_RETRIES;
6340
7503
  let attempts = 0;
6341
7504
  let validated = false;
6342
7505
  let currentQuery = answerQuery;
6343
7506
  let currentQueryStr = typeof answerQuery === "string" ? answerQuery : answerQuery?.sql || "";
6344
7507
  let lastError = "";
6345
7508
  logger.info(`[${this.getProviderName()}] Validating answer component query before streaming...`);
6346
- while (attempts < MAX_RETRIES && !validated) {
7509
+ while (attempts < maxRetries && !validated) {
6347
7510
  attempts++;
6348
7511
  try {
6349
- const cacheKey = this.getQueryCacheKey(currentQuery);
7512
+ const cacheKey = this.queryService.getQueryCacheKey(currentQuery);
6350
7513
  if (cacheKey) {
6351
- logger.debug(`[${this.getProviderName()}] Answer component query validation attempt ${attempts}/${MAX_RETRIES}`);
7514
+ logger.debug(`[${this.getProviderName()}] Answer component query validation attempt ${attempts}/${maxRetries}`);
6352
7515
  const result2 = await collections["database"]["execute"]({ sql: cacheKey });
6353
7516
  queryCache.set(cacheKey, result2);
6354
7517
  validated = true;
@@ -6363,11 +7526,11 @@ ${executedToolsText}`);
6363
7526
  }
6364
7527
  } catch (validationError) {
6365
7528
  lastError = validationError instanceof Error ? validationError.message : String(validationError);
6366
- logger.warn(`[${this.getProviderName()}] Answer component query validation failed (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6367
- if (attempts < MAX_RETRIES) {
7529
+ logger.warn(`[${this.getProviderName()}] Answer component query validation failed (attempt ${attempts}/${maxRetries}): ${lastError}`);
7530
+ if (attempts < maxRetries) {
6368
7531
  try {
6369
7532
  logger.info(`[${this.getProviderName()}] Requesting LLM to fix answer component query...`);
6370
- const fixedQueryStr = await this.requestQueryFix(
7533
+ const fixedQueryStr = await this.queryService.requestQueryFix(
6371
7534
  currentQueryStr,
6372
7535
  lastError,
6373
7536
  {
@@ -6377,7 +7540,7 @@ ${executedToolsText}`);
6377
7540
  },
6378
7541
  apiKey
6379
7542
  );
6380
- const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.defaultLimit, 10);
7543
+ const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.defaultLimit, MAX_COMPONENT_QUERY_LIMIT);
6381
7544
  if (typeof currentQuery === "string") {
6382
7545
  currentQuery = limitedFixedQuery;
6383
7546
  } else {
@@ -6418,7 +7581,7 @@ ${executedToolsText}`);
6418
7581
  },
6419
7582
  {
6420
7583
  model: this.getModelForTask("complex"),
6421
- maxTokens: 8192,
7584
+ maxTokens: MAX_TOKENS_COMPONENT_MATCHING,
6422
7585
  temperature: 0,
6423
7586
  apiKey: this.getApiKey(apiKey),
6424
7587
  partial: partialCallback
@@ -6454,137 +7617,14 @@ ${executedToolsText}`);
6454
7617
  logger.warn(`[${this.getProviderName()}] Component ${mc.componentId} not found in available components`);
6455
7618
  return null;
6456
7619
  }
6457
- let cleanedProps = { ...mc.props };
6458
- if (cleanedProps.externalTool) {
6459
- const toolId = cleanedProps.externalTool.toolId;
6460
- const validToolIds = (executedTools || []).map((t) => t.id);
6461
- const isValidTool = toolId && typeof toolId === "string" && validToolIds.includes(toolId);
6462
- if (!isValidTool) {
6463
- logger.warn(`[${this.getProviderName()}] externalTool.toolId "${toolId}" not found in executed tools [${validToolIds.join(", ")}], setting to null`);
6464
- cleanedProps.externalTool = null;
6465
- } else {
6466
- const executedTool = executedTools?.find((t) => t.id === toolId);
6467
- if (executedTool?.outputSchema?.fields && cleanedProps.config) {
6468
- const validFieldNames = executedTool.outputSchema.fields.map((f) => f.name);
6469
- const validFieldNamesLower = validFieldNames.map((n) => n.toLowerCase());
6470
- const findMatchingField = (fieldName, configKey) => {
6471
- if (!fieldName) return null;
6472
- const lowerField = fieldName.toLowerCase();
6473
- const exactIdx = validFieldNamesLower.indexOf(lowerField);
6474
- if (exactIdx !== -1) return validFieldNames[exactIdx];
6475
- const containsMatches = validFieldNames.filter(
6476
- (_, i) => validFieldNamesLower[i].includes(lowerField) || lowerField.includes(validFieldNamesLower[i])
6477
- );
6478
- if (containsMatches.length === 1) return containsMatches[0];
6479
- const fieldTypes = executedTool.outputSchema.fields.reduce((acc, f) => {
6480
- acc[f.name] = f.type;
6481
- return acc;
6482
- }, {});
6483
- const numericConfigKeys = ["yAxisKey", "valueKey", "aggregationField", "sizeKey"];
6484
- const stringConfigKeys = ["xAxisKey", "nameKey", "labelKey", "groupBy"];
6485
- if (numericConfigKeys.includes(configKey)) {
6486
- const numericFields = validFieldNames.filter((f) => fieldTypes[f] === "number");
6487
- const match = numericFields.find((f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase()));
6488
- if (match) return match;
6489
- if (numericFields.length > 0) {
6490
- logger.warn(`[${this.getProviderName()}] No match for "${fieldName}", using first numeric field: ${numericFields[0]}`);
6491
- return numericFields[0];
6492
- }
6493
- }
6494
- if (stringConfigKeys.includes(configKey)) {
6495
- const stringFields = validFieldNames.filter((f) => fieldTypes[f] === "string");
6496
- const match = stringFields.find((f) => f.toLowerCase().includes(lowerField) || lowerField.includes(f.toLowerCase()));
6497
- if (match) return match;
6498
- if (stringFields.length > 0) {
6499
- logger.warn(`[${this.getProviderName()}] No match for "${fieldName}", using first string field: ${stringFields[0]}`);
6500
- return stringFields[0];
6501
- }
6502
- }
6503
- logger.warn(`[${this.getProviderName()}] No match for "${fieldName}", using first field: ${validFieldNames[0]}`);
6504
- return validFieldNames[0];
6505
- };
6506
- const configFieldsToValidate = [
6507
- "xAxisKey",
6508
- "yAxisKey",
6509
- "valueKey",
6510
- "nameKey",
6511
- "labelKey",
6512
- "groupBy",
6513
- "aggregationField",
6514
- "seriesKey",
6515
- "sizeKey",
6516
- "xAggregationField",
6517
- "yAggregationField"
6518
- ];
6519
- for (const configKey of configFieldsToValidate) {
6520
- const fieldValue = cleanedProps.config[configKey];
6521
- if (fieldValue && typeof fieldValue === "string") {
6522
- if (!validFieldNames.includes(fieldValue)) {
6523
- const correctedField = findMatchingField(fieldValue, configKey);
6524
- if (correctedField) {
6525
- logger.warn(`[${this.getProviderName()}] Correcting config.${configKey}: "${fieldValue}" \u2192 "${correctedField}"`);
6526
- cleanedProps.config[configKey] = correctedField;
6527
- }
6528
- }
6529
- }
6530
- }
6531
- if (Array.isArray(cleanedProps.config.series)) {
6532
- cleanedProps.config.series = cleanedProps.config.series.map((s) => {
6533
- if (s.dataKey && typeof s.dataKey === "string" && !validFieldNames.includes(s.dataKey)) {
6534
- const correctedField = findMatchingField(s.dataKey, "yAxisKey");
6535
- if (correctedField) {
6536
- logger.warn(`[${this.getProviderName()}] Correcting series.dataKey: "${s.dataKey}" \u2192 "${correctedField}"`);
6537
- return { ...s, dataKey: correctedField };
6538
- }
6539
- }
6540
- return s;
6541
- });
6542
- }
6543
- }
6544
- }
6545
- }
6546
- if (cleanedProps.query) {
6547
- const queryStr = typeof cleanedProps.query === "string" ? cleanedProps.query : cleanedProps.query?.sql || "";
6548
- if (queryStr.includes("OPENJSON") || queryStr.includes("JSON_VALUE")) {
6549
- logger.warn(`[${this.getProviderName()}] Query contains OPENJSON/JSON_VALUE (invalid - cannot parse tool result), setting query to null`);
6550
- cleanedProps.query = null;
6551
- }
6552
- }
6553
- if (cleanedProps.query) {
6554
- const queryStr = typeof cleanedProps.query === "string" ? cleanedProps.query : cleanedProps.query?.sql || "";
6555
- const { query: fixedQuery, fixed, fixes } = validateAndFixSqlQuery(queryStr);
6556
- if (fixed) {
6557
- logger.warn(`[${this.getProviderName()}] SQL fixes applied to component query: ${fixes.join("; ")}`);
6558
- if (typeof cleanedProps.query === "string") {
6559
- cleanedProps.query = fixedQuery;
6560
- } else if (cleanedProps.query?.sql) {
6561
- cleanedProps.query.sql = fixedQuery;
6562
- }
6563
- }
6564
- }
6565
- if (cleanedProps.query) {
6566
- if (typeof cleanedProps.query === "string") {
6567
- cleanedProps.query = ensureQueryLimit(
6568
- cleanedProps.query,
6569
- this.defaultLimit,
6570
- 10
6571
- // maxLimit - enforce maximum of 10 rows for component queries
6572
- );
6573
- } else if (cleanedProps.query?.sql) {
6574
- cleanedProps.query = {
6575
- ...cleanedProps.query,
6576
- sql: ensureQueryLimit(
6577
- cleanedProps.query.sql,
6578
- this.defaultLimit,
6579
- 10
6580
- // maxLimit - enforce maximum of 10 rows for component queries
6581
- )
6582
- };
6583
- }
6584
- }
6585
- if (cleanedProps.query && cleanedProps.externalTool) {
6586
- logger.info(`[${this.getProviderName()}] Both query and externalTool exist, keeping both - frontend will decide`);
6587
- }
7620
+ const cleanedProps = processComponentProps(
7621
+ mc.props,
7622
+ executedTools,
7623
+ {
7624
+ providerName: this.getProviderName(),
7625
+ defaultLimit: this.defaultLimit
7626
+ }
7627
+ );
6588
7628
  return {
6589
7629
  ...originalComponent,
6590
7630
  props: {
@@ -6598,7 +7638,7 @@ ${executedToolsText}`);
6598
7638
  logger.info(`[${this.getProviderName()}] Starting query validation for ${finalComponents.length} components...`);
6599
7639
  logCollector?.info(`Validating queries for ${finalComponents.length} components...`);
6600
7640
  try {
6601
- const validationResult = await this.validateAndRetryComponentQueries(
7641
+ const validationResult = await this.queryService.validateComponentQueries(
6602
7642
  finalComponents,
6603
7643
  collections,
6604
7644
  apiKey,
@@ -6638,153 +7678,6 @@ ${executedToolsText}`);
6638
7678
  };
6639
7679
  }
6640
7680
  }
6641
- /**
6642
- * Validate a single component's query with retry logic
6643
- * @param component - The component to validate
6644
- * @param collections - Collections object containing database execute function
6645
- * @param apiKey - Optional API key for LLM calls
6646
- * @param logCollector - Optional log collector for logging
6647
- * @returns Object with validated component (or null if failed) and query result
6648
- */
6649
- async validateSingleComponentQuery(component, collections, apiKey, logCollector) {
6650
- const MAX_RETRIES = 3;
6651
- const query = component.props?.query;
6652
- const originalQueryKey = this.getQueryCacheKey(query);
6653
- const queryStr = typeof query === "string" ? query : query?.sql || "";
6654
- let finalQueryKey = originalQueryKey;
6655
- let currentQuery = typeof query === "string" ? query : { sql: query?.sql || "", values: query?.values, params: query?.params };
6656
- let currentQueryStr = queryStr;
6657
- let validated = false;
6658
- let lastError = "";
6659
- let result = null;
6660
- let attempts = 0;
6661
- logger.info(`[${this.getProviderName()}] Validating query for component: ${component.name} (${component.type})`);
6662
- while (attempts < MAX_RETRIES && !validated) {
6663
- attempts++;
6664
- try {
6665
- logger.debug(`[${this.getProviderName()}] Query validation attempt ${attempts}/${MAX_RETRIES} for ${component.name}`);
6666
- const validationResult = await this.executeQueryForValidation(currentQuery, collections);
6667
- result = validationResult.result;
6668
- validated = true;
6669
- queryCache.set(validationResult.cacheKey, result);
6670
- logger.info(`[${this.getProviderName()}] \u2713 Query validated for ${component.name} (attempt ${attempts}) - cached for frontend`);
6671
- logCollector?.info(`\u2713 Query validated for ${component.name}`);
6672
- if (currentQueryStr !== queryStr) {
6673
- const fixedQuery = typeof query === "string" ? currentQueryStr : { ...query, sql: currentQueryStr };
6674
- component.props = {
6675
- ...component.props,
6676
- query: fixedQuery
6677
- };
6678
- finalQueryKey = this.getQueryCacheKey(fixedQuery);
6679
- logger.info(`[${this.getProviderName()}] Updated ${component.name} with fixed query`);
6680
- }
6681
- } catch (error) {
6682
- lastError = error instanceof Error ? error.message : String(error);
6683
- logger.warn(`[${this.getProviderName()}] Query validation failed for ${component.name} (attempt ${attempts}/${MAX_RETRIES}): ${lastError}`);
6684
- logCollector?.warn(`Query validation failed for ${component.name}: ${lastError}`);
6685
- if (attempts >= MAX_RETRIES) {
6686
- logger.error(`[${this.getProviderName()}] \u2717 Max retries reached for ${component.name}, excluding from response`);
6687
- logCollector?.error(`Max retries reached for ${component.name}, component excluded from response`);
6688
- break;
6689
- }
6690
- logger.info(`[${this.getProviderName()}] Requesting query fix from LLM for ${component.name}...`);
6691
- logCollector?.info(`Requesting query fix for ${component.name}...`);
6692
- try {
6693
- const fixedQueryStr = await this.requestQueryFix(
6694
- currentQueryStr,
6695
- lastError,
6696
- {
6697
- name: component.name,
6698
- type: component.type,
6699
- title: component.props?.title
6700
- },
6701
- apiKey
6702
- );
6703
- if (fixedQueryStr && fixedQueryStr !== currentQueryStr) {
6704
- logger.info(`[${this.getProviderName()}] Received fixed query for ${component.name}, retrying...`);
6705
- const limitedFixedQuery = ensureQueryLimit(fixedQueryStr, this.defaultLimit, 10);
6706
- currentQueryStr = limitedFixedQuery;
6707
- if (typeof currentQuery === "string") {
6708
- currentQuery = limitedFixedQuery;
6709
- } else {
6710
- currentQuery = { ...currentQuery, sql: limitedFixedQuery };
6711
- }
6712
- } else {
6713
- logger.warn(`[${this.getProviderName()}] LLM returned same or empty query, stopping retries`);
6714
- break;
6715
- }
6716
- } catch (fixError) {
6717
- const fixErrorMsg = fixError instanceof Error ? fixError.message : String(fixError);
6718
- logger.error(`[${this.getProviderName()}] Failed to get query fix from LLM: ${fixErrorMsg}`);
6719
- break;
6720
- }
6721
- }
6722
- }
6723
- if (!validated) {
6724
- logger.warn(`[${this.getProviderName()}] Component ${component.name} excluded from response due to failed query validation`);
6725
- logCollector?.warn(`Component ${component.name} excluded from response`);
6726
- }
6727
- return {
6728
- component: validated ? component : null,
6729
- queryKey: finalQueryKey,
6730
- result,
6731
- validated
6732
- };
6733
- }
6734
- /**
6735
- * Validate component queries against the database and retry with LLM fixes if they fail
6736
- * Uses parallel execution for faster validation
6737
- * @param components - Array of components with potential queries
6738
- * @param collections - Collections object containing database execute function
6739
- * @param apiKey - Optional API key for LLM calls
6740
- * @param logCollector - Optional log collector for logging
6741
- * @returns Object with validated components and a map of query results
6742
- */
6743
- async validateAndRetryComponentQueries(components, collections, apiKey, logCollector) {
6744
- const queryResults = /* @__PURE__ */ new Map();
6745
- const validatedComponents = [];
6746
- const componentsWithoutQuery = [];
6747
- const componentsWithQuery = [];
6748
- for (const component of components) {
6749
- if (!component.props?.query) {
6750
- componentsWithoutQuery.push(component);
6751
- } else {
6752
- componentsWithQuery.push(component);
6753
- }
6754
- }
6755
- validatedComponents.push(...componentsWithoutQuery);
6756
- if (componentsWithQuery.length === 0) {
6757
- return { components: validatedComponents, queryResults };
6758
- }
6759
- logger.info(`[${this.getProviderName()}] Validating ${componentsWithQuery.length} component queries in parallel...`);
6760
- logCollector?.info(`Validating ${componentsWithQuery.length} component queries in parallel...`);
6761
- const validationPromises = componentsWithQuery.map(
6762
- (component) => this.validateSingleComponentQuery(component, collections, apiKey, logCollector)
6763
- );
6764
- const results = await Promise.allSettled(validationPromises);
6765
- for (let i = 0; i < results.length; i++) {
6766
- const result = results[i];
6767
- const component = componentsWithQuery[i];
6768
- if (result.status === "fulfilled") {
6769
- const { component: validatedComponent, queryKey, result: queryResult, validated } = result.value;
6770
- if (validated && validatedComponent) {
6771
- validatedComponents.push(validatedComponent);
6772
- if (queryResult) {
6773
- queryResults.set(queryKey, queryResult);
6774
- queryResults.set(`${component.id}:${queryKey}`, queryResult);
6775
- }
6776
- }
6777
- } else {
6778
- logger.error(`[${this.getProviderName()}] Unexpected error validating ${component.name}: ${result.reason}`);
6779
- logCollector?.error(`Unexpected error validating ${component.name}: ${result.reason}`);
6780
- }
6781
- }
6782
- logger.info(`[${this.getProviderName()}] Parallel validation complete: ${validatedComponents.length}/${components.length} components validated`);
6783
- return {
6784
- components: validatedComponents,
6785
- queryResults
6786
- };
6787
- }
6788
7681
  /**
6789
7682
  * Classify user question into category and detect external tools needed
6790
7683
  * Determines if question is for data analysis, requires external tools, or needs text response
@@ -6809,24 +7702,8 @@ ${executedToolsText}`);
6809
7702
  SCHEMA_DOC: schemaDoc || "No database schema available",
6810
7703
  CURRENT_DATETIME: getCurrentDateTimeForPrompt()
6811
7704
  });
6812
- const extractTextContent = (content) => {
6813
- if (typeof content === "string") return content;
6814
- if (Array.isArray(content)) {
6815
- return content.map((item) => {
6816
- if (typeof item === "string") return item;
6817
- if (item && typeof item.text === "string") return item.text;
6818
- if (item && item.content && typeof item.content === "string") return item.content;
6819
- return JSON.stringify(item, null, 2);
6820
- }).join("\n\n---\n\n");
6821
- }
6822
- if (content && typeof content === "object") {
6823
- if (typeof content.text === "string") return content.text;
6824
- return JSON.stringify(content, null, 2);
6825
- }
6826
- return String(content);
6827
- };
6828
- logger.logLLMPrompt("classifyQuestionCategory", "system", extractTextContent(prompts.system));
6829
- logger.logLLMPrompt("classifyQuestionCategory", "user", extractTextContent(prompts.user));
7705
+ logger.logLLMPrompt("classifyQuestionCategory", "system", extractPromptText(prompts.system));
7706
+ logger.logLLMPrompt("classifyQuestionCategory", "user", extractPromptText(prompts.user));
6830
7707
  const result = await LLM.stream(
6831
7708
  {
6832
7709
  sys: prompts.system,
@@ -6834,7 +7711,7 @@ ${executedToolsText}`);
6834
7711
  },
6835
7712
  {
6836
7713
  model: this.getModelForTask("simple"),
6837
- maxTokens: 1500,
7714
+ maxTokens: MAX_TOKENS_CLASSIFICATION,
6838
7715
  temperature: 0,
6839
7716
  apiKey: this.getApiKey(apiKey)
6840
7717
  },
@@ -6907,7 +7784,7 @@ ${executedToolsText}`);
6907
7784
  },
6908
7785
  {
6909
7786
  model: this.getModelForTask("complex"),
6910
- maxTokens: 8192,
7787
+ maxTokens: MAX_TOKENS_ADAPTATION,
6911
7788
  temperature: 0,
6912
7789
  apiKey: this.getApiKey(apiKey)
6913
7790
  },
@@ -7024,7 +7901,7 @@ ${executedToolsText}`);
7024
7901
  prompt: userPrompt,
7025
7902
  collections,
7026
7903
  userId,
7027
- topK: 3
7904
+ topK: KNOWLEDGE_BASE_TOP_K
7028
7905
  });
7029
7906
  const knowledgeBaseContext = kbResult.combinedContext;
7030
7907
  const prompts = await promptLoader.loadPrompts("text-response", {
@@ -7036,24 +7913,8 @@ ${executedToolsText}`);
7036
7913
  AVAILABLE_EXTERNAL_TOOLS: availableToolsDoc,
7037
7914
  CURRENT_DATETIME: getCurrentDateTimeForPrompt()
7038
7915
  });
7039
- const extractText = (content) => {
7040
- if (typeof content === "string") return content;
7041
- if (Array.isArray(content)) {
7042
- return content.map((item) => {
7043
- if (typeof item === "string") return item;
7044
- if (item && typeof item.text === "string") return item.text;
7045
- if (item && item.content && typeof item.content === "string") return item.content;
7046
- return JSON.stringify(item, null, 2);
7047
- }).join("\n\n---\n\n");
7048
- }
7049
- if (content && typeof content === "object") {
7050
- if (typeof content.text === "string") return content.text;
7051
- return JSON.stringify(content, null, 2);
7052
- }
7053
- return String(content);
7054
- };
7055
- logger.logLLMPrompt("generateTextResponse", "system", extractText(prompts.system));
7056
- logger.logLLMPrompt("generateTextResponse", "user", extractText(prompts.user));
7916
+ logger.logLLMPrompt("generateTextResponse", "system", extractPromptText(prompts.system));
7917
+ logger.logLLMPrompt("generateTextResponse", "user", extractPromptText(prompts.user));
7057
7918
  logger.debug(`[${this.getProviderName()}] Loaded text-response prompts with schema`);
7058
7919
  logger.debug(`[${this.getProviderName()}] System prompt length: ${prompts.system.length}, User prompt length: ${prompts.user.length}`);
7059
7920
  logCollector?.info("Generating text response with query execution capability...");
@@ -7165,324 +8026,24 @@ ${executedToolsText}`);
7165
8026
  logger.info(`[${this.getProviderName()}] Added ${addedToolIds.size} unique tool definitions from ${executableTools.length} tool calls (${externalTools.length - executableTools.length} deferred tools await form input)`);
7166
8027
  logger.info(`[${this.getProviderName()}] Complete tools array:`, JSON.stringify(tools, null, 2));
7167
8028
  }
7168
- const queryAttempts = /* @__PURE__ */ new Map();
7169
- const MAX_QUERY_ATTEMPTS = 6;
7170
- const toolAttempts = /* @__PURE__ */ new Map();
7171
- const MAX_TOOL_ATTEMPTS = 3;
7172
- const executedToolsList = [];
7173
- let maxAttemptsReached = false;
7174
- let fullStreamedText = "";
7175
- let streamBuffer = "";
7176
- let flushTimer = null;
7177
- const FLUSH_INTERVAL = 50;
7178
- const flushStreamBuffer = () => {
7179
- if (streamBuffer && streamCallback) {
7180
- streamCallback(streamBuffer);
7181
- streamBuffer = "";
7182
- }
7183
- flushTimer = null;
7184
- };
7185
- const wrappedStreamCallback = streamCallback ? (chunk) => {
7186
- fullStreamedText += chunk;
7187
- streamBuffer += chunk;
7188
- if (chunk.includes("\n") || chunk.length > 100) {
7189
- if (flushTimer) {
7190
- clearTimeout(flushTimer);
7191
- flushTimer = null;
7192
- }
7193
- flushStreamBuffer();
7194
- } else if (!flushTimer) {
7195
- flushTimer = setTimeout(flushStreamBuffer, FLUSH_INTERVAL);
7196
- }
7197
- } : void 0;
7198
- const flushStream = () => {
7199
- if (flushTimer) {
7200
- clearTimeout(flushTimer);
7201
- flushTimer = null;
7202
- }
7203
- flushStreamBuffer();
7204
- };
7205
- const streamDelay = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
7206
- const withProgressHeartbeat = async (operation, progressMessage, intervalMs = 1e3) => {
7207
- if (!wrappedStreamCallback) {
7208
- return operation();
7209
- }
7210
- const startTime = Date.now();
7211
- await streamDelay(30);
7212
- wrappedStreamCallback(`\u23F3 ${progressMessage}`);
7213
- const heartbeatInterval = setInterval(() => {
7214
- const elapsedSeconds = Math.floor((Date.now() - startTime) / 1e3);
7215
- if (elapsedSeconds >= 1) {
7216
- wrappedStreamCallback(` (${elapsedSeconds}s)`);
7217
- }
7218
- }, intervalMs);
7219
- try {
7220
- const result2 = await operation();
7221
- return result2;
7222
- } finally {
7223
- clearInterval(heartbeatInterval);
7224
- wrappedStreamCallback("\n\n");
7225
- }
7226
- };
7227
- const toolHandler = async (toolName, toolInput) => {
7228
- if (toolName === "execute_query") {
7229
- let sql = toolInput.sql;
7230
- const params = toolInput.params || {};
7231
- const reasoning = toolInput.reasoning;
7232
- sql = ensureQueryLimit(sql, 10, 10);
7233
- const queryKey = sql.toLowerCase().replace(/\s+/g, " ").trim();
7234
- const attempts = (queryAttempts.get(queryKey) || 0) + 1;
7235
- queryAttempts.set(queryKey, attempts);
7236
- logger.info(`[${this.getProviderName()}] Executing query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${sql.substring(0, 100)}...`);
7237
- if (Object.keys(params).length > 0) {
7238
- logger.info(`[${this.getProviderName()}] Query params: ${JSON.stringify(params)}`);
7239
- }
7240
- if (reasoning) {
7241
- logCollector?.info(`Query reasoning: ${reasoning}`);
7242
- }
7243
- if (attempts > MAX_QUERY_ATTEMPTS) {
7244
- const errorMsg = `Maximum query attempts (${MAX_QUERY_ATTEMPTS}) reached. Unable to generate a valid query for your question.`;
7245
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
7246
- logCollector?.error(errorMsg);
7247
- maxAttemptsReached = true;
7248
- if (wrappedStreamCallback) {
7249
- wrappedStreamCallback(`
7250
-
7251
- \u274C ${errorMsg}
7252
-
7253
- Please try rephrasing your question or simplifying your request.
7254
-
7255
- `);
7256
- }
7257
- throw new Error(errorMsg);
7258
- }
7259
- try {
7260
- flushStream();
7261
- if (wrappedStreamCallback) {
7262
- const paramsDisplay = Object.keys(params).length > 0 ? `
7263
- **Parameters:** ${JSON.stringify(params)}` : "";
7264
- if (attempts === 1) {
7265
- wrappedStreamCallback(`
7266
-
7267
- \u{1F50D} **Analyzing your question...**
7268
-
7269
- `);
7270
- await streamDelay(50);
7271
- if (reasoning) {
7272
- wrappedStreamCallback(`\u{1F4AD} ${reasoning}
7273
-
7274
- `);
7275
- await streamDelay(50);
7276
- }
7277
- wrappedStreamCallback(`\u{1F4DD} **Generated SQL Query:**
7278
- \`\`\`sql
7279
- ${sql}
7280
- \`\`\`${paramsDisplay}
7281
-
7282
- `);
7283
- await streamDelay(50);
7284
- } else {
7285
- wrappedStreamCallback(`
7286
-
7287
- \u{1F504} **Retrying with corrected query (attempt ${attempts}/${MAX_QUERY_ATTEMPTS})...**
7288
-
7289
- `);
7290
- await streamDelay(50);
7291
- if (reasoning) {
7292
- wrappedStreamCallback(`\u{1F4AD} ${reasoning}
7293
-
7294
- `);
7295
- await streamDelay(50);
7296
- }
7297
- wrappedStreamCallback(`\u{1F4DD} **Corrected SQL Query:**
7298
- \`\`\`sql
7299
- ${sql}
7300
- \`\`\`${paramsDisplay}
7301
-
7302
- `);
7303
- await streamDelay(50);
7304
- }
7305
- }
7306
- logCollector?.logQuery(
7307
- `Executing SQL query (attempt ${attempts})`,
7308
- { sql, params },
7309
- { reasoning, attempt: attempts }
7310
- );
7311
- if (!collections || !collections["database"] || !collections["database"]["execute"]) {
7312
- throw new Error("Database collection not registered. Please register database.execute collection to execute queries.");
7313
- }
7314
- const queryPayload = Object.keys(params).length > 0 ? { sql: JSON.stringify({ sql, values: params }) } : { sql };
7315
- const result2 = await withProgressHeartbeat(
7316
- () => collections["database"]["execute"](queryPayload),
7317
- "Executing database query",
7318
- 800
7319
- // Send heartbeat every 800ms for responsive feedback
7320
- );
7321
- const data = result2?.data || result2;
7322
- const rowCount = result2?.count ?? (Array.isArray(data) ? data.length : "N/A");
7323
- logger.info(`[${this.getProviderName()}] Query executed successfully, rows returned: ${rowCount}`);
7324
- logCollector?.info(`Query successful, returned ${rowCount} rows`);
7325
- if (wrappedStreamCallback) {
7326
- wrappedStreamCallback(`
7327
- \u2705 **Query executed successfully!**
7328
-
7329
- `);
7330
- await streamDelay(50);
7331
- if (Array.isArray(data) && data.length > 0) {
7332
- const firstRow = data[0];
7333
- const columns = Object.keys(firstRow);
7334
- if (data.length === 1 && columns.length === 1) {
7335
- const value = firstRow[columns[0]];
7336
- wrappedStreamCallback(`**Result:** ${value}
7337
-
7338
- `);
7339
- await streamDelay(50);
7340
- } else if (data.length > 0) {
7341
- wrappedStreamCallback(`**Retrieved ${rowCount} rows**
7342
-
7343
- `);
7344
- await streamDelay(50);
7345
- wrappedStreamCallback(`<DataTable>${JSON.stringify(data)}</DataTable>
7346
-
7347
- `);
7348
- await streamDelay(50);
7349
- }
7350
- } else if (Array.isArray(data) && data.length === 0) {
7351
- wrappedStreamCallback(`**No rows returned.**
7352
-
7353
- `);
7354
- await streamDelay(50);
7355
- }
7356
- wrappedStreamCallback(`\u{1F4CA} **Analyzing results...**
7357
-
7358
- `);
7359
- }
7360
- return JSON.stringify(data, null, 2);
7361
- } catch (error) {
7362
- const errorMsg = error instanceof Error ? error.message : String(error);
7363
- logger.error(`[${this.getProviderName()}] Query execution failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7364
- logCollector?.error(`Query failed (attempt ${attempts}/${MAX_QUERY_ATTEMPTS}): ${errorMsg}`);
7365
- userPromptErrorLogger.logSqlError(sql, error instanceof Error ? error : new Error(errorMsg), Object.keys(params).length > 0 ? Object.values(params) : void 0);
7366
- if (wrappedStreamCallback) {
7367
- wrappedStreamCallback(`\u274C **Query execution failed:**
7368
- \`\`\`
7369
- ${errorMsg}
7370
- \`\`\`
7371
-
7372
- `);
7373
- if (attempts < MAX_QUERY_ATTEMPTS) {
7374
- wrappedStreamCallback(`\u{1F527} **Generating corrected query...**
7375
-
7376
- `);
7377
- }
7378
- }
7379
- throw new Error(`Query execution failed: ${errorMsg}`);
7380
- }
7381
- } else {
7382
- const externalTool = externalTools?.find((t) => t.id === toolName);
7383
- if (externalTool) {
7384
- const attempts = (toolAttempts.get(toolName) || 0) + 1;
7385
- toolAttempts.set(toolName, attempts);
7386
- logger.info(`[${this.getProviderName()}] Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
7387
- logCollector?.info(`Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
7388
- if (attempts > MAX_TOOL_ATTEMPTS) {
7389
- const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${externalTool.name}`;
7390
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
7391
- logCollector?.error(errorMsg);
7392
- if (wrappedStreamCallback) {
7393
- wrappedStreamCallback(`
7394
-
7395
- \u274C ${errorMsg}
7396
-
7397
- Please try rephrasing your request or contact support.
7398
-
7399
- `);
7400
- }
7401
- throw new Error(errorMsg);
7402
- }
7403
- try {
7404
- flushStream();
7405
- if (wrappedStreamCallback) {
7406
- if (attempts === 1) {
7407
- wrappedStreamCallback(`
7408
-
7409
- \u{1F517} **Executing ${externalTool.name}...**
7410
-
7411
- `);
7412
- } else {
7413
- wrappedStreamCallback(`
7414
-
7415
- \u{1F504} **Retrying ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...**
7416
-
7417
- `);
7418
- }
7419
- await streamDelay(50);
7420
- }
7421
- const result2 = await withProgressHeartbeat(
7422
- () => externalTool.fn(toolInput),
7423
- `Running ${externalTool.name}`,
7424
- 800
7425
- // Send heartbeat every 800ms
7426
- );
7427
- logger.info(`[${this.getProviderName()}] External tool ${externalTool.name} executed successfully`);
7428
- logCollector?.info(`\u2713 ${externalTool.name} executed successfully`);
7429
- if (!executedToolsList.find((t) => t.id === externalTool.id)) {
7430
- let resultSummary = null;
7431
- if (result2) {
7432
- const resultStr = typeof result2 === "string" ? result2 : JSON.stringify(result2);
7433
- if (resultStr.length > 1e3) {
7434
- resultSummary = {
7435
- _preview: resultStr.substring(0, 1e3) + "... (truncated)",
7436
- _totalLength: resultStr.length,
7437
- _recordCount: Array.isArray(result2) ? result2.length : result2?.data?.length || result2?.contacts?.length || result2?.salesorders?.length || "unknown"
7438
- };
7439
- } else {
7440
- resultSummary = result2;
7441
- }
7442
- }
7443
- executedToolsList.push({
7444
- id: externalTool.id,
7445
- name: externalTool.name,
7446
- params: toolInput,
7447
- // The actual parameters used in this execution
7448
- result: resultSummary,
7449
- // Store summary instead of full result to save memory
7450
- outputSchema: externalTool.outputSchema
7451
- // Include output schema for component config generation
7452
- });
7453
- logger.info(`[${this.getProviderName()}] Tracked executed tool: ${externalTool.name} with params: ${JSON.stringify(toolInput)}`);
7454
- }
7455
- if (wrappedStreamCallback) {
7456
- wrappedStreamCallback(`\u2705 **${externalTool.name} completed successfully**
7457
-
7458
- `);
7459
- await streamDelay(50);
7460
- }
7461
- return JSON.stringify(result2, null, 2);
7462
- } catch (error) {
7463
- const errorMsg = error instanceof Error ? error.message : String(error);
7464
- logger.error(`[${this.getProviderName()}] External tool ${externalTool.name} failed (attempt ${attempts}/${MAX_TOOL_ATTEMPTS}): ${errorMsg}`);
7465
- logCollector?.error(`\u2717 ${externalTool.name} failed: ${errorMsg}`);
7466
- userPromptErrorLogger.logToolError(externalTool.name, toolInput, error instanceof Error ? error : new Error(errorMsg));
7467
- if (wrappedStreamCallback) {
7468
- wrappedStreamCallback(`\u274C **${externalTool.name} failed:**
7469
- \`\`\`
7470
- ${errorMsg}
7471
- \`\`\`
7472
-
7473
- `);
7474
- if (attempts < MAX_TOOL_ATTEMPTS) {
7475
- wrappedStreamCallback(`\u{1F527} **Retrying with adjusted parameters...**
7476
-
7477
- `);
7478
- }
7479
- }
7480
- throw new Error(`Tool execution failed: ${errorMsg}`);
7481
- }
7482
- }
7483
- throw new Error(`Unknown tool: ${toolName}`);
7484
- }
7485
- };
8029
+ const streamBuffer = new StreamBuffer(streamCallback);
8030
+ const toolExecutor = new ToolExecutorService({
8031
+ providerName: this.getProviderName(),
8032
+ collections,
8033
+ streamBuffer,
8034
+ logCollector
8035
+ });
8036
+ const executableExternalTools = externalTools?.map((t) => ({
8037
+ id: t.id,
8038
+ name: t.name,
8039
+ description: t.description,
8040
+ fn: t.fn,
8041
+ limit: t.limit,
8042
+ outputSchema: t.outputSchema,
8043
+ executionType: t.executionType,
8044
+ userProvidedData: t.userProvidedData
8045
+ })) || [];
8046
+ const toolHandler = toolExecutor.createToolHandler(executableExternalTools);
7486
8047
  const result = await LLM.streamWithTools(
7487
8048
  {
7488
8049
  sys: prompts.system,
@@ -7492,18 +8053,16 @@ ${errorMsg}
7492
8053
  toolHandler,
7493
8054
  {
7494
8055
  model: this.getModelForTask("complex"),
7495
- maxTokens: 4e3,
8056
+ maxTokens: MAX_TOKENS_TEXT_RESPONSE,
7496
8057
  temperature: 0,
7497
8058
  apiKey: this.getApiKey(apiKey),
7498
- partial: wrappedStreamCallback
7499
- // Pass the wrapped streaming callback to LLM
8059
+ partial: streamBuffer.hasCallback() ? (chunk) => streamBuffer.write(chunk) : void 0
7500
8060
  },
7501
- 20
7502
- // max iterations: allows for 6 query retries + 3 tool retries + final response + buffer
8061
+ MAX_TOOL_CALLING_ITERATIONS
7503
8062
  );
7504
8063
  logger.info(`[${this.getProviderName()}] Text response stream completed`);
7505
- const textResponse = fullStreamedText || result || "I apologize, but I was unable to generate a response.";
7506
- if (maxAttemptsReached) {
8064
+ const textResponse = streamBuffer.getFullText() || result || "I apologize, but I was unable to generate a response.";
8065
+ if (toolExecutor.isMaxAttemptsReached()) {
7507
8066
  const methodDuration2 = Date.now() - methodStartTime;
7508
8067
  logger.warn(`[${this.getProviderName()}] [TIMING] DONE ${methodName} in ${methodDuration2}ms | result: max attempts reached`);
7509
8068
  logCollector?.error("Failed to generate valid query after maximum attempts");
@@ -7526,10 +8085,10 @@ ${errorMsg}
7526
8085
  textLength: textResponse.length
7527
8086
  }
7528
8087
  );
7529
- flushStream();
7530
- if (wrappedStreamCallback && components && components.length > 0 && category !== "general") {
7531
- wrappedStreamCallback("\n\n\u{1F4CA} **Generating visualization components...**\n\n");
7532
- wrappedStreamCallback("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
8088
+ streamBuffer.flush();
8089
+ if (streamBuffer.hasCallback() && components && components.length > 0 && category !== "general") {
8090
+ streamBuffer.write("\n\n\u{1F4CA} **Generating visualization components...**\n\n");
8091
+ streamBuffer.write("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
7533
8092
  }
7534
8093
  let matchedComponents = [];
7535
8094
  let layoutTitle = "Dashboard";
@@ -7555,11 +8114,11 @@ ${errorMsg}
7555
8114
  logger.info(`[${this.getProviderName()}] Generated ${actions.length} follow-up actions for general question`);
7556
8115
  } else if (components && components.length > 0) {
7557
8116
  logger.info(`[${this.getProviderName()}] Matching components from text response...`);
7558
- logger.info(`[${this.getProviderName()}] componentStreamCallback setup: wrappedStreamCallback=${!!wrappedStreamCallback}, category=${category}`);
7559
- const componentStreamCallback = wrappedStreamCallback && category !== "data_modification" ? (component) => {
8117
+ logger.info(`[${this.getProviderName()}] componentStreamCallback setup: hasCallback=${streamBuffer.hasCallback()}, category=${category}`);
8118
+ const componentStreamCallback = streamBuffer.hasCallback() && category !== "data_modification" ? (component) => {
7560
8119
  logger.info(`[${this.getProviderName()}] componentStreamCallback INVOKED for: ${component.name} (${component.type})`);
7561
8120
  const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
7562
- wrappedStreamCallback(answerMarker);
8121
+ streamBuffer.write(answerMarker);
7563
8122
  logger.info(`[${this.getProviderName()}] Streamed answer component to frontend: ${component.name} (${component.type})`);
7564
8123
  } : void 0;
7565
8124
  logger.info(`[${this.getProviderName()}] componentStreamCallback created: ${!!componentStreamCallback}`);
@@ -7586,7 +8145,7 @@ ${errorMsg}
7586
8145
  logCollector,
7587
8146
  componentStreamCallback,
7588
8147
  deferredTools,
7589
- executedToolsList,
8148
+ toolExecutor.getExecutedTools(),
7590
8149
  collections,
7591
8150
  userId
7592
8151
  );
@@ -7670,7 +8229,7 @@ ${errorMsg}
7670
8229
  userPrompt,
7671
8230
  collections,
7672
8231
  userId,
7673
- similarityThreshold: 0.99
8232
+ similarityThreshold: EXACT_MATCH_SIMILARITY_THRESHOLD
7674
8233
  });
7675
8234
  if (conversationMatch) {
7676
8235
  logger.info(`[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`);
@@ -7685,7 +8244,7 @@ ${errorMsg}
7685
8244
  logger.info(`[${this.getProviderName()}] Skipping cached result - Form components contain stale defaultValues, fetching fresh data`);
7686
8245
  logCollector?.info("Skipping cache for form - fetching current values from database...");
7687
8246
  } else if (!component) {
7688
- if (conversationMatch.similarity >= 0.99) {
8247
+ if (conversationMatch.similarity >= EXACT_MATCH_SIMILARITY_THRESHOLD) {
7689
8248
  const elapsedTime2 = Date.now() - startTime;
7690
8249
  logger.info(`[${this.getProviderName()}] \u2713 Exact match for general question - returning cached text response`);
7691
8250
  logCollector?.info(`\u2713 Exact match for general question - returning cached response`);
@@ -7707,7 +8266,7 @@ ${errorMsg}
7707
8266
  logCollector?.info("Similar match found but was a general conversation - processing as new question");
7708
8267
  }
7709
8268
  } else {
7710
- if (conversationMatch.similarity >= 0.99) {
8269
+ if (conversationMatch.similarity >= EXACT_MATCH_SIMILARITY_THRESHOLD) {
7711
8270
  const elapsedTime2 = Date.now() - startTime;
7712
8271
  logger.info(`[${this.getProviderName()}] \u2713 100% match - returning UI block directly without adaptation`);
7713
8272
  logCollector?.info(`\u2713 Exact match (${(conversationMatch.similarity * 100).toFixed(2)}%) - returning cached result`);
@@ -7902,7 +8461,7 @@ ${errorMsg}
7902
8461
  },
7903
8462
  {
7904
8463
  model: this.getModelForTask("simple"),
7905
- maxTokens: 1200,
8464
+ maxTokens: MAX_TOKENS_NEXT_QUESTIONS,
7906
8465
  temperature: 0,
7907
8466
  apiKey: this.getApiKey(apiKey)
7908
8467
  },