Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Streamlit Chat Migration Guide #5642

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,5 @@ doc/reference/*
.config/code-server/*
.conda/*
.jupyter/*
app_panel.py
app_streamlit.py
Binary file added doc/_static/images/panel_chat_entry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/_static/images/panel_chat_input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/_static/images/streamlit_chat_input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/_static/images/streamlit_chat_message.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
'global',
'indicators',
'widgets',
'chat',
],
'titles': {
'Vega': 'Altair & Vega',
Expand Down
352 changes: 352 additions & 0 deletions doc/how_to/streamlit_migration/chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
# Create Chat Interfaces
Copy link
Collaborator Author

@MarcSkovMadsen MarcSkovMadsen Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asset for upload

streamlit-status

panel-status


Both Streamlit and Panel provides special components to help you build conversational apps.

| Streamlit | Panel | Description |
| -------------------- | ------------------- | -------------------------------------- |
| [`chat_message`](https://docs.streamlit.io/library/api-reference/chat/st.chat_message) | [`ChatEntry`](../../../examples/reference/chat/ChatEntry.ipynb) | Display a chat message |
| [`chat_input`](https://docs.streamlit.io/library/api-reference/chat/st.chat_input) | | Input a chat message |
| [`status`](https://docs.streamlit.io/library/api-reference/status/st.status) | | Display the output of long-running tasks in a container |
| | [`ChatFeed`](../../../examples/reference/chat/ChatFeed.ipynb) | Display multiple chat messages |
| | [`ChatInterface`](../../../examples/reference/chat/ChatInterface.ipynb) | High-level, easy to use chat interface |
| [`StreamlitCallbackHandler`](https://python.langchain.com/docs/integrations/callbacks/streamlit) | [`PanelCallbackHandler`](../../../examples/reference/chat/ChatInterface.ipynb) | Display the thoughts and actions of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
| [`StreamlitChatMessageHistory`](https://python.langchain.com/docs/integrations/memory/streamlit_chat_message_history) | | Persist the memory of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |

The starting point for most Panel users is the *high-level* [`ChatInterface`](../../../examples/reference/chat/ChatInterface.ipyn), not the *low-level* [`ChatEntry`](../../../examples/reference/chat/ChatEntry.ipynb) and [`ChatFeed`](../../../examples/reference/chat/ChatFeed.ipynb) components.

For inspiration check out the many chat components and examples at [panel-chat-examples](https://holoviz-topics.github.io/panel-chat-examples/).

## Chat Message

Lets see how-to migrate an app that is using `st.chat_message`.

### Streamlit Chat Message Example

```python
import streamlit as st

with st.chat_message("user"):
st.image("https://streamlit.io/images/brand/streamlit-logo-primary-colormark-darktext.png")
st.write("# A faster way to build and share data apps")
```

![Streamlit chat_entry](../../_static/images/streamlit_chat_message.png)

### Panel Chat Message Example

```python
import panel as pn

pn.extension(design="material")

message = pn.Column(
"https://panel.holoviz.org/_images/logo_horizontal_light_theme.png",
"# The powerful data exploration & web app framework for Python"
)
pn.chat.ChatEntry(value=message, user="user").servable()
```

![Panel ChatEntry](../../_static/images/panel_chat_entry.png)

## Chat Input

Lets see how-to migrate an app that is using `st.chat_input`.

### Streamlit Chat Input

```python
import streamlit as st

prompt = st.chat_input("Say something")
if prompt:
st.write(f"User has sent the following prompt: {prompt}")
```

![Streamlit chat_input](../../_static/images/streamlit_chat_input.png)

### Panel Chat Input

Panel does not provide a dedicated *chat input* component because it is built into Panels high-level `ChatInterface`.

Below we will show you how to build and use a custom `ChatInput` widget.

```python
Copy link
Collaborator Author

@MarcSkovMadsen MarcSkovMadsen Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a component like this to Panels chat components @ahuang11 and @philippjfr ?

Copy link
Collaborator Author

@MarcSkovMadsen MarcSkovMadsen Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This custom Viewer component also shows some of the challenges it has.

  • The component is not and cannot be used as a widget. I.e. you need to pn.bind to and pn.depends on chat_input.param.value not chat_input. There is no .from_param method etc.
  • It takes a lot of boiler plate code to separate the viewer params from the _layout params. And getting sizing_mode, width, ... properly linked up to the _layout is almost impossible.

I.e. you can make custom components using Viewer. But I can't see how you can create something that Panel would be willing to include as a proper widget without really a lot of code.

import param

import panel as pn

pn.extension(design="material")


class ChatInput(pn.viewable.Viewer):
value = param.String()

disabled = param.Boolean()
max_length = param.Integer(default=5000)
placeholder = param.String("Send a message")

def __init__(self, **params):
layout_params = {
key: value
for key, value in params.items()
if not key in ["value", "placeholder", "disabled", "max_length"]
}
params = {
key: value for key, value in params.items() if key not in layout_params
}

super().__init__(**params)

self._text_input = pn.widgets.TextInput(
align="center",
disabled=self.param.disabled,
max_length=self.param.max_length,
name="Message",
placeholder=self.param.placeholder,
sizing_mode="stretch_width",
)
self._submit_button = pn.widgets.Button(
align="center",
disabled=self.param.disabled,
icon="send",
margin=(18, 5, 10, 0),
name="",
sizing_mode="fixed",
)
pn.bind(
self._update_value,
value=self._text_input,
event=self._submit_button,
watch=True,
)

self._layout = pn.Row(
self._text_input, self._submit_button, align="center", **layout_params
)

def __panel__(self):
return self._layout

def _update_value(self, value, event):
self.value = value or self.value
self._text_input.value = ""
```

Let us use the custom `ChatInput` widget.

```Python
chat_input = ChatInput(placeholder="Say something")


@pn.depends(chat_input.param.value)
def message(prompt):
if not prompt:
return ""
return f"User has sent the following prompt: {prompt}"


pn.Column(message, chat_input, margin=50).servable()
```

![Panel ChatInput](../../_static/images/panel_chat_input.png)

## Chat Status

Lets see how-to migrate an app that is using `st.status`.

### Streamlit Chat Status

```python
import time
import streamlit as st

with st.status("Downloading data...", expanded=True):
st.write("Searching for data...")
time.sleep(1.5)
st.write("Downloading data...")
time.sleep(1.5)
st.write("Validating data...")
time.sleep(1.5)

st.button("Run")
```

![Streamlit status](https://user-images.githubusercontent.com/42288570/275434382-992f352f-676a-4167-aad0-1fcc2745c130.gif)

### Panel Chat Status

Panel does not provide a dedicated *status* component. Instead it is built into Panels high-level `ChatInterface` as well as provided by a long list of alternative Panel [*indicators*](https://panel.holoviz.org/reference/index.html#indicators).

Below we will show you how to build and use a custom `Status` indicator.

```python
import time
Copy link
Collaborator Author

@MarcSkovMadsen MarcSkovMadsen Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a Status component like this to Panel?

If yes, we should consider How we want to show this status if we have not yet started a run. Should it be disabled, not visible or have some custom look?

The current implementation accepts str steps only. That is probably not what we want if we add it to Panel. It should probably accept as a minimum Markdown and at best any Panel component.

The steps should probably not be constant.

Copy link
Collaborator Author

@MarcSkovMadsen MarcSkovMadsen Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe object and objects would be more in line with Panels naming convention than step and steps?


from contextlib import contextmanager

import param

import panel as pn

from panel.widgets.indicators import LoadingSpinner

pn.extension(design="material")

COLORS = {
"running": "green",
"complete": "black",
"error": "red",
"next": "lightgray",
}


class Status(pn.viewable.Viewer):
value = param.Selector(default="complete", objects=["running", "complete", "error"])
title = param.String()

bgcolor = param.ObjectSelector(
default=LoadingSpinner.param.bgcolor.default,
objects=LoadingSpinner.param.bgcolor.objects,
)
color = param.ObjectSelector(
default="success", objects=LoadingSpinner.param.color.objects
)
collapsed = param.Boolean(default=True)

steps = param.List(constant=True)
step = param.Parameter(constant=True)

def __init__(self, title: str, **params):
params["title"] = title
params["steps"] = params.get("steps", [])
layout_params = {
key: value
for key, value in params.items()
if not key
in ["value", "title", "collapsed", "bgcolor", "color", "steps", "step"]
}
params = {
key: value for key, value in params.items() if key not in layout_params
}
super().__init__(**params)

self._indicators = {
"running": pn.indicators.LoadingSpinner(
value=True,
color=self.param.color,
bgcolor=self.param.bgcolor,
size=25,
# margin=(15, 0, 0, 0),
),
"complete": "✔️",
"error": "❌",
}

self._title_pane = pn.pane.Markdown(self.param.title, align="center")
self._header_row = pn.Row(
pn.panel(self._indicator, sizing_mode="fixed", width=40, align="center"),
self._title_pane,
sizing_mode="stretch_width",
margin=(0, 5),
)
self._details_pane = pn.pane.HTML(
self._details, margin=(10, 5, 10, 55), sizing_mode="stretch_width"
)
self._layout = pn.Card(
self._details_pane,
header=self._header_row,
collapsed=self.param.collapsed,
**layout_params,
)

def __panel__(self):
return self._layout

@param.depends("value")
def _indicator(self):
return self._indicators[self.value]

@property
def _step_color(self):
return COLORS[self.value]

def _step_index(self):
if self.step not in self.steps:
return 0
return self.steps.index(self.step)

@param.depends("step", "value")
def _details(self):
steps = self.steps

if not steps:
return ""

index = self._step_index()

html = ""
for step in steps[:index]:
html += f"<div style='color:{COLORS['complete']}'>{step}</div>"
step = steps[index]
html += f"<div style='color:{self._step_color}'>{step}</div>"
for step in steps[index + 1 :]:
html += f"<div style='color:{COLORS['next']};'>{step}</div>"

return html

def progress(self, step: str):
with param.edit_constant(self):
self.value = "running"
if not step in self.steps:
self.steps = self.steps + [step]
self.step = step

def reset(self):
with param.edit_constant(self):
self.steps = []
self.value = self.param.value.default

def start(self):
with param.edit_constant(self):
self.step = None
self.value = "running"

def complete(self):
self.value = "complete"

@contextmanager
def report(self):
self.start()
try:
yield self.progress
except Exception as ex:
self.value = "error"
else:
self.complete()
```

Let us use the custom `Status` indicator.

```python
status = Status("Downloading data...", collapsed=False, sizing_mode="stretch_width")


def run(_):
with status.report() as progress:
progress("Searching for data...")
time.sleep(1.5)
progress("Downloading data...")
time.sleep(1.5)
progress("Validating data...")
time.sleep(1.5)


run_button = pn.widgets.Button(name="Run", on_click=run)

pn.Column(
status,
run_button,
).servable()
```

![Panel Status](https://user-images.githubusercontent.com/42288570/275440464-5a610fd8-b1c9-4c1e-8f5e-c9a9f407bc36.gif)
7 changes: 7 additions & 0 deletions doc/how_to/streamlit_migration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ How to improve the performance with caching
How to store state for a session
:::

:::{grid-item-card} {octicon}`dependabottack;2.5em;sd-mr-1 sd-animate-grow50` Chat Interfaces
:link: chat
:link-type: doc

How to create create chat interfaces
:::

:::{grid-item-card} {octicon}`stack;2.5em;sd-mr-1 sd-animate-grow50` Multi Page Apps
:link: multipage_apps
:link-type: doc
Expand Down
Loading