A fine-tuned version of
numind/NuExtract-tiny-v1.5
(Qwen2.5-0.5B backbone) specialised for
resume / CV structured extraction.
Given raw resume text in any format, the model returns a clean JSON object with name,
contact details, skills, work experience, education, and other details — ready to plug
into a hiring pipeline, ATS, or LangChain workflow.
Near-zero train/val gap throughout — no overfitting observed.
Best checkpoint (step 284, val loss 0.2296) loaded automatically.
1{
2 "name": "string or null",
3 "email": "string or null",
4 "phone": "string or null",
5 "website": "string or null",
6 "skills": ["string"],
7 "experience": [{"title": "string", "company": "string", "duration": "string"}],
8 "education": [{"degree": "string", "institution": "string", "year": "string"}],
9 "other_details": ["string"]
10}
1FROM hf.co/nimendraai/NuExtract-tiny-Resume-Data-Extractor:Q4_K_M
2
3PARAMETER temperature 0
4PARAMETER top_k 10
5PARAMETER top_p 0.9
6PARAMETER repeat_penalty 1.1
7PARAMETER seed 42
8PARAMETER num_ctx 2048
9PARAMETER num_predict 600
10PARAMETER stop "<|end-output|>"
11PARAMETER stop "<|endoftext|>"
12
13TEMPLATE """<|input|>
14### Template:
15{
16 "name": "",
17 "email": "",
18 "phone": "",
19 "website": "",
20 "skills": [""],
21 "experience": [{"title": "", "company": "", "duration": ""}],
22 "education": [{"degree": "", "institution": "", "year": ""}],
23 "other_details": [""]
24}
25### Text:
26{{ .Prompt }}
27
28<|output|>
29"""
30
31LICENSE """Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0"""
1curl http://localhost:11434/api/generate \
2 -X POST \
3 -H "Content-Type: application/json" \
4 -d '{
5 "model": "agenthire-extractor",
6 "format": "json",
7 "stream": false,
8 "prompt": "<resume text here>"
9 }'
1import json
2import torch
3from transformers import AutoModelForCausalLM, AutoTokenizer
4
5model_name = "nimendraai/NuExtract-tiny-Resume-Data-Extractor"
6model = AutoModelForCausalLM.from_pretrained(
7 model_name, torch_dtype=torch.bfloat16, trust_remote_code=True
8).eval().cuda()
9tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
10
11TEMPLATE = json.dumps({
12 "name": "", "email": "", "phone": "", "website": "",
13 "skills": [""],
14 "experience": [{"title": "", "company": "", "duration": ""}],
15 "education": [{"degree": "", "institution": "", "year": ""}],
16 "other_details": [""],
17}, indent=4)
18
19def extract_first_json(text):
20 depth, start = 0, None
21 for i, ch in enumerate(text):
22 if ch == "{":
23 if start is None: start = i
24 depth += 1
25 elif ch == "}":
26 depth -= 1
27 if depth == 0 and start is not None:
28 return text[start:i+1]
29 return text
30
31def extract(resume_text: str) -> dict:
32 prompt = (
33 "<|input|>\n"
34 f"### Template:\n{TEMPLATE}\n"
35 f"### Text:\n{resume_text}\n\n"
36 "<|output|>"
37 )
38 inputs = tokenizer(
39 prompt, return_tensors="pt", truncation=True, max_length=2048
40 ).to(model.device)
41 with torch.no_grad():
42 out = model.generate(
43 **inputs, max_new_tokens=512, do_sample=False
44 )
45 decoded = tokenizer.decode(out[0], skip_special_tokens=True)
46 raw = decoded.split("<|output|>")[-1].strip()
47 return json.loads(extract_first_json(raw))
1from langchain_ollama import OllamaLLM
2from pydantic import BaseModel, Field
3from typing import Optional
4import json
5
6class Experience(BaseModel):
7 title: str = Field(default="")
8 company: str = Field(default="")
9 duration: str = Field(default="")
10
11class Education(BaseModel):
12 degree: str = Field(default="")
13 institution: str = Field(default="")
14 year: str = Field(default="")
15
16class ResumeExtraction(BaseModel):
17 name: Optional[str] = None
18 email: Optional[str] = None
19 phone: Optional[str] = None
20 website: Optional[str] = None
21 skills: list[str] = Field(default_factory=list)
22 experience: list[Experience] = Field(default_factory=list)
23 education: list[Education] = Field(default_factory=list)
24 other_details: list[str] = Field(default_factory=list)
25
26def extract_first_json(text):
27 depth, start = 0, None
28 for i, ch in enumerate(text):
29 if ch == "{":
30 if start is None: start = i
31 depth += 1
32 elif ch == "}":
33 depth -= 1
34 if depth == 0 and start is not None:
35 return text[start:i+1]
36 return text
37
38llm = OllamaLLM(model="agenthire-extractor", format="json", temperature=0)
39
40def extract_resume(text: str) -> ResumeExtraction:
41 raw = llm.invoke(text)
42 return ResumeExtraction(**json.loads(extract_first_json(raw)))
43
44# Batch processing
45resumes = [resume_1, resume_2, resume_3]
46results = [
47 ResumeExtraction(**json.loads(extract_first_json(r)))
48 for r in llm.batch(resumes)
49]
50
51# Pipeline with scoring
52from langchain_core.prompts import PromptTemplate
53from langchain_ollama import OllamaLLM as ScoreLLM
54
55scoring_prompt = PromptTemplate.from_template(
56 "Job: {job_description}\n\nCandidate: {candidate}\n\n"
57 "Score 1-10 and explain."
58)
59scorer = ScoreLLM(model="llama3", temperature=0.3)
60
61def process_application(resume_text, job_description):
62 candidate = extract_resume(resume_text).model_dump()
63 evaluation = (scoring_prompt | scorer).invoke({
64 "job_description": job_description,
65 "candidate": json.dumps(candidate, indent=2),
66 })
67 return {"candidate": candidate, "evaluation": evaluation}
1def extract_first_json(text):
2 depth, start = 0, None
3 for i, ch in enumerate(text):
4 if ch == "{":
5 if start is None: start = i
6 depth += 1
7 elif ch == "}":
8 depth -= 1
9 if depth == 0 and start is not None:
10 return text[start:i+1]
11 return text
12
13result = json.loads(extract_first_json(raw_output))
1@misc{nuextract2024,
2 author = {NuMind},
3 title = {NuExtract: A Foundation Model for Structured Extraction},
4 year = {2024},
5 url = {https://numind.ai/blog/nuextract-a-foundation-model-for-structured-extraction}
6}
MIT — same as the base model
numind/NuExtract-tiny-v1.5.
This was trained 2x faster with
Unsloth