"""ReAct (Reasoning + Acting) planner implementation.""" import logging from typing import List, Dict, Any, Optional, Callable from enum import Enum from src.core.config import get_settings logger = logging.getLogger(__name__) class ActionType(Enum): """Types of actions in ReAct loop.""" THOUGHT = "thought" ACTION = "action" OBSERVATION = "observation" FINAL_ANSWER = "final_answer" class ReActPlanner: """ReAct planner that implements thought-action-observation loop.""" def __init__( self, max_iterations: int = 10, tools: Optional[List[Dict[str, Any]]] = None, ): """ Initialize ReAct planner. Args: max_iterations: Maximum number of ReAct iterations tools: List of available tools with their schemas """ self.max_iterations = max_iterations self.tools = tools or [] self.tool_map = {tool["name"]: tool for tool in self.tools} def plan( self, query: str, context: Optional[str] = None, llm_call: Optional[Callable] = None, ) -> Dict[str, Any]: """ Generate a plan using ReAct methodology. Args: query: User query context: Optional context information llm_call: Function to call LLM (should return structured response) Returns: Plan dictionary with steps and reasoning """ if not llm_call: raise ValueError("llm_call function is required") steps = [] observations = [] current_context = context or "" for iteration in range(self.max_iterations): # Build prompt for this iteration prompt = self._build_react_prompt( query=query, context=current_context, steps=steps, observations=observations, ) # Get LLM response try: response = llm_call(prompt) step = self._parse_react_response(response) steps.append(step) # Check if we have a final answer if step["type"] == ActionType.FINAL_ANSWER: return { "query": query, "steps": steps, "final_answer": step.get("content", ""), "iterations": iteration + 1, } # Execute action if needed if step["type"] == ActionType.ACTION: observation = self._execute_action(step) observations.append(observation) steps.append({ "type": ActionType.OBSERVATION, "content": observation, "iteration": iteration + 1, }) except Exception as e: logger.error(f"Error in ReAct iteration {iteration}: {e}") steps.append({ "type": ActionType.OBSERVATION, "content": f"Error: {str(e)}", "iteration": iteration + 1, }) # Max iterations reached return { "query": query, "steps": steps, "final_answer": None, "iterations": self.max_iterations, "status": "max_iterations_reached", } def _build_react_prompt( self, query: str, context: str, steps: List[Dict[str, Any]], observations: List[str], ) -> str: """Build ReAct prompt.""" prompt_parts = [ "You are a helpful assistant that uses the ReAct (Reasoning + Acting) methodology.", "You can think, take actions using tools, and observe results.", "", "Available tools:", ] for tool in self.tools: prompt_parts.append(f"- {tool['name']}: {tool.get('description', '')}") if "parameters" in tool: prompt_parts.append(f" Parameters: {tool['parameters']}") prompt_parts.extend([ "", "Format your responses as:", "Thought: ", "Action: ", "Action Input: ", "Observation: ", "", "When you have the final answer, use:", "Final Answer: ", "", ]) if context: prompt_parts.extend([ f"Context: {context}", "", ]) prompt_parts.append(f"Question: {query}") prompt_parts.append("") # Add previous steps if steps: prompt_parts.append("Previous steps:") for step in steps[-3:]: # Last 3 steps for context if step["type"] == ActionType.THOUGHT: prompt_parts.append(f"Thought: {step.get('content', '')}") elif step["type"] == ActionType.ACTION: prompt_parts.append(f"Action: {step.get('action', '')}") prompt_parts.append(f"Action Input: {step.get('input', '')}") elif step["type"] == ActionType.OBSERVATION: prompt_parts.append(f"Observation: {step.get('content', '')}") prompt_parts.append("") prompt_parts.append("Your response:") return "\n".join(prompt_parts) def _parse_react_response(self, response: str) -> Dict[str, Any]: """Parse LLM response into ReAct step.""" response = response.strip() # Check for final answer if response.startswith("Final Answer:"): return { "type": ActionType.FINAL_ANSWER, "content": response.replace("Final Answer:", "").strip(), } # Parse thought if "Thought:" in response: thought_part = response.split("Thought:")[1].split("Action:")[0].strip() else: thought_part = "" # Parse action action_name = None action_input = None if "Action:" in response: action_line = response.split("Action:")[1].split("Observation:")[0].strip() if "Action Input:" in action_line: parts = action_line.split("Action Input:") action_name = parts[0].strip() action_input = parts[1].strip() else: action_name = action_line if action_name: return { "type": ActionType.ACTION, "thought": thought_part, "action": action_name, "input": action_input or "", } else: return { "type": ActionType.THOUGHT, "content": thought_part or response, } def _execute_action(self, step: Dict[str, Any]) -> str: """Execute an action using available tools.""" action_name = step.get("action") action_input = step.get("input", "") if action_name not in self.tool_map: return f"Error: Tool '{action_name}' not found" tool = self.tool_map[action_name] tool_func = tool.get("function") if not tool_func: return f"Error: Tool '{action_name}' has no implementation" try: # Parse input (assuming JSON format) import json try: params = json.loads(action_input) if action_input else {} except: params = {"query": action_input} if action_input else {} result = tool_func(**params) return str(result) except Exception as e: return f"Error executing {action_name}: {str(e)}" def add_tool(self, tool: Dict[str, Any]) -> None: """Add a tool to the planner.""" self.tools.append(tool) self.tool_map[tool["name"]] = tool def get_tools(self) -> List[Dict[str, Any]]: """Get list of available tools.""" return self.tools