meshagent-tools 0.24.0__tar.gz → 0.24.3__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 (32) hide show
  1. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/CHANGELOG.md +9 -0
  2. {meshagent_tools-0.24.0/meshagent_tools.egg-info → meshagent_tools-0.24.3}/PKG-INFO +2 -2
  3. meshagent_tools-0.24.3/meshagent/tools/script.py +313 -0
  4. meshagent_tools-0.24.3/meshagent/tools/storage.py +744 -0
  5. meshagent_tools-0.24.3/meshagent/tools/version.py +1 -0
  6. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3/meshagent_tools.egg-info}/PKG-INFO +2 -2
  7. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent_tools.egg-info/SOURCES.txt +1 -0
  8. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent_tools.egg-info/requires.txt +1 -1
  9. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/pyproject.toml +1 -1
  10. meshagent_tools-0.24.0/meshagent/tools/storage.py +0 -210
  11. meshagent_tools-0.24.0/meshagent/tools/version.py +0 -1
  12. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/LICENSE +0 -0
  13. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/MANIFEST.in +0 -0
  14. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/README.md +0 -0
  15. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/__init__.py +0 -0
  16. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/blob.py +0 -0
  17. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/config.py +0 -0
  18. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/database.py +0 -0
  19. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/datetime.py +0 -0
  20. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/discovery.py +0 -0
  21. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/document_tools.py +0 -0
  22. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/hosting.py +0 -0
  23. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/multi_tool.py +0 -0
  24. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/pydantic.py +0 -0
  25. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/strict_schema.py +0 -0
  26. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/tool.py +0 -0
  27. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/toolkit.py +0 -0
  28. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/uuid.py +0 -0
  29. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent/tools/web_toolkit.py +0 -0
  30. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent_tools.egg-info/dependency_links.txt +0 -0
  31. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/meshagent_tools.egg-info/top_level.txt +0 -0
  32. {meshagent_tools-0.24.0 → meshagent_tools-0.24.3}/setup.cfg +0 -0
@@ -1,3 +1,12 @@
1
+ ## [0.24.3]
2
+ - Stability
3
+
4
+ ## [0.24.2]
5
+ - Stability
6
+
7
+ ## [0.24.1]
8
+ - Stability
9
+
1
10
  ## [0.24.0]
2
11
  - Breaking: removed `AgentsClient.ask` and `list_agents` from the Python SDK.
3
12
  - Breaking: `AgentCallContext` renamed to `TaskContext`, planning module and Pydantic agent utilities removed, and discovery toolkit no longer lists agents.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-tools
3
- Version: 0.24.0
3
+ Version: 0.24.3
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.24.0
15
+ Requires-Dist: meshagent-api~=0.24.3
16
16
  Requires-Dist: aiohttp~=3.10
17
17
  Requires-Dist: opentelemetry-distro~=0.54b1
18
18
  Dynamic: license-file
