<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joe</title>
    <description>The latest articles on DEV Community by Joe (@thtmexicnkid).</description>
    <link>https://dev.to/thtmexicnkid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1144082%2F040f1a2d-688b-4a79-a69a-15162b40550c.png</url>
      <title>DEV Community: Joe</title>
      <link>https://dev.to/thtmexicnkid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thtmexicnkid"/>
    <language>en</language>
    <item>
      <title>Elastic D&amp;D - Update 16 - Bug &amp; Logic Fixes</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 12 Jan 2024 21:12:26 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-update-16-bug-logic-fixes-5gac</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-update-16-bug-logic-fixes-5gac</guid>
      <description>&lt;p&gt;In the last post we talked about how fixing the password reset function. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-15-fixing-password-reset-3nb4"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug &amp;amp; Logic Fixes
&lt;/h2&gt;

&lt;p&gt;First of all, Happy New Year everyone! &lt;/p&gt;

&lt;p&gt;I haven't added any major features, but I have gotten to work on a few minor things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added a function to get current and previous session numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The query in this function gets a recent log that is NOT from "today" ("now/d" is the current date) and grabs the "session" field. It then adds 1 to that value for current session and returns both numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def elastic_get_session_numbers(log_index):
    # creates Elastic connection
    client = Elasticsearch(
        elastic_url,
        ca_certs=elastic_ca_certs,
        api_key=elastic_api_key
    )

    # gets last session number
    response = client.search(index=log_index,size=1,sort=["@timestamp:desc"],source=["session"],query={"bool":{"must":[{"range":{"@timestamp":{"lt":"now/d"}}}]}})

    # grab last session number and calculate current session number
    last_session = response["hits"]["hits"][0]["_source"]["session"]
    current_session = int(last_session) + 1

    return last_session, current_session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Added current session number to the sidebar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following code is added to the bottom of the sidebar, displaying the current session number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;st.text("Current session: " + str(st.session_state.current_session))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Fixed the function to get previous session summary to use previous session number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The function now uses the following code for the Elastic query, which specifies the session number grabbed from the function described above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response = client.search(index=log_index,size=1,source=["message"],query={"bool":{"must":[{"match":{"type":"overview"}},{"match":{"session":session_number}}]}})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Fixed the function to get previous session summary to return generic message if no overview note was added in that session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This part was added out of necessity, as an error would occur if there was no overview log to be found. I'm giving a public thank you to &lt;a class="mentioned-user" href="https://dev.to/rusty13jr"&gt;@rusty13jr&lt;/a&gt; for finding this for me!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try:
        summary = response["hits"]["hits"][0]["_source"]["message"]
    except:
        summary = "No overview log was submitted from last session."

    return summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Added placeholders for question prompts that Veverbot cannot use AI to answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These will be used in the near future, and will do the work necessary to return useful information to questions that cannot utilize the KNN search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# question prompts
    column1, column2, column3, column4, column5 = st.columns(5)
    column1.button("Question 1 Placeholder",type="primary",on_click=None)
    column2.button("Question 2 Placeholder",type="primary",on_click=None)
    column3.button("Question 3 Placeholder",type="primary",on_click=None)
    column4.button("Question 4 Placeholder",type="primary",on_click=None)
    column5.button("Question 5 Placeholder",type="primary",on_click=None)
    st.header("",divider="grey")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;I finally have my group in the program and taking notes. Them using it has exposed some bugs and code errors, ultimately making the program better. This is much appreciated by me, so please reach out with any issues you run into if you are using the program as well!&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>elasticsearch</category>
      <category>python</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 15 - Fixing Password Reset</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 22 Dec 2023 18:03:16 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-15-fixing-password-reset-3nb4</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-15-fixing-password-reset-3nb4</guid>
      <description>&lt;p&gt;In the last post we talked about how rewriting the Note Input. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-14-note-input-rewrite-lp8"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing Password Reset
&lt;/h2&gt;

&lt;p&gt;I finally gave access to my project to my D&amp;amp;D group! This would have been exciting, except when they all went to reset their passwords from "changeme", there were some issues. Specifically, the update_yml function wasn't able to locate the "config" variable.&lt;/p&gt;

&lt;p&gt;Old code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def update_yml():
    # updates login authentication configuration file
    with open(streamlit_project_path + "auth.yml", 'w') as file:
        yaml.dump(config, file, default_flow_style=False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, I should have realized this, but this function wasn't able to grab the variable from outside of the function. I made this change to pass the configuration into the function and it works now.&lt;/p&gt;

&lt;p&gt;New code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def update_yml(updated_config):
    # updates login authentication configuration file
    with open(streamlit_project_path + "auth.yml", 'w') as file:
        yaml.dump(updated_config, file, default_flow_style=False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I felt really dumb once I realized what was happening. I legitimately couldn't figure out what was wrong and had to sleep off the frustration in order to spot this. Saying that to say, silly mistakes happen sometimes and that's okay.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Logon Issue
&lt;/h2&gt;

&lt;p&gt;Funnily enough, while I was troubleshooting the YAML issue, I noticed that users could still log in even if they put in an incorrect password. This shouldn't have been the case, and it ended up being a logic issue in the code.&lt;/p&gt;

&lt;p&gt;See, when inputting an incorrect password, the username field was still being populated, which bypassed my check in the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if not st.session_state.username:
    DISPLAY LOGON WIDGET
else:
    DISPLAY HOME PAGE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we initialize the authentication status along with the username, as well as check that instead of the username:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initialize_session_state(["username","authentication_status"])
...
if st.session_state.authentication_status in (False,None):
    DISPLAY LOGON WIDGET
else:
    DISPLAY HOME PAGE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;I'm glad I was able to identify the faulty parts of both of these issues. The program as a whole functions better with these changes in place. &lt;strong&gt;&lt;em&gt;This is a friendly reminder to tell me about issues that you come across via Github. I will get to them!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I will be taking next week off for the holidays, but work on the player dashboard will begin soon after!&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 14 - Note Input Rewrite</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Sat, 16 Dec 2023 18:45:58 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-14-note-input-rewrite-lp8</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-14-note-input-rewrite-lp8</guid>
      <description>&lt;p&gt;In the last post we talked about how text chunking. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-13-text-chunking-30m7"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Note Input Rewrite
&lt;/h2&gt;

&lt;p&gt;The inspiration behind this rewrite actually comes from my girlfriend, who mentioned having some sort of glossary/index added to the application. I thought that was a great idea, but the data structure needed to be tweaked for that. In the process of tweaking the structure, I had an overwhelming urge to clean up my code, so here we are!&lt;/p&gt;

&lt;p&gt;Logically, I see the code in two major sections -- a data collection section, and a processing/indexing section.&lt;/p&gt;

&lt;p&gt;Full code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 12/09/2023
# 
# Streamlit - Note Input Page - Allows the user to store audio or text notes in Elasticsearch.

import streamlit as st
from functions import *
from variables import *

# set streamlit app to use centered format
st.set_page_config(layout="centered")

# initializes session state, loads login authentication configuration
initialize_session_state(["username"])
config, authenticator = load_yml()

# makes user log on to view page
if not st.session_state.username:
    error_message("UNAUTHORIZED: Please login on the Home page.",False)
else:
    with st.sidebar:
        # adds elastic d&amp;amp;d logo to sidebar
        display_image(streamlit_data_path + "banner.png","auto")
        st.divider()
        # add character picture to sidebar, if available
        try:
            display_image(streamlit_data_path + st.session_state.username + ".png","auto")
        except:
            print("Picture unavailable for home page sidebar.")
    st.header('Note Input',divider=True)
    # gather information for log_payload in form
    form_variable_list = ["log_id","log_type","log_session","log_index","file","location_name","location_description","overview_summary","person_name","person_description","quest_name","quest_description","quest_finished","submitted","transcribed_text","content","content_vector"]
    st.session_state["log_type"] = st.selectbox("What kind of note is this?", ["audio","location","miscellaneous","overview","person","quest"])
    if st.session_state.log_type == "quest":
        st.session_state["quest_type"] = st.selectbox("Is this quest new or existing?", ["New","Existing"])
    with st.form(st.session_state.log_type, clear_on_submit=True):
        st.session_state["log_session"] = st.slider("Which session is this?", 0, 250)
        st.session_state["log_id"] = "session" + str(st.session_state.log_session) + "-" + generate_unique_id()
        ###CHECK IF LOG_ID EXISTS, RE-GENERATE IF IT DOES###
        if st.session_state.log_type == "audio":
            st.session_state["log_index"] = "dnd-notes-transcribed"
            if assemblyai_api_key:
                st.session_state["file"] = st.file_uploader("Choose audio file",type=[".3ga",".8svx",".aac",".ac3",".aif",".aiff",".alac",".amr",".ape",".au",".dss",".flac",".flv",".m2ts",".m4a",".m4b",".m4p",".m4p",".m4r",".m4v",".mogg",".mov",".mp2",".mp3",".mp4",".mpga",".mts",".mxf",".oga",".ogg",".opus",".qcp",".ts",".tta",".voc",".wav",".webm",".wma",".wv"])
            else:
                st.session_state["file"] = st.file_uploader("Choose audio file",type=[".wav"])
            if st.session_state.file is not None:
                st.session_state["ready_for_submission"] = True
            else:
                st.warning('Please upload a file and submit')
        else:
            st.session_state["log_index"] = "dnd-notes-" + st.session_state.username
            if st.session_state.log_type == "location":
                st.session_state["location_name"] = text_cleanup(st.text_input("Input location name:"))
                st.session_state["location_description"] = text_cleanup(st.text_area("Input location description:"))
                if st.session_state.location_name is not None and st.session_state.location_description is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the location name, description, and submit')
            elif st.session_state.log_type == "miscellaneous":
                st.session_state["miscellaneous_note"] = text_cleanup(st.text_area("Input miscellaneous note:"))
                if st.session_state.miscellaneous_note is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter miscellaneous note and submit')
            elif st.session_state.log_type == "overview":
                st.session_state["overview_summary"] = text_cleanup(st.text_area("Input session summary:"))
                if st.session_state.overview_summary is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the session overview/summary and submit')
            elif st.session_state.log_type == "person":
                st.session_state["person_name"] = text_cleanup(st.text_input("Input person name:"))
                st.session_state["person_description"] = text_cleanup(st.text_area("Input person description:"))
                if st.session_state.person_name is not None and st.session_state.person_description is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the person name, description, and submit')
            elif st.session_state.log_type == "quest":
                if st.session_state.quest_type == "Existing":
                    st.session_state["quest_name"] = st.selectbox("Select quest to update", elastic_get_quests())
                else:
                    st.session_state["quest_name"] = st.text_input("Input quest name:")
                st.session_state["quest_description"] = text_cleanup(st.text_area("Input quest description / update:"))
                st.session_state["quest_finished"] = st.checkbox("Is the quest finished?")
                if st.session_state.quest_name is not None and st.session_state.quest_description is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the quest name, description, mark the status, and submit')
        # submit form, process data, and index log_payload
        st.session_state["submitted"] = st.form_submit_button("Submit")
        if st.session_state.submitted == True and st.session_state.ready_for_submission == True:
            # audio to text transcription
            if st.session_state.log_type == "audio":
                if assemblyai_api_key:
                    st.session_state["transcribed_text"] = text_cleanup(transcribe_audio_paid(st.session_state.file))
                else:
                    st.session_state["transcribed_text"] = text_cleanup(transcribe_audio_free(st.session_state.file))
                if st.session_state.transcribed_text not in (None,""):
                    chunk_array = split_text_with_overlap(st.session_state.transcribed_text)
                    for chunk in chunk_array:
                        st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". " + chunk
                        st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                        if st.session_state.content_vector == None:
                            error_message("AI API vectorization failure",2)
                        else:
                            st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.transcribed_text,"content":st.session_state.content,"content_vector":st.session_state.content_vector})
                            elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # location logs
            elif st.session_state.log_type == "location":
                chunk_array = split_text_with_overlap(st.session_state.location_description)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". The location is " + st.session_state.location_name + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.location_name + ". " + st.session_state.location_description,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"location":{"name":st.session_state.location_name,"description":st.session_state.location_description}})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # miscellaneous logs
            elif st.session_state.log_type == "miscellaneous":
                chunk_array = split_text_with_overlap(st.session_state.miscellaneous_note)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.miscellaneous_note,"content":st.session_state.content,"content_vector":st.session_state.content_vector})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # overview logs
            elif st.session_state.log_type == "overview":
                chunk_array = split_text_with_overlap(st.session_state.overview_summary)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.overview_summary,"content":st.session_state.content,"content_vector":st.session_state.content_vector})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # person logs
            elif st.session_state.log_type == "person":
                chunk_array = split_text_with_overlap(st.session_state.person_description)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". The person's name is " + st.session_state.person_name + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.person_name + ". " + st.session_state.person_description,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"person":{"name":st.session_state.person_name,"description":st.session_state.person_description}})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # quest logs
            elif st.session_state.log_type == "quest":
                if st.session_state.quest_finished == True:
                    elastic_update_quest_status(st.session_state.quest_name)
                    status = "The quest has been completed."
                else:
                    status = "The quest has not been completed yet."
                chunk_array = split_text_with_overlap(st.session_state.quest_description)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". The quest is " + st.session_state.quest_name + ". " + status + " " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.quest_name + ". " + st.session_state.quest_description + status,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"quest":{"name":st.session_state.quest_name,"description":st.session_state.quest_description,"finished":st.session_state.quest_finished}})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
    clear_session_state(form_variable_list)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data Collection
