Spaces:
Sleeping
Sleeping
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())
|