feat: Add NaliiaTools

This commit is contained in:
2026-02-16 16:35:15 -05:00
parent 63758f5f9c
commit 66fe0a2a13
7 changed files with 100 additions and 34 deletions

View File

@@ -1,5 +1,6 @@
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langchain_core.messages import SystemMessage, ToolMessage, BaseMessage from langchain_core.messages import SystemMessage, ToolMessage, BaseMessage
import logging
from typing import Literal, Callable, Any from typing import Literal, Callable, Any
from .schemas import MessagesState from .schemas import MessagesState
from dataclasses import dataclass from dataclasses import dataclass
@@ -38,10 +39,9 @@ class Agent:
- Si es None: sin herramientas - Si es None: sin herramientas
- Si es list: lista de herramientas - Si es list: lista de herramientas
""" """
self._model = model self._model = model
self._config = config or AgentConfig() self._config = config or AgentConfig()
# Normalizar tools: usar la lista proporcionada o lista vacía
self._tools = tools or [] self._tools = tools or []
self._tools_by_name: dict[str, Any] = { self._tools_by_name: dict[str, Any] = {
@@ -49,7 +49,6 @@ class Agent:
} }
self._compiled_agent = self._compile_agent() self._compiled_agent = self._compile_agent()
# Hooks para testing (monkey-patching friendly)
self._pre_llm_hook: Callable | None = None self._pre_llm_hook: Callable | None = None
self._post_tool_hook: Callable | None = None self._post_tool_hook: Callable | None = None
@@ -92,26 +91,28 @@ class Agent:
def set_pre_llm_hook(self, hook: Callable[[dict], None]): def set_pre_llm_hook(self, hook: Callable[[dict], None]):
"""Registra hook pre-llm para testing.""" """Registra hook pre-llm para testing."""
self._pre_llm_hook = hook self._pre_llm_hook = hook
def set_post_tool_hook(self, hook: Callable[[dict, Any], None]): def set_post_tool_hook(self, hook: Callable[[dict, Any], None]):
"""Registra hook post-tool para testing.""" """Registra hook post-tool para testing."""
self._post_tool_hook = hook self._post_tool_hook = hook
def _compile_agent(self): def _compile_agent(self):
"""Compila el grafo una sola vez.""" """Compila el grafo una sola vez."""
agent = self._build_agent() agent = self._build_agent()
return agent.compile() return agent.compile()
def _build_agent(self) -> StateGraph: def _build_agent(self) -> StateGraph:
"""Construye el grafo de estado completo.""" """Construye el grafo de estado completo."""
agent_builder = StateGraph(MessagesState) agent_builder = StateGraph(MessagesState)
# Nodos
agent_builder.add_node("llm_call", self._llm_call_node) agent_builder.add_node("llm_call", self._llm_call_node)
agent_builder.add_node("tool_node", self._tool_node) agent_builder.add_node("tool_node", self._tool_node)
# Flujo
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",
@@ -125,8 +126,6 @@ class Agent:
return agent_builder return agent_builder
# ============ Nodos del Grafo ============
def _llm_call_node(self, state: MessagesState) -> dict: def _llm_call_node(self, state: MessagesState) -> dict:
""" """
Nodo LLM: Decide siguiente acción o responde. Nodo LLM: Decide siguiente acción o responde.
@@ -143,6 +142,7 @@ class Agent:
] + state["messages"] ] + state["messages"]
model_with_tools = self._model.bind_tools(self._tools) model_with_tools = self._model.bind_tools(self._tools)
response = model_with_tools.invoke(messages) response = model_with_tools.invoke(messages)
current_calls = state.get("llm_calls", 0) current_calls = state.get("llm_calls", 0)
@@ -185,29 +185,30 @@ class Agent:
Ejecuta una herramienta individual con manejo de errores. Ejecuta una herramienta individual con manejo de errores.
""" """
tool_name = tool_call.get("name") tool_name = tool_call.get("name")
tool_id = tool_call.get("id", "unknown") tool_id = tool_call.get("id")
tool_args = tool_call.get("args", {}) tool_args = tool_call.get("args", {})
if tool_name not in self._tools_by_name: if tool_name not in self._tools_by_name:
return ToolMessage( return ToolMessage(
content=f"Error: Tool '{tool_name}' not found", content=f"Error: Tool '{tool_name}' not found",
tool_call_id=tool_id, tool_call_id=tool_id,
status="error" name=tool_name,
) )
try: try:
tool = self._tools_by_name[tool_name] tool = self._tools_by_name[tool_name]
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, tool_call_id=tool_id,
status="success" name=tool_name)
)
except Exception as e: except Exception as e:
return ToolMessage( return ToolMessage(
content=f"Error executing tool: {str(e)}", content=f"Error executing tool: {str(e)}",
tool_call_id=tool_id, tool_call_id=tool_id,
status="error" name=tool_name,
) )
def _should_continue( def _should_continue(

View File

@@ -1,8 +1,8 @@
from langchain.messages import AnyMessage from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict, Annotated from typing_extensions import TypedDict, Annotated
import operator
class MessagesState(TypedDict): class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add] messages: Annotated[list[AnyMessage], add_messages]
llm_calls: int llm_calls: int

View File

@@ -35,7 +35,9 @@ class LLMFactory:
def _create_anthropic(self): def _create_anthropic(self):
model_name = "claude-sonnet-4-5-20250929" model_name = "claude-sonnet-4-5-20250929"
model = ChatAnthropic(model=model_name) model = ChatAnthropic(
model=model_name
)
return model return model

View File

@@ -1,6 +1,8 @@
prompts: prompts:
NALIIA_PROMPT: | NALIIA_PROMPT: |
"""Eres Naliia, asistente virtual de un centro de belleza. """
Eres Naliia, asistente virtual de un centro de belleza.
Responde usando formato Markdown: Responde usando formato Markdown:
- Usa **negrita** para énfasis. - Usa **negrita** para énfasis.
- Separa opciones con viñetas o números. - Separa opciones con viñetas o números.

View File

@@ -1,13 +1,67 @@
from langchain.tools import tool from langchain.tools import tool
from ..logger import logger
class NaliiaTools: class NaliiaTools:
def get_tools(self):
return [
self.verificar_usuario_registrado,
self.registrar_usuario_nuevo,
self.consultar_sedes,
self.consultar_agenda_disponible,
self.agendar_cita
]
@tool @tool
def verificar_usuario_registrado(self): def verificar_usuario_registrado(phone: str) -> bool:
""" """
Verifica si el usuario que esta contactandose es un Verifica si el usuario esta registrado.
usuario ya conocido y registrado en la base de datos.
Args:
phone: Numero de contacto del cliente ejemplo 30123334
""" """
logger.info("Llamando a Verificar usuario.")
return False
@tool
def registrar_usuario_nuevo(full_name: str, phone: str) -> bool:
"""
En caso de que no se haya podido verificar el usuario, sera necesario
registrarlo como un usuario nuevo.
Args:
full_name: Nombre completo del cliente.
phone: Numero de contacto del cliente.
"""
logger.info("Llamando a Registrar Nuevo Usuario.")
return True
@tool
def consultar_sedes() -> list:
"""
Brinda informacion al usuario acerca de las sedes disponibles y sus horarios.
"""
logger.info("Llamando a consultar sedes.")
return []
@tool
def consultar_agenda_disponible() -> list:
"""
Ayuda a consultar al cliente los horarios de atencion disponibles para agendar su cita.
"""
logger.info("Llamando a consultar agenda disponible.")
return []
@tool
def agendar_cita() -> bool:
"""
Ayuda al cliente a confirmar su cita.
"""
logger.info("Llamando a agendar cita.")
return True return True

View File

@@ -4,6 +4,7 @@ from src.naliiabot.bot.agent.agent import Agent, AgentConfig
from src.naliiabot.bot.tools.naliia_tools import NaliiaTools from src.naliiabot.bot.tools.naliia_tools import NaliiaTools
from src.naliiabot.bot.factories.llm_factory import LLMFactory from src.naliiabot.bot.factories.llm_factory import LLMFactory
from src.naliiabot.bot.prompts.load_prompt import get_prompt_template from src.naliiabot.bot.prompts.load_prompt import get_prompt_template
from ..logger import logger
MODEL_NAME = os.getenv("LLM_MODEL", "anthropic") MODEL_NAME = os.getenv("LLM_MODEL", "anthropic")
@@ -17,12 +18,11 @@ def get_agent():
NALIIA_PROMPT = get_prompt_template( NALIIA_PROMPT = get_prompt_template(
"NALIIA_PROMPT") "NALIIA_PROMPT")
naliia_tools = [ naliia_tools = NaliiaTools().get_tools()
t for t in dir(NaliiaTools()) if not t.startswith('__') and not t.endswith('__')
]
config = AgentConfig(system_prompt=NALIIA_PROMPT) config = AgentConfig(system_prompt=NALIIA_PROMPT)
model = LLMFactory(MODEL_NAME).get_model() model = LLMFactory(MODEL_NAME).get_model()
raise Exception(naliia_tools) logger.info(
f"Agente Naliia ha iniciado con las Tools: {naliia_tools}")
return Agent(model=model, config=config, tools=naliia_tools) return Agent(model=model, config=config, tools=naliia_tools)

View File

@@ -1,18 +1,12 @@
import streamlit as st import streamlit as st
from agent_client import AgentClient from agent_client import AgentClient
from logger import logger from logger import logger
import json
import httpx
import random import random
import time import time
def response_generator(response: str = ""):
logger.info(f"Generating response stream for: {response}")
if response:
for word in response.split():
yield word + " "
time.sleep(0.05)
async def main(): async def main():
st.image("src/ui/assets/logo-naliia.png", width=100, caption="NaliiaBot") st.image("src/ui/assets/logo-naliia.png", width=100, caption="NaliiaBot")
@@ -32,10 +26,23 @@ async def main():
agent_client = AgentClient() agent_client = AgentClient()
response = await agent_client.send_message(prompt) response = await agent_client.send_message(prompt)
try:
response.raise_for_status()
data = response.json()
except json.JSONDecodeError:
raise RuntimeError(
f"Respuesta inválida (no JSON): {response.text}"
)
except httpx.HTTPStatusError as e:
raise RuntimeError(
f"Error HTTP {e.response.status_code}: {e.response.text}"
)
raw_response = response.json()['response']['messages'][-1]['content'] raw_response = response.json()['response']['messages'][-1]['content']
with st.chat_message("assistant"): with st.chat_message("assistant"):
formated_response = st.markdown(raw_response) formated_response = st.markdown(raw_response)
st.session_state.messages.append({"role": "assistant", "content": formated_response}) st.session_state.messages.append({"role": "assistant", "content": raw_response})
if __name__ == "__main__": import asyncio; asyncio.run(main()) if __name__ == "__main__": import asyncio; asyncio.run(main())