Skip to content

Commit 5d0040b

Browse files
christsoclaude
andauthored
fix(core): suppress noisy warning for inline llm-grader prompts (#903)
Add explicit file:// prefix for prompt file references. Bare strings are always treated as inline text — no file resolution attempted. Closes #901 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fcf6c30 commit 5d0040b

2 files changed

Lines changed: 112 additions & 24 deletions

File tree

packages/core/src/evaluation/loaders/evaluator-parser.ts

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import { resolveFileReference } from './file-resolver.js';
99
const ANSI_YELLOW = '\u001b[33m';
1010
const ANSI_RESET = '\u001b[0m';
1111

12+
/**
13+
* Prefix for explicit file references in prompt strings.
14+
* Consistent with case-file-loader.ts which uses "file://" for test-case file references.
15+
*
16+
* Usage:
17+
* prompt: "file://prompts/grader.md" → explicit file, error if not found
18+
* prompt: "grader.md" → inline text (never resolved as file)
19+
* prompt: "Evaluate the response" → inline text
20+
*/
21+
const PROMPT_FILE_PREFIX = 'file://';
22+
1223
/**
1324
* Normalize evaluator type names from legacy snake_case to internal kebab-case.
1425
* Accepts both forms for backward compatibility:
@@ -428,14 +439,27 @@ async function parseEvaluatorList(
428439
threshold: thresholdValue,
429440
};
430441
} else {
431-
// llm-grader aggregator
432-
const aggregatorPrompt = asString(rawAggregator.prompt);
442+
// llm-grader aggregator — same file:// prefix logic as evaluator prompts
443+
const rawAggPrompt = asString(rawAggregator.prompt);
444+
let aggregatorPrompt: string | undefined;
433445
let promptPath: string | undefined;
434446

435-
if (aggregatorPrompt) {
436-
const resolved = await resolveFileReference(aggregatorPrompt, searchRoots);
437-
if (resolved.resolvedPath) {
438-
promptPath = path.resolve(resolved.resolvedPath);
447+
if (rawAggPrompt) {
448+
if (rawAggPrompt.startsWith(PROMPT_FILE_PREFIX)) {
449+
// Explicit file reference — error if not found
450+
const fileRef = rawAggPrompt.slice(PROMPT_FILE_PREFIX.length);
451+
aggregatorPrompt = fileRef;
452+
const resolved = await resolveFileReference(fileRef, searchRoots);
453+
if (resolved.resolvedPath) {
454+
promptPath = path.resolve(resolved.resolvedPath);
455+
} else {
456+
throw new Error(
457+
`Composite aggregator in '${evalId}': prompt file not found: ${resolved.displayPath}`,
458+
);
459+
}
460+
} else {
461+
// Bare string — always treat as inline text, no file resolution
462+
aggregatorPrompt = rawAggPrompt;
439463
}
440464
}
441465

@@ -1144,26 +1168,32 @@ async function parseEvaluatorList(
11441168
promptScriptConfig = rawPrompt.config as Record<string, unknown>;
11451169
}
11461170
} else if (typeof rawPrompt === 'string') {
1147-
// Text template prompt (existing behavior)
1148-
prompt = rawPrompt;
1149-
const resolved = await resolveFileReference(prompt, searchRoots);
1150-
if (resolved.resolvedPath) {
1151-
promptPath = path.resolve(resolved.resolvedPath);
1152-
// Validate custom prompt content upfront - throws error if validation fails
1153-
try {
1154-
await validateCustomPromptContent(promptPath);
1155-
} catch (error) {
1156-
const message = error instanceof Error ? error.message : String(error);
1157-
// Add context and re-throw for the caller to handle
1158-
throw new Error(`Evaluator '${name}' template (${promptPath}): ${message}`);
1171+
// Text template prompt — supports explicit file:// prefix for file references.
1172+
// "file://prompts/grader.md" → explicit file reference, error if not found
1173+
// "grader.md" → inline text (no file resolution)
1174+
// "Evaluate the response" → inline text
1175+
1176+
if (rawPrompt.startsWith(PROMPT_FILE_PREFIX)) {
1177+
// Explicit file reference — strip prefix and resolve. Error if not found.
1178+
const fileRef = rawPrompt.slice(PROMPT_FILE_PREFIX.length);
1179+
prompt = fileRef;
1180+
const resolved = await resolveFileReference(fileRef, searchRoots);
1181+
if (resolved.resolvedPath) {
1182+
promptPath = path.resolve(resolved.resolvedPath);
1183+
try {
1184+
await validateCustomPromptContent(promptPath);
1185+
} catch (error) {
1186+
const message = error instanceof Error ? error.message : String(error);
1187+
throw new Error(`Evaluator '${name}' template (${promptPath}): ${message}`);
1188+
}
1189+
} else {
1190+
throw new Error(
1191+
`Evaluator '${name}' in '${evalId}': prompt file not found: ${resolved.displayPath}`,
1192+
);
11591193
}
11601194
} else {
1161-
logWarning(
1162-
`Inline prompt used for evaluator '${name}' in '${evalId}' (file not found: ${resolved.displayPath})`,
1163-
resolved.attempted.length > 0
1164-
? resolved.attempted.map((attempt) => ` Tried: ${attempt}`)
1165-
: undefined,
1166-
);
1195+
// Bare string — always treat as inline text, no file resolution
1196+
prompt = rawPrompt;
11671197
}
11681198
}
11691199

packages/core/test/evaluation/loaders/evaluator-parser.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,3 +1780,61 @@ describe('parseEvaluators - string shorthand in assertions', () => {
17801780
expect(evaluators).toBeUndefined();
17811781
});
17821782
});
1783+
1784+
describe('parseEvaluators - file:// prefix prompt resolution', () => {
1785+
let tempDir: string;
1786+
1787+
beforeAll(async () => {
1788+
tempDir = path.join(os.tmpdir(), `agentv-test-file-prefix-${Date.now()}`);
1789+
await mkdir(tempDir, { recursive: true });
1790+
await writeFile(path.join(tempDir, 'grader.md'), 'Evaluate the quality of {{ output }}');
1791+
});
1792+
1793+
afterAll(async () => {
1794+
await rm(tempDir, { recursive: true, force: true });
1795+
});
1796+
1797+
it('file:// prefix resolves existing file', async () => {
1798+
const evaluators = await parseEvaluators(
1799+
{
1800+
assertions: [{ name: 'quality', type: 'llm-grader', prompt: 'file://grader.md' }],
1801+
},
1802+
undefined,
1803+
[tempDir],
1804+
'test-1',
1805+
);
1806+
expect(evaluators).toHaveLength(1);
1807+
const config = evaluators?.[0] as LlmGraderEvaluatorConfig;
1808+
expect(config.promptPath).toBeTruthy();
1809+
expect(config.promptPath).toContain('grader.md');
1810+
});
1811+
1812+
it('file:// prefix throws when file not found', async () => {
1813+
await expect(
1814+
parseEvaluators(
1815+
{
1816+
assertions: [{ name: 'missing', type: 'llm-grader', prompt: 'file://nonexistent.md' }],
1817+
},
1818+
undefined,
1819+
[tempDir],
1820+
'test-1',
1821+
),
1822+
).rejects.toThrow(/prompt file not found/);
1823+
});
1824+
1825+
it('bare path is always treated as inline text even if file exists', async () => {
1826+
const evaluators = await parseEvaluators(
1827+
{
1828+
assertions: [{ name: 'quality', type: 'llm-grader', prompt: 'grader.md' }],
1829+
},
1830+
undefined,
1831+
[tempDir],
1832+
'test-1',
1833+
);
1834+
expect(evaluators).toHaveLength(1);
1835+
const config = evaluators?.[0] as LlmGraderEvaluatorConfig;
1836+
// Bare string is inline text — no file resolution, no promptPath
1837+
expect(config.prompt).toBe('grader.md');
1838+
expect(config.promptPath).toBeUndefined();
1839+
});
1840+
});

0 commit comments

Comments
 (0)