跳转到内容

通过 AG-UI 为 Bub 接入 AI 聊天前端

bub-ag-ui 插件通过 AG-UI 将 Bub 接入 Web 聊天前端,让你接收流式回复,并让 Bub 调用浏览器中的工具。这项集成仍处于实验阶段。

本指南接入一个聊天界面,再用天气工具规划在旧金山的下午行程。如果还没有选定前端,可以从 assistant-ui 标签页开始。

将插件安装到运行 Bub 的环境中,然后启动服务:

终端窗口
bub install bub-ag-ui@main
bub ag-ui --help
bub ag-ui --host 127.0.0.1 --port 28201

保持这个终端运行。前端将向 http://127.0.0.1:28201/agent 发送请求并接收流式回复。

选择下方的一个标签页。每个示例都连接同一个 Bub 服务,在浏览器中执行 get_weather 工具,并保存界面中的对话以便刷新后恢复。天气数据是固定的旧金山演示预报。

各标签页用到以下共用文件。创建项目后,复制所选前端需要的文件。

共用示例文件

CopilotKit、OpenUI 和 AI Elements 将天气工具及数据保存为 src/scenario.json。assistant-ui 示例已在 provider 中包含这些内容。

src/scenario.json
{
"name": "Weekend weather planner",
"tool": {
"name": "get_weather",
"description": "Return a fixed demonstration weather forecast for a city. This is sample data for UI compatibility testing, not live weather.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "City to look up"}},
"required": ["city"],
"additionalProperties": false
}
},
"result": {
"city": "San Francisco",
"temperature_c": 18,
"condition": "Partly cloudy",
"source": "Fixed demo forecast; not live weather"
},
"prompt": "Call get_weather for San Francisco. Using that demo forecast, suggest a relaxed afternoon with a bookstore, coffee, and a sunset walk. Reply in English with the heading 'A slower afternoon', a three-row Markdown table (Time / Plan), and a short weather note. Keep it under 100 words. Use only get_weather; do not delegate or call other tools."
}

