当前位置:   article > 正文

【Gradio】构建自定义多模态聊天机器人

gradio 多模态聊天系统

55a77da8d3b7cae7ed98c7fa8c6f539e.png

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

您可以跟随这篇文章的作者,通过以下 YouTube 视频实现聊天机器人组件!

这里是我们的多模态聊天机器人组件将会是什么样子的预览:

ab509c942f7d8496538aea0f2d2f01fa.png

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

对于这个演示,我们将调整现有的 Gradio Chatbot 组件,以便在同一消息中显示文本和媒体文件。让我们通过模板化 Chatbot 组件源代码来创建一个新的自定义组件目录。

gradio cc create MultimodalChatbot --template Chatbot

我们准备好出发了!

✍️ 提示:确保在 pyproject.toml 文件中修改 Author 键。

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

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

我们将要做的第一件事是创建我们组件的 data_model 。 data_model 是您的 Python 组件将接收并发送给运行 UI 的 JavaScript 客户端的数据格式。您可以在后端指南中了解更多关于 data_model 的信息。

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

从 gradio.data_classes 导入 FileData 和 GradioModel 类,并修改现有的 ChatbotData 类,使其看起来如下:

  1. # 定义文件消息类,继承自 GradioModel
  2. class FileMessage(GradioModel):
  3. # 文件数据,类型为 FileData
  4. file: FileData
  5. # 可选的替代文本,类型为可选的字符串
  6. alt_text: Optional[str] = None
  7. # 定义多模态消息类,继承自 GradioModel
  8. class MultimodalMessage(GradioModel):
  9. # 文本消息,类型为可选的字符串
  10. text: Optional[str] = None
  11. # 文件消息,类型为 FileMessage 类的列表,是可选的
  12. files: Optional[List[FileMessage]] = None
  13. # 定义聊天机器人数据类,继承自 GradioRootModel
  14. class ChatbotData(GradioRootModel):
  15. # 聊天数据,类型为包含多模态消息类的元组的列表
  16. root: List[Tuple[Optional[MultimodalMessage], Optional[MultimodalMessage]]]
  17. # 定义多模态聊天机器人组件,继承自 Component
  18. class MultimodalChatbot(Component):
  19. ...
  20. # 多模态聊天机器人组件的数据模型为聊天机器人数据类
  21. data_model = ChatbotData

✍️ 提示:`data_model`是使用`Pydantic V2`实现的。请阅读此处的文档。

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

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

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

  1. # 定义预处理方法,输入参数为聊天数据,类型为 ChatbotData 或 None
  2. def preprocess(
  3. self,
  4. payload: ChatbotData | None,
  5. ) -> List[MultimodalMessage] | None:
  6. # 如果输入参数为空,则直接返回
  7. if payload is None:
  8. return payload
  9. # 如果存在聊天数据,则返回其中的 root 属性,即所有的聊天消息
  10. return payload.root

✍️ 提示:了解`preprocess`和`postprocess`方法背后的原理,请参阅关键概念指南

在 postprocess 方法中,我们将强制 python 函数返回的每条消息都是一个 MultimodalMessage 类。我们还将清理 text 字段中的任何缩进,以便它可以在前端正确显示为 markdown。

我们可以保留 postprocess 方法并修改 _postprocess_chat_messages

  1. # 定义后处理聊天消息的方法,输入参数为多模态消息,类型为 MultimodalMessage 或 字典 或 None
  2. def _postprocess_chat_messages(
  3. self, chat_message: MultimodalMessage | dict | None
  4. ) -> MultimodalMessage | None:
  5. # 如果聊天消息为空,则直接返回 None
  6. if chat_message is None:
  7. return None
  8. # 如果聊天消息是一个字典,则将其转化为 MultimodalMessage 类型
  9. if isinstance(chat_message, dict):
  10. chat_message = MultimodalMessage(**chat_message)
  11. # 清理聊天消息的文本,使用 cleandoc 方法对文本进行清理,如果文本不存在,则返回一个空字符串
  12. chat_message.text = inspect.cleandoc(chat_message.text or "")
  13. # 遍历消息中的所有文件
  14. for file_ in chat_message.files:
  15. # 使用 get_mimetype 方法获取文件的 MIME 类型,并设置给文件的 mime_type 属性
  16. file_.file.mime_type = client_utils.get_mimetype(file_.file.path)
  17. # 返回处理过后的聊天消息
  18. return chat_message

在我们结束后端代码之前,让我们修改 example_value 和 example_payload 方法,以返回 ChatbotData 的有效字典表示形式:

  1. # 定义example_value方法,返回一个任意类型的值
  2. def example_value(self) -> Any:
  3. # 返回一个示例聊天信息(仅包含文本“Hello!”)的列表
  4. return [[{"text": "Hello!", "files": []}, None]]
  5. # 定义example_payload方法,返回一个任意类型的值
  6. def example_payload(self) -> Any:
  7. # 返回一个示例聊天信息(仅包含文本“Hello!”)的列表
  8. return [[{"text": "Hello!", "files": []}, None]]

