0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
1129 lines
39 KiB
TypeScript
1129 lines
39 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { evaluate } from '../../src/evaluator';
|
|
import Eval from '../../src/models/eval';
|
|
import {
|
|
generateOptimizedPromptCandidates,
|
|
optimizePromptTestSuite,
|
|
} from '../../src/optimizer/promptOptimizer';
|
|
import { getDefaultProviders } from '../../src/providers/defaults';
|
|
import { createMockProvider } from '../factories/provider';
|
|
|
|
import type { CompletedPrompt, EvaluateResult, TestSuite } from '../../src/types/index';
|
|
|
|
vi.mock('../../src/evaluator', () => ({
|
|
evaluate: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../src/providers/defaults', () => ({
|
|
getDefaultProviders: vi.fn(),
|
|
}));
|
|
|
|
describe('prompt optimizer', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it('normalizes provider candidates and drops duplicates or unchanged prompts', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [
|
|
{ hypothesis: 'same prompt', prompt: 'Original prompt' },
|
|
{ hypothesis: 'first', prompt: 'Improved prompt' },
|
|
{ hypothesis: 'duplicate', prompt: 'Improved prompt' },
|
|
{ hypothesis: 'second', prompt: 'Another improved prompt' },
|
|
],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const candidates = await generateOptimizedPromptCandidates({
|
|
prompt: ' Original prompt ',
|
|
failures: [],
|
|
successes: [],
|
|
});
|
|
|
|
expect(candidates).toEqual([
|
|
{ hypothesis: 'first', prompt: 'Improved prompt' },
|
|
{ hypothesis: 'second', prompt: 'Another improved prompt' },
|
|
]);
|
|
});
|
|
|
|
it('sanitizes and bounds optimizer evidence before sending it to the suggestions provider', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Safer prompt'),
|
|
});
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const failure = {
|
|
...evalResult(0, false, 'grading reason '.repeat(140)),
|
|
vars: {
|
|
apiKey: 'sk-proj-this-should-never-leak-12345678901234567890',
|
|
largeContext: 'z'.repeat(1800),
|
|
},
|
|
response: {
|
|
output: 'optimizer output '.repeat(140),
|
|
},
|
|
namedScores: {
|
|
token: 'Bearer this-is-also-sensitive-12345678901234567890',
|
|
},
|
|
} as unknown as EvaluateResult;
|
|
|
|
await generateOptimizedPromptCandidates({
|
|
prompt: 'Original prompt',
|
|
failures: [failure],
|
|
successes: [],
|
|
});
|
|
|
|
const outerPayload = JSON.parse(String(vi.mocked(provider.callApi).mock.calls[0][0]));
|
|
const content = JSON.parse(outerPayload[1].content);
|
|
const evidence = content.failures[0];
|
|
|
|
expect(evidence.vars).toContain('[REDACTED]');
|
|
expect(evidence.vars).not.toContain('sk-proj-this-should-never-leak');
|
|
expect(evidence.namedScores).toContain('[REDACTED]');
|
|
expect(evidence.output).toMatch(/\[truncated]$/);
|
|
expect(evidence.gradingReason).toMatch(/\[truncated]$/);
|
|
expect(evidence.vars.length).toBeLessThanOrEqual(1214);
|
|
expect(evidence.output.length).toBeLessThanOrEqual(1214);
|
|
expect(evidence.gradingReason.length).toBeLessThanOrEqual(1214);
|
|
});
|
|
|
|
it('evaluates candidates from the selected prompt and provider', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Make the policy explicit.', prompt: 'Optimized B' }],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized B'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative B 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative B 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselinePrompts = [completedPrompt('Prompt B', 'B', 0.8)];
|
|
const baselineResults = [
|
|
evalResult(0, false, 'Missed the required JSON format.'),
|
|
evalResult(0, true, 'Matched the required JSON format.'),
|
|
];
|
|
const baselineEval = evalWith(baselinePrompts, baselineResults);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Prompt B', 'B', 0.8),
|
|
completedPrompt('Optimized B', 'B [optimized 1]', 0.9),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [
|
|
createMockProvider({ id: 'target-provider-a' }),
|
|
createMockProvider({ id: 'target-provider-b' }),
|
|
],
|
|
prompts: [
|
|
{ raw: 'Prompt A', label: 'A' },
|
|
{ raw: 'Prompt B', label: 'B' },
|
|
],
|
|
tests: [{}],
|
|
};
|
|
|
|
const result = await optimizePromptTestSuite({}, testSuite, {
|
|
promptIndex: 1,
|
|
providerIndex: 1,
|
|
});
|
|
|
|
expect(result.improved).toBe(true);
|
|
expect(result.baselinePrompt.label).toBe('B');
|
|
expect(result.bestPrompt.label).toBe('B [optimized 1]');
|
|
expect(vi.mocked(evaluate).mock.calls[0][0].prompts).toEqual([{ raw: 'Prompt B', label: 'B' }]);
|
|
expect(vi.mocked(evaluate).mock.calls[0][0].providers[0].id()).toBe('target-provider-b');
|
|
expect(provider.callApi).toHaveBeenCalledTimes(3);
|
|
expect(String(vi.mocked(provider.callApi).mock.calls[0][0])).toContain(
|
|
'Missed the required JSON format.',
|
|
);
|
|
});
|
|
|
|
it('keeps an earlier-round improvement when a later round fails to generate candidates', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized B'),
|
|
});
|
|
// Round 1 generation succeeds; round 2 generation fails (e.g. the prompt has
|
|
// converged and the suggestions provider returns an error / no new candidates).
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized B'))
|
|
.mockResolvedValueOnce({ error: 'simulated suggestions provider failure' });
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith(
|
|
[completedPrompt('Prompt B', 'B', 0.5)],
|
|
[evalResult(0, false, 'Missed the required JSON format.')],
|
|
);
|
|
const round1Eval = evalWith(
|
|
[
|
|
completedPrompt('Prompt B', 'B', 0.5),
|
|
completedPrompt('Optimized B', 'B [optimized 1]', 0.9),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate).mockResolvedValueOnce(baselineEval).mockResolvedValueOnce(round1Eval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Prompt B', label: 'B' }],
|
|
tests: [{}],
|
|
};
|
|
|
|
const result = await optimizePromptTestSuite({}, testSuite);
|
|
|
|
// The round-1 improvement must survive the round-2 failure instead of the
|
|
// whole command throwing and discarding it.
|
|
expect(result.improved).toBe(true);
|
|
expect(result.bestPrompt.label).toBe('B [optimized 1]');
|
|
expect(result.rounds).toHaveLength(1);
|
|
// Round 2 generation was attempted (and failed) before stopping.
|
|
expect(provider.callApi).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('surfaces a first-round candidate generation failure instead of reporting no improvement', async () => {
|
|
const provider = createMockProvider({ id: 'optimizer-provider' });
|
|
vi.mocked(provider.callApi).mockResolvedValueOnce({
|
|
error: 'simulated suggestions provider failure',
|
|
});
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Prompt B', 'B', 0.5)], []);
|
|
vi.mocked(evaluate).mockResolvedValueOnce(baselineEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Prompt B', label: 'B' }],
|
|
tests: [{}],
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
/Failed to generate optimized prompt candidates/,
|
|
);
|
|
});
|
|
|
|
it('defaults to the first prompt and provider', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Tighten the wording.', prompt: 'Optimized A' }],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized A'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative A 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative A 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Prompt A', 'A', 0.6)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Prompt A', 'A', 0.6),
|
|
completedPrompt('Optimized A', 'A [optimized 1]', 0.7),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [
|
|
createMockProvider({ id: 'target-provider-a' }),
|
|
createMockProvider({ id: 'target-provider-b' }),
|
|
],
|
|
prompts: [
|
|
{ raw: 'Prompt A', label: 'A' },
|
|
{ raw: 'Prompt B', label: 'B' },
|
|
],
|
|
tests: [{}],
|
|
};
|
|
|
|
await optimizePromptTestSuite({}, testSuite);
|
|
|
|
expect(vi.mocked(evaluate).mock.calls[0][0].prompts).toEqual([{ raw: 'Prompt A', label: 'A' }]);
|
|
expect(vi.mocked(evaluate).mock.calls[0][0].providers[0].id()).toBe('target-provider-a');
|
|
});
|
|
|
|
it('keeps the baseline when a search candidate only ties its score', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Equivalent quality.', prompt: 'Tied prompt' }],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Tied prompt'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative tie 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative tie 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Base', 'Base', 0.8)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Base', 'Base', 0.8),
|
|
completedPrompt('Tied prompt', 'Base [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Base', label: 'Base' }],
|
|
tests: [{}],
|
|
};
|
|
|
|
const result = await optimizePromptTestSuite({}, testSuite);
|
|
|
|
expect(result.bestPrompt.label).toBe('Base');
|
|
expect(result.improved).toBe(false);
|
|
});
|
|
|
|
it('rejects provider indices outside the resolved provider list', async () => {
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Prompt A', label: 'A' }],
|
|
tests: [{}],
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite, { providerIndex: 1 })).rejects.toThrow(
|
|
'Provider index 1 is out of range. Available provider indices: 0-0.',
|
|
);
|
|
});
|
|
|
|
it('preserves config evaluation runtime options for internal optimizer evals', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
tests: [{}],
|
|
};
|
|
|
|
await optimizePromptTestSuite(
|
|
{
|
|
evaluateOptions: {
|
|
cache: true,
|
|
delay: 50,
|
|
maxConcurrency: 1,
|
|
repeat: 2,
|
|
showProgressBar: true,
|
|
},
|
|
},
|
|
testSuite,
|
|
);
|
|
|
|
expect(vi.mocked(evaluate).mock.calls[0][2]).toEqual(
|
|
expect.objectContaining({
|
|
cache: false,
|
|
delay: 50,
|
|
eventSource: 'library',
|
|
maxConcurrency: 1,
|
|
repeat: 2,
|
|
showProgressBar: false,
|
|
silent: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('strips outputPath from internal optimizer evals so they do not pollute the user output file', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
tests: [{}, {}],
|
|
};
|
|
|
|
await optimizePromptTestSuite({ outputPath: 'results.jsonl' }, testSuite, {
|
|
validationSplit: 0.5,
|
|
});
|
|
|
|
const evalCalls = vi.mocked(evaluate).mock.calls;
|
|
expect(evalCalls).toHaveLength(8);
|
|
for (const call of evalCalls) {
|
|
expect(call[1]).toBeInstanceOf(Eval);
|
|
expect(call[1].config.outputPath).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('keeps optimized candidates eligible when provider prompt routing uses prompt ids', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Clarify the instruction.', prompt: 'Optimized Seed' }],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Alternative Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.7),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ id: 'seed-id', raw: 'Seed', label: 'Seed' }],
|
|
providerPromptMap: {
|
|
'target-provider': ['seed-id'],
|
|
},
|
|
tests: [{}],
|
|
};
|
|
|
|
await optimizePromptTestSuite({}, testSuite);
|
|
|
|
const candidateSuite = vi.mocked(evaluate).mock.calls[1][0];
|
|
expect(candidateSuite.providerPromptMap).toEqual({
|
|
'target-provider': ['seed-id', 'Seed', 'Seed [optimized 1]'],
|
|
});
|
|
expect(candidateSuite.prompts[0].id).toBe('seed-id');
|
|
expect(candidateSuite.prompts[1].id).toEqual(expect.any(String));
|
|
expect(candidateSuite.prompts[1].id).not.toBe('seed-id');
|
|
});
|
|
|
|
it('keeps exact-label provider prompt routing intact after adopting a candidate', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
providerPromptMap: {
|
|
'target-provider': ['Seed'],
|
|
},
|
|
tests: [{}],
|
|
};
|
|
|
|
await optimizePromptTestSuite({}, testSuite);
|
|
|
|
expect(vi.mocked(evaluate).mock.calls[2][0].providerPromptMap).toEqual({
|
|
'target-provider': ['Seed', 'Seed [optimized 1]', 'Seed [optimized 1] [optimized 1]'],
|
|
});
|
|
});
|
|
|
|
it('extends prompt filters for explicit, default, and scenario tests when candidates are added', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
defaultTest: { prompts: ['Seed'] },
|
|
tests: [{ prompts: ['Seed'] }],
|
|
scenarios: [
|
|
{
|
|
config: [{ prompts: ['Seed'] }],
|
|
tests: [{ prompts: ['Seed'] }],
|
|
},
|
|
],
|
|
};
|
|
|
|
await optimizePromptTestSuite({}, testSuite);
|
|
|
|
const candidateSuite = vi.mocked(evaluate).mock.calls[1][0];
|
|
expect(candidateSuite.tests?.[0].prompts).toEqual(['Seed', 'Seed [optimized 1]']);
|
|
expect(
|
|
candidateSuite.defaultTest && typeof candidateSuite.defaultTest === 'object'
|
|
? candidateSuite.defaultTest.prompts
|
|
: undefined,
|
|
).toEqual(['Seed', 'Seed [optimized 1]']);
|
|
expect(candidateSuite.scenarios?.[0].config[0].prompts).toEqual(['Seed', 'Seed [optimized 1]']);
|
|
expect(candidateSuite.scenarios?.[0].tests[0].prompts).toEqual(['Seed', 'Seed [optimized 1]']);
|
|
});
|
|
|
|
it('rejects validation splits for scenario-based optimization instead of pretending they are held out', async () => {
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
scenarios: [
|
|
{
|
|
config: [{}],
|
|
tests: [{}],
|
|
},
|
|
],
|
|
};
|
|
|
|
await expect(
|
|
optimizePromptTestSuite({}, testSuite, {
|
|
validationSplit: 0.2,
|
|
}),
|
|
).rejects.toThrow(
|
|
'validationSplit is not supported for scenario-based prompt optimization; expand scenarios into explicit tests first.',
|
|
);
|
|
});
|
|
|
|
it('clones mutable test suites before each evaluate call so default assertions do not stack', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
const seenAssertCounts: number[] = [];
|
|
let evaluateCallCount = 0;
|
|
vi.mocked(evaluate).mockImplementation(async (suite) => {
|
|
seenAssertCounts.push(suite.tests?.[0].assert?.length || 0);
|
|
if (suite.tests?.[0] && suite.defaultTest && typeof suite.defaultTest === 'object') {
|
|
suite.tests[0].assert = [
|
|
...(suite.defaultTest.assert || []),
|
|
...(suite.tests[0].assert || []),
|
|
];
|
|
}
|
|
evaluateCallCount += 1;
|
|
return evaluateCallCount === 1 ? baselineEval : candidateEval;
|
|
});
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
defaultTest: {
|
|
assert: [{ type: 'contains', value: 'default' }],
|
|
},
|
|
tests: [
|
|
{
|
|
assert: [{ type: 'contains', value: 'local' }],
|
|
},
|
|
],
|
|
};
|
|
|
|
await optimizePromptTestSuite({}, testSuite);
|
|
|
|
expect(seenAssertCounts).toEqual([1, 1, 1, 1]);
|
|
});
|
|
|
|
it('rejects prompt optimization when no tests or scenarios are configured', async () => {
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
'Prompt optimization requires at least one configured test or scenario.',
|
|
);
|
|
});
|
|
|
|
it('still rejects prompt optimization when defaultTest is empty', async () => {
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
defaultTest: {},
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
'Prompt optimization requires at least one configured test or scenario.',
|
|
);
|
|
});
|
|
|
|
it('rejects a defaultTest-only config when an empty scenarios array is present', async () => {
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
// An empty `scenarios` array still suppresses `eval`'s implicit-test
|
|
// synthesis, so this config runs zero tests and must be rejected.
|
|
scenarios: [],
|
|
defaultTest: {
|
|
assert: [{ type: 'contains', value: 'default' }],
|
|
},
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
'Prompt optimization requires at least one configured test or scenario.',
|
|
);
|
|
});
|
|
|
|
it('accepts a defaultTest-only config with assertions and a scoring function', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Optimized Seed 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineEval = evalWith([completedPrompt('Seed', 'Seed', 0.5)], []);
|
|
const candidateEval = evalWith(
|
|
[
|
|
completedPrompt('Seed', 'Seed', 0.5),
|
|
completedPrompt('Optimized Seed', 'Seed [optimized 1]', 0.8),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval)
|
|
.mockResolvedValueOnce(candidateEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
// No `tests` or `scenarios`: `eval` synthesizes one implicit test and applies
|
|
// `defaultTest`, so the optimizer preflight must accept this config too.
|
|
defaultTest: {
|
|
assert: [{ type: 'contains', value: 'default' }],
|
|
assertScoringFunction: () => ({ pass: true, score: 1, reason: 'scored' }),
|
|
},
|
|
};
|
|
|
|
const result = await optimizePromptTestSuite({}, testSuite);
|
|
|
|
expect(result.improved).toBe(true);
|
|
expect(result.bestPrompt.label).toBe('Seed [optimized 1]');
|
|
expect(evaluate).toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
['alone', {}],
|
|
['with variables', { vars: { name: 'Alice' } }],
|
|
])('rejects a defaultTest-only scoring function %s when it has no assertions', async (_, rest) => {
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
defaultTest: {
|
|
...rest,
|
|
assertScoringFunction: () => ({ pass: true, score: 1, reason: 'scored' }),
|
|
},
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
'Prompt optimization requires at least one configured test or scenario.',
|
|
);
|
|
expect(evaluate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('throws a clear error when prompt/provider filters scope every test away from the selected pair', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized Seed'),
|
|
});
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
// The configured test exists (so the preflight test count is non-zero) but
|
|
// its prompt filter scopes it away from the selected prompt, so the baseline
|
|
// evaluation schedules zero result rows.
|
|
vi.mocked(evaluate).mockResolvedValueOnce(emptyEval([completedPrompt('Seed', 'Seed', 0)]));
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed' }],
|
|
tests: [{ prompts: ['some-other-prompt'] }],
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
'No eval test cases ran for the selected prompt/provider — check filters and other test scoping options.',
|
|
);
|
|
// The optimizer must fail fast instead of running candidate rounds against
|
|
// empty evidence.
|
|
expect(vi.mocked(evaluate)).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('errors when the validation split produces no runnable tests', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: optimizerResponse('Optimized'),
|
|
});
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
// The baseline search eval has rows, but the held-out validation split is
|
|
// scoped away by filters and schedules zero result rows.
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(evalWith([completedPrompt('Base', 'Base', 0.6)], []))
|
|
.mockResolvedValueOnce(emptyEval([completedPrompt('Base', 'Base', 0)]));
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Base', label: 'Base' }],
|
|
tests: [{}, {}, {}, {}, {}],
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite, { validationSplit: 0.4 })).rejects.toThrow(
|
|
'No eval test cases ran for the validation split',
|
|
);
|
|
// Fails fast after the baseline search + validation evals, before rounds.
|
|
expect(vi.mocked(evaluate)).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('rejects function-backed prompts before evaluating unchanged function output', async () => {
|
|
vi.mocked(evaluate).mockRejectedValue(new Error('baseline evaluation should not run'));
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Seed', label: 'Seed', function: async () => 'Seed' }],
|
|
tests: [{}],
|
|
};
|
|
|
|
await expect(optimizePromptTestSuite({}, testSuite)).rejects.toThrow(
|
|
'Prompt optimization currently supports literal string prompts only.',
|
|
);
|
|
expect(evaluate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses validation score to reject search-only overfitting when split is enabled', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Overfit the search set.', prompt: 'Overfit prompt' }],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineSearchEval = evalWith([completedPrompt('Base', 'Base', 0.6)], []);
|
|
const baselineValidationEval = evalWith([completedPrompt('Base', 'Base', 0.7)], []);
|
|
const candidateSearchEval = evalWith(
|
|
[
|
|
completedPrompt('Base', 'Base', 0.6),
|
|
completedPrompt('Overfit prompt', 'Base [optimized 1]', 0.95),
|
|
],
|
|
[],
|
|
);
|
|
const candidateValidationEval = evalWith(
|
|
[
|
|
completedPrompt('Base', 'Base', 0.7),
|
|
completedPrompt('Overfit prompt', 'Base [optimized 1]', 0.4),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineSearchEval)
|
|
.mockResolvedValueOnce(baselineValidationEval)
|
|
.mockResolvedValueOnce(candidateSearchEval)
|
|
.mockResolvedValueOnce(candidateValidationEval)
|
|
.mockResolvedValueOnce(candidateSearchEval)
|
|
.mockResolvedValueOnce(candidateValidationEval)
|
|
.mockResolvedValueOnce(candidateSearchEval)
|
|
.mockResolvedValueOnce(candidateValidationEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Base', label: 'Base' }],
|
|
tests: [{}, {}, {}, {}, {}],
|
|
};
|
|
|
|
const result = await optimizePromptTestSuite({}, testSuite, { validationSplit: 0.4 });
|
|
|
|
expect(result.validationTestCount).toBe(2);
|
|
expect(result.searchTestCount).toBe(3);
|
|
expect(result.bestPrompt.label).toBe('Base');
|
|
expect(result.bestValidationPrompt?.label).toBe('Base');
|
|
expect(result.improved).toBe(false);
|
|
});
|
|
|
|
it('keeps the baseline when validation and search scores both tie', async () => {
|
|
const provider = createMockProvider({
|
|
id: 'optimizer-provider',
|
|
response: {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Equivalent quality.', prompt: 'Validation tie' }],
|
|
}),
|
|
},
|
|
});
|
|
vi.mocked(provider.callApi)
|
|
.mockResolvedValueOnce(optimizerResponse('Validation tie'))
|
|
.mockResolvedValueOnce(optimizerResponse('Validation tie 2'))
|
|
.mockResolvedValueOnce(optimizerResponse('Validation tie 3'));
|
|
vi.mocked(getDefaultProviders).mockResolvedValue({
|
|
embeddingProvider: provider,
|
|
gradingJsonProvider: provider,
|
|
gradingProvider: provider,
|
|
moderationProvider: provider,
|
|
suggestionsProvider: provider,
|
|
synthesizeProvider: provider,
|
|
} as any);
|
|
|
|
const baselineSearchEval = evalWith([completedPrompt('Base', 'Base', 0.6)], []);
|
|
const baselineValidationEval = evalWith([completedPrompt('Base', 'Base', 0.7)], []);
|
|
const candidateSearchEval = evalWith(
|
|
[
|
|
completedPrompt('Base', 'Base', 0.6),
|
|
completedPrompt('Validation tie', 'Base [optimized 1]', 0.6),
|
|
],
|
|
[],
|
|
);
|
|
const candidateValidationEval = evalWith(
|
|
[
|
|
completedPrompt('Base', 'Base', 0.7),
|
|
completedPrompt('Validation tie', 'Base [optimized 1]', 0.7),
|
|
],
|
|
[],
|
|
);
|
|
|
|
vi.mocked(evaluate)
|
|
.mockResolvedValueOnce(baselineSearchEval)
|
|
.mockResolvedValueOnce(baselineValidationEval)
|
|
.mockResolvedValueOnce(candidateSearchEval)
|
|
.mockResolvedValueOnce(candidateValidationEval)
|
|
.mockResolvedValueOnce(candidateSearchEval)
|
|
.mockResolvedValueOnce(candidateValidationEval)
|
|
.mockResolvedValueOnce(candidateSearchEval)
|
|
.mockResolvedValueOnce(candidateValidationEval);
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [createMockProvider({ id: 'target-provider' })],
|
|
prompts: [{ raw: 'Base', label: 'Base' }],
|
|
tests: [{}, {}, {}, {}, {}],
|
|
};
|
|
|
|
const result = await optimizePromptTestSuite({}, testSuite, { validationSplit: 0.4 });
|
|
|
|
expect(result.bestPrompt.label).toBe('Base');
|
|
expect(result.bestValidationPrompt?.label).toBe('Base');
|
|
expect(result.improved).toBe(false);
|
|
});
|
|
});
|
|
|
|
function completedPrompt(raw: string, label: string, score: number): CompletedPrompt {
|
|
return {
|
|
raw,
|
|
label,
|
|
provider: 'provider',
|
|
metrics: {
|
|
score,
|
|
testPassCount: 0,
|
|
testFailCount: 0,
|
|
testErrorCount: 0,
|
|
assertPassCount: 0,
|
|
assertFailCount: 0,
|
|
totalLatencyMs: 0,
|
|
tokenUsage: {},
|
|
namedScores: {},
|
|
namedScoresCount: {},
|
|
cost: 0,
|
|
},
|
|
} as CompletedPrompt;
|
|
}
|
|
|
|
function evalResult(promptIdx: number, success: boolean, reason: string): EvaluateResult {
|
|
return {
|
|
promptIdx,
|
|
testIdx: 0,
|
|
testCase: {},
|
|
promptId: 'prompt',
|
|
provider: { id: 'provider' },
|
|
prompt: { raw: 'Prompt B', label: 'B' },
|
|
vars: {},
|
|
failureReason: 0,
|
|
success,
|
|
score: success ? 1 : 0,
|
|
latencyMs: 0,
|
|
gradingResult: {
|
|
pass: success,
|
|
score: success ? 1 : 0,
|
|
reason,
|
|
},
|
|
namedScores: {},
|
|
} as EvaluateResult;
|
|
}
|
|
|
|
function evalWith(prompts: CompletedPrompt[], results: EvaluateResult[]): Eval {
|
|
const evalRecord = new Eval({}, { persisted: false, prompts });
|
|
evalRecord.prompts = prompts;
|
|
// These optimizer tests often omit result rows when they only care about
|
|
// prompt scores. Backfill placeholders so the zero-runnable-tests guard only
|
|
// fires for cases that intentionally use `emptyEval`.
|
|
const backfilledResults =
|
|
results.length === 0
|
|
? prompts.map((_prompt, promptIdx) => evalResult(promptIdx, true, 'placeholder result'))
|
|
: results;
|
|
evalRecord.results = backfilledResults as any;
|
|
return evalRecord;
|
|
}
|
|
|
|
/**
|
|
* Builds an Eval whose results are genuinely empty, mirroring an evaluator run
|
|
* where active test scoping left no runnable result rows.
|
|
*/
|
|
function emptyEval(prompts: CompletedPrompt[]): Eval {
|
|
const evalRecord = new Eval({}, { persisted: false, prompts });
|
|
evalRecord.prompts = prompts;
|
|
evalRecord.results = [];
|
|
return evalRecord;
|
|
}
|
|
|
|
function optimizerResponse(prompt: string) {
|
|
return {
|
|
output: JSON.stringify({
|
|
candidates: [{ hypothesis: 'Try a distinct candidate.', prompt }],
|
|
}),
|
|
};
|
|
}
|