LangGraph Redis Checkpointer and MongoDB Persistence Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Implement LangGraph com Redis como o checkpointer principal para gerenciamento de estado de alta performance, e integrar MongoDB para persistência de longo prazo de dados selecionados da aplicação.
Architecture: O LangGraph utilizará RedisSaver para o gerenciamento de seu estado interno, aproveitando a velocidade do Redis para o contexto de interação em tempo real. Dados específicos da aplicação, como agendamentos confirmados ou perfis de usuário, serão persistidos no MongoDB via ações explícitas (ferramentas) do agente dentro do fluxo do LangGraph, garantindo durabilidade e armazenamento de longo prazo para informações críticas.
Tech Stack: Python, LangGraph, Redis, MongoDB, uv (para gerenciamento de dependências), pymongo, langgraph-checkpoint-redis.
File Structure
- Create:
src/amon_claw/infrastructure/database/redis/client.py - Responsibility: Inicializar e prover a instância do cliente Redis e
RedisSaver. - Create:
src/amon_claw/infrastructure/database/mongodb/client.py - Responsibility: Inicializar e prover a instância do cliente MongoDB e conexão com o banco de dados.
- Modify:
pyproject.toml - Responsibility: Adicionar as dependências
langgraph-checkpoint-redisepymongo. - Modify:
compose.yml - Responsibility: Garantir que os serviços Redis e MongoDB estejam definidos e configurados corretamente.
- Modify:
src/amon_claw/core/config.py - Responsibility: Definir as configurações de conexão para Redis e MongoDB.
- Modify:
src/amon_claw/main.py - Responsibility: Integrar o
RedisSavercom a aplicação LangGraph e fornecer mecanismos para interação com o MongoDB. - Create:
src/amon_claw/application/use_cases/appointment_persistence.py - Responsibility: Conter a lógica para salvar agendamentos ou outros dados específicos no MongoDB.
- Create:
tests/infrastructure/database/redis/test_client.py - Responsibility: Testar a inicialização do cliente Redis e do
RedisSaver. - Create:
tests/infrastructure/database/mongodb/test_client.py - Responsibility: Testar a inicialização do cliente MongoDB e a funcionalidade de persistência.
Task 1: Atualizar pyproject.toml com novas dependências
Files:
- Modify: pyproject.toml
- Step 1: Adicionar as dependências
langgraph-checkpoint-redisepymongo
# pyproject.toml
[project.dependencies]
python = ">=3.11,<3.13"
langchain = "^0.1.13"
langgraph = "^0.0.30"
pydantic = "^2.6.4"
python-dotenv = "^1.0.1"
uvicorn = "^0.28.0"
fastapi = "^0.110.0"
mkdocs = "^1.5.3"
mkdocs-material = "^9.5.15"
mkdocstrings = { extras = ["python"], version = "^0.24.0" }
loguru = "^0.7.2"
beanie = "^1.24.0"
motor = "^3.3.2"
langgraph-checkpoint-redis = "^0.0.1" # Adicionar esta linha
pymongo = "^4.6.2" # Adicionar esta linha
[tool.uv.sources]
# ... (restante do arquivo)
- Step 2: Commit
git add pyproject.toml
git commit -m "feat: Adiciona dependências para Redis Checkpointer e MongoDB"
Task 2: Atualizar compose.yml para os serviços Redis e MongoDB
Files:
- Modify: compose.yml
- Step 1: Adicionar/Verificar os serviços Redis e MongoDB
# compose.yml
services:
amon_claw_app:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
environment:
# Exemplo de variáveis de ambiente, ajuste conforme necessário
REDIS_HOST: redis
REDIS_PORT: 6379
MONGO_URI: mongodb://mongo:27017/amon_claw_db
depends_on:
- redis
- mongo
redis:
image: redis/redis-stack:latest # Usar redis-stack para módulos RedisJSON e RediSearch
ports:
- "6379:6379"
- "8001:8001" # Porta para RedisInsight (opcional)
volumes:
- redis_data:/data # Volume persistente para dados do Redis
mongo:
image: mongo:latest
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db # Volume persistente para dados do MongoDB
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USERNAME} # Se usar autenticação
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD} # Se usar autenticação
volumes:
redis_data:
mongo_data:
- Step 2: Commit
git add compose.yml
git commit -m "infra: Adiciona e configura serviços Redis e MongoDB no compose.yml"
Task 3: Adicionar configurações Redis e MongoDB a amon_claw/core/config.py
Files:
- Modify: src/amon_claw/core/config.py
- Step 1: Adicionar variáveis de ambiente para Redis e MongoDB
# src/amon_claw/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pathlib import Path
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# ... (outras configurações existentes)
# Configurações do Redis
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
# Configurações do MongoDB
MONGO_URI: str = "mongodb://localhost:27017/amon_claw_db"
MONGO_DB_NAME: str = "amon_claw_db" # Pode ser inferido da URI, mas é bom ter explícito
settings = Settings()
- Step 2: Commit
git add src/amon_claw/core/config.py
git commit -m "feat: Adiciona configurações para Redis e MongoDB"
Task 4: Implementar Cliente Redis e RedisSaver em amon_claw/infrastructure/database/redis/client.py
Files:
- Create: src/amon_claw/infrastructure/database/redis/client.py
- Step 1: Criar o arquivo
client.pycom o cliente Redis eRedisSaver
# src/amon_claw/infrastructure/database/redis/client.py
from redis import Redis
from langgraph.checkpoint.redis import RedisSaver
from amon_claw.core.config import settings
from loguru import logger
_redis_client: Redis | None = None
_redis_saver: RedisSaver | None = None
def get_redis_client() -> Redis:
"""Retorna uma instância singleton do cliente Redis."""
global _redis_client
if _redis_client is None:
try:
_redis_client = Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
decode_responses=True # Decodifica respostas para string por padrão
)
_redis_client.ping() # Testa a conexão
logger.info(f"Conectado ao Redis em {settings.REDIS_HOST}:{settings.REDIS_PORT}")
except Exception as e:
logger.error(f"Falha ao conectar ao Redis: {e}")
raise
return _redis_client
def get_redis_saver() -> RedisSaver:
"""Retorna uma instância singleton do RedisSaver para LangGraph."""
global _redis_saver
if _redis_saver is None:
redis_client = get_redis_client()
_redis_saver = RedisSaver(redis_client=redis_client)
logger.info("RedisSaver para LangGraph inicializado.")
return _redis_saver
# Exemplo de uso (opcional, pode ser removido ou movido para testes)
if __name__ == "__main__":
client = get_redis_client()
saver = get_redis_saver()
print("Redis client e saver obtidos com sucesso!")
- Step 2: Commit
git add src/amon_claw/infrastructure/database/redis/client.py
git commit -m "feat: Implementa cliente Redis e RedisSaver para LangGraph"
Task 5: Implementar Cliente MongoDB em amon_claw/infrastructure/database/mongodb/client.py
Files:
- Create: src/amon_claw/infrastructure/database/mongodb/client.py
- Step 1: Criar o arquivo
client.pycom o cliente MongoDB
# src/amon_claw/infrastructure/database/mongodb/client.py
from motor.motor_asyncio import AsyncIOMotorClient
from amon_claw.core.config import settings
from loguru import logger
_mongo_client: AsyncIOMotorClient | None = None
def get_mongo_client() -> AsyncIOMotorClient:
"""Retorna uma instância singleton do cliente MongoDB assíncrono."""
global _mongo_client
if _mongo_client is None:
try:
_mongo_client = AsyncIOMotorClient(settings.MONGO_URI)
# Testar a conexão (ex: listar nomes de dbs)
# await _mongo_client.list_database_names() # Não é possível em sync context
logger.info(f"Conectado ao MongoDB em {settings.MONGO_URI}")
except Exception as e:
logger.error(f"Falha ao conectar ao MongoDB: {e}")
raise
return _mongo_client
def get_mongo_db():
"""Retorna a instância do banco de dados MongoDB."""
client = get_mongo_client()
return client[settings.MONGO_DB_NAME]
# Exemplo de uso (opcional, pode ser removido ou movido para testes)
if __name__ == "__main__":
import asyncio
async def test_connection():
client = get_mongo_client()
db = get_mongo_db()
try:
await client.admin.command('ping')
logger.info("MongoDB ping bem-sucedido!")
logger.info(f"Conectado ao banco de dados: {db.name}")
except Exception as e:
logger.error(f"Falha ao pingar MongoDB: {e}")
finally:
client.close()
asyncio.run(test_connection())
- Step 2: Commit
git add src/amon_claw/infrastructure/database/mongodb/client.py
git commit -m "feat: Implementa cliente MongoDB assíncrono"
Task 6: Integrar RedisSaver na aplicação LangGraph em amon_claw/main.py
Files:
- Modify: src/amon_claw/main.py
- Step 1: Importar e usar
get_redis_saverao compilar o grafo
# src/amon_claw/main.py
# ... (outros imports)
from langgraph.graph import StateGraph, END
from amon_claw.infrastructure.database.redis.client import get_redis_saver
from loguru import logger
from typing import TypedDict, Annotated
import operator
import asyncio # Para rodar o exemplo async
class AgentState(TypedDict):
"""Representa o estado do agente."""
messages: Annotated[list, operator.add]
# Outros campos de estado aqui
def create_langgraph_app():
# Exemplo de grafo simples, substitua pela sua lógica
workflow = StateGraph(AgentState)
def agent_node(state):
logger.info(f"Executando agent_node com estado: {state}")
# Lógica do seu agente aqui
return {"messages": ["Agent response!"]}
workflow.add_node("agent", agent_node)
workflow.set_entry_point("agent")
workflow.add_edge("agent", END) # Ou para outro nó
checkpointer = get_redis_saver() # Obter o RedisSaver
app = workflow.compile(checkpointer=checkpointer)
logger.info("LangGraph app compilado com RedisSaver.")
return app
# Exemplo de uso
if __name__ == "__main__":
from pprint import pprint
async def run_app_example():
app = create_langgraph_app()
# Usar um thread_id configurável para a persistência
# Cada interação com um thread_id diferente cria/carrega um checkpoint diferente
config = {"configurable": {"thread_id": "test_session_123"}}
# Invocação inicial
inputs = {"messages": ["Olá, como posso ajudar?"]}
response = await app.ainvoke(inputs, config=config)
logger.info("Primeira invocação:")
pprint(response)
# Invocação subsequente com o mesmo thread_id deve retomar o estado
inputs_2 = {"messages": ["Qual é o status do meu pedido?"]}
response_2 = await app.ainvoke(inputs_2, config=config)
logger.info("Segunda invocação (mesmo thread_id):")
pprint(response_2)
# Note que o RedisSaver salva o estado em 'messages', então a segunda
# invocação terá as mensagens da primeira + as da segunda.
asyncio.run(run_app_example())
- Step 2: Commit
Task 7: Criar uma funcionalidade básica de persistência para MongoDB
Essa tarefa visa criar uma "ferramenta" (função) que o LangGraph possa chamar para salvar dados específicos no MongoDB. Isso simula a ação do agente de "persistir o agendamento concluído".
Files:
- Create: src/amon_claw/application/use_cases/appointment_persistence.py
- Modify: src/amon_claw/main.py (para incluir e usar essa ferramenta)
- Step 1: Criar o arquivo
appointment_persistence.py
# src/amon_claw/application/use_cases/appointment_persistence.py
from amon_claw.infrastructure.database.mongodb.client import get_mongo_db
from loguru import logger
from typing import Any
import datetime
async def save_appointment_to_db(appointment_data: dict[str, Any]) -> dict[str, Any]:
"""
Salva dados de um agendamento concluído ou importante no MongoDB.
Esta função seria chamada por uma 'ferramenta' do LangGraph.
"""
db = get_mongo_db()
appointments_collection = db.appointments # Nome da coleção
# Adiciona um timestamp para registro
appointment_data["created_at"] = datetime.datetime.now(datetime.timezone.utc)
try:
result = await appointments_collection.insert_one(appointment_data)
logger.info(f"Agendamento salvo no MongoDB com ID: {result.inserted_id}")
return {"status": "success", "inserted_id": str(result.inserted_id)}
except Exception as e:
logger.error(f"Erro ao salvar agendamento no MongoDB: {e}")
return {"status": "error", "message": str(e)}
# Exemplo de como um tool poderia ser definido (não é a implementação real da ferramenta LangGraph aqui)
# from langchain_core.tools import tool
# @tool
# async def persist_final_appointment_details(data: dict[str, Any]) -> str:
# """Persiste os detalhes finais de um agendamento no banco de dados."""
# response = await save_appointment_to_db(data)
# return str(response)
- Step 2: Modificar
main.pypara integrar a funcionalidade de persistência (exemplo com uma "tool" fictícia)
# src/amon_claw/main.py
# ... (outros imports)
from langgraph.graph import StateGraph, END
from amon_claw.infrastructure.database.redis.client import get_redis_saver
from amon_claw.application.use_cases.appointment_persistence import save_appointment_to_db # Novo import
from loguru import logger
from typing import TypedDict, Annotated
import operator
import asyncio # Para rodar o exemplo async
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
appointment_details: dict # Novo campo para detalhes do agendamento
# Define um nó que simula a decisão de salvar no MongoDB
async def save_to_mongo_node(state: AgentState):
logger.info(f"Executando save_to_mongo_node. Estado: {state}")
if state.get("appointment_details"):
# Chamar a função de persistência
result = await save_appointment_to_db(state["appointment_details"])
logger.info(f"Resultado da persistência no MongoDB: {result}")
return {"messages": [f"Agendamento persistido. Status: {result.get('status')}"]}
return {"messages": ["Nenhum detalhe de agendamento para persistir."]}
def create_langgraph_app():
workflow = StateGraph(AgentState)
def agent_node(state):
logger.info(f"Executando agent_node com estado: {state}")
# Simula o agente identificando um agendamento e adicionando detalhes ao estado
current_messages = state.get("messages", [])
if any("marcar corte" in msg for msg in current_messages):
logger.info("Agente identificou intenção de marcar corte.")
# Simula a extração de detalhes do agendamento, incluindo a "prioridade"
appointment_details = {
"service": "Corte de Cabelo",
"date": "2026-04-25",
"time": "10:00",
"priority": True, # A informação temporária de prioridade
"client_name": "Amon"
}
return {"messages": ["Ok, vamos marcar seu corte!"], "appointment_details": appointment_details}
return {"messages": ["Agent response!"]}
workflow.add_node("agent", agent_node)
workflow.add_node("persist_appointment", save_to_mongo_node) # Adiciona o novo nó
workflow.set_entry_point("agent")
workflow.add_edge("agent", "persist_appointment") # Agente -> Persistir
workflow.add_edge("persist_appointment", END) # Persistir -> Fim
checkpointer = get_redis_saver()
app = workflow.compile(checkpointer=checkpointer)
logger.info("LangGraph app compilado com RedisSaver e persistência MongoDB.")
return app
# Exemplo de uso
if __name__ == "__main__":
from pprint import pprint
async def run_app_example():
app = create_langgraph_app()
config = {"configurable": {"thread_id": "test_session_mongo"}}
# Simula uma conversa onde um agendamento é marcado e persistido
inputs = {"messages": ["Quero marcar um corte de cabelo com prioridade!"]}
response = await app.ainvoke(inputs, config=config)
logger.info("Primeira invocação (agendamento):")
pprint(response)
# Invocação subsequente com o mesmo thread_id
inputs_2 = {"messages": ["Obrigado!"]}
response_2 = await app.ainvoke(inputs_2, config=config)
logger.info("Segunda invocação (mesmo thread_id):")
pprint(response_2)
asyncio.run(run_app_example())
- Step 3: Commit
git add src/amon_claw/application/use_cases/appointment_persistence.py src/amon_claw/main.py
git commit -m "feat: Adiciona funcionalidade de persistência MongoDB e integra ao LangGraph"
Task 8: Adicionar testes para o Cliente Redis e RedisSaver
Files:
- Create: tests/infrastructure/database/redis/test_client.py
- Step 1: Criar o arquivo
test_client.pypara o Redis
# tests/infrastructure/database/redis/test_client.py
import pytest
from redis import Redis
from langgraph.checkpoint.redis import RedisSaver
from amon_claw.infrastructure.database.redis.client import get_redis_client, get_redis_saver
from amon_claw.core.config import settings
# Usar um fixture para garantir um cliente Redis limpo para cada teste
@pytest.fixture(name="redis_client", scope="function")
def redis_client_fixture():
# Assegura que o cliente Redis singleton seja redefinido para cada teste
# para evitar interferência entre testes.
# Em um ambiente de teste real, você pode querer um Redis de teste separado
# ou mockar a conexão. Aqui, estamos usando a conexão real definida em settings.
client = Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, decode_responses=True)
client.flushdb() # Limpa o banco de dados Redis para cada teste
yield client
client.flushdb()
client.close()
def test_get_redis_client(redis_client: Redis):
"""Testa se o cliente Redis é obtido e está funcionando."""
assert isinstance(redis_client, Redis)
assert redis_client.ping()
def test_get_redis_saver(redis_client: Redis):
"""Testa se o RedisSaver é obtido e usa o cliente Redis correto."""
saver = get_redis_saver()
assert isinstance(saver, RedisSaver)
# Verifica se o cliente interno do saver é o que esperamos
assert saver.red.connection_pool.connection_kwargs['host'] == settings.REDIS_HOST
assert saver.red.connection_pool.connection_kwargs['port'] == settings.REDIS_PORT
def test_redis_saver_checkpoint_functionality(redis_client: Redis):
"""Testa a funcionalidade básica de checkpoint do RedisSaver."""
saver = get_redis_saver()
thread_id = "test_thread_123"
config = {"configurable": {"thread_id": thread_id}}
# Salva um estado inicial
initial_state = {"messages": [{"role": "user", "content": "Hello!"}], "steps": 1}
saver.put(config, initial_state)
# Verifica se o estado foi salvo
retrieved_state = saver.get(config)
assert retrieved_state is not None
assert retrieved_state["checkpoint"]["payload"] == initial_state
# Salva um novo estado (simulando uma atualização)
updated_state = {"messages": [{"role": "assistant", "content": "Hi there!"}], "steps": 2}
saver.put(config, updated_state)
# Verifica se o estado atualizado foi salvo
retrieved_updated_state = saver.get(config)
assert retrieved_updated_state is not None
assert retrieved_updated_state["checkpoint"]["payload"] == updated_state
# Verifica se a chave existe no Redis
assert redis_client.exists(f"langgraph_checkpoint_{thread_id}")
# Testar se um thread_id inexistente retorna None
non_existent_config = {"configurable": {"thread_id": "non_existent"}}
assert saver.get(non_existent_config) is None
- Step 2: Rodar os testes para verificar o comportamento
Run: pytest tests/infrastructure/database/redis/test_client.py
Expected: PASS
- Step 3: Commit
git add tests/infrastructure/database/redis/test_client.py
git commit -m "test: Adiciona testes para cliente Redis e RedisSaver"
Task 9: Adicionar testes para o Cliente MongoDB
Files:
- Create: tests/infrastructure/database/mongodb/test_client.py
- Step 1: Criar o arquivo
test_client.pypara o MongoDB
# tests/infrastructure/database/mongodb/test_client.py
import pytest
from motor.motor_asyncio import AsyncIOMotorClient
from amon_claw.infrastructure.database.mongodb.client import get_mongo_client, get_mongo_db
from amon_claw.core.config import settings
import asyncio
# Fixture para garantir um cliente MongoDB assíncrono para cada teste
@pytest.fixture(name="mongo_client", scope="function")
async def mongo_client_fixture():
# Assegura que o cliente MongoDB singleton seja redefinido para cada teste
client = AsyncIOMotorClient(settings.MONGO_URI)
# Limpa o banco de dados de teste antes de cada teste
await client[settings.MONGO_DB_NAME].command("dropDatabase")
yield client
# Limpa o banco de dados de teste depois de cada teste
await client[settings.MONGO_DB_NAME].command("dropDatabase")
client.close()
@pytest.mark.asyncio
async def test_get_mongo_client(mongo_client: AsyncIOMotorClient):
"""Testa se o cliente MongoDB assíncrono é obtido e está funcionando."""
assert isinstance(mongo_client, AsyncIOMotorClient)
# Tenta pingar o servidor para verificar a conexão
result = await mongo_client.admin.command('ping')
assert result['ok'] == 1.0
@pytest.mark.asyncio
async def test_get_mongo_db(mongo_client: AsyncIOMotorClient):
"""Testa se o banco de dados MongoDB é obtido corretamente."""
db = get_mongo_db()
assert db.name == settings.MONGO_DB_NAME
# Verifica se é uma instância de um banco de dados Motor
assert hasattr(db, 'command')
@pytest.mark.asyncio
async def test_save_appointment_to_db():
"""Testa a funcionalidade de salvar agendamentos no MongoDB."""
from amon_claw.application.use_cases.appointment_persistence import save_appointment_to_db
db = get_mongo_db()
appointments_collection = db.appointments
test_appointment = {
"service": "Manicure",
"date": "2026-05-01",
"time": "14:00",
"client_name": "Maria",
"priority": False
}
result = await save_appointment_to_db(test_appointment)
assert result["status"] == "success"
assert "inserted_id" in result
# Verifica se o documento foi realmente inserido
inserted_doc = await appointments_collection.find_one({"_id": result["inserted_id"]})
assert inserted_doc is not None
assert inserted_doc["service"] == "Manicure"
assert "created_at" in inserted_doc
# Limpa o documento inserido após o teste
await appointments_collection.delete_one({"_id": result["inserted_id"]})
- Step 2: Rodar os testes para verificar o comportamento
Run: pytest tests/infrastructure/database/mongodb/test_client.py
Expected: PASS
- Step 3: Commit
git add tests/infrastructure/database/mongodb/test_client.py
git commit -m "test: Adiciona testes para cliente MongoDB e funcionalidade de persistência"