vellum-ai 0.9.16rc2__py3-none-any.whl → 0.10.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (245) hide show
  1. vellum/plugins/__init__.py +0 -0
  2. vellum/plugins/pydantic.py +74 -0
  3. vellum/plugins/utils.py +19 -0
  4. vellum/plugins/vellum_mypy.py +639 -3
  5. vellum/workflows/README.md +90 -0
  6. vellum/workflows/__init__.py +5 -0
  7. vellum/workflows/constants.py +43 -0
  8. vellum/workflows/descriptors/__init__.py +0 -0
  9. vellum/workflows/descriptors/base.py +339 -0
  10. vellum/workflows/descriptors/tests/test_utils.py +83 -0
  11. vellum/workflows/descriptors/utils.py +90 -0
  12. vellum/workflows/edges/__init__.py +5 -0
  13. vellum/workflows/edges/edge.py +23 -0
  14. vellum/workflows/emitters/__init__.py +5 -0
  15. vellum/workflows/emitters/base.py +14 -0
  16. vellum/workflows/environment/__init__.py +5 -0
  17. vellum/workflows/environment/environment.py +7 -0
  18. vellum/workflows/errors/__init__.py +6 -0
  19. vellum/workflows/errors/types.py +20 -0
  20. vellum/workflows/events/__init__.py +31 -0
  21. vellum/workflows/events/node.py +125 -0
  22. vellum/workflows/events/tests/__init__.py +0 -0
  23. vellum/workflows/events/tests/test_event.py +216 -0
  24. vellum/workflows/events/types.py +52 -0
  25. vellum/workflows/events/utils.py +5 -0
  26. vellum/workflows/events/workflow.py +139 -0
  27. vellum/workflows/exceptions.py +15 -0
  28. vellum/workflows/expressions/__init__.py +0 -0
  29. vellum/workflows/expressions/accessor.py +52 -0
  30. vellum/workflows/expressions/and_.py +32 -0
  31. vellum/workflows/expressions/begins_with.py +31 -0
  32. vellum/workflows/expressions/between.py +38 -0
  33. vellum/workflows/expressions/coalesce_expression.py +41 -0
  34. vellum/workflows/expressions/contains.py +30 -0
  35. vellum/workflows/expressions/does_not_begin_with.py +31 -0
  36. vellum/workflows/expressions/does_not_contain.py +30 -0
  37. vellum/workflows/expressions/does_not_end_with.py +31 -0
  38. vellum/workflows/expressions/does_not_equal.py +25 -0
  39. vellum/workflows/expressions/ends_with.py +31 -0
  40. vellum/workflows/expressions/equals.py +25 -0
  41. vellum/workflows/expressions/greater_than.py +33 -0
  42. vellum/workflows/expressions/greater_than_or_equal_to.py +33 -0
  43. vellum/workflows/expressions/in_.py +31 -0
  44. vellum/workflows/expressions/is_blank.py +24 -0
  45. vellum/workflows/expressions/is_not_blank.py +24 -0
  46. vellum/workflows/expressions/is_not_null.py +21 -0
  47. vellum/workflows/expressions/is_not_undefined.py +22 -0
  48. vellum/workflows/expressions/is_null.py +21 -0
  49. vellum/workflows/expressions/is_undefined.py +22 -0
  50. vellum/workflows/expressions/less_than.py +33 -0
  51. vellum/workflows/expressions/less_than_or_equal_to.py +33 -0
  52. vellum/workflows/expressions/not_between.py +38 -0
  53. vellum/workflows/expressions/not_in.py +31 -0
  54. vellum/workflows/expressions/or_.py +32 -0
  55. vellum/workflows/graph/__init__.py +3 -0
  56. vellum/workflows/graph/graph.py +131 -0
  57. vellum/workflows/graph/tests/__init__.py +0 -0
  58. vellum/workflows/graph/tests/test_graph.py +437 -0
  59. vellum/workflows/inputs/__init__.py +5 -0
  60. vellum/workflows/inputs/base.py +55 -0
  61. vellum/workflows/logging.py +14 -0
  62. vellum/workflows/nodes/__init__.py +46 -0
  63. vellum/workflows/nodes/bases/__init__.py +7 -0
  64. vellum/workflows/nodes/bases/base.py +332 -0
  65. vellum/workflows/nodes/bases/base_subworkflow_node/__init__.py +5 -0
  66. vellum/workflows/nodes/bases/base_subworkflow_node/node.py +10 -0
  67. vellum/workflows/nodes/bases/tests/__init__.py +0 -0
  68. vellum/workflows/nodes/bases/tests/test_base_node.py +125 -0
  69. vellum/workflows/nodes/core/__init__.py +16 -0
  70. vellum/workflows/nodes/core/error_node/__init__.py +5 -0
  71. vellum/workflows/nodes/core/error_node/node.py +26 -0
  72. vellum/workflows/nodes/core/inline_subworkflow_node/__init__.py +5 -0
  73. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +73 -0
  74. vellum/workflows/nodes/core/map_node/__init__.py +5 -0
  75. vellum/workflows/nodes/core/map_node/node.py +147 -0
  76. vellum/workflows/nodes/core/map_node/tests/__init__.py +0 -0
  77. vellum/workflows/nodes/core/map_node/tests/test_node.py +65 -0
  78. vellum/workflows/nodes/core/retry_node/__init__.py +5 -0
  79. vellum/workflows/nodes/core/retry_node/node.py +106 -0
  80. vellum/workflows/nodes/core/retry_node/tests/__init__.py +0 -0
  81. vellum/workflows/nodes/core/retry_node/tests/test_node.py +93 -0
  82. vellum/workflows/nodes/core/templating_node/__init__.py +5 -0
  83. vellum/workflows/nodes/core/templating_node/custom_filters.py +12 -0
  84. vellum/workflows/nodes/core/templating_node/exceptions.py +2 -0
  85. vellum/workflows/nodes/core/templating_node/node.py +123 -0
  86. vellum/workflows/nodes/core/templating_node/render.py +55 -0
  87. vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +21 -0
  88. vellum/workflows/nodes/core/try_node/__init__.py +5 -0
  89. vellum/workflows/nodes/core/try_node/node.py +110 -0
  90. vellum/workflows/nodes/core/try_node/tests/__init__.py +0 -0
  91. vellum/workflows/nodes/core/try_node/tests/test_node.py +82 -0
  92. vellum/workflows/nodes/displayable/__init__.py +31 -0
  93. vellum/workflows/nodes/displayable/api_node/__init__.py +5 -0
  94. vellum/workflows/nodes/displayable/api_node/node.py +44 -0
  95. vellum/workflows/nodes/displayable/bases/__init__.py +11 -0
  96. vellum/workflows/nodes/displayable/bases/api_node/__init__.py +5 -0
  97. vellum/workflows/nodes/displayable/bases/api_node/node.py +70 -0
  98. vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py +5 -0
  99. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +60 -0
  100. vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py +5 -0
  101. vellum/workflows/nodes/displayable/bases/inline_prompt_node/constants.py +13 -0
  102. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +118 -0
  103. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +98 -0
  104. vellum/workflows/nodes/displayable/bases/search_node.py +90 -0
  105. vellum/workflows/nodes/displayable/code_execution_node/__init__.py +5 -0
  106. vellum/workflows/nodes/displayable/code_execution_node/node.py +197 -0
  107. vellum/workflows/nodes/displayable/code_execution_node/tests/__init__.py +0 -0
  108. vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/__init__.py +0 -0
  109. vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/main.py +3 -0
  110. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +111 -0
  111. vellum/workflows/nodes/displayable/code_execution_node/utils.py +10 -0
  112. vellum/workflows/nodes/displayable/conditional_node/__init__.py +5 -0
  113. vellum/workflows/nodes/displayable/conditional_node/node.py +25 -0
  114. vellum/workflows/nodes/displayable/final_output_node/__init__.py +5 -0
  115. vellum/workflows/nodes/displayable/final_output_node/node.py +43 -0
  116. vellum/workflows/nodes/displayable/guardrail_node/__init__.py +5 -0
  117. vellum/workflows/nodes/displayable/guardrail_node/node.py +97 -0
  118. vellum/workflows/nodes/displayable/inline_prompt_node/__init__.py +5 -0
  119. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +41 -0
  120. vellum/workflows/nodes/displayable/merge_node/__init__.py +5 -0
  121. vellum/workflows/nodes/displayable/merge_node/node.py +10 -0
  122. vellum/workflows/nodes/displayable/prompt_deployment_node/__init__.py +5 -0
  123. vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +45 -0
  124. vellum/workflows/nodes/displayable/search_node/__init__.py +5 -0
  125. vellum/workflows/nodes/displayable/search_node/node.py +26 -0
  126. vellum/workflows/nodes/displayable/subworkflow_deployment_node/__init__.py +5 -0
  127. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +156 -0
  128. vellum/workflows/nodes/displayable/tests/__init__.py +0 -0
  129. vellum/workflows/nodes/displayable/tests/test_inline_text_prompt_node.py +148 -0
  130. vellum/workflows/nodes/displayable/tests/test_search_node_wth_text_output.py +134 -0
  131. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +80 -0
  132. vellum/workflows/nodes/utils.py +27 -0
  133. vellum/workflows/outputs/__init__.py +6 -0
  134. vellum/workflows/outputs/base.py +196 -0
  135. vellum/workflows/ports/__init__.py +7 -0
  136. vellum/workflows/ports/node_ports.py +75 -0
  137. vellum/workflows/ports/port.py +75 -0
  138. vellum/workflows/ports/utils.py +40 -0
  139. vellum/workflows/references/__init__.py +17 -0
  140. vellum/workflows/references/environment_variable.py +20 -0
  141. vellum/workflows/references/execution_count.py +20 -0
  142. vellum/workflows/references/external_input.py +49 -0
  143. vellum/workflows/references/input.py +7 -0
  144. vellum/workflows/references/lazy.py +55 -0
  145. vellum/workflows/references/node.py +43 -0
  146. vellum/workflows/references/output.py +78 -0
  147. vellum/workflows/references/state_value.py +23 -0
  148. vellum/workflows/references/vellum_secret.py +15 -0
  149. vellum/workflows/references/workflow_input.py +41 -0
  150. vellum/workflows/resolvers/__init__.py +5 -0
  151. vellum/workflows/resolvers/base.py +15 -0
  152. vellum/workflows/runner/__init__.py +5 -0
  153. vellum/workflows/runner/runner.py +588 -0
  154. vellum/workflows/runner/types.py +18 -0
  155. vellum/workflows/state/__init__.py +5 -0
  156. vellum/workflows/state/base.py +327 -0
  157. vellum/workflows/state/context.py +18 -0
  158. vellum/workflows/state/encoder.py +57 -0
  159. vellum/workflows/state/store.py +28 -0
  160. vellum/workflows/state/tests/__init__.py +0 -0
  161. vellum/workflows/state/tests/test_state.py +113 -0
  162. vellum/workflows/types/__init__.py +0 -0
  163. vellum/workflows/types/core.py +91 -0
  164. vellum/workflows/types/generics.py +14 -0
  165. vellum/workflows/types/stack.py +39 -0
  166. vellum/workflows/types/tests/__init__.py +0 -0
  167. vellum/workflows/types/tests/test_utils.py +76 -0
  168. vellum/workflows/types/utils.py +164 -0
  169. vellum/workflows/utils/__init__.py +0 -0
  170. vellum/workflows/utils/names.py +13 -0
  171. vellum/workflows/utils/tests/__init__.py +0 -0
  172. vellum/workflows/utils/tests/test_names.py +15 -0
  173. vellum/workflows/utils/tests/test_vellum_variables.py +25 -0
  174. vellum/workflows/utils/vellum_variables.py +81 -0
  175. vellum/workflows/vellum_client.py +18 -0
  176. vellum/workflows/workflows/__init__.py +5 -0
  177. vellum/workflows/workflows/base.py +365 -0
  178. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.10.0.dist-info}/METADATA +2 -1
  179. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.10.0.dist-info}/RECORD +245 -7
  180. vellum_cli/__init__.py +72 -0
  181. vellum_cli/aliased_group.py +103 -0
  182. vellum_cli/config.py +96 -0
  183. vellum_cli/image_push.py +112 -0
  184. vellum_cli/logger.py +36 -0
  185. vellum_cli/pull.py +73 -0
  186. vellum_cli/push.py +121 -0
  187. vellum_cli/tests/test_config.py +100 -0
  188. vellum_cli/tests/test_pull.py +152 -0
  189. vellum_ee/workflows/__init__.py +0 -0
  190. vellum_ee/workflows/display/__init__.py +0 -0
  191. vellum_ee/workflows/display/base.py +73 -0
  192. vellum_ee/workflows/display/nodes/__init__.py +4 -0
  193. vellum_ee/workflows/display/nodes/base_node_display.py +116 -0
  194. vellum_ee/workflows/display/nodes/base_node_vellum_display.py +36 -0
  195. vellum_ee/workflows/display/nodes/get_node_display_class.py +25 -0
  196. vellum_ee/workflows/display/nodes/tests/__init__.py +0 -0
  197. vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +47 -0
  198. vellum_ee/workflows/display/nodes/types.py +18 -0
  199. vellum_ee/workflows/display/nodes/utils.py +33 -0
  200. vellum_ee/workflows/display/nodes/vellum/__init__.py +32 -0
  201. vellum_ee/workflows/display/nodes/vellum/api_node.py +205 -0
  202. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +71 -0
  203. vellum_ee/workflows/display/nodes/vellum/conditional_node.py +217 -0
  204. vellum_ee/workflows/display/nodes/vellum/final_output_node.py +61 -0
  205. vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +49 -0
  206. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +170 -0
  207. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +99 -0
  208. vellum_ee/workflows/display/nodes/vellum/map_node.py +100 -0
  209. vellum_ee/workflows/display/nodes/vellum/merge_node.py +48 -0
  210. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +68 -0
  211. vellum_ee/workflows/display/nodes/vellum/search_node.py +193 -0
  212. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +58 -0
  213. vellum_ee/workflows/display/nodes/vellum/templating_node.py +67 -0
  214. vellum_ee/workflows/display/nodes/vellum/tests/__init__.py +0 -0
  215. vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +106 -0
  216. vellum_ee/workflows/display/nodes/vellum/try_node.py +38 -0
  217. vellum_ee/workflows/display/nodes/vellum/utils.py +76 -0
  218. vellum_ee/workflows/display/tests/__init__.py +0 -0
  219. vellum_ee/workflows/display/tests/workflow_serialization/__init__.py +0 -0
  220. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +426 -0
  221. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +607 -0
  222. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +1175 -0
  223. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +235 -0
  224. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +511 -0
  225. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +372 -0
  226. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +272 -0
  227. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +289 -0
  228. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +354 -0
  229. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +123 -0
  230. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +84 -0
  231. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +233 -0
  232. vellum_ee/workflows/display/types.py +46 -0
  233. vellum_ee/workflows/display/utils/__init__.py +0 -0
  234. vellum_ee/workflows/display/utils/tests/__init__.py +0 -0
  235. vellum_ee/workflows/display/utils/tests/test_uuids.py +16 -0
  236. vellum_ee/workflows/display/utils/uuids.py +24 -0
  237. vellum_ee/workflows/display/utils/vellum.py +121 -0
  238. vellum_ee/workflows/display/vellum.py +357 -0
  239. vellum_ee/workflows/display/workflows/__init__.py +5 -0
  240. vellum_ee/workflows/display/workflows/base_workflow_display.py +302 -0
  241. vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +32 -0
  242. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +386 -0
  243. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.10.0.dist-info}/LICENSE +0 -0
  244. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.10.0.dist-info}/WHEEL +0 -0
  245. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.10.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,90 @@
