用 NiceGUI 为 nanobot 打造 Web GUI:让 AI 对话更便捷

0 阅读7分钟

用 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)虽然高效,但复制粘贴对话内容总是不够方便。如果能有一个图形界面,可以远程访问,那体验会好很多。

今天我来分享如何用 NiceGUInanobot 打造一个 Web GUI 界面。界面如下图所示:

image.png

一、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

🎯 设计目标

  1. 便捷交互:支持复制粘贴,告别繁琐的 CLI 操作
  2. 远程访问:通过 Web 界面,随时随地访问 AI 对话
  3. 美观界面:利用 NiceGUI 的组件,打造现代化体验
  4. 功能完整:保留 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 操作线程安全

五、总结

📌 本文要点

  1. nanobot 是一个轻量级的 Python AI Agent 框架,适合学习和二次开发
  2. NiceGUI 让 Python 开发者也能轻松打造精美的 Web 界面
  3. 通过 AgentHook 机制,可以实时获取 nanobot 的流式输出和工具调用
  4. card_container 上下文管理确保了 UI 更新的线程安全
  5. Web 界面的优势:跨平台、远程访问、无需安装

💡 进一步优化

  • 添加消息时间戳
  • 支持图片上传
  • 保存对话历史到文件
  • 集成更多 nanobot Skills
  • 打包成独立桌面应用

📚 参考资料


本文为本人原创,首发于掘金。
如果你有任何问题或想法,欢迎在评论区交流!