pygeai-orchestration 0.1.0b2__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.
Files changed (61) hide show
  1. pygeai_orchestration/__init__.py +99 -0
  2. pygeai_orchestration/cli/__init__.py +7 -0
  3. pygeai_orchestration/cli/__main__.py +11 -0
  4. pygeai_orchestration/cli/commands/__init__.py +13 -0
  5. pygeai_orchestration/cli/commands/base.py +192 -0
  6. pygeai_orchestration/cli/error_handler.py +123 -0
  7. pygeai_orchestration/cli/formatters.py +419 -0
  8. pygeai_orchestration/cli/geai_orch.py +270 -0
  9. pygeai_orchestration/cli/interactive.py +265 -0
  10. pygeai_orchestration/cli/texts/help.py +169 -0
  11. pygeai_orchestration/core/__init__.py +130 -0
  12. pygeai_orchestration/core/base/__init__.py +23 -0
  13. pygeai_orchestration/core/base/agent.py +121 -0
  14. pygeai_orchestration/core/base/geai_agent.py +144 -0
  15. pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
  16. pygeai_orchestration/core/base/orchestrator.py +142 -0
  17. pygeai_orchestration/core/base/pattern.py +161 -0
  18. pygeai_orchestration/core/base/tool.py +149 -0
  19. pygeai_orchestration/core/common/__init__.py +18 -0
  20. pygeai_orchestration/core/common/context.py +140 -0
  21. pygeai_orchestration/core/common/memory.py +176 -0
  22. pygeai_orchestration/core/common/message.py +50 -0
  23. pygeai_orchestration/core/common/state.py +181 -0
  24. pygeai_orchestration/core/composition.py +190 -0
  25. pygeai_orchestration/core/config.py +356 -0
  26. pygeai_orchestration/core/exceptions.py +400 -0
  27. pygeai_orchestration/core/handlers.py +380 -0
  28. pygeai_orchestration/core/utils/__init__.py +37 -0
  29. pygeai_orchestration/core/utils/cache.py +138 -0
  30. pygeai_orchestration/core/utils/config.py +94 -0
  31. pygeai_orchestration/core/utils/logging.py +57 -0
  32. pygeai_orchestration/core/utils/metrics.py +184 -0
  33. pygeai_orchestration/core/utils/validators.py +140 -0
  34. pygeai_orchestration/dev/__init__.py +15 -0
  35. pygeai_orchestration/dev/debug.py +288 -0
  36. pygeai_orchestration/dev/templates.py +321 -0
  37. pygeai_orchestration/dev/testing.py +301 -0
  38. pygeai_orchestration/patterns/__init__.py +15 -0
  39. pygeai_orchestration/patterns/multi_agent.py +237 -0
  40. pygeai_orchestration/patterns/planning.py +219 -0
  41. pygeai_orchestration/patterns/react.py +221 -0
  42. pygeai_orchestration/patterns/reflection.py +134 -0
  43. pygeai_orchestration/patterns/tool_use.py +170 -0
  44. pygeai_orchestration/tests/__init__.py +1 -0
  45. pygeai_orchestration/tests/test_base_classes.py +187 -0
  46. pygeai_orchestration/tests/test_cache.py +184 -0
  47. pygeai_orchestration/tests/test_cli_formatters.py +232 -0
  48. pygeai_orchestration/tests/test_common.py +214 -0
  49. pygeai_orchestration/tests/test_composition.py +265 -0
  50. pygeai_orchestration/tests/test_config.py +301 -0
  51. pygeai_orchestration/tests/test_dev_utils.py +337 -0
  52. pygeai_orchestration/tests/test_exceptions.py +327 -0
  53. pygeai_orchestration/tests/test_handlers.py +307 -0
  54. pygeai_orchestration/tests/test_metrics.py +171 -0
  55. pygeai_orchestration/tests/test_patterns.py +165 -0
  56. pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
  57. pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
  58. pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
  59. pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
  60. pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
  61. pygeai_orchestration-0.1.0b2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,237 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from ..core.base import BasePattern, PatternConfig, PatternResult, PatternType
