@botpress/evals 1.19.0-beta.7 → 1.19.0-beta.9
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 +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +85 -26
- package/dist/client.js.map +3 -3
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graders/index.js +17 -7
- package/dist/graders/index.js.map +3 -3
- package/dist/graders/llm.d.ts +2 -0
- package/dist/graders/llm.d.ts.map +1 -1
- package/dist/graders/llm.js +24 -7
- package/dist/graders/llm.js.map +3 -3
- package/dist/graders/response.js +15 -6
- package/dist/graders/response.js.map +3 -3
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +60 -8
- package/dist/loader.js.map +3 -3
- package/dist/runner.d.ts +2 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +187 -79
- package/dist/runner.js.map +3 -3
- package/dist/sse-collector.d.ts.map +1 -1
- package/dist/sse-collector.js +50 -2
- package/dist/sse-collector.js.map +3 -3
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js.map +2 -2
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -1
- package/dist/types.js.map +3 -3
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,9 +1,84 @@
|
|
|
1
1
|
// src/runner.ts
|
|
2
2
|
import { Client as BpClient2 } from "@botpress/client";
|
|
3
3
|
|
|
4
|
+
// src/definition.ts
|
|
5
|
+
var Eval = class {
|
|
6
|
+
name;
|
|
7
|
+
description;
|
|
8
|
+
tags;
|
|
9
|
+
type;
|
|
10
|
+
setup;
|
|
11
|
+
conversation;
|
|
12
|
+
outcome;
|
|
13
|
+
options;
|
|
14
|
+
constructor(def) {
|
|
15
|
+
this.name = def.name;
|
|
16
|
+
this.conversation = def.conversation;
|
|
17
|
+
if (def.description !== void 0) this.description = def.description;
|
|
18
|
+
if (def.tags !== void 0) this.tags = def.tags;
|
|
19
|
+
if (def.type !== void 0) this.type = def.type;
|
|
20
|
+
if (def.setup !== void 0) this.setup = def.setup;
|
|
21
|
+
if (def.outcome !== void 0) this.outcome = def.outcome;
|
|
22
|
+
if (def.options !== void 0) this.options = def.options;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/types.ts
|
|
27
|
+
var defaultLogger = console;
|
|
28
|
+
|
|
4
29
|
// src/client.ts
|
|
5
30
|
import { Client as BpClient } from "@botpress/client";
|
|
6
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
|
+
// src/errors.ts
|
|
78
|
+
var EvalRunnerError = class extends AdkError {
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/client.ts
|
|
7
82
|
var defaultChatClient = null;
|
|
8
83
|
function getDefaultChatClient() {
|
|
9
84
|
if (defaultChatClient) return defaultChatClient;
|
|
@@ -11,7 +86,10 @@ function getDefaultChatClient() {
|
|
|
11
86
|
const mod = require2("@botpress/chat");
|
|
12
87
|
const ChatClient = mod.default?.Client ?? mod.Client;
|
|
13
88
|
if (!ChatClient) {
|
|
14
|
-
throw new
|
|
89
|
+
throw new EvalRunnerError({
|
|
90
|
+
code: "CHAT_CLIENT_MISSING",
|
|
91
|
+
message: "@botpress/chat did not expose a Client export. Pass EvalRunnerConfig.chatClient explicitly."
|
|
92
|
+
});
|
|
15
93
|
}
|
|
16
94
|
return ChatClient;
|
|
17
95
|
}
|
|
@@ -26,18 +104,23 @@ var ChatSession = class {
|
|
|
26
104
|
const ChatClient = this.chatClient ?? getDefaultChatClient();
|
|
27
105
|
this.client = await ChatClient.connect({ webhookId: this.webhookId });
|
|
28
106
|
}
|
|
29
|
-
|
|
107
|
+
/** Invariant guard — calling any session method before connect() is a runner bug. */
|
|
108
|
+
assertConnected() {
|
|
30
109
|
if (!this.client) {
|
|
31
|
-
throw new
|
|
110
|
+
throw new EvalRunnerError({
|
|
111
|
+
code: "CHAT_NOT_CONNECTED",
|
|
112
|
+
message: "ChatSession not connected. Call connect() first."
|
|
113
|
+
});
|
|
32
114
|
}
|
|
33
|
-
return this.client
|
|
115
|
+
return this.client;
|
|
116
|
+
}
|
|
117
|
+
get userId() {
|
|
118
|
+
return this.assertConnected().user.id;
|
|
34
119
|
}
|
|
35
120
|
async ensureConversation() {
|
|
36
|
-
|
|
37
|
-
throw new Error("ChatSession not connected. Call connect() first.");
|
|
38
|
-
}
|
|
121
|
+
const client = this.assertConnected();
|
|
39
122
|
if (!this.conversationId) {
|
|
40
|
-
const conv = await
|
|
123
|
+
const conv = await client.createConversation({});
|
|
41
124
|
this.conversationId = conv.conversation.id;
|
|
42
125
|
return conv.conversation.id;
|
|
43
126
|
}
|
|
@@ -47,21 +130,17 @@ var ChatSession = class {
|
|
|
47
130
|
* Send a user message. Fire-and-forget — observation is via SSECollector.
|
|
48
131
|
*/
|
|
49
132
|
async sendMessage(message) {
|
|
50
|
-
|
|
51
|
-
throw new Error("ChatSession not connected. Call connect() first.");
|
|
52
|
-
}
|
|
133
|
+
const client = this.assertConnected();
|
|
53
134
|
const conversationId = await this.ensureConversation();
|
|
54
|
-
await
|
|
135
|
+
await client.createMessage({ conversationId, payload: { type: "text", text: message } });
|
|
55
136
|
}
|
|
56
137
|
/**
|
|
57
138
|
* Send an event. Fire-and-forget — observation is via SSECollector.
|
|
58
139
|
*/
|
|
59
140
|
async sendEvent(type, payload) {
|
|
60
|
-
|
|
61
|
-
throw new Error("ChatSession not connected. Call connect() first.");
|
|
62
|
-
}
|
|
141
|
+
const client = this.assertConnected();
|
|
63
142
|
const conversationId = await this.ensureConversation();
|
|
64
|
-
await
|
|
143
|
+
await client.createEvent({ type, payload, conversationId });
|
|
65
144
|
}
|
|
66
145
|
};
|
|
67
146
|
async function discoverWebhookId(botId, token, apiUrl) {
|
|
@@ -71,26 +150,31 @@ async function discoverWebhookId(botId, token, apiUrl) {
|
|
|
71
150
|
const chat = Object.values(integrations).find((int) => int.name === "chat");
|
|
72
151
|
const webhookId = chat?.webhookId;
|
|
73
152
|
if (!webhookId || typeof webhookId !== "string") {
|
|
74
|
-
throw new
|
|
75
|
-
|
|
153
|
+
throw new EvalRunnerError({
|
|
154
|
+
code: "CHAT_INTEGRATION_MISSING",
|
|
155
|
+
message: [
|
|
76
156
|
"The `chat` integration is not installed on this bot \u2014 evals require it.",
|
|
77
157
|
"",
|
|
78
158
|
"To fix:",
|
|
79
159
|
" 1. Install it: adk add chat",
|
|
80
160
|
" 2. Redeploy: adk dev (or: adk deploy)"
|
|
81
|
-
].join("\n")
|
|
82
|
-
|
|
161
|
+
].join("\n"),
|
|
162
|
+
expected: true
|
|
163
|
+
});
|
|
83
164
|
}
|
|
84
165
|
return webhookId;
|
|
85
166
|
}
|
|
86
|
-
async function assertChatChannelBound(devServerUrl, headers = {}) {
|
|
167
|
+
async function assertChatChannelBound(devServerUrl, headers = {}, logger = defaultLogger) {
|
|
87
168
|
let conversations = [];
|
|
88
169
|
try {
|
|
89
170
|
const res = await fetch(`${devServerUrl}/api/agent`, { headers });
|
|
90
171
|
if (!res.ok) return;
|
|
91
172
|
const agent = await res.json();
|
|
92
173
|
conversations = agent.conversations ?? [];
|
|
93
|
-
} catch {
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logger?.warn(
|
|
176
|
+
`Skipping chat.channel binding pre-flight \u2014 could not query ${devServerUrl}/api/agent: ${err instanceof Error ? err.message : String(err)}`
|
|
177
|
+
);
|
|
94
178
|
return;
|
|
95
179
|
}
|
|
96
180
|
if (conversations.length === 0) return;
|
|
@@ -101,8 +185,9 @@ async function assertChatChannelBound(devServerUrl, headers = {}) {
|
|
|
101
185
|
};
|
|
102
186
|
const bound = conversations.some((c) => matchesChatChannel(c.channel));
|
|
103
187
|
if (bound) return;
|
|
104
|
-
throw new
|
|
105
|
-
|
|
188
|
+
throw new EvalRunnerError({
|
|
189
|
+
code: "CHAT_CHANNEL_UNBOUND",
|
|
190
|
+
message: [
|
|
106
191
|
"No Conversation is bound to `chat.channel` \u2014 messages via `@botpress/chat` will time out silently.",
|
|
107
192
|
"",
|
|
108
193
|
"To fix, bind a Conversation in `src/conversations/` to `chat.channel` (or `*`):",
|
|
@@ -111,8 +196,9 @@ async function assertChatChannelBound(devServerUrl, headers = {}) {
|
|
|
111
196
|
" channel: ['chat.channel', ...], // array form",
|
|
112
197
|
"",
|
|
113
198
|
"The `chat` integration is how evals and `adk chat --single` send messages programmatically."
|
|
114
|
-
].join("\n")
|
|
115
|
-
|
|
199
|
+
].join("\n"),
|
|
200
|
+
expected: true
|
|
201
|
+
});
|
|
116
202
|
}
|
|
117
203
|
|
|
118
204
|
// src/sse-collector.ts
|
|
@@ -159,10 +245,15 @@ var SSECollector = class _SSECollector {
|
|
|
159
245
|
signal: this.abortController.signal
|
|
160
246
|
});
|
|
161
247
|
if (!response.ok) {
|
|
162
|
-
throw new
|
|
248
|
+
throw new EvalRunnerError({
|
|
249
|
+
code: "SSE_CONNECT_FAILED",
|
|
250
|
+
message: `SSE connection failed: ${response.status} ${response.statusText}`,
|
|
251
|
+
expected: true,
|
|
252
|
+
suggestion: "Make sure the dev server is running (`adk dev`) and reachable."
|
|
253
|
+
});
|
|
163
254
|
}
|
|
164
255
|
if (!response.body) {
|
|
165
|
-
throw new
|
|
256
|
+
throw new EvalRunnerError({ code: "SSE_NO_BODY", message: "SSE response has no body" });
|
|
166
257
|
}
|
|
167
258
|
this.connected = true;
|
|
168
259
|
this._consumeStream(response.body).catch((err) => {
|
|
@@ -628,13 +719,15 @@ var MODEL_ALIASES = ["fast", "best"];
|
|
|
628
719
|
var _cognitive = null;
|
|
629
720
|
var _availableModels = [];
|
|
630
721
|
var _judgeModel = "fast";
|
|
722
|
+
var _log = defaultLogger;
|
|
631
723
|
function _isModelValid(model) {
|
|
632
724
|
if (MODEL_ALIASES.includes(model)) return true;
|
|
633
725
|
if (_availableModels.length === 0) return false;
|
|
634
726
|
return _availableModels.includes(model);
|
|
635
727
|
}
|
|
636
728
|
async function initLLMJudge(credentials, options) {
|
|
637
|
-
const log = options?.logger ??
|
|
729
|
+
const log = options?.logger ?? defaultLogger;
|
|
730
|
+
_log = log;
|
|
638
731
|
const client = new Client({
|
|
639
732
|
token: credentials.token,
|
|
640
733
|
apiUrl: credentials.apiUrl,
|
|
@@ -659,11 +752,12 @@ async function gradeLLMJudge(botResponse, criteria, context) {
|
|
|
659
752
|
const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD));
|
|
660
753
|
try {
|
|
661
754
|
if (!_cognitive) {
|
|
755
|
+
_log.warn(`LLM judge unavailable for "${criteria}": no credentials configured (initLLMJudge not called)`);
|
|
662
756
|
return {
|
|
663
757
|
assertion: `llm_judge: "${criteria}"`,
|
|
664
|
-
pass:
|
|
758
|
+
pass: false,
|
|
665
759
|
expected: criteria,
|
|
666
|
-
actual: "
|
|
760
|
+
actual: "FAILED \u2014 LLM judge unavailable: no credentials configured"
|
|
667
761
|
};
|
|
668
762
|
}
|
|
669
763
|
if (!_isModelValid(_judgeModel)) {
|
|
@@ -693,11 +787,12 @@ Criteria: ${criteria}`
|
|
|
693
787
|
const rawContent = output?.choices?.[0]?.content;
|
|
694
788
|
const content = typeof rawContent === "string" ? rawContent : null;
|
|
695
789
|
if (!content) {
|
|
790
|
+
_log.warn(`LLM judge returned an empty response for "${criteria}"`);
|
|
696
791
|
return {
|
|
697
792
|
assertion: `llm_judge: "${criteria}"`,
|
|
698
|
-
pass:
|
|
793
|
+
pass: false,
|
|
699
794
|
expected: criteria,
|
|
700
|
-
actual: "
|
|
795
|
+
actual: "FAILED \u2014 LLM judge returned empty response"
|
|
701
796
|
};
|
|
702
797
|
}
|
|
703
798
|
const verdict = JSON.parse(content);
|
|
@@ -708,11 +803,12 @@ Criteria: ${criteria}`
|
|
|
708
803
|
actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`
|
|
709
804
|
};
|
|
710
805
|
} catch (err) {
|
|
806
|
+
_log.warn(`LLM judge failed for "${criteria}": ${err.message}`);
|
|
711
807
|
return {
|
|
712
808
|
assertion: `llm_judge: "${criteria}"`,
|
|
713
|
-
pass:
|
|
809
|
+
pass: false,
|
|
714
810
|
expected: criteria,
|
|
715
|
-
actual: `
|
|
811
|
+
actual: `FAILED \u2014 LLM judge unavailable: ${err.message}`
|
|
716
812
|
};
|
|
717
813
|
}
|
|
718
814
|
}
|
|
@@ -1053,30 +1149,6 @@ function gradeOutcome(mutations, evalDef, workflowSpans) {
|
|
|
1053
1149
|
// src/loader.ts
|
|
1054
1150
|
import { readdirSync, existsSync } from "fs";
|
|
1055
1151
|
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
1152
|
function isEvalDefinition(value) {
|
|
1081
1153
|
return value !== null && typeof value === "object" && typeof value.name === "string" && value.name !== "" && Array.isArray(value.conversation);
|
|
1082
1154
|
}
|
|
@@ -1087,15 +1159,17 @@ async function loadEvalFile(filePath) {
|
|
|
1087
1159
|
mod = await import(absPath);
|
|
1088
1160
|
} catch (err) {
|
|
1089
1161
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
-
throw new
|
|
1091
|
-
|
|
1162
|
+
throw new EvalRunnerError({
|
|
1163
|
+
code: "EVAL_LOAD_FAILED",
|
|
1164
|
+
message: `Failed to load eval file ${filePath}: ${msg}
|
|
1092
1165
|
|
|
1093
1166
|
Make sure your eval file:
|
|
1094
1167
|
- Has no syntax or type errors
|
|
1095
1168
|
- Exports one or more \`new Eval({...})\` instances
|
|
1096
1169
|
- Has all dependencies installed (\`bun install\`)`,
|
|
1097
|
-
|
|
1098
|
-
|
|
1170
|
+
expected: true,
|
|
1171
|
+
cause: err
|
|
1172
|
+
});
|
|
1099
1173
|
}
|
|
1100
1174
|
const results = [];
|
|
1101
1175
|
for (const [key, value] of Object.entries(mod)) {
|
|
@@ -1105,9 +1179,11 @@ Make sure your eval file:
|
|
|
1105
1179
|
}
|
|
1106
1180
|
}
|
|
1107
1181
|
if (results.length === 0) {
|
|
1108
|
-
throw new
|
|
1109
|
-
|
|
1110
|
-
|
|
1182
|
+
throw new EvalRunnerError({
|
|
1183
|
+
code: "EVAL_FILE_EMPTY",
|
|
1184
|
+
message: `Invalid eval file ${filePath}: no valid evals found. Export one or more \`new Eval({...})\` instances (as default or named exports).`,
|
|
1185
|
+
expected: true
|
|
1186
|
+
});
|
|
1111
1187
|
}
|
|
1112
1188
|
return results;
|
|
1113
1189
|
}
|
|
@@ -1125,7 +1201,12 @@ async function loadEvalsFromDir(dirPath) {
|
|
|
1125
1201
|
const seen = /* @__PURE__ */ new Set();
|
|
1126
1202
|
for (const e of evals) {
|
|
1127
1203
|
if (seen.has(e.name)) {
|
|
1128
|
-
throw new
|
|
1204
|
+
throw new EvalRunnerError({
|
|
1205
|
+
code: "EVAL_DUPLICATE_NAME",
|
|
1206
|
+
message: `Duplicate eval name "${e.name}" found in ${dirPath} \u2014 names must be unique across the evals directory.`,
|
|
1207
|
+
expected: true,
|
|
1208
|
+
details: { name: e.name }
|
|
1209
|
+
});
|
|
1129
1210
|
}
|
|
1130
1211
|
seen.add(e.name);
|
|
1131
1212
|
}
|
|
@@ -1189,7 +1270,10 @@ async function seedEvalState(client, ctx, setup) {
|
|
|
1189
1270
|
}
|
|
1190
1271
|
if (state.conversation) {
|
|
1191
1272
|
if (!ctx.conversationId) {
|
|
1192
|
-
throw new
|
|
1273
|
+
throw new EvalRunnerError({
|
|
1274
|
+
code: "EVAL_SEED_NO_CONVERSATION",
|
|
1275
|
+
message: "Cannot seed conversation state before a conversation is created."
|
|
1276
|
+
});
|
|
1193
1277
|
}
|
|
1194
1278
|
writes.push(
|
|
1195
1279
|
client.setState({
|
|
@@ -1225,6 +1309,7 @@ function evalMetadata(evalDef) {
|
|
|
1225
1309
|
async function runEval(evalDef, connection, options = {}) {
|
|
1226
1310
|
const devServerUrl = options.devServerUrl || DEFAULT_DEV_SERVER_URL;
|
|
1227
1311
|
const devServerHeaders = options.devServerHeaders ?? {};
|
|
1312
|
+
const logger = options.logger ?? defaultLogger;
|
|
1228
1313
|
const idleTimeout = evalDef.options?.idleTimeout ?? options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT;
|
|
1229
1314
|
const judgePassThreshold = evalDef.options?.judgePassThreshold ?? options.judgePassThreshold;
|
|
1230
1315
|
const start = Date.now();
|
|
@@ -1272,19 +1357,28 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1272
1357
|
if (conversationId) {
|
|
1273
1358
|
await collector.connect({ conversationId });
|
|
1274
1359
|
} else if (evalDef.conversation.length > 0) {
|
|
1275
|
-
throw new
|
|
1360
|
+
throw new EvalRunnerError({
|
|
1361
|
+
code: "EVAL_NO_CONVERSATION_ID",
|
|
1362
|
+
message: "Cannot run conversation eval without a conversationId \u2014 no chat session or setup created one."
|
|
1363
|
+
});
|
|
1276
1364
|
}
|
|
1277
1365
|
for (let i = 0; i < evalDef.conversation.length; i++) {
|
|
1278
1366
|
if (options.signal?.aborted) break;
|
|
1279
1367
|
const turn = evalDef.conversation[i];
|
|
1368
|
+
const turnConfigError = (message) => new EvalRunnerError({
|
|
1369
|
+
code: "EVAL_TURN_CONFIG_INVALID",
|
|
1370
|
+
message: `Turn ${i + 1}: ${message}`,
|
|
1371
|
+
expected: true,
|
|
1372
|
+
details: { turn: i + 1 }
|
|
1373
|
+
});
|
|
1280
1374
|
if (turn.expectSilence && turn.assert?.response) {
|
|
1281
|
-
throw
|
|
1375
|
+
throw turnConfigError(`'expectSilence' and 'assert.response' are mutually exclusive.`);
|
|
1282
1376
|
}
|
|
1283
1377
|
if (turn.user && turn.event) {
|
|
1284
|
-
throw
|
|
1378
|
+
throw turnConfigError(`'user' and 'event' are mutually exclusive.`);
|
|
1285
1379
|
}
|
|
1286
1380
|
if (!turn.user && !turn.event) {
|
|
1287
|
-
throw
|
|
1381
|
+
throw turnConfigError(`must have either 'user' or 'event'.`);
|
|
1288
1382
|
}
|
|
1289
1383
|
const turnLabelForEvent = turn.event ? `[event: ${turn.event.type}]` : turn.user;
|
|
1290
1384
|
options.onProgress?.({
|
|
@@ -1328,11 +1422,17 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1328
1422
|
const botResponse = turnData.messages.join("\n");
|
|
1329
1423
|
const botDuration = turnData.handlerDuration;
|
|
1330
1424
|
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
1425
|
const evalStart = Date.now();
|
|
1335
1426
|
let assertions = [];
|
|
1427
|
+
const noResponse = !turn.expectSilence && turnData.messages.length === 0;
|
|
1428
|
+
if (noResponse) {
|
|
1429
|
+
assertions.push({
|
|
1430
|
+
assertion: "response",
|
|
1431
|
+
pass: false,
|
|
1432
|
+
expected: "Bot produces a response",
|
|
1433
|
+
actual: `Turn ${i + 1}: bot produced no response.`
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1336
1436
|
if (turn.expectSilence) {
|
|
1337
1437
|
const wasSilent = turnData.messages.length === 0;
|
|
1338
1438
|
assertions.push({
|
|
@@ -1342,7 +1442,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1342
1442
|
actual: wasSilent ? "No response" : `Bot responded: "${botResponse}"`
|
|
1343
1443
|
});
|
|
1344
1444
|
}
|
|
1345
|
-
if (turn.assert?.response) {
|
|
1445
|
+
if (turn.assert?.response && !noResponse) {
|
|
1346
1446
|
assertions = await gradeResponse(botResponse, turn.assert.response, {
|
|
1347
1447
|
userMessage: turnLabel,
|
|
1348
1448
|
...judgePassThreshold !== void 0 ? { judgePassThreshold } : {}
|
|
@@ -1434,13 +1534,19 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1434
1534
|
...aborted ? { error: "Eval aborted" } : {}
|
|
1435
1535
|
};
|
|
1436
1536
|
} catch (err) {
|
|
1537
|
+
const adkErr = isAdkError(err) ? err : void 0;
|
|
1538
|
+
if (!adkErr || !adkErr.expected) {
|
|
1539
|
+
logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${err.message}`);
|
|
1540
|
+
options.onException?.(err, { source: "evals", eval_name: evalDef.name });
|
|
1541
|
+
}
|
|
1437
1542
|
return {
|
|
1438
1543
|
...evalMetadata(evalDef),
|
|
1439
1544
|
turns,
|
|
1440
1545
|
outcomeAssertions,
|
|
1441
1546
|
pass: false,
|
|
1442
1547
|
duration: Date.now() - start,
|
|
1443
|
-
error: err.message
|
|
1548
|
+
error: err.message,
|
|
1549
|
+
...adkErr ? { errorCode: adkErr.code } : {}
|
|
1444
1550
|
};
|
|
1445
1551
|
} finally {
|
|
1446
1552
|
collector.disconnect();
|
|
@@ -1488,7 +1594,7 @@ async function runEvalSuite(config, filter) {
|
|
|
1488
1594
|
};
|
|
1489
1595
|
const devServerUrl = config.devServerUrl || DEFAULT_DEV_SERVER_URL;
|
|
1490
1596
|
const devServerHeaders = config.devServerHeaders ?? {};
|
|
1491
|
-
await assertChatChannelBound(devServerUrl, devServerHeaders);
|
|
1597
|
+
await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
|
|
1492
1598
|
const reports = [];
|
|
1493
1599
|
config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
|
|
1494
1600
|
for (let i = 0; i < evals.length; i++) {
|
|
@@ -1508,7 +1614,9 @@ async function runEvalSuite(config, filter) {
|
|
|
1508
1614
|
...config.evalOptions?.judgePassThreshold !== void 0 ? { judgePassThreshold: config.evalOptions.judgePassThreshold } : {},
|
|
1509
1615
|
...config.onProgress !== void 0 ? { onProgress: config.onProgress } : {},
|
|
1510
1616
|
...config.signal !== void 0 ? { signal: config.signal } : {},
|
|
1511
|
-
...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {}
|
|
1617
|
+
...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {},
|
|
1618
|
+
...config.logger !== void 0 ? { logger: config.logger } : {},
|
|
1619
|
+
...config.onException !== void 0 ? { onException: config.onException } : {}
|
|
1512
1620
|
});
|
|
1513
1621
|
reports.push(report);
|
|
1514
1622
|
config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
|