lionagi 0.1.1__py3-none-any.whl → 0.1.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 (44) hide show
  1. lionagi/core/execute/structure_executor.py +21 -1
  2. lionagi/core/flow/monoflow/ReAct.py +3 -1
  3. lionagi/core/flow/monoflow/followup.py +3 -1
  4. lionagi/core/generic/component.py +197 -120
  5. lionagi/core/generic/condition.py +2 -0
  6. lionagi/core/generic/edge.py +33 -33
  7. lionagi/core/graph/graph.py +1 -1
  8. lionagi/core/tool/tool_manager.py +10 -9
  9. lionagi/experimental/report/form.py +64 -0
  10. lionagi/experimental/report/report.py +138 -0
  11. lionagi/experimental/report/util.py +47 -0
  12. lionagi/experimental/tool/schema.py +3 -3
  13. lionagi/experimental/tool/tool_manager.py +1 -1
  14. lionagi/experimental/validator/rule.py +139 -0
  15. lionagi/experimental/validator/validator.py +56 -0
  16. lionagi/experimental/work/__init__.py +10 -0
  17. lionagi/experimental/work/async_queue.py +54 -0
  18. lionagi/experimental/work/schema.py +60 -17
  19. lionagi/experimental/work/work_function.py +55 -77
  20. lionagi/experimental/work/worker.py +56 -12
  21. lionagi/experimental/work2/__init__.py +0 -0
  22. lionagi/experimental/work2/form.py +371 -0
  23. lionagi/experimental/work2/report.py +289 -0
  24. lionagi/experimental/work2/schema.py +30 -0
  25. lionagi/experimental/{work → work2}/tests.py +1 -1
  26. lionagi/experimental/work2/util.py +0 -0
  27. lionagi/experimental/work2/work.py +0 -0
  28. lionagi/experimental/work2/work_function.py +89 -0
  29. lionagi/experimental/work2/worker.py +12 -0
  30. lionagi/integrations/storage/storage_util.py +4 -4
  31. lionagi/integrations/storage/structure_excel.py +268 -0
  32. lionagi/integrations/storage/to_excel.py +18 -9
  33. lionagi/libs/__init__.py +4 -0
  34. lionagi/tests/test_core/generic/__init__.py +0 -0
  35. lionagi/tests/test_core/generic/test_component.py +89 -0
  36. lionagi/version.py +1 -1
  37. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/METADATA +1 -1
  38. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/RECORD +43 -27
  39. lionagi/experimental/work/_logger.py +0 -25
  40. /lionagi/experimental/{work/exchange.py → report/__init__.py} +0 -0
  41. /lionagi/experimental/{work/util.py → validator/__init__.py} +0 -0
  42. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/LICENSE +0 -0
  43. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/WHEEL +0 -0
  44. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/top_level.txt +0 -0
@@ -288,7 +288,7 @@ class StructureExecutor(BaseExecutor, Graph):
288
288
  while self.pending_ins[key]:
289
289
  mail: BaseMail = self.pending_ins[key].popleft()
290
290
  try:
291
- if mail == "end":
291
+ if mail.category == "end":
292
292
  self.execute_stop = True
293
293
  return
294
294
  next_nodes = await self._handle_mail(mail)
@@ -312,3 +312,23 @@ class StructureExecutor(BaseExecutor, Graph):
312
312
  while not self.execute_stop:
313
313
  await self.forward()
314
314
  await AsyncUtil.sleep(refresh_time)
