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.

Files changed (47) hide show
  1. zrb/attr/type.py +10 -7
  2. zrb/builtin/git.py +12 -1
  3. zrb/builtin/llm/chat_completion.py +274 -0
  4. zrb/builtin/llm/chat_session_cmd.py +90 -28
  5. zrb/builtin/llm/chat_trigger.py +7 -1
  6. zrb/builtin/llm/history.py +4 -4
  7. zrb/builtin/llm/tool/api.py +3 -1
  8. zrb/builtin/llm/tool/cli.py +2 -1
  9. zrb/builtin/llm/tool/code.py +11 -3
  10. zrb/builtin/llm/tool/file.py +112 -142
  11. zrb/builtin/llm/tool/note.py +36 -16
  12. zrb/builtin/llm/tool/rag.py +17 -8
  13. zrb/builtin/llm/tool/sub_agent.py +41 -15
  14. zrb/config/config.py +108 -13
  15. zrb/config/default_prompt/file_extractor_system_prompt.md +16 -16
  16. zrb/config/default_prompt/interactive_system_prompt.md +11 -11
  17. zrb/config/default_prompt/repo_extractor_system_prompt.md +16 -16
  18. zrb/config/default_prompt/repo_summarizer_system_prompt.md +3 -3
  19. zrb/config/default_prompt/summarization_prompt.md +54 -8
  20. zrb/config/default_prompt/system_prompt.md +15 -15
  21. zrb/config/llm_rate_limitter.py +24 -5
  22. zrb/input/option_input.py +13 -1
  23. zrb/task/llm/agent.py +42 -144
  24. zrb/task/llm/agent_runner.py +152 -0
  25. zrb/task/llm/config.py +8 -7
  26. zrb/task/llm/conversation_history.py +35 -24
  27. zrb/task/llm/conversation_history_model.py +4 -11
  28. zrb/task/llm/default_workflow/coding/workflow.md +2 -3
  29. zrb/task/llm/file_replacement.py +206 -0
  30. zrb/task/llm/file_tool_model.py +57 -0
  31. zrb/task/llm/history_processor.py +206 -0
  32. zrb/task/llm/history_summarization.py +2 -179
  33. zrb/task/llm/print_node.py +14 -5
  34. zrb/task/llm/prompt.py +8 -19
  35. zrb/task/llm/subagent_conversation_history.py +41 -0
  36. zrb/task/llm/tool_wrapper.py +27 -12
  37. zrb/task/llm_task.py +55 -47
  38. zrb/util/attr.py +17 -10
  39. zrb/util/cli/text.py +6 -4
  40. zrb/util/git.py +2 -2
  41. zrb/util/yaml.py +1 -0
  42. zrb/xcom/xcom.py +10 -0
  43. {zrb-1.21.6.dist-info → zrb-1.21.28.dist-info}/METADATA +5 -5
  44. {zrb-1.21.6.dist-info → zrb-1.21.28.dist-info}/RECORD +46 -41
  45. zrb/task/llm/history_summarization_tool.py +0 -24
  46. {zrb-1.21.6.dist-info → zrb-1.21.28.dist-info}/WHEEL +0 -0
  47. {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 = dict[str, StrAttr] | Callable[[AnyContext], dict[str, Any]]
12
- StrListAttr = list[StrAttr] | Callable[[AnyContext], list[str]]
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 == "main" or branch == "master":
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 = ["/attach", "/attachment", "/attachments"]
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
- return old_yolo_mode
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 get_command_param(user_input, ATTACHMENT_CMD, SET_SUB_CMD)
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 ",".join([old_attachment, new_attachment])
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 get_command_param(user_input, WORKFLOW_CMD, SET_SUB_CMD)
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("/bye", "Quit from chat session"),
179
- _show_command("/multi", "Start multiline input"),
180
- _show_command("/end", "End multiline input"),
181
- _show_command("/attachment", "Show current attachment"),
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
- "set", "<attachment1,attachment2,...>", "Attach a file"
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
- "set", "<workflow1,workflow2,..>", "Set active workflows"
240
+ SET_SUB_CMD[0],
241
+ "<file1-path,file2-path,...>",
242
+ ATTACHMENT_SET_SUB_CMD_DESC,
191
243
  ),
192
- _show_subcommand("clear", "", "Deactivate all workflows"),
193
- _show_command("/save <file-path>", "Save last response to a file"),
194
- _show_command("/yolo", "Show current YOLO mode"),
195
- _show_command_param(
196
- "<true | false | tool1,tool2,...>", "Set YOLO mode"
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
- _show_command("/run", ""),
199
- _show_command_param(
200
- "<cli-command>", "Run a non-interactive CLI command"
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("/help", "Show this message"),
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,
@@ -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
- return await reader.prompt_async()
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
@@ -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.any_shared_context import AnySharedContext
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: AnySharedContext) -> dict[str, Any] | list | None:
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: AnySharedContext, history_data: ConversationHistory):
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
@@ -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(latitude=34.0522, longitude=-118.2437, temperature_unit='fahrenheit')
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.
@@ -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 the system (e.g., `git`, `pip`) and ask for confirmation.
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:
@@ -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 results.
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(path='src/my_project', query='Summarize the main functionalities by analyzing Python files.', extensions=['py'])
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 = ""