Coverage for src / dataknobs_bots / reasoning / react.py: 12%
73 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-16 10:13 -0700
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-16 10:13 -0700
1"""ReAct (Reasoning + Acting) reasoning strategy."""
3import logging
4from typing import Any
6from .base import ReasoningStrategy
8logger = logging.getLogger(__name__)
11class ReActReasoning(ReasoningStrategy):
12 """ReAct (Reasoning + Acting) strategy.
14 This strategy implements the ReAct pattern where the LLM:
15 1. Reasons about what to do (Thought)
16 2. Takes an action (using tools if needed)
17 3. Observes the result
18 4. Repeats until task is complete
20 This is useful for:
21 - Multi-step problem solving
22 - Tasks requiring tool use
23 - Complex reasoning chains
25 Attributes:
26 max_iterations: Maximum number of reasoning loops
27 verbose: Whether to enable debug-level logging
28 store_trace: Whether to store reasoning trace in conversation metadata
30 Example:
31 ```python
32 strategy = ReActReasoning(
33 max_iterations=5,
34 verbose=True,
35 store_trace=True
36 )
37 response = await strategy.generate(
38 manager=conversation_manager,
39 llm=llm_provider,
40 tools=[search_tool, calculator_tool]
41 )
42 ```
43 """
45 def __init__(
46 self,
47 max_iterations: int = 5,
48 verbose: bool = False,
49 store_trace: bool = False,
50 ):
51 """Initialize ReAct reasoning strategy.
53 Args:
54 max_iterations: Maximum reasoning/action iterations
55 verbose: Enable debug-level logging for reasoning steps
56 store_trace: Store reasoning trace in conversation metadata
57 """
58 self.max_iterations = max_iterations
59 self.verbose = verbose
60 self.store_trace = store_trace
62 async def generate(
63 self,
64 manager: Any,
65 llm: Any,
66 tools: list[Any] | None = None,
67 **kwargs: Any,
68 ) -> Any:
69 """Generate response using ReAct loop.
71 The ReAct loop:
72 1. Generate response (may include tool calls)
73 2. If tool calls present, execute them
74 3. Add observations to conversation
75 4. Repeat until no more tool calls or max iterations
77 Args:
78 manager: ConversationManager instance
79 llm: LLM provider instance
80 tools: Optional list of available tools
81 **kwargs: Generation parameters
83 Returns:
84 Final LLM response
85 """
86 if not tools:
87 # No tools available, fall back to simple generation
88 logger.info(
89 "ReAct: No tools available, falling back to simple generation",
90 extra={"conversation_id": manager.conversation_id},
91 )
92 return await manager.complete(**kwargs)
94 # Initialize trace if enabled
95 trace = [] if self.store_trace else None
97 # Get log level based on verbose setting
98 log_level = logging.DEBUG if self.verbose else logging.INFO
100 logger.log(
101 log_level,
102 "ReAct: Starting reasoning loop",
103 extra={
104 "conversation_id": manager.conversation_id,
105 "max_iterations": self.max_iterations,
106 "tools_available": len(tools),
107 },
108 )
110 # ReAct loop
111 for iteration in range(self.max_iterations):
112 iteration_trace = {
113 "iteration": iteration + 1,
114 "tool_calls": [],
115 }
117 logger.log(
118 log_level,
119 "ReAct: Starting iteration",
120 extra={
121 "conversation_id": manager.conversation_id,
122 "iteration": iteration + 1,
123 "max_iterations": self.max_iterations,
124 },
125 )
127 # Generate response with tools
128 response = await manager.complete(tools=tools, **kwargs)
130 # Check if we have tool calls
131 if not hasattr(response, "tool_calls") or not response.tool_calls:
132 # No tool calls, we're done
133 logger.log(
134 log_level,
135 "ReAct: No tool calls in response, finishing",
136 extra={
137 "conversation_id": manager.conversation_id,
138 "iteration": iteration + 1,
139 },
140 )
142 if trace is not None:
143 iteration_trace["status"] = "completed"
144 trace.append(iteration_trace)
145 await self._store_trace(manager, trace)
147 return response
149 num_tool_calls = len(response.tool_calls)
150 logger.log(
151 log_level,
152 "ReAct: Executing tool calls",
153 extra={
154 "conversation_id": manager.conversation_id,
155 "iteration": iteration + 1,
156 "num_tools": num_tool_calls,
157 "tools": [tc.name for tc in response.tool_calls],
158 },
159 )
161 # Execute all tool calls
162 for tool_call in response.tool_calls:
163 tool_trace = {
164 "name": tool_call.name,
165 "parameters": tool_call.parameters,
166 }
168 try:
169 # Find the tool
170 tool = self._find_tool(tool_call.name, tools)
171 if not tool:
172 observation = f"Error: Tool '{tool_call.name}' not found"
173 tool_trace["status"] = "error"
174 tool_trace["error"] = "Tool not found"
176 logger.warning(
177 "ReAct: Tool not found",
178 extra={
179 "conversation_id": manager.conversation_id,
180 "iteration": iteration + 1,
181 "tool_name": tool_call.name,
182 },
183 )
184 else:
185 # Execute the tool
186 result = await tool.execute(**tool_call.parameters)
187 observation = f"Tool result: {result}"
188 tool_trace["status"] = "success"
189 tool_trace["result"] = str(result)
191 logger.log(
192 log_level,
193 "ReAct: Tool executed successfully",
194 extra={
195 "conversation_id": manager.conversation_id,
196 "iteration": iteration + 1,
197 "tool_name": tool_call.name,
198 "result_length": len(str(result)),
199 },
200 )
202 # Add observation to conversation
203 await manager.add_message(
204 content=f"Observation from {tool_call.name}: {observation}",
205 role="system",
206 )
208 except Exception as e:
209 # Handle tool execution errors
210 error_msg = f"Error executing tool {tool_call.name}: {e!s}"
211 tool_trace["status"] = "error"
212 tool_trace["error"] = str(e)
214 logger.error(
215 "ReAct: Tool execution failed",
216 extra={
217 "conversation_id": manager.conversation_id,
218 "iteration": iteration + 1,
219 "tool_name": tool_call.name,
220 "error": str(e),
221 },
222 exc_info=True,
223 )
225 await manager.add_message(content=error_msg, role="system")
227 if trace is not None:
228 iteration_trace["tool_calls"].append(tool_trace)
230 if trace is not None:
231 iteration_trace["status"] = "continued"
232 trace.append(iteration_trace)
234 # Max iterations reached, generate final response without tools
235 logger.log(
236 log_level,
237 "ReAct: Max iterations reached, generating final response",
238 extra={
239 "conversation_id": manager.conversation_id,
240 "iterations_used": self.max_iterations,
241 },
242 )
244 if trace is not None:
245 trace.append({"status": "max_iterations_reached"})
246 await self._store_trace(manager, trace)
248 return await manager.complete(**kwargs)
250 async def _store_trace(self, manager: Any, trace: list[dict[str, Any]]) -> None:
251 """Store reasoning trace in conversation metadata.
253 Args:
254 manager: ConversationManager instance
255 trace: Reasoning trace data
256 """
257 try:
258 # Get existing metadata
259 metadata = manager.conversation.metadata or {}
261 # Add trace to metadata
262 metadata["reasoning_trace"] = trace
264 # Update conversation metadata
265 await manager.storage.update_metadata(
266 conversation_id=manager.conversation_id,
267 metadata=metadata,
268 )
270 logger.debug(
271 "ReAct: Stored reasoning trace in conversation metadata",
272 extra={
273 "conversation_id": manager.conversation_id,
274 "trace_items": len(trace),
275 },
276 )
277 except Exception as e:
278 logger.warning(
279 "ReAct: Failed to store reasoning trace",
280 extra={
281 "conversation_id": manager.conversation_id,
282 "error": str(e),
283 },
284 )
286 def _find_tool(self, tool_name: str, tools: list[Any]) -> Any | None:
287 """Find a tool by name.
289 Args:
290 tool_name: Name of the tool to find
291 tools: List of available tools
293 Returns:
294 Tool instance or None if not found
295 """
296 for tool in tools:
297 if tool.name == tool_name:
298 return tool
299 return None