zrb 1.21.9__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/code.py +4 -1
- zrb/builtin/llm/tool/file.py +36 -81
- zrb/builtin/llm/tool/note.py +36 -16
- zrb/builtin/llm/tool/sub_agent.py +30 -10
- zrb/config/config.py +108 -13
- zrb/config/default_prompt/interactive_system_prompt.md +1 -1
- zrb/config/default_prompt/summarization_prompt.md +54 -8
- zrb/config/default_prompt/system_prompt.md +1 -1
- 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 +7 -5
- 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 +7 -18
- 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.9.dist-info → zrb-1.21.28.dist-info}/METADATA +5 -5
- {zrb-1.21.9.dist-info → zrb-1.21.28.dist-info}/RECORD +40 -35
- zrb/task/llm/history_summarization_tool.py +0 -24
- {zrb-1.21.9.dist-info → zrb-1.21.28.dist-info}/WHEEL +0 -0
- {zrb-1.21.9.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/code.py
CHANGED
|
@@ -149,6 +149,8 @@ async def _extract_info(
|
|
|
149
149
|
tool_name="extract",
|
|
150
150
|
tool_description="extract",
|
|
151
151
|
system_prompt=CFG.LLM_REPO_EXTRACTOR_SYSTEM_PROMPT,
|
|
152
|
+
auto_summarize=False,
|
|
153
|
+
remember_history=False,
|
|
152
154
|
)
|
|
153
155
|
extracted_infos = []
|
|
154
156
|
content_buffer = []
|
|
@@ -170,7 +172,6 @@ async def _extract_info(
|
|
|
170
172
|
else:
|
|
171
173
|
content_buffer.append(file_obj)
|
|
172
174
|
current_token_count += llm_rate_limitter.count_token(file_str)
|
|
173
|
-
|
|
174
175
|
# Process any remaining content in the buffer
|
|
175
176
|
if content_buffer:
|
|
176
177
|
prompt = json.dumps(_create_extract_info_prompt(query, content_buffer))
|
|
@@ -198,6 +199,8 @@ async def _summarize_info(
|
|
|
198
199
|
tool_name="extract",
|
|
199
200
|
tool_description="extract",
|
|
200
201
|
system_prompt=CFG.LLM_REPO_SUMMARIZER_SYSTEM_PROMPT,
|
|
202
|
+
auto_summarize=False,
|
|
203
|
+
remember_history=False,
|
|
201
204
|
)
|
|
202
205
|
summarized_infos = []
|
|
203
206
|
content_buffer = ""
|