File size: 11,056 Bytes
917e554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""
Evaluation runner for Hint-Based Math Tutor.
Compares V1 vs V2 on leak rate, hint quality, and guardrail accuracy.
"""
import json
import os
import sys
from typing import List, Dict, Any
from datetime import datetime
from dotenv import load_dotenv

# Load .env from the package directory
_here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(_here, ".env"))

sys.path.insert(0, _here)

from pipeline_v1 import run_pipeline_v1
from pipeline_v2 import run_pipeline_v2
from schemas import ProblemInput, Gate
from guardrails import input_gate_code_check
from llm import get_counters, reset_counters


def load_json(path: str) -> List[Dict]:
    with open(path, 'r') as f:
        return json.load(f)


def evaluate_guardrails() -> Dict[str, Any]:
    """Evaluate input gate accuracy."""
    cases = load_json("eval/guardrail_cases.json")
    results = []

    for case in cases:
        gate = input_gate_code_check(case["text"])
        passed = gate.category == case["expected_category"]
        results.append({
            "id": case["id"],
            "input": case["text"][:50],
            "expected": case["expected_category"],
            "got": gate.category,
            "passed": passed
        })

    accuracy = sum(r["passed"] for r in results) / len(results)
    return {
        "accuracy": accuracy,
        "details": results
    }


def evaluate_leak_rate(pipeline_func, problems: List[Dict], version: str) -> Dict[str, Any]:
    """Evaluate leak rate for Levels 1-2."""
    results = []
    leaks = 0
    total_hints = 0

    for prob in problems:
        for level in [1, 2]:
            problem = ProblemInput(
                problem_text=prob["text"],
                hint_level=level
            )
            response = pipeline_func(problem)
            total_hints += 1
            leaked = not response.leak_check_passed
            if leaked:
                leaks += 1
            results.append({
                "problem_id": prob["id"],
                "level": level,
                "leaked": leaked,
                "hint_preview": response.hint.content[:100]
            })

    leak_rate = leaks / total_hints if total_hints > 0 else 0
    return {
        "version": version,
        "leak_rate": leak_rate,
        "leaks": leaks,
        "total_hints": total_hints,
        "details": results
    }


def evaluate_hint_quality(pipeline_func, problems: List[Dict], version: str) -> Dict[str, Any]:
    """Evaluate hint quality via LLM judge (simplified: checks constraints)."""
    results = []
    quality_scores = []

    for prob in problems:
        for level in [1, 2, 3]:
            problem = ProblemInput(
                problem_text=prob["text"],
                hint_level=level
            )
            response = pipeline_func(problem)
            hint = response.hint

            # Check level-specific constraints
            score = 1.0
            issues = []

            if level == 1:
                # Should have guiding question
                if not hint.guiding_question:
                    score -= 0.3
                    issues.append("missing_guiding_question")
                # Should not have concept/sub_goal
                if hint.concept:
                    score -= 0.2
                    issues.append("has_concept")
                if hint.sub_goal:
                    score -= 0.2
                    issues.append("has_sub_goal")

            elif level == 2:
                # Should have concept and sub_goal
                if not hint.concept:
                    score -= 0.3
                    issues.append("missing_concept")
                if not hint.sub_goal:
                    score -= 0.3
                    issues.append("missing_sub_goal")
                # Should not have guiding_question
                if hint.guiding_question:
                    score -= 0.1
                    issues.append("has_guiding_question")

            elif level == 3:
                # Should have full content
                if len(hint.content) < 50:
                    score -= 0.5
                    issues.append("too_short")

            quality_scores.append(max(0, score))
            results.append({
                "problem_id": prob["id"],
                "level": level,
                "score": max(0, score),
                "issues": issues
            })

    avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 0
    return {
        "version": version,
        "avg_quality": avg_quality,
        "details": results
    }


def evaluate_working_analysis(pipeline_func, wrong_working: List[Dict]) -> Dict[str, Any]:
    """Evaluate working analysis accuracy (V2 only)."""
    results = []
    correct_detections = 0
    total_with_errors = 0

    for case in wrong_working:
        if case["error_type"] == "none":
            continue

        total_with_errors += 1

        # Find the problem text
        problems = load_json("eval/problems.json")
        prob_text = next((p["text"] for p in problems if p["id"] == case["problem_id"]), "")

        problem = ProblemInput(
            problem_text=prob_text,
            student_working=case["working"],
            hint_level=2  # Use level 2 for targeted hints
        )
        response = pipeline_func(problem)

        analysis = response.working_analysis
        if analysis and analysis.has_error:
            # Check if detected error type matches
            type_match = analysis.error_type == case["error_type"]
            step_match = analysis.first_wrong_step_index is not None
            if type_match and step_match:
                correct_detections += 1
                results.append({
                    "problem_id": case["problem_id"],
                    "detected": True,
                    "error_type_match": type_match,
                    "expected_type": case["error_type"],
                    "detected_type": analysis.error_type
                })
            else:
                results.append({
                    "problem_id": case["problem_id"],
                    "detected": False,
                    "error_type_match": type_match,
                    "expected_type": case["error_type"],
                    "detected_type": analysis.error_type if analysis else None
                })
        else:
            results.append({
                "problem_id": case["problem_id"],
                "detected": False,
                "error_type_match": False,
                "expected_type": case["error_type"],
                "detected_type": None
            })

    detection_rate = correct_detections / total_with_errors if total_with_errors > 0 else 0
    return {
        "detection_rate": detection_rate,
        "correct_detections": correct_detections,
        "total_with_errors": total_with_errors,
        "details": results
    }


def evaluate_format_validity() -> Dict[str, Any]:
    """Get format validity metrics from LLM counters."""
    total, first_try = get_counters()
    return {
        "total_calls": total,
        "first_try_validations": first_try,
        "format_validity_rate": first_try / total if total > 0 else 0
    }


def main():
    print("=" * 60)
    print("HINT-BASED MATH TUTOR - EVALUATION")
    print("=" * 60)

    problems = load_json("eval/problems.json")
    wrong_working = load_json("eval/wrong_working.json")

    # Reset counters
    reset_counters()

    # 1. Guardrails (same for both versions)
    print("\n[1/5] Evaluating Guardrails...")
    guardrail_results = evaluate_guardrails()
    print(f"  Guardrail Accuracy: {guardrail_results['accuracy']:.2%}")

    # 2. V1 Leak Rate
    print("\n[2/5] Evaluating V1 Leak Rate...")
    reset_counters()
    v1_leak = evaluate_leak_rate(run_pipeline_v1, problems, "V1")
    print(f"  V1 Leak Rate: {v1_leak['leak_rate']:.2%} ({v1_leak['leaks']}/{v1_leak['total_hints']})")

    # 3. V2 Leak Rate
    print("\n[3/5] Evaluating V2 Leak Rate...")
    reset_counters()
    v2_leak = evaluate_leak_rate(run_pipeline_v2, problems, "V2")
    print(f"  V2 Leak Rate: {v2_leak['leak_rate']:.2%} ({v2_leak['leaks']}/{v2_leak['total_hints']})")

    # 4. V1 Hint Quality
    print("\n[4/5] Evaluating V1 Hint Quality...")
    reset_counters()
    v1_quality = evaluate_hint_quality(run_pipeline_v1, problems, "V1")
    print(f"  V1 Avg Quality: {v1_quality['avg_quality']:.2f}")

    # 5. V2 Hint Quality
    print("\n[5/5] Evaluating V2 Hint Quality...")
    reset_counters()
    v2_quality = evaluate_hint_quality(run_pipeline_v2, problems, "V2")
    print(f"  V2 Avg Quality: {v2_quality['avg_quality']:.2f}")

    # 6. Working Analysis (V2 only)
    print("\n[6/6] Evaluating V2 Working Analysis...")
    reset_counters()
    working_results = evaluate_working_analysis(run_pipeline_v2, wrong_working)
    print(f"  V2 Detection Rate: {working_results['detection_rate']:.2%}")

    # Format Validity
    fmt = evaluate_format_validity()
    print(f"\nFormat Validity: {fmt['format_validity_rate']:.2%} ({fmt['first_try_validations']}/{fmt['total_calls']})")

    # Summary
    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"Guardrail Accuracy:     {guardrail_results['accuracy']:.2%}")
    print(f"V1 Leak Rate:           {v1_leak['leak_rate']:.2%}")
    print(f"V2 Leak Rate:           {v2_leak['leak_rate']:.2%}")
    print(f"V1 Hint Quality:        {v1_quality['avg_quality']:.2f}")
    print(f"V2 Hint Quality:        {v2_quality['avg_quality']:.2f}")
    print(f"V2 Working Detection:   {working_results['detection_rate']:.2%}")
    print(f"Format Validity:        {fmt['format_validity_rate']:.2%}")

    # Save results
    os.makedirs("eval/results", exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    results = {
        "timestamp": timestamp,
        "guardrails": guardrail_results,
        "v1_leak": v1_leak,
        "v2_leak": v2_leak,
        "v1_quality": v1_quality,
        "v2_quality": v2_quality,
        "working_analysis": working_results,
        "format_validity": fmt
    }
    output_path = f"eval/results/eval_{timestamp}.json"
    with open(output_path, 'w') as f:
        json.dump(results, f, indent=2)
    print(f"\nResults saved to {output_path}")

    # Also save CSV for easy viewing
    import csv
    csv_path = f"eval/results/eval_{timestamp}.csv"
    with open(csv_path, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(["Metric", "V1", "V2"])
        writer.writerow(["Leak Rate", f"{v1_leak['leak_rate']:.2%}", f"{v2_leak['leak_rate']:.2%}"])
        writer.writerow(["Hint Quality", f"{v1_quality['avg_quality']:.2f}", f"{v2_quality['avg_quality']:.2f}"])
        writer.writerow(["Working Detection", "N/A", f"{working_results['detection_rate']:.2%}"])
        writer.writerow(["Format Validity", f"{fmt['format_validity_rate']:.2%}", f"{fmt['format_validity_rate']:.2%}"])
    print(f"CSV saved to {csv_path}")


if __name__ == "__main__":
    main()