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.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. 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
+ }