Streamlit refresh whole page due to `query = st.chat_input()`

I am writing a chatbot following the docs on coversational app : Build conversational apps - Streamlit Docs

When running the below code, the log “End of chatbot” will be printed twice , and the title would flash twice too.
I tried to comment out code line by line, it seems query = st.chat_input("Ask me questions?") would cause the promblem, but i don’t know why

My code skeleton is like:

import streamlit_authenticator as stauth
import streamlit as st
import yaml
from yaml.loader import SafeLoader

def authenticate(authenticator):
    # authentication_status: True, False, None
    # user: the registered name
    # username: the nick name, this is the unique key
    auth_status = st.session_state.get("authentication_status")
    if auth_status:
        with st.sidebar:
            st.write(f'Welcome *{st.session_state["name"]}*')
        authenticator.logout('Logout', 'sidebar', key='unique_key')
        return True, st.session_state["username"]
    elif auth_status is False:
        st.error('Username or password is incorrect, have you registered?')
    elif auth_status is None:
        #st.write('Please register or login')
        pass

    return False, None

def render_sign_in_or_up(config, authenticator):
    sign_in, sign_up = st.tabs(['Sign In', 'Sign Up'])
    with sign_in:
        render_sign_in(config, authenticator)

    with sign_up:
        render_sign_up(config, authenticator)

def render_sign_in(config, authenticator):
   authenticator.login('Login', 'main')

def render_sign_up(config, authenticator):
    try:
        if authenticator.register_user('Register user', preauthorization=False):
            st.success('User registered successfully')
            write_config(config) # the newly registered user info is in config
    except Exception as e:
        st.error(e)

CONFIG_FILE = '/tmp/config.yaml'
def auth(): 
    with open(CONFIG_FILE) as file_:
        config = yaml.load(file_, Loader=SafeLoader)

    authenticator = stauth.Authenticate(
        config['credentials'],
        config['cookie']['name'],
        config['cookie']['key'],
        config['cookie']['expiry_days'],
        config['preauthorized']
    )
    success, username = authenticate(authenticator)
    if not success:
        render_sign_in_or_up(config, authenticator)
        return None
    else:
        return username

def render_client_side_chat_history(user_id):
    if "messages" not in st.session_state:
        st.session_state.messages = {}

    if user_id not in st.session_state.messages:
        st.session_state.messages[user_id] = []
    
    for message in st.session_state.messages[user_id]:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])
    
def render_user_query(query, user_id):
    st.session_state.messages[user_id].append({"role": "user", "content": query})
    with st.chat_message("user"):
        st.markdown(query)

def build_chain():
   # just return the langchain Chain, nothing related to streamlit
   return None

def get_render_bot_answer(chain,  user_id, query):
  with st.chat_message("assistant"):
          bot_response = 'LLM response here' # use the chain to run the query in LLM
          st.markdown(bot_response)
  st.session_state.messages[user_id].append({"role": "assistant", "content": bot_response})

def chatbot(user_id):
    st.title("Chat with me")
    chain = build_chain() # wrapper on a langchain chain
    render_client_side_chat_history(user_id)

    query = st.chat_input("Ask me questions?")
    if query:
         render_user_query(query, user_id)
         get_render_bot_answer(chain, query)
    print("End of chatbot")

user_id = auth()
if user_id:
    chatbot(user_id)

The config.yaml used in code is like:

credentials:
  usernames:
    jsmith:
      email: jsmith@gmail.com
      name: John Smith
      password: abc # To be replaced with hashed password
    rbriggs:
      email: rbriggs@gmail.com
      name: Rebecca Briggs
      password: def # To be replaced with hashed password
cookie:
  expiry_days: 30
  key: random_signature_key # Must be string
  name: random_cookie_name
preauthorized:
  emails:
  - melsby@gmail.com

From the “Get started” section in the docs:

Streamlit reruns your entire Python script from top to bottom […] Whenever a user interacts with widgets in the app. For example, when dragging a slider, entering text in an input box, or clicking a button.

Well, but I tried the demo before, it seems the title will not reflesh when i input something? The user experience is bad: you input something, the whole page refresh, with a moment of blank, then everything shows up again, plus the text just inputed…

There can be multiple reasons but here are a few I can think of:

  • Because you are using st.session_state to store the messages. Streamlit will rerun the app whenever the session state changes, so you might want to use a different way to store the messages, such as a global variable or a database.
  • Another reason why the page might refresh twice is because you are using st.chat_input inside a function. Streamlit will rerun the function whenever the chat input changes, so you might want to move the chat input outside of the function or use st.experimental_memo to cache the function output.
  • A third reason why the page might refresh twice is because you are using st.write to print the log message. Streamlit will rerun the app whenever st.write is called, so you might want to use st.echo or st.code instead.

Not sure what you mean by that. The demo script reruns on each user interaction too.

Hi, Goyo. The demo would also rerun the whole script when chat_input receives new content. But the UI effect is not the same: the demo would just refresh the dialogue part and shows up the new input, while my code would cause the whole UI reloaded, which visually causing everything including the title refreshing, leaving the page for a moment of blank

Hi, mathcuber. Thank you for your suggestion.

  • using st.session_state to store the messages is what is done in the official demo, I just follow its practice.

  • The script will reload twice when I run the streamlit app. At that time, the chat input hasn’t receive user input yet. That’s weird part. I tried to also comment out the whole authentication logic, it seems the load twice problem can be fixed too. But since the authentication logic is done before we enter the main dialogue loading, I don’t know why that could affect anyway

Well, it is expected that different pieces of code have different behavior. Unfortunately your code has too many undefined names, so I cannot run it and see what it does.

Hi, Goyo, I’ve updated a poc code skeleton that could run in the question text, it will print ‘End of chatbot’ twice when you run it.

I think that is because, on each rerun, you are reading and parsing the config file, instantiating stauth.Authenticate and calling its login method.

That makes little sense and it is probably triggering additional reruns and otherwise harming performance.

Hi, Goyo. My purpose is authenticate user before enter the chatbot dialogue, and streamlit will load the whole logic everytime, that is not what I wanted.

Your code is at odds with your purpose, it is executing the authentication dance on each rerun. If the user is already authenticated you don’t need to do it again.

well, this is what I struggle to achieve.

streamlit run run the script from top to bottom, how can I make the auth logic run only once? Thanks.

You usually use an if statement for that, just as you did to call render_sign_in_or_up() only when it makes sense. Maybe something like this:

if user_already_authenticated:
    do_the_interesting_stuff()
else:
    perform_authentication()

Thanks Goyo, I add the short-cut test in auth() to test if the st.session_state[‘authentication_status’] is already True to indicate wheter user is already authenticated, and the problem got fixed.