lionagi 0.14.11__py3-none-any.whl → 0.15.1__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 (38) hide show
  1. lionagi/libs/concurrency.py +1 -0
  2. lionagi/libs/token_transform/perplexity.py +2 -1
  3. lionagi/libs/token_transform/symbolic_compress_context.py +8 -7
  4. lionagi/ln/__init__.py +49 -0
  5. lionagi/ln/_async_call.py +294 -0
  6. lionagi/ln/_list_call.py +130 -0
  7. lionagi/ln/_models.py +126 -0
  8. lionagi/ln/_to_list.py +176 -0
  9. lionagi/ln/_types.py +146 -0
  10. lionagi/{libs → ln}/concurrency/__init__.py +4 -2
  11. lionagi/ln/concurrency/utils.py +15 -0
  12. lionagi/models/hashable_model.py +1 -2
  13. lionagi/operations/brainstorm/brainstorm.py +2 -1
  14. lionagi/operations/flow.py +3 -3
  15. lionagi/operations/manager.py +10 -8
  16. lionagi/operations/node.py +2 -3
  17. lionagi/operations/plan/plan.py +3 -3
  18. lionagi/protocols/generic/event.py +47 -6
  19. lionagi/protocols/generic/pile.py +1 -1
  20. lionagi/service/hooks/_types.py +2 -2
  21. lionagi/session/branch.py +14 -3
  22. lionagi/session/session.py +55 -25
  23. lionagi/utils.py +90 -510
  24. lionagi/version.py +1 -1
  25. {lionagi-0.14.11.dist-info → lionagi-0.15.1.dist-info}/METADATA +4 -4
  26. {lionagi-0.14.11.dist-info → lionagi-0.15.1.dist-info}/RECORD +36 -30
  27. lionagi/libs/hash/__init__.py +0 -3
  28. lionagi/libs/hash/manager.py +0 -26
  29. /lionagi/{libs/hash/hash_dict.py → ln/_hash.py} +0 -0
  30. /lionagi/{libs → ln}/concurrency/cancel.py +0 -0
  31. /lionagi/{libs → ln}/concurrency/errors.py +0 -0
  32. /lionagi/{libs → ln}/concurrency/patterns.py +0 -0
  33. /lionagi/{libs → ln}/concurrency/primitives.py +0 -0
  34. /lionagi/{libs → ln}/concurrency/resource_tracker.py +0 -0
  35. /lionagi/{libs → ln}/concurrency/task.py +0 -0
  36. /lionagi/{libs → ln}/concurrency/throttle.py +0 -0
  37. {lionagi-0.14.11.dist-info → lionagi-0.15.1.dist-info}/WHEEL +0 -0
  38. {lionagi-0.14.11.dist-info → lionagi-0.15.1.dist-info}/licenses/LICENSE +0 -0
lionagi/ln/_to_list.py ADDED
@@ -0,0 +1,176 @@
1
+ from collections.abc import Iterable, Mapping
2
+ from dataclasses import dataclass
3
+ from enum import Enum as _Enum
4
+ from typing import Any, ClassVar
5
+
6
+ from pydantic import BaseModel
7
+ from pydantic_core import PydanticUndefinedType
8
+
9
+ from ._hash import hash_dict
10
+ from ._models import Params
11
+ from ._types import UndefinedType, UnsetType
12
+
13
+ __all__ = ("to_list", "ToListParams")
14
+
15
+
16
+ _SKIP_TYPE = (str, bytes, bytearray, Mapping, BaseModel, _Enum)
17
+ _TUPLE_SET_TYPES = (tuple, set, frozenset)
18
+ _SKIP_TUPLE_SET = (*_SKIP_TYPE, *_TUPLE_SET_TYPES)
19
+ _SINGLETONE_TYPES = (UndefinedType, UnsetType, PydanticUndefinedType)
20
+ _BYTE_LIKE_TYPES = (str, bytes, bytearray)
21
+
22
+
23
+ def to_list(
24
+ input_: Any,
25
+ /,
26
+ *,
27
+ flatten: bool = False,
28
+ dropna: bool = False,
29
+ unique: bool = False,
30
+ use_values: bool = False,
31
+ flatten_tuple_set: bool = False,
32
+ ) -> list:
33
+ """Convert input to a list with optional transformations.
34
+
35
+ Transforms various input types into a list with configurable processing
36
+ options for flattening, filtering, and value extraction.
37
+
38
+ Args:
39
+ input_: Value to convert to list.
40
+ flatten: If True, recursively flatten nested iterables.
41
+ dropna: If True, remove None and undefined values.
42
+ unique: If True, remove duplicates (requires flatten=True).
43
+ use_values: If True, extract values from enums/mappings.
44
+ flatten_tuple_items: If True, include tuples in flattening.
45
+ flatten_set_items: If True, include sets in flattening.
46
+
47
+ Raises:
48
+ ValueError: If unique=True is used without flatten=True.
49
+ """
50
+
51
+ def _process_list(
52
+ lst: list[Any],
53
+ flatten: bool,
54
+ dropna: bool,
55
+ ) -> list[Any]:
56
+ """Process list according to flatten and dropna options.
57
+
58
+ Args:
59
+ lst: Input list to process.
60
+ flatten: Whether to flatten nested iterables.
61
+ dropna: Whether to remove None/undefined values.
62
+ """
63
+ result = []
64
+ skip_types = _SKIP_TYPE if flatten_tuple_set else _SKIP_TUPLE_SET
65
+
66
+ for item in lst:
67
+ if dropna and (
68
+ item is None or isinstance(item, _SINGLETONE_TYPES)
69
+ ):
70
+ continue
71
+
72
+ is_iterable = isinstance(item, Iterable)
73
+ should_skip = isinstance(item, skip_types)
74
+
75
+ if is_iterable and not should_skip:
76
+ item_list = list(item)
77
+ if flatten:
78
+ result.extend(_process_list(item_list, flatten, dropna))
79
+ else:
80
+ result.append(_process_list(item_list, flatten, dropna))
81
+ else:
82
+ result.append(item)
83
+
84
+ return result
85
+
86
+ def _to_list_type(input_: Any, use_values: bool) -> list[Any]:
87
+ """Convert input to initial list based on type.
88
+
89
+ Args:
90
+ input_: Value to convert to list.
91
+ use_values: Whether to extract values from containers.
92
+ """
93
+ if input_ is None or isinstance(input_, _SINGLETONE_TYPES):
94
+ return []
95
+
96
+ if isinstance(input_, list):
97
+ return input_
98
+
99
+ if isinstance(input_, type) and issubclass(input_, _Enum):
100
+ members = input_.__members__.values()
101
+ return (
102
+ [member.value for member in members]
103
+ if use_values
104
+ else list(members)
105
+ )
106
+
107
+ if isinstance(input_, _BYTE_LIKE_TYPES):
108
+ return list(input_) if use_values else [input_]
109
+
110
+ if isinstance(input_, Mapping):
111
+ return (
112
+ list(input_.values())
113
+ if use_values and hasattr(input_, "values")
114
+ else [input_]
115
+ )
116
+
117
+ if isinstance(input_, BaseModel):
118
+ return [input_]
119
+
120
+ if isinstance(input_, Iterable) and not isinstance(
121
+ input_, _BYTE_LIKE_TYPES
122
+ ):
123
+ return list(input_)
124
+
125
+ return [input_]
126
+
127
+ if unique and not flatten:
128
+ raise ValueError("unique=True requires flatten=True")
129
+
130
+ initial_list = _to_list_type(input_, use_values=use_values)
131
+ processed = _process_list(initial_list, flatten=flatten, dropna=dropna)
132
+
133
+ if unique:
134
+ seen = set()
135
+ out = []
136
+ try:
137
+ return [x for x in processed if not (x in seen or seen.add(x))]
138
+ except TypeError:
139
+ for i in processed:
140
+ hash_value = None
141
+ try:
142
+ hash_value = hash(i)
143
+ except TypeError:
144
+ if isinstance(i, (BaseModel, Mapping)):
145
+ hash_value = hash_dict(i)
146
+ else:
147
+ raise ValueError(
148
+ "Unhashable type encountered in list unique value processing."
149
+ )
150
+ if hash_value not in seen:
151
+ seen.add(hash_value)
152
+ out.append(i)
153
+ return out
154
+
155
+ return processed
156
+
157
+
158
+ @dataclass(slots=True, frozen=True, init=False)
159
+ class ToListParams(Params):
160
+ _func: ClassVar[Any] = to_list
161
+
162
+ flatten: bool
163
+ """If True, recursively flatten nested iterables."""
164
+ dropna: bool
165
+ """If True, remove None and undefined values."""
166
+ unique: bool
167
+ """If True, remove duplicates (requires flatten=True)."""
168
+ use_values: bool
169
+ """If True, extract values from enums/mappings."""
170
+ flatten_tuple_set: bool
171
+ """If True, include tuples and sets in flattening."""
172
+
173
+ def __call__(self, input_: Any, **kw) -> list:
174
+ """Convert parameters to a list."""
175
+ partial = self.as_partial()
176
+ return partial(input_, **kw)
lionagi/ln/_types.py ADDED
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum as _Enum
4
+ from typing import Any, Final, Literal, TypeVar, Union
5
+
6
+ from typing_extensions import TypedDict
7
+
8
+ __all__ = (
9
+ "Undefined",
10
+ "Unset",
11
+ "MaybeUndefined",
12
+ "MaybeUnset",
13
+ "MaybeSentinel",
14
+ "SingletonType",
15
+ "UndefinedType",
16
+ "UnsetType",
17
+ "KeysDict",
18
+ "T",
19
+ "Enum",
20
+ "is_sentinel",
21
+ "not_sentinel",
22
+ )
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class _SingletonMeta(type):
28
+ """Metaclass that guarantees exactly one instance per subclass.
29
+
30
+ This ensures that sentinel values maintain identity across the entire application,
31
+ allowing safe identity checks with 'is' operator.
32
+ """
33
+
34
+ _cache: dict[type, SingletonType] = {}
35
+
36
+ def __call__(cls, *a, **kw):
37
+ if cls not in cls._cache:
38
+ cls._cache[cls] = super().__call__(*a, **kw)
39
+ return cls._cache[cls]
40
+
41
+
42
+ class SingletonType(metaclass=_SingletonMeta):
43
+ """Base class for singleton sentinel types.
44
+
45
+ Provides consistent interface for sentinel values with:
46
+ - Identity preservation across deepcopy
47
+ - Falsy boolean evaluation
48
+ - Clear string representation
49
+ """
50
+
51
+ __slots__: tuple[str, ...] = ()
52
+
53
+ def __deepcopy__(self, memo): # copy & deepcopy both noop
54
+ return self
55
+
56
+ def __copy__(self):
57
+ return self
58
+
59
+ # concrete classes *must* override the two methods below
60
+ def __bool__(self) -> bool: ...
61
+ def __repr__(self) -> str: ...
62
+
63
+
64
+ class UndefinedType(SingletonType):
65
+ """Sentinel for a key or field entirely missing from a namespace.
66
+
67
+ Use this when:
68
+ - A field has never been set
69
+ - A key doesn't exist in a mapping
70
+ - A value is conceptually undefined (not just unset)
71
+
72
+ Example:
73
+ >>> d = {"a": 1}
74
+ >>> d.get("b", Undefined) is Undefined
75
+ True
76
+ """
77
+
78
+ __slots__ = ()
79
+
80
+ def __bool__(self) -> Literal[False]:
81
+ return False
82
+
83
+ def __repr__(self) -> Literal["Undefined"]:
84
+ return "Undefined"
85
+
86
+ def __str__(self) -> Literal["Undefined"]:
87
+ return "Undefined"
88
+
89
+
90
+ class UnsetType(SingletonType):
91
+ """Sentinel for a key present but value not yet provided.
92
+
93
+ Use this when:
94
+ - A parameter exists but hasn't been given a value
95
+ - Distinguishing between None and "not provided"
96
+ - API parameters that are optional but need explicit handling
97
+
98
+ Example:
99
+ >>> def func(param=Unset):
100
+ ... if param is not Unset:
101
+ ... # param was explicitly provided
102
+ ... process(param)
103
+ """
104
+
105
+ __slots__ = ()
106
+
107
+ def __bool__(self) -> Literal[False]:
108
+ return False
109
+
110
+ def __repr__(self) -> Literal["Unset"]:
111
+ return "Unset"
112
+
113
+ def __str__(self) -> Literal["Unset"]:
114
+ return "Unset"
115
+
116
+
117
+ Undefined: Final = UndefinedType()
118
+ """A key or field entirely missing from a namespace"""
119
+ Unset: Final = UnsetType()
120
+ """A key present but value not yet provided."""
121
+
122
+ MaybeUndefined = Union[T, UndefinedType]
123
+ MaybeUnset = Union[T, UnsetType]
124
+ MaybeSentinel = Union[T, UndefinedType, UnsetType]
125
+
126
+
127
+ def is_sentinel(value: Any) -> bool:
128
+ """Check if a value is any sentinel (Undefined or Unset)."""
129
+ return value is Undefined or value is Unset
130
+
131
+
132
+ def not_sentinel(value: Any) -> bool:
133
+ """Check if a value is NOT a sentinel. Useful for filtering operations."""
134
+ return value is not Undefined and value is not Unset
135
+
136
+
137
+ class Enum(_Enum):
138
+ @classmethod
139
+ def allowed(cls) -> tuple[str, ...]:
140
+ return tuple(e.value for e in cls)
141
+
142
+
143
+ class KeysDict(TypedDict, total=False):
144
+ """TypedDict for keys dictionary."""
145
+
146
+ key: Any # Represents any key-type pair
@@ -22,8 +22,9 @@ from .resource_tracker import (
22
22
  untrack_resource,
23
23
  )
24
24
  from .task import TaskGroup, create_task_group
25
+ from .utils import is_coro_func
25
26
 
26
- __all__ = [
27
+ __all__ = (
27
28
  "TaskGroup",
28
29
  "create_task_group",
29
30
  "CancelScope",
@@ -46,4 +47,5 @@ __all__ = [
46
47
  "untrack_resource",
47
48
  "cleanup_check",
48
49
  "get_global_tracker",
49
- ]
50
+ "is_coro_func",
51
+ )
@@ -0,0 +1,15 @@
1
+ import asyncio
2
+ from collections.abc import Callable
3
+ from functools import lru_cache
4
+ from typing import Any
5
+
6
+ __all__ = ("is_coro_func",)
7
+
8
+
9
+ @lru_cache(maxsize=None)
10
+ def _is_coro_func(func: Callable[..., Any]) -> bool:
11
+ return asyncio.iscoroutinefunction(func)
12
+
13
+
14
+ def is_coro_func(func: Callable[..., Any]) -> bool:
15
+ return _is_coro_func(func)
@@ -1,8 +1,7 @@
1
1
  from pydantic import BaseModel
2
2
  from typing_extensions import Self
3
3
 
4
- from lionagi.libs.hash.hash_dict import hash_dict
5
- from lionagi.utils import UNDEFINED
4
+ from lionagi.utils import UNDEFINED, hash_dict
6
5
 
7
6
 
8
7
  class HashableModel(BaseModel):
@@ -12,10 +12,11 @@ from lionagi.fields.instruct import (
12
12
  Instruct,
13
13
  InstructResponse,
14
14
  )
15
+ from lionagi.ln import alcall
15
16
  from lionagi.protocols.generic.element import ID
16
17
  from lionagi.session.branch import Branch
17
18
  from lionagi.session.session import Session
18
- from lionagi.utils import alcall, to_list
19
+ from lionagi.utils import to_list
19
20
 
20
21
  from ..utils import prepare_instruct, prepare_session
21
22
  from .prompt import PROMPT
@@ -12,9 +12,9 @@ using Events for synchronization and CapacityLimiter for concurrency control.
12
12
  import os
13
13
  from typing import Any
14
14
 
15
- from lionagi.libs.concurrency.primitives import CapacityLimiter
16
- from lionagi.libs.concurrency.primitives import Event as ConcurrencyEvent
17
- from lionagi.libs.concurrency.task import create_task_group
15
+ from lionagi.ln.concurrency.primitives import CapacityLimiter
16
+ from lionagi.ln.concurrency.primitives import Event as ConcurrencyEvent
17
+ from lionagi.ln.concurrency.task import create_task_group
18
18
  from lionagi.operations.node import Operation
19
19
  from lionagi.protocols.types import EventStatus, Graph
20
20
  from lionagi.session.branch import Branch
@@ -1,6 +1,7 @@
1
1
  from collections.abc import Callable
2
2
 
3
3
  from lionagi.protocols._concepts import Manager
4
+ from lionagi.utils import is_coro_func
4
5
 
5
6
  """
6
7
  experimental
@@ -8,14 +9,15 @@ experimental
8
9
 
9
10
 
10
11
  class OperationManager(Manager):
11
- def __init__(self, *args, **kwargs):
12
+ def __init__(self):
12
13
  super().__init__()
13
14
  self.registry: dict[str, Callable] = {}
14
- self.register_operations(*args, **kwargs)
15
15
 
16
- def register_operations(self, *args, **kwargs) -> None:
17
- operations = {}
18
- if args:
19
- operations = {i.__name__ for i in args if hasattr(i, "__name__")}
20
- operations.update(kwargs)
21
- self.registry.update(operations)
16
+ def register(self, operation: str, func: Callable, update: bool = False):
17
+ if operation in self.registry and not update:
18
+ raise ValueError(f"Operation '{operation}' is already registered.")
19
+ if not is_coro_func(func):
20
+ raise ValueError(
21
+ f"Operation '{operation}' must be an async function."
22
+ )
23
+ self.registry[operation] = func
@@ -26,7 +26,7 @@ logger = logging.getLogger("operation")
26
26
 
27
27
 
28
28
  class Operation(Node, Event):
29
- operation: BranchOperations
29
+ operation: BranchOperations | str
30
30
  parameters: dict[str, Any] | BaseModel = Field(
31
31
  default_factory=dict,
32
32
  description="Parameters for the operation",
@@ -74,12 +74,11 @@ class Operation(Node, Event):
74
74
  return self.execution.response if self.execution else None
75
75
 
76
76
  async def invoke(self, branch: Branch):
77
- meth = getattr(branch, self.operation, None)
77
+ meth = branch.get_operation(self.operation)
78
78
  if meth is None:
79
79
  raise ValueError(f"Unsupported operation type: {self.operation}")
80
80
 
81
81
  start = asyncio.get_event_loop().time()
82
-
83
82
  try:
84
83
  self.execution.status = EventStatus.PROCESSING
85
84
  self.branch_id = branch.id
@@ -11,10 +11,10 @@ from lionagi.fields.instruct import (
11
11
  Instruct,
12
12
  InstructResponse,
13
13
  )
14
+ from lionagi.ln import alcall
14
15
  from lionagi.protocols.types import ID
15
16
  from lionagi.session.branch import Branch
16
17
  from lionagi.session.session import Session
17
- from lionagi.utils import alcall
18
18
 
19
19
  from ..utils import prepare_instruct, prepare_session
20
20
  from .prompt import EXPANSION_PROMPT, PLAN_PROMPT
@@ -366,8 +366,8 @@ async def plan(
366
366
  parallel_chunk_results = await alcall(
367
367
  all_chunks,
368
368
  execute_chunk_sequentially,
369
- flatten=True,
370
- dropna=True,
369
+ output_flatten=True,
370
+ output_dropna=True,
371
371
  )
372
372
 
373
373
  out.execute = parallel_chunk_results
@@ -2,11 +2,16 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ import contextlib
6
+ import json
5
7
  from enum import Enum
6
8
  from typing import Any
7
9
 
8
10
  from pydantic import Field, field_serializer
9
11
 
12
+ from lionagi.ln import Unset
13
+ from lionagi.utils import to_dict
14
+
10
15
  from .element import Element
11
16
 
12
17
  __all__ = (
@@ -16,6 +21,9 @@ __all__ = (
16
21
  )
17
22
 
18
23
 
24
+ _SIMPLE_TYPE = (str, bytes, bytearray, int, float, type(None), Enum)
25
+
26
+
19
27
  class EventStatus(str, Enum):
20
28
  """Status states for tracking action execution progress.
21
29
 
@@ -79,6 +87,44 @@ class Execution:
79
87
  f"response={self.response}, error={self.error})"
80
88
  )
