Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/loaders/registry.py: 32%
74 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Loader registry and smart loader for document format auto-detection.
3Provides ``LoaderRegistry`` (extension/mime-to-loader map) and
4``SmartLoader`` (auto-detects format and delegates to the right loader).
5"""
7from __future__ import annotations
9from pathlib import Path
10from typing import Any
11from urllib.parse import urlparse
13from lexigram.ai.rag.chunking.types import Chunk
14from lexigram.ai.rag.loaders.core import AbstractDocumentLoader
15from lexigram.ai.rag.types import RAGError
16from lexigram.logging import (
17 get_logger,
18)
19from lexigram.primitives.registry import Registry
21logger = get_logger(__name__)
24class UnsupportedFormatError(RAGError):
25 """Raised when no loader is registered for the given file format or URL."""
27 _code: str = "LEX_ERR_RAG_016"
29 def __init__(self, source: str) -> None:
30 """Create an UnsupportedFormatError.
32 Args:
33 source: The source path or URL that could not be matched.
34 """
35 super().__init__(f"No loader registered for source: {source!r}")
36 self.source = source
39class LoaderRegistry:
40 """Registry mapping file extensions and URL schemes to loader instances.
42 Uses extension strings (including the leading dot) as keys, e.g.
43 ``.pdf``, ``.json``. The special key ``"url"`` is used for HTTP/HTTPS
44 sources.
46 Follows the registry pattern — no ``if/elif`` chains.
48 Example:
49 >>> registry = LoaderRegistry()
50 >>> registry.register([".txt", ".md"], TextLoader())
51 >>> registry.register([".json", ".jsonl"], JSONLoader())
52 >>> loader = registry.get_loader("report.json")
53 """
55 def __init__(self) -> None:
56 """Initialize an empty LoaderRegistry."""
57 self._registry: Registry[str, AbstractDocumentLoader] = Registry(name="loaders")
59 def register(self, extensions: list[str], loader: AbstractDocumentLoader) -> None:
60 """Register a loader for one or more extensions.
62 Args:
63 extensions: List of file extensions (e.g. ``[".pdf"]``) or
64 the special value ``["url"]`` for HTTP/HTTPS sources.
65 loader: Loader instance to handle those extensions.
66 """
67 for ext in extensions:
68 self._registry.register(ext.lower(), loader)
70 def get_loader(self, source: str) -> AbstractDocumentLoader | None:
71 """Return the loader registered for this source, or None.
73 Args:
74 source: File path string or URL.
76 Returns:
77 The registered loader, or ``None`` if no match found.
78 """
79 src = str(source)
80 parsed = urlparse(src)
81 if parsed.scheme in ("http", "https"):
82 return self._registry.get("url")
83 ext = Path(src).suffix.lower()
84 return self._registry.get(ext)
86 async def load(self, source: str | Path, **kwargs: Any) -> list[Chunk]:
87 """Resolve the loader and load the source.
89 Args:
90 source: File path or URL.
91 **kwargs: Passed through to the resolved loader.
93 Returns:
94 List of chunks.
96 Raises:
97 UnsupportedFormatError: If no loader is registered for this
98 extension or URL scheme.
99 RAGError: If the underlying loader raises.
100 """
101 loader = self.get_loader(str(source))
102 if loader is None:
103 raise UnsupportedFormatError(str(source))
104 return await loader.load(source, **kwargs)
107def build_default_registry() -> LoaderRegistry:
108 """Build a ``LoaderRegistry`` pre-populated with all built-in loaders.
110 Only loaders whose optional dependencies are available are registered;
111 a missing dependency causes that loader to be silently skipped.
113 Returns:
114 A ready-to-use registry with loaders registered for well-known
115 extensions.
116 """
117 from lexigram.ai.rag.loaders.core import (
118 CSVLoader,
119 HTMLLoader,
120 JSONLoader,
121 MarkdownLoader,
122 TextLoader,
123 )
124 from lexigram.ai.rag.loaders.p1_loaders import (
125 CodeLoader,
126 DocxLoader,
127 EmailLoader,
128 ExcelLoader,
129 WebScraperLoader,
130 )
132 registry = LoaderRegistry()
134 # P0 loaders (no optional deps)
135 registry.register([".txt", ".rst"], TextLoader())
136 registry.register([".md", ".markdown"], MarkdownLoader())
137 registry.register([".json", ".jsonl"], JSONLoader())
138 registry.register([".csv"], CSVLoader())
139 registry.register([".tsv"], CSVLoader(delimiter="\t"))
141 # P0 — beautifulsoup4 optional
142 try:
143 registry.register([".html", ".htm"], HTMLLoader())
144 registry.register(["url"], WebScraperLoader()) # type: ignore[arg-type]
145 except ImportError as e:
146 logger.debug(
147 "loader_registration_skipped",
148 loaders=["HTMLLoader", "WebScraperLoader"],
149 error=str(e),
150 )
152 # P0 — pypdf optional
153 try:
154 from lexigram.ai.rag.loaders.core import PDFLoader
156 registry.register([".pdf"], PDFLoader())
157 except ImportError as e:
158 logger.debug("loader_registration_skipped", loaders=["PDFLoader"], error=str(e))
160 # P1 loaders
161 try:
162 registry.register([".docx"], DocxLoader()) # type: ignore[arg-type]
163 except ImportError as e:
164 logger.debug(
165 "loader_registration_skipped", loaders=["DocxLoader"], error=str(e)
166 )
168 try:
169 registry.register([".xlsx", ".xls"], ExcelLoader()) # type: ignore[arg-type]
170 except ImportError as e:
171 logger.debug(
172 "loader_registration_skipped", loaders=["ExcelLoader"], error=str(e)
173 )
175 registry.register([".eml"], EmailLoader()) # type: ignore[arg-type]
177 code_loader = CodeLoader()
178 registry.register(
179 [".py", ".js", ".ts", ".java", ".go", ".rs", ".rb", ".cpp", ".c", ".cs"],
180 code_loader, # type: ignore[arg-type]
181 )
183 return registry
186class SmartLoader:
187 """Auto-detect file format from extension or URL scheme and delegate.
189 Uses :class:`LoaderRegistry` internally. If no custom registry is
190 provided, :func:`build_default_registry` is called on first use.
192 Example:
193 >>> loader = SmartLoader()
194 >>> chunks = await loader.load("report.pdf")
195 >>> chunks = await loader.load("https://example.com/page")
196 """
198 def __init__(self, registry: LoaderRegistry | None = None) -> None:
199 """Initialize SmartLoader.
201 Args:
202 registry: Optional pre-configured registry. Defaults to the
203 result of :func:`build_default_registry`.
204 """
205 self._registry = registry
207 @property
208 def registry(self) -> LoaderRegistry:
209 """Lazy-initialize the registry on first access."""
210 if self._registry is None:
211 self._registry = build_default_registry()
212 return self._registry
214 async def load(self, source: str | Path, **kwargs: Any) -> list[Chunk]:
215 """Load a source using the auto-detected loader.
217 Args:
218 source: File path or URL.
219 **kwargs: Passed through to the resolved loader.
221 Returns:
222 List of chunks.
224 Raises:
225 UnsupportedFormatError: If the file extension or URL scheme has
226 no registered loader.
227 RAGError: If loading fails.
228 """
229 return await self.registry.load(source, **kwargs)