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.
@@ -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
@@ -0,0 +1,9 @@
1
+ """
2
+ LLM module.
3
+ """
4
+
5
+ from .answer import generate_answer
6
+
7
+ __all__ = [
8
+ "generate_answer",
9
+ ]
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,11 @@
1
+ """
2
+ Retrieval module.
3
+ """
4
+
5
+ from .retriever import Retriever
6
+ from .search import format_search_results
7
+
8
+ __all__ = [
9
+ "Retriever",
10
+ "format_search_results",
11
+ ]
@@ -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
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
@@ -0,0 +1,9 @@
1
+ """
2
+ Vector database module.
3
+ """
4
+
5
+ from .manager import VectorDBManager
6
+
7
+ __all__ = [
8
+ "VectorDBManager",
9
+ ]
@@ -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