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:
- Transforms unstructured text into strictly-typed Pydantic models
- Infers missing information using industry knowledge and context
- Assigns proficiency percentages to skills based on usage patterns
- Generates professional bullet points from raw work experience descriptions
- Recommends job roles based on skill combinations and experience
- 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:
- Self-inference of
predicted_field- LLM determines job category from resume content - Explicit inference rules - All inferred values marked with "(inferred)" suffix
- 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:
- Tailored Resume Service (backend/app/services/resume_generator/graph.py72-234)
- ATS Evaluation Service (backend/app/services/ats_evaluator/graph.py48-120)
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:
- Input: Cleaned text from NLP pipeline + basic extracted info
- Context: Predicted job category (optional; V2 can self-infer)
- Output: Rich structured data ready for UI rendering
- 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:
- Text is pre-cleaned - URLs, excessive whitespace, and artifacts removed before LLM sees it
- Temperature 0.1 - Reduces unnecessary token generation in output
- Structured output format - JSON schema constraint reduces exploratory generation
- 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