ommlds 0.0.0.dev449__py3-none-any.whl → 0.0.0.dev451__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 ommlds might be problematic. Click here for more details.

Files changed (67) hide show
  1. ommlds/.omlish-manifests.json +3 -3
  2. ommlds/backends/anthropic/protocol/_marshal.py +1 -1
  3. ommlds/backends/anthropic/protocol/sse/_marshal.py +1 -1
  4. ommlds/backends/anthropic/protocol/sse/assemble.py +1 -1
  5. ommlds/backends/anthropic/protocol/types.py +30 -9
  6. ommlds/backends/google/protocol/__init__.py +3 -0
  7. ommlds/backends/google/protocol/_marshal.py +16 -0
  8. ommlds/backends/google/protocol/types.py +303 -76
  9. ommlds/backends/mlx/generation.py +1 -1
  10. ommlds/backends/openai/protocol/_marshal.py +1 -1
  11. ommlds/cli/main.py +29 -8
  12. ommlds/cli/sessions/chat/code.py +124 -0
  13. ommlds/cli/sessions/chat/interactive.py +2 -5
  14. ommlds/cli/sessions/chat/printing.py +5 -4
  15. ommlds/cli/sessions/chat/prompt.py +2 -2
  16. ommlds/cli/sessions/chat/state.py +1 -0
  17. ommlds/cli/sessions/chat/tools.py +3 -5
  18. ommlds/cli/tools/config.py +2 -1
  19. ommlds/cli/tools/inject.py +13 -3
  20. ommlds/minichain/__init__.py +12 -0
  21. ommlds/minichain/_marshal.py +39 -0
  22. ommlds/minichain/backends/impls/anthropic/chat.py +78 -10
  23. ommlds/minichain/backends/impls/google/chat.py +95 -12
  24. ommlds/minichain/backends/impls/google/tools.py +149 -0
  25. ommlds/minichain/chat/_marshal.py +1 -1
  26. ommlds/minichain/content/_marshal.py +24 -3
  27. ommlds/minichain/content/json.py +13 -0
  28. ommlds/minichain/content/materialize.py +13 -20
  29. ommlds/minichain/content/prepare.py +4 -0
  30. ommlds/minichain/json.py +20 -0
  31. ommlds/minichain/lib/code/prompts.py +6 -0
  32. ommlds/minichain/lib/fs/context.py +18 -4
  33. ommlds/minichain/lib/fs/errors.py +6 -0
  34. ommlds/minichain/lib/fs/tools/edit.py +104 -0
  35. ommlds/minichain/lib/fs/{catalog → tools}/ls.py +3 -3
  36. ommlds/minichain/lib/fs/{catalog → tools}/read.py +6 -6
  37. ommlds/minichain/lib/fs/tools/recursivels/__init__.py +0 -0
  38. ommlds/minichain/lib/fs/{catalog → tools}/recursivels/execution.py +2 -2
  39. ommlds/minichain/lib/todo/__init__.py +0 -0
  40. ommlds/minichain/lib/todo/context.py +54 -0
  41. ommlds/minichain/lib/todo/tools/__init__.py +0 -0
  42. ommlds/minichain/lib/todo/tools/read.py +44 -0
  43. ommlds/minichain/lib/todo/tools/write.py +335 -0
  44. ommlds/minichain/lib/todo/types.py +60 -0
  45. ommlds/minichain/llms/_marshal.py +1 -1
  46. ommlds/minichain/services/_marshal.py +1 -1
  47. ommlds/minichain/tools/_marshal.py +1 -1
  48. ommlds/minichain/tools/execution/catalog.py +2 -1
  49. ommlds/minichain/tools/execution/executors.py +8 -3
  50. ommlds/minichain/tools/execution/reflect.py +43 -5
  51. ommlds/minichain/tools/fns.py +46 -9
  52. ommlds/minichain/tools/jsonschema.py +11 -1
  53. ommlds/minichain/tools/reflect.py +9 -2
  54. ommlds/minichain/tools/types.py +9 -0
  55. ommlds/minichain/utils.py +27 -0
  56. ommlds/minichain/vectors/_marshal.py +1 -1
  57. ommlds/tools/ocr.py +7 -1
  58. {ommlds-0.0.0.dev449.dist-info → ommlds-0.0.0.dev451.dist-info}/METADATA +3 -3
  59. {ommlds-0.0.0.dev449.dist-info → ommlds-0.0.0.dev451.dist-info}/RECORD +67 -53
  60. /ommlds/minichain/lib/{fs/catalog → code}/__init__.py +0 -0
  61. /ommlds/minichain/lib/fs/{catalog/recursivels → tools}/__init__.py +0 -0
  62. /ommlds/minichain/lib/fs/{catalog → tools}/recursivels/rendering.py +0 -0
  63. /ommlds/minichain/lib/fs/{catalog → tools}/recursivels/running.py +0 -0
  64. {ommlds-0.0.0.dev449.dist-info → ommlds-0.0.0.dev451.dist-info}/WHEEL +0 -0
  65. {ommlds-0.0.0.dev449.dist-info → ommlds-0.0.0.dev451.dist-info}/entry_points.txt +0 -0
  66. {ommlds-0.0.0.dev449.dist-info → ommlds-0.0.0.dev451.dist-info}/licenses/LICENSE +0 -0
  67. {ommlds-0.0.0.dev449.dist-info → ommlds-0.0.0.dev451.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,149 @@
1
+ """
2
+ https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models
3
+ """
4
+ import typing as ta
5
+
6
+ from omlish import check
7
+ from omlish import dataclasses as dc
8
+
9
+ from .....backends.google.protocol import types as pt
10
+ from ....content.prepare import ContentStrPreparer
11
+ from ....content.prepare import default_content_str_preparer
12
+ from ....tools.types import EnumToolDtype
13
+ from ....tools.types import MappingToolDtype
14
+ from ....tools.types import NullableToolDtype
15
+ from ....tools.types import ObjectToolDtype
16
+ from ....tools.types import PrimitiveToolDtype
17
+ from ....tools.types import SequenceToolDtype
18
+ from ....tools.types import ToolDtype
19
+ from ....tools.types import ToolSpec
20
+ from ....tools.types import TupleToolDtype
21
+ from ....tools.types import UnionToolDtype
22
+
23
+
24
+ ##
25
+
26
+
27
+ def _shallow_dc_asdict_not_none(o: ta.Any) -> dict[str, ta.Any]:
28
+ return {k: v for k, v in dc.shallow_asdict(o).items() if v is not None}
29
+
30
+
31
+ PT_TYPE_BY_PRIMITIVE_TYPE: ta.Mapping[str, pt.Type] = {
32
+ 'string': 'STRING',
33
+ 'number': 'NUMBER',
34
+ 'integer': 'INTEGER',
35
+ 'boolean': 'BOOLEAN',
36
+ 'array': 'ARRAY',
37
+ 'null': 'NULL',
38
+ }
39
+
40
+
41
+ class ToolSchemaRenderer:
42
+ def __init__(
43
+ self,
44
+ *,
45
+ content_str_preparer: ContentStrPreparer | None = None,
46
+ ) -> None:
47
+ super().__init__()
48
+
49
+ if content_str_preparer is None:
50
+ content_str_preparer = default_content_str_preparer()
51
+ self._content_str_preparer = content_str_preparer
52
+
53
+ def render_type(self, t: ToolDtype) -> pt.Schema:
54
+ if isinstance(t, PrimitiveToolDtype):
55
+ return pt.Schema(type=PT_TYPE_BY_PRIMITIVE_TYPE[t.type])
56
+
57
+ if isinstance(t, UnionToolDtype):
58
+ return pt.Schema(
59
+ any_of=[self.render_type(a) for a in t.args],
60
+ )
61
+
62
+ if isinstance(t, NullableToolDtype):
63
+ return pt.Schema(**{
64
+ **_shallow_dc_asdict_not_none(self.render_type(t.type)),
65
+ **dict(nullable=True),
66
+ })
67
+
68
+ if isinstance(t, SequenceToolDtype):
69
+ return pt.Schema(
70
+ type='ARRAY',
71
+ items=self.render_type(t.element),
72
+ )
73
+
74
+ if isinstance(t, MappingToolDtype):
75
+ # FIXME: t.key
76
+ # return {
77
+ # 'type': 'object',
78
+ # 'additionalProperties': self.render_type(t.value),
79
+ # }
80
+ raise NotImplementedError
81
+
82
+ if isinstance(t, TupleToolDtype):
83
+ # return {
84
+ # 'type': 'array',
85
+ # 'prefixItems': [self.render_type(e) for e in t.elements],
86
+ # }
87
+ raise NotImplementedError
88
+
89
+ if isinstance(t, EnumToolDtype):
90
+ return pt.Schema(**{
91
+ **_shallow_dc_asdict_not_none(self.render_type(t.type)),
92
+ **dict(enum=list(t.values)),
93
+ })
94
+
95
+ if isinstance(t, ObjectToolDtype):
96
+ return pt.Schema(
97
+ type='OBJECT',
98
+ properties={
99
+ k: self.render_type(v)
100
+ for k, v in t.fields.items()
101
+ },
102
+ )
103
+
104
+ raise TypeError(t)
105
+
106
+ def render_tool_params(self, ts: ToolSpec) -> pt.Schema:
107
+ pr_dct: dict[str, pt.Schema] | None = None
108
+ req_lst: list[str] | None = None
109
+ if ts.params is not None:
110
+ pr_dct = {}
111
+ req_lst = []
112
+ for p in ts.params or []:
113
+ pr_dct[check.non_empty_str(p.name)] = pt.Schema(**{
114
+ **(dict(description=self._content_str_preparer.prepare_str(p.desc)) if p.desc is not None else {}),
115
+ **(_shallow_dc_asdict_not_none(self.render_type(p.type)) if p.type is not None else {}),
116
+ })
117
+ if p.required:
118
+ req_lst.append(check.non_empty_str(p.name))
119
+
120
+ return pt.Schema(
121
+ type='OBJECT',
122
+ **(dict(properties=pr_dct) if pr_dct is not None else {}), # type: ignore[arg-type]
123
+ **(dict(required=req_lst) if req_lst is not None else {}), # type: ignore[arg-type]
124
+ )
125
+
126
+ def render_tool(self, ts: ToolSpec) -> pt.FunctionDeclaration:
127
+ ret_dct = {
128
+ **(dict(description=self._content_str_preparer.prepare_str(ts.returns_desc)) if ts.returns_desc is not None else {}), # noqa
129
+ **(_shallow_dc_asdict_not_none(self.render_type(ts.returns_type)) if ts.returns_type is not None else {}),
130
+ }
131
+
132
+ return pt.FunctionDeclaration(
133
+ name=check.non_empty_str(ts.name),
134
+ description=self._content_str_preparer.prepare_str(ts.desc) if ts.desc is not None else None, # type: ignore[arg-type] # noqa
135
+ behavior='BLOCKING',
136
+ parameters=self.render_tool_params(ts) if ts.params else None,
137
+ response=(pt.Schema(**ret_dct) if ret_dct else None),
138
+ )
139
+
140
+
141
+ ##
142
+
143
+
144
+ def build_tool_spec_schema(ts: ToolSpec) -> pt.FunctionDeclaration:
145
+ return ToolSchemaRenderer().render_tool(ts)
146
+
147
+
148
+ def build_tool_spec_params_schema(ts: ToolSpec) -> pt.Schema:
149
+ return ToolSchemaRenderer().render_tool_params(ts)
@@ -12,7 +12,7 @@ from .messages import Message
12
12
 
13
13
 
14
14
  @lang.static_init
15
- def _install_standard_marshalling() -> None:
15
+ def _install_standard_marshaling() -> None:
16
16
  msgs_poly = msh.polymorphism_from_subclasses(
17
17
  Message,
18
18
  naming=msh.Naming.SNAKE,
@@ -9,6 +9,7 @@ from omlish import reflect as rfl
9
9
  from omlish.funcs import match as mfs
10
10
 
11
11
  from .images import ImageContent # noqa
12
+ from .json import JsonContent # noqa
12
13
  from .materialize import CanContent
13
14
  from .materialize import _InnerCanContent
14
15
  from .sequence import BlockContent # noqa
@@ -139,28 +140,48 @@ class _ImageContentUnmarshaler(msh.Unmarshaler):
139
140
  ##
140
141
 
141
142
 
143
+ class _JsonContentMarshaler(msh.Marshaler):
144
+ def marshal(self, ctx: msh.MarshalContext, o: ta.Any) -> msh.Value:
145
+ return ta.cast(msh.Value, check.isinstance(o, JsonContent).v)
146
+
147
+
148
+ class _JsonContentUnmarshaler(msh.Unmarshaler):
149
+ def unmarshal(self, ctx: msh.UnmarshalContext, v: msh.Value) -> ta.Any:
150
+ return JsonContent(v)
151
+
152
+
153
+ ##
154
+
155
+
142
156
  @lang.static_init
143
- def _install_standard_marshalling() -> None:
157
+ def _install_standard_marshaling() -> None:
144
158
  extended_content_poly = msh.Polymorphism(
145
159
  ExtendedContent,
146
160
  [
147
161
  msh.Impl(InlineContent, 'inline'),
148
162
  msh.Impl(BlockContent, 'block'),
149
163
  msh.Impl(ImageContent, 'image'),
164
+ msh.Impl(JsonContent, 'json'),
150
165
  msh.Impl(TextContent, 'text'),
151
166
  ],
152
167
  )
153
168
 
154
169
  msh.install_standard_factories(
155
170
  msh.PolymorphismMarshalerFactory(extended_content_poly),
156
- msh.TypeMapMarshalerFactory({ImageContent: _ImageContentMarshaler()}),
171
+ msh.TypeMapMarshalerFactory({
172
+ ImageContent: _ImageContentMarshaler(),
173
+ JsonContent: _JsonContentMarshaler(),
174
+ }),
157
175
  _ContentMarshalerFactory(),
158
176
  _CanContentMarshalerFactory(),
159
177
  )
160
178
 
161
179
  msh.install_standard_factories(
162
180
  msh.PolymorphismUnmarshalerFactory(extended_content_poly),
163
- msh.TypeMapUnmarshalerFactory({ImageContent: _ImageContentUnmarshaler()}),
181
+ msh.TypeMapUnmarshalerFactory({
182
+ ImageContent: _ImageContentUnmarshaler(),
183
+ JsonContent: _JsonContentUnmarshaler(),
184
+ }),
164
185
  _ContentUnmarshalerFactory(),
165
186
  _CanContentUnmarshalerFactory(),
166
187
  )
@@ -0,0 +1,13 @@
1
+ from omlish import dataclasses as dc
2
+ from omlish import lang
3
+
4
+ from ..json import JsonValue
5
+ from .simple import SimpleSingleExtendedContent
6
+
7
+
8
+ ##
9
+
10
+
11
+ @dc.dataclass(frozen=True)
12
+ class JsonContent(SimpleSingleExtendedContent, lang.Final):
13
+ v: JsonValue
@@ -130,7 +130,13 @@ class ContentMaterializer:
130
130
 
131
131
  @_materialize.register
132
132
  def _materialize_iterable(self, o: ta.Iterable) -> Content:
133
- return [self.materialize(e) for e in o]
133
+ # `collections.abc.Iterable` appears as a virtual base in the dispatch c3.mro for ContentNamespace before `type`
134
+ # does (due to NamespaceMeta having `__iter__`), so handle that here too.
135
+ if isinstance(o, type) and issubclass(o, ContentNamespace):
136
+ return self._materialize_namespace_type(o)
137
+
138
+ else:
139
+ return [self.materialize(e) for e in o]
134
140
 
135
141
  @_materialize.register
136
142
  def _materialize_none(self, o: None) -> Content:
@@ -151,25 +157,12 @@ class ContentMaterializer:
151
157
  def _materialize_namespace_type(self, o: type[ContentNamespace]) -> Content:
152
158
  check.issubclass(o, ContentNamespace)
153
159
 
154
- def rec(v: ta.Any) -> ta.Generator[Content]:
155
- if isinstance(v, (bytes, bytearray, ta.Mapping)):
156
- return
157
-
158
- elif isinstance(v, type):
159
- if issubclass(v, ContentNamespace):
160
- for n, e in v:
161
- if n.startswith('_'):
162
- continue
163
- yield from rec(e)
164
-
165
- elif isinstance(v, ta.Iterable):
166
- for e in v:
167
- yield from rec(e)
168
-
169
- else:
170
- yield self.materialize(v)
171
-
172
- return list(rec(o))
160
+ out: list[Content] = []
161
+ for n, e in o:
162
+ if n.startswith('_'):
163
+ continue
164
+ out.append(self.materialize(e))
165
+ return out
173
166
 
174
167
  #
175
168
 
@@ -1,3 +1,7 @@
1
+ """
2
+ TODO:
3
+ - re-softwrap
4
+ """
1
5
  import abc
2
6
  import dataclasses as dc
3
7
  import typing as ta
ommlds/minichain/json.py CHANGED
@@ -1,3 +1,4 @@
1
+ # ruff: noqa: UP007
1
2
  import typing as ta
2
3
 
3
4
  from omlish import dataclasses as dc
@@ -11,3 +12,22 @@ from omlish import lang
11
12
  class JsonSchema(lang.Final):
12
13
  name: str
13
14
  root: ta.Any
15
+
16
+
17
+ ##
18
+
19
+
20
+ JsonValue: ta.TypeAlias = ta.Union[
21
+ ta.Mapping[str, 'JsonValue'],
22
+
23
+ ta.Sequence['JsonValue'],
24
+
25
+ str,
26
+
27
+ int,
28
+ float,
29
+
30
+ bool,
31
+
32
+ None,
33
+ ]
@@ -0,0 +1,6 @@
1
+ CODE_AGENT_SYSTEM_PROMPT = """
2
+ You are an interactive assistant specializing in programming tasks.
3
+
4
+ Your goal is to assist the user by accomplishing the tasks and answering the questions given to you by the user using
5
+ your available tools.
6
+ """
@@ -8,6 +8,7 @@ from .binfiles import has_binary_file_extension
8
8
  from .binfiles import is_binary_file
9
9
  from .errors import RequestedPathDoesNotExistError
10
10
  from .errors import RequestedPathOutsideRootDirError
11
+ from .errors import RequestedPathWriteNotPermittedError
11
12
  from .errors import RequestedPathWrongTypeError
12
13
  from .suggestions import get_path_suggestions
13
14
 
@@ -15,19 +16,28 @@ from .suggestions import get_path_suggestions
15
16
  ##
16
17
 
17
18
 
18
- class FsToolContext:
19
+ class FsContext:
19
20
  def __init__(
20
21
  self,
21
22
  *,
22
23
  root_dir: str | None = None,
24
+ writes_permitted: bool = False,
23
25
  ) -> None:
24
26
  super().__init__()
25
27
 
26
28
  self._root_dir = root_dir
29
+ self._writes_permitted = writes_permitted
30
+
27
31
  self._abs_root_dir = os.path.abspath(root_dir) if root_dir is not None else None
28
32
 
29
33
  #
30
34
 
35
+ @property
36
+ def writes_permitted(self) -> bool:
37
+ return self._writes_permitted
38
+
39
+ #
40
+
31
41
  def check_requested_path(self, req_path: str) -> None:
32
42
  abs_req_path = os.path.abspath(req_path)
33
43
 
@@ -76,6 +86,7 @@ class FsToolContext:
76
86
  req_path: str,
77
87
  *,
78
88
  text: bool = False,
89
+ write: bool = False,
79
90
  ) -> os.stat_result:
80
91
  self.check_requested_path(req_path)
81
92
 
@@ -86,7 +97,7 @@ class FsToolContext:
86
97
  req_path,
87
98
  suggested_paths=get_path_suggestions(
88
99
  req_path,
89
- filter=lambda e: (e.is_file() and not has_binary_file_extension(e.name)),
100
+ filter=lambda e: (e.is_file() and not (text and has_binary_file_extension(e.name))),
90
101
  ),
91
102
  ) from None
