Eric Botti
commited on
Commit
·
758a706
1
Parent(s):
f566386
more robust message logging
Browse files- src/game.py +1 -1
- src/message.py +46 -26
- 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('%
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
11 |
-
"""The
|
|
|
|
|
|
|
|
|
12 |
content: str
|
13 |
"""The content of the message."""
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
else:
|
20 |
-
return "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
28 |
rounds_played_as_herd: int = 0
|
29 |
-
"""The number of times the player has been in the
|
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 =
|
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.
|
|
|
100 |
output = await self.format_output.ainvoke({"output_format": output_format})
|
101 |
|
102 |
else:
|
103 |
-
self.
|
|
|
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 = [
|
132 |
response = await self.controller.ainvoke(formatted_messages)
|
133 |
|
134 |
-
self.add_to_history(
|
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 =
|
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 |
|