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
mail/core/tasks.py ADDED
@@ -0,0 +1,311 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Addison Kline
3
+
4
+ import datetime
5
+ import heapq
6
+ from asyncio import PriorityQueue
7
+ from typing import Literal, cast
8
+
9
+ import ujson
10
+ from sse_starlette import ServerSentEvent
11
+
12
+ from mail.core.message import MAILMessage, create_agent_address
13
+
14
+ QueueItem = tuple[int, int, MAILMessage]
15
+
16
+
17
+ class MAILTask:
18
+ """
19
+ A discrete collection of messages between agents working towards a common goal.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ task_id: str,
25
+ task_owner: str,
26
+ task_contributors: list[str],
27
+ ) -> None:
28
+ self.task_id = task_id
29
+ self.task_owner = task_owner
30
+ self.task_contributors = task_contributors
31
+ self.start_time = datetime.datetime.now(datetime.UTC)
32
+ self.events: list[ServerSentEvent] = []
33
+ self.is_running = False
34
+ self.task_message_queue: list[QueueItem] = []
35
+ self.remote_swarms: set[str] = set()
36
+ self.completed = False
37
+ # Title for UI task history (generated once via Haiku)
38
+ self.title: str | None = None
39
+
40
+ def add_event(self, event: ServerSentEvent) -> None:
41
+ """
42
+ Add a new event to the task.
43
+ """
44
+ self.events.append(event)
45
+
46
+ def get_messages(self) -> list[MAILMessage]:
47
+ """
48
+ Get all messages for the task.
49
+ """
50
+ messages: list[MAILMessage] = []
51
+
52
+ for sse in self.events:
53
+ if sse.event == "new_message":
54
+ data = sse.data
55
+ if data is None:
56
+ continue
57
+ if isinstance(data, str):
58
+ try:
59
+ data = ujson.loads(data)
60
+ except ValueError:
61
+ continue
62
+ if not isinstance(data, dict):
63
+ continue
64
+ extra_data = data.get("extra_data")
65
+ if extra_data is None:
66
+ continue
67
+ full_message = extra_data.get("full_message")
68
+ if full_message is None:
69
+ continue
70
+
71
+ messages.append(cast(MAILMessage, full_message))
72
+
73
+ return messages
74
+
75
+ def get_messages_by_agent(
76
+ self,
77
+ agent: str,
78
+ sent: bool = True,
79
+ received: bool = True,
80
+ ) -> list[MAILMessage]:
81
+ """
82
+ Get all messages for a given agent (whether sent or received).
83
+ """
84
+ agent_address = create_agent_address(agent)
85
+
86
+ sent_messages: list[MAILMessage] = []
87
+ if sent:
88
+ sent_messages = [
89
+ message
90
+ for message in self.get_messages()
91
+ if message["message"]["sender"] == agent_address
92
+ ]
93
+ received_messages: list[MAILMessage] = []
94
+ if received:
95
+ for message in self.get_messages():
96
+ match message["msg_type"]:
97
+ case "request" | "response":
98
+ if message["message"]["recipient"] == agent_address: # type: ignore
99
+ received_messages.append(message)
100
+ case "broadcast" | "interrupt" | "broadcast_complete":
101
+ if agent_address in message["message"]["recipients"]: # type: ignore
102
+ received_messages.append(message)
103
+ case _:
104
+ raise ValueError(f"invalid message type: {message['msg_type']}")
105
+
106
+ return sent_messages + received_messages
107
+
108
+ def get_messages_by_type(
109
+ self,
110
+ message_type: Literal[
111
+ "request", "response", "broadcast", "interrupt", "broadcast_complete"
112
+ ],
113
+ ) -> list[MAILMessage]:
114
+ """
115
+ Get all messages of a given type.
116
+ """
117
+ return [
118
+ message
119
+ for message in self.get_messages()
120
+ if message["msg_type"] == message_type
121
+ ]
122
+
123
+ def get_messages_by_system(self) -> list[MAILMessage]:
124
+ """
125
+ Get all messages from the system.
126
+ """
127
+ return [
128
+ message
129
+ for message in self.get_messages()
130
+ if message["message"]["sender"]["address_type"] == "system"
131
+ ]
132
+
133
+ def get_messages_by_user(self) -> list[MAILMessage]:
134
+ """
135
+ Get all messages from the user.
136
+ """
137
+ return [
138
+ message
139
+ for message in self.get_messages()
140
+ if message["message"]["sender"]["address_type"] == "user"
141
+ ]
142
+
143
+ def get_lifetime(self) -> datetime.timedelta:
144
+ """
145
+ Get the lifetime of the task.
146
+ """
147
+ return datetime.datetime.now(datetime.UTC) - self.start_time
148
+
149
+ async def queue_stash(
150
+ self,
151
+ message_queue: PriorityQueue[QueueItem],
152
+ ) -> None:
153
+ """
154
+ Remove any queued messages for this task from the shared runtime queue and
155
+ store them for later restoration when the task resumes.
156
+ """
157
+ try:
158
+ raw_queue = list(getattr(message_queue, "_queue", []))
159
+ except Exception:
160
+ raw_queue = []
161
+
162
+ if not raw_queue:
163
+ self.task_message_queue = []
164
+ return
165
+
166
+ remaining: list[QueueItem] = []
167
+ stashed: list[QueueItem] = []
168
+
169
+ for item in raw_queue:
170
+ try:
171
+ queued_task_id = item[2]["message"]["task_id"]
172
+ except Exception:
173
+ remaining.append(item)
174
+ continue
175
+
176
+ if queued_task_id == self.task_id:
177
+ stashed.append(item)
178
+ else:
179
+ remaining.append(item)
180
+
181
+ if not stashed:
182
+ self.task_message_queue = []
183
+ return
184
+
185
+ try:
186
+ message_queue._queue.clear() # type: ignore[attr-defined]
187
+ message_queue._queue.extend(remaining) # type: ignore[attr-defined]
188
+ heapq.heapify(message_queue._queue) # type: ignore[attr-defined]
189
+ except Exception:
190
+ # If direct manipulation fails, reassigning the queue isn't critical;
191
+ # continue with captured snapshot to avoid losing the task state.
192
+ pass
193
+
194
+ unfinished = getattr(message_queue, "_unfinished_tasks", None)
195
+ if isinstance(unfinished, int):
196
+ message_queue._unfinished_tasks = max(0, unfinished - len(stashed)) # type: ignore[attr-defined]
197
+
198
+ self.task_message_queue = stashed
199
+
200
+ async def queue_load(
201
+ self,
202
+ message_queue: PriorityQueue[QueueItem],
203
+ ) -> None:
204
+ """
205
+ Restore any previously stashed messages for this task back into the shared
206
+ runtime queue.
207
+ """
208
+ if not self.task_message_queue:
209
+ return
210
+
211
+ stashed = list(self.task_message_queue)
212
+
213
+ try:
214
+ message_queue._queue.extend(stashed) # type: ignore[attr-defined]
215
+ heapq.heapify(message_queue._queue) # type: ignore[attr-defined]
216
+ except Exception:
217
+ # If we cannot directly restore to the shared queue, keep the snapshot
218
+ # so that a future attempt can retry.
219
+ return
220
+
221
+ unfinished = getattr(message_queue, "_unfinished_tasks", None)
222
+ if isinstance(unfinished, int):
223
+ message_queue._unfinished_tasks = unfinished + len(stashed) # type: ignore[attr-defined]
224
+
225
+ self.task_message_queue = []
226
+ self.completed = False
227
+
228
+ def mark_complete(self) -> None:
229
+ """
230
+ Mark the task as complete and stop active processing.
231
+ """
232
+ self.completed = True
233
+ self.is_running = False
234
+
235
+ def resume(self) -> None:
236
+ """
237
+ Mark the completed task as running again.
238
+ """
239
+ self.completed = False
240
+ self.is_running = True
241
+
242
+ def add_remote_swarm(self, remote_swarm: str) -> None:
243
+ """
244
+ Track a remote swarm participating in this task.
245
+ """
246
+ self.remote_swarms.add(remote_swarm)
247
+
248
+ def to_db_dict(self) -> dict:
249
+ """
250
+ Serialize the task to a dictionary for database storage.
251
+ Does not include events (stored separately) or task_message_queue (not persisted).
252
+ """
253
+ return {
254
+ "task_id": self.task_id,
255
+ "task_owner": self.task_owner,
256
+ "task_contributors": self.task_contributors,
257
+ "remote_swarms": list(self.remote_swarms),
258
+ "is_running": self.is_running,
259
+ "completed": self.completed,
260
+ "start_time": self.start_time.isoformat(),
261
+ "title": self.title,
262
+ }
263
+
264
+ @classmethod
265
+ def from_db_dict(cls, data: dict) -> "MAILTask":
266
+ """
267
+ Create a MAILTask from a database record dictionary.
268
+ """
269
+ task = cls(
270
+ task_id=data["task_id"],
271
+ task_owner=data["task_owner"],
272
+ task_contributors=data.get("task_contributors", []),
273
+ )
274
+ # Restore state from DB
275
+ task.is_running = data.get("is_running", False)
276
+ task.completed = data.get("completed", False)
277
+ task.remote_swarms = set(data.get("remote_swarms", []))
278
+
279
+ # Parse start_time if it's a string
280
+ start_time = data.get("start_time")
281
+ if start_time:
282
+ if isinstance(start_time, str):
283
+ task.start_time = datetime.datetime.fromisoformat(start_time)
284
+ elif isinstance(start_time, datetime.datetime):
285
+ task.start_time = start_time
286
+
287
+ # Restore title
288
+ task.title = data.get("title")
289
+
290
+ return task
291
+
292
+ def add_event_from_db(self, event_data: dict) -> None:
293
+ """
294
+ Add an event from a database record.
295
+ """
296
+ import json
297
+
298
+ # Parse event data if it's a JSON string
299
+ data = event_data.get("data")
300
+ if isinstance(data, str):
301
+ try:
302
+ data = json.loads(data)
303
+ except json.JSONDecodeError:
304
+ pass # Keep as string if not valid JSON
305
+
306
+ event = ServerSentEvent(
307
+ event=event_data.get("event"),
308
+ data=data,
309
+ id=event_data.get("id"),
310
+ )
311
+ self.events.append(event)