Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions docs/api/agent-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Agent Tool Plugins

**Entry point:** `opm.agents.toolbox`

Tool plugins extend personas with executable functions. A `ToolBox` groups related tools, handles bus-based discovery, and validates inputs/outputs via Pydantic.

---

## Core Classes

### `AgentTool` — `ovos_plugin_manager/templates/agent_tools.py`

A dataclass defining a single executable function and its contract.

| Field | Type | Description |
|---|---|---|
| `name` | `str` | Unique snake_case identifier used by agents/LLMs to reference the tool. |
| `description` | `str` | Natural language description; essential for LLM reasoning. |
| `argument_schema` | `Type[ToolArguments]` | Pydantic model defining required input structure. |
| `output_schema` | `Type[ToolOutput]` | Pydantic model defining guaranteed output structure. |
| `tool_call` | `Callable[[ToolArguments], ToolCallReturn]` | Function that executes the tool logic. |

### `ToolArguments` / `ToolOutput`

Base Pydantic models for tool contracts. Subclass these when defining a tool. JSON Schema is generated automatically via `model_json_schema()` for LLM consumption.

### `ToolBox` (ABC) — `ovos_plugin_manager/templates/agent_tools.py`

Abstract base class for tool plugins. Groups related `AgentTool` instances, registers messagebus handlers, and enforces validation.

**Abstract method — must implement:**

```python
def discover_tools(self) -> List[AgentTool]
```

Returns the list of tools provided by this plugin. Called at init and on `refresh_tools()`. Must be idempotent.

**Key methods:**

| Method | Description |
|---|---|
| `bind(bus)` | Attach to messagebus; registers discovery and call handlers. |
| `call_tool(name, tool_kwargs)` | Execute a tool by name with full input/output validation. |
| `get_tool(name)` | Retrieve an `AgentTool` by name; triggers lazy refresh if not cached. |
| `refresh_tools()` | Re-run `discover_tools()` and update the internal cache. |

**Property:**

```python
@property
def tool_json_list(self) -> List[Dict]
```

Returns all tools serialized with JSON Schema argument/output schemas — suitable for sending to an LLM's `tools` API parameter.

---

## Messagebus Interface

Tools expose themselves on the bus automatically when `bind(bus)` is called.

| Message | Direction | Description |
|---|---|---|
| `ovos.persona.tools.discover` | → ToolBox | Broadcast discovery request. |
| `ovos.persona.tools.discover` (response) | ← ToolBox | Returns `{"tools": [...], "toolbox_id": "..."}`. |
| `ovos.persona.tools.<toolbox_id>.call` | → ToolBox | Call a specific tool with `{"name": "...", "kwargs": {...}}`. |
| `ovos.persona.tools.<toolbox_id>.call` (response) | ← ToolBox | Returns `{"result": {...}}` or `{"error": "..."}`. |

This allows agent plugins in separate processes (e.g. MCP or UTCP servers) to discover and call tools dynamically without importing the plugin directly.

---

## Plugin Registration

Register your `ToolBox` subclass in `pyproject.toml`:

```toml
[project.entry-points."opm.agents.toolbox"]
my_toolbox = "my_package:MyToolBox"
```

---

## Writing a ToolBox Plugin

```python
from ovos_plugin_manager.templates.agent_tools import AgentTool, ToolArguments, ToolBox, ToolOutput
from pydantic import Field
from typing import List


class WeatherArgs(ToolArguments):
location: str = Field(..., description="City name or coordinates.")


class WeatherOutput(ToolOutput):
temperature_c: float
condition: str


def fetch_weather(args: WeatherArgs) -> WeatherOutput:
# ... call weather API ...
return WeatherOutput(temperature_c=21.5, condition="Sunny")


class WeatherToolBox(ToolBox):
def __init__(self, bus=None):
super().__init__(toolbox_id="weather_tools", bus=bus)

def discover_tools(self) -> List[AgentTool]:
return [
AgentTool(
name="get_weather",
description="Get the current weather for a location.",
argument_schema=WeatherArgs,
output_schema=WeatherOutput,
tool_call=fetch_weather,
)
]
```

---

## Validation Behaviour

`call_tool()` enforces a strict lifecycle:

1. **Input** — if `tool_kwargs` is a `dict`, validated against `argument_schema` via Pydantic. If already a `ToolArguments` instance, type-checked against the declared schema.
2. **Execution** — `tool.tool_call(validated_args)` is called.
3. **Output** — if the result is a `dict`, validated against `output_schema`. If already a `ToolOutput` instance, type-checked.

On failure: `ValueError` for input problems, `RuntimeError` for execution or output problems.

Errors from `discover_tools()` at init are logged at `DEBUG` level and retried lazily on first `get_tool()` call. This supports dynamic discovery plugins (MCP, UTCP) where tools may not be available at startup.

---

## Discovery

```python
from ovos_plugin_manager.persona import find_toolbox_plugins

plugins = find_toolbox_plugins()
# {"my_toolbox": MyToolBox, ...}
```
5 changes: 4 additions & 1 deletion docs/api/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ Streaming/non-streaming conversational LLM backend.
#### Abstract method

```python
def chat(self, messages: List[AgentMessage], lang: Optional[str] = None) -> Optional[str]
def continue_chat(self, messages: List[AgentMessage],
session_id: str = "default",
lang: Optional[str] = None,
units: Optional[str] = None) -> AgentMessage
```

Generate a response given a list of `AgentMessage` objects.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ loading installed plugins at runtime via Python entry points.
- [PHAL](api/phal.md) — Platform/Hardware Abstraction Layer
- [Transformers](api/transformers.md) — Audio, Utterance, Dialog, Metadata, TTS, Intent transformers
- [Agents & Solvers](api/agents.md) — NLP solver and agent engine plugins
- [Agent Tools](api/agent-tools.md) — ToolBox plugin API (`opm.persona.tool`)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- [Pipeline](api/pipeline.md) — Intent matching pipeline plugins
- [Language](api/language.md) — Translation and language detection plugins

Expand Down
15 changes: 13 additions & 2 deletions ovos_plugin_manager/persona.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from typing import Type, Dict, Any

from ovos_plugin_manager.templates.agent_tools import ToolBox
from ovos_plugin_manager.utils import PluginTypes


def find_persona_plugins() -> dict:
def find_persona_plugins() -> Dict[str, Dict[str, Any]]:
"""
Find all installed plugins
Find all installed persona definitions
@return: dict plugin names to entrypoints (persona entrypoint are just dicts)
"""
from ovos_plugin_manager.utils import find_plugins
return find_plugins(PluginTypes.PERSONA)


def find_toolbox_plugins() -> Dict[str, Type[ToolBox]]:
"""
Find all installed ToolBox plugins.

@return: dict of toolbox_id to ToolBox subclass
"""
from ovos_plugin_manager.utils import find_plugins
return find_plugins(PluginTypes.AGENT_TOOLBOX)
Loading
Loading