mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 09:28:34 +00:00
Decouple tool configs from root schema
This commit is contained in:
parent
8a51059bdf
commit
e015f469ad
@ -217,6 +217,7 @@ class AgentLoop:
|
|||||||
runtime_events: RuntimeEventBus | None = None,
|
runtime_events: RuntimeEventBus | None = None,
|
||||||
runtime_model_publisher: Callable[[str, str | None], None] | None = None,
|
runtime_model_publisher: Callable[[str, str | None], None] | None = None,
|
||||||
):
|
):
|
||||||
|
from nanobot.agent.tools.config import tool_config_by_key
|
||||||
from nanobot.config.schema import ToolsConfig
|
from nanobot.config.schema import ToolsConfig
|
||||||
|
|
||||||
_tc = tools_config or ToolsConfig()
|
_tc = tools_config or ToolsConfig()
|
||||||
@ -254,8 +255,8 @@ class AgentLoop:
|
|||||||
else defaults.tool_hint_max_length
|
else defaults.tool_hint_max_length
|
||||||
)
|
)
|
||||||
self.tools_config = _tc
|
self.tools_config = _tc
|
||||||
self.web_config = _tc.web
|
self.web_config = tool_config_by_key(_tc, "web")
|
||||||
self.exec_config = _tc.exec
|
self.exec_config = tool_config_by_key(_tc, "exec")
|
||||||
self._image_generation_provider_configs = dict(image_generation_provider_configs or {})
|
self._image_generation_provider_configs = dict(image_generation_provider_configs or {})
|
||||||
if (
|
if (
|
||||||
image_generation_provider_config is not None
|
image_generation_provider_config is not None
|
||||||
@ -439,6 +440,7 @@ class AgentLoop:
|
|||||||
|
|
||||||
def _register_default_tools(self) -> None:
|
def _register_default_tools(self) -> None:
|
||||||
"""Register the default set of tools via plugin loader."""
|
"""Register the default set of tools via plugin loader."""
|
||||||
|
from nanobot.agent.tools.config import tool_config
|
||||||
from nanobot.agent.tools.context import ToolContext
|
from nanobot.agent.tools.context import ToolContext
|
||||||
from nanobot.agent.tools.loader import ToolLoader
|
from nanobot.agent.tools.loader import ToolLoader
|
||||||
|
|
||||||
@ -459,9 +461,10 @@ class AgentLoop:
|
|||||||
registered = loader.load(ctx, self.tools)
|
registered = loader.load(ctx, self.tools)
|
||||||
|
|
||||||
# MyTool needs runtime state reference — manual registration
|
# MyTool needs runtime state reference — manual registration
|
||||||
if self.tools_config.my.enable:
|
my_config = tool_config(self.tools_config, MyTool)
|
||||||
|
if my_config.enable:
|
||||||
self.tools.register(
|
self.tools.register(
|
||||||
MyTool(runtime_state=self, modify_allowed=self.tools_config.my.allow_set)
|
MyTool(runtime_state=self, modify_allowed=my_config.allow_set)
|
||||||
)
|
)
|
||||||
registered.append("my")
|
registered.append("my")
|
||||||
|
|
||||||
|
|||||||
@ -115,10 +115,12 @@ class SubagentManager:
|
|||||||
|
|
||||||
def _subagent_tools_config(self) -> ToolsConfig:
|
def _subagent_tools_config(self) -> ToolsConfig:
|
||||||
"""Build a ToolsConfig scoped for subagent use."""
|
"""Build a ToolsConfig scoped for subagent use."""
|
||||||
|
from nanobot.agent.tools.config import tool_config_by_key
|
||||||
|
|
||||||
return ToolsConfig(
|
return ToolsConfig(
|
||||||
exec=self.tools_config.exec,
|
exec=tool_config_by_key(self.tools_config, "exec"),
|
||||||
web=self.tools_config.web,
|
web=tool_config_by_key(self.tools_config, "web"),
|
||||||
file=self.tools_config.file,
|
file=tool_config_by_key(self.tools_config, "file"),
|
||||||
restrict_to_workspace=self.restrict_to_workspace,
|
restrict_to_workspace=self.restrict_to_workspace,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
88
nanobot/agent/tools/config.py
Normal file
88
nanobot/agent/tools/config.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""Tool-owned configuration parsing helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
_CONFIG_CLASSES_BY_KEY: dict[str, type[BaseModel]] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_values(config: BaseModel) -> dict[str, Any]:
|
||||||
|
return getattr(config, "__pydantic_extra__", None) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _set_extra_value(config: BaseModel, key: str, value: Any) -> None:
|
||||||
|
setattr(config, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def _config_classes_by_key() -> dict[str, type[BaseModel]]:
|
||||||
|
global _CONFIG_CLASSES_BY_KEY
|
||||||
|
if _CONFIG_CLASSES_BY_KEY is not None:
|
||||||
|
return _CONFIG_CLASSES_BY_KEY
|
||||||
|
|
||||||
|
from nanobot.agent.tools.loader import ToolLoader
|
||||||
|
|
||||||
|
classes: dict[str, type[BaseModel]] = {}
|
||||||
|
for tool_cls in ToolLoader().discover_config_classes():
|
||||||
|
key = getattr(tool_cls, "config_key", "")
|
||||||
|
config_cls = tool_cls.config_cls()
|
||||||
|
if not key or config_cls is None:
|
||||||
|
continue
|
||||||
|
previous = classes.get(key)
|
||||||
|
if previous is not None and previous is not config_cls:
|
||||||
|
logger.warning(
|
||||||
|
"Tool config key collision for %s: %s replaces %s",
|
||||||
|
key,
|
||||||
|
config_cls.__name__,
|
||||||
|
previous.__name__,
|
||||||
|
)
|
||||||
|
classes[key] = config_cls
|
||||||
|
_CONFIG_CLASSES_BY_KEY = classes
|
||||||
|
return classes
|
||||||
|
|
||||||
|
|
||||||
|
def _materialize_config(config: BaseModel, key: str, config_cls: type[BaseModel]) -> BaseModel:
|
||||||
|
raw = _extra_values(config).get(key, None)
|
||||||
|
if isinstance(raw, config_cls):
|
||||||
|
return raw
|
||||||
|
if raw is None:
|
||||||
|
parsed = config_cls()
|
||||||
|
elif isinstance(raw, BaseModel):
|
||||||
|
parsed = config_cls.model_validate(raw.model_dump(mode="python"))
|
||||||
|
else:
|
||||||
|
parsed = config_cls.model_validate(raw)
|
||||||
|
_set_extra_value(config, key, parsed)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def tool_config(config: Any, tool_cls: type[Any]) -> Any:
|
||||||
|
"""Return the parsed config section for one tool class."""
|
||||||
|
key = getattr(tool_cls, "config_key", "")
|
||||||
|
config_cls = tool_cls.config_cls()
|
||||||
|
if not key or config_cls is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(config, BaseModel):
|
||||||
|
return getattr(config, key)
|
||||||
|
return _materialize_config(config, key, config_cls)
|
||||||
|
|
||||||
|
|
||||||
|
def tool_config_by_key(config: Any, key: str) -> Any:
|
||||||
|
"""Return the parsed config section for a tool config key."""
|
||||||
|
if not isinstance(config, BaseModel):
|
||||||
|
return getattr(config, key)
|
||||||
|
config_cls = _config_classes_by_key().get(key)
|
||||||
|
if config_cls is None:
|
||||||
|
raise KeyError(key)
|
||||||
|
return _materialize_config(config, key, config_cls)
|
||||||
|
|
||||||
|
|
||||||
|
def materialize_tool_configs(config: Any) -> Any:
|
||||||
|
"""Parse all discoverable tool config sections on a ToolsConfig object."""
|
||||||
|
if not isinstance(config, BaseModel):
|
||||||
|
return config
|
||||||
|
for key, config_cls in _config_classes_by_key().items():
|
||||||
|
_materialize_config(config, key, config_cls)
|
||||||
|
return config
|
||||||
@ -28,10 +28,15 @@ class ToolLoader:
|
|||||||
self._plugins: dict[str, type[Tool]] | None = None
|
self._plugins: dict[str, type[Tool]] | None = None
|
||||||
|
|
||||||
def discover(self) -> list[type[Tool]]:
|
def discover(self) -> list[type[Tool]]:
|
||||||
|
"""Discover concrete tools that should be registered automatically."""
|
||||||
if self._test_classes is not None:
|
if self._test_classes is not None:
|
||||||
return list(self._test_classes)
|
return list(self._test_classes)
|
||||||
if self._discovered is not None:
|
if self._discovered is not None:
|
||||||
return self._discovered
|
return self._discovered
|
||||||
|
self._discovered = self._discover_package_tools(include_non_discoverable=False)
|
||||||
|
return self._discovered
|
||||||
|
|
||||||
|
def _discover_package_tools(self, *, include_non_discoverable: bool) -> list[type[Tool]]:
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
results: list[type[Tool]] = []
|
results: list[type[Tool]] = []
|
||||||
for _importer, module_name, _ispkg in pkgutil.iter_modules(self._package.__path__):
|
for _importer, module_name, _ispkg in pkgutil.iter_modules(self._package.__path__):
|
||||||
@ -50,15 +55,23 @@ class ToolLoader:
|
|||||||
and attr is not Tool
|
and attr is not Tool
|
||||||
and not attr_name.startswith("_")
|
and not attr_name.startswith("_")
|
||||||
and not getattr(attr, "__abstractmethods__", None)
|
and not getattr(attr, "__abstractmethods__", None)
|
||||||
and getattr(attr, "_plugin_discoverable", True)
|
and (include_non_discoverable or getattr(attr, "_plugin_discoverable", True))
|
||||||
and id(attr) not in seen
|
and id(attr) not in seen
|
||||||
):
|
):
|
||||||
seen.add(id(attr))
|
seen.add(id(attr))
|
||||||
results.append(attr)
|
results.append(attr)
|
||||||
results.sort(key=lambda cls: cls.__name__)
|
results.sort(key=lambda cls: cls.__name__)
|
||||||
self._discovered = results
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def discover_config_classes(self) -> list[type[Tool]]:
|
||||||
|
"""Discover tool classes that declare owned config models."""
|
||||||
|
classes = self._discover_package_tools(include_non_discoverable=True)
|
||||||
|
classes.extend(self._discover_plugins().values())
|
||||||
|
return [
|
||||||
|
cls for cls in classes
|
||||||
|
if getattr(cls, "config_key", "") and cls.config_cls() is not None
|
||||||
|
]
|
||||||
|
|
||||||
def _discover_plugins(self) -> dict[str, type[Tool]]:
|
def _discover_plugins(self) -> dict[str, type[Tool]]:
|
||||||
"""Discover external tool plugins registered via entry_points."""
|
"""Discover external tool plugins registered via entry_points."""
|
||||||
if self._plugins is not None:
|
if self._plugins is not None:
|
||||||
@ -84,6 +97,8 @@ class ToolLoader:
|
|||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
def load(self, ctx: Any, registry: ToolRegistry, *, scope: str = "core") -> list[str]:
|
def load(self, ctx: Any, registry: ToolRegistry, *, scope: str = "core") -> list[str]:
|
||||||
|
from nanobot.agent.tools.config import tool_config
|
||||||
|
|
||||||
registered: list[str] = []
|
registered: list[str] = []
|
||||||
builtin_names: set[str] = set()
|
builtin_names: set[str] = set()
|
||||||
sources = [(self.discover(), False), (self._discover_plugins().values(), True)]
|
sources = [(self.discover(), False), (self._discover_plugins().values(), True)]
|
||||||
@ -93,6 +108,7 @@ class ToolLoader:
|
|||||||
try:
|
try:
|
||||||
if scope not in getattr(tool_cls, "_scopes", {"core"}):
|
if scope not in getattr(tool_cls, "_scopes", {"core"}):
|
||||||
continue
|
continue
|
||||||
|
tool_config(ctx.config, tool_cls)
|
||||||
if not tool_cls.enabled(ctx):
|
if not tool_cls.enabled(ctx):
|
||||||
continue
|
continue
|
||||||
tool = tool_cls.create(ctx)
|
tool = tool_cls.create(ctx)
|
||||||
|
|||||||
@ -50,6 +50,12 @@ def load_config(config_path: Path | None = None) -> Config:
|
|||||||
except (json.JSONDecodeError, ValueError, pydantic.ValidationError) as e:
|
except (json.JSONDecodeError, ValueError, pydantic.ValidationError) as e:
|
||||||
raise ValueError(f"Failed to load config from {path}: {e}") from e
|
raise ValueError(f"Failed to load config from {path}: {e}") from e
|
||||||
|
|
||||||
|
from nanobot.agent.tools.config import materialize_tool_configs
|
||||||
|
|
||||||
|
try:
|
||||||
|
materialize_tool_configs(config.tools)
|
||||||
|
except (ValueError, pydantic.ValidationError) as e:
|
||||||
|
raise ValueError(f"Failed to load config from {path}: {e}") from e
|
||||||
_apply_ssrf_whitelist(config)
|
_apply_ssrf_whitelist(config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@ -72,6 +78,9 @@ def save_config(config: Config, config_path: Path | None = None) -> None:
|
|||||||
path = config_path or get_config_path()
|
path = config_path or get_config_path()
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
from nanobot.agent.tools.config import materialize_tool_configs
|
||||||
|
|
||||||
|
materialize_tool_configs(config.tools)
|
||||||
data = config.model_dump(mode="json", by_alias=True)
|
data = config.model_dump(mode="json", by_alias=True)
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from pydantic import AliasChoices, ConfigDict, Field, model_validator
|
from pydantic import AliasChoices, ConfigDict, Field, model_validator
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
@ -10,26 +10,6 @@ from pydantic_settings import BaseSettings
|
|||||||
from nanobot.config_base import Base
|
from nanobot.config_base import Base
|
||||||
from nanobot.cron.types import CronSchedule
|
from nanobot.cron.types import CronSchedule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from nanobot.agent.tools.cli_apps import CliAppsToolConfig
|
|
||||||
from nanobot.agent.tools.filesystem import FileToolsConfig
|
|
||||||
from nanobot.agent.tools.image_generation import ImageGenerationToolConfig
|
|
||||||
from nanobot.agent.tools.self import MyToolConfig
|
|
||||||
from nanobot.agent.tools.shell import ExecToolConfig
|
|
||||||
from nanobot.agent.tools.web import WebToolsConfig
|
|
||||||
|
|
||||||
_TOOL_CONFIG_REF_NAMES = frozenset({
|
|
||||||
"CliAppsToolConfig",
|
|
||||||
"ExecToolConfig",
|
|
||||||
"FileToolsConfig",
|
|
||||||
"ImageGenerationToolConfig",
|
|
||||||
"MyToolConfig",
|
|
||||||
"WebFetchConfig",
|
|
||||||
"WebSearchConfig",
|
|
||||||
"WebToolsConfig",
|
|
||||||
})
|
|
||||||
_tool_config_refs_ready = False
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelsConfig(Base):
|
class ChannelsConfig(Base):
|
||||||
"""Configuration for chat channels.
|
"""Configuration for chat channels.
|
||||||
@ -316,29 +296,16 @@ class MCPServerConfig(Base):
|
|||||||
enabled_tools: list[str] = Field(default_factory=lambda: ["*"]) # Only register these tools; accepts raw MCP names or wrapped mcp_<server>_<tool> names; ["*"] = all tools; [] = no tools
|
enabled_tools: list[str] = Field(default_factory=lambda: ["*"]) # Only register these tools; accepts raw MCP names or wrapped mcp_<server>_<tool> names; ["*"] = all tools; [] = no tools
|
||||||
|
|
||||||
|
|
||||||
def _lazy_default(module_path: str, class_name: str) -> Any:
|
|
||||||
"""Deferred import helper for ToolsConfig default factories."""
|
|
||||||
import importlib
|
|
||||||
module = importlib.import_module(module_path)
|
|
||||||
return getattr(module, class_name)()
|
|
||||||
|
|
||||||
|
|
||||||
class ToolsConfig(Base):
|
class ToolsConfig(Base):
|
||||||
"""Tools configuration.
|
"""Tools configuration.
|
||||||
|
|
||||||
Field types for tool-specific sub-configs are resolved via model_rebuild()
|
Concrete tool sub-configs are stored as extra fields and parsed by the
|
||||||
at the bottom of this file so tool config classes can stay next to their
|
owning tool module when tools are loaded. This keeps the root schema from
|
||||||
tool implementations.
|
importing or naming concrete tool configuration classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
web: WebToolsConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.web", "WebToolsConfig"))
|
model_config = ConfigDict(extra="allow")
|
||||||
exec: ExecToolConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.shell", "ExecToolConfig"))
|
|
||||||
file: FileToolsConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.filesystem", "FileToolsConfig"))
|
|
||||||
cli_apps: CliAppsToolConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.cli_apps", "CliAppsToolConfig"))
|
|
||||||
my: MyToolConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.self", "MyToolConfig"))
|
|
||||||
image_generation: ImageGenerationToolConfig = Field(
|
|
||||||
default_factory=lambda: _lazy_default("nanobot.agent.tools.image_generation", "ImageGenerationToolConfig"),
|
|
||||||
)
|
|
||||||
restrict_to_workspace: bool = False # policy intent: keep tool access inside workspace when possible
|
restrict_to_workspace: bool = False # policy intent: keep tool access inside workspace when possible
|
||||||
webui_allow_local_service_access: bool = Field(
|
webui_allow_local_service_access: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
@ -352,16 +319,18 @@ class ToolsConfig(Base):
|
|||||||
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
||||||
ssrf_whitelist: list[str] = Field(default_factory=list) # CIDR ranges to exempt from SSRF blocking (e.g. ["100.64.0.0/10"] for Tailscale)
|
ssrf_whitelist: list[str] = Field(default_factory=list) # CIDR ranges to exempt from SSRF blocking (e.g. ["100.64.0.0/10"] for Tailscale)
|
||||||
|
|
||||||
def __init__(self, **values: Any) -> None:
|
def __getattr__(self, name: str) -> Any:
|
||||||
if not type(self).__pydantic_complete__:
|
try:
|
||||||
_resolve_tool_config_refs()
|
return super().__getattr__(name)
|
||||||
super().__init__(**values)
|
except AttributeError as exc:
|
||||||
|
if name.startswith("_"):
|
||||||
|
raise
|
||||||
|
from nanobot.agent.tools.config import tool_config_by_key
|
||||||
|
|
||||||
@classmethod
|
try:
|
||||||
def model_validate(cls, obj: Any, *args: Any, **kwargs: Any) -> "ToolsConfig":
|
return tool_config_by_key(self, name)
|
||||||
if not cls.__pydantic_complete__:
|
except KeyError:
|
||||||
_resolve_tool_config_refs()
|
raise exc from None
|
||||||
return super().model_validate(obj, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseSettings):
|
||||||
@ -379,17 +348,6 @@ class Config(BaseSettings):
|
|||||||
validation_alias=AliasChoices("modelPresets", "model_presets"),
|
validation_alias=AliasChoices("modelPresets", "model_presets"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, **values: Any) -> None:
|
|
||||||
if not type(self).__pydantic_complete__:
|
|
||||||
_resolve_tool_config_refs()
|
|
||||||
super().__init__(**values)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_validate(cls, obj: Any, *args: Any, **kwargs: Any) -> "Config":
|
|
||||||
if not cls.__pydantic_complete__:
|
|
||||||
_resolve_tool_config_refs()
|
|
||||||
return super().model_validate(obj, *args, **kwargs)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _validate_model_preset(self) -> "Config":
|
def _validate_model_preset(self) -> "Config":
|
||||||
if "default" in self.model_presets:
|
if "default" in self.model_presets:
|
||||||
@ -577,47 +535,3 @@ class Config(BaseSettings):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
model_config = ConfigDict(env_prefix="NANOBOT_", env_nested_delimiter="__")
|
model_config = ConfigDict(env_prefix="NANOBOT_", env_nested_delimiter="__")
|
||||||
|
|
||||||
|
|
||||||
def _resolve_tool_config_refs() -> None:
|
|
||||||
"""Resolve forward references in ToolsConfig by importing tool config classes.
|
|
||||||
|
|
||||||
Must be called after all modules are loaded (breaks circular imports).
|
|
||||||
Re-exports the classes into this module's namespace so existing imports
|
|
||||||
like ``from nanobot.config.schema import ExecToolConfig`` continue to work.
|
|
||||||
"""
|
|
||||||
global _tool_config_refs_ready
|
|
||||||
if _tool_config_refs_ready:
|
|
||||||
return
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from nanobot.agent.tools.cli_apps import CliAppsToolConfig
|
|
||||||
from nanobot.agent.tools.filesystem import FileToolsConfig
|
|
||||||
from nanobot.agent.tools.image_generation import ImageGenerationToolConfig
|
|
||||||
from nanobot.agent.tools.self import MyToolConfig
|
|
||||||
from nanobot.agent.tools.shell import ExecToolConfig
|
|
||||||
from nanobot.agent.tools.web import WebFetchConfig, WebSearchConfig, WebToolsConfig
|
|
||||||
|
|
||||||
# Re-export into this module's namespace
|
|
||||||
mod = sys.modules[__name__]
|
|
||||||
mod.ExecToolConfig = ExecToolConfig # type: ignore[attr-defined]
|
|
||||||
mod.FileToolsConfig = FileToolsConfig # type: ignore[attr-defined]
|
|
||||||
mod.CliAppsToolConfig = CliAppsToolConfig # type: ignore[attr-defined]
|
|
||||||
mod.WebToolsConfig = WebToolsConfig # type: ignore[attr-defined]
|
|
||||||
mod.WebSearchConfig = WebSearchConfig # type: ignore[attr-defined]
|
|
||||||
mod.WebFetchConfig = WebFetchConfig # type: ignore[attr-defined]
|
|
||||||
mod.MyToolConfig = MyToolConfig # type: ignore[attr-defined]
|
|
||||||
mod.ImageGenerationToolConfig = ImageGenerationToolConfig # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
ToolsConfig.model_rebuild()
|
|
||||||
Config.model_rebuild()
|
|
||||||
_tool_config_refs_ready = True
|
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> Any:
|
|
||||||
if name in _TOOL_CONFIG_REF_NAMES:
|
|
||||||
_resolve_tool_config_refs()
|
|
||||||
if name in globals():
|
|
||||||
return globals()[name]
|
|
||||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
||||||
|
|||||||
@ -7,10 +7,11 @@ from unittest.mock import AsyncMock, MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent.loop import AgentLoop
|
from nanobot.agent.loop import AgentLoop
|
||||||
|
from nanobot.agent.tools.image_generation import ImageGenerationToolConfig
|
||||||
from nanobot.bus.events import InboundMessage
|
from nanobot.bus.events import InboundMessage
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.config.loader import set_config_path
|
from nanobot.config.loader import set_config_path
|
||||||
from nanobot.config.schema import ImageGenerationToolConfig, ProviderConfig, ToolsConfig
|
from nanobot.config.schema import ProviderConfig, ToolsConfig
|
||||||
from nanobot.providers.base import LLMResponse, ToolCallRequest
|
from nanobot.providers.base import LLMResponse, ToolCallRequest
|
||||||
from nanobot.providers.image_generation import GeneratedImageResponse
|
from nanobot.providers.image_generation import GeneratedImageResponse
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,16 @@ import pytest
|
|||||||
|
|
||||||
from nanobot.agent.tools.cli_apps import CliAppsTool
|
from nanobot.agent.tools.cli_apps import CliAppsTool
|
||||||
from nanobot.agent.tools.filesystem import ReadFileTool
|
from nanobot.agent.tools.filesystem import ReadFileTool
|
||||||
from nanobot.agent.tools.image_generation import ImageGenerationError, ImageGenerationTool
|
from nanobot.agent.tools.image_generation import (
|
||||||
|
ImageGenerationError,
|
||||||
|
ImageGenerationTool,
|
||||||
|
ImageGenerationToolConfig,
|
||||||
|
)
|
||||||
from nanobot.agent.tools.message import MessageTool
|
from nanobot.agent.tools.message import MessageTool
|
||||||
from nanobot.agent.tools.shell import ExecTool
|
from nanobot.agent.tools.shell import ExecTool
|
||||||
from nanobot.agent.tools.spawn import SpawnTool
|
from nanobot.agent.tools.spawn import SpawnTool
|
||||||
|
from nanobot.apps.cli.service import CliAppManager, CliAppsRuntimeConfig
|
||||||
|
from nanobot.config.schema import ProviderConfig
|
||||||
from nanobot.security.workspace_access import (
|
from nanobot.security.workspace_access import (
|
||||||
WORKSPACE_SCOPE_METADATA_KEY,
|
WORKSPACE_SCOPE_METADATA_KEY,
|
||||||
WorkspaceScopeError,
|
WorkspaceScopeError,
|
||||||
@ -20,8 +26,6 @@ from nanobot.security.workspace_access import (
|
|||||||
validate_workspace_scope_payload,
|
validate_workspace_scope_payload,
|
||||||
workspace_scope_from_metadata,
|
workspace_scope_from_metadata,
|
||||||
)
|
)
|
||||||
from nanobot.apps.cli.service import CliAppManager, CliAppsRuntimeConfig
|
|
||||||
from nanobot.config.schema import ImageGenerationToolConfig, ProviderConfig
|
|
||||||
|
|
||||||
PNG_BYTES = (
|
PNG_BYTES = (
|
||||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
|
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
|
||||||
|
|||||||
@ -20,6 +20,32 @@ print("nanobot.config.schema" in sys.modules)
|
|||||||
assert result.stdout.strip() == "False"
|
assert result.stdout.strip() == "False"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_schema_import_does_not_load_builtin_tool_modules():
|
||||||
|
code = """
|
||||||
|
import sys
|
||||||
|
import nanobot.config.schema
|
||||||
|
print(any(
|
||||||
|
name in sys.modules
|
||||||
|
for name in (
|
||||||
|
"nanobot.agent.tools.cli_apps",
|
||||||
|
"nanobot.agent.tools.filesystem",
|
||||||
|
"nanobot.agent.tools.image_generation",
|
||||||
|
"nanobot.agent.tools.self",
|
||||||
|
"nanobot.agent.tools.shell",
|
||||||
|
"nanobot.agent.tools.web",
|
||||||
|
)
|
||||||
|
))
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.stdout.strip() == "False"
|
||||||
|
|
||||||
|
|
||||||
def test_builtin_tool_configs_do_not_depend_on_config_schema_base():
|
def test_builtin_tool_configs_do_not_depend_on_config_schema_base():
|
||||||
repo = Path(__file__).resolve().parents[2]
|
repo = Path(__file__).resolve().parents[2]
|
||||||
tool_paths = sorted((repo / "nanobot/agent/tools").glob("*.py"))
|
tool_paths = sorted((repo / "nanobot/agent/tools").glob("*.py"))
|
||||||
|
|||||||
@ -6,9 +6,9 @@ from typing import Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent.tools.image_generation import ImageGenerationTool
|
from nanobot.agent.tools.image_generation import ImageGenerationTool, ImageGenerationToolConfig
|
||||||
from nanobot.config.loader import set_config_path
|
from nanobot.config.loader import set_config_path
|
||||||
from nanobot.config.schema import ImageGenerationToolConfig, ProviderConfig
|
from nanobot.config.schema import ProviderConfig
|
||||||
from nanobot.providers.image_generation import GeneratedImageResponse
|
from nanobot.providers.image_generation import GeneratedImageResponse
|
||||||
|
|
||||||
PNG_BYTES = (
|
PNG_BYTES = (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user