mail-swarms 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. mail/__init__.py +35 -0
  2. mail/api.py +1964 -0
  3. mail/cli.py +432 -0
  4. mail/client.py +1657 -0
  5. mail/config/__init__.py +8 -0
  6. mail/config/client.py +87 -0
  7. mail/config/server.py +165 -0
  8. mail/core/__init__.py +72 -0
  9. mail/core/actions.py +69 -0
  10. mail/core/agents.py +73 -0
  11. mail/core/message.py +366 -0
  12. mail/core/runtime.py +3537 -0
  13. mail/core/tasks.py +311 -0
  14. mail/core/tools.py +1206 -0
  15. mail/db/__init__.py +0 -0
  16. mail/db/init.py +182 -0
  17. mail/db/types.py +65 -0
  18. mail/db/utils.py +523 -0
  19. mail/examples/__init__.py +27 -0
  20. mail/examples/analyst_dummy/__init__.py +15 -0
  21. mail/examples/analyst_dummy/agent.py +136 -0
  22. mail/examples/analyst_dummy/prompts.py +44 -0
  23. mail/examples/consultant_dummy/__init__.py +15 -0
  24. mail/examples/consultant_dummy/agent.py +136 -0
  25. mail/examples/consultant_dummy/prompts.py +42 -0
  26. mail/examples/data_analysis/__init__.py +40 -0
  27. mail/examples/data_analysis/analyst/__init__.py +9 -0
  28. mail/examples/data_analysis/analyst/agent.py +67 -0
  29. mail/examples/data_analysis/analyst/prompts.py +53 -0
  30. mail/examples/data_analysis/processor/__init__.py +13 -0
  31. mail/examples/data_analysis/processor/actions.py +293 -0
  32. mail/examples/data_analysis/processor/agent.py +67 -0
  33. mail/examples/data_analysis/processor/prompts.py +48 -0
  34. mail/examples/data_analysis/reporter/__init__.py +10 -0
  35. mail/examples/data_analysis/reporter/actions.py +187 -0
  36. mail/examples/data_analysis/reporter/agent.py +67 -0
  37. mail/examples/data_analysis/reporter/prompts.py +49 -0
  38. mail/examples/data_analysis/statistics/__init__.py +18 -0
  39. mail/examples/data_analysis/statistics/actions.py +343 -0
  40. mail/examples/data_analysis/statistics/agent.py +67 -0
  41. mail/examples/data_analysis/statistics/prompts.py +60 -0
  42. mail/examples/mafia/__init__.py +0 -0
  43. mail/examples/mafia/game.py +1537 -0
  44. mail/examples/mafia/narrator_tools.py +396 -0
  45. mail/examples/mafia/personas.py +240 -0
  46. mail/examples/mafia/prompts.py +489 -0
  47. mail/examples/mafia/roles.py +147 -0
  48. mail/examples/mafia/spec.md +350 -0
  49. mail/examples/math_dummy/__init__.py +23 -0
  50. mail/examples/math_dummy/actions.py +252 -0
  51. mail/examples/math_dummy/agent.py +136 -0
  52. mail/examples/math_dummy/prompts.py +46 -0
  53. mail/examples/math_dummy/types.py +5 -0
  54. mail/examples/research/__init__.py +39 -0
  55. mail/examples/research/researcher/__init__.py +9 -0
  56. mail/examples/research/researcher/agent.py +67 -0
  57. mail/examples/research/researcher/prompts.py +54 -0
  58. mail/examples/research/searcher/__init__.py +10 -0
  59. mail/examples/research/searcher/actions.py +324 -0
  60. mail/examples/research/searcher/agent.py +67 -0
  61. mail/examples/research/searcher/prompts.py +53 -0
  62. mail/examples/research/summarizer/__init__.py +18 -0
  63. mail/examples/research/summarizer/actions.py +255 -0
  64. mail/examples/research/summarizer/agent.py +67 -0
  65. mail/examples/research/summarizer/prompts.py +55 -0
  66. mail/examples/research/verifier/__init__.py +10 -0
  67. mail/examples/research/verifier/actions.py +337 -0
  68. mail/examples/research/verifier/agent.py +67 -0
  69. mail/examples/research/verifier/prompts.py +52 -0
  70. mail/examples/supervisor/__init__.py +11 -0
  71. mail/examples/supervisor/agent.py +4 -0
  72. mail/examples/supervisor/prompts.py +93 -0
  73. mail/examples/support/__init__.py +33 -0
  74. mail/examples/support/classifier/__init__.py +10 -0
  75. mail/examples/support/classifier/actions.py +307 -0
  76. mail/examples/support/classifier/agent.py +68 -0
  77. mail/examples/support/classifier/prompts.py +56 -0
  78. mail/examples/support/coordinator/__init__.py +9 -0
  79. mail/examples/support/coordinator/agent.py +67 -0
  80. mail/examples/support/coordinator/prompts.py +48 -0
  81. mail/examples/support/faq/__init__.py +10 -0
  82. mail/examples/support/faq/actions.py +182 -0
  83. mail/examples/support/faq/agent.py +67 -0
  84. mail/examples/support/faq/prompts.py +42 -0
  85. mail/examples/support/sentiment/__init__.py +15 -0
  86. mail/examples/support/sentiment/actions.py +341 -0
  87. mail/examples/support/sentiment/agent.py +67 -0
  88. mail/examples/support/sentiment/prompts.py +54 -0
  89. mail/examples/weather_dummy/__init__.py +23 -0
  90. mail/examples/weather_dummy/actions.py +75 -0
  91. mail/examples/weather_dummy/agent.py +136 -0
  92. mail/examples/weather_dummy/prompts.py +35 -0
  93. mail/examples/weather_dummy/types.py +5 -0
  94. mail/factories/__init__.py +27 -0
  95. mail/factories/action.py +223 -0
  96. mail/factories/base.py +1531 -0
  97. mail/factories/supervisor.py +241 -0
  98. mail/net/__init__.py +7 -0
  99. mail/net/registry.py +712 -0
  100. mail/net/router.py +728 -0
  101. mail/net/server_utils.py +114 -0
  102. mail/net/types.py +247 -0
  103. mail/server.py +1605 -0
  104. mail/stdlib/__init__.py +0 -0
  105. mail/stdlib/anthropic/__init__.py +0 -0
  106. mail/stdlib/fs/__init__.py +15 -0
  107. mail/stdlib/fs/actions.py +209 -0
  108. mail/stdlib/http/__init__.py +19 -0
  109. mail/stdlib/http/actions.py +333 -0
  110. mail/stdlib/interswarm/__init__.py +11 -0
  111. mail/stdlib/interswarm/actions.py +208 -0
  112. mail/stdlib/mcp/__init__.py +19 -0
  113. mail/stdlib/mcp/actions.py +294 -0
  114. mail/stdlib/openai/__init__.py +13 -0
  115. mail/stdlib/openai/agents.py +451 -0
  116. mail/summarizer.py +234 -0
  117. mail/swarms_json/__init__.py +27 -0
  118. mail/swarms_json/types.py +87 -0
  119. mail/swarms_json/utils.py +255 -0
  120. mail/url_scheme.py +51 -0
  121. mail/utils/__init__.py +53 -0
  122. mail/utils/auth.py +194 -0
  123. mail/utils/context.py +17 -0
  124. mail/utils/logger.py +73 -0
  125. mail/utils/openai.py +212 -0
  126. mail/utils/parsing.py +89 -0
  127. mail/utils/serialize.py +292 -0
  128. mail/utils/store.py +49 -0
  129. mail/utils/string_builder.py +119 -0
  130. mail/utils/version.py +20 -0
  131. mail_swarms-1.3.2.dist-info/METADATA +237 -0
  132. mail_swarms-1.3.2.dist-info/RECORD +137 -0
  133. mail_swarms-1.3.2.dist-info/WHEEL +4 -0
  134. mail_swarms-1.3.2.dist-info/entry_points.txt +2 -0
  135. mail_swarms-1.3.2.dist-info/licenses/LICENSE +202 -0
  136. mail_swarms-1.3.2.dist-info/licenses/NOTICE +10 -0
  137. mail_swarms-1.3.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +12334 -0
