A minimal clone that scrapes jobs and uses a local LLM to draft cover letters
Welcome to another official UVF IT guide! In this quick build you’ll create a tiny command‑line job‑search assistant inspired by the AI‑Job‑Search project. We’ll scrape a public job board, store results in SQLite, and call a free‑tier local LLM (e.g., Ollama) to generate a one‑sentence cover‑letter snippet.
The whole thing fits into a single Python file and runs on your laptop in about 45 minutes. No paid APIs, no cloud accounts – just Python, SQLite, and a free LLM container.
git installed; the guide assumes a Unix‑like shell (Linux/macOS or WSL).Create a fresh directory for the clone and initialise a Python virtual environment so dependencies stay isolated. Then install the tiny set of libraries we need: requests for HTTP calls, rich for pretty CLI output, and ollama client to talk to the local LLM. All of this runs locally – no cloud credentials required.
mkdir -p ai_job_cli && cd ai_job_cli
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install requests rich ollamaWe’ll store jobs in a tiny SQLite file called jobs.db. The schema has three columns – an auto‑increment id, a title, and a URL – enough to identify a posting later. Next we write a function that fetches the public RemoteOK JSON feed, extracts each job’s title and URL, and inserts any new entries into the database. This keeps the CLI fast and offline‑friendly.
job_cli.py
import sqlite3, json, requests
from pathlib import Path
DB_PATH = Path('jobs.db')
def init_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL UNIQUE
)''')
conn.commit()
conn.close()
def fetch_remoteok():
resp = requests.get('https://remoteok.com/api')
resp.raise_for_status()
return resp.json()
def store_jobs(jobs):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
for job in jobs:
title = job.get('position') or job.get('title')
url = job.get('url')
if title and url:
try:
cur.execute('INSERT INTO jobs (title, url) VALUES (?,?)', (title, url))
except sqlite3.IntegrityError:
pass # duplicate, ignore
conn.commit()
conn.close()
if __name__ == '__main__':
init_db()
data = fetch_remoteok()
store_jobs(data)
print('✅ Jobs scraped and saved to', DB_PATH)In this step we connect the SQLite job list to a tiny command‑line menu and ask a local Ollama model for a one‑sentence cover‑letter snippet. The menu lets the user pick a job by its title, then we send the job description to the model via its REST endpoint. The response is printed so you can copy‑paste it into an application or store it later.
We keep everything in a single file, job_cli.py, so the script can be run directly after the scraper phase. The code uses the standard library (sqlite3, json, urllib.request) and a tiny helper function to talk to Ollama, making it easy to swap the model or the API URL later.
job_cli.py
import sqlite3, json, urllib.request, sys
DB_PATH = 'jobs.db'
OLLAMA_URL = 'http://localhost:11434/api/generate'
MODEL = 'llama2' # change to any model you have locally
def fetch_jobs():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute('SELECT id, title, company FROM jobs')
rows = cur.fetchall()
conn.close()
return rows
def choose_job(jobs):
print('Available jobs:')
for i, (jid, title, company) in enumerate(jobs, 1):
print(f"{i}. {title} @ {company}")
choice = input('Select a job number: ')
try:
idx = int(choice) - 1
return jobs[idx]
except Exception:
sys.exit('Invalid selection')
def generate_cover(job):
prompt = f"Write a short, enthusiastic cover‑letter line for the position '{job[1]}' at {job[2]}."
payload = json.dumps({"model": MODEL, "prompt": prompt, "stream": False}).encode('utf-8')
req = urllib.request.Request(OLLAMA_URL, data=payload, headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req) as resp:
data = json.load(resp)
return data.get('response', '').strip()
if __name__ == '__main__':
jobs = fetch_jobs()
if not jobs:
sys.exit('No jobs found in the database.')
job = choose_job(jobs)
line = generate_cover(job)
print('\nGenerated cover line:')
print(line)Make sure the Ollama service is up and the model you referenced is available, then simply execute the CLI script. The script will list the scraped jobs, let you pick one, and show the AI‑generated sentence.
If you see the list and a short sentence, the integration is working correctly.
# Start Ollama (if not already running)
ollama serve &
# Give it a moment to start
sleep 3
# Run the CLI
python job_cli.pyAfter a successful run, you can inspect the SQLite file to confirm the jobs were stored and experiment with different LLM models by changing the MODEL constant. Adding a --save flag is a simple way to persist the generated line back into the database for later reference.
These small extensions turn the prototype into a more useful personal tool without adding much complexity.
# Verify jobs were saved
sqlite3 jobs.db "SELECT id, title, company FROM jobs LIMIT 5;"
# Try another model (e.g., phi)
sed -i '' 's/MODEL = .*/MODEL = "phi"/' job_cli.py
python job_cli.py
# Optional: add a flag to store the line (demo only, not implemented yet)
# python job_cli.py --save| Check | Command / Why it matters |
|---|---|
| SQLite DB contains scraped jobs | sqlite3 jobs.db "SELECT COUNT(*) FROM jobs;" should output a positive integer |
| Ollama container is running | curl -s http://localhost:11434/api/version returns JSON with version info |
| CLI prints a cover line | Running python job_cli.py and selecting a job displays a short sentence |
You now have a working AI‑powered job‑search CLI. Feel free to swap the RemoteOK endpoint for other boards, enrich the schema, or hook up a better LLM. Happy hunting!