ezrag-engine 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.
- ezrag/__init__.py +7 -0
- ezrag/engine.py +349 -0
- ezrag_engine-0.1.0.dist-info/METADATA +117 -0
- ezrag_engine-0.1.0.dist-info/RECORD +6 -0
- ezrag_engine-0.1.0.dist-info/WHEEL +5 -0
- ezrag_engine-0.1.0.dist-info/top_level.txt +1 -0
ezrag/__init__.py
ADDED
ezrag/engine.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""ezrag.engine — hidden orchestration of the EZRAG pipeline.
|
|
2
|
+
|
|
3
|
+
Everything heavy lives here and stays invisible to developers:
|
|
4
|
+
text chunking, embedding generation, in-memory similarity search,
|
|
5
|
+
context bundling and LLM query execution. Consumers only touch the
|
|
6
|
+
public surface exposed by ``ezrag.EZRAG``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os as _os
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path as _Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import numpy as np
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
np = None # type: ignore
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from huggingface_hub import InferenceClient
|
|
25
|
+
except ImportError: # pragma: no cover
|
|
26
|
+
InferenceClient = None # type: ignore
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import litellm
|
|
30
|
+
except ImportError: # pragma: no cover
|
|
31
|
+
litellm = None # type: ignore
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("ezrag")
|
|
34
|
+
|
|
35
|
+
__all__ = ["EZRAG", "Chunk"]
|
|
36
|
+
|
|
37
|
+
DEFAULT_EMBEDDING_MODEL = "all-MiniLM-L6-v2"
|
|
38
|
+
DEFAULT_LLM = _os.getenv("EZRAG_LLM", "mistralai/Mistral-7B-Instruct-v0.2")
|
|
39
|
+
|
|
40
|
+
_SENTENCE_BOUNDARY = re.compile(r"(?<=[.!?。!?])\s+")
|
|
41
|
+
_WHITESPACE = re.compile(r"\s+")
|
|
42
|
+
|
|
43
|
+
_SYSTEM_PROMPT = (
|
|
44
|
+
"You are a precise, grounded assistant. Answer ONLY from the provided "
|
|
45
|
+
"CONTEXT delimited by <context></context>. Cite every fact with source "
|
|
46
|
+
"tags like [1]. If the CONTEXT is empty or clearly cannot answer the "
|
|
47
|
+
"question, reply exactly with 'I do not have enough information to "
|
|
48
|
+
"answer.' Never invent facts."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalize_text(text: str) -> str:
|
|
53
|
+
return _WHITESPACE.sub(" ", text).strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Chunk:
|
|
58
|
+
"""A retrievable unit of knowledge plus provenance and relevance score."""
|
|
59
|
+
|
|
60
|
+
text: str
|
|
61
|
+
score: float = 0.0
|
|
62
|
+
metadata: Dict[str, Any] = field(default_factory=dict, compare=False)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EZRAG:
|
|
66
|
+
"""Lazy, 3-line RAG: ``EZRAG().load(source).ask(question)``.
|
|
67
|
+
|
|
68
|
+
Chunking, embeddings, in-memory retrieval and LLM calls are handled
|
|
69
|
+
transparently. Nothing touches the network until data is loaded or a
|
|
70
|
+
question is asked.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
llm: Optional[str] = None,
|
|
76
|
+
embedding_model: str = DEFAULT_EMBEDDING_MODEL,
|
|
77
|
+
provider: str = "hf",
|
|
78
|
+
hf_token: Optional[str] = None,
|
|
79
|
+
top_k: int = 4,
|
|
80
|
+
min_score: float = 0.10,
|
|
81
|
+
chunk_size: int = 900,
|
|
82
|
+
chunk_overlap: int = 120,
|
|
83
|
+
max_new_tokens: int = 400,
|
|
84
|
+
temperature: float = 0.3,
|
|
85
|
+
) -> None:
|
|
86
|
+
self.llm = llm or _os.getenv("EZRAG_LLM") or DEFAULT_LLM
|
|
87
|
+
self.embedding_model = embedding_model
|
|
88
|
+
self.provider = provider
|
|
89
|
+
self.hf_token = hf_token or _os.getenv("HF_TOKEN")
|
|
90
|
+
self.top_k = top_k
|
|
91
|
+
self.min_score = min_score
|
|
92
|
+
self.chunk_size = chunk_size
|
|
93
|
+
self.chunk_overlap = chunk_overlap
|
|
94
|
+
self.max_new_tokens = max_new_tokens
|
|
95
|
+
self.temperature = temperature
|
|
96
|
+
self._encoder = None
|
|
97
|
+
self._vectors: List[np.ndarray] = []
|
|
98
|
+
self._chunks: List[Chunk] = []
|
|
99
|
+
self._cache: Dict[str, np.ndarray] = {}
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------ #
|
|
102
|
+
# Public API
|
|
103
|
+
# ------------------------------------------------------------------ #
|
|
104
|
+
|
|
105
|
+
def load(self, source: Any) -> "EZRAG":
|
|
106
|
+
"""Index a raw text, a file path, or a ``{name: text}`` mapping."""
|
|
107
|
+
if isinstance(source, dict):
|
|
108
|
+
for name, payload in source.items():
|
|
109
|
+
self._read(payload, {"source": str(name)})
|
|
110
|
+
elif isinstance(source, (list, tuple)):
|
|
111
|
+
for item in source:
|
|
112
|
+
self.load(item)
|
|
113
|
+
else:
|
|
114
|
+
self._read(source, {})
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def load_files(self, *paths: Union[str, _Path]) -> "EZRAG":
|
|
118
|
+
"""Index one or more ``.txt``/``.md`` files from disk."""
|
|
119
|
+
for path in paths:
|
|
120
|
+
self.load(path)
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def search(self, question: str, top_k: Optional[int] = None) -> List[Chunk]:
|
|
124
|
+
"""Return the most relevant chunks for a question, best first."""
|
|
125
|
+
if not self._vectors:
|
|
126
|
+
raise RuntimeError("no knowledge loaded yet - call load() first")
|
|
127
|
+
query_vec = self._encode([Chunk(text=question)])[0]
|
|
128
|
+
query_vec = query_vec / (float(np.linalg.norm(query_vec)) + 1e-9)
|
|
129
|
+
k = top_k or self.top_k
|
|
130
|
+
candidates: List[Chunk] = []
|
|
131
|
+
offset = 0
|
|
132
|
+
for block in self._vectors:
|
|
133
|
+
scores = block @ query_vec
|
|
134
|
+
for local in np.argsort(scores)[::-1][:k]:
|
|
135
|
+
score = float(scores[local])
|
|
136
|
+
if score < self.min_score:
|
|
137
|
+
continue
|
|
138
|
+
chunk = self._chunks[offset + int(local)]
|
|
139
|
+
candidates.append(
|
|
140
|
+
Chunk(text=chunk.text, score=score, metadata=dict(chunk.metadata))
|
|
141
|
+
)
|
|
142
|
+
offset += block.shape[0]
|
|
143
|
+
candidates.sort(key=lambda c: c.score, reverse=True)
|
|
144
|
+
return candidates[:k]
|
|
145
|
+
|
|
146
|
+
def ask(self, question: str, **overrides: Any) -> str:
|
|
147
|
+
"""Retrieve evidence and answer using the configured LLM."""
|
|
148
|
+
top_k = overrides.pop("top_k", self.top_k)
|
|
149
|
+
hits = self.search(question, top_k=top_k)
|
|
150
|
+
if not hits:
|
|
151
|
+
return "I do not have enough information in the loaded documents to answer this."
|
|
152
|
+
context = self._bundle(hits)
|
|
153
|
+
prompt = self._build_prompt(question, context)
|
|
154
|
+
logger.info("asking %s with %d retrieved chunks", self.llm, len(hits))
|
|
155
|
+
return self._call_llm(prompt, **overrides)
|
|
156
|
+
|
|
157
|
+
def query(self, question: str, **overrides: Any) -> str:
|
|
158
|
+
"""Alias for :meth:`ask`."""
|
|
159
|
+
return self.ask(question, **overrides)
|
|
160
|
+
|
|
161
|
+
def stats(self) -> Dict[str, Any]:
|
|
162
|
+
"""Diagnostic summary of the indexed knowledge base."""
|
|
163
|
+
return {
|
|
164
|
+
"chunks": len(self._chunks),
|
|
165
|
+
"vectors": sum(int(b.shape[0]) for b in self._vectors),
|
|
166
|
+
"embedding_model": self.embedding_model,
|
|
167
|
+
"llm": self.llm,
|
|
168
|
+
"provider": self.provider,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def __len__(self) -> int:
|
|
172
|
+
return len(self._chunks)
|
|
173
|
+
|
|
174
|
+
def __repr__(self) -> str:
|
|
175
|
+
return (
|
|
176
|
+
f"EZRAG(embedding={self.embedding_model!r}, llm={self.llm!r}, "
|
|
177
|
+
f"provider={self.provider!r}, chunks={len(self._chunks)})"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------ #
|
|
181
|
+
# Ingestion
|
|
182
|
+
# ------------------------------------------------------------------ #
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _is_path(value: str) -> bool:
|
|
186
|
+
try:
|
|
187
|
+
return _Path(value).is_file()
|
|
188
|
+
except (OSError, ValueError):
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _read(self, source: Any, metadata: Dict[str, Any]) -> List[Chunk]:
|
|
192
|
+
try:
|
|
193
|
+
if hasattr(source, "read"):
|
|
194
|
+
payload = source.read()
|
|
195
|
+
elif isinstance(source, _Path):
|
|
196
|
+
payload = source.read_text(encoding="utf-8")
|
|
197
|
+
elif isinstance(source, str) and self._is_path(source):
|
|
198
|
+
payload = _Path(source).read_text(encoding="utf-8")
|
|
199
|
+
elif isinstance(source, str):
|
|
200
|
+
payload = source
|
|
201
|
+
else:
|
|
202
|
+
raise TypeError(f"unsupported source type: {type(source).__name__}")
|
|
203
|
+
except (UnicodeDecodeError, PermissionError) as exc:
|
|
204
|
+
raise ValueError(f"cannot read source {source!r}: {exc}") from exc
|
|
205
|
+
return self._ingest(str(payload), metadata)
|
|
206
|
+
|
|
207
|
+
def _ingest(self, payload: str, metadata: Dict[str, Any]) -> List[Chunk]:
|
|
208
|
+
chunks = self._chunk_text(payload)
|
|
209
|
+
logger.info("ingesting %d chunks (%d chars)", len(chunks), len(payload))
|
|
210
|
+
for start in range(0, len(chunks), 128):
|
|
211
|
+
batch = chunks[start:start + 128]
|
|
212
|
+
vectors = self._encode(batch)
|
|
213
|
+
self._vectors.append(vectors)
|
|
214
|
+
for i, chunk in enumerate(batch):
|
|
215
|
+
chunk_meta = dict(metadata)
|
|
216
|
+
chunk_meta["chunk_index"] = len(self._chunks) + i
|
|
217
|
+
self._chunks.append(
|
|
218
|
+
Chunk(text=chunk.text, metadata=chunk_meta)
|
|
219
|
+
)
|
|
220
|
+
return chunks
|
|
221
|
+
|
|
222
|
+
def _chunk_text(self, text: str) -> List[Chunk]:
|
|
223
|
+
"""Split text into sentence-aware chunks with a sliding overlap."""
|
|
224
|
+
text = _normalize_text(text)
|
|
225
|
+
if not text:
|
|
226
|
+
return []
|
|
227
|
+
if len(text) <= self.chunk_size:
|
|
228
|
+
return [Chunk(text=text)]
|
|
229
|
+
chunks: List[Chunk] = []
|
|
230
|
+
current = ""
|
|
231
|
+
for sentence in _SENTENCE_BOUNDARY.split(text):
|
|
232
|
+
candidate = f"{current} {sentence}".strip() if current else sentence
|
|
233
|
+
if len(candidate) > self.chunk_size and current:
|
|
234
|
+
chunks.append(Chunk(text=current))
|
|
235
|
+
overlap = (
|
|
236
|
+
current[-(self.chunk_overlap or 0):] if self.chunk_overlap else ""
|
|
237
|
+
)
|
|
238
|
+
current = f"{overlap} {sentence}".strip()
|
|
239
|
+
else:
|
|
240
|
+
current = candidate
|
|
241
|
+
if current:
|
|
242
|
+
chunks.append(Chunk(text=current))
|
|
243
|
+
return chunks
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------------ #
|
|
246
|
+
# Embeddings
|
|
247
|
+
# ------------------------------------------------------------------ #
|
|
248
|
+
|
|
249
|
+
def _build_encoder(self) -> None:
|
|
250
|
+
if np is None:
|
|
251
|
+
raise RuntimeError("missing dependency: pip install numpy")
|
|
252
|
+
try:
|
|
253
|
+
from sentence_transformers import SentenceTransformer
|
|
254
|
+
except ImportError as exc:
|
|
255
|
+
raise RuntimeError(
|
|
256
|
+
"missing dependency: pip install sentence-transformers"
|
|
257
|
+
) from exc
|
|
258
|
+
logger.info("loading embedding model %s", self.embedding_model)
|
|
259
|
+
self._encoder = SentenceTransformer(self.embedding_model)
|
|
260
|
+
|
|
261
|
+
def _encode(self, chunks: List[Chunk]) -> np.ndarray:
|
|
262
|
+
"""Encode chunk texts into normalised float32 vectors (batched)."""
|
|
263
|
+
texts = [c.text for c in chunks]
|
|
264
|
+
vectors: Dict[int, np.ndarray] = {}
|
|
265
|
+
remaining: List[Tuple[int, str]] = []
|
|
266
|
+
for i, text in enumerate(texts):
|
|
267
|
+
hit = self._cache.get(text)
|
|
268
|
+
if hit is None:
|
|
269
|
+
remaining.append((i, text))
|
|
270
|
+
else:
|
|
271
|
+
vectors[i] = hit
|
|
272
|
+
if remaining:
|
|
273
|
+
if self._encoder is None:
|
|
274
|
+
self._build_encoder()
|
|
275
|
+
fresh = self._encoder.encode(
|
|
276
|
+
[text for _, text in remaining],
|
|
277
|
+
normalize_embeddings=True,
|
|
278
|
+
show_progress_bar=False,
|
|
279
|
+
)
|
|
280
|
+
for (i, text), vec in zip(remaining, np.asarray(fresh, dtype=np.float32)):
|
|
281
|
+
vectors[i] = vec
|
|
282
|
+
if len(self._cache) < 20_000:
|
|
283
|
+
self._cache[text] = vec
|
|
284
|
+
return np.stack([vectors[i] for i in range(len(texts))])
|
|
285
|
+
|
|
286
|
+
# ------------------------------------------------------------------ #
|
|
287
|
+
# Context bundling + LLM execution
|
|
288
|
+
# ------------------------------------------------------------------ #
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _bundle(hits: List[Chunk]) -> str:
|
|
292
|
+
lines = ["<context>"]
|
|
293
|
+
for i, hit in enumerate(hits, start=1):
|
|
294
|
+
src = hit.metadata.get("source", "document")
|
|
295
|
+
idx = hit.metadata.get("chunk_index", i)
|
|
296
|
+
lines.append(f"[{i}] (source: {src}, chunk #{idx}) {hit.text}")
|
|
297
|
+
lines.append(f"</context>\nBest match score: {hits[0].score:.2f}")
|
|
298
|
+
return "\n".join(lines)
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def _build_prompt(question: str, context: str) -> str:
|
|
302
|
+
return (
|
|
303
|
+
f"{_SYSTEM_PROMPT}\n\n"
|
|
304
|
+
f"QUESTION: {question}\n\n"
|
|
305
|
+
f"{context}\n\n"
|
|
306
|
+
"ANSWER:"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def _call_llm(self, prompt: str, **overrides: Any) -> str:
|
|
310
|
+
model = overrides.pop("llm", None) or self.llm
|
|
311
|
+
provider = overrides.pop("provider", self.provider)
|
|
312
|
+
max_new_tokens = overrides.pop("max_new_tokens", self.max_new_tokens)
|
|
313
|
+
temperature = overrides.pop("temperature", self.temperature)
|
|
314
|
+
|
|
315
|
+
if provider == "hf":
|
|
316
|
+
if InferenceClient is None:
|
|
317
|
+
raise RuntimeError(
|
|
318
|
+
"the 'hf' provider requires huggingface_hub: pip install huggingface_hub"
|
|
319
|
+
)
|
|
320
|
+
client = InferenceClient(model=model, token=self.hf_token)
|
|
321
|
+
output = client.text_generation(
|
|
322
|
+
prompt,
|
|
323
|
+
max_new_tokens=max_new_tokens,
|
|
324
|
+
temperature=temperature,
|
|
325
|
+
top_p=0.9,
|
|
326
|
+
stop_sequences=["</s>", "<|endoftext|>"],
|
|
327
|
+
)
|
|
328
|
+
if hasattr(output, "generated_text"):
|
|
329
|
+
return str(output.generated_text).strip()
|
|
330
|
+
return str(output).strip()
|
|
331
|
+
|
|
332
|
+
if provider == "litellm":
|
|
333
|
+
if litellm is None:
|
|
334
|
+
raise RuntimeError(
|
|
335
|
+
"the 'litellm' provider requires litellm: pip install litellm"
|
|
336
|
+
)
|
|
337
|
+
response = litellm.completion(
|
|
338
|
+
model=model,
|
|
339
|
+
messages=[{"role": "user", "content": prompt}],
|
|
340
|
+
temperature=temperature,
|
|
341
|
+
max_tokens=max_new_tokens,
|
|
342
|
+
)
|
|
343
|
+
return str(response["choices"][0]["message"]["content"]).strip()
|
|
344
|
+
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"unknown provider {provider!r}; use 'hf' (Hugging Face Serverless) "
|
|
347
|
+
"or 'litellm' (any OpenAI-compatible gateway)"
|
|
348
|
+
)
|
|
349
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ezrag-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A 3-line, zero-boilerplate, cloud-ready RAG pipeline for Python and Hugging Face Spaces.
|
|
5
|
+
Author-email: Oguru Vinay Reddy <Vinayreddy.ace@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://huggingface.co/
|
|
8
|
+
Project-URL: Repository, https://github.com/<your-github-username>/ezrag
|
|
9
|
+
Project-URL: Documentation, https://github.com/<your-github-username>/ezrag#readme
|
|
10
|
+
Keywords: rag,llm,retrieval-augmented-generation,huggingface,embeddings,vector-search
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: sentence-transformers>=3.0.0
|
|
24
|
+
Requires-Dist: huggingface_hub>=0.23.0
|
|
25
|
+
Requires-Dist: numpy>=1.26.0
|
|
26
|
+
Requires-Dist: requests>=2.31.0
|
|
27
|
+
Requires-Dist: litellm>=1.40.0
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+
# **ezrag** — 3-Line, Zero-Boilerplate, Cloud-Ready RAG
|
|
32
|
+
|
|
33
|
+
**A production-grade Retrieval-Augmented Generation framework for Python and Hugging Face Spaces.**
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why ezrag?
|
|
44
|
+
|
|
45
|
+
Most RAG stacks take hours to wire up: embedding models, vector stores, chunkers,
|
|
46
|
+
prompts, and LLM clients. ezrag hides all of that behind one class, so you go from
|
|
47
|
+
an empty folder to a **grounded, citation-aware answer** in exactly three lines of
|
|
48
|
+
code.
|
|
49
|
+
|
|
50
|
+
**What you get out of the box:**
|
|
51
|
+
|
|
52
|
+
- **Chunking** — sentence-aware splitting with a sliding overlap, zero configuration
|
|
53
|
+
- **Embeddings** — a lightweight `sentence-transformers` model, loaded lazily
|
|
54
|
+
- **Vector DB** — a self-contained in-memory store with fast NumPy cosine search
|
|
55
|
+
- **Context bundling** — evidence is ranked, tagged `[1] [2] [3] ...` and injected into the prompt
|
|
56
|
+
- **LLM execution** — unified calls through the Hugging Face Serverless API or LiteLLM
|
|
57
|
+
- **Weak-evidence guard** — answers truthfully when the documents do not cover the question
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install ezrag
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
That is the entire experience — import, load, ask:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from ezrag import EZRAG # 1. import
|
|
71
|
+
|
|
72
|
+
rag = EZRAG().load(SOURCE) # 2. initialise + load your source
|
|
73
|
+
print(rag.ask("Your question here")) # 3. ask — grounded, cited answer
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
No environment variables to set up, no vector database to spin up, no prompting
|
|
77
|
+
prompts to craft. One object call chain produces a grounded, cited answer.
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
| Parameter | Default | Purpose |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `llm` | `mistralai/Mistral-7B-Instruct-v0.2` | LLM identifier or endpoint |
|
|
84
|
+
| `embedding_model` | `all-MiniLM-L6-v2` | sentence-transformers model |
|
|
85
|
+
| `provider` | `"hf"` | `"hf"` (HF Serverless) or `"litellm"` |
|
|
86
|
+
| `hf_token` | env `HF_TOKEN` | token for protected / gated models |
|
|
87
|
+
| `top_k` | `4` | number of evidence chunks retrieved |
|
|
88
|
+
| `min_score` | `0.10` | retrieval similarity floor |
|
|
89
|
+
| `chunk_size` | `900` | target chunk length (characters) |
|
|
90
|
+
| `chunk_overlap` | `120` | sliding context overlap |
|
|
91
|
+
|
|
92
|
+
## Providers
|
|
93
|
+
|
|
94
|
+
- **Hugging Face Serverless** (default) — no custom API wiring. Set the `HF_TOKEN`
|
|
95
|
+
environment variable only when the model is gated.
|
|
96
|
+
- **LiteLLM** — point `llm` at any OpenAI-compatible endpoint, for example
|
|
97
|
+
`openai/gpt-4o` or `azure/gpt-4o`.
|
|
98
|
+
|
|
99
|
+
## How It Works
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
source ──▶ chunk ──▶ embed ──▶ in-memory index
|
|
103
|
+
│
|
|
104
|
+
question ──▶ embed ──▶ top-k search ──┘
|
|
105
|
+
│
|
|
106
|
+
context bundle ──▶ LLM ──▶ cited answer
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Hugging Face Spaces
|
|
110
|
+
|
|
111
|
+
ezrag runs out of the box in a Hugging Face Space — pip pulls in every runtime
|
|
112
|
+
dependency automatically. Store a `HF_TOKEN` Space secret if your chosen model is
|
|
113
|
+
gated.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT © Oguru Vinay Reddy
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ezrag/__init__.py,sha256=Xfk0iZRFiNL4AMn3X9drkFdi4AZqwcmR965hlk6-2Uk,157
|
|
2
|
+
ezrag/engine.py,sha256=V4CEzjOQUJWzkqx_ZkehQ9ABTbEtytHTIH20KEd8tn0,13293
|
|
3
|
+
ezrag_engine-0.1.0.dist-info/METADATA,sha256=0XpaKSri8N2C8i_Z1wSnjJOVIkL9t-8hkCqZU24NDYQ,4395
|
|
4
|
+
ezrag_engine-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
ezrag_engine-0.1.0.dist-info/top_level.txt,sha256=5GepBIq-mzRxEBqwhAyFK3d6EOwjevtdn2pCQQxoIJI,6
|
|
6
|
+
ezrag_engine-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ezrag
|