kolega-code 0.1.0__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.
- kolega_code/__init__.py +151 -0
- kolega_code/agent/__init__.py +42 -0
- kolega_code/agent/baseagent.py +998 -0
- kolega_code/agent/browseragent.py +123 -0
- kolega_code/agent/coder.py +157 -0
- kolega_code/agent/common.py +41 -0
- kolega_code/agent/compression.py +81 -0
- kolega_code/agent/context.py +112 -0
- kolega_code/agent/conversation.py +408 -0
- kolega_code/agent/generalagent.py +146 -0
- kolega_code/agent/investigationagent.py +123 -0
- kolega_code/agent/planningagent.py +187 -0
- kolega_code/agent/prompt_provider.py +196 -0
- kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
- kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
- kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
- kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
- kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
- kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
- kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
- kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
- kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
- kolega_code/agent/prompts.py +192 -0
- kolega_code/agent/tests/__init__.py +0 -0
- kolega_code/agent/tests/llm/__init__.py +0 -0
- kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
- kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
- kolega_code/agent/tests/llm/test_client.py +773 -0
- kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
- kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
- kolega_code/agent/tests/llm/test_exceptions.py +249 -0
- kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
- kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
- kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
- kolega_code/agent/tests/llm/test_model_specs.py +17 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
- kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
- kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
- kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
- kolega_code/agent/tests/services/__init__.py +1 -0
- kolega_code/agent/tests/services/test_browser.py +447 -0
- kolega_code/agent/tests/services/test_browser_parity.py +353 -0
- kolega_code/agent/tests/services/test_file_system.py +699 -0
- kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
- kolega_code/agent/tests/services/test_terminal.py +154 -0
- kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
- kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
- kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
- kolega_code/agent/tests/test_base_agent.py +1942 -0
- kolega_code/agent/tests/test_coder_attachments.py +330 -0
- kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
- kolega_code/agent/tests/test_commands.py +179 -0
- kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
- kolega_code/agent/tests/test_empty_message_handling.py +48 -0
- kolega_code/agent/tests/test_general_agent.py +242 -0
- kolega_code/agent/tests/test_html.py +320 -0
- kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
- kolega_code/agent/tests/test_planning_agent.py +227 -0
- kolega_code/agent/tests/test_prompt_provider.py +271 -0
- kolega_code/agent/tests/test_tool_registry.py +102 -0
- kolega_code/agent/tests/test_tools.py +549 -0
- kolega_code/agent/tests/tool_backend/__init__.py +0 -0
- kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
- kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
- kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
- kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
- kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
- kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
- kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
- kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
- kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
- kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
- kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
- kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
- kolega_code/agent/tool_backend/agent_tool.py +414 -0
- kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
- kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
- kolega_code/agent/tool_backend/base_tool.py +217 -0
- kolega_code/agent/tool_backend/browser_tool.py +271 -0
- kolega_code/agent/tool_backend/build_tool.py +93 -0
- kolega_code/agent/tool_backend/create_file_tool.py +52 -0
- kolega_code/agent/tool_backend/glob_tool.py +323 -0
- kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
- kolega_code/agent/tool_backend/memory_tool.py +79 -0
- kolega_code/agent/tool_backend/read_file_tool.py +119 -0
- kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
- kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
- kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
- kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
- kolega_code/agent/tool_backend/streaming_tool.py +47 -0
- kolega_code/agent/tool_backend/terminal_tool.py +643 -0
- kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
- kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
- kolega_code/agent/tools.py +1704 -0
- kolega_code/agent/utils/commands.py +94 -0
- kolega_code/cli/__init__.py +1 -0
- kolega_code/cli/app.py +2756 -0
- kolega_code/cli/config.py +280 -0
- kolega_code/cli/connection.py +49 -0
- kolega_code/cli/file_index.py +147 -0
- kolega_code/cli/main.py +564 -0
- kolega_code/cli/mentions.py +155 -0
- kolega_code/cli/messages.py +89 -0
- kolega_code/cli/provider_registry.py +96 -0
- kolega_code/cli/session_store.py +207 -0
- kolega_code/cli/settings.py +87 -0
- kolega_code/cli/skills.py +409 -0
- kolega_code/cli/slash_commands.py +108 -0
- kolega_code/cli/tests/__init__.py +1 -0
- kolega_code/cli/tests/test_app.py +4251 -0
- kolega_code/cli/tests/test_cli_config.py +171 -0
- kolega_code/cli/tests/test_connection.py +26 -0
- kolega_code/cli/tests/test_file_index.py +103 -0
- kolega_code/cli/tests/test_main.py +455 -0
- kolega_code/cli/tests/test_mentions.py +108 -0
- kolega_code/cli/tests/test_session_store.py +67 -0
- kolega_code/cli/tests/test_settings.py +62 -0
- kolega_code/cli/tests/test_skills.py +157 -0
- kolega_code/cli/tests/test_slash_commands.py +88 -0
- kolega_code/cli/theme.py +180 -0
- kolega_code/config.py +154 -0
- kolega_code/events.py +202 -0
- kolega_code/llm/client.py +300 -0
- kolega_code/llm/exceptions.py +285 -0
- kolega_code/llm/instrumented_client.py +520 -0
- kolega_code/llm/models.py +1368 -0
- kolega_code/llm/providers/__init__.py +0 -0
- kolega_code/llm/providers/anthropic.py +387 -0
- kolega_code/llm/providers/base.py +71 -0
- kolega_code/llm/providers/google.py +157 -0
- kolega_code/llm/providers/models.py +37 -0
- kolega_code/llm/providers/openai.py +363 -0
- kolega_code/llm/ratelimit.py +40 -0
- kolega_code/llm/specs.py +67 -0
- kolega_code/llm/tool_execution_ids.py +18 -0
- kolega_code/models/__init__.py +9 -0
- kolega_code/models/sandbox_terminal_state.py +47 -0
- kolega_code/runtime.py +50 -0
- kolega_code/sandbox/README.md +200 -0
- kolega_code/sandbox/__init__.py +21 -0
- kolega_code/sandbox/async_filesystem.py +475 -0
- kolega_code/sandbox/base.py +297 -0
- kolega_code/sandbox/browser.py +25 -0
- kolega_code/sandbox/event_loop.py +43 -0
- kolega_code/sandbox/filesystem.py +341 -0
- kolega_code/sandbox/local.py +118 -0
- kolega_code/sandbox/serializer.py +175 -0
- kolega_code/sandbox/terminal.py +868 -0
- kolega_code/sandbox/utils.py +216 -0
- kolega_code/services/base.py +255 -0
- kolega_code/services/browser.py +444 -0
- kolega_code/services/file_system.py +749 -0
- kolega_code/services/html.py +221 -0
- kolega_code/services/terminal.py +903 -0
- kolega_code/tools/__init__.py +22 -0
- kolega_code/tools/core.py +33 -0
- kolega_code/tools/definitions.py +81 -0
- kolega_code/tools/registry.py +73 -0
- kolega_code-0.1.0.dist-info/METADATA +157 -0
- kolega_code-0.1.0.dist-info/RECORD +171 -0
- kolega_code-0.1.0.dist-info/WHEEL +4 -0
- kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
- kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple, Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
from .base_tool import BaseTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SearchCodebaseTool(BaseTool):
|
|
10
|
+
# Define binary extensions to skip without checking content
|
|
11
|
+
BINARY_EXTENSIONS = {
|
|
12
|
+
".pyc",
|
|
13
|
+
".so",
|
|
14
|
+
".dll",
|
|
15
|
+
".exe",
|
|
16
|
+
".bin",
|
|
17
|
+
".jar",
|
|
18
|
+
".war",
|
|
19
|
+
".jpg",
|
|
20
|
+
".jpeg",
|
|
21
|
+
".png",
|
|
22
|
+
".gif",
|
|
23
|
+
".bmp",
|
|
24
|
+
".ico",
|
|
25
|
+
".svg",
|
|
26
|
+
".pdf",
|
|
27
|
+
".zip",
|
|
28
|
+
".tar",
|
|
29
|
+
".gz",
|
|
30
|
+
".tgz",
|
|
31
|
+
".rar",
|
|
32
|
+
".7z",
|
|
33
|
+
".mp3",
|
|
34
|
+
".mp4",
|
|
35
|
+
".avi",
|
|
36
|
+
".mov",
|
|
37
|
+
".mkv",
|
|
38
|
+
".wav",
|
|
39
|
+
".o",
|
|
40
|
+
".obj",
|
|
41
|
+
".class",
|
|
42
|
+
".binary",
|
|
43
|
+
".wasm",
|
|
44
|
+
".node",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Define directories to exclude
|
|
48
|
+
EXCLUDE_DIRS = {
|
|
49
|
+
".git",
|
|
50
|
+
".svn",
|
|
51
|
+
".hg",
|
|
52
|
+
".idea",
|
|
53
|
+
".vscode",
|
|
54
|
+
"__pycache__",
|
|
55
|
+
"node_modules",
|
|
56
|
+
"venv",
|
|
57
|
+
"env",
|
|
58
|
+
".env",
|
|
59
|
+
"dist",
|
|
60
|
+
"build",
|
|
61
|
+
"target",
|
|
62
|
+
"bin",
|
|
63
|
+
"obj",
|
|
64
|
+
".next",
|
|
65
|
+
".nuxt",
|
|
66
|
+
"coverage",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async def search_codebase(self, pattern: str, file_pattern: str = "*", case_sensitive: bool = False, literal: bool = True) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Search the codebase for files containing a specific pattern (grep functionality).
|
|
72
|
+
|
|
73
|
+
Uses grep command in sandbox environments for maximum efficiency (single command).
|
|
74
|
+
Uses optimized Python implementation for local filesystems.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
pattern: The pattern to search for in files
|
|
78
|
+
file_pattern: Optional glob pattern to filter which files to search (default: all files)
|
|
79
|
+
case_sensitive: Whether the search should be case-sensitive (default: False)
|
|
80
|
+
literal: Whether to treat the pattern as literal text (True) or as a regular expression (False) (default: True)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Markdown formatted list of files and matches, limited to 128 results
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
Exception: If any error occurs during the search operation
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
await self.log_info(f"Searching codebase for pattern: '{pattern}'", sender=self.caller.agent_name)
|
|
90
|
+
|
|
91
|
+
# If literal search, escape special regex characters
|
|
92
|
+
search_pattern = re.escape(pattern) if literal else pattern
|
|
93
|
+
|
|
94
|
+
# Compile the regex pattern to validate it
|
|
95
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
96
|
+
try:
|
|
97
|
+
regex = re.compile(search_pattern, flags)
|
|
98
|
+
except re.error as e:
|
|
99
|
+
error_msg = f"Invalid regular expression: {str(e)}"
|
|
100
|
+
await self.log_error(error_msg, sender=self.caller.agent_name)
|
|
101
|
+
return f"Error: {error_msg}"
|
|
102
|
+
|
|
103
|
+
# Use grep for sandbox environments (single command execution)
|
|
104
|
+
if hasattr(self.filesystem, "sandbox"):
|
|
105
|
+
return await self._search_with_grep_sandbox(search_pattern, file_pattern, case_sensitive, pattern, literal)
|
|
106
|
+
else:
|
|
107
|
+
# Use optimized Python implementation for local filesystem
|
|
108
|
+
return await self._search_with_python(search_pattern, file_pattern, case_sensitive, regex, pattern)
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
error_msg = f"Error searching codebase: {str(e)}"
|
|
112
|
+
await self.log_error(error_msg, sender=self.caller.agent_name)
|
|
113
|
+
return f"Error: {error_msg}"
|
|
114
|
+
|
|
115
|
+
async def _search_with_grep_sandbox(self, pattern: str, file_pattern: str, case_sensitive: bool, original_pattern: str, literal: bool) -> str:
|
|
116
|
+
"""Use single grep command for sandbox environments - most efficient approach"""
|
|
117
|
+
|
|
118
|
+
# Build grep command
|
|
119
|
+
grep_flags = [
|
|
120
|
+
"-r", # Recursive
|
|
121
|
+
"-n", # Show line numbers
|
|
122
|
+
"--binary-files=without-match", # Skip binary files
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# Case sensitivity
|
|
126
|
+
if not case_sensitive:
|
|
127
|
+
grep_flags.append("-i")
|
|
128
|
+
|
|
129
|
+
# Use fixed string matching for literal searches, extended regex otherwise
|
|
130
|
+
if literal:
|
|
131
|
+
grep_flags.append("-F") # Fixed string matching
|
|
132
|
+
else:
|
|
133
|
+
grep_flags.append("-E") # Extended regex
|
|
134
|
+
|
|
135
|
+
# File pattern
|
|
136
|
+
if file_pattern != "*":
|
|
137
|
+
grep_flags.append(f'--include={file_pattern}')
|
|
138
|
+
|
|
139
|
+
# Exclude directories
|
|
140
|
+
for exclude_dir in self.EXCLUDE_DIRS:
|
|
141
|
+
grep_flags.append(f"--exclude-dir={exclude_dir}")
|
|
142
|
+
|
|
143
|
+
# Exclude binary extensions
|
|
144
|
+
for ext in self.BINARY_EXTENSIONS:
|
|
145
|
+
grep_flags.append(f'--exclude=*{ext}')
|
|
146
|
+
|
|
147
|
+
# Build command with awk processing for formatting
|
|
148
|
+
# Use the original pattern for grep when literal=True (no escaping needed with -F flag)
|
|
149
|
+
grep_pattern = original_pattern if literal else pattern
|
|
150
|
+
grep_cmd = f"grep {' '.join(grep_flags)} '{grep_pattern}' . 2>/dev/null"
|
|
151
|
+
|
|
152
|
+
# AWK script to format output exactly like the original
|
|
153
|
+
awk_script = r"""| awk '
|
|
154
|
+
BEGIN {
|
|
155
|
+
file_count = 0;
|
|
156
|
+
current_file = "";
|
|
157
|
+
match_count = 0;
|
|
158
|
+
lines = "";
|
|
159
|
+
lines_shown = 0;
|
|
160
|
+
max_lines_per_file = 5;
|
|
161
|
+
max_files = 128;
|
|
162
|
+
max_line_length = 200;
|
|
163
|
+
}
|
|
164
|
+
{
|
|
165
|
+
# Parse grep output: filename:line_number:content
|
|
166
|
+
colon1 = index($0, ":");
|
|
167
|
+
colon2 = index(substr($0, colon1 + 1), ":") + colon1;
|
|
168
|
+
|
|
169
|
+
file = substr($0, 1, colon1 - 1);
|
|
170
|
+
line_num = substr($0, colon1 + 1, colon2 - colon1 - 1);
|
|
171
|
+
line_content = substr($0, colon2 + 1);
|
|
172
|
+
|
|
173
|
+
# Remove leading/trailing whitespace from content
|
|
174
|
+
gsub(/^[ \t]+|[ \t]+$/, "", line_content);
|
|
175
|
+
|
|
176
|
+
# Truncate long lines to 200 characters
|
|
177
|
+
if (length(line_content) > max_line_length) {
|
|
178
|
+
line_content = substr(line_content, 1, max_line_length) "...";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (file != current_file) {
|
|
182
|
+
# Print previous file results if any
|
|
183
|
+
if (current_file != "") {
|
|
184
|
+
print "- **" current_file "** (" match_count " matches)";
|
|
185
|
+
print substr(lines, 1, length(lines)-1); # Remove trailing newline
|
|
186
|
+
if (lines_shown < match_count && lines_shown >= max_lines_per_file) {
|
|
187
|
+
print " ... and " (match_count - max_lines_per_file) " more matches";
|
|
188
|
+
}
|
|
189
|
+
print "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Check if we reached the file limit
|
|
193
|
+
file_count++;
|
|
194
|
+
if (file_count > max_files) {
|
|
195
|
+
reached_limit = 1;
|
|
196
|
+
exit;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Start new file
|
|
200
|
+
current_file = file;
|
|
201
|
+
match_count = 0;
|
|
202
|
+
lines = "";
|
|
203
|
+
lines_shown = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
match_count++;
|
|
207
|
+
if (lines_shown < max_lines_per_file) {
|
|
208
|
+
lines = lines " Line " line_num ": " line_content "\n";
|
|
209
|
+
lines_shown++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
END {
|
|
213
|
+
# Print last file
|
|
214
|
+
if (current_file != "" && file_count <= max_files) {
|
|
215
|
+
print "- **" current_file "** (" match_count " matches)";
|
|
216
|
+
print substr(lines, 1, length(lines)-1); # Remove trailing newline
|
|
217
|
+
if (lines_shown < match_count && lines_shown >= max_lines_per_file) {
|
|
218
|
+
print " ... and " (match_count - max_lines_per_file) " more matches";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Add warning if limit reached
|
|
223
|
+
if (file_count > max_files || reached_limit) {
|
|
224
|
+
print "";
|
|
225
|
+
print "⚠️ **Note:** Showing only the first " max_files " results. There are more matches in the codebase.";
|
|
226
|
+
}
|
|
227
|
+
}'
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
full_cmd = f"cd {self.filesystem.root_path} && {grep_cmd} {awk_script}"
|
|
231
|
+
|
|
232
|
+
# Execute the single command - sandbox is always async
|
|
233
|
+
result = await self.filesystem.sandbox.commands.run(full_cmd)
|
|
234
|
+
|
|
235
|
+
if result.exit_code != 0 or not result.stdout.strip():
|
|
236
|
+
return f"No matches found for pattern '{original_pattern}'"
|
|
237
|
+
|
|
238
|
+
# Format the final output
|
|
239
|
+
output = f"# Search Results for '{original_pattern}'\n\n"
|
|
240
|
+
output += result.stdout.strip()
|
|
241
|
+
|
|
242
|
+
return output
|
|
243
|
+
|
|
244
|
+
async def _search_with_python(self, pattern: str, file_pattern: str, case_sensitive: bool, regex, original_pattern: str) -> str:
|
|
245
|
+
"""Optimized Python implementation for local filesystems"""
|
|
246
|
+
|
|
247
|
+
# Get files with their info
|
|
248
|
+
files_with_info = await self._get_files_batch_local(file_pattern)
|
|
249
|
+
|
|
250
|
+
# Search through files
|
|
251
|
+
results = []
|
|
252
|
+
total_matches = 0
|
|
253
|
+
max_results = 128
|
|
254
|
+
max_file_size = 10 * 1024 * 1024 # 10MB
|
|
255
|
+
|
|
256
|
+
for file_path, file_size in files_with_info:
|
|
257
|
+
# Skip files that are too large
|
|
258
|
+
if file_size > max_file_size:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
# Skip binary files by extension
|
|
262
|
+
if self._is_likely_binary_by_extension(file_path):
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Read file content once
|
|
267
|
+
content = self.filesystem.read_text(file_path)
|
|
268
|
+
|
|
269
|
+
# Quick binary check on content (first 1024 bytes)
|
|
270
|
+
if "\x00" in content[:1024]:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Search for matches
|
|
274
|
+
matches = list(regex.finditer(content))
|
|
275
|
+
|
|
276
|
+
if matches:
|
|
277
|
+
# Get matching lines with context
|
|
278
|
+
matching_lines = self._extract_matching_lines(content, matches, max_lines=5)
|
|
279
|
+
|
|
280
|
+
# Add to results
|
|
281
|
+
results.append(f"- **{file_path}** ({len(matches)} matches)\n" + "\n".join(matching_lines))
|
|
282
|
+
|
|
283
|
+
total_matches += 1
|
|
284
|
+
if total_matches >= max_results:
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
except Exception:
|
|
288
|
+
# Skip files that can't be read
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# Format results
|
|
292
|
+
if results:
|
|
293
|
+
result_text = f"# Search Results for '{original_pattern}'\n\n"
|
|
294
|
+
if total_matches >= max_results:
|
|
295
|
+
result_text += f"⚠️ **Note:** Showing only the first {max_results} results. There are more matches in the codebase.\n\n"
|
|
296
|
+
result_text += "\n\n".join(results)
|
|
297
|
+
return result_text
|
|
298
|
+
else:
|
|
299
|
+
return f"No matches found for pattern '{original_pattern}'"
|
|
300
|
+
|
|
301
|
+
async def _get_files_batch_local(self, file_pattern: str) -> List[Tuple[str, int]]:
|
|
302
|
+
"""
|
|
303
|
+
Get files for local filesystem with minimal stat calls.
|
|
304
|
+
"""
|
|
305
|
+
# Get files using glob
|
|
306
|
+
if file_pattern == "*":
|
|
307
|
+
files = self.filesystem.glob("**/*")
|
|
308
|
+
else:
|
|
309
|
+
files = self.filesystem.glob(f"**/{file_pattern}")
|
|
310
|
+
|
|
311
|
+
files_with_info = []
|
|
312
|
+
for file_path in files:
|
|
313
|
+
# Quick path-based exclusions
|
|
314
|
+
if self._should_exclude_by_path(file_path):
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# Check if it's a file and get size in one stat call
|
|
318
|
+
try:
|
|
319
|
+
if self.filesystem.is_file(file_path):
|
|
320
|
+
path_obj = self.filesystem.get_path(file_path)
|
|
321
|
+
# Use the existing _should_exclude_file logic from base class
|
|
322
|
+
if self._should_exclude_file(path_obj):
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
# Get size from stat
|
|
326
|
+
try:
|
|
327
|
+
stat_info = path_obj.stat()
|
|
328
|
+
size = stat_info.st_size
|
|
329
|
+
except:
|
|
330
|
+
size = 0
|
|
331
|
+
|
|
332
|
+
files_with_info.append((file_path, size))
|
|
333
|
+
except Exception:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
return files_with_info
|
|
337
|
+
|
|
338
|
+
def _should_exclude_by_path(self, file_path: str) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Check if file should be excluded based on its path alone.
|
|
341
|
+
"""
|
|
342
|
+
path_parts = Path(file_path).parts
|
|
343
|
+
return any(part in self.EXCLUDE_DIRS for part in path_parts)
|
|
344
|
+
|
|
345
|
+
def _is_likely_binary_by_extension(self, file_path: str) -> bool:
|
|
346
|
+
"""
|
|
347
|
+
Quick binary check based on file extension.
|
|
348
|
+
"""
|
|
349
|
+
return Path(file_path).suffix.lower() in self.BINARY_EXTENSIONS
|
|
350
|
+
|
|
351
|
+
def _extract_matching_lines(self, content: str, matches: List, max_lines: int = 5) -> List[str]:
|
|
352
|
+
"""
|
|
353
|
+
Extract lines containing matches with line numbers.
|
|
354
|
+
"""
|
|
355
|
+
lines = content.splitlines()
|
|
356
|
+
line_matches = {}
|
|
357
|
+
|
|
358
|
+
# Find which lines have matches
|
|
359
|
+
for match in matches:
|
|
360
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
361
|
+
if line_num not in line_matches:
|
|
362
|
+
line_content = lines[line_num - 1].strip()
|
|
363
|
+
# Truncate long lines to 200 characters
|
|
364
|
+
if len(line_content) > 200:
|
|
365
|
+
line_content = line_content[:200] + "..."
|
|
366
|
+
line_matches[line_num] = line_content
|
|
367
|
+
|
|
368
|
+
# Format output - matching original format exactly
|
|
369
|
+
matching_lines = []
|
|
370
|
+
for line_num in sorted(line_matches.keys())[:max_lines]:
|
|
371
|
+
matching_lines.append(f" Line {line_num}: {line_matches[line_num]}")
|
|
372
|
+
|
|
373
|
+
# Important: show total matches minus shown lines, not line count
|
|
374
|
+
if len(line_matches) > max_lines:
|
|
375
|
+
matching_lines.append(f" ... and {len(matches) - max_lines} more matches")
|
|
376
|
+
|
|
377
|
+
return matching_lines
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Base class for tools that support streaming responses."""
|
|
2
|
+
|
|
3
|
+
from .base_tool import BaseTool
|
|
4
|
+
from kolega_code.events import AgentEvent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StreamingTool(BaseTool):
|
|
8
|
+
"""Base class for tools that support streaming responses."""
|
|
9
|
+
|
|
10
|
+
async def send_streaming_update(
|
|
11
|
+
self,
|
|
12
|
+
content: str,
|
|
13
|
+
tool_call_id: str,
|
|
14
|
+
tool_name: str,
|
|
15
|
+
is_complete: bool = False,
|
|
16
|
+
stream_mode: str = "replace",
|
|
17
|
+
):
|
|
18
|
+
"""Send a streaming update for this tool's execution.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
content: The partial or complete content to stream
|
|
22
|
+
tool_call_id: The ID of the tool call this update belongs to
|
|
23
|
+
tool_name: The name of the tool being executed
|
|
24
|
+
is_complete: Whether this is the final update
|
|
25
|
+
stream_mode: Whether incomplete updates should replace or append to the visible stream
|
|
26
|
+
"""
|
|
27
|
+
# Attach dispatch metadata when the calling agent is a sub-agent so the
|
|
28
|
+
# UI can route this stream to the right sub-agent display.
|
|
29
|
+
sub_agent_info = None
|
|
30
|
+
sub_agent_context = getattr(self.caller, "sub_agent_context", None)
|
|
31
|
+
if getattr(self.caller, "sub_agent", False) is True and isinstance(sub_agent_context, dict):
|
|
32
|
+
sub_agent_info = dict(sub_agent_context)
|
|
33
|
+
|
|
34
|
+
event = AgentEvent(
|
|
35
|
+
sender=self.caller.agent_name,
|
|
36
|
+
event_type="tool_streaming_update",
|
|
37
|
+
content={
|
|
38
|
+
"text": content,
|
|
39
|
+
"tool_call_id": tool_call_id,
|
|
40
|
+
"tool_name": tool_name,
|
|
41
|
+
"is_complete": is_complete,
|
|
42
|
+
"stream_mode": stream_mode,
|
|
43
|
+
},
|
|
44
|
+
is_streaming=not is_complete,
|
|
45
|
+
sub_agent_info=sub_agent_info,
|
|
46
|
+
)
|
|
47
|
+
await self.connection_manager.broadcast_event(event, self.workspace_id, self.thread_id)
|