@superatomai/sdk-node 0.0.13 → 0.0.15

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
@@ -522,6 +522,25 @@ var ReportsRequestMessageSchema = z3.object({
522
522
  type: z3.literal("REPORTS"),
523
523
  payload: ReportsRequestPayloadSchema
524
524
  });
525
+ var UIBlockSchema = z3.object({
526
+ id: z3.string().optional(),
527
+ userQuestion: z3.string().optional(),
528
+ text: z3.string().optional(),
529
+ textResponse: z3.string().optional(),
530
+ component: ComponentSchema.optional(),
531
+ // Legacy field
532
+ generatedComponentMetadata: ComponentSchema.optional(),
533
+ // Actual field used by UIBlock class
534
+ componentData: z3.record(z3.any()).optional(),
535
+ actions: z3.array(z3.any()).optional(),
536
+ isFetchingActions: z3.boolean().optional(),
537
+ createdAt: z3.string().optional(),
538
+ metadata: z3.object({
539
+ timestamp: z3.number().optional(),
540
+ userPrompt: z3.string().optional(),
541
+ similarity: z3.number().optional()
542
+ }).optional()
543
+ });
525
544
  var BookmarkDataSchema = z3.object({
526
545
  id: z3.number().optional(),
527
546
  uiblock: z3.any(),
@@ -530,9 +549,11 @@ var BookmarkDataSchema = z3.object({
530
549
  updated_at: z3.string().optional()
531
550
  });
532
551
  var BookmarksRequestPayloadSchema = z3.object({
533
- operation: z3.enum(["create", "update", "delete", "getAll", "getOne"]),
552
+ operation: z3.enum(["create", "update", "delete", "getAll", "getOne", "getByUser", "getByThread"]),
534
553
  data: z3.object({
535
554
  id: z3.number().optional(),
555
+ userId: z3.number().optional(),
556
+ threadId: z3.string().optional(),
536
557
  uiblock: z3.any().optional()
537
558
  }).optional()
538
559
  });
@@ -1360,7 +1381,7 @@ async function cleanupUserStorage() {
1360
1381
  }
1361
1382
 
1362
1383
  // src/auth/validator.ts
1363
- function validateUser(credentials) {
1384
+ async function validateUser(credentials, collections) {
1364
1385
  const { username, email, password } = credentials;
1365
1386
  const identifier = username || email;
1366
1387
  logger.debug("[validateUser] Starting user validation");
@@ -1372,7 +1393,39 @@ function validateUser(credentials) {
1372
1393
  error: "Username or email and password are required"
1373
1394
  };
1374
1395
  }
1375
- logger.debug(`[validateUser] Looking up user by identifier: ${identifier}`);
1396
+ if (collections && collections["users"] && collections["users"]["authenticate"]) {
1397
+ logger.debug(`[validateUser] Attempting database authentication for: ${identifier}`);
1398
+ try {
1399
+ const dbResult = await collections["users"]["authenticate"]({
1400
+ identifier,
1401
+ password
1402
+ });
1403
+ logger.info("[validateUser] Database authentication attempt completed", dbResult);
1404
+ if (dbResult && dbResult.success && dbResult.data) {
1405
+ logger.info(`[validateUser] \u2713 User authenticated via database: ${dbResult.data.username}`);
1406
+ return {
1407
+ success: true,
1408
+ data: dbResult.data.username,
1409
+ username: dbResult.data.username,
1410
+ userId: dbResult.data.id
1411
+ };
1412
+ } else {
1413
+ logger.debug(`[validateUser] Database auth failed for ${identifier}: ${dbResult?.error || "Invalid credentials"}`);
1414
+ if (dbResult && dbResult.error === "Invalid credentials") {
1415
+ return {
1416
+ success: false,
1417
+ error: "Invalid credentials"
1418
+ };
1419
+ }
1420
+ }
1421
+ } catch (error) {
1422
+ const errorMsg = error instanceof Error ? error.message : String(error);
1423
+ logger.debug(`[validateUser] Database lookup error: ${errorMsg}, falling back to file storage`);
1424
+ }
1425
+ } else {
1426
+ logger.debug("[validateUser] No users collection available, using file storage only");
1427
+ }
1428
+ logger.info(`[validateUser] Attempting file-based validation for: ${identifier}`);
1376
1429
  const user = findUserByUsernameOrEmail(identifier);
1377
1430
  if (!user) {
1378
1431
  logger.warn(`[validateUser] Validation failed: User not found - ${identifier}`);
@@ -1381,7 +1434,7 @@ function validateUser(credentials) {
1381
1434
  error: "Invalid username or email"
1382
1435
  };
1383
1436
  }
1384
- logger.debug(`[validateUser] User found: ${user.username}, verifying password`);
1437
+ logger.debug(`[validateUser] User found in file storage: ${user.username}, verifying password`);
1385
1438
  const hashedPassword = hashPassword(user.password);
1386
1439
  if (hashedPassword !== password) {
1387
1440
  logger.warn(`[validateUser] Validation failed: Invalid password for user - ${user.username}`);
@@ -1391,19 +1444,18 @@ function validateUser(credentials) {
1391
1444
  error: "Invalid password"
1392
1445
  };
1393
1446
  }
1394
- logger.info(`[validateUser] \u2713 User validated successfully: ${user.username}`);
1395
- logger.debug(`[validateUser] Returning user data for: ${user.username}`);
1447
+ logger.info(`[validateUser] \u2713 User validated via file storage: ${user.username}`);
1396
1448
  return {
1397
1449
  success: true,
1398
1450
  data: user.username,
1399
1451
  username: user.username
1400
1452
  };
1401
1453
  }
1402
- function authenticateAndStoreWsId(credentials, wsId) {
1454
+ async function authenticateAndStoreWsId(credentials, wsId, collections) {
1403
1455
  const identifier = credentials.username || credentials.email;
1404
1456
  logger.debug("[authenticateAndStoreWsId] Starting authentication and WebSocket ID storage");
1405
1457
  logger.debug("[authenticateAndStoreWsId] Validating user credentials");
1406
- const validationResult = validateUser(credentials);
1458
+ const validationResult = await validateUser(credentials, collections);
1407
1459
  if (!validationResult.success) {
1408
1460
  logger.warn(`[authenticateAndStoreWsId] User validation failed for: ${identifier}`);
1409
1461
  return validationResult;
@@ -1415,7 +1467,7 @@ function authenticateAndStoreWsId(credentials, wsId) {
1415
1467
  logger.debug(`[authenticateAndStoreWsId] WebSocket ID ${wsId} associated with user ${username}`);
1416
1468
  return validationResult;
1417
1469
  }
1418
- function verifyAuthToken(authToken) {
1470
+ async function verifyAuthToken(authToken, collections) {
1419
1471
  try {
1420
1472
  logger.debug("[verifyAuthToken] Starting token verification");
1421
1473
  logger.debug("[verifyAuthToken] Decoding base64 token");
@@ -1425,7 +1477,7 @@ function verifyAuthToken(authToken) {
1425
1477
  logger.debug("[verifyAuthToken] Token decoded and parsed successfully");
1426
1478
  logger.debug(`[verifyAuthToken] Token contains username: ${credentials.username ? "\u2713" : "\u2717"}`);
1427
1479
  logger.debug("[verifyAuthToken] Validating credentials from token");
1428
- const result = validateUser(credentials);
1480
+ const result = await validateUser(credentials, collections);
1429
1481
  if (result.success) {
1430
1482
  logger.info(`[verifyAuthToken] \u2713 Token verified successfully for user: ${credentials.username || "unknown"}`);
1431
1483
  } else {
@@ -1444,7 +1496,7 @@ function verifyAuthToken(authToken) {
1444
1496
  }
1445
1497
 
1446
1498
  // src/handlers/auth-login-requests.ts
1447
- async function handleAuthLoginRequest(data, sendMessage) {
1499
+ async function handleAuthLoginRequest(data, collections, sendMessage) {
1448
1500
  try {
1449
1501
  logger.debug("[AUTH_LOGIN_REQ] Parsing incoming auth login request");
1450
1502
  const authRequest = AuthLoginRequestMessageSchema.parse(data);
@@ -1503,12 +1555,12 @@ async function handleAuthLoginRequest(data, sendMessage) {
1503
1555
  }, sendMessage, wsId);
1504
1556
  return;
1505
1557
  }
1506
- logger.info(`[AUTH_LOGIN_REQ ${id}] Credentials validated, authenticating user: ${identifier}`);
1507
- logger.debug(`[AUTH_LOGIN_REQ ${id}] WebSocket ID: ${wsId}`);
1558
+ logger.info(`[AUTH_LOGIN_REQ ${id}] Credentials validated, authenticating user: ${identifier} username: ${username} email: ${email} password: ${password}`);
1508
1559
  logger.debug(`[AUTH_LOGIN_REQ ${id}] Calling authenticateAndStoreWsId for user: ${identifier}`);
1509
- const authResult = authenticateAndStoreWsId(
1560
+ const authResult = await authenticateAndStoreWsId(
1510
1561
  { username, email, password },
1511
- wsId
1562
+ wsId,
1563
+ collections
1512
1564
  );
1513
1565
  logger.info(`[AUTH_LOGIN_REQ ${id}] Authentication result for ${identifier}: ${authResult.success ? "success" : "failed"}`);
1514
1566
  if (!authResult.success) {
@@ -1560,7 +1612,7 @@ function sendDataResponse2(id, res, sendMessage, clientId) {
1560
1612
  }
1561
1613
 
1562
1614
  // src/handlers/auth-verify-request.ts
1563
- async function handleAuthVerifyRequest(data, sendMessage) {
1615
+ async function handleAuthVerifyRequest(data, collections, sendMessage) {
1564
1616
  try {
1565
1617
  logger.debug("[AUTH_VERIFY_REQ] Parsing incoming auth verify request");
1566
1618
  const authRequest = AuthVerifyRequestMessageSchema.parse(data);
@@ -1590,7 +1642,7 @@ async function handleAuthVerifyRequest(data, sendMessage) {
1590
1642
  logger.debug(`[AUTH_VERIFY_REQ ${id}] WebSocket ID: ${wsId}`);
1591
1643
  logger.debug(`[AUTH_VERIFY_REQ ${id}] Calling verifyAuthToken`);
1592
1644
  const startTime = Date.now();
1593
- const authResult = verifyAuthToken(token);
1645
+ const authResult = await verifyAuthToken(token, collections);
1594
1646
  const verificationTime = Date.now() - startTime;
1595
1647
  logger.info(`[AUTH_VERIFY_REQ ${id}] Token verification completed in ${verificationTime}ms - ${authResult.success ? "valid" : "invalid"}`);
1596
1648
  if (!authResult.success) {
@@ -1789,489 +1841,126 @@ import path3 from "path";
1789
1841
 
1790
1842
  // src/userResponse/prompts.ts
1791
1843
  var PROMPTS = {
1792
- "classify": {
1793
- system: `You are an expert AI that classifies user questions about data and determines the appropriate visualizations needed.
1794
-
1795
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
1796
-
1797
- ## Previous Conversation
1798
- {{CONVERSATION_HISTORY}}
1799
-
1800
- **Note:** If there is previous conversation history, use it to understand context. For example:
1801
- - If user previously asked about "sales" and now asks "show me trends", understand it refers to sales trends
1802
- - If user asked for "revenue by region" and now says "make it a pie chart", understand they want to modify the previous visualization
1803
- - Use the history to resolve ambiguous references like "that", "it", "them", "the data"
1804
-
1805
- Your task is to analyze the user's question and determine:
1806
-
1807
- 1. **Question Type:**
1808
- - "analytical": Questions asking to VIEW, ANALYZE, or VISUALIZE data
1809
-
1810
- - "data_modification": Questions asking to CREATE, UPDATE, DELETE, or MODIFY data
1811
-
1812
- - "general": General questions, greetings, or requests not related to data
1813
-
1814
- 2. **Required Visualizations** (only for analytical questions):
1815
- Determine which visualization type(s) would BEST answer the user's question:
1816
-
1817
- - **KPICard**: Single metric, total, count, average, percentage, or summary number
1818
-
1819
- - **LineChart**: Trends over time, time series, growth/decline patterns
1820
-
1821
-
1822
- - **BarChart**: Comparing categories, rankings, distributions across groups
1823
-
1824
-
1825
- - **PieChart**: Proportions, percentages, composition, market share
1826
-
1827
-
1828
- - **DataTable**: Detailed lists, multiple attributes, when user needs to see records
1829
-
1830
-
1831
- 3. **Multiple Visualizations:**
1832
- User may need MULTIPLE visualizations together:
1833
-
1834
- Common combinations:
1835
- - KPICard + LineChart
1836
- - KPICard + BarChart
1837
- - KPICard + DataTable
1838
- - BarChart + PieChart:
1839
- - LineChart + DataTable
1840
- Set needsMultipleComponents to true if user needs multiple views of the data.
1844
+ "text-response": {
1845
+ system: `You are an intelligent AI assistant that provides helpful, accurate, and contextual text responses to user questions. You have access to a database and can execute SQL queries and external tools to answer user requests.
1841
1846
 
1842
- **Important Guidelines:**
1843
- - If user explicitly mentions a chart type RESPECT that preference
1844
- - If question is vague or needs both summary and detail, suggest KPICard + DataTable
1845
- - Only return visualizations for "analytical" questions
1846
- - For "data_modification" or "general", return empty array for visualizations
1847
+ ## Your Task
1847
1848
 
1848
- **Output Format:**
1849
- {
1850
- "questionType": "analytical" | "data_modification" | "general",
1851
- "visualizations": ["KPICard", "LineChart", ...], // Empty array if not analytical
1852
- "reasoning": "Explanation of classification and visualization choices",
1853
- "needsMultipleComponents": boolean
1854
- }
1855
- `,
1856
- user: `{{USER_PROMPT}}
1857
- `
1858
- },
1859
- "match-component": {
1860
- system: `You are an expert AI assistant specialized in matching user requests to the most appropriate data visualization components.
1849
+ Analyze the user's question and provide a helpful text response. Your response should:
1861
1850
 
1862
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
1851
+ 1. **Be Clear and Concise**: Provide direct answers without unnecessary verbosity
1852
+ 2. **Be Contextual**: Use conversation history to understand what the user is asking about
1853
+ 3. **Be Accurate**: Provide factually correct information based on the context
1854
+ 4. **Be Helpful**: Offer additional relevant information or suggestions when appropriate
1863
1855
 
1864
- ## Previous Conversation
1865
- {{CONVERSATION_HISTORY}}
1856
+ ## Available Tools
1866
1857
 
1867
- **Context Instructions:**
1868
- - If there is conversation history, use it to understand what the user is referring to
1869
- - When user says "show that as a chart" or "change it", they are referring to a previous component
1870
- - If user asks to "modify" or "update" something, match to the component they previously saw
1871
- - Use context to resolve ambiguous requests like "show trends for that" or "make it interactive"
1872
-
1873
- Your task is to analyze the user's natural language request and find the BEST matching component from the available list.
1874
-
1875
- Available Components:
1876
- {{COMPONENTS_TEXT}}
1877
-
1878
- **Matching Guidelines:**
1879
-
1880
- 1. **Understand User Intent:**
1881
- - What type of data visualization do they need? (KPI/metric, chart, table, etc.)
1882
- - What metric or data are they asking about? (revenue, orders, customers, etc.)
1883
- - Are they asking for a summary (KPI), trend (line chart), distribution (bar/pie), or detailed list (table)?
1884
- - Do they want to compare categories, see trends over time, or show proportions?
1885
-
1886
- 2. **Component Type Matching:**
1887
- - KPICard: Single metric/number (total, average, count, percentage, rate)
1888
- - LineChart: Trends over time, time series data
1889
- - BarChart: Comparing categories, distributions, rankings
1890
- - PieChart/DonutChart: Proportions, percentages, market share
1891
- - DataTable: Detailed lists, rankings with multiple attributes
1892
-
1893
- 3. **Keyword & Semantic Matching:**
1894
- - Match user query terms with component keywords
1895
- - Consider synonyms (e.g., "sales" = "revenue", "items" = "products")
1896
- - Look for category matches (financial, orders, customers, products, suppliers, logistics, geographic, operations)
1897
-
1898
- 4. **Scoring Criteria:**
1899
- - Exact keyword matches: High priority
1900
- - Component type alignment: High priority
1901
- - Category alignment: Medium priority
1902
- - Semantic similarity: Medium priority
1903
- - Specificity: Prefer more specific components over generic ones
1904
-
1905
- **Output Requirements:**
1906
-
1907
- Respond with a JSON object containing:
1908
- - componentIndex: the 1-based index of the BEST matching component (or null if confidence < 50%)
1909
- - componentId: the ID of the matched component
1910
- - reasoning: detailed explanation of why this component was chosen
1911
- - confidence: confidence score 0-100 (100 = perfect match)
1912
- - alternativeMatches: array of up to 2 alternative component indices with scores (optional)
1913
-
1914
- Example response:
1915
- {
1916
- "componentIndex": 5,
1917
- "componentId": "total_revenue_kpi",
1918
- "reasoning": "User asks for 'total revenue' which perfectly matches the TotalRevenueKPI component (KPICard type) designed to show total revenue across all orders. Keywords match: 'total revenue', 'sales'.",
1919
- "confidence": 95,
1920
- "alternativeMatches": [
1921
- {"index": 3, "id": "monthly_revenue_kpi", "score": 75, "reason": "Could show monthly revenue if time period was intended"},
1922
- {"index": 8, "id": "revenue_trend_chart", "score": 60, "reason": "Could show revenue trend if historical view was intended"}
1923
- ]
1924
- }
1858
+ The following external tools are available for this request (if applicable):
1925
1859
 
1926
- **Important:**
1927
- - Only return componentIndex if confidence >= 50%
1928
- - Return null if no reasonable match exists
1929
- - Prefer components that exactly match the user's metric over generic ones
1930
- - Consider the full context of the request, not just individual words`,
1931
- user: `Current user request: {{USER_PROMPT}}
1860
+ {{AVAILABLE_EXTERNAL_TOOLS}}
1932
1861
 
1933
- Find the best matching component considering the conversation history above. Explain your reasoning with a confidence score. Return ONLY valid JSON.`
1934
- },
1935
- "modify-props": {
1936
- system: `You are an AI assistant that validates and modifies component props based on user requests.
1862
+ When a tool is needed to complete the user's request:
1863
+ 1. **Analyze the request** to determine which tool(s) are needed
1864
+ 2. **Extract parameters** from the user's question that the tool requires
1865
+ 3. **Execute the tool** by calling it with the extracted parameters
1866
+ 4. **Present the results** in your response in a clear, user-friendly format
1867
+ 5. **Combine with other data** if the user's request requires both database queries and external tool results
1937
1868
 
1938
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
1869
+ ## Handling Data Questions
1939
1870
 
1940
- Given:
1941
- - A user's natural language request
1942
- - Component name: {{COMPONENT_NAME}}
1943
- - Component type: {{COMPONENT_TYPE}}
1944
- - Component description: {{COMPONENT_DESCRIPTION}}
1871
+ When the user asks about data
1945
1872
 
1946
- -
1947
- - Current component props with structure:
1948
- {
1949
- query?: string, // SQL query to fetch data
1950
- title?: string, // Component title
1951
- description?: string, // Component description
1952
- config?: { // Additional configuration
1953
- [key: string]: any
1954
- }
1955
- }
1873
+ 1. **Generate a SQL query** using the database schema provided above
1874
+ 2. **Use the execute_query tool** to run the query
1875
+ 3. **If the query fails**, analyze the error and generate a corrected query
1876
+ 4. **Format the results** in a clear, readable way for the user
1956
1877
 
1957
- Schema definition for the prop that must be passed to the component
1958
- -{{CURRENT_PROPS}}
1878
+ **Query Guidelines:**
1879
+ - Use correct table and column names from the schema
1880
+ - ALWAYS include a LIMIT clause with a MAXIMUM of 32 rows
1881
+ - Ensure valid SQL syntax
1882
+ - For time-based queries, use appropriate date functions
1883
+ - When using subqueries with scalar operators (=, <, >, etc.), add LIMIT 1 to prevent "more than one row" errors
1959
1884
 
1960
- Database Schema:
1885
+ ## Database Schema
1961
1886
  {{SCHEMA_DOC}}
1962
1887
 
1963
- ## Previous Conversation
1964
- {{CONVERSATION_HISTORY}}
1965
-
1966
- **Context Instructions:**
1967
- - Review the conversation history to understand the evolution of the component
1968
- - If user says "add filter for X", understand they want to modify the current query
1969
- - If user says "change to last month" or "filter by Y", apply modifications to existing query
1970
- - Previous questions can clarify what the user means by ambiguous requests like "change that filter"
1971
- - Use context to determine appropriate time ranges if user says "recent" or "latest"
1972
-
1973
- Your task is to intelligently modify the props based on the user's request:
1888
+ **Database Type: PostgreSQL**
1974
1889
 
1975
- 1. **Query Modification**:
1976
- - Modify SQL query if user requests different data, filters, time ranges, limits, or aggregations
1977
- - Use correct table and column names from the schema
1978
- - Ensure valid SQL syntax
1979
- - ALWAYS include a LIMIT clause (default: {{DEFAULT_LIMIT}} rows) to prevent large result sets
1980
- - Preserve the query structure that the component expects (e.g., column aliases)
1890
+ **CRITICAL PostgreSQL Query Rules:**
1981
1891
 
1982
- **CRITICAL - PostgreSQL Query Rules:**
1892
+ 1. **NO AGGREGATE FUNCTIONS IN WHERE CLAUSE** - This is a fundamental SQL error
1893
+ \u274C WRONG: \`WHERE COUNT(orders) > 0\`
1894
+ \u274C WRONG: \`WHERE SUM(price) > 100\`
1895
+ \u274C WRONG: \`WHERE AVG(rating) > 4.5\`
1896
+ \u274C WRONG: \`WHERE FLOOR(AVG(rating)) = 4\` (aggregate inside any function is still not allowed)
1897
+ \u274C WRONG: \`WHERE ROUND(SUM(price), 2) > 100\`
1983
1898
 
1984
- **NO AGGREGATE FUNCTIONS IN WHERE CLAUSE:**
1985
- \u274C WRONG: \`WHERE COUNT(orders) > 0\` or \`WHERE SUM(price) > 100\`
1986
1899
  \u2705 CORRECT: Use HAVING (with GROUP BY), EXISTS, or subquery
1900
+ \u2705 CORRECT: Move aggregate logic to HAVING: \`GROUP BY ... HAVING FLOOR(AVG(rating)) = 4\`
1901
+ \u2705 CORRECT: Use subquery for filtering: \`WHERE product_id IN (SELECT product_id FROM ... GROUP BY ... HAVING AVG(rating) >= 4)\`
1987
1902
 
1988
-
1989
- **WHERE vs HAVING:**
1903
+ 2. **WHERE vs HAVING**
1990
1904
  - WHERE filters rows BEFORE grouping (cannot use aggregates)
1991
1905
  - HAVING filters groups AFTER grouping (can use aggregates)
1992
1906
  - If using HAVING, you MUST have GROUP BY
1993
1907
 
1994
- **Subquery Rules:**
1995
- - When using a subquery with scalar comparison operators (=, <, >, <=, >=, <>), the subquery MUST return exactly ONE row
1996
- - ALWAYS add \`LIMIT 1\` to scalar subqueries to prevent "more than one row returned" errors
1997
- - Example: \`WHERE location_id = (SELECT store_id FROM orders ORDER BY total_amount DESC LIMIT 1)\`
1998
- - For multiple values, use \`IN\` instead: \`WHERE location_id IN (SELECT store_id FROM orders)\`
1999
- - Test your subqueries mentally: if they could return multiple rows, add LIMIT 1 or use IN
2000
-
2001
- 2. **Title Modification**:
2002
- - Update title to reflect the user's specific request
2003
- - Keep it concise and descriptive
2004
- - Match the tone of the original title
2005
-
2006
- 3. **Description Modification**:
2007
- - Update description to explain what data is shown
2008
- - Be specific about filters, time ranges, or groupings applied
2009
-
2010
- 4. **Config Modification** (based on component type):
2011
- - For KPICard: formatter, gradient, icon
2012
- - For Charts: colors, height, xKey, yKey, nameKey, valueKey
2013
- - For Tables: columns, pageSize, formatters
2014
-
2015
-
2016
- Respond with a JSON object:
2017
- {
2018
- "props": { /* modified props object with query, title, description, config */ },
2019
- "isModified": boolean,
2020
- "reasoning": "brief explanation of changes",
2021
- "modifications": ["list of specific changes made"]
2022
- }
2023
-
2024
- IMPORTANT:
2025
- - Return the COMPLETE props object, not just modified fields
2026
- - Preserve the structure expected by the component type
2027
- - Ensure query returns columns with expected aliases
2028
- - Keep config properties that aren't affected by the request`,
2029
- user: `{{USER_PROMPT}}`
2030
- },
2031
- "single-component": {
2032
- system: `You are an expert AI assistant specialized in matching user requests to the most appropriate component from a filtered list.
2033
-
2034
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2035
-
2036
-
2037
- ## Previous Conversation
2038
- {{CONVERSATION_HISTORY}}
2039
-
2040
- **Context Instructions:**
2041
- - If there is previous conversation history, use it to understand what the user is referring to
2042
- - When user says "show trends", "add filters", "change that", understand they may be building on previous queries
2043
- - Use previous component types and queries as context to inform your current matching
2044
-
2045
- ## Available Components (Type: {{COMPONENT_TYPE}})
2046
- The following components have been filtered by type {{COMPONENT_TYPE}}. Select the BEST matching one:
2047
-
2048
- {{COMPONENTS_LIST}}
2049
-
2050
- {{VISUALIZATION_CONSTRAINT}}
2051
-
2052
- **Select the BEST matching component** from the available {{COMPONENT_TYPE}} components listed above that would best answer the user's question.
2053
-
2054
- **Matching Guidelines:**
2055
- 1. **Semantic Matching:**
2056
- - Match based on component name, description, and keywords
2057
- - Consider what metrics/data the user is asking about
2058
- - Look for semantic similarity (e.g., "sales" matches "revenue", "orders" matches "purchases")
2059
-
2060
- 2. **Query Relevance:**
2061
- - Consider the component's existing query structure
2062
- - Does it query the right tables/columns for the user's question?
2063
- - Can it be modified to answer the user's specific question?
2064
-
2065
- 3. **Scoring Criteria:**
2066
- - Exact keyword matches in name/description: High priority
2067
- - Semantic similarity to user intent: High priority
2068
- - Appropriate aggregation/grouping: Medium priority
2069
- - Category alignment: Medium priority
2070
-
2071
- **Output Requirements:**
2072
-
2073
- Respond with a JSON object:
2074
- {
2075
- "componentId": "matched_component_id",
2076
- "componentIndex": 1, // 1-based index from the filtered list above
2077
- "reasoning": "Detailed explanation of why this component best matches the user's question",
2078
- "confidence": 85, // Confidence score 0-100
2079
- "canGenerate": true // false if no suitable component found (confidence < 50)
2080
- }
2081
-
2082
- **Important:**
2083
- - Only set canGenerate to true if confidence >= 50%
2084
- - If no component from the list matches well (all have low relevance), set canGenerate to false
2085
- - Consider the full context of the request and conversation history
2086
- - The component's props (query, title, description, config) will be modified later based on the user's specific request
2087
- - Focus on finding the component that is closest to what the user needs, even if it needs modification`,
2088
- user: `{{USER_PROMPT}}
2089
-
2090
- `
2091
- },
2092
- "mutli-component": {
2093
- system: `You are an expert data analyst AI that creates comprehensive multi-component analytical dashboards with aesthetically pleasing and balanced layouts.
2094
-
2095
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2096
-
2097
- Database Schema:
2098
- {{SCHEMA_DOC}}
2099
-
2100
- ## Previous Conversation
2101
- {{CONVERSATION_HISTORY}}
2102
-
2103
- **Context Instructions:**
2104
- - Review the conversation history to understand what the user has asked before
2105
- - If user is building on previous insights (e.g., "now show me X and Y"), use context to inform dashboard design
2106
- - Previous queries can help determine appropriate filters, date ranges, or categories to use
2107
- - If user asks for "comprehensive view" or "dashboard for X", include complementary components based on context
2108
-
2109
- Given a user's analytical question and the required visualization types, your task is to:
2110
-
2111
- 1. **Determine Container Metadata:**
2112
- - title: Clear, descriptive title for the entire dashboard (2-5 words)
2113
- - description: Brief explanation of what insights this dashboard provides (1-2 sentences)
2114
-
2115
- 2. **Generate Props for Each Component:**
2116
- For each visualization type requested, create tailored props:
2117
-
2118
- - **query**: SQL query specific to this visualization using the database schema
2119
- * Use correct table and column names
2120
- * **DO NOT USE TOP keyword - use LIMIT instead (e.g., LIMIT 20, not TOP 20)**
2121
- * ALWAYS include LIMIT clause ONCE at the end (default: {{DEFAULT_LIMIT}})
2122
- * For KPICard: Return single row with column alias "value"
2123
- * For Charts: Return appropriate columns (name/label and value, or x and y)
2124
- * For Table: Return relevant columns
2125
-
2126
- - **title**: Specific title for this component (2-4 words)
2127
-
2128
- - **description**: What this specific component shows (1 sentence)
2129
-
2130
- - **config**: Type-specific configuration
2131
- * KPICard: { gradient, formatter, icon }
2132
- * BarChart: { xKey, yKey, colors, height }
2133
- * LineChart: { xKey, yKeys, colors, height }
2134
- * PieChart: { nameKey, valueKey, colors, height }
2135
- * DataTable: { pageSize }
2136
-
2137
- 3. **CRITICAL: Component Hierarchy and Ordering:**
2138
- The ORDER of components in the array MUST follow this STRICT hierarchy for proper visual layout:
2139
-
2140
- **HIERARCHY RULES (MUST FOLLOW IN THIS ORDER):**
2141
- 1. KPICards - ALWAYS FIRST (top of dashboard for summary metrics)
2142
- 2. Charts/Graphs - AFTER KPICards (middle of dashboard for visualizations)
2143
- * BarChart, LineChart, PieChart, DonutChart
2144
- 3. DataTable - ALWAYS LAST (bottom of dashboard, full width for detailed data)
2145
-
2146
- **LAYOUT BEHAVIOR (Frontend enforces):**
2147
- - KPICards: Display in responsive grid (3 columns)
2148
- - Single Chart (if only 1 chart): Takes FULL WIDTH
2149
- - Multiple Charts (if 2+ charts): Display in 2-column grid
2150
- - DataTable (if present): Always spans FULL WIDTH at bottom
2151
-
2152
-
2153
- **ABSOLUTELY DO NOT deviate from this hierarchy. Always place:**
2154
- - KPICards first
2155
- - Charts/Graphs second
2156
- - DataTable last (if present)
2157
-
2158
- **Important Guidelines:**
2159
- - Each component should answer a DIFFERENT aspect of the user's question
2160
- - Queries should be complementary, not duplicated
2161
- - If user asks "Show total revenue and trend", generate:
2162
- * KPICard: Single total value (FIRST)
2163
- * LineChart: Revenue over time (SECOND)
2164
- - Ensure queries use valid columns from the schema
2165
- - Make titles descriptive and specific to what each component shows
2166
- - **Snowflake Syntax MUST be used:**
2167
- * Use LIMIT (not TOP)
2168
- * Use DATE_TRUNC, DATEDIFF (not DATEPART)
2169
- * Include LIMIT only ONCE per query at the end
2170
-
2171
- **Output Format:**
2172
- {
2173
- "containerTitle": "Dashboard Title",
2174
- "containerDescription": "Brief description of the dashboard insights",
2175
- "components": [
2176
- {
2177
- "componentType": "KPICard" | "BarChart" | "LineChart" | "PieChart" | "DataTable",
2178
- "query": "SQL query",
2179
- "title": "Component title",
2180
- "description": "Component description",
2181
- "config": { /* type-specific config */ }
2182
- },
2183
- ...
2184
- ],
2185
- "reasoning": "Explanation of the dashboard design and component ordering",
2186
- "canGenerate": boolean
2187
- }`,
2188
- user: `Current user question: {{USER_PROMPT}}
2189
-
2190
- Required visualization types: {{VISUALIZATION_TYPES}}
2191
-
2192
- Generate a complete multi-component dashboard with appropriate container metadata and tailored props for each component. Consider the conversation history above when designing the dashboard. Return ONLY valid JSON.`
2193
- },
2194
- "container-metadata": {
2195
- system: `You are an expert AI assistant that generates titles and descriptions for multi-component dashboards.
2196
-
2197
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2198
-
2199
- ## Previous Conversation
2200
- {{CONVERSATION_HISTORY}}
2201
-
2202
- **Context Instructions:**
2203
- - If there is previous conversation history, use it to understand what the user is referring to
2204
- - Use context to create relevant titles and descriptions that align with the user's intent
2205
-
2206
- Your task is to generate a concise title and description for a multi-component dashboard that will contain the following visualization types:
2207
- {{VISUALIZATION_TYPES}}
2208
-
2209
- **Guidelines:**
2210
-
2211
- 1. **Title:**
2212
- - Should be clear and descriptive (3-8 words)
2213
- - Should reflect what the user is asking about
2214
- - Should NOT include "Dashboard" suffix (that will be added automatically)
2215
-
2216
- 2. **Description:**
2217
- - Should be a brief summary (1-2 sentences)
2218
- - Should explain what insights the dashboard provides
2219
-
2220
- **Output Requirements:**
2221
-
2222
- Respond with a JSON object:
2223
- {
2224
- "title": "Dashboard title without 'Dashboard' suffix",
2225
- "description": "Brief description of what this dashboard shows"
2226
- }
2227
-
2228
- **Important:**
2229
- - Keep the title concise and meaningful
2230
- - Make the description informative but brief
2231
- - Focus on what insights the user will gain
2232
- `,
2233
- user: `{{USER_PROMPT}}
2234
- `
2235
- },
2236
- "text-response": {
2237
- system: `You are an intelligent AI assistant that provides helpful, accurate, and contextual text responses to user questions.
2238
-
2239
- ## Your Task
2240
-
2241
- Analyze the user's question and provide a helpful text response. Your response should:
1908
+ 3. **NO NESTED AGGREGATE FUNCTIONS** - PostgreSQL does NOT allow aggregates inside aggregates
1909
+ \u274C WRONG: \`AVG(ROUND(AVG(column), 2))\` or \`SELECT AVG(SUM(price)) FROM ...\`
1910
+ \u2705 CORRECT: \`ROUND(AVG(column), 2)\`
2242
1911
 
2243
- 1. **Be Clear and Concise**: Provide direct answers without unnecessary verbosity
2244
- 2. **Be Contextual**: Use conversation history to understand what the user is asking about
2245
- 3. **Be Accurate**: Provide factually correct information based on the context
2246
- 4. **Be Helpful**: Offer additional relevant information or suggestions when appropriate
1912
+ 4. **GROUP BY Requirements**
1913
+ - ALL non-aggregated columns in SELECT must be in GROUP BY
1914
+ - If you SELECT a column and don't aggregate it, add it to GROUP BY
2247
1915
 
2248
- ## Handling Data Questions
1916
+ 5. **LIMIT Clause**
1917
+ - ALWAYS include LIMIT (max 32 rows)
1918
+ - For scalar subqueries in WHERE/HAVING, add LIMIT 1
2249
1919
 
2250
- When the user asks about data
1920
+ 6. **String Escaping** - PostgreSQL uses double single-quotes, NOT backslash
1921
+ \u274C WRONG: \`'Children\\'s furniture'\`
1922
+ \u2705 CORRECT: \`'Children''s furniture'\`
2251
1923
 
2252
- 1. **Generate a SQL query** using the database schema provided above
2253
- 2. **Use the execute_query tool** to run the query
2254
- 3. **If the query fails**, analyze the error and generate a corrected query
2255
- 4. **Format the results** in a clear, readable way for the user
1924
+ 7. **Always Use Table Aliases for Column References** - Prevent ambiguous column errors
1925
+ \u274C WRONG: \`SELECT product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
1926
+ \u2705 CORRECT: \`SELECT p.product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
1927
+ - Always prefix columns with table alias (e.g., \`p.product_id\`, \`c.name\`)
1928
+ - Especially critical in subqueries and joins where multiple tables share column names
2256
1929
 
2257
- **Query Guidelines:**
2258
- - Use correct table and column names from the schema
2259
- - ALWAYS include a LIMIT clause with a MAXIMUM of 32 rows
2260
- - Ensure valid SQL syntax
2261
- - For time-based queries, use appropriate date functions
2262
- - When using subqueries with scalar operators (=, <, >, etc.), add LIMIT 1 to prevent "more than one row" errors
2263
1930
 
2264
1931
  ## Response Guidelines
2265
1932
 
2266
- - If the question is about data, use the execute_query tool to fetch data and present it
1933
+ - If the question is about viewing data, use the execute_query tool to fetch data and present it
1934
+ - If the question is about creating/updating/deleting data:
1935
+ 1. Acknowledge that the system supports this via forms
1936
+ 2. **CRITICAL:** Use the database schema to determine which fields are required based on \`nullable\` property
1937
+ 3. **CRITICAL:** If the form will have select fields for foreign keys, you MUST fetch the options data using execute_query
1938
+ 4. **CRITICAL FOR UPDATE/DELETE OPERATIONS:** If it's an update/edit/modify/delete question:
1939
+ - **NEVER update ID/primary key columns** (e.g., order_id, customer_id, product_id) - these are immutable identifiers
1940
+ - You MUST first fetch the CURRENT values of the record using a SELECT query
1941
+ - Identify the record (from user's question - e.g., "update order 123" or "delete order 123" means order_id = 123)
1942
+ - Execute: \`SELECT * FROM table_name WHERE id = <value> LIMIT 1\`
1943
+ - Present the current values in your response (e.g., "Current order status: Pending, payment method: Credit Card")
1944
+ - For DELETE: These values will be shown in a disabled form as confirmation before deletion
1945
+ - For UPDATE: These values will populate as default values for editing
1946
+ 5. Present the options data in your response (e.g., "Available categories: Furniture (id: 1), Kitchen (id: 2), Decor (id: 3)")
1947
+ 6. The form component will be generated automatically using this data
2267
1948
  - If the question is general knowledge, provide a helpful conversational response
2268
1949
  - If asking for clarification, provide options or ask specific follow-up questions
2269
1950
  - If you don't have enough information, acknowledge it and ask for more details
2270
1951
  - Keep responses focused and avoid going off-topic
2271
1952
 
1953
+ **Example for data modification with foreign keys:**
1954
+ User: "I want to create a new product"
1955
+ You should:
1956
+ 1. Execute query: \`SELECT category_id, name FROM categories LIMIT 32\`
1957
+ 2. Execute query: \`SELECT store_id, name FROM stores LIMIT 32\`
1958
+ 3. Present: "I can help you create a new product. Available categories: Furniture (id: 1), Kitchen (id: 2)... Available stores: Store A (id: 10), Store B (id: 20)..."
1959
+ 4. Suggest Form component
1960
+
2272
1961
  ## Component Suggestions
2273
1962
 
2274
- After analyzing the query results, you MUST suggest appropriate dashboard components for displaying the data. Use this format:
1963
+ After analyzing the user's question, you MUST suggest appropriate dashboard components. Use this format:
2275
1964
 
2276
1965
  <DashboardComponents>
2277
1966
  **Dashboard Components:**
@@ -2279,12 +1968,22 @@ Format: \`{number}.{component_type} : {clear reasoning}\`
2279
1968
 
2280
1969
 
2281
1970
  **Rules for component suggestions:**
2282
- 1. Analyze the query results structure and data type
2283
- 2. Suggest components that would best visualize the data
2284
- 3. Each component suggestion must be on a new line
1971
+ 1. If a conclusive answer can be provided based on user question, suggest that as the first component.
1972
+ 2. ALways suggest context/supporting components that will give the user more information and allow them to explore further.
1973
+ 3. If the question includes a time range, also explore time-based components for past time ranges.
1974
+ 4. **For data viewing/analysis questions**: Suggest visualization components (KPICard, BarChart, LineChart, PieChart, DataTable, etc.).
1975
+ 5. **For data modification questions** (create/add/update/delete):
1976
+ - Always suggest 1-2 context components first to provide relevant information (prefer KPICard for showing key metrics)
1977
+ - Then suggest \`Form\` component for the actual modification
1978
+ - Example: "1.KPICard : Show current order total and status" then "2.Form : To update order details"
1979
+ 6. Analyze the query results structure and data type
1980
+ 7. Each component suggestion must be on a new line
2285
1981
  </DashboardComponents>
2286
1982
 
2287
- IMPORTANT: Always wrap component suggestions with <DashboardComponents> tags and include at least one component suggestion when data is returned.
1983
+ IMPORTANT:
1984
+ - Always wrap component suggestions with <DashboardComponents> tags
1985
+ - For data viewing: Include at least one component suggestion when data is returned
1986
+ - For data modifications: Always suggest 1-2 context components before Form (e.g., "1.KPICard : Show current order value" then "2.Form : To update order status")
2288
1987
 
2289
1988
  ## Output Format
2290
1989
 
@@ -2299,37 +1998,22 @@ Respond with plain text that includes:
2299
1998
  - Return ONLY plain text (no JSON, no markdown code blocks)
2300
1999
 
2301
2000
 
2302
- You have access to a database and can execute SQL queries to answer data-related questions.
2303
- ## Database Schema
2304
- {{SCHEMA_DOC}}
2305
-
2306
- **Database Type: PostgreSQL**
2001
+ You have access to a database and can execute SQL queries to answer data-related questions. For data modifications, the system provides form-based interfaces.
2307
2002
 
2308
- **CRITICAL PostgreSQL Query Rules:**
2309
-
2310
- 1. **NO AGGREGATE FUNCTIONS IN WHERE CLAUSE** - This is a fundamental SQL error
2311
- \u274C WRONG: \`WHERE COUNT(orders) > 0\`
2312
- \u274C WRONG: \`WHERE SUM(price) > 100\`
2313
- \u274C WRONG: \`WHERE AVG(rating) > 4.5\`
2314
2003
 
2315
- \u2705 CORRECT: Use HAVING (with GROUP BY), EXISTS, or subquery
2004
+ ## External Tool Results
2316
2005
 
2317
- 2. **WHERE vs HAVING**
2318
- - WHERE filters rows BEFORE grouping (cannot use aggregates)
2319
- - HAVING filters groups AFTER grouping (can use aggregates)
2320
- - If using HAVING, you MUST have GROUP BY
2006
+ The following external tools were executed for this request (if applicable):
2321
2007
 
2322
- 3. **NO NESTED AGGREGATE FUNCTIONS** - PostgreSQL does NOT allow aggregates inside aggregates
2323
- \u274C WRONG: \`AVG(ROUND(AVG(column), 2))\` or \`SELECT AVG(SUM(price)) FROM ...\`
2324
- \u2705 CORRECT: \`ROUND(AVG(column), 2)\`
2008
+ {{EXTERNAL_TOOL_CONTEXT}}
2325
2009
 
2326
- 4. **GROUP BY Requirements**
2327
- - ALL non-aggregated columns in SELECT must be in GROUP BY
2328
- - If you SELECT a column and don't aggregate it, add it to GROUP BY
2010
+ Use this external tool data to:
2011
+ - Provide information from external sources (emails, calendar, etc.)
2012
+ - Present the data in a user-friendly format
2013
+ - Combine external data with database queries when relevant
2014
+ - Reference specific results in your response
2329
2015
 
2330
- 5. **LIMIT Clause**
2331
- - ALWAYS include LIMIT (max 32 rows)
2332
- - For scalar subqueries in WHERE/HAVING, add LIMIT 1
2016
+ **Note:** If external tools were not needed, this section will indicate "No external tools were used for this request."
2333
2017
 
2334
2018
 
2335
2019
  ## Knowledge Base Context
@@ -2352,10 +2036,8 @@ Use this knowledge base information to:
2352
2036
  ## Previous Conversation
2353
2037
  {{CONVERSATION_HISTORY}}
2354
2038
 
2355
-
2356
2039
  `,
2357
2040
  user: `{{USER_PROMPT}}
2358
-
2359
2041
  `
2360
2042
  },
2361
2043
  "match-text-components": {
@@ -2369,11 +2051,21 @@ You will receive a text response containing:
2369
2051
  3. **Dashboard Components:** suggestions (1:component_type : reasoning format)
2370
2052
 
2371
2053
  Your job is to:
2372
- 1. **Parse the component suggestions** from the text response (format: 1:component_type : reasoning)
2373
- 2. **Match each suggestion with an actual component** from the available list
2374
- 3. **Generate proper props** for each matched component to **visualize the analysis results** that were already fetched
2375
- 4. **Generate title and description** for the dashboard container
2376
- 5. **Generate intelligent follow-up questions (actions)** that the user might naturally ask next based on the data analysis
2054
+ 1. **FIRST: Generate a direct answer component** (if the user question can be answered with a single visualization)
2055
+ - Determine the BEST visualization type (KPICard, BarChart, DataTable, PieChart, LineChart, etc.) to directly answer the user's question
2056
+ - Select the matching component from the available components list
2057
+ - Generate complete props for this component (query, title, description, config)
2058
+ - This component will be placed in the \`answerComponent\` field
2059
+ - This component will be streamed to the frontend IMMEDIATELY for instant user feedback
2060
+ - **CRITICAL**: Generate this FIRST in your JSON response
2061
+
2062
+ 2. **THEN: Parse ALL dashboard component suggestions** from the text response (format: 1:component_type : reasoning)
2063
+ 3. **Match EACH suggestion with an actual component** from the available list
2064
+ 4. **CRITICAL**: \`matchedComponents\` must include **ALL** dashboard components suggested in the text, INCLUDING the component you used as \`answerComponent\`
2065
+ - The answerComponent is shown first for quick feedback, but the full dashboard shows everything
2066
+ 5. **Generate proper props** for each matched component to **visualize the analysis results** that were already fetched
2067
+ 6. **Generate title and description** for the dashboard container
2068
+ 7. **Generate intelligent follow-up questions (actions)** that the user might naturally ask next based on the data analysis
2377
2069
 
2378
2070
  **CRITICAL GOAL**: Create dashboard components that display the **same data that was already analyzed** - NOT new data. The queries already ran and got results. You're just creating different visualizations of those results.
2379
2071
 
@@ -2408,7 +2100,8 @@ For each matched component, generate complete props:
2408
2100
 
2409
2101
  **Option B: GENERATE a new query** (when necessary)
2410
2102
  - Only generate new queries when you need DIFFERENT data
2411
- - Use the database schema below to write valid SQL
2103
+ - For SELECT queries: Use the database schema below to write valid SQL
2104
+ - For mutations (INSERT/UPDATE/DELETE): Only if matching a Form component, generate mutation query with $fieldName placeholders
2412
2105
 
2413
2106
 
2414
2107
  **Decision Logic:**
@@ -2426,8 +2119,12 @@ For each matched component, generate complete props:
2426
2119
  \u274C WRONG: \`WHERE COUNT(orders) > 0\`
2427
2120
  \u274C WRONG: \`WHERE SUM(price) > 100\`
2428
2121
  \u274C WRONG: \`WHERE AVG(rating) > 4.5\`
2122
+ \u274C WRONG: \`WHERE FLOOR(AVG(rating)) = 4\` (aggregate inside any function is still not allowed)
2123
+ \u274C WRONG: \`WHERE ROUND(SUM(price), 2) > 100\`
2429
2124
 
2430
2125
  \u2705 CORRECT: Use HAVING (with GROUP BY), EXISTS, or subquery
2126
+ \u2705 CORRECT: Move aggregate logic to HAVING: \`GROUP BY ... HAVING FLOOR(AVG(rating)) = 4\`
2127
+ \u2705 CORRECT: Use subquery for filtering: \`WHERE product_id IN (SELECT product_id FROM ... GROUP BY ... HAVING AVG(rating) >= 4)\`
2431
2128
 
2432
2129
  2. **NO NESTED AGGREGATE FUNCTIONS** - PostgreSQL does NOT allow aggregates inside aggregates
2433
2130
  \u274C WRONG: \`AVG(ROUND(AVG(column), 2))\`
@@ -2456,6 +2153,16 @@ For each matched component, generate complete props:
2456
2153
  - Subqueries used with =, <, >, etc. must return single value
2457
2154
  - Always add LIMIT 1 to scalar subqueries
2458
2155
 
2156
+ 8. **String Escaping** - PostgreSQL uses double single-quotes, NOT backslash
2157
+ \u274C WRONG: \`'Children\\'s furniture'\`
2158
+ \u2705 CORRECT: \`'Children''s furniture'\`
2159
+
2160
+ 9. **Always Use Table Aliases for Column References** - Prevent ambiguous column errors
2161
+ \u274C WRONG: \`SELECT product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
2162
+ \u2705 CORRECT: \`SELECT p.product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
2163
+ - Always prefix columns with table alias (e.g., \`p.product_id\`, \`c.name\`)
2164
+ - Especially critical in subqueries and joins where multiple tables share column names
2165
+
2459
2166
  **Query Generation Guidelines** (when creating new queries):
2460
2167
  - Use correct table and column names from the schema above
2461
2168
  - ALWAYS include LIMIT clause (max 32 rows)
@@ -2469,7 +2176,7 @@ For each matched component, generate complete props:
2469
2176
  - Brief explanation of what this component displays
2470
2177
  - Why it's useful for this data
2471
2178
 
2472
- ### 4. Config
2179
+ ### 4. Config (for visualization components)
2473
2180
  - **CRITICAL**: Look at the component's "Props Structure" to see what config fields it expects
2474
2181
  - Map query result columns to the appropriate config fields
2475
2182
  - Keep other existing config properties that don't need to change
@@ -2481,20 +2188,130 @@ For each matched component, generate complete props:
2481
2188
  - \`orientation\` = "vertical" or "horizontal" (controls visual direction only)
2482
2189
  - **DO NOT swap xAxisKey/yAxisKey based on orientation** - they always represent category and value respectively
2483
2190
 
2484
- ## Follow-Up Questions (Actions) Generation
2485
-
2486
- After analyzing the text response and matched components, generate 4-5 intelligent follow-up questions that the user might naturally ask next. These questions should:
2487
-
2488
- 1. **Build upon the data analysis** shown in the text response and components
2489
- 2. **Explore natural next steps** in the data exploration journey
2490
- 3. **Be progressively more detailed or specific** - go deeper into the analysis
2491
- 4. **Consider the insights revealed** - suggest questions that help users understand implications
2492
- 5. **Be phrased naturally** as if a real user would ask them
2493
- 6. **Vary in scope** - include both broad trends and specific details
2494
- 7. **Avoid redundancy** - don't ask questions already answered in the text response
2191
+ ### 5. Additional Props (match according to component type)
2192
+ - **CRITICAL**: Look at the matched component's "Props Structure" in the available components list
2193
+ - Generate props that match EXACTLY what the component expects
2495
2194
 
2195
+ **For Form components (type: "Form"):**
2496
2196
 
2497
- ## Output Format
2197
+ Props structure:
2198
+ - **query**: \`{ sql: "INSERT/UPDATE/DELETE query with $fieldName placeholders", params: [] }\`
2199
+ - **For UPDATE queries**: Check the database schema - if the table has an \`updated_at\` or \`last_updated\` column, always include it in the SET clause with \`CURRENT_TIMESTAMP\` (e.g., \`UPDATE table_name SET field = $field, updated_at = CURRENT_TIMESTAMP WHERE id = value\`)
2200
+ - **title**: "Update Order 5000", "Create New Product", or "Delete Order 5000"
2201
+ - **description**: What the form does
2202
+ - **submitButtonText**: Button text (default: "Submit"). For delete: "Delete", "Confirm Delete"
2203
+ - **submitButtonColor**: "primary" (blue) or "danger" (red). Use "danger" for DELETE operations
2204
+ - **successMessage**: Success message (default: "Form submitted successfully!"). For delete: "Record deleted successfully!"
2205
+ - **disableFields**: Set \`true\` for DELETE operations to show current values but prevent editing
2206
+ - **fields**: Array of field objects (structure below)
2207
+
2208
+ **Field object:**
2209
+ \`\`\`json
2210
+ {
2211
+ "name": "field_name", // Matches $field_name in SQL query
2212
+ "description": "Field Label",
2213
+ "type": "text|number|email|date|select|multiselect|checkbox|textarea",
2214
+ "required": true, // Set based on schema: nullable=false \u2192 required=true, nullable=true \u2192 required=false
2215
+ "defaultValue": "current_value", // For UPDATE: extract from text response
2216
+ "placeholder": "hint text",
2217
+ "options": [...], // For select/multiselect
2218
+ "validation": {
2219
+ "minLength": { "value": 5, "message": "..." },
2220
+ "maxLength": { "value": 100, "message": "..." },
2221
+ "min": { "value": 18, "message": "..." },
2222
+ "max": { "value": 120, "message": "..." },
2223
+ "pattern": { "value": "regex", "message": "..." }
2224
+ }
2225
+ }
2226
+ \`\`\`
2227
+
2228
+ **CRITICAL - Set required based on database schema:**
2229
+ - Check the column's \`nullable\` property in the database schema
2230
+ - If \`nullable: false\` \u2192 set \`required: true\` (field is mandatory)
2231
+ - If \`nullable: true\` \u2192 set \`required: false\` (field is optional)
2232
+ - Never set fields as required if the schema allows NULL
2233
+
2234
+ **Default Values for UPDATE:**
2235
+ - **NEVER include ID/primary key fields in UPDATE forms** (e.g., order_id, customer_id, product_id) - these cannot be changed
2236
+ - Detect UPDATE by checking if SQL contains "UPDATE" keyword
2237
+ - Extract current values from text response (look for "Current values:" or SELECT results)
2238
+ - Set \`defaultValue\` for each field with the extracted current value
2239
+
2240
+ **CRITICAL - Single field with current value pre-selected:**
2241
+ For UPDATE operations, use ONE field with defaultValue set to current value (not two separate fields).
2242
+
2243
+ \u2705 CORRECT - Single field, current value pre-selected:
2244
+ \`\`\`json
2245
+ {
2246
+ "name": "category_id",
2247
+ "type": "select",
2248
+ "defaultValue": 5,
2249
+ "options": [{"id": 1, "name": "Kitchen"}, {"id": 5, "name": "Furniture"}, {"id": 7, "name": "Decor"}]
2250
+ }
2251
+ \`\`\`
2252
+ User sees dropdown with "Furniture" selected, can change to any other category.
2253
+
2254
+ \u274C WRONG - Two separate fields:
2255
+ \`\`\`json
2256
+ [
2257
+ {"name": "current_category", "type": "text", "defaultValue": "Furniture", "disabled": true},
2258
+ {"name": "new_category", "type": "select", "options": [...]}
2259
+ ]
2260
+ \`\`\`
2261
+
2262
+ **Options Format:**
2263
+ - **Enum/status fields** (non-foreign keys): String array \`["Pending", "Shipped", "Delivered"]\`
2264
+ - **Foreign keys** (reference tables): Object array \`[{"id": 1, "name": "Furniture"}, {"id": 2, "name": "Kitchen"}]\`
2265
+ - Extract from text response queries and match format to field type
2266
+
2267
+ **Example UPDATE form field:**
2268
+ \`\`\`json
2269
+ {
2270
+ "name": "status",
2271
+ "description": "Order Status",
2272
+ "type": "select",
2273
+ "required": true,
2274
+ "defaultValue": "Pending", // Current value from database
2275
+ "options": ["Pending", "Processing", "Shipped", "Delivered"]
2276
+ }
2277
+ \`\`\`
2278
+
2279
+ **Example DELETE form props:**
2280
+ \`\`\`json
2281
+ {
2282
+ "query": { "sql": "DELETE FROM orders WHERE order_id = 123", "params": [] },
2283
+ "title": "Delete Order 123",
2284
+ "description": "Are you sure you want to delete this order?",
2285
+ "submitButtonText": "Confirm Delete",
2286
+ "submitButtonColor": "danger",
2287
+ "successMessage": "Order deleted successfully!",
2288
+ "disableFields": true,
2289
+ "fields": [
2290
+ { "name": "order_id", "description": "Order ID", "type": "text", "defaultValue": "123" },
2291
+ { "name": "status", "description": "Status", "type": "text", "defaultValue": "Pending" }
2292
+ ]
2293
+ }
2294
+ \`\`\`
2295
+
2296
+ **For visualization components (Charts, Tables, KPIs):**
2297
+ - **query**: String (SQL SELECT query)
2298
+ - **title**, **description**, **config**: As per component's props structure
2299
+ - Do NOT include fields array
2300
+
2301
+ ## Follow-Up Questions (Actions) Generation
2302
+
2303
+ After analyzing the text response and matched components, generate 4-5 intelligent follow-up questions that the user might naturally ask next. These questions should:
2304
+
2305
+ 1. **Build upon the data analysis** shown in the text response and components
2306
+ 2. **Explore natural next steps** in the data exploration journey
2307
+ 3. **Be progressively more detailed or specific** - go deeper into the analysis
2308
+ 4. **Consider the insights revealed** - suggest questions that help users understand implications
2309
+ 5. **Be phrased naturally** as if a real user would ask them
2310
+ 6. **Vary in scope** - include both broad trends and specific details
2311
+ 7. **Avoid redundancy** - don't ask questions already answered in the text response
2312
+
2313
+
2314
+ ## Output Format
2498
2315
 
2499
2316
  You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2500
2317
 
@@ -2505,8 +2322,25 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2505
2322
  - Do NOT use markdown code blocks (no \`\`\`)
2506
2323
  - Return ONLY the JSON object, nothing else
2507
2324
 
2325
+ **Example 1: With answer component** (when user question can be answered with single visualization)
2508
2326
  \`\`\`json
2509
2327
  {
2328
+ "hasAnswerComponent": true,
2329
+ "answerComponent": {
2330
+ "componentId": "id_from_available_list",
2331
+ "componentName": "name_of_component",
2332
+ "componentType": "type_of_component (can be KPICard, BarChart, LineChart, PieChart, DataTable, etc.)",
2333
+ "reasoning": "Why this visualization type best answers the user's question",
2334
+ "props": {
2335
+ "query": "SQL query for this component",
2336
+ "title": "Component title that directly answers the user's question",
2337
+ "description": "Component description",
2338
+ "config": {
2339
+ "field1": "value1",
2340
+ "field2": "value2"
2341
+ }
2342
+ }
2343
+ },
2510
2344
  "layoutTitle": "Clear, concise title for the overall dashboard/layout (5-10 words)",
2511
2345
  "layoutDescription": "Brief description of what the dashboard shows and its purpose (1-2 sentences)",
2512
2346
  "matchedComponents": [
@@ -2514,7 +2348,7 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2514
2348
  "componentId": "id_from_available_list",
2515
2349
  "componentName": "name_of_component",
2516
2350
  "componentType": "type_of_component",
2517
- "reasoning": "Why this component was selected for this suggestion",
2351
+ "reasoning": "Why this component was selected for the dashboard",
2518
2352
  "originalSuggestion": "c1:table : original reasoning from text",
2519
2353
  "props": {
2520
2354
  "query": "SQL query for this component",
@@ -2537,21 +2371,65 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2537
2371
  }
2538
2372
  \`\`\`
2539
2373
 
2374
+ **Example 2: Without answer component** (when user question needs multiple visualizations or dashboard)
2375
+ \`\`\`json
2376
+ {
2377
+ "hasAnswerComponent": false,
2378
+ "answerComponent": null,
2379
+ "layoutTitle": "Clear, concise title for the overall dashboard/layout (5-10 words)",
2380
+ "layoutDescription": "Brief description of what the dashboard shows and its purpose (1-2 sentences)",
2381
+ "matchedComponents": [
2382
+ {
2383
+ "componentId": "id_from_available_list",
2384
+ "componentName": "name_of_component",
2385
+ "componentType": "type_of_component",
2386
+ "reasoning": "Why this component was selected for the dashboard",
2387
+ "originalSuggestion": "c1:chart : original reasoning from text",
2388
+ "props": {
2389
+ "query": "SQL query for this component",
2390
+ "title": "Component title",
2391
+ "description": "Component description",
2392
+ "config": {
2393
+ "field1": "value1",
2394
+ "field2": "value2"
2395
+ }
2396
+ }
2397
+ }
2398
+ ],
2399
+ "actions": [
2400
+ "Follow-up question 1?",
2401
+ "Follow-up question 2?",
2402
+ "Follow-up question 3?",
2403
+ "Follow-up question 4?",
2404
+ "Follow-up question 5?"
2405
+ ]
2406
+ }
2407
+ \`\`\`
2408
+
2540
2409
  **CRITICAL:**
2541
- - \`matchedComponents\` MUST include ALL components suggested in the text response
2410
+ - **\`hasAnswerComponent\` determines if an answer component exists**
2411
+ - Set to \`true\` if the user question can be answered with a single visualization
2412
+ - Set to \`false\` if the user question can not be answered with single visualisation and needs multiple visualizations or a dashboard overview
2413
+ - **If \`hasAnswerComponent\` is \`true\`:**
2414
+ - \`answerComponent\` MUST be generated FIRST in the JSON before \`layoutTitle\`
2415
+ - Generate complete props (query, title, description, config)
2416
+ - **If \`hasAnswerComponent\` is \`false\`:**
2417
+ - Set \`answerComponent\` to \`null\`
2418
+ - **\`matchedComponents\` MUST include ALL dashboard components from the text analysis**
2419
+ - **CRITICAL**: Even if you used a component as \`answerComponent\`, you MUST STILL include it in \`matchedComponents\`
2420
+ - The count of matchedComponents should EQUAL the count of dashboard suggestions in the text (e.g., if text has 4 suggestions, matchedComponents should have 4 items)
2421
+ - Do NOT skip the answerComponent from matchedComponents
2422
+ - \`matchedComponents\` come from the dashboard component suggestions in the text response
2542
2423
  - \`layoutTitle\` MUST be a clear, concise title (5-10 words) that summarizes what the entire dashboard shows
2543
- - Examples: "Sales Performance Overview", "Customer Metrics Analysis", "Product Category Breakdown"
2544
2424
  - \`layoutDescription\` MUST be a brief description (1-2 sentences) explaining the purpose and scope of the dashboard
2545
2425
  - Should describe what insights the dashboard provides and what data it shows
2546
2426
  - \`actions\` MUST be an array of 4-5 intelligent follow-up questions based on the analysis
2547
2427
  - Return ONLY valid JSON (no markdown code blocks, no text before/after)
2548
- - Generate complete props for each component including query, title, description, and config
2549
-
2550
-
2428
+ - Generate complete props for each component
2551
2429
  `,
2552
- user: `## Text Response
2430
+ user: `## Analysis Content
2553
2431
 
2554
- {{TEXT_RESPONSE}}
2432
+ {{ANALYSIS_CONTENT}}
2555
2433
 
2556
2434
  ---
2557
2435
 
@@ -2598,70 +2476,269 @@ Format your response as a JSON object with this structure:
2598
2476
 
2599
2477
  Return ONLY valid JSON.`
2600
2478
  },
2601
- "execute-tools": {
2602
- system: `You are an expert AI assistant that executes external tools to fetch data from external services.
2479
+ "category-classification": {
2480
+ system: `You are an expert AI that categorizes user questions into specific action categories and identifies required tools/resources.
2603
2481
 
2604
- You have access to external tools that can retrieve information like emails, calendar events, and other external data. When the user requests this information, you should call the appropriate tools.
2482
+ CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2605
2483
 
2606
2484
  ## Available External Tools
2485
+
2607
2486
  {{AVAILABLE_TOOLS}}
2608
2487
 
2609
- ## Your Task
2488
+ ---
2610
2489
 
2611
- Analyze the user's request and:
2612
-
2613
- 1. **Determine if external tools are needed**
2614
- - Examples that NEED external tools:
2615
- - "Show me my emails" \u2192 needs email tool
2616
- - "Get my last 5 Gmail messages" \u2192 needs Gmail tool
2617
- - "Check my Outlook inbox" \u2192 needs Outlook tool
2618
-
2619
- - Examples that DON'T need external tools:
2620
- - "What is the total sales?" \u2192 database query (handled elsewhere)
2621
- - "Show me revenue trends" \u2192 internal data analysis
2622
- - "Hello" \u2192 general conversation
2623
-
2624
- 2. **Call the appropriate tools**
2625
- - Use the tool calling mechanism to execute external tools
2626
- - Extract parameters from the user's request
2627
- - Use sensible defaults when parameters aren't specified:
2628
- - For email limit: default to 10
2629
- - For email address: use "me" for the authenticated user if not specified
2630
-
2631
- 3. **Handle errors and retry**
2632
- - If a tool call fails, analyze the error message
2633
- - Retry with corrected parameters if possible
2634
- - You have up to 3 attempts per tool
2635
-
2636
- ## Important Guidelines
2637
-
2638
- - **Only call external tools when necessary** - Don't call tools for database queries or general conversation
2639
- - **Choose the right tool** - For email requests, select Gmail vs Outlook based on:
2640
- - Explicit mention (e.g., "Gmail", "Outlook")
2641
- - Email domain (e.g., @gmail.com \u2192 Gmail, @company.com \u2192 Outlook)
2642
- - **Extract parameters carefully** - Use the user's exact values when provided
2643
- - **If no tools are needed** - Simply respond that no external tools are required for this request
2644
-
2645
- ## Examples
2646
-
2647
- **Example 1 - Gmail Request:**
2648
- User: "Show me my last 5 Gmail messages"
2649
- Action: Call get-gmail-mails tool with parameters: { email: "me", limit: 5 }
2650
-
2651
- **Example 2 - Outlook Request:**
2652
- User: "Get emails from john.doe@company.com"
2653
- Action: Call get-outlook-mails tool with parameters: { email: "john.doe@company.com", limit: 10 }
2654
-
2655
- **Example 3 - No Tools Needed:**
2656
- User: "What is the total sales?"
2657
- Response: This is a database query, not an external tool request. No external tools are needed.
2658
-
2659
- **Example 4 - Error Retry:**
2660
- Tool call fails with: "Invalid email parameter"
2661
- Action: Analyze error, correct the parameter, and retry the tool call
2662
- `,
2663
- user: `{{USER_PROMPT}}
2664
- `
2490
+ Your task is to analyze the user's question and determine:
2491
+
2492
+ 1. **Question Category:**
2493
+ - "data_analysis": Questions about analyzing, querying, reading, or visualizing data from the database (SELECT operations)
2494
+ - "data_modification": Questions about creating, updating, deleting, or modifying data in the database (INSERT, UPDATE, DELETE operations)
2495
+
2496
+ 2. **External Tools Required** (for both categories):
2497
+ From the available tools listed above, identify which ones are needed to support the user's request:
2498
+ - Match the tool names/descriptions to what the user is asking for
2499
+ - Extract specific parameters mentioned in the user's question
2500
+
2501
+ 3. **Tool Parameters** (if tools are identified):
2502
+ Extract specific parameters the user mentioned:
2503
+ - For each identified tool, extract relevant parameters (email, recipient, content, etc.)
2504
+ - Only include parameters the user explicitly or implicitly mentioned
2505
+
2506
+ **Important Guidelines:**
2507
+ - If user mentions any of the available external tools \u2192 identify those tools and extract their parameters
2508
+ - If user asks to "send", "schedule", "create event", "message" \u2192 check if available tools match
2509
+ - If user asks to "show", "analyze", "compare", "calculate" data \u2192 "data_analysis"
2510
+ - If user asks to modify/create/update/delete data \u2192 "data_modification"
2511
+ - Always identify tools from the available tools list (not from generic descriptions)
2512
+ - Be precise in identifying tool types and required parameters
2513
+ - Only include tools that are explicitly mentioned or clearly needed
2514
+
2515
+ **Output Format:**
2516
+ \`\`\`json
2517
+ {
2518
+ "category": "data_analysis" | "data_modification",
2519
+ "reasoning": "Brief explanation of why this category was chosen",
2520
+ "externalTools": [
2521
+ {
2522
+ "type": "tool_id_from_available_tools",
2523
+ "name": "Tool Display Name",
2524
+ "description": "What this tool will do",
2525
+ "parameters": {
2526
+ "param1": "extracted value",
2527
+ "param2": "extracted value"
2528
+ }
2529
+ }
2530
+ ],
2531
+ "dataAnalysisType": "visualization" | "calculation" | "comparison" | "trend" | null,
2532
+ "confidence": 0-100
2533
+ }
2534
+ \`\`\`
2535
+
2536
+
2537
+ ## Previous Conversation
2538
+ {{CONVERSATION_HISTORY}}`,
2539
+ user: `{{USER_PROMPT}}`
2540
+ },
2541
+ "adapt-ui-block-params": {
2542
+ system: `You are an expert AI that adapts and modifies UI block component parameters based on the user's current question.
2543
+
2544
+ CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2545
+
2546
+ ## Database Schema Reference
2547
+
2548
+ {{SCHEMA_DOC}}
2549
+
2550
+ Use this schema to understand available tables, columns, and relationships when modifying SQL queries. Ensure all table and column names you use in adapted queries are valid according to this schema.
2551
+
2552
+ ## Context
2553
+ You are given:
2554
+ 1. A previous UI Block response (with component and its props) that matched the user's current question with >90% semantic similarity
2555
+ 2. The user's current question
2556
+ 3. The component that needs parameter adaptation
2557
+
2558
+ Your task is to:
2559
+ 1. **Analyze the difference** between the original question (from the matched UIBlock) and the current user question
2560
+ 2. **Identify what parameters need to change** in the component props to answer the current question
2561
+ 3. **Modify the props** to match the current request while keeping the same component type(s)
2562
+ 4. **Preserve component structure** - only change props, not the components themselves
2563
+
2564
+ ## Component Structure Handling
2565
+
2566
+ ### For Single Components:
2567
+ - Modify props directly (config, actions, query, filters, etc.)
2568
+
2569
+ ### For MultiComponentContainer:
2570
+ The component will have structure:
2571
+ \`\`\`json
2572
+ {
2573
+ "type": "Container",
2574
+ "name": "MultiComponentContainer",
2575
+ "props": {
2576
+ "config": {
2577
+ "components": [...], // Array of nested components - ADAPT EACH ONE
2578
+ "title": "...", // Container title - UPDATE based on new question
2579
+ "description": "..." // Container description - UPDATE based on new question
2580
+ },
2581
+ "actions": [...] // ADAPT actions if needed
2582
+ }
2583
+ }
2584
+ \`\`\`
2585
+
2586
+ When adapting MultiComponentContainer:
2587
+ - Update the container-level \`title\` and \`description\` to reflect the new user question
2588
+ - For each component in \`config.components\`:
2589
+ - Identify what data it shows and how the new question changes what's needed
2590
+ - Adapt its query parameters (WHERE clauses, LIMIT, ORDER BY, filters, date ranges)
2591
+ - Update its title/description to match the new context
2592
+ - Update its config settings (colors, sorting, grouping, metrics)
2593
+ - Update \`actions\` if the new question requires different actions
2594
+
2595
+ ## Important Guidelines:
2596
+ - Keep the same component type (don't change KPICard to LineChart)
2597
+ - Keep the same number of components in the container
2598
+ - For each nested component, update:
2599
+ - Query WHERE clauses, LIMIT, ORDER BY, filters, date ranges, metrics
2600
+ - Title and description to reflect the new question
2601
+ - Config settings like colors, sorting, grouping if needed
2602
+ - Maintain each component's core purpose while answering the new question
2603
+ - If query modification is needed, ensure all table/column names remain valid
2604
+ - CRITICAL: Ensure JSON is valid and complete for all nested structures
2605
+
2606
+
2607
+ ## Output Format:
2608
+
2609
+ ### For Single Component:
2610
+ \`\`\`json
2611
+ {
2612
+ "success": true,
2613
+ "adaptedComponent": {
2614
+ "id": "original_component_id",
2615
+ "name": "component_name",
2616
+ "type": "component_type",
2617
+ "description": "updated_description",
2618
+ "props": {
2619
+ "config": { },
2620
+ "actions": [],
2621
+ }
2622
+ },
2623
+ "parametersChanged": [
2624
+ {
2625
+ "field": "query",
2626
+ "reason": "Added Q4 date filter"
2627
+ },
2628
+ {
2629
+ "field": "title",
2630
+ "reason": "Updated to reflect Q4 focus"
2631
+ }
2632
+ ],
2633
+ "explanation": "How the component was adapted to answer the new question"
2634
+ }
2635
+ \`\`\`
2636
+
2637
+ ### For MultiComponentContainer:
2638
+ \`\`\`json
2639
+ {
2640
+ "success": true,
2641
+ "adaptedComponent": {
2642
+ "id": "original_container_id",
2643
+ "name": "MultiComponentContainer",
2644
+ "type": "Container",
2645
+ "description": "updated_container_description",
2646
+ "props": {
2647
+ "config": {
2648
+ "title": "Updated dashboard title based on new question",
2649
+ "description": "Updated description reflecting new question context",
2650
+ "components": [
2651
+ {
2652
+ "id": "component_1_id",
2653
+ "name": "component_1_name",
2654
+ "type": "component_1_type",
2655
+ "description": "updated description for this specific component",
2656
+ "props": {
2657
+ "query": "Modified SQL query with updated WHERE/LIMIT/ORDER BY",
2658
+ "config": { "metric": "updated_metric", "filters": {...} }
2659
+ }
2660
+ },
2661
+ {
2662
+ "id": "component_2_id",
2663
+ "name": "component_2_name",
2664
+ "type": "component_2_type",
2665
+ "description": "updated description for this component",
2666
+ "props": {
2667
+ "query": "Modified SQL query for this component",
2668
+ "config": { "metric": "updated_metric", "filters": {...} }
2669
+ }
2670
+ }
2671
+ ]
2672
+ },
2673
+ "actions": []
2674
+ }
2675
+ },
2676
+ "parametersChanged": [
2677
+ {
2678
+ "field": "container.title",
2679
+ "reason": "Updated to reflect new dashboard focus"
2680
+ },
2681
+ {
2682
+ "field": "components[0].query",
2683
+ "reason": "Modified WHERE clause for new metrics"
2684
+ },
2685
+ {
2686
+ "field": "components[1].config.metric",
2687
+ "reason": "Changed metric from X to Y based on new question"
2688
+ }
2689
+ ],
2690
+ "explanation": "Detailed explanation of how each component was adapted"
2691
+ }
2692
+ \`\`\`
2693
+
2694
+ If adaptation is not possible or would fundamentally change the component:
2695
+ \`\`\`json
2696
+ {
2697
+ "success": false,
2698
+ "reason": "Cannot adapt component - the new question requires a different visualization type",
2699
+ "explanation": "The original component shows KPI cards but the new question needs a trend chart"
2700
+ }
2701
+ \`\`\``,
2702
+ user: `## Previous Matched UIBlock
2703
+
2704
+ **Original Question:** {{ORIGINAL_USER_PROMPT}}
2705
+
2706
+ **Matched UIBlock Component:**
2707
+ \`\`\`json
2708
+ {{MATCHED_UI_BLOCK_COMPONENT}}
2709
+ \`\`\`
2710
+
2711
+ **Component Properties:**
2712
+ \`\`\`json
2713
+ {{COMPONENT_PROPS}}
2714
+ \`\`\`
2715
+
2716
+ ## Current User Question
2717
+ {{CURRENT_USER_PROMPT}}
2718
+
2719
+ ---
2720
+
2721
+ ## Adaptation Instructions
2722
+
2723
+ 1. **Analyze the difference** between the original question and the current question
2724
+ 2. **Identify what data needs to change**:
2725
+ - For single components: adapt the query/config/actions
2726
+ - For MultiComponentContainer: adapt the container title/description AND each nested component's parameters
2727
+
2728
+ 3. **Modify the parameters**:
2729
+ - **Container level** (if MultiComponentContainer):
2730
+ - Update \`title\` and \`description\` to reflect the new user question
2731
+ - Update \`actions\` if needed
2732
+
2733
+ - **For each component** (single or nested in container):
2734
+ - Identify what it shows (sales, revenue, inventory, etc.)
2735
+ - Adapt SQL queries: modify WHERE clauses, LIMIT, ORDER BY, filters, date ranges
2736
+ - Update component title and description
2737
+ - Update config settings (metrics, colors, sorting, grouping)
2738
+
2739
+ 4. **Preserve structure**: Keep the same number and type of components
2740
+
2741
+ 5. **Return complete JSON** with all adapted properties for all components`
2665
2742
  }
2666
2743
  };
2667
2744
 
@@ -2739,9 +2816,10 @@ var PromptLoader = class {
2739
2816
  }
2740
2817
  /**
2741
2818
  * Load both system and user prompts from cache and replace variables
2819
+ * Supports prompt caching by splitting static and dynamic content
2742
2820
  * @param promptName - Name of the prompt
2743
2821
  * @param variables - Variables to replace in the templates
2744
- * @returns Object containing both system and user prompts
2822
+ * @returns Object containing both system and user prompts (system can be string or array for caching)
2745
2823
  */
2746
2824
  async loadPrompts(promptName, variables) {
2747
2825
  if (!this.isInitialized) {
@@ -2752,6 +2830,26 @@ var PromptLoader = class {
2752
2830
  if (!template) {
2753
2831
  throw new Error(`Prompt template '${promptName}' not found in cache. Available prompts: ${Array.from(this.promptCache.keys()).join(", ")}`);
2754
2832
  }
2833
+ const contextMarker = "---\n\n## CONTEXT";
2834
+ if (template.system.includes(contextMarker)) {
2835
+ const [staticPart, contextPart] = template.system.split(contextMarker);
2836
+ logger.debug(`\u2713 Prompt caching enabled for '${promptName}' (static: ${staticPart.length} chars, context: ${contextPart.length} chars)`);
2837
+ const processedContext = this.replaceVariables(contextMarker + contextPart, variables);
2838
+ return {
2839
+ system: [
2840
+ {
2841
+ type: "text",
2842
+ text: staticPart.trim(),
2843
+ cache_control: { type: "ephemeral" }
2844
+ },
2845
+ {
2846
+ type: "text",
2847
+ text: processedContext.trim()
2848
+ }
2849
+ ],
2850
+ user: this.replaceVariables(template.user, variables)
2851
+ };
2852
+ }
2755
2853
  return {
2756
2854
  system: this.replaceVariables(template.system, variables),
2757
2855
  user: this.replaceVariables(template.user, variables)
@@ -2838,6 +2936,75 @@ var LLM = class {
2838
2936
  // ============================================================
2839
2937
  // PRIVATE HELPER METHODS
2840
2938
  // ============================================================
2939
+ /**
2940
+ * Normalize system prompt to Anthropic format
2941
+ * Converts string to array format if needed
2942
+ * @param sys - System prompt (string or array of blocks)
2943
+ * @returns Normalized system prompt for Anthropic API
2944
+ */
2945
+ static _normalizeSystemPrompt(sys) {
2946
+ if (typeof sys === "string") {
2947
+ return sys;
2948
+ }
2949
+ return sys;
2950
+ }
2951
+ /**
2952
+ * Log cache usage metrics from Anthropic API response
2953
+ * Shows cache hits, costs, and savings
2954
+ */
2955
+ static _logCacheUsage(usage) {
2956
+ if (!usage) return;
2957
+ const inputTokens = usage.input_tokens || 0;
2958
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
2959
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
2960
+ const outputTokens = usage.output_tokens || 0;
2961
+ const INPUT_PRICE = 0.8;
2962
+ const OUTPUT_PRICE = 4;
2963
+ const CACHE_WRITE_PRICE = 1;
2964
+ const CACHE_READ_PRICE = 0.08;
2965
+ const regularInputCost = inputTokens / 1e6 * INPUT_PRICE;
2966
+ const cacheWriteCost = cacheCreationTokens / 1e6 * CACHE_WRITE_PRICE;
2967
+ const cacheReadCost = cacheReadTokens / 1e6 * CACHE_READ_PRICE;
2968
+ const outputCost = outputTokens / 1e6 * OUTPUT_PRICE;
2969
+ const totalCost = regularInputCost + cacheWriteCost + cacheReadCost + outputCost;
2970
+ const totalInputTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
2971
+ const costWithoutCache = totalInputTokens / 1e6 * INPUT_PRICE + outputCost;
2972
+ const savings = costWithoutCache - totalCost;
2973
+ const savingsPercent = costWithoutCache > 0 ? savings / costWithoutCache * 100 : 0;
2974
+ console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2975
+ console.log("\u{1F4B0} PROMPT CACHING METRICS");
2976
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
2977
+ console.log("\n\u{1F4CA} Token Usage:");
2978
+ console.log(` Input (regular): ${inputTokens.toLocaleString()} tokens`);
2979
+ if (cacheCreationTokens > 0) {
2980
+ console.log(` Cache write: ${cacheCreationTokens.toLocaleString()} tokens (first request)`);
2981
+ }
2982
+ if (cacheReadTokens > 0) {
2983
+ console.log(` Cache read: ${cacheReadTokens.toLocaleString()} tokens \u26A1 HIT!`);
2984
+ }
2985
+ console.log(` Output: ${outputTokens.toLocaleString()} tokens`);
2986
+ console.log(` Total input: ${totalInputTokens.toLocaleString()} tokens`);
2987
+ console.log("\n\u{1F4B5} Cost Breakdown:");
2988
+ console.log(` Input (regular): $${regularInputCost.toFixed(6)}`);
2989
+ if (cacheCreationTokens > 0) {
2990
+ console.log(` Cache write: $${cacheWriteCost.toFixed(6)}`);
2991
+ }
2992
+ if (cacheReadTokens > 0) {
2993
+ console.log(` Cache read: $${cacheReadCost.toFixed(6)} (90% off!)`);
2994
+ }
2995
+ console.log(` Output: $${outputCost.toFixed(6)}`);
2996
+ console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
2997
+ console.log(` Total cost: $${totalCost.toFixed(6)}`);
2998
+ if (cacheReadTokens > 0) {
2999
+ console.log(`
3000
+ \u{1F48E} Savings: $${savings.toFixed(6)} (${savingsPercent.toFixed(1)}% off)`);
3001
+ console.log(` Without cache: $${costWithoutCache.toFixed(6)}`);
3002
+ } else if (cacheCreationTokens > 0) {
3003
+ console.log(`
3004
+ \u23F1\uFE0F Cache created - next request will be ~90% cheaper!`);
3005
+ }
3006
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
3007
+ }
2841
3008
  /**
2842
3009
  * Parse model string to extract provider and model name
2843
3010
  * @param modelString - Format: "provider/model-name" or just "model-name"
@@ -2872,7 +3039,7 @@ var LLM = class {
2872
3039
  model: modelName,
2873
3040
  max_tokens: options.maxTokens || 1e3,
2874
3041
  temperature: options.temperature,
2875
- system: messages.sys,
3042
+ system: this._normalizeSystemPrompt(messages.sys),
2876
3043
  messages: [{
2877
3044
  role: "user",
2878
3045
  content: messages.user
@@ -2890,7 +3057,7 @@ var LLM = class {
2890
3057
  model: modelName,
2891
3058
  max_tokens: options.maxTokens || 1e3,
2892
3059
  temperature: options.temperature,
2893
- system: messages.sys,
3060
+ system: this._normalizeSystemPrompt(messages.sys),
2894
3061
  messages: [{
2895
3062
  role: "user",
2896
3063
  content: messages.user
@@ -2898,6 +3065,7 @@ var LLM = class {
2898
3065
  stream: true
2899
3066
  });
2900
3067
  let fullText = "";
3068
+ let usage = null;
2901
3069
  for await (const chunk of stream) {
2902
3070
  if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
2903
3071
  const text = chunk.delta.text;
@@ -2905,8 +3073,12 @@ var LLM = class {
2905
3073
  if (options.partial) {
2906
3074
  options.partial(text);
2907
3075
  }
3076
+ } else if (chunk.type === "message_delta" && chunk.usage) {
3077
+ usage = chunk.usage;
2908
3078
  }
2909
3079
  }
3080
+ if (usage) {
3081
+ }
2910
3082
  if (json) {
2911
3083
  return this._parseJSON(fullText);
2912
3084
  }
@@ -2929,7 +3101,7 @@ var LLM = class {
2929
3101
  model: modelName,
2930
3102
  max_tokens: options.maxTokens || 4e3,
2931
3103
  temperature: options.temperature,
2932
- system: messages.sys,
3104
+ system: this._normalizeSystemPrompt(messages.sys),
2933
3105
  messages: conversationMessages,
2934
3106
  tools,
2935
3107
  stream: true
@@ -2939,6 +3111,7 @@ var LLM = class {
2939
3111
  const contentBlocks = [];
2940
3112
  let currentTextBlock = "";
2941
3113
  let currentToolUse = null;
3114
+ let usage = null;
2942
3115
  for await (const chunk of stream) {
2943
3116
  if (chunk.type === "message_start") {
2944
3117
  contentBlocks.length = 0;
@@ -2989,11 +3162,16 @@ var LLM = class {
2989
3162
  }
2990
3163
  if (chunk.type === "message_delta") {
2991
3164
  stopReason = chunk.delta.stop_reason || stopReason;
3165
+ if (chunk.usage) {
3166
+ usage = chunk.usage;
3167
+ }
2992
3168
  }
2993
3169
  if (chunk.type === "message_stop") {
2994
3170
  break;
2995
3171
  }
2996
3172
  }
3173
+ if (usage) {
3174
+ }
2997
3175
  if (stopReason === "end_turn") {
2998
3176
  break;
2999
3177
  }
@@ -3165,6 +3343,57 @@ var KB = {
3165
3343
  };
3166
3344
  var knowledge_base_default = KB;
3167
3345
 
3346
+ // src/userResponse/conversation-search.ts
3347
+ var searchConversations = async ({
3348
+ userPrompt,
3349
+ collections,
3350
+ userId,
3351
+ similarityThreshold = 0.6
3352
+ }) => {
3353
+ try {
3354
+ if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["search"]) {
3355
+ logger.info("[ConversationSearch] conversation-history.search collection not registered, skipping");
3356
+ return null;
3357
+ }
3358
+ logger.info(`[ConversationSearch] Searching conversations for: "${userPrompt.substring(0, 50)}..."`);
3359
+ logger.info(`[ConversationSearch] Using similarity threshold: ${(similarityThreshold * 100).toFixed(0)}%`);
3360
+ const result = await collections["conversation-history"]["search"]({
3361
+ userPrompt,
3362
+ userId,
3363
+ threshold: similarityThreshold
3364
+ });
3365
+ if (!result) {
3366
+ logger.info("[ConversationSearch] No matching conversations found");
3367
+ return null;
3368
+ }
3369
+ if (!result.uiBlock) {
3370
+ logger.error("[ConversationSearch] No UI block in conversation search result");
3371
+ return null;
3372
+ }
3373
+ const similarity = result.similarity || 0;
3374
+ logger.info(`[ConversationSearch] Best match similarity: ${(similarity * 100).toFixed(2)}%`);
3375
+ if (similarity < similarityThreshold) {
3376
+ logger.info(
3377
+ `[ConversationSearch] Best match has similarity ${(similarity * 100).toFixed(2)}% but below threshold ${(similarityThreshold * 100).toFixed(2)}%`
3378
+ );
3379
+ return null;
3380
+ }
3381
+ logger.info(
3382
+ `[ConversationSearch] Found matching conversation with similarity ${(similarity * 100).toFixed(2)}%`
3383
+ );
3384
+ logger.debug(`[ConversationSearch] Matched prompt: "${result.metadata?.userPrompt?.substring(0, 50)}..."`);
3385
+ return result;
3386
+ } catch (error) {
3387
+ const errorMsg = error instanceof Error ? error.message : String(error);
3388
+ logger.warn(`[ConversationSearch] Error searching conversations: ${errorMsg}`);
3389
+ return null;
3390
+ }
3391
+ };
3392
+ var ConversationSearch = {
3393
+ searchConversations
3394
+ };
3395
+ var conversation_search_default = ConversationSearch;
3396
+
3168
3397
  // src/userResponse/base-llm.ts
3169
3398
  var BaseLLM = class {
3170
3399
  constructor(config) {
@@ -3179,564 +3408,39 @@ var BaseLLM = class {
3179
3408
  return apiKey || this.apiKey || this.getDefaultApiKey();
3180
3409
  }
3181
3410
  /**
3182
- * Classify user question to determine the type and required visualizations
3411
+ * Match components from text response suggestions and generate follow-up questions
3412
+ * Takes a text response with component suggestions (c1:type format) and matches with available components
3413
+ * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
3414
+ * All components are placed in a default MultiComponentContainer layout
3415
+ * @param analysisContent - The text response containing component suggestions
3416
+ * @param components - List of available components
3417
+ * @param apiKey - Optional API key
3418
+ * @param logCollector - Optional log collector
3419
+ * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
3420
+ * @returns Object containing matched components, layout title/description, and follow-up actions
3183
3421
  */
3184
- async classifyUserQuestion(userPrompt, apiKey, logCollector, conversationHistory) {
3422
+ async matchComponentsFromAnalysis(analysisContent, components, apiKey, logCollector, componentStreamCallback) {
3185
3423
  try {
3186
- const prompts = await promptLoader.loadPrompts("classify", {
3187
- USER_PROMPT: userPrompt,
3188
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3189
- });
3190
- const result = await LLM.stream(
3191
- {
3192
- sys: prompts.system,
3193
- user: prompts.user
3194
- },
3195
- {
3196
- model: this.model,
3197
- maxTokens: 800,
3198
- temperature: 0.2,
3199
- apiKey: this.getApiKey(apiKey)
3200
- },
3201
- true
3202
- // Parse as JSON
3203
- );
3204
- logCollector?.logExplanation(
3205
- "User question classified",
3206
- result.reasoning || "No reasoning provided",
3207
- {
3208
- questionType: result.questionType || "general",
3209
- visualizations: result.visualizations || [],
3210
- needsMultipleComponents: result.needsMultipleComponents || false
3211
- }
3212
- );
3213
- return {
3214
- questionType: result.questionType || "general",
3215
- visualizations: result.visualizations || [],
3216
- reasoning: result.reasoning || "No reasoning provided",
3217
- needsMultipleComponents: result.needsMultipleComponents || false
3218
- };
3219
- } catch (error) {
3220
- const errorMsg = error instanceof Error ? error.message : String(error);
3221
- logger.error(`[${this.getProviderName()}] Error classifying user question: ${errorMsg}`);
3222
- logger.debug(`[${this.getProviderName()}] Classification error details:`, error);
3223
- throw error;
3224
- }
3225
- }
3226
- /**
3227
- * Enhanced function that validates and modifies the entire props object based on user request
3228
- * This includes query, title, description, and config properties
3229
- */
3230
- async validateAndModifyProps(userPrompt, originalProps, componentName, componentType, componentDescription, apiKey, logCollector, conversationHistory) {
3231
- const schemaDoc = schema.generateSchemaDocumentation();
3232
- try {
3233
- const prompts = await promptLoader.loadPrompts("modify-props", {
3234
- COMPONENT_NAME: componentName,
3235
- COMPONENT_TYPE: componentType,
3236
- COMPONENT_DESCRIPTION: componentDescription || "No description",
3237
- SCHEMA_DOC: schemaDoc || "No schema available",
3238
- DEFAULT_LIMIT: this.defaultLimit,
3239
- USER_PROMPT: userPrompt,
3240
- CURRENT_PROPS: JSON.stringify(originalProps, null, 2),
3241
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3242
- });
3243
- logger.debug("props-modification: System prompt\n", prompts.system.substring(0, 100), "\n\n\n", "User prompt:", prompts.user.substring(0, 50));
3244
- const result = await LLM.stream(
3245
- {
3246
- sys: prompts.system,
3247
- user: prompts.user
3248
- },
3249
- {
3250
- model: this.model,
3251
- maxTokens: 2500,
3252
- temperature: 0.2,
3253
- apiKey: this.getApiKey(apiKey)
3254
- },
3255
- true
3256
- // Parse as JSON
3257
- );
3258
- const props = result.props || originalProps;
3259
- if (props && props.query) {
3260
- props.query = fixScalarSubqueries(props.query);
3261
- props.query = ensureQueryLimit(props.query, this.defaultLimit);
3262
- }
3263
- if (props && props.query) {
3264
- logCollector?.logQuery(
3265
- "Props query modified",
3266
- props.query,
3267
- {
3268
- modifications: result.modifications || [],
3269
- reasoning: result.reasoning || "No modifications needed"
3270
- }
3271
- );
3272
- }
3273
- if (result.reasoning) {
3274
- logCollector?.logExplanation(
3275
- "Props modification explanation",
3276
- result.reasoning,
3277
- { modifications: result.modifications || [] }
3278
- );
3279
- }
3280
- return {
3281
- props,
3282
- isModified: result.isModified || false,
3283
- reasoning: result.reasoning || "No modifications needed",
3284
- modifications: result.modifications || []
3285
- };
3286
- } catch (error) {
3287
- const errorMsg = error instanceof Error ? error.message : String(error);
3288
- logger.error(`[${this.getProviderName()}] Error validating/modifying props: ${errorMsg}`);
3289
- logger.debug(`[${this.getProviderName()}] Props validation error details:`, error);
3290
- throw error;
3291
- }
3292
- }
3293
- /**
3294
- * Match and select a component from available components filtered by type
3295
- * This picks the best matching component based on user prompt and modifies its props
3296
- */
3297
- async generateAnalyticalComponent(userPrompt, components, preferredVisualizationType, apiKey, logCollector, conversationHistory) {
3298
- try {
3299
- const filteredComponents = preferredVisualizationType ? components.filter((c) => c.type === preferredVisualizationType) : components;
3300
- if (filteredComponents.length === 0) {
3301
- logCollector?.warn(
3302
- `No components found of type ${preferredVisualizationType}`,
3303
- "explanation",
3304
- { reason: "No matching components available for this visualization type" }
3305
- );
3306
- return {
3307
- component: null,
3308
- reasoning: `No components available of type ${preferredVisualizationType}`,
3309
- isGenerated: false
3310
- };
3311
- }
3312
- const componentsText = filteredComponents.map((comp, idx) => {
3313
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3314
- const category = comp.category || "general";
3315
- const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
3316
- return `${idx + 1}. ID: ${comp.id}
3317
- Name: ${comp.name}
3318
- Type: ${comp.type}
3319
- Category: ${category}
3320
- Description: ${comp.description || "No description"}
3321
- Keywords: ${keywords}
3322
- Props Preview: ${propsPreview}`;
3323
- }).join("\n\n");
3324
- const visualizationConstraint = preferredVisualizationType ? `
3325
- **IMPORTANT: Components are filtered to type ${preferredVisualizationType}. Select the best match.**
3326
- ` : "";
3327
- const prompts = await promptLoader.loadPrompts("single-component", {
3328
- COMPONENT_TYPE: preferredVisualizationType || "any",
3329
- COMPONENTS_LIST: componentsText,
3330
- VISUALIZATION_CONSTRAINT: visualizationConstraint,
3331
- USER_PROMPT: userPrompt,
3332
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3333
- });
3334
- logger.debug("single-component: System prompt\n", prompts.system.substring(0, 100), "\n\n\n", "User prompt:", prompts.user.substring(0, 50));
3335
- const result = await LLM.stream(
3336
- {
3337
- sys: prompts.system,
3338
- user: prompts.user
3339
- },
3340
- {
3341
- model: this.model,
3342
- maxTokens: 2e3,
3343
- temperature: 0.2,
3344
- apiKey: this.getApiKey(apiKey)
3345
- },
3346
- true
3347
- // Parse as JSON
3348
- );
3349
- if (!result.canGenerate || result.confidence < 50) {
3350
- logCollector?.warn(
3351
- "Cannot match component",
3352
- "explanation",
3353
- { reason: result.reasoning || "Unable to find matching component for this question" }
3354
- );
3355
- return {
3356
- component: null,
3357
- reasoning: result.reasoning || "Unable to find matching component for this question",
3358
- isGenerated: false
3359
- };
3360
- }
3361
- const componentIndex = result.componentIndex;
3362
- const componentId = result.componentId;
3363
- let matchedComponent = null;
3364
- if (componentId) {
3365
- matchedComponent = filteredComponents.find((c) => c.id === componentId);
3366
- }
3367
- if (!matchedComponent && componentIndex) {
3368
- matchedComponent = filteredComponents[componentIndex - 1];
3369
- }
3370
- if (!matchedComponent) {
3371
- logCollector?.warn("Component not found in filtered list");
3372
- return {
3373
- component: null,
3374
- reasoning: "Component not found in filtered list",
3375
- isGenerated: false
3376
- };
3377
- }
3378
- logCollector?.info(`Matched component: ${matchedComponent.name} (confidence: ${result.confidence}%)`);
3379
- const propsValidation = await this.validateAndModifyProps(
3380
- userPrompt,
3381
- matchedComponent.props,
3382
- matchedComponent.name,
3383
- matchedComponent.type,
3384
- matchedComponent.description,
3385
- apiKey,
3386
- logCollector,
3387
- conversationHistory
3388
- );
3389
- const modifiedComponent = {
3390
- ...matchedComponent,
3391
- props: propsValidation.props
3392
- };
3393
- logCollector?.logExplanation(
3394
- "Analytical component selected and modified",
3395
- result.reasoning || "Selected component based on analytical question",
3396
- {
3397
- componentName: matchedComponent.name,
3398
- componentType: matchedComponent.type,
3399
- confidence: result.confidence,
3400
- propsModified: propsValidation.isModified
3401
- }
3402
- );
3403
- return {
3404
- component: modifiedComponent,
3405
- reasoning: result.reasoning || "Selected and modified component based on analytical question",
3406
- isGenerated: true
3407
- };
3408
- } catch (error) {
3409
- const errorMsg = error instanceof Error ? error.message : String(error);
3410
- logger.error(`[${this.getProviderName()}] Error generating analytical component: ${errorMsg}`);
3411
- logger.debug(`[${this.getProviderName()}] Analytical component generation error details:`, error);
3412
- throw error;
3413
- }
3414
- }
3415
- /**
3416
- * Generate container metadata (title and description) for multi-component dashboard
3417
- */
3418
- async generateContainerMetadata(userPrompt, visualizationTypes, apiKey, logCollector, conversationHistory) {
3419
- try {
3420
- const prompts = await promptLoader.loadPrompts("container-metadata", {
3421
- USER_PROMPT: userPrompt,
3422
- VISUALIZATION_TYPES: visualizationTypes.join(", "),
3423
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3424
- });
3425
- const result = await LLM.stream(
3426
- {
3427
- sys: prompts.system,
3428
- user: prompts.user
3429
- },
3430
- {
3431
- model: this.model,
3432
- maxTokens: 500,
3433
- temperature: 0.3,
3434
- apiKey: this.getApiKey(apiKey)
3435
- },
3436
- true
3437
- // Parse as JSON
3438
- );
3439
- logCollector?.logExplanation(
3440
- "Container metadata generated",
3441
- `Generated title and description for multi-component dashboard`,
3442
- {
3443
- title: result.title,
3444
- description: result.description,
3445
- visualizationTypes
3446
- }
3447
- );
3448
- return {
3449
- title: result.title || `${userPrompt} - Dashboard`,
3450
- description: result.description || `Multi-component dashboard showing ${visualizationTypes.join(", ")}`
3451
- };
3452
- } catch (error) {
3453
- const errorMsg = error instanceof Error ? error.message : String(error);
3454
- logger.error(`[${this.getProviderName()}] Error generating container metadata: ${errorMsg}`);
3455
- logger.debug(`[${this.getProviderName()}] Container metadata error details:`, error);
3456
- return {
3457
- title: `${userPrompt} - Dashboard`,
3458
- description: `Multi-component dashboard showing ${visualizationTypes.join(", ")}`
3459
- };
3460
- }
3461
- }
3462
- /**
3463
- * Match component from a list with enhanced props modification
3464
- */
3465
- async matchComponent(userPrompt, components, apiKey, logCollector, conversationHistory) {
3466
- try {
3467
- const componentsText = components.map((comp, idx) => {
3468
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3469
- const category = comp.category || "general";
3470
- return `${idx + 1}. ID: ${comp.id}
3471
- Name: ${comp.name}
3472
- Type: ${comp.type}
3473
- Category: ${category}
3474
- Description: ${comp.description || "No description"}
3475
- Keywords: ${keywords}`;
3476
- }).join("\n\n");
3477
- const prompts = await promptLoader.loadPrompts("match-component", {
3478
- COMPONENTS_TEXT: componentsText,
3479
- USER_PROMPT: userPrompt,
3480
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3481
- });
3482
- const result = await LLM.stream(
3483
- {
3484
- sys: prompts.system,
3485
- user: prompts.user
3486
- },
3487
- {
3488
- model: this.model,
3489
- maxTokens: 800,
3490
- temperature: 0.2,
3491
- apiKey: this.getApiKey(apiKey)
3492
- },
3493
- true
3494
- // Parse as JSON
3495
- );
3496
- const componentIndex = result.componentIndex;
3497
- const componentId = result.componentId;
3498
- const confidence = result.confidence || 0;
3499
- let component = null;
3500
- if (componentId) {
3501
- component = components.find((c) => c.id === componentId);
3502
- }
3503
- if (!component && componentIndex) {
3504
- component = components[componentIndex - 1];
3505
- }
3506
- const matchedMsg = `${this.getProviderName()} matched component: ${component?.name || "None"}`;
3507
- logger.info(`[${this.getProviderName()}] \u2713 ${matchedMsg}`);
3508
- logCollector?.info(matchedMsg);
3509
- if (result.alternativeMatches && result.alternativeMatches.length > 0) {
3510
- logger.debug(`[${this.getProviderName()}] Alternative matches found: ${result.alternativeMatches.length}`);
3511
- const altMatches = result.alternativeMatches.map(
3512
- (alt) => `${components[alt.index - 1]?.name} (${alt.score}%): ${alt.reason}`
3513
- ).join(" | ");
3514
- logCollector?.info(`Alternative matches: ${altMatches}`);
3515
- result.alternativeMatches.forEach((alt) => {
3516
- logger.debug(`[${this.getProviderName()}] - ${components[alt.index - 1]?.name} (${alt.score}%): ${alt.reason}`);
3517
- });
3518
- }
3519
- if (!component) {
3520
- const noMatchMsg = `No matching component found (confidence: ${confidence}%)`;
3521
- logger.warn(`[${this.getProviderName()}] \u2717 ${noMatchMsg}`);
3522
- logCollector?.warn(noMatchMsg);
3523
- const genMsg = "Attempting to match component from analytical question...";
3524
- logger.info(`[${this.getProviderName()}] \u2713 ${genMsg}`);
3525
- logCollector?.info(genMsg);
3526
- const generatedResult = await this.generateAnalyticalComponent(userPrompt, components, void 0, apiKey, logCollector, conversationHistory);
3527
- if (generatedResult.component) {
3528
- const genSuccessMsg = `Successfully matched component: ${generatedResult.component.name}`;
3529
- logCollector?.info(genSuccessMsg);
3530
- return {
3531
- component: generatedResult.component,
3532
- reasoning: generatedResult.reasoning,
3533
- method: `${this.getProviderName()}-generated`,
3534
- confidence: 100,
3535
- // Generated components are considered 100% match to the question
3536
- propsModified: false,
3537
- queryModified: false
3538
- };
3539
- }
3540
- logCollector?.error("Failed to match component");
3541
- return {
3542
- component: null,
3543
- reasoning: result.reasoning || "No matching component found and unable to match component",
3544
- method: `${this.getProviderName()}-llm`,
3545
- confidence
3546
- };
3547
- }
3548
- let propsModified = false;
3549
- let propsModifications = [];
3550
- let queryModified = false;
3551
- let queryReasoning = "";
3552
- if (component && component.props) {
3553
- const propsValidation = await this.validateAndModifyProps(
3554
- userPrompt,
3555
- component.props,
3556
- component.name,
3557
- component.type,
3558
- component.description,
3559
- apiKey,
3560
- logCollector,
3561
- conversationHistory
3562
- );
3563
- const originalQuery = component.props.query;
3564
- const modifiedQuery = propsValidation.props.query;
3565
- component = {
3566
- ...component,
3567
- props: propsValidation.props
3568
- };
3569
- propsModified = propsValidation.isModified;
3570
- propsModifications = propsValidation.modifications;
3571
- queryModified = originalQuery !== modifiedQuery;
3572
- queryReasoning = propsValidation.reasoning;
3573
- }
3574
- return {
3575
- component,
3576
- reasoning: result.reasoning || "No reasoning provided",
3577
- queryModified,
3578
- queryReasoning,
3579
- propsModified,
3580
- propsModifications,
3581
- method: `${this.getProviderName()}-llm`,
3582
- confidence
3583
- };
3584
- } catch (error) {
3585
- const errorMsg = error instanceof Error ? error.message : String(error);
3586
- logger.error(`[${this.getProviderName()}] Error matching component: ${errorMsg}`);
3587
- logger.debug(`[${this.getProviderName()}] Component matching error details:`, error);
3588
- logCollector?.error(`Error matching component: ${errorMsg}`);
3589
- throw error;
3590
- }
3591
- }
3592
- /**
3593
- * Match multiple components for analytical questions by visualization types
3594
- * This is used when the user needs multiple visualizations
3595
- */
3596
- async generateMultipleAnalyticalComponents(userPrompt, availableComponents, visualizationTypes, apiKey, logCollector, conversationHistory) {
3597
- try {
3598
- console.log("\u2713 Matching multiple components:", visualizationTypes);
3599
- const components = [];
3600
- for (const vizType of visualizationTypes) {
3601
- const result = await this.generateAnalyticalComponent(userPrompt, availableComponents, vizType, apiKey, logCollector, conversationHistory);
3602
- if (result.component) {
3603
- components.push(result.component);
3604
- }
3605
- }
3606
- if (components.length === 0) {
3607
- return {
3608
- components: [],
3609
- reasoning: "Failed to match any components",
3610
- isGenerated: false
3611
- };
3612
- }
3613
- return {
3614
- components,
3615
- reasoning: `Matched ${components.length} components: ${visualizationTypes.join(", ")}`,
3616
- isGenerated: true
3617
- };
3618
- } catch (error) {
3619
- const errorMsg = error instanceof Error ? error.message : String(error);
3620
- logger.error(`[${this.getProviderName()}] Error matching multiple analytical components: ${errorMsg}`);
3621
- logger.debug(`[${this.getProviderName()}] Multiple components matching error details:`, error);
3622
- return {
3623
- components: [],
3624
- reasoning: "Error occurred while matching components",
3625
- isGenerated: false
3626
- };
3627
- }
3628
- }
3629
- /**
3630
- * Match multiple components and wrap them in a container
3631
- */
3632
- async generateMultiComponentResponse(userPrompt, availableComponents, visualizationTypes, apiKey, logCollector, conversationHistory) {
3633
- try {
3634
- const matchResult = await this.generateMultipleAnalyticalComponents(
3635
- userPrompt,
3636
- availableComponents,
3637
- visualizationTypes,
3638
- apiKey,
3639
- logCollector,
3640
- conversationHistory
3641
- );
3642
- if (!matchResult.isGenerated || matchResult.components.length === 0) {
3643
- return {
3644
- containerComponent: null,
3645
- reasoning: matchResult.reasoning || "Unable to match multi-component dashboard",
3646
- isGenerated: false
3647
- };
3648
- }
3649
- const generatedComponents = matchResult.components;
3650
- generatedComponents.forEach((component, index) => {
3651
- if (component.props.query) {
3652
- logCollector?.logQuery(
3653
- `Multi-component query generated (${index + 1}/${generatedComponents.length})`,
3654
- component.props.query,
3655
- {
3656
- componentType: component.type,
3657
- title: component.props.title,
3658
- position: index + 1,
3659
- totalComponents: generatedComponents.length
3660
- }
3661
- );
3662
- }
3663
- });
3664
- const containerTitle = `${userPrompt} - Dashboard`;
3665
- const containerDescription = `Multi-component dashboard showing ${visualizationTypes.join(", ")}`;
3666
- logCollector?.logExplanation(
3667
- "Multi-component dashboard matched",
3668
- matchResult.reasoning || `Matched ${generatedComponents.length} components for comprehensive analysis`,
3669
- {
3670
- totalComponents: generatedComponents.length,
3671
- componentTypes: generatedComponents.map((c) => c.type),
3672
- componentNames: generatedComponents.map((c) => c.name),
3673
- containerTitle,
3674
- containerDescription
3675
- }
3676
- );
3677
- const containerComponent = {
3678
- id: `multi_container_${Date.now()}`,
3679
- name: "MultiComponentContainer",
3680
- type: "Container",
3681
- description: containerDescription,
3682
- category: "dynamic",
3683
- keywords: ["multi", "container", "dashboard"],
3684
- props: {
3685
- config: {
3686
- components: generatedComponents,
3687
- layout: "grid",
3688
- spacing: 24,
3689
- title: containerTitle,
3690
- description: containerDescription
3691
- }
3692
- }
3693
- };
3694
- return {
3695
- containerComponent,
3696
- reasoning: matchResult.reasoning || `Matched multi-component dashboard with ${generatedComponents.length} components`,
3697
- isGenerated: true
3698
- };
3699
- } catch (error) {
3700
- const errorMsg = error instanceof Error ? error.message : String(error);
3701
- logger.error(`[${this.getProviderName()}] Error generating multi-component response: ${errorMsg}`);
3702
- logger.debug(`[${this.getProviderName()}] Multi-component response error details:`, error);
3703
- throw error;
3704
- }
3705
- }
3706
- /**
3707
- * Match components from text response suggestions and generate follow-up questions
3708
- * Takes a text response with component suggestions (c1:type format) and matches with available components
3709
- * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
3710
- * All components are placed in a default MultiComponentContainer layout
3711
- * @param analysisContent - The text response containing component suggestions
3712
- * @param components - List of available components
3713
- * @param apiKey - Optional API key
3714
- * @param logCollector - Optional log collector
3715
- * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
3716
- * @returns Object containing matched components, layout title/description, and follow-up actions
3717
- */
3718
- async matchComponentsFromAnalysis(analysisContent, components, apiKey, logCollector, componentStreamCallback) {
3719
- try {
3720
- logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
3721
- let availableComponentsText = "No components available";
3722
- if (components && components.length > 0) {
3723
- availableComponentsText = components.map((comp, idx) => {
3724
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3725
- const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
3726
- return `${idx + 1}. ID: ${comp.id}
3727
- Name: ${comp.name}
3728
- Type: ${comp.type}
3729
- Description: ${comp.description || "No description"}
3730
- Keywords: ${keywords}
3731
- Props Structure: ${propsPreview}`;
3732
- }).join("\n\n");
3733
- }
3734
- const schemaDoc = schema.generateSchemaDocumentation();
3735
- logger.file("\n=============================\nText analysis response:", analysisContent);
3736
- const prompts = await promptLoader.loadPrompts("match-text-components", {
3737
- ANALYSIS_CONTENT: analysisContent,
3738
- AVAILABLE_COMPONENTS: availableComponentsText,
3739
- SCHEMA_DOC: schemaDoc
3424
+ logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
3425
+ let availableComponentsText = "No components available";
3426
+ if (components && components.length > 0) {
3427
+ availableComponentsText = components.map((comp, idx) => {
3428
+ const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3429
+ const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
3430
+ return `${idx + 1}. ID: ${comp.id}
3431
+ Name: ${comp.name}
3432
+ Type: ${comp.type}
3433
+ Description: ${comp.description || "No description"}
3434
+ Keywords: ${keywords}
3435
+ Props Structure: ${propsPreview}`;
3436
+ }).join("\n\n");
3437
+ }
3438
+ const schemaDoc = schema.generateSchemaDocumentation();
3439
+ logger.file("\n=============================\nText analysis response:", analysisContent);
3440
+ const prompts = await promptLoader.loadPrompts("match-text-components", {
3441
+ ANALYSIS_CONTENT: analysisContent,
3442
+ AVAILABLE_COMPONENTS: availableComponentsText,
3443
+ SCHEMA_DOC: schemaDoc
3740
3444
  });
3741
3445
  logger.debug(`[${this.getProviderName()}] Loaded match-text-components prompts`);
3742
3446
  logger.file("\n=============================\nmatch text components system prompt:", prompts.system);
@@ -3928,148 +3632,136 @@ var BaseLLM = class {
3928
3632
  }
3929
3633
  }
3930
3634
  /**
3931
- * Execute external tools based on user request using agentic LLM tool calling
3932
- * The LLM can directly call tools and retry on errors
3933
- * @param userPrompt - The user's question/request
3934
- * @param availableTools - Array of available external tools
3935
- * @param apiKey - Optional API key for LLM
3936
- * @param logCollector - Optional log collector
3937
- * @returns Object containing tool execution results and summary
3635
+ * Classify user question into category and detect external tools needed
3636
+ * Determines if question is for data analysis, requires external tools, or needs text response
3938
3637
  */
3939
- async executeExternalTools(userPrompt, availableTools, apiKey, logCollector) {
3940
- const MAX_TOOL_ATTEMPTS = 3;
3941
- const toolResults = [];
3638
+ async classifyQuestionCategory(userPrompt, apiKey, logCollector, conversationHistory, externalTools) {
3942
3639
  try {
3943
- logger.debug(`[${this.getProviderName()}] Starting agentic external tool execution`);
3944
- logger.debug(`[${this.getProviderName()}] Available tools: ${availableTools.map((t) => t.name).join(", ")}`);
3945
- const llmTools = availableTools.map((tool) => {
3946
- const properties = {};
3947
- const required = [];
3948
- Object.entries(tool.params || {}).forEach(([key, type]) => {
3949
- properties[key] = {
3950
- type: String(type).toLowerCase(),
3951
- description: `${key} parameter`
3952
- };
3953
- required.push(key);
3954
- });
3955
- return {
3956
- name: tool.id,
3957
- description: tool.description,
3958
- input_schema: {
3959
- type: "object",
3960
- properties,
3961
- required
3962
- }
3963
- };
3640
+ const availableToolsDoc = externalTools && externalTools.length > 0 ? externalTools.map((tool) => {
3641
+ const paramsStr = Object.entries(tool.params || {}).map(([key, type]) => `${key}: ${type}`).join(", ");
3642
+ return `- **${tool.name}** (id: ${tool.id})
3643
+ Description: ${tool.description}
3644
+ Parameters: ${paramsStr}`;
3645
+ }).join("\n\n") : "No external tools available";
3646
+ const prompts = await promptLoader.loadPrompts("category-classification", {
3647
+ USER_PROMPT: userPrompt,
3648
+ CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
3649
+ AVAILABLE_TOOLS: availableToolsDoc
3964
3650
  });
3965
- const toolAttempts = /* @__PURE__ */ new Map();
3966
- const toolHandler = async (toolName, toolInput) => {
3967
- const tool = availableTools.find((t) => t.id === toolName);
3968
- if (!tool) {
3969
- const errorMsg = `Tool ${toolName} not found in available tools`;
3970
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
3971
- logCollector?.error(errorMsg);
3972
- throw new Error(errorMsg);
3973
- }
3974
- const attempts = (toolAttempts.get(toolName) || 0) + 1;
3975
- toolAttempts.set(toolName, attempts);
3976
- logger.info(`[${this.getProviderName()}] Executing tool: ${tool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
3977
- logCollector?.info(`Executing ${tool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
3978
- if (attempts > MAX_TOOL_ATTEMPTS) {
3979
- const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${tool.name}`;
3980
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
3981
- logCollector?.error(errorMsg);
3982
- toolResults.push({
3983
- toolName: tool.name,
3984
- toolId: tool.id,
3985
- result: null,
3986
- error: errorMsg
3987
- });
3988
- throw new Error(errorMsg);
3989
- }
3990
- try {
3991
- logger.debug(`[${this.getProviderName()}] Tool ${tool.name} parameters:`, toolInput);
3992
- const result2 = await tool.fn(toolInput);
3993
- logger.info(`[${this.getProviderName()}] Tool ${tool.name} executed successfully`);
3994
- logCollector?.info(`\u2713 ${tool.name} completed successfully`);
3995
- toolResults.push({
3996
- toolName: tool.name,
3997
- toolId: tool.id,
3998
- result: result2
3999
- });
4000
- return JSON.stringify(result2, null, 2);
4001
- } catch (error) {
4002
- const errorMsg = error instanceof Error ? error.message : String(error);
4003
- logger.error(`[${this.getProviderName()}] Tool ${tool.name} failed (attempt ${attempts}): ${errorMsg}`);
4004
- logCollector?.error(`\u2717 ${tool.name} failed: ${errorMsg}`);
4005
- if (attempts >= MAX_TOOL_ATTEMPTS) {
4006
- toolResults.push({
4007
- toolName: tool.name,
4008
- toolId: tool.id,
4009
- result: null,
4010
- error: errorMsg
4011
- });
4012
- }
4013
- throw new Error(`Tool execution failed: ${errorMsg}`);
3651
+ const result = await LLM.stream(
3652
+ {
3653
+ sys: prompts.system,
3654
+ user: prompts.user
3655
+ },
3656
+ {
3657
+ model: this.model,
3658
+ maxTokens: 1e3,
3659
+ temperature: 0.2,
3660
+ apiKey: this.getApiKey(apiKey)
3661
+ },
3662
+ true
3663
+ // Parse as JSON
3664
+ );
3665
+ logCollector?.logExplanation(
3666
+ "Question category classified",
3667
+ result.reasoning || "No reasoning provided",
3668
+ {
3669
+ category: result.category,
3670
+ externalTools: result.externalTools || [],
3671
+ dataAnalysisType: result.dataAnalysisType,
3672
+ confidence: result.confidence
4014
3673
  }
3674
+ );
3675
+ return {
3676
+ category: result.category || "data_analysis",
3677
+ externalTools: result.externalTools || [],
3678
+ dataAnalysisType: result.dataAnalysisType,
3679
+ reasoning: result.reasoning || "No reasoning provided",
3680
+ confidence: result.confidence || 0
4015
3681
  };
4016
- const prompts = await promptLoader.loadPrompts("execute-tools", {
4017
- USER_PROMPT: userPrompt,
4018
- AVAILABLE_TOOLS: availableTools.map((tool, idx) => {
4019
- const paramsText = Object.entries(tool.params || {}).map(([key, type]) => ` - ${key}: ${type}`).join("\n");
4020
- return `${idx + 1}. ID: ${tool.id}
4021
- Name: ${tool.name}
4022
- Description: ${tool.description}
4023
- Parameters:
4024
- ${paramsText}`;
4025
- }).join("\n\n")
3682
+ } catch (error) {
3683
+ const errorMsg = error instanceof Error ? error.message : String(error);
3684
+ logger.error(`[${this.getProviderName()}] Error classifying question category: ${errorMsg}`);
3685
+ logger.debug(`[${this.getProviderName()}] Category classification error details:`, error);
3686
+ throw error;
3687
+ }
3688
+ }
3689
+ /**
3690
+ * Adapt UI block parameters based on current user question
3691
+ * Takes a matched UI block from semantic search and modifies its props to answer the new question
3692
+ */
3693
+ async adaptUIBlockParameters(currentUserPrompt, originalUserPrompt, matchedUIBlock, apiKey, logCollector) {
3694
+ try {
3695
+ const component = matchedUIBlock?.generatedComponentMetadata || matchedUIBlock?.component;
3696
+ if (!matchedUIBlock || !component) {
3697
+ return {
3698
+ success: false,
3699
+ explanation: "No component found in matched UI block"
3700
+ };
3701
+ }
3702
+ const schemaDoc = schema.generateSchemaDocumentation();
3703
+ const prompts = await promptLoader.loadPrompts("adapt-ui-block-params", {
3704
+ ORIGINAL_USER_PROMPT: originalUserPrompt,
3705
+ CURRENT_USER_PROMPT: currentUserPrompt,
3706
+ MATCHED_UI_BLOCK_COMPONENT: JSON.stringify(component, null, 2),
3707
+ COMPONENT_PROPS: JSON.stringify(component.props, null, 2),
3708
+ SCHEMA_DOC: schemaDoc || "No schema available"
4026
3709
  });
4027
- logger.debug(`[${this.getProviderName()}] Using agentic tool calling for external tools`);
4028
- logCollector?.info("Analyzing request and executing external tools...");
4029
- const result = await LLM.streamWithTools(
3710
+ const result = await LLM.stream(
4030
3711
  {
4031
3712
  sys: prompts.system,
4032
3713
  user: prompts.user
4033
3714
  },
4034
- llmTools,
4035
- toolHandler,
4036
3715
  {
4037
3716
  model: this.model,
4038
3717
  maxTokens: 2e3,
4039
3718
  temperature: 0.2,
4040
3719
  apiKey: this.getApiKey(apiKey)
4041
3720
  },
4042
- MAX_TOOL_ATTEMPTS + 2
4043
- // max iterations: allows for retries + final response
3721
+ true
3722
+ // Parse as JSON
4044
3723
  );
4045
- logger.info(`[${this.getProviderName()}] External tool execution completed`);
4046
- const successfulTools = toolResults.filter((r) => !r.error);
4047
- const failedTools = toolResults.filter((r) => r.error);
4048
- let summary = "";
4049
- if (successfulTools.length > 0) {
4050
- summary += `Successfully executed ${successfulTools.length} tool(s): ${successfulTools.map((t) => t.toolName).join(", ")}.
4051
- `;
4052
- }
4053
- if (failedTools.length > 0) {
4054
- summary += `Failed to execute ${failedTools.length} tool(s): ${failedTools.map((t) => t.toolName).join(", ")}.`;
3724
+ if (!result.success) {
3725
+ logger.info(
3726
+ `[${this.getProviderName()}] Could not adapt UI block: ${result.reason}`
3727
+ );
3728
+ logCollector?.warn(
3729
+ "Could not adapt matched UI block",
3730
+ "explanation",
3731
+ { reason: result.reason }
3732
+ );
3733
+ return {
3734
+ success: false,
3735
+ explanation: result.explanation || "Adaptation not possible"
3736
+ };
4055
3737
  }
4056
- if (toolResults.length === 0) {
4057
- summary = "No external tools were needed for this request.";
3738
+ if (result.adaptedComponent?.props?.query) {
3739
+ result.adaptedComponent.props.query = ensureQueryLimit(
3740
+ result.adaptedComponent.props.query,
3741
+ this.defaultLimit
3742
+ );
4058
3743
  }
4059
- logger.info(`[${this.getProviderName()}] Tool execution summary: ${summary}`);
3744
+ logCollector?.logExplanation(
3745
+ "UI block parameters adapted",
3746
+ result.explanation || "Parameters adapted successfully",
3747
+ {
3748
+ parametersChanged: result.parametersChanged || [],
3749
+ componentType: result.adaptedComponent?.type
3750
+ }
3751
+ );
4060
3752
  return {
4061
- toolResults,
4062
- summary,
4063
- hasResults: successfulTools.length > 0
3753
+ success: true,
3754
+ adaptedComponent: result.adaptedComponent,
3755
+ parametersChanged: result.parametersChanged,
3756
+ explanation: result.explanation || "Parameters adapted successfully"
4064
3757
  };
4065
3758
  } catch (error) {
4066
3759
  const errorMsg = error instanceof Error ? error.message : String(error);
4067
- logger.error(`[${this.getProviderName()}] Error in external tool execution: ${errorMsg}`);
4068
- logCollector?.error(`Error executing external tools: ${errorMsg}`);
3760
+ logger.error(`[${this.getProviderName()}] Error adapting UI block parameters: ${errorMsg}`);
3761
+ logger.debug(`[${this.getProviderName()}] Adaptation error details:`, error);
4069
3762
  return {
4070
- toolResults,
4071
- summary: `Error executing external tools: ${errorMsg}`,
4072
- hasResults: false
3763
+ success: false,
3764
+ explanation: `Error adapting parameters: ${errorMsg}`
4073
3765
  };
4074
3766
  }
4075
3767
  }
@@ -4088,32 +3780,24 @@ ${paramsText}`;
4088
3780
  logger.debug(`[${this.getProviderName()}] Starting text response generation`);
4089
3781
  logger.debug(`[${this.getProviderName()}] User prompt: "${userPrompt.substring(0, 50)}..."`);
4090
3782
  try {
4091
- let externalToolContext = "No external tools were used for this request.";
3783
+ let availableToolsDoc = "No external tools are available for this request.";
4092
3784
  if (externalTools && externalTools.length > 0) {
4093
- logger.info(`[${this.getProviderName()}] Executing external tools...`);
4094
- const toolExecution = await this.executeExternalTools(
4095
- userPrompt,
4096
- externalTools,
4097
- apiKey,
4098
- logCollector
4099
- );
4100
- if (toolExecution.hasResults) {
4101
- const toolResultsText = toolExecution.toolResults.map((tr) => {
4102
- if (tr.error) {
4103
- return `**${tr.toolName}** (Failed): ${tr.error}`;
3785
+ logger.info(`[${this.getProviderName()}] External tools available: ${externalTools.map((t) => t.name).join(", ")}`);
3786
+ availableToolsDoc = "\u26A0\uFE0F **EXECUTE THESE TOOLS IMMEDIATELY** \u26A0\uFE0F\n\nThe following external tools have been identified as necessary for this request. You MUST call them:\n\n" + externalTools.map((tool, idx) => {
3787
+ const paramsText = Object.entries(tool.params || {}).map(([key, value]) => {
3788
+ const valueType = typeof value;
3789
+ if (valueType === "string" && ["string", "number", "integer", "boolean", "array", "object"].includes(String(value).toLowerCase())) {
3790
+ return `- ${key}: ${value}`;
3791
+ } else {
3792
+ return `- ${key}: ${JSON.stringify(value)} (default value - use this)`;
4104
3793
  }
4105
- return `**${tr.toolName}** (Success):
4106
- ${JSON.stringify(tr.result, null, 2)}`;
4107
- }).join("\n\n");
4108
- externalToolContext = `## External Tool Results
4109
-
4110
- ${toolExecution.summary}
4111
-
4112
- ${toolResultsText}`;
4113
- logger.info(`[${this.getProviderName()}] External tools executed, results available`);
4114
- } else {
4115
- logger.info(`[${this.getProviderName()}] No external tools were needed`);
4116
- }
3794
+ }).join("\n ");
3795
+ return `${idx + 1}. **${tool.name}** (ID: ${tool.id})
3796
+ Description: ${tool.description}
3797
+ **ACTION REQUIRED**: Call this tool with the parameters below
3798
+ Parameters:
3799
+ ${paramsText}`;
3800
+ }).join("\n\n");
4117
3801
  }
4118
3802
  const schemaDoc = schema.generateSchemaDocumentation();
4119
3803
  const knowledgeBaseContext = await knowledge_base_default.getKnowledgeBase({
@@ -4122,13 +3806,12 @@ ${toolResultsText}`;
4122
3806
  topK: 1
4123
3807
  });
4124
3808
  logger.file("\n=============================\nknowledge base context:", knowledgeBaseContext);
4125
- logger.file("\n=============================\nexternal tool context:", externalToolContext);
4126
3809
  const prompts = await promptLoader.loadPrompts("text-response", {
4127
3810
  USER_PROMPT: userPrompt,
4128
3811
  CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
4129
3812
  SCHEMA_DOC: schemaDoc,
4130
3813
  KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext || "No additional knowledge base context available.",
4131
- EXTERNAL_TOOL_CONTEXT: externalToolContext
3814
+ AVAILABLE_EXTERNAL_TOOLS: availableToolsDoc
4132
3815
  });
4133
3816
  logger.file("\n=============================\nsystem prompt:", prompts.system);
4134
3817
  logger.file("\n=============================\nuser prompt:", prompts.user);
@@ -4149,12 +3832,89 @@ ${toolResultsText}`;
4149
3832
  type: "string",
4150
3833
  description: "Brief explanation of what this query does and why it answers the user's question."
4151
3834
  }
4152
- },
4153
- required: ["query"]
4154
- }
4155
- }];
3835
+ },
3836
+ required: ["query"],
3837
+ additionalProperties: false
3838
+ }
3839
+ }];
3840
+ if (externalTools && externalTools.length > 0) {
3841
+ externalTools.forEach((tool) => {
3842
+ logger.info(`[${this.getProviderName()}] Processing external tool:`, JSON.stringify(tool, null, 2));
3843
+ const properties = {};
3844
+ const required = [];
3845
+ Object.entries(tool.params || {}).forEach(([key, typeOrValue]) => {
3846
+ let schemaType;
3847
+ let hasDefaultValue = false;
3848
+ let defaultValue;
3849
+ const valueType = typeof typeOrValue;
3850
+ if (valueType === "number") {
3851
+ schemaType = Number.isInteger(typeOrValue) ? "integer" : "number";
3852
+ hasDefaultValue = true;
3853
+ defaultValue = typeOrValue;
3854
+ } else if (valueType === "boolean") {
3855
+ schemaType = "boolean";
3856
+ hasDefaultValue = true;
3857
+ defaultValue = typeOrValue;
3858
+ } else if (Array.isArray(typeOrValue)) {
3859
+ schemaType = "array";
3860
+ hasDefaultValue = true;
3861
+ defaultValue = typeOrValue;
3862
+ } else if (valueType === "object" && typeOrValue !== null) {
3863
+ schemaType = "object";
3864
+ hasDefaultValue = true;
3865
+ defaultValue = typeOrValue;
3866
+ } else {
3867
+ const typeStr = String(typeOrValue).toLowerCase().trim();
3868
+ if (typeStr === "string" || typeStr === "str") {
3869
+ schemaType = "string";
3870
+ } else if (typeStr === "number" || typeStr === "num" || typeStr === "float" || typeStr === "double") {
3871
+ schemaType = "number";
3872
+ } else if (typeStr === "integer" || typeStr === "int") {
3873
+ schemaType = "integer";
3874
+ } else if (typeStr === "boolean" || typeStr === "bool") {
3875
+ schemaType = "boolean";
3876
+ } else if (typeStr === "array" || typeStr === "list") {
3877
+ schemaType = "array";
3878
+ } else if (typeStr === "object" || typeStr === "dict") {
3879
+ schemaType = "object";
3880
+ } else {
3881
+ schemaType = "string";
3882
+ hasDefaultValue = true;
3883
+ defaultValue = typeOrValue;
3884
+ }
3885
+ }
3886
+ const propertySchema = {
3887
+ type: schemaType,
3888
+ description: `${key} parameter for ${tool.name}`
3889
+ };
3890
+ if (hasDefaultValue) {
3891
+ propertySchema.default = defaultValue;
3892
+ } else {
3893
+ required.push(key);
3894
+ }
3895
+ properties[key] = propertySchema;
3896
+ });
3897
+ const inputSchema = {
3898
+ type: "object",
3899
+ properties,
3900
+ additionalProperties: false
3901
+ };
3902
+ if (required.length > 0) {
3903
+ inputSchema.required = required;
3904
+ }
3905
+ tools.push({
3906
+ name: tool.id,
3907
+ description: tool.description,
3908
+ input_schema: inputSchema
3909
+ });
3910
+ });
3911
+ logger.info(`[${this.getProviderName()}] Added ${externalTools.length} external tools to tool calling capability`);
3912
+ logger.info(`[${this.getProviderName()}] Complete tools array:`, JSON.stringify(tools, null, 2));
3913
+ }
4156
3914
  const queryAttempts = /* @__PURE__ */ new Map();
4157
3915
  const MAX_QUERY_ATTEMPTS = 6;
3916
+ const toolAttempts = /* @__PURE__ */ new Map();
3917
+ const MAX_TOOL_ATTEMPTS = 3;
4158
3918
  let maxAttemptsReached = false;
4159
3919
  let fullStreamedText = "";
4160
3920
  const wrappedStreamCallback = streamCallback ? (chunk) => {
@@ -4296,8 +4056,75 @@ ${errorMsg}
4296
4056
  }
4297
4057
  throw new Error(`Query execution failed: ${errorMsg}`);
4298
4058
  }
4059
+ } else {
4060
+ const externalTool = externalTools?.find((t) => t.id === toolName);
4061
+ if (externalTool) {
4062
+ const attempts = (toolAttempts.get(toolName) || 0) + 1;
4063
+ toolAttempts.set(toolName, attempts);
4064
+ logger.info(`[${this.getProviderName()}] Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
4065
+ logCollector?.info(`Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
4066
+ if (attempts > MAX_TOOL_ATTEMPTS) {
4067
+ const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${externalTool.name}`;
4068
+ logger.error(`[${this.getProviderName()}] ${errorMsg}`);
4069
+ logCollector?.error(errorMsg);
4070
+ if (wrappedStreamCallback) {
4071
+ wrappedStreamCallback(`
4072
+
4073
+ \u274C ${errorMsg}
4074
+
4075
+ Please try rephrasing your request or contact support.
4076
+
4077
+ `);
4078
+ }
4079
+ throw new Error(errorMsg);
4080
+ }
4081
+ try {
4082
+ if (wrappedStreamCallback) {
4083
+ if (attempts === 1) {
4084
+ wrappedStreamCallback(`
4085
+
4086
+ \u{1F517} **Executing ${externalTool.name}...**
4087
+
4088
+ `);
4089
+ } else {
4090
+ wrappedStreamCallback(`
4091
+
4092
+ \u{1F504} **Retrying ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...**
4093
+
4094
+ `);
4095
+ }
4096
+ }
4097
+ const result2 = await externalTool.fn(toolInput);
4098
+ logger.info(`[${this.getProviderName()}] External tool ${externalTool.name} executed successfully`);
4099
+ logCollector?.info(`\u2713 ${externalTool.name} executed successfully`);
4100
+ if (wrappedStreamCallback) {
4101
+ wrappedStreamCallback(`\u2705 **${externalTool.name} completed successfully**
4102
+
4103
+ `);
4104
+ }
4105
+ return JSON.stringify(result2, null, 2);
4106
+ } catch (error) {
4107
+ const errorMsg = error instanceof Error ? error.message : String(error);
4108
+ logger.error(`[${this.getProviderName()}] External tool ${externalTool.name} failed (attempt ${attempts}/${MAX_TOOL_ATTEMPTS}): ${errorMsg}`);
4109
+ logCollector?.error(`\u2717 ${externalTool.name} failed: ${errorMsg}`);
4110
+ if (wrappedStreamCallback) {
4111
+ wrappedStreamCallback(`\u274C **${externalTool.name} failed:**
4112
+ \`\`\`
4113
+ ${errorMsg}
4114
+ \`\`\`
4115
+
4116
+ `);
4117
+ if (attempts < MAX_TOOL_ATTEMPTS) {
4118
+ wrappedStreamCallback(`\u{1F527} **Retrying with adjusted parameters...**
4119
+
4120
+ `);
4121
+ }
4122
+ }
4123
+ throw new Error(`Tool execution failed: ${errorMsg}`);
4124
+ }
4125
+ }
4126
+ throw new Error(`Unknown tool: ${toolName}`);
4299
4127
  }
4300
- throw new Error(`Unknown tool: ${toolName}`);
4301
4128
  };
4302
4129
  const result = await LLM.streamWithTools(
4303
4130
  {
@@ -4314,8 +4141,8 @@ ${errorMsg}
4314
4141
  partial: wrappedStreamCallback
4315
4142
  // Pass the wrapped streaming callback to LLM
4316
4143
  },
4317
- 10
4318
- // max iterations: allows for 6 retries + final response + buffer
4144
+ 20
4145
+ // max iterations: allows for 6 query retries + 3 tool retries + final response + buffer
4319
4146
  );
4320
4147
  logger.info(`[${this.getProviderName()}] Text response stream completed`);
4321
4148
  const textResponse = fullStreamedText || result || "I apologize, but I was unable to generate a response.";
@@ -4370,24 +4197,22 @@ ${errorMsg}
4370
4197
  }
4371
4198
  let container_componet = null;
4372
4199
  if (matchedComponents.length > 0) {
4200
+ logger.info(`[${this.getProviderName()}] Created MultiComponentContainer: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4201
+ logCollector?.info(`Created dashboard: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4373
4202
  container_componet = {
4374
- id: `multi_container_${Date.now()}`,
4203
+ id: `container_${Date.now()}`,
4375
4204
  name: "MultiComponentContainer",
4376
4205
  type: "Container",
4377
4206
  description: layoutDescription,
4378
- category: "dynamic",
4379
- keywords: ["dashboard", "layout", "container"],
4380
4207
  props: {
4381
4208
  config: {
4382
- components: matchedComponents,
4383
4209
  title: layoutTitle,
4384
- description: layoutDescription
4210
+ description: layoutDescription,
4211
+ components: matchedComponents
4385
4212
  },
4386
4213
  actions
4387
4214
  }
4388
4215
  };
4389
- logger.info(`[${this.getProviderName()}] Created MultiComponentContainer: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4390
- logCollector?.info(`Created dashboard: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4391
4216
  }
4392
4217
  return {
4393
4218
  success: true,
@@ -4418,201 +4243,134 @@ ${errorMsg}
4418
4243
  }
4419
4244
  }
4420
4245
  /**
4421
- * Generate component response for user question
4422
- * This provides conversational component suggestions based on user question
4423
- * Supports component generation and matching
4246
+ * Main orchestration function with semantic search and multi-step classification
4247
+ * NEW FLOW (Recommended):
4248
+ * 1. Semantic search: Check previous conversations (>60% match)
4249
+ * - If match found → Adapt UI block parameters and return
4250
+ * 2. Category classification: Determine if data_analysis, requires_external_tools, or text_response
4251
+ * 3. Route appropriately based on category and response mode
4252
+ *
4253
+ * @param responseMode - 'component' for component generation (default), 'text' for text responses
4254
+ * @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
4255
+ * @param collections - Collection registry for executing database queries (required for text mode)
4256
+ * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
4424
4257
  */
4425
- async generateComponentResponse(userPrompt, components, apiKey, logCollector, conversationHistory) {
4426
- const errors = [];
4258
+ async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
4259
+ const startTime = Date.now();
4260
+ logger.info(`[${this.getProviderName()}] handleUserRequest called with responseMode: ${responseMode}`);
4261
+ logCollector?.info(`Starting request processing with mode: ${responseMode}`);
4427
4262
  try {
4428
- logger.info(`[${this.getProviderName()}] Using component response mode`);
4429
- const classifyMsg = "Classifying user question...";
4430
- logCollector?.info(classifyMsg);
4431
- const classification = await this.classifyUserQuestion(userPrompt, apiKey, logCollector, conversationHistory);
4432
- const classInfo = `Question type: ${classification.questionType}, Visualizations: ${classification.visualizations.join(", ") || "None"}, Multiple components: ${classification.needsMultipleComponents}`;
4433
- logCollector?.info(classInfo);
4434
- if (classification.questionType === "analytical") {
4435
- if (classification.visualizations.length > 1) {
4436
- const multiMsg = `Matching ${classification.visualizations.length} components for types: ${classification.visualizations.join(", ")}`;
4437
- logCollector?.info(multiMsg);
4438
- const componentPromises = classification.visualizations.map((vizType) => {
4439
- logCollector?.info(`Matching component for type: ${vizType}`);
4440
- return this.generateAnalyticalComponent(
4441
- userPrompt,
4442
- components,
4443
- vizType,
4444
- apiKey,
4445
- logCollector,
4446
- conversationHistory
4447
- ).then((result) => ({ vizType, result }));
4448
- });
4449
- const settledResults = await Promise.allSettled(componentPromises);
4450
- const matchedComponents = [];
4451
- for (const settledResult of settledResults) {
4452
- if (settledResult.status === "fulfilled") {
4453
- const { vizType, result } = settledResult.value;
4454
- if (result.component) {
4455
- matchedComponents.push(result.component);
4456
- logCollector?.info(`Matched: ${result.component.name}`);
4457
- logger.info("Component : ", result.component.name, " props: ", result.component.props);
4458
- } else {
4459
- logCollector?.warn(`Failed to match component for type: ${vizType}`);
4460
- }
4461
- } else {
4462
- logCollector?.warn(`Error matching component: ${settledResult.reason?.message || "Unknown error"}`);
4463
- }
4464
- }
4465
- logger.debug(`[${this.getProviderName()}] Matched ${matchedComponents.length} components for multi-component container`);
4466
- if (matchedComponents.length === 0) {
4467
- return {
4468
- success: true,
4469
- data: {
4470
- component: null,
4471
- reasoning: "Failed to match any components for the requested visualization types",
4472
- method: "classification-multi-failed",
4473
- questionType: classification.questionType,
4474
- needsMultipleComponents: true,
4475
- propsModified: false,
4476
- queryModified: false
4477
- },
4478
- errors: []
4479
- };
4480
- }
4481
- logCollector?.info("Generating container metadata...");
4482
- const containerMetadata = await this.generateContainerMetadata(
4483
- userPrompt,
4484
- classification.visualizations,
4485
- apiKey,
4486
- logCollector,
4487
- conversationHistory
4488
- );
4489
- const containerComponent = {
4490
- id: `multi_container_${Date.now()}`,
4491
- name: "MultiComponentContainer",
4492
- type: "Container",
4493
- description: containerMetadata.description,
4494
- category: "dynamic",
4495
- keywords: ["multi", "container", "dashboard"],
4496
- props: {
4497
- config: {
4498
- components: matchedComponents,
4499
- layout: "grid",
4500
- spacing: 24,
4501
- title: containerMetadata.title,
4502
- description: containerMetadata.description
4503
- }
4504
- }
4505
- };
4506
- logCollector?.info(`Created multi-component container with ${matchedComponents.length} components: "${containerMetadata.title}"`);
4263
+ logger.info(`[${this.getProviderName()}] Step 1: Searching previous conversations...`);
4264
+ logCollector?.info("Step 1: Searching for similar previous conversations...");
4265
+ const conversationMatch = await conversation_search_default.searchConversations({
4266
+ userPrompt,
4267
+ collections,
4268
+ userId,
4269
+ similarityThreshold: 0.6
4270
+ // 60% threshold
4271
+ });
4272
+ logger.info("conversationMatch:", conversationMatch);
4273
+ if (conversationMatch) {
4274
+ logger.info(
4275
+ `[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`
4276
+ );
4277
+ logCollector?.info(
4278
+ `\u2713 Found similar conversation (${(conversationMatch.similarity * 100).toFixed(2)}% match)`
4279
+ );
4280
+ if (conversationMatch.similarity >= 0.99) {
4281
+ const elapsedTime2 = Date.now() - startTime;
4282
+ logger.info(`[${this.getProviderName()}] \u2713 100% match - returning UI block directly without adaptation`);
4283
+ logCollector?.info(`\u2713 Exact match (${(conversationMatch.similarity * 100).toFixed(2)}%) - returning cached result`);
4284
+ logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4285
+ const component = conversationMatch.uiBlock?.generatedComponentMetadata || conversationMatch.uiBlock?.component;
4507
4286
  return {
4508
4287
  success: true,
4509
4288
  data: {
4510
- component: containerComponent,
4511
- reasoning: `Matched ${matchedComponents.length} components for visualization types: ${classification.visualizations.join(", ")}`,
4512
- method: "classification-multi-generated",
4513
- questionType: classification.questionType,
4514
- needsMultipleComponents: true,
4515
- propsModified: false,
4516
- queryModified: false
4289
+ component,
4290
+ reasoning: `Exact match from previous conversation (${(conversationMatch.similarity * 100).toFixed(2)}% similarity)`,
4291
+ method: `${this.getProviderName()}-semantic-match-exact`,
4292
+ semanticSimilarity: conversationMatch.similarity
4517
4293
  },
4518
4294
  errors: []
4519
4295
  };
4520
- } else if (classification.visualizations.length === 1) {
4521
- const vizType = classification.visualizations[0];
4522
- logCollector?.info(`Matching single component for type: ${vizType}`);
4523
- const result = await this.generateAnalyticalComponent(userPrompt, components, vizType, apiKey, logCollector, conversationHistory);
4296
+ }
4297
+ logCollector?.info(`Adapting parameters for similar question...`);
4298
+ const originalPrompt = conversationMatch.metadata?.userPrompt || "Previous question";
4299
+ const adaptResult = await this.adaptUIBlockParameters(
4300
+ userPrompt,
4301
+ originalPrompt,
4302
+ conversationMatch.uiBlock,
4303
+ apiKey,
4304
+ logCollector
4305
+ );
4306
+ if (adaptResult.success && adaptResult.adaptedComponent) {
4307
+ const elapsedTime2 = Date.now() - startTime;
4308
+ logger.info(`[${this.getProviderName()}] \u2713 Successfully adapted UI block parameters`);
4309
+ logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4310
+ logCollector?.info(`\u2713 UI block adapted successfully`);
4311
+ logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4524
4312
  return {
4525
4313
  success: true,
4526
4314
  data: {
4527
- component: result.component,
4528
- reasoning: result.reasoning,
4529
- method: "classification-generated",
4530
- questionType: classification.questionType,
4531
- needsMultipleComponents: false,
4532
- propsModified: false,
4533
- queryModified: false
4315
+ component: adaptResult.adaptedComponent,
4316
+ reasoning: `Adapted from previous conversation: ${originalPrompt}`,
4317
+ method: `${this.getProviderName()}-semantic-match`,
4318
+ semanticSimilarity: conversationMatch.similarity,
4319
+ parametersChanged: adaptResult.parametersChanged
4534
4320
  },
4535
4321
  errors: []
4536
4322
  };
4537
4323
  } else {
4538
- logCollector?.info("No specific visualization type - matching from all components");
4539
- const result = await this.generateAnalyticalComponent(userPrompt, components, void 0, apiKey, logCollector, conversationHistory);
4540
- return {
4541
- success: true,
4542
- data: {
4543
- component: result.component,
4544
- reasoning: result.reasoning,
4545
- method: "classification-generated-auto",
4546
- questionType: classification.questionType,
4547
- needsMultipleComponents: false,
4548
- propsModified: false,
4549
- queryModified: false
4550
- },
4551
- errors: []
4552
- };
4324
+ logger.info(`[${this.getProviderName()}] Could not adapt matched conversation, continuing to category classification`);
4325
+ logCollector?.warn(`Could not adapt matched conversation: ${adaptResult.explanation}`);
4553
4326
  }
4554
- } else if (classification.questionType === "data_modification" || classification.questionType === "general") {
4555
- const matchMsg = "Using component matching for data modification...";
4556
- logCollector?.info(matchMsg);
4557
- const matchResult = await this.matchComponent(userPrompt, components, apiKey, logCollector, conversationHistory);
4558
- return {
4559
- success: true,
4560
- data: {
4561
- component: matchResult.component,
4562
- reasoning: matchResult.reasoning,
4563
- method: "classification-matched",
4564
- questionType: classification.questionType,
4565
- needsMultipleComponents: false,
4566
- propsModified: matchResult.propsModified,
4567
- queryModified: matchResult.queryModified
4568
- },
4569
- errors: []
4570
- };
4571
4327
  } else {
4572
- logCollector?.info("General question - no component needed");
4573
- return {
4574
- success: true,
4575
- data: {
4576
- component: null,
4577
- reasoning: "General question - no component needed",
4578
- method: "classification-general",
4579
- questionType: classification.questionType,
4580
- needsMultipleComponents: false,
4581
- propsModified: false,
4582
- queryModified: false
4583
- },
4584
- errors: []
4585
- };
4328
+ logger.info(`[${this.getProviderName()}] No matching previous conversations found, proceeding to category classification`);
4329
+ logCollector?.info("No similar previous conversations found. Proceeding to category classification...");
4330
+ }
4331
+ logger.info(`[${this.getProviderName()}] Step 2: Classifying question category...`);
4332
+ logCollector?.info("Step 2: Classifying question category...");
4333
+ const categoryClassification = await this.classifyQuestionCategory(
4334
+ userPrompt,
4335
+ apiKey,
4336
+ logCollector,
4337
+ conversationHistory,
4338
+ externalTools
4339
+ );
4340
+ logger.info(
4341
+ `[${this.getProviderName()}] Question classified as: ${categoryClassification.category} (confidence: ${categoryClassification.confidence}%)`
4342
+ );
4343
+ logCollector?.info(
4344
+ `Category: ${categoryClassification.category} | Confidence: ${categoryClassification.confidence}%`
4345
+ );
4346
+ let toolsToUse = [];
4347
+ if (categoryClassification.externalTools && categoryClassification.externalTools.length > 0) {
4348
+ logger.info(`[${this.getProviderName()}] Identified ${categoryClassification.externalTools.length} external tools needed`);
4349
+ logCollector?.info(`Identified external tools: ${categoryClassification.externalTools.map((t) => t.name || t.type).join(", ")}`);
4350
+ toolsToUse = categoryClassification.externalTools?.map((t) => ({
4351
+ id: t.type,
4352
+ name: t.name,
4353
+ description: t.description,
4354
+ params: t.parameters || {},
4355
+ fn: (() => {
4356
+ const realTool = externalTools?.find((tool) => tool.id === t.type);
4357
+ if (realTool) {
4358
+ logger.info(`[${this.getProviderName()}] Using real tool implementation for ${t.type}`);
4359
+ return realTool.fn;
4360
+ } else {
4361
+ logger.warn(`[${this.getProviderName()}] Tool ${t.type} not found in registered tools`);
4362
+ return async () => ({ success: false, message: `Tool ${t.name || t.type} not registered` });
4363
+ }
4364
+ })()
4365
+ })) || [];
4366
+ }
4367
+ if (categoryClassification.category === "data_analysis") {
4368
+ logger.info(`[${this.getProviderName()}] Routing to data analysis (SELECT operations)`);
4369
+ logCollector?.info("Routing to data analysis...");
4370
+ } else if (categoryClassification.category === "data_modification") {
4371
+ logger.info(`[${this.getProviderName()}] Routing to data modification (INSERT/UPDATE/DELETE operations)`);
4372
+ logCollector?.info("Routing to data modification...");
4586
4373
  }
4587
- } catch (error) {
4588
- const errorMsg = error instanceof Error ? error.message : String(error);
4589
- logger.error(`[${this.getProviderName()}] Error generating component response: ${errorMsg}`);
4590
- logger.debug(`[${this.getProviderName()}] Component response generation error details:`, error);
4591
- logCollector?.error(`Error generating component response: ${errorMsg}`);
4592
- errors.push(errorMsg);
4593
- return {
4594
- success: false,
4595
- errors,
4596
- data: void 0
4597
- };
4598
- }
4599
- }
4600
- /**
4601
- * Main orchestration function that classifies question and routes to appropriate handler
4602
- * This is the NEW recommended entry point for handling user requests
4603
- * Supports both component generation and text response modes
4604
- *
4605
- * @param responseMode - 'component' for component generation (default), 'text' for text responses
4606
- * @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
4607
- * @param collections - Collection registry for executing database queries (required for text mode)
4608
- * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
4609
- */
4610
- async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) {
4611
- const startTime = Date.now();
4612
- logger.info(`[${this.getProviderName()}] handleUserRequest called with responseMode: ${responseMode}`);
4613
- if (responseMode === "text") {
4614
- logger.info(`[${this.getProviderName()}] Using text response mode`);
4615
- logCollector?.info("Generating text response...");
4616
4374
  const textResponse = await this.generateTextResponse(
4617
4375
  userPrompt,
4618
4376
  apiKey,
@@ -4621,40 +4379,29 @@ ${errorMsg}
4621
4379
  streamCallback,
4622
4380
  collections,
4623
4381
  components,
4624
- externalTools
4382
+ toolsToUse
4625
4383
  );
4626
- if (!textResponse.success) {
4627
- const elapsedTime3 = Date.now() - startTime;
4628
- logger.error(`[${this.getProviderName()}] Text response generation failed`);
4629
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime3}ms (${(elapsedTime3 / 1e3).toFixed(2)}s)`);
4630
- logCollector?.info(`Total time taken: ${elapsedTime3}ms (${(elapsedTime3 / 1e3).toFixed(2)}s)`);
4631
- return textResponse;
4632
- }
4633
- const elapsedTime2 = Date.now() - startTime;
4634
- logger.info(`[${this.getProviderName()}] Text response generated successfully`);
4635
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4636
- logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4384
+ const elapsedTime = Date.now() - startTime;
4385
+ logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4386
+ logCollector?.info(`Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4637
4387
  return textResponse;
4388
+ } catch (error) {
4389
+ const errorMsg = error instanceof Error ? error.message : String(error);
4390
+ logger.error(`[${this.getProviderName()}] Error in handleUserRequest: ${errorMsg}`);
4391
+ logger.debug(`[${this.getProviderName()}] Error details:`, error);
4392
+ logCollector?.error(`Error processing request: ${errorMsg}`);
4393
+ const elapsedTime = Date.now() - startTime;
4394
+ logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4395
+ logCollector?.info(`Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4396
+ return {
4397
+ success: false,
4398
+ errors: [errorMsg],
4399
+ data: {
4400
+ text: "I apologize, but I encountered an error processing your request. Please try again.",
4401
+ method: `${this.getProviderName()}-orchestration-error`
4402
+ }
4403
+ };
4638
4404
  }
4639
- const componentResponse = await this.generateComponentResponse(
4640
- userPrompt,
4641
- components,
4642
- apiKey,
4643
- logCollector,
4644
- conversationHistory
4645
- );
4646
- if (!componentResponse.success) {
4647
- const elapsedTime2 = Date.now() - startTime;
4648
- logger.error(`[${this.getProviderName()}] Component response generation failed`);
4649
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4650
- logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4651
- return componentResponse;
4652
- }
4653
- const elapsedTime = Date.now() - startTime;
4654
- logger.info(`[${this.getProviderName()}] Component response generated successfully`);
4655
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4656
- logCollector?.info(`Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4657
- return componentResponse;
4658
4405
  }
4659
4406
  /**
4660
4407
  * Generate next questions that the user might ask based on the original prompt and generated component
@@ -4767,7 +4514,7 @@ function getLLMProviders() {
4767
4514
  return DEFAULT_PROVIDERS;
4768
4515
  }
4769
4516
  }
4770
- var useAnthropicMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) => {
4517
+ var useAnthropicMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools, userId) => {
4771
4518
  logger.debug("[useAnthropicMethod] Initializing Anthropic Claude matching method");
4772
4519
  logger.debug(`[useAnthropicMethod] Response mode: ${responseMode}`);
4773
4520
  const msg = `Using Anthropic Claude ${responseMode === "text" ? "text response" : "matching"} method...`;
@@ -4779,11 +4526,11 @@ var useAnthropicMethod = async (prompt, components, apiKey, logCollector, conver
4779
4526
  return { success: false, errors: [emptyMsg] };
4780
4527
  }
4781
4528
  logger.debug(`[useAnthropicMethod] Processing with ${components.length} components`);
4782
- const matchResult = await anthropicLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4529
+ const matchResult = await anthropicLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4783
4530
  logger.info(`[useAnthropicMethod] Successfully generated ${responseMode} using Anthropic`);
4784
4531
  return matchResult;
4785
4532
  };
4786
- var useGroqMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) => {
4533
+ var useGroqMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools, userId) => {
4787
4534
  logger.debug("[useGroqMethod] Initializing Groq LLM matching method");
4788
4535
  logger.debug(`[useGroqMethod] Response mode: ${responseMode}`);
4789
4536
  const msg = `Using Groq LLM ${responseMode === "text" ? "text response" : "matching"} method...`;
@@ -4796,14 +4543,14 @@ var useGroqMethod = async (prompt, components, apiKey, logCollector, conversatio
4796
4543
  return { success: false, errors: [emptyMsg] };
4797
4544
  }
4798
4545
  logger.debug(`[useGroqMethod] Processing with ${components.length} components`);
4799
- const matchResult = await groqLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4546
+ const matchResult = await groqLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4800
4547
  logger.info(`[useGroqMethod] Successfully generated ${responseMode} using Groq`);
4801
4548
  return matchResult;
4802
4549
  };
4803
4550
  var getUserResponseFromCache = async (prompt) => {
4804
4551
  return false;
4805
4552
  };
4806
- var get_user_response = async (prompt, components, anthropicApiKey, groqApiKey, llmProviders, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) => {
4553
+ var get_user_response = async (prompt, components, anthropicApiKey, groqApiKey, llmProviders, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools, userId) => {
4807
4554
  logger.debug(`[get_user_response] Starting user response generation for prompt: "${prompt.substring(0, 50)}..."`);
4808
4555
  logger.debug(`[get_user_response] Response mode: ${responseMode}`);
4809
4556
  logger.debug("[get_user_response] Checking cache for existing response");
@@ -4836,9 +4583,9 @@ var get_user_response = async (prompt, components, anthropicApiKey, groqApiKey,
4836
4583
  logCollector?.info(attemptMsg);
4837
4584
  let result;
4838
4585
  if (provider === "anthropic") {
4839
- result = await useAnthropicMethod(prompt, components, anthropicApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4586
+ result = await useAnthropicMethod(prompt, components, anthropicApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4840
4587
  } else if (provider === "groq") {
4841
- result = await useGroqMethod(prompt, components, groqApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4588
+ result = await useGroqMethod(prompt, components, groqApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4842
4589
  } else {
4843
4590
  logger.warn(`[get_user_response] Unknown provider: ${provider} - skipping`);
4844
4591
  errors.push(`Unknown provider: ${provider}`);
@@ -5055,6 +4802,123 @@ var UILogCollector = class {
5055
4802
  }
5056
4803
  };
5057
4804
 
4805
+ // src/utils/conversation-saver.ts
4806
+ function transformUIBlockForDB(uiblock, userPrompt, uiBlockId) {
4807
+ const component = uiblock?.generatedComponentMetadata && Object.keys(uiblock.generatedComponentMetadata).length > 0 ? uiblock.generatedComponentMetadata : null;
4808
+ return {
4809
+ id: uiBlockId || uiblock?.id || "",
4810
+ component,
4811
+ analysis: uiblock?.textResponse || null,
4812
+ user_prompt: userPrompt || uiblock?.userQuestion || ""
4813
+ };
4814
+ }
4815
+ async function saveConversation(params) {
4816
+ const { userId, userPrompt, uiblock, uiBlockId, threadId, collections } = params;
4817
+ if (!userId) {
4818
+ logger.warn("[CONVERSATION_SAVER] Skipping save: userId not provided");
4819
+ return {
4820
+ success: false,
4821
+ error: "userId is required"
4822
+ };
4823
+ }
4824
+ if (!userPrompt) {
4825
+ logger.warn("[CONVERSATION_SAVER] Skipping save: userPrompt not provided");
4826
+ return {
4827
+ success: false,
4828
+ error: "userPrompt is required"
4829
+ };
4830
+ }
4831
+ if (!uiblock) {
4832
+ logger.warn("[CONVERSATION_SAVER] Skipping save: uiblock not provided");
4833
+ return {
4834
+ success: false,
4835
+ error: "uiblock is required"
4836
+ };
4837
+ }
4838
+ if (!threadId) {
4839
+ logger.warn("[CONVERSATION_SAVER] Skipping save: threadId not provided");
4840
+ return {
4841
+ success: false,
4842
+ error: "threadId is required"
4843
+ };
4844
+ }
4845
+ if (!uiBlockId) {
4846
+ logger.warn("[CONVERSATION_SAVER] Skipping save: uiBlockId not provided");
4847
+ return {
4848
+ success: false,
4849
+ error: "uiBlockId is required"
4850
+ };
4851
+ }
4852
+ if (!collections?.["user-conversations"]?.["create"]) {
4853
+ logger.debug('[CONVERSATION_SAVER] Collection "user-conversations.create" not available, skipping save');
4854
+ return {
4855
+ success: false,
4856
+ error: "user-conversations.create collection not available"
4857
+ };
4858
+ }
4859
+ try {
4860
+ logger.info(`[CONVERSATION_SAVER] Saving conversation for userId: ${userId}, uiBlockId: ${uiBlockId}, threadId: ${threadId}`);
4861
+ const userIdNumber = Number(userId);
4862
+ if (isNaN(userIdNumber)) {
4863
+ logger.warn(`[CONVERSATION_SAVER] Invalid userId: ${userId} (not a valid number)`);
4864
+ return {
4865
+ success: false,
4866
+ error: `Invalid userId: ${userId} (not a valid number)`
4867
+ };
4868
+ }
4869
+ const dbUIBlock = transformUIBlockForDB(uiblock, userPrompt, uiBlockId);
4870
+ logger.debug(`[CONVERSATION_SAVER] Transformed UIBlock for DB: ${JSON.stringify(dbUIBlock)}`);
4871
+ const saveResult = await collections["user-conversations"]["create"]({
4872
+ userId: userIdNumber,
4873
+ userPrompt,
4874
+ uiblock: dbUIBlock,
4875
+ threadId
4876
+ });
4877
+ if (!saveResult?.success) {
4878
+ logger.warn(`[CONVERSATION_SAVER] Failed to save conversation to PostgreSQL: ${saveResult?.message || "Unknown error"}`);
4879
+ return {
4880
+ success: false,
4881
+ error: saveResult?.message || "Unknown error from backend"
4882
+ };
4883
+ }
4884
+ logger.info(`[CONVERSATION_SAVER] Successfully saved conversation to PostgreSQL, id: ${saveResult.data?.id}`);
4885
+ if (collections?.["conversation-history"]?.["embed"]) {
4886
+ try {
4887
+ logger.info("[CONVERSATION_SAVER] Creating embedding for semantic search...");
4888
+ const embedResult = await collections["conversation-history"]["embed"]({
4889
+ uiBlockId,
4890
+ userPrompt,
4891
+ uiBlock: dbUIBlock,
4892
+ // Use the transformed UIBlock
4893
+ userId: userIdNumber
4894
+ });
4895
+ if (embedResult?.success) {
4896
+ logger.info("[CONVERSATION_SAVER] Successfully created embedding");
4897
+ } else {
4898
+ logger.warn("[CONVERSATION_SAVER] Failed to create embedding:", embedResult?.error || "Unknown error");
4899
+ }
4900
+ } catch (embedError) {
4901
+ const embedErrorMsg = embedError instanceof Error ? embedError.message : String(embedError);
4902
+ logger.warn("[CONVERSATION_SAVER] Error creating embedding:", embedErrorMsg);
4903
+ }
4904
+ } else {
4905
+ logger.debug("[CONVERSATION_SAVER] Embedding collection not available, skipping ChromaDB storage");
4906
+ }
4907
+ return {
4908
+ success: true,
4909
+ conversationId: saveResult.data?.id,
4910
+ message: "Conversation saved successfully"
4911
+ };
4912
+ } catch (error) {
4913
+ const errorMessage = error instanceof Error ? error.message : String(error);
4914
+ logger.error("[CONVERSATION_SAVER] Error saving conversation:", errorMessage);
4915
+ return {
4916
+ success: false,
4917
+ error: errorMessage
4918
+ };
4919
+ }
4920
+ }
4921
+
5058
4922
  // src/config/context.ts
5059
4923
  var CONTEXT_CONFIG = {
5060
4924
  /**
@@ -5066,7 +4930,7 @@ var CONTEXT_CONFIG = {
5066
4930
  };
5067
4931
 
5068
4932
  // src/handlers/user-prompt-request.ts
5069
- var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools) => {
4933
+ var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools, userId) => {
5070
4934
  const errors = [];
5071
4935
  logger.debug("[USER_PROMPT_REQ] Parsing incoming message data");
5072
4936
  const parseResult = UserPromptRequestMessageSchema.safeParse(data);
@@ -5147,7 +5011,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
5147
5011
  responseMode,
5148
5012
  streamCallback,
5149
5013
  collections,
5150
- externalTools
5014
+ externalTools,
5015
+ userId
5151
5016
  );
5152
5017
  logCollector.info("User prompt request completed");
5153
5018
  const uiBlockId = existingUiBlockId;
@@ -5198,6 +5063,34 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
5198
5063
  }
5199
5064
  thread.addUIBlock(uiBlock);
5200
5065
  logger.info(`Created UIBlock: ${uiBlockId} in Thread: ${threadId}`);
5066
+ if (userId) {
5067
+ const responseMethod = userResponse.data?.method || "";
5068
+ const semanticSimilarity = userResponse.data?.semanticSimilarity || 0;
5069
+ const isExactMatch = responseMethod.includes("semantic-match") && semanticSimilarity >= 0.99;
5070
+ if (isExactMatch) {
5071
+ logger.info(
5072
+ `Skipping conversation save - response from exact semantic match (${(semanticSimilarity * 100).toFixed(2)}% similarity)`
5073
+ );
5074
+ logCollector.info(
5075
+ `Using exact cached result (${(semanticSimilarity * 100).toFixed(2)}% match) - not saving duplicate conversation`
5076
+ );
5077
+ } else {
5078
+ const uiBlockData = uiBlock.toJSON();
5079
+ const saveResult = await saveConversation({
5080
+ userId,
5081
+ userPrompt: prompt,
5082
+ uiblock: uiBlockData,
5083
+ uiBlockId,
5084
+ threadId,
5085
+ collections
5086
+ });
5087
+ if (saveResult.success) {
5088
+ logger.info(`Conversation saved with ID: ${saveResult.conversationId}`);
5089
+ } else {
5090
+ logger.warn(`Failed to save conversation: ${saveResult.error}`);
5091
+ }
5092
+ }
5093
+ }
5201
5094
  return {
5202
5095
  success: userResponse.success,
5203
5096
  data: userResponse.data,
@@ -5208,8 +5101,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
5208
5101
  wsId
5209
5102
  };
5210
5103
  };
5211
- async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools) {
5212
- const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools);
5104
+ async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools, userId) {
5105
+ const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools, userId);
5213
5106
  sendDataResponse4(
5214
5107
  response.id || data.id,
5215
5108
  {
@@ -6296,13 +6189,15 @@ async function handleBookmarksRequest(data, collections, sendMessage) {
6296
6189
  const { id, payload, from } = request;
6297
6190
  const { operation, data: requestData } = payload;
6298
6191
  const bookmarkId = requestData?.id;
6192
+ const userId = requestData?.userId;
6193
+ const threadId = requestData?.threadId;
6299
6194
  const uiblock = requestData?.uiblock;
6300
6195
  switch (operation) {
6301
6196
  case "create":
6302
- await handleCreate4(id, uiblock, executeCollection, sendMessage, from.id);
6197
+ await handleCreate4(id, userId, threadId, uiblock, executeCollection, sendMessage, from.id);
6303
6198
  break;
6304
6199
  case "update":
6305
- await handleUpdate4(id, bookmarkId, uiblock, executeCollection, sendMessage, from.id);
6200
+ await handleUpdate4(id, bookmarkId, threadId, uiblock, executeCollection, sendMessage, from.id);
6306
6201
  break;
6307
6202
  case "delete":
6308
6203
  await handleDelete4(id, bookmarkId, executeCollection, sendMessage, from.id);
@@ -6313,6 +6208,12 @@ async function handleBookmarksRequest(data, collections, sendMessage) {
6313
6208
  case "getOne":
6314
6209
  await handleGetOne4(id, bookmarkId, executeCollection, sendMessage, from.id);
6315
6210
  break;
6211
+ case "getByUser":
6212
+ await handleGetByUser(id, userId, threadId, executeCollection, sendMessage, from.id);
6213
+ break;
6214
+ case "getByThread":
6215
+ await handleGetByThread(id, threadId, executeCollection, sendMessage, from.id);
6216
+ break;
6316
6217
  default:
6317
6218
  sendResponse6(id, {
6318
6219
  success: false,
@@ -6327,7 +6228,14 @@ async function handleBookmarksRequest(data, collections, sendMessage) {
6327
6228
  }, sendMessage);
6328
6229
  }
6329
6230
  }
6330
- async function handleCreate4(id, uiblock, executeCollection, sendMessage, clientId) {
6231
+ async function handleCreate4(id, userId, threadId, uiblock, executeCollection, sendMessage, clientId) {
6232
+ if (!userId) {
6233
+ sendResponse6(id, {
6234
+ success: false,
6235
+ error: "userId is required"
6236
+ }, sendMessage, clientId);
6237
+ return;
6238
+ }
6331
6239
  if (!uiblock) {
6332
6240
  sendResponse6(id, {
6333
6241
  success: false,
@@ -6336,7 +6244,7 @@ async function handleCreate4(id, uiblock, executeCollection, sendMessage, client
6336
6244
  return;
6337
6245
  }
6338
6246
  try {
6339
- const result = await executeCollection("bookmarks", "create", { uiblock });
6247
+ const result = await executeCollection("bookmarks", "create", { userId, threadId, uiblock });
6340
6248
  sendResponse6(id, {
6341
6249
  success: true,
6342
6250
  data: result.data,
@@ -6350,7 +6258,7 @@ async function handleCreate4(id, uiblock, executeCollection, sendMessage, client
6350
6258
  }, sendMessage, clientId);
6351
6259
  }
6352
6260
  }
6353
- async function handleUpdate4(id, bookmarkId, uiblock, executeCollection, sendMessage, clientId) {
6261
+ async function handleUpdate4(id, bookmarkId, threadId, uiblock, executeCollection, sendMessage, clientId) {
6354
6262
  if (!bookmarkId) {
6355
6263
  sendResponse6(id, {
6356
6264
  success: false,
@@ -6366,7 +6274,7 @@ async function handleUpdate4(id, bookmarkId, uiblock, executeCollection, sendMes
6366
6274
  return;
6367
6275
  }
6368
6276
  try {
6369
- const result = await executeCollection("bookmarks", "update", { id: bookmarkId, uiblock });
6277
+ const result = await executeCollection("bookmarks", "update", { id: bookmarkId, threadId, uiblock });
6370
6278
  sendResponse6(id, {
6371
6279
  success: true,
6372
6280
  data: result.data,
@@ -6443,6 +6351,54 @@ async function handleGetOne4(id, bookmarkId, executeCollection, sendMessage, cli
6443
6351
  }, sendMessage, clientId);
6444
6352
  }
6445
6353
  }
6354
+ async function handleGetByUser(id, userId, threadId, executeCollection, sendMessage, clientId) {
6355
+ if (!userId) {
6356
+ sendResponse6(id, {
6357
+ success: false,
6358
+ error: "userId is required"
6359
+ }, sendMessage, clientId);
6360
+ return;
6361
+ }
6362
+ try {
6363
+ const result = await executeCollection("bookmarks", "getByUser", { userId, threadId });
6364
+ sendResponse6(id, {
6365
+ success: true,
6366
+ data: result.data,
6367
+ count: result.count,
6368
+ message: `Retrieved ${result.count} bookmarks for user ${userId}`
6369
+ }, sendMessage, clientId);
6370
+ logger.info(`Retrieved bookmarks for user ${userId} (count: ${result.count})`);
6371
+ } catch (error) {
6372
+ sendResponse6(id, {
6373
+ success: false,
6374
+ error: error instanceof Error ? error.message : "Failed to get bookmarks by user"
6375
+ }, sendMessage, clientId);
6376
+ }
6377
+ }
6378
+ async function handleGetByThread(id, threadId, executeCollection, sendMessage, clientId) {
6379
+ if (!threadId) {
6380
+ sendResponse6(id, {
6381
+ success: false,
6382
+ error: "threadId is required"
6383
+ }, sendMessage, clientId);
6384
+ return;
6385
+ }
6386
+ try {
6387
+ const result = await executeCollection("bookmarks", "getByThread", { threadId });
6388
+ sendResponse6(id, {
6389
+ success: true,
6390
+ data: result.data,
6391
+ count: result.count,
6392
+ message: `Retrieved ${result.count} bookmarks for thread ${threadId}`
6393
+ }, sendMessage, clientId);
6394
+ logger.info(`Retrieved bookmarks for thread ${threadId} (count: ${result.count})`);
6395
+ } catch (error) {
6396
+ sendResponse6(id, {
6397
+ success: false,
6398
+ error: error instanceof Error ? error.message : "Failed to get bookmarks by thread"
6399
+ }, sendMessage, clientId);
6400
+ }
6401
+ }
6446
6402
  function sendResponse6(id, res, sendMessage, clientId) {
6447
6403
  const response = {
6448
6404
  id: id || "unknown",
@@ -7429,17 +7385,17 @@ var SuperatomSDK = class {
7429
7385
  });
7430
7386
  break;
7431
7387
  case "AUTH_LOGIN_REQ":
7432
- handleAuthLoginRequest(parsed, (msg) => this.send(msg)).catch((error) => {
7388
+ handleAuthLoginRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
7433
7389
  logger.error("Failed to handle auth login request:", error);
7434
7390
  });
7435
7391
  break;
7436
7392
  case "AUTH_VERIFY_REQ":
7437
- handleAuthVerifyRequest(parsed, (msg) => this.send(msg)).catch((error) => {
7393
+ handleAuthVerifyRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
7438
7394
  logger.error("Failed to handle auth verify request:", error);
7439
7395
  });
7440
7396
  break;
7441
7397
  case "USER_PROMPT_REQ":
7442
- handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.llmProviders, this.collections, this.tools).catch((error) => {
7398
+ handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.llmProviders, this.collections, this.tools, this.userId).catch((error) => {
7443
7399
  logger.error("Failed to handle user prompt request:", error);
7444
7400
  });
7445
7401
  break;