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.
- pygeai_orchestration/__init__.py +99 -0
- pygeai_orchestration/cli/__init__.py +7 -0
- pygeai_orchestration/cli/__main__.py +11 -0
- pygeai_orchestration/cli/commands/__init__.py +13 -0
- pygeai_orchestration/cli/commands/base.py +192 -0
- pygeai_orchestration/cli/error_handler.py +123 -0
- pygeai_orchestration/cli/formatters.py +419 -0
- pygeai_orchestration/cli/geai_orch.py +270 -0
- pygeai_orchestration/cli/interactive.py +265 -0
- pygeai_orchestration/cli/texts/help.py +169 -0
- pygeai_orchestration/core/__init__.py +130 -0
- pygeai_orchestration/core/base/__init__.py +23 -0
- pygeai_orchestration/core/base/agent.py +121 -0
- pygeai_orchestration/core/base/geai_agent.py +144 -0
- pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
- pygeai_orchestration/core/base/orchestrator.py +142 -0
- pygeai_orchestration/core/base/pattern.py +161 -0
- pygeai_orchestration/core/base/tool.py +149 -0
- pygeai_orchestration/core/common/__init__.py +18 -0
- pygeai_orchestration/core/common/context.py +140 -0
- pygeai_orchestration/core/common/memory.py +176 -0
- pygeai_orchestration/core/common/message.py +50 -0
- pygeai_orchestration/core/common/state.py +181 -0
- pygeai_orchestration/core/composition.py +190 -0
- pygeai_orchestration/core/config.py +356 -0
- pygeai_orchestration/core/exceptions.py +400 -0
- pygeai_orchestration/core/handlers.py +380 -0
- pygeai_orchestration/core/utils/__init__.py +37 -0
- pygeai_orchestration/core/utils/cache.py +138 -0
- pygeai_orchestration/core/utils/config.py +94 -0
- pygeai_orchestration/core/utils/logging.py +57 -0
- pygeai_orchestration/core/utils/metrics.py +184 -0
- pygeai_orchestration/core/utils/validators.py +140 -0
- pygeai_orchestration/dev/__init__.py +15 -0
- pygeai_orchestration/dev/debug.py +288 -0
- pygeai_orchestration/dev/templates.py +321 -0
- pygeai_orchestration/dev/testing.py +301 -0
- pygeai_orchestration/patterns/__init__.py +15 -0
- pygeai_orchestration/patterns/multi_agent.py +237 -0
- pygeai_orchestration/patterns/planning.py +219 -0
- pygeai_orchestration/patterns/react.py +221 -0
- pygeai_orchestration/patterns/reflection.py +134 -0
- pygeai_orchestration/patterns/tool_use.py +170 -0
- pygeai_orchestration/tests/__init__.py +1 -0
- pygeai_orchestration/tests/test_base_classes.py +187 -0
- pygeai_orchestration/tests/test_cache.py +184 -0
- pygeai_orchestration/tests/test_cli_formatters.py +232 -0
- pygeai_orchestration/tests/test_common.py +214 -0
- pygeai_orchestration/tests/test_composition.py +265 -0
- pygeai_orchestration/tests/test_config.py +301 -0
- pygeai_orchestration/tests/test_dev_utils.py +337 -0
- pygeai_orchestration/tests/test_exceptions.py +327 -0
- pygeai_orchestration/tests/test_handlers.py +307 -0
- pygeai_orchestration/tests/test_metrics.py +171 -0
- pygeai_orchestration/tests/test_patterns.py +165 -0
- pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
- pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
- pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
- pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
- pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
- 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
|