Production-grade, fully local, open-source resume screening pipeline powered by GraphRAG, GLiNER, and Neo4j.
No cloud APIs. No data leaves your machine. Every decision is auditable.
RES-AI is a 5-phase AI pipeline that turns raw resume PDFs into a queryable knowledge graph in Neo4j, then ranks candidates against a job description using graph traversal and an optional local LLM (Ollama).
It was built specifically to solve three problems with traditional resume screening:
| Problem | How RES-AI Solves It |
|---|---|
| Bias from names, gender, location | Microsoft Presidio scrubs all PII before any AI sees the data |
| Keyword-matching misses related skills | GraphRAG traverses a skill taxonomy (FastAPI β Python β Backend) |
| Black-box AI decisions | Every score is traceable to graph edges with confidence scores |
PDF Resume
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PHASE 2 β Secure Ingestion (main.py) β
β β
β Docling βββΊ raw Markdown β
β β β
β GLiNER βββΊ zero-shot NER βββΊ structured entities β
β β (Python, FastAPI, Blue Team β¦) β
β β β
β Presidio βββΊ PII redacted Markdown β
β β (names, phones, emails, locations gone) β
β β β
β Neo4j βββΊ Candidate node + typed relationship edges β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PHASE 3 β GraphRAG & Knowledge Graph (taxonomy.py) β
β β
β ESCO Skill Taxonomy seeded into Neo4j β
β (FastAPI β IS_FRAMEWORK_OF β Python) β
β β
β GraphRAGLinker connects candidate entities β
β to taxonomy nodes via SIMILAR_TO edges β
β β
β Multi-hop queries: "find Python devs" matches β
β FastAPI, Django, PyTorch, Flask candidates too β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PHASE 4 β JD Evaluation (evaluator.py) β
β β
β GLiNER parses job description β required skill list β
β Neo4j graph ranks candidates by skill coverage β
β Optional: Ollama LLM writes narrative summaries β
β (DeepSeek-R1, Llama-3, or any local model) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PHASE 5 β Enterprise API (api.py) β
β β
β FastAPI REST server with Swagger UI β
β Audit logging to audit.log (every request) β
β Prompt injection guard on all JD inputs β
β File type + size validation on uploads β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Layer | Technology | Why |
|---|---|---|
| Document parsing | Docling (IBM) | Preserves tables, columns, headers in PDFs |
| Entity extraction | GLiNER | Zero-shot NER β no fine-tuning needed |
| PII redaction | Microsoft Presidio | Production-grade, used at Microsoft scale |
| Knowledge graph | Neo4j | Graph DB with Cypher query language |
| Skill taxonomy | ESCO-inspired (built-in) | 100+ skill relationships across 10 domains |
| LLM (optional) | Ollama | Fully local β DeepSeek-R1, Llama-3, etc. |
| REST API | FastAPI | Async, auto Swagger docs, production-ready |
| CLI | argparse (built-in) | No extra deps β python run.py ingest ... |
crt/
βββ schema.py # Shared ENTITY_SCHEMA β single source of truth
βββ main.py # Phase 2: ingestion orchestrator
βββ taxonomy.py # Phase 3: ESCO skill taxonomy + GraphRAG linker
βββ query_engine.py # Phase 3: graph traversal query engine
βββ evaluator.py # Phase 4: JD parser + candidate scorer
βββ api.py # Phase 5: FastAPI REST server + audit logging
βββ run.py # CLI entry point β all phases via subcommands
βββ requirements.txt # All Python dependencies
βββ .env.example # Environment variable template
Before installing, make sure you have:
- Python 3.10+
- Neo4j 5.x running locally (Docker is the easiest way)
- Ollama (optional β only needed for LLM narrative summaries)
docker run -d \
--name neo4j \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/supersecretpassword \
neo4j:5Neo4j Browser will be at http://localhost:7474
# 1. Navigate to the project folder
cd "C:\Users\ssanj\OneDrive\Documents\HUB_2.0\RES-AI"
# 2. Activate your virtual environment
.\venv\Scripts\Activate
# 3. Install all dependencies
pip install -r crt\requirements.txt
# 4. Download the SpaCy English model (required by Presidio)
python -m spacy download en_core_web_lg
# 5. Set up environment variables
copy crt\.env.example crt\.env
# Open crt\.env and set your Neo4j password if different from defaultAll commands are run from inside the crt/ folder:
cd crtpython run.py ingest --resume "..\sample_resume\Resume.pdf"What happens:
- PDF β layout-aware Markdown (Docling)
- 12 entity categories extracted: Programming Language, Framework, Database, Cloud Platform, Security Tool, Soft Skill, Job Title, Degree, University, Certification, Years of Experience, Industry
- All PII (name, phone, email, location) redacted
- Candidate node + typed relationship edges written to Neo4j
- Skill taxonomy seeded and candidate linked to taxonomy graph
- Two output files saved:
<ID>_safe.mdand<ID>_entities.json
Options:
| Flag | Description |
|---|---|
--resume FILE |
Path to a single PDF or DOCX |
--folder DIR |
Process all PDFs in a folder (batch mode) |
--force |
Re-ingest even if the same file was already processed |
--no-taxonomy |
Skip Phase 3 taxonomy linking |
--json |
Print result as JSON |
python run.py ingest --folder "..\resumes_folder\"python run.py evaluate --jd "Looking for a Python backend developer with FastAPI, PostgreSQL, Docker, and AWS experience."# From a text file
python run.py evaluate --jd-file job_description.txt --top-n 5# With Ollama LLM narrative summaries (requires Ollama running)
python run.py evaluate --jd "..." --llmSample output:
Rank 1 | CANDIDATE_A1B2C3D4E5F6 | score=8 | coverage=80%
matched: Python, Fastapi, Postgresql, Docker
Rank 2 | CANDIDATE_G7H8I9J0K1L2 | score=5 | coverage=50%
matched: Python, Aws
# Finds candidates who know Python OR anything in the Python ecosystem
python run.py search --skill Python
# Finds all cybersecurity candidates via taxonomy traversal
python run.py search --skill "Blue Team"
# Exact match only (no ecosystem traversal)
python run.py search --skill Python --no-ecosystempython run.py stats========================================
RES-AI Knowledge Graph Stats
Candidates : 12
Skill nodes : 347
Relationships : 891
Taxonomy nodes : 108
========================================
python run.py seed-taxonomySafe to run multiple times β all writes use MERGE (idempotent).
python run.py serve
# Dev mode with hot-reload:
python run.py serve --reloadOpen http://localhost:8000/docs for the full interactive Swagger UI.
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Liveness probe |
GET |
/stats |
Knowledge graph statistics |
POST |
/ingest |
Upload a resume (multipart/form-data) |
GET |
/candidates |
List all candidates |
GET |
/candidates/{id} |
Full candidate profile |
GET |
/query/skill/{name} |
Find candidates by skill (GraphRAG) |
POST |
/query/skills |
Find candidates matching ALL listed skills |
GET |
/query/security |
Find Blue Team / SOC candidates |
POST |
/evaluate |
Rank candidates against a JD |
Open http://localhost:7474 and run these in the Cypher browser:
-- See the full graph
MATCH (c:Candidate)-[r]->(n) RETURN c, r, n
-- See one candidate's complete profile
MATCH (c:Candidate {id: 'CANDIDATE_XXXX'})-[r]->(n) RETURN c, r, n
-- Find all Python developers (ecosystem traversal)
MATCH (root:ProgrammingLanguage {name: 'Python'})
MATCH (related)-[:IS_FRAMEWORK_OF|IS_LIBRARY_OF*1..2]->(root)
WITH collect(related.name) + ['Python'] AS ecosystem
MATCH (c:Candidate)-[r]->(n) WHERE n.name IN ecosystem
RETURN DISTINCT c.id, collect(n.name) AS matched_skills
-- Rank candidates by skill count
MATCH (c:Candidate)-[r]->(n)
RETURN c.id, count(n) AS skill_count, collect(n.name)[..5] AS sample_skills
ORDER BY skill_count DESC
-- See the skill taxonomy
MATCH (child)-[r]->(parent)
WHERE child.taxonomy = true
RETURN child.name, type(r), parent.name
LIMIT 50
-- Check schema constraints
SHOW CONSTRAINTSCopy .env.example to .env and edit:
| Variable | Default | Description |
|---|---|---|
NEO4J_URI |
bolt://localhost:7687 |
Neo4j bolt connection |
NEO4J_USER |
neo4j |
Neo4j username |
NEO4J_PASSWORD |
supersecretpassword |
Neo4j password |
TARGET_RESUME |
(path) | Default resume for python main.py |
GLINER_MODEL |
urchade/gliner_medium-v2.1 |
GLiNER model name |
NER_THRESHOLD |
0.40 |
Confidence threshold for entity extraction |
MAX_UPLOAD_MB |
20 |
API upload size limit |
OLLAMA_URL |
http://localhost:11434 |
Ollama server URL |
OLLAMA_MODEL |
deepseek-r1:8b |
Local LLM model for summaries |
- PII Redaction β names, phones, emails, URLs, locations removed before graph storage
- Dynamic Allow-list β technical terms (e.g. "Python", "Firebase") are protected from being redacted by Presidio
- SHA-256 Deduplication β same resume won't be processed twice unless
--forceis used - Prompt Injection Guard β API rejects JD text containing patterns like
"ignore previous instructions" - Audit Logging β every API request logged to
audit.logwith timestamp, endpoint, client IP, status code, and response time - File Validation β only
.pdfand.docxaccepted, with configurable size limit
| Phase | Status |
|---|---|
| Phase 1 β Infrastructure (Docker, Neo4j, Ollama) | β Manual setup (see Prerequisites) |
| Phase 2 β Secure Ingestion (Docling + GLiNER + Presidio + Neo4j) | β Complete |
| Phase 3 β GraphRAG & Skill Taxonomy | β Complete |
| Phase 4 β JD Evaluation (GLiNER + graph scoring + Ollama) | β Complete |
| Phase 5 β FastAPI + Audit Logging | β Complete |
| Next β Next.js Dashboard (visual skill graph) | π Planned |
| Next β ELK Stack / Splunk SIEM integration | π Planned |
| Next β DSPy BootstrapFewShot optimizer with golden examples | π Planned |
Private / Internal Use. All data processed locally β no external API calls.