├── assets ├── demo.png └── demo-overcooked000.png ├── requirements.txt ├── setup ├── game.yaml ├── gameovercooked.yaml └── game2.yaml ├── env.py ├── LICENSE ├── docs └── README-cn.md ├── aagpt.py ├── aagpt-overcooked.py ├── utils.py ├── README.md ├── overcooked ├── env.py ├── agent.py └── utils.py └── agent.py /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aialt/AAGPT/HEAD/assets/demo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse==1.4.0 2 | openai==0.27.4 3 | pinecone-client==2.2.1 4 | PyYAML==6.0 -------------------------------------------------------------------------------- /assets/demo-overcooked000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aialt/AAGPT/HEAD/assets/demo-overcooked000.png -------------------------------------------------------------------------------- /setup/game.yaml: -------------------------------------------------------------------------------- 1 | common: 2 | openai_api_key: Your OpenAI API Key 3 | openai_model: gpt-3.5-turbo # gpt-3.5-turbo, gpt4, text-davinci-003 4 | env: 5 | env_name: dev 6 | # currently env_openai_api_key uses the same oepnai api key 7 | env_openai_api_key: =same_to_common 8 | agent: 9 | agent_id: 1 10 | agent_type: "agent_gptmem" 11 | # agent_openai_api_key uses the same oepnai api key 12 | agent_openai_api_key: =same_to_common 13 | goal: How to play pro in dota2 14 | init_task: Develop a task list 15 | agent_life: 256 -------------------------------------------------------------------------------- /setup/gameovercooked.yaml: -------------------------------------------------------------------------------- 1 | common: 2 | openai_api_key: Your OpenAI API Key 3 | openai_model: gpt-3.5-turbo # gpt-3.5-turbo, gpt4, text-davinci-003 4 | level: partial-divider_salad 5 | task: make a tomato salad and deliver it # or change others: make a tomato and lettuce salad and deliver it 6 | agents: 7 | n: 2 8 | names: [agent1, agent2] 9 | env: 10 | env_name: opencooking:overcookedEnv-v0 11 | # currently env_openai_api_key uses the same oepnai api key 12 | env_openai_api_key: =same_to_common 13 | agent1: 14 | agent_id: 1 15 | agent2: 16 | agent_id: 2 -------------------------------------------------------------------------------- /setup/game2.yaml: -------------------------------------------------------------------------------- 1 | common: 2 | openai_api_key: Your OpenAI API Key 3 | openai_model: gpt-3.5-turbo # gpt-3.5-turbo, gpt4, text-davinci-003 4 | env: 5 | env_name: dev 6 | # currently env_openai_api_key uses the same oepnai api key 7 | env_openai_api_key: =same_to_common 8 | agent: 9 | agent_id: 1 10 | agent_type: "agent_pineconemem" 11 | # agent_openai_api_key uses the same oepnai api key 12 | agent_openai_api_key: =same_to_common 13 | goal: How to play pro in dota2 14 | init_task: Develop a task list 15 | agent_use_pinecone: true 16 | # [your api key, your pinecone region] 17 | agent_pinecone_api_key: [Your Pinecone API , Your Pinecone Region] 18 | agent_pinecone_index: auto-agent-gpt-index 19 | agent_life: 256 -------------------------------------------------------------------------------- /env.py: -------------------------------------------------------------------------------- 1 | from utils import openai_call 2 | 3 | 4 | class Env: 5 | def __init__(self, config): 6 | self.env_config = config["env"] 7 | 8 | def exec(self, agent, task): 9 | """Execute the given task using the agent and return the result.""" 10 | # Get the context of the top 5 related tasks from the agent's memory 11 | context = agent.context_search(5) 12 | 13 | # Extract the task name 14 | task = task["task_name"] 15 | 16 | # Prepare the prompt for the AI 17 | prompt = f""" 18 | You are an AI who performs one task based on the following objective: {agent.goal}\n. 19 | Take into account these previously completed tasks: {context}\n. 20 | Your task: {task}\nResponse:""" 21 | 22 | # Call the OpenAI API to get the result 23 | return openai_call(prompt, temperature=0.7, max_tokens=2000) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Meng F 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/README-cn.md: -------------------------------------------------------------------------------- 1 | # AAGPT 自动代理GPT 2 | 3 | Auto-Agent-GPT(AAGPT)是一个实验性的开源应用程序,目的是展示大型语言模型的能力。 4 | 5 |
6 | 7 |
8 | 9 | ## 特性 10 | - 不同的记忆储存方式 11 | - 使用Prompt,用GPT本身作为记忆处理 12 | - 利用向量数据库用作内存(需要Pinecone API密钥) 13 | - 为执行的代理设置寿命限制,以达到可以节省成本的目的 14 | 15 | ## 安装 16 | 要安装AAGPT,请按照以下步骤进行操作: 17 | 18 | 1. 从GitHub克隆AAGPT存储库并导航到下载的文件夹。 19 | 20 | ```bash: 21 | git clone git@github.com:hyintell/AAGPT.git 22 | cd AAGPT 23 | ``` 24 | 2. 在终端中使用以下命令和pip: 25 | 26 | ```bash 27 | pip install -r requirements.txt 28 | ``` 29 | ## 快速尝试 30 | 只需要上述两步,您就可以并结合您的OpenAI API密钥来使用AAGPT的自然语言处理能力。这将为您提供一个强大的工具,使您能够自动化许多与文本处理相关的任务,例如文本摘要、机器翻译、文本分类、问答系统等。使用AAGPT,您可以快速创建自己的文本处理应用程序,并为您的项目带来更高的生产力和创造力。 31 | 32 | 1. 导航并打开 `setup/game.yaml` 文件, 在 `openai_api_key` 空处输入您的penAI API密钥. 33 | 34 | 2. 回到上层文件夹,并运行 `aagpt.py` 文件: 35 | 36 | ```bash 37 | python aagpt.py 38 | ``` 39 | ## 详细设置以及参数 40 | 为了更详细的使用AAGPT,您需要设置相关的API和参数才能使用该应用程序。您可以按照以下步骤完成此操作: 41 | 1. 导航到 `setup` 文件夹。 42 | ```bash 43 | cd setup 44 | ``` 45 | 2. 在`setup`文件夹中有两个环境设置文件,分别是`game.yaml`和`game2.yaml`.他们分别代表两个不同的游戏设定。`game.yaml`将Prompt储存,并使用ChatGPT作为内存处理。`game2.yaml`将生成的结果储存为向量形式,并使用Pinecone作为云端内存处理工具。您可以选择其中一个来设置API。 46 | 3. 以 `game.yaml` 文件为例子, 你将使用ChatGPT作为记忆处理方式,因此您需要填写以下相关信息: 47 | * `openai_api_key`:您的OpenAI密钥. 如果您没有密钥,您可以在OpenAI网站上创建一个免费账户并获取它. 48 | * `openai_model`: 您想要使用的OpenAI模型. 在默认情况下,我们使用的是 `"gpt-3.5-turbo` 模型. (可选择以下模型:"gpt-3.5-turbo"、"gpt4"或"text-davinci-003"。) 49 | * `env_openai_api_key`: 针对于环境的OpenAI密钥,您可以与上面的密钥保持一致. 50 | * `agent_openai_api_key`: 针对于代理的OpenAI密钥,您可以与上面的密钥保持一致. 51 | * `goal`: 您想要达到的目标或者问题,AAGPT会围绕着这个问题进行任务的建设的讨论,例如:`"怎样解决辛普森悖论?"` 52 | * `init_task`: 传递给aagpt任务列表最初始的任务,提出的任务越想详细,AAGPT就会有更好的引导,例如:`"辛普森悖论的定义是什么,在什么情况下会出现这个悖论,列出解决的这个问题的任务列表"` 53 | * `agent_life`: 代理运行的上限,默认情况下是256次更新. 54 | 55 | 另外:您可以选择使用以 Pinecone 作为内存存储,即 `game2.yaml`环境设定文件。 除了上述设置外,您还需要填写以下信息: 56 | * `agent_pinecone_api_key`: 填入的格式应为`[Pinecone API, Pinecone Region]`, 其中,`Pinecone API`为Pinecone密钥, `Pinecone Region`为服务器地区. 两者都可以Pinecone的API界面找到。如果您没有相关密钥,您可以在Pinecone网站上创建一个免费账户并获取它. 57 | * `agent_pinecone_index`: 要使用的 Pinecone 记忆库索引名称。 默认情况下,我们使用“aagpt_agent_index”。需要注意的是,免费的 Pinecone 账户只能创建一个索引,因此您需要确保您的 Pinecone 账户中没有其他索引或者充值以创建更多的索引。 58 | 59 | 60 | ## 使用 61 | 设置正确的 API 后,您可以通过在终端中执行 `aagpt.py` 文件来测试 AAGPT: 62 | ```bash 63 | python aagpt.py 64 | ``` 65 | 66 | AAGPT 运行后,您可以通过输入提示并观察其响应来开始与其交互。 67 | 68 | 如果要更改环境的设置,可以使用以下命令: 69 | 70 | ```bash 71 | python aagpt.py --world_root setup/game2.yaml 72 | ``` 73 | 74 | ## 未来的工作 75 | - [ ] 一个网页端的界面 76 | - [ ] 支持更多的记忆储存方式 77 | - [ ] 支持多个智能体代理的回答和动作 78 | - [ ] 支持更多现有的LLM的模型 79 | 80 | ## 鸣谢 81 | 82 | 我们非常感谢开源项目所做的贡献: [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) and [BabyAGI](https://github.com/yoheinakajima/babyagi). 83 | -------------------------------------------------------------------------------- /aagpt.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time 3 | import os 4 | import yaml 5 | 6 | from agent import AgentGPTMEM, AgentPCMEM 7 | from env import Env 8 | import utils 9 | 10 | 11 | os.system('cls' if os.name == 'nt' else 'clear') 12 | 13 | def setup_world(): 14 | # Parse command line arguments 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument('--world_root', type=str, default='setup/game.yaml') 17 | args = parser.parse_args() 18 | 19 | # Load world setup from YAML file 20 | with open(args.world_root, 'r') as f: 21 | ws = yaml.load(f, Loader=yaml.FullLoader) 22 | 23 | # Perform common setup operations 24 | utils.common(ws) 25 | 26 | return ws 27 | 28 | 29 | def main_loop(agent, env): 30 | # Initialize time step counter 31 | time_step = 0 32 | 33 | # Main loop 34 | while True: 35 | time_step += 1 36 | 37 | # If agent has tasks to perform 38 | if agent.task_list: 39 | 40 | print("=" * os.get_terminal_size().columns) 41 | goal_des = " GOAL: " + agent.goal + " " 42 | print("\033[95m\033[1m" + "=" * ((os.get_terminal_size().columns - len(goal_des)) // 2) + goal_des + "=" * ((os.get_terminal_size().columns - len(goal_des)) // 2) + "\033[0m\033[0m") 43 | print("=" * os.get_terminal_size().columns) 44 | 45 | # Display the current tasks 46 | print("\033[94m\033[1m" + "\nTASK LIST:\n" + "\033[0m\033[0m") 47 | for t in agent.task_list: 48 | print("\033[94m" + str(t["task_id"]) + ": " + t["task_name"] + "\033[0m") 49 | 50 | # Perform the next task 51 | task = agent.act() 52 | print("\033[92m\033[1m" + "\nCURRENT TASK:\n" + "\033[0m\033[0m") 53 | print("\033[92m" + task["task_name"] + "\033[0m") 54 | 55 | # Execute the task in the environment 56 | result = env.exec(agent, task) 57 | print("\033[93m\033[1m" + "\nRESULT:\n" + "\033[0m\033[0m") 58 | print("\033[93m" + result + "\033[0m") 59 | 60 | # Update the agent with the task result 61 | agent.receive(result) 62 | 63 | print("\n" + "\033[91m" + "LIFE: " + str(time_step) + "/" + str(agent.life)+ "\033[0m") 64 | # Sleep for 1 second before the next iteration 65 | time.sleep(1) 66 | 67 | # End the loop if the agent's life is over 68 | if time_step > agent.life: 69 | print("\nGood Game:)") 70 | break 71 | 72 | 73 | if __name__ == "__main__": 74 | # Set up the world 75 | ws = setup_world() 76 | 77 | # Create the agent based on the world setup 78 | if ws["agent"]["agent_type"] == "agent_gptmem": 79 | agent = AgentGPTMEM(ws) 80 | else: 81 | agent = AgentPCMEM(ws) 82 | 83 | # Create the environment 84 | env = Env(ws) 85 | 86 | # Start the main loop 87 | main_loop(agent, env) 88 | -------------------------------------------------------------------------------- /aagpt-overcooked.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import cv2 3 | import yaml 4 | from overcooked.agent import GPTAgent, ChatBot 5 | from overcooked.env import OvercookedEnvGPT 6 | from overcooked.utils import get_task_queue, colors, fix_seed 7 | 8 | 9 | """ 10 | ChatGPT for Overcooking 11 | """ 12 | def parse_arguments(): 13 | parser = argparse.ArgumentParser("Overcooked 2 argument parser") 14 | # Environment 15 | parser.add_argument('--world_root', type=str, default='setup/gameovercooked.yaml') 16 | parser.add_argument("--max-num-timesteps", type=int, default=100, help="Max number of timesteps to run") 17 | parser.add_argument("--max-num-subtasks", type=int, default=14, help="Max number of subtasks for recipe") 18 | parser.add_argument("--seed", type=int, default=1, help="Fix pseudorandom seed") 19 | parser.add_argument("--with-image-obs", action="store_true", default=True, help="Return observations as images (instead of objects)") 20 | # Visualizations 21 | parser.add_argument("--record", action="store_true", default=False, help="Save observation at each time step as an image in misc/game/record") 22 | parser.add_argument("--render", action="store_true", help="render the images") 23 | # GPT 24 | parser.add_argument("--gpt", action="store_true", default=True) 25 | return parser.parse_args() 26 | 27 | 28 | def main_loop(args): 29 | """The main loop for running experiments.""" 30 | print("Initializing environment and agents.") 31 | 32 | # Load world setup from YAML file 33 | with open(args.world_root, 'r') as f: 34 | ws = yaml.load(f, Loader=yaml.FullLoader) 35 | 36 | num_agents = ws["common"]["agents"]["n"] 37 | level = ws["common"]["level"] 38 | env = OvercookedEnvGPT(num_agents, level, arglist=args) 39 | obs = env.reset() 40 | 41 | # initialize the agent 42 | agent1 = GPTAgent(1, level, args) 43 | agent2 = GPTAgent(2, level, args) 44 | 45 | chatbot = ChatBot(num_agents, ws, args) 46 | 47 | task_queue = get_task_queue(ws, chatbot, agent1, agent2) 48 | 49 | # start to do the queue 50 | task_id, cur_agent_id, global_steps, max_steps = 0, 0, 1, 200 51 | # initialise the agent's state 52 | action_dict = {'agent-1': (0, 0), 'agent-2': (0, 0)} 53 | _, _, _, info = env.step(action_dict=action_dict) 54 | while True: 55 | f = task_queue[task_id][0] 56 | arg = task_queue[task_id][1] 57 | if str(agent1) in str(f): 58 | cur_agent_id = 1 59 | print("agent1 is in the task") 60 | agent1.reset_state() 61 | # update state 62 | agent1_states = info["agents_states"]["agent-1"] 63 | agent1.set_state(location=agent1_states["loc"], action_str=agent1_states["action_str"], action_loc=agent1_states["action_loc"]) 64 | elif str(agent2) in str(f): 65 | cur_agent_id = 2 66 | print("agent2 is in the task") 67 | agent2.reset_state() 68 | # update state 69 | agent2_states = info["agents_states"]["agent-2"] 70 | agent2.set_state(location=agent2_states["loc"], action_str=agent2_states["action_str"], action_loc=agent2_states["action_loc"]) 71 | # execute the subtask... 72 | subtask_finish, action = f(arg) 73 | if cur_agent_id == 1: 74 | action_dict = {'agent-1': action, 'agent-2': (0, 0)} 75 | elif cur_agent_id == 2: 76 | action_dict = {'agent-1': (0, 0), 'agent-2': action} 77 | # execute the action and 78 | _, _, _, info = env.step(action_dict=action_dict) 79 | global_steps += 1 80 | if global_steps > max_steps: 81 | print("Max Timestep Has Reached!") 82 | break 83 | if args.render: 84 | cv2.imshow('Overcooked', info['image_obs'][:,:,::-1]) 85 | cv2.waitKey(30) 86 | if subtask_finish: 87 | print(colors.GREEN + f"task complete: {str(f)}({str(arg)})" + colors.ENDC) 88 | task_id += 1 89 | if task_id == len(task_queue): 90 | print(colors.GREEN + f"ALL TASKS COMPLETE: score={global_steps} (lower the better)" + colors.ENDC) 91 | break 92 | 93 | 94 | if __name__ == '__main__': 95 | args = parse_arguments() 96 | fix_seed(seed=args.seed) 97 | main_loop(args) 98 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | import openai 4 | import pinecone 5 | 6 | 7 | OPENAI_API_MODEL = "" 8 | 9 | def common(config): 10 | # Set up API keys and models from the configuration 11 | OPENAI_API_KEY = config['common']['openai_api_key'] 12 | assert OPENAI_API_KEY, "OPENAI_API_KEY environment variable is missing from config yaml" 13 | 14 | global OPENAI_API_MODEL 15 | OPENAI_API_MODEL = config['common']['openai_model'] 16 | assert OPENAI_API_MODEL, "OPENAI_API_MODEL environment variable is missing from config yaml" 17 | 18 | # Print a message if using GPT-4 19 | if "gpt-4" in OPENAI_API_MODEL.lower(): 20 | print("\033[92m\033[1m\n>>USING GPT-4.\033[0m\033[0m") 21 | 22 | # Get the goal and initial task from the configuration 23 | OBJECTIVE = config['agent']['goal'] 24 | INITIAL_TASK = config['agent']['init_task'] 25 | 26 | assert OBJECTIVE, "OBJECTIVE environment variable is missing from config yaml" 27 | assert INITIAL_TASK, "INITIAL_TASK environment variable is missing from config yaml" 28 | 29 | # Configure OpenAI and Pinecone 30 | openai.api_key = OPENAI_API_KEY 31 | 32 | if config["agent"]["agent_type"] == "agent_pineconemem": 33 | PINECONE_API_KEY = config['agent']['agent_pinecone_api_key'][0] 34 | assert PINECONE_API_KEY, "PINECONE_API_KEY environment variable is missing from config yaml" 35 | 36 | PINECONE_ENVIRONMENT = config['agent']['agent_pinecone_api_key'][1] 37 | assert PINECONE_ENVIRONMENT, "PINECONE_ENVIRONMENT environment variable is missing from config yaml" 38 | 39 | pinecone.init(api_key=PINECONE_API_KEY, environment=PINECONE_ENVIRONMENT) 40 | 41 | def openai_call( 42 | prompt: str, 43 | temperature: float = 0.5, 44 | max_tokens: int = 100, 45 | ): 46 | model = OPENAI_API_MODEL 47 | while True: 48 | try: 49 | if model.startswith("llama"): 50 | # Spawn a subprocess to run llama.cpp 51 | cmd = ["llama/main", "-p", prompt] 52 | result = subprocess.run(cmd, shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, text=True) 53 | return result.stdout.strip() 54 | elif not model.startswith("gpt-"): 55 | # Use completion API 56 | response = openai.Completion.create( 57 | engine=model, 58 | prompt=prompt, 59 | temperature=temperature, 60 | max_tokens=max_tokens, 61 | top_p=1, 62 | frequency_penalty=0, 63 | presence_penalty=0, 64 | ) 65 | return response.choices[0].text.strip() 66 | else: 67 | # Use chat completion API 68 | messages = [{"role": "system", "content": prompt}] 69 | response = openai.ChatCompletion.create( 70 | model=model, 71 | messages=messages, 72 | temperature=temperature, 73 | max_tokens=max_tokens, 74 | n=1, 75 | stop=None, 76 | ) 77 | return response.choices[0].message.content.strip() 78 | except openai.error.RateLimitError: 79 | print( 80 | "The OpenAI API rate limit has been exceeded. Waiting 10 seconds and trying again." 81 | ) 82 | time.sleep(10) # Wait 10 seconds and try again 83 | else: 84 | break 85 | 86 | def get_ada_embedding(text): 87 | """Get the ada embedding of the given text.""" 88 | text = text.replace("\n", " ") 89 | return openai.Embedding.create(input=[text], model="text-embedding-ada-002")[ 90 | "data" 91 | ][0]["embedding"] 92 | 93 | def memory_as_pinecone(table_name): 94 | """Create a Pinecone index with the given table_name.""" 95 | dimension = 1536 96 | metric = "cosine" 97 | pod_type = "p1" 98 | if table_name not in pinecone.list_indexes(): 99 | pinecone.create_index( 100 | table_name, dimension=dimension, metric=metric, pod_type=pod_type 101 | ) 102 | index = pinecone.Index(table_name) 103 | return index -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AAGPT 2 | 3 | Auto-Agent-GPT (a.k.a AAGPT) is another experimental open-source application showcasing the capabilities of large language models. 4 | 5 | Support: general tasks, overcooked game 6 | 7 | Language: [[English]](README.md) [[中文]](docs/README-cn.md) 8 | 9 |
10 | 11 |
12 | 13 | ## Features 14 | - Memory support 15 | - GPT as memory 16 | - Vector database as memory (requires a PINECONE API key) 17 | - Lifespan limit for an agent (may save money) 18 | - Support for playing the overcooked game 19 | 20 | ## Installation 21 | To install AAGPT, follow these steps: 22 | 23 | 1. Clone the AAGPT repository from GitHub and navigate to the downloaded folder. 24 | 25 | ```bash: 26 | git clone git@github.com:hyintell/AAGPT.git 27 | cd AAGPT 28 | ``` 29 | 2. Use the following command in your terminal with pip: 30 | 31 | ```bash 32 | pip install -r requirements.txt 33 | ``` 34 | ## Quickplay 35 | Just two steps, you can start using AAGPT's natural language processing abilities with your OpenAI API key. 36 | 37 | 1. Open the `setup/game.yaml` file and enter your OpenAI API key in the `openai_api_key` field. 38 | 39 | 2. Navigate to the AAGPT folder and run the following command: 40 | 41 | ```bash 42 | python aagpt.py 43 | ``` 44 | ## Setup 45 | After installing AAGPT, you will need to set up related APIs to use the application. You can do this by following these steps: 46 | 1. Navigate to the setup folder in the AAGPT directory: 47 | ```bash 48 | cd setup 49 | ``` 50 | 2. In the `setup` folder, there are two game settings, `game.yaml` which using the Chatgpt as momery store and `game2.yaml` which using the Pinecone as momery store. You can choose one of them to set up the API. 51 | 3. In the `game.yaml` file, you will use GPT as memory store, so please fill in the following information: 52 | * `openai_api_key`: Your OpenAI API key. If you don't have one, you can create a free account and get an API key from the OpenAI website. 53 | * `openai_model`: The OpenAI ChatGPT model to use. Choose from "gpt-3.5-turbo", "gpt4", or "text-davinci-003". 54 | * `env_openai_api_key`: OpenAI ChatGPT Key for env, you can keep same as the common. 55 | * `agent_openai_api_key`: OpenAI ChatGPT Key for agents, you can keep same as the common. 56 | * `goal`: The main objective of the AI agent. 57 | * `init_task`: The initial tasks to be appended to the task list. 58 | * `agent_life`: The life-time of the agents, in default, we set it to 256. 59 | 60 | Note: Optionally, you can use `game2.yaml` which using Pinecone as memory store. In addition to the above settings, you will need to fill in the following information: 61 | * `agent_pinecone_api_key`: The form will be a list `[Your Pinecone API , Your Pinecone Region]`, the first is pinecone API, and second will be the region of your index, you can get it from the Pinecone website. 62 | * `agent_pinecone_index`: The index name of the Pinecone index to use. In default, we use `aagpt_agent_index`. 63 | 64 | ## Playing Overcooked 65 |
66 | 67 |
68 | 69 | 1. Install [opencooking](https://github.com/hyintell/opencooking) envs. 70 | 71 | 2. Let's play 72 | 73 | ```bash 74 | python aagpt-overcooked.py --render 75 | ``` 76 | 77 | ## Usage 78 | After setting the correct APIs, you can test AAGPT by executing the `aagpt.py` file in your terminal: 79 | 80 | ```bash 81 | python aagpt.py 82 | ``` 83 | 84 | Once AAGPT is running, you can start interacting with it by typing in prompts and observing its responses. 85 | 86 | If you want to change the setup or memory setting, you can use the following command: 87 | 88 | ```bash 89 | python aagpt.py --world_root setup/game2.yaml 90 | ``` 91 | 92 | ## Todo 93 | - [ ] UI 94 | - [ ] More memory support 95 | - [ ] Multi-agent support 96 | - [ ] More LLMs support 97 | 98 | ## Acknowledgement 99 | 100 | We are deeply grateful for the contributions made by open-source projects: [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) and [BabyAGI](https://github.com/yoheinakajima/babyagi). 101 | 102 | 103 | ## REFERENCES 104 | - Auto-GPT 105 | - BabyAGI 106 | - gym-cooking 107 | - OvercookedGPT 108 | - overcooked_ai 109 | -------------------------------------------------------------------------------- /overcooked/env.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List 2 | import copy 3 | import numpy as np 4 | from overcooked.utils import * 5 | from opencooking.utils.world import World 6 | from opencooking.utils.core import * 7 | from opencooking.misc.game.gameimage import GameImage 8 | from opencooking.envs.overcooked_environment import OvercookedEnvironment 9 | from collections import namedtuple 10 | 11 | 12 | CollisionRepr = namedtuple("CollisionRepr", "time agent_names agent_locations") 13 | 14 | OPEN_DIVIDER_SALAD = [[1, 1, 1, 1, 1, 1, 1], 15 | [1, 0, 0, 0, 0, 0, 1], 16 | [1, 0, 0, 0, 0, 0, 1], 17 | [1, 0, 0, 0, 0, 0, 1], 18 | [1, 0, 0, 0, 0, 0, 1], 19 | [1, 0, 0, 0, 0, 0, 1], 20 | [1, 1, 1, 1, 1, 1, 1]] 21 | OPEN_DIVIDER_SALAD = np.transpose(OPEN_DIVIDER_SALAD) 22 | 23 | OPEN_DIVIDER_SALAD_L = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 24 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 25 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 26 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 27 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 28 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 29 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 30 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 31 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 32 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 33 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] 34 | OPEN_DIVIDER_SALAD_L = np.transpose(OPEN_DIVIDER_SALAD_L) 35 | 36 | PARTIAL_DEVIDER_SALAD =[[1, 1, 1, 1, 1, 1, 1], 37 | [1, 0, 0, 1, 0, 0, 1], 38 | [1, 0, 0, 1, 0, 0, 1], 39 | [1, 0, 0, 1, 0, 0, 1], 40 | [1, 0, 0, 1, 0, 0, 1], 41 | [1, 0, 0, 0, 0, 0, 1], 42 | [1, 1, 1, 1, 1, 1, 1]] 43 | PARTIAL_DEVIDER_SALAD = np.transpose(PARTIAL_DEVIDER_SALAD) 44 | 45 | PARTIAL_DEVIDER_SALAD_L = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 46 | [1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], 47 | [1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], 48 | [1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], 49 | [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], 50 | [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], 51 | [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], 52 | [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1], 53 | [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1], 54 | [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1], 55 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] 56 | PARTIAL_DEVIDER_SALAD_L = np.transpose(PARTIAL_DEVIDER_SALAD_L) 57 | 58 | FULL_DIVIDER_SALAD = [[1, 1, 1, 1, 1, 1, 1], 59 | [1, 0, 0, 1, 0, 0, 1], 60 | [1, 0, 0, 1, 0, 0, 1], 61 | [1, 0, 0, 1, 0, 0, 1], 62 | [1, 0, 0, 1, 0, 0, 1], 63 | [1, 0, 0, 1, 0, 0, 1], 64 | [1, 1, 1, 1, 1, 1, 1]] 65 | FULL_DIVIDER_SALAD = np.transpose(FULL_DIVIDER_SALAD) 66 | 67 | # x, y 68 | ITEM_LOCATIONS = { 69 | "tomato": (5, 0), 70 | "lettuce": (6, 1), 71 | "cutboard0": (0, 1), 72 | "cutboard1": (0, 2), 73 | "plate0": (5, 6), 74 | "plate1": (6, 5), 75 | 76 | "counter0": (3, 1), 77 | "counter1": (3, 2), 78 | "counter2": (3, 3), 79 | "counter3": (3, 4), 80 | 81 | "star": (0, 3) 82 | } 83 | 84 | ITEM_LOCATIONS_L = { 85 | "tomato": (12, 0), 86 | "lettuce": (13, 1), 87 | "cutboard0": (0, 1), 88 | "cutboard1": (0, 2), 89 | "plate0": (12, 10), 90 | "plate1": (13, 9), 91 | 92 | "counter0": (6, 6), 93 | "counter1": (6, 7), 94 | "counter2": (6, 8), 95 | "counter3": (6, 9), 96 | 97 | "star": (0, 9) 98 | } 99 | 100 | MOVABLES = ["tomato", "lettuce", "plate0", "plate1"] 101 | 102 | 103 | def identify_items_at(location: Tuple[int, int], item_locations: dict) -> List[str]: 104 | result = [] 105 | for item, loc in item_locations.items(): 106 | if (loc[0] == location[0]) and (loc[1] == location[1]): 107 | result.append(item) 108 | return result 109 | 110 | 111 | def get_dst_tuple(item: str, level: list, item_locations: dict) -> Tuple[Tuple[int, int], list]: 112 | destination: Tuple[int, int] = item_locations[item] 113 | level: list = copy.deepcopy(level) 114 | level[destination[0]][destination[1]] = 0 115 | return destination, level 116 | 117 | 118 | class GPTWorld(World): 119 | NAV_ACTIONS = [(0, 1), (0, -1), (-1, 0), (1, 0)] 120 | def __init__(self, arglist): 121 | super().__init__(arglist) 122 | 123 | def get_gridsquare_list_at(self, location) -> list: 124 | gss = list(filter(lambda o: o.location == location, self.get_object_list())) 125 | assert len(gss) > 0, "{} gridsquares at {}: {}".format(len(gss), location, gss) 126 | return gss 127 | 128 | 129 | class OvercookedEnvGPT(OvercookedEnvironment): 130 | def __init__(self, num_agents, level, arglist): 131 | super().__init__(num_agents, level, arglist) 132 | 133 | def reset(self): 134 | self.world = GPTWorld(arglist=self.arglist) 135 | self.recipes = [] 136 | self.sim_agents = [] 137 | self.agent_actions = {} 138 | self.t = 0 139 | # For visualizing episode. 140 | self.rep = [] 141 | # For tracking data during an episode. 142 | self.collisions = [] 143 | self.termination_info = "" 144 | self.successful = False 145 | # Load world & distances. 146 | self.load_level( 147 | level=self.level, 148 | num_agents=self.num_agents) 149 | self.all_subtasks = self.run_recipes() 150 | self.world.make_loc_to_gridsquare() 151 | self.world.make_reachability_graph() 152 | self.cache_distances() 153 | self.obs_tm1 = copy.copy(self) 154 | 155 | if self.arglist.record or self.arglist.with_image_obs: 156 | self.game = GameImage( 157 | filename=self.filename, 158 | world=self.world, 159 | sim_agents=self.sim_agents, 160 | record=self.arglist.record) 161 | self.game.on_init() 162 | if self.arglist.record: 163 | self.game.save_image_obs(self.t) 164 | return copy.copy(self) 165 | 166 | def step(self, action_dict): 167 | # Track internal environment info. 168 | self.t += 1 169 | print("===============================") 170 | print("[environment.step] @ TIMESTEP {}".format(self.t)) 171 | print("===============================") 172 | # Get actions. 173 | for sim_agent in self.sim_agents: 174 | sim_agent.action = action_dict[sim_agent.name] 175 | # Check collisions. 176 | self.check_collisions() 177 | self.obs_tm1 = copy.copy(self) 178 | # Execute. 179 | agents_states = self.execute_navigation() 180 | for agent_ in self.sim_agents: 181 | agents_states[agent_.name]['loc'] = agent_.location 182 | # Visualize. 183 | self.display() 184 | self.print_agents() 185 | if self.arglist.record: 186 | self.game.save_image_obs(self.t) 187 | # Get a plan-representation observation. 188 | new_obs = copy.copy(self) 189 | # Get an image observation 190 | image_obs = self.game.get_image_obs() 191 | 192 | done = self.done() 193 | reward = self.reward() 194 | info = {"t": self.t, "obs": new_obs, 195 | "image_obs": image_obs, 196 | "done": done, "termination_info": self.termination_info, "agents_states": agents_states} 197 | return new_obs, reward, done, info 198 | 199 | def execute_navigation(self): 200 | agents_states = {} 201 | for agent in self.sim_agents: 202 | action_str, action_loc = interact(agent=agent, world=self.world) 203 | agents_states[agent.name] = {'action_str': action_str, 'action_loc': action_loc} 204 | self.agent_actions[agent.name] = agent.action 205 | return agents_states 206 | -------------------------------------------------------------------------------- /agent.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from typing import Dict, List 3 | from utils import memory_as_pinecone, get_ada_embedding, openai_call 4 | 5 | 6 | class AgentGPTMEM: 7 | def __init__(self, config): 8 | self.agent_config = config["agent"] 9 | self.task_list = deque([]) 10 | self.life = self.agent_config["agent_life"] 11 | self.memory = self.build_memory() 12 | first_task = {"task_id": 1, "task_name": self.agent_config["init_task"]} 13 | self.add_task(first_task) 14 | self.task_id_counter = 1 15 | self.task_turnon = None 16 | self.goal = self.agent_config["goal"] 17 | 18 | def build_memory(self): 19 | # Initialize memory as an empty list 20 | history = [] 21 | return history 22 | 23 | def act(self): 24 | # Get the next task from the task list 25 | self.task_turnon = self.task_list.popleft() 26 | return self.task_turnon 27 | 28 | def add_task(self, task: Dict): 29 | # Add a task to the task list 30 | self.task_list.append(task) 31 | 32 | def receive(self, result): 33 | enriched_result = { 34 | "data": result 35 | } # This is where you should enrich the result if needed 36 | task = self.task_turnon 37 | 38 | # Add the task and its result to the memory 39 | self.memory.append({"task": task["task_name"], "result": result}) 40 | 41 | # Create new tasks based on the result 42 | new_tasks = self.task_creation( 43 | self.goal, 44 | enriched_result, 45 | task["task_name"], 46 | [t["task_name"] for t in self.task_list], 47 | ) 48 | 49 | # Add the new tasks to the task list 50 | for new_task in new_tasks: 51 | self.task_id_counter += 1 52 | new_task.update({"task_id": self.task_id_counter}) 53 | self.add_task(new_task) 54 | self.this_task_id = int(task["task_id"]) 55 | 56 | # Prioritize the tasks in the task list 57 | self.prioritization(self.this_task_id) 58 | 59 | def prioritization(self, this_task_id: int): 60 | task_names = [t["task_name"] for t in self.task_list] 61 | next_task_id = int(this_task_id) + 1 62 | prompt = f""" 63 | You are a task prioritization AI tasked with cleaning the formatting of and reprioritizing the following tasks: {task_names}. 64 | Consider the ultimate objective of your team:{self.goal}. 65 | Do not remove any tasks. Return the result as a numbered list, like: 66 | #. First task 67 | #. Second task 68 | Start the task list with number {next_task_id}.""" 69 | response = openai_call(prompt) 70 | new_tasks = response.split("\n") if "\n" in response else [response] 71 | self.task_list = deque() 72 | for task_string in new_tasks: 73 | task_parts = task_string.strip().split(".", 1) 74 | if len(task_parts) == 2: 75 | task_id = task_parts[0].strip() 76 | task_name = task_parts[1].strip() 77 | self.task_list.append({"task_id": task_id, "task_name": task_name}) 78 | 79 | def task_creation( 80 | self, objective: str, result: Dict, task_description: str, task_list: List[str] 81 | ): 82 | prompt = f""" 83 | You are a task creation AI that uses the result of an execution agent to create new tasks with the following objective: {objective}, 84 | The last completed task has the result: {result}. 85 | This result was based on this task description: {task_description}. These are incomplete tasks: {', '.join(task_list)}. 86 | Based on the result, create new tasks to be completed by the AI system that do not overlap with incomplete tasks. 87 | Return the tasks as an array.""" 88 | response = openai_call(prompt) 89 | new_tasks = response.split("\n") if "\n" in response else [response] 90 | return [{"task_name": task_name} for task_name in new_tasks] 91 | 92 | def context_search(self, n: int, lookback = 10): 93 | completed_tasks = [ item["result"] + "\n" for item in self.memory[-lookback:]] 94 | prompt = f""" 95 | You are a task creation AI that uses the result of an execution agent to search finished tasks with the following objective: {self.goal}, 96 | The rencent completed tasks are: {completed_tasks}. 97 | Based on the completed tasks, find tasks that are releveant to the objective. 98 | Return the tasks as an array.""" 99 | response = openai_call(prompt) 100 | return response 101 | 102 | 103 | class AgentPCMEM: 104 | def __init__(self, config): 105 | self.agent_config = config["agent"] 106 | self.pinecone_index = self.agent_config["agent_pinecone_index"] 107 | self.task_list = deque([]) 108 | self.memory = self.build_memory() 109 | self.life = self.agent_config["agent_life"] 110 | first_task = {"task_id": 1, "task_name": self.agent_config["init_task"]} 111 | self.add_task(first_task) 112 | self.task_id_counter = 1 113 | self.task_turnon = None 114 | self.goal = self.agent_config["goal"] 115 | 116 | def build_memory(self): 117 | """Create Pinecone index and return it as memory.""" 118 | index = memory_as_pinecone(self.pinecone_index) 119 | return index 120 | 121 | def act(self): 122 | self.task_turnon = self.task_list.popleft() 123 | return self.task_turnon 124 | 125 | def add_task(self, task: Dict): 126 | self.task_list.append(task) 127 | 128 | def receive(self, result): 129 | 130 | enriched_result = {"data": result} 131 | task = self.task_turnon 132 | result_id = f"result_{task['task_id']}" 133 | vector = get_ada_embedding(enriched_result["data"]) 134 | self.memory.upsert( 135 | [(result_id, vector, {"task": task["task_name"], "result": result})], 136 | namespace=self.goal 137 | ) 138 | 139 | new_tasks = self.task_creation( 140 | self.goal, 141 | enriched_result, 142 | task["task_name"], 143 | [t["task_name"] for t in self.task_list], 144 | ) 145 | 146 | for new_task in new_tasks: 147 | self.task_id_counter += 1 148 | new_task.update({"task_id": self.task_id_counter}) 149 | self.add_task(new_task) 150 | self.this_task_id = int(task["task_id"]) 151 | 152 | self.prioritization(self.this_task_id) 153 | 154 | def prioritization(self, this_task_id: int): 155 | task_names = [t["task_name"] for t in self.task_list] 156 | next_task_id = int(this_task_id) + 1 157 | prompt = f""" 158 | You are a task prioritization AI tasked with cleaning the formatting of and reprioritizing the following tasks: {task_names}. 159 | Consider the ultimate objective of your team:{self.goal}. 160 | Do not remove any tasks. Return the result as a numbered list, like: 161 | #. First task 162 | #. Second task 163 | Start the task list with number {next_task_id}.""" 164 | response = openai_call(prompt) 165 | new_tasks = response.split("\n") if "\n" in response else [response] 166 | self.task_list = deque() 167 | for task_string in new_tasks: 168 | task_parts = task_string.strip().split(".", 1) 169 | if len(task_parts) == 2: 170 | task_id = task_parts[0].strip() 171 | task_name = task_parts[1].strip() 172 | self.task_list.append({"task_id": task_id, "task_name": task_name}) 173 | 174 | def task_creation( 175 | self, objective: str, result: Dict, task_description: str, task_list: List[str] 176 | ): 177 | prompt = f""" 178 | You are a task creation AI that uses the result of an execution agent to create new tasks with the following objective: {objective}, 179 | The last completed task has the result: {result}. 180 | This result was based on this task description: {task_description}. These are incomplete tasks: {', '.join(task_list)}. 181 | Based on the result, create new tasks to be completed by the AI system that do not overlap with incomplete tasks. 182 | Return the tasks as an array.""" 183 | response = openai_call(prompt) 184 | new_tasks = response.split("\n") if "\n" in response else [response] 185 | return [{"task_name": task_name} for task_name in new_tasks] 186 | 187 | 188 | def context_search(self, n: int): 189 | query_embedding = get_ada_embedding(self.goal) 190 | results = self.memory.query(query_embedding, top_k=n, include_metadata=True, namespace=self.goal) 191 | sorted_results = sorted(results.matches, key=lambda x: x.score, reverse=True) 192 | return [(str(item.metadata["task"])) for item in sorted_results] -------------------------------------------------------------------------------- /overcooked/agent.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | import os 3 | from overcooked.utils import * 4 | from overcooked.env import * 5 | from opencooking.utils.utils import * 6 | import openai 7 | 8 | 9 | # Reference: 10 | # https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb 11 | class ChatBot: 12 | def __init__(self, num_agents: int, config: dict, arglist): 13 | if not("openai_api_key" in os.environ): 14 | openai.api_key = config["common"]["openai_api_key"] 15 | self.model: str = config["common"]["openai_model"] 16 | self.messages: list = [] 17 | 18 | instruction, example = None, None 19 | self.num_agents: int = num_agents 20 | if self.num_agents == 1: 21 | with open("utils/chatgpt/single_agent_instruction.txt", "r") as f: 22 | instruction = f.read() 23 | with open("utils/chatgpt/single_agent_example.txt", "r") as f: 24 | example = f.read() 25 | elif self.num_agents == 2: 26 | instruction = ocma_instruction 27 | example = ocma_example 28 | else: 29 | assert False, f"num_agents must be 1 or 2: {self.num_agents}" 30 | 31 | self.messages.append({"role": "system", "content": "You are a Python programmer. Help me write code in Python."}) 32 | self.messages.append({"role": "user", "content": instruction}) 33 | 34 | # one-shot learning 35 | self.messages.append({ 36 | "role": "system", 37 | "name": "example_user", 38 | "content": "Make a lettuce salad." 39 | }) 40 | self.messages.append({"role": "system", "name": "example_assistant", "content": example}) 41 | 42 | def __call__(self, message): 43 | self.messages.append({"role": "user", "content": message}) 44 | result: str = self.execute() 45 | print(result) 46 | self.messages.append({"role": "assistant", "content": result}) 47 | return result 48 | 49 | def execute(self) -> str: 50 | try: 51 | completion = openai.ChatCompletion.create(model=self.model, messages=self.messages) 52 | #print(completion.usage) # number of tokens consumed 53 | return completion.choices[0].message.content 54 | except Exception as e: 55 | print(e) 56 | return colors.RED + f"ERROR: {e}" + colors.ENDC 57 | 58 | 59 | class GPTWorld(World): 60 | NAV_ACTIONS = [(0, 1), (0, -1), (-1, 0), (1, 0)] 61 | def __init__(self, arglist): 62 | super().__init__(arglist) 63 | 64 | def get_gridsquare_list_at(self, location) -> list: 65 | gss = list(filter(lambda o: o.location == location, self.get_object_list())) 66 | assert len(gss) > 0, "{} gridsquares at {}: {}".format(len(gss), location, gss) 67 | return gss 68 | 69 | 70 | class GPTAgent: 71 | def __init__(self, id: int, level: str, arglist): 72 | assert 0 <= id <= 4 73 | self.id = id 74 | self.location = None 75 | self.on_hand = None 76 | self.level = None 77 | self.item_locations = ITEM_LOCATIONS 78 | self.history = [] 79 | self.prev_state = None 80 | global g_max_steps 81 | if level == "open-divider_salad": 82 | self.level = OPEN_DIVIDER_SALAD 83 | elif level == "open-divider_salad_large": 84 | self.level = OPEN_DIVIDER_SALAD_L 85 | self.item_locations = ITEM_LOCATIONS_L 86 | g_max_steps = 200 87 | elif level == "partial-divider_salad": 88 | self.level = PARTIAL_DEVIDER_SALAD 89 | elif level == "partial-divider_salad_large": 90 | self.level = PARTIAL_DEVIDER_SALAD_L 91 | self.item_locations = ITEM_LOCATIONS_L 92 | g_max_steps = 200 93 | elif level == "full-divider_salad": 94 | self.level = FULL_DIVIDER_SALAD 95 | else: 96 | assert False, f"unknown level: {arglist.level}" 97 | 98 | def set_state(self, location: Tuple[int, int], action_str: str, action_loc: Tuple[int, int]): 99 | """ set the latest game state 100 | Args: 101 | location (Tuple[int, int]): agent's current location 102 | action_str (str): action taken by the agent 103 | action_loc (Tuple[int, int]): location where the action was taken 104 | """ 105 | self.location = location 106 | if action_str is None: 107 | return 108 | if self.prev_state is not None: 109 | # discard duplicate state 110 | if (self.prev_state[0] == location) and (self.prev_state[1] == action_str) and (self.prev_state[2] == action_loc): 111 | return 112 | description = action_str 113 | items: List[str] = identify_items_at(action_loc, self.item_locations) 114 | if len(items) > 0: 115 | # remove duplicated items 116 | if ("sliced" in description) or ("picked" in description): 117 | if "tomato" in description: 118 | items.remove("tomato") 119 | if "lettuce" in description: 120 | items.remove("lettuce") 121 | if ("picked" in description) and (len(items) > 0): 122 | description += " from" 123 | # change description for merged plate 124 | elif ("merged plate" in description) and (self.on_hand is not None): 125 | description = "put sliced " + ", ".join(self.on_hand) + " onto" 126 | description += ' ' + ", ".join(items) 127 | print(colors.GREEN + f"agent{self.id}.set_state(): " + description + colors.ENDC) 128 | self.history.append(description) 129 | if "picked" in description: 130 | # identify what item was picked up 131 | for item in self.item_locations.keys(): 132 | if (item in description) and (item in MOVABLES): 133 | if self.on_hand is None: 134 | self.on_hand = [item] 135 | else: 136 | self.on_hand.append(item) 137 | elif ("put" in description) or ("merged" in description): 138 | if self.on_hand is not None: 139 | # update the location of the item 140 | for item in MOVABLES: 141 | for obj in self.on_hand: 142 | if item in obj: 143 | self.item_locations[item] = action_loc 144 | self.on_hand = None 145 | if self.on_hand is not None: 146 | print(colors.YELLOW + f"agent{self.id}.on_hand = {self.on_hand}" + colors.ENDC) 147 | self.prev_state = (location, action_str, action_loc) 148 | 149 | def reset_state(self, reset_on_hand: bool=False): 150 | """ reset the game state of the agent 151 | Args: 152 | reset_on_hand (bool, optional): reset the on_hand variable. Defaults to False. 153 | """ 154 | self.location = None 155 | self.action_str = None 156 | self.action_loc = None 157 | if reset_on_hand: 158 | self.on_hand = None 159 | 160 | def move_to(self, destination: Tuple[int, int]) -> bool: 161 | """ move to the specified destination 162 | Args: 163 | destination (Tuple[int, int]): 2D coordinate of the destination 164 | Returns: 165 | bool: True when the agent has reached the destination 166 | """ 167 | act = (0, 0) 168 | if not isinstance(destination, tuple): 169 | print(colors.RED + f"ERROR: destination is not a tuple: {destination}" + colors.ENDC) 170 | return False, act 171 | if self.__has_reached(destination): 172 | print(colors.YELLOW + f"agent{self.id}.move_to(): reached destination" + colors.ENDC) 173 | return True, act 174 | dx = destination[0] - self.location[0] 175 | dy = destination[1] - self.location[1] 176 | print(colors.YELLOW + f"agent{self.id}.move_to(): source={self.location}, destination={destination}, (dx, dy) = ({dx}, {dy})" + colors.ENDC) 177 | global g_keyboard 178 | if dx < 0: 179 | """ 180 | g_keyboard.press(Key.left) 181 | g_keyboard.release(Key.left) 182 | """ 183 | return False, (-1, 0) 184 | elif dx > 0: 185 | """ 186 | g_keyboard.press(Key.right) 187 | g_keyboard.release(Key.right) 188 | """ 189 | return False, (1, 0) 190 | if dy < 0: 191 | """ 192 | g_keyboard.press(Key.up) 193 | g_keyboard.release(Key.up) 194 | """ 195 | return False, (0, -1) 196 | elif dy > 0: 197 | """ 198 | g_keyboard.press(Key.down) 199 | g_keyboard.release(Key.down) 200 | """ 201 | return False, (0, 1) 202 | 203 | def fetch(self, item: str) -> bool: 204 | """ move to the item's location and pick it up 205 | Args: 206 | item (str): item to be picked up 207 | Returns: 208 | bool: success or failure 209 | """ 210 | act = (0, 0) 211 | if self.on_hand is not None: 212 | for obj in self.on_hand: 213 | if item in obj: 214 | return True, act # item is already in hand 215 | for key in self.item_locations.keys(): 216 | if item == key: 217 | destination, level = get_dst_tuple(item, self.level, self.item_locations) 218 | path: List[Tuple[int, int]] = find_path(self.location, destination, level) 219 | print(colors.YELLOW + f"agent{self.id}.fetch(): path={path}" + colors.ENDC) 220 | _, act = self.move_to(path[1]) 221 | break 222 | return False, act 223 | 224 | def put_onto(self, item) -> bool: 225 | """ place the object in hand onto the specified item 226 | Args: 227 | item (str or Tuple[int, int]): where to put the object 228 | Returns: 229 | bool: True if the task is closed 230 | """ 231 | act = (0, 0) 232 | if self.on_hand is None: 233 | #print(colors.RED + f"GPTAgent.put_onto(): nothing in hand to put" + colors.ENDC) 234 | return True, act 235 | destination, level = None, None 236 | if isinstance(item, str): 237 | if not(item in self.item_locations.keys()): 238 | print(colors.RED + f"agent{self.id}.put_onto(): invalid item: {item}" + colors.ENDC) 239 | return True, act 240 | destination, level = get_dst_tuple(item, self.level, self.item_locations) 241 | elif isinstance(item, tuple): 242 | pass #TODO: also accept 2D coordinate 243 | else: 244 | assert False, f"item must be str or Tuple[int, int]: {type(item)}" 245 | path: List[Tuple[int, int]] = find_path(self.location, destination, level) 246 | print(colors.YELLOW + f"agent{self.id}.put_onto(): path={path}" + colors.ENDC) 247 | _, act = self.move_to(path[1]) 248 | return False, act 249 | 250 | def slice_on(self, item: str) -> bool: 251 | """ slice food at the specified item's location 252 | Args: 253 | item (str): the name of the item to chop on (must be a cutboard) 254 | Returns: 255 | bool: True if the task is closed 256 | """ 257 | act = (0, 0) 258 | if not(item in self.item_locations.keys()): 259 | print(colors.RED + f"agent{self.id}.slice_on(): invalid item: {item}" + colors.ENDC) 260 | return True, act 261 | if not("cutboard" in item): 262 | print(colors.RED + f"agent{self.id}.slice_on(): cannot slice on {item}" + colors.ENDC) 263 | return True, act 264 | destination: Tuple[int, int] = self.item_locations[item] 265 | for description in self.history[::-1]: 266 | if ("put" in description) and (item in description): 267 | _, act = self.move_to(destination) 268 | break 269 | elif "sliced" in description: 270 | return True, act 271 | return False, act 272 | 273 | def deliver(self, dummy=None) -> bool: 274 | """ deliver the food to the goal destination (i.e., "star") 275 | Args: 276 | dummy (_type_, optional): ignored 277 | Returns: 278 | bool: True if the task is closed 279 | """ 280 | act = (0, 0) 281 | destination = list(self.item_locations["star"]) 282 | destination[0] += 1 283 | #self.move_to(tuple(destination)): 284 | flag, act = self.move_to(tuple(destination)) 285 | if flag: 286 | return flag, (-1, 0) 287 | return flag, act 288 | 289 | def __has_reached(self, destination) -> bool: 290 | return (self.location[0] == destination[0]) and (self.location[1] == destination[1]) -------------------------------------------------------------------------------- /overcooked/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Tuple, List 3 | import random 4 | import re 5 | from opencooking.utils.core import * 6 | from opencooking.utils.utils import * 7 | 8 | 9 | class colors: 10 | RED = "\033[31m" 11 | ENDC = "\033[m" 12 | GREEN = "\033[32m" 13 | YELLOW = "\033[33m" 14 | BLUE = "\033[34m" 15 | 16 | 17 | def fix_seed(seed): 18 | np.random.seed(seed) 19 | random.seed(seed) 20 | 21 | 22 | def __extract_object_names(s: str) -> str: 23 | result = [] 24 | if "Tomato" in s: 25 | result.append("tomato") 26 | if "Lettuce" in s: 27 | result.append("lettuce") 28 | if "Plate" in s: 29 | result.append("plate") 30 | return ", ".join(result) 31 | 32 | 33 | def interact(agent, world) -> Tuple[str, Tuple[int, int]]: 34 | """Carries out interaction for this agent taking this action in this world. 35 | 36 | The action that needs to be executed is stored in `agent.action`. 37 | """ 38 | 39 | action_str = None 40 | action_loc = None 41 | 42 | # agent does nothing (i.e. no arrow key) 43 | if agent.action == (0, 0): 44 | return action_str, action_loc 45 | 46 | action_loc = world.inbounds(tuple(np.asarray(agent.location) + np.asarray(agent.action))) 47 | gs = world.get_gridsquare_at(action_loc) 48 | 49 | # if floor in front --> move to that square 50 | if isinstance(gs, Floor): #and gs.holding is None: 51 | action_str = "moved to" 52 | agent.move_to(gs.location) 53 | 54 | # if holding something 55 | elif agent.holding is not None: 56 | # not None only when agent puts foods on cutboard or plate, or delivers 57 | 58 | # if delivery in front --> deliver 59 | if isinstance(gs, Delivery): 60 | obj = agent.holding 61 | #print(f"holding && delivering: obj.contents = {obj.contents}") 62 | 63 | if obj.is_deliverable(): 64 | action_str = f"delivered {__extract_object_names(str(obj.contents))} at" 65 | gs.acquire(obj) 66 | agent.release() 67 | print('\nDelivered {}!'.format(obj.full_name)) 68 | 69 | # if occupied gridsquare in front --> try merging 70 | elif world.is_occupied(gs.location): 71 | # Get object on gridsquare/counter 72 | obj = world.get_object_at(gs.location, None, find_held_objects = False) 73 | #print(f"holding && occupied: obj.contents = {obj.contents}") 74 | 75 | if mergeable(agent.holding, obj): 76 | action_str = f"merged {__extract_object_names(str(obj.contents))} with" 77 | world.remove(obj) 78 | o = gs.release() # agent is holding object 79 | world.remove(agent.holding) 80 | agent.acquire(obj) 81 | world.insert(agent.holding) 82 | # if playable version, merge onto counter first 83 | if world.arglist.gpt: 84 | # --gpt 85 | gs.acquire(agent.holding) 86 | agent.release() 87 | 88 | # if holding something, empty gridsquare in front --> chop or drop 89 | elif not world.is_occupied(gs.location): 90 | obj = agent.holding 91 | #print(f"holding && not(occupied): obj.contents = {obj.contents}") 92 | 93 | if isinstance(gs, Cutboard) and obj.needs_chopped() and not world.arglist.gpt: 94 | # normally chop, but if in playable game mode then put down first 95 | obj.chop() 96 | else: 97 | # --gpt 98 | action_str = f"put {__extract_object_names(str(obj.contents))} onto" 99 | gs.acquire(obj) # obj is put onto gridsquare 100 | agent.release() 101 | assert world.get_object_at(gs.location, obj, find_held_objects =\ 102 | False).is_held == False, "Verifying put down works" 103 | 104 | # if not holding anything 105 | elif agent.holding is None: 106 | # not empty in front --> pick up 107 | if world.is_occupied(gs.location) and not isinstance(gs, Delivery): 108 | obj = world.get_object_at(gs.location, None, find_held_objects = False) 109 | #print(f"not(holding) && occupied: obj.contents = {obj.contents}") 110 | 111 | # if in playable game mode, then chop raw items on cutting board 112 | if isinstance(gs, Cutboard) and obj.needs_chopped() and world.arglist.gpt: 113 | # --gpt 114 | action_str = f"sliced {__extract_object_names(str(obj.contents))} on" 115 | obj.chop() 116 | else: 117 | action_str = f"picked up {__extract_object_names(str(obj.contents))}" 118 | gs.release() 119 | agent.acquire(obj) 120 | 121 | # if empty in front --> interact 122 | elif not world.is_occupied(gs.location): 123 | pass 124 | 125 | return action_str, action_loc 126 | 127 | 128 | def index_2d(data, search): 129 | for i, e in enumerate(data): 130 | try: 131 | return i, e.index(search) 132 | except ValueError: 133 | pass 134 | raise ValueError("{!r} is not in list".format(search)) 135 | 136 | 137 | def find_path(start: Tuple[int, int], end: Tuple[int, int], level: list, cost: int=1) -> List[Tuple[int, int]]: 138 | path = search(level, cost, start, end) 139 | #print(path, type(path)) 140 | print('\n'.join([''.join([colors.GREEN + "{:" ">3d}".format(item) + colors.ENDC if item >=0 else\ 141 | "{:" ">3d}".format(item) for item in row]) for row in np.transpose(path)])) 142 | result = [] 143 | i = 0 144 | while True: 145 | try: 146 | position = index_2d(path, i) 147 | except ValueError: 148 | break 149 | result.append(position) 150 | i += 1 151 | return result 152 | 153 | 154 | # Reference: 155 | # https://github.com/BaijayantaRoy/Medium-Article/blob/master/A_Star.ipynb 156 | class Node: 157 | """ 158 | A node class for A* Pathfinding 159 | parent is parent of the current Node 160 | position is current position of the Node in the maze 161 | g is cost from start to current Node 162 | h is heuristic based estimated cost for current Node to end Node 163 | f is total cost of present node i.e. : f = g + h 164 | """ 165 | 166 | def __init__(self, parent=None, position=None): 167 | self.parent = parent 168 | self.position = position 169 | 170 | self.g = 0 171 | self.h = 0 172 | self.f = 0 173 | def __eq__(self, other): 174 | return self.position == other.position 175 | 176 | 177 | #This function return the path of the search 178 | def return_path(current_node,maze): 179 | path = [] 180 | no_rows, no_columns = np.shape(maze) 181 | # here we create the initialized result maze with -1 in every position 182 | result = [[-1 for i in range(no_columns)] for j in range(no_rows)] 183 | current = current_node 184 | while current is not None: 185 | path.append(current.position) 186 | current = current.parent 187 | # Return reversed path as we need to show from start to end path 188 | path = path[::-1] 189 | start_value = 0 190 | # we update the path of start to end found by A-star serch with every step incremented by 1 191 | for i in range(len(path)): 192 | result[path[i][0]][path[i][1]] = start_value 193 | start_value += 1 194 | return result 195 | 196 | 197 | def search(maze, cost, start, end): 198 | """ 199 | Returns a list of tuples as a path from the given start to the given end in the given maze 200 | :param maze: 201 | :param cost 202 | :param start: 203 | :param end: 204 | :return: 205 | """ 206 | 207 | # Create start and end node with initized values for g, h and f 208 | start_node = Node(None, tuple(start)) 209 | start_node.g = start_node.h = start_node.f = 0 210 | end_node = Node(None, tuple(end)) 211 | end_node.g = end_node.h = end_node.f = 0 212 | 213 | # Initialize both yet_to_visit and visited list 214 | # in this list we will put all node that are yet_to_visit for exploration. 215 | # From here we will find the lowest cost node to expand next 216 | yet_to_visit_list = [] 217 | # in this list we will put all node those already explored so that we don't explore it again 218 | visited_list = [] 219 | 220 | # Add the start node 221 | yet_to_visit_list.append(start_node) 222 | 223 | # Adding a stop condition. This is to avoid any infinite loop and stop 224 | # execution after some reasonable number of steps 225 | outer_iterations = 0 226 | max_iterations = (len(maze) // 2) ** 10 227 | 228 | # what squares do we search . serarch movement is left-right-top-bottom 229 | #(4 movements) from every positon 230 | 231 | move = [[-1, 0 ], # go up 232 | [ 0, -1], # go left 233 | [ 1, 0 ], # go down 234 | [ 0, 1 ]] # go right 235 | 236 | """ 237 | 1) We first get the current node by comparing all f cost and selecting the lowest cost node for further expansion 238 | 2) Check max iteration reached or not . Set a message and stop execution 239 | 3) Remove the selected node from yet_to_visit list and add this node to visited list 240 | 4) Perofmr Goal test and return the path else perform below steps 241 | 5) For selected node find out all children (use move to find children) 242 | a) get the current postion for the selected node (this becomes parent node for the children) 243 | b) check if a valid position exist (boundary will make few nodes invalid) 244 | c) if any node is a wall then ignore that 245 | d) add to valid children node list for the selected parent 246 | 247 | For all the children node 248 | a) if child in visited list then ignore it and try next node 249 | b) calculate child node g, h and f values 250 | c) if child in yet_to_visit list then ignore it 251 | d) else move the child to yet_to_visit list 252 | """ 253 | #find maze has got how many rows and columns 254 | no_rows, no_columns = np.shape(maze) 255 | 256 | # Loop until you find the end 257 | 258 | while len(yet_to_visit_list) > 0: 259 | 260 | # Every time any node is referred from yet_to_visit list, counter of limit operation incremented 261 | outer_iterations += 1 262 | 263 | # Get the current node 264 | current_node = yet_to_visit_list[0] 265 | current_index = 0 266 | for index, item in enumerate(yet_to_visit_list): 267 | if item.f < current_node.f: 268 | current_node = item 269 | current_index = index 270 | 271 | # if we hit this point return the path such as it may be no solution or 272 | # computation cost is too high 273 | if outer_iterations > max_iterations: 274 | print ("giving up on pathfinding too many iterations") 275 | return return_path(current_node,maze) 276 | 277 | # Pop current node out off yet_to_visit list, add to visited list 278 | yet_to_visit_list.pop(current_index) 279 | visited_list.append(current_node) 280 | 281 | # test if goal is reached or not, if yes then return the path 282 | if current_node == end_node: 283 | return return_path(current_node,maze) 284 | 285 | # Generate children from all adjacent squares 286 | children = [] 287 | 288 | for new_position in move: 289 | 290 | # Get node position 291 | node_position = (current_node.position[0] + new_position[0], current_node.position[1] + new_position[1]) 292 | 293 | # Make sure within range (check if within maze boundary) 294 | if (node_position[0] > (no_rows - 1) or 295 | node_position[0] < 0 or 296 | node_position[1] > (no_columns -1) or 297 | node_position[1] < 0): 298 | continue 299 | 300 | # Make sure walkable terrain 301 | if maze[node_position[0]][node_position[1]] != 0: 302 | continue 303 | 304 | # Create new node 305 | new_node = Node(current_node, node_position) 306 | 307 | # Append 308 | children.append(new_node) 309 | 310 | # Loop through children 311 | for child in children: 312 | 313 | # Child is on the visited list (search entire visited list) 314 | if len([visited_child for visited_child in visited_list if visited_child == child]) > 0: 315 | continue 316 | 317 | # Create the f, g, and h values 318 | child.g = current_node.g + cost 319 | ## Heuristic costs calculated here, this is using eucledian distance 320 | child.h = (((child.position[0] - end_node.position[0]) ** 2) + 321 | ((child.position[1] - end_node.position[1]) ** 2)) 322 | 323 | child.f = child.g + child.h 324 | 325 | # Child is already in the yet_to_visit list and g cost is already lower 326 | if len([i for i in yet_to_visit_list if child == i and child.g > i.g]) > 0: 327 | continue 328 | 329 | # Add the child to the yet_to_visit list 330 | yet_to_visit_list.append(child) 331 | 332 | 333 | __code_block_regex = re.compile(r"```(.*?)```", re.DOTALL) 334 | 335 | def __extract_python_code(content: str) -> str: 336 | global __code_block_regex 337 | code_blocks: list = __code_block_regex.findall(content) 338 | if code_blocks: 339 | full_code = "\n" 340 | for block in code_blocks: 341 | if block.startswith("python"): 342 | full_code += block[7:] + "\n" 343 | elif block.startswith(" python"): 344 | full_code += block[8:] + "\n" 345 | else: 346 | #pass 347 | full_code += block[0:] + "\n" 348 | print(colors.GREEN + "\n=========== execution =============") 349 | print(full_code) 350 | print("===================================" + colors.ENDC) 351 | return full_code 352 | else: 353 | return None 354 | 355 | 356 | def get_task_queue(ws, g_chatbot, agent1, agent2): 357 | #question = input(colors.GREEN + "Enter a task: " + colors.ENDC) 358 | task = ws["common"]["task"] 359 | print("The task: " + task + colors.ENDC) 360 | print(colors.YELLOW + "ChatGPT: Thinking...please wait..." + colors.ENDC) 361 | num_retries = 0 362 | max_retries = 5 363 | while num_retries < max_retries: 364 | response: str = g_chatbot(task) 365 | print("\n-------------------------- response --------------------------") 366 | print(colors.YELLOW + "ChatGPT: " + colors.ENDC + response) 367 | code: str = __extract_python_code(response) 368 | if code is None: 369 | print(colors.RED + "ERROR: no python code found in the response. Retrying..." + colors.ENDC) 370 | num_retries += 1 371 | question = "You must generate valid Python code. Please try again." 372 | continue 373 | else: 374 | if len(code) == 0: 375 | print(colors.RED + "ERROR: python code is empty. Retrying..." + colors.ENDC) 376 | num_retries += 1 377 | question = "You must generate valid Python code. Please try again." 378 | continue 379 | else: 380 | print("\nPlease wait while I execute the above code...") 381 | try: 382 | # existing local vars must be given explicitly as a dict 383 | ldict = {"agent1": agent1, "agent2": agent2} 384 | exec(code, globals(), ldict)#locals()) 385 | task_queue = ldict["task_queue"] 386 | print("Done executing code.") 387 | break 388 | except Exception as e: 389 | print(colors.RED + "ERROR: could not execute the code: {}\nRetrying...".format(e) + colors.ENDC) 390 | num_retries += 1 391 | question = "While executing your code I've encountered the following error: {}\nPlease fix the error and show me valid code.".format(e) 392 | continue 393 | print("Excecuting the task queue in the simulator...") 394 | return task_queue 395 | 396 | 397 | ocma_instruction = """I would like you to help me work with AI agents called "agent1" and "agent2" in a kitchen environment similar to the video game Overcooked. 398 | Inside the kitchen there are the following items: ["tomato", "lettuce", "plate0", "plate1", "cutboard0", "cutboard1", "counter0", "counter1", "counter2", "counter3"]. 399 | 400 | Each agent has the following functions that you can use to make them take actions: 401 | fetch(item: str) - go to the item's location and pick it up 402 | put_onto(item: str) - put the object in hand onto the item 403 | slice_on(item: str) - slice food (item must be a cutboard) 404 | deliver(None) - deliver the cooked food 405 | 406 | Remember that two agents must work together. 407 | Only agent1 is able to slice foods on a cutboard. 408 | agent2 should pick up foods and plates and place them on the counter for agent1. 409 | 410 | When I ask you to do something, please give me a list of tasks in Python code that is needed to achieve the goal. 411 | You must strictly satisfy the following requirements when you write code for me: 412 | - You must put your code in a single Markdown code block starting with ```python and ending with ```. 413 | - You must not use any hypothetical functions or variables that you think exist. Use only the functions that I listed above. 414 | - Your code must be immediately executable via the exec() function. 415 | - You must create a list named task_queue and store each function and its argument as a tuple. 416 | 417 | Get ready! 418 | """ 419 | 420 | ocma_example = """```python 421 | # the goal is to make a lettuce salad. Think about what tasks need to be accomplished step by step. 422 | task_queue = [] 423 | 424 | # 1. agent2 picks up lettuce 425 | task_queue.append((agent2.fetch, "lettuce")) 426 | 427 | # 2. agent2 puts the lettuce onto counter0 for agent1 (agent2 already has lettuce in hand) 428 | task_queue.append((agent2.put_onto, "counter0")) 429 | 430 | # 3. agent1 picks up the lettuce from counter0 431 | task_queue.append((agent1.fetch, "lettuce")) 432 | 433 | # 4. agent1 puts the lettuce onto cutboard0 (agent1 already has lettuce in hand) 434 | task_queue.append((agent1.put_onto, "cutboard0")) 435 | 436 | # 5. agent1 slices the lettuce (lettuce is already on cutboard0). remember: only agent1 can slice foods 437 | task_queue.append((agent1.slice_on, "cutboard0")) 438 | 439 | # 6. agent2 picks up plate0 440 | task_queue.append((agent2.fetch, "plate0")) 441 | 442 | # 7. agent2 puts plate0 onto counter0 for agent1 443 | task_queue.append((agent2.put_onto, "counter0")) 444 | 445 | # 8. agent1 picks up the sliced lettuce 446 | task_queue.append((agent1.fetch, "lettuce")) 447 | 448 | # 9. agent1 puts the sliced lettuce onto plate0 (agent1 already has the sliced lettuce in hand) 449 | task_queue.append((agent1.put_onto, "plate0")) 450 | 451 | # 10. agent1 picks up the plate with the sliced lettuce 452 | task_queue.append((agent1.fetch, "lettuce")) 453 | 454 | # 11. agent1 delivers (agent1 already has the salad in hand) 455 | task_queue.append((agent1.deliver, None)) 456 | ``` 457 | """ --------------------------------------------------------------------------------