# Herramientas (Tools)

Las herramientas son código que puede ser ejecutado por un agente para realizar acciones. Una herramienta puede ser una función simple como una calculadora, o una llamada a una API de un servicio externo como la consulta de precios de acciones o pronóstico del clima. En el contexto de agentes de IA, las herramientas están diseñadas para ser ejecutadas por agentes en respuesta a llamadas a funciones generadas por modelos.

**Saptiva-Agents** proporciona el módulo `saptiva_agents.tools` con un conjunto de herramientas integradas y utilidades para crear y ejecutar herramientas personalizadas.

### Herramientas Integradas

Ejemplos de herramientas integradas:

* `mcp_server_tools` para usar servidores MCP como herramientas.
* `HttpTool` para hacer solicitudes HTTP a APIs REST.
* `LangChainToolAdapter` para usar herramientas de LangChain.

***

### Herramientas de Función Personalizadas

Una herramienta también puede ser una función de Python simple que realiza una acción específica. Para crear una herramienta de función personalizada, solo necesitas definir una función en Python y usar la clase `FunctionTool` para envolverla.

La clase `FunctionTool` utiliza descripciones y anotaciones de tipo para informar al modelo LLM cuándo y cómo utilizar una función dada. La descripción proporciona contexto sobre el propósito de la función y los casos de uso previstos, mientras que las anotaciones de tipo informan al modelo sobre los parámetros esperados y el tipo de retorno.

Por ejemplo, una herramienta simple para obtener el precio de una acción podría verse así:

```python
import random

from saptiva_agents.core import CancellationToken
from saptiva_agents.tools import FunctionTool
from typing_extensions import Annotated

# Retorna un precio de acción aleatorio con fines demostrativos.
async def get_stock_price(ticker: str, date: Annotated[str, "Fecha en formato YYYY/MM/DD"]) -> float:
    return random.uniform(10, 200)

# Crear una herramienta de función.
stock_price_tool = FunctionTool(get_stock_price, description="Obtener el precio de la acción.")

# Ejecutar la herramienta.
cancellation_token = CancellationToken()
result = await stock_price_tool.run_json({"ticker": "AAPL", "date": "2021/01/01"}, cancellation_token)

# Imprimir el resultado.
print(stock_price_tool.return_value_as_string(result))

```

```
143.83831971965762
```

***

### Herramientas de Llamada con Clientes de Modelo

En **Saptiva-Agents**, cada herramienta es una subclase de `BaseTool`, que genera automáticamente el esquema JSON de la herramienta. Los clientes de modelo utilizan este esquema para generar llamadas a herramientas.

```python
stock_price_tool.schema
```

```
{'name': 'get_stock_price',
 'description': 'Get the stock price.',
 'parameters': {'type': 'object',
  'properties': {'ticker': {'description': 'ticker',
    'title': 'Ticker',
    'type': 'string'},
   'date': {'description': 'Date in YYYY/MM/DD',
    'title': 'Date',
    'type': 'string'}},
  'required': ['ticker', 'date'],
  'additionalProperties': False},
 'strict': False}
```

Los clientes de modelo utilizan el esquema JSON de las herramientas para generar llamadas a herramientas.

Aquí tienes un ejemplo de cómo usar la clase `FunctionTool` con un `SaptivaAIChatCompletionClient`. Otras clases de cliente de modelo pueden utilizarse de forma similar. Consulta la sección [**Cliente De Modelo**](cliente-de-modelo) para más detalles.

```python
import json
from saptiva_agents.models import AssistantMessage, FunctionExecutionResult, FunctionExecutionResultMessage, UserMessage
from saptiva_agents.base import SaptivaAIChatCompletionClient

model_client = SaptivaAIChatCompletionClient(model="Saptiva Legacy", apy_key="TU_SAPTIVA_API_KEY")

user_message = UserMessage(content="What is the stock price of AAPL on 2021/01/01?", source="user")

cancellation_token = CancellationToken()
create_result = await model_client.create(
    messages=[user_message], tools=[stock_price_tool], cancellation_token=cancellation_token
)
create_result.content
```

