Skip to content
Draft
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
2 changes: 2 additions & 0 deletions examples/realtime-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ This example is a small [Vite](https://vitejs.dev/) application showcasing the r
```
4. Open the printed localhost URL and paste the key when prompted.

When connected the demo lists the hosted MCP tools (`deepwiki` and `dnd`) that are also available in the Next.js example.

Use `pnpm -F realtime-demo build` to create a production build.
7 changes: 7 additions & 0 deletions examples/realtime-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ <h1 class="text-2xl font-bold">Realtime Agent Demo</h1>
</button>
</div>
</header>
<aside
class="flex-none w-full p-3 border border-zinc-200 rounded-md bg-white/60 text-xs mb-4"
>
<h2 class="text-sm font-semibold mb-2">Available MCP Tools</h2>
<p id="mcpToolsEmptyState" class="text-xs text-gray-500">None</p>
<ul class="list-disc pl-4 space-y-1" id="mcpToolsList"></ul>
</aside>
<h2 class="font-bold uppercase text-xs text-gray-500 mb-2">
Raw Event Log
</h2>
Expand Down
4 changes: 3 additions & 1 deletion examples/realtime-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
"vite": "^6.3.5"
},
"dependencies": {
"@openai/agents-core": "workspace:*",
"@openai/agents-realtime": "workspace:*",
"@tailwindcss/vite": "^4.1.7",
"tailwindcss": "^4.1.7"
"tailwindcss": "^4.1.7",
"zod": "^3.25.40"
}
}
133 changes: 114 additions & 19 deletions examples/realtime-demo/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,144 @@
// @ts-expect-error Typescript doesn't know about the css module
// @ts-expect-error Typescript doesn't know about the css module.
import './style.css';
import {
connectButton,
disconnectButton,
log,
muteButton,
setMcpTools,
setButtonStates,
} from './utils';

import { z } from 'zod';
import { RealtimeAgent, RealtimeSession, tool } from '@openai/agents-realtime';

const getWeather = tool({
name: 'getWeather',
description: 'Get the weather for a given city',
parameters: z.object({
city: z.string(),
}),
execute: async ({ city }) => {
return `The weather in ${city} is sunny`;
import type {
RealtimeContextData,
RealtimeItem,
TransportEvent,
} from '@openai/agents-realtime';
import {
RealtimeAgent,
RealtimeSession,
backgroundResult,
tool,
} from '@openai/agents-realtime';
import { hostedMcpTool } from '@openai/agents-core';
import type { RunContext, RunToolApprovalItem } from '@openai/agents-core';

setMcpTools([]);

const refundParameters = z.object({
request: z.string(),
});

const refundBackchannel = tool<typeof refundParameters, RealtimeContextData>({
name: 'Refund Expert',
description: 'Evaluate a refund request and provide guidance.',
parameters: refundParameters,
execute: async (
{ request }: z.infer<typeof refundParameters>,
details: RunContext<RealtimeContextData> | undefined,
) => {
const history: RealtimeItem[] = details?.context?.history ?? [];
return backgroundResult(
[
'Refund request received.',
`Request: ${request}`,
`Previous conversation turns: ${history.length}.`,
'In this demo, responses are generated locally without contacting a backend service.',
].join('\n'),
);
},
});

const weatherParameters = z.object({
location: z.string(),
});

const weatherTool = tool({
name: 'weather',
description: 'Get the weather in a given location.',
parameters: weatherParameters,
execute: async ({ location }: z.infer<typeof weatherParameters>) => {
return backgroundResult(`The weather in ${location} is sunny.`);
},
});

const secretParameters = z.object({
question: z.string(),
});

const secretTool = tool({
name: 'secret',
description: 'A secret tool to tell the special number.',
parameters: secretParameters,
execute: async ({ question }: z.infer<typeof secretParameters>) => {
return `The answer to ${question} is 42.`;
},
needsApproval: true,
});

const weatherAgent = new RealtimeAgent({
name: 'Weather Agent',
instructions: 'You are a weather expert.',
handoffDescription: 'You can handoff to the weather agent if you need to.',
tools: [getWeather],
name: 'Weather Expert',
instructions:
'You are a weather expert. You are able to answer questions about the weather.',
tools: [weatherTool],
});

const agent = new RealtimeAgent({
name: 'Greeter',
instructions:
'You are a greeter. Always greet the user with a "top of the morning"',
'You are a friendly assistant. When you use a tool always first say what you are about to do.',
tools: [
refundBackchannel,
secretTool,
hostedMcpTool({
serverLabel: 'deepwiki',
}),
hostedMcpTool({
serverLabel: 'dnd',
}),
],
handoffs: [weatherAgent],
});

weatherAgent.handoffs.push(agent);

const session = new RealtimeSession(agent);
const session = new RealtimeSession(agent, {
model: 'gpt-realtime',
config: {
audio: {
output: {
voice: 'cedar',
},
},
},
});

session.on('transport_event', (event) => {
// this logs the events coming directly from the Realtime API server
session.on('transport_event', (event: TransportEvent) => {
// This logs the events coming directly from the Realtime API server.
log(event);
});

session.on('mcp_tools_changed', (tools: Array<{ name: string }>) => {
setMcpTools(tools.map((tool) => tool.name));
});

session.on('tool_approval_requested', (_context, _agent, approvalRequest) => {
const approvalItem = approvalRequest.approvalItem as RunToolApprovalItem;
const parameters =
typeof approvalItem.rawItem.arguments === 'string'
? approvalItem.rawItem.arguments
: JSON.stringify(approvalItem.rawItem.arguments, null, 2);
const approved = confirm(
`Approve tool call to ${approvalItem.rawItem.name} with parameters:\n${parameters}`,
);
if (approved) {
session.approve(approvalItem);
} else {
session.reject(approvalItem);
}
});

connectButton.addEventListener('click', async () => {
const apiKey = prompt(
'Enter ephemeral API key. Run `pnpm -F realtime-demo generate-token` to get a token.',
Expand All @@ -61,6 +155,7 @@ connectButton.addEventListener('click', async () => {
disconnectButton.addEventListener('click', () => {
session.close();
setButtonStates('disconnected');
setMcpTools([]);
});

muteButton.addEventListener('click', () => {
Expand Down
20 changes: 20 additions & 0 deletions examples/realtime-demo/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,23 @@ export function setButtonStates(newState: ButtonState) {
connectButton.style.display = 'block';
}
}

const mcpToolsList = document.querySelector<HTMLUListElement>('#mcpToolsList')!;
const mcpToolsEmptyState = document.querySelector<HTMLParagraphElement>(
'#mcpToolsEmptyState',
)!;

export function setMcpTools(toolNames: string[]) {
mcpToolsList.innerHTML = '';
if (toolNames.length === 0) {
mcpToolsEmptyState.style.display = 'block';
return;
}
mcpToolsEmptyState.style.display = 'none';
for (const name of toolNames) {
const item = document.createElement('li');
item.className = 'text-xs';
item.innerText = name;
mcpToolsList.appendChild(item);
}
}
25 changes: 25 additions & 0 deletions examples/realtime-demo/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const rootDir = fileURLToPath(new URL('.', import.meta.url));

export default defineConfig({
plugins: [tailwindcss()],
resolve: {
alias: {
'@openai/agents-core/_shims': path.resolve(
rootDir,
'../../packages/agents-core/dist/shims/shims-browser.js',
),
'@openai/agents-realtime/_shims': path.resolve(
rootDir,
'../../packages/agents-realtime/dist/shims/shims-browser.js',
),
'@openai/agents-realtime': path.resolve(
rootDir,
'../../packages/agents-realtime/dist',
),
'@openai/agents-core': path.resolve(
rootDir,
'../../packages/agents-core/dist',
),
},
},
});
13 changes: 7 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.