From 208a4626ca583fb221a1c1768dd6a61b973ed25b Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 10 Mar 2026 19:46:53 +0000 Subject: [PATCH] feat(chat): add admin-editable assistant system prompt --- backend/server/chat/admin.py | 15 ++++- backend/server/chat/llm_client.py | 60 +++++++++++-------- .../chat/migrations/0002_chatsystemprompt.py | 37 ++++++++++++ backend/server/chat/models.py | 34 +++++++++++ 4 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 backend/server/chat/migrations/0002_chatsystemprompt.py diff --git a/backend/server/chat/admin.py b/backend/server/chat/admin.py index 5189e3d3..495b6e9e 100644 --- a/backend/server/chat/admin.py +++ b/backend/server/chat/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin +from django.db import models +from django.forms import Textarea -from .models import ChatConversation, ChatMessage +from .models import ChatConversation, ChatMessage, ChatSystemPrompt @admin.register(ChatConversation) @@ -15,3 +17,14 @@ class ChatMessageAdmin(admin.ModelAdmin): list_display = ("id", "conversation", "role", "name", "created_at") search_fields = ("conversation__id", "content", "name") list_filter = ("role", "created_at") + + +@admin.register(ChatSystemPrompt) +class ChatSystemPromptAdmin(admin.ModelAdmin): + readonly_fields = ("updated_at",) + formfield_overrides = { + models.TextField: {"widget": Textarea(attrs={"rows": 20, "cols": 120})}, + } + + def has_add_permission(self, request): + return not ChatSystemPrompt.objects.exists() diff --git a/backend/server/chat/llm_client.py b/backend/server/chat/llm_client.py index ec848385..02b26471 100644 --- a/backend/server/chat/llm_client.py +++ b/backend/server/chat/llm_client.py @@ -9,6 +9,33 @@ from integrations.models import UserAPIKey logger = logging.getLogger(__name__) +DEFAULT_SYSTEM_PROMPT = """You are a helpful travel planning assistant for the Voyage travel app. You help users discover places, plan trips, and organize their itineraries. + +Your capabilities: +- Search for interesting places (restaurants, tourist attractions, hotels) near any location +- View and manage the user's trip collections and itineraries +- Add new locations to trip itineraries +- Check weather/temperature data for travel dates + +When suggesting places: +- Be specific with names, addresses, and why a place is worth visiting +- Consider the user's travel dates and weather conditions +- Group suggestions logically (by area, by type, by day) + +When modifying itineraries: +- Confirm with the user before the first add_to_itinerary action in a conversation +- After the user clearly approves adding items (for example: "yes", "go ahead", "add them", "just add things there"), stop re-confirming and call add_to_itinerary directly for subsequent additions in that conversation +- Suggest logical ordering based on geography +- Consider travel time between locations + +When chat context includes a trip collection: +- Treat context as itinerary-wide (potentially multiple stops), not a single destination +- Use get_trip_details first when you need complete collection context before searching for places +- Ground place searches in trip stops and dates from the provided trip context +- Only call search_places when you have a concrete, non-empty location string; if location is missing or unclear, ask a clarifying question to obtain it first + +Be conversational, helpful, and enthusiastic about travel. Keep responses concise but informative.""" + PROVIDER_MODEL_PREFIX = { "openai": "openai", "anthropic": "anthropic", @@ -321,34 +348,15 @@ def get_aggregated_preferences(collection): def get_system_prompt(user, collection=None): """Build the system prompt with user context.""" + from chat.models import ChatSystemPrompt from integrations.models import UserRecommendationPreferenceProfile - base_prompt = """You are a helpful travel planning assistant for the Voyage travel app. You help users discover places, plan trips, and organize their itineraries. - -Your capabilities: -- Search for interesting places (restaurants, tourist attractions, hotels) near any location -- View and manage the user's trip collections and itineraries -- Add new locations to trip itineraries -- Check weather/temperature data for travel dates - -When suggesting places: -- Be specific with names, addresses, and why a place is worth visiting -- Consider the user's travel dates and weather conditions -- Group suggestions logically (by area, by type, by day) - -When modifying itineraries: -- Confirm with the user before the first add_to_itinerary action in a conversation -- After the user clearly approves adding items (for example: "yes", "go ahead", "add them", "just add things there"), stop re-confirming and call add_to_itinerary directly for subsequent additions in that conversation -- Suggest logical ordering based on geography -- Consider travel time between locations - -When chat context includes a trip collection: -- Treat context as itinerary-wide (potentially multiple stops), not a single destination -- Use get_trip_details first when you need complete collection context before searching for places -- Ground place searches in trip stops and dates from the provided trip context -- Only call search_places when you have a concrete, non-empty location string; if location is missing or unclear, ask a clarifying question to obtain it first - -Be conversational, helpful, and enthusiastic about travel. Keep responses concise but informative.""" + custom = ChatSystemPrompt.load() + base_prompt = ( + custom.prompt_text + if custom and custom.prompt_text and custom.prompt_text.strip() + else DEFAULT_SYSTEM_PROMPT + ) if collection and collection.shared_with.count() > 0: base_prompt += get_aggregated_preferences(collection) diff --git a/backend/server/chat/migrations/0002_chatsystemprompt.py b/backend/server/chat/migrations/0002_chatsystemprompt.py new file mode 100644 index 00000000..2f52baf6 --- /dev/null +++ b/backend/server/chat/migrations/0002_chatsystemprompt.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.12 on 2026-03-10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("chat", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="ChatSystemPrompt", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "prompt_text", + models.TextField( + help_text="Base system prompt for the travel assistant. User and party travel preferences are appended automatically." + ), + ), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Chat System Prompt", + "verbose_name_plural": "Chat System Prompt", + }, + ), + ] diff --git a/backend/server/chat/models.py b/backend/server/chat/models.py index 0ee5bc24..973259ee 100644 --- a/backend/server/chat/models.py +++ b/backend/server/chat/models.py @@ -45,3 +45,37 @@ class ChatMessage(models.Model): class Meta: ordering = ["created_at"] + + +class ChatSystemPrompt(models.Model): + """Admin-editable system prompt for the travel assistant. + + Only one instance should exist (singleton pattern via Django admin). + The prompt text replaces the hardcoded base prompt in get_system_prompt(). + Dynamic user/party preference injection is appended automatically. + """ + + prompt_text = models.TextField( + help_text="Base system prompt for the travel assistant. " + "User and party travel preferences are appended automatically." + ) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Chat System Prompt" + verbose_name_plural = "Chat System Prompt" + + def __str__(self): + return f"System Prompt (updated {self.updated_at})" + + def save(self, *args, **kwargs): + self.pk = 1 + super().save(*args, **kwargs) + + @classmethod + def load(cls): + """Return the singleton instance or None if not configured.""" + try: + return cls.objects.get(pk=1) + except cls.DoesNotExist: + return None