mktgtech commited on
Commit
dd3fe6b
·
verified ·
1 Parent(s): 07f6c1a

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -474
app.py DELETED
@@ -1,474 +0,0 @@
1
- import os
2
- import time
3
- import requests
4
- import json
5
- import pandas as pd
6
- import gradio as gr
7
- import plotly.express as px
8
- from datetime import date, timedelta
9
-
10
- # =====================================================
11
- # CONFIG / SECRETS
12
- # =====================================================
13
- API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
14
- APP_PASSWORD = os.getenv("APP_PASSWORD")
15
-
16
- if not API_TOKEN or not APP_PASSWORD:
17
- print("⚠️ WARNING: Secrets missing. App will launch but API calls will fail.")
18
- API_TOKEN = "PLACEHOLDER"
19
- APP_PASSWORD = "password"
20
-
21
- ACCOUNT_ID = "255333"
22
- BASE_URL = "https://api.leadfeeder.com"
23
- PAGE_SIZE = 100
24
- HEADERS = {
25
- "Authorization": f"Token token={API_TOKEN}",
26
- "Accept": "application/json"
27
- }
28
- CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
29
-
30
- # =====================================================
31
- # 1. CAMPAIGN MANAGER & LOGIC
32
- # =====================================================
33
- DEFAULT_CAMPAIGNS = {
34
- "MS Tech": [
35
- "microsoft", "power", "dynamics", "crm", "365", "xamarin", "sharepoint",
36
- "dot", ".net", "asp.net", "azure", "copilot", "real-estate-website-development",
37
- "manufacturing-production", "legacy-hrms", "healthcare-appointment", "eld-fleet",
38
- "legacy-erp", "clinical-trial", "digital-e-learning", "bi-analytics",
39
- "application-modernization", "data-warehouse", "workforce", "contingent"
40
- ],
41
- "Fintech": [
42
- "fintech", "finance", "money", "credit", "debit", "card", "invest", "bank",
43
- "wallet", "account", "bookkeeping", "payment", "loan", "nfc", "lend", "debt",
44
- "tax", "invoice", "quot", "borrow", "bill", "coin", "currency", "aml", "kyc",
45
- "blockchain", "cryptocurrency", "budget", "expense", "wealth", "sox", "sarbanes"
46
- ],
47
- "Adtech": [
48
- "programmatic", "publisher", "media", "entertainment", "trading", "ott",
49
- "email", "news", "social", "marketing", "video", "audio", "radio", "livestream",
50
- "bid", "anime", "dooh", "vod", "rtb", "supply-side-platform", "demand-side-platform",
51
- "data-management-platform"
52
- ]
53
- }
54
-
55
- def load_campaign_rules():
56
- if os.path.exists(CAMPAIGN_CONFIG_FILE):
57
- try:
58
- with open(CAMPAIGN_CONFIG_FILE, "r") as f:
59
- return json.load(f)
60
- except:
61
- return DEFAULT_CAMPAIGNS
62
- return DEFAULT_CAMPAIGNS
63
-
64
- def categorize_quality(score):
65
- if pd.isna(score): return "Unknown"
66
- try:
67
- s = int(score)
68
- if 8 <= s <= 10: return "High Quality (8-10)"
69
- if 5 <= s <= 7: return "Mid Quality (5-7)"
70
- if 1 <= s <= 4: return "Low Quality (1-4)"
71
- return "Low Quality (0)"
72
- except:
73
- return "Unknown"
74
-
75
- def get_campaign_match(text, rules):
76
- if not text or not isinstance(text, str):
77
- return None
78
- text_lower = text.lower()
79
- for campaign_name, keywords in rules.items():
80
- for kw in keywords:
81
- if kw.lower() in text_lower:
82
- return campaign_name
83
- return None
84
-
85
- def apply_business_logic(df):
86
- if df is None or df.empty: return df
87
-
88
- if "lead_quality_score" in df.columns:
89
- df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
90
-
91
- rules = load_campaign_rules()
92
-
93
- def resolve_campaign(row):
94
- path = row.get("landing_page_path")
95
- # Strictly check landing page to avoid generic categorizations
96
- if path and path not in ["", "/", "/home"]:
97
- match = get_campaign_match(path, rules)
98
- if match:
99
- return match
100
- return "Uncategorized"
101
-
102
- df["Campaign"] = df.apply(resolve_campaign, axis=1)
103
- return df
104
-
105
- # =====================================================
106
- # 2. PRESETS (Original structure preserved)
107
- # =====================================================
108
- DASHBOARD_PRESETS = {
109
- # --- Added New Presets ---
110
- "Visits by Campaign": ("Campaign", "total_visits", "sum"),
111
- "Leads by Campaign": ("Campaign", "company_name", "count"),
112
- "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
113
- "Visits by City": ("city", "total_visits", "sum"),
114
- "Leads by City": ("city", "company_name", "count"),
115
- "Leads by Country": ("country", "company_name", "count"),
116
- "Leads by Industry": ("primary_industry", "company_name", "count"),
117
-
118
- # --- Original Presets ---
119
- "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
120
- "Total Visits by Country": ("country", "total_visits", "sum"),
121
- "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
122
- "Top Accounts by Visits": ("company_name", "total_visits", "sum"),
123
- "Companies by Country": ("country", "company_name", "count"),
124
- "Accounts by Assignee": ("assignee", "company_name", "count"),
125
- "Visits by Assignee": ("assignee", "total_visits", "sum"),
126
- }
127
-
128
- TREND_PRESETS = {
129
- # --- Added New Presets ---
130
- "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
131
- "Leads Trend by Campaign": ("company_name", "count", "Campaign"),
132
- "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
133
- "Visits Trend by City": ("total_visits", "sum", "city"),
134
- "Leads Trend by City": ("company_name", "count", "city"),
135
- "Leads Trend by Country": ("company_name", "count", "country"),
136
- "Leads Trend by Industry": ("company_name", "count", "primary_industry"),
137
-
138
- # --- Original Presets ---
139
- "Total Visits Trend": ("total_visits", "sum", None),
140
- "Active Accounts Trend": ("company_name", "count", None),
141
- "Avg Lead Quality Trend": ("lead_quality_score", "mean", None),
142
- "Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
143
- "Visits Trend by Country": ("total_visits", "sum", "country"),
144
- "Visits Trend by Assignee": ("total_visits", "sum", "assignee"),
145
- }
146
-
147
- # =====================================================
148
- # 3. API HANDLING (Original structure preserved)
149
- # =====================================================
150
- def make_request(url, params=None):
151
- retries = 3
152
- while retries > 0:
153
- r = requests.get(url, headers=HEADERS, params=params, timeout=45)
154
- if r.status_code == 429:
155
- time.sleep(61)
156
- retries -= 1
157
- continue
158
- r.raise_for_status()
159
- return r.json()
160
- raise Exception("Max retries exceeded")
161
-
162
- def fetch_basic_leads(start_date, end_date):
163
- page = 1
164
- rows = []
165
- print(f"🚀 Fetching full company list for {start_date} to {end_date}...")
166
- while True:
167
- try:
168
- js = make_request(
169
- f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads",
170
- params={"start_date": start_date, "end_date": end_date, "page[number]": page, "page[size]": PAGE_SIZE, "include": "location"}
171
- )
172
- data = js.get("data", [])
173
- if not data: break
174
-
175
- included = js.get("included", [])
176
- loc_map = {str(i["id"]): i["attributes"] for i in included if i["type"] == "locations"}
177
-
178
- for lead in data:
179
- a = lead["attributes"]
180
- loc_id = lead.get("relationships", {}).get("location", {}).get("data", {}).get("id")
181
- loc = loc_map.get(str(loc_id), {})
182
-
183
- rows.append({
184
- "lead_id": lead.get("id"),
185
- "company_name": a.get("name"),
186
- "website_url": a.get("website_url"),
187
- "phone": a.get("phone"),
188
- "business_id": a.get("business_id"),
189
- "primary_industry": a.get("industry"),
190
- "all_industries": ", ".join([i.get("name") for i in a.get("industries", [])]) if a.get("industries") else None,
191
- "first_visit_date": a.get("first_visit_date"),
192
- "last_visit_date": a.get("last_visit_date"),
193
- "total_visits": a.get("visits"),
194
- "lead_quality_score": a.get("quality"),
195
- "revenue": a.get("revenue"),
196
- "employee_count": a.get("employee_count"),
197
- "employees_min": a.get("employees_range", {}).get("min") if a.get("employees_range") else None,
198
- "employees_max": a.get("employees_range", {}).get("max") if a.get("employees_range") else None,
199
- "assignee": a.get("assignee"),
200
- "emailed_to": a.get("emailed_to"),
201
- "crm_lead_id": a.get("crm_lead_id"),
202
- "crm_organization_id": a.get("crm_organization_id"),
203
- "tags": ", ".join(a.get("tags", [])) if a.get("tags") else None,
204
- "linkedin_url": a.get("linkedin_url"),
205
- "twitter_handle": a.get("twitter_handle"),
206
- "facebook_url": a.get("facebook_url"),
207
- "country": loc.get("country"),
208
- "region": loc.get("region"),
209
- "city": loc.get("city"),
210
- "leadfeeder_url": a.get("view_in_leadfeeder"),
211
- "landing_page_path": None,
212
- "exit_page_path": None
213
- })
214
- print(f"✅ Page {page} loaded. Rows: {len(rows)}")
215
- page += 1
216
- except Exception as e:
217
- print(f"Error on page {page}: {e}")
218
- break
219
-
220
- df = pd.DataFrame(rows)
221
- if not df.empty:
222
- df["last_visit_date"] = pd.to_datetime(df["last_visit_date"], errors="coerce")
223
- return df
224
-
225
- def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
226
- if df.empty: return df
227
- target_df = df.head(max_rows) if max_rows else df
228
- total = len(target_df)
229
-
230
- print(f"🕵️ Deep enriching {total} rows ({start_date} to {end_date})...")
231
- for index, row in target_df.iterrows():
232
- if row.get("landing_page_path"): continue
233
- lead_id = row["lead_id"]
234
- try:
235
- visit_data = make_request(
236
- f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
237
- params={"start_date": start_date, "end_date": end_date, "page[size]": 1, "include": "page_views"}
238
- )
239
- visits = visit_data.get("data", [])
240
- included = visit_data.get("included", [])
241
- landing = None
242
- exit_p = None
243
-
244
- if visits:
245
- visit = visits[0]
246
- v_attrs = visit.get("attributes", {})
247
- landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
248
-
249
- visit_route = v_attrs.get("visit_route", [])
250
- if visit_route and isinstance(visit_route, list):
251
- last_step = visit_route[-1]
252
- if not exit_p: exit_p = last_step.get("page_path") or last_step.get("page_url")
253
- if not landing:
254
- first_step = visit_route[0]
255
- landing = first_step.get("page_path") or first_step.get("page_url")
256
-
257
- if not landing or not exit_p:
258
- pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
259
- pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
260
- if pv_ids:
261
- if not landing:
262
- first_pv = pv_map.get(pv_ids[0])
263
- if first_pv: landing = first_pv.get("url") or first_pv.get("path")
264
- if not exit_p:
265
- last_pv = pv_map.get(pv_ids[-1])
266
- if last_pv: exit_p = last_pv.get("url") or last_pv.get("path")
267
-
268
- df.at[index, "landing_page_path"] = landing
269
- df.at[index, "exit_page_path"] = exit_p
270
- except Exception as e:
271
- print(f"Failed to enrich lead {lead_id}: {e}")
272
-
273
- if max_rows and index % 5 == 0:
274
- progress(index / total, desc="Enriching...")
275
- return df
276
-
277
- # =====================================================
278
- # 4. WRAPPERS
279
- # =====================================================
280
- def load_preview(start, end):
281
- df = fetch_basic_leads(start, end)
282
- if df.empty: return df, pd.DataFrame(), "⚠️ No data found."
283
- df_preview = df.copy()
284
- df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=50)
285
- df_preview = apply_business_logic(df_preview)
286
- return df, df_preview.head(50), f"✅ Loaded {len(df):,} companies. Preview top 50."
287
-
288
- def download_full_excel(df, start, end):
289
- if df is None or df.empty: return None
290
- print("⏳ Starting full enrichment for Excel export...")
291
- enriched_df = enrich_leads_with_visits(df.copy(), start, end)
292
- final_df = apply_business_logic(enriched_df)
293
- path = "/tmp/leadfeeder_campaign_data.xlsx"
294
- final_df.to_excel(path, index=False)
295
- return path
296
-
297
- def inspect_raw_json(lead_id, start_date, end_date):
298
- if not lead_id: return "Please enter a Lead ID"
299
- try:
300
- url = f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits"
301
- params = {"start_date": start_date, "end_date": end_date, "page[size]": 1, "include": "page_views"}
302
- r = requests.get(url, headers=HEADERS, params=params)
303
- return json.dumps(r.json(), indent=2)
304
- except Exception as e:
305
- return str(e)
306
-
307
- # =====================================================
308
- # 5. CHART ENGINES (Restored to exact original logic)
309
- # =====================================================
310
- def build_kpis(df):
311
- if df is None or df.empty: return 0, 0, 0, 0, 0, 0
312
- return (
313
- len(df),
314
- df["total_visits"].gt(0).sum(),
315
- int(df["total_visits"].sum()),
316
- round(df["lead_quality_score"].mean(), 2),
317
- round(df["crm_organization_id"].notna().mean() * 100, 1),
318
- round(df["linkedin_url"].notna().mean() * 100, 1),
319
- )
320
-
321
- def build_dashboard(df, preset, top_n):
322
- if df is None or df.empty: return px.bar(title="No Data")
323
- if "Campaign" not in df.columns: df = apply_business_logic(df)
324
-
325
- dim, metric, agg = DASHBOARD_PRESETS[preset]
326
- if agg == "count":
327
- grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
328
- else:
329
- grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
330
- return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset)
331
-
332
- def build_trend(df, preset, grain, filter_values):
333
- if df is None or df.empty: return px.line(title="No Data Loaded")
334
- if "Campaign" not in df.columns: df = apply_business_logic(df)
335
-
336
- metric, agg, segment = TREND_PRESETS[preset]
337
- df = df.dropna(subset=["last_visit_date"]).copy()
338
-
339
- if segment and filter_values:
340
- df = df[df[segment].isin(filter_values)]
341
- if df.empty: return px.line(title="No data for selected filters")
342
-
343
- if grain == "Weekly":
344
- df["period"] = df["last_visit_date"].dt.to_period("W").astype(str)
345
- elif grain == "Monthly":
346
- df["period"] = df["last_visit_date"].dt.to_period("M").astype(str)
347
- else:
348
- df["period"] = df["last_visit_date"].dt.date
349
-
350
- val = metric
351
- if agg == "count":
352
- df["_v"] = df[metric].notna().astype(int)
353
- val = "_v"
354
-
355
- if segment:
356
- ts = df.groupby(["period", segment])[val].agg(agg).reset_index()
357
- title = f"{preset} (Filtered)" if filter_values else preset
358
- return px.line(ts, x="period", y=val, color=segment, template="plotly_white", title=title)
359
-
360
- ts = df.groupby("period")[val].agg(agg).reset_index()
361
- return px.line(ts, x="period", y=val, markers=True, template="plotly_white", title=preset)
362
-
363
- def get_trend_filter_options(df, preset):
364
- if df is None or df.empty: return gr.update(choices=[], value=None, visible=False)
365
- if "Campaign" not in df.columns: df = apply_business_logic(df)
366
-
367
- metric, agg, segment = TREND_PRESETS[preset]
368
- if not segment: return gr.update(choices=[], value=None, visible=False)
369
-
370
- options = sorted(df[segment].astype(str).unique().tolist())
371
- top_5 = df[segment].value_counts().head(5).index.tolist()
372
- return gr.update(choices=options, value=top_5, visible=True, label=f"Filter {segment}")
373
-
374
- # =====================================================
375
- # 6. UI LAYOUT
376
- # =====================================================
377
- with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
378
- gr.Markdown("## 🚀 Leadfeeder Analytics & Campaign Manager")
379
-
380
- with gr.Row():
381
- pwd = gr.Textbox(type="password", label="App Password")
382
- gr.Button("Auth").click(lambda p: gr.Info("Success") if p==APP_PASSWORD else gr.Error("Invalid"), pwd, None)
383
- df_state = gr.State()
384
- status = gr.Markdown()
385
-
386
- with gr.Tabs():
387
- # --- TAB 1: DATA ---
388
- with gr.Tab("📋 Data & Report"):
389
- with gr.Row():
390
- start = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
391
- end = gr.Textbox(label="End Date", value=date.today().isoformat())
392
- with gr.Row():
393
- btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
394
- btn_dl = gr.Button("2. Enrich & Download Full Excel")
395
- file_dl = gr.File(label="Download Excel")
396
-
397
- table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True)
398
- btn_load.click(load_preview, [start, end], [df_state, table, status])
399
- btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
400
-
401
- # --- TAB 2: DASHBOARD ---
402
- with gr.Tab("📊 Dashboard"):
403
- kpis = [gr.Number(label=l) for l in ["Total Companies", "Active Companies", "Total Visits", "Avg Quality", "CRM %", "LinkedIn %"]]
404
- gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
405
-
406
- with gr.Row():
407
- preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Visits by Campaign")
408
- top_n = gr.Slider(5, 50, value=10, label="Top N Items")
409
- chart = gr.Plot()
410
- gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
411
-
412
- # --- TAB 3: TRENDS ---
413
- with gr.Tab("📈 Trends"):
414
- with gr.Row():
415
- trend_view = gr.Dropdown(choices=list(TREND_PRESETS.keys()), label="Select Trend", value="Visits Trend by Campaign")
416
- grain = gr.Radio(["Daily", "Weekly", "Monthly"], value="Daily", label="Granularity")
417
- filter_dropdown = gr.Dropdown(multiselect=True, visible=False, label="Filter Segments")
418
- plot = gr.Plot()
419
- trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
420
- gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
421
-
422
- # --- TAB 4: SETTINGS ---
423
- with gr.Tab("⚙️ Campaign Settings"):
424
- gr.Markdown("### Manage Campaign Groups")
425
- gr.Markdown("Select a campaign below and paste your keywords separated by commas.")
426
-
427
- init_rules = load_campaign_rules()
428
- camp_choices = list(init_rules.keys()) + ["+ Create New Campaign"]
429
-
430
- with gr.Row():
431
- camp_dropdown = gr.Dropdown(choices=camp_choices, label="Select Campaign to Edit", value=camp_choices[0])
432
- new_camp_name = gr.Textbox(label="New Campaign Name", visible=False)
433
-
434
- kw_input = gr.Textbox(label="Keywords (comma separated)", lines=5, value=", ".join(init_rules.get(camp_choices[0], [])))
435
-
436
- def update_ui_on_select(selected_camp):
437
- rules = load_campaign_rules()
438
- if selected_camp == "+ Create New Campaign":
439
- return gr.update(visible=True, value=""), gr.update(value="")
440
- else:
441
- kws = rules.get(selected_camp, [])
442
- return gr.update(visible=False), gr.update(value=", ".join(kws))
443
-
444
- camp_dropdown.change(update_ui_on_select, inputs=[camp_dropdown], outputs=[new_camp_name, kw_input])
445
-
446
- save_config_btn = gr.Button("💾 Save Configuration", variant="primary")
447
- config_status = gr.Markdown()
448
-
449
- def save_easy_config(selected_camp, new_name, kw_string):
450
- rules = load_campaign_rules()
451
- raw_kws = kw_string.split(",")
452
- clean_kws = [k.strip().lower() for k in raw_kws if k.strip()]
453
- target_camp = new_name.strip() if selected_camp == "+ Create New Campaign" else selected_camp
454
- if not target_camp:
455
- return "❌ Error: Campaign name cannot be empty.", gr.update()
456
-
457
- rules[target_camp] = clean_kws
458
- with open(CAMPAIGN_CONFIG_FILE, "w") as f:
459
- json.dump(rules, f, indent=4)
460
-
461
- updated_choices = list(rules.keys()) + ["+ Create New Campaign"]
462
- return f"✅ Saved successfully! Updated keywords for '{target_camp}'.", gr.update(choices=updated_choices, value=target_camp)
463
-
464
- save_config_btn.click(save_easy_config, inputs=[camp_dropdown, new_camp_name, kw_input], outputs=[config_status, camp_dropdown])
465
-
466
- # --- TAB 5: DEBUGGER ---
467
- with gr.Tab("🛠️ Debugger"):
468
- dbg_id = gr.Textbox(label="Lead ID")
469
- dbg_btn = gr.Button("Inspect Raw JSON")
470
- dbg_out = gr.Code(language="json")
471
- dbg_btn.click(inspect_raw_json, [dbg_id, start, end], dbg_out)
472
-
473
- if __name__ == "__main__":
474
- demo.launch(server_name="0.0.0.0", server_port=7860)