nvidia-nat 1.3.0.dev2__py3-none-any.whl → 1.3.0rc2__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 (250) hide show
  1. aiq/__init__.py +2 -2
  2. nat/agent/base.py +24 -15
  3. nat/agent/dual_node.py +9 -4
  4. nat/agent/prompt_optimizer/prompt.py +68 -0
  5. nat/agent/prompt_optimizer/register.py +149 -0
  6. nat/agent/react_agent/agent.py +79 -47
  7. nat/agent/react_agent/register.py +50 -22
  8. nat/agent/reasoning_agent/reasoning_agent.py +11 -9
  9. nat/agent/register.py +1 -1
  10. nat/agent/rewoo_agent/agent.py +326 -148
  11. nat/agent/rewoo_agent/prompt.py +19 -22
  12. nat/agent/rewoo_agent/register.py +54 -27
  13. nat/agent/tool_calling_agent/agent.py +84 -28
  14. nat/agent/tool_calling_agent/register.py +51 -28
  15. nat/authentication/api_key/api_key_auth_provider.py +2 -2
  16. nat/authentication/credential_validator/bearer_token_validator.py +557 -0
  17. nat/authentication/http_basic_auth/http_basic_auth_provider.py +1 -1
  18. nat/authentication/interfaces.py +5 -2
  19. nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +69 -36
  20. nat/authentication/oauth2/oauth2_resource_server_config.py +124 -0
  21. nat/authentication/register.py +0 -1
  22. nat/builder/builder.py +56 -24
  23. nat/builder/component_utils.py +9 -5
  24. nat/builder/context.py +68 -17
  25. nat/builder/eval_builder.py +16 -11
  26. nat/builder/framework_enum.py +1 -0
  27. nat/builder/front_end.py +1 -1
  28. nat/builder/function.py +378 -8
  29. nat/builder/function_base.py +3 -3
  30. nat/builder/function_info.py +6 -8
  31. nat/builder/user_interaction_manager.py +2 -2
  32. nat/builder/workflow.py +13 -1
  33. nat/builder/workflow_builder.py +281 -76
  34. nat/cli/cli_utils/config_override.py +2 -2
  35. nat/cli/commands/evaluate.py +1 -1
  36. nat/cli/commands/info/info.py +16 -6
  37. nat/cli/commands/info/list_channels.py +1 -1
  38. nat/cli/commands/info/list_components.py +7 -8
  39. nat/cli/commands/mcp/__init__.py +14 -0
  40. nat/cli/commands/mcp/mcp.py +986 -0
  41. nat/cli/commands/object_store/__init__.py +14 -0
  42. nat/cli/commands/object_store/object_store.py +227 -0
  43. nat/cli/commands/optimize.py +90 -0
  44. nat/cli/commands/registry/publish.py +2 -2
  45. nat/cli/commands/registry/pull.py +2 -2
  46. nat/cli/commands/registry/remove.py +2 -2
  47. nat/cli/commands/registry/search.py +15 -17
  48. nat/cli/commands/start.py +16 -5
  49. nat/cli/commands/uninstall.py +1 -1
  50. nat/cli/commands/workflow/templates/config.yml.j2 +14 -13
  51. nat/cli/commands/workflow/templates/pyproject.toml.j2 +4 -1
  52. nat/cli/commands/workflow/templates/register.py.j2 +2 -3
  53. nat/cli/commands/workflow/templates/workflow.py.j2 +35 -21
  54. nat/cli/commands/workflow/workflow_commands.py +62 -22
  55. nat/cli/entrypoint.py +8 -10
  56. nat/cli/main.py +3 -0
  57. nat/cli/register_workflow.py +38 -4
  58. nat/cli/type_registry.py +75 -6
  59. nat/control_flow/__init__.py +0 -0
  60. nat/control_flow/register.py +20 -0
  61. nat/control_flow/router_agent/__init__.py +0 -0
  62. nat/control_flow/router_agent/agent.py +329 -0
  63. nat/control_flow/router_agent/prompt.py +48 -0
  64. nat/control_flow/router_agent/register.py +91 -0
  65. nat/control_flow/sequential_executor.py +166 -0
  66. nat/data_models/agent.py +34 -0
  67. nat/data_models/api_server.py +74 -66
  68. nat/data_models/authentication.py +23 -9
  69. nat/data_models/common.py +1 -1
  70. nat/data_models/component.py +2 -0
  71. nat/data_models/component_ref.py +11 -0
  72. nat/data_models/config.py +41 -17
  73. nat/data_models/dataset_handler.py +1 -1
  74. nat/data_models/discovery_metadata.py +4 -4
  75. nat/data_models/evaluate.py +4 -1
  76. nat/data_models/function.py +34 -0
  77. nat/data_models/function_dependencies.py +14 -6
  78. nat/data_models/gated_field_mixin.py +242 -0
  79. nat/data_models/intermediate_step.py +3 -3
  80. nat/data_models/optimizable.py +119 -0
  81. nat/data_models/optimizer.py +149 -0
  82. nat/data_models/span.py +41 -3
  83. nat/data_models/swe_bench_model.py +1 -1
  84. nat/data_models/temperature_mixin.py +44 -0
  85. nat/data_models/thinking_mixin.py +86 -0
  86. nat/data_models/top_p_mixin.py +44 -0
  87. nat/embedder/nim_embedder.py +1 -1
  88. nat/embedder/openai_embedder.py +1 -1
  89. nat/embedder/register.py +0 -1
  90. nat/eval/config.py +3 -1
  91. nat/eval/dataset_handler/dataset_handler.py +71 -7
  92. nat/eval/evaluate.py +86 -31
  93. nat/eval/evaluator/base_evaluator.py +1 -1
  94. nat/eval/evaluator/evaluator_model.py +13 -0
  95. nat/eval/intermediate_step_adapter.py +1 -1
  96. nat/eval/rag_evaluator/evaluate.py +2 -2
  97. nat/eval/rag_evaluator/register.py +3 -3
  98. nat/eval/register.py +4 -1
  99. nat/eval/remote_workflow.py +3 -3
  100. nat/eval/runtime_evaluator/__init__.py +14 -0
  101. nat/eval/runtime_evaluator/evaluate.py +123 -0
  102. nat/eval/runtime_evaluator/register.py +100 -0
  103. nat/eval/swe_bench_evaluator/evaluate.py +6 -6
  104. nat/eval/trajectory_evaluator/evaluate.py +1 -1
  105. nat/eval/trajectory_evaluator/register.py +1 -1
  106. nat/eval/tunable_rag_evaluator/evaluate.py +4 -7
  107. nat/eval/utils/eval_trace_ctx.py +89 -0
  108. nat/eval/utils/weave_eval.py +18 -9
  109. nat/experimental/decorators/experimental_warning_decorator.py +27 -7
  110. nat/experimental/test_time_compute/functions/plan_select_execute_function.py +7 -3
  111. nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +3 -3
  112. nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +1 -1
  113. nat/experimental/test_time_compute/models/strategy_base.py +5 -4
  114. nat/experimental/test_time_compute/register.py +0 -1
  115. nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +1 -3
  116. nat/front_ends/console/authentication_flow_handler.py +82 -30
  117. nat/front_ends/console/console_front_end_plugin.py +8 -5
  118. nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +52 -17
  119. nat/front_ends/fastapi/dask_client_mixin.py +65 -0
  120. nat/front_ends/fastapi/fastapi_front_end_config.py +36 -5
  121. nat/front_ends/fastapi/fastapi_front_end_controller.py +4 -4
  122. nat/front_ends/fastapi/fastapi_front_end_plugin.py +135 -4
  123. nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +452 -282
  124. nat/front_ends/fastapi/job_store.py +518 -99
  125. nat/front_ends/fastapi/main.py +11 -19
  126. nat/front_ends/fastapi/message_handler.py +13 -14
  127. nat/front_ends/fastapi/message_validator.py +19 -19
  128. nat/front_ends/fastapi/response_helpers.py +4 -4
  129. nat/front_ends/fastapi/step_adaptor.py +2 -2
  130. nat/front_ends/fastapi/utils.py +57 -0
  131. nat/front_ends/mcp/introspection_token_verifier.py +73 -0
  132. nat/front_ends/mcp/mcp_front_end_config.py +10 -1
  133. nat/front_ends/mcp/mcp_front_end_plugin.py +45 -13
  134. nat/front_ends/mcp/mcp_front_end_plugin_worker.py +116 -8
  135. nat/front_ends/mcp/tool_converter.py +44 -14
  136. nat/front_ends/register.py +0 -1
  137. nat/front_ends/simple_base/simple_front_end_plugin_base.py +3 -1
  138. nat/llm/aws_bedrock_llm.py +24 -12
  139. nat/llm/azure_openai_llm.py +13 -6
  140. nat/llm/litellm_llm.py +69 -0
  141. nat/llm/nim_llm.py +20 -8
  142. nat/llm/openai_llm.py +14 -6
  143. nat/llm/register.py +4 -1
  144. nat/llm/utils/env_config_value.py +2 -3
  145. nat/llm/utils/thinking.py +215 -0
  146. nat/meta/pypi.md +9 -9
  147. nat/object_store/register.py +0 -1
  148. nat/observability/exporter/base_exporter.py +3 -3
  149. nat/observability/exporter/file_exporter.py +1 -1
  150. nat/observability/exporter/processing_exporter.py +309 -81
  151. nat/observability/exporter/span_exporter.py +35 -15
  152. nat/observability/exporter_manager.py +7 -7
  153. nat/observability/mixin/file_mixin.py +7 -7
  154. nat/observability/mixin/redaction_config_mixin.py +42 -0
  155. nat/observability/mixin/tagging_config_mixin.py +62 -0
  156. nat/observability/mixin/type_introspection_mixin.py +420 -107
  157. nat/observability/processor/batching_processor.py +5 -7
  158. nat/observability/processor/falsy_batch_filter_processor.py +55 -0
  159. nat/observability/processor/processor.py +3 -0
  160. nat/observability/processor/processor_factory.py +70 -0
  161. nat/observability/processor/redaction/__init__.py +24 -0
  162. nat/observability/processor/redaction/contextual_redaction_processor.py +125 -0
  163. nat/observability/processor/redaction/contextual_span_redaction_processor.py +66 -0
  164. nat/observability/processor/redaction/redaction_processor.py +177 -0
  165. nat/observability/processor/redaction/span_header_redaction_processor.py +92 -0
  166. nat/observability/processor/span_tagging_processor.py +68 -0
  167. nat/observability/register.py +6 -4
  168. nat/profiler/calc/calc_runner.py +3 -4
  169. nat/profiler/callbacks/agno_callback_handler.py +1 -1
  170. nat/profiler/callbacks/langchain_callback_handler.py +6 -6
  171. nat/profiler/callbacks/llama_index_callback_handler.py +3 -3
  172. nat/profiler/callbacks/semantic_kernel_callback_handler.py +3 -3
  173. nat/profiler/data_frame_row.py +1 -1
  174. nat/profiler/decorators/framework_wrapper.py +62 -13
  175. nat/profiler/decorators/function_tracking.py +160 -3
  176. nat/profiler/forecasting/models/forecasting_base_model.py +3 -1
  177. nat/profiler/forecasting/models/linear_model.py +1 -1
  178. nat/profiler/forecasting/models/random_forest_regressor.py +1 -1
  179. nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +1 -1
  180. nat/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +1 -1
  181. nat/profiler/inference_optimization/data_models.py +3 -3
  182. nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +8 -9
  183. nat/profiler/inference_optimization/token_uniqueness.py +1 -1
  184. nat/profiler/parameter_optimization/__init__.py +0 -0
  185. nat/profiler/parameter_optimization/optimizable_utils.py +93 -0
  186. nat/profiler/parameter_optimization/optimizer_runtime.py +67 -0
  187. nat/profiler/parameter_optimization/parameter_optimizer.py +153 -0
  188. nat/profiler/parameter_optimization/parameter_selection.py +107 -0
  189. nat/profiler/parameter_optimization/pareto_visualizer.py +380 -0
  190. nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
  191. nat/profiler/parameter_optimization/update_helpers.py +66 -0
  192. nat/profiler/profile_runner.py +14 -9
  193. nat/profiler/utils.py +4 -2
  194. nat/registry_handlers/local/local_handler.py +2 -2
  195. nat/registry_handlers/package_utils.py +1 -2
  196. nat/registry_handlers/pypi/pypi_handler.py +23 -26
  197. nat/registry_handlers/register.py +3 -4
  198. nat/registry_handlers/rest/rest_handler.py +12 -13
  199. nat/retriever/milvus/retriever.py +2 -2
  200. nat/retriever/nemo_retriever/retriever.py +1 -1
  201. nat/retriever/register.py +0 -1
  202. nat/runtime/loader.py +2 -2
  203. nat/runtime/runner.py +106 -8
  204. nat/runtime/session.py +69 -8
  205. nat/settings/global_settings.py +16 -5
  206. nat/tool/chat_completion.py +5 -2
  207. nat/tool/code_execution/local_sandbox/local_sandbox_server.py +3 -3
  208. nat/tool/datetime_tools.py +49 -9
  209. nat/tool/document_search.py +2 -2
  210. nat/tool/github_tools.py +450 -0
  211. nat/tool/memory_tools/get_memory_tool.py +1 -1
  212. nat/tool/nvidia_rag.py +1 -1
  213. nat/tool/register.py +2 -9
  214. nat/tool/retriever.py +3 -2
  215. nat/utils/callable_utils.py +70 -0
  216. nat/utils/data_models/schema_validator.py +3 -3
  217. nat/utils/decorators.py +210 -0
  218. nat/utils/exception_handlers/automatic_retries.py +104 -51
  219. nat/utils/exception_handlers/schemas.py +1 -1
  220. nat/utils/io/yaml_tools.py +2 -2
  221. nat/utils/log_levels.py +25 -0
  222. nat/utils/reactive/base/observable_base.py +2 -2
  223. nat/utils/reactive/base/observer_base.py +1 -1
  224. nat/utils/reactive/observable.py +2 -2
  225. nat/utils/reactive/observer.py +4 -4
  226. nat/utils/reactive/subscription.py +1 -1
  227. nat/utils/settings/global_settings.py +6 -8
  228. nat/utils/type_converter.py +4 -3
  229. nat/utils/type_utils.py +9 -5
  230. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/METADATA +42 -18
  231. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/RECORD +238 -196
  232. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/entry_points.txt +1 -0
  233. nat/cli/commands/info/list_mcp.py +0 -304
  234. nat/tool/github_tools/create_github_commit.py +0 -133
  235. nat/tool/github_tools/create_github_issue.py +0 -87
  236. nat/tool/github_tools/create_github_pr.py +0 -106
  237. nat/tool/github_tools/get_github_file.py +0 -106
  238. nat/tool/github_tools/get_github_issue.py +0 -166
  239. nat/tool/github_tools/get_github_pr.py +0 -256
  240. nat/tool/github_tools/update_github_issue.py +0 -100
  241. nat/tool/mcp/exceptions.py +0 -142
  242. nat/tool/mcp/mcp_client.py +0 -255
  243. nat/tool/mcp/mcp_tool.py +0 -96
  244. nat/utils/exception_handlers/mcp.py +0 -211
  245. /nat/{tool/github_tools → agent/prompt_optimizer}/__init__.py +0 -0
  246. /nat/{tool/mcp → authentication/credential_validator}/__init__.py +0 -0
  247. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/WHEEL +0 -0
  248. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  249. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/licenses/LICENSE.md +0 -0
  250. {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc2.dist-info}/top_level.txt +0 -0
