A minimal, free‑tier clone of the AI‑powered job search tool
Welcome to another official UVF IT guide! In this quick‑fire tutorial you’ll build a tiny command‑line app that scrapes remote‑job listings, uses a local LLM (via Ollama) to match your profile, and prints tailored cover‑letter snippets – all running on your machine.
We’ll mimic the core flow of Mads Lorentzen’s AI‑Job‑Search project without any paid APIs, so you can get a working prototype in about 45 minutes.
git installed; the guide assumes a Unix‑like shell.llama2). Results will be less polished but functional.Create a fresh folder for the project and initialise a Python virtual environment so our dependencies stay isolated.
Install the tiny libraries we need – requests for HTTP calls and sqlite3 which is already bundled with Python – then activate the environment.
mkdir ai_job_search && cd ai_job_search
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install requests
# Verify installation
python -c "import requests, sqlite3; print('Setup OK')"Pull the first page of remote software‑engineer jobs from the public GitHub Jobs API and cache them locally in a SQLite file.
The script creates a simple jobs table if it doesn’t exist, inserts each posting, and prints how many records were saved – giving you instant feedback that the clone works.
fetch_jobs.py
import json, sqlite3, requests
DB_PATH = 'jobs.db'
API_URL = 'https://jobs.github.com/positions.json?description=software+engineer&location=remote'
def init_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
type TEXT,
url TEXT,
created_at TEXT,
company TEXT,
location TEXT,
title TEXT,
description TEXT
)''')
conn.commit()
return conn
def fetch_jobs():
resp = requests.get(API_URL, headers={'Accept': 'application/json'})
resp.raise_for_status()
return resp.json()
def cache_jobs(jobs, conn):
cur = conn.cursor()
for job in jobs:
cur.execute('''INSERT OR REPLACE INTO jobs
(id,type,url,created_at,company,location,title,description)
VALUES (?,?,?,?,?,?,?,?)''',
(job.get('id'), job.get('type'), job.get('url'), job.get('created_at'),
job.get('company'), job.get('location'), job.get('title'), job.get('description')))
conn.commit()
print(f"Cached {len(jobs)} jobs into {DB_PATH}")
if __name__ == '__main__':
conn = init_db()
jobs = fetch_jobs()
cache_jobs(jobs, conn)
conn.close()In this step we call Ollama’s local llama2 model to compare each stored job description with a static candidate profile. The model returns a relevance score, letting us pick the most promising posting. We also ask the model to draft a short cover‑letter snippet for that top match, so the CLI can show a concrete example of AI‑generated content.
We keep the logic tiny: load jobs from SQLite, send a prompt that includes the profile and each job description, parse the numeric score from the response, and remember the best one. The snippet is generated with a second prompt that references the chosen job. This gives us a working “matching” layer without any external APIs.
match_jobs.py
import sqlite3, json, subprocess, textwrap
DB_PATH = 'jobs.db'
PROFILE = "Experienced Python developer with 3+ years in data pipelines, eager to apply AI to real‑world problems."
def fetch_jobs():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute('SELECT id, title, description FROM jobs')
rows = cur.fetchall()
conn.close()
return [{'id': r[0], 'title': r[1], 'description': r[2]} for r in rows]
def ollama_prompt(prompt):
# Call Ollama locally; assumes `ollama run llama2` is available
result = subprocess.run(['ollama', 'run', 'llama2'], input=prompt.encode(), capture_output=True, text=True)
return result.stdout.strip()
def score_job(job):
prompt = textwrap.dedent(f"""
You are a recruiter AI. Given the candidate profile below and a job description, return a relevance score from 0 to 100.
Profile: {PROFILE}
Job Title: {job['title']}
Job Description: {job['description']}
Score:""")
response = ollama_prompt(prompt)
try:
return float(response.split('\n')[0])
except ValueError:
return 0.0
def generate_snippet(job):
prompt = textwrap.dedent(f"""
Write a short (2‑3 sentence) cover‑letter paragraph for a candidate with the profile:
{PROFILE}
applying to the following position:
Title: {job['title']}
Description: {job['description']}
""")
return ollama_prompt(prompt)
def find_best_match():
jobs = fetch_jobs()
best = None
best_score = -1
for job in jobs:
sc = score_job(job)
if sc > best_score:
best_score = sc
best = job
snippet = generate_snippet(best) if best else ''
return best, best_score, snippet
if __name__ == '__main__':
job, score, snippet = find_best_match()
print(json.dumps({'job': job, 'score': score, 'snippet': snippet}, ensure_ascii=False, indent=2))Now we tie everything together: a small command‑line interface that first ensures the database exists, then runs the matching logic and prints a friendly report. Users will invoke the script with python cli.py (or make it executable) and see the top‑scoring job plus the AI‑generated cover‑letter snippet.
The CLI also offers a --list flag to dump all fetched jobs, demonstrating how additional commands can be layered on later without changing the core matching module. This keeps the entry point simple while still being extensible.
cli.py
import argparse, json, os, subprocess, sys
from match_jobs import find_best_match, fetch_jobs
def ensure_db():
if not os.path.exists('jobs.db'):
print('Database not found. Initialising...')
# Simple schema creation; in a real project you would have a separate init script
import sqlite3
conn = sqlite3.connect('jobs.db')
cur = conn.cursor()
cur.execute('''CREATE TABLE jobs (id INTEGER PRIMARY KEY, title TEXT, description TEXT)''')
conn.commit()
conn.close()
print('Empty DB created. Populate it with jobs before running the match.')
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description='Local AI Job‑Search CLI')
parser.add_argument('--list', action='store_true', help='List all stored jobs')
args = parser.parse_args()
ensure_db()
if args.list:
jobs = fetch_jobs()
print(json.dumps(jobs, ensure_ascii=False, indent=2))
return
job, score, snippet = find_best_match()
if not job:
print('No jobs found in the database.')
return
print('\n=== Top Match ===')
print(f"Title : {job['title']}")
print(f"Score : {score:.2f}")
print('\nCover Letter Snippet:')
print(snippet)
if __name__ == '__main__':
main()Run the CLI and watch the output. If you see a job title, a numeric score, and a short paragraph, the clone is working. From here you can start adding features: a --filter keyword flag to only consider jobs containing certain terms, or persisting the generated snippet back into SQLite for later reference.
All modifications should reuse the existing functions – keep the matching logic isolated in match_jobs.py and let cli.py handle user interaction. This separation makes future extensions (e.g., swapping Ollama for OpenAI) straightforward.
chmod +x cli.py
# Ensure Ollama is running and the llama2 model is pulled
ollama pull llama2
# Populate the DB with a couple of sample jobs (run once)
python - <<'PY'
import sqlite3, textwrap
conn = sqlite3.connect('jobs.db')
cur = conn.cursor()
cur.execute('INSERT INTO jobs (title, description) VALUES (?,?)', (
'AI Engineer',
textwrap.dedent('''
Build and maintain AI pipelines, work with LLMs, and deploy models to production.
''')
))
cur.execute('INSERT INTO jobs (title, description) VALUES (?,?)', (
'Data Analyst',
'Analyze datasets, create dashboards, and support data‑driven decisions.'
))
conn.commit()
conn.close()
PY
# Run the CLI
./cli.py| Check | Command / Why it matters |
|---|---|
| Jobs are fetched and stored | sqlite3 jobs.db "SELECT COUNT(*) FROM jobs;" # should return a positive integer |
| Top match printed with a score | Observe the console output after running ./cli.py – it shows 'Top match' and a numeric score |
| Cover‑letter snippet appears | The CLI output includes a paragraph under 'Cover Letter Snippet' |
You now have a functional local AI job‑search CLI. Experiment by adding keyword filters, persisting cover letters, or swapping Ollama for another open‑source LLM. Happy hunting!