lionagi 0.18.0__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.
Files changed (93) hide show
  1. lionagi/__init__.py +102 -59
  2. lionagi/_errors.py +0 -5
  3. lionagi/adapters/spec_adapters/__init__.py +9 -0
  4. lionagi/adapters/spec_adapters/_protocol.py +236 -0
  5. lionagi/adapters/spec_adapters/pydantic_field.py +158 -0
  6. lionagi/fields.py +83 -0
  7. lionagi/ln/__init__.py +3 -1
  8. lionagi/ln/_async_call.py +2 -2
  9. lionagi/ln/concurrency/primitives.py +4 -4
  10. lionagi/ln/concurrency/task.py +1 -0
  11. lionagi/ln/fuzzy/_fuzzy_match.py +2 -2
  12. lionagi/ln/types/__init__.py +51 -0
  13. lionagi/ln/types/_sentinel.py +154 -0
  14. lionagi/ln/{types.py → types/base.py} +108 -168
  15. lionagi/ln/types/operable.py +221 -0
  16. lionagi/ln/types/spec.py +441 -0
  17. lionagi/models/field_model.py +69 -7
  18. lionagi/models/hashable_model.py +2 -3
  19. lionagi/models/model_params.py +4 -3
  20. lionagi/operations/ReAct/ReAct.py +1 -1
  21. lionagi/operations/act/act.py +3 -3
  22. lionagi/operations/builder.py +5 -7
  23. lionagi/operations/fields.py +380 -0
  24. lionagi/operations/flow.py +4 -6
  25. lionagi/operations/node.py +4 -4
  26. lionagi/operations/operate/operate.py +123 -89
  27. lionagi/operations/operate/operative.py +198 -0
  28. lionagi/operations/operate/step.py +203 -0
  29. lionagi/operations/select/select.py +1 -1
  30. lionagi/operations/select/utils.py +7 -1
  31. lionagi/operations/types.py +7 -7
  32. lionagi/protocols/action/manager.py +5 -6
  33. lionagi/protocols/contracts.py +2 -2
  34. lionagi/protocols/generic/__init__.py +22 -0
  35. lionagi/protocols/generic/element.py +36 -127
  36. lionagi/protocols/generic/pile.py +9 -10
  37. lionagi/protocols/generic/progression.py +23 -22
  38. lionagi/protocols/graph/edge.py +6 -5
  39. lionagi/protocols/ids.py +6 -49
  40. lionagi/protocols/messages/__init__.py +3 -1
  41. lionagi/protocols/messages/base.py +7 -6
  42. lionagi/protocols/messages/instruction.py +0 -1
  43. lionagi/protocols/messages/message.py +2 -2
  44. lionagi/protocols/types.py +1 -11
  45. lionagi/service/connections/__init__.py +3 -0
  46. lionagi/service/connections/providers/claude_code_cli.py +3 -2
  47. lionagi/service/hooks/_types.py +1 -1
  48. lionagi/service/hooks/_utils.py +1 -1
  49. lionagi/service/hooks/hook_event.py +3 -8
  50. lionagi/service/hooks/hook_registry.py +5 -5
  51. lionagi/service/hooks/hooked_event.py +61 -1
  52. lionagi/service/imodel.py +24 -20
  53. lionagi/service/third_party/claude_code.py +1 -2
  54. lionagi/service/third_party/openai_models.py +24 -22
  55. lionagi/service/token_calculator.py +1 -94
  56. lionagi/session/branch.py +26 -228
  57. lionagi/session/session.py +5 -90
  58. lionagi/version.py +1 -1
  59. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/METADATA +6 -5
  60. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/RECORD +62 -82
  61. lionagi/fields/__init__.py +0 -47
  62. lionagi/fields/action.py +0 -188
  63. lionagi/fields/base.py +0 -153
  64. lionagi/fields/code.py +0 -239
  65. lionagi/fields/file.py +0 -234
  66. lionagi/fields/instruct.py +0 -135
  67. lionagi/fields/reason.py +0 -55
  68. lionagi/fields/research.py +0 -52
  69. lionagi/operations/brainstorm/__init__.py +0 -2
  70. lionagi/operations/brainstorm/brainstorm.py +0 -498
  71. lionagi/operations/brainstorm/prompt.py +0 -11
  72. lionagi/operations/instruct/__init__.py +0 -2
  73. lionagi/operations/instruct/instruct.py +0 -28
  74. lionagi/operations/plan/__init__.py +0 -6
  75. lionagi/operations/plan/plan.py +0 -386
  76. lionagi/operations/plan/prompt.py +0 -25
  77. lionagi/operations/utils.py +0 -45
  78. lionagi/protocols/forms/__init__.py +0 -2
  79. lionagi/protocols/forms/base.py +0 -85
  80. lionagi/protocols/forms/flow.py +0 -79
  81. lionagi/protocols/forms/form.py +0 -86
  82. lionagi/protocols/forms/report.py +0 -48
  83. lionagi/protocols/mail/__init__.py +0 -2
  84. lionagi/protocols/mail/exchange.py +0 -220
  85. lionagi/protocols/mail/mail.py +0 -51
  86. lionagi/protocols/mail/mailbox.py +0 -103
  87. lionagi/protocols/mail/manager.py +0 -218
  88. lionagi/protocols/mail/package.py +0 -101
  89. lionagi/protocols/operatives/__init__.py +0 -2
  90. lionagi/protocols/operatives/operative.py +0 -362
  91. lionagi/protocols/operatives/step.py +0 -227
  92. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/WHEEL +0 -0
  93. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,441 @@
