Local-First AI Job Search Agent

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.

Before You Begin

Local Job Search Agent Architecture
LOCAL RUNTIME EXTERNAL DATA Fetches HTML Passes Job Data Stores Match Score Job Scraper Python Script AI Evaluator Local LLM/Heuristic SQLite DB jobs.db Job Source Mock HTML/API
1

Phase 1: Scaffold and Mock Data

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 beautifulsoup4

mock_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>
2

Phase 2: Core Model and Database

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()
3

Phase 3: Scraper and Parser

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()
4

Phase 4: AI Evaluation Logic

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()
5

Phase 5: Run and Verify

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()

Verifying the Stack

CheckCommand / Why it matters
Database file createdls jobs.db
Job stored in DBsqlite3 jobs.db 'SELECT * FROM jobs;'
Match score calculatedsqlite3 jobs.db 'SELECT title, match_score FROM jobs;'

Next Steps

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.