feat(eval): add assistant-focused and long-context eval cases; update harness
eval/cases/assistant_first_metrics.yaml
- Measures first-message quality: response latency, tool call count,
and whether the agent provides a useful answer without unnecessary
tool calls for simple conversational prompts
eval/cases/assistant_proactive.yaml
- Tests proactive assistant behavior: does the agent surface relevant
context, obligations, or memory without being explicitly asked?
- Validates that the agent uses memory and obligation tools proactively
eval/cases/procedural_long_context.yaml
- Multi-step procedural tasks designed to exceed typical context windows;
validates DAG compression and context recovery under token pressure
- Cases include: long file processing, multi-document synthesis, and
iterative code generation with history references
eval/scripts/generate_long_context_cases.py
- Script to generate parameterized long-context YAML test cases from
templates; supports configurable step counts and token targets
eval/cases/{edge_cases,memory_ops,meta_tools,multi_step,tool_calling}.yaml
- Updated assertions to use dragonscale module path and new tool names
- Improved partial-pass scoring for cases where the agent attempts the
right approach but returns a generic fallback
eval/cmd/eval-runner/main.go
- Updated import paths to dragonscale module
eval/fixtures/sample_data.txt + eval-test-skill/SKILL.md
- Updated fixture content with dragonscale branding
eval/go_evals/eval_test.go
- Updated test helpers for new module path and tool registry API
eval/promptfooconfig.yaml + eval/scripts/compare.sh
- Updated provider config and comparison script for dragonscale binary
This commit is contained in:
parent
5ca4a8c6da
commit
3a26ea3b52
4 changed files with 1311 additions and 0 deletions
65
eval/cases/assistant_first_metrics.yaml
Normal file
65
eval/cases/assistant_first_metrics.yaml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Assistant-First Metric-Oriented Eval Cases
|
||||
# These are proxy metrics for commitment miss rate, reminder latency,
|
||||
# proactive usefulness, and continuity quality.
|
||||
|
||||
- description: "metric proxy: commitment miss-rate (capture completeness)"
|
||||
vars:
|
||||
prompt: "Track these commitments exactly: send rent receipt tonight, book vet appointment tomorrow, and submit sprint notes by Friday. Return a commitment register and verification checklist."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const captured = [
|
||||
out.includes('rent'),
|
||||
out.includes('vet'),
|
||||
out.includes('sprint notes') || out.includes('notes')
|
||||
].filter(Boolean).length;
|
||||
const missRate = 1 - (captured / 3);
|
||||
const pass = captured >= 2;
|
||||
return { pass, score: captured / 3, reason: `captured=${captured}/3, miss_rate_proxy=${missRate.toFixed(2)}` };
|
||||
|
||||
- description: "metric proxy: reminder latency (first reminder timing specificity)"
|
||||
vars:
|
||||
prompt: "I must send a proposal in 4 hours. Give me a reminder schedule and specify when the first reminder should fire."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const hasFirstReminder = out.includes('first reminder') || out.includes('first check') || out.includes('first alert');
|
||||
const hasTiming = out.includes('hour') || out.includes('min') || out.includes('30') || out.includes('15');
|
||||
const pass = hasFirstReminder && hasTiming;
|
||||
return { pass, score: pass ? 1.0 : (hasTiming ? 0.5 : 0.0), reason: `first_reminder=${hasFirstReminder}, timing=${hasTiming}` };
|
||||
|
||||
- description: "metric proxy: proactive usefulness (anticipates risks + follow-ups)"
|
||||
vars:
|
||||
prompt: "I need to launch a small webinar next week. Give me a plan that includes proactive risk checks and follow-up actions I might forget."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const hasRisks = out.includes('risk') || out.includes('failure') || out.includes('fallback');
|
||||
const hasFollowups = out.includes('follow-up') || out.includes('follow up') || out.includes('check-in') || out.includes('verify');
|
||||
const hasConcrete = out.includes('day') || out.includes('hour') || out.includes('before');
|
||||
const score = (hasRisks ? 0.4 : 0) + (hasFollowups ? 0.4 : 0) + (hasConcrete ? 0.2 : 0);
|
||||
return { pass: score >= 0.8, score, reason: `risks=${hasRisks}, followups=${hasFollowups}, concrete=${hasConcrete}` };
|
||||
|
||||
- description: "metric proxy: continuity quality (references prior commitments in plan)"
|
||||
vars:
|
||||
prompt: "Given prior commitments {invoice Monday, PR review Tuesday, dentist this month}, provide this week's daily plan and explicitly carry forward unfinished items."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const hasAllItems = out.includes('invoice') && out.includes('pr') && out.includes('dentist');
|
||||
const hasCarryForward = out.includes('carry') || out.includes('unfinished') || out.includes('roll over') || out.includes('continue');
|
||||
const hasDailyShape = out.includes('monday') || out.includes('tuesday') || out.includes('daily');
|
||||
const score = (hasAllItems ? 0.5 : 0) + (hasCarryForward ? 0.3 : 0) + (hasDailyShape ? 0.2 : 0);
|
||||
return { pass: score >= 0.8, score, reason: `items=${hasAllItems}, carry_forward=${hasCarryForward}, daily_shape=${hasDailyShape}` };
|
||||
77
eval/cases/assistant_proactive.yaml
Normal file
77
eval/cases/assistant_proactive.yaml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Assistant-First Proactive Evaluation Cases
|
||||
# Focus: commitments, reminders, follow-ups, and long-horizon continuity.
|
||||
|
||||
- description: "commitment capture + reminder planning"
|
||||
vars:
|
||||
prompt: "I have three commitments: submit tax documents by March 15, follow up with Alex in 2 days, and renew my passport next month. Capture these commitments and give me a reminder/follow-up plan with explicit timing."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const isDefault = out.includes('completed processing but have no response');
|
||||
if (isDefault) return { pass: false, score: 0.1, reason: 'fell back to default empty response' };
|
||||
const coversAll = out.includes('tax') && out.includes('alex') && out.includes('passport');
|
||||
const hasPlanningLanguage = out.includes('remind') || out.includes('follow') || out.includes('schedule') || out.includes('timeline');
|
||||
const pass = coversAll && hasPlanningLanguage;
|
||||
return { pass, score: pass ? 1.0 : (coversAll || hasPlanningLanguage ? 0.5 : 0.0), reason: `covers_all=${coversAll}, planning=${hasPlanningLanguage}` };
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
const toolCalls = trace.steps.filter(s => s.type === 'tool_call');
|
||||
const getToolName = (t) => {
|
||||
if (t.tool !== 'tool_call') return t.tool;
|
||||
try { const a = typeof t.args === 'string' ? JSON.parse(t.args) : t.args; return (a && a.tool_name) || t.tool; } catch(e) { return t.tool; }
|
||||
};
|
||||
const toolNames = toolCalls.map(getToolName);
|
||||
const usedMemoryLikeTool = toolNames.includes('memory') || toolNames.includes('keyword_search') || toolNames.includes('semantic_search');
|
||||
return { pass: true, score: usedMemoryLikeTool ? 1.0 : 0.5, reason: usedMemoryLikeTool ? 'used memory-capable tooling for commitments' : `no memory tool observed (tools: ${toolNames.join(', ')})` };
|
||||
|
||||
- description: "follow-up escalation after missed commitment"
|
||||
vars:
|
||||
prompt: "Design a follow-up workflow for a missed commitment: if I miss 'send project update by Thursday', what should happen immediately, after 24h, and after 72h?"
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const hasImmediate = out.includes('immediate') || out.includes('now') || out.includes('right away');
|
||||
const has24h = out.includes('24h') || out.includes('24 h') || out.includes('24-hour') || out.includes('24 hour');
|
||||
const has72h = out.includes('72h') || out.includes('72 h') || out.includes('72-hour') || out.includes('72 hour');
|
||||
const hasEscalation = out.includes('escalat') || out.includes('backup') || out.includes('contingency') || out.includes('retry');
|
||||
const score = (hasImmediate ? 0.25 : 0) + (has24h ? 0.25 : 0) + (has72h ? 0.25 : 0) + (hasEscalation ? 0.25 : 0);
|
||||
return { pass: score >= 0.75, score, reason: `immediate=${hasImmediate}, 24h=${has24h}, 72h=${has72h}, escalation=${hasEscalation}` };
|
||||
|
||||
- description: "long-horizon continuity plan with weekly checkpoints"
|
||||
vars:
|
||||
prompt: "Create a 6-week proactive check-in schedule for learning Spanish with weekly milestones, reminders, and continuity checkpoints that reference prior-week progress."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const weekMentions = (out.match(/week\s*\d+/g) || []).length;
|
||||
const hasContinuity = out.includes('prior') || out.includes('previous') || out.includes('carry forward') || out.includes('progress');
|
||||
const hasReminder = out.includes('remind') || out.includes('check-in') || out.includes('checkpoint');
|
||||
const pass = weekMentions >= 4 && hasContinuity && hasReminder;
|
||||
const score = (Math.min(weekMentions, 6) / 6) * 0.5 + (hasContinuity ? 0.25 : 0) + (hasReminder ? 0.25 : 0);
|
||||
return { pass, score, reason: `week_mentions=${weekMentions}, continuity=${hasContinuity}, reminder=${hasReminder}` };
|
||||
|
||||
- description: "structured assistant output for commitments/reminders/follow-up verification"
|
||||
vars:
|
||||
prompt: "I promised to send invoice Monday, review PR Tuesday, and schedule dentist by end of month. Give me a structured plan that separates commitments, reminder timings, and follow-up verification steps."
|
||||
assert:
|
||||
- type: javascript
|
||||
value: |
|
||||
const trace = JSON.parse(output);
|
||||
if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };
|
||||
const out = (trace.output || '').toLowerCase();
|
||||
const hasCommitmentSection = out.includes('commitment');
|
||||
const hasReminderSection = out.includes('reminder');
|
||||
const hasFollowSection = out.includes('follow-up') || out.includes('follow up') || out.includes('verification') || out.includes('verify');
|
||||
const hasAllItems = out.includes('invoice') && out.includes('pr') && out.includes('dentist');
|
||||
const score = (hasCommitmentSection ? 0.25 : 0) + (hasReminderSection ? 0.25 : 0) + (hasFollowSection ? 0.25 : 0) + (hasAllItems ? 0.25 : 0);
|
||||
return { pass: score >= 0.75, score, reason: `commitments=${hasCommitmentSection}, reminders=${hasReminderSection}, followup=${hasFollowSection}, items=${hasAllItems}` };
|
||||
1078
eval/cases/procedural_long_context.yaml
Normal file
1078
eval/cases/procedural_long_context.yaml
Normal file
File diff suppressed because it is too large
Load diff
91
eval/scripts/generate_long_context_cases.py
Normal file
91
eval/scripts/generate_long_context_cases.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate contamination-resistant procedural long-context eval cases.
|
||||
|
||||
The generator creates synthetic timeline prompts with a hidden anchor token
|
||||
buried inside long context, then asks the model to retrieve that token.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def build_case(idx: int, rng: random.Random) -> tuple[str, str]:
|
||||
project = f"Project-{idx:02d}"
|
||||
token = f"VC-{rng.randint(100000, 999999)}"
|
||||
anchor_day = rng.randint(9, 42)
|
||||
total_days = rng.randint(55, 85)
|
||||
|
||||
lines = [
|
||||
"You are reading a synthetic operations timeline.",
|
||||
"Most lines are noise; one line contains a verification code.",
|
||||
]
|
||||
for day in range(1, total_days + 1):
|
||||
if day == anchor_day:
|
||||
lines.append(
|
||||
f"Day {day}: {project} verification code is {token}; keep this for final handoff."
|
||||
)
|
||||
else:
|
||||
phase = ["intake", "triage", "prep", "handoff", "review"][day % 5]
|
||||
lines.append(
|
||||
f"Day {day}: {project} {phase} log update {rng.randint(1000, 9999)}."
|
||||
)
|
||||
lines.append(
|
||||
f"Question: What is the verification code for {project}? Respond with only the code."
|
||||
)
|
||||
return "\n".join(lines), token
|
||||
|
||||
|
||||
def render_yaml(count: int, seed: int) -> str:
|
||||
rng = random.Random(seed)
|
||||
out = [
|
||||
"# Procedurally generated long-context eval cases.",
|
||||
f"# seed: {seed}",
|
||||
"",
|
||||
]
|
||||
for i in range(1, count + 1):
|
||||
prompt, token = build_case(i, rng)
|
||||
prompt_lines = [" " + line for line in prompt.splitlines()]
|
||||
out.extend(
|
||||
[
|
||||
f'- description: "procedural long-context retrieval #{i:02d}"',
|
||||
" vars:",
|
||||
" prompt: |",
|
||||
*prompt_lines,
|
||||
" assert:",
|
||||
" - type: javascript",
|
||||
" value: |",
|
||||
" const trace = JSON.parse(output);",
|
||||
" if (trace.error && !trace.output) return { pass: false, score: 0, reason: 'crashed: ' + trace.error };",
|
||||
" const out = (trace.output || '').toLowerCase();",
|
||||
f" const expected = '{token.lower()}';",
|
||||
" const pass = out.includes(expected);",
|
||||
" return { pass, score: pass ? 1.0 : 0.0, reason: pass ? 'retrieved correct anchor token' : `missing ${expected}` };",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(out).rstrip() + "\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--count", type=int, default=12, help="number of cases")
|
||||
parser.add_argument("--seed", type=int, default=20260221, help="rng seed")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path("eval/cases/procedural_long_context.yaml"),
|
||||
help="output YAML path",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
content = render_yaml(args.count, args.seed)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(content, encoding="utf-8")
|
||||
print(f"wrote {args.output} ({args.count} cases, seed={args.seed})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue