fix(ai): critical fixes for agent-redesign - provider selection and auto-learn
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
This commit is contained in:
@@ -335,25 +335,22 @@ Be conversational, helpful, and enthusiastic about travel. Keep responses concis
|
||||
else:
|
||||
try:
|
||||
profile = UserRecommendationPreferenceProfile.objects.get(user=user)
|
||||
preference_lines = []
|
||||
|
||||
if profile.cuisines:
|
||||
preference_lines.append(
|
||||
f"🍽️ **Cuisine Preferences**: {profile.cuisines}"
|
||||
)
|
||||
if profile.interests:
|
||||
preference_lines.append(
|
||||
f"🎯 **Interests**: {_format_interests(profile.interests)}"
|
||||
)
|
||||
if profile.trip_style:
|
||||
preference_lines.append(f"✈️ **Travel Style**: {profile.trip_style}")
|
||||
if profile.notes:
|
||||
preference_lines.append(f"📝 **Additional Notes**: {profile.notes}")
|
||||
if profile.interests or profile.trip_style or profile.notes:
|
||||
base_prompt += "\n\n## Traveler Preferences\n"
|
||||
base_prompt += "*(Automatically inferred from travel history)*\n\n"
|
||||
|
||||
if preference_lines:
|
||||
base_prompt += "\n\n## Traveler Preferences\n" + "\n".join(
|
||||
preference_lines
|
||||
)
|
||||
if profile.interests:
|
||||
interests_str = (
|
||||
", ".join(profile.interests)
|
||||
if isinstance(profile.interests, list)
|
||||
else str(profile.interests)
|
||||
)
|
||||
base_prompt += f"🎯 **Interests**: {interests_str}\n"
|
||||
if profile.trip_style:
|
||||
base_prompt += f"✈️ **Travel Style**: {profile.trip_style}\n"
|
||||
if profile.notes:
|
||||
base_prompt += f"📍 **Patterns**: {profile.notes}\n"
|
||||
except UserRecommendationPreferenceProfile.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from adventures.models import Collection
|
||||
@@ -19,6 +20,8 @@ from ..llm_client import (
|
||||
from ..models import ChatConversation, ChatMessage
|
||||
from ..serializers import ChatConversationSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ChatConversationSerializer
|
||||
@@ -108,6 +111,15 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def send_message(self, request, pk=None):
|
||||
# Auto-learn preferences from user's travel history
|
||||
from integrations.utils.auto_profile import update_auto_preference_profile
|
||||
|
||||
try:
|
||||
update_auto_preference_profile(request.user)
|
||||
except Exception as exc:
|
||||
logger.warning("Auto-profile update failed: %s", exc)
|
||||
# Continue anyway - not critical
|
||||
|
||||
conversation = self.get_object()
|
||||
user_content = (request.data.get("message") or "").strip()
|
||||
if not user_content:
|
||||
@@ -323,6 +335,93 @@ class ChatProviderCatalogViewSet(viewsets.ViewSet):
|
||||
def list(self, request):
|
||||
return Response(get_provider_catalog(user=request.user))
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def models(self, request, pk=None):
|
||||
"""Fetch available models from a provider's API."""
|
||||
from chat.llm_client import get_llm_api_key
|
||||
|
||||
provider = (pk or "").lower()
|
||||
|
||||
api_key = get_llm_api_key(request.user, provider)
|
||||
if not api_key:
|
||||
return Response(
|
||||
{"error": "No API key configured for this provider"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
try:
|
||||
if provider == "openai":
|
||||
import openai
|
||||
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
models = client.models.list()
|
||||
chat_models = [
|
||||
model.id
|
||||
for model in models
|
||||
if any(prefix in model.id for prefix in ["gpt-", "o1-", "chatgpt"])
|
||||
]
|
||||
return Response({"models": sorted(set(chat_models), reverse=True)})
|
||||
|
||||
if provider in ["anthropic", "claude"]:
|
||||
return Response(
|
||||
{
|
||||
"models": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022",
|
||||
"claude-3-haiku-20240307",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
if provider in ["gemini", "google"]:
|
||||
return Response(
|
||||
{
|
||||
"models": [
|
||||
"gemini-2.0-flash",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-flash-8b",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
if provider in ["groq"]:
|
||||
return Response(
|
||||
{
|
||||
"models": [
|
||||
"llama-3.3-70b-versatile",
|
||||
"llama-3.1-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
"mixtral-8x7b-32768",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
if provider in ["ollama"]:
|
||||
import requests
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://localhost:11434/api/tags", timeout=5
|
||||
)
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
models = [item["name"] for item in data.get("models", [])]
|
||||
return Response({"models": models})
|
||||
except Exception:
|
||||
pass
|
||||
return Response({"models": []})
|
||||
|
||||
return Response({"models": []})
|
||||
except Exception as exc:
|
||||
logger.error("Failed to fetch models for %s: %s", provider, exc)
|
||||
return Response(
|
||||
{"error": f"Failed to fetch models: {str(exc)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
from .capabilities import CapabilitiesView
|
||||
from .day_suggestions import DaySuggestionsView
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 25
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 1000
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 1000
|
||||
168
backend/server/integrations/utils/auto_profile.py
Normal file
168
backend/server/integrations/utils/auto_profile.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user