zrb 1.21.29__py3-none-any.whl → 2.0.0a4__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 (192) hide show
  1. zrb/__init__.py +118 -129
  2. zrb/builtin/__init__.py +54 -2
  3. zrb/builtin/llm/chat.py +147 -0
  4. zrb/callback/callback.py +8 -1
  5. zrb/cmd/cmd_result.py +2 -1
  6. zrb/config/config.py +491 -280
  7. zrb/config/helper.py +84 -0
  8. zrb/config/web_auth_config.py +50 -35
  9. zrb/context/any_shared_context.py +13 -2
  10. zrb/context/context.py +31 -3
  11. zrb/context/print_fn.py +13 -0
  12. zrb/context/shared_context.py +14 -1
  13. zrb/input/option_input.py +30 -2
  14. zrb/llm/agent/__init__.py +9 -0
  15. zrb/llm/agent/agent.py +215 -0
  16. zrb/llm/agent/summarizer.py +20 -0
  17. zrb/llm/app/__init__.py +10 -0
  18. zrb/llm/app/completion.py +281 -0
  19. zrb/llm/app/confirmation/allow_tool.py +66 -0
  20. zrb/llm/app/confirmation/handler.py +178 -0
  21. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  22. zrb/llm/app/keybinding.py +34 -0
  23. zrb/llm/app/layout.py +117 -0
  24. zrb/llm/app/lexer.py +155 -0
  25. zrb/llm/app/redirection.py +28 -0
  26. zrb/llm/app/style.py +16 -0
  27. zrb/llm/app/ui.py +733 -0
  28. zrb/llm/config/__init__.py +4 -0
  29. zrb/llm/config/config.py +122 -0
  30. zrb/llm/config/limiter.py +247 -0
  31. zrb/llm/history_manager/__init__.py +4 -0
  32. zrb/llm/history_manager/any_history_manager.py +23 -0
  33. zrb/llm/history_manager/file_history_manager.py +91 -0
  34. zrb/llm/history_processor/summarizer.py +108 -0
  35. zrb/llm/note/__init__.py +3 -0
  36. zrb/llm/note/manager.py +122 -0
  37. zrb/llm/prompt/__init__.py +29 -0
  38. zrb/llm/prompt/claude_compatibility.py +92 -0
  39. zrb/llm/prompt/compose.py +55 -0
  40. zrb/llm/prompt/default.py +51 -0
  41. zrb/llm/prompt/markdown/mandate.md +23 -0
  42. zrb/llm/prompt/markdown/persona.md +3 -0
  43. zrb/llm/prompt/markdown/summarizer.md +21 -0
  44. zrb/llm/prompt/note.py +41 -0
  45. zrb/llm/prompt/system_context.py +46 -0
  46. zrb/llm/prompt/zrb.py +41 -0
  47. zrb/llm/skill/__init__.py +3 -0
  48. zrb/llm/skill/manager.py +86 -0
  49. zrb/llm/task/__init__.py +4 -0
  50. zrb/llm/task/llm_chat_task.py +316 -0
  51. zrb/llm/task/llm_task.py +245 -0
  52. zrb/llm/tool/__init__.py +39 -0
  53. zrb/llm/tool/bash.py +75 -0
  54. zrb/llm/tool/code.py +266 -0
  55. zrb/llm/tool/file.py +419 -0
  56. zrb/llm/tool/note.py +70 -0
  57. zrb/{builtin/llm → llm}/tool/rag.py +8 -5
  58. zrb/llm/tool/search/brave.py +53 -0
  59. zrb/llm/tool/search/searxng.py +47 -0
  60. zrb/llm/tool/search/serpapi.py +47 -0
  61. zrb/llm/tool/skill.py +19 -0
  62. zrb/llm/tool/sub_agent.py +70 -0
  63. zrb/llm/tool/web.py +97 -0
  64. zrb/llm/tool/zrb_task.py +66 -0
  65. zrb/llm/util/attachment.py +101 -0
  66. zrb/llm/util/prompt.py +104 -0
  67. zrb/llm/util/stream_response.py +178 -0
  68. zrb/session/any_session.py +0 -3
  69. zrb/session/session.py +1 -1
  70. zrb/task/base/context.py +25 -13
  71. zrb/task/base/execution.py +52 -47
  72. zrb/task/base/lifecycle.py +7 -4
  73. zrb/task/base_task.py +48 -49
  74. zrb/task/base_trigger.py +4 -1
  75. zrb/task/cmd_task.py +6 -0
  76. zrb/task/http_check.py +11 -5
  77. zrb/task/make_task.py +3 -0
  78. zrb/task/rsync_task.py +5 -0
  79. zrb/task/scaffolder.py +7 -4
  80. zrb/task/scheduler.py +3 -0
  81. zrb/task/tcp_check.py +6 -4
  82. zrb/util/ascii_art/art/bee.txt +17 -0
  83. zrb/util/ascii_art/art/cat.txt +9 -0
  84. zrb/util/ascii_art/art/ghost.txt +16 -0
  85. zrb/util/ascii_art/art/panda.txt +17 -0
  86. zrb/util/ascii_art/art/rose.txt +14 -0
  87. zrb/util/ascii_art/art/unicorn.txt +15 -0
  88. zrb/util/ascii_art/banner.py +92 -0
  89. zrb/util/cli/markdown.py +22 -2
  90. zrb/util/cmd/command.py +33 -10
  91. zrb/util/file.py +51 -32
  92. zrb/util/match.py +78 -0
  93. zrb/util/run.py +3 -3
  94. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/METADATA +9 -15
  95. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/RECORD +100 -128
  96. zrb/attr/__init__.py +0 -0
  97. zrb/builtin/llm/attachment.py +0 -40
  98. zrb/builtin/llm/chat_completion.py +0 -274
  99. zrb/builtin/llm/chat_session.py +0 -270
  100. zrb/builtin/llm/chat_session_cmd.py +0 -288
  101. zrb/builtin/llm/chat_trigger.py +0 -79
  102. zrb/builtin/llm/history.py +0 -71
  103. zrb/builtin/llm/input.py +0 -27
  104. zrb/builtin/llm/llm_ask.py +0 -269
  105. zrb/builtin/llm/previous-session.js +0 -21
  106. zrb/builtin/llm/tool/__init__.py +0 -0
  107. zrb/builtin/llm/tool/api.py +0 -75
  108. zrb/builtin/llm/tool/cli.py +0 -52
  109. zrb/builtin/llm/tool/code.py +0 -236
  110. zrb/builtin/llm/tool/file.py +0 -560
  111. zrb/builtin/llm/tool/note.py +0 -84
  112. zrb/builtin/llm/tool/sub_agent.py +0 -150
  113. zrb/builtin/llm/tool/web.py +0 -171
  114. zrb/builtin/project/__init__.py +0 -0
  115. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  116. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  117. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  118. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  119. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  120. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  121. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  122. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  123. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  124. zrb/builtin/project/create/__init__.py +0 -0
  125. zrb/builtin/shell/__init__.py +0 -0
  126. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  127. zrb/callback/__init__.py +0 -0
  128. zrb/cmd/__init__.py +0 -0
  129. zrb/config/default_prompt/interactive_system_prompt.md +0 -29
  130. zrb/config/default_prompt/persona.md +0 -1
  131. zrb/config/default_prompt/summarization_prompt.md +0 -57
  132. zrb/config/default_prompt/system_prompt.md +0 -38
  133. zrb/config/llm_config.py +0 -339
  134. zrb/config/llm_context/config.py +0 -166
  135. zrb/config/llm_context/config_parser.py +0 -40
  136. zrb/config/llm_context/workflow.py +0 -81
  137. zrb/config/llm_rate_limitter.py +0 -190
  138. zrb/content_transformer/__init__.py +0 -0
  139. zrb/context/__init__.py +0 -0
  140. zrb/dot_dict/__init__.py +0 -0
  141. zrb/env/__init__.py +0 -0
  142. zrb/group/__init__.py +0 -0
  143. zrb/input/__init__.py +0 -0
  144. zrb/runner/__init__.py +0 -0
  145. zrb/runner/web_route/__init__.py +0 -0
  146. zrb/runner/web_route/home_page/__init__.py +0 -0
  147. zrb/session/__init__.py +0 -0
  148. zrb/session_state_log/__init__.py +0 -0
  149. zrb/session_state_logger/__init__.py +0 -0
  150. zrb/task/__init__.py +0 -0
  151. zrb/task/base/__init__.py +0 -0
  152. zrb/task/llm/__init__.py +0 -0
  153. zrb/task/llm/agent.py +0 -204
  154. zrb/task/llm/agent_runner.py +0 -152
  155. zrb/task/llm/config.py +0 -122
  156. zrb/task/llm/conversation_history.py +0 -209
  157. zrb/task/llm/conversation_history_model.py +0 -67
  158. zrb/task/llm/default_workflow/coding/workflow.md +0 -41
  159. zrb/task/llm/default_workflow/copywriting/workflow.md +0 -68
  160. zrb/task/llm/default_workflow/git/workflow.md +0 -118
  161. zrb/task/llm/default_workflow/golang/workflow.md +0 -128
  162. zrb/task/llm/default_workflow/html-css/workflow.md +0 -135
  163. zrb/task/llm/default_workflow/java/workflow.md +0 -146
  164. zrb/task/llm/default_workflow/javascript/workflow.md +0 -158
  165. zrb/task/llm/default_workflow/python/workflow.md +0 -160
  166. zrb/task/llm/default_workflow/researching/workflow.md +0 -153
  167. zrb/task/llm/default_workflow/rust/workflow.md +0 -162
  168. zrb/task/llm/default_workflow/shell/workflow.md +0 -299
  169. zrb/task/llm/error.py +0 -95
  170. zrb/task/llm/file_replacement.py +0 -206
  171. zrb/task/llm/file_tool_model.py +0 -57
  172. zrb/task/llm/history_processor.py +0 -206
  173. zrb/task/llm/history_summarization.py +0 -25
  174. zrb/task/llm/print_node.py +0 -221
  175. zrb/task/llm/prompt.py +0 -321
  176. zrb/task/llm/subagent_conversation_history.py +0 -41
  177. zrb/task/llm/tool_wrapper.py +0 -361
  178. zrb/task/llm/typing.py +0 -3
  179. zrb/task/llm/workflow.py +0 -76
  180. zrb/task/llm_task.py +0 -379
  181. zrb/task_status/__init__.py +0 -0
  182. zrb/util/__init__.py +0 -0
  183. zrb/util/cli/__init__.py +0 -0
  184. zrb/util/cmd/__init__.py +0 -0
  185. zrb/util/codemod/__init__.py +0 -0
  186. zrb/util/string/__init__.py +0 -0
  187. zrb/xcom/__init__.py +0 -0
  188. /zrb/{config/default_prompt/file_extractor_system_prompt.md → llm/prompt/markdown/file_extractor.md} +0 -0
  189. /zrb/{config/default_prompt/repo_extractor_system_prompt.md → llm/prompt/markdown/repo_extractor.md} +0 -0
  190. /zrb/{config/default_prompt/repo_summarizer_system_prompt.md → llm/prompt/markdown/repo_summarizer.md} +0 -0
  191. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +0 -0
  192. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,92 @@
1
+ import os
2
+ import random
3
+
4
+
5
+ def create_banner(art: str | None = None, text: str | None = None) -> str:
6
+ # First get art using _get_art_only
7
+ art_content = _get_art_only(art)
8
+
9
+ # If no text provided, just return the art
10
+ if text is None or text.strip() == "":
11
+ return art_content
12
+
13
+ # Find the longest line in the art, make every line has the same length
14
+ art_lines = art_content.splitlines()
15
+ if not art_lines:
16
+ return text
17
+
18
+ # Find the maximum line length in the art
19
+ max_art_length = max(len(line) for line in art_lines)
20
+
21
+ # Pad all art lines to the same length
22
+ padded_art_lines = [line.ljust(max_art_length) for line in art_lines]
23
+
24
+ # Split text into lines
25
+ text_lines = text.splitlines()
26
+
27
+ # Combine art and text lines
28
+ combined_lines = []
29
+
30
+ # Determine the maximum number of lines we need
31
+ max_lines = max(len(padded_art_lines), len(text_lines))
32
+
33
+ # Calculate vertical offsets for centering
34
+ art_offset = (max_lines - len(padded_art_lines)) // 2
35
+ text_offset = (max_lines - len(text_lines)) // 2
36
+
37
+ for i in range(max_lines):
38
+ # Get art line (or empty string if we've run out of art lines)
39
+ art_index = i - art_offset
40
+ if 0 <= art_index < len(padded_art_lines):
41
+ art_line = padded_art_lines[art_index]
42
+ else:
43
+ art_line = " " * max_art_length
44
+
45
+ # Get text line (or empty string if we've run out of text lines)
46
+ text_index = i - text_offset
47
+ if 0 <= text_index < len(text_lines):
48
+ text_line = text_lines[text_index]
49
+ else:
50
+ text_line = ""
51
+
52
+ # Combine art and text lines
53
+ combined_line = art_line + " " + text_line
54
+ combined_lines.append(combined_line)
55
+
56
+ # Return the combined result
57
+ return "\n".join(combined_lines)
58
+
59
+
60
+ def _get_art_only(art: str | None = None) -> str:
61
+ # If name is provided
62
+ if art is not None:
63
+ # 1) name is a file, load the content of the file, return
64
+ expanded_name = os.path.expanduser(art)
65
+ if os.path.isfile(expanded_name):
66
+ with open(expanded_name, "r") as f:
67
+ return f.read()
68
+
69
+ # 2) name is a string, but not a file
70
+ # Check if art/name.txt exists in the script directory
71
+ cwd = os.path.dirname(__file__)
72
+ art_path = os.path.join(cwd, "art", f"{art}.txt")
73
+ if os.path.isfile(art_path):
74
+ with open(art_path, "r") as f:
75
+ return f.read()
76
+
77
+ # 3) otherwise load random file from art/ directory
78
+ cwd = os.path.dirname(__file__)
79
+ art_dir = os.path.join(cwd, "art")
80
+ # Get all .txt files in the art directory
81
+ try:
82
+ art_files = [f for f in os.listdir(art_dir) if f.endswith(".txt")]
83
+ except FileNotFoundError:
84
+ # If art directory doesn't exist, return empty string
85
+ return ""
86
+ if not art_files:
87
+ return ""
88
+ # Select a random file
89
+ random_file = random.choice(art_files)
90
+ random_file_path = os.path.join(art_dir, random_file)
91
+ with open(random_file_path, "r") as f:
92
+ return f.read()
zrb/util/cli/markdown.py CHANGED
@@ -1,11 +1,31 @@
1
- def render_markdown(markdown_text: str) -> str:
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from rich.theme import Theme
5
+
6
+
7
+ def render_markdown(
8
+ markdown_text: str, width: int | None = None, theme: "Theme | None" = None
9
+ ) -> str:
2
10
  """
3
11
  Renders Markdown to a string, ensuring link URLs are visible.
4
12
  """
