ak0601 commited on
Commit
64072e5
·
verified ·
1 Parent(s): 8bbfbd0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +549 -387
app.py CHANGED
@@ -1,20 +1,15 @@
1
  import os
2
- import re
3
  import json
4
  import time
5
- import asyncio
6
  from datetime import datetime
7
  from typing import List, Dict, Any, Optional, Union
8
- from pydantic import BaseModel, Field, EmailStr, field_validator
9
  from fastapi import FastAPI, HTTPException, Query, Depends, Request
10
  from fastapi.responses import JSONResponse, Response
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.openapi.utils import get_openapi
13
  import httpx
14
  from dotenv import load_dotenv
15
- import pandas as pd
16
- import psycopg2
17
- from sqlalchemy import create_engine, inspect, text
18
 
19
  # LangChain and OpenAI imports
20
  try:
@@ -25,30 +20,21 @@ except ImportError:
25
  LANGCHAIN_AVAILABLE = False
26
  print("Warning: LangChain not available. Install with: pip install langchain langchain-openai")
27
 
28
- load_dotenv()
29
 
30
  # Configuration
31
  SMARTLEAD_API_KEY = os.getenv("SMARTLEAD_API_KEY", "your-api-key-here")
32
  SMARTLEAD_BASE_URL = "https://server.smartlead.ai/api/v1"
33
- DB_PARAMS = {
34
- 'dbname': os.getenv("DB_NAME"),
35
- 'user': os.getenv("DB_USER"),
36
- 'password': os.getenv("DB_PASSWORD"),
37
- 'host': os.getenv("DB_HOST"),
38
- 'port': os.getenv("DB_PORT")
39
- }
40
- DATABASE_URL = f"postgresql://{DB_PARAMS['user']}:{DB_PARAMS['password']}@{DB_PARAMS['host']}:{DB_PARAMS['port']}/{DB_PARAMS['dbname']}"
41
 
42
  # Initialize FastAPI app
43
  app = FastAPI(
44
  title="Smartlead API - Complete Integration",
45
- version="2.1.0",
46
  description="Comprehensive FastAPI wrapper for Smartlead email automation platform",
47
  docs_url="/docs",
48
  redoc_url="/redoc"
49
  )
50
 
51
-
52
  # Add CORS middleware
