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:
ZanzyTHEbar 2026-02-21 19:02:04 +00:00
parent 5ca4a8c6da
commit 3a26ea3b52
4 changed files with 1311 additions and 0 deletions

View 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}` };

View 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}` };

File diff suppressed because it is too large Load diff

View 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()