5
13
  from rich.console import Console
6
14
  from rich.markdown import Markdown
15
+ from rich.theme import Theme
16
+
17
+ if theme is None:
18
+ theme = Theme(
19
+ {
20
+ "markdown.link": "bold bright_cyan underline",
21
+ "markdown.link_url": "italic bright_cyan underline",
22
+ # Optional: You can customize headers or code blocks here too
23
+ "markdown.h1": "bold magenta",
24
+ "markdown.code": "bold white on #333333",
25
+ }
26
+ )
7
27
 
8
- console = Console()
28
+ console = Console(width=width, theme=theme, force_terminal=True)
9
29
  markdown = Markdown(markdown_text, hyperlinks=False)
10
30
  with console.capture() as capture:
11
31
  console.print(markdown)
zrb/util/cmd/command.py CHANGED
@@ -5,7 +5,7 @@ import signal
5
5
  import sys
6
6
  from collections import deque
7
7
  from collections.abc import Callable
8
- from typing import TextIO
8
+ from typing import Any, TextIO
9
9
 
10
10
  import psutil
11
11
 
@@ -62,6 +62,8 @@ async def run_command(
62
62
  register_pid_method: Callable[[int], None] | None = None,
63
63
  max_output_line: int = 1000,
64
64
  max_error_line: int = 1000,
65
+ max_display_line: int | None = None,
66
+ timeout: int = 3600,
65
67
  is_interactive: bool = False,
66
68
  ) -> tuple[CmdResult, int]:
