@graphorin/evals 0.5.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/cli/index.d.ts +43 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +102 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/csv.d.ts +23 -0
- package/dist/loaders/csv.d.ts.map +1 -0
- package/dist/loaders/csv.js +116 -0
- package/dist/loaders/csv.js.map +1 -0
- package/dist/loaders/dmr.d.ts +31 -0
- package/dist/loaders/dmr.d.ts.map +1 -0
- package/dist/loaders/dmr.js +121 -0
- package/dist/loaders/dmr.js.map +1 -0
- package/dist/loaders/from-traces.d.ts +32 -0
- package/dist/loaders/from-traces.d.ts.map +1 -0
- package/dist/loaders/from-traces.js +66 -0
- package/dist/loaders/from-traces.js.map +1 -0
- package/dist/loaders/index.d.ts +9 -0
- package/dist/loaders/index.js +9 -0
- package/dist/loaders/iterable.d.ts +12 -0
- package/dist/loaders/iterable.d.ts.map +1 -0
- package/dist/loaders/iterable.js +16 -0
- package/dist/loaders/iterable.js.map +1 -0
- package/dist/loaders/jsonl.d.ts +34 -0
- package/dist/loaders/jsonl.d.ts.map +1 -0
- package/dist/loaders/jsonl.js +64 -0
- package/dist/loaders/jsonl.js.map +1 -0
- package/dist/loaders/locomo.d.ts +32 -0
- package/dist/loaders/locomo.d.ts.map +1 -0
- package/dist/loaders/locomo.js +155 -0
- package/dist/loaders/locomo.js.map +1 -0
- package/dist/loaders/longmemeval.d.ts +36 -0
- package/dist/loaders/longmemeval.d.ts.map +1 -0
- package/dist/loaders/longmemeval.js +135 -0
- package/dist/loaders/longmemeval.js.map +1 -0
- package/dist/loaders/memory-eval.d.ts +67 -0
- package/dist/loaders/memory-eval.d.ts.map +1 -0
- package/dist/regression.d.ts +13 -0
- package/dist/regression.d.ts.map +1 -0
- package/dist/regression.js +63 -0
- package/dist/regression.js.map +1 -0
- package/dist/reporters/html.d.ts +11 -0
- package/dist/reporters/html.d.ts.map +1 -0
- package/dist/reporters/html.js +60 -0
- package/dist/reporters/html.js.map +1 -0
- package/dist/reporters/index.d.ts +6 -0
- package/dist/reporters/index.js +7 -0
- package/dist/reporters/json.d.ts +11 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +10 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/reporters/junit.d.ts +11 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +29 -0
- package/dist/reporters/junit.js.map +1 -0
- package/dist/reporters/markdown.d.ts +9 -0
- package/dist/reporters/markdown.d.ts.map +1 -0
- package/dist/reporters/markdown.js +59 -0
- package/dist/reporters/markdown.js.map +1 -0
- package/dist/reporters/terminal.d.ts +9 -0
- package/dist/reporters/terminal.d.ts.map +1 -0
- package/dist/reporters/terminal.js +38 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/runner.d.ts +11 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +181 -0
- package/dist/runner.js.map +1 -0
- package/dist/scorers/code/exact-match.d.ts +22 -0
- package/dist/scorers/code/exact-match.d.ts.map +1 -0
- package/dist/scorers/code/exact-match.js +67 -0
- package/dist/scorers/code/exact-match.js.map +1 -0
- package/dist/scorers/code/json-path.d.ts +18 -0
- package/dist/scorers/code/json-path.d.ts.map +1 -0
- package/dist/scorers/code/json-path.js +71 -0
- package/dist/scorers/code/json-path.js.map +1 -0
- package/dist/scorers/code/predicate.d.ts +18 -0
- package/dist/scorers/code/predicate.d.ts.map +1 -0
- package/dist/scorers/code/predicate.js +19 -0
- package/dist/scorers/code/predicate.js.map +1 -0
- package/dist/scorers/code/regex.d.ts +15 -0
- package/dist/scorers/code/regex.d.ts.map +1 -0
- package/dist/scorers/code/regex.js +37 -0
- package/dist/scorers/code/regex.js.map +1 -0
- package/dist/scorers/index.d.ts +13 -0
- package/dist/scorers/index.js +13 -0
- package/dist/scorers/llm/judge.d.ts +57 -0
- package/dist/scorers/llm/judge.d.ts.map +1 -0
- package/dist/scorers/llm/judge.js +108 -0
- package/dist/scorers/llm/judge.js.map +1 -0
- package/dist/scorers/prebuilt/index.d.ts +22 -0
- package/dist/scorers/prebuilt/index.d.ts.map +1 -0
- package/dist/scorers/prebuilt/index.js +49 -0
- package/dist/scorers/prebuilt/index.js.map +1 -0
- package/dist/scorers/trajectory/argument-validity.d.ts +25 -0
- package/dist/scorers/trajectory/argument-validity.d.ts.map +1 -0
- package/dist/scorers/trajectory/argument-validity.js +46 -0
- package/dist/scorers/trajectory/argument-validity.js.map +1 -0
- package/dist/scorers/trajectory/correct-tool-selected.d.ts +19 -0
- package/dist/scorers/trajectory/correct-tool-selected.d.ts.map +1 -0
- package/dist/scorers/trajectory/correct-tool-selected.js +45 -0
- package/dist/scorers/trajectory/correct-tool-selected.js.map +1 -0
- package/dist/scorers/trajectory/final-state-correct.d.ts +21 -0
- package/dist/scorers/trajectory/final-state-correct.d.ts.map +1 -0
- package/dist/scorers/trajectory/final-state-correct.js +42 -0
- package/dist/scorers/trajectory/final-state-correct.js.map +1 -0
- package/dist/scorers/trajectory/recovery-after-error.d.ts +15 -0
- package/dist/scorers/trajectory/recovery-after-error.d.ts.map +1 -0
- package/dist/scorers/trajectory/recovery-after-error.js +37 -0
- package/dist/scorers/trajectory/recovery-after-error.js.map +1 -0
- package/dist/scorers/trajectory/redundant-call-detection.d.ts +19 -0
- package/dist/scorers/trajectory/redundant-call-detection.d.ts.map +1 -0
- package/dist/scorers/trajectory/redundant-call-detection.js +39 -0
- package/dist/scorers/trajectory/redundant-call-detection.js.map +1 -0
- package/dist/scorers/trajectory/types.d.ts +49 -0
- package/dist/scorers/trajectory/types.d.ts.map +1 -0
- package/dist/scorers/trajectory/util.js +71 -0
- package/dist/scorers/trajectory/util.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locomo.js","names":["parsed: unknown","out: Case<MemoryEvalInput, string>[]","indices: number[]","sessions: MemoryEvalSession[]","turns: MemoryEvalTurn[]","role: 'user' | 'assistant'"],"sources":["../../src/loaders/locomo.ts"],"sourcesContent":["/**\n * Real LOCOMO dataset loader (arXiv:2402.17753 —\n * https://snap-research.github.io/locomo/). LOCOMO ships as a JSON\n * **array** of samples; each sample bundles one multi-session\n * conversation with many QA pairs over it:\n *\n * ```jsonc\n * {\n * \"sample_id\": \"conv-26\",\n * \"conversation\": {\n * \"speaker_a\": \"Caroline\",\n * \"speaker_b\": \"Melanie\",\n * \"session_1_date_time\": \"1:56 pm on 8 May, 2023\",\n * \"session_1\": [ { \"speaker\": \"Caroline\", \"dia_id\": \"D1:1\", \"text\": \"...\" }, ... ],\n * \"session_2_date_time\": \"...\",\n * \"session_2\": [ ... ]\n * },\n * \"qa\": [\n * { \"question\": \"...\", \"answer\": \"...\", \"evidence\": [\"D1:1\"], \"category\": 1 },\n * { \"question\": \"...\", \"category\": 5, \"adversarial_answer\": \"Not mentioned ...\" }\n * ]\n * }\n * ```\n *\n * Each QA pair becomes one {@link MemoryEvalInput} case that shares the\n * sample's sessions. LOCOMO categories map to abilities as:\n * `1`→multi-session (multi-hop), `2`→temporal, `5`→abstention\n * (adversarial), everything else (`3` open-domain, `4` single-hop)\n * →info-extraction.\n *\n * @packageDocumentation\n */\n\nimport { readFile } from 'node:fs/promises';\n\nimport type { Case, Dataset } from '@graphorin/observability/eval';\n\nimport type {\n MemoryEvalAbility,\n MemoryEvalInput,\n MemoryEvalSession,\n MemoryEvalTurn,\n} from './memory-eval.js';\n\n/** @stable */\nexport interface LoadLocomoOptions {\n /** Local path to the LOCOMO JSON (under `benchmarks/.datasets/`). */\n readonly path: string;\n /** Optional dataset name surfaced in `Dataset.metadata.name`. */\n readonly name?: string;\n /** Optional description surfaced in `Dataset.metadata.description`. */\n readonly description?: string;\n}\n\n/**\n * Read a LOCOMO JSON file and return a fully-materialised\n * {@link Dataset} — one case per QA pair, scored against the reference\n * answer string (LLM-judge \"J\").\n *\n * @stable\n */\nexport async function loadLocomoDataset(\n options: LoadLocomoOptions,\n): Promise<Dataset<MemoryEvalInput, string>> {\n const text = await readFile(options.path, 'utf8');\n const cases = parseLocomo(text);\n return {\n cases,\n metadata: {\n name: options.name ?? 'locomo',\n ...(options.description !== undefined ? { description: options.description } : {}),\n createdAt: new Date(),\n },\n };\n}\n\n/**\n * Pure parser. Exported so tests can exercise the mapping without\n * touching the filesystem.\n *\n * @stable\n */\nexport function parseLocomo(text: string): ReadonlyArray<Case<MemoryEvalInput, string>> {\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new Error('[graphorin/evals] LOCOMO dataset must be a JSON array of samples.');\n }\n const samples = parsed as ReadonlyArray<unknown>;\n const out: Case<MemoryEvalInput, string>[] = [];\n for (let i = 0; i < samples.length; i++) {\n const sample = samples[i];\n if (sample === null || typeof sample !== 'object') {\n throw new Error(`[graphorin/evals] LOCOMO sample ${i} is not an object.`);\n }\n appendSampleCases(sample as Record<string, unknown>, i, out);\n }\n return out;\n}\n\nfunction appendSampleCases(\n sample: Record<string, unknown>,\n index: number,\n out: Case<MemoryEvalInput, string>[],\n): void {\n const sampleId = asString(sample.sample_id) ?? `locomo-${index}`;\n const conversation = isRecord(sample.conversation) ? sample.conversation : {};\n const sessions = extractSessions(conversation);\n const qa = asArray(sample.qa);\n for (let q = 0; q < qa.length; q++) {\n const item = qa[q];\n if (!isRecord(item)) continue;\n const question = asString(item.question);\n if (question === undefined) continue;\n const category = typeof item.category === 'number' ? item.category : undefined;\n const ability = mapCategory(category);\n const answer = asString(item.answer) ?? asString(item.adversarial_answer) ?? '';\n const evidence = asArray(item.evidence).filter((e): e is string => typeof e === 'string');\n out.push({\n id: `${sampleId}-q${q}`,\n input: { haystackSessions: sessions, question, ability },\n expected: answer,\n metadata: {\n datasetName: 'locomo',\n sampleId,\n ability,\n ...(category !== undefined ? { category } : {}),\n evidence,\n },\n });\n }\n}\n\nfunction extractSessions(conversation: Record<string, unknown>): MemoryEvalSession[] {\n const speakerA = asString(conversation.speaker_a);\n const indices: number[] = [];\n for (const key of Object.keys(conversation)) {\n const m = /^session_(\\d+)$/.exec(key);\n if (m?.[1] !== undefined) indices.push(Number.parseInt(m[1], 10));\n }\n indices.sort((a, b) => a - b);\n const sessions: MemoryEvalSession[] = [];\n for (const n of indices) {\n const raw = asArray(conversation[`session_${n}`]);\n const date = asString(conversation[`session_${n}_date_time`]);\n const turns: MemoryEvalTurn[] = [];\n for (const t of raw) {\n if (!isRecord(t)) continue;\n const content = asString(t.text) ?? asString(t.content);\n if (content === undefined) continue;\n const speaker = asString(t.speaker);\n const role: 'user' | 'assistant' =\n speakerA !== undefined && speaker !== undefined && speaker !== speakerA\n ? 'assistant'\n : 'user';\n turns.push({ role, content, ...(date !== undefined ? { timestamp: date } : {}) });\n }\n sessions.push({ id: `session_${n}`, turns });\n }\n return sessions;\n}\n\n/** Maps a LOCOMO numeric `category` onto a {@link MemoryEvalAbility}. */\nfunction mapCategory(category: number | undefined): MemoryEvalAbility {\n switch (category) {\n case 1:\n return 'multi-session';\n case 2:\n return 'temporal';\n case 5:\n return 'abstention';\n default:\n return 'info-extraction';\n }\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction asArray(value: unknown): ReadonlyArray<unknown> {\n return Array.isArray(value) ? (value as ReadonlyArray<unknown>) : [];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MemoryEvalAbility, MemoryEvalInput } from "./memory-eval.js";
|
|
2
|
+
import { Case, Dataset } from "@graphorin/observability/eval";
|
|
3
|
+
|
|
4
|
+
//#region src/loaders/longmemeval.d.ts
|
|
5
|
+
|
|
6
|
+
/** @stable */
|
|
7
|
+
interface LoadLongMemEvalOptions {
|
|
8
|
+
/** Local path to the dataset JSON (under `benchmarks/.datasets/`). */
|
|
9
|
+
readonly path: string;
|
|
10
|
+
/** Which release this file is. `'S'` (~115 sessions) is the default target. */
|
|
11
|
+
readonly variant?: 'S' | 'M';
|
|
12
|
+
/** When set, keep only cases whose mapped ability is in this list. */
|
|
13
|
+
readonly abilities?: ReadonlyArray<MemoryEvalAbility>;
|
|
14
|
+
/** Optional dataset name surfaced in `Dataset.metadata.name`. */
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
/** Optional description surfaced in `Dataset.metadata.description`. */
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read a LongMemEval JSON file and return a fully-materialised
|
|
21
|
+
* {@link Dataset} of {@link MemoryEvalInput} cases scored against the
|
|
22
|
+
* reference `answer` string.
|
|
23
|
+
*
|
|
24
|
+
* @stable
|
|
25
|
+
*/
|
|
26
|
+
declare function loadLongMemEvalDataset(options: LoadLongMemEvalOptions): Promise<Dataset<MemoryEvalInput, string>>;
|
|
27
|
+
/**
|
|
28
|
+
* Pure parser. Exported so tests can exercise the mapping without
|
|
29
|
+
* touching the filesystem.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
declare function parseLongMemEval(text: string, abilities?: ReadonlyArray<MemoryEvalAbility>): ReadonlyArray<Case<MemoryEvalInput, string>>;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { LoadLongMemEvalOptions, loadLongMemEvalDataset, parseLongMemEval };
|
|
36
|
+
//# sourceMappingURL=longmemeval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"longmemeval.d.ts","names":[],"sources":["../../src/loaders/longmemeval.ts"],"sourcesContent":[],"mappings":";;;;;;UAwCiB,sBAAA;;;;;;uBAMM,cAAc;;;;;;;;;;;;;iBAcf,sBAAA,UACX,yBACR,QAAQ,QAAQ;;;;;;;iBAoBH,gBAAA,2BAEF,cAAc,qBACzB,cAAc,KAAK"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
//#region src/loaders/longmemeval.ts
|
|
4
|
+
/**
|
|
5
|
+
* LongMemEval dataset loader (ICLR 2025, arXiv:2410.10813 —
|
|
6
|
+
* https://github.com/xiaowu0162/LongMemEval). LongMemEval ships as a
|
|
7
|
+
* JSON **array** of question objects:
|
|
8
|
+
*
|
|
9
|
+
* ```jsonc
|
|
10
|
+
* {
|
|
11
|
+
* "question_id": "abc_abs", // `_abs` suffix marks abstention
|
|
12
|
+
* "question_type": "temporal-reasoning",
|
|
13
|
+
* "question": "Where did I live in 2021?",
|
|
14
|
+
* "answer": "Berlin",
|
|
15
|
+
* "question_date": "2023/05/20 (Sat) 14:00",
|
|
16
|
+
* "haystack_session_ids": ["s1", "s2"],
|
|
17
|
+
* "haystack_dates": ["2021/03/01 ...", "2023/01/05 ..."],
|
|
18
|
+
* "haystack_sessions": [
|
|
19
|
+
* [ { "role": "user", "content": "..." }, { "role": "assistant", "content": "...", "has_answer": true } ],
|
|
20
|
+
* [ ... ]
|
|
21
|
+
* ]
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* The three parallel arrays (`haystack_session_ids` / `haystack_dates`
|
|
26
|
+
* / `haystack_sessions`) are zipped by index; one date applies to every
|
|
27
|
+
* turn in its session.
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Read a LongMemEval JSON file and return a fully-materialised
|
|
33
|
+
* {@link Dataset} of {@link MemoryEvalInput} cases scored against the
|
|
34
|
+
* reference `answer` string.
|
|
35
|
+
*
|
|
36
|
+
* @stable
|
|
37
|
+
*/
|
|
38
|
+
async function loadLongMemEvalDataset(options) {
|
|
39
|
+
const cases = parseLongMemEval(await readFile(options.path, "utf8"), options.abilities);
|
|
40
|
+
const variantSuffix = options.variant !== void 0 ? `_${options.variant.toLowerCase()}` : "";
|
|
41
|
+
return {
|
|
42
|
+
cases,
|
|
43
|
+
metadata: {
|
|
44
|
+
name: options.name ?? `longmemeval${variantSuffix}`,
|
|
45
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
46
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Pure parser. Exported so tests can exercise the mapping without
|
|
52
|
+
* touching the filesystem.
|
|
53
|
+
*
|
|
54
|
+
* @stable
|
|
55
|
+
*/
|
|
56
|
+
function parseLongMemEval(text, abilities) {
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
if (!Array.isArray(parsed)) throw new Error("[graphorin/evals] LongMemEval dataset must be a JSON array of questions.");
|
|
59
|
+
const rows = parsed;
|
|
60
|
+
const out = [];
|
|
61
|
+
for (let i = 0; i < rows.length; i++) {
|
|
62
|
+
const row = rows[i];
|
|
63
|
+
if (row === null || typeof row !== "object") throw new Error(`[graphorin/evals] LongMemEval row ${i} is not an object.`);
|
|
64
|
+
const mapped = mapRow(row, i);
|
|
65
|
+
if (abilities !== void 0 && !abilities.includes(mapped.input.ability ?? "info-extraction")) continue;
|
|
66
|
+
out.push(mapped);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function mapRow(row, index) {
|
|
71
|
+
const question = asString(row.question);
|
|
72
|
+
if (question === void 0) throw new Error(`[graphorin/evals] LongMemEval row ${index} is missing a 'question'.`);
|
|
73
|
+
const questionId = asString(row.question_id) ?? `longmemeval-${index}`;
|
|
74
|
+
const questionType = asString(row.question_type) ?? "";
|
|
75
|
+
const askedAt = asString(row.question_date);
|
|
76
|
+
const ability = mapAbility(questionType, questionId);
|
|
77
|
+
const rawSessions = asArray(row.haystack_sessions);
|
|
78
|
+
const sessionIds = asArray(row.haystack_session_ids);
|
|
79
|
+
const sessionDates = asArray(row.haystack_dates);
|
|
80
|
+
const haystackSessions = [];
|
|
81
|
+
for (let s = 0; s < rawSessions.length; s++) {
|
|
82
|
+
const rawTurns = asArray(rawSessions[s]);
|
|
83
|
+
const date = asString(sessionDates[s]);
|
|
84
|
+
const turns = [];
|
|
85
|
+
for (const t of rawTurns) {
|
|
86
|
+
if (t === null || typeof t !== "object") continue;
|
|
87
|
+
const content = asString(t.content);
|
|
88
|
+
if (content === void 0) continue;
|
|
89
|
+
const role = t.role === "assistant" ? "assistant" : "user";
|
|
90
|
+
turns.push({
|
|
91
|
+
role,
|
|
92
|
+
content,
|
|
93
|
+
...date !== void 0 ? { timestamp: date } : {}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
haystackSessions.push({
|
|
97
|
+
id: asString(sessionIds[s]) ?? `session-${s}`,
|
|
98
|
+
turns
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: questionId,
|
|
103
|
+
input: {
|
|
104
|
+
haystackSessions,
|
|
105
|
+
question,
|
|
106
|
+
...askedAt !== void 0 ? { askedAt } : {},
|
|
107
|
+
ability
|
|
108
|
+
},
|
|
109
|
+
expected: asString(row.answer) ?? "",
|
|
110
|
+
metadata: {
|
|
111
|
+
datasetName: "longmemeval",
|
|
112
|
+
ability,
|
|
113
|
+
questionType
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Maps a LongMemEval `question_type` / `_abs` suffix onto a {@link MemoryEvalAbility}. */
|
|
118
|
+
function mapAbility(questionType, questionId) {
|
|
119
|
+
const qt = questionType.toLowerCase();
|
|
120
|
+
if (questionId.endsWith("_abs") || qt.includes("abstention")) return "abstention";
|
|
121
|
+
if (qt.includes("temporal")) return "temporal";
|
|
122
|
+
if (qt.includes("knowledge-update") || qt.includes("knowledge_update")) return "knowledge-update";
|
|
123
|
+
if (qt.includes("multi-session") || qt.includes("multi_session")) return "multi-session";
|
|
124
|
+
return "info-extraction";
|
|
125
|
+
}
|
|
126
|
+
function asString(value) {
|
|
127
|
+
return typeof value === "string" ? value : void 0;
|
|
128
|
+
}
|
|
129
|
+
function asArray(value) {
|
|
130
|
+
return Array.isArray(value) ? value : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { loadLongMemEvalDataset, parseLongMemEval };
|
|
135
|
+
//# sourceMappingURL=longmemeval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"longmemeval.js","names":["parsed: unknown","out: Case<MemoryEvalInput, string>[]","haystackSessions: MemoryEvalSession[]","turns: MemoryEvalTurn[]"],"sources":["../../src/loaders/longmemeval.ts"],"sourcesContent":["/**\n * LongMemEval dataset loader (ICLR 2025, arXiv:2410.10813 —\n * https://github.com/xiaowu0162/LongMemEval). LongMemEval ships as a\n * JSON **array** of question objects:\n *\n * ```jsonc\n * {\n * \"question_id\": \"abc_abs\", // `_abs` suffix marks abstention\n * \"question_type\": \"temporal-reasoning\",\n * \"question\": \"Where did I live in 2021?\",\n * \"answer\": \"Berlin\",\n * \"question_date\": \"2023/05/20 (Sat) 14:00\",\n * \"haystack_session_ids\": [\"s1\", \"s2\"],\n * \"haystack_dates\": [\"2021/03/01 ...\", \"2023/01/05 ...\"],\n * \"haystack_sessions\": [\n * [ { \"role\": \"user\", \"content\": \"...\" }, { \"role\": \"assistant\", \"content\": \"...\", \"has_answer\": true } ],\n * [ ... ]\n * ]\n * }\n * ```\n *\n * The three parallel arrays (`haystack_session_ids` / `haystack_dates`\n * / `haystack_sessions`) are zipped by index; one date applies to every\n * turn in its session.\n *\n * @packageDocumentation\n */\n\nimport { readFile } from 'node:fs/promises';\n\nimport type { Case, Dataset } from '@graphorin/observability/eval';\n\nimport type {\n MemoryEvalAbility,\n MemoryEvalInput,\n MemoryEvalSession,\n MemoryEvalTurn,\n} from './memory-eval.js';\n\n/** @stable */\nexport interface LoadLongMemEvalOptions {\n /** Local path to the dataset JSON (under `benchmarks/.datasets/`). */\n readonly path: string;\n /** Which release this file is. `'S'` (~115 sessions) is the default target. */\n readonly variant?: 'S' | 'M';\n /** When set, keep only cases whose mapped ability is in this list. */\n readonly abilities?: ReadonlyArray<MemoryEvalAbility>;\n /** Optional dataset name surfaced in `Dataset.metadata.name`. */\n readonly name?: string;\n /** Optional description surfaced in `Dataset.metadata.description`. */\n readonly description?: string;\n}\n\n/**\n * Read a LongMemEval JSON file and return a fully-materialised\n * {@link Dataset} of {@link MemoryEvalInput} cases scored against the\n * reference `answer` string.\n *\n * @stable\n */\nexport async function loadLongMemEvalDataset(\n options: LoadLongMemEvalOptions,\n): Promise<Dataset<MemoryEvalInput, string>> {\n const text = await readFile(options.path, 'utf8');\n const cases = parseLongMemEval(text, options.abilities);\n const variantSuffix = options.variant !== undefined ? `_${options.variant.toLowerCase()}` : '';\n return {\n cases,\n metadata: {\n name: options.name ?? `longmemeval${variantSuffix}`,\n ...(options.description !== undefined ? { description: options.description } : {}),\n createdAt: new Date(),\n },\n };\n}\n\n/**\n * Pure parser. Exported so tests can exercise the mapping without\n * touching the filesystem.\n *\n * @stable\n */\nexport function parseLongMemEval(\n text: string,\n abilities?: ReadonlyArray<MemoryEvalAbility>,\n): ReadonlyArray<Case<MemoryEvalInput, string>> {\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new Error('[graphorin/evals] LongMemEval dataset must be a JSON array of questions.');\n }\n const rows = parsed as ReadonlyArray<unknown>;\n const out: Case<MemoryEvalInput, string>[] = [];\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (row === null || typeof row !== 'object') {\n throw new Error(`[graphorin/evals] LongMemEval row ${i} is not an object.`);\n }\n const mapped = mapRow(row as Record<string, unknown>, i);\n if (abilities !== undefined && !abilities.includes(mapped.input.ability ?? 'info-extraction')) {\n continue;\n }\n out.push(mapped);\n }\n return out;\n}\n\nfunction mapRow(row: Record<string, unknown>, index: number): Case<MemoryEvalInput, string> {\n const question = asString(row.question);\n if (question === undefined) {\n throw new Error(`[graphorin/evals] LongMemEval row ${index} is missing a 'question'.`);\n }\n const questionId = asString(row.question_id) ?? `longmemeval-${index}`;\n const questionType = asString(row.question_type) ?? '';\n const askedAt = asString(row.question_date);\n const ability = mapAbility(questionType, questionId);\n\n const rawSessions = asArray(row.haystack_sessions);\n const sessionIds = asArray(row.haystack_session_ids);\n const sessionDates = asArray(row.haystack_dates);\n const haystackSessions: MemoryEvalSession[] = [];\n for (let s = 0; s < rawSessions.length; s++) {\n const rawTurns = asArray(rawSessions[s]);\n const date = asString(sessionDates[s]);\n const turns: MemoryEvalTurn[] = [];\n for (const t of rawTurns) {\n if (t === null || typeof t !== 'object') continue;\n const content = asString((t as Record<string, unknown>).content);\n if (content === undefined) continue;\n const role = (t as Record<string, unknown>).role === 'assistant' ? 'assistant' : 'user';\n turns.push({ role, content, ...(date !== undefined ? { timestamp: date } : {}) });\n }\n haystackSessions.push({ id: asString(sessionIds[s]) ?? `session-${s}`, turns });\n }\n\n return {\n id: questionId,\n input: {\n haystackSessions,\n question,\n ...(askedAt !== undefined ? { askedAt } : {}),\n ability,\n },\n expected: asString(row.answer) ?? '',\n metadata: { datasetName: 'longmemeval', ability, questionType },\n };\n}\n\n/** Maps a LongMemEval `question_type` / `_abs` suffix onto a {@link MemoryEvalAbility}. */\nfunction mapAbility(questionType: string, questionId: string): MemoryEvalAbility {\n const qt = questionType.toLowerCase();\n if (questionId.endsWith('_abs') || qt.includes('abstention')) return 'abstention';\n if (qt.includes('temporal')) return 'temporal';\n if (qt.includes('knowledge-update') || qt.includes('knowledge_update')) return 'knowledge-update';\n if (qt.includes('multi-session') || qt.includes('multi_session')) return 'multi-session';\n return 'info-extraction';\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction asArray(value: unknown): ReadonlyArray<unknown> {\n return Array.isArray(value) ? (value as ReadonlyArray<unknown>) : [];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/loaders/memory-eval.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared "memory system under test" contract used by the long-term
|
|
4
|
+
* memory eval loaders ({@link loadLongMemEvalDataset},
|
|
5
|
+
* {@link loadLocomoDataset}, {@link loadDmrDataset}) and the benchmark
|
|
6
|
+
* runner. A loader turns a dataset's native JSON into
|
|
7
|
+
* `Case<MemoryEvalInput, string>`: the input carries the haystack of
|
|
8
|
+
* prior sessions plus the question; the expected output is the
|
|
9
|
+
* reference answer string, graded by the `llmJudge` "J" scorer.
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally **type-only** and carries no dependency
|
|
12
|
+
* on `@graphorin/memory` — the concrete system-under-test agent lives
|
|
13
|
+
* in the benchmark package so `@graphorin/evals` stays a generic
|
|
14
|
+
* harness.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* One conversational turn inside a haystack session.
|
|
20
|
+
*
|
|
21
|
+
* @stable
|
|
22
|
+
*/
|
|
23
|
+
interface MemoryEvalTurn {
|
|
24
|
+
readonly role: 'user' | 'assistant';
|
|
25
|
+
readonly content: string;
|
|
26
|
+
/** Dataset-native (often ISO-8601) timestamp, when the dataset provides one. */
|
|
27
|
+
readonly timestamp?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One prior session in the haystack the memory system must ingest
|
|
31
|
+
* before the question is asked.
|
|
32
|
+
*
|
|
33
|
+
* @stable
|
|
34
|
+
*/
|
|
35
|
+
interface MemoryEvalSession {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly turns: ReadonlyArray<MemoryEvalTurn>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The five LongMemEval abilities. Every loader maps its dataset-native
|
|
41
|
+
* category onto one of these so per-ability scoring is comparable
|
|
42
|
+
* across datasets; the raw category is preserved in `Case.metadata`.
|
|
43
|
+
*
|
|
44
|
+
* @stable
|
|
45
|
+
*/
|
|
46
|
+
type MemoryEvalAbility = 'info-extraction' | 'multi-session' | 'temporal' | 'knowledge-update' | 'abstention';
|
|
47
|
+
/**
|
|
48
|
+
* Input handed to the memory system under test for one eval case.
|
|
49
|
+
*
|
|
50
|
+
* @stable
|
|
51
|
+
*/
|
|
52
|
+
interface MemoryEvalInput {
|
|
53
|
+
/** Prior sessions to ingest before the question is asked. */
|
|
54
|
+
readonly haystackSessions: ReadonlyArray<MemoryEvalSession>;
|
|
55
|
+
/** The question to answer from memory. */
|
|
56
|
+
readonly question: string;
|
|
57
|
+
/**
|
|
58
|
+
* When the question is asked. Drives temporal / knowledge-update
|
|
59
|
+
* reasoning; dataset-native string (not necessarily ISO-8601).
|
|
60
|
+
*/
|
|
61
|
+
readonly askedAt?: string;
|
|
62
|
+
/** Mapped ability ({@link MemoryEvalAbility}); the raw category lives in metadata. */
|
|
63
|
+
readonly ability?: MemoryEvalAbility;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { MemoryEvalAbility, MemoryEvalInput, MemoryEvalSession, MemoryEvalTurn };
|
|
67
|
+
//# sourceMappingURL=memory-eval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-eval.d.ts","names":[],"sources":["../../src/loaders/memory-eval.ts"],"sourcesContent":[],"mappings":";;AAsBA;AAaA;AAYA;AAYA;;;;;;;;;;;;;;;;;UArCiB,cAAA;;;;;;;;;;;;UAaA,iBAAA;;kBAEC,cAAc;;;;;;;;;KAUpB,iBAAA;;;;;;UAYK,eAAA;;6BAEY,cAAc;;;;;;;;;qBAStB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EvalReport, RegressionOptions, RegressionReport } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/regression.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect regressions between `current` and `baseline` reports.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
declare function detectRegressions<I, O>(current: EvalReport<I, O>, baseline: EvalReport<I, O>, options?: RegressionOptions): RegressionReport<I, O>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { detectRegressions };
|
|
13
|
+
//# sourceMappingURL=regression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regression.d.ts","names":[],"sources":["../src/regression.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAiCG,iBAJa,iBAIb,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,OAAA,EAHQ,UAGR,CAHmB,CAGnB,EAHsB,CAGtB,CAAA,EAAA,QAAA,EAFS,UAET,CAFoB,CAEpB,EAFuB,CAEvB,CAAA,EAAA,OAAA,CAAA,EADQ,iBACR,CAAA,EAAA,gBAAA,CAAiB,CAAjB,EAAoB,CAApB,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/regression.ts
|
|
2
|
+
const DEFAULT_PASS_RATE_DROP_PCT = 5;
|
|
3
|
+
const DEFAULT_AVG_SCORE_DROP = .05;
|
|
4
|
+
const DEFAULT_AVG_DURATION_INCREASE_MS = Number.POSITIVE_INFINITY;
|
|
5
|
+
/**
|
|
6
|
+
* Detect regressions between `current` and `baseline` reports.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
function detectRegressions(current, baseline, options = {}) {
|
|
11
|
+
const maxPassRateDropPct = options.maxPassRateDropPct ?? DEFAULT_PASS_RATE_DROP_PCT;
|
|
12
|
+
const maxAvgScoreDrop = options.maxAvgScoreDrop ?? DEFAULT_AVG_SCORE_DROP;
|
|
13
|
+
const maxAvgDurationIncreaseMs = options.maxAvgDurationIncreaseMs ?? DEFAULT_AVG_DURATION_INCREASE_MS;
|
|
14
|
+
const findings = [];
|
|
15
|
+
const currentPassRate = total(current) === 0 ? 0 : current.summary.passed / total(current);
|
|
16
|
+
const baselinePassRate = total(baseline) === 0 ? 0 : baseline.summary.passed / total(baseline);
|
|
17
|
+
const passRateDropPct = (baselinePassRate - currentPassRate) * 100;
|
|
18
|
+
if (passRateDropPct > maxPassRateDropPct) findings.push({
|
|
19
|
+
kind: "pass-rate-drop",
|
|
20
|
+
message: `pass rate dropped by ${passRateDropPct.toFixed(2)}% (${(currentPassRate * 100).toFixed(2)}% < baseline ${(baselinePassRate * 100).toFixed(2)}% − tolerance ${maxPassRateDropPct.toFixed(2)}%).`,
|
|
21
|
+
delta: -passRateDropPct
|
|
22
|
+
});
|
|
23
|
+
const durationDelta = current.summary.avgDurationMs - baseline.summary.avgDurationMs;
|
|
24
|
+
if (durationDelta > maxAvgDurationIncreaseMs) findings.push({
|
|
25
|
+
kind: "avg-duration-increase",
|
|
26
|
+
message: `avg duration increased by ${durationDelta.toFixed(2)} ms (${current.summary.avgDurationMs.toFixed(2)} ms > baseline ${baseline.summary.avgDurationMs.toFixed(2)} ms − tolerance ${maxAvgDurationIncreaseMs} ms).`,
|
|
27
|
+
delta: durationDelta
|
|
28
|
+
});
|
|
29
|
+
for (const [scorer, baselineRow] of Object.entries(baseline.summary.byScorer)) {
|
|
30
|
+
const currentRow = current.summary.byScorer[scorer];
|
|
31
|
+
if (currentRow === void 0) {
|
|
32
|
+
findings.push({
|
|
33
|
+
kind: "scorer-removed",
|
|
34
|
+
scorer,
|
|
35
|
+
message: `scorer '${scorer}' is in the baseline but missing from the current run.`,
|
|
36
|
+
delta: NaN
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (typeof baselineRow.avgScore === "number" && typeof currentRow.avgScore === "number") {
|
|
41
|
+
const drop = baselineRow.avgScore - currentRow.avgScore;
|
|
42
|
+
if (drop > maxAvgScoreDrop) findings.push({
|
|
43
|
+
kind: "avg-score-drop",
|
|
44
|
+
scorer,
|
|
45
|
+
message: `'${scorer}' avg score dropped by ${drop.toFixed(4)} (${currentRow.avgScore.toFixed(4)} < baseline ${baselineRow.avgScore.toFixed(4)} − tolerance ${maxAvgScoreDrop}).`,
|
|
46
|
+
delta: -drop
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
hasRegressions: findings.length > 0,
|
|
52
|
+
findings,
|
|
53
|
+
current,
|
|
54
|
+
baseline
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function total(r) {
|
|
58
|
+
return r.summary.total;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { detectRegressions };
|
|
63
|
+
//# sourceMappingURL=regression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regression.js","names":["findings: RegressionFinding[]"],"sources":["../src/regression.ts"],"sourcesContent":["/**\n * Compares two `EvalReport`s — typically the freshly-produced report\n * vs a stored baseline — and lists every metric that regressed beyond\n * the configured tolerance.\n *\n * @packageDocumentation\n */\n\nimport type {\n EvalReport,\n RegressionFinding,\n RegressionOptions,\n RegressionReport,\n} from './types.js';\n\nconst DEFAULT_PASS_RATE_DROP_PCT = 5;\nconst DEFAULT_AVG_SCORE_DROP = 0.05;\n// EB-4: the avg-duration gate is OPT-IN (default off). Absolute wall-clock\n// budgets are environment-sensitive — a baseline recorded on a fast\n// workstation vs a loaded CI runner, or real LLM-latency jitter of whole\n// seconds, would flag spurious \"regressions\". Callers that genuinely want a\n// duration budget pass an explicit finite `maxAvgDurationIncreaseMs`.\nconst DEFAULT_AVG_DURATION_INCREASE_MS = Number.POSITIVE_INFINITY;\n\n/**\n * Detect regressions between `current` and `baseline` reports.\n *\n * @stable\n */\nexport function detectRegressions<I, O>(\n current: EvalReport<I, O>,\n baseline: EvalReport<I, O>,\n options: RegressionOptions = {},\n): RegressionReport<I, O> {\n const maxPassRateDropPct = options.maxPassRateDropPct ?? DEFAULT_PASS_RATE_DROP_PCT;\n const maxAvgScoreDrop = options.maxAvgScoreDrop ?? DEFAULT_AVG_SCORE_DROP;\n const maxAvgDurationIncreaseMs =\n options.maxAvgDurationIncreaseMs ?? DEFAULT_AVG_DURATION_INCREASE_MS;\n const findings: RegressionFinding[] = [];\n\n const currentPassRate = total(current) === 0 ? 0 : current.summary.passed / total(current);\n const baselinePassRate = total(baseline) === 0 ? 0 : baseline.summary.passed / total(baseline);\n const passRateDropPct = (baselinePassRate - currentPassRate) * 100;\n if (passRateDropPct > maxPassRateDropPct) {\n findings.push({\n kind: 'pass-rate-drop',\n message:\n `pass rate dropped by ${passRateDropPct.toFixed(2)}% ` +\n `(${(currentPassRate * 100).toFixed(2)}% < baseline ${(baselinePassRate * 100).toFixed(2)}% ` +\n `− tolerance ${maxPassRateDropPct.toFixed(2)}%).`,\n delta: -passRateDropPct,\n });\n }\n\n const durationDelta = current.summary.avgDurationMs - baseline.summary.avgDurationMs;\n if (durationDelta > maxAvgDurationIncreaseMs) {\n findings.push({\n kind: 'avg-duration-increase',\n message:\n `avg duration increased by ${durationDelta.toFixed(2)} ms ` +\n `(${current.summary.avgDurationMs.toFixed(2)} ms > baseline ` +\n `${baseline.summary.avgDurationMs.toFixed(2)} ms − tolerance ${maxAvgDurationIncreaseMs} ms).`,\n delta: durationDelta,\n });\n }\n\n for (const [scorer, baselineRow] of Object.entries(baseline.summary.byScorer)) {\n const currentRow = current.summary.byScorer[scorer];\n if (currentRow === undefined) {\n findings.push({\n kind: 'scorer-removed',\n scorer,\n message: `scorer '${scorer}' is in the baseline but missing from the current run.`,\n delta: Number.NaN,\n });\n continue;\n }\n if (typeof baselineRow.avgScore === 'number' && typeof currentRow.avgScore === 'number') {\n const drop = baselineRow.avgScore - currentRow.avgScore;\n if (drop > maxAvgScoreDrop) {\n findings.push({\n kind: 'avg-score-drop',\n scorer,\n message:\n `'${scorer}' avg score dropped by ${drop.toFixed(4)} ` +\n `(${currentRow.avgScore.toFixed(4)} < baseline ${baselineRow.avgScore.toFixed(4)} ` +\n `− tolerance ${maxAvgScoreDrop}).`,\n delta: -drop,\n });\n }\n }\n }\n\n return {\n hasRegressions: findings.length > 0,\n findings,\n current,\n baseline,\n };\n}\n\nfunction total<I, O>(r: EvalReport<I, O>): number {\n return r.summary.total;\n}\n"],"mappings":";AAeA,MAAM,6BAA6B;AACnC,MAAM,yBAAyB;AAM/B,MAAM,mCAAmC,OAAO;;;;;;AAOhD,SAAgB,kBACd,SACA,UACA,UAA6B,EAAE,EACP;CACxB,MAAM,qBAAqB,QAAQ,sBAAsB;CACzD,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,2BACJ,QAAQ,4BAA4B;CACtC,MAAMA,WAAgC,EAAE;CAExC,MAAM,kBAAkB,MAAM,QAAQ,KAAK,IAAI,IAAI,QAAQ,QAAQ,SAAS,MAAM,QAAQ;CAC1F,MAAM,mBAAmB,MAAM,SAAS,KAAK,IAAI,IAAI,SAAS,QAAQ,SAAS,MAAM,SAAS;CAC9F,MAAM,mBAAmB,mBAAmB,mBAAmB;AAC/D,KAAI,kBAAkB,mBACpB,UAAS,KAAK;EACZ,MAAM;EACN,SACE,wBAAwB,gBAAgB,QAAQ,EAAE,CAAC,MAC9C,kBAAkB,KAAK,QAAQ,EAAE,CAAC,gBAAgB,mBAAmB,KAAK,QAAQ,EAAE,CAAC,gBAC3E,mBAAmB,QAAQ,EAAE,CAAC;EAC/C,OAAO,CAAC;EACT,CAAC;CAGJ,MAAM,gBAAgB,QAAQ,QAAQ,gBAAgB,SAAS,QAAQ;AACvE,KAAI,gBAAgB,yBAClB,UAAS,KAAK;EACZ,MAAM;EACN,SACE,6BAA6B,cAAc,QAAQ,EAAE,CAAC,OAClD,QAAQ,QAAQ,cAAc,QAAQ,EAAE,CAAC,iBAC1C,SAAS,QAAQ,cAAc,QAAQ,EAAE,CAAC,kBAAkB,yBAAyB;EAC1F,OAAO;EACR,CAAC;AAGJ,MAAK,MAAM,CAAC,QAAQ,gBAAgB,OAAO,QAAQ,SAAS,QAAQ,SAAS,EAAE;EAC7E,MAAM,aAAa,QAAQ,QAAQ,SAAS;AAC5C,MAAI,eAAe,QAAW;AAC5B,YAAS,KAAK;IACZ,MAAM;IACN;IACA,SAAS,WAAW,OAAO;IAC3B,OAAO;IACR,CAAC;AACF;;AAEF,MAAI,OAAO,YAAY,aAAa,YAAY,OAAO,WAAW,aAAa,UAAU;GACvF,MAAM,OAAO,YAAY,WAAW,WAAW;AAC/C,OAAI,OAAO,gBACT,UAAS,KAAK;IACZ,MAAM;IACN;IACA,SACE,IAAI,OAAO,yBAAyB,KAAK,QAAQ,EAAE,CAAC,IAChD,WAAW,SAAS,QAAQ,EAAE,CAAC,cAAc,YAAY,SAAS,QAAQ,EAAE,CAAC,eAClE,gBAAgB;IACjC,OAAO,CAAC;IACT,CAAC;;;AAKR,QAAO;EACL,gBAAgB,SAAS,SAAS;EAClC;EACA;EACA;EACD;;AAGH,SAAS,MAAY,GAA6B;AAChD,QAAO,EAAE,QAAQ"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EvalReport } from "@graphorin/observability/eval";
|
|
2
|
+
|
|
3
|
+
//#region src/reporters/html.d.ts
|
|
4
|
+
|
|
5
|
+
/** @stable */
|
|
6
|
+
declare function renderHtmlReport<I, O>(report: EvalReport<I, O>, options?: {
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { renderHtmlReport };
|
|
11
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","names":[],"sources":["../../src/reporters/html.ts"],"sourcesContent":[],"mappings":";;;;;iBAYgB,+BACN,WAAW,GAAG"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/reporters/html.ts
|
|
2
|
+
/** @stable */
|
|
3
|
+
function renderHtmlReport(report, options = {}) {
|
|
4
|
+
const title = escapeHtml(options.title ?? "graphorin/evals report");
|
|
5
|
+
const summary = report.summary;
|
|
6
|
+
const passRate = summary.total === 0 ? 0 : summary.passed / summary.total * 100;
|
|
7
|
+
const scorerRows = Object.entries(summary.byScorer).map(([scorer, row]) => {
|
|
8
|
+
const avg = row.avgScore === null ? "n/a" : row.avgScore.toFixed(4);
|
|
9
|
+
return `<tr><td><code>${escapeHtml(scorer)}</code></td><td>${row.passed}</td><td>${row.failed}</td><td>${avg}</td></tr>`;
|
|
10
|
+
}).join("");
|
|
11
|
+
const failureRows = report.results.flatMap((r) => {
|
|
12
|
+
return r.scores.filter((s) => !s.result.pass).map((f) => `<tr><td><code>${escapeHtml(r.caseId)}</code></td><td><code>${escapeHtml(f.scorer)}</code></td><td>${escapeHtml(f.result.reason ?? "")}</td></tr>`);
|
|
13
|
+
}).join("");
|
|
14
|
+
return `<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<title>${title}</title>
|
|
19
|
+
<style>
|
|
20
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 2rem; max-width: 64rem; color: #1a1a1a; }
|
|
21
|
+
h1 { margin-top: 0; }
|
|
22
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
23
|
+
th, td { padding: 0.4rem 0.6rem; text-align: left; border-bottom: 1px solid #e5e5e5; }
|
|
24
|
+
th { background: #f5f5f5; }
|
|
25
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: 0.6rem; }
|
|
26
|
+
.summary-card { background: #f5f5f5; padding: 0.8rem; border-radius: 6px; }
|
|
27
|
+
.summary-card .label { font-size: 0.8rem; text-transform: uppercase; color: #666; letter-spacing: 0.04em; }
|
|
28
|
+
.summary-card .value { font-size: 1.4rem; font-weight: 600; }
|
|
29
|
+
code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<h1>${title}</h1>
|
|
34
|
+
<div class="summary-grid">
|
|
35
|
+
<div class="summary-card"><div class="label">Total</div><div class="value">${summary.total}</div></div>
|
|
36
|
+
<div class="summary-card"><div class="label">Passed</div><div class="value">${summary.passed}</div></div>
|
|
37
|
+
<div class="summary-card"><div class="label">Failed</div><div class="value">${summary.failed}</div></div>
|
|
38
|
+
<div class="summary-card"><div class="label">Pass rate</div><div class="value">${passRate.toFixed(1)}%</div></div>
|
|
39
|
+
<div class="summary-card"><div class="label">Avg ms</div><div class="value">${summary.avgDurationMs.toFixed(0)}</div></div>
|
|
40
|
+
</div>
|
|
41
|
+
<h2>Per-scorer</h2>
|
|
42
|
+
<table>
|
|
43
|
+
<thead><tr><th>Scorer</th><th>Pass</th><th>Fail</th><th>Avg score</th></tr></thead>
|
|
44
|
+
<tbody>${scorerRows || "<tr><td colspan=\"4\">No scorers.</td></tr>"}</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
<h2>Failures</h2>
|
|
47
|
+
<table>
|
|
48
|
+
<thead><tr><th>Case</th><th>Scorer</th><th>Reason</th></tr></thead>
|
|
49
|
+
<tbody>${failureRows || "<tr><td colspan=\"3\">No failures.</td></tr>"}</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
}
|
|
54
|
+
function escapeHtml(value) {
|
|
55
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { renderHtmlReport };
|
|
60
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","names":[],"sources":["../../src/reporters/html.ts"],"sourcesContent":["/**\n * HTML reporter. Renders an {@link EvalReport} as a self-contained\n * HTML document — no external CSS / JS — so artifact viewers can\n * render it directly. Designed to fit on a single page; for richer\n * dashboards consume the JSON reporter instead.\n *\n * @packageDocumentation\n */\n\nimport type { EvalReport } from '@graphorin/observability/eval';\n\n/** @stable */\nexport function renderHtmlReport<I, O>(\n report: EvalReport<I, O>,\n options: { readonly title?: string } = {},\n): string {\n const title = escapeHtml(options.title ?? 'graphorin/evals report');\n const summary = report.summary;\n const passRate = summary.total === 0 ? 0 : (summary.passed / summary.total) * 100;\n const scorerRows = Object.entries(summary.byScorer)\n .map(([scorer, row]) => {\n const avg = row.avgScore === null ? 'n/a' : row.avgScore.toFixed(4);\n return `<tr><td><code>${escapeHtml(scorer)}</code></td><td>${row.passed}</td><td>${row.failed}</td><td>${avg}</td></tr>`;\n })\n .join('');\n const failureRows = report.results\n .flatMap((r) => {\n const failures = r.scores.filter((s) => !s.result.pass);\n return failures.map(\n (f) =>\n `<tr><td><code>${escapeHtml(r.caseId)}</code></td><td><code>${escapeHtml(f.scorer)}</code></td><td>${escapeHtml(f.result.reason ?? '')}</td></tr>`,\n );\n })\n .join('');\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>${title}</title>\n<style>\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 2rem; max-width: 64rem; color: #1a1a1a; }\n h1 { margin-top: 0; }\n table { border-collapse: collapse; width: 100%; margin: 1rem 0; }\n th, td { padding: 0.4rem 0.6rem; text-align: left; border-bottom: 1px solid #e5e5e5; }\n th { background: #f5f5f5; }\n .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: 0.6rem; }\n .summary-card { background: #f5f5f5; padding: 0.8rem; border-radius: 6px; }\n .summary-card .label { font-size: 0.8rem; text-transform: uppercase; color: #666; letter-spacing: 0.04em; }\n .summary-card .value { font-size: 1.4rem; font-weight: 600; }\n code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; }\n</style>\n</head>\n<body>\n<h1>${title}</h1>\n<div class=\"summary-grid\">\n <div class=\"summary-card\"><div class=\"label\">Total</div><div class=\"value\">${summary.total}</div></div>\n <div class=\"summary-card\"><div class=\"label\">Passed</div><div class=\"value\">${summary.passed}</div></div>\n <div class=\"summary-card\"><div class=\"label\">Failed</div><div class=\"value\">${summary.failed}</div></div>\n <div class=\"summary-card\"><div class=\"label\">Pass rate</div><div class=\"value\">${passRate.toFixed(1)}%</div></div>\n <div class=\"summary-card\"><div class=\"label\">Avg ms</div><div class=\"value\">${summary.avgDurationMs.toFixed(0)}</div></div>\n</div>\n<h2>Per-scorer</h2>\n<table>\n <thead><tr><th>Scorer</th><th>Pass</th><th>Fail</th><th>Avg score</th></tr></thead>\n <tbody>${scorerRows || '<tr><td colspan=\"4\">No scorers.</td></tr>'}</tbody>\n</table>\n<h2>Failures</h2>\n<table>\n <thead><tr><th>Case</th><th>Scorer</th><th>Reason</th></tr></thead>\n <tbody>${failureRows || '<tr><td colspan=\"3\">No failures.</td></tr>'}</tbody>\n</table>\n</body>\n</html>`;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"],"mappings":";;AAYA,SAAgB,iBACd,QACA,UAAuC,EAAE,EACjC;CACR,MAAM,QAAQ,WAAW,QAAQ,SAAS,yBAAyB;CACnE,MAAM,UAAU,OAAO;CACvB,MAAM,WAAW,QAAQ,UAAU,IAAI,IAAK,QAAQ,SAAS,QAAQ,QAAS;CAC9E,MAAM,aAAa,OAAO,QAAQ,QAAQ,SAAS,CAChD,KAAK,CAAC,QAAQ,SAAS;EACtB,MAAM,MAAM,IAAI,aAAa,OAAO,QAAQ,IAAI,SAAS,QAAQ,EAAE;AACnE,SAAO,iBAAiB,WAAW,OAAO,CAAC,kBAAkB,IAAI,OAAO,WAAW,IAAI,OAAO,WAAW,IAAI;GAC7G,CACD,KAAK,GAAG;CACX,MAAM,cAAc,OAAO,QACxB,SAAS,MAAM;AAEd,SADiB,EAAE,OAAO,QAAQ,MAAM,CAAC,EAAE,OAAO,KAAK,CACvC,KACb,MACC,iBAAiB,WAAW,EAAE,OAAO,CAAC,wBAAwB,WAAW,EAAE,OAAO,CAAC,kBAAkB,WAAW,EAAE,OAAO,UAAU,GAAG,CAAC,YAC1I;GACD,CACD,KAAK,GAAG;AACX,QAAO;;;;SAIA,MAAM;;;;;;;;;;;;;;;MAeT,MAAM;;+EAEmE,QAAQ,MAAM;gFACb,QAAQ,OAAO;gFACf,QAAQ,OAAO;mFACZ,SAAS,QAAQ,EAAE,CAAC;gFACvB,QAAQ,cAAc,QAAQ,EAAE,CAAC;;;;;WAKtG,cAAc,8CAA4C;;;;;WAK1D,eAAe,+CAA6C;;;;;AAMvE,SAAS,WAAW,OAAuB;AACzC,QAAO,MACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,QAAQ"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { renderHtmlReport } from "./html.js";
|
|
2
|
+
import { renderJsonReport } from "./json.js";
|
|
3
|
+
import { renderJunitReport } from "./junit.js";
|
|
4
|
+
import { renderMarkdownReport } from "./markdown.js";
|
|
5
|
+
import { renderTerminalReport } from "./terminal.js";
|
|
6
|
+
export { renderHtmlReport, renderJsonReport, renderJunitReport, renderMarkdownReport, renderTerminalReport };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { renderHtmlReport } from "./html.js";
|
|
2
|
+
import { renderJsonReport } from "./json.js";
|
|
3
|
+
import { renderJunitReport } from "./junit.js";
|
|
4
|
+
import { renderMarkdownReport } from "./markdown.js";
|
|
5
|
+
import { renderTerminalReport } from "./terminal.js";
|
|
6
|
+
|
|
7
|
+
export { renderHtmlReport, renderJsonReport, renderJunitReport, renderMarkdownReport, renderTerminalReport };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EvalReport } from "@graphorin/observability/eval";
|
|
2
|
+
|
|
3
|
+
//#region src/reporters/json.d.ts
|
|
4
|
+
|
|
5
|
+
/** @stable */
|
|
6
|
+
declare function renderJsonReport<I, O>(report: EvalReport<I, O>, options?: {
|
|
7
|
+
readonly pretty?: boolean;
|
|
8
|
+
}): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { renderJsonReport };
|
|
11
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","names":[],"sources":["../../src/reporters/json.ts"],"sourcesContent":[],"mappings":";;;;;iBAWgB,+BACN,WAAW,GAAG"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/reporters/json.ts
|
|
2
|
+
/** @stable */
|
|
3
|
+
function renderJsonReport(report, options = {}) {
|
|
4
|
+
const indent = options.pretty === true ? 2 : 0;
|
|
5
|
+
return JSON.stringify(report, null, indent);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
//#endregion
|
|
9
|
+
export { renderJsonReport };
|
|
10
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","names":[],"sources":["../../src/reporters/json.ts"],"sourcesContent":["/**\n * JSON reporter. Returns the canonical JSON-serialised representation\n * of an {@link EvalReport} so downstream tooling (dashboards,\n * spreadsheets, regression checkers) can consume it deterministically.\n *\n * @packageDocumentation\n */\n\nimport type { EvalReport } from '@graphorin/observability/eval';\n\n/** @stable */\nexport function renderJsonReport<I, O>(\n report: EvalReport<I, O>,\n options: { readonly pretty?: boolean } = {},\n): string {\n const indent = options.pretty === true ? 2 : 0;\n return JSON.stringify(report, null, indent);\n}\n"],"mappings":";;AAWA,SAAgB,iBACd,QACA,UAAyC,EAAE,EACnC;CACR,MAAM,SAAS,QAAQ,WAAW,OAAO,IAAI;AAC7C,QAAO,KAAK,UAAU,QAAQ,MAAM,OAAO"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EvalReport } from "@graphorin/observability/eval";
|
|
2
|
+
|
|
3
|
+
//#region src/reporters/junit.d.ts
|
|
4
|
+
|
|
5
|
+
/** @stable */
|
|
6
|
+
declare function renderJunitReport<I, O>(report: EvalReport<I, O>, options?: {
|
|
7
|
+
readonly suiteName?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { renderJunitReport };
|
|
11
|
+
//# sourceMappingURL=junit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"junit.d.ts","names":[],"sources":["../../src/reporters/junit.ts"],"sourcesContent":[],"mappings":";;;;;iBAYgB,gCACN,WAAW,GAAG"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/reporters/junit.ts
|
|
2
|
+
/** @stable */
|
|
3
|
+
function renderJunitReport(report, options = {}) {
|
|
4
|
+
const suiteName = options.suiteName ?? "graphorin-evals";
|
|
5
|
+
const totalTests = report.results.reduce((acc, r) => acc + r.scores.length, 0);
|
|
6
|
+
const failures = report.results.reduce((acc, r) => acc + r.scores.filter((s) => !s.result.pass).length, 0);
|
|
7
|
+
const totalTime = report.summary.avgDurationMs * report.summary.total / 1e3;
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
|
10
|
+
lines.push(`<testsuite name="${escapeXml(suiteName)}" tests="${totalTests}" failures="${failures}" time="${totalTime.toFixed(3)}">`);
|
|
11
|
+
for (const r of report.results) for (const { scorer, result } of r.scores) {
|
|
12
|
+
const time = (r.durationMs / 1e3).toFixed(3);
|
|
13
|
+
lines.push(` <testcase classname="${escapeXml(scorer)}" name="${escapeXml(r.caseId)}" time="${time}">`);
|
|
14
|
+
if (!result.pass) {
|
|
15
|
+
const message = escapeXml(result.reason ?? "failed");
|
|
16
|
+
lines.push(` <failure message="${message}">${message}</failure>`);
|
|
17
|
+
}
|
|
18
|
+
lines.push(" </testcase>");
|
|
19
|
+
}
|
|
20
|
+
lines.push("</testsuite>");
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
function escapeXml(value) {
|
|
24
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { renderJunitReport };
|
|
29
|
+
//# sourceMappingURL=junit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"junit.js","names":["lines: string[]"],"sources":["../../src/reporters/junit.ts"],"sourcesContent":["/**\n * JUnit XML reporter. Renders an {@link EvalReport} as a JUnit\n * `<testsuite>` document so CI systems (GitHub Actions, GitLab,\n * CircleCI, Jenkins) can render pass / fail counts in the standard\n * test-report widget.\n *\n * @packageDocumentation\n */\n\nimport type { EvalReport } from '@graphorin/observability/eval';\n\n/** @stable */\nexport function renderJunitReport<I, O>(\n report: EvalReport<I, O>,\n options: { readonly suiteName?: string } = {},\n): string {\n const suiteName = options.suiteName ?? 'graphorin-evals';\n const totalTests = report.results.reduce((acc, r) => acc + r.scores.length, 0);\n const failures = report.results.reduce(\n (acc, r) => acc + r.scores.filter((s) => !s.result.pass).length,\n 0,\n );\n const totalTime = (report.summary.avgDurationMs * report.summary.total) / 1000;\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n `<testsuite name=\"${escapeXml(suiteName)}\" tests=\"${totalTests}\" failures=\"${failures}\" time=\"${totalTime.toFixed(3)}\">`,\n );\n for (const r of report.results) {\n for (const { scorer, result } of r.scores) {\n const time = (r.durationMs / 1000).toFixed(3);\n lines.push(\n ` <testcase classname=\"${escapeXml(scorer)}\" name=\"${escapeXml(r.caseId)}\" time=\"${time}\">`,\n );\n if (!result.pass) {\n const message = escapeXml(result.reason ?? 'failed');\n lines.push(` <failure message=\"${message}\">${message}</failure>`);\n }\n lines.push(' </testcase>');\n }\n }\n lines.push('</testsuite>');\n return lines.join('\\n');\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"],"mappings":";;AAYA,SAAgB,kBACd,QACA,UAA2C,EAAE,EACrC;CACR,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,aAAa,OAAO,QAAQ,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,QAAQ,EAAE;CAC9E,MAAM,WAAW,OAAO,QAAQ,QAC7B,KAAK,MAAM,MAAM,EAAE,OAAO,QAAQ,MAAM,CAAC,EAAE,OAAO,KAAK,CAAC,QACzD,EACD;CACD,MAAM,YAAa,OAAO,QAAQ,gBAAgB,OAAO,QAAQ,QAAS;CAC1E,MAAMA,QAAkB,EAAE;AAC1B,OAAM,KAAK,6CAAyC;AACpD,OAAM,KACJ,oBAAoB,UAAU,UAAU,CAAC,WAAW,WAAW,cAAc,SAAS,UAAU,UAAU,QAAQ,EAAE,CAAC,IACtH;AACD,MAAK,MAAM,KAAK,OAAO,QACrB,MAAK,MAAM,EAAE,QAAQ,YAAY,EAAE,QAAQ;EACzC,MAAM,QAAQ,EAAE,aAAa,KAAM,QAAQ,EAAE;AAC7C,QAAM,KACJ,0BAA0B,UAAU,OAAO,CAAC,UAAU,UAAU,EAAE,OAAO,CAAC,UAAU,KAAK,IAC1F;AACD,MAAI,CAAC,OAAO,MAAM;GAChB,MAAM,UAAU,UAAU,OAAO,UAAU,SAAS;AACpD,SAAM,KAAK,yBAAyB,QAAQ,IAAI,QAAQ,YAAY;;AAEtE,QAAM,KAAK,gBAAgB;;AAG/B,OAAM,KAAK,eAAe;AAC1B,QAAO,MAAM,KAAK,KAAK;;AAGzB,SAAS,UAAU,OAAuB;AACxC,QAAO,MACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,SAAS"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EvalReport } from "@graphorin/observability/eval";
|
|
2
|
+
|
|
3
|
+
//#region src/reporters/markdown.d.ts
|
|
4
|
+
|
|
5
|
+
/** @stable */
|
|
6
|
+
declare function renderMarkdownReport<I, O>(report: EvalReport<I, O>): string;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { renderMarkdownReport };
|
|
9
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","names":[],"sources":["../../src/reporters/markdown.ts"],"sourcesContent":[],"mappings":";;;;;iBAWgB,mCAAmC,WAAW,GAAG"}
|