赞
踩
这是我们构建自定义多模态聊天机器人组件两部分系列的第一部分。在第一部分中,我们将修改 Gradio 聊天机器人组件,使其能够在同一消息中显示文本和媒体文件(视频、音频、图片)。在第二部分中,我们将构建一个自定义的文本框组件,该组件能够向聊天机器人发送多模态消息(文本和媒体文件)。
您可以跟随这篇文章的作者,通过以下 YouTube 视频实现聊天机器人组件!
这里是我们的多模态聊天机器人组件将会是什么样子的预览:
对于这个演示,我们将调整现有的 Gradio Chatbot
组件,以便在同一消息中显示文本和媒体文件。让我们通过模板化 Chatbot
组件源代码来创建一个新的自定义组件目录。
gradio cc create MultimodalChatbot --template Chatbot
我们准备好出发了!
✍️ 提示:确保在 pyproject.toml
文件中修改 Author
键。
在您最喜欢的代码编辑器中打开 multimodalchatbot.py
文件,让我们开始修改组件的后端。
我们将要做的第一件事是创建我们组件的 data_model
。 data_model
是您的 Python 组件将接收并发送给运行 UI 的 JavaScript 客户端的数据格式。您可以在后端指南中了解更多关于 data_model
的信息。
对于我们的组件,每个聊天机器人消息将包含两个键:一个 text
键用于显示文本消息,以及一个可选的媒体文件列表,可以显示在文本下方。
从 gradio.data_classes
导入 FileData
和 GradioModel
类,并修改现有的 ChatbotData
类,使其看起来如下:
- # 定义文件消息类,继承自 GradioModel
- class FileMessage(GradioModel):
- # 文件数据,类型为 FileData
- file: FileData
- # 可选的替代文本,类型为可选的字符串
- alt_text: Optional[str] = None
-
-
- # 定义多模态消息类,继承自 GradioModel
- class MultimodalMessage(GradioModel):
- # 文本消息,类型为可选的字符串
- text: Optional[str] = None
- # 文件消息,类型为 FileMessage 类的列表,是可选的
- files: Optional[List[FileMessage]] = None
-
-
- # 定义聊天机器人数据类,继承自 GradioRootModel
- class ChatbotData(GradioRootModel):
- # 聊天数据,类型为包含多模态消息类的元组的列表
- root: List[Tuple[Optional[MultimodalMessage], Optional[MultimodalMessage]]]
-
-
- # 定义多模态聊天机器人组件,继承自 Component
- class MultimodalChatbot(Component):
- ...
- # 多模态聊天机器人组件的数据模型为聊天机器人数据类
- data_model = ChatbotData

✍️ 提示:`data_model`是使用`Pydantic V2`实现的。请阅读此处的文档。
我们已经完成了最困难的部分!
对于 preprocess
方法,我们将保持简单,将一个 MultimodalMessage
列表传递给使用此组件作为输入的 python 函数。这将允许我们组件的用户使用 .text
和 .files
属性访问聊天机器人数据。这是您可以在实现中修改的设计选择!我们可以像这样返回带有 root
属性的 ChatbotData
的消息列表:
- # 定义预处理方法,输入参数为聊天数据,类型为 ChatbotData 或 None
- def preprocess(
- self,
- payload: ChatbotData | None,
- ) -> List[MultimodalMessage] | None:
- # 如果输入参数为空,则直接返回
- if payload is None:
- return payload
- # 如果存在聊天数据,则返回其中的 root 属性,即所有的聊天消息
- return payload.root
✍️ 提示:了解`preprocess`和`postprocess`方法背后的原理,请参阅关键概念指南
在 postprocess
方法中,我们将强制 python 函数返回的每条消息都是一个 MultimodalMessage
类。我们还将清理 text
字段中的任何缩进,以便它可以在前端正确显示为 markdown。
我们可以保留 postprocess
方法并修改 _postprocess_chat_messages
- # 定义后处理聊天消息的方法,输入参数为多模态消息,类型为 MultimodalMessage 或 字典 或 None
- def _postprocess_chat_messages(
- self, chat_message: MultimodalMessage | dict | None
- ) -> MultimodalMessage | None:
- # 如果聊天消息为空,则直接返回 None
- if chat_message is None:
- return None
- # 如果聊天消息是一个字典,则将其转化为 MultimodalMessage 类型
- if isinstance(chat_message, dict):
- chat_message = MultimodalMessage(**chat_message)
- # 清理聊天消息的文本,使用 cleandoc 方法对文本进行清理,如果文本不存在,则返回一个空字符串
- chat_message.text = inspect.cleandoc(chat_message.text or "")
- # 遍历消息中的所有文件
- for file_ in chat_message.files:
- # 使用 get_mimetype 方法获取文件的 MIME 类型,并设置给文件的 mime_type 属性
- file_.file.mime_type = client_utils.get_mimetype(file_.file.path)
- # 返回处理过后的聊天消息
- return chat_message

