trubrics / streamlit-feedback

Collect user feedback from within your Streamlit app
MIT License
129 stars 14 forks source link

Best way to add streamlit-feedback into chatbot #21

Open aiakubovich opened 8 months ago

aiakubovich commented 8 months ago

Thank you for awesome package. I was able to add streamlit-feedback into chatbot app via st.form:

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

....

        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)

It works but there two problems: 1) To get it work user first need click on SUBMIT and only then to "Save feedback". image

If user click "Save feedback" then st.session_state.fb_k will be None

2) Feedback inside st.form does not look very good and I am looking to ways to get rid of st.form but still have the same functionaly.

I looked in examples.py in the repo and similar issues but did not find anything that would help to resolve the problem.

Full app 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)
jeffkayne commented 8 months ago

Hey @aiakubovich, is there a reason for using st.form for this? You can also feed your handle_feedback function into the on_submit parameter. Let me know if that works for you

aiakubovich commented 8 months ago

Hi @jeffkayne, if I use:

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

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

jeffkayne commented 8 months ago

Hey @aiakubovich, what versions are you using?

Everything is working on my side with versions streamlit==1.30.0 streamlit-feedback==0.1.2

Here you can see me running this with the following code:

import streamlit as st
from streamlit_feedback import streamlit_feedback

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

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

https://github.com/trubrics/streamlit-feedback/assets/43336277/458a4e26-9108-47ba-9f56-50becc2d88b2

aiakubovich commented 8 months ago

Hey @jeffkayne, thank you for looking at it.

Your code works for me as well but there needed to be st.chat_message in code to reproduce that problem. Let me give you 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] image

[2a] image

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

[2b] image

[3b] image

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

jeffkayne commented 8 months ago

I'm not sure what you are trying to achieve. You must take into account that the streamlit_feedback component triggers a page reload immediately (this is how streamlit components work). Now on this rerun of the script, when you get to the streamlit_feedback line, it runs the on_submit callback if specified.

The reason there is a different functionality in the code with an st.form is that within an st.form any actions or changes of state are not applied until st.form_submit_button is called.

I hope this helps

Shubh789da commented 4 months ago

I am also facing the same issue does anyone found the answer?