@axlsdk/eval 0.10.4 → 0.11.1
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 +218 -36
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +217 -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":";AAEA,SAAS,MAAM,GAAmB;AAChC,SAAO,KAAK,MAAM,IAAI,GAAG,IAAI;AAC/B;AAEO,SAAS,YAAY,UAAsB,WAAuC;AACvF,MAAI,SAAS,YAAY,UAAU,SAAS;AAC1C,UAAM,IAAI;AAAA,MACR,kDAAkD,SAAS,OAAO,SAAS,UAAU,OAAO;AAAA,IAC9F;AAAA,EACF;AAEA,QAAM,sBAAsB,OAAO,KAAK,SAAS,QAAQ,OAAO,EAAE,KAAK;AACvE,QAAM,uBAAuB,OAAO,KAAK,UAAU,QAAQ,OAAO,EAAE,KAAK;AACzE,MAAI,KAAK,UAAU,mBAAmB,MAAM,KAAK,UAAU,oBAAoB,GAAG;AAChF,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AAEA,QAAM,UAAqC,CAAC;AAC5C,aAAW,QAAQ,qBAAqB;AACtC,UAAM,QAAQ,SAAS,QAAQ,QAAQ,IAAI,EAAE;AAC7C,UAAM,QAAQ,UAAU,QAAQ,QAAQ,IAAI,EAAE;AAC9C,UAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,UAAM,eAAe,QAAQ,IAAI,MAAO,QAAQ,QAAS,GAAG,IAAI;AAChE,YAAQ,IAAI,IAAI,EAAE,cAAc,OAAO,eAAe,OAAO,OAAO,aAAa;AAAA,EACnF;AAEA,QAAM,cAAgC,CAAC;AACvC,QAAM,eAAkC,CAAC;AAEzC,QAAM,YAAY,KAAK,IAAI,SAAS,MAAM,QAAQ,UAAU,MAAM,MAAM;AACxE,WAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,UAAM,QAAQ,SAAS,MAAM,CAAC;AAC9B,UAAM,QAAQ,UAAU,MAAM,CAAC;AAC/B,QAAI,MAAM,SAAS,MAAM,MAAO;AAChC,eAAW,QAAQ,qBAAqB;AACtC,YAAM,SAAS,MAAM,OAAO,IAAI;AAChC,YAAM,SAAS,MAAM,OAAO,IAAI;AAChC,UAAI,WAAW,UAAa,WAAW,UAAa,SAAS,KAAK,SAAS,EAAG;AAC9E,YAAM,QAAQ,MAAM,SAAS,MAAM;AACnC,UAAI,QAAQ;AACV,oBAAY,KAAK;AAAA,UACf,OAAO,MAAM;AAAA,UACb,QAAQ;AAAA,UACR,eAAe;AAAA,UACf,gBAAgB;AAAA,UAChB;AAAA,QACF,CAAC;AAAA,eACM,QAAQ;AACf,qBAAa,KAAK;AAAA,UAChB,OAAO,MAAM;AAAA,UACb,QAAQ;AAAA,UACR,eAAe;AAAA,UACf,gBAAgB;AAAA,UAChB;AAAA,QACF,CAAC;AAAA,IACL;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,qBAAqB;AACtC,UAAM,IAAI,QAAQ,IAAI;AACtB,QAAI,KAAK,IAAI,EAAE,KAAK,IAAI,MAAO;AAC7B,YAAM,YAAY,EAAE,QAAQ,IAAI,aAAa;AAC7C,YAAM,OAAO,EAAE,QAAQ,IAAI,MAAM;AACjC,YAAM;AAAA,QACJ,IAAI,IAAI,KAAK,SAAS,OAAO,IAAI,GAAG,EAAE,aAAa,QAAQ,CAAC,CAAC,MAAM,EAAE,aAAa,QAAQ,CAAC,CAAC,OAAO,EAAE,cAAc,QAAQ,CAAC,CAAC;AAAA,MAC/H;AAAA,IACF;AAAA,EACF;AACA,QAAM,aAAa,aAAa,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,wBAAwB,SAAS,YAAY,MAAM,oBAAoB,aAAa,MAAM;AAEhK,SAAO;AAAA,IACL,UAAU,EAAE,IAAI,SAAS,IAAI,UAAU,SAAS,SAAS;AAAA,IACzD,WAAW,EAAE,IAAI,UAAU,IAAI,UAAU,UAAU,SAAS;AAAA,IAC5D;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,EACX;AACF;;;AC/EA,SAAS,kBAAkB;AAE3B,SAAS,aAAa,QAMpB;AACA,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,EAAE;AAC1E,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC/C,QAAM,OAAO,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,OAAO;AACxD,QAAM,MAAM,OAAO,CAAC;AACpB,QAAM,MAAM,OAAO,OAAO,SAAS,CAAC;AACpC,QAAM,MAAM,OAAO,KAAK,MAAM,OAAO,SAAS,GAAG,CAAC;AAClD,QAAM,MAAM,OAAO,KAAK,MAAM,OAAO,SAAS,IAAI,CAAC;AACnD,SAAO,EAAE,MAAMA,OAAM,IAAI,GAAG,KAAKA,OAAM,GAAG,GAAG,KAAKA,OAAM,GAAG,GAAG,KAAKA,OAAM,GAAG,GAAG,KAAKA,OAAM,GAAG,EAAE;AACjG;AAEA,SAASA,OAAM,GAAmB;AAChC,SAAO,KAAK,MAAM,IAAI,GAAI,IAAI;AAChC;AAEA,SAAS,UAAU,MAAsB;AACvC,QAAM,QAAQ,KAAK,MAAM,eAAe;AACxC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,yBAAyB,IAAI,GAAG;AAC5D,SAAO,WAAW,MAAM,CAAC,CAAC;AAC5B;AAUA,eAAsB,QACpB,QACA,iBAIA,UACA,SACqB;AACrB,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,KAAK,WAAW;AACtB,QAAM,QAAQ,MAAM,OAAO,QAAQ,SAAS;AAC5C,QAAM,cAAc,OAAO,eAAe;AAC1C,QAAM,cAAc,OAAO,SAAS,UAAU,OAAO,MAAM,IAAI;AAE/D,MAAI,UAAU;AACZ,eAAW,UAAU,OAAO,SAAS;AACnC,UAAI,OAAO,OAAO;AAChB,QAAC,OAAuD,YAAY;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAwB,CAAC;AAC/B,MAAI,YAAY;AAChB,MAAI,iBAAiB;AAErB,iBAAe,YAAY,MAAwC;AACjE,QAAI,gBAAgB;AAClB,gBAAU,KAAK;AAAA,QACb,OAAO,KAAK;AAAA,QACZ,aAAa,KAAK;AAAA,QAClB,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,QAAQ,CAAC;AAAA,MACX,CAAC;AACD;AAAA,IACF;AAEA,UAAM,WAAqB;AAAA,MACzB,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,QAAQ;AAAA,MACR,QAAQ,CAAC;AAAA,IACX;AACA,QAAI;AACF,YAAM,SAAS,MAAM,gBAAgB,KAAK,OAAO,OAAO;AACxD,eAAS,SAAS,OAAO;AACzB,UAAI,OAAO,QAAQ,MAAM;AACvB,qBAAa,OAAO;AAAA,MACtB;AAAA,IACF,SAAS,KAAK;AACZ,eAAS,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAChE,gBAAU,KAAK,QAAQ;AACvB;AAAA,IACF;AAEA,QAAI,eAAe,QAAQ,YAAY,aAAa;AAClD,uBAAiB;AAAA,IACnB;AAEA,eAAW,UAAU,OAAO,SAAS;AACnC,UAAI;AACF,cAAM,QAAQ,MAAM,OAAO,MAAM,SAAS,QAAQ,KAAK,OAAO,KAAK,WAAW;AAC9E,YAAI,QAAQ,KAAK,QAAQ,GAAG;AAC1B,cAAI,CAAC,SAAS,OAAQ,UAAS,SAAS,CAAC;AACzC,mBAAS,OAAO;AAAA,YACd,WAAW,OAAO,IAAI,iCAAiC,KAAK,cAAc,KAAK,UAAU,KAAK,KAAK,CAAC;AAAA,UACtG;AACA,mBAAS,OAAO,OAAO,IAAI,IAAI;AAAA,QACjC,OAAO;AACL,mBAAS,OAAO,OAAO,IAAI,IAAIA,OAAM,KAAK;AAAA,QAC5C;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,CAAC,SAAS,OAAQ,UAAS,SAAS,CAAC;AACzC,iBAAS,OAAO;AAAA,UACd,WAAW,OAAO,IAAI,YAAY,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QACpF;AACA,iBAAS,OAAO,OAAO,IAAI,IAAI;AAAA,MACjC;AAAA,IACF;AACA,cAAU,KAAK,QAAQ;AAAA,EACzB;AAEA,MAAI,QAAQ;AACZ,iBAAe,UAAyB;AACtC,WAAO,QAAQ,MAAM,QAAQ;AAC3B,YAAM,eAAe;AACrB,YAAM,YAAY,MAAM,YAAY,CAAC;AAAA,IACvC;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,MAAM,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC;AAC3F,QAAM,QAAQ,IAAI,OAAO;AAEzB,QAAM,WAAW,UAAU,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE;AAClD,QAAM,cAAc,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI;AACpD,QAAM,cAAsC,CAAC;AAC7C,aAAW,QAAQ,aAAa;AAC9B,UAAM,SAAS,UACZ,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,IAAI,MAAM,UAAa,EAAE,OAAO,IAAI,KAAK,CAAC,EAC7E,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC;AAC5B,gBAAY,IAAI,IAAI,aAAa,MAAM;AAAA,EACzC;AAEA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,SAAS,OAAO,QAAQ;AAAA,IACxB,UAAU,OAAO,YAAY,CAAC;AAAA,IAC9B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC;AAAA,IACA,UAAU,KAAK,IAAI,IAAI;AAAA,IACvB,OAAO;AAAA,IACP,SAAS,EAAE,OAAO,MAAM,QAAQ,UAAU,SAAS,YAAY;AAAA,EACjE;AACF;","names":["round"]}
|
package/dist/cli.cjs
CHANGED
|
@@ -24,7 +24,7 @@ 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
30
|
|
|
@@ -221,16 +221,89 @@ async function runEval(config, executeWorkflow, provider, runtime) {
|
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// src/cli-utils.ts
|
|
225
|
+
var import_node_path = require("path");
|
|
226
|
+
var import_node_fs = require("fs");
|
|
227
|
+
var import_node_url = require("url");
|
|
228
|
+
var CONFIG_CANDIDATES = [
|
|
229
|
+
"axl.config.mts",
|
|
230
|
+
"axl.config.ts",
|
|
231
|
+
"axl.config.mjs",
|
|
232
|
+
"axl.config.js"
|
|
233
|
+
];
|
|
234
|
+
function findConfig(cwd) {
|
|
235
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
236
|
+
const p = (0, import_node_path.resolve)(cwd, name);
|
|
237
|
+
if ((0, import_node_fs.existsSync)(p)) return p;
|
|
238
|
+
}
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
241
|
+
function needsTsxLoader(filePath) {
|
|
242
|
+
return /\.[mc]?tsx?$/.test(filePath);
|
|
243
|
+
}
|
|
244
|
+
function resolveRuntime(mod) {
|
|
245
|
+
const def = mod.default;
|
|
246
|
+
return def?.default ?? def ?? mod.runtime;
|
|
247
|
+
}
|
|
248
|
+
var tsImportFn;
|
|
249
|
+
async function importModule(filePath, parentURL) {
|
|
250
|
+
if (needsTsxLoader(filePath)) {
|
|
251
|
+
if (tsImportFn === void 0) {
|
|
252
|
+
try {
|
|
253
|
+
const mod = await import("tsx/esm/api");
|
|
254
|
+
tsImportFn = mod.tsImport ?? null;
|
|
255
|
+
} catch {
|
|
256
|
+
tsImportFn = null;
|
|
257
|
+
console.warn(
|
|
258
|
+
"[axl-eval] Warning: tsx is not installed. TypeScript files require tsx.\n Install it with: npm install -D tsx"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (tsImportFn) {
|
|
263
|
+
return await tsImportFn((0, import_node_url.pathToFileURL)(filePath).href, parentURL);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return await import((0, import_node_url.pathToFileURL)(filePath).href);
|
|
267
|
+
}
|
|
268
|
+
async function registerConditions(conditions) {
|
|
269
|
+
try {
|
|
270
|
+
const nodeModule = await import("module");
|
|
271
|
+
const hookCode = [
|
|
272
|
+
`const extra = ${JSON.stringify(conditions)};`,
|
|
273
|
+
`export async function resolve(specifier, context, nextResolve) {`,
|
|
274
|
+
` return nextResolve(specifier, {`,
|
|
275
|
+
` ...context,`,
|
|
276
|
+
` conditions: [...new Set([...context.conditions, ...extra])],`,
|
|
277
|
+
` });`,
|
|
278
|
+
`}`
|
|
279
|
+
].join("\n");
|
|
280
|
+
nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
|
|
281
|
+
} catch {
|
|
282
|
+
console.warn("[axl-eval] Warning: --conditions requires Node.js 20.6+");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
224
286
|
// src/cli.ts
|
|
287
|
+
var import_meta = {};
|
|
288
|
+
var KNOWN_FLAGS = /* @__PURE__ */ new Set(["--output", "--config", "--conditions", "--fail-on-regression"]);
|
|
225
289
|
async function main() {
|
|
226
290
|
const args = process.argv.slice(2);
|
|
227
|
-
if (args.length === 0 || args[0] === "--help") {
|
|
291
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
228
292
|
console.log(`
|
|
229
293
|
Usage:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
294
|
+
axl-eval <path> Run eval file(s)
|
|
295
|
+
axl-eval <path> --output <file> Save results to JSON
|
|
296
|
+
axl-eval <path> --config <file> Use config file for runtime
|
|
297
|
+
axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
|
|
298
|
+
axl-eval compare <a> <b> Compare two eval result files
|
|
299
|
+
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
300
|
+
|
|
301
|
+
Config auto-detection (when --config is not specified):
|
|
302
|
+
${CONFIG_CANDIDATES.join(" -> ")}
|
|
303
|
+
|
|
304
|
+
When a config is found, the exported AxlRuntime is passed to executeWorkflow
|
|
305
|
+
and cost is tracked automatically via runtime.trackCost().
|
|
306
|
+
When no config is found, a bare AxlRuntime is created (providers from env vars).
|
|
234
307
|
`);
|
|
235
308
|
process.exit(0);
|
|
236
309
|
}
|
|
@@ -244,7 +317,7 @@ async function runCompare(args) {
|
|
|
244
317
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
245
318
|
const files = args.filter((a) => !a.startsWith("--"));
|
|
246
319
|
if (files.length !== 2) {
|
|
247
|
-
console.error("Usage:
|
|
320
|
+
console.error("Usage: axl-eval compare <baseline.json> <candidate.json>");
|
|
248
321
|
process.exit(1);
|
|
249
322
|
}
|
|
250
323
|
const baseline = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
|
|
@@ -282,10 +355,10 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
282
355
|
function collectEvalFiles(p) {
|
|
283
356
|
const resolved = path.resolve(p);
|
|
284
357
|
try {
|
|
285
|
-
const stat = (0,
|
|
358
|
+
const stat = (0, import_node_fs2.statSync)(resolved);
|
|
286
359
|
if (stat.isDirectory()) {
|
|
287
|
-
const entries = (0,
|
|
288
|
-
return entries.filter((e) =>
|
|
360
|
+
const entries = (0, import_node_fs2.readdirSync)(resolved);
|
|
361
|
+
return entries.filter((e) => /\.eval\.[mc]?[jt]sx?$/.test(e)).map((e) => path.join(resolved, e));
|
|
289
362
|
}
|
|
290
363
|
return [resolved];
|
|
291
364
|
} catch {
|
|
@@ -317,16 +390,96 @@ function formatTable(result) {
|
|
|
317
390
|
);
|
|
318
391
|
return lines.join("\n");
|
|
319
392
|
}
|
|
320
|
-
async function
|
|
393
|
+
async function resolveRuntimeFromConfig(configPath) {
|
|
394
|
+
try {
|
|
395
|
+
const mod = await importModule(configPath, import_meta.url);
|
|
396
|
+
const runtime = resolveRuntime(mod);
|
|
397
|
+
if (!runtime || typeof runtime.execute !== "function") {
|
|
398
|
+
console.error(`Config must export a default AxlRuntime instance.`);
|
|
399
|
+
if (runtime) {
|
|
400
|
+
const keys = Object.keys(runtime).slice(0, 5).join(", ");
|
|
401
|
+
console.error(` Got: ${typeof runtime}${keys ? ` with keys: { ${keys} }` : ""}`);
|
|
402
|
+
}
|
|
403
|
+
console.error(
|
|
404
|
+
`Example:
|
|
405
|
+
import { AxlRuntime } from '@axlsdk/axl';
|
|
406
|
+
export default new AxlRuntime({ ... });`
|
|
407
|
+
);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
return runtime;
|
|
411
|
+
} catch (err) {
|
|
412
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
413
|
+
if (/Cannot use import statement|Unexpected reserved word|top-level await|exports is not defined/.test(
|
|
414
|
+
msg
|
|
415
|
+
)) {
|
|
416
|
+
const ext = path.extname(configPath);
|
|
417
|
+
console.error(`[axl-eval] Config failed to load due to a CJS/ESM compatibility issue.`);
|
|
418
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
419
|
+
console.error(
|
|
420
|
+
` Tip: try renaming to .mts to force ESM format, or ensure tsx is installed and up to date.`
|
|
421
|
+
);
|
|
422
|
+
} else {
|
|
423
|
+
console.error(` Tip: add "type": "module" to your package.json.`);
|
|
424
|
+
}
|
|
425
|
+
console.error();
|
|
426
|
+
}
|
|
427
|
+
console.error(`Failed to load config:`, err);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function getRuntime(configArg, conditions) {
|
|
432
|
+
if (conditions && conditions.length > 0) {
|
|
433
|
+
await registerConditions(conditions);
|
|
434
|
+
}
|
|
435
|
+
if (configArg) {
|
|
436
|
+
const configPath = path.resolve(process.cwd(), configArg);
|
|
437
|
+
const stat = (0, import_node_fs2.statSync)(configPath, { throwIfNoEntry: false });
|
|
438
|
+
if (!stat?.isFile()) {
|
|
439
|
+
console.error(`Config file not found: ${configPath}`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
console.error(`[axl-eval] Loading config from ${configPath}`);
|
|
443
|
+
return resolveRuntimeFromConfig(configPath);
|
|
444
|
+
}
|
|
445
|
+
const found = findConfig(process.cwd());
|
|
446
|
+
if (found) {
|
|
447
|
+
console.error(`[axl-eval] Auto-detected config: ${found}`);
|
|
448
|
+
return resolveRuntimeFromConfig(found);
|
|
449
|
+
}
|
|
450
|
+
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
451
|
+
return new AxlRuntime();
|
|
452
|
+
}
|
|
453
|
+
function parseEvalArgs(args) {
|
|
321
454
|
let outputPath;
|
|
455
|
+
let configArg;
|
|
456
|
+
let conditions = [];
|
|
322
457
|
const paths = [];
|
|
323
458
|
for (let i = 0; i < args.length; i++) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
459
|
+
const arg = args[i];
|
|
460
|
+
if (arg === "--output" || arg === "--config" || arg === "--conditions") {
|
|
461
|
+
if (i + 1 >= args.length) {
|
|
462
|
+
console.error(`Error: ${arg} requires a value`);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
const value = args[++i];
|
|
466
|
+
if (arg === "--output") outputPath = value;
|
|
467
|
+
else if (arg === "--config") configArg = value;
|
|
468
|
+
else
|
|
469
|
+
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
470
|
+
} else if (arg.startsWith("--")) {
|
|
471
|
+
if (!KNOWN_FLAGS.has(arg)) {
|
|
472
|
+
console.error(`Unknown flag: ${arg}`);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
paths.push(arg);
|
|
328
477
|
}
|
|
329
478
|
}
|
|
479
|
+
return { outputPath, configArg, conditions, paths };
|
|
480
|
+
}
|
|
481
|
+
async function runEvalCommand(args) {
|
|
482
|
+
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
330
483
|
if (paths.length === 0) {
|
|
331
484
|
console.error("Error: No eval file path provided");
|
|
332
485
|
process.exit(1);
|
|
@@ -339,33 +492,62 @@ async function runEvalCommand(args) {
|
|
|
339
492
|
console.error("Error: No eval files found");
|
|
340
493
|
process.exit(1);
|
|
341
494
|
}
|
|
495
|
+
const runtime = await getRuntime(configArg, conditions);
|
|
342
496
|
const results = [];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
497
|
+
try {
|
|
498
|
+
for (const filePath of evalFiles) {
|
|
499
|
+
try {
|
|
500
|
+
const mod = await importModule(path.resolve(filePath), import_meta.url);
|
|
501
|
+
const evalConfig = mod.default ?? mod.config ?? mod;
|
|
502
|
+
if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {
|
|
503
|
+
console.error(
|
|
504
|
+
`Error: ${filePath} does not export a valid eval config (missing workflow, dataset, or scorers)`
|
|
505
|
+
);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
let executeWorkflow;
|
|
509
|
+
if (mod.executeWorkflow) {
|
|
510
|
+
executeWorkflow = async (input, rt) => {
|
|
511
|
+
const { result: result2, cost: trackedCost } = await runtime.trackCost(async () => {
|
|
512
|
+
return mod.executeWorkflow(input, rt);
|
|
513
|
+
});
|
|
514
|
+
return { output: result2.output, cost: result2.cost ?? trackedCost };
|
|
515
|
+
};
|
|
516
|
+
} else if (runtime.getWorkflow(evalConfig.workflow)) {
|
|
517
|
+
executeWorkflow = async (input) => {
|
|
518
|
+
const { result: result2, cost } = await runtime.trackCost(async () => {
|
|
519
|
+
return runtime.execute(evalConfig.workflow, input);
|
|
520
|
+
});
|
|
521
|
+
return { output: result2, cost };
|
|
522
|
+
};
|
|
523
|
+
} else {
|
|
524
|
+
console.warn(
|
|
525
|
+
`[axl-eval] Warning: ${filePath} does not export executeWorkflow \u2014 using input passthrough`
|
|
526
|
+
);
|
|
527
|
+
executeWorkflow = async (input) => ({ output: input });
|
|
528
|
+
}
|
|
529
|
+
const result = await runEval(evalConfig, executeWorkflow, mod.provider, runtime);
|
|
530
|
+
results.push(result);
|
|
531
|
+
console.log("\n" + formatTable(result) + "\n");
|
|
532
|
+
} catch (err) {
|
|
348
533
|
console.error(
|
|
349
|
-
`Error
|
|
534
|
+
`Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
350
535
|
);
|
|
351
|
-
continue;
|
|
352
536
|
}
|
|
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
537
|
}
|
|
538
|
+
if (outputPath && results.length > 0) {
|
|
539
|
+
const output = results.length === 1 ? results[0] : results;
|
|
540
|
+
const outputDir = path.dirname(path.resolve(outputPath));
|
|
541
|
+
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
542
|
+
await (0, import_promises.writeFile)(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
543
|
+
console.log(`Results saved to ${outputPath}`);
|
|
544
|
+
}
|
|
545
|
+
} finally {
|
|
546
|
+
await runtime.shutdown().catch(() => {
|
|
547
|
+
});
|
|
362
548
|
}
|
|
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}`);
|
|
549
|
+
if (results.length === 0) {
|
|
550
|
+
process.exit(1);
|
|
369
551
|
}
|
|
370
552
|
}
|
|
371
553
|
main().catch((err) => {
|