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.
- zrb/__init__.py +118 -129
- zrb/builtin/__init__.py +54 -2
- zrb/builtin/llm/chat.py +147 -0
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +491 -280
- zrb/config/helper.py +84 -0
- zrb/config/web_auth_config.py +50 -35
- zrb/context/any_shared_context.py +13 -2
- zrb/context/context.py +31 -3
- zrb/context/print_fn.py +13 -0
- zrb/context/shared_context.py +14 -1
- zrb/input/option_input.py +30 -2
- zrb/llm/agent/__init__.py +9 -0
- zrb/llm/agent/agent.py +215 -0
- zrb/llm/agent/summarizer.py +20 -0
- zrb/llm/app/__init__.py +10 -0
- zrb/llm/app/completion.py +281 -0
- zrb/llm/app/confirmation/allow_tool.py +66 -0
- zrb/llm/app/confirmation/handler.py +178 -0
- zrb/llm/app/confirmation/replace_confirmation.py +77 -0
- zrb/llm/app/keybinding.py +34 -0
- zrb/llm/app/layout.py +117 -0
- zrb/llm/app/lexer.py +155 -0
- zrb/llm/app/redirection.py +28 -0
- zrb/llm/app/style.py +16 -0
- zrb/llm/app/ui.py +733 -0
- zrb/llm/config/__init__.py +4 -0
- zrb/llm/config/config.py +122 -0
- zrb/llm/config/limiter.py +247 -0
- zrb/llm/history_manager/__init__.py +4 -0
- zrb/llm/history_manager/any_history_manager.py +23 -0
- zrb/llm/history_manager/file_history_manager.py +91 -0
- zrb/llm/history_processor/summarizer.py +108 -0
- zrb/llm/note/__init__.py +3 -0
- zrb/llm/note/manager.py +122 -0
- zrb/llm/prompt/__init__.py +29 -0
- zrb/llm/prompt/claude_compatibility.py +92 -0
- zrb/llm/prompt/compose.py +55 -0
- zrb/llm/prompt/default.py +51 -0
- zrb/llm/prompt/markdown/mandate.md +23 -0
- zrb/llm/prompt/markdown/persona.md +3 -0
- zrb/llm/prompt/markdown/summarizer.md +21 -0
- zrb/llm/prompt/note.py +41 -0
- zrb/llm/prompt/system_context.py +46 -0
- zrb/llm/prompt/zrb.py +41 -0
- zrb/llm/skill/__init__.py +3 -0
- zrb/llm/skill/manager.py +86 -0
- zrb/llm/task/__init__.py +4 -0
- zrb/llm/task/llm_chat_task.py +316 -0
- zrb/llm/task/llm_task.py +245 -0
- zrb/llm/tool/__init__.py +39 -0
- zrb/llm/tool/bash.py +75 -0
- zrb/llm/tool/code.py +266 -0
- zrb/llm/tool/file.py +419 -0
- zrb/llm/tool/note.py +70 -0
- zrb/{builtin/llm → llm}/tool/rag.py +8 -5
- zrb/llm/tool/search/brave.py +53 -0
- zrb/llm/tool/search/searxng.py +47 -0
- zrb/llm/tool/search/serpapi.py +47 -0
- zrb/llm/tool/skill.py +19 -0
- zrb/llm/tool/sub_agent.py +70 -0
- zrb/llm/tool/web.py +97 -0
- zrb/llm/tool/zrb_task.py +66 -0
- zrb/llm/util/attachment.py +101 -0
- zrb/llm/util/prompt.py +104 -0
- zrb/llm/util/stream_response.py +178 -0
- zrb/session/any_session.py +0 -3
- zrb/session/session.py +1 -1
- zrb/task/base/context.py +25 -13
- zrb/task/base/execution.py +52 -47
- zrb/task/base/lifecycle.py +7 -4
- zrb/task/base_task.py +48 -49
- zrb/task/base_trigger.py +4 -1
- zrb/task/cmd_task.py +6 -0
- zrb/task/http_check.py +11 -5
- zrb/task/make_task.py +3 -0
- zrb/task/rsync_task.py +5 -0
- zrb/task/scaffolder.py +7 -4
- zrb/task/scheduler.py +3 -0
- zrb/task/tcp_check.py +6 -4
- zrb/util/ascii_art/art/bee.txt +17 -0
- zrb/util/ascii_art/art/cat.txt +9 -0
- zrb/util/ascii_art/art/ghost.txt +16 -0
- zrb/util/ascii_art/art/panda.txt +17 -0
- zrb/util/ascii_art/art/rose.txt +14 -0
- zrb/util/ascii_art/art/unicorn.txt +15 -0
- zrb/util/ascii_art/banner.py +92 -0
- zrb/util/cli/markdown.py +22 -2
- zrb/util/cmd/command.py +33 -10
- zrb/util/file.py +51 -32
- zrb/util/match.py +78 -0
- zrb/util/run.py +3 -3
- {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/METADATA +9 -15
- {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/RECORD +100 -128
- zrb/attr/__init__.py +0 -0
- zrb/builtin/llm/attachment.py +0 -40
- zrb/builtin/llm/chat_completion.py +0 -274
- zrb/builtin/llm/chat_session.py +0 -270
- zrb/builtin/llm/chat_session_cmd.py +0 -288
- zrb/builtin/llm/chat_trigger.py +0 -79
- zrb/builtin/llm/history.py +0 -71
- zrb/builtin/llm/input.py +0 -27
- zrb/builtin/llm/llm_ask.py +0 -269
- zrb/builtin/llm/previous-session.js +0 -21
- zrb/builtin/llm/tool/__init__.py +0 -0
- zrb/builtin/llm/tool/api.py +0 -75
- zrb/builtin/llm/tool/cli.py +0 -52
- zrb/builtin/llm/tool/code.py +0 -236
- zrb/builtin/llm/tool/file.py +0 -560
- zrb/builtin/llm/tool/note.py +0 -84
- zrb/builtin/llm/tool/sub_agent.py +0 -150
- zrb/builtin/llm/tool/web.py +0 -171
- zrb/builtin/project/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
- zrb/builtin/project/create/__init__.py +0 -0
- zrb/builtin/shell/__init__.py +0 -0
- zrb/builtin/shell/autocomplete/__init__.py +0 -0
- zrb/callback/__init__.py +0 -0
- zrb/cmd/__init__.py +0 -0
- zrb/config/default_prompt/interactive_system_prompt.md +0 -29
- zrb/config/default_prompt/persona.md +0 -1
- zrb/config/default_prompt/summarization_prompt.md +0 -57
- zrb/config/default_prompt/system_prompt.md +0 -38
- zrb/config/llm_config.py +0 -339
- zrb/config/llm_context/config.py +0 -166
- zrb/config/llm_context/config_parser.py +0 -40
- zrb/config/llm_context/workflow.py +0 -81
- zrb/config/llm_rate_limitter.py +0 -190
- zrb/content_transformer/__init__.py +0 -0
- zrb/context/__init__.py +0 -0
- zrb/dot_dict/__init__.py +0 -0
- zrb/env/__init__.py +0 -0
- zrb/group/__init__.py +0 -0
- zrb/input/__init__.py +0 -0
- zrb/runner/__init__.py +0 -0
- zrb/runner/web_route/__init__.py +0 -0
- zrb/runner/web_route/home_page/__init__.py +0 -0
- zrb/session/__init__.py +0 -0
- zrb/session_state_log/__init__.py +0 -0
- zrb/session_state_logger/__init__.py +0 -0
- zrb/task/__init__.py +0 -0
- zrb/task/base/__init__.py +0 -0
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent.py +0 -204
- zrb/task/llm/agent_runner.py +0 -152
- zrb/task/llm/config.py +0 -122
- zrb/task/llm/conversation_history.py +0 -209
- zrb/task/llm/conversation_history_model.py +0 -67
- zrb/task/llm/default_workflow/coding/workflow.md +0 -41
- zrb/task/llm/default_workflow/copywriting/workflow.md +0 -68
- zrb/task/llm/default_workflow/git/workflow.md +0 -118
- zrb/task/llm/default_workflow/golang/workflow.md +0 -128
- zrb/task/llm/default_workflow/html-css/workflow.md +0 -135
- zrb/task/llm/default_workflow/java/workflow.md +0 -146
- zrb/task/llm/default_workflow/javascript/workflow.md +0 -158
- zrb/task/llm/default_workflow/python/workflow.md +0 -160
- zrb/task/llm/default_workflow/researching/workflow.md +0 -153
- zrb/task/llm/default_workflow/rust/workflow.md +0 -162
- zrb/task/llm/default_workflow/shell/workflow.md +0 -299
- zrb/task/llm/error.py +0 -95
- zrb/task/llm/file_replacement.py +0 -206
- zrb/task/llm/file_tool_model.py +0 -57
- zrb/task/llm/history_processor.py +0 -206
- zrb/task/llm/history_summarization.py +0 -25
- zrb/task/llm/print_node.py +0 -221
- zrb/task/llm/prompt.py +0 -321
- zrb/task/llm/subagent_conversation_history.py +0 -41
- zrb/task/llm/tool_wrapper.py +0 -361
- zrb/task/llm/typing.py +0 -3
- zrb/task/llm/workflow.py +0 -76
- zrb/task/llm_task.py +0 -379
- zrb/task_status/__init__.py +0 -0
- zrb/util/__init__.py +0 -0
- zrb/util/cli/__init__.py +0 -0
- zrb/util/cmd/__init__.py +0 -0
- zrb/util/codemod/__init__.py +0 -0
- zrb/util/string/__init__.py +0 -0
- zrb/xcom/__init__.py +0 -0
- /zrb/{config/default_prompt/file_extractor_system_prompt.md → llm/prompt/markdown/file_extractor.md} +0 -0
- /zrb/{config/default_prompt/repo_extractor_system_prompt.md → llm/prompt/markdown/repo_extractor.md} +0 -0
- /zrb/{config/default_prompt/repo_summarizer_system_prompt.md → llm/prompt/markdown/repo_summarizer.md} +0 -0
- {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
20
|
+
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zrb
|
|
3
|
-
Version:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
135
|
-
from zrb.
|
|
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
|
-
|
|
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
|
|