""" Post-Processing Hook Generator - Phase 2.9 Auto-generates middleware Python scripts for post-processing operations in optimization workflows. This handles the "post_processing_hooks" from Phase 2.7 LLM analysis. Hook scripts sit between optimization steps to: - Calculate custom objective functions - Combine multiple metrics with weights - Apply complex formulas - Transform results for next step Examples: - Weighted objective: 0.7 * norm_stress + 0.3 * norm_disp - Custom constraint: max_stress / yield_strength < 1.0 - Multi-criteria metric: sqrt(stress^2 + disp^2) Author: Atomizer Development Team Version: 0.1.0 (Phase 2.9) Last Updated: 2025-01-16 """ from typing import Dict, Any, List, Optional from dataclasses import dataclass from pathlib import Path import textwrap @dataclass class GeneratedHook: """Result of hook generation.""" script_name: str script_content: str inputs_required: List[str] outputs_created: List[str] description: str hook_type: str # 'weighted_objective', 'custom_formula', 'constraint', etc. class HookGenerator: """ Generates post-processing hook scripts for optimization workflows. Hook scripts are standalone Python modules that execute between optimization steps to perform custom calculations, combine metrics, or transform results. """ def __init__(self): """Initialize the hook generator.""" self.supported_hook_types = { 'weighted_objective', 'weighted_combination', 'custom_formula', 'constraint_check', 'multi_objective', 'custom_metric', 'comparison', 'threshold_check' } def generate_from_llm_output(self, hook_spec: Dict[str, Any]) -> GeneratedHook: """ Generate hook script from LLM-analyzed post-processing requirement. Args: hook_spec: Dictionary from LLM with keys: - action: str (e.g., "weighted_objective") - description: str - params: dict with inputs/weights/formula/etc. Returns: GeneratedHook with complete Python script """ action = hook_spec.get('action', '').lower() params = hook_spec.get('params', {}) description = hook_spec.get('description', '') # Determine hook type and generate appropriate script if 'weighted' in action or 'combination' in action: return self._generate_weighted_objective(params, description) elif 'formula' in action or 'custom' in action: return self._generate_custom_formula(params, description) elif 'constraint' in action or 'check' in action: return self._generate_constraint_check(params, description) elif 'comparison' in action or 'compare' in action: return self._generate_comparison(params, description) else: # Generic hook return self._generate_generic_hook(action, params, description) def _generate_weighted_objective(self, params: Dict[str, Any], description: str) -> GeneratedHook: """ Generate weighted objective function hook. Example params: { "inputs": ["norm_stress", "norm_disp"], "weights": [0.7, 0.3], "formula": "0.7 * norm_stress + 0.3 * norm_disp", # optional "objective": "minimize" } """ inputs = params.get('inputs', []) weights = params.get('weights', []) formula = params.get('formula', '') objective = params.get('objective', 'minimize') # Validate inputs and weights match if len(inputs) != len(weights): weights = [1.0 / len(inputs)] * len(inputs) # Equal weights if mismatch # Generate script name script_name = f"hook_weighted_objective_{'_'.join(inputs)}.py" # Build formula if not provided if not formula: terms = [f"{w} * {inp}" for w, inp in zip(weights, inputs)] formula = " + ".join(terms) # Generate script content script_content = f'''""" Weighted Objective Function Hook Auto-generated by Atomizer Phase 2.9 {description} Inputs: {', '.join(inputs)} Weights: {', '.join(map(str, weights))} Formula: {formula} Objective: {objective} """ import sys import json from pathlib import Path def weighted_objective({', '.join(inputs)}): """ Calculate weighted objective from multiple inputs. Args: {self._format_args_doc(inputs)} Returns: float: Weighted objective value """ result = {formula} return result def main(): """ Main entry point for hook execution. Reads inputs from JSON file, calculates objective, writes output. """ # Parse command line arguments if len(sys.argv) < 2: print("Usage: python {{}} ".format(sys.argv[0])) sys.exit(1) input_file = Path(sys.argv[1]) # Read inputs if not input_file.exists(): print(f"Error: Input file {{input_file}} not found") sys.exit(1) with open(input_file, 'r') as f: inputs = json.load(f) # Extract required inputs {self._format_input_extraction(inputs)} # Calculate weighted objective result = weighted_objective({', '.join(inputs)}) # Write output output_file = input_file.parent / "weighted_objective_result.json" output = {{ "weighted_objective": result, "objective_type": "{objective}", "inputs_used": {{{', '.join([f'"{inp}": {inp}' for inp in inputs])}}}, "formula": "{formula}" }} with open(output_file, 'w') as f: json.dump(output, f, indent=2) print(f"Weighted objective calculated: {{result:.6f}}") print(f"Result saved to: {{output_file}}") return result if __name__ == '__main__': main() ''' return GeneratedHook( script_name=script_name, script_content=script_content, inputs_required=inputs, outputs_created=['weighted_objective'], description=description or f"Weighted combination of {', '.join(inputs)}", hook_type='weighted_objective' ) def _generate_custom_formula(self, params: Dict[str, Any], description: str) -> GeneratedHook: """ Generate custom formula hook. Example params: { "inputs": ["max_stress", "yield_strength"], "formula": "max_stress / yield_strength", "output_name": "safety_factor" } """ inputs = params.get('inputs', []) formula = params.get('formula', '') output_name = params.get('output_name', 'custom_result') if not formula: raise ValueError("Custom formula hook requires 'formula' parameter") script_name = f"hook_custom_{output_name}.py" script_content = f'''""" Custom Formula Hook Auto-generated by Atomizer Phase 2.9 {description} Formula: {output_name} = {formula} Inputs: {', '.join(inputs)} """ import sys import json from pathlib import Path def calculate_{output_name}({', '.join(inputs)}): """ Calculate custom metric using formula. Args: {self._format_args_doc(inputs)} Returns: float: {output_name} """ {output_name} = {formula} return {output_name} def main(): """Main entry point for hook execution.""" if len(sys.argv) < 2: print("Usage: python {{}} ".format(sys.argv[0])) sys.exit(1) input_file = Path(sys.argv[1]) # Read inputs with open(input_file, 'r') as f: inputs = json.load(f) # Extract required inputs {self._format_input_extraction(inputs)} # Calculate result result = calculate_{output_name}({', '.join(inputs)}) # Write output output_file = input_file.parent / "{output_name}_result.json" output = {{ "{output_name}": result, "formula": "{formula}", "inputs_used": {{{', '.join([f'"{inp}": {inp}' for inp in inputs])}}} }} with open(output_file, 'w') as f: json.dump(output, f, indent=2) print(f"{output_name} = {{result:.6f}}") print(f"Result saved to: {{output_file}}") return result if __name__ == '__main__': main() ''' return GeneratedHook( script_name=script_name, script_content=script_content, inputs_required=inputs, outputs_created=[output_name], description=description or f"Custom formula: {formula}", hook_type='custom_formula' ) def _generate_constraint_check(self, params: Dict[str, Any], description: str) -> GeneratedHook: """ Generate constraint checking hook. Example params: { "inputs": ["max_stress", "yield_strength"], "condition": "max_stress < yield_strength", "threshold": 1.0, "constraint_name": "stress_limit" } """ inputs = params.get('inputs', []) condition = params.get('condition', '') threshold = params.get('threshold', 1.0) constraint_name = params.get('constraint_name', 'constraint') script_name = f"hook_constraint_{constraint_name}.py" script_content = f'''""" Constraint Check Hook Auto-generated by Atomizer Phase 2.9 {description} Constraint: {condition} Threshold: {threshold} """ import sys import json from pathlib import Path def check_{constraint_name}({', '.join(inputs)}): """ Check constraint condition. Args: {self._format_args_doc(inputs)} Returns: tuple: (satisfied: bool, value: float, violation: float) """ value = {condition if condition else f"{inputs[0]} / {threshold}"} satisfied = value <= {threshold} violation = max(0.0, value - {threshold}) return satisfied, value, violation def main(): """Main entry point for hook execution.""" if len(sys.argv) < 2: print("Usage: python {{}} ".format(sys.argv[0])) sys.exit(1) input_file = Path(sys.argv[1]) # Read inputs with open(input_file, 'r') as f: inputs = json.load(f) # Extract required inputs {self._format_input_extraction(inputs)} # Check constraint satisfied, value, violation = check_{constraint_name}({', '.join(inputs)}) # Write output output_file = input_file.parent / "{constraint_name}_check.json" output = {{ "constraint_name": "{constraint_name}", "satisfied": satisfied, "value": value, "threshold": {threshold}, "violation": violation, "inputs_used": {{{', '.join([f'"{inp}": {inp}' for inp in inputs])}}} }} with open(output_file, 'w') as f: json.dump(output, f, indent=2) status = "SATISFIED" if satisfied else "VIOLATED" print(f"Constraint {{status}}: {{value:.6f}} (threshold: {threshold})") if not satisfied: print(f"Violation: {{violation:.6f}}") print(f"Result saved to: {{output_file}}") return value if __name__ == '__main__': main() ''' return GeneratedHook( script_name=script_name, script_content=script_content, inputs_required=inputs, outputs_created=[constraint_name, f'{constraint_name}_satisfied', f'{constraint_name}_violation'], description=description or f"Constraint check: {condition}", hook_type='constraint_check' ) def _generate_comparison(self, params: Dict[str, Any], description: str) -> GeneratedHook: """ Generate comparison hook (min/max ratio, difference, etc.). Example params: { "inputs": ["min_force", "avg_force"], "operation": "ratio", "output_name": "min_to_avg_ratio" } """ inputs = params.get('inputs', []) operation = params.get('operation', 'ratio').lower() output_name = params.get('output_name', f"{operation}_result") if len(inputs) < 2: raise ValueError("Comparison hook requires at least 2 inputs") # Determine formula based on operation if operation == 'ratio': formula = f"{inputs[0]} / {inputs[1]}" elif operation == 'difference': formula = f"{inputs[0]} - {inputs[1]}" elif operation == 'percent_difference': formula = f"(({inputs[0]} - {inputs[1]}) / {inputs[1]}) * 100.0" else: formula = f"{inputs[0]} / {inputs[1]}" # Default to ratio script_name = f"hook_compare_{output_name}.py" script_content = f'''""" Comparison Hook Auto-generated by Atomizer Phase 2.9 {description} Operation: {operation} Formula: {output_name} = {formula} """ import sys import json from pathlib import Path def compare_{operation}({', '.join(inputs)}): """ Compare values using {operation}. Args: {self._format_args_doc(inputs)} Returns: float: Comparison result """ result = {formula} return result def main(): """Main entry point for hook execution.""" if len(sys.argv) < 2: print("Usage: python {{}} ".format(sys.argv[0])) sys.exit(1) input_file = Path(sys.argv[1]) # Read inputs with open(input_file, 'r') as f: inputs = json.load(f) # Extract required inputs {self._format_input_extraction(inputs)} # Calculate comparison result = compare_{operation}({', '.join(inputs)}) # Write output output_file = input_file.parent / "{output_name}.json" output = {{ "{output_name}": result, "operation": "{operation}", "formula": "{formula}", "inputs_used": {{{', '.join([f'"{inp}": {inp}' for inp in inputs])}}} }} with open(output_file, 'w') as f: json.dump(output, f, indent=2) print(f"{output_name} = {{result:.6f}}") print(f"Result saved to: {{output_file}}") return result if __name__ == '__main__': main() ''' return GeneratedHook( script_name=script_name, script_content=script_content, inputs_required=inputs, outputs_created=[output_name], description=description or f"{operation.capitalize()} of {', '.join(inputs)}", hook_type='comparison' ) def _generate_generic_hook(self, action: str, params: Dict[str, Any], description: str) -> GeneratedHook: """Generate generic hook for unknown action types.""" inputs = params.get('inputs', ['input_value']) formula = params.get('formula', 'input_value') output_name = params.get('output_name', 'result') script_name = f"hook_generic_{action.replace(' ', '_')}.py" script_content = f'''""" Generic Hook Auto-generated by Atomizer Phase 2.9 {description} Action: {action} """ import sys import json from pathlib import Path def process({', '.join(inputs)}): """Process inputs according to action.""" # TODO: Implement {action} result = {formula} return result def main(): """Main entry point for hook execution.""" if len(sys.argv) < 2: print("Usage: python {{}} ".format(sys.argv[0])) sys.exit(1) input_file = Path(sys.argv[1]) with open(input_file, 'r') as f: inputs = json.load(f) {self._format_input_extraction(inputs)} result = process({', '.join(inputs)}) output_file = input_file.parent / "{output_name}.json" with open(output_file, 'w') as f: json.dump({{"result": result}}, f, indent=2) print(f"Result: {{result}}") return result if __name__ == '__main__': main() ''' return GeneratedHook( script_name=script_name, script_content=script_content, inputs_required=inputs, outputs_created=[output_name], description=description or f"Generic hook: {action}", hook_type='generic' ) def _format_args_doc(self, args: List[str]) -> str: """Format argument documentation for docstrings.""" lines = [] for arg in args: lines.append(f" {arg}: float") return '\n'.join(lines) def _format_input_extraction(self, inputs: List[str]) -> str: """Format input extraction code.""" lines = [] for inp in inputs: lines.append(f' {inp} = inputs.get("{inp}")') lines.append(f' if {inp} is None:') lines.append(f' print(f"Error: Required input \'{inp}\' not found")') lines.append(f' sys.exit(1)') return '\n'.join(lines) def generate_batch(self, hook_specs: List[Dict[str, Any]]) -> List[GeneratedHook]: """ Generate multiple hook scripts. Args: hook_specs: List of hook specifications from LLM Returns: List of GeneratedHook objects """ return [self.generate_from_llm_output(spec) for spec in hook_specs] def save_hook_to_file(self, hook: GeneratedHook, output_dir: Path) -> Path: """ Save generated hook script to file. Args: hook: GeneratedHook object output_dir: Directory to save script Returns: Path to saved script file """ output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) script_path = output_dir / hook.script_name with open(script_path, 'w') as f: f.write(hook.script_content) return script_path def generate_hook_registry(self, hooks: List[GeneratedHook], output_file: Path): """ Generate a registry file documenting all hooks. Args: hooks: List of generated hooks output_file: Path to registry JSON file """ registry = { "hooks": [ { "name": hook.script_name, "type": hook.hook_type, "description": hook.description, "inputs": hook.inputs_required, "outputs": hook.outputs_created } for hook in hooks ] } import json with open(output_file, 'w') as f: json.dump(registry, f, indent=2) def generate_lifecycle_hook(self, hook_spec: Dict[str, Any], hook_point: str = "post_calculation") -> str: """ Generate a hook compatible with Atomizer's lifecycle hook system (Phase 1). This creates a hook that integrates with HookManager and can be loaded from the plugins directory structure. Args: hook_spec: Hook specification from LLM (same as generate_from_llm_output) hook_point: Which lifecycle point to hook into (default: post_calculation) Returns: Complete Python module content with register_hooks() function Example output file: optimization_engine/plugins/post_calculation/weighted_objective.py """ # Generate the core hook logic first generated_hook = self.generate_from_llm_output(hook_spec) action = hook_spec.get('action', '').lower() params = hook_spec.get('params', {}) description = hook_spec.get('description', '') # Extract function name from hook type if 'weighted' in action: func_name = "weighted_objective_hook" elif 'formula' in action or 'custom' in action: output_name = params.get('output_name', 'custom_result') func_name = f"{output_name}_hook" elif 'constraint' in action: constraint_name = params.get('constraint_name', 'constraint') func_name = f"{constraint_name}_hook" elif 'comparison' in action: operation = params.get('operation', 'comparison') func_name = f"{operation}_hook" else: func_name = "custom_hook" # Build the lifecycle-compatible hook module module_content = f'''""" {description} Auto-generated lifecycle hook by Atomizer Phase 2.9 Hook Point: {hook_point} Inputs: {', '.join(generated_hook.inputs_required)} Outputs: {', '.join(generated_hook.outputs_created)} """ import logging from typing import Dict, Any, Optional logger = logging.getLogger(__name__) def {func_name}(context: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ {description} Args: context: Hook context containing: - trial_number: Current optimization trial - results: Dictionary with extracted FEA results - calculations: Dictionary with inline calculation results Returns: Dictionary with calculated values to add to context """ logger.info(f"Executing {func_name} for trial {{context.get('trial_number', 'unknown')}}") # Extract inputs from context results = context.get('results', {{}}) calculations = context.get('calculations', {{}}) ''' # Add input extraction based on hook type for input_var in generated_hook.inputs_required: module_content += f''' {input_var} = calculations.get('{input_var}') or results.get('{input_var}') if {input_var} is None: logger.error(f"Required input '{input_var}' not found in context") raise ValueError(f"Missing required input: {input_var}") ''' # Add the core calculation logic if 'weighted' in action: inputs = params.get('inputs', []) weights = params.get('weights', []) formula = params.get('formula', '') if not formula: terms = [f"{w} * {inp}" for w, inp in zip(weights, inputs)] formula = " + ".join(terms) module_content += f''' # Calculate weighted objective result = {formula} logger.info(f"Weighted objective calculated: {{result:.6f}}") return {{ 'weighted_objective': result, '{generated_hook.outputs_created[0]}': result }} ''' elif 'formula' in action or 'custom' in action: formula = params.get('formula', '') output_name = params.get('output_name', 'custom_result') module_content += f''' # Calculate using custom formula {output_name} = {formula} logger.info(f"{output_name} = {{{output_name}:.6f}}") return {{ '{output_name}': {output_name} }} ''' elif 'constraint' in action: condition = params.get('condition', '') threshold = params.get('threshold', 1.0) constraint_name = params.get('constraint_name', 'constraint') module_content += f''' # Check constraint value = {condition if condition else f"{generated_hook.inputs_required[0]} / {threshold}"} satisfied = value <= {threshold} violation = max(0.0, value - {threshold}) status = "SATISFIED" if satisfied else "VIOLATED" logger.info(f"Constraint {{status}}: {{value:.6f}} (threshold: {threshold})") return {{ '{constraint_name}': value, '{constraint_name}_satisfied': satisfied, '{constraint_name}_violation': violation }} ''' elif 'comparison' in action: operation = params.get('operation', 'ratio').lower() inputs = params.get('inputs', []) output_name = params.get('output_name', f"{operation}_result") if operation == 'ratio': formula = f"{inputs[0]} / {inputs[1]}" elif operation == 'difference': formula = f"{inputs[0]} - {inputs[1]}" elif operation == 'percent_difference': formula = f"(({inputs[0]} - {inputs[1]}) / {inputs[1]}) * 100.0" else: formula = f"{inputs[0]} / {inputs[1]}" module_content += f''' # Calculate comparison result = {formula} logger.info(f"{output_name} = {{result:.6f}}") return {{ '{output_name}': result }} ''' # Add registration function for HookManager module_content += f''' def register_hooks(hook_manager): """ Register this hook with the HookManager. This function is called automatically when the plugin is loaded. Args: hook_manager: The HookManager instance """ hook_manager.register_hook( hook_point='{hook_point}', function={func_name}, description="{description}", name="{func_name}", priority=100, enabled=True ) logger.info(f"Registered {func_name} at {hook_point}") ''' return module_content def main(): """Test the hook generator.""" print("=" * 80) print("Phase 2.9: Post-Processing Hook Generator Test") print("=" * 80) print() generator = HookGenerator() # Test cases from Phase 2.7 LLM output test_hooks = [ { "action": "weighted_objective", "description": "Combine normalized stress (70%) and displacement (30%)", "params": { "inputs": ["norm_stress", "norm_disp"], "weights": [0.7, 0.3], "objective": "minimize" } }, { "action": "custom_formula", "description": "Calculate safety factor", "params": { "inputs": ["max_stress", "yield_strength"], "formula": "yield_strength / max_stress", "output_name": "safety_factor" } }, { "action": "comparison", "description": "Compare min force to average", "params": { "inputs": ["min_force", "avg_force"], "operation": "ratio", "output_name": "min_to_avg_ratio" } }, { "action": "constraint_check", "description": "Check if stress is below yield", "params": { "inputs": ["max_stress", "yield_strength"], "condition": "max_stress / yield_strength", "threshold": 1.0, "constraint_name": "yield_constraint" } } ] print("Test Hook Generation:") print() for i, hook_spec in enumerate(test_hooks, 1): print(f"{i}. {hook_spec['description']}") hook = generator.generate_from_llm_output(hook_spec) print(f" Script: {hook.script_name}") print(f" Type: {hook.hook_type}") print(f" Inputs: {', '.join(hook.inputs_required)}") print(f" Outputs: {', '.join(hook.outputs_created)}") print() # Generate and save example hooks print("=" * 80) print("Example: Weighted Objective Hook Script") print("=" * 80) print() weighted_hook = generator.generate_from_llm_output(test_hooks[0]) print(weighted_hook.script_content) # Save hooks to files output_dir = Path("generated_hooks") print("=" * 80) print(f"Saving generated hooks to: {output_dir}") print("=" * 80) print() generated_hooks = generator.generate_batch(test_hooks) for hook in generated_hooks: script_path = generator.save_hook_to_file(hook, output_dir) print(f"[OK] Saved: {script_path}") # Generate registry registry_path = output_dir / "hook_registry.json" generator.generate_hook_registry(generated_hooks, registry_path) print(f"[OK] Registry: {registry_path}") if __name__ == '__main__': main()