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.
Files changed (78) hide show
  1. gllm_core/__init__.py +1 -0
  2. gllm_core/__init__.pyi +0 -0
  3. gllm_core/adapters/__init__.py +5 -0
  4. gllm_core/adapters/__init__.pyi +3 -0
  5. gllm_core/adapters/tool/__init__.py +6 -0
  6. gllm_core/adapters/tool/__init__.pyi +4 -0
  7. gllm_core/adapters/tool/google_adk.py +91 -0
  8. gllm_core/adapters/tool/google_adk.pyi +23 -0
  9. gllm_core/adapters/tool/langchain.py +130 -0
  10. gllm_core/adapters/tool/langchain.pyi +31 -0
  11. gllm_core/constants.py +55 -0
  12. gllm_core/constants.pyi +36 -0
  13. gllm_core/event/__init__.py +6 -0
  14. gllm_core/event/__init__.pyi +4 -0
  15. gllm_core/event/event_emitter.py +211 -0
  16. gllm_core/event/event_emitter.pyi +155 -0
  17. gllm_core/event/handler/__init__.py +7 -0
  18. gllm_core/event/handler/__init__.pyi +5 -0
  19. gllm_core/event/handler/console_event_handler.py +48 -0
  20. gllm_core/event/handler/console_event_handler.pyi +32 -0
  21. gllm_core/event/handler/event_handler.py +89 -0
  22. gllm_core/event/handler/event_handler.pyi +51 -0
  23. gllm_core/event/handler/print_event_handler.py +130 -0
  24. gllm_core/event/handler/print_event_handler.pyi +33 -0
  25. gllm_core/event/handler/stream_event_handler.py +85 -0
  26. gllm_core/event/handler/stream_event_handler.pyi +62 -0
  27. gllm_core/event/hook/__init__.py +5 -0
  28. gllm_core/event/hook/__init__.pyi +3 -0
  29. gllm_core/event/hook/event_hook.py +30 -0
  30. gllm_core/event/hook/event_hook.pyi +18 -0
  31. gllm_core/event/hook/json_stringify_event_hook.py +32 -0
  32. gllm_core/event/hook/json_stringify_event_hook.pyi +16 -0
  33. gllm_core/event/messenger.py +133 -0
  34. gllm_core/event/messenger.pyi +66 -0
  35. gllm_core/schema/__init__.py +8 -0
  36. gllm_core/schema/__init__.pyi +6 -0
  37. gllm_core/schema/chunk.py +148 -0
  38. gllm_core/schema/chunk.pyi +66 -0
  39. gllm_core/schema/component.py +546 -0
  40. gllm_core/schema/component.pyi +205 -0
  41. gllm_core/schema/event.py +50 -0
  42. gllm_core/schema/event.pyi +33 -0
  43. gllm_core/schema/schema_generator.py +150 -0
  44. gllm_core/schema/schema_generator.pyi +35 -0
  45. gllm_core/schema/tool.py +418 -0
  46. gllm_core/schema/tool.pyi +198 -0
  47. gllm_core/utils/__init__.py +32 -0
  48. gllm_core/utils/__init__.pyi +13 -0
  49. gllm_core/utils/analyzer.py +256 -0
  50. gllm_core/utils/analyzer.pyi +123 -0
  51. gllm_core/utils/binary_handler_factory.py +99 -0
  52. gllm_core/utils/binary_handler_factory.pyi +62 -0
  53. gllm_core/utils/chunk_metadata_merger.py +102 -0
  54. gllm_core/utils/chunk_metadata_merger.pyi +41 -0
  55. gllm_core/utils/concurrency.py +184 -0
  56. gllm_core/utils/concurrency.pyi +94 -0
  57. gllm_core/utils/event_formatter.py +69 -0
  58. gllm_core/utils/event_formatter.pyi +30 -0
  59. gllm_core/utils/google_sheets.py +115 -0
  60. gllm_core/utils/google_sheets.pyi +18 -0
  61. gllm_core/utils/imports.py +91 -0
  62. gllm_core/utils/imports.pyi +42 -0
  63. gllm_core/utils/logger_manager.py +339 -0
  64. gllm_core/utils/logger_manager.pyi +176 -0
  65. gllm_core/utils/main_method_resolver.py +185 -0
  66. gllm_core/utils/main_method_resolver.pyi +54 -0
  67. gllm_core/utils/merger_method.py +130 -0
  68. gllm_core/utils/merger_method.pyi +49 -0
  69. gllm_core/utils/retry.py +258 -0
  70. gllm_core/utils/retry.pyi +41 -0
  71. gllm_core/utils/similarity.py +29 -0
  72. gllm_core/utils/similarity.pyi +10 -0
  73. gllm_core/utils/validation.py +26 -0
  74. gllm_core/utils/validation.pyi +12 -0
  75. gllm_core_binary-0.4.4.dist-info/METADATA +177 -0
  76. gllm_core_binary-0.4.4.dist-info/RECORD +78 -0
  77. gllm_core_binary-0.4.4.dist-info/WHEEL +5 -0
  78. 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
+ )