Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions client/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export default function App() {
const audioElement = useRef(null);

async function startSession() {
const bot = window.location.href.split('/')[4].split('?')[0].split('#')[0]
const api = bot ? `https://feedbot-${bot}-app.azurewebsites.net` : 'http://localhost:7071'

// Get an ephemeral key from the Fastify server
const tokenResponse = await fetch("/token");
const tokenResponse = await fetch(api+"/api/messages/realtime/token", {method: 'POST'});
const data = await tokenResponse.json();
const EPHEMERAL_KEY = data.client_secret.value;
const EPHEMERAL_KEY = data.value;

// Create a peer connection
const pc = new RTCPeerConnection();
Expand All @@ -39,9 +42,9 @@ export default function App() {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

const baseUrl = "https://api.openai.com/v1/realtime";
const model = "gpt-4o-realtime-preview-2024-12-17";
const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
const baseUrl = "https://api.openai.com/v1/realtime/calls";
const model = "gpt-realtime";
const sdpResponse = await fetch(`${baseUrl}?model=${model}`,{
method: "POST",
body: offer.sdp,
headers: {
Expand All @@ -56,6 +59,20 @@ export default function App() {
};
await pc.setRemoteDescription(answer);

const location = sdpResponse.headers.get("Location");
const callId = location?.split("/").pop();
console.log('callId', callId);

fetch(api+'/api/messages/realtime/calls', {
//fetch('https://feedbot-master-realtime-voice-app.azurewebsites.net/api/messages/realtime/calls?code=cb4ba4ab-93f9-4048-bd10-c4bda0b175d0', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': `Bearer ${EPHEMERAL_KEY}`},
body: JSON.stringify({call_id: callId})
})
.then(response => response.text())
.then(response => console.log('Bot hosting response', response))
.catch(err => console.error(err));

peerConnection.current = pc;
}

Expand Down Expand Up @@ -127,7 +144,7 @@ export default function App() {
<>
<nav className="absolute top-0 left-0 right-0 h-16 flex items-center">
<div className="flex items-center gap-4 w-full m-4 pb-2 border-0 border-b border-solid border-gray-200">
<img style={{ width: "24px" }} src={logo} />
<img style={{ width: "56px" }} src="https://feedyou.ai/wp-content/uploads/2022/02/Feedyou_logo_red_clean.svg" />
<h1>realtime console</h1>
</div>
</nav>
Expand All @@ -148,12 +165,12 @@ export default function App() {
</section>
</section>
<section className="absolute top-0 w-[380px] right-0 bottom-0 p-4 pt-0 overflow-y-auto">
<ToolPanel
{/*<ToolPanel
sendClientEvent={sendClientEvent}
sendTextMessage={sendTextMessage}
events={events}
isSessionActive={isSessionActive}
/>
/>*/}
</section>
</main>
</>
Expand Down
138 changes: 93 additions & 45 deletions client/components/ToolPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
import { useEffect, useState } from "react";

const functionDescription = `
Call this function when a user asks for a color palette.
`;

const sessionUpdate = {
type: "session.update",
session: {
instructions:
"Řekni přesně: 'Děkuji, zapsáno. Přeji pěkný den!'",
voice: "marin",
temperature: 0.6,
input_audio_transcription: {
model: "whisper-1",
},
tools: [
{
type: "function",
name: "display_color_palette",
description: functionDescription,
name: "get_appointment_slots",
description:
"Použij tuto funkci pro získání dostupných termínů, volitelně s filtrem daného dne.",
parameters: {
type: "object",
strict: true,
properties: {
theme: {
date: {
type: "string",
description: "Description of the theme for the color scheme.",
description: "Preferované datum pro schůzku",
},
colors: {
type: "array",
description: "Array of five hex color codes based on the theme.",
items: {
type: "string",
description: "Hex color code",
},
},
},
},
{
type: "function",
name: "create_appointment",
description:
"Použij tuto funkci pro založení schůzky, povinná je volba data+času a volitelně poznámka",
parameters: {
type: "object",
strict: true,
properties: {
dateTime: {
type: "string",
description: "Vybrané datum a čas schůzky",
},
note: {
type: "string",
description: "Volitelná poznámka",
},
},
required: ["theme", "colors"],
required: ["dateTime"],
},
},
],
tool_choice: "auto",
},
};

function FunctionCallOutput({ functionCallOutput }) {
const { theme, colors } = JSON.parse(functionCallOutput.arguments);

const colorBoxes = colors.map((color) => (
<div
key={color}
className="w-full h-16 rounded-md flex items-center justify-center border border-gray-200"
style={{ backgroundColor: color }}
>
<p className="text-sm font-bold text-black bg-slate-100 rounded-md p-2 border border-black">
{color}
</p>
</div>
));

function FunctionCallOutput({ functionCallOutputs }) {
return (
<div className="flex flex-col gap-2">
<p>Theme: {theme}</p>
{colorBoxes}
<pre className="text-xs bg-gray-100 rounded-md p-2 overflow-x-auto">
{JSON.stringify(functionCallOutput, null, 2)}
<ul>
{functionCallOutputs.map((output) => (
<li>{JSON.stringify(output, null, 2)}</li>
))}
</ul>
</pre>
</div>
);
Expand All @@ -69,14 +73,15 @@ export default function ToolPanel({
events,
}) {
const [functionAdded, setFunctionAdded] = useState(false);
const [functionCallOutput, setFunctionCallOutput] = useState(null);
const [functionCallOutputs, setFunctionCallOutputs] = useState([]);

useEffect(() => {
if (!events || events.length === 0) return;

const firstEvent = events[events.length - 1];
if (!functionAdded && firstEvent.type === "session.created") {
sendClientEvent(sessionUpdate);
sendClientEvent({ type: "response.create" });
setFunctionAdded(true);
}

Expand All @@ -86,19 +91,62 @@ export default function ToolPanel({
mostRecentEvent.response.output
) {
mostRecentEvent.response.output.forEach((output) => {
let args = {};
try {
args = JSON.parse(output.arguments || "{}");
} catch (e) {
console.warn("Failed to parse arguments", output);
}

if (
output.type === "function_call" &&
output.name === "get_appointment_slots"
) {
setFunctionCallOutputs([...functionCallOutputs, output]);
setTimeout(() => {
sendClientEvent({
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: output.call_id,
output:
`{"slots": ["${args.date}T10:00:00","${args.date}T14:30:00"]}`
/*!args.date || args.date === "2025-02-25"*/
/*true
? '{"slots": ["2025-02-2510:00:00","2025-02-25T14:30:00"]}'
: args.date === "2025-02-25" ? '{"slots": ["2025-02-2511:00:00","2025-02-25T15:30:00", "2025-02-25T17:00:00"]}' : '{"slots": []}',*/
},
});
sendClientEvent({
type: "response.create",
response: {
instructions:
"Dej uživateli na výběr jeden z navržených termínů [\"2025-02-2510:00:00\",\"2025-02-25T14:30:00\"]. Hodiny čti správně česky v prvním pádě např. 'v deset hodin', 've čtrnáct třicet' apod. Pokud nejsou žádné k dispozici tak požádej o jiný den případně o nejbližší možný. Nenebádej k výběru dní, pro které nemáš nalezené sloty.",
},
});
}, 200);
}

if (
output.type === "function_call" &&
output.name === "display_color_palette"
output.name === "create_appointment" &&
args.dateTime
) {
setFunctionCallOutput(output);
setFunctionCallOutputs([...functionCallOutputs, output]);
setTimeout(() => {
sendClientEvent({
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: output.call_id,
output: '{"selectedSlot": "' + args.dateTime + '"}',
},
});
sendClientEvent({
type: "response.create",
response: {
instructions: `
ask for feedback about the color palette - don't repeat
the colors, just ask if they like the colors.
`,
instructions:
"Finálně potvrď uživateli objednání na vybraný čas (čti česky v prvním pádě např. 'čtrnáct třicet') zeptej se jestli nechce něco přidat do poznámky. Pokud ano tak znovu zavolej funkci create_appointment, pokud ne tak se jen hezky rozluč.",
},
});
}, 500);
Expand All @@ -110,17 +158,17 @@ export default function ToolPanel({
useEffect(() => {
if (!isSessionActive) {
setFunctionAdded(false);
setFunctionCallOutput(null);
setFunctionCallOutputs([]);
}
}, [isSessionActive]);

return (
<section className="h-full w-full flex flex-col gap-4">
<div className="h-full bg-gray-50 rounded-md p-4">
<h2 className="text-lg font-bold">Color Palette Tool</h2>
<h2 className="text-lg font-bold">Tool outputs</h2>
{isSessionActive ? (
functionCallOutput ? (
<FunctionCallOutput functionCallOutput={functionCallOutput} />
functionCallOutputs.length > 0 ? (
<FunctionCallOutput functionCallOutputs={functionCallOutputs} />
) : (
<p>Ask for advice on a color palette...</p>
)
Expand Down
11 changes: 11 additions & 0 deletions client/entry-client.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import App from "./components/App";
import "./base.css";

ReactDOM.hydrateRoot(
document.getElementById("root"),
<StrictMode>
<App />
</StrictMode>,
);
10 changes: 5 additions & 5 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<html>
<head>
<meta charset="utf-8" />
<title>OpenAI Realtime Console</title>
<meta name="description" content="OpenAI Realtime Console" />
<meta name="author" content="OpenAI" />
<link rel="icon" href="/assets/openai-logomark.svg" />
<title>Feedyou Realtime Console</title>
<meta name="description" content="Feedyou Realtime Console" />
<meta name="author" content="Feedyou" />
<link rel="icon" href="https://feedyou.ai/wp-content/uploads/2022/02/Feedyou_logo_red_clean.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/base.css" />
<link
Expand All @@ -25,5 +25,5 @@
<div id="root"><!-- element --></div>
</body>
<!-- hydration -->
<script type="module" src="/:mount.js"></script>
<script type="module" src="./entry-client.jsx"></script>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"start": "node server.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client --ssrManifest",
"build:server": "vite build --outDir dist/server --ssr /index.js",
"build:server": "vite build --outDir dist/server --ssr ./server.js",
"devinstall": "zx ../../devinstall.mjs -- node server.js --dev",
"lint": "eslint . --ext .js,.jsx --fix"
},
Expand Down
35 changes: 25 additions & 10 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,34 @@ await server.vite.ready();

// Server-side API route to return an ephemeral realtime session token
server.get("/token", async () => {
const r = await fetch("https://api.openai.com/v1/realtime/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
const sessionConfig = JSON.stringify({
session: {
type: "realtime",
model: "gpt-realtime",
audio: {
input: {
turn_detection: { type: "semantic_vad", create_response: true }
},
output: {
voice: "marin",
},
},
tracing: "auto"
},
body: JSON.stringify({
model: "gpt-4o-realtime-preview-2024-12-17",
voice: "verse",
}),
});
const response = await fetch(
"https://api.openai.com/v1/realtime/client_secrets",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: sessionConfig,
}
);

return new Response(r.body, {
return new Response(response.body, {
status: 200,
headers: {
"Content-Type": "application/json",
Expand Down
5 changes: 3 additions & 2 deletions src/pages/token.json.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// WARN unused?
export async function GET() {
const r = await fetch("https://api.openai.com/v1/realtime/sessions", {
method: "POST",
Expand All @@ -6,8 +7,8 @@ export async function GET() {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-realtime-preview-2024-12-17",
voice: "verse",
model: "gpt-realtime",
voice: "marin"
}),
});

Expand Down
1 change: 1 addition & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import viteFastifyReact from "@fastify/react/plugin";
const path = fileURLToPath(import.meta.url);

export default {
base: 'https://feedyou.blob.core.windows.net/realtime-console',
root: join(dirname(path), "client"),
plugins: [viteReact(), viteFastifyReact()],
ssr: {
Expand Down