goapauto 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
goapauto/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ GOAP (Goal-Oriented Action Planning) implementation for AI agents.
3
+
4
+ This package provides a flexible framework for creating goal-driven AI agents
5
+ using the GOAP (Goal-Oriented Action Planning) architecture.
6
+
7
+ Key Components:
8
+ - Planner: The main planning engine that finds optimal action sequences
9
+ - Action: Base class for defining actions with preconditions and effects
10
+ - Actions: Collection of available actions
11
+ - Goal: Represents objectives that the agent wants to achieve
12
+ - WorldState: Tracks the current state of the world
13
+ - PlanResult: Type alias for the planning result (plan, status) tuple
14
+ - PlanStats: Statistics about the planning process
15
+ - Plan: Type alias for a list of action names
16
+
17
+ Example usage:
18
+ >>> from goapauto import Planner, Goal, WorldState, Actions
19
+ >>>
20
+ >>> # Define initial state
21
+ >>> world_state = WorldState({'has_key': False, 'door_open': False})
22
+ >>>
23
+ >>> # Create actions
24
+ >>> actions = Actions()
25
+ >>> actions.add_action(
26
+ ... name="pickup_key",
27
+ ... preconditions={'key_available': True},
28
+ ... effects={'has_key': True},
29
+ ... cost=1.0
30
+ ... )
31
+ >>>
32
+ >>> # Create goal
33
+ >>> goal = Goal("OpenDoor", 1, {'door_open': True})
34
+ >>>
35
+ >>> # Plan and execute
36
+ >>> planner = Planner(actions)
37
+ >>> plan, status = planner.generate_plan(world_state, goal)
38
+ """
39
+
40
+ from .models.goap_planner import Planner, PlanResult, PlanStats, Plan
41
+ from .models.goal import Goal
42
+ from .models.actions import Action, Actions
43
+ from .models.worldstate import WorldState
44
+
45
+ __version__ = "0.1.1"
46
+ __all__ = [
47
+ 'Planner',
48
+ 'Goal',
49
+ 'Action',
50
+ 'Actions',
51
+ 'WorldState',
52
+ 'PlanResult',
53
+ 'PlanStats',
54
+ 'Plan'
55
+ ]
@@ -0,0 +1,231 @@
1
+ import copy
2
+ import logging
3
+ from typing import Any, Dict, List, Optional, Type, TypeVar, Union
4
+ from dataclasses import dataclass
5
+
6
+ from models.worldstate import WorldState
7
+
8
+ logger = logging.getLogger(__name__)
9
+ T = TypeVar('T', bound='Action')
10
+
11
+ @dataclass
12
+ class Action:
13
+ """Represents an action the agent can take.
14
+
15
+ Attributes:
16
+ name: Unique identifier for the action
17
+ preconditions: Dictionary of state requirements that must be met for the action to be applicable
18
+ effects: Dictionary of state changes that result from applying this action
19
+ cost: The cost of executing this action (used for pathfinding)
20
+ """
21
+ name: str
22
+ preconditions: Dict[str, Any]
23
+ effects: Dict[str, Any]
24
+ cost: int = 1
25
+
26
+ def __post_init__(self) -> None:
27
+ """Validate the action after initialization."""
28
+ if not isinstance(self.name, str) or not self.name.strip():
29
+ raise ValueError("Action name must be a non-empty string")
30
+ if not isinstance(self.preconditions, dict):
31
+ raise TypeError("Preconditions must be a dictionary")
32
+ if not isinstance(self.effects, dict):
33
+ raise TypeError("Effects must be a dictionary")
34
+ if not isinstance(self.cost, (int, float)) or self.cost <= 0:
35
+ raise ValueError("Cost must be a positive number")
36
+
37
+ def is_applicable(self, state: Any) -> bool:
38
+ """Check if this action can be applied to the given state.
39
+
40
+ Args:
41
+ state: The current world state to check against
42
+
43
+ Returns:
44
+ bool: True if all preconditions are met, False otherwise
45
+ """
46
+ if not hasattr(state, '__dict__'):
47
+ raise TypeError("State must be an object with attributes")
48
+
49
+ logger.debug('Checking applicability of action: %s', self.name)
50
+ try:
51
+ for attr, expected_value in self.preconditions.items():
52
+ if not hasattr(state, attr):
53
+ logger.debug('State missing required attribute: %s', attr)
54
+ return False
55
+ if getattr(state, attr) != expected_value:
56
+ logger.debug('Precondition not met: %s != %s',
57
+ getattr(state, attr), expected_value)
58
+ return False
59
+ return True
60
+ except Exception as e:
61
+ logger.error('Error checking action applicability: %s', str(e), exc_info=True)
62
+ return False
63
+
64
+ def apply(self, state: Any) -> Any:
65
+ """Apply this action to the current state and return a new state.
66
+
67
+ Args:
68
+ state: The current world state to apply the action to
69
+
70
+ Returns:
71
+ A new state with the action's effects applied
72
+
73
+ Raises:
74
+ ValueError: If the action cannot be applied to the current state
75
+ """
76
+ if not self.is_applicable(state):
77
+ raise ValueError(f"Action {self.name} is not applicable to the current state")
78
+
79
+ logger.info('Applying action: %s', self.name)
80
+ try:
81
+ # Create a deep copy to avoid modifying the original state
82
+ new_state = copy.deepcopy(state)
83
+
84
+ # Apply each effect to the new state
85
+ for attr, value in self.effects.items():
86
+ setattr(new_state, attr, value)
87
+
88
+ logger.debug('New state after %s: %s', self.name, new_state)
89
+ return new_state
90
+
91
+ except Exception as e:
92
+ logger.error('Failed to apply action %s: %s', self.name, str(e), exc_info=True)
93
+ raise
94
+
95
+ def __str__(self) -> str:
96
+ """Return a string representation of the action."""
97
+ return (f"{self.__class__.__name__}('{self.name}', "
98
+ f"preconditions={self.preconditions}, "
99
+ f"effects={self.effects}, cost={self.cost})")
100
+
101
+ def __repr__(self) -> str:
102
+ """Return the canonical string representation of the action."""
103
+ return str(self)
104
+
105
+ class Actions:
106
+ """Manages a collection of available actions for the GOAP planner.
107
+
108
+ This class provides methods to add, retrieve, and manage actions that can be
109
+ used by the planner to achieve goals. It ensures that all actions are valid
110
+ and provides efficient lookup and iteration capabilities.
111
+ """
112
+
113
+ def __init__(self) -> None:
114
+ """Initialize an empty collection of actions."""
115
+ self._actions: List[Action] = []
116
+
117
+ def add_action(self, name: str, preconditions: Dict[str, Any],
118
+ effects: Dict[str, Any], cost: int = 1) -> None:
119
+ """Add a single action to the collection.
120
+
121
+ Args:
122
+ name: Unique identifier for the action
123
+ preconditions: Dictionary of state requirements for the action
124
+ effects: Dictionary of state changes caused by the action
125
+ cost: The cost of executing this action (default: 1)
126
+
127
+ Raises:
128
+ ValueError: If an action with the same name already exists
129
+ TypeError: If any parameter has an invalid type
130
+ """
131
+ if not isinstance(name, str) or not name.strip():
132
+ raise ValueError("Action name must be a non-empty string")
133
+
134
+ if self.get_action(name) is not None:
135
+ raise ValueError(f"Action with name '{name}' already exists")
136
+
137
+ try:
138
+ action = Action(name, preconditions, effects, cost)
139
+ self._actions.append(action)
140
+ logger.debug('Added action: %s', name)
141
+ except Exception as e:
142
+ logger.error('Failed to add action %s: %s', name, str(e))
143
+ raise
144
+
145
+ def add_actions(self, action_definitions: List[tuple]) -> None:
146
+ """Add multiple actions to the collection.
147
+
148
+ Args:
149
+ action_definitions: List of action definitions where each definition is a tuple
150
+ in the format (name: str, preconditions: dict, effects: dict, cost: int)
151
+
152
+ Example:
153
+ actions = Actions()
154
+ actions.add_actions([
155
+ ("open_door", {"door_locked": False}, {"door_open": True}, 1),
156
+ ("unlock_door", {"has_key": True}, {"door_locked": False}, 2)
157
+ ])
158
+ """
159
+ if not isinstance(action_definitions, (list, tuple)):
160
+ raise TypeError("action_definitions must be a list or tuple")
161
+
162
+ for i, action_def in enumerate(action_definitions):
163
+ try:
164
+ if not isinstance(action_def, (list, tuple)) or len(action_def) != 4:
165
+ raise ValueError(f"Action definition at index {i} must be a 4-tuple "
166
+ "(name, preconditions, effects, cost)")
167
+ self.add_action(*action_def)
168
+ except Exception as e:
169
+ logger.error('Error adding action at index %d: %s', i, str(e))
170
+ raise
171
+
172
+ def get_action(self, name: str) -> Optional[Action]:
173
+ """Retrieve an action by its name.
174
+
175
+ Args:
176
+ name: The name of the action to retrieve
177
+
178
+ Returns:
179
+ The Action object if found, None otherwise
180
+ """
181
+ if not isinstance(name, str):
182
+ raise TypeError("Action name must be a string")
183
+
184
+ for action in self._actions:
185
+ if action.name == name:
186
+ return action
187
+ return None
188
+
189
+ def get_actions(self) -> List[Action]:
190
+ """Get a list of all actions in the collection.
191
+
192
+ Returns:
193
+ A new list containing all Action objects
194
+ """
195
+ return self._actions.copy()
196
+
197
+ def clear_actions(self) -> None:
198
+ """Remove all actions from the collection."""
199
+ self._actions.clear()
200
+ logger.info('Cleared all actions')
201
+
202
+ def filter_actions(self, state: Any) -> List[Action]:
203
+ """Get a list of all actions that can be applied to the given state.
204
+
205
+ Args:
206
+ state: The state to check against action preconditions
207
+
208
+ Returns:
209
+ A list of applicable Action objects
210
+ """
211
+ return [action for action in self._actions if action.is_applicable(state)]
212
+
213
+ def __iter__(self) -> 'Actions':
214
+ """Return an iterator over all actions."""
215
+ return iter(self._actions)
216
+
217
+ def __len__(self) -> int:
218
+ """Return the number of actions in the collection."""
219
+ return len(self._actions)
220
+
221
+ def __contains__(self, name: str) -> bool:
222
+ """Check if an action with the given name exists."""
223
+ return self.get_action(name) is not None
224
+
225
+ def __str__(self) -> str:
226
+ """Return a string representation of the actions collection."""
227
+ return f"Actions({len(self._actions)} actions available)"
228
+
229
+ def __repr__(self) -> str:
230
+ """Return a detailed string representation of the actions collection."""
231
+ return f"<{self.__class__.__name__} with {len(self._actions)} actions>"
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from typing import Any, Dict, Optional, Type, TypeVar, Union
4
+ from dataclasses import dataclass, field
5
+ from copy import deepcopy
6
+
7
+ logger = logging.getLogger(__name__)
8
+ T = TypeVar('T', bound='Goal')
9
+
10
+ class Goal:
11
+ """Represents a goal with a target state and optional priority.
12
+
13
+ A goal defines a desired state that the planner should try to achieve.
14
+ Goals can have different priorities, with lower numbers indicating
15
+ higher priority (e.g., priority 1 is higher than priority 2).
16
+
17
+ Attributes:
18
+ target_state: A dictionary mapping state attributes to desired values
19
+ priority: The priority level (lower number = higher priority)
20
+ name: A human-readable name for the goal (defaults to string representation of target_state)
21
+ """
22
+
23
+ def __init__(self,
24
+ target_state: Dict[str, Any],
25
+ priority: int = 1,
26
+ name: Optional[str] = None):
27
+ """Initialize a new Goal instance.
28
+
29
+ Args:
30
+ target_state: Dictionary mapping state attributes to desired values.
31
+ Must be a non-empty dictionary.
32
+ priority: Priority level (lower number = higher priority). Must be >= 1.
33
+ name: Optional name for the goal. If not provided, will use string
34
+ representation of target_state.
35
+
36
+ Raises:
37
+ ValueError: If target_state is empty or priority is invalid.
38
+ TypeError: If target_state is not a dictionary or priority is not an integer.
39
+ """
40
+ if not isinstance(target_state, dict):
41
+ raise TypeError(f"target_state must be a dictionary, got {type(target_state)}")
42
+ if not target_state:
43
+ raise ValueError("target_state cannot be empty")
44
+ if not isinstance(priority, int):
45
+ raise TypeError(f"priority must be an integer, got {type(priority)}")
46
+ if priority < 1:
47
+ raise ValueError(f"priority must be >= 1, got {priority}")
48
+
49
+ self._target_state = target_state.copy() # Store a copy to prevent external modification
50
+ self._priority = priority
51
+ self._name = name or str(target_state)
52
+
53
+ @property
54
+ def target_state(self) -> Dict[str, Any]:
55
+ """Get the target state dictionary (read-only)."""
56
+ return self._target_state.copy()
57
+
58
+ @property
59
+ def priority(self) -> int:
60
+ """Get the priority of this goal (read-only)."""
61
+ return self._priority
62
+
63
+ @property
64
+ def name(self) -> str:
65
+ """Get the name of this goal (read-only)."""
66
+ return self._name
67
+
68
+ def is_satisfied(self, world_state: Any) -> bool:
69
+ """Check if this goal is satisfied by the given world state.
70
+
71
+ Args:
72
+ world_state: The world state to check against. Should be an object with
73
+ attributes matching the target_state keys.
74
+
75
+ Returns:
76
+ bool: True if all conditions in target_state are satisfied, False otherwise.
77
+
78
+ Raises:
79
+ AttributeError: If world_state is not an object with attributes.
80
+ """
81
+ if not hasattr(world_state, '__dict__'):
82
+ raise TypeError("world_state must be an object with attributes")
83
+
84
+ try:
85
+ return all(
86
+ getattr(world_state, attr) == value
87
+ for attr, value in self._target_state.items()
88
+ )
89
+ except AttributeError as e:
90
+ logger.error("Error checking goal satisfaction: %s", str(e))
91
+ return False
92
+
93
+ def get_unsatisfied_conditions(self, world_state: Any) -> Dict[str, tuple[Any, Any]]:
94
+ """Get the conditions that are not satisfied in the current world state.
95
+
96
+ Args:
97
+ world_state: The world state to check against.
98
+
99
+ Returns:
100
+ A dictionary mapping attribute names to tuples of (current_value, desired_value)
101
+ for all conditions that are not satisfied.
102
+
103
+ Raises:
104
+ AttributeError: If world_state is not an object with attributes.
105
+ """
106
+ if not hasattr(world_state, '__dict__'):
107
+ raise TypeError("world_state must be an object with attributes")
108
+
109
+ try:
110
+ return {
111
+ attr: (getattr(world_state, attr, None), desired_value)
112
+ for attr, desired_value in self._target_state.items()
113
+ if getattr(world_state, attr, None) != desired_value
114
+ }
115
+ except Exception as e:
116
+ logger.error("Error getting unsatisfied conditions: %s", str(e))
117
+ raise
118
+
119
+ def copy(self: T) -> T:
120
+ """Create a deep copy of this goal.
121
+
122
+ Returns:
123
+ A new Goal instance with the same target_state, priority, and name.
124
+ """
125
+ return self.__class__(
126
+ target_state=deepcopy(self._target_state),
127
+ priority=self._priority,
128
+ name=self._name
129
+ )
130
+
131
+ def __eq__(self, other: object) -> bool:
132
+ """Check if two goals are equal.
133
+
134
+ Two goals are considered equal if they have the same target_state,
135
+ priority, and name.
136
+ """
137
+ if not isinstance(other, Goal):
138
+ return NotImplemented
139
+
140
+ return (
141
+ self._target_state == other._target_state
142
+ and self._priority == other._priority
143
+ and self._name == other._name
144
+ )
145
+
146
+ def __hash__(self) -> int:
147
+ """Compute a hash value for this goal."""
148
+ return hash((
149
+ frozenset(self._target_state.items()),
150
+ self._priority,
151
+ self._name
152
+ ))
153
+
154
+ def __str__(self) -> str:
155
+ """Return a string representation of the goal."""
156
+ return f"{self.__class__.__name__}({self._name}, priority={self._priority})"
157
+
158
+ def __repr__(self) -> str:
159
+ """Return a detailed string representation of the goal."""
160
+ return (
161
+ f"<{self.__class__.__name__} "
162
+ f"name='{self._name}', "
163
+ f"priority={self._priority}, "
164
+ f"target_state={self._target_state}>"
165
+ )