fix(chat): stabilize assistant add flow and location routing

This commit is contained in:
2026-03-10 15:59:07 +00:00
parent 49abfad192
commit 1ad9d20037
7 changed files with 60 additions and 9 deletions

View File

@@ -51,10 +51,11 @@ Run in order: format → lint → check → build.
- Use DaisyUI semantic colors/classes (`bg-primary`, `text-base-content`). - Use DaisyUI semantic colors/classes (`bg-primary`, `text-base-content`).
- Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG`. - Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG`.
- Chat model override: dropdown selector fed by `GET /api/chat/providers/{provider}/models/`; per-provider persistence via `localStorage` key `voyage_chat_model_prefs`; backend `send_message` accepts optional `model`. - Chat model override: dropdown selector fed by `GET /api/chat/providers/{provider}/models/`; per-provider persistence via `localStorage` key `voyage_chat_model_prefs`; backend `send_message` accepts optional `model`.
- Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides `get_trip_details`-first reasoning and confirms only before first `add_to_itinerary`. - Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides `get_trip_details`-first reasoning and confirms only before first `add_to_itinerary`; `search_places` prompt guard requires the LLM to have a concrete location string before calling the tool (asks clarifying question otherwise).
- Chat tool output: `role=tool` messages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload via `rebuildConversationMessages()`. - Chat tool output: `role=tool` messages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload via `rebuildConversationMessages()`.
- Chat errors: `_safe_error_payload()` maps LiteLLM exceptions to sanitized user-safe categories (never raw `exc.message`). - Chat errors: `_safe_error_payload()` maps LiteLLM exceptions to sanitized user-safe categories (never raw `exc.message`).
- Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history. - Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history.
- Geocoding: `background_geocode_and_assign()` runs in a thread after Location save; populates `region`, `city`, `country`, and also fills `Location.location` from reverse geocode `display_name` (truncated to field max_length) if blank or different.
- Chat agent tools (`get_trip_details`, `add_to_itinerary`) respect collection sharing — both owners and `shared_with` members can use them; `list_trips` remains owner-only. - Chat agent tools (`get_trip_details`, `add_to_itinerary`) respect collection sharing — both owners and `shared_with` members can use them; `list_trips` remains owner-only.
- Do **not** attempt to fix known test/config issues during feature work. - Do **not** attempt to fix known test/config issues during feature work.
- Commit and merge completed feature branches promptly once validation passes (avoid leaving finished work unmerged). - Commit and merge completed feature branches promptly once validation passes (avoid leaving finished work unmerged).

View File

@@ -67,10 +67,11 @@ Run in this order:
- Security: handle CSRF tokens via `/auth/csrf/` and `X-CSRFToken` - Security: handle CSRF tokens via `/auth/csrf/` and `X-CSRFToken`
- Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG` - Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG`
- Chat model override: dropdown selector fed by `GET /api/chat/providers/{provider}/models/`; persisted in `localStorage` key `voyage_chat_model_prefs`; backend accepts optional `model` param in `send_message` - Chat model override: dropdown selector fed by `GET /api/chat/providers/{provider}/models/`; persisted in `localStorage` key `voyage_chat_model_prefs`; backend accepts optional `model` param in `send_message`
- Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides `get_trip_details`-first reasoning and confirms only before first `add_to_itinerary` - Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides `get_trip_details`-first reasoning and confirms only before first `add_to_itinerary`; `search_places` prompt guard requires the LLM to have a concrete location string before calling the tool (asks clarifying question otherwise)
- Chat tool output: `role=tool` messages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload via `rebuildConversationMessages()` - Chat tool output: `role=tool` messages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload via `rebuildConversationMessages()`
- Chat error surfacing: `_safe_error_payload()` maps LiteLLM exceptions to sanitized user-safe categories (never forwards raw `exc.message`) - Chat error surfacing: `_safe_error_payload()` maps LiteLLM exceptions to sanitized user-safe categories (never forwards raw `exc.message`)
- Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history - Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history
- Geocoding: `background_geocode_and_assign()` runs in a thread after Location save; populates `region`, `city`, `country`, and also fills `Location.location` from reverse geocode `display_name` (truncated to field max_length) if blank or different
## Conventions ## Conventions
- Do **not** attempt to fix known test/configuration issues as part of feature work. - Do **not** attempt to fix known test/configuration issues as part of feature work.

View File

