Arquitectura de Memoria Persistente para Chatbots en Telegram usando MongoDB

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 MongoDB.

# INTRODUCCIÓN
# FUJO DE TRABAJO
  • 🧑 Usuario
       ▼
    💬 Telegram Bot
       ▼
    📦 GraphBot (orquestador)
       ├── 🧠 Memoria a corto plazo (MongoDB)                                           ├── 🧠 Memoria a largo plazo (MongoDB)
       └── 🔁 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 en una secuencia clara
def create_workflow_graph(memories_retriever=None):
    graph_builder = StateGraph(StateBot)
    if memories_retriever is None:
        memories_retriever = get_retriever_mongodb(
        	k=5, collection_name="memories", index_name="memories-vector-index", 
          	filters=["chat_id"]
        )
    graph_builder.add_node("memory_extraction_node", 
                           partial(memory_extraction_handler, retriever=memories_retriever))
    graph_builder.add_node("memory_injection_node", 
                           partial(memory_injection_handler, retriever=memories_retriever))
    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

  • Es una forma de buscar por significado, no solo por palabras exactas.
  • En lugar de comparar texto literal, convierte frases en vectores numéricos (embeddings) y encuentra los que se parecen en sentido.
  • ❌ “auto” ≠ “carro” en una búsqueda normal
    ✅ En búsqueda semántica: “auto” ≈ “carro” (porque significan lo mismo)
# 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

MongoDB

  • Base de datos NoSQL orientada a documentos
  • Guarda información en formato JSON flexible (BSON)
  • No necesita esquemas fijos ni relaciones tradicionales
  • Escalable y rápida para grandes volúmenes de datos.
  • Ideal para memoria persistente de chatbots
  • Soporta búsqueda de texto y similitud (Atlas Search)
# SUPABASE

MongoDB como Memoria de Corto Plazo

  • Usamos AsyncMongoDBSaver de langgraph.checkpoint.mongodb 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)}}
  async with AsyncMongoDBSaver.from_conn_string(
    settings.MONGO_DB_URL,
    db_name=settings.MONGO_DB_NAME
  ) as checkpointer:
    graph = self.graph_builder.compile(checkpointer=checkpointer)
    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=checkpointer

    • Se generan las tablas de la imagen

# SUPABASE

MongoDB Atlas Search

  • Es el motor de búsqueda avanzada integrado en MongoDB Atlas.
    Permite buscar por texto, filtros y significado usando índices optimizados.
  • Lo que hace

    🔠 Texto completo: busca palabras y frases en documentos.

    🧠 Búsqueda vectorial: encuentra resultados por similitud semántica.

    ⚙️ Filtros combinados: mezcla búsquedas por texto y campos (ej. chat_id).

# SUPABASE

MongoDB Atlas Search como Memoria de Largo Plazo

  • Configuramos el vector store de MongoDB para establecer la conexión y poder guardar y recuperar información en una colecció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 colección los vectores más similares y recuperamos la información que sea relevante.
# VECTOR STORE

MongoDBVectorStore

  • Administra instancias únicas de MongoDBAtlasVectorSearch
  • Evita recrear conexiones innecesarias
  • Crea el índice vectorial si no existe
  • Centraliza acceso a la colección y embeddings
class MongoDBVectorStore:
    _instances: ClassVar[Dict[str, MongoDBAtlasVectorSearch]] = {}
    EMBEDDING_DIMENSION = 1536

    @classmethod
    def get_instance(
        cls,
        collection_name: str,
        index_name: str,
        filters: Optional[list] = None
    ) -> MongoDBAtlasVectorSearch:
        instance_key = f"{collection_name}:{index_name}"
        if instance_key not in cls._instances:
            collection = cls._get_collection(collection_name)
            vector_store = cls._create_vector_store(collection, index_name)
            cls._ensure_vector_index(vector_store, collection, index_name, filters)
            cls._instances[instance_key] = vector_store
        return cls._instances[instance_key]
# VECTOR STORE

