Implement a full chat-based travel agent using LiteLLM for multi-provider LLM support (OpenAI, Anthropic, Gemini, Ollama, Groq, Mistral, etc.). Backend: - New 'chat' Django app with ChatConversation and ChatMessage models - Streaming SSE endpoint via StreamingHttpResponse - 5 agent tools: search_places, list_trips, get_trip_details, add_to_itinerary, get_weather - LiteLLM client wrapper with per-user API key retrieval - System prompt with user preference context injection Frontend: - New /chat route with full-page chat UI (DaisyUI + Tailwind) - Collapsible conversation sidebar with CRUD - SSE streaming response display with tool call visualization - Provider selector dropdown - SSE proxy fix to stream text/event-stream without buffering - Navbar link and i18n keys
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|
import { fetchCSRFToken } from '$lib/index.server';
|
|
import { json } from '@sveltejs/kit';
|
|
|
|
/** @type {import('./$types').RequestHandler} */
|
|
export async function GET(event) {
|
|
const { url, params, request, fetch, cookies } = event;
|
|
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
|
return handleRequest(url, params, request, fetch, cookies, searchParam);
|
|
}
|
|
|
|
/** @type {import('./$types').RequestHandler} */
|
|
export async function POST({ url, params, request, fetch, cookies }) {
|
|
const searchParam = url.search ? `${url.search}` : '';
|
|
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
|
}
|
|
|
|
export async function PATCH({ url, params, request, fetch, cookies }) {
|
|
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
|
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
|
}
|
|
|
|
export async function PUT({ url, params, request, fetch, cookies }) {
|
|
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
|
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
|
}
|
|
|
|
export async function DELETE({ url, params, request, fetch, cookies }) {
|
|
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
|
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
|
}
|
|
|
|
async function handleRequest(
|
|
url: any,
|
|
params: any,
|
|
request: any,
|
|
fetch: any,
|
|
cookies: any,
|
|
searchParam: string,
|
|
requreTrailingSlash: boolean | undefined = false
|
|
) {
|
|
const path = params.path;
|
|
let targetUrl = `${endpoint}/api/${path}`;
|
|
|
|
// Ensure the path ends with a trailing slash
|
|
if (requreTrailingSlash && !targetUrl.endsWith('/')) {
|
|
targetUrl += '/';
|
|
}
|
|
|
|
// Append query parameters to the path correctly
|
|
targetUrl += searchParam; // This will add ?format=json or &format=json to the URL
|
|
|
|
const headers = new Headers(request.headers);
|
|
|
|
// Delete existing csrf cookie by setting an expired date
|
|
cookies.delete('csrftoken', { path: '/' });
|
|
|
|
// Generate a new csrf token (using your existing fetchCSRFToken function)
|
|
const csrfToken = await fetchCSRFToken();
|
|
if (!csrfToken) {
|
|
return json({ error: 'CSRF token is missing or invalid' }, { status: 400 });
|
|
}
|
|
|
|
// Set the new csrf token in both headers and cookies
|
|
const sessionId = cookies.get('sessionid');
|
|
const cookieHeader = `csrftoken=${csrfToken}` + (sessionId ? `; sessionid=${sessionId}` : '');
|
|
|
|
try {
|
|
const response = await fetch(targetUrl, {
|
|
method: request.method,
|
|
headers: {
|
|
...Object.fromEntries(headers),
|
|
'X-CSRFToken': csrfToken,
|
|
Cookie: cookieHeader
|
|
},
|
|
body:
|
|
request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined,
|
|
credentials: 'include' // This line ensures cookies are sent with the request
|
|
});
|
|
|
|
if (response.status === 204) {
|
|
return new Response(null, {
|
|
status: 204,
|
|
headers: response.headers
|
|
});
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || '';
|
|
const cleanHeaders = new Headers(response.headers);
|
|
cleanHeaders.delete('set-cookie');
|
|
|
|
// Stream SSE responses through without buffering
|
|
if (contentType.includes('text/event-stream')) {
|
|
return new Response(response.body, {
|
|
status: response.status,
|
|
headers: cleanHeaders
|
|
});
|
|
}
|
|
|
|
const responseData = await response.arrayBuffer();
|
|
|
|
return new Response(responseData, {
|
|
status: response.status,
|
|
headers: cleanHeaders
|
|
});
|
|
} catch (error) {
|
|
console.error('Error forwarding request:', error);
|
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
|
}
|
|
}
|