67
69
  """
@@ -77,6 +79,8 @@ async def run_command(
77
79
  actual_print_method = print_method if print_method is not None else print
78
80
  if cwd is None:
79
81
  cwd = os.getcwd()
82
+ if max_display_line is None:
83
+ max_display_line = max(max_output_line, max_error_line)
80
84
  # While environment variables alone weren't the fix, they are still
81
85
  # good practice for encouraging simpler output from tools.
82
86
  child_env = (env_map or os.environ).copy()
@@ -95,17 +99,33 @@ async def run_command(
95
99
  if register_pid_method is not None:
96
100
  register_pid_method(cmd_process.pid)
97
101
  # Use the new, simple, and correct stream reader.
102
+ display_lines = deque(maxlen=max_display_line if max_display_line > 0 else 0)
98
103
  stdout_task = asyncio.create_task(
99
- __read_stream(cmd_process.stdout, actual_print_method, max_output_line)
104
+ __read_stream(
105
+ cmd_process.stdout, actual_print_method, max_output_line, display_lines
106
+ )
100
107
  )
101
108
  stderr_task = asyncio.create_task(
102
- __read_stream(cmd_process.stderr, actual_print_method, max_error_line)
109
+ __read_stream(
110
+ cmd_process.stderr, actual_print_method, max_error_line, display_lines
111
+ )
112
+ )
113
+ timeout_task = (
114
+ asyncio.create_task(asyncio.sleep(timeout)) if timeout and timeout > 0 else None
103
115
  )
104
116
  try:
105
- return_code = await cmd_process.wait()
117
+ wait_task = asyncio.create_task(cmd_process.wait())
118
+ done, pending = await asyncio.wait(
119
+ {wait_task, timeout_task} if timeout_task else {wait_task},
120
+ return_when=asyncio.FIRST_COMPLETED,
121
+ )
122
+ if timeout_task and timeout_task in done:
123
+ raise asyncio.TimeoutError()
124
+ return_code = wait_task.result()
106
125
  stdout, stderr = await asyncio.gather(stdout_task, stderr_task)
107
- return CmdResult(stdout, stderr), return_code
108
- except (KeyboardInterrupt, asyncio.CancelledError):
126
+ display = "\r\n".join(display_lines)
127
+ return CmdResult(stdout, stderr, display=display), return_code
128
+ except (KeyboardInterrupt, asyncio.CancelledError, asyncio.TimeoutError):
109
129
  try:
110
130
  os.killpg(cmd_process.pid, signal.SIGINT)
111
131
  await asyncio.wait_for(cmd_process.wait(), timeout=2.0)
@@ -133,13 +153,14 @@ def __get_cmd_stdin(is_interactive: bool) -> int | TextIO:
133
153
  async def __read_stream(
134
154
  stream: asyncio.StreamReader,
135
155
  print_method: Callable[..., None],
136
- max_lines: int,
156
+ max_line: int,
157
+ display_queue: deque[Any],
137
158
  ) -> str:
138
159
  """
