zrb 1.21.28__py3-none-any.whl → 1.21.31__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.

@@ -1,7 +1,5 @@
1
1
  import os
2
-
3
- from prompt_toolkit.completion import CompleteEvent, Completer, Completion
4
- from prompt_toolkit.document import Document
2
+ from typing import TYPE_CHECKING
5
3
 
6
4
  from zrb.builtin.llm.chat_session_cmd import (
7
5
  ADD_SUB_CMD,
@@ -36,239 +34,254 @@ from zrb.builtin.llm.chat_session_cmd import (
36
34
  YOLO_SET_TRUE_CMD_DESC,
37
35
  )
38
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
39
45
 
40
- class ChatCompleter(Completer):
46
+ class ChatCompleter(Completer):
41
47
 
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
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
51
57
 
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
- ]:
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()):
65
82
  yield Completion(
66
- prefixed_option,
83
+ command,
67
84
  start_position=-len(text),
85
+ display_meta=description,
68
86
  )
69
87
 
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()):
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
+ ]:
76
98
  yield Completion(
77
- command,
78
- start_position=-len(text),
79
- display_meta=description,
99
+ prefixed_option,
100
+ start_position=-len(token),
80
101
  )
81
102
 
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()))
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()))
133
141
 
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
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 = ""
167
168
  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)
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]
174
194
  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)
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
217
207
  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]]
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
255
286
 
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
287
+ return ChatCompleter()
@@ -3,6 +3,7 @@ import os
3
3
  from asyncio import StreamReader
4
4
  from typing import TYPE_CHECKING, Any, Callable, Coroutine
5
5
 
6
+ from zrb.builtin.llm.chat_completion import get_chat_completer
6
7
  from zrb.context.any_context import AnyContext
7
8
  from zrb.util.run import run_async
8
9
 
@@ -57,13 +58,11 @@ class LLMChatTrigger:
57
58
  """Reads one line of input using the provided reader."""
58
59
  from prompt_toolkit import PromptSession
59
60
 
60
- from zrb.builtin.llm.chat_completion import ChatCompleter
61
-
62
61
  try:
63
62
  if isinstance(reader, PromptSession):
64
63
  bottom_toolbar = f"📁 Current directory: {os.getcwd()}"
65
64
  return await reader.prompt_async(
66
- completer=ChatCompleter(), bottom_toolbar=bottom_toolbar
65
+ completer=get_chat_completer(), bottom_toolbar=bottom_toolbar
67
66
  )
68
67
  line_bytes = await reader.readline()
69
68
  if not line_bytes:
@@ -22,7 +22,7 @@ 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
 
@@ -31,22 +31,34 @@ def run_shell_command(command: str) -> ShellCommandResult:
31
31
  IMPORTANT: Long-running processes should be run in the background (e.g., `command &`).
32
32
 
33
33
  Example:
34
- run_shell_command(command='ls -l')
34
+ run_shell_command(command='ls -l', timeout=30)
35
35
 
36
36
  Args:
37
37
  command (str): The exact shell command to be executed.
38
+ timeout (int): The maximum time in seconds to wait for the command to finish.
39
+ Defaults to 30.
38
40
 
39
41
  Returns:
40
42
  dict: return_code, stdout, and stderr.
41
43
  """
42
- result = subprocess.run(
43
- command,
44
- shell=True,
45
- capture_output=True,
46
- text=True,
47
- )
48
- return {
49
- "return_code": result.returncode,
50
- "stdout": result.stdout,
51
- "stderr": result.stderr,
52
- }
44
+ try:
45
+ result = subprocess.run(
46
+ command,
47
+ shell=True,
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=timeout,
51
+ )
52
+ return {
53
+ "return_code": int(result.returncode),
54
+ "stdout": str(result.stdout or ""),
55
+ "stderr": str(result.stderr or ""),
56
+ }
57
+ except subprocess.TimeoutExpired as e:
58
+ stdout = e.stdout.decode() if isinstance(e.stdout, bytes) else (e.stdout or "")
59
+ stderr = e.stderr.decode() if isinstance(e.stderr, bytes) else (e.stderr or "")
60
+ return {
61
+ "return_code": 124,
62
+ "stdout": str(stdout),
63
+ "stderr": f"{stderr}\nError: Command timed out after {timeout} seconds".strip(),
64
+ }
@@ -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 quality of analysis depends entirely on the query. Vague queries yield poor
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:
@@ -516,6 +516,12 @@ async def analyze_file(
516
516
  """
