Frontend does not clear deleted session content

I’ve developed an app with a multi-turn chat interface. Initially, I use a “Restart” button to send an empty request to the server, prompting the bot to initiate a conversation. Users can continue the conversation through an input box, and the ongoing dialogue is displayed in the chat column. The above function works fine.

The “Clear” button functions correctly too, clearing the history immediately as intended.

However, I’m encountering an issue with the “Restart” button:

My goal is for this button to clear the existing conversation history and then resend the initial request to start a new session.

Although as shown in my log, st.session.messages updates correctly, reflecting the cleared state, the conversation history remains visible on the rendered webpage.

Could anyone help resolve this issue where the conversation history doesn’t disappear upon restarting?

import sys
import json
import requests
import streamlit as st

def init_chat_history():
	with chat_column:
		if "messages" in st.session_state:
			for message in st.session_state.messages:
				avatar = '🐼' if message["role"] == "user" else '🤖'
				with st.chat_message(message["role"], avatar=avatar):
					st.markdown(message["content"])
		else:
			st.session_state.messages = []

	return st.session_state.messages

st.set_page_config(page_title='Demo', layout="wide")
st.title('Demo')
chat_column, side_column  = st.columns((0.6, 0.4))

def stream_chat(messages):
	data = {
		'uid': 'web-demo',
		'session_id': "1",
		'messages': messages
	}

	response = requests.post(
		"http://127.0.0.1:7081/stream_chat",
		json=data,
		stream=True)
	answer = ''
	for byte_line in response.iter_lines():
		byte_line = byte_line.rstrip(b'\n')
		if byte_line.startswith(b'data:'):
			data = json.loads(byte_line.decode().lstrip('data:'))
			answer += data['text']
			yield answer

def clear():
	if hasattr(st.session_state, 'messages'):
		del st.session_state.messages

def start():
	if hasattr(st.session_state, 'messages'):
		del st.session_state.messages

	messages = init_chat_history()

	with chat_column:
		with st.chat_message("assistant", avatar='🤖'):
			placeholder = st.empty()
			for response in stream_chat(messages):
				placeholder.markdown(response)

	messages.append({"role": "assistant", "content": response})
	return response


def main():
	messages = init_chat_history()

	st.button("Restart", on_click=start)
	st.button("Clear", on_click=clear)

	if (prompt := st.chat_input("Enter to sent. Shift + Enter to change line.")):
		with chat_column:
			with st.chat_message("user", avatar='🐼'):
				st.markdown(prompt)
			messages.append({"role": "user", "content": prompt})
			with st.chat_message("assistant", avatar='🤖'):
				placeholder = st.empty()
				for response in stream_chat(messages):
					placeholder.markdown(response)
				messages.append({"role": "assistant", "content": response})

if __name__ == "__main__":
	main()

image1: 0.5s after clicking “Restart”, the result is streaming here, leaving the previous turn conversation history uncleaned.

image2: 1s after clicking “Restart”, history is cleaned, but the the bot’s greeting duplicates (actually it is from this new round, not the previous round)

images 3: as the conversation continues, the extra bot’s greeting are replaced by normal conversation.

So image1 and image2 are not what I expected. Image 3 is normal but it only happens after the user gives a query.

The thing is, when I click “Clear”, it seems the logic is same with “Restart”, but it cleans all display instantly as I intended.

After clicking the restart, post the image of what you see.

Thanks, I posted some images above.

Streamlit’s flow of execution is not difficult to follow.

  1. At main(), after the Restart button is clicked, streamlit executes the start() because that is the callback function.
    • At start(), it will delete the messages
    • At start(), it calls init_chat_history(), it prints the contents of messages if there is otherwise it sets the messages to an empty list.
    • At start(), it calls stream_chat without user prompt because the messages is empty but it will print its response. That is the first response that you see in the app.
    • At start(), it saves the response in the messages, take a note on this.
  2. Streamlit then goes to the top of the script start reading import sys and down. This is normal and adheres to the main concept of reading the code from top to bottom and this is part of the flow as a result of hitting that Restart button.
    • It reads import sys
    • It reads import json and so on
    • It does not go inside the function declaration/definition init_chat_history().
    • It reads st.set_page_config(page_title=‘Demo’, layout=“wide”)
    • It reads st.title(…) and so on
    • It does not go inside the function declaration stream_chat().
    • It does not go inside the function declaration clear().
    • It does not go inside the function declaration start().
    • At main(), it executes messages = init_chat_history()
      • at init_chat_history(), it prints the messages, since the messages is no longer empty, it prints the second response we see in the app

That is the end. We see two responses from assistant.

Suggestions to improve code

  • Call st.set_page_config(page_title='Demo', layout="wide") after the imports
  • In init_chat_history() do not print messages, create a dedicated function to print the messages so that the code is easier to follow.
  • Anything in the session state can be accessed anywhere in the app.
    Printing messages is easier. It is not necessary to put the messages as an argument to a function because it is already in the session state.
1 Like

Thanks a lot for your help.

Because if prompt := chat_input() can accept input more than once, the main thread must repeat itself somewhere, which caused what I have seen.

Before diving into the “main concepts” page which I need to learn, I tried the official example of streaming chat, I copied the “Simple Chat” code and added some logic between # # # # # #

It displays the same problem.

In your Suggestions to improve code you have mentioned both practice for solving the problem and for better style of coding, but due to my limited knowledge to streamlit now, I get lost which is the key to fix the problem.

