@axlsdk/eval 0.10.3 → 0.11.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/README.md +108 -14
- package/dist/{chunk-LMQJNZWT.js → chunk-D7KCUS6H.js} +1 -1
- package/dist/chunk-D7KCUS6H.js.map +1 -0
- package/dist/cli.cjs +251 -36
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +251 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-LMQJNZWT.js.map +0 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ const qualityJudge = llmScorer({
|
|
|
85
85
|
});
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
### `runEval(config, executeFn, provider
|
|
88
|
+
### `runEval(config, executeFn, provider, runtime)`
|
|
89
89
|
|
|
90
90
|
Run an evaluation:
|
|
91
91
|
|
|
@@ -103,6 +103,8 @@ const results = await runEval(
|
|
|
103
103
|
const output = await runtime.execute('my-workflow', input);
|
|
104
104
|
return { output };
|
|
105
105
|
},
|
|
106
|
+
undefined, // provider (only needed for llmScorer)
|
|
107
|
+
runtime,
|
|
106
108
|
);
|
|
107
109
|
|
|
108
110
|
console.log(results.summary);
|
|
@@ -158,11 +160,11 @@ export default defineEval({
|
|
|
158
160
|
});
|
|
159
161
|
```
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
When running via `runtime.eval()` or `runtime.runRegisteredEval()`, the runner calls `runtime.execute(workflow, input)` by default, which requires the workflow to be registered on the runtime.
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
When running via the CLI, the eval runner resolves the execution function in order: (1) the exported `executeWorkflow` function, (2) `runtime.execute(workflow, input)` if the workflow is registered on the runtime, or (3) a passthrough that returns the input as the output (with a warning).
|
|
164
166
|
|
|
165
|
-
The function receives `(input, runtime
|
|
167
|
+
The function receives `(input, runtime)` — the second argument is the `AxlRuntime` instance, always provided by the CLI, Studio, and `runtime.runRegisteredEval()`. Use it to call agents without needing a registered workflow:
|
|
166
168
|
|
|
167
169
|
```typescript
|
|
168
170
|
// evals/qa-quality.eval.ts — calls an agent directly via the runtime
|
|
@@ -192,15 +194,14 @@ export default defineEval({
|
|
|
192
194
|
scorers: [notEmpty],
|
|
193
195
|
});
|
|
194
196
|
|
|
195
|
-
export async function executeWorkflow(input: { question: string }, runtime
|
|
196
|
-
if (!runtime) throw new Error('This eval requires a runtime — run via Studio or runtime.runRegisteredEval()');
|
|
197
|
+
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
197
198
|
const ctx = runtime.createContext();
|
|
198
199
|
const output = await ctx.ask(qaAgent, input.question);
|
|
199
|
-
return { output };
|
|
200
|
+
return { output }; // cost tracked automatically via runtime.trackCost()
|
|
200
201
|
}
|
|
201
202
|
```
|
|
202
203
|
|
|
203
|
-
When no runtime is needed (e.g., testing a pure function),
|
|
204
|
+
When no runtime is needed (e.g., testing a pure function), omit the runtime parameter — these evals work from both the CLI and Studio:
|
|
204
205
|
|
|
205
206
|
```typescript
|
|
206
207
|
// evals/parser.eval.ts — tests a pure function, no runtime needed
|
|
@@ -210,9 +211,81 @@ export async function executeWorkflow(input: { raw: string }) {
|
|
|
210
211
|
}
|
|
211
212
|
```
|
|
212
213
|
|
|
213
|
-
> **Note:** The CLI
|
|
214
|
+
> **Note:** The CLI always provides a runtime to `executeWorkflow`. By default it auto-detects `axl.config.*` in the working directory or falls back to a bare `AxlRuntime` (providers resolved from environment variables). Use `--config` for explicit config files.
|
|
215
|
+
|
|
216
|
+
### Cost Tracking
|
|
217
|
+
|
|
218
|
+
Cost is tracked automatically. You don't need to return `cost` from your `executeWorkflow` — the eval runner wraps each item with `runtime.trackCost()`, which captures cost from all `createContext()` and `execute()` calls.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
222
|
+
const ctx = runtime.createContext();
|
|
223
|
+
const output = await ctx.ask(myAgent, input.question);
|
|
224
|
+
return { output }; // cost captured automatically
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
To override the automatic cost (e.g., to exclude setup calls), return it explicitly:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
return { output, cost: ctx.totalCost };
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
You can also read `ctx.totalCost` at any point to inspect accumulated cost.
|
|
235
|
+
|
|
236
|
+
### Common Patterns
|
|
237
|
+
|
|
238
|
+
**Budget per item** — cap cost to prevent runaway evals:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
242
|
+
const ctx = runtime.createContext({ budget: '$0.50' });
|
|
243
|
+
const output = await ctx.ask(myAgent, input.question);
|
|
244
|
+
return { output };
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Timeout per item** — cancel items that take too long:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
252
|
+
const controller = new AbortController();
|
|
253
|
+
setTimeout(() => controller.abort(), 30_000);
|
|
254
|
+
const ctx = runtime.createContext({ signal: controller.signal });
|
|
255
|
+
const output = await ctx.ask(myAgent, input.question);
|
|
256
|
+
return { output };
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Auto-approve tools** — when testing agents that have tools with `requireApproval`:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
264
|
+
const ctx = runtime.createContext({
|
|
265
|
+
awaitHumanHandler: async () => ({ approved: true }),
|
|
266
|
+
});
|
|
267
|
+
const output = await ctx.ask(myAgent, input.question);
|
|
268
|
+
return { output };
|
|
269
|
+
}
|
|
270
|
+
```
|
|
214
271
|
|
|
215
|
-
|
|
272
|
+
**Multi-turn evaluation** — test follow-up responses with conversation history:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
export async function executeWorkflow(
|
|
276
|
+
input: { setupPrompt: string; setupResponse: string; followUp: string },
|
|
277
|
+
runtime: AxlRuntime,
|
|
278
|
+
) {
|
|
279
|
+
const ctx = runtime.createContext({
|
|
280
|
+
sessionHistory: [
|
|
281
|
+
{ role: 'user', content: input.setupPrompt },
|
|
282
|
+
{ role: 'assistant', content: input.setupResponse },
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
const output = await ctx.ask(myAgent, input.followUp);
|
|
286
|
+
return { output };
|
|
287
|
+
}
|
|
288
|
+
```
|
|
216
289
|
|
|
217
290
|
## CLI
|
|
218
291
|
|
|
@@ -220,18 +293,39 @@ Run evaluations from the command line:
|
|
|
220
293
|
|
|
221
294
|
```bash
|
|
222
295
|
# Run an eval file
|
|
223
|
-
npx axl
|
|
296
|
+
npx axl-eval ./evals/math.eval.ts
|
|
224
297
|
|
|
225
298
|
# Run all evals in a directory
|
|
226
|
-
npx axl
|
|
299
|
+
npx axl-eval ./evals/
|
|
227
300
|
|
|
228
301
|
# Save results to a file
|
|
229
|
-
npx axl
|
|
302
|
+
npx axl-eval ./evals/math.eval.ts --output ./results/baseline.json
|
|
303
|
+
|
|
304
|
+
# Use a config file for runtime access
|
|
305
|
+
npx axl-eval ./evals/ --config ./axl.config.ts
|
|
306
|
+
|
|
307
|
+
# Use custom import conditions (monorepo source exports)
|
|
308
|
+
npx axl-eval ./evals/ --config ./axl.config.ts --conditions development
|
|
230
309
|
|
|
231
310
|
# Compare two results
|
|
232
|
-
npx axl
|
|
311
|
+
npx axl-eval compare ./results/baseline.json ./results/candidate.json
|
|
312
|
+
|
|
313
|
+
# Fail CI if regressions detected
|
|
314
|
+
npx axl-eval compare baseline.json candidate.json --fail-on-regression
|
|
233
315
|
```
|
|
234
316
|
|
|
317
|
+
TypeScript config and eval files require [tsx](https://github.com/privatenumber/tsx) as a dev dependency (`npm install -D tsx`).
|
|
318
|
+
|
|
319
|
+
### Runtime Resolution
|
|
320
|
+
|
|
321
|
+
The CLI resolves an `AxlRuntime` to pass to `executeWorkflow`:
|
|
322
|
+
|
|
323
|
+
1. **`--config <path>`** — use an explicit config file
|
|
324
|
+
2. **Auto-detect** — search for `axl.config.mts` → `.ts` → `.mjs` → `.js` in cwd
|
|
325
|
+
3. **Fallback** — create a bare `new AxlRuntime()` (providers resolve from environment variables)
|
|
326
|
+
|
|
327
|
+
When a runtime is available, each `executeWorkflow` call is wrapped with `runtime.trackCost()` for automatic cost attribution.
|
|
328
|
+
|
|
235
329
|
## Studio Integration
|
|
236
330
|
|
|
237
331
|
Eval files can also be lazy-loaded by the Studio middleware, enabling the Eval Runner panel without static imports that would create circular dependencies:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compare.ts","../src/runner.ts"],"sourcesContent":["import type { EvalResult, EvalComparison, EvalRegression, EvalImprovement } from './types.js';\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\nexport function evalCompare(baseline: EvalResult, candidate: EvalResult): EvalComparison {\n if (baseline.dataset !== candidate.dataset) {\n throw new Error(\n `Cannot compare evals from different datasets: \"${baseline.dataset}\" vs \"${candidate.dataset}\"`,\n );\n }\n\n const baselineScorerNames = Object.keys(baseline.summary.scorers).sort();\n const candidateScorerNames = Object.keys(candidate.summary.scorers).sort();\n if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {\n throw new Error(`Cannot compare evals with different scorers`);\n }\n\n const scorers: EvalComparison['scorers'] = {};\n for (const name of baselineScorerNames) {\n const bMean = baseline.summary.scorers[name].mean;\n const cMean = candidate.summary.scorers[name].mean;\n const delta = round(cMean - bMean);\n const deltaPercent = bMean > 0 ? round((delta / bMean) * 100) : 0;\n scorers[name] = { baselineMean: bMean, candidateMean: cMean, delta, deltaPercent };\n }\n\n const regressions: EvalRegression[] = [];\n const improvements: EvalImprovement[] = [];\n\n const minLength = Math.min(baseline.items.length, candidate.items.length);\n for (let i = 0; i < minLength; i++) {\n const bItem = baseline.items[i];\n const cItem = candidate.items[i];\n if (bItem.error || cItem.error) continue;\n for (const name of baselineScorerNames) {\n const bScore = bItem.scores[name];\n const cScore = cItem.scores[name];\n if (bScore === undefined || cScore === undefined || bScore < 0 || cScore < 0) continue;\n const delta = round(cScore - bScore);\n if (delta < -0.1)\n regressions.push({\n input: bItem.input,\n scorer: name,\n baselineScore: bScore,\n candidateScore: cScore,\n delta,\n });\n else if (delta > 0.1)\n improvements.push({\n input: bItem.input,\n scorer: name,\n baselineScore: bScore,\n candidateScore: cScore,\n delta,\n });\n }\n }\n\n const parts: string[] = [];\n for (const name of baselineScorerNames) {\n const s = scorers[name];\n if (Math.abs(s.delta) > 0.001) {\n const direction = s.delta > 0 ? 'improves' : 'regresses';\n const sign = s.delta > 0 ? '+' : '';\n parts.push(\n `'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})`,\n );\n }\n }\n const summaryStr = `candidate ${parts.length > 0 ? parts.join(', ') : 'no significant changes'} with ${regressions.length} regressions and ${improvements.length} improvements`;\n\n return {\n baseline: { id: baseline.id, metadata: baseline.metadata },\n candidate: { id: candidate.id, metadata: candidate.metadata },\n scorers,\n regressions,\n improvements,\n summary: summaryStr,\n };\n}\n","import type { AxlRuntime } from '@axlsdk/axl';\nimport type { EvalConfig, EvalResult, EvalItem, EvalSummary } from './types.js';\nimport { randomUUID } from 'node:crypto';\n\nfunction computeStats(scores: number[]): {\n mean: number;\n min: number;\n max: number;\n p50: number;\n p95: number;\n} {\n if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };\n const sorted = [...scores].sort((a, b) => a - b);\n const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;\n const min = sorted[0];\n const max = sorted[sorted.length - 1];\n const p50 = sorted[Math.floor(sorted.length * 0.5)];\n const p95 = sorted[Math.floor(sorted.length * 0.95)];\n return { mean: round(mean), min: round(min), max: round(max), p50: round(p50), p95: round(p95) };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction parseCost(cost: string): number {\n const match = cost.match(/^\\$?([\\d.]+)$/);\n if (!match) throw new Error(`Invalid cost format: \"${cost}\"`);\n return parseFloat(match[1]);\n}\n\n/** Minimal provider interface for LLM scoring (avoids depending on core axl package). */\ntype LlmScorerProvider = {\n chat(\n messages: { role: string; content: string }[],\n options: { model: string; temperature?: number },\n ): Promise<{ content: string }>;\n};\n\nexport async function runEval(\n config: EvalConfig,\n executeWorkflow: (\n input: unknown,\n runtime: AxlRuntime,\n ) => Promise<{ output: unknown; cost?: number }>,\n provider: LlmScorerProvider | undefined,\n runtime: AxlRuntime,\n): Promise<EvalResult> {\n const startTime = Date.now();\n const id = randomUUID();\n const items = await config.dataset.getItems();\n const concurrency = config.concurrency ?? 5;\n const budgetLimit = config.budget ? parseCost(config.budget) : undefined;\n\n if (provider) {\n for (const scorer of config.scorers) {\n if (scorer.isLlm) {\n (scorer as unknown as { _provider: LlmScorerProvider })._provider = provider;\n }\n }\n }\n\n const evalItems: EvalItem[] = [];\n let totalCost = 0;\n let budgetExceeded = false;\n\n async function processItem(item: (typeof items)[0]): Promise<void> {\n if (budgetExceeded) {\n evalItems.push({\n input: item.input,\n annotations: item.annotations,\n output: null,\n error: 'Budget exceeded',\n scores: {},\n });\n return;\n }\n\n const evalItem: EvalItem = {\n input: item.input,\n annotations: item.annotations,\n output: null,\n scores: {},\n };\n try {\n const result = await executeWorkflow(item.input, runtime);\n evalItem.output = result.output;\n if (result.cost != null) {\n totalCost += result.cost;\n }\n } catch (err) {\n evalItem.error = err instanceof Error ? err.message : String(err);\n evalItems.push(evalItem);\n return;\n }\n\n if (budgetLimit != null && totalCost > budgetLimit) {\n budgetExceeded = true;\n }\n\n for (const scorer of config.scorers) {\n try {\n const score = await scorer.score(evalItem.output, item.input, item.annotations);\n if (score < 0 || score > 1) {\n if (!evalItem.errors) evalItem.errors = [];\n evalItem.errors.push(\n `Scorer \"${scorer.name}\" returned out-of-range score ${score} for input ${JSON.stringify(item.input)}`,\n );\n evalItem.scores[scorer.name] = -1;\n } else {\n evalItem.scores[scorer.name] = round(score);\n }\n } catch (err) {\n if (!evalItem.errors) evalItem.errors = [];\n evalItem.errors.push(\n `Scorer \"${scorer.name}\" threw: ${err instanceof Error ? err.message : String(err)}`,\n );\n evalItem.scores[scorer.name] = -1;\n }\n }\n evalItems.push(evalItem);\n }\n\n let index = 0;\n async function runNext(): Promise<void> {\n while (index < items.length) {\n const currentIndex = index++;\n await processItem(items[currentIndex]);\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());\n await Promise.all(workers);\n\n const failures = evalItems.filter((i) => i.error).length;\n const scorerNames = config.scorers.map((s) => s.name);\n const scorerStats: EvalSummary['scorers'] = {};\n for (const name of scorerNames) {\n const scores = evalItems\n .filter((i) => !i.error && i.scores[name] !== undefined && i.scores[name] >= 0)\n .map((i) => i.scores[name]);\n scorerStats[name] = computeStats(scores);\n }\n\n return {\n id,\n workflow: config.workflow,\n dataset: config.dataset.name,\n metadata: config.metadata ?? {},\n timestamp: new Date().toISOString(),\n totalCost,\n duration: Date.now() - startTime,\n items: evalItems,\n summary: { count: items.length, failures, scorers: scorerStats },\n };\n}\n"],"mappings":"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","names":["round"]}
|
package/dist/cli.cjs
CHANGED
|
@@ -24,9 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
27
|
+
var import_node_fs2 = require("fs");
|
|
28
28
|
var import_promises = require("fs/promises");
|
|
29
29
|
var path = __toESM(require("path"), 1);
|
|
30
|
+
var import_node_url2 = require("url");
|
|
30
31
|
|
|
31
32
|
// src/compare.ts
|
|
32
33
|
function round(n) {
|
|
@@ -221,16 +222,110 @@ async function runEval(config, executeWorkflow, provider, runtime) {
|
|
|
221
222
|
};
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
// src/cli-utils.ts
|
|
226
|
+
var import_node_path = require("path");
|
|
227
|
+
var import_node_fs = require("fs");
|
|
228
|
+
var import_node_url = require("url");
|
|
229
|
+
var CONFIG_CANDIDATES = [
|
|
230
|
+
"axl.config.mts",
|
|
231
|
+
"axl.config.ts",
|
|
232
|
+
"axl.config.mjs",
|
|
233
|
+
"axl.config.js"
|
|
234
|
+
];
|
|
235
|
+
function findConfig(cwd) {
|
|
236
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
237
|
+
const p = (0, import_node_path.resolve)(cwd, name);
|
|
238
|
+
if ((0, import_node_fs.existsSync)(p)) return p;
|
|
239
|
+
}
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
function needsEsmForcing(configPath) {
|
|
243
|
+
const ext = (0, import_node_path.extname)(configPath);
|
|
244
|
+
return ext === ".ts" || ext === ".tsx";
|
|
245
|
+
}
|
|
246
|
+
function needsTsxLoader(configPath) {
|
|
247
|
+
return /\.[mc]?tsx?$/.test(configPath);
|
|
248
|
+
}
|
|
249
|
+
function resolveRuntime(mod) {
|
|
250
|
+
const def = mod.default;
|
|
251
|
+
return def?.default ?? def ?? mod.runtime;
|
|
252
|
+
}
|
|
253
|
+
var tsxRegistered = false;
|
|
254
|
+
async function ensureTsxLoader() {
|
|
255
|
+
if (tsxRegistered) return;
|
|
256
|
+
tsxRegistered = true;
|
|
257
|
+
let loaded = false;
|
|
258
|
+
try {
|
|
259
|
+
const tsxEsm = await import("tsx/esm/api");
|
|
260
|
+
tsxEsm.register();
|
|
261
|
+
loaded = true;
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const tsxCjs = await import("tsx/cjs/api");
|
|
266
|
+
tsxCjs.register();
|
|
267
|
+
loaded = true;
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
if (!loaded) {
|
|
271
|
+
console.warn(
|
|
272
|
+
"[axl-eval] Warning: tsx is not installed. TypeScript files require tsx.\n Install it with: npm install -D tsx"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function forceEsmForConfig(configPath) {
|
|
277
|
+
try {
|
|
278
|
+
const nodeModule = await import("module");
|
|
279
|
+
const configUrl = (0, import_node_url.pathToFileURL)(configPath).href;
|
|
280
|
+
const hookCode = [
|
|
281
|
+
`export async function resolve(specifier, context, nextResolve) {`,
|
|
282
|
+
` const result = await nextResolve(specifier, context);`,
|
|
283
|
+
` if (result.url === ${JSON.stringify(configUrl)}) result.format = 'module';`,
|
|
284
|
+
` return result;`,
|
|
285
|
+
`}`
|
|
286
|
+
].join("\n");
|
|
287
|
+
nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function registerConditions(conditions) {
|
|
292
|
+
try {
|
|
293
|
+
const nodeModule = await import("module");
|
|
294
|
+
const hookCode = [
|
|
295
|
+
`const extra = ${JSON.stringify(conditions)};`,
|
|
296
|
+
`export async function resolve(specifier, context, nextResolve) {`,
|
|
297
|
+
` return nextResolve(specifier, {`,
|
|
298
|
+
` ...context,`,
|
|
299
|
+
` conditions: [...new Set([...context.conditions, ...extra])],`,
|
|
300
|
+
` });`,
|
|
301
|
+
`}`
|
|
302
|
+
].join("\n");
|
|
303
|
+
nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
|
|
304
|
+
} catch {
|
|
305
|
+
console.warn("[axl-eval] Warning: --conditions requires Node.js 20.6+");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
224
309
|
// src/cli.ts
|
|
310
|
+
var KNOWN_FLAGS = /* @__PURE__ */ new Set(["--output", "--config", "--conditions", "--fail-on-regression"]);
|
|
225
311
|
async function main() {
|
|
226
312
|
const args = process.argv.slice(2);
|
|
227
|
-
if (args.length === 0 || args[0] === "--help") {
|
|
313
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
228
314
|
console.log(`
|
|
229
315
|
Usage:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
316
|
+
axl-eval <path> Run eval file(s)
|
|
317
|
+
axl-eval <path> --output <file> Save results to JSON
|
|
318
|
+
axl-eval <path> --config <file> Use config file for runtime
|
|
319
|
+
axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
|
|
320
|
+
axl-eval compare <a> <b> Compare two eval result files
|
|
321
|
+
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
322
|
+
|
|
323
|
+
Config auto-detection (when --config is not specified):
|
|
324
|
+
${CONFIG_CANDIDATES.join(" -> ")}
|
|
325
|
+
|
|
326
|
+
When a config is found, the exported AxlRuntime is passed to executeWorkflow
|
|
327
|
+
and cost is tracked automatically via runtime.trackCost().
|
|
328
|
+
When no config is found, a bare AxlRuntime is created (providers from env vars).
|
|
234
329
|
`);
|
|
235
330
|
process.exit(0);
|
|
236
331
|
}
|
|
@@ -244,7 +339,7 @@ async function runCompare(args) {
|
|
|
244
339
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
245
340
|
const files = args.filter((a) => !a.startsWith("--"));
|
|
246
341
|
if (files.length !== 2) {
|
|
247
|
-
console.error("Usage:
|
|
342
|
+
console.error("Usage: axl-eval compare <baseline.json> <candidate.json>");
|
|
248
343
|
process.exit(1);
|
|
249
344
|
}
|
|
250
345
|
const baseline = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
|
|
@@ -282,10 +377,10 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
282
377
|
function collectEvalFiles(p) {
|
|
283
378
|
const resolved = path.resolve(p);
|
|
284
379
|
try {
|
|
285
|
-
const stat = (0,
|
|
380
|
+
const stat = (0, import_node_fs2.statSync)(resolved);
|
|
286
381
|
if (stat.isDirectory()) {
|
|
287
|
-
const entries = (0,
|
|
288
|
-
return entries.filter((e) =>
|
|
382
|
+
const entries = (0, import_node_fs2.readdirSync)(resolved);
|
|
383
|
+
return entries.filter((e) => /\.eval\.[mc]?[jt]sx?$/.test(e)).map((e) => path.join(resolved, e));
|
|
289
384
|
}
|
|
290
385
|
return [resolved];
|
|
291
386
|
} catch {
|
|
@@ -317,16 +412,104 @@ function formatTable(result) {
|
|
|
317
412
|
);
|
|
318
413
|
return lines.join("\n");
|
|
319
414
|
}
|
|
320
|
-
async function
|
|
415
|
+
async function resolveRuntimeFromConfig(configPath) {
|
|
416
|
+
if (needsTsxLoader(configPath)) {
|
|
417
|
+
await ensureTsxLoader();
|
|
418
|
+
}
|
|
419
|
+
if (needsEsmForcing(configPath)) {
|
|
420
|
+
await forceEsmForConfig(configPath);
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const mod = await import((0, import_node_url2.pathToFileURL)(configPath).href);
|
|
424
|
+
const runtime = resolveRuntime(mod);
|
|
425
|
+
if (!runtime || typeof runtime.execute !== "function") {
|
|
426
|
+
console.error(`Config must export a default AxlRuntime instance.`);
|
|
427
|
+
if (runtime) {
|
|
428
|
+
const keys = Object.keys(runtime).slice(0, 5).join(", ");
|
|
429
|
+
console.error(` Got: ${typeof runtime}${keys ? ` with keys: { ${keys} }` : ""}`);
|
|
430
|
+
}
|
|
431
|
+
console.error(
|
|
432
|
+
`Example:
|
|
433
|
+
import { AxlRuntime } from '@axlsdk/axl';
|
|
434
|
+
export default new AxlRuntime({ ... });`
|
|
435
|
+
);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
return runtime;
|
|
439
|
+
} catch (err) {
|
|
440
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
441
|
+
if (/Cannot use import statement|Unexpected reserved word|top-level await|exports is not defined/.test(
|
|
442
|
+
msg
|
|
443
|
+
)) {
|
|
444
|
+
const ext = path.extname(configPath);
|
|
445
|
+
console.error(`[axl-eval] Config failed to load due to a CJS/ESM compatibility issue.`);
|
|
446
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
447
|
+
const mtsPath = configPath.slice(0, -ext.length) + ".mts";
|
|
448
|
+
console.error(
|
|
449
|
+
` Tip: rename to .mts to force ESM format:
|
|
450
|
+
mv ${configPath} ${mtsPath}`
|
|
451
|
+
);
|
|
452
|
+
} else {
|
|
453
|
+
console.error(` Tip: add "type": "module" to your package.json.`);
|
|
454
|
+
}
|
|
455
|
+
console.error();
|
|
456
|
+
}
|
|
457
|
+
console.error(`Failed to load config:`, err);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function getRuntime(configArg, conditions) {
|
|
462
|
+
if (conditions && conditions.length > 0) {
|
|
463
|
+
await registerConditions(conditions);
|
|
464
|
+
}
|
|
465
|
+
if (configArg) {
|
|
466
|
+
const configPath = path.resolve(process.cwd(), configArg);
|
|
467
|
+
const stat = (0, import_node_fs2.statSync)(configPath, { throwIfNoEntry: false });
|
|
468
|
+
if (!stat?.isFile()) {
|
|
469
|
+
console.error(`Config file not found: ${configPath}`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
console.error(`[axl-eval] Loading config from ${configPath}`);
|
|
473
|
+
return resolveRuntimeFromConfig(configPath);
|
|
474
|
+
}
|
|
475
|
+
const found = findConfig(process.cwd());
|
|
476
|
+
if (found) {
|
|
477
|
+
console.error(`[axl-eval] Auto-detected config: ${found}`);
|
|
478
|
+
return resolveRuntimeFromConfig(found);
|
|
479
|
+
}
|
|
480
|
+
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
481
|
+
return new AxlRuntime();
|
|
482
|
+
}
|
|
483
|
+
function parseEvalArgs(args) {
|
|
321
484
|
let outputPath;
|
|
485
|
+
let configArg;
|
|
486
|
+
let conditions = [];
|
|
322
487
|
const paths = [];
|
|
323
488
|
for (let i = 0; i < args.length; i++) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
489
|
+
const arg = args[i];
|
|
490
|
+
if (arg === "--output" || arg === "--config" || arg === "--conditions") {
|
|
491
|
+
if (i + 1 >= args.length) {
|
|
492
|
+
console.error(`Error: ${arg} requires a value`);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
const value = args[++i];
|
|
496
|
+
if (arg === "--output") outputPath = value;
|
|
497
|
+
else if (arg === "--config") configArg = value;
|
|
498
|
+
else
|
|
499
|
+
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
500
|
+
} else if (arg.startsWith("--")) {
|
|
501
|
+
if (!KNOWN_FLAGS.has(arg)) {
|
|
502
|
+
console.error(`Unknown flag: ${arg}`);
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
paths.push(arg);
|
|
328
507
|
}
|
|
329
508
|
}
|
|
509
|
+
return { outputPath, configArg, conditions, paths };
|
|
510
|
+
}
|
|
511
|
+
async function runEvalCommand(args) {
|
|
512
|
+
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
330
513
|
if (paths.length === 0) {
|
|
331
514
|
console.error("Error: No eval file path provided");
|
|
332
515
|
process.exit(1);
|
|
@@ -339,33 +522,65 @@ async function runEvalCommand(args) {
|
|
|
339
522
|
console.error("Error: No eval files found");
|
|
340
523
|
process.exit(1);
|
|
341
524
|
}
|
|
525
|
+
if (evalFiles.some((f) => /\.[mc]?tsx?$/.test(f))) {
|
|
526
|
+
await ensureTsxLoader();
|
|
527
|
+
}
|
|
528
|
+
const runtime = await getRuntime(configArg, conditions);
|
|
342
529
|
const results = [];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
530
|
+
try {
|
|
531
|
+
for (const filePath of evalFiles) {
|
|
532
|
+
try {
|
|
533
|
+
const mod = await import((0, import_node_url2.pathToFileURL)(path.resolve(filePath)).href);
|
|
534
|
+
const evalConfig = mod.default ?? mod.config ?? mod;
|
|
535
|
+
if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {
|
|
536
|
+
console.error(
|
|
537
|
+
`Error: ${filePath} does not export a valid eval config (missing workflow, dataset, or scorers)`
|
|
538
|
+
);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
let executeWorkflow;
|
|
542
|
+
if (mod.executeWorkflow) {
|
|
543
|
+
executeWorkflow = async (input, rt) => {
|
|
544
|
+
const { result: result2, cost: trackedCost } = await runtime.trackCost(async () => {
|
|
545
|
+
return mod.executeWorkflow(input, rt);
|
|
546
|
+
});
|
|
547
|
+
return { output: result2.output, cost: result2.cost ?? trackedCost };
|
|
548
|
+
};
|
|
549
|
+
} else if (runtime.getWorkflow(evalConfig.workflow)) {
|
|
550
|
+
executeWorkflow = async (input) => {
|
|
551
|
+
const { result: result2, cost } = await runtime.trackCost(async () => {
|
|
552
|
+
return runtime.execute(evalConfig.workflow, input);
|
|
553
|
+
});
|
|
554
|
+
return { output: result2, cost };
|
|
555
|
+
};
|
|
556
|
+
} else {
|
|
557
|
+
console.warn(
|
|
558
|
+
`[axl-eval] Warning: ${filePath} does not export executeWorkflow \u2014 using input passthrough`
|
|
559
|
+
);
|
|
560
|
+
executeWorkflow = async (input) => ({ output: input });
|
|
561
|
+
}
|
|
562
|
+
const result = await runEval(evalConfig, executeWorkflow, mod.provider, runtime);
|
|
563
|
+
results.push(result);
|
|
564
|
+
console.log("\n" + formatTable(result) + "\n");
|
|
565
|
+
} catch (err) {
|
|
348
566
|
console.error(
|
|
349
|
-
`Error
|
|
567
|
+
`Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
350
568
|
);
|
|
351
|
-
continue;
|
|
352
569
|
}
|
|
353
|
-
const executeWorkflow = mod.executeWorkflow ?? (async (input) => ({ output: input }));
|
|
354
|
-
const result = await runEval(evalConfig, executeWorkflow, mod.provider);
|
|
355
|
-
results.push(result);
|
|
356
|
-
console.log("\n" + formatTable(result) + "\n");
|
|
357
|
-
} catch (err) {
|
|
358
|
-
console.error(
|
|
359
|
-
`Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
360
|
-
);
|
|
361
570
|
}
|
|
571
|
+
if (outputPath && results.length > 0) {
|
|
572
|
+
const output = results.length === 1 ? results[0] : results;
|
|
573
|
+
const outputDir = path.dirname(path.resolve(outputPath));
|
|
574
|
+
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
575
|
+
await (0, import_promises.writeFile)(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
576
|
+
console.log(`Results saved to ${outputPath}`);
|
|
577
|
+
}
|
|
578
|
+
} finally {
|
|
579
|
+
await runtime.shutdown().catch(() => {
|
|
580
|
+
});
|
|
362
581
|
}
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
const outputDir = path.dirname(path.resolve(outputPath));
|
|
366
|
-
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
367
|
-
await (0, import_promises.writeFile)(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
368
|
-
console.log(`Results saved to ${outputPath}`);
|
|
582
|
+
if (results.length === 0) {
|
|
583
|
+
process.exit(1);
|
|
369
584
|
}
|
|
370
585
|
}
|
|
371
586
|
main().catch((err) => {
|