ak0601 commited on
Commit
4202680
·
verified ·
1 Parent(s): 8f2bd72

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +288 -190
app.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  import json
3
  import time
4
  from datetime import datetime
5
- from typing import List, Dict, Any, Optional
6
  from pydantic import BaseModel, Field, EmailStr, validator
7
  from fastapi import FastAPI, HTTPException, Query, Depends, Request
8
  from fastapi.responses import JSONResponse, Response
@@ -23,7 +23,7 @@ except ImportError:
23
  load_dotenv()
24
 
25
  # Configuration
26
- SMARTLEAD_API_KEY = os.getenv("SMARTLEAD_API_KEY")
27
  SMARTLEAD_BASE_URL = "https://server.smartlead.ai/api/v1"
28
 
29
  # Initialize FastAPI app
@@ -74,13 +74,26 @@ class LeadInput(BaseModel):
74
  first_name: Optional[str] = Field(None, description="Lead's first name")
75
  last_name: Optional[str] = Field(None, description="Lead's last name")
76
  email: str = Field(..., description="Lead's email address")
77
- phone_number: Optional[str] = Field(None, description="Lead's phone number")
78
  company_name: Optional[str] = Field(None, description="Lead's company name")
79
  website: Optional[str] = Field(None, description="Lead's website")
80
  location: Optional[str] = Field(None, description="Lead's location")
81
- custom_fields: Optional[Dict[str, str]] = Field(None, description="Custom fields as key-value pairs")
82
  linkedin_profile: Optional[str] = Field(None, description="Lead's LinkedIn profile URL")
83
  company_url: Optional[str] = Field(None, description="Company website URL")
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  class LeadSettings(BaseModel):
86
  ignore_global_block_list: bool = Field(True, description="Ignore leads if they are in the global block list")
@@ -124,54 +137,82 @@ class GenerateSequencesRequest(BaseModel):
124
  job_description: str = Field(..., description="Job description to generate sequences for")
125
 
126
  class Campaign(BaseModel):
127
- id: int = Field(..., description="The unique ID of the campaign")
128
- user_id: int = Field(..., description="The ID of the user who owns the campaign")
129
- created_at: datetime = Field(..., description="Timestamp of when the campaign was created")
130
- updated_at: datetime = Field(..., description="Timestamp of the last update to the campaign")
131
- status: str = Field(..., description="Current status of the campaign (DRAFTED, ACTIVE, COMPLETED, STOPPED, PAUSED)")
132
- name: str = Field(..., description="The name of the campaign")
133
- track_settings: str = Field(..., description="Tracking settings string (e.g., 'DONT_REPLY_TO_AN_EMAIL')")
134
- scheduler_cron_value: Optional[str] = Field(None, description="Scheduling details as JSON string")
135
- min_time_btwn_emails: int = Field(..., description="Minimum time between emails in minutes")
136
- max_leads_per_day: int = Field(..., description="Maximum number of leads to process per day")
137
- stop_lead_settings: str = Field(..., description="Settings for stopping leads (REPLY_TO_AN_EMAIL, CLICK_ON_ANY_LINK, etc.)")
138
- enable_ai_esp_matching: bool = Field(..., description="Indicates if AI ESP matching is enabled")
139
- send_as_plain_text: bool = Field(..., description="Indicates if emails for this campaign are sent as plain text")
140
- follow_up_percentage: str = Field(..., description="The percentage of follow-up emails allocated (e.g., '40%')")
141
- unsubscribe_text: Optional[str] = Field(None, description="The text used for the unsubscribe link")
142
- parent_campaign_id: Optional[int] = Field(None, description="Parent campaign ID if this is a child campaign")
143
- client_id: Optional[int] = Field(None, description="The ID of the client associated with the campaign (null if not attached to a client)")
144
-
145
- @validator('scheduler_cron_value', pre=True)
146
- def parse_scheduler_cron_value(cls, v):
147
- """Parse scheduler_cron_value from JSON string to dict if needed"""
148
- if isinstance(v, str) and v:
149
- try:
150
- import json
151
- return json.loads(v)
152
- except json.JSONDecodeError:
153
- return v
154
- return v
155
-
156
- @validator('follow_up_percentage', pre=True)
157
- def parse_follow_up_percentage(cls, v):
158
- """Ensure follow_up_percentage is returned as string with % sign"""
159
- if isinstance(v, int):
160
- return f"{v}%"
161
- return v
162
 
163
  class Lead(BaseModel):
164
  id: int
165
  email: EmailStr
166
- first_name: Optional[str]
167
- last_name: Optional[str]
168
- company: Optional[str]
169
- position: Optional[str]
170
- phone_number: Optional[str]
171
- linkedin_url: Optional[str]
172
- status: Optional[str]
173
-
174
- # Additional models for new endpoints
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  class LeadCategoryUpdateRequest(BaseModel):
176
  category_id: int = Field(..., description="Category ID to assign to the lead")
