Skip to content

Commit dbb54e1

Browse files
committed
fix(runtime): stream AI SSE responses, add blank template
1 parent bce47a0 commit dbb54e1

14 files changed

Lines changed: 365 additions & 1 deletion

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
Fix: AI streaming endpoints (e.g. `POST /api/v1/ai/assistant/chat`) now
6+
actually stream Server-Sent Events instead of returning the stream
7+
descriptor JSON-serialized.
8+
9+
The shared `sendResultBase()` in `dispatcher-plugin.ts` previously had a
10+
`// pass through as JSON for now` TODO, so any dispatcher route whose
11+
`result.result` was a stream descriptor (`{ type: 'stream', events,
12+
headers, ... }`) would respond with a literal `{"type":"stream",
13+
"events":{},"vercelDataStream":true,...}` body — breaking
14+
`@object-ui/plugin-chatbot` and any other Vercel-AI-SDK consumer.
15+
16+
The dispatcher now:
17+
18+
- Detects `{ type: 'stream' | stream: true, events, headers? }` shapes.
19+
- Applies the route-provided headers (defaults to
20+
`text/event-stream`/`no-cache`/`keep-alive` when none are supplied).
21+
- Performs an empty `res.write('')` synchronously so the Hono adapter's
22+
`isStreaming` flag flips before the route handler resolves (the adapter
23+
would otherwise close the body before the first async chunk lands).
24+
- Drains the `AsyncIterable<string>` of pre-encoded SSE chunks in the
25+
background, calling `res.end()` when the iterator finishes or errors.
26+
27+
Non-stream `result.result` payloads keep the existing JSON behaviour.

