Adding thumbs up/down buttons and user's feedback into Streamlit based chatbot

I am trying to add thumbs up/down buttons and user’s feedback into Streamlit based chatbot.
I use st.chat_message to create chatbot with Streamlit. For thumbs up/down buttons and user’s feedback I use streamlit-feedback python package becouse I did not find any other way to include it into Streamlit based chatbot.

My application code looks like:

import streamlit as st
from streamlit_feedback import streamlit_feedback
...
def handle_feedback():  
    st.write(st.session_state.fb_k)
    st.toast("✔️ Feedback received!")
if "df" in st.session_state:
    if prompt := st.chat_input(placeholder=""):
       ...
       with st.form('form'):
            streamlit_feedback(feedback_type="thumbs",
                                optional_text_label="Enter your feedback here", 
                                align="flex-start", 
                                key='fb_k')
            st.form_submit_button('Save feedback', on_click=handle_feedback)
    

For some reason streamlit_feedback works only inside st.form. It creates two problems:

  1. To get it work user needs first click on “SUBMIT” button and only then to “Save feedback” button.
    image

If user click “Save feedback” without using “SUBMIT” button then st.session_state.fb_k will be None.

  1. Feedback inside st.form does not look very appealing and I am looking to ways to get rid of st.form.

I am looking for a way to resolve those problems with streamlit_feedback package or without it.

Note that streamlit_feedback package has on_submit parameter where handle_feedback could be included:

            streamlit_feedback(feedback_type="faces",
                                optional_text_label="[Optional] Please provide an explanation", 
                                align="flex-start", 
                                key='fb_k',
                                on_submit = handle_feedback)

but function:

def handle_feedback():  
    st.write(st.session_state.fb_k)
    st.toast("✔️ Feedback received!")

does not output anything (i do not see printed st.write or st.toast pop-up). So on_submit does not work for some reason.

for reference here is full application code:


from langchain.chat_models import AzureChatOpenAI
from langchain.memory import ConversationBufferWindowMemory # ConversationBufferMemory
from langchain.agents import ConversationalChatAgent, AgentExecutor, AgentType
from langchain.callbacks import StreamlitCallbackHandler
from langchain.memory.chat_message_histories import StreamlitChatMessageHistory
from langchain.agents import Tool
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
import pprint
import streamlit as st
import os
import pandas as pd
from streamlit_feedback import streamlit_feedback

def handle_feedback():  
    st.write(st.session_state.fb_k)
    st.toast("✔️ Feedback received!")

  
os.environ["OPENAI_API_KEY"] = ...
os.environ["OPENAI_API_TYPE"] = "azure"
os.environ["OPENAI_API_BASE"] = ...
os.environ["OPENAI_API_VERSION"] = "2023-08-01-preview"


@st.cache_data(ttl=72000)
def load_data_(path):
    return pd.read_csv(path) 

uploaded_file = st.sidebar.file_uploader("Choose a CSV file", type="csv")
if uploaded_file is not None:
    # If a file is uploaded, load the uploaded file
    st.session_state["df"] = load_data_(uploaded_file)