92
103
 
@@ -105,8 +116,11 @@ class FsToolContext:
105
116
  actual_type='binary file',
106
117
  )
107
118
 
119
+ if write and not self._writes_permitted:
120
+ raise RequestedPathWriteNotPermittedError(req_path)
121
+
108
122
  return st
109
123
 
110
124
 
111
- def fs_tool_context() -> FsToolContext:
112
- return tool_context()[FsToolContext]
125
+ def tool_fs_context() -> FsContext:
126
+ return tool_context()[FsContext]
@@ -93,3 +93,9 @@ class RequestedPathDoesNotExistError(RequestedPathError):
93
93
  f'Requested path {self.requested_path!r} does not exist.',
94
94
  *([f' Did you mean one of these valid paths: {self.suggested_paths!r}?'] if self.suggested_paths else []),
95
95
  ])
96
+
97
+
98
+ class RequestedPathWriteNotPermittedError(RequestedPathError):
99
+ @property
100
+ def content(self) -> str:
101
+ return f'Writes are not permitted to requested path {self.requested_path!r}.'
@@ -0,0 +1,104 @@
1
+ """
2
+ TODO:
3
+ - must read file before editing
4
+ - must re-read file if file has been modified
5
+ - loosened replacer helpers
6
+ - accept diff format impl
7
+ - injectable confirmation, diff format
8
+ """
9
+ from omlish import lang
10
+
11
+ from ....tools.execution.catalog import ToolCatalogEntry
12
+ from ....tools.execution.reflect import reflect_tool_catalog_entry
13
+ from ..context import tool_fs_context
14
+ from ..errors import RequestedPathError
15
+
16
+
17
+ ##
18
+
19
+
20
+ class EditToolError(RequestedPathError, lang.Abstract):
21
+ pass
22
+
23
+
24
+ class EmptyNewStringError(EditToolError):
25
+ @property
26
+ def content(self) -> str:
27
+ return f'The requested edit to {self.requested_path!r} was given an empty "old_string" parameter.'
28
+
29
+
30
+ class OldStringNotPresentError(EditToolError):
31
+ @property
32
+ def content(self) -> str:
33
+ return f'The requested edit to {self.requested_path!r} did not contain the given "old_string" parameter.'
34
+
35
+
36
+ class OldStringPresentMultipleTimesError(EditToolError):
37
+ @property
38
+ def content(self) -> str:
39
+ return f'The requested edit to {self.requested_path!r} contained the given "old_string" parameter multiple times.' # noqa
40
+
41
+
42
+ ##
43
+
44
+
45
+ def execute_edit_tool(
46
+ *,
47
+ file_path: str,
48
+ old_string: str,
49
+ new_string: str,
50
+ replace_all: bool = False,
51
+ ) -> str:
52
+ """
53
+ Edits the given file by replacing the string given by the 'old_string' parameter with the string given by the
54
+ 'new_string' parameter.
55
+
56
+ The file must exist, must be a valid text file, and must be given as an absolute path.
57
+
58
+ If the 'replace_all' parameter is false (the default) then 'new_string' must be present exactly once in the file,
59
+ otherwise the operation will fail. If 'replace_all' is true then all instances of 'old_string' will be replaced by
60
+ 'new_string', but the operation will fail if there are no instances of 'old_string'
61
+ present in the file.
62
+
63
+ For the operation to succeed, both 'old_string' and 'new_string' must be EXACT, including all exact indentation and
64
+ other whitespace. This *includes* trailing newlines - this operates on the file as a single string, not a list of
65
+ lines.
66
+
67
+ Args:
68
+ file_path: The path of the file to edit. Must be an absolute path.
69
+ old_string: The old string to be replaced. May not be empty, and must be exact, including all exact whitespace.
70
+ new_string: The new string to replace the old string with.
71
+ replace_all: If false (the default) then exactly one instance of 'old_string' must be present in the file to be
72
+ replaced. If true then all instances of 'old_string' will be replaced by 'new_string', but at least one
73
+ instance of 'old_string' must be present in the file.
74
+ """
75
+
76
+ if not old_string:
77
+ raise EmptyNewStringError(file_path)
78
+
79
+ ctx = tool_fs_context()
80
+ ctx.check_stat_file(file_path, text=True, write=True)
81
+
82
+ with open(file_path) as f:
83
+ old_file = f.read()
84
+
85
+ n = old_file.count(old_string)
86
+ if not n:
87
+ raise OldStringNotPresentError(file_path)
88
+
89
+ if not replace_all and n != 1:
90
+ raise OldStringPresentMultipleTimesError(file_path)
91
+
92
+ new_file = old_file.replace(old_string, new_string)
93
+
94
+ # FIXME: confirm lol
95
+
96
+ with open(file_path, 'w') as f:
97
+ f.write(new_file)
98
+
99
+ return 'The file has been edited successfully.'
100
+
101
+
102
+ @lang.cached_function
103
+ def edit_tool() -> ToolCatalogEntry:
104
+ return reflect_tool_catalog_entry(execute_edit_tool)
@@ -5,7 +5,7 @@ from omlish import lang
5
5
 
