
Creating a UI for the Document Conversion Pipeline
- By Bruce Nielson
- ML & AI Specialist
In previous posts (here and here), I introduced Gradio for creating UI for your Artificial Intelligence projects. We already saw how to connect up our RAG Pipeline to the Gradio UI so that we can chat with AI Karl Popper. However, it’s rather inconvenient that we still need to run from a terminal to add new books into our document data. It would be nice if our UI interface included a way to drag and drop files into the database. So, let’s implement that next. My code for this post can be found here.
Laying Out the UI
We’re going to again use Gradio Blocks to lay out the UI. I refactored the UI quite a bit since the last time we looked at it to make room for a document drag and drop interface. The interface, when completed, will look like this:
Notice how we now have three tabs at the top: Chat, Load, and Config. The Chat tab is just the Chat interface we covered previously. We’ll cover the Config tab in a future post. Today, we’re going to concentrate on creating the “Load” tab:
To keep this from being too complicated, I’ve refactored the interface into various sub-functions. Here is what it looks like:
def build_interface(title: str = 'RAG Chat',
system_instructions: str = "You are a helpful assistant.") -> gr.Interface:
…
rag_chat: Optional[RagChat] = None
config_data: dict = {}
load_config()
default_tab: str = "Chat"
if not rag_chat:
# No config settings yet, so set Config tab as default
default_tab: str = "Config"
css: str = """
#QuoteBoxes {
height: calc(100vh - 175px);
overflow-y: auto;
white-space: pre-wrap;
"""
with gr.Blocks(css=css) as chat_interface:
with gr.Tabs(selected=default_tab) as tabs:
chat_components = build_chat_tab(title, default_tab)
load_components = build_load_tab(default_tab)
config_components = build_config_tab(config_data)
# Unpack Chat Tab components
chat_tab = chat_components["chat_tab"]
title_md = chat_components["title_md"]
chatbot = chat_components["chatbot"]
msg = chat_components["msg"]
retrieved_quotes_box = chat_components["retrieved_quotes_box"]
raw_quotes_box = chat_components["raw_quotes_box"]
# Unpack Load Tab components
load_tab = load_components["load_tab"]
file_input = load_components["file_input"]
load_button = load_components["load_button"]
# Unpack Config Tab components
save_settings = config_components["save_settings"]
chat_title_tb = config_components["chat_title_tb"]
sys_inst_box_tb = config_components["sys_inst_box_tb"]
google_secret_tb = config_components["google_secret_tb"]
postgres_secret_tb = config_components["postgres_secret_tb"]
postgres_user_tb = config_components["postgres_user_tb"]
postgres_db_tb = config_components["postgres_db_tb"]
postgres_table_tb = config_components["postgres_table_tb"]
postgres_host_tb = config_components["postgres_host_tb"]
postgres_port_tb = config_components["postgres_port_tb"]
…
def process_with_custom_progress(files, progress=gr.Progress()):
if files is None or len(files) == 0:
# If no files, immediately yield cleared file list and 0% progress.
return
# Call the load_documents method, which now yields progress (a float between 0 and 1)
file_enumerator = rag_chat.load_documents(files)
for i, file in enumerate(files):
file_name = os.path.basename(file)
desc = f"Processing {file_name}"
prog = i / len(files)
progress(prog, desc=desc)
next(file_enumerator)
progress(1.0, desc="Finished processing")
time.sleep(0.5)
return "Finished processing"
def update_progress(files):
# Process the files and return a progress message along with an empty list to clear the widget
process_with_custom_progress(files)
return []
…
msg.submit(user_message, [msg, chatbot], [msg, chatbot], queue=True)
msg.submit(process_message, [msg, chatbot],
[chatbot, retrieved_quotes_box, raw_quotes_box], queue=True)
load_button.click(update_progress, inputs=file_input, outputs=file_input)
save_settings.click(update_config,
inputs=[google_secret_tb, postgres_secret_tb, postgres_user_tb, postgres_db_tb,
postgres_table_tb, postgres_host_tb, postgres_port_tb, chat_title_tb,
sys_inst_box_tb],
outputs=[
google_secret_tb, postgres_secret_tb, postgres_user_tb, postgres_db_tb,
postgres_table_tb, postgres_host_tb, postgres_port_tb, chat_title_tb,
sys_inst_box_tb, title_md, chat_tab, load_tab,
],
queue=True)
return chat_interface
To make this code semi-fit on the screen I removed code that isn’t relevant to this post (like the Config tab functions) and replaced it with ‘…’
This is the main layout for the Load tab:
with gr.Tabs(selected=default_tab) as tabs:
chat_components = build_chat_tab(title, default_tab)
load_components = build_load_tab(default_tab)
config_components = build_config_tab(config_data)
# Unpack Load Tab components
load_tab = load_components["load_tab"]
file_input = load_components["file_input"]
load_button = load_components["load_button"]
Not much there, right? All the real work is now being done in the buildloadtab method:
def build_load_tab(default_tab: str):
with gr.Tab(label="Load", id="Load", interactive=(default_tab == "Chat")) as load_tab:
gr.Markdown("Drag and drop your files here to load them into the database.")
gr.Markdown("Supported file types: PDF and EPUB.")
file_input = gr.File(file_count="multiple", label="Upload a file", interactive=True)
load_button = gr.Button("Load")
return {
"load_tab": load_tab,
"file_input": file_input,
"load_button": load_button,
}
This function creates a tab and then adds some markdown text followed by a Gradio File widget. Finally, we add the load button using a Gradio Button widget. Or in other words:
Of course, the real work is in wiring up the controls to functions:
load_button.click(update_progress, inputs=file_input, outputs=file_input)
This creates a button click event on the load button and takes the File widget as both the input and the output. Let’s now take a look at the update_progress() method:
def update_progress(files):
# Process the files and return a progress message along with an empty list to clear the widget
process_with_custom_progress(files)
return []
def process_with_custom_progress(files, progress=gr.Progress()):
if files is None or len(files) == 0:
# If no files, immediately yield cleared file list and 0% progress.
return
# Call the load_documents method, which now yields progress (a float between 0 and 1)
file_enumerator = rag_chat.load_documents(files)
for i, file in enumerate(files):
file_name = os.path.basename(file)
desc = f"Processing {file_name}"
prog = i / len(files)
progress(prog, desc=desc)
next(file_enumerator)
progress(1.0, desc="Finished processing")
time.sleep(0.5)
return "Finished processing"
What we’re doing is we pass in the data in the File widget as the variable ‘files’ and then call a help function to do the real work. The process_with_custom_progress helper function also passes in a Gradio Progress widget so that we can show a progress bar. We then enumerate over the files that have been dragged into the File widget and run the document conversion pipeline (as discussed in previous posts) for each file. To make this work, I had to rewrite the Document Conversion pipeline to be an enumerator so that I could run one document at a time (i.e. next(file_enumerator) and thus show progress on the progress bar.
Conclusions
And that’s all there is to it! Honestly, the hardest thing to figure out was how to use the Progress bar. Gradio’s documentation is confusing at best, but the above is an example of how to do it. Now, you should be able to do something similar for any app you need now. With this new UI, we now have a way to drag and drop files and then run them through the document conversion pipeline. I didn’t, in this post, show you all the changes I made to allow that to work, but it really just turned the pipeline into an enumerator and allowed it to receive a list of file paths. And voila! It works!