315
+
316
+ def to_excel(self, structure_name, dir="structure_storage"):
317
+ """
318
+ Exports the current structure to an Excel file using a specified structure name and directory.
319
+
320
+ This method utilizes the `to_excel` function from the `lionagi.integrations.storage.to_excel` module,
321
+ saving the current structure instance into an Excel file format. The Excel file will contain details
322
+ about nodes, edges, and other relevant data as separate sheets within the file.
323
+
324
+ Args:
325
+ structure_name (str): The name to assign to the structure within the Excel file. This name is
326
+ used as part of the file naming convention.
327
+ dir (str, optional): The directory where the Excel file will be saved. Defaults to "structure_storage".
328
+
329
+ Raises:
330
+ Exception: Propagates any exceptions raised by the `to_excel` function, which might occur during
331
+ the file writing process or data formatting.
332
+ """
333
+ from lionagi.integrations.storage.to_excel import to_excel
334
+ to_excel(self, structure_name, dir)
@@ -145,7 +145,9 @@ class MonoReAct(MonoChat):
145
145
  if not self.branch.tool_manager.has_tools:
146
146
  raise ValueError("No tools found. You need to register tools.")
147
147
 
148
- config = self.branch.tool_manager.parse_tool(tools=True, **kwargs)
148
+ if tools is None:
149
+ tools = True
150
+ config = self.branch.tool_manager.parse_tool(tools=tools, **kwargs)
149
151
  config["tool_parsed"] = True
150
152
  config["tool_choice"] = tool_choice
151
153
  return config
@@ -137,7 +137,9 @@ class MonoFollowup(MonoChat):
137
137
  if not self.branch.tool_manager.has_tools:
138
138
  raise ValueError("No tools found. You need to register tools.")
139
139
 
140
- config = self.branch.tool_manager.parse_tool(tools=True, **kwargs)
140
+ if tools is None:
141
+ tools = True
142
+ config = self.branch.tool_manager.parse_tool(tools=tools, **kwargs)
141
143
  config["tool_parsed"] = True
142
144
  config["tool_choice"] = tool_choice
143
145
  return config
@@ -1,13 +1,12 @@
1
- """This module provides base components used in lionagi"""
1
+ """base components in lionagi"""
2
2
 
3
3
  from abc import ABC
4
4
  from functools import singledispatchmethod
5
- from typing import Any, TypeVar
6
-
5
+ from typing import Any, TypeVar, Type
7
6
  from pydantic import AliasChoices, BaseModel, Field, ValidationError
8
7
  from pandas import DataFrame, Series
9
8
 
10
- from lionagi.libs import SysUtil, func_call, convert, ParseUtil, nested
9
+ from lionagi.libs import SysUtil, convert, ParseUtil, nested, func_call
11
10
 
12
11
 
13
12
  T = TypeVar("T")
@@ -28,24 +27,38 @@ class BaseComponent(BaseModel, ABC):
28
27
  validation_alias=AliasChoices("node_id", "ID", "id"),
29
28
  description="A 32-char unique hash identifier for the node.",
30
29
  )
31
-
32
30
  timestamp: str = Field(
33
31
  default_factory=lambda: SysUtil.get_timestamp(sep=None),
34
32
  description="The timestamp of when the node was created.",
35
33
  )
36
34
 
35
+ extra_fields: dict[str, Any] = Field(
36
+ default_factory=dict,
37
+ validation_alias=AliasChoices(
38
+ "extra", "additional_fields", "schema_extra", "extra_schema"
39
+ ),
40
+ description="Additional fields for the component.",
41
+ )
42
+
37
43
  class Config:
38
44
  """Model configuration settings."""
39
45
 
40
46
  extra = "allow"
41
47
  arbitrary_types_allowed = True
42
48
  populate_by_name = True
43
- validate_assignment = True
44
- validate_return = True
45
- str_strip_whitespace = True
49
+
50
+ @property
51
+ def class_name(self) -> str:
52
+ """
53
+ Retrieve the name of the class.
54
+
55
+ Returns:
56
+ str: The name of the class.
57
+ """
58
+ return self._class_name()
46
59
 
47
60
  @classmethod
48
- def class_name(cls) -> str:
61
+ def _class_name(cls) -> str:
49
62
  """
50
63
  Retrieve the name of the class.
51
64
 
@@ -54,6 +67,101 @@ class BaseComponent(BaseModel, ABC):
54
67
  """
