{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import asyncio\n", "import panel as pn\n", "\n", "pn.extension()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `ChatFeed` is a mid-level widget, that lets you manage a list of [`ChatEntry`](ChatEntry.ipynb) items.\n", "\n", "This widget provides backend methods to:\n", "- Send (append) messages to the chat log.\n", "- Stream tokens to the latest `ChatEntry` in the chat log.\n", "- Execute callbacks when a user sends a message.\n", "- Undo a number of sent `ChatEntry` objects.\n", "- Clear the chat log of all `ChatEntry` objects.\n", "\n", "See [`ChatInterface`](ChatInterface.ipynb) for a high-level, *easy to use*, *ChatGPT like* interface.\n", "\n", "![Chat Design Specification](../../assets/ChatDesignSpecification.png)\n", "\n", "#### Parameters:\n", "\n", "##### Core\n", "\n", "* **`value`** (`List[ChatEntry]`): The entries added to the chat feed.\n", "* **`renderers`** (List[Callable]): A callable or list of callables that accept the value and return a Panel object to render the value. If a list is provided, will attempt to use the first renderer that does not raise an exception. If None, will attempt to infer the renderer from the value.\n", "* **`callback`** (callable): Callback to execute when a user sends a message or when `respond` is called. The signature must include the previous message value `contents`, the previous `user` name, and the component `instance`.\n", "\n", "##### Styling\n", "\n", "* **`card_params`** (Dict): Parameters to pass to Card, such as `header`, `header_background`, `header_color`, etc.\n", "* **`entry_params`** (Dict): Parameters to pass to each ChatEntry, such as `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, and `show_timestamp`.\n", "\n", "##### Other\n", "\n", "* **`header`** (Any): The header of the chat feed; commonly used for the title. Can be a string, pane, or widget.\n", "* **`callback_user`** (str): The default user name to use for the entry provided by the callback.\n", "* **`callback_avatar`** (str | BinaryIO | pn.pane.ImageBase): The avatar to use for the user. Can be a single character text, an emoji, or anything supported by `pn.pane.Image`. If not set, uses the first character of the name.\n", "* **`placeholder_text`** (any): If placeholder is the default LoadingSpinner, the text to display next to it.\n", "* **`placeholder_threshold`** (float): Min duration in seconds of buffering before displaying the placeholder. If 0, the placeholder will be disabled. Defaults to 0.2.\n", "* **`auto_scroll_limit`** (int): Max pixel distance from the latest object in the Column to activate automatic scrolling upon update. Setting to 0 disables auto-scrolling.\n", "* **`scroll_button_threshold`** (int): Min pixel distance from the latest object in the Column to display the scroll button. Setting to 0 disables the scroll button.\n", "* **`view_latest`** (bool): Whether to scroll to the latest object on init. If not enabled the view will be on the first object. Defaults to True.\n", "\n", "#### Methods\n", "\n", "##### Core\n", "\n", "* **`send`**: Sends a value and creates a new entry in the chat log. If `respond` is `True`, additionally executes the callback, if provided.\n", "* **`stream`**: Streams a token and updates the provided entry, if provided. Otherwise creates a new entry in the chat log, so be sure the returned entry is passed back into the method, e.g. `entry = chat.stream(token, entry=entry)`. This method is primarily for outputs that are not generators--notably LangChain. For most cases, use the send method instead.\n", "\n", "##### Other\n", "\n", "* **`clear`**: Clears the chat log and returns the entries that were cleared.\n", "* **`respond`**: Executes the callback with the latest entry in the chat log.\n", "* **`undo`**: Removes the last `count` of entries from the chat log and returns them. Default `count` is 1.\n", "\n", "___" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`ChatFeed` can be initialized without any arguments." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chat_feed = pn.widgets.ChatFeed()\n", "chat_feed" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can send chat entries with the `send` method." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Hello world!\", user=\"Bot\", avatar=\"B\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `send` method returns a [`ChatEntry`](ChatEntry.ipynb), which can display any object that Panel can display. You can **interact with chat entries** like any other Panel component. You can find examples in the [`ChatEntry` Reference Notebook](ChatEntry.ipynb)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Besides messages of `str` type, the `send` method can also accept `dict`s containing the key `value` and `ChatEntry` objects." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send({\"value\": \"Welcome!\", \"user\": \"Bot\", \"avatar\": \"B\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`avatar` can also accept emojis, paths/URLs to images, and/or file-like objects." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.widgets.ChatFeed(value=[\n", " pn.widgets.ChatEntry(value=\"I'm an emoji!\", avatar=\"🤖\"),\n", " pn.widgets.ChatEntry(value=\"I'm an image!\", avatar=\"https://upload.wikimedia.org/wikipedia/commons/6/63/Yumi_UBports.png\"),\n", "])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note if you provide both the user/avatar in the `dict` and keyword argument, the keyword argument takes precedence." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send({\"value\": \"Overtaken!\", \"user\": \"Bot\"}, user=\"MegaBot\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A `callback` can be attached for a much more interesting `ChatFeed`.\n", "\n", "The signature must include the latest available message value `contents`, the latest available `user` name, and the chat `instance`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def echo_message(contents, user, instance):\n", " return f\"Echoing... {contents}\"\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=echo_message)\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Hello!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Update `callback_user` to change the default name." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chat_feed = pn.widgets.ChatFeed(callback=echo_message, callback_user=\"Echo Bot\")\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Hey!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The specified `callback` can also return a `dict` and `ChatEntry` object, which **must contain** a `value` key, and optionally a `user` and a `avatar` key, that overrides the default `callback_user`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def parrot_message(contents, user, instance):\n", " return {\"value\": f\"No, {contents.lower()}\", \"user\": \"Parrot\", \"avatar\": \"🦜\"}\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=parrot_message, callback_user=\"Echo Bot\")\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Are you a parrot?\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you do not want the callback to be triggered alongside `send`, set `respond=False`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Don't parrot this.\", respond=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can surface exceptions by setting `callback_exception` to `\"summary\"`.\n", "\n", "To see the entire traceback, you can set it to `\"verbose\"`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def bad_callback(contents, user, instance):\n", " return 1 / 0\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=bad_callback, callback_exception=\"summary\")\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chat_feed.send(\"This will fail...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `ChatFeed` also support *async* `callback`s.\n", "\n", "In fact, we recommend using *async* `callback`s whenever possible to keep your app fast and responsive." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "import asyncio\n", "pn.extension()\n", "\n", "async def parrot_message(contents, user, instance):\n", " await asyncio.sleep(2.8)\n", " return {\"value\": f\"No, {contents.lower()}\", \"user\": \"Parrot\", \"avatar\": \"🦜\"}\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=parrot_message, callback_user=\"Echo Bot\")\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Are you a parrot?\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The easiest and most optimal way to stream output is through *async generators*.\n", "\n", "If you're unfamiliar with this term, don't fret!\n", "\n", "It's simply prefixing your function with `async` and replacing `return` with `yield`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "async def stream_message(contents, user, instance):\n", " message = \"\"\n", " for character in contents:\n", " message += character\n", " yield message\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=stream_message)\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Streaming...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also continuously replace the original message if you do not concatenate the characters." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "async def replace_message(contents, user, instance):\n", " for character in contents:\n", " await asyncio.sleep(0.1)\n", " yield character\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=replace_message)\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This works extremely well with OpenAI's `create` and `acreate` functions--just be sure that `stream` is set to `True`!\n", "\n", "```python\n", "import openai\n", "import panel as pn\n", "\n", "pn.extension()\n", "\n", "async def openai_callback(contents, user, instance):\n", " response = await openai.ChatCompletion.acreate(\n", " model=\"gpt-3.5-turbo\",\n", " messages=[{\"role\": \"user\", \"content\": contents}],\n", " stream=True,\n", " )\n", " message = \"\"\n", " async for chunk in response:\n", " message += chunk[\"choices\"][0][\"delta\"].get(\"content\", \"\")\n", " yield message\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=openai_callback)\n", "chat_feed.send(\"Have you heard of HoloViz Panel?\")\n", "```\n", "\n", "![OpenAI ACreate App](https://user-images.githubusercontent.com/42288570/260281672-09da9517-9336-42df-a502-b61530bd89b3.gif)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's also possible to manually trigger the callback with `respond`. This could be useful to achieve a chain of responses from the initial message!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "async def chain_message(contents, user, instance):\n", " await asyncio.sleep(1.8)\n", " if user == \"User\":\n", " yield {\"user\": \"Bot 1\", \"value\": \"Hi User! I'm Bot 1--here to greet you.\"}\n", " instance.respond()\n", " elif user == \"Bot 1\":\n", " yield {\n", " \"user\": \"Bot 2\",\n", " \"value\": \"Hi User; I see that Bot 1 already greeted you; I'm Bot 2.\",\n", " }\n", " instance.respond()\n", " elif user == \"Bot 2\":\n", " yield {\n", " \"user\": \"Bot 3\",\n", " \"value\": \"I'm Bot 3; the last bot that will respond. See ya!\",\n", " }\n", "\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=chain_message)\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = chat_feed.send(\"Hello bots!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It can be fun to watch bots talking to each other. Beware of the token usage!\n", "\n", "```python\n", "import openai\n", "import panel as pn\n", "\n", "pn.extension()\n", "\n", "\n", "async def openai_self_chat(contents, user, instance):\n", " if user == \"User\" or user == \"ChatBot B\":\n", " user = \"ChatBot A\"\n", " avatar = \"https://upload.wikimedia.org/wikipedia/commons/6/63/Yumi_UBports.png\"\n", " elif user == \"ChatBot A\":\n", " user = \"ChatBot B\"\n", " avatar = \"https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Outreachy-bot-avatar.svg/193px-Outreachy-bot-avatar.svg.png\"\n", "\n", " response = await openai.ChatCompletion.acreate(\n", " model=\"gpt-3.5-turbo\",\n", " messages=[{\"role\": \"user\", \"content\": contents}],\n", " temperature=0,\n", " max_tokens=500,\n", " stream=True,\n", " )\n", " message = \"\"\n", " async for chunk in response:\n", " message += chunk[\"choices\"][0][\"delta\"].get(\"content\", \"\")\n", " yield {\"user\": user, \"value\": message, \"avatar\": avatar}\n", " instance.respond()\n", "\n", "\n", "chat_feed = pn.widgets.ChatFeed(callback=openai_self_chat, sizing_mode=\"stretch_width\", height=1000).servable()\n", "chat_feed.send(\"What is HoloViz Panel?\")\n", "```\n", "\n", "![OpenAI Bot Conversation](https://user-images.githubusercontent.com/42288570/260283078-de8f56c2-becc-4566-9813-618bfc81f3c2.gif)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If a returned object is not a generator (notably LangChain output), it's still possible to stream the output with the `stream` method." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chat_feed = pn.widgets.ChatFeed()\n", "\n", "# creates a new entry\n", "entry = chat_feed.stream(\"Hello\", user=\"Aspiring User\", avatar=\"🤓\")\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# streams (appends) to the previous entry\n", "entry = chat_feed.stream(\" World!\", user=\"Aspiring User\", avatar=\"🤓\", entry=entry)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `stream` method is commonly used with for loops." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chat_feed = pn.widgets.ChatFeed()\n", "chat_feed" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry = None\n", "for n in \"123456789 abcdefghijklmnopqrstuvxyz\":\n", " await asyncio.sleep(0.1)\n", " entry = chat_feed.stream(n, entry=entry)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can pass `ChatEntry` params through `entry_params`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "entry_params = dict(\n", " default_avatars={\"System\": \"S\", \"User\": \"👤\"}, reaction_icons={\"like\": \"thumb-up\"}\n", ")\n", "chat_feed = pn.widgets.ChatFeed(entry_params=entry_params)\n", "chat_feed.send(user=\"System\", value=\"This is the System speaking.\")\n", "chat_feed.send(user=\"User\", value=\"This is the User speaking.\")\n", "chat_feed" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can build your own custom chat interface too on top of `ChatFeed`, but remember there's a pre-built [`ChatInterface`](ChatInterface.ipynb)!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "import asyncio\n", "import panel as pn\n", "from panel.widgets.chat import ChatEntry, ChatFeed\n", "\n", "pn.extension()\n", "\n", "\n", "async def get_response(contents, user, instance):\n", " await asyncio.sleep(0.88)\n", " return {\n", " \"Marc\": \"It is 2\",\n", " \"Andrew\": \"It is 4\",\n", " }.get(user, \"I don't know\")\n", "\n", "\n", "ASSISTANT_AVATAR = (\n", " \"https://upload.wikimedia.org/wikipedia/commons/6/63/Yumi_UBports.png\"\n", ")\n", "\n", "chat_feed = ChatFeed(\n", " value=[ChatEntry(user=\"Assistant\", value=\"Hi There!\", avatar=ASSISTANT_AVATAR)],\n", " callback=get_response,\n", " height=500,\n", " entry_params=dict(\n", " default_avatars={\"Assistant\": ASSISTANT_AVATAR},\n", " ),\n", ")\n", "\n", "marc_button = pn.widgets.Button(\n", " name=\"Marc\",\n", " on_click=lambda event: chat_feed.send(\n", " user=\"Marc\", value=\"What is the square root of 4?\", avatar=\"🚴\"\n", " ),\n", " align=\"center\",\n", " disabled=chat_feed.param.disabled,\n", ")\n", "andrew_button = pn.widgets.Button(\n", " name=\"Andrew\",\n", " on_click=lambda event: chat_feed.send(\n", " user=\"Andrew\", value=\"What is the square root of 4 squared?\", avatar=\"🏊\"\n", " ),\n", " align=\"center\",\n", " disabled=chat_feed.param.disabled,\n", ")\n", "undo_button = pn.widgets.Button(\n", " name=\"Undo\",\n", " on_click=lambda event: chat_feed.undo(2),\n", " align=\"center\",\n", " disabled=chat_feed.param.disabled,\n", ")\n", "clear_button = pn.widgets.Button(\n", " name=\"Clear\",\n", " on_click=lambda event: chat_feed.clear(),\n", " align=\"center\",\n", " disabled=chat_feed.param.disabled,\n", ")\n", "\n", "\n", "pn.Column(\n", " chat_feed,\n", " pn.layout.Divider(),\n", " pn.Row(\n", " \"Click a button\",\n", " andrew_button,\n", " marc_button,\n", " undo_button,\n", " clear_button,\n", " ),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For an example on `renderers`, see [ChatInterface](ChatInterface.ipynb)." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }