Skip to content

Commit 4ef30f0

Browse files
committed
feat: ai chatbot plugin
1 parent bbb608b commit 4ef30f0

File tree

28 files changed

+2786
-1817
lines changed

28 files changed

+2786
-1817
lines changed

.github/workflows/e2e.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515
cancel-in-progress: true
1616
permissions:
1717
contents: read
18+
env:
19+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
1820

1921
steps:
2022
- name: Checkout
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
---
2+
title: AI Chat Plugin
3+
description: Add AI-powered chat functionality with conversation history, streaming, and customizable models
4+
---
5+
6+
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
7+
import { Callout } from "fumadocs-ui/components/callout";
8+
9+
## Installation
10+
11+
Follow these steps to add the AI Chat plugin to your Better Stack setup.
12+
13+
### 1. Add Plugin to Backend API
14+
15+
Import and register the AI Chat backend plugin in your `better-stack.ts` file:
16+
17+
```ts title="lib/better-stack.ts"
18+
import { betterStack } from "@btst/stack"
19+
import { aiChatBackendPlugin } from "@btst/stack/plugins/ai-chat/api"
20+
import { openai } from "@ai-sdk/openai"
21+
// ... your adapter imports
22+
23+
const { handler, dbSchema } = betterStack({
24+
basePath: "/api/data",
25+
plugins: {
26+
aiChat: aiChatBackendPlugin({
27+
model: openai("gpt-4o"), // Or any LanguageModel from AI SDK
28+
hooks: {
29+
onBeforeChat: async (messages, context) => {
30+
// Optional: Add authorization logic
31+
return true
32+
},
33+
}
34+
})
35+
},
36+
adapter: (db) => createMemoryAdapter(db)({})
37+
})
38+
39+
export { handler, dbSchema }
40+
```
41+
42+
The `aiChatBackendPlugin()` requires a `model` parameter (from AI SDK) and accepts optional hooks for customizing behavior (authorization, logging, etc.).
43+
44+
<Callout type="info">
45+
**Model Configuration:** You can use any model from the AI SDK, including OpenAI, Anthropic, Google, and more. Make sure to install the corresponding provider package (e.g., `@ai-sdk/openai`) and set up your API keys in environment variables.
46+
</Callout>
47+
48+
### 2. Add Plugin to Client
49+
50+
Register the AI Chat client plugin in your `better-stack-client.tsx` file:
51+
52+
```tsx title="lib/better-stack-client.tsx"
53+
import { createStackClient } from "@btst/stack/client"
54+
import { aiChatClientPlugin } from "@btst/stack/plugins/ai-chat/client"
55+
56+
const getBaseURL = () =>
57+
typeof window !== 'undefined'
58+
? (process.env.NEXT_PUBLIC_BASE_URL || window.location.origin)
59+
: (process.env.BASE_URL || "http://localhost:3000")
60+
61+
export const getStackClient = (queryClient: QueryClient) => {
62+
const baseURL = getBaseURL()
63+
return createStackClient({
64+
plugins: {
65+
aiChat: aiChatClientPlugin({
66+
apiBaseURL: baseURL,
67+
apiBasePath: "/api/data",
68+
})
69+
}
70+
})
71+
}
72+
```
73+
74+
**Required configuration:**
75+
- `apiBaseURL`: Base URL for API calls
76+
- `apiBasePath`: Path where your API is mounted (e.g., `/api/data`)
77+
78+
### 3. Generate Database Schema
79+
80+
After adding the plugin, generate your database schema using the CLI:
81+
82+
```bash
83+
npx @btst/cli generate --orm prisma --config lib/better-stack.ts
84+
```
85+
86+
This will create the necessary database tables for conversations and messages. Run migrations as needed for your ORM.
87+
88+
For more details on the CLI and all available options, see the [CLI documentation](/cli).
89+
90+
## Usage
91+
92+
The AI Chat plugin provides two routes:
93+
94+
- `/chat` - Start a new conversation
95+
- `/chat/:id` - Resume an existing conversation
96+
97+
The plugin automatically handles:
98+
- Creating and managing conversations
99+
- Saving messages to the database
100+
- Streaming AI responses in real-time
101+
- Conversation history persistence
102+
103+
## Customization
104+
105+
### Backend Hooks
106+
107+
Customize backend behavior with optional hooks:
108+
109+
<AutoTypeTable path="../packages/better-stack/src/plugins/ai-chat/api/plugin.ts" name="AiChatBackendHooks" />
110+
111+
**Example usage:**
112+
113+
```ts title="lib/better-stack.ts"
114+
import { aiChatBackendPlugin, type AiChatBackendHooks } from "@btst/stack/plugins/ai-chat/api"
115+
116+
const chatHooks: AiChatBackendHooks = {
117+
onBeforeChat: async (messages, context) => {
118+
// Add authorization logic
119+
const authHeader = context.headers?.get("authorization")
120+
if (!authHeader) {
121+
return false // Deny access
122+
}
123+
return true
124+
},
125+
onAfterChat: async (conversationId, messages, context) => {
126+
// Log conversation or trigger webhooks
127+
console.log("Chat completed:", conversationId)
128+
},
129+
}
130+
131+
const { handler, dbSchema } = betterStack({
132+
plugins: {
133+
aiChat: aiChatBackendPlugin({
134+
model: openai("gpt-4o"),
135+
hooks: chatHooks
136+
})
137+
},
138+
// ...
139+
})
140+
```
141+
142+
### Model Configuration
143+
144+
You can configure different models and tools:
145+
146+
```ts title="lib/better-stack.ts"
147+
import { openai } from "@ai-sdk/openai"
148+
import { anthropic } from "@ai-sdk/anthropic"
149+
150+
// Use OpenAI
151+
aiChat: aiChatBackendPlugin({
152+
model: openai("gpt-4o"),
153+
})
154+
155+
// Or use Anthropic
156+
aiChat: aiChatBackendPlugin({
157+
model: anthropic("claude-3-5-sonnet-20241022"),
158+
})
159+
160+
// With tools (if your model supports it)
161+
aiChat: aiChatBackendPlugin({
162+
model: openai("gpt-4o"),
163+
// Tools configuration would go here if supported
164+
})
165+
```
166+
167+
## API Endpoints
168+
169+
The plugin provides the following endpoints:
170+
171+
- `POST /api/data/chat` - Send a message and receive streaming response
172+
- `GET /api/data/conversations` - List all conversations
173+
- `GET /api/data/conversations/:id` - Get a conversation with messages
174+
- `POST /api/data/conversations` - Create a new conversation
175+
- `DELETE /api/data/conversations/:id` - Delete a conversation
176+
177+
## Client Components
178+
179+
The plugin exports a `ChatInterface` component that you can use directly:
180+
181+
```tsx
182+
import { ChatInterface } from "@btst/stack/plugins/ai-chat/client"
183+
184+
export default function ChatPage() {
185+
return (
186+
<ChatInterface
187+
apiPath="/api/data/chat"
188+
initialMessages={[]}
189+
/>
190+
)
191+
}
192+
```
193+
194+
## Features
195+
196+
- **Streaming Responses**: Real-time streaming of AI responses using AI SDK v5
197+
- **Conversation History**: Automatic persistence of conversations and messages
198+
- **Customizable Models**: Use any LanguageModel from the AI SDK
199+
- **Authorization Hooks**: Add custom authentication and authorization logic
200+
- **Type-Safe**: Full TypeScript support with proper types from AI SDK
201+

