villsim/backend/core/bdi/intention.py

290 lines
10 KiB
Python

"""Intention System for BDI agents.
Intentions represent committed plans that the agent is executing.
Unlike desires (motivations) or goals (targets), intentions are
concrete action sequences the agent has decided to pursue.
Key concepts:
- Intention persistence: agents stick to plans unless interrupted
- Commitment strategies: different levels of plan commitment
- Plan monitoring: detecting when a plan becomes invalid
"""
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from backend.core.bdi.belief import BeliefBase
from backend.core.bdi.desire import DesireManager
from backend.core.goap.goal import Goal
from backend.core.goap.action import GOAPAction
from backend.core.goap.planner import Plan
class CommitmentStrategy(Enum):
"""How strongly an agent commits to their current intention."""
REACTIVE = "reactive" # Replan every turn (no commitment)
CAUTIOUS = "cautious" # Replan if priorities shift significantly
DETERMINED = "determined" # Stick to plan unless it becomes impossible
STUBBORN = "stubborn" # Only abandon for critical interrupts
@dataclass
class Intention:
"""A committed plan that the agent is executing.
Tracks the goal, plan, and execution progress.
"""
goal: "Goal" # The goal we're pursuing
plan: "Plan" # The plan to achieve it
start_turn: int # When we committed
actions_completed: int = 0 # How many actions done
last_action_success: bool = True # Did last action succeed?
consecutive_failures: int = 0 # Failures in a row
@property
def current_action(self) -> Optional["GOAPAction"]:
"""Get the next action to execute."""
if self.plan.is_empty:
return None
remaining = self.plan.actions[self.actions_completed:]
return remaining[0] if remaining else None
@property
def is_complete(self) -> bool:
"""Check if the plan has been fully executed."""
return self.actions_completed >= len(self.plan.actions)
@property
def remaining_actions(self) -> int:
"""Number of actions left in the plan."""
return max(0, len(self.plan.actions) - self.actions_completed)
def advance(self, success: bool) -> None:
"""Mark current action as executed and advance."""
self.last_action_success = success
if success:
self.actions_completed += 1
self.consecutive_failures = 0
else:
self.consecutive_failures += 1
class IntentionManager:
"""Manages the agent's current intention and commitment.
Responsibilities:
- Maintain the current intention (goal + plan)
- Decide when to continue vs. replan
- Handle plan failure and recovery
"""
def __init__(self, commitment_strategy: CommitmentStrategy = CommitmentStrategy.CAUTIOUS):
self.current_intention: Optional[Intention] = None
self.commitment_strategy = commitment_strategy
# Tracking
self.intentions_completed: int = 0
self.intentions_abandoned: int = 0
self.total_actions_executed: int = 0
# Thresholds for reconsideration
self.max_consecutive_failures: int = 2
self.priority_switch_threshold: float = 1.5 # New goal must be 1.5x priority
@classmethod
def from_personality(cls, personality) -> "IntentionManager":
"""Create an IntentionManager with commitment strategy based on personality."""
# Derive commitment from personality traits
# High hoarding + low risk tolerance = stubborn
commitment_score = (
personality.hoarding_rate * 0.4 +
(1.0 - personality.risk_tolerance) * 0.4 +
(1.0 - personality.market_affinity) * 0.2
)
if commitment_score > 0.7:
strategy = CommitmentStrategy.STUBBORN
elif commitment_score > 0.5:
strategy = CommitmentStrategy.DETERMINED
elif commitment_score > 0.3:
strategy = CommitmentStrategy.CAUTIOUS
else:
strategy = CommitmentStrategy.REACTIVE
return cls(commitment_strategy=strategy)
def has_intention(self) -> bool:
"""Check if we have an active intention."""
return (
self.current_intention is not None and
not self.current_intention.is_complete
)
def get_next_action(self) -> Optional["GOAPAction"]:
"""Get the next action from current intention."""
if not self.has_intention():
return None
return self.current_intention.current_action
def should_reconsider(
self,
beliefs: "BeliefBase",
desire_manager: "DesireManager",
available_goals: list["Goal"],
) -> bool:
"""Determine if we should reconsider our current intention.
This implements the commitment strategy logic.
"""
# No intention = definitely need to plan
if not self.has_intention():
return True
intention = self.current_intention
# Check for critical interrupts (always reconsider)
if beliefs.has_critical_need():
# But only if current intention isn't already addressing it
urgent_need = beliefs.get_most_urgent_need()
if not self._intention_addresses_need(intention, urgent_need):
return True
# Check for too many failures
if intention.consecutive_failures >= self.max_consecutive_failures:
return True
# Strategy-specific logic
if self.commitment_strategy == CommitmentStrategy.REACTIVE:
return True # Always replan
elif self.commitment_strategy == CommitmentStrategy.CAUTIOUS:
# Reconsider if a significantly better goal is available
return self._better_goal_available(
beliefs, desire_manager, available_goals,
threshold=self.priority_switch_threshold
)
elif self.commitment_strategy == CommitmentStrategy.DETERMINED:
# Only reconsider for much better goals or if plan is failing
return (
intention.consecutive_failures > 0 or
self._better_goal_available(
beliefs, desire_manager, available_goals,
threshold=2.0 # Need 2x priority to switch
)
)
elif self.commitment_strategy == CommitmentStrategy.STUBBORN:
# Only abandon for critical needs or impossible plans
return (
beliefs.has_critical_need() or
intention.consecutive_failures >= self.max_consecutive_failures
)
return False
def _intention_addresses_need(self, intention: Intention, need: str) -> bool:
"""Check if current intention addresses a vital need."""
if not intention or not need:
return False
goal_name = intention.goal.name.lower()
need_map = {
"thirst": "thirst",
"hunger": "hunger",
"heat": "heat",
"energy": "energy",
}
return need_map.get(need, "") in goal_name
def _better_goal_available(
self,
beliefs: "BeliefBase",
desire_manager: "DesireManager",
available_goals: list["Goal"],
threshold: float,
) -> bool:
"""Check if there's a significantly better goal available."""
if not self.has_intention():
return True
current_goal = self.current_intention.goal
world_state = beliefs.to_world_state()
current_priority = current_goal.get_priority(world_state)
# Get desire-filtered goals
filtered_goals = desire_manager.filter_goals_by_desire(available_goals, beliefs)
for goal in filtered_goals:
if goal.name == current_goal.name:
continue
goal_priority = goal.get_priority(world_state)
if goal_priority > current_priority * threshold:
return True
return False
def commit_to_plan(self, goal: "Goal", plan: "Plan", current_turn: int) -> None:
"""Commit to a new intention."""
# Track abandoned intention
if self.has_intention():
self.intentions_abandoned += 1
self.current_intention = Intention(
goal=goal,
plan=plan,
start_turn=current_turn,
)
def advance_intention(self, success: bool) -> None:
"""Record action execution and advance the intention."""
if not self.has_intention():
return
self.current_intention.advance(success)
self.total_actions_executed += 1
# Check if intention is complete
if self.current_intention.is_complete:
self.intentions_completed += 1
self.current_intention = None
def abandon_intention(self, reason: str = "unknown") -> None:
"""Abandon the current intention."""
if self.has_intention():
self.intentions_abandoned += 1
self.current_intention = None
def get_plan_progress(self) -> dict:
"""Get current plan execution progress."""
if not self.has_intention():
return {"has_intention": False}
intention = self.current_intention
return {
"has_intention": True,
"goal": intention.goal.name,
"actions_completed": intention.actions_completed,
"remaining_actions": intention.remaining_actions,
"consecutive_failures": intention.consecutive_failures,
"turns_active": 0, # Would need current_turn to calculate
}
def to_dict(self) -> dict:
"""Convert to dictionary for debugging/logging."""
return {
"commitment_strategy": self.commitment_strategy.value,
"has_intention": self.has_intention(),
"current_goal": self.current_intention.goal.name if self.has_intention() else None,
"plan_progress": self.get_plan_progress(),
"stats": {
"intentions_completed": self.intentions_completed,
"intentions_abandoned": self.intentions_abandoned,
"total_actions": self.total_actions_executed,
},
}