Clone the workflow: scrape, evaluate, and tailor applications locally.
Welcome to another official UVF IT build guide. Today we are constructing a minimal, local-first AI job search agent inspired by the open-source workflow of MadsLorentzen/ai-job-search.
You will build a Python application that scrapes a sample job listing, evaluates it against a user profile using a local LLM interface, and stores the results in a SQLite database. This guide focuses on the core logic: data ingestion, AI evaluation, and persistence, without requiring paid API keys for the initial build.
pip. We use sqlite3 (built-in) and requests for scraping; no external database setup needed.Create the project directory and install dependencies. We need requests for fetching and beautifulsoup4 for parsing HTML.
Create a mock job source. Since we cannot scrape live sites reliably in a beginner guide, we create a local HTML file that mimics a job posting.
mkdir ai-job-search-agent
cd ai-job-search-agent
pip install requests beautifulsoup4mock_job.html
<!DOCTYPE html>
<html>
<head>
<title>Senior Python Developer</title>
</head>
<body>
<div class="job-posting">
<h1 class="job-title">Senior Python Developer</h1>
<div class="job-description">
<p>We are looking for an experienced Python developer to join our team.</p>
<h2>Requirements:</h2>
<ul class="requirements">
<li>5+ years of Python experience</li>
<li>Experience with Django or Flask</li>
<li>Knowledge of SQL and databases</li>
<li>Strong communication skills</li>
</ul>
</div>
</div>
</body>
</html>Set up a SQLite database to store job listings and their evaluation scores.
Create a Python script to initialize the database schema. This ensures we have a persistent store for our 'agent' to work with.
db_setup.py
import sqlite3
DB_NAME = "jobs.db"
def init_db():
"""Initialize the SQLite database and create the jobs table if it doesn't exist."""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
requirements TEXT,
match_score REAL DEFAULT 0.0,
evaluated INTEGER DEFAULT 0
)
''')
conn.commit()
conn.close()
print(f"Database '{DB_NAME}' initialized successfully.")
if __name__ == "__main__":
init_db()Build the scraper module. It will read the local HTML file (simulating a fetch) and extract the title, description, and requirements.
Insert the parsed data into the SQLite database. This represents the 'ingestion' phase of the job search agent.
scraper.py
import sqlite3
from bs4 import BeautifulSoup
def scrape_mock_job(html_file_path="mock_job.html"):
"""Reads a local HTML file and extracts job details."""
with open(html_file_path, 'r', encoding='utf-8') as file:
content = file.read()
soup = BeautifulSoup(content, 'html.parser')
# Extract title
title_tag = soup.find('h1', class_='job-title')
title = title_tag.get_text(strip=True) if title_tag else "Unknown Title"
# Extract description
desc_tag = soup.find('div', class_='job-description')
description = desc_tag.get_text(separator=' ', strip=True) if desc_tag else ""
# Extract requirements as a single string
req_list = soup.find_all('li', class_='requirements')
requirements = "; ".join([li.get_text(strip=True) for li in req_list])
return {
"title": title,
"description": description,
"requirements": requirements
}
def store_job(job_data):
"""Inserts the parsed job data into the SQLite database."""
conn = sqlite3.connect("jobs.db")
cursor = conn.cursor()
cursor.execute('''
INSERT INTO jobs (title, description, requirements, match_score, evaluated)
VALUES (?, ?, ?, 0.0, 0)
''', (job_data['title'], job_data['description'], job_data['requirements']))
conn.commit()
job_id = cursor.lastrowid
conn.close()
print(f"Stored job: {job_data['title']} (ID: {job_id})")
return job_id
def run_scraper():
"""Orchestrates scraping and storing."""
job_data = scrape_mock_job()
store_job(job_data)
if __name__ == "__main__":
run_scraper()Implement the 'AI' agent. For this minimal clone, we use a keyword-based heuristic to simulate AI evaluation. In a real scenario, you would replace this with a call to a local LLM like Ollama.
The evaluator checks if the job requirements match a predefined user profile (e.g., 'Python', 'Django'). It calculates a match score and updates the database.
evaluator.py
import sqlite3
def evaluate_jobs(db_path="jobs.db"):
"""
Simulates AI evaluation by matching job descriptions against a user profile.
In a production app, this would call a local LLM (e.g., Ollama) for nuanced reasoning.
"""
# Define a simple user profile for matching
user_skills = ["python", "django", "sql", "html", "css"]
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Fetch all jobs that haven't been evaluated yet (score is NULL)
cursor.execute("SELECT id, title, description FROM jobs WHERE match_score IS NULL")
jobs = cursor.fetchall()
for job_id, title, description in jobs:
# Simple heuristic: count matching keywords
desc_lower = description.lower()
title_lower = title.lower()
matches = 0
for skill in user_skills:
if skill in desc_lower or skill in title_lower:
matches += 1
# Calculate a simple score (0-100 scale)
score = min(100, int((matches / len(user_skills)) * 100))
# Update the database with the score
cursor.execute(
"UPDATE jobs SET match_score = ? WHERE id = ?",
(score, job_id)
)
print(f"Evaluated '{title}': Match Score {score}%")
conn.commit()
conn.close()
print("Evaluation complete.")
if __name__ == "__main__":
evaluate_jobs()Create a main runner script that orchestrates the entire workflow: init DB, scrape, evaluate.
Run the application and verify the output in the console and the SQLite database.
main.py
from db_setup import init_db
from scraper import scrape_mock_job
from evaluator import evaluate_jobs
def main():
print("--- Starting AI Job Search Agent ---")
# Step 1: Initialize Database
print("1. Initializing database...")
init_db()
# Step 2: Scrape Job Listings
print("2. Scraping job listings...")
scrape_mock_job()
# Step 3: Evaluate Jobs
print("3. Evaluating jobs against profile...")
evaluate_jobs()
print("--- Workflow Complete ---")
if __name__ == "__main__":
main()| Check | Command / Why it matters |
|---|---|
| Database file created | ls jobs.db |
| Job stored in DB | sqlite3 jobs.db 'SELECT * FROM jobs;' |
| Match score calculated | sqlite3 jobs.db 'SELECT title, match_score FROM jobs;' |
To extend this, replace the heuristic in evaluator.py with a call to a local LLM (e.g., using ollama CLI) to generate tailored cover letters.