docs/content/docs/plugins/index.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Better Stack provides a collection of full-stack plugins that you can easily int
1616
icon={<BookOpen size={20} />}
1717
description="Content management, editor, drafts, publishing, SEO, RSS feeds."
1818
/>
19+
<Card
20+
title="AI Chat Plugin"
21+
href="/plugins/ai-chat"
22+
icon={<BookOpen size={20} />}
23+
description="AI-powered chat with conversation history, streaming, and customizable models."
24+
/>
1925
<Card
2026
title="Building Plugins"
2127
href="/plugins/development"

e2e/playwright.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,18 @@ export default defineConfig({
7676
"**/*.todos.spec.ts",
7777
"**/*.auth-blog.spec.ts",
7878
"**/*.blog.spec.ts",
79+
"**/*.chat.spec.ts",
7980
],
8081
},
8182
{
8283
name: "tanstack:memory",
8384
use: { baseURL: "http://localhost:3004" },
84-
testMatch: ["**/*.blog.spec.ts"],
85+
testMatch: ["**/*.blog.spec.ts", "**/*.chat.spec.ts"],
8586
},
8687
{
8788
name: "react-router:memory",
8889
use: { baseURL: "http://localhost:3005" },
89-
testMatch: ["**/*.blog.spec.ts"],
90+
testMatch: ["**/*.blog.spec.ts", "**/*.chat.spec.ts"],
9091
},
9192
],
9293
});

