meshagent-tools 0.19.1__tar.gz → 0.20.5__tar.gz

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 (30) hide show
  1. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/CHANGELOG.md +40 -0
  2. {meshagent_tools-0.19.1/meshagent_tools.egg-info → meshagent_tools-0.20.5}/PKG-INFO +2 -2
  3. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/database.py +91 -11
  4. meshagent_tools-0.20.5/meshagent/tools/datetime.py +535 -0
  5. meshagent_tools-0.20.5/meshagent/tools/uuid.py +43 -0
  6. meshagent_tools-0.20.5/meshagent/tools/version.py +1 -0
  7. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5/meshagent_tools.egg-info}/PKG-INFO +2 -2
  8. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent_tools.egg-info/SOURCES.txt +2 -0
  9. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent_tools.egg-info/requires.txt +1 -1
  10. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/pyproject.toml +1 -1
  11. meshagent_tools-0.19.1/meshagent/tools/version.py +0 -1
  12. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/LICENSE +0 -0
  13. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/MANIFEST.in +0 -0
  14. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/README.md +0 -0
  15. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/__init__.py +0 -0
  16. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/blob.py +0 -0
  17. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/config.py +0 -0
  18. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/discovery.py +0 -0
  19. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/document_tools.py +0 -0
  20. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/hosting.py +0 -0
  21. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/multi_tool.py +0 -0
  22. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/pydantic.py +0 -0
  23. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/storage.py +0 -0
  24. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/strict_schema.py +0 -0
  25. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/tool.py +0 -0
  26. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/toolkit.py +0 -0
  27. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent/tools/web_toolkit.py +0 -0
  28. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent_tools.egg-info/dependency_links.txt +0 -0
  29. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/meshagent_tools.egg-info/top_level.txt +0 -0
  30. {meshagent_tools-0.19.1 → meshagent_tools-0.20.5}/setup.cfg +0 -0
@@ -1,3 +1,43 @@
1
+ ## [0.20.5]
2
+ - Stability
3
+
4
+ ## [0.20.4]
5
+ - Stability
6
+
7
+ ## [0.20.3]
8
+ - Stability
9
+
10
+ ## [0.20.2]
11
+ - Stability
12
+
13
+ ## [0.20.1]
14
+ - Stability
15
+
16
+ ## [0.20.0]
17
+ - Breaking: mailbox create/update requests must now include a `public` flag (SDK defaults to `False` when omitted in method calls)
18
+ - Mailbox response models include a `public` field
19
+ - Breaking: service specs now require either `external` or `container` to be set
20
+ - External service specs allow omitting the base URL
21
+ - Service template variables include optional `annotations` metadata
22
+ - CLI mailbox commands support `--public` and include the `public` value in listings
23
+ - Mailbot whitelist parsing accepts comma-separated values
24
+ - Fixed JSON schema generation for database delete/search tools
25
+
26
+ ## [0.19.5]
27
+ - Stability
28
+
29
+ ## [0.19.4]
30
+ - Stability
31
+
32
+ ## [0.19.3]
33
+ - Stability
34
+
35
+ ## [0.19.2]
36
+ - Add boolean data type support plus `nullable`/`metadata` on schema types and generated JSON Schema.
37
+ - BREAKING: OpenAI proxy client creation now takes an optional `http_client` (request logging is configured via a separate logging client helper).
38
+ - Shell tool now reuses a long-lived container with a writable root filesystem, runs commands via `bash -lc`, and defaults to the `python:3.13` image.
39
+ - Add `log_llm_requests` support to enable OpenAI request/response logging.
40
+
1
41
  ## [0.19.1]
2
42
  - Add optional metadata to agent chat contexts and propagate it through message-stream LLM delegation, including recording thread participant lists
3
43
  - Add an option for the mailbot CLI to delegate LLM interactions to a remote participant instead of using the local LLM adapter
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-tools
3
- Version: 0.19.1
3
+ Version: 0.20.5
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.19.1
15
+ Requires-Dist: meshagent-api~=0.20.5
16
16
  Requires-Dist: aiohttp~=3.10
17
17
  Requires-Dist: opentelemetry-distro~=0.54b1
18
18
  Dynamic: license-file
@@ -86,14 +86,12 @@ class DeleteRowsTool(Tool):
86
86
 
87
87
  for k, v in schema.items():
88
88
  input_schema["required"].append(k)
