evals/run-source-cards.ts
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bun
/**
* Measure how often a deployed search mode configuration presents found items
* as SourceCards, and how often it uses them where it should not.
*
* bun run eval:source-cards
* bun run eval:source-cards -- --mode quick --trials 3
*
* See evals/README.md for what this measures and why.
*/
import { config as dotenvConfig } from 'dotenv'
dotenvConfig({ path: '.env.local' })
// The search mode prompts branch on whether a general search provider is
// configured, so an unset key would silently measure a different prompt than
// the deployed one. Pinned before anything reads it. The fixture search tool
// means no request ever reaches Brave.
process.env.BRAVE_SEARCH_API_KEY ||= 'eval-pinned-general-provider'
type Mode = 'quick' | 'adaptive'
type Args = {
model?: string
mode: Mode
trials: number
concurrency: number
runName?: string
gate: boolean
ids?: string[]
}
function parseArgs(argv: string[]): Args {
const args: Args = {
trials: 1,
concurrency: 5,
gate: true,
mode: 'adaptive'
}
for (let i = 0; i < argv.length; i++) {
const flag = argv[i]
const next = () => {
const value = argv[++i]
if (value === undefined) throw new Error(`${flag} requires a value`)
return value
}
switch (flag) {
case '--model':
args.model = next()
break
case '--mode': {
const mode = next()
if (mode !== 'quick' && mode !== 'adaptive') {
throw new Error('--mode must be quick or adaptive')
}
args.mode = mode
break
}
case '--trials':
args.trials = Number(next())
break
case '--concurrency':
args.concurrency = Number(next())
break
case '--run-name':
args.runName = next()
break
case '--items':
args.ids = next()
.split(',')
.map(id => id.trim())
.filter(Boolean)
break
case '--no-gate':
args.gate = false
break
case '--help':
console.log(
[
'Usage: bun run eval:source-cards [options]',
'',
' --mode <quick|adaptive> search mode to evaluate (default: adaptive)',
' --model <id> provider:model to evaluate (default: the selected mode model)',
' --trials <n> generations per dataset item (default: 1)',
' --concurrency <n> parallel generations (default: 5)',
' --run-name <name> exact Langfuse run name (default: name + timestamp)',
' --items <a,b> run only these dataset item ids',
' --no-gate report metrics without failing on threshold violations'
].join('\n')
)
process.exit(0)
break
default:
throw new Error(`Unknown argument: ${flag}`)
}
}
if (!Number.isFinite(args.trials) || args.trials < 1) {
throw new Error('--trials must be a positive number')
}
if (!Number.isFinite(args.concurrency) || args.concurrency < 1) {
throw new Error('--concurrency must be a positive number')
}
return args
}
async function main() {
const args = parseArgs(process.argv.slice(2))
// Imported after dotenv: the provider registry reads API keys at module load.
const { LangfuseClient } = await import('@langfuse/client')
const { loadSourceCardsDataset, sourceCardsDatasetDescription } =
await import('./lib/source-cards')
const { applyGate } = await import('./lib/gate')
type Threshold = import('./lib/gate').Threshold
const { setupTracing } = await import('./lib/otel')
const {
sourceCardsEvaluator,
sourceCardsRunEvaluator,
summarizeSourceCards
} = await import('./evaluators/source-cards')
const adaptiveTask = await import('./tasks/adaptive-answer')
const quickTask = await import('./tasks/quick-answer')
const thresholds = (await import('./thresholds.json')).default
const taskConfig =
args.mode === 'quick'
? {
createTask: quickTask.createQuickAnswerTask,
defaultModel: quickTask.DEFAULT_MODEL
}
: {
createTask: adaptiveTask.createAdaptiveAnswerTask,
defaultModel: adaptiveTask.DEFAULT_MODEL
}
const model = args.model ?? taskConfig.defaultModel
const data = loadSourceCardsDataset({ trials: args.trials, ids: args.ids })
const tracing = setupTracing()
console.log(`mode: ${args.mode}`)
console.log(`model: ${model}`)
console.log(`items: ${data.length} (${args.trials} trial(s) per query)`)
console.log(`concurrency: ${args.concurrency}\n`)
const langfuse = new LangfuseClient()
const result = await langfuse.experiment.run({
name: 'source-cards',
runName: args.runName,
description: sourceCardsDatasetDescription,
metadata: { mode: args.mode, model, trials: args.trials },
data,
task: taskConfig.createTask({ model, tracingEnabled: true }),
evaluators: [sourceCardsEvaluator],
runEvaluators: [sourceCardsRunEvaluator],
maxConcurrency: args.concurrency
})
console.log(await result.format({ includeItemResults: true }))
const completionRate = result.itemResults.length / data.length
const summary = summarizeSourceCards(result.itemResults)
console.log('\nsummary:', JSON.stringify(summary, null, 2))
const config = thresholds['source-cards'] as Record<string, Threshold>
try {
if (args.gate) {
applyGate(result, [
{
metric: 'completionRate',
value: completionRate,
samples: data.length,
threshold: config.completionRate
},
{
metric: 'emissionRate',
value: summary.emissionRate,
samples: summary.listingCount,
threshold: config.emissionRate
},
{
metric: 'misuseRate',
value: summary.misuseRate,
samples: summary.otherCount,
threshold: config.misuseRate
},
{
metric: 'wellFormedRate',
value: summary.wellFormedRate,
samples: summary.emittedCount,
threshold: config.wellFormedRate
},
{
metric: 'groundedRate',
value: summary.groundedRate,
samples: summary.cardCount,
threshold: config.groundedRate
},
{
metric: 'misattributedRate',
value: summary.misattributedRate,
samples: summary.cardCount,
threshold: config.misattributedRate
},
{
metric: 'roundupMisuseRate',
value: summary.roundupMisuseRate,
samples: summary.roundupCount,
threshold: config.roundupMisuseRate
}
])
}
} finally {
await tracing.flush()
if (result.datasetRunUrl) console.log(`\n${result.datasetRunUrl}`)
}
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})