sky-dev 0.0.5__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.
- sky/__init__.py +6 -0
- sky/cli.py +833 -0
- sky/config/__init__.py +19 -0
- sky/config/models.yaml +39 -0
- sky/config/schema.py +275 -0
- sky/core/__init__.py +48 -0
- sky/core/approval.py +92 -0
- sky/core/benchmark.py +39 -0
- sky/core/chat.py +211 -0
- sky/core/fast_loop.py +330 -0
- sky/core/mode_prompts.py +106 -0
- sky/core/router.py +264 -0
- sky/core/subagent.py +302 -0
- sky/core/workflow.py +662 -0
- sky/errors.py +29 -0
- sky/memory/__init__.py +27 -0
- sky/memory/indexer.py +450 -0
- sky/memory/vectorstore.py +271 -0
- sky/security/__init__.py +17 -0
- sky/security/audit.py +23 -0
- sky/security/detection.py +94 -0
- sky/security/guardrails.py +106 -0
- sky/security/prompts.py +58 -0
- sky/security/rate_limit.py +36 -0
- sky/security/sanitize.py +152 -0
- sky/storage/__init__.py +21 -0
- sky/storage/db.py +492 -0
- sky/tools/__init__.py +26 -0
- sky/tools/fs_tools.py +139 -0
- sky/tools/git_tools.py +139 -0
- sky/tools/registry.py +219 -0
- sky/tools/search_tools.py +128 -0
- sky/tools/shell_tools.py +73 -0
- sky_dev-0.0.5.dist-info/METADATA +119 -0
- sky_dev-0.0.5.dist-info/RECORD +39 -0
- sky_dev-0.0.5.dist-info/WHEEL +5 -0
- sky_dev-0.0.5.dist-info/entry_points.txt +2 -0
- sky_dev-0.0.5.dist-info/licenses/LICENSE +21 -0
- sky_dev-0.0.5.dist-info/top_level.txt +1 -0
sky/memory/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Project Context Memory Module."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from sky.memory.indexer import RepoIndexer
|
|
7
|
+
from sky.memory.vectorstore import VectorStoreManager
|
|
8
|
+
|
|
9
|
+
def get_vector_store(*args, **kwargs):
|
|
10
|
+
from sky.memory.vectorstore import get_vector_store as _get_vector_store
|
|
11
|
+
return _get_vector_store(*args, **kwargs)
|
|
12
|
+
|
|
13
|
+
def get_indexer(*args, **kwargs):
|
|
14
|
+
from sky.memory.indexer import get_indexer as _get_indexer
|
|
15
|
+
return _get_indexer(*args, **kwargs)
|
|
16
|
+
|
|
17
|
+
def search_context(*args, **kwargs):
|
|
18
|
+
from sky.memory.indexer import search_context as _search_context
|
|
19
|
+
return _search_context(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"VectorStoreManager",
|
|
23
|
+
"RepoIndexer",
|
|
24
|
+
"get_vector_store",
|
|
25
|
+
"get_indexer",
|
|
26
|
+
"search_context",
|
|
27
|
+
]
|
sky/memory/indexer.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Repository Indexer for Semantic Code Search."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from sky.config import DexProjectConfig
|
|
11
|
+
from sky.memory.vectorstore import VectorStoreManager
|
|
12
|
+
from sky.storage import DatabaseManager
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class RepoIndexer:
|
|
17
|
+
"""Handles incremental parsing and indexing of a repository."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: DexProjectConfig, db: DatabaseManager, vector_store: VectorStoreManager) -> None:
|
|
20
|
+
"""Initialize the repository indexer.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: The active project configuration.
|
|
24
|
+
db: Database manager instance for metadata.
|
|
25
|
+
vector_store: LanceDB vector store manager.
|
|
26
|
+
"""
|
|
27
|
+
self.config = config
|
|
28
|
+
self.db = db
|
|
29
|
+
self.vector_store = vector_store
|
|
30
|
+
|
|
31
|
+
# Determine excluded directories
|
|
32
|
+
# We will use config.memory_exclude_patterns, fallback to defaults if not present
|
|
33
|
+
if hasattr(self.config, "memory_exclude_patterns"):
|
|
34
|
+
self.excluded_patterns = self.config.memory_exclude_patterns
|
|
35
|
+
else:
|
|
36
|
+
self.excluded_patterns = [".git", "node_modules", "venv", "__pycache__", ".sky", ".pytest_cache", "*.pyc"]
|
|
37
|
+
|
|
38
|
+
def _get_file_hash(self, file_path: Path) -> str:
|
|
39
|
+
"""Compute SHA-256 hash of a file's content.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
file_path: Path to the file.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
SHA-256 hex digest.
|
|
46
|
+
"""
|
|
47
|
+
sha256 = hashlib.sha256()
|
|
48
|
+
try:
|
|
49
|
+
with open(file_path, "rb") as f:
|
|
50
|
+
# Read in chunks to handle large files
|
|
51
|
+
while chunk := f.read(8192):
|
|
52
|
+
sha256.update(chunk)
|
|
53
|
+
return sha256.hexdigest()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.warning(f"Failed to hash {file_path}: {e}")
|
|
56
|
+
return ""
|
|
57
|
+
|
|
58
|
+
def _is_binary_file(self, file_path: Path) -> bool:
|
|
59
|
+
"""Check if a file is binary by looking for null bytes in the first 8KB.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
file_path: Path to the file.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if the file appears to be binary, False otherwise.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
with open(file_path, "rb") as f:
|
|
69
|
+
chunk = f.read(8192)
|
|
70
|
+
return b"\x00" in chunk
|
|
71
|
+
except Exception:
|
|
72
|
+
return True # Safe fallback
|
|
73
|
+
|
|
74
|
+
def _chunk_python(self, content: str) -> List[Dict[str, str]]:
|
|
75
|
+
"""AST-based chunking for Python files.
|
|
76
|
+
|
|
77
|
+
Extracts functions, classes, and module-level docstrings as distinct chunks.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
content: Python source code.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of chunks with their type.
|
|
84
|
+
"""
|
|
85
|
+
chunks = []
|
|
86
|
+
try:
|
|
87
|
+
tree = ast.parse(content)
|
|
88
|
+
|
|
89
|
+
# Extract module docstring
|
|
90
|
+
module_doc = ast.get_docstring(tree)
|
|
91
|
+
if module_doc:
|
|
92
|
+
chunks.append({
|
|
93
|
+
"content": f'"""\n{module_doc}\n"""',
|
|
94
|
+
"language": "python",
|
|
95
|
+
"type": "docstring"
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
lines = content.splitlines()
|
|
99
|
+
|
|
100
|
+
for node in tree.body:
|
|
101
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
102
|
+
start_line = node.lineno - 1
|
|
103
|
+
end_line = getattr(node, "end_lineno", len(lines))
|
|
104
|
+
func_content = "\n".join(lines[start_line:end_line])
|
|
105
|
+
chunks.append({
|
|
106
|
+
"content": func_content,
|
|
107
|
+
"language": "python",
|
|
108
|
+
"type": "function"
|
|
109
|
+
})
|
|
110
|
+
elif isinstance(node, ast.ClassDef):
|
|
111
|
+
start_line = node.lineno - 1
|
|
112
|
+
end_line = getattr(node, "end_lineno", len(lines))
|
|
113
|
+
class_content = "\n".join(lines[start_line:end_line])
|
|
114
|
+
chunks.append({
|
|
115
|
+
"content": class_content,
|
|
116
|
+
"language": "python",
|
|
117
|
+
"type": "class"
|
|
118
|
+
})
|
|
119
|
+
elif isinstance(node, ast.Assign):
|
|
120
|
+
start_line = node.lineno - 1
|
|
121
|
+
end_line = getattr(node, "end_lineno", start_line + 1)
|
|
122
|
+
assign_content = "\n".join(lines[start_line:end_line])
|
|
123
|
+
chunks.append({
|
|
124
|
+
"content": assign_content,
|
|
125
|
+
"language": "python",
|
|
126
|
+
"type": "assignment"
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if not chunks:
|
|
130
|
+
# If AST parsing succeeds but finds no structure, fallback
|
|
131
|
+
return self._chunk_by_lines(content, max_lines=50, overlap=10)
|
|
132
|
+
|
|
133
|
+
return chunks
|
|
134
|
+
except SyntaxError:
|
|
135
|
+
# Fallback to line-based chunking if AST parsing fails
|
|
136
|
+
return self._chunk_by_lines(content, max_lines=50, overlap=10)
|
|
137
|
+
|
|
138
|
+
def _chunk_by_lines(self, content: str, max_lines: int = 50, overlap: int = 10) -> List[Dict[str, str]]:
|
|
139
|
+
"""Chunk text content by a fixed number of lines with overlap.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
content: Text content to chunk.
|
|
143
|
+
max_lines: Maximum lines per chunk.
|
|
144
|
+
overlap: Number of lines to overlap between chunks.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of chunk dictionaries.
|
|
148
|
+
"""
|
|
149
|
+
lines = content.splitlines()
|
|
150
|
+
chunks = []
|
|
151
|
+
|
|
152
|
+
if not lines:
|
|
153
|
+
return chunks
|
|
154
|
+
|
|
155
|
+
i = 0
|
|
156
|
+
while i < len(lines):
|
|
157
|
+
end = min(i + max_lines, len(lines))
|
|
158
|
+
chunk_content = "\n".join(lines[i:end])
|
|
159
|
+
chunks.append({
|
|
160
|
+
"content": chunk_content,
|
|
161
|
+
"type": "lines"
|
|
162
|
+
})
|
|
163
|
+
if end == len(lines):
|
|
164
|
+
break
|
|
165
|
+
i += (max_lines - overlap)
|
|
166
|
+
|
|
167
|
+
return chunks
|
|
168
|
+
|
|
169
|
+
def _chunk_by_file_type(self, file_path: Path, content: str) -> List[Dict[str, str]]:
|
|
170
|
+
"""Route to appropriate chunker based on file extension."""
|
|
171
|
+
ext = file_path.suffix.lower()
|
|
172
|
+
|
|
173
|
+
if ext == ".py":
|
|
174
|
+
return self._chunk_python(content)
|
|
175
|
+
elif ext in (".md", ".txt"):
|
|
176
|
+
return self._chunk_by_lines(content, max_lines=30, overlap=5)
|
|
177
|
+
elif ext in (".js", ".ts", ".jsx", ".tsx"):
|
|
178
|
+
return self._chunk_by_lines(content, max_lines=40, overlap=10)
|
|
179
|
+
else:
|
|
180
|
+
return self._chunk_by_lines(content, max_lines=50, overlap=10)
|
|
181
|
+
|
|
182
|
+
def _get_language_for_file(self, file_path: Path) -> str:
|
|
183
|
+
"""Return a language hint based on file extension."""
|
|
184
|
+
ext = file_path.suffix.lower()
|
|
185
|
+
lang_map = {
|
|
186
|
+
".py": "python",
|
|
187
|
+
".js": "javascript",
|
|
188
|
+
".ts": "typescript",
|
|
189
|
+
".jsx": "jsx",
|
|
190
|
+
".tsx": "tsx",
|
|
191
|
+
".md": "markdown",
|
|
192
|
+
".html": "html",
|
|
193
|
+
".css": "css",
|
|
194
|
+
".json": "json",
|
|
195
|
+
".yml": "yaml",
|
|
196
|
+
".yaml": "yaml",
|
|
197
|
+
".sh": "bash",
|
|
198
|
+
".rs": "rust",
|
|
199
|
+
".go": "go",
|
|
200
|
+
".java": "java",
|
|
201
|
+
".cpp": "cpp",
|
|
202
|
+
".c": "c",
|
|
203
|
+
".h": "c",
|
|
204
|
+
".hpp": "cpp",
|
|
205
|
+
}
|
|
206
|
+
return lang_map.get(ext, "text")
|
|
207
|
+
|
|
208
|
+
def _should_exclude(self, file_path: Path) -> bool:
|
|
209
|
+
"""Check if a file or directory should be excluded from indexing."""
|
|
210
|
+
import fnmatch
|
|
211
|
+
|
|
212
|
+
# Check against configured patterns
|
|
213
|
+
path_str = str(file_path.relative_to(Path.cwd()) if file_path.is_absolute() else file_path).replace("\\", "/")
|
|
214
|
+
parts = path_str.split("/")
|
|
215
|
+
|
|
216
|
+
for pattern in self.excluded_patterns:
|
|
217
|
+
# Check full path
|
|
218
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
219
|
+
return True
|
|
220
|
+
# Check individual parts (e.g. 'node_modules' pattern matches 'src/node_modules/index.js')
|
|
221
|
+
for part in parts:
|
|
222
|
+
if fnmatch.fnmatch(part, pattern):
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def index_file(self, file_path: Path) -> bool:
|
|
228
|
+
"""Index a single file incrementally.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
file_path: Path to the file to index.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if the file was (re)indexed, False if unchanged or skipped.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
if not file_path.exists() or not file_path.is_file():
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
if self._should_exclude(file_path):
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
if self._is_binary_file(file_path):
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
# Calculate hash
|
|
247
|
+
content_hash = self._get_file_hash(file_path)
|
|
248
|
+
if not content_hash:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
rel_path = str(file_path.relative_to(Path.cwd()) if file_path.is_absolute() else file_path).replace("\\", "/")
|
|
252
|
+
|
|
253
|
+
# Check existing index entry
|
|
254
|
+
existing_entry = self.db.get_index_entry(rel_path)
|
|
255
|
+
if existing_entry and existing_entry.content_hash == content_hash:
|
|
256
|
+
# File is unchanged
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# Read content
|
|
260
|
+
try:
|
|
261
|
+
content = file_path.read_text(encoding="utf-8")
|
|
262
|
+
except UnicodeDecodeError:
|
|
263
|
+
# Attempt with fallback
|
|
264
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
265
|
+
|
|
266
|
+
# Remove old chunks if this is an update
|
|
267
|
+
if existing_entry:
|
|
268
|
+
self.vector_store.delete_chunks(rel_path)
|
|
269
|
+
|
|
270
|
+
# Chunk content
|
|
271
|
+
chunks = self._chunk_by_file_type(file_path, content)
|
|
272
|
+
language = self._get_language_for_file(file_path)
|
|
273
|
+
|
|
274
|
+
import datetime
|
|
275
|
+
last_modified = datetime.datetime.fromtimestamp(file_path.stat().st_mtime).isoformat()
|
|
276
|
+
|
|
277
|
+
# Insert new chunks
|
|
278
|
+
for idx, chunk in enumerate(chunks):
|
|
279
|
+
self.vector_store.add_chunk(
|
|
280
|
+
file_path=rel_path,
|
|
281
|
+
chunk_index=idx,
|
|
282
|
+
content=chunk["content"],
|
|
283
|
+
content_hash=content_hash,
|
|
284
|
+
last_modified=last_modified,
|
|
285
|
+
language=chunk.get("language", language)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Update database record
|
|
289
|
+
# We assume db.update_index_entry has been updated to accept language, chunks, last_modified in Phase 3
|
|
290
|
+
try:
|
|
291
|
+
self.db.update_index_entry(
|
|
292
|
+
file_path=rel_path,
|
|
293
|
+
content_hash=content_hash,
|
|
294
|
+
summary=f"Indexed {len(chunks)} chunks",
|
|
295
|
+
language=language,
|
|
296
|
+
chunks=len(chunks),
|
|
297
|
+
last_modified=last_modified
|
|
298
|
+
)
|
|
299
|
+
except TypeError:
|
|
300
|
+
# Fallback for old schema
|
|
301
|
+
self.db.update_index_entry(
|
|
302
|
+
file_path=rel_path,
|
|
303
|
+
content_hash=content_hash,
|
|
304
|
+
summary=f"Indexed {len(chunks)} chunks",
|
|
305
|
+
embedding_id="batch"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.warning(f"Failed to index {file_path}: {e}")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def index_directory(self, directory: Path, recursive: bool = True, progress_callback: Optional[Callable[[str], None]] = None) -> Dict[str, int]:
|
|
315
|
+
"""Index all files in a directory.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
directory: Directory to index.
|
|
319
|
+
recursive: Whether to index subdirectories.
|
|
320
|
+
progress_callback: Optional function to call with status updates.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Dictionary with indexing statistics.
|
|
324
|
+
"""
|
|
325
|
+
stats = {"indexed": 0, "skipped": 0, "errors": 0, "total": 0}
|
|
326
|
+
|
|
327
|
+
def walk_dir(current_dir: Path) -> None:
|
|
328
|
+
if self._should_exclude(current_dir):
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
for item in current_dir.iterdir():
|
|
333
|
+
if item.is_dir():
|
|
334
|
+
if recursive and not self._should_exclude(item):
|
|
335
|
+
walk_dir(item)
|
|
336
|
+
elif item.is_file():
|
|
337
|
+
stats["total"] += 1
|
|
338
|
+
if progress_callback:
|
|
339
|
+
progress_callback(f"Checking {item.name}")
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
if self.index_file(item):
|
|
343
|
+
stats["indexed"] += 1
|
|
344
|
+
else:
|
|
345
|
+
stats["skipped"] += 1
|
|
346
|
+
except Exception:
|
|
347
|
+
stats["errors"] += 1
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.error(f"Error traversing directory {current_dir}: {e}")
|
|
350
|
+
|
|
351
|
+
walk_dir(directory)
|
|
352
|
+
|
|
353
|
+
# Cleanup deleted files
|
|
354
|
+
pruned = self.prune_deleted_files()
|
|
355
|
+
stats["pruned"] = pruned
|
|
356
|
+
|
|
357
|
+
return stats
|
|
358
|
+
|
|
359
|
+
def get_context(self, query: str, top_k: int = 5, file_pattern: Optional[str] = None) -> str:
|
|
360
|
+
"""Retrieve relevant context for a query from the vector store.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
query: The user's query or intent.
|
|
364
|
+
top_k: Number of chunks to retrieve.
|
|
365
|
+
file_pattern: Optional file path to filter.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Formatted markdown string containing the context snippets.
|
|
369
|
+
"""
|
|
370
|
+
results = self.vector_store.search(query, top_k=top_k, file_filter=file_pattern)
|
|
371
|
+
|
|
372
|
+
if not results:
|
|
373
|
+
return ""
|
|
374
|
+
|
|
375
|
+
context_blocks = []
|
|
376
|
+
for res in results:
|
|
377
|
+
path = res["file_path"]
|
|
378
|
+
lang = res.get("language", "text")
|
|
379
|
+
content = res["content"]
|
|
380
|
+
score = res.get("score", 0.0)
|
|
381
|
+
|
|
382
|
+
block = f"### File: {path} (Relevance: {score:.2f})\n```{lang}\n{content}\n```"
|
|
383
|
+
context_blocks.append(block)
|
|
384
|
+
|
|
385
|
+
return "\n\n".join(context_blocks)
|
|
386
|
+
|
|
387
|
+
def get_file_summary(self, file_path: str) -> Optional[str]:
|
|
388
|
+
"""Get the summary of a file from the index."""
|
|
389
|
+
entry = self.db.get_index_entry(file_path)
|
|
390
|
+
return entry.summary if entry else None
|
|
391
|
+
|
|
392
|
+
def prune_deleted_files(self) -> int:
|
|
393
|
+
"""Remove entries from the index and vector store for files that no longer exist.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Number of files removed.
|
|
397
|
+
"""
|
|
398
|
+
indexed_files = []
|
|
399
|
+
# We need list_indexed_files from the db
|
|
400
|
+
if hasattr(self.db, "list_indexed_files"):
|
|
401
|
+
indexed_files = self.db.list_indexed_files()
|
|
402
|
+
elif hasattr(self.db, "get_indexed_files"):
|
|
403
|
+
indexed_files = self.db.get_indexed_files()
|
|
404
|
+
|
|
405
|
+
removed = 0
|
|
406
|
+
for rel_path in indexed_files:
|
|
407
|
+
full_path = Path.cwd() / rel_path
|
|
408
|
+
if not full_path.exists():
|
|
409
|
+
# File deleted
|
|
410
|
+
self.vector_store.delete_chunks(rel_path)
|
|
411
|
+
self.db.remove_index_entry(rel_path)
|
|
412
|
+
removed += 1
|
|
413
|
+
|
|
414
|
+
return removed
|
|
415
|
+
|
|
416
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
417
|
+
"""Get combined statistics from database and vector store."""
|
|
418
|
+
vs_stats = self.vector_store.get_stats()
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
indexed_files = self.db.get_indexed_files()
|
|
422
|
+
total_files = len(indexed_files)
|
|
423
|
+
except Exception:
|
|
424
|
+
total_files = 0
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
"total_files": total_files,
|
|
428
|
+
"total_chunks": vs_stats.get("total_chunks", 0),
|
|
429
|
+
"excluded_dirs": self.excluded_patterns,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
def close(self) -> None:
|
|
433
|
+
"""Close vector store."""
|
|
434
|
+
self.vector_store.close()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
_global_indexer: Optional[RepoIndexer] = None
|
|
438
|
+
|
|
439
|
+
def get_indexer(config: DexProjectConfig, db: DatabaseManager, vector_store: VectorStoreManager) -> RepoIndexer:
|
|
440
|
+
"""Get global indexer instance."""
|
|
441
|
+
global _global_indexer
|
|
442
|
+
if _global_indexer is None:
|
|
443
|
+
_global_indexer = RepoIndexer(config, db, vector_store)
|
|
444
|
+
return _global_indexer
|
|
445
|
+
|
|
446
|
+
def search_context(query: str, top_k: int = 5) -> str:
|
|
447
|
+
"""Search context using the global indexer."""
|
|
448
|
+
if _global_indexer:
|
|
449
|
+
return _global_indexer.get_context(query, top_k)
|
|
450
|
+
return ""
|