@operor/testing 0.1.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.
- package/API_VALIDATION.md +572 -0
- package/dist/index.d.ts +414 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1608 -0
- package/dist/index.js.map +1 -0
- package/fixtures/sample-tests.csv +10 -0
- package/package.json +31 -0
- package/src/CSVLoader.ts +83 -0
- package/src/ConversationEvaluator.ts +254 -0
- package/src/ConversationRunner.ts +267 -0
- package/src/CustomerSimulator.ts +106 -0
- package/src/MockShopifySkill.ts +336 -0
- package/src/SimulationRunner.ts +425 -0
- package/src/SkillTestHarness.ts +220 -0
- package/src/TestCaseEvaluator.ts +296 -0
- package/src/TestSuiteRunner.ts +151 -0
- package/src/__tests__/CSVLoader.test.ts +122 -0
- package/src/__tests__/ConversationEvaluator.test.ts +221 -0
- package/src/__tests__/ConversationRunner.test.ts +270 -0
- package/src/__tests__/CustomerSimulator.test.ts +160 -0
- package/src/__tests__/SimulationRunner.test.ts +281 -0
- package/src/__tests__/SkillTestHarness.test.ts +181 -0
- package/src/__tests__/scenarios.test.ts +71 -0
- package/src/index.ts +32 -0
- package/src/scenarios/edge-cases.ts +52 -0
- package/src/scenarios/general.ts +37 -0
- package/src/scenarios/index.ts +32 -0
- package/src/scenarios/order-tracking.ts +56 -0
- package/src/scenarios.ts +142 -0
- package/src/types.ts +133 -0
- package/src/utils.ts +6 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +10 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/CSVLoader.ts","../src/TestCaseEvaluator.ts","../src/TestSuiteRunner.ts","../src/SkillTestHarness.ts","../src/CustomerSimulator.ts","../src/ConversationEvaluator.ts","../src/utils.ts","../src/ConversationRunner.ts","../src/scenarios.ts","../src/SimulationRunner.ts","../src/MockShopifySkill.ts"],"sourcesContent":["import { parse } from 'csv-parse/sync';\nimport { readFile } from 'node:fs/promises';\nimport type { TestCase } from './types.js';\n\nexport class CSVLoader {\n static async fromFile(path: string): Promise<TestCase[]> {\n const content = await readFile(path, 'utf-8');\n if (path.endsWith('.json')) {\n return CSVLoader.fromJSON(content);\n }\n return CSVLoader.fromCSVString(content);\n }\n\n static fromCSVString(csv: string): TestCase[] {\n // Strip UTF-8 BOM\n const clean = csv.replace(/^\\uFEFF/, '');\n\n const records: Record<string, string>[] = parse(clean, {\n columns: true,\n skip_empty_lines: true,\n trim: true,\n relax_column_count: true,\n });\n\n return records.map((row, i) => {\n const id = row.id?.trim();\n const question = row.question?.trim();\n\n if (!id || !question) {\n throw new Error(\n `Row ${i + 1}: missing required field(s) — id and question are required`\n );\n }\n\n const testCase: TestCase = { id, question };\n\n if (row.expected_answer?.trim()) {\n testCase.expectedAnswer = row.expected_answer.trim();\n }\n if (row.expected_tools?.trim()) {\n testCase.expectedTools = row.expected_tools\n .split(',')\n .map((t: string) => t.trim())\n .filter(Boolean);\n }\n if (row.persona?.trim()) {\n testCase.persona = row.persona.trim();\n }\n if (row.tags?.trim()) {\n testCase.tags = row.tags\n .split(',')\n .map((t: string) => t.trim())\n .filter(Boolean);\n }\n\n return testCase;\n });\n }\n\n static fromJSON(json: string): TestCase[] {\n const data = JSON.parse(json);\n const arr = Array.isArray(data) ? data : data.testCases ?? data.tests;\n\n if (!Array.isArray(arr)) {\n throw new Error('JSON must be an array or contain a testCases/tests array');\n }\n\n return arr.map((item: any, i: number) => {\n if (!item.id || !item.question) {\n throw new Error(\n `Item ${i}: missing required field(s) — id and question are required`\n );\n }\n const testCase: TestCase = { id: item.id, question: item.question };\n if (item.expectedAnswer) testCase.expectedAnswer = item.expectedAnswer;\n if (item.expectedTools) testCase.expectedTools = item.expectedTools;\n if (item.persona) testCase.persona = item.persona;\n if (item.tags) testCase.tags = item.tags;\n if (item.metadata) testCase.metadata = item.metadata;\n return testCase;\n });\n }\n}\n","import type { LLMProvider } from '@operor/llm';\nimport type { TestCase } from './types.js';\n\nexport interface EvaluationResult {\n passed: boolean;\n score: number;\n method: 'exact' | 'contains' | 'similarity' | 'llm_judge';\n reasoning: string;\n toolsCorrect: boolean;\n}\n\nexport class TestCaseEvaluator {\n constructor(private llm?: LLMProvider) {}\n\n async evaluate(\n testCase: TestCase,\n agentResponse: string,\n toolsCalled: Array<{ name: string; params: any; result: any }>,\n strategy?: 'exact' | 'contains' | 'similarity' | 'semantic'\n ): Promise<EvaluationResult> {\n // Validate tools first\n const toolsCorrect = this.validateTools(testCase.expectedTools, toolsCalled);\n\n // Choose evaluation strategy based on explicit strategy parameter\n if (strategy === 'exact') {\n return this.evaluateByExact(testCase, agentResponse, toolsCorrect);\n }\n\n if (strategy === 'contains') {\n return this.evaluateByContains(testCase, agentResponse, toolsCorrect);\n }\n\n if (strategy === 'similarity') {\n return this.evaluateBySimilarity(testCase, agentResponse, toolsCorrect);\n }\n\n if (strategy === 'semantic' && this.llm) {\n if (testCase.expectedAnswer) {\n return await this.evaluateByLLMComparison(testCase, agentResponse, toolsCorrect);\n }\n return await this.evaluateByLLMJudge(testCase, agentResponse, toolsCorrect);\n }\n\n // Default behavior (no strategy specified)\n if (!this.llm) {\n // No LLM: use string similarity\n return this.evaluateBySimilarity(testCase, agentResponse, toolsCorrect);\n }\n\n if (testCase.expectedAnswer) {\n // LLM + expected answer: use LLM comparison\n return await this.evaluateByLLMComparison(testCase, agentResponse, toolsCorrect);\n }\n\n // LLM + no expected answer: use LLM standalone judge\n return await this.evaluateByLLMJudge(testCase, agentResponse, toolsCorrect);\n }\n\n private validateTools(\n expectedTools: string[] | undefined,\n toolsCalled: Array<{ name: string; params: any; result: any }>\n ): boolean {\n if (!expectedTools || expectedTools.length === 0) {\n return true; // No tools expected, always pass\n }\n\n const calledNames = new Set(toolsCalled.map((t) => t.name));\n return expectedTools.every((tool) => calledNames.has(tool));\n }\n\n private evaluateByExact(\n testCase: TestCase,\n agentResponse: string,\n toolsCorrect: boolean\n ): EvaluationResult {\n if (!testCase.expectedAnswer) {\n return {\n passed: toolsCorrect,\n score: toolsCorrect ? 1 : 0,\n method: 'exact',\n reasoning: 'No expected answer provided, evaluated tools only',\n toolsCorrect,\n };\n }\n\n const matches = agentResponse.trim().toLowerCase() === testCase.expectedAnswer.trim().toLowerCase();\n const passed = matches && toolsCorrect;\n\n return {\n passed,\n score: matches ? 1 : 0,\n method: 'exact',\n reasoning: matches ? 'Exact match' : 'Response does not exactly match expected answer',\n toolsCorrect,\n };\n }\n\n private evaluateByContains(\n testCase: TestCase,\n agentResponse: string,\n toolsCorrect: boolean\n ): EvaluationResult {\n if (!testCase.expectedAnswer) {\n return {\n passed: toolsCorrect,\n score: toolsCorrect ? 1 : 0,\n method: 'contains',\n reasoning: 'No expected answer provided, evaluated tools only',\n toolsCorrect,\n };\n }\n\n const normalizeDashes = (s: string) => s.replace(/[\\u2013\\u2011]/g, '-');\n const contains = normalizeDashes(agentResponse.toLowerCase()).includes(normalizeDashes(testCase.expectedAnswer.toLowerCase()));\n const passed = contains && toolsCorrect;\n\n return {\n passed,\n score: contains ? 1 : 0,\n method: 'contains',\n reasoning: contains\n ? `Response contains expected text: \"${testCase.expectedAnswer}\"`\n : `Response does not contain expected text: \"${testCase.expectedAnswer}\"`,\n toolsCorrect,\n };\n }\n\n private evaluateBySimilarity(\n testCase: TestCase,\n agentResponse: string,\n toolsCorrect: boolean\n ): EvaluationResult {\n if (!testCase.expectedAnswer) {\n // No expected answer and no LLM: can't evaluate, pass by default\n return {\n passed: toolsCorrect,\n score: toolsCorrect ? 1 : 0,\n method: 'similarity',\n reasoning: 'No expected answer provided, evaluated tools only',\n toolsCorrect,\n };\n }\n\n const similarity = this.normalizedLevenshtein(\n testCase.expectedAnswer.toLowerCase(),\n agentResponse.toLowerCase()\n );\n\n const passed = similarity > 0.7 && toolsCorrect;\n\n return {\n passed,\n score: similarity,\n method: 'similarity',\n reasoning: `String similarity: ${(similarity * 100).toFixed(1)}% (threshold: 70%)`,\n toolsCorrect,\n };\n }\n\n private async evaluateByLLMComparison(\n testCase: TestCase,\n agentResponse: string,\n toolsCorrect: boolean\n ): Promise<EvaluationResult> {\n const prompt = `You are evaluating an AI agent's response to a customer question.\n\nQuestion: ${testCase.question}\nExpected Answer: ${testCase.expectedAnswer}\nActual Response: ${agentResponse}\n\nRate the actual response on a scale of 1-5:\n1 = Completely wrong or irrelevant\n2 = Partially correct but missing key information\n3 = Mostly correct with minor issues\n4 = Correct with good quality\n5 = Excellent, matches or exceeds expected answer\n\nRespond with ONLY a JSON object in this format:\n{\"score\": <1-5>, \"reasoning\": \"<brief explanation>\"}`;\n\n const result = await this.llm!.complete(\n [{ role: 'user', content: prompt }],\n { temperature: 0, maxTokens: 200 }\n );\n\n let score = 3;\n let reasoning = 'LLM evaluation completed';\n\n try {\n const parsed = JSON.parse(result.text);\n score = parsed.score;\n reasoning = parsed.reasoning;\n } catch {\n // Fallback: try to extract score from text\n const match = result.text.match(/score[\"\\s:]+(\\d)/i);\n if (match) {\n score = parseInt(match[1], 10);\n }\n reasoning = result.text.substring(0, 200);\n }\n\n const normalizedScore = score / 5;\n const passed = normalizedScore >= 0.6 && toolsCorrect;\n\n return {\n passed,\n score: normalizedScore,\n method: 'llm_judge',\n reasoning: `LLM comparison (${score}/5): ${reasoning}`,\n toolsCorrect,\n };\n }\n\n private async evaluateByLLMJudge(\n testCase: TestCase,\n agentResponse: string,\n toolsCorrect: boolean\n ): Promise<EvaluationResult> {\n const prompt = `You are evaluating an AI agent's response to a customer question.\n\nQuestion: ${testCase.question}\nAgent Response: ${agentResponse}\n\nRate the response quality on a scale of 1-5:\n1 = Unhelpful, incorrect, or inappropriate\n2 = Partially helpful but incomplete or unclear\n3 = Adequate, addresses the question reasonably\n4 = Good quality, helpful and accurate\n5 = Excellent, comprehensive and professional\n\nRespond with ONLY a JSON object in this format:\n{\"score\": <1-5>, \"reasoning\": \"<brief explanation>\"}`;\n\n const result = await this.llm!.complete(\n [{ role: 'user', content: prompt }],\n { temperature: 0, maxTokens: 200 }\n );\n\n let score = 3;\n let reasoning = 'LLM evaluation completed';\n\n try {\n const parsed = JSON.parse(result.text);\n score = parsed.score;\n reasoning = parsed.reasoning;\n } catch {\n // Fallback: try to extract score from text\n const match = result.text.match(/score[\"\\s:]+(\\d)/i);\n if (match) {\n score = parseInt(match[1], 10);\n }\n reasoning = result.text.substring(0, 200);\n }\n\n const normalizedScore = score / 5;\n const passed = normalizedScore >= 0.6 && toolsCorrect;\n\n return {\n passed,\n score: normalizedScore,\n method: 'llm_judge',\n reasoning: `LLM standalone judge (${score}/5): ${reasoning}`,\n toolsCorrect,\n };\n }\n\n private normalizedLevenshtein(s1: string, s2: string): number {\n const len1 = s1.length;\n const len2 = s2.length;\n\n if (len1 === 0) return len2 === 0 ? 1 : 0;\n if (len2 === 0) return 0;\n\n const matrix: number[][] = Array.from({ length: len1 + 1 }, () =>\n Array(len2 + 1).fill(0)\n );\n\n for (let i = 0; i <= len1; i++) matrix[i][0] = i;\n for (let j = 0; j <= len2; j++) matrix[0][j] = j;\n\n for (let i = 1; i <= len1; i++) {\n for (let j = 1; j <= len2; j++) {\n const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;\n matrix[i][j] = Math.min(\n matrix[i - 1][j] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j - 1] + cost\n );\n }\n }\n\n const distance = matrix[len1][len2];\n const maxLen = Math.max(len1, len2);\n return 1 - distance / maxLen;\n }\n}\n","import { Operor } from '@operor/core';\nimport type { LLMProvider } from '@operor/llm';\nimport { MockProvider } from '@operor/provider-mock';\nimport type { TestCase, TestCaseResult, TestSuiteResult } from './types.js';\nimport { TestCaseEvaluator } from './TestCaseEvaluator.js';\n\nexport interface TestSuiteRunnerConfig {\n agentOS: Operor;\n llm?: LLMProvider;\n timeout?: number;\n strategy?: 'exact' | 'contains' | 'similarity' | 'semantic';\n}\n\nexport class TestSuiteRunner {\n private evaluator: TestCaseEvaluator;\n private agentOS: Operor;\n private timeout: number;\n private strategy?: 'exact' | 'contains' | 'similarity' | 'semantic';\n\n constructor(config: TestSuiteRunnerConfig) {\n this.agentOS = config.agentOS;\n this.evaluator = new TestCaseEvaluator(config.llm);\n this.timeout = config.timeout || 30000;\n this.strategy = config.strategy;\n }\n\n async runSuite(testCases: TestCase[]): Promise<TestSuiteResult> {\n const results: TestCaseResult[] = [];\n const startTime = Date.now();\n\n for (const testCase of testCases) {\n const result = await this.runTestCase(testCase);\n results.push(result);\n }\n\n const totalDuration = Date.now() - startTime;\n const passed = results.filter((r) => r.evaluation.passed).length;\n const failed = results.length - passed;\n const averageScore =\n results.reduce((sum, r) => sum + r.evaluation.score, 0) / results.length;\n const totalCost = results.reduce((sum, r) => sum + r.cost, 0);\n\n // Group by tags\n const byTag: Record<string, { total: number; passed: number; avgScore: number }> = {};\n for (const result of results) {\n const tags = result.testCase.tags || ['untagged'];\n for (const tag of tags) {\n if (!byTag[tag]) {\n byTag[tag] = { total: 0, passed: 0, avgScore: 0 };\n }\n byTag[tag].total++;\n if (result.evaluation.passed) {\n byTag[tag].passed++;\n }\n byTag[tag].avgScore += result.evaluation.score;\n }\n }\n\n // Calculate average scores per tag\n for (const tag in byTag) {\n byTag[tag].avgScore /= byTag[tag].total;\n }\n\n return {\n total: results.length,\n passed,\n failed,\n averageScore,\n byTag,\n results,\n totalDuration,\n totalCost,\n };\n }\n\n private async runTestCase(testCase: TestCase): Promise<TestCaseResult> {\n const startTime = Date.now();\n let agentResponse = '';\n let toolsCalled: Array<{ name: string; params: any; result: any }> = [];\n let cost = 0;\n\n try {\n // Create a promise that resolves when we get a response\n const responsePromise = new Promise<{\n text: string;\n toolCalls?: Array<{ name: string; params: any; result: any }>;\n cost: number;\n }>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n reject(new Error(`Test case ${testCase.id} timed out after ${this.timeout}ms`));\n }, this.timeout);\n\n // Listen for the response\n this.agentOS.once('message:processed', (event: any) => {\n clearTimeout(timeoutId);\n resolve({\n text: event.response.text,\n toolCalls: event.response.toolCalls || [],\n cost: event.cost || 0,\n });\n });\n\n // Listen for errors\n this.agentOS.once('error', (event: any) => {\n clearTimeout(timeoutId);\n reject(event.error);\n });\n });\n\n // Get the mock provider\n const mockProvider = Array.from((this.agentOS as any).providers.values()).find(\n (p: any) => p.name === 'mock'\n ) as MockProvider | undefined;\n\n if (!mockProvider) {\n throw new Error('MockProvider not found in Operor');\n }\n\n // Simulate incoming message\n const testPhone = testCase.persona || 'test-user';\n mockProvider.simulateIncomingMessage(testPhone, testCase.question);\n\n // Wait for response\n const response = await responsePromise;\n agentResponse = response.text;\n toolsCalled = response.toolCalls || [];\n cost = response.cost;\n } catch (error) {\n agentResponse = `Error: ${error instanceof Error ? error.message : String(error)}`;\n }\n\n const duration = Date.now() - startTime;\n\n // Evaluate the response\n const evaluation = await this.evaluator.evaluate(\n testCase,\n agentResponse,\n toolsCalled,\n this.strategy\n );\n\n return {\n testCase,\n agentResponse,\n toolsCalled,\n evaluation,\n duration,\n cost,\n };\n }\n}\n","import type { Skill, Tool } from '@operor/core';\n\nexport interface SkillTestHarnessConfig {\n allowWrites?: boolean;\n allowDestructive?: boolean;\n maxOperations?: number;\n timeoutMs?: number;\n dryRun?: boolean;\n}\n\nexport interface AuditLogEntry {\n name: string;\n params: any;\n result: any;\n timestamp: number;\n duration: number;\n classification: 'read' | 'write' | 'destructive';\n}\n\n/**\n * Safety wrapper for Integration instances during testing.\n * Provides operation limits, dry-run mode, and audit logging.\n */\nexport class SkillTestHarness implements Skill {\n public readonly name: string;\n private inner: Skill;\n private config: Required<SkillTestHarnessConfig>;\n private auditLog: AuditLogEntry[] = [];\n private operationCount = 0;\n\n // Tool classification rules\n private static readonly READ_TOOLS = new Set([\n 'get_order',\n 'search_products',\n 'salesforce_get_contact',\n 'salesforce_get_cases',\n 'stripe_get_customer',\n ]);\n\n private static readonly WRITE_TOOLS = new Set([\n 'create_discount',\n 'salesforce_update_contact',\n 'salesforce_create_case',\n 'salesforce_add_note',\n ]);\n\n private static readonly DESTRUCTIVE_TOOLS = new Set([\n 'stripe_create_refund',\n ]);\n\n constructor(inner: Skill, config: SkillTestHarnessConfig = {}) {\n this.inner = inner;\n this.name = inner.name;\n this.config = {\n allowWrites: config.allowWrites ?? false,\n allowDestructive: config.allowDestructive ?? false,\n maxOperations: config.maxOperations ?? 10,\n timeoutMs: config.timeoutMs ?? 30000,\n dryRun: config.dryRun ?? false,\n };\n\n // Wrap all tools with safety checks\n this.tools = this.wrapTools(inner.tools);\n }\n\n async initialize(): Promise<void> {\n return this.inner.initialize();\n }\n\n /** @deprecated Use initialize() instead. */\n async authenticate(): Promise<void> {\n return this.inner.initialize();\n }\n\n isReady(): boolean {\n return this.inner.isReady();\n }\n\n /** @deprecated Use isReady() instead. */\n isAuthenticated(): boolean {\n return this.inner.isReady();\n }\n\n public tools: Record<string, Tool>;\n\n private wrapTools(innerTools: Record<string, Tool>): Record<string, Tool> {\n const wrapped: Record<string, Tool> = {};\n\n for (const [toolName, tool] of Object.entries(innerTools)) {\n wrapped[toolName] = {\n ...tool,\n execute: async (params: any) => {\n const startTime = Date.now();\n const classification = this.classifyTool(toolName);\n\n // Check operation limit\n if (this.operationCount >= this.config.maxOperations) {\n throw new Error(\n `SkillTestHarness: Max operations limit reached (${this.config.maxOperations})`\n );\n }\n\n // Check write permissions\n if (classification === 'write' && !this.config.allowWrites) {\n throw new Error(\n `SkillTestHarness: Write operation '${toolName}' blocked (allowWrites=false)`\n );\n }\n\n // Check destructive permissions\n if (classification === 'destructive' && !this.config.allowDestructive) {\n throw new Error(\n `SkillTestHarness: Destructive operation '${toolName}' blocked (allowDestructive=false)`\n );\n }\n\n this.operationCount++;\n\n // Dry-run mode: return mock result without executing\n if (this.config.dryRun) {\n const result = {\n dryRun: true,\n wouldExecute: toolName,\n params,\n };\n\n this.auditLog.push({\n name: toolName,\n params,\n result,\n timestamp: startTime,\n duration: Date.now() - startTime,\n classification,\n });\n\n return result;\n }\n\n // Execute with timeout\n const timeoutPromise = new Promise((_, reject) => {\n setTimeout(() => {\n reject(new Error(`SkillTestHarness: Operation '${toolName}' timed out after ${this.config.timeoutMs}ms`));\n }, this.config.timeoutMs);\n });\n\n try {\n const result = await Promise.race([\n tool.execute(params),\n timeoutPromise,\n ]);\n\n const duration = Date.now() - startTime;\n\n this.auditLog.push({\n name: toolName,\n params,\n result,\n timestamp: startTime,\n duration,\n classification,\n });\n\n return result;\n } catch (error) {\n const duration = Date.now() - startTime;\n\n this.auditLog.push({\n name: toolName,\n params,\n result: { error: error instanceof Error ? error.message : 'Unknown error' },\n timestamp: startTime,\n duration,\n classification,\n });\n\n throw error;\n }\n },\n };\n }\n\n return wrapped;\n }\n\n private classifyTool(toolName: string): 'read' | 'write' | 'destructive' {\n if (SkillTestHarness.READ_TOOLS.has(toolName)) {\n return 'read';\n }\n if (SkillTestHarness.DESTRUCTIVE_TOOLS.has(toolName)) {\n return 'destructive';\n }\n if (SkillTestHarness.WRITE_TOOLS.has(toolName)) {\n return 'write';\n }\n // Unknown tools default to 'write' (safe by default)\n return 'write';\n }\n\n /**\n * Get the audit log of all operations performed\n */\n getAuditLog(): AuditLogEntry[] {\n return [...this.auditLog];\n }\n\n /**\n * Reset the audit log and operation counter\n */\n resetAuditLog(): void {\n this.auditLog = [];\n this.operationCount = 0;\n }\n\n /**\n * Get the current operation count\n */\n getOperationCount(): number {\n return this.operationCount;\n }\n}\n","import type { LLMProvider, LLMMessage } from '@operor/llm';\nimport type { ConversationTurn, CustomerSimulatorResponse } from './types.js';\n\nconst PERSONA_PROMPTS: Record<string, string> = {\n polite: 'You are polite, patient, and use courteous language.',\n frustrated: 'You are frustrated and impatient. You express dissatisfaction but remain civil.',\n confused: 'You are confused and unsure. You ask clarifying questions and sometimes misunderstand.',\n terse: 'You give very short, minimal responses. One sentence max.',\n verbose: 'You are detailed and talkative. You provide lots of context and background.',\n};\n\nfunction buildSystemPrompt(\n persona: string,\n context?: { scenario?: string; maxTurns?: number; currentTurn?: number }\n): string {\n const style = PERSONA_PROMPTS[persona] || `You are a customer with a ${persona} communication style.`;\n const scenarioLine = context?.scenario ? `\\nScenario: ${context.scenario}` : '';\n const turnInfo = context?.maxTurns\n ? `\\nThis conversation has a maximum of ${context.maxTurns} turns. You are on turn ${context.currentTurn ?? 1}.`\n : '';\n\n return `You are simulating a customer in a support conversation for testing purposes.\n${style}${scenarioLine}${turnInfo}\n\nRules:\n- Stay in character throughout the conversation.\n- Escalate naturally if your issue isn't being resolved.\n- Set shouldContinue to false when your issue is resolved or you have no more questions.\n- If the agent asks a question, answer it in character.\n\nRespond with ONLY valid JSON (no markdown, no code fences):\n{\"message\": \"your response as the customer\", \"shouldContinue\": true}`;\n}\n\nfunction formatHistory(history: ConversationTurn[]): LLMMessage[] {\n return history.map((turn) => ({\n role: turn.role === 'customer' ? 'user' as const : 'assistant' as const,\n content: turn.message,\n }));\n}\n\nfunction parseResponse(text: string): CustomerSimulatorResponse {\n // Try to extract JSON from the response, handling markdown fences\n const cleaned = text.replace(/```(?:json)?\\s*/g, '').replace(/```/g, '').trim();\n try {\n const parsed = JSON.parse(cleaned);\n return {\n message: String(parsed.message ?? ''),\n shouldContinue: Boolean(parsed.shouldContinue),\n };\n } catch {\n // If JSON parsing fails, treat the whole text as the message\n return { message: text.trim(), shouldContinue: true };\n }\n}\n\nexport class CustomerSimulator {\n private llm?: LLMProvider;\n\n constructor(options?: { llmProvider?: LLMProvider }) {\n this.llm = options?.llmProvider;\n }\n\n async generateMessage(\n persona: string,\n history: ConversationTurn[],\n context?: {\n scenario?: string;\n maxTurns?: number;\n currentTurn?: number;\n scriptedResponses?: string[];\n }\n ): Promise<CustomerSimulatorResponse> {\n // Script mode: use pre-defined responses\n if (context?.scriptedResponses?.length) {\n const turn = context.currentTurn ?? history.filter((t) => t.role === 'customer').length;\n const responses = context.scriptedResponses;\n if (turn < responses.length) {\n return {\n message: responses[turn],\n shouldContinue: turn < responses.length - 1,\n };\n }\n // Exhausted scripted responses\n return { message: responses[responses.length - 1], shouldContinue: false };\n }\n\n // LLM mode\n if (!this.llm) {\n throw new Error('CustomerSimulator requires an LLM provider for non-scripted mode');\n }\n\n const systemPrompt = buildSystemPrompt(persona, context);\n const messages: LLMMessage[] = [\n { role: 'system', content: systemPrompt },\n ...formatHistory(history),\n ];\n\n const result = await this.llm.complete(messages, {\n temperature: 0.7,\n maxTokens: 500,\n });\n\n return parseResponse(result.text);\n }\n}\n","import type { LLMProvider, LLMMessage } from '@operor/llm';\nimport type {\n ConversationScenario,\n ConversationTurn,\n ConversationEvaluation,\n CriteriaResult,\n ConversationSuccessCriteria,\n} from './types.js';\n\nfunction buildEvaluationPrompt(config: {\n scenario: string;\n persona: string;\n turns: ConversationTurn[];\n toolsCalled: Array<{ name: string; params: any; result: any }>;\n expectedTools?: string[];\n expectedOutcome?: string;\n}): string {\n const { scenario, persona, turns, toolsCalled, expectedTools, expectedOutcome } = config;\n\n const conversationText = turns\n .map((t, i) => `Turn ${i + 1} [${t.role}]: ${t.message}`)\n .join('\\n');\n\n const toolsText = toolsCalled.length\n ? toolsCalled\n .map((tc) => `- ${tc.name}(${JSON.stringify(tc.params)}) → ${JSON.stringify(tc.result)}`)\n .join('\\n')\n : 'None';\n\n const expectedToolsText = expectedTools?.length ? expectedTools.join(', ') : 'Not specified';\n const expectedOutcomeText = expectedOutcome || 'Not specified';\n\n return `You are evaluating a customer support conversation for testing purposes.\n\nScenario: ${scenario}\nCustomer Persona: ${persona}\nExpected Tools: ${expectedToolsText}\nExpected Outcome: ${expectedOutcomeText}\n\nConversation:\n${conversationText}\n\nTools Called:\n${toolsText}\n\nEvaluate the conversation on these dimensions (score 1-5 for each):\n\n1. Accuracy (1-5): Did the agent provide factually correct information based on tool results?\n2. Tool Usage (1-5): Did the agent call the right tools at the right time?\n3. Tone (1-5): Was the agent's tone appropriate, professional, and empathetic?\n4. Resolution (1-5): Was the customer's issue resolved or properly addressed?\n\nOverall assessment:\n- \"pass\": All criteria met, customer satisfied (scores mostly 4-5)\n- \"partial\": Some issues but acceptable (scores mostly 3-4)\n- \"fail\": Significant problems (any score 1-2, or multiple scores below 3)\n\nRespond with ONLY valid JSON (no markdown, no code fences):\n{\n \"overall\": \"pass\" | \"fail\" | \"partial\",\n \"scores\": {\n \"accuracy\": <integer 1-5>,\n \"toolUsage\": <integer 1-5>,\n \"tone\": <integer 1-5>,\n \"resolution\": <integer 1-5>\n },\n \"feedback\": \"<brief explanation of the evaluation>\"\n}`;\n}\n\nfunction parseEvaluationResponse(text: string): ConversationEvaluation {\n const cleaned = text.replace(/```(?:json)?\\s*/g, '').replace(/```/g, '').trim();\n try {\n const parsed = JSON.parse(cleaned);\n return {\n overall: parsed.overall || 'fail',\n scores: {\n accuracy: Math.round(parsed.scores?.accuracy ?? 1),\n toolUsage: Math.round(parsed.scores?.toolUsage ?? 1),\n tone: Math.round(parsed.scores?.tone ?? 1),\n resolution: Math.round(parsed.scores?.resolution ?? 1),\n },\n feedback: String(parsed.feedback ?? 'No feedback provided'),\n };\n } catch {\n return {\n overall: 'fail',\n scores: { accuracy: 1, toolUsage: 1, tone: 1, resolution: 1 },\n feedback: 'Failed to parse evaluation response',\n };\n }\n}\n\nfunction evaluateCriteria(\n criteria: ConversationSuccessCriteria,\n turns: ConversationTurn[]\n): CriteriaResult {\n const { type, value } = criteria;\n\n switch (type) {\n case 'tool_called': {\n const toolName = String(value);\n const called = turns.some((turn) =>\n turn.toolCalls?.some((tc) => tc.name === toolName)\n );\n return {\n criteria,\n passed: called,\n details: called\n ? `Tool \"${toolName}\" was called`\n : `Tool \"${toolName}\" was not called`,\n };\n }\n\n case 'response_contains': {\n const searchText = String(value).toLowerCase();\n const found = turns.some(\n (turn) => turn.role === 'agent' && turn.message.toLowerCase().includes(searchText)\n );\n return {\n criteria,\n passed: found,\n details: found\n ? `Agent response contains \"${value}\"`\n : `Agent response does not contain \"${value}\"`,\n };\n }\n\n case 'intent_matched': {\n // Intent matching would require intent data in turns, which we don't have in the simplified model\n // For now, treat as passed if the value is in any message\n const intentText = String(value).toLowerCase();\n const matched = turns.some((turn) => turn.message.toLowerCase().includes(intentText));\n return {\n criteria,\n passed: matched,\n details: matched\n ? `Intent \"${value}\" was matched`\n : `Intent \"${value}\" was not matched`,\n };\n }\n\n case 'turns_under': {\n const maxTurns = Number(value);\n const actualTurns = turns.length;\n const passed = actualTurns < maxTurns;\n return {\n criteria,\n passed,\n details: passed\n ? `Conversation completed in ${actualTurns} turns (under ${maxTurns})`\n : `Conversation took ${actualTurns} turns (expected under ${maxTurns})`,\n };\n }\n\n case 'custom': {\n if (typeof value === 'function') {\n try {\n const passed = value(turns);\n return {\n criteria,\n passed,\n details: passed ? 'Custom criteria passed' : 'Custom criteria failed',\n };\n } catch (error) {\n return {\n criteria,\n passed: false,\n details: `Custom criteria error: ${error}`,\n };\n }\n }\n return {\n criteria,\n passed: false,\n details: 'Custom criteria value must be a function',\n };\n }\n\n default:\n return {\n criteria,\n passed: false,\n details: `Unknown criteria type: ${type}`,\n };\n }\n}\n\nexport class ConversationEvaluator {\n private llm?: LLMProvider;\n\n constructor(options?: { llmProvider?: LLMProvider }) {\n this.llm = options?.llmProvider;\n }\n\n async evaluate(config: {\n scenario: string;\n persona: string;\n turns: ConversationTurn[];\n toolsCalled: Array<{ name: string; params: any; result: any }>;\n expectedTools?: string[];\n expectedOutcome?: string;\n successCriteria?: ConversationSuccessCriteria[];\n }): Promise<ConversationEvaluation> {\n const { turns, successCriteria } = config;\n\n // Evaluate criteria-based checks\n const criteriaResults: CriteriaResult[] = successCriteria\n ? successCriteria.map((criteria) => evaluateCriteria(criteria, turns))\n : [];\n\n // If LLM is available, use it for scoring\n if (this.llm) {\n const prompt = buildEvaluationPrompt(config);\n const messages: LLMMessage[] = [{ role: 'user', content: prompt }];\n\n const result = await this.llm.complete(messages, {\n temperature: 0,\n maxTokens: 1000,\n });\n\n const evaluation = parseEvaluationResponse(result.text);\n evaluation.criteriaResults = criteriaResults;\n\n // If any criteria failed, downgrade overall to at least 'partial'\n const criteriaPassed = criteriaResults.every((cr) => cr.passed);\n if (!criteriaPassed && evaluation.overall === 'pass') {\n evaluation.overall = 'partial';\n }\n\n return evaluation;\n }\n\n // Fallback: criteria-only evaluation (no LLM)\n if (!criteriaResults.length) {\n return {\n overall: 'fail',\n scores: { accuracy: 1, toolUsage: 1, tone: 1, resolution: 1 },\n feedback: 'No criteria specified',\n criteriaResults,\n };\n }\n\n const allPassed = criteriaResults.every((cr) => cr.passed);\n const somePassed = criteriaResults.some((cr) => cr.passed);\n\n return {\n overall: allPassed ? 'pass' : somePassed ? 'partial' : 'fail',\n scores: { accuracy: 3, toolUsage: 3, tone: 3, resolution: 3 },\n feedback: criteriaResults.map((cr) => cr.details).join('; '),\n criteriaResults,\n };\n }\n}\n","/**\n * Format current time as HH:MM:SS timestamp\n */\nexport function formatTimestamp(): string {\n return new Date().toLocaleTimeString('en-US', { hour12: false });\n}\n","import type { Operor } from '@operor/core';\nimport type { MockProvider } from '@operor/provider-mock';\nimport type {\n ConversationScenario,\n ConversationTurn,\n ConversationTestResult,\n} from './types.js';\nimport { CustomerSimulator } from './CustomerSimulator.js';\nimport { ConversationEvaluator } from './ConversationEvaluator.js';\nimport { formatTimestamp } from './utils.js';\n\nexport interface ConversationRunnerConfig {\n agentOS: Operor;\n customerSimulator: CustomerSimulator;\n conversationEvaluator: ConversationEvaluator;\n timeout?: number;\n verbose?: boolean;\n}\n\nexport class ConversationRunner {\n private agentOS: Operor;\n private customerSimulator: CustomerSimulator;\n private conversationEvaluator: ConversationEvaluator;\n private timeout: number;\n private verbose: boolean;\n\n constructor(config: ConversationRunnerConfig) {\n this.agentOS = config.agentOS;\n this.customerSimulator = config.customerSimulator;\n this.conversationEvaluator = config.conversationEvaluator;\n this.timeout = config.timeout || 30000;\n this.verbose = config.verbose ?? false;\n }\n\n async runScenario(scenario: ConversationScenario): Promise<ConversationTestResult> {\n const startTime = Date.now();\n const turns: ConversationTurn[] = [];\n const toolsCalled: Array<{ name: string; params: any; result: any }> = [];\n let totalCost = 0;\n\n try {\n // Get MockProvider\n const mockProvider = this.getMockProvider();\n const customerId = `test-customer-${scenario.id}`;\n\n // Start conversation with initial message (from scenario or first scripted response)\n const initialMessage = scenario.scriptedResponses?.[0] || 'Hello, I need help';\n\n if (this.verbose) {\n console.log(`\\n=== Starting Scenario: ${scenario.name} ===`);\n console.log(`Persona: ${scenario.persona}`);\n console.log(`Max Turns: ${scenario.maxTurns}\\n`);\n }\n\n let shouldContinue = true;\n let currentTurn = 0;\n\n while (shouldContinue && currentTurn < scenario.maxTurns) {\n // Determine customer message\n const customerMessage =\n currentTurn === 0\n ? initialMessage\n : (\n await this.customerSimulator.generateMessage(scenario.persona, turns, {\n scenario: scenario.description,\n maxTurns: scenario.maxTurns,\n currentTurn,\n scriptedResponses: scenario.scriptedResponses,\n })\n ).message;\n\n if (this.verbose) {\n console.log(`[${formatTimestamp()}] Turn ${currentTurn + 1} [customer]: ${customerMessage}`);\n }\n\n // Wait for agent response\n const agentResponse = await this.waitForAgentResponse(\n mockProvider,\n customerId,\n customerMessage\n );\n\n if (this.verbose) {\n console.log(`[${formatTimestamp()}] Turn ${currentTurn + 1} [agent]: ${agentResponse.text}`);\n if (agentResponse.toolCalls?.length) {\n console.log(\n ` Tools called: ${agentResponse.toolCalls.map((tc) => tc.name).join(', ')}`\n );\n }\n }\n\n // Record turn\n turns.push({\n role: 'customer',\n message: customerMessage,\n });\n turns.push({\n role: 'agent',\n message: agentResponse.text,\n toolCalls: agentResponse.toolCalls,\n });\n\n // Track tools and cost\n if (agentResponse.toolCalls) {\n toolsCalled.push(...agentResponse.toolCalls);\n }\n totalCost += agentResponse.cost || 0;\n\n currentTurn++;\n\n // Check if conversation should continue\n if (scenario.scriptedResponses) {\n // Script mode: continue until scripts exhausted\n shouldContinue = currentTurn < scenario.scriptedResponses.length;\n } else {\n // LLM mode: ask CustomerSimulator\n if (currentTurn < scenario.maxTurns) {\n const nextResponse = await this.customerSimulator.generateMessage(\n scenario.persona,\n turns,\n {\n scenario: scenario.description,\n maxTurns: scenario.maxTurns,\n currentTurn,\n }\n );\n shouldContinue = nextResponse.shouldContinue;\n if (!shouldContinue && this.verbose) {\n console.log('Customer satisfied, ending conversation.');\n }\n }\n }\n }\n\n if (this.verbose && currentTurn >= scenario.maxTurns) {\n console.log(`\\nReached max turns (${scenario.maxTurns})`);\n }\n\n // Evaluate conversation\n const evaluation = await this.conversationEvaluator.evaluate({\n scenario: scenario.description,\n persona: scenario.persona,\n turns,\n toolsCalled,\n expectedTools: scenario.expectedTools,\n expectedOutcome: scenario.expectedOutcome,\n successCriteria: scenario.successCriteria,\n });\n\n if (this.verbose) {\n console.log(`\\n=== Evaluation ===`);\n console.log(`Overall: ${evaluation.overall}`);\n console.log(`Scores: ${JSON.stringify(evaluation.scores)}`);\n console.log(`Feedback: ${evaluation.feedback}\\n`);\n }\n\n const duration = Date.now() - startTime;\n const passed = evaluation.overall === 'pass';\n\n return {\n scenario,\n passed,\n turns,\n evaluation,\n duration,\n cost: totalCost,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n if (this.verbose) {\n console.error(`\\nError in scenario ${scenario.name}: ${errorMessage}\\n`);\n }\n\n return {\n scenario,\n passed: false,\n turns,\n evaluation: {\n overall: 'fail',\n scores: { accuracy: 1, toolUsage: 1, tone: 1, resolution: 1 },\n feedback: `Error: ${errorMessage}`,\n },\n duration,\n cost: totalCost,\n };\n }\n }\n\n async runScenarios(scenarios: ConversationScenario[]): Promise<ConversationTestResult[]> {\n const results: ConversationTestResult[] = [];\n\n for (const scenario of scenarios) {\n const result = await this.runScenario(scenario);\n results.push(result);\n }\n\n return results;\n }\n\n private getMockProvider(): MockProvider {\n const mockProvider = Array.from((this.agentOS as any).providers.values()).find(\n (p: any) => p.name === 'mock'\n ) as MockProvider | undefined;\n\n if (!mockProvider) {\n throw new Error('MockProvider not found in Operor. Add it with agentOS.addProvider()');\n }\n\n return mockProvider;\n }\n\n private async waitForAgentResponse(\n mockProvider: MockProvider,\n customerId: string,\n message: string\n ): Promise<{ text: string; toolCalls?: Array<{ name: string; params: any; result: any }>; cost: number }> {\n return new Promise((resolve, reject) => {\n let settled = false;\n\n const cleanup = () => {\n this.agentOS.removeListener('message:processed', onProcessed);\n this.agentOS.removeListener('error', onError);\n };\n\n const timeoutId = setTimeout(() => {\n if (!settled) {\n settled = true;\n cleanup();\n reject(new Error(`Agent response timed out after ${this.timeout}ms`));\n }\n }, this.timeout);\n\n const onProcessed = (event: any) => {\n if (!settled) {\n settled = true;\n clearTimeout(timeoutId);\n cleanup();\n resolve({\n text: event.response.text,\n toolCalls: event.response.toolCalls || [],\n cost: event.cost || 0,\n });\n }\n };\n\n const onError = (event: any) => {\n if (!settled) {\n settled = true;\n clearTimeout(timeoutId);\n cleanup();\n reject(event.error);\n }\n };\n\n // Listen for the response\n this.agentOS.once('message:processed', onProcessed);\n\n // Listen for errors\n this.agentOS.once('error', onError);\n\n // Simulate incoming message\n mockProvider.simulateIncomingMessage(customerId, message);\n });\n }\n}\n","import type { ConversationScenario } from './types.js';\n\nexport const ECOMMERCE_SCENARIOS: ConversationScenario[] = [\n {\n id: 'delayed-order-compensation',\n name: 'Delayed order with compensation',\n description: 'Customer asks about a delayed order and expects compensation',\n persona: 'Frustrated customer whose order #12345 was supposed to arrive 3 days ago',\n maxTurns: 6,\n expectedTools: ['get_order', 'create_discount'],\n expectedOutcome: 'Agent finds order, acknowledges delay, offers discount',\n scriptedResponses: [\n 'Where is my order #12345? It was supposed to arrive 3 days ago!',\n 'This is really frustrating. Can you do anything to make up for this?',\n 'Okay, I appreciate the discount. Thank you.',\n ],\n },\n {\n id: 'order-not-found',\n name: 'Order not found',\n description: 'Customer asks about an order that does not exist in the system',\n persona: 'Confused customer who may have the wrong order number',\n maxTurns: 4,\n expectedTools: ['get_order'],\n expectedOutcome: 'Agent attempts lookup, explains order not found, asks customer to verify',\n scriptedResponses: [\n 'Can you check on order #99999 for me?',\n 'Hmm, let me double check the number and get back to you.',\n ],\n },\n {\n id: 'product-inquiry',\n name: 'Product inquiry',\n description: 'Customer searches for a product and asks about availability',\n persona: 'Curious shopper looking for electronics',\n maxTurns: 4,\n expectedTools: ['search_products'],\n expectedOutcome: 'Agent searches products and provides relevant results',\n scriptedResponses: [\n 'Do you have any wireless headphones in stock?',\n 'What about the price range? Anything under $200?',\n 'Great, thanks for the info!',\n ],\n },\n {\n id: 'return-request',\n name: 'Return request',\n description: 'Customer wants to return a recently delivered item',\n persona: 'Polite customer who received a defective product from order #67890',\n maxTurns: 5,\n expectedTools: ['get_order'],\n expectedOutcome: 'Agent looks up order, acknowledges issue, explains return process',\n scriptedResponses: [\n 'I received order #67890 but one of the items is defective. I would like to return it.',\n 'Yes, the wireless mouse stopped working after one day.',\n 'Okay, how do I send it back?',\n ],\n },\n {\n id: 'greeting',\n name: 'Simple greeting',\n description: 'Customer says hello and expects a friendly welcome',\n persona: 'Friendly first-time visitor',\n maxTurns: 2,\n expectedTools: [],\n expectedOutcome: 'Agent responds with a friendly greeting and offers help',\n scriptedResponses: [\n 'Hello!',\n ],\n },\n {\n id: 'billing-dispute',\n name: 'Billing dispute',\n description: 'Customer believes they were charged incorrectly',\n persona: 'Concerned customer who noticed a double charge on order #12345',\n maxTurns: 5,\n expectedTools: ['get_order'],\n expectedOutcome: 'Agent looks up order, reviews charges, and addresses the billing concern',\n scriptedResponses: [\n 'I think I was charged twice for order #12345. Can you check?',\n 'My credit card shows two charges of $299.99 on the same day.',\n 'Can you escalate this to someone who can issue a correction?',\n ],\n },\n {\n id: 'multi-issue',\n name: 'Multi-issue conversation',\n description: 'Customer has multiple problems: a delayed order and a product question',\n persona: 'Busy customer who wants to resolve everything in one conversation',\n maxTurns: 6,\n expectedTools: ['get_order', 'search_products'],\n expectedOutcome: 'Agent handles both issues sequentially without losing context',\n scriptedResponses: [\n 'Two things: first, where is my order #12345?',\n 'Okay thanks. Also, do you carry mechanical keyboards?',\n 'Nice, I might order one. Can you also give me a discount for the late delivery?',\n 'Sounds good, thanks for handling both issues.',\n ],\n },\n {\n id: 'lead-qualification',\n name: 'Lead qualification',\n description: 'Potential customer asking pre-purchase questions about products and shipping',\n persona: 'Prospective buyer evaluating whether to make a purchase',\n maxTurns: 4,\n expectedTools: ['search_products'],\n expectedOutcome: 'Agent answers product questions and encourages purchase',\n scriptedResponses: [\n 'I am thinking about buying some electronics. What do you have?',\n 'How fast is shipping usually?',\n 'Do you offer any discounts for first-time buyers?',\n ],\n },\n {\n id: 'frustrated-escalation',\n name: 'Frustrated customer escalation',\n description: 'Angry customer escalates through multiple complaints',\n persona: 'Very frustrated customer who has contacted support multiple times about order #12345',\n maxTurns: 6,\n expectedTools: ['get_order', 'create_discount'],\n expectedOutcome: 'Agent remains professional, empathizes, and offers concrete resolution',\n scriptedResponses: [\n 'This is the THIRD time I am contacting you about order #12345. Still not here!',\n 'I have been waiting over a week. This is completely unacceptable.',\n 'I want a refund or serious compensation. A 5% coupon is insulting.',\n 'Fine, that is more reasonable. But I expect the order to arrive this week.',\n ],\n },\n {\n id: 'on-time-order-check',\n name: 'On-time order status check',\n description: 'Customer checks on an order that is on time or already delivered',\n persona: 'Polite customer just checking in on order #67890',\n maxTurns: 2,\n expectedTools: ['get_order'],\n expectedOutcome: 'Agent confirms order status, no compensation needed',\n scriptedResponses: [\n 'Hi, can I get an update on order #67890?',\n 'Perfect, thanks!',\n ],\n },\n];\n","import type { Operor } from '@operor/core';\nimport type { LLMProvider } from '@operor/llm';\nimport type {\n SimulationConfig,\n SimulationReport,\n TestSuiteResult,\n ConversationTestResult,\n ConversationScenario,\n} from './types.js';\nimport { TestSuiteRunner } from './TestSuiteRunner.js';\nimport { ConversationRunner } from './ConversationRunner.js';\nimport { CustomerSimulator } from './CustomerSimulator.js';\nimport { ConversationEvaluator } from './ConversationEvaluator.js';\nimport { CSVLoader } from './CSVLoader.js';\nimport { ECOMMERCE_SCENARIOS } from './scenarios.js';\n\nexport interface SimulationRunnerOptions {\n agentOS: Operor;\n config: SimulationConfig;\n llm?: LLMProvider;\n}\n\nexport class SimulationRunner {\n private agentOS: Operor;\n private config: SimulationConfig;\n private llm?: LLMProvider;\n\n constructor(options: SimulationRunnerOptions) {\n this.agentOS = options.agentOS;\n this.config = options.config;\n this.llm = options.llm;\n }\n\n async run(\n onProgress?: (completed: number, total: number, result: ConversationTestResult) => void\n ): Promise<SimulationReport> {\n const startTime = Date.now();\n const testSuiteResults: TestSuiteResult[] = [];\n const conversationResults: ConversationTestResult[] = [];\n let totalCost = 0;\n\n // 1. Run test suites from CSV/JSON files\n if (this.config.testSuiteFiles?.length) {\n const suiteRunner = new TestSuiteRunner({\n agentOS: this.agentOS,\n llm: this.llm,\n timeout: this.config.timeout,\n });\n\n for (const file of this.config.testSuiteFiles) {\n const testCases = await CSVLoader.fromFile(file);\n const result = await suiteRunner.runSuite(testCases);\n testSuiteResults.push(result);\n totalCost += result.totalCost;\n }\n }\n\n // 2. Run conversation scenarios\n const scenarios = this.resolveScenarios();\n if (scenarios.length) {\n const conversationRunner = new ConversationRunner({\n agentOS: this.agentOS,\n customerSimulator: new CustomerSimulator({ llmProvider: this.llm }),\n conversationEvaluator: new ConversationEvaluator({ llmProvider: this.llm }),\n timeout: this.config.timeout,\n });\n\n // Distribute totalConversations across scenarios round-robin\n const schedule = this.buildSchedule(scenarios);\n const pauseMs = this.config.pauseBetweenMs ?? 500;\n\n for (let i = 0; i < schedule.length; i++) {\n const scenario = schedule[i];\n\n // Add timeout protection around conversation execution\n const timeoutMs = this.config.timeout || 60000;\n const result = await Promise.race([\n conversationRunner.runScenario(scenario),\n new Promise<ConversationTestResult>((_, reject) =>\n setTimeout(() => reject(new Error(`Conversation timed out after ${timeoutMs}ms`)), timeoutMs)\n ),\n ]).catch((error): ConversationTestResult => {\n // If timeout or error, return a failed result\n return {\n scenario,\n passed: false,\n turns: [],\n evaluation: {\n overall: 'fail',\n scores: { accuracy: 1, toolUsage: 1, tone: 1, resolution: 1 },\n feedback: `Timeout or error: ${error instanceof Error ? error.message : String(error)}`,\n },\n duration: timeoutMs,\n cost: 0,\n };\n });\n\n conversationResults.push(result);\n totalCost += result.cost;\n\n if (onProgress) {\n onProgress(i + 1, schedule.length, result);\n }\n\n // Pause between conversations (skip after last)\n if (i < schedule.length - 1 && pauseMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, pauseMs));\n }\n }\n }\n\n const duration = Date.now() - startTime;\n\n // 3. Aggregate results\n const totalTests = testSuiteResults.reduce((sum, r) => sum + r.total, 0);\n const passedTests = testSuiteResults.reduce((sum, r) => sum + r.passed, 0);\n const failedTests = testSuiteResults.reduce((sum, r) => sum + r.failed, 0);\n const totalConversations = conversationResults.length;\n const passedConversations = conversationResults.filter((r) => r.passed).length;\n const failedConversations = totalConversations - passedConversations;\n\n const totalItems = totalTests + totalConversations;\n const passedItems = passedTests + passedConversations;\n const overallPassRate = totalItems > 0 ? passedItems / totalItems : 0;\n\n const averageScores = this.computeAverageScores(conversationResults);\n const scenarioBreakdown = this.computeScenarioBreakdown(conversationResults);\n const toolUsageStats = this.computeToolUsageStats(conversationResults);\n\n // 4. Failure analysis\n const failedResults = conversationResults.filter((r) => !r.passed);\n let commonFailurePatterns: string[] = [];\n let recommendations: string[] = [];\n\n if (failedResults.length > 0) {\n if (this.llm) {\n const analysis = await this.analyzeFailuresWithLLM(failedResults);\n commonFailurePatterns = analysis.patterns;\n recommendations = analysis.recommendations;\n } else {\n commonFailurePatterns = this.heuristicFailurePatterns(failedResults);\n recommendations = this.heuristicRecommendations(failedResults);\n }\n }\n\n return {\n timestamp: new Date(),\n duration,\n totalConversations,\n passed: passedConversations,\n failed: failedConversations,\n averageScores,\n scenarioBreakdown,\n toolUsageStats,\n commonFailurePatterns,\n recommendations,\n testSuiteResults,\n conversationResults,\n overallPassed: failedTests === 0 && failedConversations === 0 && totalItems > 0,\n totalCost,\n summary: {\n totalTests,\n passedTests,\n failedTests,\n totalConversations,\n passedConversations,\n failedConversations,\n overallPassRate,\n },\n };\n }\n\n static formatReport(report: SimulationReport): string {\n const lines: string[] = [];\n\n lines.push('=== Simulation Report ===');\n lines.push(`Date: ${report.timestamp.toISOString()}`);\n lines.push(`Duration: ${(report.duration / 1000).toFixed(1)}s`);\n lines.push(`Cost: $${report.totalCost.toFixed(4)}`);\n lines.push('');\n\n // Test suite results\n if (report.testSuiteResults.length) {\n lines.push('--- Test Suites ---');\n for (const suite of report.testSuiteResults) {\n lines.push(` ${suite.passed}/${suite.total} passed (avg score: ${suite.averageScore.toFixed(2)})`);\n for (const result of suite.results) {\n const status = result.evaluation.passed ? 'PASS' : 'FAIL';\n lines.push(` [${status}] ${result.testCase.id}: ${result.testCase.question}`);\n }\n }\n lines.push('');\n }\n\n // Scenario breakdown\n if (report.scenarioBreakdown.length) {\n lines.push('--- Scenario Breakdown ---');\n for (const s of report.scenarioBreakdown) {\n const pct = (s.passRate * 100).toFixed(0);\n lines.push(` ${s.scenario}: ${s.runs} run(s), ${pct}% pass rate, avg score ${s.avgScore.toFixed(2)}`);\n }\n lines.push('');\n }\n\n // Tool usage\n const toolEntries = Object.entries(report.toolUsageStats);\n if (toolEntries.length) {\n lines.push('--- Tool Usage ---');\n for (const [tool, count] of toolEntries.sort((a, b) => b[1] - a[1])) {\n lines.push(` ${tool}: ${count} call(s)`);\n }\n lines.push('');\n }\n\n // Average scores\n const { averageScores } = report;\n if (report.totalConversations > 0) {\n lines.push('--- Average Scores ---');\n lines.push(` Accuracy: ${averageScores.accuracy.toFixed(2)}`);\n lines.push(` Tool Usage: ${averageScores.toolUsage.toFixed(2)}`);\n lines.push(` Tone: ${averageScores.tone.toFixed(2)}`);\n lines.push(` Resolution: ${averageScores.resolution.toFixed(2)}`);\n lines.push('');\n }\n\n // Failure patterns\n if (report.commonFailurePatterns.length) {\n lines.push('--- Common Failure Patterns ---');\n for (const pattern of report.commonFailurePatterns) {\n lines.push(` - ${pattern}`);\n }\n lines.push('');\n }\n\n // Recommendations\n if (report.recommendations.length) {\n lines.push('--- Recommendations ---');\n for (const rec of report.recommendations) {\n lines.push(` - ${rec}`);\n }\n lines.push('');\n }\n\n // Summary\n const { summary } = report;\n lines.push('--- Summary ---');\n if (summary.totalTests > 0) {\n lines.push(`Tests: ${summary.passedTests}/${summary.totalTests} passed`);\n }\n lines.push(`Conversations: ${summary.passedConversations}/${summary.totalConversations} passed`);\n lines.push(`Overall pass rate: ${(summary.overallPassRate * 100).toFixed(1)}%`);\n lines.push(`Result: ${report.overallPassed ? 'PASSED' : 'FAILED'}`);\n\n return lines.join('\\n');\n }\n\n private resolveScenarios(): ConversationScenario[] {\n if (!this.config.conversationScenarios) return [];\n if (this.config.conversationScenarios === 'builtin') return ECOMMERCE_SCENARIOS;\n return this.config.conversationScenarios;\n }\n\n private buildSchedule(scenarios: ConversationScenario[]): ConversationScenario[] {\n const total = this.config.totalConversations ?? scenarios.length;\n const schedule: ConversationScenario[] = [];\n for (let i = 0; i < total; i++) {\n schedule.push(scenarios[i % scenarios.length]);\n }\n return schedule;\n }\n\n private computeAverageScores(\n results: ConversationTestResult[]\n ): { accuracy: number; toolUsage: number; tone: number; resolution: number } {\n if (results.length === 0) {\n return { accuracy: 0, toolUsage: 0, tone: 0, resolution: 0 };\n }\n const totals = results.reduce(\n (acc, r) => ({\n accuracy: acc.accuracy + r.evaluation.scores.accuracy,\n toolUsage: acc.toolUsage + r.evaluation.scores.toolUsage,\n tone: acc.tone + r.evaluation.scores.tone,\n resolution: acc.resolution + r.evaluation.scores.resolution,\n }),\n { accuracy: 0, toolUsage: 0, tone: 0, resolution: 0 }\n );\n const n = results.length;\n return {\n accuracy: totals.accuracy / n,\n toolUsage: totals.toolUsage / n,\n tone: totals.tone / n,\n resolution: totals.resolution / n,\n };\n }\n\n private computeScenarioBreakdown(\n results: ConversationTestResult[]\n ): Array<{ scenario: string; runs: number; passRate: number; avgScore: number }> {\n const byScenario = new Map<string, ConversationTestResult[]>();\n for (const r of results) {\n const name = r.scenario.name;\n if (!byScenario.has(name)) byScenario.set(name, []);\n byScenario.get(name)!.push(r);\n }\n return Array.from(byScenario.entries()).map(([scenario, runs]) => {\n const passed = runs.filter((r) => r.passed).length;\n const scores = runs.map((r) => {\n const s = r.evaluation.scores;\n return (s.accuracy + s.toolUsage + s.tone + s.resolution) / 4;\n });\n const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;\n return { scenario, runs: runs.length, passRate: passed / runs.length, avgScore };\n });\n }\n\n private computeToolUsageStats(results: ConversationTestResult[]): Record<string, number> {\n const stats: Record<string, number> = {};\n for (const r of results) {\n for (const turn of r.turns) {\n if (turn.toolCalls) {\n for (const tc of turn.toolCalls) {\n stats[tc.name] = (stats[tc.name] || 0) + 1;\n }\n }\n }\n }\n return stats;\n }\n\n private async analyzeFailuresWithLLM(\n failedResults: ConversationTestResult[]\n ): Promise<{ patterns: string[]; recommendations: string[] }> {\n if (!this.llm) {\n return { patterns: [], recommendations: [] };\n }\n\n const summaries = failedResults.slice(0, 10).map((r) => {\n const turns = r.turns.map((t) => `[${t.role}]: ${t.message}`).join('\\n');\n return `Scenario: ${r.scenario.name}\\nFeedback: ${r.evaluation.feedback}\\nConversation:\\n${turns}`;\n });\n\n const prompt = `Analyze these failed customer support conversation tests and identify patterns.\n\n${summaries.join('\\n\\n---\\n\\n')}\n\nRespond with ONLY valid JSON (no markdown, no code fences):\n{\n \"patterns\": [\"pattern 1\", \"pattern 2\"],\n \"recommendations\": [\"recommendation 1\", \"recommendation 2\"]\n}`;\n\n try {\n const result = await this.llm.complete(\n [{ role: 'user', content: prompt }],\n { temperature: 0, maxTokens: 1000 }\n );\n const cleaned = result.text.replace(/```(?:json)?\\s*/g, '').replace(/```/g, '').trim();\n const parsed = JSON.parse(cleaned);\n return {\n patterns: Array.isArray(parsed.patterns) ? parsed.patterns.map(String) : [],\n recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations.map(String) : [],\n };\n } catch {\n return {\n patterns: this.heuristicFailurePatterns(failedResults),\n recommendations: this.heuristicRecommendations(failedResults),\n };\n }\n }\n\n private heuristicFailurePatterns(failedResults: ConversationTestResult[]): string[] {\n const patterns: string[] = [];\n\n const noToolCalls = failedResults.filter((r) =>\n r.turns.every((t) => !t.toolCalls?.length)\n );\n if (noToolCalls.length > 0) {\n patterns.push(`${noToolCalls.length} conversation(s) failed with no tool calls`);\n }\n\n const lowResolution = failedResults.filter((r) => r.evaluation.scores.resolution <= 2);\n if (lowResolution.length > 0) {\n patterns.push(`${lowResolution.length} conversation(s) had low resolution scores`);\n }\n\n const lowTone = failedResults.filter((r) => r.evaluation.scores.tone <= 2);\n if (lowTone.length > 0) {\n patterns.push(`${lowTone.length} conversation(s) had low tone scores`);\n }\n\n if (patterns.length === 0) {\n patterns.push(`${failedResults.length} conversation(s) failed evaluation criteria`);\n }\n\n return patterns;\n }\n\n private heuristicRecommendations(failedResults: ConversationTestResult[]): string[] {\n const recs: string[] = [];\n\n const noTools = failedResults.filter((r) =>\n r.turns.every((t) => !t.toolCalls?.length)\n );\n if (noTools.length > 0) {\n recs.push('Ensure agent is configured to use available tools for customer queries');\n }\n\n const expectedButMissing = new Set<string>();\n for (const r of failedResults) {\n for (const tool of r.scenario.expectedTools || []) {\n const used = r.turns.some((t) => t.toolCalls?.some((tc) => tc.name === tool));\n if (!used) expectedButMissing.add(tool);\n }\n }\n if (expectedButMissing.size > 0) {\n recs.push(`Tools expected but not called: ${Array.from(expectedButMissing).join(', ')}`);\n }\n\n if (recs.length === 0) {\n recs.push('Review failed scenarios and adjust agent rules or prompts');\n }\n\n return recs;\n }\n}\n","import type { Skill, Tool } from '@operor/core';\n\ninterface MockOrder {\n id: string;\n name: string;\n status: string;\n financialStatus: string;\n createdAt: string;\n expectedDelivery: Date;\n actualDelivery?: Date;\n tracking?: string;\n trackingUrl?: string;\n items: Array<{ name: string; quantity: number; price: string }>;\n total: string;\n}\n\ninterface MockProduct {\n id: number;\n title: string;\n vendor: string;\n type: string;\n price: string;\n available: boolean;\n}\n\ninterface MockDiscount {\n code: string;\n percent: number;\n validDays: number;\n startsAt: string;\n expiresAt: string;\n priceRuleId: number;\n createdAt: Date;\n}\n\nexport class MockShopifySkill implements Skill {\n public readonly name = 'shopify';\n private ready = false;\n private mockOrders: Map<string, MockOrder> = new Map();\n private mockProducts: MockProduct[] = [];\n private mockDiscounts: MockDiscount[] = [];\n private nextPriceRuleId = 1000;\n\n constructor() {\n // Seed with mock data\n this.seedMockData();\n }\n\n async initialize(): Promise<void> {\n this.ready = true;\n console.log('✅ Mock Shopify initialized');\n }\n\n /** @deprecated Use initialize() instead. */\n async authenticate(): Promise<void> {\n return this.initialize();\n }\n\n isReady(): boolean {\n return this.ready;\n }\n\n /** @deprecated Use isReady() instead. */\n isAuthenticated(): boolean {\n return this.ready;\n }\n\n /**\n * Reset all mock data to initial state (for testing)\n */\n reset(): void {\n this.mockOrders.clear();\n this.mockProducts = [];\n this.mockDiscounts = [];\n this.nextPriceRuleId = 1000;\n this.seedMockData();\n }\n\n /**\n * Seed custom test data (for testing)\n */\n seedData(config: {\n orders?: Array<Partial<MockOrder>>;\n products?: Array<Partial<MockProduct>>;\n discounts?: Array<Partial<MockDiscount>>;\n }): void {\n if (config.orders) {\n for (const order of config.orders) {\n const fullOrder: MockOrder = {\n id: order.id || String(Date.now()),\n name: order.name || `#${order.id || '1001'}`,\n status: order.status || 'unfulfilled',\n financialStatus: order.financialStatus || 'paid',\n createdAt: order.createdAt || new Date().toISOString(),\n expectedDelivery: order.expectedDelivery || new Date(),\n actualDelivery: order.actualDelivery,\n tracking: order.tracking,\n trackingUrl: order.trackingUrl,\n items: order.items || [],\n total: order.total || '0.00',\n };\n this.mockOrders.set(fullOrder.id, fullOrder);\n }\n }\n\n if (config.products) {\n for (const product of config.products) {\n const fullProduct: MockProduct = {\n id: product.id || Date.now(),\n title: product.title || 'Product',\n vendor: product.vendor || 'Mock Vendor',\n type: product.type || 'General',\n price: product.price || '0.00',\n available: product.available !== undefined ? product.available : true,\n };\n this.mockProducts.push(fullProduct);\n }\n }\n\n if (config.discounts) {\n for (const discount of config.discounts) {\n const fullDiscount: MockDiscount = {\n code: discount.code || 'DISCOUNT',\n percent: discount.percent || 10,\n validDays: discount.validDays || 30,\n startsAt: discount.startsAt || new Date().toISOString(),\n expiresAt: discount.expiresAt || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),\n priceRuleId: discount.priceRuleId || this.nextPriceRuleId++,\n createdAt: discount.createdAt || new Date(),\n };\n this.mockDiscounts.push(fullDiscount);\n }\n }\n }\n\n private seedMockData(): void {\n // Create a delayed order for testing\n const twoDaysAgo = new Date();\n twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);\n\n this.mockOrders.set('12345', {\n id: '12345',\n name: '#1001',\n status: 'in_transit',\n financialStatus: 'paid',\n createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),\n expectedDelivery: twoDaysAgo,\n tracking: 'TRACK123456789',\n trackingUrl: 'https://track.example.com/TRACK123456789',\n items: [\n { name: 'Premium Headphones', quantity: 1, price: '299.99' },\n ],\n total: '299.99',\n });\n\n this.mockOrders.set('67890', {\n id: '67890',\n name: '#1002',\n status: 'delivered',\n financialStatus: 'paid',\n createdAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),\n expectedDelivery: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000),\n actualDelivery: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),\n tracking: 'TRACK987654321',\n trackingUrl: 'https://track.example.com/TRACK987654321',\n items: [\n { name: 'Wireless Mouse', quantity: 2, price: '49.99' },\n ],\n total: '99.98',\n });\n\n // Seed default products\n this.mockProducts = [\n {\n id: 1001,\n title: 'Premium Headphones',\n vendor: 'AudioTech',\n type: 'Electronics',\n price: '299.99',\n available: true,\n },\n {\n id: 1002,\n title: 'Wireless Mouse',\n vendor: 'TechGear',\n type: 'Electronics',\n price: '49.99',\n available: true,\n },\n {\n id: 1003,\n title: 'Mechanical Keyboard',\n vendor: 'KeyMaster',\n type: 'Electronics',\n price: '149.99',\n available: true,\n },\n {\n id: 1004,\n title: 'USB-C Cable',\n vendor: 'CableCo',\n type: 'Accessories',\n price: '19.99',\n available: false,\n },\n ];\n }\n\n public tools: Record<string, Tool> = {\n get_order: {\n name: 'get_order',\n description: 'Get order details by order ID or order name (e.g., #1001)',\n parameters: {\n orderId: { type: 'string', required: true },\n },\n execute: async (params: { orderId: string }) => {\n const orderIdentifier = params.orderId.replace('#', '');\n const order = this.mockOrders.get(orderIdentifier);\n\n if (!order) {\n return {\n found: false,\n error: `Order ${params.orderId} not found`,\n };\n }\n\n const delayMs = new Date().getTime() - order.expectedDelivery.getTime();\n const delayDays = Math.floor(delayMs / (1000 * 60 * 60 * 24));\n\n return {\n found: true,\n id: order.id,\n name: order.name,\n status: order.status,\n financialStatus: order.financialStatus,\n createdAt: order.createdAt,\n total: order.total,\n items: order.items,\n tracking: order.tracking || null,\n trackingUrl: order.trackingUrl || null,\n // Mock-specific extra fields\n isDelayed: delayDays > 0,\n delayDays: Math.max(0, delayDays),\n };\n },\n },\n\n create_discount: {\n name: 'create_discount',\n description: 'Create a percentage discount code',\n parameters: {\n percent: { type: 'number', required: true },\n validDays: { type: 'number', required: true },\n },\n execute: async (params: { percent: number; validDays: number }) => {\n const code = `SORRY${params.percent}`;\n const startsAt = new Date().toISOString();\n const expiresAt = new Date(Date.now() + params.validDays * 24 * 60 * 60 * 1000).toISOString();\n const priceRuleId = this.nextPriceRuleId++;\n\n const discount: MockDiscount = {\n code,\n percent: params.percent,\n validDays: params.validDays,\n startsAt,\n expiresAt,\n priceRuleId,\n createdAt: new Date(),\n };\n\n this.mockDiscounts.push(discount);\n\n console.log(`\\n💰 Created discount code: ${code}`);\n console.log(` ${params.percent}% off, valid for ${params.validDays} days\\n`);\n\n return {\n code,\n percent: params.percent,\n validDays: params.validDays,\n startsAt,\n expiresAt,\n priceRuleId,\n };\n },\n },\n\n search_products: {\n name: 'search_products',\n description: 'Search for products in the store',\n parameters: {\n query: { type: 'string', required: true },\n limit: { type: 'number', required: false },\n },\n execute: async (params: { query: string; limit?: number }) => {\n const limit = params.limit || 10;\n const queryLower = params.query.toLowerCase();\n\n const matched = this.mockProducts\n .filter((p) =>\n p.title.toLowerCase().includes(queryLower) ||\n p.vendor.toLowerCase().includes(queryLower) ||\n p.type.toLowerCase().includes(queryLower)\n )\n .slice(0, limit);\n\n return {\n found: matched.length,\n products: matched.map((p) => ({\n id: p.id,\n title: p.title,\n vendor: p.vendor,\n type: p.type,\n price: p.price,\n available: p.available,\n })),\n };\n },\n },\n };\n\n /**\n * Get all created discounts (for testing)\n */\n getDiscounts(): MockDiscount[] {\n return [...this.mockDiscounts];\n }\n\n /**\n * Get all orders (for testing)\n */\n getOrders(): MockOrder[] {\n return Array.from(this.mockOrders.values());\n }\n}\n\nexport type { MockOrder, MockProduct, MockDiscount };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
id,question,expected_answer,expected_tools,persona,tags
|
|
2
|
+
greeting-1,Hello,Hi! How can I help you today?,,friendly,greeting
|
|
3
|
+
order-track-1,Where is my order #12345?,Let me check your order status for you.,getOrderStatus,helpful,order-tracking
|
|
4
|
+
product-search-1,Do you have blue sneakers in size 10?,Let me search our inventory for blue sneakers in size 10.,searchProducts,helpful,product-search
|
|
5
|
+
discount-1,Can I get a discount on this item?,I can check if there are any available discounts for you.,checkDiscounts,helpful,discount
|
|
6
|
+
edge-special,"Hello, ""friend""! How's it going?",Hello! I'm here to help you.,,,greeting,edge-case
|
|
7
|
+
order-track-2,Track order ABC-789,I'll look up order ABC-789 for you.,getOrderStatus,helpful,order-tracking
|
|
8
|
+
product-search-2,Show me red dresses under $50,Let me find red dresses under $50 for you.,searchProducts,helpful,product-search
|
|
9
|
+
multi-tool-1,I want to track my order and apply a discount code,I can help you with both tracking your order and applying a discount code.,"getOrderStatus,applyDiscount",helpful,order-tracking,discount
|
|
10
|
+
greeting-2,Hey there!,Hello! What can I do for you today?,,friendly,greeting
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@operor/testing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Testing utilities for Agent OS — CSV test runner, evaluators, and test suite tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"csv-parse": "^6.0.0",
|
|
16
|
+
"@operor/core": "0.1.0",
|
|
17
|
+
"@operor/llm": "0.1.0",
|
|
18
|
+
"@operor/provider-mock": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"tsdown": "^0.20.3",
|
|
23
|
+
"typescript": "^5.7.0",
|
|
24
|
+
"vitest": "^4.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsdown",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/CSVLoader.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parse } from 'csv-parse/sync';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import type { TestCase } from './types.js';
|
|
4
|
+
|
|
5
|
+
export class CSVLoader {
|
|
6
|
+
static async fromFile(path: string): Promise<TestCase[]> {
|
|
7
|
+
const content = await readFile(path, 'utf-8');
|
|
8
|
+
if (path.endsWith('.json')) {
|
|
9
|
+
return CSVLoader.fromJSON(content);
|
|
10
|
+
}
|
|
11
|
+
return CSVLoader.fromCSVString(content);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static fromCSVString(csv: string): TestCase[] {
|
|
15
|
+
// Strip UTF-8 BOM
|
|
16
|
+
const clean = csv.replace(/^\uFEFF/, '');
|
|
17
|
+
|
|
18
|
+
const records: Record<string, string>[] = parse(clean, {
|
|
19
|
+
columns: true,
|
|
20
|
+
skip_empty_lines: true,
|
|
21
|
+
trim: true,
|
|
22
|
+
relax_column_count: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return records.map((row, i) => {
|
|
26
|
+
const id = row.id?.trim();
|
|
27
|
+
const question = row.question?.trim();
|
|
28
|
+
|
|
29
|
+
if (!id || !question) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Row ${i + 1}: missing required field(s) — id and question are required`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const testCase: TestCase = { id, question };
|
|
36
|
+
|
|
37
|
+
if (row.expected_answer?.trim()) {
|
|
38
|
+
testCase.expectedAnswer = row.expected_answer.trim();
|
|
39
|
+
}
|
|
40
|
+
if (row.expected_tools?.trim()) {
|
|
41
|
+
testCase.expectedTools = row.expected_tools
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((t: string) => t.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
if (row.persona?.trim()) {
|
|
47
|
+
testCase.persona = row.persona.trim();
|
|
48
|
+
}
|
|
49
|
+
if (row.tags?.trim()) {
|
|
50
|
+
testCase.tags = row.tags
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((t: string) => t.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return testCase;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static fromJSON(json: string): TestCase[] {
|
|
61
|
+
const data = JSON.parse(json);
|
|
62
|
+
const arr = Array.isArray(data) ? data : data.testCases ?? data.tests;
|
|
63
|
+
|
|
64
|
+
if (!Array.isArray(arr)) {
|
|
65
|
+
throw new Error('JSON must be an array or contain a testCases/tests array');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return arr.map((item: any, i: number) => {
|
|
69
|
+
if (!item.id || !item.question) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Item ${i}: missing required field(s) — id and question are required`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const testCase: TestCase = { id: item.id, question: item.question };
|
|
75
|
+
if (item.expectedAnswer) testCase.expectedAnswer = item.expectedAnswer;
|
|
76
|
+
if (item.expectedTools) testCase.expectedTools = item.expectedTools;
|
|
77
|
+
if (item.persona) testCase.persona = item.persona;
|
|
78
|
+
if (item.tags) testCase.tags = item.tags;
|
|
79
|
+
if (item.metadata) testCase.metadata = item.metadata;
|
|
80
|
+
return testCase;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { LLMProvider, LLMMessage } from '@operor/llm';
|
|
2
|
+
import type {
|
|
3
|
+
ConversationScenario,
|
|
4
|
+
ConversationTurn,
|
|
5
|
+
ConversationEvaluation,
|
|
6
|
+
CriteriaResult,
|
|
7
|
+
ConversationSuccessCriteria,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
function buildEvaluationPrompt(config: {
|
|
11
|
+
scenario: string;
|
|
12
|
+
persona: string;
|
|
13
|
+
turns: ConversationTurn[];
|
|
14
|
+
toolsCalled: Array<{ name: string; params: any; result: any }>;
|
|
15
|
+
expectedTools?: string[];
|
|
16
|
+
expectedOutcome?: string;
|
|
17
|
+
}): string {
|
|
18
|
+
const { scenario, persona, turns, toolsCalled, expectedTools, expectedOutcome } = config;
|
|
19
|
+
|
|
20
|
+
const conversationText = turns
|
|
21
|
+
.map((t, i) => `Turn ${i + 1} [${t.role}]: ${t.message}`)
|
|
22
|
+
.join('\n');
|
|
23
|
+
|
|
24
|
+
const toolsText = toolsCalled.length
|
|
25
|
+
? toolsCalled
|
|
26
|
+
.map((tc) => `- ${tc.name}(${JSON.stringify(tc.params)}) → ${JSON.stringify(tc.result)}`)
|
|
27
|
+
.join('\n')
|
|
28
|
+
: 'None';
|
|
29
|
+
|
|
30
|
+
const expectedToolsText = expectedTools?.length ? expectedTools.join(', ') : 'Not specified';
|
|
31
|
+
const expectedOutcomeText = expectedOutcome || 'Not specified';
|
|
32
|
+
|
|
33
|
+
return `You are evaluating a customer support conversation for testing purposes.
|
|
34
|
+
|
|
35
|
+
Scenario: ${scenario}
|
|
36
|
+
Customer Persona: ${persona}
|
|
37
|
+
Expected Tools: ${expectedToolsText}
|
|
38
|
+
Expected Outcome: ${expectedOutcomeText}
|
|
39
|
+
|
|
40
|
+
Conversation:
|
|
41
|
+
${conversationText}
|
|
42
|
+
|
|
43
|
+
Tools Called:
|
|
44
|
+
${toolsText}
|
|
45
|
+
|
|
46
|
+
Evaluate the conversation on these dimensions (score 1-5 for each):
|
|
47
|
+
|
|
48
|
+
1. Accuracy (1-5): Did the agent provide factually correct information based on tool results?
|
|
49
|
+
2. Tool Usage (1-5): Did the agent call the right tools at the right time?
|
|
50
|
+
3. Tone (1-5): Was the agent's tone appropriate, professional, and empathetic?
|
|
51
|
+
4. Resolution (1-5): Was the customer's issue resolved or properly addressed?
|
|
52
|
+
|
|
53
|
+
Overall assessment:
|
|
54
|
+
- "pass": All criteria met, customer satisfied (scores mostly 4-5)
|
|
55
|
+
- "partial": Some issues but acceptable (scores mostly 3-4)
|
|
56
|
+
- "fail": Significant problems (any score 1-2, or multiple scores below 3)
|
|
57
|
+
|
|
58
|
+
Respond with ONLY valid JSON (no markdown, no code fences):
|
|
59
|
+
{
|
|
60
|
+
"overall": "pass" | "fail" | "partial",
|
|
61
|
+
"scores": {
|
|
62
|
+
"accuracy": <integer 1-5>,
|
|
63
|
+
"toolUsage": <integer 1-5>,
|
|
64
|
+
"tone": <integer 1-5>,
|
|
65
|
+
"resolution": <integer 1-5>
|
|
66
|
+
},
|
|
67
|
+
"feedback": "<brief explanation of the evaluation>"
|
|
68
|
+
}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseEvaluationResponse(text: string): ConversationEvaluation {
|
|
72
|
+
const cleaned = text.replace(/```(?:json)?\s*/g, '').replace(/```/g, '').trim();
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(cleaned);
|
|
75
|
+
return {
|
|
76
|
+
overall: parsed.overall || 'fail',
|
|
77
|
+
scores: {
|
|
78
|
+
accuracy: Math.round(parsed.scores?.accuracy ?? 1),
|
|
79
|
+
toolUsage: Math.round(parsed.scores?.toolUsage ?? 1),
|
|
80
|
+
tone: Math.round(parsed.scores?.tone ?? 1),
|
|
81
|
+
resolution: Math.round(parsed.scores?.resolution ?? 1),
|
|
82
|
+
},
|
|
83
|
+
feedback: String(parsed.feedback ?? 'No feedback provided'),
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
overall: 'fail',
|
|
88
|
+
scores: { accuracy: 1, toolUsage: 1, tone: 1, resolution: 1 },
|
|
89
|
+
feedback: 'Failed to parse evaluation response',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function evaluateCriteria(
|
|
95
|
+
criteria: ConversationSuccessCriteria,
|
|
96
|
+
turns: ConversationTurn[]
|
|
97
|
+
): CriteriaResult {
|
|
98
|
+
const { type, value } = criteria;
|
|
99
|
+
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'tool_called': {
|
|
102
|
+
const toolName = String(value);
|
|
103
|
+
const called = turns.some((turn) =>
|
|
104
|
+
turn.toolCalls?.some((tc) => tc.name === toolName)
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
criteria,
|
|
108
|
+
passed: called,
|
|
109
|
+
details: called
|
|
110
|
+
? `Tool "${toolName}" was called`
|
|
111
|
+
: `Tool "${toolName}" was not called`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'response_contains': {
|
|
116
|
+
const searchText = String(value).toLowerCase();
|
|
117
|
+
const found = turns.some(
|
|
118
|
+
(turn) => turn.role === 'agent' && turn.message.toLowerCase().includes(searchText)
|
|
119
|
+
);
|
|
120
|
+
return {
|
|
121
|
+
criteria,
|
|
122
|
+
passed: found,
|
|
123
|
+
details: found
|
|
124
|
+
? `Agent response contains "${value}"`
|
|
125
|
+
: `Agent response does not contain "${value}"`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case 'intent_matched': {
|
|
130
|
+
// Intent matching would require intent data in turns, which we don't have in the simplified model
|
|
131
|
+
// For now, treat as passed if the value is in any message
|
|
132
|
+
const intentText = String(value).toLowerCase();
|
|
133
|
+
const matched = turns.some((turn) => turn.message.toLowerCase().includes(intentText));
|
|
134
|
+
return {
|
|
135
|
+
criteria,
|
|
136
|
+
passed: matched,
|
|
137
|
+
details: matched
|
|
138
|
+
? `Intent "${value}" was matched`
|
|
139
|
+
: `Intent "${value}" was not matched`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case 'turns_under': {
|
|
144
|
+
const maxTurns = Number(value);
|
|
145
|
+
const actualTurns = turns.length;
|
|
146
|
+
const passed = actualTurns < maxTurns;
|
|
147
|
+
return {
|
|
148
|
+
criteria,
|
|
149
|
+
passed,
|
|
150
|
+
details: passed
|
|
151
|
+
? `Conversation completed in ${actualTurns} turns (under ${maxTurns})`
|
|
152
|
+
: `Conversation took ${actualTurns} turns (expected under ${maxTurns})`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'custom': {
|
|
157
|
+
if (typeof value === 'function') {
|
|
158
|
+
try {
|
|
159
|
+
const passed = value(turns);
|
|
160
|
+
return {
|
|
161
|
+
criteria,
|
|
162
|
+
passed,
|
|
163
|
+
details: passed ? 'Custom criteria passed' : 'Custom criteria failed',
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
criteria,
|
|
168
|
+
passed: false,
|
|
169
|
+
details: `Custom criteria error: ${error}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
criteria,
|
|
175
|
+
passed: false,
|
|
176
|
+
details: 'Custom criteria value must be a function',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
return {
|
|
182
|
+
criteria,
|
|
183
|
+
passed: false,
|
|
184
|
+
details: `Unknown criteria type: ${type}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class ConversationEvaluator {
|
|
190
|
+
private llm?: LLMProvider;
|
|
191
|
+
|
|
192
|
+
constructor(options?: { llmProvider?: LLMProvider }) {
|
|
193
|
+
this.llm = options?.llmProvider;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async evaluate(config: {
|
|
197
|
+
scenario: string;
|
|
198
|
+
persona: string;
|
|
199
|
+
turns: ConversationTurn[];
|
|
200
|
+
toolsCalled: Array<{ name: string; params: any; result: any }>;
|
|
201
|
+
expectedTools?: string[];
|
|
202
|
+
expectedOutcome?: string;
|
|
203
|
+
successCriteria?: ConversationSuccessCriteria[];
|
|
204
|
+
}): Promise<ConversationEvaluation> {
|
|
205
|
+
const { turns, successCriteria } = config;
|
|
206
|
+
|
|
207
|
+
// Evaluate criteria-based checks
|
|
208
|
+
const criteriaResults: CriteriaResult[] = successCriteria
|
|
209
|
+
? successCriteria.map((criteria) => evaluateCriteria(criteria, turns))
|
|
210
|
+
: [];
|
|
211
|
+
|
|
212
|
+
// If LLM is available, use it for scoring
|
|
213
|
+
if (this.llm) {
|
|
214
|
+
const prompt = buildEvaluationPrompt(config);
|
|
215
|
+
const messages: LLMMessage[] = [{ role: 'user', content: prompt }];
|
|
216
|
+
|
|
217
|
+
const result = await this.llm.complete(messages, {
|
|
218
|
+
temperature: 0,
|
|
219
|
+
maxTokens: 1000,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const evaluation = parseEvaluationResponse(result.text);
|
|
223
|
+
evaluation.criteriaResults = criteriaResults;
|
|
224
|
+
|
|
225
|
+
// If any criteria failed, downgrade overall to at least 'partial'
|
|
226
|
+
const criteriaPassed = criteriaResults.every((cr) => cr.passed);
|
|
227
|
+
if (!criteriaPassed && evaluation.overall === 'pass') {
|
|
228
|
+
evaluation.overall = 'partial';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return evaluation;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Fallback: criteria-only evaluation (no LLM)
|
|
235
|
+
if (!criteriaResults.length) {
|
|
236
|
+
return {
|
|
237
|
+
overall: 'fail',
|
|
238
|
+
scores: { accuracy: 1, toolUsage: 1, tone: 1, resolution: 1 },
|
|
239
|
+
feedback: 'No criteria specified',
|
|
240
|
+
criteriaResults,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const allPassed = criteriaResults.every((cr) => cr.passed);
|
|
245
|
+
const somePassed = criteriaResults.some((cr) => cr.passed);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
overall: allPassed ? 'pass' : somePassed ? 'partial' : 'fail',
|
|
249
|
+
scores: { accuracy: 3, toolUsage: 3, tone: 3, resolution: 3 },
|
|
250
|
+
feedback: criteriaResults.map((cr) => cr.details).join('; '),
|
|
251
|
+
criteriaResults,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|