File size: 8,171 Bytes
8bf4d58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
"""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: <your reasoning>",
            "Action: <tool_name>",
            "Action Input: <tool_parameters>",
            "Observation: <result>",
            "",
            "When you have the final answer, use:",
            "Final Answer: <your 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