AI agent chatbot
Build a simple AI agent chatbot that can read and edit Tiptap documents.
See the source code on GitHub.
Tech stack
- React + Next.js
- AI SDK by Vercel + OpenAI models
- Server AI Toolkit API
Installation
Create a Next.js project:
npx create-next-app@latest server-ai-agent-chatbotInstall the core Tiptap packages, collaboration extensions, and the Vercel AI SDK for OpenAI:
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-collaboration @tiptap-pro/provider ai @ai-sdk/react @ai-sdk/openai zod uuid yjs jsonwebtokenInstall the Server AI Toolkit package.
Pro package
The Server AI Toolkit is a pro package. Before installation, set up access to the private NPM registry by following the private registry guide.
npm install @tiptap-pro/server-ai-toolkitInstall TypeScript types for jsonwebtoken and uuid:
npm install --save-dev @types/jsonwebtoken @types/uuidGet schema awareness data
First, get the schema awareness data from your Tiptap editor. This data describes the document structure and is required for all API calls.
// lib/get-schema-awareness.ts
import { Editor } from '@tiptap/core'
import { getSchemaAwarenessData } from '@tiptap-pro/server-ai-toolkit'
export function getSchemaAwareness(editor: Editor) {
return getSchemaAwarenessData(editor)
}API endpoint
Create an API endpoint that uses the Vercel AI SDK to call the OpenAI model. Get tool definitions from the Server AI Toolkit API and execute tools via the API.
Environment variables
Before creating the helper functions, configure the following environment variables:
- TIPTAP_CLOUD_AI_API_URL: The base URL for the Server AI Toolkit API.
- TIPTAP_CLOUD_AI_SECRET: Your secret key for authenticating with the Server AI Toolkit API. Used to generate the JWT.
- TIPTAP_CLOUD_AI_APP_ID: Your App ID for the Server AI Toolkit API.
- TIPTAP_CLOUD_DOCUMENT_SERVER_ID: Your Tiptap Cloud Document Server ID. Included as the
experimental_document_server_idJWT claim so the Server AI Toolkit can fetch and save documents automatically. - TIPTAP_CLOUD_SECRET: The secret of the Tiptap Cloud Document Server.
- TIPTAP_CLOUD_DOCUMENT_SERVER_MANAGEMENT_API_SECRET: Your Document Server management API secret. Included as the
experimental_document_server_management_api_secretJWT claim for document access.
Create a .env file in your project root with these variables:
# .env
TIPTAP_CLOUD_AI_API_URL=https://api.tiptap.dev/v3/ai
TIPTAP_CLOUD_AI_SECRET=your-secret-key
TIPTAP_CLOUD_AI_APP_ID=your-app-id
TIPTAP_CLOUD_DOCUMENT_SERVER_ID=your-tiptap-cloud-document-server-id
TIPTAP_CLOUD_SECRET=your-tiptap-cloud-document-server-secret
TIPTAP_CLOUD_DOCUMENT_SERVER_MANAGEMENT_API_SECRET=your-tiptap-cloud-document-management-api-secretGet your App ID and secret key on the Server AI Toolkit settings page. The secret key is used to generate the JWT for authentication.
Helper functions
Create helper functions to interact with the Server AI Toolkit API:
Get the JWT
This function generates a JWT from the secret key for authenticating with the AI Toolkit API.
// lib/server-ai-toolkit/get-tiptap-cloud-ai-jwt-token.ts
import jwt from 'jsonwebtoken'
/**
* Generates a JWT from TIPTAP_CLOUD_AI_SECRET for authenticating with the AI Toolkit API
*/
export function getTiptapCloudAiJwtToken(): string {
const secret = process.env.TIPTAP_CLOUD_AI_SECRET
if (!secret) {
throw new Error('TIPTAP_CLOUD_AI_SECRET environment variable is not set')
}
const payload = {
// Document Server credentials for automatic document fetching/saving
experimental_document_server_id: process.env.TIPTAP_CLOUD_DOCUMENT_SERVER_ID,
experimental_document_server_management_api_secret: process.env.TIPTAP_CLOUD_DOCUMENT_SERVER_MANAGEMENT_API_SECRET,
}
return jwt.sign(payload, secret, { expiresIn: '1h' })
}Get tool definitions
This function fetches the available tool definitions from the Server AI Toolkit API.
// lib/server-ai-toolkit/get-tool-definitions.ts
import type z from 'zod'
import { getTiptapCloudAiJwtToken } from './get-tiptap-cloud-ai-jwt-token'
/**
* Gets tool definitions from the Server AI Toolkit API
*/
export async function getToolDefinitions(schemaAwarenessData: unknown): Promise<
{
name: string
description: string
inputSchema: z.core.JSONSchema.JSONSchema
}[]
> {
const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev/v3/ai'
const appId = process.env.TIPTAP_CLOUD_AI_APP_ID
if (!appId) {
throw new Error('Missing TIPTAP_CLOUD_AI_APP_ID')
}
const response = await fetch(`${apiBaseUrl}/toolkit/tools`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getTiptapCloudAiJwtToken()}`,
'X-App-Id': appId,
},
body: JSON.stringify({
schemaAwarenessData,
}),
})
if (!response.ok) {
throw new Error(`Failed to fetch tools: ${response.statusText}`)
}
const responseData = await response.json()
return responseData.tools
}Get schema awareness prompt
This function retrieves a formatted prompt string from the Server AI Toolkit API that describes the document's schema to the AI model. This prompt is included in the agent's instructions to help the AI understand what nodes, marks, and attributes are available in the document.
// lib/server-ai-toolkit/get-schema-awareness-prompt.ts
import { getTiptapCloudAiJwtToken } from './get-tiptap-cloud-ai-jwt-token'
/**
* Gets the schema awareness prompt from the Server AI Toolkit API
*/
export async function getSchemaAwarenessPrompt(schemaAwarenessData: unknown): Promise<string> {
const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev/v3/ai'
const appId = process.env.TIPTAP_CLOUD_AI_APP_ID
if (!appId) {
throw new Error('Missing TIPTAP_CLOUD_AI_APP_ID')
}
const response = await fetch(`${apiBaseUrl}/toolkit/schema-awareness-prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getTiptapCloudAiJwtToken()}`,
'X-App-Id': appId,
},
body: JSON.stringify({
schemaAwarenessData,
}),
})
if (!response.ok) {
throw new Error(`Failed to fetch schema awareness prompt: ${response.statusText}`)
}
const result: { prompt: string } = await response.json()
return result.prompt
}Execute tool
This function executes a tool via the Server AI Toolkit API. It sends the tool name, input parameters, document ID, and schema awareness data to the API. The server automatically fetches and saves the Tiptap Cloud document using the credentials in the JWT.
// lib/server-ai-toolkit/execute-tool.ts
import { getTiptapCloudAiJwtToken } from './get-tiptap-cloud-ai-jwt-token'
/**
* Executes a tool via the Server AI Toolkit API
*/
export async function executeTool(
toolName: string,
input: unknown,
documentId: string,
schemaAwarenessData: unknown,
): Promise<{ output: unknown; docChanged: boolean }> {
const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev/v3/ai'
const appId = process.env.TIPTAP_CLOUD_AI_APP_ID
if (!appId) {
throw new Error('Missing TIPTAP_CLOUD_AI_APP_ID')
}
const response = await fetch(`${apiBaseUrl}/toolkit/execute-tool`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getTiptapCloudAiJwtToken()}`,
'X-App-Id': appId,
},
body: JSON.stringify({
toolName,
input,
experimental_documentOptions: { documentId },
schemaAwarenessData,
}),
})
if (!response.ok) {
throw new Error(`Tool execution failed: ${response.statusText}`)
}
return response.json()
}Now create the main API route:
// app/api/server-ai-agent-chatbot/route.ts
import { openai } from '@ai-sdk/openai'
import { createAgentUIStreamResponse, ToolLoopAgent, tool, type UIMessage } from 'ai'
import z from 'zod'
import { executeTool } from '@/lib/server-ai-toolkit/execute-tool'
import { getSchemaAwarenessPrompt } from '@/lib/server-ai-toolkit/get-schema-awareness-prompt'
import { getToolDefinitions } from '@/lib/server-ai-toolkit/get-tool-definitions'
export async function POST(req: Request) {
const {
messages,
schemaAwarenessData,
documentId,
}: {
messages: UIMessage[]
schemaAwarenessData: unknown
documentId: string
} = await req.json()
// Get tool definitions from the Server AI Toolkit API
const toolDefinitions = await getToolDefinitions(schemaAwarenessData)
// Get schema awareness prompt from the Server AI Toolkit API
const schemaAwarenessPrompt = await getSchemaAwarenessPrompt(schemaAwarenessData)
// Convert API tool definitions to AI SDK tool format
const tools = Object.fromEntries(
toolDefinitions.map((toolDef) => [
toolDef.name,
tool({
description: toolDef.description,
inputSchema: z.fromJSONSchema(toolDef.inputSchema),
execute: async (input) => {
try {
const result = await executeTool(toolDef.name, input, documentId, schemaAwarenessData)
return result.output
} catch (error) {
console.error(`Failed to execute tool ${toolDef.name}:`, error)
return {
error: error instanceof Error ? error.message : 'Unknown error',
}
}
},
}),
]),
)
const agent = new ToolLoopAgent({
model: openai('gpt-5-mini'),
instructions: `You are an assistant that can edit rich text documents.
In your responses, be concise and to the point. However, the content of the document you generate does not need to be concise and to the point, instead, it should follow the user's request as closely as possible.
Before calling any tools, summarize you're going to do (in a sentence or less), as a high-level view of the task, like a human writer would describe it.
Rule: In your responses, do not give any details of the tool calls
Rule: In your responses, do not give any details of the HTML content of the document.
${schemaAwarenessPrompt}`,
tools,
providerOptions: {
openai: {
reasoningEffort: 'minimal',
},
},
})
return createAgentUIStreamResponse({
agent,
uiMessages: messages,
})
}Client-side setup
Create a client-side React component that renders the Tiptap Editor with collaboration support and a simple chat UI. This component leverages the useChat hook from the Vercel AI SDK to call the API endpoint and manage the chat conversation.
First, create a server action to generate a JWT for Tiptap Cloud collaboration. This function creates a secure JWT that authorizes the client to access a specific document in Tiptap Cloud's collaboration service.
// app/server-ai-agent-chatbot/actions.ts
'use server'
import jwt from 'jsonwebtoken'
const TIPTAP_CLOUD_SECRET = process.env.TIPTAP_CLOUD_SECRET
const TIPTAP_CLOUD_DOCUMENT_SERVER_ID = process.env.TIPTAP_CLOUD_DOCUMENT_SERVER_ID
export async function getCollabConfig(
userId: string,
documentName: string,
): Promise<{ token: string; appId: string }> {
if (!TIPTAP_CLOUD_SECRET) {
throw new Error('TIPTAP_CLOUD_SECRET environment variable is not set')
}
if (!TIPTAP_CLOUD_DOCUMENT_SERVER_ID) {
throw new Error('TIPTAP_CLOUD_DOCUMENT_SERVER_ID environment variable is not set')
}
const payload = {
sub: userId,
allowedDocumentNames: [documentName],
}
const token = jwt.sign(payload, TIPTAP_CLOUD_SECRET, { expiresIn: '1h' })
return { token, appId: TIPTAP_CLOUD_DOCUMENT_SERVER_ID }
}Now create the main page component:
// app/server-ai-agent-chatbot/page.tsx
'use client'
import { useChat } from '@ai-sdk/react'
import { Collaboration } from '@tiptap/extension-collaboration'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import { getSchemaAwarenessData } from '@tiptap-pro/server-ai-toolkit'
import { DefaultChatTransport } from 'ai'
import { useEffect, useRef, useState } from 'react'
import { v4 as uuid } from 'uuid'
import * as Y from 'yjs'
import { getCollabConfig } from './actions'
export default function Page() {
const [doc] = useState(() => new Y.Doc())
const [documentId] = useState(() => `server-ai-agent-chatbot/${uuid()}`)
const providerRef = useRef<TiptapCollabProvider | null>(null)
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, Collaboration.configure({ document: doc })],
})
// Get the JWT and appId from server action
useEffect(() => {
const setupProvider = async () => {
try {
const { token, appId } = await getCollabConfig('user-1', documentId)
const collabProvider = new TiptapCollabProvider({
appId,
name: documentId,
token,
document: doc,
user: 'user-1',
onOpen() {
console.log('WebSocket connection opened.')
},
onConnect() {
editor?.commands.setContent(initialContent)
},
})
providerRef.current = collabProvider
} catch (error) {
console.error('Failed to setup collaboration:', error)
}
}
setupProvider()
return () => {
if (providerRef.current) {
providerRef.current.destroy()
providerRef.current = null
}
}
}, [documentId, doc, editor])
// Fixes issue: https://github.com/vercel/ai/issues/7819
const schemaAwarenessData = editor ? getSchemaAwarenessData(editor) : null
const schemaAwarenessDataRef = useRef(schemaAwarenessData)
schemaAwarenessDataRef.current = schemaAwarenessData
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: '/api/server-ai-agent-chatbot',
body: () => ({
schemaAwarenessData: schemaAwarenessDataRef.current,
documentId,
}),
}),
})
const [input, setInput] = useState('Replace the last paragraph with a short story about Tiptap')
if (!editor) return null
return (
<div>
<EditorContent editor={editor} />
{messages?.map((message) => (
<div key={message.id} style={{ whiteSpace: 'pre-wrap' }}>
<strong>{message.role}</strong>
<br />
<div className="mt-2 whitespace-pre-wrap">
{message.parts
.filter((p) => p.type === 'text')
.map((p) => p.text)
.join('\n') || 'Loading...'}
</div>
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault()
if (input.trim()) {
sendMessage({ text: input })
setInput('')
}
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
</div>
)
}Document state management
This implementation passes a documentId via experimental_documentOptions so the Server AI
Toolkit automatically fetches and saves the document from Tiptap Cloud. The client uses Tiptap
Collaboration to sync changes in real-time.
Alternative: provide the document directly
Instead of using Tiptap Cloud documents, you can provide and manage the document yourself by
passing a document field (instead of experimental_documentOptions) in the execute-tool
request body. With this approach, you need to fetch the document before each tool execution
and save the updated document when result.docChanged is true.
See the REST API reference.
End result
With additional CSS styles, the result is a simple but polished AI chatbot application that uses the Server AI Toolkit to edit documents:
See the source code on GitHub.
Next steps
- Allow your AI to generate custom elements with the Schema awareness guide
- Learn more about the available tools in the Agents section
- Explore the REST API reference for complete endpoint documentation