@@ -13,171 +13,484 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ import inspect
17
+ import logging
18
+ import types
16
19
  from functools import lru_cache
17
20
  from typing import Any
21
+ from typing import TypeVar
18
22
  from typing import get_args
19
23
  from typing import get_origin
20
24
 
25
+ from pydantic import BaseModel
26
+ from pydantic import ValidationError
27
+ from pydantic import create_model
28
+ from pydantic.fields import FieldInfo
29
+
30
+ from nat.utils.type_utils import DecomposedType
31
+
32
+ logger = logging.getLogger(__name__)
33
+
21
34
 
22
35
  class TypeIntrospectionMixin:
23
- """Mixin class providing type introspection capabilities for generic classes.
36
+ """Hybrid mixin class providing type introspection capabilities for generic classes.
24
37
 
25
- This mixin extracts type information from generic class definitions,
26
- allowing classes to determine their InputT and OutputT types at runtime.
38
+ This mixin combines the DecomposedType class utilities with MRO traversal
39
+ to properly handle complex inheritance chains like HeaderRedactionProcessor or ProcessingExporter.
27
40
  """
28
41
 
29
- def _find_generic_types(self) -> tuple[type[Any], type[Any]] | None:
42
+ def _extract_types_from_signature_method(self) -> tuple[type[Any], type[Any]] | None:
43
+ """Extract input/output types from the signature method.
44
+
45
+ This method looks for a signature method (either defined via _signature_method class
46
+ attribute or discovered generically) and extracts input/output types from
47
+ its method signature.
48
+
49
+ Returns:
50
+ tuple[type[Any], type[Any]] | None: (input_type, output_type) or None if not found.
30
51
  """
31
- Recursively search through the inheritance hierarchy to find generic type parameters.
52
+ # First, try to get the signature method name from the class
53
+ signature_method_name = getattr(self.__class__, '_signature_method', None)
54
+
55
+ # If not defined, try to discover it generically
56
+ if not signature_method_name:
57
+ signature_method_name = self._discover_signature_method()
58
+
59
+ if not signature_method_name:
60
+ return None
61
+
62
+ # Get the method and inspect its signature
63
+ try:
64
+ method = getattr(self, signature_method_name)
65
+ sig = inspect.signature(method)
66
+
67
+ # Find the first parameter that's not 'self'
68
+ params = list(sig.parameters.values())
69
+ input_param = None
70
+ for param in params:
71
+ if param.name != 'self':
72
+ input_param = param
73
+ break
74
+
75
+ if not input_param or input_param.annotation == inspect.Parameter.empty:
76
+ return None
77
+
78
+ # Get return type
79
+ return_annotation = sig.return_annotation
80
+ if return_annotation == inspect.Signature.empty:
81
+ return None
82
+
83
+ input_type = input_param.annotation
84
+ output_type = return_annotation
85
+
86
+ # Resolve any TypeVars if needed (including nested ones)
87
+ if isinstance(input_type, TypeVar) or isinstance(
88
+ output_type, TypeVar) or self._contains_typevar(input_type) or self._contains_typevar(output_type):
89
+ # Try to resolve using the MRO approach as fallback
90
+ typevar_mapping = self._build_typevar_mapping()
91
+ input_type = self._resolve_typevar_recursively(input_type, typevar_mapping)
92
+ output_type = self._resolve_typevar_recursively(output_type, typevar_mapping)
93
+
94
+ # Only return if we have concrete types
95
+ if not isinstance(input_type, TypeVar) and not isinstance(output_type, TypeVar):
96
+ return input_type, output_type
97
+
98
+ except (AttributeError, TypeError) as e:
99
+ logger.debug("Failed to extract types from signature method '%s': %s", signature_method_name, e)
100
+
101
+ return None
102
+
103
+ def _discover_signature_method(self) -> str | None:
104
+ """Discover any method suitable for type introspection.
32
105
 
