Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion apps/web/src/content/docs/docs/next/evaluation/sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,28 @@ import { graders, type EvalConfig } from '@agentv/sdk';

const config: EvalConfig = {
name: 'hello-suite',
providers: ['mock-sdk'],
providers: [
{ id: 'mock', label: 'mock-sdk', config: { response: 'Hello from the mock provider' } },
{ id: 'openai:gpt-5-mini', label: 'grader-provider' },
],
defaults: {
provider: 'mock-sdk',
grader: 'grader-provider',
},
defaultTest: {
options: {
provider: 'grader-provider',
},
},
prompts: ['{{ task }}'],
tests: [
{
id: 'hello',
vars: { task: 'Say hello' },
inputFiles: ['../fixtures/per-test-note.md'],
options: {
provider: 'grader-provider',
},
assert: [graders.contains('Hello')],
},
],
Expand All @@ -120,6 +135,8 @@ Useful companion helpers:

The durable authored field remains `assert`. TypeScript eval config authoring does not introduce a second YAML vocabulary.

TypeScript eval configs use the same provider surface as YAML: top-level `providers` defines both systems under test and reusable grader providers, `providers[].id` names the backend/spec, `providers[].label` is the stable AgentV identity, and `defaults.provider` / `defaults.grader` select the default candidate and grader. Per-test grader provider selection belongs in `defaultTest.options.provider`, `tests[].options.provider`, or assertion-level `provider`.

## Built-In Grader Helpers

`@agentv/sdk` includes a small `graders` catalog for common deterministic and LLM-backed grader configs. These helpers return ordinary `assert` entries and serialize to the same canonical YAML you could write by hand.
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/evaluation/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export interface MaterializedEvalConfig {
readonly threshold?: number;
readonly metadata?: EvalMetadata;
readonly target?: ProviderDefinition;
readonly targets?: readonly ProviderDefinition[];
readonly task?: (input: string) => string | Promise<string>;
readonly providerFactory?: ProviderFactoryFn;
}
Expand Down Expand Up @@ -362,6 +363,7 @@ export async function evaluate(config: EvalConfig): Promise<EvalRunResult> {
testFilePath,
repoRoot,
target: resolvedTarget,
...(materialized.targets ? { targets: materialized.targets } : {}),
...(providerFactory ? { providerFactory } : {}),
maxRetries: config.maxRetries ?? 2,
agentTimeoutMs: config.agentTimeoutMs,
Expand Down Expand Up @@ -426,6 +428,9 @@ export async function materializeEvalConfig(
category: options?.category,
});
const tests = applyProgrammaticSuiteOverrides(suite.tests, config);
const suiteTargetDefinitions = suite.targetRefs
?.map((targetRef) => targetRef.definition)
.filter((definition): definition is ProviderDefinition => definition !== undefined);
return {
testFilePath,
tests,
Expand All @@ -435,7 +440,10 @@ export async function materializeEvalConfig(
budgetUsd: config.budgetUsd ?? suite.budgetUsd,
threshold: config.threshold ?? suite.threshold,
metadata: config.metadata ?? suite.metadata,
target: config.target ?? suite.inlineTarget,
target: config.target ?? suite.inlineTarget ?? suiteTargetDefinitions?.[0],
...(suiteTargetDefinitions && suiteTargetDefinitions.length > 0
? { targets: suiteTargetDefinitions }
: {}),
task: config.task,
providerFactory: suite.providerFactory,
};
Expand Down
35 changes: 30 additions & 5 deletions packages/core/src/evaluation/loaders/grader-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export async function parseGraders(
): Promise<readonly GraderConfig[] | undefined> {
const execution = rawEvalCase.execution;
const executionObject = isJsonObject(execution) ? execution : undefined;
const inheritedAssertionConfig = inheritedAssertionConfigFromOptions(rawEvalCase.options);

// Case-level graders priority: assert > execution assert.
const caseEvaluators =
Expand All @@ -255,7 +256,7 @@ export async function parseGraders(
evalId,
defaultPreprocessors,
defaultRubricPrompt,
undefined,
inheritedAssertionConfig,
inheritedGraderTarget,
);
// Parse root-level evaluators (appended after case-level)
Expand All @@ -265,7 +266,7 @@ export async function parseGraders(
evalId,
defaultPreprocessors,
defaultRubricPrompt,
undefined,
inheritedAssertionConfig,
inheritedGraderTarget,
);

Expand Down Expand Up @@ -1872,21 +1873,45 @@ function withInheritedAssertionConfig(
inheritedConfig?: JsonObject,
): JsonObject {
const ownConfig = isJsonObject(rawEvaluator.config) ? rawEvaluator.config : undefined;
if (!inheritedConfig && !ownConfig) {
const inheritedProvider =
typeof inheritedConfig?.provider === 'string' && inheritedConfig.provider.trim().length > 0
? inheritedConfig.provider.trim()
: undefined;
const inheritedConfigWithoutProvider = inheritedConfig
? Object.fromEntries(Object.entries(inheritedConfig).filter(([key]) => key !== 'provider'))
: undefined;
const inheritedConfigForConfig =
inheritedConfigWithoutProvider && Object.keys(inheritedConfigWithoutProvider).length > 0
? inheritedConfigWithoutProvider
: undefined;
if (!inheritedConfigForConfig && !ownConfig && inheritedProvider === undefined) {
return rawEvaluator;
}

const mergedConfig = {
...(inheritedConfig ?? {}),
...(inheritedConfigForConfig ?? {}),
...(ownConfig ?? {}),
};

return {
...rawEvaluator,
config: mergedConfig,
...(rawEvaluator.provider === undefined && inheritedProvider !== undefined
? { provider: inheritedProvider }
: {}),
...(Object.keys(mergedConfig).length > 0 ? { config: mergedConfig } : {}),
};
}

function inheritedAssertionConfigFromOptions(
options: JsonValue | undefined,
): JsonObject | undefined {
if (!isJsonObject(options)) {
return undefined;
}
const provider = typeof options.provider === 'string' ? options.provider.trim() : '';
return provider.length > 0 ? { provider } : undefined;
}

interface ParsedPromptField {
readonly prompt?: string;
readonly promptPath?: string;
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/evaluation/loaders/ts-eval-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const KNOWN_SNAKE_CASE_KEYS = {
budgetUsd: 'budget_usd',
conversationId: 'conversation_id',
costLimitUsd: 'cost_limit_usd',
defaultTest: 'default_test',
dependsOn: 'depends_on',
earlyExit: 'early_exit',
expectedOutput: 'expected_output',
Expand All @@ -45,6 +46,7 @@ const KNOWN_SNAKE_CASE_KEYS = {
outputPath: 'output_path',
readOnly: 'read_only',
reasoningEffort: 'reasoning_effort',
rubricPrompt: 'rubric_prompt',
skipDefaults: 'skip_defaults',
timeoutMs: 'timeout_ms',
timeoutSeconds: 'timeout_seconds',
Expand Down Expand Up @@ -193,10 +195,7 @@ function isProgrammaticEvalConfig(value: unknown): value is ProgrammaticEvalConf

function lowerTypeScriptEvalConfig(config: Record<string, unknown>): Record<string, unknown> {
const lowered = lowerEvalYamlValue(config) as Record<string, unknown>;
const { budget_usd: budgetUsd, repeat, target, ...withoutRuntimeAliases } = lowered;
if (target !== undefined && withoutRuntimeAliases.providers === undefined) {
withoutRuntimeAliases.providers = [target];
}
const { budget_usd: budgetUsd, repeat, ...withoutRuntimeAliases } = lowered;
if (budgetUsd === undefined && repeat === undefined) {
return withoutRuntimeAliases;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/evaluation/yaml-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2653,7 +2653,12 @@ function readSuiteRuntimeBlock(suite: RawTestSuite, evalFilePath: string): JsonO
}
if (suite.model !== undefined) {
throw new Error(
`Invalid eval runtime config in ${evalFilePath}: top-level 'model' is not part of eval YAML. Put model inside the target object.`,
`Invalid eval runtime config in ${evalFilePath}: top-level 'model' is not part of eval YAML. Put model inside the relevant providers[].config object.`,
);
}
if ((suite as Record<string, unknown>).graders !== undefined) {
throw new Error(
`Invalid eval runtime config in ${evalFilePath}: top-level 'graders' has been removed. Put grader providers in 'providers' and select them with defaults.grader, default_test.options.provider, tests[].options.provider, or assertion provider.`,
);
}
if (suite.runs !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import type { EvalConfig } from '../../../../src/evaluation/evaluate.js';

const config: EvalConfig = {
metadata: {
name: 'default-export-suite',
tags: ['sdk', 'typescript'],
},
const config = {
name: 'default-export-suite',
tags: ['sdk', 'typescript'],
prompts: ['{{ input }}'],
providers: [
{
id: 'mock',
label: 'inline-provider',
config: { response: 'hello there' },
},
],
tests: [
{
id: 'greeting',
vars: { input: 'Say hello' },
assert: [{ type: 'contains', value: 'hello' }],
},
],
cache: false,
cachePath: '.agentv/ts-eval-cache',
budgetUsd: 1.5,
threshold: 0.9,
target: { name: 'inline-target', provider: 'mock', response: 'hello there' },
};

export default config;
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { EvalConfig } from '../../../../src/evaluation/evaluate.js';

export const evalConfig: EvalConfig = {
export const evalConfig = {
prompts: ['{{ input }}'],
providers: ['mock-provider'],
tests: [
{
id: 'eval-config-named',
vars: { input: 'Say hello' },
assert: [{ type: 'contains', value: 'hello' }],
},
],
target: { provider: 'mock_agent' },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default {
name: 'legacy-graders',
providers: ['mock-provider'],
graders: [{ id: 'mock', label: 'grader-provider' }],
prompts: ['{{ input }}'],
tests: [
{
id: 'legacy-graders',
vars: { input: 'Say hello' },
assert: [{ type: 'contains', value: 'hello' }],
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
name: 'legacy-target',
target: 'mock-target',
prompts: ['{{ input }}'],
tests: [
{
id: 'legacy-target',
vars: { input: 'Say hello' },
assert: [{ type: 'contains', value: 'hello' }],
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
name: 'legacy-targets',
targets: ['mock-target'],
prompts: ['{{ input }}'],
tests: [
{
id: 'legacy-targets',
vars: { input: 'Say hello' },
assert: [{ type: 'contains', value: 'hello' }],
},
],
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const config = {
name: 'module-mts-config',
target: 'mock-target',
providers: ['mock-provider'],
prompts: ['{{ input }}'],
tests: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { EvalConfig } from '../../../../src/evaluation/evaluate.js';

export const config: EvalConfig = {
export const config = {
prompts: ['{{ input }}'],
providers: ['mock-provider'],
tests: [
{
id: 'named-config',
vars: { input: 'Say hello' },
assert: [{ type: 'contains', value: 'hello' }],
},
],
target: { provider: 'mock_agent' },
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { relativePrompt } from './relative-prompt.ts';

const config = {
name: 'relative-import-ts-config',
target: 'mock-target',
providers: ['mock-provider'],
tags: { experiment: 'ts-config', group: 'loader' },
prompts: [relativePrompt],
budgetUsd: 1,
Expand Down
Loading
Loading