UNPKG

@memberjunction/ng-ai-test-harness

Version:

MemberJunction AI Test Harness - A reusable component for testing AI agents and prompts with beautiful UX

330 lines 19.1 kB
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; import { BaseAngularComponent } from '@memberjunction/ng-base-types'; 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 extends BaseAngularComponent { constructor(cdr) { super(); 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 = this.ProviderToUse; // 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('MJ: 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('MJ: 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 = this.ProviderToUse; 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('MJ: 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(__ngFactoryType__) { return new (__ngFactoryType__ || 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" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], 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: [{ standalone: false, 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: 140 }); })(); //# sourceMappingURL=ai-test-harness-dialog.component.js.map