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.
Files changed (50) hide show
  1. ommlds/.omlish-manifests.json +1 -1
  2. ommlds/backends/google/protocol/__init__.py +3 -0
  3. ommlds/backends/google/protocol/_marshal.py +16 -0
  4. ommlds/backends/google/protocol/types.py +303 -76
  5. ommlds/backends/mlx/generation.py +1 -1
  6. ommlds/cli/main.py +27 -6
  7. ommlds/cli/sessions/chat/code.py +114 -0
  8. ommlds/cli/sessions/chat/interactive.py +2 -5
  9. ommlds/cli/sessions/chat/printing.py +1 -4
  10. ommlds/cli/sessions/chat/prompt.py +8 -1
  11. ommlds/cli/sessions/chat/state.py +1 -0
  12. ommlds/cli/sessions/chat/tools.py +17 -7
  13. ommlds/cli/tools/config.py +1 -0
  14. ommlds/cli/tools/inject.py +11 -3
  15. ommlds/minichain/__init__.py +4 -0
  16. ommlds/minichain/backends/impls/google/chat.py +66 -11
  17. ommlds/minichain/backends/impls/google/tools.py +149 -0
  18. ommlds/minichain/lib/code/prompts.py +6 -0
  19. ommlds/minichain/lib/fs/binfiles.py +108 -0
  20. ommlds/minichain/lib/fs/context.py +112 -0
  21. ommlds/minichain/lib/fs/errors.py +95 -0
  22. ommlds/minichain/lib/fs/suggestions.py +36 -0
  23. ommlds/minichain/lib/fs/tools/__init__.py +0 -0
  24. ommlds/minichain/lib/fs/tools/ls.py +38 -0
  25. ommlds/minichain/lib/fs/tools/read.py +115 -0
  26. ommlds/minichain/lib/fs/tools/recursivels/__init__.py +0 -0
  27. ommlds/minichain/lib/fs/tools/recursivels/execution.py +40 -0
  28. ommlds/minichain/lib/todo/__init__.py +0 -0
  29. ommlds/minichain/lib/todo/context.py +27 -0
  30. ommlds/minichain/lib/todo/tools/__init__.py +0 -0
  31. ommlds/minichain/lib/todo/tools/read.py +39 -0
  32. ommlds/minichain/lib/todo/tools/write.py +275 -0
  33. ommlds/minichain/lib/todo/types.py +55 -0
  34. ommlds/minichain/tools/execution/context.py +34 -14
  35. ommlds/minichain/tools/execution/errors.py +15 -0
  36. ommlds/minichain/tools/execution/reflect.py +0 -3
  37. ommlds/minichain/tools/jsonschema.py +11 -1
  38. ommlds/minichain/tools/reflect.py +47 -15
  39. ommlds/minichain/tools/types.py +9 -0
  40. ommlds/minichain/utils.py +27 -0
  41. {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/METADATA +3 -3
  42. {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/RECORD +49 -29
  43. ommlds/minichain/lib/fs/ls/execution.py +0 -32
  44. /ommlds/minichain/lib/{fs/ls → code}/__init__.py +0 -0
  45. /ommlds/minichain/lib/fs/{ls → tools/recursivels}/rendering.py +0 -0
  46. /ommlds/minichain/lib/fs/{ls → tools/recursivels}/running.py +0 -0
  47. {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/WHEEL +0 -0
  48. {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/entry_points.txt +0 -0
  49. {ommlds-0.0.0.dev448.dist-info → ommlds-0.0.0.dev450.dist-info}/licenses/LICENSE +0 -0
  50. {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)