33
- This method handles cases where a class inherits from a generic parent class,
34
- resolving the concrete types through the inheritance chain.
106
+ Looks for any method with the signature pattern: method(self, param: Type) -> ReturnType
107
+ Any method matching this pattern is functionally equivalent for type introspection purposes.
35
108
 
36
109
  Returns:
37
- tuple[type[Any], type[Any]] | None: (input_type, output_type) if found, None otherwise
110
+ str | None: Method name or None if not found
38
111
  """
39
- # First, try to find types directly in this class's __orig_bases__
40
- for base_cls in getattr(self.__class__, '__orig_bases__', []):
41
- base_cls_args = get_args(base_cls)
112
+ # Look through all methods to find ones that match the input/output pattern
113
+ candidates = []
42
114
 
43
- # Direct case: MyClass[InputT, OutputT]
44
- if len(base_cls_args) >= 2:
45
- return base_cls_args[0], base_cls_args[1]
115
+ for cls in self.__class__.__mro__:
116
+ for name, method in inspect.getmembers(cls, inspect.isfunction):
117
+ # Skip private methods except dunder methods
118
+ if name.startswith('_') and not name.startswith('__'):
119
+ continue
46
120
 
47
- # Indirect case: MyClass[SomeGeneric[ConcreteType]]
48
- # Need to resolve the generic parent's types
49
- if len(base_cls_args) == 1:
50
- base_origin = get_origin(base_cls)
51
- if base_origin and hasattr(base_origin, '__orig_bases__'):
52
- # Look at the parent's generic definition
53
- for parent_base in getattr(base_origin, '__orig_bases__', []):
54
- parent_args = get_args(parent_base)
55
- if len(parent_args) >= 2:
56
- # Found the pattern: ParentClass[T, list[T]]
57
- # Substitute T with our concrete type
58
- concrete_type = base_cls_args[0]
59
- input_type = self._substitute_type_var(parent_args[0], concrete_type)
60
- output_type = self._substitute_type_var(parent_args[1], concrete_type)
61
- return input_type, output_type
121
+ # Skip methods that were defined in TypeIntrospectionMixin
122
+ if hasattr(method, '__qualname__') and 'TypeIntrospectionMixin' in method.__qualname__:
123
+ logger.debug("Skipping method '%s' defined in TypeIntrospectionMixin", name)
124
+ continue
62
125
 
63
- return None
126
+ # Let signature analysis determine suitability - method names don't matter
127
+ try:
128
+ sig = inspect.signature(method)
129
+ params = list(sig.parameters.values())
130
+
131
+ # Look for methods with exactly one non-self parameter and a return annotation
132
+ non_self_params = [p for p in params if p.name != 'self']
133
+ if (len(non_self_params) == 1 and non_self_params[0].annotation != inspect.Parameter.empty
134
+ and sig.return_annotation != inspect.Signature.empty):
135
+
136
+ # Prioritize abstract methods
137
+ is_abstract = getattr(method, '__isabstractmethod__', False)
138
+ candidates.append((name, is_abstract, cls))
139
+
140
+ except (TypeError, ValueError) as e:
141
+ logger.debug("Failed to inspect signature of method '%s': %s", name, e)
142
+
143
+ if not candidates:
144
+ logger.debug("No candidates found for signature method")
145
+ return None
64
146
 
65
- def _substitute_type_var(self, type_expr: Any, concrete_type: type) -> type[Any]:
147
+ # Any method with the right signature will work for type introspection
148
+ # Prioritize abstract methods if available, otherwise use the first valid one
149
+ candidates.sort(key=lambda x: not x[1]) # Abstract methods first
150
+ return candidates[0][0]
151
+
152
+ def _resolve_typevar_recursively(self, type_arg: Any, typevar_mapping: dict[TypeVar, type[Any]]) -> Any:
153
+ """Recursively resolve TypeVars within complex types.
154
+
155
+ Args:
156
+ type_arg (Any): The type argument to resolve (could be a TypeVar, generic type, etc.)
157
+ typevar_mapping (dict[TypeVar, type[Any]]): Current mapping of TypeVars to concrete types
158
+
159
+ Returns:
160
+ Any: The resolved type with all TypeVars substituted
66
161
  """
67
- Substitute TypeVar in a type expression with a concrete type.
162
+ # If it's a TypeVar, resolve it
163
+ if isinstance(type_arg, TypeVar):
164
+ return typevar_mapping.get(type_arg, type_arg)
165
+
166
+ # If it's a generic type, decompose and resolve its arguments
167
+ try:
168
+ decomposed = DecomposedType(type_arg)
169
+ if decomposed.is_generic and decomposed.args:
170
+ # Recursively resolve all type arguments
171
+ resolved_args = []
172
+ for arg in decomposed.args:
173
+ resolved_arg = self._resolve_typevar_recursively(arg, typevar_mapping)
174
+ resolved_args.append(resolved_arg)
175
+
176
+ # Reconstruct the generic type with resolved arguments
177
+ if decomposed.origin:
178
+ return decomposed.origin[tuple(resolved_args)]
179
+
180
+ except (TypeError, AttributeError) as e:
181
+ # If we can't decompose or reconstruct, return as-is
182
+ logger.debug("Failed to decompose or reconstruct type '%s': %s", type_arg, e)
183
+
184
+ return type_arg
185
+
186
+ def _contains_typevar(self, type_arg: Any) -> bool:
187
+ """Check if a type contains any TypeVars (including nested ones).
68
188
 
