Miguel Amaya Camacho
Ingeniero Informático. Socio fundador de Tallanix S.A.C y de Xprende Tech. Activista del Software Libre y miembro fundador de la Comunidad Piurana de Software Libre VICUX y de la Comunidad de Programadores Python Piura.
Ing. José Miguel Amaya Camacho
Python Piura - Piura AI
miguel.amaya99@gmail.com
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.
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
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
# 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
# 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
💡 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
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
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
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
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
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
Soporta memoria de largo plazo
Fácil de visualizar y depurar
Ideal para bots con múltiples habilidades
# GRAFO
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
from langgraph.graph import MessagesState
class StateBot(MessagesState):
memory_context: str
# NODOS
# MEMORIA
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
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
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
# MEMORIA
# SUPABASE
# 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
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
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
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
# PG_VECTOR
# QUICKSTART
# QUICKSTART
# QUICKSTART
# VECTOR STORE
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
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
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
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
# GENERACIÓN
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
En la imagen siguiente le pregunté mi nombre sin habérselo dado, cuando ya se lo doy lo recuerda perfectamente.
# RESULTADOS
En la imagen siguiente borré las tablas que guardan la memoria de corto plazo, pero dejé las de largo plazo
By Miguel Amaya Camacho
Ingeniero Informático. Socio fundador de Tallanix S.A.C y de Xprende Tech. Activista del Software Libre y miembro fundador de la Comunidad Piurana de Software Libre VICUX y de la Comunidad de Programadores Python Piura.