maktaba 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.
- maktaba/__init__.py +42 -0
- maktaba/chunking/__init__.py +12 -0
- maktaba/chunking/base.py +83 -0
- maktaba/chunking/llamaindex.py +0 -0
- maktaba/chunking/models.py +72 -0
- maktaba/chunking/simple.py +0 -0
- maktaba/chunking/unstructured.py +343 -0
- maktaba/citation/__init__.py +0 -0
- maktaba/citation/formatter.py +37 -0
- maktaba/config.py +0 -0
- maktaba/embedding/__init__.py +9 -0
- maktaba/embedding/azure.py +0 -0
- maktaba/embedding/base.py +72 -0
- maktaba/embedding/cohere.py +0 -0
- maktaba/embedding/openai.py +111 -0
- maktaba/embedding/voyage.py +0 -0
- maktaba/exceptions.py +37 -0
- maktaba/logging.py +25 -0
- maktaba/models.py +146 -0
- maktaba/pipeline/__init__.py +0 -0
- maktaba/pipeline/ingestion.py +169 -0
- maktaba/pipeline/query.py +131 -0
- maktaba/reranking/__init__.py +0 -0
- maktaba/reranking/base.py +34 -0
- maktaba/reranking/cohere.py +87 -0
- maktaba/reranking/cross_encoder.py +0 -0
- maktaba/retrieval/__init__.py +0 -0
- maktaba/retrieval/hybrid.py +0 -0
- maktaba/retrieval/query_condenser.py +228 -0
- maktaba/retrieval/vector.py +0 -0
- maktaba/storage/__init__.py +9 -0
- maktaba/storage/base.py +137 -0
- maktaba/storage/chroma.py +119 -0
- maktaba/storage/pinecone.py +124 -0
- maktaba/storage/qdrant.py +439 -0
- maktaba/storage/weaviate.py +133 -0
- maktaba/version.py +0 -0
- maktaba-0.1.0.dist-info/METADATA +157 -0
- maktaba-0.1.0.dist-info/RECORD +41 -0
- maktaba-0.1.0.dist-info/WHEEL +4 -0
- maktaba-0.1.0.dist-info/licenses/LICENSE +21 -0
maktaba/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Maktaba (EC*()) - The library for building libraries.
|
|
3
|
+
|
|
4
|
+
Production-ready RAG infrastructure for Arabic & multilingual applications.
|
|
5
|
+
By NuhaTech.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
ChunkingError,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
EmbeddingError,
|
|
12
|
+
MaktabaError,
|
|
13
|
+
PartitionAPIError,
|
|
14
|
+
StorageError,
|
|
15
|
+
)
|
|
16
|
+
from .models import (
|
|
17
|
+
EmbeddingConfig,
|
|
18
|
+
PartitionConfig,
|
|
19
|
+
SearchResult,
|
|
20
|
+
VectorChunk,
|
|
21
|
+
VectorStoreConfig,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Exceptions
|
|
28
|
+
"MaktabaError",
|
|
29
|
+
"EmbeddingError",
|
|
30
|
+
"StorageError",
|
|
31
|
+
"ChunkingError",
|
|
32
|
+
"ConfigurationError",
|
|
33
|
+
"PartitionAPIError",
|
|
34
|
+
# Models
|
|
35
|
+
"VectorChunk",
|
|
36
|
+
"SearchResult",
|
|
37
|
+
"EmbeddingConfig",
|
|
38
|
+
"VectorStoreConfig",
|
|
39
|
+
"PartitionConfig",
|
|
40
|
+
# Version
|
|
41
|
+
"__version__",
|
|
42
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Document chunking module."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseChunker
|
|
4
|
+
from .models import ChunkMetadata, ChunkResult
|
|
5
|
+
from .unstructured import UnstructuredChunker
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BaseChunker",
|
|
9
|
+
"ChunkMetadata",
|
|
10
|
+
"ChunkResult",
|
|
11
|
+
"UnstructuredChunker",
|
|
12
|
+
]
|
maktaba/chunking/base.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Base interface for document chunkers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from .models import ChunkResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseChunker(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract base class for document chunking implementations.
|
|
13
|
+
|
|
14
|
+
All chunkers return LlamaIndex Document objects for compatibility
|
|
15
|
+
with the broader ecosystem.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def chunk_text(
|
|
20
|
+
self,
|
|
21
|
+
text: str,
|
|
22
|
+
filename: str = "document.txt",
|
|
23
|
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
|
24
|
+
**kwargs: Any,
|
|
25
|
+
) -> ChunkResult:
|
|
26
|
+
"""
|
|
27
|
+
Chunk raw text input.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
text: Raw text to chunk
|
|
31
|
+
filename: Filename to use for metadata
|
|
32
|
+
extra_metadata: Additional metadata to attach to chunks
|
|
33
|
+
**kwargs: Implementation-specific options
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ChunkResult with documents and metadata
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def chunk_file(
|
|
42
|
+
self,
|
|
43
|
+
file_path: Path | str,
|
|
44
|
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> ChunkResult:
|
|
47
|
+
"""
|
|
48
|
+
Chunk a local file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
file_path: Path to file to chunk
|
|
52
|
+
extra_metadata: Additional metadata to attach to chunks
|
|
53
|
+
**kwargs: Implementation-specific options
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ChunkResult with documents and metadata
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def chunk_url(
|
|
62
|
+
self,
|
|
63
|
+
url: str,
|
|
64
|
+
filename: str,
|
|
65
|
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> ChunkResult:
|
|
68
|
+
"""
|
|
69
|
+
Download and chunk a file from URL.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
url: URL to download file from
|
|
73
|
+
filename: Filename to use for metadata and type detection
|
|
74
|
+
extra_metadata: Additional metadata to attach to chunks
|
|
75
|
+
**kwargs: Implementation-specific options
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ChunkResult with documents and metadata
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ChunkingError: If download or chunking fails
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Data models for document chunking."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ChunkMetadata:
|
|
9
|
+
"""
|
|
10
|
+
Metadata about the chunked document.
|
|
11
|
+
|
|
12
|
+
Matches partition-api output format.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
filename: str
|
|
16
|
+
"""Original filename"""
|
|
17
|
+
|
|
18
|
+
filetype: str
|
|
19
|
+
"""MIME type (e.g., 'application/pdf')"""
|
|
20
|
+
|
|
21
|
+
size_in_bytes: int
|
|
22
|
+
"""File size in bytes"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ChunkResult:
|
|
27
|
+
"""
|
|
28
|
+
Result of document chunking operation.
|
|
29
|
+
|
|
30
|
+
Contains chunks as LlamaIndex Document objects plus metadata.
|
|
31
|
+
Matches partition-api response structure.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
documents: List[Any] # List[llama_index.core.schema.Document]
|
|
35
|
+
"""Chunked documents in LlamaIndex format"""
|
|
36
|
+
|
|
37
|
+
metadata: ChunkMetadata
|
|
38
|
+
"""File metadata"""
|
|
39
|
+
|
|
40
|
+
total_chunks: int
|
|
41
|
+
"""Total number of chunks created"""
|
|
42
|
+
|
|
43
|
+
total_characters: int
|
|
44
|
+
"""Total character count across all chunks"""
|
|
45
|
+
|
|
46
|
+
total_pages: Optional[int] = None
|
|
47
|
+
"""Total pages (if applicable to document type)"""
|
|
48
|
+
|
|
49
|
+
extra_metadata: Dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
"""Additional metadata provided by user"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
"""Convert to dictionary format (matches partition-api response)."""
|
|
55
|
+
result = {
|
|
56
|
+
"metadata": {
|
|
57
|
+
"filename": self.metadata.filename,
|
|
58
|
+
"filetype": self.metadata.filetype,
|
|
59
|
+
"sizeInBytes": self.metadata.size_in_bytes,
|
|
60
|
+
},
|
|
61
|
+
"total_chunks": self.total_chunks,
|
|
62
|
+
"total_characters": self.total_characters,
|
|
63
|
+
"documents": [doc.to_dict() for doc in self.documents],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if self.total_pages is not None:
|
|
67
|
+
result["total_pages"] = self.total_pages
|
|
68
|
+
|
|
69
|
+
if self.extra_metadata:
|
|
70
|
+
result["extra_metadata"] = self.extra_metadata
|
|
71
|
+
|
|
72
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Unstructured document chunker - extracted from partition-api."""
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ..exceptions import ChunkingError
|
|
10
|
+
from .base import BaseChunker
|
|
11
|
+
from .models import ChunkMetadata, ChunkResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnstructuredChunker(BaseChunker):
|
|
15
|
+
"""
|
|
16
|
+
Document chunker using Unstructured.io library via LlamaIndex.
|
|
17
|
+
|
|
18
|
+
This implementation extracts the core logic from partition-api (main.py)
|
|
19
|
+
and integrates it directly into Maktaba for better performance and simplicity.
|
|
20
|
+
|
|
21
|
+
Supports:
|
|
22
|
+
- Multiple file formats (PDF, DOCX, TXT, HTML, etc.)
|
|
23
|
+
- Different parsing strategies (auto, fast, hi_res, ocr_only)
|
|
24
|
+
- Different chunking strategies (basic, by_title)
|
|
25
|
+
- Automatic file type detection
|
|
26
|
+
- Page number extraction (for PDFs)
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
strategy: Literal["auto", "fast", "hi_res", "ocr_only"] = "auto",
|
|
33
|
+
chunking_strategy: Literal["basic", "by_title"] = "basic",
|
|
34
|
+
allowed_metadata_types: Tuple[type, ...] = (str, int, float, list, dict, type(None)),
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize UnstructuredChunker.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
strategy: Parsing strategy (default: "auto")
|
|
41
|
+
- "auto": Automatically choose best strategy
|
|
42
|
+
- "fast": Fast parsing (no OCR)
|
|
43
|
+
- "hi_res": High-resolution parsing (with OCR)
|
|
44
|
+
- "ocr_only": OCR-only parsing
|
|
45
|
+
chunking_strategy: How to chunk documents (default: "basic")
|
|
46
|
+
- "basic": Simple text splitting
|
|
47
|
+
- "by_title": Chunk by document structure (headings)
|
|
48
|
+
allowed_metadata_types: Allowed metadata value types
|
|
49
|
+
"""
|
|
50
|
+
self.strategy = strategy
|
|
51
|
+
self.chunking_strategy = chunking_strategy
|
|
52
|
+
self.allowed_metadata_types = allowed_metadata_types
|
|
53
|
+
|
|
54
|
+
async def chunk_text(
|
|
55
|
+
self,
|
|
56
|
+
text: str,
|
|
57
|
+
filename: str = "document.txt",
|
|
58
|
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
|
59
|
+
**kwargs: Any,
|
|
60
|
+
) -> ChunkResult:
|
|
61
|
+
"""
|
|
62
|
+
Chunk raw text input.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
text: Raw text to chunk
|
|
66
|
+
filename: Filename for metadata (default: "document.txt")
|
|
67
|
+
extra_metadata: Additional metadata to attach to chunks
|
|
68
|
+
**kwargs: Override strategy/chunking_strategy
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
ChunkResult with documents and metadata
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
# Import here to avoid requiring unstructured for other operations
|
|
75
|
+
from llama_index.readers.file import UnstructuredReader
|
|
76
|
+
from unstructured.file_utils.filetype import detect_filetype
|
|
77
|
+
|
|
78
|
+
# Convert text to bytes
|
|
79
|
+
text_bytes = text.encode("utf-8")
|
|
80
|
+
size_in_bytes = len(text_bytes)
|
|
81
|
+
file_stream = BytesIO(text_bytes)
|
|
82
|
+
|
|
83
|
+
# Detect file type
|
|
84
|
+
content_type = detect_filetype(
|
|
85
|
+
file=file_stream,
|
|
86
|
+
metadata_file_path=filename,
|
|
87
|
+
).mime_type
|
|
88
|
+
|
|
89
|
+
# Reset stream position after detection
|
|
90
|
+
file_stream.seek(0)
|
|
91
|
+
|
|
92
|
+
# Build unstructured arguments
|
|
93
|
+
unstructured_args = {
|
|
94
|
+
"strategy": kwargs.get("strategy", self.strategy),
|
|
95
|
+
"chunking_strategy": kwargs.get(
|
|
96
|
+
"chunking_strategy", self.chunking_strategy
|
|
97
|
+
),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Load and chunk document
|
|
101
|
+
reader = UnstructuredReader(
|
|
102
|
+
allowed_metadata_types=self.allowed_metadata_types,
|
|
103
|
+
)
|
|
104
|
+
documents = reader.load_data(
|
|
105
|
+
unstructured_kwargs={
|
|
106
|
+
"file": file_stream,
|
|
107
|
+
"metadata_filename": filename,
|
|
108
|
+
"content_type": content_type,
|
|
109
|
+
**unstructured_args,
|
|
110
|
+
},
|
|
111
|
+
split_documents=True,
|
|
112
|
+
extra_info=extra_metadata or {},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not documents:
|
|
116
|
+
raise ChunkingError("No chunks created from text")
|
|
117
|
+
|
|
118
|
+
# Calculate statistics
|
|
119
|
+
total_characters = sum(len(doc.text or "") for doc in documents)
|
|
120
|
+
total_chunks = len(documents)
|
|
121
|
+
total_pages = self._extract_total_pages(documents)
|
|
122
|
+
|
|
123
|
+
return ChunkResult(
|
|
124
|
+
documents=documents,
|
|
125
|
+
metadata=ChunkMetadata(
|
|
126
|
+
filename=filename,
|
|
127
|
+
filetype=content_type,
|
|
128
|
+
size_in_bytes=size_in_bytes,
|
|
129
|
+
),
|
|
130
|
+
total_chunks=total_chunks,
|
|
131
|
+
total_characters=total_characters,
|
|
132
|
+
total_pages=total_pages,
|
|
133
|
+
extra_metadata=extra_metadata or {},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise ChunkingError(f"Failed to chunk text: {str(e)}") from e
|
|
138
|
+
|
|
139
|
+
async def chunk_file(
|
|
140
|
+
self,
|
|
141
|
+
file_path: Path | str,
|
|
142
|
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
|
143
|
+
**kwargs: Any,
|
|
144
|
+
) -> ChunkResult:
|
|
145
|
+
"""
|
|
146
|
+
Chunk a local file.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
file_path: Path to file to chunk
|
|
150
|
+
extra_metadata: Additional metadata to attach to chunks
|
|
151
|
+
**kwargs: Override strategy/chunking_strategy
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
ChunkResult with documents and metadata
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
# Import here to avoid requiring unstructured for other operations
|
|
158
|
+
from llama_index.readers.file import UnstructuredReader
|
|
159
|
+
from unstructured.file_utils.filetype import detect_filetype
|
|
160
|
+
|
|
161
|
+
# Convert to Path object
|
|
162
|
+
path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
163
|
+
|
|
164
|
+
if not path.exists():
|
|
165
|
+
raise ChunkingError(f"File not found: {file_path}")
|
|
166
|
+
|
|
167
|
+
# Read file
|
|
168
|
+
file_bytes = path.read_bytes()
|
|
169
|
+
size_in_bytes = len(file_bytes)
|
|
170
|
+
file_stream = BytesIO(file_bytes)
|
|
171
|
+
|
|
172
|
+
# Detect file type
|
|
173
|
+
content_type = detect_filetype(
|
|
174
|
+
file=file_stream,
|
|
175
|
+
metadata_file_path=str(path),
|
|
176
|
+
).mime_type
|
|
177
|
+
|
|
178
|
+
# Reset stream position after detection
|
|
179
|
+
file_stream.seek(0)
|
|
180
|
+
|
|
181
|
+
# Build unstructured arguments
|
|
182
|
+
unstructured_args = {
|
|
183
|
+
"strategy": kwargs.get("strategy", self.strategy),
|
|
184
|
+
"chunking_strategy": kwargs.get(
|
|
185
|
+
"chunking_strategy", self.chunking_strategy
|
|
186
|
+
),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Load and chunk document
|
|
190
|
+
reader = UnstructuredReader(
|
|
191
|
+
allowed_metadata_types=self.allowed_metadata_types,
|
|
192
|
+
)
|
|
193
|
+
documents = reader.load_data(
|
|
194
|
+
unstructured_kwargs={
|
|
195
|
+
"file": file_stream,
|
|
196
|
+
"metadata_filename": path.name,
|
|
197
|
+
"content_type": content_type,
|
|
198
|
+
**unstructured_args,
|
|
199
|
+
},
|
|
200
|
+
split_documents=True,
|
|
201
|
+
extra_info=extra_metadata or {},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if not documents:
|
|
205
|
+
raise ChunkingError(f"No chunks created from file: {file_path}")
|
|
206
|
+
|
|
207
|
+
# Calculate statistics
|
|
208
|
+
total_characters = sum(len(doc.text or "") for doc in documents)
|
|
209
|
+
total_chunks = len(documents)
|
|
210
|
+
total_pages = self._extract_total_pages(documents)
|
|
211
|
+
|
|
212
|
+
return ChunkResult(
|
|
213
|
+
documents=documents,
|
|
214
|
+
metadata=ChunkMetadata(
|
|
215
|
+
filename=path.name,
|
|
216
|
+
filetype=content_type,
|
|
217
|
+
size_in_bytes=size_in_bytes,
|
|
218
|
+
),
|
|
219
|
+
total_chunks=total_chunks,
|
|
220
|
+
total_characters=total_characters,
|
|
221
|
+
total_pages=total_pages,
|
|
222
|
+
extra_metadata=extra_metadata or {},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
except ChunkingError:
|
|
226
|
+
raise
|
|
227
|
+
except Exception as e:
|
|
228
|
+
raise ChunkingError(f"Failed to chunk file: {str(e)}") from e
|
|
229
|
+
|
|
230
|
+
async def chunk_url(
|
|
231
|
+
self,
|
|
232
|
+
url: str,
|
|
233
|
+
filename: str,
|
|
234
|
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
|
235
|
+
**kwargs: Any,
|
|
236
|
+
) -> ChunkResult:
|
|
237
|
+
"""
|
|
238
|
+
Download and chunk a file from URL.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
url: URL to download file from
|
|
242
|
+
filename: Filename for metadata and type detection
|
|
243
|
+
extra_metadata: Additional metadata to attach to chunks
|
|
244
|
+
**kwargs: Override strategy/chunking_strategy
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
ChunkResult with documents and metadata
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ChunkingError: If download or chunking fails
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
# Import here to avoid requiring unstructured for other operations
|
|
254
|
+
from llama_index.readers.file import UnstructuredReader
|
|
255
|
+
from unstructured.file_utils.filetype import detect_filetype
|
|
256
|
+
|
|
257
|
+
# Download file
|
|
258
|
+
async with httpx.AsyncClient() as client:
|
|
259
|
+
response = await client.get(url, follow_redirects=True)
|
|
260
|
+
response.raise_for_status()
|
|
261
|
+
file_bytes = response.content
|
|
262
|
+
|
|
263
|
+
size_in_bytes = len(file_bytes)
|
|
264
|
+
file_stream = BytesIO(file_bytes)
|
|
265
|
+
|
|
266
|
+
# Detect file type
|
|
267
|
+
content_type = detect_filetype(
|
|
268
|
+
file=file_stream,
|
|
269
|
+
metadata_file_path=filename,
|
|
270
|
+
).mime_type
|
|
271
|
+
|
|
272
|
+
# Reset stream position after detection
|
|
273
|
+
file_stream.seek(0)
|
|
274
|
+
|
|
275
|
+
# Build unstructured arguments
|
|
276
|
+
unstructured_args = {
|
|
277
|
+
"strategy": kwargs.get("strategy", self.strategy),
|
|
278
|
+
"chunking_strategy": kwargs.get(
|
|
279
|
+
"chunking_strategy", self.chunking_strategy
|
|
280
|
+
),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Load and chunk document
|
|
284
|
+
reader = UnstructuredReader(
|
|
285
|
+
allowed_metadata_types=self.allowed_metadata_types,
|
|
286
|
+
)
|
|
287
|
+
documents = reader.load_data(
|
|
288
|
+
unstructured_kwargs={
|
|
289
|
+
"file": file_stream,
|
|
290
|
+
"metadata_filename": filename,
|
|
291
|
+
"content_type": content_type,
|
|
292
|
+
**unstructured_args,
|
|
293
|
+
},
|
|
294
|
+
split_documents=True,
|
|
295
|
+
extra_info=extra_metadata or {},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not documents:
|
|
299
|
+
raise ChunkingError(f"No chunks created from URL: {url}")
|
|
300
|
+
|
|
301
|
+
# Calculate statistics
|
|
302
|
+
total_characters = sum(len(doc.text or "") for doc in documents)
|
|
303
|
+
total_chunks = len(documents)
|
|
304
|
+
total_pages = self._extract_total_pages(documents)
|
|
305
|
+
|
|
306
|
+
return ChunkResult(
|
|
307
|
+
documents=documents,
|
|
308
|
+
metadata=ChunkMetadata(
|
|
309
|
+
filename=filename,
|
|
310
|
+
filetype=content_type,
|
|
311
|
+
size_in_bytes=size_in_bytes,
|
|
312
|
+
),
|
|
313
|
+
total_chunks=total_chunks,
|
|
314
|
+
total_characters=total_characters,
|
|
315
|
+
total_pages=total_pages,
|
|
316
|
+
extra_metadata=extra_metadata or {},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
except httpx.HTTPError as e:
|
|
320
|
+
raise ChunkingError(f"Failed to download file from URL: {str(e)}") from e
|
|
321
|
+
except ChunkingError:
|
|
322
|
+
raise
|
|
323
|
+
except Exception as e:
|
|
324
|
+
raise ChunkingError(f"Failed to chunk URL: {str(e)}") from e
|
|
325
|
+
|
|
326
|
+
def _extract_total_pages(self, documents: List[Any]) -> Optional[int]:
|
|
327
|
+
"""
|
|
328
|
+
Extract total page count from documents.
|
|
329
|
+
|
|
330
|
+
Matches partition-api logic: finds max page_number in metadata.
|
|
331
|
+
"""
|
|
332
|
+
total_pages = None
|
|
333
|
+
|
|
334
|
+
for doc in documents:
|
|
335
|
+
if hasattr(doc, "metadata") and isinstance(doc.metadata, dict):
|
|
336
|
+
page_number = doc.metadata.get("page_number")
|
|
337
|
+
if page_number is not None:
|
|
338
|
+
total_pages = max(
|
|
339
|
+
total_pages if total_pages is not None else 0,
|
|
340
|
+
page_number,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return total_pages
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Citation formatter for search results."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
from ..models import SearchResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_with_citations(
|
|
9
|
+
results: List[SearchResult], top_k: int = 10
|
|
10
|
+
) -> Dict[str, object]:
|
|
11
|
+
"""
|
|
12
|
+
Produce formatted context and citation entries from search results.
|
|
13
|
+
|
|
14
|
+
Returns a dict with:
|
|
15
|
+
- formatted_context: str
|
|
16
|
+
- citations: List[Dict]
|
|
17
|
+
"""
|
|
18
|
+
limited = results[:top_k]
|
|
19
|
+
|
|
20
|
+
blocks: List[str] = []
|
|
21
|
+
citations: List[Dict[str, object]] = []
|
|
22
|
+
|
|
23
|
+
for idx, res in enumerate(limited, start=1):
|
|
24
|
+
text = res.text or ""
|
|
25
|
+
blocks.append(f"[{idx}]: {text}")
|
|
26
|
+
citations.append(
|
|
27
|
+
{
|
|
28
|
+
"index": idx,
|
|
29
|
+
"id": res.id,
|
|
30
|
+
"document_id": res.document_id,
|
|
31
|
+
"chunk_id": res.chunk_id,
|
|
32
|
+
"metadata": res.metadata,
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
formatted_context = "\n\n".join(blocks)
|
|
37
|
+
return {"formatted_context": formatted_context, "citations": citations}
|
maktaba/config.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Base interface for embedding providers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List, Literal
|
|
5
|
+
|
|
6
|
+
from ..models import EmbeddingVector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseEmbedder(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class for text embedding providers.
|
|
12
|
+
|
|
13
|
+
Design principles:
|
|
14
|
+
1. Batch-first: embed_batch is the primary method
|
|
15
|
+
2. Input types: Support 'document' vs 'query' for Voyage AI
|
|
16
|
+
3. Async-first: All methods are async
|
|
17
|
+
4. Dimension aware: Each embedder knows its output dimension
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def embed_batch(
|
|
22
|
+
self,
|
|
23
|
+
texts: List[str],
|
|
24
|
+
input_type: Literal["document", "query"] = "document",
|
|
25
|
+
) -> List[EmbeddingVector]:
|
|
26
|
+
"""
|
|
27
|
+
Embed multiple texts in a single batch (primary method).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
texts: List of texts to embed
|
|
31
|
+
input_type: Type of input - 'document' for indexing, 'query' for search
|
|
32
|
+
(required by Voyage AI, ignored by OpenAI/others)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of embedding vectors, one per input text
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
EmbeddingError: If embedding fails
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
async def embed_text(
|
|
43
|
+
self,
|
|
44
|
+
text: str,
|
|
45
|
+
input_type: Literal["document", "query"] = "document",
|
|
46
|
+
) -> EmbeddingVector:
|
|
47
|
+
"""
|
|
48
|
+
Embed a single text (convenience wrapper).
|
|
49
|
+
|
|
50
|
+
This is implemented as a wrapper around embed_batch for consistency.
|
|
51
|
+
"""
|
|
52
|
+
results = await self.embed_batch([text], input_type=input_type)
|
|
53
|
+
return results[0]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def dimension(self) -> int:
|
|
58
|
+
"""
|
|
59
|
+
Get the dimension of embeddings produced by this embedder.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
- text-embedding-3-large: 3072
|
|
63
|
+
- text-embedding-3-small: 1536
|
|
64
|
+
- voyage-3-large: 1024
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def model(self) -> str:
|
|
71
|
+
"""Get the model name/identifier."""
|
|
72
|
+
pass
|
|
File without changes
|