diff options
Diffstat (limited to 'webapp/lib')
| -rw-r--r-- | webapp/lib/handle-realtime-event.ts | 231 | ||||
| -rw-r--r-- | webapp/lib/tool-templates.ts | 65 | ||||
| -rw-r--r-- | webapp/lib/twilio.ts | 13 | ||||
| -rw-r--r-- | webapp/lib/use-backend-tools.ts | 32 | ||||
| -rw-r--r-- | webapp/lib/utils.ts | 6 |
5 files changed, 347 insertions, 0 deletions
diff --git a/webapp/lib/handle-realtime-event.ts b/webapp/lib/handle-realtime-event.ts new file mode 100644 index 0000000..fb3d0bc --- /dev/null +++ b/webapp/lib/handle-realtime-event.ts @@ -0,0 +1,231 @@ +import { Item } from "@/components/types"; + +export default function handleRealtimeEvent( + ev: any, + setItems: React.Dispatch<React.SetStateAction<Item[]>> +) { + // Helper function to create a new item with default fields + function createNewItem(base: Partial<Item>): Item { + return { + object: "realtime.item", + timestamp: new Date().toLocaleTimeString(), + ...base, + } as Item; + } + + // Helper function to update an existing item if found by id, or add a new one if not. + // We can also pass partial updates to reduce repetitive code. + function updateOrAddItem(id: string, updates: Partial<Item>): void { + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === id); + if (idx >= 0) { + const updated = [...prev]; + updated[idx] = { ...updated[idx], ...updates }; + return updated; + } else { + return [...prev, createNewItem({ id, ...updates })]; + } + }); + } + + const { type } = ev; + + switch (type) { + case "session.created": { + // Starting a new session, clear all items + setItems([]); + break; + } + + case "input_audio_buffer.speech_started": { + // Create a user message item with running status and placeholder content + const { item_id } = ev; + setItems((prev) => [ + ...prev, + createNewItem({ + id: item_id, + type: "message", + role: "user", + content: [{ type: "text", text: "..." }], + status: "running", + }), + ]); + break; + } + + case "conversation.item.created": { + const { item } = ev; + if (item.type === "message") { + // A completed message from user or assistant + const updatedContent = + item.content && item.content.length > 0 ? item.content : []; + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === item.id); + if (idx >= 0) { + const updated = [...prev]; + updated[idx] = { + ...updated[idx], + ...item, + content: updatedContent, + status: "completed", + timestamp: + updated[idx].timestamp || new Date().toLocaleTimeString(), + }; + return updated; + } else { + return [ + ...prev, + createNewItem({ + ...item, + content: updatedContent, + status: "completed", + }), + ]; + } + }); + } + // NOTE: We no longer handle function_call items here. + // The handling of function_call items has been moved to the "response.output_item.done" event. + else if (item.type === "function_call_output") { + // Function call output item created + // Add the output item and mark the corresponding function_call as completed + // Also display in transcript as tool message with the response + setItems((prev) => { + const newItems = [ + ...prev, + createNewItem({ + ...item, + role: "tool", + content: [ + { + type: "text", + text: `Function call response: ${item.output}`, + }, + ], + status: "completed", + }), + ]; + + return newItems.map((m) => + m.call_id === item.call_id && m.type === "function_call" + ? { ...m, status: "completed" } + : m + ); + }); + } + break; + } + + case "conversation.item.input_audio_transcription.completed": { + // Update the user message with the final transcript + const { item_id, transcript } = ev; + setItems((prev) => + prev.map((m) => + m.id === item_id && m.type === "message" && m.role === "user" + ? { + ...m, + content: [{ type: "text", text: transcript }], + status: "completed", + } + : m + ) + ); + break; + } + + case "response.content_part.added": { + const { item_id, part, output_index } = ev; + // Append new content to the assistant message if output_index == 0 + if (part.type === "text" && output_index === 0) { + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === item_id); + if (idx >= 0) { + const updated = [...prev]; + const existingContent = updated[idx].content || []; + updated[idx] = { + ...updated[idx], + content: [ + ...existingContent, + { type: part.type, text: part.text }, + ], + }; + return updated; + } else { + // If the item doesn't exist yet, create it as a running assistant message + return [ + ...prev, + createNewItem({ + id: item_id, + type: "message", + role: "assistant", + content: [{ type: part.type, text: part.text }], + status: "running", + }), + ]; + } + }); + } + break; + } + + case "response.audio_transcript.delta": { + // Streaming transcript text (assistant) + const { item_id, delta, output_index } = ev; + if (output_index === 0 && delta) { + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === item_id); + if (idx >= 0) { + const updated = [...prev]; + const existingContent = updated[idx].content || []; + updated[idx] = { + ...updated[idx], + content: [...existingContent, { type: "text", text: delta }], + }; + return updated; + } else { + return [ + ...prev, + createNewItem({ + id: item_id, + type: "message", + role: "assistant", + content: [{ type: "text", text: delta }], + status: "running", + }), + ]; + } + }); + } + break; + } + + case "response.output_item.done": { + const { item } = ev; + if (item.type === "function_call") { + // A new function call item + // Display it in the transcript as an assistant message indicating a function is being requested + console.log("function_call", item); + setItems((prev) => [ + ...prev, + createNewItem({ + ...item, + role: "assistant", + content: [ + { + type: "text", + text: `${item.name}(${JSON.stringify( + JSON.parse(item.arguments) + )})`, + }, + ], + status: "running", + }), + ]); + } + break; + } + + default: + break; + } +} diff --git a/webapp/lib/tool-templates.ts b/webapp/lib/tool-templates.ts new file mode 100644 index 0000000..eedbfa9 --- /dev/null +++ b/webapp/lib/tool-templates.ts @@ -0,0 +1,65 @@ +export const toolTemplates = [ + { + name: "get_weather", + type: "function", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" }, + }, + }, + }, + { + name: "ping_no_args", + type: "function", + description: "A simple ping tool with no arguments", + parameters: { + type: "object", + properties: {}, + }, + }, + { + name: "get_user_nested_args", + type: "function", + description: "Fetch user profile by nested identifier", + parameters: { + type: "object", + properties: { + user: { + type: "object", + properties: { + id: { type: "string" }, + metadata: { + type: "object", + properties: { + region: { type: "string" }, + role: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + { + name: "calculate_route_more_properties", + type: "function", + description: "Calculate travel route with multiple parameters", + parameters: { + type: "object", + properties: { + start: { type: "string" }, + end: { type: "string" }, + mode: { type: "string", enum: ["car", "bike", "walk"] }, + options: { + type: "object", + properties: { + avoid_highways: { type: "boolean" }, + scenic_route: { type: "boolean" }, + }, + }, + }, + }, + }, +]; diff --git a/webapp/lib/twilio.ts b/webapp/lib/twilio.ts new file mode 100644 index 0000000..9ab6e77 --- /dev/null +++ b/webapp/lib/twilio.ts @@ -0,0 +1,13 @@ +import "server-only"; +import twilio from "twilio"; + +const { TWILIO_ACCOUNT_SID: accountSid, TWILIO_AUTH_TOKEN: authToken } = + process.env; + +if (!accountSid || !authToken) { + console.warn("Twilio credentials not set. Twilio client will be disabled."); +} + +export const twilioClient = + accountSid && authToken ? twilio(accountSid, authToken) : null; +export default twilioClient; diff --git a/webapp/lib/use-backend-tools.ts b/webapp/lib/use-backend-tools.ts new file mode 100644 index 0000000..c201163 --- /dev/null +++ b/webapp/lib/use-backend-tools.ts @@ -0,0 +1,32 @@ +import { useState, useEffect } from "react"; + +// Custom hook to fetch backend tools repeatedly +export function useBackendTools(url: string, intervalMs: number) { + const [tools, setTools] = useState<any[]>([]); + + useEffect(() => { + let isMounted = true; + + const fetchTools = () => { + fetch(url) + .then((res) => res.json()) + .then((data) => { + if (isMounted) setTools(data); + }) + .catch((error) => { + // On failure, we just let it retry after interval + console.error("Error fetching backend tools:", error); + }); + }; + + fetchTools(); + const intervalId = setInterval(fetchTools, intervalMs); + + return () => { + isMounted = false; + clearInterval(intervalId); + }; + }, [url, intervalMs]); + + return tools; +} diff --git a/webapp/lib/utils.ts b/webapp/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/webapp/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} |
