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/cli.py ADDED
@@ -0,0 +1,432 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Addison Kline
3
+
4
+ import argparse
5
+ import asyncio
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from mail import utils
12
+ from mail.client import MAILClientCLI
13
+ from mail.config import ClientConfig, ServerConfig
14
+ from mail.server import run_server
15
+ from mail.url_scheme import parse_swarm_url
16
+
17
+
18
+ def _str_to_bool(value: str | bool) -> bool:
19
+ """
20
+ Parse common string representations of booleans.
21
+ """
22
+
23
+ if isinstance(value, bool):
24
+ return value
25
+
26
+ normalized = value.lower()
27
+ if normalized in {"true", "t", "1", "yes", "y"}:
28
+ return True
29
+ if normalized in {"false", "f", "0", "no", "n"}:
30
+ return False
31
+
32
+ raise argparse.ArgumentTypeError(
33
+ f"invalid boolean value '{value}'; expected true/false"
34
+ )
35
+
36
+
37
+ def _run_server_with_args(args: argparse.Namespace) -> None:
38
+ """
39
+ Run a MAIL server with the given CLI args.
40
+ Given CLI args will override the defaults in the config file.
41
+ """
42
+ original_config_path = os.environ.get("MAIL_CONFIG_PATH")
43
+ env_overridden = False
44
+
45
+ try:
46
+ if args.config:
47
+ resolved_config = Path(args.config).expanduser().resolve()
48
+ os.environ["MAIL_CONFIG_PATH"] = str(resolved_config)
49
+ env_overridden = True
50
+
51
+ base_config = ServerConfig()
52
+
53
+ server_overrides: dict[str, object] = {}
54
+ if args.host is not None:
55
+ server_overrides["host"] = args.host
56
+ if args.port is not None:
57
+ server_overrides["port"] = args.port
58
+ if args.reload is not None:
59
+ server_overrides["reload"] = args.reload
60
+ if args.debug is not None:
61
+ server_overrides["debug"] = args.debug
62
+
63
+ swarm_overrides: dict[str, object] = {}
64
+ if args.swarm_name is not None:
65
+ swarm_overrides["name"] = args.swarm_name
66
+ if args.swarm_source is not None:
67
+ swarm_overrides["source"] = args.swarm_source
68
+ if args.swarm_registry is not None:
69
+ swarm_overrides["registry_file"] = args.swarm_registry
70
+
71
+ if swarm_overrides:
72
+ server_overrides["swarm"] = base_config.swarm.model_copy(
73
+ update=swarm_overrides
74
+ )
75
+
76
+ effective_config = (
77
+ base_config.model_copy(update=server_overrides)
78
+ if server_overrides
79
+ else base_config
80
+ )
81
+
82
+ run_server(cfg=effective_config)
83
+ finally:
84
+ if env_overridden:
85
+ if original_config_path is None:
86
+ os.environ.pop("MAIL_CONFIG_PATH", None)
87
+ else:
88
+ os.environ["MAIL_CONFIG_PATH"] = original_config_path
89
+
90
+
91
+ def _run_client_with_args(args: argparse.Namespace) -> None:
92
+ """
93
+ Run a MAIL client with the given CLI args.
94
+ """
95
+ # Parse swarm:// URLs if provided
96
+ swarm_url = parse_swarm_url(args.url)
97
+ if swarm_url:
98
+ if not swarm_url.server:
99
+ print("Error: swarm:// URL must include a 'server' parameter")
100
+ return
101
+ args.url = f"https://{swarm_url.server}"
102
+ if swarm_url.token and not args.api_key:
103
+ args.api_key = swarm_url.token
104
+
105
+ original_config_path = os.environ.get("MAIL_CONFIG_PATH")
106
+ env_overridden = False
107
+
108
+ try:
109
+ if args.config:
110
+ resolved_config = Path(args.config).expanduser().resolve()
111
+ os.environ["MAIL_CONFIG_PATH"] = str(resolved_config)
112
+ env_overridden = True
113
+
114
+ client_config = ClientConfig()
115
+ if args.timeout is not None:
116
+ client_config.timeout = args.timeout
117
+
118
+ if args.verbose:
119
+ client_config.verbose = True
120
+
121
+ client_cli = MAILClientCLI(args, config=client_config)
122
+ asyncio.run(client_cli.run())
123
+ finally:
124
+ if env_overridden:
125
+ if original_config_path is None:
126
+ os.environ.pop("MAIL_CONFIG_PATH", None)
127
+ else:
128
+ os.environ["MAIL_CONFIG_PATH"] = original_config_path
129
+
130
+
131
+ def _print_version(_args: argparse.Namespace) -> None:
132
+ """
133
+ Print the version of MAIL.
134
+ """
135
+ print(f"MAIL reference implementation version: {utils.get_version()}")
136
+ print(f"MAIL protocol version: {utils.get_protocol_version()}")
137
+ print(
138
+ "For a given MAIL reference implementation with version `x.y.z`, the protocol version is `x.y`"
139
+ )
140
+
141
+
142
+ def _run_db_init(_args: argparse.Namespace) -> None:
143
+ """
144
+ Initialize the database tables for MAIL.
145
+ """
146
+ from mail.db.init import create_tables
147
+
148
+ asyncio.run(create_tables())
149
+
150
+
151
+ def _register_url_handler(_args: argparse.Namespace) -> None:
152
+ """
153
+ Register the MAIL client as the OS handler for swarm:// URLs.
154
+ """
155
+ platform = sys.platform
156
+
157
+ if platform == "linux":
158
+ _register_linux()
159
+ elif platform == "darwin":
160
+ _register_macos()
161
+ elif platform.startswith("win"):
162
+ _register_windows()
163
+ else:
164
+ print(f"Unsupported platform: {platform}")
165
+
166
+
167
+ def _register_linux() -> None:
168
+ """
169
+ Register `swarm://` URL handler on Linux using XDG.
170
+ """
171
+ desktop_entry = """[Desktop Entry]
172
+ Name=MAIL Client
173
+ Comment=Connect to MAIL servers via swarm:// URLs
174
+ Exec=mail client %u
175
+ Type=Application
176
+ MimeType=x-scheme-handler/swarm;
177
+ Terminal=true
178
+ NoDisplay=true
179
+ """
180
+ applications_dir = Path.home() / ".local" / "share" / "applications"
181
+ applications_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ desktop_file = applications_dir / "mail-swarm.desktop"
184
+ desktop_file.write_text(desktop_entry)
185
+ print(f"Created {desktop_file}")
186
+
187
+ # Register as default handler
188
+ try:
189
+ subprocess.run(
190
+ ["xdg-mime", "default", "mail-swarm.desktop", "x-scheme-handler/swarm"],
191
+ check=True,
192
+ )
193
+ print("Registered as default handler for swarm:// URLs")
194
+ except FileNotFoundError:
195
+ print("Warning: xdg-mime not found. Please run manually:")
196
+ print(" xdg-mime default mail-swarm.desktop x-scheme-handler/swarm")
197
+ except subprocess.CalledProcessError as e:
198
+ print(f"Error registering handler: {e}")
199
+
200
+
201
+ def _register_macos() -> None:
202
+ """
203
+ Print instructions for macOS URL handler registration.
204
+ """
205
+ print("macOS URL handler registration requires app bundling.")
206
+ print()
207
+ print("To register swarm:// URLs, add this to your app's Info.plist:")
208
+ print()
209
+ print(" <key>CFBundleURLTypes</key>")
210
+ print(" <array>")
211
+ print(" <dict>")
212
+ print(" <key>CFBundleURLName</key>")
213
+ print(" <string>MAIL Swarm Protocol</string>")
214
+ print(" <key>CFBundleURLSchemes</key>")
215
+ print(" <array>")
216
+ print(" <string>swarm</string>")
217
+ print(" </array>")
218
+ print(" </dict>")
219
+ print(" </array>")
220
+
221
+
222
+ def _register_windows() -> None:
223
+ """
224
+ Print instructions for Windows URL handler registration.
225
+ """
226
+ mail_path = sys.executable.replace("python", "mail")
227
+ print("Windows URL handler registration requires registry access.")
228
+ print()
229
+ print("Run these commands in an Administrator PowerShell:")
230
+ print()
231
+ print(' New-Item -Path "HKCU:\\Software\\Classes\\swarm" -Force')
232
+ print(
233
+ ' Set-ItemProperty -Path "HKCU:\\Software\\Classes\\swarm" -Name "(Default)" -Value "URL:MAIL Swarm Protocol"'
234
+ )
235
+ print(
236
+ ' Set-ItemProperty -Path "HKCU:\\Software\\Classes\\swarm" -Name "URL Protocol" -Value ""'
237
+ )
238
+ print(
239
+ ' New-Item -Path "HKCU:\\Software\\Classes\\swarm\\shell\\open\\command" -Force'
240
+ )
241
+ print(
242
+ f' Set-ItemProperty -Path "HKCU:\\Software\\Classes\\swarm\\shell\\open\\command" -Name "(Default)" -Value \'"{mail_path}" client "%1"\''
243
+ )
244
+
245
+
246
+ def _run_ping(args: argparse.Namespace) -> None:
247
+ """
248
+ Ping a MAIL server to check connectivity.
249
+ """
250
+ import httpx
251
+
252
+ url = args.url
253
+ # Handle swarm:// URLs
254
+ swarm_url = parse_swarm_url(url)
255
+ if swarm_url:
256
+ if not swarm_url.server:
257
+ print("Error: swarm:// URL must include a 'server' parameter")
258
+ sys.exit(1)
259
+ url = f"https://{swarm_url.server}"
260
+
261
+ try:
262
+ with httpx.Client(timeout=args.timeout) as client:
263
+ response = client.get(f"{url.rstrip('/')}/health")
264
+ response.raise_for_status()
265
+ data = response.json()
266
+ print(
267
+ f"✓ {data.get('swarm_name', 'MAIL server')} is {data.get('status', 'up')}"
268
+ )
269
+ except httpx.ConnectError:
270
+ print(f"✗ Cannot connect to {url}")
271
+ sys.exit(1)
272
+ except httpx.HTTPStatusError as e:
273
+ print(f"✗ Server returned {e.response.status_code}")
274
+ sys.exit(1)
275
+ except Exception as e:
276
+ print(f"✗ Error: {e}")
277
+ sys.exit(1)
278
+
279
+
280
+ def main() -> None:
281
+ # top-level MAIL parser
282
+ parser = argparse.ArgumentParser(
283
+ prog="mail",
284
+ description="Multi-Agent Interface Layer reference implementation CLI",
285
+ epilog="For more information, see `README.md` and `docs/`",
286
+ )
287
+
288
+ # subparsers for each MAIL command
289
+ subparsers = parser.add_subparsers()
290
+
291
+ # command `server`
292
+ server_parser = subparsers.add_parser("server", help="start the MAIL server")
293
+ server_parser.set_defaults(func=_run_server_with_args)
294
+ server_parser.add_argument(
295
+ "-c",
296
+ "--config",
297
+ type=str,
298
+ required=False,
299
+ help="path to the MAIL configuration file",
300
+ )
301
+ server_parser.add_argument(
302
+ "-p",
303
+ "--port",
304
+ type=int,
305
+ required=False,
306
+ help="port to listen on",
307
+ )
308
+ server_parser.add_argument(
309
+ "-H",
310
+ "--host",
311
+ type=str,
312
+ required=False,
313
+ help="host to listen on",
314
+ )
315
+ server_parser.add_argument(
316
+ "-r",
317
+ "--reload",
318
+ action="store_true",
319
+ help="enable hot reloading",
320
+ )
321
+ server_parser.add_argument(
322
+ "-d",
323
+ "--debug",
324
+ action="store_true",
325
+ help="enable debug mode (enable optional endpoints)",
326
+ )
327
+ server_parser.add_argument(
328
+ "-n",
329
+ "--swarm-name",
330
+ type=str,
331
+ required=False,
332
+ help="name of the swarm",
333
+ )
334
+ server_parser.add_argument(
335
+ "-s",
336
+ "--swarm-source",
337
+ type=str,
338
+ required=False,
339
+ help="source of the swarm",
340
+ )
341
+ server_parser.add_argument(
342
+ "-rf",
343
+ "--swarm-registry",
344
+ type=str,
345
+ required=False,
346
+ help="registry file of the swarm",
347
+ )
348
+
349
+ # command `client`
350
+ client_parser = subparsers.add_parser("client", help="run the MAIL client")
351
+ client_parser.set_defaults(func=_run_client_with_args)
352
+ client_parser.add_argument(
353
+ "-c",
354
+ "--config",
355
+ type=str,
356
+ required=False,
357
+ help="path to the MAIL configuration file",
358
+ )
359
+ client_parser.add_argument(
360
+ "url",
361
+ type=str,
362
+ help="URL of the MAIL server (supports http://, https://, or swarm://)",
363
+ )
364
+ client_parser.add_argument(
365
+ "-ak",
366
+ "--api-key",
367
+ type=str,
368
+ required=False,
369
+ help="API key for the MAIL server",
370
+ )
371
+ client_parser.add_argument(
372
+ "-t",
373
+ "--timeout",
374
+ type=float,
375
+ required=False,
376
+ help="client request timeout time in seconds",
377
+ )
378
+ client_parser.add_argument(
379
+ "-v",
380
+ "--verbose",
381
+ action="store_true",
382
+ help="enable verbose output",
383
+ )
384
+
385
+ # command `version`
386
+ version_parser = subparsers.add_parser("version", help="print the version of MAIL")
387
+ version_parser.set_defaults(func=_print_version)
388
+
389
+ # command `db-init`
390
+ db_init_parser = subparsers.add_parser(
391
+ "db-init", help="initialize database tables for agent history persistence"
392
+ )
393
+ db_init_parser.set_defaults(func=_run_db_init)
394
+
395
+ # command `register`
396
+ register_parser = subparsers.add_parser(
397
+ "register", help="register as OS handler for swarm:// URLs"
398
+ )
399
+ register_parser.set_defaults(func=_register_url_handler)
400
+
401
+ # command `ping`
402
+ ping_parser = subparsers.add_parser(
403
+ "ping", help="check if a MAIL server is reachable"
404
+ )
405
+ ping_parser.set_defaults(func=_run_ping)
406
+ ping_parser.add_argument(
407
+ "url",
408
+ type=str,
409
+ help="URL of the MAIL server",
410
+ )
411
+ ping_parser.add_argument(
412
+ "-t",
413
+ "--timeout",
414
+ type=float,
415
+ default=5.0,
416
+ help="timeout in seconds (default: 5)",
417
+ )
418
+
419
+ # parse CLI args
420
+ args = parser.parse_args()
421
+
422
+ # if no command is provided, print the help
423
+ if not hasattr(args, "func"):
424
+ parser.print_help()
425
+ return
426
+
427
+ # run the command
428
+ args.func(args)
429
+
430
+
431
+ if __name__ == "__main__":
432
+ main()