From de47df3bf91d4475b3c51207963f7b388c9b901e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 May 2025 15:10:12 +0100 Subject: [PATCH] fix: enhance ReActAgent's reasoning iterations and update planning prompt structure --- application/agents/react_agent.py | 117 ++++++++++-------- application/prompts/react_planning_prompt.txt | 6 +- 2 files changed, 70 insertions(+), 53 deletions(-) diff --git a/application/agents/react_agent.py b/application/agents/react_agent.py index 703f980f..60646492 100644 --- a/application/agents/react_agent.py +++ b/application/agents/react_agent.py @@ -20,6 +20,8 @@ with open( "r", ) as f: final_prompt_template = f.read() + +MAX_ITERATIONS_REASONING = 10 class ReActAgent(BaseAgent): def __init__(self, *args, **kwargs): @@ -71,7 +73,6 @@ class ReActAgent(BaseAgent): content_piece = chunk.delta.text elif isinstance(chunk, str): # Simplest case: stream of strings content_piece = chunk - # Add other stream chunk formats as needed if content_piece: collected_content.append(content_piece) @@ -98,64 +99,76 @@ class ReActAgent(BaseAgent): self._prepare_tools(tools_dict) docs_together = "\n".join([doc["text"] for doc in retrieved_data]) + iterating_reasoning = 0 + while iterating_reasoning < MAX_ITERATIONS_REASONING: + iterating_reasoning += 1 + # 1. Create Plan + logger.info("ReActAgent: Creating plan...") + plan_stream = self._create_plan(query, docs_together, log_context) + current_plan_parts = [] + yield {"thought": f"Reasoning... (iteration {iterating_reasoning})\n\n"} + for line_chunk in plan_stream: + current_plan_parts.append(line_chunk) + yield {"thought": line_chunk} + self.plan = "".join(current_plan_parts) + if self.plan: + self.observations.append(f"Plan: {self.plan} Iteration: {iterating_reasoning}") - # 1. Create Plan - logger.info("ReActAgent: Creating plan...") - plan_stream = self._create_plan(query, docs_together, log_context) - current_plan_parts = [] - for line_chunk in plan_stream: - current_plan_parts.append(line_chunk) - yield {"thought": line_chunk} - self.plan = "".join(current_plan_parts) - if self.plan: - self.observations.append(f"Plan: {self.plan}") - # 2. Execute Plan (First Reasoning Step) - execution_prompt_str = (self.prompt or "") + f"\n\nFollow this plan:\n{self.plan}" - - messages = self._build_messages(execution_prompt_str, query, retrieved_data) - - resp_from_llm_gen = self._llm_gen(messages, log_context) - - initial_llm_thought_content = self._extract_content_from_llm_response(resp_from_llm_gen) - if initial_llm_thought_content: - self.observations.append(f"Initial thought/response: {initial_llm_thought_content}") - else: - logger.info("ReActAgent: Initial LLM response (before handler) had no textual content (might be only tool calls).") - - logger.info("Executing plan") - resp_after_handler = self._llm_handler(resp_from_llm_gen, tools_dict, messages, log_context) - - for tool_call_info in self.tool_calls: # Iterate over self.tool_calls populated by _llm_handler - observation_string = ( - f"Executed Action: Tool '{tool_call_info.get('tool_name', 'N/A')}' " - f"with arguments '{tool_call_info.get('arguments', '{}')}'. Result: '{str(tool_call_info.get('result', ''))[:200]}...'" + max_obs_len = 20000 + obs_str = "\n".join(self.observations) + if len(obs_str) > max_obs_len: + obs_str = obs_str[:max_obs_len] + "\n...[observations truncated]" + execution_prompt_str = ( + (self.prompt or "") + + f"\n\nFollow this plan:\n{self.plan}" + + f"\n\nObservations:\n{obs_str}" + + f"\n\nIf there is enough data to complete user query '{query}', Respond with 'SATISFIED' only. Otherwise, continue. Dont Menstion 'SATISFIED' in your response if you are not ready. " ) - self.observations.append(observation_string) + + messages = self._build_messages(execution_prompt_str, query, retrieved_data) - content_after_handler = self._extract_content_from_llm_response(resp_after_handler) - if content_after_handler: - self.observations.append(f"Response after tool execution: {content_after_handler}") - logger.info(f"ReActAgent: LLM response after tool execution: {content_after_handler[:500]}...") - else: - logger.info("ReActAgent: LLM response after handler had no textual content.") + resp_from_llm_gen = self._llm_gen(messages, log_context) + initial_llm_thought_content = self._extract_content_from_llm_response(resp_from_llm_gen) + if initial_llm_thought_content: + self.observations.append(f"Initial thought/response: {initial_llm_thought_content}") + else: + logger.info("ReActAgent: Initial LLM response (before handler) had no textual content (might be only tool calls).") + resp_after_handler = self._llm_handler(resp_from_llm_gen, tools_dict, messages, log_context) + + for tool_call_info in self.tool_calls: # Iterate over self.tool_calls populated by _llm_handler + observation_string = ( + f"Executed Action: Tool '{tool_call_info.get('tool_name', 'N/A')}' " + f"with arguments '{tool_call_info.get('arguments', '{}')}'. Result: '{str(tool_call_info.get('result', ''))[:200]}...'" + ) + self.observations.append(observation_string) - if log_context: - log_context.stacks.append( - {"component": "agent_tool_calls", "data": {"tool_calls": self.tool_calls.copy()}} - ) + content_after_handler = self._extract_content_from_llm_response(resp_after_handler) + if content_after_handler: + self.observations.append(f"Response after tool execution: {content_after_handler}") + else: + logger.info("ReActAgent: LLM response after handler had no textual content.") - yield {"sources": retrieved_data} + if log_context: + log_context.stacks.append( + {"component": "agent_tool_calls", "data": {"tool_calls": self.tool_calls.copy()}} + ) - display_tool_calls = [] - for tc in self.tool_calls: - cleaned_tc = tc.copy() - if len(str(cleaned_tc.get("result", ""))) > 50: - cleaned_tc["result"] = str(cleaned_tc["result"])[:50] + "..." - display_tool_calls.append(cleaned_tc) - if display_tool_calls: - yield {"tool_calls": display_tool_calls} + yield {"sources": retrieved_data} + + display_tool_calls = [] + for tc in self.tool_calls: + cleaned_tc = tc.copy() + if len(str(cleaned_tc.get("result", ""))) > 50: + cleaned_tc["result"] = str(cleaned_tc["result"])[:50] + "..." + display_tool_calls.append(cleaned_tc) + if display_tool_calls: + yield {"tool_calls": display_tool_calls} + + if "SATISFIED" in content_after_handler: + logger.info("ReActAgent: LLM satisfied with the plan and data. Stopping reasoning.") + break # 3. Create Final Answer based on all observations final_answer_stream = self._create_final_answer(query, self.observations, log_context) @@ -170,6 +183,8 @@ class ReActAgent(BaseAgent): if "{summaries}" in plan_prompt_filled: summaries = docs_data if docs_data else "No documents retrieved." plan_prompt_filled = plan_prompt_filled.replace("{summaries}", summaries) + plan_prompt_filled = plan_prompt_filled.replace("{prompt}", self.prompt or "") + plan_prompt_filled = plan_prompt_filled.replace("{observations}", "\n".join(self.observations)) messages = [{"role": "user", "content": plan_prompt_filled}] diff --git a/application/prompts/react_planning_prompt.txt b/application/prompts/react_planning_prompt.txt index 36d13d74..c3571845 100644 --- a/application/prompts/react_planning_prompt.txt +++ b/application/prompts/react_planning_prompt.txt @@ -5,7 +5,9 @@ Include the following elements in your thought and execution process: 2. Determine any relevant context or background information needed to understand the query. 3. List potential approaches or methods to address the query. 4. Highlight any critical factors or constraints that may influence the outcome. -5. Use available tools to help you with the analysis and execute them. Execute tools needed now instead of later. +5. Plan with available tools to help you with the analysis but dont execute them. Tools will be executed by another AI. Query: {query} -Summaries: {summaries} \ No newline at end of file +Summaries: {summaries} +Prompt: {prompt} +Observations(potentially previous tool calls): {observations}