@indemn/cli 1.0.0 → 1.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.
Files changed (104) hide show
  1. package/.claude-plugin/marketplace.json +28 -0
  2. package/README.md +29 -23
  3. package/dist/cli/agents.d.ts.map +1 -1
  4. package/dist/cli/agents.js +20 -0
  5. package/dist/cli/agents.js.map +1 -1
  6. package/dist/cli/eval.d.ts.map +1 -1
  7. package/dist/cli/eval.js +38 -0
  8. package/dist/cli/eval.js.map +1 -1
  9. package/dist/mcp/tools.d.ts.map +1 -1
  10. package/dist/mcp/tools.js +40 -0
  11. package/dist/mcp/tools.js.map +1 -1
  12. package/dist/report/agent-card/cover.d.ts +20 -0
  13. package/dist/report/agent-card/cover.d.ts.map +1 -0
  14. package/dist/report/agent-card/cover.js +58 -0
  15. package/dist/report/agent-card/cover.js.map +1 -0
  16. package/dist/report/agent-card/document.d.ts +7 -0
  17. package/dist/report/agent-card/document.d.ts.map +1 -0
  18. package/dist/report/agent-card/document.js +144 -0
  19. package/dist/report/agent-card/document.js.map +1 -0
  20. package/dist/report/agent-card/types.d.ts +77 -0
  21. package/dist/report/agent-card/types.d.ts.map +1 -0
  22. package/dist/report/agent-card/types.js +3 -0
  23. package/dist/report/agent-card/types.js.map +1 -0
  24. package/dist/report/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  25. package/dist/report/assets/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  26. package/dist/report/assets/assets/fonts/Barlow-Bold.ttf +0 -0
  27. package/dist/report/assets/assets/fonts/Barlow-Medium.ttf +0 -0
  28. package/dist/report/assets/assets/fonts/Barlow-Regular.ttf +0 -0
  29. package/dist/report/assets/assets/fonts/Barlow-SemiBold.ttf +0 -0
  30. package/dist/report/assets/fonts/Barlow-Bold.ttf +0 -0
  31. package/dist/report/assets/fonts/Barlow-Medium.ttf +0 -0
  32. package/dist/report/assets/fonts/Barlow-Regular.ttf +0 -0
  33. package/dist/report/assets/fonts/Barlow-SemiBold.ttf +0 -0
  34. package/dist/report/eval-report/cover.d.ts +16 -0
  35. package/dist/report/eval-report/cover.d.ts.map +1 -0
  36. package/dist/report/eval-report/cover.js +8 -0
  37. package/dist/report/eval-report/cover.js.map +1 -0
  38. package/dist/report/eval-report/document.d.ts +19 -0
  39. package/dist/report/eval-report/document.d.ts.map +1 -0
  40. package/dist/report/eval-report/document.js +32 -0
  41. package/dist/report/eval-report/document.js.map +1 -0
  42. package/dist/report/eval-report/example-page.d.ts +11 -0
  43. package/dist/report/eval-report/example-page.d.ts.map +1 -0
  44. package/dist/report/eval-report/example-page.js +18 -0
  45. package/dist/report/eval-report/example-page.js.map +1 -0
  46. package/dist/report/eval-report/item-page.d.ts +9 -0
  47. package/dist/report/eval-report/item-page.d.ts.map +1 -0
  48. package/dist/report/eval-report/item-page.js +72 -0
  49. package/dist/report/eval-report/item-page.js.map +1 -0
  50. package/dist/report/eval-report/matrix-page.d.ts +8 -0
  51. package/dist/report/eval-report/matrix-page.d.ts.map +1 -0
  52. package/dist/report/eval-report/matrix-page.js +31 -0
  53. package/dist/report/eval-report/matrix-page.js.map +1 -0
  54. package/dist/report/eval-report/types.d.ts +69 -0
  55. package/dist/report/eval-report/types.d.ts.map +1 -0
  56. package/dist/report/eval-report/types.js +7 -0
  57. package/dist/report/eval-report/types.js.map +1 -0
  58. package/dist/report/eval-report/v2-cover.d.ts +9 -0
  59. package/dist/report/eval-report/v2-cover.d.ts.map +1 -0
  60. package/dist/report/eval-report/v2-cover.js +32 -0
  61. package/dist/report/eval-report/v2-cover.js.map +1 -0
  62. package/dist/report/export.d.ts +17 -0
  63. package/dist/report/export.d.ts.map +1 -0
  64. package/dist/report/export.js +327 -0
  65. package/dist/report/export.js.map +1 -0
  66. package/dist/report/generate.d.ts +7 -0
  67. package/dist/report/generate.d.ts.map +1 -0
  68. package/dist/report/generate.js +30 -0
  69. package/dist/report/generate.js.map +1 -0
  70. package/dist/report/scoring.d.ts +90 -0
  71. package/dist/report/scoring.d.ts.map +1 -0
  72. package/dist/report/scoring.js +174 -0
  73. package/dist/report/scoring.js.map +1 -0
  74. package/dist/report/styles.d.ts +111 -0
  75. package/dist/report/styles.d.ts.map +1 -0
  76. package/dist/report/styles.js +134 -0
  77. package/dist/report/styles.js.map +1 -0
  78. package/docs/plans/2026-03-13-cli-exports.md +819 -0
  79. package/package.json +14 -4
  80. package/skills/eval-analysis/SKILL.md +178 -0
  81. package/skills/eval-analysis/references/data-shapes.md +279 -0
  82. package/src/cli/agents.ts +22 -0
  83. package/src/cli/eval.ts +42 -0
  84. package/src/mcp/tools.ts +50 -0
  85. package/src/report/agent-card/cover.tsx +232 -0
  86. package/src/report/agent-card/document.tsx +582 -0
  87. package/src/report/agent-card/types.ts +86 -0
  88. package/src/report/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  89. package/src/report/assets/fonts/Barlow-Bold.ttf +0 -0
  90. package/src/report/assets/fonts/Barlow-Medium.ttf +0 -0
  91. package/src/report/assets/fonts/Barlow-Regular.ttf +0 -0
  92. package/src/report/assets/fonts/Barlow-SemiBold.ttf +0 -0
  93. package/src/report/eval-report/cover.tsx +69 -0
  94. package/src/report/eval-report/document.tsx +105 -0
  95. package/src/report/eval-report/example-page.tsx +121 -0
  96. package/src/report/eval-report/item-page.tsx +211 -0
  97. package/src/report/eval-report/matrix-page.tsx +124 -0
  98. package/src/report/eval-report/types.ts +77 -0
  99. package/src/report/eval-report/v2-cover.tsx +102 -0
  100. package/src/report/export.ts +394 -0
  101. package/src/report/generate.tsx +37 -0
  102. package/src/report/scoring.ts +281 -0
  103. package/src/report/styles.ts +138 -0
  104. package/tsconfig.json +2 -1
