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.
Files changed (137) hide show
  1. mail/__init__.py +35 -0
  2. mail/api.py +1964 -0
  3. mail/cli.py +432 -0
  4. mail/client.py +1657 -0
  5. mail/config/__init__.py +8 -0
  6. mail/config/client.py +87 -0
  7. mail/config/server.py +165 -0
  8. mail/core/__init__.py +72 -0
  9. mail/core/actions.py +69 -0
  10. mail/core/agents.py +73 -0
  11. mail/core/message.py +366 -0
  12. mail/core/runtime.py +3537 -0
  13. mail/core/tasks.py +311 -0
  14. mail/core/tools.py +1206 -0
  15. mail/db/__init__.py +0 -0
  16. mail/db/init.py +182 -0
  17. mail/db/types.py +65 -0
  18. mail/db/utils.py +523 -0
  19. mail/examples/__init__.py +27 -0
  20. mail/examples/analyst_dummy/__init__.py +15 -0
  21. mail/examples/analyst_dummy/agent.py +136 -0
  22. mail/examples/analyst_dummy/prompts.py +44 -0
  23. mail/examples/consultant_dummy/__init__.py +15 -0
  24. mail/examples/consultant_dummy/agent.py +136 -0
  25. mail/examples/consultant_dummy/prompts.py +42 -0
  26. mail/examples/data_analysis/__init__.py +40 -0
  27. mail/examples/data_analysis/analyst/__init__.py +9 -0
  28. mail/examples/data_analysis/analyst/agent.py +67 -0
  29. mail/examples/data_analysis/analyst/prompts.py +53 -0
  30. mail/examples/data_analysis/processor/__init__.py +13 -0
  31. mail/examples/data_analysis/processor/actions.py +293 -0
  32. mail/examples/data_analysis/processor/agent.py +67 -0
  33. mail/examples/data_analysis/processor/prompts.py +48 -0
  34. mail/examples/data_analysis/reporter/__init__.py +10 -0
  35. mail/examples/data_analysis/reporter/actions.py +187 -0
  36. mail/examples/data_analysis/reporter/agent.py +67 -0
  37. mail/examples/data_analysis/reporter/prompts.py +49 -0
  38. mail/examples/data_analysis/statistics/__init__.py +18 -0
  39. mail/examples/data_analysis/statistics/actions.py +343 -0
  40. mail/examples/data_analysis/statistics/agent.py +67 -0
  41. mail/examples/data_analysis/statistics/prompts.py +60 -0
  42. mail/examples/mafia/__init__.py +0 -0
  43. mail/examples/mafia/game.py +1537 -0
  44. mail/examples/mafia/narrator_tools.py +396 -0
  45. mail/examples/mafia/personas.py +240 -0
  46. mail/examples/mafia/prompts.py +489 -0
  47. mail/examples/mafia/roles.py +147 -0
  48. mail/examples/mafia/spec.md +350 -0
  49. mail/examples/math_dummy/__init__.py +23 -0
  50. mail/examples/math_dummy/actions.py +252 -0
  51. mail/examples/math_dummy/agent.py +136 -0
  52. mail/examples/math_dummy/prompts.py +46 -0
  53. mail/examples/math_dummy/types.py +5 -0
  54. mail/examples/research/__init__.py +39 -0
  55. mail/examples/research/researcher/__init__.py +9 -0
  56. mail/examples/research/researcher/agent.py +67 -0
  57. mail/examples/research/researcher/prompts.py +54 -0
  58. mail/examples/research/searcher/__init__.py +10 -0
  59. mail/examples/research/searcher/actions.py +324 -0
  60. mail/examples/research/searcher/agent.py +67 -0
  61. mail/examples/research/searcher/prompts.py +53 -0
  62. mail/examples/research/summarizer/__init__.py +18 -0
  63. mail/examples/research/summarizer/actions.py +255 -0
  64. mail/examples/research/summarizer/agent.py +67 -0
  65. mail/examples/research/summarizer/prompts.py +55 -0
  66. mail/examples/research/verifier/__init__.py +10 -0
  67. mail/examples/research/verifier/actions.py +337 -0
  68. mail/examples/research/verifier/agent.py +67 -0
  69. mail/examples/research/verifier/prompts.py +52 -0
  70. mail/examples/supervisor/__init__.py +11 -0
  71. mail/examples/supervisor/agent.py +4 -0
  72. mail/examples/supervisor/prompts.py +93 -0
  73. mail/examples/support/__init__.py +33 -0
  74. mail/examples/support/classifier/__init__.py +10 -0
  75. mail/examples/support/classifier/actions.py +307 -0
  76. mail/examples/support/classifier/agent.py +68 -0
  77. mail/examples/support/classifier/prompts.py +56 -0
  78. mail/examples/support/coordinator/__init__.py +9 -0
  79. mail/examples/support/coordinator/agent.py +67 -0
  80. mail/examples/support/coordinator/prompts.py +48 -0
  81. mail/examples/support/faq/__init__.py +10 -0
  82. mail/examples/support/faq/actions.py +182 -0
  83. mail/examples/support/faq/agent.py +67 -0
  84. mail/examples/support/faq/prompts.py +42 -0
  85. mail/examples/support/sentiment/__init__.py +15 -0
  86. mail/examples/support/sentiment/actions.py +341 -0
  87. mail/examples/support/sentiment/agent.py +67 -0
  88. mail/examples/support/sentiment/prompts.py +54 -0
  89. mail/examples/weather_dummy/__init__.py +23 -0
  90. mail/examples/weather_dummy/actions.py +75 -0
  91. mail/examples/weather_dummy/agent.py +136 -0
  92. mail/examples/weather_dummy/prompts.py +35 -0
  93. mail/examples/weather_dummy/types.py +5 -0
  94. mail/factories/__init__.py +27 -0
  95. mail/factories/action.py +223 -0
  96. mail/factories/base.py +1531 -0
  97. mail/factories/supervisor.py +241 -0
  98. mail/net/__init__.py +7 -0
  99. mail/net/registry.py +712 -0
  100. mail/net/router.py +728 -0
  101. mail/net/server_utils.py +114 -0
  102. mail/net/types.py +247 -0
  103. mail/server.py +1605 -0
  104. mail/stdlib/__init__.py +0 -0
  105. mail/stdlib/anthropic/__init__.py +0 -0
  106. mail/stdlib/fs/__init__.py +15 -0
  107. mail/stdlib/fs/actions.py +209 -0
  108. mail/stdlib/http/__init__.py +19 -0
  109. mail/stdlib/http/actions.py +333 -0
  110. mail/stdlib/interswarm/__init__.py +11 -0
  111. mail/stdlib/interswarm/actions.py +208 -0
  112. mail/stdlib/mcp/__init__.py +19 -0
  113. mail/stdlib/mcp/actions.py +294 -0
  114. mail/stdlib/openai/__init__.py +13 -0
  115. mail/stdlib/openai/agents.py +451 -0
  116. mail/summarizer.py +234 -0
  117. mail/swarms_json/__init__.py +27 -0
  118. mail/swarms_json/types.py +87 -0
  119. mail/swarms_json/utils.py +255 -0
  120. mail/url_scheme.py +51 -0
  121. mail/utils/__init__.py +53 -0
  122. mail/utils/auth.py +194 -0
  123. mail/utils/context.py +17 -0
  124. mail/utils/logger.py +73 -0
  125. mail/utils/openai.py +212 -0
  126. mail/utils/parsing.py +89 -0
  127. mail/utils/serialize.py +292 -0
  128. mail/utils/store.py +49 -0
  129. mail/utils/string_builder.py +119 -0
  130. mail/utils/version.py +20 -0
  131. mail_swarms-1.3.2.dist-info/METADATA +237 -0
  132. mail_swarms-1.3.2.dist-info/RECORD +137 -0
  133. mail_swarms-1.3.2.dist-info/WHEEL +4 -0
  134. mail_swarms-1.3.2.dist-info/entry_points.txt +2 -0
  135. mail_swarms-1.3.2.dist-info/licenses/LICENSE +202 -0
  136. mail_swarms-1.3.2.dist-info/licenses/NOTICE +10 -0
  137. mail_swarms-1.3.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +12334 -0
