Skip to content

Applicable Recipes

Demonstrates how to use Panel's chat components to achieve specific tasks with popular LLM packages.

Openai Chat With Hvplot

We use OpenAI Function Calling and hvPlot to create an advanced chatbot that can create plots.

Source code for openai_chat_with_hvplot.py
"""
We use [OpenAI *Function Calling*](https://platform.openai.com/docs/guides/function-calling) and
[hvPlot](https://hvplot.holoviz.org/) to create an **advanced chatbot** that can create plots.
"""

import json
from pathlib import Path

import hvplot.pandas  # noqa
import pandas as pd
import panel as pn
from openai import AsyncOpenAI

ROOT = Path(__file__).parent

ACCENT = "#00A67E"
THEME = pn.config.theme
CSS_TO_BE_UPSTREAMED_TO_PANEL = """
a {color: var(--accent-fill-rest) !important;}
a:hover {color: var(--accent-fill-hover) !important;}
div.pn-wrapper{height: calc(100% - 25px)}
#sidebar {padding-left: 5px;background: var(--neutral-fill-active)}
"""

JSON_THEME = "light"

MODEL = "gpt-3.5-turbo-1106"
CHAT_GPT_LOGO = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/ChatGPT_logo.svg/512px-ChatGPT_logo.svg.png"
CHAT_GPT_URL = "https://chat.openai.com/"
HVPLOT_LOGO = "https://holoviz.org/assets/hvplot.png"
PANEL_LOGO = {
    "default": "https://panel.holoviz.org/_static/logo_horizontal_light_theme.png",
    "dark": "https://panel.holoviz.org/_static/logo_horizontal_dark_theme.png",
}
PANEL_URL = "https://panel.holoviz.org/index.html"

pn.chat.message.DEFAULT_AVATARS["assistant"] = HVPLOT_LOGO
pn.chat.ChatMessage.show_reaction_icons = False


@pn.cache
def _read_data():
    return pd.read_csv(
        "https://raw.githubusercontent.com/kirenz/datasets/master/gapminder.csv"
    )


DATA = _read_data()


@pn.cache
def _read_tool(name: str) -> dict:
    # See https://json-schema.org/learn/glossary
    with open(ROOT / f"tool_{name}.json", encoding="utf8") as file:
        return json.load(file)


TOOLS_MAP = {"hvplot": _read_tool("hvplot"), "renderer": _read_tool("renderer")}
TOOLS = list(TOOLS_MAP.values())

HVPLOT_ARGUMENTS = (
    "`"
    + "`, `".join(sorted(TOOLS_MAP["hvplot"]["function"]["parameters"]["properties"]))
    + "`"
)
EXPLANATION = f"""
## hvPlot by HoloViz
---

`hvPlot` is a high-level plotting library that that works almost in the same way as \
the well known `Pandas` `.plot` method.

The `.hvplot` method supports more data backends, plotting backends and provides more \
features than the `.plot` method.

## OpenAI GPT with Tools
---

We are using the OpenAI `{MODEL}` model with the `hvplot` and `renderer` *tools*.

You can refer to the following `hvplot` arguments

- {HVPLOT_ARGUMENTS}

and `renderer` arguments

- `backend`
"""

SYSTEM_PROMPT = """\
You are now a **Plotting Assistant** that helps users plot their data using `hvPlot` \
by `HoloViz`.\
"""

DATA_PROMPT = f"""\
Hi. Here is a description of your `data`.

The type is `{DATA.__class__.__name__}`. The `dtypes` are

```bash
{DATA.dtypes}
```"""

pn.extension(raw_css=[CSS_TO_BE_UPSTREAMED_TO_PANEL])

tools_pane = pn.pane.JSON(
    object=TOOLS, depth=6, theme=JSON_THEME, name="Tools", sizing_mode="stretch_both"
)
tabs_layout = pn.Tabs(
    pn.Column(name="Plot"),
    tools_pane,
    pn.Column(name="Arguments"),
    sizing_mode="stretch_both",
    styles={"border-left": "2px solid var(--neutral-fill-active)"},
    dynamic=True,
)


