justKevv commited on
Commit
40eae15
·
1 Parent(s): a5eabc7

Refactor code structure and improve organization

Browse files
api/index.py CHANGED
@@ -1 +1,29 @@
1
- from app.main import app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ # Absolute import from the project root
4
+ from app.api.routes.recommendations import router as recommendations_router
5
+
6
+ # This file is now the main entry point
7
+ app = FastAPI(
8
+ title="Student Recommendation API",
9
+ description="API for student internship recommendations",
10
+ version="1.0.0"
11
+ )
12
+
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["*"], # Allow all origins
16
+ allow_credentials=True,
17
+ allow_methods=["*"], # Allow all methods
18
+ allow_headers=["*"], # Allow all headers
19
+ )
20
+
21
+ @app.get("/", tags=["Root"])
22
+ async def read_root():
23
+ """A simple endpoint to confirm the API is running."""
24
+ return {"message": "Welcome to the Student Recommendation API!"}
25
+
26
+ # Include the routes from your recommendations module
27
+ app.include_router(recommendations_router, prefix="/api")
28
+
29
+ # The Vercel server will discover and run this 'app' object.
app/api/routes/recommendations.py CHANGED
@@ -1,15 +1,28 @@
1
- from fastapi import APIRouter
2
- from ...schemas.recommendation import ProfileRequest, RecommendationRequest
3
- from ...services import ranking
 
4
 
5
  router = APIRouter()
6
 
7
- @router.post("/predict-category", tags=["Predictions"])
8
- def predict_category(request: ProfileRequest):
9
- category = ranking.get_category_prediction(request.profile_text)
10
- return {"predicted_category": category}
 
 
 
 
 
 
11
 
12
- @router.post("/recommend-internships", tags=["Predictions"])
13
- def recommend_internships(request: RecommendationRequest):
14
- ranked_ids = ranking.get_ranked_internships(request)
15
- return {"recommendations": ranked_ids}
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ # --- Use absolute imports from 'app' ---
3
+ from app.schemas.recommendation import RecommendationRequest, RecommendationResponse, CategoryResponse
4
+ from app.services.ranking import get_ranked_internships, get_category_prediction
5
 
6
  router = APIRouter()
7
 
8
+ @router.post("/predict-category", response_model=CategoryResponse)
9
+ def predict_category(request: RecommendationRequest):
10
+ """Predicts the job category based on the user's profile text."""
11
+ try:
12
+ predicted_category = get_category_prediction(request.profile_text)
13
+ return CategoryResponse(predicted_category=predicted_category)
14
+ except Exception as e:
15
+ # Log the error for debugging
16
+ print(f"Error in /predict-category: {e}")
17
+ raise HTTPException(status_code=500, detail="An error occurred during category prediction.")
18
 
19
+ @router.post("/rank-internships", response_model=RecommendationResponse)
20
+ def rank_internships(request: RecommendationRequest):
21
+ """Ranks internships based on user profile and preferences."""
22
+ try:
23
+ ranked_ids = get_ranked_internships(request)
24
+ return RecommendationResponse(ranked_internship_ids=ranked_ids)
25
+ except Exception as e:
26
+ # Log the error for debugging
27
+ print(f"Error in /rank-internships: {e}")
28
+ raise HTTPException(status_code=500, detail="An error occurred during internship ranking.")
app/main.py DELETED
@@ -1,14 +0,0 @@
1
- from fastapi import FastAPI
2
- from .api.routes import recommendations
3
-
4
- app = FastAPI(
5
- title="Student Recommendation API",
6
- description="An API that uses machine learning to predict job categories and recommend internships.",
7
- version="1.0.0"
8
- )
9
-
10
- app.include_router(recommendations.router, prefix="/api/v1")
11
-
12
- @app.get("/", tags=["Root"])
13
- def read_root():
14
- return {"message": "Welcome to the Student Recommendation API"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/services/ranking.py CHANGED
@@ -1,127 +1,82 @@
 
 
1
  from geopy.distance import geodesic
2
  import json
3
  import os
4
  from geopy.geocoders import Nominatim
5
 
6
- from ..core.models import load_all_models, tfidf_vectorizer, le, rf_model, sentence_model
7
- from ..schemas.recommendation import RecommendationRequest
8
- from ..utils.text import clean_resume
 
9
 
10
- geolocator = Nominatim(user_agent="student_recommendation_api_v1")
11
 
 
 
12
  GEO_CACHE_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "GEO_CACHE.txt")
