e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
284 lines
7.6 KiB
Python
284 lines
7.6 KiB
Python
"""GeoGebra command validator and fixer.
|
|
|
|
This module validates GeoGebra commands and attempts to fix common mistakes
|
|
that LLMs might make when generating GeoGebra scripts.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
import re
|
|
|
|
|
|
@dataclass
|
|
class ValidationResult:
|
|
"""Result of validating a GeoGebra command."""
|
|
|
|
original: str
|
|
fixed: str
|
|
is_valid: bool
|
|
warnings: list[str] = field(default_factory=list)
|
|
errors: list[str] = field(default_factory=list)
|
|
|
|
|
|
# Commands that should use square brackets (GeoGebra standard syntax)
|
|
COMMANDS_WITH_BRACKETS = {
|
|
# Points
|
|
"Point",
|
|
"Midpoint",
|
|
"Intersect",
|
|
"Center",
|
|
"Focus",
|
|
"Vertex",
|
|
# Lines
|
|
"Line",
|
|
"Segment",
|
|
"Ray",
|
|
"Perpendicular",
|
|
"PerpendicularBisector",
|
|
"AngleBisector",
|
|
"Tangent",
|
|
"Asymptote",
|
|
"Directrix",
|
|
# Vectors
|
|
"Vector",
|
|
"UnitVector",
|
|
"PerpendicularVector",
|
|
# Circles and Conics
|
|
"Circle",
|
|
"Ellipse",
|
|
"Hyperbola",
|
|
"Parabola",
|
|
"Conic",
|
|
# Polygons
|
|
"Polygon",
|
|
# Angles
|
|
"Angle",
|
|
# Transformations
|
|
"Translate",
|
|
"Rotate",
|
|
"Reflect",
|
|
"Dilate",
|
|
# Functions
|
|
"Derivative",
|
|
"Integral",
|
|
"If",
|
|
"Function",
|
|
# Styling
|
|
"SetColor",
|
|
"SetLineThickness",
|
|
"SetPointSize",
|
|
"SetFilling",
|
|
"SetLabelVisible",
|
|
"SetCaption",
|
|
"SetVisible",
|
|
"SetLineStyle",
|
|
# View
|
|
"SetCoordSystem",
|
|
"ShowAxes",
|
|
"ShowGrid",
|
|
"ZoomIn",
|
|
"ZoomOut",
|
|
# Text
|
|
"Text",
|
|
# Other
|
|
"Locus",
|
|
"Sequence",
|
|
"Element",
|
|
"Length",
|
|
}
|
|
|
|
# Common mistakes patterns and their fixes
|
|
COMMON_MISTAKES = [
|
|
# Point({x, y}) -> (x, y)
|
|
(r"Point\s*\(\s*\{\s*([^}]+)\s*\}\s*\)", r"(\1)"),
|
|
# log(10, x) -> lg(x)
|
|
(r"\blog\s*\(\s*10\s*,\s*([^)]+)\s*\)", r"lg(\1)"),
|
|
# Remove # comments (GeoGebra doesn't support them)
|
|
(r"^\s*#.*$", ""),
|
|
]
|
|
|
|
# Patterns for detecting parentheses that should be brackets
|
|
PAREN_TO_BRACKET_PATTERN = re.compile(
|
|
r"\b(" + "|".join(COMMANDS_WITH_BRACKETS) + r")\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def fix_brackets(command: str) -> tuple[str, list[str]]:
|
|
"""Fix commands that use parentheses instead of square brackets.
|
|
|
|
Args:
|
|
command: A single GeoGebra command
|
|
|
|
Returns:
|
|
Tuple of (fixed command, list of warnings)
|
|
"""
|
|
warnings = []
|
|
fixed = command
|
|
|
|
def replace_with_brackets(match):
|
|
cmd_name = match.group(1)
|
|
args = match.group(2)
|
|
warnings.append(f"Changed {cmd_name}(...) to {cmd_name}[...]")
|
|
return f"{cmd_name}[{args}]"
|
|
|
|
fixed = PAREN_TO_BRACKET_PATTERN.sub(replace_with_brackets, fixed)
|
|
|
|
return fixed, warnings
|
|
|
|
|
|
def fix_common_mistakes(command: str) -> tuple[str, list[str]]:
|
|
"""Fix common LLM mistakes in GeoGebra commands.
|
|
|
|
Args:
|
|
command: A single GeoGebra command
|
|
|
|
Returns:
|
|
Tuple of (fixed command, list of warnings)
|
|
"""
|
|
warnings = []
|
|
fixed = command
|
|
|
|
for pattern, replacement in COMMON_MISTAKES:
|
|
if re.search(pattern, fixed, re.MULTILINE):
|
|
old = fixed
|
|
fixed = re.sub(pattern, replacement, fixed, flags=re.MULTILINE)
|
|
if old != fixed:
|
|
warnings.append(f"Fixed pattern: {pattern}")
|
|
|
|
return fixed, warnings
|
|
|
|
|
|
def validate_equation_format(command: str) -> tuple[str, list[str]]:
|
|
"""Validate and fix equation formats.
|
|
|
|
Args:
|
|
command: A single GeoGebra command
|
|
|
|
Returns:
|
|
Tuple of (fixed command, list of warnings)
|
|
"""
|
|
warnings = []
|
|
|
|
# Check for fractional coefficients in conic equations
|
|
if re.search(r"[xy]\s*\^\s*2\s*/\s*\d+", command):
|
|
warnings.append(
|
|
"Equation contains fractional coefficients. "
|
|
"Consider using integer form (e.g., '9x^2 + 4y^2 = 36' instead of 'x^2/4 + y^2/9 = 1')"
|
|
)
|
|
|
|
return command, warnings
|
|
|
|
|
|
def validate_command(command: str) -> ValidationResult:
|
|
"""Validate and fix a single GeoGebra command.
|
|
|
|
Args:
|
|
command: A single GeoGebra command
|
|
|
|
Returns:
|
|
ValidationResult with the fixed command and any warnings/errors
|
|
"""
|
|
result = ValidationResult(original=command, fixed=command, is_valid=True)
|
|
|
|
# Skip empty lines
|
|
if not command.strip():
|
|
return result
|
|
|
|
# Skip comment lines
|
|
if command.strip().startswith("#"):
|
|
result.fixed = ""
|
|
result.warnings.append("Removed comment line (GeoGebra doesn't support # comments)")
|
|
return result
|
|
|
|
# Fix common mistakes
|
|
fixed, warnings = fix_common_mistakes(command)
|
|
result.fixed = fixed
|
|
result.warnings.extend(warnings)
|
|
|
|
# Fix brackets
|
|
fixed, warnings = fix_brackets(result.fixed)
|
|
result.fixed = fixed
|
|
result.warnings.extend(warnings)
|
|
|
|
# Check equation format
|
|
_, warnings = validate_equation_format(result.fixed)
|
|
result.warnings.extend(warnings)
|
|
|
|
# Check if anything was fixed
|
|
if result.original != result.fixed:
|
|
result.is_valid = False
|
|
|
|
return result
|
|
|
|
|
|
def validate_ggbscript(script: str) -> tuple[str, list[str], list[str]]:
|
|
"""Validate and fix a complete GGBScript.
|
|
|
|
Args:
|
|
script: The full GeoGebra script content
|
|
|
|
Returns:
|
|
Tuple of (fixed script, list of all warnings, list of all errors)
|
|
"""
|
|
lines = script.split("\n")
|
|
fixed_lines = []
|
|
all_warnings = []
|
|
all_errors = []
|
|
|
|
for i, line in enumerate(lines, 1):
|
|
stripped = line.strip()
|
|
|
|
# Skip empty lines
|
|
if not stripped:
|
|
fixed_lines.append(line)
|
|
continue
|
|
|
|
result = validate_command(stripped)
|
|
|
|
if result.warnings:
|
|
for warning in result.warnings:
|
|
all_warnings.append(f"Line {i}: {warning}")
|
|
|
|
if result.errors:
|
|
for error in result.errors:
|
|
all_errors.append(f"Line {i}: {error}")
|
|
|
|
# Add the fixed line (preserve original indentation)
|
|
if result.fixed:
|
|
indent = len(line) - len(line.lstrip())
|
|
fixed_lines.append(" " * indent + result.fixed)
|
|
# If fixed is empty (e.g., removed comment), skip the line
|
|
|
|
return "\n".join(fixed_lines), all_warnings, all_errors
|
|
|
|
|
|
def get_command_help(command_name: str) -> str | None:
|
|
"""Get help text for a GeoGebra command.
|
|
|
|
Args:
|
|
command_name: The name of the command
|
|
|
|
Returns:
|
|
Help text or None if command not found
|
|
"""
|
|
help_texts = {
|
|
"Circle": "Circle[center, radius] or Circle[center, point] or Circle[A, B, C]",
|
|
"Ellipse": "Ellipse[F1, F2, a] (foci and semi-major axis) or Ellipse[F1, F2, P] (foci and point)",
|
|
"Hyperbola": "Hyperbola[F1, F2, a] or Hyperbola[F1, F2, P]",
|
|
"Parabola": "Parabola[focus, directrix_line]",
|
|
"Line": "Line[A, B] (through two points) or Line[point, parallel_line]",
|
|
"Segment": "Segment[A, B] or Segment[A, length]",
|
|
"Ray": "Ray[A, B] (from A through B)",
|
|
"Perpendicular": "Perpendicular[point, line] (perpendicular line through point)",
|
|
"Midpoint": "Midpoint[A, B] or Midpoint[segment]",
|
|
"Intersect": "Intersect[obj1, obj2] (all intersections) or Intersect[obj1, obj2, n] (nth intersection)",
|
|
"Polygon": "Polygon[A, B, C, ...] or Polygon[A, B, n] (regular n-gon)",
|
|
"SetColor": 'SetColor[obj, r, g, b] (RGB 0-255) or SetColor[obj, "Red"]',
|
|
"SetCoordSystem": "SetCoordSystem[xMin, xMax, yMin, yMax]",
|
|
"If": "If[condition, then_value, else_value]",
|
|
"Derivative": "Derivative[f] or Derivative[f, n] (nth derivative)",
|
|
"Integral": "Integral[f] (indefinite) or Integral[f, a, b] (definite)",
|
|
}
|
|
|
|
return help_texts.get(command_name)
|