ak0601 commited on
Commit
9306831
·
verified ·
1 Parent(s): 9daf18e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2 -1398
app.py CHANGED
@@ -1,1399 +1,3 @@
1
- # import os
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
9
- # from fastapi.middleware.cors import CORSMiddleware
10
- # from fastapi.openapi.utils import get_openapi
11
- # import httpx
12
- # from dotenv import load_dotenv
13
-
14
- # # LangChain and OpenAI imports
15
- # try:
16
- # from langchain_openai import ChatOpenAI
17
- # from langchain.prompts import ChatPromptTemplate
18
- # LANGCHAIN_AVAILABLE = True
19
- # except ImportError:
20
- # LANGCHAIN_AVAILABLE = False
21
- # print("Warning: LangChain not available. Install with: pip install langchain langchain-openai")
22
-
23
- # load_dotenv(override=True)
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
30
- # app = FastAPI(
31
- # title="Smartlead API - Complete Integration",
32
- # version="2.0.0",
33
- # description="Comprehensive FastAPI wrapper for Smartlead email automation platform",
34
- # docs_url="/docs",
35
- # redoc_url="/redoc"
36
- # )
37
-
38
- # # Add CORS middleware
39
- # app.add_middleware(
40
- # CORSMiddleware,
41
- # allow_origins=["*"],
42
- # allow_credentials=True,
43
- # allow_methods=["*"],
44
- # allow_headers=["*"],
45
- # )
46
-
47
- # # ============================================================================
48
- # # DATA MODELS
49
- # # ============================================================================
50
-
51
- # class CreateCampaignRequest(BaseModel):
52
- # name: str = Field(..., description="Campaign name")
53
- # client_id: Optional[int] = Field(None, description="Client ID (leave null if no client)")
54
-
55
- # class CampaignScheduleRequest(BaseModel):
56
- # timezone: str = Field(..., description="Timezone for the campaign schedule (e.g., 'America/Los_Angeles')")
57
- # days_of_the_week: List[int] = Field(..., description="Days of the week for scheduling [0=Sunday, 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, 6=Saturday]")
58
- # start_hour: str = Field(..., description="Start hour for sending emails in HH:MM format (e.g., '09:00')")
59
- # end_hour: str = Field(..., description="End hour for sending emails in HH:MM format (e.g., '18:00')")
60
- # min_time_btw_emails: int = Field(..., description="Minimum time in minutes between sending emails")
61
- # max_new_leads_per_day: int = Field(..., description="Maximum number of new leads to process per day")
62
- # schedule_start_time: str = Field(..., description="Schedule start time in ISO 8601 format (e.g., '2023-04-25T07:29:25.978Z')")
63
-
64
- # class CampaignSettingsRequest(BaseModel):
65
- # track_settings: List[str] = Field(..., description="Tracking settings array (allowed values: DONT_TRACK_EMAIL_OPEN, DONT_TRACK_LINK_CLICK, DONT_TRACK_REPLY_TO_AN_EMAIL)")
66
- # stop_lead_settings: str = Field(..., description="Settings for stopping leads (allowed values: CLICK_ON_A_LINK, OPEN_AN_EMAIL)")
67
- # unsubscribe_text: str = Field(..., description="Text for the unsubscribe link")
68
- # send_as_plain_text: bool = Field(..., description="Whether emails should be sent as plain text")
69
- # follow_up_percentage: int = Field(ge=0, le=100, description="Follow-up percentage (max 100, min 0)")
70
- # client_id: Optional[int] = Field(None, description="Client ID (leave as null if not needed)")
71
- # enable_ai_esp_matching: bool = Field(False, description="Enable AI ESP matching (by default is false)")
72
-
73
- # 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[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")
100
- # ignore_unsubscribe_list: bool = Field(True, description="Ignore leads if they are in the unsubscribe list")
101
- # ignore_duplicate_leads_in_other_campaign: bool = Field(False, description="Allow leads to be added even if they are duplicates in other campaigns")
102
-
103
- # class AddLeadsRequest(BaseModel):
104
- # lead_list: List[LeadInput] = Field(..., max_items=100, description="List of leads to add (maximum 100 leads)")
105
- # settings: Optional[LeadSettings] = Field(None, description="Settings for lead processing")
106
-
107
- # class AddLeadsResponse(BaseModel):
108
- # ok: bool = Field(..., description="Indicates if the operation was successful")
109
- # upload_count: int = Field(..., description="Number of leads successfully uploaded")
110
- # total_leads: int = Field(..., description="Total number of leads attempted to upload")
111
- # already_added_to_campaign: int = Field(..., description="Number of leads already present in the campaign")
112
- # duplicate_count: int = Field(..., description="Number of duplicate emails found")
113
- # invalid_email_count: int = Field(..., description="Number of leads with invalid email format")
114
- # unsubscribed_leads: Any = Field(..., description="Number of leads that had previously unsubscribed (can be int or empty list)")
115
-
116
- # class SeqDelayDetails(BaseModel):
117
- # delay_in_days: int = Field(..., description="Delay in days before sending this sequence")
118
-
119
- # class SeqVariant(BaseModel):
120
- # subject: str = Field(..., description="Email subject line")
121
- # email_body: str = Field(..., description="Email body content (HTML format)")
122
- # variant_label: str = Field(..., description="Variant label (A, B, C, etc.)")
123
- # id: Optional[int] = Field(None, description="Variant ID (only for updating, not for creating)")
124
-
125
- # class CampaignSequence(BaseModel):
126
- # id: Optional[int] = Field(None, description="Sequence ID (only for updating, not for creating)")
127
- # seq_number: int = Field(..., description="Sequence number (1, 2, 3, etc.)")
128
- # seq_delay_details: SeqDelayDetails = Field(..., description="Delay details for this sequence")
129
- # seq_variants: Optional[List[SeqVariant]] = Field(None, description="Email variants for A/B testing")
130
- # subject: Optional[str] = Field("", description="Subject line (blank for follow-up in same thread)")
131
- # email_body: Optional[str] = Field(None, description="Email body content (HTML format)")
132
-
133
- # class SaveSequencesRequest(BaseModel):
134
- # sequences: List[CampaignSequence] = Field(..., description="List of campaign sequences")
135
-
136
- # class GenerateSequencesRequest(BaseModel):
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: Union[str, int]
178
- # warmup_key_id: Optional[str] = None
179
- # warmup_created_at: Optional[datetime] = None
180
- # reply_rate: Optional[int] = None
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 WarmupSettingsRequest(BaseModel):
217
- # warmup_enabled: bool
218
- # total_warmup_per_day: Optional[int] = None
219
- # daily_rampup: Optional[int] = None
220
- # reply_rate_percentage: Optional[int] = None
221
- # warmup_key_id: Optional[str] = Field(None, description="String value if passed will update the custom warmup-key identifier")
222
-
223
- # class LeadCategoryUpdateRequest(BaseModel):
224
- # category_id: int = Field(..., description="Category ID to assign to the lead")
225
- # pause_lead: bool = Field(False, description="Whether to pause the lead after category update")
226
-
227
- # class CampaignStatusUpdateRequest(BaseModel):
228
- # status: str = Field(..., description="New campaign status (PAUSED, STOPPED, START)")
229
-
230
- # class ResumeLeadRequest(BaseModel):
231
- # resume_lead_with_delay_days: Optional[int] = Field(None, description="Delay in days before resuming (defaults to 0)")
232
-
233
- # class DomainBlockListRequest(BaseModel):
234
- # domain_block_list: List[str] = Field(..., description="List of domains/emails to block")
235
- # client_id: Optional[int] = Field(None, description="Client ID if blocking is client-specific")
236
-
237
- # class WebhookRequest(BaseModel):
238
- # id: Optional[int] = Field(None, description="Webhook ID (null for creating new)")
239
- # name: str = Field(..., description="Webhook name")
240
- # webhook_url: str = Field(..., description="Webhook URL")
241
- # event_types: List[str] = Field(..., description="List of event types to listen for")
242
- # categories: Optional[List[str]] = Field(None, description="List of categories to filter by")
243
-
244
- # class WebhookDeleteRequest(BaseModel):
245
- # id: int = Field(..., description="Webhook ID to delete")
246
-
247
- # class ClientRequest(BaseModel):
248
- # name: str = Field(..., description="Client name")
249
- # email: str = Field(..., description="Client email")
250
- # permission: List[str] = Field(..., description="List of permissions")
251
- # logo: Optional[str] = Field(None, description="Client logo text")
252
- # logo_url: Optional[str] = Field(None, description="Client logo URL")
253
- # password: str = Field(..., description="Client password")
254
-
255
- # class MessageHistoryRequest(BaseModel):
256
- # email_stats_id: str = Field(..., description="Email stats ID for the specific email")
257
- # email_body: str = Field(..., description="Reply message email body")
258
- # reply_message_id: str = Field(..., description="Message ID to reply to")
259
- # reply_email_time: str = Field(..., description="Time of the message being replied to")
260
- # reply_email_body: str = Field(..., description="Body of the message being replied to")
261
- # cc: Optional[str] = Field(None, description="CC recipients")
262
- # bcc: Optional[str] = Field(None, description="BCC recipients")
263
- # add_signature: bool = Field(True, description="Whether to add signature")
264
-
265
- # # ============================================================================
266
- # # HELPER FUNCTIONS
267
- # # ============================================================================
268
-
269
- # def _get_smartlead_url(endpoint: str) -> str:
270
- # return f"{SMARTLEAD_BASE_URL}/{endpoint.lstrip('/')}"
271
-
272
- # async def call_smartlead_api(method: str, endpoint: str, data: Any = None, params: Dict[str, Any] = None) -> Any:
273
- # if SMARTLEAD_API_KEY == "your-api-key-here":
274
- # raise HTTPException(status_code=400, detail="Smartlead API key not configured")
275
- # if params is None:
276
- # params = {}
277
- # params['api_key'] = SMARTLEAD_API_KEY
278
- # url = _get_smartlead_url(endpoint)
279
-
280
- # try:
281
- # async with httpx.AsyncClient(timeout=30.0) as client:
282
- # request_kwargs = {"params": params}
283
- # if data is not None:
284
- # request_kwargs["json"] = data
285
-
286
- # resp = await client.request(method, url, **request_kwargs)
287
-
288
- # if resp.status_code >= 400:
289
- # try:
290
- # error_data = resp.json()
291
- # error_message = error_data.get('message', error_data.get('error', 'Unknown error'))
292
- # raise HTTPException(status_code=resp.status_code, detail=error_message)
293
- # except (ValueError, KeyError):
294
- # raise HTTPException(status_code=resp.status_code, detail=resp.text)
295
-
296
- # return resp.json()
297
-
298
- # except httpx.TimeoutException:
299
- # raise HTTPException(status_code=408, detail="Request to Smartlead API timed out")
300
- # except httpx.RequestError as e:
301
- # raise HTTPException(status_code=503, detail=f"Failed to connect to Smartlead API: {str(e)}")
302
-
303
- # # ============================================================================
304
- # # CAMPAIGN ENDPOINTS
305
- # # ============================================================================
306
-
307
- # @app.post("/campaigns/create", response_model=Dict[str, Any], tags=["Campaigns"])
308
- # async def create_campaign(campaign: CreateCampaignRequest):
309
- # """Create a new campaign in Smartlead"""
310
- # return await call_smartlead_api("POST", "campaigns/create", data=campaign.dict())
311
-
312
- # @app.get("/campaigns", response_model=CampaignListResponse, tags=["Campaigns"])
313
- # async def list_campaigns():
314
- # """Fetch all campaigns from Smartlead API"""
315
- # campaigns = await call_smartlead_api("GET", "campaigns")
316
- # return {"campaigns": campaigns, "total": len(campaigns), "source": "smartlead"}
317
-
318
- # @app.get("/campaigns/{campaign_id}", response_model=Campaign, tags=["Campaigns"])
319
- # async def get_campaign(campaign_id: int):
320
- # """Get Campaign By Id"""
321
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}")
322
-
323
- # @app.post("/campaigns/{campaign_id}/settings", response_model=Dict[str, Any], tags=["Campaigns"])
324
- # async def update_campaign_settings(campaign_id: int, settings: CampaignSettingsRequest):
325
- # """Update Campaign General Settings"""
326
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/settings", data=settings.dict())
327
-
328
- # @app.post("/campaigns/{campaign_id}/schedule", response_model=Dict[str, Any], tags=["Campaigns"])
329
- # async def schedule_campaign(campaign_id: int, schedule: CampaignScheduleRequest):
330
- # """Update Campaign Schedule"""
331
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/schedule", data=schedule.dict())
332
-
333
- # @app.delete("/campaigns/{campaign_id}", response_model=Dict[str, Any], tags=["Campaigns"])
334
- # async def delete_campaign(campaign_id: int):
335
- # """Delete Campaign"""
336
- # return await call_smartlead_api("DELETE", f"campaigns/{campaign_id}")
337
-
338
- # @app.post("/campaigns/{campaign_id}/status", response_model=Dict[str, Any], tags=["Campaigns"])
339
- # async def patch_campaign_status(campaign_id: int, request: CampaignStatusUpdateRequest):
340
- # """Patch campaign status"""
341
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/status", data=request.dict())
342
-
343
- # @app.get("/campaigns/{campaign_id}/analytics", response_model=Any, tags=["Analytics"])
344
- # async def campaign_analytics(campaign_id: int):
345
- # """Fetch analytics for a campaign"""
346
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/analytics")
347
-
348
- # @app.get("/campaigns/{campaign_id}/statistics", response_model=Dict[str, Any], tags=["Analytics"])
349
- # async def fetch_campaign_statistics_by_campaign_id(
350
- # campaign_id: int,
351
- # offset: int = 0,
352
- # limit: int = 100,
353
- # email_sequence_number: Optional[int] = None,
354
- # email_status: Optional[str] = None
355
- # ):
356
- # """Fetch Campaign Statistics By Campaign Id"""
357
- # params = {"offset": offset, "limit": limit}
358
- # if email_sequence_number:
359
- # params["email_sequence_number"] = email_sequence_number
360
- # if email_status:
361
- # params["email_status"] = email_status
362
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/statistics", params=params)
363
-
364
- # @app.get("/campaigns/{campaign_id}/analytics-by-date", response_model=Dict[str, Any], tags=["Analytics"])
365
- # async def fetch_campaign_statistics_by_date_range(
366
- # campaign_id: int,
367
- # start_date: str,
368
- # end_date: str
369
- # ):
370
- # """Fetch Campaign Statistics By Campaign Id And Date Range"""
371
- # params = {"start_date": start_date, "end_date": end_date}
372
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/analytics-by-date", params=params)
373
-
374
- # # ============================================================================
375
- # # LEAD MANAGEMENT ENDPOINTS
376
- # # ============================================================================
377
-
378
- # @app.get("/campaigns/{campaign_id}/leads", response_model=Dict[str, Any], tags=["Leads"])
379
- # async def get_campaign_leads(campaign_id: int, offset: int = 0, limit: int = 100):
380
- # """List all leads by campaign id"""
381
- # params = {"offset": offset, "limit": limit}
382
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/leads", params=params)
383
-
384
- # @app.post("/campaigns/{campaign_id}/leads", response_model=Dict[str, Any], tags=["Leads"])
385
- # async def add_leads_to_campaign(campaign_id: int, request: AddLeadsRequest):
386
- # """Add leads to a campaign by ID with personalized welcome and closing messages"""
387
- # request_data = request.dict()
388
-
389
- # # Process each lead to generate personalized messages and clean up data
390
- # for lead in request_data.get("lead_list", []):
391
- # lead_cleaned = {k: v for k, v in lead.items() if v is not None and v != ""}
392
-
393
- # # Generate personalized welcome and closing messages using LLM
394
- # try:
395
- # personalized_messages = await generate_welcome_closing_messages(lead_cleaned)
396
-
397
- # # Initialize custom_fields if it doesn't exist
398
- # if "custom_fields" not in lead_cleaned:
399
- # lead_cleaned["custom_fields"] = {}
400
-
401
- # # Add the generated messages to custom_fields
402
- # if personalized_messages.get("welcome_message"):
403
- # lead_cleaned["custom_fields"]["Welcome_Message"] = personalized_messages["welcome_message"]
404
- # if personalized_messages.get("closing_message"):
405
- # lead_cleaned["custom_fields"]["Closing_Message"] = personalized_messages["closing_message"]
406
-
407
- # except Exception as e:
408
- # print(f"Error generating personalized messages for lead {lead_cleaned.get('email', 'unknown')}: {str(e)}")
409
- # # Continue with template messages if LLM fails
410
- # template_messages = generate_template_welcome_closing_messages(lead_cleaned)
411
- # if "custom_fields" not in lead_cleaned:
412
- # lead_cleaned["custom_fields"] = {}
413
- # if template_messages.get("welcome_message"):
414
- # lead_cleaned["custom_fields"]["Welcome_Message"] = template_messages["welcome_message"]
415
- # if template_messages.get("closing_message"):
416
- # lead_cleaned["custom_fields"]["Closing_Message"] = template_messages["closing_message"]
417
-
418
- # # Clean up custom_fields - remove None values and empty strings
419
- # if "custom_fields" in lead_cleaned:
420
- # custom_fields = lead_cleaned["custom_fields"]
421
- # if custom_fields:
422
- # custom_fields_cleaned = {k: v for k, v in custom_fields.items() if v is not None and v != ""}
423
- # if custom_fields_cleaned:
424
- # lead_cleaned["custom_fields"] = custom_fields_cleaned
425
- # else:
426
- # lead_cleaned.pop("custom_fields", None)
427
- # else:
428
- # lead_cleaned.pop("custom_fields", None)
429
-
430
- # lead.clear()
431
- # lead.update(lead_cleaned)
432
-
433
- # request_data["lead_list"] = [lead for lead in request_data["lead_list"] if lead]
434
-
435
- # if not request_data["lead_list"]:
436
- # raise HTTPException(status_code=400, detail="No valid leads to add.")
437
-
438
- # if "settings" not in request_data or request_data["settings"] is None:
439
- # request_data["settings"] = LeadSettings().dict()
440
-
441
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads", data=request_data)
442
-
443
- # @app.post("/campaigns/{campaign_id}/leads/bulk", response_model=Dict[str, Any], tags=["Leads"])
444
- # async def add_bulk_leads(campaign_id: int, leads: List[LeadInput]):
445
- # """Add multiple leads to a Smartlead campaign with personalized messages (legacy endpoint)"""
446
- # request = AddLeadsRequest(lead_list=leads)
447
- # return await add_leads_to_campaign(campaign_id, request)
448
-
449
- # @app.post("/campaigns/{campaign_id}/leads/{lead_id}/resume", response_model=Dict[str, Any], tags=["Leads"])
450
- # async def resume_lead_by_campaign_id(campaign_id: int, lead_id: int, request: ResumeLeadRequest):
451
- # """Resume Lead By Campaign ID"""
452
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads/{lead_id}/resume", data=request.dict())
453
-
454
- # @app.post("/campaigns/{campaign_id}/leads/{lead_id}/pause", response_model=Dict[str, Any], tags=["Leads"])
455
- # async def pause_lead_by_campaign_id(campaign_id: int, lead_id: int):
456
- # """Pause Lead By Campaign ID"""
457
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads/{lead_id}/pause")
458
-
459
- # @app.delete("/campaigns/{campaign_id}/leads/{lead_id}", response_model=Dict[str, Any], tags=["Leads"])
460
- # async def delete_lead_by_campaign_id(campaign_id: int, lead_id: int):
461
- # """Delete Lead By Campaign ID"""
462
- # return await call_smartlead_api("DELETE", f"campaigns/{campaign_id}/leads/{lead_id}")
463
-
464
- # @app.post("/campaigns/{campaign_id}/leads/{lead_id}/unsubscribe", response_model=Dict[str, Any], tags=["Leads"])
465
- # async def unsubscribe_lead_from_campaign(campaign_id: int, lead_id: int):
466
- # """Unsubscribe/Pause Lead From Campaign"""
467
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads/{lead_id}/unsubscribe")
468
-
469
- # @app.post("/leads/{lead_id}/unsubscribe", response_model=Dict[str, Any], tags=["Leads"])
470
- # async def unsubscribe_lead_from_all_campaigns(lead_id: int):
471
- # """Unsubscribe Lead From All Campaigns"""
472
- # return await call_smartlead_api("POST", f"leads/{lead_id}/unsubscribe")
473
-
474
- # @app.post("/leads/{lead_id}", response_model=Dict[str, Any], tags=["Leads"])
475
- # async def update_lead(lead_id: int, lead_data: Dict[str, Any]):
476
- # """Update lead using the Lead ID"""
477
- # return await call_smartlead_api("POST", f"leads/{lead_id}", data=lead_data)
478
-
479
- # @app.post("/campaigns/{campaign_id}/leads/{lead_id}/category", response_model=Dict[str, Any], tags=["Leads"])
480
- # async def update_lead_category_by_campaign(campaign_id: int, lead_id: int, request: LeadCategoryUpdateRequest):
481
- # """Update a lead's category based on their campaign"""
482
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads/{lead_id}/category", data=request.dict())
483
-
484
- # @app.post("/leads/add-domain-block-list", response_model=Dict[str, Any], tags=["Leads"])
485
- # async def add_domain_to_global_block_list(request: DomainBlockListRequest):
486
- # """Add Lead/Domain to Global Block List"""
487
- # return await call_smartlead_api("POST", "leads/add-domain-block-list", data=request.dict())
488
-
489
- # @app.get("/leads/fetch-categories", response_model=List[Dict[str, Any]], tags=["Leads"])
490
- # async def fetch_lead_categories():
491
- # """Fetch lead categories"""
492
- # return await call_smartlead_api("GET", "leads/fetch-categories")
493
-
494
- # @app.get("/leads", response_model=Dict[str, Any], tags=["Leads"])
495
- # async def fetch_lead_by_email_address(email: str):
496
- # """Fetch lead by email address"""
497
- # return await call_smartlead_api("GET", "leads", params={"email": email})
498
-
499
- # @app.get("/leads/{lead_id}/campaigns", response_model=List[Dict[str, Any]], tags=["Leads"])
500
- # async def campaigns_for_lead(lead_id: int):
501
- # """Fetch all campaigns that a lead belongs to"""
502
- # return await call_smartlead_api("GET", f"leads/{lead_id}/campaigns")
503
-
504
- # @app.get("/campaigns/{campaign_id}/leads/check", response_model=Dict[str, Any], tags=["Leads"])
505
- # async def check_lead_in_campaign(campaign_id: int, email: str):
506
- # """Check if a lead exists in a campaign using efficient indexed lookups"""
507
- # try:
508
- # lead_response = await call_smartlead_api("GET", "leads", params={"email": email})
509
-
510
- # if not lead_response or "id" not in lead_response:
511
- # return {"exists": False, "message": "Lead not found"}
512
-
513
- # lead_id = lead_response["id"]
514
- # campaigns_response = await call_smartlead_api("GET", f"leads/{lead_id}/campaigns")
515
-
516
- # if not campaigns_response:
517
- # return {"exists": False, "message": "No campaigns found for lead"}
518
-
519
- # campaign_exists = any(campaign.get("id") == campaign_id for campaign in campaigns_response)
520
-
521
- # return {"exists": campaign_exists, "message": "Lead found in campaign" if campaign_exists else "Lead not found in campaign"}
522
-
523
- # except HTTPException as e:
524
- # if e.status_code == 404:
525
- # return {"exists": False, "message": "Lead not found"}
526
- # raise e
527
- # except Exception as e:
528
- # raise HTTPException(status_code=500, detail=f"Error checking lead in campaign: {str(e)}")
529
-
530
- # @app.get("/campaigns/{campaign_id}/leads-export", tags=["Leads"])
531
- # async def export_data_from_campaign(campaign_id: int):
532
- # """Export data from a campaign as CSV"""
533
- # if SMARTLEAD_API_KEY == "your-api-key-here":
534
- # raise HTTPException(status_code=400, detail="Smartlead API key not configured")
535
-
536
- # url = _get_smartlead_url(f"campaigns/{campaign_id}/leads-export")
537
- # params = {"api_key": SMARTLEAD_API_KEY}
538
-
539
- # try:
540
- # async with httpx.AsyncClient(timeout=30.0) as client:
541
- # resp = await client.get(url, params=params)
542
-
543
- # if resp.status_code >= 400:
544
- # try:
545
- # error_data = resp.json()
546
- # error_message = error_data.get('message', error_data.get('error', 'Unknown error'))
547
- # raise HTTPException(status_code=resp.status_code, detail=error_message)
548
- # except (ValueError, KeyError):
549
- # raise HTTPException(status_code=resp.status_code, detail=resp.text)
550
-
551
- # return Response(
552
- # content=resp.text,
553
- # media_type="text/csv",
554
- # headers={"Content-Disposition": f"attachment; filename=campaign_{campaign_id}_leads.csv"}
555
- # )
556
-
557
- # except httpx.TimeoutException:
558
- # raise HTTPException(status_code=408, detail="Request to Smartlead API timed out")
559
- # except httpx.RequestError as e:
560
- # raise HTTPException(status_code=503, detail=f"Failed to connect to Smartlead API: {str(e)}")
561
-
562
- # # ============================================================================
563
- # # SEQUENCE ENDPOINTS
564
- # # ============================================================================
565
-
566
- # @app.get("/campaigns/{campaign_id}/sequences", response_model=Any, tags=["Sequences"])
567
- # async def get_campaign_sequences(campaign_id: int):
568
- # """Fetch email sequences for a campaign"""
569
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/sequences")
570
-
571
- # @app.post("/campaigns/{campaign_id}/sequences", response_model=Dict[str, Any], tags=["Sequences"])
572
- # async def save_campaign_sequences(campaign_id: int, request: SaveSequencesRequest):
573
- # """Save Campaign Sequence"""
574
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/sequences", data=request.dict())
575
-
576
- # @app.post("/campaigns/{campaign_id}/sequences/generate", response_model=Dict[str, Any], tags=["Sequences"])
577
- # async def generate_campaign_sequences(campaign_id: int, request: GenerateSequencesRequest):
578
- # """Generate Campaign Sequences using LLM"""
579
- # job_description = request.job_description
580
- # generated_sequences = await generate_sequences_with_llm(job_description)
581
- # save_request = SaveSequencesRequest(sequences=generated_sequences)
582
- # result = await call_smartlead_api("POST", f"campaigns/{campaign_id}/sequences", data=save_request.dict())
583
-
584
- # return {
585
- # "ok": True,
586
- # "message": "Sequences generated and saved successfully",
587
- # "generated_sequences": [seq for seq in generated_sequences],
588
- # "save_result": result
589
- # }
590
-
591
- # # ============================================================================
592
- # # WEBHOOK ENDPOINTS
593
- # # ============================================================================
594
-
595
- # @app.get("/campaigns/{campaign_id}/webhooks", response_model=List[Dict[str, Any]], tags=["Webhooks"])
596
- # async def fetch_webhooks_by_campaign_id(campaign_id: int):
597
- # """Fetch Webhooks By Campaign ID"""
598
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/webhooks")
599
-
600
- # @app.post("/campaigns/{campaign_id}/webhooks", response_model=Dict[str, Any], tags=["Webhooks"])
601
- # async def add_update_campaign_webhook(campaign_id: int, request: WebhookRequest):
602
- # """Add / Update Campaign Webhook"""
603
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/webhooks", data=request.dict())
604
-
605
- # @app.delete("/campaigns/{campaign_id}/webhooks", response_model=Dict[str, Any], tags=["Webhooks"])
606
- # async def delete_campaign_webhook(campaign_id: int, request: WebhookDeleteRequest):
607
- # """Delete Campaign Webhook"""
608
- # return await call_smartlead_api("DELETE", f"campaigns/{campaign_id}/webhooks", data=request.dict())
609
-
610
- # # ============================================================================
611
- # # CLIENT MANAGEMENT ENDPOINTS
612
- # # ============================================================================
613
-
614
- # @app.post("/client/save", response_model=Dict[str, Any], tags=["Clients"])
615
- # async def add_client_to_system(request: ClientRequest):
616
- # """Add Client To System (Whitelabel or not)"""
617
- # return await call_smartlead_api("POST", "client/save", data=request.dict())
618
-
619
- # @app.get("/client", response_model=List[Dict[str, Any]], tags=["Clients"])
620
- # async def fetch_all_clients():
621
- # """Fetch all clients"""
622
- # return await call_smartlead_api("GET", "client")
623
-
624
- # # ============================================================================
625
- # # MESSAGE HISTORY AND REPLY ENDPOINTS
626
- # # ============================================================================
627
-
628
- # @app.get("/campaigns/{campaign_id}/leads/{lead_id}/message-history", response_model=Dict[str, Any], tags=["Messages"])
629
- # async def fetch_lead_message_history_based_on_campaign(campaign_id: int, lead_id: int):
630
- # """Fetch Lead Message History Based On Campaign"""
631
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/leads/{lead_id}/message-history")
632
-
633
- # @app.post("/campaigns/{campaign_id}/reply-email-thread", response_model=Dict[str, Any], tags=["Messages"])
634
- # async def reply_to_lead_from_master_inbox(campaign_id: int, request: MessageHistoryRequest):
635
- # """Reply To Lead From Master Inbox via API"""
636
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/reply-email-thread", data=request.dict())
637
-
638
- # # ============================================================================
639
- # # EMAIL ACCOUNT ENDPOINTS
640
- # # ============================================================================
641
-
642
- # @app.get("/email-accounts", response_model=List[EmailAccount], tags=["Email Accounts"])
643
- # async def list_email_accounts(offset: int = 0, limit: int = 100):
644
- # """List all email accounts with optional pagination"""
645
- # params = {"offset": offset, "limit": limit}
646
- # return await call_smartlead_api("GET", "email-accounts", params=params)
647
-
648
- # @app.post("/email-accounts/save", response_model=Any, tags=["Email Accounts"])
649
- # async def save_email_account(account: Dict[str, Any]):
650
- # """Create an Email Account"""
651
- # return await call_smartlead_api("POST", "email-accounts/save", data=account)
652
-
653
- # @app.post("/email-accounts/reconnect-failed-email-accounts", response_model=Dict[str, Any], tags=["Email Accounts"])
654
- # async def reconnect_failed_email_accounts(body: Optional[Dict] = {}):
655
- # """Reconnect failed email accounts"""
656
- # return await call_smartlead_api("POST", "email-accounts/reconnect-failed-email-accounts", data={})
657
-
658
- # @app.get("/email-accounts/{account_id}", response_model=EmailAccount, tags=["Email Accounts"])
659
- # async def get_email_account(account_id: int):
660
- # """Fetch Email Account By ID"""
661
- # return await call_smartlead_api("GET", f"email-accounts/{account_id}")
662
-
663
- # @app.post("/email-accounts/{account_id}", response_model=Any, tags=["Email Accounts"])
664
- # async def update_email_account(account_id: int, payload: Dict[str, Any]):
665
- # """Update Email Account"""
666
- # return await call_smartlead_api("POST", f"email-accounts/{account_id}", data=payload)
667
-
668
- # @app.post("/email-accounts/{account_id}/warmup", response_model=Any, tags=["Email Accounts"])
669
- # async def set_warmup(account_id: int, payload: WarmupSettingsRequest):
670
- # """Add/Update Warmup To Email Account"""
671
- # return await call_smartlead_api("POST", f"email-accounts/{account_id}/warmup", data=payload.dict(exclude_none=True))
672
-
673
- # @app.get("/email-accounts/{account_id}/warmup-stats", response_model=Any, tags=["Email Accounts"])
674
- # async def get_warmup_stats(account_id: int):
675
- # """Fetch Warmup Stats By Email Account ID"""
676
- # return await call_smartlead_api("GET", f"email-accounts/{account_id}/warmup-stats")
677
-
678
- # @app.get("/campaigns/{campaign_id}/email-accounts", response_model=List[EmailAccount], tags=["Email Accounts"])
679
- # async def list_campaign_email_accounts(campaign_id: int):
680
- # """List all email accounts per campaign"""
681
- # return await call_smartlead_api("GET", f"campaigns/{campaign_id}/email-accounts")
682
-
683
- # @app.post("/campaigns/{campaign_id}/email-accounts", response_model=Any, tags=["Email Accounts"])
684
- # async def add_campaign_email_accounts(campaign_id: int, payload: Dict[str, Any]):
685
- # """Add Email Account To A Campaign"""
686
- # return await call_smartlead_api("POST", f"campaigns/{campaign_id}/email-accounts", data=payload)
687
-
688
- # @app.delete("/campaigns/{campaign_id}/email-accounts", response_model=Any, tags=["Email Accounts"])
689
- # async def remove_campaign_email_accounts(campaign_id: int, payload: Dict[str, Any]):
690
- # """Remove Email Account From A Campaign"""
691
- # return await call_smartlead_api("DELETE", f"campaigns/{campaign_id}/email-accounts", data=payload)
692
-
693
- # # ============================================================================
694
- # # UTILITY ENDPOINTS
695
- # # ============================================================================
696
-
697
- # @app.get("/health", response_model=Dict[str, Any], tags=["Utilities"])
698
- # async def health_check():
699
- # """Health check endpoint to verify API connectivity"""
700
- # try:
701
- # campaigns = await call_smartlead_api("GET", "campaigns")
702
- # return {
703
- # "status": "healthy",
704
- # "message": "Smartlead API is accessible",
705
- # "campaigns_count": len(campaigns) if isinstance(campaigns, list) else 0,
706
- # "timestamp": datetime.now().isoformat()
707
- # }
708
- # except Exception as e:
709
- # return {
710
- # "status": "unhealthy",
711
- # "message": f"Smartlead API connection failed: {str(e)}",
712
- # "timestamp": datetime.now().isoformat()
713
- # }
714
-
715
- # @app.get("/api-info", response_model=Dict[str, Any], tags=["Utilities"])
716
- # async def api_info():
717
- # """Get information about the API and available endpoints"""
718
- # return {
719
- # "name": "Smartlead API - Complete Integration",
720
- # "version": "2.0.0",
721
- # "description": "Comprehensive FastAPI wrapper for Smartlead email automation platform",
722
- # "base_url": SMARTLEAD_BASE_URL,
723
- # "available_endpoints": [
724
- # "Campaign Management",
725
- # "Lead Management",
726
- # "Sequence Management",
727
- # "Webhook Management",
728
- # "Client Management",
729
- # "Message History & Reply",
730
- # "Analytics",
731
- # "Email Account Management"
732
- # ],
733
- # "documentation": "Based on Smartlead API documentation",
734
- # "timestamp": datetime.now().isoformat()
735
- # }
736
-
737
- # # ============================================================================
738
- # # AI SEQUENCE GENERATION FUNCTIONS
739
- # # ============================================================================
740
-
741
- # async def generate_welcome_closing_messages(lead_data: Dict[str, Any]) -> Dict[str, str]:
742
- # class structure(BaseModel):
743
- # welcome_message: str = Field(description="Welcome message for the candidate")
744
- # closing_message: str = Field(description="Closing message for the candidate")
745
-
746
- # """Generate personalized welcome and closing messages using LLM based on candidate details"""
747
-
748
- # if not LANGCHAIN_AVAILABLE:
749
- # return generate_template_welcome_closing_messages(lead_data)
750
-
751
- # try:
752
- # openai_api_key = os.getenv("OPENAI_API_KEY")
753
- # if not openai_api_key:
754
- # print("Warning: OPENAI_API_KEY not set. Using template messages.")
755
- # return generate_template_welcome_closing_messages(lead_data)
756
-
757
- # llm = ChatOpenAI(
758
- # model="gpt-4o-mini",
759
- # temperature=0.7,
760
- # openai_api_key=openai_api_key
761
- # )
762
- # str_llm = llm.with_structured_output(structure, method="function_calling")
763
-
764
- # # Extract relevant information from lead data
765
- # first_name = lead_data.get("first_name", "")
766
- # last_name = lead_data.get("last_name", "")
767
- # company_name = lead_data.get("company_name", "")
768
- # location = lead_data.get("location", "")
769
- # title = lead_data.get("custom_fields", {}).get("Title", "")
770
- # linkedin_profile = lead_data.get("linkedin_profile", "")
771
-
772
- # # Create a summary of the candidate's background
773
- # candidate_info = f"""
774
- # Name: {first_name}
775
- # Company: {company_name}
776
- # Location: {location}
777
- # Title: {title}
778
- # LinkedIn: {linkedin_profile}
779
- # """
780
-
781
- # system_prompt = """You are an expert recruiter who creates personalized welcome and closing messages for email campaigns.
782
-
783
- # Based on the candidate's information, generate:
784
- # 1. A personalized welcome message (2-3 sentences) starting with "Hi first_name then change the line using <br> tag and continue with a friendly introduction about their background/company/role.
785
- # 2. A personalized closing message (1-2 sentences)
786
-
787
- # Requirements:
788
- # - Professional but friendly tone
789
- # - Reference their specific background/company/role when possible
790
- # - Keep messages concise and engaging
791
- # - Make them feel valued and understood
792
-
793
- # IMPORTANT: Respond with ONLY valid JSON. No additional text."""
794
-
795
- # prompt_template = ChatPromptTemplate.from_messages([
796
- # ("system", system_prompt),
797
- # ("human", "Generate personalized messages for this candidate: {candidate_info}")
798
- # ])
799
-
800
- # messages = prompt_template.format_messages(candidate_info=candidate_info)
801
- # response = await str_llm.ainvoke(messages)
802
-
803
- # try:
804
-
805
- # return {
806
- # "welcome_message": response.welcome_message,
807
- # "closing_message": response.closing_message
808
- # }
809
-
810
- # except Exception as parse_error:
811
- # print(f"JSON parsing failed for welcome/closing messages: {parse_error}")
812
- # return generate_template_welcome_closing_messages(lead_data)
813
-
814
- # except Exception as e:
815
- # print(f"Error generating welcome/closing messages with LLM: {str(e)}")
816
- # return generate_template_welcome_closing_messages(lead_data)
817
-
818
- # def generate_template_welcome_closing_messages(lead_data: Dict[str, Any]) -> Dict[str, str]:
819
- # """Generate template-based welcome and closing messages as fallback"""
820
-
821
- # first_name = lead_data.get("first_name", "")
822
- # company_name = lead_data.get("company_name", "")
823
- # title = lead_data.get("custom_fields", {}).get("Title", "")
824
-
825
- # # Personalized welcome message
826
- # if first_name and company_name:
827
- # welcome_message = f"Hi {first_name}, I came across your profile and was impressed by your work at {company_name}."
828
- # elif first_name:
829
- # welcome_message = f"Hi {first_name}, I came across your profile and was impressed by your background."
830
- # elif company_name:
831
- # welcome_message = f"Hi there, I came across your profile and was impressed by your work at {company_name}."
832
- # else:
833
- # welcome_message = "Hi there, I came across your profile and was impressed by your background."
834
-
835
- # # Personalized closing message
836
- # if first_name:
837
- # closing_message = f"Looking forward to connecting with you, {first_name}!"
838
- # else:
839
- # closing_message = "Looking forward to connecting with you!"
840
-
841
- # return {
842
- # "welcome_message": welcome_message,
843
- # "closing_message": closing_message
844
- # }
845
-
846
- # async def generate_sequences_with_llm(job_description: str) -> List[CampaignSequence]:
847
- # class email_seq(BaseModel):
848
- # subject: str = Field(description="Subject line for the email")
849
- # body: str = Field(description="Body of the email")
850
- # class structure(BaseModel):
851
- # introduction: email_seq = Field(description="Email sequence for sequence 1 asking for consent and interest in the role")
852
- # email_sequence_2: email_seq = Field(description="Email sequence for sequence 2 following up on updates and next steps")
853
- # email_sequence_3: email_seq = Field(description="Email sequence for sequence 3 Another variant on following up on updates and next steps")
854
-
855
-
856
-
857
- # """Generate email sequences using LangChain and OpenAI based on job description"""
858
-
859
- # if not LANGCHAIN_AVAILABLE:
860
- # return await generate_template_sequences(job_description)
861
-
862
- # try:
863
- # openai_api_key = os.getenv("OPENAI_API_KEY")
864
- # if not openai_api_key:
865
- # print("Warning: OPENAI_API_KEY not set. Using template sequences.")
866
- # return await generate_template_sequences(job_description)
867
-
868
- # llm = ChatOpenAI(
869
- # model="gpt-4o-mini",
870
- # temperature=0.7,
871
- # openai_api_key=openai_api_key
872
- # )
873
- # str_llm = llm.with_structured_output(structure, method="function_calling")
874
-
875
- # system_prompt = """You are an expert email sequence template generator for recruitment campaigns.
876
-
877
- # Generate ONLY the subject lines and email body content for 3 professional email sequences.
878
- # Write the email on behalf of Ali Taghikhani, CEO SRN.
879
-
880
- # Use the following placeholders in your templates, EXACTLY as written:
881
- # - `{{first_name}}`
882
- # - `{{company}}`
883
- # - `{{title}}`
884
- # - `{{Welcome_Message}}`
885
- # - `{{Closing_Message}}`
886
-
887
- # Email Sequence Structure:
888
- # 1. INTRODUCTION (Day 1): Start the email with `{{Welcome_Message}}`. Ask for consent and interest in the role. End the email with `{{Closing_Message}}` followed by the sender's name and title.
889
- # 2. OUTREACH (Day 3): Provide detailed job information. This is a follow-up, so it should not have a subject line.
890
- # 3. FOLLOW-UP (Day 5): Another follow-up on updates and next steps. Also no subject line.
891
-
892
- # Requirements:
893
- # - The first sequence must start with `{{Welcome_Message}}` and end with `{{Closing_Message}}`.
894
- # - Always end the email with Best regards
895
- # - The second and third sequences are follow-ups and should not have a subject line.
896
- # - All emails should be HTML formatted with proper `<br>` tags.
897
- # - Professional but friendly tone.
898
- # - Include clear call-to-actions.
899
- # - Focus on building consent and trust.
900
-
901
- # IMPORTANT: Respond with ONLY valid JSON. No additional text."""
902
-
903
- # prompt_template = ChatPromptTemplate.from_messages([
904
- # ("system", system_prompt),
905
- # ("human", "Generate email content for this job description: {job_description}")
906
- # ])
907
-
908
- # messages = prompt_template.format_messages(job_description=job_description)
909
- # response = await str_llm.ainvoke(messages)
910
-
911
- # try:
912
- # # Handle the response structure correctly
913
- # sequences = []
914
-
915
- # # Sequence 1: Introduction with A/B variants
916
- # if hasattr(response, 'introduction') and response.introduction:
917
- # # Check if body is a string or dict
918
- # intro_body = response.introduction.body
919
- # if isinstance(intro_body, dict):
920
- # # If it's a dict, extract the content
921
- # intro_body = str(intro_body)
922
-
923
- # sequences.append(CampaignSequence(
924
- # seq_number=1,
925
- # seq_delay_details=SeqDelayDetails(delay_in_days=1),
926
- # seq_variants=[
927
- # SeqVariant(
928
- # subject=response.introduction.subject,
929
- # email_body=intro_body,
930
- # variant_label="A"
931
- # )
932
- # ]
933
- # ))
934
-
935
- # # Sequence 2: Outreach
936
- # if hasattr(response, 'email_sequence_2') and response.email_sequence_2:
937
- # seq2_body = response.email_sequence_2.body
938
- # if isinstance(seq2_body, dict):
939
- # seq2_body = str(seq2_body)
940
-
941
- # sequences.append(CampaignSequence(
942
- # seq_number=2,
943
- # seq_delay_details=SeqDelayDetails(delay_in_days=3),
944
- # subject="",
945
- # email_body=seq2_body
946
- # ))
947
-
948
- # # Sequence 3: Follow-up
949
- # if hasattr(response, 'email_sequence_3') and response.email_sequence_3:
950
- # seq3_body = response.email_sequence_3.body
951
- # if isinstance(seq3_body, dict):
952
- # seq3_body = str(seq3_body)
953
-
954
- # sequences.append(CampaignSequence(
955
- # seq_number=3,
956
- # seq_delay_details=SeqDelayDetails(delay_in_days=5),
957
- # subject="",
958
- # email_body=seq3_body
959
- # ))
960
-
961
- # # Fill with templates if needed
962
- # while len(sequences) < 3:
963
- # if len(sequences) == 0:
964
- # sequences.append(CampaignSequence(
965
- # seq_number=1,
966
- # seq_delay_details=SeqDelayDetails(delay_in_days=1),
967
- # seq_variants=[
968
- # SeqVariant(
969
- # subject=f"Quick question about {job_description}",
970
- # email_body=f"""<p>Hi there,<br><br>
971
- # I came across your profile and noticed your experience in {job_description}.
972
- # I'm reaching out because we have some exciting opportunities that might be a great fit for your background.<br><br>
973
- # Before I share more details, I wanted to ask: Are you currently open to exploring new opportunities in this space?<br><br>
974
- # Would you be interested in hearing more about the roles we have available?<br><br>
975
- # Best regards,<br>
976
- # [Your Name]</p>""",
977
- # variant_label="A"
978
- # ),
979
- # SeqVariant(
980
- # subject=f"Interested in {job_description} opportunities?",
981
- # email_body=f"""<p>Hello,<br><br>
982
- # I hope this message finds you well. I'm a recruiter specializing in {job_description} positions.<br><br>
983
- # I'd love to connect and share some opportunities that align with your expertise.
984
- # Are you currently open to exploring new roles in this space?<br><br>
985
- # If so, I can send you specific details about the positions we have available.<br><br>
986
- # Thanks,<br>
987
- # [Your Name]</p>""",
988
- # variant_label="B"
989
- # )
990
- # ]
991
- # ))
992
- # elif len(sequences) == 1:
993
- # sequences.append(CampaignSequence(
994
- # seq_number=2,
995
- # seq_delay_details=SeqDelayDetails(delay_in_days=3),
996
- # subject="",
997
- # email_body=f"""<p>Hi,<br><br>
998
- # Thanks for your interest! Here are more details about the {job_description} opportunities:<br><br>
999
- # <strong>Role Details:</strong><br>
1000
- # • [Specific responsibilities]<br>
1001
- # • [Required skills and experience]<br>
1002
- # • [Team and company information]<br><br>
1003
- # <strong>Benefits:</strong><br>
1004
- # • [Compensation and benefits]<br>
1005
- # • [Growth opportunities]<br>
1006
- # • [Work environment]<br><br>
1007
- # Would you be interested in a quick call to discuss this role in more detail?<br><br>
1008
- # Best regards,<br>
1009
- # [Your Name]</p>"""
1010
- # ))
1011
- # elif len(sequences) == 2:
1012
- # sequences.append(CampaignSequence(
1013
- # seq_number=3,
1014
- # seq_delay_details=SeqDelayDetails(delay_in_days=5),
1015
- # subject="",
1016
- # email_body=f"""<p>Hi,<br><br>
1017
- # Just wanted to follow up on the {job_description} opportunity I shared.<br><br>
1018
- # Have you had a chance to review the information? I'd love to hear your thoughts and answer any questions.<br><br>
1019
- # If you're interested, I can help schedule next steps. If not, no worries at all!<br><br>
1020
- # Thanks for your time!<br>
1021
- # [Your Name]</p>"""
1022
- # ))
1023
-
1024
- # return sequences
1025
-
1026
- # except Exception as parse_error:
1027
- # print(f"JSON parsing failed: {parse_error}")
1028
- # return await generate_template_sequences(job_description)
1029
-
1030
- # except Exception as e:
1031
- # print(f"Error generating sequences with LLM: {str(e)}")
1032
- # return await generate_template_sequences(job_description)
1033
-
1034
- # def create_sequences_from_content(content: dict, job_description: str) -> List[CampaignSequence]:
1035
- # """Create CampaignSequence objects from parsed LLM content"""
1036
-
1037
- # sequences = []
1038
-
1039
- # # Sequence 1: Introduction with A/B variants
1040
- # if "sequence1_variant_a" in content and "sequence1_variant_b" in content:
1041
- # variants = []
1042
-
1043
- # if "sequence1_variant_a" in content:
1044
- # var_a = content["sequence1_variant_a"]
1045
- # variants.append(SeqVariant(
1046
- # subject=var_a.get("subject", f"Quick question about {job_description}"),
1047
- # email_body=var_a.get("body", ""),
1048
- # variant_label="A"
1049
- # ))
1050
-
1051
- # if "sequence1_variant_b" in content:
1052
- # var_b = content["sequence1_variant_b"]
1053
- # variants.append(SeqVariant(
1054
- # subject=var_b.get("subject", f"Interested in {job_description} opportunities?"),
1055
- # email_body=var_b.get("body", ""),
1056
- # variant_label="B"
1057
- # ))
1058
-
1059
- # sequences.append(CampaignSequence(
1060
- # seq_number=1,
1061
- # seq_delay_details=SeqDelayDetails(delay_in_days=1),
1062
- # seq_variants=variants
1063
- # ))
1064
-
1065
- # # Sequence 2: Outreach
1066
- # if "sequence2" in content:
1067
- # seq2_body = content["sequence2"].get("body", "")
1068
- # sequences.append(CampaignSequence(
1069
- # seq_number=2,
1070
- # seq_delay_details=SeqDelayDetails(delay_in_days=3),
1071
- # subject="",
1072
- # email_body=seq2_body
1073
- # ))
1074
-
1075
- # # Sequence 3: Follow-up
1076
- # if "sequence3" in content:
1077
- # seq3_body = content["sequence3"].get("body", "")
1078
- # sequences.append(CampaignSequence(
1079
- # seq_number=3,
1080
- # seq_delay_details=SeqDelayDetails(delay_in_days=5),
1081
- # subject="",
1082
- # email_body=seq3_body
1083
- # ))
1084
-
1085
- # # Fill with templates if needed
1086
- # while len(sequences) < 3:
1087
- # if len(sequences) == 0:
1088
- # sequences.append(CampaignSequence(
1089
- # seq_number=1,
1090
- # seq_delay_details=SeqDelayDetails(delay_in_days=1),
1091
- # seq_variants=[
1092
- # SeqVariant(
1093
- # subject=f"Quick question about {job_description}",
1094
- # email_body=f"""<p>Hi there,<br><br>
1095
- # I came across your profile and noticed your experience in {job_description}.
1096
- # I'm reaching out because we have some exciting opportunities that might be a great fit for your background.<br><br>
1097
- # Before I share more details, I wanted to ask: Are you currently open to exploring new opportunities in this space?<br><br>
1098
- # Would you be interested in hearing more about the roles we have available?<br><br>
1099
- # Best regards,<br>
1100
- # [Your Name]</p>""",
1101
- # variant_label="A"
1102
- # ),
1103
- # SeqVariant(
1104
- # subject=f"Interested in {job_description} opportunities?",
1105
- # email_body=f"""<p>Hello,<br><br>
1106
- # I hope this message finds you well. I'm a recruiter specializing in {job_description} positions.<br><br>
1107
- # I'd love to connect and share some opportunities that align with your expertise.
1108
- # Are you currently open to exploring new roles in this space?<br><br>
1109
- # If so, I can send you specific details about the positions we have available.<br><br>
1110
- # Thanks,<br>
1111
- # [Your Name]</p>""",
1112
- # variant_label="B"
1113
- # )
1114
- # ]
1115
- # ))
1116
- # elif len(sequences) == 1:
1117
- # sequences.append(CampaignSequence(
1118
- # seq_number=2,
1119
- # seq_delay_details=SeqDelayDetails(delay_in_days=3),
1120
- # subject="",
1121
- # email_body=f"""<p>Hi,<br><br>
1122
- # Thanks for your interest! Here are more details about the {job_description} opportunities:<br><br>
1123
- # <strong>Role Details:</strong><br>
1124
- # • [Specific responsibilities]<br>
1125
- # • [Required skills and experience]<br>
1126
- # • [Team and company information]<br><br>
1127
- # <strong>Benefits:</strong><br>
1128
- # • [Compensation and benefits]<br>
1129
- # • [Growth opportunities]<br>
1130
- # • [Work environment]<br><br>
1131
- # Would you be interested in a quick call to discuss this role in more detail?<br><br>
1132
- # Best regards,<br>
1133
- # [Your Name]</p>"""
1134
- # ))
1135
- # elif len(sequences) == 2:
1136
- # sequences.append(CampaignSequence(
1137
- # seq_number=3,
1138
- # seq_delay_details=SeqDelayDetails(delay_in_days=5),
1139
- # subject="",
1140
- # email_body=f"""<p>Hi,<br><br>
1141
- # Just wanted to follow up on the {job_description} opportunity I shared.<br><br>
1142
- # Have you had a chance to review the information? I'd love to hear your thoughts and answer any questions.<br><br>
1143
- # If you're interested, I can help schedule next steps. If not, no worries at all!<br><br>
1144
- # Thanks for your time!<br>
1145
- # [Your Name]</p>"""
1146
- # ))
1147
-
1148
- # return sequences
1149
-
1150
- # async def generate_template_sequences(job_description: str) -> List[CampaignSequence]:
1151
- # """Generate template-based sequences as fallback"""
1152
-
1153
- # sequences = [
1154
- # CampaignSequence(
1155
- # seq_number=1,
1156
- # seq_delay_details=SeqDelayDetails(delay_in_days=1),
1157
- # seq_variants=[
1158
- # SeqVariant(
1159
- # subject=f"Quick question about {job_description}",
1160
- # email_body=f"""<p>Hi there,<br><br>
1161
- # I came across your profile and noticed your experience in {job_description}.
1162
- # I'm reaching out because we have some exciting opportunities that might be a great fit for your background.<br><br>
1163
- # Before I share more details, I wanted to ask: Are you currently open to exploring new opportunities in this space?<br><br>
1164
- # Would you be interested in hearing more about the roles we have available?<br><br>
1165
- # Best regards,<br>
1166
- # [Your Name]</p>""",
1167
- # variant_label="A"
1168
- # ),
1169
- # SeqVariant(
1170
- # subject=f"Interested in {job_description} opportunities?",
1171
- # email_body=f"""<p>Hello,<br><br>
1172
- # I hope this message finds you well. I'm a recruiter specializing in {job_description} positions.<br><br>
1173
- # I'd love to connect and share some opportunities that align with your expertise.
1174
- # Are you currently open to exploring new roles in this space?<br><br>
1175
- # If so, I can send you specific details about the positions we have available.<br><br>
1176
- # Thanks,<br>
1177
- # [Your Name]</p>""",
1178
- # variant_label="B"
1179
- # )
1180
- # ]
1181
- # ),
1182
- # CampaignSequence(
1183
- # seq_number=2,
1184
- # seq_delay_details=SeqDelayDetails(delay_in_days=3),
1185
- # subject="",
1186
- # email_body=f"""<p>Hi,<br><br>
1187
- # Thanks for your interest! Here are more details about the {job_description} opportunities:<br><br>
1188
- # <strong>Role Details:</strong><br>
1189
- # • [Specific responsibilities]<br>
1190
- # • [Required skills and experience]<br>
1191
- # • [Team and company information]<br><br>
1192
- # <strong>Benefits:</strong><br>
1193
- # • [Compensation and benefits]<br>
1194
- # • [Growth opportunities]<br>
1195
- # • [Work environment]<br><br>
1196
- # Would you be interested in a quick call to discuss this role in more detail?<br><br>
1197
- # Best regards,<br>
1198
- # [Your Name]</p>"""
1199
- # ),
1200
- # CampaignSequence(
1201
- # seq_number=3,
1202
- # seq_delay_details=SeqDelayDetails(delay_in_days=5),
1203
- # subject="",
1204
- # email_body=f"""<p>Hi,<br><br>
1205
- # Just wanted to follow up on the {job_description} opportunity I shared.<br><br>
1206
- # Have you had a chance to review the information? I'd love to hear your thoughts and answer any questions.<br><br>
1207
- # If you're interested, I can help schedule next steps. If not, no worries at all!<br><br>
1208
- # Thanks for your time!<br>
1209
- # [Your Name]</p>"""
1210
- # )
1211
- # ]
1212
-
1213
- # return sequences
1214
-
1215
- # # ============================================================================
1216
- # # RATE LIMITING MIDDLEWARE
1217
- # # ============================================================================
1218
-
1219
- # class RateLimiter:
1220
- # def __init__(self, max_requests: int = 10, window_seconds: int = 2):
1221
- # self.max_requests = max_requests
1222
- # self.window_seconds = window_seconds
1223
- # self.requests = []
1224
-
1225
- # def is_allowed(self) -> bool:
1226
- # now = time.time()
1227
- # # Remove old requests outside the window
1228
- # self.requests = [req_time for req_time in self.requests if now - req_time < self.window_seconds]
1229
-
1230
- # if len(self.requests) >= self.max_requests:
1231
- # return False
1232
-
1233
- # self.requests.append(now)
1234
- # return True
1235
-
1236
- # # Global rate limiter instance
1237
- # rate_limiter = RateLimiter(max_requests=10, window_seconds=2)
1238
-
1239
- # @app.middleware("http")
1240
- # async def rate_limit_middleware(request: Request, call_next):
1241
- # """Rate limiting middleware to respect Smartlead's API limits"""
1242
- # if not rate_limiter.is_allowed():
1243
- # return JSONResponse(
1244
- # status_code=429,
1245
- # content={
1246
- # "error": "Rate limit exceeded",
1247
- # "message": "Too many requests. Please wait before making another request.",
1248
- # "retry_after": 2
1249
- # }
1250
- # )
1251
-
1252
- # response = await call_next(request)
1253
- # return response
1254
-
1255
- # # ============================================================================
1256
- # # ERROR HANDLING
1257
- # # ============================================================================
1258
-
1259
- # @app.exception_handler(HTTPException)
1260
- # async def http_exception_handler(request: Request, exc: HTTPException):
1261
- # """Custom HTTP exception handler"""
1262
- # return JSONResponse(
1263
- # status_code=exc.status_code,
1264
- # content={
1265
- # "error": True,
1266
- # "message": exc.detail,
1267
- # "status_code": exc.status_code,
1268
- # "timestamp": datetime.now().isoformat()
1269
- # }
1270
- # )
1271
-
1272
- # @app.exception_handler(Exception)
1273
- # async def general_exception_handler(request: Request, exc: Exception):
1274
- # """General exception handler"""
1275
- # return JSONResponse(
1276
- # status_code=500,
1277
- # content={
1278
- # "error": True,
1279
- # "message": "Internal server error",
1280
- # "detail": str(exc) if os.getenv("DEBUG", "false").lower() == "true" else "An unexpected error occurred",
1281
- # "timestamp": datetime.now().isoformat()
1282
- # }
1283
- # )
1284
-
1285
- # # ============================================================================
1286
- # # CUSTOM OPENAPI SCHEMA
1287
- # # ============================================================================
1288
-
1289
- # def custom_openapi():
1290
- # if app.openapi_schema:
1291
- # return app.openapi_schema
1292
-
1293
- # openapi_schema = get_openapi(
1294
- # title="Smartlead API - Complete Integration",
1295
- # version="2.0.0",
1296
- # description="""
1297
- # # Smartlead API - Complete Integration
1298
-
1299
- # A comprehensive FastAPI wrapper for the Smartlead email automation platform.
1300
-
1301
- # ## Features
1302
- # - **Campaign Management**: Create, update, and manage email campaigns
1303
- # - **Lead Management**: Add, update, and manage leads across campaigns with AI-powered personalization
1304
- # - **Sequence Management**: Create and manage email sequences with AI generation
1305
- # - **Webhook Management**: Set up webhooks for real-time notifications
1306
- # - **Analytics**: Get detailed campaign analytics and statistics
1307
- # - **Email Account Management**: Manage email accounts and warmup
1308
- # - **Client Management**: Handle client accounts and permissions
1309
-
1310
- # ## AI-Powered Personalization
1311
- # 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:
1312
- # - `Welcome_Message`: Personalized greeting based on candidate's background
1313
- # - `Closing_Message`: Personalized closing statement
1314
-
1315
- # ## Lead Schema
1316
- # The lead schema supports the following structure:
1317
- # ```json
1318
- # {
1319
- # "lead_list": [
1320
- # {
1321
- # "first_name": "Cristiano",
1322
- # "last_name": "Ronaldo",
1323
- # "email": "[email protected]",
1324
- # "phone_number": "0239392029",
1325
- # "company_name": "Manchester United",
1326
- # "website": "mufc.com",
1327
- # "location": "London",
1328
- # "custom_fields": {
1329
- # "Title": "Regional Manager",
1330
- # "First_Line": "Loved your recent post about remote work on Linkedin"
1331
- # },
1332
- # "linkedin_profile": "http://www.linkedin.com/in/cristianoronaldo",
1333
- # "company_url": "mufc.com"
1334
- # }
1335
- # ],
1336
- # "settings": {
1337
- # "ignore_global_block_list": true,
1338
- # "ignore_unsubscribe_list": true,
1339
- # "ignore_duplicate_leads_in_other_campaign": false
1340
- # }
1341
- # }
1342
- # ```
1343
-
1344
- # ## Authentication
1345
- # All requests require a Smartlead API key passed as a query parameter: `?api_key=YOUR_API_KEY`
1346
-
1347
- # ## Rate Limits
1348
- # - 10 requests per 2 seconds (enforced automatically)
1349
-
1350
- # ## Base URL
1351
- # - Smartlead API: `https://server.smartlead.ai/api/v1`
1352
- # """,
1353
- # routes=app.routes,
1354
- # )
1355
-
1356
- # # Add custom tags
1357
- # openapi_schema["tags"] = [
1358
- # {"name": "Campaigns", "description": "Campaign management operations"},
1359
- # {"name": "Leads", "description": "Lead management operations"},
1360
- # {"name": "Sequences", "description": "Email sequence management"},
1361
- # {"name": "Webhooks", "description": "Webhook management"},
1362
- # {"name": "Clients", "description": "Client account management"},
1363
- # {"name": "Messages", "description": "Message history and reply operations"},
1364
- # {"name": "Analytics", "description": "Campaign analytics and statistics"},
1365
- # {"name": "Email Accounts", "description": "Email account management"},
1366
- # {"name": "Utilities", "description": "Utility endpoints"}
1367
- # ]
1368
-
1369
- # app.openapi_schema = openapi_schema
1370
- # return app.openapi_schema
1371
-
1372
- # app.openapi = custom_openapi
1373
-
1374
- # # ============================================================================
1375
- # # MAIN APPLICATION ENTRY POINT
1376
- # # ============================================================================
1377
-
1378
- # if __name__ == "__main__":
1379
- # import uvicorn
1380
-
1381
- # print("�� Starting Smartlead API - Complete Integration")
1382
- # print(f"�� API Documentation: http://localhost:8000/docs")
1383
- # print(f"📖 ReDoc Documentation: http://localhost:8000/redoc")
1384
- # print(f"�� Smartlead Base URL: {SMARTLEAD_BASE_URL}")
1385
- # print(f"⚡ Rate Limit: 10 requests per 2 seconds")
1386
-
1387
- # uvicorn.run(
1388
- # "updated-final:app",
1389
- # host="0.0.0.0",
1390
- # port=8000,
1391
- # reload=True,
1392
- # log_level="info"
1393
- # )
1394
-
1395
-
1396
-
1397
  import os
