├── 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 | """
--------------------------------------------------------------------------------