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/tools.py
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2025 Addison Kline, Ryan Heaton
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Literal, Optional, cast
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
import ujson
|
|
10
|
+
from openai import pydantic_function_tool
|
|
11
|
+
from openai.resources.responses.responses import _make_tools
|
|
12
|
+
from pydantic import BaseModel, Field, model_validator
|
|
13
|
+
|
|
14
|
+
from .message import (
|
|
15
|
+
MAILBroadcast,
|
|
16
|
+
MAILInterrupt,
|
|
17
|
+
MAILMessage,
|
|
18
|
+
MAILRequest,
|
|
19
|
+
MAILResponse,
|
|
20
|
+
create_agent_address,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("mail.tools")
|
|
24
|
+
|
|
25
|
+
MAIL_TOOL_NAMES = [
|
|
26
|
+
"send_request",
|
|
27
|
+
"send_response",
|
|
28
|
+
"send_interrupt",
|
|
29
|
+
"send_broadcast",
|
|
30
|
+
"task_complete",
|
|
31
|
+
"acknowledge_broadcast",
|
|
32
|
+
"ignore_broadcast",
|
|
33
|
+
"await_message",
|
|
34
|
+
"help",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_tool_spec_name(tool: dict[str, Any]) -> str | None:
|
|
39
|
+
"""
|
|
40
|
+
Extract the logical tool name from either responses or completions tool specs.
|
|
41
|
+
"""
|
|
42
|
+
name = tool.get("name")
|
|
43
|
+
if isinstance(name, str):
|
|
44
|
+
return name
|
|
45
|
+
maybe_function = tool.get("function")
|
|
46
|
+
if isinstance(maybe_function, dict):
|
|
47
|
+
function_name = maybe_function.get("name")
|
|
48
|
+
if isinstance(function_name, str):
|
|
49
|
+
return function_name
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pydantic_model_to_tool(
|
|
54
|
+
model_cls: type[BaseModel],
|
|
55
|
+
name: str | None = None,
|
|
56
|
+
description: str | None = None,
|
|
57
|
+
style: Literal["completions", "responses"] = "completions",
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Convert a Pydantic model class into an OpenAI function tool spec.
|
|
61
|
+
|
|
62
|
+
Returns a dict in the shape expected by Chat Completions and is compatible
|
|
63
|
+
with the Responses API (we later mirror parameters → input_schema when needed).
|
|
64
|
+
"""
|
|
65
|
+
completions_tool = pydantic_function_tool(
|
|
66
|
+
model_cls, name=name, description=description
|
|
67
|
+
)
|
|
68
|
+
if style == "completions":
|
|
69
|
+
return cast(dict[str, Any], completions_tool)
|
|
70
|
+
elif style == "responses":
|
|
71
|
+
return _make_tools([completions_tool])[0] # type: ignore
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AgentToolCall(BaseModel):
|
|
75
|
+
"""
|
|
76
|
+
A tool call from an agent.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
tool_name: The name of the tool called.
|
|
80
|
+
tool_args: The arguments passed to the tool.
|
|
81
|
+
tool_call_id: The ID of the tool call.
|
|
82
|
+
completion: The full completion of the tool call, if using completions api.
|
|
83
|
+
responses: The full responses list of the tool call, if using responses api.
|
|
84
|
+
reasoning: List of reasoning/thinking text blocks preceding this tool call.
|
|
85
|
+
preamble: Text/message content that appeared before this tool call.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
tool_name: str
|
|
89
|
+
tool_args: dict[str, Any]
|
|
90
|
+
tool_call_id: str
|
|
91
|
+
completion: dict[str, Any] = Field(default_factory=dict)
|
|
92
|
+
responses: list[dict[str, Any]] = Field(default_factory=list)
|
|
93
|
+
reasoning: list[str] | None = None
|
|
94
|
+
preamble: str | None = None
|
|
95
|
+
|
|
96
|
+
@model_validator(mode="after")
|
|
97
|
+
def check_completion_or_responses(self):
|
|
98
|
+
if not self.completion and not self.responses:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Either 'completion' or 'responses' must be defined (non-empty)."
|
|
101
|
+
)
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def create_response_msg(self, content: str) -> dict[str, str]:
|
|
105
|
+
if self.completion:
|
|
106
|
+
return {
|
|
107
|
+
"role": "tool",
|
|
108
|
+
"name": self.tool_name,
|
|
109
|
+
"content": content,
|
|
110
|
+
"tool_call_id": self.tool_call_id,
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
"type": "function_call_output",
|
|
114
|
+
"call_id": self.tool_call_id,
|
|
115
|
+
"output": content,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def convert_call_to_mail_message(
|
|
120
|
+
call: AgentToolCall, sender: str, task_id: str
|
|
121
|
+
) -> MAILMessage:
|
|
122
|
+
"""
|
|
123
|
+
Convert a MAIL tool call to a MAIL message.
|
|
124
|
+
"""
|
|
125
|
+
# Convert sender string to MAILAddress (assuming it's an agent)
|
|
126
|
+
sender_address = create_agent_address(sender)
|
|
127
|
+
|
|
128
|
+
match call.tool_name:
|
|
129
|
+
case "send_request":
|
|
130
|
+
return MAILMessage(
|
|
131
|
+
id=str(uuid4()),
|
|
132
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
133
|
+
message=MAILRequest(
|
|
134
|
+
task_id=task_id,
|
|
135
|
+
request_id=str(uuid4()),
|
|
136
|
+
sender=sender_address,
|
|
137
|
+
recipient=create_agent_address(call.tool_args["target"]),
|
|
138
|
+
subject=call.tool_args["subject"],
|
|
139
|
+
body=call.tool_args["body"],
|
|
140
|
+
sender_swarm=None,
|
|
141
|
+
recipient_swarm=None,
|
|
142
|
+
routing_info={},
|
|
143
|
+
),
|
|
144
|
+
msg_type="request",
|
|
145
|
+
)
|
|
146
|
+
case "send_response" | "text_output":
|
|
147
|
+
return MAILMessage(
|
|
148
|
+
id=str(uuid4()),
|
|
149
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
150
|
+
message=MAILResponse(
|
|
151
|
+
task_id=task_id,
|
|
152
|
+
request_id=str(uuid4()),
|
|
153
|
+
sender=sender_address,
|
|
154
|
+
recipient=create_agent_address(call.tool_args["target"]),
|
|
155
|
+
subject=call.tool_args.get("subject", ""),
|
|
156
|
+
body=call.tool_args.get("body", None)
|
|
157
|
+
or call.tool_args.get("content", ""),
|
|
158
|
+
sender_swarm=None,
|
|
159
|
+
recipient_swarm=None,
|
|
160
|
+
routing_info={},
|
|
161
|
+
),
|
|
162
|
+
msg_type="response",
|
|
163
|
+
)
|
|
164
|
+
case "send_interrupt":
|
|
165
|
+
return MAILMessage(
|
|
166
|
+
id=str(uuid4()),
|
|
167
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
168
|
+
message=MAILInterrupt(
|
|
169
|
+
task_id=task_id,
|
|
170
|
+
interrupt_id=str(uuid4()),
|
|
171
|
+
sender=sender_address,
|
|
172
|
+
recipients=[create_agent_address(call.tool_args["target"])],
|
|
173
|
+
subject=call.tool_args["subject"],
|
|
174
|
+
body=call.tool_args["body"],
|
|
175
|
+
sender_swarm=None,
|
|
176
|
+
recipient_swarms=None,
|
|
177
|
+
routing_info={},
|
|
178
|
+
),
|
|
179
|
+
msg_type="interrupt",
|
|
180
|
+
)
|
|
181
|
+
case "send_broadcast":
|
|
182
|
+
return MAILMessage(
|
|
183
|
+
id=str(uuid4()),
|
|
184
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
185
|
+
message=MAILBroadcast(
|
|
186
|
+
task_id=task_id,
|
|
187
|
+
broadcast_id=str(uuid4()),
|
|
188
|
+
sender=sender_address,
|
|
189
|
+
recipients=[create_agent_address("all")],
|
|
190
|
+
subject=call.tool_args["subject"],
|
|
191
|
+
body=call.tool_args["body"],
|
|
192
|
+
sender_swarm=None,
|
|
193
|
+
recipient_swarms=None,
|
|
194
|
+
routing_info={},
|
|
195
|
+
),
|
|
196
|
+
msg_type="broadcast",
|
|
197
|
+
)
|
|
198
|
+
case "task_complete":
|
|
199
|
+
return MAILMessage(
|
|
200
|
+
id=str(uuid4()),
|
|
201
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
202
|
+
message=MAILBroadcast(
|
|
203
|
+
task_id=task_id,
|
|
204
|
+
broadcast_id=str(uuid4()),
|
|
205
|
+
sender=sender_address,
|
|
206
|
+
recipients=[create_agent_address("all")],
|
|
207
|
+
subject="Task complete",
|
|
208
|
+
body=call.tool_args["finish_message"],
|
|
209
|
+
sender_swarm=None,
|
|
210
|
+
recipient_swarms=None,
|
|
211
|
+
routing_info={},
|
|
212
|
+
),
|
|
213
|
+
msg_type="broadcast_complete",
|
|
214
|
+
)
|
|
215
|
+
case _:
|
|
216
|
+
raise ValueError(f"Unknown tool name: {call.tool_name}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def normalize_breakpoint_tool_call(
|
|
220
|
+
call: AgentToolCall, raw: dict[str, Any] | None = None
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
"""
|
|
223
|
+
Normalize a breakpoint tool call to Responses-style function_call shape.
|
|
224
|
+
"""
|
|
225
|
+
call_id: str | None = None
|
|
226
|
+
name: str | None = None
|
|
227
|
+
arguments: Any = None
|
|
228
|
+
status: str = "completed"
|
|
229
|
+
fc_id: str | None = None
|
|
230
|
+
|
|
231
|
+
if isinstance(raw, dict):
|
|
232
|
+
raw_type = raw.get("type")
|
|
233
|
+
if raw_type == "function_call":
|
|
234
|
+
call_id = raw.get("call_id")
|
|
235
|
+
name = raw.get("name")
|
|
236
|
+
arguments = raw.get("arguments")
|
|
237
|
+
status = raw.get("status") or status
|
|
238
|
+
fc_id = raw.get("id")
|
|
239
|
+
elif raw_type == "tool_use":
|
|
240
|
+
call_id = raw.get("id")
|
|
241
|
+
name = raw.get("name")
|
|
242
|
+
arguments = raw.get("input")
|
|
243
|
+
|
|
244
|
+
if name is None:
|
|
245
|
+
name = call.tool_name
|
|
246
|
+
if not call_id:
|
|
247
|
+
call_id = call.tool_call_id
|
|
248
|
+
if arguments is None:
|
|
249
|
+
arguments = call.tool_args
|
|
250
|
+
if not isinstance(arguments, str):
|
|
251
|
+
arguments = ujson.dumps(arguments)
|
|
252
|
+
if not fc_id:
|
|
253
|
+
fc_id = f"fc_{call_id}"
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"arguments": arguments,
|
|
257
|
+
"call_id": call_id,
|
|
258
|
+
"name": name,
|
|
259
|
+
"type": "function_call",
|
|
260
|
+
"id": fc_id,
|
|
261
|
+
"status": status,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def convert_manual_step_call_to_mail_message(
|
|
266
|
+
call: AgentToolCall,
|
|
267
|
+
sender: str,
|
|
268
|
+
task_id: str,
|
|
269
|
+
response_targets: list[str],
|
|
270
|
+
response_type: Literal["broadcast", "response", "request"],
|
|
271
|
+
) -> MAILMessage:
|
|
272
|
+
"""
|
|
273
|
+
Convert a MAIL tool call to a MAIL message.
|
|
274
|
+
"""
|
|
275
|
+
# Convert sender string to MAILAddress (assuming it's an agent)
|
|
276
|
+
sender_address = create_agent_address(sender)
|
|
277
|
+
targets = []
|
|
278
|
+
for target in response_targets:
|
|
279
|
+
if target == "all":
|
|
280
|
+
targets.append(create_agent_address("all"))
|
|
281
|
+
else:
|
|
282
|
+
targets.append(create_agent_address(target))
|
|
283
|
+
|
|
284
|
+
match response_type:
|
|
285
|
+
case "request":
|
|
286
|
+
return MAILMessage(
|
|
287
|
+
id=str(uuid4()),
|
|
288
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
289
|
+
message=MAILRequest(
|
|
290
|
+
task_id=task_id,
|
|
291
|
+
request_id=str(uuid4()),
|
|
292
|
+
sender=sender_address,
|
|
293
|
+
recipient=targets[0],
|
|
294
|
+
subject=call.tool_args.get("subject", ""),
|
|
295
|
+
body=call.tool_args.get("body", None)
|
|
296
|
+
or call.tool_args.get("content", ""),
|
|
297
|
+
sender_swarm=None,
|
|
298
|
+
recipient_swarm=None,
|
|
299
|
+
routing_info={},
|
|
300
|
+
),
|
|
301
|
+
msg_type="request",
|
|
302
|
+
)
|
|
303
|
+
case "response":
|
|
304
|
+
return MAILMessage(
|
|
305
|
+
id=str(uuid4()),
|
|
306
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
307
|
+
message=MAILResponse(
|
|
308
|
+
task_id=task_id,
|
|
309
|
+
request_id=str(uuid4()),
|
|
310
|
+
sender=sender_address,
|
|
311
|
+
recipient=targets[0],
|
|
312
|
+
subject=call.tool_args.get("subject", ""),
|
|
313
|
+
body=call.tool_args.get("body", None)
|
|
314
|
+
or call.tool_args.get("content", ""),
|
|
315
|
+
sender_swarm=None,
|
|
316
|
+
recipient_swarm=None,
|
|
317
|
+
routing_info={},
|
|
318
|
+
),
|
|
319
|
+
msg_type="response",
|
|
320
|
+
)
|
|
321
|
+
case "broadcast":
|
|
322
|
+
return MAILMessage(
|
|
323
|
+
id=str(uuid4()),
|
|
324
|
+
timestamp=datetime.datetime.now(datetime.UTC).isoformat(),
|
|
325
|
+
message=MAILBroadcast(
|
|
326
|
+
task_id=task_id,
|
|
327
|
+
broadcast_id=str(uuid4()),
|
|
328
|
+
sender=sender_address,
|
|
329
|
+
recipients=targets,
|
|
330
|
+
subject=call.tool_args.get("subject", ""),
|
|
331
|
+
body=call.tool_args.get("body", None)
|
|
332
|
+
or call.tool_args.get("content", ""),
|
|
333
|
+
sender_swarm=None,
|
|
334
|
+
recipient_swarms=None,
|
|
335
|
+
routing_info={},
|
|
336
|
+
),
|
|
337
|
+
msg_type="broadcast",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def create_request_tool(
|
|
342
|
+
targets: list[str],
|
|
343
|
+
enable_interswarm: bool = False,
|
|
344
|
+
style: Literal["completions", "responses"] = "completions",
|
|
345
|
+
) -> dict[str, Any]:
|
|
346
|
+
"""
|
|
347
|
+
Create a MAIL message tool to send messages to specific agents.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
class send_request(BaseModel):
|
|
351
|
+
"""Send a message to a specific target recipient agent."""
|
|
352
|
+
|
|
353
|
+
target: str = Field(
|
|
354
|
+
description=f"The target recipient agent for the message. Must be one of: {', '.join(targets)}"
|
|
355
|
+
+ (
|
|
356
|
+
", or use 'agent-name@swarm-name' format for interswarm messaging"
|
|
357
|
+
if enable_interswarm
|
|
358
|
+
else ""
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
subject: str = Field(description="The subject of the message.")
|
|
362
|
+
body: str = Field(description="The message content to send.")
|
|
363
|
+
|
|
364
|
+
tool_dict = pydantic_model_to_tool(send_request, name="send_request", style=style)
|
|
365
|
+
|
|
366
|
+
target_param = (
|
|
367
|
+
tool_dict["function"]["parameters"]["properties"]["target"]
|
|
368
|
+
if style == "completions"
|
|
369
|
+
else tool_dict["parameters"]["properties"]["target"]
|
|
370
|
+
)
|
|
371
|
+
if enable_interswarm:
|
|
372
|
+
# For interswarm messaging, we don't restrict to enum values
|
|
373
|
+
# The validation will happen at runtime
|
|
374
|
+
target_param["description"] = (
|
|
375
|
+
target_param["description"]
|
|
376
|
+
+ " (supports interswarm format: agent-name@swarm-name)"
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
target_param["enum"] = targets # This provides the allowed values to the LLM
|
|
380
|
+
|
|
381
|
+
return tool_dict
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
SEND_REQUEST_HELP_STRING = """
|
|
385
|
+
Send a message to a specific recipient agent (the `target`).
|
|
386
|
+
This is useful for initiating a conversation with another agent.
|
|
387
|
+
|
|
388
|
+
# Example
|
|
389
|
+
A MAIL swarm `example` has agents `supervisor` and `weather`.
|
|
390
|
+
`supervisor` can interact with the user and complete the task, while `weather` can call the `get_weather_forecast` action.
|
|
391
|
+
Say a user creates a task for getting the weather forecast in Tokyo:
|
|
392
|
+
|
|
393
|
+
```xml
|
|
394
|
+
<message>
|
|
395
|
+
<sender type="user">example_user</sender>
|
|
396
|
+
<recipient type="agent">supervisor</recipient>
|
|
397
|
+
<subject>New Message</subject>
|
|
398
|
+
<body>What will the weather be like in Tokyo tomorrow?</body>
|
|
399
|
+
<msg_type>request</msg_type>
|
|
400
|
+
</message>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`supervisor` can then delegate the task to `weather`:
|
|
404
|
+
|
|
405
|
+
```xml
|
|
406
|
+
<message>
|
|
407
|
+
<sender type="agent">supervisor</sender>
|
|
408
|
+
<recipient type="agent">weather</recipient>
|
|
409
|
+
<subject>Weather forecast in Tokyo</subject>
|
|
410
|
+
<body>User asked: 'What will the weather be like in Tokyo tomorrow?' Please get the forecast and respond to me with the result.</body>
|
|
411
|
+
<msg_type>request</msg_type>
|
|
412
|
+
</message>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
From there, `weather` can call the `get_weather_forecast` action, and send the result back to `supervisor` in the form of a response.
|
|
416
|
+
Finally, `supervisor` can call the `task_complete` tool with the final answer to the user's task (in this case, the weather forecast for Tokyo tomorrow).
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def create_response_tool(
|
|
421
|
+
targets: list[str],
|
|
422
|
+
enable_interswarm: bool = False,
|
|
423
|
+
style: Literal["completions", "responses"] = "completions",
|
|
424
|
+
) -> dict[str, Any]:
|
|
425
|
+
"""
|
|
426
|
+
Create a MAIL message tool to send messages to specific agents.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
class send_response(BaseModel):
|
|
430
|
+
"""Send a message to a specific target recipient agent."""
|
|
431
|
+
|
|
432
|
+
target: str = Field(
|
|
433
|
+
description=f"The target recipient agent for the message. Must be one of: {', '.join(targets)}"
|
|
434
|
+
+ (
|
|
435
|
+
", or use 'agent-name@swarm-name' format for interswarm messaging"
|
|
436
|
+
if enable_interswarm
|
|
437
|
+
else ""
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
subject: str = Field(description="The subject of the message.")
|
|
441
|
+
body: str = Field(description="The message content to send.")
|
|
442
|
+
|
|
443
|
+
tool_dict = pydantic_model_to_tool(send_response, name="send_response", style=style)
|
|
444
|
+
|
|
445
|
+
target_param = (
|
|
446
|
+
tool_dict["function"]["parameters"]["properties"]["target"]
|
|
447
|
+
if style == "completions"
|
|
448
|
+
else tool_dict["parameters"]["properties"]["target"]
|
|
449
|
+
)
|
|
450
|
+
if enable_interswarm:
|
|
451
|
+
# For interswarm messaging, we don't restrict to enum values
|
|
452
|
+
# The validation will happen at runtime
|
|
453
|
+
target_param["description"] = (
|
|
454
|
+
target_param["description"]
|
|
455
|
+
+ " (supports interswarm format: agent-name@swarm-name)"
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
target_param["enum"] = targets # This provides the allowed values to the LLM
|
|
459
|
+
|
|
460
|
+
return tool_dict
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
SEND_RESPONSE_HELP_STRING = """
|
|
464
|
+
Send a message to a specific target recipient agent (the `target`).
|
|
465
|
+
This is useful for responding to a message from another agent.
|
|
466
|
+
|
|
467
|
+
# Example
|
|
468
|
+
A MAIL swarm `example` has agents `supervisor` and `math`.
|
|
469
|
+
`supervisor` can interact with the user and complete the task, while `math` can call the `calculate_expression` action.
|
|
470
|
+
Say a user creates a task for solving the expression `2 * (3 + 4)`:
|
|
471
|
+
|
|
472
|
+
```xml
|
|
473
|
+
<message>
|
|
474
|
+
<sender type="user">example_user</sender>
|
|
475
|
+
<recipient type="agent">supervisor</recipient>
|
|
476
|
+
<subject>New Message</subject>
|
|
477
|
+
<body>What is 2 * (3 + 4)?</body>
|
|
478
|
+
<msg_type>request</msg_type>
|
|
479
|
+
</message>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`supervisor` can then delegate the task to `math`:
|
|
483
|
+
|
|
484
|
+
```xml
|
|
485
|
+
<message>
|
|
486
|
+
<sender type="agent">supervisor</sender>
|
|
487
|
+
<recipient type="agent">math</recipient>
|
|
488
|
+
<subject>Math problem</subject>
|
|
489
|
+
<body>User asked: 'What is 2 * (3 + 4)?' Please solve the expression and respond to me with the result.</body>
|
|
490
|
+
<msg_type>request</msg_type>
|
|
491
|
+
</message>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`math` can then call the `calculate_expression` action, and send the result back to `supervisor` in the form of a response:
|
|
495
|
+
|
|
496
|
+
```xml
|
|
497
|
+
<message>
|
|
498
|
+
<sender type="agent">math</sender>
|
|
499
|
+
<recipient type="agent">supervisor</recipient>
|
|
500
|
+
<subject>Re: Math problem</subject>
|
|
501
|
+
<body>The result is 14.</body>
|
|
502
|
+
<msg_type>response</msg_type>
|
|
503
|
+
</message>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Finally, `supervisor` can call the `task_complete` tool with the final answer to the user's task (in this case, the result of the expression `2 * (3 + 4)`).
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def create_interrupt_tool(
|
|
511
|
+
targets: list[str],
|
|
512
|
+
enable_interswarm: bool = False,
|
|
513
|
+
style: Literal["completions", "responses"] = "completions",
|
|
514
|
+
) -> dict[str, Any]:
|
|
515
|
+
"""
|
|
516
|
+
Create a MAIL interrupt tool to interrupt specific agents.
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
class send_interrupt(BaseModel):
|
|
520
|
+
"""Interrupt a specific target recipient agent."""
|
|
521
|
+
|
|
522
|
+
target: str = Field(
|
|
523
|
+
description=f"The target recipient agent for the interrupt. Must be one of: {', '.join(targets)}"
|
|
524
|
+
+ (
|
|
525
|
+
", or use 'agent-name@swarm-name' format for interswarm messaging"
|
|
526
|
+
if enable_interswarm
|
|
527
|
+
else ""
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
subject: str = Field(description="The subject of the interrupt.")
|
|
531
|
+
body: str = Field(description="The message content to send.")
|
|
532
|
+
|
|
533
|
+
tool_dict = pydantic_model_to_tool(
|
|
534
|
+
send_interrupt, name="send_interrupt", style=style
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
target_param = (
|
|
538
|
+
tool_dict["function"]["parameters"]["properties"]["target"]
|
|
539
|
+
if style == "completions"
|
|
540
|
+
else tool_dict["parameters"]["properties"]["target"]
|
|
541
|
+
)
|
|
542
|
+
if enable_interswarm:
|
|
543
|
+
target_param["description"] = (
|
|
544
|
+
target_param["description"]
|
|
545
|
+
+ " (supports interswarm format: agent-name@swarm-name)"
|
|
546
|
+
)
|
|
547
|
+
else:
|
|
548
|
+
target_param["enum"] = targets # This provides the allowed values to the LLM
|
|
549
|
+
|
|
550
|
+
return tool_dict
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
SEND_INTERRUPT_HELP_STRING = """
|
|
554
|
+
Interrupt a specific target recipient agent (the `target`).
|
|
555
|
+
This is useful for interrupting a specific agent's execution.
|
|
556
|
+
|
|
557
|
+
# Example
|
|
558
|
+
A MAIL swarm `example` has agents `supervisor` and `wra`.
|
|
559
|
+
`supervisor` can interact with the user and complete the task, while `wra` can call the actions `start_web_research` and `fetch_web_research`.
|
|
560
|
+
Say a user creates a task to start web research on the topic of "effect of climate change by 2050":
|
|
561
|
+
|
|
562
|
+
```xml
|
|
563
|
+
<message>
|
|
564
|
+
<sender type="user">example_user</sender>
|
|
565
|
+
<recipient type="agent">supervisor</recipient>
|
|
566
|
+
<subject>New Message</subject>
|
|
567
|
+
<body>Start web research on the topic of "effect of climate change by 2050".</body>
|
|
568
|
+
<msg_type>request</msg_type>
|
|
569
|
+
</message>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`supervisor` can then delegate the task to `wra`:
|
|
573
|
+
|
|
574
|
+
```xml
|
|
575
|
+
<message>
|
|
576
|
+
<sender type="agent">supervisor</sender>
|
|
577
|
+
<recipient type="agent">wra</recipient>
|
|
578
|
+
<subject>Web research</subject>
|
|
579
|
+
<body>User asked: 'Start web research on the topic of "effect of climate change by 2050".' Please start the research and respond to me with the result.</body>
|
|
580
|
+
<msg_type>request</msg_type>
|
|
581
|
+
</message>
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
From there, `wra` can call `start_web_research` to begin the research process for the user's query.
|
|
585
|
+
However, this task is taking too long to complete--perhaps due to API issues.
|
|
586
|
+
The user sends an interrupt to `supervisor` to check on the status of the research:
|
|
587
|
+
|
|
588
|
+
```xml
|
|
589
|
+
<message>
|
|
590
|
+
<sender type="user">example_user</sender>
|
|
591
|
+
<recipient type="agent">supervisor</recipient>
|
|
592
|
+
<subject>New Message</subject>
|
|
593
|
+
<body>The web research process is taking too long to complete. Can you check on the status?</body>
|
|
594
|
+
<msg_type>interrupt</msg_type>
|
|
595
|
+
</message>
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
`supervisor` can then call the `send_interrupt` tool to interrupt `wra`:
|
|
599
|
+
|
|
600
|
+
```xml
|
|
601
|
+
<message>
|
|
602
|
+
<sender type="agent">supervisor</sender>
|
|
603
|
+
<recipient type="agent">wra</recipient>
|
|
604
|
+
<subject>Check on web research status</subject>
|
|
605
|
+
<body>The web research process for the query "effect of climate change by 2050" is taking too long to complete. Can you check on the status?</body>
|
|
606
|
+
<msg_type>interrupt</msg_type>
|
|
607
|
+
</message>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
From there, `wra` can call `fetch_web_research` to get the status of the research process.
|
|
611
|
+
The agent can then notify `supervisor` of the status of the research process:
|
|
612
|
+
|
|
613
|
+
```xml
|
|
614
|
+
<message>
|
|
615
|
+
<sender type="agent">wra</sender>
|
|
616
|
+
<recipient type="agent">supervisor</recipient>
|
|
617
|
+
<subject>Web research status</subject>
|
|
618
|
+
<body>I checked the status on the web research process. It has not yet completed.</body>
|
|
619
|
+
<msg_type>response</msg_type>
|
|
620
|
+
</message>
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Finally, `supervisor` can call `task_complete` to inform the user of the research process status (in this case, not yet completed).
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def create_interswarm_broadcast_tool(
|
|
628
|
+
style: Literal["completions", "responses"] = "completions",
|
|
629
|
+
) -> dict[str, Any]:
|
|
630
|
+
"""
|
|
631
|
+
Create a MAIL broadcast tool for interswarm communication.
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
class send_interswarm_broadcast(BaseModel):
|
|
635
|
+
"""Broadcast a message to all known swarms."""
|
|
636
|
+
|
|
637
|
+
subject: str = Field(description="The subject of the broadcast.")
|
|
638
|
+
body: str = Field(description="The message content to send.")
|
|
639
|
+
target_swarms: list[str] = Field(
|
|
640
|
+
description="List of target swarm names. If empty, broadcasts to all known swarms.",
|
|
641
|
+
default=[],
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return pydantic_model_to_tool(
|
|
645
|
+
send_interswarm_broadcast, name="send_interswarm_broadcast", style=style
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def create_swarm_discovery_tool(
|
|
650
|
+
style: Literal["completions", "responses"] = "completions",
|
|
651
|
+
) -> dict[str, Any]:
|
|
652
|
+
"""
|
|
653
|
+
Create a tool for discovering and registering swarms.
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
class discover_swarms(BaseModel):
|
|
657
|
+
"""Discover and register new swarms from discovery endpoints."""
|
|
658
|
+
|
|
659
|
+
discovery_urls: list[str] = Field(
|
|
660
|
+
description="List of URLs to discover swarms from."
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
return pydantic_model_to_tool(discover_swarms, name="discover_swarms", style=style)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def create_broadcast_tool(
|
|
667
|
+
style: Literal["completions", "responses"] = "completions",
|
|
668
|
+
) -> dict[str, Any]:
|
|
669
|
+
"""
|
|
670
|
+
Create a MAIL broadcast tool to broadcast messages to a specified list of agents, or all agents in the swarm.
|
|
671
|
+
"""
|
|
672
|
+
|
|
673
|
+
class send_broadcast(BaseModel):
|
|
674
|
+
"""Broadcast a message to a specified list of agents, or all agents in the swarm."""
|
|
675
|
+
|
|
676
|
+
subject: str = Field(description="The subject of the broadcast.")
|
|
677
|
+
body: str = Field(description="The message content to send.")
|
|
678
|
+
targets: list[str] = Field(
|
|
679
|
+
description="The list of agents to broadcast to. Use ['all'] to broadcast to all agents in the swarm."
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return pydantic_model_to_tool(send_broadcast, name="send_broadcast", style=style)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
SEND_BROADCAST_HELP_STRING = """
|
|
686
|
+
Broadcast a message to a specified list of agents, or all agents in the swarm.
|
|
687
|
+
This is useful for disseminating information relevant to the recipients.
|
|
688
|
+
|
|
689
|
+
# Example
|
|
690
|
+
A MAIL swarm `example` has agents `supervisor`, `weather`, and `wra`.
|
|
691
|
+
The supervisor can interact with the user and complete the task, while `weather` and `wra` have their own actions.
|
|
692
|
+
Say a user creates a task that utilizes both `weather` and `wra`:
|
|
693
|
+
|
|
694
|
+
```xml
|
|
695
|
+
<message>
|
|
696
|
+
<sender type="user">example_user</sender>
|
|
697
|
+
<recipient type="agent">supervisor</recipient>
|
|
698
|
+
<subject>New Message</subject>
|
|
699
|
+
<body>Create a report on the impact of climate change by 2050.</body>
|
|
700
|
+
<msg_type>request</msg_type>
|
|
701
|
+
</message>
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
`supervisor` can then delegate specific tasks to `weather` and `wra` before preparing a final response.
|
|
705
|
+
`weather` and `wra` may communicate with each other directly, not just through `supervisor`.
|
|
706
|
+
However, `weather` does not seem to be working properly--it's unable to process `supervisor`'s messages.
|
|
707
|
+
Upon seeing this, `supervisor` sends a broadcast to `wra` to inform it of the issue:
|
|
708
|
+
|
|
709
|
+
```xml
|
|
710
|
+
<message>
|
|
711
|
+
<sender type="agent">supervisor</sender>
|
|
712
|
+
<recipients>
|
|
713
|
+
<recipient type="agent">wra</recipient>
|
|
714
|
+
</recipients>
|
|
715
|
+
<subject>Weather issue</subject>
|
|
716
|
+
<body>The weather agent is not responding to messages. Keep this in mind when preparing the report. I will let you know if there are any updates.</body>
|
|
717
|
+
<msg_type>broadcast</msg_type>
|
|
718
|
+
</message>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Alternatively, `supervisor` can send a broadcast to all agents in the swarm (including `weather`):
|
|
722
|
+
|
|
723
|
+
```xml
|
|
724
|
+
<message>
|
|
725
|
+
<sender type="agent">supervisor</sender>
|
|
726
|
+
<recipients>
|
|
727
|
+
<recipient type="agent">all</recipient>
|
|
728
|
+
</recipients>
|
|
729
|
+
<subject>Weather issue</subject>
|
|
730
|
+
<body>The weather agent is not responding to messages. Keep this in mind for this task. I will let you know if there are any updates.</body>
|
|
731
|
+
<msg_type>broadcast</msg_type>
|
|
732
|
+
</message>
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Broadcast recipients can then acknowledge the broadcast and continue with their work.
|
|
736
|
+
When the user's task is done, `task_complete` is called by `supervisor`.
|
|
737
|
+
"""
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def create_acknowledge_broadcast_tool(
|
|
741
|
+
style: Literal["completions", "responses"] = "completions",
|
|
742
|
+
) -> dict[str, Any]:
|
|
743
|
+
"""
|
|
744
|
+
Create a tool for agents to acknowledge a broadcast without replying.
|
|
745
|
+
When invoked, the runtime will store the incoming broadcast in the agent's
|
|
746
|
+
memory and will not emit any outgoing MAIL message.
|
|
747
|
+
"""
|
|
748
|
+
|
|
749
|
+
class acknowledge_broadcast(BaseModel):
|
|
750
|
+
"""
|
|
751
|
+
Store the received broadcast in memory, do not respond.
|
|
752
|
+
This MUST NOT be used in response to a message of type other than 'broadcast'.
|
|
753
|
+
Not all broadcasts warrant acknowledgement--only those that do not warrant a response.
|
|
754
|
+
"""
|
|
755
|
+
|
|
756
|
+
# Use Optional to avoid PEP 604 UnionType issues in some converters
|
|
757
|
+
note: Optional[str] = Field( # noqa: UP045
|
|
758
|
+
default=None,
|
|
759
|
+
description="Optional note to include in internal memory only.",
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
return pydantic_model_to_tool(
|
|
763
|
+
acknowledge_broadcast, name="acknowledge_broadcast", style=style
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
ACKNOWLEDGE_BROADCAST_HELP_STRING = """
|
|
768
|
+
Acknowledge a broadcast and store in memory without replying.
|
|
769
|
+
This is useful for cases where a broadcast does not warrant a response message.
|
|
770
|
+
|
|
771
|
+
# Example
|
|
772
|
+
A MAIL swarm `example` has agents `supervisor`, `weather`, and `wra`.
|
|
773
|
+
The supervisor can interact with the user and complete the task, while `weather` and `wra` have their own actions.
|
|
774
|
+
Say a user creates a task that utilizes both `weather` and `wra`:
|
|
775
|
+
|
|
776
|
+
```xml
|
|
777
|
+
<message>
|
|
778
|
+
<sender type="user">example_user</sender>
|
|
779
|
+
<recipient type="agent">supervisor</recipient>
|
|
780
|
+
<subject>New Message</subject>
|
|
781
|
+
<body>Create a report on the impact of climate change by 2050.</body>
|
|
782
|
+
<msg_type>request</msg_type>
|
|
783
|
+
</message>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
`supervisor` can then delegate specific tasks to `weather` and `wra` before preparing a final response.
|
|
787
|
+
`weather` and `wra` may communicate with each other directly, not just through `supervisor`.
|
|
788
|
+
However, `weather` does not seem to be working properly--it's unable to process `supervisor`'s messages.
|
|
789
|
+
Upon seeing this, `supervisor` sends a broadcast to `wra` to inform it of the issue:
|
|
790
|
+
|
|
791
|
+
```xml
|
|
792
|
+
<message>
|
|
793
|
+
<sender type="agent">supervisor</sender>
|
|
794
|
+
<recipients>
|
|
795
|
+
<recipient type="agent">wra</recipient>
|
|
796
|
+
</recipients>
|
|
797
|
+
<subject>Weather issue</subject>
|
|
798
|
+
<body>The weather agent is not responding to messages. Keep this in mind when preparing the report. I will let you know if there are any updates.</body>
|
|
799
|
+
<msg_type>broadcast</msg_type>
|
|
800
|
+
</message>
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Upon receiving the broadcast, `wra` can acknowledge the broadcast without replying:
|
|
804
|
+
|
|
805
|
+
```python
|
|
806
|
+
acknowledge_broadcast(note="Acknowledged that `weather` is not responding to messages.")
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
No response message is sent to `supervisor`, but the broadcast is stored in `wra`'s memory.
|
|
810
|
+
From there, the swarm can continue with their work until `task_complete` is called by `supervisor`.
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def create_ignore_broadcast_tool(
|
|
815
|
+
style: Literal["completions", "responses"] = "completions",
|
|
816
|
+
) -> dict[str, Any]:
|
|
817
|
+
"""
|
|
818
|
+
Create a tool for agents to ignore a broadcast entirely.
|
|
819
|
+
When invoked, the runtime will neither store nor respond to the broadcast.
|
|
820
|
+
"""
|
|
821
|
+
|
|
822
|
+
class ignore_broadcast(BaseModel):
|
|
823
|
+
"""Ignore the received broadcast. No memory, no response."""
|
|
824
|
+
|
|
825
|
+
# Use Optional to avoid PEP 604 UnionType issues in some converters
|
|
826
|
+
reason: Optional[str] = Field( # noqa: UP045
|
|
827
|
+
default=None,
|
|
828
|
+
description="Optional internal reason for ignoring (not sent).",
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
return pydantic_model_to_tool(
|
|
832
|
+
ignore_broadcast, name="ignore_broadcast", style=style
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
IGNORE_BROADCAST_HELP_STRING = """
|
|
837
|
+
Ignore a broadcast by not storing or responding to it.
|
|
838
|
+
This is useful for cases where a broadcast does not warrant a response message.
|
|
839
|
+
Note that the broadcast itself is not saved, but the tool call is.
|
|
840
|
+
|
|
841
|
+
# Example
|
|
842
|
+
A MAIL swarm `example` has agents `supervisor`, `weather`, and `wra`.
|
|
843
|
+
The supervisor can interact with the user and complete the task, while `weather` and `wra` have their own actions.
|
|
844
|
+
`supervisor` can communicate with both `weather` and `wra` directly, but `weather` and `wra` cannot communicate with each other.
|
|
845
|
+
Say a user creates a task that utilizes both `weather` and `wra`:
|
|
846
|
+
|
|
847
|
+
```xml
|
|
848
|
+
<message>
|
|
849
|
+
<sender type="user">example_user</sender>
|
|
850
|
+
<recipient type="agent">supervisor</recipient>
|
|
851
|
+
<subject>New Message</subject>
|
|
852
|
+
<body>Create a report on the impact of climate change by 2050.</body>
|
|
853
|
+
<msg_type>request</msg_type>
|
|
854
|
+
</message>
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
`supervisor` can then delegate specific tasks to `weather` and `wra` before preparing a final response.
|
|
858
|
+
`weather` and `wra` may communicate with each other directly, not just through `supervisor`.
|
|
859
|
+
However, `weather` does not seem to be working properly--it's unable to process `supervisor`'s messages.
|
|
860
|
+
Upon seeing this, `supervisor` sends a broadcast to `wra` to inform it of the issue:
|
|
861
|
+
|
|
862
|
+
```xml
|
|
863
|
+
<message>
|
|
864
|
+
<sender type="agent">supervisor</sender>
|
|
865
|
+
<recipients>
|
|
866
|
+
<recipient type="agent">wra</recipient>
|
|
867
|
+
</recipients>
|
|
868
|
+
<subject>Weather issue</subject>
|
|
869
|
+
<body>The weather agent is not responding to messages. Keep this in mind when preparing the report. I will let you know if there are any updates.</body>
|
|
870
|
+
<msg_type>broadcast</msg_type>
|
|
871
|
+
</message>
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
However, since `weather` and `wra` cannot communicate with each other, `wra` can ignore the broadcast without replying:
|
|
875
|
+
|
|
876
|
+
```python
|
|
877
|
+
ignore_broadcast(reason="`weather` is not responding to messages.")
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
No response message is sent to `supervisor`, but the broadcast is ignored.
|
|
881
|
+
No broadcast is saved in `wra`'s memory, but the tool call is.
|
|
882
|
+
From there, the swarm can continue with their work until `task_complete` is called by `supervisor`.
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def create_task_complete_tool(
|
|
887
|
+
style: Literal["completions", "responses"] = "completions",
|
|
888
|
+
) -> dict[str, Any]:
|
|
889
|
+
"""
|
|
890
|
+
Create a MAIL task complete tool to indicate that a task has been completed.
|
|
891
|
+
"""
|
|
892
|
+
|
|
893
|
+
class task_complete(BaseModel):
|
|
894
|
+
"""Indicate that a task has been completed. This will end the current loop, and should always be the last tool called."""
|
|
895
|
+
|
|
896
|
+
finish_message: str = Field(
|
|
897
|
+
description="""The final response to the user's task.
|
|
898
|
+
Since the user cannot see the swarm's communication, you MUST include the full answer to the user's task.
|
|
899
|
+
Furthermore, this broadcast will be sent to all agents in the swarm to notify them that the task has been completed.
|
|
900
|
+
"""
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
return pydantic_model_to_tool(task_complete, name="task_complete", style=style)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
TASK_COMPLETE_HELP_STRING = """
|
|
907
|
+
Indicate that the current task has been completed, and provide the user a final response.
|
|
908
|
+
This will end the current loop, and should always be the last tool called.
|
|
909
|
+
|
|
910
|
+
# Example
|
|
911
|
+
A MAIL swarm `example` has agents `supervisor` and `weather`.
|
|
912
|
+
The supervisor can interact with the user and complete the task, while `weather` can call the `get_weather_forecast` action.
|
|
913
|
+
Say a user creates a task for getting the weather forecast in Tokyo:
|
|
914
|
+
|
|
915
|
+
```xml
|
|
916
|
+
<message>
|
|
917
|
+
<sender type="user">example_user</sender>
|
|
918
|
+
<recipient type="agent">supervisor</recipient>
|
|
919
|
+
<subject>New Message</subject>
|
|
920
|
+
<body>What will the weather be like in Tokyo tomorrow?</body>
|
|
921
|
+
<msg_type>request</msg_type>
|
|
922
|
+
</message>
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
`supervisor` can then delegate the task to `weather`:
|
|
926
|
+
|
|
927
|
+
```xml
|
|
928
|
+
<message>
|
|
929
|
+
<sender type="agent">supervisor</sender>
|
|
930
|
+
<recipient type="agent">weather</recipient>
|
|
931
|
+
<subject>Weather forecast in Tokyo</subject>
|
|
932
|
+
<body>User asked: 'What will the weather be like in Tokyo tomorrow?' Please get the forecast and respond to me with the result.</body>
|
|
933
|
+
<msg_type>request</msg_type>
|
|
934
|
+
</message>
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
From there, `weather` can call the `get_weather_forecast` action and send the result back to `supervisor` in the form of a response:
|
|
938
|
+
|
|
939
|
+
```xml
|
|
940
|
+
<message>
|
|
941
|
+
<sender type="agent">weather</sender>
|
|
942
|
+
<recipient type="agent">supervisor</recipient>
|
|
943
|
+
<subject>Re: Weather forecast in Tokyo</subject>
|
|
944
|
+
<body>The weather forecast for Tokyo tomorrow is sunny with a temperature of 20 degrees Celsius.</body>
|
|
945
|
+
<msg_type>response</msg_type>
|
|
946
|
+
</message>
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Finally, `supervisor` can call the `task_complete` tool with the final answer to the user's task (in this case, the weather forecast for Tokyo tomorrow).
|
|
950
|
+
|
|
951
|
+
```python
|
|
952
|
+
task_complete(finish_message="The weather forecast for Tokyo tomorrow is sunny with a temperature of 20 degrees Celsius.")
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
The finish message will be sent to the user.
|
|
956
|
+
Furthermore, the finish message will be sent to all agents in the swarm to notify them that the task has been completed:
|
|
957
|
+
|
|
958
|
+
```xml
|
|
959
|
+
<message>
|
|
960
|
+
<sender type="agent">supervisor</sender>
|
|
961
|
+
<recipients>
|
|
962
|
+
<recipient type="agent">all</recipient>
|
|
963
|
+
</recipients>
|
|
964
|
+
<subject>Re: Weather forecast in Tokyo</subject>
|
|
965
|
+
<body>The weather forecast for Tokyo tomorrow is sunny with a temperature of 20 degrees Celsius.</body>
|
|
966
|
+
<msg_type>broadcast_complete</msg_type>
|
|
967
|
+
</message>
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
Note that the broadcast_complete saved in agent memory for all recipients, but no recipients are reprompted.
|
|
971
|
+
The `task_complete` call shuts down the task runtime.
|
|
972
|
+
"""
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def create_await_message_tool(
|
|
976
|
+
style: Literal["completions", "responses"] = "completions",
|
|
977
|
+
) -> dict[str, Any]:
|
|
978
|
+
"""
|
|
979
|
+
Create a MAIL await message tool to wait for a message.
|
|
980
|
+
"""
|
|
981
|
+
|
|
982
|
+
class await_message(BaseModel):
|
|
983
|
+
"""Wait until another message is received."""
|
|
984
|
+
|
|
985
|
+
reason: Optional[str] = Field( # noqa: UP045
|
|
986
|
+
default=None,
|
|
987
|
+
description="Optional reason for waiting.",
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
return pydantic_model_to_tool(await_message, name="await_message", style=style)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
AWAIT_MESSAGE_HELP_STRING = """
|
|
994
|
+
Wait until another message is received.
|
|
995
|
+
This is useful for cases where an agent needs to wait for a message before continuing.
|
|
996
|
+
|
|
997
|
+
# Example
|
|
998
|
+
A MAIL swarm `example` has agents `supervisor` and `wra`.
|
|
999
|
+
The supervisor can interact with the user and complete the task, while `wra` can call the `perform_web_research` action.
|
|
1000
|
+
Say a user creates a task for performing web research on the topic of "economic impact of AI by 2030":
|
|
1001
|
+
|
|
1002
|
+
```xml
|
|
1003
|
+
<message>
|
|
1004
|
+
<sender type="user">example_user</sender>
|
|
1005
|
+
<recipient type="agent">supervisor</recipient>
|
|
1006
|
+
<subject>New Message</subject>
|
|
1007
|
+
<body>Perform web research on the topic of "economic impact of AI by 2030".</body>
|
|
1008
|
+
<msg_type>request</msg_type>
|
|
1009
|
+
</message>
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
`supervisor` can then delegate the task to `wra`:
|
|
1013
|
+
|
|
1014
|
+
```xml
|
|
1015
|
+
<message>
|
|
1016
|
+
<sender type="agent">supervisor</sender>
|
|
1017
|
+
<recipient type="agent">wra</recipient>
|
|
1018
|
+
<subject>Web research</subject>
|
|
1019
|
+
<body>User asked: 'Perform web research on the topic of "economic impact of AI by 2030".' Please perform the research and respond to me with the result.</body>
|
|
1020
|
+
<msg_type>request</msg_type>
|
|
1021
|
+
</message>
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
From there, `wra` can call the `perform_web_research` action, and respond to `supervisor` to inform it that the research has started:
|
|
1025
|
+
|
|
1026
|
+
```xml
|
|
1027
|
+
<message>
|
|
1028
|
+
<sender type="agent">wra</sender>
|
|
1029
|
+
<recipient type="agent">supervisor</recipient>
|
|
1030
|
+
<subject>Web research</subject>
|
|
1031
|
+
<body>I have started the web research. When the process is complete, I will respond to you with the result.</body>
|
|
1032
|
+
<msg_type>response</msg_type>
|
|
1033
|
+
</message>
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
In this case, `supervisor` has nothing else to do before research is complete.
|
|
1037
|
+
Since `wra` will receive an '::action_complete_broadcast::' message when the research is complete, `supervisor` can call the `await_message` tool to wait for `wra`'s response:
|
|
1038
|
+
|
|
1039
|
+
```python
|
|
1040
|
+
await_message(reason="Waiting for wra's response.")
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
When `perform_web_research` is complete, `wra` will respond to `supervisor` with the result of the research:
|
|
1044
|
+
|
|
1045
|
+
```xml
|
|
1046
|
+
<message>
|
|
1047
|
+
<sender type="agent">wra</sender>
|
|
1048
|
+
<recipient type="agent">supervisor</recipient>
|
|
1049
|
+
<subject>Web research</subject>
|
|
1050
|
+
<body>The result of the web research is as follows. ...</body>
|
|
1051
|
+
<msg_type>response</msg_type>
|
|
1052
|
+
</message>
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
This reprompts `supervisor`, who can proceed with the task until completion.
|
|
1056
|
+
"""
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def create_help_tool(
|
|
1060
|
+
style: Literal["completions", "responses"] = "completions",
|
|
1061
|
+
) -> dict[str, Any]:
|
|
1062
|
+
"""
|
|
1063
|
+
Create a MAIL help tool to get help with MAIL.
|
|
1064
|
+
"""
|
|
1065
|
+
|
|
1066
|
+
class help(BaseModel):
|
|
1067
|
+
"""Get help with MAIL."""
|
|
1068
|
+
|
|
1069
|
+
get_summary: bool = Field(
|
|
1070
|
+
default=True, description="Whether to get a short summary of MAIL."
|
|
1071
|
+
)
|
|
1072
|
+
get_identity: bool = Field(
|
|
1073
|
+
default=False,
|
|
1074
|
+
description="Whether to get your identity (agent name, swarm, etc.).",
|
|
1075
|
+
)
|
|
1076
|
+
get_tool_help: list[
|
|
1077
|
+
Literal[
|
|
1078
|
+
"send_request",
|
|
1079
|
+
"send_response",
|
|
1080
|
+
"send_broadcast",
|
|
1081
|
+
"send_interrupt",
|
|
1082
|
+
"acknowledge_broadcast",
|
|
1083
|
+
"ignore_broadcast",
|
|
1084
|
+
"await_message",
|
|
1085
|
+
"task_complete",
|
|
1086
|
+
]
|
|
1087
|
+
] = Field(default=[], description="The tools to get help for.")
|
|
1088
|
+
get_full_protocol: bool = Field(
|
|
1089
|
+
default=False,
|
|
1090
|
+
description="Whether to get the full MAIL protocol specification.",
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
return pydantic_model_to_tool(help, name="help", style=style)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def create_mail_tools(
|
|
1097
|
+
targets: list[str],
|
|
1098
|
+
enable_interswarm: bool = False,
|
|
1099
|
+
style: Literal["completions", "responses"] = "completions",
|
|
1100
|
+
exclude_tools: list[str] | None = None,
|
|
1101
|
+
) -> list[dict[str, Any]]:
|
|
1102
|
+
"""
|
|
1103
|
+
Create MAIL tools. These should be used for all agents.
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
targets: The agents that the agent can send messages to.
|
|
1107
|
+
enable_interswarm: Whether the agent can send interswarm messages.
|
|
1108
|
+
style: The style of the tools to create.
|
|
1109
|
+
exclude_tools: The names of MAIL tools that should not be available.
|
|
1110
|
+
"""
|
|
1111
|
+
exclude_tools = exclude_tools or []
|
|
1112
|
+
all_tools = [
|
|
1113
|
+
create_request_tool(targets, enable_interswarm, style),
|
|
1114
|
+
create_response_tool(targets, enable_interswarm, style),
|
|
1115
|
+
create_acknowledge_broadcast_tool(style),
|
|
1116
|
+
create_ignore_broadcast_tool(style),
|
|
1117
|
+
create_await_message_tool(style),
|
|
1118
|
+
create_help_tool(style),
|
|
1119
|
+
]
|
|
1120
|
+
|
|
1121
|
+
# filter out the excluded tools
|
|
1122
|
+
final_tools: list[dict[str, Any]] = []
|
|
1123
|
+
for tool in all_tools:
|
|
1124
|
+
tool_name = get_tool_spec_name(tool)
|
|
1125
|
+
if tool_name is None or tool_name not in exclude_tools:
|
|
1126
|
+
final_tools.append(tool)
|
|
1127
|
+
|
|
1128
|
+
return final_tools
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def create_supervisor_tools(
|
|
1132
|
+
targets: list[str],
|
|
1133
|
+
can_complete_tasks: bool = True,
|
|
1134
|
+
enable_interswarm: bool = False,
|
|
1135
|
+
exclude_tools: list[str] | None = None,
|
|
1136
|
+
style: Literal["completions", "responses"] = "completions",
|
|
1137
|
+
_debug_include_intraswarm: bool = True,
|
|
1138
|
+
) -> list[dict[str, Any]]:
|
|
1139
|
+
"""
|
|
1140
|
+
Create MAIL supervisor-exclusive tools.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
targets: The agents that the supervisor can send messages to.
|
|
1144
|
+
can_complete_tasks: Whether the supervisor can complete tasks.
|
|
1145
|
+
enable_interswarm: Whether the supervisor can send interswarm messages.
|
|
1146
|
+
exclude_tools: The names of MAIL tools that should not be available.
|
|
1147
|
+
style: The style of the tools to create.
|
|
1148
|
+
"""
|
|
1149
|
+
exclude_tools = exclude_tools or []
|
|
1150
|
+
tools: list[dict[str, Any]] = []
|
|
1151
|
+
if _debug_include_intraswarm:
|
|
1152
|
+
tools += [
|
|
1153
|
+
create_interrupt_tool(targets, enable_interswarm, style),
|
|
1154
|
+
create_broadcast_tool(style),
|
|
1155
|
+
]
|
|
1156
|
+
|
|
1157
|
+
if enable_interswarm:
|
|
1158
|
+
tools += [
|
|
1159
|
+
create_interswarm_broadcast_tool(style),
|
|
1160
|
+
create_swarm_discovery_tool(style),
|
|
1161
|
+
]
|
|
1162
|
+
|
|
1163
|
+
if can_complete_tasks:
|
|
1164
|
+
tools.append(create_task_complete_tool(style))
|
|
1165
|
+
|
|
1166
|
+
# filter out the excluded tools
|
|
1167
|
+
final_tools: list[dict[str, Any]] = []
|
|
1168
|
+
for tool in tools:
|
|
1169
|
+
tool_name = get_tool_spec_name(tool)
|
|
1170
|
+
if tool_name is None or tool_name not in exclude_tools:
|
|
1171
|
+
final_tools.append(tool)
|
|
1172
|
+
|
|
1173
|
+
return final_tools
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def get_tool_help(
|
|
1177
|
+
tools: list[str],
|
|
1178
|
+
) -> str:
|
|
1179
|
+
"""
|
|
1180
|
+
Create a MAIL tools help string for the given list of tool names.
|
|
1181
|
+
"""
|
|
1182
|
+
result = ""
|
|
1183
|
+
for tool in tools:
|
|
1184
|
+
match tool:
|
|
1185
|
+
case "send_request":
|
|
1186
|
+
tool_help = SEND_REQUEST_HELP_STRING
|
|
1187
|
+
case "send_response":
|
|
1188
|
+
tool_help = SEND_RESPONSE_HELP_STRING
|
|
1189
|
+
case "send_broadcast":
|
|
1190
|
+
tool_help = SEND_BROADCAST_HELP_STRING
|
|
1191
|
+
case "send_interrupt":
|
|
1192
|
+
tool_help = SEND_INTERRUPT_HELP_STRING
|
|
1193
|
+
case "acknowledge_broadcast":
|
|
1194
|
+
tool_help = ACKNOWLEDGE_BROADCAST_HELP_STRING
|
|
1195
|
+
case "ignore_broadcast":
|
|
1196
|
+
tool_help = IGNORE_BROADCAST_HELP_STRING
|
|
1197
|
+
case "await_message":
|
|
1198
|
+
tool_help = AWAIT_MESSAGE_HELP_STRING
|
|
1199
|
+
case "task_complete":
|
|
1200
|
+
tool_help = TASK_COMPLETE_HELP_STRING
|
|
1201
|
+
case _:
|
|
1202
|
+
raise ValueError(f"unknown tool name: {tool}")
|
|
1203
|
+
|
|
1204
|
+
result += f"===== Tool `{tool}` =====\n{tool_help}\n\n"
|
|
1205
|
+
|
|
1206
|
+
return result
|