@@ -75,10 +75,11 @@ Run in this exact order:
- CSRF handling: use `/auth/csrf/` + `X-CSRFToken` - CSRF handling: use `/auth/csrf/` + `X-CSRFToken`
- Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG` - Chat providers: dynamic catalog from `GET /api/chat/providers/`; configured in `CHAT_PROVIDER_CONFIG`
- Chat model override: dropdown selector fed by `GET /api/chat/providers/{provider}/models/`; persisted in `localStorage` key `voyage_chat_model_prefs`; backend accepts optional `model` param in `send_message` - Chat model override: dropdown selector fed by `GET /api/chat/providers/{provider}/models/`; persisted in `localStorage` key `voyage_chat_model_prefs`; backend accepts optional `model` param in `send_message`
- Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides `get_trip_details`-first reasoning and confirms only before first `add_to_itinerary` - Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides `get_trip_details`-first reasoning and confirms only before first `add_to_itinerary`; `search_places` prompt guard requires the LLM to have a concrete location string before calling the tool (asks clarifying question otherwise)
- Chat tool output: `role=tool` messages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload via `rebuildConversationMessages()` - Chat tool output: `role=tool` messages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload via `rebuildConversationMessages()`
- Chat error surfacing: `_safe_error_payload()` maps LiteLLM exceptions to sanitized user-safe categories (never forwards raw `exc.message`) - Chat error surfacing: `_safe_error_payload()` maps LiteLLM exceptions to sanitized user-safe categories (never forwards raw `exc.message`)
- Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history - Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history
- Geocoding: `background_geocode_and_assign()` runs in a thread after Location save; populates `region`, `city`, `country`, and also fills `Location.location` from reverse geocode `display_name` (truncated to field max_length) if blank or different
## Conventions ## Conventions
- Do **not** attempt to fix known test/configuration issues as part of feature work. - Do **not** attempt to fix known test/configuration issues as part of feature work.

View File

@@ -49,9 +49,19 @@ def background_geocode_and_assign(location_id: str):
if country: if country:
location.country = country location.country = country
# Save updated location info update_fields = ["region", "city", "country"]
display_name = (result.get("display_name") or "").strip()
if display_name:
max_length = Location._meta.get_field("location").max_length
if max_length:
display_name = display_name[:max_length]
if location.location != display_name:
location.location = display_name
update_fields.append("location")
# Save updated location info, skip geocode threading # Save updated location info, skip geocode threading
location.save(update_fields=["region", "city", "country"], _skip_geocode=True) location.save(update_fields=update_fields, _skip_geocode=True)
except Exception as e: except Exception as e:
# Optional: log or print the error # Optional: log or print the error

View File

@@ -346,6 +346,7 @@ When chat context includes a trip collection:
- Treat context as itinerary-wide (potentially multiple stops), not a single destination - 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 - 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 - 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.""" Be conversational, helpful, and enthusiastic about travel. Keep responses concise but informative."""

View File

@@ -2,7 +2,7 @@
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { mdiSend, mdiPlus, mdiDelete, mdiMenu, mdiClose } from '@mdi/js'; import { mdiSend, mdiPlus, mdiDelete, mdiMenu, mdiClose } from '@mdi/js';
import type { ChatProviderCatalogEntry } from '$lib/types.js'; import type { ChatProviderCatalogEntry, CollectionItineraryItem, Location } from '$lib/types.js';
import { addToast } from '$lib/toasts'; import { addToast } from '$lib/toasts';
type ToolResultEntry = { type ToolResultEntry = {
@@ -82,7 +82,7 @@
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
close: void; close: void;
itemAdded: { locationId: string; date: string }; itemAdded: { location: Location; itineraryItem: CollectionItineraryItem; date: string };
}>(); }>();
const MODEL_PREFS_STORAGE_KEY = 'voyage_chat_model_prefs'; const MODEL_PREFS_STORAGE_KEY = 'voyage_chat_model_prefs';
@@ -620,7 +620,9 @@
throw new Error('Failed to add to itinerary'); throw new Error('Failed to add to itinerary');
} }
dispatch('itemAdded', { locationId: location.id, date }); const itineraryItem = await itineraryResponse.json();
dispatch('itemAdded', { location, itineraryItem, date });
addToast('success', $t('added_successfully')); addToast('success', $t('added_successfully'));
closeDateSelector(); closeDateSelector();
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { Collection, ContentImage, Location, Collaborator, Lodging } from '$lib/types'; import type {
Collection,
ContentImage,
Location,
Collaborator,
Lodging,
CollectionItineraryItem
} from '$lib/types';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@@ -90,6 +97,33 @@
collection = { ...collection }; // trigger reactivity so cost summary & UI refresh immediately collection = { ...collection }; // trigger reactivity so cost summary & UI refresh immediately
} }
type AssistantItemAddedDetail = {
location: Location;
itineraryItem: CollectionItineraryItem;
date: string;
};
function handleAssistantItemAdded(event: CustomEvent<AssistantItemAddedDetail>) {
const { location, itineraryItem } = event.detail;
upsertCollectionItem('locations', location);
if (!itineraryItem || itineraryItem.id === undefined || itineraryItem.id === null) {
return;
}
const items = collection.itinerary || [];
const exists = items.some((entry) => String(entry.id) === String(itineraryItem.id));
collection = {
...collection,
itinerary: exists
? items.map((entry) =>
String(entry.id) === String(itineraryItem.id) ? itineraryItem : entry
)
: [...items, itineraryItem]
};
}
// Helper to upload prefilled images (temp ids starting with 'rec-') sequentially // Helper to upload prefilled images (temp ids starting with 'rec-') sequentially
async function importPrefilledImagesForItem( async function importPrefilledImagesForItem(
item: any, item: any,
@@ -1314,6 +1348,7 @@
startDate={collection.start_date || undefined} startDate={collection.start_date || undefined}
endDate={collection.end_date || undefined} endDate={collection.end_date || undefined}
destination={collectionDestination} destination={collectionDestination}
on:itemAdded={handleAssistantItemAdded}
/> />
<CollectionRecommendationView bind:collection user={data.user} /> <CollectionRecommendationView bind:collection user={data.user} />
</div> </div>