517
517
  Analyzes a file using a sub-agent for complex questions.
518
518
 
519
+ CRITICAL: The query must contain ALL necessary context, instructions, and information.
520
+ The sub-agent performing the analysis does NOT share your current conversation
521
+ history, memory, or global context.
522
+ The quality of analysis depends entirely on the query. Vague queries yield poor
523
+ results.
524
+
519
525
  Example:
520
526
  analyze_file(path='src/main.py', query='Summarize the main function.')
521
527
 
@@ -5,9 +5,9 @@ from zrb.config.llm_context.config import llm_context_config
5
5
 
6
6
  def read_long_term_note() -> str:
7
7
  """
8
- Retrieves the GLOBAL long-term memory shared across ALL sessions and projects.
8
+ Retrieves the GLOBAL 🧠 Long Term Note shared across ALL sessions and projects.
9
9
 
10
- CRITICAL: Consult this first for user preferences, facts, and cross-project context.
10
+ Use this to recall user preferences, facts, and cross-project context.
11
11
 
12
12
  Returns:
13
13
  str: The current global note content.
@@ -18,7 +18,7 @@ def read_long_term_note() -> str:
18
18
 
19
19
  def write_long_term_note(content: str) -> str:
20
20
  """
21
- Persists CRITICAL facts to the GLOBAL long-term memory.
21
+ Persists CRITICAL facts to the GLOBAL 🧠 Long Term Note.
22
22
 
23
23
  USE EAGERLY to save or update:
24
24
  - User preferences (e.g., "I prefer Python", "No unit tests").
@@ -27,7 +27,7 @@ def write_long_term_note(content: str) -> str:
27
27
  - Cross-project goals.
28
28
  - Anything that will be useful for future interaction across projects.
29
29
 
30
- WARNING: This OVERWRITES the entire global note.
30
+ WARNING: This OVERWRITES the entire Long Term Note.
31
31
 
32
32
  Args:
33
33
  content (str): The text to strictly memorize.
@@ -41,7 +41,7 @@ def write_long_term_note(content: str) -> str:
41
41
 
42
42
  def read_contextual_note(path: str | None = None) -> str:
43
43
  """
44
- Retrieves LOCAL memory specific to a file or directory path.
44
+ Retrieves LOCAL 📝 Contextual Note specific to a directory path.
45
45
 
46
46
  Use to recall project-specific architecture, code summaries, or past decisions
47
47
  relevant to the current working location.
@@ -61,7 +61,7 @@ def read_contextual_note(path: str | None = None) -> str:
61
61
 
62
62
  def write_contextual_note(content: str, path: str | None = None) -> str:
63
63
  """
64
- Persists LOCAL facts specific to a file or directory.
64
+ Persists LOCAL facts specific to a directory into 📝 Contextual Note.
65
65
 
66
66
  USE EAGERLY to save or update:
67
67
  - Architectural patterns for this project/directory.
@@ -69,7 +69,7 @@ def write_contextual_note(content: str, path: str | None = None) -> str:
69
69
  - Specific guidelines for this project.
70
70
  - Anything related to this directory that will be useful for future interaction.
71
71
 
72
- WARNING: This OVERWRITES the note for the specific path.
72
+ WARNING: This OVERWRITES the entire Contextual Note for a directory.
73
73
 
74
74
  Args:
75
75
  content (str): The text to memorize for this location.
@@ -0,0 +1 @@
1
+ # This file makes the directory a Python package
@@ -0,0 +1,60 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from zrb.config.config import CFG
6
+
7
+
8
+ def search_internet(
9
+ query: str,
10
+ page: int = 1,
11
+ safe_search: str | None = None,
12
+ language: str | None = None,
13
+ ) -> dict[str, Any]:
14
+ """
15
+ Performs an internet search using Brave Search.
16
+
17
+ Use this tool to find up-to-date information, answer questions about current events,
18
+ or research topics using a search engine.
19
+
20
+ Args:
21
+ query (str): The natural language search query (e.g., 'Soto Madura').
22
+ Do NOT include instructions, meta-talk, or internal reasoning.
23
+ Use concise terms as a human would in a search engine.
24
+ page (int): Search result page number. Defaults to 1.
25
+ safe_search (str | None): Safety setting. 'strict', 'moderate', or 'off'.
26
+ If None, uses the system default configuration.
27
+ language (str | None): Language code (e.g., 'en').
28
+ If None, uses the system default configuration.
29
+
30
+ Returns:
31
+ dict: Summary of search results (titles, links, snippets).
32
+ """
33
+ if safe_search is None:
34
+ safe_search = CFG.BRAVE_API_SAFE
35
+ if language is None:
36
+ language = CFG.BRAVE_API_LANG
37
+
38
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa
39
+
40
+ response = requests.get(
41
+ "https://api.search.brave.com/res/v1/web/search",
42
+ headers={
43
+ "User-Agent": user_agent,
44
+ "Accept": "application/json",
45
+ "x-subscription-token": CFG.BRAVE_API_KEY,
46
+ },
47
+ params={
48
+ "q": query,
49
+ "count": "10",
50
+ "offset": (page - 1) * 10,
51
+ "safesearch": safe_search,
52
+ "search_lang": language,
53
+ "summary": "true",
54
+ },
55
+ )
56
+ if response.status_code != 200:
57
+ raise Exception(
58
+ f"Error: Unable to retrieve search results (status code: {response.status_code})"
59
+ )
60
+ return response.json()
@@ -0,0 +1,55 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from zrb.config.config import CFG
6
+
7
+
8
+ def search_internet(
9
+ query: str,
10
+ page: int = 1,
11
+ safe_search: int | None = None,
12
+ language: str | None = None,
13
+ ) -> dict[str, Any]:
14
+ """
15
+ Performs an internet search using SearXNG.
16
+
17
+ Use this tool to find up-to-date information, answer questions about current events,
18
+ or research topics using a search engine.
19
+
20
+ Args:
21
+ query (str): The natural language search query (e.g., 'Soto Madura').
22
+ Do NOT include instructions, meta-talk, or internal reasoning.
23
+ Use concise terms as a human would in a search engine.
24
+ page (int): Search result page number. Defaults to 1.
25
+ safe_search (int | None): Safety setting. 0 (None), 1 (Moderate), 2 (Strict).
26
+ If None, uses the system default configuration.
27
+ language (str | None): Language code (e.g., 'en').
28
+ If None, uses the system default configuration.
29
+
30
+ Returns:
31
+ dict: Summary of search results (titles, links, snippets).
32
+ """
33
+ if safe_search is None:
34
+ safe_search = CFG.SEARXNG_SAFE
35
+ if language is None:
36
+ language = CFG.SEARXNG_LANG
37
+
38
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa
39
+
40
+ response = requests.get(
41
+ url=f"{CFG.SEARXNG_BASE_URL}/search",
42
+ headers={"User-Agent": user_agent},
43
+ params={
44
+ "q": query,
45
+ "format": "json",
46
+ "pageno": page,
47
+ "safesearch": safe_search,
48
+ "language": language,
49
+ },
50
+ )
51
+ if response.status_code != 200:
52
+ raise Exception(
53
+ f"Error: Unable to retrieve search results (status code: {response.status_code})"
54
+ )
55
+ return response.json()
@@ -0,0 +1,55 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from zrb.config.config import CFG
6
+
7
+
8
+ def search_internet(
9
+ query: str,
10
+ page: int = 1,
11
+ safe_search: str | None = None,
12
+ language: str | None = None,
13
+ ) -> dict[str, Any]:
14
+ """
15
+ Performs an internet search using SerpApi (Google).
16
+
17
+ Use this tool to find up-to-date information, answer questions about current events,
18
+ or research topics using a search engine.
19
+
20
+ Args:
21
+ query (str): The natural language search query (e.g., 'Soto Madura').
22
+ Do NOT include instructions, meta-talk, or internal reasoning.
23
+ Use concise terms as a human would in a search engine.
24
+ page (int): Search result page number. Defaults to 1.
25
+ safe_search (str | None): Safety setting. 'active' or 'off'.
26
+ If None, uses the system default configuration.
27
+ language (str | None): Two-letter language code (e.g., 'en', 'id').
28
+ If None, uses the system default configuration.
29
+
30
+ Returns:
31
+ dict: Summary of search results (titles, links, snippets).
32
+ """
33
+ if safe_search is None:
34
+ safe_search = CFG.SERPAPI_SAFE
35
+ if language is None:
36
+ language = CFG.SERPAPI_LANG
37
+
38
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa
39
+
40
+ response = requests.get(
41
+ "https://serpapi.com/search",
42
+ headers={"User-Agent": user_agent},
43
+ params={
44
+ "q": query,
45
+ "start": (page - 1) * 10,
46
+ "hl": language,
47
+ "safe": safe_search,
48
+ "api_key": CFG.SERPAPI_KEY,
49
+ },
50
+ )
51
+ if response.status_code != 200:
52
+ raise Exception(
53
+ f"Error: Unable to retrieve search results (status code: {response.status_code})"
54
+ )
55
+ return response.json()
@@ -2,6 +2,7 @@ from collections.abc import Callable
2
2
  from typing import Any
