lionagi 0.18.1__py3-none-any.whl → 0.18.2__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 +102 -59
- lionagi/adapters/spec_adapters/__init__.py +9 -0
- lionagi/adapters/spec_adapters/_protocol.py +236 -0
- lionagi/adapters/spec_adapters/pydantic_field.py +158 -0
- lionagi/ln/_async_call.py +2 -2
- lionagi/ln/fuzzy/_fuzzy_match.py +2 -2
- lionagi/ln/types/__init__.py +51 -0
- lionagi/ln/types/_sentinel.py +154 -0
- lionagi/ln/{types.py → types/base.py} +108 -168
- lionagi/ln/types/operable.py +221 -0
- lionagi/ln/types/spec.py +441 -0
- lionagi/models/field_model.py +57 -3
- lionagi/models/model_params.py +4 -3
- lionagi/operations/operate/operate.py +116 -84
- lionagi/operations/operate/operative.py +142 -305
- lionagi/operations/operate/step.py +162 -181
- lionagi/operations/types.py +6 -6
- lionagi/protocols/messages/message.py +2 -2
- lionagi/version.py +1 -1
- {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/METADATA +1 -1
- {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/RECORD +23 -16
- {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/WHEEL +0 -0
- {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/licenses/LICENSE +0 -0
lionagi/__init__.py
CHANGED
|
@@ -5,94 +5,137 @@ import logging
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from . import ln as ln
|
|
8
|
+
from .ln.types import DataClass, Operable, Params, Spec, Undefined, Unset
|
|
8
9
|
from .version import __version__
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from pydantic import BaseModel, Field
|
|
12
13
|
|
|
13
14
|
from . import _types as types
|
|
15
|
+
from .models.field_model import FieldModel
|
|
16
|
+
from .models.operable_model import OperableModel
|
|
14
17
|
from .operations.builder import OperationGraphBuilder as Builder
|
|
15
18
|
from .operations.node import Operation
|
|
16
19
|
from .protocols.action.manager import load_mcp_tools
|
|
20
|
+
from .protocols.types import (
|
|
21
|
+
Edge,
|
|
22
|
+
Element,
|
|
23
|
+
Event,
|
|
24
|
+
Graph,
|
|
25
|
+
Node,
|
|
26
|
+
Pile,
|
|
27
|
+
Progression,
|
|
28
|
+
)
|
|
29
|
+
from .service.broadcaster import Broadcaster
|
|
30
|
+
from .service.hooks import HookedEvent, HookRegistry
|
|
17
31
|
from .service.imodel import iModel
|
|
18
32
|
from .session.session import Branch, Session
|
|
19
33
|
|
|
20
|
-
|
|
21
34
|
logger = logging.getLogger(__name__)
|
|
22
35
|
logger.setLevel(logging.INFO)
|
|
23
36
|
|
|
24
|
-
# Module-level lazy loading cache
|
|
25
37
|
_lazy_imports = {}
|
|
26
38
|
|
|
27
39
|
|
|
40
|
+
def _get_obj(name: str, module: str):
|
|
41
|
+
global _lazy_imports
|
|
42
|
+
from lionagi.ln import import_module
|
|
43
|
+
|
|
44
|
+
obj_ = import_module("lionagi", module_name=module, import_name=name)
|
|
45
|
+
|
|
46
|
+
_lazy_imports[name] = obj_
|
|
47
|
+
return obj_
|
|
48
|
+
|
|
49
|
+
|
|
28
50
|
def __getattr__(name: str):
|
|
29
|
-
|
|
51
|
+
global _lazy_imports
|
|
30
52
|
if name in _lazy_imports:
|
|
31
53
|
return _lazy_imports[name]
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
55
|
+
match name:
|
|
56
|
+
case "Session":
|
|
57
|
+
return _get_obj("Session", "session.session")
|
|
58
|
+
case "Branch":
|
|
59
|
+
return _get_obj("Branch", "session.branch")
|
|
60
|
+
case "iModel":
|
|
61
|
+
return _get_obj("iModel", "service.imodel")
|
|
62
|
+
case "Builder":
|
|
63
|
+
return _get_obj("OperationGraphBuilder", "operations.builder")
|
|
64
|
+
case "Operation":
|
|
65
|
+
return _get_obj("Operation", "operations.node")
|
|
66
|
+
case "load_mcp_tools":
|
|
67
|
+
return _get_obj("load_mcp_tools", "protocols.action.manager")
|
|
68
|
+
case "FieldModel":
|
|
69
|
+
return _get_obj("FieldModel", "models.field_model")
|
|
70
|
+
case "OperableModel":
|
|
71
|
+
return _get_obj("OperableModel", "models.operable_model")
|
|
72
|
+
case "Element":
|
|
73
|
+
return _get_obj("Element", "protocols.generic.element")
|
|
74
|
+
case "Pile":
|
|
75
|
+
return _get_obj("Pile", "protocols.generic.pile")
|
|
76
|
+
case "Progression":
|
|
77
|
+
return _get_obj("Progression", "protocols.generic.progression")
|
|
78
|
+
case "Node":
|
|
79
|
+
return _get_obj("Node", "protocols.graph.node")
|
|
80
|
+
case "Edge":
|
|
81
|
+
return _get_obj("Edge", "protocols.graph.edge")
|
|
82
|
+
case "Graph":
|
|
83
|
+
return _get_obj("Graph", "protocols.graph.graph")
|
|
84
|
+
case "Event":
|
|
85
|
+
return _get_obj("Event", "protocols.generic.event")
|
|
86
|
+
case "HookRegistry":
|
|
87
|
+
return _get_obj("HookRegistry", "service.hooks.hook_registry")
|
|
88
|
+
case "HookedEvent":
|
|
89
|
+
return _get_obj("HookedEvent", "service.hooks.hooked_event")
|
|
90
|
+
case "Broadcaster":
|
|
91
|
+
return _get_obj("Broadcaster", "service.broadcaster")
|
|
92
|
+
case "BaseModel":
|
|
93
|
+
from pydantic import BaseModel
|
|
94
|
+
|
|
95
|
+
_lazy_imports["BaseModel"] = BaseModel
|
|
96
|
+
return BaseModel
|
|
97
|
+
case "Field":
|
|
98
|
+
from pydantic import Field
|
|
99
|
+
|
|
100
|
+
_lazy_imports["Field"] = Field
|
|
101
|
+
return Field
|
|
102
|
+
case "types":
|
|
103
|
+
from . import _types as types
|
|
104
|
+
|
|
105
|
+
_lazy_imports["types"] = types
|
|
106
|
+
return types
|
|
82
107
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
83
108
|
|
|
84
109
|
|
|
85
110
|
__all__ = (
|
|
86
|
-
"Session",
|
|
87
|
-
"Branch",
|
|
88
|
-
"iModel",
|
|
89
|
-
"types",
|
|
90
111
|
"__version__",
|
|
91
112
|
"BaseModel",
|
|
92
|
-
"
|
|
93
|
-
"
|
|
113
|
+
"Branch",
|
|
114
|
+
"Broadcaster",
|
|
94
115
|
"Builder",
|
|
116
|
+
"DataClass",
|
|
117
|
+
"Edge",
|
|
118
|
+
"Element",
|
|
119
|
+
"Event",
|
|
120
|
+
"Field",
|
|
121
|
+
"FieldModel",
|
|
122
|
+
"Graph",
|
|
123
|
+
"HookRegistry",
|
|
124
|
+
"HookedEvent",
|
|
125
|
+
"Node",
|
|
126
|
+
"Operable",
|
|
127
|
+
"OperableModel",
|
|
95
128
|
"Operation",
|
|
96
|
-
"
|
|
129
|
+
"Params",
|
|
130
|
+
"Pile",
|
|
131
|
+
"Progression",
|
|
132
|
+
"Session",
|
|
133
|
+
"Spec",
|
|
134
|
+
"Undefined",
|
|
135
|
+
"Unset",
|
|
136
|
+
"iModel",
|
|
97
137
|
"ln",
|
|
138
|
+
"load_mcp_tools",
|
|
139
|
+
"logger",
|
|
140
|
+
"types",
|
|
98
141
|
)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Abstract base class for Spec adapters.
|
|
2
|
+
|
|
3
|
+
Adapters convert framework-agnostic Spec objects to framework-specific
|
|
4
|
+
field and model definitions (Pydantic, msgspec, attrs, dataclasses).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from lionagi.ln.types import Operable, Spec
|
|
12
|
+
|
|
13
|
+
__all__ = ("SpecAdapter",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SpecAdapter(ABC):
|
|
17
|
+
"""Base adapter for converting Spec to framework-specific formats.
|
|
18
|
+
|
|
19
|
+
Abstract Methods (must implement):
|
|
20
|
+
- create_field: Spec → framework field
|
|
21
|
+
- create_model: Operable → framework model class
|
|
22
|
+
- validate_model: dict → validated model instance
|
|
23
|
+
- dump_model: model instance → dict
|
|
24
|
+
|
|
25
|
+
Concrete Methods (shared):
|
|
26
|
+
- parse_json: Extract JSON from text
|
|
27
|
+
- fuzzy_match_fields: Match dict keys to model fields
|
|
28
|
+
- validate_response: Full validation pipeline
|
|
29
|
+
- update_model: Update model instance (uses dump_model + validate_model)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# ---- Abstract Methods ----
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def create_field(cls, spec: "Spec") -> Any:
|
|
37
|
+
"""Convert Spec to framework-specific field definition.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
spec: Spec object
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Framework-specific field (FieldInfo, Attribute, Field, etc.)
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def create_model(
|
|
50
|
+
cls,
|
|
51
|
+
operable: "Operable",
|
|
52
|
+
model_name: str,
|
|
53
|
+
include: set[str] | None = None,
|
|
54
|
+
exclude: set[str] | None = None,
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> type:
|
|
57
|
+
"""Generate model class from Operable.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
operable: Operable containing specs
|
|
61
|
+
model_name: Name for generated model
|
|
62
|
+
include: Only include these field names
|
|
63
|
+
exclude: Exclude these field names
|
|
64
|
+
**kwargs: Framework-specific options
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Generated model class
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def validate_model(cls, model_cls: type, data: dict) -> Any:
|
|
74
|
+
"""Validate dict data into model instance.
|
|
75
|
+
|
|
76
|
+
Framework-agnostic validation hook. Each adapter implements
|
|
77
|
+
the appropriate validation mechanism:
|
|
78
|
+
- Pydantic: model_cls.model_validate(data)
|
|
79
|
+
- msgspec: msgspec.convert(data, type=model_cls)
|
|
80
|
+
- attrs: model_cls(**data)
|
|
81
|
+
- dataclasses: model_cls(**data)
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
model_cls: Model class
|
|
85
|
+
data: Dictionary data to validate
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Validated model instance
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def dump_model(cls, instance: Any) -> dict:
|
|
95
|
+
"""Dump model instance to dictionary.
|
|
96
|
+
|
|
97
|
+
Framework-agnostic serialization hook. Each adapter implements
|
|
98
|
+
the appropriate serialization mechanism:
|
|
99
|
+
- Pydantic: instance.model_dump()
|
|
100
|
+
- msgspec: msgspec.to_builtins(instance)
|
|
101
|
+
- attrs: attr.asdict(instance)
|
|
102
|
+
- dataclasses: dataclasses.asdict(instance)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
instance: Model instance
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary representation
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def create_validator(cls, spec: "Spec") -> Any:
|
|
114
|
+
"""Generate framework-specific validators from Spec metadata.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
spec: Spec with validator metadata
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Framework-specific validator, or None if not supported
|
|
121
|
+
"""
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# ---- Concrete Methods (Shared) ----
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def parse_json(cls, text: str, fuzzy: bool = True) -> dict | list | Any:
|
|
128
|
+
"""Extract and parse JSON from text.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
text: Raw text potentially containing JSON
|
|
132
|
+
fuzzy: Use fuzzy parsing (markdown extraction)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Parsed JSON object
|
|
136
|
+
"""
|
|
137
|
+
from lionagi.ln import extract_json
|
|
138
|
+
|
|
139
|
+
data = extract_json(text, fuzzy_parse=fuzzy)
|
|
140
|
+
|
|
141
|
+
# Unwrap single-item lists/tuples
|
|
142
|
+
if isinstance(data, (list, tuple)) and len(data) == 1:
|
|
143
|
+
data = data[0]
|
|
144
|
+
|
|
145
|
+
return data
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
@abstractmethod
|
|
149
|
+
def fuzzy_match_fields(
|
|
150
|
+
cls, data: dict, model_cls: type, strict: bool = False
|
|
151
|
+
) -> dict:
|
|
152
|
+
"""Match data keys to model fields with fuzzy matching.
|
|
153
|
+
|
|
154
|
+
Framework-specific method - each adapter must implement based on how
|
|
155
|
+
their framework exposes field definitions.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
data: Raw data dictionary
|
|
159
|
+
model_cls: Target model class
|
|
160
|
+
strict: If True, raise on unmatched; if False, force coercion
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary with keys matched to model fields
|
|
164
|
+
"""
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def validate_response(
|
|
169
|
+
cls,
|
|
170
|
+
text: str,
|
|
171
|
+
model_cls: type,
|
|
172
|
+
strict: bool = False,
|
|
173
|
+
fuzzy_parse: bool = True,
|
|
174
|
+
) -> Any | None:
|
|
175
|
+
"""Validate and parse response text into model instance.
|
|
176
|
+
|
|
177
|
+
Pipeline: parse_json → fuzzy_match_fields → validate_model
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
text: Raw response text
|
|
181
|
+
model_cls: Target model class
|
|
182
|
+
strict: If True, raise on errors; if False, return None
|
|
183
|
+
fuzzy_parse: Use fuzzy JSON parsing
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Validated model instance, or None if validation fails (strict=False)
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
# Step 1: Parse JSON
|
|
190
|
+
data = cls.parse_json(text, fuzzy=fuzzy_parse)
|
|
191
|
+
|
|
192
|
+
# Step 2: Fuzzy match fields
|
|
193
|
+
matched_data = cls.fuzzy_match_fields(
|
|
194
|
+
data, model_cls, strict=strict
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Step 3: Validate with framework-specific method
|
|
198
|
+
instance = cls.validate_model(model_cls, matched_data)
|
|
199
|
+
|
|
200
|
+
return instance
|
|
201
|
+
|
|
202
|
+
except (ValueError, TypeError, KeyError, AttributeError) as e:
|
|
203
|
+
# Catch validation-related exceptions only
|
|
204
|
+
# ValueError: JSON/parsing errors, validation failures
|
|
205
|
+
# TypeError: Type mismatches during validation
|
|
206
|
+
# KeyError: Missing required fields
|
|
207
|
+
# AttributeError: Field access errors
|
|
208
|
+
if strict:
|
|
209
|
+
raise
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def update_model(
|
|
214
|
+
cls,
|
|
215
|
+
instance: Any,
|
|
216
|
+
updates: dict,
|
|
217
|
+
model_cls: type | None = None,
|
|
218
|
+
) -> Any:
|
|
219
|
+
"""Update existing model instance with new data.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
instance: Existing model instance
|
|
223
|
+
updates: Dictionary of updates
|
|
224
|
+
model_cls: Optional model class (defaults to instance's class)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
New validated model instance with updates applied
|
|
228
|
+
"""
|
|
229
|
+
model_cls = model_cls or type(instance)
|
|
230
|
+
|
|
231
|
+
# Merge existing data with updates
|
|
232
|
+
current_data = cls.dump_model(instance)
|
|
233
|
+
current_data.update(updates)
|
|
234
|
+
|
|
235
|
+
# Validate merged data
|
|
236
|
+
return cls.validate_model(model_cls, current_data)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Pydantic adapter for Spec system."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from lionagi.ln.types import Unset, is_sentinel
|
|
7
|
+
|
|
8
|
+
from ._protocol import SpecAdapter
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from pydantic.fields import FieldInfo
|
|
13
|
+
|
|
14
|
+
from lionagi.ln.types import Operable, Spec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@functools.lru_cache(maxsize=1)
|
|
18
|
+
def _get_pydantic_field_params() -> set[str]:
|
|
19
|
+
"""Get valid Pydantic Field parameters (cached, thread-safe)."""
|
|
20
|
+
import inspect
|
|
21
|
+
|
|
22
|
+
from pydantic import Field as PydanticField
|
|
23
|
+
|
|
24
|
+
params = set(inspect.signature(PydanticField).parameters.keys())
|
|
25
|
+
params.discard("kwargs")
|
|
26
|
+
return params
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PydanticSpecAdapter(SpecAdapter):
|
|
30
|
+
"""Pydantic implementation of SpecAdapter."""
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def create_field(cls, spec: "Spec") -> "FieldInfo":
|
|
34
|
+
"""Create a Pydantic FieldInfo object from Spec."""
|
|
35
|
+
from pydantic import Field as PydanticField
|
|
36
|
+
|
|
37
|
+
# Get valid Pydantic Field parameters (cached)
|
|
38
|
+
pydantic_field_params = _get_pydantic_field_params()
|
|
39
|
+
|
|
40
|
+
# Extract metadata for FieldInfo
|
|
41
|
+
field_kwargs = {}
|
|
42
|
+
|
|
43
|
+
if not is_sentinel(spec.metadata, none_as_sentinel=True):
|
|
44
|
+
for meta in spec.metadata:
|
|
45
|
+
if meta.key == "default":
|
|
46
|
+
# Handle callable defaults as default_factory
|
|
47
|
+
if callable(meta.value):
|
|
48
|
+
field_kwargs["default_factory"] = meta.value
|
|
49
|
+
else:
|
|
50
|
+
field_kwargs["default"] = meta.value
|
|
51
|
+
elif meta.key == "validator":
|
|
52
|
+
# Validators are handled separately in create_model
|
|
53
|
+
continue
|
|
54
|
+
elif meta.key in pydantic_field_params:
|
|
55
|
+
# Pass through standard Pydantic field attributes
|
|
56
|
+
field_kwargs[meta.key] = meta.value
|
|
57
|
+
elif meta.key in {"nullable", "listable"}:
|
|
58
|
+
# These are FieldTemplate markers, don't pass to FieldInfo
|
|
59
|
+
pass
|
|
60
|
+
else:
|
|
61
|
+
# Filter out unserializable objects from json_schema_extra
|
|
62
|
+
if isinstance(meta.value, type):
|
|
63
|
+
# Skip type objects - can't be serialized
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# Any other metadata goes in json_schema_extra
|
|
67
|
+
if "json_schema_extra" not in field_kwargs:
|
|
68
|
+
field_kwargs["json_schema_extra"] = {}
|
|
69
|
+
field_kwargs["json_schema_extra"][meta.key] = meta.value
|
|
70
|
+
|
|
71
|
+
# Handle nullable case - ensure default is set if not already
|
|
72
|
+
if (
|
|
73
|
+
spec.is_nullable
|
|
74
|
+
and "default" not in field_kwargs
|
|
75
|
+
and "default_factory" not in field_kwargs
|
|
76
|
+
):
|
|
77
|
+
field_kwargs["default"] = None
|
|
78
|
+
|
|
79
|
+
field_info = PydanticField(**field_kwargs)
|
|
80
|
+
field_info.annotation = spec.annotation
|
|
81
|
+
|
|
82
|
+
return field_info
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def create_validator(cls, spec: "Spec") -> dict | None:
|
|
86
|
+
"""Create Pydantic field_validator from Spec metadata."""
|
|
87
|
+
if (v := spec.get("validator")) is Unset:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
from pydantic import field_validator
|
|
91
|
+
|
|
92
|
+
field_name = spec.name or "field"
|
|
93
|
+
return {f"{field_name}_validator": field_validator(field_name)(v)}
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def create_model(
|
|
97
|
+
cls,
|
|
98
|
+
op: "Operable",
|
|
99
|
+
model_name: str,
|
|
100
|
+
include: set[str] | None = None,
|
|
101
|
+
exclude: set[str] | None = None,
|
|
102
|
+
base_type: type["BaseModel"] | None = None,
|
|
103
|
+
doc: str | None = None,
|
|
104
|
+
) -> type["BaseModel"]:
|
|
105
|
+
"""Generate Pydantic BaseModel from Operable."""
|
|
106
|
+
from lionagi.models.model_params import ModelParams
|
|
107
|
+
|
|
108
|
+
use_specs = op.get_specs(include=include, exclude=exclude)
|
|
109
|
+
use_fields = {i.name: cls.create_field(i) for i in use_specs if i.name}
|
|
110
|
+
|
|
111
|
+
model_cls = ModelParams(
|
|
112
|
+
name=model_name,
|
|
113
|
+
parameter_fields=use_fields,
|
|
114
|
+
base_type=base_type,
|
|
115
|
+
inherit_base=True,
|
|
116
|
+
doc=doc,
|
|
117
|
+
).create_new_model()
|
|
118
|
+
|
|
119
|
+
model_cls.model_rebuild()
|
|
120
|
+
return model_cls
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def fuzzy_match_fields(
|
|
124
|
+
cls, data: dict, model_cls: type["BaseModel"], strict: bool = False
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Match data keys to Pydantic model fields with fuzzy matching.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
data: Raw data dictionary
|
|
130
|
+
model_cls: Pydantic model class
|
|
131
|
+
strict: If True, raise on unmatched; if False, force coercion
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary with keys matched to model fields
|
|
135
|
+
"""
|
|
136
|
+
from lionagi.ln import fuzzy_match_keys
|
|
137
|
+
from lionagi.ln.types import Undefined
|
|
138
|
+
|
|
139
|
+
handle_mode = "raise" if strict else "force"
|
|
140
|
+
|
|
141
|
+
matched = fuzzy_match_keys(
|
|
142
|
+
data, model_cls.model_fields, handle_unmatched=handle_mode
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Filter out undefined values
|
|
146
|
+
return {k: v for k, v in matched.items() if v != Undefined}
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def validate_model(
|
|
150
|
+
cls, model_cls: type["BaseModel"], data: dict
|
|
151
|
+
) -> "BaseModel":
|
|
152
|
+
"""Validate dict data into Pydantic model instance."""
|
|
153
|
+
return model_cls.model_validate(data)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def dump_model(cls, instance: "BaseModel") -> dict:
|
|
157
|
+
"""Dump Pydantic model instance to dictionary."""
|
|
158
|
+
return instance.model_dump()
|
lionagi/ln/_async_call.py
CHANGED
|
@@ -15,7 +15,7 @@ from .concurrency import (
|
|
|
15
15
|
is_coro_func,
|
|
16
16
|
move_on_after,
|
|
17
17
|
)
|
|
18
|
-
from .types import Params, T, Unset, not_sentinel
|
|
18
|
+
from .types import ModelConfig, Params, T, Unset, not_sentinel
|
|
19
19
|
|
|
20
20
|
_INITIALIZED = False
|
|
21
21
|
_MODEL_LIKE = None
|
|
@@ -262,7 +262,7 @@ async def bcall(
|
|
|
262
262
|
@dataclass(slots=True, init=False, frozen=True)
|
|
263
263
|
class AlcallParams(Params):
|
|
264
264
|
# ClassVar attributes
|
|
265
|
-
|
|
265
|
+
_config: ClassVar[ModelConfig] = ModelConfig(none_as_sentinel=True)
|
|
266
266
|
_func: ClassVar[Any] = alcall
|
|
267
267
|
|
|
268
268
|
# input processing
|
lionagi/ln/fuzzy/_fuzzy_match.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Any, ClassVar, Literal
|
|
3
3
|
|
|
4
|
-
from ..types import KeysLike, Params, Unset
|
|
4
|
+
from ..types import KeysLike, ModelConfig, Params, Unset
|
|
5
5
|
from ._string_similarity import (
|
|
6
6
|
SIMILARITY_ALGO_MAP,
|
|
7
7
|
SIMILARITY_TYPE,
|
|
@@ -152,7 +152,7 @@ def fuzzy_match_keys(
|
|
|
152
152
|
|
|
153
153
|
@dataclass(slots=True, init=False, frozen=True)
|
|
154
154
|
class FuzzyMatchKeysParams(Params):
|
|
155
|
-
|
|
155
|
+
_config: ClassVar[ModelConfig] = ModelConfig(none_as_sentinel=False)
|
|
156
156
|
_func: ClassVar[Any] = fuzzy_match_keys
|
|
157
157
|
|
|
158
158
|
similarity_algo: SIMILARITY_TYPE | SimilarityFunc = "jaro_winkler"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from ._sentinel import (
|
|
2
|
+
MaybeSentinel,
|
|
3
|
+
MaybeUndefined,
|
|
4
|
+
MaybeUnset,
|
|
5
|
+
SingletonType,
|
|
6
|
+
T,
|
|
7
|
+
Undefined,
|
|
8
|
+
UndefinedType,
|
|
9
|
+
Unset,
|
|
10
|
+
UnsetType,
|
|
11
|
+
is_sentinel,
|
|
12
|
+
not_sentinel,
|
|
13
|
+
)
|
|
14
|
+
from .base import (
|
|
15
|
+
DataClass,
|
|
16
|
+
Enum,
|
|
17
|
+
KeysDict,
|
|
18
|
+
KeysLike,
|
|
19
|
+
Meta,
|
|
20
|
+
ModelConfig,
|
|
21
|
+
Params,
|
|
22
|
+
)
|
|
23
|
+
from .operable import Operable
|
|
24
|
+
from .spec import CommonMeta, Spec
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
# Sentinel types
|
|
28
|
+
"Undefined",
|
|
29
|
+
"Unset",
|
|
30
|
+
"MaybeUndefined",
|
|
31
|
+
"MaybeUnset",
|
|
32
|
+
"MaybeSentinel",
|
|
33
|
+
"SingletonType",
|
|
34
|
+
"UndefinedType",
|
|
35
|
+
"UnsetType",
|
|
36
|
+
"is_sentinel",
|
|
37
|
+
"not_sentinel",
|
|
38
|
+
# Base classes
|
|
39
|
+
"ModelConfig",
|
|
40
|
+
"Enum",
|
|
41
|
+
"Params",
|
|
42
|
+
"DataClass",
|
|
43
|
+
"Meta",
|
|
44
|
+
"KeysDict",
|
|
45
|
+
"KeysLike",
|
|
46
|
+
"T",
|
|
47
|
+
# Spec system
|
|
48
|
+
"Spec",
|
|
49
|
+
"CommonMeta",
|
|
50
|
+
"Operable",
|
|
51
|
+
)
|