from typing import List, Dict, Any, Optional, Tuple
import re
from dataclasses import dataclass
from abc import ABC, abstractmethod
@dataclass
class ReActStep:
"""Represents a single step in the ReAct cycle"""
step_type: str # 'thought', 'action', 'observation'
content: str
metadata: Dict[str, Any] = None
@dataclass
class ReActTrace:
"""Complete trace of ReAct execution"""
steps: List[ReActStep]
final_answer: Optional[str] = None
success: bool = False
class Tool(ABC):
"""Abstract base class for ReAct tools"""
@property
@abstractmethod
def name(self) -> str:
pass
@property
@abstractmethod
def description(self) -> str:
pass
@abstractmethod
async def execute(self, input_str: str) -> str:
pass
class SearchTool(Tool):
"""Example search tool implementation"""
@property
def name(self) -> str:
return "search"
@property
def description(self) -> str:
return "Search for information on the internet. Input should be a search query."
async def execute(self, input_str: str) -> str:
# Simulate search API call
return f"Search results for '{input_str}': [Relevant information found...]"
class CalculatorTool(Tool):
"""Example calculator tool implementation"""
@property
def name(self) -> str:
return "calculator"
@property
def description(self) -> str:
return "Perform mathematical calculations. Input should be a mathematical expression."
async def execute(self, input_str: str) -> str:
try:
# Safe evaluation of mathematical expressions
result = eval(input_str.replace("^", "**"))
return f"Result: {result}"
except Exception as e:
return f"Error: {str(e)}"
class ReActAgent:
"""ReAct pattern implementation for intelligent agents"""
def __init__(self,
llm_client,
tools: List[Tool],
max_iterations: int = 10,
verbose: bool = True):
self.llm = llm_client
self.tools = {tool.name: tool for tool in tools}
self.max_iterations = max_iterations
self.verbose = verbose
# ReAct prompt template
self.react_prompt = self._build_react_prompt()
def _build_react_prompt(self) -> str:
"""Build the ReAct prompt template"""
tool_descriptions = "\n".join([
f"- {tool.name}: {tool.description}"
for tool in self.tools.values()
])
return f"""You are an intelligent agent that can reason and act to solve problems.
Available tools:
{tool_descriptions}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{', '.join(self.tools.keys())}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {{question}}"""
async def run(self, question: str) -> ReActTrace:
"""Execute ReAct cycle to answer a question"""
trace = ReActTrace(steps=[])
# Initialize the prompt with the question
prompt = self.react_prompt.format(question=question)
current_context = prompt
for iteration in range(self.max_iterations):
if self.verbose:
print(f"\n--- Iteration {iteration + 1} ---")
# Generate reasoning and action
response = await self.llm.generate(current_context)
# Parse the response
thought, action, action_input, final_answer = self._parse_response(response)
if thought:
step = ReActStep("thought", thought)
trace.steps.append(step)
current_context += f"\nThought: {thought}"
if self.verbose:
print(f"Thought: {thought}")
if final_answer:
trace.final_answer = final_answer
trace.success = True
if self.verbose:
print(f"Final Answer: {final_answer}")
break
if action and action_input:
# Record the action
step = ReActStep("action", f"{action}: {action_input}")
trace.steps.append(step)
current_context += f"\nAction: {action}\nAction Input: {action_input}"
if self.verbose:
print(f"Action: {action}")
print(f"Action Input: {action_input}")
# Execute the action
if action in self.tools:
try:
observation = await self.tools[action].execute(action_input)
except Exception as e:
observation = f"Error executing {action}: {str(e)}"
else:
observation = f"Error: Unknown action '{action}'"
# Record the observation
step = ReActStep("observation", observation)
trace.steps.append(step)
current_context += f"\nObservation: {observation}"
if self.verbose:
print(f"Observation: {observation}")
else:
# If no valid action is found, break to avoid infinite loop
if self.verbose:
print("No valid action found, ending execution")
break
if not trace.success:
trace.final_answer = "Unable to determine the answer within the iteration limit"
return trace
def _parse_response(self, response: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
"""Parse LLM response to extract thought, action, action input, and final answer"""
thought = None
action = None
action_input = None
final_answer = None
# Extract thought
thought_match = re.search(r"Thought:\s*(.*?)(?=\n(?:Action|Final Answer)|\Z)", response, re.DOTALL)
if thought_match:
thought = thought_match.group(1).strip()
# Extract final answer
final_answer_match = re.search(r"Final Answer:\s*(.*)", response, re.DOTALL)
if final_answer_match:
final_answer = final_answer_match.group(1).strip()
return thought, action, action_input, final_answer
# Extract action
action_match = re.search(r"Action:\s*(.*?)(?=\n|\Z)", response)
if action_match:
action = action_match.group(1).strip()
# Extract action input
action_input_match = re.search(r"Action Input:\s*(.*?)(?=\n|\Z)", response)
if action_input_match:
action_input = action_input_match.group(1).strip()
return thought, action, action_input, final_answer
def get_trace_summary(self, trace: ReActTrace) -> Dict[str, Any]:
"""Generate a summary of the ReAct trace"""
return {
"total_steps": len(trace.steps),
"thoughts": len([s for s in trace.steps if s.step_type == "thought"]),
"actions": len([s for s in trace.steps if s.step_type == "action"]),
"observations": len([s for s in trace.steps if s.step_type == "observation"]),
"success": trace.success,
"final_answer": trace.final_answer
}
# Advanced ReAct implementation with memory and planning
class AdvancedReActAgent(ReActAgent):
"""Enhanced ReAct agent with memory and planning capabilities"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.memory = [] # Store previous interactions
self.planning_enabled = True
async def run_with_planning(self, question: str) -> ReActTrace:
"""Execute ReAct with initial planning phase"""
if self.planning_enabled:
# Generate initial plan
plan_prompt = f"""Given the question: "{question}"
Create a high-level plan to solve this problem. Consider:
1. What information do you need?
2. What tools might be useful?
3. What are the key steps?
Plan:"""
plan_response = await self.llm.generate(plan_prompt)
if self.verbose:
print(f"Initial Plan: {plan_response}")
# Store plan in memory
self.memory.append({
"type": "plan",
"question": question,
"plan": plan_response
})
# Execute normal ReAct cycle
trace = await self.run(question)
# Store trace in memory
self.memory.append({
"type": "execution",
"question": question,
"trace": trace
})
return trace
def get_memory_context(self) -> str:
"""Get relevant context from memory"""
if not self.memory:
return ""
context = "Previous interactions:\n"
for item in self.memory[-3:]: # Last 3 interactions
if item["type"] == "execution":
context += f"Q: {item['question']}\nA: {item['trace'].final_answer}\n\n"
return context
# Usage example
async def main():
# Initialize tools
tools = [
SearchTool(),
CalculatorTool()
]
# Create ReAct agent
agent = AdvancedReActAgent(
llm_client=your_llm_client,
tools=tools,
max_iterations=10,
verbose=True
)
# Execute query
question = "What is the population of Tokyo and how does it compare to New York?"
trace = await agent.run_with_planning(question)
# Print results
print(f"\nFinal Answer: {trace.final_answer}")
print(f"Execution Summary: {agent.get_trace_summary(trace)}")
# Error handling and recovery
class RobustReActAgent(AdvancedReActAgent):
"""ReAct agent with enhanced error handling and recovery"""
async def run(self, question: str) -> ReActTrace:
"""Execute ReAct with error recovery"""
max_retries = 3
for attempt in range(max_retries):
try:
trace = await super().run(question)
# Validate the trace
if self._validate_trace(trace):
return trace
else:
if attempt < max_retries - 1:
if self.verbose:
print(f"Trace validation failed, retrying... (attempt {attempt + 1})")
continue
else:
trace.final_answer = "Unable to generate a valid solution"
return trace
except Exception as e:
if attempt < max_retries - 1:
if self.verbose:
print(f"Error occurred: {e}, retrying... (attempt {attempt + 1})")
continue
else:
# Return error trace
error_trace = ReActTrace(
steps=[ReActStep("error", f"Execution failed: {str(e)}")],
final_answer="Error occurred during execution",
success=False
)
return error_trace
def _validate_trace(self, trace: ReActTrace) -> bool:
"""Validate the quality of a ReAct trace"""
if not trace.success or not trace.final_answer:
return False
# Check for reasonable number of steps
if len(trace.steps) < 2:
return False
# Check for balanced reasoning and action
thoughts = len([s for s in trace.steps if s.step_type == "thought"])
actions = len([s for s in trace.steps if s.step_type == "action"])
if thoughts == 0 or actions == 0:
return False
return True
if __name__ == "__main__":
import asyncio
asyncio.run(main())