@botpress/evals 1.19.0-beta.14 → 1.19.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +9 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +74 -20
- package/dist/client.js.map +4 -4
- package/dist/definition.d.ts +2 -3
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js.map +2 -2
- package/dist/graders/index.js +74 -21
- package/dist/graders/index.js.map +2 -2
- package/dist/graders/llm.d.ts +12 -12
- package/dist/graders/llm.d.ts.map +1 -1
- package/dist/graders/llm.js +75 -30
- package/dist/graders/llm.js.map +2 -2
- package/dist/graders/response.js +65 -16
- package/dist/graders/response.js.map +2 -2
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -10
- package/dist/index.js.map +3 -3
- package/dist/loader.js +39 -1
- package/dist/loader.js.map +3 -3
- package/dist/runner.d.ts +1 -5
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +217 -141
- package/dist/runner.js.map +4 -4
- package/dist/{spans/sse-collector.d.ts → sse-collector.d.ts} +4 -6
- package/dist/sse-collector.d.ts.map +1 -0
- package/dist/{spans/sse-collector.js → sse-collector.js} +49 -17
- package/dist/sse-collector.js.map +7 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/transformer.d.ts +2 -2
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +1 -1
- package/dist/transformer.js.map +2 -2
- package/dist/types.d.ts +33 -38
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +2 -2
- package/package.json +6 -66
- package/dist/manifest.d.ts +0 -13
- package/dist/manifest.d.ts.map +0 -1
- package/dist/spans/index.d.ts +0 -5
- package/dist/spans/index.d.ts.map +0 -1
- package/dist/spans/index.js +0 -642
- package/dist/spans/index.js.map +0 -7
- package/dist/spans/local-span-source.d.ts +0 -16
- package/dist/spans/local-span-source.d.ts.map +0 -1
- package/dist/spans/local-span-source.js +0 -478
- package/dist/spans/local-span-source.js.map +0 -7
- package/dist/spans/span-source.d.ts +0 -20
- package/dist/spans/span-source.d.ts.map +0 -1
- package/dist/spans/span-source.js +0 -1
- package/dist/spans/span-source.js.map +0 -7
- package/dist/spans/sse-collector.d.ts.map +0 -1
- package/dist/spans/sse-collector.js.map +0 -7
- package/dist/spans/trace.d.ts.map +0 -1
- package/dist/spans/vortex-span-source.d.ts +0 -34
- package/dist/spans/vortex-span-source.d.ts.map +0 -1
- package/dist/spans/vortex-span-source.js +0 -166
- package/dist/spans/vortex-span-source.js.map +0 -7
- package/dist/stores/eval-definition-loader.d.ts +0 -4
- package/dist/stores/eval-definition-loader.d.ts.map +0 -1
- package/dist/stores/eval-definition-loader.js +0 -104
- package/dist/stores/eval-definition-loader.js.map +0 -7
- package/dist/stores/eval-store.d.ts +0 -61
- package/dist/stores/eval-store.d.ts.map +0 -1
- package/dist/stores/eval-store.js +0 -1
- package/dist/stores/eval-store.js.map +0 -7
- package/dist/stores/index.d.ts +0 -8
- package/dist/stores/index.d.ts.map +0 -1
- package/dist/stores/index.js +0 -1118
- package/dist/stores/index.js.map +0 -7
- package/dist/stores/local-eval-store.d.ts +0 -66
- package/dist/stores/local-eval-store.d.ts.map +0 -1
- package/dist/stores/local-eval-store.js +0 -557
- package/dist/stores/local-eval-store.js.map +0 -7
- package/dist/stores/vortex-eval-store-entry.d.ts +0 -6
- package/dist/stores/vortex-eval-store-entry.d.ts.map +0 -1
- package/dist/stores/vortex-eval-store-entry.js +0 -664
- package/dist/stores/vortex-eval-store-entry.js.map +0 -7
- package/dist/stores/vortex-eval-store.d.ts +0 -78
- package/dist/stores/vortex-eval-store.d.ts.map +0 -1
- package/dist/stores/vortex-eval-store.js +0 -562
- package/dist/stores/vortex-eval-store.js.map +0 -7
- /package/dist/{spans/trace.d.ts → trace.d.ts} +0 -0
- /package/dist/{spans/trace.js → trace.js} +0 -0
- /package/dist/{spans/trace.js.map → trace.js.map} +0 -0
package/dist/runner.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/runner.ts
|
|
2
|
+
import { Client as BpClient2 } from "@botpress/client";
|
|
3
|
+
|
|
1
4
|
// src/definition.ts
|
|
2
5
|
var Eval = class {
|
|
3
6
|
name;
|
|
@@ -23,33 +26,84 @@ var Eval = class {
|
|
|
23
26
|
// src/types.ts
|
|
24
27
|
var defaultLogger = console;
|
|
25
28
|
|
|
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
|
+
|
|
26
77
|
// src/errors.ts
|
|
27
|
-
import { AdkError } from "@botpress/analytics";
|
|
28
78
|
var EvalRunnerError = class extends AdkError {
|
|
29
79
|
};
|
|
30
80
|
|
|
31
81
|
// src/client.ts
|
|
32
82
|
var TRACE_STREAM_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
83
|
+
var defaultChatClient = null;
|
|
84
|
+
function getDefaultChatClient() {
|
|
85
|
+
if (defaultChatClient) return defaultChatClient;
|
|
86
|
+
const require2 = createRequire(import.meta.url);
|
|
87
|
+
const mod = require2("@botpress/chat");
|
|
88
|
+
const ChatClient = mod.default?.Client ?? mod.Client;
|
|
89
|
+
if (!ChatClient) {
|
|
90
|
+
throw new EvalRunnerError({
|
|
91
|
+
code: "CHAT_CLIENT_MISSING",
|
|
92
|
+
message: "@botpress/chat did not expose a Client export. Pass EvalRunnerConfig.chatClient explicitly."
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return ChatClient;
|
|
96
|
+
}
|
|
33
97
|
var ChatSession = class {
|
|
34
|
-
constructor(
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this._chatWebhookId = _chatWebhookId;
|
|
38
|
-
this._chatBaseUrl = _chatBaseUrl;
|
|
39
|
-
this._chatClient = _chatClient;
|
|
98
|
+
constructor(webhookId, chatClient) {
|
|
99
|
+
this.webhookId = webhookId;
|
|
100
|
+
this.chatClient = chatClient;
|
|
40
101
|
}
|
|
41
102
|
client = null;
|
|
42
103
|
conversationId = null;
|
|
43
104
|
async connect() {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
/* webpackIgnore: true */
|
|
47
|
-
"@botpress/chat"
|
|
48
|
-
)).Client;
|
|
49
|
-
this.client = await ChatClientCtor.connect({
|
|
50
|
-
webhookId,
|
|
51
|
-
...this._chatBaseUrl ? { baseApiUrl: this._chatBaseUrl } : {}
|
|
52
|
-
});
|
|
105
|
+
const ChatClient = this.chatClient ?? getDefaultChatClient();
|
|
106
|
+
this.client = await ChatClient.connect({ webhookId: this.webhookId });
|
|
53
107
|
}
|
|
54
108
|
/** Invariant guard — calling any session method before connect() is a runner bug. */
|
|
55
109
|
assertConnected() {
|
|
@@ -81,13 +135,17 @@ var ChatSession = class {
|
|
|
81
135
|
const conversationId = await this.ensureConversation();
|
|
82
136
|
await client.createMessage({ conversationId, payload: { type: "text", text: message } });
|
|
83
137
|
}
|
|
84
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Send an event. Fire-and-forget — observation is via SSECollector.
|
|
140
|
+
*/
|
|
141
|
+
async sendEvent(type, payload) {
|
|
85
142
|
const client = this.assertConnected();
|
|
86
143
|
const conversationId = await this.ensureConversation();
|
|
87
|
-
await client.createEvent({ payload, conversationId });
|
|
144
|
+
await client.createEvent({ type, payload, conversationId });
|
|
88
145
|
}
|
|
89
146
|
};
|
|
90
|
-
async function discoverWebhookId(
|
|
147
|
+
async function discoverWebhookId(botId, token, apiUrl) {
|
|
148
|
+
const client = new BpClient({ token, botId, apiUrl });
|
|
91
149
|
const { bot } = await client.getBot({ id: botId });
|
|
92
150
|
const integrations = bot.integrations || {};
|
|
93
151
|
const chat = Object.values(integrations).find((int) => int.name === "chat");
|
|
@@ -215,17 +273,13 @@ async function assertChatChannelBound(devServerUrl, headers = {}, logger = defau
|
|
|
215
273
|
});
|
|
216
274
|
}
|
|
217
275
|
|
|
218
|
-
// src/
|
|
276
|
+
// src/sse-collector.ts
|
|
219
277
|
function spanKey(span) {
|
|
220
278
|
return `${span.id.trace}:${span.id.span}`;
|
|
221
279
|
}
|
|
222
280
|
function spanData(span) {
|
|
223
281
|
return span.data && typeof span.data === "object" ? span.data : {};
|
|
224
282
|
}
|
|
225
|
-
var TURN_HANDLER_SPAN_NAMES = /* @__PURE__ */ new Set(["handler.conversation", "handler.event"]);
|
|
226
|
-
function isTurnHandlerSpan(span) {
|
|
227
|
-
return TURN_HANDLER_SPAN_NAMES.has(span.name);
|
|
228
|
-
}
|
|
229
283
|
var SSECollector = class _SSECollector {
|
|
230
284
|
devServerUrl;
|
|
231
285
|
headers;
|
|
@@ -287,22 +341,20 @@ var SSECollector = class _SSECollector {
|
|
|
287
341
|
}
|
|
288
342
|
/**
|
|
289
343
|
* Mark the beginning of a new turn.
|
|
290
|
-
* Records which
|
|
291
|
-
*
|
|
292
|
-
* handler completions.
|
|
344
|
+
* Records which handler.conversation span IDs have been seen so far,
|
|
345
|
+
* so waitForTurnComplete() only resolves on NEW handler completions.
|
|
293
346
|
*/
|
|
294
347
|
startTurn() {
|
|
295
348
|
this.turnStartKeys = new Set(this.spanMap.keys());
|
|
296
349
|
for (const [key, span] of this.spanMap) {
|
|
297
|
-
if (
|
|
350
|
+
if (span.name === "handler.conversation") {
|
|
298
351
|
this.seenHandlerKeys.add(key);
|
|
299
352
|
}
|
|
300
353
|
}
|
|
301
354
|
}
|
|
302
355
|
static SETTLE_MS = 100;
|
|
303
356
|
/**
|
|
304
|
-
* Wait for a
|
|
305
|
-
* reach terminal status (ok or error).
|
|
357
|
+
* Wait for a handler.conversation span to reach terminal status (ok or error).
|
|
306
358
|
* Only considers spans NOT seen before the last startTurn() call.
|
|
307
359
|
*
|
|
308
360
|
* After the handler closes, waits for a quiet period (no new spans for SETTLE_MS)
|
|
@@ -321,7 +373,7 @@ var SSECollector = class _SSECollector {
|
|
|
321
373
|
const timer = setTimeout(() => {
|
|
322
374
|
this._removeWaiter(waiter);
|
|
323
375
|
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
324
|
-
const reason = this.streamError ? `SSE stream died: ${this.streamError.message}` : "no handler.conversation
|
|
376
|
+
const reason = this.streamError ? `SSE stream died: ${this.streamError.message}` : "no handler.conversation span completed";
|
|
325
377
|
reject(
|
|
326
378
|
new Error(
|
|
327
379
|
`waitForTurnComplete timed out after ${opts.timeout}ms \u2014 ${reason}; diagnostics: ${this._formatTurnDiagnostics()}`
|
|
@@ -569,7 +621,7 @@ ${value}` : value;
|
|
|
569
621
|
const handlerStatuses = /* @__PURE__ */ new Set();
|
|
570
622
|
for (const span of turnSpans) {
|
|
571
623
|
spanNames.add(span.name);
|
|
572
|
-
if (
|
|
624
|
+
if (span.name === "handler.conversation") {
|
|
573
625
|
handlerStatuses.add(span.status);
|
|
574
626
|
}
|
|
575
627
|
}
|
|
@@ -582,7 +634,7 @@ ${value}` : value;
|
|
|
582
634
|
`totalSpans=${this.spanMap.size}`,
|
|
583
635
|
`turnSpans=${turnSpans.length}`,
|
|
584
636
|
`turnSpanNames=${this._formatSet(spanNames)}`,
|
|
585
|
-
`
|
|
637
|
+
`handlerConversationStatuses=${this._formatSet(handlerStatuses)}`
|
|
586
638
|
].join(", ");
|
|
587
639
|
}
|
|
588
640
|
_formatSet(values, limit = 12) {
|
|
@@ -618,7 +670,7 @@ ${value}` : value;
|
|
|
618
670
|
}
|
|
619
671
|
}
|
|
620
672
|
/**
|
|
621
|
-
* Check if there is a
|
|
673
|
+
* Check if there is a handler.conversation span that:
|
|
622
674
|
* 1. Was NOT seen before startTurn()
|
|
623
675
|
* 2. Has terminal status (ok or error)
|
|
624
676
|
*
|
|
@@ -629,7 +681,7 @@ ${value}` : value;
|
|
|
629
681
|
*/
|
|
630
682
|
_hasNewCompletedHandler() {
|
|
631
683
|
for (const [key, span] of this.spanMap) {
|
|
632
|
-
if (
|
|
684
|
+
if (span.name === "handler.conversation" && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
|
|
633
685
|
this.seenHandlerKeys.add(key);
|
|
634
686
|
return true;
|
|
635
687
|
}
|
|
@@ -656,35 +708,6 @@ ${value}` : value;
|
|
|
656
708
|
}
|
|
657
709
|
};
|
|
658
710
|
|
|
659
|
-
// src/spans/local-span-source.ts
|
|
660
|
-
var LocalSpanSource = class {
|
|
661
|
-
collector;
|
|
662
|
-
constructor(devServerUrl, headers = {}) {
|
|
663
|
-
this.collector = new SSECollector(devServerUrl, headers);
|
|
664
|
-
}
|
|
665
|
-
async connect(filter) {
|
|
666
|
-
await this.collector.connect(filter);
|
|
667
|
-
}
|
|
668
|
-
startTurn() {
|
|
669
|
-
this.collector.startTurn();
|
|
670
|
-
}
|
|
671
|
-
async waitForTurnComplete(opts) {
|
|
672
|
-
await this.collector.waitForTurnComplete(opts);
|
|
673
|
-
}
|
|
674
|
-
async waitForWorkflow(name, opts) {
|
|
675
|
-
await this.collector.waitForWorkflow(name, opts);
|
|
676
|
-
}
|
|
677
|
-
getTurnSpans() {
|
|
678
|
-
return this.collector.getTurnSpans();
|
|
679
|
-
}
|
|
680
|
-
getAllSpans() {
|
|
681
|
-
return this.collector.getAllSpans();
|
|
682
|
-
}
|
|
683
|
-
disconnect() {
|
|
684
|
-
this.collector.disconnect();
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
|
|
688
711
|
// src/transformer.ts
|
|
689
712
|
function data(span) {
|
|
690
713
|
return span.data && typeof span.data === "object" ? span.data : {};
|
|
@@ -772,7 +795,7 @@ function extractWorkflowSpans(spans) {
|
|
|
772
795
|
}
|
|
773
796
|
function extractHandler(spans) {
|
|
774
797
|
const handler = spans.find(
|
|
775
|
-
(s) => (s.name === "handler.conversation" || s.name === "handler.
|
|
798
|
+
(s) => (s.name === "handler.conversation" || s.name === "handler.workflow") && s.status !== "running"
|
|
776
799
|
);
|
|
777
800
|
if (!handler) {
|
|
778
801
|
return { handlerDuration: 0, handlerStatus: "ok" };
|
|
@@ -784,26 +807,55 @@ function extractHandler(spans) {
|
|
|
784
807
|
}
|
|
785
808
|
|
|
786
809
|
// src/graders/llm.ts
|
|
810
|
+
import { Client } from "@botpress/client";
|
|
787
811
|
import { Cognitive } from "@botpress/cognitive";
|
|
788
|
-
|
|
812
|
+
var DEFAULT_PASS_THRESHOLD = 3;
|
|
813
|
+
function buildJudgeSystemPrompt(passThreshold) {
|
|
814
|
+
return `You are an evaluation judge for a chatbot. You will be given:
|
|
815
|
+
- The user's message
|
|
816
|
+
- The bot's response
|
|
817
|
+
- Grading criteria
|
|
818
|
+
|
|
819
|
+
Your job is to determine if the bot's response meets the criteria.
|
|
820
|
+
|
|
821
|
+
Respond with a JSON object:
|
|
822
|
+
{
|
|
823
|
+
"pass": true/false,
|
|
824
|
+
"score": 1-5,
|
|
825
|
+
"reason": "brief explanation"
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
Scoring guide:
|
|
829
|
+
- 5: Fully meets criteria, excellent response
|
|
830
|
+
- 4: Mostly meets criteria, minor issues
|
|
831
|
+
- 3: Partially meets criteria, notable gaps
|
|
832
|
+
- 2: Barely meets criteria, significant issues
|
|
833
|
+
- 1: Does not meet criteria
|
|
834
|
+
|
|
835
|
+
A score of ${passThreshold} or above is a pass.`;
|
|
836
|
+
}
|
|
789
837
|
var MODEL_ALIASES = ["fast", "best"];
|
|
790
|
-
var
|
|
838
|
+
var _cognitive = null;
|
|
791
839
|
var _availableModels = [];
|
|
792
840
|
var _judgeModel = "fast";
|
|
793
|
-
var _failClosed = true;
|
|
794
841
|
var _log = defaultLogger;
|
|
795
842
|
function _isModelValid(model) {
|
|
796
843
|
if (MODEL_ALIASES.includes(model)) return true;
|
|
797
844
|
if (_availableModels.length === 0) return false;
|
|
798
845
|
return _availableModels.includes(model);
|
|
799
846
|
}
|
|
800
|
-
async function initLLMJudge(
|
|
847
|
+
async function initLLMJudge(credentials, options) {
|
|
801
848
|
const log = options?.logger ?? defaultLogger;
|
|
802
849
|
_log = log;
|
|
803
|
-
|
|
804
|
-
|
|
850
|
+
const client = new Client({
|
|
851
|
+
token: credentials.token,
|
|
852
|
+
apiUrl: credentials.apiUrl,
|
|
853
|
+
botId: credentials.botId,
|
|
854
|
+
...credentials.workspaceId !== void 0 ? { workspaceId: credentials.workspaceId } : {}
|
|
855
|
+
});
|
|
856
|
+
_cognitive = new Cognitive({ client, __experimental_beta: true });
|
|
805
857
|
try {
|
|
806
|
-
_availableModels = Array.from((await
|
|
858
|
+
_availableModels = Array.from((await _cognitive.fetchRemoteModels()).keys());
|
|
807
859
|
} catch (err) {
|
|
808
860
|
log.warn(`Failed to fetch available models: ${err.message}. LLM judge will be unavailable.`);
|
|
809
861
|
_availableModels = [];
|
|
@@ -814,42 +866,66 @@ async function initLLMJudge(client, options) {
|
|
|
814
866
|
} else if (!_isModelValid(_judgeModel)) {
|
|
815
867
|
log.warn(`Configured LLM judge model "${_judgeModel}" is invalid. Run "adk models" to list available models.`);
|
|
816
868
|
}
|
|
817
|
-
_zai = new Zai({ client: cognitive, modelId: _judgeModel });
|
|
818
869
|
}
|
|
819
870
|
async function gradeLLMJudge(botResponse, criteria, context) {
|
|
820
|
-
const
|
|
871
|
+
const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD));
|
|
821
872
|
try {
|
|
822
|
-
if (!
|
|
873
|
+
if (!_cognitive) {
|
|
823
874
|
_log.warn(`LLM judge unavailable for "${criteria}": no credentials configured (initLLMJudge not called)`);
|
|
824
875
|
return {
|
|
825
|
-
assertion
|
|
826
|
-
pass:
|
|
827
|
-
skipped: true,
|
|
876
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
877
|
+
pass: false,
|
|
828
878
|
expected: criteria,
|
|
829
879
|
actual: "FAILED \u2014 LLM judge unavailable: no credentials configured"
|
|
830
880
|
};
|
|
831
881
|
}
|
|
832
882
|
if (!_isModelValid(_judgeModel)) {
|
|
833
883
|
return {
|
|
834
|
-
assertion
|
|
884
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
835
885
|
pass: false,
|
|
836
886
|
expected: criteria,
|
|
837
887
|
actual: "FAILED \u2014 invalid LLM judge model"
|
|
838
888
|
};
|
|
839
889
|
}
|
|
840
|
-
const { output } = await
|
|
890
|
+
const { output } = await _cognitive.generateContent({
|
|
891
|
+
model: _judgeModel,
|
|
892
|
+
temperature: 0,
|
|
893
|
+
responseFormat: "json_object",
|
|
894
|
+
systemPrompt: buildJudgeSystemPrompt(threshold),
|
|
895
|
+
messages: [
|
|
896
|
+
{
|
|
897
|
+
role: "user",
|
|
898
|
+
content: `User message: ${context.userMessage}
|
|
899
|
+
|
|
900
|
+
Bot response: ${botResponse}
|
|
901
|
+
|
|
902
|
+
Criteria: ${criteria}`
|
|
903
|
+
}
|
|
904
|
+
]
|
|
905
|
+
});
|
|
906
|
+
const rawContent = output?.choices?.[0]?.content;
|
|
907
|
+
const content = typeof rawContent === "string" ? rawContent : null;
|
|
908
|
+
if (!content) {
|
|
909
|
+
_log.warn(`LLM judge returned an empty response for "${criteria}"`);
|
|
910
|
+
return {
|
|
911
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
912
|
+
pass: false,
|
|
913
|
+
expected: criteria,
|
|
914
|
+
actual: "FAILED \u2014 LLM judge returned empty response"
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const verdict = JSON.parse(content);
|
|
841
918
|
return {
|
|
842
|
-
assertion
|
|
843
|
-
pass:
|
|
919
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
920
|
+
pass: verdict.score >= threshold,
|
|
844
921
|
expected: criteria,
|
|
845
|
-
actual:
|
|
922
|
+
actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`
|
|
846
923
|
};
|
|
847
924
|
} catch (err) {
|
|
848
925
|
_log.warn(`LLM judge failed for "${criteria}": ${err.message}`);
|
|
849
926
|
return {
|
|
850
|
-
assertion
|
|
851
|
-
pass:
|
|
852
|
-
skipped: true,
|
|
927
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
928
|
+
pass: false,
|
|
853
929
|
expected: criteria,
|
|
854
930
|
actual: `FAILED \u2014 LLM judge unavailable: ${err.message}`
|
|
855
931
|
};
|
|
@@ -1272,7 +1348,6 @@ function filterEvals(evals, filter) {
|
|
|
1272
1348
|
}
|
|
1273
1349
|
|
|
1274
1350
|
// src/runner.ts
|
|
1275
|
-
import { isAdkError } from "@botpress/analytics";
|
|
1276
1351
|
import { randomUUID } from "crypto";
|
|
1277
1352
|
var DEFAULT_IDLE_TIMEOUT = 3e4;
|
|
1278
1353
|
var DEFAULT_DEV_SERVER_URL = "http://localhost:3001";
|
|
@@ -1359,22 +1434,27 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1359
1434
|
const start = Date.now();
|
|
1360
1435
|
const turns = [];
|
|
1361
1436
|
let outcomeAssertions = [];
|
|
1362
|
-
const collector =
|
|
1437
|
+
const collector = new SSECollector(devServerUrl, devServerHeaders);
|
|
1363
1438
|
try {
|
|
1364
|
-
const session = new ChatSession(
|
|
1365
|
-
connection.client,
|
|
1366
|
-
connection.botId,
|
|
1367
|
-
options.chatWebhookId,
|
|
1368
|
-
options.chatBaseUrl,
|
|
1369
|
-
options.chatClient
|
|
1370
|
-
);
|
|
1439
|
+
const session = new ChatSession(connection.webhookId, options.chatClient);
|
|
1371
1440
|
await session.connect();
|
|
1441
|
+
let bpClient = null;
|
|
1442
|
+
const getBpClient = () => {
|
|
1443
|
+
if (!bpClient) {
|
|
1444
|
+
bpClient = new BpClient2({
|
|
1445
|
+
token: connection.token,
|
|
1446
|
+
botId: connection.botId,
|
|
1447
|
+
apiUrl: connection.apiUrl
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
return bpClient;
|
|
1451
|
+
};
|
|
1372
1452
|
let conversationId = "";
|
|
1373
1453
|
if (evalDef.setup?.state?.conversation || evalDef.setup?.workflow) {
|
|
1374
1454
|
conversationId = await session.ensureConversation();
|
|
1375
1455
|
}
|
|
1376
1456
|
await seedEvalState(
|
|
1377
|
-
|
|
1457
|
+
getBpClient(),
|
|
1378
1458
|
{
|
|
1379
1459
|
botId: connection.botId,
|
|
1380
1460
|
userId: session.userId,
|
|
@@ -1383,7 +1463,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1383
1463
|
evalDef.setup
|
|
1384
1464
|
);
|
|
1385
1465
|
await triggerEvalWorkflow(
|
|
1386
|
-
|
|
1466
|
+
getBpClient(),
|
|
1387
1467
|
{
|
|
1388
1468
|
userId: session.userId,
|
|
1389
1469
|
...conversationId ? { conversationId } : {}
|
|
@@ -1419,18 +1499,18 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1419
1499
|
if (!turn.user && !turn.event) {
|
|
1420
1500
|
throw turnConfigError(`must have either 'user' or 'event'.`);
|
|
1421
1501
|
}
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1502
|
+
const turnLabelForEvent = turn.event ? `[event: ${turn.event.type}]` : turn.user;
|
|
1503
|
+
options.onProgress?.({
|
|
1424
1504
|
type: "turn_start",
|
|
1425
1505
|
evalName: evalDef.name,
|
|
1426
1506
|
evalIndex: options.evalIndex ?? 0,
|
|
1427
|
-
|
|
1507
|
+
turnNumber: i + 1,
|
|
1428
1508
|
totalTurns: evalDef.conversation.length,
|
|
1429
|
-
userMessage:
|
|
1509
|
+
userMessage: turnLabelForEvent
|
|
1430
1510
|
});
|
|
1431
1511
|
collector.startTurn();
|
|
1432
1512
|
if (turn.event) {
|
|
1433
|
-
await session.sendEvent(turn.event.payload);
|
|
1513
|
+
await session.sendEvent(turn.event.type, turn.event.payload ?? {});
|
|
1434
1514
|
} else {
|
|
1435
1515
|
await session.sendMessage(turn.user);
|
|
1436
1516
|
}
|
|
@@ -1460,6 +1540,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1460
1540
|
const turnData = transformSpans(turnSpans);
|
|
1461
1541
|
const botResponse = turnData.messages.join("\n");
|
|
1462
1542
|
const botDuration = turnData.handlerDuration;
|
|
1543
|
+
const turnLabel = turn.event ? `[event: ${turn.event.type}]` : turn.user;
|
|
1463
1544
|
const evalStart = Date.now();
|
|
1464
1545
|
let assertions = [];
|
|
1465
1546
|
const noResponse = !turn.expectSilence && turnData.messages.length === 0;
|
|
@@ -1514,7 +1595,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1514
1595
|
const turnPass = assertions.every((a) => a.pass);
|
|
1515
1596
|
const evalDuration = Date.now() - evalStart;
|
|
1516
1597
|
const turnReport = {
|
|
1517
|
-
|
|
1598
|
+
turnNumber: i + 1,
|
|
1518
1599
|
userMessage: turnLabel,
|
|
1519
1600
|
botResponse,
|
|
1520
1601
|
assertions,
|
|
@@ -1523,11 +1604,11 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1523
1604
|
evalDuration
|
|
1524
1605
|
};
|
|
1525
1606
|
turns.push(turnReport);
|
|
1526
|
-
|
|
1607
|
+
options.onProgress?.({
|
|
1527
1608
|
type: "turn_complete",
|
|
1528
1609
|
evalName: evalDef.name,
|
|
1529
1610
|
evalIndex: options.evalIndex ?? 0,
|
|
1530
|
-
|
|
1611
|
+
turnNumber: i + 1,
|
|
1531
1612
|
totalTurns: evalDef.conversation.length,
|
|
1532
1613
|
turnReport
|
|
1533
1614
|
});
|
|
@@ -1574,15 +1655,7 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1574
1655
|
} catch (err) {
|
|
1575
1656
|
const adkErr = isAdkError(err) ? err : void 0;
|
|
1576
1657
|
if (!adkErr || !adkErr.expected) {
|
|
1577
|
-
|
|
1578
|
-
const errStack = err.stack ?? "";
|
|
1579
|
-
logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${errMsg}`);
|
|
1580
|
-
logger.error(` stack: ${errStack}`);
|
|
1581
|
-
logger.error(` eval: ${evalDef.name}, turn: ${turns.length}/${evalDef.conversation.length}`);
|
|
1582
|
-
logger.error(` connection: botId=${connection.botId}`);
|
|
1583
|
-
logger.error(
|
|
1584
|
-
` spanSource: ${options.spanSource?.constructor?.name ?? "default"}, devServerUrl: ${options.devServerUrl ?? "n/a"}`
|
|
1585
|
-
);
|
|
1658
|
+
logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${err.message}`);
|
|
1586
1659
|
options.onException?.(err, { source: "evals", eval_name: evalDef.name });
|
|
1587
1660
|
}
|
|
1588
1661
|
return {
|
|
@@ -1600,15 +1673,9 @@ async function runEval(evalDef, connection, options = {}) {
|
|
|
1600
1673
|
}
|
|
1601
1674
|
async function runEvalSuite(config, filter) {
|
|
1602
1675
|
const start = Date.now();
|
|
1603
|
-
const runId =
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
allEvals = config.definitions;
|
|
1607
|
-
} else if (config.agentPath) {
|
|
1608
|
-
allEvals = await loadEvalsFromDir(`${config.agentPath}/evals`);
|
|
1609
|
-
} else {
|
|
1610
|
-
throw new Error("Either `definitions` or `agentPath` must be provided in EvalRunnerConfig");
|
|
1611
|
-
}
|
|
1676
|
+
const runId = randomUUID().replace(/-/g, "").slice(0, 26);
|
|
1677
|
+
const evalsDir = `${config.agentPath}/evals`;
|
|
1678
|
+
const allEvals = await loadEvalsFromDir(evalsDir);
|
|
1612
1679
|
const evals = filterEvals(allEvals, filter);
|
|
1613
1680
|
if (evals.length === 0) {
|
|
1614
1681
|
return {
|
|
@@ -1622,26 +1689,38 @@ async function runEvalSuite(config, filter) {
|
|
|
1622
1689
|
...filter !== void 0 ? { filter } : {}
|
|
1623
1690
|
};
|
|
1624
1691
|
}
|
|
1625
|
-
await initLLMJudge(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1692
|
+
await initLLMJudge(
|
|
1693
|
+
{
|
|
1694
|
+
token: config.credentials.token,
|
|
1695
|
+
apiUrl: config.credentials.apiUrl,
|
|
1696
|
+
botId: config.credentials.botId,
|
|
1697
|
+
...config.credentials.workspaceId !== void 0 ? { workspaceId: config.credentials.workspaceId } : {}
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
|
|
1701
|
+
...config.logger !== void 0 ? { logger: config.logger } : {}
|
|
1702
|
+
}
|
|
1703
|
+
);
|
|
1704
|
+
let webhookId = config.credentials.webhookId;
|
|
1705
|
+
if (!webhookId) {
|
|
1706
|
+
webhookId = await discoverWebhookId(config.credentials.botId, config.credentials.token, config.credentials.apiUrl);
|
|
1707
|
+
}
|
|
1629
1708
|
const connection = {
|
|
1630
|
-
|
|
1631
|
-
botId: config.botId
|
|
1709
|
+
webhookId,
|
|
1710
|
+
botId: config.credentials.botId,
|
|
1711
|
+
token: config.credentials.token,
|
|
1712
|
+
apiUrl: config.credentials.apiUrl
|
|
1632
1713
|
};
|
|
1633
1714
|
const devServerUrl = config.devServerUrl || DEFAULT_DEV_SERVER_URL;
|
|
1634
1715
|
const devServerHeaders = config.devServerHeaders ?? {};
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
await assertTraceStreamReadable(devServerUrl, devServerHeaders);
|
|
1638
|
-
}
|
|
1716
|
+
await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
|
|
1717
|
+
await assertTraceStreamReadable(devServerUrl, devServerHeaders);
|
|
1639
1718
|
const reports = [];
|
|
1640
|
-
|
|
1719
|
+
config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
|
|
1641
1720
|
for (let i = 0; i < evals.length; i++) {
|
|
1642
1721
|
if (config.signal?.aborted) break;
|
|
1643
1722
|
const evalDef = evals[i];
|
|
1644
|
-
|
|
1723
|
+
config.onProgress?.({
|
|
1645
1724
|
type: "eval_start",
|
|
1646
1725
|
evalName: evalDef.name,
|
|
1647
1726
|
index: i,
|
|
@@ -1657,13 +1736,10 @@ async function runEvalSuite(config, filter) {
|
|
|
1657
1736
|
...config.signal !== void 0 ? { signal: config.signal } : {},
|
|
1658
1737
|
...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {},
|
|
1659
1738
|
...config.logger !== void 0 ? { logger: config.logger } : {},
|
|
1660
|
-
...config.onException !== void 0 ? { onException: config.onException } : {}
|
|
1661
|
-
...config.createSpanSource !== void 0 ? { spanSource: config.createSpanSource() } : {},
|
|
1662
|
-
...config.chatWebhookId !== void 0 ? { chatWebhookId: config.chatWebhookId } : {},
|
|
1663
|
-
...config.chatBaseUrl !== void 0 ? { chatBaseUrl: config.chatBaseUrl } : {}
|
|
1739
|
+
...config.onException !== void 0 ? { onException: config.onException } : {}
|
|
1664
1740
|
});
|
|
1665
1741
|
reports.push(report);
|
|
1666
|
-
|
|
1742
|
+
config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
|
|
1667
1743
|
}
|
|
1668
1744
|
const runReport = {
|
|
1669
1745
|
id: runId,
|
|
@@ -1678,7 +1754,7 @@ async function runEvalSuite(config, filter) {
|
|
|
1678
1754
|
if (config.signal?.aborted) {
|
|
1679
1755
|
runReport.aborted = true;
|
|
1680
1756
|
}
|
|
1681
|
-
|
|
1757
|
+
config.onProgress?.({ type: "suite_complete", report: runReport });
|
|
1682
1758
|
return runReport;
|
|
1683
1759
|
}
|
|
1684
1760
|
export {
|