55
68
  return cls.__name__
56
69
 
70
+ def to_json_str(self, *args, **kwargs) -> str:
71
+ """
72
+ Convert the component to a JSON string.
73
+
74
+ Returns:
75
+ str: The JSON string representation of the component.
76
+ """
77
+ dict_ = self.to_dict(*args, **kwargs)
78
+ return convert.to_str(dict_)
79
+
80
+ def to_dict(self, *args, **kwargs) -> dict[str, Any]:
81
+ """
82
+ Convert the component to a dictionary.
83
+
84
+ Returns:
85
+ dict[str, Any]: The dictionary representation of the component.
86
+ """
87
+ dict_ = self.model_dump(*args, by_alias=True, **kwargs)
88
+ for field_name in list(self.extra_fields.keys()):
89
+ if field_name not in dict_:
90
+ dict_[field_name] = getattr(self, field_name, None)
91
+ dict_.pop("extra_fields", None)
92
+ return dict_
93
+
94
+ def to_xml(self, *args, **kwargs) -> str:
95
+ """
96
+ Convert the component to an XML string.
97
+
98
+ Returns:
99
+ str: The XML string representation of the component.
100
+ """
101
+ import xml.etree.ElementTree as ET
102
+
103
+ root = ET.Element(self.__class__.__name__)
104
+
105
+ def convert(dict_obj: dict, parent: ET.Element) -> None:
106
+ for key, val in dict_obj.items():
107
+ if isinstance(val, dict):
108
+ element = ET.SubElement(parent, key)
109
+ convert(val, element)
110
+ else:
111
+ element = ET.SubElement(parent, key)
112
+ element.text = str(val)
113
+
114
+ convert(self.to_dict(*args, **kwargs), root)
115
+ return ET.tostring(root, encoding="unicode")
116
+
117
+ def to_pd_series(self, *args, pd_kwargs: dict | None = None, **kwargs) -> Series:
118
+ """
119
+ Convert the node to a Pandas Series.
120
+
121
+ Args:
122
+ pd_kwargs (dict | None): Additional keyword arguments for Pandas Series.
123
+
124
+ Returns:
125
+ Series: The Pandas Series representation of the node.
126
+ """
127
+ pd_kwargs = {} if pd_kwargs is None else pd_kwargs
128
+ dict_ = self.to_dict(*args, **kwargs)
129
+ return Series(dict_, **pd_kwargs)
130
+
131
+ def _add_field(
132
+ self,
133
+ field_name: str,
134
+ annotation: Any | Type | None = Any,
135
+ default: Any | None = None,
136
+ value: Any | None = None,
137
+ field: Any = None,
138
+ **kwargs,
139
+ ) -> None:
140
+ """
141
+ Add a field to the model after initialization.
142
+
143
+ Args:
144
+ field_name (str): The name of the field.
145
+ annotation (Any | Type | None): The type annotation for the field.
146
+ default (Any | None): The default value for the field.
147
+ value (Any | None): The initial value for the field.
148
+ field (Any): The Field object for the field.
149
+ **kwargs: Additional keyword arguments for the Field object.
150
+ """
151
+ field = field or Field(default=default, **kwargs)
152
+ self.extra_fields[field_name] = field
153
+ if annotation:
154
+ self.extra_fields[field_name].annotation = annotation
155
+
156
+ if not value and (a := self._get_field_attr(field_name, "default", None)):
157
+ value = a
158
+
159
+ self.__setattr__(field_name, value)
160
+
161
+ @property
162
+ def _all_fields(self):
163
+ return {**self.model_fields, **self.extra_fields}
164
+
57
165
  @property
58
166
  def _field_annotations(self) -> dict:
59
167
  """
@@ -62,21 +170,20 @@ class BaseComponent(BaseModel, ABC):
62
170
  Returns:
63
171
  dict: A dictionary mapping field names to their annotations.
64
172
  """
65
- return self._get_field_annotation(list(self.model_fields.keys()))
66
173
 
