meshagent-tools 0.20.2__py3-none-any.whl → 0.20.4__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.
- meshagent/tools/database.py +90 -6
- meshagent/tools/datetime.py +535 -0
- meshagent/tools/uuid.py +43 -0
- meshagent/tools/version.py +1 -1
- {meshagent_tools-0.20.2.dist-info → meshagent_tools-0.20.4.dist-info}/METADATA +2 -2
- {meshagent_tools-0.20.2.dist-info → meshagent_tools-0.20.4.dist-info}/RECORD +9 -7
- {meshagent_tools-0.20.2.dist-info → meshagent_tools-0.20.4.dist-info}/WHEEL +0 -0
- {meshagent_tools-0.20.2.dist-info → meshagent_tools-0.20.4.dist-info}/licenses/LICENSE +0 -0
- {meshagent_tools-0.20.2.dist-info → meshagent_tools-0.20.4.dist-info}/top_level.txt +0 -0
meshagent/tools/database.py
CHANGED
|
@@ -91,7 +91,7 @@ class DeleteRowsTool(Tool):
|
|
|
91
91
|
super().__init__(
|
|
92
92
|
name=f"delete_{table}_rows",
|
|
93
93
|
title=f"delete {table} rows",
|
|
94
|
-
description=f"
|
|
94
|
+
description=f"delete from {table} where rows match the specified values (specify null for a column to exclude it from the search)",
|
|
95
95
|
input_schema=input_schema,
|
|
96
96
|
)
|
|
97
97
|
|
|
@@ -179,7 +179,7 @@ class SearchTool(Tool):
|
|
|
179
179
|
self.table = table
|
|
180
180
|
self.namespace = namespace
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
query = {
|
|
183
183
|
"type": "object",
|
|
184
184
|
"required": [],
|
|
185
185
|
"additionalProperties": False,
|
|
@@ -187,9 +187,28 @@ class SearchTool(Tool):
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
for k, v in schema.items():
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
query["required"].append(k)
|
|
191
|
+
query["properties"][k] = v.to_json_schema()
|
|
192
192
|
|
|
193
|
+
input_schema = {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"required": ["query", "limit", "offset", "select"],
|
|
196
|
+
"additionalProperties": False,
|
|
197
|
+
"properties": {
|
|
198
|
+
"query": query,
|
|
199
|
+
"select": {
|
|
200
|
+
"type": "array",
|
|
201
|
+
"description": f"the columns to return, available columns: {','.join(schema.keys())}",
|
|
202
|
+
"items": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
"limit": {"type": "integer"},
|
|
207
|
+
"offset": {"type": "integer"},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
print(input_schema)
|
|
193
212
|
super().__init__(
|
|
194
213
|
name=f"search_{table}",
|
|
195
214
|
title=f"search {table}",
|
|
@@ -197,15 +216,79 @@ class SearchTool(Tool):
|
|
|
197
216
|
input_schema=input_schema,
|
|
198
217
|
)
|
|
199
218
|
|
|
200
|
-
async def execute(
|
|
219
|
+
async def execute(
|
|
220
|
+
self,
|
|
221
|
+
context: ToolContext,
|
|
222
|
+
query: object,
|
|
223
|
+
limit: int,
|
|
224
|
+
offset: int,
|
|
225
|
+
select: list[str],
|
|
226
|
+
):
|
|
201
227
|
search = {}
|
|
202
228
|
|
|
203
|
-
for k, v in
|
|
229
|
+
for k, v in query.items():
|
|
204
230
|
if v is not None:
|
|
205
231
|
search[k] = v
|
|
206
232
|
|
|
207
233
|
return {
|
|
208
234
|
"rows": await context.room.database.search(
|
|
235
|
+
select=select,
|
|
236
|
+
table=self.table,
|
|
237
|
+
where=search if len(search) > 0 else None,
|
|
238
|
+
namespace=self.namespace,
|
|
239
|
+
offset=offset,
|
|
240
|
+
limit=limit,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class CountTool(Tool):
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
table: str,
|
|
250
|
+
schema: dict[str, DataType],
|
|
251
|
+
namespace: Optional[list[str]] = None,
|
|
252
|
+
):
|
|
253
|
+
self.table = table
|
|
254
|
+
self.namespace = namespace
|
|
255
|
+
|
|
256
|
+
query = {
|
|
257
|
+
"type": "object",
|
|
258
|
+
"required": [],
|
|
259
|
+
"additionalProperties": False,
|
|
260
|
+
"properties": {},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
input_schema = {
|
|
264
|
+
"type": "object",
|
|
265
|
+
"required": ["query"],
|
|
266
|
+
"additionalProperties": False,
|
|
267
|
+
"properties": {
|
|
268
|
+
"query": query,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for k, v in schema.items():
|
|
273
|
+
query["required"].append(k)
|
|
274
|
+
query["properties"][k] = v.to_json_schema()
|
|
275
|
+
|
|
276
|
+
super().__init__(
|
|
277
|
+
name=f"count_{table}",
|
|
278
|
+
title=f"count_{table}",
|
|
279
|
+
description=f"count matching rows in the {table} table",
|
|
280
|
+
input_schema=input_schema,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def execute(self, context: ToolContext, query: object):
|
|
284
|
+
search = {}
|
|
285
|
+
|
|
286
|
+
for k, v in query.items():
|
|
287
|
+
if v is not None:
|
|
288
|
+
search[k] = v
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
"rows": await context.room.database.count(
|
|
209
292
|
table=self.table,
|
|
210
293
|
where=search if len(search) > 0 else None,
|
|
211
294
|
namespace=self.namespace,
|
|
@@ -326,6 +409,7 @@ class DatabaseToolkit(RemoteToolkit):
|
|
|
326
409
|
)
|
|
327
410
|
)
|
|
328
411
|
|
|
412
|
+
tools.append(CountTool(table=table, schema=schema, namespace=namespace))
|
|
329
413
|
tools.append(SearchTool(table=table, schema=schema, namespace=namespace))
|
|
330
414
|
tools.append(
|
|
331
415
|
AdvancedSearchTool(table=table, schema=schema, namespace=namespace)
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from typing import Any, Literal, Optional
|
|
3
|
+
from zoneinfo import ZoneInfo
|
|
4
|
+
from .config import ToolkitConfig
|
|
5
|
+
from .tool import Tool
|
|
6
|
+
from .toolkit import ToolContext, ToolkitBuilder
|
|
7
|
+
from .hosting import RemoteToolkit, Toolkit
|
|
8
|
+
from meshagent.api.room_server_client import RoomClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ----------------------------
|
|
12
|
+
# Helpers
|
|
13
|
+
# ----------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _now_utc() -> datetime:
|
|
17
|
+
return datetime.now(timezone.utc)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_tz(tz: Optional[str]) -> timezone:
|
|
21
|
+
"""
|
|
22
|
+
Returns a tzinfo. If tz is None -> UTC.
|
|
23
|
+
If tz is provided, uses zoneinfo when available; falls back to UTC if unknown.
|
|
24
|
+
"""
|
|
25
|
+
if not tz:
|
|
26
|
+
return timezone.utc
|
|
27
|
+
if ZoneInfo is None:
|
|
28
|
+
# zoneinfo not available; safest fallback
|
|
29
|
+
return timezone.utc
|
|
30
|
+
try:
|
|
31
|
+
return ZoneInfo(tz) # type: ignore[arg-type]
|
|
32
|
+
except Exception:
|
|
33
|
+
return timezone.utc
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_aware(dt: datetime, tz: Optional[str] = None) -> datetime:
|
|
37
|
+
"""
|
|
38
|
+
If dt is naive, interpret it in tz (or UTC if tz not provided) and make aware.
|
|
39
|
+
If aware, leave as-is.
|
|
40
|
+
"""
|
|
41
|
+
if dt.tzinfo is None:
|
|
42
|
+
return dt.replace(tzinfo=_get_tz(tz))
|
|
43
|
+
return dt
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _to_tz(dt: datetime, tz: Optional[str]) -> datetime:
|
|
47
|
+
dt = _ensure_aware(dt, tz=None)
|
|
48
|
+
return dt.astimezone(_get_tz(tz))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _iso(dt: datetime) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Canonical DB-friendly ISO with offset.
|
|
54
|
+
"""
|
|
55
|
+
dt = _ensure_aware(dt)
|
|
56
|
+
# Keep offset; many DBs accept it; if you prefer 'Z', format_utc tool handles that.
|
|
57
|
+
return dt.isoformat()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_iso(s: str, assume_tz: Optional[str] = None) -> datetime:
|
|
61
|
+
"""
|
|
62
|
+
Parse ISO-8601-ish strings. If it ends with 'Z', treat as UTC.
|
|
63
|
+
If parsed datetime is naive, attach assume_tz (or UTC).
|
|
64
|
+
"""
|
|
65
|
+
s2 = s.strip()
|
|
66
|
+
if s2.endswith("Z"):
|
|
67
|
+
s2 = s2[:-1] + "+00:00"
|
|
68
|
+
dt = datetime.fromisoformat(s2)
|
|
69
|
+
return _ensure_aware(dt, assume_tz)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _start_of_day(dt: datetime) -> datetime:
|
|
73
|
+
dt = _ensure_aware(dt)
|
|
74
|
+
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _end_of_day(dt: datetime) -> datetime:
|
|
78
|
+
# inclusive end: 23:59:59.999999
|
|
79
|
+
dt = _ensure_aware(dt)
|
|
80
|
+
return dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _start_of_month(dt: datetime) -> datetime:
|
|
84
|
+
dt = _ensure_aware(dt)
|
|
85
|
+
return dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _end_of_month(dt: datetime) -> datetime:
|
|
89
|
+
dt = _ensure_aware(dt)
|
|
90
|
+
# go to first of next month then subtract a microsecond
|
|
91
|
+
if dt.month == 12:
|
|
92
|
+
nxt = dt.replace(
|
|
93
|
+
year=dt.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
nxt = dt.replace(
|
|
97
|
+
month=dt.month + 1, day=1, hour=0, minute=0, second=0, microsecond=0
|
|
98
|
+
)
|
|
99
|
+
return nxt - timedelta(microseconds=1)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _start_of_week(dt: datetime, week_start: int) -> datetime:
|
|
103
|
+
"""
|
|
104
|
+
week_start: 0=Mon ... 6=Sun
|
|
105
|
+
"""
|
|
106
|
+
dt = _ensure_aware(dt)
|
|
107
|
+
# Python weekday(): Mon=0..Sun=6
|
|
108
|
+
delta_days = (dt.weekday() - week_start) % 7
|
|
109
|
+
return _start_of_day(dt - timedelta(days=delta_days))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _end_of_week(dt: datetime, week_start: int) -> datetime:
|
|
113
|
+
return (
|
|
114
|
+
_start_of_week(dt, week_start) + timedelta(days=7) - timedelta(microseconds=1)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ----------------------------
|
|
119
|
+
# Tools
|
|
120
|
+
# ----------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NowTool(Tool):
|
|
124
|
+
def __init__(self):
|
|
125
|
+
super().__init__(
|
|
126
|
+
name="now",
|
|
127
|
+
title="now",
|
|
128
|
+
description="Get current time. Returns both UTC and (optional) timezone-local ISO strings.",
|
|
129
|
+
input_schema={
|
|
130
|
+
"type": "object",
|
|
131
|
+
"required": ["tz"],
|
|
132
|
+
"additionalProperties": False,
|
|
133
|
+
"properties": {
|
|
134
|
+
"tz": {
|
|
135
|
+
"type": ["string", "null"],
|
|
136
|
+
"description": "IANA timezone name (e.g. 'America/Los_Angeles'). If omitted, only UTC is returned.",
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def execute(self, context: ToolContext, tz: Optional[str] = None):
|
|
143
|
+
utc = _now_utc()
|
|
144
|
+
out: dict[str, Any] = {"utc": _iso(utc)}
|
|
145
|
+
if tz:
|
|
146
|
+
out["local"] = _iso(_to_tz(utc, tz))
|
|
147
|
+
out["tz"] = tz
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TodayTool(Tool):
|
|
152
|
+
def __init__(self):
|
|
153
|
+
super().__init__(
|
|
154
|
+
name="today_range",
|
|
155
|
+
title="today range",
|
|
156
|
+
description="Get the start/end of 'today' in a given timezone (defaults to UTC). Useful for date filters.",
|
|
157
|
+
input_schema={
|
|
158
|
+
"type": "object",
|
|
159
|
+
"required": ["tz"],
|
|
160
|
+
"additionalProperties": False,
|
|
161
|
+
"properties": {
|
|
162
|
+
"tz": {
|
|
163
|
+
"type": ["string", "null"],
|
|
164
|
+
"description": "IANA timezone name (default UTC).",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def execute(self, context: ToolContext, tz: Optional[str] = None):
|
|
171
|
+
base = _to_tz(_now_utc(), tz)
|
|
172
|
+
start = _start_of_day(base)
|
|
173
|
+
end = _end_of_day(base)
|
|
174
|
+
return {"start": _iso(start), "end": _iso(end), "tz": tz or "UTC"}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class WeekRangeTool(Tool):
|
|
178
|
+
def __init__(self):
|
|
179
|
+
super().__init__(
|
|
180
|
+
name="week_range",
|
|
181
|
+
title="week range",
|
|
182
|
+
description="Get start/end of the week containing a given datetime (or now), in a timezone. Week start configurable.",
|
|
183
|
+
input_schema={
|
|
184
|
+
"type": "object",
|
|
185
|
+
"required": ["dt", "tz", "week_start"],
|
|
186
|
+
"additionalProperties": False,
|
|
187
|
+
"properties": {
|
|
188
|
+
"dt": {
|
|
189
|
+
"type": ["string", "null"],
|
|
190
|
+
"description": "ISO datetime. If omitted, uses now.",
|
|
191
|
+
},
|
|
192
|
+
"tz": {
|
|
193
|
+
"type": ["string", "null"],
|
|
194
|
+
"description": "IANA timezone name (default UTC).",
|
|
195
|
+
},
|
|
196
|
+
"week_start": {
|
|
197
|
+
"type": "integer",
|
|
198
|
+
"description": "0=Mon .. 6=Sun (default 0).",
|
|
199
|
+
"minimum": 0,
|
|
200
|
+
"maximum": 6,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
async def execute(
|
|
207
|
+
self,
|
|
208
|
+
context: ToolContext,
|
|
209
|
+
dt: Optional[str] = None,
|
|
210
|
+
tz: Optional[str] = None,
|
|
211
|
+
week_start: int = 0,
|
|
212
|
+
):
|
|
213
|
+
base = _to_tz(_parse_iso(dt, assume_tz=tz) if dt else _now_utc(), tz)
|
|
214
|
+
start = _start_of_week(base, week_start)
|
|
215
|
+
end = _end_of_week(base, week_start)
|
|
216
|
+
iso_year, iso_week, iso_wday = base.isocalendar()
|
|
217
|
+
return {
|
|
218
|
+
"start": _iso(start),
|
|
219
|
+
"end": _iso(end),
|
|
220
|
+
"tz": tz or "UTC",
|
|
221
|
+
"week_start": week_start,
|
|
222
|
+
"iso": {"year": iso_year, "week": iso_week, "weekday": iso_wday},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class MonthRangeTool(Tool):
|
|
227
|
+
def __init__(self):
|
|
228
|
+
super().__init__(
|
|
229
|
+
name="month_range",
|
|
230
|
+
title="month range",
|
|
231
|
+
description="Get start/end of the month containing a given datetime (or now), in a timezone.",
|
|
232
|
+
input_schema={
|
|
233
|
+
"type": "object",
|
|
234
|
+
"required": ["dt", "tz"],
|
|
235
|
+
"additionalProperties": False,
|
|
236
|
+
"properties": {
|
|
237
|
+
"dt": {
|
|
238
|
+
"type": ["string", "null"],
|
|
239
|
+
"description": "ISO datetime. If omitted, uses now.",
|
|
240
|
+
},
|
|
241
|
+
"tz": {
|
|
242
|
+
"type": ["string", "null"],
|
|
243
|
+
"description": "IANA timezone name (default UTC).",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
async def execute(
|
|
250
|
+
self, context: ToolContext, dt: Optional[str] = None, tz: Optional[str] = None
|
|
251
|
+
):
|
|
252
|
+
base = _to_tz(_parse_iso(dt, assume_tz=tz) if dt else _now_utc(), tz)
|
|
253
|
+
start = _start_of_month(base)
|
|
254
|
+
end = _end_of_month(base)
|
|
255
|
+
return {
|
|
256
|
+
"start": _iso(start),
|
|
257
|
+
"end": _iso(end),
|
|
258
|
+
"tz": tz or "UTC",
|
|
259
|
+
"year": base.year,
|
|
260
|
+
"month": base.month,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class AddDurationTool(Tool):
|
|
265
|
+
def __init__(self):
|
|
266
|
+
super().__init__(
|
|
267
|
+
name="add_duration",
|
|
268
|
+
title="add duration",
|
|
269
|
+
description="Add a duration to an ISO datetime. Supports days/hours/minutes/seconds.",
|
|
270
|
+
input_schema={
|
|
271
|
+
"type": "object",
|
|
272
|
+
"required": ["dt", "tz", "days", "hours", "minutes", "seconds"],
|
|
273
|
+
"additionalProperties": False,
|
|
274
|
+
"properties": {
|
|
275
|
+
"dt": {"type": "string", "description": "Base ISO datetime."},
|
|
276
|
+
"tz": {
|
|
277
|
+
"type": ["string", "null"],
|
|
278
|
+
"description": "IANA timezone name. If dt is naive, interpret it in this timezone (default UTC). Also controls output tz.",
|
|
279
|
+
},
|
|
280
|
+
"days": {"type": "integer"},
|
|
281
|
+
"hours": {"type": "integer"},
|
|
282
|
+
"minutes": {"type": "integer"},
|
|
283
|
+
"seconds": {"type": "integer"},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def execute(
|
|
289
|
+
self,
|
|
290
|
+
context: ToolContext,
|
|
291
|
+
*,
|
|
292
|
+
dt: str,
|
|
293
|
+
tz: Optional[str] = None,
|
|
294
|
+
days: int = 0,
|
|
295
|
+
hours: int = 0,
|
|
296
|
+
minutes: int = 0,
|
|
297
|
+
seconds: int = 0,
|
|
298
|
+
):
|
|
299
|
+
base = _parse_iso(dt, assume_tz=tz)
|
|
300
|
+
base = _to_tz(base, tz)
|
|
301
|
+
out = base + timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
|
302
|
+
return {"dt": _iso(out), "tz": tz or "UTC"}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class DiffTool(Tool):
|
|
306
|
+
def __init__(self):
|
|
307
|
+
super().__init__(
|
|
308
|
+
name="diff",
|
|
309
|
+
title="diff",
|
|
310
|
+
description="Compute dt2 - dt1. Returns seconds, and a simple breakdown.",
|
|
311
|
+
input_schema={
|
|
312
|
+
"type": "object",
|
|
313
|
+
"required": ["dt1", "dt2", "assume_tz"],
|
|
314
|
+
"additionalProperties": False,
|
|
315
|
+
"properties": {
|
|
316
|
+
"dt1": {"type": "string", "description": "ISO datetime 1."},
|
|
317
|
+
"dt2": {"type": "string", "description": "ISO datetime 2."},
|
|
318
|
+
"assume_tz": {
|
|
319
|
+
"type": ["string", "null"],
|
|
320
|
+
"description": "IANA timezone name. If either input is naive, interpret it in this timezone (default UTC).",
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
async def execute(
|
|
327
|
+
self,
|
|
328
|
+
context: ToolContext,
|
|
329
|
+
*,
|
|
330
|
+
dt1: str,
|
|
331
|
+
dt2: str,
|
|
332
|
+
assume_tz: Optional[str] = None,
|
|
333
|
+
):
|
|
334
|
+
a = _parse_iso(dt1, assume_tz=assume_tz)
|
|
335
|
+
b = _parse_iso(dt2, assume_tz=assume_tz)
|
|
336
|
+
delta = b - a
|
|
337
|
+
total_seconds = int(delta.total_seconds())
|
|
338
|
+
sign = -1 if total_seconds < 0 else 1
|
|
339
|
+
secs = abs(total_seconds)
|
|
340
|
+
|
|
341
|
+
days, rem = divmod(secs, 86400)
|
|
342
|
+
hours, rem = divmod(rem, 3600)
|
|
343
|
+
minutes, seconds = divmod(rem, 60)
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"seconds": total_seconds,
|
|
347
|
+
"breakdown": {
|
|
348
|
+
"sign": sign,
|
|
349
|
+
"days": days,
|
|
350
|
+
"hours": hours,
|
|
351
|
+
"minutes": minutes,
|
|
352
|
+
"seconds": seconds,
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class ParseTool(Tool):
|
|
358
|
+
def __init__(self):
|
|
359
|
+
super().__init__(
|
|
360
|
+
name="parse_iso",
|
|
361
|
+
title="parse iso datetime",
|
|
362
|
+
description="Parse an ISO datetime string and return normalized ISO plus components.",
|
|
363
|
+
input_schema={
|
|
364
|
+
"type": "object",
|
|
365
|
+
"required": ["dt", "assume_tz", "tz"],
|
|
366
|
+
"additionalProperties": False,
|
|
367
|
+
"properties": {
|
|
368
|
+
"dt": {
|
|
369
|
+
"type": "string",
|
|
370
|
+
"description": "ISO datetime (accepts trailing 'Z').",
|
|
371
|
+
},
|
|
372
|
+
"assume_tz": {
|
|
373
|
+
"type": ["string", "null"],
|
|
374
|
+
"description": "IANA timezone name. If dt is naive, interpret it in this timezone (default UTC).",
|
|
375
|
+
},
|
|
376
|
+
"tz": {
|
|
377
|
+
"type": ["string", "null"],
|
|
378
|
+
"description": "IANA timezone name. Convert output to this timezone (default keep parsed tz).",
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
async def execute(
|
|
385
|
+
self,
|
|
386
|
+
context: ToolContext,
|
|
387
|
+
*,
|
|
388
|
+
dt: str,
|
|
389
|
+
assume_tz: Optional[str] = None,
|
|
390
|
+
tz: Optional[str] = None,
|
|
391
|
+
):
|
|
392
|
+
parsed = _parse_iso(dt, assume_tz=assume_tz)
|
|
393
|
+
if tz:
|
|
394
|
+
parsed = _to_tz(parsed, tz)
|
|
395
|
+
iso_year, iso_week, iso_wday = parsed.isocalendar()
|
|
396
|
+
return {
|
|
397
|
+
"iso": _iso(parsed),
|
|
398
|
+
"components": {
|
|
399
|
+
"year": parsed.year,
|
|
400
|
+
"month": parsed.month,
|
|
401
|
+
"day": parsed.day,
|
|
402
|
+
"hour": parsed.hour,
|
|
403
|
+
"minute": parsed.minute,
|
|
404
|
+
"second": parsed.second,
|
|
405
|
+
"microsecond": parsed.microsecond,
|
|
406
|
+
},
|
|
407
|
+
"weekday": parsed.weekday(), # Mon=0..Sun=6
|
|
408
|
+
"iso_week": {"year": iso_year, "week": iso_week, "weekday": iso_wday},
|
|
409
|
+
"tz": str(parsed.tzinfo),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class FormatTool(Tool):
|
|
414
|
+
def __init__(self):
|
|
415
|
+
super().__init__(
|
|
416
|
+
name="format_dt",
|
|
417
|
+
title="format datetime",
|
|
418
|
+
description="Format an ISO datetime using strftime. (Use for human-readable strings.)",
|
|
419
|
+
input_schema={
|
|
420
|
+
"type": "object",
|
|
421
|
+
"required": ["dt", "fmt", "assume_tz", "tz"],
|
|
422
|
+
"additionalProperties": False,
|
|
423
|
+
"properties": {
|
|
424
|
+
"dt": {"type": "string", "description": "ISO datetime."},
|
|
425
|
+
"fmt": {
|
|
426
|
+
"type": "string",
|
|
427
|
+
"description": "strftime format string, e.g. '%Y-%m-%d %H:%M:%S'.",
|
|
428
|
+
},
|
|
429
|
+
"assume_tz": {
|
|
430
|
+
"type": ["string", "null"],
|
|
431
|
+
"description": "IANA timezone name. If dt is naive, interpret it in this timezone (default UTC).",
|
|
432
|
+
},
|
|
433
|
+
"tz": {
|
|
434
|
+
"type": ["string", "null"],
|
|
435
|
+
"description": "IANA timezone name. Convert before formatting (default: keep).",
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
async def execute(
|
|
442
|
+
self,
|
|
443
|
+
context: ToolContext,
|
|
444
|
+
*,
|
|
445
|
+
dt: str,
|
|
446
|
+
fmt: str,
|
|
447
|
+
assume_tz: Optional[str] = None,
|
|
448
|
+
tz: Optional[str] = None,
|
|
449
|
+
):
|
|
450
|
+
parsed = _parse_iso(dt, assume_tz=assume_tz)
|
|
451
|
+
if tz:
|
|
452
|
+
parsed = _to_tz(parsed, tz)
|
|
453
|
+
return {"text": parsed.strftime(fmt)}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class UtcZTool(Tool):
|
|
457
|
+
def __init__(self):
|
|
458
|
+
super().__init__(
|
|
459
|
+
name="to_utc_z",
|
|
460
|
+
title="to utc Z",
|
|
461
|
+
description="Convert an ISO datetime to UTC and return an RFC3339-ish Z string (e.g. 2026-01-11T12:34:56Z).",
|
|
462
|
+
input_schema={
|
|
463
|
+
"type": "object",
|
|
464
|
+
"required": ["dt", "assume_tz", "drop_microseconds"],
|
|
465
|
+
"additionalProperties": False,
|
|
466
|
+
"properties": {
|
|
467
|
+
"dt": {"type": "string", "description": "ISO datetime."},
|
|
468
|
+
"assume_tz": {
|
|
469
|
+
"type": ["string", "null"],
|
|
470
|
+
"description": "IANA timezone name. If dt is naive, interpret it in this timezone (default UTC).",
|
|
471
|
+
},
|
|
472
|
+
"drop_microseconds": {
|
|
473
|
+
"type": "boolean",
|
|
474
|
+
"description": "Default true.",
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
async def execute(
|
|
481
|
+
self,
|
|
482
|
+
context: ToolContext,
|
|
483
|
+
*,
|
|
484
|
+
dt: str,
|
|
485
|
+
assume_tz: Optional[str] = None,
|
|
486
|
+
drop_microseconds: bool = True,
|
|
487
|
+
):
|
|
488
|
+
parsed = _parse_iso(dt, assume_tz=assume_tz)
|
|
489
|
+
u = parsed.astimezone(timezone.utc)
|
|
490
|
+
if drop_microseconds:
|
|
491
|
+
u = u.replace(microsecond=0)
|
|
492
|
+
# emit Z
|
|
493
|
+
s = u.isoformat().replace("+00:00", "Z")
|
|
494
|
+
return {"dt": s}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ----------------------------
|
|
498
|
+
# Toolkit
|
|
499
|
+
# ----------------------------
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class DatetimeToolkit(RemoteToolkit):
|
|
503
|
+
def __init__(self):
|
|
504
|
+
tools = [
|
|
505
|
+
NowTool(),
|
|
506
|
+
TodayTool(),
|
|
507
|
+
WeekRangeTool(),
|
|
508
|
+
MonthRangeTool(),
|
|
509
|
+
AddDurationTool(),
|
|
510
|
+
DiffTool(),
|
|
511
|
+
ParseTool(),
|
|
512
|
+
FormatTool(),
|
|
513
|
+
UtcZTool(),
|
|
514
|
+
]
|
|
515
|
+
super().__init__(
|
|
516
|
+
name="datetime",
|
|
517
|
+
title="datetime",
|
|
518
|
+
description="Useful datetime utilities: now/ranges/parse/format/add/diff",
|
|
519
|
+
tools=tools,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class DatetimeToolkitConfig(ToolkitConfig):
|
|
524
|
+
name: Literal["datetime"] = "datetime"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class DatetimeToolkitBuilder(ToolkitBuilder):
|
|
528
|
+
def __init__(self):
|
|
529
|
+
super().__init__(name="datetime", type=DatetimeToolkitConfig)
|
|
530
|
+
|
|
531
|
+
async def make(
|
|
532
|
+
self, *, room: RoomClient, model: str, config: DatetimeToolkitConfig
|
|
533
|
+
) -> Toolkit:
|
|
534
|
+
# no room dependency required; purely local computations
|
|
535
|
+
return DatetimeToolkit()
|
meshagent/tools/uuid.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from .tool import Tool
|
|
3
|
+
from .toolkit import ToolContext
|
|
4
|
+
from .hosting import RemoteToolkit
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UuidV4Tool(Tool):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__(
|
|
10
|
+
name="uuid_v4",
|
|
11
|
+
title="uuid v4",
|
|
12
|
+
description="Generate UUIDv4 strings (standard 8-4-4-4-12 format).",
|
|
13
|
+
input_schema={
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["count"],
|
|
16
|
+
"additionalProperties": False,
|
|
17
|
+
"properties": {
|
|
18
|
+
"count": {
|
|
19
|
+
"type": "integer",
|
|
20
|
+
"description": "How many UUIDs to generate (default 1).",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def execute(self, context: ToolContext, *, count: int = 1):
|
|
27
|
+
# uuid.uuid4() returns a UUID object; str(...) yields canonical form:
|
|
28
|
+
# xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
29
|
+
uuids = [str(uuid.uuid4()) for _ in range(int(count or 1))]
|
|
30
|
+
if count == 1:
|
|
31
|
+
return {"uuid": uuids[0]}
|
|
32
|
+
return {"uuids": uuids, "count": len(uuids)}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UUIDToolkit(RemoteToolkit):
|
|
36
|
+
def __init__(self):
|
|
37
|
+
tools = [UuidV4Tool()]
|
|
38
|
+
super().__init__(
|
|
39
|
+
name="uuid",
|
|
40
|
+
title="uuid",
|
|
41
|
+
description="Generate uuids",
|
|
42
|
+
tools=tools,
|
|
43
|
+
)
|
meshagent/tools/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.20.
|
|
1
|
+
__version__ = "0.20.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-tools
|
|
3
|
-
Version: 0.20.
|
|
3
|
+
Version: 0.20.4
|
|
4
4
|
Summary: Tools for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -12,7 +12,7 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: pyjwt~=2.10
|
|
13
13
|
Requires-Dist: pytest~=8.4
|
|
14
14
|
Requires-Dist: pytest-asyncio~=0.26
|
|
15
|
-
Requires-Dist: meshagent-api~=0.20.
|
|
15
|
+
Requires-Dist: meshagent-api~=0.20.4
|
|
16
16
|
Requires-Dist: aiohttp~=3.10
|
|
17
17
|
Requires-Dist: opentelemetry-distro~=0.54b1
|
|
18
18
|
Dynamic: license-file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
meshagent/tools/__init__.py,sha256=1zgD5OvAJP10eERoE72VbDu9hFVfAqCWUXw3SiCYFTE,1285
|
|
2
2
|
meshagent/tools/blob.py,sha256=aDW_z8R4HrmrYzAWoWm13Ypqcxcl4rL1Dc0ESnQETLM,1473
|
|
3
3
|
meshagent/tools/config.py,sha256=zH2xGxg28K7Tg-aYor6LXdzf0LRxS9iE0679H1FuWhE,79
|
|
4
|
-
meshagent/tools/database.py,sha256=
|
|
4
|
+
meshagent/tools/database.py,sha256=TXy7L7_0IxjKM9TqZlx8om4SP_bTjTWqPfNosljb0kE,12910
|
|
5
|
+
meshagent/tools/datetime.py,sha256=2pOUOWopYIsc5y4EoFo_1PdBaBcTSkeOOs_EqdqYTk0,17503
|
|
5
6
|
meshagent/tools/discovery.py,sha256=GZcC4XCgk0ftcYCBxuWlaIsLV5vU4-gXiu0HhlDUlwY,1861
|
|
6
7
|
meshagent/tools/document_tools.py,sha256=LMULXOSBjsvhKjqzxUxe8586t0Vol0v1Btu5v6ofm7A,11755
|
|
7
8
|
meshagent/tools/hosting.py,sha256=l1BCgnSrCJQsWU9Kycq3hEI4ZlYxffDfde6QeJUfko0,10678
|
|
@@ -11,10 +12,11 @@ meshagent/tools/storage.py,sha256=Y79G__Mp4swJ3tnm-zXtD-SqDeKU9kqHEVoUQH_QedA,72
|
|
|
11
12
|
meshagent/tools/strict_schema.py,sha256=IytdAANa6lsfrsg5FsJuqYrxH9D_fayl-Lc9EwgLJSM,6277
|
|
12
13
|
meshagent/tools/tool.py,sha256=9OAlbfaHqfgJnCDBSW-8PS0Z1K1KjWGD3JBUyiHOxAk,3131
|
|
13
14
|
meshagent/tools/toolkit.py,sha256=rCCkpQBoSkmmhjnRGA4jx0QP-ds6WTJ0PkQVnf1Ls7s,3843
|
|
14
|
-
meshagent/tools/
|
|
15
|
+
meshagent/tools/uuid.py,sha256=mzRwDmXy39U5lHhd9wqV4r-ZdS8jPfDTTs4UfW4KHJQ,1342
|
|
16
|
+
meshagent/tools/version.py,sha256=8BDEBRu96nHUJh0tYmos3SRV4xNzQNFZYR61tRE85tQ,23
|
|
15
17
|
meshagent/tools/web_toolkit.py,sha256=IoOYjOBmcbQsqWT14xYg02jjWpWmGOkDSxt2U-LQoaA,1258
|
|
16
|
-
meshagent_tools-0.20.
|
|
17
|
-
meshagent_tools-0.20.
|
|
18
|
-
meshagent_tools-0.20.
|
|
19
|
-
meshagent_tools-0.20.
|
|
20
|
-
meshagent_tools-0.20.
|
|
18
|
+
meshagent_tools-0.20.4.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
|
|
19
|
+
meshagent_tools-0.20.4.dist-info/METADATA,sha256=oxxgXJEDdBwB6MY6j5u8ZmlCorTdDux_hF18U_KAsdU,2878
|
|
20
|
+
meshagent_tools-0.20.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
meshagent_tools-0.20.4.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
22
|
+
meshagent_tools-0.20.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|