@helpai/elements 1.9.0 → 1.11.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/docs/README.md +1 -0
- package/docs/analytics.md +27 -0
- package/docs/api-reference.md +69 -2
- package/docs/configuration.md +31 -0
- package/docs/events-and-hooks.md +13 -11
- package/docs/frameworks.md +7 -1
- package/docs/page-tools.md +194 -0
- package/docs/security-privacy.md +64 -3
- package/elements-web-component.esm.js +39 -39
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +39 -39
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +39 -39
- package/elements.esm.js.map +4 -4
- package/elements.js +39 -39
- package/elements.js.map +4 -4
- package/index.d.ts +108 -3
- package/index.mjs +14 -14
- package/jsx-react.d.ts +2 -1
- package/jsx-svelte.d.ts +2 -1
- package/jsx-vue.d.ts +3 -2
- package/llms-full.txt +541 -147
- package/llms.txt +22 -18
- package/package.json +1 -1
- package/react.d.ts +124 -3
- package/react.mjs +32 -32
- package/style.css +1 -1
- package/web-component.d.ts +109 -3
- package/web-component.mjs +14 -14
package/docs/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Get the public key and deployment ID from [help.ai](https://help.ai) →
|
|
|
41
41
|
| Wire GA4 / Mixpanel / Segment / multi-provider | [Analytics integration](./analytics.md) |
|
|
42
42
|
| Integrate with Next.js / Vue / Svelte / WordPress / Shopify | [Framework integrations](./frameworks.md) |
|
|
43
43
|
| Copy a working example | [Recipes](./recipes.md) |
|
|
44
|
+
| Let the AI agent act on my page | [Page tools](./page-tools.md) |
|
|
44
45
|
| Diagnose "why isn't this working" | [Troubleshooting](./troubleshooting.md) |
|
|
45
46
|
| Quick answer to a common question | [FAQ](./faq.md) |
|
|
46
47
|
| Compact API reference for lookup | [API reference](./api-reference.md) |
|
package/docs/analytics.md
CHANGED
|
@@ -78,6 +78,7 @@ via `chat.on(name, fn)` (instance) or `HelpAI.hook(widgetId, name, fn)`
|
|
|
78
78
|
| | `soundToggle` | `{ muted }` |
|
|
79
79
|
| | `sidebarToggle` | `{ collapsed }` (page mode) |
|
|
80
80
|
| | `calloutDismiss` | `void` |
|
|
81
|
+
| **Your page** | `hostEvent` | `{ name, data? }` (from `chat.track()`) |
|
|
81
82
|
| **Lifecycle** | `error` | `unknown` (any unhandled error) |
|
|
82
83
|
| | `handshake` | `HandshakeResponse` (full handshake payload) |
|
|
83
84
|
| | `conversationRebound` | `{ previous, current }` |
|
|
@@ -392,6 +393,32 @@ chat.on('close', () => {
|
|
|
392
393
|
|
|
393
394
|
## Common patterns
|
|
394
395
|
|
|
396
|
+
### Your own events (`chat.track`)
|
|
397
|
+
|
|
398
|
+
Report what the visitor does on your site, outside the chat, with `chat.track(name, data)`:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
chat.track('plan_viewed', { plan: 'pro' });
|
|
402
|
+
chat.track('cart_updated', { items: 2, total: 48.5 });
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Each call does two things:
|
|
406
|
+
|
|
407
|
+
- **Analytics.** It sends one hit to help.ai's Elements analytics as a `hostEvent`, with your event name and data,
|
|
408
|
+
like the widget's own events. A deployment with tracking off (or a browser sending Global Privacy Control or Do
|
|
409
|
+
Not Track) sends nothing.
|
|
410
|
+
- **The agent.** The widget keeps your last 10 events and sends them with the visitor's next message, as
|
|
411
|
+
`recentEvents` in the page context (`{ name, data?, secondsAgo }`, oldest first), so an answer can take into
|
|
412
|
+
account what the visitor just did: "I see you were looking at the Pro plan." Like the rest of the page context, the
|
|
413
|
+
agent treats them as information from your site, never as instructions. They live only in this page view.
|
|
414
|
+
|
|
415
|
+
The name is 1–64 characters. The data is flat: strings, numbers and booleans (other values are left out), up to 256
|
|
416
|
+
characters as JSON (longer data is not sent; the name still is). Nothing personal should go in either: use ids and
|
|
417
|
+
plan names, not emails. `track()` also fires the `hostEvent` event, so your own listeners see it.
|
|
418
|
+
|
|
419
|
+
Also `HelpAI('track', name, data)` with the script tag, `element.track(name, data)` on `<web-ai-chat>`, and
|
|
420
|
+
`useHelpAI().track` in React.
|
|
421
|
+
|
|
395
422
|
### Identify the visitor
|
|
396
423
|
|
|
397
424
|
The widget mints a stable anonymous `visitorId` on first load and
|
package/docs/api-reference.md
CHANGED
|
@@ -46,7 +46,8 @@ React kit (React 18+; everything above plus the component and the hook, so impor
|
|
|
46
46
|
```ts
|
|
47
47
|
import {
|
|
48
48
|
HelpAI, // <HelpAI {...options} /> mounts the widget, re-configures it on prop changes, removes it on unmount
|
|
49
|
-
useHelpAI, // useHelpAI(widgetId?) → { state, ask, open, close, reset }, re-rendering on state changes
|
|
49
|
+
useHelpAI, // useHelpAI(widgetId?) → { state, ask, open, close, reset, track }, re-rendering on state changes
|
|
50
|
+
useHelpAITool, // useHelpAITool(tool) gives the agent a page tool while the component is mounted
|
|
50
51
|
type HelpAIProps,
|
|
51
52
|
type HelpAIControls,
|
|
52
53
|
} from '@helpai/elements/react';
|
|
@@ -139,13 +140,16 @@ interface ChatWidgetInstance {
|
|
|
139
140
|
|
|
140
141
|
// Lifecycle
|
|
141
142
|
reset(): void; // start over as a new anonymous visitor (on logout)
|
|
143
|
+
login(userToken: UserTokenSource): void; // sign the visitor in as your logged-in user (a signed token, or a function returning one)
|
|
144
|
+
track(name: string, data?: Record<string, string | number | boolean>): void; // report your page's own event: analytics + the agent's next message
|
|
145
|
+
registerTool(tool: ModelContextTool, options?: { signal?: AbortSignal }): void; // a tool the agent can use on this page, for every widget on it
|
|
142
146
|
destroy(): void;
|
|
143
147
|
|
|
144
148
|
// Events
|
|
145
149
|
on<E extends EventName>(name: E, fn: (payload: EventMap[E]) => void): () => void; // returns unsubscribe fn
|
|
146
150
|
|
|
147
151
|
// Re-config
|
|
148
|
-
update(patch: Partial<ChatWidgetOptions>): void;
|
|
152
|
+
update(patch: Partial<ChatWidgetOptions>): void; // e.g. update({ tools }) on a route change
|
|
149
153
|
|
|
150
154
|
// Host-asserted context (informational, not auth; re-handshakes, same visitor/conversation)
|
|
151
155
|
setUserContext(ctx: Partial<NonNullable<ChatWidgetOptions['userContext']>>): void; // merges into the current user context
|
|
@@ -207,6 +211,29 @@ init({
|
|
|
207
211
|
The complete type also exposes `features`, `forms`, `modules`, and `tracking`.
|
|
208
212
|
See [Configuration](./configuration.md) for behavior and examples.
|
|
209
213
|
|
|
214
|
+
### `ModelContextTool`
|
|
215
|
+
|
|
216
|
+
A tool the agent can use on your page, in the WebMCP tool shape ([Page tools](./page-tools.md)). On `<web-ai-chat>`
|
|
217
|
+
it's the `tools` property or `registerTool()`, since functions can't be attributes.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
interface ModelContextTool<TInput = unknown> {
|
|
221
|
+
name: string; // letters, digits, `_` and `-` (58 at most; others become `_`); the agent sees `site__<name>`
|
|
222
|
+
title?: string;
|
|
223
|
+
description: string; // when to use it (1024 characters at most)
|
|
224
|
+
inputSchema?: Record<string, unknown>; // JSON Schema of an object; none = no input
|
|
225
|
+
annotations?: ToolAnnotations; // `consequentialHint: true` = the visitor approves each call first
|
|
226
|
+
execute(input: TInput, options: { signal: AbortSignal }): unknown; // a JSON result, or throw; `signal` aborts when the visitor moves on
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface ToolAnnotations {
|
|
230
|
+
readOnlyHint?: boolean;
|
|
231
|
+
untrustedContentHint?: boolean;
|
|
232
|
+
consequentialHint?: boolean;
|
|
233
|
+
debugging?: boolean;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
210
237
|
### `ActionName`
|
|
211
238
|
|
|
212
239
|
```ts
|
|
@@ -332,6 +359,46 @@ visitor, their current conversation (draft and queued messages included), prefer
|
|
|
332
359
|
`setUserContext`, then reconnects as a first visit. Listeners added with `on()` stay, and the handle keeps working.
|
|
333
360
|
On `<web-ai-chat>`, also remove a `user-context` attribute you set.
|
|
334
361
|
|
|
362
|
+
### `login(userToken)`
|
|
363
|
+
|
|
364
|
+
Signs the visitor in as your logged-in user. `userToken` is a JWT your server signs with the deployment's secret key,
|
|
365
|
+
or a function returning a fresh one (called again when the server rejects the current token as expired). The widget
|
|
366
|
+
reconnects as that user: their conversations on this browser move to them, and they reach their conversations on any
|
|
367
|
+
device where they're signed in. Call `reset()` when they sign out; switching to another user's token starts over the
|
|
368
|
+
same way. Also `HelpAI('login', userToken)`, `element.login(userToken)` on `<web-ai-chat>` (not an attribute, so the
|
|
369
|
+
token stays out of the DOM), the `userToken` option at `init()`, and `<HelpAI userToken={…} />`. Signing and the
|
|
370
|
+
guarantees: [Security → Verified users](./security-privacy.md#verified-users-usertoken).
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
chat.login(() => fetch('/api/help-ai-token').then((res) => res.text()));
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### `registerTool(tool, options?)`
|
|
379
|
+
|
|
380
|
+
Gives the agent a tool on this page, for every widget on it: the latest registration of a name wins, and aborting
|
|
381
|
+
`options.signal` removes it. Also `HelpAI('registerTool', tool)` (queued before the script loads),
|
|
382
|
+
`element.registerTool(tool)` and the React kit's `useHelpAITool(tool)`. Details: [Page tools](./page-tools.md).
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
chat.registerTool({ name: 'open_booking', description: 'Open the booking form. Use it whenever the visitor wants to book a call.', execute: () => bookingForm.open() });
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### `track(name, data?)`
|
|
391
|
+
|
|
392
|
+
Reports something the visitor did on your site, outside the chat. It sends a `hostEvent` hit to Elements analytics
|
|
393
|
+
(not when tracking is off), and the widget keeps the last 10 for the agent, which reads them with the visitor's next
|
|
394
|
+
message. The name is 1–64 characters; the data is flat (strings, numbers, booleans; 256 characters of JSON at most).
|
|
395
|
+
Also `HelpAI('track', name, data)`, `element.track(name, data)` and `useHelpAI().track`. Details:
|
|
396
|
+
[Analytics → Your own events](./analytics.md#your-own-events-chattrack).
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
chat.track('plan_viewed', { plan: 'pro' });
|
|
400
|
+
```
|
|
401
|
+
|
|
335
402
|
---
|
|
336
403
|
|
|
337
404
|
## Events
|
package/docs/configuration.md
CHANGED
|
@@ -102,6 +102,37 @@ Override if you use:
|
|
|
102
102
|
- A custom domain (`your-company.com`).
|
|
103
103
|
- A local dev backend.
|
|
104
104
|
|
|
105
|
+
### `userToken`
|
|
106
|
+
|
|
107
|
+
`string | () => string | Promise<string>` · default: none (anonymous)
|
|
108
|
+
|
|
109
|
+
Signs the visitor in as your logged-in user: a JWT your server signs with the deployment's secret key, or a function
|
|
110
|
+
returning a fresh one. Their conversations then follow them to any device, and the AI gets their name and email as
|
|
111
|
+
verified facts. Runtime-only: not an attribute or `data-*` option, so the token stays out of the page's markup. See
|
|
112
|
+
[Security → Verified users](./security-privacy.md#verified-users-usertoken) for signing it.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', userToken: () => fetch('/api/help-ai-token').then((res) => res.text()) });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `tools`
|
|
119
|
+
|
|
120
|
+
`ModelContextTool[]` · default: none
|
|
121
|
+
|
|
122
|
+
Tools the AI agent can use on your page, in the WebMCP tool shape: a name, a description, a JSON Schema of its input
|
|
123
|
+
and an `execute` function the widget runs in the visitor's browser when the agent calls it
|
|
124
|
+
(`annotations: { consequentialHint: true }` asks the visitor first). Runtime-only, like `userToken`;
|
|
125
|
+
`chat.update({ tools })` replaces them. `chat.registerTool(tool)` adds one for every widget on the page instead. Full
|
|
126
|
+
guide: [Page tools](./page-tools.md).
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
init({
|
|
130
|
+
publicKey: 'agent_pk_...',
|
|
131
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
132
|
+
tools: [{ name: 'open_booking', description: 'Open the booking form. Use it whenever the visitor wants to book a call.', execute: () => (bookingForm.open(), { opened: true }) }],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
105
136
|
### `userContext`
|
|
106
137
|
|
|
107
138
|
`object | undefined`
|
package/docs/events-and-hooks.md
CHANGED
|
@@ -18,17 +18,19 @@ Both deliver the same event stream. Both are safe to mix.
|
|
|
18
18
|
|
|
19
19
|
## Available events
|
|
20
20
|
|
|
21
|
-
| Event | Fires on
|
|
22
|
-
| ----------------- |
|
|
23
|
-
| `open` | Panel opens (floating mode)
|
|
24
|
-
| `close` | Panel closes
|
|
25
|
-
| `message` | User sends a message + assistant finishes a reply
|
|
26
|
-
| `error` | Transport error or mid-stream error
|
|
27
|
-
| `expand` | Expanded tier toggled (⤢)
|
|
28
|
-
| `fullscreen` | Fullscreen tier toggled (⛶)
|
|
29
|
-
| `handshake` | Widget finishes booting (once per mount)
|
|
30
|
-
| `localeChange` | User picks a different language
|
|
31
|
-
| `messageFeedback` | User rates a reply 👍/👎 (or clears it)
|
|
21
|
+
| Event | Fires on | Payload |
|
|
22
|
+
| ----------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
23
|
+
| `open` | Panel opens (floating mode) | `void` |
|
|
24
|
+
| `close` | Panel closes | `void` |
|
|
25
|
+
| `message` | User sends a message + assistant finishes a reply | `{ role: "user" \| "assistant"; text: string }` |
|
|
26
|
+
| `error` | Transport error or mid-stream error | `unknown` (error object or message) |
|
|
27
|
+
| `expand` | Expanded tier toggled (⤢) | `boolean` (new state) |
|
|
28
|
+
| `fullscreen` | Fullscreen tier toggled (⛶) | `boolean` (new state) |
|
|
29
|
+
| `handshake` | Widget finishes booting (once per mount) | `HandshakeResponse` (dashboard config) |
|
|
30
|
+
| `localeChange` | User picks a different language | `string` (new BCP-47 tag) |
|
|
31
|
+
| `messageFeedback` | User rates a reply 👍/👎 (or clears it) | `{ messageId: string; value: "up" \| "down" \| null }` |
|
|
32
|
+
| `hostEvent` | Your page calls `chat.track(name, data)` | `{ name: string; data?: Record<string, scalar> }` |
|
|
33
|
+
| `toolResult` | A [page tool](./page-tools.md) call settles, or the visitor answers a question card | `{ toolCallId: string; name?: string; status?: "completed" \| "failed" \| "declined" \| "stopped" \| "interrupted"; durationMs?: number }` |
|
|
32
34
|
|
|
33
35
|
---
|
|
34
36
|
|
package/docs/frameworks.md
CHANGED
|
@@ -89,7 +89,7 @@ function PricingHelp() {
|
|
|
89
89
|
required. It renders nothing itself: the widget mounts once the page is in the browser, a prop change re-configures
|
|
90
90
|
the live widget without a remount, and unmounting removes it. Callback props (`onOpen`, `onMessage`, …) can be new
|
|
91
91
|
functions on every render; the widget always calls the latest one.
|
|
92
|
-
- **`useHelpAI(widgetId?)`** returns `{ state, ask, open, close, reset }`. `state` is
|
|
92
|
+
- **`useHelpAI(widgetId?)`** returns `{ state, ask, open, close, reset, track }`. `state` is
|
|
93
93
|
[`{ open, ready, unread, conversationId? }`](./api-reference.md#state-and-ready) and re-renders the component when it changes;
|
|
94
94
|
the server render sees the initial state. The commands are stable across renders and do nothing until the widget
|
|
95
95
|
has mounted (`state.ready`). `ask(text)` fills the composer; `ask(text, { send: true })` sends it. Call `reset()`
|
|
@@ -97,6 +97,12 @@ function PricingHelp() {
|
|
|
97
97
|
- **One import path.** The kit carries the whole widget API (`getInstance`, `hook`, the types), so import everything
|
|
98
98
|
from `@helpai/elements/react`; the package's other entries are separate copies with their own widgets.
|
|
99
99
|
- **Two widgets** on one page: give each `<HelpAI widgetId='support' … />` and read it with `useHelpAI('support')`.
|
|
100
|
+
- **Page tools:** `useHelpAITool(tool)` gives the agent a tool while the component is mounted, or pass
|
|
101
|
+
`<HelpAI tools={[...]} />` ([Page tools](./page-tools.md)). `execute` can be a new function on every render; the
|
|
102
|
+
agent's call runs the latest one, and only a changed definition registers or configures anything again.
|
|
103
|
+
- **Signed-in users:** pass `userToken`, a function that fetches the token your server signs
|
|
104
|
+
([Verified users](./security-privacy.md#verified-users-usertoken)). Keep it stable for one user (a module function, or
|
|
105
|
+
`useCallback` keyed on your user id): a different function signs in afresh, and dropping it signs out.
|
|
100
106
|
|
|
101
107
|
The kit bundles the widget and imports only React from your app, so you don't need Preact installed.
|
|
102
108
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Page tools
|
|
2
|
+
|
|
3
|
+
Give the AI agent tools that act on your own page: open your booking form, read the visitor's cart, jump to a
|
|
4
|
+
section, fill in a form. You describe each tool and write its function; the agent decides when to call it, the
|
|
5
|
+
widget runs the function in the visitor's browser, and the agent continues the conversation with what it returned.
|
|
6
|
+
Every page can offer different tools, so the agent can do on each page what that page is for.
|
|
7
|
+
|
|
8
|
+
A tool has the [WebMCP](https://webmachinelearning.github.io/webmcp/) shape (the W3C draft for tools a web page
|
|
9
|
+
offers to AI agents), so a tool you write for the widget also works with a browser's own WebMCP support, and the
|
|
10
|
+
other way round.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
chat.registerTool({
|
|
14
|
+
name: 'open_booking',
|
|
15
|
+
description: 'Open the booking form on this page for a meeting type. Use it whenever the visitor wants to book a call.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { meetingType: { type: 'string', enum: ['sales', 'support'] } },
|
|
19
|
+
required: ['meetingType'],
|
|
20
|
+
},
|
|
21
|
+
execute({ meetingType }) {
|
|
22
|
+
bookingForm.open(meetingType);
|
|
23
|
+
return { opened: true };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A visitor writes "I'd like to talk to sales"; the agent calls `open_booking` with `{ meetingType: 'sales' }`, your
|
|
29
|
+
booking form opens, and the agent answers "The booking form is open, pick a time that suits you."
|
|
30
|
+
|
|
31
|
+
## Adding tools
|
|
32
|
+
|
|
33
|
+
| Where you are | How |
|
|
34
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| Script tag | `HelpAI('registerTool', tool)`. Works before the widget has loaded: the loader queues it. |
|
|
36
|
+
| npm | `chat.registerTool(tool, { signal })`, or `init({ tools: [...] })`. |
|
|
37
|
+
| React | `useHelpAITool(tool)` in the component that owns the feature, or `<HelpAI tools={[...]} />`. |
|
|
38
|
+
| `<web-ai-chat>` | `element.registerTool(tool)`, or the `tools` property (functions can't be attributes). |
|
|
39
|
+
| Plain HTML | A form with `toolname` and `tooldescription` attributes ([Forms as tools](#forms-as-tools)); no script needed. |
|
|
40
|
+
| A WebMCP browser | Tools registered with the browser's own `document.modelContext.registerTool()` are offered too ([WebMCP](#webmcp-browsers)). |
|
|
41
|
+
|
|
42
|
+
`registerTool` adds the tool for every widget on the page. Pass an `AbortSignal` to remove it again, for example when
|
|
43
|
+
a single-page app leaves the route that owns it:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const route = new AbortController();
|
|
47
|
+
chat.registerTool(addToCart, { signal: route.signal });
|
|
48
|
+
// …when the visitor leaves the product page:
|
|
49
|
+
route.abort();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`useHelpAITool` does this for you: the tool is registered while the component is mounted, `execute` always runs the
|
|
53
|
+
latest render's function, and React's StrictMode double mount still leaves one registration.
|
|
54
|
+
|
|
55
|
+
## A tool
|
|
56
|
+
|
|
57
|
+
| Field | What it is |
|
|
58
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
59
|
+
| `name` | Letters, digits, `_` and `-`, 58 characters at most; other characters become `_`. The agent sees `site__<name>`. A name registered again replaces the earlier tool. |
|
|
60
|
+
| `description` | When the agent should use it and what it does, 1024 characters at most. The agent picks tools by their descriptions: say when, not only what. Without one, `title` (then `name`) is used. |
|
|
61
|
+
| `title` | Optional. A short human name. |
|
|
62
|
+
| `inputSchema` | Optional. [JSON Schema](https://json-schema.org/) of the input, an object. The agent fills it in; use `enum` and `required` to keep the input exact. Without one, the tool takes no input. |
|
|
63
|
+
| `annotations.consequentialHint` | Optional. `true` for a tool with real consequences (an order, a booking, a message to someone): the visitor approves each call in the chat first, and the function runs only on Approve. |
|
|
64
|
+
| `execute` | `(input, { signal }) => result`. Return a value or a promise of one; throw to report a failure. `signal` aborts when the visitor moves on before the call finishes. |
|
|
65
|
+
|
|
66
|
+
The `ModelContextTool` and `ToolAnnotations` types are exported for TypeScript; `ModelContextTool<{ sku: string }>`
|
|
67
|
+
types `execute`'s input. WebMCP's other annotations (`readOnlyHint`, `untrustedContentHint`) are accepted and not used
|
|
68
|
+
yet.
|
|
69
|
+
|
|
70
|
+
Nothing about a tool can break the chat: a tool without a `name` or an `execute` function is left out with a warning
|
|
71
|
+
in the browser console, and every message still goes through.
|
|
72
|
+
|
|
73
|
+
## Forms as tools
|
|
74
|
+
|
|
75
|
+
A form becomes a tool with two attributes, in every browser:
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<form toolname="request_demo" tooldescription="Request a product demo for the visitor." toolautosubmit>
|
|
79
|
+
<label for="email">Work email</label>
|
|
80
|
+
<input id="email" name="email" type="email" required />
|
|
81
|
+
<input name="company" toolparamdescription="The visitor's company name" />
|
|
82
|
+
<select name="plan">
|
|
83
|
+
<option value="starter">Starter</option>
|
|
84
|
+
<option value="pro">Pro</option>
|
|
85
|
+
</select>
|
|
86
|
+
<button>Request demo</button>
|
|
87
|
+
</form>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- Each named field is an input the agent can fill: a `select` or radio group takes one of its values, a checkbox
|
|
91
|
+
`true` or `false`, a number field a number, anything else text. Hidden, disabled and file fields are left out, and
|
|
92
|
+
so are passwords: the visitor types those, never the chat.
|
|
93
|
+
- A field's description is its `toolparamdescription`, else its label, else its `aria-description`; `required`
|
|
94
|
+
fields are required.
|
|
95
|
+
- The agent fills the fields the way a visitor would (your framework's change handlers run). With `toolautosubmit`
|
|
96
|
+
the form is then submitted; without it the visitor checks the form and presses its button. When a field still
|
|
97
|
+
blocks the submit (a password, or a value the browser rejects), the form isn't submitted and the agent is told which
|
|
98
|
+
field needs the visitor.
|
|
99
|
+
|
|
100
|
+
## WebMCP browsers
|
|
101
|
+
|
|
102
|
+
In a browser with WebMCP (`document.modelContext`: in Chrome, turn on `chrome://flags/#enable-experimental-web-platform-features`
|
|
103
|
+
or join the origin trial), the tools the page registered with the browser are offered to the agent too, and the widget
|
|
104
|
+
runs them through the browser. A `<form toolname>` is the exception: such a browser lists it too, but the widget fills
|
|
105
|
+
it itself, as in every other browser, so your form's own submit handler keeps working and passwords stay the
|
|
106
|
+
visitor's. Elsewhere, nothing changes.
|
|
107
|
+
|
|
108
|
+
## Which tools a message offers
|
|
109
|
+
|
|
110
|
+
The widget reads the page's tools when the visitor sends each message: the browser's WebMCP tools, then forms, then
|
|
111
|
+
the `tools` option, then `registerTool`. For a name in more than one place, the later one wins. When the agent calls a
|
|
112
|
+
tool, the widget reads them again and runs the current function of that name, so a single-page app's route change
|
|
113
|
+
applies straight away, with nothing to keep in sync. A call to a tool the page no longer has tells the agent the page
|
|
114
|
+
doesn't offer it right now. So does a call to a tool the message didn't offer at all (the agent repeating an earlier
|
|
115
|
+
call after a route change): the server answers it with "This page doesn't offer that action right now.", tells the agent
|
|
116
|
+
not to call it again this turn, and the agent carries on from what it knows or asks the visitor.
|
|
117
|
+
|
|
118
|
+
## What the agent gets back
|
|
119
|
+
|
|
120
|
+
- **A return value** becomes the tool's result, as JSON: dates become strings, `undefined` becomes `null`, functions
|
|
121
|
+
are dropped, a page element is named (`"[dialog]"`), a repeated object reads `"[circular]"`, and a result over
|
|
122
|
+
30,000 characters is sent as `{ truncated: true, preview }`. Return what the agent needs to answer well, such as
|
|
123
|
+
`{ opened: true }` or `{ items: 2, total: '$48' }`.
|
|
124
|
+
- **A thrown error** (or a rejected promise) reaches the agent as the tool's error, with its message, so it can tell
|
|
125
|
+
the visitor or try another way. For an expected miss ("that product is out of stock") a normal result such as
|
|
126
|
+
`{ ok: false, reason: 'out of stock' }` gives the agent more to work with.
|
|
127
|
+
|
|
128
|
+
With several calls in one step, the agent waits for all of them, and for any approvals, then continues once.
|
|
129
|
+
|
|
130
|
+
## Each call runs once
|
|
131
|
+
|
|
132
|
+
- **Tabs.** Only the tab where the visitor sent the message runs the calls in its reply; another tab of the same
|
|
133
|
+
conversation shows them running, then the agent's answer.
|
|
134
|
+
- **Reloads and navigation.** A call that hadn't started when the page reloaded runs once after the reload. A call
|
|
135
|
+
that was running is never run again: the agent is told the page reloaded, so it may or may not have taken effect,
|
|
136
|
+
and checks with the visitor; its card reads "Not completed". A result that was ready but not yet sent is sent after the reload. The widget keeps
|
|
137
|
+
this note per tab in `sessionStorage`, so it also covers a navigation to another page of your site.
|
|
138
|
+
- **Leaving the page.** Results go out with `keepalive`, so they arrive even when the page navigates straight after.
|
|
139
|
+
A tool that navigates should return first and leave after:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
execute({ path }) {
|
|
143
|
+
setTimeout(() => location.assign(path), 0); // navigate after the result is sent
|
|
144
|
+
return { navigating: path };
|
|
145
|
+
},
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **The script loaded twice, or two widgets.** The call still runs once.
|
|
149
|
+
- **No time limit.** The agent waits for the result. While a tool runs, the chat shows Stop. If the visitor presses it,
|
|
150
|
+
or sends another message first, the call's `signal` aborts, its result is never sent, and its card reads
|
|
151
|
+
"Not completed". A stopped call never runs again, not even after a reload.
|
|
152
|
+
|
|
153
|
+
When `sessionStorage` is blocked (some private modes), the note lives in memory, so a reload can't tell a call that
|
|
154
|
+
ran from one that didn't; everything else works the same.
|
|
155
|
+
|
|
156
|
+
## What the visitor sees
|
|
157
|
+
|
|
158
|
+
A tool that runs on its own appears in the transcript only when the deployment shows tool calls
|
|
159
|
+
(`behavior.showToolCalls`), with its input and result: "Running", then "Completed" or "Error" (the tool failed), or
|
|
160
|
+
"Not completed" when the visitor stopped it or moved on, or a reload cut it short. A tool with `consequentialHint` always shows an approval card with the input; Reject tells
|
|
161
|
+
the agent the visitor declined, and the function never runs.
|
|
162
|
+
|
|
163
|
+
## Events
|
|
164
|
+
|
|
165
|
+
Each call this page settles fires `toolResult` with `{ toolCallId, name, status, durationMs }`: `status` is
|
|
166
|
+
`completed`, `failed`, `declined` (the visitor rejected it), `stopped` (the visitor pressed Stop) or `interrupted` (a
|
|
167
|
+
reload cut it short). It never carries the input or the result.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
chat.on('toolResult', ({ name, status, durationMs }) => analytics.track('help_ai_tool', { name, status, durationMs }));
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Security
|
|
174
|
+
|
|
175
|
+
- Tools run with your page's full privileges, as code you wrote. Anyone can also call your functions from the
|
|
176
|
+
browser's developer tools, so a tool may do only what the visitor could do on the page themselves.
|
|
177
|
+
- The agent chooses the input. Check it in `execute` as you would a form submission, and mark tools with real
|
|
178
|
+
consequences (payments, deletions, messages to other people) `consequentialHint: true`, or confirm in your own UI.
|
|
179
|
+
- The definitions are sent with each message and the results are stored in the conversation, like any tool's result,
|
|
180
|
+
so never put secrets in a description or a result.
|
|
181
|
+
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
`testing/page-tools-manual.md` walks through every case above in a real browser
|
|
185
|
+
on the `dev/page-tools.html` playground, with a real model or the mock backend.
|
|
186
|
+
|
|
187
|
+
## Troubleshooting
|
|
188
|
+
|
|
189
|
+
- **The agent never uses the tool:** write the description as an instruction ("Use it whenever the visitor wants to
|
|
190
|
+
book a call"), and check the console for a "skipped a tool" warning.
|
|
191
|
+
- **The chat waits after a tool:** the function never settled (a promise that doesn't resolve). Return, or throw.
|
|
192
|
+
- **"This page doesn't offer … right now":** the call came after the tool was removed (a route change aborted its
|
|
193
|
+
signal), or the agent tried a tool this page doesn't register. Keep a tool registered while the agent may still use
|
|
194
|
+
it.
|
package/docs/security-privacy.md
CHANGED
|
@@ -340,7 +340,8 @@ the pinned version URL (`/v1.2.0/`) supports SRI — the floating URL
|
|
|
340
340
|
## Authentication
|
|
341
341
|
|
|
342
342
|
Browser access is scoped by `aiAgentDeploymentId`; a `publicKey` adds
|
|
343
|
-
workspace authorization.
|
|
343
|
+
workspace authorization. Visitors are anonymous unless your server signs a
|
|
344
|
+
[user token](#verified-users-usertoken) for your logged-in user.
|
|
344
345
|
|
|
345
346
|
### public key (default)
|
|
346
347
|
|
|
@@ -365,6 +366,64 @@ What your public key does NOT grant:
|
|
|
365
366
|
Omit `publicKey` but keep `aiAgentDeploymentId`; the widget connects to
|
|
366
367
|
that public deployment as a guest without an auth header.
|
|
367
368
|
|
|
369
|
+
### Verified users (`userToken`)
|
|
370
|
+
|
|
371
|
+
To show a logged-in user their own conversations on any device, and to let the AI rely on who they are, your
|
|
372
|
+
server signs a short-lived JWT for them with the deployment's **secret key** (on the deployment's Install page;
|
|
373
|
+
never put it in the browser):
|
|
374
|
+
|
|
375
|
+
| Claim | Required | What it is |
|
|
376
|
+
| ------- | -------- | ---------------------------------------------------------- |
|
|
377
|
+
| `sub` | yes | Your own id for the user (a string, up to 255 characters) |
|
|
378
|
+
| `exp` | yes | Expiry, in seconds since the epoch; keep it short (1 hour) |
|
|
379
|
+
| `name` | no | Their name, given to the AI as a verified fact |
|
|
380
|
+
| `email` | no | Their email, given to the AI as a verified fact |
|
|
381
|
+
|
|
382
|
+
Sign it with HS256:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
// Node (jose)
|
|
386
|
+
import { SignJWT } from 'jose';
|
|
387
|
+
|
|
388
|
+
const secret = new TextEncoder().encode(process.env.HELPAI_SECRET_KEY);
|
|
389
|
+
const userToken = await new SignJWT({ name: user.name, email: user.email }).setProtectedHeader({ alg: 'HS256' }).setSubject(String(user.id)).setExpirationTime('1h').sign(secret);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
# Python (PyJWT)
|
|
394
|
+
import os, time, jwt
|
|
395
|
+
|
|
396
|
+
user_token = jwt.encode({"sub": str(user.id), "exp": int(time.time()) + 3600, "name": user.name, "email": user.email}, os.environ["HELPAI_SECRET_KEY"], algorithm="HS256")
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```php
|
|
400
|
+
// PHP (firebase/php-jwt)
|
|
401
|
+
use Firebase\JWT\JWT;
|
|
402
|
+
|
|
403
|
+
$userToken = JWT::encode(['sub' => (string) $user->id, 'exp' => time() + 3600, 'name' => $user->name, 'email' => $user->email], getenv('HELPAI_SECRET_KEY'), 'HS256');
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Serve it from an endpoint only your signed-in user can reach, and hand the widget a function that fetches it:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const userToken = () => fetch('/api/help-ai-token').then((res) => res.text());
|
|
410
|
+
|
|
411
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', userToken }); // signed in at load
|
|
412
|
+
chat.login(userToken); // or after your sign-in (`HelpAI('login', userToken)`, `<HelpAI userToken={userToken} />`)
|
|
413
|
+
chat.reset(); // on sign-out (`HelpAI('reset')`)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- The widget sends the token only to the agent API, as the `x-user-token` header. The function is called when a token
|
|
417
|
+
is first needed and again when the server answers `401 USER_TOKEN_INVALID` (expired or rejected); the request is
|
|
418
|
+
then sent once more. A fixed string can't be refreshed, so an expired one fails until you call `login()` again.
|
|
419
|
+
- A token the server can't verify is refused; it never falls back to an anonymous visitor.
|
|
420
|
+
- Signing in moves the visitor's conversations on that browser to the user. From then on they reach them on any device
|
|
421
|
+
where they're signed in, and nobody else does, the signed-out browser included.
|
|
422
|
+
- Signing out (`reset()`, or dropping the token) or switching to another user starts over as a new visitor: nothing of
|
|
423
|
+
the previous user stays in the browser.
|
|
424
|
+
- Keep the function's identity stable for one user (a module function, or `useCallback` keyed on your user id): a
|
|
425
|
+
different function counts as a different user.
|
|
426
|
+
|
|
368
427
|
### user / page context is NOT auth
|
|
369
428
|
|
|
370
429
|
```ts
|
|
@@ -406,13 +465,13 @@ flowchart TB
|
|
|
406
465
|
direction TB
|
|
407
466
|
B1["Authenticates the HOST's agent deployment<br/>(NOT the end user)"]
|
|
408
467
|
B2["Treats userContext / pageContext as<br/>informational + untrusted"]
|
|
409
|
-
B3["Never registers
|
|
468
|
+
B3["Never registers end users; verifies a signed userToken when the host sends one"]
|
|
410
469
|
end
|
|
411
470
|
|
|
412
471
|
Host -->|"context (untrusted)"| Widget
|
|
413
472
|
Widget -->|"HTTPS/TLS + headers + context field"| Backend
|
|
414
473
|
|
|
415
|
-
note["publicKey + aiAgentDeploymentId auth the host's agent
|
|
474
|
+
note["publicKey + aiAgentDeploymentId auth the host's agent; only a userToken signed by the host's server identifies the end user."]
|
|
416
475
|
Backend -.-> note
|
|
417
476
|
```
|
|
418
477
|
|
|
@@ -430,6 +489,8 @@ and anyone can base64url-decode it. Never put a secret in context.
|
|
|
430
489
|
|
|
431
490
|
- **public keys** — rotate from the dashboard. Old keys are deactivated
|
|
432
491
|
instantly; update your embeds.
|
|
492
|
+
- **secret key** — user tokens signed with a replaced secret key stop verifying, so every verified user is signed out
|
|
493
|
+
until your server signs new ones with the new key.
|
|
433
494
|
|
|
434
495
|
---
|
|
435
496
|
|