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
zrb/builtin/llm/tool/file.py
DELETED
|
@@ -1,560 +0,0 @@
|
|
|
1
|
-
import fnmatch
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
from typing import Any, Optional
|
|
6
|
-
|
|
7
|
-
from zrb.builtin.llm.tool.sub_agent import create_sub_agent_tool
|
|
8
|
-
from zrb.config.config import CFG
|
|
9
|
-
from zrb.config.llm_rate_limitter import llm_rate_limitter
|
|
10
|
-
from zrb.context.any_context import AnyContext
|
|
11
|
-
from zrb.task.llm.file_tool_model import FileReplacement, FileToRead, FileToWrite
|
|
12
|
-
from zrb.util.file import read_file, read_file_with_line_numbers, write_file
|
|
13
|
-
|
|
14
|
-
DEFAULT_EXCLUDED_PATTERNS = [
|
|
15
|
-
# Common Python artifacts
|
|
16
|
-
"__pycache__",
|
|
17
|
-
"*.pyc",
|
|
18
|
-
"*.pyo",
|
|
19
|
-
"*.pyd",
|
|
20
|
-
".Python",
|
|
21
|
-
"build",
|
|
22
|
-
"develop-eggs",
|
|
23
|
-
"dist",
|
|
24
|
-
"downloads",
|
|
25
|
-
"eggs",
|
|
26
|
-
".eggs",
|
|
27
|
-
"lib",
|
|
28
|
-
"lib64",
|
|
29
|
-
"parts",
|
|
30
|
-
"sdist",
|
|
31
|
-
"var",
|
|
32
|
-
"wheels",
|
|
33
|
-
"share/python-wheels",
|
|
34
|
-
"*.egg-info",
|
|
35
|
-
".installed.cfg",
|
|
36
|
-
"*.egg",
|
|
37
|
-
"MANIFEST",
|
|
38
|
-
# Virtual environments
|
|
39
|
-
".env",
|
|
40
|
-
".venv",
|
|
41
|
-
"env",
|
|
42
|
-
"venv",
|
|
43
|
-
"ENV",
|
|
44
|
-
"VENV",
|
|
45
|
-
# Editor/IDE specific
|
|
46
|
-
".idea",
|
|
47
|
-
".vscode",
|
|
48
|
-
"*.swp",
|
|
49
|
-
"*.swo",
|
|
50
|
-
"*.swn",
|
|
51
|
-
# OS specific
|
|
52
|
-
".DS_Store",
|
|
53
|
-
"Thumbs.db",
|
|
54
|
-
# Version control
|
|
55
|
-
".git",
|
|
56
|
-
".hg",
|
|
57
|
-
".svn",
|
|
58
|
-
# Node.js
|
|
59
|
-
"node_modules",
|
|
60
|
-
"npm-debug.log*",
|
|
61
|
-
"yarn-debug.log*",
|
|
62
|
-
"yarn-error.log*",
|
|
63
|
-
# Test/Coverage artifacts
|
|
64
|
-
".history",
|
|
65
|
-
".tox",
|
|
66
|
-
".nox",
|
|
67
|
-
".coverage",
|
|
68
|
-
".coverage.*",
|
|
69
|
-
".cache",
|
|
70
|
-
".pytest_cache",
|
|
71
|
-
".hypothesis",
|
|
72
|
-
"htmlcov",
|
|
73
|
-
# Compiled files
|
|
74
|
-
"*.so",
|
|
75
|
-
"*.dylib",
|
|
76
|
-
"*.dll",
|
|
77
|
-
# Minified files
|
|
78
|
-
"*.min.css",
|
|
79
|
-
"*.min.js",
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def list_files(
|
|
84
|
-
path: str = ".",
|
|
85
|
-
include_hidden: bool = False,
|
|
86
|
-
depth: int = 3,
|
|
87
|
-
excluded_patterns: Optional[list[str]] = None,
|
|
88
|
-
) -> dict[str, list[str]]:
|
|
89
|
-
"""
|
|
90
|
-
Lists files recursively up to a specified depth.
|
|
91
|
-
|
|
92
|
-
Example:
|
|
93
|
-
list_files(path='src', include_hidden=False, depth=2)
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
path (str): Directory path. Defaults to current directory.
|
|
97
|
-
include_hidden (bool): Include hidden files. Defaults to False.
|
|
98
|
-
depth (int): Maximum depth to traverse. Defaults to 3.
|
|
99
|
-
Minimum depth is 1 (current directory only).
|
|
100
|
-
excluded_patterns (list[str]): Glob patterns to exclude.
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
dict: {'files': [relative_paths]}
|
|
104
|
-
"""
|
|
105
|
-
all_files: list[str] = []
|
|
106
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
107
|
-
# Explicitly check if path exists before proceeding
|
|
108
|
-
if not os.path.exists(abs_path):
|
|
109
|
-
# Raise FileNotFoundError, which is a subclass of OSError
|
|
110
|
-
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
111
|
-
# Determine effective exclusion patterns
|
|
112
|
-
patterns_to_exclude = (
|
|
113
|
-
excluded_patterns
|
|
114
|
-
if excluded_patterns is not None
|
|
115
|
-
else DEFAULT_EXCLUDED_PATTERNS
|
|
116
|
-
)
|
|
117
|
-
if depth <= 0:
|
|
118
|
-
depth = 1
|
|
119
|
-
try:
|
|
120
|
-
initial_depth = abs_path.rstrip(os.sep).count(os.sep)
|
|
121
|
-
for root, dirs, files in os.walk(abs_path, topdown=True):
|
|
122
|
-
current_depth = root.rstrip(os.sep).count(os.sep) - initial_depth
|
|
123
|
-
if current_depth >= depth - 1:
|
|
124
|
-
del dirs[:]
|
|
125
|
-
dirs[:] = [
|
|
126
|
-
d
|
|
127
|
-
for d in dirs
|
|
128
|
-
if (include_hidden or not _is_hidden(d))
|
|
129
|
-
and not is_excluded(d, patterns_to_exclude)
|
|
130
|
-
]
|
|
131
|
-
for filename in files:
|
|
132
|
-
if (include_hidden or not _is_hidden(filename)) and not is_excluded(
|
|
133
|
-
filename, patterns_to_exclude
|
|
134
|
-
):
|
|
135
|
-
full_path = os.path.join(root, filename)
|
|
136
|
-
rel_full_path = os.path.relpath(full_path, abs_path)
|
|
137
|
-
if not is_excluded(rel_full_path, patterns_to_exclude):
|
|
138
|
-
all_files.append(rel_full_path)
|
|
139
|
-
return {"files": sorted(all_files)}
|
|
140
|
-
|
|
141
|
-
except (OSError, IOError) as e:
|
|
142
|
-
raise OSError(f"Error listing files in {path}: {e}")
|
|
143
|
-
except Exception as e:
|
|
144
|
-
raise RuntimeError(f"Unexpected error listing files in {path}: {e}")
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _is_hidden(path: str) -> bool:
|
|
148
|
-
"""
|
|
149
|
-
Check if path is hidden (starts with '.') but ignore '.' and '..'.
|
|
150
|
-
Args:
|
|
151
|
-
path: File or directory path to check
|
|
152
|
-
Returns:
|
|
153
|
-
True if the path is hidden, False otherwise
|
|
154
|
-
"""
|
|
155
|
-
basename = os.path.basename(path)
|
|
156
|
-
# Ignore '.' and '..' as they are not typically considered hidden in listings
|
|
157
|
-
if basename == "." or basename == "..":
|
|
158
|
-
return False
|
|
159
|
-
return basename.startswith(".")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def is_excluded(name: str, patterns: list[str]) -> bool:
|
|
163
|
-
"""Check if a name/path matches any exclusion patterns."""
|
|
164
|
-
for pattern in patterns:
|
|
165
|
-
if fnmatch.fnmatch(name, pattern):
|
|
166
|
-
return True
|
|
167
|
-
# Split the path using the OS path separator.
|
|
168
|
-
parts = name.split(os.path.sep)
|
|
169
|
-
# Check each part of the path.
|
|
170
|
-
for part in parts:
|
|
171
|
-
if fnmatch.fnmatch(part, pattern):
|
|
172
|
-
return True
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def read_from_file(
|
|
177
|
-
file: FileToRead | list[FileToRead],
|
|
178
|
-
) -> dict[str, Any]:
|
|
179
|
-
"""
|
|
180
|
-
Reads content from one or more files, optionally specifying line ranges.
|
|
181
|
-
|
|
182
|
-
Examples:
|
|
183
|
-
```
|
|
184
|
-
# Read entire content of a single file
|
|
185
|
-
read_from_file(file={'path': 'path/to/file.txt'})
|
|
186
|
-
|
|
187
|
-
# Read specific lines from a file
|
|
188
|
-
# The content will be returned with line numbers in the format: "LINE_NUMBER | line content"
|
|
189
|
-
read_from_file(file={'path': 'path/to/large_file.log', 'start_line': 100, 'end_line': 150})
|
|
190
|
-
|
|
191
|
-
# Read multiple files
|
|
192
|
-
read_from_file(file=[
|
|
193
|
-
{'path': 'path/to/file1.txt'},
|
|
194
|
-
{'path': 'path/to/file2.txt', 'start_line': 1, 'end_line': 5}
|
|
195
|
-
])
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
file (FileToRead | list[FileToRead]): A single file configuration or a list of them.
|
|
200
|
-
|
|
201
|
-
Returns:
|
|
202
|
-
dict: Content and metadata for a single file, or a dict of results for multiple files.
|
|
203
|
-
The `content` field in the returned dictionary will have line numbers in the format: "LINE_NUMBER | line content"
|
|
204
|
-
"""
|
|
205
|
-
is_list = isinstance(file, list)
|
|
206
|
-
files = file if is_list else [file]
|
|
207
|
-
|
|
208
|
-
results = {}
|
|
209
|
-
for file_config in files:
|
|
210
|
-
path = file_config["path"]
|
|
211
|
-
start_line = file_config.get("start_line", None)
|
|
212
|
-
end_line = file_config.get("end_line", None)
|
|
213
|
-
try:
|
|
214
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
215
|
-
if not os.path.exists(abs_path):
|
|
216
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
217
|
-
|
|
218
|
-
content = read_file_with_line_numbers(abs_path)
|
|
219
|
-
lines = content.splitlines()
|
|
220
|
-
total_lines = len(lines)
|
|
221
|
-
|
|
222
|
-
start_idx = (start_line - 1) if start_line is not None else 0
|
|
223
|
-
end_idx = end_line if end_line is not None else total_lines
|
|
224
|
-
|
|
225
|
-
if start_idx < 0:
|
|
226
|
-
start_idx = 0
|
|
227
|
-
if end_idx > total_lines:
|
|
228
|
-
end_idx = total_lines
|
|
229
|
-
if start_idx > end_idx:
|
|
230
|
-
start_idx = end_idx
|
|
231
|
-
|
|
232
|
-
selected_lines = lines[start_idx:end_idx]
|
|
233
|
-
content_result = "\n".join(selected_lines)
|
|
234
|
-
|
|
235
|
-
results[path] = {
|
|
236
|
-
"path": path,
|
|
237
|
-
"content": content_result,
|
|
238
|
-
"start_line": start_idx + 1,
|
|
239
|
-
"end_line": end_idx,
|
|
240
|
-
"total_lines": total_lines,
|
|
241
|
-
}
|
|
242
|
-
except Exception as e:
|
|
243
|
-
if not is_list:
|
|
244
|
-
if isinstance(e, (OSError, IOError)):
|
|
245
|
-
raise OSError(f"Error reading file {path}: {e}") from e
|
|
246
|
-
raise RuntimeError(f"Unexpected error reading file {path}: {e}") from e
|
|
247
|
-
results[path] = f"Error reading file: {e}"
|
|
248
|
-
|
|
249
|
-
if is_list:
|
|
250
|
-
return results
|
|
251
|
-
|
|
252
|
-
return results[files[0]["path"]]
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def write_to_file(
|
|
256
|
-
file: FileToWrite | list[FileToWrite],
|
|
257
|
-
) -> str | dict[str, Any]:
|
|
258
|
-
"""
|
|
259
|
-
Writes content to one or more files, with options for overwrite, append, or exclusive
|
|
260
|
-
creation.
|
|
261
|
-
|
|
262
|
-
**CRITICAL - PREVENT JSON ERRORS:**
|
|
263
|
-
1. **ESCAPING:** Do NOT double-escape quotes.
|
|
264
|
-
- CORRECT: "content": "He said \"Hello\""
|
|
265
|
-
- WRONG: "content": "He said \\"Hello\\"" <-- This breaks JSON parsing!
|
|
266
|
-
2. **SIZE LIMIT:** Content MUST NOT exceed 4000 characters.
|
|
267
|
-
- Exceeding this causes truncation and EOF errors.
|
|
268
|
-
- Split larger content into multiple sequential calls (first 'w', then 'a').
|
|
269
|
-
|
|
270
|
-
Examples:
|
|
271
|
-
```
|
|
272
|
-
# Overwrite 'file.txt' with initial content
|
|
273
|
-
write_to_file(file={'path': 'path/to/file.txt', 'content': 'Initial content.'})
|
|
274
|
-
|
|
275
|
-
# Append a second chunk to 'file.txt' (note the newline at the beginning of the content)
|
|
276
|
-
write_to_file(file={'path': 'path/to/file.txt', 'content': '\nSecond chunk.', 'mode': 'a'})
|
|
277
|
-
|
|
278
|
-
# Write to multiple files
|
|
279
|
-
write_to_file(file=[
|
|
280
|
-
{'path': 'path/to/file1.txt', 'content': 'Content for file 1'},
|
|
281
|
-
{'path': 'path/to/file2.txt', 'content': 'Content for file 2', 'mode': 'w'}
|
|
282
|
-
])
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Args:
|
|
286
|
-
file (FileToWrite | list[FileToWrite]): A single file configuration or a list of them.
|
|
287
|
-
|
|
288
|
-
Returns:
|
|
289
|
-
Success message for single file, or dict with success/errors for multiple files.
|
|
290
|
-
"""
|
|
291
|
-
# Normalize to list
|
|
292
|
-
files = file if isinstance(file, list) else [file]
|
|
293
|
-
|
|
294
|
-
success = []
|
|
295
|
-
errors = {}
|
|
296
|
-
for file_config in files:
|
|
297
|
-
path = file_config["path"]
|
|
298
|
-
content = file_config["content"]
|
|
299
|
-
mode = file_config.get("mode", "w")
|
|
300
|
-
try:
|
|
301
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
302
|
-
# The underlying utility creates the directory, so we don't need to do it here.
|
|
303
|
-
write_file(abs_path, content, mode=mode)
|
|
304
|
-
success.append(path)
|
|
305
|
-
except Exception as e:
|
|
306
|
-
errors[path] = f"Error writing file: {e}"
|
|
307
|
-
|
|
308
|
-
# Return appropriate response based on input type
|
|
309
|
-
if isinstance(file, list):
|
|
310
|
-
return {"success": success, "errors": errors}
|
|
311
|
-
else:
|
|
312
|
-
if errors:
|
|
313
|
-
raise RuntimeError(
|
|
314
|
-
f"Error writing file {file['path']}: {errors[file['path']]}"
|
|
315
|
-
)
|
|
316
|
-
return f"Successfully wrote to file: {file['path']} in mode '{file.get('mode', 'w')}'"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def search_files(
|
|
320
|
-
path: str,
|
|
321
|
-
regex: str,
|
|
322
|
-
file_pattern: Optional[str] = None,
|
|
323
|
-
include_hidden: bool = True,
|
|
324
|
-
) -> dict[str, Any]:
|
|
325
|
-
"""
|
|
326
|
-
Searches for a regex pattern in files within a directory.
|
|
327
|
-
|
|
328
|
-
Example:
|
|
329
|
-
search_files(path='src', regex='class \\w+', file_pattern='*.py', include_hidden=False)
|
|
330
|
-
|
|
331
|
-
Args:
|
|
332
|
-
path (str): Directory to search.
|
|
333
|
-
regex (str): Regex pattern.
|
|
334
|
-
file_pattern (str): Glob pattern filter.
|
|
335
|
-
include_hidden (bool): Include hidden files.
|
|
336
|
-
|
|
337
|
-
Returns:
|
|
338
|
-
dict: Summary and list of matches.
|
|
339
|
-
"""
|
|
340
|
-
try:
|
|
341
|
-
pattern = re.compile(regex)
|
|
342
|
-
except re.error as e:
|
|
343
|
-
raise ValueError(f"Invalid regex pattern: {e}")
|
|
344
|
-
search_results = {"summary": "", "results": []}
|
|
345
|
-
match_count = 0
|
|
346
|
-
searched_file_count = 0
|
|
347
|
-
file_match_count = 0
|
|
348
|
-
try:
|
|
349
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
350
|
-
for root, dirs, files in os.walk(abs_path):
|
|
351
|
-
# Skip hidden directories
|
|
352
|
-
dirs[:] = [d for d in dirs if include_hidden or not _is_hidden(d)]
|
|
353
|
-
for filename in files:
|
|
354
|
-
# Skip hidden files
|
|
355
|
-
if not include_hidden and _is_hidden(filename):
|
|
356
|
-
continue
|
|
357
|
-
# Apply file pattern filter if provided
|
|
358
|
-
if file_pattern and not fnmatch.fnmatch(filename, file_pattern):
|
|
359
|
-
continue
|
|
360
|
-
file_path = os.path.join(root, filename)
|
|
361
|
-
rel_file_path = os.path.relpath(file_path, os.getcwd())
|
|
362
|
-
searched_file_count += 1
|
|
363
|
-
try:
|
|
364
|
-
matches = _get_file_matches(file_path, pattern)
|
|
365
|
-
if matches:
|
|
366
|
-
file_match_count += 1
|
|
367
|
-
match_count += len(matches)
|
|
368
|
-
search_results["results"].append(
|
|
369
|
-
{"file": rel_file_path, "matches": matches}
|
|
370
|
-
)
|
|
371
|
-
except IOError as e:
|
|
372
|
-
search_results["results"].append(
|
|
373
|
-
{"file": rel_file_path, "error": str(e)}
|
|
374
|
-
)
|
|
375
|
-
if match_count == 0:
|
|
376
|
-
search_results["summary"] = (
|
|
377
|
-
f"No matches found for pattern '{regex}' in path '{path}' "
|
|
378
|
-
f"(searched {searched_file_count} files)."
|
|
379
|
-
)
|
|
380
|
-
else:
|
|
381
|
-
search_results["summary"] = (
|
|
382
|
-
f"Found {match_count} matches in {file_match_count} files "
|
|
383
|
-
f"(searched {searched_file_count} files)."
|
|
384
|
-
)
|
|
385
|
-
return search_results
|
|
386
|
-
except (OSError, IOError) as e:
|
|
387
|
-
raise OSError(f"Error searching files in {path}: {e}")
|
|
388
|
-
except Exception as e:
|
|
389
|
-
raise RuntimeError(f"Unexpected error searching files in {path}: {e}")
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def _get_file_matches(
|
|
393
|
-
file_path: str,
|
|
394
|
-
pattern: re.Pattern,
|
|
395
|
-
context_lines: int = 2,
|
|
396
|
-
) -> list[dict[str, Any]]:
|
|
397
|
-
"""Search for regex matches in a file with context."""
|
|
398
|
-
try:
|
|
399
|
-
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
400
|
-
lines = f.readlines()
|
|
401
|
-
matches = []
|
|
402
|
-
for line_idx, line in enumerate(lines):
|
|
403
|
-
if pattern.search(line):
|
|
404
|
-
line_num = line_idx + 1
|
|
405
|
-
context_start = max(0, line_idx - context_lines)
|
|
406
|
-
context_end = min(len(lines), line_idx + context_lines + 1)
|
|
407
|
-
match_data = {
|
|
408
|
-
"line_number": line_num,
|
|
409
|
-
"line_content": line.rstrip(),
|
|
410
|
-
"context_before": [
|
|
411
|
-
lines[j].rstrip() for j in range(context_start, line_idx)
|
|
412
|
-
],
|
|
413
|
-
"context_after": [
|
|
414
|
-
lines[j].rstrip() for j in range(line_idx + 1, context_end)
|
|
415
|
-
],
|
|
416
|
-
}
|
|
417
|
-
matches.append(match_data)
|
|
418
|
-
return matches
|
|
419
|
-
except (OSError, IOError) as e:
|
|
420
|
-
raise IOError(f"Error reading {file_path}: {e}")
|
|
421
|
-
except Exception as e:
|
|
422
|
-
raise RuntimeError(f"Unexpected error processing {file_path}: {e}")
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def replace_in_file(
|
|
426
|
-
file: FileReplacement | list[FileReplacement],
|
|
427
|
-
) -> str | dict[str, Any]:
|
|
428
|
-
"""
|
|
429
|
-
Replaces exact text in files.
|
|
430
|
-
|
|
431
|
-
**CRITICAL INSTRUCTIONS:**
|
|
432
|
-
1. **READ FIRST:** Use `read_file` to get exact content. Do not guess.
|
|
433
|
-
2. **EXACT MATCH:** `old_text` must match file content EXACTLY (whitespace, newlines).
|
|
434
|
-
3. **ESCAPING:** Do NOT double-escape quotes in `new_text`. Use `\"`, not `\\"`.
|
|
435
|
-
4. **SIZE LIMIT:** `new_text` MUST NOT exceed 4000 chars to avoid truncation/EOF errors.
|
|
436
|
-
5. **MINIMAL CONTEXT:** Keep `old_text` small (target lines + 2-3 context lines).
|
|
437
|
-
6. **DEFAULT:** Replaces **ALL** occurrences. Set `count=1` for first occurrence only.
|
|
438
|
-
|
|
439
|
-
Examples:
|
|
440
|
-
```
|
|
441
|
-
# Replace ALL occurrences
|
|
442
|
-
replace_in_file(file=[
|
|
443
|
-
{'path': 'file.txt', 'old_text': 'foo', 'new_text': 'bar'},
|
|
444
|
-
{'path': 'file.txt', 'old_text': 'baz', 'new_text': 'qux'}
|
|
445
|
-
])
|
|
446
|
-
|
|
447
|
-
# Replace ONLY the first occurrence
|
|
448
|
-
replace_in_file(
|
|
449
|
-
file={'path': 'file.txt', 'old_text': 'foo', 'new_text': 'bar', 'count': 1}
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
# Replace code block (include context for safety)
|
|
453
|
-
replace_in_file(
|
|
454
|
-
file={
|
|
455
|
-
'path': 'app.py',
|
|
456
|
-
'old_text': ' def old_fn():\n pass',
|
|
457
|
-
'new_text': ' def new_fn():\n pass'
|
|
458
|
-
}
|
|
459
|
-
)
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
Args:
|
|
463
|
-
file: Single replacement config or list of them.
|
|
464
|
-
|
|
465
|
-
Returns:
|
|
466
|
-
Success message or error dict.
|
|
467
|
-
"""
|
|
468
|
-
# Normalize to list
|
|
469
|
-
file_replacements = file if isinstance(file, list) else [file]
|
|
470
|
-
# Group replacements by file path to minimize file I/O
|
|
471
|
-
replacements_by_path: dict[str, list[FileReplacement]] = {}
|
|
472
|
-
for r in file_replacements:
|
|
473
|
-
path = r["path"]
|
|
474
|
-
if path not in replacements_by_path:
|
|
475
|
-
replacements_by_path[path] = []
|
|
476
|
-
replacements_by_path[path].append(r)
|
|
477
|
-
success = []
|
|
478
|
-
errors = {}
|
|
479
|
-
for path, replacements in replacements_by_path.items():
|
|
480
|
-
try:
|
|
481
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
482
|
-
if not os.path.exists(abs_path):
|
|
483
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
484
|
-
content = read_file(abs_path)
|
|
485
|
-
original_content = content
|
|
486
|
-
# Apply all replacements for this file
|
|
487
|
-
for replacement in replacements:
|
|
488
|
-
old_text = replacement["old_text"]
|
|
489
|
-
new_text = replacement["new_text"]
|
|
490
|
-
count = replacement.get("count", -1)
|
|
491
|
-
if old_text not in content:
|
|
492
|
-
raise ValueError(f"old_text not found in file: {path}")
|
|
493
|
-
# Replace occurrences
|
|
494
|
-
content = content.replace(old_text, new_text, count)
|
|
495
|
-
# Only write if content actually changed
|
|
496
|
-
if content != original_content:
|
|
497
|
-
write_file(abs_path, content)
|
|
498
|
-
success.append(path)
|
|
499
|
-
else:
|
|
500
|
-
success.append(f"{path} (no changes needed)")
|
|
501
|
-
except Exception as e:
|
|
502
|
-
errors[path] = f"Error applying replacement to {path}: {e}"
|
|
503
|
-
# Return appropriate response based on input type
|
|
504
|
-
if isinstance(file, list):
|
|
505
|
-
return {"success": success, "errors": errors}
|
|
506
|
-
path = file["path"]
|
|
507
|
-
if errors:
|
|
508
|
-
error_message = errors[path]
|
|
509
|
-
raise RuntimeError(f"Error applying replacement to {path}: {error_message}")
|
|
510
|
-
return f"Successfully applied replacement(s) to {path}"
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
async def analyze_file(
|
|
514
|
-
ctx: AnyContext, path: str, query: str, token_threshold: int | None = None
|
|
515
|
-
) -> dict[str, Any]:
|
|
516
|
-
"""
|
|
517
|
-
Analyzes a file using a sub-agent for complex questions.
|
|
518
|
-
|
|
519
|
-
Example:
|
|
520
|
-
analyze_file(path='src/main.py', query='Summarize the main function.')
|
|
521
|
-
|
|
522
|
-
Args:
|
|
523
|
-
ctx (AnyContext): The execution context.
|
|
524
|
-
path (str): The path to the file to analyze.
|
|
525
|
-
query (str): A specific analysis query with clear guidelines and
|
|
526
|
-
necessary information.
|
|
527
|
-
token_threshold (int | None): Max tokens.
|
|
528
|
-
|
|
529
|
-
Returns:
|
|
530
|
-
Analysis results.
|
|
531
|
-
"""
|
|
532
|
-
if token_threshold is None:
|
|
533
|
-
token_threshold = CFG.LLM_FILE_ANALYSIS_TOKEN_THRESHOLD
|
|
534
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
535
|
-
if not os.path.exists(abs_path):
|
|
536
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
537
|
-
file_content = read_file(abs_path)
|
|
538
|
-
_analyze_file = create_sub_agent_tool(
|
|
539
|
-
tool_name="analyze_file",
|
|
540
|
-
tool_description=(
|
|
541
|
-
"Analyze file content using LLM sub-agent "
|
|
542
|
-
"for complex questions about code structure, documentation "
|
|
543
|
-
"quality, or file-specific analysis. Use for questions that "
|
|
544
|
-
"require understanding beyond simple text reading."
|
|
545
|
-
),
|
|
546
|
-
system_prompt=CFG.LLM_FILE_EXTRACTOR_SYSTEM_PROMPT,
|
|
547
|
-
tools=[read_from_file, search_files],
|
|
548
|
-
auto_summarize=False,
|
|
549
|
-
remember_history=False,
|
|
550
|
-
)
|
|
551
|
-
payload = json.dumps(
|
|
552
|
-
{
|
|
553
|
-
"instruction": query,
|
|
554
|
-
"file_path": abs_path,
|
|
555
|
-
"file_content": llm_rate_limitter.clip_prompt(
|
|
556
|
-
file_content, token_threshold
|
|
557
|
-
),
|
|
558
|
-
}
|
|
559
|
-
)
|
|
560
|
-
return await _analyze_file(ctx, payload)
|
zrb/builtin/llm/tool/note.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
from zrb.config.llm_context.config import llm_context_config
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def read_long_term_note() -> str:
|
|
7
|
-
"""
|
|
8
|
-
Retrieves the GLOBAL long-term memory shared across ALL sessions and projects.
|
|
9
|
-
|
|
10
|
-
CRITICAL: Consult this first for user preferences, facts, and cross-project context.
|
|
11
|
-
|
|
12
|
-
Returns:
|
|
13
|
-
str: The current global note content.
|
|
14
|
-
"""
|
|
15
|
-
contexts = llm_context_config.get_notes()
|
|
16
|
-
return contexts.get("/", "")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def write_long_term_note(content: str) -> str:
|
|
20
|
-
"""
|
|
21
|
-
Persists CRITICAL facts to the GLOBAL long-term memory.
|
|
22
|
-
|
|
23
|
-
USE EAGERLY to save or update:
|
|
24
|
-
- User preferences (e.g., "I prefer Python", "No unit tests").
|
|
25
|
-
- User information (e.g., user name, user email address).
|
|
26
|
-
- Important facts (e.g., "My API key is in .env").
|
|
27
|
-
- Cross-project goals.
|
|
28
|
-
- Anything that will be useful for future interaction across projects.
|
|
29
|
-
|
|
30
|
-
WARNING: This OVERWRITES the entire global note.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
content (str): The text to strictly memorize.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
str: Confirmation message.
|
|
37
|
-
"""
|
|
38
|
-
llm_context_config.write_note(content, "/")
|
|
39
|
-
return "Global long-term note saved."
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def read_contextual_note(path: str | None = None) -> str:
|
|
43
|
-
"""
|
|
44
|
-
Retrieves LOCAL memory specific to a file or directory path.
|
|
45
|
-
|
|
46
|
-
Use to recall project-specific architecture, code summaries, or past decisions
|
|
47
|
-
relevant to the current working location.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
path (str | None): Target file/dir. Defaults to current working directory (CWD).
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
str: The local note content for the path.
|
|
54
|
-
"""
|
|
55
|
-
if path is None:
|
|
56
|
-
path = os.getcwd()
|
|
57
|
-
abs_path = os.path.abspath(path)
|
|
58
|
-
contexts = llm_context_config.get_notes(cwd=abs_path)
|
|
59
|
-
return contexts.get(abs_path, "")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def write_contextual_note(content: str, path: str | None = None) -> str:
|
|
63
|
-
"""
|
|
64
|
-
Persists LOCAL facts specific to a file or directory.
|
|
65
|
-
|
|
66
|
-
USE EAGERLY to save or update:
|
|
67
|
-
- Architectural patterns for this project/directory.
|
|
68
|
-
- Summaries of large files or directories.
|
|
69
|
-
- Specific guidelines for this project.
|
|
70
|
-
- Anything related to this directory that will be useful for future interaction.
|
|
71
|
-
|
|
72
|
-
WARNING: This OVERWRITES the note for the specific path.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
content (str): The text to memorize for this location.
|
|
76
|
-
path (str | None): Target file/dir. Defaults to CWD.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
str: Confirmation message.
|
|
80
|
-
"""
|
|
81
|
-
if path is None:
|
|
82
|
-
path = os.getcwd()
|
|
83
|
-
llm_context_config.write_note(content, path)
|
|
84
|
-
return f"Contextual note saved for: {path}"
|