67
- def _get_field_attr(self, k: str, attr: str, default=False) -> Any:
174
+ return self._get_field_annotation(list(self._all_fields.keys()))
175
+
176
+ def _get_field_attr(self, k: str, attr: str, default: Any = False) -> Any:
68
177
  """
69
178
  Get the value of a field attribute.
70
179
 
71
180
  Args:
72
181
  k (str): The field name.
73
182
  attr (str): The attribute name.
74
- default (Any): Default value to return if the attribute is not
75
- found.
183
+ default (Any): Default value to return if the attribute is not found.
76
184
 
77
185
  Returns:
78
- Any: The value of the field attribute, or the default value if not
79
- found.
186
+ Any: The value of the field attribute, or the default value if not found.
80
187
 
81
188
  Raises:
82
189
  ValueError: If the field does not have the specified attribute.
@@ -84,8 +191,9 @@ class BaseComponent(BaseModel, ABC):
84
191
  try:
85
192
  if not self._field_has_attr(k, attr):
86
193
  raise ValueError(f"field {k} has no attribute {attr}")
87
- field = self.model_fields[k]
88
- a = getattr(field, attr)
194
+
195
+ field = self._all_fields[k]
196
+ a = getattr(field, attr, None)
89
197
  if not a:
90
198
  try:
91
199
  a = field.json_schema_extra[attr]
@@ -99,7 +207,7 @@ class BaseComponent(BaseModel, ABC):
99
207
  raise e
100
208
 
101
209
  @singledispatchmethod
102
- def _get_field_annotation(self, field_name: str) -> Any:
210
+ def _get_field_annotation(self, field_name: Any) -> Any:
103
211
  """
104
212
  Get the annotation for a field.
105
213
 
@@ -112,7 +220,7 @@ class BaseComponent(BaseModel, ABC):
112
220
  raise TypeError(f"Unsupported type {type(field_name)}")
113
221
 
114
222
  @_get_field_annotation.register(str)
115
- def _(self, field_name) -> dict[str, Any]:
223
+ def _(self, field_name: str) -> dict[str, Any]:
116
224
  """
117
225
  Get the annotation for a field as a dictionary.
118
226
 
@@ -120,10 +228,9 @@ class BaseComponent(BaseModel, ABC):
120
228
  field_name (str): The name of the field.
121
229
 
122
230
  Returns:
123
- dict[str, Any]: A dictionary mapping the field name to its
124
- annotation.
231
+ dict[str, Any]: A dictionary mapping the field name to its annotation.
125
232
  """
126
- dict_ = {field_name: self.model_fields[field_name].annotation}
233
+ dict_ = {field_name: self._all_fields[field_name].annotation}
127
234
  for k, v in dict_.items():
128
235
  if "|" in str(v):
129
236
  v = str(v)
@@ -134,37 +241,20 @@ class BaseComponent(BaseModel, ABC):
134
241
  return dict_
135
242
 
136
243
  @_get_field_annotation.register(list)
137
- def _(self, field_name) -> dict[str, Any]:
138
- """
139
- Get the annotations for multiple fields as a dictionary.
140
-
141
- Args:
142
- field_name (list): A list or tuple of field names.
143
-
144
- Returns:
145
- dict[str, Any]: A dictionary mapping field names to their
146
- annotations.
147
- """
148
- dict_ = {}
149
- for i in field_name:
150
- dict_.update(self._get_field_annotation(i))
151
- return dict_
152
-
153
244
  @_get_field_annotation.register(tuple)
154
- def _(self, field_name) -> dict[str, Any]:
245
+ def _(self, field_names: list | tuple) -> dict[str, Any]:
155
246
  """
156
247
  Get the annotations for multiple fields as a dictionary.
157
248
 
158
249
  Args:
159
- field_name (tuple): A list or tuple of field names.
250
+ field_names (list | tuple): A list or tuple of field names.
160
251
 
161
252
  Returns:
162
- dict[str, Any]: A dictionary mapping field names to their
163
- annotations.
253
+ dict[str, Any]: A dictionary mapping field names to their annotations.
164
254
  """