在我们结束后端代码之前,让我们修改 example_value
和 example_payload
方法,以返回 ChatbotData
的有效字典表示形式:
- # 定义example_value方法,返回一个任意类型的值
- def example_value(self) -> Any:
- # 返回一个示例聊天信息(仅包含文本“Hello!”)的列表
- return [[{"text": "Hello!", "files": []}, None]]
-
-
- # 定义example_payload方法,返回一个任意类型的值
- def example_payload(self) -> Any:
- # 返回一个示例聊天信息(仅包含文本“Hello!”)的列表
- return [[{"text": "Hello!", "files": []}, None]]
恭喜 - 后端已完成!
Chatbot
组件的前端分为两部分 - Index.svelte
文件和 shared/Chatbot.svelte
文件。 Index.svelte
文件对从服务器接收的数据进行一些处理,然后将对话的渲染委托给 shared/Chatbot.svelte
文件。首先我们将修改 Index.svelte
文件,以对后端将返回的新数据类型进行处理。
让我们开始将我们的自定义类型从我们的 Python data_model
移植到 TypeScript。打开 frontend/shared/utils.ts
,并在文件顶部添加以下类型定义:
- // 定义了一个名为FileMessage的类型,
- // 其中包含一个必需的file属性,类型为FileData,
- // 和一个可选的alt_text属性,类型为字符串(string)。
- export type FileMessage = {
- file: FileData;
- alt_text?: string;
- };
-
-
- // 定义了一个名为MultimodalMessage的类型,
- // 其中包含一个必需的text属性,类型为字符串(string),
- // 和一个可选的files属性,类型为FileMessage类型的数组。
- export type MultimodalMessage = {
- text: string;
- files?: FileMessage[];
- }

现在让我们在 Index.svelte
中导入它们,并修改 value
和 _value
的类型注释。
- // 从 "shared/utils" 导入 FileMessage 和 MultimodalMessage 类型
- import type { FileMessage, MultimodalMessage } from "./shared/utils";
-
-
- // 定义一个名为 value 的变量,
- // 其类型为一个数组,数组中的每个元素是一个长度为2的元组(Tuple),
- // 且元组的两个元素都是 MultimodalMessage 或 null。
- export let value: [
- MultimodalMessage | null,
- MultimodalMessage | null
- ][] = [];
-
-
- // 定义一个名为 _value 的变量,其类型与 value 变量相同
- // 但该 _value 变量并未被赋初始值
- let _value: [
- MultimodalMessage | null,
- MultimodalMessage | null
- ][];

我们需要规范化每条消息,以确保每个文件都有一个正确的 URL 来获取其内容。我们还需要在 text
键中格式化任何嵌入的文件链接。让我们添加一个 process_message
实用程序函数,并在 value
发生变化时应用它。
- // 定义一个函数process_message,
- // 接受一个MultimodalMessage或null作为参数,
- // 返回一个MultimodalMessage或null
- function process_message(msg: MultimodalMessage | null): MultimodalMessage | null {
- // 如果传入的消息为null,则直接返回null
- if (msg === null) {
- return msg;
- }
- // 将消息文本重定向至某个URL
- // (具体实现取决于redirect_src_url函数,
- // 在此未给出)
- msg.text = redirect_src_url(msg.text);
- // 对消息中的files进行标准化处理
- // (具体实现取决于normalize_messages函数,
- // 在此未给出)
- msg.files = msg.files.map(normalize_messages);
- // 返回处理后的消息对象
- return msg;
- }
-
-
- // 创建一个响应式变量_value,
- // 它的值取决于value变量
- // 如果value为空,则_value也为空数组;
- // 否则,_value的值为value数组中的每一对
- // [user_msg, bot_msg]经过process_message函数处理后的结果。
- $: _value = value
- ? value.map(([user_msg, bot_msg]) => [
- process_message(user_msg),
- process_message(bot_msg)
- ])
- : [];

_value
是一个响应式变量,它会随着value
的变化而相应变化。让我们开始类似于 Index.svelte
文件,首先修改类型注释。在 <script>
部分的顶部导入 Mulimodal
消息,并使用它来为 value
和 old_value
变量进行类型定义。
- // 从 "utils" 导入 MultimodalMessage 类型
- import type { MultimodalMessage } from "./utils";
-
-
- // 定义一个名为value的变量,
- // 它的类型可以是一个数组(其中每个元素为一个元组,元组的长度为2,元素类型可以是MultimodalMessage或null),
- // 也可以是null。
- export let value:
- | [
- MultimodalMessage | null,
- MultimodalMessage | null
- ][]
- | null;
-
-
- // 定义一个名为old_value的变量,它的类型与value变量一样,初始值为null。
- let old_value:
- | [
- MultimodalMessage | null,
- MultimodalMessage | null
- ][]
- | null = null;