53
  app.add_middleware(
54
  CORSMiddleware,
@@ -96,17 +82,16 @@ class LeadInput(BaseModel):
96
  linkedin_profile: Optional[str] = Field(None, description="Lead's LinkedIn profile URL")
97
  company_url: Optional[str] = Field(None, description="Company website URL")
98
 
99
- @field_validator('custom_fields')
100
- @classmethod
101
  def validate_custom_fields(cls, v):
102
  if v is not None and len(v) > 20:
103
  raise ValueError('Custom fields cannot exceed 20 fields')
104
  return v
105
 
106
- @field_validator('phone_number')
107
- @classmethod
108
  def validate_phone_number(cls, v):
109
  if v is not None:
 
110
  return str(v)
111
  return v
112
 
@@ -148,20 +133,8 @@ class CampaignSequence(BaseModel):
148
  class SaveSequencesRequest(BaseModel):
149
  sequences: List[CampaignSequence] = Field(..., description="List of campaign sequences")
150
 
151
- class Job(BaseModel):
152
- id: str
153
- company_name: Optional[str] = None
154
- job_title: Optional[str] = None
155
- job_location: Optional[str] = None
156
- company_blurb: Optional[str] = None
157
- company_culture: Optional[str] = None
158
- description: Optional[str] = None
159
- company_size: Optional[str] = None
160
- requirements: Optional[str] = None
161
- salary: Optional[str] = None
162
-
163
  class GenerateSequencesRequest(BaseModel):
164
- job_id: str = Field(..., description="Job ID to fetch from database and generate sequences for")
165
 
166
  class Campaign(BaseModel):
167
  id: int
@@ -170,8 +143,8 @@ class Campaign(BaseModel):
170
  updated_at: datetime
171
  status: str
172
  name: str
173
- track_settings: Union[str, List[Any]]
174
- scheduler_cron_value: Optional[Union[str, Dict[str, Any]]] = None
175
  min_time_btwn_emails: int
176
  max_leads_per_day: int
177
  stop_lead_settings: str
@@ -179,7 +152,7 @@ class Campaign(BaseModel):
179
  client_id: Optional[int] = None
180
  enable_ai_esp_matching: bool
181
  send_as_plain_text: bool
182
- follow_up_percentage: Optional[Union[str, int]] = None
183
 
184
  class CampaignListResponse(BaseModel):
185
  campaigns: List[Campaign]
@@ -201,10 +174,10 @@ class WarmupDetails(BaseModel):
201
  status: str
202
  total_sent_count: int
203
  total_spam_count: int
204
- warmup_reputation: str
205
  warmup_key_id: Optional[str] = None
206
  warmup_created_at: Optional[datetime] = None
207
- reply_rate: int
208
  blocked_reason: Optional[str] = None
209
 
210
  class EmailAccount(BaseModel):
@@ -240,6 +213,13 @@ class EmailAccount(BaseModel):
240
  campaign_count: Optional[int] = None
241
  warmup_details: Optional[WarmupDetails] = None
242
 
 
 
 
 
 
 
 
243
  class LeadCategoryUpdateRequest(BaseModel):
244
  category_id: int = Field(..., description="Category ID to assign to the lead")
245
  pause_lead: bool = Field(False, description="Whether to pause the lead after category update")
@@ -282,115 +262,10 @@ class MessageHistoryRequest(BaseModel):
282
  bcc: Optional[str] = Field(None, description="BCC recipients")
283
  add_signature: bool = Field(True, description="Whether to add signature")
284
 
285
- class AddLeadsAndSequencesRequest(BaseModel):
286
- lead_list: List[LeadInput] = Field(..., max_items=100, description="List of leads to add (maximum 100 leads)")
287
- settings: Optional[LeadSettings] = Field(None, description="Settings for lead processing")
288
- job_id: str = Field(..., description="Job ID to fetch from database and generate sequences for")
289
-
290
  # ============================================================================
291
  # HELPER FUNCTIONS
292
  # ============================================================================
293
 
294
- def get_database_connection():
295
- """Get database connection"""
296
- try:
297
- conn_string = f"postgresql://{DB_PARAMS['user']}:{DB_PARAMS['password']}@{DB_PARAMS['host']}:{DB_PARAMS['port']}/{DB_PARAMS['dbname']}"
298
- return create_engine(conn_string)
299
- except Exception as e:
300
- raise HTTPException(status_code=500, detail=f"Database connection failed: {str(e)}")
301
-
302
- def fetch_job_by_id(job_id: str) -> Job:
303
- """Fetch job details from database by ID using pandas DataFrame"""
304
- try:
305
- conn = get_database_connection()
306
- df = pd.read_sql_table("jobs", con=conn)
307
-
308
- # Filter the DataFrame to find the job with the specified ID
309
- job_row = df[df['job_id'] == job_id]
310
-
311
- if job_row.empty:
312
- raise HTTPException(status_code=404, detail=f"Job with ID {job_id} not found")
313
-
314
- # Get the first (and should be only) row
315
-
316
- return Job(
317
- id=str(job_row.iloc[0]['job_id']),
318
- company_name=str(job_row.iloc[0]['company_name']) if pd.notna(job_row.iloc[0]['company_name']) else None,
319
- job_title=str(job_row.iloc[0]['job_title']) if pd.notna(job_row.iloc[0]['job_title']) else None,
320
- job_location=str(job_row.iloc[0]['job_location']) if pd.notna(job_row.iloc[0]['job_location']) else None,
321
- company_blurb=str(job_row.iloc[0]['company_blurb']) if pd.notna(job_row.iloc[0]['company_blurb']) else None,
322
- company_culture=str(job_row.iloc[0]['company_culture']) if pd.notna(job_row.iloc[0]['company_culture']) else None,
323
- description=str(job_row.iloc[0]['description']) if pd.notna(job_row.iloc[0]['description']) else None,
324
- company_size=str(job_row.iloc[0]['company_size']) if pd.notna(job_row.iloc[0]['company_size']) else None,
325
- requirements=str(job_row.iloc[0]['requirements']) if pd.notna(job_row.iloc[0]['requirements']) else None,
326
- salary=str(job_row.iloc[0]['salary']) if pd.notna(job_row.iloc[0]['salary']) else None
327
- )
328
-
329
- except HTTPException:
330
- raise
331
- except Exception as e:
332
- raise HTTPException(status_code=500, detail=f"Error fetching job: {str(e)}")
333
-
334
- def list_available_jobs(limit: int = 10) -> List[Dict[str, Any]]:
335
- """List available jobs from database using pandas DataFrame"""
336
- try:
337
- conn = get_database_connection()
338
- df = pd.read_sql_table("jobs", con=conn)
339
-
340
- # Select only the required columns and limit the results
341
- selected_columns = ['job_id', 'company_name', 'job_title', 'job_location', 'company_size', 'salary']
342
- df_subset = df[selected_columns].head(limit)
343
-
344
- # Convert DataFrame to list of dictionaries
345
- jobs_list = []
346
- for _, row in df_subset.iterrows():
347
- jobs_list.append({
348
- "id": str(row['job_id']), # Ensure ID is returned as string
349
- "company_name": str(row['company_name']) if pd.notna(row['company_name']) else None,
350
- "job_title": str(row['job_title']) if pd.notna(row['job_title']) else None,
351
- "job_location": str(row['job_location']) if pd.notna(row['job_location']) else None,
352
- "company_size": str(row['company_size']) if pd.notna(row['company_size']) else None,
353
- "salary": str(row['salary']) if pd.notna(row['salary']) else None
354
- })
355
-
356
- return jobs_list
357
-
358
- except Exception as e:
359
- raise HTTPException(status_code=500, detail=f"Error fetching jobs: {str(e)}")
360
-
361
- def build_job_description(job: Job) -> str:
362
- """Build a comprehensive job description from job details"""
363
- parts = []
364
-
365
- if job.company_name:
366
- parts.append(f"Company: {job.company_name}")
367
-
368
- if job.job_title:
369
- parts.append(f"Position: {job.job_title}")
370
-
371
- if job.job_location:
372
- parts.append(f"Location: {job.job_location}")
373
-
374
- if job.company_size:
375
- parts.append(f"Company Size: {job.company_size}")
376
-
377
- if job.salary:
378
- parts.append(f"Salary: {job.salary}")
379
-
380
- if job.company_blurb:
381
- parts.append(f"About the Company: {job.company_blurb}")
382
-
383
- if job.company_culture:
384
- parts.append(f"Company Culture: {job.company_culture}")
385
-
386
- if job.description:
387
- parts.append(f"Job Description: {job.description}")
388
-
389
- if job.requirements:
390
- parts.append(f"Requirements: {job.requirements}")
391
-
392
- return "\n\n".join(parts)
393
-
394
  def _get_smartlead_url(endpoint: str) -> str:
395
  return f"{SMARTLEAD_BASE_URL}/{endpoint.lstrip('/')}"
396
 
@@ -404,10 +279,11 @@ async def call_smartlead_api(method: str, endpoint: str, data: Any = None, param
404
 
405
  try:
406
  async with httpx.AsyncClient(timeout=30.0) as client:
407
- if method.upper() in ("GET", "DELETE"):
408
- resp = await client.request(method, url, params=params)
409
- else:
410
- resp = await client.request(method, url, params=params, json=data)
 
411
 
412
  if resp.status_code >= 400:
413
  try:
@@ -505,40 +381,62 @@ async def get_campaign_leads(campaign_id: int, offset: int = 0, limit: int = 100
505
  params = {"offset": offset, "limit": limit}
506
  return await call_smartlead_api("GET", f"campaigns/{campaign_id}/leads", params=params)
507
 
508
- # *** MODIFIED: add_leads_to_campaign now uses asyncio.gather for performance ***
509
  @app.post("/campaigns/{campaign_id}/leads", response_model=Dict[str, Any], tags=["Leads"])
510
  async def add_leads_to_campaign(campaign_id: int, request: AddLeadsRequest):
511
  """Add leads to a campaign by ID with personalized welcome and closing messages"""
 
512
 
513
- async def process_lead(lead: Dict[str, Any]) -> Dict[str, Any]:
514
- """Inner function to process a single lead."""
515
  lead_cleaned = {k: v for k, v in lead.items() if v is not None and v != ""}
516
 
 
517
  try:
518
  personalized_messages = await generate_welcome_closing_messages(lead_cleaned)
 
 
519
  if "custom_fields" not in lead_cleaned:
520
  lead_cleaned["custom_fields"] = {}
521
- lead_cleaned["custom_fields"]["Welcome_Message"] = personalized_messages.get("welcome_message", "")
522
- lead_cleaned["custom_fields"]["Closing_Message"] = personalized_messages.get("closing_message", "")
 
 
 
 
 
523
  except Exception as e:
524
- print(f"Error generating AI messages for {lead.get('email')}: {e}. Falling back to template.")
 
525
  template_messages = generate_template_welcome_closing_messages(lead_cleaned)
526
  if "custom_fields" not in lead_cleaned:
527
  lead_cleaned["custom_fields"] = {}
528
- lead_cleaned["custom_fields"]["Welcome_Message"] = template_messages["welcome_message"]
529
- lead_cleaned["custom_fields"]["Closing_Message"] = template_messages["closing_message"]
 
 
530
 
531
- return lead_cleaned
532
-
533
- # Create a list of concurrent tasks for AI processing
534
- tasks = [process_lead(lead.dict()) for lead in request.lead_list]
535
- processed_leads = await asyncio.gather(*tasks)
536
-
537
- # Prepare the final request data for Smartlead
538
- request_data = {
539
- "lead_list": processed_leads,
540
- "settings": request.settings.dict() if request.settings else LeadSettings().dict()
541
- }
 
 
 
 
 
 
 
 
 
 
 
542
 
543
  return await call_smartlead_api("POST", f"campaigns/{campaign_id}/leads", data=request_data)
544
 
@@ -548,39 +446,6 @@ async def add_bulk_leads(campaign_id: int, leads: List[LeadInput]):
548
  request = AddLeadsRequest(lead_list=leads)
549
  return await add_leads_to_campaign(campaign_id, request)
550
 
551
- @app.post("/campaigns/{campaign_id}/leads-and-sequences", response_model=Dict[str, Any], tags=["Leads"])
552
- async def add_leads_and_generate_sequences(campaign_id: int, request: AddLeadsAndSequencesRequest):
553
- """Add leads to campaign and immediately generate informed sequences using their data"""
554
-
555
- # Step 1: Add leads with personalized messages
556
- leads_request = AddLeadsRequest(lead_list=request.lead_list, settings=request.settings)
557
- leads_result = await add_leads_to_campaign(campaign_id, leads_request)
558
-
559
- # Step 2: Fetch job details and generate informed sequences
560
- try:
561
- job = fetch_job_by_id(request.job_id)
562
- job_description = build_job_description(job)
563
- generated_sequences = await generate_sequences_with_llm(job_description, campaign_id)
564
- save_request = SaveSequencesRequest(sequences=generated_sequences)
565
- sequences_result = await call_smartlead_api("POST", f"campaigns/{campaign_id}/sequences", data=save_request.dict())
566
-
567
- except Exception as e:
568
- print(f"Error generating sequences after adding leads: {str(e)}")
569
- # Fallback to generic sequences
570
- job = fetch_job_by_id(request.job_id)
571
- job_description = build_job_description(job)
572
- generated_sequences = await generate_sequences_with_llm(job_description)
573
- save_request = SaveSequencesRequest(sequences=generated_sequences)
574
- sequences_result = await call_smartlead_api("POST", f"campaigns/{campaign_id}/sequences", data=save_request.dict())
575
-
576
- return {
577
- "ok": True,
578
- "message": "Leads added and informed sequences generated successfully",
579
- "leads_result": leads_result,
580
- "sequences_result": sequences_result,
581
- "generated_sequences": [seq for seq in generated_sequences]
582
- }
583
-
584
  @app.post("/campaigns/{campaign_id}/leads/{lead_id}/resume", response_model=Dict[str, Any], tags=["Leads"])
585
  async def resume_lead_by_campaign_id(campaign_id: int, lead_id: int, request: ResumeLeadRequest):
586
  """Resume Lead By Campaign ID"""
@@ -708,28 +573,17 @@ async def save_campaign_sequences(campaign_id: int, request: SaveSequencesReques
708
  """Save Campaign Sequence"""
709
  return await call_smartlead_api("POST", f"campaigns/{campaign_id}/sequences", data=request.dict())
710
 
711
- # *** MODIFIED: generate_campaign_sequences now uses the corrected AI function ***
712
  @app.post("/campaigns/{campaign_id}/sequences/generate", response_model=Dict[str, Any], tags=["Sequences"])
713
  async def generate_campaign_sequences(campaign_id: int, request: GenerateSequencesRequest):
714
- """Generate a campaign sequence template using AI that leverages personalized custom fields."""
715
- job_id = request.job_id
716
-
717
- # Fetch job details from database
718
- job = fetch_job_by_id(job_id)
719
-
720
- # Build comprehensive job description from job details
721
- job_description = build_job_description(job)
722
-
723
- # Generate the smart template
724
- generated_sequences = await generate_sequences_with_llm(job_description, campaign_id)
725
-
726
- # Save the template to the campaign
727
  save_request = SaveSequencesRequest(sequences=generated_sequences)
728
  result = await call_smartlead_api("POST", f"campaigns/{campaign_id}/sequences", data=save_request.dict())
729
 
730
  return {
731
  "ok": True,
732
- "message": "Sequence template generated and saved successfully. It will use personalized fields for each lead.",
733
  "generated_sequences": [seq for seq in generated_sequences],
734
  "save_result": result
735
  }
@@ -796,6 +650,11 @@ async def save_email_account(account: Dict[str, Any]):
796
  """Create an Email Account"""
797
  return await call_smartlead_api("POST", "email-accounts/save", data=account)
798
 
 
 
 
 
 
799
  @app.get("/email-accounts/{account_id}", response_model=EmailAccount, tags=["Email Accounts"])
800
  async def get_email_account(account_id: int):
801
  """Fetch Email Account By ID"""
@@ -807,9 +666,9 @@ async def update_email_account(account_id: int, payload: Dict[str, Any]):
807
  return await call_smartlead_api("POST", f"email-accounts/{account_id}", data=payload)
808
 
809
  @app.post("/email-accounts/{account_id}/warmup", response_model=Any, tags=["Email Accounts"])
810
- async def set_warmup(account_id: int, payload: Dict[str, Any]):
811
  """Add/Update Warmup To Email Account"""
812
- return await call_smartlead_api("POST", f"email-accounts/{account_id}/warmup", data=payload)
813
 
814
  @app.get("/email-accounts/{account_id}/warmup-stats", response_model=Any, tags=["Email Accounts"])
815
  async def get_warmup_stats(account_id: int):
@@ -831,11 +690,6 @@ async def remove_campaign_email_accounts(campaign_id: int, payload: Dict[str, An
831
  """Remove Email Account From A Campaign"""
832
  return await call_smartlead_api("DELETE", f"campaigns/{campaign_id}/email-accounts", data=payload)
833
 
834
- @app.post("/email-accounts/reconnect-failed-email-accounts", response_model=Dict[str, Any], tags=["Email Accounts"])
835
- async def reconnect_failed_email_accounts():
836
- """Reconnect failed email accounts"""
837
- return await call_smartlead_api("POST", "email-accounts/reconnect-failed-email-accounts")
838
-
839
  # ============================================================================
840
  # UTILITY ENDPOINTS
841
  # ============================================================================
@@ -880,16 +734,6 @@ async def api_info():
880
  "timestamp": datetime.now().isoformat()
881
  }
882
 
883
- @app.get("/jobs", response_model=List[Dict[str, Any]], tags=["Jobs"])
884
- async def get_available_jobs(limit: int = Query(10, ge=1, le=100, description="Number of jobs to return")):
885
- """List available jobs from the database"""
886
- return list_available_jobs(limit)
887
-
888
- @app.get("/jobs/{job_id}", response_model=Job, tags=["Jobs"])
889
- async def get_job_by_id(job_id: str):
890
- """Get job details by ID"""
891
- return fetch_job_by_id(job_id)
892
-
893
  # ============================================================================
894
  # AI SEQUENCE GENERATION FUNCTIONS
895
  # ============================================================================
@@ -911,204 +755,427 @@ async def generate_welcome_closing_messages(lead_data: Dict[str, Any]) -> Dict[s
911
  return generate_template_welcome_closing_messages(lead_data)
912
 
913
  llm = ChatOpenAI(
914
- model="gpt-4o",
915
  temperature=0.7,
916
  openai_api_key=openai_api_key
917
  )
918
- str_llm = llm.with_structured_output(structure)
919
 
 
920
  first_name = lead_data.get("first_name", "")
 
921
  company_name = lead_data.get("company_name", "")
 
922
  title = lead_data.get("custom_fields", {}).get("Title", "")
 
 
 
 
 
 
 
 
 
 
 
 
923
 
924
- candidate_info = f"Name: {first_name}, Company: {company_name}, Title: {title}"
 
 
 
 
 
 
 
 
 
 
925
 
926
- system_prompt = """You are an expert recruiter creating personalized messages. Generate a 2-sentence welcome message and a 1-sentence closing message. For welcome start with Hi name then change the line with <br> tag and then write the welcome message. Be professional and friendly and sound like real human recruitor. Reference their background. Respond with ONLY valid JSON."""
927
  prompt_template = ChatPromptTemplate.from_messages([
928
  ("system", system_prompt),
929
- ("human", "Generate messages for this candidate: {candidate_info}")
930
  ])
931
 
932
  messages = prompt_template.format_messages(candidate_info=candidate_info)
933
  response = await str_llm.ainvoke(messages)
934
 
935
- return {
936
- "welcome_message": response.welcome_message,
937
- "closing_message": response.closing_message
938
- }
 
 
 
 
 
 
 
939
  except Exception as e:
940
  print(f"Error generating welcome/closing messages with LLM: {str(e)}")
941
  return generate_template_welcome_closing_messages(lead_data)
942
 
943
  def generate_template_welcome_closing_messages(lead_data: Dict[str, Any]) -> Dict[str, str]:
944
  """Generate template-based welcome and closing messages as fallback"""
945
- first_name = lead_data.get("first_name", "there")
946
- welcome_message = f"Hi {first_name}, I came across your profile and was impressed by your background."
947
- closing_message = f"Looking forward to connecting with you, {first_name}!"
948
- return {"welcome_message": welcome_message, "closing_message": closing_message}
949
-
950
- # *** MODIFIED: generate_sequences_with_llm now creates a smart template ***
951
- async def generate_sequences_with_llm(job_description: str, campaign_id: Optional[int] = None) -> List[CampaignSequence]:
952
- """Generate an email sequence template using LangChain and OpenAI, optionally informed by campaign lead data."""
953
 
954
- class EmailContent(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
955
  subject: str = Field(description="Subject line for the email")
956
- body: str = Field(description="Body of the email, using placeholders")
 
 
 
 
957
 
958
- class SequenceStructure(BaseModel):
959
- introduction: EmailContent
960
- follow_up_1: EmailContent
961
- follow_up_2: EmailContent
962
 
 
 
 
963
  if not LANGCHAIN_AVAILABLE:
964
- return generate_template_sequences(job_description)
965
 
966
  try:
967
  openai_api_key = os.getenv("OPENAI_API_KEY")
968
  if not openai_api_key:
969
  print("Warning: OPENAI_API_KEY not set. Using template sequences.")
970
- return generate_template_sequences(job_description)
971
 
972
- # If campaign_id is provided, fetch lead data to inform the template
973
- lead_context = ""
974
- if campaign_id:
975
- try:
976
- leads_response = await call_smartlead_api("GET", f"campaigns/{campaign_id}/leads", params={"limit": 10})
977
- campaign_leads = leads_response.get("leads", []) if isinstance(leads_response, dict) else leads_response
978
-
979
- if campaign_leads:
980
- # Sample lead data to inform template generation
981
- sample_leads = campaign_leads[:3]
982
- lead_info = []
983
- for lead in sample_leads:
984
- custom_fields = lead.get("custom_fields", {})
985
- lead_info.append({
986
- "first_name": lead.get("first_name", ""),
987
- "company": lead.get("company_name", ""),
988
- "title": custom_fields.get("Title", ""),
989
- "welcome_msg": custom_fields.get("Welcome_Message", ""),
990
- "closing_msg": custom_fields.get("Closing_Message", "")
991
- })
992
-
993
- lead_context = f"\n\nCampaign Lead Context (sample of {len(campaign_leads)} leads):\n{json.dumps(lead_info, indent=2)}"
994
-
995
- except Exception as e:
996
- print(f"Could not fetch lead data for campaign {campaign_id}: {e}")
997
 
998
- llm = ChatOpenAI(model="gpt-4o", temperature=0.7, openai_api_key=openai_api_key)
999
- structured_llm = llm.with_structured_output(SequenceStructure)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
 
1001
- system_prompt = """You are an expert email sequence template generator for recruitment campaigns on behalf of 'Ali Taghikhani, CEO SRN'.
1002
-
1003
- Your task is to generate a 3-step email sequence template for a given job description.
1004
- Email Sequence Structure:
1005
- 1. INTRODUCTION (Day 1): Ask for consent and interest in the role, In the starting use the welcome message placeholder after the salutation, and in the end use closing message template along with the name and title of sender
1006
- 2. OUTREACH (Day 3): Provide detailed job information
1007
- 3. FOLLOW-UP (Day 5): Follow up on updates and next steps
1008
- Requirements:
1009
- - First sequence will only ask about the consent and interest in the role, no other information is needed.
1010
- - Second and third sequences are follow-ups (no subject line needed) in the 3rd sequence try providing some information about the role and the company.
1011
- - All emails should be HTML formatted with proper <br> tags
1012
- - Professional but friendly tone
1013
- - Include clear call-to-actions
1014
- - Focus on building consent and trust
1015
-
1016
- **CRITICAL FORMATTING RULES:**
1017
- 1. **PLACEHOLDER FORMAT IS ESSENTIAL:** You MUST use double curly braces for all placeholders.
1018
- - **CORRECT:** `{{first_name}}`
1019
- - **INCORRECT:** `{first_name}` or `[first_name]` or `<first_name>`
1020
- 2. **REQUIRED PLACEHOLDERS:** You MUST include `{{Welcome_Message}}` and `{{Closing_Message}}` in the first email. You should also use `{{first_name}}` in follow-ups.
1021
- 3. **FIRST EMAIL STRUCTURE:** The first email's body MUST begin with `{{Welcome_Message}}` and end with `{{Closing_Message}}`.
1022
- 4. **SIGNATURE:** End EVERY email body with `<br><br>Best regards`.
1023
- 5. **EXAMPLE BODY:**
1024
- ```html
1025
- {{Welcome_Message}}<br><br>I saw your profile and was impressed. We have an opening for a Senior Engineer that seems like a great fit.<br><br>{{Closing_Message}}<br><br>Best regards,<br>Ali Taghikhani<br>CEO, SRN>
1026
- ```
1027
- Always try to start the message with the salutation except for the first email.
1028
- If lead context is provided, use it to make the templates more relevant.
1029
- Respond with ONLY a valid JSON object matching the required structure.
1030
- """
1031
-
1032
- prompt = ChatPromptTemplate.from_messages([
1033
  ("system", system_prompt),
1034
- ("human", "Generate the 3-step email sequence template for this job description: {job_description}{lead_context}")
1035
  ])
1036
 
1037
- # By using .partial, we tell LangChain to treat the Smartlead placeholders as literals
1038
- # and not expect them as input variables. This is the correct way to handle this.
1039
- partial_prompt = prompt.partial(
1040
- first_name="",
1041
- company_name="",
1042
- **{"Welcome_Message": "", "Closing_Message": "", "Title": ""}
1043
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1044
 
1045
- chain = partial_prompt | structured_llm
1046
- response = await chain.ainvoke({"job_description": job_description, "lead_context": lead_context})
 
 
 
 
 
 
1047
 
1048
- # Post-process the AI's response to enforce double curly braces.
1049
- # This is a robust way to fix the AI's tendency to use single braces.
1050
- def fix_braces(text: str) -> str:
1051
- if not text:
1052
- return ""
1053
- # This regex finds all occurrences of `{...}` that are not `{{...}}`
1054
- # and replaces them with `{{...}}`.
1055
- return re.sub(r'{([^{}\n]+)}', r'{{\1}}', text)
1056
-
1057
- sequences = [
1058
- CampaignSequence(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1059
  seq_number=1,
1060
  seq_delay_details=SeqDelayDetails(delay_in_days=1),
1061
- seq_variants=[SeqVariant(
1062
- subject=fix_braces(response.introduction.subject),
1063
- email_body=fix_braces(response.introduction.body),
1064
- variant_label="A"
1065
- )]
1066
- ),
1067
- CampaignSequence(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1068
  seq_number=2,
1069
  seq_delay_details=SeqDelayDetails(delay_in_days=3),
1070
- subject="", # Same thread
1071
- email_body=fix_braces(response.follow_up_1.body)
1072
- ),
1073
- CampaignSequence(
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
  seq_number=3,
1075
  seq_delay_details=SeqDelayDetails(delay_in_days=5),
1076
- subject="", # Same thread
1077
- email_body=fix_braces(response.follow_up_2.body)
1078
- )
1079
- ]
1080
- return sequences
1081
-
1082
- except Exception as e:
1083
- print(f"Error generating sequences with LLM: {str(e)}. Falling back to template.")
1084
- return generate_template_sequences(job_description)
1085
-
1086
- def generate_template_sequences(job_description: str) -> List[CampaignSequence]:
1087
- """Generate template-based sequences as fallback, using correct placeholders."""
1088
 
1089
- # This is the corrected structure for the first email
1090
- first_email_body = f"""<p>{{{{custom.Welcome_Message}}}}<br><br>
1091
- I'm reaching out because we have some exciting opportunities for a {job_description} that might be a great fit for your background. Are you currently open to exploring new roles?<br><br>
1092
- {{{{custom.Closing_Message}}}}<br><br>
1093
- Best regards,<br>Ali Taghikhani<br>CEO, SRN</p>"""
1094
-
1095
- follow_up_1_body = f"""<p>Hi {{{{first_name}}}},<br><br>
1096
- Just wanted to follow up on my previous email regarding the {job_description} role. I'd love to hear your thoughts when you have a moment.<br><br>
1097
- Best regards,<br>Ali Taghikhani<br>CEO, SRN</p>"""
1098
-
1099
- follow_up_2_body = f"""<p>Hi {{{{first_name}}}},<br><br>
1100
- Checking in one last time about the {job_description} opportunity. If the timing isn't right, no worries at all. Otherwise, I look forward to hearing from you.<br><br>
1101
- Best regards,<br>Ali Taghikhani<br>CEO, SRN</p>"""
1102
 
 
 
 
1103
  sequences = [
1104
  CampaignSequence(
1105
  seq_number=1,
1106
  seq_delay_details=SeqDelayDetails(delay_in_days=1),
1107
  seq_variants=[
1108
  SeqVariant(
1109
- subject=f"Regarding a {job_description} opportunity",
1110
- email_body=first_email_body,
 
 
 
 
 
 
1111
  variant_label="A"
 
 
 
 
 
 
 
 
 
 
 
1112
  )
1113
  ]
1114
  ),
@@ -1116,15 +1183,33 @@ Best regards,<br>Ali Taghikhani<br>CEO, SRN</p>"""
1116
  seq_number=2,
1117
  seq_delay_details=SeqDelayDetails(delay_in_days=3),
1118
  subject="",
1119
- email_body=follow_up_1_body
 
 
 
 
 
 
 
 
 
 
 
 
1120
  ),
1121
- CampaignSequence(
1122
  seq_number=3,
1123
  seq_delay_details=SeqDelayDetails(delay_in_days=5),
1124
  subject="",
1125
- email_body=follow_up_2_body
 
 
 
 
 
1126
  )
1127
  ]
 
1128
  return sequences
1129
 
1130
  # ============================================================================
@@ -1139,12 +1224,16 @@ class RateLimiter:
1139
 
1140
  def is_allowed(self) -> bool:
1141
  now = time.time()
 
1142
  self.requests = [req_time for req_time in self.requests if now - req_time < self.window_seconds]
 
1143
  if len(self.requests) >= self.max_requests:
1144
  return False
 
1145
  self.requests.append(now)
1146
  return True
1147
 
 
1148
  rate_limiter = RateLimiter(max_requests=10, window_seconds=2)
1149
 
1150
  @app.middleware("http")
@@ -1153,8 +1242,13 @@ async def rate_limit_middleware(request: Request, call_next):
1153
  if not rate_limiter.is_allowed():
1154
  return JSONResponse(
1155
  status_code=429,
1156
- content={"error": "Rate limit exceeded"}
 
 
 
 
1157
  )
 
1158
  response = await call_next(request)
1159
  return response
1160
 
@@ -1167,7 +1261,12 @@ async def http_exception_handler(request: Request, exc: HTTPException):
1167
  """Custom HTTP exception handler"""
1168
  return JSONResponse(
1169
  status_code=exc.status_code,
1170
- content={"error": True, "message": exc.detail}
 
 
 
 
 
1171
  )
1172
 
1173
  @app.exception_handler(Exception)
@@ -1176,9 +1275,10 @@ async def general_exception_handler(request: Request, exc: Exception):
1176
  return JSONResponse(
1177
  status_code=500,
1178
  content={
1179
- "error": True,
1180
  "message": "Internal server error",
1181
- "detail": str(exc) if os.getenv("DEBUG") else None
 
1182
  }
1183
  )
1184
 
@@ -1192,11 +1292,68 @@ def custom_openapi():
1192
 
1193
  openapi_schema = get_openapi(
1194
  title="Smartlead API - Complete Integration",
1195
- version="2.1.0",
1196
- description="A comprehensive FastAPI wrapper for the Smartlead email automation platform.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1197
  routes=app.routes,
1198
  )
1199
 
 
1200
  openapi_schema["tags"] = [
1201
  {"name": "Campaigns", "description": "Campaign management operations"},
1202
  {"name": "Leads", "description": "Lead management operations"},
@@ -1221,11 +1378,16 @@ app.openapi = custom_openapi
1221
  if __name__ == "__main__":
1222
  import uvicorn
1223
 
1224
- print("Starting Smartlead API - Complete Integration")
 
 
 
 
 
1225
  uvicorn.run(
1226
- "__main__:app",
1227
  host="0.0.0.0",
1228
  port=8000,
1229
  reload=True,
1230
  log_level="info"
1231
- )
 
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:
 
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,
 
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
 
 
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
 
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
 
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]
 
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):
 
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")
 
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
 
 
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:
 
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
 
 
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"""
 
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
  }
 
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"""
 
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):
 
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
  # ============================================================================
 
734
  "timestamp": datetime.now().isoformat()
735
  }
736
 
 
 
 
 
 
 
 
 
 
 
737
  # ============================================================================
738
  # AI SEQUENCE GENERATION FUNCTIONS
739
  # ============================================================================
 
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
  ),
 
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
  # ============================================================================
 
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")
 
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
 
 
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)
 
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
 
 
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"},
 
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
+ )