@aikaara/chat-sdk 0.1.4 → 0.2.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 +435 -0
- package/dist/{headless-BhsiNVQj.mjs → AikaaraChatClient-C4lWcRsS.mjs} +89 -29
- package/dist/AikaaraChatClient-ChZ2bL9f.cjs +1 -0
- package/dist/cdn/aikaara-chat.iife.js +24 -17
- package/dist/headless.cjs +8 -1
- package/dist/headless.d.ts +250 -2
- package/dist/headless.mjs +10588 -9
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +252 -2
- package/dist/index.mjs +21 -18
- package/dist/ui.cjs +3 -3
- package/dist/ui.d.ts +207 -1
- package/dist/ui.mjs +26 -14
- package/package.json +4 -1
- package/dist/headless-CrgIWcf7.cjs +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# @aikaara/chat-sdk
|
|
2
|
+
|
|
3
|
+
Embeddable chat widget and headless client for the Aikaara AI agent platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @aikaara/chat-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Three Entry Points
|
|
12
|
+
|
|
13
|
+
| Import Path | Use Case |
|
|
14
|
+
|-------------|----------|
|
|
15
|
+
| `@aikaara/chat-sdk` | Full bundle: widget + headless + `mount()`/`unmount()` |
|
|
16
|
+
| `@aikaara/chat-sdk/headless` | Headless only: no DOM dependency. For React Native, custom UI, server-side |
|
|
17
|
+
| `@aikaara/chat-sdk/ui` | Web Components only |
|
|
18
|
+
|
|
19
|
+
## Quick Start — Chat Widget (CDN)
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script src="https://cdn.aikaara.com/chat-sdk/latest/aikaara-chat.iife.js"></script>
|
|
23
|
+
<script>
|
|
24
|
+
AikaaraChat.mount({
|
|
25
|
+
baseUrl: 'https://api.aikaara.com',
|
|
26
|
+
userToken: 'ut_...',
|
|
27
|
+
title: 'Support',
|
|
28
|
+
primaryColor: '#6366f1',
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start — Headless Client
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { AikaaraChatClient } from '@aikaara/chat-sdk/headless';
|
|
37
|
+
|
|
38
|
+
const client = new AikaaraChatClient({
|
|
39
|
+
baseUrl: 'https://api.aikaara.com',
|
|
40
|
+
userToken: 'ut_...',
|
|
41
|
+
apiKey: 'ak_...',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await client.connect();
|
|
45
|
+
client.on('message:received', (msg) => console.log(msg.content));
|
|
46
|
+
client.on('stream:update', ({ content }) => updateUI(content));
|
|
47
|
+
|
|
48
|
+
await client.sendMessage('Hello!');
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start — Headless with JWT Auth (Dashboard / Internal Apps)
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { AikaaraChatClient } from '@aikaara/chat-sdk/headless';
|
|
55
|
+
|
|
56
|
+
const client = new AikaaraChatClient({
|
|
57
|
+
baseUrl: 'https://api.aikaara.com',
|
|
58
|
+
userToken: 'ut_...',
|
|
59
|
+
authToken: jwtToken, // Bearer token for REST calls
|
|
60
|
+
channel: 'sidekick',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await client.connect();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## App Context — Connecting the Agent to Your App
|
|
67
|
+
|
|
68
|
+
The SDK doesn't know about your app's routes, pages, or entities. You provide that context at runtime via `setContext()`, and the agent uses it to navigate, edit forms, and stay aware of what the user is doing.
|
|
69
|
+
|
|
70
|
+
### Why This Matters
|
|
71
|
+
|
|
72
|
+
Without context, the agent doesn't know:
|
|
73
|
+
- What page the user is on (`/products/42`? `/orders`?)
|
|
74
|
+
- What routes exist in your app (so it can navigate)
|
|
75
|
+
- What entity the user is viewing (so it can edit the right form)
|
|
76
|
+
|
|
77
|
+
### Providing Context on Route Changes
|
|
78
|
+
|
|
79
|
+
Call `setContext()` whenever the user navigates in your app:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { AikaaraChatClient } from '@aikaara/chat-sdk/headless';
|
|
83
|
+
|
|
84
|
+
const client = new AikaaraChatClient({
|
|
85
|
+
baseUrl: 'https://api.aikaara.com',
|
|
86
|
+
userToken: 'ut_...',
|
|
87
|
+
authToken: jwt,
|
|
88
|
+
channel: 'sidekick',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await client.connect();
|
|
92
|
+
|
|
93
|
+
// Call on every route change (e.g., in a React useEffect, Vue watch, etc.)
|
|
94
|
+
client.setContext({
|
|
95
|
+
currentPage: '/products/42',
|
|
96
|
+
entityType: 'product',
|
|
97
|
+
entityId: '42',
|
|
98
|
+
availableRoutes: {
|
|
99
|
+
'Product list': '/products',
|
|
100
|
+
'Order list': '/orders',
|
|
101
|
+
'Customer list': '/customers',
|
|
102
|
+
'Settings': '/settings',
|
|
103
|
+
'Analytics': '/analytics',
|
|
104
|
+
},
|
|
105
|
+
custom: {
|
|
106
|
+
formFields: ['name', 'price', 'description', 'category'],
|
|
107
|
+
userName: 'Jane',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### What Happens Under the Hood
|
|
113
|
+
|
|
114
|
+
1. `setContext()` sends a PATCH to the backend, storing the context in conversation metadata
|
|
115
|
+
2. The backend interpolates `{{current_page}}`, `{{available_routes}}`, and `{{custom_context}}` into the agent's system prompt
|
|
116
|
+
3. The agent now knows your app's routes and can call `navigate_to` with them
|
|
117
|
+
4. The agent knows the current entity and can call `edit_current_entity` for it
|
|
118
|
+
|
|
119
|
+
### AppContext Interface
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface AppContext {
|
|
123
|
+
/** Current page/route path in your app (e.g., '/products/42') */
|
|
124
|
+
currentPage: string;
|
|
125
|
+
/** Entity type on the current page (e.g., 'product', 'order') */
|
|
126
|
+
entityType?: string;
|
|
127
|
+
/** Entity ID on the current page */
|
|
128
|
+
entityId?: string | number;
|
|
129
|
+
/** Project/workspace ID if applicable */
|
|
130
|
+
projectId?: string | number;
|
|
131
|
+
/** Routes the agent can navigate to — map of label to path */
|
|
132
|
+
availableRoutes?: Record<string, string>;
|
|
133
|
+
/** Any additional context for the agent (form fields, user info, etc.) */
|
|
134
|
+
custom?: Record<string, unknown>;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Handling Navigation
|
|
139
|
+
|
|
140
|
+
The agent doesn't control your router — it returns a path, and you handle it:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
client.on('action:navigate', ({ navigate_to }) => {
|
|
144
|
+
// Your router (React Router, Vue Router, Next.js, etc.)
|
|
145
|
+
router.push(navigate_to);
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The `navigate_to` value will be one of the paths from your `availableRoutes`, or a path the agent constructs from context (e.g., `/products/42`).
|
|
150
|
+
|
|
151
|
+
### Framework Examples
|
|
152
|
+
|
|
153
|
+
**React (with React Router)**
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { useEffect } from 'react';
|
|
157
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
158
|
+
|
|
159
|
+
function useSidekickContext(client: AikaaraChatClient) {
|
|
160
|
+
const location = useLocation();
|
|
161
|
+
const navigate = useNavigate();
|
|
162
|
+
|
|
163
|
+
// Update context on route change
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const match = location.pathname.match(/^\/(products|orders|customers)\/(\d+)/);
|
|
166
|
+
client.setContext({
|
|
167
|
+
currentPage: location.pathname,
|
|
168
|
+
entityType: match?.[1],
|
|
169
|
+
entityId: match?.[2],
|
|
170
|
+
availableRoutes: {
|
|
171
|
+
'Products': '/products',
|
|
172
|
+
'Orders': '/orders',
|
|
173
|
+
'Customers': '/customers',
|
|
174
|
+
'Analytics': '/analytics',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}, [location.pathname]);
|
|
178
|
+
|
|
179
|
+
// Handle navigation from agent
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const off = client.on('action:navigate', ({ navigate_to }) => {
|
|
182
|
+
navigate(navigate_to);
|
|
183
|
+
});
|
|
184
|
+
return off;
|
|
185
|
+
}, [navigate]);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Vue 3 (with Vue Router)**
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { watch } from 'vue';
|
|
193
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
194
|
+
|
|
195
|
+
function useSidekickContext(client: AikaaraChatClient) {
|
|
196
|
+
const route = useRoute();
|
|
197
|
+
const router = useRouter();
|
|
198
|
+
|
|
199
|
+
watch(() => route.fullPath, (path) => {
|
|
200
|
+
client.setContext({
|
|
201
|
+
currentPage: path,
|
|
202
|
+
entityType: route.params.type as string,
|
|
203
|
+
entityId: route.params.id as string,
|
|
204
|
+
availableRoutes: { Products: '/products', Orders: '/orders' },
|
|
205
|
+
});
|
|
206
|
+
}, { immediate: true });
|
|
207
|
+
|
|
208
|
+
client.on('action:navigate', ({ navigate_to }) => {
|
|
209
|
+
router.push(navigate_to);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Vanilla JS / Any Framework**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// On route change (however your app detects it)
|
|
218
|
+
window.addEventListener('popstate', () => {
|
|
219
|
+
client.setContext({ currentPage: window.location.pathname });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// On navigation action from agent
|
|
223
|
+
client.on('action:navigate', ({ navigate_to }) => {
|
|
224
|
+
window.history.pushState(null, '', navigate_to);
|
|
225
|
+
// Trigger your app's route handler
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## FormBridge — AI-Driven Form Editing
|
|
230
|
+
|
|
231
|
+
The `FormBridge` lets AI agents visually edit forms in your application. When the agent calls tools like `edit_current_entity`, the bridge pushes field updates to your registered form — the user sees changes live and can confirm or reject them.
|
|
232
|
+
|
|
233
|
+
### How It Works
|
|
234
|
+
|
|
235
|
+
1. Your agent has tools like `edit_current_entity`, `save_current_entity`, and `test_tool_by_id`
|
|
236
|
+
2. When the agent calls these tools, the result flows through the WebSocket as a `tool_execution_end` event
|
|
237
|
+
3. The SDK's `AikaaraChatClient` parses the result and emits typed action events
|
|
238
|
+
4. `FormBridge` listens for these events and pushes updates to your registered form
|
|
239
|
+
|
|
240
|
+
### Integration Steps
|
|
241
|
+
|
|
242
|
+
#### 1. Import FormBridge from the SDK
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { AikaaraChatClient, FormBridge } from '@aikaara/chat-sdk/headless';
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### 2. Pass it your AikaaraChatClient
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const client = new AikaaraChatClient({
|
|
252
|
+
baseUrl: 'https://api.aikaara.com',
|
|
253
|
+
userToken: 'ut_...',
|
|
254
|
+
authToken: jwtToken,
|
|
255
|
+
channel: 'sidekick',
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const bridge = new FormBridge(client);
|
|
259
|
+
await client.connect();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### 3. Register your forms with `registerForm()`
|
|
263
|
+
|
|
264
|
+
Call this when your form component mounts. Only one form can be active at a time (the current page).
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
bridge.registerForm({
|
|
268
|
+
entityType: 'product', // matches what the agent sends
|
|
269
|
+
entityId: 42, // the entity being edited
|
|
270
|
+
onFieldUpdate: (fields) => {
|
|
271
|
+
// Update your form UI — fields is FieldUpdate[]
|
|
272
|
+
for (const { field, value } of fields) {
|
|
273
|
+
formState[field] = value;
|
|
274
|
+
rerenderField(field); // your UI framework's update mechanism
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
onSave: async () => {
|
|
278
|
+
// Called when the agent triggers save_current_entity
|
|
279
|
+
await api.updateProduct(42, formState);
|
|
280
|
+
},
|
|
281
|
+
onTest: async (params) => {
|
|
282
|
+
// Optional: called when the agent triggers test_tool_by_id
|
|
283
|
+
await api.testProduct(42, params);
|
|
284
|
+
},
|
|
285
|
+
getCurrentValues: () => ({ ...formState }),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Unregister when the form unmounts
|
|
289
|
+
bridge.unregisterForm('product', 42);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### 4. The bridge automatically handles tool results
|
|
293
|
+
|
|
294
|
+
When the agent calls these backend tools, the bridge takes action automatically:
|
|
295
|
+
|
|
296
|
+
| Agent Tool | Bridge Action |
|
|
297
|
+
|-----------|---------------|
|
|
298
|
+
| `edit_current_entity` | Calls `onFieldUpdate()` with the field changes |
|
|
299
|
+
| `save_current_entity` | Calls `onSave()` on the registered form |
|
|
300
|
+
| `test_tool_by_id` | Calls `onTest()` with parameters |
|
|
301
|
+
|
|
302
|
+
If no form is registered when an `edit_current_entity` result arrives, the bridge queues it. When a matching form later registers, the queued edits are applied automatically.
|
|
303
|
+
|
|
304
|
+
#### 5. Build your own confirmation UI using bridge events
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
bridge.on('edit:applied', ({ entityType, entityId, fields }) => {
|
|
308
|
+
// Show a confirmation banner: "AI edited: name, description"
|
|
309
|
+
showBanner({
|
|
310
|
+
message: `AI edited: ${fields.map(f => f.field).join(', ')}`,
|
|
311
|
+
onConfirm: () => dismissBanner(),
|
|
312
|
+
onReject: () => {
|
|
313
|
+
// Revert fields using previousValue
|
|
314
|
+
revertFields(fields);
|
|
315
|
+
dismissBanner();
|
|
316
|
+
},
|
|
317
|
+
onSave: async () => {
|
|
318
|
+
await bridge.requestSave();
|
|
319
|
+
dismissBanner();
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
bridge.on('edit:pending', ({ entityType, entityId, fields }) => {
|
|
325
|
+
// Edits queued — no matching form registered yet
|
|
326
|
+
// Navigate the user to the right page, then the form will pick them up
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
bridge.on('save:success', () => toast.success('Saved by AI'));
|
|
330
|
+
bridge.on('save:error', ({ error }) => toast.error(`Save failed: ${error}`));
|
|
331
|
+
bridge.on('test:triggered', ({ toolId, parameters }) => {
|
|
332
|
+
// Show test UI if needed
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Lower-Level: Action Events on AikaaraChatClient
|
|
337
|
+
|
|
338
|
+
If you don't need `FormBridge` and want to handle actions yourself:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
client.on('action:edit_entity', (action) => {
|
|
342
|
+
// action: { entity_type, entity_id, fields: FieldUpdate[] }
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
client.on('action:save_entity', () => {
|
|
346
|
+
// Trigger save
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
client.on('action:navigate', ({ navigate_to }) => {
|
|
350
|
+
// Route change: e.g., router.push(navigate_to)
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
client.on('action:test_tool', ({ tool_id, parameters }) => {
|
|
354
|
+
// Run a tool test
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Raw tool lifecycle events
|
|
358
|
+
client.on('tool:start', ({ toolName, args }) => showSpinner(toolName));
|
|
359
|
+
client.on('tool:end', ({ toolName, result, isError }) => hideSpinner(toolName));
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### React Hook Example
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { useEffect, useRef } from 'react';
|
|
366
|
+
import type { FormBridge, FieldUpdate } from '@aikaara/chat-sdk/headless';
|
|
367
|
+
|
|
368
|
+
function useFormBridge(
|
|
369
|
+
bridge: FormBridge,
|
|
370
|
+
options: {
|
|
371
|
+
entityType: string;
|
|
372
|
+
entityId: string | number | undefined;
|
|
373
|
+
onFieldUpdate: (fields: FieldUpdate[]) => void;
|
|
374
|
+
onSave: () => Promise<void>;
|
|
375
|
+
onTest?: (params?: Record<string, unknown>) => Promise<void>;
|
|
376
|
+
getCurrentValues: () => Record<string, unknown>;
|
|
377
|
+
}
|
|
378
|
+
) {
|
|
379
|
+
const refs = useRef(options);
|
|
380
|
+
refs.current = options;
|
|
381
|
+
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (!options.entityId) return;
|
|
384
|
+
bridge.registerForm({
|
|
385
|
+
entityType: options.entityType,
|
|
386
|
+
entityId: options.entityId,
|
|
387
|
+
onFieldUpdate: (f) => refs.current.onFieldUpdate(f),
|
|
388
|
+
onSave: () => refs.current.onSave(),
|
|
389
|
+
onTest: refs.current.onTest ? (p) => refs.current.onTest!(p) : undefined,
|
|
390
|
+
getCurrentValues: () => refs.current.getCurrentValues(),
|
|
391
|
+
});
|
|
392
|
+
return () => bridge.unregisterForm(options.entityType, options.entityId!);
|
|
393
|
+
}, [bridge, options.entityType, options.entityId]);
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Agent Events
|
|
398
|
+
|
|
399
|
+
Events broadcast over ActionCable from the Aikaara Rails backend:
|
|
400
|
+
|
|
401
|
+
| Event | Description |
|
|
402
|
+
|-------|-------------|
|
|
403
|
+
| `status` | Processing state (processing/completed) |
|
|
404
|
+
| `message_start/update/end` | Streaming response lifecycle |
|
|
405
|
+
| `message_queued` | User message acknowledged |
|
|
406
|
+
| `tool_execution_start/update/end` | Tool call lifecycle |
|
|
407
|
+
| `agent_start/end` | Agent session lifecycle |
|
|
408
|
+
| `turn_start/end` | Conversation turn lifecycle |
|
|
409
|
+
| `auto_retry_start/end` | Auto-retry on failure |
|
|
410
|
+
| `cancelled` | Processing cancelled |
|
|
411
|
+
|
|
412
|
+
## TypeScript Types
|
|
413
|
+
|
|
414
|
+
All types are exported from `@aikaara/chat-sdk/headless`:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import type {
|
|
418
|
+
// Connection
|
|
419
|
+
ConnectionConfig, ConnectionState, ChatClientConfig,
|
|
420
|
+
// Messages
|
|
421
|
+
Message, ToolCall, ToolCallResult,
|
|
422
|
+
// Events
|
|
423
|
+
AgentEvent, AgentEventType, ChatEvents,
|
|
424
|
+
// App Context
|
|
425
|
+
AppContext,
|
|
426
|
+
// Form Bridge
|
|
427
|
+
FieldUpdate, FormRegistration, FormBridgeEvents,
|
|
428
|
+
// Actions
|
|
429
|
+
EditEntityAction, SaveEntityAction, TestToolAction, NavigateAction, AgentAction,
|
|
430
|
+
} from '@aikaara/chat-sdk/headless';
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
MIT
|
|
@@ -163,8 +163,8 @@ class m {
|
|
|
163
163
|
this.handlers.clear();
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
const f = 1e3,
|
|
167
|
-
class
|
|
166
|
+
const f = 1e3, _ = 10, w = 400, k = 600, T = "#6366f1", I = 12, E = "system-ui, -apple-system, sans-serif", C = "Type a message...", A = "bottom-right", M = "light", O = { x: 20, y: 20 }, d = "aikaara_conversation_id";
|
|
167
|
+
class b extends m {
|
|
168
168
|
client;
|
|
169
169
|
config;
|
|
170
170
|
state = "disconnected";
|
|
@@ -220,7 +220,7 @@ class _ extends m {
|
|
|
220
220
|
this.state !== t && (this.state = t, this.emit("connection:state", t));
|
|
221
221
|
}
|
|
222
222
|
scheduleReconnect() {
|
|
223
|
-
const t = this.config.maxReconnectAttempts ??
|
|
223
|
+
const t = this.config.maxReconnectAttempts ?? _;
|
|
224
224
|
if (this.reconnectAttempt >= t) {
|
|
225
225
|
this.emit("error", new Error("Max reconnection attempts reached"));
|
|
226
226
|
return;
|
|
@@ -246,12 +246,13 @@ class _ extends m {
|
|
|
246
246
|
return `${t.replace(/^http/, "ws")}/cable?token=${e}`;
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
-
class
|
|
249
|
+
class y {
|
|
250
250
|
baseUrl;
|
|
251
251
|
apiKey;
|
|
252
|
+
authToken;
|
|
252
253
|
userToken;
|
|
253
|
-
constructor(t, e, s) {
|
|
254
|
-
this.baseUrl = t, this.userToken = e, this.apiKey = s;
|
|
254
|
+
constructor(t, e, s, n) {
|
|
255
|
+
this.baseUrl = t, this.userToken = e, this.apiKey = s, this.authToken = n;
|
|
255
256
|
}
|
|
256
257
|
async createConversation(t) {
|
|
257
258
|
const e = {
|
|
@@ -264,6 +265,10 @@ class S {
|
|
|
264
265
|
};
|
|
265
266
|
return this.request("POST", "/api/v1/conversations", e);
|
|
266
267
|
}
|
|
268
|
+
async updateContext(t, e) {
|
|
269
|
+
const s = this.authToken ? `/dashboard/sidekick_conversations/${t}` : `/api/v1/conversations/${t}`;
|
|
270
|
+
await this.request("PATCH", s, e);
|
|
271
|
+
}
|
|
267
272
|
async getMessages(t) {
|
|
268
273
|
return (await this.request(
|
|
269
274
|
"GET",
|
|
@@ -294,22 +299,22 @@ class S {
|
|
|
294
299
|
"Content-Type": "application/json",
|
|
295
300
|
Accept: "application/json"
|
|
296
301
|
};
|
|
297
|
-
this.apiKey && (n["X-Api-Key"] = this.apiKey);
|
|
302
|
+
this.apiKey && (n["X-Api-Key"] = this.apiKey), this.authToken && (n.Authorization = `Bearer ${this.authToken}`);
|
|
298
303
|
const i = `${this.baseUrl}${e}`, l = { method: t, headers: n };
|
|
299
304
|
s && (l.body = JSON.stringify(s));
|
|
300
|
-
const
|
|
301
|
-
if (!
|
|
302
|
-
const
|
|
305
|
+
const a = await fetch(i, l);
|
|
306
|
+
if (!a.ok) {
|
|
307
|
+
const c = await a.text();
|
|
303
308
|
let h;
|
|
304
309
|
try {
|
|
305
|
-
const u = JSON.parse(
|
|
306
|
-
h = u.error || u.message ||
|
|
310
|
+
const u = JSON.parse(c);
|
|
311
|
+
h = u.error || u.message || c;
|
|
307
312
|
} catch {
|
|
308
|
-
h =
|
|
313
|
+
h = c;
|
|
309
314
|
}
|
|
310
|
-
throw new Error(`API error ${
|
|
315
|
+
throw new Error(`API error ${a.status}: ${h}`);
|
|
311
316
|
}
|
|
312
|
-
const r = await
|
|
317
|
+
const r = await a.json();
|
|
313
318
|
if (r && typeof r == "object" && "success" in r) {
|
|
314
319
|
if (!r.success)
|
|
315
320
|
throw new Error(`API error: ${r.message || "Request failed"}`);
|
|
@@ -375,7 +380,7 @@ class v {
|
|
|
375
380
|
this._messages = [];
|
|
376
381
|
}
|
|
377
382
|
}
|
|
378
|
-
class
|
|
383
|
+
class S {
|
|
379
384
|
_conversationId;
|
|
380
385
|
persist;
|
|
381
386
|
constructor(t, e = !0) {
|
|
@@ -417,7 +422,7 @@ class U extends m {
|
|
|
417
422
|
subscription = null;
|
|
418
423
|
config;
|
|
419
424
|
constructor(t) {
|
|
420
|
-
super(), this.config = t, this.connection = new
|
|
425
|
+
super(), this.config = t, this.connection = new b(t), this.api = new y(t.baseUrl, t.userToken, t.apiKey, t.authToken), this.messageStore = new v(), this.conversationManager = new S(t.conversationId), this.connection.on("connection:state", (e) => {
|
|
421
426
|
this.emit("connection:state", e), this.config.onConnectionStateChange?.(e);
|
|
422
427
|
}), this.connection.on("error", (e) => {
|
|
423
428
|
this.emit("error", e), this.config.onError?.(e);
|
|
@@ -470,9 +475,51 @@ class U extends m {
|
|
|
470
475
|
get isConnected() {
|
|
471
476
|
return this.connection.connectionState === "connected";
|
|
472
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Update the agent's context with information about the host app's current state.
|
|
480
|
+
* Call this on route changes so the agent knows what page/entity the user is viewing.
|
|
481
|
+
*
|
|
482
|
+
* The context is stored in conversation metadata and interpolated into the system prompt.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* // On route change
|
|
487
|
+
* client.setContext({
|
|
488
|
+
* currentPage: '/products/42',
|
|
489
|
+
* entityType: 'product',
|
|
490
|
+
* entityId: '42',
|
|
491
|
+
* availableRoutes: { products: '/products', orders: '/orders' },
|
|
492
|
+
* });
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
async setContext(t) {
|
|
496
|
+
const e = this.conversationManager.conversationId;
|
|
497
|
+
e && await this.api.updateContext(e, {
|
|
498
|
+
current_page: t.currentPage,
|
|
499
|
+
entity_type: t.entityType,
|
|
500
|
+
entity_id: t.entityId,
|
|
501
|
+
project_id: t.projectId,
|
|
502
|
+
available_routes: t.availableRoutes,
|
|
503
|
+
custom_context: t.custom
|
|
504
|
+
});
|
|
505
|
+
}
|
|
473
506
|
async disconnect() {
|
|
474
507
|
this.subscription && (this.subscription = null), await this.connection.disconnect();
|
|
475
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Parse structured action results from tool execution output.
|
|
511
|
+
* When the agent calls tools like `edit_current_entity`, `save_current_entity`,
|
|
512
|
+
* `navigate_to`, or `test_tool_by_id`, the result contains an action payload
|
|
513
|
+
* that the SDK emits as a typed event for the host app to handle.
|
|
514
|
+
*/
|
|
515
|
+
parseActionResult(t) {
|
|
516
|
+
try {
|
|
517
|
+
const e = typeof t == "string" ? JSON.parse(t) : t;
|
|
518
|
+
if (!e || typeof e != "object") return;
|
|
519
|
+
e.navigate_to ? this.emit("action:navigate", e) : e.action === "edit_entity" ? this.emit("action:edit_entity", e) : e.action === "save_entity" ? this.emit("action:save_entity", e) : e.action === "test_tool" && this.emit("action:test_tool", e);
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
476
523
|
handleBroadcast(t) {
|
|
477
524
|
const e = this.conversationManager.conversationId;
|
|
478
525
|
switch (t.type) {
|
|
@@ -515,9 +562,22 @@ class U extends m {
|
|
|
515
562
|
s && this.messageStore.confirmOptimistic(s.id);
|
|
516
563
|
break;
|
|
517
564
|
}
|
|
518
|
-
case "tool_execution_start":
|
|
565
|
+
case "tool_execution_start": {
|
|
566
|
+
this.emit("tool:start", {
|
|
567
|
+
toolName: t.tool_name || "",
|
|
568
|
+
args: t.args || {}
|
|
569
|
+
}), this.emit("status", t.type);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "tool_execution_end": {
|
|
573
|
+
this.emit("tool:end", {
|
|
574
|
+
toolName: t.tool_name || "",
|
|
575
|
+
result: t.result,
|
|
576
|
+
isError: !!t.is_error
|
|
577
|
+
}), this.emit("status", t.type), this.parseActionResult(t.result);
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
519
580
|
case "tool_execution_update":
|
|
520
|
-
case "tool_execution_end":
|
|
521
581
|
case "agent_start":
|
|
522
582
|
case "agent_end":
|
|
523
583
|
case "turn_start":
|
|
@@ -537,15 +597,15 @@ export {
|
|
|
537
597
|
m as E,
|
|
538
598
|
v as M,
|
|
539
599
|
U as a,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
600
|
+
y as b,
|
|
601
|
+
b as c,
|
|
602
|
+
S as d,
|
|
543
603
|
C as e,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
k,
|
|
550
|
-
|
|
604
|
+
I as f,
|
|
605
|
+
E as g,
|
|
606
|
+
k as h,
|
|
607
|
+
w as i,
|
|
608
|
+
A as j,
|
|
609
|
+
T as k,
|
|
610
|
+
M as l
|
|
551
611
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";class p{identifier;callbacks={};sendFn;constructor(t,e){this.identifier=t,this.sendFn=e}onReceived(t){return this.callbacks.received=t,this}onConnected(t){return this.callbacks.connected=t,this}onDisconnected(t){return this.callbacks.disconnected=t,this}onRejected(t){return this.callbacks.rejected=t,this}perform(t,e={}){this.sendFn({action:t,...e})}_notifyReceived(t){this.callbacks.received?.(t)}_notifyConnected(){this.callbacks.connected?.()}_notifyDisconnected(){this.callbacks.disconnected?.()}_notifyRejected(){this.callbacks.rejected?.()}}class l{ws=null;url;subscriptions=new Map;welcomePromise=null;pendingSubscriptions=new Map;constructor(t){this.url=t}connect(){return new Promise((t,e)=>{this.welcomePromise={resolve:t,reject:e},this.ws=new WebSocket(this.url),this.ws.onopen=()=>{},this.ws.onmessage=s=>{this.handleMessage(s)},this.ws.onerror=()=>{const s=new Error("WebSocket connection error");this.welcomePromise?.reject(s),this.welcomePromise=null},this.ws.onclose=()=>{this.subscriptions.forEach(s=>s._notifyDisconnected())}})}disconnect(){this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null),this.subscriptions.forEach(t=>t._notifyDisconnected()),this.subscriptions.clear()}subscribe(t){const e=JSON.stringify(t),s=new p(e,n=>{this.send({command:"message",identifier:e,data:JSON.stringify(n)})});return this.subscriptions.set(e,s),this.send({command:"subscribe",identifier:e}),s}subscribeAsync(t){const e=this.subscribe(t),s=e.identifier;return new Promise((n,i)=>{this.pendingSubscriptions.set(s,{resolve:()=>n(e),reject:i}),setTimeout(()=>{this.pendingSubscriptions.has(s)&&(this.pendingSubscriptions.delete(s),i(new Error(`Subscription timeout for ${s}`)))},1e4)})}unsubscribe(t){this.send({command:"unsubscribe",identifier:t}),this.subscriptions.delete(t)}perform(t,e,s={}){this.send({command:"message",identifier:t,data:JSON.stringify({action:e,...s})})}get isConnected(){return this.ws?.readyState===WebSocket.OPEN}send(t){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(t))}handleMessage(t){let e;try{e=JSON.parse(t.data)}catch{return}switch(e.type){case"welcome":this.welcomePromise?.resolve(),this.welcomePromise=null;break;case"ping":break;case"confirm_subscription":{const s=e.identifier;this.subscriptions.get(s)?._notifyConnected();const i=this.pendingSubscriptions.get(s);i&&(i.resolve(),this.pendingSubscriptions.delete(s));break}case"reject_subscription":{const s=e.identifier;this.subscriptions.get(s)?._notifyRejected(),this.subscriptions.delete(s);const i=this.pendingSubscriptions.get(s);i&&(i.reject(new Error(`Subscription rejected: ${s}`)),this.pendingSubscriptions.delete(s));break}case"disconnect":this.subscriptions.forEach(s=>s._notifyDisconnected());break;default:{e.identifier&&e.message!==void 0&&this.subscriptions.get(e.identifier)?._notifyReceived(e.message);break}}}}class u{handlers=new Map;on(t,e){return this.handlers.has(t)||this.handlers.set(t,new Set),this.handlers.get(t).add(e),()=>this.off(t,e)}off(t,e){this.handlers.get(t)?.delete(e)}emit(t,e){this.handlers.get(t)?.forEach(s=>{try{s(e)}catch(n){console.error(`Error in event handler for "${t}":`,n)}})}removeAllListeners(){this.handlers.clear()}}const y=1e3,S=10,w=400,T=600,E="#6366f1",I=12,k="system-ui, -apple-system, sans-serif",A="Type a message...",C="bottom-right",M="light",O={x:20,y:20},d="aikaara_conversation_id";class f extends u{client;config;state="disconnected";reconnectAttempt=0;reconnectTimer=null;constructor(t){super(),this.config=t;const e=this.buildWsUrl(t.baseUrl,t.userToken);this.client=new l(e)}async connect(){this.setState("connecting");try{await this.client.connect(),this.setState("connected"),this.reconnectAttempt=0}catch(t){if(this.setState("disconnected"),this.config.reconnect!==!1)this.scheduleReconnect();else throw t}}async disconnect(){this.clearReconnectTimer(),this.client.disconnect(),this.setState("disconnected")}subscribeToConversation(t){return this.client.subscribeAsync({channel:"ConversationChannel",conversation_id:t})}sendMessage(t,e){const s=JSON.stringify({channel:"ConversationChannel",conversation_id:t});this.client.perform(s,"send_message",{content:e})}sendUserEvent(t,e,s,n){const i=JSON.stringify({channel:"ConversationChannel",conversation_id:t});this.client.perform(i,"send_user_event",{event_key:e,...s&&{value:s},...n&&{source:n}})}get connectionState(){return this.state}setState(t){this.state!==t&&(this.state=t,this.emit("connection:state",t))}scheduleReconnect(){const t=this.config.maxReconnectAttempts??S;if(this.reconnectAttempt>=t){this.emit("error",new Error("Max reconnection attempts reached"));return}this.setState("reconnecting");const s=(this.config.reconnectInterval??y)*Math.pow(2,this.reconnectAttempt);this.reconnectAttempt++,this.reconnectTimer=setTimeout(async()=>{try{const n=this.buildWsUrl(this.config.baseUrl,this.config.userToken);this.client=new l(n),await this.connect()}catch{}},s)}clearReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}buildWsUrl(t,e){if(this.config.wsUrl){const n=this.config.wsUrl.includes("?")?"&":"?";return`${this.config.wsUrl}${n}token=${e}`}return`${t.replace(/^http/,"ws")}/cable?token=${e}`}}class _{baseUrl;apiKey;authToken;userToken;constructor(t,e,s,n){this.baseUrl=t,this.userToken=e,this.apiKey=s,this.authToken=n}async createConversation(t){const e={conversation:{...t.extUid&&{ext_uid:t.extUid},...t.systemPromptId&&{system_prompt_id:t.systemPromptId},...t.channel&&{channel:t.channel},...t.title&&{title:t.title}}};return this.request("POST","/api/v1/conversations",e)}async updateContext(t,e){const s=this.authToken?`/dashboard/sidekick_conversations/${t}`:`/api/v1/conversations/${t}`;await this.request("PATCH",s,e)}async getMessages(t){return(await this.request("GET",`/api/v1/conversations/${t}/messages`)).map(this.mapMessage)}mapMessage(t){return{id:String(t.id),conversationId:String(t.conversation_id),role:t.role,content:t.content||"",toolCalls:t.tool_calls?.map(e=>({id:e.id,type:e.type,function:e.function})),toolCallResults:t.tool_call_results,tokensInput:t.tokens_input,tokensOutput:t.tokens_output,metadata:t.metadata,createdAt:t.created_at,status:"complete"}}async request(t,e,s){const n={"Content-Type":"application/json",Accept:"application/json"};this.apiKey&&(n["X-Api-Key"]=this.apiKey),this.authToken&&(n.Authorization=`Bearer ${this.authToken}`);const i=`${this.baseUrl}${e}`,g={method:t,headers:n};s&&(g.body=JSON.stringify(s));const a=await fetch(i,g);if(!a.ok){const c=await a.text();let h;try{const m=JSON.parse(c);h=m.error||m.message||c}catch{h=c}throw new Error(`API error ${a.status}: ${h}`)}const r=await a.json();if(r&&typeof r=="object"&&"success"in r){if(!r.success)throw new Error(`API error: ${r.message||"Request failed"}`);return r.data}return r}}class b{_messages=[];optimisticCounter=0;get messages(){return[...this._messages]}addOptimistic(t,e,s){const n={id:`optimistic_${++this.optimisticCounter}`,conversationId:s,role:t,content:e,createdAt:new Date().toISOString(),status:"sending"};return this._messages.push(n),n}confirmOptimistic(t){const e=this._messages.find(s=>s.id===t);e&&(e.status="sent")}addStreamingMessage(t){const e={id:`streaming_${Date.now()}`,conversationId:t,role:"assistant",content:"",createdAt:new Date().toISOString(),status:"streaming"};return this._messages.push(e),e}updateStreaming(t){const e=this._messages.findLast(s=>s.status==="streaming");e&&(e.content=t)}appendToStreaming(t){const e=this._messages.findLast(s=>s.status==="streaming");e&&(e.content+=t)}get streamingContent(){return this._messages.findLast(e=>e.status==="streaming")?.content||""}finalizeStreaming(t){const e=this._messages.findLast(s=>s.status==="streaming");return e&&(e.status="complete",t&&(e.tokensInput=t.tokensInput,e.tokensOutput=t.tokensOutput)),e}addMessage(t){this._messages.push(t)}setMessages(t){this._messages=[...t]}clear(){this._messages=[]}}class v{_conversationId;persist;constructor(t,e=!0){this.persist=e,this._conversationId=t||this.loadFromStorage()}get conversationId(){return this._conversationId}set conversationId(t){this._conversationId=t,this.persist&&t&&this.saveToStorage(t)}clear(){if(this._conversationId=null,this.persist)try{localStorage.removeItem(d)}catch{}}loadFromStorage(){if(!this.persist)return null;try{return localStorage.getItem(d)}catch{return null}}saveToStorage(t){try{localStorage.setItem(d,t)}catch{}}}class U extends u{connection;api;messageStore;conversationManager;subscription=null;config;constructor(t){super(),this.config=t,this.connection=new f(t),this.api=new _(t.baseUrl,t.userToken,t.apiKey,t.authToken),this.messageStore=new b,this.conversationManager=new v(t.conversationId),this.connection.on("connection:state",e=>{this.emit("connection:state",e),this.config.onConnectionStateChange?.(e)}),this.connection.on("error",e=>{this.emit("error",e),this.config.onError?.(e)})}async connect(){if(await this.connection.connect(),!this.conversationManager.conversationId){const t=await this.api.createConversation({systemPromptId:this.config.systemPromptId,channel:this.config.channel||"widget",extUid:this.config.extUid});this.conversationManager.conversationId=String(t.id)}this.subscription=await this.connection.subscribeToConversation(this.conversationManager.conversationId),this.subscription.onReceived(t=>{this.handleBroadcast(t)}),await this.loadHistory()}async sendMessage(t){const e=this.conversationManager.conversationId;if(!e)throw new Error("No active conversation");const s=this.messageStore.addOptimistic("user",t,e);this.emit("message:sent",s),this.config.onMessage?.(s),this.connection.sendMessage(e,t)}async sendUserEvent(t,e,s){const n=this.conversationManager.conversationId;if(!n)throw new Error("No active conversation");this.connection.sendUserEvent(n,t,e,s)}async loadHistory(){const t=this.conversationManager.conversationId;if(!t)return[];try{const e=await this.api.getMessages(t);return this.messageStore.setMessages(e),e}catch{return[]}}get messages(){return this.messageStore.messages}get conversationId(){return this.conversationManager.conversationId}get isConnected(){return this.connection.connectionState==="connected"}async setContext(t){const e=this.conversationManager.conversationId;e&&await this.api.updateContext(e,{current_page:t.currentPage,entity_type:t.entityType,entity_id:t.entityId,project_id:t.projectId,available_routes:t.availableRoutes,custom_context:t.custom})}async disconnect(){this.subscription&&(this.subscription=null),await this.connection.disconnect()}parseActionResult(t){try{const e=typeof t=="string"?JSON.parse(t):t;if(!e||typeof e!="object")return;e.navigate_to?this.emit("action:navigate",e):e.action==="edit_entity"?this.emit("action:edit_entity",e):e.action==="save_entity"?this.emit("action:save_entity",e):e.action==="test_tool"&&this.emit("action:test_tool",e)}catch{}}handleBroadcast(t){const e=this.conversationManager.conversationId;switch(t.type){case"status":{const s=t.status;this.emit("status",s),this.config.onStatusChange?.(s),s==="processing"&&this.emit("typing:start",void 0);break}case"error":{const s=new Error(t.message||"Unknown error");this.emit("error",s),this.config.onError?.(s);break}case"message_start":{if(t.role==="assistant"){const s=this.messageStore.addStreamingMessage(e);this.emit("stream:start",{messageId:s.id}),this.emit("typing:start",void 0)}break}case"message_update":{const s=t.delta||"",n=t.content||"";n?this.messageStore.updateStreaming(n):s&&this.messageStore.appendToStreaming(s);const i=this.messageStore.streamingContent;this.emit("stream:update",{delta:s,content:i}),this.config.onStreamUpdate?.(s,i);break}case"message_end":{const s=t.usage,n=this.messageStore.finalizeStreaming(s?{tokensInput:s.tokens_input||0,tokensOutput:s.tokens_output||0}:void 0);this.emit("typing:stop",void 0),n&&(this.emit("stream:end",{messageId:n.id,usage:s?{tokensInput:s.tokens_input||0,tokensOutput:s.tokens_output||0}:void 0}),this.emit("message:received",n),this.config.onMessage?.(n));break}case"message_queued":{const s=this.messageStore.messages.findLast(n=>n.status==="sending");s&&this.messageStore.confirmOptimistic(s.id);break}case"tool_execution_start":{this.emit("tool:start",{toolName:t.tool_name||"",args:t.args||{}}),this.emit("status",t.type);break}case"tool_execution_end":{this.emit("tool:end",{toolName:t.tool_name||"",result:t.result,isError:!!t.is_error}),this.emit("status",t.type),this.parseActionResult(t.result);break}case"tool_execution_update":case"agent_start":case"agent_end":case"turn_start":case"turn_end":case"auto_retry_start":case"auto_retry_end":case"cancelled":this.emit("status",t.type);break}}}exports.ActionCableClient=l;exports.AikaaraChatClient=U;exports.ApiClient=_;exports.ChannelSubscription=p;exports.ConnectionManager=f;exports.ConversationManager=v;exports.DEFAULT_BORDER_RADIUS=I;exports.DEFAULT_FONT_FAMILY=k;exports.DEFAULT_OFFSET=O;exports.DEFAULT_PLACEHOLDER=A;exports.DEFAULT_POSITION=C;exports.DEFAULT_PRIMARY_COLOR=E;exports.DEFAULT_THEME=M;exports.DEFAULT_WIDGET_HEIGHT=T;exports.DEFAULT_WIDGET_WIDTH=w;exports.EventEmitter=u;exports.MessageStore=b;
|