@memberjunction/query-gen 0.0.1 → 2.126.1

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.
Files changed (138) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +34 -0
  3. package/COORDINATOR.md +768 -0
  4. package/IMPLEMENTATION_PLAN.md +1753 -0
  5. package/LLM_ENTITY_GROUPING_PLAN.md +977 -0
  6. package/README.md +675 -29
  7. package/dist/cli/commands/export.d.ts +15 -0
  8. package/dist/cli/commands/export.d.ts.map +1 -0
  9. package/dist/cli/commands/export.js +178 -0
  10. package/dist/cli/commands/export.js.map +1 -0
  11. package/dist/cli/commands/generate.d.ts +19 -0
  12. package/dist/cli/commands/generate.d.ts.map +1 -0
  13. package/dist/cli/commands/generate.js +282 -0
  14. package/dist/cli/commands/generate.js.map +1 -0
  15. package/dist/cli/commands/validate.d.ts +17 -0
  16. package/dist/cli/commands/validate.d.ts.map +1 -0
  17. package/dist/cli/commands/validate.js +193 -0
  18. package/dist/cli/commands/validate.js.map +1 -0
  19. package/dist/cli/config.d.ts +51 -0
  20. package/dist/cli/config.d.ts.map +1 -0
  21. package/dist/cli/config.js +142 -0
  22. package/dist/cli/config.js.map +1 -0
  23. package/dist/cli/index.d.ts +13 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +57 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/core/EntityGrouper.d.ts +74 -0
  28. package/dist/core/EntityGrouper.d.ts.map +1 -0
  29. package/dist/core/EntityGrouper.js +246 -0
  30. package/dist/core/EntityGrouper.js.map +1 -0
  31. package/dist/core/MetadataExporter.d.ts +59 -0
  32. package/dist/core/MetadataExporter.d.ts.map +1 -0
  33. package/dist/core/MetadataExporter.js +151 -0
  34. package/dist/core/MetadataExporter.js.map +1 -0
  35. package/dist/core/QueryDatabaseWriter.d.ts +50 -0
  36. package/dist/core/QueryDatabaseWriter.d.ts.map +1 -0
  37. package/dist/core/QueryDatabaseWriter.js +152 -0
  38. package/dist/core/QueryDatabaseWriter.js.map +1 -0
  39. package/dist/core/QueryFixer.d.ts +48 -0
  40. package/dist/core/QueryFixer.d.ts.map +1 -0
  41. package/dist/core/QueryFixer.js +115 -0
  42. package/dist/core/QueryFixer.js.map +1 -0
  43. package/dist/core/QueryRefiner.d.ts +94 -0
  44. package/dist/core/QueryRefiner.d.ts.map +1 -0
  45. package/dist/core/QueryRefiner.js +267 -0
  46. package/dist/core/QueryRefiner.js.map +1 -0
  47. package/dist/core/QueryTester.d.ts +70 -0
  48. package/dist/core/QueryTester.d.ts.map +1 -0
  49. package/dist/core/QueryTester.js +243 -0
  50. package/dist/core/QueryTester.js.map +1 -0
  51. package/dist/core/QueryWriter.d.ts +57 -0
  52. package/dist/core/QueryWriter.d.ts.map +1 -0
  53. package/dist/core/QueryWriter.js +184 -0
  54. package/dist/core/QueryWriter.js.map +1 -0
  55. package/dist/core/QuestionGenerator.d.ts +58 -0
  56. package/dist/core/QuestionGenerator.d.ts.map +1 -0
  57. package/dist/core/QuestionGenerator.js +145 -0
  58. package/dist/core/QuestionGenerator.js.map +1 -0
  59. package/dist/data/schema.d.ts +230 -0
  60. package/dist/data/schema.d.ts.map +1 -0
  61. package/dist/data/schema.js +6 -0
  62. package/dist/data/schema.js.map +1 -0
  63. package/dist/index.d.ts +28 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +77 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/prompts/PromptNames.d.ts +32 -0
  68. package/dist/prompts/PromptNames.d.ts.map +1 -0
  69. package/dist/prompts/PromptNames.js +35 -0
  70. package/dist/prompts/PromptNames.js.map +1 -0
  71. package/dist/utils/category-builder.d.ts +28 -0
  72. package/dist/utils/category-builder.d.ts.map +1 -0
  73. package/dist/utils/category-builder.js +90 -0
  74. package/dist/utils/category-builder.js.map +1 -0
  75. package/dist/utils/entity-helpers.d.ts +49 -0
  76. package/dist/utils/entity-helpers.d.ts.map +1 -0
  77. package/dist/utils/entity-helpers.js +189 -0
  78. package/dist/utils/entity-helpers.js.map +1 -0
  79. package/dist/utils/error-handlers.d.ts +19 -0
  80. package/dist/utils/error-handlers.d.ts.map +1 -0
  81. package/dist/utils/error-handlers.js +41 -0
  82. package/dist/utils/error-handlers.js.map +1 -0
  83. package/dist/utils/graph-helpers.d.ts +51 -0
  84. package/dist/utils/graph-helpers.d.ts.map +1 -0
  85. package/dist/utils/graph-helpers.js +82 -0
  86. package/dist/utils/graph-helpers.js.map +1 -0
  87. package/dist/utils/prompt-helpers.d.ts +25 -0
  88. package/dist/utils/prompt-helpers.d.ts.map +1 -0
  89. package/dist/utils/prompt-helpers.js +66 -0
  90. package/dist/utils/prompt-helpers.js.map +1 -0
  91. package/dist/utils/query-helpers.d.ts +23 -0
  92. package/dist/utils/query-helpers.d.ts.map +1 -0
  93. package/dist/utils/query-helpers.js +34 -0
  94. package/dist/utils/query-helpers.js.map +1 -0
  95. package/dist/utils/user-helpers.d.ts +15 -0
  96. package/dist/utils/user-helpers.d.ts.map +1 -0
  97. package/dist/utils/user-helpers.js +32 -0
  98. package/dist/utils/user-helpers.js.map +1 -0
  99. package/dist/vectors/EmbeddingService.d.ts +58 -0
  100. package/dist/vectors/EmbeddingService.d.ts.map +1 -0
  101. package/dist/vectors/EmbeddingService.js +90 -0
  102. package/dist/vectors/EmbeddingService.js.map +1 -0
  103. package/dist/vectors/SimilaritySearch.d.ts +51 -0
  104. package/dist/vectors/SimilaritySearch.d.ts.map +1 -0
  105. package/dist/vectors/SimilaritySearch.js +85 -0
  106. package/dist/vectors/SimilaritySearch.js.map +1 -0
  107. package/docs/API.md +1040 -0
  108. package/docs/ARCHITECTURE.md +1120 -0
  109. package/examples/advanced-usage.ts +401 -0
  110. package/examples/basic-usage.ts +285 -0
  111. package/package.json +48 -6
  112. package/src/cli/commands/export.ts +173 -0
  113. package/src/cli/commands/generate.ts +330 -0
  114. package/src/cli/commands/validate.ts +185 -0
  115. package/src/cli/config.ts +203 -0
  116. package/src/cli/index.ts +63 -0
  117. package/src/core/EntityGrouper.ts +318 -0
  118. package/src/core/MetadataExporter.ts +148 -0
  119. package/src/core/QueryDatabaseWriter.ts +187 -0
  120. package/src/core/QueryFixer.ts +153 -0
  121. package/src/core/QueryRefiner.ts +382 -0
  122. package/src/core/QueryTester.ts +264 -0
  123. package/src/core/QueryWriter.ts +239 -0
  124. package/src/core/QuestionGenerator.ts +199 -0
  125. package/src/data/golden-queries.json +1371 -0
  126. package/src/data/schema.ts +252 -0
  127. package/src/index.ts +49 -0
  128. package/src/prompts/PromptNames.ts +36 -0
  129. package/src/utils/category-builder.ts +97 -0
  130. package/src/utils/entity-helpers.ts +203 -0
  131. package/src/utils/error-handlers.ts +41 -0
  132. package/src/utils/graph-helpers.ts +99 -0
  133. package/src/utils/prompt-helpers.ts +79 -0
  134. package/src/utils/query-helpers.ts +32 -0
  135. package/src/utils/user-helpers.ts +39 -0
  136. package/src/vectors/EmbeddingService.ts +109 -0
  137. package/src/vectors/SimilaritySearch.ts +108 -0
  138. package/tsconfig.json +39 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * QueryTester - Tests and validates SQL queries
