TalentSync

Home Projects TalentSync Backend Services Resume Analysis Service Comprehensive Analysis With Llm

Comprehensive Analysis with LLM

This document explains how Google Gemini 2.0 Flash LLM is used to perform deep semantic analysis of resume text, transforming unstructured or semi-structured content into rich, structured data suitable for UI rendering and further processing. This step follows the ML classification pipeline (see ML Classification Pipeline) and produces the final comprehensive analysis output.

The LLM-based analysis performs tasks that traditional NLP techniques (regex, keyword matching) cannot reliably handle: inferring proficiency levels, understanding context and nuance, reformatting experiences into professional bullet points, and filling gaps with statistically reasonable inferences when information is sparse.


Purpose and Architecture

After text extraction, cleaning, and ML-based job category prediction, the resume still requires semantic interpretation to produce a structured analysis that can drive UI components. The LLM serves as a semantic parser that:

  1. Transforms unstructured text into strictly-typed Pydantic models
  2. Infers missing information using industry knowledge and context
  3. Assigns proficiency percentages to skills based on usage patterns
  4. Generates professional bullet points from raw work experience descriptions
  5. Recommends job roles based on skill combinations and experience
  6. Handles edge cases where resume formatting is inconsistent

Sources: backend/server.py234-453


Data Model Structure

The LLM is prompted to produce JSON conforming to the ComprehensiveAnalysisData Pydantic model, which serves as the contract between the backend and frontend for rendering detailed analysis views.

Core Data Models

Model Purpose Key Constraints
SkillProficiency Quantify skill expertise percentage must be 0-100; typically 5-7 skills max
UIDetailedWorkExperienceEntry Structured work history company_and_duration format: "Company
UIProjectEntry Project portfolio technologies_used as list for filtering/searching
LanguageEntry Language proficiency Format: "Language (Level)" e.g., "English (Professional)"
EducationEntry Educational background Free-form string for flexibility

Sources: backend/server.py173-209


LLM Integration Architecture

System Configuration

The LLM is initialized globally at application startup and shared across all analysis requests:

Configuration Parameters:

  • Model: gemini-2.0-flash - Fast, balanced model for production use
  • Temperature: 0.1 - Low temperature for deterministic, structured outputs
  • Provider: Google Generative AI via LangChain integration

Sources: backend/server.py68-85


Prompt Engineering Strategy

Prompt Evolution: Three Versions

The codebase contains three distinct prompt templates, reflecting iterative refinement:

Version 1: Basic Comprehensive Analysis

The original prompt (backend/server.py234-329) required providing the predicted job category separately and focused on basic field extraction. This version was replaced by V2.

Version 2: Self-Inferring Comprehensive Analysis (Current Production)

The V2 prompt (backend/server.py373-453) is the primary production prompt. Key improvements:

  1. Self-inference of predicted_field - LLM determines job category from resume content
  2. Explicit inference rules - All inferred values marked with "(inferred)" suffix
  3. Clearer data type specifications - Precise formatting requirements for each field

Prompt Structure:

Instructions:
1. Name, Email, Contact: Populate from basic_info_json; if missing, extract from extracted_resume_text.
2. Predicted Field:
   - Examine the resume's skills, projects, job titles, and domain-specific keywords.
   - Infer the candidate's primary professional field (e.g., "Software Engineering", "Data Science"...)
   - If the field is ambiguous, choose the closest match and append "(inferred)".
3. Skills Analysis:
   - Identify the top 5-7 key technical and/or soft skills.
   - Assign percentage (0-100) based on frequency, context, and depth.
   - If the resume lists very few skills, infer common ones for the predicted field and tag with "(inferred)".
4. Recommended Roles: Suggest 3-4 job titles aligned with the inferred field, skills, and experience level.
5. Languages: Extract all languages and proficiency levels. If none are provided, add "English (Professional) (inferred)".
6. Education: List each distinct qualification. If absent, infer a typical qualification for the predicted field and tag "(inferred)".
7. Work Experience: For every significant experience, populate role, company_and_duration, and 2-5 concise bullet points.
8. Projects: For each project, extract title, technologies_used, and description. If no projects are mentioned, create 1-2 typical projects for the predicted field and mark "(inferred)".
9. General Inference Rule: Always prefer direct extraction. Any inferred value must have "(inferred)" appended.

Sources: backend/server.py373-453

Version 3: Format-Then-Analyze (Advanced Pipeline)

The format-analyze prompt (backend/server.py503-601) implements a two-phase approach:

Phase 1: Clean and reformat messy extracted text into professional structure
Phase 2: Perform comprehensive analysis on cleaned text

This version outputs both cleaned_text and analysis in a single JSON object, useful for resumes with severe formatting issues.

Sources: backend/server.py503-601


Invocation Patterns

Direct Invocation (Simple Analysis)

For straightforward resume analysis, the prompt is invoked directly via LangChain:

Typical code pattern:

# Format the prompt with input data
messages = comprehensive_analysis_prompt_v2.format_messages(
    extracted_resume_text=cleaned_text,
    basic_info_json=json.dumps(basic_info)
)

# Chain: prompt -> llm
chain = comprehensive_analysis_prompt_v2 | llm
result = chain.invoke({
    "extracted_resume_text": cleaned_text,
    "basic_info_json": json.dumps(basic_info)
})

# Parse response
json_str = result.content if hasattr(result, 'content') else str(result)
data = json.loads(json_str)
comprehensive_analysis = ComprehensiveAnalysisData(**data)

Sources: backend/server.py456-462

Graph-Based Invocation (With Tool Access)

For advanced scenarios (e.g., tailored resume generation), the LLM is integrated into a LangGraph state machine with tool access:

This pattern is used in:

In these cases, the GraphBuilder or ATSEvaluatorGraph class wraps the LLM with tool binding, allowing it to fetch external data (company research, job market trends) before generating the final structured output.

Sources: backend/app/services/resume_generator/graph.py26-70 backend/app/services/ats_evaluator/graph.py48-120


JSON Parsing and Error Handling

Response Extraction Strategy

The LLM sometimes includes markdown code fences or preamble text before the JSON. A robust extraction pipeline handles these cases:

Implementation example from ATS evaluator:

# Strip code fences
code_fence_pattern = re.compile(r"^```(json)?\n", re.IGNORECASE)
content_str = code_fence_pattern.sub("", content_str)
if content_str.endswith("```"):
    content_str = content_str[:content_str.rfind("```")]

# Extract JSON substring
if content_str.startswith("{"):
    try:
        json_obj = json.loads(content_str)
    except json.JSONDecodeError:
        end_pos = content_str.rfind("}")
        json_obj = json.loads(content_str[:end_pos + 1])
else:
    start = content_str.find("{")
    end = content_str.rfind("}") + 1
    json_obj = json.loads(content_str[start:end])

Sources: backend/app/services/ats_evaluator/graph.py153-206 backend/app/services/resume_generator/graph.py200-234

Pydantic Validation

After JSON parsing, Pydantic performs strict validation:

Common validation failures:

  • Missing required fields (name, email, predicted_field)
  • Wrong types (string instead of list, missing percentage in skills)
  • Invalid nested structures (bullet_points not a list)

Sources: backend/server.py198-209


Integration with Resume Analysis Pipeline

Complete Data Flow

The comprehensive LLM analysis is the final stage of the resume analysis pipeline:

Key Integration Points:

  1. Input: Cleaned text from NLP pipeline + basic extracted info
  2. Context: Predicted job category (optional; V2 can self-infer)
  3. Output: Rich structured data ready for UI rendering
  4. Storage: Both raw analysis JSON and cleaned_text saved to database

Sources: backend/server.py234-601


Inference and Gap Filling

A critical feature of the LLM-based approach is intelligent gap filling when resume information is sparse or missing.

Inference Rules

Field Inference Strategy Marker
predicted_field Analyze skills, job titles, project domains "(inferred)" suffix
skills_analysis If <3 skills detected, add 2-3 common skills for predicted field "(inferred)" in skill_name
languages If none mentioned, add "English (Professional)" "(inferred)" suffix
education If missing, add typical degree for field (e.g., "Bachelor's in Computer Science") "(inferred)" suffix
projects If no projects listed, create 1-2 representative projects "(inferred)" in title

Inference Example

Input resume: Minimal resume with only "Python, JavaScript" as skills and one job title "Developer"

LLM Output:

{
  "skills_analysis": [
    {"skill_name": "Python", "percentage": 75},
    {"skill_name": "JavaScript", "percentage": 70},
    {"skill_name": "Web Development (inferred)", "percentage": 65},
    {"skill_name": "Problem Solving (inferred)", "percentage": 60}
  ],
  "recommended_roles": [
    "Full Stack Developer",
    "Backend Developer",
    "Software Engineer"
  ],
  "languages": [
    {"language": "English (Professional) (inferred)"}
  ],
  "education": [
    {"education_detail": "Bachelor's in Computer Science (inferred)"}
  ],
  "projects": [
    {
      "title": "Web Application Development (inferred)",
      "technologies_used": ["Python", "JavaScript"],
      "description": "Developed web applications using Python and JavaScript frameworks."
    }
  ]
}

