Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: react agent integration #179

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions camel/agents/ReAct_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. ===========
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to lower case react_agent.py

# 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 @ CAMEL-AI.org. All Rights Reserved. ===========
from typing import Any, Dict, List, Optional, Tuple

from colorama import Fore
import re

from camel.agents import ChatAgent, BaseToolAgent
from camel.agents.chat_agent import ChatAgentResponse
from camel.messages import ChatMessage, SystemMessage
from camel.typing import ModelType
from camel.utils import print_text_animated


REACT_PROMPT_INSTRUCT = """
For each time of running, you should firstly give a thought about what action to be taken next for solving the target task, and then give an action to be executed.

You should use Thought to describe your thoughts about your plan to obtain information related to the target task.
And you should use Action to indicate what actions should be done, and then return.
Each action statement should be in the format: Action[<action_input>]
You do not need to output any observation! The observations will be obtained by the external tool executing the action you selected and be provided to you.
When there are multiple search-related actions available, consider using others when one search engine fails.

Your available actions are:

Finish[<argument>]
- input <argument>: any value to be returned
- This action finishes this task and returns any value specified by <argument>

"""

REACT_PROMPT_EXAMPLE = """
Remember: For each time of running, you should return after you output an action!

Here are is an example session (this example is only for presentation and works only if the available actions include "Search")
Task: Answer the question: What is the capital of France?
Thought: To answer this question, I should search for information about France.
Action: Search[France]

At this point you should return. And you will be called again with this:

Observation: France is a country. The capital is Paris.

You then output:

Thought: From the observation, the capital of France is Paris.
Action: Finish[Paris]
"""

# Regular verifier of Action statements
ACTION_RE = re.compile('^Action: *')

# Color settings for logging
TASK_COLOR = Fore.LIGHTYELLOW_EX
THOUGHT_COLOR = Fore.LIGHTCYAN_EX
ACTION_COLOR = Fore.LIGHTRED_EX
OBSERVE_COLOR = Fore.LIGHTGREEN_EX


def parse_action(act_str):
# example act_str: Search[entity]
print(f"Action string: {act_str}")
action = act_str.split('[')[0]
action_input = act_str[:-1].split('[')[1]
return action, action_input


class ReActAgent(ChatAgent):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the nearest future we are going to move away from inheritance to a "part of" principle for agents. A chat agent must be a field of ReactAgent.

r"""Class for managing conversions of a CAMEL agent following ReAct pattern.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every introduced code must be covered by a test.


Args:
system_message (SystemMessage): The system message for the chat agent.
model (ModelType, optional): The LLM model to use for generating
responses. (default :obj:`ModelType.GPT_4`)
model_config (Any, optional): Configuration options for the LLM model.
(default: :obj:`None`)
message_window_size (int, optional): The maximum number of previous
messages to include in the context window. If `None`, no windowing
is performed. (default: :obj:`None`)
action_space (Dict[Any], optional): The action space for the ReAct
agent. (default: :obj:`None`)
verbose (bool, optional): Whether to print the critic's messages.
"""

def __init__(
self,
system_message: SystemMessage,
model: ModelType = ModelType.GPT_4,
model_config: Optional[Any] = None,
message_window_size: Optional[int] = None,
action_space: Dict[str, BaseToolAgent] = None,
verbose: bool = False,
) -> None:
self.action_space = action_space

action_space_prompt = self.get_action_space_prompt()
init_prompt = '\n'.join([
REACT_PROMPT_INSTRUCT,
action_space_prompt,
REACT_PROMPT_EXAMPLE,
])
system_message.content = init_prompt

self.verbose = verbose
super().__init__(
system_message=system_message,
model=model,
model_config=model_config,
message_window_size=message_window_size,
)


def get_action_space_prompt(self) -> str:
r"""Returns the action space prompt.

Returns:
str: The action space prompt.
"""
tool_agents = set()
for agent in self.action_space.values():
tool_agents.add(agent)

return "\n".join(agent.description for agent in tool_agents)

def parse_thought_and_action(self,
response: ChatAgentResponse) -> Tuple[str,str]:
response_msg = response.msg.content
contents = [a.split(':')[-1].strip() for a in response_msg.split('\n')]
return contents[0], contents[1]


def step(
self,
input_message: ChatMessage,
max_turns: int = 10,
) -> Tuple[ChatMessage, bool, Dict[str, Any]]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature of step does not follow the signature of the base class. If you want to introduce a new signature you must name the method differently

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but this pattern was borrowed fromEmbodiedAgent and ChatAgent where they both used a completely different signature as the one in base class (def step(self) -> None:). I think the version in the base (BaseAgent) is too abstract and does not return anything and should not be used?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use EmbodiedAgent as a reference. There are some deficiencies in the code that we will fix soon. Please check your code with mypy and make sure you code does not introduce any warnings. Soon mypy will be made obligatory.

