Skip to content

Commit 6b4d014

Browse files
authored
docs(transform): publish promptfoo file transform example (#1661)
1 parent 6f92049 commit 6b4d014

11 files changed

Lines changed: 164 additions & 83 deletions

File tree

apps/web/src/content/docs/docs/next/evaluation/eval-cases.mdx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -410,18 +410,7 @@ when the agent output is a `ContentFile` block rather than plain text:
410410
```yaml
411411
default_test:
412412
options:
413-
transform: >-
414-
return (() => {
415-
const file = Array.isArray(output)
416-
? output.find((block) => block?.type === "file" && block.path?.endsWith(".xlsx"))
417-
: undefined;
418-
if (!file) return output;
419-
const result = Bun.spawnSync(["bun", "run", "scripts/transforms/xlsx-to-csv.ts"], {
420-
stdin: JSON.stringify({ path: file.path, media_type: file.media_type })
421-
});
422-
if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr).trim());
423-
return new TextDecoder().decode(result.stdout).trim();
424-
})()
413+
transform: file://scripts/transforms/xlsx-to-csv.ts
425414
426415
tests:
427416
- id: spreadsheet-eval
@@ -493,6 +482,12 @@ tests:
493482
value: "fix"
494483
```
495484

485+
`default_test.options.transform` is inherited by every test. A
486+
`tests[].options.transform` entry overrides that inherited transform for one
487+
test, and assertion-level `transform` applies only to that assertion after the
488+
test/default transform has produced the candidate text for the rest of the
489+
graders.
490+
496491
## Metadata
497492

498493
Pass additional context through the `metadata` field:

apps/web/src/content/docs/docs/next/evaluation/examples.mdx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,7 @@ description: Grade spreadsheet output via a transform
111111
target: file_output
112112
default_test:
113113
options:
114-
transform: >-
115-
return (() => {
116-
const file = Array.isArray(output)
117-
? output.find((block) => block?.type === "file" && block.path?.endsWith(".xlsx"))
118-
: undefined;
119-
if (!file) return output;
120-
const result = Bun.spawnSync(["bun", "run", "../scripts/transforms/xlsx-to-csv.ts"], {
121-
stdin: JSON.stringify({ path: file.path, media_type: file.media_type })
122-
});
123-
if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr).trim());
124-
return new TextDecoder().decode(result.stdout).trim();
125-
})()
114+
transform: file://../scripts/transforms/xlsx-to-csv.ts
126115
127116
tests:
128117
- id: spreadsheet-output

