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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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." } -------------------------------------------------- """