3
+ *
4
+ * Renders Nunjucks templates with sample parameter values and executes
5
+ * queries against the database. Handles error fixing with retry loop.
6
+ */
7
+
8
+ import * as nunjucks from 'nunjucks';
9
+ import {
10
+ DatabaseProviderBase,
11
+ RunQuerySQLFilterManager,
12
+ UserInfo,
13
+ LogError,
14
+ } from '@memberjunction/core';
15
+ import { extractErrorMessage } from '../utils/error-handlers';
16
+ import {
17
+ GeneratedQuery,
18
+ QueryTestResult,
19
+ EntityMetadataForPrompt,
20
+ BusinessQuestion,
21
+ } from '../data/schema';
22
+ import { QueryFixer } from './QueryFixer';
23
+ import { QueryGenConfig } from '../cli/config';
24
+
25
+ /**
26
+ * QueryTester class
27
+ * Tests SQL queries by rendering templates and executing against database
28
+ */
29
+ export class QueryTester {
30
+ private nunjucksEnv: nunjucks.Environment;
31
+
32
+ constructor(
33
+ private dataProvider: DatabaseProviderBase,
34
+ private entityMetadata: EntityMetadataForPrompt[],
35
+ private businessQuestion: BusinessQuestion,
36
+ private contextUser: UserInfo,
37
+ private config: QueryGenConfig
38
+ ) {
39
+ // Initialize Nunjucks environment with SQL-safe filters
40
+ this.nunjucksEnv = new nunjucks.Environment(null, {
41
+ autoescape: false,
42
+ throwOnUndefined: true,
43
+ trimBlocks: true,
44
+ lstripBlocks: true,
45
+ });
46
+
47
+ // Add custom SQL-safe filters from RunQuerySQLFilterManager
48
+ const filterManager = RunQuerySQLFilterManager.Instance;
49
+ const filters = filterManager.getAllFilters();
50
+
51
+ for (const filter of filters) {
52
+ if (filter.implementation) {
53
+ this.nunjucksEnv.addFilter(filter.name, filter.implementation);
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Test a query by rendering template with sample values and executing it
60
+ * Retries up to maxAttempts times, calling QueryFixer on failures
61
+ *
62
+ * @param query - Generated query to test
63
+ * @param maxAttempts - Maximum number of retry attempts (default: 5)
64
+ * @returns Test result with success status, SQL, and sample data
65
+ */
66
+ async testQuery(
67
+ query: GeneratedQuery,
68
+ maxAttempts: number = 5
69
+ ): Promise<QueryTestResult> {
70
+ let attempt = 0;
71
+ let lastError: string | undefined;
72
+ let currentQuery = query;
73
+
74
+ while (attempt < maxAttempts) {
75
+ attempt++;
76
+
77
+ try {
78
+ // 1. Render template with sample parameter values
79
+ const renderedSQL = this.renderQueryTemplate(currentQuery);
80
+
81
+ // 2. Execute SQL on database
82
+ const results = await this.executeSQLQuery(renderedSQL);
83
+
84
+ // 3. Success! (Empty results are valid - query executed without errors)
85
+ // Note: We don't validate rowCount because:
86
+ // - Empty results may indicate no data in database (not a query error)
87
+ // - Query structure can be correct even with zero rows returned
88
+ // - Testing should focus on SQL syntax/execution, not data presence
89
+ return {
90
+ success: true,
91
+ renderedSQL,
92
+ rowCount: results.length,
93
+ sampleRows: results.slice(0, 10), // Return first 10 rows
94
+ attempts: attempt,
95
+ };
96
+ } catch (error: unknown) {
97
+ lastError = extractErrorMessage(error, 'Query Testing');
98
+ if (this.config.verbose) {
99
+ LogError(`Attempt ${attempt}/${maxAttempts} failed: ${lastError}`);
100
+ }
101
+
102
+ // 5. If not last attempt, try to fix the query
103
+ if (attempt < maxAttempts) {
104
+ currentQuery = await this.fixQuery(currentQuery, lastError);
105
+ }
106
+ }
107
+ }
108
+
109
+ // Failed after max attempts
110
+ return {
111
+ success: false,
112
+ error: lastError,
113
+ attempts: maxAttempts,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Render query template with sample parameter values
119
+ * Uses QueryParameterProcessor for proper type handling
120
+ *
121
+ * @param query - Generated query with parameters
122
+ * @returns Rendered SQL string ready for execution
123
+ */
124
+ private renderQueryTemplate(query: GeneratedQuery): string {
125
+ // Build parameter values object
126
+ const paramValues: Record<string, unknown> = {};
127
+
128
+ for (const param of query.parameters) {
129
+ const rawValue = param.sampleValue;
130
+ if (rawValue !== undefined && rawValue !== null) {
131
+ paramValues[param.name] = this.processParameterValue(rawValue, param.type);
132
+ }
133
+ }
134
+
135
+ try {
136
+ // Render template using Nunjucks with SQL-safe filters
137
+ const renderedSQL = this.nunjucksEnv.renderString(query.sql, paramValues);
138
+ return renderedSQL;
139
+ } catch (error: unknown) {
140
+ throw new Error(
141
+ `Template rendering failed: ${extractErrorMessage(error, 'Nunjucks')}`
142
+ );
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Processes a raw parameter value based on its type, handling special cases like arrays.
148
+ * Follows Skip-Brain pattern for parameter processing.
149
+ * For array types, this function will:
150
+ * - Parse JSON arrays if the value is a JSON string
151
+ * - Split comma-separated strings as a fallback
152
+ * - Return as-is for sqlIn filter to handle
153
+ *
154
+ * @param rawValue - The raw parameter value (from sampleValue)
155
+ * @param paramType - The parameter type ('string', 'number', 'date', 'boolean', 'array')
156
+ * @returns Processed value ready for use in Nunjucks template
157
+ */
158
+ private processParameterValue(rawValue: unknown, paramType: string): unknown {
159
+ if (rawValue === undefined || rawValue === null) {
160
+ return rawValue;
161
+ }
162
+
163
+ // For array type parameters, ensure value is compatible with sqlIn filter
164
+ if (paramType === 'array' && typeof rawValue === 'string') {
165
+ try {
166
+ // Try to parse as JSON array
167
+ const parsed = JSON.parse(rawValue);
168
+ if (Array.isArray(parsed)) {
169
+ return parsed;
170
+ }
171
+ } catch {
172
+ // Not valid JSON - return as-is for sqlIn filter to handle comma-separated strings
173
+ }
174
+ // Return comma-separated string as-is - sqlIn filter handles this
175
+ return rawValue;
176
+ }
177
+
178
+ // For non-array types, convert as needed
179
+ switch (paramType) {
180
+ case 'number':
181
+ if (typeof rawValue === 'string') {
182
+ const num = Number(rawValue);
183
+ if (isNaN(num)) {
184
+ throw new Error(`Invalid number sample value: ${rawValue}`);
185
+ }
186
+ return num;
187
+ }
188
+ return rawValue;
189
+
190
+ case 'boolean':
191
+ if (typeof rawValue === 'string') {
192
+ const lower = rawValue.toLowerCase();
193
+ if (lower !== 'true' && lower !== 'false') {
194
+ throw new Error(`Invalid boolean sample value: ${rawValue}`);
195
+ }
196
+ return lower === 'true';
197
+ }
198
+ return rawValue;
199
+
200
+ case 'date':
201
+ if (typeof rawValue === 'string') {
202
+ const date = new Date(rawValue);
203
+ if (isNaN(date.getTime())) {
204
+ throw new Error(`Invalid date sample value: ${rawValue}`);
205
+ }
206
+ return date;
207
+ }
208
+ return rawValue;
209
+
210
+ case 'string':
211
+ default:
212
+ return rawValue;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Execute SQL query against database
218
+ * Uses DataProvider to run query with contextUser
219
+ *
220
+ * @param sql - Rendered SQL query
221
+ * @returns Array of result rows
222
+ */
223
+ private async executeSQLQuery(sql: string): Promise<unknown[]> {
224
+ try {
225
+ const result = await this.dataProvider.ExecuteSQL(
226
+ sql,
227
+ undefined,
228
+ undefined,
229
+ this.contextUser
230
+ );
231
+
232
+ if (!result || !Array.isArray(result)) {
233
+ throw new Error('ExecuteSQL returned invalid result format');
234
+ }
235
+
236
+ return result;
237
+ } catch (error: unknown) {
238
+ throw new Error(
239
+ `SQL execution failed: ${extractErrorMessage(error, 'Database')}`
240
+ );
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Fix a query that failed to execute
246
+ * Uses QueryFixer with AI to analyze error and generate correction
247
+ *
248
+ * @param query - Query that failed
249
+ * @param errorMessage - Error message from execution
250
+ * @returns Corrected query
251
+ */
252
+ private async fixQuery(
253
+ query: GeneratedQuery,
254
+ errorMessage: string
255
+ ): Promise<GeneratedQuery> {
256
+ const fixer = new QueryFixer(this.contextUser, this.config);
257
+ return await fixer.fixQuery(
258
+ query,
259
+ errorMessage,
260
+ this.entityMetadata,
261
+ this.businessQuestion
262
+ );
263
+ }
264
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * QueryWriter - Generates SQL query templates using AI with few-shot learning
3
+ *
4
+ * Uses the SQL Query Writer AI prompt to generate Nunjucks SQL templates
5
+ * based on business questions and similar golden query examples.
6
+ */
7
+
8
+ import { AIEngine } from '@memberjunction/aiengine';
9
+ import { AIPromptEntityExtended } from '@memberjunction/core-entities';
10
+ import { UserInfo, LogStatus } from '@memberjunction/core';
11
+ import { QueryGenConfig } from '../cli/config';
12
+ import { extractErrorMessage } from '../utils/error-handlers';
13
+ import { executePromptWithOverrides } from '../utils/prompt-helpers';
14
+ import { BusinessQuestion, GeneratedQuery, EntityMetadataForPrompt, GoldenQuery } from '../data/schema';
15
+ import { PROMPT_SQL_QUERY_WRITER } from '../prompts/PromptNames';
16
+
17
+ /**
18
+ * QueryWriter class
19
+ * Generates Nunjucks SQL query templates using AI with few-shot learning
20
+ */
21
+ export class QueryWriter {
22
+ constructor(
23
+ private contextUser: UserInfo,
24
+ private config: QueryGenConfig
25
+ ) {}
26
+
27
+ /**
28
+ * Generate SQL query template for a business question
29
+ * Uses few-shot learning with similar golden query examples
30
+ *
31
+ * @param businessQuestion - Business question to answer with SQL
32
+ * @param entityMetadata - Available entity metadata for query
33
+ * @param fewShotExamples - Similar golden queries for few-shot learning
34
+ * @returns Generated SQL query with parameters and output schema
35
+ */
36
+ async generateQuery(
37
+ businessQuestion: BusinessQuestion,
38
+ entityMetadata: EntityMetadataForPrompt[],
39
+ fewShotExamples: GoldenQuery[]
40
+ ): Promise<GeneratedQuery> {
41
+ try {
42
+ // Ensure AIEngine is configured
43
+ const aiEngine = AIEngine.Instance;
44
+ await aiEngine.Config(false, this.contextUser);
45
+
46
+ // Find the SQL Query Writer prompt
47
+ const prompt = this.findPromptByName(aiEngine, PROMPT_SQL_QUERY_WRITER);
48
+
49
+ // Prepare prompt data
50
+ const promptData = {
51
+ userQuestion: businessQuestion.userQuestion,
52
+ description: businessQuestion.description,
53
+ technicalDescription: businessQuestion.technicalDescription,
54
+ entityMetadata,
55
+ fewShotExamples,
56
+ };
57
+
58
+ // Execute AI prompt
59
+ const generatedQuery = await this.executePrompt(prompt, promptData);
60
+
61
+ // Validate the generated query structure
62
+ this.validateGeneratedQuery(generatedQuery);
63
+
64
+ return generatedQuery;
65
+ } catch (error: unknown) {
66
+ throw new Error(
67
+ extractErrorMessage(error, 'QueryWriter.generateQuery')
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Find prompt by name in AIEngine cache
74
+ * Throws if prompt not found
75
+ */
76
+ private findPromptByName(
77
+ aiEngine: AIEngine,
78
+ promptName: string
79
+ ): AIPromptEntityExtended {
80
+ const prompt = aiEngine.Prompts.find((p) => p.Name === promptName);
81
+ if (!prompt) {
82
+ throw new Error(`Prompt '${promptName}' not found in AIEngine cache`);
83
+ }
84
+ return prompt;
85
+ }
86
+
87
+ /**
88
+ * Execute the SQL Query Writer AI prompt with retry logic for validation failures
89
+ * Parses JSON response and validates structure, retrying with feedback if validation fails
90
+ */
91
+ private async executePrompt(
92
+ prompt: AIPromptEntityExtended,
93
+ promptData: {
94
+ userQuestion: string;
95
+ description: string;
96
+ technicalDescription: string;
97
+ entityMetadata: EntityMetadataForPrompt[];
98
+ fewShotExamples: GoldenQuery[];
99
+ }
100
+ ): Promise<GeneratedQuery> {
101
+ const maxRetries = 3;
102
+ let lastError: Error | null = null;
103
+ let lastResult: GeneratedQuery | null = null;
104
+
105
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
106
+ try {
107
+ // Execute AI prompt
108
+ const result = await executePromptWithOverrides<GeneratedQuery>(
109
+ prompt,
110
+ promptData,
111
+ this.contextUser,
112
+ this.config
113
+ );
114
+
115
+ if (!result || !result.success) {
116
+ throw new Error(
117
+ `AI prompt execution failed: ${result?.errorMessage || 'Unknown error'}`
118
+ );
119
+ }
120
+
121
+ if (!result.result) {
122
+ throw new Error('AI prompt returned no result');
123
+ }
124
+
125
+ lastResult = result.result;
126
+
127
+ // Validate the generated query structure
128
+ // This will throw if validation fails
129
+ this.validateGeneratedQuery(lastResult);
130
+
131
+ // Validation passed, return the query
132
+ return lastResult;
133
+ } catch (error: unknown) {
134
+ lastError = error instanceof Error ? error : new Error(String(error));
135
+
136
+ // If this is the last attempt, throw the error
137
+ if (attempt === maxRetries) {
138
+ throw new Error(
139
+ `Query generation failed after ${maxRetries + 1} attempts: ${lastError.message}`
140
+ );
141
+ }
142
+
143
+ // Log retry attempt
144
+ if (this.config.verbose) {
145
+ LogStatus(`⚠️ Query validation failed on attempt ${attempt + 1}/${maxRetries + 1}: ${lastError.message}`);
146
+ LogStatus(` Retrying with validation feedback...`);
147
+ }
148
+
149
+ // Add validation feedback to the prompt data for next attempt
150
+ // This helps the LLM correct its mistakes
151
+ promptData = {
152
+ ...promptData,
153
+ // Add feedback about what went wrong
154
+ validationFeedback: `Previous attempt failed validation: ${lastError.message}. Please correct this issue.`,
155
+ } as typeof promptData & { validationFeedback: string };
156
+ }
157
+ }
158
+
159
+ // Should never reach here due to throw in loop, but TypeScript needs this
160
+ throw lastError || new Error('Query generation failed');
161
+ }
162
+
163
+ /**
164
+ * Validate generated query structure
165
+ * Ensures query has proper SQL template syntax and valid metadata
166
+ *
167
+ * @param query - Generated query to validate
168
+ * @throws Error if query structure is invalid
169
+ */
170
+ private validateGeneratedQuery(query: GeneratedQuery): void {
171
+ // Validate SQL is present
172
+ if (!query.sql || query.sql.trim().length === 0) {
173
+ throw new Error('Generated query has empty SQL');
174
+ }
175
+
176
+ // Validate SQL contains base view references (not raw tables)
177
+ if (!this.usesBaseViews(query.sql)) {
178
+ throw new Error('Generated SQL must use base views (vw*), not raw tables');
179
+ }
180
+
181
+ // Validate parameters array
182
+ if (!Array.isArray(query.parameters)) {
183
+ throw new Error('Generated query parameters must be an array');
184
+ }
185
+
186
+ // Validate each parameter
187
+ for (const param of query.parameters) {
188
+ this.validateParameter(param);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Check if SQL uses base views (vw* pattern)
194
+ * Basic heuristic: looks for view names in FROM and JOIN clauses
195
+ */
196
+ private usesBaseViews(sql: string): boolean {
197
+ // Look for view patterns like [schema].[vw...] or FROM vw...
198
+ const viewPattern = /\b(FROM|JOIN)\s+(\[\w+\]\.)?\[?vw\w+\]?/i;
199
+ return viewPattern.test(sql);
200
+ }
201
+
202
+ /**
203
+ * Validate a single query parameter
204
+ * Ensures all required fields are present and valid
205
+ */
206
+ private validateParameter(param: unknown): void {
207
+ if (!param || typeof param !== 'object') {
208
+ throw new Error('Query parameter must be an object');
209
+ }
210
+
211
+ const p = param as Record<string, unknown>;
212
+
213
+ if (!p.name || typeof p.name !== 'string') {
214
+ throw new Error('Query parameter must have a valid name');
215
+ }
216
+
217
+ if (!p.type || typeof p.type !== 'string') {
218
+ throw new Error(`Query parameter '${p.name}' must have a valid type`);
219
+ }
220
+
221
+ if (typeof p.isRequired !== 'boolean') {
222
+ throw new Error(`Query parameter '${p.name}' must have isRequired boolean`);
223
+ }
224
+
225
+ if (!p.description || typeof p.description !== 'string') {
226
+ throw new Error(`Query parameter '${p.name}' must have a description`);
227
+ }
228
+
229
+ if (!Array.isArray(p.usage)) {
230
+ throw new Error(`Query parameter '${p.name}' must have usage array`);
231
+ }
232
+
233
+ // sampleValue is required and can be string, number, boolean, or array depending on parameter type
234
+ if (p.sampleValue === undefined || p.sampleValue === null || p.sampleValue === '') {
235
+ throw new Error(`Query parameter '${p.name}' must have a sampleValue`);
236
+ }
237
+ }
238
+
239
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * QuestionGenerator - Generates business questions for entity groups using AI
3
+ *
4
+ * Uses the Business Question Generator AI prompt to create 1-2 meaningful
5
+ * business questions per entity group that can be answered with SQL.
6
+ */
7
+
8
+ import { AIEngine } from '@memberjunction/aiengine';
9
+ import { AIPromptEntityExtended } from '@memberjunction/core-entities';
10
+ import { UserInfo, LogStatus } from '@memberjunction/core';
11
+ import { QueryGenConfig } from '../cli/config';
12
+ import { extractErrorMessage } from '../utils/error-handlers';
13
+ import { formatEntityGroupForPrompt } from '../utils/entity-helpers';
14
+ import { executePromptWithOverrides } from '../utils/prompt-helpers';
15
+ import { EntityGroup, BusinessQuestion } from '../data/schema';
16
+ import { PROMPT_BUSINESS_QUESTION_GENERATOR } from '../prompts/PromptNames';
17
+
18
+ /**
19
+ * Result structure from Business Question Generator AI prompt
20
+ */
21
+ interface QuestionGeneratorResult {
22
+ questions: BusinessQuestion[];
23
+ }
24
+
25
+ /**
26
+ * QuestionGenerator class
27
+ * Generates domain-specific business questions using AI
28
+ */
29
+ export class QuestionGenerator {
30
+ constructor(
31
+ private contextUser: UserInfo,
32
+ private config: QueryGenConfig
33
+ ) {}
34
+
35
+ /**
36
+ * Generate business questions for an entity group
37
+ * Uses AI to create 1-2 meaningful questions per entity group
38
+ *
39
+ * @param entityGroup - Entity group to generate questions for
40
+ * @returns Array of validated business questions
41
+ */
42
+ async generateQuestions(entityGroup: EntityGroup): Promise<BusinessQuestion[]> {
43
+ try {
44
+ // Ensure AIEngine is configured
45
+ const aiEngine = AIEngine.Instance;
46
+ await aiEngine.Config(false, this.contextUser);
47
+
48
+ // Find the Business Question Generator prompt
49
+ const prompt = this.findPromptByName(aiEngine, PROMPT_BUSINESS_QUESTION_GENERATOR);
50
+
51
+ // Format entity group for prompt
52
+ const entityMetadata = formatEntityGroupForPrompt(entityGroup);
53
+
54
+ // Execute AI prompt
55
+ const result = await this.executePrompt(prompt, entityMetadata);
56
+
57
+ // Validate and filter questions
58
+ const totalGenerated = result.questions.length;
59
+ const validQuestions = this.validateQuestions(result.questions, entityGroup);
60
+
61
+ // Log if questions were filtered out
62
+ if (this.config.verbose && totalGenerated > validQuestions.length) {
63
+ LogStatus(
64
+ `QuestionGenerator: Filtered out ${totalGenerated - validQuestions.length} of ${totalGenerated} questions for ${entityGroup.primaryEntity.Name}`
65
+ );
66
+ }
67
+
68
+ // Warn if no valid questions generated
69
+ if (validQuestions.length === 0 && totalGenerated > 0) {
70
+ LogStatus(
71
+ `⚠️ QuestionGenerator: All ${totalGenerated} generated questions were filtered out for ${entityGroup.primaryEntity.Name}`
72
+ );
73
+ }
74
+
75
+ return validQuestions;
76
+ } catch (error: unknown) {
77
+ throw new Error(
78
+ extractErrorMessage(error, 'QuestionGenerator.generateQuestions')
79
+ );
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Find prompt by name in AIEngine cache
85
+ * Throws if prompt not found
86
+ */
87
+ private findPromptByName(
88
+ aiEngine: AIEngine,
89
+ promptName: string
90
+ ): AIPromptEntityExtended {
91
+ const prompt = aiEngine.Prompts.find((p) => p.Name === promptName);
92
+ if (!prompt) {
93
+ throw new Error(`Prompt '${promptName}' not found in AIEngine cache`);
94
+ }
95
+ return prompt;
96
+ }
97
+
98
+ /**
99
+ * Execute the AI prompt with model/vendor overrides
100
+ * Parses JSON response and validates structure
101
+ */
102
+ private async executePrompt(
103
+ prompt: AIPromptEntityExtended,
104
+ entityMetadata: unknown
105
+ ): Promise<QuestionGeneratorResult> {
106
+ const result = await executePromptWithOverrides<QuestionGeneratorResult>(
107
+ prompt,
108
+ { entityGroupMetadata: entityMetadata },
109
+ this.contextUser,
110
+ this.config
111
+ );
112
+
113
+ if (!result || !result.success) {
114
+ throw new Error(`AI prompt execution failed: ${result?.errorMessage || 'Unknown error'}`);
115
+ }
116
+
117
+ if (!result.result) {
118
+ throw new Error('AI prompt returned no result');
119
+ }
120
+
121
+ return result.result;
122
+ }
123
+
124
+
125
+ /**
126
+ * Validate and filter business questions
127
+ * Removes low-quality or unanswerable questions
128
+ *
129
+ * @param questions - Raw questions from AI
130
+ * @param entityGroup - Entity group for validation context
131
+ * @returns Filtered array of valid questions
132
+ */
133
+ private validateQuestions(
134
+ questions: BusinessQuestion[],
135
+ entityGroup: EntityGroup
136
+ ): BusinessQuestion[] {
137
+ const entityNames = new Set(entityGroup.entities.map((e) => e.Name));
138
+
139
+ return questions.filter((q) => {
140
+ // Must have required fields
141
+ if (!this.hasRequiredFields(q)) {
142
+ return false;
143
+ }
144
+
145
+ // Must reference entities in the group
146
+ if (!this.referencesGroupEntities(q, entityNames)) {
147
+ return false;
148
+ }
149
+
150
+ // Must not be overly generic
151
+ if (this.isTooGeneric(q)) {
152
+ return false;
153
+ }
154
+
155
+ return true;
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Check if question has all required fields
161
+ */
162
+ private hasRequiredFields(question: BusinessQuestion): boolean {
163
+ return !!(
164
+ question.userQuestion &&
165
+ question.description &&
166
+ question.technicalDescription &&
167
+ question.complexity &&
168
+ Array.isArray(question.entities) &&
169
+ question.entities.length > 0
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Check if question references entities in the group
175
+ */
176
+ private referencesGroupEntities(
177
+ question: BusinessQuestion,
178
+ entityNames: Set<string>
179
+ ): boolean {
180
+ return question.entities.some((entityName) => entityNames.has(entityName));
181
+ }
182
+
183
+ /**
184
+ * Check if question is too generic to be useful
185
+ */
186
+ private isTooGeneric(question: BusinessQuestion): boolean {
187
+ const genericPatterns = [
188
+ /show\s+me\s+all/i,
189
+ /list\s+all/i,
190
+ /get\s+all/i,
191
+ /display\s+all/i,
192
+ /^what\s+is/i,
193
+ ];
194
+
195
+ return genericPatterns.some((pattern) =>
196
+ pattern.test(question.userQuestion)
197
+ );
198
+ }
199
+ }