lionagi 0.17.9__py3-none-any.whl → 0.17.11__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 (151) hide show
  1. lionagi/__init__.py +1 -2
  2. lionagi/_class_registry.py +1 -2
  3. lionagi/_errors.py +1 -2
  4. lionagi/adapters/async_postgres_adapter.py +2 -10
  5. lionagi/config.py +1 -2
  6. lionagi/fields/action.py +1 -2
  7. lionagi/fields/base.py +3 -0
  8. lionagi/fields/code.py +3 -0
  9. lionagi/fields/file.py +3 -0
  10. lionagi/fields/instruct.py +1 -2
  11. lionagi/fields/reason.py +3 -2
  12. lionagi/fields/research.py +3 -0
  13. lionagi/libs/__init__.py +1 -2
  14. lionagi/libs/file/__init__.py +1 -2
  15. lionagi/libs/file/chunk.py +1 -2
  16. lionagi/libs/file/process.py +1 -2
  17. lionagi/libs/schema/__init__.py +1 -2
  18. lionagi/libs/schema/as_readable.py +1 -2
  19. lionagi/libs/schema/extract_code_block.py +1 -2
  20. lionagi/libs/schema/extract_docstring.py +1 -2
  21. lionagi/libs/schema/function_to_schema.py +1 -2
  22. lionagi/libs/schema/load_pydantic_model_from_schema.py +1 -2
  23. lionagi/libs/validate/__init__.py +1 -2
  24. lionagi/libs/validate/common_field_validators.py +1 -2
  25. lionagi/libs/validate/validate_boolean.py +1 -2
  26. lionagi/ln/fuzzy/_string_similarity.py +1 -2
  27. lionagi/ln/types.py +45 -0
  28. lionagi/models/__init__.py +1 -2
  29. lionagi/models/field_model.py +392 -286
  30. lionagi/models/hashable_model.py +98 -14
  31. lionagi/models/model_params.py +272 -271
  32. lionagi/models/operable_model.py +9 -10
  33. lionagi/models/schema_model.py +1 -2
  34. lionagi/operations/ReAct/ReAct.py +1 -2
  35. lionagi/operations/ReAct/__init__.py +1 -2
  36. lionagi/operations/ReAct/utils.py +1 -2
  37. lionagi/operations/__init__.py +1 -2
  38. lionagi/operations/_act/__init__.py +1 -2
  39. lionagi/operations/_act/act.py +1 -2
  40. lionagi/operations/brainstorm/__init__.py +1 -2
  41. lionagi/operations/brainstorm/brainstorm.py +1 -2
  42. lionagi/operations/brainstorm/prompt.py +1 -2
  43. lionagi/operations/builder.py +1 -2
  44. lionagi/operations/chat/__init__.py +1 -2
  45. lionagi/operations/chat/chat.py +1 -2
  46. lionagi/operations/communicate/communicate.py +1 -2
  47. lionagi/operations/flow.py +1 -2
  48. lionagi/operations/instruct/__init__.py +1 -2
  49. lionagi/operations/instruct/instruct.py +1 -2
  50. lionagi/operations/interpret/__init__.py +1 -2
  51. lionagi/operations/interpret/interpret.py +1 -2
  52. lionagi/operations/operate/__init__.py +1 -2
  53. lionagi/operations/operate/operate.py +1 -2
  54. lionagi/operations/parse/__init__.py +1 -2
  55. lionagi/operations/parse/parse.py +1 -2
  56. lionagi/operations/plan/__init__.py +1 -2
  57. lionagi/operations/plan/plan.py +1 -2
  58. lionagi/operations/plan/prompt.py +1 -2
  59. lionagi/operations/select/__init__.py +1 -2
  60. lionagi/operations/select/select.py +1 -2
  61. lionagi/operations/select/utils.py +1 -2
  62. lionagi/operations/types.py +1 -2
  63. lionagi/operations/utils.py +1 -2
  64. lionagi/protocols/__init__.py +1 -2
  65. lionagi/protocols/_concepts.py +1 -2
  66. lionagi/protocols/action/__init__.py +1 -2
  67. lionagi/protocols/action/function_calling.py +3 -20
  68. lionagi/protocols/action/manager.py +34 -4
  69. lionagi/protocols/action/tool.py +1 -2
  70. lionagi/protocols/contracts.py +1 -2
  71. lionagi/protocols/forms/__init__.py +1 -2
  72. lionagi/protocols/forms/base.py +1 -2
  73. lionagi/protocols/forms/flow.py +1 -2
  74. lionagi/protocols/forms/form.py +1 -2
  75. lionagi/protocols/forms/report.py +1 -2
  76. lionagi/protocols/generic/__init__.py +1 -2
  77. lionagi/protocols/generic/element.py +17 -65
  78. lionagi/protocols/generic/event.py +1 -2
  79. lionagi/protocols/generic/log.py +14 -12
  80. lionagi/protocols/generic/pile.py +6 -4
  81. lionagi/protocols/generic/processor.py +1 -2
  82. lionagi/protocols/generic/progression.py +1 -2
  83. lionagi/protocols/graph/__init__.py +1 -2
  84. lionagi/protocols/graph/edge.py +1 -2
  85. lionagi/protocols/graph/graph.py +1 -2
  86. lionagi/protocols/graph/node.py +1 -2
  87. lionagi/protocols/ids.py +1 -2
  88. lionagi/protocols/mail/__init__.py +1 -2
  89. lionagi/protocols/mail/exchange.py +1 -2
  90. lionagi/protocols/mail/mail.py +1 -2
  91. lionagi/protocols/mail/mailbox.py +1 -2
  92. lionagi/protocols/mail/manager.py +1 -2
  93. lionagi/protocols/mail/package.py +1 -2
  94. lionagi/protocols/messages/__init__.py +1 -2
  95. lionagi/protocols/messages/action_request.py +1 -2
  96. lionagi/protocols/messages/action_response.py +1 -2
  97. lionagi/protocols/messages/assistant_response.py +1 -2
  98. lionagi/protocols/messages/base.py +1 -2
  99. lionagi/protocols/messages/instruction.py +1 -2
  100. lionagi/protocols/messages/manager.py +1 -2
  101. lionagi/protocols/messages/message.py +1 -2
  102. lionagi/protocols/messages/system.py +1 -2
  103. lionagi/protocols/operatives/__init__.py +1 -2
  104. lionagi/protocols/operatives/operative.py +30 -8
  105. lionagi/protocols/operatives/step.py +1 -2
  106. lionagi/protocols/types.py +1 -2
  107. lionagi/service/connections/__init__.py +1 -2
  108. lionagi/service/connections/api_calling.py +1 -2
  109. lionagi/service/connections/endpoint.py +1 -2
  110. lionagi/service/connections/endpoint_config.py +1 -2
  111. lionagi/service/connections/header_factory.py +1 -2
  112. lionagi/service/connections/match_endpoint.py +1 -2
  113. lionagi/service/connections/mcp/__init__.py +1 -2
  114. lionagi/service/connections/mcp/wrapper.py +1 -2
  115. lionagi/service/connections/providers/__init__.py +1 -2
  116. lionagi/service/connections/providers/anthropic_.py +1 -2
  117. lionagi/service/connections/providers/claude_code_cli.py +1 -2
  118. lionagi/service/connections/providers/exa_.py +1 -2
  119. lionagi/service/connections/providers/nvidia_nim_.py +2 -27
  120. lionagi/service/connections/providers/oai_.py +1 -2
  121. lionagi/service/connections/providers/ollama_.py +1 -2
  122. lionagi/service/connections/providers/perplexity_.py +1 -2
  123. lionagi/service/hooks/__init__.py +1 -1
  124. lionagi/service/hooks/_types.py +1 -1
  125. lionagi/service/hooks/_utils.py +1 -1
  126. lionagi/service/hooks/hook_event.py +1 -1
  127. lionagi/service/hooks/hook_registry.py +1 -1
  128. lionagi/service/hooks/hooked_event.py +1 -2
  129. lionagi/service/imodel.py +1 -2
  130. lionagi/service/manager.py +1 -2
  131. lionagi/service/rate_limited_processor.py +1 -2
  132. lionagi/service/resilience.py +1 -2
  133. lionagi/service/third_party/anthropic_models.py +3 -5
  134. lionagi/service/third_party/claude_code.py +1 -2
  135. lionagi/service/token_calculator.py +1 -2
  136. lionagi/service/types.py +1 -2
  137. lionagi/session/__init__.py +1 -2
  138. lionagi/session/branch.py +1 -2
  139. lionagi/session/session.py +1 -2
  140. lionagi/tools/__init__.py +1 -2
  141. lionagi/tools/base.py +1 -2
  142. lionagi/tools/file/__init__.py +1 -2
  143. lionagi/tools/file/reader.py +1 -2
  144. lionagi/tools/types.py +1 -2
  145. lionagi/utils.py +1 -2
  146. lionagi/version.py +1 -1
  147. {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/METADATA +2 -2
  148. lionagi-0.17.11.dist-info/RECORD +199 -0
  149. lionagi-0.17.9.dist-info/RECORD +0 -199
  150. {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/WHEEL +0 -0
  151. {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/licenses/LICENSE +0 -0
@@ -1,335 +1,336 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
1
+ # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
3
2
  # SPDX-License-Identifier: Apache-2.0
4
3
 
4
+ """ModelParams implementation using Params base class with aggressive caching.
5
+
6
+ This module provides ModelParams, a configuration class for dynamically creating
7
+ Pydantic models with explicit behavior and aggressive caching for performance.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
5
12
  import inspect
13
+ import os
14
+ import threading
15
+ from collections import OrderedDict
6
16
  from collections.abc import Callable
17
+ from dataclasses import dataclass
18
+ from dataclasses import field as dc_field
19
+ from typing import Any, ClassVar
7
20
 
8
- from pydantic import (
9
- BaseModel,
10
- Field,
11
- PrivateAttr,
12
- create_model,
13
- field_validator,
14
- model_validator,
15
- )
21
+ from pydantic import BaseModel, create_model
16
22
  from pydantic.fields import FieldInfo
17
- from typing_extensions import Self
18
-
19
- from lionagi.libs.validate.common_field_validators import (
20
- validate_boolean_field,
21
- validate_list_dict_str_keys,
22
- validate_model_to_type,
23
- validate_nullable_string_field,
24
- validate_same_dtype_flat_list,
25
- validate_str_str_dict,
26
- )
23
+
24
+ from lionagi.ln.types import Params
27
25
  from lionagi.utils import copy
28
26
 
29
27
  from .field_model import FieldModel
30
- from .schema_model import SchemaModel
31
28
 
32
29
  __all__ = ("ModelParams",)
33
30
 
34
-
35
- class ModelParams(SchemaModel):
36
- """Configuration class for dynamically creating new Pydantic models.
37
-
38
- This class provides a flexible way to create Pydantic models with customizable
39
- fields, validators, and configurations. It supports inheritance from base
40
- models, field exclusion, and custom validation rules.
41
-
42
- Args:
43
- name: Name for the generated model class.
44
- parameter_fields: Field definitions for the model.
45
- base_type: Base model class to inherit from.
46
- field_models: List of field model definitions.
47
- exclude_fields: Fields to exclude from the final model.
48
- field_descriptions: Custom descriptions for fields.
49
- inherit_base: Whether to inherit from base_type.
50
- config_dict: Pydantic model configuration.
51
- doc: Docstring for the generated model.
52
- frozen: Whether the model should be immutable.
31
+ # Global cache configuration
32
+ _MODEL_CACHE_SIZE = int(os.environ.get("LIONAGI_MODEL_CACHE_SIZE", "1000"))
33
+ _model_cache: OrderedDict[int, type[BaseModel]] = OrderedDict()
34
+ _model_cache_lock = threading.RLock()
35
+
36
+
37
+ @dataclass(slots=True, frozen=True, init=False)
38
+ class ModelParams(Params):
39
+ """Configuration for dynamically creating Pydantic models.
40
+
41
+ This class provides a way to configure and create Pydantic models dynamically
42
+ with explicit behavior (no silent conversions) and aggressive caching for
43
+ performance optimization.
44
+
45
+ Key features:
46
+ - All unspecified fields are explicitly Unset (not None or empty)
47
+ - No silent type conversions - fails fast on incorrect types
48
+ - Aggressive caching of created models with LRU eviction
49
+ - Thread-safe model creation and caching
50
+ - Not directly instantiable - requires keyword arguments
51
+
52
+ Attributes:
53
+ name: Name for the generated model class
54
+ parameter_fields: Field definitions for the model
55
+ base_type: Base model class to inherit from
56
+ field_models: List of FieldModel definitions
57
+ exclude_fields: Fields to exclude from the final model
58
+ field_descriptions: Custom descriptions for fields
59
+ inherit_base: Whether to inherit from base_type
60
+ config_dict: Pydantic model configuration
61
+ doc: Docstring for the generated model
62
+ frozen: Whether the model should be immutable
63
+
64
+ Environment Variables:
65
+ LIONAGI_MODEL_CACHE_SIZE: Maximum number of cached models (default: 1000)
53
66
 
54
67
  Examples:
55
68
  >>> params = ModelParams(
56
69
  ... name="UserModel",
70
+ ... frozen=True,
57
71
  ... field_models=[
58
- ... FieldModel(name="username", annotation=str),
59
- ... FieldModel(name="age", annotation=int, default=0)
72
+ ... FieldModel(str, name="username"),
73
+ ... FieldModel(int, name="age", default=0)
60
74
  ... ],
61
75
  ... doc="A user model with basic attributes."
62
76
  ... )
63
77
  >>> UserModel = params.create_new_model()
64
- """
65
-
66
- name: str | None = Field(
67
- default=None, description="Name for the generated model class"
68
- )
69
-
70
- parameter_fields: dict[str, FieldInfo] = Field(
71
- default_factory=dict, description="Field definitions for the model"
72
- )
73
-
74
- base_type: type[BaseModel] = Field(
75
- default=BaseModel, description="Base model class to inherit from"
76
- )
77
-
78
- field_models: list[FieldModel] = Field(
79
- default_factory=list, description="List of field model definitions"
80
- )
81
-
82
- exclude_fields: list = Field(
83
- default_factory=list,
84
- description="Fields to exclude from the final model",
85
- )
86
-
87
- field_descriptions: dict = Field(
88
- default_factory=dict, description="Custom descriptions for fields"
89
- )
90
-
91
- inherit_base: bool = Field(
92
- default=True, description="Whether to inherit from base_type"
93
- )
94
78
 
95
- config_dict: dict | None = Field(
96
- default=None, description="Pydantic model configuration"
97
- )
79
+ >>> # All unspecified fields are Unset
80
+ >>> params2 = ModelParams(name="SimpleModel")
81
+ >>> assert params2.doc is Unset
82
+ >>> assert params2.frozen is Unset
83
+ """
98
84
 
99
- doc: str | None = Field(
100
- default=None, description="Docstring for the generated model"
85
+ # Class configuration - let Params handle Unset population
86
+ _prefill_unset: ClassVar[bool] = True
87
+ _none_as_sentinel: ClassVar[bool] = True
88
+
89
+ # Public fields (all start as Unset when not provided)
90
+ name: str | None
91
+ parameter_fields: dict[str, FieldInfo]
92
+ base_type: type[BaseModel]
93
+ field_models: list[FieldModel]
94
+ exclude_fields: list[str]
95
+ field_descriptions: dict[str, str]
96
+ inherit_base: bool
97
+ config_dict: dict[str, Any] | None
98
+ doc: str | None
99
+ frozen: bool
100
+
101
+ # Private computed state
102
+ _final_fields: dict[str, FieldInfo] = dc_field(
103
+ default_factory=dict, init=False
101
104
  )
102
-
103
- frozen: bool = Field(
104
- default=False, description="Whether the model should be immutable"
105
+ _validators: dict[str, Callable] = dc_field(
106
+ default_factory=dict, init=False
105
107
  )
106
- _validators: dict[str, Callable] | None = PrivateAttr(default=None)
107
- _use_keys: set[str] = PrivateAttr(default_factory=set)
108
108
 
109
- @property
110
- def use_fields(self) -> dict[str, tuple[type, FieldInfo]]:
111
- """Get field definitions to use in new model.
109
+ def _validate(self) -> None:
110
+ """Validate types and setup model configuration.
112
111
 
113
- Filters and combines fields from parameter_fields and field_models based on
114
- the _use_keys set, preparing them for use in model creation.
115
-
116
- Returns:
117
- A dictionary mapping field names to tuples of (type, FieldInfo),
118
- containing only the fields that should be included in the new model.
119
- """
120
- params = {
121
- k: v
122
- for k, v in self.parameter_fields.items()
123
- if k in self._use_keys
124
- }
125
- # Add field_models with proper type annotations
126
- for f in self.field_models:
127
- if f.name in self._use_keys:
128
- params[f.name] = f.field_info
129
- # Set the annotation from the FieldModel's base_type
130
- params[f.name].annotation = f.base_type
131
-
132
- return {k: (v.annotation, v) for k, v in params.items()}
133
-
134
- @field_validator("parameter_fields", mode="before")
135
- def _validate_parameters(cls, value) -> dict[str, FieldInfo]:
136
- """Validate parameter field definitions.
137
-
138
- Args:
139
- value: Value to validate.
140
-
141
- Returns:
142
- dict[str, FieldInfo]: Validated parameter fields.
143
-
144
- Raises:
145
- ValueError: If parameter fields are invalid.
146
- """
147
- if value in [None, {}, []]:
148
- return {}
149
- if not isinstance(value, dict):
150
- raise ValueError("Fields must be a dictionary.")
151
- for k, v in value.items():
152
- if not isinstance(k, str):
153
- raise ValueError("Field names must be strings.")
154
- if not isinstance(v, FieldInfo):
155
- raise ValueError("Field values must be FieldInfo objects.")
156
- return copy(value)
157
-
158
- @field_validator("base_type", mode="before")
159
- def _validate_base(cls, value) -> type[BaseModel]:
160
- """Validate base model type.
161
-
162
- Args:
163
- value: Value to validate.
164
-
165
- Returns:
166
- type[BaseModel]: Validated base model type.
112
+ This method performs minimal domain-specific validation, then processes
113
+ and merges field definitions from various sources.
167
114
 
168
115
  Raises:
169
- ValueError: If base type is invalid.
116
+ ValueError: If base_type is not a BaseModel subclass
170
117
  """
171
- return validate_model_to_type(cls, value)
172
-
173
- @field_validator("exclude_fields", mode="before")
174
- def _validate_fields(cls, value) -> list[str]:
175
- """Validate excluded fields list.
118
+ # Let parent handle basic Unset population
119
+ Params._validate(self)
120
+
121
+ # Minimal domain validation - only check what matters
122
+ if not self._is_sentinel(self.base_type):
123
+ if not (
124
+ inspect.isclass(self.base_type)
125
+ and issubclass(self.base_type, BaseModel)
126
+ ):
127
+ raise ValueError(
128
+ f"base_type must be BaseModel subclass, got {self.base_type}"
129
+ )
176
130
 
177
- Args:
178
- value: Value to validate.
131
+ # Process and merge all field sources
132
+ self._process_fields()
179
133
 
180
- Returns:
181
- list[str]: Validated list of field names to exclude.
134
+ def _process_fields(self) -> None:
135
+ """Merge all field sources into final configuration.
182
136
 
183
- Raises:
184
- ValueError: If field names are invalid.
137
+ This method processes and combines fields from parameter_fields, base_type,
138
+ and field_models, handling exclusions and descriptions.
185
139
  """
186
- return validate_list_dict_str_keys(cls, value)
187
-
188
- @field_validator("field_descriptions", mode="before")
189
- def _validate_field_descriptions(cls, value) -> dict[str, str]:
190
- """Validate field descriptions dictionary.
191
-
192
- Args:
193
- value: Value to validate.
140
+ fields = {}
141
+ validators = {}
194
142
 
195
- Returns:
196
- dict[str, str]: Validated field descriptions.
143
+ # Start with explicit parameter_fields
144
+ if not self._is_sentinel(self.parameter_fields):
145
+ # Handle empty values - treat them as no fields
146
+ if not self.parameter_fields:
147
+ pass # Empty dict/list/None - no fields to add
148
+ elif isinstance(self.parameter_fields, dict):
149
+ # Validate parameter_fields contain FieldInfo instances
150
+ for name, field_info in self.parameter_fields.items():
151
+ if not isinstance(field_info, FieldInfo):
152
+ raise ValueError(
153
+ f"parameter_fields must contain FieldInfo instances, got {type(field_info)} for field '{name}'"
154
+ )
155
+ fields.update(copy(self.parameter_fields))
156
+ else:
157
+ raise ValueError(
158
+ f"parameter_fields must be a dictionary, got {type(self.parameter_fields)}"
159
+ )
160
+
161
+ # Add base_type fields (respecting exclusions)
162
+ if not self._is_sentinel(self.base_type):
163
+ base_fields = copy(self.base_type.model_fields)
164
+ if not self._is_sentinel(self.exclude_fields):
165
+ base_fields = {
166
+ k: v
167
+ for k, v in base_fields.items()
168
+ if k not in self.exclude_fields
169
+ }
170
+ fields.update(base_fields)
171
+
172
+ # Process field_models
173
+ if not self._is_sentinel(self.field_models):
174
+ # Coerce to list if single FieldModel instance
175
+ field_models_list = (
176
+ [self.field_models]
177
+ if isinstance(self.field_models, FieldModel)
178
+ else self.field_models
179
+ )
180
+
181
+ for fm in field_models_list:
182
+ if not isinstance(fm, FieldModel):
183
+ raise ValueError(
184
+ f"field_models must contain FieldModel instances, got {type(fm)}"
185
+ )
197
186
 
198
- Raises:
199
- ValueError: If descriptions are invalid.
200
- """
201
- return validate_str_str_dict(cls, value)
187
+ # Apply descriptions first
188
+ field_models = field_models_list
189
+ if not self._is_sentinel(self.field_descriptions):
190
+ field_models = [
191
+ (
192
+ fm.with_description(self.field_descriptions[fm.name])
193
+ if fm.name in self.field_descriptions
194
+ else fm
195
+ )
196
+ for fm in field_models
197
+ ]
202
198
 
203
- @field_validator("inherit_base", mode="before")
204
- def _validate_inherit_base(cls, value) -> bool:
205
- """Validate inherit_base flag.
199
+ # Extract fields and validators using public interface
200
+ for fm in field_models:
201
+ fields[fm.name] = fm.create_field()
202
+ fields[fm.name].annotation = fm.annotation
206
203
 
207
- Args:
208
- value: Value to validate.
204
+ # Use the public field_validator property
205
+ if fm.field_validator:
206
+ validators.update(fm.field_validator)
209
207
 
210
- Returns:
211
- bool: Validated inherit_base value.
212
- """
213
- return validate_boolean_field(cls, value, default=True)
208
+ # Store computed state
209
+ object.__setattr__(self, "_final_fields", fields)
210
+ object.__setattr__(self, "_validators", validators)
214
211
 
215
- @field_validator("name", mode="before")
216
- def _validate_name(cls, value) -> str | None:
217
- """Validate model name.
212
+ @property
213
+ def use_fields(self) -> dict[str, tuple[type, FieldInfo]]:
214
+ """Get field definitions to use in new model.
218
215
 
219
- Args:
220
- value: Value to validate.
216
+ Filters and prepares fields based on processed configuration.
221
217
 
222
218
  Returns:
223
- str | None: Validated model name.
224
-
225
- Raises:
226
- ValueError: If name is invalid.
219
+ Dictionary mapping field names to (type, FieldInfo) tuples
227
220
  """
228
- return validate_nullable_string_field(cls, value, field_name="Name")
229
-
230
- @field_validator("field_models", mode="before")
231
- def _validate_field_models(cls, value) -> list[FieldModel]:
232
- """Validate field model definitions.
221
+ if not hasattr(self, "_final_fields"):
222
+ return {}
233
223
 
234
- Args:
235
- value: Value to validate.
224
+ return {k: (v.annotation, v) for k, v in self._final_fields.items()}
236
225
 
237
- Returns:
238
- list[FieldModel]: Validated field models.
226
+ @property
227
+ def _use_keys(self) -> set[str]:
228
+ """Get field keys for backward compatibility.
239
229
 
240
- Raises:
241
- ValueError: If field models are invalid.
230
+ Returns the set of field names that will be used in the generated model.
231
+ This is derived from _final_fields for consistency.
242
232
  """
233
+ if not hasattr(self, "_final_fields"):
234
+ return set()
235
+ return set(self._final_fields.keys())
243
236
 
244
- return validate_same_dtype_flat_list(cls, value, FieldModel)
237
+ def _get_cache_key(self) -> int:
238
+ """Create a hashable cache key from object state.
245
239
 
246
- @model_validator(mode="after")
247
- def validate_param_model(self) -> Self:
248
- """Validate complete model configuration.
249
-
250
- Performs comprehensive validation and setup of the model parameters:
251
- 1. Updates parameter fields from base type if present
252
- 2. Merges field models into parameter fields
253
- 3. Manages field inclusion/exclusion via _use_keys
254
- 4. Sets up validators from field models
255
- 5. Applies field descriptions
256
- 6. Handles model name resolution
257
-
258
- Returns:
259
- The validated model instance with all configurations applied.
240
+ Converts unhashable types to hashable representations for caching.
260
241
  """
261
- if self.base_type is not None:
262
- self.parameter_fields.update(copy(self.base_type.model_fields))
263
-
264
- self.parameter_fields.update(
265
- {f.name: f.field_info for f in self.field_models}
266
- )
267
-
268
- use_keys = list(self.parameter_fields.keys())
269
- use_keys.extend(list(self._use_keys))
270
-
271
- if self.exclude_fields:
272
- use_keys = [i for i in use_keys if i not in self.exclude_fields]
273
-
274
- self._use_keys = set(use_keys)
275
-
276
- validators = {}
277
-
278
- for i in self.field_models:
279
- if i.field_validator is not None:
280
- validators.update(i.field_validator)
281
- self._validators = validators
282
-
283
- if self.field_descriptions:
284
- # Update field_models with descriptions (create new instances since they're immutable)
285
- updated_field_models = []
286
- for i in self.field_models:
287
- if i.name in self.field_descriptions:
288
- # Create new FieldModel with updated description
289
- updated_field_model = i.with_description(
290
- self.field_descriptions[i.name]
291
- )
292
- updated_field_models.append(updated_field_model)
293
- else:
294
- updated_field_models.append(i)
295
- self.field_models = updated_field_models
296
-
297
- if not isinstance(self.name, str):
298
- if hasattr(self.base_type, "class_name"):
299
- if callable(self.base_type.class_name):
300
- self.name = self.base_type.class_name()
301
- else:
302
- self.name = self.base_type.class_name
303
- elif inspect.isclass(self.base_type):
304
- self.name = self.base_type.__name__
305
-
306
- return self
242
+ state = self.to_dict()
243
+
244
+ def make_hashable(obj):
245
+ if isinstance(obj, dict):
246
+ return tuple(
247
+ sorted((k, make_hashable(v)) for k, v in obj.items())
248
+ )
249
+ elif isinstance(obj, list):
250
+ return tuple(make_hashable(x) for x in obj)
251
+ elif isinstance(obj, set):
252
+ return tuple(sorted(make_hashable(x) for x in obj))
253
+ else:
254
+ return obj
255
+
256
+ hashable_state = make_hashable(state)
257
+ return hash(hashable_state)
307
258
 
308
259
  def create_new_model(self) -> type[BaseModel]:
309
260
  """Create new Pydantic model with specified configuration.
310
261
 
311
262
  This method generates a new Pydantic model class based on the configured
312
- parameters, including fields, validators, and inheritance settings.
263
+ parameters. Results are cached for performance when the same configuration
264
+ is used multiple times.
313
265
 
314
266
  Returns:
315
- type[BaseModel]: Newly created Pydantic model class.
267
+ Newly created or cached Pydantic model class
316
268
  """
317
- base_type = self.base_type if self.inherit_base else None
318
-
319
- if base_type and self.exclude_fields:
320
- if any(
321
- i in self.exclude_fields for i in self.base_type.model_fields
269
+ # Create stable cache key from hashable representation
270
+ cache_key = self._get_cache_key()
271
+
272
+ # Check cache first
273
+ with _model_cache_lock:
274
+ if cache_key in _model_cache:
275
+ _model_cache.move_to_end(cache_key)
276
+ return _model_cache[cache_key]
277
+
278
+ # Determine model name
279
+ model_name = self.name
280
+ if self._is_sentinel(model_name) and not self._is_sentinel(
281
+ self.base_type
282
+ ):
283
+ if hasattr(self.base_type, "class_name"):
284
+ model_name = self.base_type.class_name
285
+ if callable(model_name):
286
+ model_name = model_name()
287
+ else:
288
+ model_name = self.base_type.__name__
289
+
290
+ if self._is_sentinel(model_name):
291
+ model_name = "GeneratedModel"
292
+
293
+ # Determine base class
294
+ base_type = None
295
+ if (
296
+ not self._is_sentinel(self.inherit_base)
297
+ and self.inherit_base
298
+ and not self._is_sentinel(self.base_type)
299
+ ):
300
+ # Don't inherit if we're excluding base fields
301
+ if self._is_sentinel(self.exclude_fields) or not any(
302
+ f in self.exclude_fields for f in self.base_type.model_fields
322
303
  ):
323
- base_type = None
304
+ base_type = self.base_type
324
305
 
325
- a: type[BaseModel] = create_model(
326
- self.name or "StepModel",
327
- __config__=self.config_dict,
328
- __doc__=self.doc,
306
+ # Create the model
307
+ model = create_model(
308
+ model_name,
329
309
  __base__=base_type,
330
- __validators__=self._validators,
310
+ __config__=(
311
+ self.config_dict
312
+ if not self._is_sentinel(self.config_dict)
313
+ else None
314
+ ),
315
+ __doc__=self.doc if not self._is_sentinel(self.doc) else None,
316
+ __validators__=self._validators if self._validators else None,
331
317
  **self.use_fields,
332
318
  )
333
- if self.frozen:
334
- a.model_config["frozen"] = True
335
- return a
319
+
320
+ # Apply frozen configuration
321
+ if not self._is_sentinel(self.frozen) and self.frozen:
322
+ model.model_config["frozen"] = True
323
+
324
+ # Cache the result
325
+ with _model_cache_lock:
326
+ _model_cache[cache_key] = model
327
+
328
+ # LRU eviction
329
+ while len(_model_cache) > _MODEL_CACHE_SIZE:
330
+ try:
331
+ _model_cache.popitem(last=False) # Remove oldest
332
+ except KeyError:
333
+ # Handle race condition
334
+ break
335
+
336
+ return model
@@ -1,5 +1,4 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
1
+ # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
3
2
  # SPDX-License-Identifier: Apache-2.0
4
3
 
5
4
  from typing import Any, TypeVar
@@ -115,13 +114,13 @@ class OperableModel(HashableModel):
115
114
  if isinstance(value, dict):
116
115
  for k, v in value.items():
117
116
  if isinstance(v, FieldModel):
118
- out[k] = v.field_info
117
+ out[k] = v.create_field()
119
118
  elif isinstance(v, FieldInfo):
120
119
  out[k] = v
121
120
  return out
122
121
 
123
122
  elif isinstance(value, list) and is_same_dtype(value, FieldModel):
124
- return {v.name: v.field_info for v in value}
123
+ return {v.name: v.create_field() for v in value}
125
124
 
126
125
  raise ValueError("Invalid extra_fields value")
127
126
 
@@ -139,7 +138,7 @@ class OperableModel(HashableModel):
139
138
  if isinstance(self.extra_fields, dict):
140
139
  for k, v in self.extra_fields.items():
141
140
  if isinstance(v, FieldModel):
142
- extra_fields[k] = v.field_info
141
+ extra_fields[k] = v.create_field()
143
142
  extra_field_models[k] = v
144
143
  elif isinstance(v, FieldInfo):
145
144
  extra_fields[k] = v
@@ -149,7 +148,7 @@ class OperableModel(HashableModel):
149
148
  for v in self.extra_fields:
150
149
  # list[FieldModel]
151
150
  if isinstance(v, FieldModel):
152
- extra_fields[v.name] = v.field_info
151
+ extra_fields[v.name] = v.create_field()
153
152
  extra_field_models[v.name] = v
154
153
 
155
154
  # Handle list[tuple[str, FieldInfo | FieldModel]]
@@ -158,11 +157,11 @@ class OperableModel(HashableModel):
158
157
  if isinstance(v[1], FieldInfo):
159
158
  extra_fields[v[0]] = v[1]
160
159
  if isinstance(v[1], FieldModel):
161
- extra_fields[v[1].name] = v[1].field_info
160
+ extra_fields[v[1].name] = v[1].create_field()
162
161
  extra_field_models[v[1].name] = v[1]
163
162
 
164
- self.extra_fields = extra_fields
165
- self.extra_field_models = extra_field_models
163
+ object.__setattr__(self, "extra_fields", extra_fields)
164
+ object.__setattr__(self, "extra_field_models", extra_field_models)
166
165
  return self
167
166
 
168
167
  @override
@@ -349,7 +348,7 @@ class OperableModel(HashableModel):
349
348
  raise ValueError(
350
349
  "Invalid field_model, should be a FieldModel object"
351
350
  )
352
- self.extra_fields[field_name] = field_model.field_info
351
+ self.extra_fields[field_name] = field_model.create_field()
353
352
  self.extra_field_models[field_name] = field_model
354
353
 
355
354
  # Handle kwargs