codetether 1.2.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.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- ui/monitor.js +2662 -0
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spotless Bin Co MCP Tools
|
|
3
|
+
|
|
4
|
+
Exposes spotlessbinco marketing services through MCP, enabling agents
|
|
5
|
+
to access CreativeDirector, Campaigns, Automations, Audiences, and Analytics.
|
|
6
|
+
|
|
7
|
+
These tools bridge the A2A/MCP world to the spotlessbinco oRPC and Rust APIs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import aiohttp
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Configuration from environment
|
|
20
|
+
SPOTLESSBINCO_API_URL = os.environ.get(
|
|
21
|
+
'SPOTLESSBINCO_API_URL', 'http://localhost:8081'
|
|
22
|
+
)
|
|
23
|
+
SPOTLESSBINCO_RUST_URL = os.environ.get(
|
|
24
|
+
'SPOTLESSBINCO_RUST_URL', 'http://localhost:8080'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Shared HTTP session
|
|
28
|
+
_session: Optional[aiohttp.ClientSession] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _get_session() -> aiohttp.ClientSession:
|
|
32
|
+
"""Get or create shared HTTP session."""
|
|
33
|
+
global _session
|
|
34
|
+
if _session is None or _session.closed:
|
|
35
|
+
connector = aiohttp.TCPConnector(limit=50, limit_per_host=20)
|
|
36
|
+
_session = aiohttp.ClientSession(
|
|
37
|
+
connector=connector,
|
|
38
|
+
timeout=aiohttp.ClientTimeout(total=120),
|
|
39
|
+
headers={'Content-Type': 'application/json'},
|
|
40
|
+
)
|
|
41
|
+
return _session
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _call_api(
|
|
45
|
+
method: str,
|
|
46
|
+
url: str,
|
|
47
|
+
data: Optional[Dict] = None,
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""Make an API call to spotlessbinco."""
|
|
50
|
+
session = await _get_session()
|
|
51
|
+
try:
|
|
52
|
+
async with session.request(method, url, json=data) as resp:
|
|
53
|
+
text = await resp.text()
|
|
54
|
+
if resp.status in (200, 201):
|
|
55
|
+
try:
|
|
56
|
+
return json.loads(text)
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
return {'success': True, 'response': text}
|
|
59
|
+
else:
|
|
60
|
+
logger.error(f'API error {resp.status}: {text[:500]}')
|
|
61
|
+
return {'error': text[:500], 'status': resp.status}
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f'API call failed: {e}')
|
|
64
|
+
return {'error': str(e)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _call_orpc(procedure: str, data: Dict) -> Dict[str, Any]:
|
|
68
|
+
"""Call an oRPC procedure on the spotlessbinco TypeScript API."""
|
|
69
|
+
return await _call_api(
|
|
70
|
+
method='POST',
|
|
71
|
+
url=f'{SPOTLESSBINCO_API_URL}/orpc/{procedure}',
|
|
72
|
+
data=data,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def _call_rust(
|
|
77
|
+
endpoint: str, method: str = 'POST', data: Optional[Dict] = None
|
|
78
|
+
) -> Dict[str, Any]:
|
|
79
|
+
"""Call the spotlessbinco Rust API."""
|
|
80
|
+
return await _call_api(
|
|
81
|
+
method=method,
|
|
82
|
+
url=f'{SPOTLESSBINCO_RUST_URL}{endpoint}',
|
|
83
|
+
data=data,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# =============================================================================
|
|
88
|
+
# CREATIVE DIRECTOR TOOLS
|
|
89
|
+
# =============================================================================
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def spotless_generate_creative(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Generate an ad creative image from winning ad copy using CreativeDirector.
|
|
95
|
+
|
|
96
|
+
Uses Gemini for prompt engineering and Imagen-3.0 for image generation.
|
|
97
|
+
Returns asset_id, image_url, and the AI-enhanced visual prompt.
|
|
98
|
+
"""
|
|
99
|
+
concept = args.get('concept')
|
|
100
|
+
aspect_ratio = args.get('aspect_ratio', '1:1')
|
|
101
|
+
initiative_id = args.get('initiative_id')
|
|
102
|
+
|
|
103
|
+
if not concept:
|
|
104
|
+
return {'error': 'concept is required'}
|
|
105
|
+
|
|
106
|
+
result = await _call_rust(
|
|
107
|
+
'/api/creative-assets/generate',
|
|
108
|
+
data={
|
|
109
|
+
'concept': concept,
|
|
110
|
+
'aspect_ratio': aspect_ratio,
|
|
111
|
+
'initiative_id': initiative_id,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def spotless_batch_generate_creatives(
|
|
119
|
+
args: Dict[str, Any],
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Generate multiple ad creatives in batch from a list of concepts.
|
|
123
|
+
|
|
124
|
+
More efficient than calling generate_creative multiple times.
|
|
125
|
+
"""
|
|
126
|
+
concepts = args.get('concepts', [])
|
|
127
|
+
aspect_ratio = args.get('aspect_ratio', '1:1')
|
|
128
|
+
initiative_id = args.get('initiative_id')
|
|
129
|
+
|
|
130
|
+
if not concepts:
|
|
131
|
+
return {'error': 'concepts array is required'}
|
|
132
|
+
|
|
133
|
+
result = await _call_rust(
|
|
134
|
+
'/api/creative-assets/batch-generate',
|
|
135
|
+
data={
|
|
136
|
+
'concepts': concepts,
|
|
137
|
+
'aspect_ratio': aspect_ratio,
|
|
138
|
+
'initiative_id': initiative_id,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def spotless_get_top_creatives(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
146
|
+
"""
|
|
147
|
+
Get top performing creative assets ranked by performance score.
|
|
148
|
+
|
|
149
|
+
Use this to find winning creatives that should be scaled.
|
|
150
|
+
"""
|
|
151
|
+
limit = args.get('limit', 10)
|
|
152
|
+
|
|
153
|
+
result = await _call_rust(
|
|
154
|
+
f'/api/creative-assets/top-performers?limit={limit}', method='GET'
|
|
155
|
+
)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def spotless_analyze_creative_performance(
|
|
160
|
+
args: Dict[str, Any],
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
"""
|
|
163
|
+
Analyze creative concept performance and get AI recommendations.
|
|
164
|
+
|
|
165
|
+
Returns winning concepts, average performance, and recommendations
|
|
166
|
+
for what types of creatives to generate more of.
|
|
167
|
+
"""
|
|
168
|
+
result = await _call_rust(
|
|
169
|
+
'/api/creative-assets/analyze-concepts', method='GET'
|
|
170
|
+
)
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# =============================================================================
|
|
175
|
+
# CAMPAIGN MANAGEMENT TOOLS
|
|
176
|
+
# =============================================================================
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def spotless_create_campaign(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
180
|
+
"""
|
|
181
|
+
Create a marketing campaign on one or more ad platforms.
|
|
182
|
+
|
|
183
|
+
Supports Facebook, TikTok, and Google Ads. Uses the UnifiedCampaignManager
|
|
184
|
+
to deploy to multiple platforms with a single call.
|
|
185
|
+
"""
|
|
186
|
+
name = args.get('name')
|
|
187
|
+
platforms = args.get('platforms', ['facebook'])
|
|
188
|
+
objective = args.get('objective', 'CONVERSIONS')
|
|
189
|
+
budget = args.get('budget', 100)
|
|
190
|
+
budget_type = args.get('budget_type', 'daily')
|
|
191
|
+
targeting = args.get('targeting', {})
|
|
192
|
+
creative_asset_ids = args.get('creative_asset_ids', [])
|
|
193
|
+
funnel_id = args.get('funnel_id')
|
|
194
|
+
initiative_id = args.get('initiative_id')
|
|
195
|
+
|
|
196
|
+
if not name:
|
|
197
|
+
return {'error': 'name is required'}
|
|
198
|
+
|
|
199
|
+
result = await _call_orpc(
|
|
200
|
+
'campaigns/create',
|
|
201
|
+
{
|
|
202
|
+
'name': name,
|
|
203
|
+
'platforms': platforms,
|
|
204
|
+
'objective': objective,
|
|
205
|
+
'budget': budget,
|
|
206
|
+
'budgetType': budget_type,
|
|
207
|
+
'targeting': targeting,
|
|
208
|
+
'creativeAssetIds': creative_asset_ids,
|
|
209
|
+
'funnelId': funnel_id,
|
|
210
|
+
'initiativeId': initiative_id,
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def spotless_update_campaign_status(
|
|
218
|
+
args: Dict[str, Any],
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Update a campaign's status (active, paused, archived).
|
|
222
|
+
|
|
223
|
+
Use to pause underperforming campaigns or resume paused ones.
|
|
224
|
+
"""
|
|
225
|
+
campaign_id = args.get('campaign_id')
|
|
226
|
+
status = args.get('status')
|
|
227
|
+
|
|
228
|
+
if not campaign_id or not status:
|
|
229
|
+
return {'error': 'campaign_id and status are required'}
|
|
230
|
+
|
|
231
|
+
result = await _call_orpc(
|
|
232
|
+
'campaigns/updateStatus',
|
|
233
|
+
{
|
|
234
|
+
'id': campaign_id,
|
|
235
|
+
'status': status,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
async def spotless_update_campaign_budget(
|
|
243
|
+
args: Dict[str, Any],
|
|
244
|
+
) -> Dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Update a campaign's budget.
|
|
247
|
+
|
|
248
|
+
Use to scale up successful campaigns or reduce spend on underperformers.
|
|
249
|
+
"""
|
|
250
|
+
campaign_id = args.get('campaign_id')
|
|
251
|
+
budget = args.get('budget')
|
|
252
|
+
|
|
253
|
+
if not campaign_id or budget is None:
|
|
254
|
+
return {'error': 'campaign_id and budget are required'}
|
|
255
|
+
|
|
256
|
+
result = await _call_orpc(
|
|
257
|
+
'campaigns/updateBudget',
|
|
258
|
+
{
|
|
259
|
+
'id': campaign_id,
|
|
260
|
+
'budget': budget,
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def spotless_get_campaign_metrics(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Get performance metrics for a specific campaign.
|
|
270
|
+
|
|
271
|
+
Returns impressions, clicks, conversions, spend, CTR, CPC, ROAS, etc.
|
|
272
|
+
"""
|
|
273
|
+
campaign_id = args.get('campaign_id')
|
|
274
|
+
|
|
275
|
+
if not campaign_id:
|
|
276
|
+
return {'error': 'campaign_id is required'}
|
|
277
|
+
|
|
278
|
+
result = await _call_orpc('campaigns/getMetrics', {'id': campaign_id})
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def spotless_list_campaigns(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
List all campaigns, optionally filtered by status or initiative.
|
|
285
|
+
"""
|
|
286
|
+
status = args.get('status')
|
|
287
|
+
initiative_id = args.get('initiative_id')
|
|
288
|
+
platform = args.get('platform')
|
|
289
|
+
|
|
290
|
+
params = {}
|
|
291
|
+
if status:
|
|
292
|
+
params['status'] = status
|
|
293
|
+
if initiative_id:
|
|
294
|
+
params['initiativeId'] = initiative_id
|
|
295
|
+
if platform:
|
|
296
|
+
params['platform'] = platform
|
|
297
|
+
|
|
298
|
+
result = await _call_orpc('campaigns/list', params)
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# =============================================================================
|
|
303
|
+
# AUTOMATION TOOLS
|
|
304
|
+
# =============================================================================
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def spotless_create_automation(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
308
|
+
"""
|
|
309
|
+
Create an automation workflow for email/SMS sequences.
|
|
310
|
+
|
|
311
|
+
Automations can be triggered by form submissions, tags, purchases,
|
|
312
|
+
or upsell declines. Steps include email, SMS, wait, conditions, and tags.
|
|
313
|
+
"""
|
|
314
|
+
name = args.get('name')
|
|
315
|
+
trigger_type = args.get('trigger_type', 'form_submit')
|
|
316
|
+
trigger_config = args.get('trigger_config', {})
|
|
317
|
+
steps = args.get('steps', [])
|
|
318
|
+
auto_activate = args.get('auto_activate', True)
|
|
319
|
+
|
|
320
|
+
if not name:
|
|
321
|
+
return {'error': 'name is required'}
|
|
322
|
+
|
|
323
|
+
# Build workflow nodes and edges
|
|
324
|
+
nodes, edges = _build_automation_graph(trigger_type, trigger_config, steps)
|
|
325
|
+
|
|
326
|
+
result = await _call_orpc(
|
|
327
|
+
'automations/create',
|
|
328
|
+
{
|
|
329
|
+
'name': name,
|
|
330
|
+
'nodes': nodes,
|
|
331
|
+
'edges': edges,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Activate if requested
|
|
336
|
+
if result.get('success') and auto_activate and result.get('id'):
|
|
337
|
+
await _call_orpc(
|
|
338
|
+
'automations/updateStatus',
|
|
339
|
+
{
|
|
340
|
+
'id': result['id'],
|
|
341
|
+
'status': 'active',
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
result['status'] = 'active'
|
|
345
|
+
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _build_automation_graph(
|
|
350
|
+
trigger_type: str, trigger_config: Dict, steps: List
|
|
351
|
+
) -> tuple:
|
|
352
|
+
"""Build automation nodes and edges from step definitions."""
|
|
353
|
+
nodes = []
|
|
354
|
+
edges = []
|
|
355
|
+
|
|
356
|
+
# Start node
|
|
357
|
+
nodes.append(
|
|
358
|
+
{
|
|
359
|
+
'id': 'start-1',
|
|
360
|
+
'type': 'trigger',
|
|
361
|
+
'position': {'x': 100, 'y': 100},
|
|
362
|
+
'data': {
|
|
363
|
+
'type': 'start',
|
|
364
|
+
'label': 'Start',
|
|
365
|
+
'config': {'trigger': trigger_type, **trigger_config},
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
prev_node_id = 'start-1'
|
|
371
|
+
y_pos = 200
|
|
372
|
+
|
|
373
|
+
for i, step in enumerate(steps):
|
|
374
|
+
node_id = f'node-{i + 1}'
|
|
375
|
+
|
|
376
|
+
# Parse step type and config
|
|
377
|
+
if isinstance(step, str):
|
|
378
|
+
node_type, node_config, label = _parse_step_string(step)
|
|
379
|
+
else:
|
|
380
|
+
node_type = step.get('type', 'action')
|
|
381
|
+
node_config = step.get('config', {})
|
|
382
|
+
label = step.get('name', node_type)
|
|
383
|
+
|
|
384
|
+
nodes.append(
|
|
385
|
+
{
|
|
386
|
+
'id': node_id,
|
|
387
|
+
'type': 'action',
|
|
388
|
+
'position': {'x': 100, 'y': y_pos},
|
|
389
|
+
'data': {
|
|
390
|
+
'type': node_type,
|
|
391
|
+
'label': label,
|
|
392
|
+
'config': node_config,
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
edges.append(
|
|
398
|
+
{
|
|
399
|
+
'id': f'edge-{prev_node_id}-{node_id}',
|
|
400
|
+
'source': prev_node_id,
|
|
401
|
+
'target': node_id,
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
prev_node_id = node_id
|
|
406
|
+
y_pos += 100
|
|
407
|
+
|
|
408
|
+
# End node
|
|
409
|
+
nodes.append(
|
|
410
|
+
{
|
|
411
|
+
'id': 'end-1',
|
|
412
|
+
'type': 'end',
|
|
413
|
+
'position': {'x': 100, 'y': y_pos},
|
|
414
|
+
'data': {'type': 'end', 'label': 'End', 'config': {}},
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
edges.append(
|
|
418
|
+
{
|
|
419
|
+
'id': f'edge-{prev_node_id}-end-1',
|
|
420
|
+
'source': prev_node_id,
|
|
421
|
+
'target': 'end-1',
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
return nodes, edges
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _parse_step_string(step: str) -> tuple:
|
|
429
|
+
"""Parse a step string like 'wait_2_days' or 'welcome_email'."""
|
|
430
|
+
if step.startswith('wait_'):
|
|
431
|
+
parts = step.split('_')
|
|
432
|
+
duration = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 1
|
|
433
|
+
unit = parts[2] if len(parts) > 2 else 'days'
|
|
434
|
+
return 'wait', {'duration': duration, 'unit': unit}, step
|
|
435
|
+
elif 'email' in step:
|
|
436
|
+
return 'email', {'templateName': step}, step
|
|
437
|
+
elif 'sms' in step:
|
|
438
|
+
return 'sms', {'templateName': step}, step
|
|
439
|
+
elif 'call' in step:
|
|
440
|
+
return 'call', {}, step
|
|
441
|
+
elif 'tag' in step:
|
|
442
|
+
tag_name = step.replace('tag_', '').replace('_', ' ')
|
|
443
|
+
return 'tag', {'tagName': tag_name}, step
|
|
444
|
+
else:
|
|
445
|
+
return 'action', {}, step
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
async def spotless_trigger_automation(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
449
|
+
"""
|
|
450
|
+
Manually trigger automations of a specific type.
|
|
451
|
+
|
|
452
|
+
Useful for testing or triggering automations programmatically.
|
|
453
|
+
"""
|
|
454
|
+
trigger_type = args.get('trigger_type')
|
|
455
|
+
lead_id = args.get('lead_id')
|
|
456
|
+
customer_id = args.get('customer_id')
|
|
457
|
+
email = args.get('email')
|
|
458
|
+
metadata = args.get('metadata', {})
|
|
459
|
+
|
|
460
|
+
if not trigger_type:
|
|
461
|
+
return {'error': 'trigger_type is required'}
|
|
462
|
+
|
|
463
|
+
result = await _call_orpc(
|
|
464
|
+
'automations/trigger',
|
|
465
|
+
{
|
|
466
|
+
'triggerType': trigger_type,
|
|
467
|
+
'leadId': lead_id,
|
|
468
|
+
'customerId': customer_id,
|
|
469
|
+
'email': email,
|
|
470
|
+
'metadata': metadata,
|
|
471
|
+
},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def spotless_list_automations(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
478
|
+
"""
|
|
479
|
+
List all automation workflows.
|
|
480
|
+
"""
|
|
481
|
+
status = args.get('status')
|
|
482
|
+
|
|
483
|
+
result = await _call_orpc(
|
|
484
|
+
'automations/list', {'status': status} if status else {}
|
|
485
|
+
)
|
|
486
|
+
return result
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
async def spotless_update_automation_status(
|
|
490
|
+
args: Dict[str, Any],
|
|
491
|
+
) -> Dict[str, Any]:
|
|
492
|
+
"""
|
|
493
|
+
Update an automation's status (draft, active, paused, archived).
|
|
494
|
+
"""
|
|
495
|
+
automation_id = args.get('automation_id')
|
|
496
|
+
status = args.get('status')
|
|
497
|
+
|
|
498
|
+
if not automation_id or not status:
|
|
499
|
+
return {'error': 'automation_id and status are required'}
|
|
500
|
+
|
|
501
|
+
result = await _call_orpc(
|
|
502
|
+
'automations/updateStatus',
|
|
503
|
+
{
|
|
504
|
+
'id': automation_id,
|
|
505
|
+
'status': status,
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# =============================================================================
|
|
513
|
+
# AUDIENCE TOOLS
|
|
514
|
+
# =============================================================================
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
async def spotless_create_geo_audience(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
518
|
+
"""
|
|
519
|
+
Create a geographic targeting audience by zip codes.
|
|
520
|
+
|
|
521
|
+
Syncs to specified ad platforms (Facebook, TikTok, Google).
|
|
522
|
+
"""
|
|
523
|
+
name = args.get('name')
|
|
524
|
+
zip_codes = args.get('zip_codes', [])
|
|
525
|
+
platforms = args.get('platforms', ['facebook', 'tiktok'])
|
|
526
|
+
initiative_id = args.get('initiative_id')
|
|
527
|
+
|
|
528
|
+
if not name or not zip_codes:
|
|
529
|
+
return {'error': 'name and zip_codes are required'}
|
|
530
|
+
|
|
531
|
+
result = await _call_orpc(
|
|
532
|
+
'audiences/create',
|
|
533
|
+
{
|
|
534
|
+
'name': name,
|
|
535
|
+
'type': 'geo',
|
|
536
|
+
'initiativeId': initiative_id,
|
|
537
|
+
'targeting': {'zipCodes': zip_codes},
|
|
538
|
+
'platforms': platforms,
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
async def spotless_create_lookalike_audience(
|
|
546
|
+
args: Dict[str, Any],
|
|
547
|
+
) -> Dict[str, Any]:
|
|
548
|
+
"""
|
|
549
|
+
Create a lookalike audience from existing customers.
|
|
550
|
+
|
|
551
|
+
Sources: existing_customers, high_value_customers, recent_converters
|
|
552
|
+
"""
|
|
553
|
+
name = args.get('name')
|
|
554
|
+
source = args.get('source', 'existing_customers')
|
|
555
|
+
lookalike_percent = args.get('lookalike_percent', 1)
|
|
556
|
+
platforms = args.get('platforms', ['facebook', 'tiktok'])
|
|
557
|
+
initiative_id = args.get('initiative_id')
|
|
558
|
+
|
|
559
|
+
if not name:
|
|
560
|
+
return {'error': 'name is required'}
|
|
561
|
+
|
|
562
|
+
result = await _call_orpc(
|
|
563
|
+
'audiences/createLookalike',
|
|
564
|
+
{
|
|
565
|
+
'name': name,
|
|
566
|
+
'initiativeId': initiative_id,
|
|
567
|
+
'sourceType': source,
|
|
568
|
+
'lookalikePercent': lookalike_percent,
|
|
569
|
+
'platforms': platforms,
|
|
570
|
+
},
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
return result
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
async def spotless_create_custom_audience(
|
|
577
|
+
args: Dict[str, Any],
|
|
578
|
+
) -> Dict[str, Any]:
|
|
579
|
+
"""
|
|
580
|
+
Create a custom audience from email/phone lists.
|
|
581
|
+
|
|
582
|
+
For Customer Match on ad platforms.
|
|
583
|
+
"""
|
|
584
|
+
name = args.get('name')
|
|
585
|
+
emails = args.get('emails', [])
|
|
586
|
+
phones = args.get('phones', [])
|
|
587
|
+
platforms = args.get('platforms', ['facebook', 'tiktok', 'google'])
|
|
588
|
+
initiative_id = args.get('initiative_id')
|
|
589
|
+
|
|
590
|
+
if not name:
|
|
591
|
+
return {'error': 'name is required'}
|
|
592
|
+
if not emails and not phones:
|
|
593
|
+
return {'error': 'emails or phones are required'}
|
|
594
|
+
|
|
595
|
+
result = await _call_orpc(
|
|
596
|
+
'audiences/createCustom',
|
|
597
|
+
{
|
|
598
|
+
'name': name,
|
|
599
|
+
'initiativeId': initiative_id,
|
|
600
|
+
'emails': emails,
|
|
601
|
+
'phones': phones,
|
|
602
|
+
'platforms': platforms,
|
|
603
|
+
},
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
return result
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
async def spotless_get_trash_zone_zips(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
610
|
+
"""
|
|
611
|
+
Get zip codes for specified trash zones.
|
|
612
|
+
|
|
613
|
+
Useful for building geo audiences based on service areas.
|
|
614
|
+
"""
|
|
615
|
+
zone_ids = args.get('zone_ids', [])
|
|
616
|
+
|
|
617
|
+
if not zone_ids:
|
|
618
|
+
return {'error': 'zone_ids array is required'}
|
|
619
|
+
|
|
620
|
+
result = await _call_rust(
|
|
621
|
+
f'/api/trash-zones/zip-codes?zones={",".join(map(str, zone_ids))}',
|
|
622
|
+
method='GET',
|
|
623
|
+
)
|
|
624
|
+
return result
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# =============================================================================
|
|
628
|
+
# ANALYTICS TOOLS
|
|
629
|
+
# =============================================================================
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
async def spotless_get_unified_metrics(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
633
|
+
"""
|
|
634
|
+
Get unified marketing metrics aggregated across all ad platforms.
|
|
635
|
+
|
|
636
|
+
Returns impressions, clicks, conversions, spend, CTR, CPC, CPM, ROAS.
|
|
637
|
+
"""
|
|
638
|
+
start_date = args.get('start_date')
|
|
639
|
+
end_date = args.get('end_date')
|
|
640
|
+
initiative_id = args.get('initiative_id')
|
|
641
|
+
|
|
642
|
+
if not start_date or not end_date:
|
|
643
|
+
return {'error': 'start_date and end_date are required'}
|
|
644
|
+
|
|
645
|
+
result = await _call_orpc(
|
|
646
|
+
'analytics/getUnifiedMetrics',
|
|
647
|
+
{
|
|
648
|
+
'startDate': start_date,
|
|
649
|
+
'endDate': end_date,
|
|
650
|
+
'initiativeId': initiative_id,
|
|
651
|
+
},
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return result
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
async def spotless_get_roi_metrics(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
658
|
+
"""
|
|
659
|
+
Get ROI metrics combining ad spend with revenue from Stripe.
|
|
660
|
+
|
|
661
|
+
Returns total spend, total revenue, ROAS, and profit.
|
|
662
|
+
"""
|
|
663
|
+
start_date = args.get('start_date')
|
|
664
|
+
end_date = args.get('end_date')
|
|
665
|
+
|
|
666
|
+
if not start_date or not end_date:
|
|
667
|
+
return {'error': 'start_date and end_date are required'}
|
|
668
|
+
|
|
669
|
+
result = await _call_orpc(
|
|
670
|
+
'analytics/getROIMetrics',
|
|
671
|
+
{
|
|
672
|
+
'startDate': start_date,
|
|
673
|
+
'endDate': end_date,
|
|
674
|
+
},
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
return result
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
async def spotless_get_channel_performance(
|
|
681
|
+
args: Dict[str, Any],
|
|
682
|
+
) -> Dict[str, Any]:
|
|
683
|
+
"""
|
|
684
|
+
Get performance metrics for a specific marketing channel.
|
|
685
|
+
|
|
686
|
+
Channels: facebook, tiktok, google, email, sms, direct_mail
|
|
687
|
+
"""
|
|
688
|
+
channel = args.get('channel')
|
|
689
|
+
start_date = args.get('start_date')
|
|
690
|
+
end_date = args.get('end_date')
|
|
691
|
+
|
|
692
|
+
if not channel:
|
|
693
|
+
return {'error': 'channel is required'}
|
|
694
|
+
|
|
695
|
+
# Route to appropriate platform API
|
|
696
|
+
if channel in ('facebook', 'tiktok', 'google'):
|
|
697
|
+
result = await _call_orpc(
|
|
698
|
+
f'{channel}/getMetrics',
|
|
699
|
+
{
|
|
700
|
+
'startDate': start_date,
|
|
701
|
+
'endDate': end_date,
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
result = await _call_orpc(
|
|
706
|
+
'analytics/getChannelMetrics',
|
|
707
|
+
{
|
|
708
|
+
'channel': channel,
|
|
709
|
+
'startDate': start_date,
|
|
710
|
+
'endDate': end_date,
|
|
711
|
+
},
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
return result
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
async def spotless_thompson_sample_budget(
|
|
718
|
+
args: Dict[str, Any],
|
|
719
|
+
) -> Dict[str, Any]:
|
|
720
|
+
"""
|
|
721
|
+
Get optimal budget allocation using Thompson Sampling bandit algorithm.
|
|
722
|
+
|
|
723
|
+
Returns allocation percentages per channel and decision type (explore/exploit).
|
|
724
|
+
"""
|
|
725
|
+
channels = args.get('channels', ['meta_ads', 'tiktok_ads', 'door_hangers'])
|
|
726
|
+
initiative_id = args.get('initiative_id')
|
|
727
|
+
zip_code = args.get('zip_code')
|
|
728
|
+
|
|
729
|
+
result = await _call_rust(
|
|
730
|
+
'/api/ml/thompson-sample',
|
|
731
|
+
data={
|
|
732
|
+
'channels': channels,
|
|
733
|
+
'initiative_id': initiative_id,
|
|
734
|
+
'zip_code': zip_code,
|
|
735
|
+
},
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return result
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
async def spotless_get_conversion_attribution(
|
|
742
|
+
args: Dict[str, Any],
|
|
743
|
+
) -> Dict[str, Any]:
|
|
744
|
+
"""
|
|
745
|
+
Get multi-touch attribution data for conversions.
|
|
746
|
+
|
|
747
|
+
Shows the customer journey from first touch to conversion.
|
|
748
|
+
"""
|
|
749
|
+
customer_id = args.get('customer_id')
|
|
750
|
+
conversion_id = args.get('conversion_id')
|
|
751
|
+
|
|
752
|
+
if not customer_id and not conversion_id:
|
|
753
|
+
return {'error': 'customer_id or conversion_id is required'}
|
|
754
|
+
|
|
755
|
+
params = {}
|
|
756
|
+
if customer_id:
|
|
757
|
+
params['customer_id'] = customer_id
|
|
758
|
+
if conversion_id:
|
|
759
|
+
params['conversion_id'] = conversion_id
|
|
760
|
+
|
|
761
|
+
result = await _call_rust(
|
|
762
|
+
'/api/attribution/chain', method='GET', data=params
|
|
763
|
+
)
|
|
764
|
+
return result
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# =============================================================================
|
|
768
|
+
# PLATFORM-SPECIFIC TOOLS
|
|
769
|
+
# =============================================================================
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
async def spotless_sync_facebook_metrics(
|
|
773
|
+
args: Dict[str, Any],
|
|
774
|
+
) -> Dict[str, Any]:
|
|
775
|
+
"""
|
|
776
|
+
Sync campaign metrics from Facebook Ads.
|
|
777
|
+
|
|
778
|
+
Pulls latest impressions, clicks, spend, and conversions.
|
|
779
|
+
"""
|
|
780
|
+
result = await _call_orpc('facebook/syncMetrics', {})
|
|
781
|
+
return result
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
async def spotless_sync_tiktok_metrics(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
785
|
+
"""
|
|
786
|
+
Sync campaign metrics from TikTok Ads.
|
|
787
|
+
"""
|
|
788
|
+
result = await _call_orpc('tiktok/syncMetrics', {})
|
|
789
|
+
return result
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
async def spotless_sync_google_metrics(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
793
|
+
"""
|
|
794
|
+
Sync campaign metrics from Google Ads.
|
|
795
|
+
"""
|
|
796
|
+
result = await _call_orpc('google/syncMetrics', {})
|
|
797
|
+
return result
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
async def spotless_send_facebook_conversion(
|
|
801
|
+
args: Dict[str, Any],
|
|
802
|
+
) -> Dict[str, Any]:
|
|
803
|
+
"""
|
|
804
|
+
Send a conversion event to Facebook CAPI.
|
|
805
|
+
|
|
806
|
+
For server-side conversion tracking.
|
|
807
|
+
"""
|
|
808
|
+
event_name = args.get('event_name', 'Purchase')
|
|
809
|
+
email = args.get('email')
|
|
810
|
+
phone = args.get('phone')
|
|
811
|
+
value = args.get('value')
|
|
812
|
+
currency = args.get('currency', 'USD')
|
|
813
|
+
event_id = args.get('event_id')
|
|
814
|
+
|
|
815
|
+
result = await _call_orpc(
|
|
816
|
+
'facebook/sendConversion',
|
|
817
|
+
{
|
|
818
|
+
'eventName': event_name,
|
|
819
|
+
'email': email,
|
|
820
|
+
'phone': phone,
|
|
821
|
+
'value': value,
|
|
822
|
+
'currency': currency,
|
|
823
|
+
'eventId': event_id,
|
|
824
|
+
},
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
return result
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
async def spotless_send_tiktok_event(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
831
|
+
"""
|
|
832
|
+
Send an event to TikTok Events API.
|
|
833
|
+
"""
|
|
834
|
+
event_name = args.get('event_name')
|
|
835
|
+
email = args.get('email')
|
|
836
|
+
phone = args.get('phone')
|
|
837
|
+
value = args.get('value')
|
|
838
|
+
event_id = args.get('event_id')
|
|
839
|
+
|
|
840
|
+
result = await _call_orpc(
|
|
841
|
+
'tiktok/sendEvent',
|
|
842
|
+
{
|
|
843
|
+
'eventName': event_name,
|
|
844
|
+
'email': email,
|
|
845
|
+
'phone': phone,
|
|
846
|
+
'value': value,
|
|
847
|
+
'eventId': event_id,
|
|
848
|
+
},
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
# =============================================================================
|
|
855
|
+
# TOOL DEFINITIONS FOR MCP REGISTRATION
|
|
856
|
+
# =============================================================================
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def get_marketing_tools() -> List[Dict[str, Any]]:
|
|
860
|
+
"""
|
|
861
|
+
Get the list of marketing MCP tool definitions.
|
|
862
|
+
|
|
863
|
+
Returns tools formatted for MCP registration.
|
|
864
|
+
"""
|
|
865
|
+
return [
|
|
866
|
+
# Creative Director Tools
|
|
867
|
+
{
|
|
868
|
+
'name': 'spotless_generate_creative',
|
|
869
|
+
'description': 'Generate an ad creative image from winning ad copy using Gemini Imagen. Returns asset_id, image_url, and enhanced visual prompt.',
|
|
870
|
+
'inputSchema': {
|
|
871
|
+
'type': 'object',
|
|
872
|
+
'properties': {
|
|
873
|
+
'concept': {
|
|
874
|
+
'type': 'string',
|
|
875
|
+
'description': 'The ad copy or concept to visualize',
|
|
876
|
+
},
|
|
877
|
+
'aspect_ratio': {
|
|
878
|
+
'type': 'string',
|
|
879
|
+
'enum': ['1:1', '9:16', '16:9'],
|
|
880
|
+
'default': '1:1',
|
|
881
|
+
'description': 'Image aspect ratio (1:1 for feed, 9:16 for stories)',
|
|
882
|
+
},
|
|
883
|
+
'initiative_id': {
|
|
884
|
+
'type': 'string',
|
|
885
|
+
'description': 'Optional parent initiative ID',
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
'required': ['concept'],
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
'name': 'spotless_batch_generate_creatives',
|
|
893
|
+
'description': 'Generate multiple ad creatives in batch from a list of concepts. More efficient than individual calls.',
|
|
894
|
+
'inputSchema': {
|
|
895
|
+
'type': 'object',
|
|
896
|
+
'properties': {
|
|
897
|
+
'concepts': {
|
|
898
|
+
'type': 'array',
|
|
899
|
+
'items': {'type': 'string'},
|
|
900
|
+
'description': 'List of concepts to generate',
|
|
901
|
+
},
|
|
902
|
+
'aspect_ratio': {'type': 'string', 'default': '1:1'},
|
|
903
|
+
'initiative_id': {'type': 'string'},
|
|
904
|
+
},
|
|
905
|
+
'required': ['concepts'],
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
'name': 'spotless_get_top_creatives',
|
|
910
|
+
'description': 'Get top performing creative assets ranked by performance score. Use to find winning creatives to scale.',
|
|
911
|
+
'inputSchema': {
|
|
912
|
+
'type': 'object',
|
|
913
|
+
'properties': {
|
|
914
|
+
'limit': {
|
|
915
|
+
'type': 'integer',
|
|
916
|
+
'default': 10,
|
|
917
|
+
'description': 'Number of top creatives to return',
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
'name': 'spotless_analyze_creative_performance',
|
|
924
|
+
'description': 'Analyze creative concept performance and get AI recommendations for what to generate more of.',
|
|
925
|
+
'inputSchema': {'type': 'object', 'properties': {}},
|
|
926
|
+
},
|
|
927
|
+
# Campaign Tools
|
|
928
|
+
{
|
|
929
|
+
'name': 'spotless_create_campaign',
|
|
930
|
+
'description': 'Create a marketing campaign on Facebook, TikTok, or Google Ads. Supports multi-platform deployment.',
|
|
931
|
+
'inputSchema': {
|
|
932
|
+
'type': 'object',
|
|
933
|
+
'properties': {
|
|
934
|
+
'name': {'type': 'string', 'description': 'Campaign name'},
|
|
935
|
+
'platforms': {
|
|
936
|
+
'type': 'array',
|
|
937
|
+
'items': {
|
|
938
|
+
'type': 'string',
|
|
939
|
+
'enum': ['facebook', 'tiktok', 'google'],
|
|
940
|
+
},
|
|
941
|
+
'default': ['facebook'],
|
|
942
|
+
},
|
|
943
|
+
'objective': {
|
|
944
|
+
'type': 'string',
|
|
945
|
+
'enum': [
|
|
946
|
+
'CONVERSIONS',
|
|
947
|
+
'TRAFFIC',
|
|
948
|
+
'AWARENESS',
|
|
949
|
+
'LEADS',
|
|
950
|
+
],
|
|
951
|
+
'default': 'CONVERSIONS',
|
|
952
|
+
},
|
|
953
|
+
'budget': {
|
|
954
|
+
'type': 'number',
|
|
955
|
+
'description': 'Budget amount',
|
|
956
|
+
'default': 100,
|
|
957
|
+
},
|
|
958
|
+
'budget_type': {
|
|
959
|
+
'type': 'string',
|
|
960
|
+
'enum': ['daily', 'lifetime'],
|
|
961
|
+
'default': 'daily',
|
|
962
|
+
},
|
|
963
|
+
'targeting': {
|
|
964
|
+
'type': 'object',
|
|
965
|
+
'description': 'Audience targeting config',
|
|
966
|
+
},
|
|
967
|
+
'creative_asset_ids': {
|
|
968
|
+
'type': 'array',
|
|
969
|
+
'items': {'type': 'integer'},
|
|
970
|
+
'description': 'Creative asset IDs to use',
|
|
971
|
+
},
|
|
972
|
+
'funnel_id': {
|
|
973
|
+
'type': 'string',
|
|
974
|
+
'description': 'Funnel to link for attribution',
|
|
975
|
+
},
|
|
976
|
+
'initiative_id': {'type': 'string'},
|
|
977
|
+
},
|
|
978
|
+
'required': ['name'],
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
'name': 'spotless_update_campaign_status',
|
|
983
|
+
'description': "Update a campaign's status (active, paused, archived). Use to pause underperformers.",
|
|
984
|
+
'inputSchema': {
|
|
985
|
+
'type': 'object',
|
|
986
|
+
'properties': {
|
|
987
|
+
'campaign_id': {
|
|
988
|
+
'type': 'string',
|
|
989
|
+
'description': 'Campaign ID',
|
|
990
|
+
},
|
|
991
|
+
'status': {
|
|
992
|
+
'type': 'string',
|
|
993
|
+
'enum': ['active', 'paused', 'archived'],
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
'required': ['campaign_id', 'status'],
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
'name': 'spotless_update_campaign_budget',
|
|
1001
|
+
'description': "Update a campaign's budget. Use to scale successful campaigns.",
|
|
1002
|
+
'inputSchema': {
|
|
1003
|
+
'type': 'object',
|
|
1004
|
+
'properties': {
|
|
1005
|
+
'campaign_id': {'type': 'string'},
|
|
1006
|
+
'budget': {'type': 'number'},
|
|
1007
|
+
},
|
|
1008
|
+
'required': ['campaign_id', 'budget'],
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
'name': 'spotless_get_campaign_metrics',
|
|
1013
|
+
'description': 'Get performance metrics for a campaign: impressions, clicks, conversions, spend, CTR, CPC, ROAS.',
|
|
1014
|
+
'inputSchema': {
|
|
1015
|
+
'type': 'object',
|
|
1016
|
+
'properties': {'campaign_id': {'type': 'string'}},
|
|
1017
|
+
'required': ['campaign_id'],
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
'name': 'spotless_list_campaigns',
|
|
1022
|
+
'description': 'List all campaigns, optionally filtered by status, platform, or initiative.',
|
|
1023
|
+
'inputSchema': {
|
|
1024
|
+
'type': 'object',
|
|
1025
|
+
'properties': {
|
|
1026
|
+
'status': {
|
|
1027
|
+
'type': 'string',
|
|
1028
|
+
'enum': ['active', 'paused', 'archived'],
|
|
1029
|
+
},
|
|
1030
|
+
'platform': {
|
|
1031
|
+
'type': 'string',
|
|
1032
|
+
'enum': ['facebook', 'tiktok', 'google'],
|
|
1033
|
+
},
|
|
1034
|
+
'initiative_id': {'type': 'string'},
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
# Automation Tools
|
|
1039
|
+
{
|
|
1040
|
+
'name': 'spotless_create_automation',
|
|
1041
|
+
'description': 'Create an email/SMS automation workflow. Triggered by form submissions, tags, purchases, or upsell declines.',
|
|
1042
|
+
'inputSchema': {
|
|
1043
|
+
'type': 'object',
|
|
1044
|
+
'properties': {
|
|
1045
|
+
'name': {
|
|
1046
|
+
'type': 'string',
|
|
1047
|
+
'description': 'Automation name',
|
|
1048
|
+
},
|
|
1049
|
+
'trigger_type': {
|
|
1050
|
+
'type': 'string',
|
|
1051
|
+
'enum': [
|
|
1052
|
+
'form_submit',
|
|
1053
|
+
'tag_added',
|
|
1054
|
+
'purchase',
|
|
1055
|
+
'decline_upsell',
|
|
1056
|
+
],
|
|
1057
|
+
'default': 'form_submit',
|
|
1058
|
+
},
|
|
1059
|
+
'trigger_config': {
|
|
1060
|
+
'type': 'object',
|
|
1061
|
+
'description': 'Trigger-specific config (e.g., formId, tagName)',
|
|
1062
|
+
},
|
|
1063
|
+
'steps': {
|
|
1064
|
+
'type': 'array',
|
|
1065
|
+
'items': {'type': 'string'},
|
|
1066
|
+
'description': "Workflow steps like 'welcome_email', 'wait_2_days', 'follow_up_sms'",
|
|
1067
|
+
},
|
|
1068
|
+
'auto_activate': {'type': 'boolean', 'default': True},
|
|
1069
|
+
},
|
|
1070
|
+
'required': ['name'],
|
|
1071
|
+
},
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
'name': 'spotless_trigger_automation',
|
|
1075
|
+
'description': 'Manually trigger automations of a specific type. Useful for testing.',
|
|
1076
|
+
'inputSchema': {
|
|
1077
|
+
'type': 'object',
|
|
1078
|
+
'properties': {
|
|
1079
|
+
'trigger_type': {'type': 'string'},
|
|
1080
|
+
'lead_id': {'type': 'integer'},
|
|
1081
|
+
'customer_id': {'type': 'integer'},
|
|
1082
|
+
'email': {'type': 'string'},
|
|
1083
|
+
'metadata': {'type': 'object'},
|
|
1084
|
+
},
|
|
1085
|
+
'required': ['trigger_type'],
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
'name': 'spotless_list_automations',
|
|
1090
|
+
'description': 'List all automation workflows.',
|
|
1091
|
+
'inputSchema': {
|
|
1092
|
+
'type': 'object',
|
|
1093
|
+
'properties': {
|
|
1094
|
+
'status': {
|
|
1095
|
+
'type': 'string',
|
|
1096
|
+
'enum': ['draft', 'active', 'paused', 'archived'],
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
'name': 'spotless_update_automation_status',
|
|
1103
|
+
'description': "Update an automation's status (draft, active, paused, archived).",
|
|
1104
|
+
'inputSchema': {
|
|
1105
|
+
'type': 'object',
|
|
1106
|
+
'properties': {
|
|
1107
|
+
'automation_id': {'type': 'string'},
|
|
1108
|
+
'status': {
|
|
1109
|
+
'type': 'string',
|
|
1110
|
+
'enum': ['draft', 'active', 'paused', 'archived'],
|
|
1111
|
+
},
|
|
1112
|
+
},
|
|
1113
|
+
'required': ['automation_id', 'status'],
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
# Audience Tools
|
|
1117
|
+
{
|
|
1118
|
+
'name': 'spotless_create_geo_audience',
|
|
1119
|
+
'description': 'Create a geographic targeting audience by zip codes. Syncs to ad platforms.',
|
|
1120
|
+
'inputSchema': {
|
|
1121
|
+
'type': 'object',
|
|
1122
|
+
'properties': {
|
|
1123
|
+
'name': {'type': 'string'},
|
|
1124
|
+
'zip_codes': {'type': 'array', 'items': {'type': 'string'}},
|
|
1125
|
+
'platforms': {
|
|
1126
|
+
'type': 'array',
|
|
1127
|
+
'items': {'type': 'string'},
|
|
1128
|
+
'default': ['facebook', 'tiktok'],
|
|
1129
|
+
},
|
|
1130
|
+
'initiative_id': {'type': 'string'},
|
|
1131
|
+
},
|
|
1132
|
+
'required': ['name', 'zip_codes'],
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
'name': 'spotless_create_lookalike_audience',
|
|
1137
|
+
'description': 'Create a lookalike audience from existing customers, high-value customers, or recent converters.',
|
|
1138
|
+
'inputSchema': {
|
|
1139
|
+
'type': 'object',
|
|
1140
|
+
'properties': {
|
|
1141
|
+
'name': {'type': 'string'},
|
|
1142
|
+
'source': {
|
|
1143
|
+
'type': 'string',
|
|
1144
|
+
'enum': [
|
|
1145
|
+
'existing_customers',
|
|
1146
|
+
'high_value_customers',
|
|
1147
|
+
'recent_converters',
|
|
1148
|
+
],
|
|
1149
|
+
'default': 'existing_customers',
|
|
1150
|
+
},
|
|
1151
|
+
'lookalike_percent': {
|
|
1152
|
+
'type': 'integer',
|
|
1153
|
+
'minimum': 1,
|
|
1154
|
+
'maximum': 10,
|
|
1155
|
+
'default': 1,
|
|
1156
|
+
'description': '1-10, lower = more similar',
|
|
1157
|
+
},
|
|
1158
|
+
'platforms': {
|
|
1159
|
+
'type': 'array',
|
|
1160
|
+
'items': {'type': 'string'},
|
|
1161
|
+
'default': ['facebook', 'tiktok'],
|
|
1162
|
+
},
|
|
1163
|
+
'initiative_id': {'type': 'string'},
|
|
1164
|
+
},
|
|
1165
|
+
'required': ['name'],
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
'name': 'spotless_create_custom_audience',
|
|
1170
|
+
'description': 'Create a custom audience from email/phone lists for Customer Match.',
|
|
1171
|
+
'inputSchema': {
|
|
1172
|
+
'type': 'object',
|
|
1173
|
+
'properties': {
|
|
1174
|
+
'name': {'type': 'string'},
|
|
1175
|
+
'emails': {'type': 'array', 'items': {'type': 'string'}},
|
|
1176
|
+
'phones': {'type': 'array', 'items': {'type': 'string'}},
|
|
1177
|
+
'platforms': {
|
|
1178
|
+
'type': 'array',
|
|
1179
|
+
'items': {'type': 'string'},
|
|
1180
|
+
'default': ['facebook', 'tiktok', 'google'],
|
|
1181
|
+
},
|
|
1182
|
+
'initiative_id': {'type': 'string'},
|
|
1183
|
+
},
|
|
1184
|
+
'required': ['name'],
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
'name': 'spotless_get_trash_zone_zips',
|
|
1189
|
+
'description': 'Get zip codes for specified trash zones. Useful for building geo audiences.',
|
|
1190
|
+
'inputSchema': {
|
|
1191
|
+
'type': 'object',
|
|
1192
|
+
'properties': {
|
|
1193
|
+
'zone_ids': {'type': 'array', 'items': {'type': 'string'}}
|
|
1194
|
+
},
|
|
1195
|
+
'required': ['zone_ids'],
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
# Analytics Tools
|
|
1199
|
+
{
|
|
1200
|
+
'name': 'spotless_get_unified_metrics',
|
|
1201
|
+
'description': 'Get unified marketing metrics aggregated across all ad platforms. Returns impressions, clicks, conversions, spend, CTR, CPC, CPM, ROAS.',
|
|
1202
|
+
'inputSchema': {
|
|
1203
|
+
'type': 'object',
|
|
1204
|
+
'properties': {
|
|
1205
|
+
'start_date': {
|
|
1206
|
+
'type': 'string',
|
|
1207
|
+
'description': 'ISO date string',
|
|
1208
|
+
},
|
|
1209
|
+
'end_date': {
|
|
1210
|
+
'type': 'string',
|
|
1211
|
+
'description': 'ISO date string',
|
|
1212
|
+
},
|
|
1213
|
+
'initiative_id': {'type': 'string'},
|
|
1214
|
+
},
|
|
1215
|
+
'required': ['start_date', 'end_date'],
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
'name': 'spotless_get_roi_metrics',
|
|
1220
|
+
'description': 'Get ROI metrics combining ad spend with revenue from Stripe. Returns ROAS and profit.',
|
|
1221
|
+
'inputSchema': {
|
|
1222
|
+
'type': 'object',
|
|
1223
|
+
'properties': {
|
|
1224
|
+
'start_date': {'type': 'string'},
|
|
1225
|
+
'end_date': {'type': 'string'},
|
|
1226
|
+
},
|
|
1227
|
+
'required': ['start_date', 'end_date'],
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
'name': 'spotless_get_channel_performance',
|
|
1232
|
+
'description': 'Get performance metrics for a specific marketing channel (facebook, tiktok, google, email, sms, direct_mail).',
|
|
1233
|
+
'inputSchema': {
|
|
1234
|
+
'type': 'object',
|
|
1235
|
+
'properties': {
|
|
1236
|
+
'channel': {
|
|
1237
|
+
'type': 'string',
|
|
1238
|
+
'enum': [
|
|
1239
|
+
'facebook',
|
|
1240
|
+
'tiktok',
|
|
1241
|
+
'google',
|
|
1242
|
+
'email',
|
|
1243
|
+
'sms',
|
|
1244
|
+
'direct_mail',
|
|
1245
|
+
],
|
|
1246
|
+
},
|
|
1247
|
+
'start_date': {'type': 'string'},
|
|
1248
|
+
'end_date': {'type': 'string'},
|
|
1249
|
+
},
|
|
1250
|
+
'required': ['channel'],
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
'name': 'spotless_thompson_sample_budget',
|
|
1255
|
+
'description': 'Get optimal budget allocation using Thompson Sampling bandit algorithm. Returns allocation per channel and explore/exploit decision.',
|
|
1256
|
+
'inputSchema': {
|
|
1257
|
+
'type': 'object',
|
|
1258
|
+
'properties': {
|
|
1259
|
+
'channels': {
|
|
1260
|
+
'type': 'array',
|
|
1261
|
+
'items': {'type': 'string'},
|
|
1262
|
+
'default': ['meta_ads', 'tiktok_ads', 'door_hangers'],
|
|
1263
|
+
},
|
|
1264
|
+
'initiative_id': {'type': 'string'},
|
|
1265
|
+
'zip_code': {'type': 'string'},
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
'name': 'spotless_get_conversion_attribution',
|
|
1271
|
+
'description': 'Get multi-touch attribution data showing customer journey from first touch to conversion.',
|
|
1272
|
+
'inputSchema': {
|
|
1273
|
+
'type': 'object',
|
|
1274
|
+
'properties': {
|
|
1275
|
+
'customer_id': {'type': 'integer'},
|
|
1276
|
+
'conversion_id': {'type': 'string'},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
# Platform Sync Tools
|
|
1281
|
+
{
|
|
1282
|
+
'name': 'spotless_sync_facebook_metrics',
|
|
1283
|
+
'description': 'Sync latest campaign metrics from Facebook Ads.',
|
|
1284
|
+
'inputSchema': {'type': 'object', 'properties': {}},
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
'name': 'spotless_sync_tiktok_metrics',
|
|
1288
|
+
'description': 'Sync latest campaign metrics from TikTok Ads.',
|
|
1289
|
+
'inputSchema': {'type': 'object', 'properties': {}},
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
'name': 'spotless_sync_google_metrics',
|
|
1293
|
+
'description': 'Sync latest campaign metrics from Google Ads.',
|
|
1294
|
+
'inputSchema': {'type': 'object', 'properties': {}},
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
'name': 'spotless_send_facebook_conversion',
|
|
1298
|
+
'description': 'Send a conversion event to Facebook CAPI for server-side tracking.',
|
|
1299
|
+
'inputSchema': {
|
|
1300
|
+
'type': 'object',
|
|
1301
|
+
'properties': {
|
|
1302
|
+
'event_name': {'type': 'string', 'default': 'Purchase'},
|
|
1303
|
+
'email': {'type': 'string'},
|
|
1304
|
+
'phone': {'type': 'string'},
|
|
1305
|
+
'value': {'type': 'number'},
|
|
1306
|
+
'currency': {'type': 'string', 'default': 'USD'},
|
|
1307
|
+
'event_id': {'type': 'string'},
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
'name': 'spotless_send_tiktok_event',
|
|
1313
|
+
'description': 'Send an event to TikTok Events API.',
|
|
1314
|
+
'inputSchema': {
|
|
1315
|
+
'type': 'object',
|
|
1316
|
+
'properties': {
|
|
1317
|
+
'event_name': {'type': 'string'},
|
|
1318
|
+
'email': {'type': 'string'},
|
|
1319
|
+
'phone': {'type': 'string'},
|
|
1320
|
+
'value': {'type': 'number'},
|
|
1321
|
+
'event_id': {'type': 'string'},
|
|
1322
|
+
},
|
|
1323
|
+
'required': ['event_name'],
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
]
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
# Tool handler mapping
|
|
1330
|
+
MARKETING_TOOL_HANDLERS = {
|
|
1331
|
+
# Creative
|
|
1332
|
+
'spotless_generate_creative': spotless_generate_creative,
|
|
1333
|
+
'spotless_batch_generate_creatives': spotless_batch_generate_creatives,
|
|
1334
|
+
'spotless_get_top_creatives': spotless_get_top_creatives,
|
|
1335
|
+
'spotless_analyze_creative_performance': spotless_analyze_creative_performance,
|
|
1336
|
+
# Campaigns
|
|
1337
|
+
'spotless_create_campaign': spotless_create_campaign,
|
|
1338
|
+
'spotless_update_campaign_status': spotless_update_campaign_status,
|
|
1339
|
+
'spotless_update_campaign_budget': spotless_update_campaign_budget,
|
|
1340
|
+
'spotless_get_campaign_metrics': spotless_get_campaign_metrics,
|
|
1341
|
+
'spotless_list_campaigns': spotless_list_campaigns,
|
|
1342
|
+
# Automations
|
|
1343
|
+
'spotless_create_automation': spotless_create_automation,
|
|
1344
|
+
'spotless_trigger_automation': spotless_trigger_automation,
|
|
1345
|
+
'spotless_list_automations': spotless_list_automations,
|
|
1346
|
+
'spotless_update_automation_status': spotless_update_automation_status,
|
|
1347
|
+
# Audiences
|
|
1348
|
+
'spotless_create_geo_audience': spotless_create_geo_audience,
|
|
1349
|
+
'spotless_create_lookalike_audience': spotless_create_lookalike_audience,
|
|
1350
|
+
'spotless_create_custom_audience': spotless_create_custom_audience,
|
|
1351
|
+
'spotless_get_trash_zone_zips': spotless_get_trash_zone_zips,
|
|
1352
|
+
# Analytics
|
|
1353
|
+
'spotless_get_unified_metrics': spotless_get_unified_metrics,
|
|
1354
|
+
'spotless_get_roi_metrics': spotless_get_roi_metrics,
|
|
1355
|
+
'spotless_get_channel_performance': spotless_get_channel_performance,
|
|
1356
|
+
'spotless_thompson_sample_budget': spotless_thompson_sample_budget,
|
|
1357
|
+
'spotless_get_conversion_attribution': spotless_get_conversion_attribution,
|
|
1358
|
+
# Platform Sync
|
|
1359
|
+
'spotless_sync_facebook_metrics': spotless_sync_facebook_metrics,
|
|
1360
|
+
'spotless_sync_tiktok_metrics': spotless_sync_tiktok_metrics,
|
|
1361
|
+
'spotless_sync_google_metrics': spotless_sync_google_metrics,
|
|
1362
|
+
'spotless_send_facebook_conversion': spotless_send_facebook_conversion,
|
|
1363
|
+
'spotless_send_tiktok_event': spotless_send_tiktok_event,
|
|
1364
|
+
}
|