Skip to content

Commit 02923c2

Browse files
christsoCopilot
andauthored
feat(cli): add initial promptfoo import command (#1253)
* feat(cli): add initial promptfoo import command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): expand promptfoo import coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): preserve explicit promptfoo test ids Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5278f48 commit 02923c2

4 files changed

Lines changed: 1579 additions & 2 deletions

File tree

apps/cli/src/commands/import/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { importClaudeCommand } from './claude.js';
44
import { importCodexCommand } from './codex.js';
55
import { importCopilotCommand } from './copilot.js';
66
import { importHuggingFaceCommand } from './huggingface.js';
7+
import { importPromptfooCommand } from './promptfoo.js';
78

89
export const importCommand = subcommands({
910
name: 'import',
@@ -13,5 +14,6 @@ export const importCommand = subcommands({
1314
codex: importCodexCommand,
1415
copilot: importCopilotCommand,
1516
huggingface: importHuggingFaceCommand,
17+
promptfoo: importPromptfooCommand,
1618
},
1719
});
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { afterEach, describe, expect, it } from 'bun:test';
2+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import path from 'node:path';
5+
6+
import { convertPromptfooToAgentvSuite, convertPromptfooToAgentvYaml } from './promptfoo.js';
7+
8+
const tempDirs: string[] = [];
9+
10+
afterEach(async () => {
11+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
12+
});
13+
14+
describe('promptfoo import', () => {
15+
it('converts inline promptfoo configs into AgentV suite defaults and tests', async () => {
16+
const dir = await mkdtemp(path.join(tmpdir(), 'agentv-promptfoo-'));
17+
tempDirs.push(dir);
18+
19+
const configPath = path.join(dir, 'promptfooconfig.yaml');
20+
await writeFile(
21+
configPath,
22+
`
23+
description: Sample promptfoo suite
24+
prompts:
25+
- "Answer clearly: {{question}}"
26+
providers:
27+
- openai:gpt-5-mini
28+
defaultTest:
29+
assert:
30+
- type: contains
31+
value: Answer
32+
tests:
33+
- id: capital
34+
description: Capital answer stays deterministic
35+
vars:
36+
question: What is the capital of France?
37+
assert:
38+
- type: equals
39+
value: Paris
40+
`,
41+
'utf8',
42+
);
43+
44+
const suite = await convertPromptfooToAgentvSuite({ inputPath: configPath });
45+
46+
expect(suite.name).toBe('promptfooconfig');
47+
expect(suite.description).toBe('Sample promptfoo suite');
48+
expect(suite.execution).toEqual({ targets: ['openai-gpt-5-mini'] });
49+
expect(suite.assertions).toEqual([{ type: 'contains', value: 'Answer' }]);
50+
expect(suite.tests).toHaveLength(1);
51+
expect(suite.tests[0]).toMatchObject({
52+
id: 'capital',
53+
criteria: 'Capital answer stays deterministic',
54+
input: 'Answer clearly: What is the capital of France?',
55+
assertions: [{ type: 'equals', value: 'Paris' }],
56+
metadata: {
57+
promptfoo: {
58+
vars: { question: 'What is the capital of France?' },
59+
prompt_label: 'prompt-1',
60+
},
61+
},
62+
});
63+
});
64+
65+
it('loads prompt files and external JSONL tests', async () => {
66+
const dir = await mkdtemp(path.join(tmpdir(), 'agentv-promptfoo-'));
67+
tempDirs.push(dir);
68+
69+
const promptPath = path.join(dir, 'prompt.txt');
70+
const testsPath = path.join(dir, 'tests.jsonl');
71+
const configPath = path.join(dir, 'promptfooconfig.yaml');
72+
73+
await writeFile(promptPath, 'Please answer: {{question}}', 'utf8');
74+
await writeFile(
75+
testsPath,
76+
[
77+
JSON.stringify({
78+
id: 'math',
79+
vars: { question: 'What is 2 + 2?' },
80+
assert: [{ type: 'equals', value: '4' }],
81+
}),
82+
].join('\n'),
83+
'utf8',
84+
);
85+
await writeFile(
86+
configPath,
87+
`
88+
prompts:
89+
- file://./prompt.txt
90+
tests: file://./tests.jsonl
91+
`,
92+
'utf8',
93+
);
94+
95+
const yaml = await convertPromptfooToAgentvYaml(configPath);
96+
expect(yaml).toContain('# Converted from promptfoo config:');
97+
expect(yaml).toContain('id: math');
98+
expect(yaml).toContain('input: "Please answer: What is 2 + 2?"');
99+
expect(yaml).toContain('type: equals');
100+
});
101+
102+
it('imports promptfoo CSV datasets with __expected columns', async () => {
103+
const dir = await mkdtemp(path.join(tmpdir(), 'agentv-promptfoo-'));
104+
tempDirs.push(dir);
105+
106+
const testsPath = path.join(dir, 'tests.csv');
107+
const configPath = path.join(dir, 'promptfooconfig.yaml');
108+
109+
await writeFile(
110+
testsPath,
111+
[
112+
'__description,question,__expected,__expected2,__threshold,__metadata:category',
113+
'"Capital question","What is the capital of France?","equals: Paris","contains: Paris",0.8,geography',
114+
].join('\n'),
115+
'utf8',
116+
);
117+
await writeFile(
118+
configPath,
119+
`
120+
prompts:
121+
- "Question: {{question}}"
122+
tests: file://./tests.csv
123+
`,
124+
'utf8',
125+
);
126+
127+
const suite = await convertPromptfooToAgentvSuite({ inputPath: configPath });
128+
expect(suite.tests).toHaveLength(1);
129+
expect(suite.tests[0]).toMatchObject({
130+
id: 'capital-question',
131+
criteria: 'Capital question',
132+
input: 'Question: What is the capital of France?',
133+
assertions: [
134+
{ type: 'equals', value: 'Paris' },
135+
{ type: 'contains', value: 'Paris' },
136+
],
137+
execution: {
138+
threshold: 0.8,
139+
},
140+
metadata: {
141+
category: 'geography',
142+
promptfoo: {
143+
vars: {
144+
question: 'What is the capital of France?',
145+
},
146+
},
147+
},
148+
});
149+
});
150+
151+
it('fails clearly on unsupported promptfoo javascript assertions', async () => {
152+
const dir = await mkdtemp(path.join(tmpdir(), 'agentv-promptfoo-'));
153+
tempDirs.push(dir);
154+
155+
const configPath = path.join(dir, 'promptfooconfig.yaml');
156+
await writeFile(
157+
configPath,
158+
`
159+
prompts:
160+
- "Hello {{name}}"
161+
tests:
162+
- vars:
163+
name: Chris
164+
assert:
165+
- type: javascript
166+
value: output.includes("Chris")
167+
`,
168+
'utf8',
169+
);
170+
171+
await expect(convertPromptfooToAgentvSuite({ inputPath: configPath })).rejects.toThrow(
172+
"Unsupported promptfoo assertion 'javascript'",
173+
);
174+
});
175+
176+
it('preserves explicit promptfoo test ids verbatim', async () => {
177+
const dir = await mkdtemp(path.join(tmpdir(), 'agentv-promptfoo-'));
178+
tempDirs.push(dir);
179+
180+
const configPath = path.join(dir, 'promptfooconfig.yaml');
181+
await writeFile(
182+
configPath,
183+
`
184+
prompts:
185+
- "Hello {{name}}"
186+
tests:
187+
- id: "---special/id---"
188+
vars:
189+
name: Chris
190+
assert:
191+
- type: contains
192+
value: Chris
193+
`,
194+
'utf8',
195+
);
196+
197+
const suite = await convertPromptfooToAgentvSuite({ inputPath: configPath });
198+
expect(suite.tests[0]?.id).toBe('---special/id---');
199+
});
200+
});

0 commit comments

Comments
 (0)