Skip to content

Commit fc3bf48

Browse files
ebwu95YashK2005
andauthored
matching algorithm api (#53)
Implemented matching algo + api, which returns a list of volunteers in descending order of match score. TO DO: Change the return data depending on what's needed on admin console side Store refused matches --------- Co-authored-by: YashK2005 <[email protected]>
1 parent dfbc24b commit fc3bf48

File tree

13 files changed

+1173
-4
lines changed

13 files changed

+1173
-4
lines changed

.github/workflows/backend-ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
working-directory: ./backend
6767
run: |
6868
echo "POSTGRES_TEST_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
69+
echo "POSTGRES_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
6970
echo "SECRET_KEY=test-secret-key-for-ci" >> .env
7071
echo "ENVIRONMENT=test" >> .env
7172
@@ -163,6 +164,7 @@ jobs:
163164
working-directory: ./backend
164165
run: |
165166
echo "POSTGRES_TEST_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
167+
echo "POSTGRES_DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/llsc_test" >> .env
166168
echo "SECRET_KEY=test-secret-key-for-ci" >> .env
167169
echo "ENVIRONMENT=test" >> .env
168170
echo "TEST_SCRIPT_BACKEND_URL=http://localhost:8000" >> .env
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any, Dict, List
3+
from uuid import UUID
4+
5+
6+
class IMatchingService(ABC):
7+
"""
8+
Interface for the Matching Service, defining methods to find
9+
potential matches between users.
10+
"""
11+
12+
@abstractmethod
13+
async def get_matches(self, user_id: UUID) -> List[Dict[str, Any]]:
14+
"""
15+
Find potential matches based on the given user ID.
16+
17+
:param user_id: ID of the user to find matches for
18+
:type user_id: UUID
19+
:return: List of dictionaries with 'user' and 'score' keys
20+
:rtype: List[Dict[str, Any]]
21+
:raises Exception: If matching process fails
22+
"""
23+
pass

backend/app/routes/matching.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from uuid import UUID
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
from sqlalchemy.orm import Session
5+
6+
from app.schemas.matching import RelevantUsersResponse
7+
from app.services.implementations.matching_service import MatchingService
8+
from app.utilities.db_utils import get_db
9+
10+
router = APIRouter(
11+
prefix="/matching",
12+
tags=["matching"],
13+
)
14+
15+
16+
def get_matching_service(db: Session = Depends(get_db)):
17+
return MatchingService(db)
18+
19+
20+
@router.get("/{user_id}", response_model=RelevantUsersResponse)
21+
async def get_matches(user_id: UUID, matching_service: MatchingService = Depends(get_matching_service)):
22+
"""
23+
Get potential user matches based on the user's profile.
24+
"""
25+
try:
26+
matched_data = await matching_service.get_matches(user_id)
27+
return RelevantUsersResponse(matches=matched_data)
28+
except ValueError as ve:
29+
raise HTTPException(status_code=404, detail=str(ve))
30+
except HTTPException as http_ex:
31+
raise http_ex
32+
except Exception as e:
33+
raise HTTPException(status_code=500, detail=str(e))

backend/app/schemas/matching.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
Pydantic schemas for matching-related data validation and serialization.
3+
"""
4+
5+
from typing import List
6+
7+
from pydantic import BaseModel
8+
9+
from .user import UserBase
10+
11+
12+
class MatchedUser(BaseModel):
13+
"""
14+
Schema for a matched user with their compatibility score.
15+
"""
16+
17+
user: UserBase
18+
score: float
19+
20+
21+
class RelevantUsersResponse(BaseModel):
22+
"""
23+
Response schema for matching endpoint containing a list of relevant users with scores.
24+
"""
25+
26+
matches: List[MatchedUser]

backend/app/schemas/user.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class UserBase(BaseModel):
4545
email: EmailStr
4646
role: UserRole
4747

48+
model_config = ConfigDict(from_attributes=True)
49+
4850

4951
class UserCreateRequest(UserBase):
5052
"""

backend/app/seeds/match_status.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Seed match status data."""
2+
3+
from sqlalchemy.orm import Session
4+
5+
from app.models.MatchStatus import MatchStatus
6+
7+
8+
def seed_match_status(session: Session) -> None:
9+
"""Seed the match_status table with default statuses."""
10+
11+
match_status_data = [
12+
{"id": 1, "name": "pending"},
13+
{"id": 2, "name": "confirmed"},
14+
{"id": 3, "name": "cancelled"},
15+
{"id": 4, "name": "completed"},
16+
{"id": 5, "name": "no_show"},
17+
{"id": 6, "name": "rescheduled"},
18+
]
19+
20+
for status_data in match_status_data:
21+
# Check if status already exists
22+
existing_status = session.query(MatchStatus).filter_by(id=status_data["id"]).first()
23+
if not existing_status:
24+
status = MatchStatus(**status_data)
25+
session.add(status)
26+
print(f"Added match status: {status_data['name']}")
27+
else:
28+
print(f"Match status already exists: {status_data['name']}")
29+
30+
session.commit()
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""Seed ranking preferences data."""
2+
3+
from sqlalchemy.orm import Session
4+
5+
from app.models.RankingPreference import RankingPreference
6+
from app.models.User import User
7+
from app.utilities.form_constants import QualityId, TreatmentId
8+
9+
10+
def seed_ranking_preferences(session: Session) -> None:
11+
"""Seed the ranking_preferences table with sample ranking data for testing all matching cases."""
12+
13+
# Find users by email instead of hardcoding UUIDs
14+
15+
# Test Case 1: Patient wants cancer patient volunteer
16+
sarah_user = session.query(User).filter_by(email="[email protected]").first()
17+
sarah_id = sarah_user.id if sarah_user else None
18+
19+
# Test Case 2: Caregiver wants ONLY cancer patient volunteers
20+
lisa_user = session.query(User).filter_by(email="[email protected]").first()
21+
lisa_id = lisa_user.id if lisa_user else None
22+
23+
# Test Case 3: Caregiver wants ONLY caregiver volunteers
24+
karen_user = session.query(User).filter_by(email="[email protected]").first()
25+
karen_id = karen_user.id if karen_user else None
26+
27+
ranking_data = [
28+
# CASE 1: Sarah (patient) wants patient volunteers - 5 preferences
29+
{
30+
"user_id": sarah_id,
31+
"target_role": "patient",
32+
"kind": "quality",
33+
"quality_id": QualityId.SAME_DIAGNOSIS,
34+
"scope": "self",
35+
"rank": 1,
36+
},
37+
{
38+
"user_id": sarah_id,
39+
"target_role": "patient",
40+
"kind": "quality",
41+
"quality_id": QualityId.SAME_GENDER_IDENTITY,
42+
"scope": "self",
43+
"rank": 2,
44+
},
45+
{
46+
"user_id": sarah_id,
47+
"target_role": "patient",
48+
"kind": "quality",
49+
"quality_id": QualityId.SAME_AGE,
50+
"scope": "self",
51+
"rank": 3,
52+
},
53+
{
54+
"user_id": sarah_id,
55+
"target_role": "patient",
56+
"kind": "treatment",
57+
"treatment_id": TreatmentId.CHEMOTHERAPY,
58+
"scope": "self",
59+
"rank": 4,
60+
},
61+
{
62+
"user_id": sarah_id,
63+
"target_role": "patient",
64+
"kind": "treatment",
65+
"treatment_id": TreatmentId.RADIATION,
66+
"scope": "self",
67+
"rank": 5,
68+
},
69+
# CASE 2: Lisa (caregiver) wants ONLY patient volunteers - 5 preferences
70+
{
71+
"user_id": lisa_id,
72+
"target_role": "patient",
73+
"kind": "quality",
74+
"quality_id": QualityId.SAME_DIAGNOSIS,
75+
"scope": "loved_one",
76+
"rank": 1,
77+
},
78+
{
79+
"user_id": lisa_id,
80+
"target_role": "patient",
81+
"kind": "quality",
82+
"quality_id": QualityId.SAME_GENDER_IDENTITY,
83+
"scope": "loved_one",
84+
"rank": 2,
85+
},
86+
{
87+
"user_id": lisa_id,
88+
"target_role": "patient",
89+
"kind": "quality",
90+
"quality_id": QualityId.SAME_AGE,
91+
"scope": "loved_one",
92+
"rank": 3,
93+
},
94+
{
95+
"user_id": lisa_id,
96+
"target_role": "patient",
97+
"kind": "treatment",
98+
"treatment_id": TreatmentId.CHEMOTHERAPY,
99+
"scope": "loved_one",
100+
"rank": 4,
101+
},
102+
{
103+
"user_id": lisa_id,
104+
"target_role": "patient",
105+
"kind": "treatment",
106+
"treatment_id": TreatmentId.RADIATION,
107+
"scope": "loved_one",
108+
"rank": 5,
109+
},
110+
# CASE 3: Karen (caregiver) wants ONLY caregiver volunteers - 5 preferences
111+
{
112+
"user_id": karen_id,
113+
"target_role": "caregiver",
114+
"kind": "quality",
115+
"quality_id": QualityId.SAME_MARITAL_STATUS,
116+
"scope": "self",
117+
"rank": 1,
118+
},
119+
{
120+
"user_id": karen_id,
121+
"target_role": "caregiver",
122+
"kind": "quality",
123+
"quality_id": QualityId.SAME_PARENTAL_STATUS,
124+
"scope": "self",
125+
"rank": 2,
126+
},
127+
{
128+
"user_id": karen_id,
129+
"target_role": "caregiver",
130+
"kind": "quality",
131+
"quality_id": QualityId.SAME_GENDER_IDENTITY,
132+
"scope": "self",
133+
"rank": 3,
134+
},
135+
{
136+
"user_id": karen_id,
137+
"target_role": "caregiver",
138+
"kind": "quality",
139+
"quality_id": QualityId.SAME_AGE,
140+
"scope": "self",
141+
"rank": 4,
142+
},
143+
{
144+
"user_id": karen_id,
145+
"target_role": "caregiver",
146+
"kind": "quality",
147+
"quality_id": QualityId.SAME_DIAGNOSIS,
148+
"scope": "loved_one", # Match caregiver's loved one diagnosis with volunteer's loved one
149+
"rank": 5,
150+
},
151+
]
152+
153+
for pref_data in ranking_data:
154+
# Skip Karen's preferences if she doesn't exist yet
155+
if pref_data["user_id"] is None:
156+
continue
157+
158+
# Check if preference already exists
159+
existing_pref = (
160+
session.query(RankingPreference)
161+
.filter_by(
162+
user_id=pref_data["user_id"],
163+
target_role=pref_data["target_role"],
164+
kind=pref_data["kind"],
165+
rank=pref_data["rank"],
166+
)
167+
.first()
168+
)
169+
170+
if not existing_pref:
171+
preference = RankingPreference(**pref_data)
172+
session.add(preference)
173+
print(
174+
f"Added ranking preference: {pref_data['kind']} rank {pref_data['rank']} for user {pref_data['user_id']}"
175+
)
176+
else:
177+
print(
178+
f"Ranking preference already exists: {pref_data['kind']} rank {pref_data['rank']} for user {pref_data['user_id']}"
179+
)
180+
181+
session.commit()

backend/app/seeds/runner.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
from .experiences import seed_experiences
1616
from .forms import seed_forms
1717
from .qualities import seed_qualities
18+
from .ranking_preferences import seed_ranking_preferences
1819
from .roles import seed_roles
1920
from .treatments import seed_treatments
21+
from .users import seed_users
2022

2123
# Load environment variables
2224
load_dotenv()
@@ -55,6 +57,8 @@ def seed_database(verbose: bool = True) -> None:
5557
("Experiences", seed_experiences),
5658
("Qualities", seed_qualities),
5759
("Forms", seed_forms),
60+
("Users", seed_users),
61+
("Ranking Preferences", seed_ranking_preferences),
5862
]
5963

6064
for name, seed_func in seed_functions:

0 commit comments

Comments
 (0)