Eric Botti commited on
Commit
8d942c4
·
1 Parent(s): 672c019

converted player classes to pydantic, adding player logging

Browse files
src/agent_interfaces.py CHANGED
@@ -1,35 +1,30 @@
1
  from json import JSONDecodeError
2
- from typing import Type, NewType
3
  import json
4
 
5
  from openai import OpenAI
6
  from colorama import Fore, Style
7
- from pydantic import BaseModel, ValidationError
8
 
9
  from output_formats import OutputFormatModel
10
  from message import Message, AgentMessage
11
  from data_collection import save
12
 
13
 
14
- class BaseAgentInterface:
15
  """
16
  The interface that agents use to receive info from and interact with the game.
17
  This is the base class and should not be used directly.
18
  """
19
 
 
 
 
 
 
 
20
  is_human: bool = False
21
-
22
- def __init__(
23
- self,
24
- agent_id: str = None,
25
- log_messages: bool = True
26
- ):
27
- self.id = agent_id
28
- """The id of the agent."""
29
- self.log_messages = log_messages
30
- """Whether to log messages or not."""
31
- self.messages = []
32
- """The message history of the agent."""
33
 
34
  @property
35
  def is_ai(self):
@@ -37,7 +32,7 @@ class BaseAgentInterface:
37
 
38
  def add_message(self, message: Message):
39
  """Adds a message to the message history, without generating a response."""
40
- bound_message = AgentMessage.from_message(message, self.id, len(self.messages))
41
  if self.log_messages:
42
  save(bound_message)
43
  self.messages.append(bound_message)
@@ -129,16 +124,14 @@ class BaseAgentInterface:
129
  raise NotImplementedError
130
 
131
 
132
- AgentInterface = NewType("AgentInterface", BaseAgentInterface)
133
-
134
-
135
  class OpenAIAgentInterface(BaseAgentInterface):
136
  """An interface that uses the OpenAI API (or compatible 3rd parties) to generate responses."""
 
137
 
138
- def __init__(self, agent_id: str, model_name: str = "gpt-3.5-turbo"):
139
- super().__init__(agent_id)
140
- self.model_name = model_name
141
- self.client = OpenAI()
142
 
143
  def _generate(self) -> str:
144
  """Generates a response using the message history"""
@@ -153,7 +146,7 @@ class OpenAIAgentInterface(BaseAgentInterface):
153
 
154
 
155
  class HumanAgentInterface(BaseAgentInterface):
156
- is_human = True
157
 
