lionagi 0.0.312__py3-none-any.whl → 0.2.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 (268) hide show
  1. lionagi/__init__.py +61 -3
  2. lionagi/core/__init__.py +0 -14
  3. lionagi/core/_setting/_setting.py +59 -0
  4. lionagi/core/action/__init__.py +14 -0
  5. lionagi/core/action/function_calling.py +136 -0
  6. lionagi/core/action/manual.py +1 -0
  7. lionagi/core/action/node.py +109 -0
  8. lionagi/core/action/tool.py +114 -0
  9. lionagi/core/action/tool_manager.py +356 -0
  10. lionagi/core/agent/__init__.py +0 -3
  11. lionagi/core/agent/base_agent.py +45 -36
  12. lionagi/core/agent/eval/evaluator.py +1 -0
  13. lionagi/core/agent/eval/vote.py +40 -0
  14. lionagi/core/agent/learn/learner.py +59 -0
  15. lionagi/core/agent/plan/unit_template.py +1 -0
  16. lionagi/core/collections/__init__.py +17 -0
  17. lionagi/core/collections/_logger.py +319 -0
  18. lionagi/core/collections/abc/__init__.py +53 -0
  19. lionagi/core/collections/abc/component.py +615 -0
  20. lionagi/core/collections/abc/concepts.py +297 -0
  21. lionagi/core/collections/abc/exceptions.py +150 -0
  22. lionagi/core/collections/abc/util.py +45 -0
  23. lionagi/core/collections/exchange.py +161 -0
  24. lionagi/core/collections/flow.py +426 -0
  25. lionagi/core/collections/model.py +419 -0
  26. lionagi/core/collections/pile.py +913 -0
  27. lionagi/core/collections/progression.py +236 -0
  28. lionagi/core/collections/util.py +64 -0
  29. lionagi/core/director/direct.py +314 -0
  30. lionagi/core/director/director.py +2 -0
  31. lionagi/core/engine/branch_engine.py +333 -0
  32. lionagi/core/engine/instruction_map_engine.py +204 -0
  33. lionagi/core/engine/sandbox_.py +14 -0
  34. lionagi/core/engine/script_engine.py +99 -0
  35. lionagi/core/executor/base_executor.py +90 -0
  36. lionagi/core/executor/graph_executor.py +330 -0
  37. lionagi/core/executor/neo4j_executor.py +384 -0
  38. lionagi/core/generic/__init__.py +7 -0
  39. lionagi/core/generic/edge.py +112 -0
  40. lionagi/core/generic/edge_condition.py +16 -0
  41. lionagi/core/generic/graph.py +236 -0
  42. lionagi/core/generic/hyperedge.py +1 -0
  43. lionagi/core/generic/node.py +220 -0
  44. lionagi/core/generic/tree.py +48 -0
  45. lionagi/core/generic/tree_node.py +79 -0
  46. lionagi/core/mail/__init__.py +7 -3
  47. lionagi/core/mail/mail.py +25 -0
  48. lionagi/core/mail/mail_manager.py +142 -58
  49. lionagi/core/mail/package.py +45 -0
  50. lionagi/core/mail/start_mail.py +36 -0
  51. lionagi/core/message/__init__.py +19 -0
  52. lionagi/core/message/action_request.py +133 -0
  53. lionagi/core/message/action_response.py +135 -0
  54. lionagi/core/message/assistant_response.py +95 -0
  55. lionagi/core/message/instruction.py +234 -0
  56. lionagi/core/message/message.py +101 -0
  57. lionagi/core/message/system.py +86 -0
  58. lionagi/core/message/util.py +283 -0
  59. lionagi/core/report/__init__.py +4 -0
  60. lionagi/core/report/base.py +217 -0
  61. lionagi/core/report/form.py +231 -0
  62. lionagi/core/report/report.py +166 -0
  63. lionagi/core/report/util.py +28 -0
  64. lionagi/core/rule/__init__.py +0 -0
  65. lionagi/core/rule/_default.py +16 -0
  66. lionagi/core/rule/action.py +99 -0
  67. lionagi/core/rule/base.py +238 -0
  68. lionagi/core/rule/boolean.py +56 -0
  69. lionagi/core/rule/choice.py +47 -0
  70. lionagi/core/rule/mapping.py +96 -0
  71. lionagi/core/rule/number.py +71 -0
  72. lionagi/core/rule/rulebook.py +109 -0
  73. lionagi/core/rule/string.py +52 -0
  74. lionagi/core/rule/util.py +35 -0
  75. lionagi/core/session/__init__.py +0 -3
  76. lionagi/core/session/branch.py +431 -0
  77. lionagi/core/session/directive_mixin.py +287 -0
  78. lionagi/core/session/session.py +230 -902
  79. lionagi/core/structure/__init__.py +1 -0
  80. lionagi/core/structure/chain.py +1 -0
  81. lionagi/core/structure/forest.py +1 -0
  82. lionagi/core/structure/graph.py +1 -0
  83. lionagi/core/structure/tree.py +1 -0
  84. lionagi/core/unit/__init__.py +5 -0
  85. lionagi/core/unit/parallel_unit.py +245 -0
  86. lionagi/core/unit/template/__init__.py +0 -0
  87. lionagi/core/unit/template/action.py +81 -0
  88. lionagi/core/unit/template/base.py +51 -0
  89. lionagi/core/unit/template/plan.py +84 -0
  90. lionagi/core/unit/template/predict.py +109 -0
  91. lionagi/core/unit/template/score.py +124 -0
  92. lionagi/core/unit/template/select.py +104 -0
  93. lionagi/core/unit/unit.py +362 -0
  94. lionagi/core/unit/unit_form.py +305 -0
  95. lionagi/core/unit/unit_mixin.py +1168 -0
  96. lionagi/core/unit/util.py +71 -0
  97. lionagi/core/validator/__init__.py +0 -0
  98. lionagi/core/validator/validator.py +364 -0
  99. lionagi/core/work/__init__.py +0 -0
  100. lionagi/core/work/work.py +76 -0
  101. lionagi/core/work/work_function.py +101 -0
  102. lionagi/core/work/work_queue.py +103 -0
  103. lionagi/core/work/worker.py +258 -0
  104. lionagi/core/work/worklog.py +120 -0
  105. lionagi/experimental/__init__.py +0 -0
  106. lionagi/experimental/compressor/__init__.py +0 -0
  107. lionagi/experimental/compressor/base.py +46 -0
  108. lionagi/experimental/compressor/llm_compressor.py +247 -0
  109. lionagi/experimental/compressor/llm_summarizer.py +61 -0
  110. lionagi/experimental/compressor/util.py +70 -0
  111. lionagi/experimental/directive/__init__.py +19 -0
  112. lionagi/experimental/directive/parser/__init__.py +0 -0
  113. lionagi/experimental/directive/parser/base_parser.py +282 -0
  114. lionagi/experimental/directive/template/__init__.py +0 -0
  115. lionagi/experimental/directive/template/base_template.py +79 -0
  116. lionagi/experimental/directive/template/schema.py +36 -0
  117. lionagi/experimental/directive/tokenizer.py +73 -0
  118. lionagi/experimental/evaluator/__init__.py +0 -0
  119. lionagi/experimental/evaluator/ast_evaluator.py +131 -0
  120. lionagi/experimental/evaluator/base_evaluator.py +218 -0
  121. lionagi/experimental/knowledge/__init__.py +0 -0
  122. lionagi/experimental/knowledge/base.py +10 -0
  123. lionagi/experimental/knowledge/graph.py +0 -0
  124. lionagi/experimental/memory/__init__.py +0 -0
  125. lionagi/experimental/strategies/__init__.py +0 -0
  126. lionagi/experimental/strategies/base.py +1 -0
  127. lionagi/integrations/bridge/autogen_/__init__.py +0 -0
  128. lionagi/integrations/bridge/autogen_/autogen_.py +124 -0
  129. lionagi/integrations/bridge/langchain_/documents.py +4 -0
  130. lionagi/integrations/bridge/llamaindex_/index.py +30 -0
  131. lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py +6 -0
  132. lionagi/integrations/bridge/llamaindex_/llama_pack.py +227 -0
  133. lionagi/integrations/bridge/llamaindex_/node_parser.py +6 -9
  134. lionagi/integrations/bridge/pydantic_/pydantic_bridge.py +1 -0
  135. lionagi/integrations/bridge/transformers_/__init__.py +0 -0
  136. lionagi/integrations/bridge/transformers_/install_.py +36 -0
  137. lionagi/integrations/chunker/__init__.py +0 -0
  138. lionagi/integrations/chunker/chunk.py +312 -0
  139. lionagi/integrations/config/oai_configs.py +38 -7
  140. lionagi/integrations/config/ollama_configs.py +1 -1
  141. lionagi/integrations/config/openrouter_configs.py +14 -2
  142. lionagi/integrations/loader/__init__.py +0 -0
  143. lionagi/integrations/loader/load.py +253 -0
  144. lionagi/integrations/loader/load_util.py +195 -0
  145. lionagi/integrations/provider/_mapping.py +46 -0
  146. lionagi/integrations/provider/litellm.py +2 -1
  147. lionagi/integrations/provider/mlx_service.py +16 -9
  148. lionagi/integrations/provider/oai.py +91 -4
  149. lionagi/integrations/provider/ollama.py +7 -6
  150. lionagi/integrations/provider/openrouter.py +115 -8
  151. lionagi/integrations/provider/services.py +2 -2
  152. lionagi/integrations/provider/transformers.py +18 -22
  153. lionagi/integrations/storage/__init__.py +3 -0
  154. lionagi/integrations/storage/neo4j.py +665 -0
  155. lionagi/integrations/storage/storage_util.py +287 -0
  156. lionagi/integrations/storage/structure_excel.py +285 -0
  157. lionagi/integrations/storage/to_csv.py +63 -0
  158. lionagi/integrations/storage/to_excel.py +83 -0
  159. lionagi/libs/__init__.py +26 -1
  160. lionagi/libs/ln_api.py +78 -23
  161. lionagi/libs/ln_context.py +37 -0
  162. lionagi/libs/ln_convert.py +21 -9
  163. lionagi/libs/ln_func_call.py +69 -28
  164. lionagi/libs/ln_image.py +107 -0
  165. lionagi/libs/ln_knowledge_graph.py +405 -0
  166. lionagi/libs/ln_nested.py +26 -11
  167. lionagi/libs/ln_parse.py +110 -14
  168. lionagi/libs/ln_queue.py +117 -0
  169. lionagi/libs/ln_tokenize.py +164 -0
  170. lionagi/{core/prompt/field_validator.py → libs/ln_validate.py} +79 -14
  171. lionagi/libs/special_tokens.py +172 -0
  172. lionagi/libs/sys_util.py +107 -2
  173. lionagi/lions/__init__.py +0 -0
  174. lionagi/lions/coder/__init__.py +0 -0
  175. lionagi/lions/coder/add_feature.py +20 -0
  176. lionagi/lions/coder/base_prompts.py +22 -0
  177. lionagi/lions/coder/code_form.py +13 -0
  178. lionagi/lions/coder/coder.py +168 -0
  179. lionagi/lions/coder/util.py +96 -0
  180. lionagi/lions/researcher/__init__.py +0 -0
  181. lionagi/lions/researcher/data_source/__init__.py +0 -0
  182. lionagi/lions/researcher/data_source/finhub_.py +191 -0
  183. lionagi/lions/researcher/data_source/google_.py +199 -0
  184. lionagi/lions/researcher/data_source/wiki_.py +96 -0
  185. lionagi/lions/researcher/data_source/yfinance_.py +21 -0
  186. lionagi/tests/integrations/__init__.py +0 -0
  187. lionagi/tests/libs/__init__.py +0 -0
  188. lionagi/tests/libs/test_field_validators.py +353 -0
  189. lionagi/tests/{test_libs → libs}/test_func_call.py +23 -21
  190. lionagi/tests/{test_libs → libs}/test_nested.py +36 -21
  191. lionagi/tests/{test_libs → libs}/test_parse.py +1 -1
  192. lionagi/tests/libs/test_queue.py +67 -0
  193. lionagi/tests/test_core/collections/__init__.py +0 -0
  194. lionagi/tests/test_core/collections/test_component.py +206 -0
  195. lionagi/tests/test_core/collections/test_exchange.py +138 -0
  196. lionagi/tests/test_core/collections/test_flow.py +145 -0
  197. lionagi/tests/test_core/collections/test_pile.py +171 -0
  198. lionagi/tests/test_core/collections/test_progression.py +129 -0
  199. lionagi/tests/test_core/generic/__init__.py +0 -0
  200. lionagi/tests/test_core/generic/test_edge.py +67 -0
  201. lionagi/tests/test_core/generic/test_graph.py +96 -0
  202. lionagi/tests/test_core/generic/test_node.py +106 -0
  203. lionagi/tests/test_core/generic/test_tree_node.py +73 -0
  204. lionagi/tests/test_core/test_branch.py +115 -292
  205. lionagi/tests/test_core/test_form.py +46 -0
  206. lionagi/tests/test_core/test_report.py +105 -0
  207. lionagi/tests/test_core/test_validator.py +111 -0
  208. lionagi/version.py +1 -1
  209. {lionagi-0.0.312.dist-info → lionagi-0.2.1.dist-info}/LICENSE +12 -11
  210. {lionagi-0.0.312.dist-info → lionagi-0.2.1.dist-info}/METADATA +19 -118
  211. lionagi-0.2.1.dist-info/RECORD +240 -0
  212. lionagi/core/branch/__init__.py +0 -4
  213. lionagi/core/branch/base_branch.py +0 -654
  214. lionagi/core/branch/branch.py +0 -471
  215. lionagi/core/branch/branch_flow_mixin.py +0 -96
  216. lionagi/core/branch/executable_branch.py +0 -347
  217. lionagi/core/branch/util.py +0 -323
  218. lionagi/core/direct/__init__.py +0 -6
  219. lionagi/core/direct/predict.py +0 -161
  220. lionagi/core/direct/score.py +0 -278
  221. lionagi/core/direct/select.py +0 -169
  222. lionagi/core/direct/utils.py +0 -87
  223. lionagi/core/direct/vote.py +0 -64
  224. lionagi/core/flow/base/baseflow.py +0 -23
  225. lionagi/core/flow/monoflow/ReAct.py +0 -238
  226. lionagi/core/flow/monoflow/__init__.py +0 -9
  227. lionagi/core/flow/monoflow/chat.py +0 -95
  228. lionagi/core/flow/monoflow/chat_mixin.py +0 -263
  229. lionagi/core/flow/monoflow/followup.py +0 -214
  230. lionagi/core/flow/polyflow/__init__.py +0 -1
  231. lionagi/core/flow/polyflow/chat.py +0 -248
  232. lionagi/core/mail/schema.py +0 -56
  233. lionagi/core/messages/__init__.py +0 -3
  234. lionagi/core/messages/schema.py +0 -533
  235. lionagi/core/prompt/prompt_template.py +0 -316
  236. lionagi/core/schema/__init__.py +0 -22
  237. lionagi/core/schema/action_node.py +0 -29
  238. lionagi/core/schema/base_mixin.py +0 -296
  239. lionagi/core/schema/base_node.py +0 -199
  240. lionagi/core/schema/condition.py +0 -24
  241. lionagi/core/schema/data_logger.py +0 -354
  242. lionagi/core/schema/data_node.py +0 -93
  243. lionagi/core/schema/prompt_template.py +0 -67
  244. lionagi/core/schema/structure.py +0 -910
  245. lionagi/core/tool/__init__.py +0 -3
  246. lionagi/core/tool/tool_manager.py +0 -280
  247. lionagi/integrations/bridge/pydantic_/base_model.py +0 -7
  248. lionagi/tests/test_core/test_base_branch.py +0 -427
  249. lionagi/tests/test_core/test_chat_flow.py +0 -63
  250. lionagi/tests/test_core/test_mail_manager.py +0 -75
  251. lionagi/tests/test_core/test_prompts.py +0 -51
  252. lionagi/tests/test_core/test_session.py +0 -254
  253. lionagi/tests/test_core/test_session_base_util.py +0 -312
  254. lionagi/tests/test_core/test_tool_manager.py +0 -95
  255. lionagi-0.0.312.dist-info/RECORD +0 -111
  256. /lionagi/core/{branch/base → _setting}/__init__.py +0 -0
  257. /lionagi/core/{flow → agent/eval}/__init__.py +0 -0
  258. /lionagi/core/{flow/base → agent/learn}/__init__.py +0 -0
  259. /lionagi/core/{prompt → agent/plan}/__init__.py +0 -0
  260. /lionagi/core/{tool/manual.py → agent/plan/plan.py} +0 -0
  261. /lionagi/{tests/test_integrations → core/director}/__init__.py +0 -0
  262. /lionagi/{tests/test_libs → core/engine}/__init__.py +0 -0
  263. /lionagi/{tests/test_libs/test_async.py → core/executor/__init__.py} +0 -0
  264. /lionagi/tests/{test_libs → libs}/test_api.py +0 -0
  265. /lionagi/tests/{test_libs → libs}/test_convert.py +0 -0
  266. /lionagi/tests/{test_libs → libs}/test_sys_util.py +0 -0
  267. {lionagi-0.0.312.dist-info → lionagi-0.2.1.dist-info}/WHEEL +0 -0
  268. {lionagi-0.0.312.dist-info → lionagi-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,910 +0,0 @@
1
- """
2
- This module contains classes for representing and manipulating graph structures.
3
-
4
- The module includes the following classes:
5
- - Relationship: Represents a relationship between two nodes in a graph.
6
- - Graph: Represents a graph structure, consisting of nodes and their relationships.
7
- - Structure: Represents a structure that extends the Graph class with additional functionality.
8
-
9
- The Structure class adds methods for managing and executing a graph structure, including
10
- adding nodes and relationships, checking conditions, processing incoming and outgoing mails,
11
- and executing the structure.
12
- """
13
-
14
- import time
15
- from typing import List, Any, Dict, Callable
16
- from collections import deque
17
- from pydantic import Field
18
-
19
- from lionagi.libs import SysUtil, func_call, AsyncUtil
20
-
21
- from .base_node import BaseRelatableNode, BaseNode, Tool
22
- from lionagi.core.mail.schema import BaseMail
23
-
24
- from lionagi.core.schema.condition import Condition
25
-
26
- from lionagi.core.schema.action_node import ActionNode, ActionSelection
27
- from lionagi.core.schema.base_node import Tool
28
-
29
-
30
- class Relationship(BaseRelatableNode):
31
- """
32
- Represents a relationship between two nodes in a graph.
33
-
34
- Attributes:
35
- source_node_id (str): The identifier of the source node.
36
- target_node_id (str): The identifier of the target node.
37
- condition (Dict[str, Any]): A dictionary representing conditions for the relationship.
38
-
39
- Examples:
40
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
41
- >>> relationship.add_condition({"key": "value"})
42
- >>> condition_value = relationship.get_condition("key")
43
- >>> relationship.remove_condition("key")
44
- """
45
-
46
- source_node_id: str
47
- target_node_id: str
48
- bundle: bool = False
49
- condition: Callable = None
50
-
51
- def add_condition(self, condition: Condition):
52
- """
53
- Adds a condition to the relationship.
54
-
55
- Args:
56
- condition (Condition): The condition to add.
57
-
58
- Raises:
59
- ValueError: If the condition is not an instance of the Condition class.
60
- """
61
- if not isinstance(condition, Condition):
62
- raise ValueError(
63
- "Invalid condition type, please use Condition class to build a valid condition"
64
- )
65
- self.condition = condition
66
-
67
- def check_condition(self, source_obj):
68
- """
69
- Checks the condition of the relationship.
70
-
71
- Args:
72
- source_obj: The source object to evaluate the condition against.
73
-
74
- Returns:
75
- The result of evaluating the condition.
76
-
77
- Raises:
78
- ValueError: If the relationship condition function is invalid.
79
- """
80
- try:
81
- return bool(self.condition(source_obj))
82
- except:
83
- raise ValueError("Invalid relationship condition function")
84
-
85
- def _source_existed(self, obj: Dict[str, Any]) -> bool:
86
- """
87
- Checks if the source node exists in a given object.
88
-
89
- Args:
90
- obj (Dict[str, Any]): The object to check.
91
-
92
- Returns:
93
- bool: True if the source node exists, False otherwise.
94
- """
95
- return self.source_node_id in obj.keys()
96
-
97
- def _target_existed(self, obj: Dict[str, Any]) -> bool:
98
- """
99
- Checks if the target node exists in a given object.
100
-
101
- Args:
102
- obj (Dict[str, Any]): The object to check.
103
-
104
- Returns:
105
- bool: True if the target node exists, False otherwise.
106
- """
107
- return self.target_node_id in obj.keys()
108
-
109
- def _is_in(self, obj: Dict[str, Any]) -> bool:
110
- """
111
- Validates the existence of both source and target nodes in a given object.
112
-
113
- Args:
114
- obj (Dict[str, Any]): The object to check.
115
-
116
- Returns:
117
- bool: True if both nodes exist.
118
-
119
- Raises:
120
- ValueError: If either the source or target node does not exist.
121
- """
122
- if self._source_existed(obj) and self._target_existed(obj):
123
- return True
124
-
125
- elif self._source_existed(obj):
126
- raise ValueError(f"Target node {self.source_node_id} does not exist")
127
- else:
128
- raise ValueError(f"Source node {self.target_node_id} does not exist")
129
-
130
- def __str__(self) -> str:
131
- """
132
- Returns a simple string representation of the Relationship.
133
-
134
- Examples:
135
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
136
- >>> str(relationship)
137
- 'Relationship (id_=None, from=node1, to=node2, label=None)'
138
- """
139
- return (
140
- f"Relationship (id_={self.id_}, from={self.source_node_id}, to={self.target_node_id}, "
141
- f"label={self.label})"
142
- )
143
-
144
- def __repr__(self) -> str:
145
- """
146
- Returns a detailed string representation of the Relationship.
147
-
148
- Examples:
149
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
150
- >>> repr(relationship)
151
- 'Relationship(id_=None, from=node1, to=node2, content=None, metadata=None, label=None)'
152
- """
153
- return (
154
- f"Relationship(id_={self.id_}, from={self.source_node_id}, to={self.target_node_id}, "
155
- f"content={self.content}, metadata={self.metadata}, label={self.label})"
156
- )
157
-
158
-
159
- class Graph(BaseRelatableNode):
160
- """
161
- Represents a graph structure, consisting of nodes and their relationships.
162
-
163
- Attributes:
164
- nodes (Dict[str, BaseNode]): A dictionary of nodes in the graph.
165
- relationships (Dict[str, Relationship]): A dictionary of relationships between nodes in the graph.
166
- node_relationships (Dict[str, Dict[str, Dict[str, str]]]): A dictionary tracking the relationships of each node.
167
-
168
- Examples:
169
- >>> graph = Graph()
170
- >>> node = BaseNode(id_='node1')
171
- >>> graph.add_node(node)
172
- >>> graph.node_exists(node)
173
- True
174
- >>> relationship = Relationship(id_='rel1', source_node_id='node1', target_node_id='node2')
175
- >>> graph.add_relationship(relationship)
176
- >>> graph.relationship_exists(relationship)
177
- True
178
- """
179
-
180
- nodes: dict = Field(default={})
181
- relationships: dict = Field(default={})
182
- node_relationships: dict = Field(default={})
183
-
184
- def add_node(self, node: BaseNode) -> None:
185
- """
186
- Adds a node to the graph.
187
-
188
- Args:
189
- node (BaseNode): The node to add to the graph.
190
- """
191
-
192
- self.nodes[node.id_] = node
193
- self.node_relationships[node.id_] = {"in": {}, "out": {}}
194
-
195
- def add_relationship(self, relationship: Relationship) -> None:
196
- """
197
- Adds a relationship between nodes in the graph.
198
-
199
- Args:
200
- relationship (Relationship): The relationship to add.
201
-
202
- Raises:
203
- KeyError: If either the source or target node of the relationship is not found in the graph.
204
- """
205
- if relationship.source_node_id not in self.node_relationships.keys():
206
- raise KeyError(f"node {relationship.source_node_id} is not found.")
207
- if relationship.target_node_id not in self.node_relationships.keys():
208
- raise KeyError(f"node {relationship.target_node_id} is not found.")
209
-
210
- self.relationships[relationship.id_] = relationship
211
- self.node_relationships[relationship.source_node_id]["out"][
212
- relationship.id_
213
- ] = relationship.target_node_id
214
- self.node_relationships[relationship.target_node_id]["in"][
215
- relationship.id_
216
- ] = relationship.source_node_id
217
-
218
- def get_node_relationships(
219
- self, node: BaseNode = None, out_edge=True
220
- ) -> List[Relationship]:
221
- """
222
- Retrieves relationships of a specific node or all relationships in the graph.
223
-
224
- Args:
225
- node (Optional[BaseNode]): The node whose relationships to retrieve. If None, retrieves all relationships.
226
- out_edge (bool): Whether to retrieve outgoing relationships. If False, retrieves incoming relationships.
227
-
228
- Returns:
229
- List[Relationship]: A list of relationships.
230
-
231
- Raises:
232
- KeyError: If the specified node is not found in the graph.
233
- """
234
- if node is None:
235
- return list(self.relationships.values())
236
-
237
- if node.id_ not in self.nodes.keys():
238
- raise KeyError(f"node {node.id_} is not found")
239
-
240
- if out_edge:
241
- relationship_ids = list(self.node_relationships[node.id_]["out"].keys())
242
- relationships = func_call.lcall(
243
- relationship_ids, lambda i: self.relationships[i]
244
- )
245
- return relationships
246
- else:
247
- relationship_ids = list(self.node_relationships[node.id_]["in"].keys())
248
- relationships = func_call.lcall(
249
- relationship_ids, lambda i: self.relationships[i]
250
- )
251
- return relationships
252
-
253
- def get_predecessors(self, node: BaseNode):
254
- """
255
- Retrieves the predecessor nodes of a given node.
256
-
257
- Args:
258
- node (BaseNode): The node whose predecessors to retrieve.
259
-
260
- Returns:
261
- list: A list of predecessor nodes.
262
- """
263
- node_ids = list(self.node_relationships[node.id_]["in"].values())
264
- nodes = func_call.lcall(node_ids, lambda i: self.nodes[i])
265
- return nodes
266
-
267
- def get_successors(self, node: BaseNode):
268
- """
269
- Retrieves the successor nodes of a given node.
270
-
271
- Args:
272
- node (BaseNode): The node whose successors to retrieve.
273
-
274
- Returns:
275
- list: A list of successor nodes.
276
- """
277
- node_ids = list(self.node_relationships[node.id_]["out"].values())
278
- nodes = func_call.lcall(node_ids, lambda i: self.nodes[i])
279
- return nodes
280
-
281
- def remove_node(self, node: BaseNode) -> BaseNode:
282
- """
283
- Removes a node and its associated relationship from the graph.
284
-
285
- Args:
286
- node (BaseNode): The node to remove.
287
-
288
- Returns:
289
- BaseNode: The removed node.
290
-
291
- Raises:
292
- KeyError: If the node is not found in the graph.
293
- """
294
- if node.id_ not in self.nodes.keys():
295
- raise KeyError(f"node {node.id_} is not found")
296
-
297
- out_relationship = self.node_relationships[node.id_]["out"]
298
- for relationship_id, node_id in out_relationship.items():
299
- self.node_relationships[node_id]["in"].pop(relationship_id)
300
- self.relationships.pop(relationship_id)
301
-
302
- in_relationship = self.node_relationships[node.id_]["in"]
303
- for relationship_id, node_id in in_relationship.items():
304
- self.node_relationships[node_id]["out"].pop(relationship_id)
305
- self.relationships.pop(relationship_id)
306
-
307
- self.node_relationships.pop(node.id_)
308
- return self.nodes.pop(node.id_)
309
-
310
- def remove_relationship(self, relationship: Relationship) -> Relationship:
311
- """
312
- Removes a relationship from the graph.
313
-
314
- Args:
315
- relationship (Relationship): The relationship to remove.
316
-
317
- Returns:
318
- Relationship: The removed relationship.
319
-
320
- Raises:
321
- KeyError: If the relationship is not found in the graph.
322
- """
323
- if relationship.id_ not in self.relationships.keys():
324
- raise KeyError(f"relationship {relationship.id_} is not found")
325
-
326
- self.node_relationships[relationship.source_node_id]["out"].pop(
327
- relationship.id_
328
- )
329
- self.node_relationships[relationship.target_node_id]["in"].pop(relationship.id_)
330
-
331
- return self.relationships.pop(relationship.id_)
332
-
333
- def node_exist(self, node: BaseNode) -> bool:
334
- """
335
- Checks if a node exists in the graph.
336
-
337
- Args:
338
- node (BaseNode): The node to check.
339
-
340
- Returns:
341
- bool: True if the node exists, False otherwise.
342
- """
343
- if node.id_ in self.nodes.keys():
344
- return True
345
- else:
346
- return False
347
-
348
- def relationship_exist(self, relationship: Relationship) -> bool:
349
- """
350
- Checks if a relationship exists in the graph.
351
-
352
- Args:
353
- relationship (Relationship): The relationship to check.
354
-
355
- Returns:
356
- bool: True if the relationship exists, False otherwise.
357
- """
358
- if relationship.id_ in self.relationships.keys():
359
- return True
360
- else:
361
- return False
362
-
363
- def is_empty(self) -> bool:
364
- """
365
- Determines if the graph is empty.
366
-
367
- Returns:
368
- bool: True if the graph has no nodes, False otherwise.
369
- """
370
- if self.nodes:
371
- return False
372
- else:
373
- return True
374
-
375
- def clear(self) -> None:
376
- """Clears the graph of all nodes and relationship."""
377
- self.nodes.clear()
378
- self.relationships.clear()
379
- self.node_relationships.clear()
380
-
381
- def to_networkx(self, **kwargs) -> Any:
382
- """
383
- Converts the graph to a NetworkX graph object.
384
-
385
- Args:
386
- **kwargs: Additional keyword arguments to pass to the NetworkX DiGraph constructor.
387
-
388
- Returns:
389
- Any: A NetworkX directed graph representing the graph.
390
-
391
- Examples:
392
- >>> graph = Graph()
393
- >>> nx_graph = graph.to_networkx()
394
- """
395
-
396
- SysUtil.check_import("networkx")
397
-
398
- from networkx import DiGraph
399
-
400
- g = DiGraph(**kwargs)
401
- for node_id, node in self.nodes.items():
402
- node_info = node.to_dict()
403
- node_info.pop("node_id")
404
- node_info.update({"class_name": node.__class__.__name__})
405
- g.add_node(node_id, **node_info)
406
-
407
- for _, relationship in self.relationships.items():
408
- relationship_info = relationship.to_dict()
409
- relationship_info.pop("node_id")
410
- relationship_info.update({"class_name": relationship.__class__.__name__})
411
- source_node_id = relationship_info.pop("source_node_id")
412
- target_node_id = relationship_info.pop("target_node_id")
413
- g.add_edge(source_node_id, target_node_id, **relationship_info)
414
-
415
- return g
416
-
417
-
418
- class Structure(BaseRelatableNode):
419
- """
420
- Represents a structure that extends the Graph class with additional functionality.
421
-
422
- Attributes:
423
- graph (Graph): The underlying graph structure.
424
- pending_ins (dict): A dictionary of pending incoming mails.
425
- pending_outs (deque): A deque of pending outgoing mails.
426
- execute_stop (bool): A flag indicating whether the execution should stop.
427
- condition_check_result (bool | None): The result of the last condition check.
428
-
429
- Methods:
430
- add_node(node: BaseNode) -> None:
431
- Adds a node to the structure's graph.
432
-
433
- add_relationship(from_node: BaseNode, to_node: BaseNode, bundle=False, condition=None, **kwargs) -> None:
434
- Adds a relationship between two nodes in the structure's graph.
435
-
436
- get_relationships() -> list[Relationship]:
437
- Retrieves all relationships in the structure's graph.
438
-
439
- get_node_relationships(node: BaseNode, out_edge=True, labels=None) -> list[Relationship]:
440
- Retrieves relationships of a specific node in the structure's graph.
441
-
442
- get_predecessors(node: BaseNode) -> list[BaseNode]:
443
- Retrieves the predecessor nodes of a given node in the structure's graph.
444
-
445
- get_successors(node: BaseNode) -> list[BaseNode]:
446
- Retrieves the successor nodes of a given node in the structure's graph.
447
-
448
- node_exist(node: BaseNode) -> bool:
449
- Checks if a node exists in the structure's graph.
450
-
451
- relationship_exist(relationship: Relationship) -> bool:
452
- Checks if a relationship exists in the structure's graph.
453
-
454
- remove_node(node: BaseNode) -> BaseNode:
455
- Removes a node from the structure's graph.
456
-
457
- remove_relationship(relationship: Relationship) -> Relationship:
458
- Removes a relationship from the structure's graph.
459
-
460
- is_empty() -> bool:
461
- Determines if the structure's graph is empty.
462
-
463
- get_heads() -> list[BaseNode]:
464
- Retrieves the head nodes of the structure's graph.
465
-
466
- parse_to_action(instruction: BaseNode, bundled_nodes: deque) -> ActionNode:
467
- Parses an instruction and bundled nodes into an ActionNode.
468
-
469
- async check_condition(relationship: Relationship, executable_id: str) -> bool:
470
- Checks the condition of a relationship.
471
-
472
- check_condition_structure(relationship: Relationship) -> bool:
473
- Checks the condition of a relationship within the structure.
474
-
475
- async get_next_step(current_node: BaseNode, executable_id: str) -> list[BaseNode]:
476
- Retrieves the next step nodes based on the current node and executable ID.
477
-
478
- acyclic() -> bool:
479
- Checks if the structure's graph is acyclic.
480
-
481
- send(recipient_id: str, category: str, package: Any) -> None:
482
- Sends a mail to a recipient.
483
-
484
- process_relationship_condition(relationship_id: str) -> None:
485
- Processes the condition of a relationship.
486
-
487
- async process() -> None:
488
- Processes the pending incoming mails and performs the corresponding actions.
489
-
490
- async execute(refresh_time=1) -> None:
491
- Executes the structure by processing incoming mails and updating the execution state.
492
- """
493
-
494
- graph: Graph = Graph()
495
- pending_ins: dict = {}
496
- pending_outs: deque = deque()
497
- execute_stop: bool = False
498
- condition_check_result: bool | None = None
499
-
500
- def add_node(self, node: BaseNode | list[BaseNode]):
501
- """
502
- Adds a node to the structure's graph.
503
-
504
- Args:
505
- node (BaseNode): The node to add.
506
- """
507
- func_call.lcall(node, self.graph.add_node)
508
-
509
- def add_relationship(
510
- self,
511
- from_node: BaseNode,
512
- to_node: BaseNode,
513
- bundle=False,
514
- condition=None,
515
- **kwargs,
516
- ):
517
- """
518
- Adds a relationship between two nodes in the structure's graph.
519
-
520
- Args:
521
- from_node (BaseNode): The source node of the relationship.
522
- to_node (BaseNode): The target node of the relationship.
523
- bundle (bool): Whether the relationship is bundled (default: False).
524
- condition (Optional[Condition]): The condition for the relationship (default: None).
525
- **kwargs: Additional keyword arguments for the relationship.
526
-
527
- Raises:
528
- ValueError: If the source node is a Tool or ActionSelection.
529
- """
530
- if isinstance(from_node, Tool) or isinstance(from_node, ActionSelection):
531
- raise ValueError(
532
- f"type {type(from_node)} should not be the head of the relationship, "
533
- f"please switch position and attach it to the tail of the relationship"
534
- )
535
- if isinstance(to_node, Tool) or isinstance(to_node, ActionSelection):
536
- bundle = True
537
- relationship = Relationship(
538
- source_node_id=from_node.id_,
539
- target_node_id=to_node.id_,
540
- bundle=bundle,
541
- **kwargs,
542
- )
543
- if condition:
544
- relationship.add_condition(condition)
545
- self.graph.add_relationship(relationship)
546
-
547
- def get_relationships(self) -> list[Relationship]:
548
- """
549
- Retrieves all relationships in the structure's graph.
550
-
551
- Returns:
552
- list[Relationship]: A list of all relationships.
553
- """
554
- return self.graph.get_node_relationships()
555
-
556
- def get_node_relationships(self, node: BaseNode, out_edge=True, labels=None):
557
- """
558
- Retrieves relationships of a specific node in the structure's graph.
559
-
560
- Args:
561
- node (BaseNode): The node whose relationships to retrieve.
562
- out_edge (bool): Whether to retrieve outgoing relationships (default: True).
563
- labels (Optional[list]): The labels of the relationships to retrieve (default: None).
564
-
565
- Returns:
566
- list[Relationship]: A list of relationships for the specified node.
567
- """
568
- relationships = self.graph.get_node_relationships(node, out_edge)
569
- if labels:
570
- if not isinstance(labels, list):
571
- labels = [labels]
572
- result = []
573
- for r in relationships:
574
- if r.label in labels:
575
- result.append(r)
576
- relationships = result
577
- return relationships
578
-
579
- def get_predecessors(self, node: BaseNode):
580
- """
581
- Retrieves the predecessor nodes of a given node in the structure's graph.
582
-
583
- Args:
584
- node (BaseNode): The node whose predecessors to retrieve.
585
-
586
- Returns:
587
- list[BaseNode]: A list of predecessor nodes.
588
- """
589
- return self.graph.get_predecessors(node)
590
-
591
- def get_successors(self, node: BaseNode):
592
- """
593
- Retrieves the successor nodes of a given node in the structure's graph.
594
-
595
- Args:
596
- node (BaseNode): The node whose successors to retrieve.
597
-
598
- Returns:
599
- list[BaseNode]: A list of successor nodes.
600
- """
601
- return self.graph.get_successors(node)
602
-
603
- def node_exist(self, node: BaseNode) -> bool:
604
- """
605
- Checks if a node exists in the structure's graph.
606
-
607
- Args:
608
- node (BaseNode): The node to check.
609
-
610
- Returns:
611
- bool: True if the node exists, False otherwise.
612
- """
613
- return self.graph.node_exist(node)
614
-
615
- def relationship_exist(self, relationship: Relationship) -> bool:
616
- """
617
- Checks if a relationship exists in the structure's graph.
618
-
619
- Args:
620
- relationship (Relationship): The relationship to check.
621
-
622
- Returns:
623
- bool: True if the relationship exists, False otherwise.
624
- """
625
- return self.graph.relationship_exist(relationship)
626
-
627
- def remove_node(self, node: BaseNode) -> BaseNode:
628
- """
629
- Removes a node from the structure's graph.
630
-
631
- Args:
632
- node (BaseNode): The node to remove.
633
-
634
- Returns:
635
- BaseNode: The removed node.
636
- """
637
- return self.graph.remove_node(node)
638
-
639
- def remove_relationship(self, relationship: Relationship) -> Relationship:
640
- """
641
- Removes a relationship from the structure's graph.
642
-
643
- Args:
644
- relationship (Relationship): The relationship to remove.
645
-
646
- Returns:
647
- Relationship: The removed relationship.
648
- """
649
- return self.graph.remove_relationship(relationship)
650
-
651
- def is_empty(self) -> bool:
652
- """
653
- Determines if the structure's graph is empty.
654
-
655
- Returns:
656
- bool: True if the graph has no nodes, False otherwise.
657
- """
658
- return self.graph.is_empty()
659
-
660
- def get_heads(self):
661
- """
662
- Retrieves the head nodes of the structure's graph.
663
-
664
- Returns:
665
- list[BaseNode]: A list of head nodes.
666
- """
667
- heads = []
668
- for key in self.graph.node_relationships:
669
- if not self.graph.node_relationships[key]["in"]:
670
- heads.append(self.graph.nodes[key])
671
- return heads
672
-
673
- @staticmethod
674
- def parse_to_action(instruction: BaseNode, bundled_nodes: deque):
675
- """
676
- Parses an instruction and bundled nodes into an ActionNode.
677
-
678
- Args:
679
- instruction (BaseNode): The instruction node.
680
- bundled_nodes (deque): A deque of bundled nodes.
681
-
682
- Returns:
683
- ActionNode: The parsed ActionNode.
684
-
685
- Raises:
686
- ValueError: If an invalid bundled node is encountered.
687
- """
688
- action_node = ActionNode(instruction)
689
- while bundled_nodes:
690
- node = bundled_nodes.popleft()
691
- if isinstance(node, ActionSelection):
692
- action_node.action = node.action
693
- action_node.action_kwargs = node.action_kwargs
694
- elif isinstance(node, Tool):
695
- action_node.tools.append(node)
696
- else:
697
- raise ValueError("Invalid bundles nodes")
698
- return action_node
699
-
700
- async def check_condition(self, relationship, executable_id):
701
- """
702
- Checks the condition of a relationship.
703
-
704
- Args:
705
- relationship (Relationship): The relationship to check the condition for.
706
- executable_id (str): The ID of the executable.
707
-
708
- Returns:
709
- bool: True if the condition is met, False otherwise.
710
-
711
- Raises:
712
- ValueError: If the source type of the condition is invalid.
713
- """
714
- if relationship.condition.source_type == "structure":
715
- return self.check_condition_structure(relationship)
716
- elif relationship.condition.source_type == "executable":
717
- self.send(
718
- recipient_id=executable_id, category="condition", package=relationship
719
- )
720
- while self.condition_check_result is None:
721
- await AsyncUtil.sleep(0.1)
722
- self.process_relationship_condition(relationship.id_)
723
- continue
724
- check_result = self.condition_check_result
725
- self.condition_check_result = None
726
- return check_result
727
- else:
728
- raise ValueError("Invalid source_type.")
729
-
730
- def check_condition_structure(self, relationship):
731
- """
732
- Checks the condition of a relationship within the structure.
733
-
734
- Args:
735
- relationship (Relationship): The relationship to check the condition for.
736
-
737
- Returns:
738
- bool: The result of the condition check.
739
- """
740
- return relationship.condition(self)
741
-
742
- async def get_next_step(self, current_node: BaseNode, executable_id):
743
- """
744
- Retrieves the next step nodes based on the current node and executable ID.
745
-
746
- Args:
747
- current_node (BaseNode): The current node.
748
- executable_id (str): The ID of the executable.
749
-
750
- Returns:
751
- list[BaseNode]: A list of next step nodes.
752
- """
753
- next_nodes = []
754
- next_relationships = self.get_node_relationships(current_node)
755
- for relationship in next_relationships:
756
- if relationship.bundle:
757
- continue
758
- if relationship.condition:
759
- check = await self.check_condition(relationship, executable_id)
760
- if not check:
761
- continue
762
- node = self.graph.nodes[relationship.target_node_id]
763
- further_relationships = self.get_node_relationships(node)
764
- bundled_nodes = deque()
765
- for f_relationship in further_relationships:
766
- if f_relationship.bundle:
767
- bundled_nodes.append(
768
- self.graph.nodes[f_relationship.target_node_id]
769
- )
770
- if bundled_nodes:
771
- node = self.parse_to_action(node, bundled_nodes)
772
- next_nodes.append(node)
773
- return next_nodes
774
-
775
- def acyclic(self):
776
- """
777
- Checks if the structure's graph is acyclic.
778
-
779
- Returns:
780
- bool: True if the graph is acyclic, False otherwise.
781
- """
782
- check_deque = deque(self.graph.nodes.keys())
783
- check_dict = {
784
- key: 0 for key in self.graph.nodes.keys()
785
- } # 0: not visited, 1: temp, 2: perm
786
-
787
- def visit(key):
788
- if check_dict[key] == 2:
789
- return True
790
- elif check_dict[key] == 1:
791
- return False
792
-
793
- check_dict[key] = 1
794
-
795
- out_relationships = self.graph.get_node_relationships(self.graph.nodes[key])
796
- for node in out_relationships:
797
- check = visit(node.target_node_id)
798
- if not check:
799
- return False
800
-
801
- check_dict[key] = 2
802
- return True
803
-
804
- while check_deque:
805
- key = check_deque.pop()
806
- check = visit(key)
807
- if not check:
808
- return False
809
- return True
810
-
811
- def send(self, recipient_id: str, category: str, package: Any) -> None:
812
- """
813
- Sends a mail to a recipient.
814
-
815
- Args:
816
- recipient_id (str): The ID of the recipient.
817
- category (str): The category of the mail.
818
- package (Any): The package to send.
819
- """
820
- mail = BaseMail(
821
- sender_id=self.id_,
822
- recipient_id=recipient_id,
823
- category=category,
824
- package=package,
825
- )
826
- self.pending_outs.append(mail)
827
-
828
- def process_relationship_condition(self, relationship_id):
829
- """
830
- Processes the condition of a relationship.
831
-
832
- Args:
833
- relationship_id (str): The ID of the relationship to process the condition for.
834
- """
835
- for key in list(self.pending_ins.keys()):
836
- skipped_requests = deque()
837
- while self.pending_ins[key]:
838
- mail = self.pending_ins[key].popleft()
839
- if (
840
- mail.category == "condition"
841
- and mail.package["relationship_id"] == relationship_id
842
- ):
843
- self.condition_check_result = mail.package["check_result"]
844
- else:
845
- skipped_requests.append(mail)
846
- self.pending_ins[key] = skipped_requests
847
-
848
- async def process(self) -> None:
849
- """
850
- Processes the pending incoming mails and performs the corresponding actions.
851
- """
852
- for key in list(self.pending_ins.keys()):
853
- while self.pending_ins[key]:
854
- mail = self.pending_ins[key].popleft()
855
- if mail.category == "start":
856
- next_nodes = self.get_heads()
857
- elif mail.category == "end":
858
- self.execute_stop = True
859
- return
860
- elif mail.category == "node_id":
861
- if mail.package not in self.graph.nodes:
862
- raise ValueError(
863
- f"Node {mail.package} does not exist in the structure {self.id_}"
864
- )
865
- next_nodes = await self.get_next_step(
866
- self.graph.nodes[mail.package], mail.sender_id
867
- )
868
- elif mail.category == "node" and isinstance(mail.package, BaseNode):
869
- if not self.node_exist(mail.package):
870
- raise ValueError(
871
- f"Node {mail.package} does not exist in the structure {self.id_}"
872
- )
873
- next_nodes = await self.get_next_step(mail.package, mail.sender_id)
874
- else:
875
- raise ValueError(f"Invalid mail type for structure")
876
-
877
- if not next_nodes: # tail
878
- self.send(
879
- recipient_id=mail.sender_id, category="end", package="end"
880
- )
881
- else:
882
- if len(next_nodes) == 1:
883
- self.send(
884
- recipient_id=mail.sender_id,
885
- category="node",
886
- package=next_nodes[0],
887
- )
888
- else:
889
- self.send(
890
- recipient_id=mail.sender_id,
891
- category="node_list",
892
- package=next_nodes,
893
- )
894
-
895
- async def execute(self, refresh_time=1):
896
- """
897
- Executes the structure by processing incoming mails and updating the execution state.
898
-
899
- Args:
900
- refresh_time (int): The refresh time for execution (default: 1).
901
-
902
- Raises:
903
- ValueError: If the structure's graph is not acyclic.
904
- """
905
- if not self.acyclic():
906
- raise ValueError("Structure is not acyclic")
907
-
908
- while not self.execute_stop:
909
- await self.process()
910
- await AsyncUtil.sleep(refresh_time)