177
  pause_lead: bool = Field(False, description="Whether to pause the lead after category update")
@@ -237,61 +278,19 @@ async def call_smartlead_api(method: str, endpoint: str, data: Any = None, param
237
  resp = await client.request(method, url, params=params, json=data)
238
 
239
  if resp.status_code >= 400:
240
- # Try to parse error response as JSON for better error handling
241
  try:
242
  error_data = resp.json()
243
  error_message = error_data.get('message', error_data.get('error', 'Unknown error'))
244
- error_detail = error_data.get('detail', error_data.get('description', ''))
245
-
246
- raise HTTPException(
247
- status_code=resp.status_code,
248
- detail={
249
- "error": True,
250
- "message": error_message,
251
- "detail": error_detail,
252
- "endpoint": endpoint,
253
- "method": method,
254
- "status_code": resp.status_code
255
- }
256
- )
257
  except (ValueError, KeyError):
258
- # Fallback to raw text if JSON parsing fails
259
- raise HTTPException(
260
- status_code=resp.status_code,
261
- detail={
262
- "error": True,
263
- "message": "API request failed",
264
- "detail": resp.text,
265
- "endpoint": endpoint,
266
- "method": method,
267
- "status_code": resp.status_code
268
- }
269
- )
270
 
271
  return resp.json()
272
 
273
  except httpx.TimeoutException:
274
- raise HTTPException(
275
- status_code=408,
276
- detail={
277
- "error": True,
278
- "message": "Request timeout",
279
- "detail": "The request to Smartlead API timed out",
280
- "endpoint": endpoint,
281
- "method": method
282
- }
283
- )
284
  except httpx.RequestError as e:
285
- raise HTTPException(
286
- status_code=503,
287
- detail={
288
- "error": True,
289
- "message": "Service unavailable",
290
- "detail": f"Failed to connect to Smartlead API: {str(e)}",
291
- "endpoint": endpoint,
292
- "method": method
293
- }
294
- )
295
 
296
  # ============================================================================
297
  # CAMPAIGN ENDPOINTS
@@ -302,7 +301,7 @@ async def create_campaign(campaign: CreateCampaignRequest):
302
  """Create a new campaign in Smartlead"""
303
  return await call_smartlead_api("POST", "campaigns/create", data=campaign.dict())
304
 
305
- @app.get("/campaigns", response_model=Dict[str, Any], tags=["Campaigns"])
306
  async def list_campaigns():
307
  """Fetch all campaigns from Smartlead API"""
308
  campaigns = await call_smartlead_api("GET", "campaigns")
@@ -376,13 +375,39 @@ async def get_campaign_leads(campaign_id: int, offset: int = 0, limit: int = 100
376
 
377
  @app.post("/campaigns/{campaign_id}/leads", response_model=Dict[str, Any], tags=["Leads"])
378
  async def add_leads_to_campaign(campaign_id: int, request: AddLeadsRequest):
379
- """Add leads to a campaign by ID"""
380
  request_data = request.dict()
381
 
382
- # Clean up the data - remove None values and empty strings
383
  for lead in request_data.get("lead_list", []):
384
  lead_cleaned = {k: v for k, v in lead.items() if v is not None and v != ""}
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  if "custom_fields" in lead_cleaned:
387
  custom_fields = lead_cleaned["custom_fields"]
388
  if custom_fields:
@@ -400,22 +425,18 @@ async def add_leads_to_campaign(campaign_id: int, request: AddLeadsRequest):
400
  request_data["lead_list"] = [lead for lead in request_data["lead_list"] if lead]
401
 
402
  if not request_data["lead_list"]:
403
- raise HTTPException(status_code=400, detail="No valid leads to add. Please provide at least one lead with an email address.")
404
 
405
  if "settings" not in request_data or request_data["settings"] is None:
406
- request_data["settings"] = {
407
- "ignore_global_block_list": True,
408
- "ignore_unsubscribe_list": True,
409
- "ignore_duplicate_leads_in_other_campaign": False
410
- }
411
 
412
  return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads", data=request_data)
413
 
414
  @app.post("/campaigns/{campaign_id}/leads/bulk", response_model=Dict[str, Any], tags=["Leads"])
415
  async def add_bulk_leads(campaign_id: int, leads: List[LeadInput]):
416
- """Add multiple leads to a Smartlead campaign (legacy endpoint)"""
417
  request = AddLeadsRequest(lead_list=leads)
418
- return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads", data=request.dict())
419
 
420
  @app.post("/campaigns/{campaign_id}/leads/{lead_id}/resume", response_model=Dict[str, Any], tags=["Leads"])
421
  async def resume_lead_by_campaign_id(campaign_id: int, lead_id: int, request: ResumeLeadRequest):
@@ -476,51 +497,24 @@ async def campaigns_for_lead(lead_id: int):
476
  async def check_lead_in_campaign(campaign_id: int, email: str):
477
  """Check if a lead exists in a campaign using efficient indexed lookups"""
478
  try:
479
- # Step 1: Get the global lead_id for the email address
480
  lead_response = await call_smartlead_api("GET", "leads", params={"email": email})
481
 
482
  if not lead_response or "id" not in lead_response:
483
- return {
484
- "exists": False,
485
- "message": "Lead not found",
486
- "email": email,
487
- "campaign_id": campaign_id
488
- }
489
 
490
  lead_id = lead_response["id"]
491
-
492
- # Step 2: Get all campaigns for this lead
493
  campaigns_response = await call_smartlead_api("GET", f"leads/{lead_id}/campaigns")
494
 
495
  if not campaigns_response:
496
- return {
497
- "exists": False,
498
- "message": "No campaigns found for lead",
499
- "email": email,
500
- "campaign_id": campaign_id,
501
- "lead_id": lead_id
502
- }
503
 
504
- # Step 3: Check if the target campaign_id exists in the lead's campaigns
505
  campaign_exists = any(campaign.get("id") == campaign_id for campaign in campaigns_response)
506
 
507
- return {
508
- "exists": campaign_exists,
509
- "message": "Lead found in campaign" if campaign_exists else "Lead not found in campaign",
510
- "email": email,
511
- "campaign_id": campaign_id,
512
- "lead_id": lead_id,
513
- "total_campaigns_for_lead": len(campaigns_response)
514
- }
515
 
516
  except HTTPException as e:
517
  if e.status_code == 404:
518
- return {
519
- "exists": False,
520
- "message": "Lead not found",
521
- "email": email,
522
- "campaign_id": campaign_id
523
- }
524
  raise e
525
  except Exception as e:
526
  raise HTTPException(status_code=500, detail=f"Error checking lead in campaign: {str(e)}")
@@ -539,68 +533,23 @@ async def export_data_from_campaign(campaign_id: int):
539
  resp = await client.get(url, params=params)
540
 
541
  if resp.status_code >= 400:
542
- # Try to parse error response as JSON for better error handling
543
  try:
544
  error_data = resp.json()
545
  error_message = error_data.get('message', error_data.get('error', 'Unknown error'))
546
- error_detail = error_data.get('detail', error_data.get('description', ''))
547
-
548
- raise HTTPException(
549
- status_code=resp.status_code,
550
- detail={
551
- "error": True,
552
- "message": error_message,
553
- "detail": error_detail,
554
- "endpoint": f"campaigns/{campaign_id}/leads-export",
555
- "method": "GET",
556
- "status_code": resp.status_code
557
- }
558
- )
559
  except (ValueError, KeyError):
560
- # Fallback to raw text if JSON parsing fails
561
- raise HTTPException(
562
- status_code=resp.status_code,
563
- detail={
564
- "error": True,
565
- "message": "API request failed",
566
- "detail": resp.text,
567
- "endpoint": f"campaigns/{campaign_id}/leads-export",
568
- "method": "GET",
569
- "status_code": resp.status_code
570
- }
571
- )
572
 
573
- # Return CSV data with proper headers
574
  return Response(
575
  content=resp.text,
576
  media_type="text/csv",
577
- headers={
578
- "Content-Disposition": f"attachment; filename=campaign_{campaign_id}_leads.csv"
579
- }
580
  )
581
 
582
  except httpx.TimeoutException:
583
- raise HTTPException(
584
- status_code=408,
585
- detail={
586
- "error": True,
587
- "message": "Request timeout",
588
- "detail": "The request to Smartlead API timed out",
589
- "endpoint": f"campaigns/{campaign_id}/leads-export",
590
- "method": "GET"
591
- }
592
- )
593
  except httpx.RequestError as e:
594
- raise HTTPException(
595
- status_code=503,
596
- detail={
597
- "error": True,
598
- "message": "Service unavailable",
599
- "detail": f"Failed to connect to Smartlead API: {str(e)}",
600
- "endpoint": f"campaigns/{campaign_id}/leads-export",
601
- "method": "GET"
602
- }
603
- )
604
 
605
  # ============================================================================
606
  # SEQUENCE ENDPOINTS
@@ -682,7 +631,7 @@ async def reply_to_lead_from_master_inbox(campaign_id: int, request: MessageHist
682
  # EMAIL ACCOUNT ENDPOINTS
683
  # ============================================================================
684
 
685
- @app.get("/email-accounts", response_model=Dict[str, Any], tags=["Email Accounts"])
686
  async def list_email_accounts(offset: int = 0, limit: int = 100):
687
  """List all email accounts with optional pagination"""
688
  params = {"offset": offset, "limit": limit}
@@ -693,7 +642,7 @@ async def save_email_account(account: Dict[str, Any]):
693
  """Create an Email Account"""
694
  return await call_smartlead_api("POST", "email-accounts/save", data=account)
695
 
696
- @app.get("/email-accounts/{account_id}", response_model=Any, tags=["Email Accounts"])
697
  async def get_email_account(account_id: int):
698
  """Fetch Email Account By ID"""
699
  return await call_smartlead_api("GET", f"email-accounts/{account_id}")
@@ -713,7 +662,7 @@ async def get_warmup_stats(account_id: int):
713
  """Fetch Warmup Stats By Email Account ID"""
714
  return await call_smartlead_api("GET", f"email-accounts/{account_id}/warmup-stats")
715
 
716
- @app.get("/campaigns/{campaign_id}/email-accounts", response_model=Any, tags=["Email Accounts"])
717
  async def list_campaign_email_accounts(campaign_id: int):
718
  """List all email accounts per campaign"""
719
  return await call_smartlead_api("GET", f"campaigns/{campaign_id}/email-accounts")
@@ -781,6 +730,121 @@ async def api_info():
781
  # AI SEQUENCE GENERATION FUNCTIONS
782
  # ============================================================================
783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  async def generate_sequences_with_llm(job_description: str) -> List[CampaignSequence]:
785
  """Generate email sequences using LangChain and OpenAI based on job description"""
786
 
@@ -1134,13 +1198,47 @@ def custom_openapi():
1134
 
1135
  ## Features
1136
  - **Campaign Management**: Create, update, and manage email campaigns
1137
- - **Lead Management**: Add, update, and manage leads across campaigns
1138
  - **Sequence Management**: Create and manage email sequences with AI generation
1139
  - **Webhook Management**: Set up webhooks for real-time notifications
1140
  - **Analytics**: Get detailed campaign analytics and statistics
1141
  - **Email Account Management**: Manage email accounts and warmup
1142
  - **Client Management**: Handle client accounts and permissions
1143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1144
  ## Authentication
1145
  All requests require a Smartlead API key passed as a query parameter: `?api_key=YOUR_API_KEY`
1146
 
 
2
  import json
3
  import time
4
  from datetime import datetime
5
+ from typing import List, Dict, Any, Optional, Union
6
  from pydantic import BaseModel, Field, EmailStr, validator
7
  from fastapi import FastAPI, HTTPException, Query, Depends, Request
8
  from fastapi.responses import JSONResponse, Response
 
23
  load_dotenv()
24
 
25
  # Configuration
26
+ SMARTLEAD_API_KEY = os.getenv("SMARTLEAD_API_KEY", "your-api-key-here")
27
  SMARTLEAD_BASE_URL = "https://server.smartlead.ai/api/v1"
28
 
29
  # Initialize FastAPI app
 
74
  first_name: Optional[str] = Field(None, description="Lead's first name")
75
  last_name: Optional[str] = Field(None, description="Lead's last name")
76
  email: str = Field(..., description="Lead's email address")
77
+ phone_number: Optional[Union[str, int]] = Field(None, description="Lead's phone number (can be string or integer)")
78
  company_name: Optional[str] = Field(None, description="Lead's company name")
79
  website: Optional[str] = Field(None, description="Lead's website")
80
  location: Optional[str] = Field(None, description="Lead's location")
81
+ custom_fields: Optional[Dict[str, str]] = Field(None, description="Custom fields as key-value pairs (max 20 fields)")
82
  linkedin_profile: Optional[str] = Field(None, description="Lead's LinkedIn profile URL")
83
  company_url: Optional[str] = Field(None, description="Company website URL")
84
+
85
+ @validator('custom_fields')
86
+ def validate_custom_fields(cls, v):
87
+ if v is not None and len(v) > 20:
88
+ raise ValueError('Custom fields cannot exceed 20 fields')
89
+ return v
90
+
91
+ @validator('phone_number')
92
+ def validate_phone_number(cls, v):
93
+ if v is not None:
94
+ # Convert to string if it's an integer
95
+ return str(v)
96
+ return v
97
 
98
  class LeadSettings(BaseModel):
99
  ignore_global_block_list: bool = Field(True, description="Ignore leads if they are in the global block list")
 
137
  job_description: str = Field(..., description="Job description to generate sequences for")
138
 
139
  class Campaign(BaseModel):
140
+ id: int
141
+ user_id: int
142
+ created_at: datetime
143
+ updated_at: datetime
144
+ status: str
145
+ name: str
146
+ track_settings: Union[str, List[Any]] # FIX: Accept string or list
147
+ scheduler_cron_value: Optional[Union[str, Dict[str, Any]]] = None # FIX: Accept string or dict
148
+ min_time_btwn_emails: int
149
+ max_leads_per_day: int
150
+ stop_lead_settings: str
151
+ unsubscribe_text: Optional[str] = None
152
+ client_id: Optional[int] = None
153
+ enable_ai_esp_matching: bool
154
+ send_as_plain_text: bool
155
+ follow_up_percentage: Optional[Union[str, int]] = None # FIX: Accept string or int
156
+
157
+ class CampaignListResponse(BaseModel):
158
+ campaigns: List[Campaign]
159
+ total: int
160
+ source: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  class Lead(BaseModel):
163
  id: int
164
  email: EmailStr
165
+ first_name: Optional[str] = None
166
+ last_name: Optional[str] = None
167
+ company: Optional[str] = None
168
+ position: Optional[str] = None
169
+ phone_number: Optional[str] = None
170
+ linkedin_url: Optional[str] = None
171
+ status: Optional[str] = None
172
+
173
+ class WarmupDetails(BaseModel):
174
+ status: str
175
+ total_sent_count: int
176
+ total_spam_count: int
177
+ warmup_reputation: str
178
+ warmup_key_id: Optional[str] = None
179
+ warmup_created_at: Optional[datetime] = None
180
+ reply_rate: int
181
+ blocked_reason: Optional[str] = None
182
+
183
+ class EmailAccount(BaseModel):
184
+ id: int
185
+ created_at: datetime
186
+ updated_at: datetime
187
+ user_id: int
188
+ from_name: str
189
+ from_email: str
190
+ username: str
191
+ password: Optional[str] = None
192
+ smtp_host: Optional[str] = None
193
+ smtp_port: Optional[int] = None
194
+ smtp_port_type: Optional[str] = None
195
+ message_per_day: int
196
+ different_reply_to_address: Optional[str] = None
197
+ is_different_imap_account: bool
198
+ imap_username: Optional[str] = None
199
+ imap_password: Optional[str] = None
200
+ imap_host: Optional[str] = None
201
+ imap_port: Optional[int] = None
202
+ imap_port_type: Optional[str] = None
203
+ signature: Optional[str] = None
204
+ custom_tracking_domain: Optional[str] = None
205
+ bcc_email: Optional[str] = None
206
+ is_smtp_success: bool
207
+ is_imap_success: bool
208
+ smtp_failure_error: Optional[str] = None
209
+ imap_failure_error: Optional[str] = None
210
+ type: str
211
+ daily_sent_count: int
212
+ client_id: Optional[int] = None
213
+ campaign_count: Optional[int] = None
214
+ warmup_details: Optional[WarmupDetails] = None
215
+
216
  class LeadCategoryUpdateRequest(BaseModel):
217
  category_id: int = Field(..., description="Category ID to assign to the lead")
218
  pause_lead: bool = Field(False, description="Whether to pause the lead after category update")
 
278
  resp = await client.request(method, url, params=params, json=data)
279
 
280
  if resp.status_code >= 400:
 
281
  try:
282
  error_data = resp.json()
283
  error_message = error_data.get('message', error_data.get('error', 'Unknown error'))
284
+ raise HTTPException(status_code=resp.status_code, detail=error_message)
 
 
 
 
 
 
 
 
 
 
 
 
285
  except (ValueError, KeyError):
286
+ raise HTTPException(status_code=resp.status_code, detail=resp.text)
 
 
 
 
 
 
 
 
 
 
 
287
 
288
  return resp.json()
289
 
290
  except httpx.TimeoutException:
291
+ raise HTTPException(status_code=408, detail="Request to Smartlead API timed out")
 
 
 
 
 
 
 
 
 
292
  except httpx.RequestError as e:
293
+ raise HTTPException(status_code=503, detail=f"Failed to connect to Smartlead API: {str(e)}")
 
 
 
 
 
 
 
 
 
294
 
295
  # ============================================================================
296
  # CAMPAIGN ENDPOINTS
 
301
  """Create a new campaign in Smartlead"""
302
  return await call_smartlead_api("POST", "campaigns/create", data=campaign.dict())
303
 
304
+ @app.get("/campaigns", response_model=CampaignListResponse, tags=["Campaigns"])
305
  async def list_campaigns():
306
  """Fetch all campaigns from Smartlead API"""
307
  campaigns = await call_smartlead_api("GET", "campaigns")
 
375
 
376
  @app.post("/campaigns/{campaign_id}/leads", response_model=Dict[str, Any], tags=["Leads"])
377
  async def add_leads_to_campaign(campaign_id: int, request: AddLeadsRequest):
378
+ """Add leads to a campaign by ID with personalized welcome and closing messages"""
379
  request_data = request.dict()
380
 
381
+ # Process each lead to generate personalized messages and clean up data
382
  for lead in request_data.get("lead_list", []):
383
  lead_cleaned = {k: v for k, v in lead.items() if v is not None and v != ""}
384
 
385
+ # Generate personalized welcome and closing messages using LLM
386
+ try:
387
+ personalized_messages = await generate_welcome_closing_messages(lead_cleaned)
388
+
389
+ # Initialize custom_fields if it doesn't exist
390
+ if "custom_fields" not in lead_cleaned:
391
+ lead_cleaned["custom_fields"] = {}
392
+
393
+ # Add the generated messages to custom_fields
394
+ if personalized_messages.get("welcome_message"):
395
+ lead_cleaned["custom_fields"]["Welcome_Message"] = personalized_messages["welcome_message"]
396
+ if personalized_messages.get("closing_message"):
397
+ lead_cleaned["custom_fields"]["Closing_Message"] = personalized_messages["closing_message"]
398
+
399
+ except Exception as e:
400
+ print(f"Error generating personalized messages for lead {lead_cleaned.get('email', 'unknown')}: {str(e)}")
401
+ # Continue with template messages if LLM fails
402
+ template_messages = generate_template_welcome_closing_messages(lead_cleaned)
403
+ if "custom_fields" not in lead_cleaned:
404
+ lead_cleaned["custom_fields"] = {}
405
+ if template_messages.get("welcome_message"):
406
+ lead_cleaned["custom_fields"]["Welcome_Message"] = template_messages["welcome_message"]
407
+ if template_messages.get("closing_message"):
408
+ lead_cleaned["custom_fields"]["Closing_Message"] = template_messages["closing_message"]
409
+
410
+ # Clean up custom_fields - remove None values and empty strings
411
  if "custom_fields" in lead_cleaned:
412
  custom_fields = lead_cleaned["custom_fields"]
413
  if custom_fields:
 
425
  request_data["lead_list"] = [lead for lead in request_data["lead_list"] if lead]
426
 
427
  if not request_data["lead_list"]:
428
+ raise HTTPException(status_code=400, detail="No valid leads to add.")
429
 
430
  if "settings" not in request_data or request_data["settings"] is None:
431
+ request_data["settings"] = LeadSettings().dict()
 
 
 
 
432
 
433
  return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads", data=request_data)
434
 
435
  @app.post("/campaigns/{campaign_id}/leads/bulk", response_model=Dict[str, Any], tags=["Leads"])
436
  async def add_bulk_leads(campaign_id: int, leads: List[LeadInput]):
437
+ """Add multiple leads to a Smartlead campaign with personalized messages (legacy endpoint)"""
438
  request = AddLeadsRequest(lead_list=leads)
439
+ return await add_leads_to_campaign(campaign_id, request)
440
 
441
  @app.post("/campaigns/{campaign_id}/leads/{lead_id}/resume", response_model=Dict[str, Any], tags=["Leads"])
442
  async def resume_lead_by_campaign_id(campaign_id: int, lead_id: int, request: ResumeLeadRequest):
 
497
  async def check_lead_in_campaign(campaign_id: int, email: str):
498
  """Check if a lead exists in a campaign using efficient indexed lookups"""
499
  try:
 
500
  lead_response = await call_smartlead_api("GET", "leads", params={"email": email})
501
 
502
  if not lead_response or "id" not in lead_response:
503
+ return {"exists": False, "message": "Lead not found"}
 
 
 
 
 
504
 
505
  lead_id = lead_response["id"]
 
 
506
  campaigns_response = await call_smartlead_api("GET", f"leads/{lead_id}/campaigns")
507
 
508
  if not campaigns_response:
509
+ return {"exists": False, "message": "No campaigns found for lead"}
 
 
 
 
 
 
510
 
 
511
  campaign_exists = any(campaign.get("id") == campaign_id for campaign in campaigns_response)
512
 
513
+ return {"exists": campaign_exists, "message": "Lead found in campaign" if campaign_exists else "Lead not found in campaign"}
 
 
 
 
 
 
 
514
 
515
  except HTTPException as e:
516
  if e.status_code == 404:
517
+ return {"exists": False, "message": "Lead not found"}
 
 
 
 
 
518
  raise e
519
  except Exception as e:
520
  raise HTTPException(status_code=500, detail=f"Error checking lead in campaign: {str(e)}")
 
533
  resp = await client.get(url, params=params)
534
 
535
  if resp.status_code >= 400:
 
536
  try:
537
  error_data = resp.json()
538
  error_message = error_data.get('message', error_data.get('error', 'Unknown error'))
539
+ raise HTTPException(status_code=resp.status_code, detail=error_message)
 
 
 
 
 
 
 
 
 
 
 
 
540
  except (ValueError, KeyError):
541
+ raise HTTPException(status_code=resp.status_code, detail=resp.text)
 
 
 
 
 
 
 
 
 
 
 
542
 
 
543
  return Response(
544
  content=resp.text,
545
  media_type="text/csv",
546
+ headers={"Content-Disposition": f"attachment; filename=campaign_{campaign_id}_leads.csv"}
 
 
547
  )
548
 
549
  except httpx.TimeoutException:
550
+ raise HTTPException(status_code=408, detail="Request to Smartlead API timed out")
 
 
 
 
 
 
 
 
 
551
  except httpx.RequestError as e:
552
+ raise HTTPException(status_code=503, detail=f"Failed to connect to Smartlead API: {str(e)}")
 
 
 
 
 
 
 
 
 
553
 
554
  # ============================================================================
555
  # SEQUENCE ENDPOINTS
 
631
  # EMAIL ACCOUNT ENDPOINTS
632
  # ============================================================================
633
 
634
+ @app.get("/email-accounts", response_model=List[EmailAccount], tags=["Email Accounts"])
635
  async def list_email_accounts(offset: int = 0, limit: int = 100):
636
  """List all email accounts with optional pagination"""
637
  params = {"offset": offset, "limit": limit}
 
642
  """Create an Email Account"""
643
  return await call_smartlead_api("POST", "email-accounts/save", data=account)
644
 
645
+ @app.get("/email-accounts/{account_id}", response_model=EmailAccount, tags=["Email Accounts"])
646
  async def get_email_account(account_id: int):
647
  """Fetch Email Account By ID"""
648
  return await call_smartlead_api("GET", f"email-accounts/{account_id}")
 
662
  """Fetch Warmup Stats By Email Account ID"""
663
  return await call_smartlead_api("GET", f"email-accounts/{account_id}/warmup-stats")
664
 
665
+ @app.get("/campaigns/{campaign_id}/email-accounts", response_model=List[EmailAccount], tags=["Email Accounts"])
666
  async def list_campaign_email_accounts(campaign_id: int):
667
  """List all email accounts per campaign"""
668
  return await call_smartlead_api("GET", f"campaigns/{campaign_id}/email-accounts")
 
730
  # AI SEQUENCE GENERATION FUNCTIONS
731
  # ============================================================================
732
 
733
+ async def generate_welcome_closing_messages(lead_data: Dict[str, Any]) -> Dict[str, str]:
734
+ """Generate personalized welcome and closing messages using LLM based on candidate details"""
735
+
736
+ if not LANGCHAIN_AVAILABLE:
737
+ return generate_template_welcome_closing_messages(lead_data)
738
+
739
+ try:
740
+ openai_api_key = os.getenv("OPENAI_API_KEY")
741
+ if not openai_api_key:
742
+ print("Warning: OPENAI_API_KEY not set. Using template messages.")
743
+ return generate_template_welcome_closing_messages(lead_data)
744
+
745
+ llm = ChatOpenAI(
746
+ model="gpt-4",
747
+ temperature=0.7,
748
+ openai_api_key=openai_api_key
749
+ )
750
+
751
+ # Extract relevant information from lead data
752
+ first_name = lead_data.get("first_name", "")
753
+ last_name = lead_data.get("last_name", "")
754
+ company_name = lead_data.get("company_name", "")
755
+ location = lead_data.get("location", "")
756
+ title = lead_data.get("custom_fields", {}).get("Title", "")
757
+ linkedin_profile = lead_data.get("linkedin_profile", "")
758
+
759
+ # Create a summary of the candidate's background
760
+ candidate_info = f"""
761
+ Name: {first_name} {last_name}
762
+ Company: {company_name}
763
+ Location: {location}
764
+ Title: {title}
765
+ LinkedIn: {linkedin_profile}
766
+ """
767
+
768
+ system_prompt = """You are an expert recruiter who creates personalized welcome and closing messages for email campaigns.
769
+
770
+ Based on the candidate's information, generate:
771
+ 1. A personalized welcome message (2-3 sentences)
772
+ 2. A personalized closing message (1-2 sentences)
773
+
774
+ Requirements:
775
+ - Professional but friendly tone
776
+ - Reference their specific background/company/role when possible
777
+ - Keep messages concise and engaging
778
+ - Make them feel valued and understood
779
+
780
+ Respond with ONLY a JSON object:
781
+ {
782
+ "welcome_message": "Personalized welcome message here",
783
+ "closing_message": "Personalized closing message here"
784
+ }
785
+
786
+ IMPORTANT: Respond with ONLY valid JSON. No additional text."""
787
+
788
+ prompt_template = ChatPromptTemplate.from_messages([
789
+ ("system", system_prompt),
790
+ ("human", "Generate personalized messages for this candidate: {candidate_info}")
791
+ ])
792
+
793
+ messages = prompt_template.format_messages(candidate_info=candidate_info)
794
+ response = await llm.ainvoke(messages)
795
+
796
+ try:
797
+ content = response.content.strip()
798
+
799
+ if content.startswith("```json"):
800
+ content = content[7:]
801
+ if content.endswith("```"):
802
+ content = content[:-3]
803
+
804
+ content = content.strip()
805
+ parsed_data = json.loads(content)
806
+
807
+ return {
808
+ "welcome_message": parsed_data.get("welcome_message", ""),
809
+ "closing_message": parsed_data.get("closing_message", "")
810
+ }
811
+
812
+ except Exception as parse_error:
813
+ print(f"JSON parsing failed for welcome/closing messages: {parse_error}")
814
+ return generate_template_welcome_closing_messages(lead_data)
815
+
816
+ except Exception as e:
817
+ print(f"Error generating welcome/closing messages with LLM: {str(e)}")
818
+ return generate_template_welcome_closing_messages(lead_data)
819
+
820
+ def generate_template_welcome_closing_messages(lead_data: Dict[str, Any]) -> Dict[str, str]:
821
+ """Generate template-based welcome and closing messages as fallback"""
822
+
823
+ first_name = lead_data.get("first_name", "")
824
+ company_name = lead_data.get("company_name", "")
825
+ title = lead_data.get("custom_fields", {}).get("Title", "")
826
+
827
+ # Personalized welcome message
828
+ if first_name and company_name:
829
+ welcome_message = f"Hi {first_name}, I came across your profile and was impressed by your work at {company_name}."
830
+ elif first_name:
831
+ welcome_message = f"Hi {first_name}, I came across your profile and was impressed by your background."
832
+ elif company_name:
833
+ welcome_message = f"Hi there, I came across your profile and was impressed by your work at {company_name}."
834
+ else:
835
+ welcome_message = "Hi there, I came across your profile and was impressed by your background."
836
+
837
+ # Personalized closing message
838
+ if first_name:
839
+ closing_message = f"Looking forward to connecting with you, {first_name}!"
840
+ else:
841
+ closing_message = "Looking forward to connecting with you!"
842
+
843
+ return {
844
+ "welcome_message": welcome_message,
845
+ "closing_message": closing_message
846
+ }
847
+
848
  async def generate_sequences_with_llm(job_description: str) -> List[CampaignSequence]:
849
  """Generate email sequences using LangChain and OpenAI based on job description"""
850
 
 
1198
 
1199
  ## Features
1200
  - **Campaign Management**: Create, update, and manage email campaigns
1201
+ - **Lead Management**: Add, update, and manage leads across campaigns with AI-powered personalization
1202
  - **Sequence Management**: Create and manage email sequences with AI generation
1203
  - **Webhook Management**: Set up webhooks for real-time notifications
1204
  - **Analytics**: Get detailed campaign analytics and statistics
1205
  - **Email Account Management**: Manage email accounts and warmup
1206
  - **Client Management**: Handle client accounts and permissions
1207
 
1208
+ ## AI-Powered Personalization
1209
+ When adding leads to campaigns, the API automatically generates personalized welcome and closing messages using LLM (Language Model) based on candidate details. These messages are added to the custom_fields as:
1210
+ - `Welcome_Message`: Personalized greeting based on candidate's background
1211
+ - `Closing_Message`: Personalized closing statement
1212
+
1213
+ ## Lead Schema
1214
+ The lead schema supports the following structure:
1215
+ ```json
1216
+ {
1217
+ "lead_list": [
1218
+ {
1219
+ "first_name": "Cristiano",
1220
+ "last_name": "Ronaldo",
1221
+ "email": "[email protected]",
1222
+ "phone_number": "0239392029",
1223
+ "company_name": "Manchester United",
1224
+ "website": "mufc.com",
1225
+ "location": "London",
1226
+ "custom_fields": {
1227
+ "Title": "Regional Manager",
1228
+ "First_Line": "Loved your recent post about remote work on Linkedin"
1229
+ },
1230
+ "linkedin_profile": "http://www.linkedin.com/in/cristianoronaldo",
1231
+ "company_url": "mufc.com"
1232
+ }
1233
+ ],
1234
+ "settings": {
1235
+ "ignore_global_block_list": true,
1236
+ "ignore_unsubscribe_list": true,
1237
+ "ignore_duplicate_leads_in_other_campaign": false
1238
+ }
1239
+ }
1240
+ ```
1241
+
1242
  ## Authentication
1243
  All requests require a Smartlead API key passed as a query parameter: `?api_key=YOUR_API_KEY`
1244