165
255
  dict_ = {}
166
- for i in field_name:
167
- dict_.update(self._get_field_annotation(i))
256
+ for field_name in field_names:
257
+ dict_.update(self._get_field_annotation(field_name))
168
258
  return dict_
169
259
 
170
260
  def _field_has_attr(self, k: str, attr: str) -> bool:
@@ -178,7 +268,7 @@ class BaseComponent(BaseModel, ABC):
178
268
  Returns:
179
269
  bool: True if the field has the attribute, False otherwise.
180
270
  """
181
- field = self.model_fields.get(k, None)
271
+ field = self._all_fields.get(k, None)
182
272
  if field is None:
183
273
  raise ValueError(f"Field {k} not found in model fields.")
184
274
 
@@ -186,65 +276,16 @@ class BaseComponent(BaseModel, ABC):
186
276
  if not a:
187
277
  try:
188
278
  a = (
189
- self.model_fields[k].json_schema_extra[attr] is not None
190
- and attr in self.model_fields[k].json_schema_extra
279
+ self._all_fields[k].json_schema_extra[attr] is not None
280
+ and attr in self._all_fields[k].json_schema_extra
191
281
  )
192
282
  return a if isinstance(a, bool) else False
193
283
  except Exception:
194
284
  return False
195
285
  return a
196
286
 
197
- def to_json_str(self, *args, **kwargs) -> str:
198
- """
199
- Convert the component to a JSON string.
200
-
201
- Returns:
202
- str: The JSON string representation of the component.
203
- """
204
- return self.model_dump_json(*args, by_alias=True, **kwargs)
205
-
206
- def to_dict(self, *args, **kwargs) -> dict[str, Any]:
207
- """
208
- Convert the component to a dictionary.
209
-
210
- Returns:
211
- dict[str, Any]: The dictionary representation of the component.
212
- """
213
- return self.model_dump(*args, by_alias=True, **kwargs)
214
-
215
- def to_xml(self) -> str:
216
- """
217
- Convert the component to an XML string.
218
-
219
- Returns:
220
- str: The XML string representation of the component.
221
- """
222
- import xml.etree.ElementTree as ET
223
-
224
- root = ET.Element(self.__class__.__name__)
225
-
226
- def convert(dict_obj, parent):
227
- for key, val in dict_obj.items():
228
- if isinstance(val, dict):
229
- element = ET.SubElement(parent, key)
230
- convert(val, element)
231
- else:
232
- element = ET.SubElement(parent, key)
233
- element.text = str(val)
234
-
235
- convert(self.to_dict(), root)
236
- return ET.tostring(root, encoding="unicode")
237
-
238
- def to_pd_series(self, *args, pd_kwargs: dict | None = None, **kwargs) -> Series:
239
- """
240
- Convert the node to a Pandas Series.
241
-
242
- Returns:
243
- Series: The Pandas Series representation of the node.
244
- """
245
- pd_kwargs = {} if pd_kwargs is None else pd_kwargs
246
- dict_ = self.to_dict(*args, **kwargs)
247
- return Series(dict_, **pd_kwargs)
287
+ def __str__(self):
288
+ return f"{self.__class__.__name__}({self.to_json_str()})"
248
289
 
249
290
 
250
291
  class BaseNode(BaseComponent):
@@ -264,7 +305,7 @@ class BaseNode(BaseComponent):
264
305
 
265
306
  metadata: dict[str, Any] = Field(
266
307
  default_factory=dict,
267
- alias="meta",
308
+ validation_alias="meta",
268
309
  description="Additional metadata for the node.",
269
310
  )
270
311
 
@@ -276,20 +317,36 @@ class BaseNode(BaseComponent):
276
317
 
277
318
  Args:
278
319
  obj (Any): The object to create the node from.
320
+ *args: Additional positional arguments.
321
+ **kwargs: Additional keyword arguments.
279
322
 
280
323
  Raises:
281
324
  NotImplementedError: If the object type is not supported.
282
325
  """
