Spaces:
Build error
Build error
UnivAI001 commited on
Commit ·
6798e8f
0
Parent(s):
initial commit - AI automation agents
Browse files- .gitignore +5 -0
- Job Finder/app.py +102 -0
- Job Finder/cover_letter.py +61 -0
- Job Finder/fetcher.py +90 -0
- Job Finder/formatter.py +52 -0
- Job Finder/requirements.txt +8 -0
- Lead Agent/agents.py +41 -0
- Lead Agent/app.py +42 -0
- Lead Agent/crew.py +48 -0
- Lead Agent/requirements.txt +9 -0
- Lead Agent/tasks.py +67 -0
- Lead Agent/tools.py +53 -0
- agent.py +29 -0
- app.py +20 -0
- main_app.py +197 -0
- requirements.txt +12 -0
- root_tools.py +51 -0
- tools.py +43 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.venv
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
*.pyo
|
Job Finder/app.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pdfplumber
|
| 3 |
+
from fetcher import fetch_jobs
|
| 4 |
+
from formatter import format_jobs
|
| 5 |
+
from cover_letter import generate_cover_letter
|
| 6 |
+
|
| 7 |
+
# store jobs globally so cover letter tab can access them
|
| 8 |
+
job_store = []
|
| 9 |
+
|
| 10 |
+
def read_cv(cv_file):
|
| 11 |
+
if cv_file is None:
|
| 12 |
+
return ""
|
| 13 |
+
try:
|
| 14 |
+
with pdfplumber.open(cv_file.name) as pdf:
|
| 15 |
+
text = ""
|
| 16 |
+
for page in pdf.pages:
|
| 17 |
+
text += page.extract_text() or ""
|
| 18 |
+
return text
|
| 19 |
+
except Exception as e:
|
| 20 |
+
return f"Could not read CV: {e}"
|
| 21 |
+
|
| 22 |
+
def search_jobs(job_title):
|
| 23 |
+
global job_store
|
| 24 |
+
jobs = fetch_jobs(job_title)
|
| 25 |
+
job_store = jobs
|
| 26 |
+
if not jobs:
|
| 27 |
+
return "No jobs found. Try a broader search term."
|
| 28 |
+
formatted = format_jobs(jobs, job_title, "")
|
| 29 |
+
return formatted
|
| 30 |
+
|
| 31 |
+
def create_cover_letter(job_number, cv_file):
|
| 32 |
+
global job_store
|
| 33 |
+
cv_text = read_cv(cv_file)
|
| 34 |
+
|
| 35 |
+
if not cv_text:
|
| 36 |
+
return "Please upload your CV first."
|
| 37 |
+
|
| 38 |
+
if not job_store:
|
| 39 |
+
return "Please search for jobs first."
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
index = int(job_number) - 1
|
| 43 |
+
job = job_store[index]
|
| 44 |
+
except Exception:
|
| 45 |
+
return "Invalid job number. Please enter a number from the job results."
|
| 46 |
+
|
| 47 |
+
return generate_cover_letter(
|
| 48 |
+
job["title"],
|
| 49 |
+
job["company"],
|
| 50 |
+
job["description"],
|
| 51 |
+
cv_text
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
with gr.Blocks(title="AI Job Finder Agent") as ui:
|
| 55 |
+
gr.Markdown("# 🤖 AI Job Finder Agent")
|
| 56 |
+
gr.Markdown("Find remote jobs and generate tailored cover letters from your CV")
|
| 57 |
+
|
| 58 |
+
with gr.Tab("🔍 Find Jobs"):
|
| 59 |
+
job_input = gr.Textbox(
|
| 60 |
+
label="What role are you looking for?",
|
| 61 |
+
placeholder="e.g. Python Developer, Designer, Data Analyst..."
|
| 62 |
+
)
|
| 63 |
+
search_btn = gr.Button("Find Jobs", variant="primary")
|
| 64 |
+
job_results = gr.Markdown()
|
| 65 |
+
status_text = gr.Textbox(label="Status", interactive=False, value="Idle")
|
| 66 |
+
|
| 67 |
+
def search_jobs_with_status(job_title):
|
| 68 |
+
if not job_title or not job_title.strip():
|
| 69 |
+
return "", "Please enter a search query."
|
| 70 |
+
return search_jobs(job_title), "Search completed"
|
| 71 |
+
|
| 72 |
+
search_btn.click(
|
| 73 |
+
fn=search_jobs_with_status,
|
| 74 |
+
inputs=[job_input],
|
| 75 |
+
outputs=[job_results, status_text]
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
with gr.Tab("✉️ Generate Cover Letter"):
|
| 79 |
+
gr.Markdown("Upload your CV and enter the job number from the search results")
|
| 80 |
+
cv_upload = gr.File(label="Upload CV (PDF)", file_types=[".pdf"])
|
| 81 |
+
job_number = gr.Textbox(
|
| 82 |
+
label="Job Number",
|
| 83 |
+
placeholder="e.g. 1, 2, 3..."
|
| 84 |
+
)
|
| 85 |
+
cl_btn = gr.Button("Generate Cover Letter", variant="primary")
|
| 86 |
+
cl_output = gr.Markdown()
|
| 87 |
+
cl_status = gr.Textbox(label="Status", interactive=False, value="Idle")
|
| 88 |
+
|
| 89 |
+
def create_cover_letter_with_status(job_number, cv_file):
|
| 90 |
+
if not job_number or not str(job_number).isdigit():
|
| 91 |
+
return "", "Invalid job number"
|
| 92 |
+
if not cv_file:
|
| 93 |
+
return "", "Upload CV first"
|
| 94 |
+
return create_cover_letter(job_number, cv_file), "Cover letter generated"
|
| 95 |
+
|
| 96 |
+
cl_btn.click(
|
| 97 |
+
fn=create_cover_letter_with_status,
|
| 98 |
+
inputs=[job_number, cv_upload],
|
| 99 |
+
outputs=[cl_output, cl_status]
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
ui.launch()
|
Job Finder/cover_letter.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from groq import Groq
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
env_file = Path(__file__).parent / ".env"
|
| 7 |
+
if not env_file.exists():
|
| 8 |
+
env_file = Path(__file__).parent.parent / ".env"
|
| 9 |
+
load_dotenv(env_file)
|
| 10 |
+
|
| 11 |
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 12 |
+
|
| 13 |
+
def extract_cv_info(cv_text):
|
| 14 |
+
response = client.chat.completions.create(
|
| 15 |
+
model="llama-3.3-70b-versatile",
|
| 16 |
+
messages=[
|
| 17 |
+
{
|
| 18 |
+
"role": "system",
|
| 19 |
+
"content": "Extract key information from this CV. Return only: full name, key skills, years of experience, and notable achievements."
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"role": "user",
|
| 23 |
+
"content": cv_text
|
| 24 |
+
}
|
| 25 |
+
]
|
| 26 |
+
)
|
| 27 |
+
return response.choices[0].message.content
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def generate_cover_letter(job_title, company, job_description, cv_text):
|
| 31 |
+
cv_info = extract_cv_info(cv_text)
|
| 32 |
+
|
| 33 |
+
response = client.chat.completions.create(
|
| 34 |
+
model="llama-3.3-70b-versatile",
|
| 35 |
+
messages=[
|
| 36 |
+
{
|
| 37 |
+
"role": "system",
|
| 38 |
+
"content": "You are an expert career coach. Write concise compelling cover letters in 3 paragraphs. End with the applicant's full name only."
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"role": "user",
|
| 42 |
+
"content": f"""
|
| 43 |
+
Write a tailored cover letter for this job:
|
| 44 |
+
|
| 45 |
+
Job Title: {job_title}
|
| 46 |
+
Company: {company}
|
| 47 |
+
Job Description: {job_description}
|
| 48 |
+
|
| 49 |
+
Applicant Info extracted from CV:
|
| 50 |
+
{cv_info}
|
| 51 |
+
|
| 52 |
+
Rules:
|
| 53 |
+
- 3 paragraphs max
|
| 54 |
+
- Tailor specifically to the job description
|
| 55 |
+
- End with applicant's full name only, no generic sign-offs
|
| 56 |
+
"""
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
)
|
| 60 |
+
return response.choices[0].message.content
|
| 61 |
+
|
Job Finder/fetcher.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
env_file = Path(__file__).parent / ".env"
|
| 9 |
+
if not env_file.exists():
|
| 10 |
+
env_file = Path(__file__).parent.parent / ".env"
|
| 11 |
+
load_dotenv(env_file)
|
| 12 |
+
|
| 13 |
+
ADZUNA_APP_ID = os.getenv("ADZUNA_APP_ID")
|
| 14 |
+
ADZUNA_APP_KEY = os.getenv("ADZUNA_APP_KEY")
|
| 15 |
+
|
| 16 |
+
DEFAULT_DATE_RANGE = 30
|
| 17 |
+
|
| 18 |
+
def filter_by_date(jobs, days=DEFAULT_DATE_RANGE):
|
| 19 |
+
cutoff = datetime.now() - timedelta(days=days)
|
| 20 |
+
filtered = []
|
| 21 |
+
for job in jobs:
|
| 22 |
+
pub_date = job.get("publication_date", "")
|
| 23 |
+
if pub_date:
|
| 24 |
+
try:
|
| 25 |
+
job_date = datetime.strptime(pub_date[:10], "%Y-%m-%d")
|
| 26 |
+
if job_date >= cutoff:
|
| 27 |
+
filtered.append(job)
|
| 28 |
+
except Exception:
|
| 29 |
+
filtered.append(job)
|
| 30 |
+
else:
|
| 31 |
+
filtered.append(job)
|
| 32 |
+
return filtered
|
| 33 |
+
|
| 34 |
+
def fetch_remotive(job_title):
|
| 35 |
+
try:
|
| 36 |
+
url = f"https://remotive.com/api/remote-jobs?search={job_title}&limit=15"
|
| 37 |
+
response = requests.get(url, timeout=10)
|
| 38 |
+
jobs = response.json().get("jobs", [])
|
| 39 |
+
results = []
|
| 40 |
+
for job in jobs:
|
| 41 |
+
soup = BeautifulSoup(job.get("description", ""), "html.parser")
|
| 42 |
+
clean_description = soup.get_text()[:600]
|
| 43 |
+
results.append({
|
| 44 |
+
"title": job.get("title"),
|
| 45 |
+
"company": job.get("company_name"),
|
| 46 |
+
"location": "Remote",
|
| 47 |
+
"url": job.get("url"),
|
| 48 |
+
"description": clean_description,
|
| 49 |
+
"publication_date": job.get("publication_date", "")[:10]
|
| 50 |
+
})
|
| 51 |
+
return results
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Remotive error: {e}")
|
| 54 |
+
return []
|
| 55 |
+
|
| 56 |
+
def fetch_adzuna(job_title):
|
| 57 |
+
try:
|
| 58 |
+
countries = ["gb", "us", "au", "ca"]
|
| 59 |
+
results = []
|
| 60 |
+
for country in countries:
|
| 61 |
+
url = (
|
| 62 |
+
f"https://api.adzuna.com/v1/api/jobs/{country}/search/1"
|
| 63 |
+
f"?app_id={ADZUNA_APP_ID}"
|
| 64 |
+
f"&app_key={ADZUNA_APP_KEY}"
|
| 65 |
+
f"&what={job_title}"
|
| 66 |
+
f"&results_per_page=5"
|
| 67 |
+
)
|
| 68 |
+
response = requests.get(url, timeout=10)
|
| 69 |
+
jobs = response.json().get("results", [])
|
| 70 |
+
for job in jobs:
|
| 71 |
+
results.append({
|
| 72 |
+
"title": job.get("title"),
|
| 73 |
+
"company": job.get("company", {}).get("display_name", "Unknown"),
|
| 74 |
+
"location": job.get("location", {}).get("display_name", country.upper()),
|
| 75 |
+
"url": job.get("redirect_url"),
|
| 76 |
+
"description": job.get("description", "")[:600],
|
| 77 |
+
"publication_date": job.get("created", "")[:10]
|
| 78 |
+
})
|
| 79 |
+
return results
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"Adzuna error: {e}")
|
| 82 |
+
return []
|
| 83 |
+
|
| 84 |
+
def fetch_jobs(job_title, days=DEFAULT_DATE_RANGE):
|
| 85 |
+
remotive_jobs = fetch_remotive(job_title)
|
| 86 |
+
adzuna_jobs = fetch_adzuna(job_title)
|
| 87 |
+
all_jobs = remotive_jobs + adzuna_jobs
|
| 88 |
+
recent_jobs = filter_by_date(all_jobs, days=days)
|
| 89 |
+
return recent_jobs
|
| 90 |
+
|
Job Finder/formatter.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from groq import Groq
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
env_file = Path(__file__).parent / ".env"
|
| 7 |
+
if not env_file.exists():
|
| 8 |
+
env_file = Path(__file__).parent.parent / ".env"
|
| 9 |
+
load_dotenv(env_file)
|
| 10 |
+
|
| 11 |
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 12 |
+
|
| 13 |
+
def format_jobs(jobs, job_title, experience_level):
|
| 14 |
+
if not jobs:
|
| 15 |
+
return "No jobs found. Try different search terms."
|
| 16 |
+
|
| 17 |
+
raw = ""
|
| 18 |
+
for i, job in enumerate(jobs):
|
| 19 |
+
raw += f"""
|
| 20 |
+
Job {i+1}:
|
| 21 |
+
Title: {job['title']}
|
| 22 |
+
Company: {job['company']}
|
| 23 |
+
Location: {job['location']}
|
| 24 |
+
URL: {job['url']}
|
| 25 |
+
Description: {job['description']}
|
| 26 |
+
---
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
response = client.chat.completions.create(
|
| 30 |
+
model="llama-3.3-70b-versatile",
|
| 31 |
+
messages=[
|
| 32 |
+
{
|
| 33 |
+
"role": "system",
|
| 34 |
+
"content": "You are a job search assistant. Format job listings clearly and helpfully."
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"role": "user",
|
| 38 |
+
"content": f"""
|
| 39 |
+
Here are raw job listings for a {experience_level} {job_title}.
|
| 40 |
+
Format each one cleanly with:
|
| 41 |
+
- Job title and company
|
| 42 |
+
- Location
|
| 43 |
+
- Key requirements from description
|
| 44 |
+
- Direct apply link
|
| 45 |
+
|
| 46 |
+
Raw data:
|
| 47 |
+
{raw}
|
| 48 |
+
"""
|
| 49 |
+
}
|
| 50 |
+
]
|
| 51 |
+
)
|
| 52 |
+
return response.choices[0].message.content
|
Job Finder/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
groq
|
| 2 |
+
gradio
|
| 3 |
+
requests
|
| 4 |
+
python-dotenv
|
| 5 |
+
pdfplumber
|
| 6 |
+
beautifulsoup4
|
| 7 |
+
BeautifulSoup4
|
| 8 |
+
huggingface_hub
|
Lead Agent/agents.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from crewai import Agent, LLM
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
llm = LLM(
|
| 8 |
+
model="groq/llama-3.3-70b-versatile",
|
| 9 |
+
api_key=os.getenv("GROQ_API_KEY")
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
researcher = Agent(
|
| 13 |
+
role="Lead Researcher",
|
| 14 |
+
goal="Find and profile businesses matching the target audience",
|
| 15 |
+
backstory="""You are an expert business researcher.
|
| 16 |
+
You find detailed information about companies and
|
| 17 |
+
identify what they do and who they serve.""",
|
| 18 |
+
llm=llm,
|
| 19 |
+
verbose=True
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
analyst = Agent(
|
| 23 |
+
role="Business Analyst",
|
| 24 |
+
goal="Analyze each lead and identify pain points relevant to the service being offered",
|
| 25 |
+
backstory="""You are a sharp business analyst who understands
|
| 26 |
+
company needs. You identify gaps and opportunities where
|
| 27 |
+
a service could genuinely help a business.""",
|
| 28 |
+
llm=llm,
|
| 29 |
+
verbose=True
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
writer = Agent(
|
| 33 |
+
role="Outreach Specialist",
|
| 34 |
+
goal="Write personalized compelling outreach messages for each lead",
|
| 35 |
+
backstory="""You are an expert copywriter specializing in
|
| 36 |
+
cold outreach. You write messages that feel personal,
|
| 37 |
+
reference specific details about the company, and
|
| 38 |
+
clearly communicate value without being salesy.""",
|
| 39 |
+
llm=llm,
|
| 40 |
+
verbose=True
|
| 41 |
+
)
|
Lead Agent/app.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from crew import run_lead_gen
|
| 3 |
+
|
| 4 |
+
def generate_leads(target_audience, service, sender_name):
|
| 5 |
+
if not target_audience or not service or not sender_name:
|
| 6 |
+
return "Please fill in all fields.", "Idle"
|
| 7 |
+
|
| 8 |
+
# This call is synchronous, but gradio will show progress once a long-running request starts.
|
| 9 |
+
result = run_lead_gen(target_audience, service, sender_name)
|
| 10 |
+
return result, "Completed"
|
| 11 |
+
|
| 12 |
+
with gr.Blocks(title="AI Lead Gen Agent") as ui:
|
| 13 |
+
gr.Markdown("# 🤖 AI Lead Generation & Outreach Agent")
|
| 14 |
+
gr.Markdown("Find leads and generate personalized outreach messages automatically")
|
| 15 |
+
|
| 16 |
+
with gr.Row():
|
| 17 |
+
with gr.Column():
|
| 18 |
+
target_input = gr.Textbox(
|
| 19 |
+
label="Target Audience",
|
| 20 |
+
placeholder="e.g. digital marketing agencies in London"
|
| 21 |
+
)
|
| 22 |
+
service_input = gr.Textbox(
|
| 23 |
+
label="Your Service",
|
| 24 |
+
placeholder="e.g. I build AI automation tools for businesses"
|
| 25 |
+
)
|
| 26 |
+
name_input = gr.Textbox(
|
| 27 |
+
label="Your Name",
|
| 28 |
+
placeholder="e.g. Uche"
|
| 29 |
+
)
|
| 30 |
+
run_btn = gr.Button("Generate Leads", variant="primary")
|
| 31 |
+
|
| 32 |
+
status = gr.Textbox(label="Progress", interactive=False, value="Idle")
|
| 33 |
+
output = gr.Markdown()
|
| 34 |
+
|
| 35 |
+
run_btn.click(
|
| 36 |
+
fn=generate_leads,
|
| 37 |
+
inputs=[target_input, service_input, name_input],
|
| 38 |
+
outputs=[output, status],
|
| 39 |
+
show_progress=True
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
ui.launch()
|
Lead Agent/crew.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Ensure this folder has priority for local imports (avoids root `tools.py` conflict)
|
| 5 |
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 6 |
+
if THIS_DIR not in sys.path:
|
| 7 |
+
sys.path.insert(0, THIS_DIR)
|
| 8 |
+
|
| 9 |
+
from crewai import Crew, Process
|
| 10 |
+
from agents import researcher, analyst, writer
|
| 11 |
+
from tasks import create_tasks
|
| 12 |
+
from tools import gather_leads
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def run_lead_gen(target_audience, service, sender_name):
|
| 16 |
+
# Step 0 - improve search query to favor real business sites over directory pages
|
| 17 |
+
search_query = f"{target_audience} official website contact"
|
| 18 |
+
|
| 19 |
+
# Step 1 - gather raw leads using tools
|
| 20 |
+
raw_leads = gather_leads(search_query)
|
| 21 |
+
|
| 22 |
+
if not raw_leads:
|
| 23 |
+
return "No leads found. Try a different target audience."
|
| 24 |
+
# format leads for tasks
|
| 25 |
+
leads_data = ""
|
| 26 |
+
for i, lead in enumerate(raw_leads):
|
| 27 |
+
leads_data += f"""
|
| 28 |
+
Lead {i+1}:
|
| 29 |
+
Name: {lead['name']}
|
| 30 |
+
Website: {lead['website']}
|
| 31 |
+
Summary: {lead['summary']}
|
| 32 |
+
Details: {lead['details']}
|
| 33 |
+
---
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# Step 2 - create tasks with lead data
|
| 37 |
+
tasks = create_tasks(leads_data, service, sender_name, target_audience)
|
| 38 |
+
|
| 39 |
+
# Step 3 - assemble and run crew
|
| 40 |
+
crew = Crew(
|
| 41 |
+
agents=[researcher, analyst, writer],
|
| 42 |
+
tasks=tasks,
|
| 43 |
+
process=Process.sequential,
|
| 44 |
+
verbose=True
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
result = crew.kickoff()
|
| 48 |
+
return str(result)
|
Lead Agent/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
crewai
|
| 2 |
+
crewai-tools
|
| 3 |
+
groq
|
| 4 |
+
gradio
|
| 5 |
+
requests
|
| 6 |
+
beautifulsoup4
|
| 7 |
+
duckduckgo-search
|
| 8 |
+
python-dotenv
|
| 9 |
+
ddgs
|
Lead Agent/tasks.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Task
|
| 2 |
+
from agents import researcher, analyst, writer
|
| 3 |
+
|
| 4 |
+
def create_tasks(leads_data, service, sender_name, target_audience):
|
| 5 |
+
|
| 6 |
+
research_task = Task(
|
| 7 |
+
description=f"""
|
| 8 |
+
Review this list of potential leads for '{target_audience}':
|
| 9 |
+
|
| 10 |
+
{leads_data}
|
| 11 |
+
|
| 12 |
+
For each lead extract and summarize:
|
| 13 |
+
- Company name
|
| 14 |
+
- What they do
|
| 15 |
+
- Who they serve
|
| 16 |
+
- Their website
|
| 17 |
+
|
| 18 |
+
Return a clean structured list of leads.
|
| 19 |
+
""",
|
| 20 |
+
expected_output="A structured list of leads with company name, description, and website",
|
| 21 |
+
agent=researcher
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
analysis_task = Task(
|
| 25 |
+
description=f"""
|
| 26 |
+
Using the researched leads, analyze each company.
|
| 27 |
+
|
| 28 |
+
The service being offered is: {service}
|
| 29 |
+
|
| 30 |
+
For each company identify:
|
| 31 |
+
- Their likely pain points
|
| 32 |
+
- Why they would benefit from this service
|
| 33 |
+
- One specific detail that makes them a strong fit
|
| 34 |
+
|
| 35 |
+
Be specific and realistic.
|
| 36 |
+
""",
|
| 37 |
+
expected_output="Analysis of each lead with pain points and fit assessment",
|
| 38 |
+
agent=analyst
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
outreach_task = Task(
|
| 42 |
+
description=f"""
|
| 43 |
+
Write a personalized outreach message for each lead.
|
| 44 |
+
|
| 45 |
+
Sender name: {sender_name}
|
| 46 |
+
Service offered: {service}
|
| 47 |
+
|
| 48 |
+
Each message must:
|
| 49 |
+
- Open by referencing something specific about their business
|
| 50 |
+
- Clearly explain the value of the service in one sentence
|
| 51 |
+
- End with a simple low-pressure call to action
|
| 52 |
+
- Be under 150 words
|
| 53 |
+
- Feel human and genuine, not salesy
|
| 54 |
+
|
| 55 |
+
Format output as:
|
| 56 |
+
|
| 57 |
+
## [Company Name]
|
| 58 |
+
**Website:** [url]
|
| 59 |
+
**Message:**
|
| 60 |
+
[outreach message]
|
| 61 |
+
---
|
| 62 |
+
""",
|
| 63 |
+
expected_output="Personalized outreach messages for each lead formatted cleanly",
|
| 64 |
+
agent=writer
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
return [research_task, analysis_task, outreach_task]
|
Lead Agent/tools.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import time
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from ddgs import DDGS
|
| 5 |
+
|
| 6 |
+
SKIP_DOMAINS = [
|
| 7 |
+
"tripadvisor", "tiktok", "facebook", "instagram",
|
| 8 |
+
"twitter", "yelp", "google", "wikipedia", "youtube",
|
| 9 |
+
"infoguidenigeria", "nairaland"
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
def search_leads(query, max_results=10):
|
| 13 |
+
try:
|
| 14 |
+
time.sleep(2)
|
| 15 |
+
with DDGS() as ddgs:
|
| 16 |
+
results = list(ddgs.text(query, max_results=max_results))
|
| 17 |
+
return results
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print(f"Search error: {e}")
|
| 20 |
+
return []
|
| 21 |
+
|
| 22 |
+
def scrape_website(url):
|
| 23 |
+
try:
|
| 24 |
+
response = requests.get(url, timeout=5)
|
| 25 |
+
response.raise_for_status()
|
| 26 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 27 |
+
paragraphs = soup.find_all("p")
|
| 28 |
+
content = " ".join([p.get_text() for p in paragraphs[:15]])
|
| 29 |
+
return content[:800]
|
| 30 |
+
except Exception:
|
| 31 |
+
return ""
|
| 32 |
+
|
| 33 |
+
def gather_leads(target_audience):
|
| 34 |
+
results = search_leads(target_audience)
|
| 35 |
+
leads = []
|
| 36 |
+
for r in results:
|
| 37 |
+
url = r.get("href", "")
|
| 38 |
+
title = r.get("title", "")
|
| 39 |
+
body = r.get("body", "")
|
| 40 |
+
|
| 41 |
+
# skip directories and social media
|
| 42 |
+
if url and any(domain in url.lower() for domain in SKIP_DOMAINS):
|
| 43 |
+
continue
|
| 44 |
+
|
| 45 |
+
if url:
|
| 46 |
+
extra_info = scrape_website(url)
|
| 47 |
+
leads.append({
|
| 48 |
+
"name": title,
|
| 49 |
+
"website": url,
|
| 50 |
+
"summary": body,
|
| 51 |
+
"details": extra_info
|
| 52 |
+
})
|
| 53 |
+
return leads
|
agent.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from groq import Groq
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from root_tools import gather_research
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 9 |
+
|
| 10 |
+
def generate_report(topic):
|
| 11 |
+
raw_content, sources = gather_research(topic) # unpack both now
|
| 12 |
+
|
| 13 |
+
response = client.chat.completions.create(
|
| 14 |
+
model="llama-3.3-70b-versatile",
|
| 15 |
+
messages=[
|
| 16 |
+
{
|
| 17 |
+
"role": "system",
|
| 18 |
+
"content": "You are an expert research analyst. Write clear structured reports."
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"role": "user",
|
| 22 |
+
"content": f"Using this research data, write a detailed structured report on: {topic}\n\nData:\n{raw_content}"
|
| 23 |
+
}
|
| 24 |
+
]
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
report = response.choices[0].message.content
|
| 28 |
+
sources_section = "\n\n---\n## Sources\n" + "\n".join(sources)
|
| 29 |
+
return report + sources_section
|
app.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from agent import generate_report
|
| 3 |
+
|
| 4 |
+
def run_agent(topic):
|
| 5 |
+
if not topic or not topic.strip():
|
| 6 |
+
return "", "Please enter a valid topic."
|
| 7 |
+
|
| 8 |
+
# indicate in-progress state with status message
|
| 9 |
+
output_text = generate_report(topic)
|
| 10 |
+
return output_text, "Completed"
|
| 11 |
+
|
| 12 |
+
ui = gr.Interface(
|
| 13 |
+
fn=run_agent,
|
| 14 |
+
inputs=gr.Textbox(placeholder="Enter any topic...", label="Research Topic"),
|
| 15 |
+
outputs=[gr.Markdown(label="Research Report"), gr.Textbox(label="Status", interactive=False)],
|
| 16 |
+
title="AI Research & Report Agent",
|
| 17 |
+
description="Enter a topic and get a full research report instantly"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
ui.launch()
|
main_app.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import importlib.util
|
| 4 |
+
|
| 5 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 6 |
+
|
| 7 |
+
# Add structural paths for submodules
|
| 8 |
+
if BASE_DIR not in sys.path:
|
| 9 |
+
sys.path.insert(0, BASE_DIR)
|
| 10 |
+
|
| 11 |
+
JOB_FINDER_DIR = os.path.join(BASE_DIR, "Job Finder")
|
| 12 |
+
LEAD_AGENT_DIR = os.path.join(BASE_DIR, "Lead Agent")
|
| 13 |
+
|
| 14 |
+
if JOB_FINDER_DIR not in sys.path:
|
| 15 |
+
sys.path.insert(0, JOB_FINDER_DIR)
|
| 16 |
+
if LEAD_AGENT_DIR not in sys.path:
|
| 17 |
+
sys.path.insert(0, LEAD_AGENT_DIR)
|
| 18 |
+
|
| 19 |
+
import gradio as gr
|
| 20 |
+
from agent import generate_report
|
| 21 |
+
|
| 22 |
+
# Dynamic loader helper
|
| 23 |
+
|
| 24 |
+
def load_module_from_path(name, path):
|
| 25 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 26 |
+
module = importlib.util.module_from_spec(spec)
|
| 27 |
+
spec.loader.exec_module(module)
|
| 28 |
+
return module
|
| 29 |
+
|
| 30 |
+
# Lazy module caches: each tab can fail independently without crashing the whole app.
|
| 31 |
+
job_fetcher = None
|
| 32 |
+
job_formatter = None
|
| 33 |
+
job_cover_letter = None
|
| 34 |
+
lead_crew = None
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def ensure_job_modules_loaded():
|
| 38 |
+
global job_fetcher, job_formatter, job_cover_letter
|
| 39 |
+
if job_fetcher and job_formatter and job_cover_letter:
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
job_fetcher = load_module_from_path("job_fetcher", os.path.join(JOB_FINDER_DIR, "fetcher.py"))
|
| 43 |
+
job_formatter = load_module_from_path("job_formatter", os.path.join(JOB_FINDER_DIR, "formatter.py"))
|
| 44 |
+
job_cover_letter = load_module_from_path("job_cover_letter", os.path.join(JOB_FINDER_DIR, "cover_letter.py"))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def ensure_lead_module_loaded():
|
| 48 |
+
global lead_crew
|
| 49 |
+
if lead_crew:
|
| 50 |
+
return
|
| 51 |
+
|
| 52 |
+
lead_crew = load_module_from_path("lead_crew", os.path.join(LEAD_AGENT_DIR, "crew.py"))
|
| 53 |
+
|
| 54 |
+
# Job Finder state
|
| 55 |
+
job_store = []
|
| 56 |
+
|
| 57 |
+
def search_jobs_wrapper(job_title):
|
| 58 |
+
global job_store
|
| 59 |
+
if not job_title or not job_title.strip():
|
| 60 |
+
return "Please enter a job title."
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
ensure_job_modules_loaded()
|
| 64 |
+
jobs = job_fetcher.fetch_jobs(job_title)
|
| 65 |
+
except Exception as e:
|
| 66 |
+
return f"Job Finder failed to load: {e}"
|
| 67 |
+
|
| 68 |
+
job_store = jobs
|
| 69 |
+
if not jobs:
|
| 70 |
+
return "No jobs found. Try a broader search term."
|
| 71 |
+
try:
|
| 72 |
+
return job_formatter.format_jobs(jobs, job_title, "")
|
| 73 |
+
except Exception as e:
|
| 74 |
+
return f"Job formatting failed: {e}"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def search_jobs_with_status(job_title):
|
| 78 |
+
return search_jobs_wrapper(job_title), "Completed"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def create_cover_letter_wrapper(job_number, cv_file):
|
| 82 |
+
global job_store
|
| 83 |
+
try:
|
| 84 |
+
ensure_job_modules_loaded()
|
| 85 |
+
except Exception as e:
|
| 86 |
+
return f"Job Finder failed to load: {e}"
|
| 87 |
+
|
| 88 |
+
if cv_file is None:
|
| 89 |
+
return "Please upload your CV first."
|
| 90 |
+
|
| 91 |
+
cv_text = ""
|
| 92 |
+
try:
|
| 93 |
+
import pdfplumber
|
| 94 |
+
with pdfplumber.open(cv_file.name) as pdf:
|
| 95 |
+
for page in pdf.pages:
|
| 96 |
+
cv_text += page.extract_text() or ""
|
| 97 |
+
except Exception as e:
|
| 98 |
+
return f"Could not read CV: {e}"
|
| 99 |
+
|
| 100 |
+
if not job_store:
|
| 101 |
+
return "Please search for jobs first."
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
index = int(job_number) - 1
|
| 105 |
+
job = job_store[index]
|
| 106 |
+
except Exception:
|
| 107 |
+
return "Invalid job number. Please enter a number from the job results."
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
return job_cover_letter.generate_cover_letter(
|
| 111 |
+
job["title"], job["company"], job["description"], cv_text
|
| 112 |
+
)
|
| 113 |
+
except Exception as e:
|
| 114 |
+
return f"Cover letter generation failed: {e}"
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def create_cover_letter_with_status(job_number, cv_file):
|
| 118 |
+
return create_cover_letter_wrapper(job_number, cv_file), "Completed"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def generate_report_with_status(topic):
|
| 122 |
+
if not topic or not topic.strip():
|
| 123 |
+
return "Please enter a topic.", "Idle"
|
| 124 |
+
try:
|
| 125 |
+
return generate_report(topic), "Completed"
|
| 126 |
+
except Exception as e:
|
| 127 |
+
return f"Research agent failed: {e}", "Failed"
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def generate_leads_wrapper(target_audience, service, sender_name):
|
| 131 |
+
if not target_audience or not service or not sender_name:
|
| 132 |
+
return "Please fill in all fields.", "Idle"
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
ensure_lead_module_loaded()
|
| 136 |
+
result = lead_crew.run_lead_gen(target_audience, service, sender_name)
|
| 137 |
+
return result, "Completed"
|
| 138 |
+
except Exception as e:
|
| 139 |
+
return f"Lead Agent failed: {e}", "Failed"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
with gr.Blocks(title="AI All-in-One Agents") as ui:
|
| 143 |
+
gr.Markdown("# AI All-in-One Agents\nUse the tabs to switch between Research, Job Finder, and Lead Gen agents.")
|
| 144 |
+
|
| 145 |
+
with gr.Tab("Research & Report"):
|
| 146 |
+
topic_input = gr.Textbox(label="Topic", placeholder="Enter research topic...", lines=1)
|
| 147 |
+
topic_btn = gr.Button("Generate Report")
|
| 148 |
+
topic_output = gr.Markdown()
|
| 149 |
+
topic_status = gr.Textbox(label="Status", interactive=False, value="Idle")
|
| 150 |
+
topic_btn.click(
|
| 151 |
+
fn=generate_report_with_status,
|
| 152 |
+
inputs=[topic_input],
|
| 153 |
+
outputs=[topic_output, topic_status],
|
| 154 |
+
show_progress=True,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
with gr.Tab("Job Finder"):
|
| 158 |
+
job_input = gr.Textbox(label="Job title", placeholder="e.g. Python Developer")
|
| 159 |
+
job_btn = gr.Button("Find Jobs")
|
| 160 |
+
jobs_output = gr.Markdown()
|
| 161 |
+
jobs_status = gr.Textbox(label="Status", interactive=False, value="Idle")
|
| 162 |
+
job_btn.click(
|
| 163 |
+
fn=search_jobs_with_status,
|
| 164 |
+
inputs=[job_input],
|
| 165 |
+
outputs=[jobs_output, jobs_status],
|
| 166 |
+
show_progress=True,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
gr.Markdown("---")
|
| 170 |
+
cv_upload = gr.File(label="Upload CV (PDF)", file_types=[".pdf"])
|
| 171 |
+
job_number = gr.Textbox(label="Job number", placeholder="e.g. 1")
|
| 172 |
+
cover_btn = gr.Button("Generate Cover Letter")
|
| 173 |
+
cover_output = gr.Markdown()
|
| 174 |
+
cover_status = gr.Textbox(label="Status", interactive=False, value="Idle")
|
| 175 |
+
cover_btn.click(
|
| 176 |
+
fn=create_cover_letter_with_status,
|
| 177 |
+
inputs=[job_number, cv_upload],
|
| 178 |
+
outputs=[cover_output, cover_status],
|
| 179 |
+
show_progress=True,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
with gr.Tab("Lead Gen & Outreach"):
|
| 183 |
+
target_input = gr.Textbox(label="Target Audience", placeholder="e.g. digital marketing agencies")
|
| 184 |
+
service_input = gr.Textbox(label="Your Service", placeholder="e.g. AI automation consulting")
|
| 185 |
+
name_input = gr.Textbox(label="Your Name", placeholder="e.g. Uche")
|
| 186 |
+
lead_btn = gr.Button("Generate Leads")
|
| 187 |
+
lead_output = gr.Markdown()
|
| 188 |
+
status_output = gr.Textbox(label="Status", interactive=False, value="Idle")
|
| 189 |
+
|
| 190 |
+
lead_btn.click(
|
| 191 |
+
fn=generate_leads_wrapper,
|
| 192 |
+
inputs=[target_input, service_input, name_input],
|
| 193 |
+
outputs=[lead_output, status_output],
|
| 194 |
+
show_progress=True,
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
ui.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
groq
|
| 2 |
+
gradio
|
| 3 |
+
requests
|
| 4 |
+
beautifulsoup4
|
| 5 |
+
BeautifulSoup4
|
| 6 |
+
duckduckgo-search
|
| 7 |
+
ddgs
|
| 8 |
+
python-dotenv
|
| 9 |
+
pdfplumber
|
| 10 |
+
huggingface_hub
|
| 11 |
+
crewai
|
| 12 |
+
crewai-tools
|
root_tools.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from ddgs import DDGS
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def search_web(query, max_results=5):
|
| 8 |
+
try:
|
| 9 |
+
with DDGS() as ddgs:
|
| 10 |
+
return list(ddgs.text(query, max_results=max_results))
|
| 11 |
+
except Exception:
|
| 12 |
+
return []
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def scrape_page(url, timeout=8):
|
| 16 |
+
try:
|
| 17 |
+
r = requests.get(url, timeout=timeout, headers={"User-Agent": "Mozilla/5.0"})
|
| 18 |
+
r.raise_for_status()
|
| 19 |
+
content_type = (r.headers.get("Content-Type") or "").lower()
|
| 20 |
+
# Ignore binary/non-HTML responses (PDFs commonly produce gibberish text in reports).
|
| 21 |
+
if "pdf" in content_type or ("html" not in content_type and "text" not in content_type):
|
| 22 |
+
return ""
|
| 23 |
+
soup = BeautifulSoup(r.text, "html.parser")
|
| 24 |
+
for script in soup(["script", "style"]):
|
| 25 |
+
script.decompose()
|
| 26 |
+
text = "\n".join(line.strip() for line in soup.stripped_strings)
|
| 27 |
+
return text[:3500]
|
| 28 |
+
except Exception:
|
| 29 |
+
return ""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def gather_research(topic):
|
| 33 |
+
results = search_web(topic + " research report -filetype:pdf")
|
| 34 |
+
all_content = ""
|
| 35 |
+
sources = []
|
| 36 |
+
|
| 37 |
+
for result in results[:5]:
|
| 38 |
+
url = result.get("href", "")
|
| 39 |
+
title = result.get("title", url)
|
| 40 |
+
if not url:
|
| 41 |
+
continue
|
| 42 |
+
|
| 43 |
+
content = scrape_page(url)
|
| 44 |
+
if content:
|
| 45 |
+
all_content += content + "\n"
|
| 46 |
+
sources.append(f"- [{title}]({url})")
|
| 47 |
+
|
| 48 |
+
if not all_content:
|
| 49 |
+
all_content = f"General background context on {topic}."
|
| 50 |
+
|
| 51 |
+
return all_content, sources
|
tools.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ddgs import DDGS
|
| 2 |
+
import requests
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
def search_web(query):
|
| 7 |
+
try:
|
| 8 |
+
with DDGS() as ddgs:
|
| 9 |
+
results = list(ddgs.text(query, max_results=5))
|
| 10 |
+
return results
|
| 11 |
+
except Exception as e:
|
| 12 |
+
print(f"Search error: {e}")
|
| 13 |
+
return []
|
| 14 |
+
|
| 15 |
+
def scrape_page(url):
|
| 16 |
+
try:
|
| 17 |
+
response = requests.get(url, timeout=5)
|
| 18 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 19 |
+
paragraphs = soup.find_all("p")
|
| 20 |
+
content = " ".join([p.get_text() for p in paragraphs[:20]])
|
| 21 |
+
return content
|
| 22 |
+
except:
|
| 23 |
+
return ""
|
| 24 |
+
|
| 25 |
+
def gather_research(topic):
|
| 26 |
+
time.sleep(2)
|
| 27 |
+
results = search_web(topic)
|
| 28 |
+
|
| 29 |
+
if not results:
|
| 30 |
+
return "No search results found. Try again in a few minutes.", []
|
| 31 |
+
|
| 32 |
+
all_content = ""
|
| 33 |
+
sources = []
|
| 34 |
+
|
| 35 |
+
for r in results:
|
| 36 |
+
url = r.get("href", "") # back to href - confirmed correct
|
| 37 |
+
title = r.get("title", url)
|
| 38 |
+
|
| 39 |
+
if url:
|
| 40 |
+
all_content += scrape_page(url) + "\n"
|
| 41 |
+
sources.append(f"- [{title}]({url})")
|
| 42 |
+
|
| 43 |
+
return all_content, sources
|