13
  GEO_CACHE = {}
 
14
 
15
  def load_geo_cache():
16
- global GEO_CACHE
 
17
  if os.path.exists(GEO_CACHE_FILE):
18
  with open(GEO_CACHE_FILE, "r") as f:
19
- try:
20
- GEO_CACHE = json.load(f)
21
- except json.JSONDecodeError:
22
- GEO_CACHE = {}
23
 
24
  def save_geo_cache():
25
- with open(GEO_CACHE_FILE, "w") as f:
26
- json.dump(GEO_CACHE, f)
27
-
28
- load_geo_cache()
29
 
30
  def geo_coords(city_name: str) -> tuple | None:
31
- """
32
- Geocodes a city name to (latitude, longitude).
33
- Uses an in-memory cache to avoid repeated API calls.
34
- """
35
  city_name = city_name.lower().strip()
36
- if city_name in GEO_CACHE:
37
- return GEO_CACHE[city_name]
38
  try:
39
- print(f"--- Geocoding and caching new city: {city_name} ---")
40
  location = geolocator.geocode(f"{city_name}, Indonesia")
41
-
42
  if location:
43
  coords = (location.latitude, location.longitude)
44
  GEO_CACHE[city_name] = coords
45
  save_geo_cache()
46
  return coords
47
  else:
48
- print(f"Location not found for {city_name}")
49
  GEO_CACHE[city_name] = None
50
  save_geo_cache()
51
  return None
52
- except Exception as e:
53
- print(f"Error geocoding {city_name}: {e}")
54
- return None
55
 
56
  def get_category_prediction(profile_text: str) -> str:
57
- """Processes text and predicts the job category."""
58
- if rf_model is None:
59
- load_all_models()
60
-
61
  cleaned_text = profile_text.lower()
62
  vectorized_text = tfidf_vectorizer.transform([cleaned_text])
63
  prediction_encoded = rf_model.predict(vectorized_text)[0]
64
  category = le.inverse_transform([prediction_encoded])[0]
65
  return category
66
 
67
- def get_ranked_internships(request: RecommendationRequest) -> list[int]:
68
- """Performs two-stage ranking with dynamic geocoding."""
69
-
70
- if sentence_model is None:
71
- load_all_models()
72
-
73
  profile_text_to_encode = request.profile_text
74
-
75
  if request.predicted_category:
76
  profile_text_to_encode = f"The user's predicted job category is {request.predicted_category}. Based on that, consider their profile: {request.profile_text}"
77
-
78
  profile_embedding = sentence_model.encode(profile_text_to_encode)
79
  internship_texts = [internship.internship_text for internship in request.internships]
80
-
81
- if not internship_texts:
82
- return []
83
-
84
  internship_embeddings = sentence_model.encode(internship_texts)
85
  cosine_score = sentence_model.similarity(profile_embedding, internship_embeddings)[0].tolist()
86
-
87
- print("--- FastAPI Debugging ---")
88
- print(f"Received {len(internship_texts)} internships to rank.")
89
- print(f"Calculated Cosine Scores: {cosine_score}")
90
- print("--------------------------")
91
-
92
- ranked_by_similarity = []
93
- for i, internship in enumerate(request.internships):
94
- ranked_by_similarity.append({
95
- "id": internship.id,
96
- "similarity_score": cosine_score[i],
97
- "location": internship.location,
98
- })
99
-
100
  final_ranked_list = []
101
  user_coords = geo_coords(request.preferred_location)
102
-
103
- print(user_coords, request.preferred_location)
104
-
105
-
106
  for internship in ranked_by_similarity:
107
  final_score = internship['similarity_score']
108
-
109
  if user_coords:
110
  internship_coords = geo_coords(internship['location'])
111
  if internship_coords:
112
  distance_km = geodesic(user_coords, internship_coords).kilometers
113
- if distance_km < 1:
114
- final_score += 2.0
115
- elif distance_km < 150:
116
- final_score += 0.75
117
-
118
  internship['final_score'] = final_score
119
  final_ranked_list.append(internship)
120
-
121
  final_ranked_list.sort(key=lambda x: x['final_score'], reverse=True)
