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