st.chat_message, st.chat_input y st.write_stream para interfaces de chat

Intermedio
Streamlit
Streamlit
Actualizado: 26/04/2026

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:

  1. @st.cache_resource cachea 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.
  2. if "mensajes_llm" not in st.session_state: inicializa el historial solo la primera vez que se carga la app.
  3. El bucle for msg in st.session_state.mensajes_llm repinta el historial completo en cada rerun, lo cual es necesario porque Streamlit no recuerda los mensajes pintados entre ejecuciones.
  4. if prompt := st.chat_input(...) usa el operador walrus para capturar el prompt solo cuando el usuario envía algo.
  5. client.chat.completions.create(..., stream=True) devuelve un generador de chunks. Cada chunk contiene un fragmento del texto.
  6. 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_state para que el usuario pueda empezar de cero.
  • Para producción, guarda el historial en una base de datos y usa st.session_state solo como caché de sesión: así los usuarios pueden retomar conversaciones entre visitas.
  • Combina st.chat_message con st.status o st.spinner para mostrar progreso cuando el LLM tarda en responder.
Alan Sastre - Autor del tutorial

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.