Fix 1: Provider/Model Selection (Critical - unblocks LLM)
- Add /api/chat/providers/{id}/models/ endpoint to fetch available models
- Auto-select first configured provider instead of hardcoded 'openai'
- Add model dropdown populated from provider API
- Filter provider list to only show configured providers
- Show helpful error when no providers configured
Fix 2: Auto-Learn Preferences (Replaces manual input)
- Create auto_profile.py utility to infer preferences from user data
- Learn interests from Activity sport types and Location categories
- Learn trip style from Lodging types (hostel=budget, resort=luxury, etc.)
- Learn geographic preferences from VisitedRegion/VisitedCity
- Call auto-learn on every chat start (send_message)
- System prompt now indicates preferences are auto-inferred
Fix 3: Remove Manual Preference UI
- Remove travel_preferences section from Settings
- Remove preference form fields and save logic
- Remove preference fetch from server-side load
- Keep UserRecommendationPreferenceProfile type for backend use
The LLM should now work correctly:
- Users with any configured provider will have it auto-selected
- Model list is fetched dynamically from provider API
- Preferences are learned from actual travel history
169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
"""
|
|
Auto-learn user preferences from their travel history.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.db.models import Count
|
|
|
|
from adventures.models import Activity, Location, Lodging
|
|
from integrations.models import UserRecommendationPreferenceProfile
|
|
from worldtravel.models import VisitedCity, VisitedRegion
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Mapping of lodging types to travel styles
|
|
LODGING_STYLE_MAP = {
|
|
"hostel": "budget",
|
|
"campground": "outdoor",
|
|
"cabin": "outdoor",
|
|
"camping": "outdoor",
|
|
"resort": "luxury",
|
|
"villa": "luxury",
|
|
"hotel": "comfort",
|
|
"apartment": "independent",
|
|
"bnb": "local",
|
|
"boat": "adventure",
|
|
}
|
|
|
|
# Activity sport types to interest categories
|
|
ACTIVITY_INTEREST_MAP = {
|
|
"hiking": "hiking & nature",
|
|
"walking": "walking tours",
|
|
"running": "fitness",
|
|
"cycling": "cycling",
|
|
"swimming": "water sports",
|
|
"surfing": "water sports",
|
|
"kayaking": "water sports",
|
|
"skiing": "winter sports",
|
|
"snowboarding": "winter sports",
|
|
"climbing": "adventure sports",
|
|
}
|
|
|
|
|
|
def build_auto_preference_profile(user) -> dict:
|
|
"""
|
|
Automatically build preference profile from user's existing data.
|
|
|
|
Analyzes:
|
|
- Activities (sport types) → interests
|
|
- Location categories → interests
|
|
- Lodging types → trip style
|
|
- Visited regions/cities → geographic preferences
|
|
|
|
Returns dict with: cuisines, interests, trip_style, notes
|
|
"""
|
|
profile = {
|
|
"cuisines": None,
|
|
"interests": [],
|
|
"trip_style": None,
|
|
"notes": None,
|
|
}
|
|
|
|
try:
|
|
activity_interests = (
|
|
Activity.objects.filter(user=user)
|
|
.values("sport_type")
|
|
.annotate(count=Count("id"))
|
|
.exclude(sport_type__isnull=True)
|
|
.exclude(sport_type="")
|
|
.order_by("-count")[:5]
|
|
)
|
|
|
|
for activity in activity_interests:
|
|
sport = activity["sport_type"]
|
|
if sport:
|
|
interest = ACTIVITY_INTEREST_MAP.get(
|
|
sport.lower(), sport.replace("_", " ")
|
|
)
|
|
if interest not in profile["interests"]:
|
|
profile["interests"].append(interest)
|
|
|
|
category_interests = (
|
|
Location.objects.filter(user=user)
|
|
.values("category__name")
|
|
.annotate(count=Count("id"))
|
|
.exclude(category__name__isnull=True)
|
|
.exclude(category__name="")
|
|
.order_by("-count")[:5]
|
|
)
|
|
|
|
for category in category_interests:
|
|
category_name = category["category__name"]
|
|
if category_name and category_name.lower() not in [
|
|
i.lower() for i in profile["interests"]
|
|
]:
|
|
profile["interests"].append(category_name)
|
|
|
|
top_lodging = (
|
|
Lodging.objects.filter(user=user)
|
|
.values("type")
|
|
.annotate(count=Count("id"))
|
|
.exclude(type__isnull=True)
|
|
.exclude(type="")
|
|
.order_by("-count")
|
|
.first()
|
|
)
|
|
|
|
if top_lodging and top_lodging["type"]:
|
|
lodging_type = top_lodging["type"].lower()
|
|
profile["trip_style"] = LODGING_STYLE_MAP.get(lodging_type, lodging_type)
|
|
|
|
top_regions = list(
|
|
VisitedRegion.objects.filter(user=user)
|
|
.values("region__name")
|
|
.annotate(count=Count("id"))
|
|
.exclude(region__name__isnull=True)
|
|
.order_by("-count")[:3]
|
|
)
|
|
|
|
if top_regions:
|
|
region_names = [r["region__name"] for r in top_regions if r["region__name"]]
|
|
if region_names:
|
|
profile["notes"] = f"Frequently visits: {', '.join(region_names)}"
|
|
|
|
if not profile["notes"]:
|
|
top_cities = list(
|
|
VisitedCity.objects.filter(user=user)
|
|
.values("city__name")
|
|
.annotate(count=Count("id"))
|
|
.exclude(city__name__isnull=True)
|
|
.order_by("-count")[:3]
|
|
)
|
|
if top_cities:
|
|
city_names = [c["city__name"] for c in top_cities if c["city__name"]]
|
|
if city_names:
|
|
profile["notes"] = f"Frequently visits: {', '.join(city_names)}"
|
|
|
|
profile["interests"] = profile["interests"][:8]
|
|
except Exception as exc:
|
|
logger.error("Error building auto profile for user %s: %s", user.id, exc)
|
|
|
|
return profile
|
|
|
|
|
|
def update_auto_preference_profile(user) -> UserRecommendationPreferenceProfile:
|
|
"""
|
|
Build and save auto-learned profile to database.
|
|
Called automatically when chat starts.
|
|
"""
|
|
auto_data = build_auto_preference_profile(user)
|
|
|
|
profile, created = UserRecommendationPreferenceProfile.objects.update_or_create(
|
|
user=user,
|
|
defaults={
|
|
"cuisines": auto_data["cuisines"],
|
|
"interests": auto_data["interests"],
|
|
"trip_style": auto_data["trip_style"],
|
|
"notes": auto_data["notes"],
|
|
},
|
|
)
|
|
|
|
logger.info(
|
|
"%s auto profile for user %s",
|
|
"Created" if created else "Updated",
|
|
user.id,
|
|
)
|
|
return profile
|