feat: add customer phone to agent state on new conversations

This commit is contained in:
2026-03-09 21:10:31 -05:00
parent f78908ad88
commit 4b2cf1e568
3 changed files with 40 additions and 60 deletions

View File

@@ -11,6 +11,7 @@ from dataclasses import dataclass
@dataclass @dataclass
class AgentConfig: class AgentConfig:
"""Configuración inmutable del agente para fácil testing.""" """Configuración inmutable del agente para fácil testing."""
system_prompt: str = ( system_prompt: str = (
"helpful assistant that can call tools when needed." "helpful assistant that can call tools when needed."
" Always respond with a message." " Always respond with a message."
@@ -30,7 +31,7 @@ class Agent:
model: Any, model: Any,
config: AgentConfig | None = None, config: AgentConfig | None = None,
checkpointer: Union[BaseCheckpointSaver, None] = None, checkpointer: Union[BaseCheckpointSaver, None] = None,
tools: list | None = None tools: list | None = None,
): ):
""" """
Inicializa el agente. Inicializa el agente.
@@ -48,9 +49,7 @@ class Agent:
self._tools = tools or [] self._tools = tools or []
self._checkpointer = checkpointer or InMemorySaver() 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
}
self._compiled_agent = self._compile_agent() self._compiled_agent = self._compile_agent()
self._pre_llm_hook: Callable | None = None self._pre_llm_hook: Callable | None = None
@@ -66,14 +65,17 @@ class Agent:
"""Retorna configuración actual.""" """Retorna configuración actual."""
return self._config return self._config
@property
def checkpointer(self):
"""Retorna el checkpointer para acceso al estado."""
return self._checkpointer
@property @property
def graph(self) -> StateGraph: def graph(self) -> StateGraph:
"""Expone el grafo para inspección en tests.""" """Expone el grafo para inspección en tests."""
return self._build_agent() return self._build_agent()
async def ainvoke( async def ainvoke(self, state: dict, config: RunnableConfig | None = None) -> dict:
self, state: dict, config: RunnableConfig | None = None
) -> dict:
""" """
Versión asíncrona de invoke, necesaria para checkpointers de DB. 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 # Llamamos al método ainvoke del grafo compilado
return await self._compiled_agent.ainvoke(state, config=config) return await self._compiled_agent.ainvoke(state, config=config)
def invoke( def invoke(self, state: dict, config: RunnableConfig | None = None) -> dict:
self, state: dict, config: RunnableConfig | None = None
) -> dict:
""" """
Ejecuta el agente con el estado inicial. Ejecuta el agente con el estado inicial.
@@ -139,12 +139,7 @@ class Agent:
agent_builder.add_edge(START, "llm_call") agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges( agent_builder.add_conditional_edges(
"llm_call", "llm_call", self._should_continue, {"tool_node": "tool_node", END: END}
self._should_continue,
{
"tool_node": "tool_node",
END: END
}
) )
agent_builder.add_edge("tool_node", "llm_call") agent_builder.add_edge("tool_node", "llm_call")
@@ -161,9 +156,9 @@ class Agent:
if self._pre_llm_hook: if self._pre_llm_hook:
self._pre_llm_hook(state) self._pre_llm_hook(state)
messages = [ messages = [SystemMessage(content=self._config.system_prompt)] + state[
SystemMessage(content=self._config.system_prompt) "messages"
] + state["messages"] ]
model_with_tools = self._model.bind_tools(self._tools) model_with_tools = self._model.bind_tools(self._tools)
@@ -177,10 +172,7 @@ class Agent:
f"Max iterations ({self._config.max_iterations}) exceeded" f"Max iterations ({self._config.max_iterations}) exceeded"
) )
return { return {"messages": state["messages"] + [response], "llm_calls": new_calls}
"messages": state["messages"] + [response],
"llm_calls": new_calls
}
def _tool_node(self, state: MessagesState) -> dict: def _tool_node(self, state: MessagesState) -> dict:
""" """
@@ -200,9 +192,7 @@ class Agent:
if self._post_tool_hook: if self._post_tool_hook:
self._post_tool_hook(state, result) self._post_tool_hook(state, result)
return { return {"messages": state["messages"] + tool_results}
"messages": state["messages"] + tool_results
}
def _execute_tool(self, tool_call: dict) -> ToolMessage: def _execute_tool(self, tool_call: dict) -> ToolMessage:
""" """
@@ -224,9 +214,8 @@ class Agent:
observation = tool.invoke(tool_args) observation = tool.invoke(tool_args)
return ToolMessage( return ToolMessage(
content=str(observation), content=str(observation), tool_call_id=tool_id, name=tool_name
tool_call_id=tool_id, )
name=tool_name)
except Exception as e: except Exception as e:
return ToolMessage( return ToolMessage(
@@ -235,9 +224,7 @@ class Agent:
name=tool_name, name=tool_name,
) )
def _should_continue( def _should_continue(self, state: MessagesState) -> Literal["tool_node", END]:
self, state: MessagesState
) -> Literal["tool_node", END]:
""" """
Condición de transición: ¿El LLM solicitó herramientas? Condición de transición: ¿El LLM solicitó herramientas?
""" """

View File

@@ -6,3 +6,4 @@ from typing_extensions import TypedDict, Annotated
class MessagesState(TypedDict): class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages] messages: Annotated[list[AnyMessage], add_messages]
llm_calls: int llm_calls: int
customer_phone: str

View File

@@ -22,25 +22,13 @@ async def send_whatsapp_message(request_data: SendMessageScheme):
delay = request_data.get("delay", 1200) delay = request_data.get("delay", 1200)
endpoint = f"{instance_url}/message/sendText/{instance_name}" endpoint = f"{instance_url}/message/sendText/{instance_name}"
headers = { headers = {"apikey": apikey, "Content-Type": "application/json"}
"apikey": apikey,
"Content-Type": "application/json"
}
payload = { payload = {"number": jid, "text": text, "delay": delay, "linkPreview": False}
"number": jid,
"text": text,
"delay": delay,
"linkPreview": False
}
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
response = await client.post( response = await client.post(endpoint, json=payload, headers=headers)
endpoint,
json=payload,
headers=headers
)
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
@@ -72,30 +60,34 @@ async def webhook_chat(request: Request, agent=Depends(get_agent)):
if not user_message: if not user_message:
return {"reply": "No text message provided."} return {"reply": "No text message provided."}
config = { customer_phone = thread_id.split("@")[0]
"configurable": {
"thread_id": thread_id 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( agent_response = await agent.ainvoke(initial_state, config=config)
{"messages": messages}, config=config
)
agent_response_content = agent_response["messages"][-1].content agent_response_content = agent_response["messages"][-1].content
clean_jid = thread_id.split('@')[0]
await send_whatsapp_message( await send_whatsapp_message(
SendMessageScheme( SendMessageScheme(
instance_url=server_url, instance_url=server_url,
instance_name=instance_name, instance_name=instance_name,
apikey=api_key, apikey=api_key,
jid=clean_jid, jid=customer_phone,
text=agent_response_content, text=agent_response_content,
delay=1200 delay=1200,
)) )
)
except Exception as e: except Exception as e:
logger.error(f"Error processing webhook: {e}") logger.error(f"Error processing webhook: {e}")