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/builtin/llm/tool/file.py
DELETED
|
@@ -1,626 +0,0 @@
|
|
|
1
|
-
import fnmatch
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import sys
|
|
6
|
-
from typing import Any, Optional
|
|
7
|
-
|
|
8
|
-
from zrb.builtin.llm.tool.sub_agent import create_sub_agent_tool
|
|
9
|
-
from zrb.config.config import CFG
|
|
10
|
-
from zrb.config.llm_rate_limitter import llm_rate_limitter
|
|
11
|
-
from zrb.context.any_context import AnyContext
|
|
12
|
-
from zrb.util.file import read_file, read_file_with_line_numbers, write_file
|
|
13
|
-
|
|
14
|
-
if sys.version_info >= (3, 12):
|
|
15
|
-
from typing import TypedDict
|
|
16
|
-
else:
|
|
17
|
-
from typing_extensions import TypedDict
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class FileToWrite(TypedDict):
|
|
21
|
-
"""Represents a file to be written, with a 'path' and 'content'."""
|
|
22
|
-
|
|
23
|
-
path: str
|
|
24
|
-
content: str
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
DEFAULT_EXCLUDED_PATTERNS = [
|
|
28
|
-
# Common Python artifacts
|
|
29
|
-
"__pycache__",
|
|
30
|
-
"*.pyc",
|
|
31
|
-
"*.pyo",
|
|
32
|
-
"*.pyd",
|
|
33
|
-
".Python",
|
|
34
|
-
"build",
|
|
35
|
-
"develop-eggs",
|
|
36
|
-
"dist",
|
|
37
|
-
"downloads",
|
|
38
|
-
"eggs",
|
|
39
|
-
".eggs",
|
|
40
|
-
"lib",
|
|
41
|
-
"lib64",
|
|
42
|
-
"parts",
|
|
43
|
-
"sdist",
|
|
44
|
-
"var",
|
|
45
|
-
"wheels",
|
|
46
|
-
"share/python-wheels",
|
|
47
|
-
"*.egg-info",
|
|
48
|
-
".installed.cfg",
|
|
49
|
-
"*.egg",
|
|
50
|
-
"MANIFEST",
|
|
51
|
-
# Virtual environments
|
|
52
|
-
".env",
|
|
53
|
-
".venv",
|
|
54
|
-
"env",
|
|
55
|
-
"venv",
|
|
56
|
-
"ENV",
|
|
57
|
-
"VENV",
|
|
58
|
-
# Editor/IDE specific
|
|
59
|
-
".idea",
|
|
60
|
-
".vscode",
|
|
61
|
-
"*.swp",
|
|
62
|
-
"*.swo",
|
|
63
|
-
"*.swn",
|
|
64
|
-
# OS specific
|
|
65
|
-
".DS_Store",
|
|
66
|
-
"Thumbs.db",
|
|
67
|
-
# Version control
|
|
68
|
-
".git",
|
|
69
|
-
".hg",
|
|
70
|
-
".svn",
|
|
71
|
-
# Node.js
|
|
72
|
-
"node_modules",
|
|
73
|
-
"npm-debug.log*",
|
|
74
|
-
"yarn-debug.log*",
|
|
75
|
-
"yarn-error.log*",
|
|
76
|
-
# Test/Coverage artifacts
|
|
77
|
-
".history",
|
|
78
|
-
".tox",
|
|
79
|
-
".nox",
|
|
80
|
-
".coverage",
|
|
81
|
-
".coverage.*",
|
|
82
|
-
".cache",
|
|
83
|
-
".pytest_cache",
|
|
84
|
-
".hypothesis",
|
|
85
|
-
"htmlcov",
|
|
86
|
-
# Compiled files
|
|
87
|
-
"*.so",
|
|
88
|
-
"*.dylib",
|
|
89
|
-
"*.dll",
|
|
90
|
-
# Minified files
|
|
91
|
-
"*.min.css",
|
|
92
|
-
"*.min.js",
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def list_files(
|
|
97
|
-
path: str = ".",
|
|
98
|
-
recursive: bool = True,
|
|
99
|
-
include_hidden: bool = False,
|
|
100
|
-
excluded_patterns: Optional[list[str]] = None,
|
|
101
|
-
) -> str:
|
|
102
|
-
"""
|
|
103
|
-
Lists the files and directories within a specified path.
|
|
104
|
-
|
|
105
|
-
This is a fundamental tool for exploring the file system. Use it to
|
|
106
|
-
discover the structure of a directory, find specific files, or get a
|
|
107
|
-
general overview of the project layout before performing other operations.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
path (str, optional): The directory path to list. Defaults to the
|
|
111
|
-
current directory (".").
|
|
112
|
-
recursive (bool, optional): If True, lists files and directories
|
|
113
|
-
recursively. If False, lists only the top-level contents.
|
|
114
|
-
Defaults to True.
|
|
115
|
-
include_hidden (bool, optional): If True, includes hidden files and
|
|
116
|
-
directories (those starting with a dot). Defaults to False.
|
|
117
|
-
excluded_patterns (list[str], optional): A list of glob patterns to
|
|
118
|
-
exclude from the listing. This is useful for ignoring irrelevant
|
|
119
|
-
files like build artifacts or virtual environments. Defaults to a
|
|
120
|
-
standard list of common exclusion patterns.
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
str: A JSON string containing a list of file and directory paths
|
|
124
|
-
relative to the input path.
|
|
125
|
-
Example: '{"files": ["src/main.py", "README.md"]}'
|
|
126
|
-
Raises:
|
|
127
|
-
FileNotFoundError: If the specified path does not exist.
|
|
128
|
-
"""
|
|
129
|
-
all_files: list[str] = []
|
|
130
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
131
|
-
# Explicitly check if path exists before proceeding
|
|
132
|
-
if not os.path.exists(abs_path):
|
|
133
|
-
# Raise FileNotFoundError, which is a subclass of OSError
|
|
134
|
-
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
135
|
-
# Determine effective exclusion patterns
|
|
136
|
-
patterns_to_exclude = (
|
|
137
|
-
excluded_patterns
|
|
138
|
-
if excluded_patterns is not None
|
|
139
|
-
else DEFAULT_EXCLUDED_PATTERNS
|
|
140
|
-
)
|
|
141
|
-
try:
|
|
142
|
-
if recursive:
|
|
143
|
-
for root, dirs, files in os.walk(abs_path, topdown=True):
|
|
144
|
-
# Filter directories in-place
|
|
145
|
-
dirs[:] = [
|
|
146
|
-
d
|
|
147
|
-
for d in dirs
|
|
148
|
-
if (include_hidden or not _is_hidden(d))
|
|
149
|
-
and not is_excluded(d, patterns_to_exclude)
|
|
150
|
-
]
|
|
151
|
-
# Process files
|
|
152
|
-
for filename in files:
|
|
153
|
-
if (include_hidden or not _is_hidden(filename)) and not is_excluded(
|
|
154
|
-
filename, patterns_to_exclude
|
|
155
|
-
):
|
|
156
|
-
full_path = os.path.join(root, filename)
|
|
157
|
-
# Check rel path for patterns like '**/node_modules/*'
|
|
158
|
-
rel_full_path = os.path.relpath(full_path, abs_path)
|
|
159
|
-
is_rel_path_excluded = is_excluded(
|
|
160
|
-
rel_full_path, patterns_to_exclude
|
|
161
|
-
)
|
|
162
|
-
if not is_rel_path_excluded:
|
|
163
|
-
all_files.append(full_path)
|
|
164
|
-
else:
|
|
165
|
-
# Non-recursive listing (top-level only)
|
|
166
|
-
for item in os.listdir(abs_path):
|
|
167
|
-
full_path = os.path.join(abs_path, item)
|
|
168
|
-
# Include both files and directories if not recursive
|
|
169
|
-
if (include_hidden or not _is_hidden(item)) and not is_excluded(
|
|
170
|
-
item, patterns_to_exclude
|
|
171
|
-
):
|
|
172
|
-
all_files.append(full_path)
|
|
173
|
-
# Return paths relative to the original path requested
|
|
174
|
-
try:
|
|
175
|
-
rel_files = [os.path.relpath(f, abs_path) for f in all_files]
|
|
176
|
-
return json.dumps({"files": sorted(rel_files)})
|
|
177
|
-
except (
|
|
178
|
-
ValueError
|
|
179
|
-
) as e: # Handle case where path is '.' and abs_path is CWD root
|
|
180
|
-
if "path is on mount '" in str(e) and "' which is not on mount '" in str(e):
|
|
181
|
-
# If paths are on different mounts, just use absolute paths
|
|
182
|
-
rel_files = all_files
|
|
183
|
-
return json.dumps({"files": sorted(rel_files)})
|
|
184
|
-
raise
|
|
185
|
-
except (OSError, IOError) as e:
|
|
186
|
-
raise OSError(f"Error listing files in {path}: {e}")
|
|
187
|
-
except Exception as e:
|
|
188
|
-
raise RuntimeError(f"Unexpected error listing files in {path}: {e}")
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _is_hidden(path: str) -> bool:
|
|
192
|
-
"""
|
|
193
|
-
Check if path is hidden (starts with '.') but ignore '.' and '..'.
|
|
194
|
-
Args:
|
|
195
|
-
path: File or directory path to check
|
|
196
|
-
Returns:
|
|
197
|
-
True if the path is hidden, False otherwise
|
|
198
|
-
"""
|
|
199
|
-
basename = os.path.basename(path)
|
|
200
|
-
# Ignore '.' and '..' as they are not typically considered hidden in listings
|
|
201
|
-
if basename == "." or basename == "..":
|
|
202
|
-
return False
|
|
203
|
-
return basename.startswith(".")
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def is_excluded(name: str, patterns: list[str]) -> bool:
|
|
207
|
-
"""Check if a name/path matches any exclusion patterns."""
|
|
208
|
-
for pattern in patterns:
|
|
209
|
-
if fnmatch.fnmatch(name, pattern):
|
|
210
|
-
return True
|
|
211
|
-
# Split the path using the OS path separator.
|
|
212
|
-
parts = name.split(os.path.sep)
|
|
213
|
-
# Check each part of the path.
|
|
214
|
-
for part in parts:
|
|
215
|
-
if fnmatch.fnmatch(part, pattern):
|
|
216
|
-
return True
|
|
217
|
-
return False
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def read_from_file(
|
|
221
|
-
path: str,
|
|
222
|
-
start_line: Optional[int] = None,
|
|
223
|
-
end_line: Optional[int] = None,
|
|
224
|
-
) -> str:
|
|
225
|
-
"""
|
|
226
|
-
Reads the content of a file, optionally from a specific start line to an
|
|
227
|
-
end line.
|
|
228
|
-
|
|
229
|
-
This tool is essential for inspecting file contents. It can read both text
|
|
230
|
-
and PDF files. The returned content is prefixed with line numbers, which is
|
|
231
|
-
crucial for providing context when you need to modify the file later with
|
|
232
|
-
the `apply_diff` tool.
|
|
233
|
-
|
|
234
|
-
Use this tool to:
|
|
235
|
-
- Examine the source code of a file.
|
|
236
|
-
- Read configuration files.
|
|
237
|
-
- Check the contents of a document.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
path (str): The path to the file to read.
|
|
241
|
-
start_line (int, optional): The 1-based line number to start reading
|
|
242
|
-
from. If omitted, reading starts from the beginning of the file.
|
|
243
|
-
end_line (int, optional): The 1-based line number to stop reading at
|
|
244
|
-
(inclusive). If omitted, reads to the end of the file.
|
|
245
|
-
|
|
246
|
-
Returns:
|
|
247
|
-
str: A JSON object containing the file path, the requested content
|
|
248
|
-
with line numbers, the start and end lines, and the total number
|
|
249
|
-
of lines in the file.
|
|
250
|
-
Example: '{"path": "src/main.py", "content": "1| import os\n2|
|
|
251
|
-
3| print(\"Hello, World!\")", "start_line": 1, "end_line": 3,
|
|
252
|
-
"total_lines": 3}'
|
|
253
|
-
Raises:
|
|
254
|
-
FileNotFoundError: If the specified file does not exist.
|
|
255
|
-
"""
|
|
256
|
-
|
|
257
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
258
|
-
# Check if file exists
|
|
259
|
-
if not os.path.exists(abs_path):
|
|
260
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
261
|
-
try:
|
|
262
|
-
content = read_file_with_line_numbers(abs_path)
|
|
263
|
-
lines = content.splitlines()
|
|
264
|
-
total_lines = len(lines)
|
|
265
|
-
# Adjust line indices (convert from 1-based to 0-based)
|
|
266
|
-
start_idx = (start_line - 1) if start_line is not None else 0
|
|
267
|
-
end_idx = end_line if end_line is not None else total_lines
|
|
268
|
-
# Validate indices
|
|
269
|
-
if start_idx < 0:
|
|
270
|
-
start_idx = 0
|
|
271
|
-
if end_idx > total_lines:
|
|
272
|
-
end_idx = total_lines
|
|
273
|
-
if start_idx > end_idx:
|
|
274
|
-
start_idx = end_idx
|
|
275
|
-
# Select the lines for the result
|
|
276
|
-
selected_lines = lines[start_idx:end_idx]
|
|
277
|
-
content_result = "\n".join(selected_lines)
|
|
278
|
-
return json.dumps(
|
|
279
|
-
{
|
|
280
|
-
"path": path,
|
|
281
|
-
"content": content_result,
|
|
282
|
-
"start_line": start_idx + 1, # Convert back to 1-based for output
|
|
283
|
-
"end_line": end_idx, # end_idx is already exclusive upper bound
|
|
284
|
-
"total_lines": total_lines,
|
|
285
|
-
}
|
|
286
|
-
)
|
|
287
|
-
except (OSError, IOError) as e:
|
|
288
|
-
raise OSError(f"Error reading file {path}: {e}")
|
|
289
|
-
except Exception as e:
|
|
290
|
-
raise RuntimeError(f"Unexpected error reading file {path}: {e}")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def write_to_file(
|
|
294
|
-
path: str,
|
|
295
|
-
content: str,
|
|
296
|
-
) -> str:
|
|
297
|
-
"""
|
|
298
|
-
Writes content to a file, completely overwriting it if it exists or
|
|
299
|
-
creating it if it doesn't.
|
|
300
|
-
|
|
301
|
-
Use this tool to create new files or to replace the entire content of
|
|
302
|
-
existing files. This is a destructive operation, so be certain of your
|
|
303
|
-
actions. Always read the file first to understand its contents before
|
|
304
|
-
overwriting it, unless you are creating a new file.
|
|
305
|
-
|
|
306
|
-
Args:
|
|
307
|
-
path (str): The path to the file to write to.
|
|
308
|
-
content (str): The full, complete content to be written to the file.
|
|
309
|
-
Do not use partial content or omit any lines.
|
|
310
|
-
|
|
311
|
-
Returns:
|
|
312
|
-
str: A JSON object indicating success or failure.
|
|
313
|
-
Example: '{"success": true, "path": "new_file.txt"}'
|
|
314
|
-
"""
|
|
315
|
-
try:
|
|
316
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
317
|
-
# Ensure directory exists
|
|
318
|
-
directory = os.path.dirname(abs_path)
|
|
319
|
-
if directory and not os.path.exists(directory):
|
|
320
|
-
os.makedirs(directory, exist_ok=True)
|
|
321
|
-
write_file(abs_path, content)
|
|
322
|
-
result_data = {"success": True, "path": path}
|
|
323
|
-
return json.dumps(result_data)
|
|
324
|
-
except (OSError, IOError) as e:
|
|
325
|
-
raise OSError(f"Error writing file {path}: {e}")
|
|
326
|
-
except Exception as e:
|
|
327
|
-
raise RuntimeError(f"Unexpected error writing file {path}: {e}")
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def search_files(
|
|
331
|
-
path: str,
|
|
332
|
-
regex: str,
|
|
333
|
-
file_pattern: Optional[str] = None,
|
|
334
|
-
include_hidden: bool = True,
|
|
335
|
-
) -> str:
|
|
336
|
-
"""
|
|
337
|
-
Searches for a regular expression (regex) pattern within files in a
|
|
338
|
-
specified directory.
|
|
339
|
-
|
|
340
|
-
This tool is invaluable for finding specific code, configuration, or text
|
|
341
|
-
across multiple files. Use it to locate function definitions, variable
|
|
342
|
-
assignments, error messages, or any other text pattern.
|
|
343
|
-
|
|
344
|
-
Args:
|
|
345
|
-
path (str): The directory path to start the search from.
|
|
346
|
-
regex (str): The Python-compatible regular expression pattern to search
|
|
347
|
-
for.
|
|
348
|
-
file_pattern (str, optional): A glob pattern to filter which files get
|
|
349
|
-
searched (e.g., "*.py", "*.md"). If omitted, all files are
|
|
350
|
-
searched.
|
|
351
|
-
include_hidden (bool, optional): If True, the search will include
|
|
352
|
-
hidden files and directories. Defaults to True.
|
|
353
|
-
|
|
354
|
-
Returns:
|
|
355
|
-
str: A JSON object containing a summary of the search and a list of
|
|
356
|
-
results. Each result includes the file path and a list of matches,
|
|
357
|
-
with each match showing the line number, line content, and a few
|
|
358
|
-
lines of context from before and after the match.
|
|
359
|
-
Raises:
|
|
360
|
-
ValueError: If the provided `regex` pattern is invalid.
|
|
361
|
-
"""
|
|
362
|
-
try:
|
|
363
|
-
pattern = re.compile(regex)
|
|
364
|
-
except re.error as e:
|
|
365
|
-
raise ValueError(f"Invalid regex pattern: {e}")
|
|
366
|
-
search_results = {"summary": "", "results": []}
|
|
367
|
-
match_count = 0
|
|
368
|
-
searched_file_count = 0
|
|
369
|
-
file_match_count = 0
|
|
370
|
-
try:
|
|
371
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
372
|
-
for root, dirs, files in os.walk(abs_path):
|
|
373
|
-
# Skip hidden directories
|
|
374
|
-
dirs[:] = [d for d in dirs if include_hidden or not _is_hidden(d)]
|
|
375
|
-
for filename in files:
|
|
376
|
-
# Skip hidden files
|
|
377
|
-
if not include_hidden and _is_hidden(filename):
|
|
378
|
-
continue
|
|
379
|
-
# Apply file pattern filter if provided
|
|
380
|
-
if file_pattern and not fnmatch.fnmatch(filename, file_pattern):
|
|
381
|
-
continue
|
|
382
|
-
file_path = os.path.join(root, filename)
|
|
383
|
-
rel_file_path = os.path.relpath(file_path, os.getcwd())
|
|
384
|
-
searched_file_count += 1
|
|
385
|
-
try:
|
|
386
|
-
matches = _get_file_matches(file_path, pattern)
|
|
387
|
-
if matches:
|
|
388
|
-
file_match_count += 1
|
|
389
|
-
match_count += len(matches)
|
|
390
|
-
search_results["results"].append(
|
|
391
|
-
{"file": rel_file_path, "matches": matches}
|
|
392
|
-
)
|
|
393
|
-
except IOError as e:
|
|
394
|
-
search_results["results"].append(
|
|
395
|
-
{"file": rel_file_path, "error": str(e)}
|
|
396
|
-
)
|
|
397
|
-
if match_count == 0:
|
|
398
|
-
search_results["summary"] = (
|
|
399
|
-
f"No matches found for pattern '{regex}' in path '{path}' "
|
|
400
|
-
f"(searched {searched_file_count} files)."
|
|
401
|
-
)
|
|
402
|
-
else:
|
|
403
|
-
search_results["summary"] = (
|
|
404
|
-
f"Found {match_count} matches in {file_match_count} files "
|
|
405
|
-
f"(searched {searched_file_count} files)."
|
|
406
|
-
)
|
|
407
|
-
return json.dumps(
|
|
408
|
-
search_results
|
|
409
|
-
) # No need for pretty printing for LLM consumption
|
|
410
|
-
except (OSError, IOError) as e:
|
|
411
|
-
raise OSError(f"Error searching files in {path}: {e}")
|
|
412
|
-
except Exception as e:
|
|
413
|
-
raise RuntimeError(f"Unexpected error searching files in {path}: {e}")
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _get_file_matches(
|
|
417
|
-
file_path: str, pattern: re.Pattern, context_lines: int = 2
|
|
418
|
-
) -> list[dict[str, Any]]:
|
|
419
|
-
"""Search for regex matches in a file with context."""
|
|
420
|
-
try:
|
|
421
|
-
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
422
|
-
lines = f.readlines()
|
|
423
|
-
matches = []
|
|
424
|
-
for line_idx, line in enumerate(lines):
|
|
425
|
-
if pattern.search(line):
|
|
426
|
-
line_num = line_idx + 1
|
|
427
|
-
context_start = max(0, line_idx - context_lines)
|
|
428
|
-
context_end = min(len(lines), line_idx + context_lines + 1)
|
|
429
|
-
match_data = {
|
|
430
|
-
"line_number": line_num,
|
|
431
|
-
"line_content": line.rstrip(),
|
|
432
|
-
"context_before": [
|
|
433
|
-
lines[j].rstrip() for j in range(context_start, line_idx)
|
|
434
|
-
],
|
|
435
|
-
"context_after": [
|
|
436
|
-
lines[j].rstrip() for j in range(line_idx + 1, context_end)
|
|
437
|
-
],
|
|
438
|
-
}
|
|
439
|
-
matches.append(match_data)
|
|
440
|
-
return matches
|
|
441
|
-
except (OSError, IOError) as e:
|
|
442
|
-
raise IOError(f"Error reading {file_path}: {e}")
|
|
443
|
-
except Exception as e:
|
|
444
|
-
raise RuntimeError(f"Unexpected error processing {file_path}: {e}")
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def replace_in_file(
|
|
448
|
-
path: str,
|
|
449
|
-
old_string: str,
|
|
450
|
-
new_string: str,
|
|
451
|
-
) -> str:
|
|
452
|
-
"""
|
|
453
|
-
Replaces the first occurrence of a string in a file.
|
|
454
|
-
|
|
455
|
-
This tool is for making targeted modifications to a file. It is a
|
|
456
|
-
single-step operation that is generally safer and more ergonomic than
|
|
457
|
-
`write_to_file` for small changes.
|
|
458
|
-
|
|
459
|
-
To ensure the replacement is applied correctly and to avoid ambiguity, the
|
|
460
|
-
`old_string` parameter should be a unique, multi-line string that includes
|
|
461
|
-
context from before and after the code you want to change.
|
|
462
|
-
|
|
463
|
-
Args:
|
|
464
|
-
path (str): The path of the file to modify.
|
|
465
|
-
old_string (str): The exact, verbatim string to search for and replace.
|
|
466
|
-
This should be a unique, multi-line block of text.
|
|
467
|
-
new_string (str): The new string that will replace the `old_string`.
|
|
468
|
-
|
|
469
|
-
Returns:
|
|
470
|
-
str: A JSON object indicating the success or failure of the operation.
|
|
471
|
-
Raises:
|
|
472
|
-
FileNotFoundError: If the specified file does not exist.
|
|
473
|
-
ValueError: If the `old_string` is not found in the file.
|
|
474
|
-
"""
|
|
475
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
476
|
-
if not os.path.exists(abs_path):
|
|
477
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
478
|
-
try:
|
|
479
|
-
content = read_file(abs_path)
|
|
480
|
-
if old_string not in content:
|
|
481
|
-
raise ValueError(f"old_string not found in file: {path}")
|
|
482
|
-
new_content = content.replace(old_string, new_string, 1)
|
|
483
|
-
write_file(abs_path, new_content)
|
|
484
|
-
return json.dumps({"success": True, "path": path})
|
|
485
|
-
except ValueError as e:
|
|
486
|
-
raise e
|
|
487
|
-
except (OSError, IOError) as e:
|
|
488
|
-
raise OSError(f"Error applying replacement to {path}: {e}")
|
|
489
|
-
except Exception as e:
|
|
490
|
-
raise RuntimeError(f"Unexpected error applying replacement to {path}: {e}")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
async def analyze_file(
|
|
494
|
-
ctx: AnyContext, path: str, query: str, token_limit: int | None = None
|
|
495
|
-
) -> str:
|
|
496
|
-
"""
|
|
497
|
-
Performs a deep, goal-oriented analysis of a single file using a sub-agent.
|
|
498
|
-
|
|
499
|
-
This tool is ideal for complex questions about a single file that go beyond
|
|
500
|
-
simple reading or searching. It uses a specialized sub-agent to analyze the
|
|
501
|
-
file's content in relation to a specific query.
|
|
502
|
-
|
|
503
|
-
To ensure a focused and effective analysis, it is crucial to provide a
|
|
504
|
-
clear and specific query. Vague queries will result in a vague analysis
|
|
505
|
-
and may cause the tool to run for a long time.
|
|
506
|
-
|
|
507
|
-
Use this tool to:
|
|
508
|
-
- Summarize the purpose and functionality of a script or configuration file.
|
|
509
|
-
- Extract the structure of a file (e.g., "List all the function names in
|
|
510
|
-
this Python file").
|
|
511
|
-
- Perform a detailed code review of a specific file.
|
|
512
|
-
- Answer complex questions like, "How is the 'User' class used in this
|
|
513
|
-
file?".
|
|
514
|
-
|
|
515
|
-
Args:
|
|
516
|
-
path (str): The path to the file to be analyzed.
|
|
517
|
-
query (str): A clear and specific question or instruction about what to
|
|
518
|
-
analyze in the file.
|
|
519
|
-
- Good query: "What is the purpose of the 'User' class in this
|
|
520
|
-
file?"
|
|
521
|
-
- Good query: "List all the function names in this Python file."
|
|
522
|
-
- Bad query: "Analyze this file."
|
|
523
|
-
- Bad query: "Tell me about this code."
|
|
524
|
-
token_limit (int, optional): The maximum token length of the file
|
|
525
|
-
content to be passed to the analysis sub-agent.
|
|
526
|
-
|
|
527
|
-
Returns:
|
|
528
|
-
str: A detailed, markdown-formatted analysis of the file, tailored to
|
|
529
|
-
the specified query.
|
|
530
|
-
Raises:
|
|
531
|
-
FileNotFoundError: If the specified file does not exist.
|
|
532
|
-
"""
|
|
533
|
-
if token_limit is None:
|
|
534
|
-
token_limit = CFG.LLM_FILE_ANALYSIS_TOKEN_LIMIT
|
|
535
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
536
|
-
if not os.path.exists(abs_path):
|
|
537
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
538
|
-
file_content = read_file(abs_path)
|
|
539
|
-
_analyze_file = create_sub_agent_tool(
|
|
540
|
-
tool_name="analyze_file",
|
|
541
|
-
tool_description="analyze file with LLM capability",
|
|
542
|
-
system_prompt=CFG.LLM_FILE_EXTRACTOR_SYSTEM_PROMPT,
|
|
543
|
-
tools=[read_from_file, search_files],
|
|
544
|
-
)
|
|
545
|
-
payload = json.dumps(
|
|
546
|
-
{"instruction": query, "file_path": abs_path, "file_content": file_content}
|
|
547
|
-
)
|
|
548
|
-
clipped_payload = llm_rate_limitter.clip_prompt(payload, token_limit)
|
|
549
|
-
return await _analyze_file(ctx, clipped_payload)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
def read_many_files(paths: list[str]) -> str:
|
|
553
|
-
"""
|
|
554
|
-
Reads and returns the full content of multiple files at once.
|
|
555
|
-
|
|
556
|
-
This tool is highly efficient for gathering context from several files
|
|
557
|
-
simultaneously. Use it when you need to understand how different files in a
|
|
558
|
-
project relate to each other, or when you need to inspect a set of related
|
|
559
|
-
configuration or source code files.
|
|
560
|
-
|
|
561
|
-
Args:
|
|
562
|
-
paths (list[str]): A list of paths to the files you want to read. It is
|
|
563
|
-
crucial to provide accurate paths. Use the `list_files` tool first
|
|
564
|
-
if you are unsure about the exact file locations.
|
|
565
|
-
|
|
566
|
-
Returns:
|
|
567
|
-
str: A JSON object where keys are the file paths and values are their
|
|
568
|
-
corresponding contents, prefixed with line numbers. If a file
|
|
569
|
-
cannot be read, its value will be an error message.
|
|
570
|
-
Example: '{"results": {"src/api.py": "1| import ...",
|
|
571
|
-
"config.yaml": "1| key: value"}}'
|
|
572
|
-
"""
|
|
573
|
-
results = {}
|
|
574
|
-
for path in paths:
|
|
575
|
-
try:
|
|
576
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
577
|
-
if not os.path.exists(abs_path):
|
|
578
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
579
|
-
content = read_file_with_line_numbers(abs_path)
|
|
580
|
-
results[path] = content
|
|
581
|
-
except Exception as e:
|
|
582
|
-
results[path] = f"Error reading file: {e}"
|
|
583
|
-
return json.dumps({"results": results})
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
def write_many_files(files: list[FileToWrite]) -> str:
|
|
587
|
-
"""
|
|
588
|
-
Writes content to multiple files in a single, atomic operation.
|
|
589
|
-
|
|
590
|
-
This tool is for applying widespread changes to a project, such as
|
|
591
|
-
creating a set of new files from a template, updating multiple
|
|
592
|
-
configuration files, or performing a large-scale refactoring.
|
|
593
|
-
|
|
594
|
-
Each file's content is completely replaced. If a file does not exist, it
|
|
595
|
-
will be created. If it exists, its current content will be entirely
|
|
596
|
-
overwritten. Therefore, you must provide the full, intended content for
|
|
597
|
-
each file.
|
|
598
|
-
|
|
599
|
-
Args:
|
|
600
|
-
files: A list of file objects, where each object is a dictionary
|
|
601
|
-
containing a 'path' and the complete 'content'.
|
|
602
|
-
|
|
603
|
-
Returns:
|
|
604
|
-
str: A JSON object summarizing the operation, listing successfully
|
|
605
|
-
written files and any files that failed, along with corresponding
|
|
606
|
-
error messages.
|
|
607
|
-
Example: '{"success": ["file1.py", "file2.txt"], "errors": {}}'
|
|
608
|
-
"""
|
|
609
|
-
success = []
|
|
610
|
-
errors = {}
|
|
611
|
-
# 4. Access the data using dictionary key-lookup syntax.
|
|
612
|
-
for file in files:
|
|
613
|
-
try:
|
|
614
|
-
# Use file['path'] and file['content'] instead of file.path
|
|
615
|
-
path = file["path"]
|
|
616
|
-
content = file["content"]
|
|
617
|
-
|
|
618
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
619
|
-
directory = os.path.dirname(abs_path)
|
|
620
|
-
if directory and not os.path.exists(directory):
|
|
621
|
-
os.makedirs(directory, exist_ok=True)
|
|
622
|
-
write_file(abs_path, content)
|
|
623
|
-
success.append(path)
|
|
624
|
-
except Exception as e:
|
|
625
|
-
errors[path] = f"Error writing file: {e}"
|
|
626
|
-
return json.dumps({"success": success, "errors": errors})
|