diff --git a/src/naliiabot/bot/agent/agent.py b/src/naliiabot/bot/agent/agent.py index 2b997a5..d65e4e5 100644 --- a/src/naliiabot/bot/agent/agent.py +++ b/src/naliiabot/bot/agent/agent.py @@ -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.""" diff --git a/tests/conftest.py b/tests/conftest.py index 7514e1e..0034a9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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" + } \ No newline at end of file diff --git a/tests/test_webhook.py b/tests/test_webhook.py index c461192..055b0ec 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -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?"} \ No newline at end of file + assert response.json().get("reply") == "Respuesta mock" \ No newline at end of file