7장. 웹 UI 연동과 배포
채팅 화면: chat_message 와 chat_input
메신저 앱을 열면 위에는 주고받은 말풍선이 차례로 쌓여 있고, 아래에는 글을 적는 칸이 붙어 있습니다. 내가 보낸 말은 한쪽에, 상대의 말은 다른 쪽에 아이콘과 함께 보입니다. Streamlit 은 이 꼴을 함수 두 개로 만듭니다. 말풍선은 st.chat_message, 아래의 입력칸은 st.chat_input 입니다.
화면을 본뜬 그림입니다. 실제 캡처가 아니고, 실제 화면의 색과 아이콘은 판마다 조금 다릅니다. 번호는 화면 바깥에 붙였고 오른쪽 목록과 짝을 이룹니다. 1번이 제목, 2번이 손님의 말풍선(st.chat_message("user")), 3번이 비서의 말풍선(st.chat_message("assistant")), 4번이 화면 맨 아래에 붙는 입력창(st.chat_input), 5번이 설정을 두는 사이드바입니다. 여기서 메신저의 말풍선 목록이 st.session_state 에 쌓인 대화 기록, 글 적는 칸이 st.chat_input 입니다. 비유와 다른 점은, 메신저는 지난 말풍선이 화면에 그대로 남아 있지만 Streamlit 은 다시 돌 때마다 화면을 새로 그리므로 지난 말풍선을 기록에서 꺼내 우리가 다시 그려야 한다는 것입니다.
채팅 화면의 두 함수
| 쓰는 모양 | 하는 일 | 알아 둘 것 |
|---|---|---|
with st.chat_message("user"): | 손님의 말풍선을 연다. 이 안에서 st.markdown(…) 으로 글을 쓴다 | 이름이 "user", "human" 이면 사람 아이콘 |
with st.chat_message("assistant"): | 비서의 말풍선을 연다 | "assistant", "ai" 면 로봇 아이콘 |
question = st.chat_input("질문을 적어 주세요") | 화면 아래 입력창. 괄호 안의 글은 빈칸에 흐리게 보이는 안내 글 | 손님이 Enter 를 친 실행에서만 글을, 나머지 실행에서는 None 을 돌려준다 |
if question := st.chat_input(…): | 질문이 들어온 실행에서만 안쪽을 돈다 | := 는 값을 변수에 넣으면서 바로 견주는 연산자다 |
짧은 예시
받은 말을 따라 말하는 작은 채팅 화면입니다. 질문 두 개를 넣어 봅니다.
from streamlit.testing.v1 import AppTest
APP = """
import streamlit as st
if "messages" not in st.session_state:
st.session_state.messages = []
# 1) 지금까지의 대화를 말풍선으로 다시 그린다
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.markdown(m["content"])
# 2) 새 질문이 들어온 실행에서만 아래가 돈다
if question := st.chat_input("질문을 적어 주세요"):
st.session_state.messages.append({"role": "user", "content": question})
with st.chat_message("user"):
st.markdown(question)
reply = f"'{question}' 라고 하셨군요." # 아직은 따라 말하기 봇
st.session_state.messages.append({"role": "assistant", "content": reply})
with st.chat_message("assistant"):
st.markdown(reply)
"""
at = AppTest.from_string(APP).run()
for q in ["제주도 가요", "2박 3일이요"]:
# set_value: 입력창에 글을 넣고 Enter 를 누른 것과 같다
at.chat_input[0].set_value(q).run()
for m in at.chat_message:
print(m.name, "|", m.markdown[0].value)
print("메모장의 기록:", len(at.session_state["messages"]), "개")user | 제주도 가요
assistant | '제주도 가요' 라고 하셨군요.
user | 2박 3일이요
assistant | '2박 3일이요' 라고 하셨군요.
메모장의 기록: 4 개둘째 질문을 넣은 실행에서 화면에는 말풍선 네 개가 보입니다. 앞의 두 개는 1)의 반복문이 기록에서 꺼내 다시 그린 것이고, 뒤의 두 개는 2)에서 새로 그린 것입니다. 기록을 {"role": …, "content": …} 사전으로 담은 것은 1장에서 본 메시지의 꼴 그대로입니다.
코드가 의미하는 것
이 코드는 메신저 화면의 두 동작, 지난 말풍선을 기록에서 꺼내 다시 그리기와 새 질문이 오면 말풍선 한 쌍을 더하기를 흉내 낸 것입니다. 답은 아직 받은 말을 따라 말할 뿐입니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
for m in st.session_state.messages: 와 with st.chat_message(m["role"]): | 기록을 하나씩 말풍선으로 그린다 | 다시 실행마다 화면이 비워지므로, 지난 대화를 보이게 하는 것은 이 반복문입니다 |
if question := st.chat_input(…): | 질문이 들어온 실행에서만 안쪽을 돈다 | 손님이 Enter 를 친 실행에서만 답을 만듭니다. 나중에 모델을 부를 자리도 이 안입니다 |
messages.append({"role": "user", "content": question}) | 질문을 사전 하나로 기록에 넣는다 | 1장의 메시지 꼴 그대로 담아 두어, 다음 절에서 체인에 바로 넘깁니다 |
reply = f"'{question}' 라고 하셨군요." | 답을 만든다 | 챗봇 로직(주방)이 들어갈 자리입니다. 지금은 따라 말하기로 채워 두었습니다 |
at.chat_input[0].set_value(q).run() | 입력창에 글을 넣고 Enter 를 친 것처럼 다시 실행한다 | 손님 한 사람이 질문 두 개를 차례로 보낸 시험입니다 |
결과에서 읽을 것은 두 가지입니다.
- 말풍선은 user 와 assistant 가 번갈아 네 개입니다. 앞의 두 개는 기록에서 다시 그린 것, 뒤의 두 개는 둘째 질문에 새로 그린 것입니다.
- 메모장의 기록도 4 개입니다. 화면의 말풍선 수와 기록 수가 같습니다. 화면은 기록을 그대로 옮겨 그린 것이라는 뜻입니다.
주의할 점
- 1)의 다시 그리기를 빼면 새 질문을 보낼 때마다 앞의 말풍선이 사라집니다. 화면은 다시 돌 때마다 비워지기 때문입니다.
- 기록에 넣는 역할 이름은
"user","assistant"로 맞춰 둡니다. 이 꼴은 LangChain 의MessagesPlaceholder에도 그대로 넣을 수 있어서 다음 절에서 체인에 바로 넘깁니다. st.chat_input은 한 화면에 하나만 둡니다. 사이드바 안이나 다른 부품 안에 넣지 않고 맨 바깥에 두면 화면 맨 아래에 붙습니다.