&lt;/h3&gt;

&lt;p&gt;The first half of the code mainly deals with data input and sorting that data into variables that the second half of the code will use for manipulation and/or payloads.&lt;/p&gt;

&lt;p&gt;These payloads are important, as they provide the new data structure mentioned above.&lt;/p&gt;

&lt;p&gt;The data collection code consists of lines 32-89:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # gather information for log_payload in form
    form_variable_list = ["log_id","log_type","log_session","log_index","file","location_name","location_description","overview_summary","person_name","person_description","quest_name","quest_description","quest_finished","submitted","transcribed_text","content","content_vector"]
    st.session_state["log_type"] = st.selectbox("What kind of note is this?", ["audio","location","miscellaneous","overview","person","quest"])
    if st.session_state.log_type == "quest":
        st.session_state["quest_type"] = st.selectbox("Is this quest new or existing?", ["New","Existing"])
    with st.form(st.session_state.log_type, clear_on_submit=True):
        st.session_state["log_session"] = st.slider("Which session is this?", 0, 250)
        st.session_state["log_id"] = "session" + str(st.session_state.log_session) + "-" + generate_unique_id()
        ###CHECK IF LOG_ID EXISTS, RE-GENERATE IF IT DOES###
        if st.session_state.log_type == "audio":
            st.session_state["log_index"] = "dnd-notes-transcribed"
            if assemblyai_api_key:
                st.session_state["file"] = st.file_uploader("Choose audio file",type=[".3ga",".8svx",".aac",".ac3",".aif",".aiff",".alac",".amr",".ape",".au",".dss",".flac",".flv",".m2ts",".m4a",".m4b",".m4p",".m4p",".m4r",".m4v",".mogg",".mov",".mp2",".mp3",".mp4",".mpga",".mts",".mxf",".oga",".ogg",".opus",".qcp",".ts",".tta",".voc",".wav",".webm",".wma",".wv"])
            else:
                st.session_state["file"] = st.file_uploader("Choose audio file",type=[".wav"])
            if st.session_state.file is not None:
                st.session_state["ready_for_submission"] = True
            else:
                st.warning('Please upload a file and submit')
        else:
            st.session_state["log_index"] = "dnd-notes-" + st.session_state.username
            if st.session_state.log_type == "location":
                st.session_state["location_name"] = text_cleanup(st.text_input("Input location name:"))
                st.session_state["location_description"] = text_cleanup(st.text_area("Input location description:"))
                if st.session_state.location_name is not None and st.session_state.location_description is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the location name, description, and submit')
            elif st.session_state.log_type == "miscellaneous":
                st.session_state["miscellaneous_note"] = text_cleanup(st.text_area("Input miscellaneous note:"))
                if st.session_state.miscellaneous_note is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter miscellaneous note and submit')
            elif st.session_state.log_type == "overview":
                st.session_state["overview_summary"] = text_cleanup(st.text_area("Input session summary:"))
                if st.session_state.overview_summary is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the session overview/summary and submit')
            elif st.session_state.log_type == "person":
                st.session_state["person_name"] = text_cleanup(st.text_input("Input person name:"))
                st.session_state["person_description"] = text_cleanup(st.text_area("Input person description:"))
                if st.session_state.person_name is not None and st.session_state.person_description is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the person name, description, and submit')
            elif st.session_state.log_type == "quest":
                if st.session_state.quest_type == "Existing":
                    st.session_state["quest_name"] = st.selectbox("Select quest to update", elastic_get_quests())
                else:
                    st.session_state["quest_name"] = st.text_input("Input quest name:")
                st.session_state["quest_description"] = text_cleanup(st.text_area("Input quest description / update:"))
                st.session_state["quest_finished"] = st.checkbox("Is the quest finished?")
                if st.session_state.quest_name is not None and st.session_state.quest_description is not None:
                    st.session_state["ready_for_submission"] = True
                else:
                    st.warning('Please enter the quest name, description, mark the status, and submit')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Processing / Indexing
&lt;/h3&gt;

&lt;p&gt;The second half of the code manipulates data inside of the variables set in the first half of code, builds payloads, and sends those off for indexing into Elastic.&lt;/p&gt;

&lt;p&gt;The processing / indexing code consists of lines 90-168:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# submit form, process data, and index log_payload
        st.session_state["submitted"] = st.form_submit_button("Submit")
        if st.session_state.submitted == True and st.session_state.ready_for_submission == True:
            # audio to text transcription
            if st.session_state.log_type == "audio":
                if assemblyai_api_key:
                    st.session_state["transcribed_text"] = text_cleanup(transcribe_audio_paid(st.session_state.file))
                else:
                    st.session_state["transcribed_text"] = text_cleanup(transcribe_audio_free(st.session_state.file))
                if st.session_state.transcribed_text not in (None,""):
                    chunk_array = split_text_with_overlap(st.session_state.transcribed_text)
                    for chunk in chunk_array:
                        st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". " + chunk
                        st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                        if st.session_state.content_vector == None:
                            error_message("AI API vectorization failure",2)
                        else:
                            st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.transcribed_text,"content":st.session_state.content,"content_vector":st.session_state.content_vector})
                            elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # location logs
            elif st.session_state.log_type == "location":
                chunk_array = split_text_with_overlap(st.session_state.location_description)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". The location is " + st.session_state.location_name + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.location_name + ". " + st.session_state.location_description,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"location":{"name":st.session_state.location_name,"description":st.session_state.location_description}})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # miscellaneous logs
            elif st.session_state.log_type == "miscellaneous":
                chunk_array = split_text_with_overlap(st.session_state.miscellaneous_note)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.miscellaneous_note,"content":st.session_state.content,"content_vector":st.session_state.content_vector})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # overview logs
            elif st.session_state.log_type == "overview":
                chunk_array = split_text_with_overlap(st.session_state.overview_summary)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.overview_summary,"content":st.session_state.content,"content_vector":st.session_state.content_vector})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # person logs
            elif st.session_state.log_type == "person":
                chunk_array = split_text_with_overlap(st.session_state.person_description)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". The person's name is " + st.session_state.person_name + ". " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.person_name + ". " + st.session_state.person_description,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"person":{"name":st.session_state.person_name,"description":st.session_state.person_description}})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
            # quest logs
            elif st.session_state.log_type == "quest":
                if st.session_state.quest_finished == True:
                    elastic_update_quest_status(st.session_state.quest_name)
                    status = "The quest has been completed."
                else:
                    status = "The quest has not been completed yet."
                chunk_array = split_text_with_overlap(st.session_state.quest_description)
                for chunk in chunk_array:
                    st.session_state["content"] = "This note is from session " + str(st.session_state.log_session) + ". The quest is " + st.session_state.quest_name + ". " + status + " " + chunk
                    st.session_state["content_vector"] = api_get_vector_object(st.session_state.content)
                    if st.session_state.content_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.quest_name + ". " + st.session_state.quest_description + status,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"quest":{"name":st.session_state.quest_name,"description":st.session_state.quest_description,"finished":st.session_state.quest_finished}})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Data Structure
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Audio Logs
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.transcribed_text,"content":st.session_state.content,"content_vector":st.session_state.content_vector}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;: a unique identifier for a log or group of logs if split by text chunking function&lt;br&gt;
&lt;strong&gt;type&lt;/strong&gt;: what kind of log it is (audio, location, etc.)&lt;br&gt;
&lt;strong&gt;session&lt;/strong&gt;: the session number&lt;br&gt;
&lt;strong&gt;message&lt;/strong&gt;: the entire block of transcribed text&lt;br&gt;
&lt;strong&gt;content&lt;/strong&gt;: a combination of all relevant information (session number, etc.) and the chunk of text provided by the text chunking function&lt;br&gt;
&lt;strong&gt;content_vector&lt;/strong&gt;: the vector object of the content field, used by Veverbot for returning relevant results&lt;/p&gt;

&lt;h5&gt;
  
  
  Location Logs
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.location_name + ". " + st.session_state.location_description,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"location":{"name":st.session_state.location_name,"description":st.session_state.location_description}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;: a unique identifier for a log or group of logs if split by text chunking function&lt;br&gt;
&lt;strong&gt;type&lt;/strong&gt;: what kind of log it is (audio, location, etc.)&lt;br&gt;
&lt;strong&gt;session&lt;/strong&gt;: the session number&lt;br&gt;
&lt;strong&gt;message&lt;/strong&gt;: a combination of location name and description&lt;br&gt;
&lt;strong&gt;content&lt;/strong&gt;: a combination of all relevant information (session number, etc.) and the chunk of text provided by the text chunking function&lt;br&gt;
&lt;strong&gt;content_vector&lt;/strong&gt;: the vector object of the content field, used by Veverbot for returning relevant results&lt;br&gt;
&lt;strong&gt;location.name&lt;/strong&gt;: the name of the location, to be used in the glossary/index&lt;br&gt;
&lt;strong&gt;location.description&lt;/strong&gt;: the description of the location, to be used in the glossary/index&lt;/p&gt;

&lt;h5&gt;
  
  
  Miscellaneous Logs
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.miscellaneous_note,"content":st.session_state.content,"content_vector":st.session_state.content_vector}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;: a unique identifier for a log or group of logs if split by text chunking function&lt;br&gt;
&lt;strong&gt;type&lt;/strong&gt;: what kind of log it is (audio, location, etc.)&lt;br&gt;
&lt;strong&gt;session&lt;/strong&gt;: the session number&lt;br&gt;
&lt;strong&gt;message&lt;/strong&gt;: the entire block of note text&lt;br&gt;
&lt;strong&gt;content&lt;/strong&gt;: a combination of all relevant information (session number, etc.) and the chunk of text provided by the text chunking function&lt;br&gt;
&lt;strong&gt;content_vector&lt;/strong&gt;: the vector object of the content field, used by Veverbot for returning relevant results&lt;/p&gt;

&lt;h5&gt;
  
  
  Overview Logs
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.overview_summary,"content":st.session_state.content,"content_vector":st.session_state.content_vector}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;: a unique identifier for a log or group of logs if split by text chunking function&lt;br&gt;
&lt;strong&gt;type&lt;/strong&gt;: what kind of log it is (audio, location, etc.)&lt;br&gt;
&lt;strong&gt;session&lt;/strong&gt;: the session number&lt;br&gt;
&lt;strong&gt;message&lt;/strong&gt;: the entire block of overview text&lt;br&gt;
&lt;strong&gt;content&lt;/strong&gt;: a combination of all relevant information (session number, etc.) and the chunk of text provided by the text chunking function&lt;br&gt;
&lt;strong&gt;content_vector&lt;/strong&gt;: the vector object of the content field, used by Veverbot for returning relevant results&lt;/p&gt;

&lt;h5&gt;
  
  
  Person Logs
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.person_name + ". " + st.session_state.person_description,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"person":{"name":st.session_state.person_name,"description":st.session_state.person_description}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;: a unique identifier for a log or group of logs if split by text chunking function&lt;br&gt;
&lt;strong&gt;type&lt;/strong&gt;: what kind of log it is (audio, location, etc.)&lt;br&gt;
&lt;strong&gt;session&lt;/strong&gt;: the session number&lt;br&gt;
&lt;strong&gt;message&lt;/strong&gt;: a combination of person name and description&lt;br&gt;
&lt;strong&gt;content&lt;/strong&gt;: a combination of all relevant information (session number, etc.) and the chunk of text provided by the text chunking function&lt;br&gt;
&lt;strong&gt;content_vector&lt;/strong&gt;: the vector object of the content field, used by Veverbot for returning relevant results&lt;br&gt;
&lt;strong&gt;person.name&lt;/strong&gt;: the name of the NPC, to be used in the glossary/index&lt;br&gt;
&lt;strong&gt;person.description&lt;/strong&gt;: the description of the NPC, to be used in the glossary/index&lt;/p&gt;

&lt;h5&gt;
  
  
  Quest Logs
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":st.session_state.log_id,"type":st.session_state.log_type,"session":st.session_state.log_session,"message":st.session_state.quest_name + ". " + st.session_state.quest_description + status,"content":st.session_state.content,"content_vector":st.session_state.content_vector,"quest":{"name":st.session_state.quest_name,"description":st.session_state.quest_description,"finished":st.session_state.quest_finished}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;id&lt;/strong&gt;: a unique identifier for a log or group of logs if split by text chunking function&lt;br&gt;
&lt;strong&gt;type&lt;/strong&gt;: what kind of log it is (audio, location, etc.)&lt;br&gt;
&lt;strong&gt;session&lt;/strong&gt;: the session number&lt;br&gt;
&lt;strong&gt;message&lt;/strong&gt;: a combination of quest name, description, and status&lt;br&gt;
&lt;strong&gt;content&lt;/strong&gt;: a combination of all relevant information (session number, etc.) and the chunk of text provided by the text chunking function&lt;br&gt;
&lt;strong&gt;content_vector&lt;/strong&gt;: the vector object of the content field, used by Veverbot for returning relevant results&lt;br&gt;
&lt;strong&gt;quest.name&lt;/strong&gt;: the name of the quest, to be used in the glossary/index&lt;br&gt;
&lt;strong&gt;quest.description&lt;/strong&gt;: the description/update of the quest, to be used in the glossary/index&lt;br&gt;
&lt;strong&gt;quest.finished&lt;/strong&gt;: the status of the quest, to be used in the glossary/index&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;Overall, the rewrite went smoothly! I feel that I can do much more with the new structure and it will be easier to add fields in the future under the location, person, and quest objects.&lt;/p&gt;

&lt;p&gt;Next week, I may begin talking about the new player dashboard that will be replacing the home page. However, it has come to my attention that the password reset functionality is broken, so I may be fixing that instead.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 13 - Text Chunking</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 08 Dec 2023 15:44:00 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-13-text-chunking-30m7</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-13-text-chunking-30m7</guid>
      <description>&lt;p&gt;In the last post we talked about how Veverbot works. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-12-veverbot-asking-questions-and-receiving-answers-3lgf"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Chunking
&lt;/h2&gt;

&lt;p&gt;Chunking is the process of breaking something large into smaller, more manageable pieces. For example, the free audio transcription method uses this on the audio file. You can see that &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-10-audio-transcription-changes-11ij"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While using Veverbot, I noticed that larger text passages were awful for returning relevant information back to the AI assistant. To make Veverbot better, I have been working on breaking these large text passages into smaller ones with context; meaning that the text chunks have some overlap in order to return better responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python Function
&lt;/h3&gt;

&lt;p&gt;Accomplishing chunking with overlap ended up being fairly easy. Using the &lt;a href="https://www.nltk.org/"&gt;Natural Language Toolkit&lt;/a&gt;, specifically Punkt, we are able to tokenize text passages into an array of sentences. From there, we can loop through this array and check the length of the chunk and sentence. If the sum is greater than the chunk_size variable, it is added to the chunks array and the overlap is calculated. The overlap is calculated the same way, except in reverse, which makes this process quite fast. When it is finished, the function returns an array of text chunks to use in the log_payload for Elastic indexing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def split_text_with_overlap(text, chunk_size=500, overlap_size=100):
    # download punky and initialize tokenizer
    nltk.download("punkt")
    tokenizer = nltk.tokenize.punkt.PunktSentenceTokenizer()

    # separate text into an array of sentences
    array = tokenizer.tokenize(text)

    # if length of text chunk &amp;gt; 500, index document
    # afterwards, prepend previous 100 characters for context overlap
    chunks = []
    chunk = ""
    for index, sentence in enumerate(array):
        if (len(chunk) + len(sentence)) &amp;gt;= chunk_size:
            chunks.append(chunk)

            overlap = ""
            overlap_length = len(overlap)
            overlap_index = index - 1
            while ((overlap_length + len(array[overlap_index])) &amp;lt; overlap_size) and overlap_index != -1:
                overlap = (array[overlap_index] + overlap)
                overlap_length = len(overlap)
                overlap_index = overlap_index - 1
            chunk = overlap + sentence
        else:
            chunk += sentence
    # index last bit of text that may not hit length limit
    chunks.append(chunk)

    return chunks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I will show how this process fits into note input once I finish my rewrite of that page. It is almost done and I am super happy with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;I am quite pleased with how this process panned out. It works very well and it is lightning fast, which is something that I was worried about.&lt;/p&gt;

&lt;p&gt;I plan on finishing my note input rewrite by next week so I hope to talk about that in the next post. If not, I can begin talking about the new player dashboard that will be replacing the home page.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>elasticsearch</category>
      <category>python</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 12 - Veverbot - Asking Questions and Receiving Answers</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 17 Nov 2023 17:31:20 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-12-veverbot-asking-questions-and-receiving-answers-3lgf</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-12-veverbot-asking-questions-and-receiving-answers-3lgf</guid>
      <description>&lt;p&gt;In the last post we talked about Veverbot and data vectorization. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-11-veverbot-data-vectorization-5f9g"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The first bit of this post will be similar to the last post. If you are caught up, you can skip ahead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Veverbot
&lt;/h2&gt;

&lt;p&gt;Veverbot is my own custom AI assistant that aims to help players get quick answers about things that happened during their campaign so far. This is absolutely a work-in-progress, but even the first iteration of him is very cool.&lt;/p&gt;

&lt;p&gt;We have already talked about the logging process, so today I will be talking about what needs to be done to ask questions and receive answers from Veverbot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elastic Configuration
&lt;/h3&gt;

&lt;p&gt;To refresh your memory, I want to provide the Elastic templates in place for this data. Currently, I am using two templates: one for the "dnd-notes-*" indices, and another for an index named "virtual_dm-questions_answers". The second index contains the questions that players ask Veverbot, as well as the responses that Veverbot provides back to the players.&lt;/p&gt;

&lt;h4&gt;
  
  
  dnd-notes-* component template
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
      "name": "dnd-notes",
      "component_template": {
        "template": {
          "mappings": {
            "properties": {
              "@timestamp": {
                "format": "strict_date_optional_time",
                "type": "date"
              },
              "session": {
                "type": "long"
              },
              "name": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "finished": {
                "type": "boolean"
              },
              "message": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "type": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "message_vector": {
                "dims": 1536,
                "similarity": "cosine",
                "index": "true",
                "type": "dense_vector"
              }
            }
          }
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  virtual_dm-questions_answers component template
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
      "name": "virtual_dm-questions_answers",
      "component_template": {
        "template": {
          "mappings": {
            "properties": {
              "question_vector": {
                "dims": 1536,
                "similarity": "cosine",
                "index": "true",
                "type": "dense_vector"
              },
              "answer": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "question": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "answer_vector": {
                "dims": 1536,
                "similarity": "cosine",
                "index": "true",
                "type": "dense_vector"
              }
            }
          }
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The mappings and templates are automatically created via the docker-compose file! This is simply educational, a user will not have to deal with the creation of any of this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Asking Questions
&lt;/h3&gt;

&lt;p&gt;I showed the code for this page in the Streamlit app &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-8-streamlit-changes-b7g"&gt;here&lt;/a&gt;. Definitely go check that out.&lt;/p&gt;

&lt;p&gt;Asking Veverbot a question is fairly straightforward with the chat window implementation -- type a questions into the chat bar!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9qb3l6ipjt8uu5sfc85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9qb3l6ipjt8uu5sfc85.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, the question is stored in a variable, the question is vectorized via FastAPI (see &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-9-fastapi-1an2"&gt;this post&lt;/a&gt;) and stored in another variable. &lt;/p&gt;

&lt;h3&gt;
  
  
  Receiving Answers
&lt;/h3&gt;

&lt;p&gt;To receive an answer, a &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html" rel="noopener noreferrer"&gt;Kibana KNN query&lt;/a&gt; is run with the vectorized question. &lt;/p&gt;

&lt;p&gt;Both the question and the query results are then sent to OpenAI via FastAPI (see above link) to formulate a coherent response. This response is returned to the chat window below the question.&lt;/p&gt;

&lt;p&gt;The question and the response from OpenAI is also stored in an Elastic index for later use that is to be determined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;Full disclosure -- our D&amp;amp;D group hasn't played in a few weeks and I haven't put as much effort into the project in that time. Saying that to say, I have no clue what I will talk about next week. Maybe keeping up with the blog will motivate me to dedicate a few hours each week to this; only time will tell.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid" rel="noopener noreferrer"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
      <category>ai</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 11 - Veverbot - Data Vectorization</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Sat, 04 Nov 2023 15:31:57 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-11-veverbot-data-vectorization-5f9g</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-11-veverbot-data-vectorization-5f9g</guid>
      <description>&lt;p&gt;Last week we talked about audio transcription changes. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-10-audio-transcription-changes-11ij"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Veverbot
&lt;/h2&gt;

&lt;p&gt;Veverbot is my own custom AI assistant that aims to help players get quick answers about things that happened during their campaign so far. This is absolutely a work-in-progress, but even the first iteration of him is very cool.&lt;/p&gt;

&lt;p&gt;This is a fairly involved process, so today I will be talking about what needs to be done from the logging / Elastic configuration side of things in order for Veverbot to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elastic Configuration
&lt;/h3&gt;

&lt;p&gt;For Veverbot to work, we simply need to add/adjust the mappings of index templates. Currently, I am using two templates: one for the "dnd-notes-*" indices, and another for an index named "virtual_dm-questions_answers". The second index contains the questions that players ask Veverbot, as well as the responses that Veverbot provides back to the players.&lt;/p&gt;

&lt;h4&gt;
  
  
  dnd-notes-* component template
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
      "name": "dnd-notes",
      "component_template": {
        "template": {
          "mappings": {
            "properties": {
              "@timestamp": {
                "format": "strict_date_optional_time",
                "type": "date"
              },
              "session": {
                "type": "long"
              },
              "name": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "finished": {
                "type": "boolean"
              },
              "message": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "type": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "message_vector": {
                "dims": 1536,
                "similarity": "cosine",
                "index": "true",
                "type": "dense_vector"
              }
            }
          }
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  virtual_dm-questions_answers component template
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
      "name": "virtual_dm-questions_answers",
      "component_template": {
        "template": {
          "mappings": {
            "properties": {
              "question_vector": {
                "dims": 1536,
                "similarity": "cosine",
                "index": "true",
                "type": "dense_vector"
              },
              "answer": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "question": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "ignore_above": 256,
                    "type": "keyword"
                  }
                }
              },
              "answer_vector": {
                "dims": 1536,
                "similarity": "cosine",
                "index": "true",
                "type": "dense_vector"
              }
            }
          }
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The mappings and templates are automatically created via the docker-compose file! This is simply educational, a user will not have to deal with the creation of any of this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Logging
&lt;/h3&gt;

&lt;p&gt;With the mappings in place, we can now ingest logs with a dense_vector field. If you recall, this step happens on the note input page of Streamlit and is applied to every note that gets sent to Elastic.&lt;/p&gt;

&lt;h4&gt;
  
  
  Audio Note
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;st.session_state["message_vector"] = api_get_vector_object(st.session_state.transcribed_text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Text Note
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;st.session_state["message_vector"] = api_get_vector_object(st.session_state.log_message)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function that gets called simply makes a get request to the FastAPI that was talked about in the week 9 blog post!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def api_get_vector_object(text):
    # returns vector object from supplied text

    fastapi_endpoint = "/get_vector_object/"
    full_url = fastapi_url + fastapi_endpoint + text
    response = requests.get(full_url)

    try:
        message_vector = response.json()
    except:
        message_vector = None
        print(response.content)

    return message_vector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API accepts the text as a variable, creates an embedding via OpenAI, and returns the vector object from the embedding. This vector object is what will allow Veverbot to compare user questions to player notes and return an answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app.get("/get_vector_object/{text}")
async def get_vector_object(text):
    import openai

    openai.api_key = "API_KEY"
    embedding_model = "text-embedding-ada-002"
    openai_embedding = openai.Embedding.create(input=text, model=embedding_model)

    return openai_embedding["data"][0]["embedding"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log is indexed as normal, now with dense_vector field. This field is what will allow Veverbot to compare user questions to player notes and return an answer, which we will talk about next week!&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;As previously stated, next week I will be talking about Veverbot from the Streamlit side. I will essentially walk through the user experience and what is happening in the background to produce the "conversation" that happens on the front end.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
      <category>ai</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 10 - Audio Transcription Changes</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Sat, 28 Oct 2023 14:38:00 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-10-audio-transcription-changes-11ij</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-10-audio-transcription-changes-11ij</guid>
      <description>&lt;p&gt;Last week we talked about FastAPI. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-9-fastapi-1an2"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I decided to write about the audio transcription changes this week, as I finally got some code in place to give users an alternative method. Previously, audio to text was using something called AssemblyAI. However, transcribing 15-20 hours of audio was costing ~$8-15 per month. This code gives users the option to do it for free, though it does take much longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speech Recognition
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/SpeechRecognition/"&gt;Speech Recognition&lt;/a&gt; is a Python library for performing speech recognition via multiple APIs. It has support for both online and offline APIs, which makes it pretty powerful. For our use-case, I utilized the OpenAI Whisper method.&lt;/p&gt;

&lt;p&gt;Here's the full code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def transcribe_audio_free(file_object):
    # get extension
    filename, file_extension = os.path.splitext(file_object.name)

    # create temp file
    with NamedTemporaryFile(suffix=file_extension,delete=False) as temp:
        temp.write(file_object.getvalue())
        temp.seek(0)

        # split file into chunks
        audio = AudioSegment.from_file(temp.name)
        audio_chunks = split_on_silence(audio,
            # experiment with this value for your target audio file
            min_silence_len=3000,
            # adjust this per requirement
            silence_thresh=audio.dBFS-30,
            # keep the silence for 1 second, adjustable as well
            keep_silence=100,
        )

        # create a directory to store the audio chunks
        folder_name = "audio-chunks"
        if not os.path.isdir(folder_name):
            os.mkdir(folder_name)
        whole_text = ""

        # process each chunk 
        for i, audio_chunk in enumerate(audio_chunks, start=1):
            # export audio chunk and save it in the `folder_name` directory.
            chunk_filename = os.path.join(folder_name, f"chunk{i}.wav")
            audio_chunk.export(chunk_filename, format="wav")
            # recognize the chunk
            try:
                # audio to text
                r = sr.Recognizer()
                uploaded_chunk = sr.AudioFile(chunk_filename)
                with uploaded_chunk as source:
                    chunk_audio = r.record(source)
                text = r.recognize_whisper(chunk_audio,"medium")
            except sr.UnknownValueError as e:
                print("Error:", str(e))
            else:
                text = f"{text.capitalize()}. "
                print(chunk_filename, ":", text)
                whole_text += text

        # close temp file
        temp.close()
        os.unlink(temp.name)

    # clean up the audio-chunks folders
    shutil.rmtree(folder_name)

    # return the text for all chunks detected
    return whole_text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's really not much here, so I'll quickly step through the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a temporary file&lt;/li&gt;
&lt;li&gt;Loads temporary file into PyDub and splits it into smaller files&lt;/li&gt;
&lt;li&gt;Creates a directory to store the smaller files&lt;/li&gt;
&lt;li&gt;Iterates through the smaller files
a. Places the file into the directory
b. Performs speech-to-text via Whisper
c. Adds transcribed text to "whole_text" variable&lt;/li&gt;
&lt;li&gt;Closes the temporary file&lt;/li&gt;
&lt;li&gt;Removes the directory&lt;/li&gt;
&lt;li&gt;Returns "whole_text"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You may have to change the values inside of &lt;code&gt;audio_chunks = split_on_silence()&lt;/code&gt; to better work with your file. 3000, -30, 100 was the sweet spot during testing for me.&lt;br&gt;
You may have to use a different model for Whisper. You can change "medium" to a model that better fits your use-case here: &lt;code&gt;text = r.recognize_whisper(chunk_audio,"medium")&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;Please note that the paid method takes significantly less time and is generally worth using in my opinion. I may work on writing in a progress bar for the free method at some point. Regardless, both methods will be available for use.&lt;/p&gt;

&lt;p&gt;Next week, I will begin showing off Veverbot and the mechanisms in place to get him to work. I promise.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 9 - FastAPI</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 20 Oct 2023 17:07:54 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-9-fastapi-1an2</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-9-fastapi-1an2</guid>
      <description>&lt;p&gt;Last week we talked about the changes to the Streamlit application. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-8-streamlit-changes-b7g"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  FastAPI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fastapi.tiangolo.com/"&gt;FastAPI&lt;/a&gt; is a Python library used for creating, you guessed it, APIs. As the name implies, it's quick and completely custom, which is powerful.&lt;/p&gt;

&lt;p&gt;Currently, I have a few endpoints built; both of which help with the functionality of Veverbot. Here's the full API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/04/2023
# 
# FastAPI app that facilitates Virtual DM processes and whatever else I think of.

import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message":"Hello World"}

@app.get("/get_vector_object/{text}")
async def get_vector_object(text):
    import openai

    openai.api_key = "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"
    embedding_model = "text-embedding-ada-002"
    openai_embedding = openai.Embedding.create(input=text, model=embedding_model)

    return openai_embedding["data"][0]["embedding"]

@app.get("/get_question_answer/{question}/{query_results}")
async def get_question_answer(question,query_results):
    import openai

    summary = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Answer the following question:" 
            + question 
            + "by using the following text:" 
            + query_results},
        ]
    )

    answers = []
    for choice in summary.choices:
        answers.append(choice.message.content)

    return answers

if __name__ == '__main__':
    uvicorn.run("main:app", port=8000, host='0.0.0.0', reload=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  API Endpoints
&lt;/h3&gt;

&lt;p&gt;You define custom API endpoints with &lt;code&gt;@app.get()&lt;/code&gt;. The great thing about FastAPI is that it can handle variable input, which is done by including &lt;code&gt;{variable_name}&lt;/code&gt; in the endpoint path. Multiple variable input is supported as well!&lt;/p&gt;

&lt;h4&gt;
  
  
  Root
&lt;/h4&gt;

&lt;p&gt;The root endpoint is simply here to allow us to test if we can access the API from remote locations. If you see "Hello World", then you're good to go!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app.get("/")
async def root():
    return {"message":"Hello World"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Get Vector Object
&lt;/h4&gt;

&lt;p&gt;This endpoint does exactly what the name says: gets a vector object of the variable text input. We then use this vector object in KNN queries to assist Veverbot in returning helpful results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app.get("/get_vector_object/{text}")
async def get_vector_object(text):
    import openai

    openai.api_key = "API_KEY"
    embedding_model = "text-embedding-ada-002"
    openai_embedding = openai.Embedding.create(input=text, model=embedding_model)

    return openai_embedding["data"][0]["embedding"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Get Question Answer
&lt;/h4&gt;

&lt;p&gt;Again, this endpoint does exactly what the name says: returns an answer to a question that is asked to Veverbot. There are two variables here -- the question that is asked to Veverbot, and the KNN query results of the asked question. Both of these are sent to OpenAI and a sentence(s) answer is returned, which is used for Veverbot's response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app.get("/get_question_answer/{question}/{query_results}")
async def get_question_answer(question,query_results):
    import openai

    summary = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Answer the following question:" 
            + question 
            + "by using the following text:" 
            + query_results},
        ]
    )

    answers = []
    for choice in summary.choices:
        answers.append(choice.message.content)

    return answers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;This is a work-in-progess. I have plans to add more endpoints, mainly moving some Python functions over here since it would keep some of the larger ones together. Once I get audio transcription swapped from AssemblyAI to something free, I will probably move that to the API as well.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
      <category>ai</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 8 - Streamlit Changes</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 13 Oct 2023 18:45:37 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-8-streamlit-changes-b7g</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-8-streamlit-changes-b7g</guid>
      <description>&lt;p&gt;Last week we talked about setting up port forwarding in order to access the application remotely. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-7-port-forwarding-50jd"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Streamlit Changes
&lt;/h2&gt;

&lt;p&gt;As I began working on the AI assistant, I quickly ran into an issue: Streamlit's chat widgets could not be added into a sidebar, tab, or other existing container. This meant that I either had to include it on the first "page" in my current code, which was the logon page so that was impossible, or implement a true page system. Luckily enough, Streamlit had support for this per their &lt;a href="https://docs.streamlit.io/library/get-started/multipage-apps/create-a-multipage-app" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Because the code is mostly the same, I won't go in-depth on most of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structure
&lt;/h3&gt;

&lt;p&gt;The Streamlit application now consists of 6 parts: variables, functions, a home page, a note input page, an AI assistant page, and an account page. The home page is in the same directory that "main.py" was in, along with the functions and variables, and the pages are in a new directory named "pages".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kmq3b1wbk35kftkldla.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kmq3b1wbk35kftkldla.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45ia9ndlhxeqnrpl36hw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45ia9ndlhxeqnrpl36hw.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Functions and Variables
&lt;/h4&gt;

&lt;p&gt;The purpose of splitting the variables and functions into their own files was mainly a cleanliness thing. It keeps everything separate and out of the page files. &lt;/p&gt;

&lt;p&gt;Doing this and loading them into other scripts the way I did is generally frowned upon, but it worked out great for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;functions.py&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/04/2023
# 
# Streamlit - Backend - Houses all functions used in pages of the application.

import json
import requests
import streamlit as st
import streamlit_authenticator as stauth
import time
import yaml
from elasticsearch import Elasticsearch
from PIL import Image
from variables import *
from yaml.loader import SafeLoader

### FUNCTIONS ###
def api_get_question_answer(question,query_results):
    # returns an answer to a question asked to virtual DM

    fastapi_endpoint = "/get_question_answer/"
    full_url = fastapi_url + fastapi_endpoint + question + "/" + query_results
    response = requests.get(full_url)

    try:
        answer = response.json()
    except:
        answer = None
        print(response.content)

    return answer

def api_get_vector_object(text):
    # returns vector object from supplied text

    fastapi_endpoint = "/get_vector_object/"
    full_url = fastapi_url + fastapi_endpoint + text
    response = requests.get(full_url)

    try:
        message_vector = response.json()
    except:
        message_vector = None
        print(response.content)

    return message_vector

def text_cleanup(text):
    punctuation = ["/", "?"]
    for symbol in punctuation:
        text = text.replace(symbol," ")

    return text

def clear_session_state(variable_list):
    # deletes variables from streamlit session state
    for variable in variable_list:
        try:
            del st.session_state[variable]
        except:
            pass

def display_image(image_path):
    # displays an image via path relative to streamlit app script
    image = Image.open(image_path)
    st.image(image)

def elastic_ai_notes_query(vector_object):
    # queries Elastic via a KNN query to return answers to questions via virtual DM

    # creates Elastic connection
    client = Elasticsearch(
        elastic_url,
        ca_certs=elastic_ca_certs,
        api_key=elastic_api_key
    )

    # sends document to index with success or failure message
    response = client.search(index="dnd-notes-*",knn={"field":"message_vector","query_vector":vector_object,"k":10,"num_candidates":100})

    return response['hits']['hits'][0]['_source']["message"]

    # close Elastic connection
    client.close()

def elastic_get_quests():
    # queries Elastic for unfinished quests and returns array    
    quest_names = []

    # creates Elastic connection
    client = Elasticsearch(
        elastic_url,
        ca_certs=elastic_ca_certs,
        api_key=elastic_api_key
    )

    # gets unfinished quests
    response = client.search(index=st.session_state.log_index,size=0,query={"bool":{"must":[{"match":{"type.keyword":"quest"}}],"must_not":[{"match":{"finished":"true"}}]}},aggregations={"unfinished_quests":{"terms":{"field":"name.keyword"}}})

    for line in response["aggregations"]["unfinished_quests"]["buckets"]:
        quest_names.append(line["key"])

    return quest_names

    # close Elastic connection
    client.close()

def elastic_index_document(index,document,status_message):
    # sends a document to an Elastic index

    # creates Elastic connection
    client = Elasticsearch(
        elastic_url,
        ca_certs=elastic_ca_certs,
        api_key=elastic_api_key
    )

    # sends document to index with success or failure message
    response = client.index(index=index,document=document)

    if status_message == True:
        if response["result"] == "created":
            success_message("Note creation successful")
        else:
            error_message("Note creation failure",2)
    else:
        pass

    # close Elastic connection
    client.close()

def elastic_kibana_setup(yml_config):
    # creates empty placeholder indices and data views for each player, as well as for transcribed notes

    # builds list of index patterns and descriptive data view names from YAML configuration
    kibana_setup = {"dnd-notes-*":"All Notes","dnd-notes-transcribed":"Audio Transcription Notes","virtual_dm-questions_answers":"Virtual DM Notes"}
    for username in yml_config["credentials"]["usernames"]:
        index = "dnd-notes-" + username
        name = yml_config["credentials"]["usernames"][username]["name"] + "'s Notes"
        kibana_setup[index] = name

    # creates indices and data views from usernames
    for entry in kibana_setup:
        index = entry
        name = kibana_setup[entry]

        # creates Elastic connection
        client = Elasticsearch(
            elastic_url,
            ca_certs=elastic_ca_certs,
            api_key=elastic_api_key
        )

        # creates index if it does not already exist
        response = client.indices.exists(index=index)
        if response != True:
            try:
                client.indices.create(index=index)
            except:
                pass

        # close Elastic connection
        client.close()

        # check if data view already exists
        url = kibana_url + "/api/data_views/data_view/" + index
        auth = "ApiKey " + elastic_api_key
        headers = {"kbn-xsrf":"true","Authorization":auth}
        response = requests.get(url,headers=headers)
        # if data view doesn't exist, create it
        if response.status_code != 200:
            url = kibana_url + "/api/data_views/data_view"
            json = {"data_view":{"title":index,"name":name,"id":index,"timeFieldName":"@timestamp"}}
            response = requests.post(url,headers=headers,json=json)
            # could put some error message here, don't think I need to yet

def elastic_update_quest_status(quest_name):
    # queries Elastic for unfinished quests and returns array

    # creates Elastic connection
    client = Elasticsearch(
        elastic_url,
        ca_certs=elastic_ca_certs,
        api_key=elastic_api_key
    )

    # gets unfinished quests
    query_response = client.search(index=st.session_state.log_index,size=10000,query={"bool":{"must":[{"match":{"name.keyword":quest_name}}],"must_not":[{"match":{"finished":"true"}}]}})

    for line in query_response["hits"]["hits"]:
        line_id = line["_id"]
        update_response = client.update(index="dnd-notes-corver_flickerspring",id=line_id,doc={"finished":st.session_state.quest_finished})

    # close Elastic connection
    client.close()

def error_message(text,timeframe):
    # displays error message    
    error = st.error(text)

    if timeframe == False:
        pass
    else:
        time.sleep(seconds)
        error.empty()

def initialize_session_state(variable_list):
    # creates empty variables in streamlit session state
    for variable in variable_list:
        if variable not in st.session_state:
            st.session_state[variable] = None

def load_yml():
    # loads login authentication configuration    
    with open(streamlit_project_path + "auth.yml") 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']
    )

    return config, authenticator

def success_message(text):
    # displays success message
    success = st.success(text)
    time.sleep(2)
    success.empty()

def transcribe_audio(file):
    # transcribes an audio file to text

    # get file url
    headers = {'authorization':assemblyai_api_key}
    response = requests.post('https://api.assemblyai.com/v2/upload',headers=headers,data=file)
    url = response.json()["upload_url"]
    # get transcribe id
    endpoint = "https://api.assemblyai.com/v2/transcript"
    json = {"audio_url":url}
    headers = {"authorization":assemblyai_api_key,"content-type":"application/json"}
    response = requests.post(endpoint, json=json, headers=headers)
    transcribe_id = response.json()['id']
    result = {}
    #polling
    while result.get("status") != "processing":
        # get text
        endpoint = f"https://api.assemblyai.com/v2/transcript/{transcribe_id}"
        headers = {"authorization":assemblyai_api_key}
        result = requests.get(endpoint, headers=headers).json()

    while result.get("status") != 'completed':
        # get text
        endpoint = f"https://api.assemblyai.com/v2/transcript/{transcribe_id}"
        headers = {"authorization":assemblyai_api_key}
        result = requests.get(endpoint, headers=headers).json()

    return result['text']

def update_yml():
    # updates login authentication configuration file
    with open(streamlit_project_path + "auth.yml", 'w') as file:
        yaml.dump(config, file, default_flow_style=False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;variables.py&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/03/2023
# 
# Streamlit - Backend - Houses variables that are loaded into pages of the application.

### VARIABLES ###
# *** change this to fit your environment ***
assemblyai_api_key = "API_KEY"
elastic_api_key = "API_KEY"

# *** DO NOT CHANGE ***
elastic_url = "https://es01:9200"
elastic_ca_certs = "certs/ca/ca.crt"
fastapi_url = "http://api:8000"
kibana_url = "http://kibana:5601"
streamlit_data_path = "data/"
streamlit_project_path = "streamlit/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Home Page
&lt;/h4&gt;

&lt;p&gt;The home page consists mostly of the old logon page code. When logged in, however, it now displays a welcome message and instructions on how to use the application!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/04/2023
# 
# Streamlit - Main Page - Displays a welcome message and explains how to navigate and use the application.

import streamlit as st
from functions import *
from variables import *

# displays application title
display_image(streamlit_data_path + "banner.png")

# initializes session state, loads login authentication configuration, and performs index/data view setup in Elastic
initialize_session_state(["username"])
config, authenticator = load_yml()
elastic_kibana_setup(config)

# makes user log on to view page
if not st.session_state.username:
    # displays login and registration widgets
    tab1, tab2 = st.tabs(["Login", "Register"])
    # login tab
    with tab1:
        try:
            name,authentication_status,username = authenticator.login("Login","main")
            if authentication_status:
                st.rerun()
            elif authentication_status == False:
                error_message('Username/password is incorrect')
            elif authentication_status == None:
                st.warning('Please enter your username and password')
        except:
            pass
    # registration tab
    with tab2:
        try:
            if authenticator.register_user('Register', preauthorization=True):
                success('User registered successfully')
                update_yml()
        except Exception as e:
            error_message(e)
else:
    st.header('Welcome!',divider=True)
    welcome_message = '''
    ## Elastic D&amp;amp;D is an ongoing project to facilitate note-taking and other functions derived from elements of D&amp;amp;D (Veverbot the AI assistant, roll data, etc.)

    ### You can navigate between pages of the application with the sidebar on the left:
    ##### The Home page is where you can go to refresh your memory on how to use the Elastic D&amp;amp;D application.
    ##### The Note Input page is used for storing notes for viewing and use with Virtual DM functions. Currently, you can input notes via an audio file or text.
    ##### The Veverbot page is an active chat session with your own personal AI assistant! Ask Veverbot questions about your campaign and it will give you answers, hopefully.
    ##### The Account page is used for changing your password and logging off.

    ### Stay up-to-date with the progress of this project on the [Github](https://github.com/thtmexicnkid/elastic-dnd) and the [blog](https://dev.to/thtmexicnkid)!

    ## **Thanks for using Elastic D&amp;amp;D!**
    '''
    st.markdown(welcome_message)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Note Input Page
&lt;/h4&gt;

&lt;p&gt;The note input page consists mostly of the old note input tab code. If trying to access this page while not logged in, it will display an unauthorized message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/04/2023
# 
# Streamlit - Note Input Page - Allows the user to store audio or text notes in Elasticsearch.

import streamlit as st
from functions import *
from variables import *

# displays application title and sets page accordingly
display_image(streamlit_data_path + "banner.png")

# initializes session state, loads login authentication configuration, and performs index/data view setup in Elastic
initialize_session_state(["username"])
config, authenticator = load_yml()

# makes user log on to view page
if not st.session_state.username:
    error_message("UNAUTHORIZED: Please login on the Home page.",False)
else:
    st.header('Note Input',divider=True)
    st.session_state["log_index"] = "dnd-notes-" + st.session_state.username
    st.session_state["note_type"] = st.selectbox("Audio or Text?", ["Audio","Text"], index=0)
    # runs app_page2_* functions depending on what is selected in selectbox
    if st.session_state.note_type == "Audio":
        #list of variables to clear from session state once finished
        audio_form_variable_list = ["log_type","log_session","file","submitted","transcribed_text","log_payload","message_vector"]

        # displays note form widgets, creates note payload, sends payload to an Elastic index, and handles error / success / warning messages
        with st.form("audio_form", clear_on_submit=True):
            st.session_state["log_type"] = "audio"
            st.session_state["log_session"] = st.slider("Which session is this?", 0, 250)
            st.session_state["file"] = st.file_uploader("Choose audio file",type=[".3ga",".8svx",".aac",".ac3",".aif",".aiff",".alac",".amr",".ape",".au",".dss",".flac",".flv",".m2ts",".m4a",".m4b",".m4p",".m4p",".m4r",".m4v",".mogg",".mov",".mp2",".mp3",".mp4",".mpga",".mts",".mxf",".oga",".ogg",".opus",".qcp",".ts",".tta",".voc",".wav",".webm",".wma",".wv"])
            st.session_state["submitted"] = st.form_submit_button("Upload file")
            if st.session_state.submitted and st.session_state.file is not None:
                # removes forward slash that will break the API call for AI functionality
                st.session_state["transcribed_text"] = text_cleanup(transcribe_audio(st.session_state.file))
                if st.session_state.transcribed_text is not None:
                    # gets vector object for use with AI functionality
                    st.session_state["message_vector"] = api_get_vector_object(st.session_state.transcribed_text)
                    if st.session_state.message_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"session":st.session_state.log_session,"type":st.session_state.log_type,"message":st.session_state.transcribed_text,"message_vector":st.session_state.message_vector})
                        elastic_index_document("dnd-notes-transcribed",st.session_state.log_payload,True)
                else:
                    error_message("Audio transcription failure",2)
            else:
                st.warning('Please upload a file and submit')

        # clears session state
        clear_session_state(audio_form_variable_list)
    elif st.session_state.note_type == "Text":
        #list of variables to clear from session state once finished
        text_form_variable_list = ["log_type","log_session","note_taker","log_index","quest_type","quest_name","quest_finished","log_message","submitted","log_payload","message_vector"]

        # displays note form widgets, creates note payload, sends payload to an Elastic index, and handles error / success / warning messages
        st.session_state["log_type"] = st.selectbox("What kind of note is this?", ["location","miscellaneous","overview","person","quest"])
        # displays note form for quest log type
        if st.session_state.log_type == "quest":
            st.session_state["quest_type"] = st.selectbox("Is this a new or existing quest?", ["New","Existing"])
            if st.session_state.quest_type == "New":
                with st.form("text_form_new_quest", clear_on_submit=True):
                    st.session_state["log_session"] = st.slider("Which session is this?", 0, 250)
                    st.session_state["quest_name"] = st.text_input("What is the name of the quest?")
                    st.session_state["quest_finished"] = st.checkbox("Did you finish the quest?")
                    # removes forward slash that will break the API call for AI functionality
                    st.session_state["log_message"] = text_cleanup(st.text_area("Input note text:"))
                    st.session_state["submitted"] = st.form_submit_button("Upload note")
                    if st.session_state.submitted == True and st.session_state.log_message is not None:
                        # gets vector object for use with AI functionality
                        st.session_state["message_vector"] = api_get_vector_object(st.session_state.log_message)
                        if st.session_state.message_vector == None:
                            error_message("AI API vectorization failure",2)
                        else:
                            st.session_state["log_payload"] = json.dumps({"finished":st.session_state.quest_finished,"message":st.session_state.log_message,"name":st.session_state.quest_name,"session":st.session_state.log_session,"type":st.session_state.log_type,"message_vector":st.session_state.message_vector})
                            elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
                            st.rerun()
                    else:
                        st.warning('Please input note text and submit')
            else:
                quest_names = elastic_get_quests()
                with st.form("text_form_existing_quest", clear_on_submit=True):
                    st.session_state["log_session"] = st.slider("Which session is this?", 0, 250)
                    st.session_state["quest_name"] = st.selectbox("Which quest are you updating?", quest_names)
                    st.session_state["quest_finished"] = st.checkbox("Did you finish the quest?")
                    st.session_state["log_message"] = text_cleanup(st.text_area("Input note text:"))
                    st.session_state["submitted"] = st.form_submit_button("Upload note")
                    if st.session_state.submitted == True and st.session_state.log_message is not None:
                        # updates previous quest records to finished: true
                        if st.session_state.quest_finished == True:
                            elastic_update_quest_status(st.session_state.quest_name)
                        else:
                            pass
                        # gets vector object for use with AI functionality
                        st.session_state["message_vector"] = api_get_vector_object(st.session_state.log_message)
                        if st.session_state.message_vector == None:
                            error_message("AI API vectorization failure",2)
                        else:
                            st.session_state["log_payload"] = json.dumps({"finished":st.session_state.quest_finished,"message":st.session_state.log_message,"name":st.session_state.quest_name,"session":st.session_state.log_session,"type":st.session_state.log_type,"message_vector":st.session_state.message_vector})
                            elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
                            st.rerun()
                    else:
                        st.warning('Please input note text and submit')
        # displays note form for all other log types
        else:
            with st.form("text_form_wo_quest", clear_on_submit=True):
                st.session_state["log_session"] = st.number_input("Which session is this?", 0, 250)
                st.session_state["log_message"] = text_cleanup(st.text_area("Input note text:"))
                st.session_state["submitted"] = st.form_submit_button("Upload Note")
                if st.session_state.submitted == True and st.session_state.log_message is not None:
                    # gets vector object for use with AI functionality
                    st.session_state["message_vector"] = api_get_vector_object(st.session_state.log_message)
                    if st.session_state.message_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"message":st.session_state.log_message,"session":st.session_state.log_session,"type":st.session_state.log_type,"message_vector":st.session_state.message_vector})
                        elastic_index_document(st.session_state.log_index,st.session_state.log_payload,True)
                        st.rerun()
                else:
                    st.warning('Please input note text and submit')

        # clears session state
        clear_session_state(text_form_variable_list)
    else:
        pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The AI Assistant Page
&lt;/h4&gt;

&lt;p&gt;Meet Veverbot, your D&amp;amp;D AI assistant! This page is all brand new code. I will be getting into this in-depth in a couple of weeks, but here is your preview! If trying to access this page while not logged in, it will display an unauthorized message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/04/2023
# 
# Streamlit - Virtual DM Page - Allows the user to ask questions and receive answers automatically.

import streamlit as st
from functions import *
from variables import *

# displays application title and sets page accordingly
display_image(streamlit_data_path + "banner.png")

# initializes session state, loads login authentication configuration, and performs index/data view setup in Elastic
initialize_session_state(["username"])
config, authenticator = load_yml()

# makes user log on to view page
if not st.session_state.username:
    error_message("UNAUTHORIZED: Please login on the Home page.",False)
else:
    st.header('Veverbot',divider=True)
    st.session_state["log_index"] = "dnd-notes-" + st.session_state.username
    virtual_dm_variable_list = ["question","response","question_vector","query_results","answer","answer_vector","log_payload"]

    # 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"])

    # React to user input
    st.session_state["question"] = st.chat_input("Ask Veverbot a question")
    if st.session_state.question:
        st.session_state["question"] = text_cleanup(st.session_state.question)
        # Display user message in chat message container
        st.chat_message("user").markdown(st.session_state.question)
        # Add user message to chat history
        st.session_state.messages.append({"role": "user", "content": st.session_state.question})
        # Display assistant response in chat message container
        response = f"Veverbot searching for answer to the question -- \"{st.session_state.question}\""
        with st.chat_message("assistant"):
            st.markdown(response)
            st.session_state.messages.append({"role": "assistant", "content": response})
            # gets vector object for use with AI functionality
            st.session_state["question_vector"] = api_get_vector_object(st.session_state.question)
            if st.session_state.question_vector == None:
                error_message("AI API vectorization failure",2)
            else:
                st.session_state["query_results"] = elastic_ai_notes_query(st.session_state.question_vector)
                st.session_state["answers"] = api_get_question_answer(st.session_state.question,st.session_state.query_results)
                for answer in st.session_state.answers:
                    st.markdown(answer)
                    st.session_state.messages.append({"role": "assistant", "content": answer})
                    st.session_state["answer_vector"] = api_get_vector_object(answer)
                    if st.session_state.answer_vector == None:
                        error_message("AI API vectorization failure",2)
                    else:
                        st.session_state["log_payload"] = json.dumps({"question":st.session_state.question,"question_vector":st.session_state.question_vector,"answer":answer,"answer_vector":st.session_state.answer_vector})
                        elastic_index_document("virtual_dm-questions_answers",st.session_state.log_payload,False)

    clear_session_state(virtual_dm_variable_list)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Account Page
&lt;/h4&gt;

&lt;p&gt;The note input page consists mostly of the old account tab code. If trying to access this page while not logged in, it will display an unauthorized message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Elastic D&amp;amp;D
# Author: thtmexicnkid
# Last Updated: 10/04/2023
# 
# Streamlit - Account Page - Allows the user to change their password and log out.

import streamlit as st
from functions import *
from variables import *

# displays application title and sets page accordingly
display_image(streamlit_data_path + "banner.png")

# initializes session state, loads login authentication configuration, and performs index/data view setup in Elastic
initialize_session_state(["username"])
config, authenticator = load_yml()

# makes user log on to view page
if not st.session_state.username:
    error_message("UNAUTHORIZED: Please login on the Home page.",False)
else:
    st.header('Account',divider=True)
    try:
        if authenticator.reset_password(st.session_state.username, 'Reset password'):
            success_message('Password modified successfully')
            update_yml()
    except Exception as e:
        error_message(e,2)
    authenticator.logout('Logout', 'main')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;I really like the way that the Streamlit application turned out. It is organized, neat, and functions great with this page structure.&lt;/p&gt;

&lt;p&gt;Next week, I want to get into the code for the API I am actively working on. Mostly, it is handling functions for the AI assistant, so it would make sense to get into that next.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid" rel="noopener noreferrer"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 7 - Port Forwarding</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Sat, 07 Oct 2023 17:17:55 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-7-port-forwarding-50jd</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-7-port-forwarding-50jd</guid>
      <description>&lt;p&gt;Last week we talked about moving to a docker implementation. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-6-docker-implementation-2n71"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Port Forwarding
&lt;/h2&gt;

&lt;p&gt;Port Forwarding is the process of allowing remote devices to connect to local devices by means of network redirection via a router or firewall.&lt;/p&gt;

&lt;p&gt;As far as for Elastic D&amp;amp;D, this process is necessary to expose both Kibana and Streamlit to the internet so my group members in other countries can still use the application. In my use-case, all configuration is done in my router settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  How-To
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This may be specific to my router. Your settings may be in a different menu, called something completely different, etc. Please be mindful of that!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Find your default gateway address
Open Command Prompt, run "ipconfig", and grab your "default gateway" address. This should allow you to log into your router.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cFC9SA6R--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ioqz2yzj9hm4p4u7wzj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cFC9SA6R--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ioqz2yzj9hm4p4u7wzj.png" alt="Image description" width="637" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open a web browser and navigate to your default gateway address
&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zm1oaLLZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfn873ae38sdv98sauo5.png" alt="Image description" width="435" height="77"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You may need a password to access your router settings or certain menus. You can usually find this on the back of your router.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Navigate to port forwarding settings&lt;br&gt;
Per my router, port forwarding settings are under Firewall -&amp;gt; NAT/Gaming.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ecZ48hHv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65dlq1bq72rf017fw7g3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ecZ48hHv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65dlq1bq72rf017fw7g3.png" alt="Image description" width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure port forwarding for both Kibana and Streamlit&lt;br&gt;
Per my router, I set up a service entry for both Kibana and Streamlit...&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rVPICjTP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yj8kg3gxa7btpaf8znyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rVPICjTP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yj8kg3gxa7btpaf8znyq.png" alt="Image description" width="475" height="268"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IY8I_vqy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdpsuivf6hmb7mbjcwyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IY8I_vqy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdpsuivf6hmb7mbjcwyh.png" alt="Image description" width="468" height="266"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...and then pointed the service applications to my device.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Mcn-iXdY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a5pgoy7ohdiuty4w4tr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Mcn-iXdY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a5pgoy7ohdiuty4w4tr8.png" alt="Image description" width="620" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access the applications via your public IP address
I found my public IP address via &lt;a href="https://www.whatismyip.com/"&gt;https://www.whatismyip.com/&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kibana can now be accessed by remote machines at &lt;a href="http://PUBLIC_IP:5601"&gt;http://PUBLIC_IP:5601&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Streamlit can now be accessed by remote machines at &lt;a href="http://PUBLIC_IP:8501"&gt;http://PUBLIC_IP:8501&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is only for remote access. If you are trying to access the application from the local network, you need to use "localhost" instead of the public IP. I learned this the hard way...for almost 3 weeks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;This part took a very long time because, as I mentioned above, I was testing from my local network instead of remotely. Definitely a lesson learned.&lt;/p&gt;

&lt;p&gt;I also got the chance to rewrite the entire Streamlit app to utilize pages! This allowed me to proceed with Veverbot, your very own D&amp;amp;D AI assistant! Lots of cool stuff to talk about in the coming weeks.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>network</category>
      <category>networking</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 6 - Docker Implementation</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 29 Sep 2023 15:27:41 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-6-docker-implementation-2n71</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-6-docker-implementation-2n71</guid>
      <description>&lt;p&gt;Last week we talked about the audio note input tab. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-5-audio-note-input-3bpj"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;After finishing the first iteration of the Streamlit application, I started thinking about how to make this project accessible to a wider group of people. In it's current state, you had to know how to configure Elasticsearch/Kibana, as well as have an environment to effectively run them, in addition to running the Python Streamlit application. I had heard about Docker, but I had never used it before; so I decided to give it a try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; is a product that serves virtualization in containers on a host machine.&lt;/p&gt;

&lt;p&gt;I utilize &lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt; to perform all of the setup for me, creating necessary volumes, networks, and containers. The full Docker Compose file is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "3.8"

volumes:
    certs:
        driver: local
    esdata01:
        driver: local
    kibanadata:
        driver: local
    streamlitdata:
        driver: local

networks:
    default:
        name: elastic-dnd-internal
        external: false

services:
    setup:
        image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
        volumes:
            - certs:/usr/share/elasticsearch/config/certs
        user: "0"
        command: &amp;gt;
            bash -c '
                if [ x${ELASTIC_PASSWORD} == x ]; then
                    echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
                    exit 1;
                elif [ x${KIBANA_PASSWORD} == x ]; then
                    echo "Set the KIBANA_PASSWORD environment variable in the .env file";
                    exit 1;
                fi;
                if [ ! -f config/certs/ca.zip ]; then
                    echo "Creating CA";
                    bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
                    unzip config/certs/ca.zip -d config/certs;
                fi;
                if [ ! -f config/certs/certs.zip ]; then
                    echo "Creating certs";
                    echo -ne \
                    "instances:\n"\
                    "  - name: es01\n"\
                    "    dns:\n"\
                    "      - es01\n"\
                    "      - localhost\n"\
                    "    ip:\n"\
                    "      - 127.0.0.1\n"\
                    "  - name: kibana\n"\
                    "    dns:\n"\
                    "      - kibana\n"\
                    "      - localhost\n"\
                    "    ip:\n"\
                    "      - 127.0.0.1\n"\
                    &amp;gt; config/certs/instances.yml;
                    bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
                    unzip config/certs/certs.zip -d config/certs;
                fi;
                echo "Setting file permissions"
                chown -R root:root config/certs;
                find . -type d -exec chmod 750 \{\} \;;
                find . -type f -exec chmod 640 \{\} \;;
                echo "Waiting for Elasticsearch availability";
                until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
                echo "Setting kibana_system password";
                until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_ingest/pipeline/add_timestamp -d "{\"description\":\"Pipeline to automatically add @timestamp to incoming logs.\",\"processors\":[{\"set\":{\"field\":\"@timestamp\",\"value\":\"{{_ingest.timestamp}}\",\"ignore_empty_value\":true,\"ignore_failure\":true}}]}"
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_ingest/pipeline/dnd-notes -d "{\"description\":\"Pipeline to manipulate dnd notes logs.\",\"processors\":[{\"pipeline\":{\"name\":\"add_timestamp\"}}]}"
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_component_template/dnd-notes -d "{\"template\":{\"mappings\":{\"dynamic\":\"true\",\"dynamic_date_formats\":[\"strict_date_optional_time\",\"yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z\"],\"dynamic_templates\":[],\"date_detection\":true,\"numeric_detection\":false,\"properties\":{\"@timestamp\":{\"type\":\"date\",\"format\":\"strict_date_optional_time\"},\"finished\":{\"type\":\"boolean\"},\"message\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}},\"name\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}},\"session\":{\"type\":\"long\"},\"type\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}}}}}}"
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_index_template/dnd-notes -d "{\"index_patterns\":[\"dnd-notes-*\"],\"template\":{\"settings\":{\"index\":{\"number_of_shards\":\"1\",\"number_of_replicas\":\"0\",\"default_pipeline\":\"dnd-notes\"}},\"mappings\":{\"_routing\":{\"required\":false},\"numeric_detection\":false,\"dynamic_date_formats\":[\"strict_date_optional_time\",\"yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z\"],\"dynamic\":true,\"_source\":{\"excludes\":[],\"includes\":[],\"enabled\":true},\"dynamic_templates\":[],\"date_detection\":true}},\"composed_of\":[\"dnd-notes\"]}"
                echo "All done!";
            '
        healthcheck:
            test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
            interval: 1s
            timeout: 5s
            retries: 120
    es01:
        depends_on:
            setup:
                condition: service_healthy
        image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
        labels:
            co.elastic.logs/module: elasticsearch
        volumes:
            - certs:/usr/share/elasticsearch/config/certs
            - esdata01:/usr/share/elasticsearch/data
        ports:
            - ${ES_PORT}:9200
        environment:
            - node.name=es01
            - cluster.name=${CLUSTER_NAME}
            - discovery.type=single-node
            - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
            - bootstrap.memory_lock=true
            - xpack.security.enabled=true
            - xpack.security.http.ssl.enabled=true
            - xpack.security.http.ssl.key=certs/es01/es01.key
            - xpack.security.http.ssl.certificate=certs/es01/es01.crt
            - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
            - xpack.security.transport.ssl.enabled=true
            - xpack.security.transport.ssl.key=certs/es01/es01.key
            - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
            - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
            - xpack.security.transport.ssl.verification_mode=certificate
            - xpack.license.self_generated.type=${LICENSE}
        mem_limit: ${ES_MEM_LIMIT}
        ulimits:
            memlock:
                soft: -1
                hard: -1
        healthcheck:
            test:
                [
                "CMD-SHELL",
                "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
                ]
            interval: 10s
            timeout: 10s
            retries: 120
    api:
        depends_on:
            es01:
                condition: service_healthy
        build:
            dockerfile: .\dockerfile-api
            context: .\
        ports:
            - ${API_PORT}:8000
        volumes:
            - '.\data:/usr/src/app/data:delegated'
            - '.\project\api:/usr/src/app/api:delegated'
    kibana:
        depends_on:
            es01:
                condition: service_healthy
        image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
        labels:
            co.elastic.logs/module: kibana
        volumes:
            - certs:/usr/share/kibana/config/certs
            - kibanadata:/usr/share/kibana/data
        ports:
            - ${KIBANA_PORT}:5601
        environment:
            - SERVERNAME=kibana
            - ELASTICSEARCH_HOSTS=https://es01:9200
            - ELASTICSEARCH_USERNAME=kibana_system
            - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
            - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt
            - XPACK_SECURITY_ENCRYPTIONKEY=${ENCRYPTION_KEY}
            - XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${ENCRYPTION_KEY}
            - XPACK_REPORTING_ENCRYPTIONKEY=${ENCRYPTION_KEY}
        mem_limit: ${KB_MEM_LIMIT}
        healthcheck:
            test:
                [
                "CMD-SHELL",
                "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'",
                ]
            interval: 10s
            timeout: 10s
            retries: 120
    streamlit:
        depends_on:
            kibana:
                condition: service_healthy
        build:
            dockerfile: .\dockerfile-streamlit
            context: .\
        ports:
            - ${STREAMLIT_PORT}:8501
        volumes:
            - certs:/usr/src/app/certs
            - '.\data:/usr/src/app/data:delegated'
            - '.\project\streamlit:/usr/src/app/streamlit:delegated'
            - '.\.streamlit:/usr/src/app/.streamlit:delegated'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first few lines take care of setting up "volumes", which are essentially data drives that store information for Docker to use, "networks", which are internal or external networks that Docker can use, and "services", which are the containers.&lt;/p&gt;

&lt;p&gt;As you can see, my current Docker implementation consists of 3 Elastic containers (a setup container, an Elasticsearch container, and a Kibana container), and 2 Python containers (a Streamlit container, and a FastAPI container).&lt;/p&gt;

&lt;h3&gt;
  
  
  Elastic Containers
&lt;/h3&gt;

&lt;p&gt;Funnily enough, the Elastic containers were quite easy to set up because of a great article by my contact for this project -- the man himself: Eddie. Check it out &lt;a href="https://www.elastic.co/blog/getting-started-with-the-elastic-stack-and-docker-compose"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;For the most part, I followed this guide and added additional pieces to automate some settings, templates, etc. associated with this project.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The .env file in the project directory is very important here. It defines passwords, port numbers, names, etc. for use with the variables inside of the Docker Compose file. &lt;strong&gt;Be sure to set these variables before trying to set this up!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Setup Container
&lt;/h4&gt;

&lt;p&gt;The setup container sets up passwords, creates certs, and places the Elastic D&amp;amp;D backend pipelines and templates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setup:
        image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
        volumes:
            - certs:/usr/share/elasticsearch/config/certs
        user: "0"
        command: &amp;gt;
            bash -c '
                if [ x${ELASTIC_PASSWORD} == x ]; then
                    echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
                    exit 1;
                elif [ x${KIBANA_PASSWORD} == x ]; then
                    echo "Set the KIBANA_PASSWORD environment variable in the .env file";
                    exit 1;
                fi;
                if [ ! -f config/certs/ca.zip ]; then
                    echo "Creating CA";
                    bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
                    unzip config/certs/ca.zip -d config/certs;
                fi;
                if [ ! -f config/certs/certs.zip ]; then
                    echo "Creating certs";
                    echo -ne \
                    "instances:\n"\
                    "  - name: es01\n"\
                    "    dns:\n"\
                    "      - es01\n"\
                    "      - localhost\n"\
                    "    ip:\n"\
                    "      - 127.0.0.1\n"\
                    "  - name: kibana\n"\
                    "    dns:\n"\
                    "      - kibana\n"\
                    "      - localhost\n"\
                    "    ip:\n"\
                    "      - 127.0.0.1\n"\
                    &amp;gt; config/certs/instances.yml;
                    bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
                    unzip config/certs/certs.zip -d config/certs;
                fi;
                echo "Setting file permissions"
                chown -R root:root config/certs;
                find . -type d -exec chmod 750 \{\} \;;
                find . -type f -exec chmod 640 \{\} \;;
                echo "Waiting for Elasticsearch availability";
                until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
                echo "Setting kibana_system password";
                until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_ingest/pipeline/add_timestamp -d "{\"description\":\"Pipeline to automatically add @timestamp to incoming logs.\",\"processors\":[{\"set\":{\"field\":\"@timestamp\",\"value\":\"{{_ingest.timestamp}}\",\"ignore_empty_value\":true,\"ignore_failure\":true}}]}"
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_ingest/pipeline/dnd-notes -d "{\"description\":\"Pipeline to manipulate dnd notes logs.\",\"processors\":[{\"pipeline\":{\"name\":\"add_timestamp\"}}]}"
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_component_template/dnd-notes -d "{\"template\":{\"mappings\":{\"dynamic\":\"true\",\"dynamic_date_formats\":[\"strict_date_optional_time\",\"yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z\"],\"dynamic_templates\":[],\"date_detection\":true,\"numeric_detection\":false,\"properties\":{\"@timestamp\":{\"type\":\"date\",\"format\":\"strict_date_optional_time\"},\"finished\":{\"type\":\"boolean\"},\"message\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}},\"name\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}},\"session\":{\"type\":\"long\"},\"type\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}}}}}}"
                curl -s -X PUT --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_index_template/dnd-notes -d "{\"index_patterns\":[\"dnd-notes-*\"],\"template\":{\"settings\":{\"index\":{\"number_of_shards\":\"1\",\"number_of_replicas\":\"0\",\"default_pipeline\":\"dnd-notes\"}},\"mappings\":{\"_routing\":{\"required\":false},\"numeric_detection\":false,\"dynamic_date_formats\":[\"strict_date_optional_time\",\"yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z\"],\"dynamic\":true,\"_source\":{\"excludes\":[],\"includes\":[],\"enabled\":true},\"dynamic_templates\":[],\"date_detection\":true}},\"composed_of\":[\"dnd-notes\"]}"
                echo "All done!";
            '
        healthcheck:
            test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
            interval: 1s
            timeout: 5s
            retries: 120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Elasticsearch Container
&lt;/h4&gt;

&lt;p&gt;The Elasticsearch container creates an Elasticsearch node for storing data and connecting with Kibana, and will only begin when the Setup container is healthy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;es01:
        depends_on:
            setup:
                condition: service_healthy
        image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
        labels:
            co.elastic.logs/module: elasticsearch
        volumes:
            - certs:/usr/share/elasticsearch/config/certs
            - esdata01:/usr/share/elasticsearch/data
        ports:
            - ${ES_PORT}:9200
        environment:
            - node.name=es01
            - cluster.name=${CLUSTER_NAME}
            - discovery.type=single-node
            - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
            - bootstrap.memory_lock=true
            - xpack.security.enabled=true
            - xpack.security.http.ssl.enabled=true
            - xpack.security.http.ssl.key=certs/es01/es01.key
            - xpack.security.http.ssl.certificate=certs/es01/es01.crt
            - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
            - xpack.security.transport.ssl.enabled=true
            - xpack.security.transport.ssl.key=certs/es01/es01.key
            - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
            - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
            - xpack.security.transport.ssl.verification_mode=certificate
            - xpack.license.self_generated.type=${LICENSE}
        mem_limit: ${ES_MEM_LIMIT}
        ulimits:
            memlock:
                soft: -1
                hard: -1
        healthcheck:
            test:
                [
                "CMD-SHELL",
                "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
                ]
            interval: 10s
            timeout: 10s
            retries: 120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Kibana Container
&lt;/h4&gt;

&lt;p&gt;The Kibana container creates a Kibana instance that connects to the Elasticsearch container, and allows users to view their notes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kibana:
        depends_on:
            es01:
                condition: service_healthy
        image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
        labels:
            co.elastic.logs/module: kibana
        volumes:
            - certs:/usr/share/kibana/config/certs
            - kibanadata:/usr/share/kibana/data
        ports:
            - ${KIBANA_PORT}:5601
        environment:
            - SERVERNAME=kibana
            - ELASTICSEARCH_HOSTS=https://es01:9200
            - ELASTICSEARCH_USERNAME=kibana_system
            - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
            - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt
            - XPACK_SECURITY_ENCRYPTIONKEY=${ENCRYPTION_KEY}
            - XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${ENCRYPTION_KEY}
            - XPACK_REPORTING_ENCRYPTIONKEY=${ENCRYPTION_KEY}
        mem_limit: ${KB_MEM_LIMIT}
        healthcheck:
            test:
                [
                "CMD-SHELL",
                "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'",
                ]
            interval: 10s
            timeout: 10s
            retries: 120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python Containers
&lt;/h3&gt;

&lt;p&gt;Both of these containers are built with dockerfiles, which allows for more control of container creation in this instance; especially since we have Python dependencies and have to run the programs with a command.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The dockerfiles in the project directory are very important here. They handle all of the container setup for the applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Streamlit Container
&lt;/h4&gt;

&lt;p&gt;The Streamlit container hosts and runs the application. &lt;em&gt;&lt;strong&gt;I am still working on exposing the application to a public IP address. Getting this piece right will be essential for D&amp;amp;D groups that are not on the same network.&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;streamlit:
        depends_on:
            kibana:
                condition: service_healthy
        build:
            dockerfile: .\dockerfile-streamlit
            context: .\
        ports:
            - ${STREAMLIT_PORT}:8501
        volumes:
            - certs:/usr/src/app/certs
            - '.\data:/usr/src/app/data:delegated'
            - '.\project\streamlit:/usr/src/app/streamlit:delegated'
            - '.\.streamlit:/usr/src/app/.streamlit:delegated'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Streamlit Dockerfile
&lt;/h5&gt;

&lt;p&gt;The Streamlit dockerfile handles installation of Python dependencies, creating directories, and running the application command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;###############
# BUILD IMAGE #
###############
FROM python:3.8.2-slim-buster AS build

# set root user
USER root

# virtualenv
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

# add and install requirements
RUN pip install --upgrade pip
COPY ./requirements-streamlit.txt .
RUN pip install -r requirements-streamlit.txt

#################
# RUNTIME IMAGE #
#################
FROM python:3.8.2-slim-buster AS runtime

# create app directory
RUN mkdir -p /usr/src/app

# copy from build image
COPY --from=build /opt/venv /opt/venv

# set working directory
WORKDIR /usr/src/app

# disables lag in stdout/stderr output
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

# Path
ENV PATH="/opt/venv/bin:$PATH"

# Run streamlit
CMD streamlit run streamlit/main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  FastAPI Container
&lt;/h4&gt;

&lt;p&gt;The FastAPI container hosts and runs the API code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api:
        depends_on:
            es01:
                condition: service_healthy
        build:
            dockerfile: .\dockerfile-api
            context: .\
        ports:
            - ${API_PORT}:8000
        volumes:
            - '.\data:/usr/src/app/data:delegated'
            - '.\project\api:/usr/src/app/api:delegated'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  FastAPI Dockerfile
&lt;/h5&gt;

&lt;p&gt;The FastAPI dockerfile handles installation of Python dependencies, creating directories, and running the Python command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;###############
# BUILD IMAGE #
###############
FROM python:3.8.2-slim-buster AS build

# set root user
USER root

# virtualenv
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

# add and install requirements
RUN pip install --upgrade pip
COPY ./requirements-api.txt .
RUN pip install -r requirements-api.txt

#################
# RUNTIME IMAGE #
#################
FROM python:3.8.2-slim-buster AS runtime

# create app directory
RUN mkdir -p /usr/src/app

# copy from build image
COPY --from=build /opt/venv /opt/venv

# set working directory
WORKDIR /usr/src/app

# disables lag in stdout/stderr output
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

# Path
ENV PATH="/opt/venv/bin:$PATH"

# Run streamlit
CMD python3 api/main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;This section is subject to change. More containers may be added (such as NGINX) as needs arise, especially since I am working with network-related tasks.&lt;/p&gt;

&lt;p&gt;Next week, I will &lt;em&gt;hopefully&lt;/em&gt; be covering my progress of exposing Kibana and Streamlit to my public IP, which allows use of the project by my entire D&amp;amp;D group. I am currently having trouble with Streamlit, so it may not go as planned.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
      <category>docker</category>
    </item>
    <item>
      <title>Elastic D&amp;D - Update 5 - Audio Note Input</title>
      <dc:creator>Joe</dc:creator>
      <pubDate>Fri, 22 Sep 2023 13:29:34 +0000</pubDate>
      <link>https://dev.to/thtmexicnkid/elastic-dd-week-5-audio-note-input-3bpj</link>
      <guid>https://dev.to/thtmexicnkid/elastic-dd-week-5-audio-note-input-3bpj</guid>
      <description>&lt;p&gt;Last week we talked about the text note input tab. If you missed it, you can check that out &lt;a href="https://dev.to/thtmexicnkid/elastic-dd-week-4-text-note-input-3bk7"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Coding the Audio Note Input Tab
&lt;/h2&gt;

&lt;p&gt;This tab was conceptualized with AI and data vectorization in mind. I wanted a way to take the recordings from a session, transcribe audio to text, and index that whole object in Elastic. This data would then give us more data for when we start asking the Virtual DM questions.&lt;/p&gt;

&lt;p&gt;The tab is a single form that appears when you select "Audio" from the log type select box.&lt;/p&gt;

&lt;p&gt;Much like the text note input tab, the goal of the form is to get enough relevant data to form a JSON payload to send to Elastic for indexing. From there your notes are stored and you are able to search them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Using forms is really nice because the data in the widgets is removed once you hit submit. This saves you from having to manually remove your note that you just typed. However, this only removes that data from the GUI, not the session state.&lt;/li&gt;
&lt;li&gt;I used a combination of the "text_form_variable_list" list and the "clear_session_state" function to maintain a clear session state for every note. If this wasn't in here, you may have lingering data in variables and your notes wouldn't be as accurate.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def app_page2_audio():
    # displays audio note form and widgets
    import json

    #list of variables to clear from session state once finished
    audio_form_variable_list = ["log_type","log_session","file","submitted","transcribed_text","log_payload"]

    # displays note form widgets, creates note payload, sends payload to an Elastic index, and handles error / success / warning messages
    with st.form("audio_form", clear_on_submit=True):
        st.session_state["log_type"] = "audio"
        st.session_state["log_session"] = st.slider("Which session is this?", 0, 250)
        st.session_state["file"] = st.file_uploader("Choose audio file",type=[".3ga",".8svx",".aac",".ac3",".aif",".aiff",".alac",".amr",".ape",".au",".dss",".flac",".flv",".m2ts",".m4a",".m4b",".m4p",".m4p",".m4r",".m4v",".mogg",".mov",".mp2",".mp3",".mp4",".mpga",".mts",".mxf",".oga",".ogg",".opus",".qcp",".ts",".tta",".voc",".wav",".webm",".wma",".wv"])
        st.session_state["submitted"] = st.form_submit_button("Upload file")
        if st.session_state.submitted and st.session_state.file is not None:
            st.session_state["transcribed_text"] = transcribe_audio(st.session_state.file)
            if st.session_state.transcribed_text is not None:
                st.session_state["log_payload"] = json.dumps({"session":st.session_state.log_session,"type":st.session_state.log_type,"message":st.session_state.transcribed_text})
                elastic_index_document("dnd-notes-transcribed",st.session_state.log_payload)
            else:
                error_message("Audio transcription failure")
        else:
            st.warning('Please upload a file and submit')

    # clears session state
    clear_session_state(audio_form_variable_list)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Audio Form
&lt;/h3&gt;

&lt;p&gt;As mentioned above, this is the only form for this log type. It has the user input a session number and upload a file. Once submitted, a function turns audio into text, which helps build the JSON payload for indexing into Elastic.&lt;/p&gt;

&lt;h4&gt;
  
  
  Transcribe Audio Function
&lt;/h4&gt;

&lt;p&gt;This function is the workhorse of the audio note input tab and makes use of AssemblyAI. It takes the file that was uploaded and makes an API call to get the file URL, takes the file URL and makes an API call to gets the transcribe ID, and polls until the status is completed. Once the status is completed, the function returns the text object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def transcribe_audio(file):
    # transcribes an audio file to text
    import requests

    # get file url
    headers = {'authorization':assemblyai_api_key}
    response = requests.post('https://api.assemblyai.com/v2/upload',headers=headers,data=file)
    url = response.json()["upload_url"]
    # get transcribe id
    endpoint = "https://api.assemblyai.com/v2/transcript"
    json = {"audio_url":url}
    headers = {"authorization":assemblyai_api_key,"content-type":"application/json"}
    response = requests.post(endpoint, json=json, headers=headers)
    transcribe_id = response.json()['id']
    result = {}
    #polling
    while result.get("status") != "processing":
        # get text
        endpoint = f"https://api.assemblyai.com/v2/transcript/{transcribe_id}"
        headers = {"authorization":assemblyai_api_key}
        result = requests.get(endpoint, headers=headers).json()

    while result.get("status") != 'completed':
        # get text
        endpoint = f"https://api.assemblyai.com/v2/transcript/{transcribe_id}"
        headers = {"authorization":assemblyai_api_key}
        result = requests.get(endpoint, headers=headers).json()

    return result['text']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Related Functions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def clear_session_state(variable_list):
    # deletes variables from streamlit session state
    for variable in variable_list:
        try:
            del st.session_state[variable]
        except:
            pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def elastic_index_document(index,document):
    # sends a document to an Elastic index
    from elasticsearch import Elasticsearch

    # creates Elastic connection
    client = Elasticsearch(
        elastic_url,
        ca_certs=elastic_ca_certs,
        api_key=elastic_api_key
    )

    # sends document to index with success or failure message
    response = client.index(index=index,document=document)

    if response["result"] == "created":
        success_message("Note creation successful")
    else:
        error_message("Note creation failure")

    # close Elastic connection
    client.close()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def error_message(text):
    # displays error message
    import time

    error = st.error(text)
    time.sleep(1)
    error.empty()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;This section is subject to change. As I continue working on making the project more accessible, I may migrate this to a service that doesn't require putting a few dollars into it every month. It is worth it to me and my use case, but I think it's worth trying to make everything completely free.&lt;/p&gt;

&lt;p&gt;Next week, I will be covering the process of moving this project to Docker. I did so to make it much easier for people to set this up for their own D&amp;amp;D groups and I think it is worth talking about.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thtmexicnkid/elastic-dnd"&gt;GitHub Repo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://allmylinks.com/thtmexicnkid"&gt;Socials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Coding,&lt;br&gt;
Joe&lt;/p&gt;

</description>
      <category>python</category>
      <category>elasticsearch</category>
    </item>
  </channel>
</rss>
