onetool-mcp 1.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
ot/config/mcp.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""MCP server configuration for OneTool proxy.
|
|
2
|
+
|
|
3
|
+
Defines configuration for connecting to external MCP servers that are
|
|
4
|
+
proxied through OneTool's single `run` tool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
|
+
|
|
14
|
+
# Use canonical early secrets loader from secrets.py (eliminates duplication)
|
|
15
|
+
from ot.config.secrets import get_early_secret
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def expand_secrets(value: str) -> str:
|
|
19
|
+
"""Expand ${VAR} patterns using secrets.yaml ONLY.
|
|
20
|
+
|
|
21
|
+
Use this for configuration values that MUST be in secrets.yaml.
|
|
22
|
+
This enforces that sensitive values are stored in the gitignored secrets file,
|
|
23
|
+
not in environment variables that might leak into logs or process lists.
|
|
24
|
+
|
|
25
|
+
Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax.
|
|
26
|
+
|
|
27
|
+
When to use:
|
|
28
|
+
- Config file values (URLs, API keys, database connections)
|
|
29
|
+
- Anywhere secrets should be explicit and fail loudly if missing
|
|
30
|
+
|
|
31
|
+
When NOT to use:
|
|
32
|
+
- Subprocess environment pass-through (use expand_subprocess_env instead)
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
value: String potentially containing ${VAR} patterns.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
String with variables expanded from secrets.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If variable not found in secrets and no default provided.
|
|
42
|
+
"""
|
|
43
|
+
pattern = re.compile(r"\$\{([^}:]+)(?::-([^}]*))?\}")
|
|
44
|
+
missing_vars: list[str] = []
|
|
45
|
+
|
|
46
|
+
def replace(match: re.Match[str]) -> str:
|
|
47
|
+
var_name = match.group(1)
|
|
48
|
+
default_value = match.group(2)
|
|
49
|
+
# Read from secrets only - no os.environ
|
|
50
|
+
secret_value = get_early_secret(var_name)
|
|
51
|
+
if secret_value is not None:
|
|
52
|
+
return secret_value
|
|
53
|
+
if default_value is not None:
|
|
54
|
+
return default_value
|
|
55
|
+
missing_vars.append(var_name)
|
|
56
|
+
return match.group(0)
|
|
57
|
+
|
|
58
|
+
result = pattern.sub(replace, value)
|
|
59
|
+
|
|
60
|
+
if missing_vars:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Missing variables in secrets.yaml: {', '.join(missing_vars)}. "
|
|
63
|
+
f"Add them to .onetool/secrets.yaml or use ${{VAR:-default}} syntax."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def expand_subprocess_env(value: str) -> str:
|
|
70
|
+
"""Expand ${VAR} for subprocess environment variables.
|
|
71
|
+
|
|
72
|
+
Use this ONLY for subprocess env configuration where pass-through is needed.
|
|
73
|
+
Searches: secrets.yaml first, then os.environ. Returns empty string if not found.
|
|
74
|
+
|
|
75
|
+
This is the ONLY place where reading os.environ is allowed. This enables
|
|
76
|
+
explicit pass-through of system environment variables like ${HOME}, ${PATH},
|
|
77
|
+
or ${USER} to subprocesses without requiring them to be in secrets.yaml.
|
|
78
|
+
|
|
79
|
+
When to use:
|
|
80
|
+
- MCP server 'env' configuration (subprocess environment)
|
|
81
|
+
- Any subprocess that needs access to system env vars
|
|
82
|
+
|
|
83
|
+
When NOT to use:
|
|
84
|
+
- Config file values (use expand_secrets instead - it enforces secrets.yaml)
|
|
85
|
+
- Anything that should fail if the secret is missing
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
value: String potentially containing ${VAR} patterns.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
String with variables expanded. Empty string if not found (silent failure).
|
|
92
|
+
"""
|
|
93
|
+
import os
|
|
94
|
+
|
|
95
|
+
pattern = re.compile(r"\$\{([^}:]+)(?::-([^}]*))?\}")
|
|
96
|
+
|
|
97
|
+
def replace(match: re.Match[str]) -> str:
|
|
98
|
+
var_name = match.group(1)
|
|
99
|
+
default_value = match.group(2)
|
|
100
|
+
# Secrets first
|
|
101
|
+
secret_value = get_early_secret(var_name)
|
|
102
|
+
if secret_value is not None:
|
|
103
|
+
return secret_value
|
|
104
|
+
# Then os.environ (for pass-through like ${HOME})
|
|
105
|
+
env_val = os.environ.get(var_name)
|
|
106
|
+
if env_val is not None:
|
|
107
|
+
return env_val
|
|
108
|
+
# Use default if provided
|
|
109
|
+
if default_value is not None:
|
|
110
|
+
return default_value
|
|
111
|
+
# Empty string if not found
|
|
112
|
+
return ""
|
|
113
|
+
|
|
114
|
+
return pattern.sub(replace, value)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class McpServerConfig(BaseModel):
|
|
118
|
+
"""Configuration for an MCP server connection.
|
|
119
|
+
|
|
120
|
+
Compatible with bench ServerConfig format, with additional
|
|
121
|
+
`enabled` field for toggling servers without removing config.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
type: Literal["http", "stdio"] = Field(description="Server connection type")
|
|
125
|
+
enabled: bool = Field(default=True, description="Whether this server is enabled")
|
|
126
|
+
url: str | None = Field(default=None, description="URL for HTTP servers")
|
|
127
|
+
headers: dict[str, str] = Field(
|
|
128
|
+
default_factory=dict, description="Headers for HTTP servers"
|
|
129
|
+
)
|
|
130
|
+
command: str | None = Field(default=None, description="Command for stdio servers")
|
|
131
|
+
args: list[str] = Field(
|
|
132
|
+
default_factory=list, description="Arguments for stdio command"
|
|
133
|
+
)
|
|
134
|
+
env: dict[str, str] = Field(
|
|
135
|
+
default_factory=dict, description="Environment variables for stdio servers"
|
|
136
|
+
)
|
|
137
|
+
timeout: int = Field(default=30, description="Connection timeout in seconds")
|
|
138
|
+
|
|
139
|
+
@field_validator("url", "command", mode="before")
|
|
140
|
+
@classmethod
|
|
141
|
+
def expand_secrets_validator(cls, v: str | None) -> str | None:
|
|
142
|
+
"""Expand ${VAR} from secrets.yaml in URL and command."""
|
|
143
|
+
if v is None:
|
|
144
|
+
return None
|
|
145
|
+
return expand_secrets(v)
|
ot/config/secrets.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Secrets loading for OneTool.
|
|
2
|
+
|
|
3
|
+
Loads secrets from secrets.yaml (gitignored) separate from committed configuration.
|
|
4
|
+
Secrets are passed to workers via JSON-RPC, not exposed as environment variables.
|
|
5
|
+
|
|
6
|
+
The secrets file path is resolved in order:
|
|
7
|
+
1. Explicit path passed to get_secrets()
|
|
8
|
+
2. Config's secrets_file setting (if config loaded and file exists)
|
|
9
|
+
3. Default locations: project (.onetool/config/secrets.yaml) then global (~/.onetool/config/secrets.yaml)
|
|
10
|
+
4. OT_SECRETS_FILE environment variable
|
|
11
|
+
|
|
12
|
+
Example secrets.yaml:
|
|
13
|
+
|
|
14
|
+
BRAVE_API_KEY: "your-brave-api-key"
|
|
15
|
+
OPENAI_API_KEY: "sk-..."
|
|
16
|
+
DATABASE_URL: "postgresql://..."
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
# Single global secrets cache
|
|
28
|
+
_secrets: dict[str, str] | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_secrets(secrets_path: Path | str | None = None) -> dict[str, str]:
|
|
32
|
+
"""Load secrets from YAML file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
secrets_path: Path to secrets file. If None or doesn't exist,
|
|
36
|
+
returns empty dict (no secrets).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary of secret name -> value
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If YAML is invalid
|
|
43
|
+
"""
|
|
44
|
+
if secrets_path is None:
|
|
45
|
+
logger.debug("No secrets path provided")
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
secrets_path = Path(secrets_path)
|
|
49
|
+
|
|
50
|
+
if not secrets_path.exists():
|
|
51
|
+
logger.debug(f"Secrets file not found: {secrets_path}")
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
logger.debug(f"Loading secrets from {secrets_path}")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with secrets_path.open() as f:
|
|
58
|
+
raw_data = yaml.safe_load(f)
|
|
59
|
+
except yaml.YAMLError as e:
|
|
60
|
+
raise ValueError(f"Invalid YAML in secrets file {secrets_path}: {e}") from e
|
|
61
|
+
except OSError as e:
|
|
62
|
+
raise ValueError(f"Error reading secrets file {secrets_path}: {e}") from e
|
|
63
|
+
|
|
64
|
+
if raw_data is None:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
if not isinstance(raw_data, dict):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Secrets file {secrets_path} must be a YAML mapping, not {type(raw_data).__name__}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Values are literal - no env var expansion
|
|
73
|
+
secrets: dict[str, str] = {}
|
|
74
|
+
for key, value in raw_data.items():
|
|
75
|
+
if not isinstance(key, str):
|
|
76
|
+
logger.warning(f"Ignoring non-string secret key: {key}")
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if value is None:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Store as literal string - no ${VAR} expansion
|
|
83
|
+
secrets[key] = str(value)
|
|
84
|
+
|
|
85
|
+
logger.info(f"Loaded {len(secrets)} secrets")
|
|
86
|
+
return secrets
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _load_from_default_locations() -> dict[str, str]:
|
|
90
|
+
"""Load secrets from default project and global locations.
|
|
91
|
+
|
|
92
|
+
Searches in order (first found wins):
|
|
93
|
+
1. Project: {effective_cwd}/.onetool/config/secrets.yaml
|
|
94
|
+
2. Global: ~/.onetool/config/secrets.yaml
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary of secret name -> value (empty if no secrets found)
|
|
98
|
+
"""
|
|
99
|
+
# Import here to avoid circular imports at module level
|
|
100
|
+
from ot.paths import CONFIG_SUBDIR, get_effective_cwd, get_global_dir
|
|
101
|
+
|
|
102
|
+
# Try project secrets first, then global
|
|
103
|
+
paths_to_try = [
|
|
104
|
+
get_effective_cwd() / ".onetool" / CONFIG_SUBDIR / "secrets.yaml",
|
|
105
|
+
get_global_dir() / CONFIG_SUBDIR / "secrets.yaml",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
for secrets_path in paths_to_try:
|
|
109
|
+
if secrets_path.exists():
|
|
110
|
+
try:
|
|
111
|
+
return load_secrets(secrets_path)
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
# Silent during bootstrap - don't spam logs
|
|
114
|
+
logger.debug(f"Error loading secrets from {secrets_path}: {e}")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_secrets(
|
|
121
|
+
secrets_path: Path | str | None = None, reload: bool = False
|
|
122
|
+
) -> dict[str, str]:
|
|
123
|
+
"""Get or load the cached secrets.
|
|
124
|
+
|
|
125
|
+
Resolution order (first match wins):
|
|
126
|
+
1. Explicit secrets_path argument
|
|
127
|
+
2. Config's secrets_file setting (if config loaded and file exists)
|
|
128
|
+
3. Default locations (.onetool/config/secrets.yaml)
|
|
129
|
+
4. OT_SECRETS_FILE environment variable
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
secrets_path: Path to secrets file (only used on first load or reload).
|
|
133
|
+
reload: Force reload secrets from disk.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary of secret name -> value
|
|
137
|
+
"""
|
|
138
|
+
global _secrets
|
|
139
|
+
|
|
140
|
+
if _secrets is None or reload:
|
|
141
|
+
# Resolution chain: explicit > config (if loaded) > defaults > env var
|
|
142
|
+
if secrets_path is None:
|
|
143
|
+
# WARNING: Do NOT call get_config() here!
|
|
144
|
+
# =========================================
|
|
145
|
+
# This function is called during config loading via:
|
|
146
|
+
# get_config() → load_config() → expand_secrets() → get_early_secret() → get_secrets()
|
|
147
|
+
#
|
|
148
|
+
# If we call get_config() here, it triggers config loading again → infinite recursion.
|
|
149
|
+
# Instead, we check _config directly - if it's None, config is still loading.
|
|
150
|
+
try:
|
|
151
|
+
import ot.config.loader
|
|
152
|
+
|
|
153
|
+
if ot.config.loader._config is not None:
|
|
154
|
+
config_path = ot.config.loader._config.get_secrets_file_path()
|
|
155
|
+
if config_path.exists():
|
|
156
|
+
secrets_path = config_path
|
|
157
|
+
except Exception:
|
|
158
|
+
pass # Module not loaded yet, fall through
|
|
159
|
+
|
|
160
|
+
# Try default locations if still no path
|
|
161
|
+
if secrets_path is None:
|
|
162
|
+
loaded = _load_from_default_locations()
|
|
163
|
+
if loaded:
|
|
164
|
+
_secrets = loaded
|
|
165
|
+
return _secrets
|
|
166
|
+
|
|
167
|
+
# Final fallback to OT_SECRETS_FILE env var
|
|
168
|
+
if secrets_path is None:
|
|
169
|
+
secrets_path = os.getenv("OT_SECRETS_FILE")
|
|
170
|
+
|
|
171
|
+
_secrets = load_secrets(secrets_path)
|
|
172
|
+
|
|
173
|
+
return _secrets
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_secret(name: str) -> str | None:
|
|
177
|
+
"""Get a single secret value by name.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: Secret name (e.g., "BRAVE_API_KEY")
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Secret value, or None if not found
|
|
184
|
+
"""
|
|
185
|
+
return get_secrets().get(name)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Alias for backward compatibility and semantic clarity during config loading
|
|
189
|
+
# Both functions now use the same unified cache
|
|
190
|
+
get_early_secret = get_secret
|
ot/config/tool_config.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Runtime tool configuration accessor.
|
|
2
|
+
|
|
3
|
+
This module provides the primary interface for tools to access their
|
|
4
|
+
configuration at runtime. Tools call get_tool_config() with their pack
|
|
5
|
+
name and optional Config schema class.
|
|
6
|
+
|
|
7
|
+
Example usage in a tool file:
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
pack = "brave"
|
|
11
|
+
|
|
12
|
+
class Config(BaseModel):
|
|
13
|
+
timeout: float = Field(default=60.0, ge=1.0, le=300.0)
|
|
14
|
+
|
|
15
|
+
def search(query: str) -> str:
|
|
16
|
+
from ot.config.tool_config import get_tool_config
|
|
17
|
+
config = get_tool_config("brave", Config)
|
|
18
|
+
# config.timeout is typed and validated
|
|
19
|
+
...
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, TypeVar, overload
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T", bound=BaseModel)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def get_tool_config(pack: str, schema: type[T]) -> T: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def get_tool_config(pack: str, schema: None = None) -> dict[str, Any]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_tool_config(
|
|
40
|
+
pack: str, schema: type[T] | None = None
|
|
41
|
+
) -> T | dict[str, Any]:
|
|
42
|
+
"""Get configuration for a tool pack.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
pack: Pack name (e.g., "brave", "ground", "context7")
|
|
46
|
+
schema: Optional Pydantic model class to validate and return typed config.
|
|
47
|
+
If provided, returns an instance of the schema with merged values.
|
|
48
|
+
If None, returns raw config dict.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
If schema provided: Instance of schema with config values merged
|
|
52
|
+
If no schema: Dict with raw config values (empty dict if not configured)
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
# With schema (recommended for type safety)
|
|
56
|
+
class Config(BaseModel):
|
|
57
|
+
timeout: float = 60.0
|
|
58
|
+
|
|
59
|
+
config = get_tool_config("brave", Config)
|
|
60
|
+
print(config.timeout) # typed as float
|
|
61
|
+
|
|
62
|
+
# Without schema (raw dict)
|
|
63
|
+
raw = get_tool_config("brave")
|
|
64
|
+
print(raw.get("timeout", 60.0))
|
|
65
|
+
"""
|
|
66
|
+
# Get raw config values for this pack
|
|
67
|
+
raw_config = _get_raw_config(pack)
|
|
68
|
+
|
|
69
|
+
if schema is None:
|
|
70
|
+
return raw_config
|
|
71
|
+
|
|
72
|
+
# Validate and return typed config instance
|
|
73
|
+
try:
|
|
74
|
+
return schema.model_validate(raw_config)
|
|
75
|
+
except Exception:
|
|
76
|
+
# If validation fails, return defaults from schema
|
|
77
|
+
return schema()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_raw_config(pack: str) -> dict[str, Any]:
|
|
81
|
+
"""Get raw config dict for a pack from loaded configuration.
|
|
82
|
+
|
|
83
|
+
This function handles both typed tools.X fields and extra fields
|
|
84
|
+
allowed via model_config. It supports:
|
|
85
|
+
1. Typed tools.X fields (e.g., tools.stats)
|
|
86
|
+
2. Extra fields for tool packs (e.g., tools.brave)
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
pack: Pack name (e.g., "brave", "ground")
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Raw config dict for the pack, or empty dict if not configured
|
|
93
|
+
"""
|
|
94
|
+
from ot.config.loader import get_config
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
config = get_config()
|
|
98
|
+
except Exception:
|
|
99
|
+
# Config not loaded yet - return empty dict
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
# Get the tools section
|
|
103
|
+
tools = config.tools
|
|
104
|
+
|
|
105
|
+
# First check for typed attribute (e.g., tools.stats)
|
|
106
|
+
if hasattr(tools, pack):
|
|
107
|
+
pack_config = getattr(tools, pack)
|
|
108
|
+
if hasattr(pack_config, "model_dump"):
|
|
109
|
+
result: dict[str, Any] = pack_config.model_dump()
|
|
110
|
+
return result
|
|
111
|
+
# Handle raw dict from extra fields
|
|
112
|
+
if isinstance(pack_config, dict):
|
|
113
|
+
return pack_config
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
# Check model_extra for dynamically allowed fields
|
|
117
|
+
if hasattr(tools, "model_extra") and tools.model_extra:
|
|
118
|
+
extra = tools.model_extra
|
|
119
|
+
if pack in extra:
|
|
120
|
+
pack_data = extra[pack]
|
|
121
|
+
if isinstance(pack_data, dict):
|
|
122
|
+
return pack_data
|
|
123
|
+
return {}
|
|
124
|
+
|
|
125
|
+
return {}
|
ot/decorators.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Tool decorators for enhanced metadata.
|
|
2
|
+
|
|
3
|
+
The @tool decorator attaches metadata to tool functions for better
|
|
4
|
+
LLM comprehension. Plain functions work without decorators.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
@tool(
|
|
8
|
+
description="Search the web using Brave Search",
|
|
9
|
+
examples=["search(query='Python news')"],
|
|
10
|
+
tags=["search", "web"],
|
|
11
|
+
)
|
|
12
|
+
def brave_search(query: str, count: int = 10) -> str:
|
|
13
|
+
'''Search the web.'''
|
|
14
|
+
...
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, TypeVar
|
|
22
|
+
|
|
23
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ToolMetadata:
|
|
28
|
+
"""Metadata attached to tool functions by the @tool decorator."""
|
|
29
|
+
|
|
30
|
+
description: str | None = None
|
|
31
|
+
examples: list[str] = field(default_factory=list)
|
|
32
|
+
tags: list[str] = field(default_factory=list)
|
|
33
|
+
enabled: bool = True
|
|
34
|
+
deprecated: bool = False
|
|
35
|
+
deprecated_message: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Attribute name used to store metadata on decorated functions
|
|
39
|
+
TOOL_METADATA_ATTR = "_tool_metadata"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def tool(
|
|
43
|
+
description: str | None = None,
|
|
44
|
+
examples: list[str] | None = None,
|
|
45
|
+
tags: list[str] | None = None,
|
|
46
|
+
enabled: bool = True,
|
|
47
|
+
deprecated: bool = False,
|
|
48
|
+
deprecated_message: str | None = None,
|
|
49
|
+
) -> Callable[[F], F]:
|
|
50
|
+
"""Decorator to add metadata to a tool function.
|
|
51
|
+
|
|
52
|
+
Attaches a ToolMetadata instance to the function for the registry
|
|
53
|
+
to extract and use for enhanced descriptions.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
description: Override the docstring description
|
|
57
|
+
examples: List of usage examples
|
|
58
|
+
tags: Categorization tags
|
|
59
|
+
enabled: Whether the tool is enabled (default True)
|
|
60
|
+
deprecated: Mark as deprecated
|
|
61
|
+
deprecated_message: Message shown when deprecated
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Decorator function
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
@tool(
|
|
68
|
+
description="Search the web using Brave Search API",
|
|
69
|
+
examples=[
|
|
70
|
+
'brave_search(query="Python news", count=5)',
|
|
71
|
+
'brave_search(query="weather today")',
|
|
72
|
+
],
|
|
73
|
+
tags=["search", "web"],
|
|
74
|
+
)
|
|
75
|
+
def brave_search(query: str, count: int = 10) -> str:
|
|
76
|
+
'''Search the web.'''
|
|
77
|
+
...
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def decorator(func: F) -> F:
|
|
81
|
+
metadata = ToolMetadata(
|
|
82
|
+
description=description,
|
|
83
|
+
examples=examples or [],
|
|
84
|
+
tags=tags or [],
|
|
85
|
+
enabled=enabled,
|
|
86
|
+
deprecated=deprecated,
|
|
87
|
+
deprecated_message=deprecated_message,
|
|
88
|
+
)
|
|
89
|
+
setattr(func, TOOL_METADATA_ATTR, metadata)
|
|
90
|
+
return func
|
|
91
|
+
|
|
92
|
+
return decorator
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_tool_metadata(func: Callable[..., Any]) -> ToolMetadata | None:
|
|
96
|
+
"""Extract ToolMetadata from a function if present.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
func: Function to check for metadata
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ToolMetadata if decorated with @tool, None otherwise
|
|
103
|
+
"""
|
|
104
|
+
return getattr(func, TOOL_METADATA_ATTR, None)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def has_tool_metadata(func: Callable[..., Any]) -> bool:
|
|
108
|
+
"""Check if a function has @tool decorator metadata.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
func: Function to check
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if function has ToolMetadata
|
|
115
|
+
"""
|
|
116
|
+
return hasattr(func, TOOL_METADATA_ATTR)
|
ot/executor/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Execution engines for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides direct host execution via SimpleExecutor.
|
|
4
|
+
The unified runner provides a single entry point for all command execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ot.executor.base import ExecutionResult
|
|
8
|
+
from ot.executor.fence_processor import strip_fences
|
|
9
|
+
from ot.executor.runner import (
|
|
10
|
+
CommandResult,
|
|
11
|
+
PreparedCommand,
|
|
12
|
+
execute_command,
|
|
13
|
+
execute_python_code,
|
|
14
|
+
prepare_command,
|
|
15
|
+
)
|
|
16
|
+
from ot.executor.simple import SimpleExecutor
|
|
17
|
+
from ot.executor.validator import (
|
|
18
|
+
ValidationResult,
|
|
19
|
+
validate_for_exec,
|
|
20
|
+
validate_python_code,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"CommandResult",
|
|
25
|
+
"ExecutionResult",
|
|
26
|
+
"PreparedCommand",
|
|
27
|
+
"SimpleExecutor",
|
|
28
|
+
"ValidationResult",
|
|
29
|
+
"execute_command",
|
|
30
|
+
"execute_python_code",
|
|
31
|
+
"prepare_command",
|
|
32
|
+
"strip_fences",
|
|
33
|
+
"validate_for_exec",
|
|
34
|
+
"validate_python_code",
|
|
35
|
+
]
|
ot/executor/base.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Base types for executor module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ExecutionResult:
|
|
10
|
+
"""Result from executing a tool."""
|
|
11
|
+
|
|
12
|
+
success: bool
|
|
13
|
+
result: str
|
|
14
|
+
duration_seconds: float = 0.0
|
|
15
|
+
executor: str = "simple"
|
|
16
|
+
error_type: str | None = None
|