恭喜 - 后端已完成!

第 3a 部分 - Index.svelte 文件 

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

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

  1. // 定义了一个名为FileMessage的类型,
  2. // 其中包含一个必需的file属性,类型为FileData,
  3. // 和一个可选的alt_text属性,类型为字符串(string)。
  4. export type FileMessage = {
  5. file: FileData;
  6. alt_text?: string;
  7. };
  8. // 定义了一个名为MultimodalMessage的类型,
  9. // 其中包含一个必需的text属性,类型为字符串(string),
  10. // 和一个可选的files属性,类型为FileMessage类型的数组。
  11. export type MultimodalMessage = {
  12. text: string;
  13. files?: FileMessage[];
  14. }

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

  1. // 从 "shared/utils" 导入 FileMessage 和 MultimodalMessage 类型
  2. import type { FileMessage, MultimodalMessage } from "./shared/utils";
  3. // 定义一个名为 value 的变量,
  4. // 其类型为一个数组,数组中的每个元素是一个长度为2的元组(Tuple),
  5. // 且元组的两个元素都是 MultimodalMessage 或 null。
  6. export let value: [
  7. MultimodalMessage | null,
  8. MultimodalMessage | null
  9. ][] = [];
  10. // 定义一个名为 _value 的变量,其类型与 value 变量相同
  11. // 但该 _value 变量并未被赋初始值
  12. let _value: [
  13. MultimodalMessage | null,
  14. MultimodalMessage | null
  15. ][];

我们需要规范化每条消息,以确保每个文件都有一个正确的 URL 来获取其内容。我们还需要在 text 键中格式化任何嵌入的文件链接。让我们添加一个 process_message 实用程序函数,并在 value 发生变化时应用它。

  1. // 定义一个函数process_message,
  2. // 接受一个MultimodalMessage或null作为参数,
  3. // 返回一个MultimodalMessage或null
  4. function process_message(msg: MultimodalMessage | null): MultimodalMessage | null {
  5. // 如果传入的消息为null,则直接返回null
  6. if (msg === null) {
  7. return msg;
  8. }
  9. // 将消息文本重定向至某个URL
  10. // (具体实现取决于redirect_src_url函数,
  11. // 在此未给出)
  12. msg.text = redirect_src_url(msg.text);
  13. // 对消息中的files进行标准化处理
  14. // (具体实现取决于normalize_messages函数,
  15. // 在此未给出)
  16. msg.files = msg.files.map(normalize_messages);
  17. // 返回处理后的消息对象
  18. return msg;
  19. }
  20. // 创建一个响应式变量_value,
  21. // 它的值取决于value变量
  22. // 如果value为空,则_value也为空数组;
  23. // 否则,_value的值为value数组中的每一对
  24. // [user_msg, bot_msg]经过process_message函数处理后的结果。
  25. $: _value = value
  26. ? value.map(([user_msg, bot_msg]) => [
  27. process_message(user_msg),
  28. process_message(bot_msg)
  29. ])
  30. : [];

以上代码的功能是接收输入消息(用户或bot的消息),并进行相应的处理(包括重定向文本的URL和标准化文件消息),然后返回处理后的消息。其中_value是一个响应式变量,它会随着value的变化而相应变化。

第 3b 部分 - Chatbot.svelte 文件 

让我们开始类似于 Index.svelte 文件,首先修改类型注释。在 <script> 部分的顶部导入 Mulimodal 消息,并使用它来为 value 和 old_value 变量进行类型定义。

  1. // 从 "utils" 导入 MultimodalMessage 类型
  2. import type { MultimodalMessage } from "./utils";
  3. // 定义一个名为value的变量,
  4. // 它的类型可以是一个数组(其中每个元素为一个元组,元组的长度为2,元素类型可以是MultimodalMessage或null),
  5. // 也可以是null。
  6. export let value:
  7. | [
  8. MultimodalMessage | null,
  9. MultimodalMessage | null
  10. ][]
  11. | null;
  12. // 定义一个名为old_value的变量,它的类型与value变量一样,初始值为null。
  13. let old_value:
  14. | [
  15. MultimodalMessage | null,
  16. MultimodalMessage | null
  17. ][]
  18. | null = null;