139
160
  Reads from the stream using the robust `readline()` and correctly
140
161
  interprets carriage returns (`\r`) as distinct print events.
141
162
  """
142
- captured_lines = deque(maxlen=max_lines if max_lines > 0 else 0)
163
+ captured_lines = deque(maxlen=max_line if max_line > 0 else 0)
143
164
  while True:
144
165
  try:
145
166
  line_bytes = await stream.readline()
@@ -149,8 +170,9 @@ async def __read_stream(
149
170
  # Safety valve for the memory limit.
150
171
  error_msg = "[ERROR] A single line of output was too long to process."
151
172
  print_method(error_msg)
152
- if max_lines > 0:
173
+ if max_line > 0:
153
174
  captured_lines.append(error_msg)
175
+ display_queue.append(error_msg)
154
176
  break
155
177
  except (KeyboardInterrupt, asyncio.CancelledError):
156
178
  raise
@@ -165,8 +187,9 @@ async def __read_stream(
165
187
  print_method(clean_part, end="\r\n")
166
188
  except Exception:
167
189
  print_method(clean_part)
168
- if max_lines > 0:
190
+ if max_line > 0:
169
191
  captured_lines.append(clean_part)
192
+ display_queue.append(clean_part)
170
193
  return "\r\n".join(captured_lines)
171
194
 
172
195
 
zrb/util/file.py CHANGED
@@ -1,3 +1,4 @@
1
+ import fnmatch
1
2
  import os
2
3
  import re
3
4
  from typing import Literal
@@ -49,38 +50,6 @@ def _read_pdf_file_content(file_path: str) -> str:
49
50
  )
50
51
 
51
52
 
52
- def read_file_with_line_numbers(
53
- file_path: str, replace_map: dict[str, str] = {}
54
- ) -> str:
55
- """Reads a file and returns content with line numbers.
56
-
57
- Args:
58
- file_path: The path to the file.
59
- replace_map: A dictionary of strings to replace.
60
-
61
- Returns:
62
- The content of the file with line numbers and replacements applied.
63
- """
64
- content = read_file(file_path, replace_map)
65
- if not content:
66
- return ""
67
- lines = content.splitlines()
68
- numbered_lines = [f"{i + 1} | {line}" for i, line in enumerate(lines)]
69
- return "\n".join(numbered_lines)
70
-
71
-
72
- def read_dir(dir_path: str) -> list[str]:
73
- """Reads a directory and returns a list of file names.
74
-
75
- Args:
76
- dir_path: The path to the directory.
77
-
78
- Returns:
79
- A list of file names in the directory.
80
- """
81
- return [f for f in os.listdir(os.path.abspath(os.path.expanduser(dir_path)))]
82
-
83
-
84
53
  def write_file(
85
54
  file_path: str,
86
55
  content: str | list[str],
@@ -106,3 +75,53 @@ def write_file(
106
75
  content += "\n"
107
76
  with open(abs_file_path, mode) as f:
108
77
  f.write(content)
78
+
79
+
80
+ def list_files(
81
+ path: str = ".",
82
+ include_hidden: bool = False,
83
+ depth: int = 3,
84
+ excluded_patterns: list[str] = [],
85
+ ) -> list[str]:
86
+ all_files: list[str] = []
87
+ abs_path = os.path.abspath(os.path.expanduser(path))
88
+ if not os.path.exists(abs_path):
89
+ raise FileNotFoundError(f"Path does not exist: {path}")
90
+
91
+ patterns_to_exclude = excluded_patterns
92
+ if depth <= 0:
93
+ depth = 1
94
+
95
+ initial_depth = abs_path.rstrip(os.sep).count(os.sep)
96
+ for root, dirs, files in os.walk(abs_path, topdown=True):
97
+ current_depth = root.rstrip(os.sep).count(os.sep) - initial_depth
98
+ if current_depth >= depth - 1:
99
+ del dirs[:]
100
+
101
+ dirs[:] = [
102
+ d
103
+ for d in dirs
104
+ if (include_hidden or not d.startswith("."))
105
+ and not _is_excluded(d, patterns_to_exclude)
106
+ ]
107
+
108
+ for filename in files:
109
+ if (include_hidden or not filename.startswith(".")) and not _is_excluded(
110
+ filename, patterns_to_exclude
111
+ ):
112
+ full_path = os.path.join(root, filename)
113
+ rel_full_path = os.path.relpath(full_path, abs_path)
114
+ if not _is_excluded(rel_full_path, patterns_to_exclude):
115
+ all_files.append(rel_full_path)
116
+ return sorted(all_files)
117
+
118
+
119
+ def _is_excluded(name: str, patterns: list[str]) -> bool:
120
+ for pattern in patterns:
121
+ if fnmatch.fnmatch(name, pattern):
122
+ return True
123
+ parts = name.split(os.path.sep)
124
+ for part in parts:
125
+ if fnmatch.fnmatch(part, pattern):
126
+ return True
127
+ return False
zrb/util/match.py ADDED
@@ -0,0 +1,78 @@
1
+ import os
2
+ import re
3
+
4
+
5
+ def fuzzy_match(text: str, pattern: str) -> tuple[bool, float]:
6
+ """
7
+ Match text against a pattern using a fuzzy search algorithm similar to VSCode's Ctrl+P.
8
+
9
+ The pattern is split into tokens by whitespace and path separators.
10
+ Each token must be found in the text (in order).
11
+
12
+ Args:
13
+ text: The string to search in.
14
+ pattern: The search pattern (e.g., "src main" or "util/io").
15
+
16
+ Returns:
17
+ A tuple (matched, score).
18
+ - matched: True if the pattern matches the text.
19
+ - score: A float representing the match quality (lower is better).
20
+ """
21
+ text_cmp = text.lower()
22
+ # Normalize pattern -> tokens split on path separators or whitespace
23
+ search_pattern = pattern.strip()
24
+ tokens = (
25
+ [t for t in re.split(rf"[{re.escape(os.path.sep)}\s]+", search_pattern) if t]
26
+ if search_pattern
27
+ else []
28
+ )
29
+ tokens = [t.lower() for t in tokens]
30
+ if not tokens:
31
+ return True, 0.0
32
+ last_pos = 0
33
+ score = 0.0
34
+ for token in tokens:
35
+ # try contiguous substring search first
36
+ idx = text_cmp.find(token, last_pos)
37
+ if idx != -1:
38
+ # good match: reward contiguous early matches
39
+ score += idx # smaller idx preferred
40
+ last_pos = idx + len(token)
41
+ else:
42
+ # fallback to subsequence matching
43
+ res = _find_subsequence_range(text_cmp, token, last_pos)
44
+ if res is None:
45
+ return False, 0.0
46
+
47
+ pos, end_pos = res
48
+
49
+ # subsequence match is less preferred than contiguous substring
50
+ score += pos + 0.5 * len(token)
51
+ last_pos = end_pos
52
+ # prefer shorter texts when score ties, so include length as tiebreaker
53
+ score += 0.01 * len(text)
54
+ return True, score
55
+
56
+
57
+ def _find_subsequence_range(
58
+ hay: str, needle: str, start: int = 0
59
+ ) -> tuple[int, int] | None:
60
+ """
61
+ Try to locate needle in hay as a subsequence starting at `start`.
62
+ Returns (start_index, end_index) where end_index is the index AFTER the last matched character.
63
+ """
64
+ if not needle:
65
+ return start, start
66
+ i = start
67
+ j = 0
68
+ first_pos = None
69
+ while i < len(hay) and j < len(needle):
70
+ if hay[i] == needle[j]:
71
+ if first_pos is None:
72
+ first_pos = i
73
+ j += 1
74
+ i += 1
75
+
76
+ if j == len(needle):
77
+ return first_pos, i
78
+ return None
zrb/util/run.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any
5
5
 
6
6
  async def run_async(value: Any) -> Any:
7
7
  """
