用 NiceGUI 为 nanobot 打造 Web GUI:让 AI 对话更便捷
标签:#Python #nanobot #NiceGUI #AI工具 #GUI开发
日期:2026-05-08
摘要:nanobot 是一个轻量级的 AI Agent 框架,但仅提供 CLI 界面。本文介绍如何使用 NiceGUI(纯 Python Web 框架)为 nanobot 打造一个基本的 Web GUI,支持复制粘贴、远程访问,让 AI 对话更加便捷高效。
前言
在 AI Agent 的世界里,命令行界面(CLI)虽然高效,但复制粘贴对话内容总是不够方便。如果能有一个图形界面,可以远程访问,那体验会好很多。
今天我来分享如何用 NiceGUI 为 nanobot 打造一个 Web GUI 界面。界面如下图所示:
一、nanobot 简介
🎯 什么是 nanobot
nanobot 是一个轻量级的 AI Agent 框架,由 HKUDS 团队开发。其特点是:麻雀虽小,五脏俱全。
⚡ 核心特点
| 特点 | 说明 |
|---|---|
| 原生 Python | 纯 Python 实现,无需其他依赖 |
| 跨平台 | 不需要 JS、不需要 WSL,Windows 即可运行 |
| 提供 API | 完整的 Python API 库,方便学习和集成 |
| Skills 兼容 | 工具和 Skills 可以和龙虾、Hermes 通用 |
| 轻量级 | 代码简洁,适合学习 AI Agent 架构 |
💡 美中不足
虽然 nanobot 功能完备,但目前只提供了 CLI 界面,没有 GUI 或 Web UI。对于日常使用来说,复制粘贴对话内容不够方便,也无法远程访问。
二、NiceGUI 简介
🎯 什么是 NiceGUI
NiceGUI 是一个基于 Python 的现代 Web UI 框架。它的核心理念是:用 Python 开发 Web 页面,无需掌握 JS 和 HTML。
⚡ 核心特点
| 特点 | 说明 |
|---|---|
| 纯 Python | 只需写 Python 代码,自动生成 Web 页面 |
| 精美 UI | 内置 Material Design 风格的组件 |
| 实时更新 | 支持响应式 UI,告别页面刷新 |
| 原生应用 | 可打包成 Windows/macOS/Linux 原生应用 |
| 易于部署 | 就是一个 Web 服务,启动即可访问 |
💡 简单示例
from nicegui import ui
ui.label('Hello, NiceGUI!')
ui.button('Click me', on_click=lambda: ui.notify('Clicked!'))
ui.run()
只需这几行代码,一个精美的 Web 页面就诞生了!
三、用 NiceGUI 打造 nanobot GUI
🎯 设计目标
- 便捷交互:支持复制粘贴,告别繁琐的 CLI 操作
- 远程访问:通过 Web 界面,随时随地访问 AI 对话
- 美观界面:利用 NiceGUI 的组件,打造现代化体验
- 功能完整:保留 nanobot 的核心功能(Skills、Tools 等)
💻 核心实现
1. 导入依赖与初始化
import asyncio
from nicegui import ui
from nanobot import Nanobot
from nanobot.agent import AgentHook, AgentHookContext
# 从配置文件初始化 nanobot
bot = Nanobot.from_config()
2. 自定义 Hook:处理流式输出与工具调用
这是核心部分,通过 ChatHook 实现流式输出和工具调用的实时显示:
class ChatHook(AgentHook):
def __init__(self):
self.content = ''
'''存放 Agent 的输出内容. 根据流式输出累加更新'''
self.user_message = ''
'''存放用户输入的消息'''
self.tool_calls = set()
'''记录当前用到的工具名称(去重)'''
self._display_ref = None # 保存 markdown_display 的引用
async def on_stream(self, ctx: AgentHookContext, delta: str):
self.content += delta
self._update_display()
async def before_execute_tools(self, ctx: AgentHookContext) -> None:
'''在工具执行前记录工具名称'''
for tc in ctx.tool_calls:
self.tool_calls.add(tc.name)
self._update_display()
def bind_display(self, display):
'''绑定 markdown_display 引用'''
self._display_ref = display
def _update_display(self):
'''更新 Markdown 显示内容'''
if self._display_ref is None:
return
parts = [f'**你**: {self.user_message}']
parts.append('\n\n---\n\n')
# 如果有工具调用,精简显示
if self.tool_calls:
tools_str = ' → '.join(sorted(self.tool_calls))
parts.append(f'🛠️ 调用工具: `{tools_str}`')
parts.append('\n\n---\n\n')
parts.append('**Agent:**\n\n')
if self.content:
parts.append(f'{self.content}▌')
else:
parts.append('_正在思考..._')
self._display_ref.set_content(''.join(parts))
关键点:
before_execute_tools钩子自动捕获 Agent 调用的工具名称_update_display方法封装显示更新逻辑,支持流式输出和工具信息的精简展示
3. 键盘事件处理
主要是实现Enter发送用户prompt,Ctrl+Enter编辑框内换行的功能:
KEYBOARD_HANDLER_JS = '''
function setupKeyboardHandler() {
const observer = new MutationObserver(() => {
const textarea = document.querySelector('textarea');
if (textarea && !textarea.dataset.hermesHandler) {
textarea.dataset.hermesHandler = 'true';
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
if (e.ctrlKey || e.metaKey) {
// Ctrl+Enter: 手动插入换行
const start = this.selectionStart;
const end = this.selectionEnd;
const value = this.value;
this.value = value.substring(0, start) + '\\n' + value.substring(end);
this.selectionStart = this.selectionEnd = start + 1;
this.dispatchEvent(new Event('input', { bubbles: true }));
e.preventDefault();
} else {
// 单纯 Enter: 阻止换行并触发发送
e.preventDefault();
this.dispatchEvent(new CustomEvent('enter-send'));
}
}
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
setupKeyboardHandler();
'''
4. 主页面 UI 布局
@ui.page('/')
def main_page():
global markdown_display, input_field, status_label, card_container
dark = ui.dark_mode()
dark.enable()
ui.add_head_html('<meta name="darkreader-lock">')
ui.add_body_html(f'<script>{KEYBOARD_HANDLER_JS}</script>')
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-4'):
# 标题
ui.label('nanobot Agent 控制台').classes('text-3xl font-bold text-center text-white')
# 输入区域
with ui.card().classes('w-full shadow-lg').props('dark'):
with ui.column().classes('w-full gap-2'):
ui.label('输入消息').classes('text-sm text-gray-300')
input_field = ui.textarea(
placeholder='按 Enter 发送,按 Ctrl+Enter 换行'
).classes('w-full').props('outlined autogrow')
input_field.on('enter-send', lambda: asyncio.create_task(handle_send()))
with ui.row().classes('w-full justify-between items-center'):
ui.label('Ctrl+Enter 换行 | Enter 发送').classes('text-xs text-gray-500')
with ui.row().classes('gap-2'):
ui.button('发送', on_click=lambda: asyncio.create_task(handle_send()),
icon='send').props('color=primary')
ui.button('新建会话', on_click=new_conversation,
icon='refresh').props('outline')
# 输出区域
with ui.card().classes('w-full h-[500px] overflow-y-auto shadow-lg').props('dark') as card_container:
with ui.scroll_area().classes('w-full h-full p-4'):
markdown_display = ui.markdown(
'### 欢迎!\n\n在下方输入消息开始对话。'
).classes('w-full')
status_label = ui.label('就绪').classes('text-sm text-gray-400 mt-2')
注意:card_container 用于后续的上下文管理,确保 UI 更新在正确环境中执行。
5. UI 上下文辅助函数
def safe_update_display(content: str):
"""安全地更新 Markdown 显示内容,确保在正确的 UI 上下文中执行"""
with card_container:
markdown_display.set_content(content)
def safe_notify(message: str, type: str = 'info', **kwargs):
"""安全地显示通知,确保在正确的 UI 上下文中执行"""
with card_container:
ui.notify(message, type=type, **kwargs)
6. 发送消息与调用 Agent
async def handle_send():
'''处理用户发送的消息'''
user_input = input_field.value.strip()
if not user_input:
with card_container:
ui.notify('请输入消息', type='warning')
return
input_field.value = '' # 清空输入框
hook = ChatHook()
hook.user_message = user_input
hook.bind_display(markdown_display) # 绑定引用
# 在正确的上下文中设置初始状态
with card_container:
markdown_display.set_content(
f'**你**: {user_input}\n\n---\n\n*Agent 正在思考...*'
)
# 调用 Agent 进行对话
try:
result = await bot.run(
user_input,
session_key="main-session",
hooks=[hook]
)
# 处理 Agent 输出,优先使用流式输出,否则使用直接输出
final_content = hook.content if hook.content else result.content
# 构建最终显示内容
parts = [f'**你**: {user_input}']
parts.append('\n\n---\n\n')
# 精简显示工具信息
if hook.tool_calls:
tools_str = ' → '.join(sorted(hook.tool_calls))
parts.append(f'🛠️ 调用工具: `{tools_str}`')
parts.append('\n\n---\n\n')
parts.append(f'**Agent:**\n\n{final_content}')
# 在正确的上下文中更新显示
with card_container:
markdown_display.set_content(''.join(parts))
# 简洁的工具使用提示
if hook.tool_calls:
tools_str = ', '.join(sorted(hook.tool_calls))
with card_container:
ui.notify(f'🛠️ 使用工具: {tools_str}', type='info', position='top-right', timeout=2000)
except Exception as e:
with card_container:
markdown_display.set_content(
f'**你**: {user_input}\n\n---\n\n**错误**: {str(e)}'
)
ui.notify(f'错误: {str(e)}', type='negative')
7. 新建会话功能
def new_conversation():
'''新建会话'''
with card_container:
markdown_display.set_content('### 新会话已开始\n\n请输入消息...')
input_field.value = ''
with card_container:
ui.notify('新会话已创建', type='positive')
8. 启动服务
if __name__ == '__main__':
ui.run(
title='nanobot Agent',
host='127.0.0.1',
port=8080,
show=True,
reload=False,
dark=True
)
🚀 使用方法
# 安装依赖
pip install nicegui nanobot
# 启动服务
python nanobot_gui.py
# 访问 http://127.0.0.1:8080
📱 远程访问
如果需要远程访问,只需修改启动参数:
ui.run(
host='0.0.0.0', # 允许外部访问
port=8080,
...
)
然后在同一网络下,用 http://<电脑IP>:8080 访问。配合内网穿透工具(如 frp、ngrok)还可实现公网访问。
四、功能特性
✨ 本文实现的功能
| 功能 | 说明 |
|---|---|
| 流式输出 | Agent 回复实时显示 |
| 工具调用捕获 | 自动记录并显示 Agent 调用的工具 |
| 精简工具展示 | 用 🛠️ 调用工具: xxx 格式直观展示 |
| Markdown 渲染 | 支持代码高亮、格式美观 |
| 键盘优化 | Enter 发送,Ctrl+Enter 换行 |
| 深色模式 | 保护眼睛的深色主题 |
| 新建会话 | 一键清空对话历史 |
| 滚动区域 | 自动滚动到最新内容 |
| 上下文安全 | 通过 card_container 确保 UI 操作线程安全 |
五、总结
📌 本文要点
- nanobot 是一个轻量级的 Python AI Agent 框架,适合学习和二次开发
- NiceGUI 让 Python 开发者也能轻松打造精美的 Web 界面
- 通过 AgentHook 机制,可以实时获取 nanobot 的流式输出和工具调用
card_container上下文管理确保了 UI 更新的线程安全- Web 界面的优势:跨平台、远程访问、无需安装
💡 进一步优化
- 添加消息时间戳
- 支持图片上传
- 保存对话历史到文件
- 集成更多 nanobot Skills
- 打包成独立桌面应用
📚 参考资料
本文为本人原创,首发于掘金。
如果你有任何问题或想法,欢迎在评论区交流!