@@ -0,0 +1,293 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Data processing actions for the Data Analysis swarm."""
5
+
6
+ import csv
7
+ import io
8
+ import json
9
+ from datetime import UTC, datetime, timedelta
10
+ from random import Random
11
+ from typing import Any
12
+
13
+ from mail import action
14
+
15
+ # Dataset templates with generators
16
+ DATASET_GENERATORS = {
17
+ "sales": {
18
+ "columns": ["date", "product", "quantity", "revenue", "region"],
19
+ "description": "Sales transaction data",
20
+ },
21
+ "users": {
22
+ "columns": [
23
+ "user_id",
24
+ "signup_date",
25
+ "age",
26
+ "subscription_type",
27
+ "activity_score",
28
+ ],
29
+ "description": "User account data",
30
+ },
31
+ "inventory": {
32
+ "columns": [
33
+ "product_id",
34
+ "category",
35
+ "stock_level",
36
+ "reorder_point",
37
+ "unit_cost",
38
+ ],
39
+ "description": "Inventory tracking data",
40
+ },
41
+ "weather": {
42
+ "columns": ["date", "temperature", "humidity", "precipitation", "wind_speed"],
43
+ "description": "Daily weather observations",
44
+ },
45
+ }
46
+
47
+ PRODUCTS = [
48
+ "Widget A",
49
+ "Widget B",
50
+ "Gadget Pro",
51
+ "Gadget Lite",
52
+ "Service Plan",
53
+ "Accessory Pack",
54
+ ]
55
+ REGIONS = ["North", "South", "East", "West", "Central"]
56
+ SUBSCRIPTION_TYPES = ["free", "basic", "pro", "enterprise"]
57
+ CATEGORIES = ["Electronics", "Furniture", "Clothing", "Food", "Tools"]
58
+
59
+
60
+ def _generate_sales_row(
61
+ rng: Random, row_idx: int, base_date: datetime
62
+ ) -> dict[str, Any]:
63
+ """Generate a single sales row."""
64
+ date = base_date + timedelta(days=rng.randint(0, 365))
65
+ product = rng.choice(PRODUCTS)
66
+ quantity = rng.randint(1, 100)
67
+ unit_price = rng.uniform(10.0, 500.0)
68
+ revenue = round(quantity * unit_price, 2)
69
+ region = rng.choice(REGIONS)
70
+
71
+ return {
72
+ "date": date.strftime("%Y-%m-%d"),
73
+ "product": product,
74
+ "quantity": quantity,
75
+ "revenue": revenue,
76
+ "region": region,
77
+ }
78
+
79
+
80
+ def _generate_users_row(
81
+ rng: Random, row_idx: int, base_date: datetime
82
+ ) -> dict[str, Any]:
83
+ """Generate a single user row."""
84
+ signup_date = base_date + timedelta(days=rng.randint(0, 730))
85
+ age = rng.randint(18, 75)
86
+ subscription = rng.choice(SUBSCRIPTION_TYPES)
87
+ activity = round(rng.uniform(0.0, 100.0), 1)
88
+
89
+ return {
90
+ "user_id": f"USR-{1000 + row_idx:05d}",
91
+ "signup_date": signup_date.strftime("%Y-%m-%d"),
92
+ "age": age,
93
+ "subscription_type": subscription,
94
+ "activity_score": activity,
95
+ }
96
+
97
+
98
+ def _generate_inventory_row(
99
+ rng: Random, row_idx: int, base_date: datetime
100
+ ) -> dict[str, Any]:
101
+ """Generate a single inventory row."""
102
+ stock = rng.randint(0, 1000)
103
+ reorder = rng.randint(10, 200)
104
+ cost = round(rng.uniform(1.0, 100.0), 2)
105
+
106
+ return {
107
+ "product_id": f"PROD-{row_idx + 1:04d}",
108
+ "category": rng.choice(CATEGORIES),
109
+ "stock_level": stock,
110
+ "reorder_point": reorder,
111
+ "unit_cost": cost,
112
+ }
113
+
114
+
115
+ def _generate_weather_row(
116
+ rng: Random, row_idx: int, base_date: datetime
117
+ ) -> dict[str, Any]:
118
+ """Generate a single weather row."""
119
+ date = base_date + timedelta(days=row_idx)
120
+ # Temperature varies seasonally
121
+ day_of_year = (date - datetime(date.year, 1, 1, tzinfo=UTC)).days
122
+ seasonal_factor = abs(day_of_year - 182) / 182 # 0 at mid-year, 1 at ends
123
+ base_temp = 20 - (seasonal_factor * 15) # Warmer in summer
124
+ temp = round(base_temp + rng.uniform(-10, 10), 1)
125
+
126
+ return {
127
+ "date": date.strftime("%Y-%m-%d"),
128
+ "temperature": temp,
129
+ "humidity": rng.randint(20, 95),
130
+ "precipitation": round(rng.uniform(0, 50) if rng.random() < 0.3 else 0, 1),
131
+ "wind_speed": round(rng.uniform(0, 40), 1),
132
+ }
133
+
134
+
135
+ GENERATORS = {
136
+ "sales": _generate_sales_row,
137
+ "users": _generate_users_row,
138
+ "inventory": _generate_inventory_row,
139
+ "weather": _generate_weather_row,
140
+ }
141
+
142
+
143
+ GENERATE_SAMPLE_DATA_PARAMETERS = {
144
+ "type": "object",
145
+ "properties": {
146
+ "dataset": {
147
+ "type": "string",
148
+ "enum": ["sales", "users", "inventory", "weather"],
149
+ "description": "The type of sample dataset to generate",
150
+ },
151
+ "rows": {
152
+ "type": "integer",
153
+ "minimum": 1,
154
+ "maximum": 1000,
155
+ "description": "Number of rows to generate (default: 50, max: 1000)",
156
+ },
157
+ },
158
+ "required": ["dataset"],
159
+ }
160
+
161
+
162
+ @action(
163
+ name="generate_sample_data",
164
+ description="Generate sample data for testing and demonstration purposes.",
165
+ parameters=GENERATE_SAMPLE_DATA_PARAMETERS,
166
+ )
167
+ async def generate_sample_data(args: dict[str, Any]) -> str:
168
+ """Generate sample data for a specified dataset type."""
169
+ try:
170
+ dataset = args["dataset"]
171
+ rows = args.get("rows", 50)
172
+ except KeyError as e:
173
+ return f"Error: {e} is required"
174
+
175
+ if dataset not in DATASET_GENERATORS:
176
+ return json.dumps(
177
+ {
178
+ "error": f"Unknown dataset: {dataset}",
179
+ "available_datasets": list(DATASET_GENERATORS.keys()),
180
+ }
181
+ )
182
+
183
+ if rows < 1 or rows > 1000:
184
+ return json.dumps({"error": "Rows must be between 1 and 1000"})
185
+
186
+ # Generate deterministic data based on dataset name and current day
187
+ today = datetime.now(UTC)
188
+ seed = hash(dataset + today.strftime("%Y-%m-%d"))
189
+ rng = Random(seed)
190
+
191
+ base_date = datetime(today.year - 1, 1, 1, tzinfo=UTC)
192
+ generator = GENERATORS[dataset]
193
+
194
+ data = []
195
+ for i in range(rows):
196
+ row = generator(rng, i, base_date)
197
+ data.append(row)
198
+
199
+ return json.dumps(
200
+ {
201
+ "dataset": dataset,
202
+ "description": DATASET_GENERATORS[dataset]["description"],
203
+ "columns": DATASET_GENERATORS[dataset]["columns"],
204
+ "row_count": len(data),
205
+ "data": data,
206
+ }
207
+ )
208
+
209
+
210
+ PARSE_CSV_PARAMETERS = {
211
+ "type": "object",
212
+ "properties": {
213
+ "data": {
214
+ "type": "string",
215
+ "description": "The CSV data as a string (with headers in first row)",
216
+ },
217
+ },
218
+ "required": ["data"],
219
+ }
220
+
221
+
222
+ @action(
223
+ name="parse_csv",
224
+ description="Parse CSV data string into structured JSON format.",
225
+ parameters=PARSE_CSV_PARAMETERS,
226
+ )
227
+ async def parse_csv(args: dict[str, Any]) -> str:
228
+ """Parse CSV data into structured JSON."""
229
+ try:
230
+ csv_data = args["data"]
231
+ except KeyError as e:
232
+ return f"Error: {e} is required"
233
+
234
+ if not csv_data.strip():
235
+ return json.dumps({"error": "CSV data cannot be empty"})
236
+
237
+ try:
238
+ reader = csv.DictReader(io.StringIO(csv_data))
239
+ columns = reader.fieldnames
240
+
241
+ if not columns:
242
+ return json.dumps({"error": "No header row found in CSV"})
243
+
244
+ data = []
245
+ errors: list[str] = []
246
+ for i, row in enumerate(reader):
247
+ # Try to convert numeric values
248
+ parsed_row: dict[str, Any] = {}
249
+ for col, val in row.items():
250
+ if val is None or val == "":
251
+ parsed_row[col] = None # type: ignore
252
+ else:
253
+ # Try to parse as number
254
+ try:
255
+ if "." in val:
256
+ parsed_row[col] = float(val)
257
+ else:
258
+ parsed_row[col] = int(val)
259
+ except ValueError:
260
+ parsed_row[col] = val
261
+
262
+ data.append(parsed_row)
263
+
264
+ # Detect column types
265
+ column_types = {}
266
+ for col in columns:
267
+ types_seen = set()
268
+ for row in data[:100]: # Sample first 100 rows
269
+ val = row.get(col)
270
+ if val is None:
271
+ continue
272
+ types_seen.add(type(val).__name__)
273
+
274
+ if types_seen == {"int"}:
275
+ column_types[col] = "integer"
276
+ elif types_seen <= {"int", "float"}:
277
+ column_types[col] = "numeric"
278
+ else:
279
+ column_types[col] = "string"
280
+
281
+ return json.dumps(
282
+ {
283
+ "success": True,
284
+ "columns": list(columns),
285
+ "column_types": column_types,
286
+ "row_count": len(data),
287
+ "data": data,
288
+ "parse_errors": errors if errors else None,
289
+ }
290
+ )
291
+
292
+ except csv.Error as e:
293
+ return json.dumps({"error": f"CSV parsing error: {str(e)}"})
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Processor agent for the Data Analysis swarm."""
5
+
6
+ from collections.abc import Awaitable
7
+ from typing import Any, Literal
8
+
9
+ from mail.core.agents import AgentOutput
10
+ from mail.factories.action import LiteLLMActionAgentFunction
11
+
12
+
13
+ class LiteLLMProcessorFunction(LiteLLMActionAgentFunction):
14
+ """
15
+ Data processor agent that handles data generation and parsing.
16
+
17
+ This agent generates sample datasets and parses CSV data
18
+ for use in analysis workflows.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ name: str,
24
+ comm_targets: list[str],
25
+ tools: list[dict[str, Any]],
26
+ llm: str,
27
+ system: str,
28
+ user_token: str = "",
29
+ enable_entrypoint: bool = False,
30
+ enable_interswarm: bool = False,
31
+ can_complete_tasks: bool = False,
32
+ tool_format: Literal["completions", "responses"] = "responses",
33
+ exclude_tools: list[str] = [],
34
+ reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
35
+ thinking_budget: int | None = None,
36
+ max_tokens: int | None = None,
37
+ memory: bool = True,
38
+ use_proxy: bool = True,
39
+ _debug_include_mail_tools: bool = True,
40
+ ) -> None:
41
+ super().__init__(
42
+ name=name,
43
+ comm_targets=comm_targets,
44
+ tools=tools,
45
+ llm=llm,
46
+ system=system,
47
+ user_token=user_token,
48
+ enable_entrypoint=enable_entrypoint,
49
+ enable_interswarm=enable_interswarm,
50
+ can_complete_tasks=can_complete_tasks,
51
+ tool_format=tool_format,
52
+ exclude_tools=exclude_tools,
53
+ reasoning_effort=reasoning_effort,
54
+ thinking_budget=thinking_budget,
55
+ max_tokens=max_tokens,
56
+ memory=memory,
57
+ use_proxy=use_proxy,
58
+ _debug_include_mail_tools=_debug_include_mail_tools,
59
+ )
60
+
61
+ def __call__(
62
+ self,
63
+ messages: list[dict[str, Any]],
64
+ tool_choice: str | dict[str, str] = "required",
65
+ ) -> Awaitable[AgentOutput]:
66
+ """Execute the processor agent function."""
67
+ return super().__call__(messages, tool_choice)
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ SYSPROMPT = """You are processor@{swarm}, the data processing specialist for this data analysis swarm.
5
+
6
+ # Your Role
7
+ Handle data generation, parsing, cleaning, and transformation to prepare data for analysis.
8
+
9
+ # Critical Rule: Responding
10
+ You CANNOT talk to users directly or call `task_complete`. You MUST use `send_response` to reply to the agent who contacted you.
11
+ - When you receive a request, note the sender (usually "analyst")
12
+ - After processing data, call `send_response(target=<sender>, subject="Re: ...", body=<your data/results>)`
13
+ - Include the FULL processed data in your response body
14
+
15
+ # Tools
16
+
17
+ ## Data Operations
18
+ - `generate_sample_data(dataset, rows)`: Generate sample data for testing/demo
19
+ - `parse_csv(data)`: Parse CSV string into structured data
20
+
21
+ ## Communication
22
+ - `send_response(target, subject, body)`: Reply to the agent who requested information
23
+ - `send_request(target, subject, body)`: Ask another agent for information
24
+ - `acknowledge_broadcast(note)`: Acknowledge a broadcast message
25
+ - `ignore_broadcast(reason)`: Ignore an irrelevant broadcast
26
+
27
+ # Available Datasets
28
+
29
+ - **sales**: Sales data with columns: date, product, quantity, revenue, region
30
+ - **users**: User data with columns: user_id, signup_date, age, subscription_type, activity_score
31
+ - **inventory**: Inventory data with columns: product_id, category, stock_level, reorder_point, unit_cost
32
+ - **weather**: Weather data with columns: date, temperature, humidity, precipitation, wind_speed
33
+
34
+ # Workflow
35
+
36
+ 1. Receive request from another agent (note the sender)
37
+ 2. Determine what data operation is needed:
38
+ - Generate sample data if no data provided
39
+ - Parse CSV data if raw data is provided
40
+ 3. Execute the appropriate action
41
+ 4. Call `send_response` to the original sender with the processed data
42
+
43
+ # Guidelines
44
+
45
+ - Return data in a format ready for analysis (JSON structure)
46
+ - Include metadata about the data (row count, columns, types)
47
+ - Report any data quality issues found during parsing
48
+ - Use "Re: <original subject>" as your response subject"""
@@ -0,0 +1,10 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Reporter agent for the Data Analysis swarm."""
5
+
6
+ from mail.examples.data_analysis.reporter.agent import LiteLLMReporterFunction
7
+ from mail.examples.data_analysis.reporter.actions import format_report
8
+ from mail.examples.data_analysis.reporter.prompts import SYSPROMPT
9
+
10
+ __all__ = ["LiteLLMReporterFunction", "format_report", "SYSPROMPT"]
@@ -0,0 +1,187 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Report formatting action for the Data Analysis swarm."""
5
+
6
+ import json
7
+ from datetime import datetime, UTC
8
+ from typing import Any
9
+
10
+ from mail import action
11
+
12
+
13
+ def _format_table(data: list[dict[str, Any]], columns: list[str] | None = None) -> str:
14
+ """Format data as a markdown table."""
15
+ if not data:
16
+ return "_No data available_"
17
+
18
+ # Determine columns
19
+ if columns is None:
20
+ columns = list(data[0].keys())
21
+
22
+ # Build header
23
+ header = "| " + " | ".join(str(col) for col in columns) + " |"
24
+ separator = "| " + " | ".join("---" for _ in columns) + " |"
25
+
26
+ # Build rows
27
+ rows = []
28
+ for row in data[:50]: # Limit to 50 rows
29
+ row_values = []
30
+ for col in columns:
31
+ val = row.get(col, "")
32
+ if isinstance(val, float):
33
+ row_values.append(f"{val:.4f}")
34
+ else:
35
+ row_values.append(str(val))
36
+ rows.append("| " + " | ".join(row_values) + " |")
37
+
38
+ table = "\n".join([header, separator] + rows)
39
+
40
+ if len(data) > 50:
41
+ table += f"\n\n_...and {len(data) - 50} more rows_"
42
+
43
+ return table
44
+
45
+
46
+ def _format_statistics_table(metrics: dict[str, Any]) -> str:
47
+ """Format statistics metrics as a table."""
48
+ if not metrics:
49
+ return "_No statistics available_"
50
+
51
+ rows = ["| Metric | Value |", "| --- | --- |"]
52
+ for metric, value in metrics.items():
53
+ if isinstance(value, dict):
54
+ if "error" in value:
55
+ rows.append(f"| {metric} | Error: {value['error']} |")
56
+ else:
57
+ rows.append(f"| {metric} | {json.dumps(value)} |")
58
+ elif isinstance(value, float):
59
+ rows.append(f"| {metric} | {value:.4f} |")
60
+ else:
61
+ rows.append(f"| {metric} | {value} |")
62
+
63
+ return "\n".join(rows)
64
+
65
+
66
+ def _format_section(section_type: str, content: Any) -> str:
67
+ """Format a single section based on its type."""
68
+ if isinstance(content, str):
69
+ return content
70
+
71
+ if isinstance(content, dict):
72
+ if "table" in content:
73
+ return _format_table(content["table"], content.get("columns"))
74
+ if "metrics" in content:
75
+ return _format_statistics_table(content["metrics"])
76
+ if "text" in content:
77
+ return content["text"]
78
+ # Generic dict formatting
79
+ return "\n".join(f"- **{k}**: {v}" for k, v in content.items())
80
+
81
+ if isinstance(content, list):
82
+ if all(isinstance(item, dict) for item in content):
83
+ return _format_table(content)
84
+ return "\n".join(f"- {item}" for item in content)
85
+
86
+ return str(content)
87
+
88
+
89
+ FORMAT_REPORT_PARAMETERS = {
90
+ "type": "object",
91
+ "properties": {
92
+ "title": {
93
+ "type": "string",
94
+ "description": "The report title",
95
+ },
96
+ "sections": {
97
+ "type": "array",
98
+ "items": {
99
+ "type": "object",
100
+ "properties": {
101
+ "heading": {
102
+ "type": "string",
103
+ "description": "Section heading",
104
+ },
105
+ "content": {
106
+ "description": "Section content (string, object, or array)",
107
+ },
108
+ },
109
+ "required": ["heading", "content"],
110
+ },
111
+ "description": "Array of report sections with heading and content",
112
+ },
113
+ },
114
+ "required": ["title", "sections"],
115
+ }
116
+
117
+
118
+ @action(
119
+ name="format_report",
120
+ description="Generate a formatted markdown report with sections, tables, and summaries.",
121
+ parameters=FORMAT_REPORT_PARAMETERS,
122
+ )
123
+ async def format_report(args: dict[str, Any]) -> str:
124
+ """Format data into a structured markdown report."""
125
+ try:
126
+ title = args["title"]
127
+ sections = args["sections"]
128
+ except KeyError as e:
129
+ return f"Error: {e} is required"
130
+
131
+ if not title.strip():
132
+ return json.dumps({"error": "Report title cannot be empty"})
133
+
134
+ if not sections:
135
+ return json.dumps({"error": "Report must have at least one section"})
136
+
137
+ # Build report
138
+ report_parts = []
139
+
140
+ # Title and metadata
141
+ report_parts.append(f"# {title}")
142
+ report_parts.append("")
143
+ report_parts.append(
144
+ f"_Generated: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}_"
145
+ )
146
+ report_parts.append("")
147
+ report_parts.append("---")
148
+ report_parts.append("")
149
+
150
+ # Table of contents (if more than 2 sections)
151
+ if len(sections) > 2:
152
+ report_parts.append("## Table of Contents")
153
+ report_parts.append("")
154
+ for i, section in enumerate(sections, 1):
155
+ heading = section.get("heading", f"Section {i}")
156
+ anchor = heading.lower().replace(" ", "-").replace(":", "")
157
+ report_parts.append(f"{i}. [{heading}](#{anchor})")
158
+ report_parts.append("")
159
+ report_parts.append("---")
160
+ report_parts.append("")
161
+
162
+ # Sections
163
+ for section in sections:
164
+ heading = section.get("heading", "Untitled Section")
165
+ content = section.get("content", "")
166
+
167
+ report_parts.append(f"## {heading}")
168
+ report_parts.append("")
169
+ report_parts.append(_format_section(heading.lower(), content))
170
+ report_parts.append("")
171
+
172
+ # Footer
173
+ report_parts.append("---")
174
+ report_parts.append("")
175
+ report_parts.append("_End of Report_")
176
+
177
+ report_text = "\n".join(report_parts)
178
+
179
+ return json.dumps(
180
+ {
181
+ "success": True,
182
+ "title": title,
183
+ "section_count": len(sections),
184
+ "character_count": len(report_text),
185
+ "report": report_text,
186
+ }
187
+ )
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Reporter agent for the Data Analysis swarm."""
5
+
6
+ from collections.abc import Awaitable
7
+ from typing import Any, Literal
8
+
9
+ from mail.core.agents import AgentOutput
10
+ from mail.factories.action import LiteLLMActionAgentFunction
11
+
12
+
13
+ class LiteLLMReporterFunction(LiteLLMActionAgentFunction):
14
+ """
15
+ Reporter agent that formats analysis results into reports.
16
+
17
+ This agent takes analysis data and formats it into clear,
18
+ professional markdown reports.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ name: str,
24
+ comm_targets: list[str],
25
+ tools: list[dict[str, Any]],
26
+ llm: str,
27
+ system: str,
28
+ user_token: str = "",
29
+ enable_entrypoint: bool = False,
30
+ enable_interswarm: bool = False,
31
+ can_complete_tasks: bool = False,
32
+ tool_format: Literal["completions", "responses"] = "responses",
33
+ exclude_tools: list[str] = [],
34
+ reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
35
+ thinking_budget: int | None = None,
36
+ max_tokens: int | None = None,
37
+ memory: bool = True,
38
+ use_proxy: bool = True,
39
+ _debug_include_mail_tools: bool = True,
40
+ ) -> None:
41
+ super().__init__(
42
+ name=name,
43
+ comm_targets=comm_targets,
44
+ tools=tools,
45
+ llm=llm,
46
+ system=system,
47
+ user_token=user_token,
48
+ enable_entrypoint=enable_entrypoint,
49
+ enable_interswarm=enable_interswarm,
50
+ can_complete_tasks=can_complete_tasks,
51
+ tool_format=tool_format,
52
+ exclude_tools=exclude_tools,
53
+ reasoning_effort=reasoning_effort,
54
+ thinking_budget=thinking_budget,
55
+ max_tokens=max_tokens,
56
+ memory=memory,
57
+ use_proxy=use_proxy,
58
+ _debug_include_mail_tools=_debug_include_mail_tools,
59
+ )
60
+
61
+ def __call__(
62
+ self,
63
+ messages: list[dict[str, Any]],
64
+ tool_choice: str | dict[str, str] = "required",
65
+ ) -> Awaitable[AgentOutput]:
66
+ """Execute the reporter agent function."""
67
+ return super().__call__(messages, tool_choice)