From 5f56e29630ec8c776151b727b0dbd2dd74d39a42 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Fri, 26 Jun 2026 16:22:11 +0530 Subject: [PATCH] Feat: Add multi-interest support to Recommender Engine 1172 --- src/routes/main_routes.py | 17 ++++++++++++----- src/static/script.js | 4 ++-- src/templates/index.html | 2 +- src/utils/recommender.py | 25 ++++++++++++++++++++----- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/routes/main_routes.py b/src/routes/main_routes.py index dec859bd..df48b529 100644 --- a/src/routes/main_routes.py +++ b/src/routes/main_routes.py @@ -115,8 +115,8 @@ def recommend(): if not payload: return jsonify({"error": "Request body must be valid JSON."}), 400 - # Reject non-string values (e.g. null, lists, numbers) before calling .strip() - string_fields = ("skills", "level", "interest", "time") + # Reject non-string values for string fields + string_fields = ("skills", "level", "time") for field in string_fields: value = payload.get(field) if value is not None and not isinstance(value, str): @@ -124,19 +124,26 @@ def recommend(): skills = (payload.get("skills") or "").strip() level = (payload.get("level") or "").strip() - interest = (payload.get("interest") or "").strip() time_availability = (payload.get("time") or "").strip() + interest = payload.get("interest") + if isinstance(interest, str): + interest = [interest.strip()] + elif isinstance(interest, list): + interest = [i.strip() for i in interest if isinstance(i, str)] + else: + interest = [] + # Validate before running the recommendation engine errors = validate_recommendation_inputs(skills, level, interest, time_availability) if errors: # Return only the first error to keep the UI message clean return jsonify({"error": errors[0]}), 400 - if interest_has_no_projects(interest): + if any(interest_has_no_projects(i) for i in interest): return jsonify({ "projects": [], - "message": "No projects are currently available for this interest area. Please check back later." + "message": "No projects are currently available for some of your interest areas. Please check back later." }), 200 recommendations_data = get_recommendations(skills, level, interest, time_availability) diff --git a/src/static/script.js b/src/static/script.js index 9aeb0bd8..5a8d9ce7 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -565,7 +565,7 @@ updateProfileWidgets(); showFieldError("level-error", "Please select your experience level."); valid = false; } - if (!document.getElementById("interest").value) { + if (document.getElementById("interest").selectedOptions.length === 0 || document.getElementById("interest").selectedOptions[0].value === "") { showFieldError("interest-error", "Please select an area of interest."); valid = false; } @@ -827,7 +827,7 @@ updateProfileWidgets(); body: JSON.stringify({ skills: JSON.stringify(selectedSkills), level: document.getElementById("level").value, - interest: document.getElementById("interest").value, + interest: Array.from(document.getElementById("interest").selectedOptions).map(opt => opt.value), time: document.getElementById("time").value }) }) diff --git a/src/templates/index.html b/src/templates/index.html index 9ae9e04f..1ed1511d 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -983,7 +983,7 @@

Find The Project

- diff --git a/src/utils/recommender.py b/src/utils/recommender.py index 45d4fa85..a5ce0e92 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -95,7 +95,9 @@ def _project_text(project): return " ".join(parts) def _user_text(user_skills, level, interest, time_availability): - return " ".join(user_skills + [level, interest, time_availability]) + if isinstance(interest, str): + interest = [interest] + return " ".join(user_skills + [level] + interest + [time_availability]) def _tf(tokens): counts = Counter(tokens) @@ -145,6 +147,8 @@ def ml_similarity_score(project, user_skills, level, interest, time_availability return _cosine_similarity(user_vector, project_vector) def score_single_project(project, user_skills, level, interest, time_availability): + if isinstance(interest, str): + interest = [interest] TIME_RANKS = ["low", "medium", "high"] user_time = time_availability.strip().lower() @@ -171,9 +175,16 @@ def score_single_project(project, user_skills, level, interest, time_availabilit score += SCORING_WEIGHTS["level"] p_interest = project.get("interest", "").lower() - u_interest = interest.lower() - # Use partial matching for interest as well - if p_interest == u_interest or (u_interest and u_interest in p_interest) or (p_interest and p_interest in u_interest): + + # Check if ANY of the user's multiple interests match the project interest + matched_interest = False + for u_interest in interest: + u_interest = u_interest.lower() + if p_interest == u_interest or (u_interest and u_interest in p_interest) or (p_interest and p_interest in u_interest): + matched_interest = True + break + + if matched_interest: score += SCORING_WEIGHTS["interest"] if project.get("time", "").lower() == time_availability.lower(): @@ -334,6 +345,8 @@ def _get_related(recommended_ids, all_projects, cluster_data): # --------------------------------------------------------------------------- def get_recommendations(skills_string, level, interest, time_availability): + if isinstance(interest, str): + interest = [interest] user_skills = parse_skills(skills_string) all_projects = load_all_projects() scored_projects = [] @@ -400,7 +413,9 @@ def validate_recommendation_inputs(skills, level, interest, time_availability): elif level.strip().lower() not in VALID_LEVELS: errors.append("Invalid experience level. Choose Beginner, Intermediate, or Advanced.") - if not interest or not isinstance(interest, str) or not interest.strip(): + if isinstance(interest, str): + interest = [interest] + if not interest or not isinstance(interest, list) or len([i for i in interest if str(i).strip()]) == 0: errors.append("Please select an area of interest.") if not time_availability or not time_availability.strip():