@memberjunction/ai-prompts 2.44.0 → 2.45.0

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.
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.LoadAIPromptRunner = exports.AIPromptRunner = exports.AIPromptRunResult = exports.AIPromptParams = void 0;
4
7
  const ai_1 = require("@memberjunction/ai");
@@ -8,6 +11,7 @@ const templates_1 = require("@memberjunction/templates");
8
11
  const ExecutionPlanner_1 = require("./ExecutionPlanner");
9
12
  const ParallelExecutionCoordinator_1 = require("./ParallelExecutionCoordinator");
10
13
  const aiengine_1 = require("@memberjunction/aiengine");
14
+ const ajv_1 = __importDefault(require("ajv"));
11
15
  class AIPromptParams {
12
16
  }
13
17
  exports.AIPromptParams = AIPromptParams;
@@ -20,10 +24,34 @@ class AIPromptRunner {
20
24
  this._templateEngine = templates_1.TemplateEngineServer.Instance;
21
25
  this._executionPlanner = new ExecutionPlanner_1.ExecutionPlanner();
22
26
  this._parallelCoordinator = new ParallelExecutionCoordinator_1.ParallelExecutionCoordinator();
27
+ if (!AIPromptRunner._ajvInstance) {
28
+ AIPromptRunner._ajvInstance = new ajv_1.default({ allErrors: true, verbose: true });
29
+ }
30
+ this._ajv = AIPromptRunner._ajvInstance;
31
+ }
32
+ static clearSchemaCache() {
33
+ AIPromptRunner._schemaCache.clear();
34
+ (0, core_1.LogStatus)('Cleared global JSON schema cache');
35
+ }
36
+ static getSchemaCacheStats() {
37
+ return {
38
+ size: AIPromptRunner._schemaCache.size,
39
+ keys: Array.from(AIPromptRunner._schemaCache.keys())
40
+ };
23
41
  }