69
189
  Args:
70
- type_expr: The type expression potentially containing TypeVars
71
- concrete_type: The concrete type to substitute
190
+ type_arg (Any): The type to check
72
191
 
73
192
  Returns:
74
- The type expression with TypeVars substituted
193
+ bool: True if the type contains any TypeVars
75
194
  """
76
- from typing import TypeVar
195
+ if isinstance(type_arg, TypeVar):
196
+ return True
77
197
 
78
- # If it's a TypeVar, substitute it
79
- if isinstance(type_expr, TypeVar):
80
- return concrete_type
198
+ try:
199
+ decomposed = DecomposedType(type_arg)
200
+ if decomposed.is_generic and decomposed.args:
201
+ return any(self._contains_typevar(arg) for arg in decomposed.args)
202
+ except (TypeError, AttributeError) as e:
203
+ logger.debug("Failed to decompose or reconstruct type '%s': %s", type_arg, e)
81
204
 
82
- # If it's a generic type like list[T], substitute the args
83
- origin = get_origin(type_expr)
84
- args = get_args(type_expr)
205
+ return False
85
206
 
86
- if origin and args:
87
- # Recursively substitute in the arguments
88
- new_args = tuple(self._substitute_type_var(arg, concrete_type) for arg in args)
89
- # Reconstruct the generic type
90
- return origin[new_args]
207
+ def _build_typevar_mapping(self) -> dict[TypeVar, type[Any]]:
208
+ """Build TypeVar to concrete type mapping from MRO traversal.
91
209
 
