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,546 @@
|
|
|
1
|
+
"""Defines an abstract base class for all components used throughout the Gen AI applications.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Dimitrij Ray (dimitrij.ray@gdplabs.id)
|
|
5
|
+
Henry Wicaksono (henry.wicaksono@gdplabs.id)
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
NONE
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import inspect
|
|
14
|
+
import logging
|
|
15
|
+
import warnings
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from traceback import format_exc
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gllm_core.event.event_emitter import EventEmitter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
|
|
26
|
+
from gllm_core.schema.schema_generator import generate_params_model
|
|
27
|
+
from gllm_core.schema.tool import Tool, tool
|
|
28
|
+
from gllm_core.utils import BinaryHandlingStrategy, binary_handler_factory
|
|
29
|
+
from gllm_core.utils.analyzer import MethodSignature, ParameterInfo, ParameterKind, RunProfile, analyze_method
|
|
30
|
+
from gllm_core.utils.logger_manager import LoggerManager
|
|
31
|
+
from gllm_core.utils.main_method_resolver import MainMethodResolver
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(method: Callable) -> Callable:
|
|
35
|
+
"""Decorate a Component method as the async main entrypoint.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
Declare the coroutine that should act as the primary execution path
|
|
39
|
+
for a `Component` subclass. The decorated coroutine will be resolved by
|
|
40
|
+
`Component.run()` unless another subclass overrides the decoration.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
method (Callable): Coroutine to mark as the main entrypoint.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Callable: The same coroutine that is passed to the decorator. The decorator only marks the method as the main
|
|
47
|
+
entrypoint. It does not wrap or change its behavior or signature.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeError: If the decorated callable is not asynchronous.
|
|
51
|
+
"""
|
|
52
|
+
actual_method = method.__func__ if isinstance(method, (classmethod, staticmethod)) else method
|
|
53
|
+
|
|
54
|
+
if not inspect.iscoroutinefunction(actual_method):
|
|
55
|
+
method_name = getattr(actual_method, "__name__", "unknown")
|
|
56
|
+
raise TypeError(f"Main method {method_name!r} must be asynchronous")
|
|
57
|
+
method.__is_main__ = True
|
|
58
|
+
return method
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Component:
|
|
62
|
+
"""An abstract base class for all components used throughout the Gen AI applications.
|
|
63
|
+
|
|
64
|
+
Every instance of Component has access to class-level `_default_log_level` and `_logger`, as detailed below.
|
|
65
|
+
For components that require high observability, it is recommended to set `_default_log_level` to `logging.INFO`
|
|
66
|
+
or higher.
|
|
67
|
+
|
|
68
|
+
Defining Custom Components:
|
|
69
|
+
There are two ways to define the main execution logic for a component:
|
|
70
|
+
|
|
71
|
+
1. **Using the @main decorator (Recommended)**:
|
|
72
|
+
Decorate an async method with `@main` to mark it as the primary entrypoint.
|
|
73
|
+
This is the preferred approach as it provides explicit control over the main method.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
class MyComponent(Component):
|
|
77
|
+
_default_log_level = logging.INFO
|
|
78
|
+
|
|
79
|
+
@main
|
|
80
|
+
async def execute(self, **kwargs: Any) -> Any:
|
|
81
|
+
return "Hello from @main!"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. **Implementing _run method (Deprecated)**:
|
|
85
|
+
Override the abstract `_run` method. This is the traditional approach and still supported.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
class MyComponent(Component):
|
|
89
|
+
_default_log_level = logging.INFO
|
|
90
|
+
|
|
91
|
+
async def _run(self, **kwargs: Any) -> Any:
|
|
92
|
+
return "Hello, World!"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `run()` method resolves the main entrypoint using the following precedence:
|
|
96
|
+
1. Method decorated with @main in the current class.
|
|
97
|
+
2. Method decorated with @main in the nearest ancestor class.
|
|
98
|
+
3. Method named in __main_method__ property.
|
|
99
|
+
4. The _run method (with deprecation warning).
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
run_profile (RunProfile): The profile of the `_run` method.
|
|
103
|
+
This property is used by `Pipeline` to analyze the input requirements of the component.
|
|
104
|
+
In most cases, unless you are working with `Pipeline` and `PipelineStep`s, you will not need to use this
|
|
105
|
+
property.
|
|
106
|
+
|
|
107
|
+
**Do not override this property in your subclass.**
|
|
108
|
+
|
|
109
|
+
You also do not need to write this attribute in your component's docstring.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
_default_log_level = logging.DEBUG
|
|
113
|
+
|
|
114
|
+
def __init_subclass__(cls, **kwargs):
|
|
115
|
+
"""Hook called when a subclass is created.
|
|
116
|
+
|
|
117
|
+
This validates the __main_method__ property and checks for multiple @main decorators
|
|
118
|
+
within the current class definition. Uses MainMethodResolver for consistent validation logic.
|
|
119
|
+
|
|
120
|
+
Note: Multiple inheritance conflicts are intentionally deferred to runtime (get_main())
|
|
121
|
+
to allow class definition to succeed.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
AttributeError: If __main_method__ refers to a non-existent method.
|
|
125
|
+
TypeError: If multiple methods are decorated with @main in the same class.
|
|
126
|
+
"""
|
|
127
|
+
super().__init_subclass__(**kwargs)
|
|
128
|
+
|
|
129
|
+
MainMethodResolver.validate_class(cls)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
@lru_cache(maxsize=None)
|
|
133
|
+
def get_main(cls) -> Callable | None:
|
|
134
|
+
"""Return the resolved main coroutine for this Component class.
|
|
135
|
+
|
|
136
|
+
This method resolves the main method for the Component class following
|
|
137
|
+
the precedence rules:
|
|
138
|
+
1. Most derived coroutine decorated with `@main`.
|
|
139
|
+
2. Method named by `__main_method__`.
|
|
140
|
+
3. `_run` coroutine as a deprecated fallback.
|
|
141
|
+
|
|
142
|
+
Results are cached for performance.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Callable | None: The coroutine that will be executed by `run()` or
|
|
146
|
+
`None` when no entrypoint can be determined.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
TypeError: If conflicting main methods are inherited from multiple ancestors.
|
|
150
|
+
"""
|
|
151
|
+
return MainMethodResolver(cls).resolve()
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
@lru_cache(maxsize=None)
|
|
155
|
+
def _get_input_params_model(cls) -> type[BaseModel] | None:
|
|
156
|
+
"""Generate and cache the input-parameter model for this component class.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
type[BaseModel] | None: A Pydantic model describing the component's
|
|
160
|
+
input parameters, or `None` when no main method can be resolved or the
|
|
161
|
+
model generation fails.
|
|
162
|
+
"""
|
|
163
|
+
main_method = cls.get_main() or cls._run
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
return generate_params_model(main_method, cls.__name__)
|
|
167
|
+
except Exception: # pragma: no cover - defensive fallback
|
|
168
|
+
logging.getLogger(__name__).exception("Failed to generate input params model for %s", cls.__name__)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def input_params(self) -> type[BaseModel] | None:
|
|
172
|
+
"""Return the Pydantic model describing this component's main method input parameters.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
type[BaseModel] | None: The cached model that mirrors the signature of
|
|
176
|
+
the resolved main method, or `None` if no main method can be
|
|
177
|
+
determined.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
```python
|
|
181
|
+
from pydantic import ValidationError
|
|
182
|
+
|
|
183
|
+
component = SomeComponent()
|
|
184
|
+
ParamsModel = component.input_params
|
|
185
|
+
assert ParamsModel.__name__ == "SomeComponentParams"
|
|
186
|
+
fields = list(ParamsModel.model_fields)
|
|
187
|
+
|
|
188
|
+
# Validation with valid params
|
|
189
|
+
params = ParamsModel(text="hello")
|
|
190
|
+
|
|
191
|
+
# Validation catches missing required fields
|
|
192
|
+
try:
|
|
193
|
+
invalid_params = ParamsModel() # Missing required 'text' field
|
|
194
|
+
except ValidationError as e:
|
|
195
|
+
print(f"Validation failed: {e.error_count()} errors")
|
|
196
|
+
|
|
197
|
+
# Argument construction
|
|
198
|
+
payload = params.model_dump()
|
|
199
|
+
result = await component.run(**payload)
|
|
200
|
+
```
|
|
201
|
+
"""
|
|
202
|
+
return self.__class__._get_input_params_model()
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def _logger(self) -> logging.Logger:
|
|
206
|
+
"""Get a logger instance configured for this component class.
|
|
207
|
+
|
|
208
|
+
The logger uses the class name as the logger name and applies
|
|
209
|
+
the configured log level for this component class.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
logging.Logger: A configured logger instance.
|
|
213
|
+
"""
|
|
214
|
+
if not hasattr(self, "_logger_instance"):
|
|
215
|
+
logger = LoggerManager().get_logger(self.__class__.__name__)
|
|
216
|
+
logger.setLevel(self.__class__._default_log_level)
|
|
217
|
+
self._logger_instance = logger
|
|
218
|
+
|
|
219
|
+
return self._logger_instance
|
|
220
|
+
|
|
221
|
+
@_logger.setter
|
|
222
|
+
def _logger(self, logger: logging.Logger) -> None:
|
|
223
|
+
"""Set a custom logger instance for this component.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
logger (logging.Logger): The logger instance to use.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
TypeError: If `logger` is not an instance of `logging.Logger`.
|
|
230
|
+
"""
|
|
231
|
+
if not isinstance(logger, logging.Logger):
|
|
232
|
+
raise TypeError("_logger must be an instance of logging.Logger")
|
|
233
|
+
|
|
234
|
+
logger.setLevel(self.__class__._default_log_level)
|
|
235
|
+
self._logger_instance = logger
|
|
236
|
+
|
|
237
|
+
async def run(self, **kwargs: Any) -> Any:
|
|
238
|
+
"""Runs the operations defined for the component.
|
|
239
|
+
|
|
240
|
+
This method emits the provided input arguments using an EventEmitter instance if available, executes the
|
|
241
|
+
resolved main method, and emits the resulting output if the EventEmitter is provided.
|
|
242
|
+
|
|
243
|
+
The main method is resolved using the following precedence:
|
|
244
|
+
1. Method decorated with @main in the current class.
|
|
245
|
+
2. Method decorated with @main in the nearest ancestor class.
|
|
246
|
+
3. Method named in __main_method__ property.
|
|
247
|
+
4. The _run method (with deprecation warning).
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
**kwargs (Any): A dictionary of arguments to be processed. May include an `event_emitter`
|
|
251
|
+
key with an EventEmitter instance.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Any: The result of the resolved main method.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
TypeError: If conflicting main methods are inherited from multiple ancestors.
|
|
258
|
+
AttributeError: If __main_method__ refers to a non-existent method.
|
|
259
|
+
"""
|
|
260
|
+
event_emitter = kwargs.get("event_emitter")
|
|
261
|
+
|
|
262
|
+
input_event = self._format_input_event(kwargs)
|
|
263
|
+
await self._log_io_event(input_event, event_emitter)
|
|
264
|
+
|
|
265
|
+
main_method = self.get_main()
|
|
266
|
+
if main_method:
|
|
267
|
+
output = await main_method(self, **kwargs)
|
|
268
|
+
else:
|
|
269
|
+
self._logger.warning(
|
|
270
|
+
"_run method is deprecated and will be removed in a future release.\n"
|
|
271
|
+
"Use the `@main` decorator instead."
|
|
272
|
+
)
|
|
273
|
+
output = await self._run(**kwargs)
|
|
274
|
+
|
|
275
|
+
output_event = self._format_output_event(output)
|
|
276
|
+
await self._log_io_event(output_event, event_emitter)
|
|
277
|
+
|
|
278
|
+
return output
|
|
279
|
+
|
|
280
|
+
def as_tool(self, name: str | None = None, description: str | None = None, title: str | None = None) -> Tool:
|
|
281
|
+
"""Convert the component's main method into a `Tool` instance.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
```python
|
|
285
|
+
from gllm_core.schema import Component, main
|
|
286
|
+
|
|
287
|
+
class MyComponent(Component):
|
|
288
|
+
@main
|
|
289
|
+
async def my_method(self, param: str) -> str:
|
|
290
|
+
return param
|
|
291
|
+
|
|
292
|
+
component = MyComponent()
|
|
293
|
+
tool = component.as_tool()
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
name (str | None, optional): Identifier for the resulting tool. Defaults to the component class name.
|
|
298
|
+
description (str | None, optional): Summary of the tool's behavior. Defaults to None, in which case the
|
|
299
|
+
main method's docstring is used.
|
|
300
|
+
title (str | None, optional): Optional display title for the tool. Defaults to None, in which case the
|
|
301
|
+
component's class name is used.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Tool: The tool wrapping the component's main method.
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
RuntimeError: If the component does not declare a main method using @main or __main_method__.
|
|
308
|
+
"""
|
|
309
|
+
main_method = self.get_main()
|
|
310
|
+
if main_method is None or main_method is self.__class__._run:
|
|
311
|
+
raise RuntimeError(
|
|
312
|
+
"Component main method is not declared. Use @main or __main_method__ before converting to a tool."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
bound_main = main_method.__get__(self, self.__class__)
|
|
316
|
+
|
|
317
|
+
tool_name = name or self.__class__.__name__
|
|
318
|
+
tool_decorator = tool(name=tool_name, description=description, title=title)
|
|
319
|
+
component_tool = tool_decorator(bound_main)
|
|
320
|
+
|
|
321
|
+
if not isinstance(component_tool, Tool): # pragma: no cover - Defensive: decorator should always return Tool
|
|
322
|
+
raise TypeError("Failed to convert component main method into a Tool instance.")
|
|
323
|
+
|
|
324
|
+
return component_tool
|
|
325
|
+
|
|
326
|
+
async def _log_io_event(self, event_message: str, event_emitter: EventEmitter | None = None) -> None:
|
|
327
|
+
"""Logs an event message and emits it via the event emitter if available.
|
|
328
|
+
|
|
329
|
+
This helper method encapsulates the pattern of logging a message and then emitting
|
|
330
|
+
the same message through an event emitter when available.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
event_message (str): The message to log and emit.
|
|
334
|
+
event_emitter (EventEmitter | None, optional): An event emitter instance. Defaults to None.
|
|
335
|
+
"""
|
|
336
|
+
if event_message:
|
|
337
|
+
self._logger.log(self.__class__._default_log_level, event_message)
|
|
338
|
+
if event_emitter:
|
|
339
|
+
await event_emitter.emit(event_message)
|
|
340
|
+
|
|
341
|
+
def _format_input_event(self, input_dict: dict) -> str:
|
|
342
|
+
"""Formats the input data into an event message string.
|
|
343
|
+
|
|
344
|
+
This method constructs a formatted event message that details the input data being processed.
|
|
345
|
+
It includes the class name and each key-value pair from the input dictionary.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
input_dict (dict): A dictionary containing the input data to be emitted.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
str: A formatted event message string representing the input data.
|
|
352
|
+
"""
|
|
353
|
+
message = f"[Start {self.__class__.__name__!r}]"
|
|
354
|
+
|
|
355
|
+
if input_dict:
|
|
356
|
+
message += " Processing input: "
|
|
357
|
+
handler = binary_handler_factory(BinaryHandlingStrategy.SHOW_SIZE)
|
|
358
|
+
for key, value in input_dict.items():
|
|
359
|
+
formatted_value = handler(value) if isinstance(value, bytes) else value
|
|
360
|
+
message += f"\n - {key}: {formatted_value!r}"
|
|
361
|
+
|
|
362
|
+
return message
|
|
363
|
+
|
|
364
|
+
def _format_output_event(self, output: Any) -> str:
|
|
365
|
+
"""Formats the output data into an event message string.
|
|
366
|
+
|
|
367
|
+
This method constructs a formatted event message that details the output produced by the process.
|
|
368
|
+
It includes the class name and the output data.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
output (Any): The output data to be emitted.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
str: A formatted event message string representing the output data.
|
|
375
|
+
"""
|
|
376
|
+
handler = binary_handler_factory(BinaryHandlingStrategy.SHOW_SIZE)
|
|
377
|
+
formatted_output = handler(output) if isinstance(output, bytes) else output
|
|
378
|
+
message = f"[Finished {self.__class__.__name__!r}] Successfully produced output:\n{formatted_output!r}"
|
|
379
|
+
return message
|
|
380
|
+
|
|
381
|
+
async def _run(self, **kwargs: Any) -> Any:
|
|
382
|
+
"""Defines the core process logic to be implemented by subclasses.
|
|
383
|
+
|
|
384
|
+
This method can be implemented in a subclass to define the specific process that will be executed.
|
|
385
|
+
It is called by the `run` method and is responsible for handling the provided input arguments and producing
|
|
386
|
+
an output.
|
|
387
|
+
|
|
388
|
+
Deprecation:
|
|
389
|
+
This method is deprecated and will be removed in a future release. Use the `@main` decorator instead.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
**kwargs (Any): A dictionary of arguments required for the process.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Any: The result of the process.
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
NotImplementedError: If the method is not implemented in a subclass.
|
|
399
|
+
"""
|
|
400
|
+
raise NotImplementedError
|
|
401
|
+
|
|
402
|
+
# If you are looking to implement a `Component`, you do not need to worry about the methods below. Simply implement
|
|
403
|
+
# `_run` method in your subclass; you are good to go.
|
|
404
|
+
#
|
|
405
|
+
# The methods below are used to analyze the `_run` method and retrieve its profile.
|
|
406
|
+
#
|
|
407
|
+
# The addition of profiling-related methods to our `Component` class is crucial for achieving effective input
|
|
408
|
+
# validation in our `Pipeline`. By dynamically analyzing the `_run` method, we can generate a detailed profile of
|
|
409
|
+
# its input requirements. This profile enables the pipeline to perform comprehensive input validation, ensuring that
|
|
410
|
+
# all components receive the correct inputs they need to function properly.
|
|
411
|
+
#
|
|
412
|
+
# This approach enhances the robustness and reliability of our pipeline while reducing the need for manual
|
|
413
|
+
# configuration and documentation of input requirements.
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def run_profile(self) -> RunProfile:
|
|
417
|
+
"""Analyzes the `_run` method and retrieves its profile.
|
|
418
|
+
|
|
419
|
+
This property method analyzes the `_run` method of the class to generate a `RunProfile` object.
|
|
420
|
+
It also updates the method signatures for methods that fully utilize the arguments.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
RunProfile: The profile of the `_run` method, including method signatures for full-pass argument usages.
|
|
424
|
+
"""
|
|
425
|
+
profile = self._analyze_run_method()
|
|
426
|
+
for method_name in profile.full_pass_methods:
|
|
427
|
+
profile.method_signatures[method_name] = self._get_method_signature(method_name)
|
|
428
|
+
return profile
|
|
429
|
+
|
|
430
|
+
@classmethod
|
|
431
|
+
@lru_cache
|
|
432
|
+
def _analyze_run_method(cls) -> RunProfile:
|
|
433
|
+
"""Analyzes the `_run` method and retrieves its profile.
|
|
434
|
+
|
|
435
|
+
This method inspects the `_run` method of the class, parses its source code into an AST,
|
|
436
|
+
and uses the `RunAnalyzer` to visit the AST nodes and generate a `RunProfile` object.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
RunProfile: The profile of the `_run` method, including method signatures for full-pass argument usages.
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
method = cls._run
|
|
443
|
+
return analyze_method(cls, method)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
warnings.warn(
|
|
446
|
+
f"Failed to analyze the _run method: {e}.\n{format_exc()}",
|
|
447
|
+
RuntimeWarning,
|
|
448
|
+
stacklevel=2,
|
|
449
|
+
)
|
|
450
|
+
return RunProfile()
|
|
451
|
+
|
|
452
|
+
@classmethod
|
|
453
|
+
def _get_method_signature(cls, method_name: str) -> MethodSignature:
|
|
454
|
+
"""Retrieves the method signature for a given method name.
|
|
455
|
+
|
|
456
|
+
This method uses the `inspect` module to get the signature of the specified method
|
|
457
|
+
and constructs a `MethodSignature` object containing parameter information and
|
|
458
|
+
whether the method is asynchronous.
|
|
459
|
+
|
|
460
|
+
Known limitations:
|
|
461
|
+
1. Nested functions or external methods are not supported.
|
|
462
|
+
2. Nested **kwargs are not supported.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
method_name (str): The name of the method to retrieve the signature for.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
MethodSignature: An object containing the method's signature details.
|
|
469
|
+
"""
|
|
470
|
+
if "." in method_name:
|
|
471
|
+
class_name, method_name = method_name.rsplit(".", 1)
|
|
472
|
+
if class_name == cls.__name__:
|
|
473
|
+
method = getattr(cls, method_name, None)
|
|
474
|
+
else:
|
|
475
|
+
# This is a nested function or external method, we can't get its signature
|
|
476
|
+
return MethodSignature(parameters={}, is_async=False)
|
|
477
|
+
else:
|
|
478
|
+
method = getattr(cls, method_name, None)
|
|
479
|
+
|
|
480
|
+
if method is None:
|
|
481
|
+
# Try to find the function in the global scope
|
|
482
|
+
method = cls._find_global_function(method_name)
|
|
483
|
+
|
|
484
|
+
if method is None:
|
|
485
|
+
# If we still can't find the method, return a default signature
|
|
486
|
+
return MethodSignature(parameters={}, is_async=False)
|
|
487
|
+
|
|
488
|
+
return cls._create_method_signature(method)
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
@lru_cache
|
|
492
|
+
def _find_global_function(func_name: str) -> Callable | None:
|
|
493
|
+
"""Finds a global function by name.
|
|
494
|
+
|
|
495
|
+
This method searches for a function with the given name in the global scope.
|
|
496
|
+
If the function is not found in the global scope, it searches in the imported modules.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
func_name (str): The name of the function to find.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Callable | None: The function if found, otherwise None.
|
|
503
|
+
"""
|
|
504
|
+
# Get the global scope
|
|
505
|
+
globals_dict = globals()
|
|
506
|
+
|
|
507
|
+
# Try to find the function in the global scope
|
|
508
|
+
if func_name in globals_dict and callable(globals_dict[func_name]):
|
|
509
|
+
return globals_dict[func_name]
|
|
510
|
+
|
|
511
|
+
# If not found, try to find it in imported modules
|
|
512
|
+
for value in globals_dict.values():
|
|
513
|
+
if inspect.ismodule(value):
|
|
514
|
+
func = getattr(value, func_name, None)
|
|
515
|
+
if callable(func):
|
|
516
|
+
return func
|
|
517
|
+
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _create_method_signature(method: Callable) -> MethodSignature:
|
|
522
|
+
"""Creates a method signature object from the provided method.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
method (Callable): The method to create a signature for.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
MethodSignature: An object containing the method's signature details.
|
|
529
|
+
"""
|
|
530
|
+
sig = inspect.signature(method)
|
|
531
|
+
params = sig.parameters
|
|
532
|
+
|
|
533
|
+
is_instance_method = list(params.keys())[0] == "self" if params else False
|
|
534
|
+
|
|
535
|
+
return MethodSignature(
|
|
536
|
+
parameters={
|
|
537
|
+
name: ParameterInfo(
|
|
538
|
+
kind=ParameterKind(str(param.kind).rsplit(".", maxsplit=1)[-1].lower()),
|
|
539
|
+
default=str(param.default) if param.default is not inspect.Parameter.empty else None,
|
|
540
|
+
annotation=str(param.annotation) if param.annotation is not inspect.Parameter.empty else None,
|
|
541
|
+
)
|
|
542
|
+
for name, param in params.items()
|
|
543
|
+
if not (is_instance_method and name == "self") # Skip 'self' for instance methods
|
|
544
|
+
},
|
|
545
|
+
is_async=inspect.iscoroutinefunction(method),
|
|
546
|
+
)
|