435 lines
14 KiB
Python
435 lines
14 KiB
Python
"""
|
|
Tests para el patrón Registry de herramientas.
|
|
|
|
Prueba la funcionalidad del ToolRegistry y la integración con el Agent.
|
|
"""
|
|
|
|
import pytest
|
|
from src.naliiabot.bot.tools.tool_registry import (
|
|
ToolRegistry,
|
|
ToolRegistryBuilder,
|
|
BaseTool
|
|
)
|
|
from src.naliiabot.bot.tools.tools import (
|
|
CalculatorTool,
|
|
GreeterTool,
|
|
WeatherTool,
|
|
TimeTool
|
|
)
|
|
from src.naliiabot.bot.agent.agent import Agent, AgentConfig
|
|
|
|
|
|
# ============================================================================
|
|
# TESTS DEL BASETOOL
|
|
# ============================================================================
|
|
|
|
|
|
class TestBaseTool:
|
|
"""Pruebas para la clase base BaseTool."""
|
|
|
|
def test_calculator_tool_properties(self):
|
|
"""Verificar que CalculatorTool implementa correctamente BaseTool."""
|
|
tool = CalculatorTool()
|
|
|
|
assert tool.name == "calculator"
|
|
assert "mathematical" in tool.description.lower()
|
|
assert "properties" in tool.args_schema
|
|
assert "required" in tool.args_schema
|
|
|
|
def test_calculator_add(self):
|
|
"""Prueba la operación de suma."""
|
|
tool = CalculatorTool()
|
|
result = tool.invoke(a=5, b=3, operation="add")
|
|
|
|
assert "8" in result
|
|
assert "add" in result
|
|
|
|
def test_calculator_subtract(self):
|
|
"""Prueba la operación de resta."""
|
|
tool = CalculatorTool()
|
|
result = tool.invoke(a=10, b=4, operation="subtract")
|
|
|
|
assert "6" in result
|
|
|
|
def test_calculator_multiply(self):
|
|
"""Prueba la operación de multiplicación."""
|
|
tool = CalculatorTool()
|
|
result = tool.invoke(a=6, b=7, operation="multiply")
|
|
|
|
assert "42" in result
|
|
|
|
def test_calculator_divide(self):
|
|
"""Prueba la operación de división."""
|
|
tool = CalculatorTool()
|
|
result = tool.invoke(a=20, b=4, operation="divide")
|
|
|
|
assert "5" in result
|
|
|
|
def test_calculator_divide_by_zero(self):
|
|
"""Prueba el manejo de división por cero."""
|
|
tool = CalculatorTool()
|
|
result = tool.invoke(a=10, b=0, operation="divide")
|
|
|
|
assert "Error" in result or "zero" in result.lower()
|
|
|
|
def test_greeter_tool_formal(self):
|
|
"""Prueba el saludo formal."""
|
|
tool = GreeterTool()
|
|
result = tool.invoke(name="Alice", tone="formal")
|
|
|
|
assert "Alice" in result
|
|
assert "pleasure" in result.lower()
|
|
|
|
def test_greeter_tool_casual(self):
|
|
"""Prueba el saludo casual."""
|
|
tool = GreeterTool()
|
|
result = tool.invoke(name="Bob", tone="casual")
|
|
|
|
assert "Bob" in result
|
|
assert "up" in result.lower()
|
|
|
|
def test_greeter_tool_friendly(self):
|
|
"""Prueba el saludo amigable."""
|
|
tool = GreeterTool()
|
|
result = tool.invoke(name="Charlie", tone="friendly")
|
|
|
|
assert "Charlie" in result
|
|
|
|
def test_weather_tool(self):
|
|
"""Prueba la herramienta de clima."""
|
|
tool = WeatherTool()
|
|
result = tool.invoke(location="Madrid", units="celsius")
|
|
|
|
assert "Madrid" in result
|
|
assert "Weather" in result or "temperature" in result.lower()
|
|
|
|
def test_time_tool_24h_format(self):
|
|
"""Prueba la herramienta de hora en formato 24h."""
|
|
tool = TimeTool()
|
|
result = tool.invoke(format="24h", timezone="UTC")
|
|
|
|
assert "UTC" in result
|
|
assert ":" in result # Contiene al menos un separador de hora
|
|
|
|
def test_time_tool_12h_format(self):
|
|
"""Prueba la herramienta de hora en formato 12h."""
|
|
tool = TimeTool()
|
|
result = tool.invoke(format="12h", timezone="EST")
|
|
|
|
assert "EST" in result
|
|
assert ("AM" in result or "PM" in result)
|
|
|
|
|
|
# ============================================================================
|
|
# TESTS DEL TOOL REGISTRY
|
|
# ============================================================================
|
|
|
|
|
|
class TestToolRegistry:
|
|
"""Pruebas para el ToolRegistry."""
|
|
|
|
def test_registry_empty_on_init(self):
|
|
"""Verificar que el registro inicia vacío."""
|
|
registry = ToolRegistry()
|
|
|
|
assert len(registry) == 0
|
|
assert registry.list_tools() == {}
|
|
|
|
def test_register_single_tool(self):
|
|
"""Registrar una sola herramienta."""
|
|
registry = ToolRegistry()
|
|
tool = CalculatorTool()
|
|
|
|
registry.register(tool)
|
|
|
|
assert len(registry) == 1
|
|
assert registry.has_tool("calculator")
|
|
assert registry.get_tool("calculator") == tool
|
|
|
|
def test_register_multiple_tools(self):
|
|
"""Registrar múltiples herramientas."""
|
|
registry = ToolRegistry()
|
|
tools = [CalculatorTool(), GreeterTool(), WeatherTool()]
|
|
|
|
registry.register_multiple(tools)
|
|
|
|
assert len(registry) == 3
|
|
assert registry.has_tool("calculator")
|
|
assert registry.has_tool("greeter")
|
|
assert registry.has_tool("get_weather")
|
|
|
|
def test_register_duplicate_tool_raises_error(self):
|
|
"""Intentar registrar una herramienta duplicada debe fallar."""
|
|
registry = ToolRegistry()
|
|
registry.register(CalculatorTool())
|
|
|
|
with pytest.raises(ValueError, match="already registered"):
|
|
registry.register(CalculatorTool())
|
|
|
|
def test_register_non_basetool_raises_error(self):
|
|
"""Registrar algo que no es BaseTool debe fallar."""
|
|
registry = ToolRegistry()
|
|
|
|
with pytest.raises(TypeError, match="must be instance of BaseTool"):
|
|
registry.register("not a tool")
|
|
|
|
def test_get_tool_existing(self):
|
|
"""Obtener una herramienta existente."""
|
|
registry = ToolRegistry()
|
|
tool = CalculatorTool()
|
|
registry.register(tool)
|
|
|
|
retrieved = registry.get_tool("calculator")
|
|
|
|
assert retrieved == tool
|
|
|
|
def test_get_tool_non_existing(self):
|
|
"""Obtener una herramienta que no existe retorna None."""
|
|
registry = ToolRegistry()
|
|
|
|
result = registry.get_tool("nonexistent")
|
|
|
|
assert result is None
|
|
|
|
def test_get_all_tools(self):
|
|
"""Obtener todas las herramientas."""
|
|
registry = ToolRegistry()
|
|
tools = [CalculatorTool(), GreeterTool()]
|
|
registry.register_multiple(tools)
|
|
|
|
all_tools = registry.get_all_tools()
|
|
|
|
assert len(all_tools) == 2
|
|
assert all(isinstance(t, BaseTool) for t in all_tools)
|
|
|
|
def test_unregister_tool(self):
|
|
"""Desregistrar una herramienta."""
|
|
registry = ToolRegistry()
|
|
registry.register(CalculatorTool())
|
|
|
|
registry.unregister("calculator")
|
|
|
|
assert len(registry) == 0
|
|
assert not registry.has_tool("calculator")
|
|
|
|
def test_unregister_non_existing_raises_error(self):
|
|
"""Desregistrar una herramienta que no existe debe fallar."""
|
|
registry = ToolRegistry()
|
|
|
|
with pytest.raises(KeyError, match="not found"):
|
|
registry.unregister("nonexistent")
|
|
|
|
def test_list_tools(self):
|
|
"""Listar todas las herramientas con descripciones."""
|
|
registry = ToolRegistry()
|
|
registry.register(CalculatorTool())
|
|
registry.register(GreeterTool())
|
|
|
|
tools_list = registry.list_tools()
|
|
|
|
assert isinstance(tools_list, dict)
|
|
assert "calculator" in tools_list
|
|
assert "greeter" in tools_list
|
|
assert len(tools_list) == 2
|
|
|
|
def test_clear_registry(self):
|
|
"""Limpiar el registro."""
|
|
registry = ToolRegistry()
|
|
registry.register_multiple([
|
|
CalculatorTool(),
|
|
GreeterTool(),
|
|
WeatherTool()
|
|
])
|
|
|
|
registry.clear()
|
|
|
|
assert len(registry) == 0
|
|
|
|
def test_contains_operator(self):
|
|
"""Usar el operador 'in' para verificar existencia."""
|
|
registry = ToolRegistry()
|
|
registry.register(CalculatorTool())
|
|
|
|
assert "calculator" in registry
|
|
assert "greeter" not in registry
|
|
|
|
def test_get_langchain_tools(self):
|
|
"""Obtener herramientas en formato LangChain."""
|
|
registry = ToolRegistry()
|
|
registry.register_multiple([
|
|
CalculatorTool(),
|
|
GreeterTool()
|
|
])
|
|
|
|
langchain_tools = registry.get_langchain_tools()
|
|
|
|
assert len(langchain_tools) == 2
|
|
assert all(hasattr(t, 'name') for t in langchain_tools)
|
|
assert all(hasattr(t, 'description') for t in langchain_tools)
|
|
|
|
def test_registry_repr(self):
|
|
"""Prueba la representación en string del registro."""
|
|
registry = ToolRegistry()
|
|
registry.register(CalculatorTool())
|
|
|
|
repr_str = repr(registry)
|
|
|
|
assert "ToolRegistry" in repr_str
|
|
assert "calculator" in repr_str
|
|
|
|
|
|
# ============================================================================
|
|
# TESTS DEL TOOL REGISTRY BUILDER
|
|
# ============================================================================
|
|
|
|
|
|
class TestToolRegistryBuilder:
|
|
"""Pruebas para el ToolRegistryBuilder."""
|
|
|
|
def test_builder_fluent_interface(self):
|
|
"""Verificar la interfaz fluida del builder."""
|
|
registry = (ToolRegistryBuilder()
|
|
.add(CalculatorTool())
|
|
.add(GreeterTool())
|
|
.build())
|
|
|
|
assert isinstance(registry, ToolRegistry)
|
|
assert len(registry) == 2
|
|
|
|
def test_builder_add_multiple(self):
|
|
"""Añadir múltiples herramientas con builder."""
|
|
registry = (ToolRegistryBuilder()
|
|
.add_multiple([
|
|
CalculatorTool(),
|
|
GreeterTool(),
|
|
WeatherTool()
|
|
])
|
|
.build())
|
|
|
|
assert len(registry) == 3
|
|
|
|
def test_builder_mixed_operations(self):
|
|
"""Mezclar operaciones add y add_multiple."""
|
|
registry = (ToolRegistryBuilder()
|
|
.add(CalculatorTool())
|
|
.add_multiple([GreeterTool(), WeatherTool()])
|
|
.add(TimeTool())
|
|
.build())
|
|
|
|
assert len(registry) == 4
|
|
assert registry.has_tool("calculator")
|
|
assert registry.has_tool("greeter")
|
|
assert registry.has_tool("get_weather")
|
|
assert registry.has_tool("get_time")
|
|
|
|
|
|
# ============================================================================
|
|
# TESTS DE INTEGRACIÓN CON AGENT
|
|
# ============================================================================
|
|
|
|
|
|
class TestAgentWithRegistry:
|
|
"""Pruebas de integración del Agent con ToolRegistry."""
|
|
|
|
def test_agent_accepts_tool_registry(self, mock_model):
|
|
"""El agente debe aceptar un ToolRegistry."""
|
|
registry = ToolRegistry()
|
|
registry.register_multiple([
|
|
CalculatorTool(),
|
|
GreeterTool()
|
|
])
|
|
|
|
agent = Agent(model=mock_model, tools=registry)
|
|
|
|
assert len(agent.tools) == 2
|
|
|
|
def test_agent_registry_list_conversion(self, mock_model):
|
|
"""El agente debe convertir ToolRegistry a lista internamente."""
|
|
registry = ToolRegistry()
|
|
registry.register(CalculatorTool())
|
|
|
|
agent = Agent(model=mock_model, tools=registry)
|
|
|
|
# Internamente debe tener la lista de tools
|
|
assert len(agent._tools) == 1
|
|
|
|
def test_agent_with_builder_registry(self, mock_model):
|
|
"""Usar Agent con ToolRegistry creado con Builder."""
|
|
registry = (ToolRegistryBuilder()
|
|
.add(CalculatorTool())
|
|
.add(WeatherTool())
|
|
.build())
|
|
|
|
agent = Agent(model=mock_model, tools=registry)
|
|
|
|
assert len(agent.tools) == 2
|
|
|
|
def test_agent_still_accepts_list_of_tools(self, mock_model):
|
|
"""El agente debe seguir aceptando listas de tools."""
|
|
tools = [CalculatorTool(), GreeterTool()]
|
|
|
|
agent = Agent(model=mock_model, tools=tools)
|
|
|
|
assert len(agent.tools) == 2
|
|
|
|
def test_agent_none_tools_default(self, mock_model):
|
|
"""El agente debe funcionar sin tools."""
|
|
agent = Agent(model=mock_model, tools=None)
|
|
|
|
assert len(agent.tools) == 0
|
|
|
|
|
|
# ============================================================================
|
|
# TESTS DE CASOS DE USO PRÁCTICOS
|
|
# ============================================================================
|
|
|
|
|
|
class TestPracticalUseCases:
|
|
"""Casos de uso prácticos del ToolRegistry."""
|
|
|
|
def test_build_customer_service_registry(self):
|
|
"""Construir un registro para un agente de servicio al cliente."""
|
|
registry = (ToolRegistryBuilder()
|
|
.add(GreeterTool())
|
|
.add(WeatherTool())
|
|
.add(TimeTool())
|
|
.build())
|
|
|
|
assert registry.has_tool("greeter")
|
|
assert len(registry) == 3
|
|
|
|
def test_dynamic_tool_addition(self):
|
|
"""Añadir herramientas dinámicamente."""
|
|
registry = ToolRegistry()
|
|
|
|
# Fase 1: registrar herramientas básicas
|
|
registry.register(CalculatorTool())
|
|
assert len(registry) == 1
|
|
|
|
# Fase 2: registrar herramientas adicionales
|
|
registry.register(GreeterTool())
|
|
assert len(registry) == 2
|
|
|
|
# Fase 3: remover si es necesario
|
|
registry.unregister("calculator")
|
|
assert len(registry) == 1
|
|
|
|
def test_tool_registry_documentation(self):
|
|
"""Generar documentación de herramientas disponibles."""
|
|
registry = (ToolRegistryBuilder()
|
|
.add_multiple([
|
|
CalculatorTool(),
|
|
GreeterTool(),
|
|
WeatherTool(),
|
|
TimeTool()
|
|
])
|
|
.build())
|
|
|
|
tools_doc = registry.list_tools()
|
|
|
|
assert len(tools_doc) == 4
|
|
for tool_name, description in tools_doc.items():
|
|
assert isinstance(tool_name, str)
|
|
assert isinstance(description, str)
|
|
assert len(description) > 0
|