The "(inferred)" marker allows the frontend to optionally display these as suggestions rather than stated facts, maintaining transparency with the user.

Sources: backend/server.py424-453


Prompt Template Parameters

Input Variables

The V2 prompt accepts two input variables:

Variable Type Purpose Example
extracted_resume_text str Cleaned resume text from NLP pipeline Full resume content after spaCy lemmatization
basic_info_json str JSON string of basic extracted info {"name": "John Doe", "email": "john@example.com", "contact": "+1234567890"}

Template Invocation

# Create the prompt template
comprehensive_analysis_prompt_v2 = PromptTemplate(
    input_variables=["extracted_resume_text", "basic_info_json"],
    template=comprehensive_analysis_prompt_template_str_v2,
)

# Format with actual data
formatted_prompt = comprehensive_analysis_prompt_v2.format(
    extracted_resume_text=cleaned_resume_text,
    basic_info_json=json.dumps(basic_info)
)

# Or use with LangChain chain
chain = comprehensive_analysis_prompt_v2 | llm
result = chain.invoke({
    "extracted_resume_text": cleaned_resume_text,
    "basic_info_json": json.dumps(basic_info)
})

Sources: backend/server.py456-462


Performance Characteristics

Latency Profile

Stage Typical Duration Notes
Prompt formatting <10ms Simple string substitution
LLM inference 2-5 seconds Depends on resume length and model load
JSON parsing <50ms Including error recovery attempts
Pydantic validation <20ms Field-level validation
Total LLM stage 2-6 seconds Dominates overall analysis time

The LLM invocation is the longest step in the resume analysis pipeline, but it provides semantic understanding that would require significantly more complex rule-based systems.

Token Optimization

To manage costs and latency:

  1. Text is pre-cleaned - URLs, excessive whitespace, and artifacts removed before LLM sees it
  2. Temperature 0.1 - Reduces unnecessary token generation in output
  3. Structured output format - JSON schema constraint reduces exploratory generation
  4. gemini-2.0-flash model - Optimized for speed while maintaining quality

Sources: backend/server.py68-85


Error Handling and Fallbacks

Failure Modes

Graceful Degradation

When LLM analysis fails, the system can still return partial results:

try:
    # Attempt comprehensive analysis with LLM
    result = llm_invoke_and_parse(resume_text, basic_info)
except Exception as e:
    # Fallback to ML classification + regex extraction only
    return ComprehensiveAnalysisResponse(
        success=False,
        message=f"Partial analysis only: {str(e)}",
        data=ComprehensiveAnalysisData(
            name=basic_info.get("name"),
            email=basic_info.get("email"),
            contact=basic_info.get("contact"),
            predicted_field=ml_predicted_category,
            skills_analysis=[],  # Empty, but schema-compliant
            recommended_roles=[],
            # ... other empty fields
        )
    )

Sources: backend/server.py68-85


Relationship to Other Services

The comprehensive LLM analysis forms the foundation for several downstream services:

Service How It Uses Comprehensive Analysis Reference
Cold Mail Generator Pre-fills email templates with skills and experience 3.3
Hiring Assistant Generates answers based on candidate's actual experience and projects 3.4
ATS Evaluation Compares structured resume data against job description requirements 3.2
Tailored Resume Uses comprehensive data as baseline, then optimizes for specific job 3.5
LinkedIn Services Converts projects and achievements into LinkedIn-ready content 3.6

All these services either consume the ComprehensiveAnalysisData directly or use it as context for their own LLM prompts.

Sources: backend/server.py198-209


Summary

The comprehensive LLM analysis transforms raw resume text into a rich, structured representation suitable for:

  • UI Rendering: Direct mapping to frontend components displaying skills charts, work history timelines, and project portfolios
  • Downstream Services: Foundation for personalized cold emails, interview preparation, and ATS evaluation
  • Gap Filling: Intelligent inference when resume information is sparse or poorly formatted
  • Semantic Understanding: Captures intent and context that regex-based extraction cannot

The use of Google Gemini 2.0 Flash at low temperature (0.1) with strict Pydantic validation ensures reliable, consistent outputs while maintaining sub-6-second latency for typical resumes.

Sources: backend/server.py68-85 backend/server.py198-209 backend/server.py373-453