feat: Implement MemorySaver
This commit is contained in:
@@ -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."""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user