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
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?
"""

View File

@@ -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

View File

@@ -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}")