3
+ from ..core.common import Message, MessageRole, Conversation, State, StateStatus
4
+ from ..core.utils import get_logger
5
+
6
+ logger = get_logger()
7
+
8
+
9
+ class AgentRole:
10
+ """
11
+ Represents an agent with a specific role in multi-agent collaboration.
12
+
13
+ :param name: str - The unique name/identifier for this agent role.
14
+ :param agent: BaseAgent - The agent instance performing this role.
15
+ :param role_description: str - Description of this agent's responsibilities and expertise.
16
+ """
17
+
18
+ def __init__(self, name: str, agent, role_description: str):
19
+ self.name = name
20
+ self.agent = agent
21
+ self.role_description = role_description
22
+ self.contributions: List[str] = []
23
+
24
+ def __repr__(self) -> str:
25
+ return f"AgentRole({self.name}: {self.role_description})"
26
+
27
+
28
+ class MultiAgentPattern(BasePattern):
29
+ """
30
+ Multi-agent pattern for collaborative problem-solving with specialized agents.
31
+
32
+ This pattern orchestrates multiple agents with different roles and expertise
33
+ to collaboratively solve complex tasks. A coordinator agent manages the workflow,
34
+ distributes work, and synthesizes contributions into a final answer.
35
+
36
+ The multi-agent pattern is ideal for:
37
+ - Tasks requiring diverse expertise (e.g., research, analysis, writing)
38
+ - Complex problems benefiting from different perspectives
39
+ - Scenarios where specialized agents outperform generalists
40
+ - Workflow requiring coordination and synthesis of multiple outputs
41
+
42
+ The pattern follows this workflow:
43
+ 1. **Coordination**: Coordinator agent analyzes task and creates distribution plan
44
+ 2. **Parallel/Sequential Execution**: Each specialized agent contributes based on role
45
+ 3. **Synthesis**: Coordinator integrates all contributions into coherent final answer
46
+
47
+ This approach enables division of labor, specialization, and richer solutions
48
+ than single-agent approaches.
49
+ """
50
+
51
+ def __init__(
52
+ self, agents: List[AgentRole], coordinator_agent, config: Optional[PatternConfig] = None
53
+ ):
54
+ """
55
+ Initialize the Multi-Agent pattern.
56
+
57
+ :param agents: List[AgentRole] - List of agent roles participating in collaboration.
58
+ :param coordinator_agent: BaseAgent - The coordinator agent managing workflow and synthesis.
59
+ :param config: Optional[PatternConfig] - Pattern configuration. If None, uses default with max_iterations=15.
60
+ """
61
+ if config is None:
62
+ config = PatternConfig(
63
+ name="multi_agent", pattern_type=PatternType.MULTI_AGENT, max_iterations=15
64
+ )
65
+ super().__init__(config)
66
+ self.agent_roles = {role.name: role for role in agents}
67
+ self.coordinator = coordinator_agent
68
+ self._conversation = Conversation(id=f"multi-agent-{id(self)}")
69
+ self._state = State()
70
+
71
+ async def execute(self, task: str, context: Optional[Dict[str, Any]] = None) -> PatternResult:
72
+ """
73
+ Execute the multi-agent pattern on the given task.
74
+
75
+ The method coordinates multiple specialized agents to collaboratively solve
76
+ the task. The coordinator first creates a distribution plan, then each agent
77
+ contributes based on its role, and finally the coordinator synthesizes all
78
+ contributions into a unified answer.
79
+
80
+ :param task: str - The task requiring collaborative multi-agent effort.
81
+ :param context: Optional[Dict[str, Any]] - Additional context for execution. Defaults to None.
82
+ :return: PatternResult - Contains success status, synthesized answer, and agent contributions.
83
+ :raises PatternExecutionError: If the multi-agent pattern execution fails.
84
+
85
+ Example:
86
+ >>> roles = [
87
+ ... AgentRole("researcher", research_agent, "Research and gather facts"),
88
+ ... AgentRole("analyst", analysis_agent, "Analyze data and identify patterns"),
89
+ ... AgentRole("writer", writing_agent, "Create clear written summaries")
90
+ ... ]
91
+ >>> pattern = MultiAgentPattern(agents=roles, coordinator_agent=coordinator)
92
+ >>> result = await pattern.execute("Analyze the impact of AI on healthcare")
93
+ >>> print(f"Final answer: {result.result}")
94
+ >>> print(f"Contributions: {result.metadata['contributions']}")
95
+ """
96
+ self.reset()
97
+ for role in self.agent_roles.values():
98
+ role.contributions.clear()
99
+ self._state.update_status(StateStatus.RUNNING)
100
+
101
+ logger.info(
102
+ f"Starting multi-agent pattern with {len(self.agent_roles)} agents for task: {task[:50]}..."
103
+ )
104
+
105
+ try:
106
+ coordination_plan = await self._coordinate_task(task)
107
+ logger.info(f"Coordination plan: {coordination_plan[:100]}...")
108
+
109
+ all_contributions = {}
110
+
111
+ for agent_name, role in self.agent_roles.items():
112
+ if self.current_iteration >= self.config.max_iterations:
113
+ break
114
+
115
+ self.increment_iteration()
116
+ logger.debug(f"Agent '{agent_name}' processing task")
117
+
118
+ state_data = {
119
+ "iteration": self.current_iteration,
120
+ "agent_name": agent_name,
121
+ "role": role.role_description,
122
+ "task": task,
123
+ "coordination_plan": coordination_plan,
124
+ "previous_contributions": all_contributions,
125
+ }
126
+
127
+ step_result = await self.step(state_data)
128
+ contribution = step_result.get("contribution")
129
+ role.contributions.append(contribution)
130
+ all_contributions[agent_name] = contribution
131
+
132
+ final_result = await self._synthesize_contributions(task, all_contributions)
133
+
134
+ self._state.update_status(StateStatus.COMPLETED)
135
+
136
+ return PatternResult(
137
+ success=True,
138
+ result=final_result,
139
+ iterations=self.current_iteration,
140
+ metadata={
141
+ "agents": list(self.agent_roles.keys()),
142
+ "contributions": all_contributions,
143
+ "coordination_plan": coordination_plan,
144
+ },
145
+ )
146
+
147
+ except Exception as e:
148
+ logger.error(f"Multi-agent pattern failed: {str(e)}")
149
+ self._state.update_status(StateStatus.FAILED)
150
+ return PatternResult(
151
+ success=False, result=None, iterations=self.current_iteration, error=str(e)
152
+ )
153
+
154
+ async def step(self, state: Dict[str, Any]) -> Dict[str, Any]:
155
+ agent_name = state.get("agent_name")
156
+ role_desc = state.get("role")
157
+ task = state.get("task")
158
+ coordination_plan = state.get("coordination_plan", "")
159
+ previous_contributions = state.get("previous_contributions", {})
160
+
161
+ role = self.agent_roles[agent_name]
162
+
163
+ context_str = ""
164
+ if previous_contributions:
165
+ context_str = "\n\nContributions from other agents:\n"
166
+ for name, contrib in previous_contributions.items():
167
+ context_str += f"- {name}: {contrib[:200]}...\n"
168
+
169
+ agent_prompt = (
170
+ f"You are acting as: {role_desc}\n\n"
171
+ f"Task: {task}\n\n"
172
+ f"Coordination plan: {coordination_plan}\n"
173
+ f"{context_str}\n"
174
+ f"Provide your contribution based on your role:"
175
+ )
176
+
177
+ contribution = await role.agent.generate(agent_prompt)
178
+
179
+ self._conversation.add_message(
180
+ Message(
181
+ role=MessageRole.ASSISTANT,
182
+ content=contribution,
183
+ metadata={"agent": agent_name, "role": role_desc},
184
+ )
185
+ )
186
+
187
+ return {"contribution": contribution, "agent": agent_name}
188
+
189
+ async def _coordinate_task(self, task: str) -> str:
190
+ agents_desc = "\n".join(
191
+ [f"- {name}: {role.role_description}" for name, role in self.agent_roles.items()]
192
+ )
193
+
194
+ coordination_prompt = (
195
+ f"Task: {task}\n\n"
196
+ f"Available agents:\n{agents_desc}\n\n"
197
+ "Create a coordination plan describing how these agents should work together "
198
+ "to accomplish the task. Be specific about what each agent should focus on."
199
+ )
200
+
201
+ plan = await self.coordinator.generate(coordination_prompt)
202
+ self._conversation.add_message(
203
+ Message(role=MessageRole.SYSTEM, content=plan, metadata={"type": "coordination_plan"})
204
+ )
205
+
206
+ return plan
207
+
208
+ async def _synthesize_contributions(self, task: str, contributions: Dict[str, str]) -> str:
209
+ synthesis_prompt = f"Original task: {task}\n\nContributions from each agent:\n"
210
+
211
+ for agent_name, contribution in contributions.items():
212
+ role_desc = self.agent_roles[agent_name].role_description
213
+ synthesis_prompt += f"\n{agent_name} ({role_desc}):\n{contribution}\n"
214
+
215
+ synthesis_prompt += (
216
+ "\nSynthesize these contributions into a comprehensive final answer. "
217
+ "Integrate insights from all agents and resolve any conflicts or overlaps."
218
+ )
219
+
220
+ final_answer = await self.coordinator.generate(synthesis_prompt)
221
+
222
+ self._conversation.add_message(
223
+ Message(
224
+ role=MessageRole.ASSISTANT,
225
+ content=final_answer,
226
+ metadata={"type": "final_synthesis"},
227
+ )
228
+ )
229
+
230
+ return final_answer
231
+
232
+ def get_agent_contributions(self, agent_name: str) -> List[str]:
233
+ role = self.agent_roles.get(agent_name)
234
+ return role.contributions.copy() if role else []
235
+
236
+ def get_conversation(self) -> Conversation:
237
+ return self._conversation
@@ -0,0 +1,219 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from ..core.base import BasePattern, PatternConfig, PatternResult, PatternType
3
+ from ..core.common import Message, MessageRole, Conversation, State, StateStatus
4
+ from ..core.utils import get_logger
5
+
6
+ logger = get_logger()
7
+
8
+
9
+ class PlanStep:
10
+ """
11
+ Represents a single step in a planning execution.
12
+
13
+ :param step_num: int - The step number in the overall plan.
14
+ :param description: str - Description of what this step accomplishes.
15
+ :param status: str - Current status: 'pending', 'completed', or 'failed'. Defaults to 'pending'.
16
+ """
17
+
18
+ def __init__(self, step_num: int, description: str, status: str = "pending"):
19
+ self.step_num = step_num
20
+ self.description = description
21
+ self.status = status
22
+ self.result: Optional[str] = None
23
+
24
+ def __repr__(self) -> str:
25
+ return f"Step {self.step_num}: {self.description} [{self.status}]"
26
+
27
+
28
+ class PlanningPattern(BasePattern):
29
+ """
30
+ Planning pattern for decomposing complex tasks into executable steps.
31
+
32
+ This pattern implements a plan-and-execute approach where the agent first
33
+ generates a structured plan breaking down the task into discrete steps,
34
+ then executes each step sequentially, and finally synthesizes the results.
35
+
36
+ The planning pattern is ideal for:
37
+ - Complex multi-step tasks requiring coordination
38
+ - Tasks benefiting from upfront decomposition and strategy
39
+ - Long-running workflows where progress tracking is important
40
+ - Tasks where step dependencies need explicit management
41
+
42
+ The pattern follows this workflow:
43
+ 1. **Plan Generation**: Agent creates a structured plan with numbered steps
44
+ 2. **Sequential Execution**: Each step is executed in order
45
+ 3. **Result Synthesis**: Final answer integrates all step results
46
+
47
+ This approach provides transparency, debuggability, and the ability to
48
+ resume or modify execution mid-workflow.
49
+ """
50
+
51
+ def __init__(self, agent, config: Optional[PatternConfig] = None):
52
+ """
53
+ Initialize the Planning pattern.
54
+
55
+ :param agent: BaseAgent - The agent to use for plan generation and step execution.
56
+ :param config: Optional[PatternConfig] - Pattern configuration. If None, uses default with max_iterations=20.
57
+ """
58
+ if config is None:
59
+ config = PatternConfig(
60
+ name="planning", pattern_type=PatternType.PLANNING, max_iterations=20
61
+ )
62
+ super().__init__(config)
63
+ self.agent = agent
64
+ self._conversation = Conversation(id=f"planning-{id(self)}")
65
+ self._state = State()
66
+ self._plan: List[PlanStep] = []
67
+
68
+ async def execute(self, task: str, context: Optional[Dict[str, Any]] = None) -> PatternResult:
69
+ """
70
+ Execute the planning pattern on the given task.
71
+
72
+ The method first generates a plan by decomposing the task, then executes
73
+ each step sequentially, and finally synthesizes all step results into a
74
+ coherent final answer.
75
+
76
+ :param task: str - The complex task to decompose and execute.
77
+ :param context: Optional[Dict[str, Any]] - Additional context for execution. Defaults to None.
78
+ :return: PatternResult - Contains success status, final answer, plan steps, and execution metadata.
79
+ :raises PatternExecutionError: If the planning pattern execution fails.
80
+
81
+ Example:
82
+ >>> pattern = PlanningPattern(agent=my_agent)
83
+ >>> result = await pattern.execute("Research and summarize the history of AI from 1950 to 2020")
84
+ >>> print(f"Answer: {result.result}")
85
+ >>> print(f"Plan steps: {result.metadata['plan']}")
86
+ """
87
+ self.reset()
88
+ self._plan.clear()
89
+ self._state.update_status(StateStatus.RUNNING)
90
+
91
+ logger.info(f"Starting planning pattern for task: {task[:50]}...")
92
+
93
+ try:
94
+ plan = await self._create_plan(task)
95
+ self._plan = plan
96
+ logger.info(f"Created plan with {len(plan)} steps")
97
+
98
+ results = []
99
+ for step in plan:
100
+ if self.current_iteration >= self.config.max_iterations:
101
+ logger.warning("Reached max iterations during plan execution")
102
+ break
103
+
104
+ self.increment_iteration()
105
+ logger.debug(f"Executing step {step.step_num}: {step.description}")
106
+
107
+ state_data = {
108
+ "iteration": self.current_iteration,
109
+ "current_step": step.step_num,
110
+ "step_description": step.description,
111
+ "previous_results": results,
112
+ }
113
+
114
+ step_result = await self.step(state_data)
115
+ step.result = step_result.get("result")
116
+ step.status = "completed" if step_result.get("success") else "failed"
117
+ results.append(step.result)
118
+
119
+ if not step_result.get("success"):
120
+ logger.error(f"Step {step.step_num} failed: {step.result}")
121
+ break
122
+
123
+ final_result = await self._synthesize_results(task, results)
124
+
125
+ self._state.update_status(StateStatus.COMPLETED)
126
+
127
+ return PatternResult(
128
+ success=True,
129
+ result=final_result,
130
+ iterations=self.current_iteration,
131
+ metadata={
132
+ "plan": [str(s) for s in self._plan],
133
+ "step_results": results,
134
+ "completed_steps": sum(1 for s in self._plan if s.status == "completed"),
135
+ },
136
+ )
137
+
138
+ except Exception as e:
139
+ logger.error(f"Planning pattern failed: {str(e)}")
140
+ self._state.update_status(StateStatus.FAILED)
141
+ return PatternResult(
142
+ success=False, result=None, iterations=self.current_iteration, error=str(e)
143
+ )
144
+
145
+ async def step(self, state: Dict[str, Any]) -> Dict[str, Any]:
146
+ step_num = state.get("current_step")
147
+ description = state.get("step_description")
148
+ previous_results = state.get("previous_results", [])
149
+
150
+ context_str = ""
151
+ if previous_results:
152
+ context_str = "\n\nPrevious results:\n"
153
+ for i, res in enumerate(previous_results, 1):
154
+ context_str += f"Step {i}: {res}\n"
155
+
156
+ prompt = f"Execute the following step:\n{description}{context_str}"
157
+
158
+ try:
159
+ result = await self.agent.generate(prompt)
160
+ self._conversation.add_message(
161
+ Message(role=MessageRole.ASSISTANT, content=result, metadata={"step": step_num})
162
+ )
163
+ return {"success": True, "result": result}
164
+ except Exception as e:
165
+ return {"success": False, "result": str(e)}
166
+
167
+ async def _create_plan(self, task: str) -> List[PlanStep]:
168
+ planning_prompt = (
169
+ f"Create a detailed step-by-step plan to accomplish this task:\n{task}\n\n"
170
+ "Provide your plan as a numbered list. Each step should be clear and actionable.\n"
171
+ "Format:\n1. <step description>\n2. <step description>\netc."
172
+ )
173
+
174
+ self._conversation.add_message(Message(role=MessageRole.USER, content=planning_prompt))
175
+ plan_response = await self.agent.generate(planning_prompt)
176
+ self._conversation.add_message(Message(role=MessageRole.ASSISTANT, content=plan_response))
177
+
178
+ plan = self._parse_plan(plan_response)
179
+ return plan
180
+
181
+ def _parse_plan(self, plan_text: str) -> List[PlanStep]:
182
+ steps = []
183
+ lines = plan_text.strip().split("\n")
184
+
185
+ for line in lines:
186
+ line = line.strip()
187
+ if not line:
188
+ continue
189
+
190
+ if line[0].isdigit() and ("." in line or ")" in line):
191
+ parts = line.split(".", 1) if "." in line else line.split(")", 1)
192
+ if len(parts) == 2:
193
+ try:
194
+ step_num = int(parts[0].strip())
195
+ description = parts[1].strip()
196
+ steps.append(PlanStep(step_num, description))
197
+ except ValueError:
198
+ continue
199
+
200
+ if not steps:
201
+ steps.append(PlanStep(1, plan_text))
202
+
203
+ return steps
204
+
205
+ async def _synthesize_results(self, task: str, results: List[str]) -> str:
206
+ synthesis_prompt = f"Original task: {task}\n\nResults from each step:\n"
207
+ for i, result in enumerate(results, 1):
208
+ synthesis_prompt += f"\nStep {i}: {result}\n"
209
+
210
+ synthesis_prompt += "\nProvide a final synthesized answer based on all the steps:"
211
+
212
+ final_answer = await self.agent.generate(synthesis_prompt)
213
+ return final_answer
214
+
215
+ def get_plan(self) -> List[PlanStep]:
216
+ return self._plan.copy()
217
+
218
+ def get_conversation(self) -> Conversation:
219
+ return self._conversation
@@ -0,0 +1,221 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from ..core.base import BasePattern, BaseTool, PatternConfig, PatternResult, PatternType
3
+ from ..core.common import Message, MessageRole, Conversation, State, StateStatus
4
+ from ..core.utils import get_logger
5
+
6
+ logger = get_logger()
7
+
8
+
9
+ class ReActPattern(BasePattern):
10
+ """
11
+ ReAct (Reasoning and Acting) pattern for structured agent decision-making.
12
+
13
+ This pattern implements the ReAct framework that interleaves reasoning traces
14
+ and task-specific actions in a structured loop. The agent follows a think-act-observe
15
+ cycle to solve complex tasks requiring iterative reasoning and tool use.
16
+
17
+ The ReAct pattern is particularly effective for:
18
+ - Multi-step reasoning tasks requiring careful thought
19
+ - Tasks requiring dynamic tool selection based on intermediate results
20
+ - Debugging and transparency (reasoning traces provide explainability)
21
+ - Complex problem-solving requiring iterative refinement
22
+
23
+ The pattern follows this cycle:
24
+ 1. **Thought**: Agent reasons about the current state and next steps
25
+ 2. **Action**: Agent selects and executes a tool or provides final answer
26
+ 3. **Observation**: Results are integrated and inform next thought
27
+
28
+ Reference: Yao et al. (2022) "ReAct: Synergizing Reasoning and Acting in Language Models"
29
+ """
30
+
31
+ def __init__(
32
+ self, agent, tools: Optional[List[BaseTool]] = None, config: Optional[PatternConfig] = None
33
+ ):
34
+ """
35
+ Initialize the ReAct pattern.
36
+
37
+ :param agent: BaseAgent - The agent to use for reasoning and action generation.
38
+ :param tools: Optional[List[BaseTool]] - List of tools available to the agent. Defaults to None.
39
+ :param config: Optional[PatternConfig] - Pattern configuration. If None, uses default with max_iterations=10.
40
+ """
41
+ if config is None:
42
+ config = PatternConfig(name="react", pattern_type=PatternType.REACT, max_iterations=10)
43
+ super().__init__(config)
44
+ self.agent = agent
45
+ self.tools = {tool.name: tool for tool in (tools or [])}
46
+ self._conversation = Conversation(id=f"react-{id(self)}")
47
+ self._state = State()
48
+ self._thought_history: List[str] = []
49
+ self._action_history: List[str] = []
50
+ self._observation_history: List[str] = []
51
+
52
+ async def execute(self, task: str, context: Optional[Dict[str, Any]] = None) -> PatternResult:
53
+ """
54
+ Execute the ReAct pattern on the given task.
55
+
56
+ The method orchestrates the think-act-observe cycle where the agent iteratively
57
+ reasons about the task, selects actions (tool calls), observes results, and
58
+ refines its approach until reaching a final answer.
59
+
60
+ :param task: str - The task or question to solve using ReAct reasoning.
61
+ :param context: Optional[Dict[str, Any]] - Additional context for execution. Defaults to None.
62
+ :return: PatternResult - Contains success status, final answer, and complete reasoning trace.
63
+ :raises PatternExecutionError: If the ReAct pattern execution fails.
64
+
65
+ Example:
66
+ >>> tools = [SearchTool(), CalculatorTool()]
67
+ >>> pattern = ReActPattern(agent=my_agent, tools=tools)
68
+ >>> result = await pattern.execute("What's the population of Tokyo times 2?")
69
+ >>> print(f"Answer: {result.result}")
70
+ >>> print(f"Reasoning trace: {result.metadata['thoughts']}")
71
+ """
72
+ self.reset()
73
+ self._thought_history.clear()
74
+ self._action_history.clear()
75
+ self._observation_history.clear()
76
+ self._state.update_status(StateStatus.RUNNING)
77
+
78
+ logger.info(f"Starting ReAct pattern for task: {task[:50]}...")
79
+
80
+ try:
81
+ system_prompt = self._build_system_prompt()
82
+ self._conversation.add_message(Message(role=MessageRole.SYSTEM, content=system_prompt))
83
+ self._conversation.add_message(Message(role=MessageRole.USER, content=f"Task: {task}"))
84
+
85
+ final_answer = None
86
+
87
+ while self.current_iteration < self.config.max_iterations:
88
+ self.increment_iteration()
89
+ logger.debug(
90
+ f"ReAct iteration {self.current_iteration}/{self.config.max_iterations}"
91
+ )
92
+
93
+ state_data = {
94
+ "iteration": self.current_iteration,
95
+ "task": task,
96
+ "history": {
97
+ "thoughts": self._thought_history,
98
+ "actions": self._action_history,
99
+ "observations": self._observation_history,
100
+ },
101
+ }
102
+
103
+ step_result = await self.step(state_data)
104
+
105
+ if step_result.get("is_final"):
106
+ final_answer = step_result.get("answer")
107
+ logger.info(f"ReAct completed at iteration {self.current_iteration}")
108
+ break
109
+
110
+ self._state.update_status(StateStatus.COMPLETED)
111
+
112
+ return PatternResult(
113
+ success=True,
114
+ result=final_answer or "No final answer reached",
115
+ iterations=self.current_iteration,
116
+ metadata={
117
+ "thoughts": self._thought_history,
118
+ "actions": self._action_history,
119
+ "observations": self._observation_history,
120
+ },
121
+ )
122
+
123
+ except Exception as e:
124
+ logger.error(f"ReAct pattern failed: {str(e)}")
125
+ self._state.update_status(StateStatus.FAILED)
126
+ return PatternResult(
127
+ success=False, result=None, iterations=self.current_iteration, error=str(e)
128
+ )
129
+
130
+ async def step(self, state: Dict[str, Any]) -> Dict[str, Any]:
131
+ history = state.get("history", {})
132
+
133
+ prompt = self._build_step_prompt(history)
134
+ response = await self.agent.generate(prompt)
135
+
136
+ thought, action, observation, is_final, answer = self._parse_response(response)
137
+
138
+ if thought:
139
+ self._thought_history.append(thought)
140
+ logger.debug(f"Thought: {thought[:100]}...")
141
+
142
+ if action:
143
+ self._action_history.append(action)
144
+ logger.debug(f"Action: {action[:100]}...")
145
+
146
+ if not is_final:
147
+ obs = await self._execute_action(action)
148
+ self._observation_history.append(obs)
149
+ logger.debug(f"Observation: {obs[:100]}...")
150
+
151
+ return {
152
+ "is_final": is_final,
153
+ "answer": answer,
154
+ "thought": thought,
155
+ "action": action,
156
+ "observation": observation,
157
+ }
158
+
159
+ def _build_system_prompt(self) -> str:
160
+ tools_desc = self._get_tools_description() if self.tools else "No tools available."
161
+ return (
162
+ "You are a reasoning agent following the ReAct pattern.\n"
163
+ "For each step, provide your response in this format:\n\n"
164
+ "Thought: <your reasoning about what to do next>\n"
165
+ "Action: <the action to take or ANSWER if you have the final answer>\n"
166
+ "Observation: <what you learned from the action>\n\n"
167
+ f"Available tools:\n{tools_desc}\n\n"
168
+ "When you have the final answer, use: Action: ANSWER <your answer>"
169
+ )
170
+
171
+ def _build_step_prompt(self, history: Dict[str, Any]) -> str:
172
+ thoughts = history.get("thoughts", [])
173
+ actions = history.get("actions", [])
174
+ observations = history.get("observations", [])
175
+
176
+ history_str = ""
177
+ for i, (t, a, o) in enumerate(zip(thoughts, actions, observations)):
178
+ history_str += f"\nStep {i + 1}:\nThought: {t}\nAction: {a}\nObservation: {o}\n"
179
+
180
+ return f"Previous steps:{history_str}\n\nProvide the next step:"
181
+
182
+ def _parse_response(self, response: str) -> tuple:
183
+ thought = ""
184
+ action = ""
185
+ observation = ""
186
+ is_final = False
187
+ answer = None
188
+
189
+ lines = response.strip().split("\n")
190
+ for line in lines:
191
+ if line.startswith("Thought:"):
192
+ thought = line[len("Thought:") :].strip()
193
+ elif line.startswith("Action:"):
194
+ action = line[len("Action:") :].strip()
195
+ if action.startswith("ANSWER"):
196
+ is_final = True
197
+ answer = action[len("ANSWER") :].strip()
198
+ elif line.startswith("Observation:"):
199
+ observation = line[len("Observation:") :].strip()
200
+
201
+ return thought, action, observation, is_final, answer
202
+
203
+ async def _execute_action(self, action: str) -> str:
204
+ parts = action.split(maxsplit=1)
205
+ tool_name = parts[0]
206
+ tool_input = parts[1] if len(parts) > 1 else ""
207
+
208
+ if tool_name in self.tools:
209
+ result = await self.tools[tool_name].execute(input=tool_input)
210
+ return str(result.result) if result.success else f"Error: {result.error}"
211
+ else:
212
+ return f"Tool '{tool_name}' not found"
213
+
214
+ def _get_tools_description(self) -> str:
215
+ if not self.tools:
216
+ return "None"
217
+ descriptions = [f"- {tool.name}: {tool.description}" for tool in self.tools.values()]
218
+ return "\n".join(descriptions)
219
+
220
+ def get_conversation(self) -> Conversation:
221
+ return self._conversation