@graphorin/evals 0.6.0 → 0.7.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 +30 -0
- package/README.md +29 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/loaders/locomo.d.ts.map +1 -1
- package/dist/loaders/locomo.js +24 -2
- package/dist/loaders/locomo.js.map +1 -1
- package/dist/loaders/memory-eval.d.ts +8 -0
- package/dist/loaders/memory-eval.d.ts.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +10 -9
- package/src/cli/index.ts +124 -0
- package/src/index.ts +136 -0
- package/src/loaders/csv.ts +147 -0
- package/src/loaders/dmr.ts +137 -0
- package/src/loaders/from-traces.ts +92 -0
- package/src/loaders/index.ts +46 -0
- package/src/loaders/iterable.ts +22 -0
- package/src/loaders/jsonl.ts +94 -0
- package/src/loaders/locomo.ts +217 -0
- package/src/loaders/longmemeval.ts +164 -0
- package/src/loaders/memory-eval.ts +80 -0
- package/src/regression.ts +133 -0
- package/src/reporters/html.ts +83 -0
- package/src/reporters/index.ts +14 -0
- package/src/reporters/json.ts +18 -0
- package/src/reporters/junit.ts +53 -0
- package/src/reporters/markdown.ts +65 -0
- package/src/reporters/terminal.ts +46 -0
- package/src/runner.ts +233 -0
- package/src/scorers/code/exact-match.ts +95 -0
- package/src/scorers/code/index.ts +10 -0
- package/src/scorers/code/json-path.ts +101 -0
- package/src/scorers/code/predicate.ts +32 -0
- package/src/scorers/code/regex.ts +55 -0
- package/src/scorers/index.ts +15 -0
- package/src/scorers/llm/index.ts +13 -0
- package/src/scorers/llm/judge.ts +170 -0
- package/src/scorers/prebuilt/index.ts +95 -0
- package/src/scorers/trajectory/argument-validity.ts +63 -0
- package/src/scorers/trajectory/correct-tool-selected.ts +61 -0
- package/src/scorers/trajectory/final-state-correct.ts +62 -0
- package/src/scorers/trajectory/index.ts +25 -0
- package/src/scorers/trajectory/recovery-after-error.ts +55 -0
- package/src/scorers/trajectory/redundant-call-detection.ts +58 -0
- package/src/scorers/trajectory/types.ts +44 -0
- package/src/scorers/trajectory/util.ts +81 -0
- package/src/stats.ts +196 -0
- package/src/types.ts +135 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV dataset loader. Implements RFC 4180 strict subset:
|
|
3
|
+
*
|
|
4
|
+
* - Comma separator (configurable).
|
|
5
|
+
* - Optional `"`-quoted cells; doubled `""` to embed a literal `"`.
|
|
6
|
+
* - Header row required by default.
|
|
7
|
+
*
|
|
8
|
+
* Columns map to `Case` fields by name: `input`, `expected`, `id`,
|
|
9
|
+
* `metadata` (parsed as JSON when present). Unknown columns are
|
|
10
|
+
* ignored unless a `mapper` is supplied.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
|
|
17
|
+
import type { Case, Dataset } from '@graphorin/observability/eval';
|
|
18
|
+
|
|
19
|
+
/** @stable */
|
|
20
|
+
export interface LoadCsvOptions {
|
|
21
|
+
readonly delimiter?: string;
|
|
22
|
+
readonly name?: string;
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
readonly mapper?: (row: Record<string, string>, index: number) => Case<unknown, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @stable */
|
|
28
|
+
export async function loadCsvDataset(
|
|
29
|
+
path: string,
|
|
30
|
+
options: LoadCsvOptions = {},
|
|
31
|
+
): Promise<Dataset<unknown, unknown>> {
|
|
32
|
+
const text = await readFile(path, 'utf8');
|
|
33
|
+
const cases = parseCsv(text, options);
|
|
34
|
+
const meta: Dataset<unknown, unknown>['metadata'] = {
|
|
35
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
36
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
37
|
+
createdAt: new Date(),
|
|
38
|
+
};
|
|
39
|
+
return { cases, metadata: meta };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure parser. Exported separately so tests can exercise the
|
|
44
|
+
* column-mapping behaviour without touching the filesystem.
|
|
45
|
+
*
|
|
46
|
+
* @stable
|
|
47
|
+
*/
|
|
48
|
+
export function parseCsv(
|
|
49
|
+
text: string,
|
|
50
|
+
options: LoadCsvOptions = {},
|
|
51
|
+
): ReadonlyArray<Case<unknown, unknown>> {
|
|
52
|
+
const delimiter = options.delimiter ?? ',';
|
|
53
|
+
const rows = parseCsvRows(text, delimiter);
|
|
54
|
+
if (rows.length === 0) return [];
|
|
55
|
+
const header = rows[0];
|
|
56
|
+
if (header === undefined) return [];
|
|
57
|
+
const dataRows = rows.slice(1);
|
|
58
|
+
const out: Case<unknown, unknown>[] = [];
|
|
59
|
+
for (let i = 0; i < dataRows.length; i++) {
|
|
60
|
+
const row = dataRows[i] ?? [];
|
|
61
|
+
if (row.every((cell) => cell.length === 0)) continue;
|
|
62
|
+
const record: Record<string, string> = {};
|
|
63
|
+
for (let c = 0; c < header.length; c++) {
|
|
64
|
+
const key = header[c] ?? '';
|
|
65
|
+
const value = row[c] ?? '';
|
|
66
|
+
record[key] = value;
|
|
67
|
+
}
|
|
68
|
+
if (options.mapper !== undefined) {
|
|
69
|
+
out.push(options.mapper(record, i));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (record.input === undefined) {
|
|
73
|
+
throw new Error(`[graphorin/evals] CSV row ${i + 1} missing required 'input' column.`);
|
|
74
|
+
}
|
|
75
|
+
const sampleCase: Case<unknown, unknown> = {
|
|
76
|
+
...(record.id !== undefined && record.id.length > 0 ? { id: record.id } : {}),
|
|
77
|
+
input: record.input,
|
|
78
|
+
...('expected' in record && record.expected !== undefined && record.expected.length > 0
|
|
79
|
+
? { expected: record.expected }
|
|
80
|
+
: {}),
|
|
81
|
+
...(record.metadata !== undefined && record.metadata.length > 0
|
|
82
|
+
? { metadata: parseMetadata(record.metadata) }
|
|
83
|
+
: {}),
|
|
84
|
+
};
|
|
85
|
+
out.push(sampleCase);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseMetadata(raw: string): Readonly<Record<string, unknown>> {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
94
|
+
return parsed as Readonly<Record<string, unknown>>;
|
|
95
|
+
}
|
|
96
|
+
return { value: parsed } as Readonly<Record<string, unknown>>;
|
|
97
|
+
} catch {
|
|
98
|
+
return { raw } as Readonly<Record<string, unknown>>;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseCsvRows(text: string, delimiter: string): string[][] {
|
|
103
|
+
const rows: string[][] = [];
|
|
104
|
+
let current: string[] = [];
|
|
105
|
+
let cell = '';
|
|
106
|
+
let inQuotes = false;
|
|
107
|
+
for (let i = 0; i < text.length; i++) {
|
|
108
|
+
const ch = text[i];
|
|
109
|
+
if (inQuotes) {
|
|
110
|
+
if (ch === '"') {
|
|
111
|
+
const next = text[i + 1];
|
|
112
|
+
if (next === '"') {
|
|
113
|
+
cell += '"';
|
|
114
|
+
i += 1;
|
|
115
|
+
} else {
|
|
116
|
+
inQuotes = false;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
cell += ch;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '"') {
|
|
124
|
+
inQuotes = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ch === delimiter) {
|
|
128
|
+
current.push(cell);
|
|
129
|
+
cell = '';
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === '\n' || ch === '\r') {
|
|
133
|
+
if (ch === '\r' && text[i + 1] === '\n') i += 1;
|
|
134
|
+
current.push(cell);
|
|
135
|
+
rows.push(current);
|
|
136
|
+
current = [];
|
|
137
|
+
cell = '';
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
cell += ch;
|
|
141
|
+
}
|
|
142
|
+
if (cell.length > 0 || current.length > 0) {
|
|
143
|
+
current.push(cell);
|
|
144
|
+
rows.push(current);
|
|
145
|
+
}
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DMR (Deep Memory Retrieval) loader - the MemGPT/Letta continuity set
|
|
3
|
+
* derived from Multi-Session Chat (arXiv:2310.08560). DMR's published
|
|
4
|
+
* shape is less standardised than LongMemEval/LOCOMO, so this loader is
|
|
5
|
+
* deliberately tolerant: it accepts a JSON **array** of items, each
|
|
6
|
+
* exposing a `question`, an `answer`, and the prior conversation under
|
|
7
|
+
* either `sessions` or `haystack_sessions`. Sessions may be a bare
|
|
8
|
+
* array of turns or an object wrapping the turns under
|
|
9
|
+
* `turns` / `messages` / `dialog`; a turn may use `role` or `speaker`
|
|
10
|
+
* and `content` or `text`.
|
|
11
|
+
*
|
|
12
|
+
* ```jsonc
|
|
13
|
+
* {
|
|
14
|
+
* "id": "dmr-1",
|
|
15
|
+
* "question": "What is the user's sister's name?",
|
|
16
|
+
* "answer": "Anna",
|
|
17
|
+
* "sessions": [
|
|
18
|
+
* { "id": "s1", "turns": [ { "role": "user", "content": "..." } ] },
|
|
19
|
+
* [ { "speaker": "assistant", "text": "..." } ]
|
|
20
|
+
* ]
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Every DMR case is treated as `multi-session` (its defining ability).
|
|
25
|
+
* The exact field names should be reconciled against the downloaded
|
|
26
|
+
* file the first time `scripts/fetch-eval-datasets.mjs` is run.
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFile } from 'node:fs/promises';
|
|
32
|
+
|
|
33
|
+
import type { Case, Dataset } from '@graphorin/observability/eval';
|
|
34
|
+
|
|
35
|
+
import type { MemoryEvalInput, MemoryEvalSession, MemoryEvalTurn } from './memory-eval.js';
|
|
36
|
+
|
|
37
|
+
/** @stable */
|
|
38
|
+
export interface LoadDmrOptions {
|
|
39
|
+
/** Local path to the DMR JSON (under `benchmarks/.datasets/`). */
|
|
40
|
+
readonly path: string;
|
|
41
|
+
/** Optional dataset name surfaced in `Dataset.metadata.name`. */
|
|
42
|
+
readonly name?: string;
|
|
43
|
+
/** Optional description surfaced in `Dataset.metadata.description`. */
|
|
44
|
+
readonly description?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read a DMR JSON file and return a fully-materialised {@link Dataset}
|
|
49
|
+
* of multi-session retrieval cases scored against the reference answer.
|
|
50
|
+
*
|
|
51
|
+
* @stable
|
|
52
|
+
*/
|
|
53
|
+
export async function loadDmrDataset(
|
|
54
|
+
options: LoadDmrOptions,
|
|
55
|
+
): Promise<Dataset<MemoryEvalInput, string>> {
|
|
56
|
+
const text = await readFile(options.path, 'utf8');
|
|
57
|
+
const cases = parseDmr(text);
|
|
58
|
+
return {
|
|
59
|
+
cases,
|
|
60
|
+
metadata: {
|
|
61
|
+
name: options.name ?? 'dmr',
|
|
62
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
63
|
+
createdAt: new Date(),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pure parser. Exported so tests can exercise the mapping without
|
|
70
|
+
* touching the filesystem.
|
|
71
|
+
*
|
|
72
|
+
* @stable
|
|
73
|
+
*/
|
|
74
|
+
export function parseDmr(text: string): ReadonlyArray<Case<MemoryEvalInput, string>> {
|
|
75
|
+
const parsed: unknown = JSON.parse(text);
|
|
76
|
+
if (!Array.isArray(parsed)) {
|
|
77
|
+
throw new Error('[graphorin/evals] DMR dataset must be a JSON array of items.');
|
|
78
|
+
}
|
|
79
|
+
const items = parsed as ReadonlyArray<unknown>;
|
|
80
|
+
const out: Case<MemoryEvalInput, string>[] = [];
|
|
81
|
+
for (let i = 0; i < items.length; i++) {
|
|
82
|
+
const item = items[i];
|
|
83
|
+
if (!isRecord(item)) {
|
|
84
|
+
throw new Error(`[graphorin/evals] DMR item ${i} is not an object.`);
|
|
85
|
+
}
|
|
86
|
+
const question = asString(item.question);
|
|
87
|
+
if (question === undefined) {
|
|
88
|
+
throw new Error(`[graphorin/evals] DMR item ${i} is missing a 'question'.`);
|
|
89
|
+
}
|
|
90
|
+
const sessions = coerceSessions(item.sessions ?? item.haystack_sessions);
|
|
91
|
+
out.push({
|
|
92
|
+
id: asString(item.id) ?? `dmr-${i}`,
|
|
93
|
+
input: { haystackSessions: sessions, question, ability: 'multi-session' },
|
|
94
|
+
expected: asString(item.answer) ?? '',
|
|
95
|
+
metadata: { datasetName: 'dmr', ability: 'multi-session' },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function coerceSessions(value: unknown): MemoryEvalSession[] {
|
|
102
|
+
const rawSessions = asArray(value);
|
|
103
|
+
const sessions: MemoryEvalSession[] = [];
|
|
104
|
+
for (let s = 0; s < rawSessions.length; s++) {
|
|
105
|
+
const raw = rawSessions[s];
|
|
106
|
+
const rawTurns = Array.isArray(raw)
|
|
107
|
+
? (raw as ReadonlyArray<unknown>)
|
|
108
|
+
: isRecord(raw)
|
|
109
|
+
? asArray(raw.turns ?? raw.messages ?? raw.dialog)
|
|
110
|
+
: [];
|
|
111
|
+
const id = isRecord(raw) ? (asString(raw.id) ?? `session-${s}`) : `session-${s}`;
|
|
112
|
+
const turns: MemoryEvalTurn[] = [];
|
|
113
|
+
for (const t of rawTurns) {
|
|
114
|
+
if (!isRecord(t)) continue;
|
|
115
|
+
const content = asString(t.content) ?? asString(t.text);
|
|
116
|
+
if (content === undefined) continue;
|
|
117
|
+
const speaker = asString(t.role) ?? asString(t.speaker);
|
|
118
|
+
const role = speaker === 'assistant' ? 'assistant' : 'user';
|
|
119
|
+
const timestamp = asString(t.timestamp);
|
|
120
|
+
turns.push({ role, content, ...(timestamp !== undefined ? { timestamp } : {}) });
|
|
121
|
+
}
|
|
122
|
+
sessions.push({ id, turns });
|
|
123
|
+
}
|
|
124
|
+
return sessions;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function asString(value: unknown): string | undefined {
|
|
128
|
+
return typeof value === 'string' ? value : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function asArray(value: unknown): ReadonlyArray<unknown> {
|
|
132
|
+
return Array.isArray(value) ? (value as ReadonlyArray<unknown>) : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
136
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
137
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loadDatasetFromTraces` - distil a dataset from the framework's
|
|
3
|
+
* replay log. The caller supplies the JSONL replay file and a small
|
|
4
|
+
* extraction function that pulls the `(input, output)` pair out of
|
|
5
|
+
* each event group; the loader handles the JSONL parsing + grouping
|
|
6
|
+
* by `runId`.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
|
|
13
|
+
import type { Case, Dataset } from '@graphorin/observability/eval';
|
|
14
|
+
|
|
15
|
+
/** @stable */
|
|
16
|
+
export interface TraceEvent {
|
|
17
|
+
readonly runId: string;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
20
|
+
readonly timestamp?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @stable */
|
|
24
|
+
export interface FromTracesOptions<I, O> {
|
|
25
|
+
/** Distil one `Case<I, O>` from every group of events sharing a `runId`. */
|
|
26
|
+
readonly extract: (events: ReadonlyArray<TraceEvent>) => Case<I, O> | null;
|
|
27
|
+
/** Optional name surfaced in `Dataset.metadata.name`. */
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
/** Optional description surfaced in `Dataset.metadata.description`. */
|
|
30
|
+
readonly description?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @stable */
|
|
34
|
+
export async function loadDatasetFromTraces<I, O>(
|
|
35
|
+
path: string,
|
|
36
|
+
options: FromTracesOptions<I, O>,
|
|
37
|
+
): Promise<Dataset<I, O>> {
|
|
38
|
+
const text = await readFile(path, 'utf8');
|
|
39
|
+
return groupAndExtract<I, O>(text, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure parser for the trace JSONL format. Exported so tests can
|
|
44
|
+
* exercise the extraction without touching the filesystem.
|
|
45
|
+
*
|
|
46
|
+
* @stable
|
|
47
|
+
*/
|
|
48
|
+
export function groupAndExtract<I, O>(
|
|
49
|
+
text: string,
|
|
50
|
+
options: FromTracesOptions<I, O>,
|
|
51
|
+
): Dataset<I, O> {
|
|
52
|
+
const lines = text.split(/\r?\n/);
|
|
53
|
+
const groups = new Map<string, TraceEvent[]>();
|
|
54
|
+
for (const raw of lines) {
|
|
55
|
+
if (raw.trim().length === 0) continue;
|
|
56
|
+
let parsed: unknown;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
|
|
63
|
+
const obj = parsed as Record<string, unknown>;
|
|
64
|
+
const runId = typeof obj.runId === 'string' ? obj.runId : null;
|
|
65
|
+
const type = typeof obj.type === 'string' ? obj.type : null;
|
|
66
|
+
if (runId === null || type === null) continue;
|
|
67
|
+
const payload =
|
|
68
|
+
obj.payload !== null && typeof obj.payload === 'object' && !Array.isArray(obj.payload)
|
|
69
|
+
? (obj.payload as Readonly<Record<string, unknown>>)
|
|
70
|
+
: ({} as Readonly<Record<string, unknown>>);
|
|
71
|
+
const event: TraceEvent = {
|
|
72
|
+
runId,
|
|
73
|
+
type,
|
|
74
|
+
payload,
|
|
75
|
+
...(typeof obj.timestamp === 'string' ? { timestamp: obj.timestamp } : {}),
|
|
76
|
+
};
|
|
77
|
+
const existing = groups.get(runId);
|
|
78
|
+
if (existing === undefined) groups.set(runId, [event]);
|
|
79
|
+
else existing.push(event);
|
|
80
|
+
}
|
|
81
|
+
const cases: Case<I, O>[] = [];
|
|
82
|
+
for (const events of groups.values()) {
|
|
83
|
+
const sample = options.extract(events);
|
|
84
|
+
if (sample !== null) cases.push(sample);
|
|
85
|
+
}
|
|
86
|
+
const meta: Dataset<I, O>['metadata'] = {
|
|
87
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
88
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
89
|
+
createdAt: new Date(),
|
|
90
|
+
};
|
|
91
|
+
return { cases, metadata: meta };
|
|
92
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataset loaders. Every loader returns a fully-materialised
|
|
3
|
+
* `Dataset` that the runner can iterate over without further
|
|
4
|
+
* I/O. Streaming loaders are a post-MVP follow-up.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
type LoadCsvOptions,
|
|
11
|
+
loadCsvDataset,
|
|
12
|
+
parseCsv,
|
|
13
|
+
} from './csv.js';
|
|
14
|
+
export {
|
|
15
|
+
type LoadDmrOptions,
|
|
16
|
+
loadDmrDataset,
|
|
17
|
+
parseDmr,
|
|
18
|
+
} from './dmr.js';
|
|
19
|
+
export {
|
|
20
|
+
type FromTracesOptions,
|
|
21
|
+
groupAndExtract,
|
|
22
|
+
loadDatasetFromTraces,
|
|
23
|
+
type TraceEvent,
|
|
24
|
+
} from './from-traces.js';
|
|
25
|
+
export { fromIterable } from './iterable.js';
|
|
26
|
+
export {
|
|
27
|
+
type LoadJsonlOptions,
|
|
28
|
+
loadJsonlDataset,
|
|
29
|
+
parseJsonl,
|
|
30
|
+
} from './jsonl.js';
|
|
31
|
+
export {
|
|
32
|
+
type LoadLocomoOptions,
|
|
33
|
+
loadLocomoDataset,
|
|
34
|
+
parseLocomo,
|
|
35
|
+
} from './locomo.js';
|
|
36
|
+
export {
|
|
37
|
+
type LoadLongMemEvalOptions,
|
|
38
|
+
loadLongMemEvalDataset,
|
|
39
|
+
parseLongMemEval,
|
|
40
|
+
} from './longmemeval.js';
|
|
41
|
+
export type {
|
|
42
|
+
MemoryEvalAbility,
|
|
43
|
+
MemoryEvalInput,
|
|
44
|
+
MemoryEvalSession,
|
|
45
|
+
MemoryEvalTurn,
|
|
46
|
+
} from './memory-eval.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fromIterable` - wrap an in-memory array / iterable as a
|
|
3
|
+
* {@link Dataset}. Useful for tests + ad-hoc data.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Case, Dataset } from '@graphorin/observability/eval';
|
|
9
|
+
|
|
10
|
+
/** @stable */
|
|
11
|
+
export function fromIterable<I, O = unknown>(
|
|
12
|
+
cases: Iterable<Case<I, O>> | ReadonlyArray<Case<I, O>>,
|
|
13
|
+
options: { readonly name?: string; readonly description?: string } = {},
|
|
14
|
+
): Dataset<I, O> {
|
|
15
|
+
const arr = Array.isArray(cases) ? (cases as ReadonlyArray<Case<I, O>>) : Array.from(cases);
|
|
16
|
+
const meta: Dataset<I, O>['metadata'] = {
|
|
17
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
18
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
19
|
+
createdAt: new Date(),
|
|
20
|
+
};
|
|
21
|
+
return { cases: arr, metadata: meta };
|
|
22
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL dataset loader. Each line is a JSON object with `input` and
|
|
3
|
+
* optional `expected` / `metadata` / `id` fields.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
|
|
10
|
+
import type { Case, Dataset } from '@graphorin/observability/eval';
|
|
11
|
+
|
|
12
|
+
/** @stable */
|
|
13
|
+
export interface LoadJsonlOptions {
|
|
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
|
+
* Map a parsed line into a `Case`. Default forwards the line
|
|
20
|
+
* verbatim. Override to translate column names or coerce types.
|
|
21
|
+
*/
|
|
22
|
+
readonly mapper?: (line: Record<string, unknown>, index: number) => Case<unknown, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read a JSONL file and return a fully-materialised {@link Dataset}.
|
|
27
|
+
* Empty lines are skipped; malformed lines throw with the line
|
|
28
|
+
* number.
|
|
29
|
+
*
|
|
30
|
+
* @stable
|
|
31
|
+
*/
|
|
32
|
+
export async function loadJsonlDataset(
|
|
33
|
+
path: string,
|
|
34
|
+
options: LoadJsonlOptions = {},
|
|
35
|
+
): Promise<Dataset<unknown, unknown>> {
|
|
36
|
+
const text = await readFile(path, 'utf8');
|
|
37
|
+
const cases = parseJsonl(text, options.mapper);
|
|
38
|
+
const meta: Dataset<unknown, unknown>['metadata'] = {
|
|
39
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
40
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
41
|
+
createdAt: new Date(),
|
|
42
|
+
};
|
|
43
|
+
return { cases, metadata: meta };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pure parser. Exported separately so tests can exercise the line-by-
|
|
48
|
+
* line behaviour without touching the filesystem.
|
|
49
|
+
*
|
|
50
|
+
* @stable
|
|
51
|
+
*/
|
|
52
|
+
export function parseJsonl(
|
|
53
|
+
text: string,
|
|
54
|
+
mapper?: LoadJsonlOptions['mapper'],
|
|
55
|
+
): ReadonlyArray<Case<unknown, unknown>> {
|
|
56
|
+
const out: Case<unknown, unknown>[] = [];
|
|
57
|
+
const lines = text.split(/\r?\n/);
|
|
58
|
+
for (let i = 0; i < lines.length; i++) {
|
|
59
|
+
const raw = lines[i] ?? '';
|
|
60
|
+
if (raw.trim().length === 0) continue;
|
|
61
|
+
let parsed: unknown;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(raw);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[graphorin/evals] failed to parse JSONL at line ${i + 1}: ${(err as Error).message}`,
|
|
67
|
+
{ cause: err },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`[graphorin/evals] JSONL line ${i + 1} must be a JSON object, got ${typeof parsed}.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const obj = parsed as Record<string, unknown>;
|
|
76
|
+
const mapped = mapper !== undefined ? mapper(obj, i) : defaultMapper(obj, i);
|
|
77
|
+
out.push(mapped);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function defaultMapper(line: Record<string, unknown>, _index: number): Case<unknown, unknown> {
|
|
83
|
+
if (!('input' in line)) {
|
|
84
|
+
throw new Error(`[graphorin/evals] JSONL row missing required 'input' field.`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...(typeof line.id === 'string' ? { id: line.id } : {}),
|
|
88
|
+
input: line.input,
|
|
89
|
+
...('expected' in line ? { expected: line.expected } : {}),
|
|
90
|
+
...(line.metadata !== undefined && typeof line.metadata === 'object' && line.metadata !== null
|
|
91
|
+
? { metadata: line.metadata as Readonly<Record<string, unknown>> }
|
|
92
|
+
: {}),
|
|
93
|
+
};
|
|
94
|
+
}
|