In this deep dive, we build a production-quality, privacy-focused resume analysis tool that runs 100% on your local machine using Streamlit, Ollama, and Google's Gemma 3 models.
๐ The Privacy Integrity Problem
Resume data is sensitive. PII (Personally Identifiable Information), career history, and contact details are not things you want to casually send to third-party APIs. Yet, most "AI Resume Analyzers" require uploading PDFs to cloud endpoints or pasting text into web interfaces that store data.
We can do better. With the rise of capable local LLMs like Gemma 3, we can build a powerful extraction engine that runs entirely on localhost.
๐ ๏ธ The Local Stack
To build this, we need three core components:
- The Brain: Ollama running Google Gemma 3 (4b or 12b).
- The Engineer: LangExtract for structured data extraction.
- The Interface: Streamlit for a reactive, Python-native UI.
Architecture
๐ง Structured Extraction with LangExtract
The hardest part of analyzing resumes is that they are unstructured. LLMs are good at chatting, but we need JSON. LangExtract solves this by enforcing a schema through example-driven extraction.
Instead of just asking "Extract skills", we define a Pydantic schema and provide few-shot examples.
1. Defining the Schema (schema.py)
We define classes for every entity we want to extract.
from pydantic import BaseModel
class ExperienceItem(BaseModel):
company: str
role: str
duration: str
skills_used: list[str]
class ResumeSchema(BaseModel):
candidate_name: str
emails: list[str]
skills: list[str]
experience: list[ExperienceItem]
# ... education, projects, etc.
2. The Extraction Engine (extractor.py)
We wrap LangExtract to handle large resumes (which can exceed context windows) by chunking the text.
import langextract as lx
def extract_resume(text: str, model_name: str) -> dict:
# 1. Chunk text if too long
chunks = chunk_text(text)
all_extractions = []
for chunk in chunks:
# 2. Call LangExtract with schema and examples
result = lx.extract(
text_or_documents=chunk,
prompt_description=get_resume_prompt(),
examples=get_resume_examples(),
model_id=model_name,
model_url="http://localhost:11434"
)
all_extractions.extend(result.extractions)
# 3. Consolidate and deduplicate results
return extractions_to_schema(all_extractions)
๐ Visualizing Evidence (No Hallucinations)
A key feature of our agent is Evidence Highlighting. We don't just trust the LLM; we verify it. LangExtract returns char_interval (start/end positions) for every extraction.
We use this to render the original text with color-coded highlights, so you can see exactly where a skill or experience claim came from.
# span_highlighter.py
def highlight_spans(text: str, extractions: list) -> str:
# ... logic to sort spans and handle overlaps ...
for start, end, label, content in spans:
color = ENTITY_COLORS[label]
# Inject HTML span with colored background and tooltips
html_parts.append(
f'<span style="background:{color}22; border-bottom:2px solid {color};">'
f'{content}</span>'
)
return "".join(html_parts)
๐จ The UI: Smart & Theme-Aware
We built a Streamlit dashboard that adapts to your system theme (Light/Dark mode) using CSS variables. It features:
- Model Selection: Switch between
gemma3:4b(fast) andgemma3:4b(detailed). - Skill Intelligence: Clusters extracted skills into domains (Frontend, Backend, DevOps, AI) using keyword analysis.
- Job Match: Pasting a Job Description triggers a fuzzy-match algorithm (
rapidfuzz) to calculate coverage percentage and identify missing skills.
๐ Running It Locally
-
Clone the repo:
git clone https://github.com/harishkotra/resume-analyzer-agent cd resume-analyzer-agent -
Install dependencies:
pip install -r requirements.txt -
Start Ollama:
ollama serve ollama pull gemma3:4b -
Launch the App:
streamlit run app.py
By combining the structural guarantees of LangExtract with the reasoning power of Gemma 3, we've built a tool that rivals commercial SaaS offeringsโwithout a single byte of data leaving your machine.
Checkout the Github repo here: https://github.com/harishkotra/resume-analyzer-agent

Top comments (0)