UnivAI001 commited on
Commit
6798e8f
·
0 Parent(s):

initial commit - AI automation agents

Browse files
.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