122
-
123
- final_ids = [item['id'] for item in final_ranked_list]
124
-
125
- print(final_ranked_list)
126
-
127
- return final_ids
 
1
+ # app/services/ranking.py
2
+
3
  from geopy.distance import geodesic
4
  import json
5
  import os
6
  from geopy.geocoders import Nominatim
7
 
8
+ # --- Use absolute imports from 'app' ---
9
+ from app.core.models import load_all_models, tfidf_vectorizer, le, rf_model, sentence_model
10
+ from app.schemas.recommendation import RecommendationRequest
11
+ from app.utils.text import clean_resume
12
 
13
+ print("--- Loading app/services/ranking.py module ---")
14
 
15
+ # This geo-caching logic is fine
16
+ geolocator = Nominatim(user_agent="student_recommendation_api_v1")
17
  GEO_CACHE_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "GEO_CACHE.txt")
18
  GEO_CACHE = {}
19
+ _geo_cache_loaded = False
20
 
21
  def load_geo_cache():
22
+ global GEO_CACHE, _geo_cache_loaded
23
+ if _geo_cache_loaded: return
24
  if os.path.exists(GEO_CACHE_FILE):
25
  with open(GEO_CACHE_FILE, "r") as f:
26
+ try: GEO_CACHE = json.load(f)
27
+ except json.JSONDecodeError: GEO_CACHE = {}
28
+ _geo_cache_loaded = True
 
29
 
30
  def save_geo_cache():
31
+ with open(GEO_CACHE_FILE, "w") as f: json.dump(GEO_CACHE, f)
 
 
 
32
 
33
  def geo_coords(city_name: str) -> tuple | None:
34
+ load_geo_cache()
 
 
 
35
  city_name = city_name.lower().strip()
36
+ if city_name in GEO_CACHE: return GEO_CACHE[city_name]
 
37
  try:
 
38
  location = geolocator.geocode(f"{city_name}, Indonesia")
 
39
  if location:
40
  coords = (location.latitude, location.longitude)
41
  GEO_CACHE[city_name] = coords
42
  save_geo_cache()
43
  return coords
44
  else:
 
45
  GEO_CACHE[city_name] = None
46
  save_geo_cache()
47
  return None
48
+ except Exception: return None
 
 
49
 
50
  def get_category_prediction(profile_text: str) -> str:
51
+ if rf_model is None: load_all_models()
 
 
 
52
  cleaned_text = profile_text.lower()
53
  vectorized_text = tfidf_vectorizer.transform([cleaned_text])
54
  prediction_encoded = rf_model.predict(vectorized_text)[0]
55
  category = le.inverse_transform([prediction_encoded])[0]
56
  return category
57
 
58
+ def get_ranked_internships(request: RecommendationRequest) -> list[int]:
59
+ if sentence_model is None: load_all_models()
 
 
 
 
60
  profile_text_to_encode = request.profile_text
 
61
  if request.predicted_category:
62
  profile_text_to_encode = f"The user's predicted job category is {request.predicted_category}. Based on that, consider their profile: {request.profile_text}"
 
63
  profile_embedding = sentence_model.encode(profile_text_to_encode)
64
  internship_texts = [internship.internship_text for internship in request.internships]
65
+ if not internship_texts: return []
 
 
 
66
  internship_embeddings = sentence_model.encode(internship_texts)
67
  cosine_score = sentence_model.similarity(profile_embedding, internship_embeddings)[0].tolist()
68
+ ranked_by_similarity = [{"id": i.id, "similarity_score": s, "location": i.location} for i, s in zip(request.internships, cosine_score)]
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  final_ranked_list = []
70
  user_coords = geo_coords(request.preferred_location)
 
 
 
 
71
  for internship in ranked_by_similarity:
72
  final_score = internship['similarity_score']
 
73
  if user_coords:
74
  internship_coords = geo_coords(internship['location'])
75
  if internship_coords:
76
  distance_km = geodesic(user_coords, internship_coords).kilometers
77
+ if distance_km < 1: final_score += 2.0
78
+ elif distance_km < 150: final_score += 0.75
 
 
 
79
  internship['final_score'] = final_score
80
  final_ranked_list.append(internship)
 
81
  final_ranked_list.sort(key=lambda x: x['final_score'], reverse=True)
82
+ return [item['id'] for item in final_ranked_list]