docugraph 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.
- docugraph/__init__.py +24 -0
- docugraph/_version.py +28 -0
- docugraph/agents/__init__.py +27 -0
- docugraph/agents/coding_agent.py +532 -0
- docugraph/agents/tools/__init__.py +62 -0
- docugraph/agents/tools/graph_query.py +203 -0
- docugraph/agents/tools/memory_ops.py +305 -0
- docugraph/agents/tools/search_code.py +305 -0
- docugraph/agents/tools/search_docs.py +148 -0
- docugraph/core/__init__.py +34 -0
- docugraph/core/config.py +183 -0
- docugraph/core/embeddings.py +394 -0
- docugraph/core/llm.py +892 -0
- docugraph/core/models.py +145 -0
- docugraph/ingestion/__init__.py +64 -0
- docugraph/ingestion/api_docs.py +646 -0
- docugraph/ingestion/chunker.py +382 -0
- docugraph/ingestion/crawler.py +305 -0
- docugraph/ingestion/git_indexer.py +556 -0
- docugraph/ingestion/local_files.py +516 -0
- docugraph/interfaces/__init__.py +1 -0
- docugraph/interfaces/cli.py +983 -0
- docugraph/interfaces/mcp_server.py +918 -0
- docugraph/retrieval/__init__.py +54 -0
- docugraph/retrieval/context_builder.py +522 -0
- docugraph/retrieval/hybrid_search.py +534 -0
- docugraph/retrieval/reranker.py +532 -0
- docugraph/storage/__init__.py +14 -0
- docugraph/storage/graph_store.py +602 -0
- docugraph/storage/memory_store.py +529 -0
- docugraph/storage/vector_store.py +367 -0
- docugraph-0.1.0.dist-info/METADATA +354 -0
- docugraph-0.1.0.dist-info/RECORD +37 -0
- docugraph-0.1.0.dist-info/WHEEL +4 -0
- docugraph-0.1.0.dist-info/entry_points.txt +3 -0
- docugraph-0.1.0.dist-info/licenses/LICENSE +201 -0
- docugraph-0.1.0.dist-info/licenses/NOTICE +10 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""Local file indexing for Markdown, RST, and text documentation.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Markdown (.md, .mdx)
|
|
5
|
+
- reStructuredText (.rst)
|
|
6
|
+
- Plain text (.txt)
|
|
7
|
+
- Code files with docstrings (optional)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import fnmatch
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from enum import StrEnum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from docugraph.core.models import Document
|
|
23
|
+
from docugraph.ingestion.chunker import Chunker, ChunkerConfig
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FileType(StrEnum):
|
|
27
|
+
"""Supported file types."""
|
|
28
|
+
|
|
29
|
+
MARKDOWN = "markdown"
|
|
30
|
+
RST = "rst"
|
|
31
|
+
TEXT = "text"
|
|
32
|
+
CODE = "code"
|
|
33
|
+
UNKNOWN = "unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class LocalFileConfig:
|
|
38
|
+
"""Configuration for local file indexing."""
|
|
39
|
+
|
|
40
|
+
# File patterns to include
|
|
41
|
+
include_patterns: list[str] = field(default_factory=lambda: ["*.md", "*.mdx", "*.rst", "*.txt"])
|
|
42
|
+
|
|
43
|
+
# File patterns to exclude
|
|
44
|
+
exclude_patterns: list[str] = field(
|
|
45
|
+
default_factory=lambda: [
|
|
46
|
+
"*.min.js",
|
|
47
|
+
"*.min.css",
|
|
48
|
+
"node_modules/*",
|
|
49
|
+
".git/*",
|
|
50
|
+
"__pycache__/*",
|
|
51
|
+
"*.pyc",
|
|
52
|
+
".venv/*",
|
|
53
|
+
"venv/*",
|
|
54
|
+
"dist/*",
|
|
55
|
+
"build/*",
|
|
56
|
+
".tox/*",
|
|
57
|
+
".pytest_cache/*",
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Directories to skip entirely
|
|
62
|
+
skip_dirs: list[str] = field(
|
|
63
|
+
default_factory=lambda: [
|
|
64
|
+
".git",
|
|
65
|
+
"node_modules",
|
|
66
|
+
"__pycache__",
|
|
67
|
+
".venv",
|
|
68
|
+
"venv",
|
|
69
|
+
".tox",
|
|
70
|
+
".pytest_cache",
|
|
71
|
+
"dist",
|
|
72
|
+
"build",
|
|
73
|
+
".eggs",
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Maximum file size in bytes (default 1MB)
|
|
78
|
+
max_file_size: int = 1024 * 1024
|
|
79
|
+
|
|
80
|
+
# Whether to follow symlinks
|
|
81
|
+
follow_symlinks: bool = False
|
|
82
|
+
|
|
83
|
+
# Extract title from content
|
|
84
|
+
extract_title: bool = True
|
|
85
|
+
|
|
86
|
+
# Include hidden files (starting with .)
|
|
87
|
+
include_hidden: bool = False
|
|
88
|
+
|
|
89
|
+
# Encoding to use for reading files
|
|
90
|
+
encoding: str = "utf-8"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class IndexedFile:
|
|
95
|
+
"""Represents an indexed file with metadata."""
|
|
96
|
+
|
|
97
|
+
path: Path
|
|
98
|
+
relative_path: str
|
|
99
|
+
file_type: FileType
|
|
100
|
+
content: str
|
|
101
|
+
title: str | None
|
|
102
|
+
size: int
|
|
103
|
+
modified_at: datetime
|
|
104
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
105
|
+
|
|
106
|
+
def to_document(self) -> Document:
|
|
107
|
+
"""Convert to Document model."""
|
|
108
|
+
return Document(
|
|
109
|
+
source_path=str(self.relative_path),
|
|
110
|
+
title=self.title,
|
|
111
|
+
content=self.content,
|
|
112
|
+
content_type=self.file_type.value,
|
|
113
|
+
metadata={
|
|
114
|
+
"file_type": self.file_type.value,
|
|
115
|
+
"size": self.size,
|
|
116
|
+
"modified_at": self.modified_at.isoformat(),
|
|
117
|
+
**self.metadata,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class LocalFileIndexer:
|
|
123
|
+
"""Indexes local files for documentation retrieval.
|
|
124
|
+
|
|
125
|
+
Walks directory trees, reads supported file types,
|
|
126
|
+
extracts content and metadata, and optionally chunks documents.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
config: LocalFileConfig | None = None,
|
|
132
|
+
chunker: Chunker | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Initialize the local file indexer.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
config: Indexer configuration
|
|
138
|
+
chunker: Optional chunker for document splitting
|
|
139
|
+
"""
|
|
140
|
+
self._config = config or LocalFileConfig()
|
|
141
|
+
self._chunker = chunker
|
|
142
|
+
|
|
143
|
+
def _detect_file_type(self, path: Path) -> FileType:
|
|
144
|
+
"""Detect file type from extension.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
path: File path
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Detected file type
|
|
151
|
+
"""
|
|
152
|
+
suffix = path.suffix.lower()
|
|
153
|
+
|
|
154
|
+
if suffix in (".md", ".mdx", ".markdown"):
|
|
155
|
+
return FileType.MARKDOWN
|
|
156
|
+
elif suffix == ".rst":
|
|
157
|
+
return FileType.RST
|
|
158
|
+
elif suffix == ".txt":
|
|
159
|
+
return FileType.TEXT
|
|
160
|
+
elif suffix in (".py", ".js", ".ts", ".java", ".go", ".rs", ".c", ".cpp", ".h"):
|
|
161
|
+
return FileType.CODE
|
|
162
|
+
else:
|
|
163
|
+
return FileType.UNKNOWN
|
|
164
|
+
|
|
165
|
+
def _matches_pattern(self, path: Path, patterns: list[str]) -> bool:
|
|
166
|
+
"""Check if path matches any pattern.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
path: File path
|
|
170
|
+
patterns: Glob patterns
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if matches any pattern
|
|
174
|
+
"""
|
|
175
|
+
path_str = str(path)
|
|
176
|
+
name = path.name
|
|
177
|
+
|
|
178
|
+
for pattern in patterns:
|
|
179
|
+
if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(path_str, pattern):
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def _should_skip_dir(self, dir_name: str) -> bool:
|
|
184
|
+
"""Check if directory should be skipped.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
dir_name: Directory name
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if should skip
|
|
191
|
+
"""
|
|
192
|
+
if dir_name in self._config.skip_dirs:
|
|
193
|
+
return True
|
|
194
|
+
return bool(not self._config.include_hidden and dir_name.startswith("."))
|
|
195
|
+
|
|
196
|
+
def _should_index_file(self, path: Path) -> bool:
|
|
197
|
+
"""Check if file should be indexed.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
path: File path
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if should index
|
|
204
|
+
"""
|
|
205
|
+
# Check hidden
|
|
206
|
+
if not self._config.include_hidden and path.name.startswith("."):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# Check include patterns
|
|
210
|
+
if not self._matches_pattern(path, self._config.include_patterns):
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
# Check exclude patterns
|
|
214
|
+
if self._matches_pattern(path, self._config.exclude_patterns):
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# Check size
|
|
218
|
+
try:
|
|
219
|
+
if path.stat().st_size > self._config.max_file_size:
|
|
220
|
+
return False
|
|
221
|
+
except OSError:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
def _extract_title_from_markdown(self, content: str) -> str | None:
|
|
227
|
+
"""Extract title from Markdown content.
|
|
228
|
+
|
|
229
|
+
Looks for:
|
|
230
|
+
1. YAML frontmatter title
|
|
231
|
+
2. First H1 heading
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
content: Markdown content
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Extracted title or None
|
|
238
|
+
"""
|
|
239
|
+
# Check for YAML frontmatter
|
|
240
|
+
if content.startswith("---"):
|
|
241
|
+
end = content.find("---", 3)
|
|
242
|
+
if end > 0:
|
|
243
|
+
frontmatter = content[3:end]
|
|
244
|
+
title_match = re.search(
|
|
245
|
+
r"^title:\s*[\"']?(.+?)[\"']?\s*$", frontmatter, re.MULTILINE
|
|
246
|
+
)
|
|
247
|
+
if title_match:
|
|
248
|
+
return title_match.group(1).strip()
|
|
249
|
+
|
|
250
|
+
# Look for first H1 heading
|
|
251
|
+
h1_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
|
|
252
|
+
if h1_match:
|
|
253
|
+
return h1_match.group(1).strip()
|
|
254
|
+
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def _extract_title_from_rst(self, content: str) -> str | None:
|
|
258
|
+
"""Extract title from RST content.
|
|
259
|
+
|
|
260
|
+
Looks for the first title (underlined with = or -).
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
content: RST content
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Extracted title or None
|
|
267
|
+
"""
|
|
268
|
+
lines = content.split("\n")
|
|
269
|
+
for i, line in enumerate(lines):
|
|
270
|
+
if i > 0 and line and len(set(line)) == 1 and line[0] in "=-~^":
|
|
271
|
+
# Previous line is the title
|
|
272
|
+
title = lines[i - 1].strip()
|
|
273
|
+
if title:
|
|
274
|
+
return title
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def _extract_title(self, content: str, file_type: FileType, path: Path) -> str | None:
|
|
278
|
+
"""Extract title from content based on file type.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
content: File content
|
|
282
|
+
file_type: File type
|
|
283
|
+
path: File path
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Extracted title or filename-based title
|
|
287
|
+
"""
|
|
288
|
+
if not self._config.extract_title:
|
|
289
|
+
return path.stem
|
|
290
|
+
|
|
291
|
+
title = None
|
|
292
|
+
|
|
293
|
+
if file_type == FileType.MARKDOWN:
|
|
294
|
+
title = self._extract_title_from_markdown(content)
|
|
295
|
+
elif file_type == FileType.RST:
|
|
296
|
+
title = self._extract_title_from_rst(content)
|
|
297
|
+
|
|
298
|
+
# Fallback to filename
|
|
299
|
+
if not title:
|
|
300
|
+
title = path.stem.replace("-", " ").replace("_", " ").title()
|
|
301
|
+
|
|
302
|
+
return title
|
|
303
|
+
|
|
304
|
+
def _read_file(self, path: Path) -> str | None:
|
|
305
|
+
"""Read file content with encoding handling.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
path: File path
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
File content or None if unreadable
|
|
312
|
+
"""
|
|
313
|
+
encodings = [self._config.encoding, "utf-8", "latin-1", "cp1252"]
|
|
314
|
+
|
|
315
|
+
for encoding in encodings:
|
|
316
|
+
try:
|
|
317
|
+
return path.read_text(encoding=encoding)
|
|
318
|
+
except (UnicodeDecodeError, LookupError):
|
|
319
|
+
continue
|
|
320
|
+
except OSError:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
def index_file(self, path: Path, base_path: Path | None = None) -> IndexedFile | None:
|
|
326
|
+
"""Index a single file.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
path: File path to index
|
|
330
|
+
base_path: Base path for computing relative paths
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
IndexedFile or None if not indexable
|
|
334
|
+
"""
|
|
335
|
+
path = path.resolve()
|
|
336
|
+
|
|
337
|
+
if not path.is_file():
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
if not self._should_index_file(path):
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
content = self._read_file(path)
|
|
344
|
+
if content is None:
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
file_type = self._detect_file_type(path)
|
|
348
|
+
title = self._extract_title(content, file_type, path)
|
|
349
|
+
|
|
350
|
+
# Compute relative path
|
|
351
|
+
if base_path:
|
|
352
|
+
try:
|
|
353
|
+
relative_path = str(path.relative_to(base_path))
|
|
354
|
+
except ValueError:
|
|
355
|
+
relative_path = str(path)
|
|
356
|
+
else:
|
|
357
|
+
relative_path = str(path)
|
|
358
|
+
|
|
359
|
+
# Get file stats
|
|
360
|
+
stat = path.stat()
|
|
361
|
+
|
|
362
|
+
return IndexedFile(
|
|
363
|
+
path=path,
|
|
364
|
+
relative_path=relative_path,
|
|
365
|
+
file_type=file_type,
|
|
366
|
+
content=content,
|
|
367
|
+
title=title,
|
|
368
|
+
size=stat.st_size,
|
|
369
|
+
modified_at=datetime.fromtimestamp(stat.st_mtime, tz=UTC),
|
|
370
|
+
metadata={
|
|
371
|
+
"encoding": self._config.encoding,
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def index_directory(
|
|
376
|
+
self,
|
|
377
|
+
directory: Path | str,
|
|
378
|
+
recursive: bool = True,
|
|
379
|
+
) -> Iterator[IndexedFile]:
|
|
380
|
+
"""Index all supported files in a directory.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
directory: Directory to index
|
|
384
|
+
recursive: Whether to recurse into subdirectories
|
|
385
|
+
|
|
386
|
+
Yields:
|
|
387
|
+
IndexedFile for each indexed file
|
|
388
|
+
"""
|
|
389
|
+
directory = Path(directory).resolve()
|
|
390
|
+
|
|
391
|
+
if not directory.is_dir():
|
|
392
|
+
raise ValueError(f"Not a directory: {directory}")
|
|
393
|
+
|
|
394
|
+
if recursive:
|
|
395
|
+
for root, dirs, files in os.walk(directory, followlinks=self._config.follow_symlinks):
|
|
396
|
+
# Filter directories in place
|
|
397
|
+
dirs[:] = [d for d in dirs if not self._should_skip_dir(d)]
|
|
398
|
+
|
|
399
|
+
for filename in files:
|
|
400
|
+
path = Path(root) / filename
|
|
401
|
+
indexed = self.index_file(path, base_path=directory)
|
|
402
|
+
if indexed:
|
|
403
|
+
yield indexed
|
|
404
|
+
else:
|
|
405
|
+
for path in directory.iterdir():
|
|
406
|
+
if path.is_file():
|
|
407
|
+
indexed = self.index_file(path, base_path=directory)
|
|
408
|
+
if indexed:
|
|
409
|
+
yield indexed
|
|
410
|
+
|
|
411
|
+
def index_to_documents(
|
|
412
|
+
self,
|
|
413
|
+
directory: Path | str,
|
|
414
|
+
recursive: bool = True,
|
|
415
|
+
) -> list[Document]:
|
|
416
|
+
"""Index directory and return Document objects.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
directory: Directory to index
|
|
420
|
+
recursive: Whether to recurse into subdirectories
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
List of Document objects
|
|
424
|
+
"""
|
|
425
|
+
documents = []
|
|
426
|
+
for indexed_file in self.index_directory(directory, recursive):
|
|
427
|
+
documents.append(indexed_file.to_document())
|
|
428
|
+
return documents
|
|
429
|
+
|
|
430
|
+
def index_and_chunk(
|
|
431
|
+
self,
|
|
432
|
+
directory: Path | str,
|
|
433
|
+
recursive: bool = True,
|
|
434
|
+
) -> tuple[list[Document], list[Any]]:
|
|
435
|
+
"""Index directory and chunk documents.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
directory: Directory to index
|
|
439
|
+
recursive: Whether to recurse into subdirectories
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Tuple of (documents, chunks)
|
|
443
|
+
"""
|
|
444
|
+
if self._chunker is None:
|
|
445
|
+
self._chunker = Chunker(ChunkerConfig())
|
|
446
|
+
|
|
447
|
+
documents = []
|
|
448
|
+
all_chunks = []
|
|
449
|
+
|
|
450
|
+
for indexed_file in self.index_directory(directory, recursive):
|
|
451
|
+
doc = indexed_file.to_document()
|
|
452
|
+
documents.append(doc)
|
|
453
|
+
|
|
454
|
+
chunks = self._chunker.chunk_document(doc)
|
|
455
|
+
all_chunks.extend(chunks)
|
|
456
|
+
|
|
457
|
+
return documents, all_chunks
|
|
458
|
+
|
|
459
|
+
def get_stats(self, directory: Path | str, recursive: bool = True) -> dict[str, Any]:
|
|
460
|
+
"""Get statistics about indexable files in a directory.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
directory: Directory to analyze
|
|
464
|
+
recursive: Whether to recurse into subdirectories
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Statistics dict
|
|
468
|
+
"""
|
|
469
|
+
stats: dict[str, Any] = {
|
|
470
|
+
"total_files": 0,
|
|
471
|
+
"total_size": 0,
|
|
472
|
+
"by_type": {},
|
|
473
|
+
"skipped": 0,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
directory = Path(directory).resolve()
|
|
477
|
+
|
|
478
|
+
for root, dirs, files in os.walk(directory, followlinks=self._config.follow_symlinks):
|
|
479
|
+
dirs[:] = [d for d in dirs if not self._should_skip_dir(d)]
|
|
480
|
+
|
|
481
|
+
for filename in files:
|
|
482
|
+
path = Path(root) / filename
|
|
483
|
+
|
|
484
|
+
if self._should_index_file(path):
|
|
485
|
+
stats["total_files"] += 1
|
|
486
|
+
try:
|
|
487
|
+
size = path.stat().st_size
|
|
488
|
+
stats["total_size"] += size
|
|
489
|
+
except OSError:
|
|
490
|
+
pass
|
|
491
|
+
|
|
492
|
+
file_type = self._detect_file_type(path).value
|
|
493
|
+
stats["by_type"][file_type] = stats["by_type"].get(file_type, 0) + 1
|
|
494
|
+
else:
|
|
495
|
+
stats["skipped"] += 1
|
|
496
|
+
|
|
497
|
+
if not recursive:
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
return stats
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def get_local_file_indexer(
|
|
504
|
+
config: LocalFileConfig | None = None,
|
|
505
|
+
chunker: Chunker | None = None,
|
|
506
|
+
) -> LocalFileIndexer:
|
|
507
|
+
"""Factory function to get a LocalFileIndexer instance.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
config: Optional configuration
|
|
511
|
+
chunker: Optional chunker instance
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
LocalFileIndexer instance
|
|
515
|
+
"""
|
|
516
|
+
return LocalFileIndexer(config=config, chunker=chunker)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""User interfaces: CLI and MCP server."""
|