agents-function-tools 0.2.0__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.
- agents_function_tools-0.2.0.dist-info/METADATA +90 -0
- agents_function_tools-0.2.0.dist-info/RECORD +14 -0
- agents_function_tools-0.2.0.dist-info/WHEEL +5 -0
- agents_function_tools-0.2.0.dist-info/licenses/LICENSE +202 -0
- agents_function_tools-0.2.0.dist-info/top_level.txt +1 -0
- function_tools/__init__.py +21 -0
- function_tools/archive.py +220 -0
- function_tools/command.py +172 -0
- function_tools/errors.py +11 -0
- function_tools/host.py +43 -0
- function_tools/http.py +136 -0
- function_tools/openai_tools.py +398 -0
- function_tools/responses.py +39 -0
- function_tools/workspace.py +433 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agents import function_tool
|
|
9
|
+
|
|
10
|
+
from .archive import ZipArchive
|
|
11
|
+
from .command import CommandPolicy, LocalCommandRunner
|
|
12
|
+
from .errors import FoundationToolError
|
|
13
|
+
from .host import HostInspector
|
|
14
|
+
from .http import HttpPolicy, HttpTextClient
|
|
15
|
+
from .responses import ToolResponse
|
|
16
|
+
from .workspace import Workspace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ToolConfig:
|
|
21
|
+
workspace_root: Path
|
|
22
|
+
command_programs: Mapping[str, str] = field(default_factory=dict)
|
|
23
|
+
max_read_bytes: int = 1_000_000
|
|
24
|
+
max_write_bytes: int = 1_000_000
|
|
25
|
+
max_command_timeout_seconds: int = 300
|
|
26
|
+
max_command_output_chars: int = 65_536
|
|
27
|
+
http_allowed_hosts: frozenset[str] = frozenset()
|
|
28
|
+
http_max_response_bytes: int = 1_000_000
|
|
29
|
+
http_timeout_seconds: int = 15
|
|
30
|
+
environment_variables: frozenset[str] = frozenset()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class FunctionToolBundle:
|
|
35
|
+
"""Tools grouped by the approval policy that must guard them."""
|
|
36
|
+
|
|
37
|
+
safe_read: tuple[Any, ...]
|
|
38
|
+
workspace_write: tuple[Any, ...]
|
|
39
|
+
workspace_execution: tuple[Any, ...]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def all(self) -> tuple[Any, ...]:
|
|
43
|
+
return self.safe_read + self.workspace_write + self.workspace_execution
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_function_tools(config: ToolConfig) -> FunctionToolBundle:
|
|
47
|
+
"""Create SDK function tools bound to one host-controlled workspace and policy."""
|
|
48
|
+
|
|
49
|
+
workspace = Workspace(
|
|
50
|
+
config.workspace_root,
|
|
51
|
+
max_read_bytes=config.max_read_bytes,
|
|
52
|
+
max_write_bytes=config.max_write_bytes,
|
|
53
|
+
)
|
|
54
|
+
archives = ZipArchive(workspace)
|
|
55
|
+
host = HostInspector(config.environment_variables)
|
|
56
|
+
http = HttpTextClient(
|
|
57
|
+
HttpPolicy(
|
|
58
|
+
allowed_hosts=config.http_allowed_hosts,
|
|
59
|
+
max_response_bytes=config.http_max_response_bytes,
|
|
60
|
+
timeout_seconds=config.http_timeout_seconds,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
command_policy = CommandPolicy(
|
|
64
|
+
programs=config.command_programs,
|
|
65
|
+
max_timeout_seconds=config.max_command_timeout_seconds,
|
|
66
|
+
max_output_chars=config.max_command_output_chars,
|
|
67
|
+
)
|
|
68
|
+
command_runner = LocalCommandRunner(
|
|
69
|
+
workspace,
|
|
70
|
+
command_policy,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@function_tool
|
|
74
|
+
def workspace_list(path: str = ".", recursive: bool = False, max_entries: int = 200) -> str:
|
|
75
|
+
"""List files inside the configured workspace.
|
|
76
|
+
|
|
77
|
+
This is read-only and retry-safe. Paths must be workspace-relative. Set
|
|
78
|
+
recursive only when nested entries are needed; output is capped by max_entries.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
return _respond(
|
|
82
|
+
"workspace_list",
|
|
83
|
+
"safe_read",
|
|
84
|
+
lambda: workspace.list_directory(path, recursive=recursive, max_entries=max_entries),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@function_tool
|
|
88
|
+
def workspace_read_text(path: str, max_bytes: int | None = None) -> str:
|
|
89
|
+
"""Read one UTF-8 text file inside the configured workspace.
|
|
90
|
+
|
|
91
|
+
This is read-only and retry-safe. Binary files, oversized files, absolute
|
|
92
|
+
paths, and paths outside the workspace are rejected.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
return _respond(
|
|
96
|
+
"workspace_read_text",
|
|
97
|
+
"safe_read",
|
|
98
|
+
lambda: workspace.read_text(path, max_bytes=max_bytes),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@function_tool
|
|
102
|
+
def workspace_stat(path: str) -> str:
|
|
103
|
+
"""Return type, size, and timestamps for one workspace-relative path.
|
|
104
|
+
|
|
105
|
+
This is read-only and retry-safe. File content is never returned.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
return _respond("workspace_stat", "safe_read", lambda: workspace.stat_path(path))
|
|
109
|
+
|
|
110
|
+
@function_tool
|
|
111
|
+
def workspace_find(
|
|
112
|
+
pattern: str,
|
|
113
|
+
path: str = ".",
|
|
114
|
+
kind: str = "any",
|
|
115
|
+
max_entries: int = 200,
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Find workspace paths with a glob-style filename pattern.
|
|
118
|
+
|
|
119
|
+
This is read-only and retry-safe. Restrict kind to file, directory, or
|
|
120
|
+
symlink when a narrower search is useful. Results are capped.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
return _respond(
|
|
124
|
+
"workspace_find",
|
|
125
|
+
"safe_read",
|
|
126
|
+
lambda: workspace.find_paths(pattern, path=path, kind=kind, max_entries=max_entries),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@function_tool
|
|
130
|
+
def workspace_hash_file(path: str, algorithm: str = "sha256") -> str:
|
|
131
|
+
"""Calculate a SHA-256, SHA-512, or BLAKE2b hash of one bounded workspace file.
|
|
132
|
+
|
|
133
|
+
This is read-only and retry-safe. File contents are not returned.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
return _respond(
|
|
137
|
+
"workspace_hash_file",
|
|
138
|
+
"safe_read",
|
|
139
|
+
lambda: workspace.hash_file(path, algorithm=algorithm),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@function_tool
|
|
143
|
+
def workspace_disk_usage() -> str:
|
|
144
|
+
"""Return storage capacity available to the configured workspace.
|
|
145
|
+
|
|
146
|
+
This is read-only and retry-safe. It does not inspect other filesystems.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
return _respond("workspace_disk_usage", "safe_read", workspace.disk_usage)
|
|
150
|
+
|
|
151
|
+
@function_tool
|
|
152
|
+
def fetch_https_text(url: str) -> str:
|
|
153
|
+
"""Fetch a bounded text response from a host-configured HTTPS allowlist.
|
|
154
|
+
|
|
155
|
+
This is read-only but not retry-safe because remote content may change.
|
|
156
|
+
Redirects, credentials in URLs, private network addresses, binary content,
|
|
157
|
+
and hosts not configured by the service are rejected.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
return _respond("fetch_https_text", "safe_read", lambda: http.fetch(url))
|
|
161
|
+
|
|
162
|
+
@function_tool
|
|
163
|
+
def host_system_info() -> str:
|
|
164
|
+
"""Return non-sensitive operating system and Python runtime metadata.
|
|
165
|
+
|
|
166
|
+
This is read-only and retry-safe. Secrets, users, process lists, network
|
|
167
|
+
configuration, and installed software are intentionally excluded.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
return _respond("host_system_info", "safe_read", host.system_info)
|
|
171
|
+
|
|
172
|
+
@function_tool
|
|
173
|
+
def host_current_time() -> str:
|
|
174
|
+
"""Return the current UTC timestamp from the host.
|
|
175
|
+
|
|
176
|
+
This is read-only and not retry-safe because time advances.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
return _respond("host_current_time", "safe_read", host.current_time)
|
|
180
|
+
|
|
181
|
+
@function_tool
|
|
182
|
+
def host_environment_get(name: str) -> str:
|
|
183
|
+
"""Read one explicitly allowlisted environment variable.
|
|
184
|
+
|
|
185
|
+
This is read-only and retry-safe. The service configuration controls which
|
|
186
|
+
variable names are readable; no variables are readable by default.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
return _respond("host_environment_get", "safe_read", lambda: host.environment(name))
|
|
190
|
+
|
|
191
|
+
@function_tool
|
|
192
|
+
def command_policy_info() -> str:
|
|
193
|
+
"""Describe available command aliases and execution limits.
|
|
194
|
+
|
|
195
|
+
This is read-only and retry-safe. Host executable paths are not revealed.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
return _respond("command_policy_info", "safe_read", command_policy.describe)
|
|
199
|
+
|
|
200
|
+
@function_tool(needs_approval=True)
|
|
201
|
+
def workspace_write_text(
|
|
202
|
+
path: str,
|
|
203
|
+
content: str,
|
|
204
|
+
overwrite: bool = False,
|
|
205
|
+
create_parents: bool = False,
|
|
206
|
+
) -> str:
|
|
207
|
+
"""Write one UTF-8 text file inside the configured workspace.
|
|
208
|
+
|
|
209
|
+
This changes the workspace and is not retry-safe unless the caller uses the
|
|
210
|
+
same content and overwrite policy. Expose it only after write approval.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
return _respond(
|
|
214
|
+
"workspace_write_text",
|
|
215
|
+
"workspace_write",
|
|
216
|
+
lambda: workspace.write_text(
|
|
217
|
+
path,
|
|
218
|
+
content,
|
|
219
|
+
overwrite=overwrite,
|
|
220
|
+
create_parents=create_parents,
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@function_tool(needs_approval=True)
|
|
225
|
+
def workspace_create_directory(path: str, parents: bool = True, exist_ok: bool = False) -> str:
|
|
226
|
+
"""Create a directory inside the configured workspace.
|
|
227
|
+
|
|
228
|
+
This changes the workspace. Expose it only after write approval. The
|
|
229
|
+
workspace root itself is protected.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
return _respond(
|
|
233
|
+
"workspace_create_directory",
|
|
234
|
+
"workspace_write",
|
|
235
|
+
lambda: workspace.create_directory(path, parents=parents, exist_ok=exist_ok),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@function_tool(needs_approval=True)
|
|
239
|
+
def workspace_copy_file(
|
|
240
|
+
source: str,
|
|
241
|
+
destination: str,
|
|
242
|
+
overwrite: bool = False,
|
|
243
|
+
create_parents: bool = False,
|
|
244
|
+
) -> str:
|
|
245
|
+
"""Copy one regular file inside the configured workspace.
|
|
246
|
+
|
|
247
|
+
This changes the workspace and is not retry-safe. Source symlinks are
|
|
248
|
+
rejected. Expose it only after write approval.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
return _respond(
|
|
252
|
+
"workspace_copy_file",
|
|
253
|
+
"workspace_write",
|
|
254
|
+
lambda: workspace.copy_file(
|
|
255
|
+
source,
|
|
256
|
+
destination,
|
|
257
|
+
overwrite=overwrite,
|
|
258
|
+
create_parents=create_parents,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@function_tool(needs_approval=True)
|
|
263
|
+
def workspace_move_path(
|
|
264
|
+
source: str,
|
|
265
|
+
destination: str,
|
|
266
|
+
overwrite: bool = False,
|
|
267
|
+
create_parents: bool = False,
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Move one file or directory inside the configured workspace.
|
|
270
|
+
|
|
271
|
+
This changes paths and can overwrite a destination when explicitly enabled.
|
|
272
|
+
It requires approval and is not retry-safe.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
return _respond(
|
|
276
|
+
"workspace_move_path",
|
|
277
|
+
"workspace_write",
|
|
278
|
+
lambda: workspace.move_path(
|
|
279
|
+
source,
|
|
280
|
+
destination,
|
|
281
|
+
overwrite=overwrite,
|
|
282
|
+
create_parents=create_parents,
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@function_tool
|
|
287
|
+
def zip_list_entries(path: str, max_entries: int = 200) -> str:
|
|
288
|
+
"""List bounded metadata for entries in a workspace ZIP archive.
|
|
289
|
+
|
|
290
|
+
This is read-only and retry-safe. Archive content is not extracted.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
return _respond(
|
|
294
|
+
"zip_list_entries",
|
|
295
|
+
"safe_read",
|
|
296
|
+
lambda: archives.list_entries(path, max_entries=max_entries),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@function_tool(needs_approval=True)
|
|
300
|
+
def zip_create(paths: list[str], destination: str, overwrite: bool = False) -> str:
|
|
301
|
+
"""Create a ZIP archive from workspace files and directories.
|
|
302
|
+
|
|
303
|
+
This changes the workspace and is not retry-safe. Symlinks and inputs above
|
|
304
|
+
the configured size limit are rejected. Expose it only after write approval.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
return _respond(
|
|
308
|
+
"zip_create",
|
|
309
|
+
"workspace_write",
|
|
310
|
+
lambda: archives.create(paths, destination, overwrite=overwrite),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@function_tool(needs_approval=True)
|
|
314
|
+
def zip_extract(path: str, destination: str, overwrite: bool = False) -> str:
|
|
315
|
+
"""Extract a workspace ZIP archive into a workspace directory.
|
|
316
|
+
|
|
317
|
+
This changes the workspace and requires approval. Path traversal, archive
|
|
318
|
+
symlinks, non-empty destinations, and oversized output are rejected.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
return _respond(
|
|
322
|
+
"zip_extract",
|
|
323
|
+
"workspace_write",
|
|
324
|
+
lambda: archives.extract(path, destination, overwrite=overwrite),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
@function_tool(needs_approval=True)
|
|
328
|
+
def workspace_delete_path(path: str, recursive: bool = False) -> str:
|
|
329
|
+
"""Delete a file, symlink, empty directory, or explicitly recursive directory.
|
|
330
|
+
|
|
331
|
+
This is destructive and not retry-safe. Expose it only after delete approval.
|
|
332
|
+
Paths outside the workspace and the workspace root are always rejected.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
return _respond(
|
|
336
|
+
"workspace_delete_path",
|
|
337
|
+
"workspace_write",
|
|
338
|
+
lambda: workspace.delete_path(path, recursive=recursive),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
@function_tool(needs_approval=True)
|
|
342
|
+
def run_local_command(
|
|
343
|
+
program: str,
|
|
344
|
+
args: list[str],
|
|
345
|
+
cwd: str = ".",
|
|
346
|
+
timeout_seconds: int = 30,
|
|
347
|
+
) -> str:
|
|
348
|
+
"""Run one host-allowlisted program without a shell.
|
|
349
|
+
|
|
350
|
+
This can change the workspace and is not generally retry-safe. Expose it only
|
|
351
|
+
after execution approval. Use a configured program alias and separate args;
|
|
352
|
+
shell syntax is not interpreted. The host must provide OS-level isolation.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
return _respond(
|
|
356
|
+
"run_local_command",
|
|
357
|
+
"workspace_execution",
|
|
358
|
+
lambda: command_runner.run(program, args, cwd=cwd, timeout_seconds=timeout_seconds),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return FunctionToolBundle(
|
|
362
|
+
safe_read=(
|
|
363
|
+
workspace_list,
|
|
364
|
+
workspace_read_text,
|
|
365
|
+
workspace_stat,
|
|
366
|
+
workspace_find,
|
|
367
|
+
workspace_hash_file,
|
|
368
|
+
workspace_disk_usage,
|
|
369
|
+
fetch_https_text,
|
|
370
|
+
host_system_info,
|
|
371
|
+
host_current_time,
|
|
372
|
+
host_environment_get,
|
|
373
|
+
command_policy_info,
|
|
374
|
+
zip_list_entries,
|
|
375
|
+
),
|
|
376
|
+
workspace_write=(
|
|
377
|
+
workspace_write_text,
|
|
378
|
+
workspace_create_directory,
|
|
379
|
+
workspace_copy_file,
|
|
380
|
+
workspace_move_path,
|
|
381
|
+
zip_create,
|
|
382
|
+
zip_extract,
|
|
383
|
+
workspace_delete_path,
|
|
384
|
+
),
|
|
385
|
+
workspace_execution=(run_local_command,),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _respond(tool: str, effect: str, action: Callable[[], Any]) -> str:
|
|
390
|
+
try:
|
|
391
|
+
return ToolResponse.success(tool, effect, action()).to_json()
|
|
392
|
+
except FoundationToolError as error:
|
|
393
|
+
return ToolResponse.failure(tool, effect, error).to_json()
|
|
394
|
+
except Exception:
|
|
395
|
+
error = FoundationToolError(
|
|
396
|
+
"INTERNAL_ERROR", "The tool failed without exposing internal details."
|
|
397
|
+
)
|
|
398
|
+
return ToolResponse.failure(tool, effect, error).to_json()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import FoundationToolError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ToolFailure:
|
|
12
|
+
code: str
|
|
13
|
+
message: str
|
|
14
|
+
retryable: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ToolResponse:
|
|
19
|
+
ok: bool
|
|
20
|
+
tool: str
|
|
21
|
+
effect: str
|
|
22
|
+
data: Any = None
|
|
23
|
+
error: ToolFailure | None = None
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def success(cls, tool: str, effect: str, data: Any) -> ToolResponse:
|
|
27
|
+
return cls(ok=True, tool=tool, effect=effect, data=data)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def failure(cls, tool: str, effect: str, error: FoundationToolError) -> ToolResponse:
|
|
31
|
+
return cls(
|
|
32
|
+
ok=False,
|
|
33
|
+
tool=tool,
|
|
34
|
+
effect=effect,
|
|
35
|
+
error=ToolFailure(error.code, error.message, error.retryable),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def to_json(self) -> str:
|
|
39
|
+
return json.dumps(asdict(self), ensure_ascii=False, separators=(",", ":"))
|