81
89
 
90
+ def to_dict(self) -> dict:
91
+ """Converts the execution state to a dictionary.
92
+
93
+ Returns:
94
+ dict: A dictionary representation of the execution state.
95
+ """
96
+ res_ = Unset
97
+ json_serializable = True
98
+
99
+ if not isinstance(self.response, _SIMPLE_TYPE):
100
+ json_serializable = False
101
+ try:
102
+ # check whether response is JSON serializable
103
+ json.dumps(self.response)
104
+ res_ = self.response
105
+ json_serializable = True
106
+ except Exception:
107
+ with contextlib.suppress(Exception):
108
+ # attempt to convert to dict
109
+ d_ = to_dict(
110
+ self.response,
111
+ recursive=True,
112
+ recursive_python_only=False,
113
+ )
114
+ json.dumps(d_)
115
+ res_ = d_
116
+ json_serializable = True
117
+
118
+ if res_ is Unset and not json_serializable:
119
+ res_ = "<unserializable>"
120
+
121
+ return {
122
+ "status": self.status.value,
123
+ "duration": self.duration,
124
+ "response": res_ or self.response,
125
+ "error": self.error,
126
+ }
127
+
82
128
 
83
129
  class Event(Element):
84
130
  """Extends Element with an execution state.
@@ -101,12 +147,7 @@ class Event(Element):
101
147
  dict: The serialized data containing status, duration, response,
102
148
  and error fields.
103
149
  """
