enhancements of resume parsing

This commit is contained in:
karuna 2026-07-29 15:39:01 +05:30
parent 957e832abd
commit b12c76d315
4 changed files with 477 additions and 1 deletions

View File

@ -269,6 +269,9 @@ RULES:
- Scan the entire document carefully before answering.
- Extract ONLY what exists in text.
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
- For list fields, always return an array. Return [] when no explicit entries exist.
- For list fields that describe objects, return one object per real document entry, not a sentence blob.
- Do not skip education, employer, project, certification, or family sections if they appear later in the document.
FIELD RULES:
- If "skills" exists, extract only explicit technical skills written in the document.
@ -283,6 +286,9 @@ FIELD RULES:
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
- If "phone" exists, return the most complete phone number found.
- If "experience" exists, return only clearly supported numeric values.
- If "education_history" exists, extract every education row/section with education_type, specialization, university, start_year, end_year, and marks_or_grade.
- If "employer_history" exists, extract every employer/project/work-experience entry with company_name, designation, date_of_joining, last_working_day, ctc, and work_description.
- If "family_details" exists, extract only explicitly written family members with relation_type, name, contact_no, dob, and location.
Schema:
{schema_text}
@ -301,7 +307,7 @@ Document:
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
"max_tokens": 1500,
"max_tokens": 4000,
}
try:
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)

View File

@ -1,3 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_recruitment_auto_doc_wizard,hr.recruitment.auto.doc.wizard,model_hr_recruitment_auto_doc_wizard,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_wizard_line,hr.recruitment.auto.doc.wizard.line,model_hr_recruitment_auto_doc_wizard_line,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_education_line,hr.recruitment.auto.doc.education.line,model_hr_recruitment_auto_doc_education_line,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_employer_line,hr.recruitment.auto.doc.employer.line,model_hr_recruitment_auto_doc_employer_line,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_family_line,hr.recruitment.auto.doc.family.line,model_hr_recruitment_auto_doc_family_line,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_certification_line,hr.recruitment.auto.doc.certification.line,model_hr_recruitment_auto_doc_certification_line,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_project_line,hr.recruitment.auto.doc.project.line,model_hr_recruitment_auto_doc_project_line,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_other_line,hr.recruitment.auto.doc.other.line,model_hr_recruitment_auto_doc_other_line,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_recruitment_auto_doc_wizard hr.recruitment.auto.doc.wizard model_hr_recruitment_auto_doc_wizard base.group_user 1 1 1 1
3 access_hr_recruitment_auto_doc_wizard_line hr.recruitment.auto.doc.wizard.line model_hr_recruitment_auto_doc_wizard_line base.group_user 1 1 1 1
4 access_hr_recruitment_auto_doc_education_line hr.recruitment.auto.doc.education.line model_hr_recruitment_auto_doc_education_line base.group_user 1 1 1 1
5 access_hr_recruitment_auto_doc_employer_line hr.recruitment.auto.doc.employer.line model_hr_recruitment_auto_doc_employer_line base.group_user 1 1 1 1
6 access_hr_recruitment_auto_doc_family_line hr.recruitment.auto.doc.family.line model_hr_recruitment_auto_doc_family_line base.group_user 1 1 1 1
7 access_hr_recruitment_auto_doc_certification_line hr.recruitment.auto.doc.certification.line model_hr_recruitment_auto_doc_certification_line base.group_user 1 1 1 1
8 access_hr_recruitment_auto_doc_project_line hr.recruitment.auto.doc.project.line model_hr_recruitment_auto_doc_project_line base.group_user 1 1 1 1
9 access_hr_recruitment_auto_doc_other_line hr.recruitment.auto.doc.other.line model_hr_recruitment_auto_doc_other_line base.group_user 1 1 1 1

View File

