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.
- lionagi/__init__.py +1 -2
- lionagi/_class_registry.py +1 -2
- lionagi/_errors.py +1 -2
- lionagi/adapters/async_postgres_adapter.py +2 -10
- lionagi/config.py +1 -2
- lionagi/fields/action.py +1 -2
- lionagi/fields/base.py +3 -0
- lionagi/fields/code.py +3 -0
- lionagi/fields/file.py +3 -0
- lionagi/fields/instruct.py +1 -2
- lionagi/fields/reason.py +3 -2
- lionagi/fields/research.py +3 -0
- lionagi/libs/__init__.py +1 -2
- lionagi/libs/file/__init__.py +1 -2
- lionagi/libs/file/chunk.py +1 -2
- lionagi/libs/file/process.py +1 -2
- lionagi/libs/schema/__init__.py +1 -2
- lionagi/libs/schema/as_readable.py +1 -2
- lionagi/libs/schema/extract_code_block.py +1 -2
- lionagi/libs/schema/extract_docstring.py +1 -2
- lionagi/libs/schema/function_to_schema.py +1 -2
- lionagi/libs/schema/load_pydantic_model_from_schema.py +1 -2
- lionagi/libs/validate/__init__.py +1 -2
- lionagi/libs/validate/common_field_validators.py +1 -2
- lionagi/libs/validate/validate_boolean.py +1 -2
- lionagi/ln/fuzzy/_string_similarity.py +1 -2
- lionagi/ln/types.py +45 -0
- lionagi/models/__init__.py +1 -2
- lionagi/models/field_model.py +392 -286
- lionagi/models/hashable_model.py +98 -14
- lionagi/models/model_params.py +272 -271
- lionagi/models/operable_model.py +9 -10
- lionagi/models/schema_model.py +1 -2
- lionagi/operations/ReAct/ReAct.py +1 -2
- lionagi/operations/ReAct/__init__.py +1 -2
- lionagi/operations/ReAct/utils.py +1 -2
- lionagi/operations/__init__.py +1 -2
- lionagi/operations/_act/__init__.py +1 -2
- lionagi/operations/_act/act.py +1 -2
- lionagi/operations/brainstorm/__init__.py +1 -2
- lionagi/operations/brainstorm/brainstorm.py +1 -2
- lionagi/operations/brainstorm/prompt.py +1 -2
- lionagi/operations/builder.py +1 -2
- lionagi/operations/chat/__init__.py +1 -2
- lionagi/operations/chat/chat.py +1 -2
- lionagi/operations/communicate/communicate.py +1 -2
- lionagi/operations/flow.py +1 -2
- lionagi/operations/instruct/__init__.py +1 -2
- lionagi/operations/instruct/instruct.py +1 -2
- lionagi/operations/interpret/__init__.py +1 -2
- lionagi/operations/interpret/interpret.py +1 -2
- lionagi/operations/operate/__init__.py +1 -2
- lionagi/operations/operate/operate.py +1 -2
- lionagi/operations/parse/__init__.py +1 -2
- lionagi/operations/parse/parse.py +1 -2
- lionagi/operations/plan/__init__.py +1 -2
- lionagi/operations/plan/plan.py +1 -2
- lionagi/operations/plan/prompt.py +1 -2
- lionagi/operations/select/__init__.py +1 -2
- lionagi/operations/select/select.py +1 -2
- lionagi/operations/select/utils.py +1 -2
- lionagi/operations/types.py +1 -2
- lionagi/operations/utils.py +1 -2
- lionagi/protocols/__init__.py +1 -2
- lionagi/protocols/_concepts.py +1 -2
- lionagi/protocols/action/__init__.py +1 -2
- lionagi/protocols/action/function_calling.py +3 -20
- lionagi/protocols/action/manager.py +34 -4
- lionagi/protocols/action/tool.py +1 -2
- lionagi/protocols/contracts.py +1 -2
- lionagi/protocols/forms/__init__.py +1 -2
- lionagi/protocols/forms/base.py +1 -2
- lionagi/protocols/forms/flow.py +1 -2
- lionagi/protocols/forms/form.py +1 -2
- lionagi/protocols/forms/report.py +1 -2
- lionagi/protocols/generic/__init__.py +1 -2
- lionagi/protocols/generic/element.py +17 -65
- lionagi/protocols/generic/event.py +1 -2
- lionagi/protocols/generic/log.py +14 -12
- lionagi/protocols/generic/pile.py +6 -4
- lionagi/protocols/generic/processor.py +1 -2
- lionagi/protocols/generic/progression.py +1 -2
- lionagi/protocols/graph/__init__.py +1 -2
- lionagi/protocols/graph/edge.py +1 -2
- lionagi/protocols/graph/graph.py +1 -2
- lionagi/protocols/graph/node.py +1 -2
- lionagi/protocols/ids.py +1 -2
- lionagi/protocols/mail/__init__.py +1 -2
- lionagi/protocols/mail/exchange.py +1 -2
- lionagi/protocols/mail/mail.py +1 -2
- lionagi/protocols/mail/mailbox.py +1 -2
- lionagi/protocols/mail/manager.py +1 -2
- lionagi/protocols/mail/package.py +1 -2
- lionagi/protocols/messages/__init__.py +1 -2
- lionagi/protocols/messages/action_request.py +1 -2
- lionagi/protocols/messages/action_response.py +1 -2
- lionagi/protocols/messages/assistant_response.py +1 -2
- lionagi/protocols/messages/base.py +1 -2
- lionagi/protocols/messages/instruction.py +1 -2
- lionagi/protocols/messages/manager.py +1 -2
- lionagi/protocols/messages/message.py +1 -2
- lionagi/protocols/messages/system.py +1 -2
- lionagi/protocols/operatives/__init__.py +1 -2
- lionagi/protocols/operatives/operative.py +30 -8
- lionagi/protocols/operatives/step.py +1 -2
- lionagi/protocols/types.py +1 -2
- lionagi/service/connections/__init__.py +1 -2
- lionagi/service/connections/api_calling.py +1 -2
- lionagi/service/connections/endpoint.py +1 -2
- lionagi/service/connections/endpoint_config.py +1 -2
- lionagi/service/connections/header_factory.py +1 -2
- lionagi/service/connections/match_endpoint.py +1 -2
- lionagi/service/connections/mcp/__init__.py +1 -2
- lionagi/service/connections/mcp/wrapper.py +1 -2
- lionagi/service/connections/providers/__init__.py +1 -2
- lionagi/service/connections/providers/anthropic_.py +1 -2
- lionagi/service/connections/providers/claude_code_cli.py +1 -2
- lionagi/service/connections/providers/exa_.py +1 -2
- lionagi/service/connections/providers/nvidia_nim_.py +2 -27
- lionagi/service/connections/providers/oai_.py +1 -2
- lionagi/service/connections/providers/ollama_.py +1 -2
- lionagi/service/connections/providers/perplexity_.py +1 -2
- lionagi/service/hooks/__init__.py +1 -1
- lionagi/service/hooks/_types.py +1 -1
- lionagi/service/hooks/_utils.py +1 -1
- lionagi/service/hooks/hook_event.py +1 -1
- lionagi/service/hooks/hook_registry.py +1 -1
- lionagi/service/hooks/hooked_event.py +1 -2
- lionagi/service/imodel.py +1 -2
- lionagi/service/manager.py +1 -2
- lionagi/service/rate_limited_processor.py +1 -2
- lionagi/service/resilience.py +1 -2
- lionagi/service/third_party/anthropic_models.py +3 -5
- lionagi/service/third_party/claude_code.py +1 -2
- lionagi/service/token_calculator.py +1 -2
- lionagi/service/types.py +1 -2
- lionagi/session/__init__.py +1 -2
- lionagi/session/branch.py +1 -2
- lionagi/session/session.py +1 -2
- lionagi/tools/__init__.py +1 -2
- lionagi/tools/base.py +1 -2
- lionagi/tools/file/__init__.py +1 -2
- lionagi/tools/file/reader.py +1 -2
- lionagi/tools/types.py +1 -2
- lionagi/utils.py +1 -2
- lionagi/version.py +1 -1
- {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/METADATA +2 -2
- lionagi-0.17.11.dist-info/RECORD +199 -0
- lionagi-0.17.9.dist-info/RECORD +0 -199
- {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/WHEEL +0 -0
- {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/licenses/LICENSE +0 -0
lionagi/models/model_params.py
CHANGED
@@ -1,335 +1,336 @@
|
|
1
|
-
# Copyright (c) 2023
|
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
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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"
|
59
|
-
... FieldModel(name="age",
|
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
|
-
|
96
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
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
|
-
|
110
|
-
|
111
|
-
"""Get field definitions to use in new model.
|
109
|
+
def _validate(self) -> None:
|
110
|
+
"""Validate types and setup model configuration.
|
112
111
|
|
113
|
-
|
114
|
-
|
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
|
116
|
+
ValueError: If base_type is not a BaseModel subclass
|
170
117
|
"""
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
178
|
-
|
131
|
+
# Process and merge all field sources
|
132
|
+
self._process_fields()
|
179
133
|
|
180
|
-
|
181
|
-
|
134
|
+
def _process_fields(self) -> None:
|
135
|
+
"""Merge all field sources into final configuration.
|
182
136
|
|
183
|
-
|
184
|
-
|
137
|
+
This method processes and combines fields from parameter_fields, base_type,
|
138
|
+
and field_models, handling exclusions and descriptions.
|
185
139
|
"""
|
186
|
-
|
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
|
-
|
196
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
208
|
-
|
204
|
+
# Use the public field_validator property
|
205
|
+
if fm.field_validator:
|
206
|
+
validators.update(fm.field_validator)
|
209
207
|
|
210
|
-
|
211
|
-
|
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
|
-
@
|
216
|
-
def
|
217
|
-
"""
|
212
|
+
@property
|
213
|
+
def use_fields(self) -> dict[str, tuple[type, FieldInfo]]:
|
214
|
+
"""Get field definitions to use in new model.
|
218
215
|
|
219
|
-
|
220
|
-
value: Value to validate.
|
216
|
+
Filters and prepares fields based on processed configuration.
|
221
217
|
|
222
218
|
Returns:
|
223
|
-
|
224
|
-
|
225
|
-
Raises:
|
226
|
-
ValueError: If name is invalid.
|
219
|
+
Dictionary mapping field names to (type, FieldInfo) tuples
|
227
220
|
"""
|
228
|
-
|
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
|
-
|
235
|
-
value: Value to validate.
|
224
|
+
return {k: (v.annotation, v) for k, v in self._final_fields.items()}
|
236
225
|
|
237
|
-
|
238
|
-
|
226
|
+
@property
|
227
|
+
def _use_keys(self) -> set[str]:
|
228
|
+
"""Get field keys for backward compatibility.
|
239
229
|
|
240
|
-
|
241
|
-
|
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
|
-
|
237
|
+
def _get_cache_key(self) -> int:
|
238
|
+
"""Create a hashable cache key from object state.
|
245
239
|
|
246
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
263
|
+
parameters. Results are cached for performance when the same configuration
|
264
|
+
is used multiple times.
|
313
265
|
|
314
266
|
Returns:
|
315
|
-
|
267
|
+
Newly created or cached Pydantic model class
|
316
268
|
"""
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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 =
|
304
|
+
base_type = self.base_type
|
324
305
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
__doc__=self.doc,
|
306
|
+
# Create the model
|
307
|
+
model = create_model(
|
308
|
+
model_name,
|
329
309
|
__base__=base_type,
|
330
|
-
|
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
|
-
|
334
|
-
|
335
|
-
|
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
|
lionagi/models/operable_model.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
# Copyright (c) 2023
|
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.
|
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.
|
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.
|
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.
|
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].
|
160
|
+
extra_fields[v[1].name] = v[1].create_field()
|
162
161
|
extra_field_models[v[1].name] = v[1]
|
163
162
|
|
164
|
-
self
|
165
|
-
self
|
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.
|
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
|