zrb 1.15.3__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 -133
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +55 -1
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/chat.py +147 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
- zrb/builtin/searxng/config/settings.yml +5671 -0
- zrb/builtin/searxng/start.py +21 -0
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +555 -169
- zrb/config/helper.py +84 -0
- zrb/config/web_auth_config.py +50 -35
- zrb/context/any_shared_context.py +20 -3
- zrb/context/context.py +39 -5
- zrb/context/print_fn.py +13 -0
- zrb/context/shared_context.py +17 -8
- zrb/group/any_group.py +3 -3
- zrb/group/group.py +3 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +41 -1
- zrb/input/text_input.py +7 -24
- 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/file_extractor.md +112 -0
- zrb/llm/prompt/markdown/mandate.md +23 -0
- zrb/llm/prompt/markdown/persona.md +3 -0
- zrb/llm/prompt/markdown/repo_extractor.md +112 -0
- zrb/llm/prompt/markdown/repo_summarizer.md +29 -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 +33 -37
- 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/runner/cli.py +21 -20
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_route/task_input_api_route.py +5 -5
- zrb/runner/web_util/user.py +7 -3
- zrb/session/any_session.py +12 -9
- zrb/session/session.py +38 -17
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +42 -22
- zrb/task/base/execution.py +67 -55
- zrb/task/base/lifecycle.py +14 -7
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +113 -50
- zrb/task/base_trigger.py +16 -6
- zrb/task/cmd_task.py +6 -0
- zrb/task/http_check.py +11 -5
- zrb/task/make_task.py +5 -3
- zrb/task/rsync_task.py +30 -10
- zrb/task/scaffolder.py +7 -4
- zrb/task/scheduler.py +7 -4
- 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/attr.py +54 -39
- zrb/util/cli/markdown.py +32 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/file.py +61 -33
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -3
- zrb/util/match.py +78 -0
- zrb/util/run.py +3 -3
- zrb/util/string/conversion.py +1 -1
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
- zrb/attr/__init__.py +0 -0
- zrb/builtin/llm/chat_session.py +0 -311
- zrb/builtin/llm/history.py +0 -71
- zrb/builtin/llm/input.py +0 -27
- zrb/builtin/llm/llm_ask.py +0 -187
- zrb/builtin/llm/previous-session.js +0 -21
- zrb/builtin/llm/tool/__init__.py +0 -0
- zrb/builtin/llm/tool/api.py +0 -71
- zrb/builtin/llm/tool/cli.py +0 -38
- zrb/builtin/llm/tool/code.py +0 -254
- zrb/builtin/llm/tool/file.py +0 -626
- zrb/builtin/llm/tool/sub_agent.py +0 -137
- zrb/builtin/llm/tool/web.py +0 -195
- 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/file_extractor_system_prompt.md +0 -12
- zrb/config/default_prompt/interactive_system_prompt.md +0 -35
- zrb/config/default_prompt/persona.md +0 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
- zrb/config/default_prompt/summarization_prompt.md +0 -16
- zrb/config/default_prompt/system_prompt.md +0 -32
- zrb/config/llm_config.py +0 -243
- zrb/config/llm_context/config.py +0 -129
- zrb/config/llm_context/config_parser.py +0 -46
- zrb/config/llm_rate_limitter.py +0 -137
- 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 -243
- zrb/task/llm/config.py +0 -103
- zrb/task/llm/conversation_history.py +0 -128
- zrb/task/llm/conversation_history_model.py +0 -242
- zrb/task/llm/default_workflow/coding.md +0 -24
- zrb/task/llm/default_workflow/copywriting.md +0 -17
- zrb/task/llm/default_workflow/researching.md +0 -18
- zrb/task/llm/error.py +0 -95
- zrb/task/llm/history_summarization.py +0 -216
- zrb/task/llm/print_node.py +0 -101
- zrb/task/llm/prompt.py +0 -325
- zrb/task/llm/tool_wrapper.py +0 -220
- zrb/task/llm/typing.py +0 -3
- zrb/task/llm_task.py +0 -341
- 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-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/llm/tool/file.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
DEFAULT_EXCLUDED_PATTERNS = [
|
|
7
|
+
"__pycache__",
|
|
8
|
+
"*.pyc",
|
|
9
|
+
"*.pyo",
|
|
10
|
+
"*.pyd",
|
|
11
|
+
".Python",
|
|
12
|
+
"build",
|
|
13
|
+
"dist",
|
|
14
|
+
".env",
|
|
15
|
+
".venv",
|
|
16
|
+
"env",
|
|
17
|
+
"venv",
|
|
18
|
+
".idea",
|
|
19
|
+
".vscode",
|
|
20
|
+
".git",
|
|
21
|
+
"node_modules",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
".coverage",
|
|
24
|
+
"htmlcov",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_files(
|
|
29
|
+
path: str = ".",
|
|
30
|
+
include_hidden: bool = False,
|
|
31
|
+
depth: int = 3,
|
|
32
|
+
excluded_patterns: list[str] | None = None,
|
|
33
|
+
) -> dict[str, list[str]]:
|
|
34
|
+
"""
|
|
35
|
+
Recursively explores and lists files within a directory tree up to a defined depth.
|
|
36
|
+
|
|
37
|
+
**WHEN TO USE:**
|
|
38
|
+
- To discover the project structure or find specific files when the path is unknown.
|
|
39
|
+
- To verify the existence of files in a directory.
|
|
40
|
+
|
|
41
|
+
**EFFICIENCY TIP:**
|
|
42
|
+
- Do NOT use this tool if you already know the file path. Use `read_file` directly.
|
|
43
|
+
- Keep `depth` low (default 3) to avoid overwhelming output.
|
|
44
|
+
|
|
45
|
+
**ARGS:**
|
|
46
|
+
- `path`: The root directory to start the search from.
|
|
47
|
+
- `include_hidden`: If True, includes hidden files and directories (starting with `.`).
|
|
48
|
+
- `depth`: Maximum levels of directories to descend.
|
|
49
|
+
- `excluded_patterns`: List of glob patterns to ignore.
|
|
50
|
+
"""
|
|
51
|
+
all_files: list[str] = []
|
|
52
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
53
|
+
if not os.path.exists(abs_path):
|
|
54
|
+
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
55
|
+
|
|
56
|
+
patterns_to_exclude = (
|
|
57
|
+
excluded_patterns
|
|
58
|
+
if excluded_patterns is not None
|
|
59
|
+
else DEFAULT_EXCLUDED_PATTERNS
|
|
60
|
+
)
|
|
61
|
+
if depth <= 0:
|
|
62
|
+
depth = 1
|
|
63
|
+
|
|
64
|
+
initial_depth = abs_path.rstrip(os.sep).count(os.sep)
|
|
65
|
+
for root, dirs, files in os.walk(abs_path, topdown=True):
|
|
66
|
+
current_depth = root.rstrip(os.sep).count(os.sep) - initial_depth
|
|
67
|
+
if current_depth >= depth - 1:
|
|
68
|
+
del dirs[:]
|
|
69
|
+
|
|
70
|
+
dirs[:] = [
|
|
71
|
+
d
|
|
72
|
+
for d in dirs
|
|
73
|
+
if (include_hidden or not d.startswith("."))
|
|
74
|
+
and not _is_excluded(d, patterns_to_exclude)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
for filename in files:
|
|
78
|
+
if (include_hidden or not filename.startswith(".")) and not _is_excluded(
|
|
79
|
+
filename, patterns_to_exclude
|
|
80
|
+
):
|
|
81
|
+
full_path = os.path.join(root, filename)
|
|
82
|
+
rel_full_path = os.path.relpath(full_path, abs_path)
|
|
83
|
+
if not _is_excluded(rel_full_path, patterns_to_exclude):
|
|
84
|
+
all_files.append(rel_full_path)
|
|
85
|
+
return {"files": sorted(all_files)}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_file(
|
|
89
|
+
path: str, start_line: int | None = None, end_line: int | None = None
|
|
90
|
+
) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Reads content from a file, optionally specifying a line range.
|
|
93
|
+
|
|
94
|
+
**EFFICIENCY TIP:**
|
|
95
|
+
- Prefer reading the **entire file** at once for full context (imports, class definitions).
|
|
96
|
+
- Only use `start_line` and `end_line` for extremely large files (e.g., logs).
|
|
97
|
+
|
|
98
|
+
**ARGS:**
|
|
99
|
+
- `path`: Path to the file to read.
|
|
100
|
+
- `start_line`: The 1-based line number to start reading from.
|
|
101
|
+
- `end_line`: The 1-based line number to stop reading at (inclusive).
|
|
102
|
+
"""
|
|
103
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
104
|
+
if not os.path.exists(abs_path):
|
|
105
|
+
return f"Error: File not found: {path}"
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
109
|
+
lines = f.readlines()
|
|
110
|
+
|
|
111
|
+
total_lines = len(lines)
|
|
112
|
+
start_idx = (start_line - 1) if start_line is not None else 0
|
|
113
|
+
end_idx = end_line if end_line is not None else total_lines
|
|
114
|
+
|
|
115
|
+
if start_idx < 0:
|
|
116
|
+
start_idx = 0
|
|
117
|
+
if end_idx > total_lines:
|
|
118
|
+
end_idx = total_lines
|
|
119
|
+
if start_idx > end_idx:
|
|
120
|
+
start_idx = end_idx
|
|
121
|
+
|
|
122
|
+
selected_lines = lines[start_idx:end_idx]
|
|
123
|
+
content_result = "".join(selected_lines)
|
|
124
|
+
|
|
125
|
+
if start_line is not None or end_line is not None:
|
|
126
|
+
return f"File: {path} (Lines {start_idx + 1}-{end_idx} of {total_lines})\n{content_result}"
|
|
127
|
+
return content_result
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return f"Error reading file {path}: {e}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_files(paths: list[str]) -> dict[str, str]:
|
|
134
|
+
"""
|
|
135
|
+
Reads content from multiple files simultaneously.
|
|
136
|
+
|
|
137
|
+
**USAGE:**
|
|
138
|
+
- Use this when you need context from several related files (e.g., a class definition and its tests).
|
|
139
|
+
|
|
140
|
+
**ARGS:**
|
|
141
|
+
- `paths`: List of file paths to read.
|
|
142
|
+
"""
|
|
143
|
+
results = {}
|
|
144
|
+
for path in paths:
|
|
145
|
+
results[path] = read_file(path)
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def write_file(path: str, content: str, mode: str = "w") -> str:
|
|
150
|
+
"""
|
|
151
|
+
Writes or appends content to a file.
|
|
152
|
+
|
|
153
|
+
**CRITICAL - PREVENT ERRORS:**
|
|
154
|
+
1. **ESCAPING:** Do NOT double-escape quotes in your JSON tool call.
|
|
155
|
+
2. **SIZE LIMIT:** DO NOT write more than 4000 characters in a single call.
|
|
156
|
+
3. **CHUNKING:** For large files, use `mode="w"` for the first chunk and `mode="a"` for the rest.
|
|
157
|
+
|
|
158
|
+
**ARGS:**
|
|
159
|
+
- `path`: Target file path.
|
|
160
|
+
- `content`: Text content to write.
|
|
161
|
+
- `mode`: File opening mode ("w" to overwrite, "a" to append).
|
|
162
|
+
"""
|
|
163
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
164
|
+
try:
|
|
165
|
+
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
166
|
+
with open(abs_path, mode, encoding="utf-8") as f:
|
|
167
|
+
f.write(content)
|
|
168
|
+
return f"Successfully wrote to {path}"
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return f"Error writing to file {path}: {e}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def write_files(files: list[dict[str, str]]) -> dict[str, str]:
|
|
174
|
+
"""
|
|
175
|
+
Performs batch write operations to multiple files.
|
|
176
|
+
|
|
177
|
+
**ARGS:**
|
|
178
|
+
- `files`: A list of dictionaries, each containing:
|
|
179
|
+
- `path` (str): Target file path.
|
|
180
|
+
- `content` (str): Text to write.
|
|
181
|
+
- `mode` (str, optional): "w" (overwrite, default) or "a" (append).
|
|
182
|
+
"""
|
|
183
|
+
results = {}
|
|
184
|
+
for file_info in files:
|
|
185
|
+
path = file_info.get("path")
|
|
186
|
+
content = file_info.get("content")
|
|
187
|
+
mode = file_info.get("mode", "w")
|
|
188
|
+
if not path or content is None:
|
|
189
|
+
results[str(path)] = "Error: Missing path or content"
|
|
190
|
+
continue
|
|
191
|
+
results[path] = write_file(path, content, mode)
|
|
192
|
+
return results
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def replace_in_file(path: str, old_text: str, new_text: str, count: int = -1) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Replaces exact text sequences within a file.
|
|
198
|
+
|
|
199
|
+
**CRITICAL INSTRUCTIONS:**
|
|
200
|
+
1. **PRECISION:** `old_text` must match the file content EXACTLY.
|
|
201
|
+
2. **READ FIRST:** Always `read_file` before replacing.
|
|
202
|
+
3. **MINIMAL CONTEXT:** Include 2-3 lines of context in `old_text` to ensure uniqueness.
|
|
203
|
+
|
|
204
|
+
**ARGS:**
|
|
205
|
+
- `path`: Path to the file to modify.
|
|
206
|
+
- `old_text`: The exact literal text to be replaced.
|
|
207
|
+
- `new_text`: The replacement text.
|
|
208
|
+
- `count`: Number of occurrences to replace (default -1 for all).
|
|
209
|
+
"""
|
|
210
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
211
|
+
if not os.path.exists(abs_path):
|
|
212
|
+
return f"Error: File not found: {path}"
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
216
|
+
content = f.read()
|
|
217
|
+
|
|
218
|
+
if old_text not in content:
|
|
219
|
+
return f"Error: '{old_text}' not found in {path}"
|
|
220
|
+
|
|
221
|
+
new_content = content.replace(old_text, new_text, count)
|
|
222
|
+
|
|
223
|
+
if content == new_content:
|
|
224
|
+
return f"No changes made to {path}"
|
|
225
|
+
|
|
226
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
227
|
+
f.write(new_content)
|
|
228
|
+
return f"Successfully updated {path}"
|
|
229
|
+
except Exception as e:
|
|
230
|
+
return f"Error replacing text in {path}: {e}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def search_files(
|
|
234
|
+
path: str,
|
|
235
|
+
regex: str,
|
|
236
|
+
file_pattern: str | None = None,
|
|
237
|
+
include_hidden: bool = True,
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
"""
|
|
240
|
+
Searches for a regular expression pattern within files.
|
|
241
|
+
|
|
242
|
+
**WHEN TO USE:**
|
|
243
|
+
- To find usages of a function, variable, or string across the project.
|
|
244
|
+
|
|
245
|
+
**ARGS:**
|
|
246
|
+
- `path`: Root directory to search.
|
|
247
|
+
- `regex`: A standard Python regular expression.
|
|
248
|
+
- `file_pattern`: Optional glob (e.g., "*.py") to restrict the search.
|
|
249
|
+
- `include_hidden`: Whether to search in hidden files/dirs.
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
pattern = re.compile(regex)
|
|
253
|
+
except re.error as e:
|
|
254
|
+
return {"error": f"Invalid regex pattern: {e}"}
|
|
255
|
+
|
|
256
|
+
search_results = {"summary": "", "results": []}
|
|
257
|
+
match_count = 0
|
|
258
|
+
searched_file_count = 0
|
|
259
|
+
file_match_count = 0
|
|
260
|
+
|
|
261
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
262
|
+
if not os.path.exists(abs_path):
|
|
263
|
+
return {"error": f"Path not found: {path}"}
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
for root, dirs, files in os.walk(abs_path):
|
|
267
|
+
# Skip hidden directories
|
|
268
|
+
dirs[:] = [d for d in dirs if include_hidden or not d.startswith(".")]
|
|
269
|
+
for filename in files:
|
|
270
|
+
# Skip hidden files
|
|
271
|
+
if not include_hidden and filename.startswith("."):
|
|
272
|
+
continue
|
|
273
|
+
# Apply file pattern filter if provided
|
|
274
|
+
if file_pattern and not fnmatch.fnmatch(filename, file_pattern):
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
file_path = os.path.join(root, filename)
|
|
278
|
+
rel_file_path = os.path.relpath(file_path, os.getcwd())
|
|
279
|
+
searched_file_count += 1
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
matches = _get_file_matches(file_path, pattern)
|
|
283
|
+
if matches:
|
|
284
|
+
file_match_count += 1
|
|
285
|
+
match_count += len(matches)
|
|
286
|
+
search_results["results"].append(
|
|
287
|
+
{"file": rel_file_path, "matches": matches}
|
|
288
|
+
)
|
|
289
|
+
except Exception:
|
|
290
|
+
# Ignore read errors for binary files etc
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
if match_count == 0:
|
|
294
|
+
search_results["summary"] = (
|
|
295
|
+
f"No matches found for pattern '{regex}' in path '{path}' "
|
|
296
|
+
f"(searched {searched_file_count} files)."
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
search_results["summary"] = (
|
|
300
|
+
f"Found {match_count} matches in {file_match_count} files "
|
|
301
|
+
f"(searched {searched_file_count} files)."
|
|
302
|
+
)
|
|
303
|
+
return search_results
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
return {"error": f"Error searching files: {e}"}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _get_file_matches(
|
|
310
|
+
file_path: str,
|
|
311
|
+
pattern: re.Pattern,
|
|
312
|
+
context_lines: int = 2,
|
|
313
|
+
) -> list[dict[str, any]]:
|
|
314
|
+
"""Search for regex matches in a file with context."""
|
|
315
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
316
|
+
lines = f.readlines()
|
|
317
|
+
matches = []
|
|
318
|
+
for line_idx, line in enumerate(lines):
|
|
319
|
+
if pattern.search(line):
|
|
320
|
+
line_num = line_idx + 1
|
|
321
|
+
context_start = max(0, line_idx - context_lines)
|
|
322
|
+
context_end = min(len(lines), line_idx + context_lines + 1)
|
|
323
|
+
match_data = {
|
|
324
|
+
"line_number": line_num,
|
|
325
|
+
"line_content": line.rstrip(),
|
|
326
|
+
"context_before": [
|
|
327
|
+
lines[j].rstrip() for j in range(context_start, line_idx)
|
|
328
|
+
],
|
|
329
|
+
"context_after": [
|
|
330
|
+
lines[j].rstrip() for j in range(line_idx + 1, context_end)
|
|
331
|
+
],
|
|
332
|
+
}
|
|
333
|
+
matches.append(match_data)
|
|
334
|
+
return matches
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _is_excluded(name: str, patterns: list[str]) -> bool:
|
|
338
|
+
for pattern in patterns:
|
|
339
|
+
if fnmatch.fnmatch(name, pattern):
|
|
340
|
+
return True
|
|
341
|
+
parts = name.split(os.path.sep)
|
|
342
|
+
for part in parts:
|
|
343
|
+
if fnmatch.fnmatch(part, pattern):
|
|
344
|
+
return True
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def analyze_file(path: str, query: str) -> str:
|
|
349
|
+
"""
|
|
350
|
+
Delegates deep analysis of a specific file to a specialized sub-agent.
|
|
351
|
+
|
|
352
|
+
**WHEN TO USE:**
|
|
353
|
+
- For complex questions about a file's logic, structure, or potential bugs.
|
|
354
|
+
- When you need a summary or specific details that require "understanding" the code.
|
|
355
|
+
|
|
356
|
+
**NOTE:** For simple data retrieval, use `read_file`.
|
|
357
|
+
|
|
358
|
+
**ARGS:**
|
|
359
|
+
- `path`: Path to the file to analyze.
|
|
360
|
+
- `query`: The specific analytical question or instruction.
|
|
361
|
+
"""
|
|
362
|
+
# Lazy imports to avoid circular dependencies
|
|
363
|
+
from zrb.config.config import CFG
|
|
364
|
+
from zrb.llm.agent.agent import create_agent, run_agent
|
|
365
|
+
from zrb.llm.config.config import llm_config
|
|
366
|
+
from zrb.llm.config.limiter import llm_limiter
|
|
367
|
+
from zrb.llm.prompt.default import get_file_extractor_system_prompt
|
|
368
|
+
|
|
369
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
370
|
+
if not os.path.exists(abs_path):
|
|
371
|
+
return f"Error: File not found: {path}"
|
|
372
|
+
|
|
373
|
+
# Read content
|
|
374
|
+
content = read_file(abs_path)
|
|
375
|
+
if content.startswith("Error:"):
|
|
376
|
+
return content
|
|
377
|
+
|
|
378
|
+
# Check token limit and truncate if necessary
|
|
379
|
+
token_threshold = CFG.LLM_FILE_ANALYSIS_TOKEN_THRESHOLD
|
|
380
|
+
# Simple character-based approximation (1 token ~ 4 chars)
|
|
381
|
+
char_limit = token_threshold * 4
|
|
382
|
+
|
|
383
|
+
clipped_content = content
|
|
384
|
+
if len(content) > char_limit:
|
|
385
|
+
clipped_content = content[:char_limit] + "\n...[TRUNCATED]..."
|
|
386
|
+
|
|
387
|
+
system_prompt = get_file_extractor_system_prompt()
|
|
388
|
+
|
|
389
|
+
# Create the sub-agent
|
|
390
|
+
agent = create_agent(
|
|
391
|
+
model=llm_config.model,
|
|
392
|
+
system_prompt=system_prompt,
|
|
393
|
+
tools=[
|
|
394
|
+
read_file,
|
|
395
|
+
search_files,
|
|
396
|
+
],
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Construct the user message
|
|
400
|
+
user_message = f"""
|
|
401
|
+
Instruction: {query}
|
|
402
|
+
File Path: {abs_path}
|
|
403
|
+
File Content:
|
|
404
|
+
```
|
|
405
|
+
{clipped_content}
|
|
406
|
+
```
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
# Run the agent
|
|
410
|
+
# We pass empty history as this is a fresh sub-task
|
|
411
|
+
# We use print as the print_fn (which streams to stdout)
|
|
412
|
+
result, _ = await run_agent(
|
|
413
|
+
agent=agent,
|
|
414
|
+
message=user_message,
|
|
415
|
+
message_history=[],
|
|
416
|
+
limiter=llm_limiter,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return str(result)
|
zrb/llm/tool/note.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Callable, List
|
|
3
|
+
|
|
4
|
+
from zrb.llm.note.manager import NoteManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_note_tools(note_manager: NoteManager) -> List[Callable]:
|
|
8
|
+
async def read_long_term_note() -> str:
|
|
9
|
+
"""
|
|
10
|
+
Retrieves your GLOBAL 🧠 Long-Term Memory.
|
|
11
|
+
This contains established preferences, personal facts, and context spanning multiple projects.
|
|
12
|
+
ALWAYS check this at the start of a session.
|
|
13
|
+
"""
|
|
14
|
+
return note_manager.read("~")
|
|
15
|
+
|
|
16
|
+
read_long_term_note.__name__ = "read_long_term_note"
|
|
17
|
+
|
|
18
|
+
async def write_long_term_note(content: str) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Updates your GLOBAL 🧠 Long-Term Memory with CRITICAL information.
|
|
21
|
+
Use this to persist user preferences, personal facts, and cross-project rules.
|
|
22
|
+
|
|
23
|
+
**WARNING:** This COMPLETELY OVERWRITES the existing Long-Term Note.
|
|
24
|
+
|
|
25
|
+
**ARGS:**
|
|
26
|
+
- `content`: The full text to store in the global memory.
|
|
27
|
+
"""
|
|
28
|
+
note_manager.write("~", content)
|
|
29
|
+
return "Global long-term note saved."
|
|
30
|
+
|
|
31
|
+
write_long_term_note.__name__ = "write_long_term_note"
|
|
32
|
+
|
|
33
|
+
async def read_contextual_note(path: str | None = None) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Retrieves LOCAL 📝 Contextual Notes for a specific project or directory.
|
|
36
|
+
Use this to recall architectural decisions or project-specific guidelines.
|
|
37
|
+
|
|
38
|
+
**ARGS:**
|
|
39
|
+
- `path`: Target file/dir path. Defaults to current working directory.
|
|
40
|
+
"""
|
|
41
|
+
if path is None:
|
|
42
|
+
path = os.getcwd()
|
|
43
|
+
return note_manager.read(path)
|
|
44
|
+
|
|
45
|
+
read_contextual_note.__name__ = "read_contextual_note"
|
|
46
|
+
|
|
47
|
+
async def write_contextual_note(content: str, path: str | None = None) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Persists LOCAL 📝 Contextual Notes for a specific project or directory.
|
|
50
|
+
Use this to save architectural patterns or progress markers for the current task.
|
|
51
|
+
|
|
52
|
+
**WARNING:** This COMPLETELY OVERWRITES the contextual note for the specified path.
|
|
53
|
+
|
|
54
|
+
**ARGS:**
|
|
55
|
+
- `content`: The full text to store in the local memory.
|
|
56
|
+
- `path`: Target file/dir path. Defaults to current working directory.
|
|
57
|
+
"""
|
|
58
|
+
if path is None:
|
|
59
|
+
path = os.getcwd()
|
|
60
|
+
note_manager.write(path, content)
|
|
61
|
+
return f"Contextual note saved for: {path}"
|
|
62
|
+
|
|
63
|
+
write_contextual_note.__name__ = "write_contextual_note"
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
read_long_term_note,
|
|
67
|
+
write_long_term_note,
|
|
68
|
+
read_contextual_note,
|
|
69
|
+
write_contextual_note,
|
|
70
|
+
]
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import sys
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
from textwrap import dedent
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
import ulid
|
|
10
11
|
|
|
@@ -44,49 +45,40 @@ def create_rag_from_directory(
|
|
|
44
45
|
openai_embedding_model: str | None = None,
|
|
45
46
|
):
|
|
46
47
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
1. Monitor a specified directory for file changes.
|
|
57
|
-
2. Automatically update a vector database (ChromaDB) with the latest
|
|
58
|
-
content.
|
|
59
|
-
3. Accept a user query, embed it, and perform a similarity search against
|
|
60
|
-
the document vectors.
|
|
61
|
-
4. Return the most relevant document chunks that match the query.
|
|
48
|
+
Create a powerful RAG (Retrieval-Augmented Generation) tool for querying a local
|
|
49
|
+
knowledge base.
|
|
50
|
+
|
|
51
|
+
This factory function generates a tool that performs semantic search over a directory of
|
|
52
|
+
documents. It automatically indexes the documents into a vector database (ChromaDB) and
|
|
53
|
+
keeps it updated as files change.
|
|
54
|
+
|
|
55
|
+
The generated tool is ideal for answering questions based on a specific set of documents,
|
|
56
|
+
such as project documentation or internal wikis.
|
|
62
57
|
|
|
63
58
|
Args:
|
|
64
|
-
tool_name (str): The name for the generated RAG tool (e.g.,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
tool_name (str): The name for the generated RAG tool (e.g., "search_project_docs").
|
|
60
|
+
tool_description (str): A clear description of what the tool does and when to use it.
|
|
61
|
+
This is what the LLM will see.
|
|
62
|
+
document_dir_path (str, optional): The path to the directory containing the documents
|
|
63
|
+
to be indexed.
|
|
64
|
+
vector_db_path (str, optional): The path where the ChromaDB vector database will be
|
|
65
|
+
stored.
|
|
66
|
+
vector_db_collection (str, optional): The name of the collection within the vector
|
|
72
67
|
database.
|
|
73
|
-
vector_db_collection (str, optional): The name of the collection within
|
|
74
|
-
the vector database.
|
|
75
68
|
chunk_size (int, optional): The size of text chunks for embedding.
|
|
76
69
|
overlap (int, optional): The overlap between text chunks.
|
|
77
|
-
max_result_count (int, optional): The maximum number of search results
|
|
78
|
-
|
|
79
|
-
file_reader (list[RAGFileReader], optional): Custom file readers for
|
|
70
|
+
max_result_count (int, optional): The maximum number of search results to return.
|
|
71
|
+
file_reader (list[RAGFileReader], optional): A list of custom file readers for
|
|
80
72
|
specific file types.
|
|
81
|
-
openai_api_key (str, optional): OpenAI API key for embeddings.
|
|
82
|
-
openai_base_url (str, optional):
|
|
73
|
+
openai_api_key (str, optional): Your OpenAI API key for generating embeddings.
|
|
74
|
+
openai_base_url (str, optional): An optional base URL for the OpenAI API.
|
|
83
75
|
openai_embedding_model (str, optional): The embedding model to use.
|
|
84
76
|
|
|
85
77
|
Returns:
|
|
86
|
-
|
|
78
|
+
An asynchronous function that serves as the RAG tool.
|
|
87
79
|
"""
|
|
88
80
|
|
|
89
|
-
async def retrieve(query: str) -> str:
|
|
81
|
+
async def retrieve(query: str) -> dict[str, Any]:
|
|
90
82
|
# Docstring will be set dynamically below
|
|
91
83
|
from chromadb import PersistentClient
|
|
92
84
|
from chromadb.config import Settings
|
|
@@ -201,16 +193,20 @@ def create_rag_from_directory(
|
|
|
201
193
|
query_embeddings=query_vector,
|
|
202
194
|
n_results=max_result_count_val,
|
|
203
195
|
)
|
|
204
|
-
return
|
|
196
|
+
return dict(results)
|
|
205
197
|
|
|
206
198
|
retrieve.__name__ = tool_name
|
|
207
199
|
retrieve.__doc__ = dedent(
|
|
208
200
|
f"""
|
|
209
201
|
{tool_description}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
202
|
+
This tool performs a semantic search across a curated knowledge base of documents.
|
|
203
|
+
It is highly effective for answering questions that require specific project knowledge not found in general training data.
|
|
204
|
+
|
|
205
|
+
**ARGS:**
|
|
206
|
+
- `query` (str): The semantic search query or question.
|
|
207
|
+
|
|
208
|
+
**RETURNS:**
|
|
209
|
+
- A dictionary containing matching document chunks ("documents") and their metadata.
|
|
214
210
|
"""
|
|
215
211
|
).strip()
|
|
216
212
|
return retrieve
|
|
@@ -0,0 +1,53 @@
|
|
|
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 a live internet search using Brave Search to retrieve up-to-date information, news, or documentation.
|
|
16
|
+
|
|
17
|
+
**WHEN TO USE:**
|
|
18
|
+
- To find the latest information on rapidly changing topics (e.g., library updates, current events).
|
|
19
|
+
- To search for documentation or examples not present in the local codebase.
|
|
20
|
+
- To verify facts or find external resources.
|
|
21
|
+
|
|
22
|
+
**ARGS:**
|
|
23
|
+
- `query`: The search string or question.
|
|
24
|
+
- `page`: Result page number (default 1).
|
|
25
|
+
"""
|
|
26
|
+
if safe_search is None:
|
|
27
|
+
safe_search = CFG.BRAVE_API_SAFE
|
|
28
|
+
if language is None:
|
|
29
|
+
language = CFG.BRAVE_API_LANG
|
|
30
|
+
|
|
31
|
+
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"
|
|
32
|
+
|
|
33
|
+
response = requests.get(
|
|
34
|
+
"https://api.search.brave.com/res/v1/web/search",
|
|
35
|
+
headers={
|
|
36
|
+
"User-Agent": user_agent,
|
|
37
|
+
"Accept": "application/json",
|
|
38
|
+
"x-subscription-token": CFG.BRAVE_API_KEY,
|
|
39
|
+
},
|
|
40
|
+
params={
|
|
41
|
+
"q": query,
|
|
42
|
+
"count": "10",
|
|
43
|
+
"offset": (page - 1) * 10,
|
|
44
|
+
"safesearch": safe_search,
|
|
45
|
+
"search_lang": language,
|
|
46
|
+
"summary": "true",
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
if response.status_code != 200:
|
|
50
|
+
raise Exception(
|
|
51
|
+
f"Error: Unable to retrieve search results (status code: {response.status_code})"
|
|
52
|
+
)
|
|
53
|
+
return response.json()
|