zrb 1.21.6__py3-none-any.whl → 1.21.28__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/attr/type.py +10 -7
- zrb/builtin/git.py +12 -1
- zrb/builtin/llm/chat_completion.py +274 -0
- zrb/builtin/llm/chat_session_cmd.py +90 -28
- zrb/builtin/llm/chat_trigger.py +7 -1
- zrb/builtin/llm/history.py +4 -4
- zrb/builtin/llm/tool/api.py +3 -1
- zrb/builtin/llm/tool/cli.py +2 -1
- zrb/builtin/llm/tool/code.py +11 -3
- zrb/builtin/llm/tool/file.py +112 -142
- zrb/builtin/llm/tool/note.py +36 -16
- zrb/builtin/llm/tool/rag.py +17 -8
- zrb/builtin/llm/tool/sub_agent.py +41 -15
- zrb/config/config.py +108 -13
- zrb/config/default_prompt/file_extractor_system_prompt.md +16 -16
- zrb/config/default_prompt/interactive_system_prompt.md +11 -11
- zrb/config/default_prompt/repo_extractor_system_prompt.md +16 -16
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +3 -3
- zrb/config/default_prompt/summarization_prompt.md +54 -8
- zrb/config/default_prompt/system_prompt.md +15 -15
- zrb/config/llm_rate_limitter.py +24 -5
- zrb/input/option_input.py +13 -1
- zrb/task/llm/agent.py +42 -144
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/config.py +8 -7
- zrb/task/llm/conversation_history.py +35 -24
- zrb/task/llm/conversation_history_model.py +4 -11
- zrb/task/llm/default_workflow/coding/workflow.md +2 -3
- zrb/task/llm/file_replacement.py +206 -0
- zrb/task/llm/file_tool_model.py +57 -0
- 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 +8 -19
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_wrapper.py +27 -12
- zrb/task/llm_task.py +55 -47
- zrb/util/attr.py +17 -10
- zrb/util/cli/text.py +6 -4
- zrb/util/git.py +2 -2
- zrb/util/yaml.py +1 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.21.6.dist-info → zrb-1.21.28.dist-info}/METADATA +5 -5
- {zrb-1.21.6.dist-info → zrb-1.21.28.dist-info}/RECORD +46 -41
- zrb/task/llm/history_summarization_tool.py +0 -24
- {zrb-1.21.6.dist-info → zrb-1.21.28.dist-info}/WHEEL +0 -0
- {zrb-1.21.6.dist-info → zrb-1.21.28.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]
|
|
8
|
-
BoolAttr = bool | fstring | Callable[[AnyContext], bool]
|
|
9
|
-
IntAttr = int | fstring | Callable[[AnyContext], int]
|
|
10
|
-
FloatAttr = float | fstring | Callable[[AnyContext], float]
|
|
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,274 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
4
|
+
from prompt_toolkit.document import Document
|
|
5
|
+
|
|
6
|
+
from zrb.builtin.llm.chat_session_cmd import (
|
|
7
|
+
ADD_SUB_CMD,
|
|
8
|
+
ATTACHMENT_ADD_SUB_CMD_DESC,
|
|
9
|
+
ATTACHMENT_CLEAR_SUB_CMD_DESC,
|
|
10
|
+
ATTACHMENT_CMD,
|
|
11
|
+
ATTACHMENT_CMD_DESC,
|
|
12
|
+
ATTACHMENT_SET_SUB_CMD_DESC,
|
|
13
|
+
CLEAR_SUB_CMD,
|
|
14
|
+
HELP_CMD,
|
|
15
|
+
HELP_CMD_DESC,
|
|
16
|
+
MULTILINE_END_CMD,
|
|
17
|
+
MULTILINE_END_CMD_DESC,
|
|
18
|
+
MULTILINE_START_CMD,
|
|
19
|
+
MULTILINE_START_CMD_DESC,
|
|
20
|
+
QUIT_CMD,
|
|
21
|
+
QUIT_CMD_DESC,
|
|
22
|
+
RUN_CLI_CMD,
|
|
23
|
+
RUN_CLI_CMD_DESC,
|
|
24
|
+
SAVE_CMD,
|
|
25
|
+
SAVE_CMD_DESC,
|
|
26
|
+
SET_SUB_CMD,
|
|
27
|
+
WORKFLOW_ADD_SUB_CMD_DESC,
|
|
28
|
+
WORKFLOW_CLEAR_SUB_CMD_DESC,
|
|
29
|
+
WORKFLOW_CMD,
|
|
30
|
+
WORKFLOW_CMD_DESC,
|
|
31
|
+
WORKFLOW_SET_SUB_CMD_DESC,
|
|
32
|
+
YOLO_CMD,
|
|
33
|
+
YOLO_CMD_DESC,
|
|
34
|
+
YOLO_SET_CMD_DESC,
|
|
35
|
+
YOLO_SET_FALSE_CMD_DESC,
|
|
36
|
+
YOLO_SET_TRUE_CMD_DESC,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChatCompleter(Completer):
|
|
41
|
+
|
|
42
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent):
|
|
43
|
+
# Slash command
|
|
44
|
+
for completion in self._complete_slash_command(document):
|
|
45
|
+
yield completion
|
|
46
|
+
for completion in self._complete_slash_file_command(document):
|
|
47
|
+
yield completion
|
|
48
|
+
# Appendix
|
|
49
|
+
for completion in self._complete_appendix(document):
|
|
50
|
+
yield completion
|
|
51
|
+
|
|
52
|
+
def _complete_slash_file_command(self, document: Document):
|
|
53
|
+
text = document.text_before_cursor
|
|
54
|
+
prefixes = []
|
|
55
|
+
for cmd in ATTACHMENT_CMD:
|
|
56
|
+
for subcmd in ADD_SUB_CMD:
|
|
57
|
+
prefixes.append(f"{cmd} {subcmd} ")
|
|
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
|
+
]:
|
|
65
|
+
yield Completion(
|
|
66
|
+
prefixed_option,
|
|
67
|
+
start_position=-len(text),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _complete_slash_command(self, document: Document):
|
|
71
|
+
text = document.text_before_cursor
|
|
72
|
+
if not text.startswith("/"):
|
|
73
|
+
return
|
|
74
|
+
for command, description in self._get_cmd_options().items():
|
|
75
|
+
if command.lower().startswith(text.lower()):
|
|
76
|
+
yield Completion(
|
|
77
|
+
command,
|
|
78
|
+
start_position=-len(text),
|
|
79
|
+
display_meta=description,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _complete_appendix(self, document: Document):
|
|
83
|
+
token = document.get_word_before_cursor(WORD=True)
|
|
84
|
+
prefix = "@"
|
|
85
|
+
if not token.startswith(prefix):
|
|
86
|
+
return
|
|
87
|
+
pattern = token[len(prefix) :]
|
|
88
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
89
|
+
for prefixed_option in [f"{prefix}{option}" for option in potential_options]:
|
|
90
|
+
yield Completion(
|
|
91
|
+
prefixed_option,
|
|
92
|
+
start_position=-len(token),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _get_cmd_options(self):
|
|
96
|
+
cmd_options = {}
|
|
97
|
+
# Add all commands with their descriptions
|
|
98
|
+
for cmd in MULTILINE_START_CMD:
|
|
99
|
+
cmd_options[cmd] = MULTILINE_START_CMD_DESC
|
|
100
|
+
for cmd in MULTILINE_END_CMD:
|
|
101
|
+
cmd_options[cmd] = MULTILINE_END_CMD_DESC
|
|
102
|
+
for cmd in QUIT_CMD:
|
|
103
|
+
cmd_options[cmd] = QUIT_CMD_DESC
|
|
104
|
+
for cmd in WORKFLOW_CMD:
|
|
105
|
+
cmd_options[cmd] = WORKFLOW_CMD_DESC
|
|
106
|
+
for subcmd in ADD_SUB_CMD:
|
|
107
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_ADD_SUB_CMD_DESC
|
|
108
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
109
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_CLEAR_SUB_CMD_DESC
|
|
110
|
+
for subcmd in SET_SUB_CMD:
|
|
111
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_SET_SUB_CMD_DESC
|
|
112
|
+
for cmd in SAVE_CMD:
|
|
113
|
+
cmd_options[cmd] = SAVE_CMD_DESC
|
|
114
|
+
for cmd in ATTACHMENT_CMD:
|
|
115
|
+
cmd_options[cmd] = ATTACHMENT_CMD_DESC
|
|
116
|
+
for subcmd in ADD_SUB_CMD:
|
|
117
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_ADD_SUB_CMD_DESC
|
|
118
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
119
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_CLEAR_SUB_CMD_DESC
|
|
120
|
+
for subcmd in SET_SUB_CMD:
|
|
121
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_SET_SUB_CMD_DESC
|
|
122
|
+
for cmd in YOLO_CMD:
|
|
123
|
+
cmd_options[cmd] = YOLO_CMD_DESC
|
|
124
|
+
for subcmd in SET_SUB_CMD:
|
|
125
|
+
cmd_options[f"{cmd} {subcmd} true"] = YOLO_SET_TRUE_CMD_DESC
|
|
126
|
+
cmd_options[f"{cmd} {subcmd} false"] = YOLO_SET_FALSE_CMD_DESC
|
|
127
|
+
cmd_options[f"{cmd} {subcmd}"] = YOLO_SET_CMD_DESC
|
|
128
|
+
for cmd in HELP_CMD:
|
|
129
|
+
cmd_options[cmd] = HELP_CMD_DESC
|
|
130
|
+
for cmd in RUN_CLI_CMD:
|
|
131
|
+
cmd_options[cmd] = RUN_CLI_CMD_DESC
|
|
132
|
+
return dict(sorted(cmd_options.items()))
|
|
133
|
+
|
|
134
|
+
def _fuzzy_path_search(
|
|
135
|
+
self,
|
|
136
|
+
pattern: str,
|
|
137
|
+
root: str | None = None,
|
|
138
|
+
max_results: int = 20,
|
|
139
|
+
include_hidden: bool = False,
|
|
140
|
+
case_sensitive: bool = False,
|
|
141
|
+
dirs: bool = True,
|
|
142
|
+
files: bool = True,
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
"""
|
|
145
|
+
Return a list of filesystem paths under `root` that fuzzy-match `pattern`.
|
|
146
|
+
- pattern: e.g. "./some/x" or "proj util/io"
|
|
147
|
+
- include_hidden: if False skip files/dirs starting with '.'
|
|
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:
|
|
161
|
+
root = os.path.dirname(expanded_pattern)
|
|
162
|
+
while root and not os.path.isdir(root) and len(root) > 1:
|
|
163
|
+
root = os.path.dirname(root)
|
|
164
|
+
if not os.path.isdir(root):
|
|
165
|
+
root = "." # Fallback
|
|
166
|
+
search_pattern = pattern
|
|
167
|
+
else:
|
|
168
|
+
try:
|
|
169
|
+
search_pattern = os.path.relpath(expanded_pattern, root)
|
|
170
|
+
if search_pattern == ".":
|
|
171
|
+
search_pattern = ""
|
|
172
|
+
except ValueError:
|
|
173
|
+
search_pattern = os.path.basename(pattern)
|
|
174
|
+
else:
|
|
175
|
+
root = "."
|
|
176
|
+
search_pattern = pattern
|
|
177
|
+
# Normalize pattern -> tokens split on path separators or whitespace
|
|
178
|
+
search_pattern = search_pattern.strip()
|
|
179
|
+
if search_pattern:
|
|
180
|
+
raw_tokens = [t for t in search_pattern.split(os.path.sep) if t]
|
|
181
|
+
else:
|
|
182
|
+
raw_tokens = []
|
|
183
|
+
# prepare tokens (case)
|
|
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]]
|
|
255
|
+
|
|
256
|
+
def _find_subsequence_pos(
|
|
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
|
|
@@ -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,4 +1,5 @@
|
|
|
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
|
|
|
@@ -56,9 +57,14 @@ class LLMChatTrigger:
|
|
|
56
57
|
"""Reads one line of input using the provided reader."""
|
|
57
58
|
from prompt_toolkit import PromptSession
|
|
58
59
|
|
|
60
|
+
from zrb.builtin.llm.chat_completion import ChatCompleter
|
|
61
|
+
|
|
59
62
|
try:
|
|
60
63
|
if isinstance(reader, PromptSession):
|
|
61
|
-
|
|
64
|
+
bottom_toolbar = f"📁 Current directory: {os.getcwd()}"
|
|
65
|
+
return await reader.prompt_async(
|
|
66
|
+
completer=ChatCompleter(), bottom_toolbar=bottom_toolbar
|
|
67
|
+
)
|
|
62
68
|
line_bytes = await reader.readline()
|
|
63
69
|
if not line_bytes:
|
|
64
70
|
return "/bye" # Signal to exit
|
zrb/builtin/llm/history.py
CHANGED
|
@@ -3,12 +3,12 @@ import os
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from zrb.config.config import CFG
|
|
6
|
-
from zrb.context.
|
|
6
|
+
from zrb.context.any_context import AnyContext
|
|
7
7
|
from zrb.task.llm.conversation_history_model import ConversationHistory
|
|
8
8
|
from zrb.util.file import read_file, write_file
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def read_chat_conversation(ctx:
|
|
11
|
+
def read_chat_conversation(ctx: AnyContext) -> dict[str, Any] | list | None:
|
|
12
12
|
"""Reads conversation history from the session file.
|
|
13
13
|
Returns the raw dictionary or list loaded from JSON, or None if not found/empty.
|
|
14
14
|
The LLMTask will handle parsing this into ConversationHistory.
|
|
@@ -51,10 +51,10 @@ def read_chat_conversation(ctx: AnySharedContext) -> dict[str, Any] | list | Non
|
|
|
51
51
|
return None
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def write_chat_conversation(ctx:
|
|
54
|
+
def write_chat_conversation(ctx: AnyContext, history_data: ConversationHistory):
|
|
55
55
|
"""Writes the conversation history data (including context) to a session file."""
|
|
56
56
|
os.makedirs(CFG.LLM_HISTORY_DIR, exist_ok=True)
|
|
57
|
-
current_session_name = ctx.session.name
|
|
57
|
+
current_session_name = ctx.session.name if ctx.session is not None else None
|
|
58
58
|
if not current_session_name:
|
|
59
59
|
ctx.log_warning("Cannot write history: Session name is empty.")
|
|
60
60
|
return
|
zrb/builtin/llm/tool/api.py
CHANGED
|
@@ -42,7 +42,9 @@ def create_get_current_weather() -> Callable:
|
|
|
42
42
|
Gets current weather conditions for a given location.
|
|
43
43
|
|
|
44
44
|
Example:
|
|
45
|
-
get_current_weather(
|
|
45
|
+
get_current_weather(
|
|
46
|
+
latitude=34.0522, longitude=-118.2437, temperature_unit='fahrenheit'
|
|
47
|
+
)
|
|
46
48
|
|
|
47
49
|
Args:
|
|
48
50
|
latitude (float): Latitude of the location.
|
zrb/builtin/llm/tool/cli.py
CHANGED
|
@@ -26,7 +26,8 @@ def run_shell_command(command: str) -> ShellCommandResult:
|
|
|
26
26
|
"""
|
|
27
27
|
Executes a non-interactive shell command on the user's machine.
|
|
28
28
|
|
|
29
|
-
CRITICAL: This tool runs with user-level permissions. Explain commands that modify
|
|
29
|
+
CRITICAL: This tool runs with user-level permissions. Explain commands that modify
|
|
30
|
+
the system (e.g., `git`, `pip`) and ask for confirmation.
|
|
30
31
|
IMPORTANT: Long-running processes should be run in the background (e.g., `command &`).
|
|
31
32
|
|
|
32
33
|
Example:
|
zrb/builtin/llm/tool/code.py
CHANGED
|
@@ -59,11 +59,16 @@ async def analyze_repo(
|
|
|
59
59
|
"""
|
|
60
60
|
Analyzes a code repository or directory to answer a specific query.
|
|
61
61
|
|
|
62
|
-
CRITICAL: The quality of analysis depends entirely on the query. Vague queries yield poor
|
|
62
|
+
CRITICAL: The quality of analysis depends entirely on the query. Vague queries yield poor
|
|
63
|
+
results.
|
|
63
64
|
IMPORTANT: This tool can be slow and expensive on large repositories. Use judiciously.
|
|
64
65
|
|
|
65
66
|
Example:
|
|
66
|
-
analyze_repo(
|
|
67
|
+
analyze_repo(
|
|
68
|
+
path='src/my_project',
|
|
69
|
+
query='Summarize the main functionalities by analyzing Python files.',
|
|
70
|
+
extensions=['py']
|
|
71
|
+
)
|
|
67
72
|
|
|
68
73
|
Args:
|
|
69
74
|
ctx (AnyContext): The execution context.
|
|
@@ -144,6 +149,8 @@ async def _extract_info(
|
|
|
144
149
|
tool_name="extract",
|
|
145
150
|
tool_description="extract",
|
|
146
151
|
system_prompt=CFG.LLM_REPO_EXTRACTOR_SYSTEM_PROMPT,
|
|
152
|
+
auto_summarize=False,
|
|
153
|
+
remember_history=False,
|
|
147
154
|
)
|
|
148
155
|
extracted_infos = []
|
|
149
156
|
content_buffer = []
|
|
@@ -165,7 +172,6 @@ async def _extract_info(
|
|
|
165
172
|
else:
|
|
166
173
|
content_buffer.append(file_obj)
|
|
167
174
|
current_token_count += llm_rate_limitter.count_token(file_str)
|
|
168
|
-
|
|
169
175
|
# Process any remaining content in the buffer
|
|
170
176
|
if content_buffer:
|
|
171
177
|
prompt = json.dumps(_create_extract_info_prompt(query, content_buffer))
|
|
@@ -193,6 +199,8 @@ async def _summarize_info(
|
|
|
193
199
|
tool_name="extract",
|
|
194
200
|
tool_description="extract",
|
|
195
201
|
system_prompt=CFG.LLM_REPO_SUMMARIZER_SYSTEM_PROMPT,
|
|
202
|
+
auto_summarize=False,
|
|
203
|
+
remember_history=False,
|
|
196
204
|
)
|
|
197
205
|
summarized_infos = []
|
|
198
206
|
content_buffer = ""
|