onetool-mcp 1.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
ot/stats/reader.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Stats reader with aggregation and filtering.
|
|
2
|
+
|
|
3
|
+
Reads JSONL stats and aggregates by period with savings calculations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime, timedelta
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
Period = Literal["day", "week", "month", "all"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ToolStats:
|
|
23
|
+
"""Aggregated statistics for a single tool."""
|
|
24
|
+
|
|
25
|
+
tool: str
|
|
26
|
+
total_calls: int
|
|
27
|
+
success_count: int
|
|
28
|
+
error_count: int
|
|
29
|
+
total_chars_in: int
|
|
30
|
+
total_chars_out: int
|
|
31
|
+
total_duration_ms: int
|
|
32
|
+
avg_duration_ms: float
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def success_rate(self) -> float:
|
|
36
|
+
"""Calculate success rate as percentage."""
|
|
37
|
+
if self.total_calls == 0:
|
|
38
|
+
return 0.0
|
|
39
|
+
return (self.success_count / self.total_calls) * 100
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
"""Convert to dictionary representation."""
|
|
43
|
+
return {
|
|
44
|
+
"tool": self.tool,
|
|
45
|
+
"total_calls": self.total_calls,
|
|
46
|
+
"success_count": self.success_count,
|
|
47
|
+
"error_count": self.error_count,
|
|
48
|
+
"success_rate": round(self.success_rate, 1),
|
|
49
|
+
"total_chars_in": self.total_chars_in,
|
|
50
|
+
"total_chars_out": self.total_chars_out,
|
|
51
|
+
"total_duration_ms": self.total_duration_ms,
|
|
52
|
+
"avg_duration_ms": round(self.avg_duration_ms, 1),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Cost per coffee for savings display (hardcoded)
|
|
57
|
+
COFFEE_COST_USD = 5.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class AggregatedStats:
|
|
62
|
+
"""Aggregated statistics summary."""
|
|
63
|
+
|
|
64
|
+
period: Period
|
|
65
|
+
start_time: str | None
|
|
66
|
+
end_time: str | None
|
|
67
|
+
total_calls: int
|
|
68
|
+
success_count: int
|
|
69
|
+
error_count: int
|
|
70
|
+
total_chars_in: int
|
|
71
|
+
total_chars_out: int
|
|
72
|
+
total_duration_ms: int
|
|
73
|
+
context_saved: int
|
|
74
|
+
time_saved_ms: int
|
|
75
|
+
tools: list[ToolStats]
|
|
76
|
+
model: str = ""
|
|
77
|
+
cost_estimate_usd: float = 0.0
|
|
78
|
+
savings_usd: float = 0.0
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def success_rate(self) -> float:
|
|
82
|
+
"""Calculate overall success rate as percentage."""
|
|
83
|
+
if self.total_calls == 0:
|
|
84
|
+
return 0.0
|
|
85
|
+
return (self.success_count / self.total_calls) * 100
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def coffees(self) -> float:
|
|
89
|
+
"""Calculate coffee equivalent of savings."""
|
|
90
|
+
return self.savings_usd / COFFEE_COST_USD
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, Any]:
|
|
93
|
+
"""Convert to dictionary representation."""
|
|
94
|
+
return {
|
|
95
|
+
"period": self.period,
|
|
96
|
+
"start_time": self.start_time,
|
|
97
|
+
"end_time": self.end_time,
|
|
98
|
+
"total_calls": self.total_calls,
|
|
99
|
+
"success_count": self.success_count,
|
|
100
|
+
"error_count": self.error_count,
|
|
101
|
+
"success_rate": round(self.success_rate, 1),
|
|
102
|
+
"total_chars_in": self.total_chars_in,
|
|
103
|
+
"total_chars_out": self.total_chars_out,
|
|
104
|
+
"total_duration_ms": self.total_duration_ms,
|
|
105
|
+
"context_saved": self.context_saved,
|
|
106
|
+
"time_saved_ms": self.time_saved_ms,
|
|
107
|
+
"model": self.model,
|
|
108
|
+
"cost_estimate_usd": round(self.cost_estimate_usd, 4),
|
|
109
|
+
"savings_usd": round(self.savings_usd, 2),
|
|
110
|
+
"coffees": round(self.coffees, 1),
|
|
111
|
+
"tools": [t.to_dict() for t in self.tools],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class StatsReader:
|
|
116
|
+
"""Reads and aggregates statistics from JSONL.
|
|
117
|
+
|
|
118
|
+
Usage:
|
|
119
|
+
reader = StatsReader(path, context_per_call=30000, time_overhead_ms=4000)
|
|
120
|
+
stats = reader.read(period="week", tool="brave.search")
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
path: Path,
|
|
126
|
+
context_per_call: int = 30000,
|
|
127
|
+
time_overhead_per_call_ms: int = 4000,
|
|
128
|
+
model: str = "anthropic/claude-opus-4.5",
|
|
129
|
+
cost_per_million_input_tokens: float = 15.0,
|
|
130
|
+
cost_per_million_output_tokens: float = 75.0,
|
|
131
|
+
chars_per_token: float = 4.0,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Initialize reader.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
path: Path to JSONL file
|
|
137
|
+
context_per_call: Context tokens saved per consolidated call
|
|
138
|
+
time_overhead_per_call_ms: Time overhead in ms saved per call
|
|
139
|
+
model: Model name for cost estimation
|
|
140
|
+
cost_per_million_input_tokens: Cost in USD per million input tokens
|
|
141
|
+
cost_per_million_output_tokens: Cost in USD per million output tokens
|
|
142
|
+
chars_per_token: Average characters per token for estimation
|
|
143
|
+
"""
|
|
144
|
+
self._path = path
|
|
145
|
+
self._context_per_call = context_per_call
|
|
146
|
+
self._time_overhead_ms = time_overhead_per_call_ms
|
|
147
|
+
self._model = model
|
|
148
|
+
self._cost_per_m_input = cost_per_million_input_tokens
|
|
149
|
+
self._cost_per_m_output = cost_per_million_output_tokens
|
|
150
|
+
self._chars_per_token = chars_per_token
|
|
151
|
+
|
|
152
|
+
def read(
|
|
153
|
+
self,
|
|
154
|
+
period: Period = "all",
|
|
155
|
+
tool: str | None = None,
|
|
156
|
+
) -> AggregatedStats:
|
|
157
|
+
"""Read and aggregate stats.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
period: Time period to filter (day/week/month/all)
|
|
161
|
+
tool: Optional tool name filter
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Aggregated statistics
|
|
165
|
+
"""
|
|
166
|
+
records = self._load_records()
|
|
167
|
+
filtered = self._filter_records(records, period, tool)
|
|
168
|
+
return self._aggregate(filtered, period)
|
|
169
|
+
|
|
170
|
+
def _load_records(self) -> list[dict[str, Any]]:
|
|
171
|
+
"""Load all records from JSONL."""
|
|
172
|
+
if not self._path.exists():
|
|
173
|
+
logger.debug(f"Stats file not found: {self._path}")
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
records: list[dict[str, Any]] = []
|
|
177
|
+
try:
|
|
178
|
+
with self._path.open() as f:
|
|
179
|
+
for line in f:
|
|
180
|
+
line = line.strip()
|
|
181
|
+
if line:
|
|
182
|
+
try:
|
|
183
|
+
records.append(json.loads(line))
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
logger.debug(f"Skipping malformed JSON line: {line[:50]}")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(f"Failed to read stats: {e}")
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
return records
|
|
191
|
+
|
|
192
|
+
def _filter_records(
|
|
193
|
+
self,
|
|
194
|
+
records: list[dict[str, Any]],
|
|
195
|
+
period: Period,
|
|
196
|
+
tool: str | None,
|
|
197
|
+
) -> list[dict[str, Any]]:
|
|
198
|
+
"""Filter records by period and tool."""
|
|
199
|
+
if not records:
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
# Calculate period cutoff
|
|
203
|
+
cutoff = self._get_period_cutoff(period)
|
|
204
|
+
|
|
205
|
+
filtered: list[dict[str, Any]] = []
|
|
206
|
+
for record in records:
|
|
207
|
+
# Filter by period
|
|
208
|
+
if cutoff is not None:
|
|
209
|
+
try:
|
|
210
|
+
ts = datetime.fromisoformat(record["ts"])
|
|
211
|
+
if ts < cutoff:
|
|
212
|
+
continue
|
|
213
|
+
except (KeyError, ValueError):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Filter by tool (only applies to tool-type records)
|
|
217
|
+
if (
|
|
218
|
+
tool is not None
|
|
219
|
+
and record.get("type") == "tool"
|
|
220
|
+
and record.get("tool") != tool
|
|
221
|
+
):
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
filtered.append(record)
|
|
225
|
+
|
|
226
|
+
return filtered
|
|
227
|
+
|
|
228
|
+
def _get_period_cutoff(self, period: Period) -> datetime | None:
|
|
229
|
+
"""Get cutoff datetime for period."""
|
|
230
|
+
if period == "all":
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
now = datetime.now(UTC)
|
|
234
|
+
if period == "day":
|
|
235
|
+
return now - timedelta(days=1)
|
|
236
|
+
elif period == "week":
|
|
237
|
+
return now - timedelta(weeks=1)
|
|
238
|
+
elif period == "month":
|
|
239
|
+
return now - timedelta(days=30)
|
|
240
|
+
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def _aggregate(
|
|
244
|
+
self, records: list[dict[str, Any]], period: Period
|
|
245
|
+
) -> AggregatedStats:
|
|
246
|
+
"""Aggregate records into summary stats.
|
|
247
|
+
|
|
248
|
+
Records are split by type:
|
|
249
|
+
- "run" records: contain chars_in/chars_out, used for run counts and savings
|
|
250
|
+
- "tool" records: contain tool name, used for per-tool breakdown
|
|
251
|
+
"""
|
|
252
|
+
if not records:
|
|
253
|
+
return AggregatedStats(
|
|
254
|
+
period=period,
|
|
255
|
+
start_time=None,
|
|
256
|
+
end_time=None,
|
|
257
|
+
total_calls=0,
|
|
258
|
+
success_count=0,
|
|
259
|
+
error_count=0,
|
|
260
|
+
total_chars_in=0,
|
|
261
|
+
total_chars_out=0,
|
|
262
|
+
total_duration_ms=0,
|
|
263
|
+
context_saved=0,
|
|
264
|
+
time_saved_ms=0,
|
|
265
|
+
tools=[],
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Separate run-level and tool-level records
|
|
269
|
+
run_records: list[dict[str, Any]] = []
|
|
270
|
+
tool_records_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
271
|
+
timestamps: list[str] = []
|
|
272
|
+
|
|
273
|
+
for record in records:
|
|
274
|
+
record_type = record.get("type", "run")
|
|
275
|
+
ts = record.get("ts")
|
|
276
|
+
if ts:
|
|
277
|
+
timestamps.append(ts)
|
|
278
|
+
|
|
279
|
+
if record_type == "run":
|
|
280
|
+
run_records.append(record)
|
|
281
|
+
elif record_type == "tool":
|
|
282
|
+
tool_name = record.get("tool", "unknown")
|
|
283
|
+
if tool_name not in tool_records_by_name:
|
|
284
|
+
tool_records_by_name[tool_name] = []
|
|
285
|
+
tool_records_by_name[tool_name].append(record)
|
|
286
|
+
|
|
287
|
+
# Sort timestamps for range
|
|
288
|
+
timestamps.sort()
|
|
289
|
+
|
|
290
|
+
# Aggregate run-level stats
|
|
291
|
+
run_count = len(run_records)
|
|
292
|
+
run_success = sum(1 for r in run_records if r.get("success") is True)
|
|
293
|
+
run_error = run_count - run_success
|
|
294
|
+
total_chars_in = sum(int(r.get("chars_in", 0)) for r in run_records)
|
|
295
|
+
total_chars_out = sum(int(r.get("chars_out", 0)) for r in run_records)
|
|
296
|
+
run_duration = sum(int(r.get("duration_ms", 0)) for r in run_records)
|
|
297
|
+
|
|
298
|
+
# Aggregate per-tool stats
|
|
299
|
+
tool_stats: list[ToolStats] = []
|
|
300
|
+
total_tool_duration = 0
|
|
301
|
+
|
|
302
|
+
for tool_name, tool_records in sorted(tool_records_by_name.items()):
|
|
303
|
+
calls = len(tool_records)
|
|
304
|
+
success = sum(1 for r in tool_records if r.get("success") is True)
|
|
305
|
+
errors = calls - success
|
|
306
|
+
duration = sum(int(r.get("duration_ms", 0)) for r in tool_records)
|
|
307
|
+
|
|
308
|
+
tool_stats.append(
|
|
309
|
+
ToolStats(
|
|
310
|
+
tool=tool_name,
|
|
311
|
+
total_calls=calls,
|
|
312
|
+
success_count=success,
|
|
313
|
+
error_count=errors,
|
|
314
|
+
total_chars_in=0, # Tool records don't have chars
|
|
315
|
+
total_chars_out=0,
|
|
316
|
+
total_duration_ms=duration,
|
|
317
|
+
avg_duration_ms=duration / calls if calls > 0 else 0,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
total_tool_duration += duration
|
|
322
|
+
|
|
323
|
+
# Calculate savings (context and time saved by consolidating run calls)
|
|
324
|
+
context_saved = run_count * self._context_per_call
|
|
325
|
+
time_saved = run_count * self._time_overhead_ms
|
|
326
|
+
|
|
327
|
+
# Calculate cost estimate (actual cost of tokens used)
|
|
328
|
+
input_tokens = total_chars_in / self._chars_per_token
|
|
329
|
+
output_tokens = total_chars_out / self._chars_per_token
|
|
330
|
+
cost_estimate = (
|
|
331
|
+
(input_tokens / 1_000_000) * self._cost_per_m_input
|
|
332
|
+
+ (output_tokens / 1_000_000) * self._cost_per_m_output
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Calculate savings estimate (cost of context overhead avoided)
|
|
336
|
+
savings_usd = (context_saved / 1_000_000) * self._cost_per_m_input
|
|
337
|
+
|
|
338
|
+
return AggregatedStats(
|
|
339
|
+
period=period,
|
|
340
|
+
start_time=timestamps[0] if timestamps else None,
|
|
341
|
+
end_time=timestamps[-1] if timestamps else None,
|
|
342
|
+
total_calls=run_count,
|
|
343
|
+
success_count=run_success,
|
|
344
|
+
error_count=run_error,
|
|
345
|
+
total_chars_in=total_chars_in,
|
|
346
|
+
total_chars_out=total_chars_out,
|
|
347
|
+
total_duration_ms=run_duration,
|
|
348
|
+
context_saved=context_saved,
|
|
349
|
+
time_saved_ms=time_saved,
|
|
350
|
+
tools=tool_stats,
|
|
351
|
+
model=self._model,
|
|
352
|
+
cost_estimate_usd=cost_estimate,
|
|
353
|
+
savings_usd=savings_usd,
|
|
354
|
+
)
|
ot/stats/timing.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Timing context manager for tool call statistics.
|
|
2
|
+
|
|
3
|
+
Provides a reusable context manager that handles timing, success/error
|
|
4
|
+
tracking, and stats recording for tool calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from ot.stats.jsonl_writer import get_client_name, record_tool_stats
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextmanager
|
|
20
|
+
def timed_tool_call(tool_name: str, client: str | None = None) -> Iterator[None]:
|
|
21
|
+
"""Context manager for timing tool calls and recording stats.
|
|
22
|
+
|
|
23
|
+
Measures execution time, tracks success/failure, and records stats
|
|
24
|
+
to the global stats writer.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
tool_name: Fully qualified tool name (e.g., "brave.search")
|
|
28
|
+
client: MCP client name. If None, uses global client name.
|
|
29
|
+
|
|
30
|
+
Yields:
|
|
31
|
+
None
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
with timed_tool_call("brave.search"):
|
|
35
|
+
result = brave.search(query="test")
|
|
36
|
+
"""
|
|
37
|
+
if client is None:
|
|
38
|
+
client = get_client_name()
|
|
39
|
+
|
|
40
|
+
start_time = time.monotonic()
|
|
41
|
+
error_type: str | None = None
|
|
42
|
+
success = True
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
yield
|
|
46
|
+
except Exception as e:
|
|
47
|
+
success = False
|
|
48
|
+
error_type = type(e).__name__
|
|
49
|
+
raise
|
|
50
|
+
finally:
|
|
51
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
52
|
+
record_tool_stats(
|
|
53
|
+
tool=tool_name,
|
|
54
|
+
duration_ms=duration_ms,
|
|
55
|
+
success=success,
|
|
56
|
+
error_type=error_type,
|
|
57
|
+
)
|
ot/support.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Centralized support information for OneTool.
|
|
2
|
+
|
|
3
|
+
Single source of truth for donation/support links, messages, and version.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
# Support URLs
|
|
11
|
+
KOFI_URL = "https://ko-fi.com/beycom"
|
|
12
|
+
KOFI_HANDLE = "beycom"
|
|
13
|
+
|
|
14
|
+
# Support messages
|
|
15
|
+
SUPPORT_MESSAGE = "If you find OneTool useful, please consider supporting development!"
|
|
16
|
+
SUPPORT_MESSAGE_SHORT = "Support OneTool development"
|
|
17
|
+
|
|
18
|
+
# For HTML reports
|
|
19
|
+
SUPPORT_HTML_TITLE = "Support OneTool"
|
|
20
|
+
SUPPORT_HTML_MESSAGE = "If you find this project useful, please consider buying me a coffee!"
|
|
21
|
+
SUPPORT_HTML_BUTTON_TEXT = "Buy me a coffee on Ko-fi"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_support_dict() -> dict[str, str]:
|
|
25
|
+
"""Get support info as a dictionary for JSON output.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict with support URLs and messages
|
|
29
|
+
"""
|
|
30
|
+
return {
|
|
31
|
+
"message": SUPPORT_MESSAGE,
|
|
32
|
+
"kofi_url": KOFI_URL,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_startup_message() -> str:
|
|
37
|
+
"""Get support message for server startup logs.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Formatted startup message with support link
|
|
41
|
+
"""
|
|
42
|
+
return f"{SUPPORT_MESSAGE_SHORT}: {KOFI_URL}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_support_banner() -> str:
|
|
46
|
+
"""Get Rich-formatted support message for CLI banners.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Rich markup string for console.print()
|
|
50
|
+
"""
|
|
51
|
+
return f"[yellow]☕ Please buy me a coffee:[/yellow] [link={KOFI_URL}]{KOFI_URL}[/link]"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_version() -> str:
|
|
55
|
+
"""Get OneTool package version.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Version string, or "dev" if not installed as a package.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return version("onetool")
|
|
62
|
+
except PackageNotFoundError:
|
|
63
|
+
return "dev"
|
ot/tools.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Inter-tool calling API for bundled and extension tools.
|
|
2
|
+
|
|
3
|
+
Provides functions for calling other tools programmatically:
|
|
4
|
+
- call_tool(): Call a tool by its full pack.function name
|
|
5
|
+
- get_pack(): Get a pack proxy for calling multiple functions
|
|
6
|
+
|
|
7
|
+
Example usage in an extension tool:
|
|
8
|
+
|
|
9
|
+
from ot.tools import call_tool, get_pack
|
|
10
|
+
|
|
11
|
+
# Call a single tool by name
|
|
12
|
+
result = call_tool("llm.transform", input=text, prompt="Summarize")
|
|
13
|
+
|
|
14
|
+
# Get a pack for multiple calls
|
|
15
|
+
brave = get_pack("brave")
|
|
16
|
+
results = brave.search(query="test")
|
|
17
|
+
|
|
18
|
+
Note: These functions are only available in bundled and extension tools.
|
|
19
|
+
Isolated tools (subprocess with PEP 723) cannot use this API.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def call_tool(name: str, **kwargs: Any) -> Any:
|
|
28
|
+
"""Call another tool by its full name.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: Full tool name with pack prefix (e.g., "llm.transform", "brave.search").
|
|
32
|
+
Must contain a dot separator.
|
|
33
|
+
**kwargs: Keyword arguments to pass to the tool function.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The result from the tool function.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If name doesn't contain a dot separator.
|
|
40
|
+
KeyError: If the pack or function is not found.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
result = call_tool("llm.transform", input="Hello", prompt="Translate to Spanish")
|
|
44
|
+
"""
|
|
45
|
+
if "." not in name:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Tool name must include pack prefix (e.g., 'pack.function'), got: {name}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from ot.executor.tool_loader import load_tool_registry
|
|
51
|
+
|
|
52
|
+
registry = load_tool_registry()
|
|
53
|
+
|
|
54
|
+
if name not in registry.functions:
|
|
55
|
+
pack_name, func_name = name.rsplit(".", 1)
|
|
56
|
+
|
|
57
|
+
if pack_name not in registry.packs:
|
|
58
|
+
available_packs = ", ".join(sorted(registry.packs.keys()))
|
|
59
|
+
raise KeyError(
|
|
60
|
+
f"Pack '{pack_name}' not found. Available packs: {available_packs}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
pack_funcs = registry.packs[pack_name]
|
|
64
|
+
if hasattr(pack_funcs, "__getattr__"):
|
|
65
|
+
# WorkerPackProxy - list functions differently
|
|
66
|
+
available_funcs = "use get_pack() to discover functions"
|
|
67
|
+
else:
|
|
68
|
+
available_funcs = ", ".join(sorted(pack_funcs.keys()))
|
|
69
|
+
raise KeyError(
|
|
70
|
+
f"Function '{func_name}' not found in pack '{pack_name}'. "
|
|
71
|
+
f"Available: {available_funcs}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return registry.functions[name](**kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_pack(name: str) -> Any:
|
|
78
|
+
"""Get a pack proxy for calling multiple functions.
|
|
79
|
+
|
|
80
|
+
Returns a proxy object that allows calling pack functions using dot notation.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: Pack name (e.g., "brave", "llm", "file").
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Pack proxy object with tool functions as attributes.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
KeyError: If the pack is not found.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
brave = get_pack("brave")
|
|
93
|
+
results = brave.search(query="python")
|
|
94
|
+
|
|
95
|
+
llm = get_pack("llm")
|
|
96
|
+
summary = llm.transform(input=text, prompt="Summarize")
|
|
97
|
+
"""
|
|
98
|
+
from ot.executor.pack_proxy import build_execution_namespace
|
|
99
|
+
from ot.executor.tool_loader import load_tool_registry
|
|
100
|
+
|
|
101
|
+
registry = load_tool_registry()
|
|
102
|
+
|
|
103
|
+
if name not in registry.packs:
|
|
104
|
+
available = ", ".join(sorted(registry.packs.keys()))
|
|
105
|
+
raise KeyError(f"Pack '{name}' not found. Available packs: {available}")
|
|
106
|
+
|
|
107
|
+
# Build namespace to get the wrapped proxy with stats tracking
|
|
108
|
+
namespace = build_execution_namespace(registry)
|
|
109
|
+
|
|
110
|
+
if name in namespace:
|
|
111
|
+
return namespace[name]
|
|
112
|
+
|
|
113
|
+
# Fallback to raw pack (shouldn't happen normally)
|
|
114
|
+
return registry.packs[name]
|
ot/utils/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""OneTool utilities.
|
|
2
|
+
|
|
3
|
+
Provides shared utilities for internal tools:
|
|
4
|
+
- Text processing: truncate, format_error, run_command
|
|
5
|
+
- Batch processing: batch_execute, normalize_items, format_batch_results
|
|
6
|
+
- Caching: cache (TTL-based memoization)
|
|
7
|
+
- HTTP utilities: safe_request, api_headers, check_api_key
|
|
8
|
+
- Dependencies: check_cli, check_lib, ensure_cli, ensure_lib
|
|
9
|
+
- Factory: lazy_client, LazyClient
|
|
10
|
+
|
|
11
|
+
Extension tools (user-created in .onetool/tools/) can import directly from
|
|
12
|
+
ot.* modules for logging, config, and inter-tool calling.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ot.utils.batch import batch_execute, format_batch_results, normalize_items
|
|
16
|
+
from ot.utils.cache import CacheNamespace, cache
|
|
17
|
+
from ot.utils.deps import (
|
|
18
|
+
Dependency,
|
|
19
|
+
DepsCheckResult,
|
|
20
|
+
check_cli,
|
|
21
|
+
check_deps,
|
|
22
|
+
check_lib,
|
|
23
|
+
check_secret,
|
|
24
|
+
ensure_cli,
|
|
25
|
+
ensure_lib,
|
|
26
|
+
requires_cli,
|
|
27
|
+
requires_lib,
|
|
28
|
+
)
|
|
29
|
+
from ot.utils.exceptions import flatten_exception_group
|
|
30
|
+
from ot.utils.factory import LazyClient, lazy_client
|
|
31
|
+
from ot.utils.format import serialize_result
|
|
32
|
+
from ot.utils.http import api_headers, check_api_key, safe_request
|
|
33
|
+
from ot.utils.platform import get_install_hint
|
|
34
|
+
from ot.utils.sanitize import (
|
|
35
|
+
sanitize_output,
|
|
36
|
+
sanitize_tag_closes,
|
|
37
|
+
sanitize_triggers,
|
|
38
|
+
wrap_external_content,
|
|
39
|
+
)
|
|
40
|
+
from ot.utils.truncate import format_error, run_command, truncate
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Cache
|
|
44
|
+
"CacheNamespace",
|
|
45
|
+
"Dependency",
|
|
46
|
+
# Dependencies
|
|
47
|
+
"DepsCheckResult",
|
|
48
|
+
# Factory
|
|
49
|
+
"LazyClient",
|
|
50
|
+
# HTTP
|
|
51
|
+
"api_headers",
|
|
52
|
+
# Batch processing
|
|
53
|
+
"batch_execute",
|
|
54
|
+
"cache",
|
|
55
|
+
"check_api_key",
|
|
56
|
+
"check_cli",
|
|
57
|
+
"check_deps",
|
|
58
|
+
"check_lib",
|
|
59
|
+
"check_secret",
|
|
60
|
+
"ensure_cli",
|
|
61
|
+
"ensure_lib",
|
|
62
|
+
# Existing utilities
|
|
63
|
+
"flatten_exception_group",
|
|
64
|
+
"format_batch_results",
|
|
65
|
+
# Truncate
|
|
66
|
+
"format_error",
|
|
67
|
+
"get_install_hint",
|
|
68
|
+
"lazy_client",
|
|
69
|
+
"normalize_items",
|
|
70
|
+
"requires_cli",
|
|
71
|
+
"requires_lib",
|
|
72
|
+
"run_command",
|
|
73
|
+
"safe_request",
|
|
74
|
+
# Sanitization
|
|
75
|
+
"sanitize_output",
|
|
76
|
+
"sanitize_tag_closes",
|
|
77
|
+
"sanitize_triggers",
|
|
78
|
+
"serialize_result",
|
|
79
|
+
"truncate",
|
|
80
|
+
"wrap_external_content",
|
|
81
|
+
]
|