From 9fd4c77c4cb3cd230d946427357a43d9fe7c5a10 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:38:29 +0530 Subject: [PATCH 01/10] implementing ReAct Agent --- camel/agents/__init__.py | 2 + camel/agents/react_agent.py | 370 ++++++++++++++++++++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 camel/agents/react_agent.py diff --git a/camel/agents/__init__.py b/camel/agents/__init__.py index 2333077714..0c80b4a04c 100644 --- a/camel/agents/__init__.py +++ b/camel/agents/__init__.py @@ -16,6 +16,7 @@ from .critic_agent import CriticAgent from .embodied_agent import EmbodiedAgent from .knowledge_graph_agent import KnowledgeGraphAgent +from .react_agent import ReActAgent from .role_assignment_agent import RoleAssignmentAgent from .search_agent import SearchAgent from .task_agent import ( @@ -41,4 +42,5 @@ 'RoleAssignmentAgent', 'SearchAgent', 'KnowledgeGraphAgent', + 'ReActAgent', ] diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py new file mode 100644 index 0000000000..61cbc16eb9 --- /dev/null +++ b/camel/agents/react_agent.py @@ -0,0 +1,370 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= + +import logging +from enum import Enum +from typing import Any, Dict, List, Optional, Tuple, Union + +from pydantic import BaseModel + +from camel.agents import ChatAgent +from camel.messages import BaseMessage +from camel.responses import ChatAgentResponse +from camel.types import RoleType + +# AgentOps decorator setting +try: + import os + + if os.getenv("AGENTOPS_API_KEY") is not None: + from agentops import track_agent + else: + raise ImportError +except (ImportError, AttributeError): + from camel.utils import track_agent + +logger = logging.getLogger(__name__) + + +class ReActActionSpace(Enum): + r"""Available actions in the ReAct framework as + defined in the original paper. + + References: + https://arxiv.org/pdf/2210.03629 + """ + + SEARCH = "Search" + LOOKUP = "Lookup" + FINISH = "Finish" + + +@track_agent(name="ReActAgent") +class ReActAgent(ChatAgent): + r"""ReAct Agent that combines reasoning and acting through: + - Thought: Reasoning about current state + - Action: Deciding what action to take + - Observation: Getting results of actions + + Args: + system_message (BaseMessage): The system message for initializing the + agent's conversation context. + model (Any, optional): The model backend to use for + response generation. Defaults to None. + tools (List[Any], optional): List of available tools for executing + actions. Defaults to None. + max_steps (int, optional): Maximum number of steps before termination. + Defaults to 10. + + References: + https://arxiv.org/pdf/2210.03629 + """ + + def __init__( + self, + system_message: BaseMessage, + model: Optional[Any] = None, + tools: Optional[List[Any]] = None, + max_steps: int = 10, + ) -> None: + super().__init__(system_message=system_message, model=model) + self.tools = tools or [] + self.scratchpad: List[Dict[str, Optional[str]]] = [] + self._set_react_prompt() + self.step_count = 0 + self.max_steps = max_steps + logger.debug("ReActAgent initialized with %d tools", len(self.tools)) + + def _set_react_prompt(self) -> None: + r"""Set up the ReAct prompt template following the paper's format. + + This method initializes the prompt template that guides the agent's + response format and behavior. + """ + self.react_prompt = ( + "Follow this STRICT format:\n\n" + "Thought: [analyze current situation]\n" + "Action: [EXACTLY ONE of these]\n" + "- Search(query=)\n" + "- Lookup(key=)\n" + "- Finish(answer=)\n\n" + "Examples:\n" + "Good: Action: Search(query=Paris population)\n" + "Bad: Action: Search Paris population\n\n" + "Rules:\n" + "1. ALWAYS validate action syntax before execution\n" + "2. If action fails, diagnose and retry\n" + "3. Use Finish() only when ready with final answer\n\n" + "Current scratchpad:\n" + "{scratchpad}" + ) + logger.debug("ReAct prompt template set") + + def _format_scratchpad(self) -> str: + r"""Format the scratchpad history for inclusion in prompts. + + Returns: + str: A formatted string containing the history of thoughts, + actions, and observations. Returns empty string if no + history exists. + """ + if not self.scratchpad: + return "" + + formatted = "Previous steps:\n" + for step in self.scratchpad: + for key, value in step.items(): + if value: + formatted += f"{key}: {value}\n" + formatted += "\n" + return formatted + + def _handle_max_steps(self) -> ChatAgentResponse: + r"""Handle the case when maximum steps are reached. + + Returns: + ChatAgentResponse: A response object containing: + - msgs: List[BaseMessage] with termination message + - terminated: Set to True + - info: Dictionary with thought, action, observation details + """ + logger.warning("Maximum steps reached, terminating execution") + final_message = BaseMessage( + role_name="Assistant", + role_type=RoleType.ASSISTANT, + meta_dict={}, + content="Maximum number of steps reached. Terminating execution.", + ) + + return ChatAgentResponse( + msgs=[final_message], + terminated=True, + info={ + "thought": "Maximum steps reached", + "action": "", + "observation": "Task terminated due to step limit", + }, + ) + + def _parse_react_components( + self, + content: str, + ) -> Tuple[Optional[str], Optional[str], Optional[str]]: + r"""Parse the response into ReAct components. + + Args: + content (str): The response content to parse. Expected to contain + sections starting with "Thought:", "Action:", and/or + "Observation:". + + Returns: + Tuple[Optional[str], Optional[str], Optional[str]]: + A tuple containing: + - thought: The parsed thought component, or None if not found + - action: The parsed action component, or None if not found + - observation: The observation, or None if not found + """ + components: Dict[str, Optional[str]] = { + "Thought": None, + "Action": None, + "Observation": None, + } + + current_component: Optional[str] = None + current_text: List[str] = [] + + for line in content.split('\n'): + for component in components: + if line.startswith(f"{component}:"): + if current_component is not None: + components[current_component] = ( + '\n'.join(current_text).strip() or None + ) + current_component = component + current_text = [line[len(component) + 1 :].strip()] + break + else: + if current_component is not None: + current_text.append(line.strip()) + + if current_component is not None: + components[current_component] = ( + '\n'.join(current_text).strip() or None + ) + + logger.debug( + "Parsed components - Thought: %s, Action: %s, Observation: %s", + bool(components["Thought"]), + bool(components["Action"]), + bool(components["Observation"]), + ) + + return ( + components["Thought"], + components["Action"], + components["Observation"], + ) + + def _execute_action(self, action: str) -> str: + r"""Execute an action using available tools. + + Args: + action (str): The action string to execute in format Action(params) + Must be one of the supported action types (Search, Lookup, or + Finish). + + Returns: + str: The result of the action execution. Returns error message if + action execution fails or no suitable tool is found. + """ + logger.debug("Executing action: %s", action) + + if action.startswith("Finish"): + logger.info("Task completion requested") + return "Task completed." + + if not self.tools: + logger.warning("No tools available to execute action") + return "No tools available to execute action." + + for tool in self.tools: + try: + if hasattr(tool, 'can_handle') and tool.can_handle(action): + logger.debug("Found tool to handle action") + if hasattr(tool, 'execute'): + return tool.execute(action) + elif callable(tool): + return tool(action) + else: + logger.error( + "Tool has no execute method or is not callable" + ) + return "Error: Tool implementation is invalid" + except Exception as e: + logger.error("Error executing action: %s", str(e)) + return f"Error executing action: {e!s}" + + logger.warning("No suitable tool found for action: %s", action) + return "Action could not be executed with available tools." + + def step( + self, + input_message: Union[BaseMessage, str], + response_format: Optional[type[BaseModel]] = None, + **kwargs: Any, + ) -> ChatAgentResponse: + r"""Perform one step of the ReAct cycle (Reasoning, Acting, Observing). + + Args: + input_message (Union[BaseMessage, str]): Input message to process. + If string, it will be converted to BaseMessage. This will be + augmented with the scratchpad history and ReAct prompt. + response_format (Optional[type[BaseModel]], optional): The expected + response format. Defaults to None. + **kwargs: Additional keyword arguments passed to the underlying + model call. + + Returns: + ChatAgentResponse: A response object containing: + - msgs: List with a single message containing the thought, + action, and observation + - terminated: True if action is Finish or max steps reached + - info: Dictionary with parsed thought, action, and observation + """ + # Convert string input to BaseMessage if needed + if isinstance(input_message, str): + input_message = BaseMessage( + role_name="User", + role_type=RoleType.USER, + meta_dict={}, + content=input_message, + ) + + if self.step_count >= self.max_steps: + logger.warning("Maximum steps (%d) reached", self.max_steps) + return self._handle_max_steps() + + self.step_count += 1 + logger.debug("Starting step %d", self.step_count) + + # Include scratchpad history in the prompt + history = self._format_scratchpad() + augmented_content = ( + f"{input_message.content}\n\n" + f"{history}\n" + f"{self.react_prompt}" + ) + augmented_message = BaseMessage( + role_name=input_message.role_name, + role_type=input_message.role_type, + meta_dict=input_message.meta_dict, + content=augmented_content, + ) + + # Get initial response + response = super().step(augmented_message) + + # Parse ReAct components + thought, action, observation = self._parse_react_components( + response.msgs[0].content + ) + + # Execute action if specified + if action: + logger.debug("Executing action: %s", action) + actual_observation = self._execute_action(action) + observation = actual_observation + + # Update scratchpad + self.scratchpad.append( + { + "Thought": thought or "", + "Action": action or "", + "Observation": observation or "", + } + ) + + # Create final response + final_content = "\n".join( + filter( + None, + [ + f"Thought: {thought}" if thought else None, + f"Action: {action}" if action else None, + f"Observation: {observation}" if observation else None, + ], + ) + ) + + final_message = BaseMessage( + role_name=response.msgs[0].role_name, + role_type=RoleType.ASSISTANT, + meta_dict=response.msgs[0].meta_dict, + content=final_content, + ) + + # Check if the action was Finish + terminated = bool(action and action.startswith("Finish")) + if terminated: + logger.info("Task completed after %d steps", self.step_count) + + return ChatAgentResponse( + msgs=[final_message], + terminated=terminated, + info={ + "thought": thought or "", + "action": action or "", + "observation": observation or "", + }, + ) From 92161a50d2af97d96d0e7dcb00977e266a0fbccc Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Tue, 28 Jan 2025 01:08:37 +0530 Subject: [PATCH 02/10] example for react agent --- examples/agents/react_agent_example.py | 56 ++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/agents/react_agent_example.py diff --git a/examples/agents/react_agent_example.py b/examples/agents/react_agent_example.py new file mode 100644 index 0000000000..dd3a2aec76 --- /dev/null +++ b/examples/agents/react_agent_example.py @@ -0,0 +1,56 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= + +from camel.agents import ReActAgent +from camel.messages import BaseMessage +from camel.toolkits import MathToolkit +from camel.types import RoleType + + +def main() -> None: + # Create system message and initialize agent + system_message = BaseMessage( + role_name="Assistant", + role_type=RoleType.ASSISTANT, + meta_dict={}, + content=( + "You are a helpful math assistant that can perform calculations. " + "Use the appropriate math functions to solve problems accurately." + ), + ) + + # Initialize toolkit and agent + math_tool = MathToolkit() + agent = ReActAgent( + system_message=system_message, + tools=[math_tool], + max_steps=5, + ) + + # Example queries + queries = [ + "What is 123.45 plus 678.90?", + "Calculate 25 multiplied by 3.14, rounded to 2 decimal places", + "Divide 1000 by 3 and round to the nearest whole number", + ] + + # Process each query - simplified! + for query in queries: + response = agent.step(query) + print("Agent response:", response.msgs[0].content) + print("-" * 50) + + +if __name__ == "__main__": + main() From 644bd86e31a3260f33f5874ffbd7aa2afe56f5c4 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:56:44 +0530 Subject: [PATCH 03/10] added react agent tests --- test/agents/test_react_agent.py | 208 ++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 test/agents/test_react_agent.py diff --git a/test/agents/test_react_agent.py b/test/agents/test_react_agent.py new file mode 100644 index 0000000000..96ecf61cbe --- /dev/null +++ b/test/agents/test_react_agent.py @@ -0,0 +1,208 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +from unittest.mock import MagicMock, PropertyMock + +import pytest +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.completion_usage import CompletionUsage + +from camel.agents import ReActAgent +from camel.messages import BaseMessage +from camel.types import RoleType + + +@pytest.fixture +def system_message(): + return BaseMessage( + role_name="assistant", + role_type=RoleType.ASSISTANT, + meta_dict=None, + content=( + "You are a helpful assistant that uses reasoning and actions " + "to complete tasks." + ), + ) + + +# Mock response for testing +model_backend_rsp = ChatCompletion( + id="mock_response_id", + choices=[ + Choice( + finish_reason="stop", + index=0, + logprobs=None, + message=ChatCompletionMessage( + content=( + "Thought: I need to search for information\n" + "Action: Search(query=test query)\n" + "Observation: Found test results" + ), + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=123456789, + model="gpt-4o-2024-05-13", + object="chat.completion", + usage=CompletionUsage( + completion_tokens=32, + prompt_tokens=15, + total_tokens=47, + ), +) + + +def setup_mocked_agent(system_message, mock_model=None, max_steps=None): + """Helper function to setup a mocked agent with proper memory mocking.""" + if mock_model is None: + mock_model = MagicMock() + mock_model.run = MagicMock(return_value=model_backend_rsp) + + agent = ReActAgent( + system_message=system_message, + model=mock_model, + max_steps=max_steps or 10, + ) + + # Mock the memory components + context_creator = agent.memory.get_context_creator() + type(context_creator).token_limit = PropertyMock(return_value=4096) + token_counter = agent.memory.get_context_creator().token_counter + token_counter.count_tokens_from_messages = MagicMock(return_value=10) + agent.step_count = 0 + + return agent + + +@pytest.mark.model_backend +def test_react_agent_init(): + """Test ReActAgent initialization.""" + system_message = BaseMessage( + role_name="assistant", + role_type=RoleType.ASSISTANT, + meta_dict=None, + content="You are a helpful assistant.", + ) + + tools = [ + lambda x: "Test result", + ] + + agent = ReActAgent( + system_message=system_message, + tools=tools, + max_steps=5, + ) + + assert agent.tools == tools + assert agent.max_steps == 5 + assert len(agent.scratchpad) == 0 + assert agent.step_count == 0 + + +@pytest.mark.model_backend +def test_react_agent_parse_components(system_message): + """Test ReActAgent's ability to parse response components.""" + agent = ReActAgent(system_message=system_message) + + test_content = ( + "Thought: I should search for information\n" + "Action: Search(query=test)\n" + "Observation: Found results" + ) + + thought, action, observation = agent._parse_react_components(test_content) + + assert thought == "I should search for information" + assert action == "Search(query=test)" + assert observation == "Found results" + + +@pytest.mark.model_backend +def test_react_agent_execute_action(system_message): + """Test ReActAgent's action execution.""" + mock_tool = MagicMock() + mock_tool.can_handle = MagicMock(return_value=True) + mock_tool.execute = MagicMock(return_value="Test result") + + agent = ReActAgent(system_message=system_message, tools=[mock_tool]) + + result = agent._execute_action("Search(query=test)") + assert result == "Test result" + + # Test Finish action + result = agent._execute_action("Finish(answer=Done)") + assert result == "Task completed." + + +@pytest.mark.model_backend +def test_react_agent_step(system_message, step_call_count=3): + """Test ReActAgent's step function.""" + mock_tool = MagicMock() + mock_tool.can_handle = MagicMock(return_value=True) + mock_tool.execute = MagicMock(return_value="Test result") + + agent = setup_mocked_agent( + system_message=system_message, + max_steps=10, + ) + agent.tools = [mock_tool] + + input_message = BaseMessage( + role_name="user", + role_type=RoleType.USER, + meta_dict={}, + content="Please help me find information.", + ) + + for i in range(step_call_count): + response = agent.step(input_message) + + assert isinstance(response.msgs, list), f"Error in round {i+1}" + assert len(response.msgs) == 1, f"Error in round {i+1}" + assert isinstance( + response.msgs[0], BaseMessage + ), f"Error in round {i+1}" + assert isinstance(response.terminated, bool), f"Error in round {i+1}" + assert isinstance(response.info, dict), f"Error in round {i+1}" + assert "thought" in response.info, f"Error in round {i+1}" + assert "action" in response.info, f"Error in round {i+1}" + assert "observation" in response.info, f"Error in round {i+1}" + + +@pytest.mark.model_backend +def test_react_agent_max_steps(system_message): + """Test ReActAgent's max steps limit.""" + agent = setup_mocked_agent(system_message=system_message, max_steps=1) + + input_message = BaseMessage( + role_name="user", + role_type=RoleType.USER, + meta_dict={}, + content="Please help me find information.", + ) + + # First step should work normally + response = agent.step(input_message) + assert not response.terminated, "First step should not terminate" + + # Second step should terminate due to max steps + response = agent.step(input_message) + assert response.terminated, "Second step should terminate" + assert response.info["thought"] == "Maximum steps reached" + assert response.info["observation"] == "Task terminated due to step limit" From c1b5c893310a53ce197e2da05c47eff0eadf606c Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Mon, 10 Feb 2025 03:56:15 +0530 Subject: [PATCH 04/10] fix --- camel/agents/react_agent.py | 154 ++++++++++--------------- examples/agents/react_agent_example.py | 36 +++++- 2 files changed, 92 insertions(+), 98 deletions(-) diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py index 61cbc16eb9..7b5a275bf6 100644 --- a/camel/agents/react_agent.py +++ b/camel/agents/react_agent.py @@ -12,17 +12,21 @@ # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= -import logging from enum import Enum -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, Field from camel.agents import ChatAgent +from camel.logger import get_logger from camel.messages import BaseMessage +from camel.models import BaseModelBackend from camel.responses import ChatAgentResponse +from camel.toolkits import FunctionTool from camel.types import RoleType +logger = get_logger(__name__) + # AgentOps decorator setting try: import os @@ -34,7 +38,15 @@ except (ImportError, AttributeError): from camel.utils import track_agent -logger = logging.getLogger(__name__) + +class ReActStep(BaseModel): + """Structured format for ReAct steps""" + + thought: str = Field(description="Reasoning about current situation") + action: str = Field(description="Action to take (Search/Lookup/Finish)") + observation: Optional[str] = Field( + None, description="Results of the action" + ) class ReActActionSpace(Enum): @@ -60,26 +72,29 @@ class ReActAgent(ChatAgent): Args: system_message (BaseMessage): The system message for initializing the agent's conversation context. - model (Any, optional): The model backend to use for - response generation. Defaults to None. - tools (List[Any], optional): List of available tools for executing - actions. Defaults to None. - max_steps (int, optional): Maximum number of steps before termination. - Defaults to 10. - - References: - https://arxiv.org/pdf/2210.03629 + model (Optional[BaseModelBackend], optional): The model backend to use + for response generation. (default: :obj:`None`) + tools (Optional[List[Union[FunctionTool, Callable]]], optional): List + of available tools that can be used to execute actions. Tools can + be either FunctionTool instances or callable functions. + (default: :obj:`None`) + max_steps (int, optional): Maximum number of reasoning steps before + forced termination. Prevents infinite loops. + (default: :obj:`10`) """ def __init__( self, system_message: BaseMessage, - model: Optional[Any] = None, - tools: Optional[List[Any]] = None, + model: Optional[BaseModelBackend] = None, + tools: Optional[List[Union[FunctionTool, Callable]]] = None, max_steps: int = 10, ) -> None: super().__init__(system_message=system_message, model=model) - self.tools = tools or [] + self.tools: List[FunctionTool] = [ + t if isinstance(t, FunctionTool) else FunctionTool(t) + for t in (tools or []) + ] self.scratchpad: List[Dict[str, Optional[str]]] = [] self._set_react_prompt() self.step_count = 0 @@ -93,19 +108,17 @@ def _set_react_prompt(self) -> None: response format and behavior. """ self.react_prompt = ( - "Follow this STRICT format:\n\n" - "Thought: [analyze current situation]\n" - "Action: [EXACTLY ONE of these]\n" - "- Search(query=)\n" - "- Lookup(key=)\n" - "- Finish(answer=)\n\n" - "Examples:\n" - "Good: Action: Search(query=Paris population)\n" - "Bad: Action: Search Paris population\n\n" - "Rules:\n" - "1. ALWAYS validate action syntax before execution\n" - "2. If action fails, diagnose and retry\n" - "3. Use Finish() only when ready with final answer\n\n" + "Respond with a JSON object containing:\n" + "- thought: Your analysis of the current situation\n" + "- action: EXACTLY ONE of:\n" + " - Search(query=)\n" + " - Lookup(key=)\n" + " - Finish(answer=)\n" + "\nExample response:\n" + '{\n' + ' "thought": "I need to find population data",\n' + ' "action": "Search(query=Paris population 2024)"\n' + '}\n\n' "Current scratchpad:\n" "{scratchpad}" ) @@ -157,65 +170,6 @@ def _handle_max_steps(self) -> ChatAgentResponse: }, ) - def _parse_react_components( - self, - content: str, - ) -> Tuple[Optional[str], Optional[str], Optional[str]]: - r"""Parse the response into ReAct components. - - Args: - content (str): The response content to parse. Expected to contain - sections starting with "Thought:", "Action:", and/or - "Observation:". - - Returns: - Tuple[Optional[str], Optional[str], Optional[str]]: - A tuple containing: - - thought: The parsed thought component, or None if not found - - action: The parsed action component, or None if not found - - observation: The observation, or None if not found - """ - components: Dict[str, Optional[str]] = { - "Thought": None, - "Action": None, - "Observation": None, - } - - current_component: Optional[str] = None - current_text: List[str] = [] - - for line in content.split('\n'): - for component in components: - if line.startswith(f"{component}:"): - if current_component is not None: - components[current_component] = ( - '\n'.join(current_text).strip() or None - ) - current_component = component - current_text = [line[len(component) + 1 :].strip()] - break - else: - if current_component is not None: - current_text.append(line.strip()) - - if current_component is not None: - components[current_component] = ( - '\n'.join(current_text).strip() or None - ) - - logger.debug( - "Parsed components - Thought: %s, Action: %s, Observation: %s", - bool(components["Thought"]), - bool(components["Action"]), - bool(components["Observation"]), - ) - - return ( - components["Thought"], - components["Action"], - components["Observation"], - ) - def _execute_action(self, action: str) -> str: r"""Execute an action using available tools. @@ -271,7 +225,7 @@ def step( If string, it will be converted to BaseMessage. This will be augmented with the scratchpad history and ReAct prompt. response_format (Optional[type[BaseModel]], optional): The expected - response format. Defaults to None. + response format. (default: :obj:`None`) **kwargs: Additional keyword arguments passed to the underlying model call. @@ -313,12 +267,22 @@ def step( ) # Get initial response - response = super().step(augmented_message) - - # Parse ReAct components - thought, action, observation = self._parse_react_components( - response.msgs[0].content - ) + response = super().step(augmented_message, response_format=ReActStep) + + # Parse response into ReActStep model + if ( + hasattr(response.msgs[0], 'parsed') + and response.msgs[0].parsed + and isinstance(response.msgs[0].parsed, ReActStep) + ): + react_step = response.msgs[0].parsed + thought = react_step.thought + action = react_step.action + observation = react_step.observation + else: + thought = "" + action = "" + observation = None # Execute action if specified if action: diff --git a/examples/agents/react_agent_example.py b/examples/agents/react_agent_example.py index dd3a2aec76..458f9bd17f 100644 --- a/examples/agents/react_agent_example.py +++ b/examples/agents/react_agent_example.py @@ -14,8 +14,9 @@ from camel.agents import ReActAgent from camel.messages import BaseMessage +from camel.models import ModelFactory from camel.toolkits import MathToolkit -from camel.types import RoleType +from camel.types import ModelPlatformType, ModelType, RoleType def main() -> None: @@ -30,11 +31,17 @@ def main() -> None: ), ) + model = ModelFactory.create( + model_platform=ModelPlatformType.DEFAULT, + model_type=ModelType.DEFAULT, + ) + # Initialize toolkit and agent math_tool = MathToolkit() agent = ReActAgent( system_message=system_message, tools=[math_tool], + model=model, max_steps=5, ) @@ -45,12 +52,35 @@ def main() -> None: "Divide 1000 by 3 and round to the nearest whole number", ] - # Process each query - simplified! + # Process each query and print raw JSON response for query in queries: response = agent.step(query) - print("Agent response:", response.msgs[0].content) + print("JSON response:", response.info) print("-" * 50) if __name__ == "__main__": main() + +""" +JSON response: { + 'thought': 'I need to calculate the sum of 123.45 and 678.90.', + 'action': 'Finish(answer=802.35)', + 'observation': 'Task completed.' +} +-------------------------------------------------- +JSON response: { + 'thought': 'I need to calculate 25 multiplied by 3.14 and round the result + to 2 decimal places.', + 'action': 'Finish(answer=78.50)', + 'observation': 'Task completed.' +} +-------------------------------------------------- +JSON response: { + 'thought': 'I need to divide 1000 by 3 and round the result to the nearest + whole number.', + 'action': 'Finish(answer=334)', + 'observation': 'Task completed.' +} +-------------------------------------------------- +""" From 077fbce229ed20f6bba959a781646aba48f37aa3 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Mon, 10 Feb 2025 03:58:49 +0530 Subject: [PATCH 05/10] fix --- examples/agents/react_agent_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/agents/react_agent_example.py b/examples/agents/react_agent_example.py index 458f9bd17f..166436c2c0 100644 --- a/examples/agents/react_agent_example.py +++ b/examples/agents/react_agent_example.py @@ -40,7 +40,7 @@ def main() -> None: math_tool = MathToolkit() agent = ReActAgent( system_message=system_message, - tools=[math_tool], + tools=math_tool.get_tools(), model=model, max_steps=5, ) From eeadc2db34486c100b35838c35019d0e9bb75226 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:17:53 +0530 Subject: [PATCH 06/10] refactor: complete track_agent implementation in commons.py --- camel/agents/chat_agent.py | 12 +----------- camel/agents/deductive_reasoner_agent.py | 12 +----------- camel/agents/embodied_agent.py | 13 +------------ camel/agents/knowledge_graph_agent.py | 13 +------------ camel/agents/react_agent.py | 12 +----------- camel/agents/role_assignment_agent.py | 12 +----------- camel/agents/search_agent.py | 13 +------------ camel/agents/task_agent.py | 13 +------------ camel/utils/commons.py | 20 +++++++++++++++++--- 9 files changed, 25 insertions(+), 95 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 03cd20b3d5..68bb881f21 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -63,6 +63,7 @@ get_model_encoding, get_pydantic_object_schema, json_to_function_code, + track_agent, ) if TYPE_CHECKING: @@ -74,17 +75,6 @@ logger = logging.getLogger(__name__) -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent - class ToolCallingRecord(BaseModel): r"""Historical records of tools called in the conversation. diff --git a/camel/agents/deductive_reasoner_agent.py b/camel/agents/deductive_reasoner_agent.py index c56e3f279f..b4a48b0124 100644 --- a/camel/agents/deductive_reasoner_agent.py +++ b/camel/agents/deductive_reasoner_agent.py @@ -20,20 +20,10 @@ from camel.models import BaseModelBackend from camel.prompts import TextPrompt from camel.types import RoleType +from camel.utils import track_agent logger = get_logger(__name__) -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent - @track_agent(name="DeductiveReasonerAgent") class DeductiveReasonerAgent(ChatAgent): diff --git a/camel/agents/embodied_agent.py b/camel/agents/embodied_agent.py index 3422389fa0..42a8d4508c 100644 --- a/camel/agents/embodied_agent.py +++ b/camel/agents/embodied_agent.py @@ -25,18 +25,7 @@ from camel.messages import BaseMessage from camel.models import BaseModelBackend from camel.responses import ChatAgentResponse -from camel.utils import print_text_animated - -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent +from camel.utils import print_text_animated, track_agent @track_agent(name="EmbodiedAgent") diff --git a/camel/agents/knowledge_graph_agent.py b/camel/agents/knowledge_graph_agent.py index e527362b6f..2fd6a46c30 100644 --- a/camel/agents/knowledge_graph_agent.py +++ b/camel/agents/knowledge_graph_agent.py @@ -26,18 +26,7 @@ Relationship, ) from camel.types import RoleType - -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent - +from camel.utils import track_agent text_prompt = """ You are tasked with extracting nodes and relationships from given content and diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py index 7b5a275bf6..669dd959a3 100644 --- a/camel/agents/react_agent.py +++ b/camel/agents/react_agent.py @@ -24,20 +24,10 @@ from camel.responses import ChatAgentResponse from camel.toolkits import FunctionTool from camel.types import RoleType +from camel.utils import track_agent logger = get_logger(__name__) -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent - class ReActStep(BaseModel): """Structured format for ReAct steps""" diff --git a/camel/agents/role_assignment_agent.py b/camel/agents/role_assignment_agent.py index beb3625a5b..731df2f5c0 100644 --- a/camel/agents/role_assignment_agent.py +++ b/camel/agents/role_assignment_agent.py @@ -19,17 +19,7 @@ from camel.models import BaseModelBackend from camel.prompts import TextPrompt from camel.types import RoleType - -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent +from camel.utils import track_agent @track_agent(name="RoleAssignmentAgent") diff --git a/camel/agents/search_agent.py b/camel/agents/search_agent.py index 91f5c3d160..b2c99dbccf 100644 --- a/camel/agents/search_agent.py +++ b/camel/agents/search_agent.py @@ -18,18 +18,7 @@ from camel.models import BaseModelBackend from camel.prompts import TextPrompt from camel.types import RoleType -from camel.utils import create_chunks - -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent +from camel.utils import create_chunks, track_agent @track_agent(name="SearchAgent") diff --git a/camel/agents/task_agent.py b/camel/agents/task_agent.py index 51557855fc..4bd4261256 100644 --- a/camel/agents/task_agent.py +++ b/camel/agents/task_agent.py @@ -18,18 +18,7 @@ from camel.models import BaseModelBackend from camel.prompts import PromptTemplateGenerator, TextPrompt from camel.types import RoleType, TaskType -from camel.utils import get_task_list - -# AgentOps decorator setting -try: - import os - - if os.getenv("AGENTOPS_API_KEY") is not None: - from agentops import track_agent - else: - raise ImportError -except (ImportError, AttributeError): - from camel.utils import track_agent +from camel.utils import get_task_list, track_agent @track_agent(name="TaskSpecifyAgent") diff --git a/camel/utils/commons.py b/camel/utils/commons.py index 29a82f86a8..2039defe63 100644 --- a/camel/utils/commons.py +++ b/camel/utils/commons.py @@ -546,10 +546,14 @@ def is_docker_running() -> bool: ToolEvent, record, ) + from agentops import ( + track_agent as agentops_track_agent, + ) else: raise ImportError except (ImportError, AttributeError): ToolEvent = None + agentops_track_agent = None def agentops_decorator(func): @@ -594,10 +598,20 @@ def __new__(cls, name, bases, dct): return super().__new__(cls, name, bases, dct) -def track_agent(*args, **kwargs): - r"""Mock track agent decorator for AgentOps.""" +def track_agent(name: str) -> Callable: + r"""Track agent decorator for AgentOps. + + Args: + name (str): The name of the agent to track. + + Returns: + Callable: A decorator function that either tracks the agent using + AgentOps or acts as a no-op if AgentOps is not available. + """ + if agentops_track_agent is not None: + return agentops_track_agent(name=name) - def noop(f): + def noop(f: Callable) -> Callable: return f return noop From e00d0940e49740e9c983960dab9d8609dcc25176 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Tue, 11 Feb 2025 02:07:48 +0530 Subject: [PATCH 07/10] changes --- camel/agents/react_agent.py | 37 ++++++++++++++++---------- examples/agents/react_agent_example.py | 10 +++---- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py index 669dd959a3..4c3b069877 100644 --- a/camel/agents/react_agent.py +++ b/camel/agents/react_agent.py @@ -104,10 +104,15 @@ def _set_react_prompt(self) -> None: " - Search(query=)\n" " - Lookup(key=)\n" " - Finish(answer=)\n" - "\nExample response:\n" + "\nExample response for Search:\n" '{\n' - ' "thought": "I need to find population data",\n' - ' "action": "Search(query=Paris population 2024)"\n' + ' "thought": "I need to find current population data",\n' + ' "action": "Search(query=Paris population estimate 2024)"\n' + '}\n\n' + "Example response for Lookup:\n" + '{\n' + ' "thought": "I need structured lookup results",\n' + ' "action": "Lookup(key=Paris_population_data)"\n' '}\n\n' "Current scratchpad:\n" "{scratchpad}" @@ -169,8 +174,8 @@ def _execute_action(self, action: str) -> str: Finish). Returns: - str: The result of the action execution. Returns error message if - action execution fails or no suitable tool is found. + str: The result of the action execution. Returns an error message + if action execution fails or no suitable tool is found. """ logger.debug("Executing action: %s", action) @@ -182,18 +187,21 @@ def _execute_action(self, action: str) -> str: logger.warning("No tools available to execute action") return "No tools available to execute action." + # Differentiate based on the type of actionable command for tool in self.tools: try: - if hasattr(tool, 'can_handle') and tool.can_handle(action): - logger.debug("Found tool to handle action") + if not hasattr(tool, 'can_handle') or tool.can_handle(action): + logger.debug( + "Found tool to handle action using tool: %s", + getattr(tool, "name", tool), + ) if hasattr(tool, 'execute'): - return tool.execute(action) + result = tool.execute(action) + return result elif callable(tool): return tool(action) else: - logger.error( - "Tool has no execute method or is not callable" - ) + logger.error("No execute method or is not callable") return "Error: Tool implementation is invalid" except Exception as e: logger.error("Error executing action: %s", str(e)) @@ -245,10 +253,11 @@ def step( # Include scratchpad history in the prompt history = self._format_scratchpad() augmented_content = ( - f"{input_message.content}\n\n" - f"{history}\n" - f"{self.react_prompt}" + f"Question: {input_message.content}\n\n" + f"Previous steps:\n{history}\n\n" + "Let's approach this step-by-step:\n" ) + augmented_message = BaseMessage( role_name=input_message.role_name, role_type=input_message.role_type, diff --git a/examples/agents/react_agent_example.py b/examples/agents/react_agent_example.py index 166436c2c0..13492c9f3f 100644 --- a/examples/agents/react_agent_example.py +++ b/examples/agents/react_agent_example.py @@ -15,7 +15,7 @@ from camel.agents import ReActAgent from camel.messages import BaseMessage from camel.models import ModelFactory -from camel.toolkits import MathToolkit +from camel.toolkits import SearchToolkit from camel.types import ModelPlatformType, ModelType, RoleType @@ -37,19 +37,17 @@ def main() -> None: ) # Initialize toolkit and agent - math_tool = MathToolkit() agent = ReActAgent( system_message=system_message, - tools=math_tool.get_tools(), + tools=[SearchToolkit().search_duckduckgo], model=model, max_steps=5, ) # Example queries queries = [ - "What is 123.45 plus 678.90?", - "Calculate 25 multiplied by 3.14, rounded to 2 decimal places", - "Divide 1000 by 3 and round to the nearest whole number", + "What is the population of Paris in 2024?", + "What is the population of the capital of France?", ] # Process each query and print raw JSON response From 2240f61827dbe16bf05406e61efa7715bea8d567 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:09:51 +0530 Subject: [PATCH 08/10] fixes --- camel/agents/react_agent.py | 104 +++++++++++++------------ examples/agents/react_agent_example.py | 71 +++++++++-------- 2 files changed, 93 insertions(+), 82 deletions(-) diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py index 4c3b069877..0e01d37cc9 100644 --- a/camel/agents/react_agent.py +++ b/camel/agents/react_agent.py @@ -80,11 +80,9 @@ def __init__( tools: Optional[List[Union[FunctionTool, Callable]]] = None, max_steps: int = 10, ) -> None: - super().__init__(system_message=system_message, model=model) - self.tools: List[FunctionTool] = [ - t if isinstance(t, FunctionTool) else FunctionTool(t) - for t in (tools or []) - ] + super().__init__( + system_message=system_message, model=model, tools=tools + ) self.scratchpad: List[Dict[str, Optional[str]]] = [] self._set_react_prompt() self.step_count = 0 @@ -98,21 +96,22 @@ def _set_react_prompt(self) -> None: response format and behavior. """ self.react_prompt = ( - "Respond with a JSON object containing:\n" - "- thought: Your analysis of the current situation\n" - "- action: EXACTLY ONE of:\n" - " - Search(query=)\n" - " - Lookup(key=)\n" - " - Finish(answer=)\n" + "You MUST ALWAYS use EXACTLY ONE of the following actions. " + "You MUST ALWAYS include a 'thought' and 'action'.\n" + "- Search(query=)\n" + "- Lookup(key=)\n" + "- Finish(answer=)\n" + "Respond with JSON object with the keys 'thought' and 'action'.\n" + "The 'action' value must be one of the three options above.\n" "\nExample response for Search:\n" '{\n' ' "thought": "I need to find current population data",\n' ' "action": "Search(query=Paris population estimate 2024)"\n' '}\n\n' - "Example response for Lookup:\n" + "Example response for Finish:\n" '{\n' - ' "thought": "I need structured lookup results",\n' - ' "action": "Lookup(key=Paris_population_data)"\n' + ' "thought":"Based on the data,I can now provide the answer",\n' + ' "action":"Finish(answer=Population is approx. 2.1 million)"\n' '}\n\n' "Current scratchpad:\n" "{scratchpad}" @@ -169,13 +168,12 @@ def _execute_action(self, action: str) -> str: r"""Execute an action using available tools. Args: - action (str): The action string to execute in format Action(params) - Must be one of the supported action types (Search, Lookup, or - Finish). + action (str): The action string in format Action(params) + e.g., "Search(query=Paris population 2024)" + or "Finish(answer=The population is 2.1M)" Returns: - str: The result of the action execution. Returns an error message - if action execution fails or no suitable tool is found. + str: The result of the action execution """ logger.debug("Executing action: %s", action) @@ -187,28 +185,31 @@ def _execute_action(self, action: str) -> str: logger.warning("No tools available to execute action") return "No tools available to execute action." - # Differentiate based on the type of actionable command - for tool in self.tools: - try: - if not hasattr(tool, 'can_handle') or tool.can_handle(action): - logger.debug( - "Found tool to handle action using tool: %s", - getattr(tool, "name", tool), - ) - if hasattr(tool, 'execute'): - result = tool.execute(action) - return result - elif callable(tool): - return tool(action) - else: - logger.error("No execute method or is not callable") - return "Error: Tool implementation is invalid" - except Exception as e: - logger.error("Error executing action: %s", str(e)) - return f"Error executing action: {e!s}" - - logger.warning("No suitable tool found for action: %s", action) - return "Action could not be executed with available tools." + try: + func_name = action.split('(')[0].strip() + params_str = action[action.find('(') + 1 : action.rfind(')')] + + params = {} + if '=' in params_str: + key, value = params_str.split('=', 1) + params[key.strip()] = value.strip() + + for tool in self.tools: + if isinstance(tool, FunctionTool): + if ( + tool.openai_tool_schema["function"]["name"].lower() + == func_name.lower() + ): + return tool(**params) + elif callable(tool): + return tool(action) + + logger.warning(f"No suitable tool found for action: {action}") + return f"No tool found matching {func_name}" + + except Exception as e: + logger.error(f"Error executing action: {e!s}") + return f"Error executing action: {e!s}" def step( self, @@ -288,15 +289,18 @@ def step( logger.debug("Executing action: %s", action) actual_observation = self._execute_action(action) observation = actual_observation + else: + observation = None # Update scratchpad - self.scratchpad.append( - { - "Thought": thought or "", - "Action": action or "", - "Observation": observation or "", - } - ) + scratchpad_entry: Dict[str, Optional[str]] = { + "Thought": thought or "", + "Action": action or "", + } + + if action: + scratchpad_entry["Observation"] = observation or None + self.scratchpad.append(scratchpad_entry) # Create final response final_content = "\n".join( @@ -305,7 +309,9 @@ def step( [ f"Thought: {thought}" if thought else None, f"Action: {action}" if action else None, - f"Observation: {observation}" if observation else None, + f"Observation: {observation}" + if action and observation + else None, ], ) ) diff --git a/examples/agents/react_agent_example.py b/examples/agents/react_agent_example.py index 13492c9f3f..a01e3d4cd9 100644 --- a/examples/agents/react_agent_example.py +++ b/examples/agents/react_agent_example.py @@ -11,12 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= - from camel.agents import ReActAgent from camel.messages import BaseMessage -from camel.models import ModelFactory from camel.toolkits import SearchToolkit -from camel.types import ModelPlatformType, ModelType, RoleType +from camel.types import RoleType def main() -> None: @@ -26,34 +24,32 @@ def main() -> None: role_type=RoleType.ASSISTANT, meta_dict={}, content=( - "You are a helpful math assistant that can perform calculations. " - "Use the appropriate math functions to solve problems accurately." + "You are a helpful assistant that can search information online. " + "Use the search tool to find accurate information and " + "provide detailed answers. " ), ) - model = ModelFactory.create( - model_platform=ModelPlatformType.DEFAULT, - model_type=ModelType.DEFAULT, - ) - - # Initialize toolkit and agent + # Initialize toolkit and agent with search_duckduckgo tool + search_tool = SearchToolkit().search_duckduckgo agent = ReActAgent( system_message=system_message, - tools=[SearchToolkit().search_duckduckgo], - model=model, + tools=[search_tool], max_steps=5, ) - # Example queries + # Example queries that require search queries = [ - "What is the population of Paris in 2024?", - "What is the population of the capital of France?", + "What is the current population of Paris?", + "Who won the Nobel Prize in Physics in 2024?", ] - # Process each query and print raw JSON response + # Process each query for query in queries: + print(f"\nQuery: {query}") + print("-" * 50) response = agent.step(query) - print("JSON response:", response.info) + print(f"Response:\n{response.info}") print("-" * 50) @@ -61,24 +57,33 @@ def main() -> None: main() """ -JSON response: { - 'thought': 'I need to calculate the sum of 123.45 and 678.90.', - 'action': 'Finish(answer=802.35)', - 'observation': 'Task completed.' -} +Example output: + +Query: What is the current population of Paris? -------------------------------------------------- -JSON response: { - 'thought': 'I need to calculate 25 multiplied by 3.14 and round the result - to 2 decimal places.', - 'action': 'Finish(answer=78.50)', - 'observation': 'Task completed.' +Response: +{ + "thought": "I found several sources that provide population estimates for + Paris. The most relevant ones indicate that the population of + Paris in 2023 is approximately 2.1 million for the city proper, + while the metropolitan area is around 11.2 million. I need to + summarize this information clearly.", + "action": "Finish", + "observation": "Task completed." } -------------------------------------------------- -JSON response: { - 'thought': 'I need to divide 1000 by 3 and round the result to the nearest - whole number.', - 'action': 'Finish(answer=334)', - 'observation': 'Task completed.' + +Query: Who won the Nobel Prize in Physics in 2024? +-------------------------------------------------- +Response: +{ + "thought": "I found multiple sources confirming the winners of the 2024 + Nobel Prize in Physics. The prize was awarded to John J. + Hopfield and Geoffrey E. Hinton for their contributions + to machine learning and artificial neural networks. + I will summarize this information.", + "action": "Finish", + "observation": "Task completed." } -------------------------------------------------- """ From 0152d9774ed1dedc9aace3e5cd5f52240cd67f64 Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Mon, 24 Feb 2025 23:19:49 +0530 Subject: [PATCH 09/10] update --- camel/agents/react_agent.py | 296 ++++++++++++++++--------- examples/agents/react_agent_example.py | 91 ++++---- 2 files changed, 246 insertions(+), 141 deletions(-) diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py index 0e01d37cc9..6958a52a4b 100644 --- a/camel/agents/react_agent.py +++ b/camel/agents/react_agent.py @@ -12,8 +12,9 @@ # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +import json from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from pydantic import BaseModel, Field @@ -30,7 +31,7 @@ class ReActStep(BaseModel): - """Structured format for ReAct steps""" + r"""Structured format for ReAct steps""" thought: str = Field(description="Reasoning about current situation") action: str = Field(description="Action to take (Search/Lookup/Finish)") @@ -80,8 +81,18 @@ def __init__( tools: Optional[List[Union[FunctionTool, Callable]]] = None, max_steps: int = 10, ) -> None: + self._set_react_prompt() + + # Combine original system message with ReAct prompt + combined_content = ( + f"{system_message.content}\n\n" f"{self.react_prompt}" + ) + react_system_message = system_message.create_new_instance( + combined_content + ) + super().__init__( - system_message=system_message, model=model, tools=tools + system_message=react_system_message, model=model, tools=tools ) self.scratchpad: List[Dict[str, Optional[str]]] = [] self._set_react_prompt() @@ -98,9 +109,12 @@ def _set_react_prompt(self) -> None: self.react_prompt = ( "You MUST ALWAYS use EXACTLY ONE of the following actions. " "You MUST ALWAYS include a 'thought' and 'action'.\n" - "- Search(query=)\n" - "- Lookup(key=)\n" - "- Finish(answer=)\n" + "- Search(query=):Use this to search information\n" + "- Lookup(key=): Use this to look up specific info\n" + "- Finish(answer=): ONLY use this when you have ALL " + "information needed to fully answer the question\n\n" + "IMPORTANT: DO NOT use Finish until you are completely certain you" + "gathered all necessary information to provide complete answer\n\n" "Respond with JSON object with the keys 'thought' and 'action'.\n" "The 'action' value must be one of the three options above.\n" "\nExample response for Search:\n" @@ -108,10 +122,15 @@ def _set_react_prompt(self) -> None: ' "thought": "I need to find current population data",\n' ' "action": "Search(query=Paris population estimate 2024)"\n' '}\n\n' - "Example response for Finish:\n" + "Example response for continuing research:\n" + '{\n' + ' "thought": "I found the population,but need to verify.",\n' + ' "action": "Search(query=Paris census official data latest)"\n' + '}\n\n' + "Example response for Finish (only when task is fully complete):\n" '{\n' - ' "thought":"Based on the data,I can now provide the answer",\n' - ' "action":"Finish(answer=Population is approx. 2.1 million)"\n' + ' "thought":"I have found and verified needed information",\n' + ' "action": "Finish(answer=Paris population is 2.1M)"\n' '}\n\n' "Current scratchpad:\n" "{scratchpad}" @@ -129,13 +148,13 @@ def _format_scratchpad(self) -> str: if not self.scratchpad: return "" - formatted = "Previous steps:\n" + formatted = "" for step in self.scratchpad: for key, value in step.items(): if value: formatted += f"{key}: {value}\n" formatted += "\n" - return formatted + return formatted.rstrip() def _handle_max_steps(self) -> ChatAgentResponse: r"""Handle the case when maximum steps are reached. @@ -164,6 +183,59 @@ def _handle_max_steps(self) -> ChatAgentResponse: }, ) + def _parse_action(self, action: str) -> Tuple[str, Dict[str, Any]]: + r"""Parse action string into function name and arguments. + + Args: + action (str): Action string in format, + "Action(param1=value1, param2=value2)" + + Returns: + Tuple[str, Dict[str, Any]]: Function name and arguments dictionary + + Raises: + ValueError: If action string is malformed + """ + try: + # Check if action is empty or malformed + if not action or '(' not in action or not action.endswith(')'): + raise ValueError(f"Malformed action: {action}") + + # Extract function name + func_name = action[: action.find('(')].strip() + if not func_name: + raise ValueError("Empty function name") + + # Extract arguments string + args_str = action[action.find('(') + 1 : action.rfind(')')].strip() + + # Handle empty arguments case + if not args_str: + return func_name, {} + + # Convert to proper JSON format + # Replace "=" with ": " and add quotes around keys + json_str = "{" + for param in args_str.split(','): + if '=' not in param: + continue + key, value = param.split('=', 1) + key = key.strip() + value = value.strip() + # Add quotes if not already present + if not (value.startswith('"') and value.endswith('"')): + value = f'"{value}"' + json_str += f'"{key}": {value},' + json_str = json_str.rstrip(',') + "}" + + # Parse JSON string + args = json.loads(json_str) + return func_name, args + + except Exception as e: + logger.error(f"Failed to parse action '{action}': {e!s}") + raise ValueError(f"Failed to parse action: {e!s}") + def _execute_action(self, action: str) -> str: r"""Execute an action using available tools. @@ -186,14 +258,10 @@ def _execute_action(self, action: str) -> str: return "No tools available to execute action." try: - func_name = action.split('(')[0].strip() - params_str = action[action.find('(') + 1 : action.rfind(')')] - - params = {} - if '=' in params_str: - key, value = params_str.split('=', 1) - params[key.strip()] = value.strip() + # Parse action using robust parser + func_name, params = self._parse_action(action) + # Find and execute matching tool for tool in self.tools: if isinstance(tool, FunctionTool): if ( @@ -207,9 +275,14 @@ def _execute_action(self, action: str) -> str: logger.warning(f"No suitable tool found for action: {action}") return f"No tool found matching {func_name}" + except ValueError as e: + error_msg = f"Invalid action format: {e}" + logger.error(error_msg) + return error_msg except Exception as e: - logger.error(f"Error executing action: {e!s}") - return f"Error executing action: {e!s}" + error_msg = f"Error executing action: {e}" + logger.error(error_msg) + return error_msg def step( self, @@ -217,23 +290,21 @@ def step( response_format: Optional[type[BaseModel]] = None, **kwargs: Any, ) -> ChatAgentResponse: - r"""Perform one step of the ReAct cycle (Reasoning, Acting, Observing). + r"""Perform ReAct cycles until task completion or max steps reached. Args: - input_message (Union[BaseMessage, str]): Input message to process. - If string, it will be converted to BaseMessage. This will be - augmented with the scratchpad history and ReAct prompt. - response_format (Optional[type[BaseModel]], optional): The expected - response format. (default: :obj:`None`) - **kwargs: Additional keyword arguments passed to the underlying - model call. + input_message (Union[BaseMessage, str]): Initial input message. + response_format (Optional[type[BaseModel]], optional): Expected + response format. + **kwargs: Additional arguments passed to underlying model call. Returns: - ChatAgentResponse: A response object containing: - - msgs: List with a single message containing the thought, - action, and observation - - terminated: True if action is Finish or max steps reached - - info: Dictionary with parsed thought, action, and observation + ChatAgentResponse: Final response containing: + - msgs: List with final message containing thought, action, + observation + - terminated: True if task finished or max steps reached + - info: Dictionary with final thought, action, + observation details """ # Convert string input to BaseMessage if needed if isinstance(input_message, str): @@ -244,73 +315,103 @@ def step( content=input_message, ) - if self.step_count >= self.max_steps: - logger.warning("Maximum steps (%d) reached", self.max_steps) - return self._handle_max_steps() + current_message = input_message + final_thought = "" + final_action = "" + final_observation = "" + + while True: + # Check for max steps + if self.step_count >= self.max_steps: + logger.warning("Maximum steps (%d) reached", self.max_steps) + return self._handle_max_steps() + + self.step_count += 1 + logger.debug("Starting step %d", self.step_count) + + # Format history and augment message with scratchpad + history = self._format_scratchpad() + augmented_content = ( + f"Question: {input_message.content}\n\n" + f"Previous steps:\n{history if history else 'None'}\n\n" + "Let's approach this step-by-step:\n" + ) - self.step_count += 1 - logger.debug("Starting step %d", self.step_count) + augmented_message = BaseMessage( + role_name=current_message.role_name, + role_type=current_message.role_type, + meta_dict=current_message.meta_dict, + content=augmented_content, + ) - # Include scratchpad history in the prompt - history = self._format_scratchpad() - augmented_content = ( - f"Question: {input_message.content}\n\n" - f"Previous steps:\n{history}\n\n" - "Let's approach this step-by-step:\n" - ) + # Get model response + response = super().step( + augmented_message, response_format=ReActStep + ) - augmented_message = BaseMessage( - role_name=input_message.role_name, - role_type=input_message.role_type, - meta_dict=input_message.meta_dict, - content=augmented_content, - ) + # Parse response + if ( + hasattr(response.msgs[0], 'parsed') + and response.msgs[0].parsed + and isinstance(response.msgs[0].parsed, ReActStep) + ): + react_step = response.msgs[0].parsed + thought = react_step.thought + action = react_step.action + observation = react_step.observation + else: + logger.error( + "Failed to parse model response into ReActStep format" + ) + thought = "" + action = "" + observation = None + + # Execute action if specified + if action: + logger.debug("Executing action: %s", action) + actual_observation = self._execute_action(action) + observation = actual_observation + else: + observation = None + + # Update scratchpad + scratchpad_entry: Dict[str, Optional[str]] = { + "Thought": thought or "", + "Action": action or "", + } + if action: + scratchpad_entry["Observation"] = observation or None + self.scratchpad.append(scratchpad_entry) + + # Store the latest step's information + final_thought = thought + final_action = action + final_observation = observation or "" + + # Check for termination conditions + terminated = bool(action and action.startswith("Finish")) + if terminated: + logger.info("Task completed after %d steps", self.step_count) + break + + # Update current message with observation for next iteration + current_message = BaseMessage( + role_name=response.msgs[0].role_name, + role_type=RoleType.ASSISTANT, + meta_dict={}, + content=str(observation) if observation else "", + ) - # Get initial response - response = super().step(augmented_message, response_format=ReActStep) - - # Parse response into ReActStep model - if ( - hasattr(response.msgs[0], 'parsed') - and response.msgs[0].parsed - and isinstance(response.msgs[0].parsed, ReActStep) - ): - react_step = response.msgs[0].parsed - thought = react_step.thought - action = react_step.action - observation = react_step.observation - else: - thought = "" - action = "" - observation = None - - # Execute action if specified - if action: - logger.debug("Executing action: %s", action) - actual_observation = self._execute_action(action) - observation = actual_observation - else: - observation = None - - # Update scratchpad - scratchpad_entry: Dict[str, Optional[str]] = { - "Thought": thought or "", - "Action": action or "", - } - - if action: - scratchpad_entry["Observation"] = observation or None - self.scratchpad.append(scratchpad_entry) - - # Create final response + # Create final response message final_content = "\n".join( filter( None, [ - f"Thought: {thought}" if thought else None, - f"Action: {action}" if action else None, - f"Observation: {observation}" - if action and observation + f"Thought: {final_thought}" if final_thought else None, + f"Action: {final_action}" if final_action else None, + f"Observation: {final_observation}" + if final_action and final_observation else None, ], ) @@ -323,17 +424,12 @@ def step( content=final_content, ) - # Check if the action was Finish - terminated = bool(action and action.startswith("Finish")) - if terminated: - logger.info("Task completed after %d steps", self.step_count) - return ChatAgentResponse( msgs=[final_message], terminated=terminated, info={ - "thought": thought or "", - "action": action or "", - "observation": observation or "", + "thought": final_thought or "", + "action": final_action or "", + "observation": final_observation or "", }, ) diff --git a/examples/agents/react_agent_example.py b/examples/agents/react_agent_example.py index a01e3d4cd9..6ade282098 100644 --- a/examples/agents/react_agent_example.py +++ b/examples/agents/react_agent_example.py @@ -11,12 +11,27 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +from typing import Optional + from camel.agents import ReActAgent from camel.messages import BaseMessage -from camel.toolkits import SearchToolkit +from camel.toolkits import FunctionTool, SearchToolkit from camel.types import RoleType +def search(query: str) -> Optional[str]: + r"""Search for information using DuckDuckGo. + + Args: + query: The search query string to look up information + + Returns: + Optional[str]: The search results or None if no results found + """ + toolkit = SearchToolkit() + return toolkit.search_duckduckgo(query=query) + + def main() -> None: # Create system message and initialize agent system_message = BaseMessage( @@ -26,16 +41,16 @@ def main() -> None: content=( "You are a helpful assistant that can search information online. " "Use the search tool to find accurate information and " - "provide detailed answers. " + "provide detailed answers. Always verify information" + "from multiple sources when possible before providing" + "a final answer." ), ) - # Initialize toolkit and agent with search_duckduckgo tool - search_tool = SearchToolkit().search_duckduckgo - agent = ReActAgent( - system_message=system_message, - tools=[search_tool], - max_steps=5, + # Initialize search tool with proper schema + search_tool = FunctionTool( + func=search, # Use our documented function + synthesize_schema=True, # Let the tool auto-generate proper schema ) # Example queries that require search @@ -48,42 +63,36 @@ def main() -> None: for query in queries: print(f"\nQuery: {query}") print("-" * 50) + + # Create a fresh agent for each query to avoid state mixing + agent = ReActAgent( + system_message=system_message, + tools=[search_tool], + max_steps=5, + ) + + # Track steps + step_num = 1 response = agent.step(query) - print(f"Response:\n{response.info}") + + # Show intermediate steps from scratchpad + for step in agent.scratchpad: + print(f"Step {step_num}:") + print(f"Thought: {step.get('Thought', '')}") + print(f"Action: {step.get('Action', '')}") + print(f"Observation: {step.get('Observation', '')}") + print() + step_num += 1 + + # Show final response + print("Final Response:") + print("{") + print(f" \"thought\": \"{response.info['thought']}\",") + print(f" \"action\": \"{response.info['action']}\",") + print(f" \"observation\": \"{response.info['observation']}\"") + print("}") print("-" * 50) if __name__ == "__main__": main() - -""" -Example output: - -Query: What is the current population of Paris? --------------------------------------------------- -Response: -{ - "thought": "I found several sources that provide population estimates for - Paris. The most relevant ones indicate that the population of - Paris in 2023 is approximately 2.1 million for the city proper, - while the metropolitan area is around 11.2 million. I need to - summarize this information clearly.", - "action": "Finish", - "observation": "Task completed." -} --------------------------------------------------- - -Query: Who won the Nobel Prize in Physics in 2024? --------------------------------------------------- -Response: -{ - "thought": "I found multiple sources confirming the winners of the 2024 - Nobel Prize in Physics. The prize was awarded to John J. - Hopfield and Geoffrey E. Hinton for their contributions - to machine learning and artificial neural networks. - I will summarize this information.", - "action": "Finish", - "observation": "Task completed." -} --------------------------------------------------- -""" From 181c3e55f0323f01c9e7ba6a5e3798943a51f0ea Mon Sep 17 00:00:00 2001 From: Rishabh <134101578+GitHoobar@users.noreply.github.com> Date: Mon, 24 Feb 2025 23:57:02 +0530 Subject: [PATCH 10/10] fix --- camel/agents/react_agent.py | 44 +++++++++++++++++++------------------ camel/utils/__init__.py | 1 + 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/camel/agents/react_agent.py b/camel/agents/react_agent.py index 6958a52a4b..57fe76b31d 100644 --- a/camel/agents/react_agent.py +++ b/camel/agents/react_agent.py @@ -81,9 +81,10 @@ def __init__( tools: Optional[List[Union[FunctionTool, Callable]]] = None, max_steps: int = 10, ) -> None: + # Set up ReAct prompt first self._set_react_prompt() - # Combine original system message with ReAct prompt + # Combine with system message before parent init combined_content = ( f"{system_message.content}\n\n" f"{self.react_prompt}" ) @@ -91,14 +92,17 @@ def __init__( combined_content ) + # Initialize parent with combined message super().__init__( - system_message=react_system_message, model=model, tools=tools + system_message=react_system_message, # Use combined message here + model=model, + tools=tools, ) + + # Initialize ReAct specific attributes self.scratchpad: List[Dict[str, Optional[str]]] = [] - self._set_react_prompt() self.step_count = 0 self.max_steps = max_steps - logger.debug("ReActAgent initialized with %d tools", len(self.tools)) def _set_react_prompt(self) -> None: r"""Set up the ReAct prompt template following the paper's format. @@ -109,12 +113,13 @@ def _set_react_prompt(self) -> None: self.react_prompt = ( "You MUST ALWAYS use EXACTLY ONE of the following actions. " "You MUST ALWAYS include a 'thought' and 'action'.\n" - "- Search(query=):Use this to search information\n" + "- Search(query=): Use this to search information\n" "- Lookup(key=): Use this to look up specific info\n" - "- Finish(answer=): ONLY use this when you have ALL " + "- Finish(answer=): ONLY use this when you have ALL" "information needed to fully answer the question\n\n" "IMPORTANT: DO NOT use Finish until you are completely certain you" - "gathered all necessary information to provide complete answer\n\n" + "have gathered all necessary information " + "to provide a complete and accurate answer.\n\n" "Respond with JSON object with the keys 'thought' and 'action'.\n" "The 'action' value must be one of the three options above.\n" "\nExample response for Search:\n" @@ -124,13 +129,13 @@ def _set_react_prompt(self) -> None: '}\n\n' "Example response for continuing research:\n" '{\n' - ' "thought": "I found the population,but need to verify.",\n' + ' "thought": "I found the population, but I need to verify",\n' ' "action": "Search(query=Paris census official data latest)"\n' '}\n\n' "Example response for Finish (only when task is fully complete):\n" '{\n' - ' "thought":"I have found and verified needed information",\n' - ' "action": "Finish(answer=Paris population is 2.1M)"\n' + ' "thought":"I have found and verified information",\n' + ' "action":"Finish(answer=Population of Paris: 2.1 million)"\n' '}\n\n' "Current scratchpad:\n" "{scratchpad}" @@ -188,7 +193,7 @@ def _parse_action(self, action: str) -> Tuple[str, Dict[str, Any]]: Args: action (str): Action string in format, - "Action(param1=value1, param2=value2)" + "Action(param1=value1, param2=value2)" Returns: Tuple[str, Dict[str, Any]]: Function name and arguments dictionary @@ -253,7 +258,7 @@ def _execute_action(self, action: str) -> str: logger.info("Task completion requested") return "Task completed." - if not self.tools: + if not self._internal_tools: logger.warning("No tools available to execute action") return "No tools available to execute action." @@ -262,12 +267,10 @@ def _execute_action(self, action: str) -> str: func_name, params = self._parse_action(action) # Find and execute matching tool - for tool in self.tools: + for tool in self._internal_tools.values(): if isinstance(tool, FunctionTool): - if ( - tool.openai_tool_schema["function"]["name"].lower() - == func_name.lower() - ): + schema_name = tool.openai_tool_schema["function"]["name"] + if schema_name.lower() == func_name.lower(): return tool(**params) elif callable(tool): return tool(action) @@ -288,6 +291,7 @@ def step( self, input_message: Union[BaseMessage, str], response_format: Optional[type[BaseModel]] = None, + reason_params: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> ChatAgentResponse: r"""Perform ReAct cycles until task completion or max steps reached. @@ -295,7 +299,7 @@ def step( Args: input_message (Union[BaseMessage, str]): Initial input message. response_format (Optional[type[BaseModel]], optional): Expected - response format. + response format. **kwargs: Additional arguments passed to underlying model call. Returns: @@ -360,9 +364,7 @@ def step( action = react_step.action observation = react_step.observation else: - logger.error( - "Failed to parse model response into ReActStep format" - ) + logger.error("Failed to parse model response into ReActStep") thought = "" action = "" observation = None diff --git a/camel/utils/__init__.py b/camel/utils/__init__.py index ffdc26a0aa..c3cc96ea29 100644 --- a/camel/utils/__init__.py +++ b/camel/utils/__init__.py @@ -23,6 +23,7 @@ download_github_subdirectory, download_tasks, func_string_to_callable, + generate_prompt_for_structured_output, get_first_int, get_prompt_template_key_words, get_pydantic_major_version,