```
[FunctionCall(id='call_tpJ5J1Xoxi84Sw4v0scH0qBM', arguments='{"ticker":"AAPL","date":"2021/01/01"}', name='get_stock_price')]
```

¿Qué está ocurriendo realmente bajo el capó cuando se llama al método `create`? El cliente del modelo toma la lista de herramientas y genera un esquema JSON para los parámetros de cada herramienta. Luego, genera una solicitud a la API del modelo con el esquema JSON de la herramienta y los demás mensajes para obtener un resultado.

Muchos modelos, como GPT-4o de OpenAI y Llama-3.2, están entrenados para producir llamadas a herramientas en forma de cadenas JSON estructuradas que se ajustan al esquema JSON de la herramienta. Los clientes de modelo de **Saptiva-Agents** luego analizan la respuesta del modelo y extraen la llamada a la herramienta desde la cadena JSON.

El resultado es una lista de objetos `FunctionCall`, que pueden usarse para ejecutar las herramientas correspondientes.

Usamos `json.loads` para analizar la cadena JSON del campo `arguments` en un diccionario de Python. El método `run_json()` toma el diccionario y ejecuta la herramienta con los argumentos proporcionados.

```python
assert isinstance(create_result.content, list)
arguments = json.loads(create_result.content[0].arguments)  # type: ignore
tool_result = await stock_price_tool.run_json(arguments, cancellation_token)
tool_result_str = stock_price_tool.return_value_as_string(tool_result)
tool_result_str
```

```
'32.381250753393104'
```

Ahora puedes hacer otra llamada al cliente del modelo para que el modelo genere una reflexión sobre el resultado de la ejecución de la herramienta.

El resultado de la llamada a la herramienta se envuelve en un objeto `FunctionExecutionResult`, que contiene el resultado de la ejecución de la herramienta y el ID de la herramienta que fue llamada. El cliente del modelo puede usar esta información para generar una reflexión sobre dicho resultado.

```python
# Crear un resultado de ejecución de función
exec_result = FunctionExecutionResult(
    call_id=create_result.content[0].id,  # type: ignore
    content=tool_result_str,
    is_error=False,
    name=stock_price_tool.name,
)

# Hacer otra llamada al modelo con el historial y el mensaje del resultado de ejecución de la función
messages = [
    user_message,
    AssistantMessage(content=create_result.content, source="assistant"),  # mensaje del asistente con la llamada a la herramienta
    FunctionExecutionResultMessage(content=[exec_result]),  # mensaje con el resultado de ejecución de la función
]
create_result = await model_client.create(messages=messages, cancellation_token=cancellation_token)  # type: ignore
print(create_result.content)
await model_client.close()
```

```
The stock price of AAPL (Apple Inc.) on January 1, 2021, was approximately $32.38.
```

***

### Agente Equipado con Herramientas

Combinando el cliente del modelo y las herramientas, puedes crear un agente equipado con herramientas que puede utilizarlas para realizar acciones y reflexionar sobre los resultados de esas acciones.

