docs/getting_started/chatapp_tutorial.md
import os
import reflex as rx
import openai
from docs.getting_started import chat_tutorial_style as style
from docs.getting_started.chat_tutorial_utils import ChatappState
# If it's in environment, no need to hardcode (openai SDK will pick it up)
if "OPENAI_API_KEY" not in os.environ:
openai.api_key = "YOUR_OPENAI_KEY"
~30 min hands-on · Build a streaming AI chatbot in pure Python — UI, state, and OpenAI integration in one Reflex app.
You can find the full source code for this app on GitHub.
rx.box(
rx.vstack(
rx.vstack(
rx.box(
rx.text("What is Reflex?", style=style.question_style),
text_align="right",
width="100%",
),
rx.box(
rx.text(
"Reflex is a way to build full-stack web apps in pure Python — "
"frontend, backend, and state all in one place.",
style=style.answer_style,
),
text_align="left",
width="100%",
),
rx.box(
rx.text("Can I deploy it?", style=style.question_style),
text_align="right",
width="100%",
),
rx.box(
rx.text(
"Yes! A single `reflex deploy` ships it to production.",
style=style.answer_style,
),
text_align="left",
width="100%",
),
spacing="0",
width="100%",
),
rx.hstack(
rx.input(
placeholder="Ask a question",
style=style.input_style,
is_disabled=True,
),
rx.button("Ask", style=style.button_style, is_disabled=True),
spacing="3",
padding_top="1.5em",
justify="center",
width="100%",
),
align="stretch",
spacing="0",
width="100%",
padding="2.5em 4em",
),
border=f"1px solid {rx.color('slate', 5)}",
border_radius="12px",
margin_y="1em",
)
In this tutorial you'll learn how to:
reflex and set up your development environment.# Video: Example of Setting up the Chat App
We will start by creating a new project and setting up our development environment. If you haven't installed uv yet, see the installation guide. Then create a new project directory and scaffold a Reflex app:
mkdir chatapp
cd chatapp
uv init
uv add reflex
uv run reflex init
When prompted to select a template, choose option **0** for a blank project.
You can run the template app to make sure everything is working.
uv run reflex run
You should see your app running at http://localhost:3000.
Reflex also starts the backend server which handles all the state management and communication with the frontend. You can test the backend server is running by navigating to http://localhost:8000/ping.
Now that we have our project set up, in the next section we will start building our app!
Let's start with defining the frontend for our chat app. In Reflex, the frontend can be broken down into independent, reusable components. See the components docs for more information.
We will modify the index function in chatapp/chatapp.py file to return a component that displays a single question and answer.
rx.container(
rx.box(
"What is Reflex?",
# The user's question is on the right.
text_align="right",
),
rx.box(
"A way to build web apps in pure Python!",
# The answer is on the left.
text_align="left",
),
)
# chatapp.py
import reflex as rx
def index() -> rx.Component:
return rx.container(
rx.box(
"What is Reflex?",
# The user's question is on the right.
text_align="right",
),
rx.box(
"A way to build web apps in pure Python!",
# The answer is on the left.
text_align="left",
),
)
# Add state and page to the app.
app = rx.App()
app.add_page(index)
Components can be nested inside each other to create complex layouts. Here we create a parent container that contains two boxes for the question and answer.
We also add some basic styling to the components. Components take in keyword arguments, called props, that modify the appearance and functionality of the component. We use the text_align prop to align the text to the left and right.
Now that we have a component that displays a single question and answer, we can reuse it to display multiple questions and answers. We will move the component to a separate function question_answer and call it from the index function.
def qa(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(question, text_align="right"),
rx.box(answer, text_align="left"),
margin_y="1em",
)
qa_pairs = [
("What is Reflex?", "A way to build web apps in pure Python!"),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
def chat() -> rx.Component:
qa_pairs = [
("What is Reflex?", "A way to build web apps in pure Python!"),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
return rx.box(*[qa(question, answer) for question, answer in qa_pairs])
rx.container(chat())
def qa(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(question, text_align="right"),
rx.box(answer, text_align="left"),
margin_y="1em",
)
def chat() -> rx.Component:
qa_pairs = [
("What is Reflex?", "A way to build web apps in pure Python!"),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
return rx.box(*[qa(question, answer) for question, answer in qa_pairs])
def index() -> rx.Component:
return rx.container(chat())
Now we want a way for the user to input a question. For this, we will use the input component to have the user add text and a button component to submit the question.
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(placeholder="Ask a question"),
rx.button("Ask"),
)
rx.container(
chat(),
action_bar(),
)
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(placeholder="Ask a question"),
rx.button("Ask"),
)
def index() -> rx.Component:
return rx.container(
chat(),
action_bar(),
)
Let's add some styling to the app. More information on styling can be found in the styling docs. To keep our code clean, we will move the styling to a separate file chatapp/style.py.
# style.py
import reflex as rx
# Common styles for questions and answers.
shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px"
chat_margin = "20%"
message_style = dict(
padding="1em",
border_radius="5px",
margin_y="0.5em",
box_shadow=shadow,
max_width="30em",
display="inline-block",
)
# Set specific styles for questions and answers.
question_style = message_style | dict(
margin_left=chat_margin, background_color=rx.color("gray", 4)
)
answer_style = message_style | dict(
margin_right=chat_margin, background_color=rx.color("accent", 8)
)
# Styles for the action bar.
input_style = dict(
border_width="1px", padding="0.5em", box_shadow=shadow, width="350px"
)
button_style = dict(background_color=rx.color("accent", 10), box_shadow=shadow)
We will import the styles in chatapp.py and use them in the components. At this point, the app should look like this:
def qa4(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(rx.text(question, style=style.question_style), text_align="right"),
rx.box(rx.text(answer, style=style.answer_style), text_align="left"),
margin_y="1em",
width="100%",
)
def chat4() -> rx.Component:
qa_pairs = [
("What is Reflex?", "A way to build web apps in pure Python!"),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
return rx.box(*[qa4(question, answer) for question, answer in qa_pairs])
def action_bar4() -> rx.Component:
return rx.hstack(
rx.input(placeholder="Ask a question", style=style.input_style),
rx.button("Ask", style=style.button_style),
)
rx.center(
rx.vstack(
chat4(),
action_bar4(),
align="center",
)
)
# chatapp.py
import reflex as rx
from chatapp import style
def qa(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(rx.text(question, style=style.question_style), text_align="right"),
rx.box(rx.text(answer, style=style.answer_style), text_align="left"),
margin_y="1em",
width="100%",
)
def chat() -> rx.Component:
qa_pairs = [
("What is Reflex?", "A way to build web apps in pure Python!"),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
return rx.box(*[qa(question, answer) for question, answer in qa_pairs])
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(placeholder="Ask a question", style=style.input_style),
rx.button("Ask", style=style.button_style),
)
def index() -> rx.Component:
return rx.center(
rx.vstack(
chat(),
action_bar(),
align="center",
)
)
app = rx.App()
app.add_page(index)
The app is looking good, but it's not very useful yet! In the next section, we will add some functionality to the app.
Now let’s make the chat app interactive by adding state. The state is where we define all the variables that can change in the app and all the functions that can modify them. You can learn more about state in the state docs.
We will create a new file called state.py in the chatapp directory. Our state will keep track of the current question being asked and the chat history. We will also define an event handler answer which will process the current question and add the answer to the chat history.
# state.py
import reflex as rx
class State(rx.State):
# The current question being asked.
question: str
# Keep track of the chat history as a list of (question, answer) tuples.
chat_history: list[tuple[str, str]]
@rx.event
def answer(self):
# Our chatbot is not very smart right now...
answer = "I don't know!"
self.chat_history.append((self.question, answer))
Now we can import the state in chatapp.py and reference it in our frontend components. We will modify the chat component to use the state instead of the current fixed questions and answers.
def qa(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(rx.text(question, style=style.question_style), text_align="right"),
rx.box(rx.text(answer, style=style.answer_style), text_align="left"),
margin_y="1em",
width="100%",
)
def chat1() -> rx.Component:
return rx.box(
rx.foreach(
ChatappState.chat_history, lambda messages: qa(messages[0], messages[1])
)
)
def action_bar1() -> rx.Component:
return rx.hstack(
rx.input(
placeholder="Ask a question",
on_change=ChatappState.set_question,
style=style.input_style,
),
rx.button("Ask", on_click=ChatappState.answer, style=style.button_style),
)
rx.container(
chat1(),
action_bar1(),
)
# chatapp.py
from chatapp.state import State
def chat() -> rx.Component:
return rx.box(
rx.foreach(State.chat_history, lambda messages: qa(messages[0], messages[1]))
)
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(
placeholder="Ask a question",
on_change=State.set_question1,
style=style.input_style,
),
rx.button("Ask", on_click=State.answer, style=style.button_style),
)
Normal Python for loops don't work for iterating over state vars because these values can change and aren't known at compile time. Instead, we use the foreach component to iterate over the chat history.
We also bind the input's on_change event to the set_question event handler, which will update the question state var while the user types in the input. We bind the button's on_click event to the answer event handler, which will process the question and add the answer to the chat history. The set_question event handler is a built-in implicitly defined event handler. Every base var has one. Learn more in the events docs under the Setters section.
Currently the input doesn't clear after the user clicks the button. We can fix this by binding the value of the input to question, with value=State.question, and clear it when we run the event handler for answer, with self.question = ''.
def action_bar2() -> rx.Component:
return rx.hstack(
rx.input(
value=ChatappState.question,
placeholder="Ask a question",
on_change=ChatappState.set_question,
style=style.input_style,
),
rx.button("Ask", on_click=ChatappState.answer2, style=style.button_style),
)
rx.container(
chat1(),
action_bar2(),
)
# chatapp.py
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(
value=State.question,
placeholder="Ask a question",
on_change=State.set_question2,
style=style.input_style,
),
rx.button("Ask", on_click=State.answer, style=style.button_style),
)
# state.py
@rx.event
def answer(self):
# Our chatbot is not very smart right now...
answer = "I don't know!"
self.chat_history.append((self.question, answer))
self.question = ""
Normally state updates are sent to the frontend when an event handler returns. However, we want to stream the text from the chatbot as it is generated. We can do this by yielding from the event handler. See the yield events docs for more info.
def action_bar3() -> rx.Component:
return rx.hstack(
rx.input(
value=ChatappState.question,
placeholder="Ask a question",
on_change=ChatappState.set_question,
style=style.input_style,
),
rx.button("Ask", on_click=ChatappState.answer3, style=style.button_style),
)
rx.container(
chat1(),
action_bar3(),
)
# state.py
import asyncio
async def answer(self):
# Our chatbot is not very smart right now...
answer = "I don't know!"
self.chat_history.append((self.question, ""))
# Clear the question input.
self.question = ""
# Yield here to clear the frontend input before continuing.
yield
for i in range(len(answer)):
# Pause to show the streaming effect.
await asyncio.sleep(0.1)
# Add one letter at a time to the output.
self.chat_history[-1] = (self.chat_history[-1][0], answer[: i + 1])
yield
In the next section, we will finish our chatbot by adding AI!
We will use OpenAI's API to give our chatbot some intelligence.
First, ensure you have an active OpenAI subscription and install the latest openai package:
pip install --upgrade openai
Then export your API key so the app can read it at runtime:
export OPENAI_API_KEY="sk-..."
Making your chatbot intelligent requires connecting to a language model API. This section explains how to integrate with OpenAI's API to power your chatbot's responses.
on_change event handler.Ask button which in turn triggers the State.answer method inside our state.py file.prompt is sent via a request to OpenAI client and returns an answer that we can trim and use to update the chat history!# chatapp.py
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(
value=State.question,
placeholder="Ask a question",
# on_change event updates the input as the user types a prompt.
on_change=State.set_question3,
style=style.input_style,
),
# on_click event triggers the API to send the prompt to OpenAI.
rx.button("Ask", on_click=State.answer, style=style.button_style),
)
# state.py
import os
from openai import AsyncOpenAI
@rx.event
async def answer(self):
# Our chatbot has some brains now!
client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
session = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": self.question}],
stop=None,
temperature=0.7,
stream=True,
)
# Add to the answer as the chatbot responds.
answer = ""
self.chat_history.append((self.question, answer))
# Clear the question input.
self.question = ""
# Yield here to clear the frontend input before continuing.
yield
async for item in session:
if hasattr(item.choices[0].delta, "content"):
if item.choices[0].delta.content is None:
# presence of 'None' indicates the end of the response
break
answer += item.choices[0].delta.content
self.chat_history[-1] = (self.chat_history[-1][0], answer)
yield
Finally, we have our chatbot!
The finished project is split across three files — chatapp.py for UI and app setup, state.py for state and API integration, and style.py for styling:
chatapp/
├── chatapp.py
├── state.py
└── style.py
The chatapp.py file:
import reflex as rx
from chatapp import style
from chatapp.state import State
def qa(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(rx.text(question, style=style.question_style), text_align="right"),
rx.box(rx.text(answer, style=style.answer_style), text_align="left"),
margin_y="1em",
)
def chat() -> rx.Component:
return rx.box(
rx.foreach(
State.chat_history,
lambda messages: qa(messages[0], messages[1]),
)
)
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(
value=State.question,
placeholder="Ask a question",
on_change=State.set_question,
style=style.input_style,
),
rx.button(
"Ask",
on_click=State.answer,
style=style.button_style,
),
)
def index() -> rx.Component:
return rx.center(
rx.vstack(
chat(),
action_bar(),
align="center",
)
)
app = rx.App()
app.add_page(index)
The state.py file:
import os
from openai import AsyncOpenAI
import reflex as rx
class State(rx.State):
question: str
chat_history: list[tuple[str, str]] = []
async def answer(self):
client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
# Start streaming completion from OpenAI
session = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": self.question}],
temperature=0.7,
stream=True,
)
# Initialize response and update UI
answer = ""
self.chat_history.append((self.question, answer))
self.question = ""
yield
# Process streaming response
async for item in session:
if hasattr(item.choices[0].delta, "content"):
if item.choices[0].delta.content is None:
break
answer += item.choices[0].delta.content
self.chat_history[-1] = (self.chat_history[-1][0], answer)
yield
The style.py file:
import reflex as rx
# Common style base
shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px"
chat_margin = "20%"
message_style = dict(
padding="1em",
border_radius="5px",
margin_y="0.5em",
box_shadow=shadow,
max_width="30em",
display="inline-block",
)
# Styles for questions and answers
question_style = message_style | dict(
margin_left=chat_margin,
background_color=rx.color("gray", 4),
)
answer_style = message_style | dict(
margin_right=chat_margin,
background_color=rx.color("accent", 8),
)
# Styles for input elements
input_style = dict(
border_width="1px", padding="0.5em", box_shadow=shadow, width="350px"
)
button_style = dict(background_color=rx.color("accent", 10), box_shadow=shadow)
Congratulations! You have built your first chatbot. From here, you can read through the rest of the documentations to learn about Reflex in more detail. The best way to learn is to build something, so try to build your own app using this as a starting point!
With our hosting service, you can deploy this app with a single command within minutes. Check out our Hosting Quick Start.