onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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.
- onetool/cli.py +63 -4
- onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
- onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
- ot/__main__.py +6 -6
- ot/config/__init__.py +48 -46
- ot/config/global_templates/__init__.py +2 -2
- ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
- ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
- ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
- ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
- ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
- ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -0
- ot/config/global_templates/onetool.yaml +3 -1
- ot/config/{defaults → global_templates}/prompts.yaml +102 -97
- ot/config/global_templates/security.yaml +31 -0
- ot/config/global_templates/servers.yaml +93 -12
- ot/config/global_templates/snippets.yaml +5 -26
- ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
- ot/config/loader.py +221 -105
- ot/config/mcp.py +5 -1
- ot/config/secrets.py +192 -190
- ot/decorators.py +116 -116
- ot/executor/__init__.py +35 -35
- ot/executor/base.py +16 -16
- ot/executor/fence_processor.py +83 -83
- ot/executor/linter.py +142 -142
- ot/executor/pep723.py +288 -288
- ot/executor/runner.py +20 -6
- ot/executor/simple.py +163 -163
- ot/executor/validator.py +603 -164
- ot/http_client.py +145 -145
- ot/logging/__init__.py +37 -37
- ot/logging/entry.py +213 -213
- ot/logging/format.py +191 -188
- ot/logging/span.py +349 -349
- ot/meta.py +236 -14
- ot/paths.py +32 -49
- ot/prompts.py +218 -218
- ot/proxy/manager.py +14 -2
- ot/registry/__init__.py +189 -189
- ot/registry/parser.py +269 -269
- ot/server.py +330 -315
- ot/shortcuts/__init__.py +15 -15
- ot/shortcuts/aliases.py +87 -87
- ot/shortcuts/snippets.py +258 -258
- ot/stats/__init__.py +35 -35
- ot/stats/html.py +2 -2
- ot/stats/reader.py +354 -354
- ot/stats/timing.py +57 -57
- ot/support.py +63 -63
- ot/tools.py +1 -1
- ot/utils/batch.py +161 -161
- ot/utils/cache.py +120 -120
- ot/utils/exceptions.py +23 -23
- ot/utils/factory.py +178 -179
- ot/utils/format.py +65 -65
- ot/utils/http.py +202 -202
- ot/utils/platform.py +45 -45
- ot/utils/truncate.py +69 -69
- ot_tools/__init__.py +4 -4
- ot_tools/_convert/__init__.py +12 -12
- ot_tools/_convert/pdf.py +254 -254
- ot_tools/diagram.yaml +167 -167
- ot_tools/scaffold.py +2 -2
- ot_tools/transform.py +124 -19
- ot_tools/web_fetch.py +94 -43
- onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
- onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
- ot/config/defaults/bench.yaml +0 -4
- ot/config/defaults/onetool.yaml +0 -25
- ot/config/defaults/servers.yaml +0 -7
- ot/config/defaults/snippets.yaml +0 -4
- ot_tools/firecrawl.py +0 -732
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/config/secrets.py
CHANGED
|
@@ -1,190 +1,192 @@
|
|
|
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.
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
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.
|
|
128
|
-
3.
|
|
129
|
-
4.
|
|
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
|
|
142
|
-
if secrets_path is None:
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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. OT_SECRETS_FILE environment variable
|
|
9
|
+
3. Config's secrets_file setting (if config loaded and file exists)
|
|
10
|
+
4. Default locations: project (.onetool/config/secrets.yaml) then global (~/.onetool/config/secrets.yaml)
|
|
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. OT_SECRETS_FILE environment variable
|
|
128
|
+
3. Config's secrets_file setting (if config loaded and file exists)
|
|
129
|
+
4. Default locations (.onetool/config/secrets.yaml)
|
|
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 > env var > config (if loaded) > defaults
|
|
142
|
+
if secrets_path is None:
|
|
143
|
+
# Check OT_SECRETS_FILE env var first (highest priority after explicit path)
|
|
144
|
+
env_path = os.getenv("OT_SECRETS_FILE")
|
|
145
|
+
if env_path:
|
|
146
|
+
secrets_path = env_path
|
|
147
|
+
|
|
148
|
+
if secrets_path is None:
|
|
149
|
+
# WARNING: Do NOT call get_config() here!
|
|
150
|
+
# =========================================
|
|
151
|
+
# This function is called during config loading via:
|
|
152
|
+
# get_config() → load_config() → expand_secrets() → get_early_secret() → get_secrets()
|
|
153
|
+
#
|
|
154
|
+
# If we call get_config() here, it triggers config loading again → infinite recursion.
|
|
155
|
+
# Instead, we check _config directly - if it's None, config is still loading.
|
|
156
|
+
try:
|
|
157
|
+
import ot.config.loader
|
|
158
|
+
|
|
159
|
+
if ot.config.loader._config is not None:
|
|
160
|
+
config_path = ot.config.loader._config.get_secrets_file_path()
|
|
161
|
+
if config_path.exists():
|
|
162
|
+
secrets_path = config_path
|
|
163
|
+
except Exception:
|
|
164
|
+
pass # Module not loaded yet, fall through
|
|
165
|
+
|
|
166
|
+
# Try default locations if still no path
|
|
167
|
+
if secrets_path is None:
|
|
168
|
+
loaded = _load_from_default_locations()
|
|
169
|
+
if loaded:
|
|
170
|
+
_secrets = loaded
|
|
171
|
+
return _secrets
|
|
172
|
+
|
|
173
|
+
_secrets = load_secrets(secrets_path)
|
|
174
|
+
|
|
175
|
+
return _secrets
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_secret(name: str) -> str | None:
|
|
179
|
+
"""Get a single secret value by name.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
name: Secret name (e.g., "BRAVE_API_KEY")
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Secret value, or None if not found
|
|
186
|
+
"""
|
|
187
|
+
return get_secrets().get(name)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Alias for backward compatibility and semantic clarity during config loading
|
|
191
|
+
# Both functions now use the same unified cache
|
|
192
|
+
get_early_secret = get_secret
|
ot/decorators.py
CHANGED
|
@@ -1,116 +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)
|
|
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)
|