Skip to content
Merged
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
760 changes: 432 additions & 328 deletions backend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"express-async-handler": "^1.2.0",
"express-validator": "^7.0.1",
"firebase": "^12.5.0",
"groq-sdk": "^0.4.0",
"http-errors": "^2.0.0",
"module-alias": "^2.2.3",
"mongodb": "^5.9.2",
Expand Down
128 changes: 128 additions & 0 deletions backend/src/controllers/SimilarityController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import createHttpError from "http-errors";
import Groq from "groq-sdk";

interface StudentData {
name: string;
school?: string;
fieldOfInterest?: string[];
projects?: string[];
hobbies?: string[];
skills?: string[];
companiesOfInterest?: string[];
major?: string;
classLevel?: string;
}

interface AlumniData {
name: string;
position?: string;
company?: string;
organizations?: string[];
specializations?: string[];
hobbies?: string[];
skills?: string[];
}

interface Similarity {
category: string;
description: string;
}

interface SimilarityResponse {
similarities: Similarity[];
summary: string;
}

export async function analyzeSimilarities(
student: StudentData,
alumni: AlumniData,
): Promise<SimilarityResponse> {
const groqApiKey = process.env.GROQ_API_KEY;

if (!groqApiKey) {
throw createHttpError(500, "Groq API key not configured");
}

const groq = new Groq({ apiKey: groqApiKey });

const prompt = `
You are an expert career mentor analyzing similarities between a student and an alumni.

STUDENT PROFILE:
- School: ${student.school || "Not provided"}
- Major: ${student.major || "Not provided"}
- Class Level: ${student.classLevel || "Not provided"}
- Field of Interest: ${student.fieldOfInterest?.join(", ") || "Not provided"}
- Skills: ${student.skills?.join(", ") || "Not provided"}
- Hobbies: ${student.hobbies?.join(", ") || "Not provided"}
- Projects: ${student.projects?.join(", ") || "Not provided"}
- Companies of Interest: ${student.companiesOfInterest?.join(", ") || "Not provided"}

ALUMNI PROFILE:
- Position: ${alumni.position || "Not provided"}
- Company: ${alumni.company || "Not provided"}
- Specializations: ${alumni.specializations?.join(", ") || "Not provided"}
- Skills: ${alumni.skills?.join(", ") || "Not provided"}
- Hobbies: ${alumni.hobbies?.join(", ") || "Not provided"}
- Organizations: ${alumni.organizations?.join(", ") || "Not provided"}

Provide info for the user about the key similarities between the student and alumni in one single
bullet point for each similarity. Be sure to reference the user/student as "you" and the alumni
as "this alumni". KEEP EACH DESCRIPTION SPECIFIC TO THE STUDEN AND AT MAX 20 WORDS.
Focus on the following categories of similarity:
1. Shared skills
2. Overlapping interests
3. Similar career goals
4. Shared hobbies and passions
5. Similar Educational backgrounds

Respond in the following JSON format (no markdown, pure JSON):
{
"similarities": [
{
"category": "Category Name",
"description": "Brief description of the similarity"
}
],
"summary": "A brief summary of overall similarity and potential mentorship value"
}`;

try {
const message = await groq.chat.completions.create({
model: "llama-3.1-8b-instant",
max_tokens: 1024,
response_format: { type: "json_object" },
messages: [
{
role: "user",
content: prompt,
},
],
});

const responseText = message.choices[0].message.content || "";

const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw createHttpError(500, "Failed to parse Groq response");
}

let similarities: SimilarityResponse;
try {
similarities = JSON.parse(responseText);
} catch (e) {
console.error("JSON.parse failed on Groq content:", responseText);
throw createHttpError(500, "Groq returned invalid JSON");
}
return similarities;
} catch (error) {
console.error("error in analyze similarities: ", error);
if (error instanceof createHttpError.HttpError) {
throw error;
}
throw createHttpError(
500,
`Error calling Groq API: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
84 changes: 82 additions & 2 deletions backend/src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import asyncHandler from "express-async-handler";
import createHttpError from "http-errors";
import validationErrorParser from "../util/validationErrorParser";
import Company from "../models/Company";
import mongoose from "mongoose";
import { analyzeSimilarities } from "../controllers/SimilarityController";

interface BaseUserResponse {
_id?: string;
Expand All @@ -30,7 +30,7 @@ interface StudentResponse extends BaseUserResponse {
interface AlumniResponse extends BaseUserResponse {
linkedIn?: string;
phoneNumber?: string;
company?: mongoose.Types.ObjectId;
company?: string;
shareProfile?: boolean;
position?: string;
organizations?: string[];
Expand Down Expand Up @@ -323,3 +323,83 @@ export const getOpenAlumni = asyncHandler(async (req, res, next) => {
data: users,
});
});

export const getAlumniSimilarities = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return next(createHttpError(400, validationErrorParser(errors)));
}

const { studentId } = matchedData(req, { locations: ["params"] });
const { id: alumniId } = matchedData(req, { locations: ["params"] });
if (!studentId) {
return next(createHttpError(400, "StudentId is required"));
}

const [alumniUser, studentUser] = await Promise.all([
User.findById(alumniId)
.populate({
path: "company",
model: Company,
})
.exec(),
User.findById(studentId).exec(),
]);

if (!alumniUser) {
return next(createHttpError(404, "Alumni user not found."));
}

if (!studentUser) {
return next(createHttpError(404, "Student user not found."));
}

if (alumniUser.type !== UserType.Alumni) {
return next(createHttpError(400, "User is not an alumni."));
}

if (studentUser.type !== UserType.Student) {
return next(createHttpError(400, "User is not a student."));
}

//Prepare data student and alumni for groq
const StudentData = {
name: studentUser.name,
school: studentUser.school,
fieldOfInterest: studentUser.fieldOfInterest,
projects: studentUser.projects,
hobbies: studentUser.hobbies,
skills: studentUser.skills,
companiesOfInterest: studentUser.companiesOfInterest,
major: studentUser.major,
classLevel: studentUser.classLevel,
};

const AlumniData = {
name: alumniUser.name,
position: alumniUser.position,
company: alumniUser.company,
organizations: alumniUser.organizations,
specializations: alumniUser.specializations,
hobbies: alumniUser.hobbies,
skills: alumniUser.skills,
};

const similarities = await analyzeSimilarities(StudentData, AlumniData);
res.status(200).json({
student: {
_id: studentUser._id,
name: studentUser.name,
email: studentUser.email,
},
alumni: {
_id: alumniUser._id,
name: alumniUser.name,
email: alumniUser.email,
position: alumniUser.position,
company: alumniUser.company,
},
similarities: similarities.similarities,
summary: similarities.summary,
});
});
5 changes: 3 additions & 2 deletions backend/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ const userSchema = new Schema({
trim: true,
},
company: {
type: Schema.Types.ObjectId,
type: String,
required: false,
trim: true,
},
shareProfile: {
type: Boolean,
Expand All @@ -108,7 +109,7 @@ const userSchema = new Schema({

// Add indexes for better performance
userSchema.index({ type: 1, shareProfile: 1 }); // Alumni search
userSchema.index({ company: 1 }); // Company filtering
userSchema.index({ company: "text" }); // Company filtering
userSchema.index({ name: "text" }); // Name search
userSchema.index({ position: "text" }); // Position search

Expand Down
6 changes: 6 additions & 0 deletions backend/src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ userRouter.get(
userController.getUserById,
);

userRouter.get(
"/similarities/:studentId/:id",
userValidator.getSimilaritiesValidator,
userController.getAlumniSimilarities,
);

userRouter.patch(
"/:id",
preprocessCompany,
Expand Down
1 change: 1 addition & 0 deletions backend/src/util/validateEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export default cleanEnv(process.env, {
AWS_SECRET_ACCESS_KEY: str(),
AWS_REGION: str(),
AWS_BUCKET_NAME: str(), // Required for S3 uploads and presigned URLs
GROQ_API_KEY: str(),
});
15 changes: 15 additions & 0 deletions backend/src/validators/userValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,18 @@ export const getOpenAlumniValidator = [
validatePositionQuery,
validateIndustry,
];

export const getSimilaritiesValidator = [
param("studentId")
.isString()
.withMessage("studentId must be a string.")
.trim()
.isLength({ min: 1 })
.withMessage("studentId is required."),
param("id")
.isString()
.withMessage("alumni id must be a string.")
.trim()
.isLength({ min: 1 })
.withMessage("alumni id is required."),
];
Loading