meshagent-tools 0.24.1__py3-none-any.whl → 0.24.3__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/script.py +313 -0
- meshagent/tools/storage.py +603 -69
- meshagent/tools/version.py +1 -1
- {meshagent_tools-0.24.1.dist-info → meshagent_tools-0.24.3.dist-info}/METADATA +2 -2
- {meshagent_tools-0.24.1.dist-info → meshagent_tools-0.24.3.dist-info}/RECORD +8 -7
- {meshagent_tools-0.24.1.dist-info → meshagent_tools-0.24.3.dist-info}/WHEEL +1 -1
- {meshagent_tools-0.24.1.dist-info → meshagent_tools-0.24.3.dist-info}/licenses/LICENSE +0 -0
- {meshagent_tools-0.24.1.dist-info → meshagent_tools-0.24.3.dist-info}/top_level.txt +0 -0
|
@@ -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}
|
meshagent/tools/storage.py
CHANGED
|
@@ -1,19 +1,470 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import mimetypes
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
1
12
|
from meshagent.api.messaging import JsonResponse, LinkResponse, FileResponse
|
|
2
|
-
from meshagent.api import RoomClient
|
|
13
|
+
from meshagent.api import RoomClient, RoomException
|
|
14
|
+
from meshagent.api.room_server_client import StorageEntry
|
|
15
|
+
|
|
3
16
|
from .config import ToolkitConfig
|
|
4
17
|
from .tool import Tool
|
|
5
18
|
from .toolkit import ToolContext, ToolkitBuilder
|
|
6
19
|
from .hosting import RemoteToolkit, Toolkit
|
|
7
|
-
import os
|
|
8
|
-
from meshagent.api import RoomException
|
|
9
20
|
from .blob import get_bytes_from_url
|
|
10
|
-
from typing import Literal
|
|
11
|
-
import json
|
|
12
|
-
import os.path
|
|
13
21
|
|
|
14
22
|
|
|
15
|
-
class
|
|
16
|
-
|
|
23
|
+
class StorageToolMount(BaseModel):
|
|
24
|
+
path: str
|
|
25
|
+
read_only: bool = False
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="forbid")
|
|
28
|
+
|
|
29
|
+
def _ensure_writable(self, path: str) -> None:
|
|
30
|
+
if self.read_only:
|
|
31
|
+
raise RoomException(f"storage mount is read-only: {path}")
|
|
32
|
+
|
|
33
|
+
async def read_file(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
context: ToolContext,
|
|
37
|
+
resolved: "_ResolvedStoragePath",
|
|
38
|
+
path: str,
|
|
39
|
+
) -> FileResponse:
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
async def write_text(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
context: ToolContext,
|
|
46
|
+
resolved: "_ResolvedStoragePath",
|
|
47
|
+
path: str,
|
|
48
|
+
text: str,
|
|
49
|
+
overwrite: bool,
|
|
50
|
+
) -> None:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
async def write_bytes(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
context: ToolContext,
|
|
57
|
+
resolved: "_ResolvedStoragePath",
|
|
58
|
+
path: str,
|
|
59
|
+
data: bytes,
|
|
60
|
+
overwrite: bool,
|
|
61
|
+
) -> None:
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
async def list_entries(
|
|
65
|
+
self, *, context: ToolContext, resolved: "_ResolvedStoragePath"
|
|
66
|
+
) -> list[StorageEntry]:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
async def get_download_url(
|
|
70
|
+
self, *, context: ToolContext, resolved: "_ResolvedStoragePath", path: str
|
|
71
|
+
) -> LinkResponse:
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class StorageToolLocalMount(StorageToolMount):
|
|
76
|
+
local_path: str
|
|
77
|
+
|
|
78
|
+
async def read_file(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
context: ToolContext,
|
|
82
|
+
resolved: "_ResolvedStoragePath",
|
|
83
|
+
path: str,
|
|
84
|
+
) -> FileResponse:
|
|
85
|
+
local_path = _require_local_path(resolved)
|
|
86
|
+
filename = os.path.basename(path)
|
|
87
|
+
mime_type, _ = mimetypes.guess_type(local_path)
|
|
88
|
+
if mime_type is None:
|
|
89
|
+
mime_type = "application/octet-stream"
|
|
90
|
+
data = await _read_local_file(local_path)
|
|
91
|
+
return FileResponse(mime_type=mime_type, name=filename, data=data)
|
|
92
|
+
|
|
93
|
+
async def write_text(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
context: ToolContext,
|
|
97
|
+
resolved: "_ResolvedStoragePath",
|
|
98
|
+
path: str,
|
|
99
|
+
text: str,
|
|
100
|
+
overwrite: bool,
|
|
101
|
+
) -> None:
|
|
102
|
+
self._ensure_writable(path)
|
|
103
|
+
local_path = _require_local_path(resolved)
|
|
104
|
+
await _write_local_text(local_path, text, overwrite)
|
|
105
|
+
|
|
106
|
+
async def write_bytes(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
context: ToolContext,
|
|
110
|
+
resolved: "_ResolvedStoragePath",
|
|
111
|
+
path: str,
|
|
112
|
+
data: bytes,
|
|
113
|
+
overwrite: bool,
|
|
114
|
+
) -> None:
|
|
115
|
+
self._ensure_writable(path)
|
|
116
|
+
local_path = _require_local_path(resolved)
|
|
117
|
+
await _write_local_bytes(local_path, data, overwrite)
|
|
118
|
+
|
|
119
|
+
async def list_entries(
|
|
120
|
+
self, *, context: ToolContext, resolved: "_ResolvedStoragePath"
|
|
121
|
+
) -> list[StorageEntry]:
|
|
122
|
+
local_path = _require_local_path(resolved)
|
|
123
|
+
return await _list_local_entries(local_path)
|
|
124
|
+
|
|
125
|
+
async def get_download_url(
|
|
126
|
+
self, *, context: ToolContext, resolved: "_ResolvedStoragePath", path: str
|
|
127
|
+
) -> LinkResponse:
|
|
128
|
+
local_path = _require_local_path(resolved)
|
|
129
|
+
file_path = Path(local_path)
|
|
130
|
+
if not file_path.exists():
|
|
131
|
+
raise RoomException(f"file not found: {local_path}")
|
|
132
|
+
if not file_path.is_file():
|
|
133
|
+
raise RoomException(f"path is not a file: {local_path}")
|
|
134
|
+
name = os.path.basename(path)
|
|
135
|
+
return LinkResponse(name=name, url=file_path.resolve().as_uri())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class StorageToolRoomMount(StorageToolMount):
|
|
139
|
+
subpath: Optional[str] = None
|
|
140
|
+
|
|
141
|
+
async def read_file(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
context: ToolContext,
|
|
145
|
+
resolved: "_ResolvedStoragePath",
|
|
146
|
+
path: str,
|
|
147
|
+
) -> FileResponse:
|
|
148
|
+
room_path = _require_room_path(resolved)
|
|
149
|
+
filename = os.path.basename(path)
|
|
150
|
+
_, extension = os.path.splitext(path)
|
|
151
|
+
if extension:
|
|
152
|
+
schema_path = _make_room_path(
|
|
153
|
+
resolved.room_root, f".schemas/{extension.lstrip('.')}" + ".json"
|
|
154
|
+
)
|
|
155
|
+
if await context.room.storage.exists(path=schema_path):
|
|
156
|
+
return FileResponse(
|
|
157
|
+
mime_type="application/json",
|
|
158
|
+
name=filename,
|
|
159
|
+
data=json.dumps(
|
|
160
|
+
await context.room.sync.describe(path=room_path)
|
|
161
|
+
).encode(),
|
|
162
|
+
)
|
|
163
|
+
return await context.room.storage.download(path=room_path)
|
|
164
|
+
|
|
165
|
+
async def write_text(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
context: ToolContext,
|
|
169
|
+
resolved: "_ResolvedStoragePath",
|
|
170
|
+
path: str,
|
|
171
|
+
text: str,
|
|
172
|
+
overwrite: bool,
|
|
173
|
+
) -> None:
|
|
174
|
+
self._ensure_writable(path)
|
|
175
|
+
room_path = _require_room_path(resolved)
|
|
176
|
+
handle = await context.room.storage.open(path=room_path, overwrite=overwrite)
|
|
177
|
+
await context.room.storage.write(handle=handle, data=text.encode("utf-8"))
|
|
178
|
+
await context.room.storage.close(handle=handle)
|
|
179
|
+
|
|
180
|
+
async def write_bytes(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
context: ToolContext,
|
|
184
|
+
resolved: "_ResolvedStoragePath",
|
|
185
|
+
path: str,
|
|
186
|
+
data: bytes,
|
|
187
|
+
overwrite: bool,
|
|
188
|
+
) -> None:
|
|
189
|
+
self._ensure_writable(path)
|
|
190
|
+
room_path = _require_room_path(resolved)
|
|
191
|
+
if not overwrite:
|
|
192
|
+
result = await context.room.storage.exists(path=room_path)
|
|
193
|
+
if result:
|
|
194
|
+
raise RoomException(
|
|
195
|
+
f"a file already exists at the path: {path}, try another filename"
|
|
196
|
+
)
|
|
197
|
+
handle = await context.room.storage.open(path=room_path, overwrite=overwrite)
|
|
198
|
+
try:
|
|
199
|
+
await context.room.storage.write(handle=handle, data=data)
|
|
200
|
+
finally:
|
|
201
|
+
await context.room.storage.close(handle=handle)
|
|
202
|
+
|
|
203
|
+
async def list_entries(
|
|
204
|
+
self, *, context: ToolContext, resolved: "_ResolvedStoragePath"
|
|
205
|
+
) -> list[StorageEntry]:
|
|
206
|
+
room_path = _require_room_path(resolved)
|
|
207
|
+
return await context.room.storage.list(path=room_path)
|
|
208
|
+
|
|
209
|
+
async def get_download_url(
|
|
210
|
+
self, *, context: ToolContext, resolved: "_ResolvedStoragePath", path: str
|
|
211
|
+
) -> LinkResponse:
|
|
212
|
+
room_path = _require_room_path(resolved)
|
|
213
|
+
name = os.path.basename(path)
|
|
214
|
+
url = await context.room.storage.download_url(path=room_path)
|
|
215
|
+
return LinkResponse(name=name, url=url)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True)
|
|
219
|
+
class _PreparedMount:
|
|
220
|
+
mount: StorageToolMount
|
|
221
|
+
virtual_path: str
|
|
222
|
+
local_root: Optional[str]
|
|
223
|
+
room_root: Optional[str]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class _ResolvedStoragePath:
|
|
228
|
+
mount: StorageToolMount
|
|
229
|
+
virtual_path: str
|
|
230
|
+
relative_path: str
|
|
231
|
+
local_path: Optional[str]
|
|
232
|
+
room_path: Optional[str]
|
|
233
|
+
room_root: Optional[str]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _normalize_mount_path(path: str) -> str:
|
|
237
|
+
if path is None:
|
|
238
|
+
raise RoomException("mount path must be set")
|
|
239
|
+
|
|
240
|
+
cleaned = path.strip()
|
|
241
|
+
if cleaned in ("", "/", "."):
|
|
242
|
+
return "/"
|
|
243
|
+
|
|
244
|
+
cleaned = cleaned.lstrip("/")
|
|
245
|
+
if cleaned == "":
|
|
246
|
+
return "/"
|
|
247
|
+
|
|
248
|
+
parts = cleaned.split("/")
|
|
249
|
+
if any(part in {".", ".."} for part in parts):
|
|
250
|
+
raise RoomException(f"invalid mount path: {path}")
|
|
251
|
+
|
|
252
|
+
return f"/{'/'.join(parts)}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _normalize_virtual_path(path: str) -> str:
|
|
256
|
+
if path is None:
|
|
257
|
+
raise RoomException("path must be set")
|
|
258
|
+
|
|
259
|
+
cleaned = path.strip()
|
|
260
|
+
if cleaned in ("", "."):
|
|
261
|
+
return "/"
|
|
262
|
+
|
|
263
|
+
cleaned = cleaned.lstrip("/")
|
|
264
|
+
if cleaned == "":
|
|
265
|
+
return "/"
|
|
266
|
+
|
|
267
|
+
parts = cleaned.split("/")
|
|
268
|
+
if any(part in {".", ".."} for part in parts):
|
|
269
|
+
raise RoomException(f"dot segments not allowed: {path}")
|
|
270
|
+
|
|
271
|
+
return f"/{'/'.join(parts)}"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _normalize_room_root(subpath: Optional[str]) -> str:
|
|
275
|
+
if subpath is None:
|
|
276
|
+
return ""
|
|
277
|
+
|
|
278
|
+
cleaned = subpath.strip()
|
|
279
|
+
if cleaned in ("", "/", "."):
|
|
280
|
+
return ""
|
|
281
|
+
|
|
282
|
+
cleaned = cleaned.strip("/")
|
|
283
|
+
parts = cleaned.split("/")
|
|
284
|
+
if any(part in {".", ".."} for part in parts):
|
|
285
|
+
raise RoomException(f"dot segments not allowed: {subpath}")
|
|
286
|
+
|
|
287
|
+
return "/".join(parts)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _prepare_mounts(
|
|
291
|
+
mounts: Optional[list[StorageToolMount]],
|
|
292
|
+
) -> list[_PreparedMount]:
|
|
293
|
+
if not mounts:
|
|
294
|
+
mounts = [StorageToolRoomMount(path="/")]
|
|
295
|
+
|
|
296
|
+
prepared = []
|
|
297
|
+
for mount in mounts:
|
|
298
|
+
virtual_path = _normalize_mount_path(mount.path)
|
|
299
|
+
if isinstance(mount, StorageToolLocalMount):
|
|
300
|
+
local_root = os.path.abspath(mount.local_path)
|
|
301
|
+
room_root = None
|
|
302
|
+
else:
|
|
303
|
+
local_root = None
|
|
304
|
+
room_root = _normalize_room_root(mount.subpath)
|
|
305
|
+
|
|
306
|
+
prepared.append(
|
|
307
|
+
_PreparedMount(
|
|
308
|
+
mount=mount,
|
|
309
|
+
virtual_path=virtual_path,
|
|
310
|
+
local_root=local_root,
|
|
311
|
+
room_root=room_root,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return prepared
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _resolve_storage_path(
|
|
319
|
+
mounts: list[_PreparedMount], path: str
|
|
320
|
+
) -> _ResolvedStoragePath:
|
|
321
|
+
virtual_path = _normalize_virtual_path(path)
|
|
322
|
+
|
|
323
|
+
matches = []
|
|
324
|
+
for mount in mounts:
|
|
325
|
+
if mount.virtual_path == "/":
|
|
326
|
+
matches.append(mount)
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
if virtual_path == mount.virtual_path or virtual_path.startswith(
|
|
330
|
+
f"{mount.virtual_path}/"
|
|
331
|
+
):
|
|
332
|
+
matches.append(mount)
|
|
333
|
+
|
|
334
|
+
if not matches:
|
|
335
|
+
raise RoomException(f"path is not within a storage mount: {path}")
|
|
336
|
+
|
|
337
|
+
selected = max(matches, key=lambda m: len(m.virtual_path))
|
|
338
|
+
relative_path = virtual_path[len(selected.virtual_path) :].lstrip("/")
|
|
339
|
+
|
|
340
|
+
local_path = None
|
|
341
|
+
room_path = None
|
|
342
|
+
room_root = selected.room_root
|
|
343
|
+
|
|
344
|
+
if selected.local_root is not None:
|
|
345
|
+
local_path = os.path.normpath(os.path.join(selected.local_root, relative_path))
|
|
346
|
+
if os.path.commonpath([selected.local_root, local_path]) != selected.local_root:
|
|
347
|
+
raise RoomException(f"path escapes the storage mount: {path}")
|
|
348
|
+
else:
|
|
349
|
+
if room_root and relative_path:
|
|
350
|
+
room_path = f"{room_root}/{relative_path}"
|
|
351
|
+
elif room_root:
|
|
352
|
+
room_path = room_root
|
|
353
|
+
else:
|
|
354
|
+
room_path = relative_path
|
|
355
|
+
|
|
356
|
+
return _ResolvedStoragePath(
|
|
357
|
+
mount=selected.mount,
|
|
358
|
+
virtual_path=virtual_path,
|
|
359
|
+
relative_path=relative_path,
|
|
360
|
+
local_path=local_path,
|
|
361
|
+
room_path=room_path,
|
|
362
|
+
room_root=room_root,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _make_room_path(room_root: Optional[str], relative_path: str) -> str:
|
|
367
|
+
room_root = room_root or ""
|
|
368
|
+
relative_path = relative_path.lstrip("/")
|
|
369
|
+
|
|
370
|
+
if room_root and relative_path:
|
|
371
|
+
return f"{room_root}/{relative_path}"
|
|
372
|
+
|
|
373
|
+
return room_root or relative_path
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _require_local_path(resolved: _ResolvedStoragePath) -> str:
|
|
377
|
+
if resolved.local_path is None:
|
|
378
|
+
raise RoomException("local path is not available for this mount")
|
|
379
|
+
return resolved.local_path
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _require_room_path(resolved: _ResolvedStoragePath) -> str:
|
|
383
|
+
if resolved.room_path is None:
|
|
384
|
+
raise RoomException("room path is not available for this mount")
|
|
385
|
+
return resolved.room_path
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def _read_local_file(path: str) -> bytes:
|
|
389
|
+
def _read() -> bytes:
|
|
390
|
+
return Path(path).read_bytes()
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
return await asyncio.to_thread(_read)
|
|
394
|
+
except FileNotFoundError:
|
|
395
|
+
raise RoomException(f"file not found: {path}")
|
|
396
|
+
except IsADirectoryError:
|
|
397
|
+
raise RoomException(f"path is a directory: {path}")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
async def _write_local_bytes(path: str, data: bytes, overwrite: bool) -> None:
|
|
401
|
+
def _write() -> None:
|
|
402
|
+
if not overwrite and os.path.exists(path):
|
|
403
|
+
raise RoomException(
|
|
404
|
+
f"a file already exists at the path: {path}, try another filename"
|
|
405
|
+
)
|
|
406
|
+
directory = os.path.dirname(path)
|
|
407
|
+
if directory:
|
|
408
|
+
os.makedirs(directory, exist_ok=True)
|
|
409
|
+
with open(path, "wb") as handle:
|
|
410
|
+
handle.write(data)
|
|
411
|
+
|
|
412
|
+
await asyncio.to_thread(_write)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
async def _write_local_text(path: str, text: str, overwrite: bool) -> None:
|
|
416
|
+
def _write() -> None:
|
|
417
|
+
if not overwrite and os.path.exists(path):
|
|
418
|
+
raise RoomException(
|
|
419
|
+
f"a file already exists at the path: {path}, try another filename"
|
|
420
|
+
)
|
|
421
|
+
directory = os.path.dirname(path)
|
|
422
|
+
if directory:
|
|
423
|
+
os.makedirs(directory, exist_ok=True)
|
|
424
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
425
|
+
handle.write(text)
|
|
426
|
+
|
|
427
|
+
await asyncio.to_thread(_write)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
async def _list_local_entries(path: str) -> list[StorageEntry]:
|
|
431
|
+
def _list() -> list[StorageEntry]:
|
|
432
|
+
try:
|
|
433
|
+
entries = []
|
|
434
|
+
for entry in os.scandir(path):
|
|
435
|
+
stat_info = entry.stat()
|
|
436
|
+
entries.append(
|
|
437
|
+
StorageEntry(
|
|
438
|
+
name=entry.name,
|
|
439
|
+
is_folder=entry.is_dir(),
|
|
440
|
+
created_at=datetime.fromtimestamp(
|
|
441
|
+
stat_info.st_ctime, tz=timezone.utc
|
|
442
|
+
),
|
|
443
|
+
updated_at=datetime.fromtimestamp(
|
|
444
|
+
stat_info.st_mtime, tz=timezone.utc
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
return entries
|
|
449
|
+
except FileNotFoundError:
|
|
450
|
+
return []
|
|
451
|
+
except NotADirectoryError:
|
|
452
|
+
return []
|
|
453
|
+
|
|
454
|
+
return await asyncio.to_thread(_list)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class _StorageTool(Tool):
|
|
458
|
+
def __init__(self, *, mounts: list[_PreparedMount], **kwargs):
|
|
459
|
+
super().__init__(**kwargs)
|
|
460
|
+
self._mounts = mounts
|
|
461
|
+
|
|
462
|
+
def _resolve_path(self, path: str) -> _ResolvedStoragePath:
|
|
463
|
+
return _resolve_storage_path(self._mounts, path)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class ReadFileTool(_StorageTool):
|
|
467
|
+
def __init__(self, *, mounts: list[_PreparedMount]):
|
|
17
468
|
super().__init__(
|
|
18
469
|
name="read_file",
|
|
19
470
|
title="read a file file",
|
|
@@ -29,25 +480,19 @@ class ReadFileTool(Tool):
|
|
|
29
480
|
}
|
|
30
481
|
},
|
|
31
482
|
},
|
|
483
|
+
mounts=mounts,
|
|
32
484
|
)
|
|
33
485
|
|
|
34
|
-
async def execute(self,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
path=
|
|
39
|
-
)
|
|
40
|
-
return FileResponse(
|
|
41
|
-
mime_type="application/json",
|
|
42
|
-
name=filename,
|
|
43
|
-
data=json.dumps(await context.room.sync.describe(path=path)).encode(),
|
|
44
|
-
)
|
|
45
|
-
else:
|
|
46
|
-
return await context.room.storage.download(path=path)
|
|
486
|
+
async def execute(self, context: ToolContext, **kwargs):
|
|
487
|
+
path = kwargs["path"]
|
|
488
|
+
resolved = self._resolve_path(path)
|
|
489
|
+
return await resolved.mount.read_file(
|
|
490
|
+
context=context, resolved=resolved, path=path
|
|
491
|
+
)
|
|
47
492
|
|
|
48
493
|
|
|
49
|
-
class WriteFileTool(
|
|
50
|
-
def __init__(self):
|
|
494
|
+
class WriteFileTool(_StorageTool):
|
|
495
|
+
def __init__(self, *, mounts: list[_PreparedMount]):
|
|
51
496
|
super().__init__(
|
|
52
497
|
name="write_file",
|
|
53
498
|
title="write text file",
|
|
@@ -71,19 +516,26 @@ class WriteFileTool(Tool):
|
|
|
71
516
|
},
|
|
72
517
|
},
|
|
73
518
|
},
|
|
519
|
+
mounts=mounts,
|
|
74
520
|
)
|
|
75
521
|
|
|
76
|
-
async def execute(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await
|
|
522
|
+
async def execute(self, context: ToolContext, **kwargs):
|
|
523
|
+
path = kwargs["path"]
|
|
524
|
+
text = kwargs["text"]
|
|
525
|
+
overwrite = kwargs["overwrite"]
|
|
526
|
+
resolved = self._resolve_path(path)
|
|
527
|
+
await resolved.mount.write_text(
|
|
528
|
+
context=context,
|
|
529
|
+
resolved=resolved,
|
|
530
|
+
path=path,
|
|
531
|
+
text=text,
|
|
532
|
+
overwrite=overwrite,
|
|
533
|
+
)
|
|
82
534
|
return "the file was saved"
|
|
83
535
|
|
|
84
536
|
|
|
85
|
-
class GetFileDownloadUrl(
|
|
86
|
-
def __init__(self):
|
|
537
|
+
class GetFileDownloadUrl(_StorageTool):
|
|
538
|
+
def __init__(self, *, mounts: list[_PreparedMount]):
|
|
87
539
|
super().__init__(
|
|
88
540
|
name="get_file_download_url",
|
|
89
541
|
title="get file download url",
|
|
@@ -99,16 +551,19 @@ class GetFileDownloadUrl(Tool):
|
|
|
99
551
|
}
|
|
100
552
|
},
|
|
101
553
|
},
|
|
554
|
+
mounts=mounts,
|
|
102
555
|
)
|
|
103
556
|
|
|
104
|
-
async def execute(self,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
557
|
+
async def execute(self, context: ToolContext, **kwargs):
|
|
558
|
+
path = kwargs["path"]
|
|
559
|
+
resolved = self._resolve_path(path)
|
|
560
|
+
return await resolved.mount.get_download_url(
|
|
561
|
+
context=context, resolved=resolved, path=path
|
|
562
|
+
)
|
|
108
563
|
|
|
109
564
|
|
|
110
|
-
class ListFilesTool(
|
|
111
|
-
def __init__(self):
|
|
565
|
+
class ListFilesTool(_StorageTool):
|
|
566
|
+
def __init__(self, *, mounts: list[_PreparedMount]):
|
|
112
567
|
super().__init__(
|
|
113
568
|
name="list_files_in_room",
|
|
114
569
|
title="list files in room",
|
|
@@ -119,17 +574,20 @@ class ListFilesTool(Tool):
|
|
|
119
574
|
"required": ["path"],
|
|
120
575
|
"properties": {"path": {"type": "string"}},
|
|
121
576
|
},
|
|
577
|
+
mounts=mounts,
|
|
122
578
|
)
|
|
123
579
|
|
|
124
|
-
async def execute(self,
|
|
125
|
-
|
|
580
|
+
async def execute(self, context: ToolContext, **kwargs):
|
|
581
|
+
path = kwargs["path"]
|
|
582
|
+
resolved = self._resolve_path(path)
|
|
583
|
+
files = await resolved.mount.list_entries(context=context, resolved=resolved)
|
|
126
584
|
return JsonResponse(
|
|
127
585
|
json={"files": list([f.model_dump(mode="json") for f in files])}
|
|
128
586
|
)
|
|
129
587
|
|
|
130
588
|
|
|
131
|
-
class SaveFileFromUrlTool(
|
|
132
|
-
def __init__(self):
|
|
589
|
+
class SaveFileFromUrlTool(_StorageTool):
|
|
590
|
+
def __init__(self, *, mounts: list[_PreparedMount]):
|
|
133
591
|
super().__init__(
|
|
134
592
|
name="save_file_from_url",
|
|
135
593
|
title="save file from url",
|
|
@@ -153,40 +611,48 @@ class SaveFileFromUrlTool(Tool):
|
|
|
153
611
|
},
|
|
154
612
|
},
|
|
155
613
|
},
|
|
614
|
+
mounts=mounts,
|
|
156
615
|
)
|
|
157
616
|
|
|
158
|
-
async def execute(
|
|
159
|
-
|
|
160
|
-
|
|
617
|
+
async def execute(self, context: ToolContext, **kwargs):
|
|
618
|
+
url = kwargs["url"]
|
|
619
|
+
path = kwargs["path"]
|
|
620
|
+
overwrite = kwargs["overwrite"]
|
|
621
|
+
resolved = self._resolve_path(path)
|
|
161
622
|
blob = await get_bytes_from_url(url=url)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
handle = await context.room.storage.open(path=path, overwrite=overwrite)
|
|
171
|
-
try:
|
|
172
|
-
await context.room.storage.write(handle=handle, data=blob.data)
|
|
173
|
-
finally:
|
|
174
|
-
await context.room.storage.close(handle=handle)
|
|
623
|
+
await resolved.mount.write_bytes(
|
|
624
|
+
context=context,
|
|
625
|
+
resolved=resolved,
|
|
626
|
+
path=path,
|
|
627
|
+
data=blob.data,
|
|
628
|
+
overwrite=overwrite,
|
|
629
|
+
)
|
|
175
630
|
|
|
176
631
|
|
|
177
632
|
class StorageToolkit(RemoteToolkit):
|
|
178
|
-
def __init__(
|
|
179
|
-
|
|
633
|
+
def __init__(
|
|
634
|
+
self,
|
|
635
|
+
read_only: bool = False,
|
|
636
|
+
mounts: Optional[list[StorageToolMount]] = None,
|
|
637
|
+
):
|
|
638
|
+
prepared_mounts = _prepare_mounts(mounts)
|
|
639
|
+
self._mounts = prepared_mounts
|
|
640
|
+
self._read_only = read_only
|
|
641
|
+
has_writable_mount = any(
|
|
642
|
+
not prepared.mount.read_only for prepared in prepared_mounts
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
if read_only or not has_writable_mount:
|
|
180
646
|
tools = [
|
|
181
|
-
ListFilesTool(),
|
|
182
|
-
|
|
183
|
-
ReadFileTool(),
|
|
184
|
-
SaveFileFromUrlTool(),
|
|
647
|
+
ListFilesTool(mounts=prepared_mounts),
|
|
648
|
+
ReadFileTool(mounts=prepared_mounts),
|
|
185
649
|
]
|
|
186
650
|
else:
|
|
187
651
|
tools = [
|
|
188
|
-
ListFilesTool(),
|
|
189
|
-
|
|
652
|
+
ListFilesTool(mounts=prepared_mounts),
|
|
653
|
+
WriteFileTool(mounts=prepared_mounts),
|
|
654
|
+
ReadFileTool(mounts=prepared_mounts),
|
|
655
|
+
SaveFileFromUrlTool(mounts=prepared_mounts),
|
|
190
656
|
]
|
|
191
657
|
super().__init__(
|
|
192
658
|
name="storage",
|
|
@@ -195,16 +661,84 @@ class StorageToolkit(RemoteToolkit):
|
|
|
195
661
|
tools=tools,
|
|
196
662
|
)
|
|
197
663
|
|
|
664
|
+
def _ensure_writable(self, path: str) -> None:
|
|
665
|
+
if self._read_only:
|
|
666
|
+
raise RoomException(f"storage toolkit is read-only: {path}")
|
|
667
|
+
|
|
668
|
+
def _resolve_path(self, path: str) -> _ResolvedStoragePath:
|
|
669
|
+
return _resolve_storage_path(self._mounts, path)
|
|
670
|
+
|
|
671
|
+
async def read_file(self, *, context: ToolContext, path: str) -> FileResponse:
|
|
672
|
+
resolved = self._resolve_path(path)
|
|
673
|
+
return await resolved.mount.read_file(
|
|
674
|
+
context=context,
|
|
675
|
+
resolved=resolved,
|
|
676
|
+
path=path,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
async def list_entries(
|
|
680
|
+
self, *, context: ToolContext, path: str
|
|
681
|
+
) -> list[StorageEntry]:
|
|
682
|
+
resolved = self._resolve_path(path)
|
|
683
|
+
return await resolved.mount.list_entries(context=context, resolved=resolved)
|
|
684
|
+
|
|
685
|
+
async def get_download_url(
|
|
686
|
+
self, *, context: ToolContext, path: str
|
|
687
|
+
) -> LinkResponse:
|
|
688
|
+
resolved = self._resolve_path(path)
|
|
689
|
+
return await resolved.mount.get_download_url(
|
|
690
|
+
context=context,
|
|
691
|
+
resolved=resolved,
|
|
692
|
+
path=path,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
async def write_text(
|
|
696
|
+
self,
|
|
697
|
+
*,
|
|
698
|
+
context: ToolContext,
|
|
699
|
+
path: str,
|
|
700
|
+
text: str,
|
|
701
|
+
overwrite: bool,
|
|
702
|
+
) -> None:
|
|
703
|
+
self._ensure_writable(path)
|
|
704
|
+
resolved = self._resolve_path(path)
|
|
705
|
+
await resolved.mount.write_text(
|
|
706
|
+
context=context,
|
|
707
|
+
resolved=resolved,
|
|
708
|
+
path=path,
|
|
709
|
+
text=text,
|
|
710
|
+
overwrite=overwrite,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
async def write_bytes(
|
|
714
|
+
self,
|
|
715
|
+
*,
|
|
716
|
+
context: ToolContext,
|
|
717
|
+
path: str,
|
|
718
|
+
data: bytes,
|
|
719
|
+
overwrite: bool,
|
|
720
|
+
) -> None:
|
|
721
|
+
self._ensure_writable(path)
|
|
722
|
+
resolved = self._resolve_path(path)
|
|
723
|
+
await resolved.mount.write_bytes(
|
|
724
|
+
context=context,
|
|
725
|
+
resolved=resolved,
|
|
726
|
+
path=path,
|
|
727
|
+
data=data,
|
|
728
|
+
overwrite=overwrite,
|
|
729
|
+
)
|
|
730
|
+
|
|
198
731
|
|
|
199
732
|
class StorageToolkitConfig(ToolkitConfig):
|
|
200
|
-
name:
|
|
733
|
+
name: str = "storage"
|
|
201
734
|
|
|
202
735
|
|
|
203
736
|
class StorageToolkitBuilder(ToolkitBuilder):
|
|
204
|
-
def __init__(self):
|
|
737
|
+
def __init__(self, *, mounts: Optional[list[StorageToolMount]] = None):
|
|
205
738
|
super().__init__(name="storage", type=StorageToolkitConfig)
|
|
739
|
+
self.mounts = mounts
|
|
206
740
|
|
|
207
741
|
async def make(
|
|
208
|
-
self, *, room: RoomClient, model: str, config:
|
|
742
|
+
self, *, room: RoomClient, model: str, config: StorageToolkitConfig
|
|
209
743
|
) -> Toolkit:
|
|
210
|
-
return StorageToolkit()
|
|
744
|
+
return StorageToolkit(mounts=self.mounts)
|
meshagent/tools/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.24.
|
|
1
|
+
__version__ = "0.24.3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-tools
|
|
3
|
-
Version: 0.24.
|
|
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.
|
|
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
|
|
@@ -8,15 +8,16 @@ meshagent/tools/document_tools.py,sha256=LMULXOSBjsvhKjqzxUxe8586t0Vol0v1Btu5v6o
|
|
|
8
8
|
meshagent/tools/hosting.py,sha256=l1BCgnSrCJQsWU9Kycq3hEI4ZlYxffDfde6QeJUfko0,10678
|
|
9
9
|
meshagent/tools/multi_tool.py,sha256=hmWZO18Y2tuFG_7rvUed9er29aXleAC-r3YpXBCZWUY,4040
|
|
10
10
|
meshagent/tools/pydantic.py,sha256=n-MD0gC-oRtHSTUDD5IV2dP-xIk-zjcDgHfgjqMgiqM,1161
|
|
11
|
-
meshagent/tools/
|
|
11
|
+
meshagent/tools/script.py,sha256=uHrJynzM0SwUHM1qXIjt-UhZLG4AQtFw-yyLv4lxGGw,10589
|
|
12
|
+
meshagent/tools/storage.py,sha256=NVpi9CZKSZUh8PTxxCdJhJy7Gzmdp55-zo2yHYGod_E,23340
|
|
12
13
|
meshagent/tools/strict_schema.py,sha256=IytdAANa6lsfrsg5FsJuqYrxH9D_fayl-Lc9EwgLJSM,6277
|
|
13
14
|
meshagent/tools/tool.py,sha256=9OAlbfaHqfgJnCDBSW-8PS0Z1K1KjWGD3JBUyiHOxAk,3131
|
|
14
15
|
meshagent/tools/toolkit.py,sha256=rCCkpQBoSkmmhjnRGA4jx0QP-ds6WTJ0PkQVnf1Ls7s,3843
|
|
15
16
|
meshagent/tools/uuid.py,sha256=mzRwDmXy39U5lHhd9wqV4r-ZdS8jPfDTTs4UfW4KHJQ,1342
|
|
16
|
-
meshagent/tools/version.py,sha256=
|
|
17
|
+
meshagent/tools/version.py,sha256=FVT3zgMnGxhctio1D7Bj2hvIqrqQQ-a9tvDQYKSSekk,23
|
|
17
18
|
meshagent/tools/web_toolkit.py,sha256=IoOYjOBmcbQsqWT14xYg02jjWpWmGOkDSxt2U-LQoaA,1258
|
|
18
|
-
meshagent_tools-0.24.
|
|
19
|
-
meshagent_tools-0.24.
|
|
20
|
-
meshagent_tools-0.24.
|
|
21
|
-
meshagent_tools-0.24.
|
|
22
|
-
meshagent_tools-0.24.
|
|
19
|
+
meshagent_tools-0.24.3.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
|
|
20
|
+
meshagent_tools-0.24.3.dist-info/METADATA,sha256=lBDQ02Y_Xaty4w6N018An8Rwg7OQHTSYajbalKekVUo,2878
|
|
21
|
+
meshagent_tools-0.24.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
+
meshagent_tools-0.24.3.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
23
|
+
meshagent_tools-0.24.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|