lionagi 0.12.2__py3-none-any.whl → 0.12.4__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.
Files changed (86) hide show
  1. lionagi/config.py +123 -0
  2. lionagi/fields/file.py +1 -1
  3. lionagi/fields/reason.py +1 -1
  4. lionagi/libs/file/concat.py +1 -6
  5. lionagi/libs/file/concat_files.py +1 -5
  6. lionagi/libs/file/save.py +1 -1
  7. lionagi/libs/package/imports.py +8 -177
  8. lionagi/libs/parse.py +30 -0
  9. lionagi/libs/schema/load_pydantic_model_from_schema.py +259 -0
  10. lionagi/libs/token_transform/perplexity.py +2 -4
  11. lionagi/libs/token_transform/synthlang_/resources/frameworks/framework_options.json +46 -46
  12. lionagi/libs/token_transform/synthlang_/translate_to_synthlang.py +1 -1
  13. lionagi/operations/chat/chat.py +2 -2
  14. lionagi/operations/communicate/communicate.py +20 -5
  15. lionagi/operations/parse/parse.py +131 -43
  16. lionagi/protocols/generic/log.py +1 -2
  17. lionagi/protocols/generic/pile.py +18 -4
  18. lionagi/protocols/messages/assistant_response.py +20 -1
  19. lionagi/protocols/messages/templates/README.md +6 -10
  20. lionagi/service/connections/__init__.py +15 -0
  21. lionagi/service/connections/api_calling.py +230 -0
  22. lionagi/service/connections/endpoint.py +410 -0
  23. lionagi/service/connections/endpoint_config.py +137 -0
  24. lionagi/service/connections/header_factory.py +56 -0
  25. lionagi/service/connections/match_endpoint.py +49 -0
  26. lionagi/service/connections/providers/__init__.py +3 -0
  27. lionagi/service/connections/providers/anthropic_.py +87 -0
  28. lionagi/service/connections/providers/exa_.py +33 -0
  29. lionagi/service/connections/providers/oai_.py +166 -0
  30. lionagi/service/connections/providers/ollama_.py +122 -0
  31. lionagi/service/connections/providers/perplexity_.py +29 -0
  32. lionagi/service/imodel.py +36 -144
  33. lionagi/service/manager.py +1 -7
  34. lionagi/service/{endpoints/rate_limited_processor.py → rate_limited_processor.py} +4 -2
  35. lionagi/service/resilience.py +545 -0
  36. lionagi/service/third_party/README.md +71 -0
  37. lionagi/service/third_party/__init__.py +0 -0
  38. lionagi/service/third_party/anthropic_models.py +159 -0
  39. lionagi/service/third_party/exa_models.py +165 -0
  40. lionagi/service/third_party/openai_models.py +18241 -0
  41. lionagi/service/third_party/pplx_models.py +156 -0
  42. lionagi/service/types.py +5 -4
  43. lionagi/session/branch.py +12 -7
  44. lionagi/tools/file/reader.py +1 -1
  45. lionagi/tools/memory/tools.py +497 -0
  46. lionagi/utils.py +921 -123
  47. lionagi/version.py +1 -1
  48. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/METADATA +33 -16
  49. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/RECORD +53 -63
  50. lionagi/libs/file/create_path.py +0 -80
  51. lionagi/libs/file/file_util.py +0 -358
  52. lionagi/libs/parse/__init__.py +0 -3
  53. lionagi/libs/parse/fuzzy_parse_json.py +0 -117
  54. lionagi/libs/parse/to_dict.py +0 -336
  55. lionagi/libs/parse/to_json.py +0 -61
  56. lionagi/libs/parse/to_num.py +0 -378
  57. lionagi/libs/parse/to_xml.py +0 -57
  58. lionagi/libs/parse/xml_parser.py +0 -148
  59. lionagi/libs/schema/breakdown_pydantic_annotation.py +0 -48
  60. lionagi/service/endpoints/__init__.py +0 -3
  61. lionagi/service/endpoints/base.py +0 -706
  62. lionagi/service/endpoints/chat_completion.py +0 -116
  63. lionagi/service/endpoints/match_endpoint.py +0 -72
  64. lionagi/service/providers/__init__.py +0 -3
  65. lionagi/service/providers/anthropic_/__init__.py +0 -3
  66. lionagi/service/providers/anthropic_/messages.py +0 -99
  67. lionagi/service/providers/exa_/models.py +0 -3
  68. lionagi/service/providers/exa_/search.py +0 -80
  69. lionagi/service/providers/exa_/types.py +0 -7
  70. lionagi/service/providers/groq_/__init__.py +0 -3
  71. lionagi/service/providers/groq_/chat_completions.py +0 -56
  72. lionagi/service/providers/ollama_/__init__.py +0 -3
  73. lionagi/service/providers/ollama_/chat_completions.py +0 -134
  74. lionagi/service/providers/openai_/__init__.py +0 -3
  75. lionagi/service/providers/openai_/chat_completions.py +0 -101
  76. lionagi/service/providers/openai_/spec.py +0 -14
  77. lionagi/service/providers/openrouter_/__init__.py +0 -3
  78. lionagi/service/providers/openrouter_/chat_completions.py +0 -62
  79. lionagi/service/providers/perplexity_/__init__.py +0 -3
  80. lionagi/service/providers/perplexity_/chat_completions.py +0 -44
  81. lionagi/service/providers/perplexity_/models.py +0 -5
  82. lionagi/service/providers/types.py +0 -17
  83. /lionagi/{service/providers/exa_/__init__.py → py.typed} +0 -0
  84. /lionagi/service/{endpoints/token_calculator.py → token_calculator.py} +0 -0
  85. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/WHEEL +0 -0
  86. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,545 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ Resilience patterns for API clients.
