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.
Files changed (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/shortcuts/snippets.py CHANGED
@@ -1,258 +1,258 @@
1
- """Snippet parsing and expansion for OneTool shortcuts.
2
-
3
- Handles snippet syntax parsing and Jinja2 template expansion:
4
- - Single-line: $wsq q1=AI q2=ML p=Compare
5
- - Multi-line: $wsq\nq1: AI\nq2: ML\np: Compare
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import re
11
- from dataclasses import dataclass
12
- from typing import TYPE_CHECKING, Any
13
-
14
- from loguru import logger
15
-
16
- if TYPE_CHECKING:
17
- from ot.config import OneToolConfig, SnippetDef
18
-
19
- try:
20
- from jinja2 import Environment, StrictUndefined, TemplateSyntaxError
21
- except ImportError as e:
22
- raise ImportError(
23
- "jinja2 is required for snippets. Install with: pip install jinja2"
24
- ) from e
25
-
26
-
27
- @dataclass
28
- class ParsedSnippet:
29
- """Result of parsing a snippet invocation."""
30
-
31
- name: str
32
- params: dict[str, str]
33
- raw: str
34
-
35
-
36
- def is_snippet(code: str) -> bool:
37
- """Check if code is a snippet invocation (starts with $).
38
-
39
- Args:
40
- code: Code to check
41
-
42
- Returns:
43
- True if code starts with $ (snippet syntax)
44
- """
45
- stripped = code.strip()
46
- # Must start with $ but not be $variable inside other code
47
- return stripped.startswith("$") and not stripped.startswith("${")
48
-
49
-
50
- def parse_snippet(code: str) -> ParsedSnippet:
51
- """Parse a snippet invocation into name and parameters.
52
-
53
- Supports two syntaxes:
54
- - Single-line: $name key=value key2=value2
55
- - Multi-line: $name\\nkey: value\\nkey2: value2
56
-
57
- Args:
58
- code: Snippet invocation string
59
-
60
- Returns:
61
- ParsedSnippet with name and extracted parameters
62
-
63
- Raises:
64
- ValueError: If snippet syntax is invalid
65
- """
66
- stripped = code.strip()
67
-
68
- if not stripped.startswith("$"):
69
- raise ValueError(f"Snippet must start with $: {stripped[:50]}")
70
-
71
- # Remove $ prefix
72
- content = stripped[1:]
73
-
74
- # Check for multi-line (has newline after snippet name)
75
- lines = content.split("\n")
76
- first_line = lines[0].strip()
77
-
78
- # Extract snippet name (first word)
79
- name_match = re.match(r"^(\w+)", first_line)
80
- if not name_match:
81
- raise ValueError(f"Invalid snippet name: {first_line[:50]}")
82
-
83
- name = name_match.group(1)
84
-
85
- # Check if multi-line or single-line
86
- if len(lines) > 1:
87
- return _parse_multiline_snippet(name, lines[1:], stripped)
88
- else:
89
- return _parse_singleline_snippet(name, first_line[len(name) :], stripped)
90
-
91
-
92
- def _strip_quotes(value: str) -> str:
93
- """Strip matching outer quotes from a value.
94
-
95
- Handles both single and double quotes. Only strips if quotes are balanced.
96
-
97
- Args:
98
- value: String that may have outer quotes
99
-
100
- Returns:
101
- String with outer quotes removed if present and balanced
102
- """
103
- if len(value) >= 2 and (
104
- (value.startswith('"') and value.endswith('"'))
105
- or (value.startswith("'") and value.endswith("'"))
106
- ):
107
- return value[1:-1]
108
- return value
109
-
110
-
111
- def _parse_singleline_snippet(name: str, params_str: str, raw: str) -> ParsedSnippet:
112
- """Parse single-line snippet parameters: key=value key2="value with spaces".
113
-
114
- Values extend until the next key= or end of string.
115
- Outer quotes are stripped from values (key="value" becomes key=value).
116
- Escaped equals (\\=) are preserved in values.
117
- """
118
- params: dict[str, str] = {}
119
- params_str = params_str.strip()
120
-
121
- if not params_str:
122
- return ParsedSnippet(name=name, params=params, raw=raw)
123
-
124
- # Replace escaped equals with placeholder
125
- placeholder = "\x00EQUALS\x00"
126
- params_str = params_str.replace("\\=", placeholder)
127
-
128
- # Find all key=value pairs
129
- # Pattern: word followed by = and then value until next word= or end
130
- pattern = r"(\w+)=((?:[^=]|$)*?)(?=\s+\w+=|$)"
131
- matches = re.findall(pattern, params_str)
132
-
133
- for key, value in matches:
134
- # Restore escaped equals and strip whitespace
135
- value = value.replace(placeholder, "=").strip()
136
- # Strip outer quotes from value (e.g., packages="react" -> packages=react)
137
- value = _strip_quotes(value)
138
- params[key] = value
139
-
140
- return ParsedSnippet(name=name, params=params, raw=raw)
141
-
142
-
143
- def _parse_multiline_snippet(name: str, lines: list[str], raw: str) -> ParsedSnippet:
144
- """Parse multi-line snippet parameters: key: value.
145
-
146
- Blank line terminates the snippet parameters.
147
- Only the first colon is the separator (colons in values are preserved).
148
- Outer quotes are stripped from values for consistency with single-line format.
149
- """
150
- params: dict[str, str] = {}
151
-
152
- for line in lines:
153
- stripped = line.strip()
154
-
155
- # Blank line terminates
156
- if not stripped:
157
- break
158
-
159
- # Parse key: value (only first colon is separator)
160
- colon_idx = stripped.find(":")
161
- if colon_idx == -1:
162
- logger.warning(f"Invalid snippet line (no colon): {stripped}")
163
- continue
164
-
165
- key = stripped[:colon_idx].strip()
166
- value = stripped[colon_idx + 1 :].strip()
167
-
168
- if not key:
169
- logger.warning(f"Empty key in snippet line: {stripped}")
170
- continue
171
-
172
- # Strip outer quotes from value for consistency
173
- value = _strip_quotes(value)
174
- params[key] = value
175
-
176
- return ParsedSnippet(name=name, params=params, raw=raw)
177
-
178
-
179
- def expand_snippet(
180
- parsed: ParsedSnippet,
181
- config: OneToolConfig,
182
- ) -> str:
183
- """Expand a parsed snippet using Jinja2 templating.
184
-
185
- Args:
186
- parsed: Parsed snippet with name and parameters
187
- config: Configuration with snippet definitions
188
-
189
- Returns:
190
- Expanded Python code from the snippet template
191
-
192
- Raises:
193
- ValueError: If snippet not found, missing required params, or Jinja2 error
194
- """
195
- if parsed.name not in config.snippets:
196
- available = ", ".join(sorted(config.snippets.keys())) or "(none)"
197
- raise ValueError(f"Unknown snippet '{parsed.name}'. Available: {available}")
198
-
199
- snippet_def: SnippetDef = config.snippets[parsed.name]
200
-
201
- # Build context with defaults and provided values
202
- context: dict[str, Any] = {}
203
-
204
- # Apply defaults first
205
- for param_name, param_def in snippet_def.params.items():
206
- if param_def.default is not None:
207
- context[param_name] = param_def.default
208
-
209
- # Apply provided values
210
- for key, value in parsed.params.items():
211
- if key not in snippet_def.params:
212
- logger.warning(
213
- f"Unknown parameter '{key}' for snippet '{parsed.name}' (ignored)"
214
- )
215
- context[key] = value
216
-
217
- # Check required parameters
218
- for param_name, param_def in snippet_def.params.items():
219
- if param_def.required and param_name not in context:
220
- raise ValueError(
221
- f"Snippet '{parsed.name}' requires parameter '{param_name}'"
222
- )
223
-
224
- # Render template with Jinja2
225
- try:
226
- env = Environment(undefined=StrictUndefined)
227
- template = env.from_string(snippet_def.body)
228
- return template.render(**context)
229
- except TemplateSyntaxError as e:
230
- raise ValueError(f"Jinja2 syntax error in snippet '{parsed.name}': {e}") from e
231
- except Exception as e:
232
- # StrictUndefined raises UndefinedError for missing variables
233
- if "undefined" in str(e).lower():
234
- raise ValueError(
235
- f"Undefined variable in snippet '{parsed.name}': {e}"
236
- ) from e
237
- raise ValueError(f"Error expanding snippet '{parsed.name}': {e}") from e
238
-
239
-
240
- def validate_snippets(config: OneToolConfig) -> list[str]:
241
- """Validate snippet definitions for Jinja2 syntax errors.
242
-
243
- Args:
244
- config: Configuration with snippet definitions
245
-
246
- Returns:
247
- List of validation errors (empty if valid)
248
- """
249
- errors: list[str] = []
250
- env = Environment(undefined=StrictUndefined)
251
-
252
- for name, snippet_def in config.snippets.items():
253
- try:
254
- env.from_string(snippet_def.body)
255
- except TemplateSyntaxError as e:
256
- errors.append(f"Snippet '{name}' has invalid Jinja2 syntax: {e}")
257
-
258
- return errors
1
+ """Snippet parsing and expansion for OneTool shortcuts.
2
+
3
+ Handles snippet syntax parsing and Jinja2 template expansion:
4
+ - Single-line: $wsq q1=AI q2=ML p=Compare
5
+ - Multi-line: $wsq\nq1: AI\nq2: ML\np: Compare
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from loguru import logger
15
+
16
+ if TYPE_CHECKING:
17
+ from ot.config import OneToolConfig, SnippetDef
18
+
19
+ try:
20
+ from jinja2 import Environment, StrictUndefined, TemplateSyntaxError
21
+ except ImportError as e:
22
+ raise ImportError(
23
+ "jinja2 is required for snippets. Install with: pip install jinja2"
24
+ ) from e
25
+
26
+
27
+ @dataclass
28
+ class ParsedSnippet:
29
+ """Result of parsing a snippet invocation."""
30
+
31
+ name: str
32
+ params: dict[str, str]
33
+ raw: str
34
+
35
+
36
+ def is_snippet(code: str) -> bool:
37
+ """Check if code is a snippet invocation (starts with $).
38
+
39
+ Args:
40
+ code: Code to check
41
+
42
+ Returns:
43
+ True if code starts with $ (snippet syntax)
44
+ """
45
+ stripped = code.strip()
46
+ # Must start with $ but not be $variable inside other code
47
+ return stripped.startswith("$") and not stripped.startswith("${")
48
+
49
+
50
+ def parse_snippet(code: str) -> ParsedSnippet:
51
+ """Parse a snippet invocation into name and parameters.
52
+
53
+ Supports two syntaxes:
54
+ - Single-line: $name key=value key2=value2
55
+ - Multi-line: $name\\nkey: value\\nkey2: value2
56
+
57
+ Args:
58
+ code: Snippet invocation string
59
+
60
+ Returns:
61
+ ParsedSnippet with name and extracted parameters
62
+
63
+ Raises:
64
+ ValueError: If snippet syntax is invalid
65
+ """
66
+ stripped = code.strip()
67
+
68
+ if not stripped.startswith("$"):
69
+ raise ValueError(f"Snippet must start with $: {stripped[:50]}")
70
+
71
+ # Remove $ prefix
72
+ content = stripped[1:]
73
+
74
+ # Check for multi-line (has newline after snippet name)
75
+ lines = content.split("\n")
76
+ first_line = lines[0].strip()
77
+
78
+ # Extract snippet name (first word)
79
+ name_match = re.match(r"^(\w+)", first_line)
80
+ if not name_match:
81
+ raise ValueError(f"Invalid snippet name: {first_line[:50]}")
82
+
83
+ name = name_match.group(1)
84
+
85
+ # Check if multi-line or single-line
86
+ if len(lines) > 1:
87
+ return _parse_multiline_snippet(name, lines[1:], stripped)
88
+ else:
89
+ return _parse_singleline_snippet(name, first_line[len(name) :], stripped)
90
+
91
+
92
+ def _strip_quotes(value: str) -> str:
93
+ """Strip matching outer quotes from a value.
94
+
95
+ Handles both single and double quotes. Only strips if quotes are balanced.
96
+
97
+ Args:
98
+ value: String that may have outer quotes
99
+
100
+ Returns:
101
+ String with outer quotes removed if present and balanced
102
+ """
103
+ if len(value) >= 2 and (
104
+ (value.startswith('"') and value.endswith('"'))
105
+ or (value.startswith("'") and value.endswith("'"))
106
+ ):
107
+ return value[1:-1]
108
+ return value
109
+
110
+
111
+ def _parse_singleline_snippet(name: str, params_str: str, raw: str) -> ParsedSnippet:
112
+ """Parse single-line snippet parameters: key=value key2="value with spaces".
113
+
114
+ Values extend until the next key= or end of string.
115
+ Outer quotes are stripped from values (key="value" becomes key=value).
116
+ Escaped equals (\\=) are preserved in values.
117
+ """
118
+ params: dict[str, str] = {}
119
+ params_str = params_str.strip()
120
+
121
+ if not params_str:
122
+ return ParsedSnippet(name=name, params=params, raw=raw)
123
+
124
+ # Replace escaped equals with placeholder
125
+ placeholder = "\x00EQUALS\x00"
126
+ params_str = params_str.replace("\\=", placeholder)
127
+
128
+ # Find all key=value pairs
129
+ # Pattern: word followed by = and then value until next word= or end
130
+ pattern = r"(\w+)=((?:[^=]|$)*?)(?=\s+\w+=|$)"
131
+ matches = re.findall(pattern, params_str)
132
+
133
+ for key, value in matches:
134
+ # Restore escaped equals and strip whitespace
135
+ value = value.replace(placeholder, "=").strip()
136
+ # Strip outer quotes from value (e.g., packages="react" -> packages=react)
137
+ value = _strip_quotes(value)
138
+ params[key] = value
139
+
140
+ return ParsedSnippet(name=name, params=params, raw=raw)
141
+
142
+
143
+ def _parse_multiline_snippet(name: str, lines: list[str], raw: str) -> ParsedSnippet:
144
+ """Parse multi-line snippet parameters: key: value.
145
+
146
+ Blank line terminates the snippet parameters.
147
+ Only the first colon is the separator (colons in values are preserved).
148
+ Outer quotes are stripped from values for consistency with single-line format.
149
+ """
150
+ params: dict[str, str] = {}
151
+
152
+ for line in lines:
153
+ stripped = line.strip()
154
+
155
+ # Blank line terminates
156
+ if not stripped:
157
+ break
158
+
159
+ # Parse key: value (only first colon is separator)
160
+ colon_idx = stripped.find(":")
161
+ if colon_idx == -1:
162
+ logger.warning(f"Invalid snippet line (no colon): {stripped}")
163
+ continue
164
+
165
+ key = stripped[:colon_idx].strip()
166
+ value = stripped[colon_idx + 1 :].strip()
167
+
168
+ if not key:
169
+ logger.warning(f"Empty key in snippet line: {stripped}")
170
+ continue
171
+
172
+ # Strip outer quotes from value for consistency
173
+ value = _strip_quotes(value)
174
+ params[key] = value
175
+
176
+ return ParsedSnippet(name=name, params=params, raw=raw)
177
+
178
+
179
+ def expand_snippet(
180
+ parsed: ParsedSnippet,
181
+ config: OneToolConfig,
182
+ ) -> str:
183
+ """Expand a parsed snippet using Jinja2 templating.
184
+
185
+ Args:
186
+ parsed: Parsed snippet with name and parameters
187
+ config: Configuration with snippet definitions
188
+
189
+ Returns:
190
+ Expanded Python code from the snippet template
191
+
192
+ Raises:
193
+ ValueError: If snippet not found, missing required params, or Jinja2 error
194
+ """
195
+ if parsed.name not in config.snippets:
196
+ available = ", ".join(sorted(config.snippets.keys())) or "(none)"
197
+ raise ValueError(f"Unknown snippet '{parsed.name}'. Available: {available}")
198
+
199
+ snippet_def: SnippetDef = config.snippets[parsed.name]
200
+
201
+ # Build context with defaults and provided values
202
+ context: dict[str, Any] = {}
203
+
204
+ # Apply defaults first
205
+ for param_name, param_def in snippet_def.params.items():
206
+ if param_def.default is not None:
207
+ context[param_name] = param_def.default
208
+
209
+ # Apply provided values
210
+ for key, value in parsed.params.items():
211
+ if key not in snippet_def.params:
212
+ logger.warning(
213
+ f"Unknown parameter '{key}' for snippet '{parsed.name}' (ignored)"
214
+ )
215
+ context[key] = value
216
+
217
+ # Check required parameters
218
+ for param_name, param_def in snippet_def.params.items():
219
+ if param_def.required and param_name not in context:
220
+ raise ValueError(
221
+ f"Snippet '{parsed.name}' requires parameter '{param_name}'"
222
+ )
223
+
224
+ # Render template with Jinja2
225
+ try:
226
+ env = Environment(undefined=StrictUndefined)
227
+ template = env.from_string(snippet_def.body)
228
+ return template.render(**context)
229
+ except TemplateSyntaxError as e:
230
+ raise ValueError(f"Jinja2 syntax error in snippet '{parsed.name}': {e}") from e
231
+ except Exception as e:
232
+ # StrictUndefined raises UndefinedError for missing variables
233
+ if "undefined" in str(e).lower():
234
+ raise ValueError(
235
+ f"Undefined variable in snippet '{parsed.name}': {e}"
236
+ ) from e
237
+ raise ValueError(f"Error expanding snippet '{parsed.name}': {e}") from e
238
+
239
+
240
+ def validate_snippets(config: OneToolConfig) -> list[str]:
241
+ """Validate snippet definitions for Jinja2 syntax errors.
242
+
243
+ Args:
244
+ config: Configuration with snippet definitions
245
+
246
+ Returns:
247
+ List of validation errors (empty if valid)
248
+ """
249
+ errors: list[str] = []
250
+ env = Environment(undefined=StrictUndefined)
251
+
252
+ for name, snippet_def in config.snippets.items():
253
+ try:
254
+ env.from_string(snippet_def.body)
255
+ except TemplateSyntaxError as e:
256
+ errors.append(f"Snippet '{name}' has invalid Jinja2 syntax: {e}")
257
+
258
+ return errors
ot/stats/__init__.py CHANGED
@@ -1,35 +1,35 @@
1
- """Runtime statistics collection for OneTool.
2
-
3
- Two-level statistics:
4
- - Run-level: Tracks run() calls, durations, and calculates context savings estimates.
5
- - Tool-level: Tracks actual tool invocations at the executor dispatch level.
6
-
7
- Records are stored in a single JSONL file with a 'type' field discriminator.
8
- """
9
-
10
- from ot.stats.html import generate_html_report
11
- from ot.stats.jsonl_writer import (
12
- JsonlStatsWriter,
13
- get_client_name,
14
- get_stats_writer,
15
- record_tool_stats,
16
- set_client_name,
17
- set_stats_writer,
18
- )
19
- from ot.stats.reader import AggregatedStats, Period, StatsReader, ToolStats
20
- from ot.stats.timing import timed_tool_call
21
-
22
- __all__ = [
23
- "AggregatedStats",
24
- "JsonlStatsWriter",
25
- "Period",
26
- "StatsReader",
27
- "ToolStats",
28
- "generate_html_report",
29
- "get_client_name",
30
- "get_stats_writer",
31
- "record_tool_stats",
32
- "set_client_name",
33
- "set_stats_writer",
34
- "timed_tool_call",
35
- ]
1
+ """Runtime statistics collection for OneTool.
2
+
3
+ Two-level statistics:
4
+ - Run-level: Tracks run() calls, durations, and calculates context savings estimates.
5
+ - Tool-level: Tracks actual tool invocations at the executor dispatch level.
6
+
7
+ Records are stored in a single JSONL file with a 'type' field discriminator.
8
+ """
9
+
10
+ from ot.stats.html import generate_html_report
11
+ from ot.stats.jsonl_writer import (
12
+ JsonlStatsWriter,
13
+ get_client_name,
14
+ get_stats_writer,
15
+ record_tool_stats,
16
+ set_client_name,
17
+ set_stats_writer,
18
+ )
19
+ from ot.stats.reader import AggregatedStats, Period, StatsReader, ToolStats
20
+ from ot.stats.timing import timed_tool_call
21
+
22
+ __all__ = [
23
+ "AggregatedStats",
24
+ "JsonlStatsWriter",
25
+ "Period",
26
+ "StatsReader",
27
+ "ToolStats",
28
+ "generate_html_report",
29
+ "get_client_name",
30
+ "get_stats_writer",
31
+ "record_tool_stats",
32
+ "set_client_name",
33
+ "set_stats_writer",
34
+ "timed_tool_call",
35
+ ]
ot/stats/html.py CHANGED
@@ -15,7 +15,7 @@ from ot.support import (
15
15
  )
16
16
 
17
17
  if TYPE_CHECKING:
18
- from ot.stats.reader import AggregatedStats
18
+ from ot.stats.reader import AggregatedStats, ToolStats
19
19
 
20
20
 
21
21
  def generate_html_report(stats: AggregatedStats) -> str:
@@ -61,7 +61,7 @@ def generate_html_report(stats: AggregatedStats) -> str:
61
61
  # Group tools by pack (prefix before the dot)
62
62
  from collections import defaultdict
63
63
 
64
- packs: dict[str, list] = defaultdict(list)
64
+ packs: dict[str, list[ToolStats]] = defaultdict(list)
65
65
  for tool in stats.tools:
66
66
  pack_name = tool.tool.split(".")[0] if "." in tool.tool else "other"
67
67
  packs[pack_name].append(tool)