24
42
  async ExecutePrompt(params) {
25
43
  const startTime = new Date();
26
- let promptRun = null;
44
+ const promptRun = null;
45
+ if (params.cancellationToken?.aborted) {
46
+ return {
47
+ success: false,
48
+ status: 'cancelled',
49
+ cancelled: true,
50
+ cancellationReason: 'user_requested',
51
+ errorMessage: 'Prompt execution was cancelled before starting',
52
+ executionTimeMS: 0,
53
+ };
54
+ }
27
55
  try {
28
56
  const prompt = params.prompt;
29
57
  if (!prompt) {
@@ -32,21 +60,28 @@ class AIPromptRunner {
32
60
  if (prompt.Status !== 'Active') {
33
61
  throw new Error(`Prompt ${prompt.Name} is not active (Status: ${prompt.Status})`);
34
62
  }
35
- await this._templateEngine.Config(false, params.contextUser);
36
- const template = await this.loadTemplate(prompt.TemplateID, params.contextUser);
37
- if (!template) {
38
- throw new Error(`Template with ID ${prompt.TemplateID} not found for prompt ${prompt.Name}`);
63
+ let renderedPromptText = '';
64
+ if (prompt.TemplateID && (!params.conversationMessages || params.templateMessageRole !== 'none')) {
65
+ await this._templateEngine.Config(false, params.contextUser);
66
+ const template = await this.loadTemplate(prompt.TemplateID, params.contextUser);
67
+ if (!template) {
68
+ throw new Error(`Template with ID ${prompt.TemplateID} not found for prompt ${prompt.Name}`);
69
+ }
70
+ const renderedPrompt = await this.renderPromptTemplate(template, params.data, params.templateData);
71
+ if (!renderedPrompt.Success) {
72
+ throw new Error(`Failed to render template for prompt ${prompt.Name}: ${renderedPrompt.Message}`);
73
+ }
74
+ renderedPromptText = renderedPrompt.Output;
39
75
  }
40
- const renderedPrompt = await this.renderPromptTemplate(template, params.data, params.templateData);
41
- if (!renderedPrompt.Success) {
42
- throw new Error(`Failed to render template for prompt ${prompt.Name}: ${renderedPrompt.Message}`);
76
+ if (params.cancellationToken?.aborted) {
77
+ throw new Error('Prompt execution was cancelled during template rendering');
43
78
  }
44
79
  const shouldUseParallelExecution = prompt.ParallelizationMode && prompt.ParallelizationMode !== 'None';
45
80
  if (shouldUseParallelExecution) {
46
- return await this.executePromptInParallel(prompt, renderedPrompt.Output, params, startTime);
81
+ return await this.executePromptInParallel(prompt, renderedPromptText, params, startTime);
47
82
  }
48
83
  else {
49
- return await this.executeSinglePrompt(prompt, renderedPrompt.Output, params, startTime);
84
+ return await this.executeSinglePrompt(prompt, renderedPromptText, params, startTime);
50
85
  }
51
86
  }
52
87
  catch (error) {
@@ -66,21 +101,29 @@ class AIPromptRunner {
66
101
  success: false,
67
102
  errorMessage: error.message,
68
103
  promptRun,
69
- executionTimeMS
104
+ executionTimeMS,
70
105
  };
71
106
  }
72
107
  }
73
108
  async executeSinglePrompt(prompt, renderedPromptText, params, startTime) {
109
+ if (params.cancellationToken?.aborted) {
110
+ throw new Error('Prompt execution was cancelled before model selection');
111
+ }
74
112
  const selectedModel = await this.selectModel(prompt, params.modelId, params.contextUser, params.configurationId, params.vendorId);
75
113
  if (!selectedModel) {
76
114
  throw new Error(`No suitable model found for prompt ${prompt.Name}`);
77
115
  }
116
+ if (params.cancellationToken?.aborted) {
117
+ throw new Error('Prompt execution was cancelled after model selection');
118
+ }
78
119
  const promptRun = await this.createPromptRun(prompt, selectedModel, params, startTime, params.vendorId);
79
- const modelResult = await this.executeModel(selectedModel, renderedPromptText, prompt, params.contextUser);
80
- const parsedResult = await this.parseAndValidateResult(modelResult, prompt, params.skipValidation);
120
+ if (params.cancellationToken?.aborted) {
121
+ throw new Error('Prompt execution was cancelled before model execution');
122
+ }
123
+ const { modelResult, parsedResult, validationAttempts } = await this.executeWithValidationRetries(selectedModel, renderedPromptText, prompt, params, promptRun);
81
124
  const endTime = new Date();
82
125
  const executionTimeMS = endTime.getTime() - startTime.getTime();
83
- await this.updatePromptRun(promptRun, modelResult, parsedResult, endTime, executionTimeMS);
126
+ await this.updatePromptRun(promptRun, modelResult, parsedResult, endTime, executionTimeMS, validationAttempts);
84
127
  const chatResult = modelResult;
85
128
  return {
86
129
  success: true,
@@ -89,23 +132,30 @@ class AIPromptRunner {
89
132
  promptRun,
90
133
  executionTimeMS,
91
134
  tokensUsed: chatResult.data?.usage?.totalTokens,
92
- validationResult: parsedResult.validationResult
135
+ validationResult: parsedResult.validationResult,
136
+ validationAttempts,
93
137
  };
94
138
  }
95
139
  async executePromptInParallel(prompt, renderedPromptText, params, startTime) {
140
+ if (params.cancellationToken?.aborted) {
141
+ throw new Error('Parallel execution was cancelled before starting');
142
+ }
96
143
  await aiengine_1.AIEngine.Instance.Config(false, params.contextUser);
97
- const promptModels = aiengine_1.AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID &&
144
+ const promptModels = aiengine_1.AIEngine.Instance.PromptModels.filter((pm) => pm.PromptID === prompt.ID &&
98
145
  (pm.Status === 'Active' || pm.Status === 'Preview') &&
99
146
  (!params.configurationId || !pm.ConfigurationID || pm.ConfigurationID === params.configurationId));
100
- const executionTasks = this._executionPlanner.createExecutionPlan(prompt, promptModels, aiengine_1.AIEngine.Instance.Models, renderedPromptText, params.contextUser, params.configurationId);
147
+ const executionTasks = this._executionPlanner.createExecutionPlan(prompt, promptModels, aiengine_1.AIEngine.Instance.Models, renderedPromptText, params.contextUser, params.configurationId, params.conversationMessages, params.templateMessageRole || 'system');
101
148
  if (executionTasks.length === 0) {
102
149
  throw new Error(`No execution tasks created for parallel execution of prompt ${prompt.Name}`);
103
150
  }
104
- const parallelResult = await this._parallelCoordinator.executeTasksInParallel(executionTasks);
151
+ if (params.cancellationToken?.aborted) {
152
+ throw new Error('Parallel execution was cancelled before task execution');
153
+ }
154
+ const parallelResult = await this._parallelCoordinator.executeTasksInParallel(executionTasks, undefined, undefined, params.cancellationToken);
105
155
  if (!parallelResult.success) {
106
156
  throw new Error(`Parallel execution failed: ${parallelResult.errors.join(', ')}`);
107
157
  }
108
- const successfulResults = parallelResult.taskResults.filter(r => r.success);
158
+ const successfulResults = parallelResult.taskResults.filter((r) => r.success);
109
159
  if (successfulResults.length === 0) {
110
160
  throw new Error(`No successful results from parallel execution`);
111
161
  }
@@ -113,9 +163,9 @@ class AIPromptRunner {
113
163
  if (successfulResults.length > 1 && prompt.ResultSelectorPromptID) {
114
164
  const selectionConfig = {
115
165
  method: 'PromptSelector',
116
- selectorPromptId: prompt.ResultSelectorPromptID
166
+ selectorPromptId: prompt.ResultSelectorPromptID,
117
167
  };
118
- const aiSelectedResult = await this._parallelCoordinator.selectBestResult(successfulResults, selectionConfig);
168
+ const aiSelectedResult = await this._parallelCoordinator.selectBestResult(successfulResults, selectionConfig, undefined, params.cancellationToken);
119
169
  if (aiSelectedResult) {
120
170
  selectedResult = aiSelectedResult;
121
171
  }
@@ -132,27 +182,68 @@ class AIPromptRunner {
132
182
  successfulTasks: parallelResult.successCount,
133
183
  failedTasks: parallelResult.failureCount,
134
184
  selectedTaskId: selectedResult.task.taskId,
135
- executionGroups: Array.from(parallelResult.groupResults.keys())
185
+ executionGroups: Array.from(parallelResult.groupResults.keys()),
136
186
  };
137
187
  if (params.data || params.templateData || parallelMetadata) {
138
188
  consolidatedPromptRun.Messages = JSON.stringify({
139
189
  data: params.data,
140
190
  templateData: params.templateData,
141
- parallelExecution: parallelMetadata
191
+ parallelExecution: parallelMetadata,
142
192
  });
143
193
  }
144
194
  const saveResult = await consolidatedPromptRun.Save();
145
195
  if (!saveResult) {
146
196
  (0, core_1.LogError)(`Failed to save consolidated AIPromptRun: ${consolidatedPromptRun.LatestResult?.Message || 'Unknown error'}`);
147
197
  }
198
+ const additionalResults = [];
199
+ const sortedResults = successfulResults.sort((a, b) => {
200
+ if (a.ranking && b.ranking) {
201
+ return a.ranking - b.ranking;
202
+ }
203
+ return 0;
204
+ });
205
+ for (const result of sortedResults) {
206
+ if (result.task.taskId !== selectedResult.task.taskId) {
207
+ const { result: parsedResultData, validationResult, validationErrors } = await this.parseAndValidateResultEnhanced(result.modelResult, prompt, params.skipValidation);
208
+ const parsedResult = { result: parsedResultData, validationResult };
209
+ additionalResults.push({
210
+ success: result.success,
211
+ rawResult: result.rawResult,
212
+ result: parsedResult.result,
213
+ executionTimeMS: result.executionTimeMS,
214
+ tokensUsed: result.tokensUsed,
215
+ validationResult: parsedResult.validationResult,
216
+ ranking: result.ranking,
217
+ judgeRationale: result.judgeRationale,
218
+ modelInfo: {
219
+ modelId: result.task.model.ID,
220
+ modelName: result.task.model.Name,
221
+ vendorId: undefined,
222
+ vendorName: result.task.model.Vendor,
223
+ },
224
+ });
225
+ }
226
+ }
227
+ const { result: selectedResultData, validationResult: selectedValidationResult } = await this.parseAndValidateResultEnhanced(selectedResult.modelResult, prompt, params.skipValidation);
228
+ const selectedParsedResult = { result: selectedResultData, validationResult: selectedValidationResult };
148
229
  return {
149
230
  success: true,
150
231
  rawResult: selectedResult.rawResult,
151
- result: selectedResult.parsedResult,
232
+ result: selectedParsedResult.result,
152
233
  promptRun: consolidatedPromptRun,
153
234
  executionTimeMS: parallelResult.totalExecutionTimeMS,
154
235
  tokensUsed: parallelResult.totalTokensUsed,
155
- validationResult: selectedResult.validationResult
236
+ validationResult: selectedParsedResult.validationResult,
237
+ additionalResults: additionalResults.length > 0 ? additionalResults : undefined,
238
+ ranking: selectedResult.ranking || 1,
239
+ judgeRationale: selectedResult.judgeRationale,
240
+ modelInfo: {
241
+ modelId: selectedResult.task.model.ID,
242
+ modelName: selectedResult.task.model.Name,
243
+ vendorId: undefined,
244
+ vendorName: selectedResult.task.model.Vendor,
245
+ },
246
+ judgeMetadata: selectedResult.judgeMetadata,
156
247
  };
157
248
  }
158
249
  async loadTemplate(templateId, _contextUser) {
@@ -175,7 +266,7 @@ class AIPromptRunner {
175
266
  const vendorResult = await rv.RunView({
176
267
  EntityName: 'AI Vendors',
177
268
  ExtraFilter: `ID='${vendorId}'`,
178
- ResultType: 'entity_object'
269
+ ResultType: 'entity_object',
179
270
  });
180
271
  if (vendorResult.Results && vendorResult.Results.length > 0) {
181
272
  vendorName = vendorResult.Results[0].Name;
@@ -189,33 +280,29 @@ class AIPromptRunner {
189
280
  }
190
281
  }
191
282
  if (explicitModelId) {
192
- const model = aiengine_1.AIEngine.Instance.Models.find(m => m.ID === explicitModelId &&
193
- (!vendorName || m.Vendor === vendorName));
283
+ const model = aiengine_1.AIEngine.Instance.Models.find((m) => m.ID === explicitModelId && (!vendorName || m.Vendor === vendorName));
194
284
  if (model && model.IsActive) {
195
285
  return model;
196
286
  }
197
287
  (0, core_1.LogError)(`Explicit model ${explicitModelId} not found, inactive, or not from specified vendor, using prompt model selection`);
198
288
  }
199
- const promptModels = aiengine_1.AIEngine.Instance.PromptModels.filter(pm => pm.PromptID === prompt.ID &&
200
- (pm.Status === 'Active' ||
201
- pm.Status === 'Preview') &&
289
+ const promptModels = aiengine_1.AIEngine.Instance.PromptModels.filter((pm) => pm.PromptID === prompt.ID &&
290
+ (pm.Status === 'Active' || pm.Status === 'Preview') &&
202
291
  (!configurationId || !pm.ConfigurationID || pm.ConfigurationID === configurationId));
203
292
  let candidateModels = [];
204
293
  if (promptModels.length > 0) {
205
294
  candidateModels = promptModels
206
- .map(pm => aiengine_1.AIEngine.Instance.Models.find(m => m.ID === pm.ModelID))
207
- .filter(m => m &&
208
- m.IsActive &&
209
- (!vendorName || m.Vendor === vendorName));
295
+ .map((pm) => aiengine_1.AIEngine.Instance.Models.find((m) => m.ID === pm.ModelID))
296
+ .filter((m) => m && m.IsActive && (!vendorName || m.Vendor === vendorName));
210
297
  candidateModels.sort((a, b) => {
211
- const aPriority = promptModels.find(pm => pm.ModelID === a.ID)?.Priority || 0;
212
- const bPriority = promptModels.find(pm => pm.ModelID === b.ID)?.Priority || 0;
298
+ const aPriority = promptModels.find((pm) => pm.ModelID === a.ID)?.Priority || 0;
299
+ const bPriority = promptModels.find((pm) => pm.ModelID === b.ID)?.Priority || 0;
213
300
  return bPriority - aPriority;
214
301
  });
215
302
  }
216
303
  else {
217
304
  const minPowerRank = prompt.MinPowerRank || 0;
218
- candidateModels = aiengine_1.AIEngine.Instance.Models.filter(m => m.IsActive &&
305
+ candidateModels = aiengine_1.AIEngine.Instance.Models.filter((m) => m.IsActive &&
219
306
  m.PowerRank >= minPowerRank &&
220
307
  (!prompt.AIModelTypeID || m.AIModelTypeID === prompt.AIModelTypeID) &&
221
308
  (!vendorName || m.Vendor === vendorName));
@@ -253,8 +340,7 @@ class AIPromptRunner {
253
340
  promptRun.VendorID = vendorId;
254
341
  }
255
342
  else {
256
- const promptModels = aiengine_1.AIEngine.Instance.PromptModels.filter(pm => pm.ModelID === model.ID)
257
- .sort((a, b) => b.Priority - a.Priority);
343
+ const promptModels = aiengine_1.AIEngine.Instance.PromptModels.filter((pm) => pm.ModelID === model.ID).sort((a, b) => b.Priority - a.Priority);
258
344
  if (promptModels.length > 0) {
259
345
  const highestPriorityModel = promptModels[0];
260
346
  promptRun.VendorID = highestPriorityModel.VendorID;
@@ -265,7 +351,7 @@ class AIPromptRunner {
265
351
  if (params.data || params.templateData) {
266
352
  promptRun.Messages = JSON.stringify({
267
353
  data: params.data,
268
- templateData: params.templateData
354
+ templateData: params.templateData,
269
355
  });
270
356
  }
271
357
  const saveResult = await promptRun.Save();
@@ -295,27 +381,408 @@ class AIPromptRunner {
295
381
  throw error;
296
382
  }
297
383
  }
298
- async executeModel(model, renderedPrompt, prompt, _contextUser) {
384
+ async executeModel(model, renderedPrompt, prompt, _contextUser, conversationMessages, templateMessageRole = 'system', cancellationToken) {
299
385
  try {
300
386
  const apiKey = (0, ai_1.GetAIAPIKey)(model.DriverClass);
301
387
  const llm = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(ai_1.BaseLLM, model.DriverClass, apiKey);
302
388
  const params = new ai_1.ChatParams();
303
389
  params.model = model.APIName;
304
- params.messages = [
305
- {
306
- role: ai_1.ChatMessageRole.user,
307
- content: renderedPrompt
308
- }
309
- ];
390
+ params.cancellationToken = cancellationToken;
391
+ params.messages = this.buildMessageArray(renderedPrompt, conversationMessages, templateMessageRole);
310
392
  if (prompt.ResponseFormat && prompt.ResponseFormat !== 'Any') {
311
393
  }
312
- return await llm.ChatCompletion(params);
394
+ if (cancellationToken) {
395
+ return await Promise.race([
396
+ llm.ChatCompletion(params),
397
+ new Promise((_, reject) => {
398
+ if (cancellationToken.aborted) {
399
+ reject(new Error('Chat completion was cancelled'));
400
+ }
401
+ else {
402
+ cancellationToken.addEventListener('abort', () => {
403
+ reject(new Error('Chat completion was cancelled'));
404
+ });
405
+ }
406
+ }),
407
+ ]);
408
+ }
409
+ else {
410
+ return await llm.ChatCompletion(params);
411
+ }
313
412
  }
314
413
  catch (error) {
315
414
  (0, core_1.LogError)(`Error executing model ${model.Name}: ${error.message}`);
316
415
  throw error;
317
416
  }
318
417
  }
418
+ buildMessageArray(renderedPrompt, conversationMessages, templateMessageRole = 'system') {
419
+ const messages = [];
420
+ if (renderedPrompt && templateMessageRole !== 'none') {
421
+ messages.push({
422
+ role: templateMessageRole === 'system' ? ai_1.ChatMessageRole.system : ai_1.ChatMessageRole.user,
423
+ content: renderedPrompt,
424
+ });
425
+ }
426
+ if (conversationMessages && conversationMessages.length > 0) {
427
+ messages.push(...conversationMessages);
428
+ }
429
+ if ((!conversationMessages || conversationMessages.length === 0) && templateMessageRole !== 'user' && renderedPrompt) {
430
+ if (templateMessageRole === 'system') {
431
+ messages.push({
432
+ role: ai_1.ChatMessageRole.user,
433
+ content: 'Please proceed with the above instructions.',
434
+ });
435
+ }
436
+ }
437
+ else if ((!conversationMessages || conversationMessages.length === 0) && !renderedPrompt) {
438
+ messages.push({
439
+ role: ai_1.ChatMessageRole.user,
440
+ content: 'Hello',
441
+ });
442
+ }
443
+ return messages;
444
+ }
445
+ async executeWithValidationRetries(selectedModel, renderedPromptText, prompt, params, promptRun) {
446
+ const validationAttempts = [];
447
+ const maxRetries = Math.max(0, prompt.MaxRetries || 0);
448
+ let lastError = null;
449
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
450
+ try {
451
+ if (params.cancellationToken?.aborted) {
452
+ throw new Error('Execution was cancelled during validation retries');
453
+ }
454
+ if (attempt > 0) {
455
+ (0, core_1.LogStatus)(`🔄 Retrying execution due to validation failure, attempt ${attempt + 1}/${maxRetries + 1}`);
456
+ await this.applyRetryDelay(prompt, attempt);
457
+ }
458
+ const modelResult = await this.executeModel(selectedModel, renderedPromptText, prompt, params.contextUser, params.conversationMessages, params.templateMessageRole || 'system', params.cancellationToken);
459
+ const { result, validationResult, validationErrors } = await this.parseAndValidateResultEnhanced(modelResult, prompt, params.skipValidation);
460
+ const validationAttempt = {
461
+ attemptNumber: attempt + 1,
462
+ success: validationResult?.Success || false,
463
+ errorMessage: validationErrors?.length ? validationErrors.map(e => e.Message).join('; ') : undefined,
464
+ validationErrors,
465
+ rawOutput: modelResult.data?.choices?.[0]?.message?.content || '',
466
+ parsedOutput: result,
467
+ timestamp: new Date(),
468
+ };
469
+ validationAttempts.push(validationAttempt);
470
+ await this.updatePromptRunWithValidationAttempt(promptRun, validationAttempt, attempt + 1, maxRetries + 1);
471
+ if (validationResult?.Success !== false) {
472
+ (0, core_1.LogStatus)(`✅ Validation succeeded on attempt ${attempt + 1}/${maxRetries + 1}`);
473
+ return {
474
+ modelResult,
475
+ parsedResult: { result, validationResult },
476
+ validationAttempts,
477
+ };
478
+ }
479
+ if (prompt.ValidationBehavior === 'Strict' && attempt < maxRetries) {
480
+ lastError = new Error(`Validation failed: ${validationErrors?.map(e => e.Message).join('; ')}`);
481
+ (0, core_1.LogStatus)(`❌ Validation failed on attempt ${attempt + 1}, will retry (Strict mode)`);
482
+ continue;
483
+ }
484
+ else {
485
+ const reason = prompt.ValidationBehavior !== 'Strict'
486
+ ? `${prompt.ValidationBehavior} mode - continuing with invalid output`
487
+ : 'max retries exceeded';
488
+ (0, core_1.LogStatus)(`⚠️ Validation failed on attempt ${attempt + 1}, stopping retries (${reason})`);
489
+ return {
490
+ modelResult,
491
+ parsedResult: { result, validationResult },
492
+ validationAttempts,
493
+ };
494
+ }
495
+ }
496
+ catch (error) {
497
+ lastError = error;
498
+ (0, core_1.LogError)(`Execution attempt ${attempt + 1} failed: ${error.message}`);
499
+ const validationAttempt = {
500
+ attemptNumber: attempt + 1,
501
+ success: false,
502
+ errorMessage: error.message,
503
+ rawOutput: '',
504
+ timestamp: new Date(),
505
+ };
506
+ validationAttempts.push(validationAttempt);
507
+ await this.updatePromptRunWithValidationAttempt(promptRun, validationAttempt, attempt + 1, maxRetries + 1);
508
+ if (attempt === maxRetries) {
509
+ throw error;
510
+ }
511
+ }
512
+ }
513
+ throw lastError || new Error('Execution failed after all retry attempts');
514
+ }
515
+ async applyRetryDelay(prompt, attemptNumber) {
516
+ const baseDelay = prompt.RetryDelayMS || 1000;
517
+ let delay = baseDelay;
518
+ switch (prompt.RetryStrategy) {
519
+ case 'Fixed':
520
+ delay = baseDelay;
521
+ break;
522
+ case 'Linear':
523
+ delay = baseDelay * attemptNumber;
524
+ break;
525
+ case 'Exponential':
526
+ delay = baseDelay * Math.pow(2, attemptNumber - 1);
527
+ break;
528
+ default:
529
+ delay = baseDelay;
530
+ }
531
+ (0, core_1.LogStatus)(`Applying retry delay: ${delay}ms (strategy: ${prompt.RetryStrategy})`);
532
+ await new Promise(resolve => setTimeout(resolve, delay));
533
+ }
534
+ async updatePromptRunWithValidationAttempt(promptRun, attempt, currentAttempt, totalAttempts) {
535
+ try {
536
+ const currentMessages = promptRun.Messages ? JSON.parse(promptRun.Messages) : {};
537
+ if (!currentMessages.validationAttempts) {
538
+ currentMessages.validationAttempts = [];
539
+ }
540
+ currentMessages.validationAttempts.push({
541
+ attempt: currentAttempt,
542
+ totalAttempts,
543
+ success: attempt.success,
544
+ timestamp: attempt.timestamp.toISOString(),
545
+ errorMessage: attempt.errorMessage,
546
+ validationErrorCount: attempt.validationErrors?.length || 0,
547
+ rawOutput: attempt.rawOutput?.substring(0, 500) + (attempt.rawOutput?.length > 500 ? '...' : ''),
548
+ parsedOutput: attempt.parsedOutput ? JSON.stringify(attempt.parsedOutput).substring(0, 500) : undefined,
549
+ validationErrors: attempt.validationErrors?.map(e => ({
550
+ source: e.Source,
551
+ message: e.Message,
552
+ type: e.Type,
553
+ value: typeof e.Value === 'string' ? e.Value.substring(0, 100) : e.Value
554
+ })) || []
555
+ });
556
+ promptRun.Messages = JSON.stringify(currentMessages);
557
+ (0, core_1.LogStatus)(`Recorded validation attempt ${currentAttempt}/${totalAttempts} for prompt run ${promptRun.ID}`);
558
+ }
559
+ catch (error) {
560
+ (0, core_1.LogError)(`Error updating prompt run with validation attempt: ${error.message}`);
561
+ }
562
+ }
563
+ getValidationDecisionDescription(finalSuccess, totalAttempts, validationBehavior) {
564
+ if (finalSuccess) {
565
+ return totalAttempts === 1
566
+ ? 'Validation passed on first attempt'
567
+ : `Validation passed after ${totalAttempts} attempts`;
568
+ }
569
+ else {
570
+ switch (validationBehavior) {
571
+ case 'Strict':
572
+ return `Validation failed after ${totalAttempts} attempts - execution marked as failed (Strict mode)`;
573
+ case 'Warn':
574
+ return `Validation failed after ${totalAttempts} attempts - warning logged, execution continued (Warn mode)`;
575
+ case 'None':
576
+ return `Validation skipped or ignored (None mode)`;
577
+ default:
578
+ return `Validation failed after ${totalAttempts} attempts - behavior: ${validationBehavior}`;
579
+ }
580
+ }
581
+ }
582
+ generateSchemaFromExample(example) {
583
+ if (typeof example !== 'object' || example === null) {
584
+ return { type: 'object' };
585
+ }
586
+ const schema = {
587
+ type: 'object',
588
+ properties: {},
589
+ required: [],
590
+ additionalProperties: false,
591
+ };
592
+ for (const [key, value] of Object.entries(example)) {
593
+ schema.properties[key] = this.generateSchemaForValue(value);
594
+ schema.required.push(key);
595
+ }
596
+ return schema;
597
+ }
598
+ generateSchemaForValue(value) {
599
+ if (value === null) {
600
+ return { type: 'null' };
601
+ }
602
+ switch (typeof value) {
603
+ case 'string':
604
+ return { type: 'string' };
605
+ case 'number':
606
+ return { type: 'number' };
607
+ case 'boolean':
608
+ return { type: 'boolean' };
609
+ case 'object':
610
+ if (Array.isArray(value)) {
611
+ if (value.length > 0) {
612
+ return {
613
+ type: 'array',
614
+ items: this.generateSchemaForValue(value[0]),
615
+ };
616
+ }
617
+ else {
618
+ return { type: 'array' };
619
+ }
620
+ }
621
+ else {
622
+ return this.generateSchemaFromExample(value);
623
+ }
624
+ default:
625
+ return { type: 'string' };
626
+ }
627
+ }
628
+ async parseAndValidateResultEnhanced(modelResult, prompt, skipValidation = false) {
629
+ const validationErrors = [];
630
+ try {
631
+ if (!modelResult.success) {
632
+ throw new Error(`Model execution failed: ${modelResult.errorMessage}`);
633
+ }
634
+ const rawOutput = modelResult.data?.choices?.[0]?.message?.content;
635
+ if (!rawOutput) {
636
+ throw new Error('No output received from model');
637
+ }
638
+ let parsedResult = rawOutput;
639
+ try {
640
+ switch (prompt.OutputType) {
641
+ case 'string':
642
+ parsedResult = rawOutput.toString();
643
+ break;
644
+ case 'number': {
645
+ const numberResult = parseFloat(rawOutput);
646
+ if (isNaN(numberResult)) {
647
+ const error = new core_1.ValidationErrorInfo('output', `Expected number output but got: ${rawOutput}`, rawOutput, core_1.ValidationErrorType.Failure);
648
+ validationErrors.push(error);
649
+ throw new Error(error.Message);
650
+ }
651
+ parsedResult = numberResult;
652
+ break;
653
+ }
654
+ case 'boolean': {
655
+ const lowerOutput = rawOutput.toLowerCase().trim();
656
+ if (['true', 'yes', '1'].includes(lowerOutput)) {
657
+ parsedResult = true;
658
+ }
659
+ else if (['false', 'no', '0'].includes(lowerOutput)) {
660
+ parsedResult = false;
661
+ }
662
+ else {
663
+ const error = new core_1.ValidationErrorInfo('output', `Expected boolean output but got: ${rawOutput}`, rawOutput, core_1.ValidationErrorType.Failure);
664
+ validationErrors.push(error);
665
+ throw new Error(error.Message);
666
+ }
667
+ break;
668
+ }
669
+ case 'date': {
670
+ const dateResult = new Date(rawOutput);
671
+ if (isNaN(dateResult.getTime())) {
672
+ const error = new core_1.ValidationErrorInfo('output', `Expected date output but got: ${rawOutput}`, rawOutput, core_1.ValidationErrorType.Failure);
673
+ validationErrors.push(error);
674
+ throw new Error(error.Message);
675
+ }
676
+ parsedResult = dateResult;
677
+ break;
678
+ }
679
+ case 'object':
680
+ try {
681
+ parsedResult = JSON.parse(rawOutput);
682
+ }
683
+ catch (jsonError) {
684
+ const error = new core_1.ValidationErrorInfo('output', `Expected JSON object but got invalid JSON: ${rawOutput}`, rawOutput, core_1.ValidationErrorType.Failure);
685
+ validationErrors.push(error);
686
+ throw new Error(error.Message);
687
+ }
688
+ break;
689
+ default:
690
+ parsedResult = rawOutput;
691
+ }
692
+ }
693
+ catch (parseError) {
694
+ const validationResult = new core_1.ValidationResult();
695
+ validationResult.Success = false;
696
+ validationResult.Errors = validationErrors;
697
+ return { result: rawOutput, validationResult, validationErrors };
698
+ }
699
+ if (!skipValidation && prompt.OutputExample && prompt.OutputType === 'object' && parsedResult) {
700
+ try {
701
+ const schemaValidationErrors = await this.validateAgainstSchema(parsedResult, prompt.OutputExample, prompt.ID);
702
+ validationErrors.push(...schemaValidationErrors);
703
+ }
704
+ catch (schemaError) {
705
+ const error = new core_1.ValidationErrorInfo('schema', `Schema validation failed: ${schemaError.message}`, undefined, core_1.ValidationErrorType.Failure);
706
+ validationErrors.push(error);
707
+ }
708
+ }
709
+ const validationResult = new core_1.ValidationResult();
710
+ validationResult.Success = validationErrors.length === 0;
711
+ validationResult.Errors = validationErrors;
712
+ return { result: parsedResult, validationResult, validationErrors };
713
+ }
714
+ catch (error) {
715
+ (0, core_1.LogError)(`Error parsing/validating result: ${error.message}`);
716
+ const validationResult = new core_1.ValidationResult();
717
+ validationResult.Success = false;
718
+ validationResult.Errors = validationErrors.length > 0 ? validationErrors : [
719
+ new core_1.ValidationErrorInfo('general', error.message, undefined, core_1.ValidationErrorType.Failure)
720
+ ];
721
+ switch (prompt.ValidationBehavior) {
722
+ case 'Strict':
723
+ return { result: undefined, validationResult, validationErrors: validationResult.Errors };
724
+ case 'Warn':
725
+ (0, core_1.LogError)(`Validation warning for prompt ${prompt.Name}: ${error.message}`);
726
+ return { result: modelResult.data?.choices?.[0]?.message?.content, validationResult, validationErrors: validationResult.Errors };
727
+ case 'None':
728
+ default:
729
+ validationResult.Success = true;
730
+ return { result: modelResult.data?.choices?.[0]?.message?.content, validationResult, validationErrors: [] };
731
+ }
732
+ }
733
+ }
734
+ async validateAgainstSchema(parsedResult, outputExample, promptId) {
735
+ const validationErrors = [];
736
+ try {
737
+ let validator = AIPromptRunner._schemaCache.get(promptId);
738
+ if (!validator) {
739
+ let exampleObject;
740
+ try {
741
+ exampleObject = JSON.parse(outputExample);
742
+ }
743
+ catch (parseError) {
744
+ const error = new core_1.ValidationErrorInfo('outputExample', `Invalid OutputExample JSON: ${parseError.message}`, outputExample, core_1.ValidationErrorType.Failure);
745
+ validationErrors.push(error);
746
+ return validationErrors;
747
+ }
748
+ const schema = this.generateSchemaFromExample(exampleObject);
749
+ try {
750
+ validator = this._ajv.compile(schema);
751
+ AIPromptRunner._schemaCache.set(promptId, validator);
752
+ const cacheStats = AIPromptRunner.getSchemaCacheStats();
753
+ (0, core_1.LogStatus)(`📋 Compiled and cached JSON schema for prompt ${promptId} (global cache size: ${cacheStats.size})`);
754
+ }
755
+ catch (compileError) {
756
+ const error = new core_1.ValidationErrorInfo('schema', `Failed to compile schema: ${compileError.message}`, schema, core_1.ValidationErrorType.Failure);
757
+ validationErrors.push(error);
758
+ return validationErrors;
759
+ }
760
+ }
761
+ const isValid = validator(parsedResult);
762
+ if (!isValid && validator.errors) {
763
+ for (const ajvError of validator.errors) {
764
+ const fieldPath = ajvError.instancePath || ajvError.schemaPath || 'root';
765
+ const message = `${ajvError.instancePath || 'root'}: ${ajvError.message}`;
766
+ const error = new core_1.ValidationErrorInfo(fieldPath, message, ajvError.data, core_1.ValidationErrorType.Failure);
767
+ validationErrors.push(error);
768
+ }
769
+ }
770
+ if (validationErrors.length === 0) {
771
+ (0, core_1.LogStatus)(`✅ Schema validation passed for prompt ${promptId}`);
772
+ }
773
+ else {
774
+ (0, core_1.LogStatus)(`❌ Schema validation found ${validationErrors.length} errors for prompt ${promptId}:`);
775
+ validationErrors.forEach((error, index) => {
776
+ (0, core_1.LogStatus)(` ${index + 1}. ${error.Source}: ${error.Message}`);
777
+ });
778
+ }
779
+ }
780
+ catch (error) {
781
+ const validationError = new core_1.ValidationErrorInfo('validation', `Unexpected validation error: ${error.message}`, undefined, core_1.ValidationErrorType.Failure);
782
+ validationErrors.push(validationError);
783
+ }
784
+ return validationErrors;
785
+ }
319
786
  async parseAndValidateResult(modelResult, prompt, skipValidation = false) {
320
787
  try {
321
788
  if (!modelResult.success) {
@@ -330,13 +797,15 @@ class AIPromptRunner {
330
797
  case 'string':
331
798
  parsedResult = rawOutput.toString();
332
799
  break;
333
- case 'number':
334
- parsedResult = parseFloat(rawOutput);
335
- if (isNaN(parsedResult)) {
800
+ case 'number': {
801
+ const numberResult = parseFloat(rawOutput);
802
+ if (isNaN(numberResult)) {
336
803
  throw new Error(`Expected number output but got: ${rawOutput}`);
337
804
  }
805
+ parsedResult = numberResult;
338
806
  break;
339
- case 'boolean':
807
+ }
808
+ case 'boolean': {
340
809
  const lowerOutput = rawOutput.toLowerCase().trim();
341
810
  if (['true', 'yes', '1'].includes(lowerOutput)) {
342
811
  parsedResult = true;
@@ -348,12 +817,15 @@ class AIPromptRunner {
348
817
  throw new Error(`Expected boolean output but got: ${rawOutput}`);
349
818
  }
350
819
  break;
351
- case 'date':
352
- parsedResult = new Date(rawOutput);
353
- if (isNaN(parsedResult.getTime())) {
820
+ }
821
+ case 'date': {
822
+ const dateResult = new Date(rawOutput);
823
+ if (isNaN(dateResult.getTime())) {
354
824
  throw new Error(`Expected date output but got: ${rawOutput}`);
355
825
  }
826
+ parsedResult = dateResult;
356
827
  break;
828
+ }
357
829
  case 'object':
358
830
  try {
359
831
  parsedResult = JSON.parse(rawOutput);
@@ -385,18 +857,45 @@ class AIPromptRunner {
385
857
  }
386
858
  }
387
859
  }
388
- async updatePromptRun(promptRun, modelResult, parsedResult, endTime, executionTimeMS) {
860
+ async updatePromptRun(promptRun, modelResult, parsedResult, endTime, executionTimeMS, validationAttempts) {
389
861
  try {
390
862
  promptRun.CompletedAt = endTime;
391
863
  promptRun.ExecutionTimeMS = executionTimeMS;
392
- promptRun.Result = typeof parsedResult.result === 'string'
393
- ? parsedResult.result
394
- : JSON.stringify(parsedResult.result);
864
+ promptRun.Result = typeof parsedResult.result === 'string' ? parsedResult.result : JSON.stringify(parsedResult.result);
395
865
  if (modelResult.data?.usage) {
396
866
  promptRun.TokensUsed = modelResult.data.usage.totalTokens;
397
867
  promptRun.TokensPrompt = modelResult.data.usage.promptTokens;
398
868
  promptRun.TokensCompletion = modelResult.data.usage.completionTokens;
399
869
  }
870
+ if (validationAttempts && validationAttempts.length > 0) {
871
+ try {
872
+ const currentMessages = promptRun.Messages ? JSON.parse(promptRun.Messages) : {};
873
+ currentMessages.validationSummary = {
874
+ totalAttempts: validationAttempts.length,
875
+ successfulAttempts: validationAttempts.filter(a => a.success).length,
876
+ finalValidationSuccess: parsedResult.validationResult?.Success || false,
877
+ validationBehavior: promptRun.Messages ? 'Logged during execution' : 'Updated at completion',
878
+ outputType: currentMessages.data?.prompt?.OutputType || 'unknown',
879
+ hasOutputExample: !!(currentMessages.data?.prompt?.OutputExample),
880
+ schemaValidationUsed: !!(currentMessages.data?.prompt?.OutputExample && currentMessages.data?.prompt?.OutputType === 'object'),
881
+ retryStrategy: currentMessages.data?.prompt?.RetryStrategy || 'Fixed',
882
+ maxRetries: currentMessages.data?.prompt?.MaxRetries || 0,
883
+ finalValidationErrors: parsedResult.validationResult?.Errors?.map(e => ({
884
+ source: e.Source,
885
+ message: e.Message,
886
+ type: e.Type,
887
+ value: e.Value
888
+ })) || [],
889
+ validationDecision: this.getValidationDecisionDescription(parsedResult.validationResult?.Success || false, validationAttempts.length, currentMessages.data?.prompt?.ValidationBehavior || 'Warn')
890
+ };
891
+ promptRun.Messages = JSON.stringify(currentMessages);
892
+ (0, core_1.LogStatus)(`Updated prompt run ${promptRun.ID} with ${validationAttempts.length} validation attempts`);
893
+ }
894
+ catch (jsonError) {
895
+ (0, core_1.LogError)(`Error updating Messages field with validation info: ${jsonError.message}`);
896
+ }
897
+ }
898
+ promptRun.Success = modelResult.success && (parsedResult.validationResult?.Success !== false);
400
899
  const saveResult = await promptRun.Save();
401
900
  if (!saveResult) {
402
901
  (0, core_1.LogError)(`Failed to update AIPromptRun with results: ${promptRun.LatestResult?.Message || 'Unknown error'}`);
@@ -408,6 +907,8 @@ class AIPromptRunner {
408
907
  }
409
908
  }
410
909
  exports.AIPromptRunner = AIPromptRunner;
910
+ AIPromptRunner._schemaCache = new Map();
911
+ AIPromptRunner._ajvInstance = null;
411
912
  function LoadAIPromptRunner() {
412
913
  }
413
914
  exports.LoadAIPromptRunner = LoadAIPromptRunner;