@memberjunction/ng-ai-test-harness
Version:
MemberJunction AI Test Harness - A reusable component for testing AI agents and prompts with beautiful UX
329 lines • 18.9 kB
JavaScript
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Metadata } from '@memberjunction/core';
import * as i0 from "@angular/core";
import * as i1 from "./ai-test-harness.component";
const _c0 = ["testHarness"];
/**
* Dialog wrapper component for the AI Agent Test Harness.
* Provides a modal dialog interface with proper sizing, header, and close functionality.
* Automatically loads agent data and initializes the test harness with provided configuration.
*
* ## Features:
* - **Automatic Agent Loading**: Loads agent by ID or uses provided entity
* - **Configurable Dimensions**: Supports custom dialog sizing
* - **Initial Data Setup**: Pre-populates data context and template variables
* - **Clean Dialog Interface**: Professional header with close button
* - **Responsive Layout**: Adapts to content and screen size
*
* ## Usage:
* This component is typically opened through the `TestHarnessDialogService` rather than directly:
* ```typescript
* const dialogRef = this.testHarnessService.openAgentTestHarness({
* agentId: 'agent-123',
* initialDataContext: { userId: 'user-456' }
* });
* ```
*/
export class AITestHarnessDialogComponent {
constructor(cdr) {
this.cdr = cdr;
/** The loaded AI agent entity for testing */
this.agent = null;
/** The loaded AI prompt entity for testing */
this.prompt = null;
/** The mode of operation - either 'agent' or 'prompt' */
this.mode = 'agent';
/** Display title for the dialog header */
this.title = 'AI Test Harness';
/** Configuration data passed from the dialog service */
this.data = {};
/** Event emitted when the dialog should be closed */
this.closeDialog = new EventEmitter();
}
/**
* Initializes the dialog component by loading agent/prompt data and configuring
* the embedded test harness with initial variables and settings.
*/
async ngOnInit() {
// Set mode from data
if (this.data.mode) {
this.mode = this.data.mode;
}
if (this.data.title) {
this.title = this.data.title;
}
const md = new Metadata();
// Load entity based on mode
if (this.mode === 'agent' || (!this.data.promptId && !this.data.prompt)) {
// Agent mode
if (this.data.agentId && !this.data.agent) {
this.agent = await md.GetEntityObject('AI Agents');
await this.agent.Load(this.data.agentId);
if (this.agent) {
this.title = this.title || `Test Harness: ${this.agent.Name}`;
}
}
else if (this.data.agent) {
this.agent = this.data.agent;
this.title = this.title || `Test Harness: ${this.agent.Name}`;
}
}
else {
// Prompt mode
this.mode = 'prompt';
if (this.data.promptId && !this.data.prompt) {
this.prompt = await md.GetEntityObject('AI Prompts');
await this.prompt.Load(this.data.promptId);
if (this.prompt) {
this.title = this.title || `Test Harness: ${this.prompt.Name}`;
}
}
else if (this.data.prompt) {
this.prompt = this.data.prompt;
this.title = this.title || `Test Harness: ${this.prompt.Name}`;
}
}
}
/**
* AfterViewInit lifecycle hook to set initial data after view is initialized
*/
async ngAfterViewInit() {
console.log('🚀 ngAfterViewInit - testHarness available:', !!this.testHarness);
console.log('📊 Dialog data:', this.data);
console.log('🎯 Mode:', this.mode);
if (this.testHarness) {
// Check if we need to load from a prompt run
if (this.data.promptRunId && this.mode === 'prompt') {
console.log('🔄 Loading from prompt run in AfterViewInit:', this.data.promptRunId);
await this.loadFromPromptRun(this.data.promptRunId);
}
else {
console.log('📌 Not loading from prompt run - promptRunId:', this.data.promptRunId, 'mode:', this.mode);
if (this.mode === 'agent') {
// Agent mode: set agent variables
if (this.data.initialDataContext) {
const variables = Object.entries(this.data.initialDataContext).map(([name, value]) => ({
name,
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
type: this.detectVariableType(value)
}));
this.testHarness.agentVariables = variables;
}
if (this.data.initialTemplateData) {
const templateVariables = Object.entries(this.data.initialTemplateData).map(([name, value]) => ({
name,
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
type: this.detectVariableType(value)
}));
this.testHarness.agentVariables = [...this.testHarness.agentVariables, ...templateVariables];
}
}
else {
// Prompt mode: set template variables
if (this.data.initialTemplateVariables) {
const variables = Object.entries(this.data.initialTemplateVariables).map(([name, value]) => ({
name,
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
type: this.detectVariableType(value)
}));
this.testHarness.templateVariables = variables;
}
// Set selected model if provided
if (this.data.selectedModelId) {
this.testHarness.selectedModelId = this.data.selectedModelId;
}
if (this.data.selectedVendorId) {
this.testHarness.selectedVendorId = this.data.selectedVendorId;
}
if (this.data.selectedConfigurationId) {
this.testHarness.selectedConfigurationId = this.data.selectedConfigurationId;
}
}
}
// Trigger change detection to ensure view updates
console.log('🔄 Triggering change detection');
this.cdr.detectChanges();
// Check after change detection
setTimeout(() => {
console.log('⏱️ After timeout - conversationMessages:', this.testHarness?.conversationMessages);
console.log('⏱️ Test harness component state:', {
mode: this.testHarness?.mode,
entity: this.testHarness?.entity?.Name,
messagesLength: this.testHarness?.conversationMessages?.length
});
}, 100);
}
}
/**
* Determines the appropriate variable type for initial data configuration.
* @param value - The value to analyze for type detection
* @returns The detected variable type
* @private
*/
detectVariableType(value) {
if (typeof value === 'boolean')
return 'boolean';
if (typeof value === 'number')
return 'number';
if (typeof value === 'object')
return 'object';
return 'string';
}
/**
* Loads data from an existing prompt run to pre-populate the test harness
* @param promptRunId - The ID of the prompt run to load
*/
async loadFromPromptRun(promptRunId) {
console.log('🔄 Loading from prompt run:', promptRunId);
const md = new Metadata();
const promptRun = await md.GetEntityObject('MJ: AI Prompt Runs');
if (await promptRun.Load(promptRunId)) {
console.log('✅ Prompt run loaded successfully');
// Load the prompt if not already loaded
if (!this.prompt && promptRun.PromptID) {
this.prompt = await md.GetEntityObject('AI Prompts');
await this.prompt.Load(promptRun.PromptID);
this.testHarness.entity = this.prompt;
// Update title to indicate we're re-running
this.title = `Re-Run: ${this.prompt.Name}`;
}
// Set the model/vendor/configuration
if (promptRun.ModelID) {
this.testHarness.selectedModelId = promptRun.ModelID;
}
if (promptRun.VendorID) {
this.testHarness.selectedVendorId = promptRun.VendorID;
}
if (promptRun.ConfigurationID) {
this.testHarness.selectedConfigurationId = promptRun.ConfigurationID;
}
// Note: We do NOT extract template variables because we want to use
// the already-rendered system prompt from the previous run, not re-render it
// Set advanced parameters
if (promptRun.Temperature != null) {
this.testHarness.advancedParams.temperature = promptRun.Temperature;
}
if (promptRun.TopP != null) {
this.testHarness.advancedParams.topP = promptRun.TopP;
}
if (promptRun.TopK != null) {
this.testHarness.advancedParams.topK = promptRun.TopK;
}
if (promptRun.MinP != null) {
this.testHarness.advancedParams.minP = promptRun.MinP;
}
if (promptRun.FrequencyPenalty != null) {
this.testHarness.advancedParams.frequencyPenalty = promptRun.FrequencyPenalty;
}
if (promptRun.PresencePenalty != null) {
this.testHarness.advancedParams.presencePenalty = promptRun.PresencePenalty;
}
if (promptRun.Seed != null) {
this.testHarness.advancedParams.seed = promptRun.Seed;
}
// Note: responseFormat is handled separately, not in advancedParams
// Use the extended entity methods to get conversation messages
console.log('📝 Raw Messages field:', promptRun.Messages);
const parsedData = promptRun.ParseMessagesData();
console.log('🔍 Parsed messages data:', parsedData);
const chatMessages = promptRun.GetChatMessages();
console.log('💬 Extracted chat messages:', chatMessages);
if (chatMessages.length > 0) {
// Convert messages to the format expected by the test harness
const convertedMessages = chatMessages.map((msg, index) => ({
id: `msg-${Date.now()}-${index}`,
role: msg.role,
content: typeof msg.content === 'string' ? msg.content :
Array.isArray(msg.content) ?
msg.content.filter(block => block.type === 'text').map(block => block.content).join('\n') :
'',
timestamp: new Date()
}));
console.log('🎯 Converted messages for test harness:', convertedMessages);
this.testHarness.conversationMessages = convertedMessages;
console.log('✅ Test harness conversationMessages set:', this.testHarness.conversationMessages);
}
else {
console.log('⚠️ No chat messages found in prompt run');
}
// Store the original prompt run ID for reference
this.testHarness.originalPromptRunId = promptRunId;
// Extract and store the system prompt for re-run
const systemPrompt = promptRun.GetSystemPrompt();
if (systemPrompt) {
this.testHarness.systemPromptOverride = systemPrompt;
}
// Add a note indicating this is a re-run
if (this.testHarness.conversationMessages.length > 0) {
// Add a system message indicating this is a re-run
this.testHarness.conversationMessages.unshift({
id: `system-${Date.now()}`,
role: 'system',
content: `[Re-running from Prompt Run #${promptRunId.substring(0, 8)}]`,
timestamp: new Date()
});
}
}
}
/**
* Closes the dialog by emitting the close event.
* This method is called by the close button in the header.
*/
close() {
this.closeDialog.emit();
}
static { this.ɵfac = function AITestHarnessDialogComponent_Factory(t) { return new (t || AITestHarnessDialogComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); }; }
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AITestHarnessDialogComponent, selectors: [["mj-ai-test-harness-dialog"]], viewQuery: function AITestHarnessDialogComponent_Query(rf, ctx) { if (rf & 1) {
i0.ɵɵviewQuery(_c0, 5);
} if (rf & 2) {
let _t;
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.testHarness = _t.first);
} }, inputs: { data: "data" }, outputs: { closeDialog: "closeDialog" }, decls: 9, vars: 4, consts: [["testHarness", ""], [1, "test-harness-dialog"], [1, "dialog-header"], [1, "close-button", 3, "click"], [1, "fa-solid", "fa-times"], [1, "dialog-content"], [3, "mode", "entity", "isVisible"]], template: function AITestHarnessDialogComponent_Template(rf, ctx) { if (rf & 1) {
const _r1 = i0.ɵɵgetCurrentView();
i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "h2");
i0.ɵɵtext(3);
i0.ɵɵelementEnd();
i0.ɵɵelementStart(4, "button", 3);
i0.ɵɵlistener("click", function AITestHarnessDialogComponent_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.close()); });
i0.ɵɵelement(5, "i", 4);
i0.ɵɵelementEnd()();
i0.ɵɵelementStart(6, "div", 5);
i0.ɵɵelement(7, "mj-ai-test-harness", 6, 0);
i0.ɵɵelementEnd()();
} if (rf & 2) {
i0.ɵɵadvance(3);
i0.ɵɵtextInterpolate(ctx.title);
i0.ɵɵadvance(4);
i0.ɵɵproperty("mode", ctx.mode)("entity", ctx.mode === "agent" ? ctx.agent : ctx.prompt)("isVisible", true);
} }, dependencies: [i1.AITestHarnessComponent], styles: [".test-harness-dialog[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n\n .dialog-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px 24px;\n border-bottom: 1px solid #e0e0e0;\n background-color: #f5f5f5;\n }\n\n .dialog-header[_ngcontent-%COMP%] h2[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 20px;\n font-weight: 500;\n }\n\n .close-button[_ngcontent-%COMP%] {\n position: relative;\n top: -4px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: background-color 0.2s;\n }\n \n .close-button[_ngcontent-%COMP%]:hover {\n background-color: rgba(0, 0, 0, 0.04);\n }\n\n .dialog-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n padding: 0;\n }\n\n [_nghost-%COMP%] .test-harness-container {\n height: 100%;\n }"] }); }
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AITestHarnessDialogComponent, [{
type: Component,
args: [{ selector: 'mj-ai-test-harness-dialog', template: `
<div class="test-harness-dialog">
<div class="dialog-header">
<h2>{{ title }}</h2>
<button class="close-button" (click)="close()">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="dialog-content">
<mj-ai-test-harness
#testHarness
[mode]="mode"
[entity]="mode === 'agent' ? agent : prompt"
[isVisible]="true">
</mj-ai-test-harness>
</div>
</div>
`, styles: ["\n .test-harness-dialog {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n\n .dialog-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px 24px;\n border-bottom: 1px solid #e0e0e0;\n background-color: #f5f5f5;\n }\n\n .dialog-header h2 {\n margin: 0;\n font-size: 20px;\n font-weight: 500;\n }\n\n .close-button {\n position: relative;\n top: -4px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: background-color 0.2s;\n }\n \n .close-button:hover {\n background-color: rgba(0, 0, 0, 0.04);\n }\n\n .dialog-content {\n flex: 1;\n overflow: hidden;\n padding: 0;\n }\n\n :host ::ng-deep .test-harness-container {\n height: 100%;\n }\n "] }]
}], () => [{ type: i0.ChangeDetectorRef }], { testHarness: [{
type: ViewChild,
args: ['testHarness', { static: false }]
}], data: [{
type: Input
}], closeDialog: [{
type: Output
}] }); })();
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AITestHarnessDialogComponent, { className: "AITestHarnessDialogComponent", filePath: "lib/ai-test-harness-dialog.component.ts", lineNumber: 138 }); })();
//# sourceMappingURL=ai-test-harness-dialog.component.js.map