-
-
Notifications
You must be signed in to change notification settings - Fork 12
feat: tool plugins #340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: tool plugins #340
Changes from 11 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
017f9af
feat: tool plugins
JarbasAl ba6f51e
Update requirements/requirements.txt
JarbasAl 8724b08
discovery
JarbasAl e0b58bb
pydantic validation
JarbasAl ad6801b
better output validation
JarbasAl 7f1634a
better input validation
JarbasAl 765e6b3
Update ovos_plugin_manager/templates/agent_tools.py
JarbasAl 86e38d2
fix coderabbit mess
JarbasAl 7691d61
fix: agent_tools bug fixes, Apache header, docs, and tests
JarbasAl 813bbf9
feat: move tool plugins to opm.agents namespace, add AgenticLoopEngine
JarbasAl a2bde31
refactor: remove AgenticLoopEngine and AGENT_LOOP from OPM
JarbasAl 1bac939
fix: address PR #340 CodeRabbit review comments
JarbasAl 81b4bce
docs: update FAQ and MAINTENANCE_REPORT for PR #340 fixes
JarbasAl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, ...} | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.