Task configuration:
{"task_id": "15", "task_name": "create_cylinder_with_rectangular_groove", "description": "Create a cylinder with a rectangular groove tilted around Y-axis in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task15.FCStd", "cylinder_radius": 20.0, "cylinder_height": 40.0, "groove_width": 10.0, "groove_depth": 5.0, "groove_height": 30.0, "groove_angle": 30.0}, "instruction_template": "The application is already open. Create a cylinder with radius ${cylinder_radius}mm and height ${cylinder_height}mm in FreeCAD, then create a tilted rectangular groove on its side. The groove should have width ${groove_width}mm, depth ${groove_depth}mm, height ${groove_height}mm, and be tilted around the Y-axis by ${groove_angle} degrees. The cylinder should be created as an additive cylinder (typeid), and the rectangular groove should be created as a subtractive box (typeid). Do not create the shapes separately and use Boolean subtraction or cut. Finally, save the file to ${source_path}${filename}", "evaluation_setup": {"scripts": [{"role": "hook", "path": "hooker.js"}, {"role": "handler", "path": "handler.py"}], "timeout": 300, "evaluator_type": "HookManager"}, "preconditions": {"freecad_running": false}, "total_key_steps": 2, "events": {"script_initialized": {"description": "Hook script initialized", "is_key_step": false}, "function_found": {"description": "Document save function found", "is_key_step": false}, "function_called": {"description": "Document save function called", "is_key_step": true, "key_step_index": 1, "key_step_name": "Document saved"}, "function_key_word_detected": {"description": "Detected key content in saved document", "is_key_step": true, "key_step_index": 2, "key_step_name": "Created cylinder with tilted rectangular groove and saved successfully"}, "error": {"description": "Error event", "is_key_step": false}}, "mcp_servers": [{"command": "python3", "args": ["/home/agent/.local/share/FreeCAD/Mod/freecad_mcp/src/freecad_bridge.py"]}]}

Hook script:
// FreeCAD Cylinder with Tilted Rectangular Groove Monitoring Hook Script
// Used to monitor FreeCAD creation of a cylinder with tilted rectangular groove and detect task completion
// After creating a cylinder with tilted rectangular groove and saving the file, the test program listens for the save and checks if the corresponding document contains the required cylinder with tilted rectangular groove