7
+
8
+ This module provides resilience patterns for API clients, including
9
+ the CircuitBreaker pattern and retry with exponential backoff.
10
+ """
11
+
12
+ import asyncio
13
+ import functools
14
+ import logging
15
+ import random
16
+ import time
17
+ from collections.abc import Awaitable, Callable
18
+ from enum import Enum
19
+ from typing import Any, TypeVar
20
+
21
+ T = TypeVar("T")
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class APIClientError(Exception):
26
+ """Base exception for all API client errors."""
27
+
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ status_code: int | None = None,
32
+ headers: dict[str, str] | None = None,
33
+ response_data: dict[str, Any] | None = None,
34
+ ):
35
+ """
36
+ Initialize the API client error.
37
+
38
+ Args:
39
+ message: The error message.
40
+ status_code: The HTTP status code, if applicable.
41
+ headers: The response headers, if applicable.
42
+ response_data: The response data, if applicable.
43
+ """
44
+ self.message = message
45
+ self.status_code = status_code
46
+ self.headers = headers or {}
47
+ self.response_data = response_data or {}
48
+ super().__init__(message)
49
+
50
+
51
+ class CircuitBreakerOpenError(APIClientError):
52
+ """Exception raised when a circuit breaker is open."""
53
+
54
+ def __init__(self, message: str, retry_after: float | None = None):
55
+ """
56
+ Initialize the circuit breaker open error.
57
+
58
+ Args:
59
+ message: The error message.
60
+ retry_after: The time to wait before retrying, in seconds.
61
+ """
62
+ super().__init__(message)
63
+ self.retry_after = retry_after
64
+
65
+
66
+ class CircuitState(Enum):
67
+ """Circuit breaker states."""
68
+
69
+ CLOSED = "closed" # Normal operation
70
+ OPEN = "open" # Failing, rejecting requests
71
+ HALF_OPEN = "half_open" # Testing if service recovered
72
+
73
+
74
+ class CircuitBreaker:
75
+ """
76
+ Circuit breaker pattern implementation for preventing calls to failing services.
77
+
78
+ The circuit breaker pattern prevents repeated calls to a failing service,
79
+ based on the principle of "fail fast" for better system resilience. When
80
+ a service fails repeatedly, the circuit opens and rejects requests for a
81
+ period of time, then transitions to a half-open state to test if the
82
+ service has recovered.
83
+
84
+ Example:
85
+ ```python
86
+ # Create a circuit breaker with a failure threshold of 5
87
+ # and a recovery time of 30 seconds
88
+ breaker = CircuitBreaker(failure_threshold=5, recovery_time=30.0)
89
+
90
+ # Execute a function with circuit breaker protection
91
+ try:
92
+ result = await breaker.execute(my_async_function, arg1, arg2, kwarg1=value1)
93
+ except CircuitBreakerOpenError:
94
+ # Handle the case where the circuit is open
95
+ with contextlib.suppress(Exception):
96
+ # Alternative approach using contextlib.suppress
97
+ pass
98
+ ```
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ failure_threshold: int = 5,
104
+ recovery_time: float = 30.0,
105
+ half_open_max_calls: int = 1,
106
+ excluded_exceptions: set[type[Exception]] | None = None,
107
+ name: str = "default",
108
+ ):
109
+ """
110
+ Initialize the circuit breaker.
111
+
112
+ Args:
113
+ failure_threshold: Number of failures before opening the circuit.
114
+ recovery_time: Time in seconds to wait before transitioning to half-open.
115
+ half_open_max_calls: Maximum number of calls allowed in half-open state.
116
+ excluded_exceptions: Set of exception types that should not count as failures.
117
+ name: Name of the circuit breaker for logging and metrics.
118
+ """
119
+ self.failure_threshold = failure_threshold
120
+ self.recovery_time = recovery_time
121
+ self.half_open_max_calls = half_open_max_calls
122
+ self.excluded_exceptions = excluded_exceptions or set()
123
+ self.name = name
124
+
125
+ # State variables
126
+ self.failure_count = 0
127
+ self.state = CircuitState.CLOSED
128
+ self.last_failure_time = 0
129
+ self._half_open_calls = 0
130
+ self._lock = asyncio.Lock()
131
+
132
+ # Metrics
133
+ self._metrics = {
134
+ "success_count": 0,
135
+ "failure_count": 0,
136
+ "rejected_count": 0,
137
+ "state_changes": [],
138
+ }
139
+
140
+ logger.debug(
141
+ f"Initialized CircuitBreaker '{self.name}' with failure_threshold={failure_threshold}, "
142
+ f"recovery_time={recovery_time}, half_open_max_calls={half_open_max_calls}"
143
+ )
144
+
145
+ @property
146
+ def metrics(self) -> dict[str, Any]:
147
+ """Get circuit breaker metrics."""
148
+ return self._metrics.copy()
149
+
150
+ def to_dict(self):
151
+ return {
152
+ "failure_threshold": self.failure_threshold,
153
+ "recovery_time": self.recovery_time,
154
+ "half_open_max_calls": self.half_open_max_calls,
155
+ "name": self.name,
156
+ }
157
+
158
+ async def _change_state(self, new_state: CircuitState) -> None:
159
+ """
160
+ Change circuit state with logging and metrics tracking.
161
+
162
+ Args:
163
+ new_state: The new circuit state.
164
+ """
165
+ old_state = self.state
166
+ if new_state != old_state:
167
+ self.state = new_state
168
+ self._metrics["state_changes"].append(
169
+ {
170
+ "time": time.time(),
171
+ "from": old_state,
172
+ "to": new_state,
173
+ }
174
+ )
175
+
176
+ logger.info(
177
+ f"Circuit '{self.name}' state changed from {old_state.value} to {new_state.value}"
178
+ )
179
+
180
+ # Reset counters on state change
181
+ if new_state == CircuitState.HALF_OPEN:
182
+ self._half_open_calls = 0
183
+ elif new_state == CircuitState.CLOSED:
184
+ self.failure_count = 0
185
+
186
+ async def _check_state(self) -> bool:
187
+ """
188
+ Check circuit state and determine if request can proceed.
189
+
190
+ Returns:
191
+ True if request can proceed, False otherwise.
192
+ """
193
+ async with self._lock:
194
+ now = time.time()
195
+
196
+ if self.state == CircuitState.OPEN:
197
+ # Check if recovery time has elapsed
198
+ if now - self.last_failure_time >= self.recovery_time:
199
+ await self._change_state(CircuitState.HALF_OPEN)
200
+ else:
201
+ recovery_remaining = self.recovery_time - (
202
+ now - self.last_failure_time
203
+ )
204
+ self._metrics["rejected_count"] += 1
205
+
206
+ logger.warning(
207
+ f"Circuit '{self.name}' is OPEN, rejecting request. "
208
+ f"Try again in {recovery_remaining:.2f}s"
209
+ )
210
+
211
+ return False
212
+
213
+ if self.state == CircuitState.HALF_OPEN:
214
+ # Only allow a limited number of calls in half-open state
215
+ if self._half_open_calls >= self.half_open_max_calls:
216
+ self._metrics["rejected_count"] += 1
217
+
218
+ logger.warning(
219
+ f"Circuit '{self.name}' is HALF_OPEN and at capacity. "
220
+ f"Try again later."
221
+ )
222
+
223
+ return False
224
+
225
+ self._half_open_calls += 1
226
+
227
+ return True
228
+
229
+ async def execute(
230
+ self, func: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any
231
+ ) -> T:
232
+ """
233
+ Execute a coroutine with circuit breaker protection.
234
+
235
+ Args:
236
+ func: The coroutine function to execute.
237
+ *args: Positional arguments for the function.
238
+ **kwargs: Keyword arguments for the function.
239
+
240
+ Returns:
241
+ The result of the function execution.
242
+
243
+ Raises:
244
+ CircuitBreakerOpenError: If the circuit is open.
245
+ Exception: Any exception raised by the function.
246
+ """
247
+ # Check if circuit allows this call
248
+ can_proceed = await self._check_state()
249
+ if not can_proceed:
250
+ remaining = self.recovery_time - (
251
+ time.time() - self.last_failure_time
252
+ )
253
+ raise CircuitBreakerOpenError(
254
+ f"Circuit breaker '{self.name}' is open. Retry after {remaining:.2f} seconds",
255
+ retry_after=remaining,
256
+ )
257
+
258
+ try:
259
+ logger.debug(
260
+ f"Executing {func.__name__} with circuit '{self.name}' state: {self.state.value}"
261
+ )
262
+ result = await func(*args, **kwargs)
263
+
264
+ # Handle success
265
+ async with self._lock:
266
+ self._metrics["success_count"] += 1
267
+
268
+ # On success in half-open state, close the circuit
269
+ if self.state == CircuitState.HALF_OPEN:
270
+ await self._change_state(CircuitState.CLOSED)
271
+
272
+ return result
273
+
274
+ except Exception as e:
275
+ # Determine if this exception should count as a circuit failure
276
+ is_excluded = any(
277
+ isinstance(e, exc_type)
278
+ for exc_type in self.excluded_exceptions
279
+ )
280
+
281
+ if not is_excluded:
282
+ async with self._lock:
283
+ self.failure_count += 1
284
+ self.last_failure_time = time.time()
285
+ self._metrics["failure_count"] += 1
286
+
287
+ # Log failure
288
+ logger.warning(
289
+ f"Circuit '{self.name}' failure: {e}. "
290
+ f"Count: {self.failure_count}/{self.failure_threshold}"
291
+ )
292
+
293
+ # Check if we need to open the circuit
294
+ if (
295
+ self.state == CircuitState.CLOSED
296
+ and self.failure_count >= self.failure_threshold
297
+ ) or self.state == CircuitState.HALF_OPEN:
298
+ await self._change_state(CircuitState.OPEN)
299
+
300
+ logger.exception(f"Circuit breaker '{self.name}' caught exception")
301
+ raise
302
+
303
+
304
+ class RetryConfig:
305
+ """Configuration for retry behavior."""
306
+
307
+ def __init__(
308
+ self,
309
+ max_retries: int = 3,
310
+ base_delay: float = 1.0,
311
+ max_delay: float = 60.0,
312
+ backoff_factor: float = 2.0,
313
+ jitter: bool = True,
314
+ jitter_factor: float = 0.2,
315
+ retry_exceptions: tuple[type[Exception], ...] = (Exception,),
316
+ exclude_exceptions: tuple[type[Exception], ...] = (),
317
+ ):
318
+ """
319
+ Initialize retry configuration.
320
+
321
+ Args:
322
+ max_retries: Maximum number of retry attempts.
323
+ base_delay: Initial delay between retries in seconds.
324
+ max_delay: Maximum delay between retries in seconds.
325
+ backoff_factor: Multiplier applied to delay after each retry.
326
+ jitter: Whether to add randomness to delay timings.
327
+ jitter_factor: How much randomness to add as a percentage.
328
+ retry_exceptions: Tuple of exception types that should trigger retry.
329
+ exclude_exceptions: Tuple of exception types that should not be retried.
330
+ """
331
+ self.max_retries = max_retries
332
+ self.base_delay = base_delay
333
+ self.max_delay = max_delay
334
+ self.backoff_factor = backoff_factor
335
+ self.jitter = jitter
336
+ self.jitter_factor = jitter_factor
337
+ self.retry_exceptions = retry_exceptions
338
+ self.exclude_exceptions = exclude_exceptions
339
+
340
+ def to_dict(self) -> dict[str, Any]:
341
+ """
342
+ Convert configuration to a dictionary.
343
+
344
+ Returns:
345
+ Dictionary representation of the configuration.
346
+ """
347
+ return {
348
+ "max_retries": self.max_retries,
349
+ "base_delay": self.base_delay,
350
+ "max_delay": self.max_delay,
351
+ "backoff_factor": self.backoff_factor,
352
+ "jitter": self.jitter,
353
+ "jitter_factor": self.jitter_factor,
354
+ }
355
+
356
+ def as_kwargs(self) -> dict[str, Any]:
357
+ """
358
+ Convert configuration to keyword arguments for retry_with_backoff.
359
+
360
+ Returns:
361
+ Dictionary of keyword arguments.
362
+ """
363
+ return {
364
+ "max_retries": self.max_retries,
365
+ "base_delay": self.base_delay,
366
+ "max_delay": self.max_delay,
367
+ "backoff_factor": self.backoff_factor,
368
+ "jitter": self.jitter,
369
+ "retry_exceptions": self.retry_exceptions,
370
+ "exclude_exceptions": self.exclude_exceptions,
371
+ }
372
+
373
+
374
+ async def retry_with_backoff(
375
+ func: Callable[..., Awaitable[T]],
376
+ *args: Any,
377
+ retry_exceptions: tuple[type[Exception], ...] = (Exception,),
378
+ exclude_exceptions: tuple[type[Exception], ...] = (),
379
+ max_retries: int = 3,
380
+ base_delay: float = 1.0,
381
+ max_delay: float = 60.0,
382
+ backoff_factor: float = 2.0,
383
+ jitter: bool = True,
384
+ jitter_factor: float = 0.2,
385
+ **kwargs: Any,
386
+ ) -> T:
387
+ """
388
+ Retry an async function with exponential backoff.
389
+
390
+ Args:
391
+ func: The async function to retry.
392
+ *args: Positional arguments for the function.
393
+ retry_exceptions: Tuple of exception types to retry.
394
+ exclude_exceptions: Tuple of exception types to not retry.
395
+ max_retries: Maximum number of retries.
396
+ base_delay: Initial delay between retries in seconds.
397
+ max_delay: Maximum delay between retries in seconds.
398
+ backoff_factor: Factor to increase delay with each retry.
399
+ jitter: Whether to add randomness to the delay.
400
+ jitter_factor: How much randomness to add as a percentage.
401
+ **kwargs: Keyword arguments for the function.
402
+
403
+ Returns:
404
+ The result of the function execution.
405
+
406
+ Raises:
407
+ Exception: The last exception raised by the function after all retries.
408
+ """
409
+ retries = 0
410
+ delay = base_delay
411
+
412
+ while True:
413
+ try:
414
+ return await func(*args, **kwargs)
415
+ except exclude_exceptions:
416
+ # Don't retry these exceptions
417
+ logger.debug(
418
+ f"Not retrying {func.__name__} for excluded exception type"
419
+ )
420
+ raise
421
+ except retry_exceptions as e:
422
+ # No need to store the exception since we're raising it if max retries reached
423
+ retries += 1
424
+ if retries > max_retries:
425
+ logger.warning(
426
+ f"Maximum retries ({max_retries}) reached for {func.__name__}"
427
+ )
428
+ raise
429
+
430
+ # Calculate backoff with optional jitter
431
+ if jitter:
432
+ # This is not used for cryptographic purposes, just for jitter
433
+ jitter_amount = random.uniform(
434
+ 1.0 - jitter_factor, 1.0 + jitter_factor
435
+ ) # noqa: S311
436
+ current_delay = min(delay * jitter_amount, max_delay)
437
+ else:
438
+ current_delay = min(delay, max_delay)
439
+
440
+ logger.info(
441
+ f"Retry {retries}/{max_retries} for {func.__name__} "
442
+ f"after {current_delay:.2f}s delay. Error: {e!s}"
443
+ )
444
+
445
+ # Increase delay for next iteration
446
+ delay = delay * backoff_factor
447
+
448
+ # Wait before retrying
449
+ await asyncio.sleep(current_delay)
450
+
451
+
452
+ def circuit_breaker(
453
+ failure_threshold: int = 5,
454
+ recovery_time: float = 30.0,
455
+ half_open_max_calls: int = 1,
456
+ excluded_exceptions: set[type[Exception]] | None = None,
457
+ name: str | None = None,
458
+ ) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
459
+ """
460
+ Decorator to apply circuit breaker pattern to an async function.
461
+
462
+ Args:
463
+ failure_threshold: Number of failures before opening the circuit.
464
+ recovery_time: Time in seconds to wait before transitioning to half-open.
465
+ half_open_max_calls: Maximum number of calls allowed in half-open state.
466
+ excluded_exceptions: Set of exception types that should not count as failures.
467
+ name: Name of the circuit breaker for logging and metrics.
468
+
469
+ Returns:
470
+ Decorator function that applies circuit breaker pattern.
471
+ """
472
+
473
+ def decorator(
474
+ func: Callable[..., Awaitable[T]],
475
+ ) -> Callable[..., Awaitable[T]]:
476
+ # Create a unique name for the circuit breaker if not provided
477
+ cb_name = name or f"cb_{func.__module__}_{func.__qualname__}"
478
+
479
+ # Create circuit breaker instance
480
+ cb = CircuitBreaker(
481
+ failure_threshold=failure_threshold,
482
+ recovery_time=recovery_time,
483
+ half_open_max_calls=half_open_max_calls,
484
+ excluded_exceptions=excluded_exceptions,
485
+ name=cb_name,
486
+ )
487
+
488
+ @functools.wraps(func)
489
+ async def wrapper(*args: Any, **kwargs: Any) -> T:
490
+ return await cb.execute(func, *args, **kwargs)
491
+
492
+ return wrapper
493
+
494
+ return decorator
495
+
496
+
497
+ def with_retry(
498
+ max_retries: int = 3,
499
+ base_delay: float = 1.0,
500
+ max_delay: float = 60.0,
501
+ backoff_factor: float = 2.0,
502
+ jitter: bool = True,
503
+ jitter_factor: float = 0.2,
504
+ retry_exceptions: tuple[type[Exception], ...] = (Exception,),
505
+ exclude_exceptions: tuple[type[Exception], ...] = (),
506
+ ) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
507
+ """
508
+ Decorator to apply retry with backoff pattern to an async function.
509
+
510
+ Args:
511
+ max_retries: Maximum number of retry attempts.
512
+ base_delay: Initial delay between retries in seconds.
513
+ max_delay: Maximum delay between retries in seconds.
514
+ backoff_factor: Multiplier applied to delay after each retry.
515
+ jitter: Whether to add randomness to delay timings.
516
+ jitter_factor: How much randomness to add as a percentage.
517
+ retry_exceptions: Tuple of exception types that should trigger retry.
518
+ exclude_exceptions: Tuple of exception types that should not be retried.
519
+
520
+ Returns:
521
+ Decorator function that applies retry pattern.
522
+ """
523
+
524
+ def decorator(
525
+ func: Callable[..., Awaitable[T]],
526
+ ) -> Callable[..., Awaitable[T]]:
527
+ @functools.wraps(func)
528
+ async def wrapper(*args: Any, **kwargs: Any) -> T:
529
+ return await retry_with_backoff(
530
+ func,
531
+ *args,
532
+ retry_exceptions=retry_exceptions,
533
+ exclude_exceptions=exclude_exceptions,
534
+ max_retries=max_retries,
535
+ base_delay=base_delay,
536
+ max_delay=max_delay,
537
+ backoff_factor=backoff_factor,
538
+ jitter=jitter,
539
+ jitter_factor=jitter_factor,
540
+ **kwargs,
541
+ )
542
+
543
+ return wrapper
544
+
545
+ return decorator
@@ -0,0 +1,71 @@
1
+ # Generated OpenAI Models
2
+
3
+ This directory contains generated schema files for OpenAI API models.
4
+
5
+ ## OpenAI Models
6
+
7
+ The `openai_models.py` file is generated from the OpenAI API schema and is not
8
+ committed to the repository. It is generated during the build process.
9
+
10
+ ### Generation Command
11
+
12
+ To generate the OpenAI models, use the following command:
13
+
14
+ ```bash
15
+ # Use exact version to guarantee byte-for-byte generation
16
+ python -m pip install 'datamodel-code-generator[http]==0.30.1'
17
+
18
+ python -m datamodel-codegen \
19
+ --url https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml \
20
+ --output lionagi/services/third_party/openai_models.py \
21
+ --allow-population-by-field-name \
22
+ --output-model-type pydantic_v2.BaseModel \
23
+ --field-constraints \
24
+ --use-schema-description \
25
+ --input-file-type openapi \
26
+ --use-field-description \
27
+ --use-one-literal-as-default \
28
+ --enum-field-as-literal all \
29
+ --use-union-operator \
30
+ --no-alias
31
+ ```
32
+
33
+ **Note**: if `bytes_aliased` and `float_aliased` are not generated, you can add
34
+ them manually onto the top of the file
35
+
36
+ ```python
37
+ from typing import Annotated
38
+ from pydantic import Field
39
+
40
+ bytes_aliased = Annotated[bytes, Field(alias="bytes")]
41
+ float_aliased = Annotated[float, Field(alias="float")]
42
+ ```
43
+
44
+ ### Avoiding Pydantic Alias Warnings
45
+
46
+ After generation, add the following line at the top of the file to avoid
47
+ Pydantic alias warnings:
48
+
49
+ ```python
50
+ from __future__ import annotations # noqa: D401,F401
51
+ ```
52
+
53
+ This prevents warnings about "alias must be outermost" that can appear if the
54
+ generated OpenAI models are imported before Pydantic's patched typing fix.
55
+
56
+ ### Why Not Committed
57
+
58
+ The OpenAI schema file is large and frequently updated. Rather than committing
59
+ this large, auto-generated file to the repository, we generate it during the
60
+ CI/CD build process. This approach:
61
+
62
+ 1. Keeps the repository size smaller
63
+ 2. Makes it easier to update to the latest OpenAI API version
64
+ 3. Avoids merge conflicts on auto-generated code
65
+ 4. Follows best practices for generated code
66
+
67
+ ### CI Integration
68
+
69
+ The file is generated as part of the pre-build step in CI, ensuring that the
70
+ wheel distribution still contains the models even though they're not in the
71
+ repository.
File without changes