ommlds 0.0.0.dev448__py3-none-any.whl → 0.0.0.dev450__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.
- ommlds/.omlish-manifests.json +1 -1
- ommlds/backends/google/protocol/__init__.py +3 -0
- ommlds/backends/google/protocol/_marshal.py +16 -0
- ommlds/backends/google/protocol/types.py +303 -76
- ommlds/backends/mlx/generation.py +1 -1
- ommlds/cli/main.py +27 -6
- ommlds/cli/sessions/chat/code.py +114 -0
- ommlds/cli/sessions/chat/interactive.py +2 -5
- ommlds/cli/sessions/chat/printing.py +1 -4
- ommlds/cli/sessions/chat/prompt.py +8 -1
- ommlds/cli/sessions/chat/state.py +1 -0
- ommlds/cli/sessions/chat/tools.py +17 -7
- ommlds/cli/tools/config.py +1 -0
- ommlds/cli/tools/inject.py +11 -3
- ommlds/minichain/__init__.py +4 -0
- ommlds/minichain/backends/impls/google/chat.py +66 -11
- ommlds/minichain/backends/impls/google/tools.py +149 -0
- ommlds/minichain/lib/code/prompts.py +6 -0
- ommlds/minichain/lib/fs/binfiles.py +108 -0
- ommlds/minichain/lib/fs/context.py +112 -0
- ommlds/minichain/lib/fs/errors.py +95 -0
- ommlds/minichain/lib/fs/suggestions.py +36 -0
- ommlds/minichain/lib/fs/tools/__init__.py +0 -0
- ommlds/minichain/lib/fs/tools/ls.py +38 -0
- ommlds/minichain/lib/fs/tools/read.py +115 -0
- ommlds/minichain/lib/fs/tools/recursivels/__init__.py +0 -0
- ommlds/minichain/lib/fs/tools/recursivels/execution.py +40 -0
- ommlds/minichain/lib/todo/__init__.py +0 -0
- ommlds/minichain/lib/todo/context.py +27 -0
- ommlds/minichain/lib/todo/tools/__init__.py +0 -0
- ommlds/minichain/lib/todo/tools/read.py +39 -0
- ommlds/minichain/lib/todo/tools/write.py +275 -0
- ommlds/minichain/lib/todo/types.py +55 -0
- ommlds/minichain/tools/execution/context.py +34 -14
- ommlds/minichain/tools/execution/errors.py +15 -0
- ommlds/minichain/tools/execution/reflect.py +0 -3
- ommlds/minichain/tools/jsonschema.py +11 -1
- ommlds/minichain/tools/reflect.py +47 -15
- ommlds/minichain/tools/types.py +9 -0
- ommlds/minichain/utils.py +27 -0
- {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/METADATA +3 -3
- {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/RECORD +49 -29
- ommlds/minichain/lib/fs/ls/execution.py +0 -32
- /ommlds/minichain/lib/{fs/ls → code}/__init__.py +0 -0
- /ommlds/minichain/lib/fs/{ls → tools/recursivels}/rendering.py +0 -0
- /ommlds/minichain/lib/fs/{ls → tools/recursivels}/running.py +0 -0
- {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
IMAGE_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
9
|
+
'jpg',
|
|
10
|
+
'jpeg',
|
|
11
|
+
'png',
|
|
12
|
+
'gif',
|
|
13
|
+
'bmp',
|
|
14
|
+
'webp',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ARCHIVE_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
19
|
+
'zip',
|
|
20
|
+
'tar',
|
|
21
|
+
'gz',
|
|
22
|
+
'xz',
|
|
23
|
+
'7z',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DOC_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
28
|
+
'doc',
|
|
29
|
+
'docx',
|
|
30
|
+
'xls',
|
|
31
|
+
'xlsx',
|
|
32
|
+
'ppt',
|
|
33
|
+
'pptx',
|
|
34
|
+
'odt',
|
|
35
|
+
'ods',
|
|
36
|
+
'odp',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
EXECUTABLE_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
41
|
+
'exe',
|
|
42
|
+
'dll',
|
|
43
|
+
'so',
|
|
44
|
+
|
|
45
|
+
'obj',
|
|
46
|
+
'o',
|
|
47
|
+
'a',
|
|
48
|
+
'lib',
|
|
49
|
+
|
|
50
|
+
'class',
|
|
51
|
+
'jar',
|
|
52
|
+
'war',
|
|
53
|
+
|
|
54
|
+
'wasm',
|
|
55
|
+
|
|
56
|
+
'pyc',
|
|
57
|
+
'pyo',
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
BLOB_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
62
|
+
'bin',
|
|
63
|
+
'dat',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
BINARY_FILE_EXTENSIONS: ta.AbstractSet[str] = frozenset([
|
|
68
|
+
*IMAGE_FILE_EXTENSIONS,
|
|
69
|
+
*ARCHIVE_FILE_EXTENSIONS,
|
|
70
|
+
*DOC_FILE_EXTENSIONS,
|
|
71
|
+
*EXECUTABLE_FILE_EXTENSIONS,
|
|
72
|
+
*BLOB_FILE_EXTENSIONS,
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def has_binary_file_extension(file_path: str) -> bool:
|
|
77
|
+
return os.path.basename(file_path).partition('.')[-1] in BINARY_FILE_EXTENSIONS
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_binary_file(
|
|
84
|
+
file_path: str,
|
|
85
|
+
*,
|
|
86
|
+
chunk_size: int = 0x1000,
|
|
87
|
+
non_printable_cutoff: float = .3,
|
|
88
|
+
|
|
89
|
+
st: os.stat_result | None = None,
|
|
90
|
+
) -> bool:
|
|
91
|
+
if st is None:
|
|
92
|
+
try:
|
|
93
|
+
st = os.stat(file_path)
|
|
94
|
+
except OSError:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
if not st.st_size:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
with open(file_path, 'rb') as f:
|
|
101
|
+
chunk = f.read(chunk_size)
|
|
102
|
+
|
|
103
|
+
if 0 in chunk:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Count "non-printable" ASCII-ish control chars (excluding TAB/LF/CR)
|
|
107
|
+
np = sum(1 for b in chunk if b < 9 or (13 < b < 32))
|
|
108
|
+
return (np / len(chunk)) > non_printable_cutoff
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import stat
|
|
3
|
+
|
|
4
|
+
from omlish import check
|
|
5
|
+
|
|
6
|
+
from ...tools.execution.context import tool_context
|
|
7
|
+
from .binfiles import has_binary_file_extension
|
|
8
|
+
from .binfiles import is_binary_file
|
|
9
|
+
from .errors import RequestedPathDoesNotExistError
|
|
10
|
+
from .errors import RequestedPathOutsideRootDirError
|
|
11
|
+
from .errors import RequestedPathWrongTypeError
|
|
12
|
+
from .suggestions import get_path_suggestions
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FsContext:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
root_dir: str | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
self._root_dir = root_dir
|
|
27
|
+
self._abs_root_dir = os.path.abspath(root_dir) if root_dir is not None else None
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
|
|
31
|
+
def check_requested_path(self, req_path: str) -> None:
|
|
32
|
+
abs_req_path = os.path.abspath(req_path)
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
self._abs_root_dir is None or
|
|
36
|
+
not (
|
|
37
|
+
abs_req_path == self._abs_root_dir or
|
|
38
|
+
abs_req_path.startswith(self._abs_root_dir + os.path.sep)
|
|
39
|
+
)
|
|
40
|
+
):
|
|
41
|
+
raise RequestedPathOutsideRootDirError(
|
|
42
|
+
req_path,
|
|
43
|
+
root_dir=check.not_none(self._root_dir),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
#
|
|
47
|
+
|
|
48
|
+
def check_stat_dir(
|
|
49
|
+
self,
|
|
50
|
+
req_path: str,
|
|
51
|
+
) -> os.stat_result:
|
|
52
|
+
self.check_requested_path(req_path)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
st = os.stat(req_path)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
raise RequestedPathDoesNotExistError(
|
|
58
|
+
req_path,
|
|
59
|
+
suggested_paths=get_path_suggestions(
|
|
60
|
+
req_path,
|
|
61
|
+
filter=lambda e: e.is_dir(),
|
|
62
|
+
),
|
|
63
|
+
) from None
|
|
64
|
+
|
|
65
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
66
|
+
raise RequestedPathWrongTypeError(
|
|
67
|
+
req_path,
|
|
68
|
+
expected_type='dir',
|
|
69
|
+
**(dict(actual_type='file') if stat.S_ISREG(st.st_mode) else {}),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return st
|
|
73
|
+
|
|
74
|
+
def check_stat_file(
|
|
75
|
+
self,
|
|
76
|
+
req_path: str,
|
|
77
|
+
*,
|
|
78
|
+
text: bool = False,
|
|
79
|
+
) -> os.stat_result:
|
|
80
|
+
self.check_requested_path(req_path)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
st = os.stat(req_path)
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
raise RequestedPathDoesNotExistError(
|
|
86
|
+
req_path,
|
|
87
|
+
suggested_paths=get_path_suggestions(
|
|
88
|
+
req_path,
|
|
89
|
+
filter=lambda e: (e.is_file() and not (text and has_binary_file_extension(e.name))),
|
|
90
|
+
),
|
|
91
|
+
) from None
|
|
92
|
+
|
|
93
|
+
if not stat.S_ISREG(st.st_mode):
|
|
94
|
+
is_dir = stat.S_ISDIR(st.st_mode)
|
|
95
|
+
raise RequestedPathWrongTypeError(
|
|
96
|
+
req_path,
|
|
97
|
+
expected_type='file',
|
|
98
|
+
**(dict(actual_type='dir') if is_dir else {}),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if text and is_binary_file(req_path, st=st):
|
|
102
|
+
raise RequestedPathWrongTypeError(
|
|
103
|
+
req_path,
|
|
104
|
+
expected_type='text file',
|
|
105
|
+
actual_type='binary file',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return st
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def fs_tool_context() -> FsContext:
|
|
112
|
+
return tool_context()[FsContext]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import typing as ta
|
|
2
|
+
|
|
3
|
+
from omlish import lang
|
|
4
|
+
|
|
5
|
+
from ...tools.execution.errors import ToolExecutionError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FsToolExecutionError(ToolExecutionError, lang.Abstract):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RequestedPathError(FsToolExecutionError, lang.Abstract):
|
|
19
|
+
def __init__(self, requested_path: str, *args: ta.Any) -> None:
|
|
20
|
+
super().__init__(requested_path, *args)
|
|
21
|
+
|
|
22
|
+
self.requested_path = requested_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RequestedPathMustBeAbsoluteError(RequestedPathError):
|
|
26
|
+
@property
|
|
27
|
+
def content(self) -> str:
|
|
28
|
+
return f'Requested path {self.requested_path!r} must be absolute.'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RequestedPathOutsideRootDirError(RequestedPathError):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
requested_path: str,
|
|
35
|
+
*,
|
|
36
|
+
root_dir: str,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(
|
|
39
|
+
requested_path,
|
|
40
|
+
root_dir,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.root_dir = root_dir
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content(self) -> str:
|
|
47
|
+
return f'Requested path {self.requested_path!r} was outside of permitted root directory {self.root_dir!r}.'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RequestedPathWrongTypeError(RequestedPathError):
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
requested_path: str,
|
|
54
|
+
*,
|
|
55
|
+
expected_type: str,
|
|
56
|
+
actual_type: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
super().__init__(
|
|
59
|
+
requested_path,
|
|
60
|
+
expected_type,
|
|
61
|
+
actual_type,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.expected_type = expected_type
|
|
65
|
+
self.actual_type = actual_type
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def content(self) -> str:
|
|
69
|
+
return ''.join([
|
|
70
|
+
f'Requested path {self.requested_path!r} must be of type {self.expected_type!r}',
|
|
71
|
+
*([f', but it is actually of type {self.actual_type!r}'] if self.actual_type is not None else []),
|
|
72
|
+
'.',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RequestedPathDoesNotExistError(RequestedPathError):
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
requested_path: str,
|
|
80
|
+
*,
|
|
81
|
+
suggested_paths: ta.Sequence[str] | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
super().__init__(
|
|
84
|
+
requested_path,
|
|
85
|
+
suggested_paths,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.suggested_paths = suggested_paths
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def content(self) -> str:
|
|
92
|
+
return ''.join([
|
|
93
|
+
f'Requested path {self.requested_path!r} does not exist.',
|
|
94
|
+
*([f' Did you mean one of these valid paths: {self.suggested_paths!r}?'] if self.suggested_paths else []),
|
|
95
|
+
])
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import os.path
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_path_suggestions(
|
|
10
|
+
bad_path: str,
|
|
11
|
+
n: int = 3,
|
|
12
|
+
*,
|
|
13
|
+
filter: ta.Callable[[os.DirEntry], bool] | None = None, # noqa
|
|
14
|
+
cutoff: float = .6,
|
|
15
|
+
) -> ta.Sequence[str] | None:
|
|
16
|
+
dn = os.path.dirname(bad_path)
|
|
17
|
+
try:
|
|
18
|
+
sdi = os.scandir(dn)
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
fl = [
|
|
23
|
+
e.name
|
|
24
|
+
for e in sdi
|
|
25
|
+
if (filter is None or filter(e))
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
os.path.join(dn, sn)
|
|
30
|
+
for sn in difflib.get_close_matches(
|
|
31
|
+
os.path.basename(bad_path),
|
|
32
|
+
fl,
|
|
33
|
+
n,
|
|
34
|
+
cutoff=cutoff,
|
|
35
|
+
)
|
|
36
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from omlish import lang
|
|
5
|
+
|
|
6
|
+
from ....tools.execution.catalog import ToolCatalogEntry
|
|
7
|
+
from ....tools.execution.reflect import reflect_tool_catalog_entry
|
|
8
|
+
from ..context import fs_tool_context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute_ls_tool(
|
|
15
|
+
dir_path: str,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Lists the contents of the specified dir.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
dir_path: The dir to list the contents of. Must be an absolute path.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ctx = fs_tool_context()
|
|
25
|
+
ctx.check_stat_dir(dir_path)
|
|
26
|
+
|
|
27
|
+
out = io.StringIO()
|
|
28
|
+
out.write('<dir>\n')
|
|
29
|
+
for e in sorted(os.scandir(dir_path), key=lambda e: e.name): # noqa
|
|
30
|
+
out.write(f'{e.name}{"/" if e.is_dir() else ""}\n')
|
|
31
|
+
out.write('</dir>\n')
|
|
32
|
+
|
|
33
|
+
return out.getvalue()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@lang.cached_function
|
|
37
|
+
def ls_tool() -> ToolCatalogEntry:
|
|
38
|
+
return reflect_tool_catalog_entry(execute_ls_tool)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- better bad unicode handling
|
|
4
|
+
- read whole file if < some size, report filesize / num lines / mtime inline
|
|
5
|
+
- fs cache
|
|
6
|
+
- track changes
|
|
7
|
+
"""
|
|
8
|
+
import io
|
|
9
|
+
import itertools
|
|
10
|
+
|
|
11
|
+
from omlish import lang
|
|
12
|
+
|
|
13
|
+
from ....tools.execution.catalog import ToolCatalogEntry
|
|
14
|
+
from ....tools.execution.reflect import reflect_tool_catalog_entry
|
|
15
|
+
from ....tools.reflect import tool_spec_override
|
|
16
|
+
from ....tools.types import ToolParam
|
|
17
|
+
from ..context import fs_tool_context
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_MAX_NUM_LINES = 2_000
|
|
24
|
+
|
|
25
|
+
MAX_LINE_LENGTH = 2_000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@tool_spec_override(
|
|
29
|
+
desc=f"""
|
|
30
|
+
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
31
|
+
|
|
32
|
+
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that
|
|
33
|
+
path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
- The file_path parameter must be an absolute path, not a relative path.
|
|
37
|
+
- By default, it reads up to {DEFAULT_MAX_NUM_LINES} lines starting from the beginning of the file.
|
|
38
|
+
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to
|
|
39
|
+
read the whole file by not providing these parameters.
|
|
40
|
+
- Any lines longer than {MAX_LINE_LENGTH} characters will be truncated with "...".
|
|
41
|
+
- Invalid unicode characters will be replaced with the unicode replacement character "\\ufffd".
|
|
42
|
+
- Results are returned using cat -n format, with line numbers starting at 1 and suffixed with a pipe character
|
|
43
|
+
"|".
|
|
44
|
+
- This tool cannot read binary files, including images.
|
|
45
|
+
""",
|
|
46
|
+
params=[
|
|
47
|
+
ToolParam(
|
|
48
|
+
'file_path',
|
|
49
|
+
desc='The absolute path to the file to read.',
|
|
50
|
+
),
|
|
51
|
+
ToolParam(
|
|
52
|
+
'line_offset',
|
|
53
|
+
desc='The line number to start reading from (0-based).',
|
|
54
|
+
),
|
|
55
|
+
ToolParam(
|
|
56
|
+
'num_lines',
|
|
57
|
+
desc=f'The number of lines to read (defaults to {DEFAULT_MAX_NUM_LINES}).',
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
def execute_read_tool(
|
|
62
|
+
file_path: str,
|
|
63
|
+
*,
|
|
64
|
+
line_offset: int = 0,
|
|
65
|
+
num_lines: int = DEFAULT_MAX_NUM_LINES,
|
|
66
|
+
) -> str:
|
|
67
|
+
ctx = fs_tool_context()
|
|
68
|
+
ctx.check_stat_file(file_path, text=True)
|
|
69
|
+
|
|
70
|
+
out = io.StringIO()
|
|
71
|
+
out.write('<file>\n')
|
|
72
|
+
|
|
73
|
+
zp = len(str(line_offset + num_lines))
|
|
74
|
+
n = line_offset
|
|
75
|
+
has_trunc = False # noqa
|
|
76
|
+
with open(file_path, errors='replace') as f:
|
|
77
|
+
fi = iter(f)
|
|
78
|
+
|
|
79
|
+
for line in itertools.islice(fi, line_offset, line_offset + num_lines):
|
|
80
|
+
out.write(f'{str(n + 1).zfill(zp):}|')
|
|
81
|
+
line = line.removesuffix('\n')
|
|
82
|
+
if len(line) > MAX_LINE_LENGTH:
|
|
83
|
+
has_trunc = True # noqa
|
|
84
|
+
out.write(line[:MAX_LINE_LENGTH])
|
|
85
|
+
out.write('...')
|
|
86
|
+
else:
|
|
87
|
+
out.write(line)
|
|
88
|
+
out.write('\n')
|
|
89
|
+
n += 1
|
|
90
|
+
|
|
91
|
+
# tl = n
|
|
92
|
+
# if (ml := lang.ilen(fi)):
|
|
93
|
+
# check.state(n == num_lines)
|
|
94
|
+
# tl += ml
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
next(fi)
|
|
98
|
+
except StopIteration:
|
|
99
|
+
has_more = False
|
|
100
|
+
else:
|
|
101
|
+
has_more = True
|
|
102
|
+
|
|
103
|
+
out.write(f'</file>\n')
|
|
104
|
+
|
|
105
|
+
if has_more:
|
|
106
|
+
out.write(
|
|
107
|
+
f'\n(File has more lines. Use "line_offset" parameter to read beyond line {line_offset + num_lines}.)\n',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return out.getvalue()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@lang.cached_function
|
|
114
|
+
def read_tool() -> ToolCatalogEntry:
|
|
115
|
+
return reflect_tool_catalog_entry(execute_read_tool)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from omlish import lang
|
|
2
|
+
|
|
3
|
+
from .....tools.execution.catalog import ToolCatalogEntry
|
|
4
|
+
from .....tools.execution.reflect import reflect_tool_catalog_entry
|
|
5
|
+
from ...context import fs_tool_context
|
|
6
|
+
from .rendering import LsLinesRenderer
|
|
7
|
+
from .running import LsRunner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute_recursive_ls_tool(
|
|
14
|
+
base_path: str,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Recursively lists the directory contents of the given base path.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
base_path: The path of the directory to list the contents of. Must be absolute.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A formatted string of the recursive directory contents.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ft_ctx = fs_tool_context()
|
|
27
|
+
ft_ctx.check_requested_path(base_path)
|
|
28
|
+
|
|
29
|
+
root = LsRunner().run(base_path)
|
|
30
|
+
lines = LsLinesRenderer().render(root)
|
|
31
|
+
return '\n'.join([
|
|
32
|
+
'<dir>',
|
|
33
|
+
*lines.lines,
|
|
34
|
+
'</dir>',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lang.cached_function
|
|
39
|
+
def recursive_ls_tool() -> ToolCatalogEntry:
|
|
40
|
+
return reflect_tool_catalog_entry(execute_recursive_ls_tool)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typing as ta
|
|
2
|
+
|
|
3
|
+
from ...tools.execution.context import tool_context
|
|
4
|
+
from .types import TodoItem
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TodoContext:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
items: ta.Sequence[TodoItem] | None = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
|
|
17
|
+
self._items = items
|
|
18
|
+
|
|
19
|
+
def get_items(self) -> ta.Sequence[TodoItem] | None:
|
|
20
|
+
return self._items
|
|
21
|
+
|
|
22
|
+
def set_items(self, items: ta.Sequence[TodoItem] | None) -> None:
|
|
23
|
+
self._items = list(items) if items is not None else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def todo_tool_context() -> TodoContext:
|
|
27
|
+
return tool_context()[TodoContext]
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from omlish import lang
|
|
2
|
+
from omlish.formats import json
|
|
3
|
+
|
|
4
|
+
from ....tools.execution.catalog import ToolCatalogEntry
|
|
5
|
+
from ....tools.execution.reflect import reflect_tool_catalog_entry
|
|
6
|
+
from ....tools.reflect import tool_spec_override
|
|
7
|
+
from ..context import todo_tool_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@tool_spec_override(
|
|
14
|
+
desc="""
|
|
15
|
+
Use this tool to read the current todo list for your current session. This tool should be used proactively and
|
|
16
|
+
frequently to ensure that you are aware of the status of the current subtask list.
|
|
17
|
+
|
|
18
|
+
You should make use of this tool often, especially in the following situations:
|
|
19
|
+
- At the beginning of conversations to see what's pending.
|
|
20
|
+
- Before starting new tasks to prioritize work.
|
|
21
|
+
- When the user asks about previous tasks or plans.
|
|
22
|
+
- Whenever you're uncertain about what to do next.
|
|
23
|
+
- After completing tasks to update your understanding of remaining work.
|
|
24
|
+
- After every few messages to ensure you're on track.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
- Returns a list of todo items in json format with their id, status, priority, and content.
|
|
28
|
+
- Use this information to track progress and plan next steps.
|
|
29
|
+
""",
|
|
30
|
+
)
|
|
31
|
+
def execute_todo_read_tool() -> str:
|
|
32
|
+
ctx = todo_tool_context()
|
|
33
|
+
|
|
34
|
+
return json.dumps_compact(ctx.get_items() or [])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lang.cached_function
|
|
38
|
+
def todo_read_tool() -> ToolCatalogEntry:
|
|
39
|
+
return reflect_tool_catalog_entry(execute_todo_read_tool)
|