def _powered_by():
    """Returns a component describing the frameworks powering the chat ui"""
    params = {"height": 50, "sizing_mode": "fixed", "margin": (10, 10)}
    return pn.Column(
        pn.Row(
            pn.pane.Image(CHAT_GPT_LOGO, **params),
            pn.pane.Image(HVPLOT_LOGO, **params),
        ),
        sizing_mode="stretch_width",
    )


def _to_code(kwargs):
    """Returns the .hvplot code corresponding to the kwargs"""
    code = "data.hvplot("
    if kwargs:
        code += "\n"
    for key, value in kwargs.items():
        code += f"    {key}={repr(value)},\n"
    code += ")"
    return code


def _update_tool_kwargs(tool_calls, original_kwargs):
    if tool_calls:
        for tool_call in tool_calls:
            name = tool_call.function.name
            kwargs = json.loads(tool_call.function.arguments)
            if kwargs:
                # the llm does not always specify both the hvplot and renderer args
                # if not is specified its most natural to assume we continue with the
                # same args as before
                original_kwargs[name] = kwargs


def _clean_tool_kwargs(kwargs):
    # Sometimes the llm adds the backend argument to the hvplot arguments
    backend = kwargs["hvplot"].pop("backend", None)
    if backend and "backend" not in kwargs["renderer"]:
        # We add the backend argument to the renderer if none is specified
        kwargs["renderer"]["backend"] = backend
    # Use responsive by default
    if "responsive" not in kwargs:
        kwargs["hvplot"]["responsive"] = True


client = AsyncOpenAI()
tool_kwargs = {"hvplot": {}, "renderer": {}}


async def callback(
    contents: str, user: str, instance
):  # pylint: disable=unused-argument
    """Responds to a task"""
    messages = instance.serialize()
    response = await client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=TOOLS,
        tool_choice="auto",
    )
    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls

    _update_tool_kwargs(tool_calls, tool_kwargs)
    _clean_tool_kwargs(tool_kwargs)
    code = _to_code(tool_kwargs["hvplot"])

    response = f"Try running\n```python\n{code}\n```\n"
    chat_interface.send(response, user="Assistant", respond=False)
    plot = DATA.hvplot(**tool_kwargs["hvplot"])
    pane = pn.pane.HoloViews(
        object=plot, sizing_mode="stretch_both", name="Plot", **tool_kwargs["renderer"]
    )
    arguments = pn.pane.JSON(
        tool_kwargs,
        sizing_mode="stretch_both",
        depth=3,
        theme=JSON_THEME,
        name="Arguments",
    )
    tabs_layout[:] = [pane, tools_pane, arguments]


chat_interface = pn.chat.ChatInterface(
    callback=callback,
    show_rerun=False,
    show_undo=False,
    show_clear=False,
    callback_exception="verbose",
)
chat_interface.send(
    SYSTEM_PROMPT,
    user="System",
    respond=False,
)
chat_interface.send(
    DATA_PROMPT,
    user="Assistant",
    respond=False,
)


component = pn.Row(chat_interface, tabs_layout, sizing_mode="stretch_both")

pn.template.FastListTemplate(
    title="Chat with hvPlot",
    sidebar=[
        _powered_by(),
        EXPLANATION,
    ],
    main=[component],
    main_layout=None,
    accent=ACCENT,
).servable()

Openai Images Dall E

Use the OpenAI API to generate images using the DALL·E model.

Source code for openai_images_dall_e.py
"""
Use the OpenAI API to generate images using the DALL·E model.
"""

import panel as pn
from openai import AsyncOpenAI

pn.extension()


async def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    if api_key_input.value:
        # use api_key_input.value if set, otherwise use OPENAI_API_KEY
        aclient.api_key = api_key_input.value

    response = await aclient.images.generate(
        model=model_buttons.value,
        prompt=contents,
        n=n_images_slider.value,
        size=size_buttons.value,
    )

    image_panes = [
        (str(i), pn.pane.Image(data.url)) for i, data in enumerate(response.data)
    ]
    return pn.Tabs(*image_panes) if len(image_panes) > 1 else image_panes[0][1]


