Building a Streamlit UI for My LangGraph AI Agent

ai agentcustomer service automationlanggraphpythonstreamlitui developmentuser interface

In my previous article, I walked through building a LangGraph-based AI agent to automate a 15-minute customer service booking session.


The agent handles the entire booking process much like a real customer service representative. It is a LangGraph-based agent that orchestrates the following operations:


  • Responds to customer queries and understands their needs.
  • Calculates the service price and informs the customer.
  • Handles the customer's acceptance or rejection.
  • Proposes optimized time slots.
  • Confirms and records the appointment.


In the first version of the agent, I did not focus much on the UI/UX. I built a simple Python CLI to test the agent's functionality. The customer service agent ran entirely in the terminal—functional for testing, but far from ideal for demonstrating a customer-facing booking experience.


The full source code is available on GitHub at customer-service-agent. Feel free to clone the repo and test it yourself.


In this article, we will build a clean, interactive Streamlit UI on top of the existing LangGraph agent.


A quick note on terminology: Throughout this article, I use agent and graph interchangeably. In LangGraph, the agent architecture is defined and executed as a compiled state graph object, so they essentially mean the same thing here.


User interface for the agent


From an implementation standpoint, Streamlit is not drastically different from a Python CLI. Both serve as a wrapper for the LangGraph agent. Streamlit is, of course, much more user-friendly and visually appealing.


The CLI interface collected input, invoked the graph, and printed the response. The Streamlit page will do the same, but it will also render structured information extracted from the graph state, such as current booking details, price quotes, and acceptance buttons.


The architecture remains the same; Streamlit simply presents the state to the user and sends user actions back to the agent.


Setting up the Streamlit page


Since we use Poetry for dependency management, we can install Streamlit with:


poetry add streamlit


This updates both pyproject.toml and poetry.lock. Next, create a new file, streamlit_app.py.


Start by importing the graph builder, models, and observability 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 customerserviceagent.graph import build_graph

from customerserviceagent.models import (

AgentState,

BookingDetails,

TimeOption,

)

from customerserviceagent.observability import (

createlangfusehandler,

flush_langfuse,

graph_config,

)


The agent graph does not include any Streamlit-specific logic. This separation is crucial because it allows you to run the graph from a CLI, an API, WhatsApp, or another frontend in the future.


The graph expects an AgentState to be initialized (e.g., graph = StateGraph(AgentState)), so we add the following in streamlit_app.py:


INITIAL_STATE: AgentState = {

"messages": [],

"booking_details": BookingDetails(),

"calculated_price": None,

"time_options": [],

"selected_slot": None,

"status": "gathering_info",

}


After the first turn (i.e., the first customer message), LangGraph's checkpointer retains the state.


Now, let's design the core page logic. We'll set up the page configuration, handle session state, and build the chat interface.


st.setpageconfig(pagetitle="Customer Service Agent", pageicon="🤖")


Initialize session state

if "messages" not in st.session_state:

st.session_state.messages = []

if "graphstate" not in st.sessionstate:

st.sessionstate.graphstate = None


Sidebar for booking details and actions

with st.sidebar:

st.header("Booking Details")

if st.sessionstate.graphstate is not None:

details = st.sessionstate.graphstate.get("booking_details", {})

st.write(f"Service: {details.service or 'N/A'}")

st.write(f"Date: {details.date or 'N/A'}")

st.write(f"Time: {details.time or 'N/A'}")

st.write(f"Name: {details.customer_name or 'N/A'}")

st.write(f"Price: ${details.price or 'N/A'}")


if st.sessionstate.graphstate.get("time_options"):

st.subheader("Available Slots")

for slot in st.sessionstate.graphstate["time_options"]:

if st.button(f"{slot.date} {slot.time}", key=slot.id):

Send selection back to the agent

pass


Main chat area

st.title("Customer Service Agent")


Display chat history

for message in st.session_state.messages:

with st.chat_message(message.type):

st.write(message.content)


Chat input

if prompt := st.chat_input("How can I help you today?"):

Add user message

st.session_state.messages.append(HumanMessage(content=prompt))

with st.chat_message("user"):

st.write(prompt)


Invoke the agent

with st.chat_message("assistant"):

with st.spinner("Thinking..."):

Build and invoke the graph

graph = build_graph()

config = graph_config(uuid4().hex)


Prepare state

if st.sessionstate.graphstate is None:

currentstate = INITIALSTATE.copy()

currentstate["messages"] = st.sessionstate.messages

else:

currentstate = st.sessionstate.graph_state

currentstate["messages"] = st.sessionstate.messages


Run the agent

result = graph.invoke(current_state, config)

st.sessionstate.graphstate = result


Display assistant response

last_message = result["messages"][-1]

if isinstance(last_message, AIMessage):

st.write(last_message.content)

st.sessionstate.messages.append(lastmessage)


Update sidebar

st.rerun()


Handling user interactions


Streamlit's reactive model requires careful handling of user actions, especially when the agent expects a decision (e.g., acceptance or rejection of a price). In the sidebar, we render action buttons that correspond to the current state. When a button is clicked, we append a synthetic HumanMessage to the chat history and then invoke the graph again with the updated state.


For example, to handle price acceptance:


if st.sessionstate.graphstate.get("calculatedprice") and not st.sessionstate.graphstate.get("priceconfirmed"):

col1, col2 = st.columns(2)

if col1.button("Accept Price"):

st.session_state.messages.append(HumanMessage(content="I accept the price."))

st.rerun()

if col2.button("Reject Price"):

st.session_state.messages.append(HumanMessage(content="I reject the price."))

st.rerun()


These buttons trigger the same graph invocation flow as a text input.


Why this approach matters in 2026


As AI agents become more prevalent in customer-facing applications, the gap between backend intelligence and frontend usability is narrowing. Streamlit remains a top choice for rapid prototyping and internal tools, but its integration pattern with agent frameworks like LangGraph has matured. By 2026, we see more standardized patterns for state synchronization, streaming responses, and embedding agents into production UIs. This example mirrors those trends: a clean separation of concerns, reusable agent logic, and a lightweight UI layer that can be swapped for a mobile app or web chat later.


In the next part, we'll add streaming outputs, persistent history, and deployment tips. Stay tuned!

via Towards Data Science

Related