1
+ <p align="center">
2
+ <h1 align="center">
3
+ Vellum Workflows SDK
4
+ </h1>
5
+ <p align="center">
6
+ <a href="https://docs.vellum.ai/developers/workflows-sdk">Learn more</a>
7
+ ·
8
+ <a href="https://www.vellum.ai/landing-pages/request-demo">Talk to us</a>
9
+ </p>
10
+ </p>
11
+
12
+ # Introduction
13
+
14
+ The Vellum Workflows SDK is a framework for defining and executing complex AI systems as graphs.
15
+
16
+ The Vellum Workflows SDK provides a declarative python syntax for both defining and executing the control flow of graphs.
17
+ Unlike other graph execution frameworks, which are functional or event-driven in nature, Vellum's Workflows SDK defines the control flow of a graph
18
+ statically and makes use of strict typing. This means that the structure of the graph is known ahead of time, and you get all the benefits of type
19
+ safety and intellisense. This ultimately makes it easier to build more predictable and robust AI systems.
20
+
21
+ Unique amongst other frameworks, Vellum's Workflows SDK also allows you to _visualize, edit, and execute_ your graph in a UI, pushing and pulling changes from
22
+ code to UI and vice versa.
23
+
24
+
25
+ ## Core Features
26
+ - **Nodes**: Nodes are the basic building blocks of a graph. They represent a single task or function that can be executed.
27
+ - **Graph Syntax**: An intuitive declarative syntax for defining the control flow of a graph.
28
+ - **Inputs and Outputs**: Both the Workflow itslef and individual Nodes can take in inputs and produce outputs, which can be used to pass information between nodes.
29
+ - **State**: Nodes can read and write to the graph's global state, which can be used to share information between nodes without defining explicit inputs and outputs.
30
+ - **Advanced Control Flow**: Support for looping, conditionals, paralellism, state forking, and more.
31
+ - **Streaming**: Nodes can stream output values back to the runner, allowing for long-running tasks like chat completions to return partial results.
32
+ - **Human-in-the-loop**: Nodes can wait for External Inputs, allowing for a pause in the Workflow until a human or external system provides input.
33
+ - **UI Integration**: Push and pull changes from code to Vellum's UI and vice versa, allowing for rapid testing and iteration.
34
+
35
+ ## Quickstart
36
+
37
+ 1. Install the Vellum Workflows SDK:
38
+
39
+ ```bash
40
+ pip install vellum-ai
41
+ ```
42
+
43
+ 2. Import the Vellum Workflows SDK and define your first Workflow:
44
+
45
+ ```python
46
+ # my_workflow.py
47
+
48
+ from workflows import BaseWorkflow
49
+ from workflows.nodes import BaseNode
50
+
51
+
52
+ class MyNode(BaseNode):
53
+
54
+ class Outputs(BaseNode.Outputs):
55
+ result: str
56
+
57
+ def run(self):
58
+ return self.Outputs(result="Hello, World!")
59
+
60
+
61
+ class MyWorkflow(BaseWorkflow):
62
+ graph = MyNode
63
+
64
+ class Outputs(BaseWorkflow.Outputs):
65
+ result = MyNode.Outputs.result
66
+
67
+
68
+ if __name__ == "__main__":
69
+ workflow = MyWorkflow()
70
+ result = workflow.run()
71
+
72
+ print(result.outputs.result)
73
+
74
+ ```
75
+ 3. Run it!
76
+
77
+ ```bash
78
+ python my_workflow.py
79
+
80
+ Note: To use most out-of-box Nodes, and to push/pull to/from the Velłum UI, you'll need a Vellum account and API key. [Talk to us](https://www.vellum.ai/landing-pages/request-demo) or visit our [pricing page](https://www.vellum.ai/pricing) for more information.
81
+
82
+
83
+ ## Documentation
84
+ Complete documentation for the Vellum Workflows SDK can be found at https://docs.vellum.ai/developers/workflows-sdk.
85
+
86
+
87
+ ## Stability
88
+
89
+ This SDK is currently in <Availability type="beta" /> and is subject to change. If you'd like to pariticpate in
90
+ our beta program, please [contact us](https://docs.vellum.ai/home/getting-started/support).
@@ -0,0 +1,5 @@
1
+ from .workflows import BaseWorkflow
2
+
3
+ __all__ = [
4
+ "BaseWorkflow",
5
+ ]
@@ -0,0 +1,43 @@
1
+ from enum import Enum
2
+ from typing import Any, cast
3
+
4
+
5
+ class _UndefMeta(type):
6
+ def __repr__(cls) -> str:
7
+ return "UNDEF"
8
+
9
+ def __getattribute__(cls, name: str) -> Any:
10
+ if name == "__class__":
11
+ # ensures that UNDEF.__class__ == UNDEF
12
+ return cls
13
+
14
+ return super().__getattribute__(name)
15
+
16
+ def __bool__(cls) -> bool:
17
+ return False
18
+
19
+
20
+ class UNDEF(metaclass=_UndefMeta):
21
+ pass
22
+
23
+
24
+ LATEST_RELEASE_TAG = "LATEST"
25
+
26
+ OMIT = cast(Any, ...)
27
+
28
+
29
+ class APIRequestMethod(Enum):
30
+ GET = "GET"
31
+ POST = "POST"
32
+ PUT = "PUT"
33
+ DELETE = "DELETE"
34
+ PATCH = "PATCH"
35
+ OPTIONS = "OPTIONS"
36
+ HEAD = "HEAD"
37
+ CONNECT = "CONNECT"
38
+ TRACE = "TRACE"
39
+
40
+
41
+ class AuthorizationType(Enum):
42
+ BEARER_TOKEN = "BEARER_TOKEN"
43
+ API_KEY = "API_KEY"
File without changes
@@ -0,0 +1,339 @@
1
+ from typing import TYPE_CHECKING, Any, Generic, Optional, Tuple, Type, TypeVar, Union, cast, overload
2
+
3
+ if TYPE_CHECKING:
4
+ from vellum.workflows.expressions.accessor import AccessorExpression
5
+ from vellum.workflows.expressions.and_ import AndExpression
6
+ from vellum.workflows.expressions.begins_with import BeginsWithExpression
7
+ from vellum.workflows.expressions.between import BetweenExpression
8
+ from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
9
+ from vellum.workflows.expressions.contains import ContainsExpression
10
+ from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
11
+ from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
12
+ from vellum.workflows.expressions.does_not_end_with import DoesNotEndWithExpression
13
+ from vellum.workflows.expressions.does_not_equal import DoesNotEqualExpression
14
+ from vellum.workflows.expressions.ends_with import EndsWithExpression
15
+ from vellum.workflows.expressions.equals import EqualsExpression
16
+ from vellum.workflows.expressions.greater_than import GreaterThanExpression
17
+ from vellum.workflows.expressions.greater_than_or_equal_to import GreaterThanOrEqualToExpression
18
+ from vellum.workflows.expressions.in_ import InExpression
19
+ from vellum.workflows.expressions.is_blank import IsBlankExpression
20
+ from vellum.workflows.expressions.is_not_blank import IsNotBlankExpression
21
+ from vellum.workflows.expressions.is_not_null import IsNotNullExpression
22
+ from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpression
23
+ from vellum.workflows.expressions.is_null import IsNullExpression
24
+ from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
25
+ from vellum.workflows.expressions.less_than import LessThanExpression
26
+ from vellum.workflows.expressions.less_than_or_equal_to import LessThanOrEqualToExpression
27
+ from vellum.workflows.expressions.not_between import NotBetweenExpression
28
+ from vellum.workflows.expressions.not_in import NotInExpression
29
+ from vellum.workflows.expressions.or_ import OrExpression
30
+ from vellum.workflows.nodes.bases import BaseNode
31
+ from vellum.workflows.state.base import BaseState
32
+
33
+ _T = TypeVar("_T")
34
+ _O = TypeVar("_O")
35
+ _O2 = TypeVar("_O2")
36
+
37
+
38
+ class BaseDescriptor(Generic[_T]):
39
+ _name: str
40
+ _types: Tuple[Type[_T], ...]
41
+ _instance: Optional[_T]
42
+
43
+ def __init__(self, *, name: str, types: Tuple[Type[_T], ...], instance: Optional[_T] = None) -> None:
44
+ self._name = name
45
+ self._types = types
46
+ self._instance = instance
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return self._name
51
+
52
+ @property
53
+ def types(self) -> Tuple[Type[_T], ...]:
54
+ return self._types
55
+
56
+ @property
57
+ def instance(self) -> Optional[_T]:
58
+ return self._instance
59
+
60
+ def resolve(self, state: "BaseState") -> _T:
61
+ raise NotImplementedError("Descriptor must implement resolve method")
62
+
63
+ def __eq__(self, other: object) -> bool:
64
+ if not isinstance(other, type(self)):
65
+ return False
66
+ return self._name == other._name
67
+
68
+ def __hash__(self) -> int:
69
+ return hash(self._name)
70
+
71
+ @overload
72
+ def __get__(self, instance: "BaseNode", owner: Type["BaseNode"]) -> _T: ...
73
+
74
+ @overload
75
+ def __get__(self, instance: Any, owner: Optional[Type]) -> "BaseDescriptor[_T]": ...
76
+
77
+ def __get__(self, instance: Any, owner: Optional[Type]) -> Union["BaseDescriptor[_T]", _T]:
78
+ if not instance:
79
+ return self
80
+
81
+ if instance.__class__.__name__ == "BaseNode":
82
+ node = cast("BaseNode", instance)
83
+ return self.resolve(node.state)
84
+
85
+ return self
86
+
87
+ # TODO: We should figure out how to remove these dynamic imports
88
+ # https://app.shortcut.com/vellum/story/4504
89
+ @overload
90
+ def __or__(self, other: "BaseDescriptor[_O]") -> "OrExpression[_T, _O]": ...
91
+
92
+ @overload
93
+ def __or__(self, other: _O) -> "OrExpression[_T, _O]": ...
94
+
95
+ def __or__(self, other: "Union[BaseDescriptor[_O], _O]") -> "OrExpression[_T, _O]":
96
+ from vellum.workflows.expressions.or_ import OrExpression
97
+
98
+ return OrExpression(lhs=self, rhs=other)
99
+
100
+ @overload
101
+ def __and__(self, other: "BaseDescriptor[_O]") -> "AndExpression[_T, _O]": ...
102
+
103
+ @overload
104
+ def __and__(self, other: _O) -> "AndExpression[_T, _O]": ...
105
+
106
+ def __and__(self, other: "Union[BaseDescriptor[_O], _O]") -> "AndExpression[_T, _O]":
107
+ from vellum.workflows.expressions.and_ import AndExpression
108
+
109
+ return AndExpression(lhs=self, rhs=other)
110
+
111
+ @overload
112
+ def coalesce(self, other: "BaseDescriptor[_O]") -> "CoalesceExpression[_T, _O]": ...
113
+
114
+ @overload
115
+ def coalesce(self, other: _O) -> "CoalesceExpression[_T, _O]": ...
116
+
117
+ def coalesce(self, other: "Union[BaseDescriptor[_O], _O]") -> "CoalesceExpression[_T, _O]":
118
+ from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
119
+
120
+ return CoalesceExpression(lhs=self, rhs=other)
121
+
122
+ def __getitem__(self, field: str) -> "AccessorExpression":
123
+ from vellum.workflows.expressions.accessor import AccessorExpression
124
+
125
+ return AccessorExpression(base=self, field=field)
126
+
127
+ @overload
128
+ def equals(self, other: "BaseDescriptor[_O]") -> "EqualsExpression[_T, _O]": ...
129
+
130
+ @overload
131
+ def equals(self, other: _O) -> "EqualsExpression[_T, _O]": ...
132
+
133
+ def equals(self, other: "Union[BaseDescriptor[_O], _O]") -> "EqualsExpression[_T, _O]":
134
+ from vellum.workflows.expressions.equals import EqualsExpression
135
+
136
+ return EqualsExpression(lhs=self, rhs=other)
137
+
138
+ @overload
139
+ def does_not_equal(self, other: "BaseDescriptor[_O]") -> "DoesNotEqualExpression[_T, _O]": ...
140
+
141
+ @overload
142
+ def does_not_equal(self, other: _O) -> "DoesNotEqualExpression[_T, _O]": ...
143
+
144
+ def does_not_equal(self, other: "Union[BaseDescriptor[_O], _O]") -> "DoesNotEqualExpression[_T, _O]":
145
+ from vellum.workflows.expressions.does_not_equal import DoesNotEqualExpression
146
+
147
+ return DoesNotEqualExpression(lhs=self, rhs=other)
148
+
149
+ @overload
150
+ def less_than(self, other: "BaseDescriptor[_O]") -> "LessThanExpression[_T, _O]": ...
151
+
152
+ @overload
153
+ def less_than(self, other: _O) -> "LessThanExpression[_T, _O]": ...
154
+
155
+ def less_than(self, other: "Union[BaseDescriptor[_O], _O]") -> "LessThanExpression[_T, _O]":
156
+ from vellum.workflows.expressions.less_than import LessThanExpression
157
+
158
+ return LessThanExpression(lhs=self, rhs=other)
159
+
160
+ @overload
161
+ def greater_than(self, other: "BaseDescriptor[_O]") -> "GreaterThanExpression[_T, _O]": ...
162
+
163
+ @overload
164
+ def greater_than(self, other: _O) -> "GreaterThanExpression[_T, _O]": ...
165
+
166
+ def greater_than(self, other: "Union[BaseDescriptor[_O], _O]") -> "GreaterThanExpression[_T, _O]":
167
+ from vellum.workflows.expressions.greater_than import GreaterThanExpression
168
+
169
+ return GreaterThanExpression(lhs=self, rhs=other)
170
+
171
+ @overload
172
+ def less_than_or_equal_to(self, other: "BaseDescriptor[_O]") -> "LessThanOrEqualToExpression[_T, _O]": ...
173
+
174
+ @overload
175
+ def less_than_or_equal_to(self, other: _O) -> "LessThanOrEqualToExpression[_T, _O]": ...
176
+
177
+ def less_than_or_equal_to(self, other: "Union[BaseDescriptor[_O], _O]") -> "LessThanOrEqualToExpression[_T, _O]":
178
+ from vellum.workflows.expressions.less_than_or_equal_to import LessThanOrEqualToExpression
179
+
180
+ return LessThanOrEqualToExpression(lhs=self, rhs=other)
181
+
182
+ @overload
183
+ def greater_than_or_equal_to(self, other: "BaseDescriptor[_O]") -> "GreaterThanOrEqualToExpression[_T, _O]": ...
184
+
185
+ @overload
186
+ def greater_than_or_equal_to(self, other: _O) -> "GreaterThanOrEqualToExpression[_T, _O]": ...
187
+
188
+ def greater_than_or_equal_to(
189
+ self, other: "Union[BaseDescriptor[_O], _O]"
190
+ ) -> "GreaterThanOrEqualToExpression[_T, _O]":
191
+ from vellum.workflows.expressions.greater_than_or_equal_to import GreaterThanOrEqualToExpression
192
+
193
+ return GreaterThanOrEqualToExpression(lhs=self, rhs=other)
194
+
195
+ @overload
196
+ def contains(self, other: "BaseDescriptor[_O]") -> "ContainsExpression[_T, _O]": ...
197
+
198
+ @overload
199
+ def contains(self, other: _O) -> "ContainsExpression[_T, _O]": ...
200
+
201
+ def contains(self, other: "Union[BaseDescriptor[_O], _O]") -> "ContainsExpression[_T, _O]":
202
+ from vellum.workflows.expressions.contains import ContainsExpression
203
+
204
+ return ContainsExpression(lhs=self, rhs=other)
205
+
206
+ @overload
207
+ def begins_with(self, other: "BaseDescriptor[_O]") -> "BeginsWithExpression[_T, _O]": ...
208
+
209
+ @overload
210
+ def begins_with(self, other: _O) -> "BeginsWithExpression[_T, _O]": ...
211
+
212
+ def begins_with(self, other: "Union[BaseDescriptor[_O], _O]") -> "BeginsWithExpression[_T, _O]":
213
+ from vellum.workflows.expressions.begins_with import BeginsWithExpression
214
+
215
+ return BeginsWithExpression(lhs=self, rhs=other)
216
+
217
+ @overload
218
+ def ends_with(self, other: "BaseDescriptor[_O]") -> "EndsWithExpression[_T, _O]": ...
219
+
220
+ @overload
221
+ def ends_with(self, other: _O) -> "EndsWithExpression[_T, _O]": ...
222
+
223
+ def ends_with(self, other: "Union[BaseDescriptor[_O], _O]") -> "EndsWithExpression[_T, _O]":
224
+ from vellum.workflows.expressions.ends_with import EndsWithExpression
225
+
226
+ return EndsWithExpression(lhs=self, rhs=other)
227
+
228
+ @overload
229
+ def does_not_contain(self, other: "BaseDescriptor[_O]") -> "DoesNotContainExpression[_T, _O]": ...
230
+
231
+ @overload
232
+ def does_not_contain(self, other: _O) -> "DoesNotContainExpression[_T, _O]": ...
233
+
234
+ def does_not_contain(self, other: "Union[BaseDescriptor[_O], _O]") -> "DoesNotContainExpression[_T, _O]":
235
+ from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
236
+
237
+ return DoesNotContainExpression(lhs=self, rhs=other)
238
+
239
+ @overload
240
+ def does_not_begin_with(self, other: "BaseDescriptor[_O]") -> "DoesNotBeginWithExpression[_T, _O]": ...
241
+
242
+ @overload
243
+ def does_not_begin_with(self, other: _O) -> "DoesNotBeginWithExpression[_T, _O]": ...
244
+
245
+ def does_not_begin_with(self, other: "Union[BaseDescriptor[_O], _O]") -> "DoesNotBeginWithExpression[_T, _O]":
246
+ from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
247
+
248
+ return DoesNotBeginWithExpression(lhs=self, rhs=other)
249
+
250
+ @overload
251
+ def does_not_end_with(self, other: "BaseDescriptor[_O]") -> "DoesNotEndWithExpression[_T, _O]": ...
252
+
253
+ @overload
254
+ def does_not_end_with(self, other: _O) -> "DoesNotEndWithExpression[_T, _O]": ...
255
+
256
+ def does_not_end_with(self, other: "Union[BaseDescriptor[_O], _O]") -> "DoesNotEndWithExpression[_T, _O]":
257
+ from vellum.workflows.expressions.does_not_end_with import DoesNotEndWithExpression
258
+
259
+ return DoesNotEndWithExpression(lhs=self, rhs=other)
260
+
261
+ def is_none(self) -> "IsNullExpression":
262
+ from vellum.workflows.expressions.is_null import IsNullExpression
263
+
264
+ return IsNullExpression(expression=self)
265
+
266
+ def is_not_none(self) -> "IsNotNullExpression":
267
+ from vellum.workflows.expressions.is_not_null import IsNotNullExpression
268
+
269
+ return IsNotNullExpression(expression=self)
270
+
271
+ def is_undefined(self) -> "IsUndefinedExpression":
272
+ from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
273
+
274
+ return IsUndefinedExpression(expression=self)
275
+
276
+ def is_not_undefined(self) -> "IsNotUndefinedExpression":
277
+ from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpression
278
+
279
+ return IsNotUndefinedExpression(expression=self)
280
+
281
+ @overload
282
+ def in_(self, other: "BaseDescriptor[_O]") -> "InExpression[_T, _O]": ...
283
+
284
+ @overload
285
+ def in_(self, other: _O) -> "InExpression[_T, _O]": ...
286
+
287
+ def in_(self, other: "Union[BaseDescriptor[_O], _O]") -> "InExpression[_T, _O]":
288
+ from vellum.workflows.expressions.in_ import InExpression
289
+
290
+ return InExpression(lhs=self, rhs=other)
291
+
292
+ @overload
293
+ def not_in(self, other: "BaseDescriptor[_O]") -> "NotInExpression[_T, _O]": ...
294
+
295
+ @overload
296
+ def not_in(self, other: _O) -> "NotInExpression[_T, _O]": ...
297
+
298
+ def not_in(self, other: "Union[BaseDescriptor[_O], _O]") -> "NotInExpression[_T, _O]":
299
+ from vellum.workflows.expressions.not_in import NotInExpression
300
+
301
+ return NotInExpression(lhs=self, rhs=other)
302
+
303
+ @overload
304
+ def between(self, start: "BaseDescriptor[_O]", end: "BaseDescriptor[_O2]") -> "BetweenExpression[_T, _O, _O2]": ...
305
+
306
+ @overload
307
+ def between(self, start: _O, end: _O2) -> "BetweenExpression[_T, _O, _O2]": ...
308
+
309
+ def between(
310
+ self, start: "Union[BaseDescriptor[_O], _O]", end: "Union[BaseDescriptor[_O2], _O2]"
311
+ ) -> "BetweenExpression[_T, _O, _O2]":
312
+ from vellum.workflows.expressions.between import BetweenExpression
313
+
314
+ return BetweenExpression(value=self, start=start, end=end)
315
+
316
+ @overload
317
+ def not_between(
318
+ self, start: "BaseDescriptor[_O]", end: "BaseDescriptor[_O2]"
319
+ ) -> "NotBetweenExpression[_T, _O, _O2]": ...
320
+
321
+ @overload
322
+ def not_between(self, start: _O, end: _O2) -> "NotBetweenExpression[_T, _O, _O2]": ...
323
+
324
+ def not_between(
325
+ self, start: "Union[BaseDescriptor[_O], _O]", end: "Union[BaseDescriptor[_O2], _O2]"
326
+ ) -> "NotBetweenExpression[_T, _O, _O2]":
327
+ from vellum.workflows.expressions.not_between import NotBetweenExpression
328
+
329
+ return NotBetweenExpression(value=self, start=start, end=end)
330
+
331
+ def is_blank(self) -> "IsBlankExpression":
332
+ from vellum.workflows.expressions.is_blank import IsBlankExpression
333
+
334
+ return IsBlankExpression(expression=self)
335
+
336
+ def is_not_blank(self) -> "IsNotBlankExpression":
337
+ from vellum.workflows.expressions.is_not_blank import IsNotBlankExpression
338
+
339
+ return IsNotBlankExpression(expression=self)
@@ -0,0 +1,83 @@
1
+ import pytest
2
+
3
+ from vellum.workflows.descriptors.utils import resolve_value
4
+ from vellum.workflows.state.base import BaseState
5
+
6
+
7
+ class FixtureState(BaseState):
8
+ alpha = 1
9
+ beta = 2
10
+
11
+ gamma = "hello"
12
+ delta = "el"
13
+
14
+ epsilon = 3
15
+ zeta = {
16
+ "foo": "bar",
17
+ }
18
+
19
+
20
+ @pytest.mark.parametrize(
21
+ ["descriptor", "expected_value"],
22
+ [
23
+ (FixtureState.alpha | FixtureState.beta, 1),
24
+ (FixtureState.alpha & FixtureState.beta, 2),
25
+ (FixtureState.beta.coalesce(FixtureState.alpha), 2),
26
+ (FixtureState.alpha.equals(FixtureState.beta), False),
27
+ (FixtureState.alpha.does_not_equal(FixtureState.beta), True),
28
+ (FixtureState.alpha.less_than(FixtureState.beta), True),
29
+ (FixtureState.alpha.greater_than(FixtureState.beta), False),
30
+ (FixtureState.alpha.less_than_or_equal_to(FixtureState.beta), True),
31
+ (FixtureState.alpha.greater_than_or_equal_to(FixtureState.beta), False),
32
+ (FixtureState.gamma.contains(FixtureState.delta), True),
33
+ (FixtureState.gamma.begins_with(FixtureState.delta), False),
34
+ (FixtureState.gamma.ends_with(FixtureState.delta), False),
35
+ (FixtureState.gamma.does_not_contain(FixtureState.delta), False),
36
+ (FixtureState.gamma.does_not_begin_with(FixtureState.delta), True),
37
+ (FixtureState.gamma.does_not_end_with(FixtureState.delta), True),
38
+ (FixtureState.alpha.is_none(), False),
39
+ (FixtureState.alpha.is_not_none(), True),
40
+ (FixtureState.delta.in_(FixtureState.gamma), True),
41
+ (FixtureState.delta.not_in(FixtureState.gamma), False),
42
+ (FixtureState.alpha.between(FixtureState.beta, FixtureState.epsilon), False),
43
+ (FixtureState.alpha.not_between(FixtureState.beta, FixtureState.epsilon), True),
44
+ (FixtureState.delta.is_blank(), False),
45
+ (FixtureState.delta.is_not_blank(), True),
46
+ (
47
+ FixtureState.alpha.equals(FixtureState.alpha)
48
+ | FixtureState.alpha.equals(FixtureState.beta) & FixtureState.alpha.equals(FixtureState.beta),
49
+ True,
50
+ ),
51
+ (FixtureState.zeta["foo"], "bar"),
52
+ ],
53
+ ids=[
54
+ "or",
55
+ "and",
56
+ "coalesce",
57
+ "eq",
58
+ "dne",
59
+ "less_than",
60
+ "greater_than",
61
+ "less_than_or_equal_to",
62
+ "greater_than_or_equal_to",
63
+ "contains",
64
+ "begins_with",
65
+ "ends_with",
66
+ "does_not_contain",
67
+ "does_not_begin_with",
68
+ "does_not_end_with",
69
+ "is_none",
70
+ "is_not_none",
71
+ "in_",
72
+ "not_in",
73
+ "between",
74
+ "not_between",
75
+ "is_blank",
76
+ "is_not_blank",
77
+ "or_and",
78
+ "accessor",
79
+ ],
80
+ )
81
+ def test_resolve_value__happy_path(descriptor, expected_value):
82
+ actual_value = resolve_value(descriptor, FixtureState())
83
+ assert actual_value == expected_value
@@ -0,0 +1,90 @@
1
+ from collections.abc import Mapping
2
+ import dataclasses
3
+ import inspect
4
+ from typing import Any, Dict, Optional, Sequence, Set, TypeVar, Union, cast, overload
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from vellum.workflows.descriptors.base import BaseDescriptor
9
+ from vellum.workflows.state.base import BaseState
10
+
11
+ _T = TypeVar("_T")
12
+
13
+
14
+ @overload
15
+ def resolve_value(
16
+ value: BaseDescriptor[_T], state: BaseState, path: str = "", memo: Optional[Dict[str, Any]] = None
17
+ ) -> _T: ...
18
+
19
+
20
+ @overload
21
+ def resolve_value(value: _T, state: BaseState, path: str = "", memo: Optional[Dict[str, Any]] = None) -> _T: ...
22
+
23
+
24
+ def resolve_value(
25
+ value: Union[BaseDescriptor[_T], _T], state: BaseState, path: str = "", memo: Optional[Dict[str, Any]] = None
26
+ ) -> _T:
27
+ """
28
+ Recursively resolves Descriptor's until we have a constant value, using BaseState.
29
+
30
+ The nonideal casts in this method are due to the `isinstance` calls detaching types
31
+ from the `_T` generic.
32
+ """
33
+
34
+ if inspect.isclass(value):
35
+ return cast(_T, value)
36
+
37
+ if isinstance(value, BaseDescriptor):
38
+ resolved_value = value.resolve(state)
39
+ if memo is not None:
40
+ memo[path] = resolved_value
41
+ return resolved_value
42
+
43
+ if isinstance(value, property) or callable(value):
44
+ return cast(_T, value)
45
+
46
+ if isinstance(value, (str, bytes)):
47
+ return cast(_T, value)
48
+
49
+ if dataclasses.is_dataclass(value):
50
+ # The `inspect.isclass` check above prevents `value` from being a class.
51
+ dataclass_value = dataclasses.replace( # type: ignore[type-var]
52
+ value,
53
+ **{
54
+ field.name: resolve_value(getattr(value, field.name), state, path=f"{path}.{field.name}", memo=memo)
55
+ for field in dataclasses.fields(value)
56
+ },
57
+ )
58
+ return cast(_T, dataclass_value)
59
+
60
+ if isinstance(value, BaseModel):
61
+ pydantic_value = value.model_copy(
62
+ update={
63
+ key: resolve_value(getattr(value, key), state, path=f"{path}.{key}", memo=memo)
64
+ for key in value.dict().keys()
65
+ }
66
+ )
67
+ return cast(_T, pydantic_value)
68
+
69
+ if isinstance(value, Mapping):
70
+ mapped_value = type(value)( # type: ignore[call-arg]
71
+ {
72
+ dict_key: resolve_value(dict_value, state, path=f"{path}.{dict_key}", memo=memo)
73
+ for dict_key, dict_value in value.items()
74
+ }
75
+ )
76
+ return cast(_T, mapped_value)
77
+
78
+ if isinstance(value, Sequence):
79
+ sequence_value = type(value)(
80
+ resolve_value(seq_value, state, path=f"{path}.{index}", memo=memo) for index, seq_value in enumerate(value)
81
+ ) # type: ignore[call-arg]
82
+ return cast(_T, sequence_value)
83
+
84
+ if isinstance(value, Set):
85
+ set_value = type(value)(
86
+ resolve_value(set_value, state, path=f"{path}.{index}", memo=memo) for index, set_value in enumerate(value)
87
+ )
88
+ return cast(_T, set_value)
89
+
90
+ return value
@@ -0,0 +1,5 @@
1
+ from .edge import Edge
2
+
3
+ __all__ = [
4
+ "Edge",
5
+ ]
@@ -0,0 +1,23 @@
1
+ from typing import TYPE_CHECKING, Any, Type
2
+
3
+ if TYPE_CHECKING:
4
+ from vellum.workflows.nodes.bases import BaseNode
5
+ from vellum.workflows.ports.port import Port
6
+
7
+
8
+ class Edge:
9
+ def __init__(self, from_port: "Port", to_node: Type["BaseNode"]):
10
+ self.from_port = from_port
11
+ self.to_node = to_node
12
+
13
+ def __eq__(self, other: Any) -> bool:
14
+ if not isinstance(other, Edge):
15
+ return False
16
+
17
+ return self.from_port == other.from_port and self.to_node == other.to_node
18
+
19
+ def __hash__(self) -> int:
20
+ return hash((self.from_port, self.to_node))
21
+
22
+ def __repr__(self) -> str:
23
+ return f"{self.from_port} >> {self.to_node}"