92
- # Otherwise, return as-is
93
- return type_expr
210
+ Returns:
211
+ dict[TypeVar, type[Any]]: Mapping of TypeVars to concrete types
212
+ """
213
+ typevar_mapping = {}
214
+
215
+ # First, check if the instance has concrete type arguments from __orig_class__
216
+ # This handles cases like BatchingProcessor[str]() where we need to map T -> str
217
+ orig_class = getattr(self, '__orig_class__', None)
218
+ if orig_class:
219
+ class_origin = get_origin(orig_class)
220
+ class_args = get_args(orig_class)
221
+ class_params = getattr(class_origin, '__parameters__', None)
222
+
223
+ if class_args and class_params:
224
+ # Map class-level TypeVars to their concrete arguments
225
+ for param, arg in zip(class_params, class_args):
226
+ typevar_mapping[param] = arg
227
+
228
+ # Then traverse the MRO to build the complete mapping
229
+ for cls in self.__class__.__mro__:
230
+ for base in getattr(cls, '__orig_bases__', []):
231
+ decomposed_base = DecomposedType(base)
232
+
233
+ if (decomposed_base.is_generic and decomposed_base.origin
234
+ and hasattr(decomposed_base.origin, '__parameters__')):
235
+ type_params = decomposed_base.origin.__parameters__
236
+ # Map each TypeVar to its concrete argument
237
+ for param, arg in zip(type_params, decomposed_base.args):
238
+ if param not in typevar_mapping: # Keep the most specific mapping
239
+ # If arg is also a TypeVar, try to resolve it
240
+ if isinstance(arg, TypeVar) and arg in typevar_mapping:
241
+ typevar_mapping[param] = typevar_mapping[arg]
242
+ else:
243
+ typevar_mapping[param] = arg
244
+
245
+ return typevar_mapping
246
+
247
+ def _extract_instance_types_from_mro(self) -> tuple[type[Any], type[Any]] | None:
248
+ """Extract Generic[InputT, OutputT] types by traversing the MRO.
249
+
250
+ This handles complex inheritance chains by looking for the base
251
+ class and resolving TypeVars through the inheritance hierarchy.
94
252
 
