From 66fe0a2a139ef9adbeac901a4f8889f7c7730a0c Mon Sep 17 00:00:00 2001 From: aserrador Date: Mon, 16 Feb 2026 16:35:15 -0500 Subject: [PATCH] feat: Add NaliiaTools --- src/naliiabot/bot/agent/agent.py | 25 ++++---- src/naliiabot/bot/agent/schemas.py | 6 +- src/naliiabot/bot/factories/llm_factory.py | 4 +- .../bot/prompts/prompts/prompts_template.yaml | 4 +- src/naliiabot/bot/tools/naliia_tools.py | 60 ++++++++++++++++++- src/naliiabotapi/api/dependencies.py | 10 ++-- src/ui/src/ui/app.py | 25 +++++--- 7 files changed, 100 insertions(+), 34 deletions(-) diff --git a/src/naliiabot/bot/agent/agent.py b/src/naliiabot/bot/agent/agent.py index 1f8d018..69bd77b 100644 --- a/src/naliiabot/bot/agent/agent.py +++ b/src/naliiabot/bot/agent/agent.py @@ -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( diff --git a/src/naliiabot/bot/agent/schemas.py b/src/naliiabot/bot/agent/schemas.py index 54b2a13..0fa63ec 100644 --- a/src/naliiabot/bot/agent/schemas.py +++ b/src/naliiabot/bot/agent/schemas.py @@ -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 diff --git a/src/naliiabot/bot/factories/llm_factory.py b/src/naliiabot/bot/factories/llm_factory.py index a2c09fc..2bb8510 100644 --- a/src/naliiabot/bot/factories/llm_factory.py +++ b/src/naliiabot/bot/factories/llm_factory.py @@ -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 diff --git a/src/naliiabot/bot/prompts/prompts/prompts_template.yaml b/src/naliiabot/bot/prompts/prompts/prompts_template.yaml index 3885520..51ff801 100644 --- a/src/naliiabot/bot/prompts/prompts/prompts_template.yaml +++ b/src/naliiabot/bot/prompts/prompts/prompts_template.yaml @@ -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. diff --git a/src/naliiabot/bot/tools/naliia_tools.py b/src/naliiabot/bot/tools/naliia_tools.py index ae9d748..3d72dda 100644 --- a/src/naliiabot/bot/tools/naliia_tools.py +++ b/src/naliiabot/bot/tools/naliia_tools.py @@ -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 diff --git a/src/naliiabotapi/api/dependencies.py b/src/naliiabotapi/api/dependencies.py index 52fc540..3aacf06 100644 --- a/src/naliiabotapi/api/dependencies.py +++ b/src/naliiabotapi/api/dependencies.py @@ -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) diff --git a/src/ui/src/ui/app.py b/src/ui/src/ui/app.py index 28798aa..c99c203 100644 --- a/src/ui/src/ui/app.py +++ b/src/ui/src/ui/app.py @@ -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())