lionagi 0.17.8__py3-none-any.whl → 0.17.10__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.
@@ -2,334 +2,336 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ """ModelParams implementation using Params base class with aggressive caching.
6
+
7
+ This module provides ModelParams, a configuration class for dynamically creating
8
+ Pydantic models with explicit behavior and aggressive caching for performance.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
5
13
  import inspect
14
+ import os
15
+ import threading
16
+ from collections import OrderedDict
6
17
  from collections.abc import Callable
18
+ from dataclasses import dataclass
19
+ from dataclasses import field as dc_field
20
+ from typing import Any, ClassVar
7
21
 
8
- from pydantic import (
9
- BaseModel,
10
- Field,
11
- PrivateAttr,
12
- create_model,
13
- field_validator,
14
- model_validator,
15
- )
22
+ from pydantic import BaseModel, create_model
16
23
  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
- )
24
+
25
+ from lionagi.ln.types import Params
27
26
  from lionagi.utils import copy
28
27
 
29
28
  from .field_model import FieldModel
30
- from .schema_model import SchemaModel
31
29
 
32
30
  __all__ = ("ModelParams",)
33
31
 
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.
32
+ # Global cache configuration
33
+ _MODEL_CACHE_SIZE = int(os.environ.get("LIONAGI_MODEL_CACHE_SIZE", "1000"))
34
+ _model_cache: OrderedDict[int, type[BaseModel]] = OrderedDict()
35
+ _model_cache_lock = threading.RLock()
36
+
37
+
38
+ @dataclass(slots=True, frozen=True, init=False)
39
+ class ModelParams(Params):
40
+ """Configuration for dynamically creating Pydantic models.
41
+
42
+ This class provides a way to configure and create Pydantic models dynamically
43
+ with explicit behavior (no silent conversions) and aggressive caching for
44
+ performance optimization.
45
+
46
+ Key features:
47
+ - All unspecified fields are explicitly Unset (not None or empty)
48
+ - No silent type conversions - fails fast on incorrect types
49
+ - Aggressive caching of created models with LRU eviction
50
+ - Thread-safe model creation and caching
51
+ - Not directly instantiable - requires keyword arguments
52
+
53
+ Attributes:
54
+ name: Name for the generated model class
55
+ parameter_fields: Field definitions for the model
56
+ base_type: Base model class to inherit from
57
+ field_models: List of FieldModel definitions
58
+ exclude_fields: Fields to exclude from the final model
59
+ field_descriptions: Custom descriptions for fields
60
+ inherit_base: Whether to inherit from base_type
61
+ config_dict: Pydantic model configuration
62
+ doc: Docstring for the generated model
63
+ frozen: Whether the model should be immutable
64
+
65
+ Environment Variables:
66
+ LIONAGI_MODEL_CACHE_SIZE: Maximum number of cached models (default: 1000)
53
67
 
54
68
  Examples:
55
69
  >>> params = ModelParams(
56
70
  ... name="UserModel",
71
+ ... frozen=True,
57
72
  ... field_models=[
58
- ... FieldModel(name="username", annotation=str),
59
- ... FieldModel(name="age", annotation=int, default=0)
73
+ ... FieldModel(str, name="username"),
74
+ ... FieldModel(int, name="age", default=0)
60
75
  ... ],
61
76
  ... doc="A user model with basic attributes."
62
77
  ... )
63
78
  >>> 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
79
 
95
- config_dict: dict | None = Field(
96
- default=None, description="Pydantic model configuration"
97
- )
80
+ >>> # All unspecified fields are Unset
81
+ >>> params2 = ModelParams(name="SimpleModel")
82
+ >>> assert params2.doc is Unset
83
+ >>> assert params2.frozen is Unset
84
+ """
98
85
 
99
- doc: str | None = Field(
100
- default=None, description="Docstring for the generated model"
86
+ # Class configuration - let Params handle Unset population
87
+ _prefill_unset: ClassVar[bool] = True
88
+ _none_as_sentinel: ClassVar[bool] = True
89
+
90
+ # Public fields (all start as Unset when not provided)
91
+ name: str | None
92
+ parameter_fields: dict[str, FieldInfo]
93
+ base_type: type[BaseModel]
94
+ field_models: list[FieldModel]
95
+ exclude_fields: list[str]
96
+ field_descriptions: dict[str, str]
97
+ inherit_base: bool
98
+ config_dict: dict[str, Any] | None
99
+ doc: str | None
100
+ frozen: bool
101
+
102
+ # Private computed state
103
+ _final_fields: dict[str, FieldInfo] = dc_field(
104
+ default_factory=dict, init=False
101
105
  )
102
-
103
- frozen: bool = Field(
104
- default=False, description="Whether the model should be immutable"
106
+ _validators: dict[str, Callable] = dc_field(
107
+ default_factory=dict, init=False
105
108
  )
106
- _validators: dict[str, Callable] | None = PrivateAttr(default=None)
107
- _use_keys: set[str] = PrivateAttr(default_factory=set)
108
109
 
109
- @property
110
- def use_fields(self) -> dict[str, tuple[type, FieldInfo]]:
111
- """Get field definitions to use in new model.
110
+ def _validate(self) -> None:
111
+ """Validate types and setup model configuration.
112
112
 
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.
113
+ This method performs minimal domain-specific validation, then processes
114
+ and merges field definitions from various sources.
167
115
 
168
116
  Raises:
169
- ValueError: If base type is invalid.
117
+ ValueError: If base_type is not a BaseModel subclass
170
118
  """
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.
119
+ # Let parent handle basic Unset population
120
+ Params._validate(self)
121
+
122
+ # Minimal domain validation - only check what matters
123
+ if not self._is_sentinel(self.base_type):
124
+ if not (
125
+ inspect.isclass(self.base_type)
126
+ and issubclass(self.base_type, BaseModel)
127
+ ):
128
+ raise ValueError(
129
+ f"base_type must be BaseModel subclass, got {self.base_type}"
130
+ )
176
131
 
177
- Args:
178
- value: Value to validate.
132
+ # Process and merge all field sources
133
+ self._process_fields()
179
134
 
180
- Returns:
181
- list[str]: Validated list of field names to exclude.
135
+ def _process_fields(self) -> None:
136
+ """Merge all field sources into final configuration.
182
137
 
183
- Raises:
184
- ValueError: If field names are invalid.
138
+ This method processes and combines fields from parameter_fields, base_type,
139
+ and field_models, handling exclusions and descriptions.
185
140
  """
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.
141
+ fields = {}
142
+ validators = {}
194
143
 
195
- Returns:
196
- dict[str, str]: Validated field descriptions.
144
+ # Start with explicit parameter_fields
145
+ if not self._is_sentinel(self.parameter_fields):
146
+ # Handle empty values - treat them as no fields
147
+ if not self.parameter_fields:
148
+ pass # Empty dict/list/None - no fields to add
149
+ elif isinstance(self.parameter_fields, dict):
150
+ # Validate parameter_fields contain FieldInfo instances
151
+ for name, field_info in self.parameter_fields.items():
152
+ if not isinstance(field_info, FieldInfo):
153
+ raise ValueError(
154
+ f"parameter_fields must contain FieldInfo instances, got {type(field_info)} for field '{name}'"
155
+ )
156
+ fields.update(copy(self.parameter_fields))
157
+ else:
158
+ raise ValueError(
159
+ f"parameter_fields must be a dictionary, got {type(self.parameter_fields)}"
160
+ )
161
+
162
+ # Add base_type fields (respecting exclusions)
163
+ if not self._is_sentinel(self.base_type):
164
+ base_fields = copy(self.base_type.model_fields)
165
+ if not self._is_sentinel(self.exclude_fields):
166
+ base_fields = {
167
+ k: v
168
+ for k, v in base_fields.items()
169
+ if k not in self.exclude_fields
170
+ }
171
+ fields.update(base_fields)
172
+
173
+ # Process field_models
174
+ if not self._is_sentinel(self.field_models):
175
+ # Coerce to list if single FieldModel instance
176
+ field_models_list = (
177
+ [self.field_models]
178
+ if isinstance(self.field_models, FieldModel)
179
+ else self.field_models
180
+ )
181
+
182
+ for fm in field_models_list:
183
+ if not isinstance(fm, FieldModel):
184
+ raise ValueError(
185
+ f"field_models must contain FieldModel instances, got {type(fm)}"
186
+ )
197
187
 
198
- Raises:
199
- ValueError: If descriptions are invalid.
200
- """
201
- return validate_str_str_dict(cls, value)
188
+ # Apply descriptions first
189
+ field_models = field_models_list
190
+ if not self._is_sentinel(self.field_descriptions):
191
+ field_models = [
192
+ (
193
+ fm.with_description(self.field_descriptions[fm.name])
194
+ if fm.name in self.field_descriptions
195
+ else fm
196
+ )
197
+ for fm in field_models
198
+ ]
202
199
 
203
- @field_validator("inherit_base", mode="before")
204
- def _validate_inherit_base(cls, value) -> bool:
205
- """Validate inherit_base flag.
200
+ # Extract fields and validators using public interface
201
+ for fm in field_models:
202
+ fields[fm.name] = fm.create_field()
203
+ fields[fm.name].annotation = fm.annotation
206
204
 
