Okay, let’s get on with using those available conversation topics.

UI

I have decided to continue displaying the topics, or at least a sub-set if there are too many of them, in the sidebar. But, I am not going to provide any interaction with that list in the side bar. When the user selects Show conversation history or Run the app, they will be presented with a set of radio buttons inside or outside a form.

In the case of Run the app, there will also be a text box allowing the user to select a new conversation topic if they don’t wish to continue with any of the saved conversations. And, that field, if no existing conversation is selected, can not be empty or blank. I may also limit it to something like 15-20 characters. When I create the thread_id any spaces will be replaced with underscores. And, I expect I will remove all other punctuation. Hope there’s an easy way to do so.

Once the form or radio button selection is submitted, a callback function will update session state variables (likely a new one or two). It may then initiate the display of the saved message or the chatbot interface depending on the selected app mode/action. Though I may build that into my series of if/elif blocks rather than in the callback function. As I type this, I am thinking the latter as we need to account for Streamlits code reruns. But…

Refactor “Show conversation history”

Let’s start with what I expect is the easier of the two. In this case I think I will just display a set of radio buttons without a form and let the callback do the required work to get things going in the right direction.

Right now I will refactor the elif st.session_state.app_mode == "Show conversation history": block to display the radio buttons. When a button is clicked, the call back function will be activated. What it does is still to be determined. If a session state variable indicates that a topic has been selected then the block will display the selected conversation history. Sounds simple enough.

Selecting the Topic

Let’s get the topic selection working.

def updt_mode():
  st.session_state.app_mode = st.session_state.app_op
  if st.session_state.app_mode == "Logout":
    init_sst(ovr_wrt=True)
  if st.session_state.app_mode == "Clear current topic":
    st.session_state.topic = ""
... ...
def sv_topic():
  st.session_state.topic = st.session_state.u_tpc
... ...
    if st.session_state.topic:
      st.sidebar.write(f"Curent topic: {st.session_state.topic}")
... ...
    app_mode = st.sidebar.radio("**What to do next?**",
      ["Show instructions", "Show the source code", "Show conversation history", "Run the app", "Clear current topic", "Logout"],
      key="app_op", on_change=updt_mode)
... ...
  elif st.session_state.app_mode == "Show conversation history":
    readme_text.empty()
    st.title("Show stored conversation history")
    if not st.session_state.topic:
      u_tp = st.radio("**Which topic?**",
        st.session_state.lst_topics,
        index=None, key="u_tpc", on_change=sv_topic)

Once again, I needed a session state variable with a different name from the key of the button widget so that I can change it when needed. I figured I also needed to add a radio button in the menu to clear the current topic so that another one could be selected.

Displaying the Conversation History

Coding progressed pretty much the same as for most of the code so far.

... ...
def get_topics(cur):
  ... ...
  t_ndx = len(st.session_state.user_id) + 1   # fix bug
... ...
# got most of this code from my sqlite_tst module
def get_conversation():
  tid = f'{st.session_state.user_id}_{st.session_state.topic.replace(" ", "_")}'
  read_cfg = {"configurable": {"thread_id": tid}}
  with sqlite3.connect(db_fl, check_same_thread=False) as conn:
    mem = SqliteSaver(conn)
    mem_lst = list(mem.list(read_cfg))
    chk_pt = mem_lst[0]
    tmp = chk_pt.checkpoint['channel_values']['messages']
    return tmp
... ...
    if not st.session_state.topic:
      ... ...
    else:
      conv = get_conversation()
      if conv:
        for i, msg in (enumerate(conv)):
          if i % 2 == 0:
            st.markdown(f"\n**:blue[{st.session_state.user_id}]**: {msg.content}")
          else:
            st.markdown(f"\n**:blue[AI]**: {msg.content}")
            st.write(f"---")

Here’s a shot of the app with the conversation selection form displayed after I selected Show conversation history.

a view of main page showing radio buttons allowing user to select topic after selecting 'Show conversation history' from the menu

And, here’s a portion of the main page displaying the conversation retrieved from the database after the I selected the pets radio button.

a view of top main page the conversation retrieved from the database after the 'pets' radio button was clicked

Won’t bother showing anything, but the Clear current topic menu item works as expected.

Refactor “Run the App”

Selecting the Topic

Let’s get the topic selection working. Pretty much similar to what we did for displaying conversations. But as mentioned, we also need to allow the user to provide a new topic.

... ...
app_ttl = "# Demo: MistralAI Chatbot"   # shortened the title
... ...
    readme_text.empty()
    st.markdown(app_ttl, unsafe_allow_html=True)
    if not st.session_state.topic:
      with st.form("c_topic"):
        u_tp = st.radio("**Which topic?**",
          st.session_state.lst_topics,
          index=None, key="u_tpc")
        c1, c2 = st.columns([1, 3])
        with c1:
          u_id = st.text_input(
            "**New Topic:**",
            placeholder="3 to 18 characters",
            max_chars=18,
            key="nw_tpc",
            label_visibility="visible",
          )
        with c2:
          st.write("")
        st.form_submit_button('Set topic', on_click=sv_topic)
    else:
# if moved the previous code to display the conversation dialog into this else block

And here’s what the above looks like in the browser window.

a view of page showing the form for selecting the topic when 'Run app selected'

Now let’s deal with saving the desired topic to session state. I am going to use the same callback as used to get the topic when displaying saved conversation histories. With whatever refactoring is required to allow for the variation in this case.

