fix(chat): stabilize assistant add flow and location routing
This commit is contained in:
@@ -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).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user