S0L009/Luna-GNN-Scorer-InferenceAPI
0
1"""Module for suggesting plans to the user using GNN information."""2 3 4import torch5import requests6import torch.nn.functional as F7import numpy as np8from typing import Literal, Dict9from get_Neo4jdata import get_vertex_data, get_inputs10from tqdm import tqdm11import json12import sys13from datetime import datetime14from dotenv import load_dotenv15import os16load_dotenv()17URL = os.getenv("URL")18 19# URL = "http://127.0.0.1:8000/"20 21class Metric:22 def __init__(self, name):23 self.name = name24 25 def __call__(self, *args, **kwargs):26 return self.evaluate(*args, **kwargs)27 28 def __repr__(self):29 return self.name30 31 def __str__(self):32 return self.name33 34 def _metricfy(self, other):35 if isinstance(other, Metric):36 return other37 else:38 obj = Metric(other)39 obj.evaluate = lambda *args, **kwargs: other40 return obj41 42 def __add__(self, other):43 obj = Metric(f"({self} + {other})")44 metric_other = self._metricfy(other)45 obj.parent_self = self46 obj.parent_other = other47 obj.evaluate = lambda *args, **kwargs: (48 self.evaluate(*args, **kwargs) +49 metric_other.evaluate(*args, **kwargs)50 )51 return obj52 53 def __radd__(self, other):54 obj = Metric(f"({other} + {self})")55 metric_other = self._metricfy(other)56 obj.parent_self = self57 obj.parent_other = other58 obj.evaluate = lambda *args, **kwargs: (59 metric_other.evaluate(*args, **kwargs) +60 self.evaluate(*args, **kwargs)61 )62 return obj63 64 def __sub__(self, other):65 obj = Metric(f"({self} - {other})")66 metric_other = self._metricfy(other)67 obj.parent_self = self68 obj.parent_other = other69 obj.evaluate = lambda *args, **kwargs: (70 self.evaluate(*args, **kwargs) -71 metric_other.evaluate(*args, **kwargs)72 )73 return obj74 75 def __rsub__(self, other):76 obj = Metric(f"({other} - {self})")77 metric_other = self._metricfy(other)78 obj.parent_self = self79 obj.parent_other = other80 obj.evaluate = lambda *args, **kwargs: (81 metric_other.evaluate(*args, **kwargs) -82 self.evaluate(*args, **kwargs)83 )84 return obj85 86 def __mul__(self, other):87 obj = Metric(f"({self} * {other})")88 metric_other = self._metricfy(other)89 obj.parent_self = self90 obj.parent_other = other91 obj.evaluate = lambda *args, **kwargs: (92 self.evaluate(*args, **kwargs) *93 metric_other.evaluate(*args, **kwargs)94 )95 return obj96 97 def __rmul__(self, other):98 obj = Metric(f"({other} * {self})")99 metric_other = self._metricfy(other)100 obj.parent_self = self101 obj.parent_other = other102 obj.evaluate = lambda *args, **kwargs: (103 metric_other.evaluate(*args, **kwargs) *104 self.evaluate(*args, **kwargs)105 )106 return obj107 108 def __truediv__(self, other):109 obj = Metric(f"({self} / {other})")110 metric_other = self._metricfy(other)111 obj.parent_self = self112 obj.parent_other = other113 obj.evaluate = lambda *args, **kwargs: (114 self.evaluate(*args, **kwargs) /115 metric_other.evaluate(*args, **kwargs)116 )117 return obj118 119 def __rtruediv__(self, other):120 obj = Metric(f"({other} / {self})")121 metric_other = self._metricfy(other)122 obj.parent_self = self123 obj.parent_other = other124 obj.evaluate = lambda *args, **kwargs: (125 metric_other.evaluate(*args, **kwargs) /126 self.evaluate(*args, **kwargs)127 )128 return obj129 130 def __pow__(self, other):131 obj = Metric(f"({self} ^ {other})")132 metric_other = self._metricfy(other)133 obj.parent_self = self134 obj.parent_other = other135 obj.evaluate = lambda *args, **kwargs: (136 self.evaluate(*args, **kwargs) **137 metric_other.evaluate(*args, **kwargs)138 )139 return obj140 141 def __rpow__(self, other):142 obj = Metric(f"({other} ^ {self})")143 metric_other = self._metricfy(other)144 obj.parent_self = self145 obj.parent_other = other146 obj.evaluate = lambda *args, **kwargs: (147 metric_other.evaluate(*args, **kwargs) **148 self.evaluate(*args, **kwargs)149 )150 return obj151 152 def __neg__(self):153 obj = Metric(f"(-{self})")154 obj.parent_self = self155 obj.evaluate = lambda *args, **kwargs: (156 -self.evaluate(*args, **kwargs)157 )158 return obj159 160 def __abs__(self):161 obj = Metric(f"abs({self})")162 obj.parent_self = self163 obj.evaluate = lambda *args, **kwargs: (164 abs(self.evaluate(*args, **kwargs))165 )166 return obj167 168 def evaluate(self, *args, **kwargs):169 """170 Evaluate the performance of the metric using the provided arguments.171 172 This method should be overridden by subclasses to implement specific173 evaluation logic for the metric.174 175 Parameters176 ----------177 *args178 Positional arguments to be passed to the metric.179 **kwargs180 Keyword arguments to be passed to the metric.181 182 Returns183 -------184 float185 The evaluation result.186 """187 ...188 189 190class UsersMetric(Metric):191 def __init__(self, name):192 super().__init__(name)193 194 def evaluate(195 self,196 user_id: str,197 venue_id: str,198 target_user_ids: list[str],199 vertex_metadata : Dict,200 scores : list = [],201 *args,202 **kwargs203 ):204 """205 Evaluate the UsersMetric using the provided arguments.206 207 Parameters208 ----------209 user_id : str210 The ID of the user to evaluate.211 venue_id : str212 The ID of the venue to evaluate.213 target_user_ids : list[str]214 List of target user IDs to evaluate against.215 *args216 Additional positional arguments to be passed to the metric.217 **kwargs218 Additional keyword arguments to be passed to the metric.219 220 Returns221 -------222 np.ndarray[float]223 The evaluation result.224 """225 ...226 227 228class UserUserCompatibility(UsersMetric):229 def __init__(self, name = 'U'):230 super().__init__(name)231 232 def evaluate(233 self,234 user_id: str,235 venue_id: str,236 target_user_ids: list[str],237 vertex_metadata : Dict,238 scores : list = [],239 *args,240 **kwargs241 ):242 user_score = scores[0]243 target_user_scores = scores[1:]244 compatibility_scores = [245 (user_score + score) / 2 246 for score in tqdm(target_user_scores, desc="Calculating compatibility scores: ")247 ]248 249 print(f"compatibility_scores: {compatibility_scores}")250 return np.array(compatibility_scores) 251 252 253class UserVenueCompatibility(UsersMetric):254 def __init__(self, name = 'V'):255 super().__init__(name)256 257 def evaluate(258 self,259 user_id: str,260 venue_id: str,261 target_user_ids: list[str],262 vertex_metadata : Dict,263 scores : list = [],264 *args,265 **kwargs266 ):267 print(f"\nuservenue scores: {scores[1:]}")268 return np.array(scores[1:])269 270# ๐ด271class UserMutuality(UsersMetric):272 def __init__(self, name = 'M'):273 super().__init__(name)274 275 def evaluate(276 self,277 user_id: str,278 venue_id: str,279 target_user_ids: list[str],280 vertex_metadata : Dict,281 scores : list = [],282 *args,283 **kwargs284 ):285 # hav to get mutual friends of the user and target_user286 user_mutuality = np.random.randint(2, size=len(target_user_ids))287 print(f"\nuser mutuality score {user_mutuality}")288 return user_mutuality289 290 291class BusinessCompatibility(UsersMetric):292 def __init__(self, name = 'B'):293 super().__init__(name)294 295 def evaluate(296 self,297 user_id: str,298 venue_id: str,299 target_user_ids: list[str],300 vertex_metadata : Dict,301 scores : list = [],302 *args,303 **kwargs304 ):305 user_occupation = vertex_metadata[user_id].get("occupation", "").lower()306 user_school = vertex_metadata[user_id].get("school", "").lower()307 308 print(f"\nuser occupation: {user_occupation}, user school: {user_school}")309 310 BusinessCompatibility_mask = []311 for target_user in tqdm(target_user_ids, desc="Calculating business compatibility scores: "):312 target_school = vertex_metadata[target_user].get("school", "").lower()313 target_occupation = vertex_metadata[target_user].get("occupation", "").lower()314 315 score = 0316 if user_school and target_school and user_school == target_school:317 score += 0.5318 if user_occupation and target_occupation and user_occupation == target_occupation:319 score += 0.5320 321 BusinessCompatibility_mask.append(score)322 323 print(f"business compatibility score {BusinessCompatibility_mask}")324 return np.array(BusinessCompatibility_mask)325 326 327class LunaScore(UsersMetric):328 def __init__(self, name = 'L'):329 super().__init__(name)330 331 def evaluate(332 self,333 user_id: str,334 venue_id: str,335 target_user_ids: list[str],336 vertex_metadata : Dict,337 scores : list = [],338 *args,339 **kwargs340 ):341 342 target_user_lunaScores = [343 vertex_metadata[target_user].get("lunaScore", 5)344 for target_user in tqdm(target_user_ids, desc="Calculating luna scores: ")345 ]346 print(f"\ntarget_user_lunaScores: {target_user_lunaScores}")347 return np.array(target_user_lunaScores)348 349 # user_lunaScore = vertex_metadata[user_id].get("lunaScore", 5)350 # higher_lunaScores = list(range(user_lunaScore + 1, 11))351 # if user_lunaScore != 10:352 # logits = [-abs(user_lunaScore - b) for b in higher_lunaScores]353 # exp_logits = np.exp(logits)354 # probs = exp_logits / np.sum(exp_logits)355 # higher_lunaScore = np.random.choice(higher_lunaScores, p=probs)356 # else:357 # higher_lunaScore = 10358 359 # target_user_lunaScore_mask = [360 # 1 if vertex_metadata[target_user].get("lunaScore", 5) == user_lunaScore or 361 # vertex_metadata[target_user].get("lunaScore", 5) == higher_lunaScore else 0362 # for target_user in target_user_ids363 # ]364 # print(f"lunaScore compatibility score {target_user_lunaScore_mask}")365 # return np.array(target_user_lunaScore_mask)366 367# ๐ด368class RepetitionFilter(UsersMetric):369 def __init__(self, name = 'R'):370 super().__init__(name)371 372 def evaluate(373 self,374 user_id: str,375 venue_id: str,376 target_user_ids: list[str],377 vertex_metadata : Dict,378 scores : list = [],379 *args,380 **kwargs381 ): 382 repetition_score = np.random.randint(2, size=len(target_user_ids))383 print(f"\nrepetition score {repetition_score}")384 return repetition_score385 386 387class AgeFilter(UsersMetric):388 def __init__(389 self,390 name='A',391 soft=False,392 threshold=2,393 decay_rate=0.1394 ):395 super().__init__(name)396 self.soft = soft397 self.threshold = threshold # acceptable age range398 self.decay_rate = decay_rate # used when soft=False399 self.current_date = datetime.now()400 401 def getAge(402 self,403 birth_date : datetime, 404 ):405 userAge = self.current_date.year - birth_date.year406 407 # Remove 1 year if the birthday hasn't occurred yet this year408 if (self.current_date.month, self.current_date.day) < (birth_date.month, birth_date.day):409 userAge -= 1410 return userAge411 412 def evaluate(413 self,414 user_id: str,415 venue_id: str,416 target_user_ids: list[str],417 vertex_metadata: Dict,418 scores: list = [],419 *args,420 **kwargs421 ):422 # default Age -> "25" as of 2025423 default_birthday = "2000-04-12T00:00:00+00:00"424 425 user_age = self.getAge(426 birth_date=datetime.fromisoformat(427 vertex_metadata.get(user_id, {}).get("birthday", default_birthday)428 )429 )430 431 age_masks = []432 for target_user in tqdm(target_user_ids, desc="Calculating age masks: "):433 434 # Get the target user's birthday from vertex_metadata and calculate the age difference435 target_age = self.getAge(436 birth_date=datetime.fromisoformat(437 vertex_metadata.get(target_user, {}).get("birthday", default_birthday) # Use default birthday if not found438 )439 )440 age_diff = abs(user_age - target_age)441 442 if self.soft:443 score = 1 if age_diff <= self.threshold else 0444 else:445 if age_diff <= self.threshold:446 score = 1447 else:448 score = np.exp(-self.decay_rate * (age_diff - self.threshold))449 450 age_masks.append(score)451 452 print(f"\nage compatibility score {age_masks}")453 return np.array(age_masks)454 455class ExistingFilter(UsersMetric):456 def __init__(self, name = 'A'):457 super().__init__(name)458 459 def evaluate(460 self,461 user_id: str,462 venue_id: str,463 target_user_ids: list[str],464 vertex_metadata : Dict,465 scores : list = [],466 *args,467 **kwargs468 ):469 # Get the existing friends of the user470 existing_friends = set(vertex_metadata["existing-Friends"].get(user_id, []))471 print(f"\nuser-existing friends: {existing_friends}")472 473 # Check if the target user is already a friend474 existingFilter_mask = [475 1 if target_user in existing_friends else 0476 for target_user in tqdm(target_user_ids, desc="Calculating existing filter masks: ")477 ]478 print(f"existing filter mask {existingFilter_mask}")479 return np.array(existingFilter_mask)480 481 482class OppositeGenderFilter(UsersMetric):483 def __init__(self, name = 'G'):484 super().__init__(name)485 486 def evaluate(487 self,488 user_id: str,489 venue_id: str,490 target_user_ids: list[str],491 vertex_metadata : Dict,492 scores : list = [],493 *args,494 **kwargs495 ):496 user_gender = vertex_metadata[user_id]["gender"].lower()497 498 opposite_gender_mask = [499 1 if vertex_metadata[target_user]["gender"].lower() != user_gender else 0500 for target_user in tqdm(target_user_ids, desc="Calculating Opposite Gender masks: ")501 ]502 503 print(f"\nopposite gender compatibility score {opposite_gender_mask}")504 return np.array(opposite_gender_mask)505 506 507def make_plan(508 user_id: str,509 venue_id: str,510 target_user_ids: list[str],511 vertices_metadata : Dict,512 scores : list,513 plan_type: Literal['new', 'existing', 'business', 'mixed'],514 temperature: float = 0.1,515): 516 517 try:518 scores_map = {519 'mutuals': {520 'score': (521 2 * UserUserCompatibility() +522 2 * UserVenueCompatibility() +523 3 * LunaScore() +524 2 * UserMutuality() +525 BusinessCompatibility()526 ),527 'filter': (528 RepetitionFilter() *529 AgeFilter(soft=False) *530 (1 - ExistingFilter())531 )532 },533 'friends': {534 'score': (535 2 * UserUserCompatibility() +536 2 * UserVenueCompatibility() +537 LunaScore()538 ),539 'filter': (540 RepetitionFilter() *541 ExistingFilter()542 )543 },544 'networking': {545 'score': (546 UserUserCompatibility() +547 UserVenueCompatibility() +548 LunaScore() +549 2 * BusinessCompatibility()550 ),551 'filter': (552 RepetitionFilter() *553 AgeFilter(soft=True) *554 (1 - ExistingFilter())555 )556 },557 'compatibility': { # different gender558 'score': (559 2 * UserUserCompatibility() +560 UserVenueCompatibility() +561 3 * LunaScore() +562 0.5 * UserMutuality()563 ),564 'filter': (565 RepetitionFilter() *566 AgeFilter(soft=False) *567 OppositeGenderFilter()568 )569 },570 'similarity': { # same gender571 'score': (572 2 * UserUserCompatibility() +573 UserVenueCompatibility() +574 3 * LunaScore() +575 0.5 * UserMutuality()576 ),577 'filter': (578 RepetitionFilter() *579 AgeFilter(soft=False) *580 (1 - OppositeGenderFilter())581 )582 },583 }584 plan_score_bundle = scores_map[plan_type]585 586 scores = plan_score_bundle['score'].evaluate(587 user_id=user_id, venue_id=venue_id, target_user_ids=target_user_ids, 588 vertex_metadata=vertices_metadata, scores=scores589 )590 filters = plan_score_bundle['filter'].evaluate(591 user_id=user_id, venue_id=venue_id, target_user_ids=target_user_ids, 592 vertex_metadata=vertices_metadata, scores=scores593 )594 scores *= filters595 logits = scores / temperature596 probs = F.softmax(torch.tensor(logits), dim=0).numpy()597 sorted_indices = np.argsort(probs)[::-1]598 sorted_target_users = [target_user_ids[i] for i in sorted_indices]599 600 # giving the users in descending order of their scores, top_k in post-processing601 return {602 "status" : True,603 "data" : sorted_target_users,604 "message" : "Plan made successfully"605 }606 607 # target_user = np.random.choice(target_user_ids, p=probs)608 # Sort target_user_ids based on probs in descending order609 # return target_user610 except Exception as e:611 return {612 "status" : False,613 "data" : None,614 "message" : f"Error making plan: {e}"615 }616 617 618 619""" 620# objective = 2 * LunaScore() + 2 * UserMutuality() + 1.5 * BusinessCompatibility()621# objective *= RepetitionFilter() * AgeFilter(soft=True)622 623# print(objective)624# print(objective.evaluate('1', '2', ['3', '4']))625 626# # def planmaker(user_id, venue_id, target_user_ids, objective):627 628# print(np.array([1, 2, 3]) ** np.array([1, 2, 3]))629 630# print(f'plan: {_make_plan("1", "2", ["3", "4"], "mutuals")}')631 632 """633 634 635if __name__ == '__main__':636 637 def Outbound_Inbound_API(userId: str, endpoint: str, URL: str):638 """639 Calls the appropriate endpoint to get outbound or inbound connections for a given userId.640 """641 Outbound = "relationships/connected/get-all-outbound-connections"642 Inbound = "relationships/connected/get-all-inbound-connections"643 Endpoint = {"outbound": Outbound, "inbound": Inbound}644 645 return requests.get(f"{URL}{Endpoint[endpoint]}/{userId}").json()646 647 def getStatusApprovedUsers(data):648 """649 Extracts userIds of connections with 'accepted' relationship status.650 """651 status_approved_users_ids = []652 for user in data:653 if user["relationship"]["status"] == "accepted":654 status_approved_users_ids.append(user["user"]["userId"])655 return status_approved_users_ids656 657 # Dictionary to store metadata of all users and location658 vertices_metadata = dict()659 660 # Reading inputs (from json. should be replaced with API)661 inputs = get_inputs()662 user_id = inputs["user_id"]663 location_id = inputs["location_id"]664 665 # This API retrieves all users who are within 3 degrees of separation from the given user (also returns their associated metadata).666 target_ids = requests.get(667 f"{URL}relationships/connected/get-all-1-2-3-degree-connections/{user_id}"668 ).json()669 670 # Extract userIds and populate metadata671 for idx in tqdm(range(len(target_ids)), desc="Collecting metadata for target users:", unit="user"):672 data = target_ids[idx][0] # Accessing inner list673 vertices_metadata[data["userId"]] = data674 target_ids[idx] = data["userId"] # Replace list with just the userId675 676 # Fetch metadata for the main user677 for usrId in tqdm([user_id], desc="Collecting metadata for main User"):678 try:679 data = get_vertex_data(id=usrId, type="user", URL=URL)680 if data["status"]:681 vertices_metadata[usrId] = data["data"]682 else:683 print(f"Error fetching user data for {usrId}: {data['message']}")684 sys.exit(1)685 except Exception as e:686 print(f"Error fetching user data for {usrId}: {e}")687 sys.exit(1)688 689 # Get existing (approved) friends of each user690 vertices_metadata["existing-Friends"] = {}691 all_users = [user_id] + target_ids692 693 for userId in tqdm(all_users, desc="Collecting existing friends for all users:", unit="user"):694 try:695 Outbound_data = Outbound_Inbound_API(userId=userId, endpoint="outbound", URL=URL)696 Outbound_Ids = getStatusApprovedUsers(Outbound_data)697 698 Inbound_data = Outbound_Inbound_API(userId=userId, endpoint="inbound", URL=URL)699 Inbound_Ids = getStatusApprovedUsers(Inbound_data)700 701 vertices_metadata["existing-Friends"][userId] = Outbound_Ids + Inbound_Ids702 except Exception as e:703 print(f"Error fetching existing friends for {userId}: {e}")704 sys.exit(1)705 706 # Fetch metadata for the location707 try:708 data = get_vertex_data(id=location_id, type="location", URL=URL)709 vertices_metadata[location_id] = data["data"]710 except Exception as e:711 print(f"Error fetching location data for {location_id}: {e}")712 sys.exit(1)713 714 print(f"\nvertices_metadata: {vertices_metadata}\n")715 716 with open("vertices_metadata.json", "w") as f:717 json.dump(vertices_metadata, f, indent=4)718 719 720 print(make_plan(721 user_id=user_id,722 venue_id=location_id,723 target_user_ids=target_ids,724 vertices_metadata=vertices_metadata,725 plan_type='mutuals',726 temperature=0.1727 ))728 