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
115 lines
3.6 KiB
Python
115 lines
3.6 KiB
Python
from django.db import IntegrityError
|
|
|
|
from .models import (
|
|
EncryptionConfigurationError,
|
|
ImmichIntegration,
|
|
UserAISettings,
|
|
UserAPIKey,
|
|
UserRecommendationPreferenceProfile,
|
|
)
|
|
from rest_framework import serializers
|
|
|
|
|
|
class ImmichIntegrationSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = ImmichIntegration
|
|
fields = "__all__"
|
|
read_only_fields = ["id", "user"]
|
|
|
|
def to_representation(self, instance):
|
|
representation = super().to_representation(instance)
|
|
representation.pop("user", None)
|
|
return representation
|
|
|
|
|
|
class UserAPIKeySerializer(serializers.ModelSerializer):
|
|
api_key = serializers.CharField(write_only=True, required=True, allow_blank=False)
|
|
masked_api_key = serializers.CharField(read_only=True)
|
|
|
|
class Meta:
|
|
model = UserAPIKey
|
|
fields = [
|
|
"id",
|
|
"provider",
|
|
"api_key",
|
|
"masked_api_key",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = ["id", "masked_api_key", "created_at", "updated_at"]
|
|
|
|
def validate_provider(self, value):
|
|
return (value or "").strip().lower()
|
|
|
|
def create(self, validated_data):
|
|
api_key = validated_data.pop("api_key")
|
|
user = self.context["request"].user
|
|
|
|
provider = validated_data.get("provider")
|
|
|
|
try:
|
|
instance, _ = UserAPIKey.objects.get_or_create(
|
|
user=user,
|
|
provider=provider,
|
|
defaults={"encrypted_api_key": ""},
|
|
)
|
|
instance.set_api_key(api_key)
|
|
except EncryptionConfigurationError as exc:
|
|
raise serializers.ValidationError({"api_key": str(exc)}) from exc
|
|
except IntegrityError:
|
|
# Defensive retry: in highly concurrent requests a competing create can
|
|
# still race. Fall back to updating the existing row instead of 500.
|
|
instance = UserAPIKey.objects.get(user=user, provider=provider)
|
|
try:
|
|
instance.set_api_key(api_key)
|
|
except EncryptionConfigurationError as exc:
|
|
raise serializers.ValidationError({"api_key": str(exc)}) from exc
|
|
|
|
instance.save(update_fields=["encrypted_api_key", "updated_at"])
|
|
return instance
|
|
|
|
def update(self, instance, validated_data):
|
|
api_key = validated_data.pop("api_key", None)
|
|
for attr, value in validated_data.items():
|
|
setattr(instance, attr, value)
|
|
if api_key is not None:
|
|
try:
|
|
instance.set_api_key(api_key)
|
|
except EncryptionConfigurationError as exc:
|
|
raise serializers.ValidationError({"api_key": str(exc)}) from exc
|
|
instance.save()
|
|
return instance
|
|
|
|
def to_representation(self, instance):
|
|
representation = super().to_representation(instance)
|
|
representation.pop("api_key", None)
|
|
return representation
|
|
|
|
|
|
class UserRecommendationPreferenceProfileSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = UserRecommendationPreferenceProfile
|
|
fields = [
|
|
"id",
|
|
"cuisines",
|
|
"interests",
|
|
"trip_style",
|
|
"notes",
|
|
"created_at",
|
|
"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"]
|