janito 0.13.0__py3-none-any.whl → 0.14.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.
@@ -1,240 +1,226 @@
1
- import os
2
- import fnmatch
3
- import re
4
- from typing import List, Tuple
5
- from janito.tools.rich_console import print_info, print_success, print_error, print_warning
6
- from janito.tools.usage_tracker import track_usage
7
-
8
-
9
- @track_usage('search_operations')
10
- def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True) -> Tuple[str, bool]:
11
- """
12
- Search for text patterns within files matching a filename pattern.
13
- Files in .gitignore are always ignored.
14
-
15
- Args:
16
- text_pattern: Text pattern to search for within files
17
- file_pattern: Pattern to match file names against (default: "*")
18
- Multiple patterns can be specified using semicolons or spaces as separators
19
- Examples: "*.py *.toml *.sh *.md test*"
20
- root_dir: Root directory to start search from (default: current directory)
21
- recursive: Whether to search recursively in subdirectories (default: True)
22
-
23
- Returns:
24
- A tuple containing (message, is_error)
25
- """
26
- # Simplified initial message
27
- print_info(f"Searching for '{text_pattern}' in '{file_pattern}'", "Text Search")
28
- try:
29
- # Convert to absolute path if relative
30
- abs_root = os.path.abspath(root_dir)
31
-
32
- if not os.path.isdir(abs_root):
33
- error_msg = f"Error: Directory '{root_dir}' does not exist"
34
- print_error(error_msg, "Directory Error")
35
- return error_msg, True
36
-
37
- # Compile the regex pattern for better performance
38
- try:
39
- regex = re.compile(text_pattern)
40
- except re.error:
41
- # Simplified error message without the specific regex error details
42
- error_msg = f"Error: Invalid regex pattern '{text_pattern}'"
43
- print_error(error_msg, "Search Error")
44
- return error_msg, True
45
-
46
- matching_files = []
47
- match_count = 0
48
- results = []
49
-
50
- # Get gitignore patterns
51
- ignored_patterns = _get_gitignore_patterns(abs_root)
52
-
53
- # Use os.walk for recursive behavior
54
- if recursive:
55
- for dirpath, dirnames, filenames in os.walk(abs_root):
56
- # Skip ignored directories
57
- dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
58
-
59
- # Handle multiple patterns separated by semicolons or spaces
60
- patterns = []
61
- if ';' in file_pattern:
62
- patterns = file_pattern.split(';')
63
- elif ' ' in file_pattern:
64
- patterns = file_pattern.split()
65
- else:
66
- patterns = [file_pattern]
67
-
68
- for pattern in patterns:
69
- for filename in fnmatch.filter(filenames, pattern):
70
- file_path = os.path.join(dirpath, filename)
71
-
72
- # Skip ignored files
73
- if _is_ignored(file_path, ignored_patterns, abs_root):
74
- continue
75
-
76
- # Skip if already processed this file
77
- if file_path in matching_files:
78
- continue
79
-
80
- file_matches = _search_file(file_path, regex, abs_root)
81
- if file_matches:
82
- matching_files.append(file_path)
83
- match_count += len(file_matches)
84
- results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
85
- results.extend(file_matches)
86
- else:
87
- # Non-recursive mode - only search in the specified directory
88
- # Handle multiple patterns separated by semicolons or spaces
89
- patterns = []
90
- if ';' in file_pattern:
91
- patterns = file_pattern.split(';')
92
- elif ' ' in file_pattern:
93
- patterns = file_pattern.split()
94
- else:
95
- patterns = [file_pattern]
96
-
97
- for pattern in patterns:
98
- for filename in fnmatch.filter(os.listdir(abs_root), pattern):
99
- file_path = os.path.join(abs_root, filename)
100
-
101
- # Skip ignored files
102
- if _is_ignored(file_path, ignored_patterns, abs_root):
103
- continue
104
-
105
- # Skip if already processed this file
106
- if file_path in matching_files:
107
- continue
108
-
109
- if os.path.isfile(file_path):
110
- file_matches = _search_file(file_path, regex, abs_root)
111
- if file_matches:
112
- matching_files.append(file_path)
113
- match_count += len(file_matches)
114
- results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
115
- results.extend(file_matches)
116
-
117
- if matching_files:
118
- # Only print the count summary, not the full results
119
- summary = f"{match_count} matches in {len(matching_files)} files"
120
- print_success(summary, "Search Results")
121
-
122
- # Still return the full results for programmatic use
123
- result_text = "\n".join(results)
124
- result_msg = f"Searching for '{text_pattern}' in files matching '{file_pattern}':{result_text}\n{summary}"
125
- return result_msg, False
126
- else:
127
- result_msg = f"No matches found for '{text_pattern}' in files matching '{file_pattern}'"
128
- print_warning("No matches found.")
129
- return result_msg, False
130
-
131
- except Exception as e:
132
- error_msg = f"Error searching text: {str(e)}"
133
- print_error(error_msg, "Search Error")
134
- return error_msg, True
135
-
136
-
137
- def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str]:
138
- """
139
- Search for regex pattern in a file and return matching lines with line numbers.
140
-
141
- Args:
142
- file_path: Path to the file to search
143
- pattern: Compiled regex pattern to search for
144
- root_dir: Root directory (for path display)
145
-
146
- Returns:
147
- List of formatted matches with line numbers and content
148
- """
149
- matches = []
150
- try:
151
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
152
- for i, line in enumerate(f, 1):
153
- if pattern.search(line):
154
- # Truncate long lines for display
155
- display_line = line.strip()
156
- if len(display_line) > 100:
157
- display_line = display_line[:97] + "..."
158
- matches.append(f" Line {i}: {display_line}")
159
- except (UnicodeDecodeError, IOError):
160
- # Skip binary files or files with encoding issues
161
- pass
162
- return matches
163
-
164
-
165
- def _get_gitignore_patterns(root_dir: str) -> List[str]:
166
- """
167
- Get patterns from .gitignore files.
168
-
169
- Args:
170
- root_dir: Root directory to start from
171
-
172
- Returns:
173
- List of gitignore patterns
174
- """
175
- patterns = []
176
-
177
- # Check for .gitignore in the root directory
178
- gitignore_path = os.path.join(root_dir, '.gitignore')
179
- if os.path.isfile(gitignore_path):
180
- try:
181
- with open(gitignore_path, 'r', encoding='utf-8') as f:
182
- for line in f:
183
- line = line.strip()
184
- # Skip empty lines and comments
185
- if line and not line.startswith('#'):
186
- patterns.append(line)
187
- except Exception:
188
- pass
189
-
190
- # Add common patterns that are always ignored
191
- common_patterns = [
192
- '.git/', '.venv/', 'venv/', '__pycache__/', '*.pyc',
193
- '*.pyo', '*.pyd', '.DS_Store', '*.so', '*.egg-info/'
194
- ]
195
- patterns.extend(common_patterns)
196
-
197
- return patterns
198
-
199
-
200
- def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
201
- """
202
- Check if a path should be ignored based on gitignore patterns.
203
-
204
- Args:
205
- path: Path to check
206
- patterns: List of gitignore patterns
207
- root_dir: Root directory for relative paths
208
-
209
- Returns:
210
- True if the path should be ignored, False otherwise
211
- """
212
- # Get the relative path from the root directory
213
- rel_path = os.path.relpath(path, root_dir)
214
-
215
- # Convert to forward slashes for consistency with gitignore patterns
216
- rel_path = rel_path.replace(os.sep, '/')
217
-
218
- # Add trailing slash for directories
219
- if os.path.isdir(path) and not rel_path.endswith('/'):
220
- rel_path += '/'
221
-
222
- for pattern in patterns:
223
- # Handle negation patterns (those starting with !)
224
- if pattern.startswith('!'):
225
- continue # Skip negation patterns for simplicity
226
-
227
- # Handle directory-specific patterns (those ending with /)
228
- if pattern.endswith('/'):
229
- if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
230
- return True
231
-
232
- # Handle file patterns
233
- if fnmatch.fnmatch(rel_path, pattern):
234
- return True
235
-
236
- # Handle patterns without wildcards as path prefixes
237
- if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
238
- return True
239
-
1
+ import os
2
+ import fnmatch
3
+ import re
4
+ import glob
5
+ from typing import List, Tuple
6
+ from janito.tools.rich_console import print_info, print_success, print_error, print_warning
7
+ from janito.tools.usage_tracker import track_usage
8
+
9
+
10
+ @track_usage('search_operations')
11
+ def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True) -> Tuple[str, bool]:
12
+ """
13
+ Search for text patterns within files matching a filename pattern.
14
+ Files in .gitignore are always ignored.
15
+
16
+ Args:
17
+ text_pattern: Text pattern to search for within files
18
+ file_pattern: Pattern to match file paths against (e.g., "*.py", "*/tools/*.py")
19
+ Multiple patterns can be specified using semicolons or spaces as separators
20
+ root_dir: Root directory to start search from (default: current directory)
21
+ recursive: Whether to search recursively in subdirectories (default: True)
22
+
23
+ Returns:
24
+ A tuple containing (message, is_error)
25
+ """
26
+ # Simplified initial message
27
+ print_info(f"Searching for '{text_pattern}' in '{file_pattern}'", "Text Search")
28
+ try:
29
+ # Convert to absolute path if relative
30
+ abs_root = os.path.abspath(root_dir)
31
+
32
+ if not os.path.isdir(abs_root):
33
+ error_msg = f"Error: Directory '{root_dir}' does not exist"
34
+ print_error(error_msg, "Directory Error")
35
+ return error_msg, True
36
+
37
+ # Compile the regex pattern for better performance
38
+ try:
39
+ regex = re.compile(text_pattern)
40
+ except re.error:
41
+ # Simplified error message without the specific regex error details
42
+ error_msg = f"Error: Invalid regex pattern '{text_pattern}'"
43
+ print_error(error_msg, "Search Error")
44
+ return error_msg, True
45
+
46
+ matching_files = []
47
+ match_count = 0
48
+ results = []
49
+
50
+ # Get gitignore patterns
51
+ ignored_patterns = _get_gitignore_patterns(abs_root)
52
+
53
+ # Handle multiple patterns separated by semicolons or spaces
54
+ patterns = []
55
+ if ';' in file_pattern:
56
+ patterns = file_pattern.split(';')
57
+ elif ' ' in file_pattern and not (os.path.sep in file_pattern or '/' in file_pattern):
58
+ # Only split by space if the pattern doesn't appear to be a path
59
+ patterns = file_pattern.split()
60
+ else:
61
+ patterns = [file_pattern]
62
+
63
+ # Process each pattern
64
+ for pattern in patterns:
65
+ # Construct the glob pattern with the root directory
66
+ glob_pattern = os.path.join(abs_root, pattern) if not pattern.startswith(os.path.sep) else pattern
67
+
68
+ # Use recursive glob if needed
69
+ if recursive:
70
+ # Use ** pattern for recursive search if not already in the pattern
71
+ if '**' not in glob_pattern:
72
+ # Check if the pattern already has a directory component
73
+ if os.path.sep in pattern or '/' in pattern:
74
+ # Pattern already has directory component, keep as is
75
+ pass
76
+ else:
77
+ # Add ** to search in all subdirectories
78
+ glob_pattern = os.path.join(abs_root, '**', pattern)
79
+
80
+ # Use recursive=True for Python 3.5+ glob
81
+ glob_files = glob.glob(glob_pattern, recursive=True)
82
+ else:
83
+ # Non-recursive mode - only search in the specified directory
84
+ glob_files = glob.glob(glob_pattern)
85
+
86
+ # Process matching files
87
+ for file_path in glob_files:
88
+ # Skip directories and already processed files
89
+ if not os.path.isfile(file_path) or file_path in matching_files:
90
+ continue
91
+
92
+ # Skip ignored files
93
+ if _is_ignored(file_path, ignored_patterns, abs_root):
94
+ continue
95
+
96
+ file_matches = _search_file(file_path, regex, abs_root)
97
+ if file_matches:
98
+ matching_files.append(file_path)
99
+ match_count += len(file_matches)
100
+ results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
101
+ results.extend(file_matches)
102
+
103
+ if matching_files:
104
+ # Only print the count summary, not the full results
105
+ summary = f"{match_count} matches in {len(matching_files)} files"
106
+ print_success(summary, "Search Results")
107
+
108
+ # Still return the full results for programmatic use
109
+ result_text = "\n".join(results)
110
+ result_msg = f"Searching for '{text_pattern}' in files matching '{file_pattern}':{result_text}\n{summary}"
111
+ return result_msg, False
112
+ else:
113
+ result_msg = f"No matches found for '{text_pattern}' in files matching '{file_pattern}'"
114
+ print_warning("No matches found.")
115
+ return result_msg, False
116
+
117
+ except Exception as e:
118
+ error_msg = f"Error searching text: {str(e)}"
119
+ print_error(error_msg, "Search Error")
120
+ return error_msg, True
121
+
122
+
123
+ def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str]:
124
+ """
125
+ Search for regex pattern in a file and return matching lines with line numbers.
126
+
127
+ Args:
128
+ file_path: Path to the file to search
129
+ pattern: Compiled regex pattern to search for
130
+ root_dir: Root directory (for path display)
131
+
132
+ Returns:
133
+ List of formatted matches with line numbers and content
134
+ """
135
+ matches = []
136
+ try:
137
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
138
+ for i, line in enumerate(f, 1):
139
+ if pattern.search(line):
140
+ # Truncate long lines for display
141
+ display_line = line.strip()
142
+ if len(display_line) > 100:
143
+ display_line = display_line[:97] + "..."
144
+ matches.append(f" Line {i}: {display_line}")
145
+ except (UnicodeDecodeError, IOError):
146
+ # Skip binary files or files with encoding issues
147
+ pass
148
+ return matches
149
+
150
+
151
+ def _get_gitignore_patterns(root_dir: str) -> List[str]:
152
+ """
153
+ Get patterns from .gitignore files.
154
+
155
+ Args:
156
+ root_dir: Root directory to start from
157
+
158
+ Returns:
159
+ List of gitignore patterns
160
+ """
161
+ patterns = []
162
+
163
+ # Check for .gitignore in the root directory
164
+ gitignore_path = os.path.join(root_dir, '.gitignore')
165
+ if os.path.isfile(gitignore_path):
166
+ try:
167
+ with open(gitignore_path, 'r', encoding='utf-8') as f:
168
+ for line in f:
169
+ line = line.strip()
170
+ # Skip empty lines and comments
171
+ if line and not line.startswith('#'):
172
+ patterns.append(line)
173
+ except Exception:
174
+ pass
175
+
176
+ # Add common patterns that are always ignored
177
+ common_patterns = [
178
+ '.git/', '.venv/', 'venv/', '__pycache__/', '*.pyc',
179
+ '*.pyo', '*.pyd', '.DS_Store', '*.so', '*.egg-info/'
180
+ ]
181
+ patterns.extend(common_patterns)
182
+
183
+ return patterns
184
+
185
+
186
+ def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
187
+ """
188
+ Check if a path should be ignored based on gitignore patterns.
189
+
190
+ Args:
191
+ path: Path to check
192
+ patterns: List of gitignore patterns
193
+ root_dir: Root directory for relative paths
194
+
195
+ Returns:
196
+ True if the path should be ignored, False otherwise
197
+ """
198
+ # Get the relative path from the root directory
199
+ rel_path = os.path.relpath(path, root_dir)
200
+
201
+ # Convert to forward slashes for consistency with gitignore patterns
202
+ rel_path = rel_path.replace(os.sep, '/')
203
+
204
+ # Add trailing slash for directories
205
+ if os.path.isdir(path) and not rel_path.endswith('/'):
206
+ rel_path += '/'
207
+
208
+ for pattern in patterns:
209
+ # Handle negation patterns (those starting with !)
210
+ if pattern.startswith('!'):
211
+ continue # Skip negation patterns for simplicity
212
+
213
+ # Handle directory-specific patterns (those ending with /)
214
+ if pattern.endswith('/'):
215
+ if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
216
+ return True
217
+
218
+ # Handle file patterns
219
+ if fnmatch.fnmatch(rel_path, pattern):
220
+ return True
221
+
222
+ # Handle patterns without wildcards as path prefixes
223
+ if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
224
+ return True
225
+
240
226
  return False
