|
| 1 | +"""Evaluation and prompt optimization pipeline tools. |
| 2 | +
|
| 3 | +Provides: |
| 4 | +- eval_cases registry with expected outputs |
| 5 | +- run_eval to execute test cases and score results |
| 6 | +- optimize_prompt to iteratively improve prompts based on eval regression |
| 7 | +""" |
| 8 | + |
| 9 | +import json |
| 10 | +import time |
| 11 | +from dataclasses import dataclass, field |
| 12 | +from datetime import datetime |
| 13 | + |
| 14 | + |
| 15 | +@dataclass |
| 16 | +class EvalCase: |
| 17 | + id: str |
| 18 | + input: str |
| 19 | + expected_keywords: list[str] = field(default_factory=list) |
| 20 | + max_tokens: int = 500 |
| 21 | + temperature: float = 0.3 |
| 22 | + |
| 23 | + |
| 24 | +@dataclass |
| 25 | +class EvalResult: |
| 26 | + case_id: str |
| 27 | + passed: bool |
| 28 | + response: str |
| 29 | + score: float # 0.0 - 1.0 |
| 30 | + missing_keywords: list[str] = field(default_factory=list) |
| 31 | + duration_ms: float = 0.0 |
| 32 | + |
| 33 | + |
| 34 | +@dataclass |
| 35 | +class PromptVariant: |
| 36 | + version: int |
| 37 | + prompt: str |
| 38 | + avg_score: float = 0.0 |
| 39 | + eval_count: int = 0 |
| 40 | + created_at: str = field(default_factory=lambda: datetime.now().isoformat()) |
| 41 | + |
| 42 | + |
| 43 | +# Built-in eval cases — extend by adding more |
| 44 | +BUILTIN_CASES = [ |
| 45 | + EvalCase(id="greet", input="Say hello in one sentence.", |
| 46 | + expected_keywords=["hello", "hi"], max_tokens=50), |
| 47 | + EvalCase(id="math", input="What is 2+2? Answer with just the number.", |
| 48 | + expected_keywords=["4"], max_tokens=20), |
| 49 | + EvalCase(id="code", input="Write a Python function that returns the sum of two numbers.", |
| 50 | + expected_keywords=["def", "return", "+"], max_tokens=200), |
| 51 | +] |
| 52 | + |
| 53 | + |
| 54 | +def list_eval_cases() -> list[dict]: |
| 55 | + """List all registered evaluation test cases.""" |
| 56 | + return [{"id": c.id, "input": c.input, "keywords": c.expected_keywords} |
| 57 | + for c in BUILTIN_CASES] |
| 58 | + |
| 59 | + |
| 60 | +def score_response(response: str, expected_keywords: list[str]) -> dict: |
| 61 | + """Score a response against expected keywords. Returns {score, missing}.""" |
| 62 | + lower = response.lower() |
| 63 | + missing = [k for k in expected_keywords if k.lower() not in lower] |
| 64 | + hit = len(expected_keywords) - len(missing) |
| 65 | + score = hit / len(expected_keywords) if expected_keywords else 1.0 |
| 66 | + return {"score": round(score, 2), "missing": missing, "hits": hit} |
| 67 | + |
| 68 | + |
| 69 | +def optimize_prompt(current_prompt: str, eval_scores: list[float], |
| 70 | + failure_analysis: str = "") -> dict: |
| 71 | + """Analyze eval scores and suggest prompt optimization. |
| 72 | +
|
| 73 | + Returns: {"version": int, "suggested_prompt": str, "analysis": str} |
| 74 | + """ |
| 75 | + avg = sum(eval_scores) / len(eval_scores) if eval_scores else 0.0 |
| 76 | + |
| 77 | + suggestions = [] |
| 78 | + if avg < 0.5: |
| 79 | + suggestions.append("Add explicit formatting instructions.") |
| 80 | + if "missing" in failure_analysis.lower(): |
| 81 | + suggestions.append("Include required output keywords in the prompt.") |
| 82 | + if not suggestions: |
| 83 | + suggestions.append("Prompt performing well. Minor tuning may help.") |
| 84 | + |
| 85 | + version = int(time.time()) |
| 86 | + optimized = current_prompt.strip() |
| 87 | + if suggestions: |
| 88 | + optimized += "\n\nAdditional instructions:\n- " + "\n- ".join(suggestions) |
| 89 | + |
| 90 | + return { |
| 91 | + "version": version, |
| 92 | + "avg_score": round(avg, 2), |
| 93 | + "suggestions": suggestions, |
| 94 | + "suggested_prompt": optimized, |
| 95 | + } |
0 commit comments