158
  def generate_formatted_response(
159
  self,
 
1
  from json import JSONDecodeError
2
+ from typing import Type, NewType, List, Any
3
  import json
4
 
5
  from openai import OpenAI
6
  from colorama import Fore, Style
7
+ from pydantic import BaseModel, ValidationError, Field, ConfigDict
8
 
9
  from output_formats import OutputFormatModel
10
  from message import Message, AgentMessage
11
  from data_collection import save
12
 
13
 
14
+ class BaseAgentInterface(BaseModel):
15
  """
16
  The interface that agents use to receive info from and interact with the game.
17
  This is the base class and should not be used directly.
18
  """
19
 
20
+ agent_id: str
21
+ """The id of the agent."""
22
+ log_messages: bool = True
23
+ """Whether to log messages or not."""
24
+ messages: List[Message] = []
25
+ """The message history of the agent."""
26
  is_human: bool = False
27
+ """Whether the agent is human or not."""
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  @property
30
  def is_ai(self):
 
32
 
33
  def add_message(self, message: Message):
34
  """Adds a message to the message history, without generating a response."""
35
+ bound_message = AgentMessage.from_message(message, self.agent_id, len(self.messages))
36
  if self.log_messages:
37
  save(bound_message)
38
  self.messages.append(bound_message)
 
124
  raise NotImplementedError
125
 
126
 
 
 
 
127
  class OpenAIAgentInterface(BaseAgentInterface):
128
  """An interface that uses the OpenAI API (or compatible 3rd parties) to generate responses."""
129
+ model_config = ConfigDict(protected_namespaces=())
130
 
131
+ model_name: str ="gpt-3.5-turbo"
132
+ """The name of the model to use for generating responses."""
133
+ client: Any = Field(default_factory=OpenAI, exclude=True)
134
+ """The OpenAI client used to generate responses."""
135
 
136
  def _generate(self) -> str:
137
  """Generates a response using the message history"""
 
146
 
147
 
148
  class HumanAgentInterface(BaseAgentInterface):
149
+ is_human: bool = Field(default=True, frozen=True)
150
 
151
  def generate_formatted_response(
152
  self,
src/data_collection.py CHANGED
@@ -26,11 +26,11 @@ def save(log_object: Model):
26
 
27
 
28
  def get_log_file(log_object: Model) -> str:
29
- match type(log_object):
30
- case message.AgentMessage:
31
- log_file = "messages.jsonl"
32
- # ...
33
- case _:
34
- raise ValueError(f"Unknown log object type: {type(log_object)}")
35
 
36
  return os.path.join(data_dir, log_file)
 
26
 
27
 
28
  def get_log_file(log_object: Model) -> str:
29
+ if isinstance(log_object, message.AgentMessage):
30
+ log_file = "messages.jsonl"
31
+ elif isinstance(log_object, player.Player):
32
+ log_file = "players.jsonl"
33
+ else:
34
+ raise ValueError(f"Unknown log object type: {type(log_object)}")
35
 
36
  return os.path.join(data_dir, log_file)
src/game.py CHANGED
@@ -4,7 +4,7 @@ from game_utils import *
4
  from player import Player
5
  from message import Message, MessageType
6
  from agent_interfaces import HumanAgentCLI, OpenAIAgentInterface, HumanAgentInterface
7
-
8
 
9
  # Abstracting the Game Class is a WIP so that future games can be added
10
  class Game:
@@ -34,16 +34,12 @@ class Game:
34
 
35
  def player_from_id(self, player_id: str) -> Player:
36
  """Returns a player from their ID."""
37
- return next((player for player in self.players if player.id == player_id), None)
38
 
39
  def player_from_name(self, name: str) -> Player:
40
  """Returns a player from their name."""
41
  return next((player for player in self.players if player.name == name), None)
42
 
43
- def human_player(self) -> Player:
44
- """Returns the human player."""
45
- return next((player for player in self.players if player.interface.is_human), None)
46
-
47
  def game_message(
48
  self,
49
  content: str,
@@ -88,6 +84,11 @@ class Game:
88
  """Runs the game."""
89
  raise NotImplementedError("The run_game method must be implemented by the subclass.")
90
 
 
 
 
 
 
91
  @classmethod
92
  def from_human_name(
93
  cls, human_name: str = None,
@@ -113,18 +114,19 @@ class Game:
113
 
114
  for i in range(0, cls.number_of_players):
115
  player_id = f"{game_id}-{i + 1}"
 
116
 
117
  if human_index == i:
118
- name = human_name
119
- interface = human_interface(player_id)
120
- message_level = human_message_level
121
  else:
122
- name = ai_names.pop()
123
  # all AI players use the OpenAI interface for now - this can be changed in the future
124
- interface = OpenAIAgentInterface(player_id)
125
- message_level = "info"
126
 
127
- players.append(Player(name, player_id, interface, message_level))
128
 
129
  # Add Observer - an Agent who can see all the messages, but doesn't actually play
130
  if human_index is None:
 
4
  from player import Player
5
  from message import Message, MessageType
6
  from agent_interfaces import HumanAgentCLI, OpenAIAgentInterface, HumanAgentInterface
7
+ from data_collection import save
8
 
9
  # Abstracting the Game Class is a WIP so that future games can be added
10
  class Game:
 
34
 
35
  def player_from_id(self, player_id: str) -> Player:
36
  """Returns a player from their ID."""
37
+ return next((player for player in self.players if player.player_id == player_id), None)
38
 
39
  def player_from_name(self, name: str) -> Player:
40
  """Returns a player from their name."""
41
  return next((player for player in self.players if player.name == name), None)
42
 
 
 
 
 
43
  def game_message(
44
  self,
45
  content: str,
 
84
  """Runs the game."""
85
  raise NotImplementedError("The run_game method must be implemented by the subclass.")
86
 
87
+ def end_game(self):
88
+ """Ends the game and declares a winner."""
89
+ for player in self.players:
90
+ save(player)
91
+
92
  @classmethod
93
  def from_human_name(
94
  cls, human_name: str = None,
 
114
 
115
  for i in range(0, cls.number_of_players):
116
  player_id = f"{game_id}-{i + 1}"
117
+ player_dict = {"game_id": game_id, "player_id": player_id}
118
 
119
  if human_index == i:
120
+ player_dict["name"] = human_name
121
+ player_dict["interface"] = human_interface(agent_id=player_id)
122
+ player_dict["message_level"] = human_message_level
123
  else:
124
+ player_dict["name"] = ai_names.pop()
125
  # all AI players use the OpenAI interface for now - this can be changed in the future
126
+ player_dict["interface"] = OpenAIAgentInterface(agent_id=player_id)
127
+ player_dict["message_level"] = "info"
128
 
129
+ players.append(Player(**player_dict))
130
 
131
  # Add Observer - an Agent who can see all the messages, but doesn't actually play
132
  if human_index is None:
src/game_chameleon.py CHANGED
@@ -66,11 +66,6 @@ class ChameleonGame(Game):
66
  """Returns the current herd vote tally."""
67
  return self.herd_vote_tallies[-1]
68
 
69
- @property
70
- def round_number(self) -> int:
71
- """Returns the current round number."""
72
- return len(self.herd_animals)
73
-
74
  def run_game(self):
75
  """Starts the game."""
76
 
@@ -91,9 +86,10 @@ class ChameleonGame(Game):
91
 
92
  if max(points) >= self.winning_score:
93
  self.game_state = "game_end"
94
- self.winner_id = self.players[points.index(max(points))].id
95
  winner = self.player_from_id(self.winner_id)
96
  self.game_message(f"The game is over {winner.name} has won!")
 
97
 
98
  else:
99
  self.game_state = "setup_round"
@@ -109,8 +105,8 @@ class ChameleonGame(Game):
109
  # Phase I: Collect Player Animal Descriptions
110
  if self.game_state == "animal_description":
111
  for current_player in self.players:
112
- if current_player.id not in [animal_description['player_id'] for animal_description in
113
- self.round_animal_descriptions]:
114
 
115
  response = self.player_turn_animal_description(current_player)
116
 
@@ -127,8 +123,8 @@ class ChameleonGame(Game):
127
  # Phase III: The Herd Votes for who they think the Chameleon is
128
  if self.game_state == "herd_vote":
129
  for current_player in self.players:
130
- if current_player.role == "herd" and current_player.id not in [vote['voter_id'] for vote in
131
- self.herd_vote_tally]:
132
 
133
  response = self.player_turn_herd_vote(current_player)
134
 
@@ -147,7 +143,7 @@ class ChameleonGame(Game):
147
 
148
  # Assign Roles
149
  chameleon_index = random_index(len(self.players))
150
- self.chameleon_ids.append(self.players[chameleon_index].id)
151
 
152
  for i, player in enumerate(self.players):
153
  if i == chameleon_index:
@@ -176,7 +172,7 @@ class ChameleonGame(Game):
176
  response = player.interface.generate_formatted_response(AnimalDescriptionFormat)
177
 
178
  if response:
179
- self.round_animal_descriptions.append({"player_id": player.id, "description": response.description})
180
  self.game_message(f"{player.name}: {response.description}", player, exclude=True)
181
  self.awaiting_input = False
182
  else:
@@ -221,7 +217,7 @@ class ChameleonGame(Game):
221
 
222
  voted_for_player = self.player_from_name(response.vote)
223
 
224
- player_vote = {"voter_id": player.id, "voted_for_id": voted_for_player.id}
225
 
226
  self.herd_vote_tally.append(player_vote)
227
  self.awaiting_input = False
@@ -263,11 +259,11 @@ class ChameleonGame(Game):
263
  player.points += 1
264
  # If a Herd player votes for the Chameleon = +1 Point to that player
265
  for vote in self.herd_vote_tally:
266
- if vote["voted_for_id"] == self.chameleon.id:
267
  self.player_from_id(vote['voter_id']).points += 1
268
 
269
  # If the Herd fails to accuse the Chameleon = +1 Point to the Chameleon
270
- if not accused_player_id or accused_player_id != self.chameleon.id:
271
  self.chameleon.points += 1
272
 
273
  # Print Scores
@@ -299,7 +295,7 @@ class ChameleonGame(Game):
299
  formatted_responses = ""
300
  for response in self.round_animal_descriptions:
301
  # Used to exclude the player who is currently responding, so they don't vote for themselves like a fool
302
- if response["player_id"] != exclude.id:
303
  player = self.player_from_id(response["player_id"])
304
  formatted_responses += f" - {player.name}: {response['description']}\n"
305
 
 
66
  """Returns the current herd vote tally."""
67
  return self.herd_vote_tallies[-1]
68
 
 
 
 
 
 
69
  def run_game(self):
70
  """Starts the game."""
71
 
 
86
 
87
  if max(points) >= self.winning_score:
88
  self.game_state = "game_end"
89
+ self.winner_id = self.players[points.index(max(points))].player_id
90
  winner = self.player_from_id(self.winner_id)
91
  self.game_message(f"The game is over {winner.name} has won!")
92
+ self.end_game()
93
 
94
  else:
95
  self.game_state = "setup_round"
 
105
  # Phase I: Collect Player Animal Descriptions
106
  if self.game_state == "animal_description":
107
  for current_player in self.players:
108
+ if current_player.player_id not in [animal_description['player_id'] for animal_description in
109
+ self.round_animal_descriptions]:
110
 
111
  response = self.player_turn_animal_description(current_player)
112
 
 
123
  # Phase III: The Herd Votes for who they think the Chameleon is
124
  if self.game_state == "herd_vote":
125
  for current_player in self.players:
126
+ if current_player.role == "herd" and current_player.player_id not in [vote['voter_id'] for vote in
127
+ self.herd_vote_tally]:
128
 
129
  response = self.player_turn_herd_vote(current_player)
130
 
 
143
 
144
  # Assign Roles
145
  chameleon_index = random_index(len(self.players))
146
+ self.chameleon_ids.append(self.players[chameleon_index].player_id)
147
 
148
  for i, player in enumerate(self.players):
149
  if i == chameleon_index:
 
172
  response = player.interface.generate_formatted_response(AnimalDescriptionFormat)
173
 
174
  if response:
175
+ self.round_animal_descriptions.append({"player_id": player.player_id, "description": response.description})
176
  self.game_message(f"{player.name}: {response.description}", player, exclude=True)
177
  self.awaiting_input = False
178
  else:
 
217
 
218
  voted_for_player = self.player_from_name(response.vote)
219
 
220
+ player_vote = {"voter_id": player.player_id, "voted_for_id": voted_for_player.player_id}
221
 
222
  self.herd_vote_tally.append(player_vote)
223
  self.awaiting_input = False
 
259
  player.points += 1
260
  # If a Herd player votes for the Chameleon = +1 Point to that player
261
  for vote in self.herd_vote_tally:
262
+ if vote["voted_for_id"] == self.chameleon.player_id:
263
  self.player_from_id(vote['voter_id']).points += 1
264
 
265
  # If the Herd fails to accuse the Chameleon = +1 Point to the Chameleon
266
+ if not accused_player_id or accused_player_id != self.chameleon.player_id:
267
  self.chameleon.points += 1
268
 
269
  # Print Scores
 
295
  formatted_responses = ""
296
  for response in self.round_animal_descriptions:
297
  # Used to exclude the player who is currently responding, so they don't vote for themselves like a fool
298
+ if response["player_id"] != exclude.player_id:
299
  player = self.player_from_id(response["player_id"])
300
  formatted_responses += f" - {player.name}: {response['description']}\n"
301
 
src/player.py CHANGED
@@ -1,29 +1,25 @@
1
  from typing import Literal, List, Type
2
 
3
- from agent_interfaces import AgentInterface, HumanAgentCLI, BaseAgentInterface
 
 
4
  from message import MessageType
5
 
6
  Role = Literal["chameleon", "herd"]
7
 
8
 
9
  # Abstraction is a WIP and a little premature, but I'd like to reuse this framework to create more Games in the future
10
- class Player:
11
  """Base class for a player"""
12
 
13
- def __init__(self,
14
- name: str,
15
- player_id: str,
16
- interface: BaseAgentInterface,
17
- message_level: str = "info"
18
- ):
19
- self.name = name
20
- """The name of the player."""
21
- self.id = player_id
22
- """The id of the player."""
23
- self.interface = interface
24
- """The interface used by the agent controlling the player to communicate with the game."""
25
- self.message_level = message_level
26
- """The level of messages that the player will receive. Can be "info", "verbose", or "debug"."""
27
 
28
  def can_receive_message(self, message_type: MessageType) -> bool:
29
  """Returns True if the player can receive a message of the type."""
@@ -44,29 +40,28 @@ class Player:
44
  ):
45
  """Creates an observer player."""
46
  name = "Observer"
47
- player_id = f"{game_id}_observer"
48
- interface = interface_type(player_id, log_messages)
49
 
50
- return cls(name, player_id, interface, message_level)
51
 
52
 
53
  class PlayerSubclass(Player):
54
  @classmethod
55
  def from_player(cls, player: Player):
56
  """Creates a new instance of the subclass from a player instance."""
57
- return cls(player.name, player.id, player.interface, player.message_level)
 
 
58
 
59
 
60
  class ChameleonPlayer(PlayerSubclass):
61
  """A player in the game Chameleon"""
62
 
63
- def __init__(self, *args, **kwargs):
64
- super().__init__(*args, **kwargs)
65
-
66
- self.points: int = 0
67
- """The number of points the player has."""
68
- self.roles: List[Role] = []
69
- """The role of the player in the game. Can be "chameleon" or "herd". This changes every round."""
70
 
71
  def assign_role(self, role: Role):
72
  self.roles.append(role)
 
1
  from typing import Literal, List, Type
2
 
3
+ from pydantic import BaseModel, Field
4
+
5
+ from agent_interfaces import HumanAgentCLI, BaseAgentInterface
6
  from message import MessageType
7
 
8
  Role = Literal["chameleon", "herd"]
9
 
10
 
11
  # Abstraction is a WIP and a little premature, but I'd like to reuse this framework to create more Games in the future
12
+ class Player(BaseModel):
13
  """Base class for a player"""
14
 
15
+ name: str
16
+ """The name of the player."""
17
+ player_id: str
18
+ """The id of the player."""
19
+ interface: BaseAgentInterface = Field(exclude=True)
20
+ """The interface used by the agent controlling the player to communicate with the game."""
21
+ message_level: str = "info"
22
+ """The level of messages that the player will receive. Can be "info", "verbose", or "debug"."""
 
 
 
 
 
 
23
 
24
  def can_receive_message(self, message_type: MessageType) -> bool:
25
  """Returns True if the player can receive a message of the type."""
 
40
  ):
41
  """Creates an observer player."""
42
  name = "Observer"
43
+ player_id = f"{game_id}-observer"
44
+ interface = interface_type(agent_id=player_id)
45
 
46
+ return cls(name=name, player_id=player_id, interface=interface, message_level=message_level)
47
 
48
 
49
  class PlayerSubclass(Player):
50
  @classmethod
51
  def from_player(cls, player: Player):
52
  """Creates a new instance of the subclass from a player instance."""
53
+ fields = player.model_dump()
54
+ fields['interface'] = player.interface
55
+ return cls(**fields)
56
 
57
 
58
  class ChameleonPlayer(PlayerSubclass):
59
  """A player in the game Chameleon"""
60
 
61
+ points: int = 0
62
+ """The number of points the player has."""
63
+ roles: List[Role] = []
64
+ """The role of the player in the game. Can be "chameleon" or "herd". This changes every round."""
 
 
 
65
 
66
  def assign_role(self, role: Role):
67
  self.roles.append(role)