janito/tools/think.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ Tool for thinking about something without obtaining new information or changing the database.
3
+ """
4
+ from typing import Tuple
5
+ import logging
6
+ from janito.tools.usage_tracker import track_usage
7
+ from janito.tools.rich_console import print_info
8
+
9
+ # Set up logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ @track_usage('thoughts')
13
+ def think(
14
+ thought: str,
15
+ ) -> Tuple[str, bool]:
16
+ """
17
+ Use the tool to think about something. It will not obtain new information or change the database,
18
+ but just append the thought to the log. Use it when complex reasoning or some cache memory is needed.
19
+
20
+ Args:
21
+ thought: A thought to think about.
22
+
23
+ Returns:
24
+ A tuple containing (message, is_error)
25
+ """
26
+ try:
27
+ # Log the thought
28
+ logger.info(f"Thought: {thought}")
29
+
30
+ # Print a confirmation message
31
+ print_info(f"Thought recorded: {thought[:50]}{'...' if len(thought) > 50 else ''}", "Thinking")
32
+
33
+ return (f"Thought recorded: {thought}", False)
34
+ except Exception as e:
35
+ error_msg = f"Error recording thought: {str(e)}"
36
+ logger.error(error_msg)
37
+ return (error_msg, True)
@@ -33,6 +33,7 @@ class ToolUsageTracker:
33
33
  self.search_operations = 0
34
34
  self.file_views = 0
35
35
  self.partial_file_views = 0
36
+ self.thoughts = 0 # Track the number of thoughts recorded
36
37
 
37
38
  def increment(self, counter_name: str, value: int = 1):
38
39
  """Increment a specific counter by the given value."""