Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
96 changes: 73 additions & 23 deletions netlify/functions/enhanceWithAi.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { GoogleGenerativeAI } from '@google/generative-ai';
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate } from '@langchain/core/prompts';

const enhanceWithAi = async ({
apiKey,
provider,
professionalSummary,
education,
experience,
Expand All @@ -12,10 +17,40 @@ 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' });
let model;
if (provider === 'Gemini') {
const genAI = new GoogleGenerativeAI(apiKey);
model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
} else if (provider === 'OpenAI') {
console.log('API Key:', apiKey);
model = new ChatOpenAI({
openAIApiKey: apiKey,
modelName: 'gpt-4',
temperature: 0.3,
});
} else if (provider === 'Claude') {
model = new ChatAnthropic({
anthropicApiKey: apiKey,
modelName: 'claude-3-sonnet-20240229',
temperature: 0.3,
});
} else {
throw new Error('Unrecognised AI provider');
}

const inputValues = {
professionalSummary,
education,
experience,
projects,
skills: skills.join(', '),
profileVsJobCriteria,
};

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 systemPrompt = 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 @@ -26,10 +61,10 @@ const enhanceWithAi = async ({

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

**Enhancement Guidelines:**
Expand All @@ -50,9 +85,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 +96,9 @@ const enhanceWithAi = async ({
"soft skill1",
"soft skill2"
]
},
}},
"transferableExperience": [
{
{{
"company": "Company Name",
"position": "Job Title/Role",
"startDate": "Month Year",
Expand All @@ -72,19 +107,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,17 +128,32 @@ 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.join(', ')}, Job Criteria: ${profileVsJobCriteria}`,
],
]);

Only return valid JSON without any additional formatting or commentary.`;
let responseText;

const result = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: systemPrompt }] }],
});
if (provider === 'Gemini') {
const formattedPrompt = await systemPrompt.format(inputValues);
const result = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: formattedPrompt }] }],
});

responseText = result.response.text();
} else {
const chain = systemPrompt.pipe(model);
const result = await chain.invoke(inputValues);
responseText = (result as { content: string }).content;
}

const responseText = result.response.text();
const cleanedResponse = responseText
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '')
Expand All @@ -112,8 +162,8 @@ Only return valid JSON without any additional formatting or commentary.`;
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?.options?.context?.provider;
if (!value || !provider) return false;
return validateApiKey(value, provider);
}),
provider: yup
.string()
.oneOf(['Gemini', 'OpenAI', 'Claude'], '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
17 changes: 15 additions & 2 deletions netlify/utils/validations.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
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);

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