89
- schema = v.to_json_schema()
90
- schema["type"] = [schema["type"], "null"]
91
- input_schema["properties"][k] = schema
89
+ input_schema["properties"][k] = v.to_json_schema()
92
90
 
93
91
  super().__init__(
94
92
  name=f"delete_{table}_rows",
95
93
  title=f"delete {table} rows",
96
- description=f"search {table} table for rows with the specified values (specify null for a column to exclude it from the search) and then delete them",
94
+ description=f"delete from {table} where rows match the specified values (specify null for a column to exclude it from the search)",
97
95
  input_schema=input_schema,
98
96
  )
99
97
 
@@ -181,7 +179,7 @@ class SearchTool(Tool):
181
179
  self.table = table
182
180
  self.namespace = namespace
183
181
 
184
- input_schema = {
182
+ query = {
185
183
  "type": "object",
186
184
  "required": [],
187
185
  "additionalProperties": False,
@@ -189,11 +187,28 @@ class SearchTool(Tool):
189
187
  }
190
188
 
191
189
  for k, v in schema.items():
192
- input_schema["required"].append(k)
193
- schema = v.to_json_schema()
194
- schema["type"] = [schema["type"], "null"]
195
- input_schema["properties"][k] = schema
190
+ query["required"].append(k)
191
+ query["properties"][k] = v.to_json_schema()
196
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)
197
212
  super().__init__(
198
213
  name=f"search_{table}",
199
214
  title=f"search {table}",
@@ -201,15 +216,79 @@ class SearchTool(Tool):
201
216
  input_schema=input_schema,
202
217
  )
203
218
 
204
- async def execute(self, context: ToolContext, **values):
219
+ async def execute(
220
+ self,
221
+ context: ToolContext,
222
+ query: object,
223
+ limit: int,
224
+ offset: int,
225
+ select: list[str],
226
+ ):
205
227
  search = {}
206
228
 
207
- for k, v in values.items():
229
+ for k, v in query.items():
208
230
  if v is not None:
209
231
  search[k] = v
210
232
 
211
233
  return {
212
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(
213
292
  table=self.table,
214
293
  where=search if len(search) > 0 else None,
215
294
  namespace=self.namespace,
@@ -330,6 +409,7 @@ class DatabaseToolkit(RemoteToolkit):
330
409
  )
331
410
  )
332
411
 
412
+ tools.append(CountTool(table=table, schema=schema, namespace=namespace))
333
413
  tools.append(SearchTool(table=table, schema=schema, namespace=namespace))
334
414
  tools.append(
335
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()
@@ -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
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.20.5"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-tools
3
- Version: 0.19.1
3
+ Version: 0.20.5
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.19.1
15
+ Requires-Dist: meshagent-api~=0.20.5
16
16
  Requires-Dist: aiohttp~=3.10
17
17
  Requires-Dist: opentelemetry-distro~=0.54b1
18
18
  Dynamic: license-file
@@ -7,6 +7,7 @@ meshagent/tools/__init__.py
7
7
  meshagent/tools/blob.py
8
8
  meshagent/tools/config.py
9
9
  meshagent/tools/database.py
10
+ meshagent/tools/datetime.py
10
11
  meshagent/tools/discovery.py
11
12
  meshagent/tools/document_tools.py
12
13
  meshagent/tools/hosting.py
@@ -16,6 +17,7 @@ meshagent/tools/storage.py
16
17
  meshagent/tools/strict_schema.py
17
18
  meshagent/tools/tool.py
18
19
  meshagent/tools/toolkit.py
20
+ meshagent/tools/uuid.py
19
21
  meshagent/tools/version.py
20
22
  meshagent/tools/web_toolkit.py
21
23
  meshagent_tools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
1
1
  pyjwt~=2.10
2
2
  pytest~=8.4
3
3
  pytest-asyncio~=0.26
4
- meshagent-api~=0.19.1
4
+ meshagent-api~=0.20.5
5
5
  aiohttp~=3.10
6
6
  opentelemetry-distro~=0.54b1
@@ -12,7 +12,7 @@ dependencies = [
12
12
  "pyjwt~=2.10",
13
13
  "pytest~=8.4",
14
14
  "pytest-asyncio~=0.26",
15
- "meshagent-api~=0.19.1",
15
+ "meshagent-api~=0.20.5",
16
16
  "aiohttp~=3.10",
17
17
  "opentelemetry-distro~=0.54b1"
18
18
  ]
@@ -1 +0,0 @@
1
- __version__ = "0.19.1"