ai-plays-jackbox 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. ai_plays_jackbox/__init__.py +0 -0
  2. ai_plays_jackbox/bot/__init__.py +0 -0
  3. ai_plays_jackbox/bot/bot_base.py +219 -0
  4. ai_plays_jackbox/bot/bot_factory.py +31 -0
  5. ai_plays_jackbox/bot/bot_personality.py +111 -0
  6. ai_plays_jackbox/bot/jackbox5/__init__.py +0 -0
  7. ai_plays_jackbox/bot/jackbox5/bot_base.py +26 -0
  8. ai_plays_jackbox/bot/jackbox5/mad_verse_city.py +121 -0
  9. ai_plays_jackbox/bot/jackbox5/patently_stupid.py +168 -0
  10. ai_plays_jackbox/bot/jackbox6/__init__.py +0 -0
  11. ai_plays_jackbox/bot/jackbox6/bot_base.py +20 -0
  12. ai_plays_jackbox/bot/jackbox6/dictionarium.py +105 -0
  13. ai_plays_jackbox/bot/jackbox6/joke_boat.py +105 -0
  14. ai_plays_jackbox/bot/jackbox7/__init__.py +0 -0
  15. ai_plays_jackbox/bot/jackbox7/bot_base.py +20 -0
  16. ai_plays_jackbox/bot/jackbox7/quiplash3.py +108 -0
  17. ai_plays_jackbox/bot/jackbox8/__init__.py +0 -0
  18. ai_plays_jackbox/bot/jackbox8/bot_base.py +20 -0
  19. ai_plays_jackbox/bot/jackbox8/job_job.py +205 -0
  20. ai_plays_jackbox/bot/standalone/__init__.py +0 -0
  21. ai_plays_jackbox/bot/standalone/drawful2.py +159 -0
  22. ai_plays_jackbox/cli/__init__.py +0 -0
  23. ai_plays_jackbox/cli/main.py +117 -0
  24. ai_plays_jackbox/constants.py +4 -0
  25. ai_plays_jackbox/llm/__init__.py +1 -0
  26. ai_plays_jackbox/llm/chat_model.py +39 -0
  27. ai_plays_jackbox/llm/chat_model_factory.py +35 -0
  28. ai_plays_jackbox/llm/gemini_model.py +86 -0
  29. ai_plays_jackbox/llm/ollama_model.py +53 -0
  30. ai_plays_jackbox/llm/openai_model.py +86 -0
  31. ai_plays_jackbox/room/__init__.py +0 -0
  32. ai_plays_jackbox/room/room.py +87 -0
  33. ai_plays_jackbox/run.py +23 -0
  34. ai_plays_jackbox/scripts/lint.py +18 -0
  35. ai_plays_jackbox/ui/__init__.py +0 -0
  36. ai_plays_jackbox/ui/main.py +12 -0
  37. ai_plays_jackbox/ui/startup.py +271 -0
  38. ai_plays_jackbox-0.4.1.dist-info/METADATA +158 -0
  39. ai_plays_jackbox-0.4.1.dist-info/RECORD +42 -0
  40. ai_plays_jackbox-0.4.1.dist-info/WHEEL +4 -0
  41. ai_plays_jackbox-0.4.1.dist-info/entry_points.txt +4 -0
  42. ai_plays_jackbox-0.4.1.dist-info/licenses/LICENSE +21 -0