def update_model_params(model):
    if model == "dall-e-2":
        size_buttons.param.update(
            options=["256x256", "512x512", "1024x1024"],
            value="256x256",
        )
        n_images_slider.param.update(
            start=1,
            end=10,
            value=1,
        )
    else:
        size_buttons.param.update(
            options=["1024x1024", "1024x1792", "1792x1024"],
            value="1024x1024",
        )
        n_images_slider.param.update(
            start=1,
            end=1,
            value=1,
        )


aclient = AsyncOpenAI()
api_key_input = pn.widgets.PasswordInput(
    placeholder="sk-... uses $OPENAI_API_KEY if not set",
    sizing_mode="stretch_width",
    styles={"color": "black"},
)
model_buttons = pn.widgets.RadioButtonGroup(
    options=["dall-e-2", "dall-e-3"],
    value="dall-e-2",
    name="Model",
    sizing_mode="stretch_width",
)
size_buttons = pn.widgets.RadioButtonGroup(
    options=["256x256", "512x512", "1024x1024"],
    name="Size",
    sizing_mode="stretch_width",
)
n_images_slider = pn.widgets.IntSlider(
    start=1, end=10, value=1, name="Number of images"
)
pn.bind(update_model_params, model_buttons, watch=True)
chat_interface = pn.chat.ChatInterface(
    callback=callback,
    callback_user="DALL·E",
    help_text="Send a message to get a reply from DALL·E!",
)
template = pn.template.BootstrapTemplate(
    title="OpenAI DALL·E",
    header_background="#212121",
    main=[chat_interface],
    header=[api_key_input],
    sidebar=[model_buttons, size_buttons, n_images_slider],
)
template.servable()

Openai Two Bots

Demonstrates how to use the ChatInterface to create two bots that chat with each other.

Highlights:

  • The user decides the callback user and avatar for the response.
  • A system message is used to control the conversation flow.
Source code for openai_two_bots.py
"""
Demonstrates how to use the `ChatInterface` to create two bots that chat with each
other.

Highlights:

- The user decides the callback user and avatar for the response.
- A system message is used to control the conversation flow.
"""

import panel as pn
from openai import AsyncOpenAI

pn.extension()


async def callback(
    contents: str,
    user: str,
    instance: pn.chat.ChatInterface,
):
    if user in ["User", "Happy Bot"]:
        callback_user = "Nerd Bot"
        callback_avatar = "🤓"
    elif user == "Nerd Bot":
        callback_user = "Happy Bot"
        callback_avatar = "😃"

    if len(instance.objects) % 6 == 0:  # stop at every 6 messages
        instance.send(
            "That's it for now! Thanks for chatting!", user="System", respond=False
        )
        return

    prompt = f"Reply profoundly about '{contents}', then follow up with a question."
    messages = [{"role": "user", "content": prompt}]
    response = await aclient.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
        stream=True,
        max_tokens=250,
        temperature=0.1,
    )

    message = ""
    async for chunk in response:
        part = chunk.choices[0].delta.content
        if part is not None:
            message += part
            yield {"user": callback_user, "avatar": callback_avatar, "object": message}

    instance.respond()


aclient = AsyncOpenAI()
chat_interface = pn.chat.ChatInterface(
    callback=callback,
    help_text="Enter a topic for the bots to discuss! Beware the token usage!",
)
chat_interface.servable()

Langchain Chat With Pandas

Demonstrates how to use the ChatInterface and PanelCallbackHandler to create a chatbot to talk to your Pandas DataFrame. This is heavily inspired by the LangChain chat_pandas_df Reference Example.

Source code for langchain_chat_with_pandas.py
"""
Demonstrates how to use the `ChatInterface` and `PanelCallbackHandler` to create a
chatbot to talk to your Pandas DataFrame. This is heavily inspired by the
[LangChain `chat_pandas_df` Reference Example](https://github.com/langchain-ai/streamlit-agent/blob/main/streamlit_agent/chat_pandas_df.py).
"""