MongoDBVectorStore

  • _get_collection() → conecta y obtiene la colección

  • _create_vector_store() → crea el vector store

  • _ensure_vector_index() → verifica o crea el índice

@staticmethod
def _get_collection(collection_name: str):
  MongoDBConnection.connect_to_sync_mongo()
  db = MongoDBConnection.get_sync_db()
  return db[collection_name]

@staticmethod
def _create_vector_store(collection, index_name: str) -> MongoDBAtlasVectorSearch:
  return MongoDBAtlasVectorSearch(
    collection=collection,
    embedding=get_embeddings(),
    index_name=index_name,
    relevance_score_fn="cosine"
  )
# VECTOR STORE

MongoDBVectorStore

@classmethod
def _ensure_vector_index(
  cls,
  vector_store: MongoDBAtlasVectorSearch,
  collection,
  index_name: str,
  filters: Optional[list]
):
  existing_indexes = collection.list_search_indexes()
  existing_names = [idx["name"] for idx in existing_indexes]
  if index_name not in existing_names:
    try:
      vector_store.create_vector_search_index(
        dimensions=cls.EMBEDDING_DIMENSION,
        filters=filters or []
      )
      except Exception as e:
        print(f"Warning: No se pudo crear el índice vectorial: {e}")
# RETRIEVER

MongoDBVectorStore

  • Creamos una sola instancia reutilizable de MongoDBVectorStore para evitar reconexiones y mejorar el rendimiento del backend.
@lru_cache
def get_mongo_db_vector_store() -> MongoDBVectorStore:
    return MongoDBVectorStore()
# RETRIEVER

Recuperador(Retriever)

  • Creamos un retriever reutilizable que busque los k vectores más relevantes en una colección de MongoDB (usando el índice vectorial y filtros por contexto).
def get_retriever_mongodb(
    k: int, collection_name: str, index_name: str, filters: list
):
    vector_store = get_mongo_db_vector_store().get_instance(
        collection_name=collection_name,
        index_name=index_name,
        filters=filters
    )
    return vector_store.as_retriever(search_kwargs={"k": k})
# EXTRACCIÓN

Nodo de Extracción de Memoria

  • En el nodo memory_extraction_node de nuestro grafo, insertamos el dato del usuario en mongodb usando el método aadd_texts, esto guardara el texto,  el vector numérico y los metadatos.
async def memory_extraction_node(
  state: StateBot, retriever, config: RunnableConfig
):
    chain = get_memory_chain()
    response = await chain.ainvoke({"message": state["messages"][-1]})
    if response.is_important and retriever:
        config = config.get("configurable")
        chat_id = config.get("chat_id")
        try:
            await retriever.vectorstore.aadd_texts(
                texts=[response.formatted_memory],
                metadatas=[{"chat_id": chat_id}]
            )
        except Exception as e:
            print(f"Error almacenando memoria: {str(e)}")
    return {}
# INYECCIÓN

Nodo de Inyección de Memoria

  • En el nodo memory_injection_node de nuestro grafo, hacemos una busqueda semántica en mongodb de acuerdo al último mensaje del usuario, los resultados se agregan al estado del bot.
async def memory_injection_node(
  state: StateBot, retriever, config: RunnableConfig
):
    if not retriever:
        return {"memory_context": ""}
    config = config.get("configurable")
    chat_id = config.get("chat_id")
    last_message = state["messages"][-1].content
    try:
        relevant_docs = await retriever.ainvoke(
            last_message,
            pre_filter={"chat_id": chat_id}
        )
        memory_context = "\n".join(doc.page_content for doc in relevant_docs)
        return {"memory_context": memory_context}
    except Exception as e:
        print(f"Error recuperando memorias: {str(e)}")
        return {"memory_context": ""}
# DOCUMENTS

Colección memories

  • Si vamos al gestor de mongodb y seleccionamos la colección memories 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

Arquitectura de Memoria Persistente para Chatbots en Telegram usando MongoDB

By Miguel Amaya Camacho

Arquitectura de Memoria Persistente para Chatbots en Telegram usando MongoDB

  • 99