amd-gaia 0.15.2__py3-none-any.whl → 0.15.3__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.
- {amd_gaia-0.15.2.dist-info → amd_gaia-0.15.3.dist-info}/METADATA +2 -1
- {amd_gaia-0.15.2.dist-info → amd_gaia-0.15.3.dist-info}/RECORD +17 -15
- {amd_gaia-0.15.2.dist-info → amd_gaia-0.15.3.dist-info}/WHEEL +1 -1
- gaia/agents/base/agent.py +272 -23
- gaia/agents/base/console.py +208 -9
- gaia/agents/tools/__init__.py +11 -0
- gaia/agents/tools/file_tools.py +715 -0
- gaia/cli.py +242 -2
- gaia/installer/init_command.py +433 -103
- gaia/installer/lemonade_installer.py +62 -3
- gaia/llm/lemonade_client.py +143 -0
- gaia/llm/lemonade_manager.py +55 -11
- gaia/llm/providers/lemonade.py +9 -0
- gaia/version.py +2 -2
- {amd_gaia-0.15.2.dist-info → amd_gaia-0.15.3.dist-info}/entry_points.txt +0 -0
- {amd_gaia-0.15.2.dist-info → amd_gaia-0.15.3.dist-info}/licenses/LICENSE.md +0 -0
- {amd_gaia-0.15.2.dist-info → amd_gaia-0.15.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""
|
|
4
|
+
Shared File Search and Management Tools.
|
|
5
|
+
|
|
6
|
+
Provides common file search and read operations that can be used across multiple agents.
|
|
7
|
+
These tools are agent-agnostic and don't depend on specific agent functionality.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileSearchToolsMixin:
|
|
21
|
+
"""
|
|
22
|
+
Mixin providing shared file search and read operations.
|
|
23
|
+
|
|
24
|
+
Tools provided:
|
|
25
|
+
- search_file: Search filesystem for files by name/pattern
|
|
26
|
+
- search_directory: Search filesystem for directories by name
|
|
27
|
+
- read_file: Read any file with intelligent type-based analysis
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def _format_file_list(self, file_paths: list) -> list:
|
|
31
|
+
"""Format file paths for numbered display to user."""
|
|
32
|
+
file_list = []
|
|
33
|
+
for i, fpath in enumerate(file_paths, 1):
|
|
34
|
+
p = Path(fpath)
|
|
35
|
+
file_list.append(
|
|
36
|
+
{
|
|
37
|
+
"number": i,
|
|
38
|
+
"name": p.name,
|
|
39
|
+
"path": str(fpath),
|
|
40
|
+
"directory": str(p.parent),
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
return file_list
|
|
44
|
+
|
|
45
|
+
def register_file_search_tools(self) -> None:
|
|
46
|
+
"""Register shared file search tools."""
|
|
47
|
+
from gaia.agents.base.tools import tool
|
|
48
|
+
|
|
49
|
+
@tool(
|
|
50
|
+
atomic=True,
|
|
51
|
+
name="search_file",
|
|
52
|
+
description="Search for files by name/pattern across entire drive(s). Searches common locations first, then does deep search. Use when user asks 'find X on my drive'.",
|
|
53
|
+
parameters={
|
|
54
|
+
"file_pattern": {
|
|
55
|
+
"type": "str",
|
|
56
|
+
"description": "File name pattern to search for (e.g., 'oil', 'manual', '*.pdf'). Supports partial matches.",
|
|
57
|
+
"required": True,
|
|
58
|
+
},
|
|
59
|
+
"search_all_drives": {
|
|
60
|
+
"type": "bool",
|
|
61
|
+
"description": "Search all available drives (default: True on Windows)",
|
|
62
|
+
"required": False,
|
|
63
|
+
},
|
|
64
|
+
"file_types": {
|
|
65
|
+
"type": "str",
|
|
66
|
+
"description": "Comma-separated file extensions to filter (e.g., 'pdf,docx,txt'). Default: all document types",
|
|
67
|
+
"required": False,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
def search_file(
|
|
72
|
+
file_pattern: str, search_all_drives: bool = True, file_types: str = None
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Search for files with intelligent prioritization.
|
|
76
|
+
|
|
77
|
+
Strategy:
|
|
78
|
+
1. Search common document locations first (fast)
|
|
79
|
+
2. If not found, search entire drive(s) (thorough)
|
|
80
|
+
3. Filter by document file types for speed
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# Document file extensions to search
|
|
84
|
+
if file_types:
|
|
85
|
+
doc_extensions = {
|
|
86
|
+
f".{ext.strip().lower()}" for ext in file_types.split(",")
|
|
87
|
+
}
|
|
88
|
+
else:
|
|
89
|
+
doc_extensions = {
|
|
90
|
+
".pdf",
|
|
91
|
+
".doc",
|
|
92
|
+
".docx",
|
|
93
|
+
".txt",
|
|
94
|
+
".md",
|
|
95
|
+
".csv",
|
|
96
|
+
".json",
|
|
97
|
+
".xlsx",
|
|
98
|
+
".xls",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
matching_files = []
|
|
102
|
+
pattern_lower = file_pattern.lower()
|
|
103
|
+
searched_locations = []
|
|
104
|
+
|
|
105
|
+
def matches_pattern_and_type(file_path: Path) -> bool:
|
|
106
|
+
"""Check if file matches pattern and is a document type."""
|
|
107
|
+
name_match = pattern_lower in file_path.name.lower()
|
|
108
|
+
type_match = file_path.suffix.lower() in doc_extensions
|
|
109
|
+
return name_match and type_match
|
|
110
|
+
|
|
111
|
+
def search_location(location: Path, max_depth: int = 999):
|
|
112
|
+
"""Search a specific location up to max_depth."""
|
|
113
|
+
if not location.exists():
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
searched_locations.append(str(location))
|
|
117
|
+
logger.debug(f"Searching {location}...")
|
|
118
|
+
|
|
119
|
+
def search_recursive(current_path: Path, depth: int):
|
|
120
|
+
if depth > max_depth or len(matching_files) >= 20:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
for item in current_path.iterdir():
|
|
125
|
+
# Skip system/hidden directories
|
|
126
|
+
if item.name.startswith(
|
|
127
|
+
(".", "$", "Windows", "Program Files")
|
|
128
|
+
):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if item.is_file():
|
|
132
|
+
if matches_pattern_and_type(item):
|
|
133
|
+
matching_files.append(str(item.resolve()))
|
|
134
|
+
logger.debug(f"Found: {item.name}")
|
|
135
|
+
elif item.is_dir() and depth < max_depth:
|
|
136
|
+
search_recursive(item, depth + 1)
|
|
137
|
+
except (PermissionError, OSError) as e:
|
|
138
|
+
logger.debug(f"Skipping {current_path}: {e}")
|
|
139
|
+
|
|
140
|
+
search_recursive(location, 0)
|
|
141
|
+
|
|
142
|
+
# Phase 0: Search CURRENT WORKING DIRECTORY first and thoroughly
|
|
143
|
+
cwd = Path.cwd()
|
|
144
|
+
home = Path.home()
|
|
145
|
+
|
|
146
|
+
# Show progress to user
|
|
147
|
+
if hasattr(self, "console") and hasattr(self.console, "start_progress"):
|
|
148
|
+
self.console.start_progress(
|
|
149
|
+
f"🔍 Searching current directory ({cwd.name}) for '{file_pattern}'..."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"Phase 0: Deep search of current directory for '{file_pattern}'..."
|
|
154
|
+
)
|
|
155
|
+
logger.debug(f"Current directory: {cwd}")
|
|
156
|
+
|
|
157
|
+
# Search current directory thoroughly (unlimited depth)
|
|
158
|
+
search_location(cwd, max_depth=999)
|
|
159
|
+
|
|
160
|
+
# If found in CWD, return immediately
|
|
161
|
+
if matching_files:
|
|
162
|
+
if hasattr(self, "console") and hasattr(
|
|
163
|
+
self.console, "stop_progress"
|
|
164
|
+
):
|
|
165
|
+
self.console.stop_progress()
|
|
166
|
+
|
|
167
|
+
# Add helpful context about where it was found
|
|
168
|
+
return {
|
|
169
|
+
"status": "success",
|
|
170
|
+
"files": matching_files[:10],
|
|
171
|
+
"file_list": self._format_file_list(matching_files[:10]),
|
|
172
|
+
"count": len(matching_files),
|
|
173
|
+
"search_context": "current_directory",
|
|
174
|
+
"display_message": f"✓ Found {len(matching_files)} file(s) in current directory ({cwd.name})",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Phase 1: Search common locations
|
|
178
|
+
if hasattr(self, "console") and hasattr(self.console, "start_progress"):
|
|
179
|
+
self.console.start_progress(
|
|
180
|
+
"🔍 Searching common folders (Documents, Downloads, Desktop)..."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
logger.debug("Phase 1: Searching common document locations...")
|
|
184
|
+
|
|
185
|
+
common_locations = [
|
|
186
|
+
home / "Documents",
|
|
187
|
+
home / "Downloads",
|
|
188
|
+
home / "Desktop",
|
|
189
|
+
home / "OneDrive",
|
|
190
|
+
home / "Google Drive",
|
|
191
|
+
home / "Dropbox",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
for location in common_locations:
|
|
195
|
+
if len(matching_files) >= 10:
|
|
196
|
+
break
|
|
197
|
+
search_location(location, max_depth=5)
|
|
198
|
+
|
|
199
|
+
# If found in common locations, return
|
|
200
|
+
if matching_files:
|
|
201
|
+
if hasattr(self, "console") and hasattr(
|
|
202
|
+
self.console, "stop_progress"
|
|
203
|
+
):
|
|
204
|
+
self.console.stop_progress()
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"status": "success",
|
|
208
|
+
"files": matching_files[:10],
|
|
209
|
+
"file_list": self._format_file_list(matching_files[:10]),
|
|
210
|
+
"count": len(matching_files),
|
|
211
|
+
"total_locations_searched": len(searched_locations),
|
|
212
|
+
"search_context": "common_locations",
|
|
213
|
+
"display_message": f"✓ Found {len(matching_files)} file(s) in common locations",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Phase 2: Deep drive search if still not found
|
|
217
|
+
if hasattr(self, "console") and hasattr(self.console, "start_progress"):
|
|
218
|
+
self.console.start_progress(
|
|
219
|
+
"🔍 Deep search across all drives (this may take a minute)..."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
logger.debug("Phase 2: Deep search across drive(s)...")
|
|
223
|
+
|
|
224
|
+
if platform.system() == "Windows" and search_all_drives:
|
|
225
|
+
# Search all available drives on Windows
|
|
226
|
+
import string
|
|
227
|
+
|
|
228
|
+
for drive_letter in string.ascii_uppercase:
|
|
229
|
+
drive = Path(f"{drive_letter}:/")
|
|
230
|
+
if drive.exists():
|
|
231
|
+
logger.debug(f"Searching drive {drive_letter}:...")
|
|
232
|
+
search_location(drive, max_depth=999)
|
|
233
|
+
if len(matching_files) >= 10:
|
|
234
|
+
break
|
|
235
|
+
else:
|
|
236
|
+
# On Linux/Mac, search from root
|
|
237
|
+
search_location(Path("/"), max_depth=999)
|
|
238
|
+
|
|
239
|
+
# Stop progress indicator
|
|
240
|
+
if hasattr(self, "console") and hasattr(self.console, "stop_progress"):
|
|
241
|
+
self.console.stop_progress()
|
|
242
|
+
|
|
243
|
+
# Return final results
|
|
244
|
+
if matching_files:
|
|
245
|
+
return {
|
|
246
|
+
"status": "success",
|
|
247
|
+
"files": matching_files[:10],
|
|
248
|
+
"file_list": self._format_file_list(matching_files[:10]),
|
|
249
|
+
"count": len(matching_files),
|
|
250
|
+
"total_locations_searched": len(searched_locations),
|
|
251
|
+
"display_message": f"✓ Found {len(matching_files)} file(s) after deep search",
|
|
252
|
+
"user_instruction": "If multiple files found, display numbered list and ask user to select one.",
|
|
253
|
+
}
|
|
254
|
+
else:
|
|
255
|
+
# Build helpful message about what was searched
|
|
256
|
+
search_summary = []
|
|
257
|
+
if str(cwd) in searched_locations:
|
|
258
|
+
search_summary.append(f"current directory ({cwd.name})")
|
|
259
|
+
if len(searched_locations) > 1:
|
|
260
|
+
search_summary.append(
|
|
261
|
+
f"{len(searched_locations)} total locations"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
searched_str = (
|
|
265
|
+
", ".join(search_summary)
|
|
266
|
+
if search_summary
|
|
267
|
+
else f"{len(searched_locations)} locations"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"status": "success",
|
|
272
|
+
"files": [],
|
|
273
|
+
"count": 0,
|
|
274
|
+
"total_locations_searched": len(searched_locations),
|
|
275
|
+
"search_summary": searched_str,
|
|
276
|
+
"display_message": f"❌ No files found matching '{file_pattern}'",
|
|
277
|
+
"searched": f"Searched {searched_str}",
|
|
278
|
+
"suggestion": "Try a different search term, check spelling, or provide the full file path if you know it.",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(f"Error searching for files: {e}")
|
|
283
|
+
import traceback
|
|
284
|
+
|
|
285
|
+
logger.error(traceback.format_exc())
|
|
286
|
+
return {
|
|
287
|
+
"status": "error",
|
|
288
|
+
"error": str(e),
|
|
289
|
+
"has_errors": True,
|
|
290
|
+
"operation": "search_file",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@tool(
|
|
294
|
+
atomic=True,
|
|
295
|
+
name="search_directory",
|
|
296
|
+
description="Search for a directory by name starting from a root path. Use when user asks to find or index 'my data folder' or similar.",
|
|
297
|
+
parameters={
|
|
298
|
+
"directory_name": {
|
|
299
|
+
"type": "str",
|
|
300
|
+
"description": "Name of directory to search for (e.g., 'data', 'documents')",
|
|
301
|
+
"required": True,
|
|
302
|
+
},
|
|
303
|
+
"search_root": {
|
|
304
|
+
"type": "str",
|
|
305
|
+
"description": "Root path to start search from (default: user's home directory)",
|
|
306
|
+
"required": False,
|
|
307
|
+
},
|
|
308
|
+
"max_depth": {
|
|
309
|
+
"type": "int",
|
|
310
|
+
"description": "Maximum depth to search (default: 4)",
|
|
311
|
+
"required": False,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
def search_directory(
|
|
316
|
+
directory_name: str, search_root: str = None, max_depth: int = 4
|
|
317
|
+
) -> Dict[str, Any]:
|
|
318
|
+
"""
|
|
319
|
+
Search for directories by name.
|
|
320
|
+
|
|
321
|
+
Returns list of matching directory paths.
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
# Default to home directory if no root specified
|
|
325
|
+
if search_root is None:
|
|
326
|
+
search_root = str(Path.home())
|
|
327
|
+
|
|
328
|
+
search_root = Path(search_root).resolve()
|
|
329
|
+
|
|
330
|
+
if not search_root.exists():
|
|
331
|
+
return {
|
|
332
|
+
"status": "error",
|
|
333
|
+
"error": f"Search root does not exist: {search_root}",
|
|
334
|
+
"has_errors": True,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
logger.debug(
|
|
338
|
+
f"Searching for directory '{directory_name}' from {search_root}"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
matching_dirs = []
|
|
342
|
+
|
|
343
|
+
def search_recursive(current_path: Path, depth: int):
|
|
344
|
+
"""Recursively search for matching directories."""
|
|
345
|
+
if depth > max_depth:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
for item in current_path.iterdir():
|
|
350
|
+
if item.is_dir():
|
|
351
|
+
# Check if name matches (case-insensitive)
|
|
352
|
+
if directory_name.lower() in item.name.lower():
|
|
353
|
+
matching_dirs.append(str(item.resolve()))
|
|
354
|
+
logger.debug(f"Found matching directory: {item}")
|
|
355
|
+
|
|
356
|
+
# Continue searching subdirectories
|
|
357
|
+
if depth < max_depth:
|
|
358
|
+
search_recursive(item, depth + 1)
|
|
359
|
+
except (PermissionError, OSError) as e:
|
|
360
|
+
# Skip directories we can't access
|
|
361
|
+
logger.debug(f"Skipping {current_path}: {e}")
|
|
362
|
+
|
|
363
|
+
search_recursive(search_root, 0)
|
|
364
|
+
|
|
365
|
+
if matching_dirs:
|
|
366
|
+
return {
|
|
367
|
+
"status": "success",
|
|
368
|
+
"directories": matching_dirs[:10], # Limit to 10 results
|
|
369
|
+
"count": len(matching_dirs),
|
|
370
|
+
"message": f"Found {len(matching_dirs)} matching directories",
|
|
371
|
+
}
|
|
372
|
+
else:
|
|
373
|
+
return {
|
|
374
|
+
"status": "success",
|
|
375
|
+
"directories": [],
|
|
376
|
+
"count": 0,
|
|
377
|
+
"message": f"No directories matching '{directory_name}' found",
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Error searching for directory: {e}")
|
|
382
|
+
return {
|
|
383
|
+
"status": "error",
|
|
384
|
+
"error": str(e),
|
|
385
|
+
"has_errors": True,
|
|
386
|
+
"operation": "search_directory",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@tool(
|
|
390
|
+
atomic=True,
|
|
391
|
+
name="read_file",
|
|
392
|
+
description="Read any file and intelligently analyze based on file type. Supports Python, Markdown, and other text files.",
|
|
393
|
+
parameters={
|
|
394
|
+
"file_path": {
|
|
395
|
+
"type": "str",
|
|
396
|
+
"description": "Path to the file to read",
|
|
397
|
+
"required": True,
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
def read_file(file_path: str) -> Dict[str, Any]:
|
|
402
|
+
"""Read any file and intelligently analyze based on file type.
|
|
403
|
+
|
|
404
|
+
Automatically detects file type and provides appropriate analysis:
|
|
405
|
+
- Python files (.py): Syntax validation + symbol extraction (functions/classes)
|
|
406
|
+
- Markdown files (.md): Headers + code blocks + links
|
|
407
|
+
- Other text files: Raw content
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
file_path: Path to the file to read
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Dictionary with file content and type-specific metadata
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
if not os.path.exists(file_path):
|
|
417
|
+
return {"status": "error", "error": f"File not found: {file_path}"}
|
|
418
|
+
|
|
419
|
+
# Read file content
|
|
420
|
+
try:
|
|
421
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
422
|
+
content = f.read()
|
|
423
|
+
except UnicodeDecodeError:
|
|
424
|
+
# Binary file
|
|
425
|
+
with open(file_path, "rb") as f:
|
|
426
|
+
content_bytes = f.read()
|
|
427
|
+
return {
|
|
428
|
+
"status": "success",
|
|
429
|
+
"file_path": file_path,
|
|
430
|
+
"file_type": "binary",
|
|
431
|
+
"content": f"[Binary file, {len(content_bytes)} bytes]",
|
|
432
|
+
"is_binary": True,
|
|
433
|
+
"size_bytes": len(content_bytes),
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Detect file type by extension
|
|
437
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
438
|
+
|
|
439
|
+
# Base result with common fields
|
|
440
|
+
result = {
|
|
441
|
+
"status": "success",
|
|
442
|
+
"file_path": file_path,
|
|
443
|
+
"content": content,
|
|
444
|
+
"line_count": len(content.splitlines()),
|
|
445
|
+
"size_bytes": len(content.encode("utf-8")),
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Python file - add symbol extraction
|
|
449
|
+
if ext == ".py":
|
|
450
|
+
result["file_type"] = "python"
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
tree = ast.parse(content)
|
|
454
|
+
result["is_valid"] = True
|
|
455
|
+
result["errors"] = []
|
|
456
|
+
|
|
457
|
+
# Extract symbols
|
|
458
|
+
symbols = []
|
|
459
|
+
for node in ast.walk(tree):
|
|
460
|
+
if isinstance(
|
|
461
|
+
node, (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
462
|
+
):
|
|
463
|
+
symbols.append(
|
|
464
|
+
{
|
|
465
|
+
"name": node.name,
|
|
466
|
+
"type": "function",
|
|
467
|
+
"line": node.lineno,
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
elif isinstance(node, ast.ClassDef):
|
|
471
|
+
symbols.append(
|
|
472
|
+
{
|
|
473
|
+
"name": node.name,
|
|
474
|
+
"type": "class",
|
|
475
|
+
"line": node.lineno,
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
result["symbols"] = symbols
|
|
479
|
+
except SyntaxError as e:
|
|
480
|
+
result["is_valid"] = False
|
|
481
|
+
result["errors"] = [str(e)]
|
|
482
|
+
|
|
483
|
+
# Markdown file - extract structure
|
|
484
|
+
elif ext == ".md":
|
|
485
|
+
import re
|
|
486
|
+
|
|
487
|
+
result["file_type"] = "markdown"
|
|
488
|
+
|
|
489
|
+
# Extract headers
|
|
490
|
+
headers = re.findall(r"^#{1,6}\s+(.+)$", content, re.MULTILINE)
|
|
491
|
+
result["headers"] = headers
|
|
492
|
+
|
|
493
|
+
# Extract code blocks
|
|
494
|
+
code_blocks = re.findall(r"```(\w*)\n(.*?)```", content, re.DOTALL)
|
|
495
|
+
result["code_blocks"] = [
|
|
496
|
+
{"language": lang, "code": code} for lang, code in code_blocks
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
# Extract links
|
|
500
|
+
links = re.findall(r"\[([^\]]+)\]\(([^)]+)\)", content)
|
|
501
|
+
result["links"] = [
|
|
502
|
+
{"text": text, "url": url} for text, url in links
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
# Other text files
|
|
506
|
+
else:
|
|
507
|
+
result["file_type"] = ext[1:] if ext else "text"
|
|
508
|
+
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
return {"status": "error", "error": str(e)}
|
|
513
|
+
|
|
514
|
+
@tool(
|
|
515
|
+
atomic=True,
|
|
516
|
+
name="search_file_content",
|
|
517
|
+
description="Search for text patterns within files on disk (like grep). Searches actual file contents, not indexed documents.",
|
|
518
|
+
parameters={
|
|
519
|
+
"pattern": {
|
|
520
|
+
"type": "str",
|
|
521
|
+
"description": "Text pattern or keyword to search for",
|
|
522
|
+
"required": True,
|
|
523
|
+
},
|
|
524
|
+
"directory": {
|
|
525
|
+
"type": "str",
|
|
526
|
+
"description": "Directory to search in (default: current directory)",
|
|
527
|
+
"required": False,
|
|
528
|
+
},
|
|
529
|
+
"file_pattern": {
|
|
530
|
+
"type": "str",
|
|
531
|
+
"description": "File pattern to filter (e.g., '*.py', '*.txt'). Default: all text files",
|
|
532
|
+
"required": False,
|
|
533
|
+
},
|
|
534
|
+
"case_sensitive": {
|
|
535
|
+
"type": "bool",
|
|
536
|
+
"description": "Whether search should be case-sensitive (default: False)",
|
|
537
|
+
"required": False,
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
def search_file_content(
|
|
542
|
+
pattern: str,
|
|
543
|
+
directory: str = ".",
|
|
544
|
+
file_pattern: str = None,
|
|
545
|
+
case_sensitive: bool = False,
|
|
546
|
+
) -> Dict[str, Any]:
|
|
547
|
+
"""
|
|
548
|
+
Search for text patterns within files (grep-like functionality).
|
|
549
|
+
|
|
550
|
+
Searches actual file contents on disk, not RAG indexed documents.
|
|
551
|
+
"""
|
|
552
|
+
try:
|
|
553
|
+
import fnmatch
|
|
554
|
+
|
|
555
|
+
directory = Path(directory).resolve()
|
|
556
|
+
|
|
557
|
+
if not directory.exists():
|
|
558
|
+
return {
|
|
559
|
+
"status": "error",
|
|
560
|
+
"error": f"Directory not found: {directory}",
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# Text file extensions to search
|
|
564
|
+
text_extensions = {
|
|
565
|
+
".txt",
|
|
566
|
+
".md",
|
|
567
|
+
".py",
|
|
568
|
+
".js",
|
|
569
|
+
".java",
|
|
570
|
+
".c",
|
|
571
|
+
".cpp",
|
|
572
|
+
".h",
|
|
573
|
+
".json",
|
|
574
|
+
".xml",
|
|
575
|
+
".yaml",
|
|
576
|
+
".yml",
|
|
577
|
+
".csv",
|
|
578
|
+
".log",
|
|
579
|
+
".ini",
|
|
580
|
+
".conf",
|
|
581
|
+
".sh",
|
|
582
|
+
".bat",
|
|
583
|
+
".html",
|
|
584
|
+
".css",
|
|
585
|
+
".sql",
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
matches = []
|
|
589
|
+
files_searched = 0
|
|
590
|
+
search_pattern = pattern if case_sensitive else pattern.lower()
|
|
591
|
+
|
|
592
|
+
def search_file(file_path: Path):
|
|
593
|
+
"""Search within a single file."""
|
|
594
|
+
try:
|
|
595
|
+
with open(
|
|
596
|
+
file_path, "r", encoding="utf-8", errors="ignore"
|
|
597
|
+
) as f:
|
|
598
|
+
for line_num, line in enumerate(f, 1):
|
|
599
|
+
search_line = line if case_sensitive else line.lower()
|
|
600
|
+
if search_pattern in search_line:
|
|
601
|
+
matches.append(
|
|
602
|
+
{
|
|
603
|
+
"file": str(file_path),
|
|
604
|
+
"line": line_num,
|
|
605
|
+
"content": line.strip()[
|
|
606
|
+
:200
|
|
607
|
+
], # Limit line length
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
if len(matches) >= 100: # Limit total matches
|
|
611
|
+
return False
|
|
612
|
+
return True
|
|
613
|
+
except Exception:
|
|
614
|
+
return True # Continue searching
|
|
615
|
+
|
|
616
|
+
# Search files
|
|
617
|
+
for file_path in directory.rglob("*"):
|
|
618
|
+
if not file_path.is_file():
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
# Filter by file pattern if provided
|
|
622
|
+
if file_pattern:
|
|
623
|
+
if not fnmatch.fnmatch(file_path.name, file_pattern):
|
|
624
|
+
continue
|
|
625
|
+
else:
|
|
626
|
+
# Only search text files
|
|
627
|
+
if file_path.suffix.lower() not in text_extensions:
|
|
628
|
+
continue
|
|
629
|
+
|
|
630
|
+
files_searched += 1
|
|
631
|
+
if not search_file(file_path):
|
|
632
|
+
break # Hit match limit
|
|
633
|
+
|
|
634
|
+
if matches:
|
|
635
|
+
return {
|
|
636
|
+
"status": "success",
|
|
637
|
+
"pattern": pattern,
|
|
638
|
+
"matches": matches[:50], # Return first 50
|
|
639
|
+
"total_matches": len(matches),
|
|
640
|
+
"files_searched": files_searched,
|
|
641
|
+
"message": f"Found {len(matches)} matches in {files_searched} files",
|
|
642
|
+
}
|
|
643
|
+
else:
|
|
644
|
+
return {
|
|
645
|
+
"status": "success",
|
|
646
|
+
"pattern": pattern,
|
|
647
|
+
"matches": [],
|
|
648
|
+
"total_matches": 0,
|
|
649
|
+
"files_searched": files_searched,
|
|
650
|
+
"message": f"No matches found for '{pattern}' in {files_searched} files",
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
except Exception as e:
|
|
654
|
+
logger.error(f"Error searching file content: {e}")
|
|
655
|
+
return {
|
|
656
|
+
"status": "error",
|
|
657
|
+
"error": str(e),
|
|
658
|
+
"has_errors": True,
|
|
659
|
+
"operation": "search_file_content",
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
@tool(
|
|
663
|
+
atomic=True,
|
|
664
|
+
name="write_file",
|
|
665
|
+
description="Write content to any file. Creates parent directories if needed.",
|
|
666
|
+
parameters={
|
|
667
|
+
"file_path": {
|
|
668
|
+
"type": "str",
|
|
669
|
+
"description": "Path where to write the file",
|
|
670
|
+
"required": True,
|
|
671
|
+
},
|
|
672
|
+
"content": {
|
|
673
|
+
"type": "str",
|
|
674
|
+
"description": "Content to write to the file",
|
|
675
|
+
"required": True,
|
|
676
|
+
},
|
|
677
|
+
"create_dirs": {
|
|
678
|
+
"type": "bool",
|
|
679
|
+
"description": "Whether to create parent directories (default: True)",
|
|
680
|
+
"required": False,
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
def write_file(
|
|
685
|
+
file_path: str, content: str, create_dirs: bool = True
|
|
686
|
+
) -> Dict[str, Any]:
|
|
687
|
+
"""
|
|
688
|
+
Write content to a file.
|
|
689
|
+
|
|
690
|
+
Generic file writer for any file type.
|
|
691
|
+
"""
|
|
692
|
+
try:
|
|
693
|
+
file_path = Path(file_path)
|
|
694
|
+
|
|
695
|
+
# Create parent directories if needed
|
|
696
|
+
if create_dirs and file_path.parent:
|
|
697
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
698
|
+
|
|
699
|
+
# Write the file
|
|
700
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
701
|
+
f.write(content)
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
"status": "success",
|
|
705
|
+
"file_path": str(file_path),
|
|
706
|
+
"bytes_written": len(content.encode("utf-8")),
|
|
707
|
+
"line_count": len(content.splitlines()),
|
|
708
|
+
}
|
|
709
|
+
except Exception as e:
|
|
710
|
+
logger.error(f"Error writing file: {e}")
|
|
711
|
+
return {
|
|
712
|
+
"status": "error",
|
|
713
|
+
"error": str(e),
|
|
714
|
+
"operation": "write_file",
|
|
715
|
+
}
|