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
@@ -0,0 +1,396 @@
1
+ """Tool loading and discovery for command execution.
2
+
3
+ Handles:
4
+ - Loading tool functions from config-defined tool files
5
+ - Caching based on file modification times
6
+ - Pack extraction from tool modules
7
+ - PEP 723 detection for routing to worker processes
8
+
9
+ Used by the runner to make tools available during code execution.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import sys
16
+ from collections import OrderedDict
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from loguru import logger
22
+
23
+ from ot.executor.pep723 import ToolFileInfo, categorize_tools
24
+ from ot.executor.worker_proxy import create_worker_proxy
25
+ from ot.paths import get_effective_cwd
26
+
27
+
28
+ def _get_bundled_tools_dir() -> Path | None:
29
+ """Get the bundled tools directory from the ot_tools package.
30
+
31
+ Returns:
32
+ Path to ot_tools package directory, or None if not found.
33
+ """
34
+ try:
35
+ import ot_tools
36
+
37
+ return Path(ot_tools.__file__).parent
38
+ except (ImportError, AttributeError):
39
+ return None
40
+
41
+
42
+ if TYPE_CHECKING:
43
+ from ot.config.loader import OneToolConfig
44
+
45
+
46
+ @dataclass
47
+ class LoadedTools:
48
+ """Registry of loaded tool functions with pack support.
49
+
50
+ The functions dict uses full pack-qualified names as keys (e.g., "brave.search")
51
+ to avoid collisions when multiple packs have functions with the same name.
52
+ The packs dict provides grouped access by pack.
53
+ """
54
+
55
+ functions: dict[str, Any] # Full name -> callable (e.g., "brave.search" -> func)
56
+ packs: dict[str, dict[str, Any]] # Nested: pack -> {name -> callable}
57
+ worker_tools: list[ToolFileInfo] = field(
58
+ default_factory=list
59
+ ) # Tools using workers
60
+
61
+
62
+ # Module cache: stores (LoadedTools, mtime_dict) for each tools_dir
63
+ # Uses OrderedDict for LRU eviction with bounded size
64
+ _MODULE_CACHE_MAXSIZE = 16
65
+ _module_cache: OrderedDict[Path, tuple[LoadedTools, dict[str, float]]] = OrderedDict()
66
+
67
+
68
+ def _cache_get(key: Path) -> tuple[LoadedTools, dict[str, float]] | None:
69
+ """Get from cache with LRU update."""
70
+ if key in _module_cache:
71
+ _module_cache.move_to_end(key)
72
+ return _module_cache[key]
73
+ return None
74
+
75
+
76
+ def _cache_set(key: Path, value: tuple[LoadedTools, dict[str, float]]) -> None:
77
+ """Set in cache with LRU eviction."""
78
+ if key in _module_cache:
79
+ _module_cache.move_to_end(key)
80
+ _module_cache[key] = value
81
+ while len(_module_cache) > _MODULE_CACHE_MAXSIZE:
82
+ _module_cache.popitem(last=False)
83
+
84
+
85
+ def _get_tool_files(
86
+ tools_dir: Path | None, config: OneToolConfig | None
87
+ ) -> tuple[set[Path], set[Path], Path]:
88
+ """Resolve tool files from config, bundled package, or directory.
89
+
90
+ Always includes bundled tools from ot_tools package, plus any
91
+ additional tools from config or explicit tools_dir.
92
+
93
+ Args:
94
+ tools_dir: Explicit tools directory path.
95
+ config: Loaded configuration (may be None).
96
+
97
+ Returns:
98
+ Tuple of (all tool file paths, internal tool paths, cache key).
99
+ """
100
+ tool_files: list[Path] = []
101
+ internal_files: set[Path] = set()
102
+
103
+ # Always include bundled tools from ot_tools package
104
+ bundled_dir = _get_bundled_tools_dir()
105
+ if bundled_dir and bundled_dir.exists():
106
+ bundled_files = [f for f in bundled_dir.glob("*.py") if f.name != "__init__.py"]
107
+ tool_files.extend(bundled_files)
108
+ # Mark bundled tools as internal (shipped with OneTool)
109
+ internal_files = {f.resolve() for f in bundled_files if f.exists()}
110
+ logger.debug(f"Found {len(bundled_files)} bundled tools from {bundled_dir}")
111
+
112
+ # Add config-specified tools (these are extension tools, not internal)
113
+ config_tool_files = config.get_tool_files() if config else []
114
+ if config_tool_files:
115
+ tool_files.extend(config_tool_files)
116
+ cache_key = Path("__config__")
117
+ elif tools_dir is not None:
118
+ # Explicit tools_dir provided - use it
119
+ if tools_dir.exists():
120
+ tools_dir = tools_dir.resolve()
121
+ tool_files.extend(tools_dir.glob("*.py"))
122
+ cache_key = tools_dir
123
+ else:
124
+ cache_key = Path("__bundled__")
125
+
126
+ if not tool_files:
127
+ return set(), set(), Path("__no_tools__")
128
+
129
+ current_files = {f.resolve() for f in tool_files if f.exists()}
130
+ return current_files, internal_files, cache_key
131
+
132
+
133
+ def _check_cache(cache_key: Path, current_files: set[Path]) -> LoadedTools | None:
134
+ """Return cached registry if valid, None if stale or missing.
135
+
136
+ Args:
137
+ cache_key: Key for cache lookup.
138
+ current_files: Set of current tool file paths.
139
+
140
+ Returns:
141
+ Cached LoadedTools if valid, None otherwise.
142
+ """
143
+ cached = _cache_get(cache_key)
144
+ if cached is None:
145
+ return None
146
+
147
+ cached_registry, cached_mtimes = cached
148
+ cached_files = {Path(f) for f in cached_mtimes}
149
+
150
+ if current_files != cached_files:
151
+ return None
152
+
153
+ for py_file in current_files:
154
+ try:
155
+ if py_file.stat().st_mtime != cached_mtimes.get(str(py_file), 0):
156
+ return None
157
+ except OSError:
158
+ return None
159
+
160
+ return cached_registry
161
+
162
+
163
+ def _load_worker_tools(
164
+ worker_tools: list[ToolFileInfo],
165
+ config_dict: dict[str, Any],
166
+ secrets: dict[str, Any],
167
+ packs: dict[str, dict[str, Any]],
168
+ mtimes: dict[str, float],
169
+ ) -> tuple[dict[str, Any], list[ToolFileInfo]]:
170
+ """Load PEP 723 tools via worker proxies.
171
+
172
+ Args:
173
+ worker_tools: List of tool file info for extension tools.
174
+ config_dict: Configuration as dict.
175
+ secrets: Secrets dict.
176
+ packs: Packs dict to populate.
177
+ mtimes: Modification times dict to populate.
178
+
179
+ Returns:
180
+ Tuple of (functions dict, loaded extension tools list).
181
+ """
182
+ functions: dict[str, Any] = {}
183
+ loaded_workers: list[ToolFileInfo] = []
184
+
185
+ for tool_info in worker_tools:
186
+ py_file = tool_info.path
187
+ try:
188
+ mtimes[str(py_file)] = py_file.stat().st_mtime
189
+
190
+ pack = tool_info.pack
191
+ if pack and pack in packs:
192
+ logger.warning(
193
+ f"Pack collision: '{pack}' already defined, "
194
+ f"merging functions from {py_file.stem}"
195
+ )
196
+
197
+ proxy = create_worker_proxy(
198
+ tool_path=py_file,
199
+ functions=tool_info.functions,
200
+ config=config_dict,
201
+ secrets=secrets,
202
+ )
203
+
204
+ if pack:
205
+ if pack not in packs:
206
+ packs[pack] = {}
207
+ packs[pack] = proxy # type: ignore[assignment]
208
+ for func_name in tool_info.functions:
209
+ full_name = f"{pack}.{func_name}"
210
+ functions[full_name] = getattr(proxy, func_name)
211
+ else:
212
+ for func_name in tool_info.functions:
213
+ functions[func_name] = getattr(proxy, func_name)
214
+
215
+ loaded_workers.append(tool_info)
216
+ logger.debug(
217
+ f"Loaded extension tool {py_file.stem} with {len(tool_info.functions)} functions"
218
+ )
219
+
220
+ except Exception as e:
221
+ logger.warning(f"Failed to load extension tool {py_file.stem}: {e}")
222
+
223
+ return functions, loaded_workers
224
+
225
+
226
+ def _load_inprocess_tools(
227
+ inprocess_tools: list[ToolFileInfo],
228
+ packs: dict[str, dict[str, Any]],
229
+ mtimes: dict[str, float],
230
+ ) -> dict[str, Any]:
231
+ """Load regular Python tools via importlib.
232
+
233
+ Args:
234
+ inprocess_tools: List of tool file info for in-process tools.
235
+ packs: Packs dict to populate.
236
+ mtimes: Modification times dict to populate.
237
+
238
+ Returns:
239
+ Functions dict with loaded tools.
240
+ """
241
+ functions: dict[str, Any] = {}
242
+
243
+ for tool_info in inprocess_tools:
244
+ py_file = tool_info.path
245
+ module_name = f"tools.{py_file.stem}"
246
+
247
+ try:
248
+ mtimes[str(py_file)] = py_file.stat().st_mtime
249
+
250
+ spec = importlib.util.spec_from_file_location(module_name, py_file)
251
+ if spec is None or spec.loader is None:
252
+ continue
253
+
254
+ module = importlib.util.module_from_spec(spec)
255
+ sys.modules[module_name] = module
256
+ spec.loader.exec_module(module)
257
+
258
+ pack = getattr(module, "pack", None)
259
+ if pack and pack in packs:
260
+ logger.warning(
261
+ f"Pack collision: '{pack}' already defined, "
262
+ f"merging functions from {py_file.stem}"
263
+ )
264
+
265
+ export_names = getattr(module, "__all__", None)
266
+ if export_names is None:
267
+ export_names = [n for n in dir(module) if not n.startswith("_")]
268
+
269
+ for name in export_names:
270
+ obj = getattr(module, name, None)
271
+ if obj is not None and callable(obj) and not isinstance(obj, type):
272
+ if pack:
273
+ if pack not in packs:
274
+ packs[pack] = {}
275
+ packs[pack][name] = obj
276
+ full_name = f"{pack}.{name}"
277
+ functions[full_name] = obj
278
+ else:
279
+ functions[name] = obj
280
+
281
+ except Exception as e:
282
+ logger.warning(f"Failed to load tool module {py_file.stem}: {e}")
283
+
284
+ return functions
285
+
286
+
287
+ def load_tool_registry(tools_dir: Path | None = None) -> LoadedTools:
288
+ """Load all tool functions from config tool files with pack support.
289
+
290
+ Uses caching based on file modification times to avoid redundant loading.
291
+ Reads `pack` module variable from each tool file to group functions.
292
+
293
+ Tool loading strategy:
294
+ - Internal tools (bundled with OneTool from ot_tools package): Run in-process
295
+ via importlib. These tools have no PEP 723 headers and use ot.* imports.
296
+ - Extension tools (user-created without PEP 723 headers): Run in-process
297
+ with full ot.* access (logging, config, inter-tool calling).
298
+ - Isolated tools (user-created with PEP 723 headers): Run in worker
299
+ subprocesses with isolated dependencies. Fully standalone, no ot imports.
300
+
301
+ The core 'ot' pack (from meta.py) is always registered regardless of config.
302
+
303
+ Args:
304
+ tools_dir: Explicit path to tools directory. If not provided,
305
+ tool files are loaded from config. If neither is available,
306
+ only the core 'ot' pack will be available.
307
+
308
+ Returns:
309
+ LoadedTools with functions dict (pack-qualified keys) and packs dict.
310
+ """
311
+ from ot.config.loader import get_config
312
+ from ot.config.secrets import get_secrets
313
+
314
+ config = get_config()
315
+ current_files, internal_files, cache_key = _get_tool_files(tools_dir, config)
316
+
317
+ if not current_files:
318
+ return LoadedTools(functions={}, packs={})
319
+
320
+ cached = _check_cache(cache_key, current_files)
321
+ if cached is not None:
322
+ return cached
323
+
324
+ logger.debug(f"Loading tools from {cache_key} ({len(current_files)} files)")
325
+
326
+ packs: dict[str, dict[str, Any]] = {}
327
+ mtimes: dict[str, float] = {}
328
+
329
+ # Categorize tools: internal (bundled) vs extension (user-created)
330
+ # Internal tools run in-process, extension tools with PEP 723 use workers
331
+ worker_tools, inprocess_tools = categorize_tools(
332
+ list(current_files), internal_files
333
+ )
334
+
335
+ secrets_path = config.get_secrets_file_path() if config else None
336
+ secrets = get_secrets(secrets_path)
337
+ config_dict = config.model_dump() if config else {}
338
+
339
+ # Inject path context for isolated worker tools
340
+ config_dict["_project_path"] = str(get_effective_cwd())
341
+ if config and config._config_dir:
342
+ config_dict["_config_dir"] = str(config._config_dir)
343
+
344
+ worker_funcs, worker_tools_list = _load_worker_tools(
345
+ worker_tools, config_dict, secrets, packs, mtimes
346
+ )
347
+ inprocess_funcs = _load_inprocess_tools(inprocess_tools, packs, mtimes)
348
+
349
+ functions = {**worker_funcs, **inprocess_funcs}
350
+
351
+ # Register the core 'ot' pack from meta.py (not loaded from tools_dir)
352
+ ot_funcs = _register_ot_pack(packs)
353
+ functions.update(ot_funcs)
354
+
355
+ registry = LoadedTools(
356
+ functions=functions, packs=packs, worker_tools=worker_tools_list
357
+ )
358
+ _cache_set(cache_key, (registry, mtimes))
359
+
360
+ return registry
361
+
362
+
363
+ def _register_ot_pack(packs: dict[str, dict[str, Any]]) -> dict[str, Any]:
364
+ """Register the core 'ot' pack from ot.meta module.
365
+
366
+ The 'ot' pack provides introspection functions (tools, packs, config, etc.)
367
+ and is always available regardless of tools_dir configuration.
368
+
369
+ Args:
370
+ packs: Packs dict to add 'ot' pack to.
371
+
372
+ Returns:
373
+ Functions dict with ot.* entries.
374
+ """
375
+ from ot.meta import PACK_NAME, get_ot_pack_functions
376
+
377
+ ot_functions = get_ot_pack_functions()
378
+ packs[PACK_NAME] = ot_functions
379
+
380
+ # Build full function names
381
+ return {f"{PACK_NAME}.{name}": func for name, func in ot_functions.items()}
382
+
383
+
384
+ def load_tool_functions(tools_dir: Path | None = None) -> dict[str, Any]:
385
+ """Load all tool functions from the tools directory.
386
+
387
+ Uses caching based on file modification times to avoid redundant loading.
388
+
389
+ Args:
390
+ tools_dir: Explicit path to tools directory. If not provided,
391
+ tool files are loaded from config.
392
+
393
+ Returns:
394
+ Dictionary mapping function names to callable functions.
395
+ """
396
+ return load_tool_registry(tools_dir).functions