if "df" in st.session_state:

    msgs = StreamlitChatMessageHistory()
    memory = ConversationBufferWindowMemory(chat_memory=msgs, 
                                            return_messages=True, 
                                            k=5, 
                                            memory_key="chat_history", 
                                            output_key="output")
    if len(msgs.messages) == 0 or st.sidebar.button("Reset chat history"):
        msgs.clear()
        msgs.add_ai_message("How can I help you?")
        st.session_state.steps = {}
    avatars = {"human": "user", "ai": "assistant"}
    for idx, msg in enumerate(msgs.messages):
        with st.chat_message(avatars[msg.type]):
            # Render intermediate steps if any were saved
            for step in st.session_state.steps.get(str(idx), []):
                if step[0].tool == "_Exception":
                    continue
                # Insert a status container to display output from long-running tasks.
                with st.status(f"**{step[0].tool}**: {step[0].tool_input}", state="complete"):
                    st.write(step[0].log)
                    st.write(step[1])
            st.write(msg.content)


    if prompt := st.chat_input(placeholder=""):
        st.chat_message("user").write(prompt)

        llm = AzureChatOpenAI(
                        deployment_name = "gpt-4",
                        model_name = "gpt-4",
                        openai_api_key = os.environ["OPENAI_API_KEY"],
                        openai_api_version = os.environ["OPENAI_API_VERSION"],
                        openai_api_base = os.environ["OPENAI_API_BASE"],
                        temperature = 0, 
                        streaming=True
                        )

        prompt_ = PromptTemplate(
            input_variables=["query"],
            template="{query}"
        )
        chain_llm = LLMChain(llm=llm, prompt=prompt_)
        tool_llm_node = Tool(
            name='Large Language Model Node',
            func=chain_llm.run,
            description='This tool is useful when you need to answer general purpose queries with a large language model.'
        )

        tools = [tool_llm_node] 
        chat_agent = ConversationalChatAgent.from_llm_and_tools(llm=llm, tools=tools)

        executor = AgentExecutor.from_agent_and_tools(
                                                        agent=chat_agent,
                                                        tools=tools,
                                                        memory=memory,
                                                        return_intermediate_steps=True,
                                                        handle_parsing_errors=True,
                                                        verbose=True,
                                                    )
        

        with st.chat_message("assistant"):            
            
            st_cb = StreamlitCallbackHandler(st.container(), expand_new_thoughts=False)
            response = executor(prompt, callbacks=[st_cb, st.session_state['handler']])
            st.write(response["output"])
            st.session_state.steps[str(len(msgs.messages) - 1)] = response["intermediate_steps"]
            response_str = f'{response}'
            pp = pprint.PrettyPrinter(indent=4)
            pretty_response = pp.pformat(response_str)
              

        with st.form('form'):
            streamlit_feedback(feedback_type="thumbs",
                                optional_text_label="[Optional] Please provide an explanation", 
                                align="flex-start", 
                                key='fb_k')
            st.form_submit_button('Save feedback', on_click=handle_feedback)

Could you post a minimal reproducible code and tell what you want?

This library is tricky to use because of how streamlit works, how this library works, and how your app works.

@ferdy

sure. here is a minimal reproducible code:

import streamlit as st
from streamlit_feedback import streamlit_feedback


if "chat_history" not in st.session_state:
    st.session_state.chat_history = []


def display_answer():
    for i in st.session_state.chat_history:
        with st.chat_message("human"):
            st.write(i["question"])
        with st.chat_message("ai"):
            st.write(i["answer"])

        # If there is no feedback show N/A
        if "feedback" in i:
            st.write(f"Feedback: {i['feedback']}")
        else:
            st.write("Feedback: N/A")

def create_answer(question):
    if "chat_history" not in st.session_state:
        st.session_state.chat_history = []

    message_id = len(st.session_state.chat_history)

    st.session_state.chat_history.append({
        "question": question,
        "answer": f"{question}_Answer",
        "message_id": message_id,
    })


def fbcb():
    message_id = len(st.session_state.chat_history) - 1
    if message_id >= 0:
        st.session_state.chat_history[message_id]["feedback"] = st.session_state.fb_k
    display_answer()


if question := st.chat_input(placeholder="Ask your question here .... !!!!"):
    create_answer(question)
    display_answer()

    ## thumbs up/down work with this approach 
    with st.form('form'):
        streamlit_feedback(feedback_type="thumbs", optional_text_label="[Optional]", align="flex-start", key='fb_k')
        st.form_submit_button('Save feedback', on_click=fbcb)

    ## thumbs up/down do NOT work with this approach 
    # streamlit_feedback(feedback_type="thumbs", optional_text_label="[Optional]",  align="flex-start", key='fb_k', on_submit='streamlit_feedback')

here is how it looks like:
[1a]


[2a]

but if i use streamlit_feedback(feedback_type="thumbs", align="flex-start", key='fb_k', on_submit='streamlit_feedback') instead:
[1b]


[2b]

[3b]

I want to get rid of with st.form('form'): but still have the same feedback functionality.

Try this one it does not use a form in feedback. I did some revisions and add some comments in the code.

Code

import streamlit as st
from streamlit_feedback import streamlit_feedback
import uuid


if 'question_state' not in st.session_state:
    st.session_state.question_state = False