326
+ if not isinstance(obj, (dict, str, list, Series, DataFrame, BaseModel)):
327
+ type_ = str(type(obj))
328
+ if "llama_index" in type_:
329
+ return cls.from_obj(obj.to_dict())
330
+ elif "langchain" in type_:
331
+ langchain_json = obj.to_json()
332
+ langchain_dict = {
333
+ "lc_id": langchain_json["id"],
334
+ **langchain_json["kwargs"],
335
+ }
336
+ return cls.from_obj(langchain_dict)
337
+
283
338
  raise NotImplementedError(f"Unsupported type: {type(obj)}")
284
339
 
285
340
  @from_obj.register(dict)
286
341
  @classmethod
287
- def _from_dict(cls, obj, *args, **kwargs) -> T:
342
+ def _from_dict(cls, obj: dict, *args, **kwargs) -> T:
288
343
  """
289
344
  Create a node instance from a dictionary.
290
345
 
291
346
  Args:
292
347
  obj (dict): The dictionary to create the node from.
348
+ *args: Additional positional arguments.
349
+ **kwargs: Additional keyword arguments.
293
350
 
294
351
  Returns:
295
352
  T: The created node instance.
@@ -298,13 +355,15 @@ class BaseNode(BaseComponent):
298
355
 
299
356
  @from_obj.register(str)
300
357
  @classmethod
301
- def _from_str(cls, obj, *args, fuzzy_parse=False, **kwargs) -> T:
358
+ def _from_str(cls, obj: str, *args, fuzzy_parse: bool = False, **kwargs) -> T:
302
359
  """
303
360
  Create a node instance from a JSON string.
304
361
 
305
362
  Args:
306
363
  obj (str): The JSON string to create the node from.
364
+ *args: Additional positional arguments.
307
365
  fuzzy_parse (bool): Whether to perform fuzzy parsing.
366
+ **kwargs: Additional keyword arguments.
308
367
 
309
368
  Returns:
310
369
  T: The created node instance.
@@ -317,12 +376,14 @@ class BaseNode(BaseComponent):
317
376
 
318
377
  @from_obj.register(list)
319
378
  @classmethod
320
- def _from_list(cls, obj, *args, **kwargs) -> list[T]:
379
+ def _from_list(cls, obj: list, *args, **kwargs) -> list[T]:
321
380
  """
322
381
  Create a list of node instances from a list of objects.
323
382
 
324
383
  Args:
325
384
  obj (list): The list of objects to create nodes from.
385
+ *args: Additional positional arguments.
386
+ **kwargs: Additional keyword arguments.
326
387
 
327
388
  Returns:
328
389
  list[T]: The list of created node instances.
@@ -331,37 +392,51 @@ class BaseNode(BaseComponent):
331
392
 
332
393
  @from_obj.register(Series)
333
394
  @classmethod
334
- def _from_pd_series(cls, obj, *args, pd_kwargs=None, **kwargs) -> T:
395
+ def _from_pd_series(
396
+ cls, obj: Series, *args, pd_kwargs: dict | None = None, **kwargs
397
+ ) -> T:
335
398
  """
336
399
  Create a node instance from a Pandas Series.
337
400
 
338
401
  Args:
339
402
  obj (Series): The Pandas Series to create the node from.
403
+ *args: Additional positional arguments.
404
+ pd_kwargs (dict | None): Additional keyword arguments for Pandas Series.
405
+ **kwargs: Additional keyword arguments.
340
406
 
341
407
  Returns:
342
408
  T: The created node instance.
