fix(chat): stop 429 retry spiral and add get_weather coord fallback

- search_places: detect HTTP 429 and mark retryable=False to stop the
  retry loop immediately instead of spiraling until MAX_ITERATIONS
- get_weather: extract collection coordinates (lat/lng from first
  location with coords) and retry when LLM omits required params;
  uses sync_to_async for the DB query in the async view
- AITravelChat: deduplicate context-only tools (get_trip_details,
  get_weather) in the render pipeline to prevent duplicate place cards
  from appearing when the retry loop causes multiple get_trip_details calls
- Tests: 5 new tests covering 429 non-retryable path and weather
  coord fallback; all 39 chat tests pass
This commit is contained in:
2026-03-10 19:18:55 +00:00
parent de8625c17f
commit 635e0df0ab
4 changed files with 321 additions and 3 deletions

View File

@@ -348,7 +348,9 @@
return [...next, toolResult];
}
function uniqueToolResultsByCallId(toolResults: ToolResultEntry[] | undefined): ToolResultEntry[] {
function uniqueToolResultsByCallId(
toolResults: ToolResultEntry[] | undefined
): ToolResultEntry[] {
if (!toolResults) {
return [];
}
@@ -368,6 +370,24 @@
return unique;
}
// Context-loading tools that should render at most once per message, even if
// the retry loop caused the LLM to call them multiple times.
const CONTEXT_ONLY_TOOLS = new Set(['get_trip_details', 'get_weather']);
function deduplicateContextTools(toolResults: ToolResultEntry[]): ToolResultEntry[] {
const seenContextTool = new Set<string>();
return toolResults.filter((result) => {
const name = result.name;
if (name && CONTEXT_ONLY_TOOLS.has(name)) {
if (seenContextTool.has(name)) {
return false;
}
seenContextTool.add(name);
}
return true;
});
}
function rebuildConversationMessages(rawMessages: ChatMessage[]): ChatMessage[] {
const rebuilt = rawMessages.map((msg) => ({
...msg,
@@ -936,7 +956,7 @@
<div class="whitespace-pre-wrap">{msg.content}</div>
{#if msg.role === 'assistant' && msg.tool_results}
<div class="mt-2 space-y-2">
{#each uniqueToolResultsByCallId(msg.tool_results) as result}
{#each deduplicateContextTools(uniqueToolResultsByCallId(msg.tool_results)) as result}
{#if hasPlaceResults(result)}
<div class="grid gap-2">
{#each getPlaceResults(result) as place}