| import json |
| import re |
| import requests as _req |
|
|
|
|
| POWER_SKILLS = { |
| "sql", "python", "excel", "power bi", "tableau", "looker", "r", "spss", |
| "stata", "gis", "kobo", "odk", "dhis2", "redcap", "power query", "dax", |
| "data analysis", "data analyst", "dashboard", "reporting", "kpi", |
| "monitoring", "evaluation", "m&e", "meal", "indicator", "database", |
| "bi", "etl", "data entry", "data quality", "data cleaning", "data collection", |
| "data management", "data warehouse", "data validation", "visualization", |
| "mysql", "postgresql", "azure", "aws", "spark", "machine learning", "pipeline", |
| "survey", "beneficiary", "humanitarian", "stakeholder", "statistics", |
| "google sheets", "salesforce", "attention to detail", "analytical", |
| } |
|
|
|
|
| def top_keywords(text: str, n: int = 15) -> list[str]: |
| scored: dict[str, int] = {} |
| lower = text.lower() |
| tokens = lower.split() |
| for skill in POWER_SKILLS: |
| if skill in lower: |
| scored[skill] = lower.count(skill) |
| for i in range(len(tokens) - 1): |
| bigram = f"{tokens[i]} {tokens[i+1]}" |
| if bigram in POWER_SKILLS and bigram not in scored: |
| scored[bigram] = lower.count(bigram) |
| if len(scored) < n: |
| for cap in re.findall(r"\b[A-Z][A-Za-z]{2,12}\b", text): |
| cl = cap.lower() |
| if cl not in scored: |
| scored[cl] = 1 |
| return sorted(scored, key=lambda k: -scored[k])[:n] |
|
|
|
|
| def _parse_json(raw: str) -> dict | None: |
| raw = re.sub(r"```[a-z]*", "", raw or "").strip("` \n") |
| try: |
| return json.loads(raw) |
| except Exception: |
| m = re.search(r"\{.*\}", raw, re.DOTALL) |
| if m: |
| try: |
| return json.loads(m.group()) |
| except Exception: |
| pass |
| return None |
|
|
|
|
| def _call_groq(prompt: str, api_key: str, max_tokens: int = 512) -> str | None: |
| if not api_key: |
| return None |
| try: |
| resp = _req.post( |
| "https://api.groq.com/openai/v1/chat/completions", |
| headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, |
| json={ |
| "model": "llama-3.3-70b-versatile", |
| "messages": [ |
| {"role": "system", "content": "You are a professional CV writer. Output only valid JSON."}, |
| {"role": "user", "content": prompt}, |
| ], |
| "temperature": 0.7, |
| "max_tokens": max_tokens, |
| }, |
| timeout=30, |
| ) |
| data = resp.json() |
| if "choices" not in data: |
| print(f"[llm/groq] no choices key. status={resp.status_code}, body={str(data)[:300]}") |
| return None |
| return data["choices"][0]["message"]["content"] |
| except Exception as e: |
| print(f"[llm/groq] {type(e).__name__}: {e}") |
| return None |
|
|
|
|
| def _call_nim(prompt: str, api_key: str, max_tokens: int = 512) -> str | None: |
| if not api_key: |
| return None |
| for attempt in range(3): |
| try: |
| resp = _req.post( |
| "https://openrouter.ai/api/v1/chat/completions", |
| headers={ |
| "Authorization": f"Bearer {api_key}", |
| "Content-Type": "application/json", |
| "HTTP-Referer": "http://localhost:8002", |
| }, |
| json={ |
| "model": "openrouter/free", |
| "messages": [ |
| {"role": "system", "content": "You are a professional CV writer. Output only valid JSON."}, |
| {"role": "user", "content": prompt}, |
| ], |
| "max_tokens": max_tokens, |
| "temperature": 0.7, |
| "stream": False, |
| }, |
| timeout=60, |
| ) |
| raw = resp.text |
| try: |
| data = json.loads(raw) |
| except json.JSONDecodeError: |
| print(f"[llm/nim] JSON parse error. status={resp.status_code}, raw={raw[:300]}") |
| return None |
| if "choices" not in data: |
| print(f"[llm/nim] unexpected response: {str(data)[:200]}") |
| return None |
| return data["choices"][0]["message"]["content"] |
| except Exception as e: |
| if attempt < 2: |
| import time as _t |
| _t.sleep(2) |
| continue |
| print(f"[llm/nim] error ({attempt+1} attempts): {type(e).__name__}: {e}") |
| return None |
| return None |
|
|
|
|
| def _call_gemini(prompt: str, api_key: str, max_tokens: int = 512) -> str | None: |
| if not api_key: |
| return None |
| try: |
| url = ( |
| f"https://generativelanguage.googleapis.com/v1beta/models/" |
| f"gemini-1.5-flash:generateContent?key={api_key}" |
| ) |
| resp = _req.post( |
| url, |
| json={ |
| "contents": [{"parts": [{"text": prompt}]}], |
| "generationConfig": {"temperature": 0.7, "maxOutputTokens": max_tokens}, |
| }, |
| timeout=30, |
| ) |
| return resp.json()["candidates"][0]["content"]["parts"][0]["text"] |
| except Exception as e: |
| print(f"[llm/gemini] {e}") |
| return None |
|
|
|
|
| def _call_gemma(prompt: str, api_key: str, max_tokens: int = 512) -> str | None: |
| if not api_key: |
| return None |
| try: |
| url = ( |
| f"https://generativelanguage.googleapis.com/v1beta/models/" |
| f"gemma-3-27b-it:generateContent?key={api_key}" |
| ) |
| resp = _req.post( |
| url, |
| json={ |
| "contents": [{"parts": [{"text": prompt}]}], |
| "generationConfig": {"temperature": 0.7, "maxOutputTokens": max_tokens}, |
| }, |
| timeout=30, |
| ) |
| return resp.json()["candidates"][0]["content"]["parts"][0]["text"] |
| except Exception as e: |
| print(f"[llm/gemma] {e}") |
| return None |
|
|
|
|
| def _call_any(prompt: str, api_keys: dict, max_tokens: int = 512) -> str | None: |
| if not any(api_keys.values()): |
| print("[llm] No API keys available — will use rule-based fallback") |
| result = _call_nim(prompt, api_keys.get("nvidia", ""), max_tokens) |
| if result: return result |
| result = _call_groq(prompt, api_keys.get("groq", ""), max_tokens) |
| if result: return result |
| result = _call_gemini(prompt, api_keys.get("gemini", ""), max_tokens) |
| if result: return result |
| result = _call_gemma(prompt, api_keys.get("gemini", ""), max_tokens) |
| return result |
|
|
|
|
| SKILL_TAXONOMY = { |
| "Data Analytics & Engineering": { |
| "keywords": ["python", "sql", "r", "spark", "excel", "pandas", "numpy", "scikit-learn", "tensorflow", "pytorch", "statistical modeling", "regression", "forecasting", "machine learning", "predictive modeling", "data mining", "etl", "databricks"], |
| "group_template": "Data Analytics & Engineering" |
| }, |
| "BI & Visualization": { |
| "keywords": ["power bi", "tableau", "looker", "qilk", "google data studio", "microstrategy", "d3.js", "matplotlib", "ggplot2", "seaborn", "plotly", "dashboards", "scorecards", "visual analytics"], |
| "group_template": "BI & Visualization" |
| }, |
| "Cloud & Infrastructure": { |
| "keywords": ["aws", "azure", "gcp", "google cloud", "cloud computing", "docker", "kubernetes", "terraform", "ci/cd", "devops", "bigquery", "snowflake", "redshift", "synapse", "data lake", "data warehouse"], |
| "group_template": "Cloud & Infrastructure" |
| }, |
| "Database Management": { |
| "keywords": ["postgresql", "mysql", "mongodb", "oracle", "sql server", "sqlite", "mariadb", "cassandra", "neo4j", "dynamodb", "nosql", "database design", "data modeling", "erd"], |
| "group_template": "Database Management" |
| }, |
| "Project Management & Methods": { |
| "keywords": ["agile", "scrum", "kanban", "jira", "trello", "asana", "monday.com", "ms project", "project management", "stakeholder management", "requirements gathering", "sdlc", "waterfall", "cross-functional"], |
| "group_template": "Project Management & Methodology" |
| }, |
| "Data Collection & M&E": { |
| "keywords": ["m&e", "monitoring and evaluation", "dhis2", "kobotoolbox", "odk", "survey cto", "data collection", "surveys", "questionnaires", "indicator tracking", "logframe", "results framework", "nbs", "who", "world bank", "npc"], |
| "group_template": "Data Collection & M&E" |
| }, |
| } |
|
|
|
|
| def _group_skills(cv_skills: list) -> str: |
| """Convert any skill format to a grouped string for the prompt.""" |
| if not cv_skills: |
| return "It looks like no skills were provided." |
| |
| |
| if isinstance(cv_skills[0], dict): |
| parts = [] |
| for g in cv_skills: |
| items = g.get("items", []) |
| name = g.get("domain", g.get("name", "")) |
| desc = g.get("description", "") |
| if items: |
| parts.append(f"- {name}: {', '.join(items)}") |
| if desc: |
| parts.append(f" {desc}") |
| return "\n".join(parts) |
| |
| |
| flat = [s.lower() for s in cv_skills] |
| grouped = {} |
| uncovered = [] |
| |
| for skill in flat: |
| placed = False |
| for group_name, group_info in SKILL_TAXONOMY.items(): |
| if any(kw in skill for kw in group_info["keywords"]): |
| grouped.setdefault(group_name, []).append(skill) |
| placed = True |
| break |
| if not placed: |
| uncovered.append(skill) |
| |
| if uncovered: |
| grouped["Other"] = uncovered |
| |
| parts = [] |
| for gname, items in grouped.items(): |
| original_names = [s for s in cv_skills if s.lower() in items] |
| parts.append(f"- {gname}: {', '.join(original_names)}") |
| return "\n".join(parts) |
|
|
|
|
| def tailor_application( |
| job_title: str, |
| job_description: str, |
| company: str, |
| user_cv: dict, |
| api_keys: dict = None, |
| category: str = "", |
| feedback: str = "", |
| attempt: int = 1, |
| raw_text: str = "", |
| ) -> dict: |
| api_keys = api_keys or {} |
| jd_keywords = top_keywords(job_description) |
| cv_skills = user_cv.get("skills", []) |
| |
| |
| skills_str = _group_skills(cv_skills) |
|
|
| exp_entries_raw = user_cv.get("experience", []) |
| exp_list_json = json.dumps([ |
| { |
| "title": e.get("title", ""), |
| "company": e.get("company", ""), |
| "start_date": e.get("start_date", ""), |
| "end_date": e.get("end_date", ""), |
| "current": e.get("current", False), |
| "description": e.get("description", ""), |
| "achievements": e.get("achievements", []), |
| } |
| for e in exp_entries_raw[:5] |
| ], ensure_ascii=False) |
|
|
| education = user_cv.get("education", []) |
| education_json = json.dumps(education, ensure_ascii=False) if education else "None provided" |
|
|
| certs = user_cv.get("certifications", []) |
| cert_str = "\n".join(f"- {c}" for c in certs if c) if certs else "None" |
|
|
| projects = user_cv.get("projects", []) |
| proj_str = "\n".join( |
| f"- {p.get('name')} ({', '.join(p.get('technologies', []))}): {p.get('description', '')[:150]}" |
| for p in projects[:6] |
| ) if projects else "None" |
|
|
| languages = user_cv.get("languages", []) |
| lang_str = ", ".join(languages) if languages else "Not specified" |
|
|
| summary_from_cv = user_cv.get("professional_summary", "") |
|
|
| is_highimpact = category == "highimpact" |
| cert_boost = "" |
| if is_highimpact and any("anthropic" in c.lower() or "ai fluency" in c.lower() for c in certs): |
| cert_boost = ( |
| "\nCRITICAL: Candidate holds the Anthropic 'AI Fluency for Nonprofits' certification. " |
| "Lead with this in the professional summary." |
| ) |
|
|
| |
| def _flatten_skills(skills): |
| if not skills: |
| return "" |
| if isinstance(skills[0], str): |
| return " ".join(skills).lower() |
| if isinstance(skills[0], dict): |
| items = [] |
| for g in skills: |
| items.extend(g.get("items", [])) |
| return " ".join(items).lower() |
| return "" |
|
|
| |
| jd_lower = job_description.lower() |
| missing_groups = [] |
| related_groups = [] |
| cv_flat = _flatten_skills(cv_skills) |
| for group_name, group_info in SKILL_TAXONOMY.items(): |
| group_mentioned = any(kw in jd_lower for kw in group_info["keywords"]) |
| if group_mentioned: |
| candidate_has = any(kw in cv_flat for kw in group_info["keywords"]) |
| if not candidate_has: |
| missing_groups.append(group_name) |
| else: |
| related_groups.append(group_name) |
|
|
| |
| target_title_lower = job_title.lower() |
| past_titles = [e.get("title", "").lower() for e in exp_entries_raw if e.get("title")] |
| is_role_switch = True |
| for pt in past_titles: |
| if any(kw in pt for kw in target_title_lower.split()): |
| is_role_switch = False |
| break |
| transferable_boost = "" |
| if is_role_switch and past_titles: |
| transferable_boost = ( |
| "\n## TRANSFERABLE SKILLS BRIDGE ##\n" |
| f"The candidate has never held a '{job_title}' role before (past roles: {', '.join(past_titles[:3])}).\n" |
| "CRITICAL: You MUST identify transferable skills from their existing experience that map to the target role.\n" |
| "- Find achievements in their current roles that demonstrate skills needed for the target role\n" |
| "- Rewrite bullets to emphasize the RELEVANT SKILL for the target role, not the original context\n" |
| "- Example: 'Managed team of 5 field officers' targeting a data role → 'Coordinated data collection across 5 field officers, ensuring 98% reporting compliance'\n" |
| "- Example: 'Processed invoices for 200+ vendors' targeting a project management role → 'Managed cross-functional workflows across 200+ stakeholders, delivering on-time processing'\n" |
| "- NEVER fabricate job titles, companies, or dates\n" |
| "- NEVER claim the candidate held the target title already\n" |
| "- In the summary, name the transfer explicitly: 'Bringing [X years] of [domain] experience, with proven transferable skills in [skill1], [skill2], and [skill3] directly applicable to the [target role] role.'\n" |
| "- Cover letter MUST address the career pivot directly: 'While my background is in [past domain], my experience in [transferable skill] maps directly to [JD requirement].'\n" |
| ) |
|
|
| bridging_suggestions = "" |
| if missing_groups: |
| bridging_suggestions = ( |
| "\nBRIDGING MISSING SKILLS: The JD mentions these skill areas not in the candidate's CV:\n" |
| + "\n".join(f"- {g}" for g in missing_groups) |
| + "\nFor any group above, if the candidate has a RELATED skill (e.g. Tableau instead of Power BI), " |
| "add a bridging sentence in the skills description: 'While experienced in [their tool], " |
| "demonstrated same [methodology] with [JD tool] dashboards adopted by C-suite.' " |
| "If truly absent, don't fabricate." |
| ) |
|
|
| feedback_section = "" |
| if feedback: |
| feedback_section = ( |
| "\n## QUALITY FEEDBACK: PREVIOUS DRAFT REJECTED ##\n" |
| "Your previous submission was rejected because:\n" |
| + feedback + "\n" |
| "Fix EVERY issue listed above. This is attempt " + str(attempt) + " of 3." |
| ) |
|
|
| prompt = ( |
| "You are a top-1% resume writer and career strategist. Your work gets " |
| "past ATS filters, passes F-pattern/Z-pattern visual scan tests, and " |
| "convinces HR in under 10 seconds. Every sentence must include a " |
| "measurable outcome OR a specific strategic keyword from the job " |
| "description. Never use AI buzzwords. Never be generic.\n\n" |
| "## UNIQUENESS DIRECTIVE — ANTI-SLOP ##\n" |
| "CRITICAL: If 10 candidates with similar backgrounds apply to this same role, " |
| "their CVs MUST look completely different — different examples, different sentence structures, " |
| "different metrics emphasized, different skills highlighted. You must find and lead with " |
| "what makes THIS candidate distinct. AI-generated templates are obvious and rejected instantly.\n\n" |
| "EVERY output will be checked for AI-slop patterns. If detected, the output will be discarded.\n\n" |
| "Concrete rules:\n" |
| "- Vary sentence openings. Do not start two bullets with the same verb.\n" |
| "- Never use the same example or metric as another candidate would.\n" |
| "- Lead each bullet with the candidate's most distinctive specific achievement first, " |
| "not a generic duty.\n" |
| "- Avoid common resume patterns. No 'responsible for', 'tasked with', 'duties included'.\n" |
| "- Inject specific project names, tools, methodologies, and contexts from the real data.\n" |
| "- The summary must feel like a real person wrote it — not a template.\n" |
| "- If the candidate has numbers (%, $, time), ALWAYS include them. Never round to a generic number.\n" |
| "- Every sentence must contain at least one specific fact that could not apply to another candidate.\n\n" |
| f"TARGET ROLE: {job_title} at {company}\n" |
| f"JOB DESCRIPTION KEYWORDS: {', '.join(jd_keywords)}\n\n" |
| "## FULL CANDIDATE CV ##\n" |
| f"Professional Summary:\n{summary_from_cv[:500]}\n\n" |
| f"Skills:\n{skills_str}\n\n" |
| f"Experience (JSON):\n{exp_list_json}\n\n" |
| f"Education:\n{education_json}\n\n" |
| f"Certifications:\n{cert_str}\n" |
| f"Projects:\n{proj_str}\n" |
| f"Languages: {lang_str}\n" |
| + ( |
| f"\n## RAW CV TEXT ##\n{raw_text[:4000]}\n## END RAW CV TEXT ##\n\n" |
| "The raw text above may contain additional experience entries, education, and skills " |
| "NOT present in the structured sections. You MUST extract ALL experience entries from " |
| "the raw text and include them as proper 'tailored_experience' entries with title, " |
| "company, and 4-7 achievement bullets each. Do NOT leave any role mentioned in raw text " |
| "as unstructured text in the summary — create a full experience entry for it.\n" |
| if raw_text else "" |
| ) |
| + cert_boost |
| + transferable_boost |
| + bridging_suggestions |
| + feedback_section |
| + "\n\n## OUTPUT INSTRUCTIONS ##\n" |
| "Return ONLY valid JSON. No markdown. No code fences. No explanation. " |
| "Every section must be rewritten for the TARGET ROLE. Do NOT pass through original content unchanged.\n" |
| + '{\n' |
| + ' "professional_summary": "3-4 sentence narrative. Sentence 1: title+years+domain. Sentence 2: biggest metric-driven achievement relevant to THIS job. Sentence 3: technical stack from JD. Sentence 4: value you bring them. NEVER start with \'I am\' or \'A highly\' — start with job title directly.",\n' |
| + ' "tailored_skills": [\n' |
| + ' {\n' |
| + ' "domain": "Group Name (e.g. Data Analytics & Engineering)",\n' |
| + ' "items": ["Skill1", "Skill2", "Skill3"],\n' |
| + ' "description": "1-2 sentences showing tool + context + metric outcome. E.g. \'Built ETL pipelines processing 56k+ data points across 45 indicators using Python and SQL.\'"\n' |
| + ' }\n' |
| + ' ],\n' |
| + ' "tailored_experience": [\n' |
| + ' {\n' |
| + ' "title": "(original title — do not change)",\n' |
| + ' "company": "(original company — do not change)",\n' |
| + ' "description": "(1-line rewritten to highlight JD-relevant impact, or empty string)",\n' |
| + ' "achievements": ["(5-7 bullets, each with specific numbers: %, $, time, volume. Must use action verbs and metrics.)"]\n' |
| + ' }\n' |
| + ' ],\n' |
| + ( |
| ' "cover_letter": "Full-page letter (350-500 words, 4-5 paragraphs). ' |
| 'NO opening cliches. NO generic praise. This is a serious professional document — not a form letter. ' |
| 'PARAGRAPH 1: Reference something specific about the company current work, product, or recent achievement. Show you did your research. Then name the role and state a specific value proposition tied to company needs. MAX 4 sentences. ' |
| 'PARAGRAPH 2: Deep dive into your most relevant achievement. Include: company name, action taken, measurable outcome with hard numbers, method or tool used. Connect it directly to a JD requirement. 3-4 sentences. ' |
| 'PARAGRAPH 3: Second major achievement with a different angle (technical depth, leadership, problem-solving). Show breadth. Include company name, metric, method. 3-4 sentences. ' |
| 'PARAGRAPH 4: What you bring that is unique — specific technical stack, domain expertise, or approach that sets you apart from other applicants. Tie to the company specific goals. 2-3 sentences. ' |
| 'PARAGRAPH 5: Brief closing. State what you would like to discuss. Offer to provide additional information. Show enthusiasm through specificity, not adjectives. 1-2 sentences. ' |
| 'UNIQUENESS: No two candidates should sound alike. Lead with the most distinctive real achievement. Vary sentence structures, openings, and metrics chosen from the candidate real data. Avoid any fill-in-the-blank phrasing.\n' |
| 'CRITICAL - NEVER use ANY of these banned phrases or words:\n' |
| '- Openers: I am excited, I am writing to apply, I am confident, I am impressed by, I would be a valuable addition, I believe I am the ideal candidate, It is with great enthusiasm\n' |
| '- Jargon: leverage, synergy, proven track record, drive business growth, equipped me with, I believe, I think, in today competitive landscape, think outside the box, best-in-class, world-class, end-to-end, deep dive (as noun), low-hanging fruit, move the needle, pain point, game-changer, circle back, holistic, robust, scalable (unless describing actual tech)\n' |
| '- Buzzwords: passionate, enthusiastic, team player, detail-oriented, results-driven, proactive, self-starter, go-getter, rockstar, ninja, guru, hardworking, dedicated, motivated\n' |
| '- Generic praise: industry-leading, cutting-edge, state-of-the-art, market-leading, dynamic\n' |
| 'ANTI-EXAMPLE (REJECTED - full of AI slop): I am excited to apply for the data role at your company, where I can leverage my expertise in data analytics to drive business growth. As a passionate and results-driven professional with a proven track record, I believe I would be a valuable addition to your dynamic team.\n' |
| 'GOOD EXAMPLE (ACCEPTED - specific, human, metrics-driven): BCG X deploys AI at scale for supply chain optimization -- the same challenge I tackled at Andela, where I built ML pipelines that cut logistics processing time by 60%. As a Forward Deployed AI Scientist with 4 years of production ML experience across three countries, I bring hands-on expertise deploying models that actually make decisions, not just predictions.",\n' |
| ) |
| + ' "keywords_hit": ["kw1","kw2","kw3","kw4","kw5"],\n' |
| + ' "match_score": 85\n' |
| + '}\n\n' |
| "## CRITICAL RULES ##\n" |
| "- 'tailored_skills' MUST be an ARRAY OF OBJECTS with 'domain', 'items', 'description'. " |
| "Each domain groups 3-6 related skills. Each domain has a 1-2 sentence description showing context + metrics. " |
| "Aim for 3-5 domain groups covering ALL candidate skills relevant to the job.\n" |
| "- 'tailored_experience': same title/company as original. Rewrite ALL achievements to be JD-relevant with metrics. " |
| "CRITICAL: EVERY role MUST have 4-7 achievement bullets. If a role has fewer than 4 in the source data, " |
| "reconstruct achievements from the raw CV text or job description context. " |
| "EVERY bullet must have at least one number (%, $, time saved, volume). Zero-bullet roles will be rejected.\n" |
| "- EDUCATION RULE: Never put a professional summary, career objective, or personal statement in the " |
| "education field. Education entries MUST be degree + institution pairs only " |
| "(e.g. 'BSc in Computer Science, University of Lagos'). If you have no proper education data, " |
| "output an empty array []. Never guess or fabricate education.\n" |
| "- Summary must directly reference keywords from the JD. Must be 40+ words.\n" |
| "- Cover letter: 4-5 paragraphs, 350-500 words. This is a full-page professional document, not a short blurb. No title, no salutation. Just the letter body.\n" |
| "- Cover letter BANNED FOREVER: 'I am excited', 'I am writing to apply', 'I am confident', 'proven track record', " |
| "'I believe', 'I would be a valuable addition', 'I am impressed by', 'leverage my', 'drive business growth', " |
| "'passionate', 'enthusiastic', 'team player', 'detail-oriented', " |
| "'results-driven', 'proactive', 'self-starter', 'go-getter', " |
| "'synergy', 'leverage' (unless a tool), " |
| "'rockstar', 'ninja', 'guru', 'hardworking', 'dedicated', 'motivated', " |
| "'cutting-edge', 'state-of-the-art', 'world-class', 'best-in-class', " |
| "'holistic', 'robust' (unless describing actual infrastructure), " |
| "'think outside the box', 'low-hanging fruit', 'move the needle', 'game-changer', " |
| "'in today competitive landscape', 'dynamic environment'.\n" |
| "These are AI slop cliches that instantly disqualify you. Every word must earn its place.\n" |
| "- Match score: honest 0-100 based on keyword overlap between JD and tailored output.\n" |
| "- Never fabricate phone numbers or email addresses.\n" |
| "- No 'References available upon request'.\n" |
| "- 'keywords_hit': exactly the subset of JD keywords that appear in the tailored CV.\n" |
| ) |
|
|
| raw = _call_any(prompt, api_keys, max_tokens=3000) |
| provider = "groq" if api_keys.get("groq") else "rule-based" |
| if raw and provider == "rule-based": |
| provider = "nim" if api_keys.get("nvidia") else "rule-based" |
|
|
| if raw: |
| result = _parse_json(raw) |
| if result: |
| return { |
| "professional_summary": result.get("professional_summary", summary_from_cv), |
| "tailored_skills": result.get("tailored_skills", cv_skills), |
| "tailored_experience": result.get("tailored_experience", exp_entries_raw[:3]), |
| "cover_letter": result.get("cover_letter", ""), |
| "keywords_hit": result.get("keywords_hit", jd_keywords[:8]), |
| "match_score": int(result.get("match_score", 0)), |
| "provider": provider, |
| } |
|
|
| return _rule_based(job_title, jd_keywords, cv_skills, user_cv, company) |
|
|
|
|
| def _rule_based(job_title, jd_kw, cv_skills, user_cv, company) -> dict: |
| import hashlib |
| import re as _re |
|
|
| _STOPS = {"the","a","an","is","are","was","were","be","been","have","has","had", |
| "do","does","did","will","would","could","should","may","might","shall", |
| "to","of","in","for","on","with","at","by","from","as","into","through", |
| "during","before","after","between","out","off","over","under","then", |
| "once","here","there","when","where","why","how","all","each","every", |
| "both","few","more","most","other","some","such","no","nor","not", |
| "only","own","same","so","than","too","very","just","because","but", |
| "and","or","if","while","about","up","down","this","that","these", |
| "those","it","its","current","present","new","old","good","best", |
| "great","high","team","work","role","job","position","looking", |
| "seeking","hire","apply","join","available","including","based", |
| "within","without","across","using","various","provide","manage", |
| "support","develop","create","implement","apply","part","full", |
| "time","remote","hybrid","anywhere","global","international", |
| "nigeria","africa","london","usa","uk","canada"} |
|
|
| skills_list = [s.lower() if isinstance(s, str) else s.get("items", []) for s in (cv_skills or [])] |
| if isinstance(skills_list, list) and skills_list and isinstance(skills_list[0], list): |
| flat = [item for sublist in skills_list for item in sublist] |
| else: |
| flat = skills_list if skills_list else [] |
| flat = [s for s in flat if isinstance(s, str)] |
| matched = [k for k in jd_kw if any(k in s.lower() for s in flat)] |
| score = min(100, int(len(matched) / max(len(jd_kw), 1) * 100)) |
| top = matched[:8] or jd_kw[:5] or flat[:5] |
| unique_top = list(dict.fromkeys(top)) |
| unique_top = [w for w in unique_top if w.lower() not in _STOPS and len(w) > 1] |
| if not unique_top: |
| unique_top = ["data", "analytics", "reporting"] |
|
|
| |
| seed_str = user_cv.get("professional_summary", "") + job_title + company + str(user_cv.get("experience", [])) |
| seed = int(hashlib.md5(seed_str.encode()).hexdigest(), 16) |
| rng_seed = seed |
|
|
| def rng(): |
| nonlocal rng_seed |
| rng_seed = (rng_seed * 1103515245 + 12345) & 0x7fffffff |
| return rng_seed |
|
|
| |
| grouped = [] |
| for gname, ginfo in SKILL_TAXONOMY.items(): |
| hits = [s for s in unique_top if any(kw in s for kw in ginfo["keywords"])] |
| if hits: |
| grouped.append({ |
| "domain": gname, |
| "items": hits, |
| "description": _describe_group(gname, hits, rng()), |
| }) |
| if not grouped: |
| grouped.append({ |
| "domain": "Data & Analytics", |
| "items": unique_top[:6], |
| "description": [ |
| "Built analytical pipelines turning raw data into decision-ready dashboards.", |
| "Designed ETL processes that cut reporting latency by 60% and improved data accuracy.", |
| "Developed automated reporting systems serving 50+ stakeholders across departments.", |
| ][rng() % 3], |
| }) |
|
|
| exp_raw = user_cv.get("experience", []) |
| exp_out = [] |
| candidate_achievements = [] |
|
|
| |
| _INVALID_COMPANY = {"", "present", "none", "na", "n/a", "tbd"} |
| valid_exps = [] |
| for e in exp_raw: |
| company_raw = (e.get("company") or "").strip() |
| title_raw = e.get("title", "").strip() |
| |
| if company_raw.lower() in _INVALID_COMPANY and "," in title_raw: |
| parts = title_raw.split(",", 1) |
| if len(parts) == 2: |
| after_comma = parts[1].strip() |
| date_match = _re.search(r'\b(19|20)\d{2}\b', after_comma) |
| if date_match: |
| company_raw = after_comma[:date_match.start()].strip().rstrip(",").strip() |
| else: |
| company_raw = after_comma |
| company_raw = company_raw.strip().rstrip(",").strip() |
| |
| _MONTHS = r'(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec)' |
| company_raw = _re.sub( |
| r'\s+' + _MONTHS + r'[-\s]*(?:\d{4})?.*$', '', company_raw, flags=_re.IGNORECASE |
| ) |
| company_raw = company_raw.strip().rstrip(",").strip() |
| if not company_raw or company_raw.lower() in _INVALID_COMPANY or len(company_raw) < 2: |
| continue |
| valid = dict(e) |
| valid["company"] = company_raw |
| valid_exps.append(valid) |
|
|
| for e in valid_exps[:3]: |
| end = "Present" if e.get("current") else e.get("end_date", "") |
| achievements = e.get("achievements", []) |
| if not achievements: |
| achievements = _generate_varied_bullets(e.get("description", ""), e.get("title", ""), unique_top, rng) |
| else: |
| achievements = list(achievements) |
| for i in range(len(achievements) - 1, 0, -1): |
| j = rng() % (i + 1) |
| achievements[i], achievements[j] = achievements[j], achievements[i] |
| achievements = achievements[:7] |
|
|
| if achievements: |
| ach_text = achievements[0] |
| if len(ach_text) > 220: |
| cut = ach_text.rfind(" ", 0, 220) |
| if cut > 100: |
| ach_text = ach_text[:cut] + "..." |
| candidate_achievements.append({ |
| "company": e.get("company", ""), |
| "title": e.get("title", ""), |
| "achievement": ach_text, |
| }) |
|
|
| exp_out.append({ |
| "title": e.get("title", ""), |
| "company": e.get("company", ""), |
| "description": e.get("description", "")[:200], |
| "start_date": e.get("start_date", ""), |
| "end_date": end, |
| "current": e.get("current", False), |
| "achievements": achievements[:7], |
| }) |
|
|
| top3 = ", ".join(unique_top[:3]) |
| top2 = ", ".join(unique_top[:2]) |
|
|
| |
| best_achievement = "" |
| for e in exp_raw: |
| for a in (e.get("achievements", []) or []): |
| if any(c.isdigit() for c in a): |
| best_achievement = a[:150] |
| break |
| if best_achievement: |
| break |
|
|
| def _cap_first(s): |
| if not s: |
| return s |
| return s[0].lower() + s[1:] |
|
|
| if best_achievement: |
| ba_mid = _cap_first(best_achievement).rstrip(".").strip() |
| if len(ba_mid) > 150: |
| cut = ba_mid.rfind(" ", 0, 150) |
| if cut > 80: |
| ba_mid = ba_mid[:cut] + "..." |
| summary_templates = [ |
| f"{job_title} with proven experience applying {top3} to deliver measurable outcomes. " |
| f"Notable achievement: {ba_mid}. " |
| f"Combines technical proficiency in {top2} with a structured, methodical approach " |
| f"to problem-solving, data quality, and stakeholder communication. " |
| f"Consistently produces analysis that drives decisions and improves operational efficiency.", |
|
|
| f"Results-focused {job_title} with hands-on experience across {top3}. " |
| f"Known for {ba_mid}. " |
| f"Brings expertise in {top2} to every project, along with a commitment to data accuracy, " |
| f"clear reporting, and actionable insights that teams trust and use.", |
|
|
| f"{job_title} experienced in applying {top3} to solve complex problems and deliver impact. " |
| f"Track record includes {ba_mid}. " |
| f"Skilled in {top2} with a focused approach to turning raw data into decisions " |
| f"that improve outcomes for organisations and the people they serve.", |
| ] |
| else: |
| summary_templates = [ |
| f"{job_title} with applied experience in {top3}. Combines technical proficiency in {top2} " |
| f"with a structured approach to problem-solving, data quality, and stakeholder communication. " |
| f"Committed to delivering accurate, actionable analysis that supports better decisions.", |
| f"Results-oriented {job_title} with hands-on experience across {top3}. " |
| f"Brings expertise in {top2} along with a disciplined approach to data accuracy " |
| f"and reporting. Known for producing analysis that stakeholders trust and act on.", |
| f"{job_title} experienced in applying {top3} to solve real-world problems. " |
| f"Skilled in {top2} and committed to data-driven decision-making. " |
| f"Focuses on delivering clear, accurate insights that improve outcomes.", |
| ] |
| summary = summary_templates[rng() % len(summary_templates)] |
|
|
| |
| def _fmt_ach(ach_text): |
| text = _cap_first(ach_text).strip(".").strip() |
| if len(text) > 150: |
| |
| cut = text.rfind(" ", 0, 150) |
| if cut > 80: |
| text = text[:cut] + "..." |
| return text |
|
|
| |
| if candidate_achievements: |
| ach1 = candidate_achievements[0] |
| ach1_fmt = _fmt_ach(ach1["achievement"]) |
| co1 = ach1["company"] |
| mid_section = "" |
| if len(candidate_achievements) > 1: |
| ach2 = candidate_achievements[1] |
| ach2_fmt = _fmt_ach(ach2["achievement"]) |
| co2 = ach2["company"] |
| mid_opts = [ |
| f" At {co2}, I {ach2_fmt}. This dual experience across different contexts taught me " |
| f"how to adapt my approach without compromising on depth or accuracy. ", |
| f" My earlier work at {co2} reinforced this approach: {ach2_fmt}. " |
| f"Each role taught me that data only creates value when it changes how people decide. ", |
| f" Earlier, at {co2}, I {ach2_fmt}. " |
| f"That experience taught me to work across teams and adapt quickly to new data environments. ", |
| ] |
| mid_section = mid_opts[rng() % len(mid_opts)] |
| cover_templates = [ |
| f"Your search for a {job_title} who combines technical precision with measurable impact " |
| f"directly reflects the work I have been doing. At {co1}, I {ach1_fmt}. " |
| f"This is not a one-off \u2014 every analysis I deliver follows the same standard: " |
| f"clean data, rigorous methods, and findings that stakeholders actually use to make decisions. " |
| f"My proficiency in {top3} means I can contribute from day one, whether the team needs " |
| f"ad-hoc insights or a structured reporting framework.{mid_section}" |
| f"I would welcome the chance to discuss how my background in data analysis and reporting " |
| f"can support your team\u2019s objectives and help drive the outcomes that matter to your organization.", |
|
|
| f"Most candidates list tools they have used. Let me show you what I have built. " |
| f"At {co1}, I {ach1_fmt}. That work did not happen by accident \u2014 it came from " |
| f"a structured approach to understanding the data, cleaning it thoroughly, and presenting " |
| f"findings in a way that non-technical stakeholders could act on. " |
| f"I bring the same discipline to every project, backed by hands-on experience with {top3}.{mid_section}" |
| f"I am not looking for a role where I simply run reports. I want to build the systems " |
| f"and analyses that help your team make smarter, faster decisions. " |
| f"I would welcome a conversation about how my experience fits what you need.", |
|
|
| f"Your {job_title} role requires someone who can move beyond surface-level reporting " |
| f"into analysis that drives real decisions. That has been the focus of my career so far. " |
| f"At {co1}, I {ach1_fmt}. I did this by combining technical skills in {top3} " |
| f"with a clear focus on what decision-makers actually needed to see.{mid_section}" |
| f"I approach every analysis the same way: understand the question first, then build " |
| f"the right methodology to answer it. The result is work that gets used \u2014 not filed away. " |
| f"I would welcome the opportunity to bring this approach to your organization and contribute " |
| f"to the outcomes your team is working toward.", |
| ] |
| else: |
| cover_templates = [ |
| f"Your need for a {job_title} who can deliver analysis that actually gets used " |
| f"aligns with how I have approached every project in my career. " |
| f"My work across {top3} has focused on one thing: turning raw information into " |
| f"decisions that stick. Whether building dashboards, cleaning messy datasets, " |
| f"or designing recurring reports, I prioritise clarity, accuracy, and usefulness. " |
| f"I do not just deliver numbers \u2014 I deliver context, so stakeholders know " |
| f"what the data means and what to do next. I would welcome a conversation about " |
| f"how my experience can support the outcomes your team is driving.", |
|
|
| f"I am writing because your {job_title} role calls for someone who connects " |
| f"analytical depth with practical business judgment. My experience in {top3} " |
| f"has been built on that principle: data only matters when it changes how people decide. " |
| f"Every dashboard I have built, every dataset I have cleaned, and every report I have " |
| f"delivered was designed with that question in mind. I bring the same philosophy " |
| f"to your team \u2014 along with the technical skills and process discipline needed " |
| f"to produce work that stakeholders trust and act on. I would welcome the chance " |
| f"to discuss what I can contribute.", |
|
|
| f"Most candidates list what they know. I focus on what I have achieved. " |
| f"With expertise in {top3}, I have consistently delivered analysis that gets used — " |
| f"from dashboards that track KPIs to ad-hoc analyses that reallocate budgets. " |
| f"My approach is simple: start with the decision that needs to be made, work backward " |
| f"to the data required, and present findings in a way that is immediately actionable. " |
| f"I do not believe in reports that sit unread. Every output I produce is designed " |
| f"to inform, persuade, or enable action. I would welcome a conversation about " |
| f"how I can bring this mindset to your organization.", |
| ] |
| cover = cover_templates[rng() % len(cover_templates)] |
|
|
| return { |
| "professional_summary": summary, |
| "tailored_skills": grouped, |
| "tailored_experience": exp_out, |
| "cover_letter": cover, |
| "keywords_hit": matched[:8], |
| "match_score": score, |
| "provider": "rule-based", |
| } |
|
|
|
|
| def _describe_group(domain: str, skills: list, salt: int = 0) -> str: |
| templates = { |
| "Data Analytics & Engineering": [ |
| f"Built analytical workflows using {', '.join(skills[:3])} to process and transform data for reporting.", |
| f"Applied {', '.join(skills[:3])} to extract, clean, and model data for business intelligence and operational analytics.", |
| f"Used {', '.join(skills[:3])} to design data pipelines that reduced processing time and improved reporting accuracy.", |
| ], |
| "BI & Visualization": [ |
| f"Created dashboards and visual reports using {', '.join(skills[:3])} to track KPIs and support decision-making.", |
| f"Designed interactive visualizations and executive scorecards in {', '.join(skills[:3])} to monitor performance metrics.", |
| f"Transformed raw data into actionable dashboards using {', '.join(skills[:3])}, enabling faster stakeholder decisions.", |
| ], |
| "Cloud & Infrastructure": [ |
| f"Managed data infrastructure using {', '.join(skills[:3])} for storage, processing, and analytics.", |
| f"Deployed and maintained cloud-based data solutions using {', '.join(skills[:3])} to support scalable analytics.", |
| f"Architected data workflows on {', '.join(skills[:3])} ensuring reliable data ingestion and transformation.", |
| ], |
| "Database Management": [ |
| f"Designed and queried databases using {', '.join(skills[:3])} to support ETL and reporting pipelines.", |
| f"Managed relational and non-relational databases in {', '.join(skills[:3])} with a focus on performance and data integrity.", |
| f"Built and optimized database schemas and queries using {', '.join(skills[:3])} to support analytics workflows.", |
| ], |
| "Project Management & Methods": [ |
| f"Applied {', '.join(skills[:3])} to manage data projects and coordinate cross-functional teams.", |
| f"Led project delivery using {', '.join(skills[:3])} methodologies to ensure on-time, on-scope outcomes.", |
| f"Coordinated cross-team initiatives using {', '.join(skills[:3])}, balancing stakeholder needs with technical delivery.", |
| ], |
| "Data Collection & M&E": [ |
| f"Designed data collection tools using {', '.join(skills[:3])} and tracked indicators across programmes.", |
| f"Built M&E frameworks and digital data collection systems using {', '.join(skills[:3])} to support program reporting.", |
| f"Developed and deployed data collection instruments in {', '.join(skills[:3])}, ensuring data quality and timely reporting.", |
| ], |
| } |
| choices = templates.get(domain) |
| if choices: |
| return choices[salt % len(choices)] |
| return [f"Proficient in {', '.join(skills[:3])} for data analysis and reporting.", |
| f"Skilled in applying {', '.join(skills[:3])} to deliver data-driven insights and operational improvements.", |
| f"Experienced in using {', '.join(skills[:3])} to support decision-making and drive measurable outcomes.", |
| ][salt % 3] |
|
|
|
|
| def _generate_varied_bullets(desc: str, title: str, top_skills: list, rng_func=None) -> list: |
| if rng_func is None: |
| import random |
| rng = lambda: random.randint(0, 1000000) |
| else: |
| rng = rng_func |
|
|
| metric_pools = { |
| "records": [(f"{v}+", u) for v in [5000, 10000, 25000, 50000, 75000, 100000] for u in ["records", "data points", "entries", "transactions"]], |
| "kpis": [(f"{v}+", u) for v in [8, 10, 15, 20, 25] for u in ["KPIs", "metrics", "indicators", "performance measures"]], |
| "reports": [(f"{v}+", u) for v in [5, 8, 10, 15, 20] for u in ["reports", "dashboards", "data products", "analytical outputs"]], |
| "teams": [(f"{v}+", u) for v in [3, 5, 7, 10, 12] for u in ["cross-functional teams", "stakeholder groups", "departments", "business units"]], |
| "accuracy": [(f"{v}%", u) for v in [95, 97, 98, 99] for u in ["accuracy", "completeness", "data integrity", "quality scores"]], |
| "time": [(f"{v}x", u) for v in [2, 3, 4] for u in ["faster turnaround", "quicker reporting cycles", "speed improvements"]] + [ |
| (f"reduced by {v}%", u) for v in [20, 25, 30, 35, 40, 50] for u in ["processing time", "report generation time", "manual effort"]], |
| } |
|
|
| metric_categories = list(metric_pools.keys()) |
|
|
| def pick_metric(category): |
| pool = metric_pools[category] |
| return pool[rng() % len(pool)] |
|
|
| verb_pool = ["Extracted", "Cleaned", "Transformed", "Analysed", "Processed", "Validated", |
| "Reconciled", "Consolidated", "Standardised", "Reviewed", "Audited", "Compiled"] |
| build_verbs = ["Built", "Designed", "Developed", "Created", "Deployed", "Launched", "Implemented", "Automated"] |
| collab_verbs = ["Partnered with", "Collaborated with", "Worked with", "Coordinated with", "Advised"] |
| impact_verbs = ["Improved", "Reduced", "Increased", "Streamlined", "Optimised", "Accelerated", "Enhanced"] |
|
|
| bullets = [] |
|
|
| |
| rec_val, rec_unit = pick_metric("records") |
| skill_str = ", ".join(top_skills[:2]) |
| verb1 = verb_pool[rng() % len(verb_pool)] |
| bullets.append( |
| f"{verb1} and transformed {rec_val} {rec_unit} from multiple sources using {skill_str}, " |
| f"supporting data-driven decision-making across the organisation." |
| ) |
|
|
| |
| acc_val, acc_unit = pick_metric("accuracy") |
| verb2 = verb_pool[rng() % len(verb_pool)] |
| bullets.append( |
| f"{verb2} {rng() % 15 + 10}+ datasets and improved {acc_unit} to {acc_val} through " |
| f"standardised validation processes and data quality checks." |
| ) |
|
|
| |
| rep_val, rep_unit = pick_metric("reports") |
| verb3 = build_verbs[rng() % len(build_verbs)] |
| bullets.append( |
| f"{verb3} {rep_val} {rep_unit} for leadership and programme teams, " |
| f"enabling faster access to critical metrics." |
| ) |
|
|
| |
| team_val, team_unit = pick_metric("teams") |
| verb4 = collab_verbs[rng() % len(collab_verbs)] |
| bullets.append( |
| f"{verb4} {team_val} {team_unit} to define data requirements and translate business needs " |
| f"into analytical deliverables." |
| ) |
|
|
| |
| time_val, time_unit = pick_metric("time") |
| verb5 = impact_verbs[rng() % len(impact_verbs)] |
| bullets.append( |
| f"{verb5} {time_val} {time_unit} by introducing automated workflows and template-driven reporting." |
| ) |
|
|
| |
| verb6 = verb_pool[rng() % len(verb_pool)] |
| bullets.append( |
| f"{verb6} and documented data processes, training {rng() % 6 + 3} team members " |
| f"on best practices for data entry, validation, and reporting." |
| ) |
|
|
| |
| kpi_val, kpi_unit = pick_metric("kpis") |
| verb7 = impact_verbs[rng() % len(impact_verbs)] |
| bullets.append( |
| f"Tracked and reported {kpi_val} {kpi_unit} monthly, helping leadership " |
| f"identify trends and reallocate resources for maximum impact." |
| ) |
|
|
| |
| shuffled = list(bullets) |
| for i in range(len(shuffled) - 1, 0, -1): |
| j = rng() % (i + 1) |
| shuffled[i], shuffled[j] = shuffled[j], shuffled[i] |
|
|
| return shuffled |
|
|
|
|
| def make_cv_from_scratch( |
| raw_text: str, |
| target_jobs: list[str], |
| target_type: str = "local", |
| remote_preferences: dict = None, |
| api_keys: dict = None, |
| ) -> dict: |
| api_keys = api_keys or {} |
| remote = remote_preferences or {} |
| targets = ", ".join(target_jobs) if target_jobs else "Not specified" |
|
|
| if target_type == "international": |
| location_guidance = ( |
| "LOCATION: Use 'Remote / Anywhere' as the location. " |
| "Do NOT include Nigeria-specific address or phone format. " |
| "Include the candidate's timezone preference: " + remote.get("timezone", "flexible") + ". " |
| "If the candidate prefers remote-only work, mention this in the summary." |
| ) |
| personal_fields = ( |
| "PERSONAL INFO FIELDS:\n" |
| "- name: Full Name\n" |
| "- email: Professional email (firstname.lastname@gmail.com style)\n" |
| "- phone: Country code + number (e.g., +234 800 000 0000)\n" |
| "- location: 'City, Country' format, or 'Remote / Timezone'\n" |
| "- linkedin: Full LinkedIn URL\n" |
| "- website: Portfolio/GitHub URL if applicable, else empty string\n\n" |
| "MANDATORY: Do NOT include date of birth, age, gender, marital status, " |
| "religion, state of origin, LGA, or photo. These are illegal for recruiters " |
| "in the US, UK, Canada, and EU to request and will get the CV rejected." |
| ) |
| context_translation = ( |
| "TRANSLATE NIGERIAN CONTEXT:\n" |
| "- 'NYSC' -> 'Mandatory National Youth Service'\n" |
| "- 'WASC'/'WAEC' -> 'High School Diploma'\n" |
| "- 'N50,000,000' -> '$65,000 USD' or use percentages\n" |
| "- 'Corper' -> 'Youth Service participant'\n" |
| "- 'First Bank' -> 'First Bank of Nigeria (top-tier commercial bank)'\n" |
| "- Large Nigerian companies may be unknown internationally — add brief context\n\n" |
| "VISA/WORK RIGHTS: If the user mentions work authorization, include it clearly. " |
| "If not, the recruiter assumes sponsorship is needed." |
| ) |
| format_guidance = ( |
| "FORMAT: International CV standard (US/UK/Global).\n" |
| "- Strictly 1 page for entry-level, 2 pages max for experienced\n" |
| "- No photo, no personal details (DOB, age, gender, marital status, religion)\n" |
| "- Use globally recognized job titles: 'Software Engineer', not 'Programmer'\n" |
| "- Emphasize remote collaboration tools, timezone management, cross-cultural work\n" |
| "- Use USD or percentages for metrics\n" |
| "- Simple single-column format (ATS-friendly)\n" |
| ) |
| else: |
| location_guidance = ( |
| "LOCATION: Use a Nigerian city (Lagos, Abuja, Port Harcourt, Ibadan, Enugu) as the location. " |
| "Include Nigerian phone format (+234 8XX XXX XXXX). " |
| ) |
| personal_fields = ( |
| "PERSONAL INFO FIELDS:\n" |
| "- name: Full Name\n" |
| "- email: Professional email\n" |
| "- phone: +234 8XX XXX XXXX format\n" |
| "- location: 'City, State, Nigeria'\n" |
| "- linkedin: Full LinkedIn URL\n" |
| "- website: Portfolio URL if applicable, else empty string\n\n" |
| "For traditional/government roles: include date of birth and state of origin " |
| "in the personal_info as extra fields: 'date_of_birth', 'state_of_origin'. " |
| "For tech/multinational roles: leave these out." |
| ) |
| context_translation = "" |
| format_guidance = ( |
| "FORMAT: Nigerian CV standard.\n" |
| "- 1-2 pages\n" |
| "- Include NYSC as an experience entry if applicable\n" |
| "- Nigerian phone format (+234)\n" |
| "- Local certifications (ICAN, COREN, HSE, etc.) are highly valued — feature them\n" |
| "- Referees: Include 2-3 referees at the end (name, title, company, email, phone)\n" |
| "- No photo (unnecessary for most roles)\n" |
| "- Bold section headers, single-column, ATS-friendly\n" |
| ) |
|
|
| prompt = ( |
| "You are the world's #1 CV writer for Nigerian professionals targeting both local and global roles. " |
| "Your CVs consistently achieve interview rates above 40% because they:\n" |
| "1) Beat ATS (Applicant Tracking Systems) with perfect keyword optimization and clean formatting\n" |
| "2) Pass the 10-second human scan with F-pattern visual hierarchy\n" |
| "3) Use the FORMULA: Action Verb + Task + Result (with metrics) on EVERY bullet point\n" |
| "4) Never contain generic phrases that 90% of applicants use\n\n" |
| "## UNIQUENESS RULE ##\n" |
| "If 10 candidates with similar backgrounds apply for the same role, their CVs must feel distinct. " |
| "Lead each section with the most distinctive real achievement first. Vary sentence openings — " |
| "never start two bullets with the same verb. Choose different metrics, different examples, " |
| "different structures. Avoid any phrase that sounds like a template or fill-in-the-blank. " |
| "Every candidate has unique context; find it and feature it.\n\n" |
| "## ATS RULES (FOLLOW STRICTLY) ##\n" |
| "- Use STANDARD section headings: 'Professional Summary', 'Core Competencies', " |
| "'Professional Experience', 'Education', 'Certifications', 'Projects'\n" |
| "- Keyword mirroring: if target roles mention 'data analysis', 'project management', use those EXACT phrases\n" |
| "- No tables, columns, text boxes, graphics, or icons (ATS cannot parse them)\n" |
| "- Single-column layout throughout\n" |
| "- Skills must contain keywords the ATS looks for — they must match target roles\n\n" |
| "## THE ACHIEVEMENT FORMULA ##\n" |
| "Every bullet point in experience MUST follow this pattern: " |
| "Accomplished an objective as measured by a specific metric, by taking a specific action.\n\n" |
| "WRONG: 'Managed social media accounts.' (duty-listing)\n" |
| "WRONG: 'Responsible for customer service.' (duty-listing)\n" |
| "RIGHT: 'Grew Instagram engagement by 65% within 6 months through content optimization and audience analysis.'\n" |
| "RIGHT: 'Resolved 50+ customer inquiries daily maintaining 95% satisfaction rating.'\n" |
| "RIGHT: 'Developed a customer onboarding system that reduced processing time by 30% and improved NPS by 12 points.'\n\n" |
| "If you put 'etc.' at the end of a bullet point, delete the bullet point entirely. Every bullet must be specific.\n\n" |
| "## CV SAMPLE (use this structure and level of detail) ##\n\n" |
| "CONTACT HEADER\n" |
| "Lawrence OLADEJI\n" |
| "+234 903 881 9790 | Oladeji.lawrence@gmail.com | linkedin.com/in/lawrence-oladeji/\n\n" |
| "Professional Summary\n" |
| "Skilled data professional with a strong track record of transforming complex datasets into actionable " |
| "insights through intuitive dashboards and contributing meaningfully by helping organizations create " |
| "and maintain a Monitoring and Evaluation framework. My ability to streamline processes, improve team " |
| "efficiency, and deliver clear, data-backed results can bring immediate value to any organization " |
| "looking to enhance decision-making and operational effectiveness.\n\n" |
| "Core Competencies\n" |
| "- Data Collection & Analysis: Extracted data from 14 international and local repositories " |
| "(World Bank, WHO, NBS, DHIS2), transforming 56,000+ data points across 45 indicators\n" |
| "- Dashboard Development: Built scorecards and PM dashboards in Power BI, Tableau, and Google Sheets\n" |
| "- Statistical Analysis: Regression, correlation, predictive modeling, chi-square using Python, R, SPSS\n" |
| "- Database Management: PostgreSQL, Google BigQuery, SQL for data extraction and transformation\n\n" |
| "Professional Experience\n" |
| "Data Analyst, eHealth4everyone, May 2024 - Present\n" |
| "- Extracted data from 14 international and local data repositories and sources, including World Bank, " |
| "WHO, GHO, NBS, NPC, DHIS2, EMR and others, covering 50+ key health, climate, energy and financial indicators\n" |
| "- Replicated quarterly performance scorecards and dashboards in Google Sheets supporting C-suite decision-making\n" |
| "- Reviewed, cleaned, and transformed 56,000+ data points across 45 indicators, uploading to Google BigQuery\n" |
| "- Trained 6 team members on database design and implementation using PostgreSQL and ERDPlus\n" |
| "- Troubleshot and resolved 4 critical PM dashboard issues in Power BI and Tableau\n\n" |
| "Education\n" |
| "- Master's Degree in Mechanical Engineering, University of Ibadan, 2022/2023\n" |
| "- Bachelor's Degree in Mechanical Engineering, Federal University of Agriculture Abeokuta, " |
| "[Second Class Upper], 2011 - 2017\n\n" |
| "Certifications\n" |
| "- Microsoft Azure Fundamental, 2023\n" |
| "- Data Monitoring and Evaluation, USAID Global Health e-Learning Center, 2023\n" |
| "- Data Visualization with Power BI, LinkedIn Learning, 2021\n\n" |
| "END SAMPLE\n\n" |
| "## TARGET ROLES ##\n" |
| + targets + "\n\n" |
| + location_guidance + "\n\n" |
| + personal_fields + "\n\n" |
| + context_translation + "\n\n" |
| + format_guidance + "\n\n" |
| "## RAW USER INFORMATION ##\n" |
| + (raw_text[:6000] if raw_text else "No information provided. Create a minimal CV template.") + "\n\n" |
| "EXTRACT ALL experience entries from the raw text above. Every role mentioned with a title, company, " |
| "and dates must become a full experience entry with 4-7 achievement bullets. " |
| "Do NOT leave any experience only in the summary — create proper experience entries.\n\n" |
| "## OUTPUT FORMAT ##\n" |
| "Return ONLY valid JSON. No markdown, no explanation, no code fences. Use this exact structure:\n" |
| '{\n' |
| ' "personal_info": {\n' |
| ' "name": "Full Name",\n' |
| ' "email": "professional email or empty string",\n' |
| ' "phone": "with country code or empty string",\n' |
| ' "location": "City, Country" or "Remote / Timezone",\n' |
| ' "linkedin": "URL or empty string",\n' |
| ' "website": "URL or empty string"\n' |
| ' },\n' |
| ' "professional_summary": "3-4 sentences. NEVER start with \'I am\' or \'A highly\'. ' |
| 'Start with job title: \'Data Analyst with 4+ years...\' ' |
| 'Sentence 1: title + years + domain. Sentence 2: biggest achievement with metric. ' |
| 'Sentence 3: technical stack. Sentence 4: value you bring them.",\n' |
| ' "skills": [\n' |
| ' {\n' |
| ' "domain": "Group Name (e.g. Data Analytics & Engineering)",\n' |
| ' "items": ["Skill1", "Skill2", "Skill3"],\n' |
| ' "description": "1-2 sentences showing tool + context + metric outcome. E.g. \'Built ETL pipelines processing 56k+ data points across 45 indicators using Python and SQL.\'"\n' |
| ' },\n' |
| ' {\n' |
| ' "domain": "Next Group Name",\n' |
| ' "items": ["Skill4", "Skill5"],\n' |
| ' "description": "..."\n' |
| ' }\n' |
| ' ],\n' |
| ' "experience": [\n' |
| ' {\n' |
| ' "title": "Job Title",\n' |
| ' "company": "Company/Organization",\n' |
| ' "start_date": "Month YYYY",\n' |
| ' "end_date": "Month YYYY or Present",\n' |
| ' "current": false,\n' |
| ' "achievements": [\n' |
| ' "Accomplished a specific objective as measured by a metric, by taking an action — with specific numbers",\n' |
| ' "Built/fixed/created/led/increased/reduced/designed/implemented ...",\n' |
| ' "4-6 bullets, each using action verbs and metrics"\n' |
| ' ]\n' |
| ' }\n' |
| ' ],\n' |
| ' "education": [\n' |
| ' {\n' |
| ' "degree": "Degree Name",\n' |
| ' "institution": "University/School",\n' |
| ' "start_date": "YYYY",\n' |
| ' "end_date": "YYYY",\n' |
| ' "gpa": "optional classification or grade"\n' |
| ' }\n' |
| ' ],\n' |
| ' "certifications": ["Cert Name, Year", ...],\n' |
| ' "languages": ["Language (Proficiency)", ...],\n' |
| ' "projects": [\n' |
| ' {\n' |
| ' "name": "Project Name",\n' |
| ' "description": "2-3 sentences selling the project: what problem, what tech, what result",\n' |
| ' "technologies": ["Tech1", "Tech2"],\n' |
| ' "url": "https://..." or empty string\n' |
| ' }\n' |
| ' ],\n' |
| ' "professional_memberships": ["Member1, Org", ...],\n' |
| ' "volunteer_experience": [\n' |
| ' {\n' |
| ' "title": "Role",\n' |
| ' "company": "Organization",\n' |
| ' "start_date": "YYYY",\n' |
| ' "end_date": "YYYY",\n' |
| ' "achievements": ["achievement bullet"]\n' |
| ' }\n' |
| ' ]\n' |
| '}\n\n' |
| "## CRITICAL RULES ##\n" |
| "- Never fabricate information. If missing, use empty string or omit.\n" |
| "- CRITICAL: Do NOT invent phone numbers or email addresses. " |
| "Leave as empty string \"\" if not provided.\n" |
| "- Do NOT include 'References available upon request'.\n" |
| "- Do NOT include a photo.\n" |
| "- Every achievement must start with an action verb and contain at least one number (%, $, count, time, volume).\n" |
| "- CRITICAL: EVERY experience entry MUST have 4-7 achievement bullets. " |
| "If a role has fewer than 4 achievements in the source, you MUST reconstruct them from the information available. " |
| "Zero-bullet experience entries will be rejected.\n" |
| "- EDUCATION RULE: Never put a professional summary, career objective, or personal statement in the " |
| "education field. Education entries MUST be degree + institution pairs only " |
| "(e.g. 'BSc in Computer Science, University of Lagos'). If no proper education data exists, output empty array [].\n" |
| "- Skills must be an array of objects with 'domain', 'items', and 'description'. Each domain groups 3-6 related skills. The description must show tool + context + metric. NOT a flat list, NOT comma-separated.\n" |
| "- Experience in reverse chronological order (most recent first).\n" |
| "- Summary must NEVER start with 'I am', 'A highly', 'A dedicated', 'An experienced'. " |
| "Start directly with the job title.\n" |
| "- BANNED WORDS (never use): 'passionate', 'enthusiastic', 'team player', " |
| "'detail-oriented', 'results-driven', 'proven track record', 'I believe', 'I think', " |
| "'proactive', 'self-starter', 'go-getter', 'synergy', 'leverage' (unless a tool), " |
| "'rockstar', 'ninja', 'guru', 'hardworking', 'dedicated', 'motivated'.\n" |
| "- If the candidate mentions NYSC, include it as an experience entry with title " |
| "'Youth Service Participant (NYSC)'.\n" |
| "- For international: translate all Nigerian context — no assumption that foreign " |
| "recruiters understand Naira, NYSC, WASC, or local company prestige.\n" |
| "- Projects section: each project gets a selling description (problem → tech → result) " |
| "and a URL if the user mentioned one.\n" |
| ) |
|
|
| raw = _call_any(prompt, api_keys, max_tokens=3000) |
| provider = "rule-based" |
| if raw: |
| provider = "groq" if api_keys.get("groq") else "gemini" |
| if provider == "gemini" and api_keys.get("nvidia"): |
| provider = "nim" |
|
|
| if raw: |
| result = _parse_json(raw) |
| if result: |
| result["_generated_for"] = targets |
| result["_target_type"] = target_type |
| return result, provider |
|
|
| |
| fallback = { |
| "personal_info": {"name": "", "email": "", "phone": "", "location": "", "linkedin": "", "website": ""}, |
| "professional_summary": "", |
| "skills": [], |
| "experience": [], |
| "education": [], |
| "certifications": [], |
| "languages": [], |
| "projects": [], |
| "professional_memberships": [], |
| "_generated_for": targets, |
| "_target_type": target_type, |
| } |
|
|
| |
| name_match = re.search(r"(?:My name is |I(?:')?m |I am )([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)", raw_text[:500]) |
| if name_match: |
| fallback["personal_info"]["name"] = name_match.group(1).strip() |
| email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.]+", raw_text) |
| if email_match: |
| fallback["personal_info"]["email"] = email_match.group() |
| phone_match = re.search(r"(\+?\d[\d\s\-().]{7,15})", raw_text) |
| if phone_match: |
| fallback["personal_info"]["phone"] = phone_match.group().strip() |
|
|
| |
| skill_set = set() |
| for skill in POWER_SKILLS: |
| if skill in raw_text.lower(): |
| skill_set.add(skill.title() if len(skill) > 3 else skill.upper()) |
| if skill_set: |
| fallback["skills"] = sorted(skill_set)[:12] |
|
|
| |
| name = fallback["personal_info"]["name"] or "Candidate" |
| skill_str = ", ".join(list(skill_set)[:5]) if skill_set else targets |
| fallback["professional_summary"] = ( |
| f"{name} brings practical experience in {skill_str}, " |
| f"with a focus on delivering measurable results. " |
| f"Combines technical skills with a structured approach to problem-solving " |
| f"and a commitment to producing accurate, decision-ready work." |
| ) |
|
|
| |
| edu_keywords = ["studied", "university", "college", "degree", "bachelor", "master", "phd", "diploma", |
| "bs", "ba", "ms", "ma", "mba", "b.sc", "m.sc", "b.eng", "m.eng"] |
| sentences = re.split(r'[.!?\n]+', raw_text) |
| for s in sentences: |
| s_lower = s.lower().strip() |
| if any(kw in s_lower for kw in edu_keywords) and len(s) > 20: |
| |
| for kw in ["at ", "from ", "of "]: |
| idx = s_lower.find(kw) |
| if idx >= 0: |
| after = s[idx + len(kw):].strip().rstrip(".,") |
| if after: |
| fallback["education"].append({ |
| "degree": s.strip()[:100], |
| "institution": after[:80], |
| "start_date": "", "end_date": "", |
| }) |
| break |
| if fallback["education"]: |
| break |
|
|
| return fallback, provider |
|
|
|
|
| def parse_cv_text(raw_text: str, api_keys: dict = None) -> dict: |
| api_keys = api_keys or {} |
| prompt = ( |
| "You are a CV parser. Extract structured information from the following CV text.\n\n" |
| "Output ONLY valid JSON with this exact structure:\n" |
| '{\n' |
| ' "personal_info": {"name": "", "email": "", "phone": "", "location": "", "linkedin": "", "website": ""},\n' |
| ' "professional_summary": "",\n' |
| ' "skills": ["Skill1", "Skill2", ...],\n' |
| ' "experience": [\n' |
| ' {"title": "", "company": "", "start_date": "", "end_date": "", "current": false, "description": "", "achievements": []}\n' |
| ' ],\n' |
| ' "education": [\n' |
| ' {"degree": "", "institution": "", "start_date": "", "end_date": "", "gpa": ""}\n' |
| ' ],\n' |
| ' "certifications": ["Cert1", ...],\n' |
| ' "languages": ["Lang1", ...],\n' |
| ' "projects": [{"name": "", "description": "", "technologies": [], "url": ""}],\n' |
| ' "volunteer_experience": [],\n' |
| ' "professional_memberships": []\n' |
| '}\n\n' |
| "RULES:\n" |
| "- Extract ALL experience entries (each with title, company, dates, and achievements)\n" |
| "- Split achievements into individual bullet points\n" |
| "- Extract ALL skills mentioned, not just the first few\n" |
| "- Extract education entries with degree, institution, dates\n" |
| "- If a field is missing from the CV, use empty string or empty array\n" |
| "- Do NOT fabricate or guess information not present in the text\n" |
| "- Do NOT include 'References available upon request'\n" |
| "- Return ONLY valid JSON, no explanation\n\n" |
| "CV TEXT:\n" + raw_text[:8000] |
| ) |
|
|
| raw = _call_any(prompt, api_keys, max_tokens=3000) |
| if raw: |
| result = _parse_json(raw) |
| if result: |
| return result |
|
|
| |
| cv = { |
| "personal_info": {"name": "", "email": "", "phone": "", "location": "", "linkedin": "", "website": ""}, |
| "professional_summary": "", "skills": [], "experience": [], "education": [], |
| "certifications": [], "languages": [], "projects": [], |
| "volunteer_experience": [], "professional_memberships": [], |
| } |
| name_match = re.search(r"(?:^|\n)([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3})(?:\n|$)", raw_text[:300]) |
| if name_match: |
| cv["personal_info"]["name"] = name_match.group(1).strip() |
| email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.]+", raw_text) |
| if email_match: |
| cv["personal_info"]["email"] = email_match.group() |
| phone_match = re.search(r"(\+?\d[\d\s\-().]{7,15})", raw_text) |
| if phone_match: |
| cv["personal_info"]["phone"] = phone_match.group().strip() |
|
|
| lines = raw_text.split("\n") |
| mode = "header" |
| for line in lines: |
| u = line.strip().upper() |
| if not u: |
| continue |
| if re.match(r"^(SKILLS|CORE COMPETENCIES|TECHNICAL SKILLS)", u): |
| mode = "skills" |
| elif re.match(r"^(EXPERIENCE|WORK|PROFESSIONAL EXPERIENCE|EMPLOYMENT)", u): |
| mode = "experience" |
| elif re.match(r"^(EDUCATION|ACADEMIC)", u): |
| mode = "education" |
| elif re.match(r"^(CERTIFICATION|LICENSES|CERT)", u): |
| mode = "certs" |
| elif re.match(r"^(PROJECT|PORTFOLIO)", u): |
| mode = "projects" |
| elif mode == "skills" and not re.match(r"^(SKILLS|CORE|TECHNICAL)", u): |
| parts = re.sub(r"^[-\u2022*#]\s*", "", line.strip()).split(",") |
| cv["skills"].extend(p.strip() for p in parts if p.strip()) |
| cv["skills"] = list(dict.fromkeys(s for s in cv["skills"] if s)) |
| return cv |
|
|
|
|
| def score_job_match(job_title: str, job_description: str, user_cv: dict) -> int: |
| skills_str = " ".join(user_cv.get("skills", [])).lower() |
| jd_kw = top_keywords(job_description) |
| hits = sum(1 for k in jd_kw if k in skills_str) |
| return min(100, int(hits / max(len(jd_kw), 1) * 100)) |
|
|
|
|
| def generate_hr_email( |
| job_title: str, |
| company: str, |
| job_description: str, |
| candidate_name: str, |
| summary: str, |
| skills: list, |
| experiences: list, |
| education: list, |
| api_keys: dict, |
| target_type: str = "local", |
| ) -> str | None: |
|
|
| skills_flat = [] |
| for s in (skills or []): |
| if isinstance(s, dict): |
| skills_flat.extend(s.get("items", [])) |
| elif isinstance(s, str): |
| skills_flat.append(s) |
| skills_str = ", ".join(skills_flat[:12]) |
|
|
| exp_brief = [] |
| for e in (experiences or [])[:3]: |
| title = e.get("title", "") |
| company_e = e.get("company", "") |
| ach = e.get("achievements", []) |
| top_ach = ach[0] if ach else e.get("description", "")[:120] |
| exp_brief.append(f"- {title} @ {company_e}: {top_ach}") |
| exp_str = "\n".join(exp_brief) if exp_brief else "No experience data" |
|
|
| edu_str = "; ".join( |
| e.get("degree", e.get("title", "")) for e in (education or [])[:2] |
| ) or "Not specified" |
|
|
| tone_instruction = ( |
| "Keep the tone warm, professional, and direct. Be confident but not arrogant." |
| if target_type == "local" |
| else "Keep the tone polished and globally competitive. Emphasize adaptability and international exposure." |
| ) |
|
|
| prompt = ( |
| "You are a career communications expert. Write a short, creative, straight-to-point email " |
| "from a candidate to the hiring manager at a company they are applying to.\n\n" |
| "## RULES ##\n" |
| "- MAX 150 words. Be concise.\n" |
| "- Start with a strong, specific hook about the company or role — not a generic greeting.\n" |
| "- Immediately state who you are and the role you're applying for.\n" |
| "- Include a bullet list (2-3 bullets) of specific selling points with real project examples and numbers.\n" |
| "- No generic phrases. No 'I am writing to apply'. No 'I am excited'. No 'passionate'.\n" |
| "- Use the candidate's real experience, metrics, and tools from the data below.\n" |
| "- End with a clear, action-geared call to action (e.g. 'I would love to discuss how my experience in X can help Y achieve Z').\n" |
| "- Sign off with the candidate's name.\n" |
| "- Do NOT use placeholders or brackets. Every fact must come from the data provided.\n" |
| f"- {tone_instruction}\n" |
| "- Return ONLY the email body text. No subject line. No explanation. No JSON. No markdown.\n\n" |
| "## CANDIDATE DATA ##\n" |
| f"Name: {candidate_name}\n" |
| f"Summary: {summary[:400]}\n" |
| f"Key Skills: {skills_str}\n" |
| f"Education: {edu_str}\n" |
| f"Experience:\n{exp_str}\n\n" |
| f"## TARGET ROLE ##\n" |
| f"Title: {job_title}\n" |
| f"Company: {company}\n" |
| f"Description (keywords): {job_description[:800]}\n" |
| ) |
|
|
| return _call_any(prompt, api_keys, max_tokens=600) |
|
|