```python
import asyncio
import json
from dataclasses import dataclass
from typing import List, Sequence

from saptiva_agents.core import (
    AgentId,
    FunctionCall,
    MessageContext,
    RoutedAgent,
    SingleThreadedAgentRuntime,
    message_handler,
    CancellationToken
)
from saptiva_agents.models import (
    LLMMessage,
    SystemMessage,
    UserMessage,
)
from saptiva_agents import SAPTIVA_LEGACY
from saptiva_agents.tools import FunctionTool, Tool, ToolSchema
from saptiva_agents.models import AssistantMessage, FunctionExecutionResultMessage, FunctionExecutionResult
from saptiva_agents.base import SaptivaAIChatCompletionClient


@dataclass
class Message:
    content: str


class ToolUseAgent(RoutedAgent):
    def __init__(self, model_client: SaptivaAIChatCompletionClient, tool_schema: Sequence[Tool | ToolSchema]) -> None:
        super().__init__("An agent with tools")
        self._system_messages: List[LLMMessage] = [SystemMessage(content="You are a helpful AI assistant.")]
        self._model_client = model_client
        self._tools = tool_schema

    @message_handler
    async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:
        # Crear una sesión de mensajes.
        session: List[LLMMessage] = self._system_messages + [UserMessage(content=message.content, source="user")]

        # Ejecutar la finalización del chat con las herramientas.
        create_result = await self._model_client.create(
            messages=session,
            tools=self._tools,
            cancellation_token=ctx.cancellation_token,
        )

        # Si no hay llamadas a herramientas, devolver el resultado.
        if isinstance(create_result.content, str):
            return Message(content=create_result.content)
        assert isinstance(create_result.content, list) and all(
            isinstance(call, FunctionCall) for call in create_result.content
        )

        # Añadir el primer resultado del modelo a la sesión.
        session.append(AssistantMessage(content=create_result.content, source="assistant"))

        # Ejecutar las llamadas a herramientas.
        results = await asyncio.gather(
            *[self._execute_tool_call(call, ctx.cancellation_token) for call in create_result.content]
        )

        # Añadir los resultados de ejecución de funciones a la sesión.
        session.append(FunctionExecutionResultMessage(content=results))

        # Ejecutar nuevamente la finalización del chat para reflexionar sobre el historial y los resultados.
        create_result = await self._model_client.create(
            messages=session,
            cancellation_token=ctx.cancellation_token,
        )
        assert isinstance(create_result.content, str)

        # Devolver el resultado como mensaje.
        return Message(content=create_result.content)

    async def _execute_tool_call(
        self, call: FunctionCall, cancellation_token: CancellationToken
    ) -> FunctionExecutionResult:
        # Buscar la herramienta por nombre.
        tool = next((tool for tool in self._tools if tool.name == call.name), None)
        assert tool is not None

        # Ejecutar la herramienta y capturar el resultado.
        try:
            arguments = json.loads(call.arguments)
            result = await tool.run_json(arguments, cancellation_token)
            return FunctionExecutionResult(
                call_id=call.id, content=tool.return_value_as_string(result), is_error=False, name=tool.name
            )
        except Exception as e:
            return FunctionExecutionResult(call_id=call.id, content=str(e), is_error=True, name=tool.name)
```

Cuando maneja un mensaje de usuario, la clase `ToolUseAgent` primero usa el cliente del modelo para generar una lista de llamadas a funciones hacia las herramientas, luego ejecuta esas herramientas y genera una reflexión sobre los resultados de la ejecución. Esa reflexión es devuelta al usuario como la respuesta del agente.

#### Para ejecutar el agente, creamos el runtime y registramos el agente:

```python
# Crear el cliente del modelo.
model_client = SaptivaAIChatCompletionClient(
    model=SAPTIVA_LEGACY,
    api_key="TU_SAPTIVA_API_KEY",
)
# Crear un runtime.
runtime = SingleThreadedAgentRuntime()
# Crear las herramientas.
tools: List[Tool] = [FunctionTool(get_stock_price, description="Get the stock price.")]
# Registrar los agentes.
await ToolUseAgent.register(
    runtime,
    "tool_use_agent",
    lambda: ToolUseAgent(
        model_client=model_client,
        tool_schema=tools,
    ),
)
```

```python
AgentType(type='tool_use_agent')
```

```python
# Iniciar el procesamiento de mensajes.
runtime.start()
# Enviar un mensaje directo a la herramienta del agente.
tool_use_agent = AgentId("tool_use_agent", "default")
response = await runtime.send_message(Message("What is the stock price of NVIDIA on 2025/04/01?"), tool_use_agent)
print(response.content)
# Detener el procesamiento de mensajes.
await runtime.stop()
await model_client.close()
```

```
The stock price of NVIDIA on 2025/04/01, was approximately $140.05.
```