@@ -0,0 +1,313 @@
1
+ from meshagent.api import RoomClient
2
+ from meshagent.tools import Toolkit, ToolContext, Tool
3
+
4
+ from meshagent.agents.adapter import (
5
+ ToolkitBuilder,
6
+ )
7
+
8
+ from meshagent.api.specs.service import ContainerMountSpec, RoomStorageMountSpec
9
+ from typing import Literal
10
+ import os
11
+ from typing import Optional
12
+
13
+ import logging
14
+ import asyncio
15
+ from pydantic import BaseModel
16
+
17
+ logger = logging.getLogger("script_tool")
18
+
19
+
20
+ DEFAULT_CONTAINER_MOUNT_SPEC = ContainerMountSpec(
21
+ room=[RoomStorageMountSpec(path="/data")]
22
+ )
23
+
24
+
25
+ class ScriptToolConfig(BaseModel):
26
+ name: Literal["script"] = "script"
27
+ service_id: Optional[str] = None
28
+ commands: list[str] = None
29
+ description: Optional[str] = None
30
+ title: Optional[str] = None
31
+ tool_name: str
32
+
33
+
34
+ class ScriptToolkitBuilder(ToolkitBuilder):
35
+ def __init__(
36
+ self,
37
+ *,
38
+ name: str = "script",
39
+ commands: Optional[list[str]] = None,
40
+ working_directory: Optional[str] = None,
41
+ image: Optional[str] = "python:3.13",
42
+ mounts: Optional[ContainerMountSpec] = DEFAULT_CONTAINER_MOUNT_SPEC,
43
+ input_schema: Optional[dict] = None,
44
+ ):
45
+ super().__init__(name=name, type=ScriptToolConfig)
46
+
47
+ self.working_directory = working_directory
48
+ self.image = image
49
+ self.mounts = mounts
50
+ self.commands = commands
51
+ self.input_schema = input_schema
52
+
53
+ async def make(self, *, room: RoomClient, model: str, config: ScriptToolConfig):
54
+ return Toolkit(
55
+ name=self.name,
56
+ tools=[
57
+ ScriptTool(
58
+ name=config.tool_name,
59
+ description=config.description,
60
+ title=config.title,
61
+ service_id=config.service_id,
62
+ working_directory=self.working_directory,
63
+ image=self.image,
64
+ commands=self.commands or config.commands,
65
+ mounts=self.mounts,
66
+ input_schema=self.input_schema,
67
+ )
68
+ ],
69
+ )
70
+
71
+
72
+ class ScriptTool(Tool):
73
+ def __init__(
74
+ self,
75
+ *,
76
+ name: str,
77
+ commands: list[str],
78
+ description: Optional[str] = None,
79
+ title: Optional[str] = None,
80
+ service_id: Optional[str] = None,
81
+ working_directory: Optional[str] = None,
82
+ image: Optional[str] = "python:3.13",
83
+ mounts: Optional[ContainerMountSpec] = DEFAULT_CONTAINER_MOUNT_SPEC,
84
+ env: Optional[dict[str, str]] = None,
85
+ input_schema: Optional[dict] = None,
86
+ max_output_length: int = 32000,
87
+ timeout_ms: int = 30 * 60 * 1000,
88
+ ):
89
+ self.service_id = service_id
90
+ self.working_directory = working_directory
91
+ self.image = image
92
+ self.mounts = mounts
93
+ self._container_id = None
94
+ self.env = env
95
+ self.max_output_length = max_output_length
96
+ self.timeout_ms = timeout_ms
97
+ self.service_id = service_id
98
+ self.commands = commands
99
+
100
+ super().__init__(
101
+ name=name,
102
+ description=description,
103
+ title=title,
104
+ input_schema=input_schema
105
+ or {
106
+ "type": "object",
107
+ "required": ["prompt"],
108
+ "additionalProperties": False,
109
+ "properties": {"prompt": {"type": "string"}},
110
+ },
111
+ )
112
+
113
+ async def execute(
114
+ self,
115
+ context: ToolContext,
116
+ **kwargs,
117
+ ):
118
+ merged_env = {**os.environ}
119
+
120
+ results = []
121
+ encoding = os.device_encoding(1) or "utf-8"
122
+
123
+ left = self.max_output_length
124
+
125
+ def limit(s: str):
126
+ nonlocal left
127
+ if left is not None:
128
+ s = s[0:left]
129
+ left -= len(s)
130
+ return s
131
+ else:
132
+ return s
133
+
134
+ timeout = float(self.timeout_ms) / 1000.0 if self.timeout_ms else 20 * 1000.0
135
+
136
+ if self.image is not None or self.service_id is not None:
137
+ running = False
138
+
139
+ if self._container_id:
140
+ # make sure container is still running
141
+
142
+ for c in await context.room.containers.list():
143
+ if c.id == self._container_id or (
144
+ self.service_id is not None and c.service_id == self.service_id
145
+ ):
146
+ running = True
147
+
148
+ if not running:
149
+ if self.service_id is not None:
150
+ env = {}
151
+
152
+ for k, v in kwargs.items():
153
+ env[k.upper()] = v
154
+
155
+ logger.info(
156
+ f"executing shell script in container with env {env}"
157
+ )
158
+
159
+ self._container_id = await context.room.containers.run_service(
160
+ service_id=self.service_id,
161
+ env=env,
162
+ )
163
+
164
+ else:
165
+ self._container_id = await context.room.containers.run(
166
+ command="sleep infinity",
167
+ image=self.image,
168
+ mounts=self.mounts,
169
+ writable_root_fs=True,
170
+ env=self.env,
171
+ )
172
+
173
+ container_id = self._container_id
174
+ commands = self.commands
175
+ logger.info(
176
+ f"executing shell script in container {container_id} with timeout {timeout}: {commands}"
177
+ )
178
+ import shlex
179
+
180
+ for line in commands:
181
+ try:
182
+ # TODO: what if container start fails
183
+
184
+ exec = await context.room.containers.exec(
185
+ container_id=container_id,
186
+ command=shlex.join(["bash", "-c", line]),
187
+ tty=False,
188
+ )
189
+
190
+ stdout = bytearray()
191
+ stderr = bytearray()
192
+
193
+ try:
194
+ async with asyncio.timeout(timeout):
195
+ async for se in exec.stderr():
196
+ stderr.extend(se)
197
+
198
+ async for so in exec.stdout():
199
+ stdout.extend(so)
200
+
201
+ exit_code = await exec.result
202
+
203
+ return {
204
+ "outcome": {
205
+ "type": "exit",
206
+ "exit_code": exit_code,
207
+ },
208
+ "stdout": stdout.decode(),
209
+ "stderr": stderr.decode(),
210
+ }
211
+
212
+ except asyncio.TimeoutError:
213
+ logger.info(f"The command timed out after {timeout}s")
214
+ await exec.kill()
215
+
216
+ results.append(
217
+ {
218
+ "outcome": {"type": "timeout"},
219
+ "stdout": limit(
220
+ stdout.decode(encoding, errors="replace")
221
+ ),
222
+ "stderr": limit(
223
+ stderr.decode(encoding, errors="replace")
224
+ ),
225
+ }
226
+ )
227
+ break
228
+
229
+ except Exception as ex:
230
+ results.append(
231
+ {
232
+ "outcome": {
233
+ "type": "exit",
234
+ "exit_code": 1,
235
+ },
236
+ "stdout": "",
237
+ "stderr": f"{ex}",
238
+ }
239
+ )
240
+ break
241
+
242
+ except Exception as ex:
243
+ results.append(
244
+ {
245
+ "outcome": {
246
+ "type": "exit",
247
+ "exit_code": 1,
248
+ },
249
+ "stdout": "",
250
+ "stderr": f"{ex}",
251
+ }
252
+ )
253
+ break
254
+ else:
255
+ for line in self.commands:
256
+ logger.info(f"executing command {line} with timeout: {timeout}s")
257
+
258
+ # Spawn the process
259
+ try:
260
+ import shlex
261
+
262
+ proc = await asyncio.create_subprocess_shell(
263
+ shlex.join(["bash", "-c", line]),
264
+ cwd=self.working_directory or os.getcwd(),
265
+ env=merged_env,
266
+ stdout=asyncio.subprocess.PIPE,
267
+ stderr=asyncio.subprocess.PIPE,
268
+ )
269
+
270
+ stdout, stderr = await asyncio.wait_for(
271
+ proc.communicate(),
272
+ timeout=timeout,
273
+ )
274
+ except asyncio.TimeoutError:
275
+ logger.info(f"The command timed out after {timeout}s")
276
+ proc.kill() # send SIGKILL / TerminateProcess
277
+
278
+ stdout, stderr = await proc.communicate()
279
+
280
+ results.append(
281
+ {
282
+ "outcome": {"type": "timeout"},
283
+ "stdout": limit(stdout.decode(encoding, errors="replace")),
284
+ "stderr": limit(stderr.decode(encoding, errors="replace")),
285
+ }
286
+ )
287
+ break
288
+
289
+ except Exception as ex:
290
+ results.append(
291
+ {
292
+ "outcome": {
293
+ "type": "exit",
294
+ "exit_code": 1,
295
+ },
296
+ "stdout": "",
297
+ "stderr": f"{ex}",
298
+ }
299
+ )
300
+ break
301
+
302
+ results.append(
303
+ {
304
+ "outcome": {
305
+ "type": "exit",
306
+ "exit_code": proc.returncode,
307
+ },
308
+ "stdout": limit(stdout.decode(encoding, errors="replace")),
309
+ "stderr": limit(stderr.decode(encoding, errors="replace")),
310
+ }
311
+ )
312
+
313
+ return {"results": results}