feat: add customer phone to agent state on new conversations
This commit is contained in:
@@ -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?
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user