1
+ """Spec - Universal type specification for framework-agnostic field definitions.
2
+
3
+ This module provides the Spec class for defining field specifications that can be
4
+ adapted to any framework (Pydantic, attrs, dataclasses, etc.) via adapters.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import inspect
11
+ import os
12
+ import threading
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ from typing import Any, Callable
16
+
17
+ from typing_extensions import Annotated, OrderedDict
18
+
19
+ from lionagi.ln.concurrency.utils import is_coro_func
20
+
21
+ from ._sentinel import MaybeUndefined, Undefined, is_sentinel, not_sentinel
22
+ from .base import Meta
23
+
24
+ # Global cache for annotated types with bounded size
25
+ _MAX_CACHE_SIZE = int(os.environ.get("LIONAGI_FIELD_CACHE_SIZE", "10000"))
26
+ _annotated_cache: OrderedDict[tuple[type, tuple[Meta, ...]], type] = (
27
+ OrderedDict()
28
+ )
29
+ _cache_lock = threading.RLock() # Thread-safe access to cache
30
+
31
+
32
+ __all__ = ("Spec", "CommonMeta")
33
+
34
+
35
+ class CommonMeta(Enum):
36
+ """Common metadata keys used across field specifications."""
37
+
38
+ NAME = "name"
39
+ NULLABLE = "nullable"
40
+ LISTABLE = "listable"
41
+ VALIDATOR = "validator"
42
+ DEFAULT = "default"
43
+ DEFAULT_FACTORY = "default_factory"
44
+
45
+ @classmethod
46
+ def allowed(cls) -> set[str]:
47
+ """Return all allowed common metadata keys."""
48
+ return {i.value for i in cls}
49
+
50
+ @classmethod
51
+ def _validate_common_metas(cls, **kw):
52
+ """Validate common metadata constraints."""
53
+ if kw.get("default") and kw.get("default_factory"):
54
+ raise ValueError(
55
+ "Cannot provide both 'default' and 'default_factory'"
56
+ )
57
+ if _df := kw.get("default_factory"):
58
+ if not callable(_df):
59
+ raise ValueError("'default_factory' must be callable")
60
+ if _val := kw.get("validator"):
61
+ _val = [_val] if not isinstance(_val, list) else _val
62
+ if not all(callable(v) for v in _val):
63
+ raise ValueError(
64
+ "Validators must be a list of functions or a function"
65
+ )
66
+
67
+ @classmethod
68
+ def prepare(
69
+ cls, *args: Meta, metadata: tuple[Meta, ...] = None, **kw: Any
70
+ ) -> tuple[Meta, ...]:
71
+ """Prepare metadata tuple from various inputs, checking for duplicates.
72
+
73
+ Args:
74
+ *args: Individual Meta objects
75
+ metadata: Existing metadata tuple
76
+ **kw: Keyword arguments to convert to Meta objects
77
+
78
+ Returns:
79
+ Tuple of Meta objects
80
+
81
+ Raises:
82
+ ValueError: If duplicate keys are found
83
+ """
84
+ # Lazy import to avoid circular dependency
85
+ from .._to_list import to_list
86
+
87
+ seen_keys = set()
88
+ metas = []
89
+
90
+ # Process existing metadata
91
+ if metadata:
92
+ for meta in metadata:
93
+ if meta.key in seen_keys:
94
+ raise ValueError(f"Duplicate metadata key: {meta.key}")
95
+ seen_keys.add(meta.key)
96
+ metas.append(meta)
97
+
98
+ # Process args
99
+ if args:
100
+ _args = to_list(
101
+ args, flatten=True, flatten_tuple_set=True, dropna=True
102
+ )
103
+ for meta in _args:
104
+ if meta.key in seen_keys:
105
+ raise ValueError(f"Duplicate metadata key: {meta.key}")
106
+ seen_keys.add(meta.key)
107
+ metas.append(meta)
108
+
109
+ # Process kwargs
110
+ for k, v in kw.items():
111
+ if k in seen_keys:
112
+ raise ValueError(f"Duplicate metadata key: {k}")
113
+ seen_keys.add(k)
114
+ metas.append(Meta(k, v))
115
+
116
+ # Validate common metadata constraints
117
+ meta_dict = {m.key: m.value for m in metas}
118
+ cls._validate_common_metas(**meta_dict)
119
+
120
+ return tuple(metas)
121
+
122
+
123
+ @dataclass(frozen=True, slots=True, init=False)
124
+ class Spec:
125
+ """Framework-agnostic field specification.
126
+
127
+ A Spec defines the type and metadata for a field without coupling to any
128
+ specific framework. Use adapters to convert Spec to framework-specific
129
+ field definitions (e.g., Pydantic Field, attrs attribute).
130
+
131
+ Attributes:
132
+ base_type: The base Python type for this field
133
+ metadata: Tuple of metadata objects attached to this spec
134
+
135
+ Example:
136
+ >>> spec = Spec(str, name="username", nullable=False)
137
+ >>> spec.name
138
+ 'username'
139
+ >>> spec.annotation
140
+ str
141
+ """
142
+
143
+ base_type: type
144
+ metadata: tuple[Meta, ...]
145
+
146
+ def __init__(
147
+ self,
148
+ base_type: type = None,
149
+ *args,
150
+ metadata: tuple[Meta, ...] = None,
151
+ **kw,
152
+ ) -> None:
153
+ """Initialize Spec with type and metadata.
154
+
155
+ Args:
156
+ base_type: Base Python type
157
+ *args: Additional Meta objects
158
+ metadata: Existing metadata tuple
159
+ **kw: Keyword arguments converted to Meta objects
160
+ """
161
+ metas = CommonMeta.prepare(*args, metadata=metadata, **kw)
162
+
163
+ if not_sentinel(base_type, True):
164
+ import types
165
+
166
+ is_valid_type = (
167
+ isinstance(base_type, type)
168
+ or hasattr(base_type, "__origin__")
169
+ or isinstance(base_type, types.UnionType)
170
+ or str(type(base_type)) == "<class 'types.UnionType'>"
171
+ )
172
+ if not is_valid_type:
173
+ raise ValueError(
174
+ f"base_type must be a type or type annotation, got {base_type}"
175
+ )
176
+
177
+ # Check for async default factory and warn
178
+ if kw.get("default_factory") and is_coro_func(kw["default_factory"]):
179
+ import warnings
180
+
181
+ warnings.warn(
182
+ "Async default factories are not yet fully supported by all adapters. "
183
+ "Consider using sync factories for compatibility.",
184
+ UserWarning,
185
+ stacklevel=2,
186
+ )
187
+
188
+ object.__setattr__(self, "base_type", base_type)
189
+ object.__setattr__(self, "metadata", metas)
190
+
191
+ def __getitem__(self, key: str) -> Any:
192
+ """Get metadata value by key.
193
+
194
+ Args:
195
+ key: Metadata key
196
+
197
+ Returns:
198
+ Metadata value
199
+
200
+ Raises:
201
+ KeyError: If key not found
202
+ """
203
+ for meta in self.metadata:
204
+ if meta.key == key:
205
+ return meta.value
206
+ raise KeyError(f"Metadata key '{key}' undefined in Spec.")
207
+
208
+ def get(self, key: str, default: Any = Undefined) -> Any:
209
+ """Get metadata value by key with default.
210
+
211
+ Args:
212
+ key: Metadata key
213
+ default: Default value if key not found
214
+
215
+ Returns:
216
+ Metadata value or default
217
+ """
218
+ with contextlib.suppress(KeyError):
219
+ return self[key]
220
+ return default
221
+
222
+ @property
223
+ def name(self) -> MaybeUndefined[str]:
224
+ """Get the field name from metadata."""
225
+ return self.get(CommonMeta.NAME.value)
226
+
227
+ @property
228
+ def is_nullable(self) -> bool:
229
+ """Check if field is nullable."""
230
+ return self.get(CommonMeta.NULLABLE.value) is True
231
+
232
+ @property
233
+ def is_listable(self) -> bool:
234
+ """Check if field is listable."""
235
+ return self.get(CommonMeta.LISTABLE.value) is True
236
+
237
+ @property
238
+ def default(self) -> MaybeUndefined[Any]:
239
+ """Get default value or factory."""
240
+ return self.get(
241
+ CommonMeta.DEFAULT.value,
242
+ self.get(CommonMeta.DEFAULT_FACTORY.value),
243
+ )
244
+
245
+ @property
246
+ def has_default_factory(self) -> bool:
247
+ """Check if this spec has a default factory."""
248
+ return _is_factory(self.get(CommonMeta.DEFAULT_FACTORY.value))[0]
249
+
250
+ @property
251
+ def has_async_default_factory(self) -> bool:
252
+ """Check if this spec has an async default factory."""
253
+ return _is_factory(self.get(CommonMeta.DEFAULT_FACTORY.value))[1]
254
+
255
+ def create_default_value(self) -> Any:
256
+ """Create default value synchronously.
257
+
258
+ Returns:
259
+ Default value
260
+
261
+ Raises:
262
+ ValueError: If no default or factory defined, or if factory is async
263
+ """
264
+ if self.default is Undefined:
265
+ raise ValueError("No default value or factory defined in Spec.")
266
+ if self.has_async_default_factory:
267
+ raise ValueError(
268
+ "Default factory is asynchronous; cannot create default synchronously. "
269
+ "Use 'await spec.acreate_default_value()' instead."
270
+ )
271
+ if self.has_default_factory:
272
+ return self.default()
273
+ return self.default
274
+
275
+ async def acreate_default_value(self) -> Any:
276
+ """Create default value asynchronously.
277
+
278
+ Returns:
279
+ Default value
280
+ """
281
+ if self.has_async_default_factory:
282
+ return await self.default()
283
+ return self.create_default_value()
284
+
285
+ def with_updates(self, **kw):
286
+ """Create new Spec with updated metadata.
287
+
288
+ Args:
289
+ **kw: Metadata updates
290
+
291
+ Returns:
292
+ New Spec instance with updates
293
+ """
294
+ _filtered = [meta for meta in self.metadata if meta.key not in kw]
295
+ for k, v in kw.items():
296
+ if not_sentinel(v):
297
+ _filtered.append(Meta(k, v))
298
+ _metas = tuple(_filtered)
299
+ return type(self)(self.base_type, metadata=_metas)
300
+
301
+ def as_nullable(self) -> Spec:
302
+ """Create nullable version of this spec."""
303
+ return self.with_updates(nullable=True)
304
+
305
+ def as_listable(self) -> Spec:
306
+ """Create listable version of this spec."""
307
+ return self.with_updates(listable=True)
308
+
309
+ def with_default(self, default: Any) -> Spec:
310
+ """Create spec with default value or factory.
311
+
312
+ Args:
313
+ default: Default value or factory function
314
+
315
+ Returns:
316
+ New Spec with default
317
+ """
318
+ if callable(default):
319
+ return self.with_updates(default_factory=default)
320
+ return self.with_updates(default=default)
321
+
322
+ def with_validator(
323
+ self, validator: Callable[..., Any] | list[Callable[..., Any]]
324
+ ) -> Spec:
325
+ """Create spec with validator(s).
326
+
327
+ Args:
328
+ validator: Single validator or list of validators
329
+
330
+ Returns:
331
+ New Spec with validator(s)
332
+ """
333
+ return self.with_updates(validator=validator)
334
+
335
+ @property
336
+ def annotation(self) -> type[Any]:
337
+ """Plain type annotation representing base type, nullable, and listable.
338
+
339
+ Returns:
340
+ Type annotation
341
+ """
342
+ if is_sentinel(self.base_type, none_as_sentinel=True):
343
+ return Any
344
+ t_ = self.base_type
345
+ if self.is_listable:
346
+ t_ = list[t_]
347
+ if self.is_nullable:
348
+ return t_ | None
349
+ return t_
350
+
351
+ def annotated(self) -> type[Any]:
352
+ """Materialize this spec into an Annotated type.
353
+
354
+ This method is cached to ensure repeated calls return the same
355
+ type object for performance and identity checks. The cache is bounded
356
+ using LRU eviction to prevent unbounded memory growth.
357
+
358
+ Returns:
359
+ Annotated type with all metadata attached
360
+ """
361
+ # Check cache first with thread safety
362
+ cache_key = (self.base_type, self.metadata)
363
+
364
+ with _cache_lock:
365
+ if cache_key in _annotated_cache:
366
+ # Move to end to mark as recently used
367
+ _annotated_cache.move_to_end(cache_key)
368
+ return _annotated_cache[cache_key]
369
+
370
+ # Handle nullable case - wrap in Optional-like union
371
+ actual_type = (
372
+ Any
373
+ if is_sentinel(self.base_type, none_as_sentinel=True)
374
+ else self.base_type
375
+ )
376
+ current_metadata = (
377
+ ()
378
+ if is_sentinel(self.metadata, none_as_sentinel=True)
379
+ else self.metadata
380
+ )
381
+
382
+ if any(m.key == "nullable" and m.value for m in current_metadata):
383
+ # Use union syntax for nullable
384
+ actual_type = actual_type | None # type: ignore
385
+
386
+ if current_metadata:
387
+ args = [actual_type] + list(current_metadata)
388
+ result = Annotated.__class_getitem__(tuple(args)) # type: ignore
389
+ else:
390
+ result = actual_type # type: ignore[misc]
391
+
392
+ # Cache the result with LRU eviction
393
+ _annotated_cache[cache_key] = result # type: ignore[assignment]
394
+
395
+ # Evict oldest if cache is too large (guard against empty cache)
396
+ while len(_annotated_cache) > _MAX_CACHE_SIZE:
397
+ try:
398
+ _annotated_cache.popitem(last=False) # Remove oldest
399
+ except KeyError:
400
+ # Cache became empty during race, safe to continue
401
+ break
402
+
403
+ return result # type: ignore[return-value]
404
+
405
+ def metadict(
406
+ self, exclude: set[str] | None = None, exclude_common: bool = False
407
+ ) -> dict[str, Any]:
408
+ """Get metadata as dictionary.
409
+
410
+ Args:
411
+ exclude: Keys to exclude
412
+ exclude_common: Exclude all common metadata keys
413
+
414
+ Returns:
415
+ Dictionary of metadata
416
+ """
417
+ if exclude is None:
418
+ exclude = set()
419
+ if exclude_common:
420
+ exclude = exclude | CommonMeta.allowed()
421
+ return {
422
+ meta.key: meta.value
423
+ for meta in self.metadata
424
+ if meta.key not in exclude
425
+ }
426
+
427
+
428
+ def _is_factory(obj: Any) -> tuple[bool, bool]:
429
+ """Check if object is a factory function.
430
+
431
+ Args:
432
+ obj: Object to check
433
+
434
+ Returns:
435
+ Tuple of (is_factory, is_async)
436
+ """
437
+ if not callable(obj):
438
+ return (False, False)
439
+ if is_coro_func(obj):
440
+ return (True, True)
441
+ return (True, False)
@@ -16,7 +16,7 @@ from typing import Annotated, Any, ClassVar
16
16
  from typing_extensions import Self, override
17
17
 
18
18
  from .._errors import ValidationError
19
- from ..ln.types import Meta, Params
19
+ from ..ln.types import Meta, ModelConfig, Params, Spec
20
20
 
21
21
  # Cache of valid Pydantic Field parameters
22
22
  _PYDANTIC_FIELD_PARAMS: set[str] | None = None
@@ -77,14 +77,15 @@ class FieldModel(Params):
77
77
  """
78
78
 
79
79
  # Class configuration - let Params handle Unset population
80
- _prefill_unset: ClassVar[bool] = True
81
- _none_as_sentinel: ClassVar[bool] = True
80
+ _config: ClassVar[ModelConfig] = ModelConfig(
81
+ prefill_unset=True, none_as_sentinel=True
82
+ )
82
83
 
83
84
  # Public fields (all start as Unset when not provided)
84
85
  base_type: type[Any]
85
86
  metadata: tuple[Meta, ...]
86
87
 
87
- def __init__(self, **kwargs: Any) -> None:
88
+ def __init__(self, base_type: type[Any] = None, **kwargs: Any) -> None:
88
89
  """Initialize FieldModel with legacy compatibility.
89
90
 
90
91
  Handles backward compatibility by converting old-style kwargs to the new
@@ -94,6 +95,8 @@ class FieldModel(Params):
94
95
  **kwargs: Arbitrary keyword arguments, including legacy ones
95
96
  """
96
97
  # Convert legacy kwargs to proper format
98
+ if base_type is not None:
99
+ kwargs["base_type"] = base_type
97
100
  converted = self._convert_kwargs_to_params(**kwargs)
98
101
 
99
102
  # Set fields directly and validate
@@ -538,7 +541,7 @@ class FieldModel(Params):
538
541
  field_info = PydanticField(**field_kwargs)