Could you please tell me, based on the official example, how could I add a “Start” button to both clear conversation and send “start” to chatbot API so that I don’t have to manually click “Clear” and then type in “start” in the chat_input() ?

import streamlit as st
import random
import time

# Streamed response emulator
def response_generator():
    response = random.choice(
        [
            "Hello there! How can I assist you today?",
            "Hi, human! Is there anything I can help you with?",
            "Do you need help?",
        ]
    )
    for word in response.split():
        yield word + " "
        time.sleep(0.05)

st.title("Simple chat")

# Initialize chat history
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat messages from history on app rerun
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# # # # # #
def reset_conversation():
  st.session_state.messages = []

st.button('Reset Chat', on_click=reset_conversation)

# For sending initial input
if st.button("Start"):

    reset_conversation()
    prompt = "start"

    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    with st.chat_message("assistant"):
        response = st.write_stream(response_generator())
    st.session_state.messages.append({"role": "assistant", "content": response})
# # # # # #

# For accepting later user input
if prompt := st.chat_input("What is up?"):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    with st.chat_message("assistant"):
        response = st.write_stream(response_generator())
    st.session_state.messages.append({"role": "assistant", "content": response})

The history are not cleaned after clicking “Start”. But clicking “Reset Chat” works fine.

P.S.
Another related problem: If I replace

st.button('Reset Chat', on_click=reset_conversation)

with

if st.button('Reset Chat'):
    reset_conversation()

I have to click the button twice to get conversation cleared.

The difference between

st.button('Reset Chat')

and

st.button('Reset chat', on_click=func)

is that in the former, when clicked, streamlit rerun the code from top to bottom, starting at the top of imports.

While in the latter with on_click if clicked, it will execute first the func or callback function then goes to the top of imports, and down.

Whatever you need to do to get your desired best app’s workflow, use the callback fuction func as much as possible.

Generally using on_click in buttons or any other widgets with callback function feature is better.

That is one of the side-effects of using a button without utilizing the callback function. It depends on the design of app of course.

how about the “Start” button problem. I tried to put it into a callback but it functions the same.

If you do that, after hitting that button, streamlit reruns the code from top to bottom starting at the very top of imports and goes down. By doing that it will execute

# Display chat messages from history on app rerun
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

because it is in its path.

It then executes:

reset_conversation()
prompt = "start"

Too late now, the messages were already printed.

you mean streamlit rerun BEFORE it actually executes the code inside if st.button(“Start”) … ? In that case it explains.

However if I put it into callback like this, it still doesn’t work…

def reset_and_start_conversation():
    st.session_state.messages = []
    prompt = "start"

    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    with st.chat_message("assistant"):
        response = st.write_stream(response_generator())
    st.session_state.messages.append({"role": "assistant", "content": response})

# For sending initial input
st.button("Start-Callback", on_click=reset_and_start_conversation)

That is correct.

Start on this.

	# For sending initial input
	st.button("Start", on_click=start_cb)

It calls the callback start_cb which is:

def start_cb():
    reset_conversation()  # clears messages
    
    # Initiate conversation with bot but do not print it because
	# it will be printed later in the main. Save it though.
    prompt = "start"
    st.session_state.messages.append({"role": "user", "content": prompt})

    # Save yielded messages in a string, do not print it but save it.
    response = ''.join(word for word in response_generator())
    st.session_state.messages.append({"role": "assistant", "content": response})

Organize the code a little bit.

  • imports
  • function definitions
  • main

Full code

import streamlit as st
import random
import time


# Streamed response emulator
def response_generator():
    response = random.choice(
        [
            "Hello there! How can I assist you today?",
            "Hi, human! Is there anything I can help you with?",
            "Do you need help?",
        ]
    )
    for word in response.split():
        yield word + " "
        time.sleep(0.05)
        

def reset_conversation():
    st.session_state.messages = []


def start_cb():
    reset_conversation()  # clears messages
    
    # Initiate conversation with bot but do not print it because
	# it will be printed later in the main. Save it though.
    prompt = "start"
    st.session_state.messages.append({"role": "user", "content": prompt})

    # Save yielded messages in a string, do not print it but save it.
    response = ''.join(word for word in response_generator())
    st.session_state.messages.append({"role": "assistant", "content": response})
    

def main():
	st.title("Simple chat")

	# Initialize chat history
	if "messages" not in st.session_state:
		st.session_state.messages = []

	# Display chat messages from history on app rerun
	for message in st.session_state.messages:
		with st.chat_message(message["role"]):
			st.markdown(message["content"])

	st.button('Reset Chat', on_click=reset_conversation)

	# For sending initial input
	st.button("Start", on_click=start_cb)

	# For accepting later user input
	if prompt := st.chat_input("What is up?"):
		st.session_state.messages.append({"role": "user", "content": prompt})
		with st.chat_message("user"):
			st.markdown(prompt)
		with st.chat_message("assistant"):
			response = st.write_stream(response_generator())
		st.session_state.messages.append({"role": "assistant", "content": response})


if __name__ == '__main__':
	main()
1 Like

Oh your code works! Thank you!! :smile:
I tried my version of using callback but it won’t work. I’ll compare the code and figure out what’s wrong.

1 Like

I figured out why I insisted to show first turn right inside the callback instead of saving it for later. Because I was not familiar with streamlit executing plan and mislead by the prompt := chat_input part at the end.

In the prompt := chat_input part at the end, it was not in callback, so the order is:
display history → display current turn → add current turn to history

In my wrong callback version, it executes like:
clean history → display first turn inside callback → add first turn as history → display history
so it display same content twice.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.