if 'fbk' not in st.session_state:
    st.session_state.fbk = str(uuid.uuid4())


if "chat_history" not in st.session_state:
    st.session_state.chat_history = []


def display_answer():
    for entry in st.session_state.chat_history:
        with st.chat_message("human"):
            st.write(entry["question"])
        with st.chat_message("ai"):
            st.write(entry["answer"])

        # Do not display the feedback field since
        # we are still about to ask the user.
        if 'feedback' not in entry:
            continue

        # If there is no feedback show N/A
        if "feedback" in entry:
            st.write(f"Feedback: {entry['feedback']}")
        else:
            st.write("Feedback: N/A")


def create_answer(question):
    """Add question/answer to history."""
    # Do not save to history if question is None.
    # We reach this because streamlit reruns to get the feedback.
    if question is None:
        return
    
    message_id = len(st.session_state.chat_history)
    st.session_state.chat_history.append({
        "question": question,
        "answer": f"{question}_Answer",
        "message_id": message_id,
    })


def fbcb(response):
    """Update the history with feedback.
    
    The question and answer are already saved in history.
    Now we will add the feedback in that history entry.
    """
    last_entry = st.session_state.chat_history[-1]  # get the last entry
    last_entry.update({'feedback': response})  # update the last entry
    st.session_state.chat_history[-1] = last_entry  # replace the last entry
    display_answer()  # display hist

    # Create a new feedback by changing the key of feedback component.
    st.session_state.fbk = str(uuid.uuid4())


# Starts here.
question = st.chat_input(placeholder="Ask your question here .... !!!!")
if question:
    # We need this because of feedback. That question above
    # is a stopper. If user hits the feedback button, streamlit
    # reruns the code from top and we cannot enter back because
    # of that chat_input.
    st.session_state.question_state = True

# We are now free because st.session_state.question_state is True.
# But there are consequences. We will have to handle
# the double runs of create_answer() and display_answer()
# just to get the user feedback. 
if st.session_state.question_state:
    create_answer(question)
    display_answer()

    # Pressing a button in feedback reruns the code.
    streamlit_feedback(
        feedback_type="thumbs",
        optional_text_label="[Optional]",
        align="flex-start",
        key=st.session_state.fbk,
        on_submit=fbcb
    )

Output

1 Like

@ferdy,

Thank you so much for providing the code. I was able to integrate it into a larger app, and it works well. The only thing I still struggle with is adding the option to upload an image in addition to textual input. I had to use st.rerun() so image upload would work. The st.rerun() function call in Streamlit is used to rerun the entire script from top to bottom, and I was not able to integrate it with feedback functionally. I include a minimal reproducible sample below.

import streamlit as st
import os
from io import BytesIO  
import pandas as pd 
import streamlit as st
from streamlit_feedback import streamlit_feedback
import numpy as np
from io import BytesIO
from PIL import Image
import base64
from uuid import uuid4

if 'question_state' not in st.session_state:
    st.session_state.question_state = False

if 'fbk' not in st.session_state:
    st.session_state.fbk = str(uuid4())


if "chat_history_" not in st.session_state:
    st.session_state.chat_history_ = []


system_message = '''
You are an AI assistant 
'''

if 'history' not in st.session_state:
    st.session_state['history'] = [{'role': 'system', 'content': system_message}]
    st.session_state['counters'] = [0, 1]


def display_answer():
    # display chat
    for msg in st.session_state['history'][1:]:
        if msg['role'] == 'user':
            with st.chat_message('user'):
                for i in msg['content']:
                    if i['type'] == 'text':
                        st.write(i['text'])
                    else:
                        with st.expander('Attached Image'):
                            img = Image.open(BytesIO(base64.b64decode(i['image_url']['url'][23:])))
                            st.image(img)
        else:
            with st.chat_message('assistant'):
                msg_content = ''.join(['  ' + char if char == '\n' else char for char in msg['content']])  # fixes display issue
                st.markdown('Assistant: ' + msg_content)