我们还需要修改 handle_select
和 handle_like
函数:
- // 定义一个名为handle_select的函数,这个函数接收三个参数:
- // i 和 j 分别为两个数字,表示操作的索引位置,
- // message 是一个 MultimodalMessage 对象或者 null,
- // 这个函数不返回任何值。
- // 函数的功能是,当选中一个消息时,触发一个名为 "select" 的事件,并将相关信息作为事件的参数。
- function handle_select(
- i: number,
- j: number,
- message: MultimodalMessage | null
- ): void {
- dispatch("select", { // 触发 select 事件
- index: [i, j], // 将 i 和 j 作为索引位置信息
- value: message // 将 message 作为选中的消息传入
- });
- }
-
-
- // 定义一个名为handle_like的函数,这个函数接收四个参数:
- // i 和 j 分别为两个数字,表示操作的索引位置,
- // message 是一个 MultimodalMessage 对象或者 null,
- // liked 是一个布尔值,表示是否喜欢。
- // 这个函数不返回任何值。
- // 函数的功能是,当对一个消息进行 "喜欢/不喜欢" 操作时,触发一个名为 "like" 的事件,并将相关信息作为事件的参数。
- function handle_like(
- i: number,
- j: number,
- message: MultimodalMessage | null,
- liked: boolean
- ): void {
- dispatch("like", { // 触发 like 事件
- index: [i, j], // 将 i 和 j 作为索引位置信息
- value: message, // 将 message 作为操作的消息传入
- liked: liked // 将 liked 作为操作的类型传入 (喜欢或不喜欢)
- });
- }

现在开始有趣的部分,实际上在同一消息中渲染文本和文件!
你应该会看到类似以下的代码,它根据消息的类型确定是显示一个文件还是一个 markdown 消息:
- // 如果消息类型为字符串
- {#if typeof message === "string"}
- // 使用Markdown组件来渲染字符串类型的消息
- <Markdown
- {message} // 传入消息文本
- {latex_delimiters} // LaTeX分隔符,用于渲染LaTeX内容
- {sanitize_html} // 清理HTML,防止跨站脚本攻击
- {render_markdown} // 渲染Markdown,将Markdown格式的文本转换为 HTML
- {line_breaks} // 处理换行
- on:load={scroll} // 当内容加载完成时,执行滚动操作,以便查看新加载的内容
- />
-
-
- // 否则如果消息不为null,且消息中的文件类型包含 "audio"
- {:else if message !== null && message.file?.mime_type?.includes("audio")}
- // 使用HTML的audio元素来渲染音频类型的消息
- <audio
- data-testid="chatbot-audio" // 设置数据测试id为 "chatbot-audio",便于进行单元测试
- controls // 显示媒体播放器的默认控制器
- preload="metadata" // 在页面加载时加载音频的元数据,但不加载音频文件本身
- ...

我们将修改这段代码,使其总是显示文本消息,然后遍历文件并显示所有存在的文件:
- // 使用Markdown元素来渲染消息文本
- <Markdown
- message={message.text}
- {latex_delimiters}
- {sanitize_html}
- {render_markdown}
- {line_breaks}
- on:load={scroll}
- />
- // 循环遍历message中的文件数组
- {#each message.files as file, k}
- // 如果文件类型包含 "audio"
- {#if file !== null && file.file.mime_type?.includes("audio")}
- // 渲染audio元素来播放音频文件
- <audio
- data-testid="chatbot-audio"
- controls
- preload="metadata"
- src={file.file?.url}
- title={file.alt_text}
- on:play
- on:pause
- on:ended
- />
- // 如果文件类型包含 "video"
- {:else if message !== null && file.file?.mime_type?.includes("video")}
- // 渲染video元素来播放视频文件
- <video
- data-testid="chatbot-video"
- controls
- src={file.file?.url}
- title={file.alt_text}
- preload="auto"
- on:play
- on:pause
- on:ended
- >
- <track kind="captions" />
- </video>
- // 如果文件类型包含 "image"
- {:else if message !== null && file.file?.mime_type?.includes("image")}
- // 渲染img元素来显示图片文件
- <img
- data-testid="chatbot-image"
- src={file.file?.url}
- alt={file.alt_text}
- />
- // 如果文件不是音频、视频或图片文件
- {:else if message !== null && file.file?.url !== null}
- // 渲染a元素来提供文件下载链接
- <a
- data-testid="chatbot-file"
- href={file.file?.url}
- target="_blank"
- download={window.__is_colab__
- ? null
- : file.file?.orig_name || file.file?.path}
- >
- {file.file?.orig_name || file.file?.path}
- </a>
- // 如果消息仍在发送中
- {:else if pending_message && j === 1}
- // 渲染Pending元素表示消息仍在发送中
- <Pending {layout} />
- {/if}
- {/each}

我们做到了!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。