feat: Add NaliiaTools
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user