agentspype 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Gianluca Pagliara
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: agentspype
3
+ Version: 0.1.0
4
+ Summary: A framework for building agents that interact with the world
5
+ Author: Gianluca Pagliara
6
+ Author-email: pagliara.gianluca@gmail.com
7
+ Requires-Python: >=3.13,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Dist: eventspype (>=1.0.0,<2.0.0)
11
+ Requires-Dist: pydantic (>=2.10.4,<3.0.0)
12
+ Requires-Dist: pydot (>=3.0.3,<4.0.0)
13
+ Requires-Dist: python-statemachine (>=2.5.0,<3.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Agents Pypeline
17
+
18
+ A framework for building agents that interact with the world.
19
+
@@ -0,0 +1,3 @@
1
+ # Agents Pypeline
2
+
3
+ A framework for building agents that interact with the world.
File without changes
@@ -0,0 +1,31 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from .agent.agent import Agent
5
+
6
+
7
+ class Agency:
8
+ initialized_agents: list["Agent"] = []
9
+
10
+ @classmethod
11
+ def register_agent(cls, agent: "Agent") -> None:
12
+ """Register an initialized agent."""
13
+ if agent not in cls.initialized_agents:
14
+ agent.logger().info(f"[Agency] Registered: {agent.__class__.__name__}")
15
+ cls.initialized_agents.append(agent)
16
+
17
+ @classmethod
18
+ def deregister_agent(cls, agent: "Agent") -> None:
19
+ """Deregister an initialized agent."""
20
+ if agent in cls.initialized_agents:
21
+ agent.logger().info(f"[Agency] Deregistered: {agent.__class__.__name__}")
22
+ cls.initialized_agents.remove(agent)
23
+
24
+ @classmethod
25
+ def get_active_agents(cls) -> list["Agent"]:
26
+ """Get a list of all active agents."""
27
+ return [
28
+ agent
29
+ for agent in cls.initialized_agents
30
+ if not agent.machine.current_state.final
31
+ ]
File without changes
@@ -0,0 +1,79 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from ..agency import Agency
5
+ from .configuration import AgentConfiguration
6
+ from .definition import AgentDefinition
7
+
8
+ if TYPE_CHECKING:
9
+ from .listening import AgentListening
10
+ from .publishing import AgentPublishing
11
+ from .state_machine import AgentStateMachine
12
+ from .status import AgentStatus
13
+
14
+
15
+ class Agent:
16
+ _logger: logging.Logger | None = None
17
+
18
+ # === Definition ===
19
+
20
+ definition: AgentDefinition
21
+
22
+ # === Initialization ===
23
+
24
+ def __init__(self, configuration: AgentConfiguration | dict[str, Any]):
25
+ self._configuration = (
26
+ configuration
27
+ if isinstance(configuration, AgentConfiguration)
28
+ else self.definition.configuration_class(**configuration)
29
+ )
30
+
31
+ self._events_publishing = self.definition.events_publishing_class(self)
32
+ self._events_listening = self.definition.events_listening_class(self)
33
+ self._state_machine = self.definition.state_machine_class(self)
34
+ self._status = self.definition.status_class()
35
+
36
+ Agency.register_agent(self)
37
+
38
+ self.initialize()
39
+
40
+ def initialize(self) -> None:
41
+ pass
42
+
43
+ def teardown(self) -> None:
44
+ self.listening.unsubscribe()
45
+
46
+ Agency.deregister_agent(self)
47
+
48
+ def clone(self) -> "Agent":
49
+ return self.__class__(self.configuration)
50
+
51
+ # === Class Methods ===
52
+
53
+ @classmethod
54
+ def logger(cls) -> logging.Logger:
55
+ if cls._logger is None:
56
+ cls._logger = logging.getLogger("agent")
57
+ return cls._logger
58
+
59
+ # === Properties ===
60
+
61
+ @property
62
+ def configuration(self) -> AgentConfiguration:
63
+ return self._configuration
64
+
65
+ @property
66
+ def machine(self) -> "AgentStateMachine":
67
+ return self._state_machine
68
+
69
+ @property
70
+ def listening(self) -> "AgentListening":
71
+ return self._events_listening
72
+
73
+ @property
74
+ def publishing(self) -> "AgentPublishing":
75
+ return self._events_publishing
76
+
77
+ @property
78
+ def status(self) -> "AgentStatus":
79
+ return self._status
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class AgentConfiguration(BaseModel):
5
+ """Base configuration class for agents."""
6
+
7
+ model_config = ConfigDict(frozen=True, arbitrary_types_allowed=False)
@@ -0,0 +1,19 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+ from .configuration import AgentConfiguration
4
+ from .listening import AgentListening
5
+ from .publishing import AgentPublishing
6
+ from .state_machine import AgentStateMachine
7
+ from .status import AgentStatus
8
+
9
+
10
+ class AgentDefinition(BaseModel):
11
+ """Definition of an agent's components and configuration."""
12
+
13
+ model_config = ConfigDict(frozen=True)
14
+
15
+ state_machine_class: type[AgentStateMachine]
16
+ events_listening_class: type[AgentListening]
17
+ events_publishing_class: type[AgentPublishing]
18
+ configuration_class: type[AgentConfiguration]
19
+ status_class: type[AgentStatus]
@@ -0,0 +1,33 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+ from typing import TYPE_CHECKING
4
+
5
+ from eventspype.sub.multisubscriber import MultiSubscriber
6
+
7
+ if TYPE_CHECKING:
8
+ from .agent import Agent
9
+
10
+
11
+ class AgentListening(MultiSubscriber):
12
+ def __init__(self, agent: "Agent") -> None:
13
+ super().__init__()
14
+ self._agent = agent
15
+
16
+ # === Properties ===
17
+
18
+ @property
19
+ def agent(self) -> "Agent":
20
+ return self._agent
21
+
22
+ def logger(self) -> logging.Logger:
23
+ return self.agent.logger()
24
+
25
+ # === Subscriptions ===
26
+
27
+ @abstractmethod
28
+ def subscribe(self) -> None:
29
+ raise NotImplementedError
30
+
31
+ @abstractmethod
32
+ def unsubscribe(self) -> None:
33
+ raise NotImplementedError
@@ -0,0 +1,56 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from eventspype.pub.multipublisher import MultiPublisher
6
+ from eventspype.pub.publication import EventPublication
7
+ from statemachine import State
8
+
9
+ if TYPE_CHECKING:
10
+ from .agent import Agent
11
+
12
+
13
+ class AgentPublishing(MultiPublisher):
14
+ def __init__(self, agent: "Agent") -> None:
15
+ super().__init__()
16
+ self._agent = agent
17
+
18
+ # === Properties ===
19
+
20
+ @property
21
+ def agent(self) -> "Agent":
22
+ return self._agent
23
+
24
+
25
+ class StateAgentPublishing(AgentPublishing):
26
+ # === Definitions ===
27
+
28
+ class Events(Enum):
29
+ StateMachineTransition = "sm_transition_event"
30
+
31
+ @dataclass
32
+ class StateMachineEvent:
33
+ event: Any
34
+ new_state: State
35
+
36
+ # === Publications ===
37
+
38
+ sm_transition_event = EventPublication(
39
+ event_tag=Events.StateMachineTransition, event_class=StateMachineEvent
40
+ )
41
+
42
+ # === Events ===
43
+
44
+ def publish_transition(self, event: Any, state: State) -> None:
45
+ """
46
+ Publishes a state machine transition event.
47
+
48
+ It is recommended to call this method from the state machine.
49
+ Example:
50
+ def after_transition(self, event, state):
51
+ self.agent.publishing.publish_transition(event, state)
52
+ """
53
+ self.trigger_event(
54
+ self.sm_transition_event,
55
+ self.sm_transition_event.event_class(event, state),
56
+ )
@@ -0,0 +1,134 @@
1
+ from abc import abstractmethod
2
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
3
+
4
+ from statemachine import State, StateMachine
5
+ from statemachine.factory import StateMachineMetaclass
6
+ from statemachine.transition_list import TransitionList
7
+
8
+ from .publishing import StateAgentPublishing
9
+
10
+ if TYPE_CHECKING:
11
+ from .agent import Agent
12
+
13
+
14
+ T = TypeVar("T", bound="AgentStateMachine")
15
+
16
+
17
+ class AgentStateMachineMeta(StateMachineMetaclass):
18
+ """Metaclass for AgentStateMachine that ensures proper state machine inheritance."""
19
+
20
+ def __new__(
21
+ mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
22
+ ) -> type:
23
+ # Don't modify the base class
24
+ if name == "AgentStateMachine":
25
+ return super().__new__(mcs, name, bases, namespace)
26
+
27
+ # Create state definitions if not already defined
28
+ if "starting" not in namespace:
29
+ namespace["starting"] = State("Starting", initial=True)
30
+ if "idle" not in namespace:
31
+ namespace["idle"] = State("Idle")
32
+ if "end" not in namespace:
33
+ namespace["end"] = State("End", final=True)
34
+
35
+ # Create transition definitions if not already defined
36
+ if "start" not in namespace:
37
+ namespace["start"] = namespace["starting"].to(namespace["idle"])
38
+ if "stop" not in namespace:
39
+ namespace["stop"] = namespace["starting"].to(namespace["end"]) | namespace[
40
+ "idle"
41
+ ].to(namespace["end"])
42
+
43
+ return super().__new__(mcs, name, bases, namespace)
44
+
45
+
46
+ class AgentStateMachine(StateMachine, metaclass=AgentStateMachineMeta):
47
+ """Base class for all agent state machines.
48
+
49
+ This class provides the basic state machine structure that all agents should inherit from.
50
+ Child classes can override states and transitions by defining their own, or inherit the defaults.
51
+ """
52
+
53
+ # === States ===
54
+
55
+ starting: ClassVar[State]
56
+ idle: ClassVar[State]
57
+ end: ClassVar[State]
58
+
59
+ # === Transitions ===
60
+
61
+ start: ClassVar[TransitionList]
62
+ stop: ClassVar[TransitionList]
63
+
64
+ def __init__(self, agent: "Agent") -> None:
65
+ super().__init__()
66
+ self._agent = agent
67
+ self._should_stop = False
68
+
69
+ # === State actions ===
70
+
71
+ def on_enter_end(self) -> None:
72
+ self.agent.teardown()
73
+
74
+ # === Transitions Actions ===
75
+
76
+ def before_transition(
77
+ self, event: str, state: str, source: str, target: str
78
+ ) -> None:
79
+ if source == target:
80
+ return
81
+
82
+ self.agent.logger().debug(
83
+ f"[{self.agent.__class__.__name__}:StateMachine] ({event}) {source} -> {target}"
84
+ )
85
+
86
+ @abstractmethod
87
+ def after_transition(self, event: str, state: State) -> None:
88
+ """Handle behavior after a transition.
89
+
90
+ This method should be implemented in child classes to handle post-transition behavior.
91
+ Example implementation:
92
+ self.agent.publishing.publish_transition(event, state)
93
+ """
94
+ raise NotImplementedError
95
+
96
+ def on_start(self) -> None:
97
+ self.agent.listening.subscribe()
98
+
99
+ def on_stop(self) -> None:
100
+ self._should_stop = True
101
+
102
+ # === Conditions ===
103
+
104
+ def should_stop(self) -> bool:
105
+ return self._should_stop
106
+
107
+ # === Properties ===
108
+
109
+ @property
110
+ def agent(self) -> "Agent":
111
+ return self._agent
112
+
113
+ # === Functions ===
114
+
115
+ def safe_stop(self) -> None:
116
+ """Safely stop the state machine if not already in final state."""
117
+ if self.current_state.final:
118
+ return
119
+ self.stop(f=True)
120
+
121
+ # === Diagrams ===
122
+
123
+
124
+ # Example of how to create a concrete state machine:
125
+ class BasicAgentStateMachine(AgentStateMachine):
126
+ """A basic implementation of an agent state machine.
127
+
128
+ This class inherits all the default states and transitions from AgentStateMachine.
129
+ It only needs to implement the required after_transition method.
130
+ """
131
+
132
+ def after_transition(self, event: str, state: State) -> None:
133
+ if isinstance(self.agent.publishing, StateAgentPublishing):
134
+ self.agent.publishing.publish_transition(event, state)
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class AgentStatus(BaseModel):
5
+ """Base status class for agents."""
6
+
7
+ model_config = ConfigDict()
@@ -0,0 +1,72 @@
1
+ [tool.poetry]
2
+ name = "agentspype"
3
+ version = "0.1.0"
4
+ description = "A framework for building agents that interact with the world"
5
+ authors = ["Gianluca Pagliara <pagliara.gianluca@gmail.com>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.13"
10
+ pydantic = "^2.10.4"
11
+ python-statemachine = "^2.5.0"
12
+ pydot = "^3.0.3"
13
+ eventspype = "^1.0.0"
14
+
15
+ [tool.poetry.group.dev.dependencies]
16
+ pytest = "^7.4.3"
17
+ pytest-cov = "^4.1.0"
18
+ black = "^23.11.0"
19
+ isort = "^5.12.0"
20
+ mypy = "^1.7.0"
21
+ safety = "^2.3.5"
22
+ pre-commit = "^3.5.0"
23
+ ruff = "^0.8.4"
24
+ pytest-asyncio = "<0.25.0"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core"]
28
+ build-backend = "poetry.core.masonry.api"
29
+
30
+ [tool.black]
31
+ line-length = 88
32
+ target-version = ['py312']
33
+ include = '\.pyi?$'
34
+
35
+ [tool.isort]
36
+ profile = "black"
37
+ multi_line_output = 3
38
+ line_length = 88
39
+
40
+ [tool.mypy]
41
+ python_version = "3.13"
42
+ warn_return_any = true
43
+ warn_unused_configs = true
44
+ disallow_untyped_defs = true
45
+ check_untyped_defs = true
46
+ strict = true
47
+ disallow_untyped_decorators = false
48
+ ignore_missing_imports = true
49
+ disable_error_code = ["misc"]
50
+ exclude = ["tests/.*"]
51
+
52
+ [tool.ruff]
53
+ line-length = 88
54
+ target-version = "py312"
55
+
56
+ [tool.ruff.lint]
57
+ select = [
58
+ "E", # pycodestyle errors
59
+ "W", # pycodestyle warnings
60
+ "F", # pyflakes
61
+ "I", # isort
62
+ "C", # flake8-comprehensions
63
+ "B", # flake8-bugbear
64
+ "UP" # pyupgrade
65
+ ]
66
+ ignore = [
67
+ "E203", # See https://github.com/psf/black/issues/315
68
+ "E501" # Line too long (handled by black)
69
+ ]
70
+
71
+ [tool.ruff.lint.per-file-ignores]
72
+ "__init__.py" = ["F401"] # Ignore unused imports in __init__ files