207
- Args:
208
- value: Value to validate.
205
+ # Use the public field_validator property
206
+ if fm.field_validator:
207
+ validators.update(fm.field_validator)
209
208
 
210
- Returns:
211
- bool: Validated inherit_base value.
212
- """
213
- return validate_boolean_field(cls, value, default=True)
209
+ # Store computed state
210
+ object.__setattr__(self, "_final_fields", fields)
211
+ object.__setattr__(self, "_validators", validators)
214
212
 
215
- @field_validator("name", mode="before")
216
- def _validate_name(cls, value) -> str | None:
217
- """Validate model name.
213
+ @property
214
+ def use_fields(self) -> dict[str, tuple[type, FieldInfo]]:
215
+ """Get field definitions to use in new model.
218
216
 
219
- Args:
220
- value: Value to validate.
217
+ Filters and prepares fields based on processed configuration.
221
218
 
222
219
  Returns:
223
- str | None: Validated model name.
224
-
225
- Raises:
226
- ValueError: If name is invalid.
220
+ Dictionary mapping field names to (type, FieldInfo) tuples
227
221
  """
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.
222
+ if not hasattr(self, "_final_fields"):
223
+ return {}
233
224
 
234
- Args:
235
- value: Value to validate.
225
+ return {k: (v.annotation, v) for k, v in self._final_fields.items()}
236
226
 
237
- Returns:
238
- list[FieldModel]: Validated field models.
227
+ @property
228
+ def _use_keys(self) -> set[str]:
229
+ """Get field keys for backward compatibility.
239
230
 
240
- Raises:
241
- ValueError: If field models are invalid.
231
+ Returns the set of field names that will be used in the generated model.
232
+ This is derived from _final_fields for consistency.
242
233
  """
234
+ if not hasattr(self, "_final_fields"):
235
+ return set()
236
+ return set(self._final_fields.keys())
243
237
 
244
- return validate_same_dtype_flat_list(cls, value, FieldModel)
238
+ def _get_cache_key(self) -> int:
239
+ """Create a hashable cache key from object state.
245
240
 
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.
241
+ Converts unhashable types to hashable representations for caching.
260
242
  """
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
243
+ state = self.to_dict()
244
+
245
+ def make_hashable(obj):
246
+ if isinstance(obj, dict):
247
+ return tuple(
248
+ sorted((k, make_hashable(v)) for k, v in obj.items())
249
+ )
250
+ elif isinstance(obj, list):
251
+ return tuple(make_hashable(x) for x in obj)
252
+ elif isinstance(obj, set):
253
+ return tuple(sorted(make_hashable(x) for x in obj))
254
+ else:
255
+ return obj
256
+
257
+ hashable_state = make_hashable(state)
258
+ return hash(hashable_state)
307
259
 
308
260
  def create_new_model(self) -> type[BaseModel]:
309
261
  """Create new Pydantic model with specified configuration.
310
262
 
311
263
  This method generates a new Pydantic model class based on the configured
312
- parameters, including fields, validators, and inheritance settings.
264
+ parameters. Results are cached for performance when the same configuration
265
+ is used multiple times.
313
266
 
314
267
  Returns:
315
- type[BaseModel]: Newly created Pydantic model class.
268
+ Newly created or cached Pydantic model class
316
269
  """
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
270
+ # Create stable cache key from hashable representation
271
+ cache_key = self._get_cache_key()
272
+
273
+ # Check cache first
274
+ with _model_cache_lock:
275
+ if cache_key in _model_cache:
276
+ _model_cache.move_to_end(cache_key)
277
+ return _model_cache[cache_key]
278
+
279
+ # Determine model name
280
+ model_name = self.name
281
+ if self._is_sentinel(model_name) and not self._is_sentinel(
282
+ self.base_type
283
+ ):
284
+ if hasattr(self.base_type, "class_name"):
285
+ model_name = self.base_type.class_name
286
+ if callable(model_name):
287
+ model_name = model_name()
288
+ else:
289
+ model_name = self.base_type.__name__
290
+
291
+ if self._is_sentinel(model_name):
292
+ model_name = "GeneratedModel"
293
+
294
+ # Determine base class
295
+ base_type = None
296
+ if (
297
+ not self._is_sentinel(self.inherit_base)
298
+ and self.inherit_base
299
+ and not self._is_sentinel(self.base_type)
300
+ ):
301
+ # Don't inherit if we're excluding base fields
302
+ if self._is_sentinel(self.exclude_fields) or not any(
303
+ f in self.exclude_fields for f in self.base_type.model_fields
322
304
  ):
