feat: Implement MemorySaver

This commit is contained in:
2026-02-27 00:47:16 -05:00
parent 4b2d02eca8
commit da2c2a1528
3 changed files with 144 additions and 7 deletions

View File

@@ -1,9 +1,12 @@
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import SystemMessage, ToolMessage, BaseMessage
import logging
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig
from typing import Literal, Callable, Any
from .schemas import MessagesState
from dataclasses import dataclass
import logging
@dataclass
@@ -27,6 +30,7 @@ class Agent:
self,
model: Any,
config: AgentConfig | None = None,
checkpointer: InMemorySaver | None = None,
tools: list | None = None
):
"""
@@ -43,6 +47,7 @@ class Agent:
self._model = model
self._config = config or AgentConfig()
self._tools = tools or []
self._checkpointer = checkpointer or InMemorySaver()
self._tools_by_name: dict[str, Any] = {
tool.name: tool for tool in self._tools
@@ -67,7 +72,7 @@ class Agent:
"""Expone el grafo para inspección en tests."""
return self._build_agent()
def invoke(self, state: dict, config: dict | None = None) -> dict:
def invoke(self, state: dict, config: RunnableConfig | None = None) -> dict:
"""
Ejecuta el agente con el estado inicial.
@@ -78,6 +83,9 @@ class Agent:
Estado final después de la ejecución
"""
if not config:
config : RunnableConfig = {"configurable": {"thread_id": "1"}}
if "messages" not in state:
raise ValueError("State must contain 'messages' key")
@@ -104,7 +112,7 @@ class Agent:
agent = self._build_agent()
return agent.compile()
return agent.compile(checkpointer=self._checkpointer)
def _build_agent(self) -> StateGraph:
"""Construye el grafo de estado completo."""

View File

@@ -66,3 +66,108 @@ def anthropic_model():
anthropic_model = llmFactory.get_model()
return anthropic_model
@pytest.fixture
def send_message_payload():
"""Payload de ejemplo para enviar mensajes al webhook."""
return {
"event": "messages.upsert",
"instance": "ALEJANDRO AYALA",
"data": {
"key": {
"remoteJid": "5512992544490@s.whatsapp.net",
"remoteJidAlt": "5512992544490@s.whatsapp.net",
"fromMe": False, "id": "AC4367597BDC50537133B9C8848B2E62",
"participant": "", "addressingMode": "lid"
},
"pushName": "Alejandro",
"status": "DELIVERY_ACK",
"message": {
"conversation": "Hola",
"messageContextInfo": {
"threadId": [],
"deviceListMetadata": {
"senderKeyIndexes": [],
"recipientKeyIndexes": [],
"senderKeyHash": {
"0": 154,
"1": 106,
"2": 69,
"3": 189,
"4": 90,
"5": 105,
"6": 172,
"7": 243,
"8": 27,
"9": 98
},
"senderTimestamp": {
"low": 1772156772,
"high": 0,
"unsigned": "true"
},
"recipientKeyHash": {
"0": 16,
"1": 98,
"2": 75,
"3": 100,
"4": 131,
"5": 43,
"6": 24,
"7": 225,
"8": 244,
"9": 126
},
"recipientTimestamp": {
"low": 1772155597,
"high": 0,
"unsigned": True
}
},
"deviceListMetadataVersion": 2,
"messageSecret": {
"0": 100,
"1": 47,
"2": 112,
"3": 133,
"4": 255,
"5": 156,
"6": 129,
"7": 57,
"8": 239,
"9": 219,
"10": 74,
"11": 171,
"12": 212,
"13": 150,
"14": 112,
"15": 83,
"16": 123,
"17": 78,
"18": 189,
"19": 13,
"20": 51,
"21": 133,
"22": 129,
"23": 6,
"24": 197,
"25": 119,
"26": 197,
"27": 56,
"28": 87,
"29": 176,
"30": 135,
"31": 62
}}},
"messageType": "conversation",
"messageTimestamp": 1772168493,
"instanceId": "75a877f1-d772-422b-a6f1-6ac470af5663",
"source": "android"
},
"destination": "http://192.168.58.110:8010/webhook",
"date_time": "2026-02-27T02:01:34.178Z",
"sender": "573016859278@s.whatsapp.net",
"server_url": "http://localhost:8080",
"apikey": "98B6B820AFC3-4544-9A4C-011B0719C2D1"
}

View File

@@ -1,12 +1,36 @@
import pytest
from types import SimpleNamespace
from httpx import ASGITransport, AsyncClient
from naliiabotapi.main import app
from naliiabotapi.api.dependencies import get_agent
class TestWebhookChatBot:
"""Tests for WebhookChatBot."""
@pytest.mark.anyio
async def tests_webhook_success(self, async_client):
"""Test that the webhook endpoint returns a successful response."""
response = await async_client.post("/webhook", json={"message": "Hello"})
async def tests_webhook_success(self, send_message_payload):
"""Test that the webhook endpoint returns a successful response.
Se crea un `mock_agent` con `invoke` mockeado y se inyecta
mediante `app.dependency_overrides` antes de realizar la petición.
"""
# Crear mock del agente y su invoke
mock_agent = SimpleNamespace()
def mock_invoke(state, config=None):
return {"messages": [SimpleNamespace(content="Respuesta mock")]}
mock_agent.invoke = mock_invoke
# Sobrescribir la dependencia del agente en la app
app.dependency_overrides[get_agent] = lambda: mock_agent
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://localhost:8010") as client:
response = await client.post("/webhook", json=send_message_payload)
# Limpiar override para no afectar a otros tests
app.dependency_overrides.pop(get_agent, None)
assert response.status_code == 200
assert response.json() == {"reply": "Hello, how can I assist you?"}
assert response.json().get("reply") == "Respuesta mock"