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

View File

@@ -66,3 +66,108 @@ def anthropic_model():
anthropic_model = llmFactory.get_model() anthropic_model = llmFactory.get_model()
return anthropic_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 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: class TestWebhookChatBot:
"""Tests for WebhookChatBot.""" """Tests for WebhookChatBot."""
@pytest.mark.anyio @pytest.mark.anyio
async def tests_webhook_success(self, async_client): async def tests_webhook_success(self, send_message_payload):
"""Test that the webhook endpoint returns a successful response.""" """Test that the webhook endpoint returns a successful response.
response = await async_client.post("/webhook", json={"message": "Hello"})
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.status_code == 200
assert response.json() == {"reply": "Hello, how can I assist you?"} assert response.json().get("reply") == "Respuesta mock"