539
542
 
540
543
  # Set the annotation from base_type for backward compatibility
541
- field_info.annotation = self.base_type
544
+ field_info.annotation = self.annotation
542
545
 
543
546
  return field_info
544
547
 
@@ -745,8 +748,14 @@ class FieldModel(Params):
745
748
 
746
749
  @property
747
750
  def annotation(self) -> type[Any]:
748
- """Get field annotation (base_type) for backward compatibility."""
749
- return Any if self._is_sentinel(self.base_type) else self.base_type
751
+ if self._is_sentinel(self.base_type):
752
+ return Any
753
+ t_ = self.base_type
754
+ if self.is_listable:
755
+ t_ = list[t_]
756
+ if self.is_nullable:
757
+ t_ = t_ | None
758
+ return t_
750
759
 
751
760
  def to_dict(self) -> dict[str, Any]:
752
761
  """Convert field model to dictionary for backward compatibility.
@@ -774,6 +783,59 @@ class FieldModel(Params):
774
783
  ]
775
784
  )
776
785
 
786
+ def to_spec(self) -> "Spec":
787
+ """Convert FieldModel to Spec.
788
+
789
+ Returns:
790
+ Spec object with equivalent configuration
791
+ """
792
+ from ..ln.types import Spec
793
+
794
+ # Build kwargs for Spec constructor
795
+ kwargs = {}
796
+
797
+ # Extract name from metadata
798
+ name = self.extract_metadata("name")
799
+ if name:
800
+ kwargs["name"] = name
801
+
802
+ # Add nullable/listable flags using properties
803
+ kwargs["nullable"] = self.is_nullable
804
+ kwargs["listable"] = self.is_listable
805
+
806
+ # Extract default/default_factory
807
+ default = self.extract_metadata("default")
808
+ if default is not None:
809
+ kwargs["default"] = default
810
+
811
+ default_factory = self.extract_metadata("default_factory")
812
+ if default_factory is not None:
813
+ kwargs["default_factory"] = default_factory
814
+
815
+ # Extract validator
816
+ validator = self.extract_metadata("validator")
817
+ if validator is not None:
818
+ kwargs["validator"] = validator
819
+
820
+ # Extract description
821
+ description = self.extract_metadata("description")
822
+ if description:
823
+ kwargs["description"] = description
824
+
825
+ # Extract other common metadata
826
+ for key in ["title", "alias", "frozen", "exclude"]:
827
+ val = self.extract_metadata(key)
828
+ if val is not None:
829
+ kwargs[key] = val
830
+
831
+ # Extract json_schema_extra
832
+ json_schema_extra = self.extract_metadata("json_schema_extra")
833
+ if json_schema_extra:
834
+ for k, v in json_schema_extra.items():
835
+ kwargs[k] = v
836
+
837
+ return Spec(self.base_type, **kwargs)
838
+
777
839
  def metadata_dict(
778
840
  self, exclude: list[str] | None = None
779
841
  ) -> dict[str, Any]:
@@ -94,12 +94,11 @@ class HashableModel(BaseModel):
94
94
  def _get_default_hashable_serializer():
95
95
  global _DEFAULT_HASHABLE_SERIALIZER
96
96
  if _DEFAULT_HASHABLE_SERIALIZER is None:
97
- from lionagi.protocols.ids import Element, IDType
97
+ from lionagi.protocols.ids import Element
98
98
 
99
99
  _DEFAULT_HASHABLE_SERIALIZER = ln.get_orjson_default(
100
- order=[IDType, Element, BaseModel],
100
+ order=[Element, BaseModel],
101
101
  additional={
102
- IDType: lambda o: str(o),
103
102
  Element: lambda o: o.to_dict(),
104
103
  BaseModel: lambda o: o.model_dump(mode="json"),
105
104
  },
@@ -21,7 +21,7 @@ from typing import Any, ClassVar
21
21
  from pydantic import BaseModel, create_model
22
22
  from pydantic.fields import FieldInfo
23
23
 
24
- from lionagi.ln.types import Params
24
+ from lionagi.ln.types import ModelConfig, Params
25
25
  from lionagi.utils import copy
26
26
 
27
27
  from .field_model import FieldModel
@@ -83,8 +83,9 @@ class ModelParams(Params):
83
83
  """
