@pinecall/skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/build.mjs +204 -0
- package/package.json +29 -0
- package/skills/pinecall-concepts/SKILL.md +41 -0
- package/skills/pinecall-concepts/references/concepts/agents-and-channels.md +155 -0
- package/skills/pinecall-concepts/references/concepts/deployment-topologies.md +120 -0
- package/skills/pinecall-concepts/references/concepts/hot-reload.md +119 -0
- package/skills/pinecall-concepts/references/concepts/philosophy.md +100 -0
- package/skills/pinecall-concepts/references/concepts/server-vs-client-llm.md +119 -0
- package/skills/pinecall-examples/SKILL.md +59 -0
- package/skills/pinecall-examples/references/examples/browser-widget.md +206 -0
- package/skills/pinecall-examples/references/examples/chat-bot.md +184 -0
- package/skills/pinecall-examples/references/examples/headless-agent.md +121 -0
- package/skills/pinecall-examples/references/examples/index.md +183 -0
- package/skills/pinecall-examples/references/examples/multi-channel-bot.md +173 -0
- package/skills/pinecall-examples/references/examples/outbound-dispatch.md +109 -0
- package/skills/pinecall-examples/references/examples/turn-detection.md +150 -0
- package/skills/pinecall-guides/SKILL.md +68 -0
- package/skills/pinecall-guides/references/guides/call-ringing.md +149 -0
- package/skills/pinecall-guides/references/guides/conversation-history.md +377 -0
- package/skills/pinecall-guides/references/guides/dev-mode.md +130 -0
- package/skills/pinecall-guides/references/guides/events.md +677 -0
- package/skills/pinecall-guides/references/guides/human-takeover.md +184 -0
- package/skills/pinecall-guides/references/guides/inbound-voice.md +201 -0
- package/skills/pinecall-guides/references/guides/knowledge-bases.md +166 -0
- package/skills/pinecall-guides/references/guides/live-listening.md +199 -0
- package/skills/pinecall-guides/references/guides/multi-tenant.md +158 -0
- package/skills/pinecall-guides/references/guides/outbound-calls.md +279 -0
- package/skills/pinecall-guides/references/guides/sse-streaming.md +207 -0
- package/skills/pinecall-guides/references/guides/testing-agents.md +272 -0
- package/skills/pinecall-guides/references/guides/tools-and-functions.md +254 -0
- package/skills/pinecall-guides/references/guides/webrtc-browser.md +200 -0
- package/skills/pinecall-guides/references/guides/whatsapp.md +370 -0
- package/skills/pinecall-guides/references/guides/ws-streaming.md +235 -0
- package/skills/pinecall-quickstart/SKILL.md +54 -0
- package/skills/pinecall-quickstart/references/index.md +123 -0
- package/skills/pinecall-quickstart/references/quickstart.md +185 -0
- package/skills/pinecall-reference/SKILL.md +43 -0
- package/skills/pinecall-reference/references/reference/cli.md +578 -0
- package/skills/pinecall-reference/references/reference/events.md +366 -0
- package/skills/pinecall-reference/references/reference/llm-providers.md +263 -0
- package/skills/pinecall-reference/references/reference/rest-api.md +122 -0
- package/skills/pinecall-reference/references/reference/session-limits.md +119 -0
- package/skills/pinecall-reference/references/reference/stt-providers.md +174 -0
- package/skills/pinecall-reference/references/reference/tts-providers.md +149 -0
- package/skills/pinecall-sdk-api/SKILL.md +56 -0
- package/skills/pinecall-sdk-api/references/api/agent.md +328 -0
- package/skills/pinecall-sdk-api/references/api/call.md +324 -0
- package/skills/pinecall-sdk-api/references/api/pinecall.md +186 -0
- package/skills/pinecall-sdk-api/references/api/reply-stream.md +148 -0
- package/skills/pinecall-security/SKILL.md +37 -0
- package/skills/pinecall-security/references/security.md +138 -0
- package/skills/pinecall-web-chat/SKILL.md +38 -0
- package/skills/pinecall-web-chat/references/web/chat/chat-session.md +178 -0
- package/skills/pinecall-web-chat/references/web/chat/overview.md +98 -0
- package/skills/pinecall-web-components/SKILL.md +37 -0
- package/skills/pinecall-web-components/references/web/components/overview.md +128 -0
- package/skills/pinecall-web-voice/SKILL.md +40 -0
- package/skills/pinecall-web-voice/references/web/core/datachannel-protocol.md +149 -0
- package/skills/pinecall-web-voice/references/web/core/overview.md +70 -0
- package/skills/pinecall-web-voice/references/web/core/state-and-phases.md +153 -0
- package/skills/pinecall-web-voice/references/web/core/voice-session.md +279 -0
- package/skills/pinecall-web-widget/SKILL.md +41 -0
- package/skills/pinecall-web-widget/references/web/widget/overview.md +67 -0
- package/skills/pinecall-web-widget/references/web/widget/props.md +291 -0
- package/skills/pinecall-web-widget/references/web/widget/theming.md +131 -0
- package/skills/pinecall-web-widget/references/web/widget/tools-api.md +381 -0
- package/skills/pinecall-web-widget/references/web/widget/use-voice-session-hook.md +130 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Tools and Functions"
|
|
3
|
+
description: "Let your agent take actions: look up data, transfer calls, book appointments."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tools and Functions
|
|
7
|
+
|
|
8
|
+
Tools are how your agent moves beyond conversation into action: looking up an order, checking inventory, booking a slot, transferring to a human. In Pinecall, tools are **local functions in your process**, not webhooks.
|
|
9
|
+
|
|
10
|
+
## Defining tools
|
|
11
|
+
|
|
12
|
+
Use the `tool()` helper with a Zod schema. Tools are auto-executed by the SDK when the LLM calls them — no manual event handler needed.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { Pinecall, tool } from "@pinecall/sdk";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
const lookupOrder = tool({
|
|
19
|
+
name: "lookupOrder",
|
|
20
|
+
description: "Look up an order by its ID.",
|
|
21
|
+
schema: z.object({
|
|
22
|
+
orderId: z.string().describe("The order ID, like ORD-12345"),
|
|
23
|
+
}),
|
|
24
|
+
execute: async ({ orderId }) => {
|
|
25
|
+
return await db.orders.findOne(orderId);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const scheduleCallback = tool({
|
|
30
|
+
name: "scheduleCallback",
|
|
31
|
+
description: "Schedule a callback for a specific date and time.",
|
|
32
|
+
schema: z.object({
|
|
33
|
+
datetime: z.string().describe("ISO 8601 datetime"),
|
|
34
|
+
reason: z.string(),
|
|
35
|
+
}),
|
|
36
|
+
execute: async ({ datetime, reason }, call) => {
|
|
37
|
+
return await scheduler.book({
|
|
38
|
+
phone: call.from,
|
|
39
|
+
datetime,
|
|
40
|
+
reason,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const agent = pc.agent("support", {
|
|
46
|
+
prompt: "You are a helpful support agent. Use tools to look up information.",
|
|
47
|
+
llm: "openai/gpt-5-chat-latest",
|
|
48
|
+
voice: "elevenlabs/sarah",
|
|
49
|
+
stt: "deepgram/flux",
|
|
50
|
+
language: "en",
|
|
51
|
+
tools: [lookupOrder, scheduleCallback],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
agent.on("call.started", (call) => call.say("Hi, how can I help?"));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That's it. When the LLM decides to call `lookupOrder`, the SDK:
|
|
58
|
+
|
|
59
|
+
1. Parses the arguments through `z.object({ orderId: z.string() })`
|
|
60
|
+
2. Calls your `execute` function with the validated args + the `Call` object
|
|
61
|
+
3. Sends the result back to the LLM via `call.toolResult()`
|
|
62
|
+
|
|
63
|
+
## Tool call lifecycle
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
## Ephemeral tools (don't persist the result)
|
|
68
|
+
|
|
69
|
+
By default every tool result is saved to the conversation history — it stays in
|
|
70
|
+
the LLM context for the rest of the call and is written to the persisted
|
|
71
|
+
transcript. That's almost always what you want.
|
|
72
|
+
|
|
73
|
+
Sometimes it isn't. A tool might return a sensitive lookup (a full customer
|
|
74
|
+
record, a one-time code) or a large/noisy payload (a 5 KB JSON blob) that you
|
|
75
|
+
need *for the current reply* but don't want lingering in context or saved to the
|
|
76
|
+
database. Mark such a tool `ephemeral: true`:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const lookupSSN = tool({
|
|
80
|
+
name: "lookupSSN",
|
|
81
|
+
description: "Look up the caller's SSN to verify identity.",
|
|
82
|
+
schema: z.object({ customerId: z.string() }),
|
|
83
|
+
ephemeral: true, // result is used for this reply, then dropped from history
|
|
84
|
+
execute: async ({ customerId }) => ({ ssn: await db.getSSN(customerId) }),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
How it works: the result is still sent to the model so it can generate the
|
|
89
|
+
current reply (the API requires every tool call to be followed by its result).
|
|
90
|
+
But once that reply is produced, the server **prunes** the ephemeral result —
|
|
91
|
+
and the originating `tool_calls` entry if all of its calls were ephemeral — from
|
|
92
|
+
the history. It never reaches the next turn's context and is never written to
|
|
93
|
+
the saved transcript. The behavior is identical across voice, chat, and
|
|
94
|
+
WhatsApp.
|
|
95
|
+
|
|
96
|
+
`ephemeral` defaults to `false`, so existing tools are unchanged.
|
|
97
|
+
|
|
98
|
+
## The `call` parameter
|
|
99
|
+
|
|
100
|
+
Every `execute` function receives the `Call` object as its second argument. Use it to interact with the call mid-tool-execution:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const transferToHuman = tool({
|
|
104
|
+
name: "transferToHuman",
|
|
105
|
+
description: "Escalate to a human agent.",
|
|
106
|
+
schema: z.object({
|
|
107
|
+
department: z.enum(["sales", "support", "billing"]),
|
|
108
|
+
}),
|
|
109
|
+
execute: async ({ department }, call) => {
|
|
110
|
+
const numbers = {
|
|
111
|
+
sales: "+15551110000",
|
|
112
|
+
support: "+15551110001",
|
|
113
|
+
billing: "+15551110002",
|
|
114
|
+
};
|
|
115
|
+
call.say("Of course, let me connect you to a specialist.");
|
|
116
|
+
call.forward(numbers[department]);
|
|
117
|
+
return { transferred: true };
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Why local functions beat webhooks
|
|
123
|
+
|
|
124
|
+
Other platforms make tools webhook URLs. You define a tool, expose a public endpoint, the platform POSTs to it. The downsides pile up fast:
|
|
125
|
+
|
|
126
|
+
- **You expose a public endpoint** — attack surface, rate limiting, auth headaches
|
|
127
|
+
- **You can't reach internal services** — your DB, your Redis, your hardware
|
|
128
|
+
- **Latency** — every tool call is a network roundtrip across the public internet
|
|
129
|
+
- **Debuggability** — tool call goes out, response comes back, what happened in between?
|
|
130
|
+
|
|
131
|
+
Pinecall tools run in your process. That means:
|
|
132
|
+
|
|
133
|
+
- `await db.query(...)` works directly
|
|
134
|
+
- `await redis.get(...)` works directly
|
|
135
|
+
- `await hardware.openDoor()` works directly (if your process can reach it)
|
|
136
|
+
- Stack traces, breakpoints, and logs work normally
|
|
137
|
+
- No public surface to attack
|
|
138
|
+
- Sub-millisecond "call" overhead — it's a function call, not an HTTP request
|
|
139
|
+
|
|
140
|
+
## Common patterns
|
|
141
|
+
|
|
142
|
+
### Database lookups
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const findCustomer = tool({
|
|
146
|
+
name: "findCustomer",
|
|
147
|
+
description: "Find a customer by phone number or email.",
|
|
148
|
+
schema: z.object({
|
|
149
|
+
query: z.string().describe("Phone or email"),
|
|
150
|
+
}),
|
|
151
|
+
execute: async ({ query }) => {
|
|
152
|
+
const customer = await db.customers.find({
|
|
153
|
+
or: [{ phone: query }, { email: query }],
|
|
154
|
+
});
|
|
155
|
+
return customer ?? { error: "not_found" };
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Transfer to human
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const transferToHuman = tool({
|
|
164
|
+
name: "transferToHuman",
|
|
165
|
+
description: "Escalate to a human agent when the customer is angry or has a complex issue.",
|
|
166
|
+
schema: z.object({
|
|
167
|
+
department: z.enum(["sales", "support", "billing"]),
|
|
168
|
+
}),
|
|
169
|
+
execute: async ({ department }, call) => {
|
|
170
|
+
const numbers = { sales: "+15551110000", support: "+15551110001", billing: "+15551110002" };
|
|
171
|
+
call.say("Of course, let me connect you to a specialist.");
|
|
172
|
+
call.forward(numbers[department]);
|
|
173
|
+
return { transferred: true };
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Booking / scheduling
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const bookAppointment = tool({
|
|
182
|
+
name: "bookAppointment",
|
|
183
|
+
description: "Book an appointment in the doctor's calendar.",
|
|
184
|
+
schema: z.object({
|
|
185
|
+
datetime: z.string().describe("ISO 8601 datetime"),
|
|
186
|
+
durationMinutes: z.number(),
|
|
187
|
+
patientName: z.string(),
|
|
188
|
+
}),
|
|
189
|
+
execute: async ({ datetime, durationMinutes, patientName }) => {
|
|
190
|
+
const slot = await calendar.book({
|
|
191
|
+
start: new Date(datetime),
|
|
192
|
+
duration: durationMinutes,
|
|
193
|
+
patient: patientName,
|
|
194
|
+
});
|
|
195
|
+
return slot.success
|
|
196
|
+
? { booked: true, confirmationId: slot.id }
|
|
197
|
+
: { booked: false, error: slot.conflictReason };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### End the call
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const endCall = tool({
|
|
206
|
+
name: "endCall",
|
|
207
|
+
description: "End the call when the customer says goodbye.",
|
|
208
|
+
schema: z.object({}),
|
|
209
|
+
execute: async (_, call) => {
|
|
210
|
+
call.say("Have a great day!");
|
|
211
|
+
call.once("bot.finished", () => call.hangup());
|
|
212
|
+
return { ended: true };
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Returning errors
|
|
218
|
+
|
|
219
|
+
If a tool call fails, the SDK catches the error and returns `{ error: err.message }` to the LLM automatically. The LLM can then recover (apologize, retry, ask clarifying questions).
|
|
220
|
+
|
|
221
|
+
You can also return errors explicitly:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const lookupOrder = tool({
|
|
225
|
+
name: "lookupOrder",
|
|
226
|
+
description: "Look up an order by ID.",
|
|
227
|
+
schema: z.object({ orderId: z.string() }),
|
|
228
|
+
execute: async ({ orderId }) => {
|
|
229
|
+
const order = await db.orders.findOne(orderId);
|
|
230
|
+
if (!order) return { error: "Order not found" };
|
|
231
|
+
return order;
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Listening to tool calls (optional)
|
|
237
|
+
|
|
238
|
+
The `llm.toolCall` event still fires for every tool call — useful for logging, metrics, or UI:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
agent.on("llm.toolCall", (data, call) => {
|
|
242
|
+
console.log(`Tools called: ${data.toolCalls.map(t => t.name).join(", ")}`);
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Tools work across all channels
|
|
247
|
+
|
|
248
|
+
The same tools work for phone, WebRTC, chat, and WhatsApp. The `Call` object is your interface regardless of transport.
|
|
249
|
+
|
|
250
|
+
## What's next
|
|
251
|
+
|
|
252
|
+
- [Hot-reload](/concepts/hot-reload) — change the prompt or tools mid-call
|
|
253
|
+
- [Events reference](/reference/events) — all events including `llm.toolCall`
|
|
254
|
+
- [`Call` API reference](/api/call) — `forward`, `hangup`, etc.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "WebRTC in the Browser"
|
|
3
|
+
description: "Embed a Pinecall voice agent in your web app using the React widget."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WebRTC in the Browser
|
|
7
|
+
|
|
8
|
+
Browser users can talk to your agent directly through WebRTC — no phone number required. This is how voice copilots, in-app assistants, and live demos work.
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
The browser connects **directly** to `voice.pinecall.io` over WebRTC. Your backend's only job is minting short-lived tokens.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
Your backend never proxies audio. The audio path is browser ↔ voice server, peer-to-peer over WebRTC.
|
|
17
|
+
|
|
18
|
+
## 1. Create the agent
|
|
19
|
+
|
|
20
|
+
WebRTC works automatically for any agent — no channel declaration needed.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Pinecall } from "@pinecall/sdk";
|
|
24
|
+
|
|
25
|
+
const pc = new Pinecall({ apiKey: process.env.PINECALL_API_KEY! });
|
|
26
|
+
|
|
27
|
+
const mara = pc.agent("mara", {
|
|
28
|
+
prompt: "You are Mara. Be concise and warm.",
|
|
29
|
+
llm: "openai/gpt-5-chat-latest",
|
|
30
|
+
voice: "elevenlabs/sarah",
|
|
31
|
+
stt: "deepgram/flux",
|
|
32
|
+
language: "es",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
mara.on("call.started", (call) => call.say("¡Hola!"));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 2. Mint tokens from your backend
|
|
39
|
+
|
|
40
|
+
Your token endpoint should be behind your existing auth (session cookie, JWT, OAuth — whatever you use). The endpoint calls `createToken()` and returns the result.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Express
|
|
44
|
+
app.get("/api/token", authMiddleware, async (req, res) => {
|
|
45
|
+
const token = await mara.createToken("webrtc");
|
|
46
|
+
res.json(token);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Next.js App Router
|
|
52
|
+
export async function GET() {
|
|
53
|
+
const session = await getSession();
|
|
54
|
+
if (!session) return new Response("Unauthorized", { status: 401 });
|
|
55
|
+
const token = await mara.createToken("webrtc");
|
|
56
|
+
return Response.json(token);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The response shape:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"token": "wrtc_abc123...",
|
|
65
|
+
"server": "wss://voice.pinecall.io",
|
|
66
|
+
"expiresIn": 60
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Tokens are single-use, scoped to the agent, and expire in 60 seconds. See [Security](/security) for the full security model.
|
|
71
|
+
|
|
72
|
+
## 3. Drop in the widget
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install @pinecall/web
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { VoiceWidget } from "@pinecall/web";
|
|
80
|
+
|
|
81
|
+
export default function App() {
|
|
82
|
+
return (
|
|
83
|
+
<VoiceWidget
|
|
84
|
+
agent="mara"
|
|
85
|
+
tokenProvider={async () => {
|
|
86
|
+
const res = await fetch("/api/token", { credentials: "include" });
|
|
87
|
+
return res.json();
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
That's the entire frontend. Click the orb, talk, listen.
|
|
95
|
+
|
|
96
|
+
## Listening for events in the browser
|
|
97
|
+
|
|
98
|
+
Events arrive over the WebRTC DataChannel — you don't need SSE for in-browser UIs. The widget renders its own transcript, and exposes session status plus the full live state via the `useVoice()` hook:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { VoiceWidget, useVoice } from "@pinecall/web";
|
|
102
|
+
|
|
103
|
+
function Transcript() {
|
|
104
|
+
const { messages, status } = useVoice();
|
|
105
|
+
return messages.map((m) => <p key={m.id}>{m.role}: {m.text}</p>);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function App() {
|
|
109
|
+
return (
|
|
110
|
+
<VoiceWidget
|
|
111
|
+
agent="mara"
|
|
112
|
+
tokenProvider={getToken}
|
|
113
|
+
onStatusChange={(status) => console.log("Status:", status)}
|
|
114
|
+
>
|
|
115
|
+
<Transcript />
|
|
116
|
+
</VoiceWidget>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For lower-level control, use `@pinecall/web/core` directly — it gives you the raw event stream.
|
|
122
|
+
|
|
123
|
+
## Custom UI without the widget
|
|
124
|
+
|
|
125
|
+
If the widget doesn't fit your design, build your own UI with `@pinecall/web/core`:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { VoiceSession } from "@pinecall/web/core";
|
|
129
|
+
|
|
130
|
+
const session = new VoiceSession({
|
|
131
|
+
agent: "mara",
|
|
132
|
+
// Fetch the token from your backend instead of hitting the voice server directly
|
|
133
|
+
tokenProvider: () => fetch("/api/token").then((r) => r.json()),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Re-render whenever the session state changes (messages, status, phase, …)
|
|
137
|
+
session.subscribe(() => {
|
|
138
|
+
const { status, messages } = session.getState();
|
|
139
|
+
console.log("Status:", status, "Last:", messages.at(-1)?.text);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// connect() fetches the token (via tokenProvider) and negotiates WebRTC
|
|
143
|
+
await session.connect();
|
|
144
|
+
|
|
145
|
+
// User clicks "End"
|
|
146
|
+
session.disconnect();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Skipping the backend for demos
|
|
150
|
+
|
|
151
|
+
For pure demos or prototypes — no backend, no auth — you can opt in to public token access using `allowedOrigins`:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const demo = pc.agent("demo-bot", {
|
|
155
|
+
// ...config
|
|
156
|
+
allowedOrigins: [
|
|
157
|
+
"https://demo.mysite.com",
|
|
158
|
+
"https://*.mysite.com",
|
|
159
|
+
"http://localhost:*",
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Then the widget can fetch tokens directly from the voice server, no backend needed — omit `tokenProvider` and it hits `/webrtc/token` directly:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
<VoiceWidget agent="demo-bot" />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> **Warning:** `allowedOrigins` protects against casual embedding but not against a determined attacker (Origin headers can be spoofed from scripts/curl). For production, always use `tokenProvider` with your backend's auth. See [Security](/security).
|
|
171
|
+
|
|
172
|
+
## Chat channel (text only)
|
|
173
|
+
|
|
174
|
+
Same pattern, different token type. Chat gives you typed conversations without audio:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Backend
|
|
178
|
+
app.get("/api/chat-token", authMiddleware, async (req, res) => {
|
|
179
|
+
const token = await agent.createToken("chat");
|
|
180
|
+
res.json(token);
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Connect from the browser via WebSocket:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const ws = new WebSocket(`${server}/ws?token=${token}`);
|
|
188
|
+
ws.onmessage = (e) => {
|
|
189
|
+
const event = JSON.parse(e.data);
|
|
190
|
+
if (event.event === "chat.token") appendBotToken(event.text); // streaming token
|
|
191
|
+
if (event.event === "chat.done") finishBotMessage(event.text); // final text
|
|
192
|
+
};
|
|
193
|
+
ws.send(JSON.stringify({ event: "message", text: "Hello" }));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## What's next
|
|
197
|
+
|
|
198
|
+
- [Security](/security) — the full token security model
|
|
199
|
+
- [Multi-tenant](/guides/multi-tenant) — scope tokens per user/tenant
|
|
200
|
+
- [Dev mode](/guides/dev-mode) — slug-based isolation lets every dev have their own agent
|