95
- @property
96
- @lru_cache
97
- def input_type(self) -> type[Any]:
253
+ Returns:
254
+ tuple[type[Any], type[Any]] | None: (input_type, output_type) or None if not found
98
255
  """
99
- Get the input type of the class. The input type is determined by the generic parameters of the class.
256
+ # Use the centralized TypeVar mapping
257
+ typevar_mapping = self._build_typevar_mapping()
258
+
259
+ # Now find the first generic base with exactly 2 parameters, starting from the base classes
260
+ # This ensures we get the fundamental input/output types rather than specialized ones
261
+ for cls in reversed(self.__class__.__mro__):
262
+ for base in getattr(cls, '__orig_bases__', []):
263
+ decomposed_base = DecomposedType(base)
264
+
265
+ # Look for any generic with exactly 2 parameters (likely InputT, OutputT pattern)
266
+ if decomposed_base.is_generic and len(decomposed_base.args) == 2:
267
+ input_type = decomposed_base.args[0]
268
+ output_type = decomposed_base.args[1]
269
+
270
+ # Resolve TypeVars to concrete types using recursive resolution
271
+ input_type = self._resolve_typevar_recursively(input_type, typevar_mapping)
272
+ output_type = self._resolve_typevar_recursively(output_type, typevar_mapping)
273
+
274
+ # Only return if we have concrete types (not TypeVars)
275
+ if not isinstance(input_type, TypeVar) and not isinstance(output_type, TypeVar):
276
+ return input_type, output_type
277
+
278
+ return None
100
279
 
101
- For example, if a class is defined as `MyClass[list[int], str]`, the `input_type` is `list[int]`.
280
+ @lru_cache
281
+ def _extract_input_output_types(self) -> tuple[type[Any], type[Any]]:
282
+ """Extract both input and output types using available approaches.
102
283
 
103
- Returns
104
- -------
105
- type[Any]
106
- The input type specified in the generic parameters
284
+ Returns:
285
+ tuple[type[Any], type[Any]]: (input_type, output_type)
107
286
 
108
- Raises
109
- ------
110
- ValueError
111
- If the input type cannot be determined from the class definition
287
+ Raises:
288
+ ValueError: If types cannot be extracted
112
289
  """
113
- types = self._find_generic_types()
114
- if types:
115
- return types[0]
290
+ # First try the signature-based approach
291
+ result = self._extract_types_from_signature_method()
292
+ if result:
293
+ return result
294
+
295
+ # Fallback to MRO-based approach for complex inheritance
296
+ result = self._extract_instance_types_from_mro()
297
+ if result:
298
+ return result
116
299
 
117
- raise ValueError(f"Could not find input type for {self.__class__.__name__}")
300
+ raise ValueError(f"Could not extract input/output types from {self.__class__.__name__}. "
301
+ f"Ensure class inherits from a generic like Processor[InputT, OutputT] "
302
+ f"or has a signature method with type annotations")
303
+
304
+ @property
305
+ def input_type(self) -> type[Any]:
306
+ """Get the input type of the instance.
307
+
308
+ Returns:
309
+ type[Any]: The input type
310
+ """
311
+ return self._extract_input_output_types()[0]
118
312
 
119
313
  @property
120
- @lru_cache
121
314
  def output_type(self) -> type[Any]:
315
+ """Get the output type of the instance.
316
+
317
+ Returns:
318
+ type[Any]: The output type
122
319
  """
123
- Get the output type of the class. The output type is determined by the generic parameters of the class.
320
+ return self._extract_input_output_types()[1]
124
321
 
125
- For example, if a class is defined as `MyClass[list[int], str]`, the `output_type` is `str`.
322
+ @lru_cache
323
+ def _get_union_info(self, type_obj: type[Any]) -> tuple[bool, tuple[type, ...] | None]:
324
+ """Get union information for a type.
126
325
 
