@m4trix/evals 0.1.0 → 0.2.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,163 @@
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 { defineConfigFunction, type ConfigType } from '@m4trix/evals';
43
+
44
+ export default defineConfigFunction((): 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, latencyMetric, percentScore, tokenCountMetric } from '@m4trix/evals';
71
+ import { Schema as S } from 'effect';
72
+
73
+ const inputSchema = S.Struct({ prompt: S.String });
74
+
75
+ export const myEvaluator = Evaluator.define({
76
+ name: 'My Evaluator',
77
+ inputSchema,
78
+ outputSchema: S.Unknown,
79
+ scoreSchema: S.Struct({ scores: S.Array(S.Unknown) }),
80
+ }).evaluate(async (input) => {
81
+ const start = Date.now();
82
+ const latencyMs = Date.now() - start;
83
+
84
+ return {
85
+ scores: [
86
+ percentScore.make({ value: 85 }, { definePassed: (d) => d.value >= 50 }),
87
+ ],
88
+ metrics: [
89
+ tokenCountMetric.make({
90
+ input: input.prompt.length,
91
+ output: input.prompt.length,
92
+ inputCached: 0,
93
+ outputCached: 0,
94
+ }),
95
+ latencyMetric.make({ ms: latencyMs }),
96
+ ],
97
+ };
98
+ });
99
+ ```
100
+
101
+ ### 3) Test Case
102
+
103
+ ```ts
104
+ import { TestCase } from '@m4trix/evals';
105
+ import { Schema as S } from 'effect';
106
+
107
+ export const myTestCase = TestCase.describe({
108
+ name: 'my test case',
109
+ tags: ['demo'],
110
+ inputSchema: S.Struct({ prompt: S.String }),
111
+ input: { prompt: 'Hello from my first eval' },
112
+ });
113
+ ```
114
+
115
+ ### 4) Run
116
+
117
+ ```bash
118
+ eval-agents-simple run --dataset "My Dataset" --evaluator "My Evaluator"
119
+ ```
120
+
121
+ You can also use patterns:
122
+
123
+ ```bash
124
+ eval-agents-simple run --dataset "*My*" --evaluator "*My*"
125
+ ```
126
+
127
+ ## CLI Commands
128
+
129
+ - `eval-agents`: interactive CLI
130
+ - `eval-agents-simple run --dataset "<name or pattern>" --evaluator "<name or pattern>"`
131
+ - `eval-agents-simple generate --dataset "<dataset name>"`
132
+
133
+ ## Default Discovery and Artifacts
134
+
135
+ By default, the runner uses `process.cwd()` as discovery root and scans for:
136
+
137
+ - Datasets: `.dataset.ts`, `.dataset.tsx`, `.dataset.js`, `.dataset.mjs`
138
+ - Evaluators: `.evaluator.ts`, `.evaluator.tsx`, `.evaluator.js`, `.evaluator.mjs`
139
+ - Test cases: `.test-case.ts`, `.test-case.tsx`, `.test-case.js`, `.test-case.mjs`
140
+
141
+ Results are written to `.eval-results`.
142
+
143
+ ## Config File
144
+
145
+ When present, `m4trix-eval.config.ts` is loaded automatically from `process.cwd()`.
146
+
147
+ - Config API: `defineConfigFunction(() => ConfigType)`
148
+ - Supported exports: default object, or default function that returns config
149
+ - Discovery keys:
150
+ - `datasetFilePatterns` (or `datasetSuffixes`)
151
+ - `evaluatorFilePatterns` (or `evaluatorSuffixes`)
152
+ - `testCaseFilePatterns` (or `testCaseSuffixes`)
153
+ - `rootDir`, `excludeDirectories`
154
+
155
+ Precedence is:
156
+
157
+ 1. built-in defaults
158
+ 2. `m4trix-eval.config.ts`
159
+ 3. explicit `createRunner({...})` overrides
160
+
161
+ ## License
162
+
163
+ 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,102 @@ 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("Failed to initialize jiti for m4trix eval config loading.");
113
+ }
114
+ 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)), {
115
+ interopDefault: true,
116
+ moduleCache: true
117
+ });
118
+ return cachedLoader;
119
+ }
120
+ function resolveConfigModuleExport(loadedModule) {
121
+ if (loadedModule && typeof loadedModule === "object" && "default" in loadedModule) {
122
+ return loadedModule.default;
123
+ }
124
+ return loadedModule;
125
+ }
126
+ function resolveConfigValue(value) {
127
+ if (value === void 0 || value === null) {
128
+ return void 0;
129
+ }
130
+ if (typeof value === "function") {
131
+ return value();
132
+ }
133
+ if (typeof value !== "object") {
134
+ throw new Error(
135
+ "Invalid m4trix eval config export. Expected an object or defineConfigFunction(() => config)."
136
+ );
137
+ }
138
+ return value;
139
+ }
140
+ function loadRunnerConfigFile(cwd = process.cwd()) {
141
+ const configPath = path.resolve(cwd, CONFIG_FILE_NAME);
142
+ if (!fs.existsSync(configPath)) {
143
+ return void 0;
144
+ }
145
+ const loader = getJitiLoader();
146
+ const loaded = loader(configPath);
147
+ const exportedValue = resolveConfigModuleExport(loaded);
148
+ const config = resolveConfigValue(exportedValue);
149
+ return toRunnerConfigOverrides(config);
36
150
  }
37
151
  var jitiLoader;
38
152
  function toId(prefix, filePath, name) {
@@ -85,12 +199,12 @@ function hasOneSuffix(filePath, suffixes) {
85
199
  async function loadModuleExports(filePath) {
86
200
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
87
201
  if (!jitiLoader) {
88
- const jitiModule = await import('jiti');
89
- const createJiti = jitiModule.createJiti ?? jitiModule.default;
90
- if (!createJiti) {
202
+ const jitiModule2 = await import('jiti');
203
+ const createJiti2 = jitiModule2.createJiti ?? jitiModule2.default;
204
+ if (!createJiti2) {
91
205
  throw new Error("Failed to initialize jiti TypeScript loader");
92
206
  }
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)), {
207
+ 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
208
  interopDefault: true,
95
209
  moduleCache: true
96
210
  });
@@ -498,8 +612,18 @@ function createNameMatcher(pattern) {
498
612
  }
499
613
  return (value) => value.toLowerCase() === normalizedPattern.toLowerCase();
500
614
  }
615
+ function mergeRunnerOverrides(base, next) {
616
+ if (!base) {
617
+ return next;
618
+ }
619
+ {
620
+ return base;
621
+ }
622
+ }
501
623
  function createRunner(overrides) {
502
- return new EffectRunner(withRunnerConfig());
624
+ const fileOverrides = loadRunnerConfigFile();
625
+ const merged = mergeRunnerOverrides(fileOverrides, overrides);
626
+ return new EffectRunner(withRunnerConfig(merged));
503
627
  }
504
628
  var EffectRunner = class {
505
629
  constructor(config) {
@@ -902,7 +1026,7 @@ async function runSimpleEvalCommand(runner, datasetName, evaluatorPattern) {
902
1026
  );
903
1027
  }
904
1028
  let spinnerTimer;
905
- const done = new Promise((resolve3) => {
1029
+ const done = new Promise((resolve4) => {
906
1030
  const unsubscribe = runner.subscribeRunEvents((event) => {
907
1031
  if (event.type === "TestCaseProgress") {
908
1032
  completedCount = event.completedTestCases;
@@ -952,7 +1076,7 @@ async function runSimpleEvalCommand(runner, datasetName, evaluatorPattern) {
952
1076
  runFinished = true;
953
1077
  clearLine();
954
1078
  unsubscribe();
955
- resolve3(event);
1079
+ resolve4(event);
956
1080
  }
957
1081
  });
958
1082
  });