AI Service
The AI Service provides artificial intelligence and machine learning capabilities across the RovesUp platform. It leverages Anthropic's Claude model to analyze student resumes, match students with job opportunities, and provide personalized recommendations.
Features
- Resume/CV analysis using Claude Anthropic
- Job matching algorithms
- Skill gap analysis
- Interview preparation assistance
- Career path recommendations
- Personalized learning recommendations
Technical Implementation
Claude Anthropic Integration
We use Anthropic's Claude model to analyze student resumes and extract structured information. Claude is particularly well-suited for this task due to its strong natural language understanding capabilities and ability to follow complex instructions.
import { AnthropicClient } from '@anthropic-ai/sdk';
import { logger } from '../utils/logger';
// Anthropic configuration
const anthropic = new AnthropicClient({
apiKey: process.env.ANTHROPIC_API_KEY,
});
/**
* Analyzes a resume using Claude Anthropic
* @param resumeText The text content of the resume
* @returns Structured resume data
*/
export async function analyzeResume(resumeText: string): Promise<ResumeAnalysisResult> {
try {
// Define the system prompt for Claude
const systemPrompt = `
You are an expert resume analyzer. Your task is to extract structured information from the resume text provided.
Focus on the following sections:
1. Personal Information (name, email, phone, location)
2. Education (degrees, institutions, dates, GPA if available)
3. Work Experience (companies, positions, dates, responsibilities, achievements)
4. Skills (technical skills, soft skills, languages, proficiency levels)
5. Projects (name, description, technologies used, outcomes)
6. Certifications (name, issuing organization, date)
Format your response as a JSON object with these sections as keys.
Be precise and extract only information that is explicitly stated in the resume.
`;
// Call Claude API
const response = await anthropic.messages.create({
model: 'claude-3-sonnet-20240229',
max_tokens: 4000,
system: systemPrompt,
messages: [
{
role: 'user',
content: resumeText,
},
],
});
// Parse the response
const content = response.content[0].text;
// Extract JSON from the response
const jsonMatch = content.match(/```json\n([\s\S]*?)\n```/) ||
content.match(/{[\s\S]*}/);
if (!jsonMatch) {
throw new Error('Failed to extract JSON from Claude response');
}
const jsonString = jsonMatch[1] || jsonMatch[0];
const resumeData = JSON.parse(jsonString);
// Validate the parsed data
validateResumeData(resumeData);
// Enhance the data with additional insights
const enhancedData = await enhanceResumeData(resumeData);
return enhancedData;
} catch (error) {
logger.error('Error analyzing resume with Claude', error);
throw new Error('Resume analysis failed');
}
}
/**
* Validates the structure of the resume data
* @param data The resume data to validate
*/
function validateResumeData(data: any): void {
const requiredSections = ['personalInformation', 'education', 'workExperience', 'skills'];
for (const section of requiredSections) {
if (!data[section]) {
throw new Error(`Missing required section: ${section}`);
}
}
}
/**
* Enhances resume data with additional insights
* @param data The basic resume data
* @returns Enhanced resume data with insights
*/
async function enhanceResumeData(data: any): Promise<ResumeAnalysisResult> {
// Extract skills from work experience and projects
const extractedSkills = extractSkillsFromExperience(data);
// Merge extracted skills with explicitly stated skills
const mergedSkills = mergeSkills(data.skills, extractedSkills);
// Categorize skills
const categorizedSkills = categorizeSkills(mergedSkills);
// Generate career insights
const careerInsights = await generateCareerInsights(data);
return {
...data,
skills: categorizedSkills,
insights: careerInsights
};
}
Job Matching Algorithm
We use a combination of semantic matching and traditional keyword matching to match students with job opportunities:
/**
* Matches a student with job opportunities
* @param studentId The ID of the student
* @param filters Optional filters for job matching
* @returns Array of matched jobs with relevance scores
*/
export async function matchStudentWithJobs(
studentId: string,
filters?: JobMatchFilters
): Promise<JobMatch[]> {
try {
// Get student profile and resume data
const student = await getStudentProfile(studentId);
if (!student.resumeAnalysis) {
throw new Error('Student does not have an analyzed resume');
}
// Get available jobs
const jobs = await getAvailableJobs(filters);
// Calculate match scores for each job
const matches = await Promise.all(jobs.map(async (job) => {
const score = await calculateMatchScore(student.resumeAnalysis, job);
return {
jobId: job._id,
companyId: job.companyId,
title: job.title,
company: job.companyName,
location: job.location,
contractType: job.contractType,
score,
matchReasons: generateMatchReasons(student.resumeAnalysis, job, score)
};
}));
// Sort matches by score (descending)
return matches.sort((a, b) => b.score - a.score);
} catch (error) {
logger.error('Error matching student with jobs', error);
throw new Error('Job matching failed');
}
}
/**
* Calculates a match score between a student's resume and a job
* @param resumeAnalysis The analyzed resume data
* @param job The job posting
* @returns Match score (0-100)
*/
async function calculateMatchScore(
resumeAnalysis: ResumeAnalysisResult,
job: IJobPosting
): Promise<number> {
// Calculate skill match score (50% of total)
const skillScore = calculateSkillMatchScore(resumeAnalysis.skills, job.requirements);
// Calculate experience match score (30% of total)
const experienceScore = calculateExperienceMatchScore(resumeAnalysis.workExperience, job);
// Calculate education match score (20% of total)
const educationScore = calculateEducationMatchScore(resumeAnalysis.education, job);
// Calculate semantic similarity using Claude embeddings (additional factor)
const semanticScore = await calculateSemanticSimilarity(resumeAnalysis, job);
// Combine scores with weights
let totalScore = (skillScore * 0.5) + (experienceScore * 0.3) + (educationScore * 0.2);
// Boost score based on semantic similarity
totalScore = totalScore * (0.8 + (semanticScore * 0.2));
// Ensure score is between 0 and 100
return Math.min(100, Math.max(0, totalScore));
}
/**
* Calculates semantic similarity between resume and job using Claude embeddings
* @param resumeAnalysis The analyzed resume data
* @param job The job posting
* @returns Similarity score (0-1)
*/
async function calculateSemanticSimilarity(
resumeAnalysis: ResumeAnalysisResult,
job: IJobPosting
): Promise<number> {
try {
// Create resume text representation
const resumeText = createResumeTextRepresentation(resumeAnalysis);
// Create job text representation
const jobText = createJobTextRepresentation(job);
// Get embeddings from Claude
const resumeEmbedding = await getClaudeEmbedding(resumeText);
const jobEmbedding = await getClaudeEmbedding(jobText);
// Calculate cosine similarity
return calculateCosineSimilarity(resumeEmbedding, jobEmbedding);
} catch (error) {
logger.error('Error calculating semantic similarity', error);
return 0.5; // Default to neutral similarity on error
}
}
/**
* Gets an embedding from Claude for the given text
* @param text The text to embed
* @returns Embedding vector
*/
async function getClaudeEmbedding(text: string): Promise<number[]> {
try {
const response = await anthropic.embeddings.create({
model: 'claude-3-sonnet-20240229',
input: text,
});
return response.embedding;
} catch (error) {
logger.error('Error getting Claude embedding', error);
throw new Error('Failed to get embedding');
}
}
Skill Gap Analysis
We analyze the gap between a student's skills and the requirements of their desired job or career path:
/**
* Analyzes skill gaps for a student based on desired job or career path
* @param studentId The ID of the student
* @param targetJobId Optional target job ID
* @param careerPath Optional career path
* @returns Skill gap analysis
*/
export async function analyzeSkillGaps(
studentId: string,
targetJobId?: string,
careerPath?: string
): Promise<SkillGapAnalysis> {
try {
// Get student profile and resume data
const student = await getStudentProfile(studentId);
if (!student.resumeAnalysis) {
throw new Error('Student does not have an analyzed resume');
}
// Get target skills based on job or career path
let targetSkills: Skill[];
if (targetJobId) {
// Get skills from specific job
const job = await getJobPosting(targetJobId);
targetSkills = extractSkillsFromJobPosting(job);
} else if (careerPath) {
// Get skills from career path
targetSkills = await getSkillsForCareerPath(careerPath);
} else {
throw new Error('Either targetJobId or careerPath must be provided');
}
// Get student's current skills
const studentSkills = student.resumeAnalysis.skills;
// Identify missing skills
const missingSkills = identifyMissingSkills(studentSkills, targetSkills);
// Identify skills that need improvement
const skillsToImprove = identifySkillsToImprove(studentSkills, targetSkills);
// Generate learning recommendations
const learningRecommendations = await generateLearningRecommendations(
missingSkills,
skillsToImprove
);
return {
studentId,
targetType: targetJobId ? 'job' : 'careerPath',
targetId: targetJobId || careerPath,
missingSkills,
skillsToImprove,
learningRecommendations,
analysisDate: new Date()
};
} catch (error) {
logger.error('Error analyzing skill gaps', error);
throw new Error('Skill gap analysis failed');
}
}
Database Schema
The AI Service uses the following MongoDB schema for storing analysis results:
import mongoose, { Schema, Document } from 'mongoose';
export interface IResumeAnalysis extends Document {
studentId: mongoose.Types.ObjectId;
rawResumeText: string;
analysis: {
personalInformation: {
name?: string;
email?: string;
phone?: string;
location?: string;
};
education: Array<{
degree: string;
institution: string;
startDate?: Date;
endDate?: Date;
gpa?: number;
description?: string;
}>;
workExperience: Array<{
company: string;
position: string;
startDate?: Date;
endDate?: Date;
responsibilities: string[];
achievements: string[];
}>;
skills: Array<{
name: string;
category: string;
proficiency?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
}>;
projects: Array<{
name: string;
description: string;
technologies: string[];
url?: string;
}>;
certifications: Array<{
name: string;
issuer: string;
date?: Date;
url?: string;
}>;
};
insights: {
strengths: string[];
weaknesses: string[];
careerFit: Array<{
path: string;
score: number;
reason: string;
}>;
};
createdAt: Date;
updatedAt: Date;
}
const ResumeAnalysisSchema: Schema = new Schema({
studentId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Student' },
rawResumeText: { type: String, required: true },
analysis: {
personalInformation: {
name: { type: String },
email: { type: String },
phone: { type: String },
location: { type: String }
},
education: [{
degree: { type: String, required: true },
institution: { type: String, required: true },
startDate: { type: Date },
endDate: { type: Date },
gpa: { type: Number },
description: { type: String }
}],
workExperience: [{
company: { type: String, required: true },
position: { type: String, required: true },
startDate: { type: Date },
endDate: { type: Date },
responsibilities: [{ type: String }],
achievements: [{ type: String }]
}],
skills: [{
name: { type: String, required: true },
category: { type: String, required: true },
proficiency: {
type: String,
enum: ['beginner', 'intermediate', 'advanced', 'expert']
}
}],
projects: [{
name: { type: String, required: true },
description: { type: String, required: true },
technologies: [{ type: String }],
url: { type: String }
}],
certifications: [{
name: { type: String, required: true },
issuer: { type: String, required: true },
date: { type: Date },
url: { type: String }
}]
},
insights: {
strengths: [{ type: String }],
weaknesses: [{ type: String }],
careerFit: [{
path: { type: String, required: true },
score: { type: Number, required: true },
reason: { type: String, required: true }
}]
},
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// Pre-save hook to update the updatedAt field
ResumeAnalysisSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
export const ResumeAnalysisModel = mongoose.model<IResumeAnalysis>('ResumeAnalysis', ResumeAnalysisSchema);
API Endpoints
Resume Analysis
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/ai/analyze-resume | POST | Analyze a resume/CV | Yes (Student) |
/api/ai/resume-analysis/{id} | GET | Get resume analysis results | Yes (Student) |
Job Matching
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/ai/job-matches | GET | Get job matches for a student | Yes (Student) |
/api/ai/job-matches/explain/{jobId} | GET | Get detailed match explanation | Yes (Student) |
Skill Analysis
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/ai/skill-gaps | GET | Analyze skill gaps for a student | Yes (Student) |
/api/ai/skill-gaps/job/{jobId} | GET | Analyze skill gaps for a specific job | Yes (Student) |
/api/ai/skill-gaps/career/{careerPath} | GET | Analyze skill gaps for a career path | Yes (Student) |
Career Guidance
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/ai/career-paths | GET | Get career path recommendations | Yes (Student) |
/api/ai/interview-prep/{jobId} | GET | Get interview preparation materials | Yes (Student) |
/api/ai/learning-recommendations | GET | Get personalized learning recommendations | Yes (Student) |
Claude Anthropic Integration Details
Model Selection
We use the Claude 3 Sonnet model for most AI tasks due to its balance of performance and cost:
- Resume Analysis: Claude 3 Sonnet (20240229)
- Job Matching: Claude 3 Sonnet (20240229)
- Career Insights: Claude 3 Sonnet (20240229)
For more complex tasks requiring deeper analysis, we use Claude 3 Opus:
- Detailed Skill Gap Analysis: Claude 3 Opus (20240229)
- Interview Preparation: Claude 3 Opus (20240229)
Prompt Engineering
Our prompts for Claude are carefully designed to extract structured information from resumes. Here's an example of a system prompt for resume analysis:
You are an expert resume analyzer for a job matching platform. Your task is to extract structured information from the resume text provided.
Focus on the following sections:
1. Personal Information (name, email, phone, location)
2. Education (degrees, institutions, dates, GPA if available)
3. Work Experience (companies, positions, dates, responsibilities, achievements)
4. Skills (technical skills, soft skills, languages, proficiency levels)
5. Projects (name, description, technologies used, outcomes)
6. Certifications (name, issuing organization, date)
For each skill, categorize it into one of the following:
- Technical Skills (programming languages, tools, platforms)
- Soft Skills (communication, leadership, teamwork)
- Domain Knowledge (industry-specific knowledge)
- Languages (human languages, not programming)
Also, assess the proficiency level for each skill as:
- Beginner: Mentioned but no substantial experience
- Intermediate: Some experience or projects
- Advanced: Significant experience or achievements
- Expert: Deep expertise demonstrated through achievements
Format your response as a JSON object with these sections as keys.
Be precise and extract only information that is explicitly stated in the resume.
Error Handling and Fallbacks
We implement robust error handling for AI operations:
/**
* Safely calls Claude API with retry logic
* @param operation The operation to perform
* @param maxRetries Maximum number of retries
* @returns Operation result
*/
async function safeClaudeCall<T>(
operation: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Add exponential backoff
if (attempt > 0) {
const backoffMs = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, backoffMs));
}
return await operation();
} catch (error) {
lastError = error as Error;
logger.warn(`Claude API call failed (attempt ${attempt + 1}/${maxRetries})`, error);
// Check if error is retryable
if (!isRetryableError(error)) {
break;
}
}
}
// If we get here, all retries failed
logger.error('All Claude API call attempts failed', lastError);
throw new Error(`Claude API operation failed after ${maxRetries} attempts: ${lastError.message}`);
}
/**
* Determines if an error is retryable
* @param error The error to check
* @returns Whether the error is retryable
*/
function isRetryableError(error: any): boolean {
// Retry on rate limits and temporary server errors
if (error.status === 429 || (error.status >= 500 && error.status < 600)) {
return true;
}
// Retry on network errors
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
return true;
}
return false;
}
Security Considerations
- All API keys for Claude Anthropic are stored securely in environment variables
- Resume data is encrypted at rest
- Personal information is redacted from logs
- Access to AI analysis is restricted to the student and authorized staff
- Rate limiting is implemented to prevent abuse
- All AI operations are logged for audit purposes
Performance Optimization
To optimize performance and reduce costs:
- Caching: We cache analysis results to avoid redundant API calls
- Batch Processing: We batch multiple operations when possible
- Asynchronous Processing: Long-running analyses are processed asynchronously
- Tiered Analysis: We use a tiered approach, starting with basic analysis and only using more expensive models when necessary
/**
* Caching layer for Claude API calls
*/
class ClaudeCache {
private cache: Map<string, { result: any, timestamp: number }> = new Map();
private readonly TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
/**
* Gets a cached result or calls the operation
* @param key Cache key
* @param operation Operation to perform if cache miss
* @returns Operation result
*/
async getOrCall<T>(key: string, operation: () => Promise<T>): Promise<T> {
const cached = this.cache.get(key);
// Return cached result if valid
if (cached && Date.now() - cached.timestamp < this.TTL_MS) {
logger.debug(`Cache hit for key: ${key}`);
return cached.result;
}
// Cache miss, call operation
logger.debug(`Cache miss for key: ${key}`);
const result = await operation();
// Cache result
this.cache.set(key, {
result,
timestamp: Date.now()
});
return result;
}
/**
* Invalidates a cache entry
* @param key Cache key
*/
invalidate(key: string): void {
this.cache.delete(key);
}
/**
* Clears expired cache entries
*/
clearExpired(): void {
const now = Date.now();
for (const [key, { timestamp }] of this.cache.entries()) {
if (now - timestamp >= this.TTL_MS) {
this.cache.delete(key);
}
}
}
}
// Create singleton instance
export const claudeCache = new ClaudeCache();
Integration with Other Services
The AI Service integrates with other RovesUp services:
- Student Service: Accesses student profiles and resumes
- Company Service: Accesses job postings and requirements
- Notification Service: Sends notifications about new job matches and recommendations
Deployment
The AI Service is deployed as a Docker container in a Kubernetes cluster:
# Kubernetes deployment configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-service
namespace: rovesup
spec:
replicas: 2
selector:
matchLabels:
app: ai-service
template:
metadata:
labels:
app: ai-service
spec:
containers:
- name: ai-service
image: registry.gitlab.com/rovesup/ai-service:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: MONGODB_URI
valueFrom:
secretKeyRef:
name: ai-secrets
key: mongodb-uri
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: ai-secrets
key: anthropic-api-key
- name: STUDENT_SERVICE_URL
value: "http://student-service:3000"
- name: COMPANY_SERVICE_URL
value: "http://company-service:3000"
- name: NOTIFICATION_SERVICE_URL
value: "http://notification-service:3000"
resources:
limits:
cpu: "1000m"
memory: "1Gi"
requests:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10