gllm-core-binary 0.4.4__py3-none-manylinux_2_31_x86_64.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.
- gllm_core/__init__.py +1 -0
- gllm_core/__init__.pyi +0 -0
- gllm_core/adapters/__init__.py +5 -0
- gllm_core/adapters/__init__.pyi +3 -0
- gllm_core/adapters/tool/__init__.py +6 -0
- gllm_core/adapters/tool/__init__.pyi +4 -0
- gllm_core/adapters/tool/google_adk.py +91 -0
- gllm_core/adapters/tool/google_adk.pyi +23 -0
- gllm_core/adapters/tool/langchain.py +130 -0
- gllm_core/adapters/tool/langchain.pyi +31 -0
- gllm_core/constants.py +55 -0
- gllm_core/constants.pyi +36 -0
- gllm_core/event/__init__.py +6 -0
- gllm_core/event/__init__.pyi +4 -0
- gllm_core/event/event_emitter.py +211 -0
- gllm_core/event/event_emitter.pyi +155 -0
- gllm_core/event/handler/__init__.py +7 -0
- gllm_core/event/handler/__init__.pyi +5 -0
- gllm_core/event/handler/console_event_handler.py +48 -0
- gllm_core/event/handler/console_event_handler.pyi +32 -0
- gllm_core/event/handler/event_handler.py +89 -0
- gllm_core/event/handler/event_handler.pyi +51 -0
- gllm_core/event/handler/print_event_handler.py +130 -0
- gllm_core/event/handler/print_event_handler.pyi +33 -0
- gllm_core/event/handler/stream_event_handler.py +85 -0
- gllm_core/event/handler/stream_event_handler.pyi +62 -0
- gllm_core/event/hook/__init__.py +5 -0
- gllm_core/event/hook/__init__.pyi +3 -0
- gllm_core/event/hook/event_hook.py +30 -0
- gllm_core/event/hook/event_hook.pyi +18 -0
- gllm_core/event/hook/json_stringify_event_hook.py +32 -0
- gllm_core/event/hook/json_stringify_event_hook.pyi +16 -0
- gllm_core/event/messenger.py +133 -0
- gllm_core/event/messenger.pyi +66 -0
- gllm_core/schema/__init__.py +8 -0
- gllm_core/schema/__init__.pyi +6 -0
- gllm_core/schema/chunk.py +148 -0
- gllm_core/schema/chunk.pyi +66 -0
- gllm_core/schema/component.py +546 -0
- gllm_core/schema/component.pyi +205 -0
- gllm_core/schema/event.py +50 -0
- gllm_core/schema/event.pyi +33 -0
- gllm_core/schema/schema_generator.py +150 -0
- gllm_core/schema/schema_generator.pyi +35 -0
- gllm_core/schema/tool.py +418 -0
- gllm_core/schema/tool.pyi +198 -0
- gllm_core/utils/__init__.py +32 -0
- gllm_core/utils/__init__.pyi +13 -0
- gllm_core/utils/analyzer.py +256 -0
- gllm_core/utils/analyzer.pyi +123 -0
- gllm_core/utils/binary_handler_factory.py +99 -0
- gllm_core/utils/binary_handler_factory.pyi +62 -0
- gllm_core/utils/chunk_metadata_merger.py +102 -0
- gllm_core/utils/chunk_metadata_merger.pyi +41 -0
- gllm_core/utils/concurrency.py +184 -0
- gllm_core/utils/concurrency.pyi +94 -0
- gllm_core/utils/event_formatter.py +69 -0
- gllm_core/utils/event_formatter.pyi +30 -0
- gllm_core/utils/google_sheets.py +115 -0
- gllm_core/utils/google_sheets.pyi +18 -0
- gllm_core/utils/imports.py +91 -0
- gllm_core/utils/imports.pyi +42 -0
- gllm_core/utils/logger_manager.py +339 -0
- gllm_core/utils/logger_manager.pyi +176 -0
- gllm_core/utils/main_method_resolver.py +185 -0
- gllm_core/utils/main_method_resolver.pyi +54 -0
- gllm_core/utils/merger_method.py +130 -0
- gllm_core/utils/merger_method.pyi +49 -0
- gllm_core/utils/retry.py +258 -0
- gllm_core/utils/retry.pyi +41 -0
- gllm_core/utils/similarity.py +29 -0
- gllm_core/utils/similarity.pyi +10 -0
- gllm_core/utils/validation.py +26 -0
- gllm_core/utils/validation.pyi +12 -0
- gllm_core_binary-0.4.4.dist-info/METADATA +177 -0
- gllm_core_binary-0.4.4.dist-info/RECORD +78 -0
- gllm_core_binary-0.4.4.dist-info/WHEEL +5 -0
- gllm_core_binary-0.4.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Main method resolver for Component classes.
|
|
2
|
+
|
|
3
|
+
This module provides the MainMethodResolver class which encapsulates the logic
|
|
4
|
+
for resolving the main entrypoint method for Component subclasses.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Dimitrij Ray (dimitrij.ray@gdplabs.id)
|
|
8
|
+
|
|
9
|
+
References:
|
|
10
|
+
NONE
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC
|
|
14
|
+
from typing import Callable
|
|
15
|
+
|
|
16
|
+
from gllm_core.utils.logger_manager import LoggerManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MainMethodResolver:
|
|
20
|
+
"""Resolves the main entrypoint method for Component classes.
|
|
21
|
+
|
|
22
|
+
This resolver implements the precedence rules for determining which method
|
|
23
|
+
should be used as the main entrypoint:
|
|
24
|
+
1. Method decorated with @main in the most derived class.
|
|
25
|
+
2. Method named by __main_method__ property.
|
|
26
|
+
3. _run method (with deprecation warning).
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
cls (type): The Component class to resolve the main method for.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, component_class: type):
|
|
33
|
+
"""Initialize the resolver with a Component class.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
component_class (type): The Component class to resolve the main method for.
|
|
37
|
+
"""
|
|
38
|
+
self.cls = component_class
|
|
39
|
+
self._logger = LoggerManager().get_logger(f"MainMethodResolver.{component_class.__name__}")
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def validate_class(component_class: type) -> None:
|
|
43
|
+
"""Validate main method configuration at class definition time.
|
|
44
|
+
|
|
45
|
+
This performs early validation that can be done when a Component subclass
|
|
46
|
+
is defined, before any instances are created or methods are called.
|
|
47
|
+
|
|
48
|
+
Validations performed:
|
|
49
|
+
1. Check that __main_method__ property points to an existing method
|
|
50
|
+
2. Check that only one @main decorator is used within the same class
|
|
51
|
+
|
|
52
|
+
Note: Multiple inheritance conflicts are intentionally NOT checked here,
|
|
53
|
+
as they are deferred to runtime (get_main()) to allow class definition
|
|
54
|
+
to succeed.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
component_class (type): The Component class to validate.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
AttributeError: If __main_method__ refers to a non-existent method.
|
|
61
|
+
TypeError: If multiple methods are decorated with @main in the same class.
|
|
62
|
+
"""
|
|
63
|
+
if (method_name := getattr(component_class, "__main_method__", None)) is not None:
|
|
64
|
+
if not hasattr(component_class, method_name):
|
|
65
|
+
raise AttributeError(
|
|
66
|
+
f"Method {method_name!r} specified in __main_method__ does not exist in {component_class.__name__}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
main_methods = []
|
|
70
|
+
for name, method in component_class.__dict__.items():
|
|
71
|
+
if callable(method) and hasattr(method, "__is_main__"):
|
|
72
|
+
main_methods.append(name)
|
|
73
|
+
|
|
74
|
+
if len(main_methods) > 1:
|
|
75
|
+
raise TypeError(f"Multiple main methods defined in {component_class.__name__}: {', '.join(main_methods)}")
|
|
76
|
+
|
|
77
|
+
def resolve(self) -> Callable | None:
|
|
78
|
+
"""Resolve the main method following precedence rules.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Callable | None: The resolved main method, or None if not found.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
TypeError: If conflicting main methods are inherited from multiple ancestors.
|
|
85
|
+
"""
|
|
86
|
+
if decorated := self._resolve_decorated():
|
|
87
|
+
self._warn_if_redundant_property()
|
|
88
|
+
return decorated
|
|
89
|
+
|
|
90
|
+
if property_method := self._resolve_property():
|
|
91
|
+
return property_method
|
|
92
|
+
|
|
93
|
+
return self._resolve_legacy()
|
|
94
|
+
|
|
95
|
+
def _resolve_decorated(self) -> Callable | None:
|
|
96
|
+
"""Find the most-derived @main decorated method in the MRO (method resolution order).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Callable | None: The decorated main method, or None if not found.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
TypeError: If conflicting main methods are inherited from multiple ancestors.
|
|
103
|
+
"""
|
|
104
|
+
classes_with_main = []
|
|
105
|
+
for base in self.cls.__mro__:
|
|
106
|
+
if base.__name__ == "Component" or base is ABC:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
main_method_name = next(
|
|
110
|
+
(name for name, method in base.__dict__.items() if callable(method) and hasattr(method, "__is_main__")),
|
|
111
|
+
None,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if main_method_name:
|
|
115
|
+
classes_with_main.append((base, main_method_name))
|
|
116
|
+
|
|
117
|
+
if not classes_with_main:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
most_derived_class, method_name = classes_with_main[0]
|
|
121
|
+
actual_method = getattr(self.cls, method_name)
|
|
122
|
+
|
|
123
|
+
self._validate_no_decorator_conflicts(classes_with_main, most_derived_class, actual_method)
|
|
124
|
+
|
|
125
|
+
return actual_method
|
|
126
|
+
|
|
127
|
+
def _validate_no_decorator_conflicts(
|
|
128
|
+
self, classes_with_main: list[tuple[type, str]], most_derived_class: type, actual_method: Callable
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Validate that there are no conflicting @main decorators in multiple inheritance.
|
|
131
|
+
|
|
132
|
+
This method checks for conflicts only when multiple classes in the inheritance hierarchy have @main decorated
|
|
133
|
+
methods. It allows intentional overrides (when the most derived class defines its own @main method) but prevents
|
|
134
|
+
conflicts from multiple inheritance where different ancestors define different @main methods.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
classes_with_main (list[tuple[type, str]]): List of (class, method_name) tuples for classes with @main.
|
|
138
|
+
most_derived_class (type): The most derived class in the hierarchy with @main.
|
|
139
|
+
actual_method (Callable): The resolved method from the most derived class.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
TypeError: If conflicting main methods are inherited from multiple ancestors and the most derived class
|
|
143
|
+
is not the current class (indicating a true conflict rather than an intentional override).
|
|
144
|
+
"""
|
|
145
|
+
if len(classes_with_main) > 1 and most_derived_class is not self.cls:
|
|
146
|
+
for _, name in classes_with_main[1:]:
|
|
147
|
+
other_method = getattr(self.cls, name)
|
|
148
|
+
if other_method is not actual_method:
|
|
149
|
+
raise TypeError(
|
|
150
|
+
f"Conflicting main methods inherited from multiple ancestors in {self.cls.__name__}. "
|
|
151
|
+
"Please explicitly override with @main decorator."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _resolve_property(self) -> Callable | None:
|
|
155
|
+
"""Find method via __main_method__ property.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Callable | None: The method named by __main_method__, or None if not found.
|
|
159
|
+
"""
|
|
160
|
+
if hasattr(self.cls, "__main_method__") and self.cls.__main_method__ is not None:
|
|
161
|
+
method_name = self.cls.__main_method__
|
|
162
|
+
return getattr(self.cls, method_name, None)
|
|
163
|
+
|
|
164
|
+
def _resolve_legacy(self) -> Callable | None:
|
|
165
|
+
"""Fall back to _run method with deprecation warning.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Callable | None: The _run method, or None if not found.
|
|
169
|
+
"""
|
|
170
|
+
if hasattr(self.cls, "_run"):
|
|
171
|
+
self._logger.warning(
|
|
172
|
+
f"Using legacy _run method for {self.cls.__name__}. "
|
|
173
|
+
f"Consider using @main decorator to explicitly declare the main entrypoint.",
|
|
174
|
+
stacklevel=4,
|
|
175
|
+
)
|
|
176
|
+
return self.cls._run
|
|
177
|
+
|
|
178
|
+
def _warn_if_redundant_property(self) -> None:
|
|
179
|
+
"""Emit warning if both @main decorator and __main_method__ are defined."""
|
|
180
|
+
if self.cls.__dict__.get("__main_method__") is not None:
|
|
181
|
+
self._logger.warning(
|
|
182
|
+
f"Both @main decorator and __main_method__ property are defined in {self.cls.__name__}. "
|
|
183
|
+
"The @main decorator takes precedence. This redundant configuration should be resolved.",
|
|
184
|
+
stacklevel=4,
|
|
185
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from gllm_core.utils.logger_manager import LoggerManager as LoggerManager
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
class MainMethodResolver:
|
|
6
|
+
"""Resolves the main entrypoint method for Component classes.
|
|
7
|
+
|
|
8
|
+
This resolver implements the precedence rules for determining which method
|
|
9
|
+
should be used as the main entrypoint:
|
|
10
|
+
1. Method decorated with @main in the most derived class.
|
|
11
|
+
2. Method named by __main_method__ property.
|
|
12
|
+
3. _run method (with deprecation warning).
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
cls (type): The Component class to resolve the main method for.
|
|
16
|
+
"""
|
|
17
|
+
cls: Incomplete
|
|
18
|
+
def __init__(self, component_class: type) -> None:
|
|
19
|
+
"""Initialize the resolver with a Component class.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
component_class (type): The Component class to resolve the main method for.
|
|
23
|
+
"""
|
|
24
|
+
@staticmethod
|
|
25
|
+
def validate_class(component_class: type) -> None:
|
|
26
|
+
"""Validate main method configuration at class definition time.
|
|
27
|
+
|
|
28
|
+
This performs early validation that can be done when a Component subclass
|
|
29
|
+
is defined, before any instances are created or methods are called.
|
|
30
|
+
|
|
31
|
+
Validations performed:
|
|
32
|
+
1. Check that __main_method__ property points to an existing method
|
|
33
|
+
2. Check that only one @main decorator is used within the same class
|
|
34
|
+
|
|
35
|
+
Note: Multiple inheritance conflicts are intentionally NOT checked here,
|
|
36
|
+
as they are deferred to runtime (get_main()) to allow class definition
|
|
37
|
+
to succeed.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
component_class (type): The Component class to validate.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
AttributeError: If __main_method__ refers to a non-existent method.
|
|
44
|
+
TypeError: If multiple methods are decorated with @main in the same class.
|
|
45
|
+
"""
|
|
46
|
+
def resolve(self) -> Callable | None:
|
|
47
|
+
"""Resolve the main method following precedence rules.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Callable | None: The resolved main method, or None if not found.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
TypeError: If conflicting main methods are inherited from multiple ancestors.
|
|
54
|
+
"""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Defines a collection of merger methods.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Henry Wicaksono (henry.wicaksono@gdplabs.id)
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
NONE
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MergerMethod:
|
|
15
|
+
"""A collection of merger methods."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def concatenate(delimiter: str = "-") -> Callable[[list[Any]], str]:
|
|
19
|
+
"""Creates a function that concatenates a list of values with a delimiter.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
delimiter (str, optional): The delimiter to use when concatenating the values. Defaults to "-".
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Callable[[list[Any]], str]: A function that concatenates a list of values with the delimiter.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def _concat_with_delimiter(values: list[Any]) -> str:
|
|
29
|
+
return delimiter.join([str(value) for value in values])
|
|
30
|
+
|
|
31
|
+
return _concat_with_delimiter
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def pick_first(values: list[Any]) -> Any:
|
|
35
|
+
"""Picks the first value from a list of values.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
values (list[Any]): The values to pick from.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Any: The first value from the list.
|
|
42
|
+
"""
|
|
43
|
+
return values[0] if values else None
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def pick_last(values: list[Any]) -> Any:
|
|
47
|
+
"""Picks the last value from a list of values.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
values (list[Any]): The values to pick from.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Any: The last value from the list.
|
|
54
|
+
"""
|
|
55
|
+
return values[-1] if values else None
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def merge_overlapping_strings(delimiter: str = "\n") -> Callable[[list[str]], str]:
|
|
59
|
+
r"""Creates a function that merges a list of strings, handling common prefixes and overlaps.
|
|
60
|
+
|
|
61
|
+
The created function will:
|
|
62
|
+
- Identify and remove any common prefix shared by the strings.
|
|
63
|
+
- Process each pair of adjacent strings to remove overlapping strings.
|
|
64
|
+
- Join the cleaned strings together, including the common prefix at the beginning.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
delimiter (str, optional): The delimiter to use when merging the values. Defaults to "\n".
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Callable[[list[str]], str]: A function that merges a list of strings, handling common prefixes and overlaps.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def _merge_overlapping_strings(values: list[str]) -> str:
|
|
74
|
+
values = [str(value) for value in values]
|
|
75
|
+
common_prefix = MergerMethod._get_common_prefix(values)
|
|
76
|
+
values = [string[len(common_prefix) :] for string in values]
|
|
77
|
+
|
|
78
|
+
for idx in range(len(values) - 1):
|
|
79
|
+
overlap = MergerMethod._find_overlap(values[idx], values[idx + 1])
|
|
80
|
+
values[idx] = values[idx].replace(overlap, "")
|
|
81
|
+
|
|
82
|
+
values.insert(0, common_prefix)
|
|
83
|
+
|
|
84
|
+
return delimiter.join([string.strip() for string in values if string.strip()])
|
|
85
|
+
|
|
86
|
+
return _merge_overlapping_strings
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _get_common_prefix(strings: list[str]) -> str:
|
|
90
|
+
"""Identifies the common prefix shared by a list of strings.
|
|
91
|
+
|
|
92
|
+
This method uses `os.path.commonprefix` to find the common prefix shared by all strings in the provided
|
|
93
|
+
`strings`. If the common prefix does not end with a newline, the method removes the last partial line to
|
|
94
|
+
ensure the prefix ends at a full line break.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
strings (list[str]): A list of strings to find the common prefix.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: The common prefix shared by all strings, ending at the last full line.
|
|
101
|
+
"""
|
|
102
|
+
common_prefix = os.path.commonprefix(strings)
|
|
103
|
+
last_newline_index = common_prefix.rfind("\n")
|
|
104
|
+
|
|
105
|
+
if last_newline_index != -1:
|
|
106
|
+
return common_prefix[:last_newline_index]
|
|
107
|
+
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _find_overlap(first_string: str, second_string: str) -> str:
|
|
112
|
+
"""Finds the overlapping portion between two strings.
|
|
113
|
+
|
|
114
|
+
This method compares the end of the `first_string` with the beginning of the `second_string` to find the
|
|
115
|
+
longest overlapping substring. It returns the overlap if found, otherwise returns an empty string.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
first_string (str): The first string to compare.
|
|
119
|
+
second_string (str): The second string to compare.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str: The overlapping substring between the two strings, or an empty string if no overlap is found.
|
|
123
|
+
"""
|
|
124
|
+
max_overlap_len = min(len(first_string), len(second_string))
|
|
125
|
+
|
|
126
|
+
for i in range(max_overlap_len, 0, -1):
|
|
127
|
+
if first_string[-i:] == second_string[:i]:
|
|
128
|
+
return first_string[-i:]
|
|
129
|
+
|
|
130
|
+
return ""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
class MergerMethod:
|
|
4
|
+
"""A collection of merger methods."""
|
|
5
|
+
@staticmethod
|
|
6
|
+
def concatenate(delimiter: str = '-') -> Callable[[list[Any]], str]:
|
|
7
|
+
'''Creates a function that concatenates a list of values with a delimiter.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
delimiter (str, optional): The delimiter to use when concatenating the values. Defaults to "-".
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Callable[[list[Any]], str]: A function that concatenates a list of values with the delimiter.
|
|
14
|
+
'''
|
|
15
|
+
@staticmethod
|
|
16
|
+
def pick_first(values: list[Any]) -> Any:
|
|
17
|
+
"""Picks the first value from a list of values.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
values (list[Any]): The values to pick from.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Any: The first value from the list.
|
|
24
|
+
"""
|
|
25
|
+
@staticmethod
|
|
26
|
+
def pick_last(values: list[Any]) -> Any:
|
|
27
|
+
"""Picks the last value from a list of values.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
values (list[Any]): The values to pick from.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Any: The last value from the list.
|
|
34
|
+
"""
|
|
35
|
+
@staticmethod
|
|
36
|
+
def merge_overlapping_strings(delimiter: str = '\n') -> Callable[[list[str]], str]:
|
|
37
|
+
'''Creates a function that merges a list of strings, handling common prefixes and overlaps.
|
|
38
|
+
|
|
39
|
+
The created function will:
|
|
40
|
+
- Identify and remove any common prefix shared by the strings.
|
|
41
|
+
- Process each pair of adjacent strings to remove overlapping strings.
|
|
42
|
+
- Join the cleaned strings together, including the common prefix at the beginning.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
delimiter (str, optional): The delimiter to use when merging the values. Defaults to "\\n".
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Callable[[list[str]], str]: A function that merges a list of strings, handling common prefixes and overlaps.
|
|
49
|
+
'''
|
gllm_core/utils/retry.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Defines retry and timeout utilities.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Henry Wicaksono (henry.wicaksono@gdplabs.id)
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
NONE
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import functools
|
|
12
|
+
import inspect
|
|
13
|
+
import random
|
|
14
|
+
from typing import Any, Callable, TypeVar, overload
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, model_validator
|
|
17
|
+
|
|
18
|
+
from gllm_core.utils import LoggerManager
|
|
19
|
+
from gllm_core.utils.concurrency import syncify
|
|
20
|
+
|
|
21
|
+
logger = LoggerManager().get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
BASE_EXPONENTIAL_BACKOFF = 2.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RetryConfig(BaseModel):
|
|
29
|
+
"""Configuration for retry behavior.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
max_retries (int): Maximum number of retry attempts.
|
|
33
|
+
base_delay (float): Base delay in seconds between retries.
|
|
34
|
+
max_delay (float): Maximum delay in seconds between retries.
|
|
35
|
+
jitter (bool): Whether to add random jitter to delays.
|
|
36
|
+
timeout (float | None): Overall timeout in seconds for the entire operation. If None, timeout is disabled.
|
|
37
|
+
retry_on_exceptions (tuple[type[Exception], ...]): Tuple of exception types to retry on.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
max_retries: int = Field(default=0, ge=0)
|
|
41
|
+
base_delay: float = Field(default=1.0, gt=0.0)
|
|
42
|
+
max_delay: float = Field(default=10.0, gt=0.0)
|
|
43
|
+
jitter: bool = Field(default=True)
|
|
44
|
+
timeout: float | None = Field(default=None, gt=0.0)
|
|
45
|
+
retry_on_exceptions: tuple[type[Exception], ...] = Field(default=(Exception,))
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def validate_delay_constraints(self) -> "RetryConfig":
|
|
49
|
+
"""Validates that max_delay is greater than or equal to base_delay.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
RetryConfig: The validated configuration.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If max_delay is less than base_delay.
|
|
56
|
+
"""
|
|
57
|
+
if self.timeout == 0:
|
|
58
|
+
logger.warning(
|
|
59
|
+
"Setting timeout=0.0 is deprecated and will raise an error in v0.4. "
|
|
60
|
+
"Please use timeout=None instead to disable timeout."
|
|
61
|
+
)
|
|
62
|
+
self.timeout = None
|
|
63
|
+
|
|
64
|
+
if self.max_delay < self.base_delay:
|
|
65
|
+
raise ValueError("The 'max_delay' parameter must be greater than or equal to 'base_delay'.")
|
|
66
|
+
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _calculate_delay(attempt: int, config: RetryConfig) -> float:
|
|
71
|
+
"""Calculates the delay for the next retry attempt.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
attempt (int): The current attempt number (0-based).
|
|
75
|
+
config (RetryConfig): The retry configuration.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
float: The delay in seconds.
|
|
79
|
+
"""
|
|
80
|
+
delay = config.base_delay * (BASE_EXPONENTIAL_BACKOFF**attempt)
|
|
81
|
+
|
|
82
|
+
if config.jitter:
|
|
83
|
+
jitter_factor = random.uniform(0, 0.25)
|
|
84
|
+
delay += delay * jitter_factor
|
|
85
|
+
|
|
86
|
+
return min(delay, config.max_delay)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _retry_async(
|
|
90
|
+
func: Callable[..., Any],
|
|
91
|
+
*args: Any,
|
|
92
|
+
retry_config: RetryConfig | None = None,
|
|
93
|
+
**kwargs: Any,
|
|
94
|
+
) -> T:
|
|
95
|
+
"""Executes a function with retry logic and exponential backoff.
|
|
96
|
+
|
|
97
|
+
This function executes the provided function with retry logic. It will first try to execute the function once.
|
|
98
|
+
If the function raises an exception that matches the retry_on_exceptions, it will retry up to max_retries times
|
|
99
|
+
with exponential backoff. Therefore, the max number of attempts is max_retries + 1. If provided, the timeout
|
|
100
|
+
applies to the entire retry operation, including all attempts and delays.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
If you set timeout=10.0 and max_retries=3, the entire retry operation (including all attempts
|
|
104
|
+
and delays) will timeout after 10 seconds, not 10 seconds per attempt.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
func (Callable[..., Any]): The function to execute.
|
|
108
|
+
*args (Any): Positional arguments to pass to the function.
|
|
109
|
+
retry_config (RetryConfig | None, optional): Retry configuration. If None, uses default config.
|
|
110
|
+
Defaults to None.
|
|
111
|
+
**kwargs (Any): Keyword arguments to pass to the function.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
T: The result of the function execution.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
Exception: The last exception raised by the function if all retries are exhausted.
|
|
118
|
+
asyncio.TimeoutError: If the overall timeout is exceeded.
|
|
119
|
+
"""
|
|
120
|
+
config = retry_config or RetryConfig()
|
|
121
|
+
|
|
122
|
+
async def attempt_loop() -> T:
|
|
123
|
+
max_retries = config.max_retries + 1
|
|
124
|
+
|
|
125
|
+
for attempt in range(max_retries):
|
|
126
|
+
try:
|
|
127
|
+
result = func(*args, **kwargs)
|
|
128
|
+
if inspect.isawaitable(result):
|
|
129
|
+
return await result
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
except asyncio.TimeoutError:
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
except config.retry_on_exceptions as exc:
|
|
136
|
+
if attempt == config.max_retries:
|
|
137
|
+
logger.error(f"Function {func.__name__} failed after {max_retries} attempts. Last error: {exc}")
|
|
138
|
+
raise exc from None
|
|
139
|
+
|
|
140
|
+
delay = _calculate_delay(attempt, config)
|
|
141
|
+
logger.warning(
|
|
142
|
+
f"Function {func.__name__} failed on attempt {attempt + 1}/{max_retries}. "
|
|
143
|
+
f"Retrying in {delay:.2f} seconds. Error: {exc}"
|
|
144
|
+
)
|
|
145
|
+
await asyncio.sleep(delay)
|
|
146
|
+
|
|
147
|
+
if config.timeout is not None:
|
|
148
|
+
return await asyncio.wait_for(attempt_loop(), timeout=config.timeout)
|
|
149
|
+
|
|
150
|
+
return await attempt_loop()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@overload
|
|
154
|
+
async def retry(
|
|
155
|
+
func: Callable[..., Any],
|
|
156
|
+
*args: Any,
|
|
157
|
+
retry_config: RetryConfig | None = None,
|
|
158
|
+
**kwargs: Any,
|
|
159
|
+
) -> T: ...
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@overload
|
|
163
|
+
def retry(config: RetryConfig | None = None) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def retry(
|
|
167
|
+
func_or_config: Callable[..., Any] | RetryConfig | None = None,
|
|
168
|
+
*args: Any,
|
|
169
|
+
retry_config: RetryConfig | None = None,
|
|
170
|
+
**kwargs: Any,
|
|
171
|
+
) -> T | Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
172
|
+
"""Executes a function with retry logic or creates a retry decorator.
|
|
173
|
+
|
|
174
|
+
This function supports two usage patterns:
|
|
175
|
+
1. Direct function execution: await retry(func, *args, retry_config=config, **kwargs)
|
|
176
|
+
2. Decorator factory: @retry() or @retry(config)
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
func_or_config (Callable[..., Any] | RetryConfig | None, optional): Either a function to execute or a
|
|
180
|
+
RetryConfig for decorator usage. Defaults to None.
|
|
181
|
+
*args (Any): Positional arguments (only used in direct execution mode).
|
|
182
|
+
retry_config (RetryConfig | None, optional): Retry configuration (only used in direct execution mode).
|
|
183
|
+
Defaults to None, in which case no retry nor timeout is applied.
|
|
184
|
+
**kwargs (Any): Keyword arguments (only used in direct execution mode).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
T | Callable[[Callable[..., Any]], Callable[..., Any]]: Either the result of function execution or
|
|
188
|
+
a decorator function.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
Exception: The last exception raised by the function if all retries are exhausted.
|
|
192
|
+
asyncio.TimeoutError: If the overall timeout is exceeded.
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
# Direct function execution
|
|
196
|
+
```python
|
|
197
|
+
result = await retry(my_function, arg1, arg2, retry_config=config)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
# Decorator usage - parameterless
|
|
201
|
+
```python
|
|
202
|
+
@retry()
|
|
203
|
+
async def my_async_function():
|
|
204
|
+
# Use default settings, in which case no retry nor timeout is applied.
|
|
205
|
+
pass
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
# Decorator usage - with custom configuration
|
|
209
|
+
```python
|
|
210
|
+
@retry(RetryConfig(max_retries=3, timeout=120))
|
|
211
|
+
async def my_function():
|
|
212
|
+
# Will retry up to 3 times with 0.5s base delay
|
|
213
|
+
pass
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
# Decorator on sync functions
|
|
217
|
+
```python
|
|
218
|
+
@retry()
|
|
219
|
+
def my_sync_function():
|
|
220
|
+
# Works with sync functions too
|
|
221
|
+
return "success"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
# Decorator on class methods
|
|
225
|
+
```python
|
|
226
|
+
class MyService:
|
|
227
|
+
@retry(RetryConfig(max_retries=2))
|
|
228
|
+
async def get_data(self, id: str):
|
|
229
|
+
return {"id": id, "data": "value"}
|
|
230
|
+
```
|
|
231
|
+
"""
|
|
232
|
+
if callable(func_or_config) and not isinstance(func_or_config, RetryConfig):
|
|
233
|
+
func = func_or_config
|
|
234
|
+
|
|
235
|
+
async def async_wrapper() -> T:
|
|
236
|
+
return await _retry_async(func, *args, retry_config=retry_config, **kwargs)
|
|
237
|
+
|
|
238
|
+
return async_wrapper()
|
|
239
|
+
|
|
240
|
+
config = func_or_config
|
|
241
|
+
|
|
242
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
243
|
+
if asyncio.iscoroutinefunction(func):
|
|
244
|
+
|
|
245
|
+
@functools.wraps(func)
|
|
246
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
247
|
+
return await _retry_async(func, *args, retry_config=config, **kwargs)
|
|
248
|
+
|
|
249
|
+
return async_wrapper
|
|
250
|
+
|
|
251
|
+
@functools.wraps(func)
|
|
252
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
253
|
+
sync_retry = syncify(lambda: _retry_async(func, *args, retry_config=config, **kwargs))
|
|
254
|
+
return sync_retry()
|
|
255
|
+
|
|
256
|
+
return sync_wrapper
|
|
257
|
+
|
|
258
|
+
return decorator
|