apps/web/src/content/docs/docs/next/graders/llm-graders.mdx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -201,18 +201,7 @@ AgentV always tries a default UTF-8 text read first. That is enough for text-bas
201201
```yaml
202202
default_test:
203203
options:
204-
transform: >-
205-
return (() => {
206-
const file = Array.isArray(output)
207-
? output.find((block) => block?.type === "file" && block.path?.endsWith(".xlsx"))
208-
: undefined;
209-
if (!file) return output;
210-
const result = Bun.spawnSync(["bun", "run", "scripts/transforms/xlsx-to-csv.ts"], {
211-
stdin: JSON.stringify({ path: file.path, media_type: file.media_type })
212-
});
213-
if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr).trim());
214-
return new TextDecoder().decode(result.stdout).trim();
215-
})()
204+
transform: file://scripts/transforms/xlsx-to-csv.ts
216205
217206
tests:
218207
- id: spreadsheet-output
@@ -230,8 +219,9 @@ tests:
230219

231220
Resolution order:
232221

233-
- `tests[].options.transform` overrides `default_test.options.transform`
234-
- assertion-level `transform` applies to that grader only
222+
- `default_test.options.transform` applies to every test unless overridden
223+
- `tests[].options.transform` overrides `default_test.options.transform` for that test
224+
- assertion-level `transform` applies to that grader only, after the test/default transform
235225
- if no transform is configured, AgentV falls back to a UTF-8 text read
236226
- if the fallback read looks binary or invalid, the grader receives a warning note instead of failing the test run
237227

apps/web/src/content/docs/docs/next/reference/promptfoo-parity.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ implements equivalent semantics directly.
2424
| Align with Promptfoo | AgentV accepts the same concept, with `snake_case` where the field crosses the YAML boundary. |
2525
| Keep AgentV divergence | AgentV intentionally uses a different shape because it is clearer for repo-native agent evals. |
2626
| Keep AgentV extension | AgentV adds a capability that does not try to be Promptfoo-compatible. |
27+
| Removed/rejected surface | AgentV rejects this authored surface and points to the current supported field. |
2728
| Defer/future-scope | AgentV does not accept the Promptfoo surface yet. Use an AgentV primitive or wait for direct implementation. |
2829

2930
## Authored Config Matrix
@@ -34,11 +35,14 @@ implements equivalent semantics directly.
3435
| Test rows | `tests` can be inline rows or a case-file reference; rows carry `vars`, `assert`, metadata, prompt/provider filters, and expected data. | `tests` can be inline rows or a raw-case path; rows carry `vars`, `assert`, `expected_output`, metadata, workspace overrides, and run overrides. | Align with Promptfoo | AgentV also supports `imports.suites` and `imports.tests` for explicit composition. Raw cases do not own suite context. |
3536
| Variables | `tests[].vars` plus `defaultTest.vars`; prompt templates can reference top-level var names. | `tests[].vars` plus `default_test.vars`; templates can use `{{ name }}` or `{{ vars.name }}`. | Align with Promptfoo | Per-test vars override default vars by key. |
3637
| Default test | `defaultTest`, inline object or `file://` reference. | `default_test`, inline object or `file://` / `ref://` reference. | Align with Promptfoo | AgentV uses `snake_case` for YAML. Shared prompt matrix defaults belong in `default_test.vars`. |
38+
| Output transform | `defaultTest.options.transform`, `tests[].options.transform`, and assertion-level `transform`. | `default_test.options.transform`, `tests[].options.transform`, and assertion-level `transform`. | Align with Promptfoo | Use `transform` to shape provider output before grading, including file-output conversions such as `.xlsx` to text. `tests[].options.transform` overrides the inherited default transform; assertion-level `transform` is scoped to one grader. |
39+
| Deprecated `postprocess` | Older Promptfoo configs may still mention `postprocess`. | Rejected. | Removed/rejected surface | Use `transform`. AgentV intentionally does not adopt Promptfoo's deprecated `postprocess` spelling. |
3740
| Evaluate options | `evaluateOptions` for runtime controls. | `evaluate_options` for runtime controls. | Align with Promptfoo | AgentV uses `evaluate_options.repeat`, `evaluate_options.budget_usd`, and `evaluate_options.max_concurrency`. |
3841
| Authored concurrency | Common Promptfoo usage includes runtime options such as `maxConcurrency`. | `evaluate_options.max_concurrency`. | Keep AgentV divergence | Do not author `execution.max_concurrency` or top-level `workers` in eval YAML. CLI `--workers` remains an operator override. |
3942
| Target selection | Promptfoo normal evals use `providers`; `targets` can alias providers in unified config. | Use top-level `target` for one system under test or top-level `targets` for a target matrix. | Keep AgentV divergence | AgentV reserves `provider` for the backend/adapter kind inside a target object. Top-level `providers` is rejected to avoid overloading that term. |
4043
| Target object identity | Provider options often use `id` for backend/provider spec and optional `label` for display or matching. | Target objects use stable `id` for target identity, `provider` for backend kind, optional `runtime`, and `config` for provider settings. | Keep AgentV divergence | AgentV does not copy Promptfoo's `label`/`id` baggage because `provider` already names the backend boundary. |
4144
| Direct authored input | Promptfoo prompt authoring normally goes through `prompts` plus vars. | Top-level `input` and inline `tests[].input` are removed from normal authored eval YAML. External raw-case imports may still carry internal input rows for compatibility. | Removed AgentV extension | Author prompt text, chat/system/user messages, and file-backed prompt content as `prompts`; put row data in `tests[].vars` and shared defaults in `default_test.vars`. |
45+
| Authored preprocessors | Not Promptfoo's canonical output-shaping surface. | Rejected in current authored YAML. | Removed/rejected surface | Use `transform` at `default_test.options`, `tests[].options`, or the assertion that needs the shaped output. Historical versioned docs may still show old preprocessor examples. |
4246
| Suite assertions | `assert` entries can be strings or typed assertion objects. | `assert` entries can be strings, typed assertion objects, script graders, or AgentV extension graders. | Align with Promptfoo | Plain strings become semantic rubric checks. Use `assert`, not `assertions`, in current authored eval YAML. |
4347
| Assertion grouping | `type: assert-set` with child `assert` entries, optional `config`, `metric`, `weight`, and `threshold`. | `type: assert-set` with child `assert`, optional `config`, metric names, weights, and parent threshold. | Align with Promptfoo | Parent `config` is inherited by child assertions; child `config` keys override shared parent keys. Without `threshold`, pass/fail follows nonzero-weight child assertions. With `threshold`, the weighted aggregate score determines pass/fail. `type: composite` is rejected; use `assert-set`. |
4448
| Deterministic assertion vocabulary | Common Promptfoo types include `contains`, `icontains`, `contains-any`, `contains-all`, `starts-with`, `regex`, `is-json`, `equals`, `latency`, `cost`, `javascript`, `python`, `webhook`, `similar`, and `llm-rubric`. | AgentV accepts the implemented overlap, including `contains`, `icontains`, `contains-any`, `contains-all`, `starts-with`, `regex`, `is-json`, `equals`, `latency`, `cost`, `javascript`, `python`, `webhook`, `similar`, and `llm-rubric`. | Align with Promptfoo | Unsupported Promptfoo assertion names error instead of silently becoming custom assertion names. |

