""" MCP Tool: Build Optimization Configuration Wraps the OptimizationConfigBuilder to create an MCP-compatible tool that helps LLMs guide users through building optimization configurations. This tool: 1. Discovers the FEA model (design variables) 2. Lists available objectives and constraints 3. Builds a complete optimization_config.json based on user selections """ from pathlib import Path from typing import Dict, Any, List, Optional import json import sys # Add project root to path for imports project_root = Path(__file__).parent.parent.parent sys.path.insert(0, str(project_root)) from optimization_engine.optimization_config_builder import OptimizationConfigBuilder from mcp_server.tools.model_discovery import discover_fea_model def build_optimization_config( sim_file_path: str, design_variables: List[Dict[str, Any]], objectives: List[Dict[str, Any]], constraints: Optional[List[Dict[str, Any]]] = None, optimization_settings: Optional[Dict[str, Any]] = None, output_path: Optional[str] = None ) -> Dict[str, Any]: """ MCP Tool: Build Optimization Configuration Creates a complete optimization configuration file from user selections. Args: sim_file_path: Absolute path to .sim file design_variables: List of design variable definitions [ { 'name': 'tip_thickness', 'lower_bound': 15.0, 'upper_bound': 25.0 }, ... ] objectives: List of objective definitions [ { 'objective_key': 'minimize_mass', 'weight': 5.0, # optional 'target': None # optional, for goal programming }, ... ] constraints: Optional list of constraint definitions [ { 'constraint_key': 'max_stress_limit', 'limit_value': 200.0 }, ... ] optimization_settings: Optional dict with algorithm settings { 'n_trials': 100, 'sampler': 'TPE' } output_path: Optional path to save config JSON. Defaults to 'optimization_config.json' in sim file directory Returns: Dictionary with status and configuration details Example: >>> result = build_optimization_config( ... sim_file_path="C:/Projects/Bracket/analysis.sim", ... design_variables=[ ... {'name': 'tip_thickness', 'lower_bound': 15.0, 'upper_bound': 25.0} ... ], ... objectives=[ ... {'objective_key': 'minimize_mass', 'weight': 5.0} ... ], ... constraints=[ ... {'constraint_key': 'max_stress_limit', 'limit_value': 200.0} ... ] ... ) """ try: # Step 1: Discover model model_result = discover_fea_model(sim_file_path) if model_result['status'] != 'success': return { 'status': 'error', 'error_type': 'model_discovery_failed', 'message': model_result.get('message', 'Failed to discover FEA model'), 'suggestion': model_result.get('suggestion', 'Check that the .sim file is valid') } # Step 2: Create builder builder = OptimizationConfigBuilder(model_result) # Step 3: Validate and add design variables available_vars = {dv['name']: dv for dv in builder.list_available_design_variables()} for dv in design_variables: name = dv['name'] if name not in available_vars: return { 'status': 'error', 'error_type': 'invalid_design_variable', 'message': f"Design variable '{name}' not found in model", 'available_variables': list(available_vars.keys()), 'suggestion': f"Choose from: {', '.join(available_vars.keys())}" } builder.add_design_variable( name=name, lower_bound=dv['lower_bound'], upper_bound=dv['upper_bound'] ) # Step 4: Add objectives available_objectives = builder.list_available_objectives() for obj in objectives: obj_key = obj['objective_key'] if obj_key not in available_objectives: return { 'status': 'error', 'error_type': 'invalid_objective', 'message': f"Objective '{obj_key}' not recognized", 'available_objectives': list(available_objectives.keys()), 'suggestion': f"Choose from: {', '.join(available_objectives.keys())}" } builder.add_objective( objective_key=obj_key, weight=obj.get('weight'), target=obj.get('target') ) # Step 5: Add constraints (optional) if constraints: available_constraints = builder.list_available_constraints() for const in constraints: const_key = const['constraint_key'] if const_key not in available_constraints: return { 'status': 'error', 'error_type': 'invalid_constraint', 'message': f"Constraint '{const_key}' not recognized", 'available_constraints': list(available_constraints.keys()), 'suggestion': f"Choose from: {', '.join(available_constraints.keys())}" } builder.add_constraint( constraint_key=const_key, limit_value=const['limit_value'] ) # Step 6: Set optimization settings (optional) if optimization_settings: builder.set_optimization_settings( n_trials=optimization_settings.get('n_trials'), sampler=optimization_settings.get('sampler') ) # Step 7: Build and validate configuration config = builder.build() # Step 8: Save to file if output_path is None: sim_path = Path(sim_file_path) output_path = sim_path.parent / 'optimization_config.json' else: output_path = Path(output_path) with open(output_path, 'w') as f: json.dump(config, f, indent=2) # Step 9: Return success with summary return { 'status': 'success', 'message': 'Optimization configuration created successfully', 'config_file': str(output_path), 'summary': { 'design_variables': len(config['design_variables']), 'objectives': len(config['objectives']), 'constraints': len(config['constraints']), 'n_trials': config['optimization_settings']['n_trials'], 'sampler': config['optimization_settings']['sampler'] }, 'config': config } except ValueError as e: return { 'status': 'error', 'error_type': 'validation_error', 'message': str(e), 'suggestion': 'Check that all required fields are provided correctly' } except Exception as e: return { 'status': 'error', 'error_type': 'unexpected_error', 'message': str(e), 'suggestion': 'This may be a bug. Please report this issue.' } def list_optimization_options(sim_file_path: str) -> Dict[str, Any]: """ Helper tool: List all available optimization options for a model. This is useful for LLMs to show users what they can choose from. Args: sim_file_path: Absolute path to .sim file Returns: Dictionary with all available options """ try: # Discover model model_result = discover_fea_model(sim_file_path) if model_result['status'] != 'success': return model_result # Create builder to get options builder = OptimizationConfigBuilder(model_result) # Get all available options design_vars = builder.list_available_design_variables() objectives = builder.list_available_objectives() constraints = builder.list_available_constraints() return { 'status': 'success', 'sim_file': sim_file_path, 'available_design_variables': design_vars, 'available_objectives': objectives, 'available_constraints': constraints, 'model_info': { 'solutions': model_result.get('solutions', []), 'expression_count': len(model_result.get('expressions', [])) } } except Exception as e: return { 'status': 'error', 'error_type': 'unexpected_error', 'message': str(e) } def format_optimization_options_for_llm(options: Dict[str, Any]) -> str: """ Format optimization options for LLM consumption (Markdown). Args: options: Output from list_optimization_options() Returns: Markdown-formatted string """ if options['status'] != 'success': return f"❌ **Error**: {options['message']}\n\n💡 {options.get('suggestion', '')}" md = [] md.append(f"# Optimization Configuration Options\n") md.append(f"**Model**: `{options['sim_file']}`\n") # Design Variables md.append(f"## Available Design Variables ({len(options['available_design_variables'])})\n") if options['available_design_variables']: md.append("| Name | Current Value | Units | Suggested Bounds |") md.append("|------|---------------|-------|------------------|") for dv in options['available_design_variables']: bounds = dv['suggested_bounds'] md.append(f"| `{dv['name']}` | {dv['current_value']} | {dv['units']} | [{bounds[0]:.2f}, {bounds[1]:.2f}] |") else: md.append("⚠️ No design variables found. Model may not be parametric.") md.append("") # Objectives md.append(f"## Available Objectives\n") for key, obj in options['available_objectives'].items(): md.append(f"### `{key}`") md.append(f"- **Description**: {obj['description']}") md.append(f"- **Metric**: {obj['metric']} ({obj['units']})") md.append(f"- **Default Weight**: {obj['typical_weight']}") md.append(f"- **Extractor**: `{obj['extractor']}`") md.append("") # Constraints md.append(f"## Available Constraints\n") for key, const in options['available_constraints'].items(): md.append(f"### `{key}`") md.append(f"- **Description**: {const['description']}") md.append(f"- **Metric**: {const['metric']} ({const['units']})") md.append(f"- **Typical Value**: {const['typical_value']}") md.append(f"- **Type**: {const['constraint_type']}") md.append(f"- **Extractor**: `{const['extractor']}`") md.append("") return "\n".join(md) # For testing if __name__ == "__main__": import sys if len(sys.argv) < 2: print("Usage: python optimization_config.py ") sys.exit(1) sim_path = sys.argv[1] # Test 1: List options print("=" * 60) print("TEST 1: List Available Options") print("=" * 60) options = list_optimization_options(sim_path) print(format_optimization_options_for_llm(options)) # Test 2: Build configuration print("\n" + "=" * 60) print("TEST 2: Build Optimization Configuration") print("=" * 60) result = build_optimization_config( sim_file_path=sim_path, design_variables=[ {'name': 'tip_thickness', 'lower_bound': 15.0, 'upper_bound': 25.0}, {'name': 'support_angle', 'lower_bound': 20.0, 'upper_bound': 40.0}, ], objectives=[ {'objective_key': 'minimize_mass', 'weight': 5.0}, {'objective_key': 'minimize_max_stress', 'weight': 10.0} ], constraints=[ {'constraint_key': 'max_displacement_limit', 'limit_value': 1.0}, {'constraint_key': 'max_stress_limit', 'limit_value': 200.0} ], optimization_settings={ 'n_trials': 150, 'sampler': 'TPE' } ) if result['status'] == 'success': print(f"SUCCESS: Configuration saved to: {result['config_file']}") print(f"\nSummary:") for key, value in result['summary'].items(): print(f" - {key}: {value}") else: print(f"ERROR: {result['message']}") print(f"Suggestion: {result.get('suggestion', '')}")