@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.
Files changed (73) hide show
  1. package/dist/client.d.ts +50 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +123 -0
  4. package/dist/client.js.map +7 -0
  5. package/dist/definition.d.ts +113 -0
  6. package/dist/definition.d.ts.map +1 -0
  7. package/dist/definition.js +25 -0
  8. package/dist/definition.js.map +7 -0
  9. package/dist/graders/index.d.ts +12 -0
  10. package/dist/graders/index.d.ts.map +1 -0
  11. package/dist/graders/index.js +466 -0
  12. package/dist/graders/index.js.map +7 -0
  13. package/dist/graders/llm.d.ts +23 -0
  14. package/dist/graders/llm.d.ts.map +1 -0
  15. package/dist/graders/llm.js +125 -0
  16. package/dist/graders/llm.js.map +7 -0
  17. package/dist/graders/match.d.ts +11 -0
  18. package/dist/graders/match.d.ts.map +1 -0
  19. package/dist/graders/match.js +48 -0
  20. package/dist/graders/match.js.map +7 -0
  21. package/dist/graders/outcome.d.ts +11 -0
  22. package/dist/graders/outcome.d.ts.map +1 -0
  23. package/dist/graders/outcome.js +148 -0
  24. package/dist/graders/outcome.js.map +7 -0
  25. package/dist/graders/response.d.ts +10 -0
  26. package/dist/graders/response.d.ts.map +1 -0
  27. package/dist/graders/response.js +155 -0
  28. package/dist/graders/response.js.map +7 -0
  29. package/dist/graders/state.d.ts +16 -0
  30. package/dist/graders/state.d.ts.map +1 -0
  31. package/dist/graders/state.js +101 -0
  32. package/dist/graders/state.js.map +7 -0
  33. package/dist/graders/timing.d.ts +7 -0
  34. package/dist/graders/timing.d.ts.map +1 -0
  35. package/dist/graders/timing.js +63 -0
  36. package/dist/graders/timing.js.map +7 -0
  37. package/dist/graders/tools.d.ts +7 -0
  38. package/dist/graders/tools.d.ts.map +1 -0
  39. package/dist/graders/tools.js +122 -0
  40. package/dist/graders/tools.js.map +7 -0
  41. package/dist/graders/workflow.d.ts +7 -0
  42. package/dist/graders/workflow.d.ts.map +1 -0
  43. package/dist/graders/workflow.js +34 -0
  44. package/dist/graders/workflow.js.map +7 -0
  45. package/dist/index.d.ts +10 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +25 -0
  48. package/dist/index.js.map +7 -0
  49. package/dist/loader.d.ts +11 -0
  50. package/dist/loader.d.ts.map +1 -0
  51. package/dist/loader.js +114 -0
  52. package/dist/loader.js.map +7 -0
  53. package/dist/runner.d.ts +25 -0
  54. package/dist/runner.d.ts.map +1 -0
  55. package/dist/runner.js +1536 -0
  56. package/dist/runner.js.map +7 -0
  57. package/dist/sse-collector.d.ts +148 -0
  58. package/dist/sse-collector.d.ts.map +1 -0
  59. package/dist/sse-collector.js +386 -0
  60. package/dist/sse-collector.js.map +7 -0
  61. package/dist/trace.d.ts +56 -0
  62. package/dist/trace.d.ts.map +1 -0
  63. package/dist/trace.js +1 -0
  64. package/dist/trace.js.map +7 -0
  65. package/dist/transformer.d.ts +18 -0
  66. package/dist/transformer.d.ts.map +1 -0
  67. package/dist/transformer.js +101 -0
  68. package/dist/transformer.js.map +7 -0
  69. package/dist/types.d.ts +184 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +25 -0
  72. package/dist/types.js.map +7 -0
  73. package/package.json +99 -0
