@botpress/evals 1.19.0-beta.14 → 1.19.0-beta.16
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/dist/client.d.ts +9 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +74 -20
- package/dist/client.js.map +4 -4
- package/dist/definition.d.ts +2 -3
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js.map +2 -2
- package/dist/graders/index.js +74 -21
- package/dist/graders/index.js.map +2 -2
- package/dist/graders/llm.d.ts +12 -12
- package/dist/graders/llm.d.ts.map +1 -1
- package/dist/graders/llm.js +75 -30
- package/dist/graders/llm.js.map +2 -2
- package/dist/graders/response.js +65 -16
- package/dist/graders/response.js.map +2 -2
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -10
- package/dist/index.js.map +3 -3
- package/dist/loader.js +39 -1
- package/dist/loader.js.map +3 -3
- package/dist/runner.d.ts +1 -5
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +217 -141
- package/dist/runner.js.map +4 -4
- package/dist/{spans/sse-collector.d.ts → sse-collector.d.ts} +4 -6
- package/dist/sse-collector.d.ts.map +1 -0
- package/dist/{spans/sse-collector.js → sse-collector.js} +49 -17
- package/dist/sse-collector.js.map +7 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/transformer.d.ts +2 -2
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +1 -1
- package/dist/transformer.js.map +2 -2
- package/dist/types.d.ts +33 -38
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +2 -2
- package/package.json +6 -66
- package/dist/manifest.d.ts +0 -13
- package/dist/manifest.d.ts.map +0 -1
- package/dist/spans/index.d.ts +0 -5
- package/dist/spans/index.d.ts.map +0 -1
- package/dist/spans/index.js +0 -642
- package/dist/spans/index.js.map +0 -7
- package/dist/spans/local-span-source.d.ts +0 -16
- package/dist/spans/local-span-source.d.ts.map +0 -1
- package/dist/spans/local-span-source.js +0 -478
- package/dist/spans/local-span-source.js.map +0 -7
- package/dist/spans/span-source.d.ts +0 -20
- package/dist/spans/span-source.d.ts.map +0 -1
- package/dist/spans/span-source.js +0 -1
- package/dist/spans/span-source.js.map +0 -7
- package/dist/spans/sse-collector.d.ts.map +0 -1
- package/dist/spans/sse-collector.js.map +0 -7
- package/dist/spans/trace.d.ts.map +0 -1
- package/dist/spans/vortex-span-source.d.ts +0 -34
- package/dist/spans/vortex-span-source.d.ts.map +0 -1
- package/dist/spans/vortex-span-source.js +0 -166
- package/dist/spans/vortex-span-source.js.map +0 -7
- package/dist/stores/eval-definition-loader.d.ts +0 -4
- package/dist/stores/eval-definition-loader.d.ts.map +0 -1
- package/dist/stores/eval-definition-loader.js +0 -104
- package/dist/stores/eval-definition-loader.js.map +0 -7
- package/dist/stores/eval-store.d.ts +0 -61
- package/dist/stores/eval-store.d.ts.map +0 -1
- package/dist/stores/eval-store.js +0 -1
- package/dist/stores/eval-store.js.map +0 -7
- package/dist/stores/index.d.ts +0 -8
- package/dist/stores/index.d.ts.map +0 -1
- package/dist/stores/index.js +0 -1118
- package/dist/stores/index.js.map +0 -7
- package/dist/stores/local-eval-store.d.ts +0 -66
- package/dist/stores/local-eval-store.d.ts.map +0 -1
- package/dist/stores/local-eval-store.js +0 -557
- package/dist/stores/local-eval-store.js.map +0 -7
- package/dist/stores/vortex-eval-store-entry.d.ts +0 -6
- package/dist/stores/vortex-eval-store-entry.d.ts.map +0 -1
- package/dist/stores/vortex-eval-store-entry.js +0 -664
- package/dist/stores/vortex-eval-store-entry.js.map +0 -7
- package/dist/stores/vortex-eval-store.d.ts +0 -78
- package/dist/stores/vortex-eval-store.d.ts.map +0 -1
- package/dist/stores/vortex-eval-store.js +0 -562
- package/dist/stores/vortex-eval-store.js.map +0 -7
- /package/dist/{spans/trace.d.ts → trace.d.ts} +0 -0
- /package/dist/{spans/trace.js → trace.js} +0 -0
- /package/dist/{spans/trace.js.map → trace.js.map} +0 -0
|
@@ -1,664 +0,0 @@
|
|
|
1
|
-
// src/stores/vortex-eval-store.ts
|
|
2
|
-
var OUTCOME_TURN_INDEX = -1;
|
|
3
|
-
function vortexEntryToEvalReport(entry) {
|
|
4
|
-
const outcomeAssertions = entry.results.filter((r) => r.turnIndex === OUTCOME_TURN_INDEX).map((r) => ({
|
|
5
|
-
assertion: r.graderName,
|
|
6
|
-
pass: r.passed,
|
|
7
|
-
expected: r.evidence?.expected ?? "",
|
|
8
|
-
actual: r.evidence?.actual ?? ""
|
|
9
|
-
}));
|
|
10
|
-
const resultsByTurn = /* @__PURE__ */ new Map();
|
|
11
|
-
for (const r of entry.results) {
|
|
12
|
-
if (r.turnIndex === OUTCOME_TURN_INDEX) continue;
|
|
13
|
-
const existing = resultsByTurn.get(r.turnIndex) ?? [];
|
|
14
|
-
existing.push(r);
|
|
15
|
-
resultsByTurn.set(r.turnIndex, existing);
|
|
16
|
-
}
|
|
17
|
-
const turns = [...resultsByTurn.entries()].sort(([a], [b]) => a - b).map(([turnIndex, results]) => {
|
|
18
|
-
const assertions = results.map((r) => ({
|
|
19
|
-
assertion: r.graderName,
|
|
20
|
-
pass: r.passed,
|
|
21
|
-
expected: r.evidence?.expected ?? "",
|
|
22
|
-
actual: r.evidence?.actual ?? ""
|
|
23
|
-
}));
|
|
24
|
-
const first = results[0];
|
|
25
|
-
return {
|
|
26
|
-
turnIndex,
|
|
27
|
-
userMessage: first.userMessage,
|
|
28
|
-
botResponse: first.botResponse,
|
|
29
|
-
assertions,
|
|
30
|
-
pass: assertions.every((a) => a.pass),
|
|
31
|
-
botDuration: first.botDurationMs ?? 0,
|
|
32
|
-
evalDuration: first.graderDurationMs ?? 0
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
return {
|
|
36
|
-
name: entry.evalName,
|
|
37
|
-
description: entry.description || void 0,
|
|
38
|
-
type: entry.evalType,
|
|
39
|
-
tags: entry.tags,
|
|
40
|
-
turns,
|
|
41
|
-
outcomeAssertions,
|
|
42
|
-
pass: entry.passed ?? false,
|
|
43
|
-
duration: entry.durationMs ?? 0,
|
|
44
|
-
error: entry.error ?? void 0
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
async function mapWithConcurrency(items, concurrency, fn) {
|
|
48
|
-
const results = new Array(items.length);
|
|
49
|
-
let next = 0;
|
|
50
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
51
|
-
while (next < items.length) {
|
|
52
|
-
const i = next++;
|
|
53
|
-
results[i] = await fn(items[i]);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
await Promise.all(workers);
|
|
57
|
-
return results;
|
|
58
|
-
}
|
|
59
|
-
function turnToResultRows(turn) {
|
|
60
|
-
return graderResultsToRows(turn.assertions, {
|
|
61
|
-
turnIndex: turn.turnIndex,
|
|
62
|
-
userMessage: turn.userMessage,
|
|
63
|
-
botResponse: turn.botResponse,
|
|
64
|
-
botDurationMs: turn.botDuration,
|
|
65
|
-
graderDurationMs: turn.evalDuration
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
function outcomeToResultRows(assertions) {
|
|
69
|
-
return graderResultsToRows(assertions, {
|
|
70
|
-
turnIndex: OUTCOME_TURN_INDEX,
|
|
71
|
-
userMessage: "",
|
|
72
|
-
botResponse: "",
|
|
73
|
-
botDurationMs: 0,
|
|
74
|
-
graderDurationMs: 0
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
function graderResultsToRows(assertions, context) {
|
|
78
|
-
return assertions.map((assertion) => ({
|
|
79
|
-
...context,
|
|
80
|
-
graderName: assertion.assertion,
|
|
81
|
-
passed: assertion.pass,
|
|
82
|
-
evidence: { expected: assertion.expected, actual: assertion.actual }
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
function runMatchesWatchOptions(run, options) {
|
|
86
|
-
if (options.runId && run.id !== options.runId) return false;
|
|
87
|
-
if (options.workflowId && run.metadata?.workflowId !== options.workflowId) return false;
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
function vortexRunToReport(run) {
|
|
91
|
-
const evals = run.entries.map(vortexEntryToEvalReport);
|
|
92
|
-
return {
|
|
93
|
-
id: run.id,
|
|
94
|
-
timestamp: run.createdAt,
|
|
95
|
-
evals,
|
|
96
|
-
passed: evals.filter((e) => e.pass).length,
|
|
97
|
-
failed: evals.filter((e) => !e.pass).length,
|
|
98
|
-
total: evals.length,
|
|
99
|
-
duration: evals.reduce((s, e) => s + e.duration, 0)
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function vortexRunToSummary(run) {
|
|
103
|
-
const startMs = run.startedAt ? new Date(run.startedAt).getTime() : 0;
|
|
104
|
-
const endMs = run.completedAt ? new Date(run.completedAt).getTime() : 0;
|
|
105
|
-
if (run.entries) {
|
|
106
|
-
const evals = run.entries.map(vortexEntryToEvalReport);
|
|
107
|
-
return {
|
|
108
|
-
id: run.id,
|
|
109
|
-
timestamp: run.createdAt,
|
|
110
|
-
passed: evals.filter((e) => e.pass).length,
|
|
111
|
-
failed: evals.filter((e) => !e.pass).length,
|
|
112
|
-
total: evals.length,
|
|
113
|
-
duration: evals.reduce((s, e) => s + e.duration, 0),
|
|
114
|
-
botDuration: evals.reduce((s, e) => e.turns.reduce((ts, t) => ts + t.botDuration, s), 0),
|
|
115
|
-
evalDuration: evals.reduce((s, e) => e.turns.reduce((ts, t) => ts + t.evalDuration, s), 0),
|
|
116
|
-
evalNames: evals.map((e) => e.name),
|
|
117
|
-
aborted: false
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
id: run.id,
|
|
122
|
-
timestamp: run.createdAt,
|
|
123
|
-
passed: 0,
|
|
124
|
-
failed: 0,
|
|
125
|
-
total: 0,
|
|
126
|
-
duration: startMs && endMs ? endMs - startMs : 0,
|
|
127
|
-
botDuration: 0,
|
|
128
|
-
evalDuration: 0,
|
|
129
|
-
evalNames: [],
|
|
130
|
-
aborted: false
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
var VortexEvalStore = class {
|
|
134
|
-
url;
|
|
135
|
-
botId;
|
|
136
|
-
workspaceId;
|
|
137
|
-
token;
|
|
138
|
-
_evalManifestId;
|
|
139
|
-
_loadEvalDefinitions;
|
|
140
|
-
constructor(config) {
|
|
141
|
-
this.url = config.url.replace(/\/$/, "");
|
|
142
|
-
this.botId = config.botId;
|
|
143
|
-
if (config.workspaceId) this.workspaceId = config.workspaceId;
|
|
144
|
-
if (config.token) this.token = config.token;
|
|
145
|
-
this._evalManifestId = config.evalManifestId;
|
|
146
|
-
this._loadEvalDefinitions = config.loadEvalDefinitions;
|
|
147
|
-
}
|
|
148
|
-
// ── Eval definitions (from Files API manifest) ───────────────────────
|
|
149
|
-
async listEvals(filter) {
|
|
150
|
-
const defs = await this.getEvalDefinitions();
|
|
151
|
-
const filtered = defs.filter((e) => {
|
|
152
|
-
if (filter?.names && !filter.names.includes(e.name)) return false;
|
|
153
|
-
if (filter?.type && e.type !== filter.type) return false;
|
|
154
|
-
if (filter?.tags && !filter.tags.every((tag) => e.tags?.includes(tag))) return false;
|
|
155
|
-
return true;
|
|
156
|
-
});
|
|
157
|
-
return filtered.map((e) => ({
|
|
158
|
-
name: e.name,
|
|
159
|
-
description: e.description,
|
|
160
|
-
tags: e.tags || [],
|
|
161
|
-
type: e.type || "capability",
|
|
162
|
-
turnCount: e.conversation.length,
|
|
163
|
-
hasOutcome: !!e.outcome
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
async getEval(name) {
|
|
167
|
-
const defs = await this.getEvalDefinitions();
|
|
168
|
-
return defs.find((e) => e.name === name) ?? null;
|
|
169
|
-
}
|
|
170
|
-
// ── Run lifecycle — write ───────────────────────────────────────────
|
|
171
|
-
async createRun(runType = "scheduled", metadata) {
|
|
172
|
-
if (!this.workspaceId) {
|
|
173
|
-
throw new Error("workspaceId is required to create Vortex eval runs");
|
|
174
|
-
}
|
|
175
|
-
const triggerType = runType === "manual" ? "manual" : "scheduled";
|
|
176
|
-
const res = await this.fetch(`/v1/evals/bot/${this.botId}/runs`, {
|
|
177
|
-
method: "POST",
|
|
178
|
-
headers: { "Content-Type": "application/json" },
|
|
179
|
-
body: JSON.stringify({
|
|
180
|
-
evalManifestId: (typeof this._evalManifestId === "function" ? this._evalManifestId() : this._evalManifestId) ?? "",
|
|
181
|
-
workspaceId: this.workspaceId,
|
|
182
|
-
triggerType,
|
|
183
|
-
...metadata ? { metadata } : {}
|
|
184
|
-
})
|
|
185
|
-
});
|
|
186
|
-
if (!res.ok) {
|
|
187
|
-
const text = await res.text().catch(() => "");
|
|
188
|
-
throw new Error(`Vortex create run failed: ${res.status} ${text}`);
|
|
189
|
-
}
|
|
190
|
-
const { id } = await res.json();
|
|
191
|
-
return id;
|
|
192
|
-
}
|
|
193
|
-
async addRunResults(runId, evalReport) {
|
|
194
|
-
const entry = {
|
|
195
|
-
evalName: evalReport.name,
|
|
196
|
-
evalType: evalReport.type ?? "capability",
|
|
197
|
-
description: evalReport.description ?? "",
|
|
198
|
-
tags: evalReport.tags ?? [],
|
|
199
|
-
passed: evalReport.pass,
|
|
200
|
-
durationMs: evalReport.duration,
|
|
201
|
-
error: evalReport.error,
|
|
202
|
-
results: [...evalReport.turns.flatMap(turnToResultRows), ...outcomeToResultRows(evalReport.outcomeAssertions)]
|
|
203
|
-
};
|
|
204
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/entries`, {
|
|
205
|
-
method: "POST",
|
|
206
|
-
headers: { "Content-Type": "application/json" },
|
|
207
|
-
body: JSON.stringify({ entries: [entry] })
|
|
208
|
-
});
|
|
209
|
-
if (!res.ok) {
|
|
210
|
-
const text = await res.text().catch(() => "");
|
|
211
|
-
throw new Error(`Vortex ingest entries failed: ${res.status} ${text}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
async completeRun(runId, report) {
|
|
215
|
-
const hasRunError = report.aborted === true || report.evals.some((e) => e.error !== void 0);
|
|
216
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/entries`, {
|
|
217
|
-
method: "POST",
|
|
218
|
-
headers: { "Content-Type": "application/json" },
|
|
219
|
-
body: JSON.stringify({
|
|
220
|
-
entries: [],
|
|
221
|
-
completed: !hasRunError,
|
|
222
|
-
failed: hasRunError
|
|
223
|
-
})
|
|
224
|
-
});
|
|
225
|
-
if (!res.ok) {
|
|
226
|
-
const text = await res.text().catch(() => "");
|
|
227
|
-
throw new Error(`Vortex complete run failed: ${res.status} ${text}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// ── Per-turn incremental lifecycle (Option B) ──
|
|
231
|
-
//
|
|
232
|
-
// The eval-runner workflow builds each entry up as the eval runs (rather than
|
|
233
|
-
// writing it whole at the end), so the dev console can stream turn-by-turn
|
|
234
|
-
// progress. This is the prod ingestion path and requires Vortex to provide:
|
|
235
|
-
// startEntry POST /runs/{runId}/entries → { entries:[{ id }] } (passed:null, in-progress)
|
|
236
|
-
// appendTurnResults POST /runs/{runId}/entries/{entryId}/results (idempotent upsert by turnIndex+graderName)
|
|
237
|
-
// finalizeEntry PATCH /runs/{runId}/entries/{entryId} (sets the verdict)
|
|
238
|
-
// markRunComplete POST /runs/{runId}/complete (status → completed|failed)
|
|
239
|
-
/** Create an in-progress entry (metadata only, no results) and return its server id. */
|
|
240
|
-
async startEntry(runId, meta) {
|
|
241
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/entries`, {
|
|
242
|
-
method: "POST",
|
|
243
|
-
headers: { "Content-Type": "application/json" },
|
|
244
|
-
body: JSON.stringify({
|
|
245
|
-
entries: [
|
|
246
|
-
{
|
|
247
|
-
evalName: meta.evalName,
|
|
248
|
-
evalType: meta.evalType ?? "capability",
|
|
249
|
-
description: meta.description ?? "",
|
|
250
|
-
tags: meta.tags ?? []
|
|
251
|
-
}
|
|
252
|
-
]
|
|
253
|
-
})
|
|
254
|
-
});
|
|
255
|
-
if (!res.ok) {
|
|
256
|
-
const text = await res.text().catch(() => "");
|
|
257
|
-
throw new Error(`Vortex start entry failed: ${res.status} ${text}`);
|
|
258
|
-
}
|
|
259
|
-
const data = await res.json();
|
|
260
|
-
const entryId = data.entries?.[0]?.id ?? data.id;
|
|
261
|
-
if (!entryId) {
|
|
262
|
-
throw new Error("Vortex start entry returned no entry id");
|
|
263
|
-
}
|
|
264
|
-
return entryId;
|
|
265
|
-
}
|
|
266
|
-
/** Append one turn's grader results to an existing entry. */
|
|
267
|
-
async appendTurnResults(runId, entryId, turn) {
|
|
268
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/entries/${entryId}/results`, {
|
|
269
|
-
method: "POST",
|
|
270
|
-
headers: { "Content-Type": "application/json" },
|
|
271
|
-
body: JSON.stringify({ results: turnToResultRows(turn) })
|
|
272
|
-
});
|
|
273
|
-
if (!res.ok) {
|
|
274
|
-
const text = await res.text().catch(() => "");
|
|
275
|
-
throw new Error(`Vortex append results failed: ${res.status} ${text}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
/** Append outcome-level grader results to an existing entry. */
|
|
279
|
-
async appendOutcomeResults(runId, entryId, assertions) {
|
|
280
|
-
if (assertions.length === 0) return;
|
|
281
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/entries/${entryId}/results`, {
|
|
282
|
-
method: "POST",
|
|
283
|
-
headers: { "Content-Type": "application/json" },
|
|
284
|
-
body: JSON.stringify({ results: outcomeToResultRows(assertions) })
|
|
285
|
-
});
|
|
286
|
-
if (!res.ok) {
|
|
287
|
-
const text = await res.text().catch(() => "");
|
|
288
|
-
throw new Error(`Vortex append results failed: ${res.status} ${text}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/** Set the verdict on an entry once its eval finishes. */
|
|
292
|
-
async finalizeEntry(runId, entryId, verdict) {
|
|
293
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/entries/${entryId}`, {
|
|
294
|
-
method: "PATCH",
|
|
295
|
-
headers: { "Content-Type": "application/json" },
|
|
296
|
-
body: JSON.stringify({
|
|
297
|
-
passed: verdict.passed,
|
|
298
|
-
...verdict.durationMs !== void 0 ? { durationMs: verdict.durationMs } : {},
|
|
299
|
-
...verdict.error !== void 0 ? { error: verdict.error } : {}
|
|
300
|
-
})
|
|
301
|
-
});
|
|
302
|
-
if (!res.ok) {
|
|
303
|
-
const text = await res.text().catch(() => "");
|
|
304
|
-
throw new Error(`Vortex finalize entry failed: ${res.status} ${text}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/** Flip the run to its terminal status (replaces completeRun's empty-entries hack). */
|
|
308
|
-
async markRunComplete(runId, opts = {}) {
|
|
309
|
-
const res = await this.fetch(`/v1/evals/runs/${runId}/complete`, {
|
|
310
|
-
method: "POST",
|
|
311
|
-
headers: { "Content-Type": "application/json" },
|
|
312
|
-
body: JSON.stringify({ ...opts.failed !== void 0 ? { failed: opts.failed } : {} })
|
|
313
|
-
});
|
|
314
|
-
if (!res.ok) {
|
|
315
|
-
const text = await res.text().catch(() => "");
|
|
316
|
-
throw new Error(`Vortex complete run failed: ${res.status} ${text}`);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// ── Run results — read ─────────────────────────────────────────────
|
|
320
|
-
async loadRunResult(runId) {
|
|
321
|
-
const data = await this.fetchJson(`/v1/evals/runs/${runId}`);
|
|
322
|
-
return vortexRunToReport(data);
|
|
323
|
-
}
|
|
324
|
-
async getLatestRun() {
|
|
325
|
-
const data = await this.fetchJson(`/v1/evals/bot/${this.botId}/runs`, { limit: "1" });
|
|
326
|
-
if (!data.runs.length) return null;
|
|
327
|
-
const latest = data.runs[0];
|
|
328
|
-
return this.loadRunResult(latest.id);
|
|
329
|
-
}
|
|
330
|
-
async listRunSummaries(opts) {
|
|
331
|
-
const runs = await this.fetchRunsWithEntries(opts?.limit ?? 50, opts?.since);
|
|
332
|
-
return runs.map(vortexRunToSummary);
|
|
333
|
-
}
|
|
334
|
-
async listEvalReportsByName(evalName, opts) {
|
|
335
|
-
const limit = opts?.limit ?? 20;
|
|
336
|
-
const runs = await this.fetchRunsWithEntries(50, opts?.since);
|
|
337
|
-
const entries = [];
|
|
338
|
-
for (const run of runs) {
|
|
339
|
-
if (entries.length >= limit) break;
|
|
340
|
-
const entry = run.entries.find((e) => e.evalName === evalName);
|
|
341
|
-
if (entry) {
|
|
342
|
-
entries.push({
|
|
343
|
-
runId: run.id,
|
|
344
|
-
timestamp: run.createdAt,
|
|
345
|
-
report: vortexEntryToEvalReport(entry),
|
|
346
|
-
totalEvalsInRun: run.entries.length
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return entries;
|
|
351
|
-
}
|
|
352
|
-
async listEvalReportsBulk(opts) {
|
|
353
|
-
const per = opts?.perEval ?? 20;
|
|
354
|
-
const runs = await this.fetchRunsWithEntries(50, opts?.since);
|
|
355
|
-
const grouped = {};
|
|
356
|
-
for (const run of runs) {
|
|
357
|
-
for (const entry of run.entries) {
|
|
358
|
-
if (!grouped[entry.evalName]) grouped[entry.evalName] = [];
|
|
359
|
-
if (grouped[entry.evalName].length < per) {
|
|
360
|
-
grouped[entry.evalName].push({
|
|
361
|
-
runId: run.id,
|
|
362
|
-
timestamp: run.createdAt,
|
|
363
|
-
report: vortexEntryToEvalReport(entry),
|
|
364
|
-
totalEvalsInRun: run.entries.length
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return grouped;
|
|
370
|
-
}
|
|
371
|
-
// ── Live run tracking ──────────────────────────────────────────────
|
|
372
|
-
async *watchRun(signal, options = {}) {
|
|
373
|
-
const POLL_INTERVAL = 1500;
|
|
374
|
-
const POLL_TIMEOUT = 60 * 60 * 1e3;
|
|
375
|
-
const start = Date.now();
|
|
376
|
-
const hasExpectedRun = !!options.runId || !!options.workflowId;
|
|
377
|
-
let baselineRunId = null;
|
|
378
|
-
if (!hasExpectedRun) {
|
|
379
|
-
try {
|
|
380
|
-
const initial = await this.fetchJson(`/v1/evals/bot/${this.botId}/runs`, {
|
|
381
|
-
limit: "1"
|
|
382
|
-
});
|
|
383
|
-
baselineRunId = initial.runs[0]?.id ?? null;
|
|
384
|
-
} catch {
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
const evalMeta = await this.loadEvalMeta();
|
|
388
|
-
const startedEvals = /* @__PURE__ */ new Set();
|
|
389
|
-
const seenTurns = /* @__PURE__ */ new Set();
|
|
390
|
-
const startedTurns = /* @__PURE__ */ new Set();
|
|
391
|
-
const finalizedEvals = /* @__PURE__ */ new Set();
|
|
392
|
-
while (!signal?.aborted && Date.now() - start < POLL_TIMEOUT) {
|
|
393
|
-
try {
|
|
394
|
-
const data = await this.fetchJson(`/v1/evals/bot/${this.botId}/runs`, {
|
|
395
|
-
limit: hasExpectedRun ? "20" : "1"
|
|
396
|
-
});
|
|
397
|
-
let latest = null;
|
|
398
|
-
let raw = null;
|
|
399
|
-
for (const candidate of data.runs) {
|
|
400
|
-
if (options.runId && candidate.id !== options.runId) continue;
|
|
401
|
-
if (options.workflowId && candidate.metadata?.workflowId !== options.workflowId) {
|
|
402
|
-
const candidateRaw = await this.fetchJson(`/v1/evals/runs/${candidate.id}`);
|
|
403
|
-
if (!runMatchesWatchOptions(candidateRaw, options)) continue;
|
|
404
|
-
raw = candidateRaw;
|
|
405
|
-
}
|
|
406
|
-
latest = candidate;
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
if (!latest || !hasExpectedRun && latest.id === baselineRunId) {
|
|
410
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
raw ??= await this.fetchJson(`/v1/evals/runs/${latest.id}`);
|
|
414
|
-
const terminal = raw.status === "completed" || raw.status === "failed";
|
|
415
|
-
for (let i = 0; i < raw.entries.length; i++) {
|
|
416
|
-
const entry = raw.entries[i];
|
|
417
|
-
const evalReport = vortexEntryToEvalReport(entry);
|
|
418
|
-
const meta = evalMeta.get(entry.evalName);
|
|
419
|
-
const totalTurns = meta?.totalTurns ?? evalReport.turns.length;
|
|
420
|
-
if (!startedEvals.has(entry.evalName)) {
|
|
421
|
-
startedEvals.add(entry.evalName);
|
|
422
|
-
yield { type: "eval_start", evalName: entry.evalName, index: i, totalTurns };
|
|
423
|
-
}
|
|
424
|
-
for (const turn of evalReport.turns) {
|
|
425
|
-
const turnKey = `${entry.evalName}::${turn.turnIndex}`;
|
|
426
|
-
if (seenTurns.has(turnKey)) continue;
|
|
427
|
-
seenTurns.add(turnKey);
|
|
428
|
-
yield {
|
|
429
|
-
type: "turn_complete",
|
|
430
|
-
evalName: entry.evalName,
|
|
431
|
-
evalIndex: i,
|
|
432
|
-
turnIndex: turn.turnIndex,
|
|
433
|
-
totalTurns,
|
|
434
|
-
turnReport: turn
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
if (entry.passed != null && !finalizedEvals.has(entry.evalName)) {
|
|
438
|
-
finalizedEvals.add(entry.evalName);
|
|
439
|
-
yield { type: "eval_complete", evalName: entry.evalName, index: i, report: evalReport };
|
|
440
|
-
} else if (entry.passed == null && !terminal) {
|
|
441
|
-
const nextTurnIndex = evalReport.turns.length;
|
|
442
|
-
const startKey = `${entry.evalName}::${nextTurnIndex}`;
|
|
443
|
-
if (nextTurnIndex < totalTurns && !startedTurns.has(startKey)) {
|
|
444
|
-
startedTurns.add(startKey);
|
|
445
|
-
yield {
|
|
446
|
-
type: "turn_start",
|
|
447
|
-
evalName: entry.evalName,
|
|
448
|
-
evalIndex: i,
|
|
449
|
-
turnIndex: nextTurnIndex,
|
|
450
|
-
totalTurns,
|
|
451
|
-
userMessage: meta?.userMessages[nextTurnIndex] ?? ""
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (terminal) {
|
|
457
|
-
const finalReport = vortexRunToReport(raw);
|
|
458
|
-
for (let i = 0; i < finalReport.evals.length; i++) {
|
|
459
|
-
const evalReport = finalReport.evals[i];
|
|
460
|
-
if (finalizedEvals.has(evalReport.name)) continue;
|
|
461
|
-
finalizedEvals.add(evalReport.name);
|
|
462
|
-
yield { type: "eval_complete", evalName: evalReport.name, index: i, report: evalReport };
|
|
463
|
-
}
|
|
464
|
-
yield {
|
|
465
|
-
type: "suite_complete",
|
|
466
|
-
report: finalReport,
|
|
467
|
-
error: raw.status === "failed" ? "Eval run failed" : void 0
|
|
468
|
-
};
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
} catch {
|
|
472
|
-
}
|
|
473
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
474
|
-
}
|
|
475
|
-
if (!signal?.aborted) {
|
|
476
|
-
yield {
|
|
477
|
-
type: "suite_complete",
|
|
478
|
-
report: {
|
|
479
|
-
id: "timeout",
|
|
480
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
481
|
-
evals: [],
|
|
482
|
-
passed: 0,
|
|
483
|
-
failed: 0,
|
|
484
|
-
total: 0,
|
|
485
|
-
duration: 0
|
|
486
|
-
},
|
|
487
|
-
error: "Eval run timed out"
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
// ── Runner state ───────────────────────────────────────────────────
|
|
492
|
-
async getRunnerState() {
|
|
493
|
-
try {
|
|
494
|
-
const data = await this.fetchJson(`/v1/evals/bot/${this.botId}/runs`, {
|
|
495
|
-
status: "running",
|
|
496
|
-
limit: "1"
|
|
497
|
-
});
|
|
498
|
-
const active = data.runs[0] ?? null;
|
|
499
|
-
return { running: !!active, runId: active?.id ?? null };
|
|
500
|
-
} catch {
|
|
501
|
-
return { running: false, runId: null };
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// ── Internal helpers ───────────────────────────────────────────────
|
|
505
|
-
async getEvalDefinitions() {
|
|
506
|
-
if (!this._loadEvalDefinitions) return [];
|
|
507
|
-
return this._loadEvalDefinitions();
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* evalName → { totalTurns, userMessages } from the eval definitions, so
|
|
511
|
-
* watchRun can report `totalTurns` and synthesize `turn_start` events (with
|
|
512
|
-
* the turn's user message) for the in-flight turn — matching the dev SSE
|
|
513
|
-
* experience where the pending turn is visible before its results land.
|
|
514
|
-
*/
|
|
515
|
-
async loadEvalMeta() {
|
|
516
|
-
try {
|
|
517
|
-
const defs = await this.getEvalDefinitions();
|
|
518
|
-
return new Map(
|
|
519
|
-
defs.map((d) => [
|
|
520
|
-
d.name,
|
|
521
|
-
{ totalTurns: d.conversation.length, userMessages: d.conversation.map((t) => t.user ?? "") }
|
|
522
|
-
])
|
|
523
|
-
);
|
|
524
|
-
} catch {
|
|
525
|
-
return /* @__PURE__ */ new Map();
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
async fetchRunsWithEntries(limit, since) {
|
|
529
|
-
const data = await this.fetchJson(`/v1/evals/bot/${this.botId}/runs`, {
|
|
530
|
-
limit: String(limit)
|
|
531
|
-
});
|
|
532
|
-
let runs = data.runs;
|
|
533
|
-
if (since) {
|
|
534
|
-
runs = runs.filter((r) => new Date(r.createdAt).getTime() >= since);
|
|
535
|
-
}
|
|
536
|
-
return mapWithConcurrency(runs, 8, (r) => this.fetchJson(`/v1/evals/runs/${r.id}`));
|
|
537
|
-
}
|
|
538
|
-
async fetch(path, init) {
|
|
539
|
-
const target = new URL(`${this.url}${path}`);
|
|
540
|
-
const headers = new Headers(init?.headers);
|
|
541
|
-
if (this.token) headers.set("Authorization", `Bearer ${this.token}`);
|
|
542
|
-
return globalThis.fetch(target, { ...init, headers });
|
|
543
|
-
}
|
|
544
|
-
async fetchJson(path, params) {
|
|
545
|
-
const target = new URL(`${this.url}${path}`);
|
|
546
|
-
if (params) {
|
|
547
|
-
for (const [k, v] of Object.entries(params)) target.searchParams.set(k, v);
|
|
548
|
-
}
|
|
549
|
-
const headers = new Headers();
|
|
550
|
-
if (this.token) headers.set("Authorization", `Bearer ${this.token}`);
|
|
551
|
-
const res = await globalThis.fetch(target, { headers });
|
|
552
|
-
if (!res.ok) {
|
|
553
|
-
const text = await res.text().catch(() => "");
|
|
554
|
-
throw new Error(`Vortex ${res.status}: ${text}`);
|
|
555
|
-
}
|
|
556
|
-
return res.json();
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
// src/loader.ts
|
|
561
|
-
import { readdirSync, existsSync } from "fs";
|
|
562
|
-
import { resolve } from "path";
|
|
563
|
-
|
|
564
|
-
// src/definition.ts
|
|
565
|
-
var Eval = class {
|
|
566
|
-
name;
|
|
567
|
-
description;
|
|
568
|
-
tags;
|
|
569
|
-
type;
|
|
570
|
-
setup;
|
|
571
|
-
conversation;
|
|
572
|
-
outcome;
|
|
573
|
-
options;
|
|
574
|
-
constructor(def) {
|
|
575
|
-
this.name = def.name;
|
|
576
|
-
this.conversation = def.conversation;
|
|
577
|
-
if (def.description !== void 0) this.description = def.description;
|
|
578
|
-
if (def.tags !== void 0) this.tags = def.tags;
|
|
579
|
-
if (def.type !== void 0) this.type = def.type;
|
|
580
|
-
if (def.setup !== void 0) this.setup = def.setup;
|
|
581
|
-
if (def.outcome !== void 0) this.outcome = def.outcome;
|
|
582
|
-
if (def.options !== void 0) this.options = def.options;
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
// src/errors.ts
|
|
587
|
-
import { AdkError } from "@botpress/analytics";
|
|
588
|
-
var EvalRunnerError = class extends AdkError {
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
// src/loader.ts
|
|
592
|
-
function isEvalDefinition(value) {
|
|
593
|
-
return value !== null && typeof value === "object" && typeof value.name === "string" && value.name !== "" && Array.isArray(value.conversation);
|
|
594
|
-
}
|
|
595
|
-
async function loadEvalFile(filePath) {
|
|
596
|
-
const absPath = resolve(filePath);
|
|
597
|
-
let mod;
|
|
598
|
-
try {
|
|
599
|
-
mod = await import(absPath);
|
|
600
|
-
} catch (err) {
|
|
601
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
602
|
-
throw new EvalRunnerError({
|
|
603
|
-
code: "EVAL_LOAD_FAILED",
|
|
604
|
-
message: `Failed to load eval file ${filePath}: ${msg}
|
|
605
|
-
|
|
606
|
-
Make sure your eval file:
|
|
607
|
-
- Has no syntax or type errors
|
|
608
|
-
- Exports one or more \`new Eval({...})\` instances
|
|
609
|
-
- Has all dependencies installed (\`bun install\`)`,
|
|
610
|
-
expected: true,
|
|
611
|
-
cause: err
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
const results = [];
|
|
615
|
-
for (const [key, value] of Object.entries(mod)) {
|
|
616
|
-
if (key === "__esModule") continue;
|
|
617
|
-
if (value instanceof Eval || isEvalDefinition(value)) {
|
|
618
|
-
results.push(value);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
if (results.length === 0) {
|
|
622
|
-
throw new EvalRunnerError({
|
|
623
|
-
code: "EVAL_FILE_EMPTY",
|
|
624
|
-
message: `Invalid eval file ${filePath}: no valid evals found. Export one or more \`new Eval({...})\` instances (as default or named exports).`,
|
|
625
|
-
expected: true
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
return results;
|
|
629
|
-
}
|
|
630
|
-
async function loadEvalsFromDir(dirPath) {
|
|
631
|
-
const absDir = resolve(dirPath);
|
|
632
|
-
if (!existsSync(absDir)) {
|
|
633
|
-
return [];
|
|
634
|
-
}
|
|
635
|
-
const files = readdirSync(absDir).filter((f) => f.endsWith(".eval.ts"));
|
|
636
|
-
const evals = [];
|
|
637
|
-
for (const f of files) {
|
|
638
|
-
const defs = await loadEvalFile(`${absDir}/${f}`);
|
|
639
|
-
evals.push(...defs);
|
|
640
|
-
}
|
|
641
|
-
const seen = /* @__PURE__ */ new Set();
|
|
642
|
-
for (const e of evals) {
|
|
643
|
-
if (seen.has(e.name)) {
|
|
644
|
-
throw new EvalRunnerError({
|
|
645
|
-
code: "EVAL_DUPLICATE_NAME",
|
|
646
|
-
message: `Duplicate eval name "${e.name}" found in ${dirPath} \u2014 names must be unique across the evals directory.`,
|
|
647
|
-
expected: true,
|
|
648
|
-
details: { name: e.name }
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
seen.add(e.name);
|
|
652
|
-
}
|
|
653
|
-
return evals;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// src/stores/eval-definition-loader.ts
|
|
657
|
-
function createDiskEvalLoader(evalsDir) {
|
|
658
|
-
return () => loadEvalsFromDir(evalsDir);
|
|
659
|
-
}
|
|
660
|
-
export {
|
|
661
|
-
VortexEvalStore,
|
|
662
|
-
createDiskEvalLoader
|
|
663
|
-
};
|
|
664
|
-
//# sourceMappingURL=vortex-eval-store-entry.js.map
|