ragup 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.
- ragup/__init__.py +7 -0
- ragup/api/__init__.py +5 -0
- ragup/api/routes.py +7 -0
- ragup/api/schemas.py +15 -0
- ragup/api/server.py +81 -0
- ragup/chunking/__init__.py +9 -0
- ragup/chunking/base.py +36 -0
- ragup/chunking/recursive.py +59 -0
- ragup/config.py +48 -0
- ragup/constants.py +73 -0
- ragup/core/__init__.py +0 -0
- ragup/core/document.py +282 -0
- ragup/embeddings/__init__.py +9 -0
- ragup/embeddings/embedder.py +61 -0
- ragup/embeddings/models.py +22 -0
- ragup/exceptions.py +57 -0
- ragup/llm/__init__.py +9 -0
- ragup/llm/answer.py +54 -0
- ragup/llm/prompt.py +36 -0
- ragup/loaders/__init__.py +62 -0
- ragup/loaders/base.py +31 -0
- ragup/loaders/json_loader.py +44 -0
- ragup/loaders/pdf_loader.py +43 -0
- ragup/loaders/txt_loader.py +45 -0
- ragup/retrieval/__init__.py +11 -0
- ragup/retrieval/retriever.py +41 -0
- ragup/retrieval/search.py +55 -0
- ragup/utils/__init__.py +0 -0
- ragup/utils/helpers.py +0 -0
- ragup/utils/logger.py +47 -0
- ragup/utils/validators.py +0 -0
- ragup/vectordb/__init__.py +9 -0
- ragup/vectordb/chroma.py +101 -0
- ragup/vectordb/manager.py +51 -0
- ragup-0.1.0.dist-info/METADATA +224 -0
- ragup-0.1.0.dist-info/RECORD +39 -0
- ragup-0.1.0.dist-info/WHEEL +5 -0
- ragup-0.1.0.dist-info/licenses/LICENSE +21 -0
- ragup-0.1.0.dist-info/top_level.txt +1 -0
ragup/__init__.py
ADDED
ragup/api/__init__.py
ADDED
ragup/api/routes.py
ADDED
ragup/api/schemas.py
ADDED
ragup/api/server.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI server for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
|
|
11
|
+
from ..constants import (
|
|
12
|
+
DEFAULT_HOST,
|
|
13
|
+
DEFAULT_PORT,
|
|
14
|
+
)
|
|
15
|
+
from .schemas import (
|
|
16
|
+
AskRequest,
|
|
17
|
+
SearchRequest,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RagUpServer:
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
document,
|
|
26
|
+
api_key: str | None = None,
|
|
27
|
+
):
|
|
28
|
+
|
|
29
|
+
self.document = document
|
|
30
|
+
self.api_key = api_key
|
|
31
|
+
|
|
32
|
+
self.app = FastAPI(
|
|
33
|
+
title="RagUp API",
|
|
34
|
+
version="1.0.0",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self._register_routes()
|
|
38
|
+
|
|
39
|
+
def _register_routes(self):
|
|
40
|
+
|
|
41
|
+
@self.app.get("/health")
|
|
42
|
+
def health():
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"status": "healthy"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@self.app.post("/search")
|
|
49
|
+
def search(request: SearchRequest):
|
|
50
|
+
|
|
51
|
+
return self.document.search(
|
|
52
|
+
query=request.query,
|
|
53
|
+
top_k=request.top_k,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if self.api_key:
|
|
57
|
+
|
|
58
|
+
@self.app.post("/ask")
|
|
59
|
+
def ask(request: AskRequest):
|
|
60
|
+
|
|
61
|
+
answer = self.document.ask(
|
|
62
|
+
question=request.question,
|
|
63
|
+
api_key=self.api_key,
|
|
64
|
+
top_k=request.top_k,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"answer": answer
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def run(
|
|
72
|
+
self,
|
|
73
|
+
host: str = DEFAULT_HOST,
|
|
74
|
+
port: int = DEFAULT_PORT,
|
|
75
|
+
):
|
|
76
|
+
|
|
77
|
+
uvicorn.run(
|
|
78
|
+
self.app,
|
|
79
|
+
host=host,
|
|
80
|
+
port=port,
|
|
81
|
+
)
|
ragup/chunking/base.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interface for all text chunkers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseChunker(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Base class for all chunking strategies.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
chunk_size: int = 500,
|
|
18
|
+
overlap: int = 50,
|
|
19
|
+
):
|
|
20
|
+
self.chunk_size = chunk_size
|
|
21
|
+
self.overlap = overlap
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def split(self, text: str) -> list[str]:
|
|
25
|
+
"""
|
|
26
|
+
Split text into chunks.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
text : str
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
list[str]
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recursive text chunker.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .base import BaseChunker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RecursiveChunker(BaseChunker):
|
|
11
|
+
"""
|
|
12
|
+
Splits text into overlapping chunks.
|
|
13
|
+
|
|
14
|
+
Chunk sizes are based on words rather than characters,
|
|
15
|
+
producing more natural chunks.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def split(self, text: str) -> list[str]:
|
|
19
|
+
"""
|
|
20
|
+
Split text into overlapping chunks.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
text : str
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
list[str]
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
if not text.strip():
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
words = text.split()
|
|
35
|
+
|
|
36
|
+
if len(words) <= self.chunk_size:
|
|
37
|
+
return [" ".join(words)]
|
|
38
|
+
|
|
39
|
+
chunks = []
|
|
40
|
+
|
|
41
|
+
step = self.chunk_size - self.overlap
|
|
42
|
+
|
|
43
|
+
if step <= 0:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"overlap must be smaller than chunk_size"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
for start in range(0, len(words), step):
|
|
49
|
+
|
|
50
|
+
end = start + self.chunk_size
|
|
51
|
+
|
|
52
|
+
chunk = words[start:end]
|
|
53
|
+
|
|
54
|
+
if not chunk:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
chunks.append(" ".join(chunk))
|
|
58
|
+
|
|
59
|
+
return chunks
|
ragup/config.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global configuration object for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .constants import (
|
|
9
|
+
DEFAULT_CHUNK_OVERLAP,
|
|
10
|
+
DEFAULT_CHUNK_SIZE,
|
|
11
|
+
DEFAULT_EMBEDDING_MODEL,
|
|
12
|
+
DEFAULT_TOP_K,
|
|
13
|
+
DEFAULT_SCORE_THRESHOLD,
|
|
14
|
+
DEFAULT_PROVIDER,
|
|
15
|
+
DEFAULT_MODEL,
|
|
16
|
+
DEFAULT_TEMPERATURE,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class RagUpConfig:
|
|
22
|
+
"""
|
|
23
|
+
Central configuration for the RagUp pipeline.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Chunking
|
|
27
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE
|
|
28
|
+
overlap: int = DEFAULT_CHUNK_OVERLAP
|
|
29
|
+
|
|
30
|
+
# Embeddings
|
|
31
|
+
embedding_model: str = DEFAULT_EMBEDDING_MODEL
|
|
32
|
+
|
|
33
|
+
# Search
|
|
34
|
+
top_k: int = DEFAULT_TOP_K
|
|
35
|
+
score_threshold: float = DEFAULT_SCORE_THRESHOLD
|
|
36
|
+
|
|
37
|
+
# LLM
|
|
38
|
+
provider: str = DEFAULT_PROVIDER
|
|
39
|
+
model: Optional[str] = DEFAULT_MODEL
|
|
40
|
+
api_key: Optional[str] = None
|
|
41
|
+
temperature: float = DEFAULT_TEMPERATURE
|
|
42
|
+
|
|
43
|
+
# Indexing
|
|
44
|
+
cache: bool = True
|
|
45
|
+
rebuild: bool = False
|
|
46
|
+
|
|
47
|
+
# Logging
|
|
48
|
+
verbose: bool = False
|
ragup/constants.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global constants used throughout RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# ==========================================
|
|
8
|
+
# Package
|
|
9
|
+
# ==========================================
|
|
10
|
+
|
|
11
|
+
PACKAGE_NAME = "ragup"
|
|
12
|
+
PACKAGE_VERSION = "1.0.0"
|
|
13
|
+
|
|
14
|
+
# ==========================================
|
|
15
|
+
# Supported Files
|
|
16
|
+
# ==========================================
|
|
17
|
+
|
|
18
|
+
SUPPORTED_EXTENSIONS = {
|
|
19
|
+
".pdf",
|
|
20
|
+
".txt",
|
|
21
|
+
".json",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# ==========================================
|
|
25
|
+
# Chunking Defaults
|
|
26
|
+
# ==========================================
|
|
27
|
+
|
|
28
|
+
DEFAULT_CHUNK_SIZE = 500
|
|
29
|
+
DEFAULT_CHUNK_OVERLAP = 50
|
|
30
|
+
|
|
31
|
+
# ==========================================
|
|
32
|
+
# Embedding Defaults
|
|
33
|
+
# ==========================================
|
|
34
|
+
|
|
35
|
+
DEFAULT_EMBEDDING_MODEL = "all-MiniLM-L6-v2"
|
|
36
|
+
|
|
37
|
+
# ==========================================
|
|
38
|
+
# Vector Database
|
|
39
|
+
# ==========================================
|
|
40
|
+
|
|
41
|
+
DEFAULT_RAGUP_DIR = Path(".ragup")
|
|
42
|
+
|
|
43
|
+
DEFAULT_VECTOR_DB_DIR = DEFAULT_RAGUP_DIR / "db"
|
|
44
|
+
|
|
45
|
+
DEFAULT_METADATA_FILE = DEFAULT_RAGUP_DIR / "metadata.json"
|
|
46
|
+
|
|
47
|
+
DEFAULT_COLLECTION_NAME = "ragup_collection"
|
|
48
|
+
|
|
49
|
+
# ==========================================
|
|
50
|
+
# Search Defaults
|
|
51
|
+
# ==========================================
|
|
52
|
+
|
|
53
|
+
DEFAULT_TOP_K = 5
|
|
54
|
+
|
|
55
|
+
DEFAULT_SCORE_THRESHOLD = 0.0
|
|
56
|
+
|
|
57
|
+
# ==========================================
|
|
58
|
+
# FastAPI Defaults
|
|
59
|
+
# ==========================================
|
|
60
|
+
|
|
61
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
62
|
+
|
|
63
|
+
DEFAULT_PORT = 8085
|
|
64
|
+
|
|
65
|
+
# ==========================================
|
|
66
|
+
# LLM Defaults
|
|
67
|
+
# ==========================================
|
|
68
|
+
|
|
69
|
+
DEFAULT_TEMPERATURE = 0.0
|
|
70
|
+
|
|
71
|
+
DEFAULT_PROVIDER = "gemini"
|
|
72
|
+
|
|
73
|
+
DEFAULT_MODEL = None
|
ragup/core/__init__.py
ADDED
|
File without changes
|
ragup/core/document.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Document class for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from ..api import RagUpServer
|
|
9
|
+
from ..chunking import RecursiveChunker
|
|
10
|
+
from ..config import RagUpConfig
|
|
11
|
+
from ..embeddings import Embedder
|
|
12
|
+
from ..exceptions import FileNotIndexedError
|
|
13
|
+
from ..loaders import get_loader
|
|
14
|
+
from ..retrieval import Retriever, format_search_results
|
|
15
|
+
from ..vectordb import VectorDBManager
|
|
16
|
+
from ..llm import generate_answer
|
|
17
|
+
from ..constants import (
|
|
18
|
+
DEFAULT_METADATA_FILE,
|
|
19
|
+
)
|
|
20
|
+
class Document:
|
|
21
|
+
"""
|
|
22
|
+
Main entry point for RagUp.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
file_path: str | Path,
|
|
28
|
+
):
|
|
29
|
+
|
|
30
|
+
self.file_path = Path(file_path)
|
|
31
|
+
|
|
32
|
+
self.config = RagUpConfig()
|
|
33
|
+
|
|
34
|
+
self.embedder = Embedder(
|
|
35
|
+
self.config.embedding_model
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.chunker = RecursiveChunker(
|
|
39
|
+
chunk_size=self.config.chunk_size,
|
|
40
|
+
overlap=self.config.overlap,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.vectordb = VectorDBManager()
|
|
44
|
+
|
|
45
|
+
self.retriever = Retriever(
|
|
46
|
+
self.embedder,
|
|
47
|
+
self.vectordb,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self._indexed = False
|
|
51
|
+
|
|
52
|
+
def _load_document(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Load the document.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
loader = get_loader(self.file_path)
|
|
58
|
+
|
|
59
|
+
return loader.load()
|
|
60
|
+
|
|
61
|
+
def _chunk_document(
|
|
62
|
+
self,
|
|
63
|
+
text: str,
|
|
64
|
+
) -> list[str]:
|
|
65
|
+
"""
|
|
66
|
+
Split the document into chunks.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
return self.chunker.split(text)
|
|
70
|
+
|
|
71
|
+
def _embed_chunks(
|
|
72
|
+
self,
|
|
73
|
+
chunks: list[str],
|
|
74
|
+
) -> list[list[float]]:
|
|
75
|
+
"""
|
|
76
|
+
Generate embeddings for chunks.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
return self.embedder.embed_documents(
|
|
80
|
+
chunks
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _build_metadata(
|
|
84
|
+
self,
|
|
85
|
+
chunks: list[str],
|
|
86
|
+
) -> tuple[list[str], list[dict]]:
|
|
87
|
+
"""
|
|
88
|
+
Build chunk ids and metadata.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
stem = self.file_path.stem
|
|
92
|
+
|
|
93
|
+
ids = []
|
|
94
|
+
metadatas = []
|
|
95
|
+
|
|
96
|
+
for index in range(len(chunks)):
|
|
97
|
+
|
|
98
|
+
ids.append(
|
|
99
|
+
f"{stem}_{index + 1:04d}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
metadatas.append(
|
|
103
|
+
{
|
|
104
|
+
"source": self.file_path.name,
|
|
105
|
+
"chunk": index + 1,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return ids, metadatas
|
|
110
|
+
|
|
111
|
+
def _metadata(self) -> dict:
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"source": str(self.file_path.resolve()),
|
|
115
|
+
"last_modified": self.file_path.stat().st_mtime,
|
|
116
|
+
"chunk_size": self.chunker.chunk_size,
|
|
117
|
+
"overlap": self.chunker.overlap,
|
|
118
|
+
"embedding_model": self.config.embedding_model,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _save_metadata(self) -> None:
|
|
123
|
+
|
|
124
|
+
DEFAULT_METADATA_FILE.parent.mkdir(
|
|
125
|
+
exist_ok=True
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
with open(
|
|
129
|
+
DEFAULT_METADATA_FILE,
|
|
130
|
+
"w",
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
) as f:
|
|
133
|
+
|
|
134
|
+
json.dump(
|
|
135
|
+
self._metadata(),
|
|
136
|
+
f,
|
|
137
|
+
indent=4,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _is_cached(self) -> bool:
|
|
142
|
+
|
|
143
|
+
if not DEFAULT_METADATA_FILE.exists():
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
with open(
|
|
147
|
+
DEFAULT_METADATA_FILE,
|
|
148
|
+
"r",
|
|
149
|
+
encoding="utf-8",
|
|
150
|
+
) as f:
|
|
151
|
+
|
|
152
|
+
metadata = json.load(f)
|
|
153
|
+
|
|
154
|
+
return metadata == self._metadata()
|
|
155
|
+
|
|
156
|
+
def _ensure_index(self) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Ensure that an index exists for this document.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
if self._indexed:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if self._is_cached():
|
|
165
|
+
self._indexed = True
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
raise FileNotIndexedError(
|
|
169
|
+
"No RagUp index found for this document. Run doc.ragup() first."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _clear_index(self) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Remove the existing index before rebuilding.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
self.vectordb.reset()
|
|
178
|
+
|
|
179
|
+
def ragup(
|
|
180
|
+
self,
|
|
181
|
+
chunk_size: int | None = None,
|
|
182
|
+
overlap: int | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Build or load the RagUp index.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
if chunk_size is not None:
|
|
189
|
+
self.chunker.chunk_size = chunk_size
|
|
190
|
+
|
|
191
|
+
if overlap is not None:
|
|
192
|
+
self.chunker.overlap = overlap
|
|
193
|
+
|
|
194
|
+
if self._is_cached():
|
|
195
|
+
print("✅ Using cached index.")
|
|
196
|
+
self._indexed = True
|
|
197
|
+
|
|
198
|
+
return
|
|
199
|
+
print("🔄 Building new index...")
|
|
200
|
+
self._clear_index()
|
|
201
|
+
|
|
202
|
+
text = self._load_document()
|
|
203
|
+
|
|
204
|
+
chunks = self._chunk_document(text)
|
|
205
|
+
|
|
206
|
+
embeddings = self._embed_chunks(chunks)
|
|
207
|
+
|
|
208
|
+
ids, metadatas = self._build_metadata(chunks)
|
|
209
|
+
|
|
210
|
+
self.vectordb.add(
|
|
211
|
+
ids=ids,
|
|
212
|
+
documents=chunks,
|
|
213
|
+
embeddings=embeddings,
|
|
214
|
+
metadatas=metadatas,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self._save_metadata()
|
|
218
|
+
|
|
219
|
+
self._indexed = True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def search(
|
|
223
|
+
self,
|
|
224
|
+
query: str,
|
|
225
|
+
top_k: int = 5,
|
|
226
|
+
) -> list[dict]:
|
|
227
|
+
"""
|
|
228
|
+
Perform semantic search on the indexed document.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
self._ensure_index()
|
|
232
|
+
|
|
233
|
+
results = self.retriever.retrieve(
|
|
234
|
+
query=query,
|
|
235
|
+
top_k=top_k,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return format_search_results(results)
|
|
239
|
+
def ask(
|
|
240
|
+
self,
|
|
241
|
+
question: str,
|
|
242
|
+
api_key: str,
|
|
243
|
+
top_k: int = 5,
|
|
244
|
+
) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Answer a question using the indexed document.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
self._ensure_index()
|
|
250
|
+
|
|
251
|
+
context = self.search(
|
|
252
|
+
query=question,
|
|
253
|
+
top_k=top_k,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return generate_answer(
|
|
257
|
+
question=question,
|
|
258
|
+
context=context,
|
|
259
|
+
api_key=api_key,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def serve(
|
|
263
|
+
self,
|
|
264
|
+
api_key: str | None = None,
|
|
265
|
+
host: str = "127.0.0.1",
|
|
266
|
+
port: int = 8085,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""
|
|
269
|
+
Launch the RagUp FastAPI server.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
self._ensure_index()
|
|
273
|
+
|
|
274
|
+
server = RagUpServer(
|
|
275
|
+
document=self,
|
|
276
|
+
api_key=api_key,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
server.run(
|
|
280
|
+
host=host,
|
|
281
|
+
port=port,
|
|
282
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentence Transformer embedder.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from sentence_transformers import SentenceTransformer
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
DEFAULT_EMBEDDING_MODEL,
|
|
11
|
+
get_model_name,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Embedder:
|
|
16
|
+
"""
|
|
17
|
+
Wrapper around SentenceTransformer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
model_name: str = DEFAULT_EMBEDDING_MODEL,
|
|
23
|
+
):
|
|
24
|
+
|
|
25
|
+
self.model_name = get_model_name(model_name)
|
|
26
|
+
|
|
27
|
+
self.model = SentenceTransformer(
|
|
28
|
+
self.model_name
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def embed_documents(
|
|
32
|
+
self,
|
|
33
|
+
documents: list[str],
|
|
34
|
+
) -> list[list[float]]:
|
|
35
|
+
"""
|
|
36
|
+
Generate embeddings for document chunks.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
embeddings = self.model.encode(
|
|
40
|
+
documents,
|
|
41
|
+
convert_to_numpy=True,
|
|
42
|
+
normalize_embeddings=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return embeddings.tolist()
|
|
46
|
+
|
|
47
|
+
def embed_query(
|
|
48
|
+
self,
|
|
49
|
+
query: str,
|
|
50
|
+
) -> list[float]:
|
|
51
|
+
"""
|
|
52
|
+
Generate an embedding for a query.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
embedding = self.model.encode(
|
|
56
|
+
query,
|
|
57
|
+
convert_to_numpy=True,
|
|
58
|
+
normalize_embeddings=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return embedding.tolist()
|