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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. 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
@@ -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)
@@ -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