diff --git a/examples/distributed_simulation/main.py b/examples/distributed_simulation/main.py index 1ccad506e..1f53e9a99 100644 --- a/examples/distributed_simulation/main.py +++ b/examples/distributed_simulation/main.py @@ -196,7 +196,7 @@ def run_main_process( Msg( name="Moderator", role="assistant", - content=f"The average value is {summ/cnt} [takes {et-st} s]", + content=f"The average value is {summ / cnt} [takes {et - st} s]", ), ) diff --git a/examples/environments/README.md b/examples/environments/README.md new file mode 100644 index 000000000..640dac109 --- /dev/null +++ b/examples/environments/README.md @@ -0,0 +1,124 @@ +# Chatroom Example + +This example will show +- How to set up a chat room and use environment to share the chat history. +- How to generate a conversation between three agents. +- How to set up an auto reply assistant. + + +## Background + +Here we demonstrate two types of chat room conversations with environment, one is self-organizing dialogue, and the other is automatic reply assistant. +For self-organizing conversations, after setting the agent persona for participating in the chat room, the model will automatically generate a reply based on the set agent persona and the history of chat room. Meanwhile, each agent can also reply to the corresponding agent based on the "@" symbol. +For the automatic reply assistant, if the user does not input text for a period of time, the model will automatically generate a reply based on the history of chat room. + + +## Tested Models + +These models are tested in this example. For other models, some modifications may be needed. +- `dashscope_chat` with `qwen-turbo` +- gpt-4o + + +## Prerequisites + +- Install the lastest version of AgentScope by + +```bash +git clone https://github.com/modelscope/agentscope +cd agentscope +pip install -e .\[distribute\] +``` + +- Prepare an OpenAI API key or Dashscope API key + +## Running the Example + +First fill your OpenAI API key or Dashscope API key in `chatroom_example.py` and `chatroom_with_assistant_example.py`, then execute these files to run the chatroom. +The following are the parameters required to run the script: + +- `--use-dist`: Enable distributed mode. +- `--studio-url`: The url of agentscope studio. +- `--timeout`: Timeout for auto reply with assistant. + +For example, if you want to start the simplest example for chatroom, you can use the following command + +```bash +python chatroom_example.py +``` + +And if you want to run an example of `chatroom_with_assistant_example` in studio, you can use the following command. + +```bash +python chatroom_with_assistant_example.py --studio-url "http://127.0.0.1:5000" +``` + +Here is an example output of `python chatroom_example.py`: + +``` +2024-08-22 15:35:45.140 | INFO | agentscope.manager._model:load_model_configs:115 - Load configs for model wrapper: dash +2024-08-22 15:35:45.140 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +2024-08-22 15:35:45.140 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +2024-08-22 15:35:45.141 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +Carol: Starting our project planning session today! Let's discuss the key components for an engaging open-world game. Bob, could you share your thoughts on the game mechanics and systems? +Alice: Hey everyone! I've been thinking about the open world game we're working on. It would be great if we could gather some ideas for unique NPC behaviors that would add depth to our world. @Carol, do you have any specific scenarios or character types in mind that you think would fit well into our game environment? +2024-08-22 15:35:51.633 | INFO | envs.chatroom:__call__:228 - Alice mentioned Carol. +Bob: Sure, for the game mechanics, I think incorporating dynamic weather systems and time of day changes could enhance player immersion. This way, NPC activities and environments adapt accordingly, creating a more realistic experience. @Alice, what kind of NPC behaviors were you envisioning? Could they interact differently based on the current weather or time of day? +2024-08-22 15:35:54.194 | INFO | envs.chatroom:__call__:228 - Bob mentioned Alice. +Carol: Absolutely, integrating dynamic NPC behaviors based on the environment and time of day sounds intriguing. For example, we could have traders setting up their stalls only during certain hours, or farmers tending to their crops in daylight but resting under the stars. @Bob, how do you think we could implement these mechanics to ensure they don't feel too repetitive for players over time? +2024-08-22 15:36:02.657 | INFO | envs.chatroom:__call__:228 - Carol mentioned Bob. +Alice: Absolutely, I was thinking along those lines as well. For example, fishermen might have better luck during certain times of day or under specific weather conditions. Farmers could have urgent tasks depending on the season or weather forecasts. And maybe traders would have special items to sell or deals to offer at night or during festivals. What do you think, @Bob? +2024-08-22 15:36:03.409 | INFO | envs.chatroom:__call__:228 - Alice mentioned Bob. +Bob: I think those ideas are fantastic! To avoid repetition, we could introduce random events or quests that trigger based on the time of day or weather conditions. This would keep the gameplay fresh and engaging. Additionally, NPC preferences could change seasonally, adding another layer of complexity. For instance, a fisherman might prefer fishing during high tide, which could vary by season. I believe these dynamic elements will significantly enrich the player's experience. Let's brainstorm some specific examples and scenarios to flesh out these ideas further. +Carol: Great suggestions, everyone! Let's start documenting these ideas and create a detailed list. We'll also need to consider how these mechanics impact the game's overall narrative and pacing. Once we have a solid foundation, we can move on to designing the visual aspects and user interface. @Alice, can you give us some preliminary thoughts on how we might visually represent these dynamic changes in the game world? +2024-08-22 15:36:14.411 | INFO | envs.chatroom:__call__:228 - Carol mentioned Alice. +Alice: Great suggestion, @Bob! I agree that introducing seasonal changes and random events will make the game world more alive and interesting. Let's consider having a festival once a month where NPCs participate in unique activities, like a harvest festival in autumn or a winter market with special winter-only goods. We could also have natural disasters like storms or wildfires that affect the environment and NPC behavior, forcing players to adapt their strategies. What do you think about incorporating such unpredictable elements into our game design? +2024-08-22 15:36:15.903 | INFO | envs.chatroom:__call__:228 - Alice mentioned Bob. +Bob: Those are excellent ideas, @Alice! The festivals and natural disasters sound like they could really bring the world to life and challenge players in different ways. I'm particularly intrigued by the concept of festivals. Let's explore how we can integrate these events seamlessly into the game world and ensure they're both fun and immersive. @Carol, could you help us think about how these events might impact the overall narrative and quest progression? +2024-08-22 15:36:18.267 | INFO | envs.chatroom:__call__:228 - Bob mentioned Alice. +2024-08-22 15:36:18.268 | INFO | envs.chatroom:__call__:228 - Bob mentioned Carol. +Carol: Absolutely, integrating festivals and natural disasters will definitely add depth to the game world and provide unique challenges for players. These events can serve as pivotal points in the narrative, influencing storylines and quest objectives. For instance, a festival might require players to help prepare or manage specific aspects of the event, while a natural disaster could lead to emergent quests that involve rescue operations or rebuilding efforts. Let's outline how these events can be woven into the main storyline and side quests to maintain engagement throughout the game. +Alice: Absolutely, @Bob! Festivals and natural disasters can definitely add a lot of depth and excitement to the game. I think it's important to balance these elements so they don't overwhelm the player or disrupt the flow of the game too much. We should aim to make them integral parts of the story and questlines. For example, a festival could lead to a special quest where players help organize events or solve puzzles related to the festival. As for natural disasters, they could serve as obstacles that players must overcome, either by preparing for them or by finding creative solutions when they occur unexpectedly. Let's discuss how we can integrate these elements into our game design in a way that enhances the overall player experience. +2024-08-22 15:36:28.846 | INFO | envs.chatroom:__call__:228 - Alice mentioned Bob. +Bob: I think this plan is shaping up beautifully. Let's focus on creating detailed mechanics for festivals and disasters, ensuring they not only add to the gameplay but also enhance the storytelling. We can then move on to refining the NPC behaviors and integrating them with the environmental changes. @Carol, @Alice, let's schedule a meeting to go over these concepts in detail and start fleshing out the designs. Goodbye for now, everyone. Let's make sure to touch base soon with updates on our progress. +2024-08-22 15:36:30.553 | INFO | envs.chatroom:__call__:228 - Bob mentioned Alice. +2024-08-22 15:36:30.554 | INFO | envs.chatroom:__call__:228 - Bob mentioned Carol. +Carol: Great summary, Bob! Your plan aligns perfectly with our goals for enhancing player immersion and narrative depth. Let's indeed focus on festivals and disasters as key elements that will drive our game's dynamics. Scheduling that meeting sounds like a good idea to delve deeper into these concepts. I'll coordinate the details and send out a calendar invite shortly. Looking forward to our next steps and seeing how we can refine NPC behaviors and environmental interactions. Keep up the great work, everyone! Goodbye for now, and let's stay in touch for updates. +Alice: Great summary, Bob! I'm excited to dive deeper into these mechanics and NPC behaviors. Let's ensure we capture the essence of each festival and disaster, making them unique and memorable. Looking forward to the meeting and seeing everyone's ideas come to life. Goodbye for now! +``` + +Another example output of `python chatroom_with_assistant_example.py`: + +``` +Alice: Hi Bob, nice to meet you. Can you tell me a bit about yourself? +2024-08-22 15:43:21.982 | INFO | agentscope.manager._model:load_model_configs:115 - Load configs for model wrapper: dash +2024-08-22 15:43:21.982 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +2024-08-22 15:43:21.982 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +Bob: Of course, nice to meet you too, Alice. I'm originally from Hunan, a beautiful province in southern China known for its spicy food and stunning natural scenery. +Alice: Oh, that sounds fascinating! So, what do you do for a living, Bob? +Bob: I work as a software engineer. I've been in this field for about 5 years now, designing and developing various applications and systems. It's a challenging but rewarding job that keeps me on my toes. +Alice: That's great! It takes a lot of skill and creativity to be a good software engineer. Speaking of creativity, do you have any hobbies or activities you enjoy outside of work? +Bob: Yes, I'm quite passionate about playing board games. There's something really enjoyable about the strategy, competition, and social interaction they provide. Whether it's classic games like chess or more modern ones like Settlers of Catan, I find them all very engaging. +Alice: Board games are a wonderful way to unwind and connect with friends and family. It sounds like you have a great balance between your professional and personal life, Bob. Thanks for sharing! +Bob: Absolutely, thank you for asking, Alice. It was a pleasure chatting with you. +2024-08-22 15:43:21.985 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +Carol: Hello Bob, thank you for joining us today. To start off, could you tell me where you're from? +User input: +Bob: [auto reply] Sure, Carol. I'm from Hunan, a province in southern China famous for its spicy cuisine and breathtaking landscapes. +Carol: That's interesting! Thank you, Bob. Now, could you share which school you graduated from? +User input: +Bob: [auto reply] Sorry, but I can't answer that question based on our previous chat. +Carol: OK, Bob. What about your profession? Could you tell me more about that? +User input: +Bob: [auto reply] I work as a software engineer. My role involves designing and developing various applications and systems. I've been doing this for around 5 years now. +User input: +Carol: That sounds like an exciting career path. Lastly, could you share some of your hobbies with us? + +Bob: [auto reply] Sure, I enjoy playing board games in my free time. They offer a great mix of strategy, competition, and social interaction. From classic games like chess to modern ones like Settlers of Catan, I find them all very engaging. +User input: +Carol: Thank you, Bob, for sharing these details with us. It was great having you on the show. Have a fantastic day! + +Goodbye + +Bob: [auto reply] Goodbye! It was nice chatting with you. Have a great day! +``` \ No newline at end of file diff --git a/examples/environments/chatroom_example.py b/examples/environments/chatroom_example.py new file mode 100644 index 000000000..9215552f2 --- /dev/null +++ b/examples/environments/chatroom_example.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +"""A simple example of chatroom with three agents.""" + +import os +import argparse + +from envs.chatroom import ChatRoom, ChatRoomAgent + +import agentscope +from agentscope.message import Msg + + +def parse_args() -> argparse.Namespace: + """Parse arguments""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--logger-level", + choices=["DEBUG", "INFO"], + default="INFO", + ) + parser.add_argument( + "--use-dist", + action="store_true", + ) + parser.add_argument( + "--studio-url", + default=None, + type=str, + ) + return parser.parse_args() + + +def main(args: argparse.Namespace) -> None: + """Example for chatroom""" + # Prepare the model configuration + YOUR_MODEL_CONFIGURATION_NAME = "dash" + YOUR_MODEL_CONFIGURATION = [ + { + "model_type": "dashscope_chat", + "config_name": "dash", + "model_name": "qwen-turbo", + "api_key": os.environ.get("DASH_API_KEY", ""), + }, + ] + + # Initialize the agents + agentscope.init( + model_configs=YOUR_MODEL_CONFIGURATION, + use_monitor=False, + logger_level=args.logger_level, + studio_url=args.studio_url, + ) + + ann = Msg( + name="Boss", + content=( + "This is a game development work group, " + "please discuss how to develop an open world game." + ), + role="system", + ) + r = ChatRoom(name="chat", announcement=ann, to_dist=args.use_dist) + + # Setup the persona of Alice, Bob and Carol + alice = ChatRoomAgent( # Game Art Designer + name="Alice", + sys_prompt=r"""You are a game art designer named Alice. """ + r"""Programmer Bob and game planner Carol are your colleagues, """ + r"""and you need to collaborate with them to complete an open """ + r"""world game. Please ask appropriate question to planner or """ + r"""generate appropriate responses in this work group based on """ + r"""the following chat history. When you need to mention someone, """ + r"""you can use @ to remind them. You only need to output Alice's """ + r"""possible replies, without giving anyone else's replies or """ + r"""continuing the conversation. When the discussion is complete, """ + r"""you need to reply with a message containing 'Goodbye' to """ + r"""indicate exiting the conversation.""", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + to_dist=args.use_dist, + ) + alice.join(r) + + bob = ChatRoomAgent( # Game Programmer + name="Bob", + sys_prompt=r"""You are a game programmer named Bob. """ + r"""Art designer Alice and game planner Carol are your colleagues, """ + r"""and you need to collaborate with them to complete an open """ + r"""world game. Please ask appropriate questions or generate """ + r"""appropriate responses in the work group based on the following """ + r"""historical records. When you need to mention someone, you can """ + r"""use @ to remind them. You only need to output Bob's possible """ + r"""replies, without giving anyone else's replies or continuing """ + r"""the conversation. When the discussion is complete, you need """ + r"""to reply with a message containing 'Goodbye' to indicate """ + r"""exiting the conversation.""", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + to_dist=args.use_dist, + ) + bob.join(r) + + carol = ChatRoomAgent( # Game Designer + name="Carol", + sys_prompt=r"""You are a game planner named Carol. """ + r"""Programmer Bob and art designer Alice are your colleagues, """ + r"""and you need to guide them in developing an open world game. """ + r"""Please generate a suitable response in this work group based """ + r"""on the following chat history. When you need to mention """ + r"""someone, you can use @ to remind them. You only need to output """ + r"""Carol's possible replies, without giving anyone else's replies """ + r"""or continuing the conversation. When the discussion is """ + r"""complete, you need to reply with a message containing """ + r"""'Goodbye' to indicate exiting the conversation.""", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + to_dist=args.use_dist, + ) + carol.join(r) + + # Start the chat + r.chatting( + delay={carol.agent_id: 0, alice.agent_id: 5, bob.agent_id: 7}, + ) + + +if __name__ == "__main__": + main(parse_args()) diff --git a/examples/environments/chatroom_with_assistant_example.py b/examples/environments/chatroom_with_assistant_example.py new file mode 100644 index 000000000..599c34947 --- /dev/null +++ b/examples/environments/chatroom_with_assistant_example.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +"""A simple example of chatroom with chatting assistant.""" + +import os +import argparse + +from envs.chatroom import ChatRoom, ChatRoomAgent, ChatRoomAgentWithAssistant + +import agentscope +from agentscope.message import Msg + + +def parse_args() -> argparse.Namespace: + """Parse arguments""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--logger-level", + choices=["DEBUG", "INFO"], + default="INFO", + ) + parser.add_argument( + "--use-dist", + action="store_true", + ) + parser.add_argument( + "--studio-url", + default=None, + type=str, + ) + parser.add_argument( + "--timeout", + default=5, + type=int, + ) + return parser.parse_args() + + +def main(args: argparse.Namespace) -> None: + """Example for chatroom with assistant""" + # Prepare the model configuration + YOUR_MODEL_CONFIGURATION_NAME = "dash" + YOUR_MODEL_CONFIGURATION = [ + { + "model_type": "dashscope_chat", + "config_name": "dash", + "model_name": "qwen-turbo", + "api_key": os.environ.get("DASH_API_KEY", ""), + }, + ] + + # Initialize the agents + agentscope.init( + model_configs=YOUR_MODEL_CONFIGURATION, + use_monitor=False, + logger_level=args.logger_level, + studio_url=args.studio_url, + ) + + ann = Msg(name="", content="", role="system") + r = ChatRoom(name="chat", announcement=ann, to_dist=args.use_dist) + + # Setup the persona of Alice and Bob + alice = ChatRoomAgent( + name="Alice", + sys_prompt=r"""""", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + to_dist=args.use_dist, + ) + alice.join(r) + + bob = ChatRoomAgentWithAssistant( + name="Bob", + sys_prompt=r"""You are Bob's chat room assistant and he is """ + r"""currently unable to reply to messages. Please generate a """ + r"""suitable response based on the following chat history. """ + r"""The content you reply to must be based on the chat history. """ + r"""Please refuse to reply to questions that are beyond the scope """ + r"""of the chat history.""", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + to_dist=args.use_dist, + timeout=args.timeout, + ) + bob.join(r) + + # Setup some chatting history + alice.speak( + Msg( + name="Alice", + content=( + "Hi Bob, nice to meet you. " + "Can you tell me a bit about yourself?" + ), + role="assistant", + ), + ) + bob.speak( + Msg( + name="Bob", + content=( + "Of course, nice to meet you too, Alice. " + "I'm originally from Hunan, a beautiful province in southern " + "China known for its spicy food and stunning natural scenery." + ), + role="user", + ), + ) + alice.speak( + Msg( + name="Alice", + content=( + "Oh, that sounds fascinating! " + "So, what do you do for a living, Bob?" + ), + role="assistant", + ), + ) + bob.speak( + Msg( + name="Bob", + content=( + "I work as a software engineer. I've been in this field for " + "about 5 years now, designing and developing various " + "applications and systems. It's a challenging but rewarding " + "job that keeps me on my toes." + ), + role="user", + ), + ) + alice.speak( + Msg( + name="Alice", + content=( + "That's great! It takes a lot of skill and creativity to be " + "a good software engineer. Speaking of creativity, do you " + "have any hobbies or activities you enjoy outside of work?" + ), + role="assistant", + ), + ) + bob.speak( + Msg( + name="Bob", + content=( + "Yes, I'm quite passionate about playing board games. " + "There's something really enjoyable about the strategy, " + "competition, and social interaction they provide. Whether " + "it's classic games like chess or more modern ones like " + "Settlers of Catan, I find them all very engaging." + ), + role="user", + ), + ) + alice.speak( + Msg( + name="Alice", + content=( + "Board games are a wonderful way to unwind and connect with " + "friends and family. It sounds like you have a great balance " + "between your professional and personal life, Bob. " + "Thanks for sharing!" + ), + role="assistant", + ), + ) + bob.speak( + Msg( + name="Bob", + content=( + "Absolutely, thank you for asking, Alice. " + "It was a pleasure chatting with you." + ), + role="user", + ), + ) + + # Setup the persona of Carol + carol = ChatRoomAgent( + name="Carol", + sys_prompt=r"""You are Carol, and now you need to interview Bob. """ + r"""Just ask him where he is from, which school he graduated from, """ + r"""his profession, and his hobbies. At the end of the interview, """ + r"""please output a reply containing Goodbye to indicate the end """ + r"""of the conversation.""", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + to_dist=args.use_dist, + ) + carol.join(r) + + # Start the chat + r.chatting(delay={carol.agent_id: 0, bob.agent_id: 5}) + + +if __name__ == "__main__": + main(parse_args()) diff --git a/examples/environments/envs/chatroom.py b/examples/environments/envs/chatroom.py index 7997f0802..34f5a36b2 100644 --- a/examples/environments/envs/chatroom.py +++ b/examples/environments/envs/chatroom.py @@ -1,7 +1,12 @@ # -*- coding: utf-8 -*- """An env used as a chatroom.""" -from typing import List +from typing import List, Any, Union, Mapping, Generator, Tuple, Optional from copy import deepcopy +import re +import random +import threading +import time +from loguru import logger from agentscope.agents import AgentBase from agentscope.message import Msg @@ -15,6 +20,9 @@ Event, event_func, ) +from agentscope.models import ModelResponse +from agentscope.studio._client import _studio_client +from agentscope.web.gradio.utils import user_input class ChatRoomMember(BasicEnv): @@ -45,6 +53,16 @@ def agent(self) -> AgentBase: """Get the agent of the member.""" return self._agent + def chatting(self, delay: int = 1) -> None: + """Make the agent chatting in the chatroom.""" + time.sleep(delay) + while True: + msg = self._agent() + if "goodbye" in msg.content.lower(): + break + sleep_time = random.randint(1, 5) + time.sleep(sleep_time) + class ChatRoom(BasicEnv): """A chatroom env.""" @@ -55,6 +73,7 @@ def __init__( announcement: Msg = None, participants: List[AgentBase] = None, all_history: bool = False, + **kwargs: Any, ) -> None: """Init a ChatRoom instance. @@ -68,10 +87,11 @@ def __init__( """ super().__init__( name=name, + **kwargs, ) - self.children = { - p.name: p for p in (participants if participants else []) - } + self.children = {} + for p in participants if participants else []: + self.join(p) self.event_listeners = {} self.all_history = all_history self.history = [] @@ -87,6 +107,7 @@ def join(self, agent: AgentBase) -> bool: agent=agent, history_idx=len(self.history), ) + self.add_listener("speak", Mentioned(agent)) return True @event_func @@ -162,3 +183,202 @@ def __call__(self, env: Env, event: Event) -> None: ), ): raise EnvListenerError("Fail to add listener.") + + def chatting_parse_func(self, response: ModelResponse) -> ModelResponse: + """Parse the response of the chatting agent.""" + pattern_str = "" + for child in self.children.values(): + if pattern_str: + pattern_str += "|" + pattern_str += rf"""\s?{child.agent_name}: """ + pattern = re.compile(pattern_str, re.DOTALL) + logger.debug(repr(pattern_str)) + logger.debug(response.text) + texts = [s.strip() for s in pattern.split(response.text)] + logger.debug(texts) + return ModelResponse(text=texts[0]) + + def chatting(self, delay: Union[int, Mapping[str, int]] = 1) -> None: + """Make all agents chatting in the chatroom.""" + tasks = [] + for agent_id, child in self.children.items(): + if isinstance(delay, int): + tasks.append( + threading.Thread(target=child.chatting, args=(delay,)), + ) + else: + if agent_id not in delay: + continue + tasks.append( + threading.Thread( + target=child.chatting, + args=(delay[agent_id],), + ), + ) + for task in tasks: + task.start() + for task in tasks: + task.join() + + +class Mentioned(EventListener): + """A listener that will be called when a message is mentioned the agent""" + + def __init__( + self, + agent: AgentBase, + ) -> None: + super().__init__(name=f"mentioned_agent_{agent.name}") + self.agent = agent + self.pattern = re.compile(r"""(?<=@)\w*""", re.DOTALL) + + def __call__(self, env: Env, event: Event) -> None: + find_result = self.pattern.findall(str(event.args["message"].content)) + if self.agent.name in find_result: + logger.info( + f"{event.args['message'].name} mentioned {self.agent.name}.", + ) + self.agent.add_mentioned_message(event.args["message"]) + + +class ChatRoomAgent(AgentBase): + """A agent with chat room""" + + def __init__( # pylint: disable=W0613 + self, + name: str, + sys_prompt: str, + model_config_name: str, + **kwargs: Any, + ) -> None: + super().__init__( + name=name, + sys_prompt=sys_prompt, + model_config_name=model_config_name, + ) + self.room = None + self.mentioned_messages = [] + self.mentioned_messages_lock = threading.Lock() + + def add_mentioned_message(self, msg: Msg) -> None: + """Add mentioned messages""" + with self.mentioned_messages_lock: + self.mentioned_messages.append(msg) + + def join(self, room: ChatRoom) -> bool: + """Join a room""" + self.room = room + return room.join(self) + + def generate_hint(self) -> Msg: + """Generate a hint for the agent""" + if self.mentioned_messages: + hint = ( + self.sys_prompt + + r"""\n\nYou have be mentioned in the following message, """ + r"""please generate an appropriate response.""" + ) + for message in self.mentioned_messages: + hint += f"\n{message.name}: " + message.content + self.mentioned_messages = [] + return Msg("system", hint, role="system") + else: + return Msg("system", self.sys_prompt, role="system") + + def speak( + self, + content: Union[str, Msg, Generator[Tuple[bool, str], None, None]], + ) -> None: + """Speak to room. + + Args: + content + (`Union[str, Msg, Generator[Tuple[bool, str], None, None]]`): + The content of the message to be spoken in chatroom. + """ + super().speak(content) + self.room.speak(content) + + def reply(self, x: Msg = None) -> Msg: + """Generate reply to chat room""" + msg_hint = self.generate_hint() + self_msg = Msg(name=self.name, content="", role="assistant") + + history = self.room.get_history(self.agent_id) + prompt = self.model.format( + msg_hint, + history, + self_msg, + ) + logger.debug(prompt) + response = self.model( + prompt, + parse_func=self.room.chatting_parse_func, + max_retries=3, + ).text + msg = Msg(name=self.name, content=response, role="assistant") + if response: + self.speak(msg) + return msg + + +class ChatRoomAgentWithAssistant(ChatRoomAgent): + """A ChatRoomAgent with assistant""" + + def __init__( + self, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.timeout = timeout + + def reply(self, x: Msg = None) -> Msg: + if _studio_client.active: + logger.info( + f"Waiting for input from:\n\n" + f" * {_studio_client.get_run_detail_page_url()}\n", + ) + raw_input = _studio_client.get_user_input( + agent_id=self.agent_id, + name=self.name, + require_url=False, + required_keys=None, + timeout=self.timeout, + ) + + logger.info("Python: receive ", raw_input) + if raw_input is None: + content = None + else: + content = raw_input["content"] + else: + time.sleep(0.5) + try: + content = user_input(timeout=self.timeout) + except TimeoutError: + content = None + + if content is not None: # user input + response = content + else: # assistant reply + msg_hint = self.generate_hint() + self_msg = Msg(name=self.name, content="", role="assistant") + + history = self.room.get_history(self.agent_id) + prompt = self.model.format( + msg_hint, + history, + self_msg, + ) + logger.debug(prompt) + response = self.model( + prompt, + parse_func=self.room.chatting_parse_func, + max_retries=3, + ).text + if not response.startswith("[auto reply]"): + response = "[auto reply] " + response + msg = Msg(name=self.name, content=response, role="user") + self.speak(msg) + return msg diff --git a/examples/paper_llm_based_algorithm/src/rag.py b/examples/paper_llm_based_algorithm/src/rag.py index 173801402..01580dc4d 100644 --- a/examples/paper_llm_based_algorithm/src/rag.py +++ b/examples/paper_llm_based_algorithm/src/rag.py @@ -54,7 +54,7 @@ def get_pos_num_str(num: int) -> str: for i in range(len_passcode): pos_num_str = get_pos_num_str(i + 1) target_sentences.append( - f"The {i+1}-{pos_num_str} digit of the passcode " + f"The {i + 1}-{pos_num_str} digit of the passcode " f"to the {target_object} is {true_solution[i]}. ", ) random.shuffle(target_sentences) @@ -69,7 +69,7 @@ def get_pos_num_str(num: int) -> str: idx = random.choice(range(len_passcode)) pos_num_str = get_pos_num_str(idx + 1) s = ( - f"The {idx+1}-{pos_num_str} digit of the passcode " + f"The {idx + 1}-{pos_num_str} digit of the passcode " f"to the {obj} is {pc[idx]}. " ) length_total += len(s) diff --git a/examples/parallel_service/README.md b/examples/parallel_service/README.md new file mode 100644 index 000000000..42bc83059 --- /dev/null +++ b/examples/parallel_service/README.md @@ -0,0 +1,88 @@ +# Parallel Service Example + +This example presents a methodology for converting the `service` function into a distributed version capable of running in parallel. + +## Background + +The original implementation of the `service` functions was executed locally. In scenarios where multiple independent `service` functions need to be executed concurrently, such as executing `web_digest` followed by the retrieved results on `google_search` to produce a summary of relevant webpage content, serial execution can lead to inefficiencies due to waiting for each result sequentially. + +In this example, we will illustrate how to transform the `web_digest` function into a distributed version, enabling it to operate in a parallel fashion. This enhancement will not only improve the parallelism of the process but also significantly reduce the overall runtime. + + +## Tested Models + +These models are tested in this example. For other models, some modifications may be needed. +- `dashscope_chat` with `qwen-turbo` +- gpt-4o + + +## Prerequisites + +- Install the lastest version of AgentScope by + +```bash +git clone https://github.com/modelscope/agentscope +cd agentscope +pip install -e .\[distribute\] +``` + +- Prepare an OpenAI API key or Dashscope API key + +- For search engines, this example now supports two types of search engines, google and bing. The configuration items for each of them are as follows: + + - google + - `api-key` + - `cse-id` + - bing + - `api-key` + + +## Running the Example + +First fill your OpenAI API key or Dashscope API key in `parallel_service.py` file. +The following are the parameters required to run the script: + +- `--use-dist`: Enable distributed mode. +- `--search-engine`: The search engine used, currently supports `google` or `bing`. +- `--api-key`: API key for google or bing. +- `--cse-id`: CSE id for google (If you use bing, ignore this parameter). + +For instance, if you wish to execute an example of `web_digest` sequentially, please use the following command: + +```bash +python parallel_service.py --api-key [google-api-key] --cse-id [google-cse-id] +``` + +Conversely, if you intend to execute an example of parallel `web_digest`, you may use the following command: + +```bash +python parallel_service.py --api-key [google-api-key] --cse-id [google-cse-id] --use-dist +``` + +Here is an example output of `python parallel_service.py --api-key [google-api-key] --cse-id [google-cse-id]`: + +``` +2024-09-06 11:25:10.435 | INFO | agentscope.manager._model:load_model_configs:115 - Load configs for model wrapper: dash +2024-09-06 11:25:10.436 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +User input: Aside from the Apple Remote, what other device can control the program Apple Remote was originally designed to interact with? +User: Aside from the Apple Remote, what other device can control the program Apple Remote was originally designed to interact with? +... +system: You have failed to generate a response in the maximum iterations. Now generate a reply by summarizing the current situation. +assistant: Based on the search results, the iOS Remote Control for Apple TV is an alternative to the Apple Remote for interacting with devices like Apple TV. However, it has received mixed reviews, with some users suggesting adjustments to the touchpad sensitivity or using specific navigation techniques to improve the experience. If Zwift users are unsatisfied with the current remote functionality, they might consider exploring other platforms or hardware. +2024-09-06 11:27:24.135 | INFO | __main__:main:184 - Time taken: 115.18411183357239 seconds +``` + +Another example output of `python parallel_service.py --api-key [google-api-key] --cse-id [google-cse-id] --use-dist`: + +``` +2024-09-06 11:36:55.235 | INFO | agentscope.manager._model:load_model_configs:115 - Load configs for model wrapper: dash +2024-09-06 11:36:55.237 | INFO | agentscope.models.model:__init__:203 - Initialize model by configuration [dash] +User input: Aside from the Apple Remote, what other device can control the program Apple Remote was originally designed to interact with? +User: Aside from the Apple Remote, what other device can control the program Apple Remote was originally designed to interact with? +... +system: You have failed to generate a response in the maximum iterations. Now generate a reply by summarizing the current situation. +assistant: Thought: The search has been conducted, but there seems to be an issue with retrieving the relevant tags. Despite this, I have found an affordable alternative to the Apple Remote called the aarooGo Remote Control, which can control Apple TV. This device is compatible with all Apple TV models and offers basic controls like power, volume, and mute without a touchpad, making it a cost-effective solution for controlling Apple TV. + +Response: After conducting a search, I found an affordable alternative to the Apple Remote called the aarooGo Remote Control. This device can control Apple TV and is compatible with all Apple TV models. It offers basic controls like power, volume, and mute without a touchpad, making it a cost-effective solution for controlling your Apple TV. +2024-09-06 11:38:05.459 | INFO | __main__:main:182 - Time taken: 63.02961325645447 seconds +``` \ No newline at end of file diff --git a/examples/parallel_service/parallel_service.py b/examples/parallel_service/parallel_service.py new file mode 100644 index 000000000..2ead87b82 --- /dev/null +++ b/examples/parallel_service/parallel_service.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +"""An example parallel service execution.""" + +from typing import Sequence, Any, Callable +import os +import time +import argparse +from functools import partial +from loguru import logger + +import agentscope +from agentscope.service import ( + google_search, + bing_search, + digest_webpage, + ServiceToolkit, +) +from agentscope.service.service_response import ( + ServiceResponse, + ServiceExecStatus, +) +from agentscope.agents import UserAgent, ReActAgent +from agentscope.manager import ModelManager +from agentscope.rpc.rpc_meta import RpcMeta, async_func + + +class RpcService(metaclass=RpcMeta): + """The RPC service class.""" + + def __init__( + self, + service_func: Callable[..., Any], + **kwargs: Any, + ) -> None: + """ + Initialize the distributed service function. + + Args: + service_func (`Callable[..., Any]`): The service function to be + wrapped. + **kwargs: Additional keyword arguments passed to the service. + """ + if "model_config_name" in kwargs: + model_config_name = kwargs.pop("model_config_name") + model_manager = ModelManager.get_instance() + model = model_manager.get_model_by_config_name(model_config_name) + kwargs["model"] = model + self.service_func = partial(service_func, **kwargs) + + @async_func + def __call__(self, *args: tuple, **kwargs: dict) -> Any: + """ + Execute the service function with the given arguments. + + Args: + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + `ServiceResponse`: The execution results of the services. + """ + try: + result = self.service_func(*args, **kwargs) + except Exception as e: + result = ServiceResponse( + status=ServiceExecStatus.ERROR, + content=str(e), + ) + return result + + +def search_and_digest_webpage( + query: str, + search_engine_type: str = "google", + num_results: int = 10, + api_key: str = None, + cse_id: str = None, + model_config_name: str = None, + html_selected_tags: Sequence[str] = ("h", "p", "li", "div", "a"), + dist_search: bool = False, +) -> ServiceResponse: + """ + Search question with search engine and digest the website in search result. + + Args: + query (`str`): + The search query string. + search_engine_type (`str`, optional): the search engine to use. + Defaults to "google". + num_results (`int`, defaults to `10`): + The number of search results to return. + api_key (`str`, optional): api key for the search engine. Defaults + to None. + cse_id (`str`, optional): cse_id for the search engine. Defaults to + None. + model_config_name (`str`, optional): The name of model + configuration for this tool. Defaults to None. + html_selected_tags (Sequence[str]): + the text in elements of `html_selected_tags` will + be extracted and feed to the model. + dist_search (`bool`, optional): whether to use distributed web digest. + + Returns: + `ServiceResponse`: A dictionary with two variables: `status` and + `content`. The `status` variable is from the ServiceExecStatus enum, + and `content` is a list of search results or error information, + which depends on the `status` variable. + For each searching result, it is a dictionary with keys 'title', + 'link', 'snippet' and 'model_summary'. + """ + if search_engine_type == "google": + assert (api_key is not None) and ( + cse_id is not None + ), "google search requires 'api_key' and 'cse_id'" + search = partial( + google_search, + api_key=api_key, + cse_id=cse_id, + ) + elif search_engine_type == "bing": + assert api_key is not None, "bing search requires 'api_key'" + search = partial(bing_search, api_key=api_key) + results = search( + question=query, + num_results=num_results, + ).content + + digest = RpcService( + digest_webpage, + model_config_name=model_config_name, + to_dist=dist_search, + ) + cmds = [ + { + "func": digest, + "arguments": { + "web_text_or_url": page["link"], + "html_selected_tags": html_selected_tags, + }, + } + for page in results + ] + + def execute_cmd(cmd: dict) -> str: + service_func = cmd["func"] + kwargs = cmd.get("arguments", {}) + + # Execute the function + func_res = service_func(**kwargs) + return func_res + + # Execute the commands + execute_results = [execute_cmd(cmd=cmd) for cmd in cmds] + if dist_search: + execute_results = [exe.result() for exe in execute_results] + for result, exe_result in zip(results, execute_results): + result["model_summary"] = exe_result.content + return ServiceResponse( + ServiceExecStatus.SUCCESS, + results, + ) + + +def parse_args() -> argparse.Namespace: + """Parse arguments""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--logger-level", + choices=["DEBUG", "INFO"], + default="INFO", + ) + parser.add_argument( + "--studio-url", + default=None, + type=str, + ) + parser.add_argument( + "--use-dist", + action="store_true", + ) + parser.add_argument( + "--api-key", + type=str, + ) + parser.add_argument( + "--search-engine", + type=str, + choices=["google", "bing"], + default="google", + ) + parser.add_argument("--cse-id", type=str, default=None) + return parser.parse_args() + + +def main() -> None: + """Example for parallel service execution.""" + args = parse_args() + + # Prepare the model configuration + YOUR_MODEL_CONFIGURATION_NAME = "dash" + YOUR_MODEL_CONFIGURATION = [ + { + "model_type": "dashscope_chat", + "config_name": "dash", + "model_name": "qwen-turbo", + "api_key": os.environ.get("DASH_API_KEY", ""), + }, + ] + + # Initialize the agentscope + agentscope.init( + model_configs=YOUR_MODEL_CONFIGURATION, + use_monitor=False, + logger_level=args.logger_level, + studio_url=args.studio_url, + ) + user_agent = UserAgent() + service_toolkit = ServiceToolkit() + + service_toolkit.add( + search_and_digest_webpage, + search_engine_type=args.search_engine, + num_results=10, + api_key=args.api_key, + cse_id=args.cse_id, + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + html_selected_tags=["p", "div", "h1", "li"], + dist_search=args.use_dist, + ) + agent = ReActAgent( + name="assistant", + model_config_name=YOUR_MODEL_CONFIGURATION_NAME, + verbose=True, + service_toolkit=service_toolkit, + ) + + # User input and ReActAgent reply + x = user_agent() + start_time = time.time() + agent(x) + end_time = time.time() + logger.info(f"Time taken: {end_time - start_time} seconds") + + +if __name__ == "__main__": + main() diff --git a/src/agentscope/agents/agent.py b/src/agentscope/agents/agent.py index f03cd8178..c4e02fa4b 100644 --- a/src/agentscope/agents/agent.py +++ b/src/agentscope/agents/agent.py @@ -184,7 +184,7 @@ def speak( Args: content - (`Union[str, Msg, Generator[Tuple[bool, str], None, None]`): + (`Union[str, Msg, Generator[Tuple[bool, str], None, None]]`): The content of the message to be spoken out. If a string is given, a Msg object will be created with the agent's name, role as "assistant", and the given string as the content. diff --git a/src/agentscope/parsers/json_object_parser.py b/src/agentscope/parsers/json_object_parser.py index 441af8286..56eb579b7 100644 --- a/src/agentscope/parsers/json_object_parser.py +++ b/src/agentscope/parsers/json_object_parser.py @@ -287,7 +287,7 @@ def parse(self, response: ModelResponse) -> ModelResponse: if len(keys_missing) != 0: raise RequiredFieldNotFoundError( f"Missing required " - f"field{'' if len(keys_missing)==1 else 's'} " + f"field{'' if len(keys_missing) == 1 else 's'} " f"{_join_str_with_comma_and(keys_missing)} in the JSON " f"dictionary object.", response.text, diff --git a/src/agentscope/parsers/parser_base.py b/src/agentscope/parsers/parser_base.py index dd56df762..13b081826 100644 --- a/src/agentscope/parsers/parser_base.py +++ b/src/agentscope/parsers/parser_base.py @@ -60,7 +60,7 @@ def _extract_first_content_by_tag( raise TagNotFoundError( f"Missing " - f"tag{'' if len(missing_tags)==1 else 's'} " + f"tag{'' if len(missing_tags) == 1 else 's'} " f"{' and '.join(missing_tags)} in response: {text}", raw_response=text, missing_begin_tag=index_start == -1, diff --git a/src/agentscope/service/web/search.py b/src/agentscope/service/web/search.py index c748a3cbc..21b98bddd 100644 --- a/src/agentscope/service/web/search.py +++ b/src/agentscope/service/web/search.py @@ -188,7 +188,7 @@ def google_search( { "title": result["title"], "link": result["link"], - "snippet": result["snippet"], + "snippet": result.get("snippet", ""), } for result in results ], diff --git a/src/agentscope/strategy/mixture_of_agent.py b/src/agentscope/strategy/mixture_of_agent.py index 551e50aec..1b96b2deb 100644 --- a/src/agentscope/strategy/mixture_of_agent.py +++ b/src/agentscope/strategy/mixture_of_agent.py @@ -169,7 +169,7 @@ def _process_new_refs( i, result = future.result() new_refs[i] = result if self.show_internal: - print(f"Round {r+1}, Model_{i}: {result}") + print(f"Round {r + 1}, Model_{i}: {result}") self.references = new_refs final_res = self._get_res_with_aggregate_model(self.main_model) diff --git a/src/agentscope/studio/_app.py b/src/agentscope/studio/_app.py index 5cc50c730..e7f9bad73 100644 --- a/src/agentscope/studio/_app.py +++ b/src/agentscope/studio/_app.py @@ -716,7 +716,7 @@ def _save_workflow() -> Response: return jsonify( { "message": f"The workflow file size exceeds " - f"{FILE_SIZE_LIMIT/(1024*1024)} MB limit", + f"{FILE_SIZE_LIMIT / (1024 * 1024)} MB limit", }, ) diff --git a/src/agentscope/studio/_app_online.py b/src/agentscope/studio/_app_online.py index 6a23331b4..195e672b2 100644 --- a/src/agentscope/studio/_app_online.py +++ b/src/agentscope/studio/_app_online.py @@ -295,7 +295,7 @@ def write_and_upload(ct: str, user: str) -> str: return jsonify( { "message": f"The workflow data size exceeds " - f"{FILE_SIZE_LIMIT/(1024*1024)} MB limit", + f"{FILE_SIZE_LIMIT / (1024 * 1024)} MB limit", }, ) diff --git a/src/agentscope/studio/_client.py b/src/agentscope/studio/_client.py index e999b76b6..7acc7cad1 100644 --- a/src/agentscope/studio/_client.py +++ b/src/agentscope/studio/_client.py @@ -57,6 +57,7 @@ def get_user_input( self, require_url: bool, required_keys: list[str], + timeout: Optional[float] = None, ) -> Optional[dict]: """Get user input from studio in real-time. @@ -76,7 +77,7 @@ def get_user_input( "required_keys": required_keys, }, ) - self.input_event.wait() + self.input_event.wait(timeout=timeout) return self.user_input def close(self) -> None: @@ -173,6 +174,7 @@ def get_user_input( name: str, require_url: bool, required_keys: Optional[Union[list[str], str]] = None, + timeout: Optional[float] = None, ) -> dict: """Get user input from the studio. @@ -203,6 +205,7 @@ def get_user_input( return self.websocket_mapping[agent_id].get_user_input( require_url=require_url, required_keys=required_keys, + timeout=timeout, ) def get_run_detail_page_url(self) -> str: