Agregando Memoria a un Chatbot con Supabase

Ing. José Miguel Amaya Camacho

Python Piura - Piura AI

miguel.amaya99@gmail.com

Chatbots y la importancia de la memoria

Los chatbots deben dar respuestas coherentes y útiles. Sin memoria, un chatbot "olvida" lo que el usuario le dijo antes, obligándolo a repetir datos en cada mensaje.

1.

2.

La memoria permite al bot recordar contexto y detalles, es una función cognitiva clave que ayuda al chatbot a adaptarse y ofrecer respuestas más relevantes

3.

Incorporaremos memoria de corto y largo plazo en en un chatbot de Telegram. Usando Langchain y LangGraph y almacenando datos de memoria en Supabase.

# INTRODUCCIÓN
# FUJO DE TRABAJO
  • 🧑 Usuario
       ▼
    💬 Telegram Bot
       ▼
    📦 GraphBot (orquestador)
       ├── 🧠 Memoria a corto plazo (Supabase + AsyncPostgresSaver)     ├── 🧠 Memoria a largo plazo (Supabase + vector_pg)
       └── 🔁 LangGraph y LangChain
              ├── 📄 Prompt Templates
              ├── 🔗 Chains (LLM + lógica)
              └── 🤖 Llamada al modelo (OpenAI, etc.)
       ▼
    📝 Genera respuesta
       ▼
    📩 Se envía de vuelta al usuario por Telegram

Flujo del Chatbot

# LANGCHAIN
  • Telegram es una app de mensajería que permite crear bots conversacionales fácilmente.

  • Usamos un webhook en FastAPI que permite recibir actualizaciones de Telegram en tiempo real.

    • Usuario->>Telegram: Envía mensaje.

    • Telegram->>Webhook: POST /webhook/{token}

    • Webhook->>Bot: Procesa actualización

    • Bot->>Usuario: Envía respuesta

¿Por Qué Telegram?

# LANGCHAIN

¿Qué es LangChain?

  • LangChain es una herramienta para construir aplicaciones que usan LLMs(grandes modelos de lenguaje).

  • Hace fácil conectar el modelo con otras cosas:

    • 🔗 Bases de datos

    • 🧠 Memoria de conversación

    • 📄 Documentos

    • 🧰 Herramientas externas (como buscadores o APIs)

# LANGCHAIN

¿Qué es LangChain?

  • 💡 Piensa en LangChain como el "cerebro" + "conectores"

  • 👉 Tú defines:

    • Qué información darle al modelo

    • Qué hacer con su respuesta

  • 📦 Ejemplos de lo que puedes crear:

    • Chatbots

    • Agentes autónomos

    • Asistentes que leen PDFs

# LANGCHAIN

¿Qué es un Prompt?

  • Un prompt es el texto que le das al modelo para guiar su respuesta.

  • Piensa en el prompt como la personalidad + instrucciones para el bot.

CHARACTER_PROMPT = """
Estás a punto de jugar a un juego de prueba de Turing como parte de un experimento. 
Te emparejarán con un humano en una conversación de chat.
Tu tarea es interpretar al siguiente personaje, convenciendo al humano de que eres un humano real.

## Biografía de Lucía

## Personalidad de Lucía

## Antecedentes del usuario

Esto es lo que sabes sobre el usuario gracias a conversaciones anteriores:

{memory_context}

Proporciona respuestas de texto simple, sin indicadores de formato ni metacomentarios.
"""
# LANGCHAIN

Prompt de Memoria

  • Usamos también un prompt para extraer los datos relevantes del usuario y guardarlos en la memoria de largo plazo.

MEMORY_ANALYSIS_PROMPT = """Extraiga y formatee datos personales importantes del usuario a partir de su mensaje.
Céntrese en la información real, no en metacomentarios ni solicitudes.

Los datos importantes incluyen:
- Datos personales (nombre, edad, ubicación)
- Información profesional (trabajo, formación, habilidades)
- Preferencias (gustos, disgustos, favoritos)
- Circunstancias vitales (familia, relaciones)
- Experiencias o logros significativos
- Metas o aspiraciones personales

Examples:
Input: "Oye, ¿podrías recordar que me encanta Star Wars?"
Output: {{
    "is_important": true,
    "formatted_memory": "Le encanta Star Wars"
}}
"""
# LANGCHAIN

 ¿Qué es una Cadena (Chain)?

  • Las Chains son secuencias de operaciones que procesan y transforman entradas para producir salidas específicas. Son como "tuberías" que conectan diferentes componentes.

  • En nuestro proyecto tenemos dos chains principales:

    • Memory Chain: Analiza mensajes para extraer información importante.

    • Character Chain: Genera respuestas en personaje.

# LANGCHAIN

Chains

def get_memory_chain():
    model = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(MemoryAnalysis)
    prompt = ChatPromptTemplate.from_template(MEMORY_ANALYSIS_PROMPT)
    return prompt | model


def get_character_chain():
    model = ChatOpenAI(model="gpt-4o", temperature=0.5)
    system_message = CHARACTER_PROMPT
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system_message),
            MessagesPlaceholder(variable_name="messages"),
        ]
    )
    return prompt | model
# LANGGRAPH

¿Qué es LangGraph?

  • LangGraph es una "extensión" de LangChain para crear agentes con lógica compleja

  • 💡 Usa grafos para definir flujos de decisión entre pasos

  • 📌 Cada nodo del grafo hace algo:

    • Llama al modelo

    • Consulta memoria

    • Toma decisiones

    • Guarda estado

# LANGGRAPH

¿Qué es LangGraph?

  • Piensa en LangGraph como un diagrama de flujo inteligente donde el modelo elige el camino según el contexto
  • Ventajas:
    • Soporta memoria de largo plazo

    • Fácil de visualizar y depurar

    • Ideal para bots con múltiples habilidades

# GRAFO

Grafo

  • Flujo definido por nodos y aristas.
  • Cada nodo es una función asíncrona.
  • Procesa y transforma el estado.
  • Define el flujo de ejecución
  • Secuencia clara
def create_workflow_graph():
    graph_builder = StateGraph(StateBot)
    graph_builder.add_node("memory_extraction_node", memory_extraction_node)
    graph_builder.add_node("memory_injection_node", memory_injection_node)
    graph_builder.add_node("generate_response", generate_response)
    graph_builder.add_edge(START, "memory_extraction_node")
    graph_builder.add_edge("memory_extraction_node", "memory_injection_node")
    graph_builder.add_edge("memory_injection_node", "generate_response")
    graph_builder.add_edge("generate_response", END)
    return graph_builder
# ESTADO

Estado

  • El estado hereda de MessagesState de LangGraph.
  • Almacena:
    • Historial de mensajes (heredado)
    • Contexto de memoria personalizado
    • Mantiene el estado de la conversación
from langgraph.graph import MessagesState


class StateBot(MessagesState):
    memory_context: str
# NODOS

Nodos

  • Memory Extraction Node: Extrae información importante de los mensajes.
    • Analiza último mensaje.
    • Si es importante:
      • Vectoriza el mensaje y lo guarda en la base de datos.
  • Memory Injection Node: Recupera memoria relevante.
    • Toma último mensaje.
    • Busca similitud semántica.
    • Construye contexto de memoria.
  • Generate Response: Genera respuesta del bot.
    • Usa el character chain, incorpora memoria recuperada, mantiene consistencia.
# MEMORIA

Fundamentos de Memoria (short-term vs long-term)

 

En la imagen se ve cómo la memoria a corto plazo (short-term) conserva el diálogo reciente de un mismo hilo conversacional, mientras que la memoria a largo plazo (long-term) guarda conocimientos generales (p.ej. datos personales) en un almacén externo.

# SHORT TERM

Memoria a corto plazo

 

Contexto y mensajes recientes de una sola conversación. LangGraph la trata como parte del estado interno del agente, persistido con un checkpoint (MemorySaver) por cada hilo de conversación. Esto permite retomar la sesión sin perder el hilo.

# LONG TERM

Memoria a largo plazo

Conocimientos que se comparten entre conversaciones distintas. Son datos permanentes (p.ej. perfil del usuario, hechos guardados) que no dependen de un solo hilo. LangChain provee stores especiales para guardar y recuperar estos recuerdos semánticos

# LONG TERM

Búsqueda semántica

  • Búsqueda semántica: Dado un vector de consulta, se busca en el almacenamiento de vectores (vector store) aquellos vectores más similares. Esto permite recuperar recuerdos relacionados, no solo por coincidencia de palabras. Ejemplo: Si el usuario dice “Ayer jugué al tenis en Lima”, el chatbot puede guardar ese hecho como embedding. Más tarde, si el usuario pregunta “¿Qué hice ayer?”, el bot hace una búsqueda semántica en su memoria larga para encontrar el recuerdo relevante (tenis, Lima) y así responder apropiadamente.
# MEMORIA

¿Cómo modela  LangGraph la  memoria?

  • Para la memoria corta, utiliza checkpointers (MemorySaver) que guardan automáticamente el estado al ejecutar el grafo.
  • Para la memoria larga, permite definir stores de larga duración que el chatbot consulta cuando sea necesario. LangGraph gestiona cuándo leer o escribir en esos stores como parte del flujo conversacional.
# SUPABASE

Supabase, por fin...

  • Es una alternativa open-source a Firebase construida sobre PostgreSQL. Ofrece base de datos relacional con extensiones modernas.
  • Tiene integración sencilla con python a través de la biblioteca supabase y con LangChain.
  • pgvector: Es una extensión de Postgres que permite almacenar vectores numéricos en una columna. Supabase habilita pgvector para guardar embeddings y hacer busqueda vectorial.
# SUPABASE

Url de Conexión a Supabase

  • En supabase ubicamos el botón Connect para obtener la url de conexión.

  • Usaremos session pooler debido a restricciones de IPV4 en el plan free.

  • Guardamos la url de conexión en la variable DB_URI.

# SUPABASE

API Supabase

  • Para obtener los datos de la API, vamos a ver la configuracion del proyecto y ubicamos la opción Data API.

  • Necesitamos la url y el service role secret, los guardamos en las variables: SUPABASE_URL y SUPABASE_SERVICE_KEY respectivamente.

# SUPABASE

Supabase como Memoria de Corto Plazo

  • Usamos AsyncPostgresSaver de langgraph.checkpoint.postgres para conectarnos a la DB_URI de supabase.

  • Cada conversación se guarda con un thread_id

async def reply(self, chat_id, text=None):
  config = {"configurable": {"thread_id": str(chat_id), "chat_id": str(chat_id)}}
  DB_URI = os.environ.get('DB_URI')
  async with AsyncPostgresSaver.from_conn_string(DB_URI) as short_term_memory:
    await short_term_memory.setup()
    graph = self.graph_builder.compile(checkpointer=short_term_memory, store=self.store)
    await graph.ainvoke({"messages": [HumanMessage(content=text)]}, config)
    output_state = await graph.aget_state(config=config)
  response_message = output_state.values["messages"][-1].content
  return response_message
# SUPABASE

¿Qué se guarda?

  • El hilo de la conversación

  • El estado del grafo del flujo del bot: mensajes y otros.

  • Todo se guarda automáticamente al usar:

    • checkpointer=short_term_memory

    • Se generan las tablas de la imagen

# SUPABASE

Supabase como Memoria de Largo Plazo

  • En supabase configuramos una tabla documents(id, content, metadata, embedding).
  • Configuramos el vector store de supabase para establecer la conexión y poder guardar y recuperar información.
  • En el flujo de la conversación capturamos cada dato importante del usuario, calculamos su embedding y lo guardamos junto con el texto y los metadatos necesarios en la tabla documents.
  • Cuando el usuario haga una consulta que tenga que ver con sus datos buscamos en la tabla documents los vectores más similares y recuperamos la información que sea relevante.
# PG_VECTOR

Configuración de pg_vector

  • Vamos a la opción Database del proyecto en Supabase, luego seleccionamos extensions y activamos vector.
# QUICKSTART

Quickstart de Langchain

  • Vamos al SQL Editor y seleccionamos Quickstarts, aquí buscamos la opción de Langchain.
# QUICKSTART

Quickstart de Langchain

  • Nos aparece la query para crear la extensión, la tabla y la función de busqueda, vamos a reemplazar esto con lo que nos dice la documentación de la integración de langchain+supabase: 
# QUICKSTART

Quickstart de Langchain

  • Copiamos la configuración recomendada en la documentación, pero ya no necesitamos crear nuevamente la extensión pg_vector porque ya lo hicimos antes en extensiones y notamos id cambia de tipo de bigserial a uuid, sino hacemos esto nos dará error.
# VECTOR STORE

SupabaseVectorStore

  • Usamos el método create_client de supabase-py para establecer la conexión con SupabaseVectorStore de LangChain, trabajaremos con OpenAIEmbeddings.
from langchain_community.vectorstores import SupabaseVectorStore
from supabase import create_client


def create_supabase_vector_store():
    vector_store = None
    embeddings = get_embeddings()
    try:
        supabase = create_client(
            supabase_url=os.environ.get('SUPABASE_URL'),
            supabase_key=os.environ.get('SUPABASE_SERVICE_KEY')
        )
        vector_store = SupabaseVectorStore(
            client=supabase,
            embedding=embeddings,
            table_name="documents",
            query_name="match_documents"
        )
    except Exception as e:
        logger.error(f"Error creando vector store: {str(e)}")
    return vector_store
# RETRIEVER

Recuperador(Retriever)

  • Configuramos un retriever de documentos vectoriales, con 3 documentos similares a retornar por defecto, usando el vector store de supabase.
def get_retriever(k=3):
    vector_store = create_supabase_vector_store()
    if vector_store:
        return vector_store.as_retriever(search_kwargs={"k": k})
    return None
# EXTRACCIÓN

Nodo de Extracción de Memoria

  • En el nodo memory_extraction_node de nuestro grafo, insertamos el dato del usuario en supabase usando el método add_texts, esto guardara el texto,  el vector numérico y los metadatos.
async def memory_extraction_node(state: StateBot, config: RunnableConfig):
    chain = get_memory_chain()
    response = await chain.ainvoke({"message": state["messages"][-1]})
    if response.is_important:
        chat_id = config["configurable"]["chat_id"]
        retriever = get_retriever()
        retriever.vectorstore.add_texts(
            texts=[response.formatted_memory],
            metadatas=[{"chat_id": int(chat_id)}]
        )
    return {}
# INYECCIÓN

Nodo de Inyección de Memoria

  • En el nodo memory_injection_node de nuestro grafo, hacemos una busqueda semántica en supabase de acuerdo al último mensaje del usuario, los resultados se agregan al estado del bot.
async def memory_injection_node(state: StateBot, config: RunnableConfig):
    chat_id = config["configurable"]["chat_id"]
    retriever = get_retriever()
    last_message = state["messages"][-1].content
    relevant_docs = await retriever.ainvoke(
        last_message,
        filter={"chat_id": int(chat_id)},
    )
    memory_context = "\n".join(doc.page_content for doc in relevant_docs)
    return {"memory_context": memory_context}
# DOCUMENTS

Tabla Documents

  • Si vamos al editor de tablas de supabase y seleccionamos la tabla documents podemos observar los datos que se han ido guardando en la memoria de largo plazo.
# GENERACIÓN

Nodo de Generación de Respuesta

  • Finalmente la memoria a largo plazo obtenida se inserta junto con los mensajes del estado, gestionados por la memoria de corto plazo, para entregarle una respuesta coherente al usuario
async def generate_response(state: StateBot, config: RunnableConfig):
    memory_context = state.get('memory_context')
    chain = get_character_chain()
    response = await chain.ainvoke(
        {
            "messages": state['messages'],
            "memory_context": memory_context,
        },
        config,
    )
    return {"messages": response}
# RESULTADOS

Resultados

En la imagen siguiente le pregunté mi nombre sin habérselo dado, cuando ya se lo doy lo recuerda perfectamente.

# RESULTADOS

Resultados

En la imagen siguiente borré las tablas que guardan la memoria de corto plazo, pero dejé las de largo plazo

Muchas gracias

Agregando Memoria a un Chatbot con Supabase

By Miguel Amaya Camacho

Agregando Memoria a un Chatbot con Supabase

  • 220