3
3
  from urllib.parse import urljoin
4
4
 
5
+ from zrb.builtin.llm.tool.search import brave, searxng, serpapi
5
6
  from zrb.config.config import CFG
6
7
  from zrb.config.llm_config import llm_config
7
8
 
@@ -30,78 +31,17 @@ async def open_web_page(url: str) -> dict[str, Any]:
30
31
  def create_search_internet_tool() -> Callable:
31
32
  if llm_config.default_search_internet_tool is not None:
32
33
  return llm_config.default_search_internet_tool
33
-
34
- def search_internet(query: str, page: int = 1) -> dict[str, Any]:
35
- """
36
- Performs an internet search using a search engine.
37
- Use to find information, answer general knowledge, or research topics.
38
-
39
- Example:
40
- search_internet(query='latest AI advancements', page=1)
41
-
42
- Args:
43
- query (str): The search query.
44
- page (int, optional): Search result page number. Defaults to 1.
45
-
46
- Returns:
47
- dict: Summary of search results (titles, links, snippets).
48
- """
49
- import requests
50
-
51
- if (
52
- CFG.SEARCH_INTERNET_METHOD.strip().lower() == "serpapi"
53
- and CFG.SERPAPI_KEY != ""
54
- ):
55
- response = requests.get(
56
- "https://serpapi.com/search",
57
- headers={"User-Agent": _DEFAULT_USER_AGENT},
58
- params={
59
- "q": query,
60
- "start": (page - 1) * 10,
61
- "hl": CFG.SERPAPI_LANG,
62
- "safe": CFG.SERPAPI_SAFE,
63
- "api_key": CFG.SERPAPI_KEY,
64
- },
65
- )
66
- elif (
67
- CFG.SEARCH_INTERNET_METHOD.strip().lower() == "brave"
68
- and CFG.BRAVE_API_KEY != ""
69
- ):
70
- response = requests.get(
71
- "https://api.search.brave.com/res/v1/web/search",
72
- headers={
73
- "User-Agent": _DEFAULT_USER_AGENT,
74
- "Accept": "application/json",
75
- "x-subscription-token": CFG.BRAVE_API_KEY,
76
- },
77
- params={
78
- "q": query,
79
- "count": "10",
80
- "offset": (page - 1) * 10,
81
- "safesearch": CFG.BRAVE_API_SAFE,
82
- "search_lang": CFG.BRAVE_API_LANG,
83
- "summary": "true",
84
- },
85
- )
86
- else:
87
- response = requests.get(
88
- url=f"{CFG.SEARXNG_BASE_URL}/search",
89
- headers={"User-Agent": _DEFAULT_USER_AGENT},
90
- params={
91
- "q": query,
92
- "format": "json",
93
- "pageno": page,
94
- "safesearch": CFG.SEARXNG_SAFE,
95
- "language": CFG.SEARXNG_LANG,
96
- },
97
- )
98
- if response.status_code != 200:
99
- raise Exception(
100
- f"Error: Unable to retrieve search results (status code: {response.status_code})" # noqa
101
- )
102
- return response.json()
103
-
104
- return search_internet
34
+ if (
35
+ CFG.SEARCH_INTERNET_METHOD.strip().lower() == "serpapi"
36
+ and CFG.SERPAPI_KEY != ""
37
+ ):
38
+ return serpapi.search_internet
39
+ if (
40
+ CFG.SEARCH_INTERNET_METHOD.strip().lower() == "brave"
41
+ and CFG.BRAVE_API_KEY != ""
42
+ ):
43
+ return brave.search_internet
44
+ return searxng.search_internet
105
45
 
