raglite-toolkit 1.0.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.
- raglite/__init__.py +114 -0
- raglite/api/__init__.py +4 -0
- raglite/api/schemas.py +26 -0
- raglite/api/server.py +198 -0
- raglite/chunking/__init__.py +4 -0
- raglite/chunking/base.py +13 -0
- raglite/chunking/recursive.py +34 -0
- raglite/cli.py +212 -0
- raglite/config.py +65 -0
- raglite/constants.py +18 -0
- raglite/core/__init__.py +1 -0
- raglite/core/document.py +361 -0
- raglite/embeddings/__init__.py +13 -0
- raglite/embeddings/base.py +41 -0
- raglite/embeddings/factory.py +18 -0
- raglite/embeddings/local.py +66 -0
- raglite/embeddings/models.py +11 -0
- raglite/embeddings/remote.py +158 -0
- raglite/errors.py +47 -0
- raglite/llm/__init__.py +14 -0
- raglite/llm/answer.py +311 -0
- raglite/llm/factory.py +119 -0
- raglite/llm/models.py +12 -0
- raglite/llm/prompt.py +62 -0
- raglite/loaders/__init__.py +34 -0
- raglite/loaders/base.py +11 -0
- raglite/loaders/docx.py +15 -0
- raglite/loaders/json.py +40 -0
- raglite/loaders/markdown.py +11 -0
- raglite/loaders/pdf.py +17 -0
- raglite/loaders/txt.py +15 -0
- raglite/retrieval/__init__.py +3 -0
- raglite/retrieval/retriever.py +57 -0
- raglite/types.py +95 -0
- raglite/utils/__init__.py +1 -0
- raglite/utils/hash.py +26 -0
- raglite/utils/logger.py +44 -0
- raglite/vectordb/__init__.py +4 -0
- raglite/vectordb/base.py +55 -0
- raglite/vectordb/memory.py +108 -0
- raglite_toolkit-1.0.0.dist-info/METADATA +388 -0
- raglite_toolkit-1.0.0.dist-info/RECORD +45 -0
- raglite_toolkit-1.0.0.dist-info/WHEEL +4 -0
- raglite_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
- raglite_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
raglite/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from .constants import PACKAGE_NAME, PACKAGE_VERSION
|
|
2
|
+
|
|
3
|
+
VERSION = PACKAGE_VERSION
|
|
4
|
+
|
|
5
|
+
from .errors import (
|
|
6
|
+
RagLiteError,
|
|
7
|
+
UnsupportedFileTypeError,
|
|
8
|
+
FileNotIndexedError,
|
|
9
|
+
LoaderError,
|
|
10
|
+
ChunkingError,
|
|
11
|
+
EmbeddingError,
|
|
12
|
+
VectorDBError,
|
|
13
|
+
LLMError,
|
|
14
|
+
ConfigError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .types import (
|
|
18
|
+
LLMProviderName,
|
|
19
|
+
EmbeddingProviderName,
|
|
20
|
+
LLMProviderConfig,
|
|
21
|
+
EmbeddingProviderConfig,
|
|
22
|
+
ChunkMetadata,
|
|
23
|
+
StoredChunk,
|
|
24
|
+
SearchResult,
|
|
25
|
+
AnswerResult,
|
|
26
|
+
IndexMetadata,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .config import DocumentOptions, resolve_config
|
|
30
|
+
|
|
31
|
+
from .utils.logger import create_logger
|
|
32
|
+
|
|
33
|
+
from .loaders import (
|
|
34
|
+
BaseLoader,
|
|
35
|
+
TxtLoader,
|
|
36
|
+
MarkdownLoader,
|
|
37
|
+
JsonLoader,
|
|
38
|
+
PdfLoader,
|
|
39
|
+
DocxLoader,
|
|
40
|
+
get_loader,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
from .chunking import BaseChunker, RecursiveChunker
|
|
44
|
+
|
|
45
|
+
from .vectordb import MemoryVectorStore
|
|
46
|
+
|
|
47
|
+
from .retrieval import Retriever
|
|
48
|
+
|
|
49
|
+
from .embeddings import (
|
|
50
|
+
DEFAULT_EMBEDDING_MODELS,
|
|
51
|
+
LocalEmbedder,
|
|
52
|
+
RemoteEmbedder,
|
|
53
|
+
create_embedder,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from .llm import (
|
|
57
|
+
DEFAULT_LLM_MODELS,
|
|
58
|
+
build_system_prompt,
|
|
59
|
+
build_user_prompt,
|
|
60
|
+
create_llm,
|
|
61
|
+
generate_answer,
|
|
62
|
+
stream_answer,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
from .core.document import Document
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"PACKAGE_NAME",
|
|
69
|
+
"PACKAGE_VERSION",
|
|
70
|
+
"VERSION",
|
|
71
|
+
"RagLiteError",
|
|
72
|
+
"UnsupportedFileTypeError",
|
|
73
|
+
"FileNotIndexedError",
|
|
74
|
+
"LoaderError",
|
|
75
|
+
"ChunkingError",
|
|
76
|
+
"EmbeddingError",
|
|
77
|
+
"VectorDBError",
|
|
78
|
+
"LLMError",
|
|
79
|
+
"ConfigError",
|
|
80
|
+
"LLMProviderName",
|
|
81
|
+
"EmbeddingProviderName",
|
|
82
|
+
"LLMProviderConfig",
|
|
83
|
+
"EmbeddingProviderConfig",
|
|
84
|
+
"ChunkMetadata",
|
|
85
|
+
"StoredChunk",
|
|
86
|
+
"SearchResult",
|
|
87
|
+
"AnswerResult",
|
|
88
|
+
"IndexMetadata",
|
|
89
|
+
"DocumentOptions",
|
|
90
|
+
"resolve_config",
|
|
91
|
+
"create_logger",
|
|
92
|
+
"BaseLoader",
|
|
93
|
+
"TxtLoader",
|
|
94
|
+
"MarkdownLoader",
|
|
95
|
+
"JsonLoader",
|
|
96
|
+
"PdfLoader",
|
|
97
|
+
"DocxLoader",
|
|
98
|
+
"get_loader",
|
|
99
|
+
"BaseChunker",
|
|
100
|
+
"RecursiveChunker",
|
|
101
|
+
"MemoryVectorStore",
|
|
102
|
+
"Retriever",
|
|
103
|
+
"DEFAULT_EMBEDDING_MODELS",
|
|
104
|
+
"LocalEmbedder",
|
|
105
|
+
"RemoteEmbedder",
|
|
106
|
+
"create_embedder",
|
|
107
|
+
"DEFAULT_LLM_MODELS",
|
|
108
|
+
"build_system_prompt",
|
|
109
|
+
"build_user_prompt",
|
|
110
|
+
"create_llm",
|
|
111
|
+
"generate_answer",
|
|
112
|
+
"stream_answer",
|
|
113
|
+
"Document",
|
|
114
|
+
]
|
raglite/api/__init__.py
ADDED
raglite/api/schemas.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SearchRequest(BaseModel):
|
|
6
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
7
|
+
|
|
8
|
+
query: str = Field(..., min_length=1)
|
|
9
|
+
topK: Optional[int] = Field(default=None, alias="topK", gt=0, le=50)
|
|
10
|
+
scoreThreshold: Optional[float] = Field(
|
|
11
|
+
default=None, alias="scoreThreshold", ge=-1.0, le=1.0
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AskRequest(BaseModel):
|
|
16
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
17
|
+
|
|
18
|
+
question: str = Field(..., min_length=1)
|
|
19
|
+
topK: Optional[int] = Field(default=None, alias="topK", gt=0, le=50)
|
|
20
|
+
scoreThreshold: Optional[float] = Field(
|
|
21
|
+
default=None, alias="scoreThreshold", ge=-1.0, le=1.0
|
|
22
|
+
)
|
|
23
|
+
includeCitations: Optional[bool] = Field(
|
|
24
|
+
default=None, alias="includeCitations"
|
|
25
|
+
)
|
|
26
|
+
stream: Optional[bool] = None
|
raglite/api/server.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Optional, Dict, Any, Union
|
|
4
|
+
import uvicorn
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request, Depends
|
|
6
|
+
from fastapi.responses import StreamingResponse, JSONResponse
|
|
7
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from ..constants import PACKAGE_VERSION, DEFAULT_HOST, DEFAULT_PORT
|
|
11
|
+
from ..errors import RagLiteError
|
|
12
|
+
from ..embeddings.models import DEFAULT_EMBEDDING_MODELS
|
|
13
|
+
from .schemas import SearchRequest, AskRequest
|
|
14
|
+
|
|
15
|
+
security = HTTPBearer(auto_error=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServerHandle:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
server_thread: threading.Thread,
|
|
22
|
+
uvicorn_server: uvicorn.Server,
|
|
23
|
+
url: str,
|
|
24
|
+
):
|
|
25
|
+
self.server_thread = server_thread
|
|
26
|
+
self.uvicorn_server = uvicorn_server
|
|
27
|
+
self.url = url
|
|
28
|
+
|
|
29
|
+
def close(self) -> None:
|
|
30
|
+
"""Stop the background API server."""
|
|
31
|
+
self.uvicorn_server.should_exit = True
|
|
32
|
+
self.server_thread.join()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_app(document: Any, options: Dict[str, Any]) -> FastAPI:
|
|
36
|
+
app = FastAPI(title="RAGLite REST API")
|
|
37
|
+
|
|
38
|
+
bearer_token = options.get("bearerToken") or options.get("bearer_token")
|
|
39
|
+
|
|
40
|
+
if options.get("requestLogging"):
|
|
41
|
+
|
|
42
|
+
@app.middleware("http")
|
|
43
|
+
async def log_requests(request: Request, call_next):
|
|
44
|
+
start = time.time()
|
|
45
|
+
response = await call_next(request)
|
|
46
|
+
duration = int((time.time() - start) * 1000)
|
|
47
|
+
print(
|
|
48
|
+
f"[raglite] {request.method} {request.url.path} -> {response.status_code} ({duration}ms)"
|
|
49
|
+
)
|
|
50
|
+
return response
|
|
51
|
+
|
|
52
|
+
def authorize(
|
|
53
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
54
|
+
):
|
|
55
|
+
if bearer_token:
|
|
56
|
+
if not credentials or credentials.credentials != bearer_token:
|
|
57
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
@app.exception_handler(ValidationError)
|
|
61
|
+
async def validation_exception_handler(request: Request, exc: ValidationError):
|
|
62
|
+
return JSONResponse(
|
|
63
|
+
status_code=400,
|
|
64
|
+
content={"error": "ValidationError", "details": exc.errors()},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@app.exception_handler(RagLiteError)
|
|
68
|
+
async def raglite_exception_handler(request: Request, exc: RagLiteError):
|
|
69
|
+
return JSONResponse(
|
|
70
|
+
status_code=400,
|
|
71
|
+
content={"error": exc.__class__.__name__, "message": str(exc)},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@app.exception_handler(Exception)
|
|
75
|
+
async def general_exception_handler(request: Request, exc: Exception):
|
|
76
|
+
print("[raglite] unhandled error", exc)
|
|
77
|
+
return JSONResponse(
|
|
78
|
+
status_code=500,
|
|
79
|
+
content={"error": "InternalServerError"},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@app.get("/health")
|
|
83
|
+
async def health():
|
|
84
|
+
return {
|
|
85
|
+
"status": "ok",
|
|
86
|
+
"version": PACKAGE_VERSION,
|
|
87
|
+
"chunks": document.chunk_count,
|
|
88
|
+
"namespace": document.store_namespace,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dependencies = [Depends(authorize)] if bearer_token else []
|
|
92
|
+
|
|
93
|
+
@app.get("/info", dependencies=dependencies)
|
|
94
|
+
async def info():
|
|
95
|
+
cfg = document.resolved_config
|
|
96
|
+
llm_config = options.get("llm") or cfg.llm
|
|
97
|
+
return {
|
|
98
|
+
"version": PACKAGE_VERSION,
|
|
99
|
+
"chunkSize": cfg.chunkSize,
|
|
100
|
+
"overlap": cfg.overlap,
|
|
101
|
+
"topK": cfg.topK,
|
|
102
|
+
"embeddings": {
|
|
103
|
+
"provider": cfg.embeddings.provider,
|
|
104
|
+
"model": cfg.embeddings.model
|
|
105
|
+
or DEFAULT_EMBEDDING_MODELS.get(cfg.embeddings.provider),
|
|
106
|
+
},
|
|
107
|
+
"llmProvider": llm_config.provider if llm_config else None,
|
|
108
|
+
"chunks": document.chunk_count,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@app.post("/search", dependencies=dependencies)
|
|
112
|
+
async def search(req: SearchRequest):
|
|
113
|
+
results = document.search(
|
|
114
|
+
req.query,
|
|
115
|
+
top_k=req.topK,
|
|
116
|
+
score_threshold=req.scoreThreshold,
|
|
117
|
+
)
|
|
118
|
+
return {
|
|
119
|
+
"results": [r.model_dump(by_alias=True) for r in results]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ask_provider = options.get("llm")
|
|
123
|
+
|
|
124
|
+
if ask_provider:
|
|
125
|
+
|
|
126
|
+
@app.post("/ask", dependencies=dependencies)
|
|
127
|
+
async def ask(req: AskRequest):
|
|
128
|
+
ask_options = {
|
|
129
|
+
"llm": ask_provider,
|
|
130
|
+
"topK": req.topK,
|
|
131
|
+
"scoreThreshold": req.scoreThreshold,
|
|
132
|
+
"includeCitations": req.includeCitations,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if req.stream:
|
|
136
|
+
|
|
137
|
+
def event_generator():
|
|
138
|
+
for chunk in document.ask_stream(req.question, ask_options):
|
|
139
|
+
yield chunk
|
|
140
|
+
|
|
141
|
+
return StreamingResponse(
|
|
142
|
+
event_generator(), media_type="text/plain"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
answer = document.ask(req.question, ask_options)
|
|
146
|
+
return answer.model_dump(by_alias=True)
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
|
|
150
|
+
@app.post("/ask", dependencies=dependencies)
|
|
151
|
+
async def ask(req: AskRequest):
|
|
152
|
+
return JSONResponse(
|
|
153
|
+
status_code=503,
|
|
154
|
+
content={
|
|
155
|
+
"error": "AskDisabled",
|
|
156
|
+
"message": "No LLM provider configured on the server.",
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return app
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def create_server(
|
|
164
|
+
document: Any,
|
|
165
|
+
options: Dict[str, Any],
|
|
166
|
+
*,
|
|
167
|
+
host: Optional[str] = None,
|
|
168
|
+
port: Optional[int] = None,
|
|
169
|
+
bearer_token: Optional[str] = None,
|
|
170
|
+
) -> ServerHandle:
|
|
171
|
+
srv_host = host or options.get("host") or DEFAULT_HOST
|
|
172
|
+
srv_port = port or options.get("port") or DEFAULT_PORT
|
|
173
|
+
|
|
174
|
+
# Create server options clone
|
|
175
|
+
server_options = dict(options)
|
|
176
|
+
if bearer_token:
|
|
177
|
+
server_options["bearerToken"] = bearer_token
|
|
178
|
+
|
|
179
|
+
app = build_app(document, server_options)
|
|
180
|
+
|
|
181
|
+
config = uvicorn.Config(
|
|
182
|
+
app=app, host=srv_host, port=srv_port, log_level="warning"
|
|
183
|
+
)
|
|
184
|
+
server = uvicorn.Server(config)
|
|
185
|
+
|
|
186
|
+
thread = threading.Thread(target=server.run)
|
|
187
|
+
thread.daemon = True
|
|
188
|
+
thread.start()
|
|
189
|
+
|
|
190
|
+
url = f"http://{srv_host}:{srv_port}"
|
|
191
|
+
|
|
192
|
+
# Wait up to 2 seconds for server to boot
|
|
193
|
+
retries = 20
|
|
194
|
+
while retries > 0 and not server.started:
|
|
195
|
+
time.sleep(0.1)
|
|
196
|
+
retries -= 1
|
|
197
|
+
|
|
198
|
+
return ServerHandle(server_thread=thread, uvicorn_server=server, url=url)
|
raglite/chunking/base.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseChunker(ABC):
|
|
6
|
+
def __init__(self, chunk_size: int, overlap: int):
|
|
7
|
+
self.chunk_size = chunk_size
|
|
8
|
+
self.overlap = overlap
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def split(self, text: str) -> List[str]:
|
|
12
|
+
"""Split text into chunks."""
|
|
13
|
+
pass
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
from .base import BaseChunker
|
|
4
|
+
from ..errors import ChunkingError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RecursiveChunker(BaseChunker):
|
|
8
|
+
def split(self, text: str) -> List[str]:
|
|
9
|
+
if not text.strip():
|
|
10
|
+
return []
|
|
11
|
+
if self.overlap >= self.chunk_size:
|
|
12
|
+
raise ChunkingError(
|
|
13
|
+
f"overlap ({self.overlap}) must be smaller than chunkSize ({self.chunk_size})"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
words = [w for w in re.split(r"\s+", text) if w]
|
|
17
|
+
if len(words) <= self.chunk_size:
|
|
18
|
+
return [" ".join(words)]
|
|
19
|
+
|
|
20
|
+
step = self.chunk_size - self.overlap
|
|
21
|
+
chunks: List[str] = []
|
|
22
|
+
|
|
23
|
+
start = 0
|
|
24
|
+
while start < len(words):
|
|
25
|
+
end = start + self.chunk_size
|
|
26
|
+
slice_words = words[start:end]
|
|
27
|
+
if not slice_words:
|
|
28
|
+
break
|
|
29
|
+
chunks.append(" ".join(slice_words))
|
|
30
|
+
if end >= len(words):
|
|
31
|
+
break
|
|
32
|
+
start += step
|
|
33
|
+
|
|
34
|
+
return chunks
|
raglite/cli.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import argparse
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .constants import PACKAGE_VERSION
|
|
8
|
+
from .core.document import Document
|
|
9
|
+
|
|
10
|
+
HELP = f"""raglite v{PACKAGE_VERSION}
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
raglite index <file> [--chunk-size N] [--overlap N] [--embed-provider P] [--embed-model M] [--embed-key K] [--rebuild]
|
|
14
|
+
raglite search <file> "query" [--top-k N]
|
|
15
|
+
raglite ask <file> "question" --llm-provider P [--llm-model M] [--llm-key K] [--stream]
|
|
16
|
+
raglite serve <file> --llm-provider P [--llm-key K] [--host H] [--port N] [--token T]
|
|
17
|
+
raglite --help
|
|
18
|
+
raglite --version
|
|
19
|
+
|
|
20
|
+
Providers:
|
|
21
|
+
LLM: openai, anthropic, google, mistral, cohere, groq, xai, ollama
|
|
22
|
+
Embeddings: openai, google, mistral, cohere, voyage, ollama, local
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_common_embedding(args_dict: dict) -> dict:
|
|
27
|
+
provider = args_dict.get("embed_provider") or "local"
|
|
28
|
+
config = {"provider": provider}
|
|
29
|
+
if args_dict.get("embed_model"):
|
|
30
|
+
config["model"] = args_dict["embed_model"]
|
|
31
|
+
if args_dict.get("embed_key"):
|
|
32
|
+
config["apiKey"] = args_dict["embed_key"]
|
|
33
|
+
return config
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_llm(args_dict: dict) -> Optional[dict]:
|
|
37
|
+
provider = args_dict.get("llm_provider")
|
|
38
|
+
if not provider:
|
|
39
|
+
return None
|
|
40
|
+
config = {"provider": provider}
|
|
41
|
+
if args_dict.get("llm_model"):
|
|
42
|
+
config["model"] = args_dict["llm_model"]
|
|
43
|
+
if args_dict.get("llm_key"):
|
|
44
|
+
config["apiKey"] = args_dict["llm_key"]
|
|
45
|
+
return config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_index(args):
|
|
49
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
50
|
+
parser.add_argument("file")
|
|
51
|
+
parser.add_argument("--chunk-size", type=int)
|
|
52
|
+
parser.add_argument("--overlap", type=int)
|
|
53
|
+
parser.add_argument("--embed-provider")
|
|
54
|
+
parser.add_argument("--embed-model")
|
|
55
|
+
parser.add_argument("--embed-key")
|
|
56
|
+
parser.add_argument("--rebuild", action="store_true")
|
|
57
|
+
|
|
58
|
+
parsed = parser.parse_args(args)
|
|
59
|
+
|
|
60
|
+
embeddings = parse_common_embedding(vars(parsed))
|
|
61
|
+
doc = Document(parsed.file, {"embeddings": embeddings})
|
|
62
|
+
|
|
63
|
+
build_opts = {}
|
|
64
|
+
if parsed.chunk_size is not None:
|
|
65
|
+
build_opts["chunkSize"] = parsed.chunk_size
|
|
66
|
+
if parsed.overlap is not None:
|
|
67
|
+
build_opts["overlap"] = parsed.overlap
|
|
68
|
+
if parsed.rebuild:
|
|
69
|
+
build_opts["rebuild"] = True
|
|
70
|
+
|
|
71
|
+
result = doc.build(build_opts)
|
|
72
|
+
sys.stdout.write(f"{json.dumps(result, indent=2)}\n")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_search(args):
|
|
76
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
77
|
+
parser.add_argument("file")
|
|
78
|
+
parser.add_argument("query")
|
|
79
|
+
parser.add_argument("--top-k", type=int)
|
|
80
|
+
parser.add_argument("--embed-provider")
|
|
81
|
+
parser.add_argument("--embed-model")
|
|
82
|
+
parser.add_argument("--embed-key")
|
|
83
|
+
|
|
84
|
+
parsed = parser.parse_args(args)
|
|
85
|
+
|
|
86
|
+
embeddings = parse_common_embedding(vars(parsed))
|
|
87
|
+
doc = Document(parsed.file, {"embeddings": embeddings})
|
|
88
|
+
|
|
89
|
+
search_opts = {}
|
|
90
|
+
if parsed.top_k is not None:
|
|
91
|
+
search_opts["topK"] = parsed.top_k
|
|
92
|
+
|
|
93
|
+
results = doc.search(parsed.query, search_opts)
|
|
94
|
+
serialized = [r.model_dump(by_alias=True) for r in results]
|
|
95
|
+
sys.stdout.write(f"{json.dumps(serialized, indent=2)}\n")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_ask(args):
|
|
99
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
100
|
+
parser.add_argument("file")
|
|
101
|
+
parser.add_argument("question")
|
|
102
|
+
parser.add_argument("--top-k", type=int)
|
|
103
|
+
parser.add_argument("--embed-provider")
|
|
104
|
+
parser.add_argument("--embed-model")
|
|
105
|
+
parser.add_argument("--embed-key")
|
|
106
|
+
parser.add_argument("--llm-provider", required=True)
|
|
107
|
+
parser.add_argument("--llm-model")
|
|
108
|
+
parser.add_argument("--llm-key")
|
|
109
|
+
parser.add_argument("--stream", action="store_true")
|
|
110
|
+
|
|
111
|
+
parsed = parser.parse_args(args)
|
|
112
|
+
|
|
113
|
+
embeddings = parse_common_embedding(vars(parsed))
|
|
114
|
+
llm = parse_llm(vars(parsed))
|
|
115
|
+
|
|
116
|
+
doc = Document(parsed.file, {"embeddings": embeddings, "llm": llm})
|
|
117
|
+
|
|
118
|
+
opts = {}
|
|
119
|
+
if parsed.top_k is not None:
|
|
120
|
+
opts["topK"] = parsed.top_k
|
|
121
|
+
|
|
122
|
+
if parsed.stream:
|
|
123
|
+
for chunk in doc.ask_stream(parsed.question, opts):
|
|
124
|
+
sys.stdout.write(chunk)
|
|
125
|
+
sys.stdout.flush()
|
|
126
|
+
sys.stdout.write("\n")
|
|
127
|
+
else:
|
|
128
|
+
answer = doc.ask(parsed.question, opts)
|
|
129
|
+
sys.stdout.write(f"{answer.text}\n")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run_serve(args):
|
|
133
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
134
|
+
parser.add_argument("file")
|
|
135
|
+
parser.add_argument("--embed-provider")
|
|
136
|
+
parser.add_argument("--embed-model")
|
|
137
|
+
parser.add_argument("--embed-key")
|
|
138
|
+
parser.add_argument("--llm-provider")
|
|
139
|
+
parser.add_argument("--llm-model")
|
|
140
|
+
parser.add_argument("--llm-key")
|
|
141
|
+
parser.add_argument("--host")
|
|
142
|
+
parser.add_argument("--port", type=int)
|
|
143
|
+
parser.add_argument("--token")
|
|
144
|
+
|
|
145
|
+
parsed = parser.parse_args(args)
|
|
146
|
+
|
|
147
|
+
embeddings = parse_common_embedding(vars(parsed))
|
|
148
|
+
llm = parse_llm(vars(parsed))
|
|
149
|
+
|
|
150
|
+
doc = Document(
|
|
151
|
+
parsed.file,
|
|
152
|
+
{"embeddings": embeddings, **({"llm": llm} if llm else {})},
|
|
153
|
+
)
|
|
154
|
+
doc.build()
|
|
155
|
+
|
|
156
|
+
serve_opts = {}
|
|
157
|
+
if llm:
|
|
158
|
+
serve_opts["llm"] = llm
|
|
159
|
+
if parsed.host:
|
|
160
|
+
serve_opts["host"] = parsed.host
|
|
161
|
+
if parsed.port is not None:
|
|
162
|
+
serve_opts["port"] = parsed.port
|
|
163
|
+
if parsed.token:
|
|
164
|
+
serve_opts["bearerToken"] = parsed.token
|
|
165
|
+
|
|
166
|
+
handle = doc.serve(serve_opts)
|
|
167
|
+
sys.stdout.write(f"RagLite listening on {handle.url}\n")
|
|
168
|
+
sys.stdout.flush()
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
while True:
|
|
172
|
+
time.sleep(1)
|
|
173
|
+
except KeyboardInterrupt:
|
|
174
|
+
handle.close()
|
|
175
|
+
sys.exit(0)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main():
|
|
179
|
+
argv = sys.argv[1:]
|
|
180
|
+
if not argv or argv[0] in ("--help", "-h", "help"):
|
|
181
|
+
sys.stdout.write(HELP)
|
|
182
|
+
sys.stdout.flush()
|
|
183
|
+
sys.exit(0)
|
|
184
|
+
if argv[0] in ("--version", "-v", "version"):
|
|
185
|
+
sys.stdout.write(f"{PACKAGE_VERSION}\n")
|
|
186
|
+
sys.stdout.flush()
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
|
|
189
|
+
command = argv[0]
|
|
190
|
+
args = argv[1:]
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
if command == "index":
|
|
194
|
+
run_index(args)
|
|
195
|
+
elif command == "search":
|
|
196
|
+
run_search(args)
|
|
197
|
+
elif command == "ask":
|
|
198
|
+
run_ask(args)
|
|
199
|
+
elif command == "serve":
|
|
200
|
+
run_serve(args)
|
|
201
|
+
else:
|
|
202
|
+
sys.stderr.write(f"Unknown command: {command}\n\n{HELP}")
|
|
203
|
+
sys.stderr.flush()
|
|
204
|
+
sys.exit(2)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
sys.stderr.write(f"Error: {e}\n")
|
|
207
|
+
sys.stderr.flush()
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|
raglite/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Optional, Union, Dict, Any
|
|
2
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
3
|
+
|
|
4
|
+
from .types import EmbeddingProviderConfig, LLMProviderConfig
|
|
5
|
+
from .constants import (
|
|
6
|
+
DEFAULT_CHUNK_SIZE,
|
|
7
|
+
DEFAULT_CHUNK_OVERLAP,
|
|
8
|
+
DEFAULT_TOP_K,
|
|
9
|
+
DEFAULT_SCORE_THRESHOLD,
|
|
10
|
+
DEFAULT_STORE_DIRNAME,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DocumentOptions(BaseModel):
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
16
|
+
|
|
17
|
+
chunkSize: Optional[int] = Field(default=None, alias="chunkSize")
|
|
18
|
+
overlap: Optional[int] = None
|
|
19
|
+
topK: Optional[int] = Field(default=None, alias="topK")
|
|
20
|
+
scoreThreshold: Optional[float] = Field(default=None, alias="scoreThreshold")
|
|
21
|
+
storeDir: Optional[str] = Field(default=None, alias="storeDir")
|
|
22
|
+
embeddings: Optional[EmbeddingProviderConfig] = None
|
|
23
|
+
llm: Optional[LLMProviderConfig] = None
|
|
24
|
+
logLevel: Optional[str] = Field(default=None, alias="logLevel")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ResolvedConfig(BaseModel):
|
|
28
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
29
|
+
|
|
30
|
+
chunkSize: int = Field(..., alias="chunkSize")
|
|
31
|
+
overlap: int
|
|
32
|
+
topK: int = Field(..., alias="topK")
|
|
33
|
+
scoreThreshold: float = Field(..., alias="scoreThreshold")
|
|
34
|
+
storeDir: str = Field(..., alias="storeDir")
|
|
35
|
+
embeddings: EmbeddingProviderConfig
|
|
36
|
+
llm: Optional[LLMProviderConfig] = None
|
|
37
|
+
logLevel: str = Field(..., alias="logLevel")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_config(options: Optional[Union[DocumentOptions, Dict[str, Any]]] = None) -> ResolvedConfig:
|
|
41
|
+
if options is None:
|
|
42
|
+
opts = DocumentOptions()
|
|
43
|
+
elif isinstance(options, dict):
|
|
44
|
+
opts = DocumentOptions.model_validate(options)
|
|
45
|
+
else:
|
|
46
|
+
opts = options
|
|
47
|
+
|
|
48
|
+
chunk_size = opts.chunkSize if opts.chunkSize is not None else DEFAULT_CHUNK_SIZE
|
|
49
|
+
overlap = opts.overlap if opts.overlap is not None else DEFAULT_CHUNK_OVERLAP
|
|
50
|
+
top_k = opts.topK if opts.topK is not None else DEFAULT_TOP_K
|
|
51
|
+
score_threshold = opts.scoreThreshold if opts.scoreThreshold is not None else DEFAULT_SCORE_THRESHOLD
|
|
52
|
+
store_dir = opts.storeDir if opts.storeDir is not None else DEFAULT_STORE_DIRNAME
|
|
53
|
+
embeddings = opts.embeddings if opts.embeddings is not None else EmbeddingProviderConfig(provider="local")
|
|
54
|
+
log_level = opts.logLevel if opts.logLevel is not None else "info"
|
|
55
|
+
|
|
56
|
+
return ResolvedConfig(
|
|
57
|
+
chunkSize=chunk_size,
|
|
58
|
+
overlap=overlap,
|
|
59
|
+
topK=top_k,
|
|
60
|
+
scoreThreshold=score_threshold,
|
|
61
|
+
storeDir=store_dir,
|
|
62
|
+
embeddings=embeddings,
|
|
63
|
+
llm=opts.llm,
|
|
64
|
+
logLevel=log_level
|
|
65
|
+
)
|
raglite/constants.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
PACKAGE_NAME = "raglite-toolkit"
|
|
2
|
+
PACKAGE_VERSION = "0.1.0"
|
|
3
|
+
|
|
4
|
+
SUPPORTED_EXTENSIONS = {".pdf", ".txt", ".json", ".md", ".markdown", ".docx"}
|
|
5
|
+
|
|
6
|
+
DEFAULT_CHUNK_SIZE = 500
|
|
7
|
+
DEFAULT_CHUNK_OVERLAP = 50
|
|
8
|
+
|
|
9
|
+
DEFAULT_TOP_K = 5
|
|
10
|
+
DEFAULT_SCORE_THRESHOLD = 0.0
|
|
11
|
+
|
|
12
|
+
DEFAULT_TEMPERATURE = 0.0
|
|
13
|
+
|
|
14
|
+
DEFAULT_STORE_DIRNAME = ".raglite"
|
|
15
|
+
DEFAULT_COLLECTION_NAME = "default"
|
|
16
|
+
|
|
17
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
18
|
+
DEFAULT_PORT = 8085
|
raglite/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# core module
|