@ -484,6 +484,17 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"relation_type (father/mother/spouse/kid1/kid2), name, contact_no, dob, and location."
),
},
"certifications": {
"type": "list",
"description": "List of explicit certifications, courses, training, or licenses if present.",
},
"projects": {
"type": "list",
"description": (
"List of explicit resume projects if present. Each item should include project_name, role, "
"technologies, duration, and description when available."
),
},
}
def _get_jd_required_fields(self):
@ -514,6 +525,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"Normalize skills into clean individual names. "
"For experience values, return numeric years when clearly inferable. "
"If the resume contains education details, previous employer details, or family details, return them as structured arrays of objects using the requested field names. "
"Read the complete resume including tables, side columns, and later pages before returning JSON. "
"education_history must include every school, intermediate, diploma, graduation, post graduation, or additional qualification row that is explicitly present. "
"employer_history must include internships, current employer, previous employers, project/company experience, and work periods when explicitly present. "
"family_details must include every explicitly mentioned family member and must be [] if the resume has no family section. "
"For each employer entry, extract the role-specific work description into work_description. "
"Only include entries that are explicitly present in the document. "
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
@ -2145,6 +2160,40 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
degree = fields.Char()
skills_text = fields.Text(string="Skills")
summary = fields.Text()
education_history_json = fields.Text(string="Education History")
employer_history_json = fields.Text(string="Employer History")
family_details_json = fields.Text(string="Family Details")
other_payload_json = fields.Text(string="Other Parsed Data")
education_line_ids = fields.One2many(
"hr.recruitment.auto.doc.education.line",
"wizard_line_id",
string="Education History",
)
employer_line_ids = fields.One2many(
"hr.recruitment.auto.doc.employer.line",
"wizard_line_id",
string="Employer History",
)
family_line_ids = fields.One2many(
"hr.recruitment.auto.doc.family.line",
"wizard_line_id",
string="Family Details",
)
certification_line_ids = fields.One2many(
"hr.recruitment.auto.doc.certification.line",
"wizard_line_id",
string="Certifications",
)
project_line_ids = fields.One2many(
"hr.recruitment.auto.doc.project.line",
"wizard_line_id",
string="Projects",
)
other_line_ids = fields.One2many(
"hr.recruitment.auto.doc.other.line",
"wizard_line_id",
string="Other Parsed Data",
)
request_id = fields.Char(string="Request ID")
start_date = fields.Char()
@ -2197,6 +2246,16 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
"degree": parsed_data.get("degree"),
"skills_text": self._list_to_text(parsed_data.get("skills")),
"summary": parsed_data.get("summary"),
"education_history_json": self._json_to_text(parsed_data.get("education_history")),
"employer_history_json": self._json_to_text(parsed_data.get("employer_history")),
"family_details_json": self._json_to_text(parsed_data.get("family_details")),
"other_payload_json": self._json_to_text(self._get_other_parsed_data(parsed_data)),
"education_line_ids": self._prepare_education_line_commands(parsed_data.get("education_history")),
"employer_line_ids": self._prepare_employer_line_commands(parsed_data.get("employer_history"), parsed_data.get("projects")),
"family_line_ids": self._prepare_family_line_commands(parsed_data.get("family_details")),
"certification_line_ids": self._prepare_certification_line_commands(parsed_data.get("certifications")),
"project_line_ids": self._prepare_project_line_commands(parsed_data.get("projects")),
"other_line_ids": self._prepare_other_line_commands(self._get_other_parsed_data(parsed_data)),
"request_id": parsed_data.get("request_id"),
"start_date": parsed_data.get("start_date"),
"end_date": parsed_data.get("end_date"),
@ -2245,9 +2304,158 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
"degree": self.degree,
"skills": self._text_to_list(self.skills_text),
"summary": self.summary,
"education_history": self._get_education_history_from_lines(),
"employer_history": self._get_employer_history_from_lines(),
"family_details": self._get_family_details_from_lines(),
"certifications": self._get_certifications_from_lines(),
"projects": self._get_projects_from_lines(),
})
data.update(self._get_other_payload_from_lines())
return data
def _prepare_education_line_commands(self, education_history):
commands = [(5, 0, 0)]
for education in self._normalize_dict_list(education_history):
commands.append((0, 0, {
"education_type": education.get("education_type"),
"specialization": education.get("specialization") or education.get("name") or education.get("degree"),
"university": education.get("university") or education.get("institution") or education.get("college"),
"start_year": education.get("start_year"),
"end_year": education.get("end_year"),
"marks_or_grade": education.get("marks_or_grade") or education.get("marks") or education.get("grade") or education.get("cgpa") or education.get("percentage"),
}))
return commands
def _prepare_employer_line_commands(self, employer_history, projects=None):
commands = [(5, 0, 0)]
employers = self._normalize_shared_employer_descriptions(
self._normalize_dict_list(employer_history),
projects,
)
for employer in employers:
commands.append((0, 0, {
"company_name": employer.get("company_name") or employer.get("employer"),
"designation": employer.get("designation") or employer.get("role") or employer.get("job_title"),
"date_of_joining": employer.get("date_of_joining") or employer.get("start_date"),
"last_working_day": employer.get("last_working_day") or employer.get("end_date"),
"ctc": employer.get("ctc") or employer.get("salary"),
"work_description": employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities"),
}))
return commands
def _prepare_family_line_commands(self, family_details):
commands = [(5, 0, 0)]
for member in self._normalize_dict_list(family_details):
commands.append((0, 0, {
"relation_type": member.get("relation_type") or member.get("relation"),
"name": member.get("name"),
"contact_no": member.get("contact_no") or member.get("contact") or member.get("phone"),
"dob": member.get("dob"),
"location": member.get("location") or member.get("address"),
}))
return commands
def _prepare_certification_line_commands(self, certifications):
commands = [(5, 0, 0)]
for certification in self._normalize_value_list(certifications):
if isinstance(certification, dict):
commands.append((0, 0, {
"certification_name": certification.get("certification_name") or certification.get("name") or certification.get("title"),
"provider": certification.get("provider") or certification.get("issuer") or certification.get("organization"),
"completion_date": certification.get("completion_date") or certification.get("date") or certification.get("year"),
}))
else:
commands.append((0, 0, {"certification_name": str(certification)}))
return commands
def _prepare_project_line_commands(self, projects):
commands = [(5, 0, 0)]
for project in self._normalize_dict_list(projects):
technologies = project.get("technologies")
commands.append((0, 0, {
"project_name": project.get("project_name") or project.get("name") or project.get("title"),
"role": project.get("role"),
"technologies": ", ".join(technologies) if isinstance(technologies, list) else technologies,
"duration": project.get("duration"),
"description": project.get("description") or project.get("summary") or project.get("work_description"),
}))
return commands
def _prepare_other_line_commands(self, other_payload):
commands = [(5, 0, 0)]
for key, value in (other_payload or {}).items():
commands.append((0, 0, {
"key": key,
"value": self._json_to_text(value) or str(value),
}))
return commands
def _get_education_history_from_lines(self):
return [line._to_payload() for line in self.education_line_ids if line._to_payload()]
def _get_employer_history_from_lines(self):
return [line._to_payload() for line in self.employer_line_ids if line._to_payload()]
def _get_family_details_from_lines(self):
return [line._to_payload() for line in self.family_line_ids if line._to_payload()]
def _get_certifications_from_lines(self):
return [line._to_payload() for line in self.certification_line_ids if line._to_payload()]
def _get_projects_from_lines(self):
return [line._to_payload() for line in self.project_line_ids if line._to_payload()]
def _get_other_payload_from_lines(self):
return {
line.key: line._json_value()
for line in self.other_line_ids
if line.key and line.value not in (False, None, "")
}
def _normalize_dict_list(self, value):
if isinstance(value, list):
return [item for item in value if isinstance(item, dict)]
if isinstance(value, dict):
return [value]
return []
def _normalize_value_list(self, value):
if isinstance(value, list):
return [item for item in value if item not in (False, None, "", {})]
if value in (False, None, "", {}):
return []
return [value]
def _normalize_shared_employer_descriptions(self, employers, projects=None):
if len(employers) < 2:
return employers
descriptions = [
employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")
for employer in employers
]
descriptions = [description for description in descriptions if description]
if descriptions:
shared_description = max(descriptions, key=lambda item: len(str(item)))
else:
project_descriptions = []
for project in self._normalize_dict_list(projects):
description = project.get("description") or project.get("summary") or project.get("work_description")
if description:
project_descriptions.append("%s: %s" % (
project.get("project_name") or project.get("name") or project.get("title") or "Project",
description,
))
shared_description = "\n".join(project_descriptions) if project_descriptions else False
if not shared_description:
return employers
for employer in employers:
if not (employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")):
employer["work_description"] = shared_description
return employers
def _list_to_text(self, value):
if not value:
return False
@ -2260,6 +2468,47 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
return []
return [item.strip(" -,\t") for item in str(value).splitlines() if item.strip(" -,\t")]
def _json_to_text(self, value):
if value in (False, None, "", [], {}):
return False
return json.dumps(value, indent=2, ensure_ascii=False)
def _text_to_json_list(self, value):
parsed = self._text_to_json_value(value, [])
if isinstance(parsed, list):
return [item for item in parsed if item not in (False, None, "", {})]
if isinstance(parsed, dict):
return [parsed]
return []
def _text_to_json_dict(self, value):
parsed = self._text_to_json_value(value, {})
return parsed if isinstance(parsed, dict) else {}
def _text_to_json_value(self, value, default):
if not value:
return default
try:
return json.loads(value)
except Exception:
return default
def _get_other_parsed_data(self, parsed_data):
known_keys = {
"full_name", "first_name", "last_name", "email", "phone", "alternate_phone",
"linkedin_profile", "current_location", "current_organization",
"total_experience_years", "relevant_experience_years", "notice_period",
"degree", "skills", "summary", "education_history", "employer_history",
"family_details", "certifications", "projects", "request_id", "start_date", "end_date", "site_location",
"job_title", "job_summary", "requirements", "primary_skills",
"secondary_skills", "budget", "experience_years", "job_category",
}
return {
key: value
for key, value in (parsed_data or {}).items()
if key not in known_keys and value not in (False, None, "", [], {})
}
def _float_or_zero(self, value):
if value in (False, None, ""):
return 0.0
@ -2267,3 +2516,166 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
return float(value)
except Exception:
return 0.0
class HrRecruitmentAutoDocEducationLine(models.TransientModel):
_name = "hr.recruitment.auto.doc.education.line"
_description = "HR Recruitment Auto Document Education Line"
_order = "id"
wizard_line_id = fields.Many2one(
"hr.recruitment.auto.doc.wizard.line",
required=True,
ondelete="cascade",
)
education_type = fields.Char()
specialization = fields.Char()
university = fields.Char()
start_year = fields.Char()
end_year = fields.Char()
marks_or_grade = fields.Char()
def _to_payload(self):
self.ensure_one()
payload = {
"education_type": self.education_type,
"specialization": self.specialization,
"university": self.university,
"start_year": self.start_year,
"end_year": self.end_year,
"marks_or_grade": self.marks_or_grade,
}
return {key: value for key, value in payload.items() if value not in (False, None, "")}
class HrRecruitmentAutoDocEmployerLine(models.TransientModel):
_name = "hr.recruitment.auto.doc.employer.line"
_description = "HR Recruitment Auto Document Employer Line"
_order = "id"
wizard_line_id = fields.Many2one(
"hr.recruitment.auto.doc.wizard.line",
required=True,
ondelete="cascade",
)
company_name = fields.Char()
designation = fields.Char()
date_of_joining = fields.Char()
last_working_day = fields.Char()
ctc = fields.Char()
work_description = fields.Text()
def _to_payload(self):
self.ensure_one()
payload = {
"company_name": self.company_name,
"designation": self.designation,
"date_of_joining": self.date_of_joining,
"last_working_day": self.last_working_day,
"ctc": self.ctc,
"work_description": self.work_description,
}
return {key: value for key, value in payload.items() if value not in (False, None, "")}
class HrRecruitmentAutoDocFamilyLine(models.TransientModel):
_name = "hr.recruitment.auto.doc.family.line"
_description = "HR Recruitment Auto Document Family Line"
_order = "id"
wizard_line_id = fields.Many2one(
"hr.recruitment.auto.doc.wizard.line",
required=True,
ondelete="cascade",
)
relation_type = fields.Char()
name = fields.Char()
contact_no = fields.Char()
dob = fields.Char()
location = fields.Char()
def _to_payload(self):
self.ensure_one()
payload = {
"relation_type": self.relation_type,
"name": self.name,
"contact_no": self.contact_no,
"dob": self.dob,
"location": self.location,
}
return {key: value for key, value in payload.items() if value not in (False, None, "")}
class HrRecruitmentAutoDocCertificationLine(models.TransientModel):
_name = "hr.recruitment.auto.doc.certification.line"
_description = "HR Recruitment Auto Document Certification Line"
_order = "id"
wizard_line_id = fields.Many2one(
"hr.recruitment.auto.doc.wizard.line",
required=True,
ondelete="cascade",
)
certification_name = fields.Char()
provider = fields.Char()
completion_date = fields.Char()
def _to_payload(self):
self.ensure_one()
payload = {
"certification_name": self.certification_name,
"provider": self.provider,
"completion_date": self.completion_date,
}
return {key: value for key, value in payload.items() if value not in (False, None, "")}
class HrRecruitmentAutoDocProjectLine(models.TransientModel):
_name = "hr.recruitment.auto.doc.project.line"
_description = "HR Recruitment Auto Document Project Line"
_order = "id"
wizard_line_id = fields.Many2one(
"hr.recruitment.auto.doc.wizard.line",
required=True,
ondelete="cascade",
)
project_name = fields.Char()
role = fields.Char()
technologies = fields.Char()
duration = fields.Char()
description = fields.Text()
def _to_payload(self):
self.ensure_one()
payload = {
"project_name": self.project_name,
"role": self.role,
"technologies": [item.strip() for item in (self.technologies or "").split(",") if item.strip()],
"duration": self.duration,
"description": self.description,
}
return {key: value for key, value in payload.items() if value not in (False, None, "", [])}
class HrRecruitmentAutoDocOtherLine(models.TransientModel):
_name = "hr.recruitment.auto.doc.other.line"
_description = "HR Recruitment Auto Document Other Parsed Data Line"
_order = "id"
wizard_line_id = fields.Many2one(
"hr.recruitment.auto.doc.wizard.line",
required=True,
ondelete="cascade",
)
key = fields.Char(required=True)
value = fields.Text()
def _json_value(self):
self.ensure_one()
if not self.value:
return False
try:
return json.loads(self.value)
except Exception:
return self.value

View File

@ -95,6 +95,58 @@
<page string="Skills">
<field name="skills_text" nolabel="1" placeholder="Enter one skill per line"/>
</page>
<page string="Education">
<field name="education_line_ids" nolabel="1">
<list editable="bottom">
<field name="education_type"/>
<field name="specialization"/>
<field name="university"/>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
</list>
</field>
</page>
<page string="Experience">
<field name="employer_line_ids" nolabel="1">
<list editable="bottom">
<field name="company_name"/>
<field name="designation"/>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
<field name="work_description"/>
</list>
</field>
</page>
<page string="Family">
<field name="family_line_ids" nolabel="1">
<list editable="bottom">
<field name="relation_type"/>
<field name="name"/>
<field name="contact_no"/>
<field name="dob"/>
<field name="location"/>
</list>
</field>
</page>
<page string="Certifications">
<field name="certification_line_ids" nolabel="1">
<list editable="bottom">
<field name="certification_name"/>
<field name="provider"/>
<field name="completion_date"/>
</list>
</field>
</page>
<page string="Other">
<field name="other_line_ids" nolabel="1">
<list editable="bottom">
<field name="key"/>
<field name="value"/>
</list>
</field>
</page>
</notebook>
<notebook invisible="target_model != 'job_recruitment'">