@m4trix/evals 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pascal Lohscheidt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/Pascal-Lohscheidt/build-ai/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/Pascal-Lohscheidt/build-ai/tree/main)
2
+ [![npm version](https://img.shields.io/npm/v/@m4trix%2Fevals)](https://www.npmjs.com/package/@m4trix/evals)
3
+ [![license](https://img.shields.io/npm/l/@m4trix%2Fevals)](https://www.npmjs.com/package/@m4trix/evals)
4
+
5
+ # @m4trix/evals
6
+
7
+ `@m4trix/evals` helps you define datasets, test cases, and evaluators for repeatable AI evaluation runs.
8
+
9
+ ## Quick Start
10
+
11
+ From the repository root:
12
+
13
+ ```bash
14
+ pnpm install
15
+ pnpm run evals:build
16
+ ```
17
+
18
+ Run the bundled example project:
19
+
20
+ ```bash
21
+ cd examples/evals-example
22
+ pnpm run eval:run
23
+ ```
24
+
25
+ Generate a dataset case file from the example:
26
+
27
+ ```bash
28
+ pnpm run eval:generate
29
+ ```
30
+
31
+ ## Set Up Your First Eval
32
+
33
+ Create files under your project (for example, `src/evals/`) with these suffixes:
34
+
35
+ - `*.dataset.ts`
36
+ - `*.evaluator.ts`
37
+ - `*.test-case.ts`
38
+
39
+ Optional: create `m4trix-eval.config.ts` at your project root to customize discovery and output paths.
40
+
41
+ ```ts
42
+ import { defineConfig, type ConfigType } from '@m4trix/evals';
43
+
44
+ export default defineConfig((): ConfigType => ({
45
+ discovery: {
46
+ rootDir: 'src/evals',
47
+ datasetFilePatterns: ['.dataset.ts'],
48
+ evaluatorFilePatterns: ['.evaluator.ts'],
49
+ testCaseFilePatterns: ['.test-case.ts'],
50
+ excludeDirectories: ['node_modules', 'dist'],
51
+ },
52
+ artifactDirectory: 'src/evals/.eval-results',
53
+ }));
54
+ ```
55
+
56
+ ### 1) Dataset
57
+
58
+ ```ts
59
+ import { Dataset } from '@m4trix/evals';
60
+
61
+ export const myDataset = Dataset.define({
62
+ name: 'My Dataset',
63
+ includedTags: ['demo'],
64
+ });
65
+ ```
66
+
67
+ ### 2) Evaluator
68
+
69
+ ```ts
70
+ import { Evaluator, S, latencyMetric, percentScore, tokenCountMetric } from '@m4trix/evals';
71
+
72
+ const inputSchema = S.Struct({ prompt: S.String });
73
+
74
+ export const myEvaluator = Evaluator.define({
75
+ name: 'My Evaluator',
76
+ inputSchema,
77
+ outputSchema: S.Unknown,
78
+ scoreSchema: S.Struct({ scores: S.Array(S.Unknown) }),
79
+ }).evaluate(async (input) => {
80
+ const start = Date.now();
81
+ const latencyMs = Date.now() - start;
82
+
83
+ return {
84
+ scores: [
85
+ percentScore.make({ value: 85 }, { definePassed: (d) => d.value >= 50 }),
86
+ ],
87
+ metrics: [
88
+ tokenCountMetric.make({
89
+ input: input.prompt.length,
90
+ output: input.prompt.length,
91
+ inputCached: 0,
92
+ outputCached: 0,
93
+ }),
94
+ latencyMetric.make({ ms: latencyMs }),
95
+ ],
96
+ };
97
+ });
98
+ ```
99
+
100
+ ### 3) Test Case
101
+
102
+ ```ts
103
+ import { TestCase, S } from '@m4trix/evals';
104
+
105
+ export const myTestCase = TestCase.describe({
106
+ name: 'my test case',
107
+ tags: ['demo'],
108
+ inputSchema: S.Struct({ prompt: S.String }),
109
+ input: { prompt: 'Hello from my first eval' },
110
+ });
111
+ ```
112
+
113
+ ### 4) Run
114
+
115
+ ```bash
116
+ eval-agents-simple run --dataset "My Dataset" --evaluator "My Evaluator"
117
+ ```
118
+
119
+ You can also use patterns:
120
+
121
+ ```bash
122
+ eval-agents-simple run --dataset "*My*" --evaluator "*My*"
123
+ ```
124
+
125
+ ## CLI Commands
126
+
127
+ - `eval-agents`: interactive CLI
128
+ - `eval-agents-simple run --dataset "<name or pattern>" --evaluator "<name or pattern>"`
129
+ - `eval-agents-simple generate --dataset "<dataset name>"`
130
+
131
+ ## Default Discovery and Artifacts
132
+
133
+ By default, the runner uses `process.cwd()` as discovery root and scans for:
134
+
135
+ - Datasets: `.dataset.ts`, `.dataset.tsx`, `.dataset.js`, `.dataset.mjs`
136
+ - Evaluators: `.evaluator.ts`, `.evaluator.tsx`, `.evaluator.js`, `.evaluator.mjs`
137
+ - Test cases: `.test-case.ts`, `.test-case.tsx`, `.test-case.js`, `.test-case.mjs`
138
+
139
+ Results are written to `.eval-results`.
140
+
141
+ ## Config File
142
+
143
+ When present, `m4trix-eval.config.ts` is loaded automatically from `process.cwd()`.
144
+
145
+ - Config API: `defineConfig(() => ConfigType)`
146
+ - Supported exports: default object, or default function that returns config
147
+ - Discovery keys:
148
+ - `datasetFilePatterns` (or `datasetSuffixes`)
149
+ - `evaluatorFilePatterns` (or `evaluatorSuffixes`)
150
+ - `testCaseFilePatterns` (or `testCaseSuffixes`)
151
+ - `rootDir`, `excludeDirectories`
152
+
153
+ Precedence is:
154
+
155
+ 1. built-in defaults
156
+ 2. `m4trix-eval.config.ts`
157
+ 3. explicit `createRunner({...})` overrides
158
+
159
+ ## License
160
+
161
+ MIT
@@ -3,11 +3,33 @@
3
3
 
4
4
  var crypto = require('crypto');
5
5
  var effect = require('effect');
6
- var promises = require('fs/promises');
6
+ var fs = require('fs');
7
7
  var path = require('path');
8
+ var jitiModule = require('jiti');
9
+ var promises = require('fs/promises');
8
10
  var url = require('url');
9
11
 
10
12
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var jitiModule__namespace = /*#__PURE__*/_interopNamespace(jitiModule);
32
+
11
33
  // src/runner/config.ts
12
34
  var defaultRunnerConfig = {
13
35
  discovery: {
@@ -29,10 +51,104 @@ var defaultRunnerConfig = {
29
51
  },
30
52
  artifactDirectory: ".eval-results"
31
53
  };
54
+ function toRunnerConfigOverrides(config) {
55
+ if (!config) {
56
+ return void 0;
57
+ }
58
+ const rawDiscovery = config.discovery;
59
+ const discovery = {};
60
+ if (rawDiscovery?.rootDir !== void 0) {
61
+ discovery.rootDir = rawDiscovery.rootDir;
62
+ }
63
+ if (rawDiscovery?.datasetFilePatterns !== void 0) {
64
+ discovery.datasetSuffixes = rawDiscovery.datasetFilePatterns;
65
+ } else if (rawDiscovery?.datasetSuffixes !== void 0) {
66
+ discovery.datasetSuffixes = rawDiscovery.datasetSuffixes;
67
+ }
68
+ if (rawDiscovery?.evaluatorFilePatterns !== void 0) {
69
+ discovery.evaluatorSuffixes = rawDiscovery.evaluatorFilePatterns;
70
+ } else if (rawDiscovery?.evaluatorSuffixes !== void 0) {
71
+ discovery.evaluatorSuffixes = rawDiscovery.evaluatorSuffixes;
72
+ }
73
+ if (rawDiscovery?.testCaseFilePatterns !== void 0) {
74
+ discovery.testCaseSuffixes = rawDiscovery.testCaseFilePatterns;
75
+ } else if (rawDiscovery?.testCaseSuffixes !== void 0) {
76
+ discovery.testCaseSuffixes = rawDiscovery.testCaseSuffixes;
77
+ }
78
+ if (rawDiscovery?.excludeDirectories !== void 0) {
79
+ discovery.excludeDirectories = rawDiscovery.excludeDirectories;
80
+ }
81
+ const overrides = {};
82
+ if (config.artifactDirectory !== void 0) {
83
+ overrides.artifactDirectory = config.artifactDirectory;
84
+ }
85
+ if (Object.keys(discovery).length > 0) {
86
+ overrides.discovery = discovery;
87
+ }
88
+ return overrides;
89
+ }
32
90
  function withRunnerConfig(overrides) {
33
- {
91
+ if (!overrides) {
34
92
  return defaultRunnerConfig;
35
93
  }
94
+ const discovery = overrides.discovery ? {
95
+ ...defaultRunnerConfig.discovery,
96
+ ...overrides.discovery
97
+ } : defaultRunnerConfig.discovery;
98
+ return {
99
+ ...defaultRunnerConfig,
100
+ ...overrides,
101
+ discovery
102
+ };
103
+ }
104
+ var CONFIG_FILE_NAME = "m4trix-eval.config.ts";
105
+ var cachedLoader;
106
+ function getJitiLoader() {
107
+ if (cachedLoader) {
108
+ return cachedLoader;
109
+ }
110
+ const createJiti2 = jitiModule__namespace.createJiti ?? jitiModule__namespace.default;
111
+ if (typeof createJiti2 !== "function") {
112
+ throw new Error(
113
+ "Failed to initialize jiti for m4trix eval config loading."
114
+ );
115
+ }
116
+ cachedLoader = createJiti2((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('out.js', document.baseURI).href)), {
117
+ interopDefault: true,
118
+ moduleCache: true
119
+ });
120
+ return cachedLoader;
121
+ }
122
+ function resolveConfigModuleExport(loadedModule) {
123
+ if (loadedModule && typeof loadedModule === "object" && "default" in loadedModule) {
124
+ return loadedModule.default;
125
+ }
126
+ return loadedModule;
127
+ }
128
+ function resolveConfigValue(value) {
129
+ if (value === void 0 || value === null) {
130
+ return void 0;
131
+ }
132
+ if (typeof value === "function") {
133
+ return value();
134
+ }
135
+ if (typeof value !== "object") {
136
+ throw new Error(
137
+ "Invalid m4trix eval config export. Expected an object or defineConfig(() => config)."
138
+ );
139
+ }
140
+ return value;
141
+ }
142
+ function loadRunnerConfigFile(cwd = process.cwd()) {
143
+ const configPath = path.resolve(cwd, CONFIG_FILE_NAME);
144
+ if (!fs.existsSync(configPath)) {
145
+ return void 0;
146
+ }
147
+ const loader = getJitiLoader();
148
+ const loaded = loader(configPath);
149
+ const exportedValue = resolveConfigModuleExport(loaded);
150
+ const config = resolveConfigValue(exportedValue);
151
+ return toRunnerConfigOverrides(config);
36
152
  }
37
153
  var jitiLoader;
38
154
  function toId(prefix, filePath, name) {
@@ -85,12 +201,12 @@ function hasOneSuffix(filePath, suffixes) {
85
201
  async function loadModuleExports(filePath) {
86
202
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
87
203
  if (!jitiLoader) {
88
- const jitiModule = await import('jiti');
89
- const createJiti = jitiModule.createJiti ?? jitiModule.default;
90
- if (!createJiti) {
204
+ const jitiModule2 = await import('jiti');
205
+ const createJiti2 = jitiModule2.createJiti ?? jitiModule2.default;
206
+ if (!createJiti2) {
91
207
  throw new Error("Failed to initialize jiti TypeScript loader");
92
208
  }
93
- jitiLoader = createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('out.js', document.baseURI).href)), {
209
+ jitiLoader = createJiti2((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('out.js', document.baseURI).href)), {
94
210
  interopDefault: true,
95
211
  moduleCache: true
96
212
  });
@@ -498,8 +614,18 @@ function createNameMatcher(pattern) {
498
614
  }
499
615
  return (value) => value.toLowerCase() === normalizedPattern.toLowerCase();
500
616
  }
617
+ function mergeRunnerOverrides(base, next) {
618
+ if (!base) {
619
+ return next;
620
+ }
621
+ {
622
+ return base;
623
+ }
624
+ }
501
625
  function createRunner(overrides) {
502
- return new EffectRunner(withRunnerConfig());
626
+ const fileOverrides = loadRunnerConfigFile();
627
+ const merged = mergeRunnerOverrides(fileOverrides, overrides);
628
+ return new EffectRunner(withRunnerConfig(merged));
503
629
  }
504
630
  var EffectRunner = class {
505
631
  constructor(config) {
@@ -902,7 +1028,7 @@ async function runSimpleEvalCommand(runner, datasetName, evaluatorPattern) {
902
1028
  );
903
1029
  }
904
1030
  let spinnerTimer;
905
- const done = new Promise((resolve3) => {
1031
+ const done = new Promise((resolve4) => {
906
1032
  const unsubscribe = runner.subscribeRunEvents((event) => {
907
1033
  if (event.type === "TestCaseProgress") {
908
1034
  completedCount = event.completedTestCases;
@@ -952,7 +1078,7 @@ async function runSimpleEvalCommand(runner, datasetName, evaluatorPattern) {
952
1078
  runFinished = true;
953
1079
  clearLine();
954
1080
  unsubscribe();
955
- resolve3(event);
1081
+ resolve4(event);
956
1082
  }
957
1083
  });
958
1084
  });