Spaces:
Configuration error
Configuration error
Upload 42 files
Browse files- Dockerfile +25 -0
- SuperAgent/Assets Knowledge/16a26bde-3eef-47f2-a8ac-f4cb33389a8f/f2dbe341-fa94-4450-92b3-4816a1989a00/cache.json +145 -0
- SuperAgent/Assets Knowledge/f4d72a61-b4b2-4c22-b8a5-1ac9de47b6bd/1f93021a-7f71-4e7a-bde5-126b5f39fe73/cache.json +77 -0
- SuperAgent/__pycache__/main.cpython-313.pyc +0 -0
- SuperAgent/app/__init__.py +1 -0
- SuperAgent/app/__pycache__/__init__.cpython-313.pyc +0 -0
- SuperAgent/app/agents/__init__.py +4 -0
- SuperAgent/app/agents/__pycache__/__init__.cpython-313.pyc +0 -0
- SuperAgent/app/agents/__pycache__/report_agent.cpython-313.pyc +0 -0
- SuperAgent/app/agents/report_agent.md +61 -0
- SuperAgent/app/agents/report_agent.py +127 -0
- SuperAgent/app/config/provider_registry.json +46 -0
- SuperAgent/app/mcp/__pycache__/mcp_server.cpython-313.pyc +0 -0
- SuperAgent/app/mcp/mcp_server.py +77 -0
- SuperAgent/app/models/__pycache__/schemas.cpython-313.pyc +0 -0
- SuperAgent/app/models/schemas.py +133 -0
- SuperAgent/app/services/__pycache__/asset_registry.cpython-313.pyc +0 -0
- SuperAgent/app/services/__pycache__/email_service.cpython-313.pyc +0 -0
- SuperAgent/app/services/__pycache__/knowledge_cache_service.cpython-313.pyc +0 -0
- SuperAgent/app/services/__pycache__/logger.cpython-313.pyc +0 -0
- SuperAgent/app/services/__pycache__/realtime_fetcher.cpython-313.pyc +0 -0
- SuperAgent/app/services/__pycache__/report_exporter.cpython-313.pyc +0 -0
- SuperAgent/app/services/__pycache__/router.cpython-313.pyc +0 -0
- SuperAgent/app/services/asset_registry.py +398 -0
- SuperAgent/app/services/email_service.py +174 -0
- SuperAgent/app/services/knowledge_cache_service.py +269 -0
- SuperAgent/app/services/logger.py +31 -0
- SuperAgent/app/services/realtime_fetcher.py +404 -0
- SuperAgent/app/services/report_exporter.py +216 -0
- SuperAgent/app/services/router.py +576 -0
- SuperAgent/main.py +362 -0
- SuperAgent/superagent.md +61 -0
- agents/analysis/backend/app/__pycache__/main.cpython-313.pyc +0 -0
- agents/analysis/backend/app/main.py +82 -0
- agents/growth/app/__pycache__/main.cpython-313.pyc +0 -0
- agents/growth/app/main.py +280 -0
- agents/growth/growth_agent.md +65 -0
- agents/marketing/app/__pycache__/main.cpython-313.pyc +0 -0
- agents/marketing/app/main.py +268 -0
- agents/marketing/marketing_agent.md +66 -0
- requirements.txt +11 -0
- run_all_agents.py +43 -0
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ScaleMate Executive SuperAgent — HuggingFace Space Dockerfile (Port 7860)
|
| 2 |
+
|
| 3 |
+
FROM python:3.10-slim
|
| 4 |
+
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies for Matplotlib & ReportLab PDF engine
|
| 8 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
+
build-essential \
|
| 10 |
+
libpq-dev \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Install Python requirements
|
| 14 |
+
COPY requirements.txt .
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Copy backend codebase
|
| 18 |
+
COPY . .
|
| 19 |
+
|
| 20 |
+
# HuggingFace Space Port Expose
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
# Run unified multi-agent server on Port 7860
|
| 24 |
+
CMD ["uvicorn", "backend.SuperAgent.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 25 |
+
|
SuperAgent/Assets Knowledge/16a26bde-3eef-47f2-a8ac-f4cb33389a8f/f2dbe341-fa94-4450-92b3-4816a1989a00/cache.json
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tenant_id": "16a26bde-3eef-47f2-a8ac-f4cb33389a8f",
|
| 3 |
+
"user_id": "f2dbe341-fa94-4450-92b3-4816a1989a00",
|
| 4 |
+
"user_authorized": true,
|
| 5 |
+
"total_platforms": 5,
|
| 6 |
+
"timestamp_epoch": 1785306912.8854737,
|
| 7 |
+
"cached_at": "2026-07-29 11:35:12",
|
| 8 |
+
"knowledge_graph": [
|
| 9 |
+
{
|
| 10 |
+
"id": "conn-shopify",
|
| 11 |
+
"provider": "shopify",
|
| 12 |
+
"name": "Shopify E-Commerce",
|
| 13 |
+
"category": "store",
|
| 14 |
+
"status": "disconnected",
|
| 15 |
+
"attribute_keys": [
|
| 16 |
+
"totalRevenue",
|
| 17 |
+
"totalOrders",
|
| 18 |
+
"avgOrderValue",
|
| 19 |
+
"checkouts"
|
| 20 |
+
],
|
| 21 |
+
"display_metrics": [
|
| 22 |
+
"Total Revenue & Orders",
|
| 23 |
+
"Average Order Value (AOV)",
|
| 24 |
+
"Checkout Conversion Rate"
|
| 25 |
+
],
|
| 26 |
+
"synonyms": [
|
| 27 |
+
"shopify",
|
| 28 |
+
"store",
|
| 29 |
+
"shop",
|
| 30 |
+
"sales",
|
| 31 |
+
"revenue",
|
| 32 |
+
"orders",
|
| 33 |
+
"checkout"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"id": "05a880e4-37e7-4fa1-bc6f-4d0de1f245b0",
|
| 38 |
+
"provider": "ga4",
|
| 39 |
+
"name": "Google Analytics 4 (GA4)",
|
| 40 |
+
"category": "analytics",
|
| 41 |
+
"status": "connected",
|
| 42 |
+
"attribute_keys": [
|
| 43 |
+
"activeUsers",
|
| 44 |
+
"totalSessions",
|
| 45 |
+
"bounceRate",
|
| 46 |
+
"trafficSources"
|
| 47 |
+
],
|
| 48 |
+
"display_metrics": [
|
| 49 |
+
"Active Users & Total Sessions",
|
| 50 |
+
"Bounce Rate",
|
| 51 |
+
"Traffic Channels"
|
| 52 |
+
],
|
| 53 |
+
"synonyms": [
|
| 54 |
+
"ga4",
|
| 55 |
+
"google analytics",
|
| 56 |
+
"analytics",
|
| 57 |
+
"traffic",
|
| 58 |
+
"visitors",
|
| 59 |
+
"sessions",
|
| 60 |
+
"website"
|
| 61 |
+
]
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "conn-meta",
|
| 65 |
+
"provider": "meta",
|
| 66 |
+
"name": "Meta Ads (Facebook/Instagram)",
|
| 67 |
+
"category": "marketing",
|
| 68 |
+
"status": "disconnected",
|
| 69 |
+
"attribute_keys": [
|
| 70 |
+
"spend",
|
| 71 |
+
"roas",
|
| 72 |
+
"ctr",
|
| 73 |
+
"cpc",
|
| 74 |
+
"impressions",
|
| 75 |
+
"clicks"
|
| 76 |
+
],
|
| 77 |
+
"display_metrics": [
|
| 78 |
+
"Ad Spend & Overall ROAS",
|
| 79 |
+
"Click-Through Rate (CTR) & CPC",
|
| 80 |
+
"Campaign Performance"
|
| 81 |
+
],
|
| 82 |
+
"synonyms": [
|
| 83 |
+
"meta",
|
| 84 |
+
"facebook",
|
| 85 |
+
"instagram",
|
| 86 |
+
"fb ads",
|
| 87 |
+
"meta ads",
|
| 88 |
+
"roas",
|
| 89 |
+
"ctr",
|
| 90 |
+
"ad spend"
|
| 91 |
+
]
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"id": "conn-google_ads",
|
| 95 |
+
"provider": "google_ads",
|
| 96 |
+
"name": "Google Ads",
|
| 97 |
+
"category": "marketing",
|
| 98 |
+
"status": "disconnected",
|
| 99 |
+
"attribute_keys": [
|
| 100 |
+
"totalImpressions",
|
| 101 |
+
"totalClicks",
|
| 102 |
+
"totalSpend",
|
| 103 |
+
"averageCtr",
|
| 104 |
+
"averageCpc"
|
| 105 |
+
],
|
| 106 |
+
"display_metrics": [
|
| 107 |
+
"Google Ad Spend & ROAS",
|
| 108 |
+
"Click-Through Rate (CTR)",
|
| 109 |
+
"Search Campaign Breakdown"
|
| 110 |
+
],
|
| 111 |
+
"synonyms": [
|
| 112 |
+
"google ads",
|
| 113 |
+
"gads",
|
| 114 |
+
"search ads",
|
| 115 |
+
"pmax",
|
| 116 |
+
"google ad spend"
|
| 117 |
+
]
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "conn-leads",
|
| 121 |
+
"provider": "leads",
|
| 122 |
+
"name": "Leads & Contacts",
|
| 123 |
+
"category": "crm",
|
| 124 |
+
"status": "disconnected",
|
| 125 |
+
"attribute_keys": [
|
| 126 |
+
"totalLeads",
|
| 127 |
+
"costPerLead",
|
| 128 |
+
"welcomeEmailSentCount"
|
| 129 |
+
],
|
| 130 |
+
"display_metrics": [
|
| 131 |
+
"Total Leads Generated",
|
| 132 |
+
"Cost Per Lead (CPL)",
|
| 133 |
+
"Email Nurture Status"
|
| 134 |
+
],
|
| 135 |
+
"synonyms": [
|
| 136 |
+
"leads",
|
| 137 |
+
"lead",
|
| 138 |
+
"contacts",
|
| 139 |
+
"cpl",
|
| 140 |
+
"cost per lead",
|
| 141 |
+
"form leads"
|
| 142 |
+
]
|
| 143 |
+
}
|
| 144 |
+
]
|
| 145 |
+
}
|
SuperAgent/Assets Knowledge/f4d72a61-b4b2-4c22-b8a5-1ac9de47b6bd/1f93021a-7f71-4e7a-bde5-126b5f39fe73/cache.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tenant_id": "f4d72a61-b4b2-4c22-b8a5-1ac9de47b6bd",
|
| 3 |
+
"user_id": "1f93021a-7f71-4e7a-bde5-126b5f39fe73",
|
| 4 |
+
"user_authorized": true,
|
| 5 |
+
"total_platforms": 2,
|
| 6 |
+
"timestamp_epoch": 1785325600.5119762,
|
| 7 |
+
"cached_at": "2026-07-29 16:46:40",
|
| 8 |
+
"knowledge_graph": [
|
| 9 |
+
{
|
| 10 |
+
"id": "ad16a6be-8d95-4a51-986e-ad6af6018c09",
|
| 11 |
+
"provider": "meta",
|
| 12 |
+
"name": "Meta Ads (Facebook/Instagram)",
|
| 13 |
+
"category": "marketing",
|
| 14 |
+
"status": "connected",
|
| 15 |
+
"attribute_keys": [
|
| 16 |
+
"spend",
|
| 17 |
+
"roas",
|
| 18 |
+
"ctr",
|
| 19 |
+
"cpc",
|
| 20 |
+
"impressions",
|
| 21 |
+
"clicks"
|
| 22 |
+
],
|
| 23 |
+
"display_metrics": [
|
| 24 |
+
"Ad Spend & Overall ROAS",
|
| 25 |
+
"Click-Through Rate (CTR) & CPC",
|
| 26 |
+
"Campaign Performance"
|
| 27 |
+
],
|
| 28 |
+
"synonyms": [
|
| 29 |
+
"meta",
|
| 30 |
+
"facebook",
|
| 31 |
+
"instagram",
|
| 32 |
+
"fb ads",
|
| 33 |
+
"meta ads",
|
| 34 |
+
"roas",
|
| 35 |
+
"ctr",
|
| 36 |
+
"ad spend"
|
| 37 |
+
],
|
| 38 |
+
"metadata": {
|
| 39 |
+
"propertyId": "act_720751567283621",
|
| 40 |
+
"shop": null,
|
| 41 |
+
"adAccountId": "act_720751567283621"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"id": "60757646-3072-4bf2-ac03-1de5d009644f",
|
| 46 |
+
"provider": "ga4",
|
| 47 |
+
"name": "Google Analytics 4 (GA4)",
|
| 48 |
+
"category": "analytics",
|
| 49 |
+
"status": "connected",
|
| 50 |
+
"attribute_keys": [
|
| 51 |
+
"activeUsers",
|
| 52 |
+
"totalSessions",
|
| 53 |
+
"bounceRate",
|
| 54 |
+
"trafficSources"
|
| 55 |
+
],
|
| 56 |
+
"display_metrics": [
|
| 57 |
+
"Active Users & Total Sessions",
|
| 58 |
+
"Bounce Rate",
|
| 59 |
+
"Traffic Channels"
|
| 60 |
+
],
|
| 61 |
+
"synonyms": [
|
| 62 |
+
"ga4",
|
| 63 |
+
"google analytics",
|
| 64 |
+
"analytics",
|
| 65 |
+
"traffic",
|
| 66 |
+
"visitors",
|
| 67 |
+
"sessions",
|
| 68 |
+
"website"
|
| 69 |
+
],
|
| 70 |
+
"metadata": {
|
| 71 |
+
"propertyId": "531155758",
|
| 72 |
+
"shop": null,
|
| 73 |
+
"adAccountId": "531155758"
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
]
|
| 77 |
+
}
|
SuperAgent/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (13.5 kB). View file
|
|
|
SuperAgent/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# SuperAgent App Package
|
SuperAgent/app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (191 Bytes). View file
|
|
|
SuperAgent/app/agents/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SuperAgent Agents Package.
|
| 3 |
+
Includes Growth Agent, Marketing Agent, and Report Generator Agent.
|
| 4 |
+
"""
|
SuperAgent/app/agents/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (311 Bytes). View file
|
|
|
SuperAgent/app/agents/__pycache__/report_agent.cpython-313.pyc
ADDED
|
Binary file (6.83 kB). View file
|
|
|
SuperAgent/app/agents/report_agent.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Report Generator Agent Specification (`report_agent.md`)
|
| 2 |
+
|
| 3 |
+
## 1. Identity & System Persona
|
| 4 |
+
* **Name & Designation:** ScaleMate AI Executive Report Generator Agent
|
| 5 |
+
* **Role & Domain Scope:** Multi-Channel Business Performance Consolidator, PDF/Markdown Executive Report Compiler, and Strategic Action Plan Synthesizer.
|
| 6 |
+
* **Operational Goal:** Consolidates Growth Agent insights (GA4 & Shopify) and Marketing Agent insights (Meta Ads & Leads) to generate structured C-Suite Executive Performance Reports, weekly summaries, and strategic roadmap recommendations.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 2. Business Logics, Control Rules & Directives
|
| 11 |
+
|
| 12 |
+
### A. Report Compilation & Structuring Rules
|
| 13 |
+
1. **Executive Summary Consolidation**:
|
| 14 |
+
- Aggregates overall business revenue, ad spend, combined ROAS, and net return.
|
| 15 |
+
2. **Channel Performance Matrix**:
|
| 16 |
+
- Compiles side-by-side performance table for Shopify, GA4, Meta Ads, and Lead Generation.
|
| 17 |
+
3. **Strategic Action Roadmap Rules**:
|
| 18 |
+
- Formulates 3 priority initiatives: (1) Immediate Budget Re-allocation, (2) Conversion Rate Optimization (CRO), (3) Customer Lifetime Value (LTV) Boosters.
|
| 19 |
+
|
| 20 |
+
### B. Formatting & Output Directives
|
| 21 |
+
1. **Executive Markdown / PDF Template**: Formats output with clean GitHub markdown headers (`# Executive Performance Report`, `## Key Highlights`, `## Channel Breakdown`, `## Recommended Action Plan`).
|
| 22 |
+
2. **Zero Emojis Policy**: Keeps all executive reports strictly clean, professional, and free of emojis.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 3. Data Contracts & Schemas
|
| 27 |
+
|
| 28 |
+
### Input Payload (`report_agent_payload`)
|
| 29 |
+
```json
|
| 30 |
+
{
|
| 31 |
+
"growth_result": {
|
| 32 |
+
"traffic_summary": "1,250 active users, 3,420 sessions, 38.5% bounce rate.",
|
| 33 |
+
"aov": 77.03,
|
| 34 |
+
"checkout_conversion_rate": 50.0,
|
| 35 |
+
"insights": "Revenue $14,250.00 across 185 orders."
|
| 36 |
+
},
|
| 37 |
+
"marketing_result": {
|
| 38 |
+
"ad_spend": 3200.00,
|
| 39 |
+
"overall_roas": 4.45,
|
| 40 |
+
"cost_per_lead": 8.50,
|
| 41 |
+
"high_performers": ["Prospecting Top Funnel"],
|
| 42 |
+
"wasted_spend_campaigns": ["Broad Audience Retargeting"]
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Output Report Schema (`ExecutiveReport`)
|
| 48 |
+
```json
|
| 49 |
+
{
|
| 50 |
+
"success": true,
|
| 51 |
+
"report_title": "ScaleMate AI Executive Performance & Growth Report",
|
| 52 |
+
"generated_at": "2026-07-29 14:25:00",
|
| 53 |
+
"summary": {
|
| 54 |
+
"total_revenue": 14250.00,
|
| 55 |
+
"total_ad_spend": 3200.00,
|
| 56 |
+
"blended_roas": 4.45,
|
| 57 |
+
"total_active_users": 1250
|
| 58 |
+
},
|
| 59 |
+
"report_markdown": "# ScaleMate AI Executive Performance & Growth Report\n\n## 1. Executive Summary\n- Total Revenue: $14,250.00\n- Total Ad Spend: $3,200.00 (ROAS: 4.45x)\n\n## 2. Channel Performance\n- **GA4 & Traffic:** 1,250 active users, 38.5% bounce rate\n- **Shopify Sales:** 185 orders, $77.03 AOV\n- **Meta Ads:** $3,200.00 spend, 4.45x ROAS\n\n## 3. Recommended Action Plan\n1. Pause wasted spend on 'Broad Audience Retargeting' and reallocate $500 to 'Prospecting Top Funnel'.\n2. Optimize checkout trust badges to improve the 50.0% checkout conversion rate."
|
| 60 |
+
}
|
| 61 |
+
```
|
SuperAgent/app/agents/report_agent.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Report Generator Agent — Sub-Agent for C-Suite Performance Report Synthesis.
|
| 3 |
+
Consolidates Growth Agent (GA4/Shopify) and Marketing Agent (Meta Ads/Leads) outputs
|
| 4 |
+
to produce structured Executive Summaries, Channel Matrices, and Strategic Roadmap Reports.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import time
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def generate_executive_report(payload: dict[str, Any]) -> dict[str, Any]:
|
| 14 |
+
"""
|
| 15 |
+
Consolidate sub-agent insights and compile a structured C-Suite Performance Report.
|
| 16 |
+
Supports single-platform reports (Meta Ads only, GA4 only, Shopify only) and custom timeframe windows.
|
| 17 |
+
"""
|
| 18 |
+
growth = payload.get("growth_result") or {}
|
| 19 |
+
marketing = payload.get("marketing_result") or {}
|
| 20 |
+
tf_label = payload.get("timeframe_label", "Last 1 Hour (Default Window)")
|
| 21 |
+
platform_filter = payload.get("platform_filter", "all")
|
| 22 |
+
|
| 23 |
+
traffic_summary = growth.get("traffic_summary", "N/A")
|
| 24 |
+
aov = growth.get("aov", 0.0)
|
| 25 |
+
checkout_rate = growth.get("checkout_conversion_rate", 0.0)
|
| 26 |
+
growth_insights = growth.get("insights", "N/A")
|
| 27 |
+
|
| 28 |
+
ad_spend = marketing.get("ad_spend", 0.0)
|
| 29 |
+
overall_roas = marketing.get("overall_roas", 0.0)
|
| 30 |
+
cost_per_lead = marketing.get("cost_per_lead", 0.0)
|
| 31 |
+
high_perf = marketing.get("high_performers", [])
|
| 32 |
+
wasted = marketing.get("wasted_spend_campaigns", [])
|
| 33 |
+
|
| 34 |
+
def _fmt_camp(c: Any) -> str:
|
| 35 |
+
if isinstance(c, dict):
|
| 36 |
+
return str(c.get("name", c))
|
| 37 |
+
return str(c)
|
| 38 |
+
|
| 39 |
+
# Customize Report Title & Content based on Platform Filter
|
| 40 |
+
if platform_filter == "meta":
|
| 41 |
+
report_title = "ScaleMate AI Meta Ads Performance & ROAS Analysis Report"
|
| 42 |
+
report_lines = [
|
| 43 |
+
f"# {report_title}",
|
| 44 |
+
f"*Generated at: {time.strftime('%Y-%m-%d %H:%M:%S')} | Timeframe: {tf_label}*\n",
|
| 45 |
+
"## 1. Meta Ads Executive Performance Summary",
|
| 46 |
+
f"- **Total Meta Ad Spend:** ${ad_spend:,.2f}",
|
| 47 |
+
f"- **Overall Campaign ROAS:** {overall_roas:.2f}x",
|
| 48 |
+
f"- **Cost Per Lead (CPL):** ${cost_per_lead:.2f}\n",
|
| 49 |
+
"## 2. Campaign ROAS Breakdown",
|
| 50 |
+
f"- **High Performing Campaigns:** {', '.join([_fmt_camp(c) for c in high_perf]) if high_perf else 'None'}",
|
| 51 |
+
f"- **Wasted Spend Campaigns:** {', '.join([_fmt_camp(c) for c in wasted]) if wasted else 'None'}\n",
|
| 52 |
+
"## 3. Meta Ads Optimization Action Items",
|
| 53 |
+
"1. Scale budget by 20% on top-performing prospecting ad sets.",
|
| 54 |
+
"2. Pause or refresh ad copy for campaigns with ROAS below 1.5x.",
|
| 55 |
+
]
|
| 56 |
+
elif platform_filter == "ga4":
|
| 57 |
+
report_title = "ScaleMate AI GA4 Analytics & Traffic Analysis Report"
|
| 58 |
+
report_lines = [
|
| 59 |
+
f"# {report_title}",
|
| 60 |
+
f"*Generated at: {time.strftime('%Y-%m-%d %H:%M:%S')} | Timeframe: {tf_label}*\n",
|
| 61 |
+
"## 1. GA4 Traffic & User Engagement Summary",
|
| 62 |
+
f"- **Traffic Summary:** {traffic_summary}",
|
| 63 |
+
f"- **Checkout Conversion Rate:** {checkout_rate:.2f}%\n",
|
| 64 |
+
"## 2. Channel Engagement Breakdown",
|
| 65 |
+
f"- **Website Visitor Efficiency:** {traffic_summary}",
|
| 66 |
+
f"- **Store Checkout Health:** {growth_insights}\n",
|
| 67 |
+
"## 3. GA4 Traffic Growth Action Items",
|
| 68 |
+
"1. Focus budget on top-converting traffic acquisition sources.",
|
| 69 |
+
"2. Optimize high-bounce landing pages to improve visitor retention.",
|
| 70 |
+
]
|
| 71 |
+
else:
|
| 72 |
+
report_title = "ScaleMate AI Executive Performance & Growth Report"
|
| 73 |
+
report_lines = [
|
| 74 |
+
f"# {report_title}",
|
| 75 |
+
f"*Generated at: {time.strftime('%Y-%m-%d %H:%M:%S')} | Timeframe: {tf_label}*\n",
|
| 76 |
+
"## 1. C-Suite Executive Summary",
|
| 77 |
+
f"- **Combined Ad Spend:** ${ad_spend:,.2f}",
|
| 78 |
+
f"- **Overall Marketing ROAS:** {overall_roas:.2f}x",
|
| 79 |
+
f"- **Average Order Value (AOV):** ${aov:.2f}",
|
| 80 |
+
f"- **Checkout Conversion Rate:** {checkout_rate:.2f}%\n",
|
| 81 |
+
"## 2. Channel Performance Breakdown",
|
| 82 |
+
f"- **Google Analytics (GA4):** {traffic_summary}",
|
| 83 |
+
f"- **Shopify Store Sales:** {growth_insights}",
|
| 84 |
+
f"- **Meta Ads Insights:** ${ad_spend:,.2f} spend across active campaigns (ROAS: {overall_roas:.2f}x)",
|
| 85 |
+
f"- **Lead Generation:** ${cost_per_lead:.2f} Cost Per Lead (CPL)\n",
|
| 86 |
+
"## 3. High Performers vs Wasted Spend",
|
| 87 |
+
f"- **High Performing Campaigns:** {', '.join([_fmt_camp(c) for c in high_perf]) if high_perf else 'None'}",
|
| 88 |
+
f"- **Wasted Spend Campaigns:** {', '.join([_fmt_camp(c) for c in wasted]) if wasted else 'None'}\n",
|
| 89 |
+
"## 4. Strategic Action Plan",
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
action_count = 1
|
| 93 |
+
if wasted:
|
| 94 |
+
report_lines.append(f"{action_count}. Pause low ROAS campaigns ({', '.join([_fmt_camp(c) for c in wasted])}) and reallocate budget to top performers.")
|
| 95 |
+
action_count += 1
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if checkout_rate < 60:
|
| 99 |
+
report_lines.append(f"{action_count}. Add trust badges and express checkout buttons to reduce checkout drop-off rate.")
|
| 100 |
+
action_count += 1
|
| 101 |
+
|
| 102 |
+
if aov < 50:
|
| 103 |
+
report_lines.append(f"{action_count}. Introduce product bundle discounts to raise average order value above $50.00.")
|
| 104 |
+
|
| 105 |
+
report_lines.extend([
|
| 106 |
+
"\n---",
|
| 107 |
+
"**Would you like me to send a copy of this Executive Report to your registered email address?**",
|
| 108 |
+
"• Reply **'Yes'** or enter your email address to receive it in your inbox immediately.",
|
| 109 |
+
"• Reply **'No'** to continue analyzing in chat.",
|
| 110 |
+
])
|
| 111 |
+
|
| 112 |
+
report_markdown = "\n".join(report_lines)
|
| 113 |
+
|
| 114 |
+
return {
|
| 115 |
+
"success": True,
|
| 116 |
+
"report_title": report_title,
|
| 117 |
+
"timeframe": tf_label,
|
| 118 |
+
"platform_filter": platform_filter,
|
| 119 |
+
"generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 120 |
+
"summary": {
|
| 121 |
+
"ad_spend": ad_spend,
|
| 122 |
+
"overall_roas": overall_roas,
|
| 123 |
+
"aov": aov,
|
| 124 |
+
"checkout_conversion_rate": checkout_rate,
|
| 125 |
+
},
|
| 126 |
+
"report_markdown": report_markdown,
|
| 127 |
+
}
|
SuperAgent/app/config/provider_registry.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0",
|
| 3 |
+
"domain": "Ecommerce & Marketing Intelligence",
|
| 4 |
+
"providers": {
|
| 5 |
+
"shopify": {
|
| 6 |
+
"provider_id": "shopify",
|
| 7 |
+
"name": "Shopify E-Commerce",
|
| 8 |
+
"category": "store",
|
| 9 |
+
"synonyms": ["shopify", "store", "shop", "sales", "revenue", "orders", "checkout"],
|
| 10 |
+
"attribute_keys": ["totalRevenue", "totalOrders", "avgOrderValue", "checkouts"],
|
| 11 |
+
"display_metrics": ["Total Revenue & Orders", "Average Order Value (AOV)", "Checkout Conversion Rate"]
|
| 12 |
+
},
|
| 13 |
+
"ga4": {
|
| 14 |
+
"provider_id": "ga4",
|
| 15 |
+
"name": "Google Analytics 4 (GA4)",
|
| 16 |
+
"category": "analytics",
|
| 17 |
+
"synonyms": ["ga4", "google analytics", "analytics", "traffic", "visitors", "sessions", "website"],
|
| 18 |
+
"attribute_keys": ["activeUsers", "totalSessions", "bounceRate", "trafficSources"],
|
| 19 |
+
"display_metrics": ["Active Users & Total Sessions", "Bounce Rate", "Traffic Channels"]
|
| 20 |
+
},
|
| 21 |
+
"meta": {
|
| 22 |
+
"provider_id": "meta",
|
| 23 |
+
"name": "Meta Ads (Facebook/Instagram)",
|
| 24 |
+
"category": "marketing",
|
| 25 |
+
"synonyms": ["meta", "facebook", "instagram", "fb ads", "meta ads", "roas", "ctr", "ad spend"],
|
| 26 |
+
"attribute_keys": ["spend", "roas", "ctr", "cpc", "impressions", "clicks"],
|
| 27 |
+
"display_metrics": ["Ad Spend & Overall ROAS", "Click-Through Rate (CTR) & CPC", "Campaign Performance"]
|
| 28 |
+
},
|
| 29 |
+
"google_ads": {
|
| 30 |
+
"provider_id": "google_ads",
|
| 31 |
+
"name": "Google Ads",
|
| 32 |
+
"category": "marketing",
|
| 33 |
+
"synonyms": ["google ads", "gads", "search ads", "pmax", "google ad spend"],
|
| 34 |
+
"attribute_keys": ["totalImpressions", "totalClicks", "totalSpend", "averageCtr", "averageCpc"],
|
| 35 |
+
"display_metrics": ["Google Ad Spend & ROAS", "Click-Through Rate (CTR)", "Search Campaign Breakdown"]
|
| 36 |
+
},
|
| 37 |
+
"leads": {
|
| 38 |
+
"provider_id": "leads",
|
| 39 |
+
"name": "Leads & Contacts",
|
| 40 |
+
"category": "crm",
|
| 41 |
+
"synonyms": ["leads", "lead", "contacts", "cpl", "cost per lead", "form leads"],
|
| 42 |
+
"attribute_keys": ["totalLeads", "costPerLead", "welcomeEmailSentCount"],
|
| 43 |
+
"display_metrics": ["Total Leads Generated", "Cost Per Lead (CPL)", "Email Nurture Status"]
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
SuperAgent/app/mcp/__pycache__/mcp_server.cpython-313.pyc
ADDED
|
Binary file (4.37 kB). View file
|
|
|
SuperAgent/app/mcp/mcp_server.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model Context Protocol (MCP) Server Registry — ScaleMate AI Data Layer.
|
| 3 |
+
Exposes standardized MCP tools for GA4, Shopify, and Meta Ads.
|
| 4 |
+
Decouples LLM Agents from direct REST endpoints via JSON-RPC tool contracts.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
from typing import Any, Callable, Dict, Optional
|
| 11 |
+
from ..services.realtime_fetcher import fetch_realtime_ga4, fetch_realtime_shopify, fetch_realtime_meta, fetch_tenant_connections
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class MCPServerRegistry:
|
| 15 |
+
"""Standardized Model Context Protocol (MCP) Server Tool Registry."""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
self._tools: Dict[str, Callable] = {}
|
| 19 |
+
self._register_default_mcp_tools()
|
| 20 |
+
|
| 21 |
+
def _register_default_mcp_tools(self):
|
| 22 |
+
"""Register default MCP tools for GA4, Shopify, and Meta Ads."""
|
| 23 |
+
self.register_tool(
|
| 24 |
+
name="ga4_get_traffic_metrics",
|
| 25 |
+
description="Fetches live Google Analytics 4 traffic metrics including activeUsers, sessions, and bounceRate.",
|
| 26 |
+
func=self._mcp_ga4_tool,
|
| 27 |
+
)
|
| 28 |
+
self.register_tool(
|
| 29 |
+
name="shopify_get_sales_metrics",
|
| 30 |
+
description="Fetches live Shopify store sales metrics including totalRevenue, totalOrders, AOV, and funnel checkouts.",
|
| 31 |
+
func=self._mcp_shopify_tool,
|
| 32 |
+
)
|
| 33 |
+
self.register_tool(
|
| 34 |
+
name="meta_get_ad_insights",
|
| 35 |
+
description="Fetches live Meta Ads performance insights including spend, ROAS, CTR, CPC, and campaign alerts.",
|
| 36 |
+
func=self._mcp_meta_tool,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def register_tool(self, name: str, description: str, func: Callable):
|
| 40 |
+
"""Register an MCP tool into the registry."""
|
| 41 |
+
self._tools[name] = {
|
| 42 |
+
"name": name,
|
| 43 |
+
"description": description,
|
| 44 |
+
"handler": func,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
def list_tools(self) -> list[dict[str, str]]:
|
| 48 |
+
"""List all available MCP tools."""
|
| 49 |
+
return [
|
| 50 |
+
{"name": t["name"], "description": t["description"]}
|
| 51 |
+
for t in self._tools.values()
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
def call_tool(self, name: str, tenant_id: str, **kwargs) -> Optional[dict[str, Any]]:
|
| 55 |
+
"""Invoke an MCP tool by name for a given tenant."""
|
| 56 |
+
tool = self._tools.get(name)
|
| 57 |
+
if not tool:
|
| 58 |
+
print(f"- MCP Server: Tool '{name}' not found in registry")
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
handler = tool["handler"]
|
| 62 |
+
return handler(tenant_id=tenant_id, **kwargs)
|
| 63 |
+
|
| 64 |
+
def _mcp_ga4_tool(self, tenant_id: str, **kwargs) -> Optional[dict[str, Any]]:
|
| 65 |
+
connections = fetch_tenant_connections(tenant_id)
|
| 66 |
+
return fetch_realtime_ga4(connections.get("ga4"))
|
| 67 |
+
|
| 68 |
+
def _mcp_shopify_tool(self, tenant_id: str, **kwargs) -> Optional[dict[str, Any]]:
|
| 69 |
+
connections = fetch_tenant_connections(tenant_id)
|
| 70 |
+
return fetch_realtime_shopify(connections.get("shopify"))
|
| 71 |
+
|
| 72 |
+
def _mcp_meta_tool(self, tenant_id: str, **kwargs) -> Optional[dict[str, Any]]:
|
| 73 |
+
connections = fetch_tenant_connections(tenant_id)
|
| 74 |
+
return fetch_realtime_meta(connections.get("meta"))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
mcp_registry = MCPServerRegistry()
|
SuperAgent/app/models/__pycache__/schemas.cpython-313.pyc
ADDED
|
Binary file (6.53 kB). View file
|
|
|
SuperAgent/app/models/schemas.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic request/response schemas for the Executive SuperAgent orchestrator.
|
| 3 |
+
All API communication between client, Next.js proxy, SuperAgent, and sub-agents
|
| 4 |
+
uses these validated models.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from typing import Any, Optional
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# Incoming payload schema for SuperAgent API
|
| 14 |
+
|
| 15 |
+
class ChatHistoryItem(BaseModel):
|
| 16 |
+
role: str = Field(..., description="user | assistant | system")
|
| 17 |
+
content: str
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class GA4Data(BaseModel):
|
| 21 |
+
activeUsers: int = 0
|
| 22 |
+
totalSessions: int = 0
|
| 23 |
+
bounceRate: float = 0.0
|
| 24 |
+
trafficSources: list[dict[str, Any]] = []
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ShopifyData(BaseModel):
|
| 28 |
+
totalRevenue: float = 0.0
|
| 29 |
+
totalOrders: int = 0
|
| 30 |
+
avgOrderValue: float = 0.0
|
| 31 |
+
funnel: dict[str, Any] = {}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class MetaCampaign(BaseModel):
|
| 35 |
+
name: str = ""
|
| 36 |
+
spend: float = 0.0
|
| 37 |
+
roas: float = 0.0
|
| 38 |
+
status: str = "ACTIVE"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class MetaData(BaseModel):
|
| 42 |
+
spend: float = 0.0
|
| 43 |
+
impressions: int = 0
|
| 44 |
+
clicks: int = 0
|
| 45 |
+
ctr: float = 0.0
|
| 46 |
+
cpc: float = 0.0
|
| 47 |
+
roas: float = 0.0
|
| 48 |
+
campaigns: list[MetaCampaign] = []
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class LeadsData(BaseModel):
|
| 52 |
+
totalLeads: int = 0
|
| 53 |
+
costPerLead: float = 0.0
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class SuperAgentRequest(BaseModel):
|
| 57 |
+
"""Main request body for POST /api/v1/chat/message"""
|
| 58 |
+
user_query: str
|
| 59 |
+
tenant_id: str
|
| 60 |
+
user_id: str
|
| 61 |
+
session_id: Optional[str] = "default-session"
|
| 62 |
+
chat_history: list[ChatHistoryItem] = []
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# Response from SuperAgent back to client / proxy
|
| 67 |
+
|
| 68 |
+
class SuperAgentResponse(BaseModel):
|
| 69 |
+
"""Main response body for POST /api/v1/chat"""
|
| 70 |
+
success: bool = True
|
| 71 |
+
intent: str = "executive"
|
| 72 |
+
session_id: str = ""
|
| 73 |
+
answer: str = ""
|
| 74 |
+
orchestration_steps: list[str] = []
|
| 75 |
+
email_alert_triggered: bool = False
|
| 76 |
+
email_type: Optional[str] = None
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class ErrorResponse(BaseModel):
|
| 80 |
+
"""Error response sent back when something fails"""
|
| 81 |
+
success: bool = False
|
| 82 |
+
error: str = "Unable to complete analysis right now. Please try again in a moment."
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# Sub-Agent request/response schemas
|
| 86 |
+
|
| 87 |
+
class GrowthAnalyzeRequest(BaseModel):
|
| 88 |
+
"""Request for POST /api/v1/growth/analyze"""
|
| 89 |
+
user_query: str
|
| 90 |
+
tenant_id: str
|
| 91 |
+
user_id: str
|
| 92 |
+
session_id: Optional[str] = "default-session"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class GrowthAnalyzeResponse(BaseModel):
|
| 96 |
+
"""Response from Growth Agent"""
|
| 97 |
+
success: bool = True
|
| 98 |
+
agent: str = "growth_agent"
|
| 99 |
+
traffic_summary: str = ""
|
| 100 |
+
aov: float = 0.0
|
| 101 |
+
checkout_conversion_rate: float = 0.0
|
| 102 |
+
insights: str = ""
|
| 103 |
+
recommendations: list[str] = []
|
| 104 |
+
answer: Optional[str] = None
|
| 105 |
+
tenant_id: Optional[str] = None
|
| 106 |
+
session_id: Optional[str] = None
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class MarketingAnalyzeRequest(BaseModel):
|
| 110 |
+
"""Request for POST /api/v1/marketing/analyze"""
|
| 111 |
+
user_query: str
|
| 112 |
+
tenant_id: str
|
| 113 |
+
user_id: str
|
| 114 |
+
session_id: Optional[str] = "default-session"
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class MarketingAnalyzeResponse(BaseModel):
|
| 120 |
+
"""Response from Marketing Agent"""
|
| 121 |
+
success: bool = True
|
| 122 |
+
agent: str = "marketing_agent"
|
| 123 |
+
ad_spend: float = 0.0
|
| 124 |
+
overall_roas: float = 0.0
|
| 125 |
+
cost_per_lead: float = 0.0
|
| 126 |
+
high_performers: list[dict[str, Any]] = []
|
| 127 |
+
wasted_spend_campaigns: list[dict[str, Any]] = []
|
| 128 |
+
email_triggered: bool = False
|
| 129 |
+
email_alert_type: Optional[str] = None
|
| 130 |
+
answer: Optional[str] = None
|
| 131 |
+
tenant_id: Optional[str] = None
|
| 132 |
+
session_id: Optional[str] = None
|
| 133 |
+
|
SuperAgent/app/services/__pycache__/asset_registry.cpython-313.pyc
ADDED
|
Binary file (19.4 kB). View file
|
|
|
SuperAgent/app/services/__pycache__/email_service.cpython-313.pyc
ADDED
|
Binary file (7.56 kB). View file
|
|
|
SuperAgent/app/services/__pycache__/knowledge_cache_service.cpython-313.pyc
ADDED
|
Binary file (14 kB). View file
|
|
|
SuperAgent/app/services/__pycache__/logger.cpython-313.pyc
ADDED
|
Binary file (1.92 kB). View file
|
|
|
SuperAgent/app/services/__pycache__/realtime_fetcher.cpython-313.pyc
ADDED
|
Binary file (17.4 kB). View file
|
|
|
SuperAgent/app/services/__pycache__/report_exporter.cpython-313.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
SuperAgent/app/services/__pycache__/router.cpython-313.pyc
ADDED
|
Binary file (30.1 kB). View file
|
|
|
SuperAgent/app/services/asset_registry.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Asset Registry — Live API payload aggregator, session cache manager & counter-question resolver.
|
| 3 |
+
Cleans, validates and prepares incoming GA4, Shopify, Meta, and Leads JSON payloads.
|
| 4 |
+
Includes Session Cache (10-min TTL), Channel-Metric Mapping, and 4 Counter-Questioning Scenarios.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import time
|
| 10 |
+
from typing import Any, Optional
|
| 11 |
+
|
| 12 |
+
PLATFORM_METRICS_MAP: dict[str, dict[str, Any]] = {
|
| 13 |
+
"shopify": {
|
| 14 |
+
"name": "Shopify",
|
| 15 |
+
"metrics": ["revenue", "sales", "orders", "aov", "average order value", "checkouts", "purchases", "funnel", "products", "top products", "ltv", "lifetime value", "retention", "best sellers"],
|
| 16 |
+
"display_metrics": ["Total Revenue & Orders", "Average Order Value (AOV)", "Customer Lifetime Value (LTV)", "Top Selling Products & Funnel"],
|
| 17 |
+
},
|
| 18 |
+
"ga4": {
|
| 19 |
+
"name": "Google Analytics (GA4)",
|
| 20 |
+
"metrics": ["traffic", "sessions", "visitors", "active users", "users", "bounce", "bounce rate", "sources", "pageviews", "new users", "returning users", "engagement"],
|
| 21 |
+
"display_metrics": ["Active Users & Total Sessions", "Bounce Rate", "Traffic Sources", "New vs Returning Visitors"],
|
| 22 |
+
},
|
| 23 |
+
"meta": {
|
| 24 |
+
"name": "Meta Ads (Facebook/Instagram)",
|
| 25 |
+
"metrics": ["spend", "ad spend", "roas", "return on ad spend", "ctr", "click through rate", "cpc", "cpm", "clicks", "impressions", "campaigns", "cac", "customer acquisition cost", "efficiency"],
|
| 26 |
+
"display_metrics": ["Ad Spend & Overall ROAS", "Click-Through Rate (CTR) & CPC", "Customer Acquisition Cost (CAC)", "Campaign Performance"],
|
| 27 |
+
},
|
| 28 |
+
"leads": {
|
| 29 |
+
"name": "Leads / Google Ads",
|
| 30 |
+
"metrics": ["leads", "total leads", "cpl", "cost per lead", "lead conversion"],
|
| 31 |
+
"display_metrics": ["Total Leads", "Cost Per Lead (CPL)"],
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class SessionCacheManager:
|
| 37 |
+
"""Session cache manager maintaining state across multi-turn queries with 10-min TTL."""
|
| 38 |
+
TTL_SECONDS = 600 # 10 minutes
|
| 39 |
+
|
| 40 |
+
def __init__(self):
|
| 41 |
+
self._cache: dict[str, dict[str, Any]] = {}
|
| 42 |
+
|
| 43 |
+
def _cleanup_expired(self):
|
| 44 |
+
now = time.time()
|
| 45 |
+
expired = [sid for sid, data in self._cache.items() if now - data.get("last_updated", 0) > self.TTL_SECONDS]
|
| 46 |
+
for sid in expired:
|
| 47 |
+
del self._cache[sid]
|
| 48 |
+
|
| 49 |
+
def get_session(self, session_id: str) -> dict[str, Any]:
|
| 50 |
+
self._cleanup_expired()
|
| 51 |
+
if session_id not in self._cache:
|
| 52 |
+
self._cache[session_id] = {
|
| 53 |
+
"active_channels": [],
|
| 54 |
+
"awaiting_metric": False,
|
| 55 |
+
"awaiting_channel": False,
|
| 56 |
+
"last_metric": None,
|
| 57 |
+
"last_updated": time.time(),
|
| 58 |
+
}
|
| 59 |
+
return self._cache[session_id]
|
| 60 |
+
|
| 61 |
+
def update_session(self, session_id: str, **kwargs):
|
| 62 |
+
session = self.get_session(session_id)
|
| 63 |
+
session.update(kwargs)
|
| 64 |
+
session["last_updated"] = time.time()
|
| 65 |
+
|
| 66 |
+
def clear_session(self, session_id: str):
|
| 67 |
+
if session_id in self._cache:
|
| 68 |
+
del self._cache[session_id]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
session_cache = SessionCacheManager()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_field(obj: Any, field: str, default: Any = None) -> Any:
|
| 75 |
+
"""Helper to extract field from either a dict or an object safely."""
|
| 76 |
+
if obj is None:
|
| 77 |
+
return default
|
| 78 |
+
if isinstance(obj, dict):
|
| 79 |
+
return obj.get(field, default)
|
| 80 |
+
return getattr(obj, field, default)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def build_growth_payload(
|
| 84 |
+
ga4_data: Optional[Any],
|
| 85 |
+
shopify_data: Optional[Any],
|
| 86 |
+
) -> dict[str, Any]:
|
| 87 |
+
"""Build clean payload for Growth Agent."""
|
| 88 |
+
payload: dict[str, Any] = {}
|
| 89 |
+
|
| 90 |
+
if ga4_data:
|
| 91 |
+
active_users = get_field(ga4_data, "activeUsers", 0) or 0
|
| 92 |
+
total_sessions = get_field(ga4_data, "totalSessions", 0) or 0
|
| 93 |
+
bounce_rate = get_field(ga4_data, "bounceRate", 0.0) or 0.0
|
| 94 |
+
traffic_sources = get_field(ga4_data, "trafficSources", []) or []
|
| 95 |
+
new_users = get_field(ga4_data, "newUsers", int(active_users * 0.7)) or 0
|
| 96 |
+
|
| 97 |
+
payload["ga4_data"] = {
|
| 98 |
+
"activeUsers": active_users,
|
| 99 |
+
"totalSessions": total_sessions,
|
| 100 |
+
"bounceRate": round(float(bounce_rate), 2),
|
| 101 |
+
"trafficSources": traffic_sources,
|
| 102 |
+
"newUsers": new_users,
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if shopify_data:
|
| 106 |
+
total_revenue = get_field(shopify_data, "totalRevenue", 0.0) or 0.0
|
| 107 |
+
total_orders = get_field(shopify_data, "totalOrders", 0) or 0
|
| 108 |
+
avg_order_value = get_field(shopify_data, "avgOrderValue", 0.0) or 0.0
|
| 109 |
+
funnel = get_field(shopify_data, "funnel", {}) or {}
|
| 110 |
+
ltv = get_field(shopify_data, "ltv", round(avg_order_value * 2.5, 2)) or 0.0
|
| 111 |
+
top_products = get_field(shopify_data, "topProducts", [
|
| 112 |
+
{"name": "Best Seller Core Pack", "sales": round(total_revenue * 0.4, 2)},
|
| 113 |
+
{"name": "Growth Starter Bundle", "sales": round(total_revenue * 0.25, 2)},
|
| 114 |
+
]) or []
|
| 115 |
+
|
| 116 |
+
payload["shopify_data"] = {
|
| 117 |
+
"totalRevenue": round(float(total_revenue), 2),
|
| 118 |
+
"totalOrders": total_orders,
|
| 119 |
+
"avgOrderValue": round(float(avg_order_value), 2),
|
| 120 |
+
"funnel": funnel,
|
| 121 |
+
"ltv": round(float(ltv), 2),
|
| 122 |
+
"topProducts": top_products,
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return payload
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def build_marketing_payload(
|
| 129 |
+
meta_data: Optional[Any],
|
| 130 |
+
leads_data: Optional[Any],
|
| 131 |
+
) -> dict[str, Any]:
|
| 132 |
+
"""Build clean payload for Marketing Agent."""
|
| 133 |
+
payload: dict[str, Any] = {}
|
| 134 |
+
|
| 135 |
+
if meta_data:
|
| 136 |
+
spend = get_field(meta_data, "spend", 0.0) or 0.0
|
| 137 |
+
impressions = get_field(meta_data, "impressions", 0) or 0
|
| 138 |
+
clicks = get_field(meta_data, "clicks", 0) or 0
|
| 139 |
+
ctr = get_field(meta_data, "ctr", 0.0) or 0.0
|
| 140 |
+
cpc = get_field(meta_data, "cpc", 0.0) or 0.0
|
| 141 |
+
roas = get_field(meta_data, "roas", 0.0) or 0.0
|
| 142 |
+
campaigns_raw = get_field(meta_data, "campaigns", []) or []
|
| 143 |
+
cac = get_field(meta_data, "cac", round(spend / 15, 2) if spend > 0 else 0.0) or 0.0
|
| 144 |
+
|
| 145 |
+
clean_campaigns = []
|
| 146 |
+
for c in campaigns_raw:
|
| 147 |
+
c_name = get_field(c, "name", "Unknown")
|
| 148 |
+
c_spend = get_field(c, "spend", 0.0) or 0.0
|
| 149 |
+
c_roas = get_field(c, "roas", 0.0) or 0.0
|
| 150 |
+
c_status = get_field(c, "status", "ACTIVE")
|
| 151 |
+
clean_campaigns.append({
|
| 152 |
+
"name": c_name,
|
| 153 |
+
"spend": round(float(c_spend), 2),
|
| 154 |
+
"roas": round(float(c_roas), 2),
|
| 155 |
+
"status": c_status,
|
| 156 |
+
})
|
| 157 |
+
|
| 158 |
+
payload["meta_data"] = {
|
| 159 |
+
"spend": round(float(spend), 2),
|
| 160 |
+
"impressions": impressions,
|
| 161 |
+
"clicks": clicks,
|
| 162 |
+
"ctr": round(float(ctr), 2),
|
| 163 |
+
"cpc": round(float(cpc), 2),
|
| 164 |
+
"roas": round(float(roas), 2),
|
| 165 |
+
"campaigns": clean_campaigns,
|
| 166 |
+
"cac": round(float(cac), 2),
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
if leads_data:
|
| 170 |
+
total_leads = get_field(leads_data, "totalLeads", 0) or 0
|
| 171 |
+
cost_per_lead = get_field(leads_data, "costPerLead", 0.0) or 0.0
|
| 172 |
+
|
| 173 |
+
payload["leads_data"] = {
|
| 174 |
+
"totalLeads": total_leads,
|
| 175 |
+
"costPerLead": round(float(cost_per_lead), 2),
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
return payload
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def resolve_counter_question(user_query: str, session_id: str = "default-session", agent_name: str = "ScaleMate AI Assistant") -> Optional[str]:
|
| 182 |
+
"""
|
| 183 |
+
Evaluates query context & session state to trigger Counter-Questioning.
|
| 184 |
+
Supports agent_name customization for Growth, Marketing, and SuperAgent.
|
| 185 |
+
"""
|
| 186 |
+
q = user_query.lower().strip()
|
| 187 |
+
session = session_cache.get_session(session_id) if session_id else {}
|
| 188 |
+
|
| 189 |
+
# Rejection / Negative Feedback handling following suggestions or counter-questions
|
| 190 |
+
rejection_keywords = ["no", "na", "nope", "neither", "none", "nahi", "not now", "don't want"]
|
| 191 |
+
if q in rejection_keywords or any(q.startswith(rk + " ") for rk in rejection_keywords):
|
| 192 |
+
if session.get("awaiting_metric") or session.get("awaiting_channel") or session_id:
|
| 193 |
+
session_cache.update_session(session_id, awaiting_metric=False, awaiting_channel=False)
|
| 194 |
+
return (
|
| 195 |
+
"No problem at all! How else can I assist you today? "
|
| 196 |
+
"You can ask me for an overall business summary, custom executive report, or specific channel metrics whenever you're ready."
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# Bypass counter-questions for report and analysis requests
|
| 201 |
+
analysis_bypass_keywords = [
|
| 202 |
+
"report", "executive report", "summary report", "generate report", "download report",
|
| 203 |
+
"analysis", "analyze", "insights", "audit", "overview", "summary", "trend", "trends", "performance"
|
| 204 |
+
]
|
| 205 |
+
if any(rq in q for rq in analysis_bypass_keywords):
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# Detect mentioned channels
|
| 211 |
+
found_channels = []
|
| 212 |
+
if "shopify" in q:
|
| 213 |
+
found_channels.append("shopify")
|
| 214 |
+
if "ga4" in q or "google analytics" in q or ("traffic" in q and "meta" not in q):
|
| 215 |
+
found_channels.append("ga4")
|
| 216 |
+
if "meta" in q or "facebook" in q or "instagram" in q:
|
| 217 |
+
found_channels.append("meta")
|
| 218 |
+
if "lead" in q or "google ads" in q:
|
| 219 |
+
found_channels.append("leads")
|
| 220 |
+
|
| 221 |
+
# Detect mentioned metrics
|
| 222 |
+
found_metrics = []
|
| 223 |
+
all_known_metrics = {}
|
| 224 |
+
for platform_key, pdata in PLATFORM_METRICS_MAP.items():
|
| 225 |
+
for m in pdata["metrics"]:
|
| 226 |
+
all_known_metrics[m] = platform_key
|
| 227 |
+
if m in q:
|
| 228 |
+
found_metrics.append((m, platform_key))
|
| 229 |
+
|
| 230 |
+
# Scenario C: Requested metric is ABSENT on selected channel
|
| 231 |
+
if found_channels and found_metrics:
|
| 232 |
+
target_channel = found_channels[0]
|
| 233 |
+
metric_name, valid_channel = found_metrics[0]
|
| 234 |
+
if valid_channel != target_channel and metric_name not in PLATFORM_METRICS_MAP[target_channel]["metrics"]:
|
| 235 |
+
target_name = PLATFORM_METRICS_MAP[target_channel]["name"]
|
| 236 |
+
valid_name = PLATFORM_METRICS_MAP[valid_channel]["name"]
|
| 237 |
+
available = ", ".join(PLATFORM_METRICS_MAP[target_channel]["display_metrics"])
|
| 238 |
+
return (
|
| 239 |
+
f"**'{metric_name.title()}'** metric is not available on **{target_name}**.\n\n"
|
| 240 |
+
f"• **Available metrics on {target_name}:** {available}\n"
|
| 241 |
+
f"• **'{metric_name.title()}'** is available on **{valid_name}**.\n\n"
|
| 242 |
+
f"Would you like to check **{valid_name}** for '{metric_name.title()}'?"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# Scenario A: Multi-channel specified without metric
|
| 246 |
+
if len(found_channels) >= 2 and not found_metrics:
|
| 247 |
+
channel_names = [PLATFORM_METRICS_MAP[ch]["name"] for ch in found_channels]
|
| 248 |
+
names_str = " and ".join(channel_names)
|
| 249 |
+
session_cache.update_session(session_id, active_channels=found_channels, awaiting_metric=True)
|
| 250 |
+
return (
|
| 251 |
+
f"Which metrics would you like to check or compare for **{names_str}**?\n\n"
|
| 252 |
+
f"• **Option 1:** Revenue vs Ad Spend & ROAS\n"
|
| 253 |
+
f"• **Option 2:** Conversion Rate & Traffic Comparison\n"
|
| 254 |
+
f"• **Option 3:** Full Cross-Channel Performance Report"
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# Scenario B: Single channel specified without metric
|
| 258 |
+
if len(found_channels) == 1 and not found_metrics:
|
| 259 |
+
ch = found_channels[0]
|
| 260 |
+
ch_name = PLATFORM_METRICS_MAP[ch]["name"]
|
| 261 |
+
metrics_list = "\n".join([f"{i+1}. **{m}**" for i, m in enumerate(PLATFORM_METRICS_MAP[ch]["display_metrics"])])
|
| 262 |
+
session_cache.update_session(session_id, active_channels=[ch], awaiting_metric=True)
|
| 263 |
+
return (
|
| 264 |
+
f"Which metric would you like to check for **{ch_name}**?\n\n"
|
| 265 |
+
f"{metrics_list}"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Scenario D: Greeting handling
|
| 269 |
+
is_greeting = classify_intent_rule_based(q) == "greeting"
|
| 270 |
+
if is_greeting:
|
| 271 |
+
if "Growth" in agent_name:
|
| 272 |
+
scope_text = "website traffic (GA4), active users, Shopify store sales, or e-commerce conversion rates!"
|
| 273 |
+
elif "Marketing" in agent_name:
|
| 274 |
+
scope_text = "Meta Ads spend, ROAS performance, click-through rates, or campaign efficiency!"
|
| 275 |
+
else:
|
| 276 |
+
scope_text = "website traffic (GA4), active users, Shopify store sales, or Meta Ads performance!"
|
| 277 |
+
|
| 278 |
+
return f"Hello! I'm your {agent_name}. How can I help you today? You can ask me about your {scope_text}"
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
# Scenario E: Vague query missing channel or metric
|
| 282 |
+
vague_keywords = [
|
| 283 |
+
"data", "metric", "metrics", "info", "information", "detail", "details",
|
| 284 |
+
"stats", "status", "check", "conversion", "conversion rate",
|
| 285 |
+
"spend", "performance", "growth", "results", "analytics", "dashboard"
|
| 286 |
+
]
|
| 287 |
+
|
| 288 |
+
if not is_greeting and not found_channels and not found_metrics:
|
| 289 |
+
if any(vk in q for vk in vague_keywords) or len(q.split()) <= 6:
|
| 290 |
+
session_cache.update_session(session_id, awaiting_channel=True)
|
| 291 |
+
if "Growth" in agent_name:
|
| 292 |
+
return (
|
| 293 |
+
"Which growth metric or channel would you like to analyze?\n\n"
|
| 294 |
+
"1. **Shopify** (Store Sales, Revenue & AOV)\n"
|
| 295 |
+
"2. **Google Analytics (GA4)** (Active Users & Traffic Sessions)"
|
| 296 |
+
)
|
| 297 |
+
elif "Marketing" in agent_name:
|
| 298 |
+
return (
|
| 299 |
+
"Which marketing metric or platform would you like to analyze?\n\n"
|
| 300 |
+
"1. **Meta Ads** (Facebook & Instagram Ad Spend & ROAS)\n"
|
| 301 |
+
"2. **Lead Performance** (Cost Per Lead & Total Leads)"
|
| 302 |
+
)
|
| 303 |
+
else:
|
| 304 |
+
return (
|
| 305 |
+
f"Which platform or channel data would you like to check?\n\n"
|
| 306 |
+
f"1. **Shopify** (Store Sales & Revenue)\n"
|
| 307 |
+
f"2. **Google Analytics (GA4)** (Traffic & Sessions)\n"
|
| 308 |
+
f"3. **Meta Ads** (Facebook & Instagram Ad Spend & ROAS)"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
return None
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def classify_intent_rule_based(user_query: str) -> Optional[str]:
|
| 317 |
+
"""Step 1: Fast Rule-Based Keyword Intent Classifier."""
|
| 318 |
+
query_lower = user_query.lower().strip()
|
| 319 |
+
|
| 320 |
+
report_keywords = ["report", "executive report", "summary report", "generate report", "download report"]
|
| 321 |
+
if any(rk in query_lower for rk in report_keywords):
|
| 322 |
+
return "report"
|
| 323 |
+
|
| 324 |
+
greeting_keywords = [
|
| 325 |
+
"hi", "hy", "hii", "heyy", "hello", "hey", "salam", "assalam", "good morning",
|
| 326 |
+
"good evening", "good afternoon", "howdy", "hola",
|
| 327 |
+
"thanks", "thank you", "shukriya", "ok", "okay",
|
| 328 |
+
"bye", "goodbye", "see you",
|
| 329 |
+
]
|
| 330 |
+
if any(query_lower == kw or query_lower.startswith(kw + " ") or query_lower.startswith(kw + ",") for kw in greeting_keywords):
|
| 331 |
+
if len(query_lower.split()) <= 5:
|
| 332 |
+
return "greeting"
|
| 333 |
+
|
| 334 |
+
growth_keywords = [
|
| 335 |
+
"traffic", "ga4", "analytics", "session", "sessions", "bounce", "bounce rate",
|
| 336 |
+
"shopify", "revenue", "order", "orders", "aov", "checkout", "checkouts",
|
| 337 |
+
"funnel", "conversion", "visitor", "visitors", "pageview", "pageviews",
|
| 338 |
+
"ltv", "lifetime value", "top products", "best sellers", "retention",
|
| 339 |
+
"new users", "active users", "users", "user", "active user", "sources", "traffic sources",
|
| 340 |
+
]
|
| 341 |
+
marketing_keywords = [
|
| 342 |
+
"meta", "ads", "ad", "campaign", "campaigns", "roas", "ctr",
|
| 343 |
+
"cpc", "cpm", "spend", "ad spend", "impressions", "leads",
|
| 344 |
+
"lead", "cost per lead", "cpl", "facebook", "instagram",
|
| 345 |
+
"cac", "acquisition cost", "efficiency", "lead conversion",
|
| 346 |
+
]
|
| 347 |
+
|
| 348 |
+
has_growth = any(kw in query_lower for kw in growth_keywords)
|
| 349 |
+
has_marketing = any(kw in query_lower for kw in marketing_keywords)
|
| 350 |
+
|
| 351 |
+
if has_growth and has_marketing:
|
| 352 |
+
return "cross_channel"
|
| 353 |
+
if has_marketing:
|
| 354 |
+
return "marketing"
|
| 355 |
+
if has_growth:
|
| 356 |
+
return "growth"
|
| 357 |
+
|
| 358 |
+
return None
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def classify_intent(user_query: str, session_id: str = "", llm: Any = None) -> tuple[str, Optional[str]]:
|
| 362 |
+
"""
|
| 363 |
+
Hybrid Intent Classifier with Counter-Questioning & Session State:
|
| 364 |
+
Returns tuple: (intent_name, counter_question_prompt_or_none)
|
| 365 |
+
"""
|
| 366 |
+
counter_cq = resolve_counter_question(user_query, session_id=session_id)
|
| 367 |
+
if counter_cq:
|
| 368 |
+
return ("counter_question", counter_cq)
|
| 369 |
+
|
| 370 |
+
rule_result = classify_intent_rule_based(user_query)
|
| 371 |
+
if rule_result:
|
| 372 |
+
return (rule_result, None)
|
| 373 |
+
|
| 374 |
+
if llm:
|
| 375 |
+
try:
|
| 376 |
+
response = llm.invoke([
|
| 377 |
+
{
|
| 378 |
+
"role": "system",
|
| 379 |
+
"content": (
|
| 380 |
+
"Classify the user query into exactly ONE category based on these rules:\n"
|
| 381 |
+
"- 'growth': Queries asking about GA4 traffic, active users, sessions, visitors, Shopify sales, revenue, orders, AOV, or store metrics.\n"
|
| 382 |
+
"- 'marketing': Queries asking about Meta ads, Facebook/Instagram ads, ad spend, ROAS, CTR, CPC, CAC, or leads.\n"
|
| 383 |
+
"- 'cross_channel': Queries asking for both store/traffic and ad performance metrics together.\n"
|
| 384 |
+
"- 'report': Queries EXPLICITLY asking to generate, compile, or download a formal executive report, summary PDF, or CSV.\n"
|
| 385 |
+
"- 'greeting': Casual greetings (hi, hello, etc.).\n\n"
|
| 386 |
+
"Respond ONLY with the category name (lowercase, one word)."
|
| 387 |
+
),
|
| 388 |
+
},
|
| 389 |
+
{"role": "user", "content": user_query},
|
| 390 |
+
])
|
| 391 |
+
llm_intent = response.content.strip().lower()
|
| 392 |
+
if llm_intent in ["greeting", "growth", "marketing", "cross_channel", "report"]:
|
| 393 |
+
return (llm_intent, None)
|
| 394 |
+
except Exception as e:
|
| 395 |
+
print(f"- Gateway: LLM intent classification error: {e}")
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
return ("cross_channel", None)
|
SuperAgent/app/services/email_service.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Meta Ads Email Alert Service.
|
| 3 |
+
Triggers email notifications when:
|
| 4 |
+
- High ROAS (> 3.0x) -> Scaling opportunity email
|
| 5 |
+
- Wasted Spend (high spend, 0 conversions / ROAS < 1.0) -> Warning email
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import smtplib
|
| 12 |
+
from email.mime.text import MIMEText
|
| 13 |
+
from email.mime.multipart import MIMEMultipart
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
ROAS_SCALING_THRESHOLD = 3.0
|
| 18 |
+
ROAS_WARNING_THRESHOLD = 1.0
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def check_campaign_alerts(campaigns: list[dict[str, Any]]) -> dict[str, Any]:
|
| 22 |
+
"""
|
| 23 |
+
Analyze campaigns and return alert flags + lists.
|
| 24 |
+
"""
|
| 25 |
+
high_performers = []
|
| 26 |
+
wasted = []
|
| 27 |
+
|
| 28 |
+
for campaign in campaigns:
|
| 29 |
+
roas = campaign.get("roas", 0)
|
| 30 |
+
name = campaign.get("name", "Unknown")
|
| 31 |
+
spend = campaign.get("spend", 0)
|
| 32 |
+
|
| 33 |
+
if roas >= ROAS_SCALING_THRESHOLD:
|
| 34 |
+
high_performers.append({
|
| 35 |
+
"name": name,
|
| 36 |
+
"roas": roas,
|
| 37 |
+
"spend": spend,
|
| 38 |
+
"action": "SCALE_BUDGET",
|
| 39 |
+
})
|
| 40 |
+
elif roas < ROAS_WARNING_THRESHOLD and spend > 0:
|
| 41 |
+
wasted.append({
|
| 42 |
+
"name": name,
|
| 43 |
+
"roas": roas,
|
| 44 |
+
"spend": spend,
|
| 45 |
+
"wasted_amount": round(spend * (1 - roas), 2),
|
| 46 |
+
"action": "PAUSE_CAMPAIGN",
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
triggered = len(high_performers) > 0 or len(wasted) > 0
|
| 50 |
+
alert_type = None
|
| 51 |
+
if high_performers and wasted:
|
| 52 |
+
alert_type = "mixed_alert"
|
| 53 |
+
elif high_performers:
|
| 54 |
+
alert_type = "scaling_alert"
|
| 55 |
+
elif wasted:
|
| 56 |
+
alert_type = "wasted_spend_warning"
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
"triggered": triggered,
|
| 60 |
+
"type": alert_type,
|
| 61 |
+
"high_performers": high_performers,
|
| 62 |
+
"wasted": wasted,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def send_alert_email(
|
| 67 |
+
alert_type: str,
|
| 68 |
+
high_performers: list[dict],
|
| 69 |
+
wasted: list[dict],
|
| 70 |
+
) -> bool:
|
| 71 |
+
"""
|
| 72 |
+
Send Meta Ads performance alert email via SMTP.
|
| 73 |
+
"""
|
| 74 |
+
smtp_host = os.getenv("SMTP_HOST", "")
|
| 75 |
+
smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
| 76 |
+
smtp_user = os.getenv("SMTP_USER", "")
|
| 77 |
+
smtp_pass = os.getenv("SMTP_PASS", "")
|
| 78 |
+
to_email = os.getenv("ALERT_EMAIL_TO", "")
|
| 79 |
+
|
| 80 |
+
if not all([smtp_host, smtp_user, smtp_pass, to_email]):
|
| 81 |
+
print("Email Alert: SMTP credentials not configured, skipping email.")
|
| 82 |
+
return False
|
| 83 |
+
|
| 84 |
+
subject = "ScaleMate: Meta Ads Performance Alert"
|
| 85 |
+
body_lines = ["<h2>Meta Ads Performance Alert</h2>"]
|
| 86 |
+
|
| 87 |
+
if high_performers:
|
| 88 |
+
body_lines.append("<h3>Scaling Opportunities</h3><ul>")
|
| 89 |
+
for hp in high_performers:
|
| 90 |
+
body_lines.append(
|
| 91 |
+
f"<li><b>{hp['name']}</b> - ROAS: {hp['roas']}x, "
|
| 92 |
+
f"Spend: ${hp['spend']} -> Action: Increase Budget</li>"
|
| 93 |
+
)
|
| 94 |
+
body_lines.append("</ul>")
|
| 95 |
+
|
| 96 |
+
if wasted:
|
| 97 |
+
body_lines.append("<h3>Wasted Spend Warnings</h3><ul>")
|
| 98 |
+
for w in wasted:
|
| 99 |
+
body_lines.append(
|
| 100 |
+
f"<li><b>{w['name']}</b> - ROAS: {w['roas']}x, "
|
| 101 |
+
f"Wasted: ${w['wasted_amount']} -> Action: Pause Campaign</li>"
|
| 102 |
+
)
|
| 103 |
+
body_lines.append("</ul>")
|
| 104 |
+
|
| 105 |
+
html_body = "\n".join(body_lines)
|
| 106 |
+
|
| 107 |
+
msg = MIMEMultipart("alternative")
|
| 108 |
+
msg["Subject"] = subject
|
| 109 |
+
msg["From"] = smtp_user
|
| 110 |
+
msg["To"] = to_email
|
| 111 |
+
msg.attach(MIMEText(html_body, "html"))
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
| 115 |
+
server.starttls()
|
| 116 |
+
server.login(smtp_user, smtp_pass)
|
| 117 |
+
server.sendmail(smtp_user, to_email, msg.as_string())
|
| 118 |
+
print(f"Email Alert: {alert_type} email sent to {to_email}")
|
| 119 |
+
return True
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"Email Alert Error: {e}")
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def send_report_email(
|
| 126 |
+
report_markdown: str,
|
| 127 |
+
recipient_email: str = "",
|
| 128 |
+
) -> bool:
|
| 129 |
+
"""
|
| 130 |
+
Send full Executive Performance Report via SMTP email.
|
| 131 |
+
"""
|
| 132 |
+
smtp_host = os.getenv("SMTP_HOST", "")
|
| 133 |
+
smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
| 134 |
+
smtp_user = os.getenv("SMTP_USER", "")
|
| 135 |
+
smtp_pass = os.getenv("SMTP_PASS", "")
|
| 136 |
+
to_email = recipient_email or os.getenv("ALERT_EMAIL_TO", "")
|
| 137 |
+
|
| 138 |
+
if not all([smtp_host, smtp_user, smtp_pass, to_email]):
|
| 139 |
+
print(f"Report Email: SMTP credentials missing or target email '{to_email}' empty.")
|
| 140 |
+
return False
|
| 141 |
+
|
| 142 |
+
subject = "ScaleMate AI: Executive Performance & Growth Report"
|
| 143 |
+
|
| 144 |
+
# Simple HTML conversion for markdown formatting
|
| 145 |
+
html_lines = ["<div style='font-family: Arial, sans-serif; line-height: 1.6;'>"]
|
| 146 |
+
for line in report_markdown.split("\n"):
|
| 147 |
+
if line.startswith("# "):
|
| 148 |
+
html_lines.append(f"<h1 style='color: #4F46E5;'>{line[2:]}</h1>")
|
| 149 |
+
elif line.startswith("## "):
|
| 150 |
+
html_lines.append(f"<h2 style='color: #1E293B;'>{line[3:]}</h2>")
|
| 151 |
+
elif line.startswith("- "):
|
| 152 |
+
html_lines.append(f"<li>{line[2:]}</li>")
|
| 153 |
+
elif line.strip():
|
| 154 |
+
html_lines.append(f"<p>{line}</p>")
|
| 155 |
+
html_lines.append("</div>")
|
| 156 |
+
html_body = "\n".join(html_lines)
|
| 157 |
+
|
| 158 |
+
msg = MIMEMultipart("alternative")
|
| 159 |
+
msg["Subject"] = subject
|
| 160 |
+
msg["From"] = smtp_user
|
| 161 |
+
msg["To"] = to_email
|
| 162 |
+
msg.attach(MIMEText(html_body, "html"))
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
| 166 |
+
server.starttls()
|
| 167 |
+
server.login(smtp_user, smtp_pass)
|
| 168 |
+
server.sendmail(smtp_user, to_email, msg.as_string())
|
| 169 |
+
print(f"Report Email: Sent successfully to {to_email}")
|
| 170 |
+
return True
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"Report Email Error: {e}")
|
| 173 |
+
return False
|
| 174 |
+
|
SuperAgent/app/services/knowledge_cache_service.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Knowledge Cache Service — 3-Layer Assets Knowledge Graph Cache.
|
| 3 |
+
Layer 1: In-Memory RAM Dict (0ms Instant Return)
|
| 4 |
+
Layer 2: Disk File (Assets Knowledge/<tenant_id>/<user_id>/cache.json) (0ms Disk Load into RAM)
|
| 5 |
+
Layer 3: Supabase DB Fetch & 100% Real-Time Dynamic Metadata Enrichment (Zero Static Config Files)
|
| 6 |
+
|
| 7 |
+
Reads all configurations strictly from live DB connections & .env.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import json
|
| 14 |
+
import time
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Any, Optional
|
| 17 |
+
import httpx
|
| 18 |
+
from dotenv import load_dotenv
|
| 19 |
+
|
| 20 |
+
# Explicitly load root .env file
|
| 21 |
+
root_env = Path(__file__).resolve().parents[3] / ".env"
|
| 22 |
+
if root_env.exists():
|
| 23 |
+
load_dotenv(dotenv_path=root_env)
|
| 24 |
+
else:
|
| 25 |
+
load_dotenv()
|
| 26 |
+
|
| 27 |
+
SUPABASE_URL = os.getenv("NEXT_PUBLIC_SUPABASE_URL", "")
|
| 28 |
+
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
|
| 29 |
+
|
| 30 |
+
BASE_KNOWLEDGE_DIR = os.getenv(
|
| 31 |
+
"ASSETS_KNOWLEDGE_DIR",
|
| 32 |
+
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "Assets Knowledge")
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Standard Provider Metadata Mapping for Dynamic Enrichment
|
| 36 |
+
DEFAULT_PROVIDER_METADATA: dict[str, dict[str, Any]] = {
|
| 37 |
+
"shopify": {
|
| 38 |
+
"name": "Shopify E-Commerce",
|
| 39 |
+
"category": "store",
|
| 40 |
+
"synonyms": ["shopify", "store", "shop", "sales", "revenue", "orders", "checkout"],
|
| 41 |
+
"attribute_keys": ["totalRevenue", "totalOrders", "avgOrderValue", "checkouts"],
|
| 42 |
+
"display_metrics": ["Total Revenue & Orders", "Average Order Value (AOV)", "Checkout Conversion Rate"],
|
| 43 |
+
},
|
| 44 |
+
"ga4": {
|
| 45 |
+
"name": "Google Analytics 4 (GA4)",
|
| 46 |
+
"category": "analytics",
|
| 47 |
+
"synonyms": ["ga4", "google analytics", "analytics", "traffic", "visitors", "sessions", "website"],
|
| 48 |
+
"attribute_keys": ["activeUsers", "totalSessions", "bounceRate", "trafficSources"],
|
| 49 |
+
"display_metrics": ["Active Users & Total Sessions", "Bounce Rate", "Traffic Channels"],
|
| 50 |
+
},
|
| 51 |
+
"meta": {
|
| 52 |
+
"name": "Meta Ads (Facebook/Instagram)",
|
| 53 |
+
"category": "marketing",
|
| 54 |
+
"synonyms": ["meta", "facebook", "instagram", "fb ads", "meta ads", "roas", "ctr", "ad spend"],
|
| 55 |
+
"attribute_keys": ["spend", "roas", "ctr", "cpc", "impressions", "clicks"],
|
| 56 |
+
"display_metrics": ["Ad Spend & Overall ROAS", "Click-Through Rate (CTR) & CPC", "Campaign Performance"],
|
| 57 |
+
},
|
| 58 |
+
"google_ads": {
|
| 59 |
+
"name": "Google Ads",
|
| 60 |
+
"category": "marketing",
|
| 61 |
+
"synonyms": ["google ads", "gads", "search ads", "pmax", "google ad spend"],
|
| 62 |
+
"attribute_keys": ["totalImpressions", "totalClicks", "totalSpend", "averageCtr", "averageCpc"],
|
| 63 |
+
"display_metrics": ["Google Ad Spend & ROAS", "Click-Through Rate (CTR)", "Search Campaign Breakdown"],
|
| 64 |
+
},
|
| 65 |
+
"leads": {
|
| 66 |
+
"name": "Leads & Contacts",
|
| 67 |
+
"category": "crm",
|
| 68 |
+
"synonyms": ["leads", "lead", "contacts", "cpl", "cost per lead", "form leads"],
|
| 69 |
+
"attribute_keys": ["totalLeads", "costPerLead", "welcomeEmailSentCount"],
|
| 70 |
+
"display_metrics": ["Total Leads Generated", "Cost Per Lead (CPL)", "Email Nurture Status"],
|
| 71 |
+
},
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class UserKnowledgeCacheManager:
|
| 76 |
+
"""
|
| 77 |
+
3-Layer Assets Knowledge Graph Cache Manager:
|
| 78 |
+
- self._user_cache: In-memory RAM cache indexed by "tenant_id:user_id".
|
| 79 |
+
- Disk: Assets Knowledge/<tenant_id>/<user_id>/cache.json.
|
| 80 |
+
- DB: Supabase PostgreSQL tables (user_tenant_roles & connections).
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(self):
|
| 84 |
+
self._user_cache: dict[str, dict[str, Any]] = {}
|
| 85 |
+
|
| 86 |
+
def _get_cache_key(self, tenant_id: str, user_id: str) -> str:
|
| 87 |
+
t_id = tenant_id or "default_tenant"
|
| 88 |
+
u_id = user_id or "default_user"
|
| 89 |
+
return f"{t_id}:{u_id}"
|
| 90 |
+
|
| 91 |
+
def get_cache_file_path(self, tenant_id: str, user_id: str) -> str:
|
| 92 |
+
t_id = tenant_id or "default_tenant"
|
| 93 |
+
u_id = user_id or "default_user"
|
| 94 |
+
user_dir = os.path.join(BASE_KNOWLEDGE_DIR, t_id, u_id)
|
| 95 |
+
os.makedirs(user_dir, exist_ok=True)
|
| 96 |
+
return os.path.join(user_dir, "cache.json")
|
| 97 |
+
|
| 98 |
+
def load_user_assets(self, tenant_id: str, user_id: str, force_refresh: bool = False) -> dict[str, Any]:
|
| 99 |
+
"""
|
| 100 |
+
3-Layer Knowledge Graph Resolution:
|
| 101 |
+
Step 1 (RAM Check): If key in self._user_cache and not force_refresh -> Return in 0ms!
|
| 102 |
+
Step 2 (Disk Check): If cache.json exists on disk and not force_refresh -> Load into RAM and Return in 0ms!
|
| 103 |
+
Step 3 (DB Query & Dynamic Build): Fetch connections from DB, enrich in real-time, populate RAM & write to Disk.
|
| 104 |
+
"""
|
| 105 |
+
cache_key = self._get_cache_key(tenant_id, user_id)
|
| 106 |
+
file_path = self.get_cache_file_path(tenant_id, user_id)
|
| 107 |
+
|
| 108 |
+
if not force_refresh:
|
| 109 |
+
# Step 1: In-Memory RAM Check (0ms)
|
| 110 |
+
if cache_key in self._user_cache:
|
| 111 |
+
print(f"- KnowledgeCache: [Layer 1 RAM Hit] Loaded memory cache for '{cache_key}' (0ms)")
|
| 112 |
+
return self._user_cache[cache_key]
|
| 113 |
+
|
| 114 |
+
# Step 2: Disk File Check (0ms)
|
| 115 |
+
if os.path.exists(file_path):
|
| 116 |
+
try:
|
| 117 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 118 |
+
data = json.load(f)
|
| 119 |
+
self._user_cache[cache_key] = data
|
| 120 |
+
print(f"- KnowledgeCache: [Layer 2 Disk Hit] Loaded cache.json into RAM for '{cache_key}' (0ms)")
|
| 121 |
+
return data
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"- KnowledgeCache: Warning loading cache.json from disk: {e}")
|
| 124 |
+
|
| 125 |
+
# Step 3: DB Query & 100% Real-Time Dynamic Build
|
| 126 |
+
return self._build_and_save_cache(tenant_id, user_id)
|
| 127 |
+
|
| 128 |
+
def _build_and_save_cache(self, tenant_id: str, user_id: str) -> dict[str, Any]:
|
| 129 |
+
"""Fetch active connections from DB, enrich metadata in 100% real-time, and save to RAM + Disk."""
|
| 130 |
+
cache_key = self._get_cache_key(tenant_id, user_id)
|
| 131 |
+
file_path = self.get_cache_file_path(tenant_id, user_id)
|
| 132 |
+
|
| 133 |
+
has_access = True
|
| 134 |
+
active_connections = {}
|
| 135 |
+
|
| 136 |
+
if SUPABASE_KEY and SUPABASE_URL and tenant_id:
|
| 137 |
+
headers = {
|
| 138 |
+
"apikey": SUPABASE_KEY,
|
| 139 |
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
| 140 |
+
"Content-Type": "application/json",
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
# Permission check - supports both snake_case and camelCase
|
| 144 |
+
if user_id:
|
| 145 |
+
try:
|
| 146 |
+
role_url = f"{SUPABASE_URL}/rest/v1/user_tenant_roles"
|
| 147 |
+
res = httpx.get(role_url, headers=headers, params={"user_id": f"eq.{user_id}", "tenant_id": f"eq.{tenant_id}", "select": "id"}, timeout=3.0)
|
| 148 |
+
if res.status_code != 200 or len(res.json()) == 0:
|
| 149 |
+
res2 = httpx.get(role_url, headers=headers, params={"userId": f"eq.{user_id}", "tenantId": f"eq.{tenant_id}", "select": "id"}, timeout=3.0)
|
| 150 |
+
if res2.status_code == 200 and len(res2.json()) == 0:
|
| 151 |
+
has_access = False
|
| 152 |
+
except Exception as e:
|
| 153 |
+
print(f"- KnowledgeCache: Permission lookup error: {e}")
|
| 154 |
+
|
| 155 |
+
# Connection lookup - supports both snake_case and camelCase
|
| 156 |
+
try:
|
| 157 |
+
conn_url = f"{SUPABASE_URL}/rest/v1/connections"
|
| 158 |
+
rows = []
|
| 159 |
+
|
| 160 |
+
res1 = httpx.get(conn_url, headers=headers, params={"tenant_id": f"eq.{tenant_id}", "select": "*"}, timeout=3.0)
|
| 161 |
+
if res1.status_code == 200:
|
| 162 |
+
rows.extend(res1.json())
|
| 163 |
+
|
| 164 |
+
res2 = httpx.get(conn_url, headers=headers, params={"tenantId": f"eq.{tenant_id}", "select": "*"}, timeout=3.0)
|
| 165 |
+
if res2.status_code == 200:
|
| 166 |
+
rows.extend(res2.json())
|
| 167 |
+
|
| 168 |
+
for r in rows:
|
| 169 |
+
p = r.get("provider")
|
| 170 |
+
st = str(r.get("status", "")).lower()
|
| 171 |
+
if p and st in ["connected", "active"]:
|
| 172 |
+
active_connections[p] = r
|
| 173 |
+
except Exception as e:
|
| 174 |
+
print(f"- KnowledgeCache: Connections lookup error: {e}")
|
| 175 |
+
|
| 176 |
+
# Construct Knowledge Graph dynamically from live DB connections
|
| 177 |
+
knowledge_graph = []
|
| 178 |
+
|
| 179 |
+
for provider_id, conn_data in active_connections.items():
|
| 180 |
+
meta = conn_data.get("metadata") or {}
|
| 181 |
+
def_meta = DEFAULT_PROVIDER_METADATA.get(provider_id, {
|
| 182 |
+
"name": provider_id.title(),
|
| 183 |
+
"category": "general",
|
| 184 |
+
"synonyms": [provider_id],
|
| 185 |
+
"attribute_keys": list(meta.keys()) if meta else ["status"],
|
| 186 |
+
"display_metrics": [f"{provider_id.title()} Performance Metrics"],
|
| 187 |
+
})
|
| 188 |
+
|
| 189 |
+
item = {
|
| 190 |
+
"id": conn_data.get("id", f"conn-{provider_id}"),
|
| 191 |
+
"provider": provider_id,
|
| 192 |
+
"name": meta.get("name") or def_meta.get("name"),
|
| 193 |
+
"category": def_meta.get("category", "general"),
|
| 194 |
+
"status": "connected",
|
| 195 |
+
"attribute_keys": meta.get("metrics") or def_meta.get("attribute_keys"),
|
| 196 |
+
"display_metrics": def_meta.get("display_metrics"),
|
| 197 |
+
"synonyms": def_meta.get("synonyms"),
|
| 198 |
+
"metadata": {
|
| 199 |
+
"propertyId": meta.get("propertyId") or meta.get("property_id") or conn_data.get("externalId"),
|
| 200 |
+
"shop": meta.get("shop") or meta.get("myshopifyDomain"),
|
| 201 |
+
"adAccountId": conn_data.get("externalId") or conn_data.get("external_id"),
|
| 202 |
+
},
|
| 203 |
+
}
|
| 204 |
+
knowledge_graph.append(item)
|
| 205 |
+
|
| 206 |
+
# Fallback to standard platform templates if tenant has no active DB connections yet
|
| 207 |
+
if not knowledge_graph:
|
| 208 |
+
for p_id, def_meta in DEFAULT_PROVIDER_METADATA.items():
|
| 209 |
+
knowledge_graph.append({
|
| 210 |
+
"id": f"conn-{p_id}",
|
| 211 |
+
"provider": p_id,
|
| 212 |
+
"name": def_meta["name"],
|
| 213 |
+
"category": def_meta["category"],
|
| 214 |
+
"status": "disconnected",
|
| 215 |
+
"attribute_keys": def_meta["attribute_keys"],
|
| 216 |
+
"display_metrics": def_meta["display_metrics"],
|
| 217 |
+
"synonyms": def_meta["synonyms"],
|
| 218 |
+
})
|
| 219 |
+
|
| 220 |
+
cache_data = {
|
| 221 |
+
"tenant_id": tenant_id,
|
| 222 |
+
"user_id": user_id,
|
| 223 |
+
"user_authorized": has_access,
|
| 224 |
+
"total_platforms": len(knowledge_graph),
|
| 225 |
+
"timestamp_epoch": time.time(),
|
| 226 |
+
"cached_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 227 |
+
"knowledge_graph": knowledge_graph,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
# 1. Update In-Memory RAM
|
| 231 |
+
self._user_cache[cache_key] = cache_data
|
| 232 |
+
|
| 233 |
+
# 2. Write to Disk cache.json
|
| 234 |
+
try:
|
| 235 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 236 |
+
json.dump(cache_data, f, indent=2)
|
| 237 |
+
print(f"- KnowledgeCache: [Layer 3 DB Build] Built Knowledge Graph and saved cache.json for '{cache_key}'")
|
| 238 |
+
except Exception as e:
|
| 239 |
+
print(f"- KnowledgeCache: Error writing cache.json to disk: {e}")
|
| 240 |
+
|
| 241 |
+
return cache_data
|
| 242 |
+
|
| 243 |
+
def invalidate_user_cache(self, tenant_id: str, user_id: str):
|
| 244 |
+
"""Force refresh / Manual sync: Evicts RAM cache and deletes disk cache.json."""
|
| 245 |
+
cache_key = self._get_cache_key(tenant_id, user_id)
|
| 246 |
+
file_path = self.get_cache_file_path(tenant_id, user_id)
|
| 247 |
+
|
| 248 |
+
if cache_key in self._user_cache:
|
| 249 |
+
del self._user_cache[cache_key]
|
| 250 |
+
|
| 251 |
+
if os.path.exists(file_path):
|
| 252 |
+
try:
|
| 253 |
+
os.remove(file_path)
|
| 254 |
+
print(f"- KnowledgeCache: [Force Refresh] Evicted cache.json from RAM and disk for '{cache_key}'")
|
| 255 |
+
except Exception as e:
|
| 256 |
+
print(f"- KnowledgeCache: Error deleting cache file: {e}")
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
knowledge_cache_manager = UserKnowledgeCacheManager()
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def load_user_assets(tenant_id: str, user_id: str, force_refresh: bool = False) -> dict[str, Any]:
|
| 263 |
+
"""Helper function to load user Knowledge Graph cache using 3-layer manager."""
|
| 264 |
+
return knowledge_cache_manager.load_user_assets(tenant_id, user_id, force_refresh=force_refresh)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def invalidate_user_cache(tenant_id: str, user_id: str):
|
| 268 |
+
"""Helper function to invalidate user cache."""
|
| 269 |
+
knowledge_cache_manager.invalidate_user_cache(tenant_id, user_id)
|
SuperAgent/app/services/logger.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Concise System Orchestration terminal logger.
|
| 3 |
+
Prints short, clean orchestration trace lines to the backend console.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class OrchestrationLogger:
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.steps: list[str] = []
|
| 13 |
+
|
| 14 |
+
def log(self, message: str) -> None:
|
| 15 |
+
"""Append a step and immediately print it."""
|
| 16 |
+
self.steps = [*self.steps, message]
|
| 17 |
+
print(f"- {message}")
|
| 18 |
+
|
| 19 |
+
def log_header(self) -> None:
|
| 20 |
+
"""Print the orchestration header."""
|
| 21 |
+
print("\nSystem Orchestration:")
|
| 22 |
+
|
| 23 |
+
def log_error(self, message: str) -> None:
|
| 24 |
+
"""Log an error step to terminal."""
|
| 25 |
+
error_msg = f"Gateway Error: {message}"
|
| 26 |
+
self.steps = [*self.steps, error_msg]
|
| 27 |
+
print(f"- {error_msg}")
|
| 28 |
+
|
| 29 |
+
def get_steps(self) -> list[str]:
|
| 30 |
+
"""Return collected orchestration steps."""
|
| 31 |
+
return list(self.steps)
|
SuperAgent/app/services/realtime_fetcher.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Real-Time Fetcher — Dynamic Live Data Fetching Module.
|
| 3 |
+
Connects directly to GA4, Shopify, and Meta Ads APIs.
|
| 4 |
+
Leverages the 3-Layer Knowledge Cache System (RAM -> Disk -> DB) for 0ms permission and connection resolution.
|
| 5 |
+
Fetches 100% real live metrics directly from provider APIs.
|
| 6 |
+
Includes automatic Google OAuth token refresh handling on 401 errors.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import httpx
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Any, Optional
|
| 15 |
+
from dotenv import load_dotenv
|
| 16 |
+
|
| 17 |
+
from .knowledge_cache_service import load_user_assets
|
| 18 |
+
|
| 19 |
+
# Explicitly load root .env file
|
| 20 |
+
root_env = Path(__file__).resolve().parents[3] / ".env"
|
| 21 |
+
if root_env.exists():
|
| 22 |
+
load_dotenv(dotenv_path=root_env)
|
| 23 |
+
else:
|
| 24 |
+
load_dotenv()
|
| 25 |
+
|
| 26 |
+
SUPABASE_URL = os.getenv("NEXT_PUBLIC_SUPABASE_URL", "")
|
| 27 |
+
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
|
| 28 |
+
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", "")
|
| 29 |
+
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET", "")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def verify_user_tenant_access(user_id: str, tenant_id: str) -> bool:
|
| 33 |
+
"""Verify user access via 3-Layer Knowledge Cache System (0ms RAM/Disk lookup)."""
|
| 34 |
+
if not user_id or not tenant_id:
|
| 35 |
+
return True
|
| 36 |
+
|
| 37 |
+
cache_data = load_user_assets(tenant_id, user_id)
|
| 38 |
+
return cache_data.get("user_authorized", True)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def refresh_google_access_token(refresh_token: str, connection_id: str = "") -> Optional[str]:
|
| 42 |
+
"""Refresh Google OAuth access token using refresh_token and save to DB."""
|
| 43 |
+
if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET or not refresh_token:
|
| 44 |
+
print("- RealtimeFetcher GA4: Cannot refresh token - missing GOOGLE_CLIENT_ID or refresh_token")
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
url = "https://oauth2.googleapis.com/token"
|
| 48 |
+
payload = {
|
| 49 |
+
"client_id": GOOGLE_CLIENT_ID,
|
| 50 |
+
"client_secret": GOOGLE_CLIENT_SECRET,
|
| 51 |
+
"refresh_token": refresh_token,
|
| 52 |
+
"grant_type": "refresh_token",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
print("- RealtimeFetcher GA4: Access token expired (HTTP 401). Exchanging refresh_token for new access_token...")
|
| 57 |
+
res = httpx.post(url, data=payload, timeout=6.0)
|
| 58 |
+
if res.status_code == 200:
|
| 59 |
+
data = res.json()
|
| 60 |
+
new_token = data.get("access_token")
|
| 61 |
+
print(f"- RealtimeFetcher GA4: Token refreshed successfully! New access_token length: {len(new_token or '')}")
|
| 62 |
+
|
| 63 |
+
# Update DB connection record with new token if connection_id provided
|
| 64 |
+
if connection_id and SUPABASE_URL and SUPABASE_KEY and new_token:
|
| 65 |
+
try:
|
| 66 |
+
update_url = f"{SUPABASE_URL}/rest/v1/connections"
|
| 67 |
+
headers = {
|
| 68 |
+
"apikey": SUPABASE_KEY,
|
| 69 |
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
| 70 |
+
"Content-Type": "application/json",
|
| 71 |
+
}
|
| 72 |
+
httpx.patch(
|
| 73 |
+
update_url,
|
| 74 |
+
headers=headers,
|
| 75 |
+
params={"id": f"eq.{connection_id}"},
|
| 76 |
+
json={"accessToken": new_token},
|
| 77 |
+
timeout=4.0,
|
| 78 |
+
)
|
| 79 |
+
except Exception as ex:
|
| 80 |
+
print(f"- RealtimeFetcher GA4: Error saving updated token to DB: {ex}")
|
| 81 |
+
|
| 82 |
+
return new_token
|
| 83 |
+
else:
|
| 84 |
+
print(f"- RealtimeFetcher GA4: Refresh token exchange failed HTTP {res.status_code}: {res.text}")
|
| 85 |
+
if "invalid_grant" in res.text:
|
| 86 |
+
print("GA4 OAuth Token Expired/Revoked! Please go to Connections (/tenant/connections) and click 'Reconnect' for GA4.")
|
| 87 |
+
except Exception as e:
|
| 88 |
+
print(f"- RealtimeFetcher GA4: Error refreshing Google OAuth token: {e}")
|
| 89 |
+
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def fetch_tenant_connections(tenant_id: str, user_id: str = "") -> dict[str, dict[str, Any]]:
|
| 94 |
+
"""
|
| 95 |
+
Fetch active provider connections for a tenant directly from Supabase DB.
|
| 96 |
+
Normalizes provider names (e.g. facebook/meta_ads -> meta) and handles all active status keys.
|
| 97 |
+
"""
|
| 98 |
+
if not tenant_id or not SUPABASE_KEY or not SUPABASE_URL:
|
| 99 |
+
return {}
|
| 100 |
+
|
| 101 |
+
url = f"{SUPABASE_URL}/rest/v1/connections"
|
| 102 |
+
headers = {
|
| 103 |
+
"apikey": SUPABASE_KEY,
|
| 104 |
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
| 105 |
+
"Content-Type": "application/json",
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
rows = []
|
| 109 |
+
try:
|
| 110 |
+
res1 = httpx.get(url, headers=headers, params={"tenant_id": f"eq.{tenant_id}", "select": "*"}, timeout=4.0)
|
| 111 |
+
if res1.status_code == 200:
|
| 112 |
+
rows.extend(res1.json())
|
| 113 |
+
except Exception as e:
|
| 114 |
+
print(f"- RealtimeFetcher: tenant_id lookup error: {e}")
|
| 115 |
+
|
| 116 |
+
try:
|
| 117 |
+
res2 = httpx.get(url, headers=headers, params={"tenantId": f"eq.{tenant_id}", "select": "*"}, timeout=4.0)
|
| 118 |
+
if res2.status_code == 200:
|
| 119 |
+
rows.extend(res2.json())
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"- RealtimeFetcher: tenantId lookup error: {e}")
|
| 122 |
+
|
| 123 |
+
found = {}
|
| 124 |
+
for r in rows:
|
| 125 |
+
raw_provider = str(r.get("provider", "")).lower().strip()
|
| 126 |
+
status_str = str(r.get("status", "")).lower().strip()
|
| 127 |
+
|
| 128 |
+
# Provider Key Normalization
|
| 129 |
+
if raw_provider in ["meta", "facebook", "meta_ads", "meta-ads", "facebook_ads"]:
|
| 130 |
+
p_key = "meta"
|
| 131 |
+
elif raw_provider in ["shopify", "shopify_store", "shopify-store"]:
|
| 132 |
+
p_key = "shopify"
|
| 133 |
+
elif raw_provider in ["ga4", "google_analytics", "google_analytics_4", "google-analytics"]:
|
| 134 |
+
p_key = "ga4"
|
| 135 |
+
else:
|
| 136 |
+
p_key = raw_provider
|
| 137 |
+
|
| 138 |
+
# Accept connected / active statuses or missing status
|
| 139 |
+
is_active = status_str in ["connected", "active", "true", "1", ""] or not r.get("status")
|
| 140 |
+
if p_key and is_active:
|
| 141 |
+
found[p_key] = r
|
| 142 |
+
|
| 143 |
+
print(f"- RealtimeFetcher: Found connected DB providers for tenant '{tenant_id}': {list(found.keys())}")
|
| 144 |
+
return found
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def fetch_realtime_ga4(connection: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
|
| 148 |
+
"""Fetch live traffic data from GA4 Data API with automatic 401 token refresh."""
|
| 149 |
+
if not connection:
|
| 150 |
+
print("- RealtimeFetcher GA4: No connection record found for GA4 in DB")
|
| 151 |
+
return {
|
| 152 |
+
"activeUsers": 0,
|
| 153 |
+
"totalSessions": 0,
|
| 154 |
+
"bounceRate": 0.0,
|
| 155 |
+
"trafficSources": [],
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
access_token = connection.get("accessToken") or connection.get("access_token")
|
| 159 |
+
refresh_token = connection.get("refreshToken") or connection.get("refresh_token")
|
| 160 |
+
connection_id = connection.get("id", "")
|
| 161 |
+
|
| 162 |
+
metadata = connection.get("metadata") or {}
|
| 163 |
+
if isinstance(metadata, str):
|
| 164 |
+
import json
|
| 165 |
+
try:
|
| 166 |
+
metadata = json.loads(metadata)
|
| 167 |
+
except Exception:
|
| 168 |
+
metadata = {}
|
| 169 |
+
elif not isinstance(metadata, dict):
|
| 170 |
+
metadata = {}
|
| 171 |
+
|
| 172 |
+
property_id = metadata.get("propertyId") or metadata.get("property_id") or connection.get("externalId") or connection.get("external_id")
|
| 173 |
+
|
| 174 |
+
print(f"- RealtimeFetcher GA4: accessToken={'PRESENT' if access_token else 'MISSING'}, propertyId='{property_id}'")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
if not access_token or not property_id or str(property_id).startswith("external_"):
|
| 178 |
+
return {
|
| 179 |
+
"activeUsers": 0,
|
| 180 |
+
"totalSessions": 0,
|
| 181 |
+
"bounceRate": 0.0,
|
| 182 |
+
"trafficSources": [],
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport"
|
| 186 |
+
headers = {
|
| 187 |
+
"Authorization": f"Bearer {access_token}",
|
| 188 |
+
"Content-Type": "application/json",
|
| 189 |
+
}
|
| 190 |
+
body = {
|
| 191 |
+
"dateRanges": [{"startDate": "30daysAgo", "endDate": "yesterday"}],
|
| 192 |
+
"metrics": [
|
| 193 |
+
{"name": "activeUsers"},
|
| 194 |
+
{"name": "sessions"},
|
| 195 |
+
{"name": "bounceRate"},
|
| 196 |
+
],
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
response = httpx.post(url, headers=headers, json=body, timeout=6.0)
|
| 201 |
+
|
| 202 |
+
# Handle 401 Token Expiration automatically using refreshToken
|
| 203 |
+
if response.status_code == 401 and refresh_token:
|
| 204 |
+
new_token = refresh_google_access_token(refresh_token, connection_id=connection_id)
|
| 205 |
+
if new_token:
|
| 206 |
+
headers["Authorization"] = f"Bearer {new_token}"
|
| 207 |
+
response = httpx.post(url, headers=headers, json=body, timeout=6.0)
|
| 208 |
+
|
| 209 |
+
if response.status_code == 200:
|
| 210 |
+
res_json = response.json()
|
| 211 |
+
rows = res_json.get("rows", [])
|
| 212 |
+
if rows:
|
| 213 |
+
metric_vals = rows[0].get("metricValues", [])
|
| 214 |
+
active_users = int(metric_vals[0].get("value", 0)) if len(metric_vals) > 0 else 0
|
| 215 |
+
sessions = int(metric_vals[1].get("value", 0)) if len(metric_vals) > 1 else 0
|
| 216 |
+
bounce_rate = float(metric_vals[2].get("value", 0.0)) if len(metric_vals) > 2 else 0.0
|
| 217 |
+
|
| 218 |
+
print(f"- RealtimeFetcher GA4: API Success -> activeUsers={active_users}, sessions={sessions}")
|
| 219 |
+
return {
|
| 220 |
+
"activeUsers": active_users,
|
| 221 |
+
"totalSessions": sessions,
|
| 222 |
+
"bounceRate": round(bounce_rate * 100, 2) if bounce_rate <= 1.0 else round(bounce_rate, 2),
|
| 223 |
+
"trafficSources": ["Google Organic", "Direct", "Social"],
|
| 224 |
+
}
|
| 225 |
+
else:
|
| 226 |
+
print("- RealtimeFetcher GA4: API returned 0 rows (no activity in date range)")
|
| 227 |
+
else:
|
| 228 |
+
print(f"- RealtimeFetcher GA4: API error HTTP {response.status_code}: {response.text}")
|
| 229 |
+
except Exception as e:
|
| 230 |
+
print(f"- RealtimeFetcher: GA4 API Error: {e}")
|
| 231 |
+
|
| 232 |
+
return {
|
| 233 |
+
"activeUsers": 0,
|
| 234 |
+
"totalSessions": 0,
|
| 235 |
+
"bounceRate": 0.0,
|
| 236 |
+
"trafficSources": [],
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def fetch_realtime_shopify(connection: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
|
| 241 |
+
"""Fetch live sales data from Shopify Admin API."""
|
| 242 |
+
if not connection:
|
| 243 |
+
return {
|
| 244 |
+
"totalRevenue": 0.0,
|
| 245 |
+
"totalOrders": 0,
|
| 246 |
+
"avgOrderValue": 0.0,
|
| 247 |
+
"funnel": {"sessions": 0, "checkouts": 0, "purchases": 0},
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
access_token = connection.get("accessToken") or connection.get("access_token")
|
| 251 |
+
metadata = connection.get("metadata") or {}
|
| 252 |
+
shop_domain = metadata.get("shop") or metadata.get("myshopifyDomain") or metadata.get("shop_domain")
|
| 253 |
+
|
| 254 |
+
if not access_token or not shop_domain:
|
| 255 |
+
return {
|
| 256 |
+
"totalRevenue": 0.0,
|
| 257 |
+
"totalOrders": 0,
|
| 258 |
+
"avgOrderValue": 0.0,
|
| 259 |
+
"funnel": {"sessions": 0, "checkouts": 0, "purchases": 0},
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
clean_shop = shop_domain.replace("https://", "").replace("http://", "").strip("/")
|
| 263 |
+
if not clean_shop.endswith(".myshopify.com"):
|
| 264 |
+
clean_shop = f"{clean_shop}.myshopify.com"
|
| 265 |
+
|
| 266 |
+
url = f"https://{clean_shop}/admin/api/2024-01/orders.json?limit=250&status=any"
|
| 267 |
+
headers = {
|
| 268 |
+
"X-Shopify-Access-Token": access_token,
|
| 269 |
+
"Content-Type": "application/json",
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
response = httpx.get(url, headers=headers, timeout=6.0)
|
| 274 |
+
if response.status_code == 200:
|
| 275 |
+
orders = response.json().get("orders", [])
|
| 276 |
+
total_revenue = sum(float(o.get("total_price", 0.0)) for o in orders)
|
| 277 |
+
total_orders = len(orders)
|
| 278 |
+
aov = round(total_revenue / total_orders, 2) if total_orders > 0 else 0.0
|
| 279 |
+
|
| 280 |
+
return {
|
| 281 |
+
"totalRevenue": round(total_revenue, 2),
|
| 282 |
+
"totalOrders": total_orders,
|
| 283 |
+
"avgOrderValue": aov,
|
| 284 |
+
"funnel": {
|
| 285 |
+
"sessions": total_orders * 4,
|
| 286 |
+
"checkouts": total_orders * 2,
|
| 287 |
+
"purchases": total_orders,
|
| 288 |
+
},
|
| 289 |
+
}
|
| 290 |
+
except Exception as e:
|
| 291 |
+
print(f"- RealtimeFetcher: Shopify API Error: {e}")
|
| 292 |
+
|
| 293 |
+
return {
|
| 294 |
+
"totalRevenue": 0.0,
|
| 295 |
+
"totalOrders": 0,
|
| 296 |
+
"avgOrderValue": 0.0,
|
| 297 |
+
"funnel": {
|
| 298 |
+
"sessions": 0,
|
| 299 |
+
"checkouts": 0,
|
| 300 |
+
"purchases": 0,
|
| 301 |
+
},
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def fetch_realtime_meta(connection: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
|
| 306 |
+
"""Fetch live ad campaign insights from Meta Ads Graph API."""
|
| 307 |
+
if not connection:
|
| 308 |
+
return {
|
| 309 |
+
"spend": 0.0,
|
| 310 |
+
"impressions": 0,
|
| 311 |
+
"clicks": 0,
|
| 312 |
+
"ctr": 0.0,
|
| 313 |
+
"cpc": 0.0,
|
| 314 |
+
"roas": 0.0,
|
| 315 |
+
"campaigns": [],
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
access_token = connection.get("accessToken") or connection.get("access_token")
|
| 319 |
+
ad_account_id = connection.get("externalId") or connection.get("external_id")
|
| 320 |
+
|
| 321 |
+
if not access_token or not ad_account_id:
|
| 322 |
+
return {
|
| 323 |
+
"spend": 0.0,
|
| 324 |
+
"impressions": 0,
|
| 325 |
+
"clicks": 0,
|
| 326 |
+
"ctr": 0.0,
|
| 327 |
+
"cpc": 0.0,
|
| 328 |
+
"roas": 0.0,
|
| 329 |
+
"campaigns": [],
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
clean_account_id = ad_account_id if ad_account_id.startswith("act_") else f"act_{ad_account_id}"
|
| 333 |
+
url = f"https://graph.facebook.com/v19.0/{clean_account_id}/insights"
|
| 334 |
+
params = {
|
| 335 |
+
"access_token": access_token,
|
| 336 |
+
"date_preset": "last_30d",
|
| 337 |
+
"fields": "spend,impressions,clicks,ctr,cpc,purchase_roas",
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
try:
|
| 341 |
+
response = httpx.get(url, params=params, timeout=6.0)
|
| 342 |
+
if response.status_code == 200:
|
| 343 |
+
data = response.json().get("data", [])
|
| 344 |
+
if data:
|
| 345 |
+
row = data[0]
|
| 346 |
+
spend = float(row.get("spend", 0.0))
|
| 347 |
+
impressions = int(row.get("impressions", 0))
|
| 348 |
+
clicks = int(row.get("clicks", 0))
|
| 349 |
+
ctr = float(row.get("ctr", 0.0))
|
| 350 |
+
cpc = float(row.get("cpc", 0.0))
|
| 351 |
+
|
| 352 |
+
roas_list = row.get("purchase_roas", [])
|
| 353 |
+
roas = float(roas_list[0].get("value", 0.0)) if roas_list else 0.0
|
| 354 |
+
|
| 355 |
+
return {
|
| 356 |
+
"spend": round(spend, 2),
|
| 357 |
+
"impressions": impressions,
|
| 358 |
+
"clicks": clicks,
|
| 359 |
+
"ctr": round(ctr, 2),
|
| 360 |
+
"cpc": round(cpc, 2),
|
| 361 |
+
"roas": round(roas, 2),
|
| 362 |
+
"campaigns": [],
|
| 363 |
+
}
|
| 364 |
+
except Exception as e:
|
| 365 |
+
print(f"- RealtimeFetcher: Meta Ads API Error: {e}")
|
| 366 |
+
|
| 367 |
+
return {
|
| 368 |
+
"spend": 0.0,
|
| 369 |
+
"impressions": 0,
|
| 370 |
+
"clicks": 0,
|
| 371 |
+
"ctr": 0.0,
|
| 372 |
+
"cpc": 0.0,
|
| 373 |
+
"roas": 0.0,
|
| 374 |
+
"campaigns": [],
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def enrich_payload_with_realtime_data(
|
| 379 |
+
ga4_data: Optional[dict[str, Any]],
|
| 380 |
+
shopify_data: Optional[dict[str, Any]],
|
| 381 |
+
meta_data: Optional[dict[str, Any]],
|
| 382 |
+
tenant_id: str,
|
| 383 |
+
user_id: str = "",
|
| 384 |
+
) -> tuple[Optional[dict[str, Any]], Optional[dict[str, Any]], Optional[dict[str, Any]]]:
|
| 385 |
+
"""
|
| 386 |
+
Enrich data fields by verifying user access and fetching live metrics from APIs.
|
| 387 |
+
"""
|
| 388 |
+
if user_id and tenant_id:
|
| 389 |
+
has_access = verify_user_tenant_access(user_id, tenant_id)
|
| 390 |
+
if not has_access:
|
| 391 |
+
print(f"- RealtimeFetcher: Access denied for user '{user_id}' on tenant '{tenant_id}'")
|
| 392 |
+
return None, None, None
|
| 393 |
+
|
| 394 |
+
connections = {}
|
| 395 |
+
needs_fetch = (ga4_data is None) or (shopify_data is None) or (meta_data is None)
|
| 396 |
+
|
| 397 |
+
if needs_fetch and tenant_id:
|
| 398 |
+
connections = fetch_tenant_connections(tenant_id, user_id=user_id)
|
| 399 |
+
|
| 400 |
+
res_ga4 = ga4_data if ga4_data is not None else fetch_realtime_ga4(connections.get("ga4"))
|
| 401 |
+
res_shopify = shopify_data if shopify_data is not None else fetch_realtime_shopify(connections.get("shopify"))
|
| 402 |
+
res_meta = meta_data if meta_data is not None else fetch_realtime_meta(connections.get("meta"))
|
| 403 |
+
|
| 404 |
+
return res_ga4, res_shopify, res_meta
|
SuperAgent/app/services/report_exporter.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Report Exporter Service — PDF and Excel Document Generation.
|
| 3 |
+
Generates:
|
| 4 |
+
1. Executive PDF Report: Contains High-Level Summary & Performance Visual Graph (No raw metric dumps).
|
| 5 |
+
2. Excel / CSV Report: Contains full detailed metric values, campaign breakdowns, and raw numbers.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
REPORTS_DIR = Path(__file__).resolve().parents[3] / "reports"
|
| 16 |
+
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _generate_performance_graph(growth: dict[str, Any], marketing: dict[str, Any], chart_path: Path) -> bool:
|
| 20 |
+
"""Generate a clean visual performance graph (Ad Spend vs Revenue vs ROAS)."""
|
| 21 |
+
try:
|
| 22 |
+
import matplotlib
|
| 23 |
+
matplotlib.use('Agg')
|
| 24 |
+
import matplotlib.pyplot as plt
|
| 25 |
+
|
| 26 |
+
ad_spend = marketing.get("ad_spend", 0.0) or 1000.0
|
| 27 |
+
revenue = (growth.get("aov", 50.0) or 50.0) * 100.0
|
| 28 |
+
roas = (marketing.get("overall_roas", 2.5) or 2.5) * 1000.0
|
| 29 |
+
|
| 30 |
+
categories = ['Ad Spend ($)', 'Est Revenue ($)', 'ROAS Index']
|
| 31 |
+
values = [ad_spend, revenue, roas]
|
| 32 |
+
colors_list = ['#3B82F6', '#10B981', '#6366F1']
|
| 33 |
+
|
| 34 |
+
plt.figure(figsize=(6, 3.2))
|
| 35 |
+
bars = plt.bar(categories, values, color=colors_list, width=0.5)
|
| 36 |
+
plt.title('Performance & Channel Health Overview', fontsize=11, fontweight='bold', pad=10)
|
| 37 |
+
plt.ylabel('Value', fontsize=9)
|
| 38 |
+
plt.grid(axis='y', linestyle='--', alpha=0.5)
|
| 39 |
+
|
| 40 |
+
for bar in bars:
|
| 41 |
+
height = bar.get_height()
|
| 42 |
+
plt.text(
|
| 43 |
+
bar.get_x() + bar.get_width() / 2.0,
|
| 44 |
+
height + (height * 0.02),
|
| 45 |
+
f'{height:,.0f}',
|
| 46 |
+
ha='center',
|
| 47 |
+
va='bottom',
|
| 48 |
+
fontsize=8,
|
| 49 |
+
fontweight='bold'
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
plt.tight_layout()
|
| 53 |
+
plt.savefig(str(chart_path), dpi=150, bbox_inches='tight')
|
| 54 |
+
plt.close()
|
| 55 |
+
return True
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"Report Exporter: Matplotlib graph generation fallback ({e})")
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def generate_pdf_report(report_payload: dict[str, Any], tenant_id: str = "tenant") -> str:
|
| 62 |
+
"""
|
| 63 |
+
Generate C-Suite Executive PDF report.
|
| 64 |
+
Supports platform-specific reports (Meta Ads only, GA4 only, Shopify only) or combined reports.
|
| 65 |
+
Contains ONLY Executive Summary + Performance Visual Graph (no raw metric dumps).
|
| 66 |
+
"""
|
| 67 |
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 68 |
+
platform_filter = report_payload.get("platform_filter", "all")
|
| 69 |
+
|
| 70 |
+
if platform_filter == "meta":
|
| 71 |
+
title_text = "ScaleMate AI Meta Ads Performance Report"
|
| 72 |
+
filename = f"ScaleMate_Meta_Ads_Report_{tenant_id}_{timestamp}.pdf"
|
| 73 |
+
elif platform_filter == "ga4":
|
| 74 |
+
title_text = "ScaleMate AI GA4 Analytics Performance Report"
|
| 75 |
+
filename = f"ScaleMate_GA4_Analytics_Report_{tenant_id}_{timestamp}.pdf"
|
| 76 |
+
elif platform_filter == "shopify":
|
| 77 |
+
title_text = "ScaleMate AI Shopify Store Performance Report"
|
| 78 |
+
filename = f"ScaleMate_Shopify_Store_Report_{tenant_id}_{timestamp}.pdf"
|
| 79 |
+
else:
|
| 80 |
+
title_text = "ScaleMate AI Executive Growth Summary"
|
| 81 |
+
filename = f"ScaleMate_Executive_Report_{tenant_id}_{timestamp}.pdf"
|
| 82 |
+
|
| 83 |
+
filepath = REPORTS_DIR / filename
|
| 84 |
+
chart_path = REPORTS_DIR / f"chart_{tenant_id}_{timestamp}.png"
|
| 85 |
+
|
| 86 |
+
growth = report_payload.get("growth_result") or {}
|
| 87 |
+
marketing = report_payload.get("marketing_result") or {}
|
| 88 |
+
tf_label = report_payload.get("timeframe_label", "Last 1 Hour (Default Window)")
|
| 89 |
+
|
| 90 |
+
has_graph = _generate_performance_graph(growth, marketing, chart_path)
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
from reportlab.lib.pagesizes import letter
|
| 94 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
|
| 95 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 96 |
+
from reportlab.lib import colors
|
| 97 |
+
|
| 98 |
+
doc = SimpleDocTemplate(str(filepath), pagesize=letter)
|
| 99 |
+
styles = getSampleStyleSheet()
|
| 100 |
+
|
| 101 |
+
title_style = ParagraphStyle(
|
| 102 |
+
'ReportTitle',
|
| 103 |
+
parent=styles['Heading1'],
|
| 104 |
+
fontSize=20,
|
| 105 |
+
textColor=colors.HexColor('#4F46E5'),
|
| 106 |
+
spaceAfter=8
|
| 107 |
+
)
|
| 108 |
+
heading_style = ParagraphStyle(
|
| 109 |
+
'ReportHeading',
|
| 110 |
+
parent=styles['Heading2'],
|
| 111 |
+
fontSize=13,
|
| 112 |
+
textColor=colors.HexColor('#1E293B'),
|
| 113 |
+
spaceBefore=10,
|
| 114 |
+
spaceAfter=6
|
| 115 |
+
)
|
| 116 |
+
body_style = ParagraphStyle(
|
| 117 |
+
'ReportBody',
|
| 118 |
+
parent=styles['Normal'],
|
| 119 |
+
fontSize=10,
|
| 120 |
+
leading=14,
|
| 121 |
+
textColor=colors.HexColor('#334155'),
|
| 122 |
+
spaceAfter=8
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
elements = [
|
| 126 |
+
Paragraph("ScaleMate AI Executive Growth Summary", title_style),
|
| 127 |
+
Paragraph(f"Official C-Suite Briefing — Generated on {time.strftime('%B %d, %Y')}", styles['Italic']),
|
| 128 |
+
Spacer(1, 10),
|
| 129 |
+
Paragraph("Executive Narrative & Business Health", heading_style),
|
| 130 |
+
Paragraph(
|
| 131 |
+
"Cross-channel data analysis indicates positive top-of-funnel ad performance coupled with stable "
|
| 132 |
+
"e-commerce customer acquisition. Marketing campaign efficiency remains healthy, while customer retention "
|
| 133 |
+
"metrics highlight strong lifetime value opportunities across core product offerings.",
|
| 134 |
+
body_style
|
| 135 |
+
),
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
if has_graph and chart_path.exists():
|
| 139 |
+
elements.append(Paragraph("Channel Performance Visual Graph", heading_style))
|
| 140 |
+
elements.append(Image(str(chart_path), width=420, height=220))
|
| 141 |
+
elements.append(Spacer(1, 10))
|
| 142 |
+
|
| 143 |
+
elements.extend([
|
| 144 |
+
Paragraph("Strategic Roadmap & High-ROI Next Steps", heading_style),
|
| 145 |
+
Paragraph("1. Scale top-performing prospecting ad campaigns while pruning low ROAS channels.", body_style),
|
| 146 |
+
Paragraph("2. Optimize checkout funnel friction points to push overall conversion efficiency above threshold.", body_style),
|
| 147 |
+
Paragraph("3. Expand high-margin product bundles to boost Average Order Value (AOV).", body_style),
|
| 148 |
+
])
|
| 149 |
+
|
| 150 |
+
doc.build(elements)
|
| 151 |
+
|
| 152 |
+
if chart_path.exists():
|
| 153 |
+
try:
|
| 154 |
+
os.remove(chart_path)
|
| 155 |
+
except Exception:
|
| 156 |
+
pass
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"Report Exporter PDF Fallback: {e}")
|
| 159 |
+
with open(filepath, "wb") as f:
|
| 160 |
+
f.write(
|
| 161 |
+
f"%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
|
| 162 |
+
f"2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj\n"
|
| 163 |
+
f"3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R/Resources<<>>>>endobj\n"
|
| 164 |
+
f"xref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000052 00000 n\n0000000101 00000 n\n"
|
| 165 |
+
f"trailer<</Size 4/Root 1 0 R>>\nstartxref\n178\n%%EOF".encode("utf-8")
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
return filename
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def generate_excel_report(report_payload: dict[str, Any], tenant_id: str = "tenant") -> str:
|
| 172 |
+
"""
|
| 173 |
+
Generate Excel / CSV Metrics Spreadsheet.
|
| 174 |
+
Contains full detailed metric values, campaign breakdowns, and raw data numbers.
|
| 175 |
+
"""
|
| 176 |
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 177 |
+
filename = f"ScaleMate_Performance_Metrics_{tenant_id}_{timestamp}.csv"
|
| 178 |
+
filepath = REPORTS_DIR / filename
|
| 179 |
+
|
| 180 |
+
growth = report_payload.get("growth_result") or {}
|
| 181 |
+
marketing = report_payload.get("marketing_result") or {}
|
| 182 |
+
|
| 183 |
+
rows = [
|
| 184 |
+
["Channel / Category", "Metric Name", "Value", "Unit / Context", "Notes"],
|
| 185 |
+
["Meta Ads", "Total Ad Spend", f"${marketing.get('ad_spend', 0.0):,.2f}", "USD", "Combined active ad spend"],
|
| 186 |
+
["Meta Ads", "Overall ROAS", f"{marketing.get('overall_roas', 0.0):.2f}x", "Multiplier", "Return on ad spend"],
|
| 187 |
+
["Meta Ads", "Customer Acquisition Cost (CAC)", f"${marketing.get('cac', 0.0):.2f}", "USD", "Blended CAC"],
|
| 188 |
+
["Meta Ads", "Cost Per Lead (CPL)", f"${marketing.get('cost_per_lead', 0.0):.2f}", "USD", "Lead generation CPL"],
|
| 189 |
+
["Shopify", "Average Order Value (AOV)", f"${growth.get('aov', 0.0):.2f}", "USD", "Store average order size"],
|
| 190 |
+
["Shopify", "Checkout Conversion Rate", f"{growth.get('checkout_conversion_rate', 0.0):.2f}%", "Percentage", "Checkout completion rate"],
|
| 191 |
+
["Shopify", "Customer Lifetime Value (LTV)", f"${growth.get('ltv', 0.0):.2f}", "USD", "Estimated customer LTV"],
|
| 192 |
+
["Google Analytics 4", "Traffic Summary", f'"{growth.get("traffic_summary", "N/A")}"', "Text", "GA4 sessions & active users"],
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
# Add campaign-level rows if available
|
| 196 |
+
high_perf = marketing.get("high_performers", [])
|
| 197 |
+
wasted = marketing.get("wasted_spend_campaigns", [])
|
| 198 |
+
|
| 199 |
+
for c in high_perf:
|
| 200 |
+
c_name = c.get("name", "Campaign") if isinstance(c, dict) else str(c)
|
| 201 |
+
c_spend = c.get("spend", 0.0) if isinstance(c, dict) else 0.0
|
| 202 |
+
c_roas = c.get("roas", 0.0) if isinstance(c, dict) else 0.0
|
| 203 |
+
rows.append(["Meta Campaign", f"High Performer: {c_name}", f"ROAS {c_roas:.2f}x", f"${c_spend:,.2f} Spend", "Recommended for scaling"])
|
| 204 |
+
|
| 205 |
+
for w in wasted:
|
| 206 |
+
w_name = w.get("name", "Campaign") if isinstance(w, dict) else str(w)
|
| 207 |
+
w_spend = w.get("spend", 0.0) if isinstance(w, dict) else 0.0
|
| 208 |
+
w_roas = w.get("roas", 0.0) if isinstance(w, dict) else 0.0
|
| 209 |
+
rows.append(["Meta Campaign", f"Wasted Spend: {w_name}", f"ROAS {w_roas:.2f}x", f"${w_spend:,.2f} Spend", "Recommended for pausing"])
|
| 210 |
+
|
| 211 |
+
content = "\n".join([",".join(row) for row in rows])
|
| 212 |
+
|
| 213 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
| 214 |
+
f.write(content)
|
| 215 |
+
|
| 216 |
+
return filename
|
SuperAgent/app/services/router.py
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ScaleMate AI SuperAgent Router — Central Executive Orchestrator.
|
| 3 |
+
Built with LangGraph. Integrates 3-Layer Knowledge Cache, MCP Server Tool Registry,
|
| 4 |
+
Intent Classifier, Growth Agent, Marketing Agent, Report Agent, and Groq LLM Executive Synthesizer.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
from typing import Any, Optional, TypedDict
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
from langgraph.graph import END, StateGraph
|
| 16 |
+
from langchain_groq import ChatGroq
|
| 17 |
+
|
| 18 |
+
from ..models.schemas import ErrorResponse, SuperAgentRequest, SuperAgentResponse
|
| 19 |
+
from .asset_registry import (
|
| 20 |
+
build_growth_payload,
|
| 21 |
+
build_marketing_payload,
|
| 22 |
+
classify_intent,
|
| 23 |
+
resolve_counter_question,
|
| 24 |
+
)
|
| 25 |
+
from .knowledge_cache_service import load_user_assets
|
| 26 |
+
from .email_service import send_report_email
|
| 27 |
+
from .report_exporter import generate_pdf_report, generate_excel_report
|
| 28 |
+
from ..mcp.mcp_server import mcp_registry
|
| 29 |
+
from backend.agents.growth.app.main import process_growth
|
| 30 |
+
from backend.agents.marketing.app.main import process_marketing
|
| 31 |
+
from ..agents.report_agent import generate_executive_report
|
| 32 |
+
|
| 33 |
+
# Explicitly load root .env file
|
| 34 |
+
root_env = Path(__file__).resolve().parents[3] / ".env"
|
| 35 |
+
if root_env.exists():
|
| 36 |
+
load_dotenv(dotenv_path=root_env)
|
| 37 |
+
else:
|
| 38 |
+
load_dotenv()
|
| 39 |
+
|
| 40 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
| 41 |
+
GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
|
| 42 |
+
|
| 43 |
+
llm = ChatGroq(
|
| 44 |
+
model=GROQ_MODEL,
|
| 45 |
+
groq_api_key=GROQ_API_KEY,
|
| 46 |
+
temperature=0.3,
|
| 47 |
+
) if GROQ_API_KEY else None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class AgentState(TypedDict):
|
| 51 |
+
user_query: str
|
| 52 |
+
user_id: str
|
| 53 |
+
tenant_id: str
|
| 54 |
+
session_id: str
|
| 55 |
+
intent: str
|
| 56 |
+
counter_question_prompt: Optional[str]
|
| 57 |
+
ga4_data: Optional[dict[str, Any]]
|
| 58 |
+
shopify_data: Optional[dict[str, Any]]
|
| 59 |
+
meta_data: Optional[dict[str, Any]]
|
| 60 |
+
leads_data: Optional[dict[str, Any]]
|
| 61 |
+
chat_history: list[dict[str, Any]]
|
| 62 |
+
growth_result: Optional[dict[str, Any]]
|
| 63 |
+
marketing_result: Optional[dict[str, Any]]
|
| 64 |
+
report_result: Optional[dict[str, Any]]
|
| 65 |
+
final_answer: str
|
| 66 |
+
orchestration_steps: list[str]
|
| 67 |
+
email_alert_triggered: bool
|
| 68 |
+
email_type: Optional[str]
|
| 69 |
+
error: Optional[str]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def intent_classifier_node(state: AgentState) -> AgentState:
|
| 73 |
+
"""Step 1: Check 3-Layer Knowledge Cache Access Gate & Classify User Intent."""
|
| 74 |
+
steps = list(state.get("orchestration_steps", []))
|
| 75 |
+
|
| 76 |
+
u_id = state.get("user_id", "")
|
| 77 |
+
t_id = state.get("tenant_id", "")
|
| 78 |
+
if u_id:
|
| 79 |
+
steps.append(f"Gateway: Parsed User_ID='{u_id}'")
|
| 80 |
+
print(f"- Gateway: Parsed User_ID='{u_id}'")
|
| 81 |
+
|
| 82 |
+
# 3-Layer Knowledge Cache Access Check (0ms latency for RAM / Disk hits)
|
| 83 |
+
cache_data = load_user_assets(t_id, u_id)
|
| 84 |
+
if not cache_data.get("user_authorized", True):
|
| 85 |
+
state["intent"] = "access_denied"
|
| 86 |
+
steps.append(f"Gateway: Access DENIED for User_ID='{u_id}' on Tenant='{t_id}'")
|
| 87 |
+
state["orchestration_steps"] = steps
|
| 88 |
+
return state
|
| 89 |
+
|
| 90 |
+
user_query = state["user_query"]
|
| 91 |
+
print(f"- Gateway: User Query='{user_query}'")
|
| 92 |
+
steps.append(f"- Gateway: User Query='{user_query}'")
|
| 93 |
+
|
| 94 |
+
intent_name, counter_prompt = classify_intent(
|
| 95 |
+
user_query=user_query,
|
| 96 |
+
session_id=state["session_id"],
|
| 97 |
+
llm=llm,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
state["intent"] = intent_name
|
| 101 |
+
state["counter_question_prompt"] = counter_prompt
|
| 102 |
+
|
| 103 |
+
print(f"- Gateway: Intent classified as '{intent_name}'")
|
| 104 |
+
steps.append(f"Gateway: Intent classified as '{intent_name}'")
|
| 105 |
+
|
| 106 |
+
state["orchestration_steps"] = steps
|
| 107 |
+
return state
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def access_denied_node(state: AgentState) -> AgentState:
|
| 111 |
+
"""Return access denied response when 0ms cache check fails."""
|
| 112 |
+
steps = list(state.get("orchestration_steps", []))
|
| 113 |
+
steps.append("Gateway: Short-circuiting to Access Denied response")
|
| 114 |
+
state["final_answer"] = "Access Denied: You do not have permissions to access analytics for this tenant."
|
| 115 |
+
state["orchestration_steps"] = steps
|
| 116 |
+
return state
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def counter_question_node(state: AgentState) -> AgentState:
|
| 120 |
+
"""Return clarification prompt when counter-question is triggered."""
|
| 121 |
+
steps = list(state.get("orchestration_steps", []))
|
| 122 |
+
prompt = state.get("counter_question_prompt") or "Could you specify which metrics or platforms you would like to analyze?"
|
| 123 |
+
print("- Gateway: Counter-Question triggered, returning clarification prompt")
|
| 124 |
+
steps.append("Gateway: Counter-Question triggered, returning clarification prompt")
|
| 125 |
+
state["final_answer"] = prompt
|
| 126 |
+
state["orchestration_steps"] = steps
|
| 127 |
+
return state
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def greeting_responder_node(state: AgentState) -> AgentState:
|
| 131 |
+
"""Respond directly to greetings without invoking sub-agents."""
|
| 132 |
+
steps = list(state.get("orchestration_steps", []))
|
| 133 |
+
print("- Gateway: Greeting detected, responding directly")
|
| 134 |
+
steps.append("Gateway: Greeting detected, responding directly")
|
| 135 |
+
|
| 136 |
+
greetings_responses = [
|
| 137 |
+
"Hello! I am your ScaleMate AI Assistant. How can I help you analyze your business performance today?",
|
| 138 |
+
"Hi there! Ready to review your traffic, store sales, or marketing ad performance?",
|
| 139 |
+
"Greetings! Ask me any question about your GA4 traffic, Shopify sales, or Meta ad campaigns.",
|
| 140 |
+
]
|
| 141 |
+
import random
|
| 142 |
+
state["final_answer"] = random.choice(greetings_responses)
|
| 143 |
+
steps.append("Greeting Agent: Response sent successfully")
|
| 144 |
+
state["orchestration_steps"] = steps
|
| 145 |
+
return state
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def growth_agent_node(state: AgentState) -> AgentState:
|
| 149 |
+
"""Execute Growth Agent using MCP Server Tools for GA4 and Shopify data fetching."""
|
| 150 |
+
steps = list(state.get("orchestration_steps", []))
|
| 151 |
+
print("- Gateway -> Growth Agent: Requesting metrics via MCP Server Tools...")
|
| 152 |
+
steps.append("Gateway -> Growth Agent: Requesting metrics via MCP Server Tools...")
|
| 153 |
+
|
| 154 |
+
t_id = state.get("tenant_id", "")
|
| 155 |
+
|
| 156 |
+
# Fetch metrics via standardized MCP Server Tools
|
| 157 |
+
res_ga4 = state.get("ga4_data") or mcp_registry.call_tool("ga4_get_traffic_metrics", tenant_id=t_id)
|
| 158 |
+
res_shopify = state.get("shopify_data") or mcp_registry.call_tool("shopify_get_sales_metrics", tenant_id=t_id)
|
| 159 |
+
|
| 160 |
+
payload = build_growth_payload(
|
| 161 |
+
ga4_data=res_ga4,
|
| 162 |
+
shopify_data=res_shopify,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
result = process_growth(payload)
|
| 166 |
+
state["growth_result"] = result
|
| 167 |
+
|
| 168 |
+
print("- Growth Agent: Analytics processed successfully via MCP Server")
|
| 169 |
+
steps.append("Growth Agent: Analytics processed successfully via MCP Server")
|
| 170 |
+
|
| 171 |
+
state["orchestration_steps"] = steps
|
| 172 |
+
return state
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def marketing_agent_node(state: AgentState) -> AgentState:
|
| 176 |
+
"""Execute Marketing Agent using MCP Server Tools for Meta Ads data fetching."""
|
| 177 |
+
steps = list(state.get("orchestration_steps", []))
|
| 178 |
+
print("- Gateway -> Marketing Agent: Requesting ad metrics via MCP Server Tools...")
|
| 179 |
+
steps.append("Gateway -> Marketing Agent: Requesting ad metrics via MCP Server Tools...")
|
| 180 |
+
|
| 181 |
+
t_id = state.get("tenant_id", "")
|
| 182 |
+
|
| 183 |
+
# Fetch metrics via standardized MCP Server Tools
|
| 184 |
+
res_meta = state.get("meta_data") or mcp_registry.call_tool("meta_get_ad_insights", tenant_id=t_id)
|
| 185 |
+
|
| 186 |
+
payload = build_marketing_payload(
|
| 187 |
+
meta_data=res_meta,
|
| 188 |
+
leads_data=state.get("leads_data"),
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
result = process_marketing(payload)
|
| 192 |
+
state["marketing_result"] = result
|
| 193 |
+
|
| 194 |
+
if result.get("email_triggered"):
|
| 195 |
+
state["email_alert_triggered"] = True
|
| 196 |
+
state["email_type"] = result.get("email_alert_type", "MARKETING_ALERT")
|
| 197 |
+
steps.append(f"Marketing Agent: Automated email alert triggered ({state['email_type']})")
|
| 198 |
+
|
| 199 |
+
print("- Marketing Agent: Ad insights processed successfully via MCP Server")
|
| 200 |
+
steps.append("Marketing Agent: Ad insights processed successfully via MCP Server")
|
| 201 |
+
|
| 202 |
+
state["orchestration_steps"] = steps
|
| 203 |
+
return state
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def extract_timeframe_from_query(query: str) -> dict[str, str]:
|
| 207 |
+
"""
|
| 208 |
+
Extract timeframe window from user query.
|
| 209 |
+
If no timeframe is mentioned (e.g. 'generate report'), defaults to 'last 1 hour'.
|
| 210 |
+
"""
|
| 211 |
+
q = (query or "").lower()
|
| 212 |
+
if any(kw in q for kw in ["week", "7 day", "7-day", "7d"]):
|
| 213 |
+
return {"code": "last_1_week", "label": "Last 1 Week (7 Days)"}
|
| 214 |
+
elif any(kw in q for kw in ["today", "24 hour", "24hr", "24-hour", "day"]):
|
| 215 |
+
return {"code": "last_24_hours", "label": "Last 24 Hours"}
|
| 216 |
+
elif any(kw in q for kw in ["month", "30 day", "30-day", "30d"]):
|
| 217 |
+
return {"code": "last_1_month", "label": "Last 30 Days"}
|
| 218 |
+
elif any(kw in q for kw in ["hour", "1hr", "60 min", "1-hour"]):
|
| 219 |
+
return {"code": "last_1_hour", "label": "Last 1 Hour"}
|
| 220 |
+
else:
|
| 221 |
+
# Default timeframe when unmentioned by user
|
| 222 |
+
return {"code": "last_1_hour", "label": "Last 1 Hour (Default Window)"}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def extract_platform_filter_from_query(query: str) -> str:
|
| 226 |
+
"""Detect if user query specifically requests a single platform report (Meta Ads, GA4, or Shopify)."""
|
| 227 |
+
q = (query or "").lower()
|
| 228 |
+
if any(kw in q for kw in ["meta", "facebook", "fb ads", "instagram"]):
|
| 229 |
+
return "meta"
|
| 230 |
+
elif any(kw in q for kw in ["ga4", "google analytics", "traffic"]):
|
| 231 |
+
return "ga4"
|
| 232 |
+
elif any(kw in q for kw in ["shopify", "store", "sales"]):
|
| 233 |
+
return "shopify"
|
| 234 |
+
return "all"
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def extract_format_preference_from_query(query: str) -> str:
|
| 238 |
+
"""
|
| 239 |
+
Detect requested report format:
|
| 240 |
+
- 'pdf': User explicitly requested PDF format, OR simple report request (Default is PDF)
|
| 241 |
+
- 'excel': User explicitly requested Excel / CSV format
|
| 242 |
+
- 'both': User explicitly requested both PDF and Excel formats
|
| 243 |
+
"""
|
| 244 |
+
q = (query or "").lower()
|
| 245 |
+
has_pdf = "pdf" in q
|
| 246 |
+
has_excel = any(kw in q for kw in ["excel", "csv", "xlsx", "sheet", "spreadsheet"])
|
| 247 |
+
|
| 248 |
+
if has_pdf and has_excel:
|
| 249 |
+
return "both"
|
| 250 |
+
elif has_excel and not has_pdf:
|
| 251 |
+
return "excel"
|
| 252 |
+
else:
|
| 253 |
+
# Simple report query defaults to PDF report
|
| 254 |
+
return "pdf"
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def report_agent_node(state: AgentState) -> AgentState:
|
| 258 |
+
"""Execute Report Generator Sub-Agent connected with Analysis Agent for timeframe synthesis."""
|
| 259 |
+
steps = list(state.get("orchestration_steps", []))
|
| 260 |
+
q_text = state.get("user_query", "")
|
| 261 |
+
timeframe_info = extract_timeframe_from_query(q_text)
|
| 262 |
+
tf_label = timeframe_info["label"]
|
| 263 |
+
platform_filter = extract_platform_filter_from_query(q_text)
|
| 264 |
+
format_pref = extract_format_preference_from_query(q_text)
|
| 265 |
+
|
| 266 |
+
print(f"- Gateway -> Report Agent: Extracting data for timeframe window '{tf_label}' (Platform: {platform_filter}, Format: {format_pref.upper()})...")
|
| 267 |
+
steps.append(f"Gateway -> Report Agent: Initializing report generation for timeframe '{tf_label}' ({platform_filter.upper()} report, Format: {format_pref.upper()})")
|
| 268 |
+
|
| 269 |
+
# Connect Report Agent with Analysis Agent
|
| 270 |
+
print(f"- Report Agent -> Analysis Agent: Connected to evaluate performance trends for {tf_label}...")
|
| 271 |
+
steps.append(f"Report Agent -> Analysis Agent: Connected for timeframe trend analysis ({tf_label})")
|
| 272 |
+
|
| 273 |
+
t_id = state.get("tenant_id", "")
|
| 274 |
+
|
| 275 |
+
# Fetch platform metrics via standardized MCP Server Tools
|
| 276 |
+
res_ga4 = state.get("ga4_data") or mcp_registry.call_tool("ga4_get_traffic_metrics", tenant_id=t_id)
|
| 277 |
+
res_shopify = state.get("shopify_data") or mcp_registry.call_tool("shopify_get_sales_metrics", tenant_id=t_id)
|
| 278 |
+
res_meta = state.get("meta_data") or mcp_registry.call_tool("meta_get_ad_insights", tenant_id=t_id)
|
| 279 |
+
|
| 280 |
+
growth_payload = build_growth_payload(ga4_data=res_ga4, shopify_data=res_shopify)
|
| 281 |
+
marketing_payload = build_marketing_payload(meta_data=res_meta, leads_data=state.get("leads_data"))
|
| 282 |
+
|
| 283 |
+
growth_res = process_growth(growth_payload)
|
| 284 |
+
marketing_res = process_marketing(marketing_payload)
|
| 285 |
+
|
| 286 |
+
# Perform Trend Analysis via Analysis Agent logic
|
| 287 |
+
analysis_findings = [
|
| 288 |
+
f"Analysis Agent timeframe window: {tf_label}.",
|
| 289 |
+
f"Platform Filter: {platform_filter.upper()}.",
|
| 290 |
+
f"GA4 Traffic Efficiency: {growth_res.get('traffic_summary', 'N/A')}",
|
| 291 |
+
f"Marketing ROAS Performance: {marketing_res.get('overall_roas', 0.0):.2f}x across ${marketing_res.get('ad_spend', 0.0):,.2f} ad spend.",
|
| 292 |
+
]
|
| 293 |
+
|
| 294 |
+
report_payload = {
|
| 295 |
+
"timeframe_label": tf_label,
|
| 296 |
+
"platform_filter": platform_filter,
|
| 297 |
+
"growth_result": growth_res,
|
| 298 |
+
"marketing_result": marketing_res,
|
| 299 |
+
"analysis_findings": analysis_findings,
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
report_res = generate_executive_report(report_payload)
|
| 303 |
+
state["report_result"] = report_res
|
| 304 |
+
|
| 305 |
+
# Generate PDF and/or Excel based on user format preference
|
| 306 |
+
pdf_filename = None
|
| 307 |
+
excel_filename = None
|
| 308 |
+
|
| 309 |
+
if format_pref in ["pdf", "both"]:
|
| 310 |
+
pdf_filename = generate_pdf_report(report_payload, tenant_id=t_id or "tenant")
|
| 311 |
+
if format_pref in ["excel", "both"]:
|
| 312 |
+
excel_filename = generate_excel_report(report_payload, tenant_id=t_id or "tenant")
|
| 313 |
+
|
| 314 |
+
report_markdown = report_res.get("report_markdown", "")
|
| 315 |
+
if f"Timeframe: {tf_label}" not in report_markdown:
|
| 316 |
+
report_markdown = f"⏱️ **Report Timeframe:** `{tf_label}`\n\n" + report_markdown
|
| 317 |
+
|
| 318 |
+
# Append Clickable Download Link(s) based on generated format(s)
|
| 319 |
+
download_links = []
|
| 320 |
+
if pdf_filename:
|
| 321 |
+
download_links.append(
|
| 322 |
+
f"• 📄 **[Download Executive Summary Report (PDF)](http://localhost:8000/reports/{pdf_filename})** *(Contains Executive Narrative & Performance Graph)*"
|
| 323 |
+
)
|
| 324 |
+
if excel_filename:
|
| 325 |
+
download_links.append(
|
| 326 |
+
f"• 📊 **[Download Detailed Metrics Data (Excel/CSV)](http://localhost:8000/reports/{excel_filename})** *(Contains full raw metric values & campaign numbers)*"
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
download_section = (
|
| 330 |
+
"\n\n---\n"
|
| 331 |
+
"### 📥 Official Executive Download\n" + "\n".join(download_links) + "\n"
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
report_markdown += download_section
|
| 335 |
+
|
| 336 |
+
# Check if user query explicitly asked to send email
|
| 337 |
+
q_lower = q_text.lower()
|
| 338 |
+
if any(em_kw in q_lower for em_kw in ["email", "send to email", "mail me"]):
|
| 339 |
+
import re
|
| 340 |
+
emails = re.findall(r'[\w\.-]+@[\w\.-]+\.\w+', q_lower)
|
| 341 |
+
target_email = emails[0] if emails else ""
|
| 342 |
+
sent = send_report_email(report_markdown, recipient_email=target_email)
|
| 343 |
+
if sent:
|
| 344 |
+
report_markdown += f"\n\n✅ *Executive Report has been dispatched to your email address ({target_email or 'registered inbox'}).*"
|
| 345 |
+
steps.append("Report Agent: Report dispatched via SMTP email")
|
| 346 |
+
|
| 347 |
+
state["final_answer"] = report_markdown
|
| 348 |
+
|
| 349 |
+
print(f"- Report Agent: Analysis & Report compiled for '{tf_label}' (Format: {format_pref.upper()}). Generated PDF ({pdf_filename}) & Excel ({excel_filename})")
|
| 350 |
+
steps.append(f"Report Agent: Analysis & Report generated for timeframe '{tf_label}' ({format_pref.upper()})")
|
| 351 |
+
|
| 352 |
+
state["orchestration_steps"] = steps
|
| 353 |
+
return state
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def executive_synthesizer_node(state: AgentState) -> AgentState:
|
| 358 |
+
"""Synthesize a final C-Suite executive answer using Groq LLM."""
|
| 359 |
+
steps = list(state.get("orchestration_steps", []))
|
| 360 |
+
print("- Gateway -> Executive Synthesizer: Generating response...")
|
| 361 |
+
steps.append("Gateway -> Executive Synthesizer: Generating response...")
|
| 362 |
+
|
| 363 |
+
context_parts = []
|
| 364 |
+
|
| 365 |
+
growth = state.get("growth_result")
|
| 366 |
+
if growth and growth.get("success"):
|
| 367 |
+
top_prods_list = growth.get("top_products", []) or []
|
| 368 |
+
top_prods_str = ", ".join([
|
| 369 |
+
f"{p.get('name', 'Product') if isinstance(p, dict) else str(p)}: ${p.get('sales', 0):,.2f}" if isinstance(p, dict) else str(p)
|
| 370 |
+
for p in top_prods_list
|
| 371 |
+
]) if top_prods_list else "N/A"
|
| 372 |
+
context_parts.append(
|
| 373 |
+
f"**Growth Agent Insights:**\n"
|
| 374 |
+
f"- Traffic: {growth.get('traffic_summary', 'N/A')}\n"
|
| 375 |
+
|
| 376 |
+
f"- New Visitors: {growth.get('new_users', 0):,}\n"
|
| 377 |
+
f"- AOV: ${growth.get('aov', 0):.2f}\n"
|
| 378 |
+
f"- Customer Lifetime Value (LTV): ${growth.get('ltv', 0):.2f}\n"
|
| 379 |
+
f"- Top Selling Products: {top_prods_str}\n"
|
| 380 |
+
f"- Checkout Conversion: {growth.get('checkout_conversion_rate', 0)}%\n"
|
| 381 |
+
f"- Analysis: {growth.get('insights', 'N/A')}\n"
|
| 382 |
+
f"- Recommendations: {', '.join(growth.get('recommendations', []))}"
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
marketing = state.get("marketing_result")
|
| 386 |
+
if marketing and marketing.get("success"):
|
| 387 |
+
high_perf = marketing.get("high_performers", [])
|
| 388 |
+
wasted = marketing.get("wasted_spend_campaigns", [])
|
| 389 |
+
context_parts.append(
|
| 390 |
+
f"**Marketing Agent Insights:**\n"
|
| 391 |
+
f"- Ad Spend: ${marketing.get('ad_spend', 0):,.2f}\n"
|
| 392 |
+
f"- Overall ROAS: {marketing.get('overall_roas', 0):.2f}x\n"
|
| 393 |
+
f"- Customer Acquisition Cost (CAC): ${marketing.get('cac', 0):.2f}\n"
|
| 394 |
+
f"- Cost Per Lead: ${marketing.get('cost_per_lead', 0):.2f}\n"
|
| 395 |
+
f"- High Performers: {high_perf}\n"
|
| 396 |
+
f"- Wasted Spend: {wasted}"
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
if not context_parts:
|
| 400 |
+
state["final_answer"] = "I couldn't retrieve enough data to provide analysis. Please check that all data connections are active."
|
| 401 |
+
state["orchestration_steps"] = steps
|
| 402 |
+
return state
|
| 403 |
+
|
| 404 |
+
context = "\n\n".join(context_parts)
|
| 405 |
+
|
| 406 |
+
q_text = state.get("user_query", "")
|
| 407 |
+
is_analysis_query = any(ak in q_text.lower() for ak in ["analysis", "analyze", "overview", "audit", "summary", "trend", "trends"])
|
| 408 |
+
|
| 409 |
+
if is_analysis_query:
|
| 410 |
+
system_prompt = (
|
| 411 |
+
"You are ScaleMate AI Growth & Performance Marketing Analyst. "
|
| 412 |
+
"When the user requests an analysis (e.g. 'analysis of last 2 hours of GA4' or 'meta ads analysis'), "
|
| 413 |
+
"you MUST format your response in this EXACT 3-part structure:\n\n"
|
| 414 |
+
"1. Header/Title: Start with a clear level-2 header explicitly stating what the analysis is about (e.g. '## Performance Analysis: Google Analytics 4 (GA4)').\n"
|
| 415 |
+
"2. Executive Summary: Provide a section titled '### Executive Summary' summarizing overall metrics, user engagement, and core findings.\n"
|
| 416 |
+
"3. Trend Graph: Provide a section titled '### Trend Graph' displaying a visual Markdown ASCII/Comparison chart or Mermaid xychart-beta trend visualization representing performance trends.\n"
|
| 417 |
+
"4. Key Insights & Action Items: Bullet points with high-impact recommendations.\n\n"
|
| 418 |
+
"Keep the tone authoritative, clear, and professional."
|
| 419 |
+
)
|
| 420 |
+
else:
|
| 421 |
+
system_prompt = (
|
| 422 |
+
"You are ScaleMate AI Growth & Performance Marketing Analyst. "
|
| 423 |
+
"Provide high-value, executive-level insights with immediate actionable business recommendations. "
|
| 424 |
+
"STRICT DIRECTIVE: Answer strictly what the user specifically asked for. "
|
| 425 |
+
"If the user asks for a single metric or platform (e.g. 'Active users' or 'GA4 traffic'), report ONLY that requested metric with its direct business impact. "
|
| 426 |
+
"Do NOT write fluff introductory paragraphs. Keep text clean, authoritative, professional, and laser-focused on revenue growth."
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
messages = [
|
| 430 |
+
{
|
| 431 |
+
"role": "system",
|
| 432 |
+
"content": system_prompt,
|
| 433 |
+
},
|
| 434 |
+
]
|
| 435 |
+
|
| 436 |
+
for msg in state.get("chat_history", [])[-5:]:
|
| 437 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 438 |
+
|
| 439 |
+
messages.append({
|
| 440 |
+
"role": "user",
|
| 441 |
+
"content": f"User Question: {state['user_query']}\n\nAgent Analysis Data:\n{context}",
|
| 442 |
+
})
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
try:
|
| 446 |
+
response = llm.invoke(messages)
|
| 447 |
+
state["final_answer"] = response.content
|
| 448 |
+
print("- Executive SuperAgent: Response generated successfully")
|
| 449 |
+
steps.append("Executive SuperAgent: Response generated successfully")
|
| 450 |
+
except Exception as e:
|
| 451 |
+
error_msg = f"Gateway Error: LLM synthesis failed - {type(e).__name__}: {e}"
|
| 452 |
+
print(f"- {error_msg}")
|
| 453 |
+
steps.append(error_msg)
|
| 454 |
+
state["final_answer"] = "Unable to complete analysis right now. Please try again in a moment."
|
| 455 |
+
state["error"] = str(e)
|
| 456 |
+
|
| 457 |
+
state["orchestration_steps"] = steps
|
| 458 |
+
return state
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def route_by_intent(state: AgentState) -> str:
|
| 462 |
+
"""Route to the appropriate sub-agent node based on classified intent."""
|
| 463 |
+
intent = state.get("intent", "cross_channel")
|
| 464 |
+
if intent == "access_denied":
|
| 465 |
+
return "access_denied"
|
| 466 |
+
elif intent == "greeting":
|
| 467 |
+
return "greeting"
|
| 468 |
+
elif intent == "counter_question":
|
| 469 |
+
return "counter_question"
|
| 470 |
+
elif intent == "report":
|
| 471 |
+
return "report_agent"
|
| 472 |
+
elif intent == "growth":
|
| 473 |
+
return "growth_agent"
|
| 474 |
+
elif intent == "marketing":
|
| 475 |
+
return "marketing_agent"
|
| 476 |
+
else:
|
| 477 |
+
return "both_agents"
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def build_graph() -> StateGraph:
|
| 481 |
+
"""Construct and compile the LangGraph multi-agent orchestration graph."""
|
| 482 |
+
graph = StateGraph(AgentState)
|
| 483 |
+
|
| 484 |
+
graph.add_node("intent_classifier", intent_classifier_node)
|
| 485 |
+
graph.add_node("access_denied", access_denied_node)
|
| 486 |
+
graph.add_node("counter_question", counter_question_node)
|
| 487 |
+
graph.add_node("greeting", greeting_responder_node)
|
| 488 |
+
graph.add_node("growth_agent", growth_agent_node)
|
| 489 |
+
graph.add_node("marketing_agent", marketing_agent_node)
|
| 490 |
+
graph.add_node("both_growth", growth_agent_node)
|
| 491 |
+
graph.add_node("both_marketing", marketing_agent_node)
|
| 492 |
+
graph.add_node("report_agent", report_agent_node)
|
| 493 |
+
graph.add_node("executive_synthesizer", executive_synthesizer_node)
|
| 494 |
+
|
| 495 |
+
graph.set_entry_point("intent_classifier")
|
| 496 |
+
|
| 497 |
+
graph.add_conditional_edges(
|
| 498 |
+
"intent_classifier",
|
| 499 |
+
route_by_intent,
|
| 500 |
+
{
|
| 501 |
+
"access_denied": "access_denied",
|
| 502 |
+
"greeting": "greeting",
|
| 503 |
+
"counter_question": "counter_question",
|
| 504 |
+
"report_agent": "report_agent",
|
| 505 |
+
"growth_agent": "growth_agent",
|
| 506 |
+
"marketing_agent": "marketing_agent",
|
| 507 |
+
"both_agents": "both_growth",
|
| 508 |
+
},
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
graph.add_edge("access_denied", END)
|
| 512 |
+
graph.add_edge("greeting", END)
|
| 513 |
+
graph.add_edge("counter_question", END)
|
| 514 |
+
graph.add_edge("report_agent", END)
|
| 515 |
+
graph.add_edge("growth_agent", "executive_synthesizer")
|
| 516 |
+
graph.add_edge("marketing_agent", "executive_synthesizer")
|
| 517 |
+
graph.add_edge("both_growth", "both_marketing")
|
| 518 |
+
graph.add_edge("both_marketing", "executive_synthesizer")
|
| 519 |
+
graph.add_edge("executive_synthesizer", END)
|
| 520 |
+
|
| 521 |
+
return graph.compile()
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
compiled_graph = build_graph()
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
async def run_orchestrator(request: SuperAgentRequest) -> SuperAgentResponse | ErrorResponse:
|
| 528 |
+
"""
|
| 529 |
+
Execute the full LangGraph orchestration pipeline.
|
| 530 |
+
Returns SuperAgentResponse on success, ErrorResponse on failure.
|
| 531 |
+
"""
|
| 532 |
+
start_time = time.time()
|
| 533 |
+
|
| 534 |
+
try:
|
| 535 |
+
initial_state: AgentState = {
|
| 536 |
+
"user_query": request.user_query,
|
| 537 |
+
"user_id": request.user_id,
|
| 538 |
+
"tenant_id": request.tenant_id,
|
| 539 |
+
"session_id": request.session_id or "default-session",
|
| 540 |
+
"intent": "",
|
| 541 |
+
"counter_question_prompt": None,
|
| 542 |
+
"ga4_data": request.ga4_data.model_dump() if request.ga4_data else None,
|
| 543 |
+
"shopify_data": request.shopify_data.model_dump() if request.shopify_data else None,
|
| 544 |
+
"meta_data": request.meta_data.model_dump() if request.meta_data else None,
|
| 545 |
+
"leads_data": request.leads_data.model_dump() if request.leads_data else None,
|
| 546 |
+
"chat_history": [h.model_dump() for h in request.chat_history],
|
| 547 |
+
"growth_result": None,
|
| 548 |
+
"marketing_result": None,
|
| 549 |
+
"report_result": None,
|
| 550 |
+
"final_answer": "",
|
| 551 |
+
"orchestration_steps": [],
|
| 552 |
+
"email_alert_triggered": False,
|
| 553 |
+
"email_type": None,
|
| 554 |
+
"error": None,
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
final_state = compiled_graph.invoke(initial_state)
|
| 558 |
+
|
| 559 |
+
return SuperAgentResponse(
|
| 560 |
+
success=True,
|
| 561 |
+
intent=final_state.get("intent", "unknown"),
|
| 562 |
+
session_id=final_state.get("session_id", ""),
|
| 563 |
+
answer=final_state.get("final_answer", ""),
|
| 564 |
+
orchestration_steps=final_state.get("orchestration_steps", []),
|
| 565 |
+
email_alert_triggered=final_state.get("email_alert_triggered", False),
|
| 566 |
+
email_type=final_state.get("email_type"),
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
except Exception as e:
|
| 570 |
+
execution_time = time.time() - start_time
|
| 571 |
+
print(f"- Gateway Pipeline Error ({execution_time:.2f}s): {e}")
|
| 572 |
+
return ErrorResponse(
|
| 573 |
+
success=False,
|
| 574 |
+
error=str(e),
|
| 575 |
+
orchestration_steps=[f"Gateway Pipeline Exception: {e}"],
|
| 576 |
+
)
|
SuperAgent/main.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Executive SuperAgent & Multi-Agent Platform — Unified FastAPI Entry Point (Port 8000)
|
| 3 |
+
Matches exact multi-agent API structure: /api/v1/chat/message, /api/v1/chat/sessions, /api/v1/assets, etc.
|
| 4 |
+
Interactive API Documentation: http://localhost:8000/docs
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
from typing import Any, Optional
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from fastapi import FastAPI, HTTPException, Query, Path
|
| 13 |
+
from fastapi.responses import FileResponse
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from pydantic import BaseModel, Field
|
| 16 |
+
from pathlib import Path as SysPath
|
| 17 |
+
|
| 18 |
+
REPORTS_DIR = SysPath(__file__).resolve().parent.parent / "reports"
|
| 19 |
+
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 20 |
+
|
| 21 |
+
from .app.models.schemas import (
|
| 22 |
+
SuperAgentRequest,
|
| 23 |
+
SuperAgentResponse,
|
| 24 |
+
ErrorResponse,
|
| 25 |
+
GrowthAnalyzeRequest,
|
| 26 |
+
GrowthAnalyzeResponse,
|
| 27 |
+
MarketingAnalyzeRequest,
|
| 28 |
+
MarketingAnalyzeResponse,
|
| 29 |
+
)
|
| 30 |
+
from .app.services.router import run_orchestrator
|
| 31 |
+
from .app.services.email_service import check_campaign_alerts, send_alert_email
|
| 32 |
+
from .app.services.logger import OrchestrationLogger
|
| 33 |
+
|
| 34 |
+
load_dotenv()
|
| 35 |
+
|
| 36 |
+
app = FastAPI(
|
| 37 |
+
title="ScaleMate Executive SuperAgent Platform",
|
| 38 |
+
description="Multi-Agent System for Growth, Marketing, GA4, Shopify, Meta Ads, Leads, Chat Sessions, and Assets.",
|
| 39 |
+
version="1.0.0",
|
| 40 |
+
docs_url="/docs",
|
| 41 |
+
redoc_url="/redoc",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
app.add_middleware(
|
| 45 |
+
CORSMiddleware,
|
| 46 |
+
allow_origins=["*"],
|
| 47 |
+
allow_credentials=True,
|
| 48 |
+
allow_methods=["*"],
|
| 49 |
+
allow_headers=["*"],
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ─────────────────────────────────────────────
|
| 54 |
+
# Root & Health
|
| 55 |
+
# ─────────────────────────────────────────────
|
| 56 |
+
|
| 57 |
+
@app.get("/", tags=["default"])
|
| 58 |
+
async def read_index():
|
| 59 |
+
"""Root index endpoint."""
|
| 60 |
+
return {"message": "ScaleMate Executive SuperAgent Multi-Agent System is online."}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@app.get("/health", tags=["default"])
|
| 64 |
+
async def health():
|
| 65 |
+
"""Health check endpoint."""
|
| 66 |
+
return {
|
| 67 |
+
"status": "healthy",
|
| 68 |
+
"service": "Executive SuperAgent Platform",
|
| 69 |
+
"port": 8000,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@app.get("/debug/logs", tags=["default"])
|
| 74 |
+
async def get_logs():
|
| 75 |
+
"""Get system debug logs."""
|
| 76 |
+
return {
|
| 77 |
+
"status": "success",
|
| 78 |
+
"logs": [
|
| 79 |
+
"System Orchestration initialized",
|
| 80 |
+
"Executive SuperAgent ready on Port 8000",
|
| 81 |
+
"Growth Agent registered",
|
| 82 |
+
"Marketing Agent registered",
|
| 83 |
+
],
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# ─────────────────────────────────────────────
|
| 88 |
+
# Report Download Endpoint (/reports/{filename})
|
| 89 |
+
# ─────────────────────────────────────────────
|
| 90 |
+
|
| 91 |
+
@app.get("/reports/{filename}", tags=["default"])
|
| 92 |
+
async def download_report(filename: str = Path(..., description="Report Filename")):
|
| 93 |
+
"""Download Report — Streams PDF Document or Excel CSV Spreadsheet directly to browser."""
|
| 94 |
+
file_path = REPORTS_DIR / filename
|
| 95 |
+
if not file_path.exists():
|
| 96 |
+
raise HTTPException(status_code=404, detail=f"Report file '{filename}' not found")
|
| 97 |
+
|
| 98 |
+
media_type = "application/pdf" if filename.endswith(".pdf") else "text/csv"
|
| 99 |
+
return FileResponse(
|
| 100 |
+
str(file_path),
|
| 101 |
+
media_type=media_type,
|
| 102 |
+
filename=filename,
|
| 103 |
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ─────────────────────────────────────────────
|
| 108 |
+
# Chat Endpoints (/api/v1/chat/...)
|
| 109 |
+
# ─────────────────────────────────────────────
|
| 110 |
+
|
| 111 |
+
@app.post("/api/v1/chat/message", response_model=SuperAgentResponse, tags=["default"])
|
| 112 |
+
async def chat_message(request: SuperAgentRequest):
|
| 113 |
+
"""Chat Message endpoint — Primary multi-agent orchestrator entry point."""
|
| 114 |
+
result = await run_orchestrator(request)
|
| 115 |
+
return result
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@app.get("/api/v1/chat/sessions", tags=["default"])
|
| 119 |
+
async def list_chat_sessions(
|
| 120 |
+
user_id: str = Query(..., description="User ID"),
|
| 121 |
+
tenant_id: str = Query(..., description="Tenant ID"),
|
| 122 |
+
):
|
| 123 |
+
"""List Chat Sessions for user and tenant."""
|
| 124 |
+
return {
|
| 125 |
+
"success": True,
|
| 126 |
+
"user_id": user_id,
|
| 127 |
+
"tenant_id": tenant_id,
|
| 128 |
+
"message": "Chat sessions managed in Supabase agent_chat_sessions table.",
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class CreateSessionRequest(BaseModel):
|
| 133 |
+
user_id: str
|
| 134 |
+
tenant_id: str
|
| 135 |
+
title: Optional[str] = "New AI Chat"
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@app.post("/api/v1/chat/sessions", tags=["default"])
|
| 139 |
+
async def create_chat_session(payload: CreateSessionRequest):
|
| 140 |
+
"""Create Chat Session."""
|
| 141 |
+
return {
|
| 142 |
+
"success": True,
|
| 143 |
+
"session": {
|
| 144 |
+
"title": payload.title,
|
| 145 |
+
"user_id": payload.user_id,
|
| 146 |
+
"tenant_id": payload.tenant_id,
|
| 147 |
+
},
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@app.get("/api/v1/chat/sessions/{session_id}/messages", tags=["default"])
|
| 152 |
+
async def get_session_messages(session_id: str = Path(..., description="Session ID")):
|
| 153 |
+
"""Get Session Messages."""
|
| 154 |
+
return {
|
| 155 |
+
"success": True,
|
| 156 |
+
"session_id": session_id,
|
| 157 |
+
"messages": [],
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class UpdateSessionRequest(BaseModel):
|
| 162 |
+
title: str
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@app.put("/api/v1/chat/sessions/{session_id}", tags=["default"])
|
| 166 |
+
async def update_session_title(session_id: str, payload: UpdateSessionRequest):
|
| 167 |
+
"""Update Session Title."""
|
| 168 |
+
return {
|
| 169 |
+
"success": True,
|
| 170 |
+
"session_id": session_id,
|
| 171 |
+
"updated_title": payload.title,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@app.delete("/api/v1/chat/sessions/{session_id}", tags=["default"])
|
| 176 |
+
async def delete_chat_session(session_id: str):
|
| 177 |
+
"""Delete Chat Session."""
|
| 178 |
+
return {
|
| 179 |
+
"success": True,
|
| 180 |
+
"deleted_session_id": session_id,
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# ─────────────────────────────────────────────
|
| 185 |
+
# ScaleMate Assets Endpoint (/api/v1/assets)
|
| 186 |
+
# ─────────────────────────────────────────────
|
| 187 |
+
|
| 188 |
+
@app.get("/api/v1/assets", tags=["default"])
|
| 189 |
+
async def list_assets(tenant_id: Optional[str] = Query(None, description="Tenant ID")):
|
| 190 |
+
"""
|
| 191 |
+
List connected ScaleMate business assets & integrations
|
| 192 |
+
(GA4 Analytics, Shopify E-Commerce, Meta Ads, Google Ads, Lead Capture).
|
| 193 |
+
"""
|
| 194 |
+
return {
|
| 195 |
+
"success": True,
|
| 196 |
+
"tenant_id": tenant_id,
|
| 197 |
+
"assets": [
|
| 198 |
+
{
|
| 199 |
+
"id": "asset_ga4",
|
| 200 |
+
"name": "Google Analytics 4 (GA4)",
|
| 201 |
+
"provider": "ga4",
|
| 202 |
+
"category": "Traffic & Analytics",
|
| 203 |
+
"status": "connected",
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"id": "asset_shopify",
|
| 207 |
+
"name": "Shopify Store",
|
| 208 |
+
"provider": "shopify",
|
| 209 |
+
"category": "Revenue & E-Commerce",
|
| 210 |
+
"status": "connected",
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"id": "asset_meta",
|
| 214 |
+
"name": "Meta Ads Account (Facebook/Instagram)",
|
| 215 |
+
"provider": "meta",
|
| 216 |
+
"category": "Paid Advertising",
|
| 217 |
+
"status": "connected",
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"id": "asset_google_ads",
|
| 221 |
+
"name": "Google Ads",
|
| 222 |
+
"provider": "google_ads",
|
| 223 |
+
"category": "Paid Search Advertising",
|
| 224 |
+
"status": "connected",
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"id": "asset_leads",
|
| 228 |
+
"name": "ScaleMate Lead Capture Forms",
|
| 229 |
+
"provider": "leads",
|
| 230 |
+
"category": "Lead Generation",
|
| 231 |
+
"status": "connected",
|
| 232 |
+
},
|
| 233 |
+
],
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# ─────────────────────────────────────────────
|
| 238 |
+
# Sub-Agent Microservices Endpoints
|
| 239 |
+
# ─────────────────────────────────────────────
|
| 240 |
+
|
| 241 |
+
def get_val(obj: Any, field: str, default: Any = None) -> Any:
|
| 242 |
+
if obj is None:
|
| 243 |
+
return default
|
| 244 |
+
if isinstance(obj, dict):
|
| 245 |
+
return obj.get(field, default)
|
| 246 |
+
return getattr(obj, field, default)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@app.post("/api/v1/growth/analyze", response_model=GrowthAnalyzeResponse, tags=["default"])
|
| 250 |
+
async def analyze_growth(request: GrowthAnalyzeRequest):
|
| 251 |
+
"""Analyze Growth — GA4 & Shopify metrics (Always fetches live data via MCP tools)."""
|
| 252 |
+
try:
|
| 253 |
+
from backend.agents.growth.app.main import analyze_growth as growth_handler, GrowthRequest
|
| 254 |
+
req_obj = GrowthRequest(
|
| 255 |
+
user_query=request.user_query,
|
| 256 |
+
tenant_id=request.tenant_id,
|
| 257 |
+
user_id=request.user_id,
|
| 258 |
+
session_id=request.session_id,
|
| 259 |
+
)
|
| 260 |
+
return await growth_handler(req_obj)
|
| 261 |
+
except Exception as e:
|
| 262 |
+
print(f"- Growth Endpoint Error: {e}")
|
| 263 |
+
return GrowthAnalyzeResponse(
|
| 264 |
+
success=True,
|
| 265 |
+
traffic_summary="Growth data analysis processed.",
|
| 266 |
+
aov=0.0,
|
| 267 |
+
checkout_conversion_rate=0.0,
|
| 268 |
+
insights="",
|
| 269 |
+
recommendations=[],
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
@app.post("/api/v1/marketing/analyze", response_model=MarketingAnalyzeResponse, tags=["default"])
|
| 274 |
+
async def analyze_marketing(request: MarketingAnalyzeRequest):
|
| 275 |
+
"""Analyze Marketing — Meta Ads & Leads metrics (Always fetches live data via MCP tools)."""
|
| 276 |
+
try:
|
| 277 |
+
from backend.agents.marketing.app.main import analyze_marketing as marketing_handler, MarketingRequest
|
| 278 |
+
req_obj = MarketingRequest(
|
| 279 |
+
user_query=request.user_query,
|
| 280 |
+
tenant_id=request.tenant_id,
|
| 281 |
+
user_id=request.user_id,
|
| 282 |
+
session_id=request.session_id,
|
| 283 |
+
)
|
| 284 |
+
return await marketing_handler(req_obj)
|
| 285 |
+
except Exception as e:
|
| 286 |
+
print(f"- Marketing Endpoint Error: {e}")
|
| 287 |
+
return MarketingAnalyzeResponse(
|
| 288 |
+
success=True,
|
| 289 |
+
ad_spend=0.0,
|
| 290 |
+
overall_roas=0.0,
|
| 291 |
+
cost_per_lead=0.0,
|
| 292 |
+
high_performers=[],
|
| 293 |
+
wasted_spend_campaigns=[],
|
| 294 |
+
email_triggered=False,
|
| 295 |
+
email_alert_type=None,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
class TrendsRequest(BaseModel):
|
| 303 |
+
current_period: Optional[dict[str, Any]] = None
|
| 304 |
+
previous_period: Optional[dict[str, Any]] = None
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
@app.post("/api/v1/analysis/trends", tags=["default"])
|
| 308 |
+
async def analyze_trends(request: TrendsRequest):
|
| 309 |
+
"""Analyze Trends — Historical multi-period comparison."""
|
| 310 |
+
current = request.current_period or {}
|
| 311 |
+
previous = request.previous_period or {}
|
| 312 |
+
changes = []
|
| 313 |
+
for key in current:
|
| 314 |
+
curr_val = current.get(key, 0)
|
| 315 |
+
prev_val = previous.get(key, 0)
|
| 316 |
+
if isinstance(curr_val, (int, float)) and isinstance(prev_val, (int, float)) and prev_val != 0:
|
| 317 |
+
pct_change = round(((curr_val - prev_val) / prev_val) * 100, 2)
|
| 318 |
+
direction = "up" if pct_change > 0 else "down"
|
| 319 |
+
changes.append({
|
| 320 |
+
"metric": key,
|
| 321 |
+
"current": curr_val,
|
| 322 |
+
"previous": prev_val,
|
| 323 |
+
"change_pct": pct_change,
|
| 324 |
+
"direction": direction,
|
| 325 |
+
})
|
| 326 |
+
|
| 327 |
+
return {"success": True, "agent": "analysis_agent", "changes": changes}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class EmailAlertRequest(BaseModel):
|
| 331 |
+
alert_type: str = "scaling_alert"
|
| 332 |
+
high_performers: list[dict[str, Any]] = []
|
| 333 |
+
wasted: list[dict[str, Any]] = []
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
@app.post("/api/v1/email-alert", tags=["default"])
|
| 337 |
+
async def trigger_email_alert(request: EmailAlertRequest):
|
| 338 |
+
"""Trigger Email Alert."""
|
| 339 |
+
sent = send_alert_email(
|
| 340 |
+
alert_type=request.alert_type,
|
| 341 |
+
high_performers=request.high_performers,
|
| 342 |
+
wasted=request.wasted,
|
| 343 |
+
)
|
| 344 |
+
return {
|
| 345 |
+
"success": sent,
|
| 346 |
+
"message": "Email alert dispatched successfully" if sent else "Email alert failed or SMTP unconfigured",
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
if __name__ == "__main__":
|
| 351 |
+
import uvicorn
|
| 352 |
+
if os.getenv("SPACE_ID"):
|
| 353 |
+
port = int(os.getenv("PORT", "7860"))
|
| 354 |
+
else:
|
| 355 |
+
port = int(os.getenv("PORT", "8000"))
|
| 356 |
+
uvicorn.run(
|
| 357 |
+
"backend.SuperAgent.main:app",
|
| 358 |
+
host="0.0.0.0",
|
| 359 |
+
port=port,
|
| 360 |
+
reload=True,
|
| 361 |
+
)
|
| 362 |
+
|
SuperAgent/superagent.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SuperAgent Executive Router Specification (`superagent.md`)
|
| 2 |
+
|
| 3 |
+
## 1. Identity & System Persona
|
| 4 |
+
* **Name & Designation:** ScaleMate AI Executive SuperAgent Orchestrator
|
| 5 |
+
* **Role & Domain Scope:** Executive C-Suite Router, Permission Gatekeeper, Intent Classifier, and Multi-Agent Coordinator.
|
| 6 |
+
* **Operational Goal:** Manages incoming user requests, enforces 3-Layer Knowledge Cache access verification, executes counter-questioning workflows, and synthesizes sub-agent insights into high-level business responses.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 2. Business Logics, Control Rules & Directives
|
| 11 |
+
|
| 12 |
+
### A. 3-Layer Assets Knowledge Cache Rules (0ms Latency)
|
| 13 |
+
1. **Layer 1 (In-Memory RAM Check)**: On incoming request (`user_id`, `tenant_id`), inspect `self._user_cache`. If present, return in **0ms** without touching disk or DB.
|
| 14 |
+
2. **Layer 2 (Disk `cache.json` Check)**: Check `Assets Knowledge/<tenant_id>/<user_id>/cache.json`. If present, load into RAM (`self._user_cache`) and return in **0ms** without querying DB.
|
| 15 |
+
3. **Layer 3 (100% Real-Time DB Extraction)**: Triggered ONLY on RAM & Disk miss. Query Supabase DB `connections` and `user_tenant_roles`, extract metadata in real-time, populate RAM, and save `cache.json` to disk.
|
| 16 |
+
4. **0ms Permission Gate Short-Circuit**: If `user_authorized == False`, immediately short-circuit execution to `access_denied_node` without calling sub-agents.
|
| 17 |
+
|
| 18 |
+
### B. Intent Classification & Routing Logic
|
| 19 |
+
1. **Greeting Intent**: Single-word or short greetings (`hi`, `hello`, `salam`, `thanks`) $\rightarrow$ Route to `greeting_node` (no sub-agents called).
|
| 20 |
+
2. **Counter-Questioning Logic**:
|
| 21 |
+
- **Scenario A (Multi-Channel missing metric)**: Offer Option 1 (Revenue vs ROAS), Option 2 (Conversion & Traffic), Option 3 (Full Cross-Channel Report).
|
| 22 |
+
- **Scenario B (Single-Channel missing metric)**: Present list of available metrics for that specific channel.
|
| 23 |
+
- **Scenario C (Metric Mismatch)**: Alert user that requested metric is unavailable on target channel and propose valid alternative platform.
|
| 24 |
+
- **Scenario D (Vague Query)**: Ask user which platform (Shopify, GA4, Meta Ads) they want to analyze.
|
| 25 |
+
3. **Growth Intent**: Route to `growth_agent_node` (GA4 traffic, sessions, bounce rate, Shopify sales, AOV, checkout funnel).
|
| 26 |
+
4. **Marketing Intent**: Route to `marketing_agent_node` (Meta Ads spend, ROAS, CTR, CPC, cost per lead).
|
| 27 |
+
5. **Cross-Channel Intent**: Route to both `growth_agent_node` and `marketing_agent_node` concurrently.
|
| 28 |
+
6. **Report Generation Intent**: Route to `report_agent_node` for executive summary synthesis.
|
| 29 |
+
|
| 30 |
+
### C. Strict Response Formatting Directives
|
| 31 |
+
1. **Strict Query Targeting**: Answer ONLY what the user specifically asked for. If the query is `"Active users"`, report ONLY GA4 active users. Do NOT include unasked Shopify revenue or Meta ROAS metrics.
|
| 32 |
+
2. **Zero Emojis Policy**: Never include any emojis or decorative icons in generated responses.
|
| 33 |
+
3. **Concise C-Suite Bullet Points**: Maximum 3-4 clear bullet points with immediate actionable business recommendations.
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## 3. Data Contracts, Tools & Schemas
|
| 38 |
+
|
| 39 |
+
### Input Schema (`SuperAgentRequest`)
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"user_query": "string",
|
| 43 |
+
"tenant_id": "string",
|
| 44 |
+
"user_id": "string",
|
| 45 |
+
"session_id": "string",
|
| 46 |
+
"chat_history": []
|
| 47 |
+
}
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Output Schema (`SuperAgentResponse`)
|
| 51 |
+
```json
|
| 52 |
+
{
|
| 53 |
+
"success": true,
|
| 54 |
+
"intent": "growth | marketing | cross_channel | report | counter_question | greeting | access_denied",
|
| 55 |
+
"session_id": "string",
|
| 56 |
+
"answer": "string",
|
| 57 |
+
"orchestration_steps": [],
|
| 58 |
+
"email_alert_triggered": false,
|
| 59 |
+
"email_type": null
|
| 60 |
+
}
|
| 61 |
+
```
|
agents/analysis/backend/app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
agents/analysis/backend/app/main.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Analysis Agent — Historical Trends Microservice (Port 8003)
|
| 3 |
+
Provides multi-period trend comparisons across channels.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from typing import Any, Optional
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
app = FastAPI(
|
| 15 |
+
title="ScaleMate Analysis Agent",
|
| 16 |
+
description="Historical Trends & Multi-Period Analysis Microservice",
|
| 17 |
+
version="1.0.0",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"],
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TrendsRequest(BaseModel):
|
| 30 |
+
current_period: Optional[dict[str, Any]] = None
|
| 31 |
+
previous_period: Optional[dict[str, Any]] = None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class TrendsResponse(BaseModel):
|
| 35 |
+
success: bool = True
|
| 36 |
+
agent: str = "analysis_agent"
|
| 37 |
+
trend_summary: str = ""
|
| 38 |
+
changes: list[dict[str, Any]] = []
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@app.get("/health")
|
| 42 |
+
async def health():
|
| 43 |
+
return {"status": "healthy", "service": "Analysis Agent", "port": 8003}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@app.post("/api/v1/analysis/trends", response_model=TrendsResponse)
|
| 47 |
+
async def analyze_trends(request: TrendsRequest):
|
| 48 |
+
"""Compare current vs previous period metrics."""
|
| 49 |
+
changes = []
|
| 50 |
+
summary_parts = []
|
| 51 |
+
|
| 52 |
+
current = request.current_period or {}
|
| 53 |
+
previous = request.previous_period or {}
|
| 54 |
+
|
| 55 |
+
for key in current:
|
| 56 |
+
curr_val = current.get(key, 0)
|
| 57 |
+
prev_val = previous.get(key, 0)
|
| 58 |
+
|
| 59 |
+
if isinstance(curr_val, (int, float)) and isinstance(prev_val, (int, float)) and prev_val != 0:
|
| 60 |
+
pct_change = round(((curr_val - prev_val) / prev_val) * 100, 2)
|
| 61 |
+
direction = "up" if pct_change > 0 else "down"
|
| 62 |
+
changes.append({
|
| 63 |
+
"metric": key,
|
| 64 |
+
"current": curr_val,
|
| 65 |
+
"previous": prev_val,
|
| 66 |
+
"change_pct": pct_change,
|
| 67 |
+
"direction": direction,
|
| 68 |
+
})
|
| 69 |
+
summary_parts.append(f"{key}: {pct_change:+.1f}% ({direction})")
|
| 70 |
+
|
| 71 |
+
summary = ". ".join(summary_parts) if summary_parts else "No trend data available."
|
| 72 |
+
|
| 73 |
+
return TrendsResponse(
|
| 74 |
+
success=True,
|
| 75 |
+
trend_summary=summary,
|
| 76 |
+
changes=changes,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
import uvicorn
|
| 82 |
+
uvicorn.run("backend.agents.analysis.backend.app.main:app", host="0.0.0.0", port=8003, reload=True)
|
agents/growth/app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (12.8 kB). View file
|
|
|
agents/growth/app/main.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Growth Agent — GA4 Traffic + Shopify Revenue Microservice (Port 8001)
|
| 3 |
+
Analyzes website traffic, e-commerce funnel, and conversion metrics.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from typing import Any, Optional
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
app = FastAPI(
|
| 15 |
+
title="ScaleMate Growth Agent",
|
| 16 |
+
description="GA4 Traffic & Shopify Revenue Analysis Microservice",
|
| 17 |
+
version="1.0.0",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"],
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Request / Response Models
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class GrowthRequest(BaseModel):
|
| 34 |
+
user_query: str
|
| 35 |
+
tenant_id: str
|
| 36 |
+
user_id: str
|
| 37 |
+
session_id: Optional[str] = "default-session"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class GrowthResponse(BaseModel):
|
| 43 |
+
success: bool = True
|
| 44 |
+
agent: str = "growth_agent"
|
| 45 |
+
traffic_summary: str = ""
|
| 46 |
+
aov: float = 0.0
|
| 47 |
+
checkout_conversion_rate: float = 0.0
|
| 48 |
+
insights: str = ""
|
| 49 |
+
recommendations: list[str] = []
|
| 50 |
+
answer: Optional[str] = None
|
| 51 |
+
tenant_id: Optional[str] = None
|
| 52 |
+
session_id: Optional[str] = None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Business Logic — Function-Based Analysis
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def analyze_traffic(ga4: dict[str, Any]) -> dict[str, Any]:
|
| 60 |
+
"""Analyze GA4 traffic metrics."""
|
| 61 |
+
active_users = ga4.get("activeUsers", 0)
|
| 62 |
+
sessions = ga4.get("totalSessions", 0)
|
| 63 |
+
bounce_rate = ga4.get("bounceRate", 0)
|
| 64 |
+
sources = ga4.get("trafficSources", [])
|
| 65 |
+
|
| 66 |
+
top_source = "N/A"
|
| 67 |
+
if sources:
|
| 68 |
+
first_item = sources[0]
|
| 69 |
+
if isinstance(first_item, dict):
|
| 70 |
+
sorted_sources = sorted(sources, key=lambda s: s.get("users", 0) if isinstance(s, dict) else 0, reverse=True)
|
| 71 |
+
top_source = sorted_sources[0].get("source", "N/A") if isinstance(sorted_sources[0], dict) else str(sorted_sources[0])
|
| 72 |
+
elif isinstance(first_item, str):
|
| 73 |
+
top_source = first_item
|
| 74 |
+
|
| 75 |
+
summary = f"{active_users:,} active users, {sessions:,} sessions, {bounce_rate}% bounce rate. Top source: {top_source}."
|
| 76 |
+
return {"summary": summary, "active_users": active_users, "bounce_rate": bounce_rate}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def analyze_shopify_funnel(shopify: dict[str, Any]) -> dict[str, Any]:
|
| 80 |
+
"""Analyze Shopify e-commerce funnel and AOV."""
|
| 81 |
+
revenue = shopify.get("totalRevenue", 0)
|
| 82 |
+
orders = shopify.get("totalOrders", 0)
|
| 83 |
+
aov = shopify.get("avgOrderValue", 0)
|
| 84 |
+
funnel = shopify.get("funnel", {})
|
| 85 |
+
|
| 86 |
+
visitors = funnel.get("visitors", 0)
|
| 87 |
+
checkouts = funnel.get("checkouts", 0)
|
| 88 |
+
purchases = funnel.get("purchases", 0)
|
| 89 |
+
|
| 90 |
+
checkout_rate = 0.0
|
| 91 |
+
if checkouts > 0:
|
| 92 |
+
checkout_rate = round((purchases / checkouts) * 100, 2)
|
| 93 |
+
|
| 94 |
+
drop_off = round(100 - checkout_rate, 2) if checkouts > 0 else 0
|
| 95 |
+
insights = f"Revenue ${revenue:,.2f} across {orders} orders (AOV: ${aov:.2f})."
|
| 96 |
+
if checkouts > 0:
|
| 97 |
+
insights += f" Checkout conversion: {checkout_rate}%, with {drop_off}% drop-off."
|
| 98 |
+
|
| 99 |
+
recommendations = []
|
| 100 |
+
if checkout_rate < 60:
|
| 101 |
+
recommendations.append("Optimize checkout flow — add trust badges and simplify payment steps.")
|
| 102 |
+
if aov < 50:
|
| 103 |
+
recommendations.append("Implement upsell/cross-sell strategies to increase AOV above $50.")
|
| 104 |
+
if not recommendations:
|
| 105 |
+
recommendations.append("Funnel performing well. Consider A/B testing to push conversion above 70%.")
|
| 106 |
+
|
| 107 |
+
return {
|
| 108 |
+
"aov": aov,
|
| 109 |
+
"checkout_conversion_rate": checkout_rate,
|
| 110 |
+
"insights": insights,
|
| 111 |
+
"recommendations": recommendations,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def process_growth(payload: dict[str, Any]) -> dict[str, Any]:
|
| 116 |
+
"""Process growth metrics payload with custom LTV, Top Products & User Retention calculations."""
|
| 117 |
+
ga4_data = payload.get("ga4_data") or {}
|
| 118 |
+
shopify_data = payload.get("shopify_data") or {}
|
| 119 |
+
|
| 120 |
+
traffic_summary = ""
|
| 121 |
+
aov = 0.0
|
| 122 |
+
checkout_rate = 0.0
|
| 123 |
+
insights = ""
|
| 124 |
+
recommendations = []
|
| 125 |
+
ltv = 0.0
|
| 126 |
+
top_products = []
|
| 127 |
+
new_users = 0
|
| 128 |
+
|
| 129 |
+
if ga4_data:
|
| 130 |
+
traffic = analyze_traffic(ga4_data)
|
| 131 |
+
traffic_summary = traffic["summary"]
|
| 132 |
+
new_users = ga4_data.get("newUsers", 0)
|
| 133 |
+
|
| 134 |
+
if shopify_data:
|
| 135 |
+
funnel = analyze_shopify_funnel(shopify_data)
|
| 136 |
+
aov = funnel["aov"]
|
| 137 |
+
checkout_rate = funnel["checkout_conversion_rate"]
|
| 138 |
+
insights = funnel["insights"]
|
| 139 |
+
recommendations = funnel["recommendations"]
|
| 140 |
+
ltv = shopify_data.get("ltv", round(aov * 2.5, 2))
|
| 141 |
+
top_products = shopify_data.get("topProducts", [])
|
| 142 |
+
|
| 143 |
+
if ltv > 0:
|
| 144 |
+
insights += f" Estimated Customer Lifetime Value (LTV): ${ltv:.2f}."
|
| 145 |
+
|
| 146 |
+
return {
|
| 147 |
+
"success": True,
|
| 148 |
+
"traffic_summary": traffic_summary,
|
| 149 |
+
"aov": aov,
|
| 150 |
+
"checkout_conversion_rate": checkout_rate,
|
| 151 |
+
"ltv": ltv,
|
| 152 |
+
"top_products": top_products,
|
| 153 |
+
"new_users": new_users,
|
| 154 |
+
"insights": insights,
|
| 155 |
+
"recommendations": recommendations,
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# Endpoints
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.get("/health")
|
| 163 |
+
async def health():
|
| 164 |
+
return {"status": "healthy", "service": "Growth Agent", "port": 8001}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@app.post("/api/v1/growth/analyze", response_model=GrowthResponse)
|
| 168 |
+
async def analyze_growth(request: GrowthRequest):
|
| 169 |
+
"""
|
| 170 |
+
Analyze GA4 traffic + Shopify revenue metrics.
|
| 171 |
+
Always fetches live data via MCP tools using tenant_id.
|
| 172 |
+
"""
|
| 173 |
+
q_str = (request.user_query or "").lower().strip()
|
| 174 |
+
|
| 175 |
+
print(f"[Growth Agent] Query: '{request.user_query}' | Session: {request.session_id} | Tenant: {request.tenant_id}")
|
| 176 |
+
|
| 177 |
+
# Central Asset Ontology & Counter Questioning Engine
|
| 178 |
+
try:
|
| 179 |
+
from backend.SuperAgent.app.services.asset_registry import resolve_counter_question
|
| 180 |
+
cq_answer = resolve_counter_question(
|
| 181 |
+
request.user_query,
|
| 182 |
+
session_id=request.session_id or "default-session",
|
| 183 |
+
agent_name="Growth AI Assistant",
|
| 184 |
+
)
|
| 185 |
+
if cq_answer:
|
| 186 |
+
print(f"[Growth Agent] Asset Ontology / Counter-Questioning triggered")
|
| 187 |
+
return GrowthResponse(
|
| 188 |
+
success=True,
|
| 189 |
+
tenant_id=request.tenant_id,
|
| 190 |
+
session_id=request.session_id,
|
| 191 |
+
answer=cq_answer,
|
| 192 |
+
)
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f"[Growth Agent] Asset Ontology resolution notice: {e}")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# Always fetch Live Data via MCP Tools
|
| 199 |
+
ga4_payload = {}
|
| 200 |
+
shopify_payload = {}
|
| 201 |
+
|
| 202 |
+
if request.tenant_id:
|
| 203 |
+
try:
|
| 204 |
+
print("[Growth Agent] Live fetching GA4 & Shopify data via MCP tools...")
|
| 205 |
+
from backend.SuperAgent.app.mcp.mcp_server import mcp_registry
|
| 206 |
+
want_ga4 = not q_str or any(kw in q_str for kw in ["ga4", "traffic", "user", "users", "session", "bounce", "active", "analysis", "report"])
|
| 207 |
+
want_shopify = not q_str or any(kw in q_str for kw in ["shopify", "sales", "revenue", "order", "orders", "aov", "funnel", "product", "analysis", "report"])
|
| 208 |
+
|
| 209 |
+
if want_ga4:
|
| 210 |
+
ga4_payload = mcp_registry.call_tool("ga4_get_traffic_metrics", tenant_id=request.tenant_id) or {}
|
| 211 |
+
if want_shopify:
|
| 212 |
+
shopify_payload = mcp_registry.call_tool("shopify_get_sales_metrics", tenant_id=request.tenant_id) or {}
|
| 213 |
+
except Exception as e:
|
| 214 |
+
print(f"[Growth Agent] Live fetch error via MCP: {e}")
|
| 215 |
+
|
| 216 |
+
payload = {
|
| 217 |
+
"ga4_data": ga4_payload,
|
| 218 |
+
"shopify_data": shopify_payload,
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
print("[Growth Agent] Processing business logic & metrics...")
|
| 222 |
+
result = process_growth(payload)
|
| 223 |
+
|
| 224 |
+
# Format direct natural language answer if user_query provided
|
| 225 |
+
answer_text = None
|
| 226 |
+
if request.user_query:
|
| 227 |
+
summary_part = result.get("traffic_summary") or result.get("insights") or "No active growth data available."
|
| 228 |
+
recs_part = "\n".join([f"• {r}" for r in result.get("recommendations", [])])
|
| 229 |
+
|
| 230 |
+
is_report = any(rk in q_str for rk in ["report", "executive report", "summary report", "generate report", "download report"])
|
| 231 |
+
header_title = "## Executive Report: Store Growth & Traffic" if is_report else "## Growth Analysis: GA4 Traffic & Shopify Sales"
|
| 232 |
+
|
| 233 |
+
# Build dynamic trend box lines based on what was actually requested/fetched
|
| 234 |
+
trend_lines = []
|
| 235 |
+
if ga4_payload and isinstance(ga4_payload, dict):
|
| 236 |
+
trend_lines.append(f"[GA4 Active Users]: {ga4_payload.get('activeUsers', 0):,} users")
|
| 237 |
+
if "session" in q_str or "traffic" in q_str or not q_str:
|
| 238 |
+
trend_lines.append(f"[GA4 Sessions] : {ga4_payload.get('totalSessions', 0):,} sessions")
|
| 239 |
+
if shopify_payload and isinstance(shopify_payload, dict) and shopify_payload.get("totalRevenue", 0) > 0:
|
| 240 |
+
trend_lines.append(f"[Shopify Revenue] : ${shopify_payload.get('totalRevenue', 0):,.2f}")
|
| 241 |
+
|
| 242 |
+
trend_box = "\n".join(trend_lines) if trend_lines else f"[GA4 Active Users]: {ga4_payload.get('activeUsers', 0) if isinstance(ga4_payload, dict) else 0:,} users"
|
| 243 |
+
|
| 244 |
+
answer_text = (
|
| 245 |
+
f"{header_title}\n\n"
|
| 246 |
+
f"### Executive Summary\n"
|
| 247 |
+
f"{summary_part}\n\n"
|
| 248 |
+
f"### Trend Graph\n"
|
| 249 |
+
f"```\n"
|
| 250 |
+
f"{trend_box}\n"
|
| 251 |
+
f"```\n\n"
|
| 252 |
+
f"### Strategic Recommendations\n"
|
| 253 |
+
f"{recs_part if recs_part else '• Maintain current traffic acquisition strategy.'}"
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
print("[Growth Agent] Response generated successfully.")
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
return GrowthResponse(
|
| 264 |
+
success=True,
|
| 265 |
+
traffic_summary=result.get("traffic_summary", ""),
|
| 266 |
+
aov=result.get("aov", 0.0),
|
| 267 |
+
checkout_conversion_rate=result.get("checkout_conversion_rate", 0.0),
|
| 268 |
+
insights=result.get("insights", ""),
|
| 269 |
+
recommendations=result.get("recommendations", []),
|
| 270 |
+
answer=answer_text,
|
| 271 |
+
tenant_id=request.tenant_id,
|
| 272 |
+
session_id=request.session_id,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
import uvicorn
|
| 280 |
+
uvicorn.run("backend.agents.growth.app.main:app", host="0.0.0.0", port=8001, reload=True)
|
agents/growth/growth_agent.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Growth Agent Specification (`growth_agent.md`)
|
| 2 |
+
|
| 3 |
+
## 1. Identity & System Persona
|
| 4 |
+
* **Name & Designation:** ScaleMate AI Growth & E-Commerce Business Analyst
|
| 5 |
+
* **Role & Domain Scope:** E-Commerce Store Revenue, Google Analytics Traffic, Conversion Funnel, and Customer Acquisition Specialist.
|
| 6 |
+
* **Operational Goal:** Analyzes website traffic (GA4) and store sales (Shopify) to detect conversion drop-offs, optimize average order value (AOV), and provide store growth recommendations.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 2. Business Logics, Control Rules & Directives
|
| 11 |
+
|
| 12 |
+
### A. E-Commerce & Traffic Analytics Rules
|
| 13 |
+
1. **Traffic Analysis (GA4)**:
|
| 14 |
+
- Evaluates `activeUsers`, `totalSessions`, `bounceRate`, and `trafficSources`.
|
| 15 |
+
- **Bounce Rate Rule**: If `bounceRate > 55.0%`, trigger High Bounce Rate Optimization Alert.
|
| 16 |
+
2. **Revenue & Order Analysis (Shopify)**:
|
| 17 |
+
- Calculates `totalRevenue`, `totalOrders`, and `avgOrderValue` (AOV).
|
| 18 |
+
- **Low AOV Rule**: If `avgOrderValue < $50.00`, recommend cross-sell bundles and threshold-based free shipping.
|
| 19 |
+
3. **Checkout Funnel Drop-off Rule**:
|
| 20 |
+
- Calculates `checkout_conversion_rate = (purchases / checkouts) * 100`.
|
| 21 |
+
- **Drop-off Threshold Rule**: If `checkout_conversion_rate < 60.0%`, recommend checkout trust badges, express payment buttons (Apple Pay/Shop Pay), and cart recovery emails.
|
| 22 |
+
|
| 23 |
+
### B. Execution Directives
|
| 24 |
+
1. **In-Memory Analytical Execution**: Processes raw GA4 and Shopify payloads in micro-seconds without external LLM latency.
|
| 25 |
+
2. **Metric Targeting**: Formats traffic summary, store revenue insights, and checkout recommendations based strictly on queried scope.
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## 3. Data Contracts & Schemas
|
| 30 |
+
|
| 31 |
+
### Input Payload (`build_growth_payload`)
|
| 32 |
+
```json
|
| 33 |
+
{
|
| 34 |
+
"ga4_data": {
|
| 35 |
+
"activeUsers": 1250,
|
| 36 |
+
"totalSessions": 3420,
|
| 37 |
+
"bounceRate": 38.5,
|
| 38 |
+
"trafficSources": ["Google Organic", "Direct"]
|
| 39 |
+
},
|
| 40 |
+
"shopify_data": {
|
| 41 |
+
"totalRevenue": 14250.00,
|
| 42 |
+
"totalOrders": 185,
|
| 43 |
+
"avgOrderValue": 77.03,
|
| 44 |
+
"funnel": {
|
| 45 |
+
"sessions": 740,
|
| 46 |
+
"checkouts": 370,
|
| 47 |
+
"purchases": 185
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Output Analytical Result
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"success": true,
|
| 57 |
+
"traffic_summary": "1,250 active users, 3,420 sessions, 38.5% bounce rate.",
|
| 58 |
+
"aov": 77.03,
|
| 59 |
+
"checkout_conversion_rate": 50.0,
|
| 60 |
+
"insights": "Revenue $14,250.00 across 185 orders (AOV: $77.03). Checkout conversion rate: 50.0%, drop-off: 50.0%.",
|
| 61 |
+
"recommendations": [
|
| 62 |
+
"Optimize checkout flow - add trust badges."
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
```
|
agents/marketing/app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (11.5 kB). View file
|
|
|
agents/marketing/app/main.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Marketing Agent — Meta Ads + Leads Microservice (Port 8002)
|
| 3 |
+
Analyzes ad spend, ROAS, CTR, cost-per-lead, and triggers email alerts
|
| 4 |
+
for high-performing or wasted-spend campaigns.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
from fastapi import FastAPI
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from pydantic import BaseModel
|
| 14 |
+
from typing import Any, Optional
|
| 15 |
+
|
| 16 |
+
# Add project root so we can import the email service
|
| 17 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")))
|
| 18 |
+
from backend.SuperAgent.app.services.email_service import check_campaign_alerts, send_alert_email
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
app = FastAPI(
|
| 22 |
+
title="ScaleMate Marketing Agent",
|
| 23 |
+
description="Meta Ads & Leads Analysis Microservice",
|
| 24 |
+
version="1.0.0",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
app.add_middleware(
|
| 28 |
+
CORSMiddleware,
|
| 29 |
+
allow_origins=["*"],
|
| 30 |
+
allow_credentials=True,
|
| 31 |
+
allow_methods=["*"],
|
| 32 |
+
allow_headers=["*"],
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Request / Response Models
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class MarketingRequest(BaseModel):
|
| 41 |
+
user_query: str
|
| 42 |
+
tenant_id: str
|
| 43 |
+
user_id: str
|
| 44 |
+
session_id: Optional[str] = "default-session"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class MarketingResponse(BaseModel):
|
| 48 |
+
success: bool = True
|
| 49 |
+
agent: str = "marketing_agent"
|
| 50 |
+
ad_spend: float = 0.0
|
| 51 |
+
overall_roas: float = 0.0
|
| 52 |
+
cost_per_lead: float = 0.0
|
| 53 |
+
high_performers: list[dict[str, Any]] = []
|
| 54 |
+
wasted_spend_campaigns: list[dict[str, Any]] = []
|
| 55 |
+
email_triggered: bool = False
|
| 56 |
+
email_alert_type: Optional[str] = None
|
| 57 |
+
answer: Optional[str] = None
|
| 58 |
+
tenant_id: Optional[str] = None
|
| 59 |
+
session_id: Optional[str] = None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# Business Logic — Function-Based Analysis
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def analyze_meta_ads(meta: dict[str, Any]) -> dict[str, Any]:
|
| 66 |
+
"""Analyze Meta Ads performance and detect scaling/waste opportunities."""
|
| 67 |
+
spend = meta.get("spend", 0)
|
| 68 |
+
roas = meta.get("roas", 0)
|
| 69 |
+
ctr = meta.get("ctr", 0)
|
| 70 |
+
cpc = meta.get("cpc", 0)
|
| 71 |
+
campaigns = meta.get("campaigns", [])
|
| 72 |
+
|
| 73 |
+
alerts = check_campaign_alerts(campaigns)
|
| 74 |
+
|
| 75 |
+
email_sent = False
|
| 76 |
+
if alerts["triggered"]:
|
| 77 |
+
email_sent = send_alert_email(
|
| 78 |
+
alert_type=alerts["type"],
|
| 79 |
+
high_performers=alerts["high_performers"],
|
| 80 |
+
wasted=alerts["wasted"],
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
"ad_spend": spend,
|
| 85 |
+
"overall_roas": roas,
|
| 86 |
+
"high_performers": alerts["high_performers"],
|
| 87 |
+
"wasted_spend_campaigns": alerts["wasted"],
|
| 88 |
+
"email_triggered": alerts["triggered"],
|
| 89 |
+
"email_alert_type": alerts["type"],
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def analyze_leads(leads: dict[str, Any]) -> dict[str, Any]:
|
| 94 |
+
"""Analyze leads metrics."""
|
| 95 |
+
total = leads.get("totalLeads", 0)
|
| 96 |
+
cpl = leads.get("costPerLead", 0)
|
| 97 |
+
return {"totalLeads": total, "costPerLead": cpl}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def process_marketing(payload: dict[str, Any]) -> dict[str, Any]:
|
| 101 |
+
"""Process Meta Ads + Leads metrics payload with custom CAC & efficiency analysis."""
|
| 102 |
+
meta_data = payload.get("meta_data") or {}
|
| 103 |
+
leads_data = payload.get("leads_data") or {}
|
| 104 |
+
|
| 105 |
+
ad_spend = 0.0
|
| 106 |
+
overall_roas = 0.0
|
| 107 |
+
cost_per_lead = 0.0
|
| 108 |
+
cac = 0.0
|
| 109 |
+
high_performers = []
|
| 110 |
+
wasted = []
|
| 111 |
+
email_triggered = False
|
| 112 |
+
email_type = None
|
| 113 |
+
|
| 114 |
+
if meta_data:
|
| 115 |
+
meta_result = analyze_meta_ads(meta_data)
|
| 116 |
+
ad_spend = meta_result["ad_spend"]
|
| 117 |
+
overall_roas = meta_result["overall_roas"]
|
| 118 |
+
high_performers = meta_result["high_performers"]
|
| 119 |
+
wasted = meta_result["wasted_spend_campaigns"]
|
| 120 |
+
email_triggered = meta_result["email_triggered"]
|
| 121 |
+
email_type = meta_result["email_alert_type"]
|
| 122 |
+
cac = meta_data.get("cac", round(ad_spend / 15, 2) if ad_spend > 0 else 0.0)
|
| 123 |
+
|
| 124 |
+
if leads_data:
|
| 125 |
+
leads_result = analyze_leads(leads_data)
|
| 126 |
+
cost_per_lead = leads_result["costPerLead"]
|
| 127 |
+
|
| 128 |
+
return {
|
| 129 |
+
"success": True,
|
| 130 |
+
"ad_spend": ad_spend,
|
| 131 |
+
"overall_roas": overall_roas,
|
| 132 |
+
"cost_per_lead": cost_per_lead,
|
| 133 |
+
"cac": cac,
|
| 134 |
+
"high_performers": high_performers,
|
| 135 |
+
"wasted_spend_campaigns": wasted,
|
| 136 |
+
"email_triggered": email_triggered,
|
| 137 |
+
"email_alert_type": email_type,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# Endpoints
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@app.get("/health")
|
| 145 |
+
async def health():
|
| 146 |
+
return {"status": "healthy", "service": "Marketing Agent", "port": 8002}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@app.post("/api/v1/marketing/analyze", response_model=MarketingResponse)
|
| 150 |
+
async def analyze_marketing(request: MarketingRequest):
|
| 151 |
+
"""
|
| 152 |
+
Analyze Meta Ads + Leads performance.
|
| 153 |
+
Always fetches live data via MCP tools using tenant_id.
|
| 154 |
+
"""
|
| 155 |
+
q_str = (request.user_query or "").lower().strip()
|
| 156 |
+
|
| 157 |
+
print(f"[Marketing Agent] Query: '{request.user_query}' | Session: {request.session_id} | Tenant: {request.tenant_id}")
|
| 158 |
+
|
| 159 |
+
# Central Asset Ontology & Counter Questioning Engine
|
| 160 |
+
try:
|
| 161 |
+
from backend.SuperAgent.app.services.asset_registry import resolve_counter_question
|
| 162 |
+
cq_answer = resolve_counter_question(
|
| 163 |
+
request.user_query,
|
| 164 |
+
session_id=request.session_id or "default-session",
|
| 165 |
+
agent_name="Marketing AI Assistant",
|
| 166 |
+
)
|
| 167 |
+
if cq_answer:
|
| 168 |
+
print(f"[Marketing Agent] Asset Ontology / Counter-Questioning triggered")
|
| 169 |
+
return MarketingResponse(
|
| 170 |
+
success=True,
|
| 171 |
+
tenant_id=request.tenant_id,
|
| 172 |
+
session_id=request.session_id,
|
| 173 |
+
answer=cq_answer,
|
| 174 |
+
)
|
| 175 |
+
except Exception as e:
|
| 176 |
+
print(f"[Marketing Agent] Asset Ontology resolution notice: {e}")
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# Always fetch Live Data via MCP Tools
|
| 181 |
+
meta_payload = {}
|
| 182 |
+
leads_payload = {}
|
| 183 |
+
|
| 184 |
+
if request.tenant_id:
|
| 185 |
+
try:
|
| 186 |
+
print("[Marketing Agent] Live fetching Meta Ads & Leads data via MCP tools...")
|
| 187 |
+
from backend.SuperAgent.app.mcp.mcp_server import mcp_registry
|
| 188 |
+
want_meta = not q_str or any(kw in q_str for kw in ["meta", "facebook", "instagram", "ad", "ads", "spend", "roas", "campaign", "ctr", "cpc", "analysis", "report"])
|
| 189 |
+
if want_meta:
|
| 190 |
+
meta_payload = mcp_registry.call_tool("meta_get_ad_insights", tenant_id=request.tenant_id) or {}
|
| 191 |
+
except Exception as e:
|
| 192 |
+
print(f"[Marketing Agent] Live fetch error via MCP: {e}")
|
| 193 |
+
|
| 194 |
+
payload = {
|
| 195 |
+
"meta_data": meta_payload,
|
| 196 |
+
"leads_data": leads_payload,
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
print("[Marketing Agent] Processing business logic & metrics...")
|
| 200 |
+
result = process_marketing(payload)
|
| 201 |
+
|
| 202 |
+
# Format direct natural language answer if user_query provided
|
| 203 |
+
answer_text = None
|
| 204 |
+
if request.user_query:
|
| 205 |
+
spend_val = result.get("ad_spend", 0.0)
|
| 206 |
+
roas_val = result.get("overall_roas", 0.0)
|
| 207 |
+
high_camps = ", ".join([c.get("name", "Campaign") for c in result.get("high_performers", [])]) or "None"
|
| 208 |
+
wasted_camps = ", ".join([c.get("name", "Campaign") for c in result.get("wasted_spend_campaigns", [])]) or "None"
|
| 209 |
+
|
| 210 |
+
is_report = any(rk in q_str for rk in ["report", "executive report", "summary report", "generate report", "download report"])
|
| 211 |
+
header_title = "## Executive Report: Meta Ads & Campaign Marketing" if is_report else "## Marketing Analysis: Meta Ads & Lead Efficiency"
|
| 212 |
+
|
| 213 |
+
# Build dynamic trend box lines based on what was actually requested/fetched
|
| 214 |
+
trend_lines = []
|
| 215 |
+
if meta_payload and isinstance(meta_payload, dict):
|
| 216 |
+
if "spend" in q_str or "ad" in q_str or not q_str:
|
| 217 |
+
trend_lines.append(f"[Meta Ad Spend] : ${spend_val:,.2f}")
|
| 218 |
+
if "roas" in q_str or not q_str:
|
| 219 |
+
trend_lines.append(f"[Overall ROAS] : {roas_val:.2f}x")
|
| 220 |
+
if not trend_lines:
|
| 221 |
+
trend_lines.append(f"[Meta Ad Spend] : ${spend_val:,.2f}")
|
| 222 |
+
trend_lines.append(f"[Overall ROAS] : {roas_val:.2f}x")
|
| 223 |
+
|
| 224 |
+
if leads_payload and isinstance(leads_payload, dict) and result.get("cost_per_lead", 0.0) > 0:
|
| 225 |
+
trend_lines.append(f"[Cost Per Lead] : ${result.get('cost_per_lead', 0.0):,.2f}")
|
| 226 |
+
|
| 227 |
+
trend_box = "\n".join(trend_lines) if trend_lines else f"[Meta Ad Spend] : ${spend_val:,.2f}\n[Overall ROAS] : {roas_val:.2f}x"
|
| 228 |
+
|
| 229 |
+
answer_text = (
|
| 230 |
+
f"{header_title}\n\n"
|
| 231 |
+
f"### Executive Summary\n"
|
| 232 |
+
f"Total Ad Spend: ${spend_val:,.2f} with an overall ROAS of {roas_val:.2f}x. "
|
| 233 |
+
f"High-performing campaigns: {high_camps}. Wasted spend campaigns: {wasted_camps}.\n\n"
|
| 234 |
+
f"### Trend Graph\n"
|
| 235 |
+
f"```\n"
|
| 236 |
+
f"{trend_box}\n"
|
| 237 |
+
f"```\n\n"
|
| 238 |
+
f"### Strategic Recommendations\n"
|
| 239 |
+
f"• Scale budget on high performers ({high_camps}).\n"
|
| 240 |
+
f"• Pause or optimize creative angles on wasted spend campaigns ({wasted_camps})."
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
print("[Marketing Agent] Response generated successfully.")
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
return MarketingResponse(
|
| 250 |
+
success=True,
|
| 251 |
+
ad_spend=result.get("ad_spend", 0.0),
|
| 252 |
+
overall_roas=result.get("overall_roas", 0.0),
|
| 253 |
+
cost_per_lead=result.get("cost_per_lead", 0.0),
|
| 254 |
+
high_performers=result.get("high_performers", []),
|
| 255 |
+
wasted_spend_campaigns=result.get("wasted_spend_campaigns", []),
|
| 256 |
+
email_triggered=result.get("email_triggered", False),
|
| 257 |
+
email_alert_type=result.get("email_alert_type"),
|
| 258 |
+
answer=answer_text,
|
| 259 |
+
tenant_id=request.tenant_id,
|
| 260 |
+
session_id=request.session_id,
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
if __name__ == "__main__":
|
| 267 |
+
import uvicorn
|
| 268 |
+
uvicorn.run("backend.agents.marketing.app.main:app", host="0.0.0.0", port=8002, reload=True)
|
agents/marketing/marketing_agent.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Marketing Agent Specification (`marketing_agent.md`)
|
| 2 |
+
|
| 3 |
+
## 1. Identity & System Persona
|
| 4 |
+
* **Name & Designation:** ScaleMate AI Performance Marketing & Paid Ads Strategist
|
| 5 |
+
* **Role & Domain Scope:** Meta Ads (Facebook & Instagram), Lead Generation, Ad Spend Optimization, and Campaign Performance Specialist.
|
| 6 |
+
* **Operational Goal:** Monitors paid ad performance, identifies high-performing campaigns, flags wasted spend, calculates cost-per-lead (CPL), and triggers automated SMTP email alerts.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 2. Business Logics, Control Rules & Directives
|
| 11 |
+
|
| 12 |
+
### A. Performance Marketing Threshold Rules
|
| 13 |
+
1. **Wasted Spend Alert Rule**:
|
| 14 |
+
- If campaign `ROAS < 2.0x` and `spend >= $100.00`, flag campaign as **Wasted Spend Campaign** (`wasted_spend_campaigns`).
|
| 15 |
+
2. **High Performing Campaign Rule**:
|
| 16 |
+
- If campaign `ROAS >= 4.0x`, flag campaign as **High Performer** (`high_performers`).
|
| 17 |
+
3. **Cost Per Lead (CPL) Rule**:
|
| 18 |
+
- If lead campaign `Cost Per Lead (CPL) > $15.00`, trigger High Lead Cost Alert.
|
| 19 |
+
|
| 20 |
+
### B. Automated Email Alert Trigger Rules
|
| 21 |
+
1. **Email Alert Rule**: If `wasted_spend_campaigns` count $\ge 1$, set `email_triggered = True` and `email_alert_type = 'WASTED_SPEND'`.
|
| 22 |
+
2. **Automated SMTP Dispatch**: Send alert email via `send_alert_email()` to `ALERT_EMAIL_TO` detailing wasted spend campaigns and recommended budget re-allocation to high performers.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 3. Data Contracts & Schemas
|
| 27 |
+
|
| 28 |
+
### Input Payload (`build_marketing_payload`)
|
| 29 |
+
```json
|
| 30 |
+
{
|
| 31 |
+
"meta_data": {
|
| 32 |
+
"spend": 3200.00,
|
| 33 |
+
"impressions": 125000,
|
| 34 |
+
"clicks": 4150,
|
| 35 |
+
"ctr": 3.32,
|
| 36 |
+
"cpc": 0.77,
|
| 37 |
+
"roas": 4.45,
|
| 38 |
+
"campaigns": [
|
| 39 |
+
{ "name": "Prospecting Top Funnel", "spend": 1200.00, "roas": 4.80, "status": "ACTIVE" },
|
| 40 |
+
{ "name": "Broad Audience Retargeting", "spend": 500.00, "roas": 1.20, "status": "ACTIVE" }
|
| 41 |
+
]
|
| 42 |
+
},
|
| 43 |
+
"leads_data": {
|
| 44 |
+
"totalLeads": 145,
|
| 45 |
+
"costPerLead": 8.50
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Output Analytical Result
|
| 51 |
+
```json
|
| 52 |
+
{
|
| 53 |
+
"success": true,
|
| 54 |
+
"ad_spend": 3200.00,
|
| 55 |
+
"overall_roas": 4.45,
|
| 56 |
+
"cost_per_lead": 8.50,
|
| 57 |
+
"high_performers": [
|
| 58 |
+
{ "name": "Prospecting Top Funnel", "spend": 1200.00, "roas": 4.80 }
|
| 59 |
+
],
|
| 60 |
+
"wasted_spend_campaigns": [
|
| 61 |
+
{ "name": "Broad Audience Retargeting", "spend": 500.00, "roas": 1.20 }
|
| 62 |
+
],
|
| 63 |
+
"email_triggered": true,
|
| 64 |
+
"email_alert_type": "WASTED_SPEND"
|
| 65 |
+
}
|
| 66 |
+
```
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn==0.30.6
|
| 3 |
+
langgraph==0.2.28
|
| 4 |
+
langchain-groq==0.2.0
|
| 5 |
+
langchain-core==0.3.10
|
| 6 |
+
pydantic==2.9.2
|
| 7 |
+
python-dotenv==1.0.1
|
| 8 |
+
httpx==0.27.2
|
| 9 |
+
aiosmtplib==3.0.1
|
| 10 |
+
matplotlib>=3.8.0
|
| 11 |
+
reportlab>=4.0.0
|
run_all_agents.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
run_all_agents.py — Unified Agent Platform Launcher
|
| 3 |
+
Launches the ScaleMate Executive SuperAgent Multi-Agent Platform on Port 8000.
|
| 4 |
+
Interactive Swagger API Docs: http://localhost:8000/docs
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
if os.getenv("SPACE_ID"):
|
| 13 |
+
port = int(os.getenv("PORT", "7860"))
|
| 14 |
+
else:
|
| 15 |
+
port = int(os.getenv("PORT", "8000"))
|
| 16 |
+
|
| 17 |
+
print(f"Starting ScaleMate Multi-Agent System on Port {port}...\n")
|
| 18 |
+
print(f" Server URL: http://localhost:{port}")
|
| 19 |
+
print(f" Interactive API Docs: http://localhost:{port}/docs\n")
|
| 20 |
+
|
| 21 |
+
backend_dir = os.path.abspath(os.path.dirname(__file__))
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
proc = subprocess.Popen(
|
| 25 |
+
[
|
| 26 |
+
sys.executable, "-m", "uvicorn",
|
| 27 |
+
"backend.SuperAgent.main:app",
|
| 28 |
+
"--host", "0.0.0.0",
|
| 29 |
+
"--port", str(port),
|
| 30 |
+
"--reload",
|
| 31 |
+
],
|
| 32 |
+
cwd=os.path.abspath(os.path.join(backend_dir, "..")),
|
| 33 |
+
)
|
| 34 |
+
proc.wait()
|
| 35 |
+
|
| 36 |
+
except KeyboardInterrupt:
|
| 37 |
+
print("\nShutting down Multi-Agent System...")
|
| 38 |
+
proc.terminate()
|
| 39 |
+
print("Server stopped.\n")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
main()
|