@@ -0,0 +1,77 @@
1
+ // Eval report types
2
+
3
+ export type Severity = 'High' | 'Medium' | 'Low';
4
+
5
+ export interface EvaluatorInfo {
6
+ key: string;
7
+ name: string;
8
+ severity: Severity;
9
+ prompt?: string;
10
+ model?: string;
11
+ }
12
+
13
+ export interface MatrixCell {
14
+ score: number;
15
+ comment: string | null;
16
+ passed: boolean;
17
+ }
18
+
19
+ export interface MatrixRow {
20
+ resultId: string;
21
+ testCaseId: string;
22
+ question: string;
23
+ response: string;
24
+ cells: Record<string, MatrixCell>;
25
+ avgScore: number;
26
+ }
27
+
28
+ export interface MatrixData {
29
+ evaluators: EvaluatorInfo[];
30
+ rows: MatrixRow[];
31
+ columnAverages: Record<string, number>;
32
+ overallAverage: number;
33
+ }
34
+
35
+ export interface WeightConfig {
36
+ High: number;
37
+ Medium: number;
38
+ Low: number;
39
+ }
40
+
41
+ export const DEFAULT_WEIGHTS: WeightConfig = {
42
+ High: 2,
43
+ Medium: 1,
44
+ Low: 0.5,
45
+ };
46
+
47
+ // V2 result types
48
+ export interface CriterionScore {
49
+ criterion: string;
50
+ passed: boolean;
51
+ score: number;
52
+ reasoning: string;
53
+ }
54
+
55
+ export interface RubricRuleScore {
56
+ rule_id: string;
57
+ rule_name: string;
58
+ severity: string;
59
+ passed: boolean;
60
+ score: number;
61
+ reasoning: string;
62
+ }
63
+
64
+ export interface EvaluationResult {
65
+ result_id: string;
66
+ test_case_id: string;
67
+ input: { message?: string } & Record<string, unknown>;
68
+ output: { response?: string } & Record<string, unknown>;
69
+ scores: Record<string, { score: number; comment: string | null }>;
70
+ item_name?: string;
71
+ item_type?: 'single_turn' | 'scenario' | 'transcript' | 'voice_simulation';
72
+ criteria_scores?: CriterionScore[];
73
+ criteria_passed?: boolean;
74
+ rubric_scores?: RubricRuleScore[];
75
+ rubric_passed?: boolean;
76
+ passed?: boolean;
77
+ }
@@ -0,0 +1,102 @@
1
+ // V2 eval report cover — ported from EvalReportV2Cover.tsx
2
+ import { Page, View, Text, Image } from '@react-pdf/renderer';
3
+ import { styles, colors, logoPath } from '../styles.js';
4
+ import { getResultsScoreMetrics, getScoreHexColor } from '../scoring.js';
5
+ import type { EvaluationResult } from './types.js';
6
+
7
+ interface EvalReportV2CoverProps {
8
+ customerName: string;
9
+ reportDate: string;
10
+ results: EvaluationResult[];
11
+ }
12
+
13
+ export function EvalReportV2Cover({ customerName, reportDate, results }: EvalReportV2CoverProps) {
14
+ const metrics = getResultsScoreMetrics(results);
15
+ const criteriaRate = metrics.criteriaRate !== null ? Math.round(metrics.criteriaRate * 100) : 0;
16
+ const rubricRate = metrics.rubricRate !== null ? Math.round(metrics.rubricRate * 100) : null;
17
+
18
+ const singleTurnCount = results.filter(r => r.item_type === 'single_turn' || r.item_type === undefined).length;
19
+ const scenarioCount = results.filter(r => r.item_type === 'scenario').length;
20
+ const transcriptCount = results.filter(r => r.item_type === 'transcript').length;
21
+
22
+ return (
23
+ <Page size="A4" style={[styles.page, { justifyContent: 'center' }]}>
24
+ {/* Logo */}
25
+ <View style={{ alignItems: 'center', marginBottom: 48 }}>
26
+ <Image src={logoPath} style={{ width: 200, height: 46 }} />
27
+ </View>
28
+
29
+ {/* Title */}
30
+ <View style={{ alignItems: 'center', marginBottom: 48 }}>
31
+ <Text style={{ fontSize: 28, fontWeight: 700, color: colors.eggplant, marginBottom: 8 }}>
32
+ Evaluation Report
33
+ </Text>
34
+ <Text style={{ fontSize: 16, fontWeight: 500, color: colors.muted }}>
35
+ {customerName}
36
+ </Text>
37
+ <Text style={{ fontSize: 14, fontWeight: 400, color: colors.muted, marginTop: 4 }}>
38
+ {reportDate}
39
+ </Text>
40
+ </View>
41
+
42
+ {/* Key Highlights */}
43
+ <View style={[styles.card, { marginHorizontal: 40 }]}>
44
+ <Text style={[styles.sectionTitle, { textAlign: 'center', marginBottom: 16 }]}>
45
+ Key Highlights
46
+ </Text>
47
+
48
+ <View style={{ gap: 8 }}>
49
+ {/* Scores - side by side */}
50
+ <View style={{ flexDirection: 'row', justifyContent: 'center', gap: 32, marginBottom: 8 }}>
51
+ {metrics.criteriaTotal > 0 && (
52
+ <View style={{ alignItems: 'center' }}>
53
+ <Text style={{
54
+ fontSize: 40,
55
+ fontWeight: 700,
56
+ color: getScoreHexColor(metrics.criteriaRate),
57
+ }}>
58
+ {criteriaRate}%
59
+ </Text>
60
+ <Text style={{ fontSize: 12, fontWeight: 500, color: colors.muted }}>
61
+ Criteria ({metrics.criteriaPassed}/{metrics.criteriaTotal} checks)
62
+ </Text>
63
+ </View>
64
+ )}
65
+ {rubricRate !== null && (
66
+ <View style={{ alignItems: 'center' }}>
67
+ <Text style={{
68
+ fontSize: 40,
69
+ fontWeight: 700,
70
+ color: getScoreHexColor(metrics.rubricRate),
71
+ }}>
72
+ {rubricRate}%
73
+ </Text>
74
+ <Text style={{ fontSize: 12, fontWeight: 500, color: colors.muted }}>
75
+ Rubric ({metrics.rubricRulesPassed}/{metrics.rubricRulesTotal} rules)
76
+ </Text>
77
+ </View>
78
+ )}
79
+ </View>
80
+
81
+ {/* Item type breakdown */}
82
+ <Text style={styles.bodyText}>
83
+ {'\u2022'} {results.length} test items evaluated
84
+ {(() => {
85
+ const parts: string[] = [];
86
+ if (singleTurnCount > 0) parts.push(`${singleTurnCount} single-turn`);
87
+ if (scenarioCount > 0) parts.push(`${scenarioCount} scenario${scenarioCount > 1 ? 's' : ''}`);
88
+ if (transcriptCount > 0) parts.push(`${transcriptCount} transcript${transcriptCount > 1 ? 's' : ''}`);
89
+ return parts.length > 1 ? ` (${parts.join(', ')})` : parts.length === 1 ? ` (all ${parts[0]})` : '';
90
+ })()}
91
+ </Text>
92
+ </View>
93
+ </View>
94
+
95
+ {/* Footer */}
96
+ <View style={[styles.footer, { position: 'absolute', bottom: 40 }]}>
97
+ <Text style={styles.caption}>Powered by Indemn Evals</Text>
98
+ <Text style={styles.caption}>Page 1</Text>
99
+ </View>
100
+ </Page>
101
+ );
102
+ }
@@ -0,0 +1,394 @@
1
+ // Shared export logic used by both CLI commands and MCP tools
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { IndemnClient } from '../sdk/client.js';
5
+ import { EvalSDK } from '../sdk/eval.js';
6
+ import { RubricsSDK } from '../sdk/rubrics.js';
7
+ import { TestSetsSDK } from '../sdk/test-sets.js';
8
+ import { AgentsSDK } from '../sdk/agents.js';
9
+ import { generateAgentCardPDF, generateEvalReportPDF } from './generate.js';
10
+ import { getRunScoreMetrics, getRunCriteriaScore, getRunRubricScore, getResultsScoreMetrics } from './scoring.js';
11
+ import type { AgentCardData, AgentCardTool, AgentCardKnowledgeBase } from './agent-card/types.js';
12
+ import type { EvalReportData } from './eval-report/document.js';
13
+ import type { EvaluationResult } from './eval-report/types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Markdown export
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export async function exportEvalMarkdown(
20
+ runId: string,
21
+ outputPath?: string,
22
+ client?: IndemnClient,
23
+ ): Promise<{ file_path: string; file_name: string; file_type: string }> {
24
+ const c = client || new IndemnClient();
25
+ const evalSdk = new EvalSDK(c);
26
+ const rubricsSdk = new RubricsSDK(c);
27
+ const testSetsSdk = new TestSetsSDK(c);
28
+
29
+ const [run, results] = await Promise.all([
30
+ evalSdk.getRunStatus(runId),
31
+ evalSdk.getRunResults(runId),
32
+ ]);
33
+
34
+ const [rubric, testSet, botContext] = await Promise.all([
35
+ run.rubric_id ? rubricsSdk.getRubric(run.rubric_id).catch(() => null) : null,
36
+ run.test_set_id ? testSetsSdk.getTestSet(run.test_set_id).catch(() => null) : null,
37
+ run.agent_id ? evalSdk.getBotContext(run.agent_id).catch(() => null) : null,
38
+ ]);
39
+
40
+ const markdown = formatEvalMarkdown(run, results, rubric, testSet, botContext);
41
+ const agentName = (botContext as any)?.bot_name || run.agent_id || 'Unknown';
42
+ const date = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
43
+ const cleanName = agentName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_');
44
+ const cleanDate = date.replace(/[^a-zA-Z0-9]/g, '_');
45
+ const defaultFilename = `Indemn_Evaluation_${cleanName}_${cleanDate}.md`;
46
+
47
+ const resolvedPath = path.resolve(outputPath || defaultFilename);
48
+ fs.writeFileSync(resolvedPath, markdown);
49
+
50
+ return {
51
+ file_path: resolvedPath,
52
+ file_name: path.basename(resolvedPath),
53
+ file_type: 'markdown',
54
+ };
55
+ }
56
+
57
+ function formatEvalMarkdown(
58
+ run: any,
59
+ results: any[],
60
+ rubric: any,
61
+ testSet: any,
62
+ botContext: any,
63
+ ): string {
64
+ const lines: string[] = [];
65
+ const agentName = botContext?.bot_name || run.agent_id || 'Unknown';
66
+ const orgName = botContext?.organization_name || '';
67
+ const model = run.bot_llm_model ? `${run.bot_llm_provider || ''}/${run.bot_llm_model}` : 'N/A';
68
+ const date = run.completed_at || run.started_at || new Date().toISOString();
69
+
70
+ // Header
71
+ lines.push(`# Evaluation Results — ${agentName}`);
72
+ lines.push('');
73
+ lines.push(`**Run:** ${run.run_id}`);
74
+ lines.push(`**Date:** ${new Date(date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}`);
75
+ lines.push(`**Agent:** ${agentName} (${run.agent_id})`);
76
+ if (orgName) lines.push(`**Organization:** ${orgName}`);
77
+ lines.push(`**Model:** ${model}`);
78
+ lines.push(`**Status:** ${run.status}`);
79
+ lines.push('');
80
+
81
+ // Scores
82
+ if (run.criteria_total !== undefined) {
83
+ 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}%)`);
84
+ }
85
+ if (run.rubric_rules_total !== undefined && run.rubric_rules_total > 0) {
86
+ 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)}%)`);
87
+ }
88
+ if (run.total !== undefined) {
89
+ lines.push(`**Items:** ${run.passed ?? 0}/${run.total} passed, ${run.failed ?? 0} failed`);
90
+ }
91
+
92
+ // Component scores
93
+ if (run.component_scores) {
94
+ lines.push('');
95
+ lines.push('**Component Scores:**');
96
+ for (const [scope, data] of Object.entries(run.component_scores as Record<string, any>)) {
97
+ if (data && data.total > 0) {
98
+ lines.push(`- ${scope}: ${data.passed}/${data.total} (${Math.round((data.passed / data.total) * 100)}%)`);
99
+ }
100
+ }
101
+ }
102
+
103
+ // Rubric info
104
+ if (rubric) {
105
+ lines.push('');
106
+ lines.push(`**Rubric:** ${rubric.name} (v${rubric.version || 1}, ${rubric.rules?.length || 0} rules)`);
107
+ }
108
+
109
+ // Test set info
110
+ if (testSet) {
111
+ lines.push(`**Test Set:** ${testSet.name} (v${testSet.version || 1}, ${testSet.items?.length || 0} items)`);
112
+ }
113
+
114
+ lines.push('');
115
+ lines.push('---');
116
+ lines.push('');
117
+
118
+ // Per-result details
119
+ for (let i = 0; i < results.length; i++) {
120
+ const r = results[i];
121
+ const itemName = r.item_name || r.input?.message || `Test Item ${i + 1}`;
122
+ const itemType = r.item_type || 'single_turn';
123
+ const passed = r.passed ? 'PASS' : 'FAIL';
124
+
125
+ // Match test item from test set
126
+ let testItem: any = null;
127
+ if (testSet?.items) {
128
+ testItem = testSet.items.find((t: any) => t.item_id === r.test_case_id);
129
+ }
130
+
131
+ lines.push(`## ${i + 1}. ${itemName}`);
132
+ lines.push('');
133
+ lines.push(`**Type:** ${itemType} | **Result:** ${passed}${r.duration_ms ? ` | **Duration:** ${(r.duration_ms / 1000).toFixed(1)}s` : ''}`);
134
+ lines.push('');
135
+
136
+ // Conversation / response
137
+ if (r.output?.messages && Array.isArray(r.output.messages)) {
138
+ lines.push('### Conversation');
139
+ lines.push('');
140
+ for (const msg of r.output.messages) {
141
+ lines.push(`> **${msg.role === 'user' ? 'User' : 'Agent'}:** ${msg.content}`);
142
+ lines.push('');
143
+ }
144
+ } else if (r.output?.response) {
145
+ lines.push('### Response');
146
+ lines.push('');
147
+ lines.push(`> ${r.output.response}`);
148
+ lines.push('');
149
+ } else if (r.output?.transcript) {
150
+ lines.push('### Transcript');
151
+ lines.push('');
152
+ lines.push('```');
153
+ lines.push(r.output.transcript);
154
+ lines.push('```');
155
+ lines.push('');
156
+ }
157
+
158
+ // Criteria scores
159
+ if (r.criteria_scores && r.criteria_scores.length > 0) {
160
+ lines.push('### Criteria Scores');
161
+ lines.push('');
162
+ lines.push('| Criterion | Result | Score | Reasoning |');
163
+ lines.push('|-----------|--------|-------|-----------|');
164
+ for (const c of r.criteria_scores) {
165
+ const reasoning = (c.reasoning || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200);
166
+ lines.push(`| ${c.criterion.replace(/\|/g, '\\|')} | ${c.passed ? 'PASS' : 'FAIL'} | ${Math.round(c.score * 100)}% | ${reasoning} |`);
167
+ }
168
+ lines.push('');
169
+ }
170
+
171
+ // Rubric scores
172
+ if (r.rubric_scores && r.rubric_scores.length > 0) {
173
+ lines.push('### Rubric Scores');
174
+ lines.push('');
175
+ lines.push('| Rule | Severity | Result | Score | Reasoning |');
176
+ lines.push('|------|----------|--------|-------|-----------|');
177
+ for (const rule of r.rubric_scores) {
178
+ const reasoning = (rule.reasoning || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200);
179
+ lines.push(`| ${rule.rule_name.replace(/\|/g, '\\|')} | ${rule.severity} | ${rule.passed ? 'PASS' : 'FAIL'} | ${Math.round(rule.score * 100)}% | ${reasoning} |`);
180
+ }
181
+ lines.push('');
182
+ }
183
+
184
+ // V1 scores
185
+ if (r.scores && Object.keys(r.scores).length > 0 && !r.criteria_scores) {
186
+ lines.push('### Scores');
187
+ lines.push('');
188
+ lines.push('| Evaluator | Score | Comment |');
189
+ lines.push('|-----------|-------|---------|');
190
+ for (const [key, val] of Object.entries(r.scores as Record<string, any>)) {
191
+ const comment = (val.comment || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200);
192
+ lines.push(`| ${key} | ${val.score} | ${comment} |`);
193
+ }
194
+ lines.push('');
195
+ }
196
+
197
+ if (i < results.length - 1) {
198
+ lines.push('---');
199
+ lines.push('');
200
+ }
201
+ }
202
+
203
+ return lines.join('\n');
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Eval report PDF
208
+ // ---------------------------------------------------------------------------
209
+
210
+ export async function exportEvalReport(
211
+ runId: string,
212
+ outputPath?: string,
213
+ client?: IndemnClient,
214
+ ): Promise<{ file_path: string; file_name: string; file_type: string }> {
215
+ const c = client || new IndemnClient();
216
+ const evalSdk = new EvalSDK(c);
217
+
218
+ const [run, results] = await Promise.all([
219
+ evalSdk.getRunStatus(runId),
220
+ evalSdk.getRunResults(runId),
221
+ ]);
222
+
223
+ const botContext = run.agent_id ? await evalSdk.getBotContext(run.agent_id).catch(() => null) : null;
224
+
225
+ const isV2 = results.length > 0 && (results[0] as any).criteria_scores != null;
226
+ const customerName = (botContext as any)?.organization_name || (botContext as any)?.bot_name || run.agent_id || 'Unknown';
227
+ const reportDate = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
228
+
229
+ let reportData: EvalReportData;
230
+ if (isV2) {
231
+ reportData = { customerName, reportDate, v2Results: results as unknown as EvaluationResult[] };
232
+ } else {
233
+ // V1 — for now throw a clear error since V1 matrix transform requires evaluator config
234
+ throw new Error('V1 report format not yet supported for PDF export. Use "eval export" for markdown output.');
235
+ }
236
+
237
+ const filePath = await generateEvalReportPDF(reportData, outputPath);
238
+ return {
239
+ file_path: filePath,
240
+ file_name: path.basename(filePath),
241
+ file_type: 'pdf',
242
+ };
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Agent card PDF
247
+ // ---------------------------------------------------------------------------
248
+
249
+ export async function exportAgentCard(
250
+ agentId: string,
251
+ outputPath?: string,
252
+ client?: IndemnClient,
253
+ ): Promise<{ file_path: string; file_name: string; file_type: string }> {
254
+ const c = client || new IndemnClient();
255
+ const agentsSdk = new AgentsSDK(c);
256
+ const evalSdk = new EvalSDK(c);
257
+
258
+ const agent = await agentsSdk.getAgent(agentId);
259
+
260
+ // Fetch config and functions from copilot server
261
+ let config: any = null;
262
+ let functions: any[] = [];
263
+ try {
264
+ config = await c.copilotGet(`/organization/:org_id/ai-studio/bots/${agentId}/configurations`);
265
+ } catch { /* config may not be available */ }
266
+ try {
267
+ functions = await c.copilotGet(`/organization/:org_id/ai-studio/bots/${agentId}/functions`) as any[];
268
+ } catch { /* functions may not be available */ }
269
+
270
+ // Fetch bot context from eval service for additional data
271
+ let botContext: any = null;
272
+ try {
273
+ botContext = await evalSdk.getBotContext(agentId);
274
+ } catch { /* eval service may not have this agent */ }
275
+
276
+ // Fetch eval runs
277
+ let evalRuns: any[] = [];
278
+ try {
279
+ evalRuns = await evalSdk.listRuns(agentId);
280
+ } catch { /* evals may not be available */ }
281
+
282
+ const cardData = mapToAgentCardData(agent, config, functions, evalRuns, botContext);
283
+ const filePath = await generateAgentCardPDF(cardData, outputPath);
284
+
285
+ return {
286
+ file_path: filePath,
287
+ file_name: path.basename(filePath),
288
+ file_type: 'pdf',
289
+ };
290
+ }
291
+
292
+ function mapToAgentCardData(
293
+ agent: any,
294
+ config: any,
295
+ functions: any[],
296
+ evalRuns: any[],
297
+ botContext: any,
298
+ ): AgentCardData {
299
+ const generatedDate = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
300
+
301
+ // Map tools from bot context (richer) or functions
302
+ const tools: AgentCardTool[] = (botContext?.tools || functions || []).map((f: any) => ({
303
+ id: f.id || f._id || '',
304
+ name: f.name || f.tool_name || '',
305
+ tool_name: f.tool_name || f.name || '',
306
+ type: f.type || 'unknown',
307
+ description: f.description,
308
+ }));
309
+
310
+ // Map knowledge bases from bot context
311
+ const knowledge_bases: AgentCardKnowledgeBase[] = (botContext?.knowledge_bases || []).map((kb: any) => ({
312
+ id: kb.id || kb._id || '',
313
+ name: kb.name || '',
314
+ type: kb.type || 'unknown',
315
+ document_count: kb.document_count || 0,
316
+ }));
317
+
318
+ // System prompt
319
+ const system_prompt = botContext?.system_prompt || config?.ai_config?.system_prompt;
320
+
321
+ // LLM config
322
+ const llm_config = botContext?.llm_config ? {
323
+ provider: botContext.llm_config.provider,
324
+ model: botContext.llm_config.model,
325
+ parameters: botContext.llm_config.parameters,
326
+ } : undefined;
327
+
328
+ // Map eval runs
329
+ const completedRuns = evalRuns.filter((r: any) => r.status === 'completed');
330
+ const evaluationRuns = completedRuns.map((r: any) => {
331
+ const metrics = getRunScoreMetrics(r);
332
+ return {
333
+ run_id: r.run_id,
334
+ status: r.status,
335
+ created_at: r.created_at || r.started_at || '',
336
+ completed_at: r.completed_at,
337
+ bot_llm_model: r.bot_llm_model,
338
+ bot_llm_provider: r.bot_llm_provider,
339
+ criteriaScore: getRunCriteriaScore(r),
340
+ rubricScore: getRunRubricScore(r),
341
+ criteriaPassed: metrics.criteriaPassed,
342
+ criteriaTotal: metrics.criteriaTotal,
343
+ rubricRulesPassed: metrics.rubricRulesPassed,
344
+ rubricRulesTotal: metrics.rubricRulesTotal,
345
+ total: r.total || 0,
346
+ completed: r.completed || 0,
347
+ };
348
+ });
349
+
350
+ // Evaluation summary across all completed runs
351
+ let evaluationSummary = undefined;
352
+ if (completedRuns.length > 0) {
353
+ let totalCriteriaPassed = 0, totalCriteriaTotal = 0;
354
+ let totalRubricPassed = 0, totalRubricTotal = 0;
355
+ for (const r of completedRuns) {
356
+ const m = getRunScoreMetrics(r);
357
+ totalCriteriaPassed += m.criteriaPassed;
358
+ totalCriteriaTotal += m.criteriaTotal;
359
+ totalRubricPassed += m.rubricRulesPassed;
360
+ totalRubricTotal += m.rubricRulesTotal;
361
+ }
362
+ evaluationSummary = {
363
+ criteriaRate: totalCriteriaTotal > 0 ? totalCriteriaPassed / totalCriteriaTotal : null,
364
+ rubricRate: totalRubricTotal > 0 ? totalRubricPassed / totalRubricTotal : null,
365
+ criteriaPassed: totalCriteriaPassed,
366
+ criteriaTotal: totalCriteriaTotal,
367
+ rubricRulesPassed: totalRubricPassed,
368
+ rubricRulesTotal: totalRubricTotal,
369
+ totalRuns: completedRuns.length,
370
+ };
371
+ }
372
+
373
+ // Component scores from latest run
374
+ const latestRun = completedRuns[0];
375
+ const componentScores = latestRun?.component_scores;
376
+
377
+ return {
378
+ id: agent._id,
379
+ name: agent.name || 'Unnamed Agent',
380
+ description: agent.description,
381
+ channels: (agent.channels || ['WEB']).map((c: any) =>
382
+ typeof c === 'string' ? c : c.name || c.type || String(c)
383
+ ),
384
+ organization_id: agent.id_project || (botContext as any)?.organization_id,
385
+ system_prompt,
386
+ tools,
387
+ knowledge_bases,
388
+ llm_config,
389
+ generatedDate,
390
+ evaluationSummary,
391
+ componentScores,
392
+ evaluationRuns,
393
+ };
394
+ }
@@ -0,0 +1,37 @@
1
+ // PDF rendering driver — generates agent card and eval report PDFs
2
+ import { renderToBuffer } from '@react-pdf/renderer';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { AgentCardDocument } from './agent-card/document.js';
6
+ import { EvalReportDocument } from './eval-report/document.js';
7
+ import type { AgentCardData } from './agent-card/types.js';
8
+ import type { EvalReportData } from './eval-report/document.js';
9
+
10
+ function generateFilename(prefix: string, name: string, date: string, ext: string): string {
11
+ const cleanName = name.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_');
12
+ const cleanDate = date.replace(/[^a-zA-Z0-9]/g, '_');
13
+ return `${prefix}_${cleanName}_${cleanDate}.${ext}`;
14
+ }
15
+
16
+ export async function generateAgentCardPDF(data: AgentCardData, outputPath?: string): Promise<string> {
17
+ const filename = outputPath || generateFilename('Indemn_Agent_Card', data.name, data.generatedDate, 'pdf');
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const buffer = await renderToBuffer(<AgentCardDocument data={data} /> as any);
20
+ const resolvedPath = path.resolve(filename);
21
+ fs.writeFileSync(resolvedPath, buffer);
22
+ return resolvedPath;
23
+ }
24
+
25
+ export async function generateEvalReportPDF(data: EvalReportData, outputPath?: string): Promise<string> {
26
+ const customerName = data.customerName;
27
+ const filename = outputPath || generateFilename('Indemn_Evaluation', customerName, data.reportDate, 'pdf');
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ const buffer = await renderToBuffer(<EvalReportDocument data={data} /> as any);
30
+ const resolvedPath = path.resolve(filename);
31
+ fs.writeFileSync(resolvedPath, buffer);
32
+ return resolvedPath;
33
+ }
34
+
35
+ // Re-export types for convenience
36
+ export type { AgentCardData } from './agent-card/types.js';
37
+ export type { EvalReportData, EvalReportDataV1, EvalReportDataV2 } from './eval-report/document.js';