|
import gradio as gr |
|
import requests |
|
import os |
|
|
|
|
|
proxycurl_api_key = os.getenv("PROXYCURL_API_KEY") |
|
groq_api_key = os.getenv("GROQ_CLOUD_API_KEY") |
|
firecrawl_api_key = os.getenv("FIRECRAWL_API_KEY") |
|
|
|
class AutonomousEmailAgent: |
|
def __init__(self, linkedin_url, company_name, role, word_limit, user_name, email, phone, linkedin): |
|
self.linkedin_url = linkedin_url |
|
self.company_name = company_name |
|
self.role = role |
|
self.word_limit = word_limit |
|
self.user_name = user_name |
|
self.email = email |
|
self.phone = phone |
|
self.linkedin = linkedin |
|
self.bio = None |
|
self.skills = [] |
|
self.experiences = [] |
|
self.company_info = None |
|
self.role_description = None |
|
|
|
|
|
def autonomous_reasoning(self): |
|
print("Autonomous Reasoning: Letting the LLM fully reason and act on available data...") |
|
|
|
|
|
reasoning_prompt = f""" |
|
You are an autonomous agent responsible for generating a job application email. |
|
|
|
Here’s the current data: |
|
- LinkedIn profile: {self.linkedin_url} |
|
- Company Name: {self.company_name} |
|
- Role: {self.role} |
|
- Candidate's Bio: {self.bio} |
|
- Candidate's Skills: {', '.join(self.skills)} |
|
- Candidate's Experiences: {', '.join([exp['title'] for exp in self.experiences])} |
|
- Company Information: {self.company_info} |
|
- Role Description: {self.role_description} |
|
|
|
Based on this data, decide if it is sufficient to generate the email. If some information is missing or insufficient, respond with: |
|
1. "scrape" to fetch more data from the company website. |
|
2. "generate_email" to proceed with the email generation. |
|
3. "fallback" to use default values. |
|
|
|
After generating the email, reflect on whether the content aligns with the role and company and whether any improvements are needed. Respond clearly with one of the above options. |
|
""" |
|
|
|
|
|
url = "https://api.groq.com/openai/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {groq_api_key}", |
|
"Content-Type": "application/json", |
|
} |
|
|
|
data = { |
|
"messages": [{"role": "user", "content": reasoning_prompt}], |
|
"model": "llama3-8b-8192" |
|
} |
|
|
|
response = requests.post(url, headers=headers, json=data) |
|
if response.status_code == 200: |
|
reasoning_output = response.json()["choices"][0]["message"]["content"].strip() |
|
print("LLM Reasoning Output:", reasoning_output) |
|
|
|
|
|
return self.act_on_llm_instructions(reasoning_output) |
|
else: |
|
print(f"Error: {response.status_code}, {response.text}") |
|
return "Error: Unable to complete reasoning." |
|
|
|
|
|
def act_on_llm_instructions(self, reasoning_output): |
|
|
|
instruction = reasoning_output.lower().strip() |
|
|
|
if "scrape" in instruction: |
|
|
|
self.fetch_company_info_with_firecrawl() |
|
|
|
return self.autonomous_reasoning() |
|
|
|
elif "generate_email" in instruction: |
|
|
|
return self.generate_email() |
|
|
|
elif "fallback" in instruction: |
|
|
|
print("Action: Using fallback values for missing data.") |
|
if not self.company_info: |
|
self.company_info = "A leading company in its field." |
|
if not self.role_description: |
|
self.role_description = f"The role of {self.role} involves leadership and team management." |
|
return self.generate_email() |
|
|
|
else: |
|
|
|
print("Error: Unrecognized instruction from LLM. Proceeding with available data.") |
|
return self.generate_email() |
|
|
|
|
|
def fetch_linkedin_data(self): |
|
if not self.linkedin_url: |
|
print("Action: No LinkedIn URL provided, using default bio.") |
|
self.bio = "A professional with diverse experience." |
|
self.skills = ["Adaptable", "Hardworking"] |
|
self.experiences = ["Worked across various industries"] |
|
else: |
|
print("Action: Fetching LinkedIn data via Proxycurl.") |
|
headers = {"Authorization": f"Bearer {proxycurl_api_key}"} |
|
url = f"https://nubela.co/proxycurl/api/v2/linkedin?url={self.linkedin_url}" |
|
response = requests.get(url, headers=headers) |
|
if response.status_code == 200: |
|
data = response.json() |
|
self.bio = data.get("summary", "No bio available") |
|
self.skills = data.get("skills", []) |
|
self.experiences = data.get("experiences", []) |
|
else: |
|
print("Error: Unable to fetch LinkedIn profile. Using default bio.") |
|
self.bio = "A professional with diverse experience." |
|
self.skills = ["Adaptable", "Hardworking"] |
|
self.experiences = ["Worked across various industries"] |
|
|
|
|
|
def fetch_company_info_with_firecrawl(self): |
|
if not self.company_name: |
|
print("Action: No company name provided, using default company info.") |
|
self.company_info = "A leading company in its field." |
|
else: |
|
print(f"Action: Fetching company info for {self.company_name} using Firecrawl.") |
|
headers = {"Authorization": f"Bearer {firecrawl_api_key}"} |
|
firecrawl_url = "https://api.firecrawl.dev/v1/scrape" |
|
data = { |
|
"url": f"https://{self.company_name}.com", |
|
"patterns": ["description", "about", "careers", "company overview"] |
|
} |
|
|
|
response = requests.post(firecrawl_url, json=data, headers=headers) |
|
if response.status_code == 200: |
|
firecrawl_data = response.json() |
|
self.company_info = firecrawl_data.get("description", "No detailed company info available.") |
|
print(f"Company info fetched: {self.company_info}") |
|
else: |
|
print(f"Error: Unable to fetch company info via Firecrawl. Using default info.") |
|
self.company_info = "A leading company in its field." |
|
|
|
|
|
def generate_email(self): |
|
print("Action: Generating the email with the gathered information.") |
|
|
|
linkedin_text = f"Please find my LinkedIn profile at {self.linkedin}" if self.linkedin else "" |
|
|
|
|
|
prompt = f""" |
|
Write a professional email applying for the {self.role} position at {self.company_name}. |
|
|
|
Use the following information: |
|
- The candidate’s LinkedIn bio: {self.bio}. |
|
- The candidate’s most relevant skills: {', '.join(self.skills)}. |
|
- The candidate’s professional experience: {', '.join([exp['title'] for exp in self.experiences])}. |
|
|
|
Please research the company's public information. If no company-specific information is available, use general knowledge about the company's industry. |
|
|
|
Tailor the email dynamically to the role of **{self.role}** at {self.company_name}, aligning the candidate's skills and experiences with the expected responsibilities of the role and the company’s operations. |
|
|
|
{linkedin_text} |
|
|
|
Remove references to job posting sources unless provided. Use the LinkedIn URL for the candidate and do not include placeholders. |
|
|
|
End the email with this signature: |
|
Best regards, |
|
{self.user_name} |
|
Email: {self.email} |
|
Phone: {self.phone} |
|
LinkedIn: {self.linkedin} |
|
|
|
The email should not exceed {self.word_limit} words. |
|
""" |
|
|
|
url = "https://api.groq.com/openai/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {groq_api_key}", |
|
"Content-Type": "application/json", |
|
} |
|
|
|
data = { |
|
"messages": [{"role": "user", "content": prompt}], |
|
"model": "llama3-8b-8192" |
|
} |
|
|
|
response = requests.post(url, headers=headers, json=data) |
|
if response.status_code == 200: |
|
return response.json()["choices"][0]["message"]["content"].strip() |
|
else: |
|
print(f"Error: {response.status_code}, {response.text}") |
|
return "Error generating email. Please check your API key or try again later." |
|
|
|
|
|
def run(self): |
|
self.fetch_linkedin_data() |
|
|
|
return self.autonomous_reasoning() |
|
|
|
|
|
def gradio_ui(): |
|
|
|
name_input = gr.Textbox(label="Your Name", placeholder="Enter your name") |
|
company_input = gr.Textbox(label="Company Name or URL", placeholder="Enter the company name or website URL") |
|
role_input = gr.Textbox(label="Role Applying For", placeholder="Enter the role you are applying for") |
|
email_input = gr.Textbox(label="Your Email Address", placeholder="Enter your email address") |
|
phone_input = gr.Textbox(label="Your Phone Number", placeholder="Enter your phone number") |
|
linkedin_input = gr.Textbox(label="Your LinkedIn URL", placeholder="Enter your LinkedIn profile URL") |
|
word_limit_slider = gr.Slider(minimum=50, maximum=300, step=10, label="Email Word Limit", value=150) |
|
|
|
|
|
email_output = gr.Textbox(label="Generated Email", placeholder="Your generated email will appear here", lines=10) |
|
|
|
|
|
def create_email(name, company_name, role, email, phone, linkedin_url, word_limit): |
|
agent = AutonomousEmailAgent(linkedin_url, company_name, role, word_limit, name, email, phone, linkedin_url) |
|
return agent.run() |
|
|
|
|
|
demo = gr.Interface( |
|
fn=create_email, |
|
inputs=[name_input, company_input, role_input, email_input, phone_input, linkedin_input, word_limit_slider], |
|
outputs=[email_output], |
|
title="Email Writing AI Agent with ReAct", |
|
description="Generate a professional email for a job application using LinkedIn data, company info, and role description.", |
|
allow_flagging="never" |
|
) |
|
|
|
|
|
demo.launch() |
|
|
|
|
|
if __name__ == "__main__": |
|
gradio_ui() |
|
|