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.

Files changed (204) hide show
  1. zrb/__init__.py +118 -133
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +55 -1
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/llm/chat.py +147 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  9. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  10. zrb/builtin/searxng/config/settings.yml +5671 -0
  11. zrb/builtin/searxng/start.py +21 -0
  12. zrb/builtin/shell/autocomplete/bash.py +4 -3
  13. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  14. zrb/callback/callback.py +8 -1
  15. zrb/cmd/cmd_result.py +2 -1
  16. zrb/config/config.py +555 -169
  17. zrb/config/helper.py +84 -0
  18. zrb/config/web_auth_config.py +50 -35
  19. zrb/context/any_shared_context.py +20 -3
  20. zrb/context/context.py +39 -5
  21. zrb/context/print_fn.py +13 -0
  22. zrb/context/shared_context.py +17 -8
  23. zrb/group/any_group.py +3 -3
  24. zrb/group/group.py +3 -3
  25. zrb/input/any_input.py +5 -1
  26. zrb/input/base_input.py +18 -6
  27. zrb/input/option_input.py +41 -1
  28. zrb/input/text_input.py +7 -24
  29. zrb/llm/agent/__init__.py +9 -0
  30. zrb/llm/agent/agent.py +215 -0
  31. zrb/llm/agent/summarizer.py +20 -0
  32. zrb/llm/app/__init__.py +10 -0
  33. zrb/llm/app/completion.py +281 -0
  34. zrb/llm/app/confirmation/allow_tool.py +66 -0
  35. zrb/llm/app/confirmation/handler.py +178 -0
  36. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  37. zrb/llm/app/keybinding.py +34 -0
  38. zrb/llm/app/layout.py +117 -0
  39. zrb/llm/app/lexer.py +155 -0
  40. zrb/llm/app/redirection.py +28 -0
  41. zrb/llm/app/style.py +16 -0
  42. zrb/llm/app/ui.py +733 -0
  43. zrb/llm/config/__init__.py +4 -0
  44. zrb/llm/config/config.py +122 -0
  45. zrb/llm/config/limiter.py +247 -0
  46. zrb/llm/history_manager/__init__.py +4 -0
  47. zrb/llm/history_manager/any_history_manager.py +23 -0
  48. zrb/llm/history_manager/file_history_manager.py +91 -0
  49. zrb/llm/history_processor/summarizer.py +108 -0
  50. zrb/llm/note/__init__.py +3 -0
  51. zrb/llm/note/manager.py +122 -0
  52. zrb/llm/prompt/__init__.py +29 -0
  53. zrb/llm/prompt/claude_compatibility.py +92 -0
  54. zrb/llm/prompt/compose.py +55 -0
  55. zrb/llm/prompt/default.py +51 -0
  56. zrb/llm/prompt/markdown/file_extractor.md +112 -0
  57. zrb/llm/prompt/markdown/mandate.md +23 -0
  58. zrb/llm/prompt/markdown/persona.md +3 -0
  59. zrb/llm/prompt/markdown/repo_extractor.md +112 -0
  60. zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
  61. zrb/llm/prompt/markdown/summarizer.md +21 -0
  62. zrb/llm/prompt/note.py +41 -0
  63. zrb/llm/prompt/system_context.py +46 -0
  64. zrb/llm/prompt/zrb.py +41 -0
  65. zrb/llm/skill/__init__.py +3 -0
  66. zrb/llm/skill/manager.py +86 -0
  67. zrb/llm/task/__init__.py +4 -0
  68. zrb/llm/task/llm_chat_task.py +316 -0
  69. zrb/llm/task/llm_task.py +245 -0
  70. zrb/llm/tool/__init__.py +39 -0
  71. zrb/llm/tool/bash.py +75 -0
  72. zrb/llm/tool/code.py +266 -0
  73. zrb/llm/tool/file.py +419 -0
  74. zrb/llm/tool/note.py +70 -0
  75. zrb/{builtin/llm → llm}/tool/rag.py +33 -37
  76. zrb/llm/tool/search/brave.py +53 -0
  77. zrb/llm/tool/search/searxng.py +47 -0
  78. zrb/llm/tool/search/serpapi.py +47 -0
  79. zrb/llm/tool/skill.py +19 -0
  80. zrb/llm/tool/sub_agent.py +70 -0
  81. zrb/llm/tool/web.py +97 -0
  82. zrb/llm/tool/zrb_task.py +66 -0
  83. zrb/llm/util/attachment.py +101 -0
  84. zrb/llm/util/prompt.py +104 -0
  85. zrb/llm/util/stream_response.py +178 -0
  86. zrb/runner/cli.py +21 -20
  87. zrb/runner/common_util.py +24 -19
  88. zrb/runner/web_route/task_input_api_route.py +5 -5
  89. zrb/runner/web_util/user.py +7 -3
  90. zrb/session/any_session.py +12 -9
  91. zrb/session/session.py +38 -17
  92. zrb/task/any_task.py +24 -3
  93. zrb/task/base/context.py +42 -22
  94. zrb/task/base/execution.py +67 -55
  95. zrb/task/base/lifecycle.py +14 -7
  96. zrb/task/base/monitoring.py +12 -7
  97. zrb/task/base_task.py +113 -50
  98. zrb/task/base_trigger.py +16 -6
  99. zrb/task/cmd_task.py +6 -0
  100. zrb/task/http_check.py +11 -5
  101. zrb/task/make_task.py +5 -3
  102. zrb/task/rsync_task.py +30 -10
  103. zrb/task/scaffolder.py +7 -4
  104. zrb/task/scheduler.py +7 -4
  105. zrb/task/tcp_check.py +6 -4
  106. zrb/util/ascii_art/art/bee.txt +17 -0
  107. zrb/util/ascii_art/art/cat.txt +9 -0
  108. zrb/util/ascii_art/art/ghost.txt +16 -0
  109. zrb/util/ascii_art/art/panda.txt +17 -0
  110. zrb/util/ascii_art/art/rose.txt +14 -0
  111. zrb/util/ascii_art/art/unicorn.txt +15 -0
  112. zrb/util/ascii_art/banner.py +92 -0
  113. zrb/util/attr.py +54 -39
  114. zrb/util/cli/markdown.py +32 -0
  115. zrb/util/cli/text.py +30 -0
  116. zrb/util/cmd/command.py +33 -10
  117. zrb/util/file.py +61 -33
  118. zrb/util/git.py +2 -2
  119. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  120. zrb/util/match.py +78 -0
  121. zrb/util/run.py +3 -3
  122. zrb/util/string/conversion.py +1 -1
  123. zrb/util/truncate.py +23 -0
  124. zrb/util/yaml.py +204 -0
  125. zrb/xcom/xcom.py +10 -0
  126. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
  127. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
  128. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
  129. zrb/attr/__init__.py +0 -0
  130. zrb/builtin/llm/chat_session.py +0 -311
  131. zrb/builtin/llm/history.py +0 -71
  132. zrb/builtin/llm/input.py +0 -27
  133. zrb/builtin/llm/llm_ask.py +0 -187
  134. zrb/builtin/llm/previous-session.js +0 -21
  135. zrb/builtin/llm/tool/__init__.py +0 -0
  136. zrb/builtin/llm/tool/api.py +0 -71
  137. zrb/builtin/llm/tool/cli.py +0 -38
  138. zrb/builtin/llm/tool/code.py +0 -254
  139. zrb/builtin/llm/tool/file.py +0 -626
  140. zrb/builtin/llm/tool/sub_agent.py +0 -137
  141. zrb/builtin/llm/tool/web.py +0 -195
  142. zrb/builtin/project/__init__.py +0 -0
  143. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  144. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  145. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  146. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  147. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  148. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  149. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  150. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  151. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  152. zrb/builtin/project/create/__init__.py +0 -0
  153. zrb/builtin/shell/__init__.py +0 -0
  154. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  155. zrb/callback/__init__.py +0 -0
  156. zrb/cmd/__init__.py +0 -0
  157. zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
  158. zrb/config/default_prompt/interactive_system_prompt.md +0 -35
  159. zrb/config/default_prompt/persona.md +0 -1
  160. zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
  161. zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
  162. zrb/config/default_prompt/summarization_prompt.md +0 -16
  163. zrb/config/default_prompt/system_prompt.md +0 -32
  164. zrb/config/llm_config.py +0 -243
  165. zrb/config/llm_context/config.py +0 -129
  166. zrb/config/llm_context/config_parser.py +0 -46
  167. zrb/config/llm_rate_limitter.py +0 -137
  168. zrb/content_transformer/__init__.py +0 -0
  169. zrb/context/__init__.py +0 -0
  170. zrb/dot_dict/__init__.py +0 -0
  171. zrb/env/__init__.py +0 -0
  172. zrb/group/__init__.py +0 -0
  173. zrb/input/__init__.py +0 -0
  174. zrb/runner/__init__.py +0 -0
  175. zrb/runner/web_route/__init__.py +0 -0
  176. zrb/runner/web_route/home_page/__init__.py +0 -0
  177. zrb/session/__init__.py +0 -0
  178. zrb/session_state_log/__init__.py +0 -0
  179. zrb/session_state_logger/__init__.py +0 -0
  180. zrb/task/__init__.py +0 -0
  181. zrb/task/base/__init__.py +0 -0
  182. zrb/task/llm/__init__.py +0 -0
  183. zrb/task/llm/agent.py +0 -243
  184. zrb/task/llm/config.py +0 -103
  185. zrb/task/llm/conversation_history.py +0 -128
  186. zrb/task/llm/conversation_history_model.py +0 -242
  187. zrb/task/llm/default_workflow/coding.md +0 -24
  188. zrb/task/llm/default_workflow/copywriting.md +0 -17
  189. zrb/task/llm/default_workflow/researching.md +0 -18
  190. zrb/task/llm/error.py +0 -95
  191. zrb/task/llm/history_summarization.py +0 -216
  192. zrb/task/llm/print_node.py +0 -101
  193. zrb/task/llm/prompt.py +0 -325
  194. zrb/task/llm/tool_wrapper.py +0 -220
  195. zrb/task/llm/typing.py +0 -3
  196. zrb/task/llm_task.py +0 -341
  197. zrb/task_status/__init__.py +0 -0
  198. zrb/util/__init__.py +0 -0
  199. zrb/util/cli/__init__.py +0 -0
  200. zrb/util/cmd/__init__.py +0 -0
  201. zrb/util/codemod/__init__.py +0 -0
  202. zrb/util/string/__init__.py +0 -0
  203. zrb/xcom/__init__.py +0 -0
  204. {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
- Creates a powerful RAG tool for querying a local knowledge base.
48
-
49
- This factory function generates a tool that can perform semantic searches
50
- over a directory of documents. It automatically indexes the documents into
51
- a vector database, keeping it updated as files change. The generated tool
52
- is ideal for answering questions based on a specific set of documents,
53
- such as project documentation, research papers, or internal wikis.
54
-
55
- The created tool will:
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
- "search_project_docs").
66
- tool_description (str): A clear description of what the generated tool
67
- does and when to use it (e.g., "Searches the project's technical
68
- documentation to answer questions.").
69
- document_dir_path (str, optional): The path to the directory
70
- containing the documents to be indexed.
71
- vector_db_path (str, optional): The path to store the ChromaDB vector
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
- to return.
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): OpenAI base URL for embeddings.
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
- Callable: An asynchronous function that serves as the RAG tool.
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 json.dumps(results)
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
- Args:
211
- query (str): The user query to search for in documents.
212
- Returns:
213
- str: JSON string with search results: {{"ids": [...], "documents": [...], ...}}
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()