6
6
  from ....tools.execution.catalog import ToolCatalogEntry
7
7
  from ....tools.execution.reflect import reflect_tool_catalog_entry
8
- from ..context import fs_tool_context
8
+ from ..context import tool_fs_context
9
9
 
10
10
 
11
11
  ##
@@ -21,8 +21,8 @@ def execute_ls_tool(
21
21
  dir_path: The dir to list the contents of. Must be an absolute path.
22
22
  """
23
23
 
24
- ft_ctx = fs_tool_context()
25
- ft_ctx.check_stat_dir(dir_path)
24
+ ctx = tool_fs_context()
25
+ ctx.check_stat_dir(dir_path)
26
26
 
27
27
  out = io.StringIO()
28
28
  out.write('<dir>\n')
@@ -14,7 +14,7 @@ from ....tools.execution.catalog import ToolCatalogEntry
14
14
  from ....tools.execution.reflect import reflect_tool_catalog_entry
15
15
  from ....tools.reflect import tool_spec_override
16
16
  from ....tools.types import ToolParam
17
- from ..context import fs_tool_context
17
+ from ..context import tool_fs_context
18
18
 
19
19
 
20
20
  ##
@@ -26,8 +26,8 @@ MAX_LINE_LENGTH = 2_000
26
26
 
27
27
 
28
28
  @tool_spec_override(
29
- desc=rf"""
30
- Reads a file from the local filesystem. You can access any file directly by using this tool.
29
+ desc=f"""
30
+ Reads a file from the local filesystem. You can access any file directly by using this tool.
31
31
 
32
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
33
  path is valid. It is okay to read a file that does not exist; an error will be returned.
@@ -42,7 +42,7 @@ MAX_LINE_LENGTH = 2_000
42
42
  - Results are returned using cat -n format, with line numbers starting at 1 and suffixed with a pipe character
43
43
  "|".
44
44
  - This tool cannot read binary files, including images.
45
- """,
45
+ """,
46
46
  params=[
47
47
  ToolParam(
48
48
  'file_path',
@@ -64,8 +64,8 @@ def execute_read_tool(
64
64
  line_offset: int = 0,
65
65
  num_lines: int = DEFAULT_MAX_NUM_LINES,
66
66
  ) -> str:
67
- ft_ctx = fs_tool_context()
68
- ft_ctx.check_stat_file(file_path, text=True)
67
+ ctx = tool_fs_context()
68
+ ctx.check_stat_file(file_path, text=True)
69
69
 
70
70
  out = io.StringIO()
71
71
  out.write('<file>\n')
File without changes
@@ -2,7 +2,7 @@ from omlish import lang
2
2
 
3
3
  from .....tools.execution.catalog import ToolCatalogEntry
4
4
  from .....tools.execution.reflect import reflect_tool_catalog_entry
5
- from ...context import fs_tool_context
5
+ from ...context import tool_fs_context
6
6
  from .rendering import LsLinesRenderer
7
7
  from .running import LsRunner
8
8
 
@@ -23,7 +23,7 @@ def execute_recursive_ls_tool(
23
23
  A formatted string of the recursive directory contents.
24
24
  """
25
25
 
26
- ft_ctx = fs_tool_context()
26
+ ft_ctx = tool_fs_context()
27
27
  ft_ctx.check_requested_path(base_path)
28
28
 
29
29
  root = LsRunner().run(base_path)
File without changes
@@ -0,0 +1,54 @@
1
+ import typing as ta
2
+
3
+ from omlish import dataclasses as dc
4
+
5
+ from ...tools.execution.context import tool_context
6
+ from .types import TodoItem
7
+
8
+
9
+ ##
10
+
11
+
12
+ def _try_int(s: str | None) -> int | None:
13
+ if s is None:
14
+ return None
15
+ try:
16
+ return int(s)
17
+ except ValueError:
18
+ return None
19
+
20
+
21
+ class TodoContext:
22
+ def __init__(
23
+ self,
24
+ items: ta.Sequence[TodoItem] | None = None,
25
+ ) -> None:
26
+ super().__init__()
27
+
28
+ self._items = items
29
+ self._next_id: int = 1
30
+
31
+ def get_items(self) -> ta.Sequence[TodoItem] | None:
32
+ return self._items
33
+
34
+ def set_items(self, items: ta.Sequence[TodoItem] | None) -> ta.Sequence[TodoItem] | None:
35
+ if items and any(item.id is None for item in items):
36
+ max_id = max([
37
+ *[ii for item in items if (ii := _try_int(item.id)) is not None],
38
+ self._next_id - 1,
39
+ ])
40
+ new_items: list[TodoItem] = []
41
+ for item in items:
42
+ if item.id is None:
43
+ item = dc.replace(item, id=str(max_id + 1))
44
+ max_id += 1
45
+ new_items.append(item)
46
+ items = new_items
47
+ self._next_id = max_id + 1
48
+
49
+ self._items = list(items) if items is not None else None
50
+ return items
51
+
52
+
53
+ def tool_todo_context() -> TodoContext:
54
+ return tool_context()[TodoContext]
File without changes