Eric Botti commited on
Commit
758a706
·
1 Parent(s): f566386

more robust message logging

Browse files
Files changed (3) hide show
  1. src/game.py +1 -1
  2. src/message.py +46 -26
  3. src/player.py +18 -10
src/game.py CHANGED
@@ -39,7 +39,7 @@ class Game:
39
  ):
40
  # Game ID
41
  self.game_id = game_id()
42
- self.start_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
43
  self.log_dir = os.path.join(self.log_dir, f"{self.start_time}-{self.game_id}")
44
  os.makedirs(self.log_dir, exist_ok=True)
45
 
 
39
  ):
40
  # Game ID
41
  self.game_id = game_id()
42
+ self.start_time = datetime.now().strftime('%y%m%d-%H%M%S')
43
  self.log_dir = os.path.join(self.log_dir, f"{self.start_time}-{self.game_id}")
44
  os.makedirs(self.log_dir, exist_ok=True)
45
 
src/message.py CHANGED
@@ -1,36 +1,56 @@
1
  from typing import Literal
 
2
 
3
- from pydantic import BaseModel
 
 
 
 
 
 
4
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- # Lots of AI Libraries use HumanMessage and AIMessage as the base classes for their messages.
7
- # This doesn't make sense for our as Humans and AIs are both players in the game, meaning they have the same role.
8
- # The Langchain type field is used to convert to that syntax.
9
  class Message(BaseModel):
10
- type: Literal["game", "player", "retry", "error", "format"]
11
- """The type of the message. Can be "prompt" or "player"."""
 
 
 
 
12
  content: str
13
  """The content of the message."""
14
- @property
15
- def langchain_type(self):
16
- """Returns the langchain message type for the message."""
17
- if self.type in ["game", "retry", "error", "format"]:
18
- return "human"
 
 
 
 
 
 
 
19
  else:
20
- return "ai"
 
 
 
 
 
 
 
 
 
21
 
22
 
23
- """
24
- Right now we have two separate systems that use the word "message":
25
- 1. The Game class uses messages to communicate with the players
26
- They have types:
27
- - "game" used for all players, these are sent to the players and converted into the the above message class
28
- - "verbose", and "debug" currently for the human player only
29
- 2. The Player class uses messsage is to communicate with the controller (either the AI or the human)
30
- - "game" type messages come from the Game and are responded to by the format.
31
- - "retry", "error", and "format"
32
- - "player" is used to communicate with the AI or human player.
33
- All of these messages are logged
34
-
35
- Long term we should investigate merging these two systems so we can log verbose and debug messages if desired.
36
- """
 
1
  from typing import Literal
2
+ from pydantic import BaseModel, computed_field
3
 
4
+ """
5
+ Right now we have two separate systems that use the word "message":
6
+
7
+ 1. The Game class uses messages to communicate with the players
8
+ - "game" messages pile up in the queue and are responded to by the player once an "instructional" message is sent.
9
+ - "verbose", and "debug" currently for the human player only
10
+ This does **NOT** use the Message class defined below
11
 
12
+ 2. The Player class uses messages to communicate with the controller (either the AI or the human)
13
+ - "prompt" type messages come from the Game and are responded to by the player.
14
+ - "retry", "error", and "format" are internal messages used by the player to ensure the correct format
15
+ - "player" is used to communicate with the AI or human player.
16
+ All of these messages are logged, and use the Message class defined below
17
+
18
+ For the future we should investigate redesigning/merging these two systems to avoid confusion
19
+ """
20
+
21
+ MessageType = Literal["prompt", "player", "retry", "error", "format"]
22
 
 
 
 
23
  class Message(BaseModel):
24
+ player_id: str
25
+ """The id of the player that the message was sent by/to."""
26
+ message_number: int
27
+ """The number of the message, indicating the order in which it was sent."""
28
+ type: MessageType
29
+ """The type of the message. Can be "prompt", "player", "retry", "error", or "format"."""
30
  content: str
31
  """The content of the message."""
32
+
33
+ @computed_field
34
+ def conversation_role(self) -> str:
35
+ """The message type in the format used by the LLM."""
36
+
37
+ # Most LLMs expect the "prompt" to come from a "user" and the "response" to come from an "assistant"
38
+ # Since the agents are the ones responding to messages, take on the llm_type of "assistant"
39
+ # This can be counterintuitive since they can be controlled by either human or ai
40
+ # Further, The programmatic messages from the game are always "user"
41
+
42
+ if self.type in ["prompt", "retry", "error", "format"]:
43
+ return "user"
44
  else:
45
+ return "assistant"
46
+
47
+ @computed_field
48
+ def message_id(self) -> str:
49
+ """Returns the message id in the format used by the LLM."""
50
+ return f"{self.player_id}-{self.message_number}"
51
+
52
+ def to_controller(self) -> tuple[str, str]:
53
+ """Returns the message in a format that can be used by the controller."""
54
+ return self.conversation_role, self.content
55
 
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/player.py CHANGED
@@ -12,21 +12,22 @@ from langchain_core.exceptions import OutputParserException
12
  from pydantic import BaseModel
13
 
14
  from game_utils import log
15
- from message import Message
16
 
