Skip to content

Commit a1f2649

Browse files
aliabid94Ali Abidgradio-pr-botabidlabs
authored
Chat Interface flagging and chatbot feedback (#10272)
* changes * add changeset * changes * Update gradio/flagging.py Co-authored-by: Abubakar Abid <[email protected]> * Update gradio/chat_interface.py Co-authored-by: Abubakar Abid <[email protected]> * Update gradio/chat_interface.py Co-authored-by: Abubakar Abid <[email protected]> * Update gradio/chat_interface.py Co-authored-by: Abubakar Abid <[email protected]> * Update gradio/chat_interface.py Co-authored-by: Abubakar Abid <[email protected]> * Update gradio/components/chatbot.py Co-authored-by: Abubakar Abid <[email protected]> * changes * changes * changes * Update gradio/components/chatbot.py Co-authored-by: Abubakar Abid <[email protected]> * changes * changes * doc changes --------- Co-authored-by: Ali Abid <[email protected]> Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Abubakar Abid <[email protected]>
1 parent 4fc7fb7 commit a1f2649

File tree

18 files changed

+222
-34
lines changed

18 files changed

+222
-34
lines changed

.changeset/dirty-sides-jam.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@gradio/atoms": minor
3+
"@gradio/chatbot": minor
4+
"@gradio/utils": minor
5+
"gradio": minor
6+
---
7+
8+
feat:Chat Interface flagging and chatbot feedback
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatinterface_streaming_echo"]}, {"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 time\n", "import gradio as gr\n", "\n", "def slow_echo(message, history):\n", " for i in range(len(message)):\n", " time.sleep(0.05)\n", " yield \"You typed: \" + message[: i + 1]\n", "\n", "demo = gr.ChatInterface(slow_echo, type=\"messages\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
1+
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatinterface_streaming_echo"]}, {"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 time\n", "import gradio as gr\n", "\n", "def slow_echo(message, history):\n", " for i in range(len(message)):\n", " time.sleep(0.05)\n", " yield \"You typed: \" + message[: i + 1]\n", "\n", "demo = gr.ChatInterface(slow_echo, type=\"messages\", flagging_mode=\"manual\", flagging_options=[\"Like\", \"Spam\", \"Inappropriate\", \"Other\"])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

demo/chatinterface_streaming_echo/run.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def slow_echo(message, history):
66
time.sleep(0.05)
77
yield "You typed: " + message[: i + 1]
88

9-
demo = gr.ChatInterface(slow_echo, type="messages")
9+
demo = gr.ChatInterface(slow_echo, type="messages", flagging_mode="manual", flagging_options=["Like", "Spam", "Inappropriate", "Other"])
1010

1111
if __name__ == "__main__":
1212
demo.launch()

gradio/chat_interface.py

+26
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import builtins
88
import copy
99
import inspect
10+
import os
1011
import warnings
1112
from collections.abc import AsyncGenerator, Callable, Generator, Sequence
1213
from pathlib import Path
@@ -37,6 +38,7 @@
3738
from gradio.components.multimodal_textbox import MultimodalPostprocess, MultimodalValue
3839
from gradio.context import get_blocks_context
3940
from gradio.events import Dependency, EditData, SelectData
41+
from gradio.flagging import ChatCSVLogger
4042
from gradio.helpers import create_examples as Examples # noqa: N812
4143
from gradio.helpers import special_args, update
4244
from gradio.layouts import Accordion, Column, Group, Row
@@ -85,6 +87,9 @@ def __init__(
8587
title: str | None = None,
8688
description: str | None = None,
8789
theme: Theme | str | None = None,
90+
flagging_mode: Literal["never", "manual"] | None = None,
91+
flagging_options: list[str] | tuple[str, ...] | None = ("Like", "Dislike"),
92+
flagging_dir: str = ".gradio/flagged",
8893
css: str | None = None,
8994
css_paths: str | Path | Sequence[str | Path] | None = None,
9095
js: str | None = None,
@@ -122,6 +127,9 @@ def __init__(
122127
title: a title for the interface; if provided, appears above chatbot in large font. Also used as the tab title when opened in a browser window.
123128
description: a description for the interface; if provided, appears above the chatbot and beneath the title in regular font. Accepts Markdown and HTML content.
124129
theme: a Theme object or a string representing a theme. If a string, will look for a built-in theme with that name (e.g. "soft" or "default"), or will attempt to load a theme from the Hugging Face Hub (e.g. "gradio/monochrome"). If None, will use the Default theme.
130+
flagging_mode: one of "never", "manual". If "never", users will not see a button to flag an input and output. If "manual", users will see a button to flag.
131+
flagging_options: a list of strings representing the options that users can choose from when flagging a message. Defaults to ["Like", "Dislike"]. These two case-sensitive strings will render as "thumbs up" and "thumbs down" icon respectively next to each bot message, but any other strings appear under a separate flag icon.
132+
flagging_dir: path to the the directory where flagged data is stored. If the directory does not exist, it will be created.
125133
css: Custom css as a code string. This css will be included in the demo webpage.
126134
css_paths: Custom css as a pathlib.Path to a css file or a list of such paths. This css files will be read, concatenated, and included in the demo webpage. If the `css` parameter is also set, the css from `css` will be included first.
127135
js: Custom js as a code string. The custom js should be in the form of a single js function. This function will automatically be executed when the page loads. For more flexibility, use the head parameter to insert js inside <script> tags.
@@ -214,6 +222,18 @@ def __init__(
214222
if self._additional_inputs_in_examples:
215223
break
216224

225+
if flagging_mode is None:
226+
flagging_mode = os.getenv("GRADIO_CHAT_FLAGGING_MODE", "never") # type: ignore
227+
if flagging_mode in ["manual", "never"]:
228+
self.flagging_mode = flagging_mode
229+
else:
230+
raise ValueError(
231+
"Invalid value for `flagging_mode` parameter."
232+
"Must be: 'manual' or 'never'."
233+
)
234+
self.flagging_options = flagging_options
235+
self.flagging_dir = flagging_dir
236+
217237
with self:
218238
with Column():
219239
if title:
@@ -501,6 +521,12 @@ def _setup_events(self) -> None:
501521
show_api=False,
502522
).success(**submit_fn_kwargs).success(**synchronize_chat_state_kwargs)
503523

524+
if self.flagging_mode != "never":
525+
flagging_callback = ChatCSVLogger()
526+
flagging_callback.setup(self.flagging_dir)
527+
self.chatbot.feedback_options = self.flagging_options
528+
self.chatbot.like(flagging_callback.flag, self.chatbot)
529+
504530
def _setup_stop_events(
505531
self, event_triggers: list[Callable], events_to_cancel: list[Dependency]
506532
) -> None:

gradio/components/chatbot.py

+3
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def __init__(
194194
avatar_images: tuple[str | Path | None, str | Path | None] | None = None,
195195
sanitize_html: bool = True,
196196
render_markdown: bool = True,
197+
feedback_options: list[str] | tuple[str, ...] | None = ("Like", "Dislike"),
197198
bubble_full_width=None,
198199
line_breaks: bool = True,
199200
layout: Literal["panel", "bubble"] | None = None,
@@ -232,6 +233,7 @@ def __init__(
232233
avatar_images: Tuple of two avatar image paths or URLs for user and bot (in that order). Pass None for either the user or bot image to skip. Must be within the working directory of the Gradio app or an external URL.
233234
sanitize_html: If False, will disable HTML sanitization for chatbot messages. This is not recommended, as it can lead to security vulnerabilities.
234235
render_markdown: If False, will disable Markdown rendering for chatbot messages.
236+
feedback_options: A list of strings representing the feedback options that will be displayed to the user. The exact case-sensitive strings "Like" and "Dislike" will render as thumb icons, but any other choices will appear under a separate flag icon.
235237
bubble_full_width: Deprecated.
236238
line_breaks: If True (default), will enable Github-flavored Markdown line breaks in chatbot messages. If False, single new lines will be ignored. Only applies if `render_markdown` is True.
237239
layout: If "panel", will display the chatbot in a llm style layout. If "bubble", will display the chatbot with message bubbles, with the user and bot messages on alterating sides. Will default to "bubble".
@@ -287,6 +289,7 @@ def __init__(
287289
self.layout = layout
288290
self.show_copy_all_button = show_copy_all_button
289291
self.allow_file_downloads = allow_file_downloads
292+
self.feedback_options = feedback_options
290293
super().__init__(
291294
label=label,
292295
every=every,

gradio/events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,9 @@ def __init__(self, target: Block | None, data: Any):
300300
"""
301301
The value of the liked/disliked item.
302302
"""
303-
self.liked: bool = data.get("liked", True)
303+
self.liked: bool | str = data.get("liked", True)
304304
"""
305-
True if the item was liked, False if disliked.
305+
True if the item was liked, False if disliked, or string value if any other feedback.
306306
"""
307307

308308

gradio/flagging.py

+46
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import contextlib
44
import csv
55
import datetime
6+
import json
67
import os
78
import re
89
import time
@@ -17,6 +18,7 @@
1718

1819
import gradio as gr
1920
from gradio import utils, wasm_utils
21+
from gradio.events import LikeData
2022

2123
if TYPE_CHECKING:
2224
from gradio.components import Component
@@ -344,6 +346,50 @@ def flag(
344346
return line_count
345347

346348

349+
class ChatCSVLogger:
350+
"""
351+
Flagging callback for chat conversations.
352+
Flagged conversations and like/dislike reactions are logged to a CSV file on the machine running the gradio app.
353+
"""
354+
355+
def __init__(self):
356+
pass
357+
358+
def setup(self, flagging_dir: str):
359+
self.flagging_dir = flagging_dir
360+
os.makedirs(flagging_dir, exist_ok=True)
361+
362+
def flag(
363+
self,
364+
like_data: LikeData,
365+
messages: list,
366+
):
367+
flagging_dir = self.flagging_dir
368+
log_filepath = Path(flagging_dir) / "log.csv"
369+
is_new = not Path(log_filepath).exists()
370+
371+
feedback = (
372+
"Like"
373+
if like_data.liked is True
374+
else "Dislike"
375+
if like_data.liked is False
376+
else like_data.liked
377+
)
378+
csv_data = [
379+
json.dumps(messages),
380+
like_data.index,
381+
feedback,
382+
str(datetime.datetime.now()),
383+
]
384+
385+
with open(log_filepath, "a", encoding="utf-8", newline="") as csvfile:
386+
if is_new:
387+
writer = csv.writer(csvfile)
388+
writer.writerow(["conversation", "index", "value", "flag", "timestamp"])
389+
writer = csv.writer(csvfile)
390+
writer.writerow(utils.sanitize_list_for_csv(csv_data))
391+
392+
347393
class FlagMethod:
348394
"""
349395
Helper class that contains the flagging options and calls the flagging method. Also

guides/04_additional-features/09_environment-variables.md

+12
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ Environment variables in Gradio provide a way to customize your applications and
185185
export GRADIO_RESET_EXAMPLES_CACHE="True"
186186
```
187187

188+
### 20. `GRADIO_CHAT_FLAGGING_MODE`
189+
190+
- **Description**: Controls whether users can flag messages in `gr.ChatInterface` applications. Similar to `GRADIO_FLAGGING_MODE` but specifically for chat interfaces.
191+
- **Default**: `"never"`
192+
- **Options**: `"never"`, `"manual"`
193+
- **Example**:
194+
```sh
195+
export GRADIO_CHAT_FLAGGING_MODE="manual"
196+
```
197+
198+
199+
188200
## How to Set Environment Variables
189201

190202
To set environment variables in your terminal, use the `export` command followed by the variable name and its value. For example:

guides/05_chatbots/01_creating-a-chatbot-fast.md

+8
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ To use the endpoint, you should use either the [Gradio Python Client](/guides/ge
341341
* Slack bot [[tutorial]](../guides/creating-a-slack-bot-from-a-gradio-app)
342342
* Website widget [[tutorial]](../guides/creating-a-website-widget-from-a-gradio-chatbot)
343343

344+
## Collecting Feedback
345+
346+
To gather feedback on your generations, set `gr.ChatInterface(flagging_mode="manual")` and users can thumbs-up and down assistant responses. Each flagged response, along with the entire chat history, will get saved in a CSV file in the app folder (or wherever `flagging_dir` specifies).
347+
348+
You can also specify more feedback options via `flagging_options`, which will appear under a dedicated flag button. Here's an example that shows several flagging options. Because the case-sensitive string "Like" is one of the flagging options, the user will see a "thumbs up" icon next to each assistant message. The three other flagging options will appear under a dedicated "flag" icon.
349+
350+
$code_chatinterface_streaming_echo
351+
344352
## What's Next?
345353

346354
Now that you've learned about the `gr.ChatInterface` class and how it can be used to create chatbot UIs quickly, we recommend reading one of the following:

js/atoms/src/IconButtonWrapper.svelte

+6-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,12 @@
5858
margin-right: var(--spacing-xxs);
5959
}
6060
61-
.icon-button-wrapper :global(a.download-link:not(:last-child)::after),
62-
.icon-button-wrapper :global(button:not(:last-child)::after) {
61+
.icon-button-wrapper
62+
:global(
63+
a.download-link:not(:last-child):not(.extra-feedback-option)::after
64+
),
65+
.icon-button-wrapper
66+
:global(button:not(:last-child):not(.extra-feedback-option)::after) {
6367
content: "";
6468
position: absolute;
6569
right: -4.5px;

js/chatbot/Index.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
export let root: string;
3333
export let _selectable = false;
3434
export let likeable = false;
35+
export let feedback_options: string[] = ["Like", "Dislike"];
3536
export let show_share_button = false;
3637
export let rtl = false;
3738
export let show_copy_button = true;
@@ -126,6 +127,7 @@
126127
i18n={gradio.i18n}
127128
selectable={_selectable}
128129
{likeable}
130+
{feedback_options}
129131
{show_share_button}
130132
{show_copy_all_button}
131133
value={_value}

js/chatbot/shared/ButtonPanel.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { Retry, Undo, Edit, Check, Clear } from "@gradio/icons";
77
import { IconButtonWrapper, IconButton } from "@gradio/atoms";
88
export let likeable: boolean;
9+
export let feedback_options: string[];
910
export let show_retry: boolean;
1011
export let show_undo: boolean;
1112
export let show_edit: boolean;
@@ -93,7 +94,7 @@
9394
/>
9495
{/if}
9596
{#if likeable}
96-
<LikeDislike {handle_action} />
97+
<LikeDislike {handle_action} {feedback_options} />
9798
{/if}
9899
{/if}
99100
</IconButtonWrapper>

js/chatbot/shared/ChatBot.svelte

+11-3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
export let generating = false;
6868
export let selectable = false;
6969
export let likeable = false;
70+
export let feedback_options: string[];
7071
export let editable: "user" | "all" | null = null;
7172
export let show_share_button = false;
7273
export let show_copy_all_button = false;
@@ -202,11 +203,17 @@
202203
value: edit_message
203204
});
204205
} else {
206+
let feedback =
207+
selected === "like"
208+
? true
209+
: selected === "dislike"
210+
? false
211+
: selected?.substring(9); // remove "feedback:" prefix
205212
if (msg_format === "tuples") {
206213
dispatch("like", {
207214
index: message.index,
208215
value: message.content,
209-
liked: selected === "like"
216+
liked: feedback
210217
});
211218
} else {
212219
if (!groupedMessages) return;
@@ -218,9 +225,9 @@
218225
];
219226
220227
dispatch("like", {
221-
index: [first.index, last.index] as [number, number],
228+
index: first.index as number,
222229
value: message_group.map((m) => m.content),
223-
liked: selected === "like"
230+
liked: feedback
224231
});
225232
}
226233
}
@@ -301,6 +308,7 @@
301308
{_components}
302309
{generating}
303310
{msg_format}
311+
{feedback_options}
304312
show_like={role === "user" ? likeable && like_user_message : likeable}
305313
show_retry={_retryable && is_last_bot_message(messages, value)}
306314
show_undo={_undoable && is_last_bot_message(messages, value)}

js/chatbot/shared/Flag.svelte

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<svg
2+
id="icon"
3+
xmlns="http://www.w3.org/2000/svg"
4+
viewBox="0 0 32 32"
5+
fill="none"
6+
><path
7+
fill="currentColor"
8+
d="M6,30H4V2H28l-5.8,9L28,20H6ZM6,18H24.33L19.8,11l4.53-7H6Z"
9+
/></svg
10+
>

0 commit comments

Comments
 (0)