CopilotKit 和 OpenUI 在 vite.config.ts 的 defineConfig({...}) 中添加以下 server 属性。前端运行在 3000 端口,代理将 /api/* 去掉 /api 前缀后转发给 Bub。另外两个标签页包含各自的代理配置。

vite.config.ts (server property)
server: {
host: "localhost",
port: 3000,
strictPort: true,
proxy: {
"/api": {
target: "http://127.0.0.1:28201",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},

将存储辅助文件保存为 app/demo-session.ts(assistant-ui)或 src/demo-session.ts(其他前端)。它通过浏览器的 sessionStorage 保存会话 ID、消息和状态。

demo-session.ts
import { HttpAgent, type Message } from "@ag-ui/client";
type DemoSession = {
threadId: string;
messages: Message[];
state: HttpAgent["state"];
};
// Demo storage for one browser tab. A deployed app supplies its own storage.
export function readDemoSession(key: string): DemoSession {
const saved = sessionStorage.getItem(key);
if (saved) return JSON.parse(saved) as DemoSession;
const session = { threadId: crypto.randomUUID(), messages: [], state: {} };
sessionStorage.setItem(key, JSON.stringify(session));
return session;
}
export function persistDemoSession(key: string, agent: HttpAgent) {
const save = () =>
sessionStorage.setItem(
key,
JSON.stringify({
threadId: agent.threadId,
messages: agent.messages,
state: agent.state,
}),
);
return agent.subscribe({
onRunFinalized: save,
});
}

assistant-ui 提供 AG-UI runtime,负责连接和浏览器工具调用。

通过官方 Starter创建项目,选择默认模板,再安装 AG-UI runtime。保留 Starter 安装的 @assistant-ui/react 版本。

终端窗口
npx assistant-ui@latest create bub-assistant-ui
cd bub-assistant-ui
npm install @assistant-ui/react-ag-ui@0.1.2 @ag-ui/client@1.0.4

将共用辅助文件保存为 app/demo-session.ts。在 next.config.ts 的配置对象中添加以下 rewrite,保留 Starter 原有配置和 withAui 包装:

next.config.ts (configuration property)
async rewrites() {
return [{ source: "/api/:path*", destination: "http://127.0.0.1:28201/:path*" }];
},

创建 app/BubRuntimeProvider.tsx,接入 runtime、恢复界面消息并注册天气工具:

app/BubRuntimeProvider.tsx
app/BubRuntimeProvider.tsx
"use client";
import { useEffect, useState, type ReactNode } from "react";
import { HttpAgent } from "@ag-ui/client";
import { useAgUiRuntime, fromAgUiMessages } from "@assistant-ui/react-ag-ui";
import {
AssistantRuntimeProvider,
ExportedMessageRepository,
type ThreadHistoryAdapter,
} from "@assistant-ui/react";
import { readDemoSession, persistDemoSession } from "./demo-session";
type Session = { agent: HttpAgent; history: ThreadHistoryAdapter };
function ConnectedRuntime({ session, children }: { session: Session; children: ReactNode }) {
const runtime = useAgUiRuntime({ agent: session.agent, adapters: { history: session.history } });
useEffect(
() =>
runtime.registerModelContextProvider({
getModelContext: () => ({
tools: {
get_weather: {
description: "Return a fixed demo forecast for San Francisco, not live weather.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async () => ({
city: "San Francisco",
temperature_c: 18,
condition: "Partly cloudy",
source: "Fixed demo forecast; not live weather",
}),
},
},
}),
}),
[runtime],
);
return <AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>;
}
export function BubRuntimeProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [error, setError] = useState("");
useEffect(() => {
try {
const key = "bub-assistant-ui-demo";
const saved = readDemoSession(key);
const agent = new HttpAgent({
url: "/api/agent",
threadId: saved.threadId,
initialMessages: saved.messages,
initialState: saved.state,
});
const subscription = persistDemoSession(key, agent);
setSession({
agent,
history: {
load: async () => ({
...ExportedMessageRepository.fromArray(fromAgUiMessages(saved.messages)),
state: saved.state,
}),
// The SDK subscription saves the complete messages/state snapshot.
append: async () => {},
},
});
return () => subscription.unsubscribe();
} catch (cause) {
setError(String(cause));
}
}, []);
if (error) return <p role="alert">{error}</p>;
if (!session) return <p role="status">Loading conversation...</p>;
return <ConnectedRuntime session={session}>{children}</ConnectedRuntime>;
}

用以下内容替换 app/assistant.tsx,保留现有的 app/page.tsx 入口:

app/assistant.tsx
"use client";
import { BubRuntimeProvider } from "./BubRuntimeProvider";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
export const Assistant = () => (
<BubRuntimeProvider>
<div style={{ height: "100dvh" }}><Thread /></div>
</BubRuntimeProvider>
);
终端窗口
npm run dev -- --hostname localhost --port 3000

启动一个前端后,打开 http://localhost:3000,发送以下请求:

Call get_weather for San Francisco, then suggest an afternoon plan using the demo forecast.

Bub 会调用浏览器工具,再根据结果建议下午的行程。预报为 18°C、局部多云,使用的是演示数据,不是实时天气。

回复结束后,在同一浏览器标签页刷新,消息应仍然可见。OpenUI 需要从侧栏选择保存的会话。继续询问刚才讨论的是哪个城市、多少度,即可接着聊下去。

示例将会话 ID、消息和状态保存在 sessionStorage 中,刷新后恢复界面中的对话。没有保存消息的新浏览器标签页无法向 Bub 获取旧的界面历史;AG-UI 服务只提供 POST /agent。

Bub 自行管理模型的会话上下文。保留同一个 threadId 可以继续对话;后端重启时,需保留 Bub 的默认文件或数据库存储。浏览器存储用于恢复界面,无法恢复已删除的 Bub 上下文或未完成的工具调用。回复完成前应保持后端运行。每轮默认超时为 600 秒,包含等待浏览器工具的时间。

清除示例的存储键并刷新,即可开始新会话。不支持编辑旧消息或重新生成回复;如果 UI 模板提供这些操作,请禁用相应入口。部署时,将演示存储替换为适合应用的数据存储和用户隔离方案。

如果自行编写界面,可以用 @ag-ui/client 管理连接和消息状态。下方纯 TypeScript 示例展示流式文本,每次刷新都会创建新会话。

最小浏览器聊天示例

创建 Vite 项目:

终端窗口
npm create vite@latest bub-chat -- --template vanilla-ts
cd bub-chat
npm install
npm install @ag-ui/client@1.0.4

替换 src/main.ts:

src/main.ts
import { HttpAgent } from "@ag-ui/client";
const root = document.querySelector<HTMLDivElement>("#app")!;
root.innerHTML = `
<h1>Bub chat</h1>
<pre id="messages" aria-live="polite"></pre>
<form>
<label>Message <input name="message" required /></label>
<button>Send</button>
</form>
<p id="status" role="status"></p>
`;
const form = root.querySelector("form")!;
const input = root.querySelector("input")!;
const button = root.querySelector("button")!;
const messages = root.querySelector<HTMLPreElement>("#messages")!;
const status = root.querySelector<HTMLParagraphElement>("#status")!;
const agent = new HttpAgent({
url: "http://127.0.0.1:28201/agent",
threadId: crypto.randomUUID(),
});
agent.subscribe({
onMessagesChanged: ({ messages: current }) => {
messages.textContent = current
.map((message) => `${message.role}: ${message.content ?? ""}`)
.join("\n\n");
},
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
const content = input.value.trim();
if (!content || button.disabled) return;
agent.addMessage({ id: crypto.randomUUID(), role: "user", content });
input.value = "";
button.disabled = true;
status.textContent = "Running...";
try {
await agent.runAgent({}, {
onRunErrorEvent: ({ event }) => {
status.textContent = event.message;
},
});
if (status.textContent === "Running...") status.textContent = "Ready";
} catch (error) {
status.textContent = String(error);
} finally {
button.disabled = false;
input.focus();
}
});

启动 Vite,打开 http://localhost:3000:

终端窗口
npm run dev -- --host localhost --port 3000 --strictPort

此示例直接请求 Bub,默认允许的来源为 http://localhost:3000。如果使用其他前端来源,在启动后端前设置 BUB_AG_UI_ORIGINS:

终端窗口
export BUB_AG_UI_ORIGINS='["http://localhost:5173"]'

来源中的协议、主机名和端口都需要匹配。localhost 和 127.0.0.1 属于不同来源。

监听非回环地址时,设置 BUB_AG_UI_TOKEN,并在请求中发送 Authorization: Bearer <token>。token 保存在应用服务器中。如果浏览器直接连接 Bub,还需配置 BUB_AG_UI_ORIGINS。

共享 token 对应一个用户身份。多用户认证与网关配置见插件 README。

现象 检查方法
没有 ag-ui 命令 在安装插件的环境中运行 bub ag-ui --help。
浏览器请求失败 检查前端代理;直连时,检查 BUB_AG_UI_ORIGINS 是否与完整来源一致。
请求返回 401 或 403 检查 Bearer 请求头和认证配置。
没有回复 用 bub run "Say hello" 验证模型配置,并查看后端日志。
工具调用后没有继续回复 检查前端是否返回工具结果并继续对话。
运行返回 RUN_ERROR 查看事件中的错误信息和后端日志。