@botpress/evals 1.19.0-beta.9 → 2.0.0-alpha.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/dist/client.d.ts +21 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +104 -75
- package/dist/client.js.map +4 -4
- package/dist/definition.d.ts +9 -2
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js.map +2 -2
- package/dist/graders/index.js +21 -74
- 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 +30 -75
- package/dist/graders/llm.js.map +2 -2
- package/dist/graders/response.js +16 -65
- package/dist/graders/response.js.map +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +3 -3
- package/dist/loader.js +1 -39
- package/dist/loader.js.map +3 -3
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +318 -227
- package/dist/runner.js.map +4 -4
- package/dist/spans/index.d.ts +5 -0
- package/dist/spans/index.d.ts.map +1 -0
- package/dist/spans/index.js +687 -0
- package/dist/spans/index.js.map +7 -0
- package/dist/spans/local-span-source.d.ts +19 -0
- package/dist/spans/local-span-source.d.ts.map +1 -0
- package/dist/spans/local-span-source.js +505 -0
- package/dist/spans/local-span-source.js.map +7 -0
- package/dist/spans/span-source.d.ts +40 -0
- package/dist/spans/span-source.d.ts.map +1 -0
- package/dist/spans/span-source.js +1 -0
- package/dist/{sse-collector.d.ts → spans/sse-collector.d.ts} +44 -5
- package/dist/spans/sse-collector.d.ts.map +1 -0
- package/dist/{sse-collector.js → spans/sse-collector.js} +98 -59
- package/dist/spans/sse-collector.js.map +7 -0
- package/dist/spans/trace.d.ts.map +1 -0
- package/dist/spans/trace.js.map +7 -0
- package/dist/spans/vortex-span-source.d.ts +42 -0
- package/dist/spans/vortex-span-source.d.ts.map +1 -0
- package/dist/spans/vortex-span-source.js +184 -0
- package/dist/spans/vortex-span-source.js.map +7 -0
- package/dist/stores/eval-definition-loader.d.ts +4 -0
- package/dist/stores/eval-definition-loader.d.ts.map +1 -0
- package/dist/stores/eval-definition-loader.js +104 -0
- package/dist/stores/eval-definition-loader.js.map +7 -0
- package/dist/stores/eval-store.d.ts +61 -0
- package/dist/stores/eval-store.d.ts.map +1 -0
- package/dist/stores/eval-store.js +1 -0
- package/dist/stores/eval-store.js.map +7 -0
- package/dist/stores/index.d.ts +8 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +1118 -0
- package/dist/stores/index.js.map +7 -0
- package/dist/stores/local-eval-store.d.ts +66 -0
- package/dist/stores/local-eval-store.d.ts.map +1 -0
- package/dist/stores/local-eval-store.js +557 -0
- package/dist/stores/local-eval-store.js.map +7 -0
- package/dist/stores/vortex-eval-store-entry.d.ts +6 -0
- package/dist/stores/vortex-eval-store-entry.d.ts.map +1 -0
- package/dist/stores/vortex-eval-store-entry.js +664 -0
- package/dist/stores/vortex-eval-store-entry.js.map +7 -0
- package/dist/stores/vortex-eval-store.d.ts +78 -0
- package/dist/stores/vortex-eval-store.d.ts.map +1 -0
- package/dist/stores/vortex-eval-store.js +562 -0
- package/dist/stores/vortex-eval-store.js.map +7 -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 +38 -33
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +2 -2
- package/package.json +66 -6
- package/dist/sse-collector.d.ts.map +0 -1
- package/dist/sse-collector.js.map +0 -7
- package/dist/trace.d.ts.map +0 -1
- /package/dist/{trace.js.map → spans/span-source.js.map} +0 -0
- /package/dist/{trace.d.ts → spans/trace.d.ts} +0 -0
- /package/dist/{trace.js → spans/trace.js} +0 -0
package/dist/runner.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/runner.ts
|
|
2
|
-
import { Client as BpClient2 } from "@botpress/client";
|
|
3
|
-
|
|
4
1
|
// src/definition.ts
|
|
5
2
|
var Eval = class {
|
|
6
3
|
name;
|
|
@@ -26,83 +23,33 @@ var Eval = class {
|
|
|
26
23
|
// src/types.ts
|
|
27
24
|
var defaultLogger = console;
|
|
28
25
|
|
|
29
|
-
// src/client.ts
|
|
30
|
-
import { Client as BpClient } from "@botpress/client";
|
|
31
|
-
import { createRequire } from "module";
|
|
32
|
-
|
|
33
|
-
// ../analytics/src/errors.ts
|
|
34
|
-
var AdkError = class extends Error {
|
|
35
|
-
/**
|
|
36
|
-
* Marker for cross-module-instance checks — `instanceof` breaks when a
|
|
37
|
-
* package is duplicated in the dependency graph.
|
|
38
|
-
* @internal
|
|
39
|
-
*/
|
|
40
|
-
static __IS_ADK_BASE_ERROR = true;
|
|
41
|
-
/**
|
|
42
|
-
* Stable SCREAMING_SNAKE_CASE identifier, e.g. 'BOT_NOT_FOUND'. Subclasses
|
|
43
|
-
* pin `Code` to a literal union (`class EvalRunnerError extends
|
|
44
|
-
* AdkError<EvalErrorCode>`) so typos are compile errors and `code` narrows
|
|
45
|
-
* in switches.
|
|
46
|
-
*/
|
|
47
|
-
code;
|
|
48
|
-
/**
|
|
49
|
-
* True for user/environment conditions the developer can fix themselves
|
|
50
|
-
* (validation, auth, connectivity). False for internal bugs. Defaults to
|
|
51
|
-
* false so unclassified errors surface as bugs until someone triages them.
|
|
52
|
-
*/
|
|
53
|
-
expected;
|
|
54
|
-
/** Structured context. Sanitized before any telemetry leaves the machine. */
|
|
55
|
-
details;
|
|
56
|
-
/** Actionable next step shown to the user, e.g. "Run 'adk login' first." */
|
|
57
|
-
suggestion;
|
|
58
|
-
constructor(opts) {
|
|
59
|
-
super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
60
|
-
this.name = this.constructor.name;
|
|
61
|
-
this.code = opts.code;
|
|
62
|
-
this.expected = opts.expected ?? false;
|
|
63
|
-
if (opts.details !== void 0) {
|
|
64
|
-
this.details = opts.details;
|
|
65
|
-
}
|
|
66
|
-
if (opts.suggestion !== void 0) {
|
|
67
|
-
this.suggestion = opts.suggestion;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
function isAdkError(error) {
|
|
72
|
-
if (error instanceof AdkError) return true;
|
|
73
|
-
if (!(error instanceof Error)) return false;
|
|
74
|
-
return error.constructor?.__IS_ADK_BASE_ERROR === true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
26
|
// src/errors.ts
|
|
27
|
+
import { AdkError } from "@botpress/analytics";
|
|
78
28
|
var EvalRunnerError = class extends AdkError {
|
|
79
29
|
};
|
|
80
30
|
|
|
81
31
|
// src/client.ts
|
|
82
|
-
var
|
|
83
|
-
function getDefaultChatClient() {
|
|
84
|
-
if (defaultChatClient) return defaultChatClient;
|
|
85
|
-
const require2 = createRequire(import.meta.url);
|
|
86
|
-
const mod = require2("@botpress/chat");
|
|
87
|
-
const ChatClient = mod.default?.Client ?? mod.Client;
|
|
88
|
-
if (!ChatClient) {
|
|
89
|
-
throw new EvalRunnerError({
|
|
90
|
-
code: "CHAT_CLIENT_MISSING",
|
|
91
|
-
message: "@botpress/chat did not expose a Client export. Pass EvalRunnerConfig.chatClient explicitly."
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return ChatClient;
|
|
95
|
-
}
|
|
32
|
+
var TRACE_STREAM_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
96
33
|
var ChatSession = class {
|
|
97
|
-
constructor(
|
|
98
|
-
this.
|
|
99
|
-
this.
|
|
34
|
+
constructor(bpClient, botId, _chatWebhookId, _chatBaseUrl, _chatClient) {
|
|
35
|
+
this.bpClient = bpClient;
|
|
36
|
+
this.botId = botId;
|
|
37
|
+
this._chatWebhookId = _chatWebhookId;
|
|
38
|
+
this._chatBaseUrl = _chatBaseUrl;
|
|
39
|
+
this._chatClient = _chatClient;
|
|
100
40
|
}
|
|
101
41
|
client = null;
|
|
102
42
|
conversationId = null;
|
|
103
43
|
async connect() {
|
|
104
|
-
const
|
|
105
|
-
this.
|
|
44
|
+
const webhookId = this._chatWebhookId ?? await discoverWebhookId(this.bpClient, this.botId);
|
|
45
|
+
const ChatClientCtor = this._chatClient ?? (await import(
|
|
46
|
+
/* webpackIgnore: true */
|
|
47
|
+
"@botpress/chat"
|
|
48
|
+
)).Client;
|
|
49
|
+
this.client = await ChatClientCtor.connect({
|
|
50
|
+
webhookId,
|
|
51
|
+
...this._chatBaseUrl ? { baseApiUrl: this._chatBaseUrl } : {}
|
|
52
|
+
});
|
|
106
53
|
}
|
|
107
54
|
/** Invariant guard — calling any session method before connect() is a runner bug. */
|
|
108
55
|
assertConnected() {
|
|
@@ -126,6 +73,16 @@ var ChatSession = class {
|
|
|
126
73
|
}
|
|
127
74
|
return this.conversationId;
|
|
128
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Open a fresh conversation under the same user (the user is fixed at
|
|
78
|
+
* connect()) and make it active. Returns the new conversation id.
|
|
79
|
+
*/
|
|
80
|
+
async newConversation() {
|
|
81
|
+
const client = this.assertConnected();
|
|
82
|
+
const conv = await client.createConversation({});
|
|
83
|
+
this.conversationId = conv.conversation.id;
|
|
84
|
+
return conv.conversation.id;
|
|
85
|
+
}
|
|
129
86
|
/**
|
|
130
87
|
* Send a user message. Fire-and-forget — observation is via SSECollector.
|
|
131
88
|
*/
|
|
@@ -134,17 +91,13 @@ var ChatSession = class {
|
|
|
134
91
|
const conversationId = await this.ensureConversation();
|
|
135
92
|
await client.createMessage({ conversationId, payload: { type: "text", text: message } });
|
|
136
93
|
}
|
|
137
|
-
|
|
138
|
-
* Send an event. Fire-and-forget — observation is via SSECollector.
|
|
139
|
-
*/
|
|
140
|
-
async sendEvent(type, payload) {
|
|
94
|
+
async sendEvent(payload) {
|
|
141
95
|
const client = this.assertConnected();
|
|
142
96
|
const conversationId = await this.ensureConversation();
|
|
143
|
-
await client.createEvent({
|
|
97
|
+
await client.createEvent({ payload, conversationId });
|
|
144
98
|
}
|
|
145
99
|
};
|
|
146
|
-
async function discoverWebhookId(
|
|
147
|
-
const client = new BpClient({ token, botId, apiUrl });
|
|
100
|
+
async function discoverWebhookId(client, botId) {
|
|
148
101
|
const { bot } = await client.getBot({ id: botId });
|
|
149
102
|
const integrations = bot.integrations || {};
|
|
150
103
|
const chat = Object.values(integrations).find((int) => int.name === "chat");
|
|
@@ -156,7 +109,7 @@ async function discoverWebhookId(botId, token, apiUrl) {
|
|
|
156
109
|
"The `chat` integration is not installed on this bot \u2014 evals require it.",
|
|
157
110
|
"",
|
|
158
111
|
"To fix:",
|
|
159
|
-
" 1. Install it: adk add chat",
|
|
112
|
+
" 1. Install it: adk integrations add chat",
|
|
160
113
|
" 2. Redeploy: adk dev (or: adk deploy)"
|
|
161
114
|
].join("\n"),
|
|
162
115
|
expected: true
|
|
@@ -164,6 +117,77 @@ async function discoverWebhookId(botId, token, apiUrl) {
|
|
|
164
117
|
}
|
|
165
118
|
return webhookId;
|
|
166
119
|
}
|
|
120
|
+
async function assertTraceStreamReadable(devServerUrl, headers = {}, timeoutMs = TRACE_STREAM_PREFLIGHT_TIMEOUT_MS) {
|
|
121
|
+
const url = `${devServerUrl.replace(/\/$/, "")}/api/traces/stream?count=1`;
|
|
122
|
+
const abortController = new AbortController();
|
|
123
|
+
let timedOut = false;
|
|
124
|
+
let reader = null;
|
|
125
|
+
const timer = setTimeout(() => {
|
|
126
|
+
timedOut = true;
|
|
127
|
+
abortController.abort();
|
|
128
|
+
}, timeoutMs);
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(url, {
|
|
131
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
132
|
+
signal: abortController.signal
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
throw traceStreamPreflightError(`Trace stream pre-flight failed: ${res.status} ${res.statusText}`);
|
|
136
|
+
}
|
|
137
|
+
if (!res.body) {
|
|
138
|
+
throw traceStreamPreflightError("Trace stream pre-flight failed: response had no body.");
|
|
139
|
+
}
|
|
140
|
+
reader = res.body.getReader();
|
|
141
|
+
const decoder = new TextDecoder();
|
|
142
|
+
let buffer = "";
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
if (done) {
|
|
146
|
+
throw traceStreamPreflightError("Trace stream pre-flight failed: stream closed before snapshot.");
|
|
147
|
+
}
|
|
148
|
+
buffer += decoder.decode(value, { stream: true });
|
|
149
|
+
const parts = buffer.split("\n\n");
|
|
150
|
+
buffer = parts.pop() ?? "";
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
if (parseSSEEventName(part) === "snapshot") {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof EvalRunnerError) {
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
if (timedOut) {
|
|
162
|
+
throw traceStreamPreflightError(
|
|
163
|
+
`Trace stream pre-flight timed out after ${timeoutMs}ms before receiving a snapshot.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
throw traceStreamPreflightError(
|
|
167
|
+
`Trace stream pre-flight failed: ${err instanceof Error ? err.message : String(err)}`
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
reader?.cancel().catch(() => {
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function parseSSEEventName(block) {
|
|
176
|
+
for (const line of block.split("\n")) {
|
|
177
|
+
if (line.startsWith("event:")) {
|
|
178
|
+
return line.slice("event:".length).trim();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function traceStreamPreflightError(message) {
|
|
184
|
+
return new EvalRunnerError({
|
|
185
|
+
code: "SSE_CONNECT_FAILED",
|
|
186
|
+
message,
|
|
187
|
+
expected: true,
|
|
188
|
+
suggestion: "Make sure the dev server is running (`adk dev`) and trace streaming is available."
|
|
189
|
+
});
|
|
190
|
+
}
|
|
167
191
|
async function assertChatChannelBound(devServerUrl, headers = {}, logger = defaultLogger) {
|
|
168
192
|
let conversations = [];
|
|
169
193
|
try {
|
|
@@ -201,13 +225,17 @@ async function assertChatChannelBound(devServerUrl, headers = {}, logger = defau
|
|
|
201
225
|
});
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
// src/sse-collector.ts
|
|
228
|
+
// src/spans/sse-collector.ts
|
|
205
229
|
function spanKey(span) {
|
|
206
230
|
return `${span.id.trace}:${span.id.span}`;
|
|
207
231
|
}
|
|
208
232
|
function spanData(span) {
|
|
209
233
|
return span.data && typeof span.data === "object" ? span.data : {};
|
|
210
234
|
}
|
|
235
|
+
var TURN_HANDLER_SPAN_NAMES = /* @__PURE__ */ new Set(["handler.conversation", "handler.event"]);
|
|
236
|
+
function isTurnHandlerSpan(span) {
|
|
237
|
+
return TURN_HANDLER_SPAN_NAMES.has(span.name);
|
|
238
|
+
}
|
|
211
239
|
var SSECollector = class _SSECollector {
|
|
212
240
|
devServerUrl;
|
|
213
241
|
headers;
|
|
@@ -225,6 +253,10 @@ var SSECollector = class _SSECollector {
|
|
|
225
253
|
connected = false;
|
|
226
254
|
/** Error from the SSE stream if it died unexpectedly. */
|
|
227
255
|
streamError = null;
|
|
256
|
+
/** Passive counters used only to explain timeout failures. */
|
|
257
|
+
updateEventCount = 0;
|
|
258
|
+
keepaliveEventCount = 0;
|
|
259
|
+
lastEvent = null;
|
|
228
260
|
/** Set while a settle phase is in flight; called by disconnect() to short-circuit. */
|
|
229
261
|
_cancelSettle = null;
|
|
230
262
|
constructor(devServerUrl, headers = {}) {
|
|
@@ -240,9 +272,10 @@ var SSECollector = class _SSECollector {
|
|
|
240
272
|
const attributeValue = "conversationId" in filter ? filter.conversationId : filter.workflowId;
|
|
241
273
|
const url = `${this.devServerUrl}/api/traces/stream?attributeName=${encodeURIComponent(attributeName)}&attributeValue=${encodeURIComponent(attributeValue)}`;
|
|
242
274
|
this.abortController = new AbortController();
|
|
275
|
+
const myController = this.abortController;
|
|
243
276
|
const response = await fetch(url, {
|
|
244
277
|
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
245
|
-
signal:
|
|
278
|
+
signal: myController.signal
|
|
246
279
|
});
|
|
247
280
|
if (!response.ok) {
|
|
248
281
|
throw new EvalRunnerError({
|
|
@@ -257,28 +290,48 @@ var SSECollector = class _SSECollector {
|
|
|
257
290
|
}
|
|
258
291
|
this.connected = true;
|
|
259
292
|
this._consumeStream(response.body).catch((err) => {
|
|
260
|
-
if (this.connected) {
|
|
293
|
+
if (this.connected && this.abortController === myController) {
|
|
261
294
|
this.streamError = err instanceof Error ? err : new Error(String(err));
|
|
262
295
|
}
|
|
263
296
|
});
|
|
264
297
|
await this._waitForSnapshot();
|
|
265
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Re-point at a new conversation, keeping `spanMap` and `seenHandlerKeys` (so
|
|
301
|
+
* the earlier conversation stays visible and its completed handler can't
|
|
302
|
+
* satisfy the next turn). Resets the snapshot latch so connect() awaits the
|
|
303
|
+
* new conversation's snapshot.
|
|
304
|
+
*/
|
|
305
|
+
async repoint(filter) {
|
|
306
|
+
this.connected = false;
|
|
307
|
+
this.abortController?.abort();
|
|
308
|
+
this.abortController = null;
|
|
309
|
+
this._cancelSettle?.();
|
|
310
|
+
this._cancelSettle = null;
|
|
311
|
+
this.waiters = [];
|
|
312
|
+
this.snapshotReceived = false;
|
|
313
|
+
this.snapshotResolvers = [];
|
|
314
|
+
this.streamError = null;
|
|
315
|
+
await this.connect(filter);
|
|
316
|
+
}
|
|
266
317
|
/**
|
|
267
318
|
* Mark the beginning of a new turn.
|
|
268
|
-
* Records which handler
|
|
269
|
-
* so waitForTurnComplete() only resolves on NEW
|
|
319
|
+
* Records which turn-handler span IDs (handler.conversation / handler.event)
|
|
320
|
+
* have been seen so far, so waitForTurnComplete() only resolves on NEW
|
|
321
|
+
* handler completions.
|
|
270
322
|
*/
|
|
271
323
|
startTurn() {
|
|
272
324
|
this.turnStartKeys = new Set(this.spanMap.keys());
|
|
273
325
|
for (const [key, span] of this.spanMap) {
|
|
274
|
-
if (span
|
|
326
|
+
if (isTurnHandlerSpan(span)) {
|
|
275
327
|
this.seenHandlerKeys.add(key);
|
|
276
328
|
}
|
|
277
329
|
}
|
|
278
330
|
}
|
|
279
331
|
static SETTLE_MS = 100;
|
|
280
332
|
/**
|
|
281
|
-
* Wait for a handler
|
|
333
|
+
* Wait for a turn-handler span (handler.conversation or handler.event) to
|
|
334
|
+
* reach terminal status (ok or error).
|
|
282
335
|
* Only considers spans NOT seen before the last startTurn() call.
|
|
283
336
|
*
|
|
284
337
|
* After the handler closes, waits for a quiet period (no new spans for SETTLE_MS)
|
|
@@ -287,8 +340,10 @@ var SSECollector = class _SSECollector {
|
|
|
287
340
|
* spans may arrive slightly after the handler close event.
|
|
288
341
|
*/
|
|
289
342
|
async waitForTurnComplete(opts) {
|
|
290
|
-
|
|
291
|
-
|
|
343
|
+
const accept = opts.acceptSpanNames;
|
|
344
|
+
const settle = { quietMs: opts.settleQuietMs, maxMs: opts.settleMaxMs };
|
|
345
|
+
if (this._hasNewCompletedHandler(accept)) {
|
|
346
|
+
return this._settleAfterHandler(settle);
|
|
292
347
|
}
|
|
293
348
|
if (opts.abortSignal?.aborted) {
|
|
294
349
|
throw new Error("waitForTurnComplete aborted");
|
|
@@ -297,8 +352,13 @@ var SSECollector = class _SSECollector {
|
|
|
297
352
|
const timer = setTimeout(() => {
|
|
298
353
|
this._removeWaiter(waiter);
|
|
299
354
|
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
300
|
-
const
|
|
301
|
-
|
|
355
|
+
const expected = accept ? [...accept].join(" or ") : "handler.conversation or handler.event";
|
|
356
|
+
const reason = this.streamError ? `SSE stream died: ${this.streamError.message}` : `no ${expected} span completed`;
|
|
357
|
+
reject(
|
|
358
|
+
new Error(
|
|
359
|
+
`waitForTurnComplete timed out after ${opts.timeout}ms \u2014 ${reason}; diagnostics: ${this._formatTurnDiagnostics()}`
|
|
360
|
+
)
|
|
361
|
+
);
|
|
302
362
|
}, opts.timeout);
|
|
303
363
|
const onAbort = () => {
|
|
304
364
|
clearTimeout(timer);
|
|
@@ -307,11 +367,11 @@ var SSECollector = class _SSECollector {
|
|
|
307
367
|
};
|
|
308
368
|
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
309
369
|
const waiter = (_span) => {
|
|
310
|
-
if (this._hasNewCompletedHandler()) {
|
|
370
|
+
if (this._hasNewCompletedHandler(accept)) {
|
|
311
371
|
clearTimeout(timer);
|
|
312
372
|
this._removeWaiter(waiter);
|
|
313
373
|
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
314
|
-
this._settleAfterHandler().then(resolve2, reject);
|
|
374
|
+
this._settleAfterHandler(settle).then(resolve2, reject);
|
|
315
375
|
}
|
|
316
376
|
};
|
|
317
377
|
this.waiters.push(waiter);
|
|
@@ -325,7 +385,9 @@ var SSECollector = class _SSECollector {
|
|
|
325
385
|
* Exposes a cancellation hook via _cancelSettle so disconnect() can short-
|
|
326
386
|
* circuit an in-flight settle phase and avoid leaking timers for up to 2s.
|
|
327
387
|
*/
|
|
328
|
-
_settleAfterHandler() {
|
|
388
|
+
_settleAfterHandler(settle) {
|
|
389
|
+
const quietMs = settle?.quietMs ?? _SSECollector.SETTLE_MS;
|
|
390
|
+
const maxMs = settle?.maxMs ?? _SSECollector.MAX_SETTLE_MS;
|
|
329
391
|
return new Promise((resolve2) => {
|
|
330
392
|
let resolved = false;
|
|
331
393
|
const done = () => {
|
|
@@ -337,12 +399,12 @@ var SSECollector = class _SSECollector {
|
|
|
337
399
|
this._cancelSettle = null;
|
|
338
400
|
resolve2();
|
|
339
401
|
};
|
|
340
|
-
const hardCap = setTimeout(done,
|
|
341
|
-
let settleTimer = setTimeout(done,
|
|
402
|
+
const hardCap = setTimeout(done, maxMs);
|
|
403
|
+
let settleTimer = setTimeout(done, quietMs);
|
|
342
404
|
const settleWaiter = () => {
|
|
343
405
|
if (resolved) return;
|
|
344
406
|
clearTimeout(settleTimer);
|
|
345
|
-
settleTimer = setTimeout(done,
|
|
407
|
+
settleTimer = setTimeout(done, quietMs);
|
|
346
408
|
};
|
|
347
409
|
this.waiters.push(settleWaiter);
|
|
348
410
|
this._cancelSettle = done;
|
|
@@ -507,6 +569,7 @@ ${value}` : value;
|
|
|
507
569
|
* Dispatch a parsed SSE event to the appropriate handler.
|
|
508
570
|
*/
|
|
509
571
|
_handleEvent(eventName, payload) {
|
|
572
|
+
this._recordEvent(eventName);
|
|
510
573
|
if (eventName === "snapshot") {
|
|
511
574
|
const data2 = payload;
|
|
512
575
|
const spans = Array.isArray(data2.spans) ? data2.spans : [];
|
|
@@ -526,6 +589,44 @@ ${value}` : value;
|
|
|
526
589
|
}
|
|
527
590
|
}
|
|
528
591
|
}
|
|
592
|
+
_recordEvent(eventName) {
|
|
593
|
+
this.lastEvent = { name: eventName, at: Date.now() };
|
|
594
|
+
if (eventName === "update") {
|
|
595
|
+
this.updateEventCount++;
|
|
596
|
+
} else if (eventName === "keepalive") {
|
|
597
|
+
this.keepaliveEventCount++;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
_formatTurnDiagnostics() {
|
|
601
|
+
const turnSpans = this.getTurnSpans();
|
|
602
|
+
const spanNames = /* @__PURE__ */ new Set();
|
|
603
|
+
const handlerStatuses = /* @__PURE__ */ new Set();
|
|
604
|
+
for (const span of turnSpans) {
|
|
605
|
+
spanNames.add(span.name);
|
|
606
|
+
if (isTurnHandlerSpan(span)) {
|
|
607
|
+
handlerStatuses.add(span.status);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const lastEvent = this.lastEvent ? `${this.lastEvent.name} ${Date.now() - this.lastEvent.at}ms ago` : "none";
|
|
611
|
+
return [
|
|
612
|
+
`snapshotReceived=${this.snapshotReceived}`,
|
|
613
|
+
`totalUpdates=${this.updateEventCount}`,
|
|
614
|
+
`totalKeepalives=${this.keepaliveEventCount}`,
|
|
615
|
+
`lastEvent=${lastEvent}`,
|
|
616
|
+
`totalSpans=${this.spanMap.size}`,
|
|
617
|
+
`turnSpans=${turnSpans.length}`,
|
|
618
|
+
`turnSpanNames=${this._formatSet(spanNames)}`,
|
|
619
|
+
`turnHandlerStatuses=${this._formatSet(handlerStatuses)}`
|
|
620
|
+
].join(", ");
|
|
621
|
+
}
|
|
622
|
+
_formatSet(values, limit = 12) {
|
|
623
|
+
if (values.size === 0) {
|
|
624
|
+
return "none";
|
|
625
|
+
}
|
|
626
|
+
const items = Array.from(values);
|
|
627
|
+
const suffix = items.length > limit ? `|+${items.length - limit} more` : "";
|
|
628
|
+
return `${items.slice(0, limit).join("|")}${suffix}`;
|
|
629
|
+
}
|
|
529
630
|
/**
|
|
530
631
|
* Insert or replace a span in the map (dedup by trace+span ID).
|
|
531
632
|
*/
|
|
@@ -551,7 +652,7 @@ ${value}` : value;
|
|
|
551
652
|
}
|
|
552
653
|
}
|
|
553
654
|
/**
|
|
554
|
-
* Check if there is a handler.conversation
|
|
655
|
+
* Check if there is a turn-handler span (handler.conversation / handler.event) that:
|
|
555
656
|
* 1. Was NOT seen before startTurn()
|
|
556
657
|
* 2. Has terminal status (ok or error)
|
|
557
658
|
*
|
|
@@ -560,9 +661,9 @@ ${value}` : value;
|
|
|
560
661
|
* waitForTurnComplete check (which would otherwise resolve instantly
|
|
561
662
|
* against stale data instead of waiting for the new turn's handler).
|
|
562
663
|
*/
|
|
563
|
-
_hasNewCompletedHandler() {
|
|
664
|
+
_hasNewCompletedHandler(acceptSpanNames) {
|
|
564
665
|
for (const [key, span] of this.spanMap) {
|
|
565
|
-
if (span
|
|
666
|
+
if (isTurnHandlerSpan(span) && (!acceptSpanNames || acceptSpanNames.has(span.name)) && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
|
|
566
667
|
this.seenHandlerKeys.add(key);
|
|
567
668
|
return true;
|
|
568
669
|
}
|
|
@@ -589,6 +690,38 @@ ${value}` : value;
|
|
|
589
690
|
}
|
|
590
691
|
};
|
|
591
692
|
|
|
693
|
+
// src/spans/local-span-source.ts
|
|
694
|
+
var LocalSpanSource = class {
|
|
695
|
+
collector;
|
|
696
|
+
constructor(devServerUrl, headers = {}) {
|
|
697
|
+
this.collector = new SSECollector(devServerUrl, headers);
|
|
698
|
+
}
|
|
699
|
+
async connect(filter) {
|
|
700
|
+
await this.collector.connect(filter);
|
|
701
|
+
}
|
|
702
|
+
async repoint(filter) {
|
|
703
|
+
await this.collector.repoint(filter);
|
|
704
|
+
}
|
|
705
|
+
startTurn() {
|
|
706
|
+
this.collector.startTurn();
|
|
707
|
+
}
|
|
708
|
+
async waitForTurnComplete(opts) {
|
|
709
|
+
await this.collector.waitForTurnComplete(opts);
|
|
710
|
+
}
|
|
711
|
+
async waitForWorkflow(name, opts) {
|
|
712
|
+
await this.collector.waitForWorkflow(name, opts);
|
|
713
|
+
}
|
|
714
|
+
getTurnSpans() {
|
|
715
|
+
return this.collector.getTurnSpans();
|
|
716
|
+
}
|
|
717
|
+
getAllSpans() {
|
|
718
|
+
return this.collector.getAllSpans();
|
|
719
|
+
}
|
|
720
|
+
disconnect() {
|
|
721
|
+
this.collector.disconnect();
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
592
725
|
// src/transformer.ts
|
|
593
726
|
function data(span) {
|
|
594
727
|
return span.data && typeof span.data === "object" ? span.data : {};
|
|
@@ -676,7 +809,7 @@ function extractWorkflowSpans(spans) {
|
|
|
676
809
|
}
|
|
677
810
|
function extractHandler(spans) {
|
|
678
811
|
const handler = spans.find(
|
|
679
|
-
(s) => (s.name === "handler.conversation" || s.name === "handler.workflow") && s.status !== "running"
|
|
812
|
+
(s) => (s.name === "handler.conversation" || s.name === "handler.event" || s.name === "handler.workflow") && s.status !== "running"
|
|
680
813
|
);
|
|
681
814
|
if (!handler) {
|
|
682
815
|
return { handlerDuration: 0, handlerStatus: "ok" };
|
|
@@ -688,55 +821,26 @@ function extractHandler(spans) {
|
|
|
688
821
|
}
|
|
689
822
|
|
|
690
823
|
// src/graders/llm.ts
|
|
691
|
-
import { Client } from "@botpress/client";
|
|
692
824
|
import { Cognitive } from "@botpress/cognitive";
|
|
693
|
-
|
|
694
|
-
function buildJudgeSystemPrompt(passThreshold) {
|
|
695
|
-
return `You are an evaluation judge for a chatbot. You will be given:
|
|
696
|
-
- The user's message
|
|
697
|
-
- The bot's response
|
|
698
|
-
- Grading criteria
|
|
699
|
-
|
|
700
|
-
Your job is to determine if the bot's response meets the criteria.
|
|
701
|
-
|
|
702
|
-
Respond with a JSON object:
|
|
703
|
-
{
|
|
704
|
-
"pass": true/false,
|
|
705
|
-
"score": 1-5,
|
|
706
|
-
"reason": "brief explanation"
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
Scoring guide:
|
|
710
|
-
- 5: Fully meets criteria, excellent response
|
|
711
|
-
- 4: Mostly meets criteria, minor issues
|
|
712
|
-
- 3: Partially meets criteria, notable gaps
|
|
713
|
-
- 2: Barely meets criteria, significant issues
|
|
714
|
-
- 1: Does not meet criteria
|
|
715
|
-
|
|
716
|
-
A score of ${passThreshold} or above is a pass.`;
|
|
717
|
-
}
|
|
825
|
+
import { Zai } from "@botpress/zai";
|
|
718
826
|
var MODEL_ALIASES = ["fast", "best"];
|
|
719
|
-
var
|
|
827
|
+
var _zai = null;
|
|
720
828
|
var _availableModels = [];
|
|
721
829
|
var _judgeModel = "fast";
|
|
830
|
+
var _failClosed = true;
|
|
722
831
|
var _log = defaultLogger;
|
|
723
832
|
function _isModelValid(model) {
|
|
724
833
|
if (MODEL_ALIASES.includes(model)) return true;
|
|
725
834
|
if (_availableModels.length === 0) return false;
|
|
726
835
|
return _availableModels.includes(model);
|
|
727
836
|
}
|
|
728
|
-
async function initLLMJudge(
|
|
837
|
+
async function initLLMJudge(client, options) {
|
|
729
838
|
const log = options?.logger ?? defaultLogger;
|
|
730
839
|
_log = log;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
apiUrl: credentials.apiUrl,
|
|
734
|
-
botId: credentials.botId,
|
|
735
|
-
...credentials.workspaceId !== void 0 ? { workspaceId: credentials.workspaceId } : {}
|
|
736
|
-
});
|
|
737
|
-
_cognitive = new Cognitive({ client, __experimental_beta: true });
|
|
840
|
+
_failClosed = options?.failClosed ?? true;
|
|
841
|
+
const cognitive = new Cognitive({ client, __experimental_beta: true });
|
|
738
842
|
try {
|
|
739
|
-
_availableModels = Array.from((await
|
|
843
|
+
_availableModels = Array.from((await cognitive.fetchRemoteModels()).keys());
|
|
740
844
|
} catch (err) {
|
|
741
845
|
log.warn(`Failed to fetch available models: ${err.message}. LLM judge will be unavailable.`);
|
|
742
846
|
_availableModels = [];
|
|
@@ -747,66 +851,42 @@ async function initLLMJudge(credentials, options) {
|
|
|
747
851
|
} else if (!_isModelValid(_judgeModel)) {
|
|
748
852
|
log.warn(`Configured LLM judge model "${_judgeModel}" is invalid. Run "adk models" to list available models.`);
|
|
749
853
|
}
|
|
854
|
+
_zai = new Zai({ client: cognitive, modelId: _judgeModel });
|
|
750
855
|
}
|
|
751
856
|
async function gradeLLMJudge(botResponse, criteria, context) {
|
|
752
|
-
const
|
|
857
|
+
const assertion = `llm_judge: "${criteria}"`;
|
|
753
858
|
try {
|
|
754
|
-
if (!
|
|
859
|
+
if (!_zai) {
|
|
755
860
|
_log.warn(`LLM judge unavailable for "${criteria}": no credentials configured (initLLMJudge not called)`);
|
|
756
861
|
return {
|
|
757
|
-
assertion
|
|
758
|
-
pass:
|
|
862
|
+
assertion,
|
|
863
|
+
pass: !_failClosed,
|
|
864
|
+
skipped: true,
|
|
759
865
|
expected: criteria,
|
|
760
866
|
actual: "FAILED \u2014 LLM judge unavailable: no credentials configured"
|
|
761
867
|
};
|
|
762
868
|
}
|
|
763
869
|
if (!_isModelValid(_judgeModel)) {
|
|
764
870
|
return {
|
|
765
|
-
assertion
|
|
871
|
+
assertion,
|
|
766
872
|
pass: false,
|
|
767
873
|
expected: criteria,
|
|
768
874
|
actual: "FAILED \u2014 invalid LLM judge model"
|
|
769
875
|
};
|
|
770
876
|
}
|
|
771
|
-
const { output } = await
|
|
772
|
-
model: _judgeModel,
|
|
773
|
-
temperature: 0,
|
|
774
|
-
responseFormat: "json_object",
|
|
775
|
-
systemPrompt: buildJudgeSystemPrompt(threshold),
|
|
776
|
-
messages: [
|
|
777
|
-
{
|
|
778
|
-
role: "user",
|
|
779
|
-
content: `User message: ${context.userMessage}
|
|
780
|
-
|
|
781
|
-
Bot response: ${botResponse}
|
|
782
|
-
|
|
783
|
-
Criteria: ${criteria}`
|
|
784
|
-
}
|
|
785
|
-
]
|
|
786
|
-
});
|
|
787
|
-
const rawContent = output?.choices?.[0]?.content;
|
|
788
|
-
const content = typeof rawContent === "string" ? rawContent : null;
|
|
789
|
-
if (!content) {
|
|
790
|
-
_log.warn(`LLM judge returned an empty response for "${criteria}"`);
|
|
791
|
-
return {
|
|
792
|
-
assertion: `llm_judge: "${criteria}"`,
|
|
793
|
-
pass: false,
|
|
794
|
-
expected: criteria,
|
|
795
|
-
actual: "FAILED \u2014 LLM judge returned empty response"
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
const verdict = JSON.parse(content);
|
|
877
|
+
const { output } = await _zai.check({ userMessage: context.userMessage, botResponse }, criteria).result();
|
|
799
878
|
return {
|
|
800
|
-
assertion
|
|
801
|
-
pass:
|
|
879
|
+
assertion,
|
|
880
|
+
pass: output.value,
|
|
802
881
|
expected: criteria,
|
|
803
|
-
actual:
|
|
882
|
+
actual: `${output.value ? "Pass" : "Fail"} \u2014 ${output.explanation}`
|
|
804
883
|
};
|
|
805
884
|
} catch (err) {
|
|
806
885
|
_log.warn(`LLM judge failed for "${criteria}": ${err.message}`);
|
|
807
886
|
return {
|
|
808
|
-
assertion
|
|
809
|
-
pass:
|
|
887
|
+
assertion,
|
|
888
|
+
pass: !_failClosed,
|
|
889
|
+
skipped: true,
|
|
810
890
|
expected: criteria,
|
|
811
891
|
actual: `FAILED \u2014 LLM judge unavailable: ${err.message}`
|
|
812
892
|
};
|
|
@@ -1229,6 +1309,7 @@ function filterEvals(evals, filter) {
|
|
|
1229
1309
|
}
|
|
1230
1310
|
|
|
1231
1311
|
// src/runner.ts
|
|
1312
|
+
import { isAdkError } from "@botpress/analytics";
|
|
1232
1313
|
import { randomUUID } from "crypto";
|
|
1233
1314
|
var DEFAULT_IDLE_TIMEOUT = 3e4;
|
|
1234
1315
|
var DEFAULT_DEV_SERVER_URL = "http://localhost:3001";
|
|
@@ -1315,27 +1396,22 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1315
1396
|
const start = Date.now();
|
|
1316
1397
|
const turns = [];
|
|
1317
1398
|
let outcomeAssertions = [];
|
|
1318
|
-
const collector = new
|
|
1399
|
+
const collector = options.spanSource ?? new LocalSpanSource(devServerUrl, devServerHeaders);
|
|
1319
1400
|
try {
|
|
1320
|
-
const session = new ChatSession(
|
|
1401
|
+
const session = new ChatSession(
|
|
1402
|
+
connection.client,
|
|
1403
|
+
connection.botId,
|
|
1404
|
+
options.chatWebhookId,
|
|
1405
|
+
options.chatBaseUrl,
|
|
1406
|
+
options.chatClient
|
|
1407
|
+
);
|
|
1321
1408
|
await session.connect();
|
|
1322
|
-
let bpClient = null;
|
|
1323
|
-
const getBpClient = () => {
|
|
1324
|
-
if (!bpClient) {
|
|
1325
|
-
bpClient = new BpClient2({
|
|
1326
|
-
token: connection.token,
|
|
1327
|
-
botId: connection.botId,
|
|
1328
|
-
apiUrl: connection.apiUrl
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
return bpClient;
|
|
1332
|
-
};
|
|
1333
1409
|
let conversationId = "";
|
|
1334
1410
|
if (evalDef.setup?.state?.conversation || evalDef.setup?.workflow) {
|
|
1335
1411
|
conversationId = await session.ensureConversation();
|
|
1336
1412
|
}
|
|
1337
1413
|
await seedEvalState(
|
|
1338
|
-
|
|
1414
|
+
connection.client,
|
|
1339
1415
|
{
|
|
1340
1416
|
botId: connection.botId,
|
|
1341
1417
|
userId: session.userId,
|
|
@@ -1344,7 +1420,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1344
1420
|
evalDef.setup
|
|
1345
1421
|
);
|
|
1346
1422
|
await triggerEvalWorkflow(
|
|
1347
|
-
|
|
1423
|
+
connection.client,
|
|
1348
1424
|
{
|
|
1349
1425
|
userId: session.userId,
|
|
1350
1426
|
...conversationId ? { conversationId } : {}
|
|
@@ -1380,24 +1456,34 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1380
1456
|
if (!turn.user && !turn.event) {
|
|
1381
1457
|
throw turnConfigError(`must have either 'user' or 'event'.`);
|
|
1382
1458
|
}
|
|
1383
|
-
const
|
|
1384
|
-
options.onProgress?.({
|
|
1459
|
+
const turnLabel = turn.event ? "[event]" : turn.user;
|
|
1460
|
+
await options.onProgress?.({
|
|
1385
1461
|
type: "turn_start",
|
|
1386
1462
|
evalName: evalDef.name,
|
|
1387
1463
|
evalIndex: options.evalIndex ?? 0,
|
|
1388
|
-
|
|
1464
|
+
turnIndex: i,
|
|
1389
1465
|
totalTurns: evalDef.conversation.length,
|
|
1390
|
-
userMessage:
|
|
1466
|
+
userMessage: turnLabel
|
|
1391
1467
|
});
|
|
1468
|
+
if (turn.newConversation && i > 0) {
|
|
1469
|
+
const newConversationId = await session.newConversation();
|
|
1470
|
+
await collector.repoint({ conversationId: newConversationId });
|
|
1471
|
+
}
|
|
1392
1472
|
collector.startTurn();
|
|
1393
1473
|
if (turn.event) {
|
|
1394
|
-
await session.sendEvent(turn.event.
|
|
1474
|
+
await session.sendEvent(turn.event.payload);
|
|
1395
1475
|
} else {
|
|
1396
1476
|
await session.sendMessage(turn.user);
|
|
1397
1477
|
}
|
|
1398
1478
|
try {
|
|
1399
1479
|
await collector.waitForTurnComplete({
|
|
1400
1480
|
timeout: idleTimeout,
|
|
1481
|
+
acceptSpanNames: turn.event ? /* @__PURE__ */ new Set(["handler.event"]) : /* @__PURE__ */ new Set(["handler.conversation"]),
|
|
1482
|
+
// Long quiet window: a tool-started workflow and its callback only run
|
|
1483
|
+
// AFTER the handler closes; sending the next message into that window
|
|
1484
|
+
// can get it absorbed by their transcript saves and silently skipped.
|
|
1485
|
+
settleQuietMs: 1500,
|
|
1486
|
+
settleMaxMs: 15e3,
|
|
1401
1487
|
...options.signal !== void 0 ? { abortSignal: options.signal } : {}
|
|
1402
1488
|
});
|
|
1403
1489
|
} catch (err) {
|
|
@@ -1421,7 +1507,6 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1421
1507
|
const turnData = transformSpans(turnSpans);
|
|
1422
1508
|
const botResponse = turnData.messages.join("\n");
|
|
1423
1509
|
const botDuration = turnData.handlerDuration;
|
|
1424
|
-
const turnLabel = turn.event ? `[event: ${turn.event.type}]` : turn.user;
|
|
1425
1510
|
const evalStart = Date.now();
|
|
1426
1511
|
let assertions = [];
|
|
1427
1512
|
const noResponse = !turn.expectSilence && turnData.messages.length === 0;
|
|
@@ -1476,7 +1561,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1476
1561
|
const turnPass = assertions.every((a) => a.pass);
|
|
1477
1562
|
const evalDuration = Date.now() - evalStart;
|
|
1478
1563
|
const turnReport = {
|
|
1479
|
-
|
|
1564
|
+
turnIndex: i,
|
|
1480
1565
|
userMessage: turnLabel,
|
|
1481
1566
|
botResponse,
|
|
1482
1567
|
assertions,
|
|
@@ -1485,11 +1570,11 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1485
1570
|
evalDuration
|
|
1486
1571
|
};
|
|
1487
1572
|
turns.push(turnReport);
|
|
1488
|
-
options.onProgress?.({
|
|
1573
|
+
await options.onProgress?.({
|
|
1489
1574
|
type: "turn_complete",
|
|
1490
1575
|
evalName: evalDef.name,
|
|
1491
1576
|
evalIndex: options.evalIndex ?? 0,
|
|
1492
|
-
|
|
1577
|
+
turnIndex: i,
|
|
1493
1578
|
totalTurns: evalDef.conversation.length,
|
|
1494
1579
|
turnReport
|
|
1495
1580
|
});
|
|
@@ -1536,7 +1621,15 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1536
1621
|
} catch (err) {
|
|
1537
1622
|
const adkErr = isAdkError(err) ? err : void 0;
|
|
1538
1623
|
if (!adkErr || !adkErr.expected) {
|
|
1539
|
-
|
|
1624
|
+
const errMsg = err.message ?? String(err);
|
|
1625
|
+
const errStack = err.stack ?? "";
|
|
1626
|
+
logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${errMsg}`);
|
|
1627
|
+
logger.error(` stack: ${errStack}`);
|
|
1628
|
+
logger.error(` eval: ${evalDef.name}, turn: ${turns.length}/${evalDef.conversation.length}`);
|
|
1629
|
+
logger.error(` connection: botId=${connection.botId}`);
|
|
1630
|
+
logger.error(
|
|
1631
|
+
` spanSource: ${options.spanSource?.constructor?.name ?? "default"}, devServerUrl: ${options.devServerUrl ?? "n/a"}`
|
|
1632
|
+
);
|
|
1540
1633
|
options.onException?.(err, { source: "evals", eval_name: evalDef.name });
|
|
1541
1634
|
}
|
|
1542
1635
|
return {
|
|
@@ -1554,9 +1647,15 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1554
1647
|
}
|
|
1555
1648
|
async function runEvalSuite(config, filter) {
|
|
1556
1649
|
const start = Date.now();
|
|
1557
|
-
const runId = randomUUID().replace(/-/g, "").slice(0, 26);
|
|
1558
|
-
|
|
1559
|
-
|
|
1650
|
+
const runId = config.runId ?? randomUUID().replace(/-/g, "").slice(0, 26);
|
|
1651
|
+
let allEvals;
|
|
1652
|
+
if (config.definitions) {
|
|
1653
|
+
allEvals = config.definitions;
|
|
1654
|
+
} else if (config.agentPath) {
|
|
1655
|
+
allEvals = await loadEvalsFromDir(`${config.agentPath}/evals`);
|
|
1656
|
+
} else {
|
|
1657
|
+
throw new Error("Either `definitions` or `agentPath` must be provided in EvalRunnerConfig");
|
|
1658
|
+
}
|
|
1560
1659
|
const evals = filterEvals(allEvals, filter);
|
|
1561
1660
|
if (evals.length === 0) {
|
|
1562
1661
|
return {
|
|
@@ -1570,37 +1669,26 @@ async function runEvalSuite(config, filter) {
|
|
|
1570
1669
|
...filter !== void 0 ? { filter } : {}
|
|
1571
1670
|
};
|
|
1572
1671
|
}
|
|
1573
|
-
await initLLMJudge(
|
|
1574
|
-
{
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
botId: config.credentials.botId,
|
|
1578
|
-
...config.credentials.workspaceId !== void 0 ? { workspaceId: config.credentials.workspaceId } : {}
|
|
1579
|
-
},
|
|
1580
|
-
{
|
|
1581
|
-
...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
|
|
1582
|
-
...config.logger !== void 0 ? { logger: config.logger } : {}
|
|
1583
|
-
}
|
|
1584
|
-
);
|
|
1585
|
-
let webhookId = config.credentials.webhookId;
|
|
1586
|
-
if (!webhookId) {
|
|
1587
|
-
webhookId = await discoverWebhookId(config.credentials.botId, config.credentials.token, config.credentials.apiUrl);
|
|
1588
|
-
}
|
|
1672
|
+
await initLLMJudge(config.client, {
|
|
1673
|
+
...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
|
|
1674
|
+
...config.logger !== void 0 ? { logger: config.logger } : {}
|
|
1675
|
+
});
|
|
1589
1676
|
const connection = {
|
|
1590
|
-
|
|
1591
|
-
botId: config.
|
|
1592
|
-
token: config.credentials.token,
|
|
1593
|
-
apiUrl: config.credentials.apiUrl
|
|
1677
|
+
client: config.client,
|
|
1678
|
+
botId: config.botId
|
|
1594
1679
|
};
|
|
1595
1680
|
const devServerUrl = config.devServerUrl || DEFAULT_DEV_SERVER_URL;
|
|
1596
1681
|
const devServerHeaders = config.devServerHeaders ?? {};
|
|
1597
|
-
|
|
1682
|
+
if (!config.createSpanSource) {
|
|
1683
|
+
await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
|
|
1684
|
+
await assertTraceStreamReadable(devServerUrl, devServerHeaders);
|
|
1685
|
+
}
|
|
1598
1686
|
const reports = [];
|
|
1599
|
-
config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
|
|
1687
|
+
await config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
|
|
1600
1688
|
for (let i = 0; i < evals.length; i++) {
|
|
1601
1689
|
if (config.signal?.aborted) break;
|
|
1602
1690
|
const evalDef = evals[i];
|
|
1603
|
-
config.onProgress?.({
|
|
1691
|
+
await config.onProgress?.({
|
|
1604
1692
|
type: "eval_start",
|
|
1605
1693
|
evalName: evalDef.name,
|
|
1606
1694
|
index: i,
|
|
@@ -1616,10 +1704,13 @@ async function runEvalSuite(config, filter) {
|
|
|
1616
1704
|
...config.signal !== void 0 ? { signal: config.signal } : {},
|
|
1617
1705
|
...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {},
|
|
1618
1706
|
...config.logger !== void 0 ? { logger: config.logger } : {},
|
|
1619
|
-
...config.onException !== void 0 ? { onException: config.onException } : {}
|
|
1707
|
+
...config.onException !== void 0 ? { onException: config.onException } : {},
|
|
1708
|
+
...config.createSpanSource !== void 0 ? { spanSource: config.createSpanSource() } : {},
|
|
1709
|
+
...config.chatWebhookId !== void 0 ? { chatWebhookId: config.chatWebhookId } : {},
|
|
1710
|
+
...config.chatBaseUrl !== void 0 ? { chatBaseUrl: config.chatBaseUrl } : {}
|
|
1620
1711
|
});
|
|
1621
1712
|
reports.push(report);
|
|
1622
|
-
config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
|
|
1713
|
+
await config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
|
|
1623
1714
|
}
|
|
1624
1715
|
const runReport = {
|
|
1625
1716
|
id: runId,
|
|
@@ -1634,7 +1725,7 @@ async function runEvalSuite(config, filter) {
|
|
|
1634
1725
|
if (config.signal?.aborted) {
|
|
1635
1726
|
runReport.aborted = true;
|
|
1636
1727
|
}
|
|
1637
|
-
config.onProgress?.({ type: "suite_complete", report: runReport });
|
|
1728
|
+
await config.onProgress?.({ type: "suite_complete", report: runReport });
|
|
1638
1729
|
return runReport;
|
|
1639
1730
|
}
|
|
1640
1731
|
export {
|