summaryrefslogtreecommitdiff
path: root/webapp/lib
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/lib')
-rw-r--r--webapp/lib/handle-realtime-event.ts231
-rw-r--r--webapp/lib/tool-templates.ts65
-rw-r--r--webapp/lib/twilio.ts13
-rw-r--r--webapp/lib/use-backend-tools.ts32
-rw-r--r--webapp/lib/utils.ts6
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))
+}