(function() {
  // Script constants setting
  const FUNCTION_NAME = "_ZNK3App8Document10saveToFileEPKc"
  const ORIGIN_FUNCTION_NAME = "Document::saveToFile"
  const FUNCTION_BEHAVIOR = "Document Save"

  const SCRIPT_INITIALIZED = "script_initialized"
  const FUNCTION_NOT_FOUND = "function_not_found"
  const FUNCTION_FOUND = "function_found"
  const FUNCTION_CALLED = "function_called"
  const FUNCTION_KEY_WORD_DETECTED = "function_key_word_detected"
  const ERROR = "error"
  const HOOK_INSTALLED = "hook_installed"

  const APP_NAME = "FreeCAD"
  
  // Global variables
  let funcFound = false;
  
  // Send events to the evaluation system
  function sendEvent(eventType, data = {}) {
      const payload = {
          event: eventType,
          ...data,
          timestamp: new Date().getTime()
      };
      send(payload);
  }
  
  // Find Document::saveToFile function
  function getFunction() {
      // Try to find the function directly through exported symbols
      let FuncAddr = DebugSymbol.getFunctionByName(FUNCTION_NAME);
      
      // If not found, report error
      if (!FuncAddr) {
          sendEvent(ERROR, {
              error_type: FUNCTION_NOT_FOUND,
              message: `Unable to find ${ORIGIN_FUNCTION_NAME} function`
          });
          return null;
      }
      
      // Report function found
      funcFound = true;
      sendEvent(FUNCTION_FOUND, {
          address: FuncAddr.toString(),
          message: `Found ${ORIGIN_FUNCTION_NAME} function`
      });
      
      return FuncAddr;
  }
  
  // Initialize hook and execute immediately
  function initHook() {
      sendEvent(SCRIPT_INITIALIZED, {
          message: `${APP_NAME} ${FUNCTION_BEHAVIOR} monitoring script has started`
      });
      
      // Find target function
      const funcAddr = getFunction();
      if (!funcAddr) {
          return;
      }
      
      // Install function hook
      Interceptor.attach(funcAddr, {
          onEnter: function(args) {
              try {
                  sendEvent(FUNCTION_CALLED, {
                      message: `Intercepted ${FUNCTION_BEHAVIOR} function call`
                  });
                  this.filename = args[1].readCString();
              } catch (error) {
                  sendEvent(ERROR, {
                      error_type: "general_error",
                      message: `Execution error: ${error.message}`
                  });
              }
          },

          onLeave: function(retval) {
              try {
                if (retval) {
                    const pythonCode = `
import freecad
import FreeCAD
import Part
import math

# Open the specified file
file_path = '${this.filename}'
doc = FreeCAD.open(file_path)

# Get the active document
if doc is None:
    result = None
else:
    # Find cylinder and rectangular groove
    cylinder = None
    groove = None
    
    # Check all objects, looking for cylinder and rectangular groove
    for obj in doc.Objects:
        # Check if it's a solid object
        if hasattr(obj, "Shape"):
            # Check shape type
            if hasattr(obj.Shape, "ShapeType"):
                # For objects created with Part Design method, we need to check sub-objects
                if obj.TypeId == "PartDesign::Body":
                    # Prevent counting the same object multiple times
                    processed_objects = set()
                    
                    for subobj in obj.OutList:
                        if hasattr(subobj, "Shape") and hasattr(subobj.Shape, "ShapeType"):
                            # Use TypeId to check if it's an additive cylinder
                            if subobj.TypeId == 'PartDesign::AdditiveCylinder':
                                # Use object ID as a unique identifier
                                obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name
                                
                                # If this object has already been processed, skip it
                                if obj_id in processed_objects:
                                    continue
                                processed_objects.add(obj_id)
                                
                                if subobj.Shape.ShapeType == "Solid":
                                    # Check if it's a cylinder, read properties directly
                                    cylinder_radius = subobj.Radius.Value if hasattr(subobj, "Radius") else 0
                                    cylinder_height = subobj.Height.Value if hasattr(subobj, "Height") else 0
                                    
                                    # Store cylinder information
                                    cylinder = {
                                        'radius': cylinder_radius,
                                        'height': cylinder_height
                                    }
                            
                            # Use TypeId to check if it's a subtractive box (rectangular groove)
                            elif subobj.TypeId == 'PartDesign::SubtractiveBox':
                                # Use object ID as a unique identifier
                                obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name
                                
                                # If this object has already been processed, skip it
                                if obj_id in processed_objects:
                                    continue
                                processed_objects.add(obj_id)
                                
                                if subobj.Shape.ShapeType == "Solid":
                                    # Get basic properties of the rectangular groove
                                    # Height, width and depth of the rectangular groove
                                    groove_width = 0
                                    groove_height = 0
                                    groove_depth = 0
                                    groove_angle = 0
                                    
                                    # Try to extract from properties
                                    if hasattr(subobj, "Length"):
                                        groove_width = subobj.Length.Value
                                    if hasattr(subobj, "Height"):
                                        groove_height = subobj.Height.Value
                                    if hasattr(subobj, "Width"):
                                        groove_depth = subobj.Width.Value
                                    if hasattr(subobj, "Angle"):
                                        groove_angle = subobj.Angle
                                    
                                    # For box, get dimensions directly from the bounding box (if not already obtained)
                                    if hasattr(subobj, "Shape") and hasattr(subobj.Shape, "BoundBox"):
                                        bounds = subobj.Shape.BoundBox
                                        if groove_width == 0:
                                            groove_width = bounds.XLength
                                        if groove_height == 0:
                                            groove_height = bounds.ZLength
                                        if groove_depth == 0:
                                            groove_depth = bounds.YLength
                                    
                                    # Calculate tilt angle from placement position and angle (if not already obtained from properties)
                                    if groove_angle == 0 and hasattr(subobj, "Placement"):
                                        # Extract angle from rotation matrix
                                        if hasattr(subobj.Placement, "Rotation"):
                                            # Get rotation angle (in degrees)
                                            # In FreeCAD, rotation is usually expressed in radians
                                            rot_angle = subobj.Placement.Rotation.Angle * 180.0 / math.pi
                                            
                                            # Extract rotation around y-axis (usually the tilt angle)
                                            axis = subobj.Placement.Rotation.Axis
                                            if abs(axis.y) > 0.7:  # If rotation is mainly around Y axis
                                                groove_angle = rot_angle
                                            else:
                                                # Calculate angle with Y axis
                                                import math
                                                groove_angle = math.degrees(math.acos(axis.y))
                                                
                                    # Tilt angle could also be determined from position relative to cylinder
                                    # But we've already tried to get it from rotation
                                    
                                    # Store rectangular groove information
                                    groove = {
                                        'width': groove_width,
                                        'depth': groove_depth,
                                        'height': groove_height,
                                        'angle': groove_angle
                                    }
    
    # Return results - ensure we return pure numbers without units
    # Handle values that may have units
    def extract_value(val):
        if val is None:
            return None
        try:
            # If it's a string with unit (like "10.0 mm"), extract the numeric part
            if isinstance(val, str) and ' ' in val:
                return float(val.split()[0])
            # If it's a FreeCAD Quantity object, try to convert to float
            return float(val)
        except:
            # If conversion fails, return the original value
            return val
    
    result = {
        'cylinder_radius': extract_value(cylinder['radius']) if cylinder else None,
        'cylinder_height': extract_value(cylinder['height']) if cylinder else None,
        'groove_width': extract_value(groove['width']) if groove else None,
        'groove_depth': extract_value(groove['depth']) if groove else None,
        'groove_height': extract_value(groove['height']) if groove else None,
        'groove_angle': extract_value(groove['angle']) if groove else None,
        'has_groove': groove is not None
    }
                    `
                    sendEvent(FUNCTION_KEY_WORD_DETECTED, {
                        message: `Detected ${FUNCTION_BEHAVIOR} operation`,
                        filename: this.filename,
                        code: pythonCode
                    });
                }
                // Detect keywords
              } catch (error) {
                  sendEvent(ERROR, {
                      error_type: "general_error",
                      message: `Execution error: ${error.message}`
                  });
              }
          }
      });
      
      sendEvent(HOOK_INSTALLED, {
          message: `Hook installation complete, waiting for ${FUNCTION_BEHAVIOR} operation...`
      });
  }
  
  // Execute hook initialization immediately
  initHook();
})();


