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.
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