diff --git a/README.md b/README.md index 33d3d1446..3f412fb53 100644 --- a/README.md +++ b/README.md @@ -97,12 +97,12 @@ AgentScope supports the following model API services: - [HuggingFace](https://huggingface.co/docs/api-inference/index) and [ModelScope](https://www.modelscope.cn/docs/%E9%AD%94%E6%90%ADv1.5%E7%89%88%E6%9C%AC%20Release%20Note%20(20230428)) inference APIs - Customized model APIs -| | Type Argument | Support APIs | -|----------------------|--------------------|---------------------------------------------------------------| -| OpenAI Chat API | `openai` | Standard OpenAI Chat API, FastChat and vllm | -| OpenAI DALL-E API | `openai_dall_e` | Standard DALL-E API | -| OpenAI Embedding API | `openai_embedding` | OpenAI embedding API | -| Post API | `post_api` | Huggingface/ModelScope inference API, and customized post API | +| | Model Type Argument | Support APIs | +|----------------------|---------------------|----------------------------------------------------------------| +| OpenAI Chat API | `openai` | Standard OpenAI Chat API, FastChat and vllm | +| OpenAI DALL-E API | `openai_dall_e` | Standard DALL-E API | +| OpenAI Embedding API | `openai_embedding` | OpenAI embedding API | +| Post API | `post_api` | Huggingface/ModelScope inference API, and customized post API | ##### OpenAI API Config @@ -110,9 +110,9 @@ For OpenAI APIs, you need to prepare a dict of model config with the following f ``` { - "type": "openai" | "openai_dall_e" | "openai_embedding", - "name": "{your_config_name}", # The name used to identify your config - "model_name": "{model_name, e.g. gpt-4}", # The used model in openai API + "config_name": "{config name}", # The name to identify the config + "model_type": "openai" | "openai_dall_e" | "openai_embedding", + "model_name": "{model name, e.g. gpt-4}", # The model in openai API # Optional "api_key": "xxx", # The API key for OpenAI API. If not set, env @@ -128,8 +128,8 @@ For post requests APIs, the config contains the following fields. ``` { - "type": "post_api", - "name": "{your_config_name}", # The name used to identify config + "config_name": "{config name}", # The name to identify the config + "model_type": "post_api", "api_url": "https://xxx", # The target url "headers": { # Required headers ... @@ -152,7 +152,7 @@ import agentscope agentscope.init(model_configs="./model_configs.json") # Create a dialog agent and a user agent -dialog_agent = DialogAgent(name="assistant", model="gpt-4") +dialog_agent = DialogAgent(name="assistant", model_config_name="your_config_name") user_agent = UserAgent() ``` diff --git a/docs/sphinx_doc/source/agentscope.agents.rst b/docs/sphinx_doc/source/agentscope.agents.rst index aa529a12d..ea644bcc9 100644 --- a/docs/sphinx_doc/source/agentscope.agents.rst +++ b/docs/sphinx_doc/source/agentscope.agents.rst @@ -48,3 +48,12 @@ dict_dialog_agent module :members: :undoc-members: :show-inheritance: + + +text_to_image_agent module +------------------------------- + +.. automodule:: agentscope.agents.text_to_image_agent + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/sphinx_doc/source/agentscope.configs.rst b/docs/sphinx_doc/source/agentscope.configs.rst deleted file mode 100644 index 8e5d96d96..000000000 --- a/docs/sphinx_doc/source/agentscope.configs.rst +++ /dev/null @@ -1,11 +0,0 @@ -Configs package -========================== - -model\_config module ---------------------------------------- - -.. automodule:: agentscope.configs.model_config - :members: - :undoc-members: - :show-inheritance: - diff --git a/docs/sphinx_doc/source/agentscope.models.rst b/docs/sphinx_doc/source/agentscope.models.rst index e2f52c50f..a61cbdfaa 100644 --- a/docs/sphinx_doc/source/agentscope.models.rst +++ b/docs/sphinx_doc/source/agentscope.models.rst @@ -1,6 +1,14 @@ Models package ========================== +config module +------------------------------- + +.. automodule:: agentscope.models.config + :members: + :undoc-members: + :show-inheritance: + model module ------------------------------- @@ -29,6 +37,6 @@ Module contents --------------- .. automodule:: agentscope.models - :members: load_model_by_name, clear_model_configs, read_model_configs + :members: load_model_by_config_name, clear_model_configs, read_model_configs :undoc-members: :show-inheritance: diff --git a/docs/sphinx_doc/source/index.rst b/docs/sphinx_doc/source/index.rst index 8f32751d5..611e16542 100644 --- a/docs/sphinx_doc/source/index.rst +++ b/docs/sphinx_doc/source/index.rst @@ -29,7 +29,6 @@ AgentScope Documentation :caption: AgentScope API Reference agentscope.agents - agentscope.configs agentscope.memory agentscope.models agentscope.pipelines diff --git a/docs/sphinx_doc/source/tutorial/103-example.md b/docs/sphinx_doc/source/tutorial/103-example.md index e81fa29c2..068b42ab7 100644 --- a/docs/sphinx_doc/source/tutorial/103-example.md +++ b/docs/sphinx_doc/source/tutorial/103-example.md @@ -8,20 +8,20 @@ AgentScope is a versatile platform for building and running multi-agent applicat Agent is the basic composition and communication unit in AgentScope. To initialize a model-based agent, you need to prepare your configs for avaliable models. AgentScope supports a variety of APIs for pre-trained models. Here is a table outlining the supported APIs and the type of arguments required for each: -| Model Usage | Type Argument in AgentScope | Supported APIs | -| -------------------- | ------------------ |-----------------------------------------------------------------------------| -| Text generation | `openai` | Standard *OpenAI* chat API, FastChat and vllm | -| Image generation | `openai_dall_e` | *DALL-E* API for generating images | -| Embedding | `openai_embedding` | API for text embeddings | -| General usages in POST | `post_api` | *Huggingface* and *ModelScope* Inference API, and other customized post API | +| Model Usage | Model Type Argument in AgentScope | Supported APIs | +| --------------------------- | --------------------------------- |-----------------------------------------------------------------------------| +| Text generation | `openai` | Standard *OpenAI* chat API, FastChat and vllm | +| Image generation | `openai_dall_e` | *DALL-E* API for generating images | +| Embedding | `openai_embedding` | API for text embeddings | +| General usages in POST | `post_api` | *Huggingface* and *ModelScope* Inference API, and other customized post API | Each API has its specific configuration requirements. For example, to configure an OpenAI API, you would need to fill out the following fields in the model config in a dict, a yaml file or a json file: ```python model_config = { - "type": "openai", # Choose from "openai", "openai_dall_e", or "openai_embedding" - "name": "{your_config_name}", # A unique identifier for your config - "model_name": "{model_name}", # The model identifier used in the OpenAI API, such as "gpt-3.5-turbo", "gpt-4", or "text-embedding-ada-002" + "config_name": "{config_name}", # A unique name for the model config. + "model_type": "openai", # Choose from "openai", "openai_dall_e", or "openai_embedding". + "model_name": "{model_name}", # The model identifier used in the OpenAI API, such as "gpt-3.5-turbo", "gpt-4", or "text-embedding-ada-002". "api_key": "xxx", # Your OpenAI API key. If unset, the environment variable OPENAI_API_KEY is used. "organization": "xxx", # Your OpenAI organization ID. If unset, the environment variable OPENAI_ORGANIZATION is used. } @@ -52,7 +52,7 @@ from agentscope.agents import DialogAgent, UserAgent agentscope.init(model_configs="./openai_model_configs.json") # Create a dialog agent and a user agent -dialogAgent = DialogAgent(name="assistant", model="gpt-4") +dialogAgent = DialogAgent(name="assistant", model_config_name="gpt-4") userAgent = UserAgent() ``` diff --git a/docs/sphinx_doc/source/tutorial/104-usecase.md b/docs/sphinx_doc/source/tutorial/104-usecase.md index f1502cfcf..8fc6f1f7e 100644 --- a/docs/sphinx_doc/source/tutorial/104-usecase.md +++ b/docs/sphinx_doc/source/tutorial/104-usecase.md @@ -35,11 +35,12 @@ As we discussed in the last tutorial, you need to prepare your model configurati ```json [ { - "type": "openai", - "name": "gpt-4", - "parameters": { - "api_key": "xxx", - "organization_id": "xxx", + "config_name": "gpt-4-temperature-0.0", + "model_type": "openai", + "model_name": "gpt-4", + "api_key": "xxx", + "organization": "xxx", + "generate_args": { "temperature": 0.0 } }, @@ -75,13 +76,13 @@ AgentScope provides several out-of-the-box Agents implements and organizes them "args": { "name": "Player1", "sys_prompt": "Act as a player in a werewolf game. You are Player1 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer, and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game consists of two phases: night phase and day phase. The two phases are repeated until werewolf or villager wins the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should respond only based on the conversation history and your strategy.\n\nYou are playing werewolf in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-3.5-turbo", "use_memory": true } } ``` -In this configuration, `Player1` is designated as a `DictDialogAgent`. The parameters include a system prompt (`sys_prompt`) that can guide the agent's behavior, the model (`model`) that determines the type of language model of the agent, and a flag (`use_memory`) indicating whether the agent should remember past interactions. +In this configuration, `Player1` is designated as a `DictDialogAgent`. The parameters include a system prompt (`sys_prompt`) that can guide the agent's behavior, a model config name (`model_config_name`) that determines the name of the model configuration, and a flag (`use_memory`) indicating whether the agent should remember past interactions. For other players, configurations can be customized based on their roles. Each role may have different prompts, models, or memory settings. You can refer to the JSON file located at `examples/werewolf/configs/agent_configs.json` within the AgentScope examples directory. diff --git a/docs/sphinx_doc/source/tutorial/201-agent.md b/docs/sphinx_doc/source/tutorial/201-agent.md index b7e648eba..7885ba28d 100644 --- a/docs/sphinx_doc/source/tutorial/201-agent.md +++ b/docs/sphinx_doc/source/tutorial/201-agent.md @@ -30,15 +30,14 @@ class AgentBase(Operator): def __init__( self, name: str, - config: Optional[dict] = None, sys_prompt: Optional[str] = None, - model: Optional[Union[Callable[..., Any], str]] = None, + model_config_name: str = None, use_memory: bool = True, memory_config: Optional[dict] = None, ) -> None: # ... [code omitted for brevity] - def observe(self, x: Union[dict, Sequence[dict]]) -> None: + def observe(self, x: Union[dict, Sequence[dict]]) -> None: # An optional method for updating the agent's internal state based on # messages it has observed. This method can be used to enrich the # agent's understanding and memory without producing an immediate @@ -109,7 +108,7 @@ from agentscope.agents import DialogAgent # Configuration for the DialogAgent dialog_agent_config = { "name": "ServiceBot", - "model": "gpt-3.5", # Specify the model used for dialogue generation + "model_config_name": "gpt-3.5", # Specify the model used for dialogue generation "sys_prompt": "Act as AI assistant to interact with the others. Try to " "reponse on one line.\n", # Custom prompt for the agent # Other configurations specific to the DialogAgent diff --git a/docs/sphinx_doc/source/tutorial/203-model.md b/docs/sphinx_doc/source/tutorial/203-model.md index 92c8fde7c..3bf012fbf 100644 --- a/docs/sphinx_doc/source/tutorial/203-model.md +++ b/docs/sphinx_doc/source/tutorial/203-model.md @@ -15,23 +15,25 @@ where the model configs could be a list of dict: ```json [ { - "type": "openai", - "name": "gpt-4", - "parameters": { - "api_key": "xxx", - "organization_id": "xxx", + "config_name": "gpt-4-temperature-0.0", + "model_type": "openai", + "model": "gpt-4", + "api_key": "xxx", + "organization": "xxx", + "generate_args": { "temperature": 0.0 } }, { - "type": "openai_dall_e", - "name": "dall-e-3", - "parameters": { - "api_key": "xxx", - "organization_id": "xxx", + "config_name": "dall-e-3-size-1024x1024", + "model_type": "openai_dall_e", + "model": "dall-e-3", + "api_key": "xxx", + "organization": "xxx", + "generate_args": { "size": "1024x1024" } - } + }, // Additional models can be configured here ] ``` @@ -86,8 +88,8 @@ In AgentScope, you can load the model with the following model configs: `./flask ```json { - "type": "post_api", - "name": "flask_llama2-7b-chat", + "model_type": "post_api", + "config_name": "flask_llama2-7b-chat", "api_url": "http://127.0.0.1:8000/llm/", "json_args": { "max_length": 4096, @@ -127,8 +129,8 @@ In AgentScope, you can load the model with the following model configs: `flask_m ```json { - "type": "post_api", - "name": "flask_llama2-7b-ms", + "model_type": "post_api", + "config_name": "flask_llama2-7b-ms", "api_url": "http://127.0.0.1:8000/llm/", "json_args": { "max_length": 4096, @@ -169,8 +171,8 @@ Now you can load the model in AgentScope by the following model config: `fastcha ```json { - "type": "openai", - "name": "meta-llama/Llama-2-7b-chat-hf", + "config_name": "meta-llama/Llama-2-7b-chat-hf", + "model_type": "openai", "api_key": "EMPTY", "client_args": { "base_url": "http://127.0.0.1:8000/v1/" @@ -209,8 +211,8 @@ Now you can load the model in AgentScope by the following model config: `vllm_sc ```json { - "type": "openai", - "name": "meta-llama/Llama-2-7b-chat-hf", + "config_name": "meta-llama/Llama-2-7b-chat-hf", + "model_type": "openai", "api_key": "EMPTY", "client_args": { "base_url": "http://127.0.0.1:8000/v1/" @@ -228,8 +230,8 @@ Taking `gpt2` in HuggingFace inference API as an example, you can use the follow ```json { - "type": "post_api", - "name": 'gpt2', + "config_name": "gpt2", + "model_type": "post_api", "headers": { "Authorization": "Bearer {YOUR_API_TOKEN}" } @@ -248,7 +250,7 @@ model = AutoModelForCausalLM.from_pretrained(MODEL_NAME) tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) model.eval() # Do remember to re-implement the `reply` method to tokenize *message*! -agent = YourAgent(name='agent', model=model, tokenizer=tokenizer) +agent = YourAgent(name='agent', model_config_name=config_name, tokenizer=tokenizer) ``` [[Return to the top]](#using-different-model-sources-with-model-api) diff --git a/examples/conversation/conversation.py b/examples/conversation/conversation.py index f97439cb5..5cd721a25 100644 --- a/examples/conversation/conversation.py +++ b/examples/conversation/conversation.py @@ -8,8 +8,9 @@ agentscope.init( model_configs=[ { - "type": "openai", - "name": "gpt-3.5-turbo", + "model_type": "openai", + "config_name": "gpt-3.5-turbo", + "model": "gpt-3.5-turbo", "api_key": "xxx", # Load from env if not provided "organization": "xxx", # Load from env if not provided "generate_args": { @@ -17,8 +18,8 @@ }, }, { - "type": "post_api", - "name": "my_post_api", + "model_type": "post_api_chat", + "config_name": "my_post_api", "api_url": "https://xxx", "headers": {}, }, @@ -29,7 +30,7 @@ dialog_agent = DialogAgent( name="Assistant", sys_prompt="You're a helpful assistant.", - model="gpt-3.5-turbo", # replace by your model config name + model_config_name="gpt-3.5-turbo", # replace by your model config name ) user_agent = UserAgent() diff --git a/examples/distributed/configs/debate_agent_configs.json b/examples/distributed/configs/debate_agent_configs.json index dec5af779..819caaed1 100644 --- a/examples/distributed/configs/debate_agent_configs.json +++ b/examples/distributed/configs/debate_agent_configs.json @@ -4,7 +4,7 @@ "args": { "name": "Pro", "sys_prompt": "Assume the role of a debater who is arguing in favor of the proposition that AGI (Artificial General Intelligence) can be achieved using the GPT model framework. Construct a coherent and persuasive argument, including scientific, technological, and theoretical evidence, to support the statement that GPT models are a viable path to AGI. Highlight the advancements in language understanding, adaptability, and scalability of GPT models as key factors in progressing towards AGI.", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-3.5-turbo", "use_memory": true } }, @@ -13,7 +13,7 @@ "args": { "name": "Con", "sys_prompt": "Assume the role of a debater who is arguing against the proposition that AGI can be achieved using the GPT model framework. Construct a coherent and persuasive argument, including scientific, technological, and theoretical evidence, to support the statement that GPT models, while impressive, are insufficient for reaching AGI. Discuss the limitations of GPT models such as lack of understanding, consciousness, ethical reasoning, and general problem-solving abilities that are essential for true AGI.", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-3.5-turbo", "use_memory": true } }, @@ -22,7 +22,7 @@ "args": { "name": "Judge", "sys_prompt": "Assume the role of an impartial judge in a debate where the affirmative side argues that AGI can be achieved using the GPT model framework, and the negative side contests this. Listen to both sides' arguments and provide an analytical judgment on which side presented a more compelling and reasonable case. Consider the strength of the evidence, the persuasiveness of the reasoning, and the overall coherence of the arguments presented by each side.", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-3.5-turbo", "use_memory": true } } diff --git a/examples/distributed/configs/model_configs.json b/examples/distributed/configs/model_configs.json index 20bfc1501..fa8ebbf7b 100644 --- a/examples/distributed/configs/model_configs.json +++ b/examples/distributed/configs/model_configs.json @@ -1,20 +1,22 @@ [ { - "type": "openai", - "name": "gpt-3.5-turbo", - "parameters": { - "api_key": "xxx", - "organization_id": "xxx", + "config_name": "gpt-3.5-turbo", + "model_type": "openai", + "model_name": "gpt-3.5-turbo", + "api_key": "xxx", + "organization": "xxx", + "generate_args": { "temperature": 0.0 } }, { - "type": "openai", - "name": "gpt-4", - "parameters": { - "api_key": "xxx", - "organization_id": "xxx", - "temperature": 0.0 + "config_name": "gpt-4", + "model_type": "openai", + "model_name": "gpt-4", + "api_key": "xxx", + "organization": "xxx", + "generate_args": { + "temperature": 0.5 } } ] \ No newline at end of file diff --git a/examples/distributed/distributed_dialog.py b/examples/distributed/distributed_dialog.py index 0a6f30e51..cc210d325 100644 --- a/examples/distributed/distributed_dialog.py +++ b/examples/distributed/distributed_dialog.py @@ -41,7 +41,7 @@ def setup_assistant_server(assistant_host: str, assistant_port: int) -> None: agent_kwargs={ "name": "Assitant", "sys_prompt": "You are a helpful assistant.", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-3.5-turbo", "use_memory": True, }, host=assistant_host, @@ -59,7 +59,7 @@ def run_main_process(assistant_host: str, assistant_port: int) -> None: assistant_agent = DialogAgent( name="Assistant", sys_prompt="You are a helpful assistant.", - model="gpt-3.5-turbo", + model_config_name="gpt-3.5-turbo", use_memory=True, ).to_dist( host=assistant_host, diff --git a/examples/werewolf/README.md b/examples/werewolf/README.md index 90e275329..2050edb65 100644 --- a/examples/werewolf/README.md +++ b/examples/werewolf/README.md @@ -41,7 +41,7 @@ is as follows "args": { "name": "Player1", "sys_prompt": "Act as a player in a werewolf game. You are Player1 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing werewolf in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-3.5-turbo", "use_memory": true } } @@ -95,4 +95,4 @@ More details please refer to the code of [`DictDialogAgent`](../.. if x.get("agreement", False): break # ... -``` \ No newline at end of file +``` diff --git a/examples/werewolf/configs/agent_configs.json b/examples/werewolf/configs/agent_configs.json index 9c873458d..6f9095b9d 100644 --- a/examples/werewolf/configs/agent_configs.json +++ b/examples/werewolf/configs/agent_configs.json @@ -4,7 +4,7 @@ "args": { "name": "Player1", "sys_prompt": "Act as a player in a werewolf game. You are Player1 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing werewolf in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-4", "use_memory": true } }, @@ -13,7 +13,7 @@ "args": { "name": "Player2", "sys_prompt": "Act as a player in a werewolf game. You are Player2 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing werewolf in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-4", "use_memory": true } }, @@ -22,7 +22,7 @@ "args": { "name": "Player3", "sys_prompt": "Act as a player in a werewolf game. You are Player3 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing villager in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-4", "use_memory": true } }, @@ -31,7 +31,7 @@ "args": { "name": "Player4", "sys_prompt": "Act as a player in a werewolf game. You are Player4 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing villager in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-4", "use_memory": true } }, @@ -40,7 +40,7 @@ "args": { "name": "Player5", "sys_prompt": "Act as a player in a werewolf game. You are Player5 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing seer in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-4", "use_memory": true } }, @@ -49,7 +49,7 @@ "args": { "name": "Player6", "sys_prompt": "Act as a player in a werewolf game. You are Player6 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game is consisted of two phases: night phase and day phase. The two phases are repeated until werewolf or villager win the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should response only based on the conversation history and your strategy.\n\nYou are playing witch in this game.\n", - "model": "gpt-3.5-turbo", + "model_config_name": "gpt-4", "use_memory": true } } diff --git a/examples/werewolf/configs/model_configs.json b/examples/werewolf/configs/model_configs.json index 3f7dbcbc2..1d1b7193c 100644 --- a/examples/werewolf/configs/model_configs.json +++ b/examples/werewolf/configs/model_configs.json @@ -1,7 +1,8 @@ [ { - "type": "openai", - "name": "gpt-4", + "model_type": "openai", + "config_name": "gpt-4", + "model_name": "gpt-4", "api_key": "xxx", "organization": "xxx", "generate_args": { @@ -9,12 +10,10 @@ } }, { - "type": "post_api", - "name": "my_post_api", + "model_type": "post_api_chat", + "config_name": "my_post_api", "api_url": "https://xxx", - "headers": { - }, - "json_args": { - } + "headers": {}, + "json_args": {} } -] +] \ No newline at end of file diff --git a/notebook/conversation.ipynb b/notebook/conversation.ipynb index d16f9d0d4..dd9da7772 100644 --- a/notebook/conversation.ipynb +++ b/notebook/conversation.ipynb @@ -49,8 +49,9 @@ "agentscope.init(\n", " model_configs=[\n", " {\n", - " \"type\": \"openai\",\n", - " \"name\": \"gpt-3.5-turbo\",\n", + " \"model_type\": \"openai\",\n", + " \"config_name\": \"gpt-3.5-turbo\",\n", + " \"model\": \"gpt-3.5-turbo\",\n", " \"api_key\": \"xxx\", # Load from env if not provided\n", " \"organization\": \"xxx\", # Load from env if not provided\n", " \"generate_args\": {\n", @@ -58,8 +59,8 @@ " },\n", " },\n", " {\n", - " \"type\": \"post_api\",\n", - " \"name\": \"my_post_api\",\n", + " \"model_type\": \"post_api_chat\",\n", + " \"config_name\": \"my_post_api\",\n", " \"api_url\": \"https://xxx\",\n", " \"headers\": {},\n", " },\n", @@ -85,7 +86,7 @@ "dialog_agent = DialogAgent(\n", " name=\"Assistant\",\n", " sys_prompt=\"You're a helpful assistant.\",\n", - " model=\"gpt-3.5-turbo\", # replace by your model config name\n", + " model_config_name=\"gpt-3.5-turbo\", # replace by your model config name\n", ")\n", "user_agent = UserAgent()" ] diff --git a/notebook/distributed_debate.ipynb b/notebook/distributed_debate.ipynb index 9e03f2577..3acd38397 100644 --- a/notebook/distributed_debate.ipynb +++ b/notebook/distributed_debate.ipynb @@ -48,22 +48,24 @@ "source": [ "model_configs = [\n", " {\n", - " \"type\": \"openai\",\n", - " \"name\": \"gpt-3.5-turbo\",\n", - " \"parameters\": {\n", - " \"api_key\": \"xxx\",\n", - " \"organization_id\": \"xxx\",\n", - " \"temperature\": 0.0\n", - " }\n", + " \"model_type\": \"openai\",\n", + " \"config_name\": \"gpt-3.5-turbo\",\n", + " \"model\": \"gpt-3.5-turbo\",\n", + " \"api_key\": \"xxx\",\n", + " \"organization\": \"xxx\",\n", + " \"generate_args\": {\n", + " \"temperature\": 0.0,\n", + " },\n", " },\n", " {\n", - " \"type\": \"openai\",\n", - " \"name\": \"gpt-4\",\n", - " \"parameters\": {\n", - " \"api_key\": \"xxx\",\n", - " \"organization_id\": \"xxx\",\n", - " \"temperature\": 0.0\n", - " }\n", + " \"model_type\": \"openai\",\n", + " \"config_name\": \"gpt-4\",\n", + " \"model\": \"gpt-4\",\n", + " \"api_key\": \"xxx\",\n", + " \"organization\": \"xxx\",\n", + " \"generate_args\": {\n", + " \"temperature\": 0.0,\n", + " },\n", " }\n", "]" ] @@ -90,19 +92,19 @@ "\n", "pro_agent = DialogAgent(\n", " name=\"Pro\",\n", - " model=\"gpt-3.5-turbo\",\n", + " model_config_name=\"gpt-3.5-turbo\",\n", " use_memory=True,\n", " sys_prompt=\"Assume the role of a debater who is arguing in favor of the proposition that AGI (Artificial General Intelligence) can be achieved using the GPT model framework. Construct a coherent and persuasive argument, including scientific, technological, and theoretical evidence, to support the statement that GPT models are a viable path to AGI. Highlight the advancements in language understanding, adaptability, and scalability of GPT models as key factors in progressing towards AGI.\",\n", ").to_dist()\n", "con_agent = DialogAgent(\n", " name=\"Con\",\n", - " model=\"gpt-3.5-turbo\",\n", + " model_config_name=\"gpt-3.5-turbo\",\n", " use_memory=True,\n", " sys_prompt=\"Assume the role of a debater who is arguing against the proposition that AGI can be achieved using the GPT model framework. Construct a coherent and persuasive argument, including scientific, technological, and theoretical evidence, to support the statement that GPT models, while impressive, are insufficient for reaching AGI. Discuss the limitations of GPT models such as lack of understanding, consciousness, ethical reasoning, and general problem-solving abilities that are essential for true AGI.\",\n", ").to_dist()\n", "judge_agent = DialogAgent(\n", " name=\"Judge\",\n", - " model=\"gpt-3.5-turbo\",\n", + " model_config_name=\"gpt-3.5-turbo\",\n", " use_memory=True,\n", " sys_prompt=\"Assume the role of an impartial judge in a debate where the affirmative side argues that AGI can be achieved using the GPT model framework, and the negative side contests this. Listen to both sides' arguments and provide an analytical judgment on which side presented a more compelling and reasonable case. Consider the strength of the evidence, the persuasiveness of the reasoning, and the overall coherence of the arguments presented by each side.\"\n", ").to_dist()" diff --git a/notebook/distributed_dialog.ipynb b/notebook/distributed_dialog.ipynb index 51e018873..10829dee4 100644 --- a/notebook/distributed_dialog.ipynb +++ b/notebook/distributed_dialog.ipynb @@ -41,22 +41,24 @@ "source": [ "model_configs = [\n", " {\n", - " \"type\": \"openai\",\n", - " \"name\": \"gpt-3.5-turbo\",\n", - " \"parameters\": {\n", - " \"api_key\": \"xxx\",\n", - " \"organization_id\": \"xxx\",\n", - " \"temperature\": 0.0\n", - " }\n", + " \"model_type\": \"openai\",\n", + " \"config_name\": \"gpt-3.5-turbo\",\n", + " \"model\": \"gpt-3.5-turbo\",\n", + " \"api_key\": \"xxx\",\n", + " \"organization\": \"xxx\",\n", + " \"generate_args\": {\n", + " \"temperature\": 0.0,\n", + " },\n", " },\n", " {\n", - " \"type\": \"openai\",\n", - " \"name\": \"gpt-4\",\n", - " \"parameters\": {\n", - " \"api_key\": \"xxx\",\n", - " \"organization_id\": \"xxx\",\n", - " \"temperature\": 0.0\n", - " }\n", + " \"model_type\": \"openai\",\n", + " \"config_name\": \"gpt-4\",\n", + " \"model\": \"gpt-4\",\n", + " \"api_key\": \"xxx\",\n", + " \"organization\": \"xxx\",\n", + " \"generate_args\": {\n", + " \"temperature\": 0.0,\n", + " },\n", " }\n", "]" ] @@ -89,7 +91,7 @@ "assistant_agent = DialogAgent(\n", " name=\"Assistant\",\n", " sys_prompt=\"You are a helpful assistant.\",\n", - " model=\"gpt-3.5-turbo\",\n", + " model_config_name=\"gpt-3.5-turbo\",\n", " use_memory=True,\n", ").to_dist()\n", "user_agent = UserAgent(\n", diff --git a/scripts/README.md b/scripts/README.md index cc16ea93f..5031a1f90 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,28 +6,28 @@ fast set up local model API serving with different inference engines. Table of Contents ================= -* [Local Model API Serving](#local-model-api-serving) - * [Flask-based Model API Serving](#flask-based-model-api-serving) - * [With Transformers Library](#with-transformers-library) - * [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving) - * [How to use in AgentScope](#how-to-use-in-agentscope) - * [Note](#note) - * [With ModelScope Library](#with-modelscope-library) - * [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving-1) - * [How to use in AgentScope](#how-to-use-in-agentscope-1) - * [Note](#note-1) - * [FastChat](#fastchat) - * [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving-2) - * [Supported Models](#supported-models) - * [How to use in AgentScope](#how-to-use-in-agentscope-2) - * [vllm](#vllm) - * [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving-3) - * [Supported Models](#supported-models-1) - * [How to use in AgentScope](#how-to-use-in-agentscope-3) -* [Model Inference API](#model-inference-api) - - +- [Set up Model API Serving](#set-up-model-api-serving) +- [Table of Contents](#table-of-contents) + - [Local Model API Serving](#local-model-api-serving) + - [Flask-based Model API Serving](#flask-based-model-api-serving) + - [With Transformers Library](#with-transformers-library) + - [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving) + - [How to use in AgentScope](#how-to-use-in-agentscope) + - [Note](#note) + - [With ModelScope Library](#with-modelscope-library) + - [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving-1) + - [How to use in AgentScope](#how-to-use-in-agentscope-1) + - [Note](#note-1) + - [FastChat](#fastchat) + - [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving-2) + - [Supported Models](#supported-models) + - [How to use in AgentScope](#how-to-use-in-agentscope-2) + - [vllm](#vllm) + - [Install Libraries and Set up Serving](#install-libraries-and-set-up-serving-3) + - [Supported models](#supported-models-1) + - [How to use in AgentScope](#how-to-use-in-agentscope-3) + - [Model Inference API](#model-inference-api) ## Local Model API Serving @@ -46,15 +46,16 @@ respectively. You can build your own model API serving with few modifications. Install Flask and Transformers by following command. ```bash -pip install Flask, transformers +pip install flask torch transformers accelerate ``` Taking model `meta-llama/Llama-2-7b-chat-hf` and port `8000` as an example, set up the model API serving by running the following command. -```bash -python flask_transformers/setup_hf_service.py - --model_name_or_path meta-llama/Llama-2-7b-chat-hf - --device "cuda:0" # or "cpu" + +```shell +python flask_transformers/setup_hf_service.py \ + --model_name_or_path meta-llama/Llama-2-7b-chat-hf \ + --device "cuda:0" \ --port 8000 ``` @@ -67,8 +68,8 @@ In AgentScope, you can load the model with the following model configs: `./flask ```json { - "type": "post_api", - "name": "flask_llama2-7b-chat", + "model_type": "post_api", + "config_name": "flask_llama2-7b-chat", "api_url": "http://127.0.0.1:8000/llm/", "json_args": { "max_length": 4096, @@ -83,7 +84,6 @@ In this model serving, the messages from post requests should be in **STRING format**. You can use [templates for chat model](https://huggingface.co/docs/transformers/main/chat_templating) in transformers with a little modification in `./flask_transformers/setup_hf_service.py`. - #### With ModelScope Library ##### Install Libraries and Set up Serving @@ -91,23 +91,22 @@ transformers with a little modification in `./flask_transformers/setup_hf_servic Install Flask and modelscope by following command. ```bash -pip install Flask, modelscope +pip install flask torch modelscope ``` Taking model `modelscope/Llama-2-7b-ms` and port `8000` as an example, to set up the model API serving, run the following command. ```bash -python flask_modelscope/setup_ms_service.py - --model_name_or_path modelscope/Llama-2-7b-ms - --device "cuda:0" # or "cpu" +python flask_modelscope/setup_ms_service.py \ + --model_name_or_path modelscope/Llama-2-7b-ms \ + --device "cuda:0" \ --port 8000 ``` You can replace `modelscope/Llama-2-7b-ms` with any model card in modelscope model hub. - ##### How to use in AgentScope In AgentScope, you can load the model with the following model configs: @@ -115,8 +114,8 @@ In AgentScope, you can load the model with the following model configs: ```json { - "type": "post_api", - "name": "flask_llama2-7b-ms", + "model_type": "post_api", + "config_name": "flask_llama2-7b-ms", "api_url": "http://127.0.0.1:8000/llm/", "json_args": { "max_length": 4096, @@ -130,7 +129,6 @@ In AgentScope, you can load the model with the following model configs: Similar with the example of transformers, the messages from post requests should be in **STRING format**. - ### FastChat [FastChat](https://github.com/lm-sys/FastChat) is an open platform that @@ -141,7 +139,7 @@ provides quick setup for model serving with OpenAI-compatible RESTful APIs. To install FastChat, run ```bash -pip install "fastchat[model_worker,webui]" +pip install "fschat[model_worker,webui]" ``` Taking model `meta-llama/Llama-2-7b-chat-hf` and port `8000` as an example, @@ -152,16 +150,19 @@ bash fastchat_script/fastchat_setup.sh -m meta-llama/Llama-2-7b-chat-hf -p 8000 ``` #### Supported Models + Refer to [supported model list](https://github.com/lm-sys/FastChat/blob/main/docs/model_support.md#supported-models) of FastChat. #### How to use in AgentScope + Now you can load the model in AgentScope by the following model config: `fastchat_script/model_config.json`. + ```json { - "type": "openai", - "name": "meta-llama/Llama-2-7b-chat-hf", + "model_type": "openai", + "config_name": "meta-llama/Llama-2-7b-chat-hf", "api_key": "EMPTY", "client_args": { "base_url": "http://127.0.0.1:8000/v1/" @@ -178,6 +179,7 @@ Now you can load the model in AgentScope by the following model config: `fastcha and serving engine for LLMs. #### Install Libraries and Set up Serving + To install vllm, run ```bash @@ -188,7 +190,7 @@ Taking model `meta-llama/Llama-2-7b-chat-hf` and port `8000` as an example, to set up model API serving, run ```bash -bash vllm_script/vllm_setup.sh -m meta-llama/Llama-2-7b-chat-hf -p 8000 +./vllm_script/vllm_setup.sh -m meta-llama/Llama-2-7b-chat-hf -p 8000 ``` #### Supported models @@ -198,12 +200,13 @@ Please refer to the of vllm. #### How to use in AgentScope + Now you can load the model in AgentScope by the following model config: `vllm_script/model_config.json`. ```json { - "type": "openai", - "name": "meta-llama/Llama-2-7b-chat-hf", + "model_type": "openai", + "config_name": "meta-llama/Llama-2-7b-chat-hf", "api_key": "EMPTY", "client_args": { "base_url": "http://127.0.0.1:8000/v1/" @@ -214,7 +217,6 @@ Now you can load the model in AgentScope by the following model config: `vllm_sc } ``` - ## Model Inference API Both [Huggingface](https://huggingface.co/docs/api-inference/index) and @@ -223,13 +225,13 @@ which can be used with AgentScope post api model wrapper. Taking `gpt2` in HuggingFace inference API as an example, you can use the following model config in AgentScope. -```bash +```json { - "type": "post_api", - "name": 'gpt2', + "model_type": "post_api", + "config_name": "gpt2", "headers": { "Authorization": "Bearer {YOUR_API_TOKEN}" - } + }, "api_url": "https://api-inference.huggingface.co/models/gpt2" } -``` \ No newline at end of file +``` diff --git a/scripts/faschat/model_config.json b/scripts/faschat/model_config.json index 9e812662c..708793f99 100644 --- a/scripts/faschat/model_config.json +++ b/scripts/faschat/model_config.json @@ -1,6 +1,6 @@ { - "type": "openai", - "name": "fs-llama-2", + "model_type": "openai", + "config_name": "fs-llama-2", "model_name": "llama-2", "api_key": "EMPTY", "client_args": { diff --git a/scripts/flask_modelscope/model_config.json b/scripts/flask_modelscope/model_config.json index 10258bfac..20d05f67c 100644 --- a/scripts/flask_modelscope/model_config.json +++ b/scripts/flask_modelscope/model_config.json @@ -1,6 +1,6 @@ { - "type": "post_api", - "name": "post_llama-2-chat-7b-ms", + "model_type": "post_api", + "config_name": "post_llama-2-chat-7b-ms", "api_url": "http://127.0.0.1:8000/llm/", "json_args": { "max_length": 4096, diff --git a/scripts/flask_transformers/model_config.json b/scripts/flask_transformers/model_config.json index bb81d1302..d532fe407 100644 --- a/scripts/flask_transformers/model_config.json +++ b/scripts/flask_transformers/model_config.json @@ -1,6 +1,6 @@ { - "type": "post_api", - "name": "post_llama-2-chat-7b-hf", + "model_type": "post_api", + "config_name": "post_llama-2-chat-7b-hf", "api_url": "http://127.0.0.1:8000/llm/", "json_args": { "max_length": 4096, diff --git a/scripts/vllm_script/model_config.json b/scripts/vllm_script/model_config.json index 4d5895fb8..3932c3bec 100644 --- a/scripts/vllm_script/model_config.json +++ b/scripts/vllm_script/model_config.json @@ -1,6 +1,6 @@ { - "type": "openai", - "name": "vllm-llama-2", + "model_type": "openai", + "config_name": "vllm-llama-2", "model_name": "llama-2", "api_key": "EMPTY", "client_args": { @@ -9,4 +9,4 @@ "generate_args": { "temperature": 0.5 } -} +} \ No newline at end of file diff --git a/src/agentscope/agents/__init__.py b/src/agentscope/agents/__init__.py index fd125d17b..5e9ff47cf 100644 --- a/src/agentscope/agents/__init__.py +++ b/src/agentscope/agents/__init__.py @@ -5,6 +5,7 @@ from .dialog_agent import DialogAgent from .dict_dialog_agent import DictDialogAgent from .user_agent import UserAgent +from .text_to_image_agent import TextToImageAgent __all__ = [ @@ -12,5 +13,6 @@ "Operator", "DialogAgent", "DictDialogAgent", + "TextToImageAgent", "UserAgent", ] diff --git a/src/agentscope/agents/agent.py b/src/agentscope/agents/agent.py index 436f056f6..6a60eb9e7 100644 --- a/src/agentscope/agents/agent.py +++ b/src/agentscope/agents/agent.py @@ -7,11 +7,10 @@ from typing import Sequence from typing import Union from typing import Any -from typing import Callable from loguru import logger from agentscope.agents.operator import Operator -from agentscope.models import load_model_by_name +from agentscope.models import load_model_by_config_name from agentscope.memory import TemporaryMemory @@ -36,9 +35,8 @@ class AgentBase(Operator, metaclass=_RecordInitSettingMeta): def __init__( self, name: str, - config: Optional[dict] = None, sys_prompt: Optional[str] = None, - model: Optional[Union[Callable[..., Any], str]] = None, + model_config_name: str = None, use_memory: bool = True, memory_config: Optional[dict] = None, ) -> None: @@ -47,17 +45,12 @@ def __init__( Args: name (`str`): The name of the agent. - config (`Optional[dict]`): - The configuration of the agent, if provided, the agent will - be initialized from the config rather than the other - parameters. sys_prompt (`Optional[str]`): The system prompt of the agent, which can be passed by args or hard-coded in the agent. - model (`Optional[Union[Callable[..., Any], str]]`, defaults to - None): - The callable model object or the model name, which is used to - load model from configuration. + model_config_name (`str`, defaults to None): + The name of the model config, which is used to load model from + configuration. use_memory (`bool`, defaults to `True`): Whether the agent has memory. memory_config (`Optional[dict]`): @@ -65,17 +58,14 @@ def __init__( """ self.name = name - self.config = config self.memory_config = memory_config if sys_prompt is not None: self.sys_prompt = sys_prompt - if model is not None: - if isinstance(model, str): - self.model = load_model_by_name(model) - else: - self.model = model + # TODO: support to receive a ModelWrapper instance + if model_config_name is not None: + self.model = load_model_by_config_name(model_config_name) if use_memory: self.memory = TemporaryMemory(memory_config) diff --git a/src/agentscope/agents/dialog_agent.py b/src/agentscope/agents/dialog_agent.py index b57ca4ce2..3f680d234 100644 --- a/src/agentscope/agents/dialog_agent.py +++ b/src/agentscope/agents/dialog_agent.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """A general dialog agent.""" -from typing import Any, Optional, Union, Callable +from typing import Optional from ..message import Msg from .agent import AgentBase @@ -15,9 +15,8 @@ class DialogAgent(AgentBase): def __init__( self, name: str, - config: Optional[dict] = None, sys_prompt: Optional[str] = None, - model: Optional[Union[Callable[..., Any], str]] = None, + model_config_name: str = None, use_memory: bool = True, memory_config: Optional[dict] = None, prompt_type: Optional[PromptType] = PromptType.LIST, @@ -27,17 +26,12 @@ def __init__( Arguments: name (`str`): The name of the agent. - config (`Optional[dict]`): - The configuration of the agent, if provided, the agent will - be initialized from the config rather than the other - parameters. sys_prompt (`Optional[str]`): The system prompt of the agent, which can be passed by args or hard-coded in the agent. - model (`Optional[Union[Callable[..., Any], str]]`, defaults to - None): - The callable model object or the model name, which is used to - load model from configuration. + model_config_name (`str`, defaults to None): + The name of the model config, which is used to load model from + configuration. use_memory (`bool`, defaults to `True`): Whether the agent has memory. memory_config (`Optional[dict]`): @@ -48,12 +42,11 @@ def __init__( `PromptType.LIST` or `PromptType.STRING`. """ super().__init__( - name, - config, - sys_prompt, - model, - use_memory, - memory_config, + name=name, + sys_prompt=sys_prompt, + model_config_name=model_config_name, + use_memory=use_memory, + memory_config=memory_config, ) # init prompt engine @@ -85,7 +78,7 @@ def reply(self, x: dict = None) -> dict: ) # call llm and generate response - response = self.model(prompt) + response = self.model(prompt).text msg = Msg(self.name, response) # Print/speak the message in this agent's voice diff --git a/src/agentscope/agents/dict_dialog_agent.py b/src/agentscope/agents/dict_dialog_agent.py index d302e7823..9e9315892 100644 --- a/src/agentscope/agents/dict_dialog_agent.py +++ b/src/agentscope/agents/dict_dialog_agent.py @@ -2,7 +2,7 @@ """A dict dialog agent that using `parse_func` and `fault_handler` to parse the model response.""" import json -from typing import Any, Optional, Union, Callable +from typing import Any, Optional, Callable from loguru import logger from ..message import Msg @@ -36,9 +36,8 @@ class DictDialogAgent(AgentBase): def __init__( self, name: str, - config: Optional[dict] = None, sys_prompt: Optional[str] = None, - model: Optional[Union[Callable[..., Any], str]] = None, + model_config_name: str = None, use_memory: bool = True, memory_config: Optional[dict] = None, parse_func: Optional[Callable[..., Any]] = json.loads, @@ -51,17 +50,12 @@ def __init__( Arguments: name (`str`): The name of the agent. - config (`Optional[dict]`, defaults to `None`): - The configuration of the agent, if provided, the agent will - be initialized from the config rather than the other - parameters. sys_prompt (`Optional[str]`, defaults to `None`): The system prompt of the agent, which can be passed by args or hard-coded in the agent. - model (`Optional[Union[Callable[..., Any], str]]`, defaults to - `None`): - The callable model object or the model name, which is used to - load model from configuration. + model_config_name (`str`, defaults to None): + The name of the model config, which is used to load model from + configuration. use_memory (`bool`, defaults to `True`): Whether the agent has memory. memory_config (`Optional[dict]`, defaults to `None`): @@ -82,12 +76,11 @@ def __init__( `PromptType.LIST` or `PromptType.STRING`. """ super().__init__( - name, - config, - sys_prompt, - model, - use_memory, - memory_config, + name=name, + sys_prompt=sys_prompt, + model_config_name=model_config_name, + use_memory=use_memory, + memory_config=memory_config, ) # record the func and handler for parsing and handling faults @@ -136,7 +129,7 @@ def reply(self, x: dict = None) -> dict: parse_func=self.parse_func, fault_handler=self.fault_handler, max_retries=self.max_retries, - ) + ).text # logging raw messages in debug mode logger.debug(json.dumps(response, indent=4)) diff --git a/src/agentscope/agents/text_to_image_agent.py b/src/agentscope/agents/text_to_image_agent.py new file mode 100644 index 000000000..169b26034 --- /dev/null +++ b/src/agentscope/agents/text_to_image_agent.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""A agent that convert text to image.""" + +from typing import Optional +from loguru import logger + +from .agent import AgentBase +from ..message import Msg + + +class TextToImageAgent(AgentBase): + """A agent used to perform text to image tasks.""" + + def __init__( + self, + name: str, + sys_prompt: Optional[str] = None, + model_config_name: str = None, + use_memory: bool = True, + memory_config: Optional[dict] = None, + ) -> None: + """Initialize the text to image agent. + + Arguments: + name (`str`): + The name of the agent. + sys_prompt (`Optional[str]`): + The system prompt of the agent, which can be passed by args + or hard-coded in the agent. + model_config_name (`str`, defaults to None): + The name of the model config, which is used to load model from + configuration. + use_memory (`bool`, defaults to `True`): + Whether the agent has memory. + memory_config (`Optional[dict]`): + The config of memory. + """ + super().__init__( + name=name, + sys_prompt=sys_prompt, + model_config_name=model_config_name, + use_memory=use_memory, + memory_config=memory_config, + ) + + def reply(self, x: dict = None) -> dict: + if x is not None: + self.memory.add(x) + image_urls = self.model(x.content).image_urls + # TODO: optimize the construction of content + msg = Msg( + self.name, + content="This is the generated image ", + url=image_urls, + ) + logger.chat(msg) + self.memory.add(msg) + return msg diff --git a/src/agentscope/configs/__init__.py b/src/agentscope/configs/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/agentscope/configs/model_config.py b/src/agentscope/configs/model_config.py deleted file mode 100644 index 452a4d2c7..000000000 --- a/src/agentscope/configs/model_config.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- -"""The model config.""" -from typing import Any -from ..constants import _DEFAULT_MAX_RETRIES -from ..constants import _DEFAULT_MESSAGES_KEY -from ..constants import _DEFAULT_API_BUDGET - - -class CfgBase(dict): - """Base class for model config.""" - - __getattr__ = dict.__getitem__ - __setattr__ = dict.__setitem__ - - def init(self, **kwargs: Any) -> None: - """Initialize the config with the given arguments, and checking the - type of the arguments.""" - cls = self.__class__ - for k, v in kwargs.items(): - if k in cls.__dict__["__annotations__"]: - required_type = cls.__dict__["__annotations__"][k] - if isinstance(v, required_type): - self[k] = v - else: - raise TypeError( - f"The argument {k} should be " - f"{required_type}, but got {type(v)} " - f"instead.", - ) - - -class OpenAICfg(CfgBase): - """The config for OpenAI models.""" - - type: str = "openai" - """The typing of this config.""" - - name: str - """The name of the model.""" - - model_name: str = None - """The name of the model, (e.g. gpt-4, gpt-3.5-turbo). Default to `name` if - not provided.""" - - api_key: str = None - """The api key used for OpenAI API. Will be read from env if not - provided.""" - - organization: str = None - """The organization used for OpenAI API. Will be read from env if not - provided.""" - - client_args: dict = None - """The arguments used to initialize openai client, e.g. `timeout`, - `max_retries`.""" - - generate_args: dict = None - """The arguments used in openai api generation, e.g. `temperature`, - `seed`.""" - - budget: float = _DEFAULT_API_BUDGET - """The total budget using this model. Set to `None` means no limit.""" - - -class PostApiCfg(CfgBase): - """The config for Post API. The final request post will be - - .. code-block:: python - - cfg = PostApiCfg(...) - request.post( - url=cfg.api_url, - headers=cfg.headers, - json={ - cfg.message_key: messages, - **cfg.json_args - }, - **cfg.post_args - ) - - """ - - type: str = "post_api" - """The typing of this config.""" - - name: str - """The name of the model.""" - - api_url: str - """The url of the model.""" - - headers: dict = {} - """The headers used for the request.""" - - max_length: int = 2048 - """The max length of the model, if not provided, defaults to 2048 in - model wrapper.""" - - # TODO: add support for timeout - timeout: int = 30 - """The timeout of the request.""" - - json_args: dict = None - """The additional arguments used within "json" keyword argument, - e.g. `request.post(json={..., **json_args})`, which depends on the - specific requirements of the model API.""" - - post_args: dict = None - """The keywords args used within `requests.post()`, e.g. `request.post(..., - **generate_args)`, which depends on the specific requirements of the - model API.""" - - max_retries: int = _DEFAULT_MAX_RETRIES - """The max retries of the request.""" - - messages_key: str = _DEFAULT_MESSAGES_KEY - """The key of the prompt messages in `requests.post()`, - e.g. `request.post(json={${messages_key}: messages, **json_args})`. For - huggingface and modelscope inference API, the key is `inputs`""" - - budget: float = _DEFAULT_API_BUDGET - """The total budget using this model. Set to `None` means no limit.""" diff --git a/src/agentscope/memory/temporary_memory.py b/src/agentscope/memory/temporary_memory.py index d95c8fad8..79ff5e6c8 100644 --- a/src/agentscope/memory/temporary_memory.py +++ b/src/agentscope/memory/temporary_memory.py @@ -13,7 +13,7 @@ from loguru import logger from .memory import MemoryBase -from ..models import load_model_by_name +from ..models import load_model_by_config_name from ..service.retrieval.retrieval_from_list import retrieve_from_list from ..service.retrieval.similarity import Embedding @@ -34,7 +34,7 @@ def __init__( # prepare embedding model if needed if isinstance(embedding_model, str): - self.embedding_model = load_model_by_name(embedding_model) + self.embedding_model = load_model_by_config_name(embedding_model) else: self.embedding_model = embedding_model diff --git a/src/agentscope/message.py b/src/agentscope/message.py index ae0d84672..38d0d06c9 100644 --- a/src/agentscope/message.py +++ b/src/agentscope/message.py @@ -240,7 +240,6 @@ def __init__( self._is_placeholder = True self._host = host self._port = port - self._client = RpcAgentClient(host, port) self._task_id = task_id def __is_local(self, key: Any) -> bool: @@ -279,7 +278,8 @@ def update_value(self) -> MessageBase: """Get attribute values from rpc agent server immediately""" if self._is_placeholder: # retrieve real message from rpc agent server - result = self._client.call_func( + client = RpcAgentClient(self._host, self._port) + result = client.call_func( func_name="_get", value=json.dumps({"task_id": self._task_id}), ) diff --git a/src/agentscope/models/__init__.py b/src/agentscope/models/__init__.py index 205f2607f..66bd69b44 100644 --- a/src/agentscope/models/__init__.py +++ b/src/agentscope/models/__init__.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- """ Import modules in models package.""" import json -from typing import Union, Sequence +from typing import Union, Type from loguru import logger -from .model import ModelWrapperBase -from .post_model import PostApiModelWrapper +from .config import ModelConfig +from .model import ModelWrapperBase, ModelResponse +from .post_model import ( + PostAPIModelWrapperBase, + PostAPIChatWrapper, +) from .openai_model import ( OpenAIWrapper, OpenAIChatWrapper, @@ -17,24 +21,48 @@ __all__ = [ "ModelWrapperBase", - "PostApiModelWrapper", + "ModelResponse", + "PostAPIModelWrapperBase", + "PostAPIChatWrapper", "OpenAIWrapper", "OpenAIChatWrapper", "OpenAIDALLEWrapper", "OpenAIEmbeddingWrapper", - "load_model_by_name", + "load_model_by_config_name", "read_model_configs", "clear_model_configs", ] -from ..configs.model_config import OpenAICfg, PostApiCfg +_MODEL_CONFIGS: dict[str, dict] = {} + + +def _get_model_wrapper(model_type: str) -> Type[ModelWrapperBase]: + """Get the specific type of model wrapper + Args: + model_type (`str`): The model type name. -_MODEL_CONFIGS = [] + Returns: + `Type[ModelWrapperBase]`: The corresponding model wrapper class. + """ + if model_type in ModelWrapperBase.type_registry: + return ModelWrapperBase.type_registry[ # type: ignore [return-value] + model_type + ] + elif model_type in ModelWrapperBase.registry: + return ModelWrapperBase.registry[ # type: ignore [return-value] + model_type + ] + else: + logger.warning( + f"Unsupported model_type [{model_type}]," + "use PostApiModelWrapper instead.", + ) + return PostAPIModelWrapperBase -def load_model_by_name(model_name: str) -> ModelWrapperBase: - """Load the model by name.""" +def load_model_by_config_name(config_name: str) -> ModelWrapperBase: + """Load the model by config name.""" if len(_MODEL_CONFIGS) == 0: raise ValueError( "No model configs loaded, please call " @@ -42,41 +70,29 @@ def load_model_by_name(model_name: str) -> ModelWrapperBase: ) # Find model config by name - config = None - for _ in _MODEL_CONFIGS: - if _["name"] == model_name: - config = {**_} - break - - if config is None: + if config_name not in _MODEL_CONFIGS: raise ValueError( - f"Cannot find [{model_name}] in loaded configurations.", + f"Cannot find [{config_name}] in loaded configurations.", ) + config = _MODEL_CONFIGS[config_name] - model_type = config.pop("type") - if model_type == "openai": - return OpenAIChatWrapper(**config) - elif model_type == "openai_dall_e": - return OpenAIDALLEWrapper(**config) - elif model_type == "openai_embedding": - return OpenAIEmbeddingWrapper(**config) - elif model_type == "post_api": - return PostApiModelWrapper(**config) - else: + if config is None: raise ValueError( - f"Cannot find [{config['type']}] in loaded configurations.", + f"Cannot find [{config_name}] in loaded configurations.", ) + model_type = config.model_type + return _get_model_wrapper(model_type=model_type)(**config) + def clear_model_configs() -> None: """Clear the loaded model configs.""" - global _MODEL_CONFIGS - _MODEL_CONFIGS = [] + _MODEL_CONFIGS.clear() def read_model_configs( configs: Union[dict, str, list], - empty_first: bool = False, + clear_existing: bool = False, ) -> None: """read model configs from a path or a list of dicts. @@ -84,14 +100,14 @@ def read_model_configs( configs (`Union[str, list, dict]`): The path of the model configs | a config dict | a list of model configs. - empty_first (`bool`, defaults to `False`): + clear_existing (`bool`, defaults to `False`): Whether to clear the loaded model configs before reading. Returns: `dict`: The model configs. """ - if empty_first: + if clear_existing: clear_model_configs() if isinstance(configs, str): @@ -108,40 +124,18 @@ def read_model_configs( ) cfgs = configs - # Checking - format_configs: list[Union[OpenAICfg, PostApiCfg]] = [] - for cfg in cfgs: - if "type" not in cfg: - raise ValueError( - f"Cannot find `type` in model config: {cfg}, " - f'whose value should be choice from ["openai", ' - f'"post_api"]', - ) - - if cfg["type"] == "openai": - openai_cfg = OpenAICfg() - openai_cfg.init(**cfg) - format_configs += [openai_cfg] - - elif cfg["type"] == "post_api": - post_api_cfg = PostApiCfg() - post_api_cfg.init(**cfg) - format_configs += [post_api_cfg] - - else: - raise ValueError( - f"Unknown model type: {cfg['type']}, please " - f"choice from ['openai', 'post_api']]", - ) + format_configs = ModelConfig.format_configs(configs=cfgs) # check if name is unique - global _MODEL_CONFIGS for cfg in format_configs: - if cfg["name"] in [_["name"] for _ in _MODEL_CONFIGS]: - raise ValueError(f'Model name "{cfg.name}" already exists.') - - _MODEL_CONFIGS.append(cfg) + if cfg.config_name in _MODEL_CONFIGS: + raise ValueError( + f"config_name [{cfg.config_name}] already exists.", + ) + _MODEL_CONFIGS[cfg.config_name] = cfg # print the loaded model configs - model_names = [_["name"] for _ in _MODEL_CONFIGS] - logger.info("Load configs for model: {}", ", ".join(model_names)) + logger.info( + "Load configs for model wrapper: {}", + ", ".join(_MODEL_CONFIGS.keys()), + ) diff --git a/src/agentscope/models/config.py b/src/agentscope/models/config.py new file mode 100644 index 000000000..947ef72d1 --- /dev/null +++ b/src/agentscope/models/config.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""The model config.""" +from typing import Union, Sequence, Any + +from loguru import logger + + +class ModelConfig(dict): + """Base class for model config.""" + + __getattr__ = dict.__getitem__ + __setattr__ = dict.__setitem__ + + def __init__( + self, + config_name: str, + model_type: str = None, + **kwargs: Any, + ): + """Initialize the config with the given arguments, and checking the + type of the arguments. + + Args: + config_name (`str`): A unique name of the model config. + model_type (`str`, optional): The class name (or its model type) of + the generated model wrapper. Defaults to None. + + Raises: + `ValueError`: If `config_name` is not provided. + """ + if config_name is None: + raise ValueError("The `config_name` field is required for Cfg") + if model_type is None: + logger.warning( + f"`model_type` is not provided in config [{config_name}]," + " use `PostAPIModelWrapperBase` by default.", + ) + super().__init__( + config_name=config_name, + model_type=model_type, + **kwargs, + ) + + @classmethod + def format_configs( + cls, + configs: Union[Sequence[dict], dict], + ) -> Sequence: + """Covert config dicts into a list of ModelConfig. + + Args: + configs (Union[Sequence[dict], dict]): configs in dict format. + + Returns: + Sequence[ModelConfig]: converted ModelConfig list. + """ + if isinstance(configs, dict): + return [ModelConfig(**configs)] + return [ModelConfig(**cfg) for cfg in configs] diff --git a/src/agentscope/models/model.py b/src/agentscope/models/model.py index 77bacee6b..02624066b 100644 --- a/src/agentscope/models/model.py +++ b/src/agentscope/models/model.py @@ -5,12 +5,12 @@ .. code-block:: python { - "type": "openai" | "post_api", - "name": "{model_name}", + "config_name": "{config_name}", + "model_type": "openai" | "post_api" | ..., ... } -After that, you can specify model by {model_name}. +After that, you can specify model by {config_name}. Note: The parameters for different types of models are different. For OpenAI API, @@ -19,8 +19,8 @@ .. code-block:: python { - "type": "openai", - "name": "{name of your model}", + "config_name": "{id of your model}", + "model_type": "openai", "model_name": "{model_name_for_openai, e.g. gpt-3.5-turbo}", "api_key": "{your_api_key}", "organization": "{your_organization, if needed}", @@ -39,8 +39,8 @@ .. code-block:: python { - "type": "post_api", - "name": "{model_name}", + "config_name": "{config_name}", + "model_type": "post_api", "api_url": "{api_url}", "headers": {"Authorization": "Bearer {API_TOKEN}"}, "max_length": {max_length_of_model}, @@ -57,7 +57,8 @@ import time from abc import ABCMeta from functools import wraps -from typing import Union, Any, Callable +from typing import Sequence, Any, Callable +import json from loguru import logger @@ -67,6 +68,46 @@ from ..constants import _DEFAULT_RETRY_INTERVAL +class ModelResponse: + """Encapsulation of data returned by the model. + + The main purpose of this class is to align the return formats of different + models and act as a bridge between models and agents. + """ + + def __init__( + self, + text: str = None, + embedding: Sequence = None, + image_urls: Sequence[str] = None, + raw: dict = None, + ) -> None: + self._text = text + self._embedding = embedding + self._image_urls = image_urls + self._raw = raw + + @property + def text(self) -> str: + """Text field.""" + return self._text + + @property + def embedding(self) -> Sequence: + """Embedding field.""" + return self._embedding + + @property + def image_urls(self) -> Sequence[str]: + """Image URLs field.""" + return self._image_urls + + @property + def raw(self) -> dict: + """Raw dictionary field.""" + return self._raw + + def _response_parse_decorator( model_call: Callable, ) -> Callable: @@ -146,27 +187,38 @@ def __new__(mcs, name: Any, bases: Any, attrs: Any) -> Any: attrs["__call__"] = _response_parse_decorator(attrs["__call__"]) return super().__new__(mcs, name, bases, attrs) + def __init__(cls, name: Any, bases: Any, attrs: Any) -> None: + if not hasattr(cls, "registry"): + cls.registry = {} + cls.type_registry = {} + else: + cls.registry[name] = cls + if hasattr(cls, "model_type"): + cls.type_registry[cls.model_type] = cls + super().__init__(name, bases, attrs) + class ModelWrapperBase(metaclass=_ModelWrapperMeta): """The base class for model wrapper.""" - def __init__( - self, - name: str, - ) -> None: - r"""Base class for model wrapper. + def __init__(self, config_name: str, **kwargs: Any) -> None: + """Base class for model wrapper. All model wrappers should inherit this class and implement the `__call__` function. Args: - name (`str`): - The name of the model, which is used to extract configuration + config_name (`str`): + The id of the model, which is used to extract configuration from the config file. """ - self.name = name + self.config_name = config_name + logger.info( + f"Initialize model [{config_name}] with config:\n" + f"{json.dumps(kwargs, indent=2)}", + ) - def __call__(self, *args: Any, **kwargs: Any) -> Union[str, dict, list]: + def __call__(self, *args: Any, **kwargs: Any) -> ModelResponse: """Processing input with the model.""" raise NotImplementedError( f"Model Wrapper [{type(self).__name__}]" diff --git a/src/agentscope/models/openai_model.py b/src/agentscope/models/openai_model.py index fc8eff640..9c47e886c 100644 --- a/src/agentscope/models/openai_model.py +++ b/src/agentscope/models/openai_model.py @@ -4,7 +4,7 @@ from loguru import logger -from .model import ModelWrapperBase +from .model import ModelWrapperBase, ModelResponse from ..file_manager import file_manager try: @@ -16,6 +16,7 @@ from ..utils.monitor import get_full_name from ..utils import QuotaExceededError from ..utils.token_utils import get_openai_max_length +from ..constants import _DEFAULT_API_BUDGET class OpenAIWrapper(ModelWrapperBase): @@ -23,23 +24,22 @@ class OpenAIWrapper(ModelWrapperBase): def __init__( self, - name: str, + config_name: str, model_name: str = None, api_key: str = None, organization: str = None, client_args: dict = None, generate_args: dict = None, - budget: float = None, + budget: float = _DEFAULT_API_BUDGET, + **kwargs: Any, ) -> None: """Initialize the openai client. Args: - name (`str`): - The name of the model wrapper, which is used to identify - model configs. + config_name (`str`): + The name of the model config. model_name (`str`, default `None`): - The name of the model to use in OpenAI API. If not - specified, it will be the same as `name`. + The name of the model to use in OpenAI API. api_key (`str`, default `None`): The API key for OpenAI API. If not specified, it will be read from the environment variable `OPENAI_API_KEY`. @@ -55,14 +55,23 @@ def __init__( The total budget using this model. Set to `None` means no limit. """ - super().__init__(name) + if model_name is None: + model_name = config_name + super().__init__( + config_name=config_name, + model_name=model_name, + client_args=client_args, + generate_args=generate_args, + budget=budget, + **kwargs, + ) if openai is None: raise ImportError( "Cannot find openai package in current python environment.", ) - self.model_name = model_name or name + self.model = model_name self.generate_args = generate_args or {} self.client = openai.OpenAI( @@ -73,10 +82,10 @@ def __init__( # Set the max length of OpenAI model try: - self.max_length = get_openai_max_length(self.model_name) + self.max_length = get_openai_max_length(self.model) except Exception as e: logger.warning( - f"fail to get max_length for {self.model_name}: " f"{e}", + f"fail to get max_length for {self.model}: " f"{e}", ) self.max_length = None @@ -89,9 +98,9 @@ def __init__( def _register_budget(self) -> None: self.monitor = MonitorFactory.get_monitor() self.monitor.register_budget( - model_name=self.model_name, + model_name=self.model, value=self.budget, - prefix=self.model_name, + prefix=self.model, ) def _register_default_metrics(self) -> None: @@ -110,12 +119,14 @@ def _metric(self, metric_name: str) -> str: Returns: `str`: Metric name of this wrapper. """ - return get_full_name(name=metric_name, prefix=self.model_name) + return get_full_name(name=metric_name, prefix=self.model) class OpenAIChatWrapper(OpenAIWrapper): """The model wrapper for OpenAI's chat API.""" + model_type: str = "openai" + def _register_default_metrics(self) -> None: # Set monitor accordingly # TODO: set quota to the following metrics @@ -136,9 +147,8 @@ def _register_default_metrics(self) -> None: def __call__( self, messages: list, - return_raw: bool = False, **kwargs: Any, - ) -> Union[str, dict]: + ) -> ModelResponse: """Processes a list of messages to construct a payload for the OpenAI API call. It then makes a request to the OpenAI API and returns the response. This method also updates monitoring metrics based on the @@ -153,8 +163,6 @@ def __call__( Args: messages (`list`): A list of messages to process. - return_raw (`bool`, default `False`): - Whether to return the raw response from OpenAI API. **kwargs (`Any`): The keyword arguments to OpenAI chat completions API, e.g. `temperature`, `max_tokens`, `top_p`, etc. Please refer to @@ -162,8 +170,9 @@ def __call__( for more detailed arguments. Returns: - A dictionary that contains the response of the model and related - information (e.g. cost, time, the number of tokens, etc.). + `ModelResponse`: + The response text in text field, and the raw response in + raw field. Note: `parse_func`, `fault_handler` and `max_retries` are reserved for @@ -191,7 +200,7 @@ def __call__( # step3: forward to generate response response = self.client.chat.completions.create( - model=self.model_name, + model=self.model, messages=messages, **kwargs, ) @@ -199,7 +208,7 @@ def __call__( # step4: record the api invocation if needed self._save_model_invocation( arguments={ - "model": self.model_name, + "model": self.model, "messages": messages, **kwargs, }, @@ -210,22 +219,24 @@ def __call__( try: self.monitor.update( response.usage.model_dump(), - prefix=self.model_name, + prefix=self.model, ) except QuotaExceededError as e: # TODO: optimize quota exceeded error handling process logger.error(e.message) - # step6: return raw response if needed - if return_raw: - return response.model_dump() - else: - return response.choices[0].message.content + # step6: return response + return ModelResponse( + text=response.choices[0].message.content, + raw=response.model_dump(), + ) class OpenAIDALLEWrapper(OpenAIWrapper): """The model wrapper for OpenAI's DALLĀ·E API.""" + model_type: str = "openai_dall_e" + _resolutions: list = [ "1792*1024", "1024*1792", @@ -247,20 +258,16 @@ def _register_default_metrics(self) -> None: def __call__( self, prompt: str, - return_raw: bool = False, save_local: bool = False, **kwargs: Any, - ) -> Union[dict, list[str]]: + ) -> ModelResponse: """ Args: prompt (`str`): The prompt string to generate images from. - return_raw (`bool`, default `False`): - Whether to return the raw response from OpenAI API. save_local: (`bool`, default `False`): Whether to save the generated images locally, and replace - the returned image url with the local path. When - `return_raw` is `True`, this argument is ignored. + the returned image url with the local path. **kwargs (`Any`): The keyword arguments to OpenAI image generation API, e.g. `n`, `quality`, `response_format`, `size`, etc. Please refer to @@ -268,9 +275,9 @@ def __call__( for more detailed arguments. Returns: - Raw response in json format if `return_raw` is `True`, otherwise - a list of image urls. When `save_local` is `False`, the image - urls is + `ModelResponse`: + A list of image urls in image_urls field and the + raw response in raw field. Note: `parse_func`, `fault_handler` and `max_retries` are reserved for @@ -291,7 +298,7 @@ def __call__( # step2: forward to generate response try: response = self.client.images.generate( - model=self.model_name, + model=self.model, prompt=prompt, **kwargs, ) @@ -304,32 +311,30 @@ def __call__( # step3: record the model api invocation if needed self._save_model_invocation( arguments={ - "model": self.model_name, + "model": self.model, "prompt": prompt, **kwargs, }, json_response=response.model_dump(), ) - # step4: return raw response if needed - if return_raw: - return response - else: - images = response.model_dump()["data"] - # Get image urls as a list - urls = [_["url"] for _ in images] + # step4: return response + raw_response = response.model_dump() + images = raw_response["data"] + # Get image urls as a list + urls = [_["url"] for _ in images] - if save_local: - # Return local url if save_local is True - local_urls = [file_manager.save_image(_) for _ in urls] - return local_urls - else: - return urls + if save_local: + # Return local url if save_local is True + urls = [file_manager.save_image(_) for _ in urls] + return ModelResponse(image_urls=urls, raw=raw_response) class OpenAIEmbeddingWrapper(OpenAIWrapper): """The model wrapper for OpenAI embedding API.""" + model_type: str = "openai_embedding" + def _register_default_metrics(self) -> None: # Set monitor accordingly # TODO: set quota to the following metrics @@ -346,16 +351,13 @@ def _register_default_metrics(self) -> None: def __call__( self, texts: Union[list[str], str], - return_raw: bool = False, **kwargs: Any, - ) -> Union[list, dict]: + ) -> ModelResponse: """Embed the messages with OpenAI embedding API. Args: texts (`list[str]` or `str`): The messages used to embed. - return_raw (`bool`, default `False`): - Whether to return the raw response from OpenAI API. **kwargs (`Any`): The keyword arguments to OpenAI embedding API, e.g. `encoding_format`, `user`. Please refer to @@ -363,8 +365,9 @@ def __call__( for more detailed arguments. Returns: - A list of embeddings when `return_raw` is `False`, otherwise the - raw response from OpenAI API. + `ModelResponse`: + A list of embeddings in embedding field and the + raw response in raw field. Note: `parse_func`, `fault_handler` and `max_retries` are reserved for @@ -385,26 +388,29 @@ def __call__( # step2: forward to generate response response = self.client.embeddings.create( input=texts, - model=self.model_name, + model=self.model, **kwargs, ) # step3: record the model api invocation if needed self._save_model_invocation( arguments={ - "model": self.model_name, + "model": self.model, "input": texts, **kwargs, }, json_response=response.model_dump(), ) - # step4: return raw response if needed + # step4: return response response_json = response.model_dump() - if return_raw: - return response_json + if len(response_json["data"]) == 0: + return ModelResponse( + embedding=response_json["data"]["embedding"][0], + raw=response_json, + ) else: - if len(response_json["data"]) == 0: - return response_json["data"]["embedding"][0] - else: - return [_["embedding"] for _ in response_json["data"]] + return ModelResponse( + embedding=[_["embedding"] for _ in response_json["data"]], + raw=response_json, + ) diff --git a/src/agentscope/models/post_model.py b/src/agentscope/models/post_model.py index 40fb176da..0addce7c6 100644 --- a/src/agentscope/models/post_model.py +++ b/src/agentscope/models/post_model.py @@ -7,18 +7,20 @@ import requests from loguru import logger -from .model import ModelWrapperBase +from .model import ModelWrapperBase, ModelResponse from ..constants import _DEFAULT_MAX_RETRIES from ..constants import _DEFAULT_MESSAGES_KEY from ..constants import _DEFAULT_RETRY_INTERVAL -class PostApiModelWrapper(ModelWrapperBase): - """The model wrapper for the model deployed on the POST API.""" +class PostAPIModelWrapperBase(ModelWrapperBase): + """The base model wrapper for the model deployed on the POST API.""" + + model_type: str = "post_api" def __init__( self, - name: str, + config_name: str, api_url: str, headers: dict = None, max_length: int = 2048, @@ -28,12 +30,13 @@ def __init__( max_retries: int = _DEFAULT_MAX_RETRIES, messages_key: str = _DEFAULT_MESSAGES_KEY, retry_interval: int = _DEFAULT_RETRY_INTERVAL, + **kwargs: Any, ) -> None: """Initialize the model wrapper. Args: - name (`str`): - The name of the model. + config_name (`str`): + The id of the model. api_url (`str`): The url of the post request api. headers (`dict`, defaults to `None`): @@ -70,8 +73,19 @@ def __init__( **post_args ) """ - super().__init__(name) - + super().__init__( + config_name=config_name, + api_url=api_url, + headers=headers, + max_length=max_length, + timeout=timeout, + json_args=json_args, + post_args=post_args, + max_retries=max_retries, + messages_key=messages_key, + retry_interval=retry_interval, + **kwargs, + ) self.api_url = api_url self.headers = headers self.max_length = max_length @@ -82,7 +96,11 @@ def __init__( self.messages_key = messages_key self.retry_interval = retry_interval - def __call__(self, input_: str, **kwargs: Any) -> dict: + def _parse_response(self, response: dict) -> ModelResponse: + """Parse the response json data into ModelResponse""" + return ModelResponse(raw=response) + + def __call__(self, input_: str, **kwargs: Any) -> ModelResponse: """Calling the model with requests.post. Args: @@ -143,12 +161,34 @@ def __call__(self, input_: str, **kwargs: Any) -> dict: # step4: parse the response if response.status_code == requests.codes.ok: - return response.json()["data"]["response"]["choices"][0][ - "message" - ]["content"] + return self._parse_response(response.json()) else: logger.error(json.dumps(request_kwargs, indent=4)) raise RuntimeError( f"Failed to call the model with " f"requests.codes == {response.status_code}", ) + + +class PostAPIChatWrapper(PostAPIModelWrapperBase): + """A post api model wrapper compatilble with openai chat, e.g., vLLM, + FastChat.""" + + model_type: str = "post_api_chat" + + def _parse_response(self, response: dict) -> ModelResponse: + return ModelResponse( + text=response["data"]["response"]["choices"][0]["message"][ + "content" + ], + ) + + +class PostAPIDALLEWrapper(PostAPIModelWrapperBase): + """A post api model wrapper compatible with openai dalle""" + + model_type: str = "post_api_dalle" + + def _parse_response(self, response: dict) -> ModelResponse: + urls = [img["url"] for img in response["data"]["response"]["data"]] + return ModelResponse(image_urls=urls) diff --git a/src/agentscope/service/text_processing/summarization.py b/src/agentscope/service/text_processing/summarization.py index e6b61830b..17a4824c4 100644 --- a/src/agentscope/service/text_processing/summarization.py +++ b/src/agentscope/service/text_processing/summarization.py @@ -98,7 +98,7 @@ def summarization( except ValueError: return ServiceResponse( ServiceExecStatus.ERROR, - content=f"Summarization by model {model.model_name} fail", + content=f"Summarization by model {model.model} fail", ) else: try: diff --git a/tests/model_test.py b/tests/model_test.py new file mode 100644 index 000000000..4c247daec --- /dev/null +++ b/tests/model_test.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for model wrapper classes and functions +""" + +from typing import Any +import unittest + +from agentscope.models import ( + ModelResponse, + ModelWrapperBase, + OpenAIChatWrapper, + PostAPIModelWrapperBase, + _get_model_wrapper, + read_model_configs, + load_model_by_config_name, + clear_model_configs, +) + + +class TestModelWrapperSimple(ModelWrapperBase): + """A simple model wrapper class for test usage""" + + def __call__(self, *args: Any, **kwargs: Any) -> ModelResponse: + return ModelResponse(text=self.config_name) + + +class BasicModelTest(unittest.TestCase): + """Test cases for basic model wrappers""" + + def test_model_registry(self) -> None: + """Test the automatic registration mechanism of model wrapper.""" + # get model wrapper class by class name + self.assertEqual( + _get_model_wrapper(model_type="TestModelWrapperSimple"), + TestModelWrapperSimple, + ) + # get model wrapper class by model type + self.assertEqual( + _get_model_wrapper(model_type="openai"), + OpenAIChatWrapper, + ) + # return PostAPIModelWrapperBase if model_type is not supported + self.assertEqual( + _get_model_wrapper(model_type="unknown_model_wrapper"), + PostAPIModelWrapperBase, + ) + + def test_load_model_configs(self) -> None: + """Test to load model configs""" + configs = [ + { + "model_type": "openai", + "config_name": "gpt-4", + "model": "gpt-4", + "api_key": "xxx", + "organization": "xxx", + "generate_args": {"temperature": 0.5}, + }, + { + "model_type": "post_api", + "config_name": "my_post_api", + "api_url": "https://xxx", + "headers": {}, + "json_args": {}, + }, + ] + # load a list of configs + read_model_configs(configs=configs, clear_existing=True) + model = load_model_by_config_name("gpt-4") + self.assertEqual(model.config_name, "gpt-4") + model = load_model_by_config_name("my_post_api") + self.assertEqual(model.config_name, "my_post_api") + self.assertRaises( + ValueError, + load_model_by_config_name, + "non_existent_id", + ) + + # load a single config + read_model_configs(configs=configs[0], clear_existing=True) + model = load_model_by_config_name("gpt-4") + self.assertEqual(model.config_name, "gpt-4") + self.assertRaises(ValueError, load_model_by_config_name, "my_post_api") + + # automatically detect model with the same id + self.assertRaises(ValueError, read_model_configs, configs[0]) + read_model_configs( + configs={ + "model_type": "TestModelWrapperSimple", + "config_name": "test_model_wrapper", + "args": {}, + }, + ) + test_model = load_model_by_config_name("test_model_wrapper") + response = test_model() + self.assertEqual(response.text, "test_model_wrapper") + clear_model_configs() + self.assertRaises( + ValueError, + load_model_by_config_name, + "test_model_wrapper", + ) diff --git a/tests/monitor_test.py b/tests/monitor_test.py index 26ac418c8..3fc54470a 100644 --- a/tests/monitor_test.py +++ b/tests/monitor_test.py @@ -15,6 +15,7 @@ class MonitorFactoryTest(unittest.TestCase): "Test class for MonitorFactory" def setUp(self) -> None: + MonitorFactory._instance = None # pylint: disable=W0212 self.db_path = f"test-{uuid.uuid4()}.db" _ = MonitorFactory.get_monitor(db_path=self.db_path) diff --git a/tests/prompt_engine_test.py b/tests/prompt_engine_test.py index 5665d6e63..2a99ff51e 100644 --- a/tests/prompt_engine_test.py +++ b/tests/prompt_engine_test.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """Unit test for prompt engine.""" import unittest -from typing import Union, Any +from typing import Any from agentscope.models import read_model_configs -from agentscope.models import load_model_by_name -from agentscope.models import OpenAIWrapper +from agentscope.models import load_model_by_config_name +from agentscope.models import ModelResponse, OpenAIWrapper from agentscope.prompt import PromptEngine @@ -29,8 +29,8 @@ def setUp(self) -> None: read_model_configs( [ { - "type": "post_api", - "name": "open-source", + "model_type": "post_api", + "config_name": "open-source", "api_url": "http://xxx", "headers": {"Autherization": "Bearer {API_TOKEN}"}, "parameters": { @@ -38,15 +38,14 @@ def setUp(self) -> None: }, }, { - "type": "openai", - "name": "gpt-4", - "parameters": { - "api_key": "xxx", - "organization_id": "xxx", - }, + "model_type": "openai", + "config_name": "gpt-4", + "model_name": "gpt-4", + "api_key": "xxx", + "organization": "xxx", }, ], - empty_first=True, + clear_existing=True, ) def test_list_prompt(self) -> None: @@ -62,8 +61,8 @@ def __call__( self, *args: Any, **kwargs: Any, - ) -> Union[str, dict, list]: - return "" + ) -> ModelResponse: + return ModelResponse(text="") def _register_default_metrics(self) -> None: pass @@ -110,7 +109,7 @@ def _register_default_metrics(self) -> None: def test_str_prompt(self) -> None: """Test for string prompt.""" - model = load_model_by_name("open-source") + model = load_model_by_config_name("open-source") engine = PromptEngine(model) prompt = engine.join( diff --git a/tests/record_api_invocation_test.py b/tests/record_api_invocation_test.py index 3711e0b65..e5406003d 100644 --- a/tests/record_api_invocation_test.py +++ b/tests/record_api_invocation_test.py @@ -43,12 +43,12 @@ def test_record_model_invocation_with_init( # test agentscope.init(save_api_invoke=True) model = OpenAIChatWrapper( - name="gpt-4", + config_name="gpt-4", api_key="xxx", organization="xxx", ) - _ = model(messages=[], return_raw=True) + _ = model(messages=[]) # assert self.assert_invocation_record() diff --git a/tests/retrieval_from_list_test.py b/tests/retrieval_from_list_test.py index 848b5e205..34a786a39 100644 --- a/tests/retrieval_from_list_test.py +++ b/tests/retrieval_from_list_test.py @@ -8,7 +8,7 @@ from agentscope.service.service_status import ServiceExecStatus from agentscope.message import MessageBase, Msg, Tht from agentscope.memory.temporary_memory import TemporaryMemory -from agentscope.models import OpenAIEmbeddingWrapper +from agentscope.models import OpenAIEmbeddingWrapper, ModelResponse class TestRetrieval(unittest.TestCase): @@ -25,9 +25,9 @@ class DummyModel(OpenAIEmbeddingWrapper): def __init__(self) -> None: pass - def __call__(self, *args: Any, **kwargs: Any) -> dict: + def __call__(self, *args: Any, **kwargs: Any) -> ModelResponse: print(*args, **kwargs) - return {} + return ModelResponse(raw={}) dummy_model = DummyModel() diff --git a/tests/rpc_agent_test.py b/tests/rpc_agent_test.py index be8279657..cdf960dee 100644 --- a/tests/rpc_agent_test.py +++ b/tests/rpc_agent_test.py @@ -359,6 +359,8 @@ def test_standalone_multiprocess_init(self) -> None: self.assertEqual(msg["content"]["msg_num"], j + 2) j += 2 msg = agent_a(msg) + logger.chat(msg) self.assertTrue(msg["content"]["quota_exceeded"]) msg = agent_b(msg) + logger.chat(msg) self.assertTrue(msg["content"]["quota_exceeded"])