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.
- ragforge_framework/__init__.py +13 -0
- ragforge_framework/admin/__init__.py +1 -0
- ragforge_framework/admin/routes.py +224 -0
- ragforge_framework/app.py +88 -0
- ragforge_framework/cache/__init__.py +1 -0
- ragforge_framework/cache/repository.py +155 -0
- ragforge_framework/cache/response_cache.py +105 -0
- ragforge_framework/cache/user_manager.py +264 -0
- ragforge_framework/cli.py +91 -0
- ragforge_framework/config.py +50 -0
- ragforge_framework/core.py +42 -0
- ragforge_framework/llm/__init__.py +1 -0
- ragforge_framework/llm/gateway.py +95 -0
- ragforge_framework/llm/service.py +50 -0
- ragforge_framework/models/__init__.py +1 -0
- ragforge_framework/models/schemas.py +90 -0
- ragforge_framework/rag/__init__.py +1 -0
- ragforge_framework/rag/chain.py +28 -0
- ragforge_framework/rag/embeddings.py +10 -0
- ragforge_framework/rag/engine.py +31 -0
- ragforge_framework/rag/injest.py +81 -0
- ragforge_framework/rag/prompt.py +61 -0
- ragforge_framework/rag/retriever.py +19 -0
- ragforge_framework/rag/vectorstore.py +31 -0
- ragforge_framework-0.1.0.dist-info/METADATA +148 -0
- ragforge_framework-0.1.0.dist-info/RECORD +28 -0
- ragforge_framework-0.1.0.dist-info/WHEEL +4 -0
- ragforge_framework-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -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
|