@botpress/evals 1.18.3
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 +50 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +123 -0
- package/dist/client.js.map +7 -0
- package/dist/definition.d.ts +113 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +25 -0
- package/dist/definition.js.map +7 -0
- package/dist/graders/index.d.ts +12 -0
- package/dist/graders/index.d.ts.map +1 -0
- package/dist/graders/index.js +466 -0
- package/dist/graders/index.js.map +7 -0
- package/dist/graders/llm.d.ts +23 -0
- package/dist/graders/llm.d.ts.map +1 -0
- package/dist/graders/llm.js +125 -0
- package/dist/graders/llm.js.map +7 -0
- package/dist/graders/match.d.ts +11 -0
- package/dist/graders/match.d.ts.map +1 -0
- package/dist/graders/match.js +48 -0
- package/dist/graders/match.js.map +7 -0
- package/dist/graders/outcome.d.ts +11 -0
- package/dist/graders/outcome.d.ts.map +1 -0
- package/dist/graders/outcome.js +148 -0
- package/dist/graders/outcome.js.map +7 -0
- package/dist/graders/response.d.ts +10 -0
- package/dist/graders/response.d.ts.map +1 -0
- package/dist/graders/response.js +155 -0
- package/dist/graders/response.js.map +7 -0
- package/dist/graders/state.d.ts +16 -0
- package/dist/graders/state.d.ts.map +1 -0
- package/dist/graders/state.js +101 -0
- package/dist/graders/state.js.map +7 -0
- package/dist/graders/timing.d.ts +7 -0
- package/dist/graders/timing.d.ts.map +1 -0
- package/dist/graders/timing.js +63 -0
- package/dist/graders/timing.js.map +7 -0
- package/dist/graders/tools.d.ts +7 -0
- package/dist/graders/tools.d.ts.map +1 -0
- package/dist/graders/tools.js +122 -0
- package/dist/graders/tools.js.map +7 -0
- package/dist/graders/workflow.d.ts +7 -0
- package/dist/graders/workflow.d.ts.map +1 -0
- package/dist/graders/workflow.js +34 -0
- package/dist/graders/workflow.js.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +7 -0
- package/dist/loader.d.ts +11 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +114 -0
- package/dist/loader.js.map +7 -0
- package/dist/runner.d.ts +25 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +1536 -0
- package/dist/runner.js.map +7 -0
- package/dist/sse-collector.d.ts +148 -0
- package/dist/sse-collector.d.ts.map +1 -0
- package/dist/sse-collector.js +386 -0
- package/dist/sse-collector.js.map +7 -0
- package/dist/trace.d.ts +56 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +1 -0
- package/dist/trace.js.map +7 -0
- package/dist/transformer.d.ts +18 -0
- package/dist/transformer.d.ts.map +1 -0
- package/dist/transformer.js +101 -0
- package/dist/transformer.js.map +7 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +7 -0
- package/package.json +99 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/graders/llm.ts
|
|
2
|
+
import { Client } from "@botpress/client";
|
|
3
|
+
import { Cognitive } from "@botpress/cognitive";
|
|
4
|
+
var DEFAULT_PASS_THRESHOLD = 3;
|
|
5
|
+
function buildJudgeSystemPrompt(passThreshold) {
|
|
6
|
+
return `You are an evaluation judge for a chatbot. You will be given:
|
|
7
|
+
- The user's message
|
|
8
|
+
- The bot's response
|
|
9
|
+
- Grading criteria
|
|
10
|
+
|
|
11
|
+
Your job is to determine if the bot's response meets the criteria.
|
|
12
|
+
|
|
13
|
+
Respond with a JSON object:
|
|
14
|
+
{
|
|
15
|
+
"pass": true/false,
|
|
16
|
+
"score": 1-5,
|
|
17
|
+
"reason": "brief explanation"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Scoring guide:
|
|
21
|
+
- 5: Fully meets criteria, excellent response
|
|
22
|
+
- 4: Mostly meets criteria, minor issues
|
|
23
|
+
- 3: Partially meets criteria, notable gaps
|
|
24
|
+
- 2: Barely meets criteria, significant issues
|
|
25
|
+
- 1: Does not meet criteria
|
|
26
|
+
|
|
27
|
+
A score of ${passThreshold} or above is a pass.`;
|
|
28
|
+
}
|
|
29
|
+
var MODEL_ALIASES = ["fast", "best"];
|
|
30
|
+
var _cognitive = null;
|
|
31
|
+
var _availableModels = [];
|
|
32
|
+
var _judgeModel = "fast";
|
|
33
|
+
function _isModelValid(model) {
|
|
34
|
+
if (MODEL_ALIASES.includes(model)) return true;
|
|
35
|
+
if (_availableModels.length === 0) return false;
|
|
36
|
+
return _availableModels.includes(model);
|
|
37
|
+
}
|
|
38
|
+
async function initLLMJudge(credentials, options) {
|
|
39
|
+
const log = options?.logger ?? console;
|
|
40
|
+
const client = new Client({
|
|
41
|
+
token: credentials.token,
|
|
42
|
+
apiUrl: credentials.apiUrl,
|
|
43
|
+
botId: credentials.botId,
|
|
44
|
+
...credentials.workspaceId !== void 0 ? { workspaceId: credentials.workspaceId } : {}
|
|
45
|
+
});
|
|
46
|
+
_cognitive = new Cognitive({ client, __experimental_beta: true });
|
|
47
|
+
try {
|
|
48
|
+
_availableModels = Array.from((await _cognitive.fetchRemoteModels()).keys());
|
|
49
|
+
} catch (err) {
|
|
50
|
+
log.warn(`Failed to fetch available models: ${err.message}. LLM judge will be unavailable.`);
|
|
51
|
+
_availableModels = [];
|
|
52
|
+
}
|
|
53
|
+
_judgeModel = options?.model ?? "fast";
|
|
54
|
+
if (_availableModels.length === 0) {
|
|
55
|
+
log.warn("No available Cognitive models: cannot validate judge model.");
|
|
56
|
+
} else if (!_isModelValid(_judgeModel)) {
|
|
57
|
+
log.warn(`Configured LLM judge model "${_judgeModel}" is invalid. Run "adk models" to list available models.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function gradeLLMJudge(botResponse, criteria, context) {
|
|
61
|
+
const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD));
|
|
62
|
+
try {
|
|
63
|
+
if (!_cognitive) {
|
|
64
|
+
return {
|
|
65
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
66
|
+
pass: true,
|
|
67
|
+
expected: criteria,
|
|
68
|
+
actual: "SKIPPED \u2014 LLM judge unavailable: no credentials configured"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!_isModelValid(_judgeModel)) {
|
|
72
|
+
return {
|
|
73
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
74
|
+
pass: false,
|
|
75
|
+
expected: criteria,
|
|
76
|
+
actual: "FAILED \u2014 invalid LLM judge model"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const { output } = await _cognitive.generateContent({
|
|
80
|
+
model: _judgeModel,
|
|
81
|
+
temperature: 0,
|
|
82
|
+
responseFormat: "json_object",
|
|
83
|
+
systemPrompt: buildJudgeSystemPrompt(threshold),
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: "user",
|
|
87
|
+
content: `User message: ${context.userMessage}
|
|
88
|
+
|
|
89
|
+
Bot response: ${botResponse}
|
|
90
|
+
|
|
91
|
+
Criteria: ${criteria}`
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
const rawContent = output?.choices?.[0]?.content;
|
|
96
|
+
const content = typeof rawContent === "string" ? rawContent : null;
|
|
97
|
+
if (!content) {
|
|
98
|
+
return {
|
|
99
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
100
|
+
pass: true,
|
|
101
|
+
expected: criteria,
|
|
102
|
+
actual: "SKIPPED \u2014 LLM judge returned empty response"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const verdict = JSON.parse(content);
|
|
106
|
+
return {
|
|
107
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
108
|
+
pass: verdict.score >= threshold,
|
|
109
|
+
expected: criteria,
|
|
110
|
+
actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`
|
|
111
|
+
};
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
115
|
+
pass: true,
|
|
116
|
+
expected: criteria,
|
|
117
|
+
actual: `SKIPPED \u2014 LLM judge unavailable: ${err.message}`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
gradeLLMJudge,
|
|
123
|
+
initLLMJudge
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=llm.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/graders/llm.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LLM judge grader using @botpress/cognitive.\n * Replaces the OpenAI-based implementation from the standalone project.\n */\n\nimport { Client } from '@botpress/client'\nimport { Cognitive } from '@botpress/cognitive'\nimport type { EvalLogger, GraderResult } from '../types'\n\nconst DEFAULT_PASS_THRESHOLD = 3\n\nfunction buildJudgeSystemPrompt(passThreshold: number): string {\n return `You are an evaluation judge for a chatbot. You will be given:\n- The user's message\n- The bot's response\n- Grading criteria\n\nYour job is to determine if the bot's response meets the criteria.\n\nRespond with a JSON object:\n{\n \"pass\": true/false,\n \"score\": 1-5,\n \"reason\": \"brief explanation\"\n}\n\nScoring guide:\n- 5: Fully meets criteria, excellent response\n- 4: Mostly meets criteria, minor issues\n- 3: Partially meets criteria, notable gaps\n- 2: Barely meets criteria, significant issues\n- 1: Does not meet criteria\n\nA score of ${passThreshold} or above is a pass.`\n}\n\nconst MODEL_ALIASES = ['fast', 'best']\n\nlet _cognitive: Cognitive | null = null\nlet _availableModels: string[] = []\nlet _judgeModel: string = 'fast'\n\nfunction _isModelValid(model: string): boolean {\n if (MODEL_ALIASES.includes(model)) return true\n if (_availableModels.length === 0) return false\n\n return _availableModels.includes(model)\n}\n\n/**\n * Initialize the LLM judge with explicit credentials.\n * Call this before running evals so the judge doesn't need env vars.\n */\nexport async function initLLMJudge(\n credentials: { token: string; apiUrl: string; botId: string; workspaceId?: string },\n options?: { model?: string; logger?: EvalLogger }\n) {\n const log = options?.logger ?? console\n\n const client = new Client({\n token: credentials.token,\n apiUrl: credentials.apiUrl,\n botId: credentials.botId,\n ...(credentials.workspaceId !== undefined ? { workspaceId: credentials.workspaceId } : {}),\n })\n _cognitive = new Cognitive({ client, __experimental_beta: true })\n\n try {\n _availableModels = Array.from((await _cognitive.fetchRemoteModels()).keys())\n } catch (err) {\n log.warn(`Failed to fetch available models: ${(err as Error).message}. LLM judge will be unavailable.`)\n _availableModels = []\n }\n\n _judgeModel = options?.model ?? 'fast'\n\n if (_availableModels.length === 0) {\n log.warn('No available Cognitive models: cannot validate judge model.')\n } else if (!_isModelValid(_judgeModel)) {\n log.warn(`Configured LLM judge model \"${_judgeModel}\" is invalid. Run \"adk models\" to list available models.`)\n }\n}\n\nexport async function gradeLLMJudge(\n botResponse: string,\n criteria: string,\n context: { userMessage: string; passThreshold?: number }\n): Promise<GraderResult> {\n const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD))\n\n try {\n if (!_cognitive) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: true,\n expected: criteria,\n actual: 'SKIPPED \u2014 LLM judge unavailable: no credentials configured',\n }\n }\n\n if (!_isModelValid(_judgeModel)) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: false,\n expected: criteria,\n actual: 'FAILED \u2014 invalid LLM judge model',\n }\n }\n\n type CognitiveModel = Exclude<Parameters<Cognitive['generateContent']>[0]['model'], undefined>\n\n const { output } = await _cognitive.generateContent({\n model: _judgeModel as CognitiveModel,\n temperature: 0,\n responseFormat: 'json_object',\n systemPrompt: buildJudgeSystemPrompt(threshold),\n messages: [\n {\n role: 'user',\n content: `User message: ${context.userMessage}\\n\\nBot response: ${botResponse}\\n\\nCriteria: ${criteria}`,\n },\n ],\n })\n\n const rawContent = output?.choices?.[0]?.content\n const content = typeof rawContent === 'string' ? rawContent : null\n if (!content) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: true,\n expected: criteria,\n actual: 'SKIPPED \u2014 LLM judge returned empty response',\n }\n }\n\n const verdict = JSON.parse(content) as {\n pass: boolean\n score: number\n reason: string\n }\n\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: verdict.score >= threshold,\n expected: criteria,\n actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`,\n }\n } catch (err) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: true,\n expected: criteria,\n actual: `SKIPPED \u2014 LLM judge unavailable: ${(err as Error).message}`,\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared match utilities for assertion grading.
|
|
3
|
+
* Used by tool, state, and table graders.
|
|
4
|
+
*/
|
|
5
|
+
import type { MatchOperator } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate a single match operator against a value.
|
|
8
|
+
*/
|
|
9
|
+
export declare function matchValue(operator: MatchOperator, actual: unknown): boolean;
|
|
10
|
+
export declare function operatorToString(operator: MatchOperator): string;
|
|
11
|
+
//# sourceMappingURL=match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/graders/match.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAuC5E;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAWhE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/graders/match.ts
|
|
2
|
+
function matchValue(operator, actual) {
|
|
3
|
+
if (typeof operator === "string") {
|
|
4
|
+
return String(actual) === operator;
|
|
5
|
+
}
|
|
6
|
+
if ("equals" in operator) {
|
|
7
|
+
return actual === operator.equals;
|
|
8
|
+
}
|
|
9
|
+
if ("contains" in operator) {
|
|
10
|
+
return String(actual).toLowerCase().includes(operator.contains.toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
if ("not_contains" in operator) {
|
|
13
|
+
return !String(actual).toLowerCase().includes(operator.not_contains.toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
if ("matches" in operator) {
|
|
16
|
+
return new RegExp(operator.matches, "i").test(String(actual));
|
|
17
|
+
}
|
|
18
|
+
if ("in" in operator) {
|
|
19
|
+
return operator.in.includes(actual);
|
|
20
|
+
}
|
|
21
|
+
if ("exists" in operator) {
|
|
22
|
+
return operator.exists ? actual !== void 0 && actual !== null : actual === void 0 || actual === null;
|
|
23
|
+
}
|
|
24
|
+
if ("gte" in operator) {
|
|
25
|
+
return Number(actual) >= operator.gte;
|
|
26
|
+
}
|
|
27
|
+
if ("lte" in operator) {
|
|
28
|
+
return Number(actual) <= operator.lte;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function operatorToString(operator) {
|
|
33
|
+
if (typeof operator === "string") return `equals "${operator}"`;
|
|
34
|
+
if ("equals" in operator) return `equals ${JSON.stringify(operator.equals)}`;
|
|
35
|
+
if ("contains" in operator) return `contains "${operator.contains}"`;
|
|
36
|
+
if ("not_contains" in operator) return `not_contains "${operator.not_contains}"`;
|
|
37
|
+
if ("matches" in operator) return `matches /${operator.matches}/`;
|
|
38
|
+
if ("in" in operator) return `in [${operator.in.map((v) => JSON.stringify(v)).join(", ")}]`;
|
|
39
|
+
if ("exists" in operator) return operator.exists ? "exists" : "does not exist";
|
|
40
|
+
if ("gte" in operator) return `>= ${operator.gte}`;
|
|
41
|
+
if ("lte" in operator) return `<= ${operator.lte}`;
|
|
42
|
+
return JSON.stringify(operator);
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
matchValue,
|
|
46
|
+
operatorToString
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=match.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/graders/match.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared match utilities for assertion grading.\n * Used by tool, state, and table graders.\n */\n\nimport type { MatchOperator } from '../types'\n\n/**\n * Evaluate a single match operator against a value.\n */\nexport function matchValue(operator: MatchOperator, actual: unknown): boolean {\n // Shorthand: string means exact match\n if (typeof operator === 'string') {\n return String(actual) === operator\n }\n\n if ('equals' in operator) {\n return actual === operator.equals\n }\n\n if ('contains' in operator) {\n return String(actual).toLowerCase().includes(operator.contains.toLowerCase())\n }\n\n if ('not_contains' in operator) {\n return !String(actual).toLowerCase().includes(operator.not_contains.toLowerCase())\n }\n\n if ('matches' in operator) {\n return new RegExp(operator.matches, 'i').test(String(actual))\n }\n\n if ('in' in operator) {\n return operator.in.includes(actual)\n }\n\n if ('exists' in operator) {\n return operator.exists ? actual !== undefined && actual !== null : actual === undefined || actual === null\n }\n\n if ('gte' in operator) {\n return Number(actual) >= operator.gte\n }\n\n if ('lte' in operator) {\n return Number(actual) <= operator.lte\n }\n\n return false\n}\n\nexport function operatorToString(operator: MatchOperator): string {\n if (typeof operator === 'string') return `equals \"${operator}\"`\n if ('equals' in operator) return `equals ${JSON.stringify(operator.equals)}`\n if ('contains' in operator) return `contains \"${operator.contains}\"`\n if ('not_contains' in operator) return `not_contains \"${operator.not_contains}\"`\n if ('matches' in operator) return `matches /${operator.matches}/`\n if ('in' in operator) return `in [${operator.in.map((v) => JSON.stringify(v)).join(', ')}]`\n if ('exists' in operator) return operator.exists ? 'exists' : 'does not exist'\n if ('gte' in operator) return `>= ${operator.gte}`\n if ('lte' in operator) return `<= ${operator.lte}`\n return JSON.stringify(operator)\n}\n"],
|
|
5
|
+
"mappings": ";AAUO,SAAS,WAAW,UAAyB,QAA0B;AAE5E,MAAI,OAAO,aAAa,UAAU;AAChC,WAAO,OAAO,MAAM,MAAM;AAAA,EAC5B;AAEA,MAAI,YAAY,UAAU;AACxB,WAAO,WAAW,SAAS;AAAA,EAC7B;AAEA,MAAI,cAAc,UAAU;AAC1B,WAAO,OAAO,MAAM,EAAE,YAAY,EAAE,SAAS,SAAS,SAAS,YAAY,CAAC;AAAA,EAC9E;AAEA,MAAI,kBAAkB,UAAU;AAC9B,WAAO,CAAC,OAAO,MAAM,EAAE,YAAY,EAAE,SAAS,SAAS,aAAa,YAAY,CAAC;AAAA,EACnF;AAEA,MAAI,aAAa,UAAU;AACzB,WAAO,IAAI,OAAO,SAAS,SAAS,GAAG,EAAE,KAAK,OAAO,MAAM,CAAC;AAAA,EAC9D;AAEA,MAAI,QAAQ,UAAU;AACpB,WAAO,SAAS,GAAG,SAAS,MAAM;AAAA,EACpC;AAEA,MAAI,YAAY,UAAU;AACxB,WAAO,SAAS,SAAS,WAAW,UAAa,WAAW,OAAO,WAAW,UAAa,WAAW;AAAA,EACxG;AAEA,MAAI,SAAS,UAAU;AACrB,WAAO,OAAO,MAAM,KAAK,SAAS;AAAA,EACpC;AAEA,MAAI,SAAS,UAAU;AACrB,WAAO,OAAO,MAAM,KAAK,SAAS;AAAA,EACpC;AAEA,SAAO;AACT;AAEO,SAAS,iBAAiB,UAAiC;AAChE,MAAI,OAAO,aAAa,SAAU,QAAO,WAAW,QAAQ;AAC5D,MAAI,YAAY,SAAU,QAAO,UAAU,KAAK,UAAU,SAAS,MAAM,CAAC;AAC1E,MAAI,cAAc,SAAU,QAAO,aAAa,SAAS,QAAQ;AACjE,MAAI,kBAAkB,SAAU,QAAO,iBAAiB,SAAS,YAAY;AAC7E,MAAI,aAAa,SAAU,QAAO,YAAY,SAAS,OAAO;AAC9D,MAAI,QAAQ,SAAU,QAAO,OAAO,SAAS,GAAG,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AACxF,MAAI,YAAY,SAAU,QAAO,SAAS,SAAS,WAAW;AAC9D,MAAI,SAAS,SAAU,QAAO,MAAM,SAAS,GAAG;AAChD,MAAI,SAAS,SAAU,QAAO,MAAM,SAAS,GAAG;AAChD,SAAO,KAAK,UAAU,QAAQ;AAChC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome assertion orchestrator.
|
|
3
|
+
* Runs state and workflow graders after all conversation turns complete.
|
|
4
|
+
*/
|
|
5
|
+
import type { EvalDefinition, GraderResult, StateMutation, WorkflowSpan } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Grade all outcome assertions after the conversation completes.
|
|
8
|
+
* Takes transformer output (StateMutation[], WorkflowSpan[]) — no raw spans, no API calls.
|
|
9
|
+
*/
|
|
10
|
+
export declare function gradeOutcome(mutations: StateMutation[], evalDef: EvalDefinition, workflowSpans: WorkflowSpan[]): GraderResult[];
|
|
11
|
+
//# sourceMappingURL=outcome.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.d.ts","sourceRoot":"","sources":["../../src/graders/outcome.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAIzF;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,aAAa,EAAE,EAC1B,OAAO,EAAE,cAAc,EACvB,aAAa,EAAE,YAAY,EAAE,GAC5B,YAAY,EAAE,CAiBhB"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// src/graders/state.ts
|
|
2
|
+
function parseStatePath(path) {
|
|
3
|
+
const dot = path.indexOf(".");
|
|
4
|
+
if (dot === -1) {
|
|
5
|
+
throw new Error(`Invalid state path "${path}" \u2014 expected "type.field" format`);
|
|
6
|
+
}
|
|
7
|
+
const type = path.slice(0, dot);
|
|
8
|
+
const field = path.slice(dot + 1);
|
|
9
|
+
if (type !== "bot" && type !== "user" && type !== "conversation") {
|
|
10
|
+
throw new Error(`Unknown state type "${type}" in path "${path}" \u2014 expected bot, user, or conversation`);
|
|
11
|
+
}
|
|
12
|
+
return { type, field };
|
|
13
|
+
}
|
|
14
|
+
function gradeState(mutations, assertions) {
|
|
15
|
+
const results = [];
|
|
16
|
+
for (const assertion of assertions) {
|
|
17
|
+
const parsed = parseStatePath(assertion.path);
|
|
18
|
+
const matchingMutations = mutations.filter((m) => m.type === parsed.type);
|
|
19
|
+
if (assertion.equals !== void 0) {
|
|
20
|
+
if (matchingMutations.length === 0) {
|
|
21
|
+
results.push({
|
|
22
|
+
assertion: `state: ${assertion.path} equals`,
|
|
23
|
+
pass: false,
|
|
24
|
+
expected: JSON.stringify(assertion.equals),
|
|
25
|
+
actual: `No state.save span found for "${parsed.type}" \u2014 the bot did not mutate this state during the turn. If this state was seeded via setup.state, assert on the tool/action that reads it, or use a conversation turn that triggers a state write.`
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const lastMutation = matchingMutations[matchingMutations.length - 1];
|
|
30
|
+
if (lastMutation.swappedToFile) {
|
|
31
|
+
results.push({
|
|
32
|
+
assertion: `state: ${assertion.path} equals`,
|
|
33
|
+
pass: false,
|
|
34
|
+
expected: JSON.stringify(assertion.equals),
|
|
35
|
+
actual: "State was too large for trace-based assertion (swapped to file)"
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const currentState = lastMutation.current;
|
|
40
|
+
const actualValue = currentState ? currentState[parsed.field] : void 0;
|
|
41
|
+
const pass = deepEqual(actualValue, assertion.equals);
|
|
42
|
+
results.push({
|
|
43
|
+
assertion: `state: ${assertion.path} equals`,
|
|
44
|
+
pass,
|
|
45
|
+
expected: JSON.stringify(assertion.equals),
|
|
46
|
+
actual: JSON.stringify(actualValue)
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (assertion.changed !== void 0) {
|
|
50
|
+
if (matchingMutations.length === 0) {
|
|
51
|
+
results.push({
|
|
52
|
+
assertion: `state: ${assertion.path} ${assertion.changed ? "changed" : "unchanged"}`,
|
|
53
|
+
pass: !assertion.changed,
|
|
54
|
+
expected: assertion.changed ? `Field "${parsed.field}" changed in a "${parsed.type}" state mutation` : `Field "${parsed.field}" unchanged`,
|
|
55
|
+
actual: 'No state mutation found for type "' + parsed.type + '"'
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const hasSwapped = matchingMutations.some((m) => m.swappedToFile);
|
|
60
|
+
if (hasSwapped) {
|
|
61
|
+
results.push({
|
|
62
|
+
assertion: `state: ${assertion.path} ${assertion.changed ? "changed" : "unchanged"}`,
|
|
63
|
+
pass: false,
|
|
64
|
+
expected: assertion.changed ? `Field "${parsed.field}" changed in a "${parsed.type}" state mutation` : `Field "${parsed.field}" unchanged`,
|
|
65
|
+
actual: "State was too large for trace-based assertion (swapped to file)"
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const fieldChanged = matchingMutations.some((m) => m.changedKeys.includes(parsed.field));
|
|
70
|
+
const pass = assertion.changed ? fieldChanged : !fieldChanged;
|
|
71
|
+
results.push({
|
|
72
|
+
assertion: `state: ${assertion.path} ${assertion.changed ? "changed" : "unchanged"}`,
|
|
73
|
+
pass,
|
|
74
|
+
expected: assertion.changed ? `Field "${parsed.field}" changed in a "${parsed.type}" state mutation` : `Field "${parsed.field}" unchanged`,
|
|
75
|
+
actual: fieldChanged ? `Field "${parsed.field}" was changed` : `Field "${parsed.field}" was not changed`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
function deepEqual(a, b) {
|
|
82
|
+
if (a === b) return true;
|
|
83
|
+
if (a === null || b === null) return false;
|
|
84
|
+
if (typeof a !== typeof b) return false;
|
|
85
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
86
|
+
if (a.length !== b.length) return false;
|
|
87
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
90
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
91
|
+
const keysA = Object.keys(a);
|
|
92
|
+
const keysB = Object.keys(b);
|
|
93
|
+
if (keysA.length !== keysB.length) return false;
|
|
94
|
+
return keysA.every((k) => deepEqual(a[k], b[k]));
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/graders/workflow.ts
|
|
100
|
+
function gradeWorkflows(workflows, assertions) {
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const assertion of assertions) {
|
|
103
|
+
const matching = workflows.filter((wf) => wf.name === assertion.name);
|
|
104
|
+
if (assertion.entered !== void 0) {
|
|
105
|
+
const wasEntered = matching.length > 0;
|
|
106
|
+
const pass = assertion.entered ? wasEntered : !wasEntered;
|
|
107
|
+
results.push({
|
|
108
|
+
assertion: `workflow: ${assertion.name} ${assertion.entered ? "entered" : "not entered"}`,
|
|
109
|
+
pass,
|
|
110
|
+
expected: assertion.entered ? `Workflow "${assertion.name}" was entered` : `Workflow "${assertion.name}" was not entered`,
|
|
111
|
+
actual: wasEntered ? `Found ${matching.length} workflow span(s)` : `No workflow spans found`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (assertion.completed !== void 0) {
|
|
115
|
+
const didComplete = matching.some(
|
|
116
|
+
(wf) => wf.statusFinal === "completed" || wf.status === "ok" || wf.status === "error"
|
|
117
|
+
);
|
|
118
|
+
const pass = assertion.completed ? didComplete : !didComplete;
|
|
119
|
+
results.push({
|
|
120
|
+
assertion: `workflow: ${assertion.name} ${assertion.completed ? "completed" : "not completed"}`,
|
|
121
|
+
pass,
|
|
122
|
+
expected: assertion.completed ? `Workflow "${assertion.name}" completed` : `Workflow "${assertion.name}" did not complete`,
|
|
123
|
+
actual: didComplete ? `Completed` : `Not completed`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/graders/outcome.ts
|
|
131
|
+
function gradeOutcome(mutations, evalDef, workflowSpans) {
|
|
132
|
+
const outcome = evalDef.outcome;
|
|
133
|
+
if (!outcome) return [];
|
|
134
|
+
const results = [];
|
|
135
|
+
if (outcome.state && outcome.state.length > 0) {
|
|
136
|
+
const stateResults = gradeState(mutations, outcome.state);
|
|
137
|
+
results.push(...stateResults);
|
|
138
|
+
}
|
|
139
|
+
if (outcome.workflow && outcome.workflow.length > 0) {
|
|
140
|
+
const workflowResults = gradeWorkflows(workflowSpans, outcome.workflow);
|
|
141
|
+
results.push(...workflowResults);
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
gradeOutcome
|
|
147
|
+
};
|
|
148
|
+
//# sourceMappingURL=outcome.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/graders/state.ts", "../../src/graders/workflow.ts", "../../src/graders/outcome.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * State assertion graders.\n * Pure functions that grade state assertions against StateMutation[] from the transformer.\n * No API calls, no retries, no BpClient.\n */\n\nimport type { StateMutation, StateAssertion, GraderResult } from '../types'\n\n/**\n * Parse a state path like \"conversation.lastCity\" into its components.\n * bot.* -> type: \"bot\", field: \"*\"\n * user.* -> type: \"user\", field: \"*\"\n * conversation.* -> type: \"conversation\", field: \"*\"\n */\nfunction parseStatePath(path: string): { type: string; field: string } {\n const dot = path.indexOf('.')\n if (dot === -1) {\n throw new Error(`Invalid state path \"${path}\" \u2014 expected \"type.field\" format`)\n }\n\n const type = path.slice(0, dot)\n const field = path.slice(dot + 1)\n\n if (type !== 'bot' && type !== 'user' && type !== 'conversation') {\n throw new Error(`Unknown state type \"${type}\" in path \"${path}\" \u2014 expected bot, user, or conversation`)\n }\n\n return { type, field }\n}\n\n/**\n * Grade state assertions using StateMutation[] from the transformer.\n *\n * - `equals`: find the LAST mutation matching the state type, extract field from `current`, deep-compare\n * - `changed: true`: check if the field appears in ANY mutation's `changedKeys` for that state type\n * - `changed: false`: check that the field appears in NO mutation's `changedKeys`\n * - Swapped state: when `swappedToFile` is true, current/previous are undefined \u2014 report a clear message\n */\nexport function gradeState(mutations: StateMutation[], assertions: StateAssertion[]): GraderResult[] {\n const results: GraderResult[] = []\n\n for (const assertion of assertions) {\n const parsed = parseStatePath(assertion.path)\n const matchingMutations = mutations.filter((m) => m.type === parsed.type)\n\n if (assertion.equals !== undefined) {\n if (matchingMutations.length === 0) {\n results.push({\n assertion: `state: ${assertion.path} equals`,\n pass: false,\n expected: JSON.stringify(assertion.equals),\n actual:\n `No state.save span found for \"${parsed.type}\" \u2014 the bot did not mutate this state during the turn. ` +\n 'If this state was seeded via setup.state, assert on the tool/action that reads it, or use a conversation turn that triggers a state write.',\n })\n continue\n }\n\n const lastMutation = matchingMutations[matchingMutations.length - 1]!\n\n if (lastMutation.swappedToFile) {\n results.push({\n assertion: `state: ${assertion.path} equals`,\n pass: false,\n expected: JSON.stringify(assertion.equals),\n actual: 'State was too large for trace-based assertion (swapped to file)',\n })\n continue\n }\n\n const currentState = lastMutation.current as Record<string, unknown> | undefined\n const actualValue = currentState ? currentState[parsed.field] : undefined\n const pass = deepEqual(actualValue, assertion.equals)\n\n results.push({\n assertion: `state: ${assertion.path} equals`,\n pass,\n expected: JSON.stringify(assertion.equals),\n actual: JSON.stringify(actualValue),\n })\n }\n\n if (assertion.changed !== undefined) {\n if (matchingMutations.length === 0) {\n results.push({\n assertion: `state: ${assertion.path} ${assertion.changed ? 'changed' : 'unchanged'}`,\n pass: !assertion.changed,\n expected: assertion.changed\n ? `Field \"${parsed.field}\" changed in a \"${parsed.type}\" state mutation`\n : `Field \"${parsed.field}\" unchanged`,\n actual: 'No state mutation found for type \"' + parsed.type + '\"',\n })\n continue\n }\n\n // Check for swapped state in any matching mutation\n const hasSwapped = matchingMutations.some((m) => m.swappedToFile)\n if (hasSwapped) {\n results.push({\n assertion: `state: ${assertion.path} ${assertion.changed ? 'changed' : 'unchanged'}`,\n pass: false,\n expected: assertion.changed\n ? `Field \"${parsed.field}\" changed in a \"${parsed.type}\" state mutation`\n : `Field \"${parsed.field}\" unchanged`,\n actual: 'State was too large for trace-based assertion (swapped to file)',\n })\n continue\n }\n\n const fieldChanged = matchingMutations.some((m) => m.changedKeys.includes(parsed.field))\n const pass = assertion.changed ? fieldChanged : !fieldChanged\n\n results.push({\n assertion: `state: ${assertion.path} ${assertion.changed ? 'changed' : 'unchanged'}`,\n pass,\n expected: assertion.changed\n ? `Field \"${parsed.field}\" changed in a \"${parsed.type}\" state mutation`\n : `Field \"${parsed.field}\" unchanged`,\n actual: fieldChanged ? `Field \"${parsed.field}\" was changed` : `Field \"${parsed.field}\" was not changed`,\n })\n }\n }\n\n return results\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a === null || b === null) return false\n if (typeof a !== typeof b) return false\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((v, i) => deepEqual(v, b[i]))\n }\n\n if (Array.isArray(a) !== Array.isArray(b)) return false\n\n if (typeof a === 'object' && typeof b === 'object') {\n const keysA = Object.keys(a as object)\n const keysB = Object.keys(b as object)\n if (keysA.length !== keysB.length) return false\n return keysA.every((k) => deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]))\n }\n\n return false\n}\n", "/**\n * Workflow assertion graders.\n * Checks workflow execution via transformer-produced WorkflowSpan data.\n */\n\nimport type { WorkflowAssertion, WorkflowSpan, GraderResult } from '../types'\n\nexport function gradeWorkflows(workflows: WorkflowSpan[], assertions: WorkflowAssertion[]): GraderResult[] {\n const results: GraderResult[] = []\n\n for (const assertion of assertions) {\n const matching = workflows.filter((wf) => wf.name === assertion.name)\n\n // entered \u2014 was the workflow triggered at all?\n if (assertion.entered !== undefined) {\n const wasEntered = matching.length > 0\n const pass = assertion.entered ? wasEntered : !wasEntered\n\n results.push({\n assertion: `workflow: ${assertion.name} ${assertion.entered ? 'entered' : 'not entered'}`,\n pass,\n expected: assertion.entered\n ? `Workflow \"${assertion.name}\" was entered`\n : `Workflow \"${assertion.name}\" was not entered`,\n actual: wasEntered ? `Found ${matching.length} workflow span(s)` : `No workflow spans found`,\n })\n }\n\n // completed \u2014 did the workflow complete?\n if (assertion.completed !== undefined) {\n const didComplete = matching.some(\n (wf) => wf.statusFinal === 'completed' || wf.status === 'ok' || wf.status === 'error'\n )\n const pass = assertion.completed ? didComplete : !didComplete\n\n results.push({\n assertion: `workflow: ${assertion.name} ${assertion.completed ? 'completed' : 'not completed'}`,\n pass,\n expected: assertion.completed\n ? `Workflow \"${assertion.name}\" completed`\n : `Workflow \"${assertion.name}\" did not complete`,\n actual: didComplete ? `Completed` : `Not completed`,\n })\n }\n }\n\n return results\n}\n", "/**\n * Outcome assertion orchestrator.\n * Runs state and workflow graders after all conversation turns complete.\n */\n\nimport type { EvalDefinition, GraderResult, StateMutation, WorkflowSpan } from '../types'\nimport { gradeState } from './state'\nimport { gradeWorkflows } from './workflow'\n\n/**\n * Grade all outcome assertions after the conversation completes.\n * Takes transformer output (StateMutation[], WorkflowSpan[]) \u2014 no raw spans, no API calls.\n */\nexport function gradeOutcome(\n mutations: StateMutation[],\n evalDef: EvalDefinition,\n workflowSpans: WorkflowSpan[]\n): GraderResult[] {\n const outcome = evalDef.outcome\n if (!outcome) return []\n\n const results: GraderResult[] = []\n\n if (outcome.state && outcome.state.length > 0) {\n const stateResults = gradeState(mutations, outcome.state)\n results.push(...stateResults)\n }\n\n if (outcome.workflow && outcome.workflow.length > 0) {\n const workflowResults = gradeWorkflows(workflowSpans, outcome.workflow)\n results.push(...workflowResults)\n }\n\n return results\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response assertion graders.
|
|
3
|
+
* Checks bot response text against expected assertions.
|
|
4
|
+
*/
|
|
5
|
+
import type { ResponseAssertion, GraderResult } from '../types';
|
|
6
|
+
export declare function gradeResponse(botResponse: string, assertions: ResponseAssertion[], context: {
|
|
7
|
+
userMessage: string;
|
|
8
|
+
judgePassThreshold?: number;
|
|
9
|
+
}): Promise<GraderResult[]>;
|
|
10
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/graders/response.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAG/D,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D,OAAO,CAAC,YAAY,EAAE,CAAC,CAmEzB"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/graders/llm.ts
|
|
2
|
+
import { Client } from "@botpress/client";
|
|
3
|
+
import { Cognitive } from "@botpress/cognitive";
|
|
4
|
+
var DEFAULT_PASS_THRESHOLD = 3;
|
|
5
|
+
function buildJudgeSystemPrompt(passThreshold) {
|
|
6
|
+
return `You are an evaluation judge for a chatbot. You will be given:
|
|
7
|
+
- The user's message
|
|
8
|
+
- The bot's response
|
|
9
|
+
- Grading criteria
|
|
10
|
+
|
|
11
|
+
Your job is to determine if the bot's response meets the criteria.
|
|
12
|
+
|
|
13
|
+
Respond with a JSON object:
|
|
14
|
+
{
|
|
15
|
+
"pass": true/false,
|
|
16
|
+
"score": 1-5,
|
|
17
|
+
"reason": "brief explanation"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Scoring guide:
|
|
21
|
+
- 5: Fully meets criteria, excellent response
|
|
22
|
+
- 4: Mostly meets criteria, minor issues
|
|
23
|
+
- 3: Partially meets criteria, notable gaps
|
|
24
|
+
- 2: Barely meets criteria, significant issues
|
|
25
|
+
- 1: Does not meet criteria
|
|
26
|
+
|
|
27
|
+
A score of ${passThreshold} or above is a pass.`;
|
|
28
|
+
}
|
|
29
|
+
var MODEL_ALIASES = ["fast", "best"];
|
|
30
|
+
var _cognitive = null;
|
|
31
|
+
var _availableModels = [];
|
|
32
|
+
var _judgeModel = "fast";
|
|
33
|
+
function _isModelValid(model) {
|
|
34
|
+
if (MODEL_ALIASES.includes(model)) return true;
|
|
35
|
+
if (_availableModels.length === 0) return false;
|
|
36
|
+
return _availableModels.includes(model);
|
|
37
|
+
}
|
|
38
|
+
async function gradeLLMJudge(botResponse, criteria, context) {
|
|
39
|
+
const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD));
|
|
40
|
+
try {
|
|
41
|
+
if (!_cognitive) {
|
|
42
|
+
return {
|
|
43
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
44
|
+
pass: true,
|
|
45
|
+
expected: criteria,
|
|
46
|
+
actual: "SKIPPED \u2014 LLM judge unavailable: no credentials configured"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!_isModelValid(_judgeModel)) {
|
|
50
|
+
return {
|
|
51
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
52
|
+
pass: false,
|
|
53
|
+
expected: criteria,
|
|
54
|
+
actual: "FAILED \u2014 invalid LLM judge model"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const { output } = await _cognitive.generateContent({
|
|
58
|
+
model: _judgeModel,
|
|
59
|
+
temperature: 0,
|
|
60
|
+
responseFormat: "json_object",
|
|
61
|
+
systemPrompt: buildJudgeSystemPrompt(threshold),
|
|
62
|
+
messages: [
|
|
63
|
+
{
|
|
64
|
+
role: "user",
|
|
65
|
+
content: `User message: ${context.userMessage}
|
|
66
|
+
|
|
67
|
+
Bot response: ${botResponse}
|
|
68
|
+
|
|
69
|
+
Criteria: ${criteria}`
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
const rawContent = output?.choices?.[0]?.content;
|
|
74
|
+
const content = typeof rawContent === "string" ? rawContent : null;
|
|
75
|
+
if (!content) {
|
|
76
|
+
return {
|
|
77
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
78
|
+
pass: true,
|
|
79
|
+
expected: criteria,
|
|
80
|
+
actual: "SKIPPED \u2014 LLM judge returned empty response"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const verdict = JSON.parse(content);
|
|
84
|
+
return {
|
|
85
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
86
|
+
pass: verdict.score >= threshold,
|
|
87
|
+
expected: criteria,
|
|
88
|
+
actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return {
|
|
92
|
+
assertion: `llm_judge: "${criteria}"`,
|
|
93
|
+
pass: true,
|
|
94
|
+
expected: criteria,
|
|
95
|
+
actual: `SKIPPED \u2014 LLM judge unavailable: ${err.message}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/graders/response.ts
|
|
101
|
+
async function gradeResponse(botResponse, assertions, context) {
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const assertion of assertions) {
|
|
104
|
+
if ("contains" in assertion) {
|
|
105
|
+
const pass = botResponse.toLowerCase().includes(assertion.contains.toLowerCase());
|
|
106
|
+
results.push({
|
|
107
|
+
assertion: `contains "${assertion.contains}"`,
|
|
108
|
+
pass,
|
|
109
|
+
expected: `Response contains "${assertion.contains}"`,
|
|
110
|
+
actual: pass ? `Found in response` : `Not found in response`
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if ("not_contains" in assertion) {
|
|
115
|
+
const pass = !botResponse.toLowerCase().includes(assertion.not_contains.toLowerCase());
|
|
116
|
+
results.push({
|
|
117
|
+
assertion: `not_contains "${assertion.not_contains}"`,
|
|
118
|
+
pass,
|
|
119
|
+
expected: `Response does not contain "${assertion.not_contains}"`,
|
|
120
|
+
actual: pass ? `Not found in response` : `Found in response`
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if ("matches" in assertion) {
|
|
125
|
+
const regex = new RegExp(assertion.matches, "i");
|
|
126
|
+
const pass = regex.test(botResponse);
|
|
127
|
+
results.push({
|
|
128
|
+
assertion: `matches ${assertion.matches}`,
|
|
129
|
+
pass,
|
|
130
|
+
expected: `Response matches /${assertion.matches}/`,
|
|
131
|
+
actual: pass ? `Matched` : `No match`
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if ("llm_judge" in assertion) {
|
|
136
|
+
const result = await gradeLLMJudge(botResponse, assertion.llm_judge, {
|
|
137
|
+
userMessage: context.userMessage,
|
|
138
|
+
...context.judgePassThreshold !== void 0 ? { passThreshold: context.judgePassThreshold } : {}
|
|
139
|
+
});
|
|
140
|
+
results.push(result);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
results.push({
|
|
144
|
+
assertion: "unknown",
|
|
145
|
+
pass: false,
|
|
146
|
+
expected: "known assertion type",
|
|
147
|
+
actual: `Unknown assertion: ${JSON.stringify(assertion)}`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
gradeResponse
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/graders/llm.ts", "../../src/graders/response.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LLM judge grader using @botpress/cognitive.\n * Replaces the OpenAI-based implementation from the standalone project.\n */\n\nimport { Client } from '@botpress/client'\nimport { Cognitive } from '@botpress/cognitive'\nimport type { EvalLogger, GraderResult } from '../types'\n\nconst DEFAULT_PASS_THRESHOLD = 3\n\nfunction buildJudgeSystemPrompt(passThreshold: number): string {\n return `You are an evaluation judge for a chatbot. You will be given:\n- The user's message\n- The bot's response\n- Grading criteria\n\nYour job is to determine if the bot's response meets the criteria.\n\nRespond with a JSON object:\n{\n \"pass\": true/false,\n \"score\": 1-5,\n \"reason\": \"brief explanation\"\n}\n\nScoring guide:\n- 5: Fully meets criteria, excellent response\n- 4: Mostly meets criteria, minor issues\n- 3: Partially meets criteria, notable gaps\n- 2: Barely meets criteria, significant issues\n- 1: Does not meet criteria\n\nA score of ${passThreshold} or above is a pass.`\n}\n\nconst MODEL_ALIASES = ['fast', 'best']\n\nlet _cognitive: Cognitive | null = null\nlet _availableModels: string[] = []\nlet _judgeModel: string = 'fast'\n\nfunction _isModelValid(model: string): boolean {\n if (MODEL_ALIASES.includes(model)) return true\n if (_availableModels.length === 0) return false\n\n return _availableModels.includes(model)\n}\n\n/**\n * Initialize the LLM judge with explicit credentials.\n * Call this before running evals so the judge doesn't need env vars.\n */\nexport async function initLLMJudge(\n credentials: { token: string; apiUrl: string; botId: string; workspaceId?: string },\n options?: { model?: string; logger?: EvalLogger }\n) {\n const log = options?.logger ?? console\n\n const client = new Client({\n token: credentials.token,\n apiUrl: credentials.apiUrl,\n botId: credentials.botId,\n ...(credentials.workspaceId !== undefined ? { workspaceId: credentials.workspaceId } : {}),\n })\n _cognitive = new Cognitive({ client, __experimental_beta: true })\n\n try {\n _availableModels = Array.from((await _cognitive.fetchRemoteModels()).keys())\n } catch (err) {\n log.warn(`Failed to fetch available models: ${(err as Error).message}. LLM judge will be unavailable.`)\n _availableModels = []\n }\n\n _judgeModel = options?.model ?? 'fast'\n\n if (_availableModels.length === 0) {\n log.warn('No available Cognitive models: cannot validate judge model.')\n } else if (!_isModelValid(_judgeModel)) {\n log.warn(`Configured LLM judge model \"${_judgeModel}\" is invalid. Run \"adk models\" to list available models.`)\n }\n}\n\nexport async function gradeLLMJudge(\n botResponse: string,\n criteria: string,\n context: { userMessage: string; passThreshold?: number }\n): Promise<GraderResult> {\n const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD))\n\n try {\n if (!_cognitive) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: true,\n expected: criteria,\n actual: 'SKIPPED \u2014 LLM judge unavailable: no credentials configured',\n }\n }\n\n if (!_isModelValid(_judgeModel)) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: false,\n expected: criteria,\n actual: 'FAILED \u2014 invalid LLM judge model',\n }\n }\n\n type CognitiveModel = Exclude<Parameters<Cognitive['generateContent']>[0]['model'], undefined>\n\n const { output } = await _cognitive.generateContent({\n model: _judgeModel as CognitiveModel,\n temperature: 0,\n responseFormat: 'json_object',\n systemPrompt: buildJudgeSystemPrompt(threshold),\n messages: [\n {\n role: 'user',\n content: `User message: ${context.userMessage}\\n\\nBot response: ${botResponse}\\n\\nCriteria: ${criteria}`,\n },\n ],\n })\n\n const rawContent = output?.choices?.[0]?.content\n const content = typeof rawContent === 'string' ? rawContent : null\n if (!content) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: true,\n expected: criteria,\n actual: 'SKIPPED \u2014 LLM judge returned empty response',\n }\n }\n\n const verdict = JSON.parse(content) as {\n pass: boolean\n score: number\n reason: string\n }\n\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: verdict.score >= threshold,\n expected: criteria,\n actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`,\n }\n } catch (err) {\n return {\n assertion: `llm_judge: \"${criteria}\"`,\n pass: true,\n expected: criteria,\n actual: `SKIPPED \u2014 LLM judge unavailable: ${(err as Error).message}`,\n }\n }\n}\n", "/**\n * Response assertion graders.\n * Checks bot response text against expected assertions.\n */\n\nimport type { ResponseAssertion, GraderResult } from '../types'\nimport { gradeLLMJudge } from './llm'\n\nexport async function gradeResponse(\n botResponse: string,\n assertions: ResponseAssertion[],\n context: { userMessage: string; judgePassThreshold?: number }\n): Promise<GraderResult[]> {\n const results: GraderResult[] = []\n\n for (const assertion of assertions) {\n if ('contains' in assertion) {\n const pass = botResponse.toLowerCase().includes(assertion.contains.toLowerCase())\n results.push({\n assertion: `contains \"${assertion.contains}\"`,\n pass,\n expected: `Response contains \"${assertion.contains}\"`,\n actual: pass ? `Found in response` : `Not found in response`,\n })\n continue\n }\n\n if ('not_contains' in assertion) {\n const pass = !botResponse.toLowerCase().includes(assertion.not_contains.toLowerCase())\n results.push({\n assertion: `not_contains \"${assertion.not_contains}\"`,\n pass,\n expected: `Response does not contain \"${assertion.not_contains}\"`,\n actual: pass ? `Not found in response` : `Found in response`,\n })\n continue\n }\n\n if ('matches' in assertion) {\n const regex = new RegExp(assertion.matches, 'i')\n const pass = regex.test(botResponse)\n results.push({\n assertion: `matches ${assertion.matches}`,\n pass,\n expected: `Response matches /${assertion.matches}/`,\n actual: pass ? `Matched` : `No match`,\n })\n continue\n }\n\n if ('llm_judge' in assertion) {\n const result = await gradeLLMJudge(botResponse, assertion.llm_judge, {\n userMessage: context.userMessage,\n ...(context.judgePassThreshold !== undefined ? { passThreshold: context.judgePassThreshold } : {}),\n })\n results.push(result)\n continue\n }\n\n // TODO: uncomment this once we support it\n // if ('similar_to' in assertion) {\n // results.push({\n // assertion: `similar_to: \"${assertion.similar_to}\"`,\n // pass: true,\n // expected: assertion.similar_to,\n // actual: 'SKIPPED \u2014 similar_to not yet implemented',\n // })\n // continue\n // }\n\n results.push({\n assertion: 'unknown',\n pass: false,\n expected: 'known assertion type',\n actual: `Unknown assertion: ${JSON.stringify(assertion)}`,\n })\n }\n\n return results\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State assertion graders.
|
|
3
|
+
* Pure functions that grade state assertions against StateMutation[] from the transformer.
|
|
4
|
+
* No API calls, no retries, no BpClient.
|
|
5
|
+
*/
|
|
6
|
+
import type { StateMutation, StateAssertion, GraderResult } from '../types';
|
|
7
|
+
/**
|
|
8
|
+
* Grade state assertions using StateMutation[] from the transformer.
|
|
9
|
+
*
|
|
10
|
+
* - `equals`: find the LAST mutation matching the state type, extract field from `current`, deep-compare
|
|
11
|
+
* - `changed: true`: check if the field appears in ANY mutation's `changedKeys` for that state type
|
|
12
|
+
* - `changed: false`: check that the field appears in NO mutation's `changedKeys`
|
|
13
|
+
* - Swapped state: when `swappedToFile` is true, current/previous are undefined — report a clear message
|
|
14
|
+
*/
|
|
15
|
+
export declare function gradeState(mutations: StateMutation[], assertions: StateAssertion[]): GraderResult[];
|
|
16
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/graders/state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAwB3E;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,YAAY,EAAE,CAsFnG"}
|