Break tool config schema import cycle

This commit is contained in:
chengyongru
2026-06-13 21:59:07 +08:00
committed by Xubin Ren
parent 7ff8e02eaf
commit 3a221d74cf
9 changed files with 62 additions and 59 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ from nanobot.agent.tools.base import Tool, tool_parameters
from nanobot.agent.tools.schema import ArraySchema, BooleanSchema, IntegerSchema, StringSchema, tool_parameters_schema from nanobot.agent.tools.schema import ArraySchema, BooleanSchema, IntegerSchema, StringSchema, tool_parameters_schema
from nanobot.security.workspace_access import current_tool_workspace from nanobot.security.workspace_access import current_tool_workspace
from nanobot.apps.cli import CliAppError, CliAppManager, CliAppsRuntimeConfig from nanobot.apps.cli import CliAppError, CliAppManager, CliAppsRuntimeConfig
from nanobot.config.schema import Base from nanobot.config_base import Base
class CliAppsToolConfig(Base): class CliAppsToolConfig(Base):
+1 -1
View File
@@ -16,7 +16,7 @@ from nanobot.agent.tools.schema import (
) )
from nanobot.security.workspace_access import current_tool_workspace from nanobot.security.workspace_access import current_tool_workspace
from nanobot.config.paths import get_media_dir from nanobot.config.paths import get_media_dir
from nanobot.config.schema import Base from nanobot.config_base import Base
from nanobot.providers.image_generation import ( from nanobot.providers.image_generation import (
ImageGenerationError, ImageGenerationError,
ImageGenerationProvider, ImageGenerationProvider,
+1 -1
View File
@@ -10,7 +10,7 @@ from loguru import logger
from nanobot.agent.tools.base import Tool from nanobot.agent.tools.base import Tool
from nanobot.agent.tools.context import ContextAware, RequestContext from nanobot.agent.tools.context import ContextAware, RequestContext
from nanobot.agent.tools.runtime_state import RuntimeState from nanobot.agent.tools.runtime_state import RuntimeState
from nanobot.config.schema import Base from nanobot.config_base import Base
if TYPE_CHECKING: if TYPE_CHECKING:
from nanobot.agent.subagent import SubagentStatus from nanobot.agent.subagent import SubagentStatus
+1 -1
View File
@@ -34,7 +34,7 @@ from nanobot.agent.tools.schema import (
tool_parameters_schema, tool_parameters_schema,
) )
from nanobot.config.paths import get_media_dir from nanobot.config.paths import get_media_dir
from nanobot.config.schema import Base from nanobot.config_base import Base
from nanobot.security.workspace_access import current_scope_allows_loopback, current_tool_workspace from nanobot.security.workspace_access import current_scope_allows_loopback, current_tool_workspace
from nanobot.security.workspace_policy import is_path_within from nanobot.security.workspace_policy import is_path_within
+1 -1
View File
@@ -21,7 +21,7 @@ from nanobot.agent.tools.schema import (
StringSchema, StringSchema,
tool_parameters_schema, tool_parameters_schema,
) )
from nanobot.config.schema import Base from nanobot.config_base import Base
from nanobot.utils.helpers import build_image_content_blocks from nanobot.utils.helpers import build_image_content_blocks
# Shared constants # Shared constants
+4 -10
View File
@@ -4,10 +4,10 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal from typing import TYPE_CHECKING, Any, Literal
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator from pydantic import AliasChoices, ConfigDict, Field, model_validator
from pydantic.alias_generators import to_camel
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from nanobot.config_base import Base
from nanobot.cron.types import CronSchedule from nanobot.cron.types import CronSchedule
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -18,12 +18,6 @@ if TYPE_CHECKING:
from nanobot.agent.tools.web import WebToolsConfig from nanobot.agent.tools.web import WebToolsConfig
class Base(BaseModel):
"""Base model that accepts both camelCase and snake_case keys."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class ChannelsConfig(Base): class ChannelsConfig(Base):
"""Configuration for chat channels. """Configuration for chat channels.
@@ -320,8 +314,8 @@ class ToolsConfig(Base):
"""Tools configuration. """Tools configuration.
Field types for tool-specific sub-configs are resolved via model_rebuild() Field types for tool-specific sub-configs are resolved via model_rebuild()
at the bottom of this file to avoid circular imports (tool modules import at the bottom of this file so tool config classes can stay next to their
Base from schema.py). tool implementations.
""" """
web: WebToolsConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.web", "WebToolsConfig")) web: WebToolsConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.web", "WebToolsConfig"))
+15
View File
@@ -0,0 +1,15 @@
"""Shared Pydantic base model for configuration DTOs.
This module intentionally lives outside the ``nanobot.config`` package so
runtime modules can define local config DTOs without importing the full root
configuration schema.
"""
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class Base(BaseModel):
"""Base model that accepts both camelCase and snake_case keys."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -0,0 +1,38 @@
import ast
import subprocess
import sys
from pathlib import Path
def test_config_base_import_does_not_load_config_schema():
code = """
import sys
from nanobot.config_base import Base
print("nanobot.config.schema" in sys.modules)
"""
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():
repo = Path(__file__).resolve().parents[2]
tool_paths = sorted((repo / "nanobot/agent/tools").glob("*.py"))
violations = []
for path in tool_paths:
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not isinstance(node, ast.ImportFrom):
continue
if node.module != "nanobot.config.schema":
continue
if any(alias.name == "Base" for alias in node.names):
violations.append(str(path.relative_to(repo)))
assert violations == []
-44
View File
@@ -352,50 +352,6 @@ def test_mcp_wrappers_not_discoverable():
assert MCPPromptWrapper._plugin_discoverable is False assert MCPPromptWrapper._plugin_discoverable is False
# --- Task 8: Config round-trip tests ---
def test_config_round_trip():
"""Verify config serialization is unchanged after moving config classes."""
from nanobot.config.schema import Config
config_dict = {
"tools": {
"web": {"enable": True, "search": {"provider": "brave", "api_key": "test"}},
"exec": {"enable": False, "timeout": 120, "pathPrepend": "/venv/bin"},
"my": {"allowSet": True},
"imageGeneration": {"enabled": True, "provider": "openrouter"},
}
}
config = Config.model_validate(config_dict)
dumped = config.model_dump(mode="json", by_alias=True)
assert dumped["tools"]["my"]["allowSet"] is True
assert dumped["tools"]["imageGeneration"]["enabled"] is True
assert dumped["tools"]["exec"]["pathPrepend"] == "/venv/bin"
assert config.tools.exec.enable is False
assert config.tools.exec.timeout == 120
assert config.tools.exec.path_prepend == "/venv/bin"
assert config.tools.web.search.provider == "brave"
def test_config_defaults():
"""Verify default values match the original hardcoded schema."""
from nanobot.config.schema import Config
config = Config.model_validate({})
assert config.tools.exec.enable is True
assert config.tools.exec.timeout == 60
assert config.tools.exec.path_prepend == ""
assert config.tools.web.enable is True
assert config.tools.web.search.provider == "duckduckgo"
assert config.tools.my.enable is True
assert config.tools.my.allow_set is False
assert config.tools.image_generation.enabled is False
assert config.tools.cli_apps.enable is True
assert config.tools.restrict_to_workspace is False
# --- Task 10: Integration test --- # --- Task 10: Integration test ---