@superatomai/sdk-node 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -562,6 +562,25 @@ var ReportsRequestMessageSchema = import_zod3.z.object({
562
562
  type: import_zod3.z.literal("REPORTS"),
563
563
  payload: ReportsRequestPayloadSchema
564
564
  });
565
+ var UIBlockSchema = import_zod3.z.object({
566
+ id: import_zod3.z.string().optional(),
567
+ userQuestion: import_zod3.z.string().optional(),
568
+ text: import_zod3.z.string().optional(),
569
+ textResponse: import_zod3.z.string().optional(),
570
+ component: ComponentSchema.optional(),
571
+ // Legacy field
572
+ generatedComponentMetadata: ComponentSchema.optional(),
573
+ // Actual field used by UIBlock class
574
+ componentData: import_zod3.z.record(import_zod3.z.any()).optional(),
575
+ actions: import_zod3.z.array(import_zod3.z.any()).optional(),
576
+ isFetchingActions: import_zod3.z.boolean().optional(),
577
+ createdAt: import_zod3.z.string().optional(),
578
+ metadata: import_zod3.z.object({
579
+ timestamp: import_zod3.z.number().optional(),
580
+ userPrompt: import_zod3.z.string().optional(),
581
+ similarity: import_zod3.z.number().optional()
582
+ }).optional()
583
+ });
565
584
  var BookmarkDataSchema = import_zod3.z.object({
566
585
  id: import_zod3.z.number().optional(),
567
586
  uiblock: import_zod3.z.any(),
@@ -1829,489 +1848,126 @@ var import_path2 = __toESM(require("path"));
1829
1848
 
1830
1849
  // src/userResponse/prompts.ts
1831
1850
  var PROMPTS = {
1832
- "classify": {
1833
- system: `You are an expert AI that classifies user questions about data and determines the appropriate visualizations needed.
1834
-
1835
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
1836
-
1837
- ## Previous Conversation
1838
- {{CONVERSATION_HISTORY}}
1839
-
1840
- **Note:** If there is previous conversation history, use it to understand context. For example:
1841
- - If user previously asked about "sales" and now asks "show me trends", understand it refers to sales trends
1842
- - If user asked for "revenue by region" and now says "make it a pie chart", understand they want to modify the previous visualization
1843
- - Use the history to resolve ambiguous references like "that", "it", "them", "the data"
1844
-
1845
- Your task is to analyze the user's question and determine:
1846
-
1847
- 1. **Question Type:**
1848
- - "analytical": Questions asking to VIEW, ANALYZE, or VISUALIZE data
1849
-
1850
- - "data_modification": Questions asking to CREATE, UPDATE, DELETE, or MODIFY data
1851
-
1852
- - "general": General questions, greetings, or requests not related to data
1853
-
1854
- 2. **Required Visualizations** (only for analytical questions):
1855
- Determine which visualization type(s) would BEST answer the user's question:
1856
-
1857
- - **KPICard**: Single metric, total, count, average, percentage, or summary number
1858
-
1859
- - **LineChart**: Trends over time, time series, growth/decline patterns
1860
-
1861
-
1862
- - **BarChart**: Comparing categories, rankings, distributions across groups
1863
-
1864
-
1865
- - **PieChart**: Proportions, percentages, composition, market share
1866
-
1867
-
1868
- - **DataTable**: Detailed lists, multiple attributes, when user needs to see records
1869
-
1870
-
1871
- 3. **Multiple Visualizations:**
1872
- User may need MULTIPLE visualizations together:
1873
-
1874
- Common combinations:
1875
- - KPICard + LineChart
1876
- - KPICard + BarChart
1877
- - KPICard + DataTable
1878
- - BarChart + PieChart:
1879
- - LineChart + DataTable
1880
- Set needsMultipleComponents to true if user needs multiple views of the data.
1851
+ "text-response": {
1852
+ 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.
1881
1853
 
1882
- **Important Guidelines:**
1883
- - If user explicitly mentions a chart type RESPECT that preference
1884
- - If question is vague or needs both summary and detail, suggest KPICard + DataTable
1885
- - Only return visualizations for "analytical" questions
1886
- - For "data_modification" or "general", return empty array for visualizations
1854
+ ## Your Task
1887
1855
 
1888
- **Output Format:**
1889
- {
1890
- "questionType": "analytical" | "data_modification" | "general",
1891
- "visualizations": ["KPICard", "LineChart", ...], // Empty array if not analytical
1892
- "reasoning": "Explanation of classification and visualization choices",
1893
- "needsMultipleComponents": boolean
1894
- }
1895
- `,
1896
- user: `{{USER_PROMPT}}
1897
- `
1898
- },
1899
- "match-component": {
1900
- system: `You are an expert AI assistant specialized in matching user requests to the most appropriate data visualization components.
1856
+ Analyze the user's question and provide a helpful text response. Your response should:
1901
1857
 
1902
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
1858
+ 1. **Be Clear and Concise**: Provide direct answers without unnecessary verbosity
1859
+ 2. **Be Contextual**: Use conversation history to understand what the user is asking about
1860
+ 3. **Be Accurate**: Provide factually correct information based on the context
1861
+ 4. **Be Helpful**: Offer additional relevant information or suggestions when appropriate
1903
1862
 
1904
- ## Previous Conversation
1905
- {{CONVERSATION_HISTORY}}
1863
+ ## Available Tools
1906
1864
 
1907
- **Context Instructions:**
1908
- - If there is conversation history, use it to understand what the user is referring to
1909
- - When user says "show that as a chart" or "change it", they are referring to a previous component
1910
- - If user asks to "modify" or "update" something, match to the component they previously saw
1911
- - Use context to resolve ambiguous requests like "show trends for that" or "make it interactive"
1912
-
1913
- Your task is to analyze the user's natural language request and find the BEST matching component from the available list.
1914
-
1915
- Available Components:
1916
- {{COMPONENTS_TEXT}}
1917
-
1918
- **Matching Guidelines:**
1919
-
1920
- 1. **Understand User Intent:**
1921
- - What type of data visualization do they need? (KPI/metric, chart, table, etc.)
1922
- - What metric or data are they asking about? (revenue, orders, customers, etc.)
1923
- - Are they asking for a summary (KPI), trend (line chart), distribution (bar/pie), or detailed list (table)?
1924
- - Do they want to compare categories, see trends over time, or show proportions?
1925
-
1926
- 2. **Component Type Matching:**
1927
- - KPICard: Single metric/number (total, average, count, percentage, rate)
1928
- - LineChart: Trends over time, time series data
1929
- - BarChart: Comparing categories, distributions, rankings
1930
- - PieChart/DonutChart: Proportions, percentages, market share
1931
- - DataTable: Detailed lists, rankings with multiple attributes
1932
-
1933
- 3. **Keyword & Semantic Matching:**
1934
- - Match user query terms with component keywords
1935
- - Consider synonyms (e.g., "sales" = "revenue", "items" = "products")
1936
- - Look for category matches (financial, orders, customers, products, suppliers, logistics, geographic, operations)
1937
-
1938
- 4. **Scoring Criteria:**
1939
- - Exact keyword matches: High priority
1940
- - Component type alignment: High priority
1941
- - Category alignment: Medium priority
1942
- - Semantic similarity: Medium priority
1943
- - Specificity: Prefer more specific components over generic ones
1944
-
1945
- **Output Requirements:**
1946
-
1947
- Respond with a JSON object containing:
1948
- - componentIndex: the 1-based index of the BEST matching component (or null if confidence < 50%)
1949
- - componentId: the ID of the matched component
1950
- - reasoning: detailed explanation of why this component was chosen
1951
- - confidence: confidence score 0-100 (100 = perfect match)
1952
- - alternativeMatches: array of up to 2 alternative component indices with scores (optional)
1953
-
1954
- Example response:
1955
- {
1956
- "componentIndex": 5,
1957
- "componentId": "total_revenue_kpi",
1958
- "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'.",
1959
- "confidence": 95,
1960
- "alternativeMatches": [
1961
- {"index": 3, "id": "monthly_revenue_kpi", "score": 75, "reason": "Could show monthly revenue if time period was intended"},
1962
- {"index": 8, "id": "revenue_trend_chart", "score": 60, "reason": "Could show revenue trend if historical view was intended"}
1963
- ]
1964
- }
1865
+ The following external tools are available for this request (if applicable):
1965
1866
 
1966
- **Important:**
1967
- - Only return componentIndex if confidence >= 50%
1968
- - Return null if no reasonable match exists
1969
- - Prefer components that exactly match the user's metric over generic ones
1970
- - Consider the full context of the request, not just individual words`,
1971
- user: `Current user request: {{USER_PROMPT}}
1867
+ {{AVAILABLE_EXTERNAL_TOOLS}}
1972
1868
 
1973
- Find the best matching component considering the conversation history above. Explain your reasoning with a confidence score. Return ONLY valid JSON.`
1974
- },
1975
- "modify-props": {
1976
- system: `You are an AI assistant that validates and modifies component props based on user requests.
1869
+ When a tool is needed to complete the user's request:
1870
+ 1. **Analyze the request** to determine which tool(s) are needed
1871
+ 2. **Extract parameters** from the user's question that the tool requires
1872
+ 3. **Execute the tool** by calling it with the extracted parameters
1873
+ 4. **Present the results** in your response in a clear, user-friendly format
1874
+ 5. **Combine with other data** if the user's request requires both database queries and external tool results
1977
1875
 
1978
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
1876
+ ## Handling Data Questions
1979
1877
 
1980
- Given:
1981
- - A user's natural language request
1982
- - Component name: {{COMPONENT_NAME}}
1983
- - Component type: {{COMPONENT_TYPE}}
1984
- - Component description: {{COMPONENT_DESCRIPTION}}
1878
+ When the user asks about data
1985
1879
 
1986
- -
1987
- - Current component props with structure:
1988
- {
1989
- query?: string, // SQL query to fetch data
1990
- title?: string, // Component title
1991
- description?: string, // Component description
1992
- config?: { // Additional configuration
1993
- [key: string]: any
1994
- }
1995
- }
1880
+ 1. **Generate a SQL query** using the database schema provided above
1881
+ 2. **Use the execute_query tool** to run the query
1882
+ 3. **If the query fails**, analyze the error and generate a corrected query
1883
+ 4. **Format the results** in a clear, readable way for the user
1996
1884
 
1997
- Schema definition for the prop that must be passed to the component
1998
- -{{CURRENT_PROPS}}
1885
+ **Query Guidelines:**
1886
+ - Use correct table and column names from the schema
1887
+ - ALWAYS include a LIMIT clause with a MAXIMUM of 32 rows
1888
+ - Ensure valid SQL syntax
1889
+ - For time-based queries, use appropriate date functions
1890
+ - When using subqueries with scalar operators (=, <, >, etc.), add LIMIT 1 to prevent "more than one row" errors
1999
1891
 
2000
- Database Schema:
1892
+ ## Database Schema
2001
1893
  {{SCHEMA_DOC}}
2002
1894
 
2003
- ## Previous Conversation
2004
- {{CONVERSATION_HISTORY}}
2005
-
2006
- **Context Instructions:**
2007
- - Review the conversation history to understand the evolution of the component
2008
- - If user says "add filter for X", understand they want to modify the current query
2009
- - If user says "change to last month" or "filter by Y", apply modifications to existing query
2010
- - Previous questions can clarify what the user means by ambiguous requests like "change that filter"
2011
- - Use context to determine appropriate time ranges if user says "recent" or "latest"
2012
-
2013
- Your task is to intelligently modify the props based on the user's request:
1895
+ **Database Type: PostgreSQL**
2014
1896
 
2015
- 1. **Query Modification**:
2016
- - Modify SQL query if user requests different data, filters, time ranges, limits, or aggregations
2017
- - Use correct table and column names from the schema
2018
- - Ensure valid SQL syntax
2019
- - ALWAYS include a LIMIT clause (default: {{DEFAULT_LIMIT}} rows) to prevent large result sets
2020
- - Preserve the query structure that the component expects (e.g., column aliases)
1897
+ **CRITICAL PostgreSQL Query Rules:**
2021
1898
 
2022
- **CRITICAL - PostgreSQL Query Rules:**
1899
+ 1. **NO AGGREGATE FUNCTIONS IN WHERE CLAUSE** - This is a fundamental SQL error
1900
+ \u274C WRONG: \`WHERE COUNT(orders) > 0\`
1901
+ \u274C WRONG: \`WHERE SUM(price) > 100\`
1902
+ \u274C WRONG: \`WHERE AVG(rating) > 4.5\`
1903
+ \u274C WRONG: \`WHERE FLOOR(AVG(rating)) = 4\` (aggregate inside any function is still not allowed)
1904
+ \u274C WRONG: \`WHERE ROUND(SUM(price), 2) > 100\`
2023
1905
 
2024
- **NO AGGREGATE FUNCTIONS IN WHERE CLAUSE:**
2025
- \u274C WRONG: \`WHERE COUNT(orders) > 0\` or \`WHERE SUM(price) > 100\`
2026
1906
  \u2705 CORRECT: Use HAVING (with GROUP BY), EXISTS, or subquery
1907
+ \u2705 CORRECT: Move aggregate logic to HAVING: \`GROUP BY ... HAVING FLOOR(AVG(rating)) = 4\`
1908
+ \u2705 CORRECT: Use subquery for filtering: \`WHERE product_id IN (SELECT product_id FROM ... GROUP BY ... HAVING AVG(rating) >= 4)\`
2027
1909
 
2028
-
2029
- **WHERE vs HAVING:**
1910
+ 2. **WHERE vs HAVING**
2030
1911
  - WHERE filters rows BEFORE grouping (cannot use aggregates)
2031
1912
  - HAVING filters groups AFTER grouping (can use aggregates)
2032
1913
  - If using HAVING, you MUST have GROUP BY
2033
1914
 
2034
- **Subquery Rules:**
2035
- - When using a subquery with scalar comparison operators (=, <, >, <=, >=, <>), the subquery MUST return exactly ONE row
2036
- - ALWAYS add \`LIMIT 1\` to scalar subqueries to prevent "more than one row returned" errors
2037
- - Example: \`WHERE location_id = (SELECT store_id FROM orders ORDER BY total_amount DESC LIMIT 1)\`
2038
- - For multiple values, use \`IN\` instead: \`WHERE location_id IN (SELECT store_id FROM orders)\`
2039
- - Test your subqueries mentally: if they could return multiple rows, add LIMIT 1 or use IN
2040
-
2041
- 2. **Title Modification**:
2042
- - Update title to reflect the user's specific request
2043
- - Keep it concise and descriptive
2044
- - Match the tone of the original title
2045
-
2046
- 3. **Description Modification**:
2047
- - Update description to explain what data is shown
2048
- - Be specific about filters, time ranges, or groupings applied
2049
-
2050
- 4. **Config Modification** (based on component type):
2051
- - For KPICard: formatter, gradient, icon
2052
- - For Charts: colors, height, xKey, yKey, nameKey, valueKey
2053
- - For Tables: columns, pageSize, formatters
2054
-
2055
-
2056
- Respond with a JSON object:
2057
- {
2058
- "props": { /* modified props object with query, title, description, config */ },
2059
- "isModified": boolean,
2060
- "reasoning": "brief explanation of changes",
2061
- "modifications": ["list of specific changes made"]
2062
- }
2063
-
2064
- IMPORTANT:
2065
- - Return the COMPLETE props object, not just modified fields
2066
- - Preserve the structure expected by the component type
2067
- - Ensure query returns columns with expected aliases
2068
- - Keep config properties that aren't affected by the request`,
2069
- user: `{{USER_PROMPT}}`
2070
- },
2071
- "single-component": {
2072
- system: `You are an expert AI assistant specialized in matching user requests to the most appropriate component from a filtered list.
2073
-
2074
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2075
-
2076
-
2077
- ## Previous Conversation
2078
- {{CONVERSATION_HISTORY}}
2079
-
2080
- **Context Instructions:**
2081
- - If there is previous conversation history, use it to understand what the user is referring to
2082
- - When user says "show trends", "add filters", "change that", understand they may be building on previous queries
2083
- - Use previous component types and queries as context to inform your current matching
2084
-
2085
- ## Available Components (Type: {{COMPONENT_TYPE}})
2086
- The following components have been filtered by type {{COMPONENT_TYPE}}. Select the BEST matching one:
2087
-
2088
- {{COMPONENTS_LIST}}
2089
-
2090
- {{VISUALIZATION_CONSTRAINT}}
2091
-
2092
- **Select the BEST matching component** from the available {{COMPONENT_TYPE}} components listed above that would best answer the user's question.
2093
-
2094
- **Matching Guidelines:**
2095
- 1. **Semantic Matching:**
2096
- - Match based on component name, description, and keywords
2097
- - Consider what metrics/data the user is asking about
2098
- - Look for semantic similarity (e.g., "sales" matches "revenue", "orders" matches "purchases")
2099
-
2100
- 2. **Query Relevance:**
2101
- - Consider the component's existing query structure
2102
- - Does it query the right tables/columns for the user's question?
2103
- - Can it be modified to answer the user's specific question?
2104
-
2105
- 3. **Scoring Criteria:**
2106
- - Exact keyword matches in name/description: High priority
2107
- - Semantic similarity to user intent: High priority
2108
- - Appropriate aggregation/grouping: Medium priority
2109
- - Category alignment: Medium priority
2110
-
2111
- **Output Requirements:**
2112
-
2113
- Respond with a JSON object:
2114
- {
2115
- "componentId": "matched_component_id",
2116
- "componentIndex": 1, // 1-based index from the filtered list above
2117
- "reasoning": "Detailed explanation of why this component best matches the user's question",
2118
- "confidence": 85, // Confidence score 0-100
2119
- "canGenerate": true // false if no suitable component found (confidence < 50)
2120
- }
2121
-
2122
- **Important:**
2123
- - Only set canGenerate to true if confidence >= 50%
2124
- - If no component from the list matches well (all have low relevance), set canGenerate to false
2125
- - Consider the full context of the request and conversation history
2126
- - The component's props (query, title, description, config) will be modified later based on the user's specific request
2127
- - Focus on finding the component that is closest to what the user needs, even if it needs modification`,
2128
- user: `{{USER_PROMPT}}
2129
-
2130
- `
2131
- },
2132
- "mutli-component": {
2133
- system: `You are an expert data analyst AI that creates comprehensive multi-component analytical dashboards with aesthetically pleasing and balanced layouts.
2134
-
2135
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2136
-
2137
- Database Schema:
2138
- {{SCHEMA_DOC}}
2139
-
2140
- ## Previous Conversation
2141
- {{CONVERSATION_HISTORY}}
2142
-
2143
- **Context Instructions:**
2144
- - Review the conversation history to understand what the user has asked before
2145
- - If user is building on previous insights (e.g., "now show me X and Y"), use context to inform dashboard design
2146
- - Previous queries can help determine appropriate filters, date ranges, or categories to use
2147
- - If user asks for "comprehensive view" or "dashboard for X", include complementary components based on context
2148
-
2149
- Given a user's analytical question and the required visualization types, your task is to:
2150
-
2151
- 1. **Determine Container Metadata:**
2152
- - title: Clear, descriptive title for the entire dashboard (2-5 words)
2153
- - description: Brief explanation of what insights this dashboard provides (1-2 sentences)
2154
-
2155
- 2. **Generate Props for Each Component:**
2156
- For each visualization type requested, create tailored props:
2157
-
2158
- - **query**: SQL query specific to this visualization using the database schema
2159
- * Use correct table and column names
2160
- * **DO NOT USE TOP keyword - use LIMIT instead (e.g., LIMIT 20, not TOP 20)**
2161
- * ALWAYS include LIMIT clause ONCE at the end (default: {{DEFAULT_LIMIT}})
2162
- * For KPICard: Return single row with column alias "value"
2163
- * For Charts: Return appropriate columns (name/label and value, or x and y)
2164
- * For Table: Return relevant columns
2165
-
2166
- - **title**: Specific title for this component (2-4 words)
2167
-
2168
- - **description**: What this specific component shows (1 sentence)
2169
-
2170
- - **config**: Type-specific configuration
2171
- * KPICard: { gradient, formatter, icon }
2172
- * BarChart: { xKey, yKey, colors, height }
2173
- * LineChart: { xKey, yKeys, colors, height }
2174
- * PieChart: { nameKey, valueKey, colors, height }
2175
- * DataTable: { pageSize }
2176
-
2177
- 3. **CRITICAL: Component Hierarchy and Ordering:**
2178
- The ORDER of components in the array MUST follow this STRICT hierarchy for proper visual layout:
2179
-
2180
- **HIERARCHY RULES (MUST FOLLOW IN THIS ORDER):**
2181
- 1. KPICards - ALWAYS FIRST (top of dashboard for summary metrics)
2182
- 2. Charts/Graphs - AFTER KPICards (middle of dashboard for visualizations)
2183
- * BarChart, LineChart, PieChart, DonutChart
2184
- 3. DataTable - ALWAYS LAST (bottom of dashboard, full width for detailed data)
2185
-
2186
- **LAYOUT BEHAVIOR (Frontend enforces):**
2187
- - KPICards: Display in responsive grid (3 columns)
2188
- - Single Chart (if only 1 chart): Takes FULL WIDTH
2189
- - Multiple Charts (if 2+ charts): Display in 2-column grid
2190
- - DataTable (if present): Always spans FULL WIDTH at bottom
2191
-
2192
-
2193
- **ABSOLUTELY DO NOT deviate from this hierarchy. Always place:**
2194
- - KPICards first
2195
- - Charts/Graphs second
2196
- - DataTable last (if present)
2197
-
2198
- **Important Guidelines:**
2199
- - Each component should answer a DIFFERENT aspect of the user's question
2200
- - Queries should be complementary, not duplicated
2201
- - If user asks "Show total revenue and trend", generate:
2202
- * KPICard: Single total value (FIRST)
2203
- * LineChart: Revenue over time (SECOND)
2204
- - Ensure queries use valid columns from the schema
2205
- - Make titles descriptive and specific to what each component shows
2206
- - **Snowflake Syntax MUST be used:**
2207
- * Use LIMIT (not TOP)
2208
- * Use DATE_TRUNC, DATEDIFF (not DATEPART)
2209
- * Include LIMIT only ONCE per query at the end
2210
-
2211
- **Output Format:**
2212
- {
2213
- "containerTitle": "Dashboard Title",
2214
- "containerDescription": "Brief description of the dashboard insights",
2215
- "components": [
2216
- {
2217
- "componentType": "KPICard" | "BarChart" | "LineChart" | "PieChart" | "DataTable",
2218
- "query": "SQL query",
2219
- "title": "Component title",
2220
- "description": "Component description",
2221
- "config": { /* type-specific config */ }
2222
- },
2223
- ...
2224
- ],
2225
- "reasoning": "Explanation of the dashboard design and component ordering",
2226
- "canGenerate": boolean
2227
- }`,
2228
- user: `Current user question: {{USER_PROMPT}}
2229
-
2230
- Required visualization types: {{VISUALIZATION_TYPES}}
2231
-
2232
- 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.`
2233
- },
2234
- "container-metadata": {
2235
- system: `You are an expert AI assistant that generates titles and descriptions for multi-component dashboards.
2236
-
2237
- CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2238
-
2239
- ## Previous Conversation
2240
- {{CONVERSATION_HISTORY}}
2241
-
2242
- **Context Instructions:**
2243
- - If there is previous conversation history, use it to understand what the user is referring to
2244
- - Use context to create relevant titles and descriptions that align with the user's intent
2245
-
2246
- Your task is to generate a concise title and description for a multi-component dashboard that will contain the following visualization types:
2247
- {{VISUALIZATION_TYPES}}
2248
-
2249
- **Guidelines:**
2250
-
2251
- 1. **Title:**
2252
- - Should be clear and descriptive (3-8 words)
2253
- - Should reflect what the user is asking about
2254
- - Should NOT include "Dashboard" suffix (that will be added automatically)
2255
-
2256
- 2. **Description:**
2257
- - Should be a brief summary (1-2 sentences)
2258
- - Should explain what insights the dashboard provides
2259
-
2260
- **Output Requirements:**
2261
-
2262
- Respond with a JSON object:
2263
- {
2264
- "title": "Dashboard title without 'Dashboard' suffix",
2265
- "description": "Brief description of what this dashboard shows"
2266
- }
2267
-
2268
- **Important:**
2269
- - Keep the title concise and meaningful
2270
- - Make the description informative but brief
2271
- - Focus on what insights the user will gain
2272
- `,
2273
- user: `{{USER_PROMPT}}
2274
- `
2275
- },
2276
- "text-response": {
2277
- system: `You are an intelligent AI assistant that provides helpful, accurate, and contextual text responses to user questions.
2278
-
2279
- ## Your Task
2280
-
2281
- Analyze the user's question and provide a helpful text response. Your response should:
1915
+ 3. **NO NESTED AGGREGATE FUNCTIONS** - PostgreSQL does NOT allow aggregates inside aggregates
1916
+ \u274C WRONG: \`AVG(ROUND(AVG(column), 2))\` or \`SELECT AVG(SUM(price)) FROM ...\`
1917
+ \u2705 CORRECT: \`ROUND(AVG(column), 2)\`
2282
1918
 
2283
- 1. **Be Clear and Concise**: Provide direct answers without unnecessary verbosity
2284
- 2. **Be Contextual**: Use conversation history to understand what the user is asking about
2285
- 3. **Be Accurate**: Provide factually correct information based on the context
2286
- 4. **Be Helpful**: Offer additional relevant information or suggestions when appropriate
1919
+ 4. **GROUP BY Requirements**
1920
+ - ALL non-aggregated columns in SELECT must be in GROUP BY
1921
+ - If you SELECT a column and don't aggregate it, add it to GROUP BY
2287
1922
 
2288
- ## Handling Data Questions
1923
+ 5. **LIMIT Clause**
1924
+ - ALWAYS include LIMIT (max 32 rows)
1925
+ - For scalar subqueries in WHERE/HAVING, add LIMIT 1
2289
1926
 
2290
- When the user asks about data
1927
+ 6. **String Escaping** - PostgreSQL uses double single-quotes, NOT backslash
1928
+ \u274C WRONG: \`'Children\\'s furniture'\`
1929
+ \u2705 CORRECT: \`'Children''s furniture'\`
2291
1930
 
2292
- 1. **Generate a SQL query** using the database schema provided above
2293
- 2. **Use the execute_query tool** to run the query
2294
- 3. **If the query fails**, analyze the error and generate a corrected query
2295
- 4. **Format the results** in a clear, readable way for the user
1931
+ 7. **Always Use Table Aliases for Column References** - Prevent ambiguous column errors
1932
+ \u274C WRONG: \`SELECT product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
1933
+ \u2705 CORRECT: \`SELECT p.product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
1934
+ - Always prefix columns with table alias (e.g., \`p.product_id\`, \`c.name\`)
1935
+ - Especially critical in subqueries and joins where multiple tables share column names
2296
1936
 
2297
- **Query Guidelines:**
2298
- - Use correct table and column names from the schema
2299
- - ALWAYS include a LIMIT clause with a MAXIMUM of 32 rows
2300
- - Ensure valid SQL syntax
2301
- - For time-based queries, use appropriate date functions
2302
- - When using subqueries with scalar operators (=, <, >, etc.), add LIMIT 1 to prevent "more than one row" errors
2303
1937
 
2304
1938
  ## Response Guidelines
2305
1939
 
2306
- - If the question is about data, use the execute_query tool to fetch data and present it
1940
+ - If the question is about viewing data, use the execute_query tool to fetch data and present it
1941
+ - If the question is about creating/updating/deleting data:
1942
+ 1. Acknowledge that the system supports this via forms
1943
+ 2. **CRITICAL:** Use the database schema to determine which fields are required based on \`nullable\` property
1944
+ 3. **CRITICAL:** If the form will have select fields for foreign keys, you MUST fetch the options data using execute_query
1945
+ 4. **CRITICAL FOR UPDATE/DELETE OPERATIONS:** If it's an update/edit/modify/delete question:
1946
+ - **NEVER update ID/primary key columns** (e.g., order_id, customer_id, product_id) - these are immutable identifiers
1947
+ - You MUST first fetch the CURRENT values of the record using a SELECT query
1948
+ - Identify the record (from user's question - e.g., "update order 123" or "delete order 123" means order_id = 123)
1949
+ - Execute: \`SELECT * FROM table_name WHERE id = <value> LIMIT 1\`
1950
+ - Present the current values in your response (e.g., "Current order status: Pending, payment method: Credit Card")
1951
+ - For DELETE: These values will be shown in a disabled form as confirmation before deletion
1952
+ - For UPDATE: These values will populate as default values for editing
1953
+ 5. Present the options data in your response (e.g., "Available categories: Furniture (id: 1), Kitchen (id: 2), Decor (id: 3)")
1954
+ 6. The form component will be generated automatically using this data
2307
1955
  - If the question is general knowledge, provide a helpful conversational response
2308
1956
  - If asking for clarification, provide options or ask specific follow-up questions
2309
1957
  - If you don't have enough information, acknowledge it and ask for more details
2310
1958
  - Keep responses focused and avoid going off-topic
2311
1959
 
1960
+ **Example for data modification with foreign keys:**
1961
+ User: "I want to create a new product"
1962
+ You should:
1963
+ 1. Execute query: \`SELECT category_id, name FROM categories LIMIT 32\`
1964
+ 2. Execute query: \`SELECT store_id, name FROM stores LIMIT 32\`
1965
+ 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)..."
1966
+ 4. Suggest Form component
1967
+
2312
1968
  ## Component Suggestions
2313
1969
 
2314
- After analyzing the query results, you MUST suggest appropriate dashboard components for displaying the data. Use this format:
1970
+ After analyzing the user's question, you MUST suggest appropriate dashboard components. Use this format:
2315
1971
 
2316
1972
  <DashboardComponents>
2317
1973
  **Dashboard Components:**
@@ -2319,12 +1975,22 @@ Format: \`{number}.{component_type} : {clear reasoning}\`
2319
1975
 
2320
1976
 
2321
1977
  **Rules for component suggestions:**
2322
- 1. Analyze the query results structure and data type
2323
- 2. Suggest components that would best visualize the data
2324
- 3. Each component suggestion must be on a new line
1978
+ 1. If a conclusive answer can be provided based on user question, suggest that as the first component.
1979
+ 2. ALways suggest context/supporting components that will give the user more information and allow them to explore further.
1980
+ 3. If the question includes a time range, also explore time-based components for past time ranges.
1981
+ 4. **For data viewing/analysis questions**: Suggest visualization components (KPICard, BarChart, LineChart, PieChart, DataTable, etc.).
1982
+ 5. **For data modification questions** (create/add/update/delete):
1983
+ - Always suggest 1-2 context components first to provide relevant information (prefer KPICard for showing key metrics)
1984
+ - Then suggest \`Form\` component for the actual modification
1985
+ - Example: "1.KPICard : Show current order total and status" then "2.Form : To update order details"
1986
+ 6. Analyze the query results structure and data type
1987
+ 7. Each component suggestion must be on a new line
2325
1988
  </DashboardComponents>
2326
1989
 
2327
- IMPORTANT: Always wrap component suggestions with <DashboardComponents> tags and include at least one component suggestion when data is returned.
1990
+ IMPORTANT:
1991
+ - Always wrap component suggestions with <DashboardComponents> tags
1992
+ - For data viewing: Include at least one component suggestion when data is returned
1993
+ - 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")
2328
1994
 
2329
1995
  ## Output Format
2330
1996
 
@@ -2339,37 +2005,22 @@ Respond with plain text that includes:
2339
2005
  - Return ONLY plain text (no JSON, no markdown code blocks)
2340
2006
 
2341
2007
 
2342
- You have access to a database and can execute SQL queries to answer data-related questions.
2343
- ## Database Schema
2344
- {{SCHEMA_DOC}}
2345
-
2346
- **Database Type: PostgreSQL**
2347
-
2348
- **CRITICAL PostgreSQL Query Rules:**
2008
+ 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.
2349
2009
 
2350
- 1. **NO AGGREGATE FUNCTIONS IN WHERE CLAUSE** - This is a fundamental SQL error
2351
- \u274C WRONG: \`WHERE COUNT(orders) > 0\`
2352
- \u274C WRONG: \`WHERE SUM(price) > 100\`
2353
- \u274C WRONG: \`WHERE AVG(rating) > 4.5\`
2354
2010
 
2355
- \u2705 CORRECT: Use HAVING (with GROUP BY), EXISTS, or subquery
2011
+ ## External Tool Results
2356
2012
 
2357
- 2. **WHERE vs HAVING**
2358
- - WHERE filters rows BEFORE grouping (cannot use aggregates)
2359
- - HAVING filters groups AFTER grouping (can use aggregates)
2360
- - If using HAVING, you MUST have GROUP BY
2013
+ The following external tools were executed for this request (if applicable):
2361
2014
 
2362
- 3. **NO NESTED AGGREGATE FUNCTIONS** - PostgreSQL does NOT allow aggregates inside aggregates
2363
- \u274C WRONG: \`AVG(ROUND(AVG(column), 2))\` or \`SELECT AVG(SUM(price)) FROM ...\`
2364
- \u2705 CORRECT: \`ROUND(AVG(column), 2)\`
2015
+ {{EXTERNAL_TOOL_CONTEXT}}
2365
2016
 
2366
- 4. **GROUP BY Requirements**
2367
- - ALL non-aggregated columns in SELECT must be in GROUP BY
2368
- - If you SELECT a column and don't aggregate it, add it to GROUP BY
2017
+ Use this external tool data to:
2018
+ - Provide information from external sources (emails, calendar, etc.)
2019
+ - Present the data in a user-friendly format
2020
+ - Combine external data with database queries when relevant
2021
+ - Reference specific results in your response
2369
2022
 
2370
- 5. **LIMIT Clause**
2371
- - ALWAYS include LIMIT (max 32 rows)
2372
- - For scalar subqueries in WHERE/HAVING, add LIMIT 1
2023
+ **Note:** If external tools were not needed, this section will indicate "No external tools were used for this request."
2373
2024
 
2374
2025
 
2375
2026
  ## Knowledge Base Context
@@ -2392,10 +2043,8 @@ Use this knowledge base information to:
2392
2043
  ## Previous Conversation
2393
2044
  {{CONVERSATION_HISTORY}}
2394
2045
 
2395
-
2396
2046
  `,
2397
2047
  user: `{{USER_PROMPT}}
2398
-
2399
2048
  `
2400
2049
  },
2401
2050
  "match-text-components": {
@@ -2409,11 +2058,21 @@ You will receive a text response containing:
2409
2058
  3. **Dashboard Components:** suggestions (1:component_type : reasoning format)
2410
2059
 
2411
2060
  Your job is to:
2412
- 1. **Parse the component suggestions** from the text response (format: 1:component_type : reasoning)
2413
- 2. **Match each suggestion with an actual component** from the available list
2414
- 3. **Generate proper props** for each matched component to **visualize the analysis results** that were already fetched
2415
- 4. **Generate title and description** for the dashboard container
2416
- 5. **Generate intelligent follow-up questions (actions)** that the user might naturally ask next based on the data analysis
2061
+ 1. **FIRST: Generate a direct answer component** (if the user question can be answered with a single visualization)
2062
+ - Determine the BEST visualization type (KPICard, BarChart, DataTable, PieChart, LineChart, etc.) to directly answer the user's question
2063
+ - Select the matching component from the available components list
2064
+ - Generate complete props for this component (query, title, description, config)
2065
+ - This component will be placed in the \`answerComponent\` field
2066
+ - This component will be streamed to the frontend IMMEDIATELY for instant user feedback
2067
+ - **CRITICAL**: Generate this FIRST in your JSON response
2068
+
2069
+ 2. **THEN: Parse ALL dashboard component suggestions** from the text response (format: 1:component_type : reasoning)
2070
+ 3. **Match EACH suggestion with an actual component** from the available list
2071
+ 4. **CRITICAL**: \`matchedComponents\` must include **ALL** dashboard components suggested in the text, INCLUDING the component you used as \`answerComponent\`
2072
+ - The answerComponent is shown first for quick feedback, but the full dashboard shows everything
2073
+ 5. **Generate proper props** for each matched component to **visualize the analysis results** that were already fetched
2074
+ 6. **Generate title and description** for the dashboard container
2075
+ 7. **Generate intelligent follow-up questions (actions)** that the user might naturally ask next based on the data analysis
2417
2076
 
2418
2077
  **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.
2419
2078
 
@@ -2448,7 +2107,8 @@ For each matched component, generate complete props:
2448
2107
 
2449
2108
  **Option B: GENERATE a new query** (when necessary)
2450
2109
  - Only generate new queries when you need DIFFERENT data
2451
- - Use the database schema below to write valid SQL
2110
+ - For SELECT queries: Use the database schema below to write valid SQL
2111
+ - For mutations (INSERT/UPDATE/DELETE): Only if matching a Form component, generate mutation query with $fieldName placeholders
2452
2112
 
2453
2113
 
2454
2114
  **Decision Logic:**
@@ -2466,8 +2126,12 @@ For each matched component, generate complete props:
2466
2126
  \u274C WRONG: \`WHERE COUNT(orders) > 0\`
2467
2127
  \u274C WRONG: \`WHERE SUM(price) > 100\`
2468
2128
  \u274C WRONG: \`WHERE AVG(rating) > 4.5\`
2129
+ \u274C WRONG: \`WHERE FLOOR(AVG(rating)) = 4\` (aggregate inside any function is still not allowed)
2130
+ \u274C WRONG: \`WHERE ROUND(SUM(price), 2) > 100\`
2469
2131
 
2470
2132
  \u2705 CORRECT: Use HAVING (with GROUP BY), EXISTS, or subquery
2133
+ \u2705 CORRECT: Move aggregate logic to HAVING: \`GROUP BY ... HAVING FLOOR(AVG(rating)) = 4\`
2134
+ \u2705 CORRECT: Use subquery for filtering: \`WHERE product_id IN (SELECT product_id FROM ... GROUP BY ... HAVING AVG(rating) >= 4)\`
2471
2135
 
2472
2136
  2. **NO NESTED AGGREGATE FUNCTIONS** - PostgreSQL does NOT allow aggregates inside aggregates
2473
2137
  \u274C WRONG: \`AVG(ROUND(AVG(column), 2))\`
@@ -2496,6 +2160,16 @@ For each matched component, generate complete props:
2496
2160
  - Subqueries used with =, <, >, etc. must return single value
2497
2161
  - Always add LIMIT 1 to scalar subqueries
2498
2162
 
2163
+ 8. **String Escaping** - PostgreSQL uses double single-quotes, NOT backslash
2164
+ \u274C WRONG: \`'Children\\'s furniture'\`
2165
+ \u2705 CORRECT: \`'Children''s furniture'\`
2166
+
2167
+ 9. **Always Use Table Aliases for Column References** - Prevent ambiguous column errors
2168
+ \u274C WRONG: \`SELECT product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
2169
+ \u2705 CORRECT: \`SELECT p.product_id FROM products p JOIN product_variants pv ON p.product_id = pv.product_id\`
2170
+ - Always prefix columns with table alias (e.g., \`p.product_id\`, \`c.name\`)
2171
+ - Especially critical in subqueries and joins where multiple tables share column names
2172
+
2499
2173
  **Query Generation Guidelines** (when creating new queries):
2500
2174
  - Use correct table and column names from the schema above
2501
2175
  - ALWAYS include LIMIT clause (max 32 rows)
@@ -2509,7 +2183,7 @@ For each matched component, generate complete props:
2509
2183
  - Brief explanation of what this component displays
2510
2184
  - Why it's useful for this data
2511
2185
 
2512
- ### 4. Config
2186
+ ### 4. Config (for visualization components)
2513
2187
  - **CRITICAL**: Look at the component's "Props Structure" to see what config fields it expects
2514
2188
  - Map query result columns to the appropriate config fields
2515
2189
  - Keep other existing config properties that don't need to change
@@ -2521,40 +2195,167 @@ For each matched component, generate complete props:
2521
2195
  - \`orientation\` = "vertical" or "horizontal" (controls visual direction only)
2522
2196
  - **DO NOT swap xAxisKey/yAxisKey based on orientation** - they always represent category and value respectively
2523
2197
 
2524
- ## Follow-Up Questions (Actions) Generation
2198
+ ### 5. Additional Props (match according to component type)
2199
+ - **CRITICAL**: Look at the matched component's "Props Structure" in the available components list
2200
+ - Generate props that match EXACTLY what the component expects
2525
2201
 
2526
- 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:
2202
+ **For Form components (type: "Form"):**
2527
2203
 
2528
- 1. **Build upon the data analysis** shown in the text response and components
2529
- 2. **Explore natural next steps** in the data exploration journey
2530
- 3. **Be progressively more detailed or specific** - go deeper into the analysis
2531
- 4. **Consider the insights revealed** - suggest questions that help users understand implications
2532
- 5. **Be phrased naturally** as if a real user would ask them
2533
- 6. **Vary in scope** - include both broad trends and specific details
2534
- 7. **Avoid redundancy** - don't ask questions already answered in the text response
2204
+ Props structure:
2205
+ - **query**: \`{ sql: "INSERT/UPDATE/DELETE query with $fieldName placeholders", params: [] }\`
2206
+ - **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\`)
2207
+ - **title**: "Update Order 5000", "Create New Product", or "Delete Order 5000"
2208
+ - **description**: What the form does
2209
+ - **submitButtonText**: Button text (default: "Submit"). For delete: "Delete", "Confirm Delete"
2210
+ - **submitButtonColor**: "primary" (blue) or "danger" (red). Use "danger" for DELETE operations
2211
+ - **successMessage**: Success message (default: "Form submitted successfully!"). For delete: "Record deleted successfully!"
2212
+ - **disableFields**: Set \`true\` for DELETE operations to show current values but prevent editing
2213
+ - **fields**: Array of field objects (structure below)
2535
2214
 
2215
+ **Field object:**
2216
+ \`\`\`json
2217
+ {
2218
+ "name": "field_name", // Matches $field_name in SQL query
2219
+ "description": "Field Label",
2220
+ "type": "text|number|email|date|select|multiselect|checkbox|textarea",
2221
+ "required": true, // Set based on schema: nullable=false \u2192 required=true, nullable=true \u2192 required=false
2222
+ "defaultValue": "current_value", // For UPDATE: extract from text response
2223
+ "placeholder": "hint text",
2224
+ "options": [...], // For select/multiselect
2225
+ "validation": {
2226
+ "minLength": { "value": 5, "message": "..." },
2227
+ "maxLength": { "value": 100, "message": "..." },
2228
+ "min": { "value": 18, "message": "..." },
2229
+ "max": { "value": 120, "message": "..." },
2230
+ "pattern": { "value": "regex", "message": "..." }
2231
+ }
2232
+ }
2233
+ \`\`\`
2536
2234
 
2537
- ## Output Format
2235
+ **CRITICAL - Set required based on database schema:**
2236
+ - Check the column's \`nullable\` property in the database schema
2237
+ - If \`nullable: false\` \u2192 set \`required: true\` (field is mandatory)
2238
+ - If \`nullable: true\` \u2192 set \`required: false\` (field is optional)
2239
+ - Never set fields as required if the schema allows NULL
2538
2240
 
2539
- You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2241
+ **Default Values for UPDATE:**
2242
+ - **NEVER include ID/primary key fields in UPDATE forms** (e.g., order_id, customer_id, product_id) - these cannot be changed
2243
+ - Detect UPDATE by checking if SQL contains "UPDATE" keyword
2244
+ - Extract current values from text response (look for "Current values:" or SELECT results)
2245
+ - Set \`defaultValue\` for each field with the extracted current value
2540
2246
 
2541
- **IMPORTANT JSON FORMATTING RULES:**
2542
- - Put SQL queries on a SINGLE LINE (no newlines in the query string)
2543
- - Escape all quotes in SQL properly (use \\" for quotes inside strings)
2544
- - Remove any newlines, tabs, or special characters from SQL
2545
- - Do NOT use markdown code blocks (no \`\`\`)
2546
- - Return ONLY the JSON object, nothing else
2247
+ **CRITICAL - Single field with current value pre-selected:**
2248
+ For UPDATE operations, use ONE field with defaultValue set to current value (not two separate fields).
2547
2249
 
2250
+ \u2705 CORRECT - Single field, current value pre-selected:
2548
2251
  \`\`\`json
2549
2252
  {
2550
- "layoutTitle": "Clear, concise title for the overall dashboard/layout (5-10 words)",
2551
- "layoutDescription": "Brief description of what the dashboard shows and its purpose (1-2 sentences)",
2552
- "matchedComponents": [
2553
- {
2554
- "componentId": "id_from_available_list",
2555
- "componentName": "name_of_component",
2556
- "componentType": "type_of_component",
2557
- "reasoning": "Why this component was selected for this suggestion",
2253
+ "name": "category_id",
2254
+ "type": "select",
2255
+ "defaultValue": 5,
2256
+ "options": [{"id": 1, "name": "Kitchen"}, {"id": 5, "name": "Furniture"}, {"id": 7, "name": "Decor"}]
2257
+ }
2258
+ \`\`\`
2259
+ User sees dropdown with "Furniture" selected, can change to any other category.
2260
+
2261
+ \u274C WRONG - Two separate fields:
2262
+ \`\`\`json
2263
+ [
2264
+ {"name": "current_category", "type": "text", "defaultValue": "Furniture", "disabled": true},
2265
+ {"name": "new_category", "type": "select", "options": [...]}
2266
+ ]
2267
+ \`\`\`
2268
+
2269
+ **Options Format:**
2270
+ - **Enum/status fields** (non-foreign keys): String array \`["Pending", "Shipped", "Delivered"]\`
2271
+ - **Foreign keys** (reference tables): Object array \`[{"id": 1, "name": "Furniture"}, {"id": 2, "name": "Kitchen"}]\`
2272
+ - Extract from text response queries and match format to field type
2273
+
2274
+ **Example UPDATE form field:**
2275
+ \`\`\`json
2276
+ {
2277
+ "name": "status",
2278
+ "description": "Order Status",
2279
+ "type": "select",
2280
+ "required": true,
2281
+ "defaultValue": "Pending", // Current value from database
2282
+ "options": ["Pending", "Processing", "Shipped", "Delivered"]
2283
+ }
2284
+ \`\`\`
2285
+
2286
+ **Example DELETE form props:**
2287
+ \`\`\`json
2288
+ {
2289
+ "query": { "sql": "DELETE FROM orders WHERE order_id = 123", "params": [] },
2290
+ "title": "Delete Order 123",
2291
+ "description": "Are you sure you want to delete this order?",
2292
+ "submitButtonText": "Confirm Delete",
2293
+ "submitButtonColor": "danger",
2294
+ "successMessage": "Order deleted successfully!",
2295
+ "disableFields": true,
2296
+ "fields": [
2297
+ { "name": "order_id", "description": "Order ID", "type": "text", "defaultValue": "123" },
2298
+ { "name": "status", "description": "Status", "type": "text", "defaultValue": "Pending" }
2299
+ ]
2300
+ }
2301
+ \`\`\`
2302
+
2303
+ **For visualization components (Charts, Tables, KPIs):**
2304
+ - **query**: String (SQL SELECT query)
2305
+ - **title**, **description**, **config**: As per component's props structure
2306
+ - Do NOT include fields array
2307
+
2308
+ ## Follow-Up Questions (Actions) Generation
2309
+
2310
+ 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:
2311
+
2312
+ 1. **Build upon the data analysis** shown in the text response and components
2313
+ 2. **Explore natural next steps** in the data exploration journey
2314
+ 3. **Be progressively more detailed or specific** - go deeper into the analysis
2315
+ 4. **Consider the insights revealed** - suggest questions that help users understand implications
2316
+ 5. **Be phrased naturally** as if a real user would ask them
2317
+ 6. **Vary in scope** - include both broad trends and specific details
2318
+ 7. **Avoid redundancy** - don't ask questions already answered in the text response
2319
+
2320
+
2321
+ ## Output Format
2322
+
2323
+ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2324
+
2325
+ **IMPORTANT JSON FORMATTING RULES:**
2326
+ - Put SQL queries on a SINGLE LINE (no newlines in the query string)
2327
+ - Escape all quotes in SQL properly (use \\" for quotes inside strings)
2328
+ - Remove any newlines, tabs, or special characters from SQL
2329
+ - Do NOT use markdown code blocks (no \`\`\`)
2330
+ - Return ONLY the JSON object, nothing else
2331
+
2332
+ **Example 1: With answer component** (when user question can be answered with single visualization)
2333
+ \`\`\`json
2334
+ {
2335
+ "hasAnswerComponent": true,
2336
+ "answerComponent": {
2337
+ "componentId": "id_from_available_list",
2338
+ "componentName": "name_of_component",
2339
+ "componentType": "type_of_component (can be KPICard, BarChart, LineChart, PieChart, DataTable, etc.)",
2340
+ "reasoning": "Why this visualization type best answers the user's question",
2341
+ "props": {
2342
+ "query": "SQL query for this component",
2343
+ "title": "Component title that directly answers the user's question",
2344
+ "description": "Component description",
2345
+ "config": {
2346
+ "field1": "value1",
2347
+ "field2": "value2"
2348
+ }
2349
+ }
2350
+ },
2351
+ "layoutTitle": "Clear, concise title for the overall dashboard/layout (5-10 words)",
2352
+ "layoutDescription": "Brief description of what the dashboard shows and its purpose (1-2 sentences)",
2353
+ "matchedComponents": [
2354
+ {
2355
+ "componentId": "id_from_available_list",
2356
+ "componentName": "name_of_component",
2357
+ "componentType": "type_of_component",
2358
+ "reasoning": "Why this component was selected for the dashboard",
2558
2359
  "originalSuggestion": "c1:table : original reasoning from text",
2559
2360
  "props": {
2560
2361
  "query": "SQL query for this component",
@@ -2577,21 +2378,65 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
2577
2378
  }
2578
2379
  \`\`\`
2579
2380
 
2381
+ **Example 2: Without answer component** (when user question needs multiple visualizations or dashboard)
2382
+ \`\`\`json
2383
+ {
2384
+ "hasAnswerComponent": false,
2385
+ "answerComponent": null,
2386
+ "layoutTitle": "Clear, concise title for the overall dashboard/layout (5-10 words)",
2387
+ "layoutDescription": "Brief description of what the dashboard shows and its purpose (1-2 sentences)",
2388
+ "matchedComponents": [
2389
+ {
2390
+ "componentId": "id_from_available_list",
2391
+ "componentName": "name_of_component",
2392
+ "componentType": "type_of_component",
2393
+ "reasoning": "Why this component was selected for the dashboard",
2394
+ "originalSuggestion": "c1:chart : original reasoning from text",
2395
+ "props": {
2396
+ "query": "SQL query for this component",
2397
+ "title": "Component title",
2398
+ "description": "Component description",
2399
+ "config": {
2400
+ "field1": "value1",
2401
+ "field2": "value2"
2402
+ }
2403
+ }
2404
+ }
2405
+ ],
2406
+ "actions": [
2407
+ "Follow-up question 1?",
2408
+ "Follow-up question 2?",
2409
+ "Follow-up question 3?",
2410
+ "Follow-up question 4?",
2411
+ "Follow-up question 5?"
2412
+ ]
2413
+ }
2414
+ \`\`\`
2415
+
2580
2416
  **CRITICAL:**
2581
- - \`matchedComponents\` MUST include ALL components suggested in the text response
2417
+ - **\`hasAnswerComponent\` determines if an answer component exists**
2418
+ - Set to \`true\` if the user question can be answered with a single visualization
2419
+ - Set to \`false\` if the user question can not be answered with single visualisation and needs multiple visualizations or a dashboard overview
2420
+ - **If \`hasAnswerComponent\` is \`true\`:**
2421
+ - \`answerComponent\` MUST be generated FIRST in the JSON before \`layoutTitle\`
2422
+ - Generate complete props (query, title, description, config)
2423
+ - **If \`hasAnswerComponent\` is \`false\`:**
2424
+ - Set \`answerComponent\` to \`null\`
2425
+ - **\`matchedComponents\` MUST include ALL dashboard components from the text analysis**
2426
+ - **CRITICAL**: Even if you used a component as \`answerComponent\`, you MUST STILL include it in \`matchedComponents\`
2427
+ - 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)
2428
+ - Do NOT skip the answerComponent from matchedComponents
2429
+ - \`matchedComponents\` come from the dashboard component suggestions in the text response
2582
2430
  - \`layoutTitle\` MUST be a clear, concise title (5-10 words) that summarizes what the entire dashboard shows
2583
- - Examples: "Sales Performance Overview", "Customer Metrics Analysis", "Product Category Breakdown"
2584
2431
  - \`layoutDescription\` MUST be a brief description (1-2 sentences) explaining the purpose and scope of the dashboard
2585
2432
  - Should describe what insights the dashboard provides and what data it shows
2586
2433
  - \`actions\` MUST be an array of 4-5 intelligent follow-up questions based on the analysis
2587
2434
  - Return ONLY valid JSON (no markdown code blocks, no text before/after)
2588
- - Generate complete props for each component including query, title, description, and config
2589
-
2590
-
2435
+ - Generate complete props for each component
2591
2436
  `,
2592
- user: `## Text Response
2437
+ user: `## Analysis Content
2593
2438
 
2594
- {{TEXT_RESPONSE}}
2439
+ {{ANALYSIS_CONTENT}}
2595
2440
 
2596
2441
  ---
2597
2442
 
@@ -2638,70 +2483,269 @@ Format your response as a JSON object with this structure:
2638
2483
 
2639
2484
  Return ONLY valid JSON.`
2640
2485
  },
2641
- "execute-tools": {
2642
- system: `You are an expert AI assistant that executes external tools to fetch data from external services.
2486
+ "category-classification": {
2487
+ system: `You are an expert AI that categorizes user questions into specific action categories and identifies required tools/resources.
2643
2488
 
2644
- 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.
2489
+ CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2645
2490
 
2646
2491
  ## Available External Tools
2492
+
2647
2493
  {{AVAILABLE_TOOLS}}
2648
2494
 
2649
- ## Your Task
2495
+ ---
2650
2496
 
2651
- Analyze the user's request and:
2652
-
2653
- 1. **Determine if external tools are needed**
2654
- - Examples that NEED external tools:
2655
- - "Show me my emails" \u2192 needs email tool
2656
- - "Get my last 5 Gmail messages" \u2192 needs Gmail tool
2657
- - "Check my Outlook inbox" \u2192 needs Outlook tool
2658
-
2659
- - Examples that DON'T need external tools:
2660
- - "What is the total sales?" \u2192 database query (handled elsewhere)
2661
- - "Show me revenue trends" \u2192 internal data analysis
2662
- - "Hello" \u2192 general conversation
2663
-
2664
- 2. **Call the appropriate tools**
2665
- - Use the tool calling mechanism to execute external tools
2666
- - Extract parameters from the user's request
2667
- - Use sensible defaults when parameters aren't specified:
2668
- - For email limit: default to 10
2669
- - For email address: use "me" for the authenticated user if not specified
2670
-
2671
- 3. **Handle errors and retry**
2672
- - If a tool call fails, analyze the error message
2673
- - Retry with corrected parameters if possible
2674
- - You have up to 3 attempts per tool
2675
-
2676
- ## Important Guidelines
2677
-
2678
- - **Only call external tools when necessary** - Don't call tools for database queries or general conversation
2679
- - **Choose the right tool** - For email requests, select Gmail vs Outlook based on:
2680
- - Explicit mention (e.g., "Gmail", "Outlook")
2681
- - Email domain (e.g., @gmail.com \u2192 Gmail, @company.com \u2192 Outlook)
2682
- - **Extract parameters carefully** - Use the user's exact values when provided
2683
- - **If no tools are needed** - Simply respond that no external tools are required for this request
2684
-
2685
- ## Examples
2686
-
2687
- **Example 1 - Gmail Request:**
2688
- User: "Show me my last 5 Gmail messages"
2689
- Action: Call get-gmail-mails tool with parameters: { email: "me", limit: 5 }
2690
-
2691
- **Example 2 - Outlook Request:**
2692
- User: "Get emails from john.doe@company.com"
2693
- Action: Call get-outlook-mails tool with parameters: { email: "john.doe@company.com", limit: 10 }
2694
-
2695
- **Example 3 - No Tools Needed:**
2696
- User: "What is the total sales?"
2697
- Response: This is a database query, not an external tool request. No external tools are needed.
2698
-
2699
- **Example 4 - Error Retry:**
2700
- Tool call fails with: "Invalid email parameter"
2701
- Action: Analyze error, correct the parameter, and retry the tool call
2702
- `,
2703
- user: `{{USER_PROMPT}}
2704
- `
2497
+ Your task is to analyze the user's question and determine:
2498
+
2499
+ 1. **Question Category:**
2500
+ - "data_analysis": Questions about analyzing, querying, reading, or visualizing data from the database (SELECT operations)
2501
+ - "data_modification": Questions about creating, updating, deleting, or modifying data in the database (INSERT, UPDATE, DELETE operations)
2502
+
2503
+ 2. **External Tools Required** (for both categories):
2504
+ From the available tools listed above, identify which ones are needed to support the user's request:
2505
+ - Match the tool names/descriptions to what the user is asking for
2506
+ - Extract specific parameters mentioned in the user's question
2507
+
2508
+ 3. **Tool Parameters** (if tools are identified):
2509
+ Extract specific parameters the user mentioned:
2510
+ - For each identified tool, extract relevant parameters (email, recipient, content, etc.)
2511
+ - Only include parameters the user explicitly or implicitly mentioned
2512
+
2513
+ **Important Guidelines:**
2514
+ - If user mentions any of the available external tools \u2192 identify those tools and extract their parameters
2515
+ - If user asks to "send", "schedule", "create event", "message" \u2192 check if available tools match
2516
+ - If user asks to "show", "analyze", "compare", "calculate" data \u2192 "data_analysis"
2517
+ - If user asks to modify/create/update/delete data \u2192 "data_modification"
2518
+ - Always identify tools from the available tools list (not from generic descriptions)
2519
+ - Be precise in identifying tool types and required parameters
2520
+ - Only include tools that are explicitly mentioned or clearly needed
2521
+
2522
+ **Output Format:**
2523
+ \`\`\`json
2524
+ {
2525
+ "category": "data_analysis" | "data_modification",
2526
+ "reasoning": "Brief explanation of why this category was chosen",
2527
+ "externalTools": [
2528
+ {
2529
+ "type": "tool_id_from_available_tools",
2530
+ "name": "Tool Display Name",
2531
+ "description": "What this tool will do",
2532
+ "parameters": {
2533
+ "param1": "extracted value",
2534
+ "param2": "extracted value"
2535
+ }
2536
+ }
2537
+ ],
2538
+ "dataAnalysisType": "visualization" | "calculation" | "comparison" | "trend" | null,
2539
+ "confidence": 0-100
2540
+ }
2541
+ \`\`\`
2542
+
2543
+
2544
+ ## Previous Conversation
2545
+ {{CONVERSATION_HISTORY}}`,
2546
+ user: `{{USER_PROMPT}}`
2547
+ },
2548
+ "adapt-ui-block-params": {
2549
+ system: `You are an expert AI that adapts and modifies UI block component parameters based on the user's current question.
2550
+
2551
+ CRITICAL: You MUST respond with ONLY valid JSON, no other text before or after.
2552
+
2553
+ ## Database Schema Reference
2554
+
2555
+ {{SCHEMA_DOC}}
2556
+
2557
+ 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.
2558
+
2559
+ ## Context
2560
+ You are given:
2561
+ 1. A previous UI Block response (with component and its props) that matched the user's current question with >90% semantic similarity
2562
+ 2. The user's current question
2563
+ 3. The component that needs parameter adaptation
2564
+
2565
+ Your task is to:
2566
+ 1. **Analyze the difference** between the original question (from the matched UIBlock) and the current user question
2567
+ 2. **Identify what parameters need to change** in the component props to answer the current question
2568
+ 3. **Modify the props** to match the current request while keeping the same component type(s)
2569
+ 4. **Preserve component structure** - only change props, not the components themselves
2570
+
2571
+ ## Component Structure Handling
2572
+
2573
+ ### For Single Components:
2574
+ - Modify props directly (config, actions, query, filters, etc.)
2575
+
2576
+ ### For MultiComponentContainer:
2577
+ The component will have structure:
2578
+ \`\`\`json
2579
+ {
2580
+ "type": "Container",
2581
+ "name": "MultiComponentContainer",
2582
+ "props": {
2583
+ "config": {
2584
+ "components": [...], // Array of nested components - ADAPT EACH ONE
2585
+ "title": "...", // Container title - UPDATE based on new question
2586
+ "description": "..." // Container description - UPDATE based on new question
2587
+ },
2588
+ "actions": [...] // ADAPT actions if needed
2589
+ }
2590
+ }
2591
+ \`\`\`
2592
+
2593
+ When adapting MultiComponentContainer:
2594
+ - Update the container-level \`title\` and \`description\` to reflect the new user question
2595
+ - For each component in \`config.components\`:
2596
+ - Identify what data it shows and how the new question changes what's needed
2597
+ - Adapt its query parameters (WHERE clauses, LIMIT, ORDER BY, filters, date ranges)
2598
+ - Update its title/description to match the new context
2599
+ - Update its config settings (colors, sorting, grouping, metrics)
2600
+ - Update \`actions\` if the new question requires different actions
2601
+
2602
+ ## Important Guidelines:
2603
+ - Keep the same component type (don't change KPICard to LineChart)
2604
+ - Keep the same number of components in the container
2605
+ - For each nested component, update:
2606
+ - Query WHERE clauses, LIMIT, ORDER BY, filters, date ranges, metrics
2607
+ - Title and description to reflect the new question
2608
+ - Config settings like colors, sorting, grouping if needed
2609
+ - Maintain each component's core purpose while answering the new question
2610
+ - If query modification is needed, ensure all table/column names remain valid
2611
+ - CRITICAL: Ensure JSON is valid and complete for all nested structures
2612
+
2613
+
2614
+ ## Output Format:
2615
+
2616
+ ### For Single Component:
2617
+ \`\`\`json
2618
+ {
2619
+ "success": true,
2620
+ "adaptedComponent": {
2621
+ "id": "original_component_id",
2622
+ "name": "component_name",
2623
+ "type": "component_type",
2624
+ "description": "updated_description",
2625
+ "props": {
2626
+ "config": { },
2627
+ "actions": [],
2628
+ }
2629
+ },
2630
+ "parametersChanged": [
2631
+ {
2632
+ "field": "query",
2633
+ "reason": "Added Q4 date filter"
2634
+ },
2635
+ {
2636
+ "field": "title",
2637
+ "reason": "Updated to reflect Q4 focus"
2638
+ }
2639
+ ],
2640
+ "explanation": "How the component was adapted to answer the new question"
2641
+ }
2642
+ \`\`\`
2643
+
2644
+ ### For MultiComponentContainer:
2645
+ \`\`\`json
2646
+ {
2647
+ "success": true,
2648
+ "adaptedComponent": {
2649
+ "id": "original_container_id",
2650
+ "name": "MultiComponentContainer",
2651
+ "type": "Container",
2652
+ "description": "updated_container_description",
2653
+ "props": {
2654
+ "config": {
2655
+ "title": "Updated dashboard title based on new question",
2656
+ "description": "Updated description reflecting new question context",
2657
+ "components": [
2658
+ {
2659
+ "id": "component_1_id",
2660
+ "name": "component_1_name",
2661
+ "type": "component_1_type",
2662
+ "description": "updated description for this specific component",
2663
+ "props": {
2664
+ "query": "Modified SQL query with updated WHERE/LIMIT/ORDER BY",
2665
+ "config": { "metric": "updated_metric", "filters": {...} }
2666
+ }
2667
+ },
2668
+ {
2669
+ "id": "component_2_id",
2670
+ "name": "component_2_name",
2671
+ "type": "component_2_type",
2672
+ "description": "updated description for this component",
2673
+ "props": {
2674
+ "query": "Modified SQL query for this component",
2675
+ "config": { "metric": "updated_metric", "filters": {...} }
2676
+ }
2677
+ }
2678
+ ]
2679
+ },
2680
+ "actions": []
2681
+ }
2682
+ },
2683
+ "parametersChanged": [
2684
+ {
2685
+ "field": "container.title",
2686
+ "reason": "Updated to reflect new dashboard focus"
2687
+ },
2688
+ {
2689
+ "field": "components[0].query",
2690
+ "reason": "Modified WHERE clause for new metrics"
2691
+ },
2692
+ {
2693
+ "field": "components[1].config.metric",
2694
+ "reason": "Changed metric from X to Y based on new question"
2695
+ }
2696
+ ],
2697
+ "explanation": "Detailed explanation of how each component was adapted"
2698
+ }
2699
+ \`\`\`
2700
+
2701
+ If adaptation is not possible or would fundamentally change the component:
2702
+ \`\`\`json
2703
+ {
2704
+ "success": false,
2705
+ "reason": "Cannot adapt component - the new question requires a different visualization type",
2706
+ "explanation": "The original component shows KPI cards but the new question needs a trend chart"
2707
+ }
2708
+ \`\`\``,
2709
+ user: `## Previous Matched UIBlock
2710
+
2711
+ **Original Question:** {{ORIGINAL_USER_PROMPT}}
2712
+
2713
+ **Matched UIBlock Component:**
2714
+ \`\`\`json
2715
+ {{MATCHED_UI_BLOCK_COMPONENT}}
2716
+ \`\`\`
2717
+
2718
+ **Component Properties:**
2719
+ \`\`\`json
2720
+ {{COMPONENT_PROPS}}
2721
+ \`\`\`
2722
+
2723
+ ## Current User Question
2724
+ {{CURRENT_USER_PROMPT}}
2725
+
2726
+ ---
2727
+
2728
+ ## Adaptation Instructions
2729
+
2730
+ 1. **Analyze the difference** between the original question and the current question
2731
+ 2. **Identify what data needs to change**:
2732
+ - For single components: adapt the query/config/actions
2733
+ - For MultiComponentContainer: adapt the container title/description AND each nested component's parameters
2734
+
2735
+ 3. **Modify the parameters**:
2736
+ - **Container level** (if MultiComponentContainer):
2737
+ - Update \`title\` and \`description\` to reflect the new user question
2738
+ - Update \`actions\` if needed
2739
+
2740
+ - **For each component** (single or nested in container):
2741
+ - Identify what it shows (sales, revenue, inventory, etc.)
2742
+ - Adapt SQL queries: modify WHERE clauses, LIMIT, ORDER BY, filters, date ranges
2743
+ - Update component title and description
2744
+ - Update config settings (metrics, colors, sorting, grouping)
2745
+
2746
+ 4. **Preserve structure**: Keep the same number and type of components
2747
+
2748
+ 5. **Return complete JSON** with all adapted properties for all components`
2705
2749
  }
2706
2750
  };
2707
2751
 
@@ -2779,9 +2823,10 @@ var PromptLoader = class {
2779
2823
  }
2780
2824
  /**
2781
2825
  * Load both system and user prompts from cache and replace variables
2826
+ * Supports prompt caching by splitting static and dynamic content
2782
2827
  * @param promptName - Name of the prompt
2783
2828
  * @param variables - Variables to replace in the templates
2784
- * @returns Object containing both system and user prompts
2829
+ * @returns Object containing both system and user prompts (system can be string or array for caching)
2785
2830
  */
2786
2831
  async loadPrompts(promptName, variables) {
2787
2832
  if (!this.isInitialized) {
@@ -2792,6 +2837,26 @@ var PromptLoader = class {
2792
2837
  if (!template) {
2793
2838
  throw new Error(`Prompt template '${promptName}' not found in cache. Available prompts: ${Array.from(this.promptCache.keys()).join(", ")}`);
2794
2839
  }
2840
+ const contextMarker = "---\n\n## CONTEXT";
2841
+ if (template.system.includes(contextMarker)) {
2842
+ const [staticPart, contextPart] = template.system.split(contextMarker);
2843
+ logger.debug(`\u2713 Prompt caching enabled for '${promptName}' (static: ${staticPart.length} chars, context: ${contextPart.length} chars)`);
2844
+ const processedContext = this.replaceVariables(contextMarker + contextPart, variables);
2845
+ return {
2846
+ system: [
2847
+ {
2848
+ type: "text",
2849
+ text: staticPart.trim(),
2850
+ cache_control: { type: "ephemeral" }
2851
+ },
2852
+ {
2853
+ type: "text",
2854
+ text: processedContext.trim()
2855
+ }
2856
+ ],
2857
+ user: this.replaceVariables(template.user, variables)
2858
+ };
2859
+ }
2795
2860
  return {
2796
2861
  system: this.replaceVariables(template.system, variables),
2797
2862
  user: this.replaceVariables(template.user, variables)
@@ -2878,6 +2943,75 @@ var LLM = class {
2878
2943
  // ============================================================
2879
2944
  // PRIVATE HELPER METHODS
2880
2945
  // ============================================================
2946
+ /**
2947
+ * Normalize system prompt to Anthropic format
2948
+ * Converts string to array format if needed
2949
+ * @param sys - System prompt (string or array of blocks)
2950
+ * @returns Normalized system prompt for Anthropic API
2951
+ */
2952
+ static _normalizeSystemPrompt(sys) {
2953
+ if (typeof sys === "string") {
2954
+ return sys;
2955
+ }
2956
+ return sys;
2957
+ }
2958
+ /**
2959
+ * Log cache usage metrics from Anthropic API response
2960
+ * Shows cache hits, costs, and savings
2961
+ */
2962
+ static _logCacheUsage(usage) {
2963
+ if (!usage) return;
2964
+ const inputTokens = usage.input_tokens || 0;
2965
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
2966
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
2967
+ const outputTokens = usage.output_tokens || 0;
2968
+ const INPUT_PRICE = 0.8;
2969
+ const OUTPUT_PRICE = 4;
2970
+ const CACHE_WRITE_PRICE = 1;
2971
+ const CACHE_READ_PRICE = 0.08;
2972
+ const regularInputCost = inputTokens / 1e6 * INPUT_PRICE;
2973
+ const cacheWriteCost = cacheCreationTokens / 1e6 * CACHE_WRITE_PRICE;
2974
+ const cacheReadCost = cacheReadTokens / 1e6 * CACHE_READ_PRICE;
2975
+ const outputCost = outputTokens / 1e6 * OUTPUT_PRICE;
2976
+ const totalCost = regularInputCost + cacheWriteCost + cacheReadCost + outputCost;
2977
+ const totalInputTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
2978
+ const costWithoutCache = totalInputTokens / 1e6 * INPUT_PRICE + outputCost;
2979
+ const savings = costWithoutCache - totalCost;
2980
+ const savingsPercent = costWithoutCache > 0 ? savings / costWithoutCache * 100 : 0;
2981
+ 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");
2982
+ console.log("\u{1F4B0} PROMPT CACHING METRICS");
2983
+ 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");
2984
+ console.log("\n\u{1F4CA} Token Usage:");
2985
+ console.log(` Input (regular): ${inputTokens.toLocaleString()} tokens`);
2986
+ if (cacheCreationTokens > 0) {
2987
+ console.log(` Cache write: ${cacheCreationTokens.toLocaleString()} tokens (first request)`);
2988
+ }
2989
+ if (cacheReadTokens > 0) {
2990
+ console.log(` Cache read: ${cacheReadTokens.toLocaleString()} tokens \u26A1 HIT!`);
2991
+ }
2992
+ console.log(` Output: ${outputTokens.toLocaleString()} tokens`);
2993
+ console.log(` Total input: ${totalInputTokens.toLocaleString()} tokens`);
2994
+ console.log("\n\u{1F4B5} Cost Breakdown:");
2995
+ console.log(` Input (regular): $${regularInputCost.toFixed(6)}`);
2996
+ if (cacheCreationTokens > 0) {
2997
+ console.log(` Cache write: $${cacheWriteCost.toFixed(6)}`);
2998
+ }
2999
+ if (cacheReadTokens > 0) {
3000
+ console.log(` Cache read: $${cacheReadCost.toFixed(6)} (90% off!)`);
3001
+ }
3002
+ console.log(` Output: $${outputCost.toFixed(6)}`);
3003
+ 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`);
3004
+ console.log(` Total cost: $${totalCost.toFixed(6)}`);
3005
+ if (cacheReadTokens > 0) {
3006
+ console.log(`
3007
+ \u{1F48E} Savings: $${savings.toFixed(6)} (${savingsPercent.toFixed(1)}% off)`);
3008
+ console.log(` Without cache: $${costWithoutCache.toFixed(6)}`);
3009
+ } else if (cacheCreationTokens > 0) {
3010
+ console.log(`
3011
+ \u23F1\uFE0F Cache created - next request will be ~90% cheaper!`);
3012
+ }
3013
+ 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");
3014
+ }
2881
3015
  /**
2882
3016
  * Parse model string to extract provider and model name
2883
3017
  * @param modelString - Format: "provider/model-name" or just "model-name"
@@ -2912,7 +3046,7 @@ var LLM = class {
2912
3046
  model: modelName,
2913
3047
  max_tokens: options.maxTokens || 1e3,
2914
3048
  temperature: options.temperature,
2915
- system: messages.sys,
3049
+ system: this._normalizeSystemPrompt(messages.sys),
2916
3050
  messages: [{
2917
3051
  role: "user",
2918
3052
  content: messages.user
@@ -2930,7 +3064,7 @@ var LLM = class {
2930
3064
  model: modelName,
2931
3065
  max_tokens: options.maxTokens || 1e3,
2932
3066
  temperature: options.temperature,
2933
- system: messages.sys,
3067
+ system: this._normalizeSystemPrompt(messages.sys),
2934
3068
  messages: [{
2935
3069
  role: "user",
2936
3070
  content: messages.user
@@ -2938,6 +3072,7 @@ var LLM = class {
2938
3072
  stream: true
2939
3073
  });
2940
3074
  let fullText = "";
3075
+ let usage = null;
2941
3076
  for await (const chunk of stream) {
2942
3077
  if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
2943
3078
  const text = chunk.delta.text;
@@ -2945,8 +3080,12 @@ var LLM = class {
2945
3080
  if (options.partial) {
2946
3081
  options.partial(text);
2947
3082
  }
3083
+ } else if (chunk.type === "message_delta" && chunk.usage) {
3084
+ usage = chunk.usage;
2948
3085
  }
2949
3086
  }
3087
+ if (usage) {
3088
+ }
2950
3089
  if (json) {
2951
3090
  return this._parseJSON(fullText);
2952
3091
  }
@@ -2969,7 +3108,7 @@ var LLM = class {
2969
3108
  model: modelName,
2970
3109
  max_tokens: options.maxTokens || 4e3,
2971
3110
  temperature: options.temperature,
2972
- system: messages.sys,
3111
+ system: this._normalizeSystemPrompt(messages.sys),
2973
3112
  messages: conversationMessages,
2974
3113
  tools,
2975
3114
  stream: true
@@ -2979,6 +3118,7 @@ var LLM = class {
2979
3118
  const contentBlocks = [];
2980
3119
  let currentTextBlock = "";
2981
3120
  let currentToolUse = null;
3121
+ let usage = null;
2982
3122
  for await (const chunk of stream) {
2983
3123
  if (chunk.type === "message_start") {
2984
3124
  contentBlocks.length = 0;
@@ -3029,11 +3169,16 @@ var LLM = class {
3029
3169
  }
3030
3170
  if (chunk.type === "message_delta") {
3031
3171
  stopReason = chunk.delta.stop_reason || stopReason;
3172
+ if (chunk.usage) {
3173
+ usage = chunk.usage;
3174
+ }
3032
3175
  }
3033
3176
  if (chunk.type === "message_stop") {
3034
3177
  break;
3035
3178
  }
3036
3179
  }
3180
+ if (usage) {
3181
+ }
3037
3182
  if (stopReason === "end_turn") {
3038
3183
  break;
3039
3184
  }
@@ -3205,6 +3350,57 @@ var KB = {
3205
3350
  };
3206
3351
  var knowledge_base_default = KB;
3207
3352
 
3353
+ // src/userResponse/conversation-search.ts
3354
+ var searchConversations = async ({
3355
+ userPrompt,
3356
+ collections,
3357
+ userId,
3358
+ similarityThreshold = 0.6
3359
+ }) => {
3360
+ try {
3361
+ if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["search"]) {
3362
+ logger.info("[ConversationSearch] conversation-history.search collection not registered, skipping");
3363
+ return null;
3364
+ }
3365
+ logger.info(`[ConversationSearch] Searching conversations for: "${userPrompt.substring(0, 50)}..."`);
3366
+ logger.info(`[ConversationSearch] Using similarity threshold: ${(similarityThreshold * 100).toFixed(0)}%`);
3367
+ const result = await collections["conversation-history"]["search"]({
3368
+ userPrompt,
3369
+ userId,
3370
+ threshold: similarityThreshold
3371
+ });
3372
+ if (!result) {
3373
+ logger.info("[ConversationSearch] No matching conversations found");
3374
+ return null;
3375
+ }
3376
+ if (!result.uiBlock) {
3377
+ logger.error("[ConversationSearch] No UI block in conversation search result");
3378
+ return null;
3379
+ }
3380
+ const similarity = result.similarity || 0;
3381
+ logger.info(`[ConversationSearch] Best match similarity: ${(similarity * 100).toFixed(2)}%`);
3382
+ if (similarity < similarityThreshold) {
3383
+ logger.info(
3384
+ `[ConversationSearch] Best match has similarity ${(similarity * 100).toFixed(2)}% but below threshold ${(similarityThreshold * 100).toFixed(2)}%`
3385
+ );
3386
+ return null;
3387
+ }
3388
+ logger.info(
3389
+ `[ConversationSearch] Found matching conversation with similarity ${(similarity * 100).toFixed(2)}%`
3390
+ );
3391
+ logger.debug(`[ConversationSearch] Matched prompt: "${result.metadata?.userPrompt?.substring(0, 50)}..."`);
3392
+ return result;
3393
+ } catch (error) {
3394
+ const errorMsg = error instanceof Error ? error.message : String(error);
3395
+ logger.warn(`[ConversationSearch] Error searching conversations: ${errorMsg}`);
3396
+ return null;
3397
+ }
3398
+ };
3399
+ var ConversationSearch = {
3400
+ searchConversations
3401
+ };
3402
+ var conversation_search_default = ConversationSearch;
3403
+
3208
3404
  // src/userResponse/base-llm.ts
3209
3405
  var BaseLLM = class {
3210
3406
  constructor(config) {
@@ -3219,567 +3415,122 @@ var BaseLLM = class {
3219
3415
  return apiKey || this.apiKey || this.getDefaultApiKey();
3220
3416
  }
3221
3417
  /**
3222
- * Classify user question to determine the type and required visualizations
3418
+ * Match components from text response suggestions and generate follow-up questions
3419
+ * Takes a text response with component suggestions (c1:type format) and matches with available components
3420
+ * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
3421
+ * All components are placed in a default MultiComponentContainer layout
3422
+ * @param analysisContent - The text response containing component suggestions
3423
+ * @param components - List of available components
3424
+ * @param apiKey - Optional API key
3425
+ * @param logCollector - Optional log collector
3426
+ * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
3427
+ * @returns Object containing matched components, layout title/description, and follow-up actions
3223
3428
  */
3224
- async classifyUserQuestion(userPrompt, apiKey, logCollector, conversationHistory) {
3429
+ async matchComponentsFromAnalysis(analysisContent, components, apiKey, logCollector, componentStreamCallback) {
3225
3430
  try {
3226
- const prompts = await promptLoader.loadPrompts("classify", {
3227
- USER_PROMPT: userPrompt,
3228
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3431
+ logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
3432
+ let availableComponentsText = "No components available";
3433
+ if (components && components.length > 0) {
3434
+ availableComponentsText = components.map((comp, idx) => {
3435
+ const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3436
+ const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
3437
+ return `${idx + 1}. ID: ${comp.id}
3438
+ Name: ${comp.name}
3439
+ Type: ${comp.type}
3440
+ Description: ${comp.description || "No description"}
3441
+ Keywords: ${keywords}
3442
+ Props Structure: ${propsPreview}`;
3443
+ }).join("\n\n");
3444
+ }
3445
+ const schemaDoc = schema.generateSchemaDocumentation();
3446
+ logger.file("\n=============================\nText analysis response:", analysisContent);
3447
+ const prompts = await promptLoader.loadPrompts("match-text-components", {
3448
+ ANALYSIS_CONTENT: analysisContent,
3449
+ AVAILABLE_COMPONENTS: availableComponentsText,
3450
+ SCHEMA_DOC: schemaDoc
3229
3451
  });
3230
- const result = await LLM.stream(
3231
- {
3232
- sys: prompts.system,
3233
- user: prompts.user
3234
- },
3235
- {
3236
- model: this.model,
3237
- maxTokens: 800,
3238
- temperature: 0.2,
3239
- apiKey: this.getApiKey(apiKey)
3240
- },
3241
- true
3242
- // Parse as JSON
3243
- );
3244
- logCollector?.logExplanation(
3245
- "User question classified",
3246
- result.reasoning || "No reasoning provided",
3247
- {
3248
- questionType: result.questionType || "general",
3249
- visualizations: result.visualizations || [],
3250
- needsMultipleComponents: result.needsMultipleComponents || false
3251
- }
3252
- );
3253
- return {
3254
- questionType: result.questionType || "general",
3255
- visualizations: result.visualizations || [],
3256
- reasoning: result.reasoning || "No reasoning provided",
3257
- needsMultipleComponents: result.needsMultipleComponents || false
3258
- };
3259
- } catch (error) {
3260
- const errorMsg = error instanceof Error ? error.message : String(error);
3261
- logger.error(`[${this.getProviderName()}] Error classifying user question: ${errorMsg}`);
3262
- logger.debug(`[${this.getProviderName()}] Classification error details:`, error);
3263
- throw error;
3264
- }
3265
- }
3266
- /**
3267
- * Enhanced function that validates and modifies the entire props object based on user request
3268
- * This includes query, title, description, and config properties
3269
- */
3270
- async validateAndModifyProps(userPrompt, originalProps, componentName, componentType, componentDescription, apiKey, logCollector, conversationHistory) {
3271
- const schemaDoc = schema.generateSchemaDocumentation();
3272
- try {
3273
- const prompts = await promptLoader.loadPrompts("modify-props", {
3274
- COMPONENT_NAME: componentName,
3275
- COMPONENT_TYPE: componentType,
3276
- COMPONENT_DESCRIPTION: componentDescription || "No description",
3277
- SCHEMA_DOC: schemaDoc || "No schema available",
3278
- DEFAULT_LIMIT: this.defaultLimit,
3279
- USER_PROMPT: userPrompt,
3280
- CURRENT_PROPS: JSON.stringify(originalProps, null, 2),
3281
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3282
- });
3283
- logger.debug("props-modification: System prompt\n", prompts.system.substring(0, 100), "\n\n\n", "User prompt:", prompts.user.substring(0, 50));
3284
- const result = await LLM.stream(
3285
- {
3286
- sys: prompts.system,
3287
- user: prompts.user
3288
- },
3289
- {
3290
- model: this.model,
3291
- maxTokens: 2500,
3292
- temperature: 0.2,
3293
- apiKey: this.getApiKey(apiKey)
3294
- },
3295
- true
3296
- // Parse as JSON
3297
- );
3298
- const props = result.props || originalProps;
3299
- if (props && props.query) {
3300
- props.query = fixScalarSubqueries(props.query);
3301
- props.query = ensureQueryLimit(props.query, this.defaultLimit);
3302
- }
3303
- if (props && props.query) {
3304
- logCollector?.logQuery(
3305
- "Props query modified",
3306
- props.query,
3307
- {
3308
- modifications: result.modifications || [],
3309
- reasoning: result.reasoning || "No modifications needed"
3452
+ logger.debug(`[${this.getProviderName()}] Loaded match-text-components prompts`);
3453
+ logger.file("\n=============================\nmatch text components system prompt:", prompts.system);
3454
+ logCollector?.info("Matching components from text response...");
3455
+ let fullResponseText = "";
3456
+ let answerComponentExtracted = false;
3457
+ const answerCallback = componentStreamCallback;
3458
+ const partialCallback = answerCallback ? (chunk) => {
3459
+ fullResponseText += chunk;
3460
+ if (!answerComponentExtracted && answerCallback) {
3461
+ const hasAnswerComponentMatch = fullResponseText.match(/"hasAnswerComponent"\s*:\s*(true|false)/);
3462
+ if (!hasAnswerComponentMatch || hasAnswerComponentMatch[1] !== "true") {
3463
+ return;
3310
3464
  }
3311
- );
3312
- }
3313
- if (result.reasoning) {
3314
- logCollector?.logExplanation(
3315
- "Props modification explanation",
3316
- result.reasoning,
3317
- { modifications: result.modifications || [] }
3318
- );
3319
- }
3320
- return {
3321
- props,
3322
- isModified: result.isModified || false,
3323
- reasoning: result.reasoning || "No modifications needed",
3324
- modifications: result.modifications || []
3325
- };
3326
- } catch (error) {
3327
- const errorMsg = error instanceof Error ? error.message : String(error);
3328
- logger.error(`[${this.getProviderName()}] Error validating/modifying props: ${errorMsg}`);
3329
- logger.debug(`[${this.getProviderName()}] Props validation error details:`, error);
3330
- throw error;
3331
- }
3332
- }
3333
- /**
3334
- * Match and select a component from available components filtered by type
3335
- * This picks the best matching component based on user prompt and modifies its props
3336
- */
3337
- async generateAnalyticalComponent(userPrompt, components, preferredVisualizationType, apiKey, logCollector, conversationHistory) {
3338
- try {
3339
- const filteredComponents = preferredVisualizationType ? components.filter((c) => c.type === preferredVisualizationType) : components;
3340
- if (filteredComponents.length === 0) {
3341
- logCollector?.warn(
3342
- `No components found of type ${preferredVisualizationType}`,
3343
- "explanation",
3344
- { reason: "No matching components available for this visualization type" }
3345
- );
3346
- return {
3347
- component: null,
3348
- reasoning: `No components available of type ${preferredVisualizationType}`,
3349
- isGenerated: false
3350
- };
3351
- }
3352
- const componentsText = filteredComponents.map((comp, idx) => {
3353
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3354
- const category = comp.category || "general";
3355
- const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
3356
- return `${idx + 1}. ID: ${comp.id}
3357
- Name: ${comp.name}
3358
- Type: ${comp.type}
3359
- Category: ${category}
3360
- Description: ${comp.description || "No description"}
3361
- Keywords: ${keywords}
3362
- Props Preview: ${propsPreview}`;
3363
- }).join("\n\n");
3364
- const visualizationConstraint = preferredVisualizationType ? `
3365
- **IMPORTANT: Components are filtered to type ${preferredVisualizationType}. Select the best match.**
3366
- ` : "";
3367
- const prompts = await promptLoader.loadPrompts("single-component", {
3368
- COMPONENT_TYPE: preferredVisualizationType || "any",
3369
- COMPONENTS_LIST: componentsText,
3370
- VISUALIZATION_CONSTRAINT: visualizationConstraint,
3371
- USER_PROMPT: userPrompt,
3372
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3373
- });
3374
- logger.debug("single-component: System prompt\n", prompts.system.substring(0, 100), "\n\n\n", "User prompt:", prompts.user.substring(0, 50));
3375
- const result = await LLM.stream(
3376
- {
3377
- sys: prompts.system,
3378
- user: prompts.user
3379
- },
3380
- {
3381
- model: this.model,
3382
- maxTokens: 2e3,
3383
- temperature: 0.2,
3384
- apiKey: this.getApiKey(apiKey)
3385
- },
3386
- true
3387
- // Parse as JSON
3388
- );
3389
- if (!result.canGenerate || result.confidence < 50) {
3390
- logCollector?.warn(
3391
- "Cannot match component",
3392
- "explanation",
3393
- { reason: result.reasoning || "Unable to find matching component for this question" }
3394
- );
3395
- return {
3396
- component: null,
3397
- reasoning: result.reasoning || "Unable to find matching component for this question",
3398
- isGenerated: false
3399
- };
3400
- }
3401
- const componentIndex = result.componentIndex;
3402
- const componentId = result.componentId;
3403
- let matchedComponent = null;
3404
- if (componentId) {
3405
- matchedComponent = filteredComponents.find((c) => c.id === componentId);
3406
- }
3407
- if (!matchedComponent && componentIndex) {
3408
- matchedComponent = filteredComponents[componentIndex - 1];
3409
- }
3410
- if (!matchedComponent) {
3411
- logCollector?.warn("Component not found in filtered list");
3412
- return {
3413
- component: null,
3414
- reasoning: "Component not found in filtered list",
3415
- isGenerated: false
3416
- };
3417
- }
3418
- logCollector?.info(`Matched component: ${matchedComponent.name} (confidence: ${result.confidence}%)`);
3419
- const propsValidation = await this.validateAndModifyProps(
3420
- userPrompt,
3421
- matchedComponent.props,
3422
- matchedComponent.name,
3423
- matchedComponent.type,
3424
- matchedComponent.description,
3425
- apiKey,
3426
- logCollector,
3427
- conversationHistory
3428
- );
3429
- const modifiedComponent = {
3430
- ...matchedComponent,
3431
- props: propsValidation.props
3432
- };
3433
- logCollector?.logExplanation(
3434
- "Analytical component selected and modified",
3435
- result.reasoning || "Selected component based on analytical question",
3436
- {
3437
- componentName: matchedComponent.name,
3438
- componentType: matchedComponent.type,
3439
- confidence: result.confidence,
3440
- propsModified: propsValidation.isModified
3441
- }
3442
- );
3443
- return {
3444
- component: modifiedComponent,
3445
- reasoning: result.reasoning || "Selected and modified component based on analytical question",
3446
- isGenerated: true
3447
- };
3448
- } catch (error) {
3449
- const errorMsg = error instanceof Error ? error.message : String(error);
3450
- logger.error(`[${this.getProviderName()}] Error generating analytical component: ${errorMsg}`);
3451
- logger.debug(`[${this.getProviderName()}] Analytical component generation error details:`, error);
3452
- throw error;
3453
- }
3454
- }
3455
- /**
3456
- * Generate container metadata (title and description) for multi-component dashboard
3457
- */
3458
- async generateContainerMetadata(userPrompt, visualizationTypes, apiKey, logCollector, conversationHistory) {
3459
- try {
3460
- const prompts = await promptLoader.loadPrompts("container-metadata", {
3461
- USER_PROMPT: userPrompt,
3462
- VISUALIZATION_TYPES: visualizationTypes.join(", "),
3463
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3464
- });
3465
- const result = await LLM.stream(
3466
- {
3467
- sys: prompts.system,
3468
- user: prompts.user
3469
- },
3470
- {
3471
- model: this.model,
3472
- maxTokens: 500,
3473
- temperature: 0.3,
3474
- apiKey: this.getApiKey(apiKey)
3475
- },
3476
- true
3477
- // Parse as JSON
3478
- );
3479
- logCollector?.logExplanation(
3480
- "Container metadata generated",
3481
- `Generated title and description for multi-component dashboard`,
3482
- {
3483
- title: result.title,
3484
- description: result.description,
3485
- visualizationTypes
3486
- }
3487
- );
3488
- return {
3489
- title: result.title || `${userPrompt} - Dashboard`,
3490
- description: result.description || `Multi-component dashboard showing ${visualizationTypes.join(", ")}`
3491
- };
3492
- } catch (error) {
3493
- const errorMsg = error instanceof Error ? error.message : String(error);
3494
- logger.error(`[${this.getProviderName()}] Error generating container metadata: ${errorMsg}`);
3495
- logger.debug(`[${this.getProviderName()}] Container metadata error details:`, error);
3496
- return {
3497
- title: `${userPrompt} - Dashboard`,
3498
- description: `Multi-component dashboard showing ${visualizationTypes.join(", ")}`
3499
- };
3500
- }
3501
- }
3502
- /**
3503
- * Match component from a list with enhanced props modification
3504
- */
3505
- async matchComponent(userPrompt, components, apiKey, logCollector, conversationHistory) {
3506
- try {
3507
- const componentsText = components.map((comp, idx) => {
3508
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3509
- const category = comp.category || "general";
3510
- return `${idx + 1}. ID: ${comp.id}
3511
- Name: ${comp.name}
3512
- Type: ${comp.type}
3513
- Category: ${category}
3514
- Description: ${comp.description || "No description"}
3515
- Keywords: ${keywords}`;
3516
- }).join("\n\n");
3517
- const prompts = await promptLoader.loadPrompts("match-component", {
3518
- COMPONENTS_TEXT: componentsText,
3519
- USER_PROMPT: userPrompt,
3520
- CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
3521
- });
3522
- const result = await LLM.stream(
3523
- {
3524
- sys: prompts.system,
3525
- user: prompts.user
3526
- },
3527
- {
3528
- model: this.model,
3529
- maxTokens: 800,
3530
- temperature: 0.2,
3531
- apiKey: this.getApiKey(apiKey)
3532
- },
3533
- true
3534
- // Parse as JSON
3535
- );
3536
- const componentIndex = result.componentIndex;
3537
- const componentId = result.componentId;
3538
- const confidence = result.confidence || 0;
3539
- let component = null;
3540
- if (componentId) {
3541
- component = components.find((c) => c.id === componentId);
3542
- }
3543
- if (!component && componentIndex) {
3544
- component = components[componentIndex - 1];
3545
- }
3546
- const matchedMsg = `${this.getProviderName()} matched component: ${component?.name || "None"}`;
3547
- logger.info(`[${this.getProviderName()}] \u2713 ${matchedMsg}`);
3548
- logCollector?.info(matchedMsg);
3549
- if (result.alternativeMatches && result.alternativeMatches.length > 0) {
3550
- logger.debug(`[${this.getProviderName()}] Alternative matches found: ${result.alternativeMatches.length}`);
3551
- const altMatches = result.alternativeMatches.map(
3552
- (alt) => `${components[alt.index - 1]?.name} (${alt.score}%): ${alt.reason}`
3553
- ).join(" | ");
3554
- logCollector?.info(`Alternative matches: ${altMatches}`);
3555
- result.alternativeMatches.forEach((alt) => {
3556
- logger.debug(`[${this.getProviderName()}] - ${components[alt.index - 1]?.name} (${alt.score}%): ${alt.reason}`);
3557
- });
3558
- }
3559
- if (!component) {
3560
- const noMatchMsg = `No matching component found (confidence: ${confidence}%)`;
3561
- logger.warn(`[${this.getProviderName()}] \u2717 ${noMatchMsg}`);
3562
- logCollector?.warn(noMatchMsg);
3563
- const genMsg = "Attempting to match component from analytical question...";
3564
- logger.info(`[${this.getProviderName()}] \u2713 ${genMsg}`);
3565
- logCollector?.info(genMsg);
3566
- const generatedResult = await this.generateAnalyticalComponent(userPrompt, components, void 0, apiKey, logCollector, conversationHistory);
3567
- if (generatedResult.component) {
3568
- const genSuccessMsg = `Successfully matched component: ${generatedResult.component.name}`;
3569
- logCollector?.info(genSuccessMsg);
3570
- return {
3571
- component: generatedResult.component,
3572
- reasoning: generatedResult.reasoning,
3573
- method: `${this.getProviderName()}-generated`,
3574
- confidence: 100,
3575
- // Generated components are considered 100% match to the question
3576
- propsModified: false,
3577
- queryModified: false
3578
- };
3579
- }
3580
- logCollector?.error("Failed to match component");
3581
- return {
3582
- component: null,
3583
- reasoning: result.reasoning || "No matching component found and unable to match component",
3584
- method: `${this.getProviderName()}-llm`,
3585
- confidence
3586
- };
3587
- }
3588
- let propsModified = false;
3589
- let propsModifications = [];
3590
- let queryModified = false;
3591
- let queryReasoning = "";
3592
- if (component && component.props) {
3593
- const propsValidation = await this.validateAndModifyProps(
3594
- userPrompt,
3595
- component.props,
3596
- component.name,
3597
- component.type,
3598
- component.description,
3599
- apiKey,
3600
- logCollector,
3601
- conversationHistory
3602
- );
3603
- const originalQuery = component.props.query;
3604
- const modifiedQuery = propsValidation.props.query;
3605
- component = {
3606
- ...component,
3607
- props: propsValidation.props
3608
- };
3609
- propsModified = propsValidation.isModified;
3610
- propsModifications = propsValidation.modifications;
3611
- queryModified = originalQuery !== modifiedQuery;
3612
- queryReasoning = propsValidation.reasoning;
3613
- }
3614
- return {
3615
- component,
3616
- reasoning: result.reasoning || "No reasoning provided",
3617
- queryModified,
3618
- queryReasoning,
3619
- propsModified,
3620
- propsModifications,
3621
- method: `${this.getProviderName()}-llm`,
3622
- confidence
3623
- };
3624
- } catch (error) {
3625
- const errorMsg = error instanceof Error ? error.message : String(error);
3626
- logger.error(`[${this.getProviderName()}] Error matching component: ${errorMsg}`);
3627
- logger.debug(`[${this.getProviderName()}] Component matching error details:`, error);
3628
- logCollector?.error(`Error matching component: ${errorMsg}`);
3629
- throw error;
3630
- }
3631
- }
3632
- /**
3633
- * Match multiple components for analytical questions by visualization types
3634
- * This is used when the user needs multiple visualizations
3635
- */
3636
- async generateMultipleAnalyticalComponents(userPrompt, availableComponents, visualizationTypes, apiKey, logCollector, conversationHistory) {
3637
- try {
3638
- console.log("\u2713 Matching multiple components:", visualizationTypes);
3639
- const components = [];
3640
- for (const vizType of visualizationTypes) {
3641
- const result = await this.generateAnalyticalComponent(userPrompt, availableComponents, vizType, apiKey, logCollector, conversationHistory);
3642
- if (result.component) {
3643
- components.push(result.component);
3644
- }
3645
- }
3646
- if (components.length === 0) {
3647
- return {
3648
- components: [],
3649
- reasoning: "Failed to match any components",
3650
- isGenerated: false
3651
- };
3652
- }
3653
- return {
3654
- components,
3655
- reasoning: `Matched ${components.length} components: ${visualizationTypes.join(", ")}`,
3656
- isGenerated: true
3657
- };
3658
- } catch (error) {
3659
- const errorMsg = error instanceof Error ? error.message : String(error);
3660
- logger.error(`[${this.getProviderName()}] Error matching multiple analytical components: ${errorMsg}`);
3661
- logger.debug(`[${this.getProviderName()}] Multiple components matching error details:`, error);
3662
- return {
3663
- components: [],
3664
- reasoning: "Error occurred while matching components",
3665
- isGenerated: false
3666
- };
3667
- }
3668
- }
3669
- /**
3670
- * Match multiple components and wrap them in a container
3671
- */
3672
- async generateMultiComponentResponse(userPrompt, availableComponents, visualizationTypes, apiKey, logCollector, conversationHistory) {
3673
- try {
3674
- const matchResult = await this.generateMultipleAnalyticalComponents(
3675
- userPrompt,
3676
- availableComponents,
3677
- visualizationTypes,
3678
- apiKey,
3679
- logCollector,
3680
- conversationHistory
3681
- );
3682
- if (!matchResult.isGenerated || matchResult.components.length === 0) {
3683
- return {
3684
- containerComponent: null,
3685
- reasoning: matchResult.reasoning || "Unable to match multi-component dashboard",
3686
- isGenerated: false
3687
- };
3688
- }
3689
- const generatedComponents = matchResult.components;
3690
- generatedComponents.forEach((component, index) => {
3691
- if (component.props.query) {
3692
- logCollector?.logQuery(
3693
- `Multi-component query generated (${index + 1}/${generatedComponents.length})`,
3694
- component.props.query,
3695
- {
3696
- componentType: component.type,
3697
- title: component.props.title,
3698
- position: index + 1,
3699
- totalComponents: generatedComponents.length
3465
+ const answerComponentStartMatch = fullResponseText.match(/"answerComponent"\s*:\s*\{/);
3466
+ if (!answerComponentStartMatch) {
3467
+ return;
3468
+ }
3469
+ const startPos = answerComponentStartMatch.index + answerComponentStartMatch[0].length - 1;
3470
+ let braceDepth = 0;
3471
+ let inString = false;
3472
+ let escapeNext = false;
3473
+ let endPos = -1;
3474
+ for (let i = startPos; i < fullResponseText.length; i++) {
3475
+ const char = fullResponseText[i];
3476
+ if (escapeNext) {
3477
+ escapeNext = false;
3478
+ continue;
3479
+ }
3480
+ if (char === "\\") {
3481
+ escapeNext = true;
3482
+ continue;
3483
+ }
3484
+ if (char === '"') {
3485
+ inString = !inString;
3486
+ continue;
3487
+ }
3488
+ if (!inString) {
3489
+ if (char === "{") {
3490
+ braceDepth++;
3491
+ } else if (char === "}") {
3492
+ braceDepth--;
3493
+ if (braceDepth === 0) {
3494
+ endPos = i + 1;
3495
+ break;
3496
+ }
3497
+ }
3498
+ }
3499
+ }
3500
+ if (endPos > startPos) {
3501
+ const answerComponentString = fullResponseText.substring(startPos, endPos);
3502
+ try {
3503
+ const answerComponentData = JSON.parse(answerComponentString);
3504
+ if (answerComponentData && answerComponentData.componentId) {
3505
+ const originalComponent = components.find((c) => c.id === answerComponentData.componentId);
3506
+ if (originalComponent) {
3507
+ const answerComponent = {
3508
+ ...originalComponent,
3509
+ props: {
3510
+ ...originalComponent.props,
3511
+ ...answerComponentData.props
3512
+ }
3513
+ };
3514
+ const streamTime = (/* @__PURE__ */ new Date()).toISOString();
3515
+ logger.info(`[${this.getProviderName()}] \u2713 [${streamTime}] Answer component detected in stream: ${answerComponent.name} (${answerComponent.type}) - STREAMING TO FRONTEND NOW`);
3516
+ logCollector?.info(`\u2713 Answer component: ${answerComponent.name} (${answerComponent.type}) - streaming to frontend at ${streamTime}`);
3517
+ if (answerComponentData.props?.query) {
3518
+ logCollector?.logQuery(
3519
+ "Answer component query",
3520
+ answerComponentData.props.query,
3521
+ { componentName: answerComponent.name, componentType: answerComponent.type, reasoning: answerComponentData.reasoning }
3522
+ );
3523
+ }
3524
+ answerCallback(answerComponent);
3525
+ answerComponentExtracted = true;
3526
+ }
3527
+ }
3528
+ } catch (e) {
3529
+ logger.debug(`[${this.getProviderName()}] Partial answerComponent parse failed, waiting for more data...`);
3700
3530
  }
3701
- );
3702
- }
3703
- });
3704
- const containerTitle = `${userPrompt} - Dashboard`;
3705
- const containerDescription = `Multi-component dashboard showing ${visualizationTypes.join(", ")}`;
3706
- logCollector?.logExplanation(
3707
- "Multi-component dashboard matched",
3708
- matchResult.reasoning || `Matched ${generatedComponents.length} components for comprehensive analysis`,
3709
- {
3710
- totalComponents: generatedComponents.length,
3711
- componentTypes: generatedComponents.map((c) => c.type),
3712
- componentNames: generatedComponents.map((c) => c.name),
3713
- containerTitle,
3714
- containerDescription
3715
- }
3716
- );
3717
- const containerComponent = {
3718
- id: `multi_container_${Date.now()}`,
3719
- name: "MultiComponentContainer",
3720
- type: "Container",
3721
- description: containerDescription,
3722
- category: "dynamic",
3723
- keywords: ["multi", "container", "dashboard"],
3724
- props: {
3725
- config: {
3726
- components: generatedComponents,
3727
- layout: "grid",
3728
- spacing: 24,
3729
- title: containerTitle,
3730
- description: containerDescription
3731
3531
  }
3732
3532
  }
3733
- };
3734
- return {
3735
- containerComponent,
3736
- reasoning: matchResult.reasoning || `Matched multi-component dashboard with ${generatedComponents.length} components`,
3737
- isGenerated: true
3738
- };
3739
- } catch (error) {
3740
- const errorMsg = error instanceof Error ? error.message : String(error);
3741
- logger.error(`[${this.getProviderName()}] Error generating multi-component response: ${errorMsg}`);
3742
- logger.debug(`[${this.getProviderName()}] Multi-component response error details:`, error);
3743
- throw error;
3744
- }
3745
- }
3746
- /**
3747
- * Match components from text response suggestions and generate follow-up questions
3748
- * Takes a text response with component suggestions (c1:type format) and matches with available components
3749
- * Also generates title, description, and intelligent follow-up questions (actions) based on the analysis
3750
- * All components are placed in a default MultiComponentContainer layout
3751
- * @param textResponse - The text response containing component suggestions
3752
- * @param components - List of available components
3753
- * @param apiKey - Optional API key
3754
- * @param logCollector - Optional log collector
3755
- * @param componentStreamCallback - Optional callback to stream primary KPI component as soon as it's identified
3756
- * @returns Object containing matched components, layout title/description, and follow-up actions
3757
- */
3758
- async matchComponentsFromTextResponse(textResponse, components, apiKey, logCollector) {
3759
- try {
3760
- logger.debug(`[${this.getProviderName()}] Starting component matching from text response`);
3761
- let availableComponentsText = "No components available";
3762
- if (components && components.length > 0) {
3763
- availableComponentsText = components.map((comp, idx) => {
3764
- const keywords = comp.keywords ? comp.keywords.join(", ") : "";
3765
- const propsPreview = comp.props ? JSON.stringify(comp.props, null, 2) : "No props";
3766
- return `${idx + 1}. ID: ${comp.id}
3767
- Name: ${comp.name}
3768
- Type: ${comp.type}
3769
- Description: ${comp.description || "No description"}
3770
- Keywords: ${keywords}
3771
- Props Structure: ${propsPreview}`;
3772
- }).join("\n\n");
3773
- }
3774
- const schemaDoc = schema.generateSchemaDocumentation();
3775
- const prompts = await promptLoader.loadPrompts("match-text-components", {
3776
- TEXT_RESPONSE: textResponse,
3777
- AVAILABLE_COMPONENTS: availableComponentsText,
3778
- SCHEMA_DOC: schemaDoc
3779
- });
3780
- logger.debug(`[${this.getProviderName()}] Loaded match-text-components prompts`);
3781
- logger.file("\n=============================\nmatch text components system prompt:", prompts.system);
3782
- logCollector?.info("Matching components from text response...");
3533
+ } : void 0;
3783
3534
  const result = await LLM.stream(
3784
3535
  {
3785
3536
  sys: prompts.system,
@@ -3789,23 +3540,54 @@ var BaseLLM = class {
3789
3540
  model: this.model,
3790
3541
  maxTokens: 3e3,
3791
3542
  temperature: 0.2,
3792
- apiKey: this.getApiKey(apiKey)
3543
+ apiKey: this.getApiKey(apiKey),
3544
+ partial: partialCallback
3793
3545
  },
3794
3546
  true
3795
3547
  // Parse as JSON
3796
3548
  );
3797
3549
  logger.debug(`[${this.getProviderName()}] Component matching response parsed successfully`);
3550
+ const componentSuggestionPattern = /c\d+:(\w+)\s*:\s*(.+)/g;
3551
+ const suggestedComponents = [];
3552
+ let match;
3553
+ while ((match = componentSuggestionPattern.exec(analysisContent)) !== null) {
3554
+ suggestedComponents.push({
3555
+ type: match[1],
3556
+ reasoning: match[2].trim()
3557
+ });
3558
+ }
3798
3559
  const matchedComponents = result.matchedComponents || [];
3799
3560
  const layoutTitle = result.layoutTitle || "Dashboard";
3800
3561
  const layoutDescription = result.layoutDescription || "Multi-component dashboard";
3562
+ logger.info(`[${this.getProviderName()}] \u{1F4CA} Component Suggestions from Text Analysis: ${suggestedComponents.length}`);
3563
+ suggestedComponents.forEach((comp, idx) => {
3564
+ logger.info(`[${this.getProviderName()}] c${idx + 1}: ${comp.type} - ${comp.reasoning}`);
3565
+ });
3566
+ logger.info(`[${this.getProviderName()}] \u{1F4E6} Matched Components from LLM: ${matchedComponents.length}`);
3567
+ matchedComponents.forEach((comp, idx) => {
3568
+ logger.info(`[${this.getProviderName()}] ${idx + 1}. ${comp.componentType} (${comp.componentName}) - ${comp.originalSuggestion || "N/A"}`);
3569
+ });
3570
+ if (suggestedComponents.length !== matchedComponents.length) {
3571
+ logger.warn(`[${this.getProviderName()}] \u26A0\uFE0F MISMATCH: Text suggested ${suggestedComponents.length} components, but LLM matched ${matchedComponents.length}`);
3572
+ }
3573
+ logger.file("\n=============================\nFull LLM response:", JSON.stringify(result, null, 2));
3801
3574
  const rawActions = result.actions || [];
3802
3575
  const actions = convertQuestionsToActions(rawActions);
3803
3576
  logger.info(`[${this.getProviderName()}] Matched ${matchedComponents.length} components from text response`);
3804
3577
  logger.info(`[${this.getProviderName()}] Layout title: "${layoutTitle}"`);
3805
3578
  logger.info(`[${this.getProviderName()}] Layout description: "${layoutDescription}"`);
3806
3579
  logger.info(`[${this.getProviderName()}] Generated ${actions.length} follow-up actions`);
3580
+ if (suggestedComponents.length > 0) {
3581
+ logCollector?.info(`\u{1F4DD} Text Analysis suggested ${suggestedComponents.length} dashboard components:`);
3582
+ suggestedComponents.forEach((comp, idx) => {
3583
+ logCollector?.info(` c${idx + 1}: ${comp.type} - ${comp.reasoning}`);
3584
+ });
3585
+ }
3807
3586
  if (matchedComponents.length > 0) {
3808
- logCollector?.info(`Matched ${matchedComponents.length} components for visualization`);
3587
+ logCollector?.info(`\u{1F4E6} Matched ${matchedComponents.length} components for dashboard`);
3588
+ if (suggestedComponents.length !== matchedComponents.length) {
3589
+ logCollector?.warn(`\u26A0\uFE0F Component count mismatch: Suggested ${suggestedComponents.length}, but matched ${matchedComponents.length}`);
3590
+ }
3809
3591
  logCollector?.info(`Dashboard: "${layoutTitle}"`);
3810
3592
  matchedComponents.forEach((comp, idx) => {
3811
3593
  logCollector?.info(` ${idx + 1}. ${comp.componentName} (${comp.componentType}): ${comp.reasoning}`);
@@ -3857,148 +3639,136 @@ var BaseLLM = class {
3857
3639
  }
3858
3640
  }
3859
3641
  /**
3860
- * Execute external tools based on user request using agentic LLM tool calling
3861
- * The LLM can directly call tools and retry on errors
3862
- * @param userPrompt - The user's question/request
3863
- * @param availableTools - Array of available external tools
3864
- * @param apiKey - Optional API key for LLM
3865
- * @param logCollector - Optional log collector
3866
- * @returns Object containing tool execution results and summary
3642
+ * Classify user question into category and detect external tools needed
3643
+ * Determines if question is for data analysis, requires external tools, or needs text response
3867
3644
  */
3868
- async executeExternalTools(userPrompt, availableTools, apiKey, logCollector) {
3869
- const MAX_TOOL_ATTEMPTS = 3;
3870
- const toolResults = [];
3645
+ async classifyQuestionCategory(userPrompt, apiKey, logCollector, conversationHistory, externalTools) {
3871
3646
  try {
3872
- logger.debug(`[${this.getProviderName()}] Starting agentic external tool execution`);
3873
- logger.debug(`[${this.getProviderName()}] Available tools: ${availableTools.map((t) => t.name).join(", ")}`);
3874
- const llmTools = availableTools.map((tool) => {
3875
- const properties = {};
3876
- const required = [];
3877
- Object.entries(tool.params || {}).forEach(([key, type]) => {
3878
- properties[key] = {
3879
- type: String(type).toLowerCase(),
3880
- description: `${key} parameter`
3881
- };
3882
- required.push(key);
3883
- });
3884
- return {
3885
- name: tool.id,
3886
- description: tool.description,
3887
- input_schema: {
3888
- type: "object",
3889
- properties,
3890
- required
3891
- }
3892
- };
3647
+ const availableToolsDoc = externalTools && externalTools.length > 0 ? externalTools.map((tool) => {
3648
+ const paramsStr = Object.entries(tool.params || {}).map(([key, type]) => `${key}: ${type}`).join(", ");
3649
+ return `- **${tool.name}** (id: ${tool.id})
3650
+ Description: ${tool.description}
3651
+ Parameters: ${paramsStr}`;
3652
+ }).join("\n\n") : "No external tools available";
3653
+ const prompts = await promptLoader.loadPrompts("category-classification", {
3654
+ USER_PROMPT: userPrompt,
3655
+ CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
3656
+ AVAILABLE_TOOLS: availableToolsDoc
3893
3657
  });
3894
- const toolAttempts = /* @__PURE__ */ new Map();
3895
- const toolHandler = async (toolName, toolInput) => {
3896
- const tool = availableTools.find((t) => t.id === toolName);
3897
- if (!tool) {
3898
- const errorMsg = `Tool ${toolName} not found in available tools`;
3899
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
3900
- logCollector?.error(errorMsg);
3901
- throw new Error(errorMsg);
3902
- }
3903
- const attempts = (toolAttempts.get(toolName) || 0) + 1;
3904
- toolAttempts.set(toolName, attempts);
3905
- logger.info(`[${this.getProviderName()}] Executing tool: ${tool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
3906
- logCollector?.info(`Executing ${tool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
3907
- if (attempts > MAX_TOOL_ATTEMPTS) {
3908
- const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${tool.name}`;
3909
- logger.error(`[${this.getProviderName()}] ${errorMsg}`);
3910
- logCollector?.error(errorMsg);
3911
- toolResults.push({
3912
- toolName: tool.name,
3913
- toolId: tool.id,
3914
- result: null,
3915
- error: errorMsg
3916
- });
3917
- throw new Error(errorMsg);
3918
- }
3919
- try {
3920
- logger.debug(`[${this.getProviderName()}] Tool ${tool.name} parameters:`, toolInput);
3921
- const result2 = await tool.fn(toolInput);
3922
- logger.info(`[${this.getProviderName()}] Tool ${tool.name} executed successfully`);
3923
- logCollector?.info(`\u2713 ${tool.name} completed successfully`);
3924
- toolResults.push({
3925
- toolName: tool.name,
3926
- toolId: tool.id,
3927
- result: result2
3928
- });
3929
- return JSON.stringify(result2, null, 2);
3930
- } catch (error) {
3931
- const errorMsg = error instanceof Error ? error.message : String(error);
3932
- logger.error(`[${this.getProviderName()}] Tool ${tool.name} failed (attempt ${attempts}): ${errorMsg}`);
3933
- logCollector?.error(`\u2717 ${tool.name} failed: ${errorMsg}`);
3934
- if (attempts >= MAX_TOOL_ATTEMPTS) {
3935
- toolResults.push({
3936
- toolName: tool.name,
3937
- toolId: tool.id,
3938
- result: null,
3939
- error: errorMsg
3940
- });
3941
- }
3942
- throw new Error(`Tool execution failed: ${errorMsg}`);
3658
+ const result = await LLM.stream(
3659
+ {
3660
+ sys: prompts.system,
3661
+ user: prompts.user
3662
+ },
3663
+ {
3664
+ model: this.model,
3665
+ maxTokens: 1e3,
3666
+ temperature: 0.2,
3667
+ apiKey: this.getApiKey(apiKey)
3668
+ },
3669
+ true
3670
+ // Parse as JSON
3671
+ );
3672
+ logCollector?.logExplanation(
3673
+ "Question category classified",
3674
+ result.reasoning || "No reasoning provided",
3675
+ {
3676
+ category: result.category,
3677
+ externalTools: result.externalTools || [],
3678
+ dataAnalysisType: result.dataAnalysisType,
3679
+ confidence: result.confidence
3943
3680
  }
3681
+ );
3682
+ return {
3683
+ category: result.category || "data_analysis",
3684
+ externalTools: result.externalTools || [],
3685
+ dataAnalysisType: result.dataAnalysisType,
3686
+ reasoning: result.reasoning || "No reasoning provided",
3687
+ confidence: result.confidence || 0
3944
3688
  };
3945
- const prompts = await promptLoader.loadPrompts("execute-tools", {
3946
- USER_PROMPT: userPrompt,
3947
- AVAILABLE_TOOLS: availableTools.map((tool, idx) => {
3948
- const paramsText = Object.entries(tool.params || {}).map(([key, type]) => ` - ${key}: ${type}`).join("\n");
3949
- return `${idx + 1}. ID: ${tool.id}
3950
- Name: ${tool.name}
3951
- Description: ${tool.description}
3952
- Parameters:
3953
- ${paramsText}`;
3954
- }).join("\n\n")
3689
+ } catch (error) {
3690
+ const errorMsg = error instanceof Error ? error.message : String(error);
3691
+ logger.error(`[${this.getProviderName()}] Error classifying question category: ${errorMsg}`);
3692
+ logger.debug(`[${this.getProviderName()}] Category classification error details:`, error);
3693
+ throw error;
3694
+ }
3695
+ }
3696
+ /**
3697
+ * Adapt UI block parameters based on current user question
3698
+ * Takes a matched UI block from semantic search and modifies its props to answer the new question
3699
+ */
3700
+ async adaptUIBlockParameters(currentUserPrompt, originalUserPrompt, matchedUIBlock, apiKey, logCollector) {
3701
+ try {
3702
+ if (!matchedUIBlock || !matchedUIBlock.generatedComponentMetadata) {
3703
+ return {
3704
+ success: false,
3705
+ explanation: "No component found in matched UI block"
3706
+ };
3707
+ }
3708
+ const component = matchedUIBlock.generatedComponentMetadata;
3709
+ const schemaDoc = schema.generateSchemaDocumentation();
3710
+ const prompts = await promptLoader.loadPrompts("adapt-ui-block-params", {
3711
+ ORIGINAL_USER_PROMPT: originalUserPrompt,
3712
+ CURRENT_USER_PROMPT: currentUserPrompt,
3713
+ MATCHED_UI_BLOCK_COMPONENT: JSON.stringify(component, null, 2),
3714
+ COMPONENT_PROPS: JSON.stringify(component.props, null, 2),
3715
+ SCHEMA_DOC: schemaDoc || "No schema available"
3955
3716
  });
3956
- logger.debug(`[${this.getProviderName()}] Using agentic tool calling for external tools`);
3957
- logCollector?.info("Analyzing request and executing external tools...");
3958
- const result = await LLM.streamWithTools(
3717
+ const result = await LLM.stream(
3959
3718
  {
3960
3719
  sys: prompts.system,
3961
3720
  user: prompts.user
3962
3721
  },
3963
- llmTools,
3964
- toolHandler,
3965
3722
  {
3966
3723
  model: this.model,
3967
3724
  maxTokens: 2e3,
3968
3725
  temperature: 0.2,
3969
3726
  apiKey: this.getApiKey(apiKey)
3970
3727
  },
3971
- MAX_TOOL_ATTEMPTS + 2
3972
- // max iterations: allows for retries + final response
3728
+ true
3729
+ // Parse as JSON
3973
3730
  );
3974
- logger.info(`[${this.getProviderName()}] External tool execution completed`);
3975
- const successfulTools = toolResults.filter((r) => !r.error);
3976
- const failedTools = toolResults.filter((r) => r.error);
3977
- let summary = "";
3978
- if (successfulTools.length > 0) {
3979
- summary += `Successfully executed ${successfulTools.length} tool(s): ${successfulTools.map((t) => t.toolName).join(", ")}.
3980
- `;
3981
- }
3982
- if (failedTools.length > 0) {
3983
- summary += `Failed to execute ${failedTools.length} tool(s): ${failedTools.map((t) => t.toolName).join(", ")}.`;
3731
+ if (!result.success) {
3732
+ logger.info(
3733
+ `[${this.getProviderName()}] Could not adapt UI block: ${result.reason}`
3734
+ );
3735
+ logCollector?.warn(
3736
+ "Could not adapt matched UI block",
3737
+ "explanation",
3738
+ { reason: result.reason }
3739
+ );
3740
+ return {
3741
+ success: false,
3742
+ explanation: result.explanation || "Adaptation not possible"
3743
+ };
3984
3744
  }
3985
- if (toolResults.length === 0) {
3986
- summary = "No external tools were needed for this request.";
3745
+ if (result.adaptedComponent?.props?.query) {
3746
+ result.adaptedComponent.props.query = ensureQueryLimit(
3747
+ result.adaptedComponent.props.query,
3748
+ this.defaultLimit
3749
+ );
3987
3750
  }
3988
- logger.info(`[${this.getProviderName()}] Tool execution summary: ${summary}`);
3751
+ logCollector?.logExplanation(
3752
+ "UI block parameters adapted",
3753
+ result.explanation || "Parameters adapted successfully",
3754
+ {
3755
+ parametersChanged: result.parametersChanged || [],
3756
+ componentType: result.adaptedComponent?.type
3757
+ }
3758
+ );
3989
3759
  return {
3990
- toolResults,
3991
- summary,
3992
- hasResults: successfulTools.length > 0
3760
+ success: true,
3761
+ adaptedComponent: result.adaptedComponent,
3762
+ parametersChanged: result.parametersChanged,
3763
+ explanation: result.explanation || "Parameters adapted successfully"
3993
3764
  };
3994
3765
  } catch (error) {
3995
3766
  const errorMsg = error instanceof Error ? error.message : String(error);
3996
- logger.error(`[${this.getProviderName()}] Error in external tool execution: ${errorMsg}`);
3997
- logCollector?.error(`Error executing external tools: ${errorMsg}`);
3767
+ logger.error(`[${this.getProviderName()}] Error adapting UI block parameters: ${errorMsg}`);
3768
+ logger.debug(`[${this.getProviderName()}] Adaptation error details:`, error);
3998
3769
  return {
3999
- toolResults,
4000
- summary: `Error executing external tools: ${errorMsg}`,
4001
- hasResults: false
3770
+ success: false,
3771
+ explanation: `Error adapting parameters: ${errorMsg}`
4002
3772
  };
4003
3773
  }
4004
3774
  }
@@ -4017,32 +3787,24 @@ ${paramsText}`;
4017
3787
  logger.debug(`[${this.getProviderName()}] Starting text response generation`);
4018
3788
  logger.debug(`[${this.getProviderName()}] User prompt: "${userPrompt.substring(0, 50)}..."`);
4019
3789
  try {
4020
- let externalToolContext = "No external tools were used for this request.";
3790
+ let availableToolsDoc = "No external tools are available for this request.";
4021
3791
  if (externalTools && externalTools.length > 0) {
4022
- logger.info(`[${this.getProviderName()}] Executing external tools...`);
4023
- const toolExecution = await this.executeExternalTools(
4024
- userPrompt,
4025
- externalTools,
4026
- apiKey,
4027
- logCollector
4028
- );
4029
- if (toolExecution.hasResults) {
4030
- const toolResultsText = toolExecution.toolResults.map((tr) => {
4031
- if (tr.error) {
4032
- return `**${tr.toolName}** (Failed): ${tr.error}`;
3792
+ logger.info(`[${this.getProviderName()}] External tools available: ${externalTools.map((t) => t.name).join(", ")}`);
3793
+ 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) => {
3794
+ const paramsText = Object.entries(tool.params || {}).map(([key, value]) => {
3795
+ const valueType = typeof value;
3796
+ if (valueType === "string" && ["string", "number", "integer", "boolean", "array", "object"].includes(String(value).toLowerCase())) {
3797
+ return `- ${key}: ${value}`;
3798
+ } else {
3799
+ return `- ${key}: ${JSON.stringify(value)} (default value - use this)`;
4033
3800
  }
4034
- return `**${tr.toolName}** (Success):
4035
- ${JSON.stringify(tr.result, null, 2)}`;
4036
- }).join("\n\n");
4037
- externalToolContext = `## External Tool Results
4038
-
4039
- ${toolExecution.summary}
4040
-
4041
- ${toolResultsText}`;
4042
- logger.info(`[${this.getProviderName()}] External tools executed, results available`);
4043
- } else {
4044
- logger.info(`[${this.getProviderName()}] No external tools were needed`);
4045
- }
3801
+ }).join("\n ");
3802
+ return `${idx + 1}. **${tool.name}** (ID: ${tool.id})
3803
+ Description: ${tool.description}
3804
+ **ACTION REQUIRED**: Call this tool with the parameters below
3805
+ Parameters:
3806
+ ${paramsText}`;
3807
+ }).join("\n\n");
4046
3808
  }
4047
3809
  const schemaDoc = schema.generateSchemaDocumentation();
4048
3810
  const knowledgeBaseContext = await knowledge_base_default.getKnowledgeBase({
@@ -4051,13 +3813,12 @@ ${toolResultsText}`;
4051
3813
  topK: 1
4052
3814
  });
4053
3815
  logger.file("\n=============================\nknowledge base context:", knowledgeBaseContext);
4054
- logger.file("\n=============================\nexternal tool context:", externalToolContext);
4055
3816
  const prompts = await promptLoader.loadPrompts("text-response", {
4056
3817
  USER_PROMPT: userPrompt,
4057
3818
  CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
4058
3819
  SCHEMA_DOC: schemaDoc,
4059
3820
  KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext || "No additional knowledge base context available.",
4060
- EXTERNAL_TOOL_CONTEXT: externalToolContext
3821
+ AVAILABLE_EXTERNAL_TOOLS: availableToolsDoc
4061
3822
  });
4062
3823
  logger.file("\n=============================\nsystem prompt:", prompts.system);
4063
3824
  logger.file("\n=============================\nuser prompt:", prompts.user);
@@ -4079,11 +3840,88 @@ ${toolResultsText}`;
4079
3840
  description: "Brief explanation of what this query does and why it answers the user's question."
4080
3841
  }
4081
3842
  },
4082
- required: ["query"]
3843
+ required: ["query"],
3844
+ additionalProperties: false
4083
3845
  }
4084
3846
  }];
3847
+ if (externalTools && externalTools.length > 0) {
3848
+ externalTools.forEach((tool) => {
3849
+ logger.info(`[${this.getProviderName()}] Processing external tool:`, JSON.stringify(tool, null, 2));
3850
+ const properties = {};
3851
+ const required = [];
3852
+ Object.entries(tool.params || {}).forEach(([key, typeOrValue]) => {
3853
+ let schemaType;
3854
+ let hasDefaultValue = false;
3855
+ let defaultValue;
3856
+ const valueType = typeof typeOrValue;
3857
+ if (valueType === "number") {
3858
+ schemaType = Number.isInteger(typeOrValue) ? "integer" : "number";
3859
+ hasDefaultValue = true;
3860
+ defaultValue = typeOrValue;
3861
+ } else if (valueType === "boolean") {
3862
+ schemaType = "boolean";
3863
+ hasDefaultValue = true;
3864
+ defaultValue = typeOrValue;
3865
+ } else if (Array.isArray(typeOrValue)) {
3866
+ schemaType = "array";
3867
+ hasDefaultValue = true;
3868
+ defaultValue = typeOrValue;
3869
+ } else if (valueType === "object" && typeOrValue !== null) {
3870
+ schemaType = "object";
3871
+ hasDefaultValue = true;
3872
+ defaultValue = typeOrValue;
3873
+ } else {
3874
+ const typeStr = String(typeOrValue).toLowerCase().trim();
3875
+ if (typeStr === "string" || typeStr === "str") {
3876
+ schemaType = "string";
3877
+ } else if (typeStr === "number" || typeStr === "num" || typeStr === "float" || typeStr === "double") {
3878
+ schemaType = "number";
3879
+ } else if (typeStr === "integer" || typeStr === "int") {
3880
+ schemaType = "integer";
3881
+ } else if (typeStr === "boolean" || typeStr === "bool") {
3882
+ schemaType = "boolean";
3883
+ } else if (typeStr === "array" || typeStr === "list") {
3884
+ schemaType = "array";
3885
+ } else if (typeStr === "object" || typeStr === "dict") {
3886
+ schemaType = "object";
3887
+ } else {
3888
+ schemaType = "string";
3889
+ hasDefaultValue = true;
3890
+ defaultValue = typeOrValue;
3891
+ }
3892
+ }
3893
+ const propertySchema = {
3894
+ type: schemaType,
3895
+ description: `${key} parameter for ${tool.name}`
3896
+ };
3897
+ if (hasDefaultValue) {
3898
+ propertySchema.default = defaultValue;
3899
+ } else {
3900
+ required.push(key);
3901
+ }
3902
+ properties[key] = propertySchema;
3903
+ });
3904
+ const inputSchema = {
3905
+ type: "object",
3906
+ properties,
3907
+ additionalProperties: false
3908
+ };
3909
+ if (required.length > 0) {
3910
+ inputSchema.required = required;
3911
+ }
3912
+ tools.push({
3913
+ name: tool.id,
3914
+ description: tool.description,
3915
+ input_schema: inputSchema
3916
+ });
3917
+ });
3918
+ logger.info(`[${this.getProviderName()}] Added ${externalTools.length} external tools to tool calling capability`);
3919
+ logger.info(`[${this.getProviderName()}] Complete tools array:`, JSON.stringify(tools, null, 2));
3920
+ }
4085
3921
  const queryAttempts = /* @__PURE__ */ new Map();
4086
3922
  const MAX_QUERY_ATTEMPTS = 6;
3923
+ const toolAttempts = /* @__PURE__ */ new Map();
3924
+ const MAX_TOOL_ATTEMPTS = 3;
4087
3925
  let maxAttemptsReached = false;
4088
3926
  let fullStreamedText = "";
4089
3927
  const wrappedStreamCallback = streamCallback ? (chunk) => {
@@ -4225,8 +4063,75 @@ ${errorMsg}
4225
4063
  }
4226
4064
  throw new Error(`Query execution failed: ${errorMsg}`);
4227
4065
  }
4066
+ } else {
4067
+ const externalTool = externalTools?.find((t) => t.id === toolName);
4068
+ if (externalTool) {
4069
+ const attempts = (toolAttempts.get(toolName) || 0) + 1;
4070
+ toolAttempts.set(toolName, attempts);
4071
+ logger.info(`[${this.getProviderName()}] Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})`);
4072
+ logCollector?.info(`Executing external tool: ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...`);
4073
+ if (attempts > MAX_TOOL_ATTEMPTS) {
4074
+ const errorMsg = `Maximum attempts (${MAX_TOOL_ATTEMPTS}) reached for tool: ${externalTool.name}`;
4075
+ logger.error(`[${this.getProviderName()}] ${errorMsg}`);
4076
+ logCollector?.error(errorMsg);
4077
+ if (wrappedStreamCallback) {
4078
+ wrappedStreamCallback(`
4079
+
4080
+ \u274C ${errorMsg}
4081
+
4082
+ Please try rephrasing your request or contact support.
4083
+
4084
+ `);
4085
+ }
4086
+ throw new Error(errorMsg);
4087
+ }
4088
+ try {
4089
+ if (wrappedStreamCallback) {
4090
+ if (attempts === 1) {
4091
+ wrappedStreamCallback(`
4092
+
4093
+ \u{1F517} **Executing ${externalTool.name}...**
4094
+
4095
+ `);
4096
+ } else {
4097
+ wrappedStreamCallback(`
4098
+
4099
+ \u{1F504} **Retrying ${externalTool.name} (attempt ${attempts}/${MAX_TOOL_ATTEMPTS})...**
4100
+
4101
+ `);
4102
+ }
4103
+ }
4104
+ const result2 = await externalTool.fn(toolInput);
4105
+ logger.info(`[${this.getProviderName()}] External tool ${externalTool.name} executed successfully`);
4106
+ logCollector?.info(`\u2713 ${externalTool.name} executed successfully`);
4107
+ if (wrappedStreamCallback) {
4108
+ wrappedStreamCallback(`\u2705 **${externalTool.name} completed successfully**
4109
+
4110
+ `);
4111
+ }
4112
+ return JSON.stringify(result2, null, 2);
4113
+ } catch (error) {
4114
+ const errorMsg = error instanceof Error ? error.message : String(error);
4115
+ logger.error(`[${this.getProviderName()}] External tool ${externalTool.name} failed (attempt ${attempts}/${MAX_TOOL_ATTEMPTS}): ${errorMsg}`);
4116
+ logCollector?.error(`\u2717 ${externalTool.name} failed: ${errorMsg}`);
4117
+ if (wrappedStreamCallback) {
4118
+ wrappedStreamCallback(`\u274C **${externalTool.name} failed:**
4119
+ \`\`\`
4120
+ ${errorMsg}
4121
+ \`\`\`
4122
+
4123
+ `);
4124
+ if (attempts < MAX_TOOL_ATTEMPTS) {
4125
+ wrappedStreamCallback(`\u{1F527} **Retrying with adjusted parameters...**
4126
+
4127
+ `);
4128
+ }
4129
+ }
4130
+ throw new Error(`Tool execution failed: ${errorMsg}`);
4131
+ }
4132
+ }
4133
+ throw new Error(`Unknown tool: ${toolName}`);
4228
4134
  }
4229
- throw new Error(`Unknown tool: ${toolName}`);
4230
4135
  };
4231
4136
  const result = await LLM.streamWithTools(
4232
4137
  {
@@ -4243,8 +4148,8 @@ ${errorMsg}
4243
4148
  partial: wrappedStreamCallback
4244
4149
  // Pass the wrapped streaming callback to LLM
4245
4150
  },
4246
- 10
4247
- // max iterations: allows for 6 retries + final response + buffer
4151
+ 20
4152
+ // max iterations: allows for 6 query retries + 3 tool retries + final response + buffer
4248
4153
  );
4249
4154
  logger.info(`[${this.getProviderName()}] Text response stream completed`);
4250
4155
  const textResponse = fullStreamedText || result || "I apologize, but I was unable to generate a response.";
@@ -4280,11 +4185,17 @@ ${errorMsg}
4280
4185
  let actions = [];
4281
4186
  if (components && components.length > 0) {
4282
4187
  logger.info(`[${this.getProviderName()}] Matching components from text response...`);
4283
- const matchResult = await this.matchComponentsFromTextResponse(
4188
+ const componentStreamCallback = wrappedStreamCallback ? (component) => {
4189
+ const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
4190
+ wrappedStreamCallback(answerMarker);
4191
+ logger.info(`[${this.getProviderName()}] Streamed answer component to frontend: ${component.name} (${component.type})`);
4192
+ } : void 0;
4193
+ const matchResult = await this.matchComponentsFromAnalysis(
4284
4194
  textResponse,
4285
4195
  components,
4286
4196
  apiKey,
4287
- logCollector
4197
+ logCollector,
4198
+ componentStreamCallback
4288
4199
  );
4289
4200
  matchedComponents = matchResult.components;
4290
4201
  layoutTitle = matchResult.layoutTitle;
@@ -4293,24 +4204,22 @@ ${errorMsg}
4293
4204
  }
4294
4205
  let container_componet = null;
4295
4206
  if (matchedComponents.length > 0) {
4207
+ logger.info(`[${this.getProviderName()}] Created MultiComponentContainer: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4208
+ logCollector?.info(`Created dashboard: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4296
4209
  container_componet = {
4297
- id: `multi_container_${Date.now()}`,
4210
+ id: `container_${Date.now()}`,
4298
4211
  name: "MultiComponentContainer",
4299
4212
  type: "Container",
4300
4213
  description: layoutDescription,
4301
- category: "dynamic",
4302
- keywords: ["dashboard", "layout", "container"],
4303
4214
  props: {
4304
4215
  config: {
4305
- components: matchedComponents,
4306
4216
  title: layoutTitle,
4307
- description: layoutDescription
4217
+ description: layoutDescription,
4218
+ components: matchedComponents
4308
4219
  },
4309
4220
  actions
4310
4221
  }
4311
4222
  };
4312
- logger.info(`[${this.getProviderName()}] Created MultiComponentContainer: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4313
- logCollector?.info(`Created dashboard: "${layoutTitle}" with ${matchedComponents.length} components and ${actions.length} actions`);
4314
4223
  }
4315
4224
  return {
4316
4225
  success: true,
@@ -4341,201 +4250,134 @@ ${errorMsg}
4341
4250
  }
4342
4251
  }
4343
4252
  /**
4344
- * Generate component response for user question
4345
- * This provides conversational component suggestions based on user question
4346
- * Supports component generation and matching
4253
+ * Main orchestration function with semantic search and multi-step classification
4254
+ * NEW FLOW (Recommended):
4255
+ * 1. Semantic search: Check previous conversations (>60% match)
4256
+ * - If match found → Adapt UI block parameters and return
4257
+ * 2. Category classification: Determine if data_analysis, requires_external_tools, or text_response
4258
+ * 3. Route appropriately based on category and response mode
4259
+ *
4260
+ * @param responseMode - 'component' for component generation (default), 'text' for text responses
4261
+ * @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
4262
+ * @param collections - Collection registry for executing database queries (required for text mode)
4263
+ * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
4347
4264
  */
4348
- async generateComponentResponse(userPrompt, components, apiKey, logCollector, conversationHistory) {
4349
- const errors = [];
4265
+ async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
4266
+ const startTime = Date.now();
4267
+ logger.info(`[${this.getProviderName()}] handleUserRequest called with responseMode: ${responseMode}`);
4268
+ logCollector?.info(`Starting request processing with mode: ${responseMode}`);
4350
4269
  try {
4351
- logger.info(`[${this.getProviderName()}] Using component response mode`);
4352
- const classifyMsg = "Classifying user question...";
4353
- logCollector?.info(classifyMsg);
4354
- const classification = await this.classifyUserQuestion(userPrompt, apiKey, logCollector, conversationHistory);
4355
- const classInfo = `Question type: ${classification.questionType}, Visualizations: ${classification.visualizations.join(", ") || "None"}, Multiple components: ${classification.needsMultipleComponents}`;
4356
- logCollector?.info(classInfo);
4357
- if (classification.questionType === "analytical") {
4358
- if (classification.visualizations.length > 1) {
4359
- const multiMsg = `Matching ${classification.visualizations.length} components for types: ${classification.visualizations.join(", ")}`;
4360
- logCollector?.info(multiMsg);
4361
- const componentPromises = classification.visualizations.map((vizType) => {
4362
- logCollector?.info(`Matching component for type: ${vizType}`);
4363
- return this.generateAnalyticalComponent(
4364
- userPrompt,
4365
- components,
4366
- vizType,
4367
- apiKey,
4368
- logCollector,
4369
- conversationHistory
4370
- ).then((result) => ({ vizType, result }));
4371
- });
4372
- const settledResults = await Promise.allSettled(componentPromises);
4373
- const matchedComponents = [];
4374
- for (const settledResult of settledResults) {
4375
- if (settledResult.status === "fulfilled") {
4376
- const { vizType, result } = settledResult.value;
4377
- if (result.component) {
4378
- matchedComponents.push(result.component);
4379
- logCollector?.info(`Matched: ${result.component.name}`);
4380
- logger.info("Component : ", result.component.name, " props: ", result.component.props);
4381
- } else {
4382
- logCollector?.warn(`Failed to match component for type: ${vizType}`);
4383
- }
4384
- } else {
4385
- logCollector?.warn(`Error matching component: ${settledResult.reason?.message || "Unknown error"}`);
4386
- }
4387
- }
4388
- logger.debug(`[${this.getProviderName()}] Matched ${matchedComponents.length} components for multi-component container`);
4389
- if (matchedComponents.length === 0) {
4390
- return {
4391
- success: true,
4392
- data: {
4393
- component: null,
4394
- reasoning: "Failed to match any components for the requested visualization types",
4395
- method: "classification-multi-failed",
4396
- questionType: classification.questionType,
4397
- needsMultipleComponents: true,
4398
- propsModified: false,
4399
- queryModified: false
4400
- },
4401
- errors: []
4402
- };
4403
- }
4404
- logCollector?.info("Generating container metadata...");
4405
- const containerMetadata = await this.generateContainerMetadata(
4406
- userPrompt,
4407
- classification.visualizations,
4408
- apiKey,
4409
- logCollector,
4410
- conversationHistory
4411
- );
4412
- const containerComponent = {
4413
- id: `multi_container_${Date.now()}`,
4414
- name: "MultiComponentContainer",
4415
- type: "Container",
4416
- description: containerMetadata.description,
4417
- category: "dynamic",
4418
- keywords: ["multi", "container", "dashboard"],
4419
- props: {
4420
- config: {
4421
- components: matchedComponents,
4422
- layout: "grid",
4423
- spacing: 24,
4424
- title: containerMetadata.title,
4425
- description: containerMetadata.description
4426
- }
4427
- }
4428
- };
4429
- logCollector?.info(`Created multi-component container with ${matchedComponents.length} components: "${containerMetadata.title}"`);
4270
+ logger.info(`[${this.getProviderName()}] Step 1: Searching previous conversations...`);
4271
+ logCollector?.info("Step 1: Searching for similar previous conversations...");
4272
+ const conversationMatch = await conversation_search_default.searchConversations({
4273
+ userPrompt,
4274
+ collections,
4275
+ userId,
4276
+ similarityThreshold: 0.6
4277
+ // 60% threshold
4278
+ });
4279
+ logger.info("conversationMatch:", conversationMatch);
4280
+ if (conversationMatch) {
4281
+ logger.info(
4282
+ `[${this.getProviderName()}] \u2713 Found matching conversation with ${(conversationMatch.similarity * 100).toFixed(2)}% similarity`
4283
+ );
4284
+ logCollector?.info(
4285
+ `\u2713 Found similar conversation (${(conversationMatch.similarity * 100).toFixed(2)}% match)`
4286
+ );
4287
+ if (conversationMatch.similarity >= 0.99) {
4288
+ const elapsedTime2 = Date.now() - startTime;
4289
+ logger.info(`[${this.getProviderName()}] \u2713 100% match - returning UI block directly without adaptation`);
4290
+ logCollector?.info(`\u2713 Exact match (${(conversationMatch.similarity * 100).toFixed(2)}%) - returning cached result`);
4291
+ logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4292
+ const component = conversationMatch.uiBlock?.generatedComponentMetadata || conversationMatch.uiBlock?.component;
4430
4293
  return {
4431
4294
  success: true,
4432
4295
  data: {
4433
- component: containerComponent,
4434
- reasoning: `Matched ${matchedComponents.length} components for visualization types: ${classification.visualizations.join(", ")}`,
4435
- method: "classification-multi-generated",
4436
- questionType: classification.questionType,
4437
- needsMultipleComponents: true,
4438
- propsModified: false,
4439
- queryModified: false
4296
+ component,
4297
+ reasoning: `Exact match from previous conversation (${(conversationMatch.similarity * 100).toFixed(2)}% similarity)`,
4298
+ method: `${this.getProviderName()}-semantic-match-exact`,
4299
+ semanticSimilarity: conversationMatch.similarity
4440
4300
  },
4441
4301
  errors: []
4442
4302
  };
4443
- } else if (classification.visualizations.length === 1) {
4444
- const vizType = classification.visualizations[0];
4445
- logCollector?.info(`Matching single component for type: ${vizType}`);
4446
- const result = await this.generateAnalyticalComponent(userPrompt, components, vizType, apiKey, logCollector, conversationHistory);
4303
+ }
4304
+ logCollector?.info(`Adapting parameters for similar question...`);
4305
+ const originalPrompt = conversationMatch.metadata?.userPrompt || "Previous question";
4306
+ const adaptResult = await this.adaptUIBlockParameters(
4307
+ userPrompt,
4308
+ originalPrompt,
4309
+ conversationMatch.uiBlock,
4310
+ apiKey,
4311
+ logCollector
4312
+ );
4313
+ if (adaptResult.success && adaptResult.adaptedComponent) {
4314
+ const elapsedTime2 = Date.now() - startTime;
4315
+ logger.info(`[${this.getProviderName()}] \u2713 Successfully adapted UI block parameters`);
4316
+ logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4317
+ logCollector?.info(`\u2713 UI block adapted successfully`);
4318
+ logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4447
4319
  return {
4448
4320
  success: true,
4449
4321
  data: {
4450
- component: result.component,
4451
- reasoning: result.reasoning,
4452
- method: "classification-generated",
4453
- questionType: classification.questionType,
4454
- needsMultipleComponents: false,
4455
- propsModified: false,
4456
- queryModified: false
4322
+ component: adaptResult.adaptedComponent,
4323
+ reasoning: `Adapted from previous conversation: ${originalPrompt}`,
4324
+ method: `${this.getProviderName()}-semantic-match`,
4325
+ semanticSimilarity: conversationMatch.similarity,
4326
+ parametersChanged: adaptResult.parametersChanged
4457
4327
  },
4458
4328
  errors: []
4459
4329
  };
4460
4330
  } else {
4461
- logCollector?.info("No specific visualization type - matching from all components");
4462
- const result = await this.generateAnalyticalComponent(userPrompt, components, void 0, apiKey, logCollector, conversationHistory);
4463
- return {
4464
- success: true,
4465
- data: {
4466
- component: result.component,
4467
- reasoning: result.reasoning,
4468
- method: "classification-generated-auto",
4469
- questionType: classification.questionType,
4470
- needsMultipleComponents: false,
4471
- propsModified: false,
4472
- queryModified: false
4473
- },
4474
- errors: []
4475
- };
4331
+ logger.info(`[${this.getProviderName()}] Could not adapt matched conversation, continuing to category classification`);
4332
+ logCollector?.warn(`Could not adapt matched conversation: ${adaptResult.explanation}`);
4476
4333
  }
4477
- } else if (classification.questionType === "data_modification" || classification.questionType === "general") {
4478
- const matchMsg = "Using component matching for data modification...";
4479
- logCollector?.info(matchMsg);
4480
- const matchResult = await this.matchComponent(userPrompt, components, apiKey, logCollector, conversationHistory);
4481
- return {
4482
- success: true,
4483
- data: {
4484
- component: matchResult.component,
4485
- reasoning: matchResult.reasoning,
4486
- method: "classification-matched",
4487
- questionType: classification.questionType,
4488
- needsMultipleComponents: false,
4489
- propsModified: matchResult.propsModified,
4490
- queryModified: matchResult.queryModified
4491
- },
4492
- errors: []
4493
- };
4494
4334
  } else {
4495
- logCollector?.info("General question - no component needed");
4496
- return {
4497
- success: true,
4498
- data: {
4499
- component: null,
4500
- reasoning: "General question - no component needed",
4501
- method: "classification-general",
4502
- questionType: classification.questionType,
4503
- needsMultipleComponents: false,
4504
- propsModified: false,
4505
- queryModified: false
4506
- },
4507
- errors: []
4508
- };
4335
+ logger.info(`[${this.getProviderName()}] No matching previous conversations found, proceeding to category classification`);
4336
+ logCollector?.info("No similar previous conversations found. Proceeding to category classification...");
4337
+ }
4338
+ logger.info(`[${this.getProviderName()}] Step 2: Classifying question category...`);
4339
+ logCollector?.info("Step 2: Classifying question category...");
4340
+ const categoryClassification = await this.classifyQuestionCategory(
4341
+ userPrompt,
4342
+ apiKey,
4343
+ logCollector,
4344
+ conversationHistory,
4345
+ externalTools
4346
+ );
4347
+ logger.info(
4348
+ `[${this.getProviderName()}] Question classified as: ${categoryClassification.category} (confidence: ${categoryClassification.confidence}%)`
4349
+ );
4350
+ logCollector?.info(
4351
+ `Category: ${categoryClassification.category} | Confidence: ${categoryClassification.confidence}%`
4352
+ );
4353
+ let toolsToUse = [];
4354
+ if (categoryClassification.externalTools && categoryClassification.externalTools.length > 0) {
4355
+ logger.info(`[${this.getProviderName()}] Identified ${categoryClassification.externalTools.length} external tools needed`);
4356
+ logCollector?.info(`Identified external tools: ${categoryClassification.externalTools.map((t) => t.name || t.type).join(", ")}`);
4357
+ toolsToUse = categoryClassification.externalTools?.map((t) => ({
4358
+ id: t.type,
4359
+ name: t.name,
4360
+ description: t.description,
4361
+ params: t.parameters || {},
4362
+ fn: (() => {
4363
+ const realTool = externalTools?.find((tool) => tool.id === t.type);
4364
+ if (realTool) {
4365
+ logger.info(`[${this.getProviderName()}] Using real tool implementation for ${t.type}`);
4366
+ return realTool.fn;
4367
+ } else {
4368
+ logger.warn(`[${this.getProviderName()}] Tool ${t.type} not found in registered tools`);
4369
+ return async () => ({ success: false, message: `Tool ${t.name || t.type} not registered` });
4370
+ }
4371
+ })()
4372
+ })) || [];
4373
+ }
4374
+ if (categoryClassification.category === "data_analysis") {
4375
+ logger.info(`[${this.getProviderName()}] Routing to data analysis (SELECT operations)`);
4376
+ logCollector?.info("Routing to data analysis...");
4377
+ } else if (categoryClassification.category === "data_modification") {
4378
+ logger.info(`[${this.getProviderName()}] Routing to data modification (INSERT/UPDATE/DELETE operations)`);
4379
+ logCollector?.info("Routing to data modification...");
4509
4380
  }
4510
- } catch (error) {
4511
- const errorMsg = error instanceof Error ? error.message : String(error);
4512
- logger.error(`[${this.getProviderName()}] Error generating component response: ${errorMsg}`);
4513
- logger.debug(`[${this.getProviderName()}] Component response generation error details:`, error);
4514
- logCollector?.error(`Error generating component response: ${errorMsg}`);
4515
- errors.push(errorMsg);
4516
- return {
4517
- success: false,
4518
- errors,
4519
- data: void 0
4520
- };
4521
- }
4522
- }
4523
- /**
4524
- * Main orchestration function that classifies question and routes to appropriate handler
4525
- * This is the NEW recommended entry point for handling user requests
4526
- * Supports both component generation and text response modes
4527
- *
4528
- * @param responseMode - 'component' for component generation (default), 'text' for text responses
4529
- * @param streamCallback - Optional callback function to receive text chunks as they stream (only for text mode)
4530
- * @param collections - Collection registry for executing database queries (required for text mode)
4531
- * @param externalTools - Optional array of external tools (email, calendar, etc.) that can be called (only for text mode)
4532
- */
4533
- async handleUserRequest(userPrompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) {
4534
- const startTime = Date.now();
4535
- logger.info(`[${this.getProviderName()}] handleUserRequest called with responseMode: ${responseMode}`);
4536
- if (responseMode === "text") {
4537
- logger.info(`[${this.getProviderName()}] Using text response mode`);
4538
- logCollector?.info("Generating text response...");
4539
4381
  const textResponse = await this.generateTextResponse(
4540
4382
  userPrompt,
4541
4383
  apiKey,
@@ -4544,40 +4386,29 @@ ${errorMsg}
4544
4386
  streamCallback,
4545
4387
  collections,
4546
4388
  components,
4547
- externalTools
4389
+ toolsToUse
4548
4390
  );
4549
- if (!textResponse.success) {
4550
- const elapsedTime3 = Date.now() - startTime;
4551
- logger.error(`[${this.getProviderName()}] Text response generation failed`);
4552
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime3}ms (${(elapsedTime3 / 1e3).toFixed(2)}s)`);
4553
- logCollector?.info(`Total time taken: ${elapsedTime3}ms (${(elapsedTime3 / 1e3).toFixed(2)}s)`);
4554
- return textResponse;
4555
- }
4556
- const elapsedTime2 = Date.now() - startTime;
4557
- logger.info(`[${this.getProviderName()}] Text response generated successfully`);
4558
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4559
- logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4391
+ const elapsedTime = Date.now() - startTime;
4392
+ logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4393
+ logCollector?.info(`Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4560
4394
  return textResponse;
4395
+ } catch (error) {
4396
+ const errorMsg = error instanceof Error ? error.message : String(error);
4397
+ logger.error(`[${this.getProviderName()}] Error in handleUserRequest: ${errorMsg}`);
4398
+ logger.debug(`[${this.getProviderName()}] Error details:`, error);
4399
+ logCollector?.error(`Error processing request: ${errorMsg}`);
4400
+ const elapsedTime = Date.now() - startTime;
4401
+ logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4402
+ logCollector?.info(`Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4403
+ return {
4404
+ success: false,
4405
+ errors: [errorMsg],
4406
+ data: {
4407
+ text: "I apologize, but I encountered an error processing your request. Please try again.",
4408
+ method: `${this.getProviderName()}-orchestration-error`
4409
+ }
4410
+ };
4561
4411
  }
4562
- const componentResponse = await this.generateComponentResponse(
4563
- userPrompt,
4564
- components,
4565
- apiKey,
4566
- logCollector,
4567
- conversationHistory
4568
- );
4569
- if (!componentResponse.success) {
4570
- const elapsedTime2 = Date.now() - startTime;
4571
- logger.error(`[${this.getProviderName()}] Component response generation failed`);
4572
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4573
- logCollector?.info(`Total time taken: ${elapsedTime2}ms (${(elapsedTime2 / 1e3).toFixed(2)}s)`);
4574
- return componentResponse;
4575
- }
4576
- const elapsedTime = Date.now() - startTime;
4577
- logger.info(`[${this.getProviderName()}] Component response generated successfully`);
4578
- logger.info(`[${this.getProviderName()}] Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4579
- logCollector?.info(`Total time taken: ${elapsedTime}ms (${(elapsedTime / 1e3).toFixed(2)}s)`);
4580
- return componentResponse;
4581
4412
  }
4582
4413
  /**
4583
4414
  * Generate next questions that the user might ask based on the original prompt and generated component
@@ -4690,7 +4521,7 @@ function getLLMProviders() {
4690
4521
  return DEFAULT_PROVIDERS;
4691
4522
  }
4692
4523
  }
4693
- var useAnthropicMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) => {
4524
+ var useAnthropicMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools, userId) => {
4694
4525
  logger.debug("[useAnthropicMethod] Initializing Anthropic Claude matching method");
4695
4526
  logger.debug(`[useAnthropicMethod] Response mode: ${responseMode}`);
4696
4527
  const msg = `Using Anthropic Claude ${responseMode === "text" ? "text response" : "matching"} method...`;
@@ -4702,11 +4533,11 @@ var useAnthropicMethod = async (prompt, components, apiKey, logCollector, conver
4702
4533
  return { success: false, errors: [emptyMsg] };
4703
4534
  }
4704
4535
  logger.debug(`[useAnthropicMethod] Processing with ${components.length} components`);
4705
- const matchResult = await anthropicLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4536
+ const matchResult = await anthropicLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4706
4537
  logger.info(`[useAnthropicMethod] Successfully generated ${responseMode} using Anthropic`);
4707
4538
  return matchResult;
4708
4539
  };
4709
- var useGroqMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) => {
4540
+ var useGroqMethod = async (prompt, components, apiKey, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools, userId) => {
4710
4541
  logger.debug("[useGroqMethod] Initializing Groq LLM matching method");
4711
4542
  logger.debug(`[useGroqMethod] Response mode: ${responseMode}`);
4712
4543
  const msg = `Using Groq LLM ${responseMode === "text" ? "text response" : "matching"} method...`;
@@ -4719,14 +4550,14 @@ var useGroqMethod = async (prompt, components, apiKey, logCollector, conversatio
4719
4550
  return { success: false, errors: [emptyMsg] };
4720
4551
  }
4721
4552
  logger.debug(`[useGroqMethod] Processing with ${components.length} components`);
4722
- const matchResult = await groqLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4553
+ const matchResult = await groqLLM.handleUserRequest(prompt, components, apiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4723
4554
  logger.info(`[useGroqMethod] Successfully generated ${responseMode} using Groq`);
4724
4555
  return matchResult;
4725
4556
  };
4726
4557
  var getUserResponseFromCache = async (prompt) => {
4727
4558
  return false;
4728
4559
  };
4729
- var get_user_response = async (prompt, components, anthropicApiKey, groqApiKey, llmProviders, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools) => {
4560
+ var get_user_response = async (prompt, components, anthropicApiKey, groqApiKey, llmProviders, logCollector, conversationHistory, responseMode = "component", streamCallback, collections, externalTools, userId) => {
4730
4561
  logger.debug(`[get_user_response] Starting user response generation for prompt: "${prompt.substring(0, 50)}..."`);
4731
4562
  logger.debug(`[get_user_response] Response mode: ${responseMode}`);
4732
4563
  logger.debug("[get_user_response] Checking cache for existing response");
@@ -4759,9 +4590,9 @@ var get_user_response = async (prompt, components, anthropicApiKey, groqApiKey,
4759
4590
  logCollector?.info(attemptMsg);
4760
4591
  let result;
4761
4592
  if (provider === "anthropic") {
4762
- result = await useAnthropicMethod(prompt, components, anthropicApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4593
+ result = await useAnthropicMethod(prompt, components, anthropicApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4763
4594
  } else if (provider === "groq") {
4764
- result = await useGroqMethod(prompt, components, groqApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools);
4595
+ result = await useGroqMethod(prompt, components, groqApiKey, logCollector, conversationHistory, responseMode, streamCallback, collections, externalTools, userId);
4765
4596
  } else {
4766
4597
  logger.warn(`[get_user_response] Unknown provider: ${provider} - skipping`);
4767
4598
  errors.push(`Unknown provider: ${provider}`);
@@ -4978,6 +4809,111 @@ var UILogCollector = class {
4978
4809
  }
4979
4810
  };
4980
4811
 
4812
+ // src/utils/conversation-saver.ts
4813
+ async function saveConversation(params) {
4814
+ const { userId, userPrompt, uiblock, uiBlockId, threadId, collections } = params;
4815
+ if (!userId) {
4816
+ logger.warn("[CONVERSATION_SAVER] Skipping save: userId not provided");
4817
+ return {
4818
+ success: false,
4819
+ error: "userId is required"
4820
+ };
4821
+ }
4822
+ if (!userPrompt) {
4823
+ logger.warn("[CONVERSATION_SAVER] Skipping save: userPrompt not provided");
4824
+ return {
4825
+ success: false,
4826
+ error: "userPrompt is required"
4827
+ };
4828
+ }
4829
+ if (!uiblock) {
4830
+ logger.warn("[CONVERSATION_SAVER] Skipping save: uiblock not provided");
4831
+ return {
4832
+ success: false,
4833
+ error: "uiblock is required"
4834
+ };
4835
+ }
4836
+ if (!threadId) {
4837
+ logger.warn("[CONVERSATION_SAVER] Skipping save: threadId not provided");
4838
+ return {
4839
+ success: false,
4840
+ error: "threadId is required"
4841
+ };
4842
+ }
4843
+ if (!uiBlockId) {
4844
+ logger.warn("[CONVERSATION_SAVER] Skipping save: uiBlockId not provided");
4845
+ return {
4846
+ success: false,
4847
+ error: "uiBlockId is required"
4848
+ };
4849
+ }
4850
+ if (!collections?.["user-conversations"]?.["create"]) {
4851
+ logger.debug('[CONVERSATION_SAVER] Collection "user-conversations.create" not available, skipping save');
4852
+ return {
4853
+ success: false,
4854
+ error: "user-conversations.create collection not available"
4855
+ };
4856
+ }
4857
+ try {
4858
+ logger.info(`[CONVERSATION_SAVER] Saving conversation for userId: ${userId}, uiBlockId: ${uiBlockId}, threadId: ${threadId}`);
4859
+ const userIdNumber = Number(userId);
4860
+ if (isNaN(userIdNumber)) {
4861
+ logger.warn(`[CONVERSATION_SAVER] Invalid userId: ${userId} (not a valid number)`);
4862
+ return {
4863
+ success: false,
4864
+ error: `Invalid userId: ${userId} (not a valid number)`
4865
+ };
4866
+ }
4867
+ const saveResult = await collections["user-conversations"]["create"]({
4868
+ userId: userIdNumber,
4869
+ userPrompt,
4870
+ uiblock,
4871
+ threadId
4872
+ });
4873
+ if (!saveResult?.success) {
4874
+ logger.warn(`[CONVERSATION_SAVER] Failed to save conversation to PostgreSQL: ${saveResult?.message || "Unknown error"}`);
4875
+ return {
4876
+ success: false,
4877
+ error: saveResult?.message || "Unknown error from backend"
4878
+ };
4879
+ }
4880
+ logger.info(`[CONVERSATION_SAVER] Successfully saved conversation to PostgreSQL, id: ${saveResult.data?.id}`);
4881
+ if (collections?.["conversation-history"]?.["embed"]) {
4882
+ try {
4883
+ logger.info("[CONVERSATION_SAVER] Creating embedding for semantic search...");
4884
+ const embedResult = await collections["conversation-history"]["embed"]({
4885
+ uiBlockId,
4886
+ userPrompt,
4887
+ uiBlock: uiblock,
4888
+ userId: userIdNumber
4889
+ });
4890
+ if (embedResult?.success) {
4891
+ logger.info("[CONVERSATION_SAVER] Successfully created embedding");
4892
+ } else {
4893
+ logger.warn("[CONVERSATION_SAVER] Failed to create embedding:", embedResult?.error || "Unknown error");
4894
+ }
4895
+ } catch (embedError) {
4896
+ const embedErrorMsg = embedError instanceof Error ? embedError.message : String(embedError);
4897
+ logger.warn("[CONVERSATION_SAVER] Error creating embedding:", embedErrorMsg);
4898
+ }
4899
+ } else {
4900
+ logger.debug("[CONVERSATION_SAVER] Embedding collection not available, skipping ChromaDB storage");
4901
+ }
4902
+ return {
4903
+ success: true,
4904
+ conversationId: saveResult.data?.id,
4905
+ message: "Conversation saved successfully"
4906
+ };
4907
+ } catch (error) {
4908
+ const errorMessage = error instanceof Error ? error.message : String(error);
4909
+ logger.error("[CONVERSATION_SAVER] Error saving conversation:", errorMessage);
4910
+ return {
4911
+ success: false,
4912
+ error: errorMessage
4913
+ };
4914
+ }
4915
+ }
4916
+
4981
4917
  // src/config/context.ts
4982
4918
  var CONTEXT_CONFIG = {
4983
4919
  /**
@@ -4989,7 +4925,7 @@ var CONTEXT_CONFIG = {
4989
4925
  };
4990
4926
 
4991
4927
  // src/handlers/user-prompt-request.ts
4992
- var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools) => {
4928
+ var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools, userId) => {
4993
4929
  const errors = [];
4994
4930
  logger.debug("[USER_PROMPT_REQ] Parsing incoming message data");
4995
4931
  const parseResult = UserPromptRequestMessageSchema.safeParse(data);
@@ -5070,7 +5006,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
5070
5006
  responseMode,
5071
5007
  streamCallback,
5072
5008
  collections,
5073
- externalTools
5009
+ externalTools,
5010
+ userId
5074
5011
  );
5075
5012
  logCollector.info("User prompt request completed");
5076
5013
  const uiBlockId = existingUiBlockId;
@@ -5121,6 +5058,34 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
5121
5058
  }
5122
5059
  thread.addUIBlock(uiBlock);
5123
5060
  logger.info(`Created UIBlock: ${uiBlockId} in Thread: ${threadId}`);
5061
+ if (userId) {
5062
+ const responseMethod = userResponse.data?.method || "";
5063
+ const semanticSimilarity = userResponse.data?.semanticSimilarity || 0;
5064
+ const isExactMatch = responseMethod.includes("semantic-match") && semanticSimilarity >= 0.99;
5065
+ if (isExactMatch) {
5066
+ logger.info(
5067
+ `Skipping conversation save - response from exact semantic match (${(semanticSimilarity * 100).toFixed(2)}% similarity)`
5068
+ );
5069
+ logCollector.info(
5070
+ `Using exact cached result (${(semanticSimilarity * 100).toFixed(2)}% match) - not saving duplicate conversation`
5071
+ );
5072
+ } else {
5073
+ const uiBlockData = uiBlock.toJSON();
5074
+ const saveResult = await saveConversation({
5075
+ userId,
5076
+ userPrompt: prompt,
5077
+ uiblock: uiBlockData,
5078
+ uiBlockId,
5079
+ threadId,
5080
+ collections
5081
+ });
5082
+ if (saveResult.success) {
5083
+ logger.info(`Conversation saved with ID: ${saveResult.conversationId}`);
5084
+ } else {
5085
+ logger.warn(`Failed to save conversation: ${saveResult.error}`);
5086
+ }
5087
+ }
5088
+ }
5124
5089
  return {
5125
5090
  success: userResponse.success,
5126
5091
  data: userResponse.data,
@@ -5131,8 +5096,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
5131
5096
  wsId
5132
5097
  };
5133
5098
  };
5134
- async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools) {
5135
- const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools);
5099
+ async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools, userId) {
5100
+ const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, llmProviders, collections, externalTools, userId);
5136
5101
  sendDataResponse4(
5137
5102
  response.id || data.id,
5138
5103
  {
@@ -5159,7 +5124,6 @@ function sendDataResponse4(id, res, sendMessage, clientId) {
5159
5124
  ...res
5160
5125
  }
5161
5126
  };
5162
- logger.info("sending user prompt response", response);
5163
5127
  sendMessage(response);
5164
5128
  }
5165
5129
 
@@ -7363,7 +7327,7 @@ var SuperatomSDK = class {
7363
7327
  });
7364
7328
  break;
7365
7329
  case "USER_PROMPT_REQ":
7366
- handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.llmProviders, this.collections, this.tools).catch((error) => {
7330
+ handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.llmProviders, this.collections, this.tools, this.userId).catch((error) => {
7367
7331
  logger.error("Failed to handle user prompt request:", error);
7368
7332
  });
7369
7333
  break;