106
46
 
107
47
  async def _fetch_page_content(url: str) -> tuple[str, list[str]]:
@@ -0,0 +1,41 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from prompt_toolkit.completion import Completer
5
+
6
+
7
+ def get_tool_confirmation_completer(
8
+ options: list[str], meta_dict: dict[str, str]
9
+ ) -> "Completer":
10
+ from prompt_toolkit.completion import Completer, Completion
11
+
12
+ class ToolConfirmationCompleter(Completer):
13
+ """Custom completer for tool confirmation that doesn't auto-complete partial words."""
14
+
15
+ def __init__(self, options, meta_dict):
16
+ self.options = options
17
+ self.meta_dict = meta_dict
18
+
19
+ def get_completions(self, document, complete_event):
20
+ text = document.text.strip()
21
+ # 1. Input is empty, OR
22
+ # 2. Input exactly matches the beginning of an option
23
+ if text == "":
24
+ # Show all options when nothing is typed
25
+ for option in self.options:
26
+ yield Completion(
27
+ option,
28
+ start_position=0,
29
+ display_meta=self.meta_dict.get(option, ""),
30
+ )
31
+ return
32
+ # Only complete if text exactly matches the beginning of an option
33
+ for option in self.options:
34
+ if option.startswith(text):
35
+ yield Completion(
36
+ option,
37
+ start_position=-len(text),
38
+ display_meta=self.meta_dict.get(option, ""),
39
+ )
40
+
41
+ return ToolConfirmationCompleter(options, meta_dict)
@@ -11,11 +11,11 @@ from zrb.config.llm_rate_limitter import llm_rate_limitter
11
11
  from zrb.context.any_context import AnyContext
12
12
  from zrb.task.llm.error import ToolExecutionError
13
13
  from zrb.task.llm.file_replacement import edit_replacement, is_single_path_replacement
14
+ from zrb.task.llm.tool_confirmation_completer import get_tool_confirmation_completer
14
15
  from zrb.util.callable import get_callable_name
15
16
  from zrb.util.cli.markdown import render_markdown
