@flowget/ai-chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@flowget/ai-chat` are documented here. This
4
+ project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0:
5
+ minor = breaking).
6
+
7
+ ## [0.1.0]
8
+
9
+ Initial release — a builder-agnostic AI workflow-authoring chat.
10
+
11
+ ### Added
12
+
13
+ - **`@flowget/ai-chat/react`** — the client.
14
+ - `<WorkflowChat currentGraph={…} applyGraph={…} />`: a turnkey floating
15
+ chat panel (assistant-ui `AssistantModalPrimitive`) with a streaming
16
+ runtime, a thread, and a proposal → **approval** → apply flow. The
17
+ approval gate pauses the run; the proposal card renders Apply / Dismiss
18
+ inline; applying lays the graph out (preserving surviving-node positions)
19
+ and commits it through the host's `applyGraph`. Builder-agnostic: the host
20
+ owns the canvas via the `currentGraph` / `applyGraph` seam.
21
+ - Props for light theming and continuity: `heading`, `subtitle`, `examples`,
22
+ `endpoint`, a fully custom `transport`, and `actor` / `context` request
23
+ metadata.
24
+ - A host-pluggable `layout` prop: delegate proposal layout to your builder
25
+ (e.g. a dagre merge) instead of the built-in `layoutProposal`, keeping
26
+ ai-chat builder-agnostic (the host supplies the function).
27
+ - **Headless tier** for hosts that bring their own shell / transport:
28
+ `ChatRuntimeProvider`, `createWorkflowChatAdapter`,
29
+ `httpChatStreamTransport`, and the pure `layoutProposal` layout helper.
30
+ - **`@flowget/ai-chat/server`** — the BFF helper (React-free).
31
+ - `createChatStreamResponse(req, config, options?)`: parses + validates the
32
+ request, then streams `@flowget/ai`'s `authorStream()` to the browser as
33
+ SSE. `options.buildRequest` is the seam for injecting a **server-derived**
34
+ actor (from the host's session/JWT) over the untrusted client value;
35
+ `options.onError` maps errors to client-safe messages.
36
+ - `streamAuthorSSE(request, config, options?)`: the lower-level primitive for
37
+ hosts that build/validate the `AuthorRequest` themselves.
38
+ - `parseChatRequest(req, limits?)`: body/command size bounds and shape
39
+ validation (rejects a non-object or array `currentGraph`).
40
+ - **`@flowget/ai-chat/styles.css`** — a self-contained stylesheet. Every value
41
+ reads a `--flowget-*` design token with a sensible built-in fallback, so the
42
+ chat renders standalone and inherits a host's theme where those tokens are
43
+ defined. Placement is tunable via `--fg-aichat-z` and
44
+ `--fg-aichat-launcher-offset`; class names use the `fg-aichat-` namespace.
45
+
46
+ ### Security
47
+
48
+ - The `/server` helper is **unauthenticated by design**. The host must place
49
+ its own authentication, authorization, and rate-limiting in front of the
50
+ route, and inject a server-derived `actor` via `buildRequest` for any
51
+ actor-scoped data access. Server errors are logged server-side and never
52
+ forwarded verbatim to the browser. See the README "Security" section.
package/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Flowget
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @flowget/ai-chat
2
+
3
+ Builder-agnostic **AI workflow-authoring chat** for [Flowget](https://flowget.io).
4
+
5
+ A user describes a workflow in natural language; the chat streams a proposal,
6
+ shows an **approval card**, and — once approved — applies the laid-out graph to
7
+ your canvas. Editing works the same way (the current graph is the context).
8
+
9
+ Built on assistant-ui primitives (streaming, approval gate, thread) and
10
+ [`@flowget/ai`](https://github.com/flowgethq/ai)'s streaming `author()` — with
11
+ **no lock-in**: the LLM stays behind `@flowget/ai`'s BYO adapter, and the chat
12
+ never imports your builder.
13
+
14
+ ## Two entry points
15
+
16
+ | Import | Runs | What |
17
+ | --- | --- | --- |
18
+ | `@flowget/ai-chat/react` | browser | `<WorkflowChat>` — the chat panel + streaming runtime + proposal card |
19
+ | `@flowget/ai-chat/server` | your BFF | `createChatStreamResponse()` — SSE over `@flowget/ai`'s `authorStream()` |
20
+ | `@flowget/ai-chat/styles.css` | — | the stylesheet (import once) |
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @flowget/ai-chat @flowget/ai @assistant-ui/react
26
+ ```
27
+
28
+ `react`, `react-dom`, `@assistant-ui/react` are peer deps (you provide them).
29
+ `@flowget/ai` is an **optional** peer — install it only if you use the
30
+ `/server` helper (the authoring engine runs there). A react-only consumer with
31
+ its own backend transport can skip it.
32
+
33
+ ## Client — mount the chat
34
+
35
+ Drop `<WorkflowChat>` inside your builder. It's builder-agnostic: pass the live
36
+ `currentGraph` and an `applyGraph` callback (e.g. your store's `setGraph`). The
37
+ chat proposes; you apply.
38
+
39
+ ```tsx
40
+ import "@flowget/ai-chat/styles.css";
41
+ import { WorkflowChat } from "@flowget/ai-chat/react";
42
+ import { useFlowgetWorkflow } from "@flowget/builder"; // your builder
43
+
44
+ function BuilderChat() {
45
+ const { graph, setGraph } = useFlowgetWorkflow();
46
+ return <WorkflowChat currentGraph={graph} applyGraph={setGraph} />;
47
+ }
48
+
49
+ // inside your builder's children slot:
50
+ // <FlowgetBuilder ...><BuilderChat /></FlowgetBuilder>
51
+ ```
52
+
53
+ `WorkflowChat` props:
54
+
55
+ | Prop | Type | |
56
+ | --- | --- | --- |
57
+ | `currentGraph` | `WorkflowGraph` | live canvas graph (edit context + layout baseline) |
58
+ | `applyGraph` | `(g: WorkflowGraph) => void` | commit a laid-out graph to the canvas |
59
+ | `transport?` | `ChatStreamTransport` | fully custom backend transport (overrides `endpoint`) |
60
+ | `endpoint?` | `string` | endpoint for the default SSE transport (`/api/chat`) |
61
+ | `heading?` | `string` | panel heading (default `"Workflow AI"`) |
62
+ | `subtitle?` | `string` | panel subtitle (default `"Draft & edit with a prompt"`) |
63
+ | `examples?` | `readonly string[]` | empty-state suggestion chips; `[]` hides them |
64
+ | `layout?` | `WorkflowLayout` | lay a proposal out before apply (default `layoutProposal`) — see below |
65
+ | `actor?` | `ChatActor` | caller-identity hint — **untrusted on the wire** (see [Security](#security)) |
66
+ | `context?` | `unknown` | opaque continuity payload forwarded to the model |
67
+
68
+ ### Layout — delegate to your builder
69
+
70
+ By default the chat lays each proposal out with the built-in `layoutProposal`
71
+ (a zero-dependency BFS layered layout that preserves the positions of nodes
72
+ already on the canvas). A builder-host with a better layout — e.g. a dagre
73
+ `autoLayout` / merge — injects it via the `layout` prop, keeping ai-chat
74
+ builder-agnostic (the host supplies the function; the chat never imports your
75
+ builder):
76
+
77
+ ```tsx
78
+ import { WorkflowChat, type WorkflowLayout } from "@flowget/ai-chat/react";
79
+
80
+ const layout: WorkflowLayout = (proposed, current) => mergeGraph(current, proposed);
81
+
82
+ <WorkflowChat currentGraph={graph} applyGraph={setGraph} layout={layout} />;
83
+ ```
84
+
85
+ `layout: (proposed: ProposedGraph, current: WorkflowGraph) => WorkflowGraph`.
86
+ `layoutProposal` stays exported as the default / fallback.
87
+
88
+ ### Headless mode
89
+
90
+ `<WorkflowChat>` is a turnkey shell built from smaller pieces. To bring your own
91
+ shell — or render assistant-ui primitives yourself — compose them directly:
92
+
93
+ ```tsx
94
+ import {
95
+ ChatRuntimeProvider,
96
+ httpChatStreamTransport,
97
+ layoutProposal,
98
+ } from "@flowget/ai-chat/react";
99
+
100
+ // getCurrentGraph must be referentially stable (a ref getter, not an inline fn)
101
+ <ChatRuntimeProvider getCurrentGraph={getCurrentGraph} transport={httpChatStreamTransport("/api/chat")}>
102
+ {/* your own assistant-ui <Thread>, composer, and proposal rendering */}
103
+ </ChatRuntimeProvider>
104
+ ```
105
+
106
+ - `ChatRuntimeProvider` — wires assistant-ui's `LocalRuntime` to the authoring
107
+ adapter and provides it to your subtree.
108
+ - `createWorkflowChatAdapter(...)` — the raw `ChatModelAdapter`, if you drive
109
+ the runtime yourself.
110
+ - `httpChatStreamTransport(endpoint?)` — the default SSE transport; swap it for
111
+ any `ChatStreamTransport` (an async generator of `ChatStreamEvent`s) to reach
112
+ a custom backend.
113
+ - `layoutProposal(proposed, current?)` — the pure layout helper (BFS layered
114
+ layout; preserves the positions of nodes that already exist on the canvas).
115
+
116
+ ## Server — the BFF route
117
+
118
+ Wire your `@flowget/ai` config (your node catalog + LLM adapter) into the SSE
119
+ helper. The engine runs entirely server-side.
120
+
121
+ ```ts
122
+ import { createChatStreamResponse } from "@flowget/ai-chat/server";
123
+ import { finalizeConfig, resolveCatalog, openaiAdapter } from "@flowget/ai";
124
+
125
+ const catalog = await resolveCatalog({ registryDir: process.env.REGISTRY_DIR! });
126
+ const config = finalizeConfig(
127
+ { adapter: openaiAdapter({ apiKey: process.env.OPENROUTER_API_KEY! }), catalog },
128
+ catalog,
129
+ );
130
+
131
+ // e.g. a Bun / Node / edge handler for POST /api/chat
132
+ export async function POST(req: Request): Promise<Response> {
133
+ // ⚠️ Your auth + rate-limiting run FIRST — see Security below.
134
+ const session = await getSession(req);
135
+ if (!session) return new Response("Unauthorized", { status: 401 });
136
+
137
+ return createChatStreamResponse(req, config, {
138
+ // Inject a SERVER-DERIVED actor over the untrusted client value:
139
+ buildRequest: (parsed) => ({ ...parsed, actor: session.actor }),
140
+ });
141
+ }
142
+ ```
143
+
144
+ `createChatStreamResponse(req, config, options?)` — parses + validates the
145
+ request, then streams the authoring result as SSE. Options:
146
+
147
+ | Option | | |
148
+ | --- | --- | --- |
149
+ | `buildRequest?` | `(parsed, req) => AuthorRequest` | rebuild the request — inject a server-derived `actor` / `context` |
150
+ | `onError?` | `(err) => string` | map an error to a client-safe message (default `"Authoring failed"`) |
151
+ | `maxBodyBytes?` | `number` | request-body cap (default 1 MiB) |
152
+ | `maxCommandLength?` | `number` | `command` length cap (default 8000) |
153
+
154
+ Need to build the `AuthorRequest` yourself? Use the lower-level
155
+ `streamAuthorSSE(request, config, { signal?, onError? })`.
156
+
157
+ The wire format between `/server` and `/react` is SSE: one `data:`-framed event
158
+ per line — `text-delta`\* then a terminal `proposal | message`, or `error`.
159
+
160
+ ## Security
161
+
162
+ The `/server` helper is **unauthenticated by design** and does **no**
163
+ rate-limiting. It is streaming plumbing, not a security boundary.
164
+
165
+ **You MUST:**
166
+
167
+ - Put your own **authentication and authorization** in front of the route
168
+ (reject unauthenticated requests before calling the helper).
169
+ - Add **rate-limiting** / abuse protection — each request drives an LLM call.
170
+ - Treat the client-supplied `actor` as **untrusted**. It is plain JSON on the
171
+ request body; a browser can send anything. If any `@flowget/ai` toolset or
172
+ authorizer gates data access on the actor (e.g. by `tenantId`), derive the
173
+ actor **server-side** (from your session/JWT) and inject it with
174
+ `buildRequest` — and/or verify it with a `@flowget/ai` authorizer. Otherwise
175
+ a client can spoof it.
176
+
177
+ Server-side errors are logged on the server and never forwarded verbatim to the
178
+ browser (the client only sees a generic message, or your `onError` mapping).
179
+
180
+ ## Theming
181
+
182
+ Import the stylesheet once — it is **self-contained** and renders correctly with
183
+ no host theme. Every value reads a `--flowget-*` design token with a built-in
184
+ fallback, so where your app defines those tokens (light/dark), the chat inherits
185
+ them automatically; where it doesn't, the fallback applies.
186
+
187
+ Overridable design tokens (set them on `:root` or any ancestor):
188
+
189
+ | Token | Fallback | Used for |
190
+ | --- | --- | --- |
191
+ | `--flowget-color-accent` | `#6366f1` | primary / brand accent |
192
+ | `--flowget-color-text` | `#0f172a` | body text |
193
+ | `--flowget-color-text-subtle` | `#64748b` | secondary text |
194
+ | `--flowget-color-text-inverse` | `#ffffff` | text on the accent |
195
+ | `--flowget-color-bg` | `#ffffff` | input / chip background |
196
+ | `--flowget-color-bg-elevated` | `#ffffff` | panel background |
197
+ | `--flowget-color-surface-strong` | `#f8fafc` | proposal-card surface |
198
+ | `--flowget-color-border` | `#e2e8f0` | borders |
199
+ | `--flowget-color-border-strong` | `#cbd5e1` | hover borders |
200
+ | `--flowget-color-danger` | `#dc2626` | error state |
201
+ | `--flowget-color-success` | `#16a34a` | applied confirmation |
202
+ | `--flowget-radius-lg` / `-md` | `14px` / `9px` | corner radii |
203
+ | `--flowget-shadow-lg` | `0 12px 32px -8px rgba(15,23,42,.28)` | panel / launcher shadow |
204
+ | `--flowget-font-sans` / `-mono` | system stacks | typography |
205
+ | `--flowget-font-size-sm` / `-xs` / `-md` | `13px` / `11px` / `15px` | type scale |
206
+ | `--flowget-motion-fast` | `140ms` | transition duration |
207
+
208
+ Two package-owned knobs control placement (class prefix: `fg-aichat-`):
209
+
210
+ | Token | Fallback | |
211
+ | --- | --- | --- |
212
+ | `--fg-aichat-z` | `200` | stacking order of the launcher + panel |
213
+ | `--fg-aichat-launcher-offset` | `24px` | gap from the bottom-right corner |
214
+
215
+ ## How it fits together
216
+
217
+ ```
218
+ browser your BFF @flowget/ai
219
+ ──────── ──────── ───────────
220
+ <WorkflowChat> createChatStreamResponse(req,cfg)
221
+ LocalRuntime ── POST /api/chat ──► authorStream(cfg, request) ──► BYO adapter (LLM)
222
+ (streams text) ◄──── SSE ──────── (text-delta* → proposal|message)
223
+ proposal card → Apply → applyGraph(graph) (your canvas)
224
+ ```
225
+
226
+ ## License
227
+
228
+ FSL-1.1-ALv2