from __future__ import annotations

from pathlib import Path
from textwrap import dedent

import pandas as pd
import panel as pn
import param
import requests
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI
from langchain_experimental.agents.agent_toolkits import create_pandas_dataframe_agent

pn.extension("perspective")

PENGUINS_URL = (
    "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv"
)
PENGUINS_PATH = Path(__file__).parent / "penguins.csv"
if not PENGUINS_PATH.exists():
    response = requests.get(PENGUINS_URL)
    PENGUINS_PATH.write_text(response.text)

FILE_DOWNLOAD_STYLE = """
.bk-btn a {
    padding: 0px;
}
.bk-btn-group > button, .bk-input-group > button {
    font-size: small;
}
"""


class AgentConfig(param.Parameterized):
    """Configuration used for the Pandas Agent"""

    user = param.String("Pandas Agent")
    avatar = param.String("🐼")

    show_chain_of_thought = param.Boolean(default=False)

    def _get_agent_message(self, message: str) -> pn.chat.ChatMessage:
        return pn.chat.ChatMessage(message, user=self.user, avatar=self.avatar)


class AppState(param.Parameterized):
    data = param.DataFrame()

    llm = param.Parameter(constant=True)
    pandas_df_agent = param.Parameter(constant=True)

    config: AgentConfig = param.ClassSelector(class_=AgentConfig)

    def __init__(self, config: AgentConfig | None = None):
        if not config:
            config = AgentConfig()

        super().__init__(config=config)
        with param.edit_constant(self):
            self.llm = ChatOpenAI(
                temperature=0,
                model="gpt-3.5-turbo-0613",
                streaming=True,
            )

    @param.depends("llm", "data", on_init=True, watch=True)
    def _reset_pandas_df_agent(self):
        with param.edit_constant(self):
            if not self.error_message:
                self.pandas_df_agent = create_pandas_dataframe_agent(
                    self.llm,
                    self.data,
                    verbose=True,
                    agent_type=AgentType.OPENAI_FUNCTIONS,
                    handle_parsing_errors=True,
                )
            else:
                self.pandas_df_agent = None

    @property
    def error_message(self):
        if not self.llm and self.data is None:
            return "Please **upload a `.csv` file** and click the **send** button."
        if self.data is None:
            return "Please **upload a `.csv` file** and click the **send** button."
        return ""

    @property
    def welcome_message(self):
        return dedent(
            f"""
            I'm your <a href="\
            https://python.langchain.com/docs/integrations/toolkits/pandas" \
            target="_blank">LangChain Pandas DataFrame Agent</a>.

            I execute LLM generated Python code under the hood - this can be bad if
            the `llm` generated Python code is harmful. Use cautiously!

            {self.error_message}"""
        ).strip()

    async def callback(self, contents, user, instance):
        if isinstance(contents, pd.DataFrame):
            self.data = contents
            instance.active = 1
            message = self.config._get_agent_message(
                "You can ask me anything about the data. For example "
                "'how many species are there?'"
            )
            return message

        if self.error_message:
            message = self.config._get_agent_message(self.error_message)
            return message

        if self.config.show_chain_of_thought:
            langchain_callbacks = [
                pn.chat.langchain.PanelCallbackHandler(instance=instance)
            ]
        else:
            langchain_callbacks = []

        response = await self.pandas_df_agent.arun(
            contents, callbacks=langchain_callbacks
        )
        message = self.config._get_agent_message(response)
        return message


state = AppState()

chat_interface = pn.chat.ChatInterface(
    widgets=[
        pn.widgets.FileInput(name="Upload", accept=".csv"),
        pn.chat.ChatAreaInput(name="Message", placeholder="Send a message"),
    ],
    renderers=pn.pane.Perspective,
    callback=state.callback,
    callback_exception="verbose",
    show_rerun=False,
    show_undo=False,
    show_clear=False,
    min_height=400,
)
chat_interface.send(
    state.welcome_message,
    user=state.config.user,
    avatar=state.config.avatar,
    respond=False,
)

