-: Hey there, Eden here, and in this video
we are going to be building our front end
to our RAG retrieval pipeline.
So we are going to be building a very simple user interface
so we can visually test and QA what we've done so far.
And for that we're going to be using Streamlit.
So Streamlit is a very popular open-source package
which enable us to create very intuitive
and simple user interfaces in Python.
And yes, I said Python.
We're not going to be writing any JavaScript code today
because with Streamlit we can use Python.
And we can go for the playground, for example.
And we're going to be doing something very similar
to this chat application you're seeing right here.
So we're going to be writing an interface
in Python to do that.
So we're going to be writing, in Python, an application
that is going to yield something very similar to this.
You can check out here in the playground
some other examples.
And by the way, Streamlit originally started as being a tool
for data scientists to visualize their data.
And you can see here, for example,
here we're displaying some charts over some data,
and it's actually very, very intuitive to use.
All this interface, only a couple of lines
of Python code, which is very convenient.
And just a quick disclaimer, this is not for production,
so I wouldn't recommend you using Streamlit
if you want to expose a chat application.
For that I'm going to make another video about generative UI
and show you how you can do it with TypeScript and Next.js.
Cool. So let's go now.
And I created a new main.py file
and here I'm going to be writing the implementation.
So let's start with some imports,
and let's import from streamlit st.
And this is going to be our streamlit application object.
We are going to be working through this entire tutorial.
We want to also import the run_llm function we wrote
from the backend.core.
All right, and those are all the imports
we're going to be needing for this video.
All right, let's go implement a function
which is called _format_sources, which is going
to receive context_docs as input.
And this is going to be a list.
And here it's going to be some LangChain documents,
and it's going to return us a list of URLs.
Why do you want to do it?
Because we want to render nicely for each response
we get from the LLM after the retrieval, we want to cite
where we grounded the answer from.
So we want to display it nicely as a list.
So this is why we need this helper function.
So this function is going to be returning a list of strings,
and we simply need to go and to iterate
over the context_docs.
So this is what we're doing over here.
We're iterating over the context_docs,
which are LangChain documents.
Now if those context_docs, crossing fingers,
are going to have the metadata field, because we added
to the metadata when we index the documents,
if they're going to have a metadata field,
then what we're going to be doing, we are going
to be extracting the URL from the metadata.
So this is why we first check that there is a metadata,
and then if it is, we simply go now
and have a meta variable.
So this line over here helps us to filter out
all the documents that has this metadata key,
and we want to save this metadata into this meta variable.
So now we want to access from this meta variable,
which is the metadata.
We want to extract the sources.
If we don't have any sources,
we want to write here "Unknown."
And eventually you want to cast everything into a string
just in case we don't have a string,
so this is some defensive programming.
So this is the helper function which is going to help us
render nicely the citations of the answer of the LLM.
So let's go now and let's write some Streamlit code.
It's going to be very, very short
and we're going to see how we can get up and running
a very quick interface to prototype our application.
So I want to start by using the streamlit object,
and it has this set_page_config, which is going to configure
the basic settings of the applications,
like the page title and the layout type.
So I'm going to write that,
and let me now in the terminal
write "pipenv run streamlit run main.py" file.
So it's going to run a streamlit application,
which is going to be using the main file
as our source to the application.
And we want to run everything with pipenv,
with the virtual environment we created with pipenv.
And if you're going to be using uv, it's very similar.
To do it simply you need to go to the virtual environment
and run "streamlit run main.py."
So we can see now we popped out a small browser
and we have now an application running.
We don't really see anything
because this application is empty.
So let's go and add something to this application.
And if I'm going to write Command-L here in my browser,
you can see here this is the title of the page,
"LangChain Documentation Helper," which came from here.
And let's go now and add a title to the page.
We're going to be doing that with the method of title,
which is going to receive the body of the text
we want to display.
And here we want to display
"LangChain Documentation Helper."
So if I'm going to go back to the application here,
let me refresh it,
we can see here we have "LangChain Documentation Helper."
And notice this is running now in debug mode by default
so I don't need to stop this application and run it again.
Let's go now and create a sidebar in Streamlit.
And for that we're going to be using
the Streamlit sidebar context manager.
Now everything indented here is going to be a part
of the sidebar.
So let me write subheader session.
And this is going to display the text
in a subheader formatting.
And let's go to the application, let's look how it looks.
So here we can see we have a sidebar
which we can toggle on and off.
And we have here this subheader of session here.
So first thing I want to do is to create a button
which is going to reset our chat,
which is going to allow us to easily debug our application.
So I'm going to use this Streamlit button,
and I'm going to call this button "Clear chat."
And I'm going to use use_container_width=True.
This means that the width of this button is going
to be the width of its container, which is the sidebar.
And here you can see all of the different settings
you can use this function,
but its return value is going to be a boolean
if the button was clicked on the last run of the app or not.
So if somebody is going to click this button,
we want to clear this chat.
So this chat is going to have messages.
And I'm going to give you a quick hint.
We are going to be saving all of the messages
in the front end application, in the Streamlit application,
and we're going to be setting it in a very special place
inside the Streamlit application,
which is called session state.
So when you fire up a Streamlit application,
you basically have a dictionary
where you can store intermediate results and you can store
the data from a previous user interactions.
So we are going to be saving all of the messages we got back
and forth from the user, to the LLM, back to the LLM,
we're going to be saving that in the Streamlit application.
So what we're going to do if somebody is going to click
the button to clear the chat, we want to go and delete
everything from that session, right?
And for that we need to go and access the session_state.
This is the object how it's called in Streamlit.
And this is simply a dictionary.
So we can use the .pop method of that dictionary,
and we simply want to pop the messages key, right?
So if we're going to be doing that,
we're going to be clearing the chat.
And then after we do that, we want to rerun the application.
We want to refresh it to start from a clean slate,
so it's going to render again.
So let's go to the code now.
And here you can see we have now a session here.
Once we click it, it's actually going to clear
the session state.
We don't really have anything right now,
so it's not going to do anything, but later
it's going to remove all of the interaction
and all of the messages between us,
between the user and the LLM.
All right, so we are done with the sidebar.
Now let's go and show some user messages.
All right, so now we want to write some code
which is going to display the messages from the user
and from the LLM, we want to display all the chat messages.
And for that we want to iterate over the messages
and to simply print them.
However, when we were going to be starting the application,
we still don't have any messages.
So let's start by writing a placeholder message.
So if there aren't messages in the session state,
so this means we fired up the application,
nothing happened yet, then we want to add
to the messages key here in the session_state,
we want to add here an example message.
So let's go and add an example message
with the role of "assistant" and the content
of "Ask me anything about LangChain docs.
I'll retrieve relevant context and cite sources."
And let's go and write sources equals to an empty list here.
So this is an artificial message we are going to be storing
in the session_state in the messages key.
I remind you the message_state is simply dictionary,
which is going to have the key of "messages."
And right now we are populating it with a list
that contains one message.
And now we want to go
and we want to iterate over all the messages.
So we are going to be iterating over the session_state
in the messages.
So this is going to be a list of messages,
whether a user message or an AI message.
And here we are going to be iterating on those messages.
So for each message we want now to create a container
which is going to be holding the message.
So let's use the with streamlit.chat_message.
And this is here is going to insert
a chat message container.
And in its parameters we can give it a name.
So it can be either user, assistant, ai, human, or string.
And this is going to give that message a role,
and it's going to have a different theme
for the message accordingly, with an avatar,
and we're going to be seeing it now when we run it.
So for every message I want to go
and I want to display its content.
So everything under this indentation over here,
under chat_message, is going to be displayed
according to the current role.
So here I want to display the content of the message.
I remind you in the example message here we have the role
of "assistant" and the content here, this is the content.
To display the sources, we want to display
it nicely in a dropdown.
So we're going to be using an expander object,
which is going to insert a multielement container
that can be expanded/collapsed.
So we're going to give it the title of sources
and here we're simply going to show the sources.
So we're going to be iterating through all the links
of the sources we're going to be getting.
And we are going to use the Markdown, which is going
to be formatting the strings as Markdown here.
We can see now we have here this first message,
"Ask me anything about LangChain docs.
I'll retrieve the context and cite sources."
And notice here, here we have the avatar of a robot.
And let me go back here to write, for example, "user,"
And this is going to be changing to user here.
So this changed once I changed the role here.
Now here in the sources, for example,
let me put here "www.langchain.com,"
and here we can see the expander
and now it has an element of LangChain.
If we're going to have more, we can also add here
"www.anthropic.com."
Here we can see too.
So everything here is being formatted as Markdown.
And this dash over here is going to be displayed
in Markdown as a list item, for those of you who know.
So let's go and reset this.
So now it's going to be empty.
Cool. So this is the application so far.
So now we want to create the text area
where the user can input the message.
So let's go back, and for that we're going to be using
streamlit chat_input.
And this is going to be a container
which is going to have the chat that the user input.
So here we're going to give it some placeholder text,
"Ask a question about LangChain."
And once the user submits the query under the text area,
it's going to be saved here in prompt.
So let me go and show you that.
Let's go to the application, let's refresh it.
Here you can see the new text area.
Let me write, "Hello."
And this "Hello" is going to be saved now
to this prompt here, and when a user is going to write
a message, "Hello," we want to first display it.
So let's write if prompt,
and this is going to be executed only if the user
pressed Enter or submitted the the input.
So here if the user have inserted a prompt,
so first thing first, we appended it
to the session state now.
Now so we have the history.
And now we want to print it,
we want to display it to the user.
So we are going to be writing with streamlit chat message
like before, but this time it's going to be a user message
because we know for sure at this point of time
that this is going to be a user message
and we simply want to display it in Markdown,
display the user prompt.
So let me go and save and show you how it looks.
Let's go refresh it.
Let me write here, "Hello."
And this is simply printing it.
If we're going to be changing it to AI for example,
and we're going to be writing "Hello,"
I'm going to see it's going to be the avatar of the AI.
So let me change it back to user.
So now we want to create now an assistant message.
All right, so now under this chat message,
everything indented over here is going to be displayed
in the container of the assistant chat message.
So this is going to be the response of the LLM.
So we want to run now our agent,
and we want to execute it with the user input.
So let me now add a try/except clause here.
And this is because we're going to be running
our RAG agent here and the RAG agent may fail.
So if it fails, we still want to display something
to the user and we don't want
this entire application to crash.
So here I'm going to be using the streamlit.error
and the streamlit.exception to do some error handling.
Let's now go and use a spinner object now.
So this is going to be displaying under the chat message
here a spinner, which is going to show the user
something is happening.
And we want to write the text, "Retrieving docs
and generating answer."
And here we want to run the run_llm function
with the user prompt,
and we are going to be getting a response back, right?
So let me now show you how it's going to be looking.
So let's go and write, "Hello."
So here we can see the spinner, "Retrieving docs,"
and we can see it ended
because we got a response from the LLM.
So let's go and extract this response.
So I'm going to get the answer key from the response.
If there is by some reason no answer,
I'm simply going to be writing, "No answer returned."
So here we're going to have the answer,
and now we want to get the sources.
So now we want to have a list of URLs we can display nicely.
So I'm going to be using the _format_sources,
and I'm going to give it the context key,
which is going to hold all of the documents
which grounded the answer, if we retrieved anything.
All right, so now let's go first and let's go
and show the answer.
So let me refresh it.
Let me write, "Hello."
So we got here the answer and our function went through.
And you notice here we didn't get any documents
because we didn't ask anything about LangChain.
And let's go now and print the sources like before.
So if we have sources we want to show them.
So we're going to be using the expander object,
we're going to be giving you the title of "Sources"
and we want to iterate over the sources
and print them in Markdown as a list here.
So this is going to be printing the sources.
Let me now refresh it.
Let me write, "Hello."
We can see we are not printing it
and this is because we didn't retrieve anything.
Let's write now, "What are deep agents?"
We can see it's taking a bit longer.
And here we can see we have an answer about deep agents,
and we can see we have all the sources here,
and we can see we even have a duplicate source.
This is something which happens sometimes
and it's not a problem to fix it.
We can simply remove the duplicates
with some Python set objects.
Maybe we'll do it later.
And now, if I'm going to be sending a message,
notice what's going to be happening.
We can see our previous response disappeared,
and this is because we forgot to save it
in the session state.
So let's go and do that.
Let's add to the session_state the message,
and now we have now the role of assistant
because it's going to be the result of the agent.
The content is going to be the answer of the agent,
and source is going to come from the sources variable
after we formatted them nicely as a list.
All right, so let me write here,
"Hello, what can you do for me?"
And once we get a message, let me write
"Deep agents, explain."
And now we're going to see this text here,
which it output us we're going to be saving.
Boom, we get here an answer, and we get here the sources,
and look up, we can see we still have the history,
and this is because we added the response
of the agent to the Streamlit session state right over here.
And if we'll clear the chat,
we can see we removed everything.
Hello. So we start from a clean slate.
So this is why we wanted this clear chat here.
It's very convenient.
All right, so you can find everything
in the branch 3-frontend-finish.
Let me write git add main.
Let's write git commit -m "added frontend."
Let me git push.
And if you want to access it, you can simply go to the repo,
go to the branches here,
and here you have 3-frontend-finish.
And you can see you have your main.py file.
And here is everything we implemented.
All right, so I hope you enjoyed this video
and we built a very simple interface to run our RAG agent.
Now of course this is not production grade.
In fact, we didn't really reflect to the user
what's happening in the agent.
And when building AI agents, it's really important
to communicate to the user what's happening with the agent,
how they can trust the output of the agent,
because we want to communicate what's the state,
what's being executed, which tool is running right now,
what is the result of the tool.
So this is referred to as generative UI,
and we're going to be covering it as well in this course.
And right now we didn't really handle it.
And in the next video I'm going to show you a better way,
a bit more complex, but it's going to reflect
what's happening in our agent,
and this is going to be involving some generative UI.