127
- Returns
128
- -------
129
- type[Any]
130
- The output type specified in the generic parameters
326
+ Args:
327
+ type_obj (type[Any]): The type to analyze
131
328
 
132
- Raises
133
- ------
134
- ValueError
135
- If the output type cannot be determined from the class definition
329
+ Returns:
330
+ tuple[bool, tuple[type, ...] | None]: (is_union, union_types_or_none)
136
331
  """
137
- types = self._find_generic_types()
138
- if types:
139
- return types[1]
140
-
141
- raise ValueError(f"Could not find output type for {self.__class__.__name__}")
332
+ decomposed = DecomposedType(type_obj)
333
+ return decomposed.is_union, decomposed.args if decomposed.is_union else None
142
334
 
143
335
  @property
144
- @lru_cache
145
- def input_class(self) -> type:
336
+ def has_union_input(self) -> bool:
337
+ """Check if the input type is a union type.
338
+
339
+ Returns:
340
+ bool: True if the input type is a union type, False otherwise
146
341
  """
147
- Get the python class of the input type. This is the class that can be used to check if a value is an
148
- instance of the input type. It removes any generic or annotation information from the input type.
342
+ return self._get_union_info(self.input_type)[0]
149
343
 
150
- For example, if the input type is `list[int]`, the `input_class` is `list`.
344
+ @property
345
+ def has_union_output(self) -> bool:
346
+ """Check if the output type is a union type.
151
347
 
152
- Returns
153
- -------
154
- type
155
- The python type of the input type
348
+ Returns:
349
+ bool: True if the output type is a union type, False otherwise
156
350
  """
157
- input_origin = get_origin(self.input_type)
351
+ return self._get_union_info(self.output_type)[0]
158
352
 
159
- if input_origin is None:
160
- return self.input_type
353
+ @property
354
+ def input_union_types(self) -> tuple[type, ...] | None:
355
+ """Get the individual types in an input union.
161
356
 
162
- return input_origin
357
+ Returns:
358
+ tuple[type, ...] | None: The individual types in an input union or None if not found
359
+ """
360
+ return self._get_union_info(self.input_type)[1]
163
361
 
164
362
  @property
363
+ def output_union_types(self) -> tuple[type, ...] | None:
364
+ """Get the individual types in an output union.
365
+
366
+ Returns:
367
+ tuple[type, ...] | None: The individual types in an output union or None if not found
368
+ """
369
+ return self._get_union_info(self.output_type)[1]
370
+
371
+ def is_compatible_with_input(self, source_type: type) -> bool:
372
+ """Check if a source type is compatible with this instance's input type.
373
+
374
+ Uses Pydantic-based type compatibility checking for strict type matching.
375
+ This focuses on proper type relationships rather than batch compatibility.
376
+
377
+ Args:
378
+ source_type (type): The source type to check
379
+
380
+ Returns:
381
+ bool: True if the source type is compatible with the input type, False otherwise
382
+ """
383
+ return self._is_pydantic_type_compatible(source_type, self.input_type)
384
+
385
+ def is_output_compatible_with(self, target_type: type) -> bool:
386
+ """Check if this instance's output type is compatible with a target type.
387
+
388
+ Uses Pydantic-based type compatibility checking for strict type matching.
389
+ This focuses on proper type relationships rather than batch compatibility.
390
+
391
+ Args:
392
+ target_type (type): The target type to check
393
+
394
+ Returns:
395
+ bool: True if the output type is compatible with the target type, False otherwise
396
+ """
397
+ return self._is_pydantic_type_compatible(self.output_type, target_type)
398
+
399
+ def _is_pydantic_type_compatible(self, source_type: type, target_type: type) -> bool:
400
+ """Check strict type compatibility without batch compatibility hacks.
401
+
402
+ This focuses on proper type relationships: exact matches and subclass relationships.
403
+
404
+ Args:
405
+ source_type (type): The source type to check
406
+ target_type (type): The target type to check compatibility with
407
+
408
+ Returns:
409
+ bool: True if types are compatible, False otherwise
410
+ """
411
+ # Direct equality check (most common case)
412
+ if source_type == target_type:
413
+ return True
414
+
415
+ # Subclass relationship check
416
+ try:
417
+ if issubclass(source_type, target_type):
418
+ return True
419
+ except TypeError:
420
+ # Generic types can't use issubclass, they're only compatible if equal
421
+ logger.debug("Generic type %s cannot be used with issubclass, they're only compatible if equal",
422
+ source_type)
423
+
424
+ return False
425
+
165
426
  @lru_cache
166
- def output_class(self) -> type:
427
+ def _get_input_validator(self) -> type[BaseModel]:
428
+ """Create a Pydantic model for validating input types.
429
+
430
+ Returns:
431
+ type[BaseModel]: The Pydantic model for validating input types
167
432
  """
168
- Get the python class of the output type. This is the class that can be used to check if a value is an
169
- instance of the output type. It removes any generic or annotation information from the output type.
433
+ input_type = self.input_type
434
+ return create_model(f"{self.__class__.__name__}InputValidator", input=(input_type, FieldInfo()))
170
435
 
171
- For example, if the output type is `list[int]`, the `output_class` is `list`.
436
+ @lru_cache
437
+ def _get_output_validator(self) -> type[BaseModel]:
438
+ """Create a Pydantic model for validating output types.
172
439
 
