Skip to content

Commit e2456fd

Browse files
committed
Added Atari Boxing game
Changed the scoring instructions with more emphasis on head and fist alignment Added 1. GPT4-vision as a model 2. Attached graphical board game state to query 3. Rendered board game state for human viewing via matplotlib.pyplt
1 parent 4bdf930 commit e2456fd

11 files changed

Lines changed: 717 additions & 44 deletions

File tree

agents/gpt.py

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from dataclasses import dataclass, field
2+
from typing import Any, Dict, List
23
from api.classes import Agent, AvailableActions, Action, Observation, Rules
3-
import random
44
import openai
55
import api.util as util
66
import ast
77
import json
8+
from PIL import Image
89

910

1011
action_format_instructions_no_openended = """\
@@ -30,29 +31,38 @@
3031

3132
@dataclass
3233
class OpenAITextAgent(Agent):
33-
openai_model: str
34-
agent_type_id: str
35-
system_message: str = "You are an agent playing a game. Select the action that maximizes your probability of winning."
36-
max_retries: int = 3
37-
transparent_reasoning: bool = False
34+
openai_model : str
35+
agent_type_id : str
36+
system_message : str = "You are an agent playing a game. Select the action that maximizes your probability of winning."
37+
max_retries : int = 3
38+
transparent_reasoning : bool = False
3839
mode: int = 0 # 0 = normal, 1 = chain of thought, 2 = babble and prune
39-
40+
4041
def print(self, *args, **kwargs):
4142
if self.transparent_reasoning:
4243
print(self.agent_type_id, *args, **kwargs)
44+
45+
def get_user_message_content(self, text_prompt: str, image: Image) -> List[Dict[str, Any]]:
46+
return [
47+
{
48+
"type": "text",
49+
"text": text_prompt
50+
}
51+
]
4352

44-
def take_action(
45-
self,
46-
rules: Rules,
47-
observation: Observation,
48-
available_actions: AvailableActions,
49-
show_state: bool,
50-
):
53+
def get_request_params(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
54+
return {
55+
"model": self.openai_model,
56+
"messages": messages,
57+
"response_format": { "type": "json_object" }
58+
}
59+
60+
def take_action(self, rules: Rules, observation: Observation, available_actions: AvailableActions, show_state : bool) -> Action:
5161
valid_actions = []
5262
prompt = f"You are playing a game called {rules.title}. The rules are as follows:\n{rules.summary}\n"
5363
if rules.additional_details != None:
5464
prompt += "The following are headings with additional information about the rules that you can expand by taking the action Explain(<heading key>).\n"
55-
details_dict = {f"H{i+1}": topic for i, topic in enumerate(rules.additional_details)}
65+
details_dict = {f"H{i+1}": topic + " - " + description for i, (topic, description) in enumerate(rules.additional_details.items())}
5666
prompt += json.dumps(details_dict, indent=4)
5767
valid_actions.extend(f"Explain({h})" for h in list(details_dict.keys()))
5868

@@ -83,7 +93,10 @@ def take_action(
8393
):
8494
prompt += "Return the action Explain(<action>) to receive additional info about what any of the above actions do.\n"
8595

86-
messages = [{"role": "system", "content": self.system_message}]
96+
messages = [
97+
{"role": "system", "content": self.system_message},
98+
{"role": "user", "content": self.get_user_message_content(prompt, observation.image)},
99+
]
87100

88101
# Chain of Thought
89102
if self.mode == 1:
@@ -117,7 +130,7 @@ def take_action(
117130
)
118131
messages.append({"role": "assistant", "content": response})
119132
prompt = ""
120-
133+
121134
self.print(
122135
f"GPT listed the following actions as possibilities: {response}"
123136
)
@@ -128,22 +141,21 @@ def take_action(
128141
prompt += str(list(available_actions.openended))
129142
messages.append({"role": "user", "content": prompt})
130143

144+
145+
146+
prompt += "\nMake sure to return ONLY a JSON. It should contain an 'action' key which contains one of the valid actions. And nothing outside the curly braces of the JSON."
147+
#prompt += str(list(valid_actions))
148+
149+
#print(prompt)
131150
result = None
151+
132152
for _ in range(self.max_retries):
133-
response = (
134-
openai_client.chat.completions.create(
135-
model=self.openai_model,
136-
response_format={"type": "json_object"},
137-
messages=messages,
138-
)
139-
.choices[0]
140-
.message.content
141-
)
153+
response = openai_client.chat.completions.create(**self.get_request_params(messages)).choices[0].message.content
142154
messages.append({"role": "assistant", "content": response})
143155
self.print("GPT responded with", response)
144156

145157
try:
146-
action = ast.literal_eval(response)
158+
action = ast.literal_eval(util.extract_json(response))
147159
except:
148160
self.print("GPT returned invalid JSON")
149161
continue
@@ -154,27 +166,23 @@ def take_action(
154166
messages.append({"role": "user", "content": error_message})
155167
continue
156168

169+
157170
if action["action"] in valid_actions:
158171
self.print("GPT chose valid action", action)
159172
result = action
160173
break
161-
174+
162175
self.print("GPT returned invalid action", action)
163176
error_message = f"{action['action']} is not one of the valid actions. "
164177
error_message += "As a reminder, the valid actions are as follows:\n"
165178
error_message += f"{str(list(valid_actions))}\n"
166179
error_message += "Please return a json with the key 'action' with the action you choose and (optionally) the key 'openended_response' if you select openended response action."
167180
messages.append({"role": "user", "content": error_message})
181+
168182
if result == None:
169-
self.print(
170-
f"WARNING: GPT returned an a random action after {self.max_retries} tries"
171-
)
183+
self.print(f"WARNING: GPT returned an a random action after {self.max_retries} tries")
172184
return Action(action_id=None)
173-
return Action(
174-
action_id=result["action"],
175-
openended_response=result.get("openended_response"),
176-
)
177-
185+
return Action(action_id=result["action"], openended_response=result.get("openended_response"))
178186

179187
@dataclass
180188
class ChatGPTText(OpenAITextAgent):
@@ -199,3 +207,30 @@ class BabbleAndPrune(OpenAITextAgent):
199207
openai_model: str = "gpt-4-1106-preview"
200208
agent_type_id: str = "b&p"
201209
mode: int = 2
210+
211+
@dataclass
212+
class GPT4Vision(OpenAITextAgent):
213+
openai_model : str = "gpt-4-vision-preview"
214+
agent_type_id : str = "gpt-4-vision"
215+
is_vision_agent : bool = True
216+
217+
def get_user_message_content(self, text_prompt: str, image: Image) -> List[Dict[str, str]]:
218+
content = super().get_user_message_content(text_prompt, image)
219+
if image is not None:
220+
content.append({
221+
"type": "image_url",
222+
"image_url": {
223+
"url": f"data:image/png;base64,{util.base64_encode_image(image)}",
224+
"detail": "low"
225+
}
226+
})
227+
return content
228+
229+
def get_request_params(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
230+
return {
231+
"model": self.openai_model,
232+
"messages": messages,
233+
# As vision models have a low(but undocumented?) default value for below parameter
234+
# https://community.openai.com/t/documented-max-token-default-is-incorrect-for-gpt-4-vision-preview/507329
235+
"max_tokens": 250,
236+
}

agents/random_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
class RandomAgent(Agent):
77
agent_type_id : str = "random"
88

9-
def take_action(self, rules : Rules, observation: Observation, available_actions: AvailableActions, show_state : bool):
9+
def take_action(self, rules : Rules, observation: Observation, available_actions: AvailableActions, show_state : bool) -> Action:
1010
actions = list(available_actions.predefined.keys())
1111
return Action(action_id=random.choice(actions))

api/classes.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1-
from typing import List, Dict, Optional, Tuple
1+
from typing import List, Dict, Optional, Tuple, Type
22
from dataclasses import dataclass, field
33
from abc import abstractmethod
44
from PIL import Image
55

66

77
@dataclass
88
class Observation:
9-
text : str
9+
text : str = ""
1010
image : Image = None
11+
12+
def __eq__(self, other):
13+
if not isinstance(other, Observation):
14+
return False
15+
16+
# Check text equality
17+
if self.text != other.text:
18+
return False
19+
20+
# Check image equality
21+
if self.image is None and other.image is None:
22+
return True
23+
elif self.image is None or other.image is None:
24+
return False
25+
else:
26+
return (self.image.tobytes() == other.image.tobytes())
1127

1228
@dataclass
1329
class AvailableActions:
@@ -30,7 +46,7 @@ class Agent:
3046
agent_type_id : str
3147

3248
@abstractmethod
33-
def take_action(self, rules : dict, observation: Observation, available_actions : AvailableActions):
49+
def take_action(self, rules : dict, observation: Observation, available_actions : AvailableActions, show_state : bool) -> Action:
3450
pass
3551

3652
@dataclass
@@ -51,7 +67,7 @@ class Game:
5167
agent_2_kwargs : dict = field(default_factory=dict) # kwargs to pass to the agent 2 class when initializing.
5268

5369
@abstractmethod
54-
def init_game(self, agent_1: Agent, agent_2: Agent):
70+
def init_game(self, agent_1: Type[Agent], agent_2: Type[Agent]):
5571
pass
5672

5773
@abstractmethod

api/play_game.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ def play_game(agent_1_path, agent_2_path, game_path, num_matches = 1, save_resul
8181
util.save_json(matches, "matches.json")
8282
print("Saved match information")
8383

84-
agent_1_rating = agent_1_rating + K * (player_1_score - agent_1_expected_score)
84+
agent_1_rating = agent_1_rating + K * (player_1_score - agent_1_expected_score)
8585
agent_2_rating = agent_2_rating + K * (player_2_score - agent_2_expected_score)
86+
# Without below line, we get a KeyError: '<game_class.id>'
87+
all_ratings.setdefault(game_class.id, {})
8688
all_ratings[game_class.id][agent_1_id] = agent_1_rating
8789
all_ratings[game_class.id][agent_2_id] = agent_2_rating
8890
print("Updated elos:")

api/util.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import importlib
22
import os
33
import json
4+
from PIL import Image
5+
from io import BytesIO
6+
import base64
7+
import re
48

59
def save_json(data, file_path):
610
if not os.path.exists(file_path):
711
os.makedirs(os.path.dirname(file_path), exist_ok=True)
812
with open(file_path, "w") as f:
913
json.dump(data, f, indent=4)
1014

11-
1215
def load_json(file_path):
1316
if not os.path.exists(file_path):
1417
raise ValueError(f"File {file_path} does not exist")
@@ -18,4 +21,20 @@ def load_json(file_path):
1821
def import_class(class_path):
1922
module_path, class_name = class_path.rsplit(".", 1)
2023
module = importlib.import_module(module_path)
21-
return getattr(module, class_name)
24+
return getattr(module, class_name)
25+
26+
def base64_encode_image(image: Image) -> str:
27+
img_buffer = BytesIO()
28+
image.save(img_buffer, format="PNG")
29+
img_str = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
30+
return img_str
31+
32+
def extract_json(input: str) -> dict:
33+
json_match = re.search(r'{.*}', input, re.DOTALL)
34+
if json_match == None:
35+
raise ValueError(f"Could not find JSON in input: {input}")
36+
json_content = json_match.group(0)
37+
return json_content
38+
# Parse the JSON content into a Python dictionary
39+
response_data = json.loads(json_content)
40+
return response_data

games/atari/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Multiplayer Atari Games via PettingZoo
2+
This folder contains ported Atari Games made available in [PettingZoo](https://pettingzoo.farama.org/), a multi agent games environment with an API similar to [OpenAI gym](https://gymnasium.farama.org/).
3+
4+
5+
## Installation
6+
7+
Running these Atari games make use of [AtariARI](github.com/mila-iqia/atari-representation-learning.git
8+
) and [PettingZoo](https://pettingzoo.farama.org/environments/atari/boxing/) libraries
9+
10+
### 0. Install pip3
11+
12+
If not already installed(i.e. if pip3 command not found), install pip3:
13+
> sudo apt-get install python3-pip
14+
15+
Then, upgrade pip
16+
17+
>python3 -m pip install --upgrade pip
18+
19+
### 1. Install AtariARI
20+
21+
Successfully run below 2 commands
22+
23+
>pip3 install 'gym[atari]'
24+
25+
>pip3 install git+https://github.com/mila-iqia/atari-representation-learning.git
26+
27+
### 2. Install PettingZoo
28+
29+
Run
30+
>pip3 install 'pettingzoo[atari]
31+
32+
### 3. Install misc libraries
33+
34+
> pip3 install matplotlib
35+
36+
> pip3 install autorom
37+
38+
> AutoROM
39+
40+
## PettingZoo implementation of realtime games
41+
42+
Atari games were suppsosed to appear realtime for humans, but under the hood they are programmed as turn based games with tens of turns per second.
43+
44+
To a human, a game running at full speed still appears realtime.
45+
46+
PettingZoo models these games as [Agent Environment Cycle](https://pettingzoo.farama.org/api/aec/) environments.
47+
48+
![Alt text](image.png)
49+
50+
At each step, a player(depending on turn) is queried for their next move.
51+
52+
## GameBench implementation of PettingZoo games
53+
54+
Agents are run in background threads. The agent loop is:
55+
56+
1. Get current game state
57+
2. Query agent on what action should be done
58+
3. Store this action in a variable Act
59+
60+
At every turn, we query the stored action Act for that player and execute it.
61+
62+
## Current list of games
63+
64+
1. [Boxing](https://pettingzoo.farama.org/environments/atari/boxing/)

0 commit comments

Comments
 (0)