File size: 9,786 Bytes
1993d5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
End-to-End Evaluation Benchmark: Text -> Gemma 4 31B (Low Thinking) -> DSL v6.0 -> Solve -> Render.
Tests 5 national exam geometry problems across diverse categories.
"""

import asyncio
import json
import os
import sys
import time
from typing import Dict, Any, List

# Ensure backend root is in sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from agents.geometry_parser_agent import GeometryParserAgent
from solver.dsl_parser import DSLParser
from solver.engine import GeometryEngine
from solver.validator import GeometryValidator
from geometry_render.renderer import RendererAgent

BENCHMARK_CASES = [
    {
        "id": "geo_2016_essay_qessay_V",
        "name": "Oxyz Projection & Plane (THPT QG 2016)",
        "category": "oxyz",
        "text": (
            "Trong không gian với hệ tọa độ Oxyz cho ba điểm A(3; 2; -2), B(1; 0; 1) và C(2; -1; 3). "
            "Viết phương trình mặt phẳng đi qua A và vuông góc với đường thẳng BC. "
            "Tìm tọa độ hình chiếu vuông góc của A trên đường thẳng BC."
        ),
        "ground_truth_answer": "(P): x - y + 2z + 3 = 0; H(0; 1; -1)",
        "expected_points": ["A", "B", "C", "H"],
    },
    {
        "id": "geo_2017_123_q16",
        "name": "Regular Square Pyramid (THPT QG 2017)",
        "category": "solid_geometry (regular pyramid)",
        "text": (
            "Cho khối chóp tứ giác đều S.ABCD có cạnh đáy bằng a, cạnh bên gấp hai lần cạnh đáy (SA = 2a). "
            "Tính thể tích V của khối chóp đã cho."
        ),
        "ground_truth_answer": "V = sqrt(14)/6 * a^3",
        "expected_points": ["S", "A", "B", "C", "D"],
    },
    {
        "id": "geo_2016_essay_qessay_VII",
        "name": "Oblique Triangular Prism (THPT QG 2016)",
        "category": "solid_geometry (oblique prism)",
        "text": (
            "Cho lăng trụ ABC.A'B'C' có đáy ABC là tam giác vuông cân tại B, AC = 2a. "
            "Hình chiếu vuông góc của A' trên mặt phẳng (ABC) là trung điểm H của cạnh AC, "
            "đường thẳng A'B tạo với mặt phẳng (ABC) một góc 45°. "
            "Tính thể tích khối lăng trụ ABC.A'B'C'."
        ),
        "ground_truth_answer": "V = a^3 (A'H = a, S_ABC = a^2)",
        "expected_points": ["A", "B", "C", "A1", "B1", "C1", "H"],
    },
    {
        "id": "geo_2017_123_q18",
        "name": "Cylinder Round Body (THPT QG 2017)",
        "category": "round_bodies (cylinder)",
        "text": (
            "Tính thể tích V của khối trụ có bán kính đáy r = 4 và chiều cao h = 4*sqrt(2)."
        ),
        "ground_truth_answer": "V = 64*sqrt(2)*pi",
        "expected_points": ["O", "O1"],
    },
    {
        "id": "geo_2018_101_q25",
        "name": "Pyramid SA Perpendicular to Base (THPT QG 2018)",
        "category": "solid_geometry (pyramid)",
        "text": (
            "Cho hình chóp S.ABC có đáy là tam giác vuông đỉnh B, AB = 1, BC = 2, "
            "SA vuông góc với mặt phẳng đáy (ABC) và SA = 2. "
            "Tính khoảng cách từ A đến mặt phẳng (SBC)."
        ),
        "ground_truth_answer": "d(A, (SBC)) = 2*sqrt(5)/5 * a",
        "expected_points": ["S", "A", "B", "C"],
    },
]


async def run_single_case(case: Dict[str, Any], parser_agent: GeometryParserAgent) -> Dict[str, Any]:
    cid = case["id"]
    name = case["name"]
    text = case["text"]
    gt_ans = case.get("ground_truth_answer", "")
    print(f"\n========================================================")
    print(f"[*] RUNNING CASE: [{cid}] {name}")
    print(f"    Category: {case['category']}")
    print(f"    Đáp án chuẩn: {gt_ans}")
    print(f"    Text: {text}")

    report: Dict[str, Any] = {
        "id": cid,
        "name": name,
        "category": case["category"],
        "input_text": text,
        "ground_truth_answer": gt_ans,
        "stages": {},
        "status": "FAILED",
    }

    # Stage 1: Text -> LLM (Gemma 4 31B low thinking) -> DSL
    t0 = time.time()
    try:
        parsed_agent_out = await parser_agent.process(text)
        t_llm = (time.time() - t0) * 1000
        dsl_text = parsed_agent_out.get("geometry_dsl", "")
        shape_type = parsed_agent_out.get("type", "")
        print(f"  [1] LLM Success ({t_llm:.1f}ms): type={shape_type}")
        print(f"      Generated DSL:\n{dsl_text}")
        report["stages"]["llm_parsing"] = {
            "status": "OK",
            "latency_ms": round(t_llm, 1),
            "type": shape_type,
            "dsl": dsl_text,
            "entities": parsed_agent_out.get("entities", []),
        }
    except Exception as e:
        t_llm = (time.time() - t0) * 1000
        print(f"  [1] LLM FAILED ({t_llm:.1f}ms): {e}")
        report["stages"]["llm_parsing"] = {"status": "ERROR", "error": str(e), "latency_ms": round(t_llm, 1)}
        return report

    # Stage 2: DSL -> AST (DSLParser v6.0)
    t0 = time.time()
    try:
        dsl_parser = DSLParser()
        points, constraints, is_3d = dsl_parser.parse(dsl_text)
        t_dsl = (time.time() - t0) * 1000
        print(f"  [2] DSLParser Success ({t_dsl:.1f}ms): {len(points)} points, {len(constraints)} constraints, is_3d={is_3d}")
        report["stages"]["dsl_compiler"] = {
            "status": "OK",
            "latency_ms": round(t_dsl, 1),
            "num_points": len(points),
            "num_constraints": len(constraints),
            "is_3d": is_3d,
            "point_ids": [p.id for p in points],
        }
    except Exception as e:
        t_dsl = (time.time() - t0) * 1000
        print(f"  [2] DSLParser FAILED ({t_dsl:.1f}ms): {e}")
        report["stages"]["dsl_compiler"] = {"status": "ERROR", "error": str(e), "latency_ms": round(t_dsl, 1)}
        return report

    # Stage 3: AST -> Engine Solving (GeometryEngine)
    t0 = time.time()
    try:
        engine = GeometryEngine()
        solved_ir = engine.solve(points, constraints, is_3d=is_3d)
        t_solve = (time.time() - t0) * 1000
        if not solved_ir or not solved_ir.get("coordinates"):
            raise ValueError("GeometryEngine returned empty coordinates or failed solve.")
        coords = solved_ir.get("coordinates", {})
        print(f"  [3] GeometryEngine Success ({t_solve:.1f}ms): {len(coords)} coordinates solved")
        for pid, pos in list(coords.items())[:6]:
            print(f"        {pid}: {[round(v, 4) for v in pos]}")
        report["stages"]["geometry_engine"] = {
            "status": "OK",
            "latency_ms": round(t_solve, 1),
            "coordinates": {k: [round(v, 4) for v in val] for k, val in coords.items()},
            "num_faces": len(solved_ir.get("faces", [])),
            "num_segments": len(solved_ir.get("segments", [])),
        }
    except Exception as e:
        t_solve = (time.time() - t0) * 1000
        print(f"  [3] GeometryEngine FAILED ({t_solve:.1f}ms): {e}")
        report["stages"]["geometry_engine"] = {"status": "ERROR", "error": str(e), "latency_ms": round(t_solve, 1)}
        return report

    # Stage 4: Validation (GeometryValidator)
    validator = GeometryValidator()
    val_res = validator.validate(solved_ir, constraints, is_3d=is_3d)
    diag = val_res.to_dict()
    print(f"  [4] Validator: is_valid={diag['is_valid']}, checked={diag['checked_count']}, errors={len(diag['errors'])}")
    report["stages"]["validation"] = {
        "status": "OK" if diag["is_valid"] else "WARNING",
        "diagnostics": diag,
    }

    # Stage 5: Solving IR -> Renderer (Manim Script & Video)
    t0 = time.time()
    try:
        renderer = RendererAgent()
        manim_script = renderer.generate_manim_script(solved_ir)
        job_id = f"e2e_gemma_{cid}"
        video_path = renderer.run_manim(manim_script, job_id)
        t_render = (time.time() - t0) * 1000
        print(f"  [5] Renderer Success ({t_render:.1f}ms): video={video_path}")
        report["stages"]["renderer"] = {
            "status": "OK" if video_path else "SCRIPT_ONLY",
            "latency_ms": round(t_render, 1),
            "video_path": video_path,
            "script_length_lines": len(manim_script.splitlines()),
        }
    except Exception as e:
        t_render = (time.time() - t0) * 1000
        print(f"  [5] Renderer FAILED ({t_render:.1f}ms): {e}")
        report["stages"]["renderer"] = {"status": "ERROR", "error": str(e), "latency_ms": round(t_render, 1)}

    report["status"] = "SUCCESS"
    report["total_latency_ms"] = round(
        report["stages"]["llm_parsing"]["latency_ms"]
        + report["stages"]["dsl_compiler"]["latency_ms"]
        + report["stages"]["geometry_engine"]["latency_ms"]
        + report["stages"]["renderer"].get("latency_ms", 0),
        1
    )
    return report


async def main():
    print("================================================================")
    print("STARTING E2E BENCHMARK EVALUATION (GEMMA 4 31B LOW THINKING)")
    print("================================================================")

    parser_agent = GeometryParserAgent()
    results = []

    for case in BENCHMARK_CASES:
        res = await run_single_case(case, parser_agent)
        results.append(res)

    out_path = os.path.join(os.path.dirname(__file__), "benchmark_e2e_gemma_results.json")
    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(results, f, indent=2, ensure_ascii=False)

    print(f"\n================================================================")
    print(f"COMPLETED ALL 5 CASES. Results written to: {out_path}")
    print(f"================================================================")


if __name__ == "__main__":
    asyncio.run(main())