ragforge-framework 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,264 @@
1
+ from hashlib import sha256
2
+ from datetime import datetime
3
+
4
+ from .repository import Repository
5
+
6
+
7
+ class UserManager:
8
+ """
9
+ Manages user registration, requests tracking, rate limiting, and analytics.
10
+ """
11
+ def __init__(self, config):
12
+ self.config = config
13
+ self.repository = Repository(config.USER_CACHE_DIR)
14
+ self.daily_limit = config.DAILY_LIMIT
15
+
16
+ # ==========================================================
17
+ # PRIVATE HELPERS
18
+ # ==========================================================
19
+
20
+ def _today(self):
21
+ return datetime.now().strftime("%Y-%m-%d")
22
+
23
+ def _now(self):
24
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
25
+
26
+ def _user_id(self, ip: str):
27
+ return sha256(ip.encode()).hexdigest()
28
+
29
+ def _cache_key(self, ip: str):
30
+ return f"{self._user_id(ip)}:{self._today()}"
31
+
32
+ # ==========================================================
33
+ # USER FACTORY
34
+ # ==========================================================
35
+
36
+ def _create_user(self, ip: str):
37
+ return {
38
+ "user_id": self._user_id(ip),
39
+ "ip": ip,
40
+ "date": self._today(),
41
+ "total_requests": 0,
42
+ "llm_requests": 0,
43
+ "remaining": self.daily_limit,
44
+ "first_request": self._now(),
45
+ "last_request": self._now(),
46
+ "cache_hits": 0,
47
+ "cache_misses": 0,
48
+ "provider_usage": {},
49
+ "fallbacks": 0
50
+ }
51
+
52
+ # ==========================================================
53
+ # GET USER
54
+ # ==========================================================
55
+
56
+ def get_user(self, ip: str):
57
+ key = self._cache_key(ip)
58
+ user = self.repository.get(key)
59
+ if user is None:
60
+ user = self._create_user(ip)
61
+ self.repository.set(key, user)
62
+ return user
63
+
64
+ # ==========================================================
65
+ # SAVE USER
66
+ # ==========================================================
67
+
68
+ def save_user(self, user: dict):
69
+ key = f"{user['user_id']}:{user['date']}"
70
+ self.repository.set(key, user)
71
+
72
+ # ==========================================================
73
+ # REQUEST COUNTER
74
+ # ==========================================================
75
+
76
+ def register_request(self, ip: str):
77
+ user = self.get_user(ip)
78
+ user["total_requests"] += 1
79
+ user["last_request"] = self._now()
80
+ self.save_user(user)
81
+ return user
82
+
83
+ # ==========================================================
84
+ # RATE LIMIT
85
+ # ==========================================================
86
+
87
+ def allow_request(self, ip: str):
88
+ user = self.get_user(ip)
89
+ if user["llm_requests"] >= self.daily_limit:
90
+ return False, user
91
+ user["llm_requests"] += 1
92
+ user["remaining"] = self.daily_limit - user["llm_requests"]
93
+ user["last_request"] = self._now()
94
+ self.save_user(user)
95
+ return True, user
96
+
97
+ # ==========================================================
98
+ # CACHE HIT
99
+ # ==========================================================
100
+
101
+ def cache_hit(self, ip: str):
102
+ user = self.get_user(ip)
103
+ user["cache_hits"] += 1
104
+ self.save_user(user)
105
+
106
+ # ==========================================================
107
+ # CACHE MISS
108
+ # ==========================================================
109
+
110
+ def cache_miss(self, ip: str):
111
+ user = self.get_user(ip)
112
+ user["cache_misses"] += 1
113
+ self.save_user(user)
114
+
115
+ # ==========================================================
116
+ # PROVIDER
117
+ # ==========================================================
118
+
119
+ def provider_used(self, ip: str, provider: str):
120
+ user = self.get_user(ip)
121
+ provider = provider.lower()
122
+ if provider not in user["provider_usage"]:
123
+ user["provider_usage"][provider] = 0
124
+ user["provider_usage"][provider] += 1
125
+ self.save_user(user)
126
+
127
+ # ==========================================================
128
+ # FALLBACK
129
+ # ==========================================================
130
+
131
+ def record_fallback(self, ip: str):
132
+ user = self.get_user(ip)
133
+ user["fallbacks"] += 1
134
+ self.save_user(user)
135
+
136
+ # ==========================================================
137
+ # USER EXISTS
138
+ # ==========================================================
139
+
140
+ def user_exists(self, ip: str) -> bool:
141
+ key = self._cache_key(ip)
142
+ return self.repository.exists(key)
143
+
144
+ # ==========================================================
145
+ # GET ALL USERS
146
+ # ==========================================================
147
+
148
+ def get_all_users(self):
149
+ return self.repository.values()
150
+
151
+ # ==========================================================
152
+ # ACTIVE USERS
153
+ # ==========================================================
154
+
155
+ def active_users(self):
156
+ users = self.get_all_users()
157
+ return sorted(
158
+ users,
159
+ key=lambda user: user["last_request"],
160
+ reverse=True
161
+ )
162
+
163
+ # ==========================================================
164
+ # SEARCH USER
165
+ # ==========================================================
166
+
167
+ def search_user(self, user_id: str):
168
+ users = self.get_all_users()
169
+ for user in users:
170
+ if user["user_id"] == user_id:
171
+ return user
172
+ return None
173
+
174
+ # ==========================================================
175
+ # RESET USER
176
+ # ==========================================================
177
+
178
+ def reset_user(self, user_id: str):
179
+ users = self.repository.items()
180
+ for key, user in users:
181
+ if user["user_id"] == user_id:
182
+ self.repository.delete(key)
183
+ return True
184
+ return False
185
+
186
+ # ==========================================================
187
+ # RESET ALL USERS
188
+ # ==========================================================
189
+
190
+ def reset_all(self):
191
+ self.repository.clear()
192
+
193
+ # ==========================================================
194
+ # DELETE OLD USERS
195
+ # ==========================================================
196
+
197
+ def delete_old_users(self):
198
+ today = self._today()
199
+ users = self.repository.items()
200
+ deleted = 0
201
+ for key, user in users:
202
+ if user["date"] != today:
203
+ self.repository.delete(key)
204
+ deleted += 1
205
+ return deleted
206
+
207
+ # ==========================================================
208
+ # TOP USERS
209
+ # ==========================================================
210
+
211
+ def get_top_users(self, limit: int = 10):
212
+ users = self.get_all_users()
213
+ users.sort(
214
+ key=lambda user: user["llm_requests"],
215
+ reverse=True
216
+ )
217
+ return users[:limit]
218
+
219
+ # ==========================================================
220
+ # STATISTICS
221
+ # ==========================================================
222
+
223
+ def get_statistics(self):
224
+ users = self.get_all_users()
225
+ stats = {
226
+ "total_users": len(users),
227
+ "total_requests": 0,
228
+ "llm_requests": 0,
229
+ "cache_hits": 0,
230
+ "cache_misses": 0,
231
+ "fallbacks": 0,
232
+ "provider_usage": {}
233
+ }
234
+ for user in users:
235
+ stats["total_requests"] += user["total_requests"]
236
+ stats["llm_requests"] += user["llm_requests"]
237
+ stats["cache_hits"] += user["cache_hits"]
238
+ stats["cache_misses"] += user["cache_misses"]
239
+ stats["fallbacks"] += user["fallbacks"]
240
+ for provider, count in user["provider_usage"].items():
241
+ if provider not in stats["provider_usage"]:
242
+ stats["provider_usage"][provider] = 0
243
+ stats["provider_usage"][provider] += count
244
+ return stats
245
+
246
+ # ==========================================================
247
+ # EXPORT USERS
248
+ # ==========================================================
249
+
250
+ def export_users(self):
251
+ return {
252
+ "users": self.get_all_users(),
253
+ "statistics": self.get_statistics()
254
+ }
255
+
256
+ # ==========================================================
257
+ # DEBUG
258
+ # ==========================================================
259
+
260
+ def __len__(self):
261
+ return self.repository.count()
262
+
263
+ def __repr__(self):
264
+ return f"<UserManager users={len(self)}>"
@@ -0,0 +1,91 @@
1
+ import argparse
2
+ import sys
3
+ import os
4
+
5
+ def main():
6
+ """
7
+ RAGForge CLI Entry Point.
8
+ Processes command-line instructions for template initialization, vector ingestion, and API execution.
9
+ """
10
+ parser = argparse.ArgumentParser(description="RAGForge Framework Command Line Utility")
11
+ subparsers = parser.add_subparsers(dest="command", help="Available CLI commands")
12
+
13
+ # Command: init
14
+ subparsers.add_parser("init", help="Initialize a new RAGForge project template")
15
+
16
+ # Command: ingest
17
+ subparsers.add_parser("ingest", help="Run the document chunking and vector indexing pipeline")
18
+
19
+ # Command: run
20
+ run_parser = subparsers.add_parser("run", help="Start the FastAPI uvicorn backend")
21
+ run_parser.add_argument("--host", default="127.0.0.1", help="Host address to bind uvicorn server")
22
+ run_parser.add_argument("--port", type=int, default=8000, help="Port to run the backend API")
23
+
24
+ args = parser.parse_args()
25
+
26
+ if args.command == "init":
27
+ print("\n[RAGForge] Initializing starter template...")
28
+ os.makedirs("data", exist_ok=True)
29
+
30
+ data_file_path = os.path.join("data", "data.txt")
31
+ if not os.path.exists(data_file_path):
32
+ with open(data_file_path, "w", encoding="utf-8") as f:
33
+ f.write("Yashodeep Hundiwale is an experienced AI Engineer and creator of RAGForge.\n")
34
+ print(f" [+] Created default text source: {data_file_path}")
35
+
36
+ app_file_path = "app.py"
37
+ if not os.path.exists(app_file_path):
38
+ with open(app_file_path, "w", encoding="utf-8") as f:
39
+ f.write('''from ragforge_framework import RAGForge
40
+
41
+ # Instantiate the RAGForge framework config and services
42
+ forge = RAGForge(
43
+ data_path="data/data.txt",
44
+ faiss_path="faiss_index",
45
+ primary_model="ollama/llama3.2:latest"
46
+ )
47
+
48
+ # Expose fastapi app for production WSGI/ASGI servers (e.g. gunicorn/uvicorn)
49
+ app = forge.app
50
+
51
+ if __name__ == "__main__":
52
+ # Start the server locally when running app.py directly
53
+ forge.run(host="127.0.0.1", port=8000)
54
+ ''')
55
+ print(f" [+] Created default project entry point: {app_file_path}")
56
+
57
+ print("\n[RAGForge] Initialization completed successfully!")
58
+ print(" * Ingest your document database: ragforge ingest")
59
+ print(" * Launch your backend API: ragforge run\n")
60
+
61
+ elif args.command == "ingest":
62
+ print("\n[RAGForge] Loading Ingestion Pipeline...")
63
+ forge = get_forge_instance()
64
+ forge.ingest()
65
+
66
+ elif args.command == "run":
67
+ print(f"\n[RAGForge] Starting server on {args.host}:{args.port}...")
68
+ forge = get_forge_instance()
69
+ forge.run(host=args.host, port=args.port)
70
+
71
+ else:
72
+ parser.print_help()
73
+
74
+
75
+ def get_forge_instance():
76
+ """
77
+ Looks for a local 'app.py' file in the current working directory to load
78
+ custom configs; falls back to default settings if app.py is missing or invalid.
79
+ """
80
+ if os.path.exists("app.py"):
81
+ sys.path.insert(0, os.getcwd())
82
+ try:
83
+ import app as local_app
84
+ if hasattr(local_app, "forge"):
85
+ return local_app.forge
86
+ except Exception as e:
87
+ print(f" [!] Warning: Failed to load 'forge' instance from local 'app.py' due to: {e}")
88
+ print(" Falling back to default framework configuration...")
89
+
90
+ from ragforge_framework.core import RAGForge
91
+ return RAGForge()
@@ -0,0 +1,50 @@
1
+ import os
2
+ from typing import List, Optional
3
+
4
+ class RAGForgeConfig:
5
+ """
6
+ RAGForge Configuration class.
7
+ Stores and validates all framework parameters.
8
+ """
9
+ def __init__(
10
+ self,
11
+ primary_model: str = "ollama/llama3.2:latest",
12
+ fallback_models: Optional[List[str]] = None,
13
+ temperature: float = 0.2,
14
+ timeout: int = 30,
15
+ max_retries: int = 3,
16
+ admin_key: Optional[str] = None,
17
+ daily_limit: int = 10,
18
+ response_cache_dir: str = "./response_cache",
19
+ user_cache_dir: str = "./user_cache",
20
+ data_path: str = "data/data.txt",
21
+ faiss_path: str = "faiss_index",
22
+ embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2",
23
+ chunk_size: int = 800,
24
+ chunk_overlap: int = 100,
25
+ search_type: str = "mmr",
26
+ top_k: int = 3,
27
+ fetch_k: int = 5,
28
+ lambda_mult: float = 0.5,
29
+ ):
30
+ self.PRIMARY_MODEL = primary_model
31
+ self.FALLBACK_MODELS = fallback_models or [
32
+ "groq/llama-3.1-8b-instant",
33
+ "mistralai/mistral-small-latest",
34
+ ]
35
+ self.TEMPERATURE = temperature
36
+ self.TIMEOUT = timeout
37
+ self.MAX_RETRIES = max_retries
38
+ self.ADMIN_KEY = admin_key or os.getenv("ADMIN_KEY") or "yashodeep"
39
+ self.DAILY_LIMIT = daily_limit
40
+ self.RESPONSE_CACHE_DIR = response_cache_dir
41
+ self.USER_CACHE_DIR = user_cache_dir
42
+ self.DATA_PATH = data_path
43
+ self.FAISS_PATH = faiss_path
44
+ self.EMBEDDING_MODEL = embedding_model
45
+ self.CHUNK_SIZE = chunk_size
46
+ self.CHUNK_OVERLAP = chunk_overlap
47
+ self.SEARCH_TYPE = search_type
48
+ self.TOP_K = top_k
49
+ self.FETCH_K = fetch_k
50
+ self.LAMBDA_MULT = lambda_mult
@@ -0,0 +1,42 @@
1
+ import os
2
+ import uvicorn
3
+ from .config import RAGForgeConfig
4
+ from .app import create_app
5
+ from .rag.injest import ingest
6
+
7
+ class RAGForge:
8
+ """
9
+ RAGForge Core Orchestrator.
10
+ Exposes the programmatic API for configuring and running the RAG framework.
11
+ """
12
+ def __init__(self, **kwargs):
13
+ self.config = RAGForgeConfig(**kwargs)
14
+
15
+ # Create directories if they do not exist
16
+ os.makedirs(self.config.RESPONSE_CACHE_DIR, exist_ok=True)
17
+ os.makedirs(self.config.USER_CACHE_DIR, exist_ok=True)
18
+ os.makedirs(os.path.dirname(self.config.DATA_PATH) or ".", exist_ok=True)
19
+
20
+ # Lazy imports to prevent circular dependency issues
21
+ from .cache.user_manager import UserManager
22
+ from .cache.response_cache import ResponseCache
23
+ from .rag.engine import RAGEngine
24
+
25
+ self.user_manager = UserManager(self.config)
26
+ self.response_cache = ResponseCache(self.config)
27
+ self.engine = RAGEngine(self.config)
28
+
29
+ # Build FastAPI application
30
+ self.app = create_app(self)
31
+
32
+ def run(self, host: str = "127.0.0.1", port: int = 8000, **kwargs):
33
+ """
34
+ Starts the FastAPI backend using Uvicorn.
35
+ """
36
+ uvicorn.run(self.app, host=host, port=port, **kwargs)
37
+
38
+ def ingest(self):
39
+ """
40
+ Executes the vector ingestion pipeline for the configured knowledge base text.
41
+ """
42
+ ingest(self.config)
@@ -0,0 +1 @@
1
+ # LLM Package Initialization
@@ -0,0 +1,95 @@
1
+ import contextvars
2
+ import litellm
3
+ from litellm import completion
4
+
5
+ from ..cache.user_manager import UserManager
6
+
7
+ # Thread-safe context var to track the client's request IP
8
+ current_ip_var = contextvars.ContextVar("current_ip", default=None)
9
+
10
+ # Reference to the configuration instance to be read by the global callback
11
+ global_config = None
12
+
13
+
14
+ def set_global_config(config):
15
+ global global_config
16
+ global_config = config
17
+
18
+
19
+ def litellm_success_callback(kwargs, response_obj, start_time, end_time):
20
+ """
21
+ Global success callback triggered on every LiteLLM completion.
22
+ """
23
+ try:
24
+ ip = current_ip_var.get()
25
+ if not ip or not global_config:
26
+ return
27
+
28
+ user_manager = UserManager(global_config)
29
+
30
+ # Get the actual model that completed the request
31
+ model = response_obj.get("model") or kwargs.get("model") or ""
32
+
33
+ # Detect provider name
34
+ provider = model.split("/")[0].lower() if "/" in model else "unknown"
35
+ if provider == "llama3.2:latest":
36
+ provider = "ollama"
37
+
38
+ # Record provider usage
39
+ user_manager.provider_used(ip, provider)
40
+
41
+ # Record fallback if the responding model differs from PRIMARY_MODEL
42
+ if model != global_config.PRIMARY_MODEL:
43
+ user_manager.record_fallback(ip)
44
+ except Exception:
45
+ pass
46
+
47
+
48
+ # Register callback
49
+ litellm.success_callback = [litellm_success_callback]
50
+
51
+
52
+ class Gateway:
53
+ """
54
+ LiteLLM API Gateway.
55
+ Handles raw completion calls with auto-fallbacks and retries.
56
+ """
57
+ def __init__(self, config):
58
+ self.config = config
59
+ self.user_manager = UserManager(config)
60
+
61
+ set_global_config(config)
62
+
63
+
64
+ def _detect_provider(self, model: str):
65
+ return model.split("/")[0].lower()
66
+
67
+ def chat(
68
+ self,
69
+ ip: str,
70
+ messages: list,
71
+ temperature: float = 0.2,
72
+ timeout: int = 20,
73
+ retries: int = 3
74
+ ):
75
+ # Set the request IP contextvar so the global callback can record stats
76
+ token = current_ip_var.set(ip)
77
+ try:
78
+ response = completion(
79
+ model=self.config.PRIMARY_MODEL,
80
+ messages=messages,
81
+ temperature=temperature,
82
+ timeout=timeout,
83
+ num_retries=retries,
84
+ fallbacks=self.config.FALLBACK_MODELS
85
+ )
86
+
87
+ provider = self._detect_provider(response.model)
88
+
89
+ return {
90
+ "answer": response.choices[0].message.content,
91
+ "provider": provider,
92
+ "model": response.model
93
+ }
94
+ finally:
95
+ current_ip_var.reset(token)
@@ -0,0 +1,50 @@
1
+ from langchain_litellm import ChatLiteLLM
2
+ from litellm import completion
3
+ from .gateway import set_global_config
4
+
5
+
6
+ class LLMService:
7
+ """
8
+ LLM Interaction Service.
9
+ Wraps native LiteLLM and LangChain ChatLiteLLM model instances.
10
+ """
11
+ def __init__(self, config):
12
+ self.config = config
13
+ self.model = config.PRIMARY_MODEL
14
+ self.temperature = config.TEMPERATURE
15
+ self.timeout = config.TIMEOUT
16
+ self.max_retries = config.MAX_RETRIES
17
+ self.fallbacks = config.FALLBACK_MODELS
18
+
19
+ set_global_config(config)
20
+
21
+ # ===================================================
22
+ # Native LiteLLM
23
+ # ===================================================
24
+
25
+ def completion(self, messages):
26
+ return completion(
27
+ model=self.model,
28
+ messages=messages,
29
+ temperature=self.temperature,
30
+ timeout=self.timeout,
31
+ num_retries=self.max_retries,
32
+ fallbacks=self.fallbacks
33
+ )
34
+
35
+ # ===================================================
36
+ # LangChain Model
37
+ # ===================================================
38
+
39
+ def get_langchain_model(self):
40
+ model_kwargs = {}
41
+ if self.fallbacks:
42
+ model_kwargs["fallbacks"] = self.fallbacks
43
+
44
+ return ChatLiteLLM(
45
+ model=self.model,
46
+ temperature=self.temperature,
47
+ timeout=self.timeout,
48
+ max_retries=self.max_retries,
49
+ model_kwargs=model_kwargs
50
+ )
@@ -0,0 +1 @@
1
+ # Models Package Initialization
@@ -0,0 +1,90 @@
1
+ from pydantic import BaseModel
2
+ from typing import Dict, List
3
+
4
+
5
+ class ChatRequest(BaseModel):
6
+
7
+ prompt: str
8
+
9
+
10
+ class ChatResponse(BaseModel):
11
+
12
+ success: bool
13
+
14
+ cached: bool
15
+
16
+ answer: str
17
+
18
+ remaining_questions: int
19
+
20
+ provider: str
21
+
22
+
23
+ class UserResponse(BaseModel):
24
+
25
+ user_id: str
26
+
27
+ ip: str
28
+
29
+ total_requests: int
30
+
31
+ llm_requests: int
32
+
33
+ cache_hits: int
34
+
35
+ cache_misses: int
36
+
37
+ remaining: int
38
+
39
+ first_request: str
40
+
41
+ last_request: str
42
+
43
+ provider_usage: Dict[str, int]
44
+
45
+ fallbacks: int
46
+
47
+
48
+ class StatisticsResponse(BaseModel):
49
+
50
+ total_users: int
51
+
52
+ total_requests: int
53
+
54
+ llm_requests: int
55
+
56
+ cache_hits: int
57
+
58
+ cache_misses: int
59
+
60
+ fallbacks: int
61
+
62
+ provider_usage: Dict[str, int]
63
+
64
+
65
+ # ==========================================================
66
+ # ADMIN RESPONSE SCHEMAS
67
+ # ==========================================================
68
+
69
+ class AllUsersResponse(BaseModel):
70
+
71
+ total_users: int
72
+
73
+ users: List[UserResponse]
74
+
75
+
76
+ class TopUsersResponse(BaseModel):
77
+
78
+ users: List[UserResponse]
79
+
80
+
81
+ class CacheResponseDetail(BaseModel):
82
+
83
+ answer: str
84
+
85
+
86
+ class CacheInformationResponse(BaseModel):
87
+
88
+ cached_responses: int
89
+
90
+ responses: List[CacheResponseDetail]
@@ -0,0 +1 @@
1
+ # RAG Package Initialization