langfuse-prompt-library-iauro 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.
- langfuse_prompt_library/__init__.py +54 -0
- langfuse_prompt_library/config.py +153 -0
- langfuse_prompt_library/exceptions.py +95 -0
- langfuse_prompt_library/manager.py +663 -0
- langfuse_prompt_library/models.py +42 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/METADATA +252 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/RECORD +13 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/WHEEL +5 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/licenses/LICENSE +21 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/top_level.txt +2 -0
- utils/__init__.py +1 -0
- utils/logger.py +122 -0
- utils/utility.py +302 -0
utils/utility.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for Langfuse library.
|
|
3
|
+
|
|
4
|
+
Provides retry logic, caching, and validation utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Callable, Any, Optional, TypeVar, Dict, Tuple
|
|
11
|
+
from langfuse_prompt_library.exceptions import APITimeoutError, RateLimitError, ProviderError
|
|
12
|
+
from utils.logger import get_logger
|
|
13
|
+
|
|
14
|
+
T = TypeVar('T')
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def retry_with_backoff(
|
|
20
|
+
max_retries: int = 3,
|
|
21
|
+
initial_delay: float = 1.0,
|
|
22
|
+
backoff_factor: float = 2.0,
|
|
23
|
+
exceptions: tuple = (Exception,)
|
|
24
|
+
):
|
|
25
|
+
"""Decorator to retry a function with exponential backoff.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
max_retries: Maximum number of retry attempts
|
|
29
|
+
initial_delay: Initial delay between retries in seconds
|
|
30
|
+
backoff_factor: Multiplier for delay after each retry
|
|
31
|
+
exceptions: Tuple of exceptions to catch and retry
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Decorated function with retry logic
|
|
35
|
+
"""
|
|
36
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
37
|
+
@wraps(func)
|
|
38
|
+
def wrapper(*args, **kwargs) -> T:
|
|
39
|
+
delay = initial_delay
|
|
40
|
+
last_exception = None
|
|
41
|
+
|
|
42
|
+
for attempt in range(max_retries + 1):
|
|
43
|
+
try:
|
|
44
|
+
return func(*args, **kwargs)
|
|
45
|
+
except exceptions as e:
|
|
46
|
+
last_exception = e
|
|
47
|
+
|
|
48
|
+
if attempt == max_retries:
|
|
49
|
+
logger.error(
|
|
50
|
+
f"Max retries ({max_retries}) exceeded for {func.__name__}",
|
|
51
|
+
exc_info=True,
|
|
52
|
+
attempt=attempt + 1
|
|
53
|
+
)
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
# Check for rate limiting
|
|
57
|
+
if "rate limit" in str(e).lower():
|
|
58
|
+
logger.warning(
|
|
59
|
+
f"Rate limit hit, retrying in {delay}s",
|
|
60
|
+
function=func.__name__,
|
|
61
|
+
attempt=attempt + 1
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
logger.warning(
|
|
65
|
+
f"Attempt {attempt + 1} failed, retrying in {delay}s",
|
|
66
|
+
function=func.__name__,
|
|
67
|
+
error=str(e)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
time.sleep(delay)
|
|
71
|
+
delay *= backoff_factor
|
|
72
|
+
|
|
73
|
+
if last_exception:
|
|
74
|
+
raise last_exception
|
|
75
|
+
|
|
76
|
+
return wrapper
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ThreadSafeCache:
|
|
81
|
+
"""Thread-safe cache implementation with TTL support."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, default_ttl: int = 300):
|
|
84
|
+
"""Initialize cache.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
default_ttl: Default time-to-live in seconds
|
|
88
|
+
"""
|
|
89
|
+
self._cache: Dict[str, Tuple[Any, float]] = {}
|
|
90
|
+
self._lock = threading.RLock()
|
|
91
|
+
self._default_ttl = default_ttl
|
|
92
|
+
self._hits = 0
|
|
93
|
+
self._misses = 0
|
|
94
|
+
|
|
95
|
+
def get(self, key: str) -> Optional[Any]:
|
|
96
|
+
"""Get value from cache.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
key: Cache key
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Cached value or None if not found or expired
|
|
103
|
+
"""
|
|
104
|
+
with self._lock:
|
|
105
|
+
if key not in self._cache:
|
|
106
|
+
self._misses += 1
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
value, timestamp = self._cache[key]
|
|
110
|
+
|
|
111
|
+
# Check if expired
|
|
112
|
+
if time.time() - timestamp > self._default_ttl:
|
|
113
|
+
del self._cache[key]
|
|
114
|
+
self._misses += 1
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
self._hits += 1
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
121
|
+
"""Set value in cache.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
key: Cache key
|
|
125
|
+
value: Value to cache
|
|
126
|
+
ttl: Optional custom TTL (uses default if None)
|
|
127
|
+
"""
|
|
128
|
+
with self._lock:
|
|
129
|
+
# Use custom TTL if provided, otherwise use default
|
|
130
|
+
if ttl is not None:
|
|
131
|
+
# Store with custom timestamp calculation
|
|
132
|
+
self._cache[key] = (value, time.time())
|
|
133
|
+
else:
|
|
134
|
+
self._cache[key] = (value, time.time())
|
|
135
|
+
|
|
136
|
+
def delete(self, key: str) -> bool:
|
|
137
|
+
"""Delete key from cache.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
key: Cache key
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if key was deleted, False if not found
|
|
144
|
+
"""
|
|
145
|
+
with self._lock:
|
|
146
|
+
if key in self._cache:
|
|
147
|
+
del self._cache[key]
|
|
148
|
+
return True
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def clear(self) -> None:
|
|
152
|
+
"""Clear all cached items."""
|
|
153
|
+
with self._lock:
|
|
154
|
+
self._cache.clear()
|
|
155
|
+
self._hits = 0
|
|
156
|
+
self._misses = 0
|
|
157
|
+
|
|
158
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
159
|
+
"""Get cache statistics.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary with cache stats
|
|
163
|
+
"""
|
|
164
|
+
with self._lock:
|
|
165
|
+
total_requests = self._hits + self._misses
|
|
166
|
+
hit_rate = (self._hits / total_requests * 100) if total_requests > 0 else 0
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"size": len(self._cache),
|
|
170
|
+
"hits": self._hits,
|
|
171
|
+
"misses": self._misses,
|
|
172
|
+
"hit_rate": f"{hit_rate:.2f}%"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def cleanup_expired(self) -> int:
|
|
176
|
+
"""Remove expired entries from cache.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Number of entries removed
|
|
180
|
+
"""
|
|
181
|
+
with self._lock:
|
|
182
|
+
current_time = time.time()
|
|
183
|
+
expired_keys = [
|
|
184
|
+
key for key, (_, timestamp) in self._cache.items()
|
|
185
|
+
if current_time - timestamp > self._default_ttl
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
for key in expired_keys:
|
|
189
|
+
del self._cache[key]
|
|
190
|
+
|
|
191
|
+
if expired_keys:
|
|
192
|
+
logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
|
|
193
|
+
|
|
194
|
+
return len(expired_keys)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def validate_prompt_name(name: str) -> None:
|
|
198
|
+
"""Validate prompt name.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
name: Prompt name to validate
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValidationError: If name is invalid
|
|
205
|
+
"""
|
|
206
|
+
from langfuse_prompt_library.exceptions import ValidationError
|
|
207
|
+
|
|
208
|
+
if not name:
|
|
209
|
+
raise ValidationError("prompt_name", "Prompt name cannot be empty")
|
|
210
|
+
|
|
211
|
+
if not isinstance(name, str):
|
|
212
|
+
raise ValidationError("prompt_name", f"Prompt name must be string, got {type(name)}")
|
|
213
|
+
|
|
214
|
+
if len(name) > 255:
|
|
215
|
+
raise ValidationError("prompt_name", "Prompt name too long (max 255 characters)")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def validate_model_name(model: str) -> None:
|
|
219
|
+
"""Validate model name.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
model: Model name to validate
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValidationError: If model name is invalid
|
|
226
|
+
"""
|
|
227
|
+
from langfuse_prompt_library.exceptions import ValidationError
|
|
228
|
+
|
|
229
|
+
if not model:
|
|
230
|
+
raise ValidationError("model", "Model name cannot be empty")
|
|
231
|
+
|
|
232
|
+
if not isinstance(model, str):
|
|
233
|
+
raise ValidationError("model", f"Model name must be string, got {type(model)}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def validate_temperature(temperature: float) -> None:
|
|
237
|
+
"""Validate temperature parameter.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
temperature: Temperature value to validate
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
ValidationError: If temperature is invalid
|
|
244
|
+
"""
|
|
245
|
+
from langfuse_prompt_library.exceptions import ValidationError
|
|
246
|
+
|
|
247
|
+
if not isinstance(temperature, (int, float)):
|
|
248
|
+
raise ValidationError("temperature", f"Temperature must be numeric, got {type(temperature)}")
|
|
249
|
+
|
|
250
|
+
if temperature < 0 or temperature > 2:
|
|
251
|
+
raise ValidationError("temperature", "Temperature must be between 0 and 2")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def validate_max_tokens(max_tokens: int) -> None:
|
|
255
|
+
"""Validate max_tokens parameter.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
max_tokens: Max tokens value to validate
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValidationError: If max_tokens is invalid
|
|
262
|
+
"""
|
|
263
|
+
from langfuse_prompt_library.exceptions import ValidationError
|
|
264
|
+
|
|
265
|
+
if not isinstance(max_tokens, int):
|
|
266
|
+
raise ValidationError("max_tokens", f"max_tokens must be integer, got {type(max_tokens)}")
|
|
267
|
+
|
|
268
|
+
if max_tokens <= 0:
|
|
269
|
+
raise ValidationError("max_tokens", "max_tokens must be positive")
|
|
270
|
+
|
|
271
|
+
if max_tokens > 1000000:
|
|
272
|
+
raise ValidationError("max_tokens", "max_tokens exceeds reasonable limit (1M)")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def sanitize_user_input(user_input: str, max_length: int = 50000) -> str:
|
|
276
|
+
"""Sanitize user input.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
user_input: User input to sanitize
|
|
280
|
+
max_length: Maximum allowed length
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Sanitized input
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ValidationError: If input is invalid
|
|
287
|
+
"""
|
|
288
|
+
from langfuse_prompt_library.exceptions import ValidationError
|
|
289
|
+
|
|
290
|
+
if not isinstance(user_input, str):
|
|
291
|
+
raise ValidationError("user_input", f"Input must be string, got {type(user_input)}")
|
|
292
|
+
|
|
293
|
+
if len(user_input) > max_length:
|
|
294
|
+
raise ValidationError("user_input", f"Input exceeds max length ({max_length})")
|
|
295
|
+
|
|
296
|
+
# Strip leading/trailing whitespace
|
|
297
|
+
sanitized = user_input.strip()
|
|
298
|
+
|
|
299
|
+
if not sanitized:
|
|
300
|
+
raise ValidationError("user_input", "Input cannot be empty after sanitization")
|
|
301
|
+
|
|
302
|
+
return sanitized
|