In my earlier article, I how I constructed a LangGraph-based AI agent to automate a 15-minute customer support reserving session.
The agent handles your entire reserving course of like an actual customer support consultant. It’s a LangGraph-based agent that orchestrates the next operations:
- Responds to buyer queries and understands their wants.
- Calculates the worth for the service and informs the shopper.
- Handles the shopper’s acceptance or rejection.
- Proposes optimized time slots.
- Confirms and data the appointment.
Within the first model of the agent, I didn’t focus a lot on the UI/UX half. I simply constructed a Python CLI to check the performance of the agent.
The customer support agent ran fully within the terminal. The CLI labored properly for testing however it was not one of the simplest ways to exhibit a customer-facing reserving expertise.
The total supply code of this mission is offered on GitHub at customer-service-agent. Be at liberty to clone the repo and check it your self.
On this article, we’ll construct a clear, interactive Streamlit UI on high of the present LangGraph agent.
A fast word on the terminology: All through this text, I exploit agent and graph interchangeably. In LangGraph, the agent structure is outlined and executed as a compiled state graph object in order that they primarily imply the identical factor on this article.
Consumer interface for the agent
By way of implementation, Streamlit will not be very totally different from a Python CLI. Each function a wrapper for the LangGraph agent. Streamlit is, in fact, rather more person pleasant and appears extra interesting.
The CLI interface collected enter, invoked the graph, and printed the response. The Streamlit web page will do the identical however it can additionally render structured data extracted from the graph state comparable to present reserving particulars, value quote, and acceptance buttons.
The structure nonetheless lies in the identical software. Streamlit solely presents the state to the person and sends person actions again to the agent.
Streamlit web page
Since we’re utilizing poetry for dependency administration, we will set up streamlit utilizing:
poetry add streamlit
This updates each pyproject.toml and poetry.lock information. Then we create a brand new file streamlit_app.py .
We begin by importing the graph builder, fashions, and observibility utilities.
from __future__ import annotations
import os
from datetime import datetime
from typing import Any
from uuid import uuid4
import streamlit as st
from dotenv import load_dotenv
from langchain_core.messages import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI
from customer_service_agent.graph import build_graph
from customer_service_agent.fashions import (
AgentState,
BookingDetails,
TimeOption,
)
from customer_service_agent.observability import (
create_langfuse_handler,
flush_langfuse,
graph_config,
)
The agent graph doesn’t embody any Streamlit-specific logic. This separation is necessary as a result of it permits us to run the graph from a CLI, an API, WhatsApp, or one other frontend later.
The graph expects an Agent State to be initialized ( graph = StateGraph(AgentState) ) so we add the next in streamlit_app.py :
INITIAL_STATE: AgentState = {
"messages": [],
"booking_details": BookingDetails(),
"calculated_price": None,
"time_options": [],
"selected_slot": None,
"standing": "gathering_info",
}
After the primary flip (i.e. first buyer message), LangGraph’s checkpointer retains the state.
Streamlit reruns your entire Python script each time person interacts with a widget (e.g. sends a chat message, clicks a button, selects an appointment) so we can’t use native variables. To protect the dialog throughout Streamlit runs, we have to use session_state .
We are able to initialize the session as follows:
def initialize_session() -> None:
if "graph" in st.session_state:
return
llm = ChatOpenAI(
mannequin=os.getenv("OPENAI_MODEL", "gpt-4o-mini"),
temperature=0,
)
handler = create_langfuse_handler()
st.session_state.graph = build_graph(llm)
st.session_state.handler = handler
st.session_state.config = graph_config(
str(uuid4()),
handler,
)
st.session_state.agent_state = INITIAL_STATE.copy()
st.session_state.began = False
The perform first checks if the graph has already been created for this browser session (if "graph" in st.session_state ). With out this examine, each Streamlit rerun would exchange the graph.
The graph_config perform generates a UUID for use as thread_id , which is required by LangGraph to determine a dialog. If a brand new UUID had been generated on each Streamlit rerun, LangGraph would see each message as belonging to a brand new dialog.
We even have the Langfuse tracing integration inside this perform. The handler is saved in order that the following graph calls can use the identical tracing configuration.
Then, we now have the _invoke perform for dealing with person enter.
def _invoke(customer_text: str) -> None:
"""Submit one buyer flip to the graph and retain its newest state."""
graph_input: dict[str, Any] = {"messages": [HumanMessage(content=customer_text)]}
if not st.session_state.began:
graph_input.replace(INITIAL_STATE)
graph_input["messages"] = [HumanMessage(content=customer_text)]
st.session_state.began = True
strive:
consequence = st.session_state.graph.invoke(graph_input, config=st.session_state.config)
st.session_state.agent_state = consequence
flush_langfuse(st.session_state.handler)
besides Exception:
st.session_state.began = bool(st.session_state.agent_state.get("messages"))
st.error("The assistant couldn't course of that request. Please strive once more.")
This perform sends the shopper motion to the LangGraph agent and saves the ensuing state for the Streamlit interface. The client motion generally is a chat enter or a button click on (e.g. “Settle for quote”).
The client’s message is transformed right into a LangChain HumanMessage. The messages makes use of LangGraph’s add_messages reducer so new messages are added to the present dialog as an alternative of changing it.
For the primary message, we initialize the graph utilizing the INITIAL_STATE outlined earlier with empty reserving particulars, scheduling choices, value, and the preliminary standing.
Then, each time we obtain a brand new customized motion, we invoke the graph and replace its state with the consequence. That is the place the LLM calls occur:
consequence = st.session_state.graph.invoke(graph_input, config=st.session_state.config)
st.session_state.agent_state = consequence
This runs the shopper message by the graph. The returned graph state (i.e. outcomes ) is saved in Streamlit’s session in order that the web page can render the most recent particulars of messages, reserving abstract, value, appointment choices, and standing.
Lastly, we now have some render capabilities (_render...() ) outlined in streamlit_app.py to transform the present LangGraph state into seen Streamlit elements.
def _render_messages(state: AgentState) -> None:
if not state.get("messages"):
with st.chat_message("assistant"):
st.write(
"Hello! I may help you e-book home or sofa cleansing. "
"Inform me what you want, together with the dimensions and repair tackle."
)
return
for message in state["messages"]:
if isinstance(message, HumanMessage):
function = "person"
elif isinstance(message, AIMessage):
function = "assistant"
else:
proceed
with st.chat_message(function):
st.write(str(message.content material))
For instance, the _render_messages perform shows the dialog historical past as Streamlit chat bubbles. It receives the dialog by the most recent LangGraph state utilizing state["messages"] . If the dialog has no messages but, the perform reveals an preliminary greeting.
Let’s see the way it works
We’ve gone over the streamlit_app.py to find out how the web page is structured. It’s time to see it in motion:
We are able to check it domestically utilizing the next command:
poetry run streamlit run customer_service_agent/streamlit_app.py
It’ll open up a web page at http://localhost:8501/ . The web page seems to be like this:

As a way to check the agent, we want an OPENAI_API_KEY. It’ll price you just a few cents to check.
Here’s a chat instance:

I didn’t give the tackle and the agent requested for it as we’d anticipate. Let’s strive the identical however offering the tackle within the first message:

Since I’ve the tackle within the first message, the agent didn’t ask for it. I accepted the quote and the agent gave me three choices to decide on after which accomplished the reserving:

There may be much more we will do on the interface to make it extra person pleasant. However we now have a model that runs easily and with a pleasant and clear person interface.
I’m planning to enhance the agent and add extra performance comparable to WhatsApp integration. It might even turned out to be a product that I can promote to some native companies.
Keep tuned for what’s coming and thanks for studying!















