Снесарев Максим 308f738c37 [new] add goap agents
2026-01-19 20:45:35 +03:00

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()
]