zrb 1.21.29__py3-none-any.whl → 2.0.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of zrb might be problematic. Click here for more details.

Files changed (192) hide show
  1. zrb/__init__.py +118 -129
  2. zrb/builtin/__init__.py +54 -2
  3. zrb/builtin/llm/chat.py +147 -0
  4. zrb/callback/callback.py +8 -1
  5. zrb/cmd/cmd_result.py +2 -1
  6. zrb/config/config.py +491 -280
  7. zrb/config/helper.py +84 -0
  8. zrb/config/web_auth_config.py +50 -35
  9. zrb/context/any_shared_context.py +13 -2
  10. zrb/context/context.py +31 -3
  11. zrb/context/print_fn.py +13 -0
  12. zrb/context/shared_context.py +14 -1
  13. zrb/input/option_input.py +30 -2
  14. zrb/llm/agent/__init__.py +9 -0
  15. zrb/llm/agent/agent.py +215 -0
  16. zrb/llm/agent/summarizer.py +20 -0
  17. zrb/llm/app/__init__.py +10 -0
  18. zrb/llm/app/completion.py +281 -0
  19. zrb/llm/app/confirmation/allow_tool.py +66 -0
  20. zrb/llm/app/confirmation/handler.py +178 -0
  21. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  22. zrb/llm/app/keybinding.py +34 -0
  23. zrb/llm/app/layout.py +117 -0
  24. zrb/llm/app/lexer.py +155 -0
  25. zrb/llm/app/redirection.py +28 -0
  26. zrb/llm/app/style.py +16 -0
  27. zrb/llm/app/ui.py +733 -0
  28. zrb/llm/config/__init__.py +4 -0
  29. zrb/llm/config/config.py +122 -0
  30. zrb/llm/config/limiter.py +247 -0
  31. zrb/llm/history_manager/__init__.py +4 -0
  32. zrb/llm/history_manager/any_history_manager.py +23 -0
  33. zrb/llm/history_manager/file_history_manager.py +91 -0
  34. zrb/llm/history_processor/summarizer.py +108 -0
  35. zrb/llm/note/__init__.py +3 -0
  36. zrb/llm/note/manager.py +122 -0
  37. zrb/llm/prompt/__init__.py +29 -0
  38. zrb/llm/prompt/claude_compatibility.py +92 -0
  39. zrb/llm/prompt/compose.py +55 -0
  40. zrb/llm/prompt/default.py +51 -0
  41. zrb/llm/prompt/markdown/mandate.md +23 -0
  42. zrb/llm/prompt/markdown/persona.md +3 -0
  43. zrb/llm/prompt/markdown/summarizer.md +21 -0
  44. zrb/llm/prompt/note.py +41 -0
  45. zrb/llm/prompt/system_context.py +46 -0
  46. zrb/llm/prompt/zrb.py +41 -0
  47. zrb/llm/skill/__init__.py +3 -0
  48. zrb/llm/skill/manager.py +86 -0
  49. zrb/llm/task/__init__.py +4 -0
  50. zrb/llm/task/llm_chat_task.py +316 -0
  51. zrb/llm/task/llm_task.py +245 -0
  52. zrb/llm/tool/__init__.py +39 -0
  53. zrb/llm/tool/bash.py +75 -0
  54. zrb/llm/tool/code.py +266 -0
  55. zrb/llm/tool/file.py +419 -0
  56. zrb/llm/tool/note.py +70 -0
  57. zrb/{builtin/llm → llm}/tool/rag.py +8 -5
  58. zrb/llm/tool/search/brave.py +53 -0
  59. zrb/llm/tool/search/searxng.py +47 -0
  60. zrb/llm/tool/search/serpapi.py +47 -0
  61. zrb/llm/tool/skill.py +19 -0
  62. zrb/llm/tool/sub_agent.py +70 -0
  63. zrb/llm/tool/web.py +97 -0
  64. zrb/llm/tool/zrb_task.py +66 -0
  65. zrb/llm/util/attachment.py +101 -0
  66. zrb/llm/util/prompt.py +104 -0
  67. zrb/llm/util/stream_response.py +178 -0
  68. zrb/session/any_session.py +0 -3
  69. zrb/session/session.py +1 -1
  70. zrb/task/base/context.py +25 -13
  71. zrb/task/base/execution.py +52 -47
  72. zrb/task/base/lifecycle.py +7 -4
  73. zrb/task/base_task.py +48 -49
  74. zrb/task/base_trigger.py +4 -1
  75. zrb/task/cmd_task.py +6 -0
  76. zrb/task/http_check.py +11 -5
  77. zrb/task/make_task.py +3 -0
  78. zrb/task/rsync_task.py +5 -0
  79. zrb/task/scaffolder.py +7 -4
  80. zrb/task/scheduler.py +3 -0
  81. zrb/task/tcp_check.py +6 -4
  82. zrb/util/ascii_art/art/bee.txt +17 -0
  83. zrb/util/ascii_art/art/cat.txt +9 -0
  84. zrb/util/ascii_art/art/ghost.txt +16 -0
  85. zrb/util/ascii_art/art/panda.txt +17 -0
  86. zrb/util/ascii_art/art/rose.txt +14 -0
  87. zrb/util/ascii_art/art/unicorn.txt +15 -0
  88. zrb/util/ascii_art/banner.py +92 -0
  89. zrb/util/cli/markdown.py +22 -2
  90. zrb/util/cmd/command.py +33 -10
  91. zrb/util/file.py +51 -32
  92. zrb/util/match.py +78 -0
  93. zrb/util/run.py +3 -3
  94. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/METADATA +9 -15
  95. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/RECORD +100 -128
  96. zrb/attr/__init__.py +0 -0
  97. zrb/builtin/llm/attachment.py +0 -40
  98. zrb/builtin/llm/chat_completion.py +0 -274
  99. zrb/builtin/llm/chat_session.py +0 -270
  100. zrb/builtin/llm/chat_session_cmd.py +0 -288
  101. zrb/builtin/llm/chat_trigger.py +0 -79
  102. zrb/builtin/llm/history.py +0 -71
  103. zrb/builtin/llm/input.py +0 -27
  104. zrb/builtin/llm/llm_ask.py +0 -269
  105. zrb/builtin/llm/previous-session.js +0 -21
  106. zrb/builtin/llm/tool/__init__.py +0 -0
  107. zrb/builtin/llm/tool/api.py +0 -75
  108. zrb/builtin/llm/tool/cli.py +0 -52
  109. zrb/builtin/llm/tool/code.py +0 -236
  110. zrb/builtin/llm/tool/file.py +0 -560
  111. zrb/builtin/llm/tool/note.py +0 -84
  112. zrb/builtin/llm/tool/sub_agent.py +0 -150
  113. zrb/builtin/llm/tool/web.py +0 -171
  114. zrb/builtin/project/__init__.py +0 -0
  115. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  116. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  117. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  118. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  119. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  120. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  121. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  122. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  123. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  124. zrb/builtin/project/create/__init__.py +0 -0
  125. zrb/builtin/shell/__init__.py +0 -0
  126. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  127. zrb/callback/__init__.py +0 -0
  128. zrb/cmd/__init__.py +0 -0
  129. zrb/config/default_prompt/interactive_system_prompt.md +0 -29
  130. zrb/config/default_prompt/persona.md +0 -1
  131. zrb/config/default_prompt/summarization_prompt.md +0 -57
  132. zrb/config/default_prompt/system_prompt.md +0 -38
  133. zrb/config/llm_config.py +0 -339
  134. zrb/config/llm_context/config.py +0 -166
  135. zrb/config/llm_context/config_parser.py +0 -40
  136. zrb/config/llm_context/workflow.py +0 -81
  137. zrb/config/llm_rate_limitter.py +0 -190
  138. zrb/content_transformer/__init__.py +0 -0
  139. zrb/context/__init__.py +0 -0
  140. zrb/dot_dict/__init__.py +0 -0
  141. zrb/env/__init__.py +0 -0
  142. zrb/group/__init__.py +0 -0
  143. zrb/input/__init__.py +0 -0
  144. zrb/runner/__init__.py +0 -0
  145. zrb/runner/web_route/__init__.py +0 -0
  146. zrb/runner/web_route/home_page/__init__.py +0 -0
  147. zrb/session/__init__.py +0 -0
  148. zrb/session_state_log/__init__.py +0 -0
  149. zrb/session_state_logger/__init__.py +0 -0
  150. zrb/task/__init__.py +0 -0
  151. zrb/task/base/__init__.py +0 -0
  152. zrb/task/llm/__init__.py +0 -0
  153. zrb/task/llm/agent.py +0 -204
  154. zrb/task/llm/agent_runner.py +0 -152
  155. zrb/task/llm/config.py +0 -122
  156. zrb/task/llm/conversation_history.py +0 -209
  157. zrb/task/llm/conversation_history_model.py +0 -67
  158. zrb/task/llm/default_workflow/coding/workflow.md +0 -41
  159. zrb/task/llm/default_workflow/copywriting/workflow.md +0 -68
  160. zrb/task/llm/default_workflow/git/workflow.md +0 -118
  161. zrb/task/llm/default_workflow/golang/workflow.md +0 -128
  162. zrb/task/llm/default_workflow/html-css/workflow.md +0 -135
  163. zrb/task/llm/default_workflow/java/workflow.md +0 -146
  164. zrb/task/llm/default_workflow/javascript/workflow.md +0 -158
  165. zrb/task/llm/default_workflow/python/workflow.md +0 -160
  166. zrb/task/llm/default_workflow/researching/workflow.md +0 -153
  167. zrb/task/llm/default_workflow/rust/workflow.md +0 -162
  168. zrb/task/llm/default_workflow/shell/workflow.md +0 -299
  169. zrb/task/llm/error.py +0 -95
  170. zrb/task/llm/file_replacement.py +0 -206
  171. zrb/task/llm/file_tool_model.py +0 -57
  172. zrb/task/llm/history_processor.py +0 -206
  173. zrb/task/llm/history_summarization.py +0 -25
  174. zrb/task/llm/print_node.py +0 -221
  175. zrb/task/llm/prompt.py +0 -321
  176. zrb/task/llm/subagent_conversation_history.py +0 -41
  177. zrb/task/llm/tool_wrapper.py +0 -361
  178. zrb/task/llm/typing.py +0 -3
  179. zrb/task/llm/workflow.py +0 -76
  180. zrb/task/llm_task.py +0 -379
  181. zrb/task_status/__init__.py +0 -0
  182. zrb/util/__init__.py +0 -0
  183. zrb/util/cli/__init__.py +0 -0
  184. zrb/util/cmd/__init__.py +0 -0
  185. zrb/util/codemod/__init__.py +0 -0
  186. zrb/util/string/__init__.py +0 -0
  187. zrb/xcom/__init__.py +0 -0
  188. /zrb/{config/default_prompt/file_extractor_system_prompt.md → llm/prompt/markdown/file_extractor.md} +0 -0
  189. /zrb/{config/default_prompt/repo_extractor_system_prompt.md → llm/prompt/markdown/repo_extractor.md} +0 -0
  190. /zrb/{config/default_prompt/repo_summarizer_system_prompt.md → llm/prompt/markdown/repo_summarizer.md} +0 -0
  191. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +0 -0
  192. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
@@ -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)
@@ -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}"