flowstack-sdk 0.2.1
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/LICENSE +21 -0
- package/README.md +2278 -0
- package/dist/api/index.d.mts +1 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1065 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/index.mjs +977 -0
- package/dist/api/index.mjs.map +1 -0
- package/dist/index-BkACA2ls.d.mts +1546 -0
- package/dist/index-BkACA2ls.d.ts +1546 -0
- package/dist/index.d.mts +2325 -0
- package/dist/index.d.ts +2325 -0
- package/dist/index.js +10817 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +10610 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-BmCPwbGH.d.mts +153 -0
- package/dist/types-BmCPwbGH.d.ts +153 -0
- package/dist/wallet/index.d.mts +195 -0
- package/dist/wallet/index.d.ts +195 -0
- package/dist/wallet/index.js +1205 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/index.mjs +1190 -0
- package/dist/wallet/index.mjs.map +1 -0
- package/package.json +110 -0
package/README.md
ADDED
|
@@ -0,0 +1,2278 @@
|
|
|
1
|
+
# Flowstack SDK
|
|
2
|
+
|
|
3
|
+
Complete Backend-as-a-Service SDK for AI-powered applications. Build full-stack AI apps with authentication, workspace management, dataset handling, AI agent integration, published sites, and wallet-based payments β all in one package.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install flowstack-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
- π **Docs site:** https://flowstack.fun/docs β browsable reference for every hook, component, and utility.
|
|
12
|
+
- π **REST & SSE API:** https://flowstack.fun/openapi.json (OpenAPI 3.1) Β· human view: https://flowstack.fun/docs/rest-api
|
|
13
|
+
- π€ **For LLMs & coding agents:** https://flowstack.fun/llms.txt (index) Β· https://flowstack.fun/llms-full.txt (full corpus)
|
|
14
|
+
- π§ **Machine-readable:** [semantic graph](https://flowstack.fun/graph.jsonld) Β· [search index](https://flowstack.fun/search-index.json) Β· [registry JSON](https://flowstack.fun/api/registry.json)
|
|
15
|
+
|
|
16
|
+
The docs site is generated from a canonical registry, so the human docs, the agent-readable corpus, the semantic graph, and the API contract never drift apart. This README is the canonical source for the SDK reference.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## MCP Build Agent β Required Pre-Flight Checklist
|
|
21
|
+
|
|
22
|
+
> **If you are an AI agent using the Casino MCP tools (`casino_upload_source`, `casino_build_staged`, etc.), read this section before writing any code. These are hard constraints from production failures.**
|
|
23
|
+
|
|
24
|
+
### File upload rules
|
|
25
|
+
- `upload_source` requires ALL files in one call. The sandbox is fresh every build β no files persist from previous runs.
|
|
26
|
+
- Required minimum: `package.json`, `index.html`, `vite.config.ts`, `tsconfig.json`, `tsconfig.node.json`, `src/main.tsx`, `src/App.tsx`, and every file they import.
|
|
27
|
+
- `casino_get_app_source` returns `{}` (empty) after most builds β do NOT rely on it to retrieve previous source. Regenerate from scratch.
|
|
28
|
+
- `casino_stage_file` (singular) is removed. Use `casino_upload_source` with all files in a single dict.
|
|
29
|
+
|
|
30
|
+
### Buffer polyfill β FIRST lines of `src/main.tsx`
|
|
31
|
+
```ts
|
|
32
|
+
import { Buffer } from 'buffer';
|
|
33
|
+
if (typeof (globalThis as any).Buffer === 'undefined') { (globalThis as any).Buffer = Buffer; }
|
|
34
|
+
// all other imports below
|
|
35
|
+
```
|
|
36
|
+
Without this: `"Can't find variable: Buffer"` crash at runtime. Add `"buffer": "^6.0.3"` to `package.json` dependencies.
|
|
37
|
+
|
|
38
|
+
### `vite.config.ts` β always include
|
|
39
|
+
```ts
|
|
40
|
+
export default defineConfig({ base: '/', plugins: [react()], define: { global: 'globalThis' } });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `package.json` β always include `mermaid` and `buffer`
|
|
44
|
+
```json
|
|
45
|
+
"mermaid": "^11.4.1", "buffer": "^6.0.3"
|
|
46
|
+
```
|
|
47
|
+
Missing `mermaid` β Rollup dynamic-import error. Missing `buffer` β Buffer polyfill fails.
|
|
48
|
+
|
|
49
|
+
### Authentication β `BrokeredLoginButton` only
|
|
50
|
+
```tsx
|
|
51
|
+
import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
|
|
52
|
+
// Never use useAuth().login(), LoginButton from flowstack-sdk/wallet, or privyConfig.appId
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Styles β inline or upload, never import without uploading
|
|
56
|
+
If `main.tsx` has `import './styles.css'`, upload `src/styles.css` too. Or inline styles as a JS style element in `main.tsx` and remove the import.
|
|
57
|
+
|
|
58
|
+
### `AuthGuard` β never use `redirectTo`
|
|
59
|
+
Use `fallback={<BrokeredLoginButton />}` instead. `redirectTo="/"` kicks users out before they can sign in. For built apps with an `appScope`, `AuthGuard` can also transparently issue a guest session (`allowGuest`, default `true`) when the site has opted into guest chat β see [Add authentication](#2-add-authentication).
|
|
60
|
+
|
|
61
|
+
### CRITICAL β `upload_source` creates a DRAFT, not a live update
|
|
62
|
+
|
|
63
|
+
`casino_upload_source` returns `version_status: "draft"`. **The live site does NOT change until you call `casino_promote_version`.** Users are still on the old version. Always complete the two-step workflow:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
1. casino_upload_source(site_id, files) β check status="success", note version number
|
|
67
|
+
2. casino_promote_version(site_id, version) β NOW the site goes live
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If you skip step 2, the build succeeded but nobody sees it. There is no automatic promotion.
|
|
71
|
+
|
|
72
|
+
**Simpler alternative:** Use `casino_build_staged` which builds AND goes live in one call (no promote needed):
|
|
73
|
+
```
|
|
74
|
+
1. casino_upload_source(...) β build + validate as draft
|
|
75
|
+
OR
|
|
76
|
+
casino_build_staged(...) β build + go live immediately (preferred for updates)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### If build fails, read `build_log`:
|
|
80
|
+
- `"No package.json"` β add it to the files dict
|
|
81
|
+
- `"Could not resolve './X'"` β add the missing file
|
|
82
|
+
- `"Cannot find variable: Buffer"` β Buffer polyfill missing or not the first lines
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Table of Contents
|
|
87
|
+
|
|
88
|
+
- [Installation](#installation)
|
|
89
|
+
- [**Built Apps β Generated Site Pattern**](#built-apps--generated-site-pattern)
|
|
90
|
+
- [Quick Start (Casino Platform)](#quick-start-casino-platform)
|
|
91
|
+
- [Complete Example App (Casino Platform)](#complete-example-app-casino-platform)
|
|
92
|
+
- [Hooks Reference](#hooks-reference)
|
|
93
|
+
- [useAuth](#useauth)
|
|
94
|
+
- [useCollection](#usecollection)
|
|
95
|
+
- [usePublicCollection](#usepubliccollection)
|
|
96
|
+
- [useWorkspace](#useworkspace)
|
|
97
|
+
- [useAgent](#useagent)
|
|
98
|
+
- [useDatasets](#usedatasets)
|
|
99
|
+
- [useVisualizations](#usevisualizations)
|
|
100
|
+
- [useReports](#usereports)
|
|
101
|
+
- [useModels](#usemodels)
|
|
102
|
+
- [useSites](#usesites)
|
|
103
|
+
- [useDataSources](#usedatasources)
|
|
104
|
+
- [useQuery](#usequery)
|
|
105
|
+
- [useUserManagement](#useusermanagement)
|
|
106
|
+
- [useAuthGuard](#useauthguard)
|
|
107
|
+
- [useFlowstackStatus](#useflowstackstatus)
|
|
108
|
+
- [Additional Hooks (0.5+)](#additional-hooks-05)
|
|
109
|
+
- [Components](#components)
|
|
110
|
+
- [Wallet Module](#wallet-module)
|
|
111
|
+
- [Agent Templates & Factory](#agent-templates--factory)
|
|
112
|
+
- [API Client](#api-client)
|
|
113
|
+
- [Streaming Utilities](#streaming-utilities)
|
|
114
|
+
- [Cache Utilities](#cache-utilities)
|
|
115
|
+
- [API Route Generators](#api-route-generators)
|
|
116
|
+
- [Mock Mode](#mock-mode)
|
|
117
|
+
- [Configuration](#configuration)
|
|
118
|
+
- [Error Handling](#error-handling)
|
|
119
|
+
- [TypeScript](#typescript)
|
|
120
|
+
|
|
121
|
+
## Installation
|
|
122
|
+
|
|
123
|
+
For generated sites (CDN tarball β no registry auth needed):
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"dependencies": {
|
|
128
|
+
"flowstack-sdk": "https://sagecdn.flowstack.fun/sdk/flowstack-sdk-latest.tgz"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For local development:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm install flowstack-sdk
|
|
137
|
+
# or with local copy:
|
|
138
|
+
# "flowstack-sdk": "file:packages/flowstack-sdk"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Built Apps β Generated Site Pattern
|
|
142
|
+
|
|
143
|
+
**This is the primary pattern for sites generated by the build pipeline.** Built apps use `appScope` for per-app data isolation β they do NOT use workspaces.
|
|
144
|
+
|
|
145
|
+
### Architecture
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
useCollection β /collections/* β MongoDB (the app reads/writes data β tables, forms, cards)
|
|
149
|
+
useAgent β /stream β LLM Agent (chat AND one-shot tasks that write results to MongoDB)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **`useCollection`** is how the app reads and directly writes data (read, insert, update, delete). Direct MongoDB, no agent.
|
|
153
|
+
- **`useAgent`** handles chat *and* acts as a worker: a one-shot `query()` can have the agent **reason over data you pass in and write a structured result document** to a collection via its `data_access` tools, which the app then reads back with `useCollection`. See [Agent-Driven Data Models β Reason β Write β Read](#agent-driven-data-models--reason--write--read). MongoDB is schemaless, so the agent can write any document shape (nested objects/arrays, optional fields) with no migration β the prompt defines the shape.
|
|
154
|
+
|
|
155
|
+
### Minimal Built App
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { FlowstackProvider, useFlowstack, BrokeredLoginButton, useCollection, useAgent } from 'flowstack-sdk';
|
|
159
|
+
|
|
160
|
+
function App() {
|
|
161
|
+
return (
|
|
162
|
+
<FlowstackProvider config={{
|
|
163
|
+
baseUrl: 'https://sage-api.flowstack.fun',
|
|
164
|
+
mode: 'production',
|
|
165
|
+
appScope: '__SITE_ID__', // Hex site ID β auto-filled by build pipeline
|
|
166
|
+
// No tenantId needed β it's derived from the brokered-login JWT. Only set it
|
|
167
|
+
// if you use usePublicCollection (anonymous data has no token to derive from).
|
|
168
|
+
}}>
|
|
169
|
+
<AuthGate />
|
|
170
|
+
</FlowstackProvider>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function AuthGate() {
|
|
175
|
+
const { isAuthenticated, isInitialized } = useFlowstack();
|
|
176
|
+
if (!isInitialized) return <div>Loading...</div>;
|
|
177
|
+
// BrokeredLoginButton opens Casino's auth popup β Privy runs there, not here
|
|
178
|
+
return isAuthenticated ? <MainApp /> : (
|
|
179
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100dvh' }}>
|
|
180
|
+
<BrokeredLoginButton label="Continue with Flowstack" />
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function MainApp() {
|
|
186
|
+
const { credentials, setCredentials } = useFlowstack();
|
|
187
|
+
// Data β direct MongoDB
|
|
188
|
+
const { documents, insert, remove } = useCollection('contacts', {
|
|
189
|
+
sort: { created_at: -1 },
|
|
190
|
+
limit: 50,
|
|
191
|
+
refreshOnAgentComplete: true,
|
|
192
|
+
});
|
|
193
|
+
// AI chat β LLM agent (target an agent you registered via casino_create_agent)
|
|
194
|
+
const { query, messages, isStreaming } = useAgent(undefined, {
|
|
195
|
+
targetAgents: ['assistant'], // name you registered with casino_create_agent
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div>
|
|
200
|
+
<header>
|
|
201
|
+
{credentials?.email}
|
|
202
|
+
<button onClick={() => setCredentials(null)}>Sign out</button>
|
|
203
|
+
</header>
|
|
204
|
+
<ContactTable contacts={documents} onDelete={(id) => remove({ _id: id })} />
|
|
205
|
+
<ContactForm onSubmit={(data) => insert({ ...data, created_at: new Date().toISOString() })} />
|
|
206
|
+
<ChatSidebar query={query} messages={messages} isStreaming={isStreaming} />
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Key Rules for Built Apps
|
|
213
|
+
|
|
214
|
+
1. **`appScope` is required** in FlowstackProvider config. It scopes all data to this app. The build pipeline fills `__SITE_ID__` with the hex site_id.
|
|
215
|
+
- **Don't set `tenantId`.** For authenticated calls the backend reads it from the brokered-login JWT and ignores any client-sent value, so it's derived automatically. The *only* exception is [`usePublicCollection`](#usepubliccollection) (anonymous access has no token) β set `tenantId` on the provider there, or the hook errors.
|
|
216
|
+
2. **No workspace selection.** Built apps do NOT use `useWorkspace` or `selectWorkspace`. The backend creates workspaces automatically.
|
|
217
|
+
3. **`targetAgents` (plural array)** is required on `useAgent`. Without it, app-scoped users get 403. The name must match an agent you registered via `casino_create_agent`. Example: `targetAgents: ['support_bot']`
|
|
218
|
+
4. **SHORT collection names only.** Pass `'contacts'`, NOT `'site_abc__contacts'`. The backend auto-prefixes with the site_id.
|
|
219
|
+
5. **`useCollection` for ALL data** β tables, forms, cards, CRUD. Never route data through the agent.
|
|
220
|
+
6. **`useAgent` for AI only** β chat sidebar, analysis, reasoning. The agent reads the same MongoDB collections but is never the data layer.
|
|
221
|
+
|
|
222
|
+
### Auth Flow (Built Apps)
|
|
223
|
+
|
|
224
|
+
Built apps authenticate via `BrokeredLoginButton` β not email/password, not Google sign-in, not the wallet module. The broker opens Casino's auth popup, Casino runs Privy on its registered origin, and Privy creates an embedded wallet for the user. The popup closes and posts a scoped JWT back to the app.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
|
|
228
|
+
|
|
229
|
+
function LoginPage() {
|
|
230
|
+
// No email, no password, no Privy appId needed here
|
|
231
|
+
return (
|
|
232
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100dvh', gap: '1rem' }}>
|
|
233
|
+
<h1>Welcome</h1>
|
|
234
|
+
<BrokeredLoginButton label="Continue with Flowstack" />
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// After auth, read identity from useFlowstack() β not useAuth()
|
|
240
|
+
function Header() {
|
|
241
|
+
const { credentials, setCredentials } = useFlowstack();
|
|
242
|
+
return (
|
|
243
|
+
<header>
|
|
244
|
+
<span>{credentials?.email}</span>
|
|
245
|
+
<button onClick={() => setCredentials(null)}>Sign out</button>
|
|
246
|
+
</header>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> **Why not email/password?** End-users of built apps don't have Casino email/password accounts β they authenticate via Privy (embedded wallet + email/Google) through the broker. `useAuth().login(email, password)` will always return an error for built-app end-users.
|
|
252
|
+
|
|
253
|
+
### Data Operations (useCollection)
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// READ β reactive query, auto-refreshes after any mutation
|
|
257
|
+
const { documents, isLoading, total } = useCollection<Contact>('contacts', {
|
|
258
|
+
filter: { status: 'active' },
|
|
259
|
+
sort: { created_at: -1 },
|
|
260
|
+
limit: 50,
|
|
261
|
+
refreshOnAgentComplete: true, // auto-refresh when agent writes via chat
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// INSERT β all useCollection('contacts') instances auto-refresh
|
|
265
|
+
const { insert } = useCollection('contacts');
|
|
266
|
+
await insert({ name: 'Jane', email: 'jane@co.com', created_at: new Date().toISOString() });
|
|
267
|
+
|
|
268
|
+
// BATCH INSERT β up to 100 documents
|
|
269
|
+
await insert([{ name: 'A' }, { name: 'B' }, { name: 'C' }]);
|
|
270
|
+
|
|
271
|
+
// UPDATE
|
|
272
|
+
const { update } = useCollection('contacts');
|
|
273
|
+
await update({ _id: docId }, { $set: { status: 'inactive' } });
|
|
274
|
+
|
|
275
|
+
// DELETE
|
|
276
|
+
const { remove } = useCollection('contacts');
|
|
277
|
+
await remove({ _id: docId });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### AI Chat (useAgent)
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { useAgent } from 'flowstack-sdk';
|
|
284
|
+
import ReactMarkdown from 'react-markdown';
|
|
285
|
+
import { useState, useEffect, useRef } from 'react';
|
|
286
|
+
|
|
287
|
+
// CRITICAL: targetAgents must be a plural ARRAY
|
|
288
|
+
// Use the name you registered via casino_create_agent for this site
|
|
289
|
+
const { query, messages, isStreaming } = useAgent(undefined, {
|
|
290
|
+
targetAgents: ['assistant'],
|
|
291
|
+
});
|
|
292
|
+
const [savedMessages, setSavedMessages] = useState<any[]>([]);
|
|
293
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
294
|
+
|
|
295
|
+
// Restore chat history on mount
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
try {
|
|
298
|
+
const stored = sessionStorage.getItem('chat_history');
|
|
299
|
+
if (stored) setSavedMessages(JSON.parse(stored));
|
|
300
|
+
} catch {}
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
// Save after each completed response (cap at 50 messages)
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (isStreaming || messages.length === 0) return;
|
|
306
|
+
const all = [...savedMessages, ...messages].slice(-50);
|
|
307
|
+
sessionStorage.setItem('chat_history', JSON.stringify(all));
|
|
308
|
+
}, [messages, isStreaming]);
|
|
309
|
+
|
|
310
|
+
// Auto-scroll to newest message
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
313
|
+
}, [messages, isStreaming]);
|
|
314
|
+
|
|
315
|
+
// query() is a STREAMING function β it does NOT return the response text.
|
|
316
|
+
await query('Analyze my contacts by company size');
|
|
317
|
+
|
|
318
|
+
// Render with height-constrained container
|
|
319
|
+
<div className="chat-container"> {/* max-height: 80vh; flex column */}
|
|
320
|
+
<div className="chat-messages"> {/* flex: 1; overflow-y: auto; min-height: 0 */}
|
|
321
|
+
{[...savedMessages, ...messages].map(m => (
|
|
322
|
+
<div key={m.id} className={`message ${m.role}`}>
|
|
323
|
+
{m.role === 'assistant'
|
|
324
|
+
? <ReactMarkdown>{m.content}</ReactMarkdown>
|
|
325
|
+
: <p>{m.content}</p>}
|
|
326
|
+
</div>
|
|
327
|
+
))}
|
|
328
|
+
<div ref={messagesEndRef} />
|
|
329
|
+
</div>
|
|
330
|
+
<div className="chat-input"> {/* flex-shrink: 0 */}
|
|
331
|
+
<input ... />
|
|
332
|
+
<button>Send</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
> **Chat UX Requirements (built apps):**
|
|
338
|
+
> - Container: `max-height: 80vh`, flex column
|
|
339
|
+
> - Messages: `overflow-y: auto`, `min-height: 0` (required for flex child scrolling)
|
|
340
|
+
> - Input: `flex-shrink: 0`, pinned at bottom
|
|
341
|
+
> - Auto-scroll to newest message on `[messages, isStreaming]` change
|
|
342
|
+
> - Load/save chat history via `sessionStorage` (last 50 messages)
|
|
343
|
+
> - Wrap assistant messages in `<ReactMarkdown>` (package included in template)
|
|
344
|
+
> - Markdown links must be clickable (`<a>` tags with `target="_blank"`)
|
|
345
|
+
|
|
346
|
+
**Markdown CSS for chat messages** β add these styles so rendered markdown is readable and interactive:
|
|
347
|
+
```css
|
|
348
|
+
/* Links must be clickable and visually distinct */
|
|
349
|
+
.message a, .message.assistant a { color: var(--primary, #2563eb); text-decoration: underline; cursor: pointer; }
|
|
350
|
+
.message a:hover { opacity: 0.8; }
|
|
351
|
+
/* Tables */
|
|
352
|
+
.message table { width: 100%; border-collapse: collapse; margin: 0.5em 0; font-size: 0.9em; }
|
|
353
|
+
.message th, .message td { padding: 0.4rem 0.6rem; border: 1px solid var(--border, #e2e8f0); text-align: left; }
|
|
354
|
+
.message th { background: var(--surface-alt, #f8fafc); font-weight: 600; }
|
|
355
|
+
/* Code */
|
|
356
|
+
.message pre { background: var(--surface-alt, #1e1e1e); color: #e2e8f0; padding: 0.75rem; border-radius: 6px; overflow-x: auto; margin: 0.5em 0; }
|
|
357
|
+
.message code { font-family: 'Fira Code', 'Fira Mono', monospace; font-size: 0.85em; }
|
|
358
|
+
.message p code { background: var(--surface-alt, #f1f5f9); padding: 0.15em 0.3em; border-radius: 3px; }
|
|
359
|
+
/* Lists */
|
|
360
|
+
.message ul, .message ol { margin: 0.25em 0; padding-left: 1.5em; }
|
|
361
|
+
.message li { margin: 0.15em 0; }
|
|
362
|
+
/* Headers */
|
|
363
|
+
.message h1, .message h2, .message h3 { margin: 0.5em 0 0.25em; }
|
|
364
|
+
/* Blockquotes */
|
|
365
|
+
.message blockquote { border-left: 3px solid var(--primary, #2563eb); padding-left: 1em; margin: 0.5em 0; opacity: 0.85; }
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**ReactMarkdown link config** β make links open in new tab:
|
|
369
|
+
```tsx
|
|
370
|
+
<ReactMarkdown components={{
|
|
371
|
+
a: ({ href, children }) => <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
|
|
372
|
+
}}>{m.content}</ReactMarkdown>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Agent-Driven Data Models β Reason β Write β Read
|
|
376
|
+
|
|
377
|
+
**This is the core pattern for using an agent as a worker, not just a chatbox.** It is the proven
|
|
378
|
+
way to get a data model that works on the first try (validated end-to-end by the Caveat app).
|
|
379
|
+
|
|
380
|
+
**The mental model:**
|
|
381
|
+
|
|
382
|
+
1. The **app reads** whatever context the agent needs (via `useCollection`) and hands it to the
|
|
383
|
+
agent **inside the prompt**.
|
|
384
|
+
2. The **agent reasons**, then **writes its result as a document** to a MongoDB collection using its
|
|
385
|
+
data tools (`insert_documents` / `update_documents`). The agent is the writer; its job is to
|
|
386
|
+
persist a structured doc, not to "reply."
|
|
387
|
+
3. The **app reads the result back** via `useCollection` (reactively). **Never** parse the agent's
|
|
388
|
+
chat text or `messages[]` for data β read the persisted document.
|
|
389
|
+
|
|
390
|
+
> **MongoDB is schemaless β lean on it.** A collection imposes no fixed columns, so the agent can
|
|
391
|
+
> write **any shape**: nested objects, arrays of objects, ragged/optional fields, mixed types,
|
|
392
|
+
> per-row metadata. You do **not** need a migration, a fixed schema, or matching columns up front.
|
|
393
|
+
> Tell the agent the exact JSON shape you want in the prompt, and it will write exactly that. This
|
|
394
|
+
> is why the pattern "one-shots" a working data model: the document *is* the schema, and it can
|
|
395
|
+
> evolve per write.
|
|
396
|
+
|
|
397
|
+
**1. Read context in the app and pass it to the agent (don't make the agent query for it).**
|
|
398
|
+
Reading a shared pool in the app via `useCollection({ layer: 'shared' })` is reliable; hand the
|
|
399
|
+
data to the agent as JSON in the prompt. Strip PII (email/phone) before handing rows to the agent.
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
// Read the pool in the app β this reads the shared layer reliably.
|
|
403
|
+
const pool = useCollection<Member>('members', { layer: 'shared', limit: 200 });
|
|
404
|
+
const requester = pool.documents.find(m => m.member_key === myKey) ?? null;
|
|
405
|
+
const candidates = pool.documents.filter(m => m.member_key !== myKey);
|
|
406
|
+
|
|
407
|
+
// PII-safe projection before the agent ever sees a row.
|
|
408
|
+
const toCandidate = (m: Member) => ({
|
|
409
|
+
member_key: m.member_key, name: m.name, age: m.age, bio: m.bio, traits: m.traits,
|
|
410
|
+
// NO email, NO phone
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**2. Build a deterministic prompt that names the EXACT document shape to write.** Keep prompt
|
|
415
|
+
construction in a small builder function so it's testable and the shape is explicit. Tell the agent
|
|
416
|
+
which collection to write and the precise JSON β it will follow it verbatim.
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
function matchmakerPrompt(requester, candidates, nowIso: string) {
|
|
420
|
+
return [
|
|
421
|
+
`TASK β find honest matches for member_key "${requester.member_key}".`,
|
|
422
|
+
'Use the data provided below; do NOT query the database β it is all here.',
|
|
423
|
+
'',
|
|
424
|
+
'REQUESTER:', JSON.stringify(requester, null, 2),
|
|
425
|
+
`CANDIDATES (${candidates.length}):`, JSON.stringify(candidates, null, 2),
|
|
426
|
+
'',
|
|
427
|
+
'Steps:',
|
|
428
|
+
'1. Apply the requester\'s dealbreakers as HARD filters.',
|
|
429
|
+
'2. WRITE exactly one document to the "matches" collection with insert_documents.',
|
|
430
|
+
` Use this exact run_at (do NOT call a time tool): "${nowIso}". The document MUST be:`,
|
|
431
|
+
' { "member_key": "...", "run_at": "...", "status": "fresh",',
|
|
432
|
+
' "proposals": [{ "member_key", "confidence", "reasons": [], "opener" }],',
|
|
433
|
+
' "passed": [{ "member_key", "reason" }] }',
|
|
434
|
+
'3. ALWAYS write the doc (even if proposals is empty). Never include email/phone.',
|
|
435
|
+
'Then reply with one short sentence summarizing the result.',
|
|
436
|
+
].join('\n');
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**3. Run the agent as a one-shot task and read the result back via `useCollection`.** This is NOT a
|
|
441
|
+
chat β it's a single `query()` whose side effect is a written document. Use `capabilities:
|
|
442
|
+
['data_access']` to grant the data tools, and a `sessionKey` so this task's conversation stays
|
|
443
|
+
isolated from any chat surface (see [useAgent](#useagent)). With `refreshOnAgentComplete: true`, the
|
|
444
|
+
result collection re-fetches automatically when the agent's write tool completes β no manual
|
|
445
|
+
`refresh()` needed.
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
const agent = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'matchmaker' });
|
|
449
|
+
|
|
450
|
+
// The result collection β reads back what the agent writes.
|
|
451
|
+
const matches = useCollection<MatchDoc>('matches', {
|
|
452
|
+
layer: 'user', // per-user results; no member_key filter needed (partition is scoped)
|
|
453
|
+
sort: { run_at: -1 }, limit: 5,
|
|
454
|
+
refreshOnAgentComplete: true, // auto-refresh when the agent's insert_documents completes
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
async function findMatches() {
|
|
458
|
+
await pool.refresh();
|
|
459
|
+
await agent.query(matchmakerPrompt(toCandidate(requester), candidates.map(toCandidate),
|
|
460
|
+
new Date().toISOString()));
|
|
461
|
+
// matches.documents[0] now holds the agent-written result (auto-refreshed). Render it directly.
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const latest = matches.documents[0];
|
|
465
|
+
const proposals = latest?.proposals ?? [];
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Why write-to-collection instead of reading `toolCalls[].result`?** As of SDK 0.2.1,
|
|
469
|
+
`toolCalls[].result` does carry the tool's structured return (and `toolCalls[].status` is `'error'`
|
|
470
|
+
when a call fails), so it's fine for ephemeral/inline results. But the **durable** pattern is to
|
|
471
|
+
have the agent persist a document and read it via `useCollection`: the result survives refreshes,
|
|
472
|
+
is reactive across components, and is queryable later. Reach for the persisted collection for
|
|
473
|
+
anything you want to keep.
|
|
474
|
+
|
|
475
|
+
### Two-layer data model β `shared` vs per-user
|
|
476
|
+
|
|
477
|
+
`useCollection` (and the agent's data tools) resolve a collection to one of two namespaces via the
|
|
478
|
+
`layer` option:
|
|
479
|
+
|
|
480
|
+
- **`layer: 'user'`** (per-user) β each end-user gets their own **physically isolated** MongoDB
|
|
481
|
+
database (`u_{sha256(user_id)[:16]}`), not just a filtered view. The backend always keys this DB off
|
|
482
|
+
the *requesting* user, so one user literally cannot read or write another user's partition through
|
|
483
|
+
any path. Reads are already scoped β do **not** add a `user_id`/key filter. Use for anything truly
|
|
484
|
+
private: results, drafts, private history (e.g. `matches`).
|
|
485
|
+
- **`layer: 'shared'`** β one builder database (`b_{...}`, one per app) that **every signed-in user of
|
|
486
|
+
the app reads and (when permitted) writes**. Writes are restricted: the app cannot `insert()`
|
|
487
|
+
directly; a collection marked **`agent_writable`** in the data plan lets the app's own agent append
|
|
488
|
+
(e.g. a registrar writing member profiles), while blocking arbitrary end-user writes.
|
|
489
|
+
- **`layer: 'auto'`** (default) β resolves from the collection's configured data model.
|
|
490
|
+
|
|
491
|
+
> **β οΈ Security: `shared` is not access-controlled per user.** Rows written to a shared collection get a
|
|
492
|
+
> `_flowstack.user_id` field, but that is a **filterable field, not an enforced read ACL** β any
|
|
493
|
+
> signed-in user can read *every* row in the collection. Treat a shared collection as a public bulletin
|
|
494
|
+
> board at the storage layer. **Never put data one user must not see from another** (private messages,
|
|
495
|
+
> personal info, secrets) in a `shared` collection, even with a `user_id` filter β filtering is a UI
|
|
496
|
+
> convenience, not isolation. Only the per-user (`'user'`) layer is private.
|
|
497
|
+
|
|
498
|
+
> **Platform gap: no cross-user delivery (today).** Because the per-user DB is always keyed to the
|
|
499
|
+
> requesting user, **you cannot write into *another* user's private partition.** So patterns that
|
|
500
|
+
> require delivering data *to* a specific recipient privately β direct messages, per-user
|
|
501
|
+
> notifications, "send X to user B" β are **not achievable** with the current primitives: the only
|
|
502
|
+
> private store (`'user'`) is unreachable cross-user, and the only cross-user store (`'shared'`) is
|
|
503
|
+
> world-readable. The correct design (deliver each item into the recipient's per-user partition) needs
|
|
504
|
+
> a cross-user write primitive the platform does not yet expose.
|
|
505
|
+
|
|
506
|
+
Reads of a shared collection via `useCollection({ layer: 'shared' })` always hit the shared pool.
|
|
507
|
+
Agent-side reads of shared collections are also routed correctly as of P0-132, but the **recommended
|
|
508
|
+
pattern remains "read in the app, pass into the prompt"** (step 1 above) β it's the most reliable and
|
|
509
|
+
keeps PII out of the agent.
|
|
510
|
+
|
|
511
|
+
### File Upload β MongoDB
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
const { insert } = useCollection('transactions');
|
|
515
|
+
|
|
516
|
+
const handleCSVUpload = async (file: File) => {
|
|
517
|
+
const text = await file.text();
|
|
518
|
+
const rows = Papa.parse(text, { header: true }).data;
|
|
519
|
+
await insert(rows.map(row => ({
|
|
520
|
+
...row,
|
|
521
|
+
amount: Number(row.amount),
|
|
522
|
+
imported_at: new Date().toISOString(),
|
|
523
|
+
})));
|
|
524
|
+
// All useCollection('transactions') instances auto-refresh
|
|
525
|
+
};
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Service Connections (useConnections)
|
|
529
|
+
|
|
530
|
+
Every built app should include a Settings page where users connect external services.
|
|
531
|
+
Without connecting, the AI agent cannot access Google Analytics, Drive, Ads, YouTube, Reddit, Strava, or Twitter data.
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
import { useConnections } from 'flowstack-sdk';
|
|
535
|
+
|
|
536
|
+
const { connections, connect, disconnect, isLoading } = useConnections();
|
|
537
|
+
|
|
538
|
+
// Check status
|
|
539
|
+
connections.google.connected // true | false
|
|
540
|
+
connections.google.analytics // true | false (specific service)
|
|
541
|
+
connections.google.email // "user@gmail.com" if connected
|
|
542
|
+
connections.reddit.connected // true | false
|
|
543
|
+
connections.strava.connected // true | false
|
|
544
|
+
connections.twitter.connected // true | false
|
|
545
|
+
|
|
546
|
+
// Connect β opens OAuth popup
|
|
547
|
+
connect('google', ['analytics', 'drive']); // specific Google services
|
|
548
|
+
connect('google', ['all']); // all Google services
|
|
549
|
+
connect('reddit'); // Reddit
|
|
550
|
+
connect('strava'); // Strava
|
|
551
|
+
connect('twitter'); // Twitter/X
|
|
552
|
+
|
|
553
|
+
// Disconnect
|
|
554
|
+
disconnect('google');
|
|
555
|
+
disconnect('reddit');
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Settings page pattern:**
|
|
559
|
+
```tsx
|
|
560
|
+
function SettingsPage() {
|
|
561
|
+
const { connections, connect, disconnect } = useConnections();
|
|
562
|
+
|
|
563
|
+
const services = [
|
|
564
|
+
{ key: 'google', label: 'Google', sublabel: 'Analytics, Ads, Drive, YouTube', services: ['all'] as const },
|
|
565
|
+
{ key: 'reddit', label: 'Reddit', sublabel: 'Feed access' },
|
|
566
|
+
{ key: 'strava', label: 'Strava', sublabel: 'Activity data' },
|
|
567
|
+
{ key: 'twitter', label: 'Twitter / X', sublabel: 'Timeline and bookmarks' },
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<div>
|
|
572
|
+
<h2>Connected Services</h2>
|
|
573
|
+
<p>Connect your accounts so the AI assistant can access your data.</p>
|
|
574
|
+
{services.map(({ key, label, sublabel, services: svc }) => {
|
|
575
|
+
const status = connections[key as keyof typeof connections];
|
|
576
|
+
return (
|
|
577
|
+
<div key={key} className="connection-card">
|
|
578
|
+
<div>
|
|
579
|
+
<h3>{label}</h3>
|
|
580
|
+
<p className="muted">{sublabel}</p>
|
|
581
|
+
</div>
|
|
582
|
+
{status.connected
|
|
583
|
+
? <button onClick={() => disconnect(key as any)}>Disconnect</button>
|
|
584
|
+
: <button onClick={() => connect(key as any, svc as any)}>Connect</button>}
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
})}
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Include this Settings page in every built app's navigation. The AI chat agent can only access
|
|
594
|
+
services the user has connected.
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Quick Start (Casino Platform)
|
|
599
|
+
|
|
600
|
+
> **Note:** This section is for the Casino data science platform, not generated apps. If you're building a generated site, see [Built Apps](#built-apps--generated-site-pattern) above.
|
|
601
|
+
|
|
602
|
+
### 1. Wrap your app with the provider
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
import { FlowstackProvider } from 'flowstack-sdk';
|
|
606
|
+
|
|
607
|
+
const config = {
|
|
608
|
+
baseUrl: 'https://sage-api.flowstack.fun',
|
|
609
|
+
tenantId: 't_6fe54402be43',
|
|
610
|
+
mode: 'production',
|
|
611
|
+
jwtSecret: 'flowstack-cdn-site',
|
|
612
|
+
passwordSecret: 'flowstack-cdn-site',
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
export default function App({ children }) {
|
|
616
|
+
return (
|
|
617
|
+
<FlowstackProvider config={config}>
|
|
618
|
+
{children}
|
|
619
|
+
</FlowstackProvider>
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Important:** Generated sites always run in production mode. Auth, agent chat, and data access all connect to real infrastructure at `sage-api.flowstack.fun`. Do not use mock mode for deployed sites.
|
|
625
|
+
|
|
626
|
+
### 2. Add authentication
|
|
627
|
+
|
|
628
|
+
**Built apps must use `BrokeredLoginButton` β not `useAuth()`, not `LoginButton` from `flowstack-sdk/wallet`.**
|
|
629
|
+
|
|
630
|
+
`BrokeredLoginButton` opens a Casino popup that handles Privy login on Casino's origin (the only origin registered with Privy). The popup postMessages a scoped JWT back to your app. No Privy app ID, no wagmi/viem peer deps, no `privyConfig` needed.
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
|
|
634
|
+
|
|
635
|
+
export default function App() {
|
|
636
|
+
const { isAuthenticated, isInitialized, credentials, setCredentials } = useFlowstack();
|
|
637
|
+
|
|
638
|
+
if (!isInitialized) return <div>Loadingβ¦</div>;
|
|
639
|
+
|
|
640
|
+
if (!isAuthenticated) {
|
|
641
|
+
return (
|
|
642
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100dvh' }}>
|
|
643
|
+
<BrokeredLoginButton label="Continue with Flowstack" />
|
|
644
|
+
</div>
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
<div>
|
|
650
|
+
<p>Welcome, {credentials?.email}</p>
|
|
651
|
+
<button onClick={() => setCredentials(null)}>Sign out</button>
|
|
652
|
+
{/* your app */}
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Or use `AuthGuard` which shows `BrokeredLoginButton` automatically when not authenticated:
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
import { AuthGuard } from 'flowstack-sdk';
|
|
662
|
+
|
|
663
|
+
export default function App() {
|
|
664
|
+
return (
|
|
665
|
+
<AuthGuard fallback={<LoginScreen />}>
|
|
666
|
+
<Console />
|
|
667
|
+
</AuthGuard>
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// LoginScreen shows BrokeredLoginButton
|
|
672
|
+
function LoginScreen() {
|
|
673
|
+
return (
|
|
674
|
+
<div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}>
|
|
675
|
+
<BrokeredLoginButton />
|
|
676
|
+
</div>
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Guest chat (built apps).** When the app is built with an `appScope` and the site has enabled guest chat
|
|
682
|
+
server-side (`app_config.allowGuestChat`), `AuthGuard` transparently mints a short-lived guest session
|
|
683
|
+
(`POST /auth/guest`) for unauthenticated visitors instead of showing the login gate β removing the
|
|
684
|
+
"sign up first" friction. The opt-in is the per-site backend flag: if the site hasn't enabled it,
|
|
685
|
+
`/auth/guest` returns 403 and `AuthGuard` falls back to the normal login UI. This is a no-op without an
|
|
686
|
+
`appScope` (the Casino dashboard has none, so it always requires real login). It's on by default; pass
|
|
687
|
+
`allowGuest={false}` on a specific guard to always require login.
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
<AuthGuard fallback={<LoginScreen />} allowGuest> {/* default β guest if the site opted in */}
|
|
691
|
+
<Console />
|
|
692
|
+
</AuthGuard>
|
|
693
|
+
|
|
694
|
+
<AuthGuard fallback={<LoginScreen />} allowGuest={false}> {/* always require real login */}
|
|
695
|
+
<Billing />
|
|
696
|
+
</AuthGuard>
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**Account switching.** After `logout()`, the next `BrokeredLoginButton` click passes `force_login=1`, so
|
|
700
|
+
the broker purges its sticky Privy/Casino session and the user can sign in with a *different* account.
|
|
701
|
+
Without this, brokered re-login silently returns the same identity.
|
|
702
|
+
|
|
703
|
+
**Do not use:**
|
|
704
|
+
- `useAuth()` with `login(email, password)` β Casino end-users don't have email/password accounts
|
|
705
|
+
- `LoginButton` from `flowstack-sdk/wallet` β requires WalletProvider/Privy which can't run on built-app subdomains
|
|
706
|
+
- `privyConfig.appId` in FlowstackProvider β not needed for the broker auth flow
|
|
707
|
+
- Extra peer deps (`@privy-io/react-auth`, `wagmi`, `viem`) β only needed for Casino platform features, not built apps
|
|
708
|
+
|
|
709
|
+
### 3. Chat with AI agent
|
|
710
|
+
|
|
711
|
+
```tsx
|
|
712
|
+
import { useAgent } from 'flowstack-sdk';
|
|
713
|
+
|
|
714
|
+
function Chat() {
|
|
715
|
+
const { messages, isStreaming, query } = useAgent();
|
|
716
|
+
|
|
717
|
+
return (
|
|
718
|
+
<div>
|
|
719
|
+
{messages.map(msg => (
|
|
720
|
+
<div key={msg.id} className={msg.role}>
|
|
721
|
+
{msg.content}
|
|
722
|
+
{msg.statusLine && <span className="status">{msg.statusLine}</span>}
|
|
723
|
+
</div>
|
|
724
|
+
))}
|
|
725
|
+
<button onClick={() => query('Analyze my sales data')} disabled={isStreaming}>
|
|
726
|
+
{isStreaming ? 'Thinking...' : 'Ask'}
|
|
727
|
+
</button>
|
|
728
|
+
</div>
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Complete Example App (Casino Platform)
|
|
736
|
+
|
|
737
|
+
> **Note:** This example uses workspace selection which is for the Casino platform only. Generated/built apps should follow the [Built Apps](#built-apps--generated-site-pattern) pattern instead β no workspaces, use `appScope`.
|
|
738
|
+
|
|
739
|
+
A complete, working AI chat application in a single file:
|
|
740
|
+
|
|
741
|
+
```tsx
|
|
742
|
+
'use client';
|
|
743
|
+
|
|
744
|
+
import { useState } from 'react';
|
|
745
|
+
import {
|
|
746
|
+
FlowstackProvider,
|
|
747
|
+
LoginForm,
|
|
748
|
+
useAuth,
|
|
749
|
+
useWorkspace,
|
|
750
|
+
useAgent,
|
|
751
|
+
} from 'flowstack-sdk';
|
|
752
|
+
|
|
753
|
+
const config = {
|
|
754
|
+
baseUrl: 'https://sage-api.flowstack.fun',
|
|
755
|
+
tenantId: 't_6fe54402be43',
|
|
756
|
+
mode: 'production' as const,
|
|
757
|
+
jwtSecret: 'flowstack-cdn-site',
|
|
758
|
+
passwordSecret: 'flowstack-cdn-site',
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
function ChatApp() {
|
|
762
|
+
const { user, isAuthenticated, login, logout, isLoading: authLoading } = useAuth();
|
|
763
|
+
const { workspaces, selectedWorkspace, selectWorkspace } = useWorkspace();
|
|
764
|
+
const { messages, isStreaming, query, clearMessages } = useAgent();
|
|
765
|
+
const [input, setInput] = useState('');
|
|
766
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
767
|
+
|
|
768
|
+
// Auto-scroll to newest message during streaming
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
771
|
+
}, [messages, isStreaming]);
|
|
772
|
+
|
|
773
|
+
const handleSend = async () => {
|
|
774
|
+
if (!input.trim() || isStreaming) return;
|
|
775
|
+
const message = input;
|
|
776
|
+
setInput('');
|
|
777
|
+
await query(message);
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
if (!isAuthenticated) {
|
|
781
|
+
return (
|
|
782
|
+
<div style={{ padding: 40, maxWidth: 400, margin: '0 auto' }}>
|
|
783
|
+
<h1>AI Chat</h1>
|
|
784
|
+
<LoginForm onSuccess={() => {}} showRegisterLink />
|
|
785
|
+
</div>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (!selectedWorkspace && workspaces.length > 0) {
|
|
790
|
+
return (
|
|
791
|
+
<div style={{ padding: 40 }}>
|
|
792
|
+
<h1>Select Workspace</h1>
|
|
793
|
+
{workspaces.map(ws => (
|
|
794
|
+
<button key={ws.workspaceId} onClick={() => selectWorkspace(ws)} style={{ display: 'block', margin: '8px 0' }}>
|
|
795
|
+
{ws.name}
|
|
796
|
+
</button>
|
|
797
|
+
))}
|
|
798
|
+
</div>
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
804
|
+
<header style={{ padding: 16, borderBottom: '1px solid #eee', display: 'flex', justifyContent: 'space-between' }}>
|
|
805
|
+
<strong>{selectedWorkspace?.name || 'AI Chat'}</strong>
|
|
806
|
+
<div>
|
|
807
|
+
<button onClick={clearMessages} style={{ marginRight: 8 }}>Clear</button>
|
|
808
|
+
<button onClick={logout}>Logout</button>
|
|
809
|
+
</div>
|
|
810
|
+
</header>
|
|
811
|
+
|
|
812
|
+
<div style={{ flex: 1, overflow: 'auto', padding: 16, minHeight: 0 }}>
|
|
813
|
+
{messages.map(msg => (
|
|
814
|
+
<div key={msg.id} style={{
|
|
815
|
+
marginBottom: 16, padding: 12, borderRadius: 8,
|
|
816
|
+
backgroundColor: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
|
|
817
|
+
maxWidth: '80%', marginLeft: msg.role === 'user' ? 'auto' : 0,
|
|
818
|
+
}}>
|
|
819
|
+
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
|
820
|
+
{msg.role === 'user' ? 'You' : 'Assistant'}
|
|
821
|
+
</div>
|
|
822
|
+
{msg.role === 'assistant'
|
|
823
|
+
? <ReactMarkdown>{msg.content}</ReactMarkdown>
|
|
824
|
+
: <div style={{ whiteSpace: 'pre-wrap' }}>{msg.content}</div>}
|
|
825
|
+
{msg.statusLine && <div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>{msg.statusLine}</div>}
|
|
826
|
+
{msg.isStreaming && <span style={{ color: '#999' }}>β</span>}
|
|
827
|
+
</div>
|
|
828
|
+
))}
|
|
829
|
+
<div ref={messagesEndRef} />
|
|
830
|
+
</div>
|
|
831
|
+
|
|
832
|
+
<div style={{ padding: 16, borderTop: '1px solid #eee', display: 'flex', gap: 8 }}>
|
|
833
|
+
<input
|
|
834
|
+
value={input}
|
|
835
|
+
onChange={e => setInput(e.target.value)}
|
|
836
|
+
onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()}
|
|
837
|
+
placeholder="Ask a question..."
|
|
838
|
+
disabled={isStreaming}
|
|
839
|
+
style={{ flex: 1, padding: 12, fontSize: 16, borderRadius: 8, border: '1px solid #ddd' }}
|
|
840
|
+
/>
|
|
841
|
+
<button onClick={handleSend} disabled={isStreaming || !input.trim()}>
|
|
842
|
+
{isStreaming ? '...' : 'Send'}
|
|
843
|
+
</button>
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export default function App() {
|
|
850
|
+
return (
|
|
851
|
+
<FlowstackProvider config={config}>
|
|
852
|
+
<ChatApp />
|
|
853
|
+
</FlowstackProvider>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## Hooks Reference
|
|
861
|
+
|
|
862
|
+
### useAuth
|
|
863
|
+
|
|
864
|
+
> **Built apps: do not use this hook.** `useAuth()` is for Casino platform internals. Built apps authenticate via `BrokeredLoginButton` β see [Add authentication](#2-add-authentication) above. Calling `login(email, password)` from a built app will always fail because end-users don't have Casino credentials.
|
|
865
|
+
|
|
866
|
+
For built apps, read auth state from `useFlowstack()` instead:
|
|
867
|
+
|
|
868
|
+
```tsx
|
|
869
|
+
const { isAuthenticated, isInitialized, credentials } = useFlowstack();
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
`useAuth` (Casino platform only):
|
|
873
|
+
|
|
874
|
+
```tsx
|
|
875
|
+
const {
|
|
876
|
+
user, // User | null
|
|
877
|
+
credentials, // FlowstackCredentials | null
|
|
878
|
+
isAuthenticated,// boolean
|
|
879
|
+
isLoading, // boolean
|
|
880
|
+
error, // string | null
|
|
881
|
+
login, // (email, password) => Promise<boolean> β Casino platform only
|
|
882
|
+
register, // (email, password, name?) => Promise<boolean> β Casino platform only
|
|
883
|
+
googleSignIn, // () => Promise<void> β Casino platform only
|
|
884
|
+
logout, // () => void
|
|
885
|
+
refreshToken, // () => Promise<boolean>
|
|
886
|
+
} = useAuth();
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### useCollection
|
|
890
|
+
|
|
891
|
+
Direct MongoDB CRUD for built-app data. This is the primary data hook β use it for all tables, forms, cards, and data persistence. The agent is NOT involved in data operations.
|
|
892
|
+
|
|
893
|
+
Collection names are auto-prefixed with the site_id by the backend β pass SHORT names only (e.g., `'transactions'`, NOT `'site_abc__transactions'`).
|
|
894
|
+
|
|
895
|
+
```tsx
|
|
896
|
+
const {
|
|
897
|
+
// Read (reactive β auto-fetches on mount, re-fetches after mutations)
|
|
898
|
+
documents, // T[]
|
|
899
|
+
count, // number β documents returned (β€ limit)
|
|
900
|
+
total, // number β total matching (for pagination)
|
|
901
|
+
isLoading, // boolean
|
|
902
|
+
error, // string | null
|
|
903
|
+
refresh, // () => Promise<void> β manual re-fetch
|
|
904
|
+
|
|
905
|
+
// Write (auto-refetches all useCollection instances for this collection after success)
|
|
906
|
+
insert, // (doc | doc[]) => Promise<{ inserted_ids: string[] }>
|
|
907
|
+
update, // (filter, update, opts?) => Promise<{ modified_count: number }>
|
|
908
|
+
remove, // (filter) => Promise<{ deleted_count: number }>
|
|
909
|
+
} = useCollection<Transaction>('transactions', {
|
|
910
|
+
filter: { status: 'active' }, // MongoDB query filter
|
|
911
|
+
sort: { date: -1 }, // Sort spec
|
|
912
|
+
limit: 50, // Max documents (default 50, max 500)
|
|
913
|
+
skip: 0, // Pagination offset
|
|
914
|
+
layer: 'auto', // 'user' (per-user) | 'shared' (one pool) | 'auto' (default)
|
|
915
|
+
refreshOnAgentComplete: true, // Auto-refresh after an agent write tool completes (0.2.1+: fires
|
|
916
|
+
// for insert_documents/update_documents/insert_app_data/etc.)
|
|
917
|
+
});
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
> **`layer`** picks the namespace: `'user'` is a private per-user partition (don't filter by a user
|
|
921
|
+
> key β it's already scoped); `'shared'` is one pool all users read (app can't `insert()` directly
|
|
922
|
+
> unless the collection is `agent_writable`); `'auto'` resolves from the collection's data model. See
|
|
923
|
+
> [Two-layer data model](#two-layer-data-model--shared-vs-per-user).
|
|
924
|
+
>
|
|
925
|
+
> **`refreshOnAgentComplete: true`** is the reactive bridge for the ReasonβWriteβRead pattern β when
|
|
926
|
+
> the agent finishes writing via its data tools, this collection re-fetches automatically. (Before
|
|
927
|
+
> 0.2.1 it only matched legacy tool names and silently no-op'd, so apps called `refresh()` manually;
|
|
928
|
+
> that workaround is no longer needed.)
|
|
929
|
+
|
|
930
|
+
**Data display β reactive query:**
|
|
931
|
+
```tsx
|
|
932
|
+
function TransactionTable() {
|
|
933
|
+
const { documents, isLoading } = useCollection<Transaction>('transactions', {
|
|
934
|
+
sort: { date: -1 }, limit: 50, refreshOnAgentComplete: true,
|
|
935
|
+
});
|
|
936
|
+
if (isLoading) return <div>Loading...</div>;
|
|
937
|
+
return (
|
|
938
|
+
<table>
|
|
939
|
+
<tbody>
|
|
940
|
+
{documents.map(row => (
|
|
941
|
+
<tr key={row._id}><td>{row.date}</td><td>${row.amount}</td></tr>
|
|
942
|
+
))}
|
|
943
|
+
</tbody>
|
|
944
|
+
</table>
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
**Form submission β insert document:**
|
|
950
|
+
```tsx
|
|
951
|
+
function AddTransaction() {
|
|
952
|
+
const { insert } = useCollection('transactions');
|
|
953
|
+
const [form, setForm] = useState({ date: '', amount: '' });
|
|
954
|
+
|
|
955
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
956
|
+
e.preventDefault();
|
|
957
|
+
await insert({ ...form, amount: Number(form.amount), created_at: new Date().toISOString() });
|
|
958
|
+
setForm({ date: '', amount: '' });
|
|
959
|
+
// All useCollection('transactions') instances auto-refresh
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
return <form onSubmit={handleSubmit}>...</form>;
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
**Update and delete:**
|
|
967
|
+
```tsx
|
|
968
|
+
const { update, remove } = useCollection('transactions');
|
|
969
|
+
|
|
970
|
+
// Update
|
|
971
|
+
await update({ _id: docId }, { $set: { category: 'Food' } });
|
|
972
|
+
|
|
973
|
+
// Delete
|
|
974
|
+
await remove({ _id: docId });
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**CRITICAL:** The app **reads** all data with `useCollection` (tables, forms, cards, CRUD) β never
|
|
978
|
+
parse the agent's chat text or `messages[]` for data, and never use `useToolInvocation`. **Writes**
|
|
979
|
+
happen one of two ways: (a) deterministic user actions β `useCollection().insert/update/remove`
|
|
980
|
+
directly; (b) agent-reasoned results β the agent writes a document via its `data_access` tools and
|
|
981
|
+
the app reads it back with `useCollection` (the [Reason β Write β Read](#agent-driven-data-models--reason--write--read)
|
|
982
|
+
pattern). Either way, `useCollection` is always how the app *reads*.
|
|
983
|
+
|
|
984
|
+
### usePublicCollection
|
|
985
|
+
|
|
986
|
+
Anonymous public submissions β leaderboards, guestbooks, comment threads, voting. **No auth required.** Any visitor can read and insert. The collection must be declared in `app_config.publicCollections`.
|
|
987
|
+
|
|
988
|
+
```tsx
|
|
989
|
+
import { usePublicCollection } from 'flowstack-sdk';
|
|
990
|
+
|
|
991
|
+
const {
|
|
992
|
+
documents, // T[]
|
|
993
|
+
count, // number
|
|
994
|
+
total, // number
|
|
995
|
+
isLoading, // boolean
|
|
996
|
+
error, // string | null
|
|
997
|
+
insert, // (doc: Partial<T>) => Promise<{ inserted_id: string }>
|
|
998
|
+
refresh, // () => Promise<void>
|
|
999
|
+
} = usePublicCollection<HighScore>('high_scores', {
|
|
1000
|
+
filter: { album: 'yeezus' },
|
|
1001
|
+
sort: { score: -1 },
|
|
1002
|
+
limit: 25,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
await insert({ album: 'yeezus', name: 'KEON', score: 12500 });
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
**Key differences from `useCollection`:**
|
|
1009
|
+
- No credentials needed β works for anonymous visitors
|
|
1010
|
+
- Insert only (no update/remove β data is owned by the app, not the user)
|
|
1011
|
+
- Rate-limited server-side (default 10 writes/min, 100/day per IP)
|
|
1012
|
+
- The collection must be in `app_config.publicCollections` (ask the builder to configure it; the build pipeline sets this up based on `usePublicCollection` usage in the source)
|
|
1013
|
+
- **Requires `tenantId` on `FlowstackProvider`.** Because there's no token for anonymous visitors, the backend can't derive the tenant β so this is the one hook that needs `tenantId` set explicitly. If it's missing, `usePublicCollection` raises a clear error instead of silently using a default. (Authenticated hooks don't need it β they get the tenant from the JWT.)
|
|
1014
|
+
|
|
1015
|
+
**Builder config required in app_config.json:**
|
|
1016
|
+
```json
|
|
1017
|
+
{
|
|
1018
|
+
"publicCollections": {
|
|
1019
|
+
"high_scores": {
|
|
1020
|
+
"schema": {
|
|
1021
|
+
"album": { "type": "string", "required": true, "maxLength": 32 },
|
|
1022
|
+
"name": { "type": "string", "required": true, "maxLength": 24 },
|
|
1023
|
+
"score": { "type": "number", "required": true, "min": 0 }
|
|
1024
|
+
},
|
|
1025
|
+
"rateLimit": { "writesPerMinute": 5, "writesPerDay": 50 }
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### useWorkspace
|
|
1032
|
+
|
|
1033
|
+
Workspace management.
|
|
1034
|
+
|
|
1035
|
+
```tsx
|
|
1036
|
+
const {
|
|
1037
|
+
workspaces, // WorkspaceInfo[]
|
|
1038
|
+
selectedWorkspace, // WorkspaceInfo | null
|
|
1039
|
+
isLoading, // boolean
|
|
1040
|
+
error, // string | null
|
|
1041
|
+
createWorkspace, // (name, description?) => Promise<WorkspaceInfo | null>
|
|
1042
|
+
selectWorkspace, // (workspace) => void
|
|
1043
|
+
refreshWorkspaces, // () => Promise<void>
|
|
1044
|
+
} = useWorkspace();
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### useAgent
|
|
1048
|
+
|
|
1049
|
+
AI agent queries with streaming, tool calls, interrupts, status updates, and **automatic conversation persistence**.
|
|
1050
|
+
|
|
1051
|
+
```tsx
|
|
1052
|
+
const {
|
|
1053
|
+
messages, // ChatMessage[]
|
|
1054
|
+
isStreaming, // boolean
|
|
1055
|
+
isLoading, // boolean
|
|
1056
|
+
toolCalls, // ToolCall[] β active/completed tool executions
|
|
1057
|
+
connectedDataSources, // DataSourceBadgeInfo[] β connected data sources
|
|
1058
|
+
error, // string | null
|
|
1059
|
+
query, // (prompt: string) => Promise<void>
|
|
1060
|
+
clearMessages, // () => void β wipe in-memory messages (same session)
|
|
1061
|
+
cancelQuery, // () => void
|
|
1062
|
+
interruptAgent, // () => Promise<void> β pause agent mid-execution
|
|
1063
|
+
respondToInterrupt, // (response: string) => Promise<void> β resume with user input
|
|
1064
|
+
} = useAgent(template?, options?);
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
**Template (first arg):** `'data-science'` | `'marketing'` | `'support'` | `'custom'`. For a
|
|
1068
|
+
**persona-backed app** (your app's brain is an agent registered via `casino_create_agent`), the
|
|
1069
|
+
template is just a fallback label β the registered persona overrides it. Use `'custom'` (or omit) and
|
|
1070
|
+
select the brain with the `persona` option below. The template does **not** pick the agent.
|
|
1071
|
+
|
|
1072
|
+
**Options:**
|
|
1073
|
+
|
|
1074
|
+
| Option | Type | What it does |
|
|
1075
|
+
|---|---|---|
|
|
1076
|
+
| `capabilities` | `string[]` | Pre-load tool categories the agent may use: `'data_access'` (MongoDB read/write tools β required for the ReasonβWriteβRead pattern), `'external_integration'` (Google/SMS/web/etc.), `'site_operations'`, `'code_execution'`, `'domain_task'`, `'workspace_management'`. Grant only what the task needs. |
|
|
1077
|
+
| `persona` *(0.2.1+)* | `string` | Target a specific registered persona/subagent by name (maps to `target_agents`). Without it the backend auto-selects the **first** registered persona β so multi-persona apps **must** set this to reach the others. (`agentName` is an alias.) |
|
|
1078
|
+
| `systemPrompt` *(0.2.1+)* | `string` | Inline system-prompt override for this hook (maps to `system_prompt_override`). Use to tune behavior per-surface without registering a separate persona. |
|
|
1079
|
+
| `sessionKey` *(0.2.1+)* | `string` | Namespaces this hook's conversation. **Pass a stable, distinct value per surface** (e.g. `'interview'`, `'matchmaker'`) so independent `useAgent` instances don't share one conversation. It also persists that surface's history across reloads (same tab). Omit it only for a single-surface app. |
|
|
1080
|
+
| `tools` | `string[]` | Whitelist specific tool names (finer than `capabilities`). |
|
|
1081
|
+
| `targetAgent(s)` | `string` / `string[]` | **Deprecated** (Strands swarm, removed in P0-73). Use `persona` instead. |
|
|
1082
|
+
|
|
1083
|
+
```tsx
|
|
1084
|
+
// Persona-backed, multi-surface app: each surface gets its own brain + isolated conversation.
|
|
1085
|
+
const interview = useAgent('custom', { capabilities: ['data_access'], persona: 'interviewer', sessionKey: 'interview' });
|
|
1086
|
+
const matchmaker = useAgent('custom', { capabilities: ['data_access'], persona: 'matchmaker', sessionKey: 'matchmaker' });
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
#### Conversation Memory (Automatic)
|
|
1090
|
+
|
|
1091
|
+
`useAgent` automatically persists a stable **session ID** for each component mount. The session ID is:
|
|
1092
|
+
|
|
1093
|
+
- **Generated on first use** via `crypto.randomUUID()` (with a timestamp fallback for legacy browsers)
|
|
1094
|
+
- **Stable across turns** β every call to `query()` passes the same session ID to the backend
|
|
1095
|
+
- **Reused by Strands' `S3SessionManager`** β the agent remembers prior messages, tool calls, and context across turns
|
|
1096
|
+
- **Reset on `clearMessages()`** β starting a truly new conversation generates a fresh session ID
|
|
1097
|
+
|
|
1098
|
+
This means a chat in a built app correctly handles multi-turn queries:
|
|
1099
|
+
|
|
1100
|
+
```tsx
|
|
1101
|
+
function FinanceChat() {
|
|
1102
|
+
const { query, messages, isStreaming } = useAgent(undefined, {
|
|
1103
|
+
targetAgents: ['finance_bot'] // agent registered via casino_create_agent
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// Turn 1: "show me last month's expenses"
|
|
1107
|
+
// Turn 2: "now break it down by category" β agent remembers "last month's expenses"
|
|
1108
|
+
// Turn 3: "which one is the biggest?" β agent knows the categories from turn 2
|
|
1109
|
+
|
|
1110
|
+
return (
|
|
1111
|
+
<ChatUI messages={messages} onSend={query} isStreaming={isStreaming} />
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
**Notes:**
|
|
1117
|
+
- **Session ID is tied to the component mount.** Unmounting and remounting `useAgent` (e.g., closing and reopening a modal) starts a new conversation. Persisting sessions across unmounts/page refreshes requires application-level storage of the session ID (see Advanced).
|
|
1118
|
+
- **`clearMessages()` resets the session.** Call it to start a fresh conversation. The next `query()` creates a new backend session.
|
|
1119
|
+
- **Wallet-authenticated users** have their conversations scoped to their wallet address server-side. Same wallet across tabs/browsers can reach the same conversation if the session ID is passed explicitly.
|
|
1120
|
+
|
|
1121
|
+
#### Advanced: Explicit Session Management
|
|
1122
|
+
|
|
1123
|
+
**Simplest path (0.2.1+): pass `sessionKey`.** A stable `sessionKey` per surface gives each
|
|
1124
|
+
`useAgent` its own conversation *and* persists that conversation across reloads (same tab) β no
|
|
1125
|
+
localStorage plumbing or remount needed:
|
|
1126
|
+
|
|
1127
|
+
```tsx
|
|
1128
|
+
const chat = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'support-chat' });
|
|
1129
|
+
// Reopening/reloading this surface resumes the same conversation; a different sessionKey is a
|
|
1130
|
+
// different conversation. Call chat.clearMessages() / startNewSession() to start fresh.
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
For cross-tab or multi-conversation-per-surface persistence, manage the session ID at the
|
|
1134
|
+
application level:
|
|
1135
|
+
|
|
1136
|
+
```tsx
|
|
1137
|
+
import { useEffect, useState } from 'react';
|
|
1138
|
+
import { useAgent } from 'flowstack-sdk';
|
|
1139
|
+
|
|
1140
|
+
function PersistentChat() {
|
|
1141
|
+
// Option 1: Resume the most recent session from localStorage
|
|
1142
|
+
const [sessionId] = useState(() => {
|
|
1143
|
+
const existing = localStorage.getItem('my-app:chat:session');
|
|
1144
|
+
if (existing) return existing;
|
|
1145
|
+
const fresh = crypto.randomUUID();
|
|
1146
|
+
localStorage.setItem('my-app:chat:session', fresh);
|
|
1147
|
+
return fresh;
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// The useAgent hook will create its own sessionIdRef automatically,
|
|
1151
|
+
// but you can also invoke the lower-level `executeQueryWithConfig` with
|
|
1152
|
+
// an explicit sessionId for full control (see API reference).
|
|
1153
|
+
const { query, messages, clearMessages } = useAgent(undefined, {
|
|
1154
|
+
targetAgents: ['finance_bot'] // agent registered via casino_create_agent
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
const handleNewChat = () => {
|
|
1158
|
+
const fresh = crypto.randomUUID();
|
|
1159
|
+
localStorage.setItem('my-app:chat:session', fresh);
|
|
1160
|
+
clearMessages(); // resets the in-hook sessionIdRef as well
|
|
1161
|
+
window.location.reload(); // optional: force remount to pick up new session
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
return (
|
|
1165
|
+
<>
|
|
1166
|
+
<button onClick={handleNewChat}>+ New Chat</button>
|
|
1167
|
+
<ChatUI messages={messages} onSend={query} />
|
|
1168
|
+
</>
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
**Future SDK versions** will expose `useConversations()` and `useConversation(id)` hooks for first-class conversation listing and restoration. Until then, the pattern above is the recommended approach for multi-conversation apps.
|
|
1174
|
+
|
|
1175
|
+
#### When Conversation Memory Breaks
|
|
1176
|
+
|
|
1177
|
+
If your built app appears to "forget" every turn:
|
|
1178
|
+
|
|
1179
|
+
1. **Check your SDK version.** Apps built with SDK versions before the `sessionIdRef` fix do not pass a session ID at all β every turn lands in a new backend session. Rebuild the app via `edit_site` or `build_site` to bundle the latest SDK.
|
|
1180
|
+
2. **Check that you're not remounting the component** between every query (e.g., conditional rendering based on `isStreaming` can cause remounts).
|
|
1181
|
+
3. **Check for multiple `useAgent` instances** in the same page. Before 0.2.1 they all *shared* one
|
|
1182
|
+
tenant-wide session, so surfaces bled into each other (a matchmaker reply showing up in an
|
|
1183
|
+
interview). Fix: give each instance a distinct `sessionKey` (0.2.1+) so conversations stay
|
|
1184
|
+
isolated.
|
|
1185
|
+
4. **Check the browser console** for SDK warnings about `crypto.randomUUID` not being available (very old browsers).
|
|
1186
|
+
|
|
1187
|
+
**ChatMessage fields:**
|
|
1188
|
+
|
|
1189
|
+
| Field | Type | Description |
|
|
1190
|
+
|-------|------|-------------|
|
|
1191
|
+
| `id` | `string` | Unique message ID |
|
|
1192
|
+
| `role` | `'user' \| 'assistant'` | Message sender |
|
|
1193
|
+
| `content` | `string` | Message text |
|
|
1194
|
+
| `isStreaming` | `boolean` | Whether still receiving content |
|
|
1195
|
+
| `statusLine` | `string \| undefined` | Live status text during streaming (tool progress, agent status) |
|
|
1196
|
+
| `toolCalls` | `ToolCall[]` | Tool executions in this message |
|
|
1197
|
+
| `visualizations` | `VisualizationData[]` | Charts/images produced |
|
|
1198
|
+
| `searchResults` | `SearchResultsData[]` | Search results |
|
|
1199
|
+
|
|
1200
|
+
### useDatasets
|
|
1201
|
+
|
|
1202
|
+
Dataset operations.
|
|
1203
|
+
|
|
1204
|
+
```tsx
|
|
1205
|
+
const {
|
|
1206
|
+
datasets, // DatasetInfo[] β { id, name, rows, columns, schema?, columnNames?, createdAt?, updatedAt? }
|
|
1207
|
+
isLoading, // boolean
|
|
1208
|
+
error, // string | null
|
|
1209
|
+
uploadDataset, // (file, name?) => Promise<DatasetInfo | null>
|
|
1210
|
+
downloadDataset, // (name) => Promise<Blob | null>
|
|
1211
|
+
deleteDataset, // (name) => Promise<boolean>
|
|
1212
|
+
refreshDatasets, // () => Promise<void>
|
|
1213
|
+
} = useDatasets();
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
> **DatasetInfo fields (source of truth: `src/types/index.ts`):**
|
|
1217
|
+
> - `id: string` β stable identifier
|
|
1218
|
+
> - `name: string` β user-visible name
|
|
1219
|
+
> - `rows: number` β row count (NOT `rowCount`)
|
|
1220
|
+
> - `columns: number` β column count (NOT `columnCount`)
|
|
1221
|
+
> - `schema?: Record<string, ColumnSchema>` β optional per-column schema
|
|
1222
|
+
> - `columnNames?: string[]` β optional ordered column names
|
|
1223
|
+
> - `createdAt?: string`, `updatedAt?: string` β ISO8601 timestamps
|
|
1224
|
+
|
|
1225
|
+
### useVisualizations
|
|
1226
|
+
|
|
1227
|
+
Visualization management.
|
|
1228
|
+
|
|
1229
|
+
```tsx
|
|
1230
|
+
const {
|
|
1231
|
+
visualizations, // VisualizationData[] β { name, type, imageUrl, imageBase64, format, createdAt }
|
|
1232
|
+
isLoading, // boolean
|
|
1233
|
+
error, // string | null
|
|
1234
|
+
refreshVisualizations,// () => Promise<void>
|
|
1235
|
+
} = useVisualizations();
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### useReports
|
|
1239
|
+
|
|
1240
|
+
Report management.
|
|
1241
|
+
|
|
1242
|
+
```tsx
|
|
1243
|
+
const {
|
|
1244
|
+
reports, // ReportInfo[] β { name, content, format, createdAt }
|
|
1245
|
+
isLoading, // boolean
|
|
1246
|
+
error, // string | null
|
|
1247
|
+
refreshReports, // () => Promise<void>
|
|
1248
|
+
} = useReports();
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
### useModels
|
|
1252
|
+
|
|
1253
|
+
ML model management.
|
|
1254
|
+
|
|
1255
|
+
```tsx
|
|
1256
|
+
const {
|
|
1257
|
+
models, // ModelInfo[] β { name, framework, metrics, createdAt }
|
|
1258
|
+
isLoading, // boolean
|
|
1259
|
+
error, // string | null
|
|
1260
|
+
refreshModels, // () => Promise<void>
|
|
1261
|
+
} = useModels();
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### useSites
|
|
1265
|
+
|
|
1266
|
+
Published site management β list, create, stage files, publish to CDN, delete.
|
|
1267
|
+
|
|
1268
|
+
```tsx
|
|
1269
|
+
const {
|
|
1270
|
+
sites, // PublishedSiteInfo[]
|
|
1271
|
+
isLoading, // boolean
|
|
1272
|
+
error, // string | null
|
|
1273
|
+
createSite, // (params: CreateSiteParams) => Promise<PublishedSiteInfo | null>
|
|
1274
|
+
addFile, // (siteId, path, content) => Promise<boolean>
|
|
1275
|
+
publishSite, // (siteId) => Promise<PublishedSiteInfo | null>
|
|
1276
|
+
deleteSite, // (siteId) => Promise<boolean>
|
|
1277
|
+
refreshSites, // () => Promise<void>
|
|
1278
|
+
} = useSites();
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
**PublishedSiteInfo fields:**
|
|
1282
|
+
|
|
1283
|
+
| Field | Type | Description |
|
|
1284
|
+
|-------|------|-------------|
|
|
1285
|
+
| `id` | `string` | Site ID |
|
|
1286
|
+
| `name` | `string` | Display name |
|
|
1287
|
+
| `url` | `string` | Published CDN URL |
|
|
1288
|
+
| `shortUrl` | `string?` | Short URL for sharing |
|
|
1289
|
+
| `siteType` | `'on_demand' \| 'daily' \| 'js_build'` | How the site was created |
|
|
1290
|
+
| `fileCount` | `number` | Number of files |
|
|
1291
|
+
| `totalBytes` | `number?` | Total size |
|
|
1292
|
+
| `createdAt` | `string` | ISO timestamp |
|
|
1293
|
+
| `expiresAt` | `string?` | Expiration (if applicable) |
|
|
1294
|
+
|
|
1295
|
+
**Two publishing workflows:**
|
|
1296
|
+
|
|
1297
|
+
```tsx
|
|
1298
|
+
// Quick publish β pass files inline
|
|
1299
|
+
await createSite({
|
|
1300
|
+
name: 'My Dashboard',
|
|
1301
|
+
siteType: 'on_demand',
|
|
1302
|
+
files: { 'index.html': '<html>...</html>', 'styles.css': 'body { ... }' },
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Staged publish β add files incrementally, then publish
|
|
1306
|
+
const site = await createSite({ name: 'My App' });
|
|
1307
|
+
await addFile(site.id, 'index.html', '<html>...</html>');
|
|
1308
|
+
await addFile(site.id, 'src/App.tsx', 'export default function App() { ... }');
|
|
1309
|
+
await publishSite(site.id);
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
### useDataSources
|
|
1313
|
+
|
|
1314
|
+
External data source connections.
|
|
1315
|
+
|
|
1316
|
+
```tsx
|
|
1317
|
+
const {
|
|
1318
|
+
dataSources, // DataSource[]
|
|
1319
|
+
isLoading, // boolean
|
|
1320
|
+
error, // string | null
|
|
1321
|
+
createDataSource, // (config) => Promise<DataSource | null>
|
|
1322
|
+
testConnection, // (sourceId) => Promise<boolean>
|
|
1323
|
+
deleteDataSource, // (sourceId) => Promise<boolean>
|
|
1324
|
+
refreshDataSources,// () => Promise<void>
|
|
1325
|
+
} = useDataSources();
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
Supports: PostgreSQL, MongoDB, MySQL, Snowflake, BigQuery, S3.
|
|
1329
|
+
|
|
1330
|
+
### useQuery
|
|
1331
|
+
|
|
1332
|
+
Simple one-shot query execution (no streaming).
|
|
1333
|
+
|
|
1334
|
+
```tsx
|
|
1335
|
+
const {
|
|
1336
|
+
executeQuery, // (prompt: string) => Promise<QueryResult>
|
|
1337
|
+
result, // QueryResult | null
|
|
1338
|
+
isLoading, // boolean
|
|
1339
|
+
error, // string | null
|
|
1340
|
+
} = useQuery();
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
### useUserManagement
|
|
1344
|
+
|
|
1345
|
+
Admin user management.
|
|
1346
|
+
|
|
1347
|
+
```tsx
|
|
1348
|
+
const {
|
|
1349
|
+
users, // ManagedUser[]
|
|
1350
|
+
stats, // UserStats | null β { totalUsers, activeUsers, newUsersThisMonth }
|
|
1351
|
+
isLoading, // boolean
|
|
1352
|
+
error, // string | null
|
|
1353
|
+
canManageUsers, // boolean
|
|
1354
|
+
pagination, // { page, limit, totalCount, hasMore }
|
|
1355
|
+
refreshUsers, // (params?) => Promise<void>
|
|
1356
|
+
getUser, // (userId) => Promise<ManagedUser | null>
|
|
1357
|
+
updateUser, // (userId, updates) => Promise<boolean>
|
|
1358
|
+
suspendUser, // (userId, reason?) => Promise<boolean>
|
|
1359
|
+
reactivateUser, // (userId) => Promise<boolean>
|
|
1360
|
+
deleteUser, // (userId) => Promise<boolean>
|
|
1361
|
+
getUserActivity, // (userId, limit?) => Promise<UserActivityLog[]>
|
|
1362
|
+
refreshStats, // () => Promise<void>
|
|
1363
|
+
setPage, // (page) => void
|
|
1364
|
+
setSearch, // (search) => void
|
|
1365
|
+
setRoleFilter, // (role) => void
|
|
1366
|
+
setStatusFilter, // (status) => void
|
|
1367
|
+
} = useUserManagement();
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
**Role hierarchy:** `owner` > `admin` > `member` > `viewer`
|
|
1371
|
+
|
|
1372
|
+
### useAuthGuard
|
|
1373
|
+
|
|
1374
|
+
Programmatic route protection.
|
|
1375
|
+
|
|
1376
|
+
```tsx
|
|
1377
|
+
const {
|
|
1378
|
+
isAllowed, // boolean
|
|
1379
|
+
isLoading, // boolean
|
|
1380
|
+
shouldRedirect, // boolean
|
|
1381
|
+
redirectTo, // string | undefined
|
|
1382
|
+
} = useAuthGuard({
|
|
1383
|
+
requireAuth: true,
|
|
1384
|
+
requireWorkspace: true,
|
|
1385
|
+
redirectTo: '/login',
|
|
1386
|
+
});
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
### useFlowstackStatus
|
|
1390
|
+
|
|
1391
|
+
Backend connection monitoring.
|
|
1392
|
+
|
|
1393
|
+
```tsx
|
|
1394
|
+
const {
|
|
1395
|
+
status, // 'connected' | 'disconnected' | 'checking'
|
|
1396
|
+
isConnected, // boolean
|
|
1397
|
+
latency, // number | null (ms)
|
|
1398
|
+
error, // string | null
|
|
1399
|
+
checkConnection, // () => Promise<void>
|
|
1400
|
+
} = useFlowstackStatus({
|
|
1401
|
+
pollInterval: 30000,
|
|
1402
|
+
autoPoll: true,
|
|
1403
|
+
});
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
### Additional Hooks (0.5+)
|
|
1407
|
+
|
|
1408
|
+
The SDK ships many hooks beyond the classic fifteen above. These are fully exported from `flowstack-sdk` and share the same `FlowstackProvider` context. Treat the type signatures in `packages/flowstack-sdk/src/hooks/<name>.ts` as the source of truth β the list below is a one-liner reference and will be auto-generated from source in a future release.
|
|
1409
|
+
|
|
1410
|
+
<!-- AUTO-GENERATED:START:hooks-extended -->
|
|
1411
|
+
- **`useAgent()`** β useAgent Hook The main hook for interacting with AI agents.
|
|
1412
|
+
- **`useAgents()`** β useAgents Hook Fetches the catalog of available agents from the backend.
|
|
1413
|
+
- **`useAuth()`** β useAuth Hook Provides authentication functionality including login, register, and Google OAuth.
|
|
1414
|
+
- **`useAuthGuard()`** β useAuthGuard Hook Provides auth guard logic without rendering components.
|
|
1415
|
+
- **`useAutomations()`** β CRUD for agent cron automations (P0-85).
|
|
1416
|
+
- **`useCollection()`** β Direct MongoDB access for built-app components.
|
|
1417
|
+
- **`useCollectionExplorer()`** β Browse, query, export, and delete a specific MongoDB collection.
|
|
1418
|
+
- **`useConnections()`** β Manage external service connections (Google, Reddit, Strava, Twitter, GitHub).
|
|
1419
|
+
- **`useConversations()`** β fetches the user's past Casino builder conversations from GET /library/conversations.
|
|
1420
|
+
- **`useDataOverview()`** β Unified summary of all user-owned data.
|
|
1421
|
+
- **`useDataSources()`** β useDataSources Hook Provides data source management for connecting to external databases.
|
|
1422
|
+
- **`useDatasets()`** β useDatasets Hook Provides dataset management functionality including upload, download, and deletion.
|
|
1423
|
+
- **`useFlowstackStatus()`** β useFlowstackStatus Hook Monitors connection status and health of the Flowstack backend.
|
|
1424
|
+
- **`useIntegrations()`** β CRUD for HTTP API integrations (P0-79).
|
|
1425
|
+
- **`useIntentAgent()`** β useIntentAgent Hook Creates and manages agents dynamically based on user intent.
|
|
1426
|
+
- **`useModels()`** β useModels Hook Provides ML model management functionality.
|
|
1427
|
+
- **`useOllamaDetection()`** β Detect a local Ollama instance and list available models.
|
|
1428
|
+
- **`useProviderCredentials()`** β Manage LLM provider credentials (BYOK + Ollama).
|
|
1429
|
+
- **`usePublicCollection()`** β anonymous public submissions for built apps.
|
|
1430
|
+
- **`useQuery()`** β useQuery Hook A lower-level hook for executing queries without managing chat history.
|
|
1431
|
+
- **`useReports()`** β useReports Hook Provides report management functionality.
|
|
1432
|
+
- **`useSiteVersions()`** β See source for usage.
|
|
1433
|
+
- **`useSites()`** β useSites Hook Provides published site management β list, create, stage files, publish to CDN, delete.
|
|
1434
|
+
- **`useToolInvocation()`** β Direct tool invocation hook.
|
|
1435
|
+
- **`useUserCollections()`** β List all MongoDB collections the user owns.
|
|
1436
|
+
- **`useUserManagement()`** β useUserManagement Hook Provides user management functionality for developers to track and manage users who sign up through their apps.
|
|
1437
|
+
- **`useVisualizations()`** β useVisualizations Hook Provides access to workspace visualizations.
|
|
1438
|
+
- **`useWorkspace()`** β useWorkspace Hook Provides workspace management functionality.
|
|
1439
|
+
<!-- AUTO-GENERATED:END:hooks-extended -->
|
|
1440
|
+
|
|
1441
|
+
---
|
|
1442
|
+
|
|
1443
|
+
### useIntegrations
|
|
1444
|
+
|
|
1445
|
+
Register any HTTPS REST API as a named integration that the agent can call as a tool. Credentials are encrypted at rest; raw secrets are never returned after creation.
|
|
1446
|
+
|
|
1447
|
+
```tsx
|
|
1448
|
+
import { useIntegrations } from 'flowstack-sdk';
|
|
1449
|
+
|
|
1450
|
+
const { integrations, create, update, remove, isLoading } = useIntegrations();
|
|
1451
|
+
|
|
1452
|
+
// Register Shopify
|
|
1453
|
+
await create({
|
|
1454
|
+
name: 'Shopify',
|
|
1455
|
+
description: 'Shopify Admin API for order and product management',
|
|
1456
|
+
base_url: 'https://my-store.myshopify.com/admin/api/2024-01',
|
|
1457
|
+
auth_type: 'bearer', // 'bearer' | 'api_key_header' | 'api_key_query' | 'basic' | 'none'
|
|
1458
|
+
auth_config: { token: 'shpat_xxx' },
|
|
1459
|
+
endpoints: [
|
|
1460
|
+
{ name: 'list_orders', method: 'GET', path: '/orders.json' },
|
|
1461
|
+
{ name: 'get_order', method: 'GET', path: '/orders/{id}.json' },
|
|
1462
|
+
{ name: 'cancel_order',method: 'POST', path: '/orders/{id}/cancel.json' },
|
|
1463
|
+
],
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// Update credentials
|
|
1467
|
+
await update(id, { auth_config: { token: 'new_token' } });
|
|
1468
|
+
|
|
1469
|
+
// Remove
|
|
1470
|
+
await remove(id);
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
Return value:
|
|
1474
|
+
|
|
1475
|
+
```tsx
|
|
1476
|
+
{
|
|
1477
|
+
integrations: Integration[]; // [{integration_id, name, base_url, auth_type, endpoint_count, ...}]
|
|
1478
|
+
isLoading: boolean;
|
|
1479
|
+
error: string | null;
|
|
1480
|
+
create: (input) => Promise<Integration | null>;
|
|
1481
|
+
update: (id, input) => Promise<boolean>;
|
|
1482
|
+
remove: (id) => Promise<boolean>;
|
|
1483
|
+
get: (id) => Promise<Integration | null>; // includes full endpoint list
|
|
1484
|
+
refresh: () => Promise<void>;
|
|
1485
|
+
}
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
---
|
|
1489
|
+
|
|
1490
|
+
### useAutomations
|
|
1491
|
+
|
|
1492
|
+
Schedule agent prompts on a cron schedule via AWS EventBridge. Results can be delivered silently (stored only), by email, webhook, or as a downloadable file.
|
|
1493
|
+
|
|
1494
|
+
Schedule format: **5-field Unix cron** `"minute hour dom month dow"`
|
|
1495
|
+
|
|
1496
|
+
```tsx
|
|
1497
|
+
import { useAutomations } from 'flowstack-sdk';
|
|
1498
|
+
|
|
1499
|
+
const { automations, create, pause, resume, runNow, getRuns } = useAutomations();
|
|
1500
|
+
|
|
1501
|
+
// Daily sales digest β weekdays at 9 AM Eastern
|
|
1502
|
+
await create({
|
|
1503
|
+
name: 'Daily sales digest',
|
|
1504
|
+
prompt: "Pull yesterday's orders from Shopify, summarize revenue by product and region, flag any anomalies.",
|
|
1505
|
+
schedule: '0 9 * * 1-5',
|
|
1506
|
+
timezone: 'America/New_York',
|
|
1507
|
+
target_agents: ['shopify_analyst'], // routes to a specific agent persona; omit for default
|
|
1508
|
+
output_config: {
|
|
1509
|
+
type: 'email',
|
|
1510
|
+
to: 'team@company.com',
|
|
1511
|
+
subject_template: 'Sales digest β {date}',
|
|
1512
|
+
},
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// Run once right now (ignores schedule)
|
|
1516
|
+
await runNow(automationId);
|
|
1517
|
+
|
|
1518
|
+
// Pause / resume
|
|
1519
|
+
await pause(automationId);
|
|
1520
|
+
await resume(automationId);
|
|
1521
|
+
|
|
1522
|
+
// Get last 10 run results
|
|
1523
|
+
const runs = await getRuns(automationId, 10);
|
|
1524
|
+
// [{run_id, status, started_at, duration_ms, credits_used, output_summary, output_url}]
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
Output config options:
|
|
1528
|
+
|
|
1529
|
+
| `type` | Delivers via | Extra fields |
|
|
1530
|
+
|---|---|---|
|
|
1531
|
+
| `"silent"` | Stored only (default) | β |
|
|
1532
|
+
| `"email"` | Email | `to`, `subject_template`, `format` |
|
|
1533
|
+
| `"webhook"` | HTTP POST | `url`, `headers`, `format` |
|
|
1534
|
+
| `"file"` | Downloadable URL in `output_url` | `format` (`"csv"` \| `"json"` \| `"pdf"`) |
|
|
1535
|
+
|
|
1536
|
+
Return value:
|
|
1537
|
+
|
|
1538
|
+
```tsx
|
|
1539
|
+
{
|
|
1540
|
+
automations: Automation[]; // [{automation_id, name, schedule, status, last_run_at, run_count, ...}]
|
|
1541
|
+
isLoading: boolean;
|
|
1542
|
+
error: string | null;
|
|
1543
|
+
create: (input) => Promise<Automation | null>;
|
|
1544
|
+
update: (id, input) => Promise<boolean>;
|
|
1545
|
+
remove: (id) => Promise<boolean>;
|
|
1546
|
+
pause: (id) => Promise<boolean>;
|
|
1547
|
+
resume: (id) => Promise<boolean>;
|
|
1548
|
+
runNow: (id) => Promise<{ invoked: boolean } | null>;
|
|
1549
|
+
getRuns: (id, limit?) => Promise<AutomationRun[]>;
|
|
1550
|
+
refresh: () => Promise<void>;
|
|
1551
|
+
}
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
---
|
|
1555
|
+
|
|
1556
|
+
## Components
|
|
1557
|
+
|
|
1558
|
+
### Pre-built Page Components
|
|
1559
|
+
|
|
1560
|
+
```tsx
|
|
1561
|
+
import { AuthPage, DashboardLayout, ChatPage } from 'flowstack-sdk';
|
|
1562
|
+
|
|
1563
|
+
// Complete auth page with login/register tabs
|
|
1564
|
+
<AuthPage defaultTab="login" onSuccess={() => router.push('/dashboard')} showGoogle />
|
|
1565
|
+
|
|
1566
|
+
// Dashboard layout with sidebar
|
|
1567
|
+
<DashboardLayout sidebar={<Sidebar />} header={<Header />}>
|
|
1568
|
+
<Content />
|
|
1569
|
+
</DashboardLayout>
|
|
1570
|
+
|
|
1571
|
+
// Full chat interface
|
|
1572
|
+
<ChatPage title="AI Assistant" placeholder="Ask anything..." welcomeMessage={<Welcome />} />
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
### Form Components
|
|
1576
|
+
|
|
1577
|
+
```tsx
|
|
1578
|
+
import { LoginForm, RegisterForm, AuthGuard } from 'flowstack-sdk';
|
|
1579
|
+
|
|
1580
|
+
<LoginForm onSuccess={handleSuccess} onError={handleError} showRegisterLink />
|
|
1581
|
+
<RegisterForm onSuccess={handleSuccess} showLoginLink />
|
|
1582
|
+
|
|
1583
|
+
// Route protection
|
|
1584
|
+
<AuthGuard fallback={<LoginPage />} requireWorkspace>
|
|
1585
|
+
<ProtectedContent />
|
|
1586
|
+
</AuthGuard>
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
### Workspace & Data Components
|
|
1590
|
+
|
|
1591
|
+
```tsx
|
|
1592
|
+
import { WorkspaceSelector, CreateWorkspaceModal, DatasetUploader, ChatInterface, MessageList } from 'flowstack-sdk';
|
|
1593
|
+
|
|
1594
|
+
<WorkspaceSelector onSelect={handleSelect} />
|
|
1595
|
+
<CreateWorkspaceModal isOpen={open} onCreated={handleCreated} />
|
|
1596
|
+
<DatasetUploader onUpload={handleUpload} accept=".csv,.xlsx,.parquet" />
|
|
1597
|
+
<ChatInterface template="data-science" height={500} placeholder="Ask about your data..." />
|
|
1598
|
+
<MessageList messages={messages} isStreaming={isStreaming} />
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
### MarkdownRenderer
|
|
1602
|
+
|
|
1603
|
+
Drop-in component for rendering agent responses with full GFM support (tables, code blocks, inline formatting). Handles mid-stream table repair automatically β no Tailwind or external CSS required.
|
|
1604
|
+
|
|
1605
|
+
```tsx
|
|
1606
|
+
import { useAgent, MarkdownRenderer } from 'flowstack-sdk';
|
|
1607
|
+
|
|
1608
|
+
function Chat() {
|
|
1609
|
+
const { messages, isStreaming } = useAgent('data-science');
|
|
1610
|
+
return (
|
|
1611
|
+
<div>
|
|
1612
|
+
{messages.map(msg =>
|
|
1613
|
+
msg.role === 'assistant' ? (
|
|
1614
|
+
<MarkdownRenderer
|
|
1615
|
+
key={msg.id}
|
|
1616
|
+
content={msg.content}
|
|
1617
|
+
isStreaming={isStreaming && msg === messages.at(-1)}
|
|
1618
|
+
/>
|
|
1619
|
+
) : (
|
|
1620
|
+
<p key={msg.id}>{msg.content}</p>
|
|
1621
|
+
)
|
|
1622
|
+
)}
|
|
1623
|
+
</div>
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
| Prop | Type | Description |
|
|
1629
|
+
|------|------|-------------|
|
|
1630
|
+
| `content` | `string` | Raw markdown from `message.content` |
|
|
1631
|
+
| `isStreaming` | `boolean?` | Pass `true` while streaming to apply mid-stream table repair |
|
|
1632
|
+
| `className` | `string?` | Custom class for the wrapper `<div>` |
|
|
1633
|
+
|
|
1634
|
+
---
|
|
1635
|
+
|
|
1636
|
+
## Wallet Module
|
|
1637
|
+
|
|
1638
|
+
> **Built apps: do not use this module for authentication.** The wallet module requires peer dependencies (`@privy-io/react-auth`, `wagmi`, `viem`, `@tanstack/react-query`) and needs Privy running on the page's origin β which is only possible on `casino.flowstack.fun`, not on `*.casino.flowstack.fun` built-app subdomains. **Use `BrokeredLoginButton` for auth in built apps.**
|
|
1639
|
+
>
|
|
1640
|
+
> Use this module only when your app needs to read/display INFER token balances, accept token payments, or access wallet addresses directly.
|
|
1641
|
+
|
|
1642
|
+
Separate entry point at `flowstack-sdk/wallet` β import only when you need INFER token payments. Peer dependencies required:
|
|
1643
|
+
|
|
1644
|
+
```json
|
|
1645
|
+
{
|
|
1646
|
+
"dependencies": {
|
|
1647
|
+
"@privy-io/react-auth": ">=2.0.0",
|
|
1648
|
+
"wagmi": ">=2.0.0",
|
|
1649
|
+
"viem": ">=2.0.0",
|
|
1650
|
+
"@tanstack/react-query": ">=5.0.0"
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
### Hooks
|
|
1656
|
+
|
|
1657
|
+
```tsx
|
|
1658
|
+
import { useWalletAuth, useInferBalance, useAgentBalance, useDeposit, useBuyInfer } from 'flowstack-sdk/wallet';
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
#### useWalletAuth
|
|
1662
|
+
|
|
1663
|
+
```tsx
|
|
1664
|
+
const {
|
|
1665
|
+
isConnected, // boolean
|
|
1666
|
+
isLoading, // boolean
|
|
1667
|
+
address, // string | null β checksummed wallet address
|
|
1668
|
+
isEmbeddedWallet, // boolean β Privy embedded vs MetaMask
|
|
1669
|
+
authMethod, // 'privy' | 'siwe' | null
|
|
1670
|
+
error, // string | null
|
|
1671
|
+
login, // (method?: 'privy' | 'wallet') => Promise<void>
|
|
1672
|
+
logout, // () => void
|
|
1673
|
+
} = useWalletAuth();
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
#### useInferBalance
|
|
1677
|
+
|
|
1678
|
+
```tsx
|
|
1679
|
+
const {
|
|
1680
|
+
data, // InferBalance | null β { balance, available, queryCredits, balanceWei, heldWei, availableWei }
|
|
1681
|
+
isLoading, // boolean
|
|
1682
|
+
error, // string | null
|
|
1683
|
+
refetch, // () => Promise<void>
|
|
1684
|
+
} = useInferBalance();
|
|
1685
|
+
```
|
|
1686
|
+
|
|
1687
|
+
#### useDeposit
|
|
1688
|
+
|
|
1689
|
+
```tsx
|
|
1690
|
+
const {
|
|
1691
|
+
deposit, // (amount: number) => Promise<string | null> β returns tx hash
|
|
1692
|
+
isDepositing, // boolean
|
|
1693
|
+
txHash, // string | null
|
|
1694
|
+
error, // string | null
|
|
1695
|
+
} = useDeposit();
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
#### useBuyInfer
|
|
1699
|
+
|
|
1700
|
+
```tsx
|
|
1701
|
+
const {
|
|
1702
|
+
buy, // (amount?: number) => void β opens fiat on-ramp widget
|
|
1703
|
+
isBuying, // boolean
|
|
1704
|
+
status, // 'idle' | 'pending' | 'completed' | 'failed'
|
|
1705
|
+
error, // string | null
|
|
1706
|
+
} = useBuyInfer();
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
#### useAgentBalance
|
|
1710
|
+
|
|
1711
|
+
Polls `GET /billing/agent/balance` for the connected wallet's AGENT token balance. For built apps that need to check whether the end-user has enough AGENT to run an operation.
|
|
1712
|
+
|
|
1713
|
+
```tsx
|
|
1714
|
+
const {
|
|
1715
|
+
data, // AgentBalance | null β { balance, available, buildCredits, balanceWei, ... }
|
|
1716
|
+
isLoading, // boolean
|
|
1717
|
+
error, // string | null
|
|
1718
|
+
refetch, // () => Promise<void>
|
|
1719
|
+
} = useAgentBalance();
|
|
1720
|
+
|
|
1721
|
+
// data.available β spendable AGENT (balance minus active holds)
|
|
1722
|
+
// data.balance β total AGENT in wallet
|
|
1723
|
+
```
|
|
1724
|
+
|
|
1725
|
+
### Components
|
|
1726
|
+
|
|
1727
|
+
```tsx
|
|
1728
|
+
import {
|
|
1729
|
+
LoginButton, InferBalanceBadge, AgentBalanceBadge,
|
|
1730
|
+
NeedsAgent, PaymentRequired, BuyInferModal
|
|
1731
|
+
} from 'flowstack-sdk/wallet';
|
|
1732
|
+
|
|
1733
|
+
// Privy/SIWE login β Casino PLATFORM only, NOT for built apps
|
|
1734
|
+
// Built apps use BrokeredLoginButton from 'flowstack-sdk' instead
|
|
1735
|
+
<LoginButton />
|
|
1736
|
+
|
|
1737
|
+
// Display wallet balances
|
|
1738
|
+
<InferBalanceBadge />
|
|
1739
|
+
<AgentBalanceBadge />
|
|
1740
|
+
|
|
1741
|
+
// Gate a feature behind a minimum AGENT balance.
|
|
1742
|
+
// If insufficient β shows "Get AGENT β" CTA deep-linking to OIF /buy.
|
|
1743
|
+
// If sufficient β renders children and calls onProceed when clicked.
|
|
1744
|
+
<NeedsAgent
|
|
1745
|
+
amountNeeded={2}
|
|
1746
|
+
onProceed={generateAlbumArt}
|
|
1747
|
+
returnUrl={window.location.href}
|
|
1748
|
+
>
|
|
1749
|
+
<p>Generate a new design β costs 2 AGENT</p>
|
|
1750
|
+
</NeedsAgent>
|
|
1751
|
+
|
|
1752
|
+
// Gate content behind payment
|
|
1753
|
+
<PaymentRequired>
|
|
1754
|
+
<PaidContent />
|
|
1755
|
+
</PaymentRequired>
|
|
1756
|
+
|
|
1757
|
+
// Buy INFER tokens modal
|
|
1758
|
+
<BuyInferModal />
|
|
1759
|
+
```
|
|
1760
|
+
|
|
1761
|
+
**`<NeedsAgent>` deep-link flow:**
|
|
1762
|
+
1. Built app renders `<NeedsAgent amountNeeded={5} returnUrl={window.location.href}>`
|
|
1763
|
+
2. User has 0 AGENT β "Get AGENT β" button appears
|
|
1764
|
+
3. Click opens `https://openinferencefoundation.org/buy?returnTo=<encoded-url>&need=5`
|
|
1765
|
+
4. User buys AGENT with a credit card on OIF (no wallet setup required β Privy creates it)
|
|
1766
|
+
5. OIF redirects back to `returnUrl` β user now has AGENT and can proceed
|
|
1767
|
+
|
|
1768
|
+
---
|
|
1769
|
+
|
|
1770
|
+
## Direct Agent Access
|
|
1771
|
+
|
|
1772
|
+
Discover and target specific agents directly, bypassing the default strategic planner routing.
|
|
1773
|
+
|
|
1774
|
+
### useAgents (Discovery)
|
|
1775
|
+
|
|
1776
|
+
```tsx
|
|
1777
|
+
const { agents, isLoading, refreshAgents } = useAgents();
|
|
1778
|
+
|
|
1779
|
+
// agents: AgentInfo[] β each has name, description, tools, triggerPhrases, useFor
|
|
1780
|
+
```
|
|
1781
|
+
|
|
1782
|
+
### Targeting Custom Agents (Built Apps)
|
|
1783
|
+
|
|
1784
|
+
For built apps, `targetAgents` refers to agents **you create** via `casino_create_agent`. There are no pre-built ecosystem agents β you define the persona and capabilities, the platform enforces them.
|
|
1785
|
+
|
|
1786
|
+
```tsx
|
|
1787
|
+
// 1. Create your agent via MCP first (once, at setup time):
|
|
1788
|
+
// casino_create_agent(site_id, "support_bot", "You are a helpful support assistant...", capabilities=["data_access"])
|
|
1789
|
+
// OR for per-tool control: casino_create_agent(site_id, "support_bot", "...", tools=["query_mongodb", "count_documents"])
|
|
1790
|
+
|
|
1791
|
+
// 2. Target it from your built app:
|
|
1792
|
+
const { query, messages, isStreaming } = useAgent(undefined, {
|
|
1793
|
+
targetAgents: ['support_bot'], // must match the name you registered
|
|
1794
|
+
capabilities: ['data_access'], // advisory β agent definition overrides this
|
|
1795
|
+
});
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
**CRITICAL:** `targetAgents` is required for built-app users (`appScope` set). Without it, app-scoped users get 403. The name must exactly match a `casino_create_agent` registration for your site.
|
|
1799
|
+
|
|
1800
|
+
### Available Tool Categories
|
|
1801
|
+
|
|
1802
|
+
Declare these in `capabilities` when registering an agent or calling `useAgent`:
|
|
1803
|
+
|
|
1804
|
+
| Category | What it provides |
|
|
1805
|
+
|----------|-----------------|
|
|
1806
|
+
| `data_access` | query_mongodb, insert_documents, update_documents, aggregate_mongodb, list_collections, query_data_source, and more |
|
|
1807
|
+
| `external_integration` | web_search, web_search_deep, fetch_url, google_analytics, google_ads, google_drive, RSS, Reddit |
|
|
1808
|
+
| `code_execution` | python_exec, daytona_run_code |
|
|
1809
|
+
| `workspace_management` | save_document, list_documents, search_documents, save_report, save_visualization |
|
|
1810
|
+
| `site_operations` | build_project, write_project_file, get_site_files, publish_version |
|
|
1811
|
+
| `agent_management` | spawn_subagent, list_subagents |
|
|
1812
|
+
|
|
1813
|
+
Most built-app agents only need `data_access`. Use `casino_list_tools` (MCP) to see the full tool list for each category.
|
|
1814
|
+
|
|
1815
|
+
### Tool-level Access Control (P0-117)
|
|
1816
|
+
|
|
1817
|
+
In addition to `capabilities` (category grants), you can pass an explicit `tools` list to `casino_create_agent` for per-tool access control:
|
|
1818
|
+
|
|
1819
|
+
| Param | Granularity | Example |
|
|
1820
|
+
|-------|-------------|---------|
|
|
1821
|
+
| `capabilities` | Category β all tools in that group | `["data_access"]` β all 14 data tools |
|
|
1822
|
+
| `tools` | Individual tool names | `["query_mongodb", "count_documents"]` β only those 2 |
|
|
1823
|
+
|
|
1824
|
+
**Priority:** `tools` (specific) overrides `capabilities` (category). If only `capabilities` is set, P0-116 behavior is unchanged.
|
|
1825
|
+
|
|
1826
|
+
Use `casino_list_tools()` first to see all valid tool names and categories, then pass exact names:
|
|
1827
|
+
|
|
1828
|
+
```
|
|
1829
|
+
// Step 1 β discover available tool names:
|
|
1830
|
+
casino_list_tools()
|
|
1831
|
+
// β returns { "data": ["query_mongodb", "insert_documents", ...], "capabilities": [...], ... }
|
|
1832
|
+
|
|
1833
|
+
// Step 2 β create a read-only support bot (no write access):
|
|
1834
|
+
casino_create_agent(
|
|
1835
|
+
site_id="<your_site_id>",
|
|
1836
|
+
name="support_bot",
|
|
1837
|
+
system_prompt="You are a helpful support assistant...",
|
|
1838
|
+
tools=["query_mongodb", "count_documents", "describe_collection"]
|
|
1839
|
+
)
|
|
1840
|
+
// β capabilities auto-derived as ["data_access"], tools enforced at runtime
|
|
1841
|
+
```
|
|
1842
|
+
|
|
1843
|
+
**Always-on tool exemption:** `web_search`, `create_diagram`, `list_sites`, `get_site_files`, and the 6 category meta-tools are never filtered β only category-loaded tools are subject to the allowlist.
|
|
1844
|
+
|
|
1845
|
+
---
|
|
1846
|
+
|
|
1847
|
+
## Agent Catalog
|
|
1848
|
+
|
|
1849
|
+
There are no pre-built ecosystem agents. Agents are per-app β you create them with `casino_create_agent` via the Casino MCP, then target them by name.
|
|
1850
|
+
|
|
1851
|
+
**Creating an agent (MCP) β category grant:**
|
|
1852
|
+
```
|
|
1853
|
+
casino_create_agent(
|
|
1854
|
+
site_id="<your_site_id>",
|
|
1855
|
+
name="support_bot",
|
|
1856
|
+
system_prompt="You are a helpful support assistant...",
|
|
1857
|
+
capabilities=["data_access"] // scopes what tools this agent can use
|
|
1858
|
+
)
|
|
1859
|
+
```
|
|
1860
|
+
|
|
1861
|
+
**Creating an agent (MCP) β per-tool grant:**
|
|
1862
|
+
```
|
|
1863
|
+
casino_create_agent(
|
|
1864
|
+
site_id="<your_site_id>",
|
|
1865
|
+
name="readonly_bot",
|
|
1866
|
+
system_prompt="You are a read-only support assistant...",
|
|
1867
|
+
tools=["query_mongodb", "count_documents"] // only these 2 tools, no writes
|
|
1868
|
+
)
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
**Targeting it from your app:**
|
|
1872
|
+
```tsx
|
|
1873
|
+
const { query, messages } = useAgent(undefined, {
|
|
1874
|
+
targetAgents: ['support_bot'],
|
|
1875
|
+
capabilities: ['data_access'],
|
|
1876
|
+
});
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
The backend reads the agent definition from your app's config, injects the system prompt, and enforces the declared capabilities β the client cannot escalate to broader tool access.
|
|
1880
|
+
|
|
1881
|
+
---
|
|
1882
|
+
|
|
1883
|
+
## Agent Templates & Factory
|
|
1884
|
+
|
|
1885
|
+
### Pre-configured Templates
|
|
1886
|
+
|
|
1887
|
+
```tsx
|
|
1888
|
+
import { dataScienceTemplate, marketingTemplate, supportTemplate, createCustomTemplate } from 'flowstack-sdk';
|
|
1889
|
+
|
|
1890
|
+
// Use with ChatInterface
|
|
1891
|
+
<ChatInterface template="data-science" />
|
|
1892
|
+
<ChatInterface template="marketing" />
|
|
1893
|
+
<ChatInterface template="support" />
|
|
1894
|
+
|
|
1895
|
+
// Custom template
|
|
1896
|
+
const custom = createCustomTemplate({
|
|
1897
|
+
streaming: true,
|
|
1898
|
+
networkMode: 'PUBLIC',
|
|
1899
|
+
systemPrompt: 'You are a helpful assistant for ...',
|
|
1900
|
+
});
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1903
|
+
### Agent Factory (Dynamic Routing)
|
|
1904
|
+
|
|
1905
|
+
```tsx
|
|
1906
|
+
import { AgentFactory, IntentAnalyzer, AgentRegistry, DEFAULT_PATTERNS } from 'flowstack-sdk';
|
|
1907
|
+
|
|
1908
|
+
// Register agents
|
|
1909
|
+
const registry = new AgentRegistry();
|
|
1910
|
+
registry.register({
|
|
1911
|
+
name: 'data-analyst',
|
|
1912
|
+
description: 'Analyzes datasets and creates visualizations',
|
|
1913
|
+
template: dataScienceTemplate,
|
|
1914
|
+
patterns: [/analyz/i, /chart/i, /data/i],
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
// Analyze user intent and route to best agent
|
|
1918
|
+
const analyzer = new IntentAnalyzer({ registry });
|
|
1919
|
+
const intent = analyzer.analyze('Show me a chart of Q4 revenue');
|
|
1920
|
+
// β { category: 'data-analysis', confidence: 0.92, entities: ['Q4 revenue'] }
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
---
|
|
1924
|
+
|
|
1925
|
+
## API Client
|
|
1926
|
+
|
|
1927
|
+
Direct API access β use these when you need lower-level control than hooks provide.
|
|
1928
|
+
|
|
1929
|
+
```tsx
|
|
1930
|
+
import {
|
|
1931
|
+
// Auth
|
|
1932
|
+
login, register,
|
|
1933
|
+
// Workspaces
|
|
1934
|
+
listWorkspaces, createWorkspace, getWorkspace,
|
|
1935
|
+
// Datasets
|
|
1936
|
+
listDatasets, getDataset, getDatasetPreview, deleteDataset,
|
|
1937
|
+
// Visualizations & Reports
|
|
1938
|
+
listVisualizations, listReports,
|
|
1939
|
+
// Models & Scripts
|
|
1940
|
+
listModels, getModel, listScripts,
|
|
1941
|
+
// Data Sources
|
|
1942
|
+
listDataSources, createDataSource, testDataSource, deleteDataSource,
|
|
1943
|
+
// Sites
|
|
1944
|
+
listSites, getSite, createSite, addSiteFile, publishStagedSite, deleteSite,
|
|
1945
|
+
// User Management
|
|
1946
|
+
listUsers, getUser, updateUser, deleteUser, suspendUser, reactivateUser,
|
|
1947
|
+
getUserActivity, getUserStats, checkAdminPermissions,
|
|
1948
|
+
// Conversations
|
|
1949
|
+
getConversationHistory,
|
|
1950
|
+
// Query
|
|
1951
|
+
executeQuery, executeQueryWithConfig,
|
|
1952
|
+
// File Upload
|
|
1953
|
+
uploadFile,
|
|
1954
|
+
// Low-level
|
|
1955
|
+
flowstackFetch,
|
|
1956
|
+
} from 'flowstack-sdk';
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
All functions take `(credentials, ...params, config?)` and return `Promise<ApiResponse<T>>`.
|
|
1960
|
+
|
|
1961
|
+
---
|
|
1962
|
+
|
|
1963
|
+
## Streaming Utilities
|
|
1964
|
+
|
|
1965
|
+
Parse Server-Sent Events from agent streams.
|
|
1966
|
+
|
|
1967
|
+
```tsx
|
|
1968
|
+
import { parseSSELine, parseSSEStream, processSSEStream } from 'flowstack-sdk';
|
|
1969
|
+
|
|
1970
|
+
// Parse a single SSE line
|
|
1971
|
+
const event = parseSSELine('data: {"type":"content","content":"Hello"}');
|
|
1972
|
+
|
|
1973
|
+
// Parse a full SSE stream (async generator)
|
|
1974
|
+
for await (const event of parseSSEStream(response)) {
|
|
1975
|
+
switch (event.type) {
|
|
1976
|
+
case 'content': console.log(event.content); break;
|
|
1977
|
+
case 'tool_start': console.log('Tool:', event.tool); break;
|
|
1978
|
+
case 'tool_result': console.log('Result:', event.result); break;
|
|
1979
|
+
case 'done': console.log('Complete'); break;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// High-level: process stream with callbacks
|
|
1984
|
+
await processSSEStream(response, {
|
|
1985
|
+
onContent: (text) => appendMessage(text),
|
|
1986
|
+
onToolStart: (tool) => showToolIndicator(tool),
|
|
1987
|
+
onDone: () => finalize(),
|
|
1988
|
+
});
|
|
1989
|
+
```
|
|
1990
|
+
|
|
1991
|
+
**StreamEvent types:** `content`, `delta`, `text`, `tool_start`, `tool_result`, `tool_error`, `progress`, `status`, `interrupt`, `done`, `error`
|
|
1992
|
+
|
|
1993
|
+
---
|
|
1994
|
+
|
|
1995
|
+
## Cache Utilities
|
|
1996
|
+
|
|
1997
|
+
Per-resource client-side caching with TTL.
|
|
1998
|
+
|
|
1999
|
+
```tsx
|
|
2000
|
+
import {
|
|
2001
|
+
// Generic
|
|
2002
|
+
getCached, setCached, deleteCached,
|
|
2003
|
+
CACHE_TTL,
|
|
2004
|
+
// Resource-specific
|
|
2005
|
+
getCachedWorkspaces, setCachedWorkspaces, invalidateWorkspacesCache,
|
|
2006
|
+
getCachedDatasets, setCachedDatasets, invalidateDatasetsCache,
|
|
2007
|
+
getCachedVisualizations, setCachedVisualizations, invalidateVisualizationsCache,
|
|
2008
|
+
getCachedReports, setCachedReports, invalidateReportsCache,
|
|
2009
|
+
getCachedSites, setCachedSites, invalidateSitesCache,
|
|
2010
|
+
// Bulk invalidation
|
|
2011
|
+
invalidateWorkspaceArtifacts, // Invalidate all artifacts for a workspace
|
|
2012
|
+
invalidateAllUserCache, // Clear everything for current user
|
|
2013
|
+
} from 'flowstack-sdk';
|
|
2014
|
+
```
|
|
2015
|
+
|
|
2016
|
+
---
|
|
2017
|
+
|
|
2018
|
+
## API Route Generators
|
|
2019
|
+
|
|
2020
|
+
Generate Next.js API routes for auth endpoints.
|
|
2021
|
+
|
|
2022
|
+
```tsx
|
|
2023
|
+
// app/api/auth/login/route.ts
|
|
2024
|
+
import { createLoginRoute } from 'flowstack-sdk';
|
|
2025
|
+
|
|
2026
|
+
export const POST = createLoginRoute({
|
|
2027
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
2028
|
+
passwordSecret: process.env.PASSWORD_SECRET!,
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// app/api/auth/register/route.ts
|
|
2032
|
+
import { createRegisterRoute } from 'flowstack-sdk';
|
|
2033
|
+
|
|
2034
|
+
export const POST = createRegisterRoute({
|
|
2035
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
2036
|
+
passwordSecret: process.env.PASSWORD_SECRET!,
|
|
2037
|
+
});
|
|
2038
|
+
```
|
|
2039
|
+
|
|
2040
|
+
---
|
|
2041
|
+
|
|
2042
|
+
## Mock Mode
|
|
2043
|
+
|
|
2044
|
+
Develop and test without a backend. All hooks work offline with realistic mock data.
|
|
2045
|
+
|
|
2046
|
+
```tsx
|
|
2047
|
+
<FlowstackProvider config={{ ...config, mode: 'mock' }}>
|
|
2048
|
+
<App />
|
|
2049
|
+
</FlowstackProvider>
|
|
2050
|
+
```
|
|
2051
|
+
|
|
2052
|
+
| Hook | Behavior |
|
|
2053
|
+
|------|----------|
|
|
2054
|
+
| `useAuth` | Login/register succeed with any credentials |
|
|
2055
|
+
| `useWorkspace` | Returns 3 sample workspaces |
|
|
2056
|
+
| `useDatasets` | Returns sample datasets, upload/delete simulated |
|
|
2057
|
+
| `useAgent` | Returns contextual responses with simulated streaming |
|
|
2058
|
+
| `useUserManagement` | Returns 5 sample users with stats |
|
|
2059
|
+
| `useDataSources` | Returns sample MongoDB/PostgreSQL sources |
|
|
2060
|
+
| `useSites` | Returns empty sites array |
|
|
2061
|
+
|
|
2062
|
+
```tsx
|
|
2063
|
+
import {
|
|
2064
|
+
mockCredentials, mockUser, mockWorkspaces, mockDatasets,
|
|
2065
|
+
mockVisualizations, mockDataSources, mockManagedUsers,
|
|
2066
|
+
mockUserStats, mockUserActivity, mockChatHistory,
|
|
2067
|
+
generateMockId, mockDelay,
|
|
2068
|
+
} from 'flowstack-sdk';
|
|
2069
|
+
```
|
|
2070
|
+
|
|
2071
|
+
---
|
|
2072
|
+
|
|
2073
|
+
## Configuration
|
|
2074
|
+
|
|
2075
|
+
```tsx
|
|
2076
|
+
const config = {
|
|
2077
|
+
// Required
|
|
2078
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
2079
|
+
passwordSecret: process.env.PASSWORD_SECRET!,
|
|
2080
|
+
|
|
2081
|
+
// API
|
|
2082
|
+
baseUrl: 'https://sage-api.flowstack.fun',
|
|
2083
|
+
tenantId: 't_6fe54402be43',
|
|
2084
|
+
mode: 'production', // 'production' | 'development' | 'mock'
|
|
2085
|
+
storage: 'local', // 'local' | 'session'
|
|
2086
|
+
|
|
2087
|
+
// Google OAuth (optional)
|
|
2088
|
+
auth: {
|
|
2089
|
+
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
|
2090
|
+
},
|
|
2091
|
+
|
|
2092
|
+
// Wallet / INFER token (optional)
|
|
2093
|
+
privyConfig: { appId: 'your-privy-app-id' },
|
|
2094
|
+
chain: 'arbitrum-sepolia', // 'arbitrum-sepolia' | 'arbitrum'
|
|
2095
|
+
onRampConfig: { apiKey: 'moonpay-api-key', environment: 'sandbox' },
|
|
2096
|
+
};
|
|
2097
|
+
```
|
|
2098
|
+
|
|
2099
|
+
| Option | Type | Required | Description |
|
|
2100
|
+
|--------|------|----------|-------------|
|
|
2101
|
+
| `jwtSecret` | `string` | Yes | JWT secret for token verification |
|
|
2102
|
+
| `passwordSecret` | `string` | Yes | Secret for password hashing |
|
|
2103
|
+
| `baseUrl` | `string` | No | API base URL (default: `https://sage-api.flowstack.fun`) |
|
|
2104
|
+
| `tenantId` | `string` | No | Tenant ID for user isolation |
|
|
2105
|
+
| `mode` | `string` | No | `'production'` \| `'development'` \| `'mock'` |
|
|
2106
|
+
| `storage` | `string` | No | `'local'` \| `'session'` for credentials |
|
|
2107
|
+
| `auth.googleClientId` | `string` | No | Google OAuth client ID |
|
|
2108
|
+
| `privyConfig.appId` | `string` | No | Privy app ID for embedded wallets |
|
|
2109
|
+
| `chain` | `string` | No | Blockchain for INFER token |
|
|
2110
|
+
| `onRampConfig` | `object` | No | Fiat on-ramp (MoonPay/Transak) config |
|
|
2111
|
+
|
|
2112
|
+
### Configuration Validation
|
|
2113
|
+
|
|
2114
|
+
```tsx
|
|
2115
|
+
import { validateConfig, validateConfigOrThrow, isDevelopmentConfig, getConfigSummary } from 'flowstack-sdk';
|
|
2116
|
+
|
|
2117
|
+
const result = validateConfig(config);
|
|
2118
|
+
if (!result.valid) console.error('Errors:', result.errors);
|
|
2119
|
+
result.warnings.forEach(w => console.warn(w));
|
|
2120
|
+
|
|
2121
|
+
// Or throw on invalid
|
|
2122
|
+
validateConfigOrThrow(config);
|
|
2123
|
+
```
|
|
2124
|
+
|
|
2125
|
+
---
|
|
2126
|
+
|
|
2127
|
+
## Error Handling
|
|
2128
|
+
|
|
2129
|
+
Structured errors with codes and recovery suggestions.
|
|
2130
|
+
|
|
2131
|
+
```tsx
|
|
2132
|
+
import { FlowstackError, isFlowstackError, ErrorCodes } from 'flowstack-sdk';
|
|
2133
|
+
|
|
2134
|
+
try {
|
|
2135
|
+
await someOperation();
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
if (isFlowstackError(error)) {
|
|
2138
|
+
console.log(error.code); // 'AUTHENTICATION_FAILED'
|
|
2139
|
+
console.log(error.message); // Technical message
|
|
2140
|
+
console.log(error.userMessage); // User-friendly message
|
|
2141
|
+
console.log(error.recoveryAction); // Suggested action
|
|
2142
|
+
|
|
2143
|
+
switch (error.code) {
|
|
2144
|
+
case ErrorCodes.AUTHENTICATION_FAILED: redirectToLogin(); break;
|
|
2145
|
+
case ErrorCodes.RATE_LIMITED: showRateLimitWarning(); break;
|
|
2146
|
+
case ErrorCodes.NETWORK_ERROR: showOfflineMessage(); break;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
```
|
|
2151
|
+
|
|
2152
|
+
| Code | Description |
|
|
2153
|
+
|------|-------------|
|
|
2154
|
+
| `NETWORK_ERROR` | Unable to connect to server |
|
|
2155
|
+
| `AUTHENTICATION_FAILED` | Login credentials invalid |
|
|
2156
|
+
| `ACCOUNT_NOT_ACTIVE` | Account needs verification |
|
|
2157
|
+
| `WORKSPACE_NOT_FOUND` | Workspace doesn't exist |
|
|
2158
|
+
| `DATASET_UPLOAD_FAILED` | File upload failed |
|
|
2159
|
+
| `QUERY_FAILED` | AI query failed |
|
|
2160
|
+
| `RATE_LIMITED` | Too many requests |
|
|
2161
|
+
| `CONFIG_INVALID` | Invalid SDK configuration |
|
|
2162
|
+
| `SERVER_ERROR` | Backend server error |
|
|
2163
|
+
|
|
2164
|
+
---
|
|
2165
|
+
|
|
2166
|
+
## TypeScript
|
|
2167
|
+
|
|
2168
|
+
Full TypeScript support. Key type exports:
|
|
2169
|
+
|
|
2170
|
+
```tsx
|
|
2171
|
+
import type {
|
|
2172
|
+
// Config
|
|
2173
|
+
FlowstackConfig, AuthConfig, RedisConfig, DatabaseConfig,
|
|
2174
|
+
// Auth
|
|
2175
|
+
User, FlowstackCredentials, UserRole, UserStatus, ManagedUser,
|
|
2176
|
+
LoginRequest, LoginResponse, RegisterRequest, RegisterResponse,
|
|
2177
|
+
// Workspace
|
|
2178
|
+
WorkspaceInfo, CreateWorkspaceRequest, SessionState,
|
|
2179
|
+
// Data
|
|
2180
|
+
DatasetInfo, DatasetPreview, ColumnSchema, DatasetRow,
|
|
2181
|
+
VisualizationData, ReportInfo, ModelInfo, ScriptInfo,
|
|
2182
|
+
DataSource, DataSourceType, DataSourceConfig, ConnectionTestResult,
|
|
2183
|
+
// Sites
|
|
2184
|
+
PublishedSiteInfo, CreateSiteParams, UseSitesReturn,
|
|
2185
|
+
// Chat & Streaming
|
|
2186
|
+
ChatMessage, ToolCall, StreamEvent, StreamEventType, InterruptInfo,
|
|
2187
|
+
DataSourceBadgeInfo, SearchResult, SearchResultsData,
|
|
2188
|
+
// Agent
|
|
2189
|
+
AgentTemplate, AgentConfig, QueryOptions,
|
|
2190
|
+
UseAgentReturn, UseAgentOptions,
|
|
2191
|
+
// Factory
|
|
2192
|
+
DynamicAgentConfig, IntentCategory, IntentEntity, IntentAnalysis,
|
|
2193
|
+
IntentPattern, RegisteredAgent, UseIntentAgentReturn,
|
|
2194
|
+
// Hook Returns
|
|
2195
|
+
UseAuthReturn, UseWorkspaceReturn, UseDatasetsReturn,
|
|
2196
|
+
UseVisualizationsReturn, UseReportsReturn, UseModelsReturn,
|
|
2197
|
+
UseDataSourcesReturn, UseQueryReturn, UseUserManagementReturn,
|
|
2198
|
+
// API
|
|
2199
|
+
ApiResponse, ListResponse, FlowstackClientConfig, RequestOptions,
|
|
2200
|
+
// Context
|
|
2201
|
+
FlowstackContextValue,
|
|
2202
|
+
// User Management
|
|
2203
|
+
UserActivityLog, UpdateUserRequest, UserStats, UserListParams,
|
|
2204
|
+
// Errors
|
|
2205
|
+
FlowstackError, ErrorCode, FlowstackErrorOptions,
|
|
2206
|
+
// Config Validation
|
|
2207
|
+
ValidationResult,
|
|
2208
|
+
} from 'flowstack-sdk';
|
|
2209
|
+
|
|
2210
|
+
// Wallet types (separate entry point)
|
|
2211
|
+
import type {
|
|
2212
|
+
WalletAuthState, UseWalletAuthReturn,
|
|
2213
|
+
InferBalance, UseInferBalanceReturn,
|
|
2214
|
+
UseDepositReturn, UseBuyInferReturn,
|
|
2215
|
+
WalletProviderProps,
|
|
2216
|
+
} from 'flowstack-sdk/wallet';
|
|
2217
|
+
```
|
|
2218
|
+
|
|
2219
|
+
---
|
|
2220
|
+
|
|
2221
|
+
## MCP Tool Reference (for AI agents building Casino apps)
|
|
2222
|
+
|
|
2223
|
+
Use these tools in the Casino MCP server to build, stage, and manage apps.
|
|
2224
|
+
|
|
2225
|
+
### Build Loop
|
|
2226
|
+
|
|
2227
|
+
| Tool | Description |
|
|
2228
|
+
|---|---|
|
|
2229
|
+
| `casino_create_site(name, description)` | Allocate site_id + bootstrap scaffold files |
|
|
2230
|
+
| `casino_stage_files(site_id, files)` | Stage multiple files in one call (dict of pathβcontent) |
|
|
2231
|
+
| `casino_stage_file(site_id, path, content)` | Stage a single file |
|
|
2232
|
+
| `casino_list_staged(site_id)` | List staged files β survives failed builds |
|
|
2233
|
+
| `casino_clear_staged(site_id)` | Explicit staging reset (success clears automatically) |
|
|
2234
|
+
| `casino_build_staged(site_id, message)` | Build + publish; returns url, version, build_summary |
|
|
2235
|
+
| `casino_get_app_source(site_id)` | Fetch live source for editing |
|
|
2236
|
+
|
|
2237
|
+
#### Fast build loop (one-shot)
|
|
2238
|
+
|
|
2239
|
+
```
|
|
2240
|
+
casino_create_site("My App")
|
|
2241
|
+
β bootstrap.files (6 scaffold files, pre-wired with tenant/site config)
|
|
2242
|
+
β site_id, url
|
|
2243
|
+
|
|
2244
|
+
casino_stage_files(site_id, {
|
|
2245
|
+
...bootstrap.files, // package.json, tsconfig, vite.config.ts, index.html, src/main.tsx
|
|
2246
|
+
"src/App.tsx": "...", // your product component
|
|
2247
|
+
})
|
|
2248
|
+
|
|
2249
|
+
casino_build_staged(site_id)
|
|
2250
|
+
β { url, version, build_summary: "sandbox: 3s | install+build: 35s | total: 38s" }
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2253
|
+
If the build fails β staged files are **preserved**. Fix the error and call `casino_build_staged` again without re-staging.
|
|
2254
|
+
|
|
2255
|
+
### Agent Personas
|
|
2256
|
+
|
|
2257
|
+
| Tool | Description |
|
|
2258
|
+
|---|---|
|
|
2259
|
+
| `casino_create_agent(site_id, name, system_prompt, capabilities)` | Register new agent persona |
|
|
2260
|
+
| `casino_update_agent(site_id, name, system_prompt=None, ...)` | Patch existing agent β only provided fields updated |
|
|
2261
|
+
| `casino_list_agents(site_id)` | List registered personas |
|
|
2262
|
+
|
|
2263
|
+
`capabilities` values: `data_access`, `external_integration`, `site_operations`, `code_execution`, `domain_task`, `workspace_management`, `agent_management`
|
|
2264
|
+
|
|
2265
|
+
### App Inspection
|
|
2266
|
+
|
|
2267
|
+
| Tool | Description |
|
|
2268
|
+
|---|---|
|
|
2269
|
+
| `casino_list_apps()` | List all your Casino apps |
|
|
2270
|
+
| `casino_list_collections(site_id)` | List MongoDB collections for an app |
|
|
2271
|
+
| `casino_list_tools()` | List all tool names valid for per-tool agent access control |
|
|
2272
|
+
| `casino_get_sdk_docs(topic)` | SDK documentation (full or filtered by topic) |
|
|
2273
|
+
|
|
2274
|
+
---
|
|
2275
|
+
|
|
2276
|
+
## License
|
|
2277
|
+
|
|
2278
|
+
MIT
|