e2e/tests/smoke.chat.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test.describe("AI Chat Plugin", () => {
4+
test("should start a new conversation and send a message", async ({
5+
page,
6+
}) => {
7+
// 1. Navigate to the chat page
8+
await page.goto("/pages/chat");
9+
10+
// 2. Verify initial state
11+
await expect(page.getByText("Start a conversation...")).toBeVisible();
12+
await expect(page.getByPlaceholder("Type a message...")).toBeVisible();
13+
14+
// 3. Send a message
15+
const input = page.getByPlaceholder("Type a message...");
16+
await input.fill("Hello, world!");
17+
// Use Enter key or find the submit button
18+
await page.keyboard.press("Enter");
19+
20+
// 4. Verify user message appears
21+
await expect(page.getByText("Hello, world!")).toBeVisible({
22+
timeout: 5000,
23+
});
24+
25+
// 5. Verify AI response appears (using real OpenAI, so response content varies, but should exist)
26+
// We wait for the AI message container - look for prose class in assistant messages
27+
await expect(page.locator(".prose").nth(1)).toBeVisible({ timeout: 30000 });
28+
});
29+
});

examples/nextjs/app/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export default function Home() {
3434
<Button className="text-destructive" variant="link" asChild>
3535
<Link href="/pages/blog/new">New Post</Link>
3636
</Button>
37+
<Button className="text-destructive" variant="link" asChild>
38+
<Link href="/pages/chat">Chat</Link>
39+
</Button>
3740
</div>
3841
</main>
3942
</div>

examples/nextjs/lib/better-stack-client.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createStackClient } from "@btst/stack/client"
22
import { todosClientPlugin } from "@/lib/plugins/todo/client/client"
33
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
4+
import { aiChatClientPlugin } from "@btst/stack/plugins/ai-chat/client"
45
import { QueryClient } from "@tanstack/react-query"
56

67
// Get base URL function - works on both server and client
@@ -84,7 +85,11 @@ export const getStackClient = (
8485
);
8586
},
8687
}
88+
}),
89+
aiChat: aiChatClientPlugin({
90+
apiBaseURL: baseURL,
91+
apiBasePath: "/api/data",
8792
})
8893
}
8994
})
90-
}
95+
}

examples/nextjs/lib/better-stack.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { createMemoryAdapter } from "@btst/adapter-memory"
22
import { betterStack } from "@btst/stack"
33
import { todosBackendPlugin } from "./plugins/todo/api/backend"
44
import { blogBackendPlugin, type BlogBackendHooks } from "@btst/stack/plugins/blog/api"
5+
import { aiChatBackendPlugin } from "@btst/stack/plugins/ai-chat/api"
6+
import { openai } from "@ai-sdk/openai"
57

68
// Define blog hooks with proper types
79
// NOTE: This is the main API at /api/data - kept auth-free for regular tests
@@ -64,7 +66,10 @@ const { handler, dbSchema } = betterStack({
6466
basePath: "/api/data",
6567
plugins: {
6668
todos: todosBackendPlugin,
67-
blog: blogBackendPlugin(blogHooks)
69+
blog: blogBackendPlugin(blogHooks),
70+
aiChat: aiChatBackendPlugin({
71+
model: openai("gpt-4o"),
72+
})
6873
},
6974
adapter: (db) => createMemoryAdapter(db)({})
7075
})

examples/nextjs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"dependencies": {
1414
"@btst/adapter-memory": "^1.0.2",
1515
"@btst/stack": "workspace:*",
16+
"ai": "^5.0.94",
17+
"@ai-sdk/react": "^2.0.94",
18+
"@ai-sdk/openai": "^2.0.68",
1619
"@next/bundle-analyzer": "^16.0.0",
1720
"@radix-ui/react-checkbox": "^1.3.3",
1821
"@radix-ui/react-dropdown-menu": "^2.1.16",

examples/react-router/app/lib/better-stack-client.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createStackClient } from "@btst/stack/client"
22
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
3+
import { aiChatClientPlugin } from "@btst/stack/plugins/ai-chat/client"
34
import { QueryClient } from "@tanstack/react-query"
45

56
// Get base URL function - works on both server and client
@@ -71,7 +72,11 @@ export const getStackClient = (queryClient: QueryClient) => {
7172
);
7273
}
7374
}
75+
}),
76+
aiChat: aiChatClientPlugin({
77+
apiBaseURL: baseURL,
78+
apiBasePath: "/api/data",
7479
})
7580
}
7681
})
77-
}
82+
}

0 commit comments

Comments
 (0)