feat(ai): implement agent-redesign plan with enhanced AI travel features
Phase 1 - Configuration Infrastructure (WS1): - Add instance-level AI env vars (VOYAGE_AI_PROVIDER, VOYAGE_AI_MODEL, VOYAGE_AI_API_KEY) - Implement fallback chain: user key → instance key → error - Add UserAISettings model for per-user provider/model preferences - Enhance provider catalog with instance_configured and user_configured flags - Optimize provider catalog to avoid N+1 queries Phase 1 - User Preference Learning (WS2): - Add Travel Preferences tab to Settings page - Improve preference formatting in system prompt with emoji headers - Add multi-user preference aggregation for shared collections Phase 2 - Day-Level Suggestions Modal (WS3): - Create ItinerarySuggestionModal with 3-step flow (category → filters → results) - Add AI suggestions button to itinerary Add dropdown - Support restaurant, activity, event, and lodging categories - Backend endpoint POST /api/chat/suggestions/day/ with context-aware prompts Phase 3 - Collection-Level Chat Improvements (WS4): - Inject collection context (destination, dates) into chat system prompt - Add quick action buttons for common queries - Add 'Add to itinerary' button on search_places results - Update chat UI with travel-themed branding and improved tool result cards Phase 3 - Web Search Capability (WS5): - Add web_search agent tool using DuckDuckGo - Support location_context parameter for biased results - Handle rate limiting gracefully Phase 4 - Extensibility Architecture (WS6): - Implement decorator-based @agent_tool registry - Convert existing tools to use decorators - Add GET /api/chat/capabilities/ endpoint for tool discovery - Refactor execute_tool() to use registry pattern
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-08
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("integrations", "0007_userapikey_userrecommendationpreferenceprofile"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserAISettings",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"preferred_provider",
|
||||
models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
(
|
||||
"preferred_model",
|
||||
models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ai_settings",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User AI Settings",
|
||||
"verbose_name_plural": "User AI Settings",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -124,3 +124,23 @@ class UserRecommendationPreferenceProfile(models.Model):
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class UserAISettings(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="ai_settings",
|
||||
)
|
||||
preferred_provider = models.CharField(max_length=100, blank=True, null=True)
|
||||
preferred_model = models.CharField(max_length=100, blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User AI Settings"
|
||||
verbose_name_plural = "User AI Settings"
|
||||
|
||||
def __str__(self):
|
||||
return f"AI Settings for {self.user.username}"
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db import IntegrityError
|
||||
from .models import (
|
||||
EncryptionConfigurationError,
|
||||
ImmichIntegration,
|
||||
UserAISettings,
|
||||
UserAPIKey,
|
||||
UserRecommendationPreferenceProfile,
|
||||
)
|
||||
@@ -98,3 +99,16 @@ class UserRecommendationPreferenceProfileSerializer(serializers.ModelSerializer)
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class UserAISettingsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserAISettings
|
||||
fields = [
|
||||
"id",
|
||||
"preferred_provider",
|
||||
"preferred_model",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
@@ -6,6 +6,7 @@ from integrations.views import (
|
||||
StravaIntegrationView,
|
||||
WandererIntegrationViewSet,
|
||||
UserAPIKeyViewSet,
|
||||
UserAISettingsViewSet,
|
||||
UserRecommendationPreferenceProfileViewSet,
|
||||
)
|
||||
|
||||
@@ -22,6 +23,7 @@ router.register(
|
||||
UserRecommendationPreferenceProfileViewSet,
|
||||
basename="user-recommendation-preferences",
|
||||
)
|
||||
router.register(r"ai-settings", UserAISettingsViewSet, basename="user-ai-settings")
|
||||
|
||||
# Include the router URLs
|
||||
urlpatterns = [
|
||||
|
||||
@@ -4,3 +4,4 @@ from .strava_view import StravaIntegrationView
|
||||
from .wanderer_view import WandererIntegrationViewSet
|
||||
from .user_api_key_view import UserAPIKeyViewSet
|
||||
from .recommendation_profile_view import UserRecommendationPreferenceProfileViewSet
|
||||
from .ai_settings_view import UserAISettingsViewSet
|
||||
|
||||
39
backend/server/integrations/views/ai_settings_view.py
Normal file
39
backend/server/integrations/views/ai_settings_view.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from integrations.models import UserAISettings
|
||||
from integrations.serializers import UserAISettingsSerializer
|
||||
|
||||
|
||||
class UserAISettingsViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserAISettingsSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return UserAISettings.objects.filter(user=self.request.user)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
instance = self.get_queryset().first()
|
||||
if not instance:
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response([serializer.data], status=status.HTTP_200_OK)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
existing = UserAISettings.objects.filter(user=self.request.user).first()
|
||||
if existing:
|
||||
for field, value in serializer.validated_data.items():
|
||||
setattr(existing, field, value)
|
||||
existing.save()
|
||||
self._upserted_instance = existing
|
||||
return
|
||||
|
||||
self._upserted_instance = serializer.save(user=self.request.user)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
output = self.get_serializer(self._upserted_instance)
|
||||
return Response(output.data, status=status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user