download_button = pn.widgets.FileDownload(
    PENGUINS_PATH,
    button_type="primary",
    button_style="outline",
    height=30,
    width=335,
    stylesheets=[FILE_DOWNLOAD_STYLE],
)

layout = pn.template.MaterialTemplate(
    title="🦜 LangChain - Chat with Pandas DataFrame",
    main=[chat_interface],
    sidebar=[
        download_button,
        "#### Agent Settings",
        state.config.param.show_chain_of_thought,
    ],
)

layout.servable()

Langchain Chat With Pdf

Demonstrates how to use the ChatInterface to chat about a PDF using OpenAI, LangChain and Chroma.

Source code for langchain_chat_with_pdf.py
"""
Demonstrates how to use the `ChatInterface` to chat about a PDF using
OpenAI, [LangChain](https://python.langchain.com/docs/get_started/introduction) and
[Chroma](https://docs.trychroma.com/).
"""

import os
import tempfile

import panel as pn
from langchain.chains import RetrievalQA
from langchain.document_loaders import PyPDFLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain_community.chat_models import ChatOpenAI

pn.extension()


@pn.cache
def initialize_chain(pdf, k, chain):
    # load document
    with tempfile.NamedTemporaryFile("wb", delete=False) as f:
        f.write(pdf)

    file_name = f.name
    loader = PyPDFLoader(file_name)
    documents = loader.load()
    # split the documents into chunks
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
    texts = text_splitter.split_documents(documents)
    # select which embeddings we want to use
    embeddings = OpenAIEmbeddings()
    # create the vectorestore to use as the index
    db = Chroma.from_documents(texts, embeddings)
    # expose this index in a retriever interface
    retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": k})
    # create a chain to answer questions
    qa = RetrievalQA.from_chain_type(
        llm=ChatOpenAI(),
        chain_type=chain,
        retriever=retriever,
        return_source_documents=True,
        verbose=True,
    )
    return qa


def respond(contents, user, chat_interface):
    chat_input.placeholder = "Ask questions here!"
    if chat_interface.active == 0:
        chat_interface.active = 1
        yield {"user": "OpenAI", "value": "Let's chat about the PDF!"}

        contents.seek(0)
        pn.state.cache["pdf"] = contents.read()
        return

    qa = initialize_chain(pn.state.cache["pdf"], k_slider.value, chain_select.value)
    if key_input.value:
        os.environ["OPENAI_API_KEY"] = key_input.value

    response = qa({"query": contents})
    answers = pn.Accordion(("Response", response["result"]))
    for doc in response["source_documents"][::-1]:
        answers.append((f"Snippet from page {doc.metadata['page']}", doc.page_content))
    answers.active = [0, 1]
    yield {"user": "OpenAI", "value": answers}


# sidebar widgets
key_input = pn.widgets.PasswordInput(
    name="OpenAI Key",
    placeholder="sk-...",
)
k_slider = pn.widgets.IntSlider(
    name="Number of Relevant Chunks", start=1, end=5, step=1, value=2
)
chain_select = pn.widgets.RadioButtonGroup(
    name="Chain Type", options=["stuff", "map_reduce", "refine", "map_rerank"]
)

sidebar = pn.Column(key_input, k_slider, chain_select)

# main widgets
pdf_input = pn.widgets.FileInput(accept=".pdf", value="", height=50)
chat_input = pn.chat.ChatAreaInput(placeholder="First, upload a PDF!")
chat_interface = pn.chat.ChatInterface(
    help_text="Please first upload a PDF and click send!",
    callback=respond,
    sizing_mode="stretch_width",
    widgets=[pdf_input, chat_input],
    callback_exception="verbose",
)
chat_interface.active = 0

# layout
template = pn.template.BootstrapTemplate(sidebar=[sidebar], main=[chat_interface])
template.servable()