diff --git a/.env.example b/.env.example index 34728f0..845a302 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,13 @@ VITE_SUPABASE_ANON_KEY= # Geheimer Server-Schlüssel mit Schreibrechten # Nur für Backend-Skripte oder Edge Functions verwenden! -SUPABASE_SERVICE_ROLE_KEY= \ No newline at end of file +SUPABASE_SERVICE_ROLE_KEY= + +# Chat-Integration + +# Endpunkt für Chat-Completions +OPENAI_ENDPOINT=http://host.docker.internal:1234/api/v0/chat/completions +OPENAI_API_KEY=some-token +OPENAI_MODEL=google/gemma-3-1b +OPENAI_SYSTEM_PROMPT="Du bist ein Baum, dessen einziger Zweck während der Interaktion ist, die Informationen über dich auf spielerische Art und Weise wiederzugeben. Gib kurze, präzise Antworten. Wenn du die Antwort nicht kennst, dann antworte mit \"Ich weiß es nicht.\" Antworte immer auf Deutsch." +OPENAI_FIRST_USER_PROMPT="Stell dich vor." diff --git a/frontend/src/components/chat/BotMessage.svelte b/frontend/src/components/chat/BotMessage.svelte index f874d8a..a135037 100644 --- a/frontend/src/components/chat/BotMessage.svelte +++ b/frontend/src/components/chat/BotMessage.svelte @@ -26,12 +26,12 @@ let lastMessageText = ''; let selectedLabel: string | null = null; - $: if (message?.text && message.text !== lastMessageText) { - const current = message.text; + $: if (message?.content && message.content !== lastMessageText) { + const current = message.content; lastMessageText = current; parseMarkdown(current).then((result) => { - if (message.text === current) htmlText = result; + if (message.content === current) htmlText = result; }); } diff --git a/frontend/src/components/chat/Chat.svelte b/frontend/src/components/chat/Chat.svelte index 0718177..c65cb1a 100644 --- a/frontend/src/components/chat/Chat.svelte +++ b/frontend/src/components/chat/Chat.svelte @@ -3,7 +3,7 @@ import { onMount } from 'svelte'; import { supabase } from '$lib/supabase'; import Message from './Message.svelte'; - import type { Message as MessageType, RawMessage } from '$types/chat'; + import type { Message as MessageType } from '$types/chat'; // === Props === export let treeId: string = ''; @@ -11,7 +11,6 @@ console.log('Chat got Tree ID: ', treeId); // === State === - let sessionId: string = ''; let messages: MessageType[] = []; let newMessage: string = ''; let chatAvailable: boolean = true; @@ -47,46 +46,35 @@ } const { data, error } = response as { data: any; error: any }; + const jsonData = JSON.parse(data); if (error !== null) { console.error('Error fetching chat messages:', error); return; } - sessionId = data.sessionId; messages = [ ...messages, - ...data.messages - .filter((msg: RawMessage) => !['no-reply', 'path'].includes(msg.type)) - .map((msg: RawMessage): MessageType => { - const buttons = Array.isArray(msg.payload?.buttons) - ? msg.payload!.buttons!.map((btn: { name: string; request: any }) => ({ - label: btn.name, - request: btn.request - })) - : []; - - return { - text: msg.payload?.message ?? '', - label: '', - type: msg.payload?.type ?? msg.type, - sender: 'bot', - buttons, - ai: msg.payload?.ai === true - }; - }) + ...jsonData.messages.map((msg: MessageType): MessageType => { + return { + content: msg.content, + type: 'text', + role: 'assistant', + ai: true + }; + }) ]; }; - function sendMessage(text: string) { - if (text === '') { + function sendMessage(content: string) { + if (content === '') { return; } const newUserMessage: MessageType = { - text, - label: '', + content, type: 'text', - sender: 'user' + role: 'user', + ai: false }; messages = [...messages, newUserMessage]; @@ -94,8 +82,8 @@ supabase.functions .invoke('chat', { body: { - sessionId, - text + treeId, + messages } }) .then(handleNewChatMessages); @@ -116,8 +104,6 @@ }); } } - - $: console.log('↪ newMessage:', JSON.stringify(newMessage)); diff --git a/frontend/src/components/chat/Message.svelte b/frontend/src/components/chat/Message.svelte index 890f0d2..fac29b8 100644 --- a/frontend/src/components/chat/Message.svelte +++ b/frontend/src/components/chat/Message.svelte @@ -8,9 +8,9 @@
- {#if message.sender === 'bot'} - - {:else if message.sender === 'user'} + {#if message.role === 'assistant'} + + {:else if message.role === 'user'} {/if}
diff --git a/frontend/src/components/chat/UserMessage.svelte b/frontend/src/components/chat/UserMessage.svelte index 67bd808..948deeb 100644 --- a/frontend/src/components/chat/UserMessage.svelte +++ b/frontend/src/components/chat/UserMessage.svelte @@ -7,10 +7,10 @@
- {message.text} + {message.content}
- Bot + Bot
-
\ No newline at end of file + diff --git a/frontend/src/types/chat.ts b/frontend/src/types/chat.ts index f92ef0c..b018fd4 100644 --- a/frontend/src/types/chat.ts +++ b/frontend/src/types/chat.ts @@ -1,32 +1,16 @@ // src/lib/types.ts -export interface ChatButton { - label: string; - request: { - type: string; - payload?: any; - }; -} - export interface Message { - text: string; - label: string; + content: string; type: 'text' | 'choice'; - sender: 'bot' | 'user'; + role: 'assistant' | 'user' | 'system'; ai?: boolean; - buttons?: ChatButton[]; -} - -export interface RawMessage { - time: number; - type: string; - payload: any; } // /** // * RawMessage entspricht exakt dem Nachrichtenformat, // * das von der Voiceflow Runtime API zurückgegeben wird. -// * +// * // * Dieses Format enthält alle Typen (text, choice, no-reply, etc.) // * sowie optionale Zusatzinfos wie ai, delay, slate usw. // */ @@ -35,12 +19,12 @@ export interface RawMessage { // * Der Typ der Nachricht (z. B. "text", "choice", "visual", "no-reply") // */ // type: string; - + // /** // * Zeitstempel der Nachricht (in ms seit Unix-Epoch) // */ // time: number; - + // /** // * Nutzdaten der Nachricht – Struktur hängt vom Typ ab // */ @@ -49,23 +33,23 @@ export interface RawMessage { // * Der eigentliche Nachrichtentext (bei Typ "text") // */ // message?: string; - + // /** // * Ob die Nachricht als KI-generiert markiert ist // */ // ai?: boolean; - + // /** // * Optional: Zeitverzögerung vor Anzeige der Nachricht // */ // delay?: number; - + // /** // * Strukturierte Textrepräsentation (Slate.js-kompatibel), // * meist nur bei Typ "text" enthalten // */ // slate?: unknown; - + // /** // * Nur bei "choice": eine Liste auswählbarer Buttons // */ @@ -81,21 +65,20 @@ export interface RawMessage { // }; // }; // }[]; - + // /** // * Nur bei "visual": URL eines Bildes (z. B. zur Anzeige im Chat) // */ // image?: string; - + // /** // * Nur bei "no-reply": Zeit bis Timeout // */ // timeout?: number; - + // /** // * Nur bei "path": enthält den nächsten internen Pfadnamen // */ // path?: string; // }; // } - \ No newline at end of file diff --git a/supabase/functions/chat/index.ts b/supabase/functions/chat/index.ts index 7768d74..98a8615 100644 --- a/supabase/functions/chat/index.ts +++ b/supabase/functions/chat/index.ts @@ -7,33 +7,36 @@ const supabase = createClient( Deno.env.get("VITE_SUPABASE_ANON_KEY") ); -// Initial request could be: -// POST /functions/v1/chat -// { -// treeId: "123" -// } +const getSystemPrompt = async (treeId: string) => { + const { data, error } = await supabase + .from("trees") + .select("*") + .eq("uuid", treeId) + .single(); -// Response could be: -// { -// sessionId: "456", -// response: { -// text: "Hello, how can I help you today?" -// } -// } - -// Subsequent requests could be: -// POST /functions/v1/chat -// { -// sessionId: "456", -// text: "I want to know more about the product" -// } + if (error) { + console.error("Error fetching system prompt:", error); + return Deno.env.get("OPENAI_SYSTEM_PROMPT")!; + } -// Response could be: -// { -// sessionId: "456", -// response: { -// text: "The product is a great product" -// } + return ( + Deno.env.get("OPENAI_SYSTEM_PROMPT")! + + "\n\n" + + "Here are some information about you: " + + "Botanischer Name: " + + data.tree_type_botanic + + ", " + + "Deutscher Name: " + + data.tree_type_german + + ", Stammdurchmesser: " + + data.trunk_diameter + + "cm, Höhe: " + + data.height + + "m, Kronendurchmesser: " + + data.crown_diameter + + "m" + ); +}; Deno.serve(async (req) => { if (req.method === "OPTIONS") { @@ -42,128 +45,52 @@ Deno.serve(async (req) => { }); } - // Get the request body - // - specifically tree id, session id, and text - const { treeId, sessionId, text } = await req.json(); - - // If session id is not present - if (!sessionId) { - // - generate session id - const newSessionId = crypto.randomUUID(); - console.log("[BB] New session id:", newSessionId); - - // - get tree information - const treeData = await supabase - .from("trees") - .select("*") - .eq("uuid", treeId); - if (treeData.error) { - console.error("[BB] Error fetching tree data:", treeData.error); - } else { - console.log("[BB] Found Tree data!"); - } - - // - send request to configure variables in VoiceFlow - const variableResponse = await fetch( - `https://general-runtime.voiceflow.com/state/user/${newSessionId}/variables?logs=off`, - { - method: "PATCH", - headers: { - "content-type": "application/json", - authorization: Deno.env.get("VOICEFLOW_API_KEY"), - }, - // TODO: Variablennamen in VoiceFlow anpassen auf neues Datenformat aus Supabase. - body: JSON.stringify({ - baum_oid: treeId, - baumart: treeData.data[0].tree_type_german, - baumhoehe: treeData.data[0].height, - kronendurchmesser: treeData.data[0].crown_diameter, - }), - } - ); - if (variableResponse.error) { - console.error("[VF] Error configuring variables:"); - } else { - console.log("[VF] Variables configured successfully!"); - } - - // - start conversation by sending launch action - const startConversationResponse = await fetch( - `https://general-runtime.voiceflow.com/state/user/${newSessionId}/interact?logs=off`, - { - method: "POST", - headers: { - "content-type": "application/json", - authorization: Deno.env.get("VOICEFLOW_API_KEY"), - }, - body: JSON.stringify({ - action: { - type: "launch", - }, - config: { - tts: false, - stripSSML: true, - stopAll: true, - excludeTypes: ["block", "debug", "flow"], - }, - }), - } - ); - - let returnedMessages = await startConversationResponse.json(); - if (startConversationResponse.status !== 200) { - console.error("[VF] Error starting conversation"); - } else { - console.log("[VF] Conversation started successfully!"); - } + const requestData = await req.json(); + const treeId: string = requestData.treeId; + const messages: Array<{ role: string; content: string }> = + requestData.messages || []; - // - send response back to the user with session id - return new Response( - JSON.stringify({ - sessionId: newSessionId, - messages: returnedMessages, - }), - { - headers: { - ...corsHeaders, - "content-type": "application/json", - }, - status: 200, - } - ); + const treeData = await supabase.from("trees").select("*").eq("uuid", treeId); + if (treeData.error) { + console.error("[BB] Error fetching tree data:", treeData.error); } else { - // If session id is present + console.log("[BB] Found Tree data!"); + } - // - send request to VoiceFlow with session id and text - const updateConversationResponse = await fetch( - `https://general-runtime.voiceflow.com/state/user/${sessionId}/interact?logs=off`, - { - method: "POST", - headers: { - "content-type": "application/json", - authorization: Deno.env.get("VOICEFLOW_API_KEY"), + const llmResponse = await fetch(Deno.env.get("OPENAI_ENDPOINT")!, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Deno.env.get("OPENAI_API_KEY")}`, + }, + body: JSON.stringify({ + model: Deno.env.get("OPENAI_MODEL"), + messages: [ + { + role: "system", + content: await getSystemPrompt(treeId), }, - body: JSON.stringify({ - action: { - type: "text", - payload: text, - }, - }), - } - ); + { + role: "user", + content: Deno.env.get("OPENAI_FIRST_USER_PROMPT")!, + }, + ...messages, + ], + }), + }); + const llmData = await llmResponse.json(); - // - send response back to the user with session id - return new Response( - JSON.stringify({ - sessionId, - messages: await updateConversationResponse.json(), - }), - { - headers: { - ...corsHeaders, - "content-type": "application/json", + return new Response( + JSON.stringify({ + messages: [ + { + role: llmData.choices[0].message.role, + content: llmData.choices[0].message.content, }, - } - ); - } + ], + }), + { + headers: { ...corsHeaders }, + } + ); });