17
  Role = Literal["chameleon", "herd"]
18
 
19
  logging.basicConfig(level=logging.WARNING)
20
  logger = logging.getLogger("chameleon")
21
 
 
22
  class Player:
23
 
24
  role: Role | None = None
25
- """The role of the player in the game. Can be "chameleon" or "herd"."""
26
  rounds_played_as_chameleon: int = 0
27
- """The number of times the player has been the chameleon."""
28
  rounds_played_as_herd: int = 0
29
- """The number of times the player has been in the herd."""
30
  points: int = 0
31
  """The number of points the player has."""
32
 
@@ -86,7 +87,7 @@ class Player:
86
  # Clear the prompt queue
87
  self.prompt_queue = []
88
 
89
- message = Message(type="game", content=prompt)
90
  output = await self.generate.ainvoke(message)
91
  if self.controller_type == "ai":
92
  retries = 0
@@ -96,11 +97,13 @@ class Player:
96
  if retries < max_retries:
97
  retries += 1
98
  logger.warning(f"Player {self.id} failed to format response: {output} due to an exception: {e} \n\n Retrying {retries}/{max_retries}")
99
- self.add_to_history(Message(type="retry", content=f"Error formatting response: {e} \n\n Please try again."))
 
100
  output = await self.format_output.ainvoke({"output_format": output_format})
101
 
102
  else:
103
- self.add_to_history(Message(type="error", content=f"Error formatting response: {e} \n\n Max retries reached."))
 
104
  logging.error(f"Max retries reached due to Error: {e}")
105
  raise e
106
  else:
@@ -110,6 +113,11 @@ class Player:
110
 
111
  return output
112
 
 
 
 
 
 
113
  def add_to_history(self, message: Message):
114
  self.messages.append(message)
115
  log(message.model_dump(), self.log_filepath)
@@ -128,10 +136,10 @@ class Player:
128
  if self.controller_type == "human":
129
  response = await self.controller.ainvoke(message.content)
130
  else:
131
- formatted_messages = [(message.langchain_type, message.content) for message in self.messages]
132
  response = await self.controller.ainvoke(formatted_messages)
133
 
134
- self.add_to_history(Message(type="player", content=response.content))
135
 
136
  return response
137
 
@@ -147,7 +155,7 @@ class Player:
147
 
148
  prompt = prompt_template.invoke({"format_instructions": parser.get_format_instructions()})
149
 
150
- message = Message(type="format", content=prompt.text)
151
 
152
  response = await self.generate.ainvoke(message)
153
 
 
12
  from pydantic import BaseModel
13
 
14
  from game_utils import log
15
+ from message import Message, MessageType
16
 
17
  Role = Literal["chameleon", "herd"]
18
 
19
  logging.basicConfig(level=logging.WARNING)
20
  logger = logging.getLogger("chameleon")
21
 
22
+
23
  class Player:
24
 
25
  role: Role | None = None
26
+ """The role of the player in the game. Can be "chameleon" or "herd". This changes every round."""
27
  rounds_played_as_chameleon: int = 0
28
+ """The number of times the player has been the Chameleon."""
29
  rounds_played_as_herd: int = 0
30
+ """The number of times the player has been in the Herd."""
31
  points: int = 0
32
  """The number of points the player has."""
33
 
 
87
  # Clear the prompt queue
88
  self.prompt_queue = []
89
 
90
+ message = self.player_message("prompt", prompt)
91
  output = await self.generate.ainvoke(message)
92
  if self.controller_type == "ai":
93
  retries = 0
 
97
  if retries < max_retries:
98
  retries += 1
99
  logger.warning(f"Player {self.id} failed to format response: {output} due to an exception: {e} \n\n Retrying {retries}/{max_retries}")
100
+ retry_message = self.player_message("retry", f"Error formatting response: {e} \n\n Please try again.")
101
+ self.add_to_history(retry_message)
102
  output = await self.format_output.ainvoke({"output_format": output_format})
103
 
104
  else:
105
+ error_message = self.player_message("error", f"Error formatting response: {e} \n\n Max retries reached.")
106
+ self.add_to_history(error_message)
107
  logging.error(f"Max retries reached due to Error: {e}")
108
  raise e
109
  else:
 
113
 
114
  return output
115
 
116
+ def player_message(self, message_type: MessageType, content: str) -> Message:
117
+ """Creates a message assigned to the player."""
118
+ return Message(player_id=self.id, message_number=len(self.messages), type=message_type, content=content)
119
+
120
+
121
  def add_to_history(self, message: Message):
122
  self.messages.append(message)
123
  log(message.model_dump(), self.log_filepath)
 
136
  if self.controller_type == "human":
137
  response = await self.controller.ainvoke(message.content)
138
  else:
139
+ formatted_messages = [message.to_controller() for message in self.messages]
140
  response = await self.controller.ainvoke(formatted_messages)
141
 
142
+ self.add_to_history(self.player_message("player", response.content))
143
 
144
  return response
145
 
 
155
 
156
  prompt = prompt_template.invoke({"format_instructions": parser.get_format_instructions()})
157
 
158
+ message = self.player_message("format", prompt.text)
159
 
160
  response = await self.generate.ainvoke(message)
161