content/docs/guides/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"auth-sso",
2020
"security",
2121
"ai-capabilities",
22+
"plugin-chatbot-integration",
2223
"skills",
2324
"---Integration---",
2425
"api-reference",
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
title: Connecting plugin-chatbot to a framework AI backend
3+
description: Wire @object-ui/plugin-chatbot in Console (or any frontend) to the framework's /api/v1/ai/* REST surface — agents picker, SSE chat, models, HITL pending-action inbox.
4+
---
5+
6+
# Connecting plugin-chatbot to a framework AI backend
7+
8+
`@object-ui/plugin-chatbot` (a React component shipped from the `objectui`
9+
monorepo) is the canonical chat UI for ObjectStack Console. It speaks the
10+
Vercel AI Data Stream protocol, so it pairs natively with the AI routes
11+
exposed by `@objectstack/service-ai` once the framework dev server is run
12+
with the AI tier enabled.
13+
14+
This guide maps every chatbot configuration knob to its framework endpoint
15+
so you can drop the plugin into an app without reverse-engineering the
16+
contract.
17+
18+
## 1. Enable the AI tier on the framework side
19+
20+
The default `objectstack dev` preset does **not** include AI services. Pass
21+
`--preset full` (or any preset that opts into `ai`) and provide an OpenAI-
22+
compatible LLM gateway via env vars:
23+
24+
```bash
25+
OPENAI_BASE_URL=https://ai-gateway.vercel.sh/v1 \
26+
AI_GATEWAY_API_KEY=vck_*** \
27+
AI_GATEWAY_MODEL=openai/gpt-4.1-mini \
28+
CORS_ORIGIN=http://localhost:5173 \
29+
pnpm dev --preset full
30+
```
31+
32+
You should see `AIService` in the plugin list and three of the endpoints
33+
below should answer (the streaming one is verified in step 3):
34+
35+
```bash
36+
curl http://localhost:3002/api/v1/ai/agents
37+
# → { "agents": [{ "name": "data_chat", ... }, ...] }
38+
39+
curl http://localhost:3002/api/v1/ai/models
40+
# → { "models": [...] } ← may be empty until you register some
41+
```
42+
43+
## 2. Endpoint map
44+
45+
| Chatbot input | Framework route | Notes |
46+
|----------------|---------------------------------------------------|-------|
47+
| `useAgents({ apiBase })``${apiBase}/agents` | `GET /api/v1/ai/agents` | Response is `{ agents: [...] }` (also accepts a bare array). |
48+
| `<ChatbotEnhanced models={...} />` source | `GET /api/v1/ai/models` | Returns `{ models: [{ id, label }] }`. |
49+
| `useObjectChat({ api })` / `<ChatbotEnhanced api={...}>` | `POST /api/v1/ai/assistant/chat` | Vercel AI data-stream SSE. Body: `{ messages, agent?, conversationId?, ... }`. |
50+
| HITL pending inbox (custom UI) | `GET /api/v1/ai/pending-actions` | Filter by `?status=pending`. |
51+
| Approve a proposed action | `POST /api/v1/ai/pending-actions/:id/approve` | Body: `{ actor: string }`. |
52+
| Reject a proposed action | `POST /api/v1/ai/pending-actions/:id/reject` | Body: `{ actor: string, reason?: string }`. |
53+
54+
All endpoints accept the standard tenancy header `X-Environment-Id`. The
55+
default Hono adapter reflects CORS origins from the `CORS_ORIGIN` env var
56+
and already exposes `X-Environment-Id` in the allow-list, so cross-origin
57+
calls from a Vite dev server (`http://localhost:5173`) work out of the box.
58+
59+
## 3. Streaming contract
60+
61+
`POST /api/v1/ai/assistant/chat` returns:
62+
63+
```
64+
HTTP/1.1 200 OK
65+
content-type: text/plain; charset=utf-8
66+
x-vercel-ai-ui-message-stream: v1
67+
transfer-encoding: chunked
68+
69+
data: {"type":"start"}
70+
data: {"type":"text-delta","id":"0","delta":"Hello"}
71+
data: {"type":"finish","finishReason":"stop"}
72+
data: [DONE]
73+
```
74+
75+
This is the wire format `@ai-sdk/react`'s `useChat` consumes natively — no
76+
client-side parsing is required. Wire it up like:
77+
78+
```ts
79+
import { useObjectChat } from '@object-ui/plugin-chatbot';
80+
81+
const chat = useObjectChat({
82+
api: 'http://localhost:3002/api/v1/ai/assistant/chat',
83+
headers: { 'X-Environment-Id': 'env_local' },
84+
body: { agent: 'data_chat' },
85+
});
86+
```
87+
88+
> **Heads up:** If you ever see the response come back as a JSON envelope
89+
> like `{"type":"stream","events":{},"vercelDataStream":true,...}` you are
90+
> running an old `@objectstack/runtime` (< the patch tagged
91+
> `v5-runtime-ai-sse-stream-fix`). Upgrade and restart the dev server.
92+
93+
## 4. HITL (Human-in-the-Loop) flow
94+
95+
When the agent picks a dangerous action (e.g. `delete_task`) the tool
96+
handler enqueues an `ai_pending_action` row and the chat returns a
97+
`pending_approval` tool result. Your Console UI then:
98+
99+
1. Polls `GET /api/v1/ai/pending-actions?status=pending`.
100+
2. Renders an inbox item per row (`tool_name`, `tool_input`,
101+
`conversation_id`, `proposed_at`).
102+
3. On approve: `POST /api/v1/ai/pending-actions/:id/approve` with
103+
`{ actor: '<current user id>' }` → row transitions to `executed` with
104+
`result` populated.
105+
4. On reject: `POST /api/v1/ai/pending-actions/:id/reject` with
106+
`{ actor, reason }` → row transitions to `rejected`.
107+
108+
The Studio ships an `ai-pending-action` view with the same actions baked
109+
in; the same REST endpoints back both surfaces, so a custom Console widget
110+
needs no additional plumbing.
111+
112+
## Troubleshooting
113+
114+
- **`/api/v1/ai/*` returns 404** → the AI tier is not loaded. Re-run dev
115+
with `--preset full`.
116+
- **CORS blocked** → set `CORS_ORIGIN=http://your-frontend-origin` before
117+
launching the dev server.
118+
- **Chat returns JSON instead of streaming** → upgrade
119+
`@objectstack/runtime` to the patch above.
120+
- **`/api/v1/ai/models` is empty** → register models on the
121+
`AIService.modelRegistry` or skip the picker entirely; chat works
122+
without an explicit model when `AI_GATEWAY_MODEL` is set.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
.objectstack/
4+
*.log
5+
.DS_Store
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Blank Starter
2+
3+
Minimal ObjectStack environment — a clean slate for building.
4+
5+
## Getting started
6+
7+
```bash
8+
pnpm install
9+
pnpm dev
10+
```
11+
12+
The REST API is served at `http://localhost:3000/api`.
13+
14+
## Layout
15+
16+
- `objectstack.config.ts` — environment manifest (objects, API, plugins)
17+
- `src/objects/` — object definitions (one file per object)
18+
19+
## Next steps
20+
21+
- Add an object: see the `objectstack-data` skill.
22+
- Add a view or app: see `objectstack-ui`.
23+
- Add a flow or automation: see `objectstack-automation`.
24+
- Add an AI agent: see `objectstack-ai`.
25+
26+
Skills live in `skills/` in the ObjectStack framework repo and in the in-IDE
27+
assistant catalog.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { defineStack } from '@objectstack/spec';
2+
import * as objects from './src/objects/index.js';
3+
4+
export default defineStack({
5+
manifest: {
6+
id: 'blank',
7+
version: '0.1.0',
8+
type: 'app',
9+
name: 'Blank Starter',
10+
description: 'Minimal ObjectStack environment — a clean slate for building.',
11+
},
12+
objects: Object.values(objects),
13+
api: {
14+
rest: {
15+
enabled: true,
16+
basePath: '/api',
17+
},
18+
},
19+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "https://schemas.objectstack.dev/template-manifest.json",
3+
"name": "blank",
4+
"specVersion": "^6.0.0",
5+
"displayName": "Blank Starter",
6+
"description": "Minimal ObjectStack environment with a single object — a clean slate for building.",
7+
"category": "starter",
8+
"isStarter": true,
9+
"skills": ["objectstack-platform", "objectstack-data"],
10+
"readmePath": "README.md",
11+
"scaffold": {
12+
"variables": {
13+
"appName": { "type": "string", "prompt": "Application name", "default": "my-app" }
14+
}
15+
}
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "objectstack-blank",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "objectstack serve --watch",
8+
"start": "objectstack serve",
9+
"build": "objectstack build",
10+
"validate": "objectstack validate",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@objectstack/spec": "^6.0.0",
15+
"@objectstack/runtime": "^6.0.0",
16+
"@objectstack/driver-memory": "^6.0.0",
17+
"@objectstack/plugin-hono-server": "^6.0.0"
18+
},
19+
"devDependencies": {
20+
"@objectstack/cli": "^6.0.0",
21+
"typescript": "^5.3.0"
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Note } from './note.object.js';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { ObjectSchema, Field } from '@objectstack/spec/data';
4+
5+
export const Note = ObjectSchema.create({
6+
name: 'note',
7+
label: 'Note',
8+
pluralLabel: 'Notes',
9+
icon: 'sticky-note',
10+
description: 'A short note — the starter object for a blank environment.',
11+
12+
fields: {
13+
title: Field.text({
14+
label: 'Title',
15+
required: true,
16+
searchable: true,
17+
maxLength: 200,
18+
}),
19+
body: Field.longText({
20+
label: 'Body',
21+
}),
22+
},
23+
24+
enable: {
25+
apiEnabled: true,
26+
searchable: true,
27+
},
28+
});

0 commit comments

Comments
 (0)