examples/features/file-transforms/.agentv/providers/grader-check.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ if (!promptFile || !outputFile) {
99

1010
const prompt = readFileSync(promptFile, 'utf8');
1111
const passed = prompt.includes('spreadsheet: revenue,total') && prompt.includes('Q1,42');
12+
const rubricIds = Array.from(prompt.matchAll(/- \[(rubric-\d+)\]/g), (match) => match[1]);
13+
const checkIds = rubricIds.length > 0 ? rubricIds : ['rubric-1'];
1214

1315
writeFileSync(
1416
outputFile,
1517
JSON.stringify({
1618
text: JSON.stringify({
17-
score: passed ? 1 : 0,
18-
assertions: [
19-
{
20-
text: 'transformed file content reached the llm grader',
21-
passed,
22-
evidence: passed
23-
? 'found transformed spreadsheet text in prompt'
24-
: 'transformed spreadsheet text missing from prompt',
25-
},
26-
],
19+
checks: checkIds.map((id) => ({
20+
id,
21+
satisfied: passed,
22+
reasoning: passed
23+
? 'found transformed spreadsheet text in prompt'
24+
: 'transformed spreadsheet text missing from prompt',
25+
})),
26+
overall_reasoning: passed
27+
? 'The transformed spreadsheet rows reached the rubric prompt.'
28+
: 'The transformed spreadsheet rows did not reach the rubric prompt.',
2729
}),
2830
}),
2931
'utf8',
Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
$schema: agentv-targets-v2.2
22
targets:
33
- id: file_output
4-
provider: file-output
5-
command: bun run .agentv/providers/file-output.ts {OUTPUT_FILE}
6-
cwd: ..
4+
provider: cli
5+
runtime: host
6+
config:
7+
command: bun run .agentv/providers/file-output.ts {OUTPUT_FILE}
8+
cwd: ..
79
grader_target: grader_check
810

911
- id: grader_check
10-
provider: grader-check
11-
command: bun run .agentv/providers/grader-check.ts {PROMPT_FILE} {OUTPUT_FILE}
12-
cwd: ..
12+
provider: cli
13+
runtime: host
14+
config:
15+
command: bun run .agentv/providers/grader-check.ts {PROMPT_FILE} {OUTPUT_FILE}
16+
cwd: ..
17+
18+
- id: file_output_live
19+
provider: cli
20+
runtime: host
21+
config:
22+
command: bun run .agentv/providers/file-output.ts {OUTPUT_FILE}
23+
cwd: ..
24+
grader_target: local-openai-grader
25+
26+
- id: local-openai-grader
27+
provider: openai
28+
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
29+
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
30+
model: "{{ env.LOCAL_OPENAI_PROXY_MODEL }}"

examples/features/file-transforms/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Demonstrates how `default_test.options.transform` turns `ContentFile` outputs in
88
- an agent target returning a `ContentFile` block instead of plain text
99
- an `llm-rubric` receiving transformed spreadsheet text
1010
- relative `ContentFile.path` resolution against the target workspace
11+
- a deterministic grader target for local validation and a live LLM grader target for dogfood
1112

1213
## Running
1314

@@ -18,6 +19,15 @@ bun apps/cli/src/cli.ts eval examples/features/file-transforms/evals/suite.yaml
1819

1920
Expected result: the eval passes because the grader sees the transformed spreadsheet text from `generated/report.xlsx`.
2021

22+
To run the same file-output path with a live OpenAI-compatible grader:
23+
24+
```bash
25+
LOCAL_OPENAI_PROXY_BASE_URL=http://127.0.0.1:10531/v1 \
26+
LOCAL_OPENAI_PROXY_API_KEY=dummy-local-key \
27+
LOCAL_OPENAI_PROXY_MODEL=gpt-5.4-mini \
28+
bun apps/cli/src/cli.ts eval examples/features/file-transforms/evals/suite.yaml --target file_output_live
29+
```
30+
2131
## Key Files
2232

2333
- `evals/suite.yaml` - eval with `default_test.options.transform`

examples/features/file-transforms/evals/suite.yaml

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,4 @@ tests:
1010
input: Generate the spreadsheet report
1111
default_test:
1212
options:
13-
transform: >-
14-
return (() => {
15-
const content = Array.isArray(output) ? output : [];
16-
const matchers = ["xlsx","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xlsx"];
17-
const file = content.find((block) => {
18-
if (!block || block.type !== "file") return false;
19-
const mediaType = typeof block.media_type === "string" ? block.media_type : "";
20-
const filePath = typeof block.path === "string" ? block.path : "";
21-
return matchers.some((matcher) => mediaType === matcher || filePath.endsWith(matcher));
22-
});
23-
if (!file || typeof file.path !== "string") return output;
24-
const result = Bun.spawnSync(["bun","run","../scripts/transforms/xlsx-to-csv.ts"], {
25-
stdin: JSON.stringify({ path: file.path, media_type: file.media_type })
26-
});
27-
if (result.exitCode !== 0) {
28-
throw new Error(new TextDecoder().decode(result.stderr).trim() || "transform command failed");
29-
}
30-
return new TextDecoder().decode(result.stdout).trim();
31-
})()
13+
transform: file://../scripts/transforms/xlsx-to-csv.ts
Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,69 @@
1-
import { readFileSync } from 'node:fs';
1+
import { existsSync } from 'node:fs';
2+
import path from 'node:path';
23

3-
const payload = JSON.parse(readFileSync(0, 'utf8')) as { path?: string };
4+
type ContentFile = {
5+
type?: string;
6+
media_type?: string;
7+
path?: string;
8+
};
49

5-
if (!payload.path) {
6-
throw new Error('missing file path');
10+
const XLSX_MEDIA_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
11+
12+
function findSpreadsheet(output: unknown): ContentFile | undefined {
13+
if (!Array.isArray(output)) {
14+
return undefined;
15+
}
16+
17+
return output.find((block): block is ContentFile => {
18+
if (!block || typeof block !== 'object') {
19+
return false;
20+
}
21+
const file = block as ContentFile;
22+
return (
23+
file.type === 'file' &&
24+
typeof file.path === 'string' &&
25+
(file.path.endsWith('.xlsx') || file.media_type === XLSX_MEDIA_TYPE)
26+
);
27+
});
28+
}
29+
30+
function spreadsheetPath(file: ContentFile): string {
31+
if (!file.path) {
32+
throw new Error('missing spreadsheet path');
33+
}
34+
35+
if (path.isAbsolute(file.path)) {
36+
return file.path;
37+
}
38+
39+
return path.resolve(import.meta.dir, '..', '..', file.path);
740
}
841

9-
// Example-only placeholder transformation. Copy this script into your project
10-
// and replace it with real spreadsheet extraction logic.
11-
console.log('spreadsheet: revenue,total');
12-
console.log('Q1,42');
42+
export default function transform(output: unknown): string {
43+
const file = findSpreadsheet(output);
44+
if (!file) {
45+
throw new Error('expected a .xlsx ContentFile output');
46+
}
47+
48+
const absolutePath = spreadsheetPath(file);
49+
if (!existsSync(absolutePath)) {
50+
throw new Error(`spreadsheet not found: ${file.path}`);
51+
}
52+
53+
// Example-only placeholder conversion. Replace this with a real XLSX parser
54+
// such as SheetJS or your project's existing spreadsheet extractor.
55+
return ['spreadsheet: revenue,total', 'Q1,42'].join('\n');
56+
}
57+
58+
if (import.meta.main) {
59+
const filePath = process.argv[2];
60+
if (!filePath) {
61+
throw new Error('missing file path');
62+
}
63+
const payload: ContentFile = {
64+
type: 'file',
65+
media_type: XLSX_MEDIA_TYPE,
66+
path: filePath,
67+
};
68+
process.stdout.write(`${transform([payload])}\n`);
69+
}

skills-data/agentv-eval-migrations/references/breaking-changes.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ For a v4.42.4-era eval:
4848
13. Replace workspace hook `script:` with `command:`.
4949
14. For executable setup, prefer top-level `extensions`; keep
5050
`workspace.hooks.after_each.reset` for reset policy.
51-
15. Keep raw cases under `tests` or `imports.tests`; import full eval suites
51+
15. Replace authored `preprocessors` and deprecated Promptfoo `postprocess`
52+
with `transform` at `default_test.options`, `tests[].options`, or the
53+
assertion that needs the shaped output.
54+
16. Keep raw cases under `tests` or `imports.tests`; import full eval suites
5255
with `imports.suites`.
53-
16. Validate with `bun apps/cli/src/cli.ts validate <eval-file>`.
56+
17. Validate with `bun apps/cli/src/cli.ts validate <eval-file>`.
5457

5558
## Assertions Renamed To `assert`
5659

0 commit comments

Comments
 (0)