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
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)
|