@@ -0,0 +1,101 @@
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
+ export {
99
+ gradeState
100
+ };
101
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/graders/state.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"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Timing assertion graders.
3
+ * Checks bot response duration against expected thresholds.
4
+ */
5
+ import type { TimingAssertion, GraderResult } from '../types';
6
+ export declare function gradeTiming(botDuration: number, assertions: TimingAssertion[]): GraderResult[];
7
+ //# sourceMappingURL=timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../src/graders/timing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAG7D,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,YAAY,EAAE,CAgB9F"}
@@ -0,0 +1,63 @@
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
+
45
+ // src/graders/timing.ts
46
+ function gradeTiming(botDuration, assertions) {
47
+ const results = [];
48
+ for (const assertion of assertions) {
49
+ const pass = matchValue(assertion.response_time, botDuration);
50
+ const expected = operatorToString(assertion.response_time);
51
+ results.push({
52
+ assertion: `response_time ${expected}`,
53
+ pass,
54
+ expected: `Response time ${expected}`,
55
+ actual: `${botDuration}ms`
56
+ });
57
+ }
58
+ return results;
59
+ }
60
+ export {
61
+ gradeTiming
62
+ };
63
+ //# sourceMappingURL=timing.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/graders/match.ts", "../../src/graders/timing.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", "/**\n * Timing assertion graders.\n * Checks bot response duration against expected thresholds.\n */\n\nimport type { TimingAssertion, GraderResult } from '../types'\nimport { matchValue, operatorToString } from './match'\n\nexport function gradeTiming(botDuration: number, assertions: TimingAssertion[]): GraderResult[] {\n const results: GraderResult[] = []\n\n for (const assertion of assertions) {\n const pass = matchValue(assertion.response_time, botDuration)\n const expected = operatorToString(assertion.response_time)\n\n results.push({\n assertion: `response_time ${expected}`,\n pass,\n expected: `Response time ${expected}`,\n actual: `${botDuration}ms`,\n })\n }\n\n return results\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;;;ACtDO,SAAS,YAAY,aAAqB,YAA+C;AAC9F,QAAM,UAA0B,CAAC;AAEjC,aAAW,aAAa,YAAY;AAClC,UAAM,OAAO,WAAW,UAAU,eAAe,WAAW;AAC5D,UAAM,WAAW,iBAAiB,UAAU,aAAa;AAEzD,YAAQ,KAAK;AAAA,MACX,WAAW,iBAAiB,QAAQ;AAAA,MACpC;AAAA,MACA,UAAU,iBAAiB,QAAQ;AAAA,MACnC,QAAQ,GAAG,WAAW;AAAA,IACxB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tool assertion graders.
3
+ * Checks tool calls from traces against expected assertions.
4
+ */
5
+ import type { ToolAssertion, ToolCall, GraderResult } from '../types';
6
+ export declare function gradeTools(toolCalls: ToolCall[], assertions: ToolAssertion[]): GraderResult[];
7
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/graders/tools.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGrE,wBAAgB,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,YAAY,EAAE,CA6F7F"}
@@ -0,0 +1,122 @@
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
+
45
+ // src/graders/tools.ts
46
+ function gradeTools(toolCalls, assertions) {
47
+ return assertions.map((assertion) => {
48
+ if ("called" in assertion && !("not_called" in assertion) && !("call_order" in assertion)) {
49
+ const matches = toolCalls.filter((tc) => tc.name === assertion.called);
50
+ const wasCalled = matches.length > 0;
51
+ if (!wasCalled) {
52
+ return {
53
+ assertion: `tool called: ${assertion.called}`,
54
+ pass: false,
55
+ expected: `${assertion.called} was called`,
56
+ actual: `Not called. Tools called: [${toolCalls.map((tc) => tc.name).join(", ") || "none"}]`
57
+ };
58
+ }
59
+ if (assertion.params) {
60
+ const paramResults = [];
61
+ for (const [key, operator] of Object.entries(assertion.params)) {
62
+ const anyMatch = matches.some((tc) => matchValue(operator, tc.input[key]));
63
+ paramResults.push({
64
+ key,
65
+ pass: anyMatch,
66
+ detail: anyMatch ? `matched` : `expected ${key} ${operatorToString(operator)}, got ${JSON.stringify(matches.map((tc) => tc.input[key]))}`
67
+ });
68
+ }
69
+ const allParamsPass = paramResults.every((p) => p.pass);
70
+ const failedParams = paramResults.filter((p) => !p.pass);
71
+ return {
72
+ assertion: `tool called: ${assertion.called} with params`,
73
+ pass: allParamsPass,
74
+ expected: `${assertion.called} called with ${Object.entries(assertion.params).map(([k, v]) => `${k} ${operatorToString(v)}`).join(", ")}`,
75
+ actual: allParamsPass ? `Matched` : failedParams.map((p) => p.detail).join("; ")
76
+ };
77
+ }
78
+ return {
79
+ assertion: `tool called: ${assertion.called}`,
80
+ pass: true,
81
+ expected: `${assertion.called} was called`,
82
+ actual: `Called ${matches.length} time(s)`
83
+ };
84
+ }
85
+ if ("not_called" in assertion) {
86
+ const wasCalled = toolCalls.some((tc) => tc.name === assertion.not_called);
87
+ return {
88
+ assertion: `tool not_called: ${assertion.not_called}`,
89
+ pass: !wasCalled,
90
+ expected: `${assertion.not_called} was NOT called`,
91
+ actual: wasCalled ? `Was called` : `Not called`
92
+ };
93
+ }
94
+ if ("call_order" in assertion) {
95
+ const calledNames = toolCalls.map((tc) => tc.name);
96
+ const expectedOrder = assertion.call_order;
97
+ let cursor = 0;
98
+ for (const name of calledNames) {
99
+ if (cursor < expectedOrder.length && name === expectedOrder[cursor]) {
100
+ cursor++;
101
+ }
102
+ }
103
+ const inOrder = cursor === expectedOrder.length;
104
+ return {
105
+ assertion: `call_order: [${expectedOrder.join(" \u2192 ")}]`,
106
+ pass: inOrder,
107
+ expected: `Tools called in order: [${expectedOrder.join(" \u2192 ")}]`,
108
+ actual: `Actual order: [${calledNames.join(" \u2192 ") || "none"}]`
109
+ };
110
+ }
111
+ return {
112
+ assertion: "unknown tool assertion",
113
+ pass: false,
114
+ expected: "known assertion type",
115
+ actual: `Unknown: ${JSON.stringify(assertion)}`
116
+ };
117
+ });
118
+ }
119
+ export {
120
+ gradeTools
121
+ };
122
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/graders/match.ts", "../../src/graders/tools.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", "/**\n * Tool assertion graders.\n * Checks tool calls from traces against expected assertions.\n */\n\nimport type { ToolAssertion, ToolCall, GraderResult } from '../types'\nimport { matchValue, operatorToString } from './match'\n\nexport function gradeTools(toolCalls: ToolCall[], assertions: ToolAssertion[]): GraderResult[] {\n return assertions.map((assertion) => {\n // --- called ---\n if ('called' in assertion && !('not_called' in assertion) && !('call_order' in assertion)) {\n const matches = toolCalls.filter((tc) => tc.name === assertion.called)\n const wasCalled = matches.length > 0\n\n if (!wasCalled) {\n return {\n assertion: `tool called: ${assertion.called}`,\n pass: false,\n expected: `${assertion.called} was called`,\n actual: `Not called. Tools called: [${toolCalls.map((tc) => tc.name).join(', ') || 'none'}]`,\n }\n }\n\n // Check params if specified\n if (assertion.params) {\n const paramResults: { key: string; pass: boolean; detail: string }[] = []\n\n for (const [key, operator] of Object.entries(assertion.params)) {\n // Check across all matching calls \u2014 pass if any call matches\n const anyMatch = matches.some((tc) => matchValue(operator, tc.input[key]))\n paramResults.push({\n key,\n pass: anyMatch,\n detail: anyMatch\n ? `matched`\n : `expected ${key} ${operatorToString(operator)}, got ${JSON.stringify(matches.map((tc) => tc.input[key]))}`,\n })\n }\n\n const allParamsPass = paramResults.every((p) => p.pass)\n const failedParams = paramResults.filter((p) => !p.pass)\n\n return {\n assertion: `tool called: ${assertion.called} with params`,\n pass: allParamsPass,\n expected: `${assertion.called} called with ${Object.entries(assertion.params)\n .map(([k, v]) => `${k} ${operatorToString(v)}`)\n .join(', ')}`,\n actual: allParamsPass ? `Matched` : failedParams.map((p) => p.detail).join('; '),\n }\n }\n\n return {\n assertion: `tool called: ${assertion.called}`,\n pass: true,\n expected: `${assertion.called} was called`,\n actual: `Called ${matches.length} time(s)`,\n }\n }\n\n // --- not_called ---\n if ('not_called' in assertion) {\n const wasCalled = toolCalls.some((tc) => tc.name === assertion.not_called)\n return {\n assertion: `tool not_called: ${assertion.not_called}`,\n pass: !wasCalled,\n expected: `${assertion.not_called} was NOT called`,\n actual: wasCalled ? `Was called` : `Not called`,\n }\n }\n\n // --- call_order ---\n if ('call_order' in assertion) {\n const calledNames = toolCalls.map((tc) => tc.name)\n const expectedOrder = assertion.call_order as string[]\n\n // Check that the expected sequence appears in order (not necessarily contiguous)\n let cursor = 0\n for (const name of calledNames) {\n if (cursor < expectedOrder.length && name === expectedOrder[cursor]) {\n cursor++\n }\n }\n const inOrder = cursor === expectedOrder.length\n\n return {\n assertion: `call_order: [${expectedOrder.join(' \u2192 ')}]`,\n pass: inOrder,\n expected: `Tools called in order: [${expectedOrder.join(' \u2192 ')}]`,\n actual: `Actual order: [${calledNames.join(' \u2192 ') || 'none'}]`,\n }\n }\n\n return {\n assertion: 'unknown tool assertion',\n pass: false,\n expected: 'known assertion type',\n actual: `Unknown: ${JSON.stringify(assertion)}`,\n }\n })\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Workflow assertion graders.
3
+ * Checks workflow execution via transformer-produced WorkflowSpan data.
4
+ */
5
+ import type { WorkflowAssertion, WorkflowSpan, GraderResult } from '../types';
6
+ export declare function gradeWorkflows(workflows: WorkflowSpan[], assertions: WorkflowAssertion[]): GraderResult[];
7
+ //# sourceMappingURL=workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/graders/workflow.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE7E,wBAAgB,cAAc,CAAC,SAAS,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,YAAY,EAAE,CAwCzG"}
@@ -0,0 +1,34 @@
1
+ // src/graders/workflow.ts
2
+ function gradeWorkflows(workflows, assertions) {
3
+ const results = [];
4
+ for (const assertion of assertions) {
5
+ const matching = workflows.filter((wf) => wf.name === assertion.name);
6
+ if (assertion.entered !== void 0) {
7
+ const wasEntered = matching.length > 0;
8
+ const pass = assertion.entered ? wasEntered : !wasEntered;
9
+ results.push({
10
+ assertion: `workflow: ${assertion.name} ${assertion.entered ? "entered" : "not entered"}`,
11
+ pass,
12
+ expected: assertion.entered ? `Workflow "${assertion.name}" was entered` : `Workflow "${assertion.name}" was not entered`,
13
+ actual: wasEntered ? `Found ${matching.length} workflow span(s)` : `No workflow spans found`
14
+ });
15
+ }
16
+ if (assertion.completed !== void 0) {
17
+ const didComplete = matching.some(
18
+ (wf) => wf.statusFinal === "completed" || wf.status === "ok" || wf.status === "error"
19
+ );
20
+ const pass = assertion.completed ? didComplete : !didComplete;
21
+ results.push({
22
+ assertion: `workflow: ${assertion.name} ${assertion.completed ? "completed" : "not completed"}`,
23
+ pass,
24
+ expected: assertion.completed ? `Workflow "${assertion.name}" completed` : `Workflow "${assertion.name}" did not complete`,
25
+ actual: didComplete ? `Completed` : `Not completed`
26
+ });
27
+ }
28
+ }
29
+ return results;
30
+ }
31
+ export {
32
+ gradeWorkflows
33
+ };
34
+ //# sourceMappingURL=workflow.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/graders/workflow.ts"],
4
+ "sourcesContent": ["/**\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"],
5
+ "mappings": ";AAOO,SAAS,eAAe,WAA2B,YAAiD;AACzG,QAAM,UAA0B,CAAC;AAEjC,aAAW,aAAa,YAAY;AAClC,UAAM,WAAW,UAAU,OAAO,CAAC,OAAO,GAAG,SAAS,UAAU,IAAI;AAGpE,QAAI,UAAU,YAAY,QAAW;AACnC,YAAM,aAAa,SAAS,SAAS;AACrC,YAAM,OAAO,UAAU,UAAU,aAAa,CAAC;AAE/C,cAAQ,KAAK;AAAA,QACX,WAAW,aAAa,UAAU,IAAI,IAAI,UAAU,UAAU,YAAY,aAAa;AAAA,QACvF;AAAA,QACA,UAAU,UAAU,UAChB,aAAa,UAAU,IAAI,kBAC3B,aAAa,UAAU,IAAI;AAAA,QAC/B,QAAQ,aAAa,SAAS,SAAS,MAAM,sBAAsB;AAAA,MACrE,CAAC;AAAA,IACH;AAGA,QAAI,UAAU,cAAc,QAAW;AACrC,YAAM,cAAc,SAAS;AAAA,QAC3B,CAAC,OAAO,GAAG,gBAAgB,eAAe,GAAG,WAAW,QAAQ,GAAG,WAAW;AAAA,MAChF;AACA,YAAM,OAAO,UAAU,YAAY,cAAc,CAAC;AAElD,cAAQ,KAAK;AAAA,QACX,WAAW,aAAa,UAAU,IAAI,IAAI,UAAU,YAAY,cAAc,eAAe;AAAA,QAC7F;AAAA,QACA,UAAU,UAAU,YAChB,aAAa,UAAU,IAAI,gBAC3B,aAAa,UAAU,IAAI;AAAA,QAC/B,QAAQ,cAAc,cAAc;AAAA,MACtC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ADK eval authoring API.
3
+ *
4
+ * Runtime-heavy execution pieces are available from subpaths such as
5
+ * `@botpress/evals/runner`, `@botpress/evals/loader`, and
6
+ * `@botpress/evals/graders`.
7
+ */
8
+ export { Eval } from './definition';
9
+ export type { BotConnection, ChatClient, ChatClientConnection, ConversationTurn, EvalDefinition, EvalFilter, EvalLogger, EvalProgressEvent, EvalReport, EvalRunReport, EvalRunnerConfig, EvalSetup, GraderResult, MatchOperator, NumericOperator, OutcomeAssertions, ResponseAssertion, Span, SpanStatus, StateAssertion, StateMutation, TimingAssertion, ToolAssertion, ToolCall, TurnAssertions, TurnData, TurnReport, WorkflowAssertion, WorkflowSpan, } from './types';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAEnC,YAAY,EACV,aAAa,EACb,UAAU,EACV,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,IAAI,EACJ,UAAU,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,YAAY,GACb,MAAM,SAAS,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ // src/definition.ts
2
+ var Eval = class {
3
+ name;
4
+ description;
5
+ tags;
6
+ type;
7
+ setup;
8
+ conversation;
9
+ outcome;
10
+ options;
11
+ constructor(def) {
12
+ this.name = def.name;
13
+ this.conversation = def.conversation;
14
+ if (def.description !== void 0) this.description = def.description;
15
+ if (def.tags !== void 0) this.tags = def.tags;
16
+ if (def.type !== void 0) this.type = def.type;
17
+ if (def.setup !== void 0) this.setup = def.setup;
18
+ if (def.outcome !== void 0) this.outcome = def.outcome;
19
+ if (def.options !== void 0) this.options = def.options;
20
+ }
21
+ };
22
+ export {
23
+ Eval
24
+ };
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/definition.ts"],
4
+ "sourcesContent": ["export type MatchOperator =\n | string\n | { equals: unknown }\n | { contains: string }\n | { not_contains: string }\n | { matches: string }\n | { in: unknown[] }\n | { exists: boolean }\n | { gte: number }\n | { lte: number }\n\nexport type ResponseAssertion =\n | { contains: string }\n | { not_contains: string }\n | { matches: string }\n | { llm_judge: string }\n// | { similar_to: string } TODO: uncomment this when it gets implemented\n\nexport type ToolAssertion =\n | { called: string; params?: Record<string, MatchOperator> }\n | { not_called: string }\n | { call_order: string[] }\n\nexport interface StateAssertion {\n path: string\n equals?: unknown\n changed?: boolean\n}\n\nexport interface WorkflowAssertion {\n name: string\n entered?: boolean\n completed?: boolean\n}\n\nexport type NumericOperator = { equals: number } | { gte: number } | { lte: number }\n\nexport interface TimingAssertion {\n response_time: NumericOperator\n}\n\nexport interface TurnAssertions {\n response?: ResponseAssertion[]\n tools?: ToolAssertion[]\n state?: StateAssertion[]\n workflow?: WorkflowAssertion[]\n timing?: TimingAssertion[]\n}\n\nexport interface ConversationTurn {\n user?: string\n event?: { type: string; payload?: Record<string, unknown> }\n expectSilence?: boolean\n assert?: TurnAssertions\n}\n\nexport interface OutcomeAssertions {\n state?: StateAssertion[]\n workflow?: WorkflowAssertion[]\n}\n\nexport interface EvalSetup {\n state?: {\n bot?: Record<string, unknown>\n user?: Record<string, unknown>\n conversation?: Record<string, unknown>\n }\n workflow?: {\n trigger: string\n input?: Record<string, unknown>\n }\n}\n\nexport interface EvalDefinition {\n name: string\n description?: string\n tags?: string[]\n type?: 'capability' | 'regression'\n setup?: EvalSetup\n conversation: ConversationTurn[]\n outcome?: OutcomeAssertions\n options?: {\n idleTimeout?: number\n judgePassThreshold?: number\n }\n}\n\nexport class Eval implements EvalDefinition {\n readonly name: string\n readonly description?: string\n readonly tags?: string[]\n readonly type?: 'capability' | 'regression'\n readonly setup?: EvalSetup\n readonly conversation: ConversationTurn[]\n readonly outcome?: OutcomeAssertions\n readonly options?: {\n idleTimeout?: number\n judgePassThreshold?: number\n }\n\n constructor(def: EvalDefinition) {\n this.name = def.name\n this.conversation = def.conversation\n if (def.description !== undefined) this.description = def.description\n if (def.tags !== undefined) this.tags = def.tags\n if (def.type !== undefined) this.type = def.type\n if (def.setup !== undefined) this.setup = def.setup\n if (def.outcome !== undefined) this.outcome = def.outcome\n if (def.options !== undefined) this.options = def.options\n }\n}\n"],
5
+ "mappings": ";AAuFO,IAAM,OAAN,MAAqC;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAKT,YAAY,KAAqB;AAC/B,SAAK,OAAO,IAAI;AAChB,SAAK,eAAe,IAAI;AACxB,QAAI,IAAI,gBAAgB,OAAW,MAAK,cAAc,IAAI;AAC1D,QAAI,IAAI,SAAS,OAAW,MAAK,OAAO,IAAI;AAC5C,QAAI,IAAI,SAAS,OAAW,MAAK,OAAO,IAAI;AAC5C,QAAI,IAAI,UAAU,OAAW,MAAK,QAAQ,IAAI;AAC9C,QAAI,IAAI,YAAY,OAAW,MAAK,UAAU,IAAI;AAClD,QAAI,IAAI,YAAY,OAAW,MAAK,UAAU,IAAI;AAAA,EACpD;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Eval file loader.
3
+ * Loads *.eval.ts files from a directory using dynamic imports.
4
+ * Supports both default and named exports, and multiple evals per file.
5
+ */
6
+ import type { EvalDefinition, EvalFilter } from './types';
7
+ export declare function loadEvalFile(filePath: string): Promise<EvalDefinition[]>;
8
+ export declare function loadEvalsFromDir(dirPath: string): Promise<EvalDefinition[]>;
9
+ export declare function loadEvalByName(dirPath: string, name: string): Promise<EvalDefinition | null>;
10
+ export declare function filterEvals(evals: EvalDefinition[], filter?: EvalFilter): EvalDefinition[];
11
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAazD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAkC9E;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAuBjF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAalG;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,cAAc,EAAE,CAe1F"}
package/dist/loader.js ADDED
@@ -0,0 +1,114 @@
1
+ // src/loader.ts
2
+ import { readdirSync, existsSync } from "fs";
3
+ import { resolve } from "path";
4
+
5
+ // src/definition.ts
6
+ var Eval = class {
7
+ name;
8
+ description;
9
+ tags;
10
+ type;
11
+ setup;
12
+ conversation;
13
+ outcome;
14
+ options;
15
+ constructor(def) {
16
+ this.name = def.name;
17
+ this.conversation = def.conversation;
18
+ if (def.description !== void 0) this.description = def.description;
19
+ if (def.tags !== void 0) this.tags = def.tags;
20
+ if (def.type !== void 0) this.type = def.type;
21
+ if (def.setup !== void 0) this.setup = def.setup;
22
+ if (def.outcome !== void 0) this.outcome = def.outcome;
23
+ if (def.options !== void 0) this.options = def.options;
24
+ }
25
+ };
26
+
27
+ // src/loader.ts
28
+ function isEvalDefinition(value) {
29
+ return value !== null && typeof value === "object" && typeof value.name === "string" && value.name !== "" && Array.isArray(value.conversation);
30
+ }
31
+ async function loadEvalFile(filePath) {
32
+ const absPath = resolve(filePath);
33
+ let mod;
34
+ try {
35
+ mod = await import(absPath);
36
+ } catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ throw new Error(
39
+ `Failed to load eval file ${filePath}: ${msg}
40
+
41
+ Make sure your eval file:
42
+ - Has no syntax or type errors
43
+ - Exports one or more \`new Eval({...})\` instances
44
+ - Has all dependencies installed (\`bun install\`)`,
45
+ { cause: err }
46
+ );
47
+ }
48
+ const results = [];
49
+ for (const [key, value] of Object.entries(mod)) {
50
+ if (key === "__esModule") continue;
51
+ if (value instanceof Eval || isEvalDefinition(value)) {
52
+ results.push(value);
53
+ }
54
+ }
55
+ if (results.length === 0) {
56
+ throw new Error(
57
+ `Invalid eval file ${filePath}: no valid evals found. Export one or more \`new Eval({...})\` instances (as default or named exports).`
58
+ );
59
+ }
60
+ return results;
61
+ }
62
+ async function loadEvalsFromDir(dirPath) {
63
+ const absDir = resolve(dirPath);
64
+ if (!existsSync(absDir)) {
65
+ return [];
66
+ }
67
+ const files = readdirSync(absDir).filter((f) => f.endsWith(".eval.ts"));
68
+ const evals = [];
69
+ for (const f of files) {
70
+ const defs = await loadEvalFile(`${absDir}/${f}`);
71
+ evals.push(...defs);
72
+ }
73
+ const seen = /* @__PURE__ */ new Set();
74
+ for (const e of evals) {
75
+ if (seen.has(e.name)) {
76
+ throw new Error(`Duplicate eval name "${e.name}" found in ${dirPath}`);
77
+ }
78
+ seen.add(e.name);
79
+ }
80
+ return evals;
81
+ }
82
+ async function loadEvalByName(dirPath, name) {
83
+ const absDir = resolve(dirPath);
84
+ if (!existsSync(absDir)) return null;
85
+ const files = readdirSync(absDir).filter((f) => f.endsWith(".eval.ts"));
86
+ for (const f of files) {
87
+ const defs = await loadEvalFile(`${absDir}/${f}`);
88
+ const found = defs.find((d) => d.name === name);
89
+ if (found) return found;
90
+ }
91
+ return null;
92
+ }
93
+ function filterEvals(evals, filter) {
94
+ if (!filter) return evals;
95
+ return evals.filter((e) => {
96
+ if (filter.names && filter.names.length > 0) {
97
+ if (!filter.names.includes(e.name)) return false;
98
+ }
99
+ if (filter.tags && filter.tags.length > 0) {
100
+ if (!e.tags || !filter.tags.some((t) => e.tags.includes(t))) return false;
101
+ }
102
+ if (filter.type) {
103
+ if (e.type !== filter.type) return false;
104
+ }
105
+ return true;
106
+ });
107
+ }
108
+ export {
109
+ filterEvals,
110
+ loadEvalByName,
111
+ loadEvalFile,
112
+ loadEvalsFromDir
113
+ };
114
+ //# sourceMappingURL=loader.js.map