// Shared export logic used by both CLI commands and MCP tools import * as fs from 'fs'; import * as path from 'path'; import { IndemnClient } from '../sdk/client.js'; import { EvalSDK } from '../sdk/eval.js'; import { RubricsSDK } from '../sdk/rubrics.js'; import { TestSetsSDK } from '../sdk/test-sets.js'; import { AgentsSDK } from '../sdk/agents.js'; import { generateAgentCardPDF, generateEvalReportPDF } from './generate.js'; import { getRunScoreMetrics, getRunCriteriaScore, getRunRubricScore, getResultsScoreMetrics } from './scoring.js'; import type { AgentCardData, AgentCardTool, AgentCardKnowledgeBase } from './agent-card/types.js'; import type { EvalReportData } from './eval-report/document.js'; import type { EvaluationResult } from './eval-report/types.js'; // --------------------------------------------------------------------------- // Markdown export // --------------------------------------------------------------------------- export async function exportEvalMarkdown( runId: string, outputPath?: string, client?: IndemnClient, ): Promise<{ file_path: string; file_name: string; file_type: string }> { const c = client || new IndemnClient(); const evalSdk = new EvalSDK(c); const rubricsSdk = new RubricsSDK(c); const testSetsSdk = new TestSetsSDK(c); const [run, results] = await Promise.all([ evalSdk.getRunStatus(runId), evalSdk.getRunResults(runId), ]); const [rubric, testSet, botContext] = await Promise.all([ run.rubric_id ? rubricsSdk.getRubric(run.rubric_id).catch(() => null) : null, run.test_set_id ? testSetsSdk.getTestSet(run.test_set_id).catch(() => null) : null, run.agent_id ? evalSdk.getBotContext(run.agent_id).catch(() => null) : null, ]); const markdown = formatEvalMarkdown(run, results, rubric, testSet, botContext); const agentName = (botContext as any)?.bot_name || run.agent_id || 'Unknown'; const date = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const cleanName = agentName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_'); const cleanDate = date.replace(/[^a-zA-Z0-9]/g, '_'); const defaultFilename = `Indemn_Evaluation_${cleanName}_${cleanDate}.md`; const resolvedPath = path.resolve(outputPath || defaultFilename); fs.writeFileSync(resolvedPath, markdown); return { file_path: resolvedPath, file_name: path.basename(resolvedPath), file_type: 'markdown', }; } function formatEvalMarkdown( run: any, results: any[], rubric: any, testSet: any, botContext: any, ): string { const lines: string[] = []; const agentName = botContext?.bot_name || run.agent_id || 'Unknown'; const orgName = botContext?.organization_name || ''; const model = run.bot_llm_model ? `${run.bot_llm_provider || ''}/${run.bot_llm_model}` : 'N/A'; const date = run.completed_at || run.started_at || new Date().toISOString(); // Header lines.push(`# Evaluation Results — ${agentName}`); lines.push(''); lines.push(`**Run:** ${run.run_id}`); lines.push(`**Date:** ${new Date(date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}`); lines.push(`**Agent:** ${agentName} (${run.agent_id})`); if (orgName) lines.push(`**Organization:** ${orgName}`); lines.push(`**Model:** ${model}`); lines.push(`**Status:** ${run.status}`); lines.push(''); // Scores if (run.criteria_total !== undefined) { lines.push(`**Criteria:** ${run.criteria_passed ?? 0}/${run.criteria_total} checks passed (${run.criteria_total > 0 ? Math.round(((run.criteria_passed ?? 0) / run.criteria_total) * 100) : 0}%)`); } if (run.rubric_rules_total !== undefined && run.rubric_rules_total > 0) { lines.push(`**Rubric:** ${run.rubric_rules_passed ?? 0}/${run.rubric_rules_total} rules passed (${Math.round(((run.rubric_rules_passed ?? 0) / run.rubric_rules_total) * 100)}%)`); } if (run.total !== undefined) { lines.push(`**Items:** ${run.passed ?? 0}/${run.total} passed, ${run.failed ?? 0} failed`); } // Component scores if (run.component_scores) { lines.push(''); lines.push('**Component Scores:**'); for (const [scope, data] of Object.entries(run.component_scores as Record)) { if (data && data.total > 0) { lines.push(`- ${scope}: ${data.passed}/${data.total} (${Math.round((data.passed / data.total) * 100)}%)`); } } } // Rubric info if (rubric) { lines.push(''); lines.push(`**Rubric:** ${rubric.name} (v${rubric.version || 1}, ${rubric.rules?.length || 0} rules)`); } // Test set info if (testSet) { lines.push(`**Test Set:** ${testSet.name} (v${testSet.version || 1}, ${testSet.items?.length || 0} items)`); } lines.push(''); lines.push('---'); lines.push(''); // Per-result details for (let i = 0; i < results.length; i++) { const r = results[i]; const itemName = r.item_name || r.input?.message || `Test Item ${i + 1}`; const itemType = r.item_type || 'single_turn'; const passed = r.passed ? 'PASS' : 'FAIL'; // Match test item from test set let testItem: any = null; if (testSet?.items) { testItem = testSet.items.find((t: any) => t.item_id === r.test_case_id); } lines.push(`## ${i + 1}. ${itemName}`); lines.push(''); lines.push(`**Type:** ${itemType} | **Result:** ${passed}${r.duration_ms ? ` | **Duration:** ${(r.duration_ms / 1000).toFixed(1)}s` : ''}`); lines.push(''); // Conversation / response if (r.output?.messages && Array.isArray(r.output.messages)) { lines.push('### Conversation'); lines.push(''); for (const msg of r.output.messages) { lines.push(`> **${msg.role === 'user' ? 'User' : 'Agent'}:** ${msg.content}`); lines.push(''); } } else if (r.output?.response) { lines.push('### Response'); lines.push(''); lines.push(`> ${r.output.response}`); lines.push(''); } else if (r.output?.transcript) { lines.push('### Transcript'); lines.push(''); lines.push('```'); lines.push(r.output.transcript); lines.push('```'); lines.push(''); } // Criteria scores if (r.criteria_scores && r.criteria_scores.length > 0) { lines.push('### Criteria Scores'); lines.push(''); lines.push('| Criterion | Result | Score | Reasoning |'); lines.push('|-----------|--------|-------|-----------|'); for (const c of r.criteria_scores) { const reasoning = (c.reasoning || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200); lines.push(`| ${c.criterion.replace(/\|/g, '\\|')} | ${c.passed ? 'PASS' : 'FAIL'} | ${Math.round(c.score * 100)}% | ${reasoning} |`); } lines.push(''); } // Rubric scores if (r.rubric_scores && r.rubric_scores.length > 0) { lines.push('### Rubric Scores'); lines.push(''); lines.push('| Rule | Severity | Result | Score | Reasoning |'); lines.push('|------|----------|--------|-------|-----------|'); for (const rule of r.rubric_scores) { const reasoning = (rule.reasoning || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200); lines.push(`| ${rule.rule_name.replace(/\|/g, '\\|')} | ${rule.severity} | ${rule.passed ? 'PASS' : 'FAIL'} | ${Math.round(rule.score * 100)}% | ${reasoning} |`); } lines.push(''); } // V1 scores if (r.scores && Object.keys(r.scores).length > 0 && !r.criteria_scores) { lines.push('### Scores'); lines.push(''); lines.push('| Evaluator | Score | Comment |'); lines.push('|-----------|-------|---------|'); for (const [key, val] of Object.entries(r.scores as Record)) { const comment = (val.comment || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200); lines.push(`| ${key} | ${val.score} | ${comment} |`); } lines.push(''); } if (i < results.length - 1) { lines.push('---'); lines.push(''); } } return lines.join('\n'); } // --------------------------------------------------------------------------- // Eval report PDF // --------------------------------------------------------------------------- export async function exportEvalReport( runId: string, outputPath?: string, client?: IndemnClient, ): Promise<{ file_path: string; file_name: string; file_type: string }> { const c = client || new IndemnClient(); const evalSdk = new EvalSDK(c); const [run, results] = await Promise.all([ evalSdk.getRunStatus(runId), evalSdk.getRunResults(runId), ]); const botContext = run.agent_id ? await evalSdk.getBotContext(run.agent_id).catch(() => null) : null; const isV2 = results.length > 0 && (results[0] as any).criteria_scores != null; const customerName = (botContext as any)?.organization_name || (botContext as any)?.bot_name || run.agent_id || 'Unknown'; const reportDate = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); let reportData: EvalReportData; if (isV2) { reportData = { customerName, reportDate, v2Results: results as unknown as EvaluationResult[] }; } else { // V1 — for now throw a clear error since V1 matrix transform requires evaluator config throw new Error('V1 report format not yet supported for PDF export. Use "eval export" for markdown output.'); } const filePath = await generateEvalReportPDF(reportData, outputPath); return { file_path: filePath, file_name: path.basename(filePath), file_type: 'pdf', }; } // --------------------------------------------------------------------------- // Agent card PDF // --------------------------------------------------------------------------- export async function exportAgentCard( agentId: string, outputPath?: string, client?: IndemnClient, ): Promise<{ file_path: string; file_name: string; file_type: string }> { const c = client || new IndemnClient(); const agentsSdk = new AgentsSDK(c); const evalSdk = new EvalSDK(c); const agent = await agentsSdk.getAgent(agentId); // Fetch config and functions from copilot server let config: any = null; let functions: any[] = []; try { config = await c.copilotGet(`/organization/:org_id/ai-studio/bots/${agentId}/configurations`); } catch { /* config may not be available */ } try { functions = await c.copilotGet(`/organization/:org_id/ai-studio/bots/${agentId}/functions`) as any[]; } catch { /* functions may not be available */ } // Fetch bot context from eval service for additional data let botContext: any = null; try { botContext = await evalSdk.getBotContext(agentId); } catch { /* eval service may not have this agent */ } // Fetch eval runs let evalRuns: any[] = []; try { evalRuns = await evalSdk.listRuns(agentId); } catch { /* evals may not be available */ } const cardData = mapToAgentCardData(agent, config, functions, evalRuns, botContext); const filePath = await generateAgentCardPDF(cardData, outputPath); return { file_path: filePath, file_name: path.basename(filePath), file_type: 'pdf', }; } function mapToAgentCardData( agent: any, config: any, functions: any[], evalRuns: any[], botContext: any, ): AgentCardData { const generatedDate = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); // Map tools from bot context (richer) or functions const tools: AgentCardTool[] = (botContext?.tools || functions || []).map((f: any) => ({ id: f.id || f._id || '', name: f.name || f.tool_name || '', tool_name: f.tool_name || f.name || '', type: f.type || 'unknown', description: f.description, })); // Map knowledge bases from bot context const knowledge_bases: AgentCardKnowledgeBase[] = (botContext?.knowledge_bases || []).map((kb: any) => ({ id: kb.id || kb._id || '', name: kb.name || '', type: kb.type || 'unknown', document_count: kb.document_count || 0, })); // System prompt const system_prompt = botContext?.system_prompt || config?.ai_config?.system_prompt; // LLM config const llm_config = botContext?.llm_config ? { provider: botContext.llm_config.provider, model: botContext.llm_config.model, parameters: botContext.llm_config.parameters, } : undefined; // Map eval runs const completedRuns = evalRuns.filter((r: any) => r.status === 'completed'); const evaluationRuns = completedRuns.map((r: any) => { const metrics = getRunScoreMetrics(r); return { run_id: r.run_id, status: r.status, created_at: r.created_at || r.started_at || '', completed_at: r.completed_at, bot_llm_model: r.bot_llm_model, bot_llm_provider: r.bot_llm_provider, criteriaScore: getRunCriteriaScore(r), rubricScore: getRunRubricScore(r), criteriaPassed: metrics.criteriaPassed, criteriaTotal: metrics.criteriaTotal, rubricRulesPassed: metrics.rubricRulesPassed, rubricRulesTotal: metrics.rubricRulesTotal, total: r.total || 0, completed: r.completed || 0, }; }); // Evaluation summary across all completed runs let evaluationSummary = undefined; if (completedRuns.length > 0) { let totalCriteriaPassed = 0, totalCriteriaTotal = 0; let totalRubricPassed = 0, totalRubricTotal = 0; for (const r of completedRuns) { const m = getRunScoreMetrics(r); totalCriteriaPassed += m.criteriaPassed; totalCriteriaTotal += m.criteriaTotal; totalRubricPassed += m.rubricRulesPassed; totalRubricTotal += m.rubricRulesTotal; } evaluationSummary = { criteriaRate: totalCriteriaTotal > 0 ? totalCriteriaPassed / totalCriteriaTotal : null, rubricRate: totalRubricTotal > 0 ? totalRubricPassed / totalRubricTotal : null, criteriaPassed: totalCriteriaPassed, criteriaTotal: totalCriteriaTotal, rubricRulesPassed: totalRubricPassed, rubricRulesTotal: totalRubricTotal, totalRuns: completedRuns.length, }; } // Component scores from latest run const latestRun = completedRuns[0]; const componentScores = latestRun?.component_scores; return { id: agent._id, name: agent.name || 'Unnamed Agent', description: agent.description, channels: (agent.channels || ['WEB']).map((c: any) => typeof c === 'string' ? c : c.name || c.type || String(c) ), organization_id: agent.id_project || (botContext as any)?.organization_id, system_prompt, tools, knowledge_bases, llm_config, generatedDate, evaluationSummary, componentScores, evaluationRuns, }; }