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

View File

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

View File

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

View File

@@ -1,13 +1,67 @@
from langchain.tools import tool
from ..logger import logger
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
def verificar_usuario_registrado(self):
def verificar_usuario_registrado(phone: str) -> bool:
"""
Verifica si el usuario que esta contactandose es un
usuario ya conocido y registrado en la base de datos.
Verifica si el usuario esta registrado.
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

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.factories.llm_factory import LLMFactory
from src.naliiabot.bot.prompts.load_prompt import get_prompt_template
from ..logger import logger
MODEL_NAME = os.getenv("LLM_MODEL", "anthropic")
@@ -17,12 +18,11 @@ def get_agent():
NALIIA_PROMPT = get_prompt_template(
"NALIIA_PROMPT")
naliia_tools = [
t for t in dir(NaliiaTools()) if not t.startswith('__') and not t.endswith('__')
]
naliia_tools = NaliiaTools().get_tools()
config = AgentConfig(system_prompt=NALIIA_PROMPT)
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)

View File

@@ -1,18 +1,12 @@
import streamlit as st
from agent_client import AgentClient
from logger import logger
import json
import httpx
import random
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():
st.image("src/ui/assets/logo-naliia.png", width=100, caption="NaliiaBot")
@@ -32,10 +26,23 @@ async def main():
agent_client = AgentClient()
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']
with st.chat_message("assistant"):
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())