def sv_topic():
  if st.session_state.u_tpc:
    st.session_state.topic = st.session_state.u_tpc
  elif st.session_state.nw_tpc:
    st.session_state.topic = st.session_state.nw_tpc

Easier that I expected. And it seems to work as planned. Here’s the app display after I entered a new topic. It didn’t allow me to enter more than 18 characters. And if I clicked the Set topic button without selecting a radio button or entering a new topic, I just got the form back again.

a view of page after entering a new topic when 'Run app selected'

Now on to the hard part.

Resuming a Conversation

I don’t actually know if this is possible, but let’s give it a go. I will start by setting the thread_id value in the SqliteSaver configuration object to the appropriate value. Then start up SqliteSaver and see if it uses that conversation when querying the chatbot.

I had some issues to start with. My attempt to get and display the conversation history was at first causing blank requests to be sent to the LLM. So items were being added to the conversation at random. I did eventually sort that out.

Then, after initially showing the conversation history in chronological order, I decided to show the history in reverse. So that the latest exchange with the LLM was just below the form. Not at the bottom of the page. Reduced the scrolling to see the latest response rather considerably.

I also created a couple of functions to do the repetitive work. Plus I had to add code to display the selected conversation history after the topic was selected. Initially, I was only getting the chatbot form; no conversation history.

I won’t bother with all the mistakes/bugs/issues. I will just show the final code additions and changes. The “Run the appif block was significantly refactored so I will include the whole thing.

... ...
def mk_thread_id():
  tid = ""
  if st.session_state.topic and st.session_state.user_id:
    tid = f'{st.session_state.user_id}_{st.session_state.topic.replace(" ", "_")}'
  return tid


def disp_conv(bot_conv):
  st.write(f"---")
  for i in range(len(bot_conv)):
    if i % 2 == 0:
      st.markdown(f':blue[**Me**]: {bot_conv[i].content}')
    else:
      st.markdown(f':blue[**AI**]: {bot_conv[i].content}')
      st.write(f"---")


def disp_conv_rev(bot_conv):
  st.divider()
  st.subheader(f"Conversation, most recent first", divider="blue")
  scnd_lst_msg = len(bot_conv) - 2
  for i in range(scnd_lst_msg, -1, -2):
    st.markdown(f':blue[**Me**]: {bot_conv[i].content}')
    st.markdown(f':blue[**AI**]: {bot_conv[i+1].content}')
    st.write(f"---")
... ...
      # refactor to use function to display conversation in 'Show conversation history'
      if conv:
        disp_conv(conv)
  elif  st.session_state.app_mode == "Run the app":
    conv_tid = mk_thread_id()
    if conv_tid:
      cnfg = {"configurable": {"thread_id": conv_tid}}
      c_resume = st.session_state.topic in st.session_state.lst_topics
    readme_text.empty()
    st.markdown(app_ttl, unsafe_allow_html=True)
    if not st.session_state.topic:
      # if no topic in session state, get one
      with st.form("c_topic"):
        u_tp = st.radio("**Which topic?**",
          st.session_state.lst_topics,
          index=None, key="u_tpc")
        c1, c2 = st.columns([1, 3])
        with c1:
          u_id = st.text_input(
            "**New Topic:**",
            placeholder="3 to 18 characters",
            max_chars=18,
            key="nw_tpc",
            label_visibility="visible",
          )
        with c2:
          st.write("")
        st.form_submit_button('Set topic', on_click=sv_topic)
    else:
      with sqlite3.connect(db_fl, check_same_thread=False) as conn:
        mem = SqliteSaver(conn)
        app = workflow.compile(checkpointer=mem)

        readme_text.empty()
        # make text_area cleared after submit, default is to leave text there
        # don't want to accidently send request a second time
        with st.form("my_form", clear_on_submit=True):
          u_inp = st.text_area(
              "Enter text:",
              placeholder="Say something to the LLM or ask a question",
              key="llm_ask",
          )
          submitted = st.form_submit_button("Submit")
          # don't process empty submit
          if submitted and st.session_state.llm_ask != "":
            in_msgs = [HumanMessage(u_inp)]
            llm_said = app.invoke({"messages": in_msgs}, cnfg)
          if submitted:
            mem_lst = list(mem.list(cnfg))
            chk_pt = mem_lst[0]
            disp_conv_rev(chk_pt.checkpoint['channel_values']['messages'])
          elif not submitted and c_resume:
            # display conversation history when form first loaded
            mem_lst = list(mem.list(cnfg))
            chk_pt = mem_lst[0]
            disp_conv_rev(chk_pt.checkpoint['channel_values']['messages'])

I can assure you that the appropriate conversation history is in fact being displayed. And used by SqliteSaver when it sends the current request to the LLM.

Here’s a shot of the top of the browser window after I asked the LLM for a recipe for miso soup.

a view of top of page after submitting a request for miso soup recipe to LLM

Done

And that’s it for this one. Likely for the project as a whole. Not sure what else I could code at this point.

But perhaps I will look at using multiple LLMs. Or, maybe look at adding the ability to upload images with requests. I.E. multi-modal chatbot. Expect all of that will be considerably more complicated and or difficult.

Another thing to look at is summarizing the conversation history so fewer tokens are being sent on each request. Especially important as the conversation grows. As LLMs definitely have limits. And free LLMs often have fairly small limits. And with LLM subscriptions, you are paying for each token in and out. Not sure how I’d go about that. Perhaps have a LLM do it?

Resources