agent-patterns-py 0.1.0__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.
@@ -0,0 +1,98 @@
1
+ """
2
+ agent-patterns
3
+ ==============
4
+
5
+ Production-ready boilerplate for common agentic AI patterns in Python.
6
+
7
+ Quick start::
8
+
9
+ from agent_patterns import (
10
+ Agent, Goal, Memory,
11
+ PythonEnvironment,
12
+ AgentFunctionCallingActionLanguage,
13
+ PythonActionRegistry,
14
+ register_tool,
15
+ make_generate_response,
16
+ )
17
+
18
+ @register_tool(tags=["my_tools"], terminal=True)
19
+ def terminate(message: str) -> str:
20
+ \"\"\"End the session and return a final message.\"\"\"
21
+ return message
22
+
23
+ agent = Agent(
24
+ goals=[Goal(1, "Task", "Complete the assigned task then terminate.")],
25
+ agent_language=AgentFunctionCallingActionLanguage(),
26
+ action_registry=PythonActionRegistry(tags=["my_tools"]),
27
+ generate_response=make_generate_response("openai/gpt-4o"),
28
+ environment=PythonEnvironment(),
29
+ )
30
+
31
+ memory = agent.run("Hello!")
32
+ """
33
+
34
+ # Core GAME components
35
+ from agent_patterns.core.goal import Goal
36
+ from agent_patterns.core.memory import Memory
37
+ from agent_patterns.core.action import Action, ActionRegistry
38
+ from agent_patterns.core.context import ActionContext
39
+ from agent_patterns.core.environment import Environment, PythonEnvironment
40
+ from agent_patterns.core.agent import Agent
41
+
42
+ # Language strategies
43
+ from agent_patterns.language.base import AgentLanguage, Prompt
44
+ from agent_patterns.language.function_calling import AgentFunctionCallingActionLanguage
45
+ from agent_patterns.language.json_action import AgentJsonActionLanguage
46
+
47
+ # Tool system
48
+ from agent_patterns.tools.registry import register_tool, get_tool_metadata, PythonActionRegistry
49
+
50
+ # Utilities
51
+ from agent_patterns.utils.llm import make_generate_response, generate_response, prompt_llm
52
+
53
+ # Capabilities
54
+ from agent_patterns.capabilities.base import Capability
55
+ from agent_patterns.capabilities.plan_first import PlanFirstCapability
56
+ from agent_patterns.capabilities.progress_tracking import ProgressTrackingCapability
57
+
58
+ # Multi-agent
59
+ from agent_patterns.multi_agent.registry import AgentRegistry
60
+
61
+ # Safety
62
+ from agent_patterns.safety.reversible import ReversibleAction, ActionTransaction
63
+ from agent_patterns.safety.staged import StagedActionEnvironment
64
+
65
+ __all__ = [
66
+ # Core
67
+ "Goal",
68
+ "Memory",
69
+ "Action",
70
+ "ActionRegistry",
71
+ "ActionContext",
72
+ "Environment",
73
+ "PythonEnvironment",
74
+ "Agent",
75
+ # Language
76
+ "AgentLanguage",
77
+ "Prompt",
78
+ "AgentFunctionCallingActionLanguage",
79
+ "AgentJsonActionLanguage",
80
+ # Tools
81
+ "register_tool",
82
+ "get_tool_metadata",
83
+ "PythonActionRegistry",
84
+ # Utils
85
+ "make_generate_response",
86
+ "generate_response",
87
+ "prompt_llm",
88
+ # Capabilities
89
+ "Capability",
90
+ "PlanFirstCapability",
91
+ "ProgressTrackingCapability",
92
+ # Multi-agent
93
+ "AgentRegistry",
94
+ # Safety
95
+ "ReversibleAction",
96
+ "ActionTransaction",
97
+ "StagedActionEnvironment",
98
+ ]
@@ -0,0 +1,9 @@
1
+ from agent_patterns.capabilities.base import Capability
2
+ from agent_patterns.capabilities.plan_first import PlanFirstCapability
3
+ from agent_patterns.capabilities.progress_tracking import ProgressTrackingCapability
4
+
5
+ __all__ = [
6
+ "Capability",
7
+ "PlanFirstCapability",
8
+ "ProgressTrackingCapability",
9
+ ]
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
5
+
6
+ if TYPE_CHECKING:
7
+ from agent_patterns.core.action import Action
8
+ from agent_patterns.core.context import ActionContext
9
+ from agent_patterns.core.memory import Memory
10
+ from agent_patterns.language.base import Prompt
11
+
12
+
13
+ class Capability(ABC):
14
+ """
15
+ Base class for agent loop extensions.
16
+
17
+ Capabilities plug into eight lifecycle hooks in the agent loop,
18
+ allowing rich extensions (time awareness, planning, logging, metrics,
19
+ etc.) without modifying :class:`~agent_patterns.core.agent.Agent`.
20
+
21
+ All methods have no-op defaults — override only what you need.
22
+
23
+ Lifecycle (per ``agent.run()`` call)::
24
+
25
+ init()
26
+ ┌─ loop ─────────────────────────────────────────────────────┐
27
+ │ start_agent_loop() ← return False to stop │
28
+ │ process_prompt() │
29
+ │ [LLM call] │
30
+ │ process_response() │
31
+ │ process_action() │
32
+ │ [Environment.execute_action()] │
33
+ │ process_result() │
34
+ │ process_new_memories() │
35
+ │ end_agent_loop() │
36
+ │ should_terminate() ← return True to stop │
37
+ └─────────────────────────────────────────────────────────────┘
38
+ terminate()
39
+ """
40
+
41
+ def __init__(self, name: str, description: str) -> None:
42
+ self.name = name
43
+ self.description = description
44
+
45
+ def init(self, agent: Any, action_context: "ActionContext") -> None:
46
+ """Called once before the loop starts. Set up state here."""
47
+
48
+ def start_agent_loop(
49
+ self, agent: Any, action_context: "ActionContext"
50
+ ) -> Optional[bool]:
51
+ """
52
+ Called at the start of each iteration.
53
+ Return ``False`` to stop the loop immediately.
54
+ """
55
+ return True
56
+
57
+ def process_prompt(
58
+ self, agent: Any, action_context: "ActionContext", prompt: "Prompt"
59
+ ) -> "Prompt":
60
+ """Modify the prompt before it is sent to the LLM."""
61
+ return prompt
62
+
63
+ def process_response(
64
+ self, agent: Any, action_context: "ActionContext", response: str
65
+ ) -> str:
66
+ """Modify the raw LLM response before it is parsed."""
67
+ return response
68
+
69
+ def process_action(
70
+ self, agent: Any, action_context: "ActionContext", action: Dict
71
+ ) -> Dict:
72
+ """Modify the parsed action invocation before execution."""
73
+ return action
74
+
75
+ def process_result(
76
+ self,
77
+ agent: Any,
78
+ action_context: "ActionContext",
79
+ response: str,
80
+ action_def: "Action",
81
+ action: Dict,
82
+ result: Any,
83
+ ) -> Any:
84
+ """Modify the action result before it is stored in memory."""
85
+ return result
86
+
87
+ def process_new_memories(
88
+ self,
89
+ agent: Any,
90
+ action_context: "ActionContext",
91
+ memory: "Memory",
92
+ response: str,
93
+ result: Any,
94
+ memories: List[Dict],
95
+ ) -> List[Dict]:
96
+ """Add, remove, or modify memory entries before they are committed."""
97
+ return memories
98
+
99
+ def end_agent_loop(self, agent: Any, action_context: "ActionContext") -> None:
100
+ """Called at the end of each iteration (after memory update)."""
101
+
102
+ def should_terminate(
103
+ self, agent: Any, action_context: "ActionContext", response: str
104
+ ) -> bool:
105
+ """Return ``True`` to stop the loop after the current iteration."""
106
+ return False
107
+
108
+ def terminate(self, agent: Any, action_context: "ActionContext") -> None:
109
+ """Called once when the agent stops (cleanup, flushing, etc.)."""
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from agent_patterns.capabilities.base import Capability
6
+
7
+ if TYPE_CHECKING:
8
+ from agent_patterns.core.context import ActionContext
9
+
10
+
11
+ class PlanFirstCapability(Capability):
12
+ """
13
+ Forces the agent to create a detailed execution plan before acting.
14
+
15
+ On the first iteration, invokes
16
+ :func:`~agent_patterns.tools.planning.create_plan` and stores the
17
+ result in memory so every subsequent prompt includes the plan.
18
+
19
+ This improves reliability for multi-step tasks by anchoring the agent
20
+ to a structured strategy rather than improvising step-by-step.
21
+
22
+ Args:
23
+ plan_memory_type: Memory type tag for the plan entry.
24
+ ``"system"`` keeps it out of the visible
25
+ conversation while still influencing the prompt.
26
+ """
27
+
28
+ def __init__(self, plan_memory_type: str = "system") -> None:
29
+ super().__init__(
30
+ name="Plan First",
31
+ description="Generates an execution plan before the agent takes its first action.",
32
+ )
33
+ self.plan_memory_type = plan_memory_type
34
+ self._planned = False
35
+
36
+ def init(self, agent: Any, action_context: "ActionContext") -> None:
37
+ if self._planned:
38
+ return
39
+ self._planned = True
40
+
41
+ # Import here to avoid circular imports at module load time
42
+ from agent_patterns.tools.planning import create_plan
43
+
44
+ memory = action_context.get_memory()
45
+ action_registry = action_context.get_action_registry()
46
+
47
+ if memory is None or action_registry is None:
48
+ return
49
+
50
+ plan = create_plan(
51
+ action_context=action_context,
52
+ _memory=memory,
53
+ _action_registry=action_registry,
54
+ )
55
+
56
+ memory.add_memory({
57
+ "type": self.plan_memory_type,
58
+ "content": (
59
+ "You must follow these instructions carefully to complete the task:\n\n"
60
+ + plan
61
+ ),
62
+ })
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from agent_patterns.capabilities.base import Capability
6
+
7
+ if TYPE_CHECKING:
8
+ from agent_patterns.core.context import ActionContext
9
+
10
+
11
+ class ProgressTrackingCapability(Capability):
12
+ """
13
+ Adds a progress report to memory at the end of every N iterations.
14
+
15
+ After each tracked iteration, invokes
16
+ :func:`~agent_patterns.tools.planning.track_progress` and stores the
17
+ result so the agent can reflect on what it has accomplished and adjust
18
+ its strategy accordingly.
19
+
20
+ This is especially useful for long-running tasks where the agent needs
21
+ to self-correct rather than continue executing a stale plan.
22
+
23
+ Args:
24
+ memory_type: Type tag for progress report entries.
25
+ track_frequency: Generate a report every N iterations (default 1).
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ memory_type: str = "system",
31
+ track_frequency: int = 1,
32
+ ) -> None:
33
+ super().__init__(
34
+ name="Progress Tracking",
35
+ description="Adds an LLM-generated progress report to memory after each iteration.",
36
+ )
37
+ self.memory_type = memory_type
38
+ self.track_frequency = track_frequency
39
+ self._iteration = 0
40
+
41
+ def end_agent_loop(self, agent: Any, action_context: "ActionContext") -> None:
42
+ self._iteration += 1
43
+
44
+ if self._iteration % self.track_frequency != 0:
45
+ return
46
+
47
+ from agent_patterns.tools.planning import track_progress
48
+
49
+ memory = action_context.get_memory()
50
+ action_registry = action_context.get_action_registry()
51
+
52
+ if memory is None or action_registry is None:
53
+ return
54
+
55
+ report = track_progress(
56
+ action_context=action_context,
57
+ _memory=memory,
58
+ _action_registry=action_registry,
59
+ )
60
+
61
+ memory.add_memory({
62
+ "type": self.memory_type,
63
+ "content": f"Progress Report (iteration {self._iteration}):\n\n{report}",
64
+ })
@@ -0,0 +1,17 @@
1
+ from agent_patterns.core.goal import Goal
2
+ from agent_patterns.core.memory import Memory
3
+ from agent_patterns.core.action import Action, ActionRegistry
4
+ from agent_patterns.core.context import ActionContext
5
+ from agent_patterns.core.environment import Environment, PythonEnvironment
6
+ from agent_patterns.core.agent import Agent
7
+
8
+ __all__ = [
9
+ "Goal",
10
+ "Memory",
11
+ "Action",
12
+ "ActionRegistry",
13
+ "ActionContext",
14
+ "Environment",
15
+ "PythonEnvironment",
16
+ "Agent",
17
+ ]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, List, Optional
4
+
5
+
6
+ class Action:
7
+ """
8
+ Represents a tool the agent can invoke.
9
+
10
+ ``Action`` is the *interface* — it describes what the tool does and
11
+ what parameters it accepts. The ``Environment`` provides the
12
+ *implementation* that actually runs the underlying function.
13
+
14
+ Attributes:
15
+ name: Unique identifier matched against the LLM's tool call.
16
+ function: The Python callable to execute.
17
+ description: Human/LLM-readable description (used in the prompt).
18
+ parameters: JSON Schema object describing the tool's arguments.
19
+ terminal: When ``True``, the agent loop stops after this action.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ name: str,
25
+ function: Callable,
26
+ description: str,
27
+ parameters: Dict,
28
+ terminal: bool = False,
29
+ ) -> None:
30
+ self.name = name
31
+ self.function = function
32
+ self.description = description
33
+ self.terminal = terminal
34
+ self.parameters = parameters
35
+
36
+ def execute(self, **kwargs: Any) -> Any:
37
+ """Call the underlying function with *kwargs*."""
38
+ return self.function(**kwargs)
39
+
40
+
41
+ class ActionRegistry:
42
+ """
43
+ Maps action names to :class:`Action` objects.
44
+
45
+ Use :class:`~agent_patterns.tools.registry.PythonActionRegistry` for
46
+ automatic population from the ``@register_tool`` decorator registry.
47
+ """
48
+
49
+ def __init__(self) -> None:
50
+ self.actions: Dict[str, Action] = {}
51
+
52
+ def register(self, action: Action) -> None:
53
+ """Add *action* to the registry (overwrites if name already exists)."""
54
+ self.actions[action.name] = action
55
+
56
+ def get_action(self, name: str) -> Optional[Action]:
57
+ """Return the ``Action`` for *name*, or ``None`` if not found."""
58
+ return self.actions.get(name)
59
+
60
+ def get_actions(self) -> List[Action]:
61
+ """Return all registered actions."""
62
+ return list(self.actions.values())
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import time
6
+ from typing import TYPE_CHECKING, Callable, Dict, List, Optional
7
+
8
+ from agent_patterns.core.action import ActionRegistry
9
+ from agent_patterns.core.context import ActionContext
10
+ from agent_patterns.core.environment import Environment
11
+ from agent_patterns.core.goal import Goal
12
+ from agent_patterns.core.memory import Memory
13
+ from agent_patterns.language.base import AgentLanguage, Prompt
14
+
15
+ if TYPE_CHECKING:
16
+ from agent_patterns.capabilities.base import Capability
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class Agent:
22
+ """
23
+ Core GAME loop: Goals · Actions · Memory · Environment.
24
+
25
+ The loop runs until one of:
26
+
27
+ - A ``terminal`` action is selected.
28
+ - ``max_iterations`` is reached.
29
+ - ``max_duration_seconds`` elapses.
30
+ - A :class:`~agent_patterns.capabilities.base.Capability` signals stop.
31
+
32
+ **Capabilities** plug into eight lifecycle hooks without modifying this
33
+ class. Pass a list of them to ``capabilities``.
34
+
35
+ Example::
36
+
37
+ agent = Agent(
38
+ goals=[Goal(1, "task", "Do X")],
39
+ agent_language=AgentFunctionCallingActionLanguage(),
40
+ action_registry=PythonActionRegistry(tags=["my_tools"]),
41
+ generate_response=make_generate_response("openai/gpt-4o"),
42
+ environment=PythonEnvironment(),
43
+ )
44
+ memory = agent.run("Please do X for me.")
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ goals: List[Goal],
50
+ agent_language: AgentLanguage,
51
+ action_registry: ActionRegistry,
52
+ generate_response: Callable[[Prompt], str],
53
+ environment: Environment,
54
+ capabilities: Optional[List["Capability"]] = None,
55
+ max_iterations: int = 50,
56
+ max_duration_seconds: int = 180,
57
+ verbose: bool = True,
58
+ ) -> None:
59
+ self.goals = goals
60
+ self.generate_response = generate_response
61
+ self.agent_language = agent_language
62
+ self.actions = action_registry
63
+ self.environment = environment
64
+ self.capabilities: List["Capability"] = capabilities or []
65
+ self.max_iterations = max_iterations
66
+ self.max_duration_seconds = max_duration_seconds
67
+ self.verbose = verbose
68
+
69
+ # ------------------------------------------------------------------
70
+ # Prompt construction
71
+ # ------------------------------------------------------------------
72
+
73
+ def construct_prompt(
74
+ self, goals: List[Goal], memory: Memory, actions: ActionRegistry
75
+ ) -> Prompt:
76
+ return self.agent_language.construct_prompt(
77
+ actions=actions.get_actions(),
78
+ environment=self.environment,
79
+ goals=goals,
80
+ memory=memory,
81
+ )
82
+
83
+ # ------------------------------------------------------------------
84
+ # Response helpers
85
+ # ------------------------------------------------------------------
86
+
87
+ def get_action(self, response: str):
88
+ """Parse *response* and look up the corresponding Action."""
89
+ invocation = self.agent_language.parse_response(response)
90
+ action = self.actions.get_action(invocation["tool"])
91
+ return action, invocation
92
+
93
+ def should_terminate(self, response: str) -> bool:
94
+ """Return ``True`` if the parsed action is terminal."""
95
+ try:
96
+ action_def, _ = self.get_action(response)
97
+ return action_def is not None and action_def.terminal
98
+ except Exception:
99
+ return False
100
+
101
+ # ------------------------------------------------------------------
102
+ # Memory helpers
103
+ # ------------------------------------------------------------------
104
+
105
+ def set_current_task(self, memory: Memory, task: str) -> None:
106
+ memory.add_memory({"type": "user", "content": task})
107
+
108
+ # ------------------------------------------------------------------
109
+ # LLM call
110
+ # ------------------------------------------------------------------
111
+
112
+ def prompt_llm_for_action(self, prompt: Prompt) -> str:
113
+ return self.generate_response(prompt)
114
+
115
+ # ------------------------------------------------------------------
116
+ # Main loop
117
+ # ------------------------------------------------------------------
118
+
119
+ def run(
120
+ self,
121
+ user_input: str,
122
+ memory: Optional[Memory] = None,
123
+ action_context_props: Optional[Dict] = None,
124
+ max_iterations: Optional[int] = None,
125
+ ) -> Memory:
126
+ """
127
+ Execute the GAME loop and return the final :class:`Memory`.
128
+
129
+ Args:
130
+ user_input: The task to give the agent.
131
+ memory: Existing memory to continue from (e.g. for
132
+ multi-agent hand-offs). A fresh one is
133
+ created when ``None``.
134
+ action_context_props: Extra properties injected into
135
+ :class:`ActionContext` and available to
136
+ tools via ``_<key>`` parameters.
137
+ max_iterations: Override the instance-level default.
138
+ """
139
+ memory = memory or Memory()
140
+ action_context_props = action_context_props or {}
141
+ iterations = max_iterations if max_iterations is not None else self.max_iterations
142
+
143
+ action_context = ActionContext({
144
+ "memory": memory,
145
+ "llm": self.generate_response,
146
+ "action_registry": self.actions,
147
+ **action_context_props,
148
+ })
149
+
150
+ # Initialise capabilities (runs once)
151
+ for cap in self.capabilities:
152
+ cap.init(self, action_context)
153
+
154
+ self.set_current_task(memory, user_input)
155
+ start_time = time.time()
156
+
157
+ for _ in range(iterations):
158
+ # Hard wall on execution time
159
+ if time.time() - start_time > self.max_duration_seconds:
160
+ logger.warning("Agent stopped: max_duration_seconds (%s) reached.", self.max_duration_seconds)
161
+ break
162
+
163
+ # start_agent_loop — any capability can halt by returning False
164
+ if any(cap.start_agent_loop(self, action_context) is False for cap in self.capabilities):
165
+ break
166
+
167
+ # Build and augment prompt
168
+ prompt = self.construct_prompt(self.goals, memory, self.actions)
169
+ for cap in self.capabilities:
170
+ prompt = cap.process_prompt(self, action_context, prompt)
171
+
172
+ if self.verbose:
173
+ print("Agent thinking…")
174
+
175
+ # Call the LLM
176
+ response = self.prompt_llm_for_action(prompt)
177
+
178
+ if self.verbose:
179
+ print(f"Agent Decision: {response}")
180
+
181
+ # Capability post-processing on the raw response
182
+ for cap in self.capabilities:
183
+ response = cap.process_response(self, action_context, response)
184
+
185
+ # Parse → look up action
186
+ try:
187
+ action_def, invocation = self.get_action(response)
188
+ except Exception as exc:
189
+ logger.warning("Failed to parse agent response: %s", exc)
190
+ break
191
+
192
+ if action_def is None:
193
+ logger.warning("Unknown action in response, stopping: %s", response)
194
+ break
195
+
196
+ # Capability hook on the parsed invocation
197
+ for cap in self.capabilities:
198
+ invocation = cap.process_action(self, action_context, invocation)
199
+
200
+ # Execute in environment
201
+ result = self.environment.execute_action(
202
+ self, action_context, action_def, invocation.get("args", {})
203
+ )
204
+
205
+ if self.verbose:
206
+ print(f"Action Result: {result}")
207
+
208
+ # Capability hook on the result
209
+ for cap in self.capabilities:
210
+ result = cap.process_result(
211
+ self, action_context, response, action_def, invocation, result
212
+ )
213
+
214
+ # Build memory entries and let capabilities modify them
215
+ new_memories: List[Dict] = [
216
+ {"type": "assistant", "content": response},
217
+ {"type": "environment", "content": json.dumps(result)},
218
+ ]
219
+ for cap in self.capabilities:
220
+ new_memories = cap.process_new_memories(
221
+ self, action_context, memory, response, result, new_memories
222
+ )
223
+ for m in new_memories:
224
+ memory.add_memory(m)
225
+
226
+ # End-of-loop hook
227
+ for cap in self.capabilities:
228
+ cap.end_agent_loop(self, action_context)
229
+
230
+ # Termination checks
231
+ if self.should_terminate(response):
232
+ break
233
+ if any(cap.should_terminate(self, action_context, response) for cap in self.capabilities):
234
+ break
235
+
236
+ # Shutdown hook
237
+ for cap in self.capabilities:
238
+ cap.terminate(self, action_context)
239
+
240
+ return memory