@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +34 -0
- package/COORDINATOR.md +768 -0
- package/IMPLEMENTATION_PLAN.md +1753 -0
- package/LLM_ENTITY_GROUPING_PLAN.md +977 -0
- package/README.md +675 -29
- package/dist/cli/commands/export.d.ts +15 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +178 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +19 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +282 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +17 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +193 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/config.d.ts +51 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +142 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +57 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/EntityGrouper.d.ts +74 -0
- package/dist/core/EntityGrouper.d.ts.map +1 -0
- package/dist/core/EntityGrouper.js +246 -0
- package/dist/core/EntityGrouper.js.map +1 -0
- package/dist/core/MetadataExporter.d.ts +59 -0
- package/dist/core/MetadataExporter.d.ts.map +1 -0
- package/dist/core/MetadataExporter.js +151 -0
- package/dist/core/MetadataExporter.js.map +1 -0
- package/dist/core/QueryDatabaseWriter.d.ts +50 -0
- package/dist/core/QueryDatabaseWriter.d.ts.map +1 -0
- package/dist/core/QueryDatabaseWriter.js +152 -0
- package/dist/core/QueryDatabaseWriter.js.map +1 -0
- package/dist/core/QueryFixer.d.ts +48 -0
- package/dist/core/QueryFixer.d.ts.map +1 -0
- package/dist/core/QueryFixer.js +115 -0
- package/dist/core/QueryFixer.js.map +1 -0
- package/dist/core/QueryRefiner.d.ts +94 -0
- package/dist/core/QueryRefiner.d.ts.map +1 -0
- package/dist/core/QueryRefiner.js +267 -0
- package/dist/core/QueryRefiner.js.map +1 -0
- package/dist/core/QueryTester.d.ts +70 -0
- package/dist/core/QueryTester.d.ts.map +1 -0
- package/dist/core/QueryTester.js +243 -0
- package/dist/core/QueryTester.js.map +1 -0
- package/dist/core/QueryWriter.d.ts +57 -0
- package/dist/core/QueryWriter.d.ts.map +1 -0
- package/dist/core/QueryWriter.js +184 -0
- package/dist/core/QueryWriter.js.map +1 -0
- package/dist/core/QuestionGenerator.d.ts +58 -0
- package/dist/core/QuestionGenerator.d.ts.map +1 -0
- package/dist/core/QuestionGenerator.js +145 -0
- package/dist/core/QuestionGenerator.js.map +1 -0
- package/dist/data/schema.d.ts +230 -0
- package/dist/data/schema.d.ts.map +1 -0
- package/dist/data/schema.js +6 -0
- package/dist/data/schema.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/PromptNames.d.ts +32 -0
- package/dist/prompts/PromptNames.d.ts.map +1 -0
- package/dist/prompts/PromptNames.js +35 -0
- package/dist/prompts/PromptNames.js.map +1 -0
- package/dist/utils/category-builder.d.ts +28 -0
- package/dist/utils/category-builder.d.ts.map +1 -0
- package/dist/utils/category-builder.js +90 -0
- package/dist/utils/category-builder.js.map +1 -0
- package/dist/utils/entity-helpers.d.ts +49 -0
- package/dist/utils/entity-helpers.d.ts.map +1 -0
- package/dist/utils/entity-helpers.js +189 -0
- package/dist/utils/entity-helpers.js.map +1 -0
- package/dist/utils/error-handlers.d.ts +19 -0
- package/dist/utils/error-handlers.d.ts.map +1 -0
- package/dist/utils/error-handlers.js +41 -0
- package/dist/utils/error-handlers.js.map +1 -0
- package/dist/utils/graph-helpers.d.ts +51 -0
- package/dist/utils/graph-helpers.d.ts.map +1 -0
- package/dist/utils/graph-helpers.js +82 -0
- package/dist/utils/graph-helpers.js.map +1 -0
- package/dist/utils/prompt-helpers.d.ts +25 -0
- package/dist/utils/prompt-helpers.d.ts.map +1 -0
- package/dist/utils/prompt-helpers.js +66 -0
- package/dist/utils/prompt-helpers.js.map +1 -0
- package/dist/utils/query-helpers.d.ts +23 -0
- package/dist/utils/query-helpers.d.ts.map +1 -0
- package/dist/utils/query-helpers.js +34 -0
- package/dist/utils/query-helpers.js.map +1 -0
- package/dist/utils/user-helpers.d.ts +15 -0
- package/dist/utils/user-helpers.d.ts.map +1 -0
- package/dist/utils/user-helpers.js +32 -0
- package/dist/utils/user-helpers.js.map +1 -0
- package/dist/vectors/EmbeddingService.d.ts +58 -0
- package/dist/vectors/EmbeddingService.d.ts.map +1 -0
- package/dist/vectors/EmbeddingService.js +90 -0
- package/dist/vectors/EmbeddingService.js.map +1 -0
- package/dist/vectors/SimilaritySearch.d.ts +51 -0
- package/dist/vectors/SimilaritySearch.d.ts.map +1 -0
- package/dist/vectors/SimilaritySearch.js +85 -0
- package/dist/vectors/SimilaritySearch.js.map +1 -0
- package/docs/API.md +1040 -0
- package/docs/ARCHITECTURE.md +1120 -0
- package/examples/advanced-usage.ts +401 -0
- package/examples/basic-usage.ts +285 -0
- package/package.json +48 -6
- package/src/cli/commands/export.ts +173 -0
- package/src/cli/commands/generate.ts +330 -0
- package/src/cli/commands/validate.ts +185 -0
- package/src/cli/config.ts +203 -0
- package/src/cli/index.ts +63 -0
- package/src/core/EntityGrouper.ts +318 -0
- package/src/core/MetadataExporter.ts +148 -0
- package/src/core/QueryDatabaseWriter.ts +187 -0
- package/src/core/QueryFixer.ts +153 -0
- package/src/core/QueryRefiner.ts +382 -0
- package/src/core/QueryTester.ts +264 -0
- package/src/core/QueryWriter.ts +239 -0
- package/src/core/QuestionGenerator.ts +199 -0
- package/src/data/golden-queries.json +1371 -0
- package/src/data/schema.ts +252 -0
- package/src/index.ts +49 -0
- package/src/prompts/PromptNames.ts +36 -0
- package/src/utils/category-builder.ts +97 -0
- package/src/utils/entity-helpers.ts +203 -0
- package/src/utils/error-handlers.ts +41 -0
- package/src/utils/graph-helpers.ts +99 -0
- package/src/utils/prompt-helpers.ts +79 -0
- package/src/utils/query-helpers.ts +32 -0
- package/src/utils/user-helpers.ts +39 -0
- package/src/vectors/EmbeddingService.ts +109 -0
- package/src/vectors/SimilaritySearch.ts +108 -0
- 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
|
+
}
|