Skip to content

Commit 79d6bac

Browse files
authored
Merge pull request #43 from Commute-ai/copilot/connect-routing-with-preferences
Connect routing logic with user preferences (requires authentication)
2 parents 037264a + 10936c5 commit 79d6bac

7 files changed

Lines changed: 582 additions & 30 deletions

File tree

app/api/v1/endpoints/routes.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66

77
import logging
88
from datetime import datetime, timezone
9-
from typing import Any
9+
from typing import Any, List
1010

11-
from fastapi import APIRouter, HTTPException
11+
from fastapi import APIRouter, Depends, HTTPException
12+
from sqlalchemy.orm import Session
1213

14+
from app.db.database import get_db
15+
from app.models.user import User
1316
from app.schemas.routes import RouteSearchRequest, RouteSearchResponse
1417
from app.services.ai_agents_service import ai_agents_service
18+
from app.services.auth_service import auth_service
19+
from app.services.preference_service import preference_service
1520
from app.services.routing_service import (
1621
RoutingAPIError,
1722
RoutingDataError,
@@ -26,15 +31,21 @@
2631

2732

2833
@router.post("/search", response_model=RouteSearchResponse)
29-
async def search_routes(request: RouteSearchRequest) -> Any:
34+
async def search_routes(
35+
request: RouteSearchRequest,
36+
db: Session = Depends(get_db),
37+
current_user: User = Depends(auth_service.get_current_user),
38+
) -> Any:
3039
"""
3140
Search for public transport routes between two locations.
3241
3342
Queries the HSL (Helsinki Regional Transport) API to find route alternatives
34-
between origin and destination coordinates.
43+
between origin and destination coordinates. Requires authentication.
3544
3645
Args:
3746
request: Route search parameters including origin, destination, and preferences
47+
db: Database session
48+
current_user: Authenticated user (required)
3849
3950
Returns:
4051
RouteSearchResponse with list of available route itineraries
@@ -43,15 +54,32 @@ async def search_routes(request: RouteSearchRequest) -> Any:
4354
HTTPException: If the route search fails or returns invalid data
4455
"""
4556
logger.info(
46-
"Route search request: origin=%s, destination=%s, num_itineraries=%s",
57+
"Route search request: origin=%s, destination=%s, num_itineraries=%s, user_id=%s",
4758
request.origin,
4859
request.destination,
4960
request.num_itineraries,
61+
current_user.id,
5062
)
5163

5264
# Use current time if earliest_departure not provided
5365
earliest_departure = request.earliest_departure or datetime.now(timezone.utc)
5466

67+
# Collect user preferences from multiple sources
68+
user_preferences: List[str] = []
69+
70+
# 1. Add preferences from request (explicitly provided)
71+
if request.preferences:
72+
user_preferences.extend(request.preferences)
73+
74+
# 2. Add stored preferences from authenticated user
75+
try:
76+
stored_prefs = preference_service.get_user_preferences(db, int(current_user.id))
77+
user_preferences.extend([str(pref.prompt) for pref in stored_prefs])
78+
except Exception as e: # pylint: disable=broad-except
79+
logger.warning("Failed to fetch user preferences: %s", str(e))
80+
81+
logger.info("Using %d user preferences for route insights", len(user_preferences))
82+
5583
try:
5684
# Call routing service to fetch itineraries from HSL API
5785
itineraries = await routing_service.get_itinaries(
@@ -64,7 +92,9 @@ async def search_routes(request: RouteSearchRequest) -> Any:
6492
# Enhance each itinerary with AI insights (with graceful degradation)
6593
for itinerary in itineraries:
6694
try:
67-
await ai_agents_service.get_itinerary_insight(itinerary)
95+
await ai_agents_service.get_itinerary_insight(
96+
itinerary, user_preferences if user_preferences else None
97+
)
6898
except Exception as e: # pylint: disable=broad-except
6999
# Gracefully degrade - log warning but continue without AI insights
70100
logger.warning("Failed to get AI insights for itinerary: %s", str(e))

app/schemas/routes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ class RouteSearchRequest(BaseModel):
3030
le=10,
3131
description="Number of route alternatives to return (1-10, default: 3)",
3232
)
33+
preferences: Optional[List[str]] = Field(
34+
default=None,
35+
description=(
36+
"User preferences for route optimization " "(e.g., 'prefer walking', 'avoid buses')"
37+
),
38+
)
3339

3440

3541
class RouteSearchResponse(BaseModel):

app/services/ai_agents_service.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ async def health_check(self) -> ServiceHealth:
6565
message=f"AI-agents API check failed: {str(e)}",
6666
)
6767

68-
async def get_itinerary_insight(self, itinerary: Itinerary) -> None:
68+
async def get_itinerary_insight(
69+
self, itinerary: Itinerary, user_preferences: Optional[list] = None
70+
) -> None:
6971
"""
7072
Get AI-generated insights for a complete itinerary and enrich it with AI data.
7173
@@ -79,6 +81,7 @@ async def get_itinerary_insight(self, itinerary: Itinerary) -> None:
7981
8082
Args:
8183
itinerary: The itinerary object containing the complete journey information
84+
user_preferences: Optional list of user preference strings to consider
8285
8386
Returns:
8487
None - modifies the itinerary object in place
@@ -116,6 +119,10 @@ async def get_itinerary_insight(self, itinerary: Itinerary) -> None:
116119
],
117120
}
118121

122+
# Add user preferences if provided
123+
if user_preferences:
124+
payload["user_preferences"] = user_preferences
125+
119126
response = await client.post("/api/v1/insight/itinerary", json=payload)
120127

121128
if response.status_code == 200:

app/services/auth_service.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Optional
77

88
from fastapi import Depends, HTTPException, status
9+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
910
from jose import jwt
1011
from passlib.context import CryptContext
1112
from pydantic import ValidationError
@@ -116,6 +117,43 @@ def get_current_user(
116117
raise HTTPException(status_code=404, detail="User not found")
117118
return user
118119

120+
@staticmethod
121+
def get_current_user_optional(
122+
db: Session = Depends(get_db),
123+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
124+
) -> Optional[User]:
125+
"""
126+
Decode JWT token and return the current user if authenticated.
127+
Returns None if no token is provided (allowing anonymous access).
128+
129+
Args:
130+
db: Database session
131+
credentials: Optional HTTP Bearer credentials from request
132+
133+
Returns:
134+
User object for the authenticated user, or None if not authenticated
135+
136+
Raises:
137+
HTTPException: If token is provided but invalid
138+
"""
139+
if credentials is None:
140+
return None
141+
142+
token = credentials.credentials
143+
try:
144+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
145+
token_data = TokenPayload(**payload)
146+
except (jwt.JWTError, ValidationError):
147+
raise HTTPException(
148+
status_code=status.HTTP_403_FORBIDDEN,
149+
detail="Could not validate credentials",
150+
)
151+
152+
user = db.query(User).filter(User.id == int(token_data.sub)).first() # type: ignore
153+
if not user:
154+
raise HTTPException(status_code=404, detail="User not found")
155+
return user
156+
119157

120158
# Create a singleton instance
121159
auth_service = AuthService()

0 commit comments

Comments
 (0)