diff --git a/.changeset/rich-ducks-grow.md b/.changeset/rich-ducks-grow.md new file mode 100644 index 0000000000000..993163ee6dec4 --- /dev/null +++ b/.changeset/rich-ducks-grow.md @@ -0,0 +1,7 @@ +--- +"@gradio/chatbot": minor +"gradio": minor +"website": minor +--- + +feat:Add support for thinking LLMs directly in `gr.ChatInterface` diff --git a/demo/chatinterface_nested_thoughts/run.ipynb b/demo/chatinterface_nested_thoughts/run.ipynb new file mode 100644 index 0000000000000..cf756f381fe51 --- /dev/null +++ b/demo/chatinterface_nested_thoughts/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatinterface_nested_thoughts"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from gradio import ChatMessage\n", "import time\n", "\n", "sleep_time = 0.1\n", "long_sleep_time = 1\n", "\n", "def generate_response(message, history):\n", " start_time = time.time()\n", " responses = [\n", " ChatMessage(\n", " content=\"In order to find the current weather in San Francisco, I will need to use my weather tool.\",\n", " )\n", " ]\n", " yield responses\n", " time.sleep(sleep_time)\n", "\n", " main_thought = ChatMessage(\n", " content=\"\",\n", " metadata={\"title\": \"Using Weather Tool\", \"id\": 1, \"status\": \"pending\"},\n", " )\n", "\n", " responses.append(main_thought)\n", "\n", " yield responses\n", " time.sleep(long_sleep_time)\n", " responses[-1].content = \"Will check: weather.com and sunny.org\"\n", " yield responses\n", " time.sleep(sleep_time)\n", " responses.append(\n", " ChatMessage(\n", " content=\"Received weather from weather.com.\",\n", " metadata={\"title\": \"Checking weather.com\", \"parent_id\": 1, \"id\": 2, \"duration\": 0.05},\n", " )\n", " )\n", " yield responses\n", "\n", " sunny_start_time = time.time()\n", " time.sleep(sleep_time)\n", " sunny_thought = ChatMessage(\n", " content=\"API Error when connecting to sunny.org \ud83d\udca5\",\n", " metadata={\"title\": \"Checking sunny.org\", \"parent_id\": 1, \"id\": 3, \"status\": \"pending\"},\n", " )\n", "\n", " responses.append(sunny_thought)\n", " yield responses\n", "\n", " time.sleep(sleep_time)\n", " responses.append(\n", " ChatMessage(\n", " content=\"Failed again\",\n", " metadata={\"title\": \"I will try again\", \"id\": 4, \"parent_id\": 3, \"duration\": 0.1},\n", "\n", " )\n", " )\n", " sunny_thought.metadata[\"status\"] = \"done\"\n", " sunny_thought.metadata[\"duration\"] = time.time() - sunny_start_time\n", "\n", " main_thought.metadata[\"status\"] = \"done\"\n", " main_thought.metadata[\"duration\"] = time.time() - start_time\n", "\n", " yield responses\n", "\n", " time.sleep(long_sleep_time)\n", "\n", " responses.append(\n", " ChatMessage(\n", " content=\"Based on the data only from weather.com, the current weather in San Francisco is 60 degrees and sunny.\",\n", " )\n", " )\n", " yield responses\n", "\n", "demo = gr.ChatInterface(\n", " generate_response,\n", " type=\"messages\",\n", " title=\"Nested Thoughts Chat Interface\",\n", " examples=[\"What is the weather in San Francisco right now?\"]\n", ")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/chatinterface_nested_thoughts/run.py b/demo/chatinterface_nested_thoughts/run.py new file mode 100644 index 0000000000000..7322fc0736f91 --- /dev/null +++ b/demo/chatinterface_nested_thoughts/run.py @@ -0,0 +1,81 @@ +import gradio as gr +from gradio import ChatMessage +import time + +sleep_time = 0.1 +long_sleep_time = 1 + +def generate_response(message, history): + start_time = time.time() + responses = [ + ChatMessage( + content="In order to find the current weather in San Francisco, I will need to use my weather tool.", + ) + ] + yield responses + time.sleep(sleep_time) + + main_thought = ChatMessage( + content="", + metadata={"title": "Using Weather Tool", "id": 1, "status": "pending"}, + ) + + responses.append(main_thought) + + yield responses + time.sleep(long_sleep_time) + responses[-1].content = "Will check: weather.com and sunny.org" + yield responses + time.sleep(sleep_time) + responses.append( + ChatMessage( + content="Received weather from weather.com.", + metadata={"title": "Checking weather.com", "parent_id": 1, "id": 2, "duration": 0.05}, + ) + ) + yield responses + + sunny_start_time = time.time() + time.sleep(sleep_time) + sunny_thought = ChatMessage( + content="API Error when connecting to sunny.org 💥", + metadata={"title": "Checking sunny.org", "parent_id": 1, "id": 3, "status": "pending"}, + ) + + responses.append(sunny_thought) + yield responses + + time.sleep(sleep_time) + responses.append( + ChatMessage( + content="Failed again", + metadata={"title": "I will try again", "id": 4, "parent_id": 3, "duration": 0.1}, + + ) + ) + sunny_thought.metadata["status"] = "done" + sunny_thought.metadata["duration"] = time.time() - sunny_start_time + + main_thought.metadata["status"] = "done" + main_thought.metadata["duration"] = time.time() - start_time + + yield responses + + time.sleep(long_sleep_time) + + responses.append( + ChatMessage( + content="Based on the data only from weather.com, the current weather in San Francisco is 60 degrees and sunny.", + ) + ) + yield responses + +demo = gr.ChatInterface( + generate_response, + type="messages", + title="Nested Thoughts Chat Interface", + examples=["What is the weather in San Francisco right now?"] +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_options/run.ipynb b/demo/chatinterface_options/run.ipynb index a76a63b090d13..8be849ed28023 100644 --- a/demo/chatinterface_options/run.ipynb +++ b/demo/chatinterface_options/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatinterface_options"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import random\n", "\n", "example_code = \"\"\"\n", "Here's an example Python lambda function:\n", "\n", "lambda x: x + {}\n", "\n", "Is this correct?\n", "\"\"\"\n", "\n", "def chat(message, history):\n", " if message == \"Yes, that's correct.\":\n", " return \"Great!\"\n", " else:\n", " return {\n", " \"role\": \"assistant\",\n", " \"content\": example_code.format(random.randint(1, 100)),\n", " \"options\": [\n", " {\"value\": \"Yes, that's correct.\", \"label\": \"Yes\"},\n", " {\"value\": \"No\"}\n", " ]\n", " }\n", "\n", "demo = gr.ChatInterface(\n", " chat,\n", " type=\"messages\",\n", " examples=[\"Write an example Python lambda function.\"]\n", ")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatinterface_options"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import random\n", "\n", "example_code = \"\"\"\n", "Here's an example Python lambda function:\n", "\n", "lambda x: x + {}\n", "\n", "Is this correct?\n", "\"\"\"\n", "\n", "def chat(message, history):\n", " if message == \"Yes, that's correct.\":\n", " return \"Great!\"\n", " else:\n", " return gr.ChatMessage(\n", " content=example_code.format(random.randint(1, 100)),\n", " options=[\n", " {\"value\": \"Yes, that's correct.\", \"label\": \"Yes\"},\n", " {\"value\": \"No\"}\n", " ]\n", " )\n", "\n", "demo = gr.ChatInterface(\n", " chat,\n", " type=\"messages\",\n", " examples=[\"Write an example Python lambda function.\"]\n", ")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/chatinterface_options/run.py b/demo/chatinterface_options/run.py index 4afbf49fc1fc1..6c684074092d5 100644 --- a/demo/chatinterface_options/run.py +++ b/demo/chatinterface_options/run.py @@ -13,14 +13,13 @@ def chat(message, history): if message == "Yes, that's correct.": return "Great!" else: - return { - "role": "assistant", - "content": example_code.format(random.randint(1, 100)), - "options": [ + return gr.ChatMessage( + content=example_code.format(random.randint(1, 100)), + options=[ {"value": "Yes, that's correct.", "label": "Yes"}, {"value": "No"} - ] - } + ] + ) demo = gr.ChatInterface( chat, diff --git a/demo/chatinterface_thoughts/run.ipynb b/demo/chatinterface_thoughts/run.ipynb new file mode 100644 index 0000000000000..dd26607b9c03f --- /dev/null +++ b/demo/chatinterface_thoughts/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatinterface_thoughts"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from gradio import ChatMessage\n", "import time\n", "\n", "sleep_time = 0.5\n", "\n", "def simulate_thinking_chat(message, history):\n", " start_time = time.time()\n", " response = ChatMessage(\n", " content=\"\",\n", " metadata={\"title\": \"_Thinking_ step-by-step\", \"id\": 0, \"status\": \"pending\"}\n", " )\n", " yield response\n", "\n", " thoughts = [\n", " \"First, I need to understand the core aspects of the query...\",\n", " \"Now, considering the broader context and implications...\",\n", " \"Analyzing potential approaches to formulate a comprehensive answer...\",\n", " \"Finally, structuring the response for clarity and completeness...\"\n", " ]\n", "\n", " accumulated_thoughts = \"\"\n", " for thought in thoughts:\n", " time.sleep(sleep_time)\n", " accumulated_thoughts += f\"- {thought}\\n\\n\"\n", " response.content = accumulated_thoughts.strip()\n", " yield response\n", "\n", " response.metadata[\"status\"] = \"done\"\n", " response.metadata[\"duration\"] = time.time() - start_time\n", " yield response\n", "\n", " response = [\n", " response,\n", " ChatMessage(\n", " content=\"Based on my thoughts and analysis above, my response is: This dummy repro shows how thoughts of a thinking LLM can be progressively shown before providing its final answer.\"\n", " )\n", " ]\n", " yield response\n", "\n", "\n", "demo = gr.ChatInterface(\n", " simulate_thinking_chat,\n", " title=\"Thinking LLM Chat Interface \ud83e\udd14\",\n", " type=\"messages\",\n", ")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/chatinterface_thoughts/run.py b/demo/chatinterface_thoughts/run.py new file mode 100644 index 0000000000000..8ced54f2a0f40 --- /dev/null +++ b/demo/chatinterface_thoughts/run.py @@ -0,0 +1,49 @@ +import gradio as gr +from gradio import ChatMessage +import time + +sleep_time = 0.5 + +def simulate_thinking_chat(message, history): + start_time = time.time() + response = ChatMessage( + content="", + metadata={"title": "_Thinking_ step-by-step", "id": 0, "status": "pending"} + ) + yield response + + thoughts = [ + "First, I need to understand the core aspects of the query...", + "Now, considering the broader context and implications...", + "Analyzing potential approaches to formulate a comprehensive answer...", + "Finally, structuring the response for clarity and completeness..." + ] + + accumulated_thoughts = "" + for thought in thoughts: + time.sleep(sleep_time) + accumulated_thoughts += f"- {thought}\n\n" + response.content = accumulated_thoughts.strip() + yield response + + response.metadata["status"] = "done" + response.metadata["duration"] = time.time() - start_time + yield response + + response = [ + response, + ChatMessage( + content="Based on my thoughts and analysis above, my response is: This dummy repro shows how thoughts of a thinking LLM can be progressively shown before providing its final answer." + ) + ] + yield response + + +demo = gr.ChatInterface( + simulate_thinking_chat, + title="Thinking LLM Chat Interface 🤔", + type="messages", +) + +if __name__ == "__main__": + demo.launch() diff --git a/gradio/chat_interface.py b/gradio/chat_interface.py index 3b412a2037398..46afe4a1d312a 100644 --- a/gradio/chat_interface.py +++ b/gradio/chat_interface.py @@ -6,6 +6,7 @@ import builtins import copy +import dataclasses import inspect import os import warnings @@ -32,6 +33,7 @@ get_component_instance, ) from gradio.components.chatbot import ( + ChatMessage, ExampleMessage, Message, MessageDict, @@ -808,6 +810,11 @@ def _message_as_message_dict( for msg in message: if isinstance(msg, Message): message_dicts.append(msg.model_dump()) + elif isinstance(msg, ChatMessage): + msg.role = role + message_dicts.append( + dataclasses.asdict(msg, dict_factory=utils.dict_factory) + ) elif isinstance(msg, (str, Component)): message_dicts.append({"role": role, "content": msg}) elif ( diff --git a/gradio/components/chatbot.py b/gradio/components/chatbot.py index 430382b4702b9..450ad98b1ea27 100644 --- a/gradio/components/chatbot.py +++ b/gradio/components/chatbot.py @@ -37,6 +37,8 @@ class MetadataDict(TypedDict): title: Union[str, None] id: NotRequired[int | str] parent_id: NotRequired[int | str] + duration: NotRequired[float] + status: NotRequired[Literal["pending", "done"]] class Option(TypedDict): @@ -59,7 +61,6 @@ class MessageDict(TypedDict): role: Literal["user", "assistant", "system"] metadata: NotRequired[MetadataDict] options: NotRequired[list[Option]] - duration: NotRequired[int] class FileMessage(GradioModel): @@ -87,6 +88,14 @@ class Metadata(GradioModel): title: Optional[str] = None id: Optional[int | str] = None parent_id: Optional[int | str] = None + duration: Optional[float] = None + status: Optional[Literal["pending", "done"]] = None + + def __setitem__(self, key: str, value: Any) -> None: + setattr(self, key, value) + + def __getitem__(self, key: str) -> Any: + return getattr(self, key) class Message(GradioModel): @@ -94,7 +103,6 @@ class Message(GradioModel): metadata: Metadata = Field(default_factory=Metadata) content: Union[str, FileMessage, ComponentMessage] options: Optional[list[Option]] = None - duration: Optional[int] = None class ExampleMessage(TypedDict): @@ -110,13 +118,22 @@ class ExampleMessage(TypedDict): ] # list of file paths or URLs to be added to chatbot when example is clicked +@document() @dataclass class ChatMessage: - role: Literal["user", "assistant", "system"] + """ + A dataclass to represent a message in the Chatbot component (type="messages"). + Parameters: + content: The content of the message. Can be a string or a Gradio component. + role: The role of the message, which determines the alignment of the message in the chatbot. Can be "user", "assistant", or "system". Defaults to "assistant". + metadata: The metadata of the message, which is used to display intermediate thoughts / tool usage. Should be a dictionary with the following keys: "title" (required to display the thought), and optionally: "id" and "parent_id" (to nest thoughts), "duration" (to display the duration of the thought), "status" (to display the status of the thought). + options: The options of the message. A list of Option objects, which are dictionaries with the following keys: "label" (the text to display in the option), and optionally "value" (the value to return when the option is selected if different from the label). + """ + content: str | FileData | Component | FileDataDict | tuple | list + role: Literal["user", "assistant", "system"] = "assistant" metadata: MetadataDict | Metadata = field(default_factory=Metadata) options: Optional[list[Option]] = None - duration: Optional[int] = None class ChatbotDataMessages(GradioRootModel): @@ -545,7 +562,6 @@ def _postprocess_message_messages( content=message.content, # type: ignore metadata=message.metadata, # type: ignore options=message.options, - duration=message.duration, ) elif isinstance(message, Message): return message diff --git a/gradio/monitoring_dashboard.py b/gradio/monitoring_dashboard.py index a18a2e5088506..1a41e7b937f06 100644 --- a/gradio/monitoring_dashboard.py +++ b/gradio/monitoring_dashboard.py @@ -60,7 +60,7 @@ def gen_plot(start, end, selected_fn): if selected_fn != "All": df = df[df["function"] == selected_fn] df = df[(df["time"] >= start) & (df["time"] <= end)] - df["time"] = pd.to_datetime(df["time"], unit="s") + df["time"] = pd.to_datetime(df["time"], unit="s") # type: ignore unique_users = len(df["session_hash"].unique()) # type: ignore total_requests = len(df) diff --git a/gradio/utils.py b/gradio/utils.py index 1f9585d8e3d41..769f7dc82bb70 100644 --- a/gradio/utils.py +++ b/gradio/utils.py @@ -1609,3 +1609,16 @@ def get_icon_path(icon_name: str) -> str: set_static_paths(icon_path) return icon_path raise ValueError(f"Icon file not found: {icon_name}") + + +def dict_factory(items): + """ + A utility function to convert a dataclass that includes pydantic fields to a dictionary. + """ + d = {} + for key, value in items: + if hasattr(value, "model_dump"): + d[key] = value.model_dump() + else: + d[key] = value + return d diff --git a/guides/05_chatbots/01_creating-a-chatbot-fast.md b/guides/05_chatbots/01_creating-a-chatbot-fast.md index 27ef1b7692003..cf1a4b452cdce 100644 --- a/guides/05_chatbots/01_creating-a-chatbot-fast.md +++ b/guides/05_chatbots/01_creating-a-chatbot-fast.md @@ -4,9 +4,9 @@ Tags: LLM, CHATBOT, NLP ## Introduction -Chatbots are a popular application of large language models (LLMs). Using Gradio, you can easily build a demo of your LLM and share that with your users, or try it yourself using an intuitive chatbot UI. +Chatbots are a popular application of large language models (LLMs). Using Gradio, you can easily build a chat application and share that with your users, or try it yourself using an intuitive UI. -This tutorial uses `gr.ChatInterface()`, which is a high-level abstraction that allows you to create your chatbot UI fast, often with a _single line of Python_. It can be easily adapted to support multimodal chatbots, or chatbots that require further customization. +This tutorial uses `gr.ChatInterface()`, which is a high-level abstraction that allows you to create your chatbot UI fast, often with a _few lines of Python_. It can be easily adapted to support multimodal chatbots, or chatbots that require further customization. **Prerequisites**: please make sure you are using the latest version of Gradio: @@ -14,9 +14,9 @@ This tutorial uses `gr.ChatInterface()`, which is a high-level abstraction that $ pip install --upgrade gradio ``` -## Quickly loading from Ollama or any OpenAI-API compatible endpoint +## OpenAI-API compatible endpoints -If you have a chat server serving an OpenAI API compatible endpoint (skip ahead if you don't), you can spin up a ChatInterface in a single line. First, also run `pip install openai`. Then, with your own URL, model, and optional token: +If you have a chat server serving an OpenAI-API compatible endpoint (e.g. Ollama), you can spin up a ChatInterface in a single line of Python. First, also run `pip install openai`. Then, with your own URL, model, and optional token: ```python import gradio as gr @@ -24,9 +24,11 @@ import gradio as gr gr.load_chat("http://localhost:11434/v1/", model="llama3.2", token=None).launch() ``` +If not, don't worry, keep reading to see how to create an application around any chat model! + ## Defining a chat function -When working with `gr.ChatInterface()`, the first thing you should do is define your **chat function**. In the simplest case, your chat function should accept two arguments: `message` and `history` (the arguments can be named anything, but must be in this order). +To create a chat application with `gr.ChatInterface()`, the first thing you should do is define your **chat function**. In the simplest case, your chat function should accept two arguments: `message` and `history` (the arguments can be named anything, but must be in this order). - `message`: a `str` representing the user's most recent message. - `history`: a list of openai-style dictionaries with `role` and `content` keys, representing the previous conversation history. May also include additional keys representing message metadata. @@ -40,9 +42,19 @@ For example, the `history` could look like this: ] ``` +while the next `message` would be: + +```py +"And what is its largest city?" +``` + Your chat function simply needs to return: -* a `str` value, which is the chatbot's response based on the chat `history` and most recent `message`. +* a `str` value, which is the chatbot's response based on the chat `history` and most recent `message`, for example, in this case: + +``` +Paris is also the largest city. +``` Let's take a look at a few example chat functions: @@ -280,7 +292,8 @@ $code_chatinterface_artifacts ## Returning Complex Responses -We mentioned earlier that in the simplest case, your chat function should return a `str` response, which will be rendered as text in the chatbot. However, you can also return more complex responses as we discuss below: +We mentioned earlier that in the simplest case, your chat function should return a `str` response, which will be rendered as Markdown in the chatbot. However, you can also return more complex responses as we discuss below: + **Returning files or Gradio components** @@ -313,21 +326,55 @@ gr.ChatInterface( Similarly, you could return image files with `gr.Image`, video files with `gr.Video`, or arbitrary files with the `gr.File` component. -**Providing preset responses** +**Returning Multiple Messages** -You may want to provide preset responses that a user can choose between when conversing with your chatbot. To do this, return a complete openai-style message dictionary from your chat function, and add the `options` key to the dictionary returned from your chat function to set these responses. +You can return multiple assistant messages from your chat function simply by returning a `list` of messages, each of which is a valid chat type. This lets you, for example, send a message along with files, as in the following example: -The value corresponding to the `options` key should be a list of dictionaries, each with a `value` (a string that is the value that should be sent to the chat function when this response is clicked) and an optional `label` (if provided, is the text displayed as the preset response instead of the `value`). +$code_chatinterface_echo_multimodal -This example illustrates how to use preset responses: -$code_chatinterface_options +**Displaying intermediate thoughts or tool usage** -**Returning Multiple Messages** +The `gr.ChatInterface` class supports displaying intermediate thoughts or tool usage direct in the chatbot. -You can return multiple assistant messages from your chat function simply by returning a `list` of messages of any of the above types (you can even mix-and-match). This lets you, for example, send a message along with files, as in the following example: +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/nested-thought.png) -$code_chatinterface_echo_multimodal + To do this, you will need to return a `gr.ChatMessage` object from your chat function. Here is the schema of the `gr.ChatMessage` data class as well as two internal typed dictionaries: + + ```py +@dataclass +class ChatMessage: + content: str | Component + metadata: MetadataDict = None + options: list[Option] = None + +class MetadataDict(TypedDict): + title: Union[str, None] + id: NotRequired[int | str] + parent_id: NotRequired[int | str] + duration: NotRequired[int] + status: NotRequired[Literal["pending", "done"]] + +class Option(TypedDict): + label: NotRequired[str] + value: str + ``` + +As you can see, the `gr.ChatMessage` dataclass is similar to the openai-style message format, e.g. it has a "content" key that refers to the chat message content. But it also includes a "metadata" key whose value is a dictionary. If this dictionary includes a "title" key, the resulting message is displayed as an intermediate thought with the title being displayed on top of the thought. Here's an example showing the usage: + +$code_chatinterface_thoughts + +You can even show nested thoughts, which is useful for agent demos in which one tool may call other tools. To display nested thoughts, include "id" and "parent_id" keys in the "metadata" dictionary. See [this example for a complete demonstration](https://github.com/gradio-app/gradio/blob/main/demo/chatinterface_nested_thoughts/run.py). + +**Providing preset responses** + +When returning an assistant message, you may want to provide preset options that a user can choose in response. To do this, again, you will again return a `gr.ChatMessage` instance from your chat function. This time, make sure to set the `options` key specifying the preset responses. + +As shown in the schema for `gr.ChatMessage` above, the value corresponding to the `options` key should be a list of dictionaries, each with a `value` (a string that is the value that should be sent to the chat function when this response is clicked) and an optional `label` (if provided, is the text displayed as the preset response instead of the `value`). + +This example illustrates how to use preset responses: + +$code_chatinterface_options ## Using Your Chatbot via API diff --git a/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx b/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx index 0734637e3b1ce..0f527669aaf0f 100644 --- a/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx +++ b/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx @@ -6,6 +6,7 @@ delete components_no_dataclasses["brush"]; delete components_no_dataclasses["eraser"]; delete components_no_dataclasses["waveformoptions"]; + delete components_no_dataclasses["chatmessage"]; let events = get_object("events"); let events_matrix = get_object("events_matrix"); diff --git a/js/_website/src/lib/templates/gradio/03_components/chatbot.svx b/js/_website/src/lib/templates/gradio/03_components/chatbot.svx index 3cd1587b0e88c..40c636480a3a9 100644 --- a/js/_website/src/lib/templates/gradio/03_components/chatbot.svx +++ b/js/_website/src/lib/templates/gradio/03_components/chatbot.svx @@ -10,6 +10,7 @@ import { style_formatted_text } from "$lib/text"; let obj = get_object("chatbot"); + let chatmessage_obj = get_object("chatmessage"); let embedded_demo_obj = `[ {"role": "user", "content": "Hello World"}, @@ -53,53 +54,41 @@ The `'tuples'` type is deprecated and will be removed in a future version of Gra If the `type` is `'messages'`, then the data sent to/from the chatbot will be a list of dictionaries with `role` and `content` keys. This format is compliant with the format expected by most LLM APIs (HuggingChat, OpenAI, Claude). -The `role` key is either `'user'` or `'assistant'` and the `content` key can be one of the following: +The `role` key is either `'user'` or `'assistant'` and the `content` key can be one of the following should be a string (rendered as markdown/html) or a Gradio component (useful for displaying files). -1. A string (markdown/html is also supported). -2. A dictionary with `path` and `alt_text` keys. In this case, the file at `path` will be displayed in the chat history. Image, audio, and video files are fully embedded and visualized in the chat bubble. -The `path` key can point to a valid publicly available URL. The `alt_text` key is optional but it's good practice to provide [alt text](https://en.wikipedia.org/wiki/Alt_attribute). -3. An instance of another Gradio component. - -
-We will show examples for all three cases below - +As an example: ```python -def generate_response(history): - # A plain text response - history.append( - {"role": "assistant", content="I am happy to provide you that report and plot."} - ) - # Embed the quaterly sales report in the chat - history.append( - {"role": "assistant", content={"path": "quaterly_sales.txt", "alt_text": "Sales Report for Q2 2024"}} - ) - # Make a plot of sales data - history.append( - {"role": "assistant", content=gr.Plot(value=make_plot_from_file('quaterly_sales.txt'))} - ) - return history +import gradio as gr + +history = [ + {"role": "assistant", content="I am happy to provide you that report and plot."} + {"role": "assistant", content=gr.Plot(value=make_plot_from_file('quaterly_sales.txt'))} +] + +with gr.Blocks() as demo: + gr.Chatbot(history) + +demo.launch() ``` For convenience, you can use the `ChatMessage` dataclass so that your text editor can give you autocomplete hints and typechecks. ```python -from gradio import ChatMessage +import gradio as gr -def generate_response(history): - history.append( - ChatMessage(role="assistant", - content="How can I help you?") - ) - return history -``` +history = [ + gr.ChatMessage(role="assistant", content="How can I help you?"), + gr.ChatMessage(role="user", content="Can you make me a plot of quarterly sales?"}, + gr.ChatMessage(role="assistant", content="I am happy to provide you that report and plot."} +] -### Tuples format +with gr.Blocks() as demo: + gr.Chatbot(history) + +demo.launch() +``` -If `type` is `'tuples'`, then the data sent to/from the chatbot will be a list of tuples. -The first element of each tuple is the user message and the second element is the bot's response. -Each element can be a string (markdown/html is supported), -a tuple (in which case the first element is a filepath that will be displayed in the chatbot), -or a gradio component (see the Examples section for more details). ### Initialization @@ -178,6 +167,29 @@ demo.launch() {/if} + +### Helper Classes +
+ + +### ChatMessage + + + +```python +gradio.ChatMessage(···) +``` + + +#### Description +## {@html style_formatted_text(chatmessage_obj.description)} + + +#### Initialization + + +
+ {#if obj.guides && obj.guides.length > 0} ### Guides diff --git a/js/chatbot/shared/Thought.svelte b/js/chatbot/shared/Thought.svelte index 621584c40c942..f9df371f36e8d 100644 --- a/js/chatbot/shared/Thought.svelte +++ b/js/chatbot/shared/Thought.svelte @@ -71,11 +71,19 @@ {sanitize_html} {root} /> - {#if thought_node.content === "" || thought_node.content === null} + {#if thought_node.content === "" || thought_node.content === null || thought_node.metadata?.status === "pending"} {/if} - {#if thought_node?.duration} - {thought_node.duration || 0.16}s + {#if thought_node?.metadata?.duration} + + {#if Number.isInteger(thought_node.metadata.duration)} + {thought_node.metadata.duration}s + {:else if thought_node.metadata.duration >= 0.1} + {thought_node.metadata.duration.toFixed(1)}s + {:else} + {(thought_node.metadata.duration * 1000).toFixed(1)}ms + {/if} + {/if} diff --git a/js/chatbot/types.ts b/js/chatbot/types.ts index 65a22b87a9761..c79d8fae56611 100644 --- a/js/chatbot/types.ts +++ b/js/chatbot/types.ts @@ -6,6 +6,8 @@ export interface Metadata { title: string | null; id?: number | string | null; parent_id?: number | string | null; + duration?: number; + status?: "pending" | "done" | null; } export interface ComponentData { @@ -26,7 +28,6 @@ export interface Message { content: string | FileData | ComponentData; index: number | [number, number]; options?: Option[]; - duration?: number; } export interface TextMessage extends Message {