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