zrb 1.21.17__py3-none-any.whl → 1.21.33__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.
- zrb/attr/type.py +10 -7
- zrb/builtin/git.py +12 -1
- zrb/builtin/llm/chat_completion.py +287 -0
- zrb/builtin/llm/chat_session_cmd.py +90 -28
- zrb/builtin/llm/chat_trigger.py +6 -1
- zrb/builtin/llm/tool/cli.py +29 -13
- zrb/builtin/llm/tool/code.py +9 -1
- zrb/builtin/llm/tool/file.py +32 -6
- zrb/builtin/llm/tool/note.py +9 -9
- 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 +30 -10
- zrb/builtin/llm/tool/web.py +17 -72
- zrb/config/config.py +67 -26
- zrb/config/default_prompt/interactive_system_prompt.md +16 -13
- zrb/config/default_prompt/summarization_prompt.md +54 -8
- zrb/config/default_prompt/system_prompt.md +16 -18
- zrb/config/llm_rate_limitter.py +15 -6
- zrb/input/option_input.py +13 -1
- zrb/task/llm/agent.py +42 -143
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/conversation_history.py +35 -24
- zrb/task/llm/conversation_history_model.py +4 -11
- zrb/task/llm/history_processor.py +206 -0
- zrb/task/llm/history_summarization.py +2 -179
- zrb/task/llm/print_node.py +14 -5
- zrb/task/llm/prompt.py +2 -17
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_confirmation_completer.py +41 -0
- zrb/task/llm/tool_wrapper.py +15 -11
- zrb/task/llm_task.py +41 -40
- zrb/util/attr.py +12 -7
- zrb/util/git.py +2 -2
- zrb/xcom/xcom.py +10 -0
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/METADATA +3 -3
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/RECORD +40 -32
- zrb/task/llm/history_summarization_tool.py +0 -24
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/WHEEL +0 -0
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/entry_points.txt +0 -0
zrb/attr/type.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from typing import Any, Callable
|
|
2
2
|
|
|
3
3
|
from zrb.context.any_context import AnyContext
|
|
4
|
+
from zrb.context.any_shared_context import AnySharedContext
|
|
4
5
|
|
|
5
6
|
fstring = str
|
|
6
|
-
AnyAttr = Any | fstring | Callable[[AnyContext], Any]
|
|
7
|
-
StrAttr = str | fstring | Callable[[AnyContext], str | None]
|
|
8
|
-
BoolAttr = bool | fstring | Callable[[AnyContext], bool | None]
|
|
9
|
-
IntAttr = int | fstring | Callable[[AnyContext], int | None]
|
|
10
|
-
FloatAttr = float | fstring | Callable[[AnyContext], float | None]
|
|
11
|
-
StrDictAttr =
|
|
12
|
-
|
|
7
|
+
AnyAttr = Any | fstring | Callable[[AnyContext | AnySharedContext], Any]
|
|
8
|
+
StrAttr = str | fstring | Callable[[AnyContext | AnySharedContext], str | None]
|
|
9
|
+
BoolAttr = bool | fstring | Callable[[AnyContext | AnySharedContext], bool | None]
|
|
10
|
+
IntAttr = int | fstring | Callable[[AnyContext | AnySharedContext], int | None]
|
|
11
|
+
FloatAttr = float | fstring | Callable[[AnyContext | AnySharedContext], float | None]
|
|
12
|
+
StrDictAttr = (
|
|
13
|
+
dict[str, StrAttr] | Callable[[AnyContext | AnySharedContext], dict[str, Any]]
|
|
14
|
+
)
|
|
15
|
+
StrListAttr = list[StrAttr] | Callable[[AnyContext | AnySharedContext], list[str]]
|
zrb/builtin/git.py
CHANGED
|
@@ -82,6 +82,12 @@ async def get_git_diff(ctx: AnyContext):
|
|
|
82
82
|
|
|
83
83
|
@make_task(
|
|
84
84
|
name="prune-local-git-branches",
|
|
85
|
+
input=StrInput(
|
|
86
|
+
name="preserved-branch",
|
|
87
|
+
description="Branches to be preserved",
|
|
88
|
+
prompt="Branches to be preserved, comma separated",
|
|
89
|
+
default="master,main,dev,develop",
|
|
90
|
+
),
|
|
85
91
|
description="🧹 Prune local branches",
|
|
86
92
|
group=git_branch_group,
|
|
87
93
|
alias="prune",
|
|
@@ -93,8 +99,13 @@ async def prune_local_branches(ctx: AnyContext):
|
|
|
93
99
|
branches = await get_branches(repo_dir, print_method=ctx.print)
|
|
94
100
|
ctx.print(stylize_faint("Get current branch"))
|
|
95
101
|
current_branch = await get_current_branch(repo_dir, print_method=ctx.print)
|
|
102
|
+
preserved_branches = [
|
|
103
|
+
branch.strip()
|
|
104
|
+
for branch in ctx.input.preserved_branch.split(",")
|
|
105
|
+
if branch.strip() != ""
|
|
106
|
+
]
|
|
96
107
|
for branch in branches:
|
|
97
|
-
if branch == current_branch or branch
|
|
108
|
+
if branch == current_branch or branch in preserved_branches:
|
|
98
109
|
continue
|
|
99
110
|
ctx.print(stylize_faint(f"Removing local branch: {branch}"))
|
|
100
111
|
try:
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from zrb.builtin.llm.chat_session_cmd import (
|
|
5
|
+
ADD_SUB_CMD,
|
|
6
|
+
ATTACHMENT_ADD_SUB_CMD_DESC,
|
|
7
|
+
ATTACHMENT_CLEAR_SUB_CMD_DESC,
|
|
8
|
+
ATTACHMENT_CMD,
|
|
9
|
+
ATTACHMENT_CMD_DESC,
|
|
10
|
+
ATTACHMENT_SET_SUB_CMD_DESC,
|
|
11
|
+
CLEAR_SUB_CMD,
|
|
12
|
+
HELP_CMD,
|
|
13
|
+
HELP_CMD_DESC,
|
|
14
|
+
MULTILINE_END_CMD,
|
|
15
|
+
MULTILINE_END_CMD_DESC,
|
|
16
|
+
MULTILINE_START_CMD,
|
|
17
|
+
MULTILINE_START_CMD_DESC,
|
|
18
|
+
QUIT_CMD,
|
|
19
|
+
QUIT_CMD_DESC,
|
|
20
|
+
RUN_CLI_CMD,
|
|
21
|
+
RUN_CLI_CMD_DESC,
|
|
22
|
+
SAVE_CMD,
|
|
23
|
+
SAVE_CMD_DESC,
|
|
24
|
+
SET_SUB_CMD,
|
|
25
|
+
WORKFLOW_ADD_SUB_CMD_DESC,
|
|
26
|
+
WORKFLOW_CLEAR_SUB_CMD_DESC,
|
|
27
|
+
WORKFLOW_CMD,
|
|
28
|
+
WORKFLOW_CMD_DESC,
|
|
29
|
+
WORKFLOW_SET_SUB_CMD_DESC,
|
|
30
|
+
YOLO_CMD,
|
|
31
|
+
YOLO_CMD_DESC,
|
|
32
|
+
YOLO_SET_CMD_DESC,
|
|
33
|
+
YOLO_SET_FALSE_CMD_DESC,
|
|
34
|
+
YOLO_SET_TRUE_CMD_DESC,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from prompt_toolkit.completion import Completer
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_chat_completer() -> "Completer":
|
|
42
|
+
|
|
43
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
44
|
+
from prompt_toolkit.document import Document
|
|
45
|
+
|
|
46
|
+
class ChatCompleter(Completer):
|
|
47
|
+
|
|
48
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent):
|
|
49
|
+
# Slash command
|
|
50
|
+
for completion in self._complete_slash_command(document):
|
|
51
|
+
yield completion
|
|
52
|
+
for completion in self._complete_slash_file_command(document):
|
|
53
|
+
yield completion
|
|
54
|
+
# Appendix
|
|
55
|
+
for completion in self._complete_appendix(document):
|
|
56
|
+
yield completion
|
|
57
|
+
|
|
58
|
+
def _complete_slash_file_command(self, document: Document):
|
|
59
|
+
text = document.text_before_cursor
|
|
60
|
+
prefixes = []
|
|
61
|
+
for cmd in ATTACHMENT_CMD:
|
|
62
|
+
for subcmd in ADD_SUB_CMD:
|
|
63
|
+
prefixes.append(f"{cmd} {subcmd} ")
|
|
64
|
+
for prefix in prefixes:
|
|
65
|
+
if text.startswith(prefix):
|
|
66
|
+
pattern = text[len(prefix) :]
|
|
67
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
68
|
+
for prefixed_option in [
|
|
69
|
+
f"{prefix}{option}" for option in potential_options
|
|
70
|
+
]:
|
|
71
|
+
yield Completion(
|
|
72
|
+
prefixed_option,
|
|
73
|
+
start_position=-len(text),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _complete_slash_command(self, document: Document):
|
|
77
|
+
text = document.text_before_cursor
|
|
78
|
+
if not text.startswith("/"):
|
|
79
|
+
return
|
|
80
|
+
for command, description in self._get_cmd_options().items():
|
|
81
|
+
if command.lower().startswith(text.lower()):
|
|
82
|
+
yield Completion(
|
|
83
|
+
command,
|
|
84
|
+
start_position=-len(text),
|
|
85
|
+
display_meta=description,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _complete_appendix(self, document: Document):
|
|
89
|
+
token = document.get_word_before_cursor(WORD=True)
|
|
90
|
+
prefix = "@"
|
|
91
|
+
if not token.startswith(prefix):
|
|
92
|
+
return
|
|
93
|
+
pattern = token[len(prefix) :]
|
|
94
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
95
|
+
for prefixed_option in [
|
|
96
|
+
f"{prefix}{option}" for option in potential_options
|
|
97
|
+
]:
|
|
98
|
+
yield Completion(
|
|
99
|
+
prefixed_option,
|
|
100
|
+
start_position=-len(token),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _get_cmd_options(self):
|
|
104
|
+
cmd_options = {}
|
|
105
|
+
# Add all commands with their descriptions
|
|
106
|
+
for cmd in MULTILINE_START_CMD:
|
|
107
|
+
cmd_options[cmd] = MULTILINE_START_CMD_DESC
|
|
108
|
+
for cmd in MULTILINE_END_CMD:
|
|
109
|
+
cmd_options[cmd] = MULTILINE_END_CMD_DESC
|
|
110
|
+
for cmd in QUIT_CMD:
|
|
111
|
+
cmd_options[cmd] = QUIT_CMD_DESC
|
|
112
|
+
for cmd in WORKFLOW_CMD:
|
|
113
|
+
cmd_options[cmd] = WORKFLOW_CMD_DESC
|
|
114
|
+
for subcmd in ADD_SUB_CMD:
|
|
115
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_ADD_SUB_CMD_DESC
|
|
116
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
117
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_CLEAR_SUB_CMD_DESC
|
|
118
|
+
for subcmd in SET_SUB_CMD:
|
|
119
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_SET_SUB_CMD_DESC
|
|
120
|
+
for cmd in SAVE_CMD:
|
|
121
|
+
cmd_options[cmd] = SAVE_CMD_DESC
|
|
122
|
+
for cmd in ATTACHMENT_CMD:
|
|
123
|
+
cmd_options[cmd] = ATTACHMENT_CMD_DESC
|
|
124
|
+
for subcmd in ADD_SUB_CMD:
|
|
125
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_ADD_SUB_CMD_DESC
|
|
126
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
127
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_CLEAR_SUB_CMD_DESC
|
|
128
|
+
for subcmd in SET_SUB_CMD:
|
|
129
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_SET_SUB_CMD_DESC
|
|
130
|
+
for cmd in YOLO_CMD:
|
|
131
|
+
cmd_options[cmd] = YOLO_CMD_DESC
|
|
132
|
+
for subcmd in SET_SUB_CMD:
|
|
133
|
+
cmd_options[f"{cmd} {subcmd} true"] = YOLO_SET_TRUE_CMD_DESC
|
|
134
|
+
cmd_options[f"{cmd} {subcmd} false"] = YOLO_SET_FALSE_CMD_DESC
|
|
135
|
+
cmd_options[f"{cmd} {subcmd}"] = YOLO_SET_CMD_DESC
|
|
136
|
+
for cmd in HELP_CMD:
|
|
137
|
+
cmd_options[cmd] = HELP_CMD_DESC
|
|
138
|
+
for cmd in RUN_CLI_CMD:
|
|
139
|
+
cmd_options[cmd] = RUN_CLI_CMD_DESC
|
|
140
|
+
return dict(sorted(cmd_options.items()))
|
|
141
|
+
|
|
142
|
+
def _fuzzy_path_search(
|
|
143
|
+
self,
|
|
144
|
+
pattern: str,
|
|
145
|
+
root: str | None = None,
|
|
146
|
+
max_results: int = 20,
|
|
147
|
+
include_hidden: bool = False,
|
|
148
|
+
case_sensitive: bool = False,
|
|
149
|
+
dirs: bool = True,
|
|
150
|
+
files: bool = True,
|
|
151
|
+
) -> list[str]:
|
|
152
|
+
"""
|
|
153
|
+
Return a list of filesystem paths under `root` that fuzzy-match `pattern`.
|
|
154
|
+
- pattern: e.g. "./some/x" or "proj util/io"
|
|
155
|
+
- include_hidden: if False skip files/dirs starting with '.'
|
|
156
|
+
- dirs/files booleans let you restrict results
|
|
157
|
+
- returns list of relative paths (from root), sorted best-first
|
|
158
|
+
"""
|
|
159
|
+
search_pattern = pattern
|
|
160
|
+
if root is None:
|
|
161
|
+
# Determine root and adjust pattern if necessary
|
|
162
|
+
expanded_pattern = os.path.expanduser(pattern)
|
|
163
|
+
if os.path.isabs(expanded_pattern) or pattern.startswith("~"):
|
|
164
|
+
# For absolute paths, find the deepest existing directory
|
|
165
|
+
if os.path.isdir(expanded_pattern):
|
|
166
|
+
root = expanded_pattern
|
|
167
|
+
search_pattern = ""
|
|
168
|
+
else:
|
|
169
|
+
root = os.path.dirname(expanded_pattern)
|
|
170
|
+
while root and not os.path.isdir(root) and len(root) > 1:
|
|
171
|
+
root = os.path.dirname(root)
|
|
172
|
+
if not os.path.isdir(root):
|
|
173
|
+
root = "." # Fallback
|
|
174
|
+
search_pattern = pattern
|
|
175
|
+
else:
|
|
176
|
+
try:
|
|
177
|
+
search_pattern = os.path.relpath(expanded_pattern, root)
|
|
178
|
+
if search_pattern == ".":
|
|
179
|
+
search_pattern = ""
|
|
180
|
+
except ValueError:
|
|
181
|
+
search_pattern = os.path.basename(pattern)
|
|
182
|
+
else:
|
|
183
|
+
root = "."
|
|
184
|
+
search_pattern = pattern
|
|
185
|
+
# Normalize pattern -> tokens split on path separators or whitespace
|
|
186
|
+
search_pattern = search_pattern.strip()
|
|
187
|
+
if search_pattern:
|
|
188
|
+
raw_tokens = [t for t in search_pattern.split(os.path.sep) if t]
|
|
189
|
+
else:
|
|
190
|
+
raw_tokens = []
|
|
191
|
+
# prepare tokens (case)
|
|
192
|
+
if not case_sensitive:
|
|
193
|
+
tokens = [t.lower() for t in raw_tokens]
|
|
194
|
+
else:
|
|
195
|
+
tokens = raw_tokens
|
|
196
|
+
# specific ignore list
|
|
197
|
+
try:
|
|
198
|
+
is_recursive = os.path.abspath(os.path.expanduser(root)).startswith(
|
|
199
|
+
os.path.abspath(os.getcwd())
|
|
200
|
+
)
|
|
201
|
+
except Exception:
|
|
202
|
+
is_recursive = False
|
|
203
|
+
# walk filesystem
|
|
204
|
+
candidates: list[tuple[float, str]] = []
|
|
205
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
206
|
+
# Filter directories
|
|
207
|
+
if not include_hidden:
|
|
208
|
+
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
209
|
+
rel_dir = os.path.relpath(dirpath, root)
|
|
210
|
+
# treat '.' as empty prefix
|
|
211
|
+
if rel_dir == ".":
|
|
212
|
+
rel_dir = ""
|
|
213
|
+
# build list of entries to test depending on files/dirs flags
|
|
214
|
+
entries = []
|
|
215
|
+
if dirs:
|
|
216
|
+
entries.extend([os.path.join(rel_dir, d) for d in dirnames])
|
|
217
|
+
if files:
|
|
218
|
+
entries.extend([os.path.join(rel_dir, f) for f in filenames])
|
|
219
|
+
if not is_recursive:
|
|
220
|
+
dirnames[:] = []
|
|
221
|
+
for ent in entries:
|
|
222
|
+
# Normalize presentation: use ./ prefix for relative paths
|
|
223
|
+
display_path = ent if ent else "."
|
|
224
|
+
# Skip hidden entries unless requested (double check for rel path segments)
|
|
225
|
+
if not include_hidden:
|
|
226
|
+
if any(
|
|
227
|
+
seg.startswith(".")
|
|
228
|
+
for seg in display_path.split(os.sep)
|
|
229
|
+
if seg
|
|
230
|
+
):
|
|
231
|
+
continue
|
|
232
|
+
cand = display_path.replace(os.sep, "/") # unify separator
|
|
233
|
+
cand_cmp = cand if case_sensitive else cand.lower()
|
|
234
|
+
last_pos = 0
|
|
235
|
+
score = 0.0
|
|
236
|
+
matched_all = True
|
|
237
|
+
for token in tokens:
|
|
238
|
+
# try contiguous substring search first
|
|
239
|
+
idx = cand_cmp.find(token, last_pos)
|
|
240
|
+
if idx != -1:
|
|
241
|
+
# good match: reward contiguous early matches
|
|
242
|
+
score += idx # smaller idx preferred
|
|
243
|
+
last_pos = idx + len(token)
|
|
244
|
+
else:
|
|
245
|
+
# fallback to subsequence matching
|
|
246
|
+
pos = self._find_subsequence_pos(cand_cmp, token, last_pos)
|
|
247
|
+
if pos is None:
|
|
248
|
+
matched_all = False
|
|
249
|
+
break
|
|
250
|
+
# subsequence match is less preferred than contiguous substring
|
|
251
|
+
score += pos + 0.5 * len(token)
|
|
252
|
+
last_pos = pos + len(token)
|
|
253
|
+
if matched_all:
|
|
254
|
+
# prefer shorter paths when score ties, so include length as tiebreaker
|
|
255
|
+
score += 0.01 * len(cand)
|
|
256
|
+
out = (
|
|
257
|
+
cand
|
|
258
|
+
if os.path.abspath(cand) == cand
|
|
259
|
+
else os.path.join(root, cand)
|
|
260
|
+
)
|
|
261
|
+
candidates.append((score, out))
|
|
262
|
+
# sort by score then lexicographically and return top results
|
|
263
|
+
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
264
|
+
return [p for _, p in candidates[:max_results]]
|
|
265
|
+
|
|
266
|
+
def _find_subsequence_pos(
|
|
267
|
+
self, hay: str, needle: str, start: int = 0
|
|
268
|
+
) -> int | None:
|
|
269
|
+
"""
|
|
270
|
+
Try to locate needle in hay as a subsequence starting at `start`.
|
|
271
|
+
Returns the index of the first matched character of the subsequence or None if not
|
|
272
|
+
match.
|
|
273
|
+
"""
|
|
274
|
+
if not needle:
|
|
275
|
+
return start
|
|
276
|
+
i = start
|
|
277
|
+
j = 0
|
|
278
|
+
first_pos = None
|
|
279
|
+
while i < len(hay) and j < len(needle):
|
|
280
|
+
if hay[i] == needle[j]:
|
|
281
|
+
if first_pos is None:
|
|
282
|
+
first_pos = i
|
|
283
|
+
j += 1
|
|
284
|
+
i += 1
|
|
285
|
+
return first_pos if j == len(needle) else None
|
|
286
|
+
|
|
287
|
+
return ChatCompleter()
|
|
@@ -12,13 +12,14 @@ from zrb.util.cli.style import (
|
|
|
12
12
|
)
|
|
13
13
|
from zrb.util.file import write_file
|
|
14
14
|
from zrb.util.markdown import make_markdown_section
|
|
15
|
+
from zrb.util.string.conversion import FALSE_STRS, TRUE_STRS, to_boolean
|
|
15
16
|
|
|
16
17
|
MULTILINE_START_CMD = ["/multi", "/multiline"]
|
|
17
18
|
MULTILINE_END_CMD = ["/end"]
|
|
18
19
|
QUIT_CMD = ["/bye", "/quit", "/q", "/exit"]
|
|
19
20
|
WORKFLOW_CMD = ["/workflow", "/workflows", "/skill", "/skills", "/w"]
|
|
20
21
|
SAVE_CMD = ["/save", "/s"]
|
|
21
|
-
ATTACHMENT_CMD = ["/
|
|
22
|
+
ATTACHMENT_CMD = ["/attachment", "/attachments", "/attach"]
|
|
22
23
|
YOLO_CMD = ["/yolo"]
|
|
23
24
|
HELP_CMD = ["/help", "/info"]
|
|
24
25
|
ADD_SUB_CMD = ["add"]
|
|
@@ -26,6 +27,39 @@ SET_SUB_CMD = ["set"]
|
|
|
26
27
|
CLEAR_SUB_CMD = ["clear"]
|
|
27
28
|
RUN_CLI_CMD = ["/run", "/exec", "/execute", "/cmd", "/cli", "!"]
|
|
28
29
|
|
|
30
|
+
# Command display constants
|
|
31
|
+
MULTILINE_START_CMD_DESC = "Start multiline input"
|
|
32
|
+
MULTILINE_END_CMD_DESC = "End multiline input"
|
|
33
|
+
QUIT_CMD_DESC = "Quit from chat session"
|
|
34
|
+
WORKFLOW_CMD_DESC = "Show active workflows"
|
|
35
|
+
WORKFLOW_ADD_SUB_CMD_DESC = (
|
|
36
|
+
"Add active workflow "
|
|
37
|
+
f"(e.g., `{WORKFLOW_CMD[0]} {ADD_SUB_CMD[0]} coding,researching`)"
|
|
38
|
+
)
|
|
39
|
+
WORKFLOW_SET_SUB_CMD_DESC = (
|
|
40
|
+
"Set active workflows " f"(e.g., `{WORKFLOW_CMD[0]} {SET_SUB_CMD[0]} coding,`)"
|
|
41
|
+
)
|
|
42
|
+
WORKFLOW_CLEAR_SUB_CMD_DESC = "Deactivate all workflows"
|
|
43
|
+
SAVE_CMD_DESC = f"Save last response to a file (e.g., `{SAVE_CMD[0]} conclusion.md`)"
|
|
44
|
+
ATTACHMENT_CMD_DESC = "Show current attachment"
|
|
45
|
+
ATTACHMENT_ADD_SUB_CMD_DESC = (
|
|
46
|
+
"Attach a file " f"(e.g., `{ATTACHMENT_CMD[0]} {ADD_SUB_CMD[0]} ./logo.png`)"
|
|
47
|
+
)
|
|
48
|
+
ATTACHMENT_SET_SUB_CMD_DESC = (
|
|
49
|
+
"Set attachments "
|
|
50
|
+
f"(e.g., `{ATTACHMENT_CMD[0]} {SET_SUB_CMD[0]} ./logo.png,./diagram.png`)"
|
|
51
|
+
)
|
|
52
|
+
ATTACHMENT_CLEAR_SUB_CMD_DESC = "Clear attachment"
|
|
53
|
+
YOLO_CMD_DESC = "Show/manipulate current YOLO mode"
|
|
54
|
+
YOLO_SET_CMD_DESC = (
|
|
55
|
+
"Assign YOLO tools "
|
|
56
|
+
f"(e.g., `{YOLO_CMD[0]} {SET_SUB_CMD[0]} read_from_file,analyze_file`)"
|
|
57
|
+
)
|
|
58
|
+
YOLO_SET_TRUE_CMD_DESC = "Activate YOLO mode for all tools"
|
|
59
|
+
YOLO_SET_FALSE_CMD_DESC = "Deactivate YOLO mode for all tools"
|
|
60
|
+
RUN_CLI_CMD_DESC = "Run a non-interactive CLI command"
|
|
61
|
+
HELP_CMD_DESC = "Show info/help"
|
|
62
|
+
|
|
29
63
|
|
|
30
64
|
def print_current_yolo_mode(
|
|
31
65
|
ctx: AnyContext, current_yolo_mode_value: str | bool
|
|
@@ -115,34 +149,54 @@ def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
|
115
149
|
def get_new_yolo_mode(old_yolo_mode: str | bool, user_input: str) -> str | bool:
|
|
116
150
|
new_yolo_mode = get_command_param(user_input, YOLO_CMD)
|
|
117
151
|
if new_yolo_mode != "":
|
|
152
|
+
if new_yolo_mode in TRUE_STRS or new_yolo_mode in FALSE_STRS:
|
|
153
|
+
return to_boolean(new_yolo_mode)
|
|
118
154
|
return new_yolo_mode
|
|
119
|
-
|
|
155
|
+
if isinstance(old_yolo_mode, bool):
|
|
156
|
+
return old_yolo_mode
|
|
157
|
+
return _normalize_comma_separated_str(old_yolo_mode)
|
|
120
158
|
|
|
121
159
|
|
|
122
160
|
def get_new_attachments(old_attachment: str, user_input: str) -> str:
|
|
123
161
|
if not is_command_match(user_input, ATTACHMENT_CMD):
|
|
124
|
-
return old_attachment
|
|
162
|
+
return _normalize_comma_separated_str(old_attachment)
|
|
125
163
|
if is_command_match(user_input, ATTACHMENT_CMD, SET_SUB_CMD):
|
|
126
|
-
return
|
|
164
|
+
return _normalize_comma_separated_str(
|
|
165
|
+
get_command_param(user_input, ATTACHMENT_CMD, SET_SUB_CMD)
|
|
166
|
+
)
|
|
127
167
|
if is_command_match(user_input, ATTACHMENT_CMD, CLEAR_SUB_CMD):
|
|
128
168
|
return ""
|
|
129
169
|
if is_command_match(user_input, ATTACHMENT_CMD, ADD_SUB_CMD):
|
|
130
170
|
new_attachment = get_command_param(user_input, ATTACHMENT_CMD, ADD_SUB_CMD)
|
|
131
|
-
return
|
|
171
|
+
return _normalize_comma_separated_str(
|
|
172
|
+
",".join([old_attachment, new_attachment])
|
|
173
|
+
)
|
|
132
174
|
return old_attachment
|
|
133
175
|
|
|
134
176
|
|
|
135
177
|
def get_new_workflows(old_workflow: str, user_input: str) -> str:
|
|
136
178
|
if not is_command_match(user_input, WORKFLOW_CMD):
|
|
137
|
-
return old_workflow
|
|
179
|
+
return _normalize_comma_separated_str(old_workflow)
|
|
138
180
|
if is_command_match(user_input, WORKFLOW_CMD, SET_SUB_CMD):
|
|
139
|
-
return
|
|
181
|
+
return _normalize_comma_separated_str(
|
|
182
|
+
get_command_param(user_input, WORKFLOW_CMD, SET_SUB_CMD)
|
|
183
|
+
)
|
|
140
184
|
if is_command_match(user_input, WORKFLOW_CMD, CLEAR_SUB_CMD):
|
|
141
185
|
return ""
|
|
142
186
|
if is_command_match(user_input, WORKFLOW_CMD, ADD_SUB_CMD):
|
|
143
187
|
new_workflow = get_command_param(user_input, WORKFLOW_CMD, ADD_SUB_CMD)
|
|
144
|
-
return ",".join([old_workflow, new_workflow])
|
|
145
|
-
return old_workflow
|
|
188
|
+
return _normalize_comma_separated_str(",".join([old_workflow, new_workflow]))
|
|
189
|
+
return _normalize_comma_separated_str(old_workflow)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _normalize_comma_separated_str(comma_separated_str: str) -> str:
|
|
193
|
+
return ",".join(
|
|
194
|
+
[
|
|
195
|
+
workflow_name.strip()
|
|
196
|
+
for workflow_name in comma_separated_str.split(",")
|
|
197
|
+
if workflow_name.strip() != ""
|
|
198
|
+
]
|
|
199
|
+
)
|
|
146
200
|
|
|
147
201
|
|
|
148
202
|
def get_command_param(user_input: str, *cmd_patterns: list[str]) -> str:
|
|
@@ -175,31 +229,39 @@ def print_commands(ctx: AnyContext):
|
|
|
175
229
|
ctx.print(
|
|
176
230
|
"\n".join(
|
|
177
231
|
[
|
|
178
|
-
_show_command(
|
|
179
|
-
_show_command(
|
|
180
|
-
_show_command(
|
|
181
|
-
_show_command(
|
|
182
|
-
_show_subcommand("add", "<new-attachment>", "Attach a file"),
|
|
232
|
+
_show_command(QUIT_CMD[0], QUIT_CMD_DESC),
|
|
233
|
+
_show_command(MULTILINE_START_CMD[0], MULTILINE_START_CMD_DESC),
|
|
234
|
+
_show_command(MULTILINE_END_CMD[0], MULTILINE_END_CMD_DESC),
|
|
235
|
+
_show_command(ATTACHMENT_CMD[0], ATTACHMENT_CMD_DESC),
|
|
183
236
|
_show_subcommand(
|
|
184
|
-
|
|
237
|
+
ADD_SUB_CMD[0], "<file-path>", ATTACHMENT_ADD_SUB_CMD_DESC
|
|
185
238
|
),
|
|
186
|
-
_show_subcommand("clear", "", "Clear attachment"),
|
|
187
|
-
_show_command("/workflow", "Show active workflows"),
|
|
188
|
-
_show_subcommand("add", "<workflow>", "Add active workflow"),
|
|
189
239
|
_show_subcommand(
|
|
190
|
-
|
|
240
|
+
SET_SUB_CMD[0],
|
|
241
|
+
"<file1-path,file2-path,...>",
|
|
242
|
+
ATTACHMENT_SET_SUB_CMD_DESC,
|
|
191
243
|
),
|
|
192
|
-
_show_subcommand(
|
|
193
|
-
_show_command(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
244
|
+
_show_subcommand(CLEAR_SUB_CMD[0], "", ATTACHMENT_CLEAR_SUB_CMD_DESC),
|
|
245
|
+
_show_command(WORKFLOW_CMD[0], WORKFLOW_CMD_DESC),
|
|
246
|
+
_show_subcommand(
|
|
247
|
+
ADD_SUB_CMD[0], "<workflow>", WORKFLOW_ADD_SUB_CMD_DESC
|
|
248
|
+
),
|
|
249
|
+
_show_subcommand(
|
|
250
|
+
SET_SUB_CMD[0],
|
|
251
|
+
"<workflow1,workflow2,..>",
|
|
252
|
+
WORKFLOW_SET_SUB_CMD_DESC,
|
|
197
253
|
),
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
254
|
+
_show_subcommand(CLEAR_SUB_CMD[0], "", WORKFLOW_CLEAR_SUB_CMD_DESC),
|
|
255
|
+
_show_command(f"{SAVE_CMD[0]}", SAVE_CMD_DESC),
|
|
256
|
+
_show_command(YOLO_CMD[0], YOLO_CMD_DESC),
|
|
257
|
+
_show_subcommand(SET_SUB_CMD[0], "true", YOLO_SET_TRUE_CMD_DESC),
|
|
258
|
+
_show_subcommand(SET_SUB_CMD[0], "false", YOLO_SET_FALSE_CMD_DESC),
|
|
259
|
+
_show_subcommand(
|
|
260
|
+
SET_SUB_CMD[0], "<tool1,tool2,tool2>", YOLO_SET_CMD_DESC
|
|
201
261
|
),
|
|
202
|
-
_show_command(
|
|
262
|
+
_show_command(RUN_CLI_CMD[0], ""),
|
|
263
|
+
_show_command_param("<cli-command>", RUN_CLI_CMD_DESC),
|
|
264
|
+
_show_command(HELP_CMD[0], HELP_CMD_DESC),
|
|
203
265
|
]
|
|
204
266
|
),
|
|
205
267
|
plain=True,
|
zrb/builtin/llm/chat_trigger.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import os
|
|
2
3
|
from asyncio import StreamReader
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
|
4
5
|
|
|
6
|
+
from zrb.builtin.llm.chat_completion import get_chat_completer
|
|
5
7
|
from zrb.context.any_context import AnyContext
|
|
6
8
|
from zrb.util.run import run_async
|
|
7
9
|
|
|
@@ -58,7 +60,10 @@ class LLMChatTrigger:
|
|
|
58
60
|
|
|
59
61
|
try:
|
|
60
62
|
if isinstance(reader, PromptSession):
|
|
61
|
-
|
|
63
|
+
bottom_toolbar = f"📁 Current directory: {os.getcwd()}"
|
|
64
|
+
return await reader.prompt_async(
|
|
65
|
+
completer=get_chat_completer(), bottom_toolbar=bottom_toolbar
|
|
66
|
+
)
|
|
62
67
|
line_bytes = await reader.readline()
|
|
63
68
|
if not line_bytes:
|
|
64
69
|
return "/bye" # Signal to exit
|
zrb/builtin/llm/tool/cli.py
CHANGED
|
@@ -22,31 +22,47 @@ class ShellCommandResult(TypedDict):
|
|
|
22
22
|
stderr: str
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def run_shell_command(command: str) -> ShellCommandResult:
|
|
25
|
+
def run_shell_command(command: str, timeout: int = 30) -> ShellCommandResult:
|
|
26
26
|
"""
|
|
27
27
|
Executes a non-interactive shell command on the user's machine.
|
|
28
28
|
|
|
29
|
+
**EFFICIENCY TIP:**
|
|
30
|
+
Combine multiple shell commands into a single call using `&&` or `;` to save steps.
|
|
31
|
+
Example: `mkdir new_dir && cd new_dir && touch file.txt`
|
|
32
|
+
|
|
29
33
|
CRITICAL: This tool runs with user-level permissions. Explain commands that modify
|
|
30
34
|
the system (e.g., `git`, `pip`) and ask for confirmation.
|
|
31
35
|
IMPORTANT: Long-running processes should be run in the background (e.g., `command &`).
|
|
32
36
|
|
|
33
37
|
Example:
|
|
34
|
-
run_shell_command(command='ls -l')
|
|
38
|
+
run_shell_command(command='ls -l', timeout=30)
|
|
35
39
|
|
|
36
40
|
Args:
|
|
37
41
|
command (str): The exact shell command to be executed.
|
|
42
|
+
timeout (int): The maximum time in seconds to wait for the command to finish.
|
|
43
|
+
Defaults to 30.
|
|
38
44
|
|
|
39
45
|
Returns:
|
|
40
46
|
dict: return_code, stdout, and stderr.
|
|
41
47
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
try:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
command,
|
|
51
|
+
shell=True,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
)
|
|
56
|
+
return {
|
|
57
|
+
"return_code": int(result.returncode),
|
|
58
|
+
"stdout": str(result.stdout or ""),
|
|
59
|
+
"stderr": str(result.stderr or ""),
|
|
60
|
+
}
|
|
61
|
+
except subprocess.TimeoutExpired as e:
|
|
62
|
+
stdout = e.stdout.decode() if isinstance(e.stdout, bytes) else (e.stdout or "")
|
|
63
|
+
stderr = e.stderr.decode() if isinstance(e.stderr, bytes) else (e.stderr or "")
|
|
64
|
+
return {
|
|
65
|
+
"return_code": 124,
|
|
66
|
+
"stdout": str(stdout),
|
|
67
|
+
"stderr": f"{stderr}\nError: Command timed out after {timeout} seconds".strip(),
|
|
68
|
+
}
|
zrb/builtin/llm/tool/code.py
CHANGED
|
@@ -59,8 +59,12 @@ async def analyze_repo(
|
|
|
59
59
|
"""
|
|
60
60
|
Analyzes a code repository or directory to answer a specific query.
|
|
61
61
|
|
|
62
|
-
CRITICAL: The
|
|
62
|
+
CRITICAL: The query must contain ALL necessary context, instructions, and information.
|
|
63
|
+
The sub-agent performing the analysis does NOT share your current conversation
|
|
64
|
+
history, memory, or global context.
|
|
65
|
+
The quality of analysis depends entirely on the query. Vague queries yield poor
|
|
63
66
|
results.
|
|
67
|
+
|
|
64
68
|
IMPORTANT: This tool can be slow and expensive on large repositories. Use judiciously.
|
|
65
69
|
|
|
66
70
|
Example:
|
|
@@ -149,6 +153,8 @@ async def _extract_info(
|
|
|
149
153
|
tool_name="extract",
|
|
150
154
|
tool_description="extract",
|
|
151
155
|
system_prompt=CFG.LLM_REPO_EXTRACTOR_SYSTEM_PROMPT,
|
|
156
|
+
auto_summarize=False,
|
|
157
|
+
remember_history=False,
|
|
152
158
|
)
|
|
153
159
|
extracted_infos = []
|
|
154
160
|
content_buffer = []
|
|
@@ -197,6 +203,8 @@ async def _summarize_info(
|
|
|
197
203
|
tool_name="extract",
|
|
198
204
|
tool_description="extract",
|
|
199
205
|
system_prompt=CFG.LLM_REPO_SUMMARIZER_SYSTEM_PROMPT,
|
|
206
|
+
auto_summarize=False,
|
|
207
|
+
remember_history=False,
|
|
200
208
|
)
|
|
201
209
|
summarized_infos = []
|
|
202
210
|
content_buffer = ""
|