ommlds 0.0.0.dev436__py3-none-any.whl → 0.0.0.dev480__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 (271) hide show
  1. ommlds/.omlish-manifests.json +332 -35
  2. ommlds/__about__.py +15 -9
  3. ommlds/_hacks/__init__.py +4 -0
  4. ommlds/_hacks/funcs.py +110 -0
  5. ommlds/_hacks/names.py +158 -0
  6. ommlds/_hacks/params.py +73 -0
  7. ommlds/_hacks/patches.py +0 -3
  8. ommlds/backends/anthropic/protocol/_marshal.py +2 -2
  9. ommlds/backends/anthropic/protocol/sse/_marshal.py +1 -1
  10. ommlds/backends/anthropic/protocol/sse/assemble.py +23 -7
  11. ommlds/backends/anthropic/protocol/sse/events.py +13 -0
  12. ommlds/backends/anthropic/protocol/types.py +30 -9
  13. ommlds/backends/google/protocol/__init__.py +3 -0
  14. ommlds/backends/google/protocol/_marshal.py +16 -0
  15. ommlds/backends/google/protocol/types.py +626 -0
  16. ommlds/backends/groq/_marshal.py +23 -0
  17. ommlds/backends/groq/protocol.py +249 -0
  18. ommlds/backends/mlx/generation.py +1 -1
  19. ommlds/backends/mlx/loading.py +58 -1
  20. ommlds/backends/ollama/__init__.py +0 -0
  21. ommlds/backends/ollama/protocol.py +170 -0
  22. ommlds/backends/openai/protocol/__init__.py +9 -28
  23. ommlds/backends/openai/protocol/_common.py +18 -0
  24. ommlds/backends/openai/protocol/_marshal.py +27 -0
  25. ommlds/backends/openai/protocol/chatcompletion/chunk.py +58 -31
  26. ommlds/backends/openai/protocol/chatcompletion/contentpart.py +49 -44
  27. ommlds/backends/openai/protocol/chatcompletion/message.py +55 -43
  28. ommlds/backends/openai/protocol/chatcompletion/request.py +114 -66
  29. ommlds/backends/openai/protocol/chatcompletion/response.py +71 -45
  30. ommlds/backends/openai/protocol/chatcompletion/responseformat.py +27 -20
  31. ommlds/backends/openai/protocol/chatcompletion/tokenlogprob.py +16 -7
  32. ommlds/backends/openai/protocol/completionusage.py +24 -15
  33. ommlds/backends/tavily/__init__.py +0 -0
  34. ommlds/backends/tavily/protocol.py +301 -0
  35. ommlds/backends/tinygrad/models/llama3/__init__.py +22 -14
  36. ommlds/backends/transformers/__init__.py +0 -0
  37. ommlds/backends/transformers/filecache.py +109 -0
  38. ommlds/backends/transformers/streamers.py +73 -0
  39. ommlds/cli/asyncs.py +30 -0
  40. ommlds/cli/backends/catalog.py +93 -0
  41. ommlds/cli/backends/configs.py +9 -0
  42. ommlds/cli/backends/inject.py +31 -36
  43. ommlds/cli/backends/injection.py +16 -0
  44. ommlds/cli/backends/types.py +46 -0
  45. ommlds/cli/content/__init__.py +0 -0
  46. ommlds/cli/content/messages.py +34 -0
  47. ommlds/cli/content/strings.py +42 -0
  48. ommlds/cli/inject.py +15 -32
  49. ommlds/cli/inputs/__init__.py +0 -0
  50. ommlds/cli/inputs/asyncs.py +32 -0
  51. ommlds/cli/inputs/sync.py +75 -0
  52. ommlds/cli/main.py +270 -110
  53. ommlds/cli/rendering/__init__.py +0 -0
  54. ommlds/cli/rendering/configs.py +9 -0
  55. ommlds/cli/rendering/inject.py +31 -0
  56. ommlds/cli/rendering/markdown.py +52 -0
  57. ommlds/cli/rendering/raw.py +73 -0
  58. ommlds/cli/rendering/types.py +21 -0
  59. ommlds/cli/secrets.py +21 -0
  60. ommlds/cli/sessions/base.py +1 -1
  61. ommlds/cli/sessions/chat/chat/__init__.py +0 -0
  62. ommlds/cli/sessions/chat/chat/ai/__init__.py +0 -0
  63. ommlds/cli/sessions/chat/chat/ai/configs.py +11 -0
  64. ommlds/cli/sessions/chat/chat/ai/inject.py +74 -0
  65. ommlds/cli/sessions/chat/chat/ai/injection.py +14 -0
  66. ommlds/cli/sessions/chat/chat/ai/rendering.py +70 -0
  67. ommlds/cli/sessions/chat/chat/ai/services.py +79 -0
  68. ommlds/cli/sessions/chat/chat/ai/tools.py +44 -0
  69. ommlds/cli/sessions/chat/chat/ai/types.py +28 -0
  70. ommlds/cli/sessions/chat/chat/state/__init__.py +0 -0
  71. ommlds/cli/sessions/chat/chat/state/configs.py +11 -0
  72. ommlds/cli/sessions/chat/chat/state/inject.py +36 -0
  73. ommlds/cli/sessions/chat/chat/state/inmemory.py +33 -0
  74. ommlds/cli/sessions/chat/chat/state/storage.py +52 -0
  75. ommlds/cli/sessions/chat/chat/state/types.py +38 -0
  76. ommlds/cli/sessions/chat/chat/user/__init__.py +0 -0
  77. ommlds/cli/sessions/chat/chat/user/configs.py +17 -0
  78. ommlds/cli/sessions/chat/chat/user/inject.py +62 -0
  79. ommlds/cli/sessions/chat/chat/user/interactive.py +31 -0
  80. ommlds/cli/sessions/chat/chat/user/oneshot.py +25 -0
  81. ommlds/cli/sessions/chat/chat/user/types.py +15 -0
  82. ommlds/cli/sessions/chat/configs.py +27 -0
  83. ommlds/cli/sessions/chat/driver.py +43 -0
  84. ommlds/cli/sessions/chat/inject.py +33 -65
  85. ommlds/cli/sessions/chat/phases/__init__.py +0 -0
  86. ommlds/cli/sessions/chat/phases/inject.py +27 -0
  87. ommlds/cli/sessions/chat/phases/injection.py +14 -0
  88. ommlds/cli/sessions/chat/phases/manager.py +29 -0
  89. ommlds/cli/sessions/chat/phases/types.py +29 -0
  90. ommlds/cli/sessions/chat/session.py +27 -0
  91. ommlds/cli/sessions/chat/tools/__init__.py +0 -0
  92. ommlds/cli/sessions/chat/tools/configs.py +22 -0
  93. ommlds/cli/sessions/chat/tools/confirmation.py +46 -0
  94. ommlds/cli/sessions/chat/tools/execution.py +66 -0
  95. ommlds/cli/sessions/chat/tools/fs/__init__.py +0 -0
  96. ommlds/cli/sessions/chat/tools/fs/configs.py +12 -0
  97. ommlds/cli/sessions/chat/tools/fs/inject.py +35 -0
  98. ommlds/cli/sessions/chat/tools/inject.py +88 -0
  99. ommlds/cli/sessions/chat/tools/injection.py +44 -0
  100. ommlds/cli/sessions/chat/tools/rendering.py +58 -0
  101. ommlds/cli/sessions/chat/tools/todo/__init__.py +0 -0
  102. ommlds/cli/sessions/chat/tools/todo/configs.py +12 -0
  103. ommlds/cli/sessions/chat/tools/todo/inject.py +31 -0
  104. ommlds/cli/sessions/chat/tools/weather/__init__.py +0 -0
  105. ommlds/cli/sessions/chat/tools/weather/configs.py +12 -0
  106. ommlds/cli/sessions/chat/tools/weather/inject.py +22 -0
  107. ommlds/cli/{tools/weather.py → sessions/chat/tools/weather/tools.py} +1 -1
  108. ommlds/cli/sessions/completion/configs.py +21 -0
  109. ommlds/cli/sessions/completion/inject.py +42 -0
  110. ommlds/cli/sessions/completion/session.py +35 -0
  111. ommlds/cli/sessions/embedding/configs.py +21 -0
  112. ommlds/cli/sessions/embedding/inject.py +42 -0
  113. ommlds/cli/sessions/embedding/session.py +33 -0
  114. ommlds/cli/sessions/inject.py +28 -11
  115. ommlds/cli/state/__init__.py +0 -0
  116. ommlds/cli/state/inject.py +28 -0
  117. ommlds/cli/{state.py → state/storage.py} +41 -24
  118. ommlds/minichain/__init__.py +84 -24
  119. ommlds/minichain/_marshal.py +49 -9
  120. ommlds/minichain/_typedvalues.py +2 -4
  121. ommlds/minichain/backends/catalogs/base.py +20 -1
  122. ommlds/minichain/backends/catalogs/simple.py +2 -2
  123. ommlds/minichain/backends/catalogs/strings.py +10 -8
  124. ommlds/minichain/backends/impls/anthropic/chat.py +65 -27
  125. ommlds/minichain/backends/impls/anthropic/names.py +10 -8
  126. ommlds/minichain/backends/impls/anthropic/protocol.py +109 -0
  127. ommlds/minichain/backends/impls/anthropic/stream.py +111 -43
  128. ommlds/minichain/backends/impls/duckduckgo/search.py +1 -1
  129. ommlds/minichain/backends/impls/dummy/__init__.py +0 -0
  130. ommlds/minichain/backends/impls/dummy/chat.py +69 -0
  131. ommlds/minichain/backends/impls/google/chat.py +114 -22
  132. ommlds/minichain/backends/impls/google/search.py +7 -2
  133. ommlds/minichain/backends/impls/google/stream.py +219 -0
  134. ommlds/minichain/backends/impls/google/tools.py +149 -0
  135. ommlds/minichain/backends/impls/groq/__init__.py +0 -0
  136. ommlds/minichain/backends/impls/groq/chat.py +75 -0
  137. ommlds/minichain/backends/impls/groq/names.py +48 -0
  138. ommlds/minichain/backends/impls/groq/protocol.py +143 -0
  139. ommlds/minichain/backends/impls/groq/stream.py +125 -0
  140. ommlds/minichain/backends/impls/llamacpp/chat.py +33 -18
  141. ommlds/minichain/backends/impls/llamacpp/completion.py +1 -1
  142. ommlds/minichain/backends/impls/llamacpp/format.py +4 -2
  143. ommlds/minichain/backends/impls/llamacpp/stream.py +37 -20
  144. ommlds/minichain/backends/impls/mistral.py +20 -5
  145. ommlds/minichain/backends/impls/mlx/chat.py +96 -22
  146. ommlds/minichain/backends/impls/ollama/__init__.py +0 -0
  147. ommlds/minichain/backends/impls/ollama/chat.py +199 -0
  148. ommlds/minichain/backends/impls/openai/chat.py +18 -8
  149. ommlds/minichain/backends/impls/openai/completion.py +10 -3
  150. ommlds/minichain/backends/impls/openai/embedding.py +10 -3
  151. ommlds/minichain/backends/impls/openai/format.py +131 -106
  152. ommlds/minichain/backends/impls/openai/names.py +31 -5
  153. ommlds/minichain/backends/impls/openai/stream.py +43 -25
  154. ommlds/minichain/backends/impls/tavily.py +66 -0
  155. ommlds/minichain/backends/impls/tinygrad/chat.py +23 -16
  156. ommlds/minichain/backends/impls/transformers/sentence.py +1 -1
  157. ommlds/minichain/backends/impls/transformers/tokens.py +1 -1
  158. ommlds/minichain/backends/impls/transformers/transformers.py +155 -34
  159. ommlds/minichain/backends/strings/parsing.py +1 -1
  160. ommlds/minichain/backends/strings/resolving.py +4 -1
  161. ommlds/minichain/chat/_marshal.py +16 -9
  162. ommlds/minichain/chat/choices/adapters.py +4 -4
  163. ommlds/minichain/chat/choices/services.py +1 -1
  164. ommlds/minichain/chat/choices/stream/__init__.py +0 -0
  165. ommlds/minichain/chat/choices/stream/adapters.py +35 -0
  166. ommlds/minichain/chat/choices/stream/joining.py +31 -0
  167. ommlds/minichain/chat/choices/stream/services.py +45 -0
  168. ommlds/minichain/chat/choices/stream/types.py +43 -0
  169. ommlds/minichain/chat/choices/types.py +2 -2
  170. ommlds/minichain/chat/history.py +3 -3
  171. ommlds/minichain/chat/messages.py +55 -19
  172. ommlds/minichain/chat/services.py +3 -3
  173. ommlds/minichain/chat/stream/_marshal.py +16 -0
  174. ommlds/minichain/chat/stream/joining.py +85 -0
  175. ommlds/minichain/chat/stream/services.py +15 -21
  176. ommlds/minichain/chat/stream/types.py +32 -19
  177. ommlds/minichain/chat/tools/execution.py +8 -7
  178. ommlds/minichain/chat/tools/ids.py +9 -15
  179. ommlds/minichain/chat/tools/parsing.py +17 -26
  180. ommlds/minichain/chat/transforms/base.py +29 -38
  181. ommlds/minichain/chat/transforms/metadata.py +30 -4
  182. ommlds/minichain/chat/transforms/services.py +9 -11
  183. ommlds/minichain/content/_marshal.py +44 -20
  184. ommlds/minichain/content/json.py +13 -0
  185. ommlds/minichain/content/materialize.py +14 -21
  186. ommlds/minichain/content/prepare.py +4 -0
  187. ommlds/minichain/content/transforms/interleave.py +1 -1
  188. ommlds/minichain/content/transforms/squeeze.py +1 -1
  189. ommlds/minichain/content/transforms/stringify.py +1 -1
  190. ommlds/minichain/json.py +20 -0
  191. ommlds/minichain/lib/code/__init__.py +0 -0
  192. ommlds/minichain/lib/code/prompts.py +6 -0
  193. ommlds/minichain/lib/fs/binfiles.py +108 -0
  194. ommlds/minichain/lib/fs/context.py +126 -0
  195. ommlds/minichain/lib/fs/errors.py +101 -0
  196. ommlds/minichain/lib/fs/suggestions.py +36 -0
  197. ommlds/minichain/lib/fs/tools/__init__.py +0 -0
  198. ommlds/minichain/lib/fs/tools/edit.py +104 -0
  199. ommlds/minichain/lib/fs/tools/ls.py +38 -0
  200. ommlds/minichain/lib/fs/tools/read.py +115 -0
  201. ommlds/minichain/lib/fs/tools/recursivels/__init__.py +0 -0
  202. ommlds/minichain/lib/fs/tools/recursivels/execution.py +40 -0
  203. ommlds/minichain/lib/todo/__init__.py +0 -0
  204. ommlds/minichain/lib/todo/context.py +54 -0
  205. ommlds/minichain/lib/todo/tools/__init__.py +0 -0
  206. ommlds/minichain/lib/todo/tools/read.py +44 -0
  207. ommlds/minichain/lib/todo/tools/write.py +335 -0
  208. ommlds/minichain/lib/todo/types.py +60 -0
  209. ommlds/minichain/llms/_marshal.py +25 -17
  210. ommlds/minichain/llms/types.py +4 -0
  211. ommlds/minichain/registries/globals.py +18 -4
  212. ommlds/minichain/resources.py +66 -43
  213. ommlds/minichain/search.py +1 -1
  214. ommlds/minichain/services/_marshal.py +46 -39
  215. ommlds/minichain/services/facades.py +3 -3
  216. ommlds/minichain/services/services.py +1 -1
  217. ommlds/minichain/standard.py +8 -0
  218. ommlds/minichain/stream/services.py +152 -38
  219. ommlds/minichain/stream/wrap.py +22 -24
  220. ommlds/minichain/tools/_marshal.py +1 -1
  221. ommlds/minichain/tools/execution/catalog.py +2 -1
  222. ommlds/minichain/tools/execution/context.py +34 -14
  223. ommlds/minichain/tools/execution/errors.py +15 -0
  224. ommlds/minichain/tools/execution/executors.py +8 -3
  225. ommlds/minichain/tools/execution/reflect.py +40 -5
  226. ommlds/minichain/tools/fns.py +46 -9
  227. ommlds/minichain/tools/jsonschema.py +14 -5
  228. ommlds/minichain/tools/reflect.py +54 -18
  229. ommlds/minichain/tools/types.py +33 -1
  230. ommlds/minichain/utils.py +27 -0
  231. ommlds/minichain/vectors/_marshal.py +11 -10
  232. ommlds/nanochat/LICENSE +21 -0
  233. ommlds/nanochat/__init__.py +0 -0
  234. ommlds/nanochat/rustbpe/LICENSE +21 -0
  235. ommlds/nanochat/tokenizers.py +406 -0
  236. ommlds/server/server.py +3 -3
  237. ommlds/specs/__init__.py +0 -0
  238. ommlds/specs/mcp/__init__.py +0 -0
  239. ommlds/specs/mcp/_marshal.py +23 -0
  240. ommlds/specs/mcp/protocol.py +266 -0
  241. ommlds/tools/git.py +27 -10
  242. ommlds/tools/ocr.py +8 -9
  243. ommlds/wiki/analyze.py +2 -2
  244. ommlds/wiki/text/mfh.py +1 -5
  245. ommlds/wiki/text/wtp.py +1 -3
  246. ommlds/wiki/utils/xml.py +5 -5
  247. {ommlds-0.0.0.dev436.dist-info → ommlds-0.0.0.dev480.dist-info}/METADATA +24 -21
  248. ommlds-0.0.0.dev480.dist-info/RECORD +427 -0
  249. ommlds/cli/backends/standard.py +0 -20
  250. ommlds/cli/sessions/chat/base.py +0 -42
  251. ommlds/cli/sessions/chat/interactive.py +0 -73
  252. ommlds/cli/sessions/chat/printing.py +0 -96
  253. ommlds/cli/sessions/chat/prompt.py +0 -143
  254. ommlds/cli/sessions/chat/state.py +0 -109
  255. ommlds/cli/sessions/chat/tools.py +0 -91
  256. ommlds/cli/sessions/completion/completion.py +0 -44
  257. ommlds/cli/sessions/embedding/embedding.py +0 -42
  258. ommlds/cli/tools/config.py +0 -13
  259. ommlds/cli/tools/inject.py +0 -64
  260. ommlds/minichain/chat/stream/adapters.py +0 -69
  261. ommlds/minichain/lib/fs/ls/execution.py +0 -32
  262. ommlds-0.0.0.dev436.dist-info/RECORD +0 -303
  263. /ommlds/{cli/tools → backends/google}/__init__.py +0 -0
  264. /ommlds/{minichain/lib/fs/ls → backends/groq}/__init__.py +0 -0
  265. /ommlds/{huggingface.py → backends/huggingface.py} +0 -0
  266. /ommlds/minichain/lib/fs/{ls → tools/recursivels}/rendering.py +0 -0
  267. /ommlds/minichain/lib/fs/{ls → tools/recursivels}/running.py +0 -0
  268. {ommlds-0.0.0.dev436.dist-info → ommlds-0.0.0.dev480.dist-info}/WHEEL +0 -0
  269. {ommlds-0.0.0.dev436.dist-info → ommlds-0.0.0.dev480.dist-info}/entry_points.txt +0 -0
  270. {ommlds-0.0.0.dev436.dist-info → ommlds-0.0.0.dev480.dist-info}/licenses/LICENSE +0 -0
  271. {ommlds-0.0.0.dev436.dist-info → ommlds-0.0.0.dev480.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,126 @@
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 RequestedPathWriteNotPermittedError
12
+ from .errors import RequestedPathWrongTypeError
13
+ from .suggestions import get_path_suggestions
14
+
15
+
16
+ ##
17
+
18
+
19
+ class FsContext:
20
+ def __init__(
21
+ self,
22
+ *,
23
+ root_dir: str | None = None,
24
+ writes_permitted: bool = False,
25
+ ) -> None:
26
+ super().__init__()
27
+
28
+ self._root_dir = root_dir
29
+ self._writes_permitted = writes_permitted
30
+
31
+ self._abs_root_dir = os.path.abspath(root_dir) if root_dir is not None else None
32
+
33
+ #
34
+
35
+ @property
36
+ def writes_permitted(self) -> bool:
37
+ return self._writes_permitted
38
+
39
+ #
40
+
41
+ def check_requested_path(self, req_path: str) -> None:
42
+ abs_req_path = os.path.abspath(req_path)
43
+
44
+ if (
45
+ self._abs_root_dir is None or
46
+ not (
47
+ abs_req_path == self._abs_root_dir or
48
+ abs_req_path.startswith(self._abs_root_dir + os.path.sep)
49
+ )
50
+ ):
51
+ raise RequestedPathOutsideRootDirError(
52
+ req_path,
53
+ root_dir=check.not_none(self._root_dir),
54
+ )
55
+
56
+ #
57
+
58
+ def check_stat_dir(
59
+ self,
60
+ req_path: str,
61
+ ) -> os.stat_result:
62
+ self.check_requested_path(req_path)
63
+
64
+ try:
65
+ st = os.stat(req_path)
66
+ except FileNotFoundError:
67
+ raise RequestedPathDoesNotExistError(
68
+ req_path,
69
+ suggested_paths=get_path_suggestions(
70
+ req_path,
71
+ filter=lambda e: e.is_dir(),
72
+ ),
73
+ ) from None
74
+
75
+ if not stat.S_ISDIR(st.st_mode):
76
+ raise RequestedPathWrongTypeError(
77
+ req_path,
78
+ expected_type='dir',
79
+ **(dict(actual_type='file') if stat.S_ISREG(st.st_mode) else {}),
80
+ )
81
+
82
+ return st
83
+
84
+ def check_stat_file(
85
+ self,
86
+ req_path: str,
87
+ *,
88
+ text: bool = False,
89
+ write: bool = False,
90
+ ) -> os.stat_result:
91
+ self.check_requested_path(req_path)
92
+
93
+ try:
94
+ st = os.stat(req_path)
95
+ except FileNotFoundError:
96
+ raise RequestedPathDoesNotExistError(
97
+ req_path,
98
+ suggested_paths=get_path_suggestions(
99
+ req_path,
100
+ filter=lambda e: (e.is_file() and not (text and has_binary_file_extension(e.name))),
101
+ ),
102
+ ) from None
103
+
104
+ if not stat.S_ISREG(st.st_mode):
105
+ is_dir = stat.S_ISDIR(st.st_mode)
106
+ raise RequestedPathWrongTypeError(
107
+ req_path,
108
+ expected_type='file',
109
+ **(dict(actual_type='dir') if is_dir else {}),
110
+ )
111
+
112
+ if text and is_binary_file(req_path, st=st):
113
+ raise RequestedPathWrongTypeError(
114
+ req_path,
115
+ expected_type='text file',
116
+ actual_type='binary file',
117
+ )
118
+
119
+ if write and not self._writes_permitted:
120
+ raise RequestedPathWriteNotPermittedError(req_path)
121
+
122
+ return st
123
+
124
+
125
+ def tool_fs_context() -> FsContext:
126
+ return tool_context()[FsContext]
@@ -0,0 +1,101 @@
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
+ ])
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,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,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)
@@ -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 tool_fs_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 = tool_fs_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 tool_fs_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 = tool_fs_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 tool_fs_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 = tool_fs_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