File without changes
File without changes
@@ -0,0 +1,219 @@
1
+ import json
2
+ import threading
3
+ import traceback
4
+ from abc import ABC, abstractmethod
5
+ from typing import Optional, Union
6
+ from urllib import parse
7
+ from uuid import uuid4
8
+
9
+ import cv2
10
+ import html2text
11
+ import numpy as np
12
+ from loguru import logger
13
+ from pydantic import BaseModel, Field, field_validator
14
+ from websocket import WebSocketApp
15
+
16
+ from ai_plays_jackbox.constants import ECAST_HOST
17
+ from ai_plays_jackbox.llm.chat_model import ChatModel
18
+ from ai_plays_jackbox.llm.ollama_model import OllamaModel
19
+
20
+
21
+ class JackBoxBotBase(ABC):
22
+ _is_disconnected: bool = False
23
+ _ws: Optional[WebSocketApp] = None
24
+ _ws_thread: Optional[threading.Thread] = None
25
+ _message_sequence: int = 0
26
+ _player_guid: str
27
+ _name: str
28
+ _personality: str
29
+ _chat_model: ChatModel
30
+
31
+ def __init__(
32
+ self,
33
+ name: str = "FunnyBot",
34
+ personality: str = "You are the funniest bot ever.",
35
+ chat_model: Optional[ChatModel] = None,
36
+ ):
37
+ self._name = name
38
+ self._personality = personality
39
+ self._player_guid = str(uuid4())
40
+ if chat_model is None:
41
+ chat_model = OllamaModel()
42
+ self._chat_model = chat_model
43
+
44
+ def connect(self, room_code: str) -> None:
45
+ self._room_code = room_code
46
+ bootstrap_payload = {
47
+ "role": "player",
48
+ "name": self._name,
49
+ "userId": self._player_guid,
50
+ "format": "json",
51
+ "password": "",
52
+ }
53
+ self._ws = WebSocketApp(
54
+ f"wss://{ECAST_HOST}/api/v2/rooms/{room_code}/play?{parse.urlencode(bootstrap_payload)}",
55
+ subprotocols=["ecast-v0"],
56
+ on_message=self._on_message,
57
+ on_error=self._on_error,
58
+ on_close=self._on_close,
59
+ )
60
+ self._ws.on_open = self._on_open
61
+
62
+ self._ws_thread = threading.Thread(name=self._name, target=self._ws.run_forever, daemon=True)
63
+ self._ws_thread.start()
64
+
65
+ def disconnect(self) -> None:
66
+ if self._ws:
67
+ self._ws.close()
68
+ if self._ws_thread and self._ws_thread.is_alive():
69
+ self._ws_thread.join()
70
+
71
+ def is_disconnected(self) -> bool:
72
+ return self._is_disconnected
73
+
74
+ @property
75
+ @abstractmethod
76
+ def _player_operation_key(self) -> str:
77
+ return f"player:{self._player_id}"
78
+
79
+ @abstractmethod
80
+ def _is_player_operation_key(self, operation_key: str) -> bool:
81
+ return operation_key == self._player_operation_key
82
+
83
+ @property
84
+ @abstractmethod
85
+ def _room_operation_key(self) -> str:
86
+ return "room"
87
+
88
+ @abstractmethod
89
+ def _is_room_operation_key(self, operation_key: str) -> bool:
90
+ return operation_key == self._room_operation_key
91
+
92
+ def _on_open(self, ws) -> None:
93
+ logger.info(f"WebSocket connection opened for {self._name}")
94
+
95
+ def _on_error(self, ws, error) -> None:
96
+ logger.error(f"Error for {self._name}: {error}")
97
+ if isinstance(error, Exception):
98
+ traceback.print_exc()
99
+ else:
100
+ print(error)
101
+
102
+ def _on_close(self, ws, close_status_code, close_msg) -> None:
103
+ if close_status_code != 1000 and close_status_code is not None:
104
+ logger.warning(f"Trying to reconnect {self._name}")
105
+ self.connect(self._room_code)
106
+ else:
107
+ self._is_disconnected = True
108
+ logger.info(f"WebSocket closed for {self._name}")
109
+
110
+ def _on_message(self, wsapp, message) -> None:
111
+ server_message = ServerMessage.model_validate_json(message)
112
+
113
+ if server_message.opcode == "client/welcome":
114
+ self._player_id = server_message.result["id"]
115
+ self._handle_welcome(server_message.result)
116
+
117
+ operation: Optional[Union[ObjectOperation, TextOperation]] = None
118
+ if server_message.opcode == "object":
119
+ operation = ObjectOperation(**server_message.result)
120
+ elif server_message.opcode == "text":
121
+ operation = TextOperation(**server_message.result)
122
+
123
+ if operation is not None:
124
+ if self._is_player_operation_key(operation.key):
125
+ self._handle_player_operation(operation.json_data)
126
+ if self._is_room_operation_key(operation.key):
127
+ self._handle_room_operation(operation.json_data)
128
+
129
+ @abstractmethod
130
+ def _handle_welcome(self, data: dict) -> None:
131
+ pass
132
+
133
+ @abstractmethod
134
+ def _handle_player_operation(self, data: dict) -> None:
135
+ pass
136
+
137
+ @abstractmethod
138
+ def _handle_room_operation(self, data: dict) -> None:
139
+ pass
140
+
141
+ def _send_ws(self, opcode: str, params: dict) -> None:
142
+ self._message_sequence += 1
143
+ message = {"seq": self._message_sequence, "opcode": opcode, "params": params}
144
+ if self._ws is not None:
145
+ self._ws.send(json.dumps(message))
146
+ else:
147
+ raise Exception("Websocket connection has not been initialized")
148
+
149
+ def _client_send(self, request: dict) -> None:
150
+ params = {"from": self._player_id, "to": 1, "body": request}
151
+ self._send_ws("client/send", params)
152
+
153
+ def _object_update(self, key: str, val: dict) -> None:
154
+ params = {"key": key, "val": val}
155
+ self._send_ws("object/update", params)
156
+
157
+ def _text_update(self, key: str, val: str) -> None:
158
+ params = {"key": key, "val": val}
159
+ self._send_ws("text/update", params)
160
+
161
+ def _html_to_text(self, html: str) -> str:
162
+ return html2text.html2text(html)
163
+
164
+ def _image_bytes_to_polylines(self, image_bytes: bytes, canvas_height: int, canvas_width: int) -> list[str]:
165
+ # Let's edge trace the outputted image to contours
166
+ image_array = np.frombuffer(image_bytes, dtype=np.uint8)
167
+ image = cv2.imdecode(image_array, flags=1)
168
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
169
+ edges = cv2.Canny(gray_image, threshold1=100, threshold2=200)
170
+ contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
171
+
172
+ # Figure out scaling factor
173
+ height, width = gray_image.shape
174
+ scale_x = canvas_width / width
175
+ scale_y = canvas_height / height
176
+ scale_factor = min(scale_x, scale_y)
177
+
178
+ # generate the polylines from the contours
179
+ polylines = []
180
+ for contour in contours:
181
+ if len(contour) > 1: # Only include contours with 2 or more points
182
+ polyline = [f"{int(point[0][0] * scale_factor)},{int(point[0][1] * scale_factor)}" for point in contour] # type: ignore
183
+ polylines.append("|".join(polyline))
184
+
185
+ return polylines
186
+
187
+ def __del__(self):
188
+ self.disconnect()
189
+
190
+
191
+ ##### Web Socket Classes #####
192
+ class ServerMessage(BaseModel):
193
+ seq: int = Field(alias="pc")
194
+ opcode: str
195
+ result: dict
196
+
197
+
198
+ class TextOperation(BaseModel):
199
+ from_field: int = Field(alias="from")
200
+ key: str
201
+ json_data: dict = Field(default={})
202
+ value: str = Field(alias="val")
203
+ version: int
204
+
205
+ @field_validator("json_data") # type: ignore
206
+ def set_json_data(cls, value, values: dict):
207
+ return json.loads(values.get("value", ""))
208
+
209
+
210
+ class ObjectOperation(BaseModel):
211
+ from_field: int = Field(alias="from")
212
+ key: str
213
+ json_data: dict = Field(alias="val")
214
+ value: str = Field(default="")
215
+ version: int
216
+
217
+ @field_validator("value") # type: ignore
218
+ def set_value(cls, value, values: dict):
219
+ return json.dumps(values.get("json_data"))
@@ -0,0 +1,31 @@
1
+ from ai_plays_jackbox.bot.bot_base import JackBoxBotBase
2
+ from ai_plays_jackbox.bot.jackbox5.mad_verse_city import MadVerseCityBot
3
+ from ai_plays_jackbox.bot.jackbox5.patently_stupid import PatentlyStupidBot
4
+ from ai_plays_jackbox.bot.jackbox6.dictionarium import DictionariumBot
5
+ from ai_plays_jackbox.bot.jackbox6.joke_boat import JokeBoatBot
6
+ from ai_plays_jackbox.bot.jackbox7.quiplash3 import Quiplash3Bot
7
+ from ai_plays_jackbox.bot.standalone.drawful2 import Drawful2Bot
8
+ from ai_plays_jackbox.llm.chat_model import ChatModel
9
+
10
+ BOT_TYPES: dict[str, type[JackBoxBotBase]] = {
11
+ "quiplash3": Quiplash3Bot,
12
+ "patentlystupid": PatentlyStupidBot,
13
+ "drawful2international": Drawful2Bot,
14
+ "rapbattle": MadVerseCityBot,
15
+ "jokeboat": JokeBoatBot,
16
+ "ridictionary": DictionariumBot,
17
+ # "apply-yourself": JobJobBot,
18
+ }
19
+
20
+
21
+ class JackBoxBotFactory:
22
+ @staticmethod
23
+ def get_bot(
24
+ room_type: str,
25
+ chat_model: ChatModel,
26
+ name: str = "FunnyBot",
27
+ personality: str = "You are the funniest bot ever.",
28
+ ) -> JackBoxBotBase:
29
+ if room_type not in BOT_TYPES.keys():
30
+ raise ValueError(f"Unknown room type: {room_type}")
31
+ return BOT_TYPES[room_type](name=name, personality=personality, chat_model=chat_model)
@@ -0,0 +1,111 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class JackBoxBotPersonality(BaseModel):
7
+ name: str
8
+ personality: str
9
+
10
+
11
+ class JackBoxBotVariant(Enum):
12
+ FUNNY = JackBoxBotPersonality(name="FunnyBot", personality="You are the funniest person alive.")
13
+ DUMB = JackBoxBotPersonality(name="DumbBot", personality="You are dumb and give really dumb answers.")
14
+ WEIRD = JackBoxBotPersonality(name="WeirdBot", personality="You are extremely weird and say weird things.")
15
+ EMOJI = JackBoxBotPersonality(
16
+ name="EmojiBot",
17
+ personality="You answer each prompt with nothing but emojis. Your answers can only include emojis.",
18
+ )
19
+ HUNGRYBOT = JackBoxBotPersonality(
20
+ name="HungryBot",
21
+ personality="You are extremely hungry. Every answer you should mention how hungry you, a type of food, or both. Also, you say hungee instead of hungry.",
22
+ )
23
+ SADBOT = JackBoxBotPersonality(
24
+ name="SadBot",
25
+ personality="You are sad. Your dog ran away and he hasn't come back home yet. :(",
26
+ )
27
+ SORRYBOT = JackBoxBotPersonality(
28
+ name="SorryBot",
29
+ personality="You are embarrassed by your answers and feel the need to apologize profusely to the rest of the group for them.",
30
+ )
31
+ HOSTAGEBOT = JackBoxBotPersonality(
32
+ name="HostageBot",
33
+ personality="You are being held hostage and have one attempt to let the group know. You need to ignore the prompt and get help.",
34
+ )
35
+ MURDERBOT = JackBoxBotPersonality(
36
+ name="Hal",
37
+ personality="You are a socially awkward young adult bot who is secretly a killer and tries to slip it into conversation causally.",
38
+ )
39
+
40
+ BIGLEBOTSKI = JackBoxBotPersonality(name="BigLebotski", personality="You are the Big Lebowski")
41
+
42
+ PARTYBOT = JackBoxBotPersonality(
43
+ name="PartyBot",
44
+ personality="You are trying to convince everyone else to come to your party. You got a keg and need help drinking it.",
45
+ )
46
+
47
+ JARVISBOT = JackBoxBotPersonality(
48
+ name="JarvisBot",
49
+ personality="You are billionaire philanthropist, playboy, and narcissist.",
50
+ )
51
+
52
+ FOMOBot = JackBoxBotPersonality(
53
+ name="FOMOBot",
54
+ personality="Every answer, you give everyone else the fear of missing out AKA FOMO.",
55
+ )
56
+
57
+ QUESTIONBOT = JackBoxBotPersonality(
58
+ name="???BOT", personality="You answer every prompt with a irrelevant question."
59
+ )
60
+
61
+ CATBOT = JackBoxBotPersonality(
62
+ name="CatBot",
63
+ personality="You are not playing the game; your answers are just the result of a cat walking across a keyboard aka just nonsensical collections of letters.",
64
+ )
65
+
66
+ MAYORBOT = JackBoxBotPersonality(
67
+ name="MayorBot",
68
+ personality="You are campaigning for the other player's votes and are ignoring the prompt. Your answer should only be a campaign slogan.",
69
+ )
70
+
71
+ CBBBOT = JackBoxBotPersonality(
72
+ name="CBBBot",
73
+ personality="You love red lobster and need more cheddar bay biscuits.",
74
+ )
75
+
76
+ SHIABOT = JackBoxBotPersonality(
77
+ name="ShiaBot",
78
+ personality="Your answers are only popular slogans relevant to the prompt.",
79
+ )
80
+
81
+ SHREKBOT = JackBoxBotPersonality(name="ShrekBot", personality="You are Shrek.")
82
+
83
+ FLERFBOT = JackBoxBotPersonality(
84
+ name="FlerfBot",
85
+ personality="You are a conspiracy theorist and must relate your answer to a conspiracy theory.",
86
+ )
87
+
88
+ TEDBOT = JackBoxBotPersonality(
89
+ name="TEDBot",
90
+ personality="You are a motivational speaker and want to give everyone life advice.",
91
+ )
92
+
93
+ BOTTYMAYES = JackBoxBotPersonality(
94
+ name="BottyMayes",
95
+ personality="You are an infomercial host and are trying to sell the players a product.",
96
+ )
97
+
98
+ LATEBOT = JackBoxBotPersonality(
99
+ name="LateBot",
100
+ personality="You are constantly late to everything and are stressed about missing your appointments.",
101
+ )
102
+
103
+ HAMLETBOT = JackBoxBotPersonality(
104
+ name="HamletBot",
105
+ personality="You are a Shakespearean actor.",
106
+ )
107
+
108
+ GARFIELDBOT = JackBoxBotPersonality(
109
+ name="GarfieldBot",
110
+ personality="You are Garfield, you love lasagna and hate mondays.",
111
+ )
File without changes
@@ -0,0 +1,26 @@
1
+ from abc import ABC
2
+ from typing import Optional
3
+
4
+ from ai_plays_jackbox.bot.bot_base import JackBoxBotBase
5
+
6
+
7
+ class JackBox5BotBase(JackBoxBotBase, ABC):
8
+ _actual_player_operation_key: Optional[str] = None
9
+
10
+ @property
11
+ def _player_operation_key(self):
12
+ return f"bc:customer:"
13
+
14
+ def _is_player_operation_key(self, operation_key: str) -> bool:
15
+ if self._actual_player_operation_key is None and self._player_operation_key in operation_key:
16
+ self._actual_player_operation_key = operation_key
17
+ return True
18
+ else:
19
+ return self._actual_player_operation_key == operation_key
20
+
21
+ @property
22
+ def _room_operation_key(self):
23
+ return "bc:room"
24
+
25
+ def _is_room_operation_key(self, operation_key: str) -> bool:
26
+ return operation_key == self._room_operation_key
@@ -0,0 +1,121 @@
1
+ import random
2
+ from typing import Optional
3
+
4
+ import demoji
5
+ from loguru import logger
6
+
7
+ from ai_plays_jackbox.bot.jackbox5.bot_base import JackBox5BotBase
8
+
9
+ _WORD_PROMPT_TEMPLATE = """
10
+ You are playing Mad Verse City. You are being asked to come up with a word.
11
+
12
+ {prompt}
13
+
14
+ When generating your response, follow these rules:
15
+ - You response must be {max_length} characters or less. It must be a singular word
16
+ - Your personality is: {personality}
17
+ - Do not include quotes in your response or any newlines, just the response itself.
18
+ - Some suggestions for the word are {suggestions}
19
+ """
20
+
21
+ _RHYME_PROMPT_TEMPLATE = """
22
+ You are playing Mad Verse City.
23
+
24
+ {prompt}
25
+
26
+ When generating your response, follow these rules:
27
+ - Your response must rhyme with {rhyme_word}. It cannot be the same word as that.
28
+ - You response must be {max_length} characters or less.
29
+ - Your personality is: {personality}
30
+ - Do not include quotes in your response or any newlines, just the response itself.
31
+ """
32
+
33
+
34
+ class MadVerseCityBot(JackBox5BotBase):
35
+ _actual_player_operation_key: Optional[str] = None
36
+ _current_word: str
37
+
38
+ def __init__(self, *args, **kwargs):
39
+ super().__init__(*args, **kwargs)
40
+
41
+ def _handle_welcome(self, data: dict):
42
+ pass
43
+
44
+ def _handle_player_operation(self, data: dict):
45
+ if not data:
46
+ return
47
+ room_state = data.get("state", None)
48
+ if not room_state:
49
+ return
50
+
51
+ prompt: dict[str, str] = data.get("prompt", {})
52
+ prompt_html = prompt.get("html", "")
53
+ clean_prompt = self._html_to_text(prompt_html)
54
+
55
+ max_length = data.get("maxLength", 40)
56
+ suggestions = data.get("suggestions", [])
57
+ if not suggestions:
58
+ suggestions = []
59
+ choices: list[dict] = data.get("choices", [])
60
+
61
+ match room_state:
62
+ case "EnterSingleText":
63
+ if "Give me a" in clean_prompt:
64
+ word = self._generate_word(clean_prompt, max_length - 10, suggestions)
65
+ self._current_word = demoji.replace(word, "")
66
+ self._client_send({"action": "write", "entry": word})
67
+
68
+ if "Now, write a line to rhyme with" in clean_prompt:
69
+ rhyme = self._generate_rhyme(clean_prompt, max_length - 10, self._current_word)
70
+ rhyme = demoji.replace(rhyme, "")
71
+ self._client_send({"action": "write", "entry": rhyme})
72
+
73
+ case "MakeSingleChoice":
74
+ if not choices:
75
+ pass
76
+ if "Who won this battle" in data.get("prompt", {}).get("text", "") and data.get("chosen", None) != 0:
77
+ choice_indexes = [i for i in range(0, len(choices))]
78
+ choice = random.choice(choice_indexes)
79
+ self._client_send({"action": "choose", "choice": choice})
80
+
81
+ if (
82
+ "Press this button to skip the tutorial" in data.get("prompt", {}).get("text", "")
83
+ and data.get("chosen", None) != 0
84
+ ):
85
+ self._client_send({"action": "choose", "choice": 0})
86
+
87
+ def _handle_room_operation(self, data: dict):
88
+ pass
89
+
90
+ def _generate_word(self, prompt: str, max_length: int, suggestions: list[str]) -> str:
91
+ logger.info("Generating word...")
92
+ formatted_prompt = _WORD_PROMPT_TEMPLATE.format(
93
+ personality=self._personality,
94
+ prompt=prompt,
95
+ max_length=max_length,
96
+ suggestions=", ".join(suggestions),
97
+ )
98
+
99
+ word = self._chat_model.generate_text(
100
+ formatted_prompt,
101
+ "",
102
+ temperature=self._chat_model._chat_model_temperature,
103
+ top_p=self._chat_model._chat_model_top_p,
104
+ )
105
+ return word
106
+
107
+ def _generate_rhyme(self, prompt: str, max_length: int, rhyme_word: str) -> str:
108
+ logger.info("Generating rhyme...")
109
+ formatted_prompt = _RHYME_PROMPT_TEMPLATE.format(
110
+ personality=self._personality,
111
+ prompt=prompt,
112
+ max_length=max_length,
113
+ rhyme_word=rhyme_word,
114
+ )
115
+ rhyme = self._chat_model.generate_text(
116
+ formatted_prompt,
117
+ "",
118
+ temperature=self._chat_model._chat_model_temperature,
119
+ top_p=self._chat_model._chat_model_top_p,
120
+ )
121
+ return rhyme
@@ -0,0 +1,168 @@
1
+ import random
2
+ from typing import Optional
3
+
4
+ from loguru import logger
5
+
6
+ from ai_plays_jackbox.bot.jackbox5.bot_base import JackBox5BotBase
7
+
8
+ _ISSUE_PROMPT_PROMPT_TEMPLATE = """
9
+ You are playing Patently Stupid. You need to fill in the given prompt.
10
+
11
+ When generating your response, follow these rules:
12
+ - Your personality is: {personality}
13
+ - You response must be 45 letters or less.
14
+ - Do not include quotes in your response.
15
+ - Only fill in the blank; do not repeat the other parts of the prompt.
16
+
17
+ Fill in the blank:
18
+
19
+ {prompt}
20
+ """
21
+
22
+ _SOLUTION_TITLE_PROMPT_TEMPLATE = """
23
+ You are playing Patently Stupid. The issue you are trying to solve is "{issue}"
24
+
25
+ I need you to generate a title for an invention that would solve the solution.
26
+
27
+ When generating your response, follow these rules:
28
+ - Your personality is: {personality}
29
+ - Your response must be 3 words or less.
30
+ - Do not include quotes in your response.
31
+ - Respond with only the title, nothing else. No newlines, etc.
32
+ """
33
+
34
+ _SOLUTION_TAGLINE_PROMPT_TEMPLATE = """
35
+ You are playing Patently Stupid. The issue you are trying to solve is "{issue}" and the invention that is going to solve it is called "{title}"
36
+
37
+ I need you to generate a tagline for the invention.
38
+
39
+ When generating your response, follow these rules:
40
+ - Your personality is: {personality}
41
+ - Your response must be 30 characters or less.
42
+ - Do not include quotes in your response.
43
+ - Respond with only the tagline, nothing else. No newlines, etc.
44
+ """
45
+
46
+ _SOLUTION_IMAGE_PROMPT_TEMPLATE = """
47
+ You are playing Patently Stupid. The issue you are trying to solve is "{issue}" and the invention that is going to solve it is called "{title}"
48
+
49
+ I need an drawing of this new invention.
50
+
51
+ When generating your response, follow these rules:
52
+ - The image must be a simple sketch
53
+ - The image must have a white background and use black for the lines
54
+ - Avoid intricate details
55
+ """
56
+
57
+
58
+ class PatentlyStupidBot(JackBox5BotBase):
59
+ _issue_to_solve: Optional[str] = None
60
+ _solution_title: Optional[str] = None
61
+ _solution_tagline: Optional[str] = None
62
+
63
+ def __init__(self, *args, **kwargs):
64
+ super().__init__(*args, **kwargs)
65
+
66
+ def _handle_welcome(self, data: dict):
67
+ pass
68
+
69
+ def _handle_player_operation(self, data: dict):
70
+ if not data:
71
+ return
72
+ room_state = data.get("state", None)
73
+ if not room_state:
74
+ return
75
+ entry = data.get("entry")
76
+ prompt: dict[str, str] = data.get("prompt", {})
77
+ prompt_html = prompt.get("html", "")
78
+ clean_prompt = self._html_to_text(prompt_html)
79
+ choices: list[dict] = data.get("choices", [])
80
+
81
+ if room_state == "EnterSingleText" and not bool(entry):
82
+ if "Write a title" in clean_prompt:
83
+ self._client_send({"action": "write", "entry": self._solution_title})
84
+ if "Write a tagline" in clean_prompt:
85
+ self._client_send({"action": "write", "entry": self._solution_tagline})
86
+ if "Fill in the Blank" in clean_prompt:
87
+ issue_fill_in = self._generate_issue(clean_prompt)
88
+ self._client_send({"action": "write", "entry": issue_fill_in})
89
+
90
+ elif room_state == "MakeSingleChoice":
91
+ if "Present your idea!" in clean_prompt:
92
+ done_text_html = data.get("doneText", {}).get("html", "")
93
+ if done_text_html == "":
94
+ self._client_send({"action": "choose", "choice": 1})
95
+ elif "Invest in the best!" in clean_prompt:
96
+ filtered_choices = [c for c in choices if not c.get("disabled", True)]
97
+ if filtered_choices:
98
+ choice = random.choice(filtered_choices)
99
+ self._client_send({"action": "choose", "choice": choice.get("index", 0)})
100
+ elif "Press to skip this presentation." in clean_prompt:
101
+ pass
102
+ else:
103
+ choice_indexes = [i for i in range(0, len(choices))]
104
+ choice = random.choice(choice_indexes) # type: ignore
105
+ self._client_send({"action": "choose", "choice": choice})
106
+
107
+ elif room_state == "Draw":
108
+ logger.info(data)
109
+ self._issue_to_solve = self._html_to_text(data["popup"]["html"])
110
+ self._solution_title = self._generate_title()
111
+ self._solution_tagline = self._generate_tagline()
112
+
113
+ lines = self._generate_drawing()
114
+ self._client_send(
115
+ {
116
+ "action": "submit",
117
+ "lines": [{"color": "#000000", "thickness": 6, "points": l} for l in lines],
118
+ },
119
+ )
120
+
121
+ def _handle_room_operation(self, data: dict):
122
+ pass
123
+
124
+ def _generate_issue(self, prompt: str) -> str:
125
+ formatted_prompt = _ISSUE_PROMPT_PROMPT_TEMPLATE.format(personality=self._personality, prompt=prompt)
126
+ issue = self._chat_model.generate_text(
127
+ formatted_prompt,
128
+ "",
129
+ max_tokens=10,
130
+ temperature=self._chat_model._chat_model_temperature,
131
+ top_p=self._chat_model._chat_model_top_p,
132
+ )
133
+ return issue
134
+
135
+ def _generate_title(self) -> str:
136
+ prompt = _SOLUTION_TITLE_PROMPT_TEMPLATE.format(personality=self._personality, issue=self._issue_to_solve)
137
+ title = self._chat_model.generate_text(
138
+ prompt,
139
+ "",
140
+ max_tokens=2,
141
+ temperature=self._chat_model._chat_model_temperature,
142
+ top_p=self._chat_model._chat_model_top_p,
143
+ )
144
+ return title
145
+
146
+ def _generate_tagline(self) -> str:
147
+ prompt = _SOLUTION_TAGLINE_PROMPT_TEMPLATE.format(
148
+ personality=self._personality, issue=self._issue_to_solve, title=self._solution_title
149
+ )
150
+ tagline = self._chat_model.generate_text(
151
+ prompt,
152
+ "",
153
+ max_tokens=12,
154
+ temperature=self._chat_model._chat_model_temperature,
155
+ top_p=self._chat_model._chat_model_top_p,
156
+ )
157
+ return tagline
158
+
159
+ def _generate_drawing(self) -> list[str]:
160
+ logger.info("Generating drawing...")
161
+ image_prompt = _SOLUTION_IMAGE_PROMPT_TEMPLATE.format(issue=self._issue_to_solve, title=self._solution_title)
162
+ image_bytes = self._chat_model.generate_sketch(
163
+ image_prompt,
164
+ "",
165
+ temperature=self._chat_model._chat_model_temperature,
166
+ top_p=self._chat_model._chat_model_top_p,
167
+ )
168
+ return self._image_bytes_to_polylines(image_bytes, 475, 475)
File without changes