Add Ability to Disable Tools (#215)
# PR Description This PR introduces a new environment variable `ARCADE_DISABLED_TOOLS`. Tools that are added to this env var are not added to the worker's `ToolCatalog`. In effect, they are disabled for the worker. ## How to use the `ARCADE_DISABLED_TOOLS` environment variable * Each tool is separated by a comma. * For each tool, specify the toolkit name in camel case and the tool name in camel case. * Do not include versions. (This is a simple implementation. We can add disabling specific versions in the future if needed) * Separate the toolkit name and the tool name with your environment's tool name separator. By default, the tool name separator is `.`, but you can override this with the `ARCADE_TOOL_NAME_SEPARATOR` environment variable. Correct: `export ARCADE_DISABLED_TOOLS="Math.Add,Spotify.GetAvailableDevices,Math.Sqrt"` Incorrect: `export ARCADE_DISABLED_TOOLS="Math.Add@0.1.0,Spotify.get_available_devices,Sqrt`
This commit is contained in:
parent
899c84929b
commit
1bd8eac6ed
3 changed files with 98 additions and 5 deletions
|
|
@ -100,11 +100,6 @@ def serve_default_worker(
|
||||||
if not toolkits:
|
if not toolkits:
|
||||||
raise RuntimeError("No toolkits found in Python environment.")
|
raise RuntimeError("No toolkits found in Python environment.")
|
||||||
|
|
||||||
logger.info("Serving the following toolkits:")
|
|
||||||
for toolkit in toolkits:
|
|
||||||
num_tools = sum(len(tools) for tools in toolkit.tools.values())
|
|
||||||
logger.info(f" - {toolkit.name} ({toolkit.package_name}): {num_tools} tools")
|
|
||||||
|
|
||||||
worker_secret = os.environ.get("ARCADE_WORKER_SECRET")
|
worker_secret = os.environ.get("ARCADE_WORKER_SECRET")
|
||||||
if not disable_auth and not worker_secret:
|
if not disable_auth and not worker_secret:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
@ -124,8 +119,19 @@ def serve_default_worker(
|
||||||
worker = FastAPIWorker(
|
worker = FastAPIWorker(
|
||||||
app, secret=worker_secret, disable_auth=disable_auth, otel_meter=otel_handler.get_meter()
|
app, secret=worker_secret, disable_auth=disable_auth, otel_meter=otel_handler.get_meter()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
toolkit_tool_counts = {}
|
||||||
for toolkit in toolkits:
|
for toolkit in toolkits:
|
||||||
|
prev_tool_count = worker.catalog.get_tool_count()
|
||||||
worker.register_toolkit(toolkit)
|
worker.register_toolkit(toolkit)
|
||||||
|
new_tool_count = worker.catalog.get_tool_count()
|
||||||
|
toolkit_tool_counts[f"{toolkit.name} ({toolkit.package_name})"] = (
|
||||||
|
new_tool_count - prev_tool_count
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Serving the following toolkits:")
|
||||||
|
for name, tool_count in toolkit_tool_counts.items():
|
||||||
|
logger.info(f" - {name}: {tool_count} tools")
|
||||||
|
|
||||||
logger.info("Starting FastAPI server...")
|
logger.info("Starting FastAPI server...")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import typing
|
import typing
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -49,6 +52,8 @@ from arcade.core.utils import (
|
||||||
snake_to_pascal_case,
|
snake_to_pascal_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
InnerWireType = Literal["string", "integer", "number", "boolean", "json"]
|
InnerWireType = Literal["string", "integer", "number", "boolean", "json"]
|
||||||
WireType = Union[InnerWireType, Literal["array"]]
|
WireType = Union[InnerWireType, Literal["array"]]
|
||||||
|
|
||||||
|
|
@ -112,6 +117,34 @@ class ToolCatalog(BaseModel):
|
||||||
|
|
||||||
_tools: dict[FullyQualifiedName, MaterializedTool] = {}
|
_tools: dict[FullyQualifiedName, MaterializedTool] = {}
|
||||||
|
|
||||||
|
_disabled_tools: set[str] = set()
|
||||||
|
|
||||||
|
def __init__(self, **data) -> None: # type: ignore[no-untyped-def]
|
||||||
|
super().__init__(**data)
|
||||||
|
self._load_disabled_tools()
|
||||||
|
|
||||||
|
def _load_disabled_tools(self) -> None:
|
||||||
|
"""Load disabled tools from the environment variable.
|
||||||
|
|
||||||
|
The ARCADE_DISABLED_TOOLS environment variable should contain a
|
||||||
|
comma-separated list of tools that are to be excluded from the
|
||||||
|
catalog.
|
||||||
|
|
||||||
|
The expected format for each disabled tool is:
|
||||||
|
- [CamelCaseToolkitName][TOOL_NAME_SEPARATOR][CamelCaseToolName]
|
||||||
|
"""
|
||||||
|
disabled_tools = os.getenv("ARCADE_DISABLED_TOOLS", "").strip().split(",")
|
||||||
|
if not disabled_tools:
|
||||||
|
return
|
||||||
|
|
||||||
|
pattern = re.compile(rf"^[a-zA-Z]+{re.escape(TOOL_NAME_SEPARATOR)}[a-zA-Z]+$")
|
||||||
|
|
||||||
|
for tool in disabled_tools:
|
||||||
|
if not pattern.match(tool):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._disabled_tools.add(tool.lower())
|
||||||
|
|
||||||
def add_tool(
|
def add_tool(
|
||||||
self,
|
self,
|
||||||
tool_func: Callable,
|
tool_func: Callable,
|
||||||
|
|
@ -146,6 +179,10 @@ class ToolCatalog(BaseModel):
|
||||||
if fully_qualified_name in self._tools:
|
if fully_qualified_name in self._tools:
|
||||||
raise KeyError(f"Tool '{definition.name}' already exists in the catalog.")
|
raise KeyError(f"Tool '{definition.name}' already exists in the catalog.")
|
||||||
|
|
||||||
|
if str(fully_qualified_name).lower() in self._disabled_tools:
|
||||||
|
logger.info(f"Tool '{fully_qualified_name!s}' is disabled and will not be cataloged.")
|
||||||
|
return
|
||||||
|
|
||||||
self._tools[fully_qualified_name] = MaterializedTool(
|
self._tools[fully_qualified_name] = MaterializedTool(
|
||||||
definition=definition,
|
definition=definition,
|
||||||
tool=tool_func,
|
tool=tool_func,
|
||||||
|
|
@ -271,6 +308,12 @@ class ToolCatalog(BaseModel):
|
||||||
|
|
||||||
raise ValueError(f"Tool {name} not found.")
|
raise ValueError(f"Tool {name} not found.")
|
||||||
|
|
||||||
|
def get_tool_count(self) -> int:
|
||||||
|
"""
|
||||||
|
Get the number of tools in the catalog.
|
||||||
|
"""
|
||||||
|
return len(self._tools)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_tool_definition(
|
def create_tool_definition(
|
||||||
tool: Callable,
|
tool: Callable,
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,47 @@ def test_get_tool_by_name_with_invalid_version():
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
catalog.get_tool_by_name("SampleToolkit.SampleTool", version="2.0.0")
|
catalog.get_tool_by_name("SampleToolkit.SampleTool", version="2.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_disabled_tools(monkeypatch):
|
||||||
|
disabled_tools = (
|
||||||
|
"SampleToolkitOne.SampleToolOne," # valid
|
||||||
|
+ "SampleToolkitOne_SampleToolTwo," # invalid
|
||||||
|
+ "SampleToolkitTwo.SampleToolThree," # valid
|
||||||
|
+ "SampleToolkitTwo.SampleToolFour@0.0.1," # invalid
|
||||||
|
+ "SampleToolkitThree_SampleToolFive@0.0.1," # invalid
|
||||||
|
+ "SampleToolkitFour.sample_tool_six," # invalid
|
||||||
|
+ "sample_toolkit5.SampleTool7," # invalid
|
||||||
|
+ "sample_toolkit6.sample_tool_8" # invalid
|
||||||
|
)
|
||||||
|
expected_disabled_tools = {
|
||||||
|
"sampletoolkitone.sampletoolone",
|
||||||
|
"sampletoolkittwo.sampletoolthree",
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setenv("ARCADE_DISABLED_TOOLS", disabled_tools)
|
||||||
|
catalog = ToolCatalog()
|
||||||
|
|
||||||
|
assert catalog._disabled_tools == expected_disabled_tools
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_tool_with_disabled_tool(monkeypatch):
|
||||||
|
monkeypatch.setenv("ARCADE_DISABLED_TOOLS", "SampleToolkitOne.SampleTool")
|
||||||
|
catalog = ToolCatalog()
|
||||||
|
|
||||||
|
catalog.add_tool(sample_tool, "SampleToolkitOne")
|
||||||
|
assert len(catalog._tools) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_tool_with_empty_string_disabled_tools(monkeypatch):
|
||||||
|
monkeypatch.setenv("ARCADE_DISABLED_TOOLS", "")
|
||||||
|
catalog = ToolCatalog()
|
||||||
|
catalog.add_tool(sample_tool, "SampleToolkitOne")
|
||||||
|
assert len(catalog._tools) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_tool_with_whitespace_disabled_tools(monkeypatch):
|
||||||
|
monkeypatch.setenv("ARCADE_DISABLED_TOOLS", " ")
|
||||||
|
catalog = ToolCatalog()
|
||||||
|
catalog.add_tool(sample_tool, "SampleToolkitOne")
|
||||||
|
assert len(catalog._tools) == 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue