st.chat_message: mostrar mensajes de chat
Los componentes de chat de Streamlit fueron introducidos específicamente para construir interfaces conversacionales con modelos de lenguaje (LLM). st.chat_message renderiza un mensaje individual con el avatar y el nombre del rol especificado. El contenedor del mensaje acepta cualquier elemento de Streamlit en su interior, lo que permite incluir tablas, gráficos, código y markdown además de texto plano:
import streamlit as st
# Mensaje del usuario
with st.chat_message("user"):
st.write("¿Cuáles son los tres países con mayor PIB del mundo?")
# Mensaje del asistente
with st.chat_message("assistant"):
st.write("Los tres países con mayor PIB (nominal) del mundo en 2026 son:")
st.markdown("""
1. **Estados Unidos** — ~28 billones USD
2. **China** — ~18 billones USD
3. **Alemania** — ~4,5 billones USD
""")
# Mensaje del sistema o personalizado
with st.chat_message("assistant", avatar="🤖"):
st.info("Usando GPT-4 como modelo de lenguaje.")
sequenceDiagram
participant U as Usuario
participant CI as st.chat_input
participant SS as session_state.messages
participant CM as st.chat_message
participant LLM as OpenAI o Anthropic
participant WS as st.write_stream
U->>CI: Escribe pregunta
CI->>SS: Anade message role user
SS->>CM: Renderiza historial
CM->>LLM: Envia conversation
LLM->>WS: Stream chunks tokens
WS->>CM: Typewriter display assistant
WS->>SS: Anade message role assistant
Note over CI,WS: Loop continuo conversacional
st.chat_input: campo de entrada de chat
st.chat_input es un campo de texto especial fijo en la parte inferior de la pantalla, diseñado para el flujo de conversación:
import streamlit as st
prompt = st.chat_input("Escribe tu pregunta...")
if prompt:
st.write(f"Preguntaste: {prompt}")
A diferencia de st.text_input, st.chat_input:
- Se muestra fijo en la parte inferior de la pantalla.
- Se limpia automáticamente después de enviar.
- No provoca reruns intermedios al escribir; solo al pulsar Enter o el botón.
Chatbot completo con historial
import streamlit as st
import time
st.title("Asistente virtual de CertiDevs")
st.caption("Powered by IA. Pregunta sobre nuestros cursos.")
# Inicializar historial de conversación
if "mensajes" not in st.session_state:
st.session_state.mensajes = [
{
"role": "assistant",
"content": "¡Hola! Soy el asistente de CertiDevs. ¿En qué puedo ayudarte hoy?"
}
]
# Mostrar historial completo
for msg in st.session_state.mensajes:
with st.chat_message(msg["role"]):
st.write(msg["content"])
# Capturar nueva pregunta del usuario
if prompt := st.chat_input("Pregunta algo..."):
# Añadir mensaje del usuario al historial y mostrarlo
st.session_state.mensajes.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.write(prompt)
# Generar respuesta del asistente
with st.chat_message("assistant"):
respuesta = f"Gracias por tu pregunta sobre '{prompt}'. " \
f"Nuestro equipo te responderá en breve."
st.write(respuesta)
# Guardar respuesta en el historial
st.session_state.mensajes.append({"role": "assistant", "content": respuesta})
# Botón para limpiar
if st.session_state.mensajes and st.button("Limpiar conversación"):
st.session_state.mensajes = []
st.rerun()
st.write_stream: respuestas en streaming (efecto typewriter)
st.write_stream muestra el contenido de un iterable (generador) carácter a carácter, creando el efecto de "escritura en tiempo real" característico de los chatbots modernos:
import streamlit as st
import time
def generar_respuesta(prompt: str):
"""Generador que simula streaming de respuesta."""
respuesta = (
f"He recibido tu pregunta: '{prompt}'.\n\n"
"Analizando los datos del sistema... "
"Encontré 3 resultados relevantes en la base de conocimiento. "
"Aquí está mi respuesta detallada."
)
for palabra in respuesta.split():
yield palabra + " "
time.sleep(0.05)
if "msgs" not in st.session_state:
st.session_state.msgs = []
for msg in st.session_state.msgs:
with st.chat_message(msg["role"]):
st.write(msg["content"])
if prompt := st.chat_input("Escribe aquí..."):
st.session_state.msgs.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.write(prompt)
with st.chat_message("assistant"):
# write_stream muestra el stream y devuelve el texto completo
respuesta_completa = st.write_stream(generar_respuesta(prompt))
st.session_state.msgs.append({"role": "assistant", "content": respuesta_completa})
Integración con OpenAI (ejemplo de estructura)
import streamlit as st
from openai import OpenAI
@st.cache_resource
def obtener_cliente():
return OpenAI(api_key=st.secrets["openai_api_key"])
client = obtener_cliente()
st.title("Chat con GPT-4o")
if "mensajes_llm" not in st.session_state:
st.session_state.mensajes_llm = []
for msg in st.session_state.mensajes_llm:
with st.chat_message(msg["role"]):
st.write(msg["content"])
if prompt := st.chat_input("Pregunta a GPT-4o..."):
st.session_state.mensajes_llm.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.write(prompt)
with st.chat_message("assistant"):
# Streaming real con la API de OpenAI
stream = client.chat.completions.create(
model="gpt-4o",
messages=st.session_state.mensajes_llm,
stream=True
)
respuesta = st.write_stream(
chunk.choices[0].delta.content or ""
for chunk in stream
if chunk.choices[0].delta.content is not None
)
st.session_state.mensajes_llm.append({"role": "assistant", "content": respuesta})
Los componentes de chat de Streamlit están diseñados para integrarse con cualquier API de LLM que soporte streaming, incluyendo OpenAI, Anthropic Claude, Google Gemini y modelos locales con Ollama.
La clave para que el historial de chat funcione correctamente es almacenar todos los mensajes en
st.session_state. Sin esta persistencia, los mensajes anteriores desaparecerían en cada rerun. El patrón de "mostrar historial + capturar nuevo + generar respuesta" se repite en prácticamente todas las implementaciones de chatbot con Streamlit.
Contexto: el auge de los componentes de chat
Streamlit introdujo st.chat_message, st.chat_input y st.write_stream en 2023 como respuesta directa al auge de las aplicaciones basadas en modelos de lenguaje. Antes de estos componentes, los desarrolladores improvisaban interfaces de chat con combinaciones de st.markdown, st.container y st.text_input, lo que generaba código verboso y comportamientos inconsistentes. Hoy, estos tres bloques cubren el 90 % de las necesidades de cualquier chatbot: renderizado con avatares, entrada fija en el pie de página y streaming token a token.
La filosofía detrás de ellos es alinear la API de Streamlit con el formato de mensajes estándar de OpenAI ({"role": "user", "content": "..."}), de forma que puedas pasar directamente st.session_state.mensajes como argumento messages a cualquier API de LLM sin transformaciones intermedias.
Explicación línea por línea del patrón chatbot
En el ejemplo integrado con OpenAI hay varios detalles críticos:
@st.cache_resourcecachea la instancia del cliente OpenAI entre reruns. Sin él, se crearía un nuevo cliente en cada interacción, con el consiguiente coste de reconexión HTTP.if "mensajes_llm" not in st.session_state:inicializa el historial solo la primera vez que se carga la app.- El bucle
for msg in st.session_state.mensajes_llmrepinta el historial completo en cada rerun, lo cual es necesario porque Streamlit no recuerda los mensajes pintados entre ejecuciones. if prompt := st.chat_input(...)usa el operador walrus para capturar el prompt solo cuando el usuario envía algo.client.chat.completions.create(..., stream=True)devuelve un generador de chunks. Cada chunk contiene un fragmento del texto.st.write_stream(...)consume el generador y renderiza los fragmentos a medida que llegan, devolviendo la cadena completa al final para guardarla en el historial.
Tabla de parámetros
| Componente | Parámetro | Descripción |
|-----------|-----------|-------------|
| st.chat_message | name | Rol del mensaje: "user", "assistant" o personalizado |
| st.chat_message | avatar | Emoji, URL o path a una imagen de avatar |
| st.chat_input | placeholder | Texto gris que se muestra cuando el campo está vacío |
| st.chat_input | key | Identificador único para el widget |
| st.chat_input | max_chars | Límite de caracteres permitidos |
| st.chat_input | disabled | Desactiva el campo temporalmente |
| st.write_stream | stream | Generador, iterador o respuesta streaming compatible |
Errores comunes al construir chatbots
Olvidar persistir el historial en session_state. Sin esa persistencia, cada interacción vacía el chat. Es el error número uno al empezar con los componentes de chat.
Llamar a la API dentro del bucle de repintado. Si colocas la llamada al LLM dentro del for msg in st.session_state.mensajes en lugar de solo al recibir un nuevo prompt, la API se invocará una vez por mensaje histórico en cada rerun, quemando créditos sin sentido.
Mezclar st.chat_input con widgets en formularios. st.chat_input no puede estar dentro de un st.form porque su comportamiento asíncrono entra en conflicto con el patrón de envío por lotes del formulario.
No cachear el cliente del LLM. Instanciar un nuevo cliente en cada rerun es lento y puede provocar errores de rate limit. Usa siempre @st.cache_resource para clientes de APIs externas.
Streaming sin manejar None. Algunas APIs de LLM envían chunks con content=None al principio o al final del stream. Si no los filtras, st.write_stream puede lanzar TypeError. El patrón seguro es chunk.content or "".
Mejores prácticas
- Para chats largos, plantéate limitar el historial enviado al LLM (por ejemplo, los últimos 20 mensajes) para controlar el coste de tokens.
- Usa avatares personalizados para distinguir entre varios asistentes (un bot de soporte vs. un bot de ventas, por ejemplo).
- Añade un botón de "Limpiar conversación" que vacíe
session_statepara que el usuario pueda empezar de cero. - Para producción, guarda el historial en una base de datos y usa
st.session_statesolo como caché de sesión: así los usuarios pueden retomar conversaciones entre visitas. - Combina
st.chat_messageconst.statusost.spinnerpara mostrar progreso cuando el LLM tarda en responder.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Streamlit es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Streamlit
Explora más contenido relacionado con Streamlit y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Mostrar mensajes de chat con roles user y assistant usando st.chat_message. Capturar la entrada del usuario con st.chat_input de forma no bloqueante. Implementar streaming de respuestas con st.write_stream para efecto typewriter. Mantener el historial de conversación con st.session_state. Integrar las APIs de LLM (OpenAI, Anthropic) con la interfaz de chat de Streamlit.