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 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
- """Lazy loading for expensive imports."""
51
+ global _lazy_imports
30
52
  if name in _lazy_imports:
31
53
  return _lazy_imports[name]
32
54
 
33
- # Lazy load core components
34
- if name == "Session":
35
- from .session.session import Session
36
-
37
- _lazy_imports[name] = Session
38
- return Session
39
- elif name == "Branch":
40
- from .session.session import Branch
41
-
42
- _lazy_imports[name] = Branch
43
- return Branch
44
- # Lazy load Pydantic components
45
- elif name == "BaseModel":
46
- from pydantic import BaseModel
47
-
48
- _lazy_imports[name] = BaseModel
49
- return BaseModel
50
- elif name == "Field":
51
- from pydantic import Field
52
-
53
- _lazy_imports[name] = Field
54
- return Field
55
- # Lazy load operations
56
- elif name == "Operation":
57
- from .operations.node import Operation
58
-
59
- _lazy_imports[name] = Operation
60
- return Operation
61
- elif name == "iModel":
62
- from .service.imodel import iModel
63
-
64
- _lazy_imports[name] = iModel
65
- return iModel
66
- elif name == "types":
67
- from . import _types as types
68
-
69
- _lazy_imports["types"] = types
70
- return types
71
- elif name == "Builder":
72
- from .operations.builder import OperationGraphBuilder as Builder
73
-
74
- _lazy_imports["Builder"] = Builder
75
- return Builder
76
- elif name == "load_mcp_tools":
77
- from .protocols.action.manager import load_mcp_tools
78
-
79
- _lazy_imports["load_mcp_tools"] = load_mcp_tools
80
- return load_mcp_tools
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
- "Field",
93
- "logger",
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
- "load_mcp_tools",
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,9 @@
1
+ """Spec adapters for converting Spec objects to framework-specific field definitions."""
2
+
3
+ from ._protocol import SpecAdapter
4
+ from .pydantic_field import PydanticSpecAdapter
5
+
6
+ __all__ = (
7
+ "SpecAdapter",
8
+ "PydanticSpecAdapter",
9
+ )
@@ -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
- _none_as_sentinel: ClassVar[bool] = True
265
+ _config: ClassVar[ModelConfig] = ModelConfig(none_as_sentinel=True)
266
266
  _func: ClassVar[Any] = alcall
267
267
 
268
268
  # input processing
@@ -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
- _none_as_sentinel: ClassVar[bool] = False
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
+ )