1398
  import json
1399
  import time
@@ -2428,10 +1032,10 @@ async def generate_sequences_with_llm(job_description: str) -> List[CampaignSequ
2428
  Use the following placeholders in your templates, EXACTLY as written:
2429
  - `{{first_name}}`
2430
  - `{{company}}`
2431
- - `{{title}}`
2432
  - `{{Welcome_Message}}`
2433
  - `{{Closing_Message}}`
2434
-
2435
  Email Sequence Structure:
2436
  1. INTRODUCTION (Day 1): Start the email with `{{Welcome_Message}}`. Ask for consent and interest in the role. End the email with `{{Closing_Message}}` followed by the sender's name and title.
2437
  2. OUTREACH (Day 3): Provide detailed job information. This is a follow-up, so it should not have a subject line.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import json
3
  import time
 
1032
  Use the following placeholders in your templates, EXACTLY as written:
1033
  - `{{first_name}}`
1034
  - `{{company}}`
1035
+ - `{{Title}}`
1036
  - `{{Welcome_Message}}`
1037
  - `{{Closing_Message}}`
1038
+ - Always use double braces for placeholders
1039
  Email Sequence Structure:
1040
  1. INTRODUCTION (Day 1): Start the email with `{{Welcome_Message}}`. Ask for consent and interest in the role. End the email with `{{Closing_Message}}` followed by the sender's name and title.
1041
  2. OUTREACH (Day 3): Provide detailed job information. This is a follow-up, so it should not have a subject line.