173
- Returns
174
- -------
175
- type
176
- The python type of the output type
440
+ Returns:
441
+ type[BaseModel]: The Pydantic model for validating output types
177
442
  """
178
- output_origin = get_origin(self.output_type)
443
+ output_type = self.output_type
444
+ return create_model(f"{self.__class__.__name__}OutputValidator", output=(output_type, FieldInfo()))
179
445
 
180
- if output_origin is None:
181
- return self.output_type
446
+ def validate_input_type(self, item: Any) -> bool:
447
+ """Validate that an item matches the expected input type using Pydantic.
448
+
449
+ Args:
450
+ item (Any): The item to validate
451
+
452
+ Returns:
453
+ bool: True if the item matches the input type, False otherwise
454
+ """
455
+ try:
456
+ validator = self._get_input_validator()
457
+ validator(input=item)
458
+ return True
459
+ except ValidationError:
460
+ logger.warning("Item %s is not compatible with input type %s", item, self.input_type)
461
+ return False
182
462
 
183
- return output_origin
463
+ def validate_output_type(self, item: Any) -> bool:
464
+ """Validate that an item matches the expected output type using Pydantic.
465
+
466
+ Args:
467
+ item (Any): The item to validate
468
+
469
+ Returns:
470
+ bool: True if the item matches the output type, False otherwise
471
+ """
472
+ try:
473
+ validator = self._get_output_validator()
474
+ validator(output=item)
475
+ return True
476
+ except ValidationError:
477
+ logger.warning("Item %s is not compatible with output type %s", item, self.output_type)
478
+ return False
479
+
480
+ @lru_cache
481
+ def extract_non_optional_type(self, type_obj: type | types.UnionType) -> Any:
482
+ """Extract the non-None type from Optional[T] or Union[T, None] types.
483
+
484
+ This is useful when you need to pass a type to a system that doesn't
485
+ understand Optional types (like registries that expect concrete types).
486
+
487
+ Args:
488
+ type_obj (type | types.UnionType): The type to extract from (could be Optional[T] or Union[T, None])
489
+
490
+ Returns:
491
+ Any: The actual type without None, or the original type if not a union with None
492
+ """
493
+ decomposed = DecomposedType(type_obj) # type: ignore[arg-type]
494
+ if decomposed.is_optional:
495
+ return decomposed.get_optional_type().type
496
+ return type_obj
@@ -193,14 +193,14 @@ class BatchingProcessor(CallbackProcessor[T, list[T]], Generic[T]):
193
193
  await self._done_callback(batch)
194
194
  logger.debug("Scheduled flush routed batch of %d items through pipeline", len(batch))
195
195
  except Exception as e:
196
- logger.error("Error routing scheduled batch through pipeline: %s", e, exc_info=True)
196
+ logger.exception("Error routing scheduled batch through pipeline: %s", e)
197
197
  else:
198
198
  logger.warning("Scheduled flush created batch of %d items but no pipeline callback set",
199
199
  len(batch))
200
200
  except asyncio.CancelledError:
201
201
  pass
202
202
  except Exception as e:
203
- logger.error("Error in scheduled flush: %s", e, exc_info=True)
203
+ logger.exception("Error in scheduled flush: %s", e)
204
204
 
205
205
  async def _create_batch(self) -> list[T]:
206
206
  """Create a batch from the current queue."""
@@ -241,7 +241,7 @@ class BatchingProcessor(CallbackProcessor[T, list[T]], Generic[T]):
241
241
  try:
242
242
  await asyncio.wait_for(self._shutdown_complete_event.wait(), timeout=self._shutdown_timeout)
243
243
  logger.debug("Shutdown completion detected via event")
244
- except asyncio.TimeoutError:
244
+ except TimeoutError:
245
245
  logger.warning("Shutdown completion timeout exceeded (%s seconds)", self._shutdown_timeout)
246
246
  return
247
247
 
@@ -271,9 +271,7 @@ class BatchingProcessor(CallbackProcessor[T, list[T]], Generic[T]):
271
271
  "Successfully flushed final batch of %d items through pipeline during shutdown",
272
272
  len(final_batch))
273
273
  except Exception as e:
274
- logger.error("Error routing final batch through pipeline during shutdown: %s",
275
- e,
276
- exc_info=True)
274
+ logger.exception("Error routing final batch through pipeline during shutdown: %s", e)
277
275
  else:
278
276
  logger.warning("Final batch of %d items created during shutdown but no pipeline callback set",
279
277
  len(final_batch))
@@ -285,7 +283,7 @@ class BatchingProcessor(CallbackProcessor[T, list[T]], Generic[T]):
285
283
  logger.debug("BatchingProcessor shutdown completed successfully")
286
284
 
287
285
  except Exception as e:
288
- logger.error("Error during BatchingProcessor shutdown: %s", e, exc_info=True)
286
+ logger.exception("Error during BatchingProcessor shutdown: %s", e)
289
287
  self._shutdown_complete = True
290
288
  self._shutdown_complete_event.set()
291
289