Files
timmy-config/allegro/goap/executor.py
2026-03-31 20:02:01 +00:00

662 lines
23 KiB
Python
Executable File

#!/usr/bin/env python3
"""
GOAP Executor Module - Allegro-Primus Child Autonomy System
Executes action plans and manages plan lifecycle.
"""
import asyncio
import time
import json
from typing import Dict, List, Optional, Any, Callable
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from datetime import datetime
from actions import Action, ActionResult, ActionStatus
from planner import Plan, PlanStatus, GOAPPlanner
from goals import Goal
class ExecutionMode(Enum):
"""Execution modes"""
SEQUENTIAL = auto() # Execute actions one by one
PARALLEL = auto() # Execute independent actions in parallel
ADAPTIVE = auto() # Adapt based on context
@dataclass
class ExecutionContext:
"""Context for action execution"""
world_state: Dict[str, Any]
user_context: Dict[str, Any] = field(default_factory=dict)
system_context: Dict[str, Any] = field(default_factory=dict)
execution_history: List[Dict] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def copy(self) -> 'ExecutionContext':
"""Create a copy of the context"""
return ExecutionContext(
world_state=self.world_state.copy(),
user_context=self.user_context.copy(),
system_context=self.system_context.copy(),
execution_history=self.execution_history.copy(),
metadata=self.metadata.copy()
)
@dataclass
class ExecutionReport:
"""Report of plan execution"""
plan: Plan
success: bool
actions_completed: int
actions_failed: int
execution_time: float
start_time: float
end_time: float
final_state: Dict[str, Any]
error: Optional[str] = None
action_reports: List[Dict] = field(default_factory=list)
def to_dict(self) -> Dict:
return {
'plan': self.plan.to_dict(),
'success': self.success,
'actions_completed': self.actions_completed,
'actions_failed': self.actions_failed,
'execution_time': self.execution_time,
'start_time': self.start_time,
'end_time': self.end_time,
'final_state': self.final_state,
'error': self.error,
'action_reports': self.action_reports
}
class PlanExecutor:
"""Executes plans and manages their lifecycle"""
def __init__(
self,
mode: ExecutionMode = ExecutionMode.SEQUENTIAL,
max_retries: int = 2,
retry_delay: float = 1.0,
state_persistence_path: Optional[str] = None
):
self.mode = mode
self.max_retries = max_retries
self.retry_delay = retry_delay
self.state_persistence_path = state_persistence_path or "/root/allegro/goap/executor_state.json"
# Execution tracking
self.current_plan: Optional[Plan] = None
self.current_action_index: int = 0
self.is_running: bool = False
self.should_cancel: bool = False
# Statistics
self.total_plans_executed = 0
self.total_plans_succeeded = 0
self.total_actions_executed = 0
self.total_actions_succeeded = 0
# Callbacks
self.on_action_start: Optional[Callable[[Action, int], None]] = None
self.on_action_complete: Optional[Callable[[Action, ActionResult, int], None]] = None
self.on_plan_complete: Optional[Callable[[ExecutionReport], None]] = None
# Load persisted state
self._load_state()
def _load_state(self):
"""Load persisted executor state"""
import os
if os.path.exists(self.state_persistence_path):
try:
with open(self.state_persistence_path, 'r') as f:
data = json.load(f)
self.total_plans_executed = data.get('total_plans_executed', 0)
self.total_plans_succeeded = data.get('total_plans_succeeded', 0)
self.total_actions_executed = data.get('total_actions_executed', 0)
self.total_actions_succeeded = data.get('total_actions_succeeded', 0)
except Exception as e:
print(f"[Executor] Failed to load state: {e}")
def _save_state(self):
"""Persist executor state"""
try:
Path(self.state_persistence_path).parent.mkdir(parents=True, exist_ok=True)
with open(self.state_persistence_path, 'w') as f:
json.dump({
'total_plans_executed': self.total_plans_executed,
'total_plans_succeeded': self.total_plans_succeeded,
'total_actions_executed': self.total_actions_executed,
'total_actions_succeeded': self.total_actions_succeeded,
'last_updated': time.time()
}, f, indent=2)
except Exception as e:
print(f"[Executor] Failed to save state: {e}")
async def execute_plan(
self,
plan: Plan,
initial_context: ExecutionContext,
replan_on_failure: bool = True,
planner: Optional[GOAPPlanner] = None
) -> ExecutionReport:
"""
Execute a plan with the given context.
Args:
plan: The plan to execute
initial_context: Initial execution context
replan_on_failure: Whether to replan if actions fail
planner: Planner to use for replanning
Returns:
ExecutionReport with results
"""
self.current_plan = plan
self.current_action_index = 0
self.is_running = True
self.should_cancel = False
start_time = time.time()
plan.status = PlanStatus.IN_PROGRESS
plan.started_at = start_time
context = initial_context.copy()
action_reports = []
actions_completed = 0
actions_failed = 0
try:
# Execute actions based on mode
if self.mode == ExecutionMode.SEQUENTIAL:
result = await self._execute_sequential(
plan, context, replan_on_failure, planner
)
elif self.mode == ExecutionMode.PARALLEL:
result = await self._execute_parallel(plan, context)
else: # ADAPTIVE
result = await self._execute_adaptive(
plan, context, replan_on_failure, planner
)
# Update plan status
if result['success']:
plan.status = PlanStatus.COMPLETED
self.total_plans_succeeded += 1
else:
plan.status = PlanStatus.FAILED
self.total_plans_executed += 1
end_time = time.time()
plan.completed_at = end_time
report = ExecutionReport(
plan=plan,
success=result['success'],
actions_completed=result['completed'],
actions_failed=result['failed'],
execution_time=end_time - start_time,
start_time=start_time,
end_time=end_time,
final_state=context.world_state,
error=result.get('error'),
action_reports=result.get('reports', [])
)
# Trigger callback
if self.on_plan_complete:
self.on_plan_complete(report)
self._save_state()
return report
except Exception as e:
plan.status = PlanStatus.FAILED
end_time = time.time()
plan.completed_at = end_time
return ExecutionReport(
plan=plan,
success=False,
actions_completed=actions_completed,
actions_failed=actions_failed + 1,
execution_time=end_time - start_time,
start_time=start_time,
end_time=end_time,
final_state=context.world_state,
error=str(e),
action_reports=action_reports
)
finally:
self.is_running = False
self.current_plan = None
async def _execute_sequential(
self,
plan: Plan,
context: ExecutionContext,
replan_on_failure: bool,
planner: Optional[GOAPPlanner]
) -> Dict:
"""Execute actions sequentially"""
completed = 0
failed = 0
reports = []
for i, action in enumerate(plan.actions):
if self.should_cancel:
return {
'success': False,
'completed': completed,
'failed': failed,
'error': 'Execution cancelled',
'reports': reports
}
self.current_action_index = i
# Trigger start callback
if self.on_action_start:
self.on_action_start(action, i)
# Execute action with retries
result = await self._execute_action_with_retry(action, context)
# Update context
context.execution_history.append({
'action': action.name,
'result': result.success,
'time': time.time()
})
# Trigger complete callback
if self.on_action_complete:
self.on_action_complete(action, result, i)
reports.append({
'action': action.name,
'success': result.success,
'message': result.message,
'execution_time': result.execution_time
})
if result.success:
completed += 1
# Update world state with effects
context.world_state = action.apply_effects(context.world_state)
self.total_actions_succeeded += 1
else:
failed += 1
# Try to replan if enabled
if replan_on_failure and planner:
new_plan = planner.replan(plan, context.world_state, i)
if new_plan and new_plan.actions:
# Replace remaining actions
plan.actions = plan.actions[:i+1] + new_plan.actions
# Continue with next action
continue
else:
# Replanning failed
return {
'success': False,
'completed': completed,
'failed': failed,
'error': f"Action {action.name} failed and replanning unsuccessful",
'reports': reports
}
else:
# No replanning, fail the plan
return {
'success': False,
'completed': completed,
'failed': failed,
'error': f"Action {action.name} failed: {result.error}",
'reports': reports
}
self.total_actions_executed += 1
return {
'success': failed == 0,
'completed': completed,
'failed': failed,
'reports': reports
}
async def _execute_parallel(
self,
plan: Plan,
context: ExecutionContext
) -> Dict:
"""Execute independent actions in parallel"""
# For now, execute sequentially (parallel execution is complex)
# TODO: Implement proper parallel execution with dependency tracking
return await self._execute_sequential(plan, context, False, None)
async def _execute_adaptive(
self,
plan: Plan,
context: ExecutionContext,
replan_on_failure: bool,
planner: Optional[GOAPPlanner]
) -> Dict:
"""
Execute adaptively based on context.
Adjusts execution strategy based on system load, urgency, etc.
"""
# Check system load
cpu_percent = context.world_state.get('system', {}).get('cpu_percent', 50)
if cpu_percent > 80:
# High load - slow down and prioritize
await asyncio.sleep(1)
# Check urgency
urgency = plan.goal.state.urgency if plan.goal else 0
if urgency > 0.8:
# High urgency - skip non-essential actions
essential_actions = [
a for a in plan.actions
if a.category in ['system', 'safety']
]
plan.actions = essential_actions
return await self._execute_sequential(plan, context, replan_on_failure, planner)
async def _execute_action_with_retry(
self,
action: Action,
context: ExecutionContext
) -> ActionResult:
"""Execute an action with retry logic"""
last_result = None
for attempt in range(self.max_retries + 1):
try:
# Prepare action context
action_context = {
'world_state': context.world_state,
'user_context': context.user_context,
'system_context': context.system_context,
'attempt': attempt,
**context.metadata
}
# Execute action
result = await action.execute(action_context)
if result.success:
action.success_count += 1
return result
last_result = result
# Retry if not last attempt
if attempt < self.max_retries:
await asyncio.sleep(self.retry_delay * (attempt + 1))
except Exception as e:
last_result = ActionResult.failure_result(
error=str(e),
message=f"Exception during action execution: {action.name}"
)
if attempt < self.max_retries:
await asyncio.sleep(self.retry_delay * (attempt + 1))
action.execution_count += 1
return last_result or ActionResult.failure_result(
error="All retry attempts failed"
)
def cancel(self):
"""Cancel current execution"""
self.should_cancel = True
def get_status(self) -> Dict:
"""Get current execution status"""
if not self.is_running or not self.current_plan:
return {
'running': False,
'plan': None,
'progress': 0
}
total = len(self.current_plan.actions)
progress = (self.current_action_index / total * 100) if total > 0 else 0
return {
'running': True,
'plan': self.current_plan.to_dict(),
'current_action': self.current_action_index,
'total_actions': total,
'progress': progress,
'current_action_name': (
self.current_plan.actions[self.current_action_index].name
if self.current_action_index < total else None
)
}
def get_stats(self) -> Dict:
"""Get execution statistics"""
return {
'total_plans_executed': self.total_plans_executed,
'total_plans_succeeded': self.total_plans_succeeded,
'plan_success_rate': self.total_plans_succeeded / max(1, self.total_plans_executed),
'total_actions_executed': self.total_actions_executed,
'total_actions_succeeded': self.total_actions_succeeded,
'action_success_rate': self.total_actions_succeeded / max(1, self.total_actions_executed),
'mode': self.mode.name
}
class ExecutionScheduler:
"""Schedules plan execution based on priorities and constraints"""
def __init__(self, executor: PlanExecutor):
self.executor = executor
self.queue: List[Tuple[float, Plan, ExecutionContext]] = []
self.running: bool = False
self._task: Optional[asyncio.Task] = None
def schedule(
self,
plan: Plan,
context: ExecutionContext,
priority: float = 1.0
):
"""Schedule a plan for execution"""
# Adjust priority by goal urgency
if plan.goal:
priority += plan.goal.state.urgency * 10
priority += plan.goal.state.effective_priority / 100
self.queue.append((priority, plan, context))
self.queue.sort(key=lambda x: -x[0]) # Highest priority first
async def start(self):
"""Start the scheduler"""
self.running = True
self._task = asyncio.create_task(self._run_loop())
async def stop(self):
"""Stop the scheduler"""
self.running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
async def _run_loop(self):
"""Main scheduler loop"""
while self.running:
if self.queue and not self.executor.is_running:
priority, plan, context = self.queue.pop(0)
# Execute plan
try:
report = await self.executor.execute_plan(plan, context)
# Log completion
print(f"[Scheduler] Plan completed: {plan.goal.name if plan.goal else 'unknown'}")
print(f" Success: {report.success}")
print(f" Actions: {report.actions_completed}/{report.actions_completed + report.actions_failed}")
except Exception as e:
print(f"[Scheduler] Plan execution error: {e}")
await asyncio.sleep(0.1)
def get_queue_status(self) -> Dict:
"""Get current queue status"""
return {
'queued_plans': len(self.queue),
'queue': [
{
'priority': p,
'goal': plan.goal.name if plan.goal else 'unknown',
'actions': len(plan.actions)
}
for p, plan, _ in self.queue
]
}
class ExecutionMonitor:
"""Monitors plan execution and system health"""
def __init__(self, executor: PlanExecutor):
self.executor = executor
self.reports: List[ExecutionReport] = []
self.max_reports = 100
# Set up callbacks
executor.on_action_start = self._on_action_start
executor.on_action_complete = self._on_action_complete
executor.on_plan_complete = self._on_plan_complete
def _on_action_start(self, action: Action, index: int):
"""Called when an action starts"""
print(f"[Monitor] Action {index + 1}: {action.name} - STARTED")
def _on_action_complete(self, action: Action, result: ActionResult, index: int):
"""Called when an action completes"""
status = "SUCCESS" if result.success else "FAILED"
print(f"[Monitor] Action {index + 1}: {action.name} - {status}")
if not result.success:
print(f" Error: {result.error}")
def _on_plan_complete(self, report: ExecutionReport):
"""Called when a plan completes"""
self.reports.append(report)
if len(self.reports) > self.max_reports:
self.reports.pop(0)
# Log summary
print(f"[Monitor] Plan execution complete")
print(f" Goal: {report.plan.goal.name if report.plan.goal else 'unknown'}")
print(f" Success: {report.success}")
print(f" Time: {report.execution_time:.2f}s")
print(f" Actions: {report.actions_completed} completed, {report.actions_failed} failed")
def get_recent_reports(self, count: int = 10) -> List[ExecutionReport]:
"""Get recent execution reports"""
return self.reports[-count:]
def get_summary(self) -> Dict:
"""Get execution summary"""
if not self.reports:
return {'total_reports': 0}
total_success = sum(1 for r in self.reports if r.success)
total_time = sum(r.execution_time for r in self.reports)
total_actions = sum(r.actions_completed for r in self.reports)
return {
'total_reports': len(self.reports),
'success_rate': total_success / len(self.reports),
'average_execution_time': total_time / len(self.reports),
'total_actions_executed': total_actions,
'recent_failures': [
{
'goal': r.plan.goal.name if r.plan.goal else 'unknown',
'error': r.error,
'time': r.end_time
}
for r in self.reports[-10:] if not r.success
]
}
# Singleton executor
executor = PlanExecutor()
if __name__ == "__main__":
# Test the executor
print("=== GOAP Executor Module Test ===")
from goals import SystemHealthGoal
from actions import CheckSystemHealth, CleanupResources
from planner import GOAPPlanner
async def test_execution():
# Create test components
goal = SystemHealthGoal()
world_state = {
'system': {
'cpu_percent': 45,
'memory_percent': 60,
'disk_percent': 85,
'uptime_hours': 48,
'recent_errors': 2,
'health_checked': False
}
}
# Create plan
planner = GOAPPlanner()
plan = planner.plan(goal, world_state)
if not plan:
print("No plan found")
return
print(f"\nPlan created: {len(plan.actions)} actions")
# Create executor and monitor
exec = PlanExecutor()
monitor = ExecutionMonitor(exec)
# Create context
context = ExecutionContext(
world_state=world_state,
user_context={'user_id': 'test_user'},
system_context={'test_mode': True}
)
# Execute plan
print("\n=== Executing Plan ===")
report = await exec.execute_plan(plan, context, replan_on_failure=False)
print("\n=== Execution Report ===")
print(f"Success: {report.success}")
print(f"Actions completed: {report.actions_completed}")
print(f"Actions failed: {report.actions_failed}")
print(f"Execution time: {report.execution_time:.2f}s")
print("\n=== Executor Stats ===")
print(json.dumps(exec.get_stats(), indent=2))
# Run test
asyncio.run(test_execution())