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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Embedding model registry for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
DEFAULT_EMBEDDING_MODEL = "all-MiniLM-L6-v2"
|
|
6
|
+
|
|
7
|
+
SUPPORTED_EMBEDDING_MODELS = {
|
|
8
|
+
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
|
|
9
|
+
"bge-small-en": "BAAI/bge-small-en-v1.5",
|
|
10
|
+
"bge-base-en": "BAAI/bge-base-en-v1.5",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_model_name(model_name: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Resolve a model alias to its Hugging Face identifier.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
return SUPPORTED_EMBEDDING_MODELS.get(
|
|
20
|
+
model_name,
|
|
21
|
+
model_name,
|
|
22
|
+
)
|
ragup/exceptions.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions used throughout RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RagUpError(Exception):
|
|
7
|
+
"""Base exception for all RagUp errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsupportedFileTypeError(RagUpError):
|
|
13
|
+
"""Raised when the provided file type is not supported."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileNotIndexedError(RagUpError):
|
|
19
|
+
"""Raised when ask() or search() is called before ragup()."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidAPIKeyError(RagUpError):
|
|
25
|
+
"""Raised when an invalid API key is provided."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VectorDBError(RagUpError):
|
|
31
|
+
"""Raised when a vector database operation fails."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EmbeddingError(RagUpError):
|
|
37
|
+
"""Raised when embedding generation fails."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LLMError(RagUpError):
|
|
43
|
+
"""Raised when the LLM provider returns an error."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LoaderError(RagUpError):
|
|
49
|
+
"""Raised when a document cannot be loaded."""
|
|
50
|
+
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ChunkingError(RagUpError):
|
|
55
|
+
"""Raised when chunk generation fails."""
|
|
56
|
+
|
|
57
|
+
pass
|
ragup/llm/__init__.py
ADDED
ragup/llm/answer.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini answer generation for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from litellm import completion
|
|
8
|
+
|
|
9
|
+
from .prompt import build_prompt
|
|
10
|
+
from ..exceptions import LLMError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_GEMINI_MODEL = "gemini/gemini-2.5-flash"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_answer(
|
|
17
|
+
question: str,
|
|
18
|
+
context: list[dict],
|
|
19
|
+
api_key: str,
|
|
20
|
+
) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Generate an answer using Gemini.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if not api_key:
|
|
26
|
+
raise LLMError(
|
|
27
|
+
"A Gemini API key is required."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
prompt = build_prompt(
|
|
31
|
+
question=question,
|
|
32
|
+
context=context,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
|
|
37
|
+
response = completion(
|
|
38
|
+
model=DEFAULT_GEMINI_MODEL,
|
|
39
|
+
api_key=api_key,
|
|
40
|
+
messages=[
|
|
41
|
+
{
|
|
42
|
+
"role": "user",
|
|
43
|
+
"content": prompt,
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
temperature=0.0,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return response.choices[0].message.content
|
|
50
|
+
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
raise LLMError(
|
|
53
|
+
"Failed to generate an answer from Gemini."
|
|
54
|
+
) from exc
|
ragup/llm/prompt.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt builder for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_prompt(
|
|
9
|
+
question: str,
|
|
10
|
+
context: list[dict],
|
|
11
|
+
) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Build the prompt sent to Gemini.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
context_text = "\n\n".join(
|
|
17
|
+
item["text"] for item in context
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return f"""
|
|
21
|
+
You are a helpful AI assistant.
|
|
22
|
+
|
|
23
|
+
Answer the user's question ONLY using the provided context.
|
|
24
|
+
|
|
25
|
+
If the answer cannot be found in the context, reply:
|
|
26
|
+
|
|
27
|
+
"I could not find the answer in the provided documents."
|
|
28
|
+
|
|
29
|
+
Context:
|
|
30
|
+
{context_text}
|
|
31
|
+
|
|
32
|
+
Question:
|
|
33
|
+
{question}
|
|
34
|
+
|
|
35
|
+
Answer:
|
|
36
|
+
""".strip()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loader factory for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .base import BaseLoader
|
|
8
|
+
from .pdf_loader import PDFLoader
|
|
9
|
+
from .txt_loader import TXTLoader
|
|
10
|
+
from .json_loader import JSONLoader
|
|
11
|
+
|
|
12
|
+
from ..exceptions import UnsupportedFileTypeError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_LOADER_MAP = {
|
|
16
|
+
".pdf": PDFLoader,
|
|
17
|
+
".txt": TXTLoader,
|
|
18
|
+
".json": JSONLoader,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_loader(file_path: str | Path) -> BaseLoader:
|
|
23
|
+
"""
|
|
24
|
+
Return the appropriate loader for the given file.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
file_path : str | Path
|
|
29
|
+
Path to the document.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
BaseLoader
|
|
34
|
+
Loader instance.
|
|
35
|
+
|
|
36
|
+
Raises
|
|
37
|
+
------
|
|
38
|
+
UnsupportedFileTypeError
|
|
39
|
+
If the file extension is not supported.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
path = Path(file_path)
|
|
43
|
+
|
|
44
|
+
extension = path.suffix.lower()
|
|
45
|
+
|
|
46
|
+
loader = _LOADER_MAP.get(extension)
|
|
47
|
+
|
|
48
|
+
if loader is None:
|
|
49
|
+
raise UnsupportedFileTypeError(
|
|
50
|
+
f"Unsupported file type: {extension}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return loader(path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"BaseLoader",
|
|
58
|
+
"PDFLoader",
|
|
59
|
+
"TXTLoader",
|
|
60
|
+
"JSONLoader",
|
|
61
|
+
"get_loader",
|
|
62
|
+
]
|
ragup/loaders/base.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base loader interface for all document loaders.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseLoader(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Base class for all document loaders.
|
|
14
|
+
|
|
15
|
+
Every loader should return the document as plain text.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, file_path: str | Path):
|
|
19
|
+
self.file_path = Path(file_path)
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def load(self) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Load the document and return its text.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
str
|
|
29
|
+
Extracted document text.
|
|
30
|
+
"""
|
|
31
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON document loader.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from .base import BaseLoader
|
|
10
|
+
from ..exceptions import LoaderError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JSONLoader(BaseLoader):
|
|
14
|
+
"""
|
|
15
|
+
Loader for JSON documents.
|
|
16
|
+
|
|
17
|
+
The JSON content is converted into a formatted string
|
|
18
|
+
suitable for chunking and embedding.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def load(self) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Load a JSON file and return it as formatted text.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
str
|
|
28
|
+
JSON represented as readable text.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
33
|
+
data = json.load(f)
|
|
34
|
+
|
|
35
|
+
return json.dumps(
|
|
36
|
+
data,
|
|
37
|
+
indent=2,
|
|
38
|
+
ensure_ascii=False
|
|
39
|
+
).strip()
|
|
40
|
+
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
raise LoaderError(
|
|
43
|
+
f"Failed to load JSON: {self.file_path}"
|
|
44
|
+
) from exc
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDF document loader.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import fitz # PyMuPDF
|
|
8
|
+
|
|
9
|
+
from .base import BaseLoader
|
|
10
|
+
from ..exceptions import LoaderError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PDFLoader(BaseLoader):
|
|
14
|
+
"""
|
|
15
|
+
Loader for PDF documents.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def load(self) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Load a PDF and return its text.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
str
|
|
25
|
+
Extracted text from all pages.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
document = fitz.open(self.file_path)
|
|
30
|
+
|
|
31
|
+
pages = []
|
|
32
|
+
|
|
33
|
+
for page in document:
|
|
34
|
+
pages.append(page.get_text())
|
|
35
|
+
|
|
36
|
+
document.close()
|
|
37
|
+
|
|
38
|
+
return "\n".join(pages).strip()
|
|
39
|
+
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
raise LoaderError(
|
|
42
|
+
f"Failed to load PDF: {self.file_path}"
|
|
43
|
+
) from exc
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TXT document loader.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .base import BaseLoader
|
|
8
|
+
from ..exceptions import LoaderError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TXTLoader(BaseLoader):
|
|
12
|
+
"""
|
|
13
|
+
Loader for plain text documents.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def load(self) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Load a text file and return its contents.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
str
|
|
23
|
+
Text file contents.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return self.file_path.read_text(
|
|
28
|
+
encoding="utf-8"
|
|
29
|
+
).strip()
|
|
30
|
+
|
|
31
|
+
except UnicodeDecodeError:
|
|
32
|
+
try:
|
|
33
|
+
return self.file_path.read_text(
|
|
34
|
+
encoding="latin-1"
|
|
35
|
+
).strip()
|
|
36
|
+
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
raise LoaderError(
|
|
39
|
+
f"Failed to load TXT: {self.file_path}"
|
|
40
|
+
) from exc
|
|
41
|
+
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
raise LoaderError(
|
|
44
|
+
f"Failed to load TXT: {self.file_path}"
|
|
45
|
+
) from exc
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic retriever for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from ..embeddings import Embedder
|
|
8
|
+
from ..vectordb import VectorDBManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Retriever:
|
|
12
|
+
"""
|
|
13
|
+
Handles semantic retrieval.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
embedder: Embedder,
|
|
19
|
+
vectordb: VectorDBManager,
|
|
20
|
+
):
|
|
21
|
+
|
|
22
|
+
self.embedder = embedder
|
|
23
|
+
self.vectordb = vectordb
|
|
24
|
+
|
|
25
|
+
def retrieve(
|
|
26
|
+
self,
|
|
27
|
+
query: str,
|
|
28
|
+
top_k: int = 5,
|
|
29
|
+
) -> dict:
|
|
30
|
+
"""
|
|
31
|
+
Retrieve relevant document chunks.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
query_embedding = self.embedder.embed_query(
|
|
35
|
+
query
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return self.vectordb.search(
|
|
39
|
+
embedding=query_embedding,
|
|
40
|
+
top_k=top_k,
|
|
41
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search result formatter for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_search_results(results: dict) -> list[dict]:
|
|
9
|
+
"""
|
|
10
|
+
Convert the raw ChromaDB response into a clean list of results.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
results : dict
|
|
15
|
+
Raw response returned by ChromaDB.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
list[dict]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if not results:
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
documents = results.get("documents", [[]])[0]
|
|
26
|
+
metadatas = results.get("metadatas", [[]])[0]
|
|
27
|
+
distances = results.get("distances", [[]])[0]
|
|
28
|
+
ids = results.get("ids", [[]])[0]
|
|
29
|
+
|
|
30
|
+
formatted = []
|
|
31
|
+
|
|
32
|
+
for idx, document in enumerate(documents):
|
|
33
|
+
|
|
34
|
+
metadata = (
|
|
35
|
+
metadatas[idx]
|
|
36
|
+
if idx < len(metadatas)
|
|
37
|
+
else {}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
distance = (
|
|
41
|
+
distances[idx]
|
|
42
|
+
if idx < len(distances)
|
|
43
|
+
else None
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
result = {
|
|
47
|
+
"id": ids[idx] if idx < len(ids) else None,
|
|
48
|
+
"text": document,
|
|
49
|
+
"metadata": metadata,
|
|
50
|
+
"distance": distance,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
formatted.append(result)
|
|
54
|
+
|
|
55
|
+
return formatted
|
ragup/utils/__init__.py
ADDED
|
File without changes
|
ragup/utils/helpers.py
ADDED
|
File without changes
|
ragup/utils/logger.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_LOGGER_NAME = "ragup"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_logger(verbose: bool = False) -> logging.Logger:
|
|
13
|
+
"""
|
|
14
|
+
Returns a configured logger instance.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
verbose : bool
|
|
19
|
+
Enable DEBUG logging.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
logging.Logger
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(_LOGGER_NAME)
|
|
27
|
+
|
|
28
|
+
if logger.handlers:
|
|
29
|
+
return logger
|
|
30
|
+
|
|
31
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
32
|
+
|
|
33
|
+
logger.setLevel(level)
|
|
34
|
+
|
|
35
|
+
handler = logging.StreamHandler()
|
|
36
|
+
|
|
37
|
+
formatter = logging.Formatter(
|
|
38
|
+
"[%(levelname)s] %(message)s"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
handler.setFormatter(formatter)
|
|
42
|
+
|
|
43
|
+
logger.addHandler(handler)
|
|
44
|
+
|
|
45
|
+
logger.propagate = False
|
|
46
|
+
|
|
47
|
+
return logger
|
|
File without changes
|
ragup/vectordb/chroma.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChromaDB wrapper for RagUp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import chromadb
|
|
8
|
+
from chromadb.config import Settings
|
|
9
|
+
|
|
10
|
+
from ..constants import (
|
|
11
|
+
DEFAULT_COLLECTION_NAME,
|
|
12
|
+
DEFAULT_VECTOR_DB_DIR,
|
|
13
|
+
)
|
|
14
|
+
from ..exceptions import VectorDBError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChromaVectorDB:
|
|
18
|
+
"""
|
|
19
|
+
Wrapper around ChromaDB.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
db_path: str = str(DEFAULT_VECTOR_DB_DIR),
|
|
25
|
+
collection_name: str = DEFAULT_COLLECTION_NAME,
|
|
26
|
+
):
|
|
27
|
+
|
|
28
|
+
self.client = chromadb.PersistentClient(
|
|
29
|
+
path=db_path,
|
|
30
|
+
settings=Settings(
|
|
31
|
+
anonymized_telemetry=False
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self.collection = self.client.get_or_create_collection(
|
|
36
|
+
name=collection_name
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def add(
|
|
40
|
+
self,
|
|
41
|
+
ids: list[str],
|
|
42
|
+
documents: list[str],
|
|
43
|
+
embeddings: list[list[float]],
|
|
44
|
+
metadatas: list[dict],
|
|
45
|
+
) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Store document chunks.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
|
|
52
|
+
self.collection.add(
|
|
53
|
+
ids=ids,
|
|
54
|
+
documents=documents,
|
|
55
|
+
embeddings=embeddings,
|
|
56
|
+
metadatas=metadatas,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
raise VectorDBError(
|
|
61
|
+
"Failed to add documents to ChromaDB."
|
|
62
|
+
) from exc
|
|
63
|
+
|
|
64
|
+
def search(
|
|
65
|
+
self,
|
|
66
|
+
embedding: list[float],
|
|
67
|
+
top_k: int = 5,
|
|
68
|
+
) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
Semantic search.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
|
|
75
|
+
return self.collection.query(
|
|
76
|
+
query_embeddings=[embedding],
|
|
77
|
+
n_results=top_k,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
raise VectorDBError(
|
|
82
|
+
"Failed to query ChromaDB."
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
def count(self) -> int:
|
|
86
|
+
|
|
87
|
+
return self.collection.count()
|
|
88
|
+
|
|
89
|
+
def reset(self) -> None:
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
|
|
93
|
+
ids = self.collection.get()["ids"]
|
|
94
|
+
|
|
95
|
+
if ids:
|
|
96
|
+
self.collection.delete(ids=ids)
|
|
97
|
+
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
raise VectorDBError(
|
|
100
|
+
"Failed to reset ChromaDB."
|
|
101
|
+
) from exc
|