From 4b2cf1e568b95c761171d1e4fb3da1ca9482b5c5 Mon Sep 17 00:00:00 2001 From: aserrador Date: Mon, 9 Mar 2026 21:10:31 -0500 Subject: [PATCH] feat: add customer phone to agent state on new conversations --- src/naliiabot/bot/agent/agent.py | 51 +++++++------------ src/naliiabot/bot/agent/schemas.py | 1 + src/naliiabotapi/api/v1/webhooks/chat_hook.py | 48 ++++++++--------- 3 files changed, 40 insertions(+), 60 deletions(-) diff --git a/src/naliiabot/bot/agent/agent.py b/src/naliiabot/bot/agent/agent.py index 33d4d04..1d3c55d 100644 --- a/src/naliiabot/bot/agent/agent.py +++ b/src/naliiabot/bot/agent/agent.py @@ -11,6 +11,7 @@ from dataclasses import dataclass @dataclass class AgentConfig: """Configuración inmutable del agente para fácil testing.""" + system_prompt: str = ( "helpful assistant that can call tools when needed." " Always respond with a message." @@ -30,7 +31,7 @@ class Agent: model: Any, config: AgentConfig | None = None, checkpointer: Union[BaseCheckpointSaver, None] = None, - tools: list | None = None + tools: list | None = None, ): """ Inicializa el agente. @@ -48,9 +49,7 @@ class Agent: self._tools = tools or [] self._checkpointer = checkpointer or InMemorySaver() - self._tools_by_name: dict[str, Any] = { - tool.name: tool for tool in self._tools - } + self._tools_by_name: dict[str, Any] = {tool.name: tool for tool in self._tools} self._compiled_agent = self._compile_agent() self._pre_llm_hook: Callable | None = None @@ -66,14 +65,17 @@ class Agent: """Retorna configuración actual.""" return self._config + @property + def checkpointer(self): + """Retorna el checkpointer para acceso al estado.""" + return self._checkpointer + @property def graph(self) -> StateGraph: """Expone el grafo para inspección en tests.""" return self._build_agent() - async def ainvoke( - self, state: dict, config: RunnableConfig | None = None - ) -> dict: + async def ainvoke(self, state: dict, config: RunnableConfig | None = None) -> dict: """ Versión asíncrona de invoke, necesaria para checkpointers de DB. """ @@ -86,9 +88,7 @@ class Agent: # Llamamos al método ainvoke del grafo compilado return await self._compiled_agent.ainvoke(state, config=config) - def invoke( - self, state: dict, config: RunnableConfig | None = None - ) -> dict: + def invoke(self, state: dict, config: RunnableConfig | None = None) -> dict: """ Ejecuta el agente con el estado inicial. @@ -139,12 +139,7 @@ class Agent: agent_builder.add_edge(START, "llm_call") agent_builder.add_conditional_edges( - "llm_call", - self._should_continue, - { - "tool_node": "tool_node", - END: END - } + "llm_call", self._should_continue, {"tool_node": "tool_node", END: END} ) agent_builder.add_edge("tool_node", "llm_call") @@ -161,9 +156,9 @@ class Agent: if self._pre_llm_hook: self._pre_llm_hook(state) - messages = [ - SystemMessage(content=self._config.system_prompt) - ] + state["messages"] + messages = [SystemMessage(content=self._config.system_prompt)] + state[ + "messages" + ] model_with_tools = self._model.bind_tools(self._tools) @@ -177,10 +172,7 @@ class Agent: f"Max iterations ({self._config.max_iterations}) exceeded" ) - return { - "messages": state["messages"] + [response], - "llm_calls": new_calls - } + return {"messages": state["messages"] + [response], "llm_calls": new_calls} def _tool_node(self, state: MessagesState) -> dict: """ @@ -200,9 +192,7 @@ class Agent: if self._post_tool_hook: self._post_tool_hook(state, result) - return { - "messages": state["messages"] + tool_results - } + return {"messages": state["messages"] + tool_results} def _execute_tool(self, tool_call: dict) -> ToolMessage: """ @@ -224,9 +214,8 @@ class Agent: observation = tool.invoke(tool_args) return ToolMessage( - content=str(observation), - tool_call_id=tool_id, - name=tool_name) + content=str(observation), tool_call_id=tool_id, name=tool_name + ) except Exception as e: return ToolMessage( @@ -235,9 +224,7 @@ class Agent: name=tool_name, ) - def _should_continue( - self, state: MessagesState - ) -> Literal["tool_node", END]: + def _should_continue(self, state: MessagesState) -> Literal["tool_node", END]: """ Condición de transición: ¿El LLM solicitó herramientas? """ diff --git a/src/naliiabot/bot/agent/schemas.py b/src/naliiabot/bot/agent/schemas.py index 0fa63ec..f718204 100644 --- a/src/naliiabot/bot/agent/schemas.py +++ b/src/naliiabot/bot/agent/schemas.py @@ -6,3 +6,4 @@ from typing_extensions import TypedDict, Annotated class MessagesState(TypedDict): messages: Annotated[list[AnyMessage], add_messages] llm_calls: int + customer_phone: str diff --git a/src/naliiabotapi/api/v1/webhooks/chat_hook.py b/src/naliiabotapi/api/v1/webhooks/chat_hook.py index c53470e..396e514 100644 --- a/src/naliiabotapi/api/v1/webhooks/chat_hook.py +++ b/src/naliiabotapi/api/v1/webhooks/chat_hook.py @@ -22,25 +22,13 @@ async def send_whatsapp_message(request_data: SendMessageScheme): delay = request_data.get("delay", 1200) endpoint = f"{instance_url}/message/sendText/{instance_name}" - headers = { - "apikey": apikey, - "Content-Type": "application/json" - } + headers = {"apikey": apikey, "Content-Type": "application/json"} - payload = { - "number": jid, - "text": text, - "delay": delay, - "linkPreview": False - } + payload = {"number": jid, "text": text, "delay": delay, "linkPreview": False} async with httpx.AsyncClient() as client: try: - response = await client.post( - endpoint, - json=payload, - headers=headers - ) + response = await client.post(endpoint, json=payload, headers=headers) response.raise_for_status() return response.json() @@ -72,30 +60,34 @@ async def webhook_chat(request: Request, agent=Depends(get_agent)): if not user_message: return {"reply": "No text message provided."} - config = { - "configurable": { - "thread_id": thread_id - } + customer_phone = thread_id.split("@")[0] + + config = {"configurable": {"thread_id": thread_id}} + + initial_state = { + "messages": [HumanMessage(content=user_message)], + "llm_calls": 0, + "customer_phone": "", } - messages = [HumanMessage(content=user_message)] + existing_state = await agent.checkpointer.get(config) + if existing_state is None: + initial_state["customer_phone"] = customer_phone + logger.info(f"Nueva conversación iniciada con cliente: {customer_phone}") - agent_response = await agent.ainvoke( - {"messages": messages}, config=config - ) + agent_response = await agent.ainvoke(initial_state, config=config) agent_response_content = agent_response["messages"][-1].content - clean_jid = thread_id.split('@')[0] - await send_whatsapp_message( SendMessageScheme( instance_url=server_url, instance_name=instance_name, apikey=api_key, - jid=clean_jid, + jid=customer_phone, text=agent_response_content, - delay=1200 - )) + delay=1200, + ) + ) except Exception as e: logger.error(f"Error processing webhook: {e}")