@botpress/evals 1.19.0-beta.7 → 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 +25 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +143 -55
- 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/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graders/index.js +31 -74
- package/dist/graders/index.js.map +3 -3
- package/dist/graders/llm.d.ts +14 -12
- package/dist/graders/llm.d.ts.map +1 -1
- package/dist/graders/llm.js +46 -74
- package/dist/graders/llm.js.map +3 -3
- package/dist/graders/response.js +24 -64
- package/dist/graders/response.js.map +3 -3
- 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.d.ts.map +1 -1
- package/dist/loader.js +22 -8
- 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 +7 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +445 -246
- 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} +109 -22
- 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 +55 -33
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -1
- package/dist/types.js.map +3 -3
- 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,96 +1,204 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
1
|
+
// src/definition.ts
|
|
2
|
+
var Eval = class {
|
|
3
|
+
name;
|
|
4
|
+
description;
|
|
5
|
+
tags;
|
|
6
|
+
type;
|
|
7
|
+
setup;
|
|
8
|
+
conversation;
|
|
9
|
+
outcome;
|
|
10
|
+
options;
|
|
11
|
+
constructor(def) {
|
|
12
|
+
this.name = def.name;
|
|
13
|
+
this.conversation = def.conversation;
|
|
14
|
+
if (def.description !== void 0) this.description = def.description;
|
|
15
|
+
if (def.tags !== void 0) this.tags = def.tags;
|
|
16
|
+
if (def.type !== void 0) this.type = def.type;
|
|
17
|
+
if (def.setup !== void 0) this.setup = def.setup;
|
|
18
|
+
if (def.outcome !== void 0) this.outcome = def.outcome;
|
|
19
|
+
if (def.options !== void 0) this.options = def.options;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/types.ts
|
|
24
|
+
var defaultLogger = console;
|
|
25
|
+
|
|
26
|
+
// src/errors.ts
|
|
27
|
+
import { AdkError } from "@botpress/analytics";
|
|
28
|
+
var EvalRunnerError = class extends AdkError {
|
|
29
|
+
};
|
|
3
30
|
|
|
4
31
|
// src/client.ts
|
|
5
|
-
|
|
6
|
-
import { createRequire } from "module";
|
|
7
|
-
var defaultChatClient = null;
|
|
8
|
-
function getDefaultChatClient() {
|
|
9
|
-
if (defaultChatClient) return defaultChatClient;
|
|
10
|
-
const require2 = createRequire(import.meta.url);
|
|
11
|
-
const mod = require2("@botpress/chat");
|
|
12
|
-
const ChatClient = mod.default?.Client ?? mod.Client;
|
|
13
|
-
if (!ChatClient) {
|
|
14
|
-
throw new Error("@botpress/chat did not expose a Client export. Pass EvalRunnerConfig.chatClient explicitly.");
|
|
15
|
-
}
|
|
16
|
-
return ChatClient;
|
|
17
|
-
}
|
|
32
|
+
var TRACE_STREAM_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
18
33
|
var ChatSession = class {
|
|
19
|
-
constructor(
|
|
20
|
-
this.
|
|
21
|
-
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;
|
|
22
40
|
}
|
|
23
41
|
client = null;
|
|
24
42
|
conversationId = null;
|
|
25
43
|
async connect() {
|
|
26
|
-
const
|
|
27
|
-
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
|
+
});
|
|
28
53
|
}
|
|
29
|
-
|
|
54
|
+
/** Invariant guard — calling any session method before connect() is a runner bug. */
|
|
55
|
+
assertConnected() {
|
|
30
56
|
if (!this.client) {
|
|
31
|
-
throw new
|
|
57
|
+
throw new EvalRunnerError({
|
|
58
|
+
code: "CHAT_NOT_CONNECTED",
|
|
59
|
+
message: "ChatSession not connected. Call connect() first."
|
|
60
|
+
});
|
|
32
61
|
}
|
|
33
|
-
return this.client
|
|
62
|
+
return this.client;
|
|
63
|
+
}
|
|
64
|
+
get userId() {
|
|
65
|
+
return this.assertConnected().user.id;
|
|
34
66
|
}
|
|
35
67
|
async ensureConversation() {
|
|
36
|
-
|
|
37
|
-
throw new Error("ChatSession not connected. Call connect() first.");
|
|
38
|
-
}
|
|
68
|
+
const client = this.assertConnected();
|
|
39
69
|
if (!this.conversationId) {
|
|
40
|
-
const conv = await
|
|
70
|
+
const conv = await client.createConversation({});
|
|
41
71
|
this.conversationId = conv.conversation.id;
|
|
42
72
|
return conv.conversation.id;
|
|
43
73
|
}
|
|
44
74
|
return this.conversationId;
|
|
45
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
|
+
}
|
|
46
86
|
/**
|
|
47
87
|
* Send a user message. Fire-and-forget — observation is via SSECollector.
|
|
48
88
|
*/
|
|
49
89
|
async sendMessage(message) {
|
|
50
|
-
|
|
51
|
-
throw new Error("ChatSession not connected. Call connect() first.");
|
|
52
|
-
}
|
|
90
|
+
const client = this.assertConnected();
|
|
53
91
|
const conversationId = await this.ensureConversation();
|
|
54
|
-
await
|
|
92
|
+
await client.createMessage({ conversationId, payload: { type: "text", text: message } });
|
|
55
93
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
async sendEvent(type, payload) {
|
|
60
|
-
if (!this.client) {
|
|
61
|
-
throw new Error("ChatSession not connected. Call connect() first.");
|
|
62
|
-
}
|
|
94
|
+
async sendEvent(payload) {
|
|
95
|
+
const client = this.assertConnected();
|
|
63
96
|
const conversationId = await this.ensureConversation();
|
|
64
|
-
await
|
|
97
|
+
await client.createEvent({ payload, conversationId });
|
|
65
98
|
}
|
|
66
99
|
};
|
|
67
|
-
async function discoverWebhookId(
|
|
68
|
-
const client = new BpClient({ token, botId, apiUrl });
|
|
100
|
+
async function discoverWebhookId(client, botId) {
|
|
69
101
|
const { bot } = await client.getBot({ id: botId });
|
|
70
102
|
const integrations = bot.integrations || {};
|
|
71
103
|
const chat = Object.values(integrations).find((int) => int.name === "chat");
|
|
72
104
|
const webhookId = chat?.webhookId;
|
|
73
105
|
if (!webhookId || typeof webhookId !== "string") {
|
|
74
|
-
throw new
|
|
75
|
-
|
|
106
|
+
throw new EvalRunnerError({
|
|
107
|
+
code: "CHAT_INTEGRATION_MISSING",
|
|
108
|
+
message: [
|
|
76
109
|
"The `chat` integration is not installed on this bot \u2014 evals require it.",
|
|
77
110
|
"",
|
|
78
111
|
"To fix:",
|
|
79
|
-
" 1. Install it: adk add chat",
|
|
112
|
+
" 1. Install it: adk integrations add chat",
|
|
80
113
|
" 2. Redeploy: adk dev (or: adk deploy)"
|
|
81
|
-
].join("\n")
|
|
82
|
-
|
|
114
|
+
].join("\n"),
|
|
115
|
+
expected: true
|
|
116
|
+
});
|
|
83
117
|
}
|
|
84
118
|
return webhookId;
|
|
85
119
|
}
|
|
86
|
-
async function
|
|
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
|
+
}
|
|
191
|
+
async function assertChatChannelBound(devServerUrl, headers = {}, logger = defaultLogger) {
|
|
87
192
|
let conversations = [];
|
|
88
193
|
try {
|
|
89
194
|
const res = await fetch(`${devServerUrl}/api/agent`, { headers });
|
|
90
195
|
if (!res.ok) return;
|
|
91
196
|
const agent = await res.json();
|
|
92
197
|
conversations = agent.conversations ?? [];
|
|
93
|
-
} catch {
|
|
198
|
+
} catch (err) {
|
|
199
|
+
logger?.warn(
|
|
200
|
+
`Skipping chat.channel binding pre-flight \u2014 could not query ${devServerUrl}/api/agent: ${err instanceof Error ? err.message : String(err)}`
|
|
201
|
+
);
|
|
94
202
|
return;
|
|
95
203
|
}
|
|
96
204
|
if (conversations.length === 0) return;
|
|
@@ -101,8 +209,9 @@ async function assertChatChannelBound(devServerUrl, headers = {}) {
|
|
|
101
209
|
};
|
|
102
210
|
const bound = conversations.some((c) => matchesChatChannel(c.channel));
|
|
103
211
|
if (bound) return;
|
|
104
|
-
throw new
|
|
105
|
-
|
|
212
|
+
throw new EvalRunnerError({
|
|
213
|
+
code: "CHAT_CHANNEL_UNBOUND",
|
|
214
|
+
message: [
|
|
106
215
|
"No Conversation is bound to `chat.channel` \u2014 messages via `@botpress/chat` will time out silently.",
|
|
107
216
|
"",
|
|
108
217
|
"To fix, bind a Conversation in `src/conversations/` to `chat.channel` (or `*`):",
|
|
@@ -111,17 +220,22 @@ async function assertChatChannelBound(devServerUrl, headers = {}) {
|
|
|
111
220
|
" channel: ['chat.channel', ...], // array form",
|
|
112
221
|
"",
|
|
113
222
|
"The `chat` integration is how evals and `adk chat --single` send messages programmatically."
|
|
114
|
-
].join("\n")
|
|
115
|
-
|
|
223
|
+
].join("\n"),
|
|
224
|
+
expected: true
|
|
225
|
+
});
|
|
116
226
|
}
|
|
117
227
|
|
|
118
|
-
// src/sse-collector.ts
|
|
228
|
+
// src/spans/sse-collector.ts
|
|
119
229
|
function spanKey(span) {
|
|
120
230
|
return `${span.id.trace}:${span.id.span}`;
|
|
121
231
|
}
|
|
122
232
|
function spanData(span) {
|
|
123
233
|
return span.data && typeof span.data === "object" ? span.data : {};
|
|
124
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
|
+
}
|
|
125
239
|
var SSECollector = class _SSECollector {
|
|
126
240
|
devServerUrl;
|
|
127
241
|
headers;
|
|
@@ -139,6 +253,10 @@ var SSECollector = class _SSECollector {
|
|
|
139
253
|
connected = false;
|
|
140
254
|
/** Error from the SSE stream if it died unexpectedly. */
|
|
141
255
|
streamError = null;
|
|
256
|
+
/** Passive counters used only to explain timeout failures. */
|
|
257
|
+
updateEventCount = 0;
|
|
258
|
+
keepaliveEventCount = 0;
|
|
259
|
+
lastEvent = null;
|
|
142
260
|
/** Set while a settle phase is in flight; called by disconnect() to short-circuit. */
|
|
143
261
|
_cancelSettle = null;
|
|
144
262
|
constructor(devServerUrl, headers = {}) {
|
|
@@ -154,40 +272,66 @@ var SSECollector = class _SSECollector {
|
|
|
154
272
|
const attributeValue = "conversationId" in filter ? filter.conversationId : filter.workflowId;
|
|
155
273
|
const url = `${this.devServerUrl}/api/traces/stream?attributeName=${encodeURIComponent(attributeName)}&attributeValue=${encodeURIComponent(attributeValue)}`;
|
|
156
274
|
this.abortController = new AbortController();
|
|
275
|
+
const myController = this.abortController;
|
|
157
276
|
const response = await fetch(url, {
|
|
158
277
|
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
159
|
-
signal:
|
|
278
|
+
signal: myController.signal
|
|
160
279
|
});
|
|
161
280
|
if (!response.ok) {
|
|
162
|
-
throw new
|
|
281
|
+
throw new EvalRunnerError({
|
|
282
|
+
code: "SSE_CONNECT_FAILED",
|
|
283
|
+
message: `SSE connection failed: ${response.status} ${response.statusText}`,
|
|
284
|
+
expected: true,
|
|
285
|
+
suggestion: "Make sure the dev server is running (`adk dev`) and reachable."
|
|
286
|
+
});
|
|
163
287
|
}
|
|
164
288
|
if (!response.body) {
|
|
165
|
-
throw new
|
|
289
|
+
throw new EvalRunnerError({ code: "SSE_NO_BODY", message: "SSE response has no body" });
|
|
166
290
|
}
|
|
167
291
|
this.connected = true;
|
|
168
292
|
this._consumeStream(response.body).catch((err) => {
|
|
169
|
-
if (this.connected) {
|
|
293
|
+
if (this.connected && this.abortController === myController) {
|
|
170
294
|
this.streamError = err instanceof Error ? err : new Error(String(err));
|
|
171
295
|
}
|
|
172
296
|
});
|
|
173
297
|
await this._waitForSnapshot();
|
|
174
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
|
+
}
|
|
175
317
|
/**
|
|
176
318
|
* Mark the beginning of a new turn.
|
|
177
|
-
* Records which handler
|
|
178
|
-
* 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.
|
|
179
322
|
*/
|
|
180
323
|
startTurn() {
|
|
181
324
|
this.turnStartKeys = new Set(this.spanMap.keys());
|
|
182
325
|
for (const [key, span] of this.spanMap) {
|
|
183
|
-
if (span
|
|
326
|
+
if (isTurnHandlerSpan(span)) {
|
|
184
327
|
this.seenHandlerKeys.add(key);
|
|
185
328
|
}
|
|
186
329
|
}
|
|
187
330
|
}
|
|
188
331
|
static SETTLE_MS = 100;
|
|
189
332
|
/**
|
|
190
|
-
* Wait for a handler
|
|
333
|
+
* Wait for a turn-handler span (handler.conversation or handler.event) to
|
|
334
|
+
* reach terminal status (ok or error).
|
|
191
335
|
* Only considers spans NOT seen before the last startTurn() call.
|
|
192
336
|
*
|
|
193
337
|
* After the handler closes, waits for a quiet period (no new spans for SETTLE_MS)
|
|
@@ -196,8 +340,10 @@ var SSECollector = class _SSECollector {
|
|
|
196
340
|
* spans may arrive slightly after the handler close event.
|
|
197
341
|
*/
|
|
198
342
|
async waitForTurnComplete(opts) {
|
|
199
|
-
|
|
200
|
-
|
|
343
|
+
const accept = opts.acceptSpanNames;
|
|
344
|
+
const settle = { quietMs: opts.settleQuietMs, maxMs: opts.settleMaxMs };
|
|
345
|
+
if (this._hasNewCompletedHandler(accept)) {
|
|
346
|
+
return this._settleAfterHandler(settle);
|
|
201
347
|
}
|
|
202
348
|
if (opts.abortSignal?.aborted) {
|
|
203
349
|
throw new Error("waitForTurnComplete aborted");
|
|
@@ -206,8 +352,13 @@ var SSECollector = class _SSECollector {
|
|
|
206
352
|
const timer = setTimeout(() => {
|
|
207
353
|
this._removeWaiter(waiter);
|
|
208
354
|
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
209
|
-
const
|
|
210
|
-
|
|
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
|
+
);
|
|
211
362
|
}, opts.timeout);
|
|
212
363
|
const onAbort = () => {
|
|
213
364
|
clearTimeout(timer);
|
|
@@ -216,11 +367,11 @@ var SSECollector = class _SSECollector {
|
|
|
216
367
|
};
|
|
217
368
|
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
218
369
|
const waiter = (_span) => {
|
|
219
|
-
if (this._hasNewCompletedHandler()) {
|
|
370
|
+
if (this._hasNewCompletedHandler(accept)) {
|
|
220
371
|
clearTimeout(timer);
|
|
221
372
|
this._removeWaiter(waiter);
|
|
222
373
|
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
223
|
-
this._settleAfterHandler().then(resolve2, reject);
|
|
374
|
+
this._settleAfterHandler(settle).then(resolve2, reject);
|
|
224
375
|
}
|
|
225
376
|
};
|
|
226
377
|
this.waiters.push(waiter);
|
|
@@ -234,7 +385,9 @@ var SSECollector = class _SSECollector {
|
|
|
234
385
|
* Exposes a cancellation hook via _cancelSettle so disconnect() can short-
|
|
235
386
|
* circuit an in-flight settle phase and avoid leaking timers for up to 2s.
|
|
236
387
|
*/
|
|
237
|
-
_settleAfterHandler() {
|
|
388
|
+
_settleAfterHandler(settle) {
|
|
389
|
+
const quietMs = settle?.quietMs ?? _SSECollector.SETTLE_MS;
|
|
390
|
+
const maxMs = settle?.maxMs ?? _SSECollector.MAX_SETTLE_MS;
|
|
238
391
|
return new Promise((resolve2) => {
|
|
239
392
|
let resolved = false;
|
|
240
393
|
const done = () => {
|
|
@@ -246,12 +399,12 @@ var SSECollector = class _SSECollector {
|
|
|
246
399
|
this._cancelSettle = null;
|
|
247
400
|
resolve2();
|
|
248
401
|
};
|
|
249
|
-
const hardCap = setTimeout(done,
|
|
250
|
-
let settleTimer = setTimeout(done,
|
|
402
|
+
const hardCap = setTimeout(done, maxMs);
|
|
403
|
+
let settleTimer = setTimeout(done, quietMs);
|
|
251
404
|
const settleWaiter = () => {
|
|
252
405
|
if (resolved) return;
|
|
253
406
|
clearTimeout(settleTimer);
|
|
254
|
-
settleTimer = setTimeout(done,
|
|
407
|
+
settleTimer = setTimeout(done, quietMs);
|
|
255
408
|
};
|
|
256
409
|
this.waiters.push(settleWaiter);
|
|
257
410
|
this._cancelSettle = done;
|
|
@@ -416,6 +569,7 @@ ${value}` : value;
|
|
|
416
569
|
* Dispatch a parsed SSE event to the appropriate handler.
|
|
417
570
|
*/
|
|
418
571
|
_handleEvent(eventName, payload) {
|
|
572
|
+
this._recordEvent(eventName);
|
|
419
573
|
if (eventName === "snapshot") {
|
|
420
574
|
const data2 = payload;
|
|
421
575
|
const spans = Array.isArray(data2.spans) ? data2.spans : [];
|
|
@@ -435,6 +589,44 @@ ${value}` : value;
|
|
|
435
589
|
}
|
|
436
590
|
}
|
|
437
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
|
+
}
|
|
438
630
|
/**
|
|
439
631
|
* Insert or replace a span in the map (dedup by trace+span ID).
|
|
440
632
|
*/
|
|
@@ -460,7 +652,7 @@ ${value}` : value;
|
|
|
460
652
|
}
|
|
461
653
|
}
|
|
462
654
|
/**
|
|
463
|
-
* Check if there is a handler.conversation
|
|
655
|
+
* Check if there is a turn-handler span (handler.conversation / handler.event) that:
|
|
464
656
|
* 1. Was NOT seen before startTurn()
|
|
465
657
|
* 2. Has terminal status (ok or error)
|
|
466
658
|
*
|
|
@@ -469,9 +661,9 @@ ${value}` : value;
|
|
|
469
661
|
* waitForTurnComplete check (which would otherwise resolve instantly
|
|
470
662
|
* against stale data instead of waiting for the new turn's handler).
|
|
471
663
|
*/
|
|
472
|
-
_hasNewCompletedHandler() {
|
|
664
|
+
_hasNewCompletedHandler(acceptSpanNames) {
|
|
473
665
|
for (const [key, span] of this.spanMap) {
|
|
474
|
-
if (span
|
|
666
|
+
if (isTurnHandlerSpan(span) && (!acceptSpanNames || acceptSpanNames.has(span.name)) && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
|
|
475
667
|
this.seenHandlerKeys.add(key);
|
|
476
668
|
return true;
|
|
477
669
|
}
|
|
@@ -498,6 +690,38 @@ ${value}` : value;
|
|
|
498
690
|
}
|
|
499
691
|
};
|
|
500
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
|
+
|
|
501
725
|
// src/transformer.ts
|
|
502
726
|
function data(span) {
|
|
503
727
|
return span.data && typeof span.data === "object" ? span.data : {};
|
|
@@ -585,7 +809,7 @@ function extractWorkflowSpans(spans) {
|
|
|
585
809
|
}
|
|
586
810
|
function extractHandler(spans) {
|
|
587
811
|
const handler = spans.find(
|
|
588
|
-
(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"
|
|
589
813
|
);
|
|
590
814
|
if (!handler) {
|
|
591
815
|
return { handlerDuration: 0, handlerStatus: "ok" };
|
|
@@ -597,53 +821,26 @@ function extractHandler(spans) {
|
|
|
597
821
|
}
|
|
598
822
|
|
|
599
823
|
// src/graders/llm.ts
|
|
600
|
-
import { Client } from "@botpress/client";
|
|
601
824
|
import { Cognitive } from "@botpress/cognitive";
|
|
602
|
-
|
|
603
|
-
function buildJudgeSystemPrompt(passThreshold) {
|
|
604
|
-
return `You are an evaluation judge for a chatbot. You will be given:
|
|
605
|
-
- The user's message
|
|
606
|
-
- The bot's response
|
|
607
|
-
- Grading criteria
|
|
608
|
-
|
|
609
|
-
Your job is to determine if the bot's response meets the criteria.
|
|
610
|
-
|
|
611
|
-
Respond with a JSON object:
|
|
612
|
-
{
|
|
613
|
-
"pass": true/false,
|
|
614
|
-
"score": 1-5,
|
|
615
|
-
"reason": "brief explanation"
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
Scoring guide:
|
|
619
|
-
- 5: Fully meets criteria, excellent response
|
|
620
|
-
- 4: Mostly meets criteria, minor issues
|
|
621
|
-
- 3: Partially meets criteria, notable gaps
|
|
622
|
-
- 2: Barely meets criteria, significant issues
|
|
623
|
-
- 1: Does not meet criteria
|
|
624
|
-
|
|
625
|
-
A score of ${passThreshold} or above is a pass.`;
|
|
626
|
-
}
|
|
825
|
+
import { Zai } from "@botpress/zai";
|
|
627
826
|
var MODEL_ALIASES = ["fast", "best"];
|
|
628
|
-
var
|
|
827
|
+
var _zai = null;
|
|
629
828
|
var _availableModels = [];
|
|
630
829
|
var _judgeModel = "fast";
|
|
830
|
+
var _failClosed = true;
|
|
831
|
+
var _log = defaultLogger;
|
|
631
832
|
function _isModelValid(model) {
|
|
632
833
|
if (MODEL_ALIASES.includes(model)) return true;
|
|
633
834
|
if (_availableModels.length === 0) return false;
|
|
634
835
|
return _availableModels.includes(model);
|
|
635
836
|
}
|
|
636
|
-
async function initLLMJudge(
|
|
637
|
-
const log = options?.logger ??
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
botId: credentials.botId,
|
|
642
|
-
...credentials.workspaceId !== void 0 ? { workspaceId: credentials.workspaceId } : {}
|
|
643
|
-
});
|
|
644
|
-
_cognitive = new Cognitive({ client, __experimental_beta: true });
|
|
837
|
+
async function initLLMJudge(client, options) {
|
|
838
|
+
const log = options?.logger ?? defaultLogger;
|
|
839
|
+
_log = log;
|
|
840
|
+
_failClosed = options?.failClosed ?? true;
|
|
841
|
+
const cognitive = new Cognitive({ client, __experimental_beta: true });
|
|
645
842
|
try {
|
|
646
|
-
_availableModels = Array.from((await
|
|
843
|
+
_availableModels = Array.from((await cognitive.fetchRemoteModels()).keys());
|
|
647
844
|
} catch (err) {
|
|
648
845
|
log.warn(`Failed to fetch available models: ${err.message}. LLM judge will be unavailable.`);
|
|
649
846
|
_availableModels = [];
|
|
@@ -654,65 +851,44 @@ async function initLLMJudge(credentials, options) {
|
|
|
654
851
|
} else if (!_isModelValid(_judgeModel)) {
|
|
655
852
|
log.warn(`Configured LLM judge model "${_judgeModel}" is invalid. Run "adk models" to list available models.`);
|
|
656
853
|
}
|
|
854
|
+
_zai = new Zai({ client: cognitive, modelId: _judgeModel });
|
|
657
855
|
}
|
|
658
856
|
async function gradeLLMJudge(botResponse, criteria, context) {
|
|
659
|
-
const
|
|
857
|
+
const assertion = `llm_judge: "${criteria}"`;
|
|
660
858
|
try {
|
|
661
|
-
if (!
|
|
859
|
+
if (!_zai) {
|
|
860
|
+
_log.warn(`LLM judge unavailable for "${criteria}": no credentials configured (initLLMJudge not called)`);
|
|
662
861
|
return {
|
|
663
|
-
assertion
|
|
664
|
-
pass:
|
|
862
|
+
assertion,
|
|
863
|
+
pass: !_failClosed,
|
|
864
|
+
skipped: true,
|
|
665
865
|
expected: criteria,
|
|
666
|
-
actual: "
|
|
866
|
+
actual: "FAILED \u2014 LLM judge unavailable: no credentials configured"
|
|
667
867
|
};
|
|
668
868
|
}
|
|
669
869
|
if (!_isModelValid(_judgeModel)) {
|
|
670
870
|
return {
|
|
671
|
-
assertion
|
|
871
|
+
assertion,
|
|
672
872
|
pass: false,
|
|
673
873
|
expected: criteria,
|
|
674
874
|
actual: "FAILED \u2014 invalid LLM judge model"
|
|
675
875
|
};
|
|
676
876
|
}
|
|
677
|
-
const { output } = await
|
|
678
|
-
model: _judgeModel,
|
|
679
|
-
temperature: 0,
|
|
680
|
-
responseFormat: "json_object",
|
|
681
|
-
systemPrompt: buildJudgeSystemPrompt(threshold),
|
|
682
|
-
messages: [
|
|
683
|
-
{
|
|
684
|
-
role: "user",
|
|
685
|
-
content: `User message: ${context.userMessage}
|
|
686
|
-
|
|
687
|
-
Bot response: ${botResponse}
|
|
688
|
-
|
|
689
|
-
Criteria: ${criteria}`
|
|
690
|
-
}
|
|
691
|
-
]
|
|
692
|
-
});
|
|
693
|
-
const rawContent = output?.choices?.[0]?.content;
|
|
694
|
-
const content = typeof rawContent === "string" ? rawContent : null;
|
|
695
|
-
if (!content) {
|
|
696
|
-
return {
|
|
697
|
-
assertion: `llm_judge: "${criteria}"`,
|
|
698
|
-
pass: true,
|
|
699
|
-
expected: criteria,
|
|
700
|
-
actual: "SKIPPED \u2014 LLM judge returned empty response"
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
const verdict = JSON.parse(content);
|
|
877
|
+
const { output } = await _zai.check({ userMessage: context.userMessage, botResponse }, criteria).result();
|
|
704
878
|
return {
|
|
705
|
-
assertion
|
|
706
|
-
pass:
|
|
879
|
+
assertion,
|
|
880
|
+
pass: output.value,
|
|
707
881
|
expected: criteria,
|
|
708
|
-
actual:
|
|
882
|
+
actual: `${output.value ? "Pass" : "Fail"} \u2014 ${output.explanation}`
|
|
709
883
|
};
|
|
710
884
|
} catch (err) {
|
|
885
|
+
_log.warn(`LLM judge failed for "${criteria}": ${err.message}`);
|
|
711
886
|
return {
|
|
712
|
-
assertion
|
|
713
|
-
pass:
|
|
887
|
+
assertion,
|
|
888
|
+
pass: !_failClosed,
|
|
889
|
+
skipped: true,
|
|
714
890
|
expected: criteria,
|
|
715
|
-
actual: `
|
|
891
|
+
actual: `FAILED \u2014 LLM judge unavailable: ${err.message}`
|
|
716
892
|
};
|
|
717
893
|
}
|
|
718
894
|
}
|
|
@@ -1053,30 +1229,6 @@ function gradeOutcome(mutations, evalDef, workflowSpans) {
|
|
|
1053
1229
|
// src/loader.ts
|
|
1054
1230
|
import { readdirSync, existsSync } from "fs";
|
|
1055
1231
|
import { resolve } from "path";
|
|
1056
|
-
|
|
1057
|
-
// src/definition.ts
|
|
1058
|
-
var Eval = class {
|
|
1059
|
-
name;
|
|
1060
|
-
description;
|
|
1061
|
-
tags;
|
|
1062
|
-
type;
|
|
1063
|
-
setup;
|
|
1064
|
-
conversation;
|
|
1065
|
-
outcome;
|
|
1066
|
-
options;
|
|
1067
|
-
constructor(def) {
|
|
1068
|
-
this.name = def.name;
|
|
1069
|
-
this.conversation = def.conversation;
|
|
1070
|
-
if (def.description !== void 0) this.description = def.description;
|
|
1071
|
-
if (def.tags !== void 0) this.tags = def.tags;
|
|
1072
|
-
if (def.type !== void 0) this.type = def.type;
|
|
1073
|
-
if (def.setup !== void 0) this.setup = def.setup;
|
|
1074
|
-
if (def.outcome !== void 0) this.outcome = def.outcome;
|
|
1075
|
-
if (def.options !== void 0) this.options = def.options;
|
|
1076
|
-
}
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
// src/loader.ts
|
|
1080
1232
|
function isEvalDefinition(value) {
|
|
1081
1233
|
return value !== null && typeof value === "object" && typeof value.name === "string" && value.name !== "" && Array.isArray(value.conversation);
|
|
1082
1234
|
}
|
|
@@ -1087,15 +1239,17 @@ async function loadEvalFile(filePath) {
|
|
|
1087
1239
|
mod = await import(absPath);
|
|
1088
1240
|
} catch (err) {
|
|
1089
1241
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
-
throw new
|
|
1091
|
-
|
|
1242
|
+
throw new EvalRunnerError({
|
|
1243
|
+
code: "EVAL_LOAD_FAILED",
|
|
1244
|
+
message: `Failed to load eval file ${filePath}: ${msg}
|
|
1092
1245
|
|
|
1093
1246
|
Make sure your eval file:
|
|
1094
1247
|
- Has no syntax or type errors
|
|
1095
1248
|
- Exports one or more \`new Eval({...})\` instances
|
|
1096
1249
|
- Has all dependencies installed (\`bun install\`)`,
|
|
1097
|
-
|
|
1098
|
-
|
|
1250
|
+
expected: true,
|
|
1251
|
+
cause: err
|
|
1252
|
+
});
|
|
1099
1253
|
}
|
|
1100
1254
|
const results = [];
|
|
1101
1255
|
for (const [key, value] of Object.entries(mod)) {
|
|
@@ -1105,9 +1259,11 @@ Make sure your eval file:
|
|
|
1105
1259
|
}
|
|
1106
1260
|
}
|
|
1107
1261
|
if (results.length === 0) {
|
|
1108
|
-
throw new
|
|
1109
|
-
|
|
1110
|
-
|
|
1262
|
+
throw new EvalRunnerError({
|
|
1263
|
+
code: "EVAL_FILE_EMPTY",
|
|
1264
|
+
message: `Invalid eval file ${filePath}: no valid evals found. Export one or more \`new Eval({...})\` instances (as default or named exports).`,
|
|
1265
|
+
expected: true
|
|
1266
|
+
});
|
|
1111
1267
|
}
|
|
1112
1268
|
return results;
|
|
1113
1269
|
}
|
|
@@ -1125,7 +1281,12 @@ async function loadEvalsFromDir(dirPath) {
|
|
|
1125
1281
|
const seen = /* @__PURE__ */ new Set();
|
|
1126
1282
|
for (const e of evals) {
|
|
1127
1283
|
if (seen.has(e.name)) {
|
|
1128
|
-
throw new
|
|
1284
|
+
throw new EvalRunnerError({
|
|
1285
|
+
code: "EVAL_DUPLICATE_NAME",
|
|
1286
|
+
message: `Duplicate eval name "${e.name}" found in ${dirPath} \u2014 names must be unique across the evals directory.`,
|
|
1287
|
+
expected: true,
|
|
1288
|
+
details: { name: e.name }
|
|
1289
|
+
});
|
|
1129
1290
|
}
|
|
1130
1291
|
seen.add(e.name);
|
|
1131
1292
|
}
|
|
@@ -1148,6 +1309,7 @@ function filterEvals(evals, filter) {
|
|
|
1148
1309
|
}
|
|
1149
1310
|
|
|
1150
1311
|
// src/runner.ts
|
|
1312
|
+
import { isAdkError } from "@botpress/analytics";
|
|
1151
1313
|
import { randomUUID } from "crypto";
|
|
1152
1314
|
var DEFAULT_IDLE_TIMEOUT = 3e4;
|
|
1153
1315
|
var DEFAULT_DEV_SERVER_URL = "http://localhost:3001";
|
|
@@ -1189,7 +1351,10 @@ async function seedEvalState(client, ctx, setup) {
|
|
|
1189
1351
|
}
|
|
1190
1352
|
if (state.conversation) {
|
|
1191
1353
|
if (!ctx.conversationId) {
|
|
1192
|
-
throw new
|
|
1354
|
+
throw new EvalRunnerError({
|
|
1355
|
+
code: "EVAL_SEED_NO_CONVERSATION",
|
|
1356
|
+
message: "Cannot seed conversation state before a conversation is created."
|
|
1357
|
+
});
|
|
1193
1358
|
}
|
|
1194
1359
|
writes.push(
|
|
1195
1360
|
client.setState({
|
|
@@ -1225,32 +1390,28 @@ function evalMetadata(evalDef) {
|
|
|
1225
1390
|
async function runEval(evalDef, connection, options = {}) {
|
|
1226
1391
|
const devServerUrl = options.devServerUrl || DEFAULT_DEV_SERVER_URL;
|
|
1227
1392
|
const devServerHeaders = options.devServerHeaders ?? {};
|
|
1393
|
+
const logger = options.logger ?? defaultLogger;
|
|
1228
1394
|
const idleTimeout = evalDef.options?.idleTimeout ?? options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT;
|
|
1229
1395
|
const judgePassThreshold = evalDef.options?.judgePassThreshold ?? options.judgePassThreshold;
|
|
1230
1396
|
const start = Date.now();
|
|
1231
1397
|
const turns = [];
|
|
1232
1398
|
let outcomeAssertions = [];
|
|
1233
|
-
const collector = new
|
|
1399
|
+
const collector = options.spanSource ?? new LocalSpanSource(devServerUrl, devServerHeaders);
|
|
1234
1400
|
try {
|
|
1235
|
-
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
|
+
);
|
|
1236
1408
|
await session.connect();
|
|
1237
|
-
let bpClient = null;
|
|
1238
|
-
const getBpClient = () => {
|
|
1239
|
-
if (!bpClient) {
|
|
1240
|
-
bpClient = new BpClient2({
|
|
1241
|
-
token: connection.token,
|
|
1242
|
-
botId: connection.botId,
|
|
1243
|
-
apiUrl: connection.apiUrl
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
return bpClient;
|
|
1247
|
-
};
|
|
1248
1409
|
let conversationId = "";
|
|
1249
1410
|
if (evalDef.setup?.state?.conversation || evalDef.setup?.workflow) {
|
|
1250
1411
|
conversationId = await session.ensureConversation();
|
|
1251
1412
|
}
|
|
1252
1413
|
await seedEvalState(
|
|
1253
|
-
|
|
1414
|
+
connection.client,
|
|
1254
1415
|
{
|
|
1255
1416
|
botId: connection.botId,
|
|
1256
1417
|
userId: session.userId,
|
|
@@ -1259,7 +1420,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1259
1420
|
evalDef.setup
|
|
1260
1421
|
);
|
|
1261
1422
|
await triggerEvalWorkflow(
|
|
1262
|
-
|
|
1423
|
+
connection.client,
|
|
1263
1424
|
{
|
|
1264
1425
|
userId: session.userId,
|
|
1265
1426
|
...conversationId ? { conversationId } : {}
|
|
@@ -1272,38 +1433,57 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1272
1433
|
if (conversationId) {
|
|
1273
1434
|
await collector.connect({ conversationId });
|
|
1274
1435
|
} else if (evalDef.conversation.length > 0) {
|
|
1275
|
-
throw new
|
|
1436
|
+
throw new EvalRunnerError({
|
|
1437
|
+
code: "EVAL_NO_CONVERSATION_ID",
|
|
1438
|
+
message: "Cannot run conversation eval without a conversationId \u2014 no chat session or setup created one."
|
|
1439
|
+
});
|
|
1276
1440
|
}
|
|
1277
1441
|
for (let i = 0; i < evalDef.conversation.length; i++) {
|
|
1278
1442
|
if (options.signal?.aborted) break;
|
|
1279
1443
|
const turn = evalDef.conversation[i];
|
|
1444
|
+
const turnConfigError = (message) => new EvalRunnerError({
|
|
1445
|
+
code: "EVAL_TURN_CONFIG_INVALID",
|
|
1446
|
+
message: `Turn ${i + 1}: ${message}`,
|
|
1447
|
+
expected: true,
|
|
1448
|
+
details: { turn: i + 1 }
|
|
1449
|
+
});
|
|
1280
1450
|
if (turn.expectSilence && turn.assert?.response) {
|
|
1281
|
-
throw
|
|
1451
|
+
throw turnConfigError(`'expectSilence' and 'assert.response' are mutually exclusive.`);
|
|
1282
1452
|
}
|
|
1283
1453
|
if (turn.user && turn.event) {
|
|
1284
|
-
throw
|
|
1454
|
+
throw turnConfigError(`'user' and 'event' are mutually exclusive.`);
|
|
1285
1455
|
}
|
|
1286
1456
|
if (!turn.user && !turn.event) {
|
|
1287
|
-
throw
|
|
1457
|
+
throw turnConfigError(`must have either 'user' or 'event'.`);
|
|
1288
1458
|
}
|
|
1289
|
-
const
|
|
1290
|
-
options.onProgress?.({
|
|
1459
|
+
const turnLabel = turn.event ? "[event]" : turn.user;
|
|
1460
|
+
await options.onProgress?.({
|
|
1291
1461
|
type: "turn_start",
|
|
1292
1462
|
evalName: evalDef.name,
|
|
1293
1463
|
evalIndex: options.evalIndex ?? 0,
|
|
1294
|
-
|
|
1464
|
+
turnIndex: i,
|
|
1295
1465
|
totalTurns: evalDef.conversation.length,
|
|
1296
|
-
userMessage:
|
|
1466
|
+
userMessage: turnLabel
|
|
1297
1467
|
});
|
|
1468
|
+
if (turn.newConversation && i > 0) {
|
|
1469
|
+
const newConversationId = await session.newConversation();
|
|
1470
|
+
await collector.repoint({ conversationId: newConversationId });
|
|
1471
|
+
}
|
|
1298
1472
|
collector.startTurn();
|
|
1299
1473
|
if (turn.event) {
|
|
1300
|
-
await session.sendEvent(turn.event.
|
|
1474
|
+
await session.sendEvent(turn.event.payload);
|
|
1301
1475
|
} else {
|
|
1302
1476
|
await session.sendMessage(turn.user);
|
|
1303
1477
|
}
|
|
1304
1478
|
try {
|
|
1305
1479
|
await collector.waitForTurnComplete({
|
|
1306
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,
|
|
1307
1487
|
...options.signal !== void 0 ? { abortSignal: options.signal } : {}
|
|
1308
1488
|
});
|
|
1309
1489
|
} catch (err) {
|
|
@@ -1327,12 +1507,17 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1327
1507
|
const turnData = transformSpans(turnSpans);
|
|
1328
1508
|
const botResponse = turnData.messages.join("\n");
|
|
1329
1509
|
const botDuration = turnData.handlerDuration;
|
|
1330
|
-
const turnLabel = turn.event ? `[event: ${turn.event.type}]` : turn.user;
|
|
1331
|
-
if (!turn.expectSilence && turnData.messages.length === 0) {
|
|
1332
|
-
throw new Error(`Turn ${i + 1}: bot produced no response.`);
|
|
1333
|
-
}
|
|
1334
1510
|
const evalStart = Date.now();
|
|
1335
1511
|
let assertions = [];
|
|
1512
|
+
const noResponse = !turn.expectSilence && turnData.messages.length === 0;
|
|
1513
|
+
if (noResponse) {
|
|
1514
|
+
assertions.push({
|
|
1515
|
+
assertion: "response",
|
|
1516
|
+
pass: false,
|
|
1517
|
+
expected: "Bot produces a response",
|
|
1518
|
+
actual: `Turn ${i + 1}: bot produced no response.`
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1336
1521
|
if (turn.expectSilence) {
|
|
1337
1522
|
const wasSilent = turnData.messages.length === 0;
|
|
1338
1523
|
assertions.push({
|
|
@@ -1342,7 +1527,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1342
1527
|
actual: wasSilent ? "No response" : `Bot responded: "${botResponse}"`
|
|
1343
1528
|
});
|
|
1344
1529
|
}
|
|
1345
|
-
if (turn.assert?.response) {
|
|
1530
|
+
if (turn.assert?.response && !noResponse) {
|
|
1346
1531
|
assertions = await gradeResponse(botResponse, turn.assert.response, {
|
|
1347
1532
|
userMessage: turnLabel,
|
|
1348
1533
|
...judgePassThreshold !== void 0 ? { judgePassThreshold } : {}
|
|
@@ -1376,7 +1561,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1376
1561
|
const turnPass = assertions.every((a) => a.pass);
|
|
1377
1562
|
const evalDuration = Date.now() - evalStart;
|
|
1378
1563
|
const turnReport = {
|
|
1379
|
-
|
|
1564
|
+
turnIndex: i,
|
|
1380
1565
|
userMessage: turnLabel,
|
|
1381
1566
|
botResponse,
|
|
1382
1567
|
assertions,
|
|
@@ -1385,11 +1570,11 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1385
1570
|
evalDuration
|
|
1386
1571
|
};
|
|
1387
1572
|
turns.push(turnReport);
|
|
1388
|
-
options.onProgress?.({
|
|
1573
|
+
await options.onProgress?.({
|
|
1389
1574
|
type: "turn_complete",
|
|
1390
1575
|
evalName: evalDef.name,
|
|
1391
1576
|
evalIndex: options.evalIndex ?? 0,
|
|
1392
|
-
|
|
1577
|
+
turnIndex: i,
|
|
1393
1578
|
totalTurns: evalDef.conversation.length,
|
|
1394
1579
|
turnReport
|
|
1395
1580
|
});
|
|
@@ -1434,13 +1619,27 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1434
1619
|
...aborted ? { error: "Eval aborted" } : {}
|
|
1435
1620
|
};
|
|
1436
1621
|
} catch (err) {
|
|
1622
|
+
const adkErr = isAdkError(err) ? err : void 0;
|
|
1623
|
+
if (!adkErr || !adkErr.expected) {
|
|
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
|
+
);
|
|
1633
|
+
options.onException?.(err, { source: "evals", eval_name: evalDef.name });
|
|
1634
|
+
}
|
|
1437
1635
|
return {
|
|
1438
1636
|
...evalMetadata(evalDef),
|
|
1439
1637
|
turns,
|
|
1440
1638
|
outcomeAssertions,
|
|
1441
1639
|
pass: false,
|
|
1442
1640
|
duration: Date.now() - start,
|
|
1443
|
-
error: err.message
|
|
1641
|
+
error: err.message,
|
|
1642
|
+
...adkErr ? { errorCode: adkErr.code } : {}
|
|
1444
1643
|
};
|
|
1445
1644
|
} finally {
|
|
1446
1645
|
collector.disconnect();
|
|
@@ -1448,9 +1647,15 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1448
1647
|
}
|
|
1449
1648
|
async function runEvalSuite(config, filter) {
|
|
1450
1649
|
const start = Date.now();
|
|
1451
|
-
const runId = randomUUID().replace(/-/g, "").slice(0, 26);
|
|
1452
|
-
|
|
1453
|
-
|
|
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
|
+
}
|
|
1454
1659
|
const evals = filterEvals(allEvals, filter);
|
|
1455
1660
|
if (evals.length === 0) {
|
|
1456
1661
|
return {
|
|
@@ -1464,37 +1669,26 @@ async function runEvalSuite(config, filter) {
|
|
|
1464
1669
|
...filter !== void 0 ? { filter } : {}
|
|
1465
1670
|
};
|
|
1466
1671
|
}
|
|
1467
|
-
await initLLMJudge(
|
|
1468
|
-
{
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
botId: config.credentials.botId,
|
|
1472
|
-
...config.credentials.workspaceId !== void 0 ? { workspaceId: config.credentials.workspaceId } : {}
|
|
1473
|
-
},
|
|
1474
|
-
{
|
|
1475
|
-
...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
|
|
1476
|
-
...config.logger !== void 0 ? { logger: config.logger } : {}
|
|
1477
|
-
}
|
|
1478
|
-
);
|
|
1479
|
-
let webhookId = config.credentials.webhookId;
|
|
1480
|
-
if (!webhookId) {
|
|
1481
|
-
webhookId = await discoverWebhookId(config.credentials.botId, config.credentials.token, config.credentials.apiUrl);
|
|
1482
|
-
}
|
|
1672
|
+
await initLLMJudge(config.client, {
|
|
1673
|
+
...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
|
|
1674
|
+
...config.logger !== void 0 ? { logger: config.logger } : {}
|
|
1675
|
+
});
|
|
1483
1676
|
const connection = {
|
|
1484
|
-
|
|
1485
|
-
botId: config.
|
|
1486
|
-
token: config.credentials.token,
|
|
1487
|
-
apiUrl: config.credentials.apiUrl
|
|
1677
|
+
client: config.client,
|
|
1678
|
+
botId: config.botId
|
|
1488
1679
|
};
|
|
1489
1680
|
const devServerUrl = config.devServerUrl || DEFAULT_DEV_SERVER_URL;
|
|
1490
1681
|
const devServerHeaders = config.devServerHeaders ?? {};
|
|
1491
|
-
|
|
1682
|
+
if (!config.createSpanSource) {
|
|
1683
|
+
await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
|
|
1684
|
+
await assertTraceStreamReadable(devServerUrl, devServerHeaders);
|
|
1685
|
+
}
|
|
1492
1686
|
const reports = [];
|
|
1493
|
-
config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
|
|
1687
|
+
await config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
|
|
1494
1688
|
for (let i = 0; i < evals.length; i++) {
|
|
1495
1689
|
if (config.signal?.aborted) break;
|
|
1496
1690
|
const evalDef = evals[i];
|
|
1497
|
-
config.onProgress?.({
|
|
1691
|
+
await config.onProgress?.({
|
|
1498
1692
|
type: "eval_start",
|
|
1499
1693
|
evalName: evalDef.name,
|
|
1500
1694
|
index: i,
|
|
@@ -1508,10 +1702,15 @@ async function runEvalSuite(config, filter) {
|
|
|
1508
1702
|
...config.evalOptions?.judgePassThreshold !== void 0 ? { judgePassThreshold: config.evalOptions.judgePassThreshold } : {},
|
|
1509
1703
|
...config.onProgress !== void 0 ? { onProgress: config.onProgress } : {},
|
|
1510
1704
|
...config.signal !== void 0 ? { signal: config.signal } : {},
|
|
1511
|
-
...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {}
|
|
1705
|
+
...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {},
|
|
1706
|
+
...config.logger !== void 0 ? { logger: config.logger } : {},
|
|
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 } : {}
|
|
1512
1711
|
});
|
|
1513
1712
|
reports.push(report);
|
|
1514
|
-
config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
|
|
1713
|
+
await config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
|
|
1515
1714
|
}
|
|
1516
1715
|
const runReport = {
|
|
1517
1716
|
id: runId,
|
|
@@ -1526,7 +1725,7 @@ async function runEvalSuite(config, filter) {
|
|
|
1526
1725
|
if (config.signal?.aborted) {
|
|
1527
1726
|
runReport.aborted = true;
|
|
1528
1727
|
}
|
|
1529
|
-
config.onProgress?.({ type: "suite_complete", report: runReport });
|
|
1728
|
+
await config.onProgress?.({ type: "suite_complete", report: runReport });
|
|
1530
1729
|
return runReport;
|
|
1531
1730
|
}
|
|
1532
1731
|
export {
|