weakincentives 0.3.0__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of weakincentives might be problematic. Click here for more details.
- weakincentives/__init__.py +1 -1
- weakincentives/adapters/__init__.py +3 -2
- weakincentives/examples/code_review_prompt.py +12 -3
- weakincentives/examples/code_review_session.py +7 -3
- weakincentives/prompt/markdown.py +1 -1
- weakincentives/prompt/prompt.py +7 -7
- weakincentives/prompt/structured_output.py +2 -2
- weakincentives/prompt/tool.py +2 -2
- weakincentives/serde/dataclass_serde.py +16 -14
- weakincentives/session/session.py +5 -0
- weakincentives/tools/__init__.py +12 -0
- weakincentives/tools/asteval.py +698 -0
- weakincentives/tools/vfs.py +59 -56
- weakincentives-0.4.0.dist-info/METADATA +490 -0
- {weakincentives-0.3.0.dist-info → weakincentives-0.4.0.dist-info}/RECORD +17 -16
- weakincentives-0.3.0.dist-info/METADATA +0 -231
- {weakincentives-0.3.0.dist-info → weakincentives-0.4.0.dist-info}/WHEEL +0 -0
- {weakincentives-0.3.0.dist-info → weakincentives-0.4.0.dist-info}/licenses/LICENSE +0 -0
weakincentives/tools/vfs.py
CHANGED
|
@@ -44,14 +44,14 @@ _VFS_SECTION_TEMPLATE: Final[str] = (
|
|
|
44
44
|
" specific files; keep listings focused to reduce output.\n"
|
|
45
45
|
"2. Fetch file contents with `vfs_read_file` and work from the returned version"
|
|
46
46
|
" to avoid conflicts.\n"
|
|
47
|
-
"3. Create or update files with `vfs_write_file`; supply
|
|
47
|
+
"3. Create or update files with `vfs_write_file`; supply UTF-8 content up to"
|
|
48
48
|
" 48k characters and prefer overwriting full files unless streaming append"
|
|
49
49
|
" updates.\n"
|
|
50
50
|
"4. Remove obsolete files or directories with `vfs_delete_entry` to keep the"
|
|
51
51
|
" snapshot tidy.\n"
|
|
52
52
|
"5. Host mounts are session-initialization only; agents cannot mount additional"
|
|
53
53
|
" directories later.\n"
|
|
54
|
-
"6. Avoid mirroring large repositories or binary assets—only
|
|
54
|
+
"6. Avoid mirroring large repositories or binary assets—only UTF-8 text is"
|
|
55
55
|
" accepted and host mounts remain constrained by their configuration."
|
|
56
56
|
)
|
|
57
57
|
|
|
@@ -140,18 +140,19 @@ class VfsToolsSection(MarkdownSection[_VfsSectionParams]):
|
|
|
140
140
|
allowed_roots = tuple(_normalize_root(path) for path in allowed_host_roots)
|
|
141
141
|
session.register_reducer(VirtualFileSystem, replace_latest)
|
|
142
142
|
mount_snapshot = _materialize_mounts(mounts, allowed_roots)
|
|
143
|
+
session.seed_slice(VirtualFileSystem, (mount_snapshot,))
|
|
143
144
|
session.register_reducer(
|
|
144
145
|
WriteFile,
|
|
145
|
-
_make_write_reducer(
|
|
146
|
+
_make_write_reducer(),
|
|
146
147
|
slice_type=VirtualFileSystem,
|
|
147
148
|
)
|
|
148
149
|
session.register_reducer(
|
|
149
150
|
DeleteEntry,
|
|
150
|
-
_make_delete_reducer(
|
|
151
|
+
_make_delete_reducer(),
|
|
151
152
|
slice_type=VirtualFileSystem,
|
|
152
153
|
)
|
|
153
154
|
|
|
154
|
-
tools = _build_tools(session=session
|
|
155
|
+
tools = _build_tools(session=session)
|
|
155
156
|
super().__init__(
|
|
156
157
|
title="Virtual Filesystem Tools",
|
|
157
158
|
key="vfs.tools",
|
|
@@ -162,9 +163,9 @@ class VfsToolsSection(MarkdownSection[_VfsSectionParams]):
|
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
def _build_tools(
|
|
165
|
-
*, session: Session
|
|
166
|
+
*, session: Session
|
|
166
167
|
) -> tuple[Tool[SupportsDataclass, SupportsDataclass], ...]:
|
|
167
|
-
suite = _VfsToolSuite(session=session
|
|
168
|
+
suite = _VfsToolSuite(session=session)
|
|
168
169
|
return (
|
|
169
170
|
Tool[ListDirectory, ListDirectoryResult](
|
|
170
171
|
name="vfs_list_directory",
|
|
@@ -192,9 +193,8 @@ def _build_tools(
|
|
|
192
193
|
class _VfsToolSuite:
|
|
193
194
|
"""Collection of VFS handlers bound to a session instance."""
|
|
194
195
|
|
|
195
|
-
def __init__(self, *, session: Session
|
|
196
|
+
def __init__(self, *, session: Session) -> None:
|
|
196
197
|
self._session = session
|
|
197
|
-
self._mount_snapshot = mount_snapshot
|
|
198
198
|
|
|
199
199
|
def list_directory(self, params: ListDirectory) -> ToolResult[ListDirectoryResult]:
|
|
200
200
|
target = _normalize_optional_path(params.path)
|
|
@@ -229,9 +229,7 @@ class _VfsToolSuite:
|
|
|
229
229
|
file = _find_file(snapshot.files, path)
|
|
230
230
|
if file is None:
|
|
231
231
|
raise ToolValidationError("File does not exist in the virtual filesystem.")
|
|
232
|
-
message = (
|
|
233
|
-
f"Read {file.size_bytes} bytes from {'/'.join(file.path.segments) or '.'}."
|
|
234
|
-
)
|
|
232
|
+
message = _format_read_file_message(file)
|
|
235
233
|
return ToolResult(message=message, value=file)
|
|
236
234
|
|
|
237
235
|
def write_file(self, params: WriteFile) -> ToolResult[WriteFile]:
|
|
@@ -247,31 +245,27 @@ class _VfsToolSuite:
|
|
|
247
245
|
if mode in {"overwrite", "append"} and existing is None:
|
|
248
246
|
raise ToolValidationError("File does not exist for the requested mode.")
|
|
249
247
|
normalized = WriteFile(path=path, content=content, mode=mode)
|
|
250
|
-
|
|
251
|
-
"create": "created",
|
|
252
|
-
"overwrite": "overwritten",
|
|
253
|
-
"append": "appended",
|
|
254
|
-
}[mode]
|
|
255
|
-
message = f"File {'/'.join(path.segments) or '.'} {action}."
|
|
248
|
+
message = _format_write_file_message(path, content, mode)
|
|
256
249
|
return ToolResult(message=message, value=normalized)
|
|
257
250
|
|
|
258
251
|
def delete_entry(self, params: DeleteEntry) -> ToolResult[DeleteEntry]:
|
|
259
252
|
path = _normalize_path(params.path)
|
|
260
253
|
snapshot = self._latest_snapshot()
|
|
261
|
-
|
|
254
|
+
matches = tuple(
|
|
255
|
+
file
|
|
256
|
+
for file in snapshot.files
|
|
257
|
+
if _is_path_prefix(file.path.segments, path.segments)
|
|
258
|
+
)
|
|
259
|
+
deleted_count = len(matches)
|
|
262
260
|
if deleted_count == 0:
|
|
263
261
|
raise ToolValidationError("No files matched the provided path.")
|
|
264
262
|
normalized = DeleteEntry(path=path)
|
|
265
|
-
message =
|
|
263
|
+
message = _format_delete_message(path, matches)
|
|
266
264
|
return ToolResult(message=message, value=normalized)
|
|
267
265
|
|
|
268
266
|
def _latest_snapshot(self) -> VirtualFileSystem:
|
|
269
267
|
snapshot = select_latest(self._session, VirtualFileSystem)
|
|
270
|
-
|
|
271
|
-
return snapshot
|
|
272
|
-
return (
|
|
273
|
-
self._mount_snapshot if self._mount_snapshot.files else VirtualFileSystem()
|
|
274
|
-
)
|
|
268
|
+
return snapshot or VirtualFileSystem()
|
|
275
269
|
|
|
276
270
|
|
|
277
271
|
def _normalize_content(content: str) -> str:
|
|
@@ -279,7 +273,6 @@ def _normalize_content(content: str) -> str:
|
|
|
279
273
|
raise ToolValidationError(
|
|
280
274
|
"Content exceeds maximum length of 48,000 characters."
|
|
281
275
|
)
|
|
282
|
-
_ensure_ascii(content, "content")
|
|
283
276
|
return content
|
|
284
277
|
|
|
285
278
|
|
|
@@ -352,24 +345,49 @@ def _is_path_prefix(path: Sequence[str], prefix: Sequence[str]) -> bool:
|
|
|
352
345
|
return all(path[index] == prefix[index] for index in range(len(prefix)))
|
|
353
346
|
|
|
354
347
|
|
|
355
|
-
def _count_matching(files: Iterable[VfsFile], path: VfsPath) -> int:
|
|
356
|
-
target = path.segments
|
|
357
|
-
return sum(1 for file in files if _is_path_prefix(file.path.segments, target))
|
|
358
|
-
|
|
359
|
-
|
|
360
348
|
def _format_directory_message(
|
|
361
349
|
path: VfsPath, directories: tuple[str, ...], files: tuple[str, ...]
|
|
362
350
|
) -> str:
|
|
363
|
-
prefix =
|
|
364
|
-
|
|
365
|
-
|
|
351
|
+
prefix = _format_path(path)
|
|
352
|
+
directory_list = ", ".join(directories) if directories else "(none)"
|
|
353
|
+
file_list = ", ".join(files) if files else "(none)"
|
|
354
|
+
return f"Directory {prefix}\nsubdirectories: {directory_list}\nfiles: {file_list}"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _format_read_file_message(file: VfsFile) -> str:
|
|
358
|
+
path_label = _format_path(file.path)
|
|
359
|
+
header = (
|
|
360
|
+
f"Read file {path_label} (version {file.version}, {file.size_bytes} bytes)."
|
|
361
|
+
)
|
|
362
|
+
body = file.content if file.content else "(empty file)"
|
|
363
|
+
return f"{header}\n{body}"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _format_write_file_message(path: VfsPath, content: str, mode: WriteMode) -> str:
|
|
367
|
+
path_label = _format_path(path)
|
|
368
|
+
action = {
|
|
369
|
+
"create": "create",
|
|
370
|
+
"overwrite": "overwrite",
|
|
371
|
+
"append": "append",
|
|
372
|
+
}[mode]
|
|
373
|
+
header = f"Staged {action} for {path_label} ({len(content)} characters)."
|
|
374
|
+
body = content if content else "(empty content)"
|
|
375
|
+
return f"{header}\n{body}"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _format_delete_message(path: VfsPath, files: Sequence[VfsFile]) -> str:
|
|
379
|
+
path_label = _format_path(path)
|
|
380
|
+
deleted_paths = ", ".join(_format_path(file.path) for file in files)
|
|
366
381
|
return (
|
|
367
|
-
f"
|
|
368
|
-
f"
|
|
369
|
-
f"{'s' if file_count != 1 else ''}."
|
|
382
|
+
f"Deleted {len(files)} entr{'ies' if len(files) != 1 else 'y'} under {path_label}:"
|
|
383
|
+
f" {deleted_paths}"
|
|
370
384
|
)
|
|
371
385
|
|
|
372
386
|
|
|
387
|
+
def _format_path(path: VfsPath) -> str:
|
|
388
|
+
return "/".join(path.segments) or "."
|
|
389
|
+
|
|
390
|
+
|
|
373
391
|
def _normalize_root(path: os.PathLike[str] | str) -> Path:
|
|
374
392
|
root = Path(path).expanduser().resolve()
|
|
375
393
|
if not root.exists():
|
|
@@ -425,7 +443,6 @@ def _load_mount(mount: HostMount, allowed_roots: Sequence[Path]) -> tuple[VfsFil
|
|
|
425
443
|
content = path.read_text(encoding=_DEFAULT_ENCODING)
|
|
426
444
|
except UnicodeDecodeError as error: # pragma: no cover - defensive guard
|
|
427
445
|
raise ToolValidationError("Mounted file must be valid UTF-8.") from error
|
|
428
|
-
_ensure_ascii(content, "mounted file content")
|
|
429
446
|
size = len(content.encode(_DEFAULT_ENCODING))
|
|
430
447
|
if mount.max_bytes is not None and consumed_bytes + size > mount.max_bytes:
|
|
431
448
|
raise ToolValidationError("Host mount exceeded the configured byte budget.")
|
|
@@ -481,15 +498,13 @@ def _iter_mount_files(root: Path, follow_symlinks: bool) -> Iterable[Path]:
|
|
|
481
498
|
yield current / name
|
|
482
499
|
|
|
483
500
|
|
|
484
|
-
def _make_write_reducer(
|
|
485
|
-
mount_snapshot: VirtualFileSystem,
|
|
486
|
-
) -> Callable[
|
|
501
|
+
def _make_write_reducer() -> Callable[
|
|
487
502
|
[tuple[VirtualFileSystem, ...], DataEvent], tuple[VirtualFileSystem, ...]
|
|
488
503
|
]:
|
|
489
504
|
def reducer(
|
|
490
505
|
slice_values: tuple[VirtualFileSystem, ...], event: DataEvent
|
|
491
506
|
) -> tuple[VirtualFileSystem, ...]:
|
|
492
|
-
previous =
|
|
507
|
+
previous = slice_values[-1] if slice_values else VirtualFileSystem()
|
|
493
508
|
params = cast(WriteFile, event.value)
|
|
494
509
|
timestamp = _now()
|
|
495
510
|
files = list(previous.files)
|
|
@@ -527,15 +542,13 @@ def _make_write_reducer(
|
|
|
527
542
|
return reducer
|
|
528
543
|
|
|
529
544
|
|
|
530
|
-
def _make_delete_reducer(
|
|
531
|
-
mount_snapshot: VirtualFileSystem,
|
|
532
|
-
) -> Callable[
|
|
545
|
+
def _make_delete_reducer() -> Callable[
|
|
533
546
|
[tuple[VirtualFileSystem, ...], DataEvent], tuple[VirtualFileSystem, ...]
|
|
534
547
|
]:
|
|
535
548
|
def reducer(
|
|
536
549
|
slice_values: tuple[VirtualFileSystem, ...], event: DataEvent
|
|
537
550
|
) -> tuple[VirtualFileSystem, ...]:
|
|
538
|
-
previous =
|
|
551
|
+
previous = slice_values[-1] if slice_values else VirtualFileSystem()
|
|
539
552
|
params = cast(DeleteEntry, event.value)
|
|
540
553
|
target = params.path.segments
|
|
541
554
|
files = [
|
|
@@ -550,16 +563,6 @@ def _make_delete_reducer(
|
|
|
550
563
|
return reducer
|
|
551
564
|
|
|
552
565
|
|
|
553
|
-
def _latest_virtual_filesystem(
|
|
554
|
-
values: tuple[VirtualFileSystem, ...], mount_snapshot: VirtualFileSystem
|
|
555
|
-
) -> VirtualFileSystem:
|
|
556
|
-
if values:
|
|
557
|
-
return values[-1]
|
|
558
|
-
if mount_snapshot.files:
|
|
559
|
-
return mount_snapshot
|
|
560
|
-
return VirtualFileSystem()
|
|
561
|
-
|
|
562
|
-
|
|
563
566
|
def _index_of(files: list[VfsFile], path: VfsPath) -> int | None:
|
|
564
567
|
for index, file in enumerate(files):
|
|
565
568
|
if file.path.segments == path.segments:
|