Gradio 代理与 MCP 黑客马拉松

获奖者
Gradio logo
  1. 自定义组件
  2. 多模态聊天机器人 第一部分

构建自定义多模态聊天机器人 - 第一部分

这是构建自定义多模态聊天机器人的两部分系列文章中的第一篇。在第一部分中,我们将修改 Gradio 聊天机器人组件,使其能在同一消息中显示文本和媒体文件(视频、音频、图像)。在第二部分中,我们将构建一个自定义文本框组件,使其能够向聊天机器人发送多模态消息(文本和媒体文件)。

您可以观看以下 YouTube 视频,跟随本文作者一起实现聊天机器人组件!

以下是我们多模态聊天机器人组件的预览效果

MultiModal Chatbot

第一部分 - 创建我们的项目

在本演示中,我们将调整现有的 Gradio `Chatbot` 组件,使其能在同一消息中显示文本和媒体文件。让我们基于 `Chatbot` 组件的源代码创建新的自定义组件目录。

gradio cc create MultimodalChatbot --template Chatbot

我们准备好了!

提示: 请务必修改 `pyproject.toml` 文件中的 `Author` 键。

第二部分a - 后端数据模型

在您喜欢的代码编辑器中打开 `multimodalchatbot.py` 文件,让我们开始修改组件的后端。

我们要做的第一件事是创建组件的 `data_model`。`data_model` 是您的 Python 组件将接收并发送给运行 UI 的 JavaScript 客户端的数据格式。您可以在[后端指南](./backend)中阅读有关 `data_model` 的更多信息。

对于我们的组件,每个聊天机器人消息将包含两个键:一个用于显示文本消息的 `text` 键,以及一个可在文本下方显示的媒体文件可选列表。

从 `gradio.data_classes` 导入 `FileData` 和 `GradioModel` 类,并将现有的 `ChatbotData` 类修改为如下所示:

class FileMessage(GradioModel):
    file: FileData
    alt_text: Optional[str] = None


class MultimodalMessage(GradioModel):
    text: Optional[str] = None
    files: Optional[List[FileMessage]] = None


class ChatbotData(GradioRootModel):
    root: List[Tuple[Optional[MultimodalMessage], Optional[MultimodalMessage]]]


class MultimodalChatbot(Component):
    ...
    data_model = ChatbotData

提示: `data_model` 是使用 `Pydantic V2` 实现的。请在此处阅读文档[https://docs.pydantic.org.cn/latest/](https://docs.pydantic.org.cn/latest/)。

我们已经完成了最难的部分!

第二部分b - 预处理和后处理方法

对于 `preprocess` 方法,我们将保持其简单性,并向将此组件用作输入的 Python 函数传递 `MultimodalMessage` 列表。这将允许我们组件的用户通过 `.text` 和 `.files` 属性访问聊天机器人数据。这是一个您可以在实现中修改的设计选择!我们可以使用 `ChatbotData` 的 `root` 属性返回消息列表,如下所示:

def preprocess(
    self,
    payload: ChatbotData | None,
) -> List[MultimodalMessage] | None:
    if payload is None:
        return payload
    return payload.root

提示: 在[关键概念指南](./key-component-concepts)中了解 `preprocess` 和 `postprocess` 方法背后的原理。

在 `postprocess` 方法中,我们将强制将 Python 函数返回的每条消息转换为 `MultimodalMessage` 类。我们还将清理 `text` 字段中的任何缩进,以便在前端正确显示为 Markdown 格式。

我们可以保留 `postprocess` 方法不变,并修改 `_postprocess_chat_messages`。

def _postprocess_chat_messages(
    self, chat_message: MultimodalMessage | dict | None
) -> MultimodalMessage | None:
    if chat_message is None:
        return None
    if isinstance(chat_message, dict):
        chat_message = MultimodalMessage(**chat_message)
    chat_message.text = inspect.cleandoc(chat_message.text or "")
    for file_ in chat_message.files:
        file_.file.mime_type = client_utils.get_mimetype(file_.file.path)
    return chat_message

在完成后端代码之前,让我们修改 `example_value` 和 `example_payload` 方法,以返回 `ChatbotData` 的有效字典表示。

def example_value(self) -> Any:
    return [[{"text": "Hello!", "files": []}, None]]

def example_payload(self) -> Any:
    return [[{"text": "Hello!", "files": []}, None]]

恭喜 - 后端已完成!

第三部分a - Index.svelte 文件

`Chatbot` 组件的前端分为两部分 - `Index.svelte` 文件和 `shared/Chatbot.svelte` 文件。`Index.svelte` 文件对从服务器接收到的数据应用一些处理,然后将对话的渲染委托给 `shared/Chatbot.svelte` 文件。首先,我们将修改 `Index.svelte` 文件,以便对后端将返回的新数据类型进行处理。

让我们首先将自定义类型从 Python `data_model` 移植到 TypeScript。打开 `frontend/shared/utils.ts` 并在文件顶部添加以下类型定义:

export type FileMessage = {
	file: FileData;
	alt_text?: string;
};


export type MultimodalMessage = {
	text: string;
	files?: FileMessage[];
}

现在,让我们在 `Index.svelte` 中导入它们,并修改 `value` 和 `_value` 的类型注解。

import type { FileMessage, MultimodalMessage } from "./shared/utils";

export let value: [
    MultimodalMessage | null,
    MultimodalMessage | null
][] = [];

let _value: [
    MultimodalMessage | null,
    MultimodalMessage | null
][];

我们需要标准化每条消息,确保每个文件都有一个正确的 URL 来获取其内容。我们还需要格式化 `text` 键中任何嵌入的文件链接。让我们添加一个 `process_message` 工具函数,并在 `value` 改变时应用它。

function process_message(msg: MultimodalMessage | null): MultimodalMessage | null {
    if (msg === null) {
        return msg;
    }
    msg.text = redirect_src_url(msg.text);
    msg.files = msg.files.map(normalize_messages);
    return msg;
}

$: _value = value
    ? value.map(([user_msg, bot_msg]) => [
            process_message(user_msg),
            process_message(bot_msg)
        ])
    : [];

第三部分b - Chatbot.svelte 文件

让我们像处理 `Index.svelte` 文件一样,首先修改类型注解。在 `