84
84
 
85
85
  # Class configuration - let Params handle Unset population
86
- _prefill_unset: ClassVar[bool] = True
87
- _none_as_sentinel: ClassVar[bool] = True
86
+ _config: ClassVar[ModelConfig] = ModelConfig(
87
+ prefill_unset=True, none_as_sentinel=True
88
+ )
88
89
 
89
90
  # Public fields (all start as Unset when not provided)
90
91
  name: str | None
@@ -9,7 +9,6 @@ from pydantic import BaseModel
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
- from lionagi.fields.instruct import Instruct
13
12
  from lionagi.libs.schema.as_readable import as_readable
14
13
  from lionagi.libs.validate.common_field_validators import (
15
14
  validate_model_to_type,
@@ -18,6 +17,7 @@ from lionagi.ln.fuzzy import FuzzyMatchKeysParams
18
17
  from lionagi.models.field_model import FieldModel
19
18
  from lionagi.service.imodel import iModel
20
19
 
20
+ from ..fields import Instruct
21
21
  from ..types import (
22
22
  ActionParam,
23
23
  ChatParam,
@@ -6,10 +6,10 @@ from typing import TYPE_CHECKING, Literal
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
- from lionagi.fields.action import ActionResponseModel
10
- from lionagi.ln._async_call import AlcallParams
11
- from lionagi.protocols.types import ActionRequest, ActionResponse
9
+ from lionagi.ln import AlcallParams
10
+ from lionagi.protocols.messages import ActionRequest, ActionResponse
12
11
 
12
+ from ..fields import ActionResponseModel
13
13
  from ..types import ActionParam
14
14
 
15
15
  if TYPE_CHECKING:
@@ -46,9 +46,9 @@ class OperationGraphBuilder:
46
46
  >>> result = await session.flow(graph)
47
47
  >>>
48
48
  >>> # Expand based on results
49
- >>> if hasattr(result, 'instruct_models'):
49
+ >>> if hasattr(result, 'instruct_model'):
50
50
  ... builder.expand_from_result(
51
- ... result.instruct_models,
51
+ ... result.instruct_model,
52
52
  ... source_node_id=builder.last_operation_id,
53
53
  ... operation="instruct"
54
54
  ... )
@@ -150,7 +150,7 @@ class OperationGraphBuilder:
150
150
  based on results.
151
151
 
152
152
  Args:
153
- items: Items from result to expand (e.g., instruct_models)
153
+ items: Items from result to expand (e.g., instruct_model)
154
154
  source_node_id: ID of node that produced these items
155
155
  operation: Operation to apply to each item
156
156
  strategy: How to organize the expanded operations
@@ -253,11 +253,9 @@ class OperationGraphBuilder:
253
253
  if not sources:
254
254
  raise ValueError("No source nodes for aggregation")
255
255
 
256
- # Add aggregation metadata - convert IDType to strings for JSON serialization
256
+ # Add aggregation metadata - convert UUID to strings for JSON serialization
257
257
  agg_params = {
258
- "aggregation_sources": [
259
- str(s) for s in sources
260
- ], # Convert IDType to strings
258
+ "aggregation_sources": [str(s) for s in sources],
261
259
  "aggregation_count": len(sources),
262
260
  **parameters,
263
261
  }