diff --git a/.changeset/wise-moose-exist.md b/.changeset/wise-moose-exist.md new file mode 100644 index 0000000000000..e2182b90e8c06 --- /dev/null +++ b/.changeset/wise-moose-exist.md @@ -0,0 +1,7 @@ +--- +"@gradio/chatbot": minor +"@gradio/icons": minor +"gradio": minor +--- + +feat:Improve tool UI and support nested thoughts diff --git a/demo/chatbot_nested_thoughts/run.ipynb b/demo/chatbot_nested_thoughts/run.ipynb new file mode 100644 index 0000000000000..980c941d3585d --- /dev/null +++ b/demo/chatbot_nested_thoughts/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatbot_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(history):\n", " history.append(\n", " ChatMessage(\n", " role=\"user\", content=\"What is the weather in San Francisco right now?\"\n", " )\n", " )\n", " yield history\n", " time.sleep(sleep_time)\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\n", " content=\"In order to find the current weather in San Francisco, I will need to use my weather tool.\",\n", " )\n", " )\n", " yield history\n", " time.sleep(sleep_time)\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\n", " content=\"\",\n", " metadata={\"title\": \"Gathering Weather Websites\", \"id\": 1},\n", " )\n", " )\n", " yield history\n", " time.sleep(long_sleep_time)\n", " history[-1].content = \"Will check: weather.com and sunny.org\"\n", " yield history\n", " time.sleep(sleep_time)\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\n", " content=\"Received weather from weather.com.\",\n", " metadata={\"title\": \"API Success \u2705\", \"parent_id\": 1, \"id\": 2},\n", " )\n", " )\n", " yield history\n", " time.sleep(sleep_time)\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\n", " content=\"API Error when connecting to sunny.org.\",\n", " metadata={\"title\": \"API Error \ud83d\udca5 \", \"parent_id\": 1, \"id\": 3},\n", " )\n", " )\n", " yield history\n", " time.sleep(sleep_time)\n", "\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\n", " content=\"I will try yet again\",\n", " metadata={\"title\": \"I will try again\", \"id\": 4, \"parent_id\": 3},\n", " )\n", " )\n", " yield history\n", "\n", " time.sleep(sleep_time)\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\n", " content=\"Failed again\",\n", " metadata={\"title\": \"Failed again\", \"id\": 6, \"parent_id\": 4},\n", " )\n", " )\n", " yield history\n", "\n", "with gr.Blocks() as demo:\n", " chatbot = gr.Chatbot(type=\"messages\", height=500, show_copy_button=True)\n", " demo.load(generate_response, chatbot, chatbot)\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/chatbot_nested_thoughts/run.py b/demo/chatbot_nested_thoughts/run.py new file mode 100644 index 0000000000000..3b868a544d249 --- /dev/null +++ b/demo/chatbot_nested_thoughts/run.py @@ -0,0 +1,79 @@ +import gradio as gr +from gradio import ChatMessage +import time + +sleep_time = 0.1 +long_sleep_time = 1 + +def generate_response(history): + history.append( + ChatMessage( + role="user", content="What is the weather in San Francisco right now?" + ) + ) + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="In order to find the current weather in San Francisco, I will need to use my weather tool.", + ) + ) + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="", + metadata={"title": "Gathering Weather Websites", "id": 1}, + ) + ) + yield history + time.sleep(long_sleep_time) + history[-1].content = "Will check: weather.com and sunny.org" + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="Received weather from weather.com.", + metadata={"title": "API Success ✅", "parent_id": 1, "id": 2}, + ) + ) + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="API Error when connecting to sunny.org.", + metadata={"title": "API Error 💥 ", "parent_id": 1, "id": 3}, + ) + ) + yield history + time.sleep(sleep_time) + + history.append( + ChatMessage( + role="assistant", + content="I will try yet again", + metadata={"title": "I will try again", "id": 4, "parent_id": 3}, + ) + ) + yield history + + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="Failed again", + metadata={"title": "Failed again", "id": 6, "parent_id": 4}, + ) + ) + yield history + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(type="messages", height=500, show_copy_button=True) + demo.load(generate_response, chatbot, chatbot) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_thoughts/run.ipynb b/demo/chatbot_thoughts/run.ipynb new file mode 100644 index 0000000000000..2bbda50eee008 --- /dev/null +++ b/demo/chatbot_thoughts/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatbot_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", "def simulate_thinking_chat(message: str, history: list):\n", " \"\"\"Mimicking thinking process and response\"\"\"\n", " # Add initial empty thinking message to chat history\n", "\n", " history.append( # Adds new message to the chat history list\n", " ChatMessage( # Creates a new chat message\n", " role=\"assistant\", # Specifies this is from the assistant\n", " content=\"\", # Initially empty content\n", " metadata={\"title\": \"Thinking... \"} # Setting a thinking header here\n", " )\n", " )\n", " time.sleep(0.5)\n", " yield history # Returns current state of chat history\n", " \n", " # Define the thoughts that LLM will \"think\" through\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", " # Variable to store all thoughts as they accumulate\n", " accumulated_thoughts = \"\"\n", " \n", " # Loop through each thought\n", " for thought in thoughts:\n", " time.sleep(0.5) # Add a samll delay for realism\n", " \n", " # Add new thought to accumulated thoughts with markdown bullet point\n", " accumulated_thoughts += f\"- {thought}\\n\\n\" # \\n\\n creates line breaks\n", " \n", " # Update the thinking message with all thoughts so far\n", " history[-1] = ChatMessage( # Updates last message in history\n", " role=\"assistant\",\n", " content=accumulated_thoughts.strip(), # Remove extra whitespace\n", " metadata={\"title\": \"Thinking...\"} # Shows thinking header\n", " )\n", " yield history # Returns updated chat history\n", " \n", " # After thinking is complete, adding the final response\n", " history.append(\n", " ChatMessage(\n", " role=\"assistant\",\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 history # Returns final state of chat history\n", "\n", "# Gradio blocks with gr.chatbot\n", "with gr.Blocks() as demo:\n", " gr.Markdown(\"# Thinking LLM Demo \ud83e\udd14\")\n", " chatbot = gr.Chatbot(type=\"messages\", render_markdown=True)\n", " msg = gr.Textbox(placeholder=\"Type your message...\")\n", " \n", " msg.submit(\n", " lambda m, h: (m, h + [ChatMessage(role=\"user\", content=m)]),\n", " [msg, chatbot],\n", " [msg, chatbot]\n", " ).then(\n", " simulate_thinking_chat,\n", " [msg, chatbot],\n", " chatbot\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/chatbot_thoughts/run.py b/demo/chatbot_thoughts/run.py new file mode 100644 index 0000000000000..2cc5bf44d1770 --- /dev/null +++ b/demo/chatbot_thoughts/run.py @@ -0,0 +1,71 @@ +import gradio as gr +from gradio import ChatMessage +import time + +def simulate_thinking_chat(message: str, history: list): + """Mimicking thinking process and response""" + # Add initial empty thinking message to chat history + + history.append( # Adds new message to the chat history list + ChatMessage( # Creates a new chat message + role="assistant", # Specifies this is from the assistant + content="", # Initially empty content + metadata={"title": "Thinking... "} # Setting a thinking header here + ) + ) + time.sleep(0.5) + yield history # Returns current state of chat history + + # Define the thoughts that LLM will "think" through + 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..." + ] + + # Variable to store all thoughts as they accumulate + accumulated_thoughts = "" + + # Loop through each thought + for thought in thoughts: + time.sleep(0.5) # Add a samll delay for realism + + # Add new thought to accumulated thoughts with markdown bullet point + accumulated_thoughts += f"- {thought}\n\n" # \n\n creates line breaks + + # Update the thinking message with all thoughts so far + history[-1] = ChatMessage( # Updates last message in history + role="assistant", + content=accumulated_thoughts.strip(), # Remove extra whitespace + metadata={"title": "Thinking..."} # Shows thinking header + ) + yield history # Returns updated chat history + + # After thinking is complete, adding the final response + history.append( + ChatMessage( + role="assistant", + 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 history # Returns final state of chat history + +# Gradio blocks with gr.chatbot +with gr.Blocks() as demo: + gr.Markdown("# Thinking LLM Demo 🤔") + chatbot = gr.Chatbot(type="messages", render_markdown=True) + msg = gr.Textbox(placeholder="Type your message...") + + msg.submit( + lambda m, h: (m, h + [ChatMessage(role="user", content=m)]), + [msg, chatbot], + [msg, chatbot] + ).then( + simulate_thinking_chat, + [msg, chatbot], + chatbot + ) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/gradio/components/chatbot.py b/gradio/components/chatbot.py index 73d8810a35eec..430382b4702b9 100644 --- a/gradio/components/chatbot.py +++ b/gradio/components/chatbot.py @@ -35,6 +35,8 @@ class MetadataDict(TypedDict): title: Union[str, None] + id: NotRequired[int | str] + parent_id: NotRequired[int | str] class Option(TypedDict): @@ -57,6 +59,7 @@ class MessageDict(TypedDict): role: Literal["user", "assistant", "system"] metadata: NotRequired[MetadataDict] options: NotRequired[list[Option]] + duration: NotRequired[int] class FileMessage(GradioModel): @@ -82,6 +85,8 @@ class ChatbotDataTuples(GradioRootModel): class Metadata(GradioModel): title: Optional[str] = None + id: Optional[int | str] = None + parent_id: Optional[int | str] = None class Message(GradioModel): @@ -89,6 +94,7 @@ 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,6 +116,7 @@ class ChatMessage: content: str | FileData | Component | FileDataDict | tuple | list metadata: MetadataDict | Metadata = field(default_factory=Metadata) options: Optional[list[Option]] = None + duration: Optional[int] = None class ChatbotDataMessages(GradioRootModel): @@ -538,6 +545,7 @@ 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/js/chatbot/Chatbot.stories.svelte b/js/chatbot/Chatbot.stories.svelte index da8bc2abf7e8c..f6e4c119679bc 100644 --- a/js/chatbot/Chatbot.stories.svelte +++ b/js/chatbot/Chatbot.stories.svelte @@ -431,12 +431,73 @@ This document is a showcase of various Markdown capabilities.`, value: [ { role: "user", - content: "What is the weather like today?" + content: "What is 27 * 14?", + duration: 0.1 }, { role: "assistant", - metadata: { title: "☀️ Using Weather Tool" }, - content: "Weather looks sunny today." + duration: 10, + content: "Let me break this down step by step.", + metadata: { + id: 1, + title: "Solving multiplication", + parent_id: 0 + } + }, + { + role: "assistant", + content: "First, let's multiply 27 by 10: 27 * 10 = 270", + metadata: { + id: 2, + title: "Step 1", + parent_id: 1 + } + }, + { + role: "assistant", + content: + "We can do this quickly because multiplying by 10 just adds a zero", + metadata: { + id: 6, + title: "Quick Tip", + parent_id: 2 + } + }, + { + role: "assistant", + content: "Then multiply 27 by 4: 27 * 4 = 108", + metadata: { + id: 3, + title: "Step 2", + parent_id: 1 + } + }, + { + role: "assistant", + content: + "Adding these together: 270 + 108 = 378. Therefore, 27 * 14 = 378" + }, + { + role: "assistant", + content: "Let me verify this result using a different method.", + metadata: { + id: 4, + title: "Verification", + parent_id: 0 + } + }, + { + role: "assistant", + content: "Using the standard algorithm: 27 * 14 = (20 + 7) * (10 + 4)", + metadata: { + id: 5, + title: "Expanding", + parent_id: 4 + } + }, + { + role: "assistant", + content: "The result is confirmed to be 378." } ] }} diff --git a/js/chatbot/shared/ButtonPanel.svelte b/js/chatbot/shared/ButtonPanel.svelte index a025b50d0e97d..f9d2d2f18b5ee 100644 --- a/js/chatbot/shared/ButtonPanel.svelte +++ b/js/chatbot/shared/ButtonPanel.svelte @@ -2,9 +2,11 @@ import LikeDislike from "./LikeDislike.svelte"; import Copy from "./Copy.svelte"; import type { FileData } from "@gradio/client"; - import type { NormalisedMessage, TextMessage } from "../types"; + import type { NormalisedMessage, TextMessage, ThoughtNode } from "../types"; import { Retry, Undo, Edit, Check, Clear } from "@gradio/icons"; import { IconButtonWrapper, IconButton } from "@gradio/atoms"; + import { all_text, is_all_text } from "./utils"; + export let likeable: boolean; export let feedback_options: string[]; export let show_retry: boolean; @@ -22,25 +24,7 @@ export let layout: "bubble" | "panel"; export let dispatch: any; - function is_all_text( - message: NormalisedMessage[] | NormalisedMessage - ): message is TextMessage[] | TextMessage { - return ( - (Array.isArray(message) && - message.every((m) => typeof m.content === "string")) || - (!Array.isArray(message) && typeof message.content === "string") - ); - } - - function all_text(message: TextMessage[] | TextMessage): string { - if (Array.isArray(message)) { - return message.map((m) => m.content).join("\n"); - } - return message.content; - } - $: message_text = is_all_text(message) ? all_text(message) : ""; - $: show_copy = show_copy_button && message && is_all_text(message); @@ -134,7 +118,6 @@ .panel { display: flex; align-self: flex-start; - padding: 0 var(--spacing-xl); z-index: var(--layer-1); } diff --git a/js/chatbot/shared/ChatBot.svelte b/js/chatbot/shared/ChatBot.svelte index 3ad7d18c86556..4017a376a9458 100644 --- a/js/chatbot/shared/ChatBot.svelte +++ b/js/chatbot/shared/ChatBot.svelte @@ -395,13 +395,6 @@ } } - .message-wrap { - display: flex; - flex-direction: column; - justify-content: space-between; - margin-bottom: var(--spacing-xxl); - } - .message-wrap :global(.prose.chatbot.md) { opacity: 0.8; overflow-wrap: break-word; diff --git a/js/chatbot/shared/Message.svelte b/js/chatbot/shared/Message.svelte index d4f2a9e5cf510..9520b16056872 100644 --- a/js/chatbot/shared/Message.svelte +++ b/js/chatbot/shared/Message.svelte @@ -1,6 +1,5 @@ - -