-
Notifications
You must be signed in to change notification settings - Fork 0
fix: model download lifecycle, file picker, PDF parsing & education extraction #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -270,7 +270,6 @@ impl LlamaClient { | |
| let location = None; | ||
| let mut skills = Vec::new(); | ||
| let mut experience_years = None; | ||
| let mut education = Vec::new(); | ||
| let work_experience = Vec::new(); | ||
|
|
||
| // 1. Email Regex | ||
|
|
@@ -328,23 +327,8 @@ impl LlamaClient { | |
| } | ||
| } | ||
|
|
||
| // 6. Education heuristic | ||
| let degrees = ["Bachelor", "B.Tech", "B.E.", "B.S.", "BS", "Master", "M.Tech", "M.S.", "MS", "Ph.D", "PhD", "Associate Degree"]; | ||
| for line in text.lines() { | ||
| for deg in degrees { | ||
| if line.to_lowercase().contains(°.to_lowercase()) { | ||
| education.push(Education { | ||
| degree: deg.to_string(), | ||
| institution: line.trim().to_string(), | ||
| year: None, | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| if education.len() >= 2 { | ||
| break; | ||
| } | ||
| } | ||
| // 6. Education extraction | ||
| let education = extract_education_from_text(text); | ||
|
|
||
| ExtractedCandidate { | ||
| name, | ||
|
|
@@ -361,6 +345,164 @@ impl LlamaClient { | |
| } | ||
| } | ||
|
|
||
| /// Strictly extracts educational credentials from resume text. | ||
| /// | ||
| /// Ensures credentials only originate from authentic education sections or explicit | ||
| /// degree patterns with word boundaries, preventing false positives from technical keywords | ||
| /// (e.g., "CMS", "AWS", "systems"). | ||
| pub fn extract_education_from_text(text: &str) -> Vec<Education> { | ||
| let mut education_entries = Vec::new(); | ||
| let lines: Vec<&str> = text.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect(); | ||
|
|
||
| let mut in_edu_section = false; | ||
| let mut edu_lines = Vec::new(); | ||
|
|
||
| let section_headers = [ | ||
| "EXPERIENCE", "WORK EXPERIENCE", "PROFESSIONAL EXPERIENCE", "EMPLOYMENT HISTORY", | ||
| "CAREER HISTORY", "SKILLS", "TECHNICAL SKILLS", "PROJECTS", "KEY PROJECTS", | ||
| "PERSONAL PROJECTS", "CERTIFICATIONS", "ACHIEVEMENTS", "AWARDS", "PUBLICATIONS", | ||
| "LANGUAGES", "INTERESTS", "VOLUNTEER", "VOLUNTEERING", "SUMMARY", "PROFESSIONAL SUMMARY", | ||
| ]; | ||
|
|
||
| for line in &lines { | ||
| let clean_upper = line.trim_matches(|c: char| c == ':' || c.is_whitespace()).to_uppercase(); | ||
| if clean_upper == "EDUCATION" | ||
| || clean_upper.starts_with("EDUCATION") | ||
| || clean_upper == "ACADEMIC BACKGROUND" | ||
| || clean_upper.starts_with("ACADEMIC") | ||
| || clean_upper == "ACADEMICS" | ||
| || clean_upper == "QUALIFICATIONS" | ||
| { | ||
| in_edu_section = true; | ||
| continue; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| if in_edu_section { | ||
| if section_headers.iter().any(|&hdr| clean_upper == hdr || clean_upper.starts_with(hdr)) { | ||
| break; | ||
| } | ||
| edu_lines.push(*line); | ||
| } | ||
| } | ||
|
|
||
| let has_edu_section = !edu_lines.is_empty(); | ||
| let search_lines = if has_edu_section { | ||
| &edu_lines[..] | ||
| } else { | ||
| &lines[..] | ||
| }; | ||
|
|
||
| let degree_patterns: &[(&str, &str)] = &[ | ||
| (r"(?i)\b(Bachelor(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|B\.?E\.?|B\.?Tech|B\.?S\.?(?:c)?|BCA|BBA)\b", "Bachelor"), | ||
| (r"(?i)\b(Master(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|M\.?E\.?|M\.?Tech|M\.?S\.?(?:c)?|MCA|MBA)\b", "Master"), | ||
| (r"(?i)\b(Ph\.?D\.?|Doctorate(?:\s+of\s+[A-Za-z\s&]+)?)\b", "Ph.D"), | ||
| (r"(?i)\b(Associate(?:'s)?(?:\s+Degree|\s+of\s+[A-Za-z\s&]+)?)\b", "Associate Degree"), | ||
| (r"(?i)\b(Diploma(?:\s+in\s+[A-Za-z\s&]+)?)\b", "Diploma"), | ||
| ]; | ||
|
Comment on lines
+395
to
+401
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Demonstrate the two regex defects with equivalent Python semantics.
python3 - <<'PY'
import re
bach = r"(?i)\b(Bachelor(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|B\.?E\.?|B\.?Tech|B\.?S\.?(?:c)?|BCA|BBA)\b"
mast = r"(?i)\b(Master(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|M\.?E\.?|M\.?Tech|M\.?S\.?(?:c)?|MCA|MBA)\b"
print("word 'be':", re.search(bach, "Coursework to be completed in 2024").group(0))
print("word 'me':", re.search(mast, "The team trusted me with delivery").group(0))
print("merged line:", re.search(bach, "Bachelor of Engineering Sri Shakthi Institute of Engineering and Technology").group(0))
PY
# Confirm the declared regex version and that lookaround is unavailable.
fd -t f Cargo.toml src-tauri --exec rg -n 'regex'Repository: rigial/hirelens Length of output: 288 🌐 Web query:
💡 Result: The Rust regex crate, including version 1.11, does not support general-purpose lookaround assertions (lookahead or lookbehind) [1][2]. The crate's design philosophy prioritizes linear-time execution (worst-case O(m*n)) and avoids features that are not known to be implementable with such guarantees [1][3]. While there is ongoing development and research into adding specific forms of lookaround—such as an experimental pull request for unbounded look-behind expressions [4]—these features are generally not part of the standard, stable API provided by the crate [1][5]. Regarding inline flag groups, the crate does support them [5]. Syntax such as (?i:...) is supported for setting or clearing flags (like case-insensitivity) over a specific group [5][6]. You can use these to enable or disable flags for part of an expression, for example, (?i) for case-insensitive matching [5][6]. Other flags, such as 's' (dot-matches-newline), 'm' (multi-line), and 'x' (verbose/ignore whitespace), are also supported via this syntax [5]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- client.rs structure ---'
ast-grep outline src-tauri/src/llm/client.rs 2>/dev/null || true
printf '%s\n' '--- client.rs relevant sections ---'
sed -n '350,440p' src-tauri/src/llm/client.rs
sed -n '500,530p' src-tauri/src/llm/client.rs
printf '%s\n' '--- parser normalization references ---'
fd -t f . src-tauri/src/processing/parser --exec rg -n -C 4 'merge|normalize|line|education|institution' {} \; || true
printf '%s\n' '--- degree pattern and extraction references ---'
rg -n -C 5 'degree_patterns|Educational Institution|institution|Bachelor|Master' src-tauri/srcRepository: rigial/hirelens Length of output: 30533 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- extraction implementation ---'
sed -n '430,490p' src-tauri/src/llm/client.rs
printf '%s\n' '--- PDF normalizer implementation ---'
sed -n '1,170p' src-tauri/src/processing/parser/pdf.rs
printf '%s\n' '--- standalone behavior probe ---'
python3 - <<'PY'
import re
current = [
(r"(?i)\b(Bachelor(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|B\.?E\.?|B\.?Tech|B\.?S\.?(?:c)?|BCA|BBA)\b", "Bachelor"),
(r"(?i)\b(Master(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|M\.?E\.?|M\.?Tech|M\.?S\.?(?:c)?|MCA|MBA)\b", "Master"),
]
proposed = [
(r"\b((?i:Bachelor(?:'s)?)(?:\s+(?i:of)\s+[A-Za-z&]+(?:\s+[A-Za-z&]+){0,3}|B\.?Tech|BCA|BBA|B\.?E\.?|B\.?Sc?\.?)\b", "Bachelor"),
(r"\b((?i:Master(?:'s)?)(?:\s+(?i:of)\s+[A-Za-z&]+(?:\s+[A-Za-z&]+){0,3})?|M\.?Tech|MCA|MBA|M\.?E\.?|M\.?Sc?\.?)\b", "Master"),
]
cases = [
"Coursework to be completed in 2024",
"The team trusted me with delivery",
"Bachelor of Engineering Sri Shakthi Institute of Engineering and Technology",
"Master of Science in Computer Science, Stanford University",
"Bachelor",
"Bachelor of Engineering, Stanford University",
]
for line in cases:
print(f"\n{line!r}")
for label, patterns in (("current", current), ("proposed", proposed)):
hits = []
for pattern, category in patterns:
match = re.search(pattern, line)
if match:
degree = match.group(0)
remainder = line.replace(degree, "").strip(" ,-|\u2013\u2014\t")
hits.append((category, degree, remainder))
break
print(label, hits)
PYRepository: rigial/hirelens Length of output: 9287 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import re
current = [
(r"(?i)\b(Bachelor(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|B\.?E\.?|B\.?Tech|B\.?S\.?(?:c)?|BCA|BBA)\b", "Bachelor"),
(r"(?i)\b(Master(?:'s)?(?:\s+of\s+[A-Za-z\s&]+)?|M\.?E\.?|M\.?Tech|M\.?S\.?(?:c)?|MCA|MBA)\b", "Master"),
]
proposed = [
(r"\b((?i:Bachelor(?:'s)?)(?:\s+(?i:of)\s+[A-Za-z&]+(?:\s+[A-Za-z&]+){0,3}|B\.?Tech|BCA|BBA|B\.?E\.?|B\.?Sc?\.?)\b", "Bachelor"),
(r"\b((?i:Master(?:'s)?)(?:\s+(?i:of)\s+[A-Za-z&]+(?:\s+[A-Za-z&]+){0,3})?|M\.?Tech|MCA|MBA|M\.?E\.?|M\.?Sc?\.?)\b", "Master"),
]
cases = [
"Coursework to be completed in 2024",
"The team trusted me with delivery",
"Bachelor of Engineering Sri Shakthi Institute of Engineering and Technology",
"Master of Science in Computer Science, Stanford University",
"Bachelor",
"Bachelor of Engineering, Stanford University",
]
for line in cases:
print(f"\n{line!r}")
for label, patterns in (("current", current), ("proposed", proposed)):
try:
hits = []
for pattern, category in patterns:
match = re.search(pattern, line)
if match:
degree = match.group(0)
remainder = line.replace(degree, "").strip(" ,-|\u2013\u2014\t")
hits.append((category, degree, remainder))
break
print(label, hits)
except re.error as exc:
print(label, "INVALID REGEX:", exc)
def normalize_like_source(raw):
tokens = raw.split()
single = {"SUMMARY","PROFILE","SKILLS","EXPERIENCE","PROJECTS","EDUCATION",
"CERTIFICATIONS","ACHIEVEMENTS","AWARDS","PUBLICATIONS","LANGUAGES","INTERESTS","VOLUNTEERING"}
multi = {"PROFESSIONAL SUMMARY","EXECUTIVE SUMMARY","TECHNICAL SKILLS","SKILLS & ABILITIES",
"PROFESSIONAL EXPERIENCE","WORK EXPERIENCE","EMPLOYMENT HISTORY","CAREER HISTORY",
"KEY PROJECTS","PERSONAL PROJECTS","ACADEMIC BACKGROUND","CERTIFICATIONS & LICENSES"}
lines, current_line = [], []
i = 0
while i < len(tokens):
token = tokens[i]
nxt = tokens[i+1] if i+1 < len(tokens) else ""
nxt2 = tokens[i+2] if i+2 < len(tokens) else ""
c3 = f"{token} {nxt} {nxt2}".upper()
c2 = f"{token} {nxt}".upper()
c1 = token.upper()
count = 0
header = None
if c3 in multi:
header, count = c3, 3
elif c2 in multi:
header, count = c2, 2
elif c1 in single and nxt != "&" and nxt != "and" and not nxt.endswith(":") and not token.endswith(":"):
header, count = c1, 1
if header is not None:
if current_line:
lines.append(" ".join(current_line))
current_line = []
lines += ["", header, ""]
i += count
continue
current_line.append(token)
i += 1
if current_line:
lines.append(" ".join(current_line))
cleaned, blank = [], False
for line in lines:
line = line.strip()
if not line:
if not blank and cleaned:
cleaned.append("")
blank = True
else:
cleaned.append(line)
blank = False
return "\n".join(cleaned)
print("\nnormalized PDF-like input:")
print(normalize_like_source("EDUCATION\nBachelor of Engineering\nSri Shakthi Institute of Engineering and Technology"))
PYRepository: rigial/hirelens Length of output: 1412 Correct the degree patterns before merging.
🤖 Prompt for AI Agents |
||
|
|
||
| let compiled_patterns: Vec<(Regex, &'static str)> = degree_patterns | ||
| .iter() | ||
| .filter_map(|(pat, cat)| Regex::new(pat).ok().map(|re| (re, *cat))) | ||
| .collect(); | ||
|
|
||
| let year_re = Regex::new(r"\b(19\d{2}|20\d{2})\b").ok(); | ||
| let inst_keywords = ["University", "Institute", "College", "School", "Academy", "Polytechnic", "Campus"]; | ||
|
|
||
| let mut i = 0; | ||
| while i < search_lines.len() { | ||
| let line = search_lines[i]; | ||
| let mut found_degree: Option<String> = None; | ||
| let mut degree_category = ""; | ||
|
|
||
| for (re, cat) in &compiled_patterns { | ||
| if let Some(mat) = re.find(line) { | ||
| found_degree = Some(mat.as_str().trim().to_string()); | ||
| degree_category = *cat; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if let Some(deg) = found_degree { | ||
| // When outside an explicit education section, require strong institution keywords or explicit degree phrasing | ||
| if !has_edu_section { | ||
| let has_inst = inst_keywords.iter().any(|k| line.contains(k)); | ||
| let is_explicit_degree = deg.to_lowercase().contains("bachelor") | ||
| || deg.to_lowercase().contains("master") | ||
| || deg.to_lowercase().contains("doctorate") | ||
| || deg.to_lowercase().contains("degree"); | ||
| if !has_inst && !is_explicit_degree { | ||
| i += 1; | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| let mut year = None; | ||
| if let Some(ref y_re) = year_re { | ||
| if let Some(y_mat) = y_re.find(line) { | ||
| year = Some(y_mat.as_str().to_string()); | ||
| } | ||
| } | ||
|
|
||
| let remainder = line.replace(°, "").trim().to_string(); | ||
| let mut clean_rem = remainder.trim_matches(|c: char| c == ',' || c == '-' || c == '|' || c == '–' || c == '—' || c.is_whitespace()).to_string(); | ||
| if let Some(ref y_val) = year { | ||
| clean_rem = clean_rem.replace(y_val, ""); | ||
| clean_rem = clean_rem.trim_matches(|c: char| c == ',' || c == '-' || c == '|' || c == '–' || c == '—' || c.is_whitespace()).to_string(); | ||
| } | ||
|
|
||
| let mut institution = String::new(); | ||
| if !clean_rem.is_empty() && (inst_keywords.iter().any(|k| clean_rem.contains(k)) || clean_rem.len() > 3) { | ||
| institution = clean_rem.clone(); | ||
| } else if i + 1 < search_lines.len() { | ||
| let next_line = search_lines[i + 1]; | ||
| let next_clean_upper = next_line.trim_matches(|c: char| c == ':' || c.is_whitespace()).to_uppercase(); | ||
| let is_next_header = section_headers.iter().any(|&hdr| next_clean_upper == hdr); | ||
| let is_next_degree = compiled_patterns.iter().any(|(re, _)| re.is_match(next_line)); | ||
|
|
||
| if !is_next_header && !is_next_degree { | ||
| let mut inst_str = next_line.trim().to_string(); | ||
| if year.is_none() { | ||
| if let Some(ref y_re) = year_re { | ||
| if let Some(y_mat) = y_re.find(next_line) { | ||
| year = Some(y_mat.as_str().to_string()); | ||
| } | ||
| } | ||
| } | ||
| if let Some(ref y_val) = year { | ||
| inst_str = inst_str.replace(y_val, ""); | ||
| inst_str = inst_str.trim_matches(|c: char| c == ',' || c == '-' || c == '|' || c == '–' || c == '—' || c.is_whitespace()).to_string(); | ||
| } | ||
| institution = inst_str; | ||
| i += 1; | ||
| } | ||
| } | ||
|
|
||
| if institution.is_empty() { | ||
| institution = if !clean_rem.is_empty() { clean_rem } else { "Educational Institution".to_string() }; | ||
| } | ||
|
|
||
| let formatted_degree = if deg.len() <= 4 && !deg.contains(' ') { | ||
| format!("{} ({})", degree_category, deg) | ||
| } else { | ||
| deg | ||
| }; | ||
|
|
||
| education_entries.push(Education { | ||
| degree: formatted_degree, | ||
| institution, | ||
| year, | ||
| }); | ||
| } | ||
|
|
||
| i += 1; | ||
| if education_entries.len() >= 3 { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| education_entries | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
@@ -428,4 +570,52 @@ Bachelor of Science in Computer Science, Stanford University | |
| assert!(!analysis.summary.is_empty()); | ||
| assert!(!analysis.strengths.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_extract_education_no_false_cms_positives() { | ||
| let resume_text = r#" | ||
| KISHORE KUMAR | ||
| Software Engineer | ||
|
|
||
| EXPERIENCE | ||
| Software Engineer — Apparel Group — 6thStreet.com | ||
| ● Delivered 20+ features for 6thStreet's React Native app. | ||
| ● Managed CMS integration, enabling dynamic and seamless content updates without redeploys. | ||
| ● Integrated AWS Secrets Manager for secure credential management. | ||
|
|
||
| EDUCATION | ||
| Bachelor of Engineering | ||
| Sri Shakthi Institute of Engineering and Technology | ||
| "#; | ||
|
|
||
| let edu = extract_education_from_text(resume_text); | ||
| assert_eq!(edu.len(), 1); | ||
| assert_eq!(edu[0].degree, "Bachelor of Engineering"); | ||
| assert_eq!(edu[0].institution, "Sri Shakthi Institute of Engineering and Technology"); | ||
|
|
||
| // Verify CMS / AWS was not extracted as a degree | ||
| for e in &edu { | ||
| assert!(!e.degree.contains("CMS")); | ||
| assert!(!e.institution.contains("CMS")); | ||
| assert_ne!(e.degree, "MS"); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_extract_education_multiple_real_degrees() { | ||
| let resume_text = r#" | ||
| EDUCATION | ||
| Master of Science in Computer Science, Stanford University, 2021 | ||
| Bachelor of Technology, MIT, 2019 | ||
| "#; | ||
|
|
||
| let edu = extract_education_from_text(resume_text); | ||
| assert_eq!(edu.len(), 2); | ||
| assert!(edu[0].degree.contains("Master")); | ||
| assert_eq!(edu[0].institution, "Stanford University"); | ||
| assert_eq!(edu[0].year, Some("2021".to_string())); | ||
| assert!(edu[1].degree.contains("Bachelor")); | ||
| assert_eq!(edu[1].institution, "MIT"); | ||
| assert_eq!(edu[1].year, Some("2019".to_string())); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: rigial/hirelens
Length of output: 183
🏁 Script executed:
Repository: rigial/hirelens
Length of output: 5588
🏁 Script executed:
Repository: rigial/hirelens
Length of output: 17546
Remove
fs:defaultfromsrc-tauri/capabilities/default.json. The file picker uses@tauri-apps/plugin-dialog; no frontend code uses@tauri-apps/plugin-fs.🤖 Prompt for AI Agents