"""GOAP Debug utilities for visualization and analysis. Provides detailed information about GOAP decision-making for debugging and visualization purposes. """ from dataclasses import dataclass, field from typing import Optional, TYPE_CHECKING from .world_state import WorldState, create_world_state from .goal import Goal from .action import GOAPAction from .planner import GOAPPlanner, ReactivePlanner, Plan from .goals import get_all_goals from .actions import get_all_actions if TYPE_CHECKING: from backend.domain.agent import Agent from backend.core.market import OrderBook @dataclass class GoalDebugInfo: """Debug information for a single goal.""" name: str goal_type: str priority: float is_satisfied: bool is_selected: bool = False def to_dict(self) -> dict: return { "name": self.name, "goal_type": self.goal_type, "priority": round(self.priority, 2), "is_satisfied": self.is_satisfied, "is_selected": self.is_selected, } @dataclass class ActionDebugInfo: """Debug information for a single action.""" name: str action_type: str target_resource: Optional[str] is_valid: bool cost: float is_in_plan: bool = False plan_order: int = -1 def to_dict(self) -> dict: return { "name": self.name, "action_type": self.action_type, "target_resource": self.target_resource, "is_valid": self.is_valid, "cost": round(self.cost, 2), "is_in_plan": self.is_in_plan, "plan_order": self.plan_order, } @dataclass class PlanDebugInfo: """Debug information for the current plan.""" goal_name: str actions: list[str] total_cost: float plan_length: int def to_dict(self) -> dict: return { "goal_name": self.goal_name, "actions": self.actions, "total_cost": round(self.total_cost, 2), "plan_length": self.plan_length, } @dataclass class GOAPDebugInfo: """Complete GOAP debug information for an agent.""" agent_id: str agent_name: str world_state: dict goals: list[GoalDebugInfo] actions: list[ActionDebugInfo] current_plan: Optional[PlanDebugInfo] selected_action: Optional[str] decision_reason: str def to_dict(self) -> dict: return { "agent_id": self.agent_id, "agent_name": self.agent_name, "world_state": self.world_state, "goals": [g.to_dict() for g in self.goals], "actions": [a.to_dict() for a in self.actions], "current_plan": self.current_plan.to_dict() if self.current_plan else None, "selected_action": self.selected_action, "decision_reason": self.decision_reason, } def get_goap_debug_info( agent: "Agent", market: "OrderBook", step_in_day: int = 1, day_steps: int = 10, is_night: bool = False, ) -> GOAPDebugInfo: """Get detailed GOAP debug information for an agent. This function performs the same planning as the actual AI, but captures detailed information about the decision process. """ # Create world state state = create_world_state( agent=agent, market=market, step_in_day=step_in_day, day_steps=day_steps, is_night=is_night, ) # Get goals and actions all_goals = get_all_goals() all_actions = get_all_actions() # Evaluate all goals goal_infos = [] selected_goal = None selected_plan = None # Sort by priority goals_with_priority = [] for goal in all_goals: priority = goal.priority(state) satisfied = goal.satisfied(state) goals_with_priority.append((goal, priority, satisfied)) goals_with_priority.sort(key=lambda x: x[1], reverse=True) # Try planning for each goal planner = GOAPPlanner(max_iterations=50) for goal, priority, satisfied in goals_with_priority: if priority > 0 and not satisfied: plan = planner.plan(state, goal, all_actions) if plan and not plan.is_empty: selected_goal = goal selected_plan = plan break # Build goal debug info for goal, priority, satisfied in goals_with_priority: info = GoalDebugInfo( name=goal.name, goal_type=goal.goal_type.value, priority=priority, is_satisfied=satisfied, is_selected=(goal == selected_goal), ) goal_infos.append(info) # Build action debug info action_infos = [] plan_action_names = [] if selected_plan: plan_action_names = [a.name for a in selected_plan.actions] for action in all_actions: is_valid = action.is_valid(state) cost = action.get_cost(state) if is_valid else float('inf') in_plan = action.name in plan_action_names order = plan_action_names.index(action.name) if in_plan else -1 info = ActionDebugInfo( name=action.name, action_type=action.action_type.value, target_resource=action.target_resource.value if action.target_resource else None, is_valid=is_valid, cost=cost if cost != float('inf') else -1, is_in_plan=in_plan, plan_order=order, ) action_infos.append(info) # Sort actions: plan actions first (by order), then valid actions, then invalid action_infos.sort(key=lambda a: ( 0 if a.is_in_plan else 1, a.plan_order if a.is_in_plan else 999, 0 if a.is_valid else 1, a.cost if a.cost >= 0 else 9999, )) # Build plan debug info plan_info = None if selected_plan: plan_info = PlanDebugInfo( goal_name=selected_plan.goal.name, actions=[a.name for a in selected_plan.actions], total_cost=selected_plan.total_cost, plan_length=len(selected_plan.actions), ) # Determine selected action and reason selected_action = None reason = "No plan found" if is_night: selected_action = "Sleep" reason = "Night time: sleeping" elif selected_plan and selected_plan.first_action: selected_action = selected_plan.first_action.name reason = f"{selected_plan.goal.name}: {selected_action}" else: # Fallback to reactive planning reactive_planner = ReactivePlanner() best_action = reactive_planner.select_best_action(state, all_goals, all_actions) if best_action: selected_action = best_action.name reason = f"Reactive: {best_action.name}" # Mark the reactive action in the action list for action_info in action_infos: if action_info.name == best_action.name: action_info.is_in_plan = True action_info.plan_order = 0 return GOAPDebugInfo( agent_id=agent.id, agent_name=agent.name, world_state=state.to_dict(), goals=goal_infos, actions=action_infos, current_plan=plan_info, selected_action=selected_action, decision_reason=reason, ) def get_all_agents_goap_debug( agents: list["Agent"], market: "OrderBook", step_in_day: int = 1, day_steps: int = 10, is_night: bool = False, ) -> list[GOAPDebugInfo]: """Get GOAP debug info for all agents.""" return [ get_goap_debug_info(agent, market, step_in_day, day_steps, is_night) for agent in agents if agent.is_alive() ]