8
- Run a value asynchronously, awaiting if it's awaitable or running in a thread if not.
8
+ Run a value asynchronously, awaiting if it's awaitable or returning it directly.
9
9
 
10
10
  Args:
11
11
  value (Any): The value to run. Can be awaitable or not.
@@ -14,7 +14,7 @@ async def run_async(value: Any) -> Any:
14
14
  Any: The result of the awaited value or the value itself if not awaitable.
15
15
  """
16
16
  if isinstance(value, asyncio.Task):
17
- return value
17
+ return await value
18
18
  if inspect.isawaitable(value):
19
19
  return await value
20
- return await asyncio.to_thread(lambda: value)
20
+ return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zrb
3
- Version: 1.21.29
3
+ Version: 2.0.0a4
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
@@ -24,28 +24,28 @@ Provides-Extra: mistral
24
24
  Provides-Extra: playwright
25
25
  Provides-Extra: rag
26
26
  Provides-Extra: vertexai
27
- Requires-Dist: anthropic (>=0.70.0) ; extra == "anthropic" or extra == "all"
27
+ Requires-Dist: anthropic (>=0.75.0) ; extra == "anthropic" or extra == "all"
28
28
  Requires-Dist: beautifulsoup4 (>=4.14.2,<5.0.0)
29
29
  Requires-Dist: black (>=25.11.0,<26.0.0)
30
- Requires-Dist: boto3 (>=1.40.14) ; extra == "bedrock"
30
+ Requires-Dist: boto3 (>=1.42.14) ; extra == "bedrock"
31
31
  Requires-Dist: chromadb (>=1.3.5,<2.0.0) ; extra == "rag" or extra == "all"
32
32
  Requires-Dist: cohere (>=5.18.0) ; extra == "cohere" or extra == "all"
33
33
  Requires-Dist: fastapi[standard] (>=0.123.9,<0.124.0)
34
34
  Requires-Dist: google-auth (>=2.36.0) ; extra == "vertexai" or extra == "all"
35
- Requires-Dist: google-genai (>=1.51.0) ; extra == "google" or extra == "all"
35
+ Requires-Dist: google-genai (>=1.56.0) ; extra == "google" or extra == "all"
36
36
  Requires-Dist: groq (>=0.25.0) ; extra == "groq" or extra == "all"
37
37
  Requires-Dist: huggingface-hub[inference] (>=0.33.5,<1.0.0) ; extra == "huggingface"
38
38
  Requires-Dist: isort (>=7.0.0,<8.0.0)
39
39
  Requires-Dist: libcst (>=1.8.6,<2.0.0)
40
40
  Requires-Dist: markdownify (>=1.2.2,<2.0.0)
41
41
  Requires-Dist: mcp (>1.18.0)
42
- Requires-Dist: mistralai (>=1.9.10) ; extra == "mistral"
42
+ Requires-Dist: mistralai (>=1.9.11) ; extra == "mistral"
43
43
  Requires-Dist: openai (>=2.11.0)
44
44
  Requires-Dist: pdfplumber (>=0.11.7,<0.12.0)
45
45
  Requires-Dist: playwright (>=1.56.0,<2.0.0) ; extra == "playwright" or extra == "all"
46
46
  Requires-Dist: prompt-toolkit (>=3)
47
47
  Requires-Dist: psutil (>=7.0.0,<8.0.0)
48
- Requires-Dist: pydantic-ai-slim (>=1.32.0,<1.33.0)
48
+ Requires-Dist: pydantic-ai-slim (>=1.42.0,<1.43.0)
49
49
  Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
50
50
  Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
51
51
  Requires-Dist: python-jose[cryptography] (>=3.5.0,<4.0.0)
@@ -131,8 +131,8 @@ Add the following Python code to your `zrb_init.py`:
131
131
 
132
132
  ```python
133
133
  from zrb import cli, LLMTask, CmdTask, StrInput, Group
134
- from zrb.builtin.llm.tool.code import analyze_repo
135
- from zrb.builtin.llm.tool.file import write_to_file
134
+ from zrb.llm.tool.code import analyze_code
135
+ from zrb.llm.tool.file import write_file
136
136
 
137
137
 
138
138
  # Create a group for Mermaid-related tasks
@@ -156,7 +156,7 @@ make_mermaid_script = mermaid_group.add_task(
156
156
  "Write the script into `{ctx.input.dir}/{ctx.input.diagram}.mmd`"
157
157
  ),
158
158
  tools=[
159
- analyze_repo, write_to_file
159
+ analyze_code, write_file
160
160
  ],
161
161
  )
162
162
  )
@@ -232,13 +232,7 @@ Start a chat session with an LLM to ask questions, brainstorm ideas, or get codi
232
232
  zrb llm chat
233
233
  ```
234
234
 
235
- ### Quick Questions
236
235
 
237
- For a single question, use the `ask` command for a fast response.
238
-
239
- ```bash
240
- zrb llm ask "What is the capital of Indonesia?"
241
- ```
242
236
 
243
237
  ---
244
238