343
409
  """
344
- if pd_kwargs is None:
345
- pd_kwargs = {}
410
+ pd_kwargs = pd_kwargs or {}
346
411
  return cls.from_obj(obj.to_dict(**pd_kwargs), *args, **kwargs)
347
412
 
348
413
  @from_obj.register(DataFrame)
349
414
  @classmethod
350
- def _from_pd_dataframe(cls, obj, *args, pd_kwargs=None, **kwargs) -> list[T]:
415
+ def _from_pd_dataframe(
416
+ cls, obj: DataFrame, *args, pd_kwargs: dict | None = None, **kwargs
417
+ ) -> list[T]:
351
418
  """
352
419
  Create a list of node instances from a Pandas DataFrame.
353
420
 
354
421
  Args:
355
422
  obj (DataFrame): The Pandas DataFrame to create nodes from.
423
+ *args: Additional positional arguments.
424
+ pd_kwargs (dict | None): Additional keyword arguments for Pandas DataFrame.
425
+ **kwargs: Additional keyword arguments.
356
426
 
357
427
  Returns:
358
428
  list[T]: The list of created node instances.
359
429
  """
360
430
  if pd_kwargs is None:
361
431
  pd_kwargs = {}
362
- return [
363
- cls.from_obj(row, *args, **pd_kwargs, **kwargs) for _, row in obj.iterrows()
364
- ]
432
+
433
+ _objs = []
434
+ for index, row in obj.iterrows():
435
+ _obj = cls.from_obj(row, *args, **pd_kwargs, **kwargs)
436
+ _obj.metadata["df_index"] = index
437
+ _objs.append(_obj)
438
+
439
+ return _objs
365
440
 
366
441
  @from_obj.register(BaseModel)
367
442
  @classmethod
@@ -375,27 +450,29 @@ class BaseNode(BaseComponent):
375
450
  Returns:
376
451
  T: The created node instance.
377
452
  """
378
- if pydantic_kwargs is None:
379
- pydantic_kwargs = {"by_alias": True}
380
-
381
- config_ = {}
453
+ pydantic_kwargs = pydantic_kwargs or {"by_alias": True}
382
454
  try:
383
- config_ = obj.model_dump(**pydantic_kwargs)
384
- except:
385
- config_ = obj.dict(**pydantic_kwargs)
455
+ config_ = {}
456
+ try:
457
+ config_ = obj.model_dump(**pydantic_kwargs)
458
+ except:
459
+ config_ = obj.to_dict(**pydantic_kwargs)
460
+ else:
461
+ config_ = obj.dict(**pydantic_kwargs)
462
+ except Exception as e:
463
+ raise ValueError(f"Invalid Pydantic model for deserialization: {e}") from e
386
464
 
387
465
  return cls.from_obj(config_ | kwargs)
388
466
 
389
467
  def meta_get(
390
- self, key: str, indices: list[str | int] = None, default: Any = None
468
+ self, key: str, indices: list[str | int] | None = None, default: Any = None
391
469
  ) -> Any:
392
470
  """
393
471
  Get a value from the metadata dictionary.
394
472
 
395
473
  Args:
396
474
  key (str): The key to retrieve the value for.
397
- indices (list[str | int]): Optional list of indices for nested
398
- retrieval.
475
+ indices (list[str | int] | None): Optional list of indices for nested retrieval.
399
476
  default (Any): The default value to return if the key is not found.
400
477
 
401
478
  Returns:
@@ -452,4 +529,4 @@ class BaseNode(BaseComponent):
452
529
  """
453
530
  self.metadata = nested.nmerge(
454
531
  [self.metadata, additional_metadata], overwrite=overwrite, **kwargs
455
- )
532
+ )
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from enum import Enum
3
+ from typing import Callable
3
4
  from pydantic import BaseModel, Field
4
5
 
5
6
 
@@ -14,6 +15,7 @@ class ConditionSource(str, Enum):
14
15
 
15
16
  STRUCTURE = "structure"
16
17
  EXECUTABLE = "executable"
18
+ RULE = "rule"
17
19
 
18
20
 
19
21
  class Condition(BaseModel, ABC):