323
- base_type = None
305
+ base_type = self.base_type
324
306
 
325
- a: type[BaseModel] = create_model(
326
- self.name or "StepModel",
327
- __config__=self.config_dict,
328
- __doc__=self.doc,
307
+ # Create the model
308
+ model = create_model(
309
+ model_name,
329
310
  __base__=base_type,
330
- __validators__=self._validators,
311
+ __config__=(
312
+ self.config_dict
313
+ if not self._is_sentinel(self.config_dict)
314
+ else None
315
+ ),
316
+ __doc__=self.doc if not self._is_sentinel(self.doc) else None,
317
+ __validators__=self._validators if self._validators else None,
331
318
  **self.use_fields,
332
319
  )
333
- if self.frozen:
334
- a.model_config["frozen"] = True
335
- return a
320
+
321
+ # Apply frozen configuration
322
+ if not self._is_sentinel(self.frozen) and self.frozen:
323
+ model.model_config["frozen"] = True
324
+
325
+ # Cache the result
326
+ with _model_cache_lock:
327
+ _model_cache[cache_key] = model
328
+
329
+ # LRU eviction
330
+ while len(_model_cache) > _MODEL_CACHE_SIZE:
331
+ try:
332
+ _model_cache.popitem(last=False) # Remove oldest
333
+ except KeyError:
334
+ # Handle race condition
335
+ break
336
+
337
+ return model
@@ -115,13 +115,13 @@ class OperableModel(HashableModel):
115
115
  if isinstance(value, dict):
116
116
  for k, v in value.items():
117
117
  if isinstance(v, FieldModel):
118
- out[k] = v.field_info
118
+ out[k] = v.create_field()
119
119
  elif isinstance(v, FieldInfo):
120
120
  out[k] = v
121
121
  return out
122
122
 
123
123
  elif isinstance(value, list) and is_same_dtype(value, FieldModel):
124
- return {v.name: v.field_info for v in value}
124
+ return {v.name: v.create_field() for v in value}
125
125
 
126
126
  raise ValueError("Invalid extra_fields value")
127
127
 
@@ -139,7 +139,7 @@ class OperableModel(HashableModel):
139
139
  if isinstance(self.extra_fields, dict):
140
140
  for k, v in self.extra_fields.items():
141
141
  if isinstance(v, FieldModel):
142
- extra_fields[k] = v.field_info
142
+ extra_fields[k] = v.create_field()
143
143
  extra_field_models[k] = v
144
144
  elif isinstance(v, FieldInfo):
145
145
  extra_fields[k] = v
@@ -149,7 +149,7 @@ class OperableModel(HashableModel):
149
149
  for v in self.extra_fields:
150
150
  # list[FieldModel]
151
151
  if isinstance(v, FieldModel):
152
- extra_fields[v.name] = v.field_info
152
+ extra_fields[v.name] = v.create_field()
153
153
  extra_field_models[v.name] = v
154
154
 
155
155
  # Handle list[tuple[str, FieldInfo | FieldModel]]
@@ -158,11 +158,11 @@ class OperableModel(HashableModel):
158
158
  if isinstance(v[1], FieldInfo):
159
159
  extra_fields[v[0]] = v[1]
160
160
  if isinstance(v[1], FieldModel):
161
- extra_fields[v[1].name] = v[1].field_info
161
+ extra_fields[v[1].name] = v[1].create_field()
162
162
  extra_field_models[v[1].name] = v[1]
163
163
 
164
- self.extra_fields = extra_fields
165
- self.extra_field_models = extra_field_models
164
+ object.__setattr__(self, "extra_fields", extra_fields)
165
+ object.__setattr__(self, "extra_field_models", extra_field_models)
166
166
  return self
167
167
 
168
168
  @override
@@ -349,7 +349,7 @@ class OperableModel(HashableModel):
349
349
  raise ValueError(
350
350
  "Invalid field_model, should be a FieldModel object"
351
351
  )
352
- self.extra_fields[field_name] = field_model.field_info
352
+ self.extra_fields[field_name] = field_model.create_field()
353
353
  self.extra_field_models[field_name] = field_model
354
354
 
355
355
  # Handle kwargs