259 lines
7.5 KiB
Python
259 lines
7.5 KiB
Python
"""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()
|
|
]
|
|
|