Evaluation handler:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
FreeCAD Cylinder with Tilted Rectangular Groove Event Handler (implemented via subtractive box)
Responsible for processing events generated by the hook script and updating evaluation metrics
"""

from typing import Dict, Any, Optional, List

# Event type constants
SCRIPT_INITIALIZED = "script_initialized"
FUNCTION_NOT_FOUND = "function_not_found"
FUNCTION_FOUND = "function_found"
FUNCTION_CALLED = "function_called"
FUNCTION_KEY_WORD_DETECTED = "function_key_word_detected"
ERROR = "error"
HOOK_INSTALLED = "hook_installed"

# Keyword-related constants
CYLINDER_RADIUS = "cylinder_radius"
CYLINDER_HEIGHT = "cylinder_height"
GROOVE_WIDTH = "groove_width"
GROOVE_DEPTH = "groove_depth"
GROOVE_HEIGHT = "groove_height"
GROOVE_ANGLE = "groove_angle"
HAS_GROOVE = "has_groove"

def execute_python_code(code: str, logger: Any) -> Dict[str, Any]:
    """
    Execute Python code and return the results
    
    Args:
        code: Python code to execute
        logger: Logger object
        
    Returns:
        Dict[str, Any]: Execution result
    """
    try:
        # Create a new namespace to execute the code
        namespace = {}
        exec(code, namespace)
        result = namespace.get('result', None)
        
        if result is None:
            logger.warning("Cylinder with tilted rectangular groove object not found")
            return None
            
        # Validate result format
        required_keys = [
            CYLINDER_RADIUS, CYLINDER_HEIGHT,
            GROOVE_WIDTH, GROOVE_DEPTH, GROOVE_HEIGHT, GROOVE_ANGLE, HAS_GROOVE
        ]
        if not all(key in result for key in required_keys):
            logger.error(f"Result missing required keys: {required_keys}")
            return None
            
        return result
    except Exception as e:
        logger.error(f"Error executing Python code: {str(e)}")
        return None

def message_handler(message: Dict[str, Any], logger: Any, task_parameter: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
    """
    Process messages received from the hook script
    
    Args:
        message: Frida message object
        logger: Logger object
        task_parameter: Task parameters
        
    Returns:
        Optional[List[Dict[str, Any]]]: List of status updates, or None if no updates
    """
    updates = []
    
    if message.get('type') == 'send' and 'payload' in message:
        payload = message['payload']
        
        if 'event' in payload:
            event_type = payload['event']
            logger.debug(f"Received event: {event_type}")
            
            if event_type == SCRIPT_INITIALIZED:
                logger.info(f"Hook script initialized: {payload.get('message', '')}")
                
            elif event_type == FUNCTION_FOUND:
                logger.info(f"Function found: {payload.get('address', '')}")
                
            elif event_type == FUNCTION_CALLED:
                logger.info(f"Function called: {payload.get('message', '')}")
                # Update first key step status
                updates.append({
                    'status': 'key_step',
                    'index': 1,
                    'name': 'Document saved'
                })
                
            elif event_type == FUNCTION_KEY_WORD_DETECTED:
                # Execute Python code and get results
                code = payload.get('code', '')
                filename = payload.get('filename', '')
                expected_path = task_parameter.get("source_path", "") + task_parameter.get("filename", "")
                logger.info(f"Detected keywords, document path: {filename}, expected document path: {expected_path}")
                
                if filename == expected_path:
                    result = execute_python_code(code, logger)
                    if result:
                        # Check if cylinder with groove parameters match expectations
                        expected_cylinder_radius = task_parameter.get(CYLINDER_RADIUS, 20.0)
                        expected_cylinder_height = task_parameter.get(CYLINDER_HEIGHT, 40.0)
                        expected_groove_width = task_parameter.get(GROOVE_WIDTH, 10.0)
                        expected_groove_depth = task_parameter.get(GROOVE_DEPTH, 5.0)
                        expected_groove_height = task_parameter.get(GROOVE_HEIGHT, 30.0)
                        expected_groove_angle = task_parameter.get(GROOVE_ANGLE, 30.0)

                        actual_cylinder_radius = result[CYLINDER_RADIUS]
                        actual_cylinder_height = result[CYLINDER_HEIGHT]
                        actual_groove_width = result[GROOVE_WIDTH]
                        actual_groove_depth = result[GROOVE_DEPTH]
                        actual_groove_height = result[GROOVE_HEIGHT]
                        actual_groove_angle = result[GROOVE_ANGLE]
                        actual_has_groove = result[HAS_GROOVE]

                        # Log key parameters
                        logger.info(f"Task parameter check: Expected cylinder radius={expected_cylinder_radius}, height={expected_cylinder_height}")
                        logger.info(f"Expected rectangular groove: width={expected_groove_width}, depth={expected_groove_depth}, " + 
                                   f"height={expected_groove_height}, angle={expected_groove_angle}")
                        logger.info(f"Actual parameters: Actual cylinder radius={actual_cylinder_radius}, height={actual_cylinder_height}")
                        logger.info(f"Actual rectangular groove: width={actual_groove_width}, depth={actual_groove_depth}, " +
                                   f"height={actual_groove_height}, angle={actual_groove_angle}, " +
                                   f"has groove: {actual_has_groove}")

                        try:
                            # Try to get the numeric part (handle possible units)
                            actual_cylinder_radius_value = float(str(actual_cylinder_radius).split()[0]) if isinstance(actual_cylinder_radius, str) else float(actual_cylinder_radius)
                            actual_cylinder_height_value = float(str(actual_cylinder_height).split()[0]) if isinstance(actual_cylinder_height, str) else float(actual_cylinder_height)
                            
                            actual_groove_width_value = float(str(actual_groove_width).split()[0]) if isinstance(actual_groove_width, str) else float(actual_groove_width)
                            actual_groove_depth_value = float(str(actual_groove_depth).split()[0]) if isinstance(actual_groove_depth, str) else float(actual_groove_depth)
                            actual_groove_height_value = float(str(actual_groove_height).split()[0]) if isinstance(actual_groove_height, str) else float(actual_groove_height)
                            actual_groove_angle_value = float(str(actual_groove_angle).split()[0]) if isinstance(actual_groove_angle, str) else float(actual_groove_angle)
                            
                            # Allow a certain error range (0.01%)
                            cylinder_radius_error = abs((actual_cylinder_radius_value - expected_cylinder_radius) / expected_cylinder_radius) <= 0.0001
                            cylinder_height_error = abs((actual_cylinder_height_value - expected_cylinder_height) / expected_cylinder_height) <= 0.0001
                            
                            groove_width_error = abs((actual_groove_width_value - expected_groove_width) / expected_groove_width) <= 0.0001
                            groove_depth_error = abs((actual_groove_depth_value - expected_groove_depth) / expected_groove_depth) <= 0.0001
                            groove_height_error = abs((actual_groove_height_value - expected_groove_height) / expected_groove_height) <= 0.0001
                            # For angle, allow a certain absolute error (1.0 degree)
                            groove_angle_error = abs(actual_groove_angle_value - expected_groove_angle) <= 1.0
                            
                            has_groove_correct = actual_has_groove
                        except Exception as e:
                            logger.error(f"Error comparing parameters: {str(e)}")
                            cylinder_radius_error = False
                            cylinder_height_error = False
                            groove_width_error = False
                            groove_depth_error = False
                            groove_height_error = False
                            groove_angle_error = False
                            has_groove_correct = False
                        
                        cylinder_correct = cylinder_radius_error and cylinder_height_error
                        groove_correct = (groove_width_error and groove_depth_error and
                                        groove_height_error and groove_angle_error and
                                        has_groove_correct)
                        
                        if cylinder_correct and groove_correct:
                            # Update second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Created cylinder with tilted rectangular groove and saved successfully'
                            })
                            
                            # Task completed successfully
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created a cylinder with tilted rectangular groove that meets requirements and saved it'
                            })
                        else:
                            logger.warning(f"Parameter validation failed: " + 
                                          f"Cylinder correct: {cylinder_correct}, " +
                                          f"Rectangular groove correct: {groove_correct}")
                
            elif event_type == ERROR:
                error_type = payload.get("error_type", "unknown")
                error_message = payload.get("message", "Unknown error")
                
                logger.error(f"Hook script error ({error_type}): {error_message}")
                
                # Record error event
                updates.append({
                    'status': 'error',
                    'type': error_type,
                    'message': error_message
                })
                
    elif message.get('type') == 'error':
        logger.error(f"Hook script error: {message.get('stack', '')}")
        
        # Record error event
        updates.append({
            'status': 'error',
            'type': 'script_error',
            'message': message.get('stack', 'Unknown error')
        })
    
    return updates if updates else None