meshagent-tools 0.24.6__py3-none-any.whl → 0.25.1__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/__init__.py +25 -0
- meshagent/tools/blob.py +2 -2
- meshagent/tools/container_shell.py +226 -0
- meshagent/tools/database.py +3 -3
- meshagent/tools/script.py +38 -4
- meshagent/tools/tests/tool_decorator_test.py +45 -0
- meshagent/tools/tool.py +87 -2
- meshagent/tools/toolkit.py +7 -2
- meshagent/tools/version.py +1 -1
- meshagent/tools/web_toolkit.py +111 -18
- {meshagent_tools-0.24.6.dist-info → meshagent_tools-0.25.1.dist-info}/METADATA +4 -3
- meshagent_tools-0.25.1.dist-info/RECORD +25 -0
- meshagent_tools-0.24.6.dist-info/RECORD +0 -23
- {meshagent_tools-0.24.6.dist-info → meshagent_tools-0.25.1.dist-info}/WHEEL +0 -0
- {meshagent_tools-0.24.6.dist-info → meshagent_tools-0.25.1.dist-info}/licenses/LICENSE +0 -0
- {meshagent_tools-0.24.6.dist-info → meshagent_tools-0.25.1.dist-info}/top_level.txt +0 -0
meshagent/tools/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ from .tool import (
|
|
|
16
16
|
ToolContext,
|
|
17
17
|
Tool,
|
|
18
18
|
BaseTool,
|
|
19
|
+
tool,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
from .config import ToolkitConfig
|
|
@@ -30,6 +31,19 @@ from .hosting import (
|
|
|
30
31
|
)
|
|
31
32
|
from .multi_tool import MultiTool, MultiToolkit
|
|
32
33
|
from .version import __version__
|
|
34
|
+
from .web_toolkit import (
|
|
35
|
+
WebFetchConfig,
|
|
36
|
+
WebFetchTool,
|
|
37
|
+
WebFetchToolkitBuilder,
|
|
38
|
+
WebToolkit,
|
|
39
|
+
)
|
|
40
|
+
from .container_shell import (
|
|
41
|
+
ContainerShellToolConfig,
|
|
42
|
+
ContainerShellToolkitBuilder,
|
|
43
|
+
ContainerShellTool,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from .script import ScriptTool, ScriptToolConfig, ScriptToolkitBuilder
|
|
33
47
|
|
|
34
48
|
from meshagent.api import websocket_protocol, RoomClient, ParticipantToken
|
|
35
49
|
from meshagent.api.websocket_protocol import WebSocketClientProtocol
|
|
@@ -52,6 +66,7 @@ __all__ = [
|
|
|
52
66
|
ToolContext,
|
|
53
67
|
Toolkit,
|
|
54
68
|
Response,
|
|
69
|
+
tool,
|
|
55
70
|
LinkResponse,
|
|
56
71
|
BaseTool,
|
|
57
72
|
RemoteToolkit,
|
|
@@ -64,5 +79,15 @@ __all__ = [
|
|
|
64
79
|
make_toolkits,
|
|
65
80
|
ToolkitConfig,
|
|
66
81
|
get_bytes_from_url,
|
|
82
|
+
WebFetchConfig,
|
|
83
|
+
WebFetchTool,
|
|
84
|
+
WebFetchToolkitBuilder,
|
|
85
|
+
WebToolkit,
|
|
86
|
+
ContainerShellToolConfig,
|
|
87
|
+
ContainerShellToolkitBuilder,
|
|
88
|
+
ContainerShellTool,
|
|
89
|
+
ScriptTool,
|
|
90
|
+
ScriptToolConfig,
|
|
91
|
+
ScriptToolkitBuilder,
|
|
67
92
|
__version__,
|
|
68
93
|
]
|
meshagent/tools/blob.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from uuid import uuid4
|
|
2
2
|
import base64
|
|
3
|
-
import
|
|
3
|
+
from meshagent.api.http import new_client_session
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Blob:
|
|
@@ -48,7 +48,7 @@ async def get_bytes_from_url(*, url: str) -> Blob:
|
|
|
48
48
|
# file_name = str(uuid.uuid4())+extension
|
|
49
49
|
return Blob(mime_type=mime_type, data=content)
|
|
50
50
|
else:
|
|
51
|
-
async with
|
|
51
|
+
async with new_client_session() as session:
|
|
52
52
|
async with session.get(url=url) as response:
|
|
53
53
|
content = await response.content.read()
|
|
54
54
|
return Blob(mime_type=response.content_type, data=content)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from meshagent.api import RoomClient
|
|
2
|
+
from .tool import ToolContext, Tool
|
|
3
|
+
from .toolkit import Toolkit, ToolkitBuilder
|
|
4
|
+
|
|
5
|
+
from meshagent.api.specs.service import ContainerMountSpec, RoomStorageMountSpec
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import asyncio
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("container_shell_tool")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_CONTAINER_MOUNT_SPEC = ContainerMountSpec(
|
|
17
|
+
room=[RoomStorageMountSpec(path="/data")]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContainerShellToolConfig(BaseModel):
|
|
22
|
+
name: Literal["container_shell"] = "container_shell"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ContainerShellToolkitBuilder(ToolkitBuilder):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
name: str = "container_shell",
|
|
30
|
+
working_directory: Optional[str] = None,
|
|
31
|
+
image: Optional[str] = "python:3.13",
|
|
32
|
+
mounts: Optional[ContainerMountSpec] = DEFAULT_CONTAINER_MOUNT_SPEC,
|
|
33
|
+
env: Optional[dict[str, str]] = None,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(name=name, type=ContainerShellToolConfig)
|
|
36
|
+
|
|
37
|
+
self.working_directory = working_directory
|
|
38
|
+
self.image = image
|
|
39
|
+
self.mounts = mounts
|
|
40
|
+
self.env = env
|
|
41
|
+
|
|
42
|
+
async def make(
|
|
43
|
+
self, *, room: RoomClient, model: str, config: ContainerShellToolConfig
|
|
44
|
+
) -> Toolkit:
|
|
45
|
+
return Toolkit(
|
|
46
|
+
name=self.name,
|
|
47
|
+
tools=[
|
|
48
|
+
ContainerShellTool(
|
|
49
|
+
name=self.name,
|
|
50
|
+
working_directory=self.working_directory,
|
|
51
|
+
image=self.image,
|
|
52
|
+
mounts=self.mounts,
|
|
53
|
+
env=self.env,
|
|
54
|
+
)
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ContainerShellTool(Tool):
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
name: str = "container_shell",
|
|
64
|
+
description: Optional[str] = None,
|
|
65
|
+
title: Optional[str] = None,
|
|
66
|
+
working_directory: Optional[str] = None,
|
|
67
|
+
image: Optional[str] = "python:3.13",
|
|
68
|
+
mounts: Optional[ContainerMountSpec] = DEFAULT_CONTAINER_MOUNT_SPEC,
|
|
69
|
+
env: Optional[dict[str, str]] = None,
|
|
70
|
+
):
|
|
71
|
+
self.working_directory = working_directory
|
|
72
|
+
self.image = image
|
|
73
|
+
self.mounts = mounts
|
|
74
|
+
self._container_id = None
|
|
75
|
+
self.env = env
|
|
76
|
+
|
|
77
|
+
super().__init__(
|
|
78
|
+
name=name,
|
|
79
|
+
description=description
|
|
80
|
+
or "execute shell commands in a container and return the result",
|
|
81
|
+
title=title,
|
|
82
|
+
input_schema={
|
|
83
|
+
"type": "object",
|
|
84
|
+
"required": ["commands"],
|
|
85
|
+
"additionalProperties": False,
|
|
86
|
+
"properties": {
|
|
87
|
+
"commands": {"type": "array", "items": {"type": "string"}},
|
|
88
|
+
"max_output_length": {"type": "integer"},
|
|
89
|
+
"timeout_ms": {"type": "integer"},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async def execute(
|
|
95
|
+
self,
|
|
96
|
+
context: ToolContext,
|
|
97
|
+
**kwargs,
|
|
98
|
+
):
|
|
99
|
+
commands = kwargs.get("commands") or []
|
|
100
|
+
max_output_length = kwargs.get("max_output_length")
|
|
101
|
+
timeout_ms = kwargs.get("timeout_ms")
|
|
102
|
+
|
|
103
|
+
if not commands:
|
|
104
|
+
raise Exception("commands is required")
|
|
105
|
+
|
|
106
|
+
if self.image is None:
|
|
107
|
+
raise Exception("container_shell requires an image")
|
|
108
|
+
|
|
109
|
+
results = []
|
|
110
|
+
encoding = os.device_encoding(1) or "utf-8"
|
|
111
|
+
|
|
112
|
+
left = max_output_length
|
|
113
|
+
|
|
114
|
+
def limit(s: str):
|
|
115
|
+
nonlocal left
|
|
116
|
+
if left is not None:
|
|
117
|
+
s = s[0:left]
|
|
118
|
+
left -= len(s)
|
|
119
|
+
return s
|
|
120
|
+
else:
|
|
121
|
+
return s
|
|
122
|
+
|
|
123
|
+
timeout = float(timeout_ms) / 1000.0 if timeout_ms else 20 * 1000.0
|
|
124
|
+
|
|
125
|
+
running = False
|
|
126
|
+
|
|
127
|
+
if self._container_id:
|
|
128
|
+
for c in await context.room.containers.list():
|
|
129
|
+
if c.id == self._container_id:
|
|
130
|
+
running = True
|
|
131
|
+
|
|
132
|
+
if not running:
|
|
133
|
+
self._container_id = await context.room.containers.run(
|
|
134
|
+
command="sleep infinity",
|
|
135
|
+
image=self.image,
|
|
136
|
+
mounts=self.mounts,
|
|
137
|
+
writable_root_fs=True,
|
|
138
|
+
env=self.env,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
container_id = self._container_id
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
logger.info(
|
|
145
|
+
"executing shell commands in container %s with timeout %s: %s",
|
|
146
|
+
container_id,
|
|
147
|
+
timeout,
|
|
148
|
+
commands,
|
|
149
|
+
)
|
|
150
|
+
import shlex
|
|
151
|
+
|
|
152
|
+
for command in commands:
|
|
153
|
+
command_to_run = command
|
|
154
|
+
if self.working_directory:
|
|
155
|
+
command_to_run = (
|
|
156
|
+
f"cd {shlex.quote(self.working_directory)} && {command}"
|
|
157
|
+
)
|
|
158
|
+
exec = await context.room.containers.exec(
|
|
159
|
+
container_id=container_id,
|
|
160
|
+
command=shlex.join(["bash", "-lc", command_to_run]),
|
|
161
|
+
tty=False,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
stdout = bytearray()
|
|
165
|
+
stderr = bytearray()
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
async with asyncio.timeout(timeout):
|
|
169
|
+
async for se in exec.stderr():
|
|
170
|
+
stderr.extend(se)
|
|
171
|
+
|
|
172
|
+
async for so in exec.stdout():
|
|
173
|
+
stdout.extend(so)
|
|
174
|
+
|
|
175
|
+
exit_code = await exec.result
|
|
176
|
+
|
|
177
|
+
results.append(
|
|
178
|
+
{
|
|
179
|
+
"outcome": {
|
|
180
|
+
"type": "exit",
|
|
181
|
+
"exit_code": exit_code,
|
|
182
|
+
},
|
|
183
|
+
"stdout": stdout.decode(),
|
|
184
|
+
"stderr": stderr.decode(),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
except asyncio.TimeoutError:
|
|
189
|
+
logger.info("The command timed out after %ss", timeout)
|
|
190
|
+
await exec.kill()
|
|
191
|
+
|
|
192
|
+
results.append(
|
|
193
|
+
{
|
|
194
|
+
"outcome": {"type": "timeout"},
|
|
195
|
+
"stdout": limit(stdout.decode(encoding, errors="replace")),
|
|
196
|
+
"stderr": limit(stderr.decode(encoding, errors="replace")),
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
except Exception as ex:
|
|
202
|
+
results.append(
|
|
203
|
+
{
|
|
204
|
+
"outcome": {
|
|
205
|
+
"type": "exit",
|
|
206
|
+
"exit_code": 1,
|
|
207
|
+
},
|
|
208
|
+
"stdout": "",
|
|
209
|
+
"stderr": f"{ex}",
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
except Exception as ex:
|
|
215
|
+
results.append(
|
|
216
|
+
{
|
|
217
|
+
"outcome": {
|
|
218
|
+
"type": "exit",
|
|
219
|
+
"exit_code": 1,
|
|
220
|
+
},
|
|
221
|
+
"stdout": "",
|
|
222
|
+
"stderr": f"{ex}",
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return {"results": results}
|
meshagent/tools/database.py
CHANGED
|
@@ -128,7 +128,7 @@ class UpdateTool(Tool):
|
|
|
128
128
|
columns = ""
|
|
129
129
|
|
|
130
130
|
for k, v in schema.items():
|
|
131
|
-
columns += f"column {k} => {v.
|
|
131
|
+
columns += f"column {k} => {v.model_dump(mode='json')}"
|
|
132
132
|
|
|
133
133
|
anyOf = []
|
|
134
134
|
|
|
@@ -500,7 +500,7 @@ class AdvancedSearchTool(Tool):
|
|
|
500
500
|
columns = ""
|
|
501
501
|
|
|
502
502
|
for k, v in schema.items():
|
|
503
|
-
columns += f"column {k} => {v.
|
|
503
|
+
columns += f"column {k} => {v.model_dump(mode='json')}\n"
|
|
504
504
|
|
|
505
505
|
input_schema = {
|
|
506
506
|
"type": "object",
|
|
@@ -542,7 +542,7 @@ class AdvancedDeleteRowsTool(Tool):
|
|
|
542
542
|
columns = ""
|
|
543
543
|
|
|
544
544
|
for k, v in schema.items():
|
|
545
|
-
columns += f"column {k} => {v.
|
|
545
|
+
columns += f"column {k} => {v.model_dump(mode='json')}"
|
|
546
546
|
|
|
547
547
|
input_schema = {
|
|
548
548
|
"type": "object",
|
meshagent/tools/script.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
from meshagent.api import RoomClient
|
|
2
|
-
from meshagent.tools import
|
|
2
|
+
from meshagent.tools.tool import Tool, ToolContext
|
|
3
|
+
from meshagent.tools.toolkit import Toolkit, ToolkitBuilder
|
|
3
4
|
|
|
4
|
-
from meshagent.agents.adapter import (
|
|
5
|
-
ToolkitBuilder,
|
|
6
|
-
)
|
|
7
5
|
|
|
8
6
|
from meshagent.api.specs.service import ContainerMountSpec, RoomStorageMountSpec
|
|
9
7
|
from typing import Literal
|
|
@@ -311,3 +309,39 @@ class ScriptTool(Tool):
|
|
|
311
309
|
)
|
|
312
310
|
|
|
313
311
|
return {"results": results}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def get_script_tools(room: RoomClient):
|
|
315
|
+
services = await room.services.list()
|
|
316
|
+
|
|
317
|
+
st = []
|
|
318
|
+
|
|
319
|
+
for service in services:
|
|
320
|
+
if service.metadata.annotations is not None:
|
|
321
|
+
print("X")
|
|
322
|
+
type = service.metadata.annotations.get("meshagent.tool.type")
|
|
323
|
+
print(type)
|
|
324
|
+
commands_str = service.metadata.annotations.get("meshagent.tool.commands")
|
|
325
|
+
print(commands_str)
|
|
326
|
+
tool_name = service.metadata.annotations.get(
|
|
327
|
+
"meshagent.tool.name", service.metadata.name
|
|
328
|
+
)
|
|
329
|
+
description = service.metadata.annotations.get(
|
|
330
|
+
"meshagent.tool.description", service.metadata.description
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if type == "script" and tool_name is not None:
|
|
334
|
+
print("FOUND")
|
|
335
|
+
if commands_str is not None:
|
|
336
|
+
commands = commands_str.split("\n")
|
|
337
|
+
|
|
338
|
+
st.append(
|
|
339
|
+
ScriptTool(
|
|
340
|
+
name=tool_name,
|
|
341
|
+
description=description,
|
|
342
|
+
service_id=service.id,
|
|
343
|
+
commands=commands,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return st
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from meshagent.api.messaging import JsonResponse
|
|
5
|
+
from meshagent.tools import Toolkit, ToolContext, tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Payload(BaseModel):
|
|
9
|
+
name: str
|
|
10
|
+
count: int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Result(BaseModel):
|
|
14
|
+
name: str
|
|
15
|
+
count: int
|
|
16
|
+
flag: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@tool(name="make_payload")
|
|
20
|
+
async def make_payload(context: ToolContext, payload: Payload, flag: bool):
|
|
21
|
+
return Result(name=payload.name, count=payload.count, flag=flag)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def test_decorated_tool_executes_with_toolkit():
|
|
26
|
+
toolkit = Toolkit(name="test", tools=[make_payload])
|
|
27
|
+
context = ToolContext(room=object(), caller=object())
|
|
28
|
+
|
|
29
|
+
result = await toolkit.execute(
|
|
30
|
+
context=context,
|
|
31
|
+
name="make_payload",
|
|
32
|
+
arguments={"payload": {"name": "alpha", "count": 2}, "flag": True},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert isinstance(result, JsonResponse)
|
|
36
|
+
assert result.json == {"name": "alpha", "count": 2, "flag": True}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_decorator_schema_is_strict():
|
|
40
|
+
schema = make_payload.input_schema
|
|
41
|
+
|
|
42
|
+
assert schema["type"] == "object"
|
|
43
|
+
assert schema["additionalProperties"] is False
|
|
44
|
+
assert "payload" in schema["properties"]
|
|
45
|
+
assert "flag" in schema["properties"]
|
meshagent/tools/tool.py
CHANGED
|
@@ -3,9 +3,15 @@ from meshagent.api.participant import Participant
|
|
|
3
3
|
import logging
|
|
4
4
|
from abc import ABC
|
|
5
5
|
|
|
6
|
-
from typing import Optional, Dict, Any, Callable
|
|
6
|
+
from typing import Optional, Dict, Any, Callable, get_type_hints
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import inspect
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, create_model
|
|
11
|
+
|
|
12
|
+
from meshagent.tools.strict_schema import ensure_strict_json_schema
|
|
13
|
+
|
|
14
|
+
from meshagent.api.messaging import Response, ensure_response
|
|
9
15
|
|
|
10
16
|
|
|
11
17
|
from opentelemetry import trace
|
|
@@ -117,5 +123,84 @@ class Tool(BaseTool):
|
|
|
117
123
|
if defs is not None:
|
|
118
124
|
openai_schema["$defs"] = {**defs}
|
|
119
125
|
|
|
126
|
+
async def invoke(
|
|
127
|
+
self, context: ToolContext, arguments: dict, attachment: Optional[bytes] = None
|
|
128
|
+
) -> Response:
|
|
129
|
+
return await self.execute(context=context, **arguments)
|
|
130
|
+
|
|
120
131
|
async def execute(self, context: ToolContext, **kwargs) -> Response:
|
|
121
132
|
raise (Exception("Not implemented"))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def tool(
|
|
136
|
+
*,
|
|
137
|
+
name: Optional[str] = None,
|
|
138
|
+
title: Optional[str] = None,
|
|
139
|
+
description: Optional[str] = None,
|
|
140
|
+
rules: Optional[list[str]] = None,
|
|
141
|
+
thumbnail_url: Optional[str] = None,
|
|
142
|
+
):
|
|
143
|
+
def decorator(fn: Callable[..., Response]):
|
|
144
|
+
signature = inspect.signature(fn)
|
|
145
|
+
hints = get_type_hints(fn, include_extras=True)
|
|
146
|
+
|
|
147
|
+
supports_context = False
|
|
148
|
+
fields: dict[str, tuple[Any, Any]] = {}
|
|
149
|
+
|
|
150
|
+
for param_name, param in signature.parameters.items():
|
|
151
|
+
annotation = hints.get(param_name, Any)
|
|
152
|
+
if annotation is ToolContext:
|
|
153
|
+
supports_context = True
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
default = param.default if param.default is not inspect._empty else ...
|
|
157
|
+
fields[param_name] = (annotation, default)
|
|
158
|
+
|
|
159
|
+
InputModel = create_model(f"{fn.__name__}Input", **fields)
|
|
160
|
+
schema = InputModel.model_json_schema()
|
|
161
|
+
strict_schema = ensure_strict_json_schema(schema)
|
|
162
|
+
|
|
163
|
+
tool_name = name or fn.__name__
|
|
164
|
+
tool_title = title or tool_name
|
|
165
|
+
tool_description = (
|
|
166
|
+
description if description is not None else (fn.__doc__ or "").strip()
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
class FunctionTool(Tool):
|
|
170
|
+
def __init__(self):
|
|
171
|
+
super().__init__(
|
|
172
|
+
name=tool_name,
|
|
173
|
+
title=tool_title,
|
|
174
|
+
description=tool_description,
|
|
175
|
+
rules=rules,
|
|
176
|
+
thumbnail_url=thumbnail_url,
|
|
177
|
+
input_schema=strict_schema,
|
|
178
|
+
supports_context=supports_context,
|
|
179
|
+
)
|
|
180
|
+
self.strict = True
|
|
181
|
+
|
|
182
|
+
async def invoke(
|
|
183
|
+
self,
|
|
184
|
+
context: ToolContext,
|
|
185
|
+
arguments: dict,
|
|
186
|
+
attachment: Optional[bytes] = None,
|
|
187
|
+
) -> Response:
|
|
188
|
+
data = InputModel.model_validate(arguments)
|
|
189
|
+
parsed_args = {field: getattr(data, field) for field in fields}
|
|
190
|
+
|
|
191
|
+
if supports_context:
|
|
192
|
+
result = fn(context, **parsed_args)
|
|
193
|
+
else:
|
|
194
|
+
result = fn(**parsed_args)
|
|
195
|
+
|
|
196
|
+
if inspect.isawaitable(result):
|
|
197
|
+
result = await result
|
|
198
|
+
|
|
199
|
+
if isinstance(result, BaseModel):
|
|
200
|
+
result = result.model_dump(mode="json")
|
|
201
|
+
|
|
202
|
+
return ensure_response(result)
|
|
203
|
+
|
|
204
|
+
return FunctionTool()
|
|
205
|
+
|
|
206
|
+
return decorator
|
meshagent/tools/toolkit.py
CHANGED
|
@@ -8,7 +8,7 @@ import json
|
|
|
8
8
|
|
|
9
9
|
from typing import Optional, Literal
|
|
10
10
|
from meshagent.tools.config import ToolkitConfig
|
|
11
|
-
from meshagent.tools.tool import ToolContext, BaseTool
|
|
11
|
+
from meshagent.tools.tool import ToolContext, BaseTool, Tool
|
|
12
12
|
|
|
13
13
|
from opentelemetry import trace
|
|
14
14
|
|
|
@@ -91,7 +91,12 @@ class Toolkit(ToolkitBuilder):
|
|
|
91
91
|
schema["$defs"] = {**tool.defs}
|
|
92
92
|
|
|
93
93
|
validate(arguments, schema)
|
|
94
|
-
|
|
94
|
+
if isinstance(tool, Tool):
|
|
95
|
+
response = await tool.invoke(
|
|
96
|
+
context=context, arguments=arguments, attachment=attachment
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
raise RoomException("tools must extend the Tool class to be invokable")
|
|
95
100
|
response = ensure_response(response)
|
|
96
101
|
|
|
97
102
|
span.set_attribute("response_type", response.to_json()["type"])
|
meshagent/tools/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.25.1"
|
meshagent/tools/web_toolkit.py
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
from meshagent.api.http import new_client_session
|
|
9
|
+
from meshagent.api.messaging import FileResponse, JsonResponse, Response, TextResponse
|
|
10
|
+
from meshagent.tools.config import ToolkitConfig
|
|
11
|
+
from meshagent.tools.tool import Tool, ToolContext
|
|
12
|
+
from meshagent.tools.toolkit import Toolkit, ToolkitBuilder
|
|
13
|
+
from meshagent.api.room_server_client import RoomClient
|
|
4
14
|
|
|
5
15
|
|
|
6
16
|
class WebToolkit(Toolkit):
|
|
7
17
|
def __init__(self):
|
|
8
|
-
super().__init__(tools=[WebFetchTool()])
|
|
18
|
+
super().__init__(name="web_fetch", tools=[WebFetchTool()])
|
|
9
19
|
|
|
10
20
|
|
|
11
21
|
class WebFetchTool(Tool):
|
|
12
22
|
def __init__(self):
|
|
13
23
|
super().__init__(
|
|
14
|
-
name="
|
|
15
|
-
title="
|
|
16
|
-
description="
|
|
24
|
+
name="web_fetch",
|
|
25
|
+
title="web fetch",
|
|
26
|
+
description="fetches a url and returns text, json, or file content",
|
|
17
27
|
input_schema={
|
|
18
28
|
"type": "object",
|
|
19
29
|
"properties": {
|
|
@@ -22,21 +32,104 @@ class WebFetchTool(Tool):
|
|
|
22
32
|
"description": "the url of the web page (always start it with a proper scheme like https://)",
|
|
23
33
|
}
|
|
24
34
|
},
|
|
35
|
+
"required": ["url"],
|
|
25
36
|
"additionalProperties": False,
|
|
26
37
|
},
|
|
27
38
|
)
|
|
28
39
|
|
|
29
|
-
async def execute(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
) -> str:
|
|
35
|
-
async with ClientSession() as session:
|
|
40
|
+
async def execute(self, context: ToolContext, **kwargs: object) -> Response:
|
|
41
|
+
url = str(kwargs.get("url", ""))
|
|
42
|
+
if not url:
|
|
43
|
+
raise ValueError("url is required")
|
|
44
|
+
async with new_client_session() as session:
|
|
36
45
|
async with session.get(url) as resp:
|
|
37
|
-
|
|
46
|
+
if resp.status >= 400:
|
|
47
|
+
raise Exception(f"web fetch failed with status {resp.status}")
|
|
48
|
+
|
|
49
|
+
content_type = (resp.content_type or "").lower()
|
|
50
|
+
data = await resp.read()
|
|
51
|
+
|
|
52
|
+
if _is_json_content_type(content_type):
|
|
53
|
+
text = _decode_text(data=data, charset=resp.charset)
|
|
54
|
+
try:
|
|
55
|
+
parsed = json.loads(text)
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
return TextResponse(text=text)
|
|
58
|
+
|
|
59
|
+
if isinstance(parsed, dict):
|
|
60
|
+
return JsonResponse(json=parsed)
|
|
61
|
+
return JsonResponse(json={"data": parsed})
|
|
62
|
+
|
|
63
|
+
if _is_text_content_type(content_type):
|
|
64
|
+
text = _decode_text(data=data, charset=resp.charset)
|
|
65
|
+
if content_type == "text/html":
|
|
66
|
+
from html_to_markdown import convert
|
|
67
|
+
|
|
68
|
+
text = convert(text)
|
|
69
|
+
return TextResponse(text=text)
|
|
70
|
+
|
|
71
|
+
if _is_file_content_type(content_type):
|
|
72
|
+
filename = _infer_filename(url=url, content_type=content_type)
|
|
73
|
+
return FileResponse(
|
|
74
|
+
name=filename,
|
|
75
|
+
mime_type=content_type or "application/octet-stream",
|
|
76
|
+
data=data,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
filename = _infer_filename(url=url, content_type=content_type)
|
|
80
|
+
return FileResponse(
|
|
81
|
+
name=filename,
|
|
82
|
+
mime_type=content_type or "application/octet-stream",
|
|
83
|
+
data=data,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _decode_text(*, data: bytes, charset: str | None) -> str:
|
|
88
|
+
encoding = charset or "utf-8"
|
|
89
|
+
return data.decode(encoding, errors="replace")
|
|
90
|
+
|
|
38
91
|
|
|
39
|
-
|
|
92
|
+
def _is_json_content_type(content_type: str) -> bool:
|
|
93
|
+
if content_type in {"application/json", "text/json"}:
|
|
94
|
+
return True
|
|
95
|
+
return content_type.endswith("+json")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _is_text_content_type(content_type: str) -> bool:
|
|
99
|
+
if content_type.startswith("text/"):
|
|
100
|
+
return True
|
|
101
|
+
return content_type in {
|
|
102
|
+
"application/xml",
|
|
103
|
+
"application/xhtml+xml",
|
|
104
|
+
"application/javascript",
|
|
105
|
+
"application/x-javascript",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _is_file_content_type(content_type: str) -> bool:
|
|
110
|
+
if content_type.startswith("image/"):
|
|
111
|
+
return True
|
|
112
|
+
return content_type == "application/pdf"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _infer_filename(*, url: str, content_type: str) -> str:
|
|
116
|
+
parsed = urlparse(url)
|
|
117
|
+
basename = os.path.basename(parsed.path)
|
|
118
|
+
if basename:
|
|
119
|
+
return basename
|
|
120
|
+
extension = mimetypes.guess_extension(content_type or "") or ""
|
|
121
|
+
return f"downloaded-content{extension}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class WebFetchConfig(ToolkitConfig):
|
|
125
|
+
name: str = "web_fetch"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class WebFetchToolkitBuilder(ToolkitBuilder):
|
|
129
|
+
def __init__(self):
|
|
130
|
+
super().__init__(name="web_fetch", type=WebFetchConfig)
|
|
40
131
|
|
|
41
|
-
|
|
42
|
-
|
|
132
|
+
async def make(
|
|
133
|
+
self, *, room: RoomClient, model: str, config: WebFetchConfig
|
|
134
|
+
) -> Toolkit:
|
|
135
|
+
return WebToolkit()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.1
|
|
4
4
|
Summary: Tools for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -12,8 +12,9 @@ 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.
|
|
16
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: meshagent-api~=0.25.1
|
|
16
|
+
Requires-Dist: html-to-markdown~=2.24.3
|
|
17
|
+
Requires-Dist: aiohttp[speedups]~=3.13.0
|
|
17
18
|
Requires-Dist: opentelemetry-distro~=0.54b1
|
|
18
19
|
Dynamic: license-file
|
|
19
20
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
meshagent/tools/__init__.py,sha256=2qAAi4eUVq4q6gXyAGnjQze2Rs9Q9kIJfdm4oEheRPQ,1843
|
|
2
|
+
meshagent/tools/blob.py,sha256=Liw1zdtDc-Viy6Hcgrb4XZ8ULUPEubzShWAdIci0wWI,1505
|
|
3
|
+
meshagent/tools/config.py,sha256=zH2xGxg28K7Tg-aYor6LXdzf0LRxS9iE0679H1FuWhE,79
|
|
4
|
+
meshagent/tools/container_shell.py,sha256=BMcTi_j0-yUPk9ZkVux1j8CxDFmyrCYFn-XeT3S77Ik,7056
|
|
5
|
+
meshagent/tools/database.py,sha256=HWH7_Fm_8GexDJv_T-D6o0yJVUPmE-j31TU2AfcgaPs,18438
|
|
6
|
+
meshagent/tools/datetime.py,sha256=2pOUOWopYIsc5y4EoFo_1PdBaBcTSkeOOs_EqdqYTk0,17503
|
|
7
|
+
meshagent/tools/discovery.py,sha256=f7DJtwIiBQCxByTepsvGM2NRn-9KGxZTZMoTRCKYQ7E,1251
|
|
8
|
+
meshagent/tools/document_tools.py,sha256=LMULXOSBjsvhKjqzxUxe8586t0Vol0v1Btu5v6ofm7A,11755
|
|
9
|
+
meshagent/tools/hosting.py,sha256=l1BCgnSrCJQsWU9Kycq3hEI4ZlYxffDfde6QeJUfko0,10678
|
|
10
|
+
meshagent/tools/multi_tool.py,sha256=hmWZO18Y2tuFG_7rvUed9er29aXleAC-r3YpXBCZWUY,4040
|
|
11
|
+
meshagent/tools/pydantic.py,sha256=n-MD0gC-oRtHSTUDD5IV2dP-xIk-zjcDgHfgjqMgiqM,1161
|
|
12
|
+
meshagent/tools/script.py,sha256=eyQiufoc2ZkTUBTO58VQURnkkQA2lboED5s0-BvAvgM,11811
|
|
13
|
+
meshagent/tools/storage.py,sha256=NVpi9CZKSZUh8PTxxCdJhJy7Gzmdp55-zo2yHYGod_E,23340
|
|
14
|
+
meshagent/tools/strict_schema.py,sha256=IytdAANa6lsfrsg5FsJuqYrxH9D_fayl-Lc9EwgLJSM,6277
|
|
15
|
+
meshagent/tools/tool.py,sha256=HgvlOlz2wMrmD5aaV49fRpnGyXJwnVcH9j4wKaaPbWo,5935
|
|
16
|
+
meshagent/tools/toolkit.py,sha256=iVcCvhwWqmahDow9FN-VRWGo9MEj669Vw7TAB7Mx9Ww,4066
|
|
17
|
+
meshagent/tools/uuid.py,sha256=mzRwDmXy39U5lHhd9wqV4r-ZdS8jPfDTTs4UfW4KHJQ,1342
|
|
18
|
+
meshagent/tools/version.py,sha256=ACu2Z3Q3TFgYpAno_eu9ssJ1QULjNXvjGvyqDSHrQ_o,23
|
|
19
|
+
meshagent/tools/web_toolkit.py,sha256=Seju8gpdUoPku7Yfar_s-cVOnlweFzKj-bFqrQVup8o,4603
|
|
20
|
+
meshagent/tools/tests/tool_decorator_test.py,sha256=Fd4uvoefU8hpdSWaiYen15tqnlwoY092G6hjYafaMGE,1190
|
|
21
|
+
meshagent_tools-0.25.1.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
|
|
22
|
+
meshagent_tools-0.25.1.dist-info/METADATA,sha256=I_DrA3zW7uRwNXwllW8OSipjUKQimC_BgOP4_8xoDBI,2930
|
|
23
|
+
meshagent_tools-0.25.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
24
|
+
meshagent_tools-0.25.1.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
25
|
+
meshagent_tools-0.25.1.dist-info/RECORD,,
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
meshagent/tools/__init__.py,sha256=1zgD5OvAJP10eERoE72VbDu9hFVfAqCWUXw3SiCYFTE,1285
|
|
2
|
-
meshagent/tools/blob.py,sha256=aDW_z8R4HrmrYzAWoWm13Ypqcxcl4rL1Dc0ESnQETLM,1473
|
|
3
|
-
meshagent/tools/config.py,sha256=zH2xGxg28K7Tg-aYor6LXdzf0LRxS9iE0679H1FuWhE,79
|
|
4
|
-
meshagent/tools/database.py,sha256=Jd_YJLPIBsypSK1SziwaYb8xKnG4oafoCDs6vzAC2Oc,18396
|
|
5
|
-
meshagent/tools/datetime.py,sha256=2pOUOWopYIsc5y4EoFo_1PdBaBcTSkeOOs_EqdqYTk0,17503
|
|
6
|
-
meshagent/tools/discovery.py,sha256=f7DJtwIiBQCxByTepsvGM2NRn-9KGxZTZMoTRCKYQ7E,1251
|
|
7
|
-
meshagent/tools/document_tools.py,sha256=LMULXOSBjsvhKjqzxUxe8586t0Vol0v1Btu5v6ofm7A,11755
|
|
8
|
-
meshagent/tools/hosting.py,sha256=l1BCgnSrCJQsWU9Kycq3hEI4ZlYxffDfde6QeJUfko0,10678
|
|
9
|
-
meshagent/tools/multi_tool.py,sha256=hmWZO18Y2tuFG_7rvUed9er29aXleAC-r3YpXBCZWUY,4040
|
|
10
|
-
meshagent/tools/pydantic.py,sha256=n-MD0gC-oRtHSTUDD5IV2dP-xIk-zjcDgHfgjqMgiqM,1161
|
|
11
|
-
meshagent/tools/script.py,sha256=uHrJynzM0SwUHM1qXIjt-UhZLG4AQtFw-yyLv4lxGGw,10589
|
|
12
|
-
meshagent/tools/storage.py,sha256=NVpi9CZKSZUh8PTxxCdJhJy7Gzmdp55-zo2yHYGod_E,23340
|
|
13
|
-
meshagent/tools/strict_schema.py,sha256=IytdAANa6lsfrsg5FsJuqYrxH9D_fayl-Lc9EwgLJSM,6277
|
|
14
|
-
meshagent/tools/tool.py,sha256=9OAlbfaHqfgJnCDBSW-8PS0Z1K1KjWGD3JBUyiHOxAk,3131
|
|
15
|
-
meshagent/tools/toolkit.py,sha256=rCCkpQBoSkmmhjnRGA4jx0QP-ds6WTJ0PkQVnf1Ls7s,3843
|
|
16
|
-
meshagent/tools/uuid.py,sha256=mzRwDmXy39U5lHhd9wqV4r-ZdS8jPfDTTs4UfW4KHJQ,1342
|
|
17
|
-
meshagent/tools/version.py,sha256=1LERjg6TTlEZCKHKtH82Ox1j8xI8WMA94hEjeTPs1Io,23
|
|
18
|
-
meshagent/tools/web_toolkit.py,sha256=IoOYjOBmcbQsqWT14xYg02jjWpWmGOkDSxt2U-LQoaA,1258
|
|
19
|
-
meshagent_tools-0.24.6.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
|
|
20
|
-
meshagent_tools-0.24.6.dist-info/METADATA,sha256=AzHewWZ3JwdinUT2J0eNarP9N_IPR-GF2fKN13myULI,2878
|
|
21
|
-
meshagent_tools-0.24.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
-
meshagent_tools-0.24.6.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
23
|
-
meshagent_tools-0.24.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|