424 lines
14 KiB
Python
424 lines
14 KiB
Python
|
|
"""
|
||
|
|
LLM-Powered Workflow Analyzer - Phase 2.7
|
||
|
|
|
||
|
|
Uses Claude (LLM) to intelligently analyze user requests instead of dumb regex patterns.
|
||
|
|
This is what we should have built from the start!
|
||
|
|
|
||
|
|
Integration modes:
|
||
|
|
1. Claude Code Skill (preferred for development) - uses Claude Code's built-in AI
|
||
|
|
2. Anthropic API (fallback for standalone) - requires API key
|
||
|
|
|
||
|
|
Author: Atomizer Development Team
|
||
|
|
Version: 0.2.0 (Phase 2.7)
|
||
|
|
Last Updated: 2025-01-16
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
import tempfile
|
||
|
|
from typing import List, Dict, Any, Optional
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
try:
|
||
|
|
from anthropic import Anthropic
|
||
|
|
HAS_ANTHROPIC = True
|
||
|
|
except ImportError:
|
||
|
|
HAS_ANTHROPIC = False
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class WorkflowStep:
|
||
|
|
"""A single step in an optimization workflow."""
|
||
|
|
action: str
|
||
|
|
domain: str
|
||
|
|
params: Dict[str, Any]
|
||
|
|
step_type: str # 'engineering_feature', 'inline_calculation', 'post_processing_hook'
|
||
|
|
priority: int = 0
|
||
|
|
|
||
|
|
|
||
|
|
class LLMWorkflowAnalyzer:
|
||
|
|
"""
|
||
|
|
Uses Claude LLM to intelligently analyze optimization requests.
|
||
|
|
NO MORE DUMB REGEX PATTERNS!
|
||
|
|
|
||
|
|
Integration modes:
|
||
|
|
1. Claude Code integration (use_claude_code=True) - preferred for development
|
||
|
|
2. Direct API (api_key provided) - for standalone execution
|
||
|
|
3. Fallback heuristics (neither provided) - basic pattern matching
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, api_key: Optional[str] = None, use_claude_code: bool = True):
|
||
|
|
"""
|
||
|
|
Initialize LLM analyzer.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
api_key: Anthropic API key (optional, for standalone mode)
|
||
|
|
use_claude_code: Use Claude Code skill for analysis (default: True)
|
||
|
|
"""
|
||
|
|
self.use_claude_code = use_claude_code
|
||
|
|
self.client = None
|
||
|
|
|
||
|
|
if api_key and HAS_ANTHROPIC:
|
||
|
|
self.client = Anthropic(api_key=api_key)
|
||
|
|
self.use_claude_code = False # Prefer direct API if key provided
|
||
|
|
|
||
|
|
def analyze_request(self, user_request: str) -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Use Claude to analyze the request and extract workflow steps intelligently.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
{
|
||
|
|
'engineering_features': [...],
|
||
|
|
'inline_calculations': [...],
|
||
|
|
'post_processing_hooks': [...],
|
||
|
|
'optimization': {...}
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
|
||
|
|
prompt = f"""You are analyzing a structural optimization request for the Atomizer system.
|
||
|
|
|
||
|
|
USER REQUEST:
|
||
|
|
{user_request}
|
||
|
|
|
||
|
|
Your task: Break this down into atomic workflow steps and classify each step.
|
||
|
|
|
||
|
|
STEP TYPES:
|
||
|
|
1. ENGINEERING FEATURES - Complex FEA/CAE operations needing specialized knowledge:
|
||
|
|
- Extract results from OP2 files (displacement, stress, strain, element forces, etc.)
|
||
|
|
- Modify FEA properties (CBUSH/CBAR stiffness, PCOMP layup, material properties)
|
||
|
|
- Run simulations (SOL101, SOL103, etc.)
|
||
|
|
- Create/modify geometry in NX
|
||
|
|
|
||
|
|
2. INLINE CALCULATIONS - Simple math operations (auto-generate Python):
|
||
|
|
- Calculate average, min, max, sum
|
||
|
|
- Compare values, compute ratios
|
||
|
|
- Statistical operations
|
||
|
|
|
||
|
|
3. POST-PROCESSING HOOKS - Custom calculations between FEA steps:
|
||
|
|
- Custom objective functions combining multiple results
|
||
|
|
- Data transformations
|
||
|
|
- Filtering/aggregation logic
|
||
|
|
|
||
|
|
4. OPTIMIZATION - Algorithm and configuration:
|
||
|
|
- Optuna, genetic algorithm, etc.
|
||
|
|
- Design variables and their ranges
|
||
|
|
- Multi-objective vs single objective
|
||
|
|
|
||
|
|
IMPORTANT DISTINCTIONS:
|
||
|
|
- "extract forces from 1D elements" → ENGINEERING FEATURE (needs pyNastran/OP2 knowledge)
|
||
|
|
- "find average of forces" → INLINE CALCULATION (simple Python: sum/len)
|
||
|
|
- "compare max to average and create metric" → POST-PROCESSING HOOK (custom logic)
|
||
|
|
- Element forces vs Reaction forces are DIFFERENT (element internal forces vs nodal reactions)
|
||
|
|
- CBUSH vs CBAR are different element types with different properties
|
||
|
|
|
||
|
|
Return a JSON object with this EXACT structure:
|
||
|
|
{{
|
||
|
|
"engineering_features": [
|
||
|
|
{{
|
||
|
|
"action": "extract_1d_element_forces",
|
||
|
|
"domain": "result_extraction",
|
||
|
|
"description": "Extract element forces from 1D elements (CBAR/CBUSH) in Z direction",
|
||
|
|
"params": {{
|
||
|
|
"element_types": ["CBAR", "CBUSH"],
|
||
|
|
"result_type": "element_force",
|
||
|
|
"direction": "Z"
|
||
|
|
}}
|
||
|
|
}}
|
||
|
|
],
|
||
|
|
"inline_calculations": [
|
||
|
|
{{
|
||
|
|
"action": "calculate_average",
|
||
|
|
"description": "Calculate average of extracted forces",
|
||
|
|
"params": {{
|
||
|
|
"input": "forces_z",
|
||
|
|
"operation": "mean"
|
||
|
|
}}
|
||
|
|
}},
|
||
|
|
{{
|
||
|
|
"action": "find_minimum",
|
||
|
|
"description": "Find minimum force value",
|
||
|
|
"params": {{
|
||
|
|
"input": "forces_z",
|
||
|
|
"operation": "min"
|
||
|
|
}}
|
||
|
|
}}
|
||
|
|
],
|
||
|
|
"post_processing_hooks": [
|
||
|
|
{{
|
||
|
|
"action": "custom_objective_metric",
|
||
|
|
"description": "Compare minimum to average and create objective metric",
|
||
|
|
"params": {{
|
||
|
|
"inputs": ["min_force", "avg_force"],
|
||
|
|
"formula": "min_force / avg_force",
|
||
|
|
"objective": "minimize"
|
||
|
|
}}
|
||
|
|
}}
|
||
|
|
],
|
||
|
|
"optimization": {{
|
||
|
|
"algorithm": "genetic_algorithm",
|
||
|
|
"design_variables": [
|
||
|
|
{{
|
||
|
|
"parameter": "cbar_stiffness_x",
|
||
|
|
"type": "FEA_property",
|
||
|
|
"element_type": "CBAR"
|
||
|
|
}}
|
||
|
|
],
|
||
|
|
"objectives": [
|
||
|
|
{{
|
||
|
|
"type": "minimize",
|
||
|
|
"target": "custom_objective_metric"
|
||
|
|
}}
|
||
|
|
]
|
||
|
|
}}
|
||
|
|
}}
|
||
|
|
|
||
|
|
Analyze the request and return ONLY the JSON, no other text."""
|
||
|
|
|
||
|
|
if self.client:
|
||
|
|
# Use Claude API
|
||
|
|
response = self.client.messages.create(
|
||
|
|
model="claude-sonnet-4-20250514",
|
||
|
|
max_tokens=4000,
|
||
|
|
messages=[{
|
||
|
|
"role": "user",
|
||
|
|
"content": prompt
|
||
|
|
}]
|
||
|
|
)
|
||
|
|
|
||
|
|
# Extract JSON from response
|
||
|
|
content = response.content[0].text
|
||
|
|
|
||
|
|
# Find JSON in response
|
||
|
|
start = content.find('{')
|
||
|
|
end = content.rfind('}') + 1
|
||
|
|
json_str = content[start:end]
|
||
|
|
|
||
|
|
return json.loads(json_str)
|
||
|
|
else:
|
||
|
|
# Fallback: return a template showing expected format
|
||
|
|
return {
|
||
|
|
"engineering_features": [],
|
||
|
|
"inline_calculations": [],
|
||
|
|
"post_processing_hooks": [],
|
||
|
|
"optimization": {},
|
||
|
|
"error": "No API key provided - cannot analyze request"
|
||
|
|
}
|
||
|
|
|
||
|
|
def to_workflow_steps(self, analysis: Dict[str, Any]) -> List[WorkflowStep]:
|
||
|
|
"""Convert LLM analysis to WorkflowStep objects."""
|
||
|
|
steps = []
|
||
|
|
priority = 0
|
||
|
|
|
||
|
|
# Add engineering features
|
||
|
|
for feature in analysis.get('engineering_features', []):
|
||
|
|
steps.append(WorkflowStep(
|
||
|
|
action=feature['action'],
|
||
|
|
domain=feature['domain'],
|
||
|
|
params=feature.get('params', {}),
|
||
|
|
step_type='engineering_feature',
|
||
|
|
priority=priority
|
||
|
|
))
|
||
|
|
priority += 1
|
||
|
|
|
||
|
|
# Add inline calculations
|
||
|
|
for calc in analysis.get('inline_calculations', []):
|
||
|
|
steps.append(WorkflowStep(
|
||
|
|
action=calc['action'],
|
||
|
|
domain='calculation',
|
||
|
|
params=calc.get('params', {}),
|
||
|
|
step_type='inline_calculation',
|
||
|
|
priority=priority
|
||
|
|
))
|
||
|
|
priority += 1
|
||
|
|
|
||
|
|
# Add post-processing hooks
|
||
|
|
for hook in analysis.get('post_processing_hooks', []):
|
||
|
|
steps.append(WorkflowStep(
|
||
|
|
action=hook['action'],
|
||
|
|
domain='post_processing',
|
||
|
|
params=hook.get('params', {}),
|
||
|
|
step_type='post_processing_hook',
|
||
|
|
priority=priority
|
||
|
|
))
|
||
|
|
priority += 1
|
||
|
|
|
||
|
|
# Add optimization
|
||
|
|
opt = analysis.get('optimization', {})
|
||
|
|
if opt:
|
||
|
|
steps.append(WorkflowStep(
|
||
|
|
action='optimize',
|
||
|
|
domain='optimization',
|
||
|
|
params=opt,
|
||
|
|
step_type='engineering_feature',
|
||
|
|
priority=priority
|
||
|
|
))
|
||
|
|
|
||
|
|
return steps
|
||
|
|
|
||
|
|
def get_summary(self, analysis: Dict[str, Any]) -> str:
|
||
|
|
"""Generate human-readable summary of the analysis."""
|
||
|
|
lines = []
|
||
|
|
lines.append("LLM Workflow Analysis")
|
||
|
|
lines.append("=" * 80)
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Engineering features
|
||
|
|
eng_features = analysis.get('engineering_features', [])
|
||
|
|
lines.append(f"Engineering Features (Need Research): {len(eng_features)}")
|
||
|
|
for feature in eng_features:
|
||
|
|
lines.append(f" - {feature['action']}")
|
||
|
|
lines.append(f" Description: {feature.get('description', 'N/A')}")
|
||
|
|
lines.append(f" Domain: {feature['domain']}")
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Inline calculations
|
||
|
|
inline_calcs = analysis.get('inline_calculations', [])
|
||
|
|
lines.append(f"Inline Calculations (Auto-Generate): {len(inline_calcs)}")
|
||
|
|
for calc in inline_calcs:
|
||
|
|
lines.append(f" - {calc['action']}")
|
||
|
|
lines.append(f" Description: {calc.get('description', 'N/A')}")
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Post-processing hooks
|
||
|
|
hooks = analysis.get('post_processing_hooks', [])
|
||
|
|
lines.append(f"Post-Processing Hooks (Generate Middleware): {len(hooks)}")
|
||
|
|
for hook in hooks:
|
||
|
|
lines.append(f" - {hook['action']}")
|
||
|
|
lines.append(f" Description: {hook.get('description', 'N/A')}")
|
||
|
|
if 'formula' in hook.get('params', {}):
|
||
|
|
lines.append(f" Formula: {hook['params']['formula']}")
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Optimization
|
||
|
|
opt = analysis.get('optimization', {})
|
||
|
|
if opt:
|
||
|
|
lines.append("Optimization Configuration:")
|
||
|
|
lines.append(f" Algorithm: {opt.get('algorithm', 'N/A')}")
|
||
|
|
if 'design_variables' in opt:
|
||
|
|
lines.append(f" Design Variables: {len(opt['design_variables'])}")
|
||
|
|
for var in opt['design_variables']:
|
||
|
|
lines.append(f" - {var.get('parameter', 'N/A')} ({var.get('type', 'N/A')})")
|
||
|
|
if 'objectives' in opt:
|
||
|
|
lines.append(f" Objectives:")
|
||
|
|
for obj in opt['objectives']:
|
||
|
|
lines.append(f" - {obj.get('type', 'N/A')} {obj.get('target', 'N/A')}")
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Summary
|
||
|
|
total_steps = len(eng_features) + len(inline_calcs) + len(hooks) + (1 if opt else 0)
|
||
|
|
lines.append(f"Total Steps: {total_steps}")
|
||
|
|
lines.append(f" Engineering: {len(eng_features)} (need research/documentation)")
|
||
|
|
lines.append(f" Simple Math: {len(inline_calcs)} (auto-generate Python)")
|
||
|
|
lines.append(f" Hooks: {len(hooks)} (generate middleware)")
|
||
|
|
lines.append(f" Optimization: {1 if opt else 0}")
|
||
|
|
|
||
|
|
return "\n".join(lines)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Test the LLM workflow analyzer."""
|
||
|
|
import os
|
||
|
|
|
||
|
|
print("=" * 80)
|
||
|
|
print("LLM-Powered Workflow Analyzer Test")
|
||
|
|
print("=" * 80)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Test request
|
||
|
|
request = """I want to extract forces in direction Z of all the 1D elements and find the average of it,
|
||
|
|
then find the minimum value and compare it to the average, then assign it to a objective metric that needs to be minimized.
|
||
|
|
|
||
|
|
I want to iterate on the FEA properties of the Cbar element stiffness in X to make the objective function minimized.
|
||
|
|
|
||
|
|
I want to use genetic algorithm to iterate and optimize this"""
|
||
|
|
|
||
|
|
print("User Request:")
|
||
|
|
print(request)
|
||
|
|
print()
|
||
|
|
print("=" * 80)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Get API key from environment
|
||
|
|
api_key = os.environ.get('ANTHROPIC_API_KEY')
|
||
|
|
|
||
|
|
if not api_key:
|
||
|
|
print("WARNING: No ANTHROPIC_API_KEY found in environment")
|
||
|
|
print("Set it with: export ANTHROPIC_API_KEY=your_key_here")
|
||
|
|
print()
|
||
|
|
print("Showing expected output format instead...")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Show what the output should look like
|
||
|
|
expected = {
|
||
|
|
"engineering_features": [
|
||
|
|
{
|
||
|
|
"action": "extract_1d_element_forces",
|
||
|
|
"domain": "result_extraction",
|
||
|
|
"description": "Extract element forces from 1D elements in Z direction",
|
||
|
|
"params": {
|
||
|
|
"element_types": ["CBAR"],
|
||
|
|
"result_type": "element_force",
|
||
|
|
"direction": "Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"inline_calculations": [
|
||
|
|
{
|
||
|
|
"action": "calculate_average",
|
||
|
|
"description": "Calculate average of extracted forces",
|
||
|
|
"params": {"input": "forces_z", "operation": "mean"}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"action": "find_minimum",
|
||
|
|
"description": "Find minimum force value",
|
||
|
|
"params": {"input": "forces_z", "operation": "min"}
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"post_processing_hooks": [
|
||
|
|
{
|
||
|
|
"action": "custom_objective_metric",
|
||
|
|
"description": "Compare minimum to average",
|
||
|
|
"params": {
|
||
|
|
"inputs": ["min_force", "avg_force"],
|
||
|
|
"formula": "min_force / avg_force",
|
||
|
|
"objective": "minimize"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"optimization": {
|
||
|
|
"algorithm": "genetic_algorithm",
|
||
|
|
"design_variables": [
|
||
|
|
{"parameter": "cbar_stiffness_x", "type": "FEA_property"}
|
||
|
|
],
|
||
|
|
"objectives": [{"type": "minimize", "target": "custom_objective_metric"}]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
analyzer = LLMWorkflowAnalyzer()
|
||
|
|
print(analyzer.get_summary(expected))
|
||
|
|
return
|
||
|
|
|
||
|
|
# Use LLM to analyze
|
||
|
|
analyzer = LLMWorkflowAnalyzer(api_key=api_key)
|
||
|
|
|
||
|
|
print("Calling Claude to analyze request...")
|
||
|
|
print()
|
||
|
|
|
||
|
|
analysis = analyzer.analyze_request(request)
|
||
|
|
|
||
|
|
print("LLM Analysis Complete!")
|
||
|
|
print()
|
||
|
|
print(analyzer.get_summary(analysis))
|
||
|
|
|
||
|
|
print()
|
||
|
|
print("=" * 80)
|
||
|
|
print("Raw JSON Analysis:")
|
||
|
|
print("=" * 80)
|
||
|
|
print(json.dumps(analysis, indent=2))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|