16
17
  from zrb.util.cli.style import (
17
18
  stylize_blue,
18
- stylize_error,
19
19
  stylize_faint,
20
20
  stylize_green,
21
21
  stylize_yellow,
@@ -296,7 +296,6 @@ def _truncate_arg(arg: str, length: int = 19) -> str:
296
296
 
297
297
  async def _read_line(args: list[Any] | tuple[Any], kwargs: dict[str, Any]):
298
298
  from prompt_toolkit import PromptSession
299
- from prompt_toolkit.completion import WordCompleter
300
299
 
301
300
  options = ["yes", "no", "edit"]
302
301
  meta_dict = {
@@ -307,7 +306,7 @@ async def _read_line(args: list[Any] | tuple[Any], kwargs: dict[str, Any]):
307
306
  for key in kwargs:
308
307
  options.append(f"edit {key}")
309
308
  meta_dict[f"edit {key}"] = f"Edit tool execution parameter: {key}"
310
- completer = WordCompleter(options, ignore_case=True, meta_dict=meta_dict)
309
+ completer = get_tool_confirmation_completer(options, meta_dict)
311
310
  reader = PromptSession()
312
311
  return await reader.prompt_async(completer=completer)
313
312
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zrb
3
- Version: 1.21.28
3
+ Version: 1.21.31
4
4
  Summary: Your Automation Powerhouse
5
5
  License: AGPL-3.0-or-later
6
6
  Keywords: Automation,Task Runner,Code Generator,Monorepo,Low Code
@@ -10,23 +10,27 @@ zrb/builtin/group.py,sha256=W_IA_4414c8Wi8QazqcT6Gq6UftrKwy95yMF114EJtI,2677
10
10
  zrb/builtin/http.py,sha256=L6RE73c65wWwG5iHFN-tpOhyh56KsrgVskDd3c3YXtk,4246
11
11
  zrb/builtin/jwt.py,sha256=3M5uaQhJZbKQLjTUft1OwPz_JxtmK-xtkjxWjciOQho,2859
12
12
  zrb/builtin/llm/attachment.py,sha256=UwhOGtZY9Px9aebsKfTTw2_ZIpbsqx0XgoAonk6Ewkc,1344
13
- zrb/builtin/llm/chat_completion.py,sha256=ybOo-LZaoUjhEBd1N8R-ZpN7MCV88VHGG9Phs3yLi1w,11060
13
+ zrb/builtin/llm/chat_completion.py,sha256=cWVju5ApC2OPGM3ZWGaXwSgLwgT9leKS_TbV4z610zc,12252
14
14
  zrb/builtin/llm/chat_session.py,sha256=TmXL7IkVT22blgWqG625qpr2_BnDMIm7ERYbR61BL4Q,9154
15
15
  zrb/builtin/llm/chat_session_cmd.py,sha256=lGiN-YNtv957wYuuF_mXV0opbRN61U9pyxVVq-_Gwv8,10842
16
- zrb/builtin/llm/chat_trigger.py,sha256=TZ4u5oQ4W7RGJB2yNrmGRfSEaBbMj4SXEGZHJrYjiJo,2672
16
+ zrb/builtin/llm/chat_trigger.py,sha256=i6_lej2xYfY8k5-CMIUsdYQdUvBV9ma_DM6ZmgLaQIU,2673
17
17
  zrb/builtin/llm/history.py,sha256=qNRYq6SlJH5iuzdra_FOJxevWXCDh5YMvN2qPNLjI8I,3099
18
18
  zrb/builtin/llm/input.py,sha256=Nw-26uTWp2QhUgKJcP_IMHmtk-b542CCSQ_vCOjhvhM,877
19
19
  zrb/builtin/llm/llm_ask.py,sha256=6fUbCFjgjFiTVf-88WJZ1RD5wgOzmsVEpD3dR16Jl_8,8598
20
20
  zrb/builtin/llm/previous-session.js,sha256=xMKZvJoAbrwiyHS0OoPrWuaKxWYLoyR5sguePIoCjTY,816
21
21
  zrb/builtin/llm/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  zrb/builtin/llm/tool/api.py,sha256=ItGJLr_1vhmLcitfIKeVDrnJEWdsyqucDnQ7enIC8fQ,2394
23
- zrb/builtin/llm/tool/cli.py,sha256=iwQtiyPgd5RibUHEtO38c4KS6D4_oCdLqv_bZZqM160,1257
24
- zrb/builtin/llm/tool/code.py,sha256=GdMKCc3Z3bllly0tgtUXMoWdIA9WDjyNcoKftNksJTk,7699
25
- zrb/builtin/llm/tool/file.py,sha256=ktGWwGMl1nYtHFw1go10kuvKCbrtkjPkQ4bvDgWM5iY,19294
26
- zrb/builtin/llm/tool/note.py,sha256=HD7qRldvZUpfHW0i4Dev6UZba5j5r4Z3CugB5umL3Wk,2566
23
+ zrb/builtin/llm/tool/cli.py,sha256=eNmNgNR5H57idYAIXJ50mipAs2KzYGiN44h7xFHvF-k,1917
24
+ zrb/builtin/llm/tool/code.py,sha256=1sErE5tsticXGNhiE0bTjI21Zq6zHMgS2hVM6dsAcww,7916
25
+ zrb/builtin/llm/tool/file.py,sha256=LLdkE_hTZYDm9x3EwAo9Xsp7T_OV6ljmGLJksr-e4CM,19622
26
+ zrb/builtin/llm/tool/note.py,sha256=USDQe8F1ddQp5dhv2u35rUN9EfJQl5BaWJIs6Xg9ZZY,2597
27
27
  zrb/builtin/llm/tool/rag.py,sha256=lmoA42IWHcBOWTsWzYGklLs1YoiEldvu_Bi5bp6gbts,9439
28
+ zrb/builtin/llm/tool/search/__init__.py,sha256=wiMaie_IYt00fak-LrioC1qEZDs0SwYz4UWwidHfpFU,49
29
+ zrb/builtin/llm/tool/search/brave.py,sha256=WDRki3UZ9KnzZ15QxCube6LcgnC8G1bTTv-MWfowwe8,2013
30
+ zrb/builtin/llm/tool/search/searxng.py,sha256=0XzcegO1cO3r926z0O8ostD-R-8ysLQCJXLB4RTQ0J0,1834
31
+ zrb/builtin/llm/tool/search/serpapi.py,sha256=UxRzLqKZGcNhvOzj6CRWQFgtYTN3OCWpyT9w9OKlytc,1843
28
32
  zrb/builtin/llm/tool/sub_agent.py,sha256=UNgsvZvSQ1b5ybB0nRzTiFCJpZG3g2RLj_nzjRZzXMY,5808
29
- zrb/builtin/llm/tool/web.py,sha256=x7iHaGRr71yU3qjfKsoyHvht5SB_S3kWPXX2qliyy6k,6278
33
+ zrb/builtin/llm/tool/web.py,sha256=xtfbGNEW9KYfTg-mTp0wpVziQ5NFi6rslNzR_hvS67s,4122
30
34
  zrb/builtin/md5.py,sha256=690RV2LbW7wQeTFxY-lmmqTSVEEZv3XZbjEUW1Q3XpE,1480
31
35
  zrb/builtin/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
36
  zrb/builtin/project/add/fastapp/fastapp_input.py,sha256=MKlWR_LxWhM_DcULCtLfL_IjTxpDnDBkn9KIqNmajFs,310
@@ -378,7 +382,8 @@ zrb/task/llm/history_summarization.py,sha256=YqLm9Q5xzFVTL5XwnWu3XwaMgSxKskDFvoi
378
382
  zrb/task/llm/print_node.py,sha256=d7OMOJ2kj6iXZS2cC5cyuDF1QDFsA9FxDGX-xfirvxs,8674
379
383
  zrb/task/llm/prompt.py,sha256=jlFNWa1IMCYNQh9Ccq9DB1dq9964IOQJ5jNSC9rMpkw,11366
380
384
  zrb/task/llm/subagent_conversation_history.py,sha256=y8UbKF2GpjB-r1bc2xCpzepriVEq0wfdyFuHOwfpPfk,1647
381
- zrb/task/llm/tool_wrapper.py,sha256=1kKXtZ9Wk6gSZJUpXuccdBZH-8RaQ91lTcTrum1nLog,12440
385
+ zrb/task/llm/tool_confirmation_completer.py,sha256=rOYZdq5No9DfLUG889cr99cUX6XmoO29Yh9FUcdMMio,1534
386
+ zrb/task/llm/tool_wrapper.py,sha256=FfW2JcZ8vUVie0pekpwpQABQ8DhIv8EG7_kPSYGwsVw,12440
382
387
  zrb/task/llm/typing.py,sha256=c8VAuPBw_4A3DxfYdydkgedaP-LU61W9_wj3m3CAX1E,58
383
388
  zrb/task/llm/workflow.py,sha256=eGpUXoIKgbPFPZJ7hHy_HkGYnGWWx9-B9FNjjNm3_8I,2968
384
389
  zrb/task/llm_task.py,sha256=8tNtyufjsWUu5jJQEo9-dpl1N6fycA1ugllaKgLvOao,16571
@@ -432,7 +437,7 @@ zrb/util/truncate.py,sha256=eSzmjBpc1Qod3lM3M73snNbDOcARHukW_tq36dWdPvc,921
432
437
  zrb/util/yaml.py,sha256=MXcqQTOYzx16onFxxg6HNdavxDK4RQjvKUi2NMKd19Y,6510
433
438
  zrb/xcom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
434
439
  zrb/xcom/xcom.py,sha256=W5BOF9w5fVX2ZBlbOTMsXHbY0yU8Advm-_oWJ3DJvDM,1824
435
- zrb-1.21.28.dist-info/METADATA,sha256=fs0DhOtJarqSf8NvHERj0DK3yKjhNV902bLKYvuLa5w,10622
436
- zrb-1.21.28.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
437
- zrb-1.21.28.dist-info/entry_points.txt,sha256=-Pg3ElWPfnaSM-XvXqCxEAa-wfVI6BEgcs386s8C8v8,46
438
- zrb-1.21.28.dist-info/RECORD,,
440
+ zrb-1.21.31.dist-info/METADATA,sha256=LQK_9dOC8LRBm3Paivofzrd3MUWzaTIpXYpMk8p0suk,10622
441
+ zrb-1.21.31.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
442
+ zrb-1.21.31.dist-info/entry_points.txt,sha256=-Pg3ElWPfnaSM-XvXqCxEAa-wfVI6BEgcs386s8C8v8,46
443
+ zrb-1.21.31.dist-info/RECORD,,
File without changes