@aikaara/chat-sdk 0.1.3 → 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 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