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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ integrations/
│ ├── cartesia/ # Cartesia integration
│ ├── cloudflare/ # Cloudflare integration
│ ├── crewai/ # CrewAI framework integration
│ ├── elevenlabs/ # ElevenLabs voice agent + browser agent
│ ├── langchain/ # LangChain framework integration
│ ├── logs/ # Logging utilities
│ ├── mastra/ # Mastra AI agent integration
Expand Down
5 changes: 5 additions & 0 deletions examples/integrations/elevenlabs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ANTHROPIC_API_KEY=
BROWSERBASE_API_KEY=
BROWSERBASE_PROJECT_ID=
NEXT_PUBLIC_ELEVENLABS_AGENT_ID=
BROWSE_BIN=browse
7 changes: 7 additions & 0 deletions examples/integrations/elevenlabs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.next
node_modules
.env
.env.local
.env.*.local
.DS_Store
tsconfig.tsbuildinfo
57 changes: 57 additions & 0 deletions examples/integrations/elevenlabs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ElevenLabs + Browserbase

Standalone prototype for a shared Browserbase session controlled by a local Claude-based browser controller, with ElevenLabs as the voice shell.

## Required environment variables

Create `.env.local` or `.env` with:

```bash
ANTHROPIC_API_KEY=
BROWSERBASE_API_KEY=
BROWSERBASE_PROJECT_ID=
NEXT_PUBLIC_ELEVENLABS_AGENT_ID=
```

Optional:

```bash
BROWSE_BIN=browse
```

## Run locally

```bash
pnpm install
pnpm dev
```

Open:

```text
http://127.0.0.1:3001
```

The controller expects the Browserbase Browse CLI to be installed and available on your `PATH`. If it is installed somewhere else, point `BROWSE_BIN` at it.

## ElevenLabs agent setup

The frontend registers one client tool:

- `control_demo`

Suggested tool description for your ElevenLabs agent:

> Use this tool whenever the user asks you to navigate, click, open, read, create, edit, or continue operating the live browser session. Pass one high-level instruction at a time.

Suggested system guidance:

> You are the voice interface for a live Browserbase browser controller. The browser controller owns all navigation, clicking, reading, and page state. Use `control_demo` once for each new browser instruction from the user. After `control_demo` returns `accepted`, `running`, `queued`, or `interrupting`, give at most one short acknowledgement, then wait for controller updates. Never ask "are you there" while the controller is busy. When the controller returns `completed`, answer concisely using the final summary and current page state. When the controller returns `blocked`, ask the clarification once instead of retrying the same tool call.

Do not pass this as a runtime prompt override unless that override is explicitly enabled in the ElevenLabs agent settings; otherwise the session may immediately disconnect.

## Controller model

The local controller uses Claude Agent SDK as a step planner pinned to `claude-opus-4-7`, while browser execution runs through the Browserbase `browse` CLI against the same persistent Browserbase session used for the live iframe.

That means the controller plans from `browse snapshot` output, clicks by stable refs like `@0-5`, and can follow tab changes through the CLI instead of relying on fuzzy Playwright text matching.
22 changes: 22 additions & 0 deletions examples/integrations/elevenlabs/app/api/demo/control/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { runDemoInstruction } from "../../../../lib/demo-controller";

export const runtime = "nodejs";

const bodySchema = z.object({
demoId: z.string().uuid(),
instruction: z.string().min(3),
interrupt: z.boolean().optional()
});

export async function POST(request: Request) {
try {
const parsed = bodySchema.parse(await request.json());
const snapshot = await runDemoInstruction(parsed);
return NextResponse.json(snapshot);
} catch (error) {
const message = error instanceof Error ? error.message : "Demo control failed.";
return NextResponse.json({ error: message }, { status: 500 });
}
}
15 changes: 15 additions & 0 deletions examples/integrations/elevenlabs/app/api/demo/session/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { getDemoSnapshot } from "../../../../lib/demo-controller";

export const runtime = "nodejs";

export async function GET(request: Request) {
const url = new URL(request.url);
const demoId = url.searchParams.get("demoId");

if (!demoId) {
return NextResponse.json({ error: "Missing demoId." }, { status: 400 });
}

return NextResponse.json(getDemoSnapshot(demoId));
}
48 changes: 48 additions & 0 deletions examples/integrations/elevenlabs/app/api/demo/stream/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { getDemoSnapshot, subscribeToDemo } from "../../../../lib/demo-controller";

export const runtime = "nodejs";

export async function GET(request: Request) {
const url = new URL(request.url);
const demoId = url.searchParams.get("demoId");

if (!demoId) {
return new Response("Missing demoId.", { status: 400 });
}

const encoder = new TextEncoder();

const stream = new ReadableStream<Uint8Array>({
start(controller) {
const sendSnapshot = (snapshot = getDemoSnapshot(demoId)) => {
controller.enqueue(
encoder.encode(`event: snapshot\ndata: ${JSON.stringify(snapshot)}\n\n`)
);
};

sendSnapshot();

const unsubscribe = subscribeToDemo(demoId, (snapshot) => {
sendSnapshot(snapshot);
});

const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode("event: ping\ndata: {}\n\n"));
}, 15000);

request.signal.addEventListener("abort", () => {
clearInterval(heartbeat);
unsubscribe();
controller.close();
});
}
});

return new Response(stream, {
headers: {
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Content-Type": "text/event-stream"
}
});
}
Loading