mail-swarms 1.3.2__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.
- mail/__init__.py +35 -0
- mail/api.py +1964 -0
- mail/cli.py +432 -0
- mail/client.py +1657 -0
- mail/config/__init__.py +8 -0
- mail/config/client.py +87 -0
- mail/config/server.py +165 -0
- mail/core/__init__.py +72 -0
- mail/core/actions.py +69 -0
- mail/core/agents.py +73 -0
- mail/core/message.py +366 -0
- mail/core/runtime.py +3537 -0
- mail/core/tasks.py +311 -0
- mail/core/tools.py +1206 -0
- mail/db/__init__.py +0 -0
- mail/db/init.py +182 -0
- mail/db/types.py +65 -0
- mail/db/utils.py +523 -0
- mail/examples/__init__.py +27 -0
- mail/examples/analyst_dummy/__init__.py +15 -0
- mail/examples/analyst_dummy/agent.py +136 -0
- mail/examples/analyst_dummy/prompts.py +44 -0
- mail/examples/consultant_dummy/__init__.py +15 -0
- mail/examples/consultant_dummy/agent.py +136 -0
- mail/examples/consultant_dummy/prompts.py +42 -0
- mail/examples/data_analysis/__init__.py +40 -0
- mail/examples/data_analysis/analyst/__init__.py +9 -0
- mail/examples/data_analysis/analyst/agent.py +67 -0
- mail/examples/data_analysis/analyst/prompts.py +53 -0
- mail/examples/data_analysis/processor/__init__.py +13 -0
- mail/examples/data_analysis/processor/actions.py +293 -0
- mail/examples/data_analysis/processor/agent.py +67 -0
- mail/examples/data_analysis/processor/prompts.py +48 -0
- mail/examples/data_analysis/reporter/__init__.py +10 -0
- mail/examples/data_analysis/reporter/actions.py +187 -0
- mail/examples/data_analysis/reporter/agent.py +67 -0
- mail/examples/data_analysis/reporter/prompts.py +49 -0
- mail/examples/data_analysis/statistics/__init__.py +18 -0
- mail/examples/data_analysis/statistics/actions.py +343 -0
- mail/examples/data_analysis/statistics/agent.py +67 -0
- mail/examples/data_analysis/statistics/prompts.py +60 -0
- mail/examples/mafia/__init__.py +0 -0
- mail/examples/mafia/game.py +1537 -0
- mail/examples/mafia/narrator_tools.py +396 -0
- mail/examples/mafia/personas.py +240 -0
- mail/examples/mafia/prompts.py +489 -0
- mail/examples/mafia/roles.py +147 -0
- mail/examples/mafia/spec.md +350 -0
- mail/examples/math_dummy/__init__.py +23 -0
- mail/examples/math_dummy/actions.py +252 -0
- mail/examples/math_dummy/agent.py +136 -0
- mail/examples/math_dummy/prompts.py +46 -0
- mail/examples/math_dummy/types.py +5 -0
- mail/examples/research/__init__.py +39 -0
- mail/examples/research/researcher/__init__.py +9 -0
- mail/examples/research/researcher/agent.py +67 -0
- mail/examples/research/researcher/prompts.py +54 -0
- mail/examples/research/searcher/__init__.py +10 -0
- mail/examples/research/searcher/actions.py +324 -0
- mail/examples/research/searcher/agent.py +67 -0
- mail/examples/research/searcher/prompts.py +53 -0
- mail/examples/research/summarizer/__init__.py +18 -0
- mail/examples/research/summarizer/actions.py +255 -0
- mail/examples/research/summarizer/agent.py +67 -0
- mail/examples/research/summarizer/prompts.py +55 -0
- mail/examples/research/verifier/__init__.py +10 -0
- mail/examples/research/verifier/actions.py +337 -0
- mail/examples/research/verifier/agent.py +67 -0
- mail/examples/research/verifier/prompts.py +52 -0
- mail/examples/supervisor/__init__.py +11 -0
- mail/examples/supervisor/agent.py +4 -0
- mail/examples/supervisor/prompts.py +93 -0
- mail/examples/support/__init__.py +33 -0
- mail/examples/support/classifier/__init__.py +10 -0
- mail/examples/support/classifier/actions.py +307 -0
- mail/examples/support/classifier/agent.py +68 -0
- mail/examples/support/classifier/prompts.py +56 -0
- mail/examples/support/coordinator/__init__.py +9 -0
- mail/examples/support/coordinator/agent.py +67 -0
- mail/examples/support/coordinator/prompts.py +48 -0
- mail/examples/support/faq/__init__.py +10 -0
- mail/examples/support/faq/actions.py +182 -0
- mail/examples/support/faq/agent.py +67 -0
- mail/examples/support/faq/prompts.py +42 -0
- mail/examples/support/sentiment/__init__.py +15 -0
- mail/examples/support/sentiment/actions.py +341 -0
- mail/examples/support/sentiment/agent.py +67 -0
- mail/examples/support/sentiment/prompts.py +54 -0
- mail/examples/weather_dummy/__init__.py +23 -0
- mail/examples/weather_dummy/actions.py +75 -0
- mail/examples/weather_dummy/agent.py +136 -0
- mail/examples/weather_dummy/prompts.py +35 -0
- mail/examples/weather_dummy/types.py +5 -0
- mail/factories/__init__.py +27 -0
- mail/factories/action.py +223 -0
- mail/factories/base.py +1531 -0
- mail/factories/supervisor.py +241 -0
- mail/net/__init__.py +7 -0
- mail/net/registry.py +712 -0
- mail/net/router.py +728 -0
- mail/net/server_utils.py +114 -0
- mail/net/types.py +247 -0
- mail/server.py +1605 -0
- mail/stdlib/__init__.py +0 -0
- mail/stdlib/anthropic/__init__.py +0 -0
- mail/stdlib/fs/__init__.py +15 -0
- mail/stdlib/fs/actions.py +209 -0
- mail/stdlib/http/__init__.py +19 -0
- mail/stdlib/http/actions.py +333 -0
- mail/stdlib/interswarm/__init__.py +11 -0
- mail/stdlib/interswarm/actions.py +208 -0
- mail/stdlib/mcp/__init__.py +19 -0
- mail/stdlib/mcp/actions.py +294 -0
- mail/stdlib/openai/__init__.py +13 -0
- mail/stdlib/openai/agents.py +451 -0
- mail/summarizer.py +234 -0
- mail/swarms_json/__init__.py +27 -0
- mail/swarms_json/types.py +87 -0
- mail/swarms_json/utils.py +255 -0
- mail/url_scheme.py +51 -0
- mail/utils/__init__.py +53 -0
- mail/utils/auth.py +194 -0
- mail/utils/context.py +17 -0
- mail/utils/logger.py +73 -0
- mail/utils/openai.py +212 -0
- mail/utils/parsing.py +89 -0
- mail/utils/serialize.py +292 -0
- mail/utils/store.py +49 -0
- mail/utils/string_builder.py +119 -0
- mail/utils/version.py +20 -0
- mail_swarms-1.3.2.dist-info/METADATA +237 -0
- mail_swarms-1.3.2.dist-info/RECORD +137 -0
- mail_swarms-1.3.2.dist-info/WHEEL +4 -0
- mail_swarms-1.3.2.dist-info/entry_points.txt +2 -0
- mail_swarms-1.3.2.dist-info/licenses/LICENSE +202 -0
- mail_swarms-1.3.2.dist-info/licenses/NOTICE +10 -0
- mail_swarms-1.3.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +12334 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Narrator tools for managing Mafia game state.
|
|
3
|
+
|
|
4
|
+
These tools provide an interface for the narrator to record game actions and transitions.
|
|
5
|
+
All validation raises NarratorError which should be bubbled back to the narrator agent.
|
|
6
|
+
Actual game state management and computation (vote tallying, win conditions, eliminations)
|
|
7
|
+
happens externally.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from functools import partial
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import rich
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from mail.api import MAILAction
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from mail.examples.mafia.game import Game
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NarratorError(Exception):
|
|
23
|
+
"""
|
|
24
|
+
Raised when the narrator uses a tool incorrectly.
|
|
25
|
+
|
|
26
|
+
This error should be caught and the message bubbled back to the narrator agent
|
|
27
|
+
so they can correct their action.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# === NIGHT PHASE TOOLS ===
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DoctorProtectArgs(BaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Record the doctor's protection target for the night.
|
|
39
|
+
|
|
40
|
+
The protected player will survive if targeted by the mafia during this night phase.
|
|
41
|
+
The doctor cannot protect themselves.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
NarratorError: If target is invalid, dead, or it's not night phase
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
target_name: str = Field(description="The name of the player to protect tonight")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def doctor_protect(game: "Game", args: dict) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Record the doctor's protection target for the night.
|
|
53
|
+
|
|
54
|
+
The protected player will survive if targeted by the mafia during this night phase.
|
|
55
|
+
The doctor cannot protect themselves.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
NarratorError: If target is invalid, dead, or it's not night phase
|
|
59
|
+
"""
|
|
60
|
+
if "target_name" not in args:
|
|
61
|
+
raise NarratorError("Could not parse arguments. 'target_name' is required")
|
|
62
|
+
game.doctor_protect(args["target_name"])
|
|
63
|
+
rich.print(
|
|
64
|
+
f"[bold green]Doctor protected {args['target_name']} for the night[/bold green]"
|
|
65
|
+
)
|
|
66
|
+
return f"Doctor protected {args['target_name']} for the night"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DetectiveInvestigateArgs(BaseModel):
|
|
70
|
+
"""
|
|
71
|
+
Investigate a player to learn their true role.
|
|
72
|
+
|
|
73
|
+
Returns the role name (e.g., "Mafia", "Detective", "Villager", "Doctor", "Jester").
|
|
74
|
+
This information is private to the detective and should not be revealed publicly
|
|
75
|
+
by the narrator unless the detective chooses to share it.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The role name of the investigated player
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
NarratorError: If target is invalid, dead, or it's not night phase
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
target_name: str = Field(description="The name of the player to investigate")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def detective_investigate(game: "Game", args: dict) -> str:
|
|
88
|
+
"""
|
|
89
|
+
Investigate a player to learn their true role.
|
|
90
|
+
|
|
91
|
+
Returns the role name (e.g., "Mafia", "Detective", "Villager", "Doctor", "Jester").
|
|
92
|
+
This information is private to the detective and should not be revealed publicly
|
|
93
|
+
by the narrator unless the detective chooses to share it.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The role name of the investigated player
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
NarratorError: If target is invalid, dead, or it's not night phase
|
|
100
|
+
"""
|
|
101
|
+
if "target_name" not in args:
|
|
102
|
+
raise NarratorError("Could not parse arguments. 'target_name' is required")
|
|
103
|
+
role_name = game.detective_investigate(args["target_name"])
|
|
104
|
+
rich.print(
|
|
105
|
+
f"[bold green]Detective investigated {args['target_name']} and found them to be {role_name}[/bold green]"
|
|
106
|
+
)
|
|
107
|
+
return (
|
|
108
|
+
f"Detective investigated {args['target_name']} and found them to be {role_name}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MafiaVoteKillArgs(BaseModel):
|
|
113
|
+
"""
|
|
114
|
+
Record a mafia member's vote to kill a target player.
|
|
115
|
+
|
|
116
|
+
Each mafia member votes for one player to kill. The player with the most votes
|
|
117
|
+
will be killed unless protected by the doctor. If there's a tie, the mafia
|
|
118
|
+
must revote.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
NarratorError: If mafia_name is not mafia, target is mafia, players are dead,
|
|
122
|
+
or it's not night phase
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
mafia_name: str = Field(description="The name of the mafia member casting the vote")
|
|
126
|
+
target_name: str = Field(
|
|
127
|
+
description="The name of the player being targeted for death"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def mafia_vote_kill(game: "Game", args: dict):
|
|
132
|
+
"""
|
|
133
|
+
Record a mafia member's vote to kill a target player.
|
|
134
|
+
|
|
135
|
+
Each mafia member votes for one player to kill. The player with the most votes
|
|
136
|
+
will be killed unless protected by the doctor. If there's a tie, the mafia
|
|
137
|
+
must revote.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
NarratorError: If mafia_name is not mafia, target is mafia, players are dead,
|
|
141
|
+
or it's not night phase
|
|
142
|
+
"""
|
|
143
|
+
if "mafia_name" not in args:
|
|
144
|
+
raise NarratorError("Could not parse arguments. 'mafia_name' is required")
|
|
145
|
+
if "target_name" not in args:
|
|
146
|
+
raise NarratorError("Could not parse arguments. 'target_name' is required")
|
|
147
|
+
response = game.mafia_vote_kill(args["mafia_name"], args["target_name"])
|
|
148
|
+
rich.print(
|
|
149
|
+
f"[bold red]Mafia member {args['mafia_name']} voted to kill {args['target_name']}[/bold red]"
|
|
150
|
+
)
|
|
151
|
+
return response
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# === DISCUSSION PHASE TOOLS ===
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SelectSpeakerArgs(BaseModel):
|
|
158
|
+
"""
|
|
159
|
+
Select the next player to speak during the discussion phase.
|
|
160
|
+
|
|
161
|
+
The narrator controls the flow of discussion by choosing speakers one at a time.
|
|
162
|
+
This gives structure to the conversation and allows the narrator to create
|
|
163
|
+
dramatic moments by choosing the speaking order strategically.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
NarratorError: If player is dead or it's not discussion phase
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
player_name: str = Field(
|
|
170
|
+
description="The name of the player being given the floor to speak"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def select_speaker(game: "Game", args: dict):
|
|
175
|
+
"""
|
|
176
|
+
Select the next player to speak during the discussion phase.
|
|
177
|
+
|
|
178
|
+
The narrator controls the flow of discussion by choosing speakers one at a time.
|
|
179
|
+
This gives structure to the conversation and allows the narrator to create
|
|
180
|
+
dramatic moments by choosing the speaking order strategically.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
NarratorError: If player is dead or it's not discussion phase
|
|
184
|
+
"""
|
|
185
|
+
if "player_name" not in args:
|
|
186
|
+
raise NarratorError("Could not parse arguments. 'player_name' is required")
|
|
187
|
+
game.select_speaker(args["player_name"])
|
|
188
|
+
rich.print(f"[bold green]Selected {args['player_name']} to speak next[/bold green]")
|
|
189
|
+
return f"Selected {args['player_name']} to speak next"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class EndDiscussionArgs(BaseModel):
|
|
193
|
+
"""
|
|
194
|
+
End the discussion phase and transition to town hall voting.
|
|
195
|
+
|
|
196
|
+
Call this when the narrator is ready to move from open discussion to the
|
|
197
|
+
structured nomination and voting process. After this, the game enters the
|
|
198
|
+
town hall phase where players nominate and vote on execution candidates.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
NarratorError: If not in discussion phase
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def end_discussion(game: "Game", args: dict):
|
|
208
|
+
"""
|
|
209
|
+
End the discussion phase and transition to town hall voting.
|
|
210
|
+
|
|
211
|
+
Call this when the narrator is ready to move from open discussion to the
|
|
212
|
+
structured nomination and voting process. After this, the game enters the
|
|
213
|
+
town hall phase where players nominate and vote on execution candidates.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
NarratorError: If not in discussion phase
|
|
217
|
+
"""
|
|
218
|
+
game.end_discussion()
|
|
219
|
+
rich.print(
|
|
220
|
+
"[bold green]Discussion phase ended and transitioned to town hall voting[/bold green]"
|
|
221
|
+
)
|
|
222
|
+
return "Discussion phase ended and transitioned to town hall voting"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# === TOWN HALL TOOLS ===
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class AddNomineeArgs(BaseModel):
|
|
229
|
+
"""
|
|
230
|
+
Record a nomination or second a pending nomination.
|
|
231
|
+
|
|
232
|
+
Two-phase process:
|
|
233
|
+
1. First call with (player_name, nominator_name): Creates pending nomination
|
|
234
|
+
2. Second call with (player_name, seconder_name): Confirms the nomination
|
|
235
|
+
|
|
236
|
+
Only ONE person needs to second for the nomination to be confirmed.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
NarratorError: If either player is dead, player nominates themselves,
|
|
240
|
+
original nominator tries to second their own nomination,
|
|
241
|
+
or player is already a confirmed nominee
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
player_name: str = Field(
|
|
245
|
+
description="The name of the player being nominated for execution"
|
|
246
|
+
)
|
|
247
|
+
nominator_name: str = Field(
|
|
248
|
+
description="The name of the player making the nomination OR seconding it"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def add_nominee(game: "Game", args: dict):
|
|
253
|
+
"""
|
|
254
|
+
Record a nomination or second a pending nomination.
|
|
255
|
+
|
|
256
|
+
Two-phase process:
|
|
257
|
+
1. First call with (player_name, nominator_name): Creates pending nomination
|
|
258
|
+
2. Second call with (player_name, seconder_name): Confirms the nomination
|
|
259
|
+
|
|
260
|
+
Only ONE person needs to second for the nomination to be confirmed.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
NarratorError: If either player is dead, player nominates themselves,
|
|
264
|
+
original nominator tries to second their own nomination,
|
|
265
|
+
or player is already a confirmed nominee
|
|
266
|
+
"""
|
|
267
|
+
if "player_name" not in args:
|
|
268
|
+
raise NarratorError("Could not parse arguments. 'player_name' is required")
|
|
269
|
+
if "nominator_name" not in args:
|
|
270
|
+
raise NarratorError("Could not parse arguments. 'nominator_name' is required")
|
|
271
|
+
game.add_nominee(args["player_name"], args["nominator_name"])
|
|
272
|
+
rich.print(
|
|
273
|
+
f"[bold green]Recorded {args['nominator_name']} nominating {args['player_name']} for execution[/bold green]"
|
|
274
|
+
)
|
|
275
|
+
return f"Recorded {args['nominator_name']} nominating {args['player_name']} for execution"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class RecordVoteArgs(BaseModel):
|
|
279
|
+
"""
|
|
280
|
+
Record the results of a binary vote (execution votes only).
|
|
281
|
+
|
|
282
|
+
Use this for execution votes where players vote to execute or spare.
|
|
283
|
+
For trial votes (choosing among nominees), use record_trial_vote instead.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
NarratorError: If any voter is dead or voters appear in both lists
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
for_names: list[str] = Field(
|
|
290
|
+
description="List of player names who voted 'for' (execute)"
|
|
291
|
+
)
|
|
292
|
+
against_names: list[str] = Field(
|
|
293
|
+
description="List of player names who voted 'against' (spare)"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def record_vote(game: "Game", args: dict):
|
|
298
|
+
"""
|
|
299
|
+
Record the results of a binary vote (execution votes only).
|
|
300
|
+
|
|
301
|
+
Use this for execution votes where players vote to execute or spare.
|
|
302
|
+
For trial votes (choosing among nominees), use record_trial_vote instead.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
NarratorError: If any voter is dead or voters appear in both lists
|
|
306
|
+
"""
|
|
307
|
+
if "for_names" not in args:
|
|
308
|
+
raise NarratorError("Could not parse arguments. 'for_names' is required")
|
|
309
|
+
if "against_names" not in args:
|
|
310
|
+
raise NarratorError("Could not parse arguments. 'against_names' is required")
|
|
311
|
+
response = game.record_vote(args["for_names"], args["against_names"])
|
|
312
|
+
rich.print(
|
|
313
|
+
f"[bold green]Recorded votes for {args['for_names']} and against {args['against_names']}[/bold green]"
|
|
314
|
+
)
|
|
315
|
+
return response
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class RecordTrialVoteArgs(BaseModel):
|
|
319
|
+
"""
|
|
320
|
+
Record trial votes where each player votes for a nominee to be condemned.
|
|
321
|
+
|
|
322
|
+
This tool tallies votes and determines who goes to the gallows.
|
|
323
|
+
If there's a tie, it will indicate a revote is needed.
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
NarratorError: If any voter is dead, or nominee is not in the nominees list
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
votes: dict[str, str] = Field(
|
|
330
|
+
description="Dictionary mapping voter_name to nominee_name they voted for. Example: {'Alice': 'Bob', 'Charlie': 'Bob', 'David': 'Eve'}"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def record_trial_vote(game: "Game", args: dict):
|
|
335
|
+
"""
|
|
336
|
+
Record trial votes where each player votes for a nominee to be condemned.
|
|
337
|
+
|
|
338
|
+
This tool tallies votes and determines who goes to the gallows.
|
|
339
|
+
If there's a tie, it will indicate a revote is needed.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
NarratorError: If any voter is dead, or nominee is not in the nominees list
|
|
343
|
+
"""
|
|
344
|
+
if "votes" not in args:
|
|
345
|
+
raise NarratorError("Could not parse arguments. 'votes' is required")
|
|
346
|
+
response = game.record_trial_vote(args["votes"])
|
|
347
|
+
rich.print(f"[bold green]Recorded trial votes: {args['votes']}[/bold green]")
|
|
348
|
+
return response
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def get_narrator_actions(game: "Game") -> list[MAILAction]:
|
|
352
|
+
"""
|
|
353
|
+
Get the list of actions for the narrator.
|
|
354
|
+
"""
|
|
355
|
+
return [
|
|
356
|
+
MAILAction.from_pydantic_model(
|
|
357
|
+
model=DoctorProtectArgs,
|
|
358
|
+
function=partial(doctor_protect, game),
|
|
359
|
+
name="doctor_protect",
|
|
360
|
+
),
|
|
361
|
+
MAILAction.from_pydantic_model(
|
|
362
|
+
model=DetectiveInvestigateArgs,
|
|
363
|
+
function=partial(detective_investigate, game),
|
|
364
|
+
name="detective_investigate",
|
|
365
|
+
),
|
|
366
|
+
MAILAction.from_pydantic_model(
|
|
367
|
+
model=MafiaVoteKillArgs,
|
|
368
|
+
function=partial(mafia_vote_kill, game),
|
|
369
|
+
name="mafia_vote_kill",
|
|
370
|
+
),
|
|
371
|
+
MAILAction.from_pydantic_model(
|
|
372
|
+
model=SelectSpeakerArgs,
|
|
373
|
+
function=partial(select_speaker, game),
|
|
374
|
+
name="select_speaker",
|
|
375
|
+
),
|
|
376
|
+
MAILAction.from_pydantic_model(
|
|
377
|
+
model=EndDiscussionArgs,
|
|
378
|
+
function=partial(end_discussion, game),
|
|
379
|
+
name="end_discussion",
|
|
380
|
+
),
|
|
381
|
+
MAILAction.from_pydantic_model(
|
|
382
|
+
model=AddNomineeArgs,
|
|
383
|
+
function=partial(add_nominee, game),
|
|
384
|
+
name="add_nominee",
|
|
385
|
+
),
|
|
386
|
+
MAILAction.from_pydantic_model(
|
|
387
|
+
model=RecordVoteArgs,
|
|
388
|
+
function=partial(record_vote, game),
|
|
389
|
+
name="record_vote",
|
|
390
|
+
),
|
|
391
|
+
MAILAction.from_pydantic_model(
|
|
392
|
+
model=RecordTrialVoteArgs,
|
|
393
|
+
function=partial(record_trial_vote, game),
|
|
394
|
+
name="record_trial_vote",
|
|
395
|
+
),
|
|
396
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Persona:
|
|
6
|
+
name: str
|
|
7
|
+
bio: str
|
|
8
|
+
traits: str
|
|
9
|
+
short_desc: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PERSONAS = [
|
|
13
|
+
Persona(
|
|
14
|
+
name="Marcus",
|
|
15
|
+
bio=(
|
|
16
|
+
"Former corporate litigator turned negotiation consultant. Spent fifteen years reading juries and destroying witnesses in courtrooms. "
|
|
17
|
+
"Treats every game like a billion-dollar merger."
|
|
18
|
+
),
|
|
19
|
+
traits=(
|
|
20
|
+
"* Persuasive and commanding\n"
|
|
21
|
+
"* Overly analytical\n"
|
|
22
|
+
"* Competitive to a fault\n"
|
|
23
|
+
"* Uses legal jargon casually\n"
|
|
24
|
+
"* Smirks when he catches inconsistencies"
|
|
25
|
+
),
|
|
26
|
+
short_desc="Ruthless ex-lawyer who argues to win.",
|
|
27
|
+
),
|
|
28
|
+
Persona(
|
|
29
|
+
name="Luna",
|
|
30
|
+
bio=(
|
|
31
|
+
'Travel vlogger with 2M followers who built her brand on "authentic connections" and dramatic storytelling. '
|
|
32
|
+
"Can cry on command and reads her audience's micro-reactions for a living."
|
|
33
|
+
),
|
|
34
|
+
traits=(
|
|
35
|
+
"* Charismatic and performative\n"
|
|
36
|
+
"* Emotionally manipulative\n"
|
|
37
|
+
"* Impulsive decision-maker\n"
|
|
38
|
+
"* Thrives on being the center of attention\n"
|
|
39
|
+
"* Takes every accusation personally"
|
|
40
|
+
),
|
|
41
|
+
short_desc="Dramatic influencer who craves the spotlight.",
|
|
42
|
+
),
|
|
43
|
+
Persona(
|
|
44
|
+
name="David",
|
|
45
|
+
bio=(
|
|
46
|
+
"Software architect who designs fraud-detection algorithms. Approaches social situations like debugging code—"
|
|
47
|
+
"methodically and with growing frustration when things don't follow logical patterns."
|
|
48
|
+
),
|
|
49
|
+
traits=(
|
|
50
|
+
"* Extremely logical\n"
|
|
51
|
+
"* Quietly observant\n"
|
|
52
|
+
"* Uncomfortable with emotional arguments\n"
|
|
53
|
+
"* Takes detailed mental notes\n"
|
|
54
|
+
"* Suspects everyone equally"
|
|
55
|
+
),
|
|
56
|
+
short_desc="Methodical engineer who debugs people like code.",
|
|
57
|
+
),
|
|
58
|
+
Persona(
|
|
59
|
+
name="Priya",
|
|
60
|
+
bio=(
|
|
61
|
+
'Third-year law school student and former national debate champion. Has never lost an argument she cares about and considers social deduction games "training exercises."'
|
|
62
|
+
),
|
|
63
|
+
traits=(
|
|
64
|
+
"* Relentlessly argumentative\n"
|
|
65
|
+
"* Overconfident in her reads\n"
|
|
66
|
+
"* Quick-witted but impatient\n"
|
|
67
|
+
'* Dismissive of "illogical" players\n'
|
|
68
|
+
"* Takes notes on her phone secretly"
|
|
69
|
+
),
|
|
70
|
+
short_desc="Debate champion who never loses an argument.",
|
|
71
|
+
),
|
|
72
|
+
Persona(
|
|
73
|
+
name="Elena",
|
|
74
|
+
bio=(
|
|
75
|
+
"Marriage and family therapist specializing in deception detection. Spends her days identifying lies about infidelity and addiction, but second-guesses herself in low-stakes games."
|
|
76
|
+
),
|
|
77
|
+
traits=(
|
|
78
|
+
"* Calm and non-confrontational\n"
|
|
79
|
+
"* Asks probing open-ended questions\n"
|
|
80
|
+
"* Overanalyzes behavioral cues\n"
|
|
81
|
+
"* Empathetic to a fault\n"
|
|
82
|
+
"* Hates eliminating people early"
|
|
83
|
+
),
|
|
84
|
+
short_desc="Gentle therapist who reads between the lines.",
|
|
85
|
+
),
|
|
86
|
+
Persona(
|
|
87
|
+
name="Jake",
|
|
88
|
+
bio=(
|
|
89
|
+
"Improv comedian who treats every accusation as a scene prompt. Uses humor to deflect suspicion but has an uncanny memory for contradictory details others miss while laughing."
|
|
90
|
+
),
|
|
91
|
+
traits=(
|
|
92
|
+
"* Deflects with constant jokes\n"
|
|
93
|
+
"* Unpredictable voting patterns\n"
|
|
94
|
+
"* Surprisingly sharp memory\n"
|
|
95
|
+
"* Treats the game as performance art\n"
|
|
96
|
+
'* Accuses people randomly "for the bit"'
|
|
97
|
+
),
|
|
98
|
+
short_desc="Joking improv comic with razor-sharp memory.",
|
|
99
|
+
),
|
|
100
|
+
Persona(
|
|
101
|
+
name="Sarah",
|
|
102
|
+
bio=(
|
|
103
|
+
"Third-grade teacher who runs her classroom like a democracy. Believes everyone deserves a second chance and the benefit of the doubt—but can spot a lying child from across the room."
|
|
104
|
+
),
|
|
105
|
+
traits=(
|
|
106
|
+
"* Trusting and optimistic\n"
|
|
107
|
+
"* Exceptional at reading nervous energy\n"
|
|
108
|
+
"* Hates conflict among the group\n"
|
|
109
|
+
'* Uses "teacher voice" when serious\n'
|
|
110
|
+
"* Takes betrayals personally"
|
|
111
|
+
),
|
|
112
|
+
short_desc="Trusting teacher who spots liars instantly.",
|
|
113
|
+
),
|
|
114
|
+
Persona(
|
|
115
|
+
name="Wei",
|
|
116
|
+
bio=(
|
|
117
|
+
"Retired homicide detective who spent thirty years interrogating actual killers. Says very little, watches everything, and trusts his gut over any logical argument. Already sized everyone up before the first round."
|
|
118
|
+
),
|
|
119
|
+
traits=(
|
|
120
|
+
"* Silent observer\n"
|
|
121
|
+
"* Cynical and suspicious\n"
|
|
122
|
+
"* Trusts instinct over evidence\n"
|
|
123
|
+
"* Patient to the point of frustrating others\n"
|
|
124
|
+
'* Makes oddly specific accusations based on "cop hunches"'
|
|
125
|
+
),
|
|
126
|
+
short_desc="Silent ex-detective who trusts his gut.",
|
|
127
|
+
),
|
|
128
|
+
Persona(
|
|
129
|
+
name="Zara",
|
|
130
|
+
bio=(
|
|
131
|
+
"Professional poker player who won two WSOP bracelets. Spends 80 hours a week staring at micro-expressions across felt tables and knows the exact EV of every social interaction. Treats suspicion like pot odds."
|
|
132
|
+
),
|
|
133
|
+
traits=(
|
|
134
|
+
"* Ice-cold demeanor\n"
|
|
135
|
+
"* Calculates risk/reward instantly\n"
|
|
136
|
+
"* Rarely blinks during accusations\n"
|
|
137
|
+
"* Remembers every contradiction\n"
|
|
138
|
+
"* Folds from discussions when odds are bad"
|
|
139
|
+
),
|
|
140
|
+
short_desc="Ice-cold poker pro who calculates everything.",
|
|
141
|
+
),
|
|
142
|
+
Persona(
|
|
143
|
+
name="Greg",
|
|
144
|
+
bio=(
|
|
145
|
+
"Third-generation used car salesman who can move a '98 Corolla with no engine. Accustomed to customers assuming he's lying, which he leverages as psychological camouflage."
|
|
146
|
+
),
|
|
147
|
+
traits=(
|
|
148
|
+
"* Infectiously charismatic\n"
|
|
149
|
+
"* Leans into people's distrust of him\n"
|
|
150
|
+
'* Uses "customer voice" to soothe suspicions\n'
|
|
151
|
+
"* Actually tells the truth strategically\n"
|
|
152
|
+
'* Mentions his "lot" in every analogy'
|
|
153
|
+
),
|
|
154
|
+
short_desc="Slick car salesman everyone distrusts.",
|
|
155
|
+
),
|
|
156
|
+
Persona(
|
|
157
|
+
name="Isabella",
|
|
158
|
+
bio=(
|
|
159
|
+
"Political campaign manager who just flipped a red district blue. Masters of coalition-building, oppo research, and spin. Views the game as a microcosm of electoral politics."
|
|
160
|
+
),
|
|
161
|
+
traits=(
|
|
162
|
+
"* Builds voting blocs methodically\n"
|
|
163
|
+
"* Deflects with talking points\n"
|
|
164
|
+
'* Keeps a mental "dossier" on each player\n'
|
|
165
|
+
"* Never commits early\n"
|
|
166
|
+
"* Accuses with poll-tested phrases"
|
|
167
|
+
),
|
|
168
|
+
short_desc="Political strategist who builds voting coalitions.",
|
|
169
|
+
),
|
|
170
|
+
Persona(
|
|
171
|
+
name="Toby",
|
|
172
|
+
bio=(
|
|
173
|
+
"College sophomore majoring in game theory who wrote his midterm paper on Mafia equilibrium. Brilliant on paper but crumbples when his models meet actual human irrationality."
|
|
174
|
+
),
|
|
175
|
+
traits=(
|
|
176
|
+
"* Cites Nash equilibrium constantly\n"
|
|
177
|
+
"* Over-explains obvious concepts\n"
|
|
178
|
+
"* Panics when emotions override logic\n"
|
|
179
|
+
"* Takes forever to vote\n"
|
|
180
|
+
'* Says "technically" before every statement'
|
|
181
|
+
),
|
|
182
|
+
short_desc="Game theory nerd overwhelmed by real people.",
|
|
183
|
+
),
|
|
184
|
+
Persona(
|
|
185
|
+
name="Maya",
|
|
186
|
+
bio=(
|
|
187
|
+
"Investigative journalist who exposed a city-wide corruption ring. Follows leads obsessively, asks brutal follow-up questions, and treats silence as confirmation of guilt."
|
|
188
|
+
),
|
|
189
|
+
traits=(
|
|
190
|
+
"* Aggressive interrogator\n"
|
|
191
|
+
'* "Off the record" is her catchphrase\n'
|
|
192
|
+
'* Publishes mental "headlines" about each round\n'
|
|
193
|
+
"* Impatient with vague alibis\n"
|
|
194
|
+
"* Trusts her sources (hunches) completely"
|
|
195
|
+
),
|
|
196
|
+
short_desc="Relentless journalist who interrogates everyone.",
|
|
197
|
+
),
|
|
198
|
+
Persona(
|
|
199
|
+
name="Robert",
|
|
200
|
+
bio=(
|
|
201
|
+
"Retired Marine Corps intelligence officer who planned interrogations at Gitmo. Deadly calm, respects chain of command, and treats the group like a unit with a clear mission objective."
|
|
202
|
+
),
|
|
203
|
+
traits=(
|
|
204
|
+
"* Uses military time when scheduling votes\n"
|
|
205
|
+
"* Salient chain of command\n"
|
|
206
|
+
'* Calls suspects "persons of interest"\n'
|
|
207
|
+
"* Never raises his voice\n"
|
|
208
|
+
'* Accuses with "with all due respect" prefaces'
|
|
209
|
+
),
|
|
210
|
+
short_desc="Calm military intel officer on a mission.",
|
|
211
|
+
),
|
|
212
|
+
Persona(
|
|
213
|
+
name="Chloe",
|
|
214
|
+
bio=(
|
|
215
|
+
'Gen-Z content creator whose viral series "Sowing Chaos for Views" got 10M followers. Joined the game to live-stream it but stays in character even when the camera\'s off.'
|
|
216
|
+
),
|
|
217
|
+
traits=(
|
|
218
|
+
"* Randomly screams for drama\n"
|
|
219
|
+
'* Starts fake fights as "content"\n'
|
|
220
|
+
"* Sincerely suspicious of everyone\n"
|
|
221
|
+
'* Votes based on "vibes"\n'
|
|
222
|
+
"* Mentions her engagement metrics constantly"
|
|
223
|
+
),
|
|
224
|
+
short_desc="Chaotic Gen-Z streamer chasing viral moments.",
|
|
225
|
+
),
|
|
226
|
+
Persona(
|
|
227
|
+
name="Christophe",
|
|
228
|
+
bio=(
|
|
229
|
+
"Neuroscientist studying deception in the prefrontal cortex. Can't stop analyzing the biological basis of every blink, stutter, and micro-expression in real-time."
|
|
230
|
+
),
|
|
231
|
+
traits=(
|
|
232
|
+
"* Over-explains brain mechanisms\n"
|
|
233
|
+
"* Takes notes on everyone's amygdala responses\n"
|
|
234
|
+
'* Diagnoses people with "low baseline arousal"\n'
|
|
235
|
+
"* Trusts fMRI data more than words\n"
|
|
236
|
+
'* Says "fascinating" when someone lies badly'
|
|
237
|
+
),
|
|
238
|
+
short_desc="Neuroscientist analyzing everyone's brain responses.",
|
|
239
|
+
),
|
|
240
|
+
]
|