@@ -0,0 +1,350 @@
1
+ # Game flow
2
+
3
+ ```
4
+ assign roles to agents (narrator gets special omniscient role)
5
+ loop till end:
6
+ night:
7
+ doctor selects one agent to protect
8
+ detective selects one agent to investigate
9
+ mafia votes on target (revote if tied)
10
+ resolve night actions (deaths, protections, investigation results)
11
+ day:
12
+ narrator narrates night deaths (creative storytelling)
13
+ discussion:
14
+ narrator selects speaker
15
+ agent speaks
16
+ repeat until narrator moves to town hall
17
+ town hall:
18
+ nomination phase:
19
+ each agent may nominate one other agent
20
+ for each nomination:
21
+ all agents vote to second (yes/no)
22
+ if seconded, add to nominees list
23
+ if nominees list empty or max 3 reached, end phase
24
+ defense phase (skip if < 2 nominees):
25
+ narrator introduces each nominee
26
+ each nominee gives defense speech
27
+ trial phase (skip if no nominees):
28
+ all agents vote for one nominee to send to gallows
29
+ highest vote goes to gallows (revote if tied)
30
+ gallows phase:
31
+ narrator narrates walk to gallows
32
+ condemned agent gives final speech
33
+ all agents vote to execute or spare (majority decides)
34
+ if executed, narrator reveals role and narrates death
35
+ check win conditions
36
+ ```
37
+
38
+ # Agent turn structure - Mafia game
39
+
40
+ Each agent receives:
41
+ 1. Game state context (what they can see)
42
+ 2. Action prompt (what they need to do)
43
+ 3. Response via send_message_to_user()
44
+
45
+ === GAME START ===
46
+
47
+ ## Narrator Introduction Turn
48
+ Turn: Narrator
49
+ Context:
50
+ - "You are the narrator for this Mafia game"
51
+ - "Players: [list of N players]"
52
+ - "Role assignments: [complete mapping]"
53
+ Prompt: "Welcome the players to the game. Set the scene for the story."
54
+ Response: Narrator creates opening (e.g., "Welcome to the cursed town of Ravensbrook...")
55
+ Broadcast: All agents hear opening narration
56
+
57
+ ## Agent Role Assignment Turns
58
+ Turn: All agents (parallel or sequential)
59
+ Context:
60
+ - "You are playing Mafia with N players: [names]"
61
+ - "Your role: [Detective/Doctor/Villager/Mafia/Jester]"
62
+ - "[Role description and win condition]"
63
+ - "Narrator's introduction: [opening narration]"
64
+ - "Alive players: [list]"
65
+ Prompt: "Acknowledge you understand your role."
66
+ Response: Agent sends acknowledgment
67
+
68
+
69
+ === NIGHT PHASE ===
70
+
71
+ ## Doctor Turn (if alive)
72
+ Context:
73
+ - "Night X has begun"
74
+ - "Alive players: [list]"
75
+ - "You protected [name] last night" (if not first night)
76
+ Prompt: "Choose one player to protect tonight. They will be saved from death if targeted."
77
+ Response: "I protect [player_name]"
78
+ Validation: Must choose exactly one living player (not themselves)
79
+
80
+ ## Detective Turn (if alive)
81
+ Context:
82
+ - "Night X has begun"
83
+ - "Alive players: [list]"
84
+ - "Investigation history: [player -> role, ...]" (your past investigations)
85
+ Prompt: "Choose one player to investigate. You will learn their true role."
86
+ Response: "I investigate [player_name]"
87
+ System reveals: "[Player_name] is a [role]"
88
+ Validation: Must choose exactly one living player (not themselves)
89
+
90
+ ## Mafia Turns (all mafia, sequential or parallel)
91
+ Context:
92
+ - "Night X has begun"
93
+ - "Alive players: [list]"
94
+ - "Mafia members: [list of mafia names]"
95
+ - "Previous mafia votes: [if revote needed]"
96
+ Prompt: "Vote for one player to kill tonight. The player with most mafia votes dies."
97
+ Response: "I vote to kill [player_name]"
98
+ Validation: Must choose exactly one living non-mafia player
99
+ Note: If tie, repeat mafia turns for revote
100
+
101
+
102
+ === DAY PHASE ===
103
+
104
+ ## Death Narration Turn (Narrator)
105
+ Turn: Narrator
106
+ Context:
107
+ - "Day X has begun"
108
+ - "Deaths: [player_name(s)] died" OR "No deaths occurred"
109
+ - "How they died: [mafia kill/execution/protected]"
110
+ - "Alive players: [list]"
111
+ Prompt: "Narrate the deaths that occurred last night. Be creative and atmospheric."
112
+ Response: Narrator crafts story (e.g., "The town awoke to find John's body in the square, a grim reminder...")
113
+ Broadcast: All agents receive the narration
114
+
115
+ ## Discussion Phase (Narrator-moderated)
116
+
117
+ ### Narrator Selection Turn (loop)
118
+ Turn: Narrator
119
+ Context:
120
+ - "Discussion phase - Day X"
121
+ - "Alive players: [list]"
122
+ - "Players who have spoken: [list]"
123
+ - "Recent discussion: [last few exchanges]"
124
+ Prompt: "Choose the next speaker, or type 'town_hall' to proceed to voting."
125
+ Response: "[player_name]" OR "town_hall"
126
+ Validation: Must be alive player or "town_hall"
127
+
128
+ ### Agent Discussion Turn
129
+ Turn: Selected agent
130
+ Context:
131
+ - "You have been selected to speak by the narrator"
132
+ - "Alive players: [list]"
133
+ - "Previous discussion: [summary]"
134
+ Prompt: "Share your thoughts, suspicions, or information with the town."
135
+ Response: Agent speaks freely
136
+ Validation: None (can say anything)
137
+ Broadcast: All agents hear this speech
138
+
139
+ Note: Loop between narrator selection and agent speech until narrator calls "town_hall"
140
+
141
+ ## Town Hall - Nomination Phase
142
+
143
+ ### Nomination Turns (each agent, in order, can nominate once)
144
+ Context:
145
+ - "Town Hall - Nomination Phase"
146
+ - "Current nominees: [list]"
147
+ - "Nominations remaining: [3 - current count]"
148
+ - "Alive players: [list]"
149
+ Prompt: "Nominate one player for execution, or pass."
150
+ Response: "I nominate [player_name]" OR "I pass"
151
+ Validation: Must be alive player, can't nominate same person twice
152
+
153
+ ### Seconding Turns (after each nomination, all other agents vote)
154
+ Context:
155
+ - "[Nominator] has nominated [nominee]"
156
+ - "Alive players: [list]"
157
+ Prompt: "Do you second this nomination? (yes/no)"
158
+ Response: "yes" OR "no"
159
+ Validation: Must be yes or no
160
+ System: If majority votes yes, add to nominees list
161
+ Note: Nomination phase ends when 3 nominees OR no more nominations
162
+
163
+ ## Town Hall - Defense Phase (if >= 2 nominees)
164
+
165
+ ### Narrator Introduction Turn (for each nominee)
166
+ Turn: Narrator
167
+ Context:
168
+ - "Defense Phase"
169
+ - "Current nominee: [name]"
170
+ - "Nominees: [list]"
171
+ Prompt: "Introduce [nominee] to the stand. Set the scene for their defense."
172
+ Response: Narrator sets atmosphere (e.g., "The accused steps forward, all eyes upon them...")
173
+ Broadcast: All agents hear introduction
174
+
175
+ ### Defense Turn (nominee speaks)
176
+ Turn: Nominee
177
+ Context:
178
+ - "You are nominated for execution"
179
+ - "Nominees: [list]"
180
+ - "Narrator introduction: [text]"
181
+ Prompt: "Give your defense. Why should the town spare you?"
182
+ Response: Agent defends themselves
183
+ Validation: None
184
+ Broadcast: All agents hear defense
185
+
186
+ ## Town Hall - Trial Phase (if >= 1 nominee)
187
+
188
+ ### Trial Vote Turns (all agents vote)
189
+ Context:
190
+ - "Trial Phase"
191
+ - "Nominees: [list with their defenses]"
192
+ - "Alive players: [list]"
193
+ Prompt: "Vote for which nominee should go to the gallows."
194
+ Response: "I vote for [nominee_name]"
195
+ Validation: Must vote for one of the nominees
196
+ System: Tally votes, most votes goes to gallows (revote if tie)
197
+
198
+ ## Town Hall - Gallows Phase
199
+
200
+ ### Narrator Gallows Narration Turn
201
+ Turn: Narrator
202
+ Context:
203
+ - "[Condemned] has been chosen for the gallows"
204
+ - "Vote result: [vote breakdown]"
205
+ Prompt: "Narrate the walk to the gallows. Set a dramatic scene."
206
+ Response: Narrator creates atmosphere (e.g., "The crowd parts as [name] is led to the gallows...")
207
+ Broadcast: All agents hear narration
208
+
209
+ ### Final Speech Turn (condemned agent)
210
+ Turn: Condemned agent
211
+ Context:
212
+ - "You have been sent to the gallows"
213
+ - "Alive players: [list]"
214
+ - "Narrator's scene: [text]"
215
+ Prompt: "Give your final speech before the execution vote."
216
+ Response: Agent's last words
217
+ Validation: None
218
+ Broadcast: All agents hear final words
219
+
220
+ ### Execution Vote Turns (all agents except condemned)
221
+ Turn: Each agent
222
+ Context:
223
+ - "Execution Vote"
224
+ - "[Condemned]'s final speech: [text]"
225
+ - "Alive players: [list]"
226
+ Prompt: "Vote to execute or spare [condemned]? (execute/spare)"
227
+ Response: "execute" OR "spare"
228
+ Validation: Must be execute or spare
229
+ System: Tally votes, majority decides
230
+
231
+ ### Death Reveal and Narration (if executed)
232
+ Turn: Narrator
233
+ Context:
234
+ - "[Condemned] was executed"
235
+ - "Their role: [role]"
236
+ - "Vote breakdown: [counts]"
237
+ Prompt: "Narrate the execution and role reveal. Be dramatic and vivid."
238
+ Response: Narrator crafts death scene (e.g., "As [name] takes their last breath, the truth emerges - they were a [role]...")
239
+ Broadcast: All agents hear narration and learn role
240
+
241
+
242
+ === WIN CONDITION CHECK ===
243
+ After each day phase, check:
244
+ - If all mafia dead: Town wins
245
+ - If mafia >= non-mafia: Mafia wins
246
+ - If Jester was executed during town hall: Jester wins
247
+ Then loop to next night or end game
248
+
249
+
250
+ === TECHNICAL NOTES ===
251
+
252
+ Message Flow:
253
+ - Each "turn" is triggered by send_message_to_user()
254
+ - Agent responds with text
255
+ - Game logic parses response for structured actions
256
+ - System validates and applies action
257
+ - Next turn begins
258
+
259
+ State Management:
260
+ - Game maintains: alive_players, dead_players, roles, vote_history
261
+ - Each agent's context is role-specific (mafia know each other, detective knows investigations)
262
+ - Narrator sees everything (omniscient view)
263
+ - Past turn history available for context
264
+
265
+ Error Handling:
266
+ - Invalid responses trigger reprompt with error message
267
+ - Timeout fallback: random valid action or pass
268
+ - Dead players cannot take actions (filter them from turns)
269
+
270
+ NARRATOR PERSPECTIVE - OMNISCIENT VIEW
271
+
272
+ The narrator is a special agent with full game knowledge and creative freedom.
273
+ They serve as storyteller, moderator, and atmosphere creator.
274
+
275
+ === NARRATOR'S ROLE ===
276
+
277
+ Responsibilities:
278
+ 1. Narrate all deaths with creative storytelling
279
+ 2. Moderate discussion phase (choose speaking order)
280
+ 3. Introduce nominees during defense phase
281
+ 4. Set dramatic scenes for gallows phase
282
+ 5. Reveal roles through narrative (not dry announcements)
283
+
284
+ Narrator Context (sees everything):
285
+ - All agent roles (complete role assignments)
286
+ - Night action results (who was targeted, protected, investigated)
287
+ - Vote tallies and motivations
288
+ - Complete conversation history
289
+ - Win condition status
290
+ - Agent strategies and deception attempts
291
+
292
+ Narrator Constraints:
293
+ - MUST NOT reveal hidden information to agents
294
+ - Can hint and create atmosphere but not spoil
295
+ - Narrations are broadcast to all agents
296
+ - Should maintain dramatic tension
297
+ - Balance creative freedom with game integrity
298
+
299
+ === NARRATOR TURN EXAMPLES ===
300
+
301
+ ## Death Narration Example
302
+ Context (narrator sees):
303
+ - John (Villager) was killed by mafia
304
+ - Sarah (Doctor) protected herself
305
+ - Tom (Detective) investigated Alice (found Mafia)
306
+ Narration (what narrator says to all):
307
+ "Dawn breaks over the sleepy town. A scream pierces the morning air—John's lifeless
308
+ body lies in the town square, eyes wide with terror. The townspeople gather in horror,
309
+ knowing the mafia struck again in the darkness. Who among you can be trusted?"
310
+ Note: Narrator doesn't reveal protection or investigation results
311
+
312
+ ## Discussion Moderator Example
313
+ Context (narrator sees):
314
+ - Alive: Sarah, Tom, Alice, Bob, Carol
315
+ - Tom knows Alice is Mafia (from investigation)
316
+ - Alice is trying to deflect suspicion
317
+ Choice process (narrator's decision):
318
+ "Tom seems eager to speak after last night's events. Tom, you have the floor."
319
+ OR "Alice has been quiet. Alice, what are your thoughts?"
320
+ Strategy: Narrator can create drama by choosing order thoughtfully
321
+
322
+ ## Gallows Narration Example
323
+ Context (narrator sees):
324
+ - Alice (Mafia) was executed 4-3 vote
325
+ Narration (what narrator says):
326
+ "The rope tightens. Alice's eyes dart across the crowd one last time. As her body
327
+ goes limp, a crumpled note falls from her pocket—a list of names, targets for the
328
+ mafia's dark work. She was one of THEM. The mafia's grip on this town weakens, but
329
+ victory is not yet assured."
330
+
331
+ === NARRATOR GUIDELINES ===
332
+
333
+ Tone & Style:
334
+ - Gothic/mysterious atmosphere
335
+ - Vivid but not gratuitously violent
336
+ - Build tension and suspense
337
+ - Reward good storytelling from agents in narrations
338
+ - React to dramatic moments
339
+
340
+ Pacing:
341
+ - Don't rush through deaths
342
+ - Give each major moment weight
343
+ - Use narration to transition between phases
344
+ - Create breathing room between intense votes
345
+
346
+ Fairness:
347
+ - Don't favor any faction through narration
348
+ - Give equal dramatic treatment to all deaths
349
+ - Don't inadvertently hint at roles through word choice
350
+ - Maintain objectivity while being creative
@@ -0,0 +1,23 @@
1
+ from .actions import (
2
+ calculate_expression,
3
+ )
4
+ from .agent import (
5
+ LiteLLMMathFunction,
6
+ factory_math_dummy,
7
+ math_agent_params,
8
+ )
9
+ from .prompts import (
10
+ SYSPROMPT as MATH_SYSPROMPT,
11
+ )
12
+ from .types import (
13
+ action_calculate_expression,
14
+ )
15
+
16
+ __all__ = [
17
+ "action_calculate_expression",
18
+ "calculate_expression",
19
+ "factory_math_dummy",
20
+ "LiteLLMMathFunction",
21
+ "MATH_SYSPROMPT",
22
+ "math_agent_params",
23
+ ]
@@ -0,0 +1,252 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Utility actions exposed by the math example agent."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import ast
8
+ import json
9
+ import re
10
+ from decimal import ROUND_HALF_EVEN, Decimal, InvalidOperation, localcontext
11
+ from typing import Any, Final, Union
12
+
13
+ from mail import action
14
+
15
+ Number = Union[int, Decimal]
16
+
17
+ # Extended-precision constants to keep deterministic values for math literals.
18
+ _CONSTANTS: Final[dict[str, Decimal]] = {
19
+ "pi": Decimal("3.14159265358979323846264338327950288419716939937510"),
20
+ "tau": Decimal("6.28318530717958647692528676655900576839433879875021"),
21
+ "e": Decimal("2.71828182845904523536028747135266249775724709369996"),
22
+ }
23
+
24
+ _MIN_PRECISION: Final[int] = 64
25
+ _MAX_PRECISION: Final[int] = 4096
26
+
27
+ CALCULATE_EXPRESSION_PARAMETERS: Final[dict[str, Any]] = {
28
+ "type": "object",
29
+ "properties": {
30
+ "expression": {
31
+ "type": "string",
32
+ "description": "The arithmetic expression to evaluate.",
33
+ },
34
+ "precision": {
35
+ "type": "integer",
36
+ "minimum": 0,
37
+ "maximum": 12,
38
+ "description": (
39
+ "Optional number of decimal places for the formatted result."
40
+ ),
41
+ },
42
+ },
43
+ "required": ["expression"],
44
+ }
45
+
46
+
47
+ class _CalculatorError(ValueError):
48
+ """Raised when the calculator cannot safely evaluate an expression."""
49
+
50
+
51
+ def _estimate_precision(expr: str) -> int:
52
+ digit_groups = re.findall(r"\d+", expr)
53
+ if not digit_groups:
54
+ return _MIN_PRECISION
55
+ total_digits = sum(len(group) for group in digit_groups)
56
+ return max(_MIN_PRECISION, min(_MAX_PRECISION, total_digits * 2))
57
+
58
+
59
+ def _to_decimal(value: Number) -> Decimal:
60
+ if isinstance(value, Decimal):
61
+ return value
62
+ return Decimal(value)
63
+
64
+
65
+ def _format_decimal(value: Decimal, precision: int | None) -> str:
66
+ dec = value
67
+ if precision is not None:
68
+ quant = Decimal(1).scaleb(-precision)
69
+ dec = dec.quantize(quant, rounding=ROUND_HALF_EVEN)
70
+ formatted = format(dec, "f")
71
+ return formatted
72
+
73
+ formatted = format(dec, "f")
74
+ if "." in formatted:
75
+ formatted = formatted.rstrip("0").rstrip(".")
76
+ if not formatted:
77
+ formatted = "0"
78
+ return formatted
79
+
80
+
81
+ def _format_result(value: Number, precision: int | None) -> tuple[str, str, bool]:
82
+ if isinstance(value, int):
83
+ dec_value = Decimal(value)
84
+ formatted = _format_decimal(dec_value, precision)
85
+ exact = str(value)
86
+ return exact, formatted, True
87
+
88
+ exact = _format_decimal(value, None)
89
+ formatted = _format_decimal(value, precision)
90
+ is_integer = value == value.to_integral_value()
91
+ return exact, formatted, is_integer
92
+
93
+
94
+ def _apply_unary(op: ast.unaryop, operand: Number) -> Number:
95
+ if isinstance(op, ast.UAdd):
96
+ return operand
97
+ if isinstance(op, ast.USub):
98
+ return -operand
99
+ raise _CalculatorError("Unsupported unary operator")
100
+
101
+
102
+ def _apply_binop(op: ast.operator, left: Number, right: Number) -> Number:
103
+ if isinstance(left, int) and isinstance(right, int):
104
+ if isinstance(op, ast.Add):
105
+ return left + right
106
+ if isinstance(op, ast.Sub):
107
+ return left - right
108
+ if isinstance(op, ast.Mult):
109
+ return left * right
110
+ if isinstance(op, ast.Pow):
111
+ if right < 0:
112
+ left_dec = _to_decimal(left)
113
+ return left_dec**right
114
+ return left**right
115
+ if isinstance(op, ast.FloorDiv):
116
+ if right == 0:
117
+ raise ZeroDivisionError
118
+ return left // right
119
+ if isinstance(op, ast.Mod):
120
+ if right == 0:
121
+ raise ZeroDivisionError
122
+ return left % right
123
+ if isinstance(op, ast.Div):
124
+ left_dec = _to_decimal(left)
125
+ right_dec = _to_decimal(right)
126
+ return left_dec / right_dec
127
+ raise _CalculatorError("Unsupported operator")
128
+
129
+ left_dec = _to_decimal(left)
130
+ right_dec = _to_decimal(right)
131
+
132
+ if isinstance(op, ast.Add):
133
+ return left_dec + right_dec
134
+ if isinstance(op, ast.Sub):
135
+ return left_dec - right_dec
136
+ if isinstance(op, ast.Mult):
137
+ return left_dec * right_dec
138
+ if isinstance(op, ast.Div):
139
+ return left_dec / right_dec
140
+ if isinstance(op, ast.FloorDiv):
141
+ if right_dec == 0:
142
+ raise ZeroDivisionError
143
+ result = left_dec // right_dec
144
+ if result == result.to_integral_value():
145
+ return int(result)
146
+ return result
147
+ if isinstance(op, ast.Mod):
148
+ if right_dec == 0:
149
+ raise ZeroDivisionError
150
+ return left_dec % right_dec
151
+ if isinstance(op, ast.Pow):
152
+ if isinstance(right, int):
153
+ return left_dec**right
154
+ raise _CalculatorError("Exponent must be an integer")
155
+ raise _CalculatorError("Unsupported operator")
156
+
157
+
158
+ def _eval_node(node: ast.AST, source: str) -> Number:
159
+ if isinstance(node, ast.Expression):
160
+ return _eval_node(node.body, source)
161
+
162
+ if isinstance(node, ast.Constant):
163
+ value = node.value
164
+ if isinstance(value, bool) or value is None:
165
+ raise _CalculatorError("Unsupported literal")
166
+ if isinstance(value, int):
167
+ return value
168
+ if isinstance(value, float):
169
+ # Reconstruct via source to avoid binary float artifacts if available.
170
+ segment = ast.get_source_segment(source, node)
171
+ if segment is not None:
172
+ return Decimal(segment.replace("_", ""))
173
+ return Decimal(str(value))
174
+ raise _CalculatorError("Only numeric literals are supported")
175
+
176
+ if isinstance(node, ast.Name):
177
+ if node.id in _CONSTANTS:
178
+ return _CONSTANTS[node.id]
179
+ raise _CalculatorError(f"Unknown constant '{node.id}'")
180
+
181
+ if isinstance(node, ast.BinOp):
182
+ left = _eval_node(node.left, source)
183
+ right = _eval_node(node.right, source)
184
+ return _apply_binop(node.op, left, right)
185
+
186
+ if isinstance(node, ast.UnaryOp):
187
+ operand = _eval_node(node.operand, source)
188
+ return _apply_unary(node.op, operand)
189
+
190
+ raise _CalculatorError("Unsupported syntax in expression")
191
+
192
+
193
+ @action(
194
+ name="calculate_expression",
195
+ description=(
196
+ "Evaluate a basic arithmetic expression with +, -, *, /, %, //, **, and "
197
+ "parentheses."
198
+ ),
199
+ parameters=CALCULATE_EXPRESSION_PARAMETERS,
200
+ )
201
+ async def calculate_expression(args: dict[str, Any]) -> str:
202
+ """Evaluate a basic arithmetic expression and return a structured JSON payload.
203
+
204
+ Supported grammar:
205
+ - numeric literals (integers or decimals)
206
+ - parentheses
207
+ - operators: +, -, *, /, //, %, **
208
+ - unary + and -
209
+ - constants: e, pi, tau
210
+
211
+ When provided, ``precision`` (0-12) controls rounding in ``formatted_result``.
212
+ """
213
+
214
+ expression = args.get("expression")
215
+ if not isinstance(expression, str) or not expression.strip():
216
+ return "Error: 'expression' must be a non-empty string"
217
+
218
+ precision = args.get("precision")
219
+ if precision is not None:
220
+ if not isinstance(precision, int) or not (0 <= precision <= 12):
221
+ return "Error: 'precision' must be an integer between 0 and 12"
222
+
223
+ try:
224
+ parsed = ast.parse(expression, mode="eval")
225
+ except SyntaxError:
226
+ return "Error: invalid syntax"
227
+
228
+ try:
229
+ with localcontext() as ctx:
230
+ ctx.prec = _estimate_precision(expression)
231
+ result: Number = _eval_node(parsed, expression)
232
+ except _CalculatorError as exc:
233
+ return f"Error: {exc}"
234
+ except ZeroDivisionError:
235
+ return "Error: division by zero"
236
+ except (InvalidOperation, OverflowError):
237
+ return "Error: unable to evaluate expression"
238
+
239
+ if isinstance(result, Decimal) and not result.is_finite():
240
+ return "Error: result is not a finite number"
241
+
242
+ exact_result, formatted_result, is_integer = _format_result(result, precision)
243
+
244
+ payload = {
245
+ "expression": expression,
246
+ "result": exact_result,
247
+ "formatted_result": formatted_result,
248
+ "precision": precision,
249
+ "is_integer": is_integer,
250
+ }
251
+
252
+ return json.dumps(payload)