zrb 1.21.28__py3-none-any.whl → 1.21.37__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 zrb might be problematic. Click here for more details.
- zrb/builtin/llm/chat_completion.py +199 -222
- zrb/builtin/llm/chat_session.py +1 -1
- zrb/builtin/llm/chat_session_cmd.py +28 -11
- zrb/builtin/llm/chat_trigger.py +3 -4
- zrb/builtin/llm/tool/cli.py +45 -14
- zrb/builtin/llm/tool/code.py +5 -1
- zrb/builtin/llm/tool/file.py +17 -0
- zrb/builtin/llm/tool/note.py +7 -7
- zrb/builtin/llm/tool/search/__init__.py +1 -0
- zrb/builtin/llm/tool/search/brave.py +66 -0
- zrb/builtin/llm/tool/search/searxng.py +61 -0
- zrb/builtin/llm/tool/search/serpapi.py +61 -0
- zrb/builtin/llm/tool/sub_agent.py +4 -1
- zrb/builtin/llm/tool/web.py +17 -72
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +5 -1
- zrb/config/default_prompt/interactive_system_prompt.md +15 -12
- zrb/config/default_prompt/system_prompt.md +16 -18
- zrb/config/llm_rate_limitter.py +36 -13
- zrb/input/option_input.py +30 -2
- zrb/task/cmd_task.py +3 -0
- zrb/task/llm/agent_runner.py +6 -2
- zrb/task/llm/history_list.py +13 -0
- zrb/task/llm/history_processor.py +4 -13
- zrb/task/llm/print_node.py +64 -23
- zrb/task/llm/tool_confirmation_completer.py +41 -0
- zrb/task/llm/tool_wrapper.py +6 -4
- zrb/task/llm/workflow.py +41 -14
- zrb/task/llm_task.py +4 -5
- zrb/task/rsync_task.py +2 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/match.py +71 -0
- {zrb-1.21.28.dist-info → zrb-1.21.37.dist-info}/METADATA +1 -1
- {zrb-1.21.28.dist-info → zrb-1.21.37.dist-info}/RECORD +36 -29
- {zrb-1.21.28.dist-info → zrb-1.21.37.dist-info}/WHEEL +0 -0
- {zrb-1.21.28.dist-info → zrb-1.21.37.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
|
-
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
4
|
-
from prompt_toolkit.document import Document
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
5
3
|
|
|
6
4
|
from zrb.builtin.llm.chat_session_cmd import (
|
|
7
5
|
ADD_SUB_CMD,
|
|
@@ -35,240 +33,219 @@ from zrb.builtin.llm.chat_session_cmd import (
|
|
|
35
33
|
YOLO_SET_FALSE_CMD_DESC,
|
|
36
34
|
YOLO_SET_TRUE_CMD_DESC,
|
|
37
35
|
)
|
|
36
|
+
from zrb.util.match import fuzzy_match
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from prompt_toolkit.completion import Completer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_chat_completer() -> "Completer":
|
|
43
|
+
|
|
44
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
45
|
+
from prompt_toolkit.document import Document
|
|
38
46
|
|
|
47
|
+
class ChatCompleter(Completer):
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent):
|
|
50
|
+
# Slash command
|
|
51
|
+
for completion in self._complete_slash_command(document):
|
|
52
|
+
yield completion
|
|
53
|
+
for completion in self._complete_slash_file_command(document):
|
|
54
|
+
yield completion
|
|
55
|
+
# Appendix
|
|
56
|
+
for completion in self._complete_appendix(document):
|
|
57
|
+
yield completion
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
def _complete_slash_file_command(self, document: Document):
|
|
60
|
+
text = document.text_before_cursor
|
|
61
|
+
prefixes = []
|
|
62
|
+
for cmd in ATTACHMENT_CMD:
|
|
63
|
+
for subcmd in ADD_SUB_CMD:
|
|
64
|
+
prefixes.append(f"{cmd} {subcmd} ")
|
|
65
|
+
for prefix in prefixes:
|
|
66
|
+
if text.startswith(prefix):
|
|
67
|
+
pattern = text[len(prefix) :]
|
|
68
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=True)
|
|
69
|
+
for prefixed_option in [
|
|
70
|
+
f"{prefix}{option}" for option in potential_options
|
|
71
|
+
]:
|
|
72
|
+
yield Completion(
|
|
73
|
+
prefixed_option,
|
|
74
|
+
start_position=-len(text),
|
|
75
|
+
)
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for
|
|
57
|
-
|
|
58
|
-
for prefix in prefixes:
|
|
59
|
-
if text.startswith(prefix):
|
|
60
|
-
pattern = text[len(prefix) :]
|
|
61
|
-
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
62
|
-
for prefixed_option in [
|
|
63
|
-
f"{prefix}{option}" for option in potential_options
|
|
64
|
-
]:
|
|
77
|
+
def _complete_slash_command(self, document: Document):
|
|
78
|
+
text = document.text_before_cursor
|
|
79
|
+
if not text.startswith("/"):
|
|
80
|
+
return
|
|
81
|
+
for command, description in self._get_cmd_options().items():
|
|
82
|
+
if command.lower().startswith(text.lower()):
|
|
65
83
|
yield Completion(
|
|
66
|
-
|
|
84
|
+
command,
|
|
67
85
|
start_position=-len(text),
|
|
86
|
+
display_meta=description,
|
|
68
87
|
)
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
def _complete_appendix(self, document: Document):
|
|
90
|
+
token = document.get_word_before_cursor(WORD=True)
|
|
91
|
+
prefix = "@"
|
|
92
|
+
if not token.startswith(prefix):
|
|
93
|
+
return
|
|
94
|
+
pattern = token[len(prefix) :]
|
|
95
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=True)
|
|
96
|
+
for prefixed_option in [
|
|
97
|
+
f"{prefix}{option}" for option in potential_options
|
|
98
|
+
]:
|
|
76
99
|
yield Completion(
|
|
77
|
-
|
|
78
|
-
start_position=-len(
|
|
79
|
-
display_meta=description,
|
|
100
|
+
prefixed_option,
|
|
101
|
+
start_position=-len(token),
|
|
80
102
|
)
|
|
81
103
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
def _get_cmd_options(self):
|
|
105
|
+
cmd_options = {}
|
|
106
|
+
# Add all commands with their descriptions
|
|
107
|
+
for cmd in MULTILINE_START_CMD:
|
|
108
|
+
cmd_options[cmd] = MULTILINE_START_CMD_DESC
|
|
109
|
+
for cmd in MULTILINE_END_CMD:
|
|
110
|
+
cmd_options[cmd] = MULTILINE_END_CMD_DESC
|
|
111
|
+
for cmd in QUIT_CMD:
|
|
112
|
+
cmd_options[cmd] = QUIT_CMD_DESC
|
|
113
|
+
for cmd in WORKFLOW_CMD:
|
|
114
|
+
cmd_options[cmd] = WORKFLOW_CMD_DESC
|
|
115
|
+
for subcmd in ADD_SUB_CMD:
|
|
116
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_ADD_SUB_CMD_DESC
|
|
117
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
118
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_CLEAR_SUB_CMD_DESC
|
|
119
|
+
for subcmd in SET_SUB_CMD:
|
|
120
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_SET_SUB_CMD_DESC
|
|
121
|
+
for cmd in SAVE_CMD:
|
|
122
|
+
cmd_options[cmd] = SAVE_CMD_DESC
|
|
123
|
+
for cmd in ATTACHMENT_CMD:
|
|
124
|
+
cmd_options[cmd] = ATTACHMENT_CMD_DESC
|
|
125
|
+
for subcmd in ADD_SUB_CMD:
|
|
126
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_ADD_SUB_CMD_DESC
|
|
127
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
128
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_CLEAR_SUB_CMD_DESC
|
|
129
|
+
for subcmd in SET_SUB_CMD:
|
|
130
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_SET_SUB_CMD_DESC
|
|
131
|
+
for cmd in YOLO_CMD:
|
|
132
|
+
cmd_options[cmd] = YOLO_CMD_DESC
|
|
133
|
+
for subcmd in SET_SUB_CMD:
|
|
134
|
+
cmd_options[f"{cmd} {subcmd} true"] = YOLO_SET_TRUE_CMD_DESC
|
|
135
|
+
cmd_options[f"{cmd} {subcmd} false"] = YOLO_SET_FALSE_CMD_DESC
|
|
136
|
+
cmd_options[f"{cmd} {subcmd}"] = YOLO_SET_CMD_DESC
|
|
137
|
+
for cmd in HELP_CMD:
|
|
138
|
+
cmd_options[cmd] = HELP_CMD_DESC
|
|
139
|
+
for cmd in RUN_CLI_CMD:
|
|
140
|
+
cmd_options[cmd] = RUN_CLI_CMD_DESC
|
|
141
|
+
return dict(sorted(cmd_options.items()))
|
|
94
142
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
def _fuzzy_path_search(
|
|
144
|
+
self,
|
|
145
|
+
pattern: str,
|
|
146
|
+
root: str | None = None,
|
|
147
|
+
max_results: int = 20,
|
|
148
|
+
include_hidden: bool = False,
|
|
149
|
+
case_sensitive: bool = False,
|
|
150
|
+
dirs: bool = True,
|
|
151
|
+
files: bool = True,
|
|
152
|
+
) -> list[str]:
|
|
153
|
+
"""
|
|
154
|
+
Return a list of filesystem paths under `root` that fuzzy-match `pattern`.
|
|
155
|
+
- pattern: e.g. "./some/x" or "proj util/io"
|
|
156
|
+
- include_hidden: if False skip files/dirs starting with '.'
|
|
157
|
+
- dirs/files booleans let you restrict results
|
|
158
|
+
- returns list of relative paths (from root), sorted best-first
|
|
159
|
+
"""
|
|
160
|
+
root, search_pattern = self._get_root_and_search_pattern(pattern, root)
|
|
161
|
+
# specific ignore list
|
|
162
|
+
try:
|
|
163
|
+
abs_root = os.path.abspath(os.path.expanduser(root))
|
|
164
|
+
abs_cwd = os.path.abspath(os.getcwd())
|
|
165
|
+
# Check if root is a subdirectory of cwd or is cwd itself
|
|
166
|
+
is_recursive = (
|
|
167
|
+
abs_root.startswith(abs_cwd + os.sep) or abs_root == abs_cwd
|
|
168
|
+
)
|
|
169
|
+
except Exception:
|
|
170
|
+
is_recursive = False
|
|
171
|
+
# walk filesystem
|
|
172
|
+
candidates: list[tuple[float, str]] = []
|
|
173
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
174
|
+
# Filter directories
|
|
175
|
+
if not include_hidden:
|
|
176
|
+
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
177
|
+
rel_dir = os.path.relpath(dirpath, root)
|
|
178
|
+
# treat '.' as empty prefix
|
|
179
|
+
if rel_dir == ".":
|
|
180
|
+
rel_dir = ""
|
|
181
|
+
# build list of entries to test depending on files/dirs flags
|
|
182
|
+
entries = []
|
|
183
|
+
if dirs:
|
|
184
|
+
entries.extend([os.path.join(rel_dir, d) for d in dirnames])
|
|
185
|
+
if files:
|
|
186
|
+
entries.extend([os.path.join(rel_dir, f) for f in filenames])
|
|
187
|
+
if not is_recursive:
|
|
188
|
+
dirnames[:] = []
|
|
189
|
+
for ent in entries:
|
|
190
|
+
# Normalize presentation: use ./ prefix for relative paths
|
|
191
|
+
display_path = ent if ent else "."
|
|
192
|
+
# Skip hidden entries unless requested (double check for rel path segments)
|
|
193
|
+
if not include_hidden:
|
|
194
|
+
if any(
|
|
195
|
+
seg.startswith(".")
|
|
196
|
+
for seg in display_path.split(os.sep)
|
|
197
|
+
if seg
|
|
198
|
+
):
|
|
199
|
+
continue
|
|
200
|
+
cand = display_path.replace(os.sep, "/") # unify separator
|
|
201
|
+
matched, score = fuzzy_match(cand, search_pattern)
|
|
202
|
+
if matched:
|
|
203
|
+
out = (
|
|
204
|
+
cand
|
|
205
|
+
if os.path.abspath(cand) == cand
|
|
206
|
+
else os.path.join(root, cand)
|
|
207
|
+
)
|
|
208
|
+
candidates.append((score, out))
|
|
209
|
+
# sort by score then lexicographically and return top results
|
|
210
|
+
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
211
|
+
return [p for _, p in candidates[:max_results]]
|
|
133
212
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- dirs/files booleans let you restrict results
|
|
149
|
-
- returns list of relative paths (from root), sorted best-first
|
|
150
|
-
"""
|
|
151
|
-
search_pattern = pattern
|
|
152
|
-
if root is None:
|
|
153
|
-
# Determine root and adjust pattern if necessary
|
|
154
|
-
expanded_pattern = os.path.expanduser(pattern)
|
|
155
|
-
if os.path.isabs(expanded_pattern) or pattern.startswith("~"):
|
|
156
|
-
# For absolute paths, find the deepest existing directory
|
|
157
|
-
if os.path.isdir(expanded_pattern):
|
|
158
|
-
root = expanded_pattern
|
|
159
|
-
search_pattern = ""
|
|
160
|
-
else:
|
|
213
|
+
def _get_root_and_search_pattern(
|
|
214
|
+
self,
|
|
215
|
+
pattern: str,
|
|
216
|
+
root: str | None = None,
|
|
217
|
+
) -> tuple[str, str]:
|
|
218
|
+
search_pattern = pattern
|
|
219
|
+
if root is None:
|
|
220
|
+
# Determine root and adjust pattern if necessary
|
|
221
|
+
expanded_pattern = os.path.expanduser(pattern)
|
|
222
|
+
if os.path.isabs(expanded_pattern) or pattern.startswith("~"):
|
|
223
|
+
# For absolute paths, find the deepest existing directory
|
|
224
|
+
if os.path.isdir(expanded_pattern):
|
|
225
|
+
root = expanded_pattern
|
|
226
|
+
return (root, "")
|
|
161
227
|
root = os.path.dirname(expanded_pattern)
|
|
162
228
|
while root and not os.path.isdir(root) and len(root) > 1:
|
|
163
229
|
root = os.path.dirname(root)
|
|
164
230
|
if not os.path.isdir(root):
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if not case_sensitive:
|
|
185
|
-
tokens = [t.lower() for t in raw_tokens]
|
|
186
|
-
else:
|
|
187
|
-
tokens = raw_tokens
|
|
188
|
-
# specific ignore list
|
|
189
|
-
try:
|
|
190
|
-
is_recursive = os.path.abspath(os.path.expanduser(root)).startswith(
|
|
191
|
-
os.path.abspath(os.getcwd())
|
|
192
|
-
)
|
|
193
|
-
except Exception:
|
|
194
|
-
is_recursive = False
|
|
195
|
-
# walk filesystem
|
|
196
|
-
candidates: list[tuple[float, str]] = []
|
|
197
|
-
for dirpath, dirnames, filenames in os.walk(root):
|
|
198
|
-
# Filter directories
|
|
199
|
-
if not include_hidden:
|
|
200
|
-
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
201
|
-
rel_dir = os.path.relpath(dirpath, root)
|
|
202
|
-
# treat '.' as empty prefix
|
|
203
|
-
if rel_dir == ".":
|
|
204
|
-
rel_dir = ""
|
|
205
|
-
# build list of entries to test depending on files/dirs flags
|
|
206
|
-
entries = []
|
|
207
|
-
if dirs:
|
|
208
|
-
entries.extend([os.path.join(rel_dir, d) for d in dirnames])
|
|
209
|
-
if files:
|
|
210
|
-
entries.extend([os.path.join(rel_dir, f) for f in filenames])
|
|
211
|
-
if not is_recursive:
|
|
212
|
-
dirnames[:] = []
|
|
213
|
-
for ent in entries:
|
|
214
|
-
# Normalize presentation: use ./ prefix for relative paths
|
|
215
|
-
display_path = ent if ent else "."
|
|
216
|
-
# Skip hidden entries unless requested (double check for rel path segments)
|
|
217
|
-
if not include_hidden:
|
|
218
|
-
if any(
|
|
219
|
-
seg.startswith(".") for seg in display_path.split(os.sep) if seg
|
|
220
|
-
):
|
|
221
|
-
continue
|
|
222
|
-
cand = display_path.replace(os.sep, "/") # unify separator
|
|
223
|
-
cand_cmp = cand if case_sensitive else cand.lower()
|
|
224
|
-
last_pos = 0
|
|
225
|
-
score = 0.0
|
|
226
|
-
matched_all = True
|
|
227
|
-
for token in tokens:
|
|
228
|
-
# try contiguous substring search first
|
|
229
|
-
idx = cand_cmp.find(token, last_pos)
|
|
230
|
-
if idx != -1:
|
|
231
|
-
# good match: reward contiguous early matches
|
|
232
|
-
score += idx # smaller idx preferred
|
|
233
|
-
last_pos = idx + len(token)
|
|
234
|
-
else:
|
|
235
|
-
# fallback to subsequence matching
|
|
236
|
-
pos = self._find_subsequence_pos(cand_cmp, token, last_pos)
|
|
237
|
-
if pos is None:
|
|
238
|
-
matched_all = False
|
|
239
|
-
break
|
|
240
|
-
# subsequence match is less preferred than contiguous substring
|
|
241
|
-
score += pos + 0.5 * len(token)
|
|
242
|
-
last_pos = pos + len(token)
|
|
243
|
-
if matched_all:
|
|
244
|
-
# prefer shorter paths when score ties, so include length as tiebreaker
|
|
245
|
-
score += 0.01 * len(cand)
|
|
246
|
-
out = (
|
|
247
|
-
cand
|
|
248
|
-
if os.path.abspath(cand) == cand
|
|
249
|
-
else os.path.join(root, cand)
|
|
250
|
-
)
|
|
251
|
-
candidates.append((score, out))
|
|
252
|
-
# sort by score then lexicographically and return top results
|
|
253
|
-
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
254
|
-
return [p for _, p in candidates[:max_results]]
|
|
231
|
+
return (".", pattern) # Fallback
|
|
232
|
+
try:
|
|
233
|
+
search_pattern = os.path.relpath(expanded_pattern, root)
|
|
234
|
+
if search_pattern == ".":
|
|
235
|
+
return (root, "")
|
|
236
|
+
except ValueError:
|
|
237
|
+
return (root, os.path.basename(pattern))
|
|
238
|
+
# Handle redundant current directory prefixes (e.g., ./ or .\)
|
|
239
|
+
if pattern.startswith(f".{os.sep}"):
|
|
240
|
+
return (".", pattern[len(f".{os.sep}") :])
|
|
241
|
+
if os.sep != "/" and pattern.startswith("./"):
|
|
242
|
+
return (".", pattern[2:])
|
|
243
|
+
return (".", pattern)
|
|
244
|
+
|
|
245
|
+
if pattern.startswith(f".{os.sep}"):
|
|
246
|
+
pattern = pattern[len(f".{os.sep}") :]
|
|
247
|
+
elif os.sep != "/" and pattern.startswith("./"):
|
|
248
|
+
pattern = pattern[2:]
|
|
249
|
+
return (root, pattern)
|
|
255
250
|
|
|
256
|
-
|
|
257
|
-
self, hay: str, needle: str, start: int = 0
|
|
258
|
-
) -> int | None:
|
|
259
|
-
"""
|
|
260
|
-
Try to locate needle in hay as a subsequence starting at `start`.
|
|
261
|
-
Returns the index of the first matched character of the subsequence or None if not match.
|
|
262
|
-
"""
|
|
263
|
-
if not needle:
|
|
264
|
-
return start
|
|
265
|
-
i = start
|
|
266
|
-
j = 0
|
|
267
|
-
first_pos = None
|
|
268
|
-
while i < len(hay) and j < len(needle):
|
|
269
|
-
if hay[i] == needle[j]:
|
|
270
|
-
if first_pos is None:
|
|
271
|
-
first_pos = i
|
|
272
|
-
j += 1
|
|
273
|
-
i += 1
|
|
274
|
-
return first_pos if j == len(needle) else None
|
|
251
|
+
return ChatCompleter()
|
zrb/builtin/llm/chat_session.py
CHANGED
|
@@ -101,7 +101,7 @@ async def read_user_prompt(ctx: AnyContext) -> str:
|
|
|
101
101
|
print_current_yolo_mode(ctx, current_yolo_mode)
|
|
102
102
|
continue
|
|
103
103
|
elif is_command_match(user_input, RUN_CLI_CMD):
|
|
104
|
-
run_cli_command(ctx, user_input)
|
|
104
|
+
await run_cli_command(ctx, user_input)
|
|
105
105
|
continue
|
|
106
106
|
elif is_command_match(user_input, HELP_CMD):
|
|
107
107
|
print_commands(ctx)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import
|
|
2
|
+
from typing import Callable
|
|
3
3
|
|
|
4
|
+
from zrb.config.config import CFG
|
|
4
5
|
from zrb.context.any_context import AnyContext
|
|
5
6
|
from zrb.task.llm.workflow import get_available_workflows
|
|
6
7
|
from zrb.util.cli.markdown import render_markdown
|
|
@@ -10,6 +11,7 @@ from zrb.util.cli.style import (
|
|
|
10
11
|
stylize_error,
|
|
11
12
|
stylize_faint,
|
|
12
13
|
)
|
|
14
|
+
from zrb.util.cmd.command import run_command
|
|
13
15
|
from zrb.util.file import write_file
|
|
14
16
|
from zrb.util.markdown import make_markdown_section
|
|
15
17
|
from zrb.util.string.conversion import FALSE_STRS, TRUE_STRS, to_boolean
|
|
@@ -118,13 +120,12 @@ def save_final_result(ctx: AnyContext, user_input: str, final_result: str) -> No
|
|
|
118
120
|
ctx.print(f"Response saved to {save_path}", plain=True)
|
|
119
121
|
|
|
120
122
|
|
|
121
|
-
def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
123
|
+
async def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
122
124
|
command = get_command_param(user_input, RUN_CLI_CMD)
|
|
123
|
-
|
|
124
|
-
command,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
text=True,
|
|
125
|
+
cmd_result, return_code = await run_command(
|
|
126
|
+
[CFG.DEFAULT_SHELL, "-c", command],
|
|
127
|
+
print_method=_create_faint_print(ctx),
|
|
128
|
+
timeout=3600,
|
|
128
129
|
)
|
|
129
130
|
ctx.print(
|
|
130
131
|
render_markdown(
|
|
@@ -132,10 +133,14 @@ def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
|
132
133
|
f"`{command}`",
|
|
133
134
|
"\n".join(
|
|
134
135
|
[
|
|
135
|
-
make_markdown_section("📤 Stdout", result.stdout, as_code=True),
|
|
136
|
-
make_markdown_section("🚫 Stderr", result.stderr, as_code=True),
|
|
137
136
|
make_markdown_section(
|
|
138
|
-
"
|
|
137
|
+
"📤 Stdout", cmd_result.output, as_code=True
|
|
138
|
+
),
|
|
139
|
+
make_markdown_section(
|
|
140
|
+
"🚫 Stderr", cmd_result.error, as_code=True
|
|
141
|
+
),
|
|
142
|
+
make_markdown_section(
|
|
143
|
+
"🎯 Return code", f"Return Code: {return_code}"
|
|
139
144
|
),
|
|
140
145
|
]
|
|
141
146
|
),
|
|
@@ -146,8 +151,20 @@ def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
|
146
151
|
ctx.print("", plain=True)
|
|
147
152
|
|
|
148
153
|
|
|
154
|
+
def _create_faint_print(ctx: AnyContext) -> Callable[..., None]:
|
|
155
|
+
def print_faint(text: str):
|
|
156
|
+
ctx.print(stylize_faint(f" {text}"), plain=True)
|
|
157
|
+
|
|
158
|
+
return print_faint
|
|
159
|
+
|
|
160
|
+
|
|
149
161
|
def get_new_yolo_mode(old_yolo_mode: str | bool, user_input: str) -> str | bool:
|
|
150
|
-
|
|
162
|
+
if not is_command_match(user_input, YOLO_CMD):
|
|
163
|
+
return old_yolo_mode
|
|
164
|
+
if is_command_match(user_input, YOLO_CMD, SET_SUB_CMD):
|
|
165
|
+
new_yolo_mode = get_command_param(user_input, YOLO_CMD, SET_SUB_CMD)
|
|
166
|
+
else:
|
|
167
|
+
new_yolo_mode = get_command_param(user_input, YOLO_CMD)
|
|
151
168
|
if new_yolo_mode != "":
|
|
152
169
|
if new_yolo_mode in TRUE_STRS or new_yolo_mode in FALSE_STRS:
|
|
153
170
|
return to_boolean(new_yolo_mode)
|
zrb/builtin/llm/chat_trigger.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
from asyncio import StreamReader
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
|
5
5
|
|
|
6
|
+
from zrb.builtin.llm.chat_completion import get_chat_completer
|
|
6
7
|
from zrb.context.any_context import AnyContext
|
|
7
8
|
from zrb.util.run import run_async
|
|
8
9
|
|
|
@@ -57,13 +58,11 @@ class LLMChatTrigger:
|
|
|
57
58
|
"""Reads one line of input using the provided reader."""
|
|
58
59
|
from prompt_toolkit import PromptSession
|
|
59
60
|
|
|
60
|
-
from zrb.builtin.llm.chat_completion import ChatCompleter
|
|
61
|
-
|
|
62
61
|
try:
|
|
63
62
|
if isinstance(reader, PromptSession):
|
|
64
|
-
bottom_toolbar = f"
|
|
63
|
+
bottom_toolbar = f"📌 Current directory: {os.getcwd()}"
|
|
65
64
|
return await reader.prompt_async(
|
|
66
|
-
completer=
|
|
65
|
+
completer=get_chat_completer(), bottom_toolbar=bottom_toolbar
|
|
67
66
|
)
|
|
68
67
|
line_bytes = await reader.readline()
|
|
69
68
|
if not line_bytes:
|
zrb/builtin/llm/tool/cli.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import asyncio
|
|
2
2
|
import sys
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from zrb.config.config import CFG
|
|
6
|
+
from zrb.context.any_context import AnyContext
|
|
7
|
+
from zrb.util.cli.style import stylize_faint
|
|
8
|
+
from zrb.util.cmd.command import run_command
|
|
3
9
|
|
|
4
10
|
if sys.version_info >= (3, 12):
|
|
5
11
|
from typing import TypedDict
|
|
@@ -15,38 +21,63 @@ class ShellCommandResult(TypedDict):
|
|
|
15
21
|
return_code: The return code, 0 indicating no error
|
|
16
22
|
stdout: Standard output
|
|
17
23
|
stderr: Standard error
|
|
24
|
+
display: Combination of standard output and standard error, interlaced
|
|
18
25
|
"""
|
|
19
26
|
|
|
20
27
|
return_code: int
|
|
21
28
|
stdout: str
|
|
22
29
|
stderr: str
|
|
30
|
+
display: str
|
|
23
31
|
|
|
24
32
|
|
|
25
|
-
def run_shell_command(
|
|
33
|
+
async def run_shell_command(
|
|
34
|
+
ctx: AnyContext, command: str, timeout: int = 30
|
|
35
|
+
) -> ShellCommandResult:
|
|
26
36
|
"""
|
|
27
37
|
Executes a non-interactive shell command on the user's machine.
|
|
28
38
|
|
|
39
|
+
**EFFICIENCY TIP:**
|
|
40
|
+
Combine multiple shell commands into a single call using `&&` or `;` to save steps.
|
|
41
|
+
Example: `mkdir new_dir && cd new_dir && touch file.txt`
|
|
42
|
+
|
|
29
43
|
CRITICAL: This tool runs with user-level permissions. Explain commands that modify
|
|
30
44
|
the system (e.g., `git`, `pip`) and ask for confirmation.
|
|
31
45
|
IMPORTANT: Long-running processes should be run in the background (e.g., `command &`).
|
|
32
46
|
|
|
33
47
|
Example:
|
|
34
|
-
run_shell_command(command='ls -l')
|
|
48
|
+
run_shell_command(command='ls -l', timeout=30)
|
|
35
49
|
|
|
36
50
|
Args:
|
|
37
51
|
command (str): The exact shell command to be executed.
|
|
52
|
+
timeout (int): The maximum time in seconds to wait for the command to finish.
|
|
53
|
+
Defaults to 30.
|
|
38
54
|
|
|
39
55
|
Returns:
|
|
40
56
|
dict: return_code, stdout, and stderr.
|
|
41
57
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
try:
|
|
59
|
+
cmd_result, return_code = await run_command(
|
|
60
|
+
[CFG.DEFAULT_SHELL, "-c", command],
|
|
61
|
+
print_method=_create_faint_print(ctx),
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
)
|
|
64
|
+
return {
|
|
65
|
+
"return_code": return_code,
|
|
66
|
+
"stdout": cmd_result.output,
|
|
67
|
+
"stderr": cmd_result.error,
|
|
68
|
+
"display": cmd_result.display,
|
|
69
|
+
}
|
|
70
|
+
except asyncio.TimeoutError:
|
|
71
|
+
return {
|
|
72
|
+
"return_code": 124,
|
|
73
|
+
"stdout": "",
|
|
74
|
+
"stderr": f"Command timeout after {timeout} seconds",
|
|
75
|
+
"display": f"Command timeout after {timeout} seconds",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _create_faint_print(ctx: AnyContext) -> Callable[..., None]:
|
|
80
|
+
def print_faint(text: str):
|
|
81
|
+
ctx.print(stylize_faint(f" {text}"), plain=True)
|
|
82
|
+
|
|
83
|
+
return print_faint
|