Copy link
Member

@lightaime lightaime Jul 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not totally agree with naming all the methods differently which will make it very difficult for the user to use. Just like PyTorch, every deviated Module has a different forward method with a different signature. It is still ok. IMO, the base class method's signature could be def step(self, *args, **kwargs) -> Any:.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key feature of Module is polymorphism, a Module can be composed, the implementation can be abstracted away. In camel I do not see a single instance of polymorphic use of step() and reset(), implying it is not needed so far.

r"""Performs a step in the conversation.

Args:
input_message (ChatMessage): The input message,
**which should specify a task to do**

Returns:
Tuple[ChatMessage, bool, Dict[str, Any]]: A tuple
containing the output messages, termination status, and
additional information.
"""
response = super().step(input_message)

if response.msgs is None or len(response.msgs) == 0:
raise RuntimeError("Got None output messages.")
if response.terminated:
raise RuntimeError(f"{self.__class__.__name__} step failed.")

if self.verbose:
# print_text_animated(TASK_COLOR + f"> Task: {input_message.content}")
print(TASK_COLOR + f"> Task: {input_message.content}")

i = 0
content = input_message.content
while i < max_turns:
i += 1
content += f"\n{response.msg.content}"

# Add the thought and action into the agent's conversation history
self.update_messages(
ChatMessage(role_name=self.role_name, role_type=self.role_type,
meta_dict=self.system_message.meta_dict,
role=self.system_message.role,
content=response.msg.content)
)

# Parse the new output containing a pair of Thought and Action
thought, action = self.parse_thought_and_action(response)
action, action_input = parse_action(action)

if self.verbose:
# print_text_animated(THOUGHT_COLOR + f"> Thought: {thought}")
# print_text_animated(ACTION_COLOR + f"> Action: {action}[{action_input}]")
print(THOUGHT_COLOR + f"> Thought: {thought}")
print(ACTION_COLOR + f"> Action: {action}[{action_input}]")

# Terminate
if action == 'Finish':
break

# operation argument is for ToolAgent providing multiple actions
obs = str(self.action_space[action].step(action_input, operation=action))
if self.verbose:
# print_text_animated(OBSERVE_COLOR + f"> Observation: {obs}")
print(OBSERVE_COLOR + f"> Observation: {obs}")

# TODO not sure which type of message should be used
obs = ChatMessage(
role_type=self.role_type,
role_name=self.system_message.role_name,
meta_dict=self.system_message.meta_dict,
role=self.system_message.role,
content=obs,
)
response = super().step(obs)


# TODO: Handle failed cases
# TODO: Note the output emits all previous messages which has already been added into the
# Agent's conversation history in the previous loop. RolePlaying should not update the conversation
# history by the output again.
finish_msg = ChatMessage(role_name=self.role_name, role_type=self.role_type,
meta_dict=input_message.meta_dict, role=input_message.role,
content=content)
return finish_msg, response.terminated, response.info
56 changes: 56 additions & 0 deletions camel/agents/tool_agents/Google_tool_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# =========== Copyright 2023 @ 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 @ CAMEL-AI.org. All Rights Reserved. ===========
import os
from typing import Any, Optional

from camel.agents.tool_agents import BaseToolAgent


class GoogleToolAgent(BaseToolAgent):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every introduced code must be covered by a test.

def __init__(
self,
name: str,
*args: Any,
) -> None:
try:
from langchain.utilities import SerpAPIWrapper
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are bringing in langchain as a dependency which is not reflected in requirements.txt

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In pyproject.toml, it is

except ImportError:
raise ValueError("Could not import langchain agents"
"Please use pip install langchain")

SAK = os.getenv('SERPAPI_KEY')

# TODO to replace the LangChain wrapper with self-implemented one
# Define LangHChain-implemented agent which can access Wiki
self.search = SerpAPIWrapper(serpapi_api_key=SAK)

self.name = name
self.description = \
f"""
GoogleSearch[<keyword>]
- input <keyword>: the information to be searched using Google search engine
- Keep a rule in mind: if the current search does not return valid page, try to make the search item more general and shorter, instead of making it finer
- Example usage: GoogleSearch[What is the current weather of London?]
- by which we can obtain the weather information in London through Google
"""

def reset(self) -> None:
pass

def step(
self,
question,
**kwargs: Any,
) -> Any:
return self.search.run(question, **kwargs)
Loading
Loading