104
- return {
105
- "status": val.status.value,
106
- "duration": val.duration,
107
- "response": val.response,
108
- "error": val.error,
109
- }
150
+ return val.to_dict()
110
151
 
111
152
  @property
112
153
  def response(self) -> Any:
@@ -54,7 +54,7 @@ def async_synchronized(func: Callable):
54
54
  return wrapper
55
55
 
56
56
 
57
- class Pile(Element, Collective[E], Generic[E], Adaptable, AsyncAdaptable):
57
+ class Pile(Element, Collective[T], Generic[T], Adaptable, AsyncAdaptable):
58
58
  """Thread-safe async-compatible, ordered collection of elements.
59
59
 
60
60
  The Pile class provides a thread-safe, async-compatible collection with:
@@ -8,7 +8,7 @@ from typing import TypeVar
8
8
 
9
9
  from typing_extensions import TypedDict
10
10
 
11
- from lionagi.utils import StringEnum
11
+ from lionagi.utils import Enum
12
12
 
13
13
  SC = TypeVar("SC") # streaming chunk type
14
14
 
@@ -21,7 +21,7 @@ __all__ = (
21
21
  )
22
22
 
23
23
 
24
- class HookEventTypes(StringEnum):
24
+ class HookEventTypes(str, Enum):
25
25
  PreEventCreate = "pre_event_create"
26
26
  PreInvokation = "pre_invokation"
27
27
  PostInvokation = "post_invokation"
lionagi/session/branch.py CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from collections.abc import AsyncGenerator
5
+ from collections.abc import AsyncGenerator, Callable
6
6
  from enum import Enum
7
7
  from typing import Any, Literal
8
8
 
@@ -14,6 +14,7 @@ from lionagi.config import settings
14
14
  from lionagi.fields import Instruct
15
15
  from lionagi.libs.schema.as_readable import as_readable
16
16
  from lionagi.models.field_model import FieldModel
17
+ from lionagi.operations.manager import OperationManager
17
18
  from lionagi.protocols.action.tool import FuncTool, Tool, ToolRef
18
19
  from lionagi.protocols.types import (
19
20
  ID,
@@ -47,7 +48,9 @@ from lionagi.service.connections.endpoint import Endpoint
47
48
  from lionagi.service.types import iModel, iModelManager
48
49
  from lionagi.settings import Settings
49
50
  from lionagi.tools.base import LionTool
50
- from lionagi.utils import UNDEFINED, alcall, bcall, copy
51
+ from lionagi.utils import UNDEFINED
52
+ from lionagi.utils import alcall as alcall_legacy
53
+ from lionagi.utils import copy, is_coro_func
51
54
 
52
55
  from .prompts import LION_SYSTEM_MESSAGE
53
56
 
@@ -109,6 +112,7 @@ class Branch(Element, Communicatable, Relational):
109
112
  _action_manager: ActionManager | None = PrivateAttr(None)
110
113
  _imodel_manager: iModelManager | None = PrivateAttr(None)
111
114
  _log_manager: LogManager | None = PrivateAttr(None)
115
+ _operation_manager: OperationManager | None = PrivateAttr(None)
112
116
 
113
117
  def __init__(
114
118
  self,
@@ -229,6 +233,8 @@ class Branch(Element, Communicatable, Relational):
229
233
  else:
230
234
  self._log_manager = LogManager(**Settings.Config.LOG, logs=logs)
231
235
 
236
+ self._operation_manager = OperationManager()
237
+
232
238
  # -------------------------------------------------------------------------
233
239
  # Properties to expose managers and core data
234
240
  # -------------------------------------------------------------------------
@@ -302,6 +308,11 @@ class Branch(Element, Communicatable, Relational):
302
308
  """
303
309
  return self._action_manager.registry
304
310
 
311
+ def get_operation(self, operation: str) -> Callable | None:
312
+ if hasattr(self, operation):
313
+ return getattr(self, operation)
314
+ return self._operation_manager.registry.get(operation)
315
+
305
316
  # -------------------------------------------------------------------------
306
317
  # Cloning
307
318
  # -------------------------------------------------------------------------
@@ -1268,7 +1279,7 @@ class Branch(Element, Communicatable, Relational):
1268
1279
  action_request: ActionRequest | BaseModel | dict,
1269
1280
  **kwargs,
1270
1281
  ) -> list:
1271
- return await alcall(action_request, self._act, **kwargs)
1282
+ return await alcall_legacy(action_request, self._act, **kwargs)
1272
1283
 
1273
1284
  async def _sequential_act(
1274
1285
  self,