Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 44 additions & 30 deletions netlify/functions/enhanceWithAi.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { GoogleGenerativeAI } from '@google/generative-ai';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import getLLMWrapper from '../utils/getLLMWrapper.js';

const enhanceWithAi = async ({
apiKey,
provider,
professionalSummary,
education,
experience,
Expand All @@ -12,10 +15,20 @@ const enhanceWithAi = async ({
if (!apiKey) {
throw new Error('API key is required');
}
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });

const systemPrompt = `You are an expert AI resume writer specializing in creating ATS-optimized, recruiter-friendly CVs for tech professionals with career transitions and non-traditional backgrounds.
const inputValues = {
professionalSummary,
education,
experience,
projects,
skills: skills.join(', '),
profileVsJobCriteria,
};

const promptTemplate = ChatPromptTemplate.fromMessages([
[
'system',
`You are an expert AI resume writer specializing in creating ATS-optimized, recruiter-friendly CVs for tech professionals with career transitions and non-traditional backgrounds.

**Your Task:** Enhance the provided CV data by optimizing it for:
- ATS (Applicant Tracking System) compatibility
Expand All @@ -24,14 +37,6 @@ const enhanceWithAi = async ({
- Professional narrative coherence
- Keyword optimization for the specified skills

**Input Data:**
- Professional Summary: ${professionalSummary}
- Education: ${JSON.stringify(education)}
- Experience: ${experience}
- Projects: ${JSON.stringify(projects)}
- Skills: ${skills.join(', ')}
- Job Criteria: ${profileVsJobCriteria}

**Enhancement Guidelines:**
1. **Professional Summary**: Craft a compelling, recruiter-hooking 3-4 sentence summary that bridges past experience with tech aspirations, aligning with job criteria.
2. **Skills**: Extract and return the 4-5 most relevant technical skills and 3-5 key soft skills by analyzing both the ProfileVsJobCriteria field and the transferable experience. Present each skill as a bullet point.
Expand All @@ -50,9 +55,9 @@ const enhanceWithAi = async ({
- Present both technical and soft skills as bullet points.

**Required Output Format (JSON only):**
{
{{
"professionalSummary": "Enhanced 3-4 sentence professional summary connecting background to tech career goals and job requirements",
"skills": {
"skills": {{
"technical": [
"tech skill1",
"tech skill2"
Expand All @@ -61,9 +66,9 @@ const enhanceWithAi = async ({
"soft skill1",
"soft skill2"
]
},
}},
"transferableExperience": [
{
{{
"company": "Company Name",
"position": "Job Title/Role",
"startDate": "Month Year",
Expand All @@ -72,19 +77,19 @@ const enhanceWithAi = async ({
"Bullet point 1 highlighting transferable skills and relevant achievements",
"Bullet point 2 with quantified results where possible"
]
}
}}
],
"education": [
{
{{
"institution": "Name of school/bootcamp",
"program": "Degree/certificate name",
"startDate": "Month Year",
"endDate": "Month Year",
"highlights": "Key projects or relevant coursework"
}
}}
],
"projects": [
{
{{
"name": "Project name",
"description": "Brief explanation of project and economic, social impact and problems solved",
"technologiesUsed": [
Expand All @@ -93,27 +98,36 @@ const enhanceWithAi = async ({
],
"deployedLink": "URL if deployed",
"githubLink": "Repository URL"
}
}}
]
}
}}
Only return valid JSON without any additional formatting or commentary.`,
],
[
'user',
`Professional Summary: {professionalSummary}, Education: {education}, Experience: {experience}, Projects: {projects}, Skills: {skills}, Job Criteria: {profileVsJobCriteria}`,
],
]);

Only return valid JSON without any additional formatting or commentary.`;
const model = getLLMWrapper({ provider, apiKey, temperature: 0.3 });
const promptValue = await promptTemplate.invoke(inputValues);
const response = await model.invoke(promptValue);

const result = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: systemPrompt }] }],
});
if (!response.content) {
throw new Error('No response content from model');
}

const responseText = result.response.text();
const cleanedResponse = responseText
console.log(response.content);
const cleanedResponse = response.content
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '')
.trim();

try {
return JSON.parse(cleanedResponse);
} catch (parseError) {
console.error('Invalid JSON response from Gemini:', cleanedResponse);
throw new Error('AI returned invalid JSON format');
console.error(`Invalid JSON response from ${provider}:`, cleanedResponse);
throw new Error(`${provider} returned invalid JSON format`);
}
} catch (error) {
console.error('CV Enhancement Error:', error);
Expand Down
13 changes: 10 additions & 3 deletions netlify/functions/generateCv.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ const cvSchema = yup.object().shape({
apiKey: yup
.string()
.required('API key is required')
.test('is-valid-api-key', 'Invalid API key format', (value) => {
if (!value) return false;
return validateApiKey(value);
.test('is-valid-api-key', 'Invalid API key format', function (value) {
const provider = this?.parent?.provider;
if (!value || !provider) return false;
return validateApiKey(value, provider);
}),
provider: yup
.string()
.oneOf(['Gemini', 'OpenAI', 'Claude', 'TogetherAI'], 'Invalid provider')
.required('Provider is required'),
personalInfo: yup.object().shape({
fullName: yup.string().required('Full name is required'),
email: yup.string().email().required('Email is required'),
Expand Down Expand Up @@ -108,6 +113,7 @@ const generateCv = async (event) => {

const {
apiKey,
provider,
personalInfo,
professionalSummary,
transferableExperience,
Expand All @@ -133,6 +139,7 @@ const generateCv = async (event) => {
.filter(Boolean),
profileVsJobCriteria: jobcriteria,
apiKey,
provider,
};

const enhancedCV = await enhanceWithAi(aiInput);
Expand Down
46 changes: 46 additions & 0 deletions netlify/utils/getLLMWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ChatOpenAI } from '@langchain/openai';
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { ChatTogetherAI } from '@langchain/community/chat_models/togetherai';

export default function getLLMWrapper({ provider, apiKey, temperature = 0.3 }) {
switch (provider) {
case 'OpenAI': {
const model = 'gpt-4';
return new ChatOpenAI({
openAIApiKey: apiKey,
modelName: model,
temperature,
});
}

case 'Claude': {
const model = 'claude-3-sonnet-20240229';
return new ChatAnthropic({
anthropicApiKey: apiKey,
modelName: model,
temperature,
});
}

case 'Gemini': {
const model = 'gemini-1.5-flash';
return new ChatGoogleGenerativeAI({
apiKey,
model,
temperature: 0,
});
}
case 'TogetherAI': {
const model = 'mistralai/Mixtral-8x7B-Instruct-v0.1';
return new ChatTogetherAI({
togetherAIApiKey: apiKey,
modelName: model,
temperature,
});
}

default:
throw new Error(`Unsupported provider: ${provider}`);
}
}
20 changes: 18 additions & 2 deletions netlify/utils/validations.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
const validateApiKey = (key) => {
const validateApiKey = (key, provider) => {
const trimmedKey = key.trim();
return /^AIza[0-9A-Za-z-_]{35}$/.test(trimmedKey);
if (!trimmedKey || !provider) return false;
switch (provider) {
case 'Gemini':
return /^AIza[0-9A-Za-z-_]{35}$/.test(trimmedKey);

case 'OpenAI':
return /^sk-([a-zA-Z0-9-_]){20,}$/.test(trimmedKey);

case 'Claude':
return /^sk-ant-[a-zA-Z0-9]{40,}$/.test(trimmedKey);

case 'TogetherAI':
return /^[a-zA-Z0-9]{64,}$/.test(trimmedKey);

default:
return false;
}
};
export default validateApiKey;
Loading