ragit 0.0.1__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,146 @@
1
+ #
2
+ # Copyright RODMENA LIMITED 2025
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ """
6
+ Base provider interfaces for LLM and Embedding providers.
7
+
8
+ These abstract classes define the interface that all providers must implement,
9
+ making it easy to add new providers (Gemini, Claude, OpenAI, etc.)
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+
17
+ @dataclass
18
+ class LLMResponse:
19
+ """Response from an LLM call."""
20
+ text: str
21
+ model: str
22
+ provider: str
23
+ usage: Optional[dict] = None
24
+
25
+
26
+ @dataclass
27
+ class EmbeddingResponse:
28
+ """Response from an embedding call."""
29
+ embedding: list[float]
30
+ model: str
31
+ provider: str
32
+ dimensions: int
33
+
34
+
35
+ class BaseLLMProvider(ABC):
36
+ """
37
+ Abstract base class for LLM providers.
38
+
39
+ Implement this to add support for new LLM providers like Gemini, Claude, etc.
40
+ """
41
+
42
+ @property
43
+ @abstractmethod
44
+ def provider_name(self) -> str:
45
+ """Return the provider name (e.g., 'ollama', 'gemini', 'claude')."""
46
+ pass
47
+
48
+ @abstractmethod
49
+ def generate(
50
+ self,
51
+ prompt: str,
52
+ model: str,
53
+ system_prompt: Optional[str] = None,
54
+ temperature: float = 0.7,
55
+ max_tokens: Optional[int] = None,
56
+ ) -> LLMResponse:
57
+ """
58
+ Generate text from the LLM.
59
+
60
+ Parameters
61
+ ----------
62
+ prompt : str
63
+ The user prompt/query.
64
+ model : str
65
+ Model identifier (e.g., 'llama3', 'qwen3-vl:235b-instruct-cloud').
66
+ system_prompt : str, optional
67
+ System prompt for context/instructions.
68
+ temperature : float
69
+ Sampling temperature (0.0 to 1.0).
70
+ max_tokens : int, optional
71
+ Maximum tokens to generate.
72
+
73
+ Returns
74
+ -------
75
+ LLMResponse
76
+ The generated response.
77
+ """
78
+ pass
79
+
80
+ @abstractmethod
81
+ def is_available(self) -> bool:
82
+ """Check if the provider is available and configured."""
83
+ pass
84
+
85
+
86
+ class BaseEmbeddingProvider(ABC):
87
+ """
88
+ Abstract base class for embedding providers.
89
+
90
+ Implement this to add support for new embedding providers.
91
+ """
92
+
93
+ @property
94
+ @abstractmethod
95
+ def provider_name(self) -> str:
96
+ """Return the provider name."""
97
+ pass
98
+
99
+ @property
100
+ @abstractmethod
101
+ def dimensions(self) -> int:
102
+ """Return the embedding dimensions for the current model."""
103
+ pass
104
+
105
+ @abstractmethod
106
+ def embed(self, text: str, model: str) -> EmbeddingResponse:
107
+ """
108
+ Generate embedding for text.
109
+
110
+ Parameters
111
+ ----------
112
+ text : str
113
+ Text to embed.
114
+ model : str
115
+ Model identifier (e.g., 'nomic-embed-text').
116
+
117
+ Returns
118
+ -------
119
+ EmbeddingResponse
120
+ The embedding response.
121
+ """
122
+ pass
123
+
124
+ @abstractmethod
125
+ def embed_batch(self, texts: list[str], model: str) -> list[EmbeddingResponse]:
126
+ """
127
+ Generate embeddings for multiple texts.
128
+
129
+ Parameters
130
+ ----------
131
+ texts : list[str]
132
+ Texts to embed.
133
+ model : str
134
+ Model identifier.
135
+
136
+ Returns
137
+ -------
138
+ list[EmbeddingResponse]
139
+ List of embedding responses.
140
+ """
141
+ pass
142
+
143
+ @abstractmethod
144
+ def is_available(self) -> bool:
145
+ """Check if the provider is available and configured."""
146
+ pass
@@ -0,0 +1,250 @@
1
+ #
2
+ # Copyright RODMENA LIMITED 2025
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ """
6
+ Ollama provider for LLM and Embedding operations.
7
+
8
+ This provider connects to a local or remote Ollama server.
9
+ """
10
+
11
+ import requests
12
+ from typing import Optional
13
+
14
+ from ragit.providers.base import (
15
+ BaseLLMProvider,
16
+ BaseEmbeddingProvider,
17
+ LLMResponse,
18
+ EmbeddingResponse,
19
+ )
20
+
21
+
22
+ class OllamaProvider(BaseLLMProvider, BaseEmbeddingProvider):
23
+ """
24
+ Ollama provider for both LLM and Embedding operations.
25
+
26
+ Parameters
27
+ ----------
28
+ base_url : str
29
+ Ollama server URL (default: http://localhost:11434)
30
+ timeout : int
31
+ Request timeout in seconds (default: 120)
32
+
33
+ Examples
34
+ --------
35
+ >>> provider = OllamaProvider()
36
+ >>> response = provider.generate("What is RAG?", model="llama3")
37
+ >>> print(response.text)
38
+
39
+ >>> embedding = provider.embed("Hello world", model="nomic-embed-text")
40
+ >>> print(len(embedding.embedding))
41
+ """
42
+
43
+ # Known embedding model dimensions
44
+ EMBEDDING_DIMENSIONS = {
45
+ "nomic-embed-text": 768,
46
+ "nomic-embed-text:latest": 768,
47
+ "mxbai-embed-large": 1024,
48
+ "all-minilm": 384,
49
+ "snowflake-arctic-embed": 1024,
50
+ }
51
+
52
+ def __init__(
53
+ self,
54
+ base_url: str = "http://localhost:11434",
55
+ timeout: int = 120,
56
+ ):
57
+ self.base_url = base_url.rstrip("/")
58
+ self.timeout = timeout
59
+ self._current_embed_model: Optional[str] = None
60
+ self._current_dimensions: int = 768 # default
61
+
62
+ @property
63
+ def provider_name(self) -> str:
64
+ return "ollama"
65
+
66
+ @property
67
+ def dimensions(self) -> int:
68
+ return self._current_dimensions
69
+
70
+ def is_available(self) -> bool:
71
+ """Check if Ollama server is reachable."""
72
+ try:
73
+ response = requests.get(f"{self.base_url}/api/tags", timeout=5)
74
+ return response.status_code == 200
75
+ except requests.RequestException:
76
+ return False
77
+
78
+ def list_models(self) -> list[dict]:
79
+ """List available models on the Ollama server."""
80
+ try:
81
+ response = requests.get(f"{self.base_url}/api/tags", timeout=10)
82
+ response.raise_for_status()
83
+ return response.json().get("models", [])
84
+ except requests.RequestException as e:
85
+ raise ConnectionError(f"Failed to list Ollama models: {e}") from e
86
+
87
+ def generate(
88
+ self,
89
+ prompt: str,
90
+ model: str,
91
+ system_prompt: Optional[str] = None,
92
+ temperature: float = 0.7,
93
+ max_tokens: Optional[int] = None,
94
+ ) -> LLMResponse:
95
+ """Generate text using Ollama."""
96
+ payload = {
97
+ "model": model,
98
+ "prompt": prompt,
99
+ "stream": False,
100
+ "options": {
101
+ "temperature": temperature,
102
+ },
103
+ }
104
+
105
+ if system_prompt:
106
+ payload["system"] = system_prompt
107
+
108
+ if max_tokens:
109
+ payload["options"]["num_predict"] = max_tokens
110
+
111
+ try:
112
+ response = requests.post(
113
+ f"{self.base_url}/api/generate",
114
+ json=payload,
115
+ timeout=self.timeout,
116
+ )
117
+ response.raise_for_status()
118
+ data = response.json()
119
+
120
+ return LLMResponse(
121
+ text=data.get("response", ""),
122
+ model=model,
123
+ provider=self.provider_name,
124
+ usage={
125
+ "prompt_tokens": data.get("prompt_eval_count"),
126
+ "completion_tokens": data.get("eval_count"),
127
+ "total_duration": data.get("total_duration"),
128
+ },
129
+ )
130
+ except requests.RequestException as e:
131
+ raise ConnectionError(f"Ollama generate failed: {e}") from e
132
+
133
+ def embed(self, text: str, model: str) -> EmbeddingResponse:
134
+ """Generate embedding using Ollama."""
135
+ self._current_embed_model = model
136
+ self._current_dimensions = self.EMBEDDING_DIMENSIONS.get(model, 768)
137
+
138
+ try:
139
+ response = requests.post(
140
+ f"{self.base_url}/api/embed",
141
+ json={"model": model, "input": text},
142
+ timeout=self.timeout,
143
+ )
144
+ response.raise_for_status()
145
+ data = response.json()
146
+
147
+ embedding = data.get("embeddings", [[]])[0]
148
+ if not embedding:
149
+ raise ValueError("Empty embedding returned from Ollama")
150
+
151
+ # Update dimensions from actual response
152
+ self._current_dimensions = len(embedding)
153
+
154
+ return EmbeddingResponse(
155
+ embedding=embedding,
156
+ model=model,
157
+ provider=self.provider_name,
158
+ dimensions=len(embedding),
159
+ )
160
+ except requests.RequestException as e:
161
+ raise ConnectionError(f"Ollama embed failed: {e}") from e
162
+
163
+ def embed_batch(self, texts: list[str], model: str) -> list[EmbeddingResponse]:
164
+ """Generate embeddings for multiple texts."""
165
+ self._current_embed_model = model
166
+ self._current_dimensions = self.EMBEDDING_DIMENSIONS.get(model, 768)
167
+
168
+ try:
169
+ response = requests.post(
170
+ f"{self.base_url}/api/embed",
171
+ json={"model": model, "input": texts},
172
+ timeout=self.timeout,
173
+ )
174
+ response.raise_for_status()
175
+ data = response.json()
176
+
177
+ embeddings = data.get("embeddings", [])
178
+ if embeddings:
179
+ self._current_dimensions = len(embeddings[0])
180
+
181
+ return [
182
+ EmbeddingResponse(
183
+ embedding=emb,
184
+ model=model,
185
+ provider=self.provider_name,
186
+ dimensions=len(emb),
187
+ )
188
+ for emb in embeddings
189
+ ]
190
+ except requests.RequestException as e:
191
+ raise ConnectionError(f"Ollama batch embed failed: {e}") from e
192
+
193
+ def chat(
194
+ self,
195
+ messages: list[dict],
196
+ model: str,
197
+ temperature: float = 0.7,
198
+ max_tokens: Optional[int] = None,
199
+ ) -> LLMResponse:
200
+ """
201
+ Chat completion using Ollama.
202
+
203
+ Parameters
204
+ ----------
205
+ messages : list[dict]
206
+ List of messages with 'role' and 'content' keys.
207
+ model : str
208
+ Model identifier.
209
+ temperature : float
210
+ Sampling temperature.
211
+ max_tokens : int, optional
212
+ Maximum tokens to generate.
213
+
214
+ Returns
215
+ -------
216
+ LLMResponse
217
+ The generated response.
218
+ """
219
+ payload = {
220
+ "model": model,
221
+ "messages": messages,
222
+ "stream": False,
223
+ "options": {
224
+ "temperature": temperature,
225
+ },
226
+ }
227
+
228
+ if max_tokens:
229
+ payload["options"]["num_predict"] = max_tokens
230
+
231
+ try:
232
+ response = requests.post(
233
+ f"{self.base_url}/api/chat",
234
+ json=payload,
235
+ timeout=self.timeout,
236
+ )
237
+ response.raise_for_status()
238
+ data = response.json()
239
+
240
+ return LLMResponse(
241
+ text=data.get("message", {}).get("content", ""),
242
+ model=model,
243
+ provider=self.provider_name,
244
+ usage={
245
+ "prompt_tokens": data.get("prompt_eval_count"),
246
+ "completion_tokens": data.get("eval_count"),
247
+ },
248
+ )
249
+ except requests.RequestException as e:
250
+ raise ConnectionError(f"Ollama chat failed: {e}") from e
@@ -0,0 +1,105 @@
1
+ #
2
+ # Copyright RODMENA LIMITED 2025
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ """
6
+ Ragit utilities module.
7
+ """
8
+
9
+ from collections import deque
10
+ from collections.abc import Hashable
11
+ from datetime import datetime
12
+ from math import floor
13
+ from typing import Sequence
14
+
15
+ import pandas as pd
16
+
17
+
18
+ def get_hashable_repr(dct: dict):
19
+ """
20
+ Returns a hashable representation of the provided dictionary.
21
+ """
22
+ queue = deque((k, v, 0, None) for k, v in dct.items())
23
+ dict_unpacked = []
24
+ while queue:
25
+ key, val, lvl, p_ref = queue.pop()
26
+ if hasattr(val, "items"): # we have a nested dict
27
+ dict_unpacked.append((key, "+", lvl, p_ref))
28
+ if hash(key) != p_ref:
29
+ lvl += 1
30
+ queue.extendleft((k, v, lvl, hash(key)) for k, v in val.items())
31
+ elif isinstance(val, Hashable):
32
+ dict_unpacked.append((key, val, lvl, p_ref))
33
+ elif isinstance(val, Sequence):
34
+ dict_unpacked.append((key, "+", lvl, p_ref))
35
+ queue.extendleft((key, vv, floor(lvl) + ind * 0.01, hash(key)) for ind, vv in enumerate(val, 1))
36
+ else:
37
+ raise ValueError(f"Unsupported type in dict: {type(val)}")
38
+
39
+ return tuple(sorted(dict_unpacked, key=lambda it: (it[2], it[0])))
40
+
41
+
42
+ def remove_duplicates(items: list[dict]) -> list[dict]:
43
+ """
44
+ Deduplicates list of provided dictionary items.
45
+
46
+ Parameters
47
+ ----------
48
+ items : list[dict]
49
+ List of items to deduplicate.
50
+
51
+ Returns
52
+ -------
53
+ list[dict]
54
+ A deduplicated list of input items.
55
+ """
56
+ duplicate_tracker = set()
57
+ deduplicated_items = []
58
+ for ind, elem in enumerate(map(get_hashable_repr, items)):
59
+ if elem not in duplicate_tracker:
60
+ duplicate_tracker.add(elem)
61
+ deduplicated_items.append(items[ind])
62
+ return deduplicated_items
63
+
64
+
65
+ def handle_missing_values_in_combinations(df: pd.DataFrame) -> pd.DataFrame:
66
+ """
67
+ Handle missing values in experiment data combinations.
68
+
69
+ Parameters
70
+ ----------
71
+ df : pd.DataFrame
72
+ Experiment data with combinations being explored.
73
+
74
+ Returns
75
+ -------
76
+ pd.DataFrame
77
+ Data with NaN values properly replaced.
78
+ """
79
+ if "chunk_overlap" in df.columns:
80
+ df["chunk_overlap"] = df["chunk_overlap"].map(lambda el: 0 if pd.isna(el) else el)
81
+
82
+ return df
83
+
84
+
85
+ def datetime_str_to_epoch_time(timestamp: str | int) -> str | int:
86
+ """
87
+ Convert datetime string to epoch time.
88
+
89
+ Parameters
90
+ ----------
91
+ timestamp : str | int
92
+ Either a datetime string or a unix timestamp.
93
+
94
+ Returns
95
+ -------
96
+ int
97
+ Unix timestamp or -1 if parsing fails.
98
+ """
99
+ if not isinstance(timestamp, str):
100
+ return timestamp
101
+ try:
102
+ iso_parseable = datetime.fromisoformat(timestamp)
103
+ except ValueError:
104
+ return -1
105
+ return int(iso_parseable.timestamp())
ragit/version.py ADDED
@@ -0,0 +1,5 @@
1
+ #
2
+ # Copyright RODMENA LIMITED 2025
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ __version__ = "0.0.1"
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: ragit
3
+ Version: 0.0.1
4
+ Summary: Automatic RAG Pattern Optimization Engine
5
+ Author: RODMENA LIMITED
6
+ Maintainer-email: RODMENA LIMITED <info@rodmena.com>
7
+ License-Expression: Apache-2.0
8
+ Project-URL: Homepage, https://github.com/rodmena-limited/ragit
9
+ Project-URL: Repository, https://github.com/rodmena-limited/ragit
10
+ Project-URL: Issues, https://github.com/rodmena-limited/ragit/issues
11
+ Keywords: AI,RAG,LLM,GenAI,Optimization,Ollama
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Requires-Python: <3.14,>=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.31.0
22
+ Requires-Dist: numpy>=1.26.0
23
+ Requires-Dist: pandas>=2.2.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Requires-Dist: scikit-learn>=1.5.0
26
+ Requires-Dist: tqdm>=4.66.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: ragit[test]; extra == "dev"
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: pytest-cov; extra == "dev"
31
+ Requires-Dist: black; extra == "dev"
32
+ Requires-Dist: pylint; extra == "dev"
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest; extra == "test"
35
+ Requires-Dist: pytest-cov; extra == "test"
36
+ Requires-Dist: pytest-mock; extra == "test"
37
+ Dynamic: license-file
38
+
39
+ # ragit
40
+
41
+ Automatic RAG (Retrieval-Augmented Generation) hyperparameter optimization engine.
42
+
43
+ ## What it does
44
+
45
+ ragit finds the best configuration for your RAG pipeline by testing different combinations of:
46
+ - Chunk sizes and overlaps
47
+ - Number of retrieved chunks
48
+ - Embedding models
49
+ - LLM models
50
+
51
+ You provide documents and benchmark questions, ragit evaluates different configurations and returns the best one.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install ragit
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ```python
62
+ from ragit import RagitExperiment, Document, BenchmarkQuestion
63
+
64
+ documents = [
65
+ Document(id="doc1", content="Your document text here..."),
66
+ ]
67
+
68
+ benchmark = [
69
+ BenchmarkQuestion(
70
+ question="A question about your documents?",
71
+ ground_truth="The expected answer."
72
+ ),
73
+ ]
74
+
75
+ experiment = RagitExperiment(documents, benchmark)
76
+ results = experiment.run()
77
+
78
+ print(results[0]) # Best configuration
79
+ ```
80
+
81
+ ## License
82
+
83
+ Apache-2.0 - RODMENA LIMITED
@@ -0,0 +1,15 @@
1
+ ragit/__init__.py,sha256=kAHylscZh8-O8YdyO_5x0Wry3yTMBu0aUAPNV6z9tng,1539
2
+ ragit/version.py,sha256=0QBeKFCe0Qwtua_u4nCcHvzZyfARcJVV-zvY4l2wYwA,97
3
+ ragit/core/__init__.py,sha256=j53PFfoSMXwSbK1rRHpMbo8mX2i4R1LJ5kvTxBd7-0w,100
4
+ ragit/core/experiment/__init__.py,sha256=_olxvo2Mf6fcBNhUf5YA-v2KIMb5W3UQA2hl30HLgRw,452
5
+ ragit/core/experiment/experiment.py,sha256=ykbLqmFMsQ7gqcHqzJ8KAEKe-BQFur6aVw5imZlKJ80,14769
6
+ ragit/core/experiment/results.py,sha256=UzPSVlJTfOgu6SQhVIVT8IzN4Eo7tQQ9oNGUVANCTTU,3453
7
+ ragit/providers/__init__.py,sha256=znyg13wFgcNKVT7YYimRRwu3nvdrrQsVA251fY8LOX4,435
8
+ ragit/providers/base.py,sha256=F64KxlQh9Mq1bv842y9yUcAiWdjhPxiYVQoVXqk7ULs,3428
9
+ ragit/providers/ollama.py,sha256=AM4_DeWv7lS78KlIEh3k8XbXOf8mMazpfjKZYIN6t4k,7637
10
+ ragit/utils/__init__.py,sha256=ZTlohzMF_3hMLZDaDZ94BGvP6SdqnTjJ40AVk4S1Cl0,2867
11
+ ragit-0.0.1.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
12
+ ragit-0.0.1.dist-info/METADATA,sha256=TXd4diiW82nJaYDvixdkGLXPRi451l_LVQMjI1uIr-0,2329
13
+ ragit-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ ragit-0.0.1.dist-info/top_level.txt,sha256=pkPbG7yrw61wt9_y_xcLE2vq2a55fzockASD0yq0g4s,6
15
+ ragit-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+