662 lines
23 KiB
Python
Executable File
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())
|