我们还需要修改 handle_select 和 handle_like 函数:

  1. // 定义一个名为handle_select的函数,这个函数接收三个参数:
  2. // i 和 j 分别为两个数字,表示操作的索引位置,
  3. // message 是一个 MultimodalMessage 对象或者 null,
  4. // 这个函数不返回任何值。
  5. // 函数的功能是,当选中一个消息时,触发一个名为 "select" 的事件,并将相关信息作为事件的参数。
  6. function handle_select(
  7. i: number,
  8. j: number,
  9. message: MultimodalMessage | null
  10. ): void {
  11. dispatch("select", { // 触发 select 事件
  12. index: [i, j], // 将 i 和 j 作为索引位置信息
  13. value: message // 将 message 作为选中的消息传入
  14. });
  15. }
  16. // 定义一个名为handle_like的函数,这个函数接收四个参数:
  17. // i 和 j 分别为两个数字,表示操作的索引位置,
  18. // message 是一个 MultimodalMessage 对象或者 null,
  19. // liked 是一个布尔值,表示是否喜欢。
  20. // 这个函数不返回任何值。
  21. // 函数的功能是,当对一个消息进行 "喜欢/不喜欢" 操作时,触发一个名为 "like" 的事件,并将相关信息作为事件的参数。
  22. function handle_like(
  23. i: number,
  24. j: number,
  25. message: MultimodalMessage | null,
  26. liked: boolean
  27. ): void {
  28. dispatch("like", { // 触发 like 事件
  29. index: [i, j], // 将 i 和 j 作为索引位置信息
  30. value: message, // 将 message 作为操作的消息传入
  31. liked: liked // 将 liked 作为操作的类型传入 (喜欢或不喜欢)
  32. });
  33. }

现在开始有趣的部分,实际上在同一消息中渲染文本和文件!

你应该会看到类似以下的代码,它根据消息的类型确定是显示一个文件还是一个 markdown 消息:

  1. // 如果消息类型为字符串
  2. {#if typeof message === "string"}
  3. // 使用Markdown组件来渲染字符串类型的消息
  4. <Markdown
  5. {message} // 传入消息文本
  6. {latex_delimiters} // LaTeX分隔符,用于渲染LaTeX内容
  7. {sanitize_html} // 清理HTML,防止跨站脚本攻击
  8. {render_markdown} // 渲染Markdown,将Markdown格式的文本转换为 HTML
  9. {line_breaks} // 处理换行
  10. on:load={scroll} // 当内容加载完成时,执行滚动操作,以便查看新加载的内容
  11. />
  12. // 否则如果消息不为null,且消息中的文件类型包含 "audio"
  13. {:else if message !== null && message.file?.mime_type?.includes("audio")}
  14. // 使用HTML的audio元素来渲染音频类型的消息
  15. <audio
  16. data-testid="chatbot-audio" // 设置数据测试id为 "chatbot-audio",便于进行单元测试
  17. controls // 显示媒体播放器的默认控制器
  18. preload="metadata" // 在页面加载时加载音频的元数据,但不加载音频文件本身
  19. ...

我们将修改这段代码,使其总是显示文本消息,然后遍历文件并显示所有存在的文件:

  1. // 使用Markdown元素来渲染消息文本
  2. <Markdown
  3. message={message.text}
  4. {latex_delimiters}
  5. {sanitize_html}
  6. {render_markdown}
  7. {line_breaks}
  8. on:load={scroll}
  9. />
  10. // 循环遍历message中的文件数组
  11. {#each message.files as file, k}
  12. // 如果文件类型包含 "audio"
  13. {#if file !== null && file.file.mime_type?.includes("audio")}
  14. // 渲染audio元素来播放音频文件
  15. <audio
  16. data-testid="chatbot-audio"
  17. controls
  18. preload="metadata"
  19. src={file.file?.url}
  20. title={file.alt_text}
  21. on:play
  22. on:pause
  23. on:ended
  24. />
  25. // 如果文件类型包含 "video"
  26. {:else if message !== null && file.file?.mime_type?.includes("video")}
  27. // 渲染video元素来播放视频文件
  28. <video
  29. data-testid="chatbot-video"
  30. controls
  31. src={file.file?.url}
  32. title={file.alt_text}
  33. preload="auto"
  34. on:play
  35. on:pause
  36. on:ended
  37. >
  38. <track kind="captions" />
  39. </video>
  40. // 如果文件类型包含 "image"
  41. {:else if message !== null && file.file?.mime_type?.includes("image")}
  42. // 渲染img元素来显示图片文件
  43. <img
  44. data-testid="chatbot-image"
  45. src={file.file?.url}
  46. alt={file.alt_text}
  47. />
  48. // 如果文件不是音频、视频或图片文件
  49. {:else if message !== null && file.file?.url !== null}
  50. // 渲染a元素来提供文件下载链接
  51. <a
  52. data-testid="chatbot-file"
  53. href={file.file?.url}
  54. target="_blank"
  55. download={window.__is_colab__
  56. ? null
  57. : file.file?.orig_name || file.file?.path}
  58. >
  59. {file.file?.orig_name || file.file?.path}
  60. </a>
  61. // 如果消息仍在发送中
  62. {:else if pending_message && j === 1}
  63. // 渲染Pending元素表示消息仍在发送中
  64. <Pending {layout} />
  65. {/if}
  66. {/each}

我们做到了!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/981012?site=
推荐阅读
相关标签