def fbcb(response):
    """Update the history with feedback.
    
    The question and answer are already saved in history.
    Now we will add the feedback in that history entry.
    """
    if st.session_state.chat_history_:  
        last_entry = st.session_state.chat_history_[-1]  # get the last entry
        last_entry.update({'feedback': response})  # update the last entry
        st.session_state.chat_history_[-1] = last_entry  # replace the last entry
    
        st.write(st.session_state.chat_history_[-1]["feedback"])
        st.toast("✔️ Feedback received!")    
     
    # Create a new feedback by changing the key of feedback component.
    st.session_state.fbk = str(uuid4())



def create_answer(history):
    """Add question/answer to history."""
    # Do not save to history if question is None.
    # We reach this because streamlit reruns to get the feedback.
    if history is None:
        return

    st.session_state.chat_history_.append({  
        "content": "answer"
    })  

    st.session_state['history'].append(
        {'role': 'assistant', 'content': "answer"}
    )
    st.session_state['counters'] = [i+2 for i in st.session_state['counters']]

with st.expander('System Message'):
    st.session_state['history'][0]['content'] = st.text_area('sys message',
                                                                st.session_state['history'][0]['content'],
                                                                label_visibility='collapsed',
                                                                height=600
                                                                )

display_answer()

# get user inputs
text_input = st.text_input('Prompt', '', key=st.session_state['counters'][0], placeholder = 'Optional')
img_input = st.file_uploader('Images', accept_multiple_files=True, key=st.session_state['counters'][1])


if text_input or img_input:
    # We need this because of feedback. That question above
    # is a stopper. If user hits the feedback button, streamlit
    # reruns the code from top and we cannot enter back because
    # of that chat_input.
    st.session_state.question_state = True


# send api request
if st.button('Send'):
    if not (text_input or img_input):
        st.warning('You can\'t just send nothing!')
        st.stop()
    msg = {'role': 'user', 'content': []}
    if text_input:
        msg['content'].append({'type': 'text', 'text': text_input})
    for img in img_input:
        if img.name.split('.')[-1].lower() not in ['png', 'jpg', 'jpeg', 'gif', 'webp']:
            st.warning('Only .jpg, .png, .gif, or .webp are supported')
            st.stop()
        encoded_img = base64.b64encode(img.read()).decode('utf-8')
        msg['content'].append(
            {
                'type': 'image_url',
                'image_url': {
                    'url': f'data:image/jpeg;base64,{encoded_img}',
                    'detail': 'low'
                }
            }
        )


    st.session_state['history'].append(msg)
    history = (
        st.session_state['history']
        if st.session_state['history'][0]['content']
        else st.session_state['history'][1:]
    )

    if st.session_state.question_state:
        create_answer(history)
       
        st.rerun()
         
# Pressing a button in feedback reruns the code.
streamlit_feedback(
    feedback_type="faces",
    optional_text_label="[Optional]",
    align="flex-start",
    key=st.session_state.fbk,
    on_submit=fbcb
)


# clear chat history
if st.button('Clear'):
    st.session_state['history'] = [st.session_state['history'][0]]
    st.rerun()

Any advice is highly appreciated.

Thanks for starting this thread, I wanted to have something similar, the problem was that my application was rerun everytime I pressed the feedback button. Turns out st.cache_data is what I needed.

Basically, if you have some logic that is being executed in a main function, here’s how your Streamlit app could look like:

import uuid

import streamlit as st
from streamlit_feedback import streamlit_feedback

if "session_id" not in st.session_state:
    st.session_state["session_id"] = str(uuid.uuid4())

st.title("Demo app")

uploaded_file = st.file_uploader(
    label="Upload a PDF file", type=["pdf"]
)

@st.cache_data(persist=True, show_spinner=False)
def main(uploaded_file):
     # your main business logic processing the file is here
     result = ""     

     return result

result = main(uploaded_file)

def handle_feedback(user_response, extracted_text):
    # here you could write the feedback to GCS or Firestore for instance
    st.write(f"Session id {st.session_state.session_id}")
    st.write("Result:", result)
    st.write(f"User response: {user_response}")
    st.toast("✔️ Feedback received!")

    # reset the session ID
    st.session_state["session_id"] = str(uuid.uuid4())

streamlit_feedback(
    feedback_type="faces",
    optional_text_label="[Optional] Please provide an explanation",
    key=st.session_state.session_id,
    on_submit=handle_feedback,
    kwargs={"result": result},
)