lionagi 0.0.305__py3-none-any.whl → 0.0.307__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 (84) hide show
  1. lionagi/__init__.py +2 -5
  2. lionagi/core/__init__.py +7 -4
  3. lionagi/core/agent/__init__.py +3 -0
  4. lionagi/core/agent/base_agent.py +46 -0
  5. lionagi/core/branch/__init__.py +4 -0
  6. lionagi/core/branch/base/__init__.py +0 -0
  7. lionagi/core/branch/base_branch.py +100 -78
  8. lionagi/core/branch/branch.py +22 -34
  9. lionagi/core/branch/branch_flow_mixin.py +3 -7
  10. lionagi/core/branch/executable_branch.py +192 -0
  11. lionagi/core/branch/util.py +77 -162
  12. lionagi/core/direct/__init__.py +13 -0
  13. lionagi/core/direct/parallel_predict.py +127 -0
  14. lionagi/core/direct/parallel_react.py +0 -0
  15. lionagi/core/direct/parallel_score.py +0 -0
  16. lionagi/core/direct/parallel_select.py +0 -0
  17. lionagi/core/direct/parallel_sentiment.py +0 -0
  18. lionagi/core/direct/predict.py +174 -0
  19. lionagi/core/direct/react.py +33 -0
  20. lionagi/core/direct/score.py +163 -0
  21. lionagi/core/direct/select.py +144 -0
  22. lionagi/core/direct/sentiment.py +51 -0
  23. lionagi/core/direct/utils.py +83 -0
  24. lionagi/core/flow/__init__.py +0 -3
  25. lionagi/core/flow/monoflow/{mono_react.py → ReAct.py} +52 -9
  26. lionagi/core/flow/monoflow/__init__.py +9 -0
  27. lionagi/core/flow/monoflow/{mono_chat.py → chat.py} +11 -11
  28. lionagi/core/flow/monoflow/{mono_chat_mixin.py → chat_mixin.py} +33 -27
  29. lionagi/core/flow/monoflow/{mono_followup.py → followup.py} +7 -6
  30. lionagi/core/flow/polyflow/__init__.py +1 -0
  31. lionagi/core/flow/polyflow/{polychat.py → chat.py} +15 -3
  32. lionagi/core/mail/__init__.py +8 -0
  33. lionagi/core/mail/mail_manager.py +88 -40
  34. lionagi/core/mail/schema.py +32 -6
  35. lionagi/core/messages/__init__.py +3 -0
  36. lionagi/core/messages/schema.py +56 -25
  37. lionagi/core/prompt/__init__.py +0 -0
  38. lionagi/core/prompt/prompt_template.py +0 -0
  39. lionagi/core/schema/__init__.py +7 -5
  40. lionagi/core/schema/action_node.py +29 -0
  41. lionagi/core/schema/base_mixin.py +56 -59
  42. lionagi/core/schema/base_node.py +35 -38
  43. lionagi/core/schema/condition.py +24 -0
  44. lionagi/core/schema/data_logger.py +98 -98
  45. lionagi/core/schema/data_node.py +19 -19
  46. lionagi/core/schema/prompt_template.py +0 -0
  47. lionagi/core/schema/structure.py +293 -190
  48. lionagi/core/session/__init__.py +1 -3
  49. lionagi/core/session/session.py +196 -214
  50. lionagi/core/tool/tool_manager.py +95 -103
  51. lionagi/integrations/__init__.py +1 -3
  52. lionagi/integrations/bridge/langchain_/documents.py +17 -18
  53. lionagi/integrations/bridge/langchain_/langchain_bridge.py +14 -14
  54. lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py +22 -22
  55. lionagi/integrations/bridge/llamaindex_/node_parser.py +12 -12
  56. lionagi/integrations/bridge/llamaindex_/reader.py +11 -11
  57. lionagi/integrations/bridge/llamaindex_/textnode.py +7 -7
  58. lionagi/integrations/config/openrouter_configs.py +0 -1
  59. lionagi/integrations/provider/oai.py +26 -26
  60. lionagi/integrations/provider/services.py +38 -38
  61. lionagi/libs/__init__.py +34 -1
  62. lionagi/libs/ln_api.py +211 -221
  63. lionagi/libs/ln_async.py +53 -60
  64. lionagi/libs/ln_convert.py +118 -120
  65. lionagi/libs/ln_dataframe.py +32 -33
  66. lionagi/libs/ln_func_call.py +334 -342
  67. lionagi/libs/ln_nested.py +99 -107
  68. lionagi/libs/ln_parse.py +175 -158
  69. lionagi/libs/sys_util.py +52 -52
  70. lionagi/tests/test_core/test_base_branch.py +427 -427
  71. lionagi/tests/test_core/test_branch.py +292 -292
  72. lionagi/tests/test_core/test_mail_manager.py +57 -57
  73. lionagi/tests/test_core/test_session.py +254 -266
  74. lionagi/tests/test_core/test_session_base_util.py +299 -300
  75. lionagi/tests/test_core/test_tool_manager.py +70 -74
  76. lionagi/tests/test_libs/test_nested.py +2 -7
  77. lionagi/tests/test_libs/test_parse.py +2 -2
  78. lionagi/version.py +1 -1
  79. {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/METADATA +4 -2
  80. lionagi-0.0.307.dist-info/RECORD +115 -0
  81. lionagi-0.0.305.dist-info/RECORD +0 -94
  82. {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/LICENSE +0 -0
  83. {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/WHEEL +0 -0
  84. {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,17 @@
1
- from typing import List, Any, Dict
1
+ import time
2
+ from typing import List, Any, Dict, Callable
3
+ from collections import deque
2
4
  from pydantic import Field
3
5
 
4
- from lionagi.libs.sys_util import SysUtil
5
- from lionagi.libs import ln_func_call as func_call
6
+ from lionagi.libs import SysUtil, func_call, AsyncUtil
6
7
 
7
- from lionagi.core.schema.base_node import BaseRelatableNode, BaseNode
8
+ from .base_node import BaseRelatableNode, BaseNode, Tool
9
+ from lionagi.core.mail.schema import BaseMail
10
+
11
+ from lionagi.core.schema.condition import Condition
12
+
13
+ from lionagi.core.schema.action_node import ActionNode, ActionSelection
14
+ from lionagi.core.schema.base_node import Tool
8
15
 
9
16
 
10
17
  class Relationship(BaseRelatableNode):
@@ -12,112 +19,44 @@ class Relationship(BaseRelatableNode):
12
19
  Represents a relationship between two nodes in a graph.
13
20
 
14
21
  Attributes:
15
- source_node_id (str): The identifier of the source node.
16
- target_node_id (str): The identifier of the target node.
17
- condition (Dict[str, Any]): A dictionary representing conditions for the relationship.
22
+ source_node_id (str): The identifier of the source node.
23
+ target_node_id (str): The identifier of the target node.
24
+ condition (Dict[str, Any]): A dictionary representing conditions for the relationship.
18
25
 
19
26
  Examples:
20
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
21
- >>> relationship.add_condition({"key": "value"})
22
- >>> condition_value = relationship.get_condition("key")
23
- >>> relationship.remove_condition("key")
24
- """
25
-
26
- source_node_id: str
27
- target_node_id: str
28
- condition: dict = Field(default={})
29
-
30
- def add_condition(self, condition: Dict[str, Any]) -> None:
31
- """
32
- Adds a condition to the relationship.
33
-
34
- Args:
35
- condition: The condition to be added.
36
-
37
- Examples:
38
27
  >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
39
28
  >>> relationship.add_condition({"key": "value"})
40
- """
41
- self.condition.update(condition)
42
-
43
- def remove_condition(self, condition_key: str) -> Any:
44
- """
45
- Removes a condition from the relationship.
46
-
47
- Args:
48
- condition_key: The key of the condition to be removed.
49
-
50
- Returns:
51
- The value of the removed condition.
52
-
53
- Raises:
54
- KeyError: If the condition key is not found.
55
-
56
- Examples:
57
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2", condition={"key": "value"})
29
+ >>> condition_value = relationship.get_condition("key")
58
30
  >>> relationship.remove_condition("key")
59
- 'value'
60
- """
61
- if condition_key not in self.condition.keys():
62
- raise KeyError(f"condition {condition_key} is not found")
63
- return self.condition.pop(condition_key)
64
-
65
- def condition_exists(self, condition_key: str) -> bool:
66
- """
67
- Checks if a condition exists in the relationship.
68
-
69
- Args:
70
- condition_key: The key of the condition to check.
71
-
72
- Returns:
73
- True if the condition exists, False otherwise.
74
-
75
- Examples:
76
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2", condition={"key": "value"})
77
- >>> relationship.condition_exists("key")
78
- True
79
- """
80
- if condition_key in self.condition.keys():
81
- return True
82
- else:
83
- return False
84
-
85
- def get_condition(self, condition_key: str | None = None) -> Any:
86
- """
87
- Retrieves a specific condition or all conditions of the relationship.
88
-
89
- Args:
90
- condition_key: The key of the specific condition. If None, all conditions are returned.
31
+ """
91
32
 
92
- Returns:
93
- The requested condition or all conditions if no key is provided.
33
+ source_node_id: str
34
+ target_node_id: str
35
+ bundle: bool = False
36
+ condition: Callable = None
94
37
 
95
- Raises:
96
- ValueError: If the specified condition key does not exist.
38
+ def add_condition(self, condition: Condition):
39
+ if not isinstance(condition, Condition):
40
+ raise ValueError(
41
+ "Invalid condition type, please use Condition class to build a valid condition"
42
+ )
43
+ self.condition = condition
97
44
 
98
- Examples:
99
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2", condition={"key": "value"})
100
- >>> relationship.get_condition("key")
101
- 'value'
102
- >>> relationship.get_condition()
103
- {'key': 'value'}
104
- """
105
- if condition_key is None:
106
- return self.condition
107
- if self.condition_exists(condition_key=condition_key):
108
- return self.condition[condition_key]
109
- else:
110
- raise ValueError(f"Condition {condition_key} does not exist")
45
+ def check_condition(self, source_obj):
46
+ try:
47
+ return bool(self.condition(source_obj))
48
+ except:
49
+ raise ValueError("Invalid relationship condition function")
111
50
 
112
51
  def _source_existed(self, obj: Dict[str, Any]) -> bool:
113
52
  """
114
53
  Checks if the source node exists in a given object.
115
54
 
116
55
  Args:
117
- obj (Dict[str, Any]): The object to check.
56
+ obj (Dict[str, Any]): The object to check.
118
57
 
119
58
  Returns:
120
- bool: True if the source node exists, False otherwise.
59
+ bool: True if the source node exists, False otherwise.
121
60
  """
122
61
  return self.source_node_id in obj.keys()
123
62
 
@@ -126,10 +65,10 @@ class Relationship(BaseRelatableNode):
126
65
  Checks if the target node exists in a given object.
127
66
 
128
67
  Args:
129
- obj (Dict[str, Any]): The object to check.
68
+ obj (Dict[str, Any]): The object to check.
130
69
 
131
70
  Returns:
132
- bool: True if the target node exists, False otherwise.
71
+ bool: True if the target node exists, False otherwise.
133
72
  """
134
73
  return self.target_node_id in obj.keys()
135
74
 
@@ -138,13 +77,13 @@ class Relationship(BaseRelatableNode):
138
77
  Validates the existence of both source and target nodes in a given object.
139
78
 
140
79
  Args:
141
- obj (Dict[str, Any]): The object to check.
80
+ obj (Dict[str, Any]): The object to check.
142
81
 
143
82
  Returns:
144
- bool: True if both nodes exist.
83
+ bool: True if both nodes exist.
145
84
 
146
85
  Raises:
147
- ValueError: If either the source or target node does not exist.
86
+ ValueError: If either the source or target node does not exist.
148
87
  """
149
88
  if self._source_existed(obj) and self._target_existed(obj):
150
89
  return True
@@ -159,9 +98,9 @@ class Relationship(BaseRelatableNode):
159
98
  Returns a simple string representation of the Relationship.
160
99
 
161
100
  Examples:
162
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
163
- >>> str(relationship)
164
- 'Relationship (id_=None, from=node1, to=node2, label=None)'
101
+ >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
102
+ >>> str(relationship)
103
+ 'Relationship (id_=None, from=node1, to=node2, label=None)'
165
104
  """
166
105
  return (
167
106
  f"Relationship (id_={self.id_}, from={self.source_node_id}, to={self.target_node_id}, "
@@ -173,9 +112,9 @@ class Relationship(BaseRelatableNode):
173
112
  Returns a detailed string representation of the Relationship.
174
113
 
175
114
  Examples:
176
- >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
177
- >>> repr(relationship)
178
- 'Relationship(id_=None, from=node1, to=node2, content=None, metadata=None, label=None)'
115
+ >>> relationship = Relationship(source_node_id="node1", target_node_id="node2")
116
+ >>> repr(relationship)
117
+ 'Relationship(id_=None, from=node1, to=node2, content=None, metadata=None, label=None)'
179
118
  """
180
119
  return (
181
120
  f"Relationship(id_={self.id_}, from={self.source_node_id}, to={self.target_node_id}, "
@@ -188,20 +127,20 @@ class Graph(BaseRelatableNode):
188
127
  Represents a graph structure, consisting of nodes and their relationship.
189
128
 
190
129
  Attributes:
191
- nodes (Dict[str, BaseNode]): A dictionary of nodes in the graph.
192
- relationships (Dict[str, Relationship]): A dictionary of relationship between nodes in the graph.
193
- node_relationships (Dict[str, Dict[str, Dict[str, str]]]): A dictionary tracking the relationship of each node.
130
+ nodes (Dict[str, BaseNode]): A dictionary of nodes in the graph.
131
+ relationships (Dict[str, Relationship]): A dictionary of relationship between nodes in the graph.
132
+ node_relationships (Dict[str, Dict[str, Dict[str, str]]]): A dictionary tracking the relationship of each node.
194
133
 
195
134
  Examples:
196
- >>> graph = Graph()
197
- >>> node = BaseNode(id_='node1')
198
- >>> graph.add_node(node)
199
- >>> graph.node_exists(node)
200
- True
201
- >>> relationship = Relationship(id_='rel1', source_node_id='node1', target_node_id='node2')
202
- >>> graph.add_relationship(relationship)
203
- >>> graph.relationship_exists(relationship)
204
- True
135
+ >>> graph = Graph()
136
+ >>> node = BaseNode(id_='node1')
137
+ >>> graph.add_node(node)
138
+ >>> graph.node_exists(node)
139
+ True
140
+ >>> relationship = Relationship(id_='rel1', source_node_id='node1', target_node_id='node2')
141
+ >>> graph.add_relationship(relationship)
142
+ >>> graph.relationship_exists(relationship)
143
+ True
205
144
  """
206
145
 
207
146
  nodes: dict = Field(default={})
@@ -213,7 +152,7 @@ class Graph(BaseRelatableNode):
213
152
  Adds a node to the graph.
214
153
 
215
154
  Args:
216
- node (BaseNode): The node to add to the graph.
155
+ node (BaseNode): The node to add to the graph.
217
156
  """
218
157
 
219
158
  self.nodes[node.id_] = node
@@ -224,10 +163,10 @@ class Graph(BaseRelatableNode):
224
163
  Adds a relationship between nodes in the graph.
225
164
 
226
165
  Args:
227
- relationship (Relationship): The relationship to add.
166
+ relationship (Relationship): The relationship to add.
228
167
 
229
168
  Raises:
230
- KeyError: If either the source or target node of the relationship is not found in the graph.
169
+ KeyError: If either the source or target node of the relationship is not found in the graph.
231
170
  """
232
171
  if relationship.source_node_id not in self.node_relationships.keys():
233
172
  raise KeyError(f"node {relationship.source_node_id} is not found.")
@@ -249,14 +188,14 @@ class Graph(BaseRelatableNode):
249
188
  Retrieves relationship of a specific node or all relationship in the graph.
250
189
 
251
190
  Args:
252
- node (Optional[BaseNode]): The node whose relationship to retrieve. If None, retrieves all relationship.
253
- out_edge (bool): Whether to retrieve outgoing relationship. If False, retrieves incoming relationship.
191
+ node (Optional[BaseNode]): The node whose relationship to retrieve. If None, retrieves all relationship.
192
+ out_edge (bool): Whether to retrieve outgoing relationship. If False, retrieves incoming relationship.
254
193
 
255
194
  Returns:
256
- List[Relationship]: A list of relationship.
195
+ List[Relationship]: A list of relationship.
257
196
 
258
197
  Raises:
259
- KeyError: If the specified node is not found in the graph.
198
+ KeyError: If the specified node is not found in the graph.
260
199
  """
261
200
  if node is None:
262
201
  return list(self.relationships.values())
@@ -277,18 +216,28 @@ class Graph(BaseRelatableNode):
277
216
  )
278
217
  return relationships
279
218
 
219
+ def get_predecessors(self, node: BaseNode):
220
+ node_ids = list(self.node_relationships[node.id_]["in"].values())
221
+ nodes = func_call.lcall(node_ids, lambda i: self.nodes[i])
222
+ return nodes
223
+
224
+ def get_successors(self, node: BaseNode):
225
+ node_ids = list(self.node_relationships[node.id_]["out"].values())
226
+ nodes = func_call.lcall(node_ids, lambda i: self.nodes[i])
227
+ return nodes
228
+
280
229
  def remove_node(self, node: BaseNode) -> BaseNode:
281
230
  """
282
231
  Removes a node and its associated relationship from the graph.
283
232
 
284
233
  Args:
285
- node (BaseNode): The node to remove.
234
+ node (BaseNode): The node to remove.
286
235
 
287
236
  Returns:
288
- BaseNode: The removed node.
237
+ BaseNode: The removed node.
289
238
 
290
239
  Raises:
291
- KeyError: If the node is not found in the graph.
240
+ KeyError: If the node is not found in the graph.
292
241
  """
293
242
  if node.id_ not in self.nodes.keys():
294
243
  raise KeyError(f"node {node.id_} is not found")
@@ -311,13 +260,13 @@ class Graph(BaseRelatableNode):
311
260
  Removes a relationship from the graph.
312
261
 
313
262
  Args:
314
- relationship (Relationship): The relationship to remove.
263
+ relationship (Relationship): The relationship to remove.
315
264
 
316
265
  Returns:
317
- Relationship: The removed relationship.
266
+ Relationship: The removed relationship.
318
267
 
319
268
  Raises:
320
- KeyError: If the relationship is not found in the graph.
269
+ KeyError: If the relationship is not found in the graph.
321
270
  """
322
271
  if relationship.id_ not in self.relationships.keys():
323
272
  raise KeyError(f"relationship {relationship.id_} is not found")
@@ -334,10 +283,10 @@ class Graph(BaseRelatableNode):
334
283
  Checks if a node exists in the graph.
335
284
 
336
285
  Args:
337
- node (BaseNode): The node to check.
286
+ node (BaseNode): The node to check.
338
287
 
339
288
  Returns:
340
- bool: True if the node exists, False otherwise.
289
+ bool: True if the node exists, False otherwise.
341
290
  """
342
291
  if node.id_ in self.nodes.keys():
343
292
  return True
@@ -349,10 +298,10 @@ class Graph(BaseRelatableNode):
349
298
  Checks if a relationship exists in the graph.
350
299
 
351
300
  Args:
352
- relationship (Relationship): The relationship to check.
301
+ relationship (Relationship): The relationship to check.
353
302
 
354
303
  Returns:
355
- bool: True if the relationship exists, False otherwise.
304
+ bool: True if the relationship exists, False otherwise.
356
305
  """
357
306
  if relationship.id_ in self.relationships.keys():
358
307
  return True
@@ -364,7 +313,7 @@ class Graph(BaseRelatableNode):
364
313
  Determines if the graph is empty.
365
314
 
366
315
  Returns:
367
- bool: True if the graph has no nodes, False otherwise.
316
+ bool: True if the graph has no nodes, False otherwise.
368
317
  """
369
318
  if self.nodes:
370
319
  return False
@@ -382,14 +331,14 @@ class Graph(BaseRelatableNode):
382
331
  Converts the graph to a NetworkX graph object.
383
332
 
384
333
  Args:
385
- **kwargs: Additional keyword arguments to pass to the NetworkX DiGraph constructor.
334
+ **kwargs: Additional keyword arguments to pass to the NetworkX DiGraph constructor.
386
335
 
387
336
  Returns:
388
- Any: A NetworkX directed graph representing the graph.
337
+ Any: A NetworkX directed graph representing the graph.
389
338
 
390
339
  Examples:
391
- >>> graph = Graph()
392
- >>> nx_graph = graph.to_networkx()
340
+ >>> graph = Graph()
341
+ >>> nx_graph = graph.to_networkx()
393
342
  """
394
343
 
395
344
  SysUtil.check_import("networkx")
@@ -400,11 +349,13 @@ class Graph(BaseRelatableNode):
400
349
  for node_id, node in self.nodes.items():
401
350
  node_info = node.to_dict()
402
351
  node_info.pop("node_id")
352
+ node_info.update({"class_name": node.__class__.__name__})
403
353
  g.add_node(node_id, **node_info)
404
354
 
405
355
  for _, relationship in self.relationships.items():
406
356
  relationship_info = relationship.to_dict()
407
357
  relationship_info.pop("node_id")
358
+ relationship_info.update({"class_name": relationship.__class__.__name__})
408
359
  source_node_id = relationship_info.pop("source_node_id")
409
360
  target_node_id = relationship_info.pop("target_node_id")
410
361
  g.add_edge(source_node_id, target_node_id, **relationship_info)
@@ -413,36 +364,44 @@ class Graph(BaseRelatableNode):
413
364
 
414
365
 
415
366
  class Structure(BaseRelatableNode):
416
- """
417
- Represents the structure of a graph consisting of nodes and relationship.
418
- """
419
-
420
367
  graph: Graph = Graph()
368
+ pending_ins: dict = {}
369
+ pending_outs: deque = deque()
370
+ execute_stop: bool = False
371
+ condition_check_result: bool | None = None
421
372
 
422
- def add_node(self, node: BaseNode) -> None:
423
- """
424
- Adds a node to the structure.
425
-
426
- Args:
427
- node (T): The node instance to be added.
428
- """
373
+ def add_node(self, node: BaseNode):
429
374
  self.graph.add_node(node)
430
375
 
431
- def add_relationship(self, relationship: Relationship) -> None:
432
- """
433
- Adds a relationship to the structure.
434
-
435
- Args:
436
- relationship (R): The relationship instance to be added.
437
- """
376
+ def add_relationship(
377
+ self,
378
+ from_node: BaseNode,
379
+ to_node: BaseNode,
380
+ bundle=False,
381
+ condition=None,
382
+ **kwargs,
383
+ ):
384
+ if isinstance(from_node, Tool) or isinstance(from_node, ActionSelection):
385
+ raise ValueError(
386
+ f"type {type(from_node)} should not be the head of the relationship, "
387
+ f"please switch position and attach it to the tail of the relationship"
388
+ )
389
+ if isinstance(to_node, Tool) or isinstance(to_node, ActionSelection):
390
+ bundle = True
391
+ relationship = Relationship(
392
+ source_node_id=from_node.id_,
393
+ target_node_id=to_node.id_,
394
+ bundle=bundle,
395
+ **kwargs,
396
+ )
397
+ if condition:
398
+ relationship.add_condition(condition)
438
399
  self.graph.add_relationship(relationship)
439
400
 
440
401
  def get_relationships(self) -> list[Relationship]:
441
402
  return self.graph.get_node_relationships()
442
403
 
443
- def get_node_relationships(
444
- self, node: BaseNode, out_edge=True, labels=None
445
- ) -> List[Relationship]:
404
+ def get_node_relationships(self, node: BaseNode, out_edge=True, labels=None):
446
405
  relationships = self.graph.get_node_relationships(node, out_edge)
447
406
  if labels:
448
407
  if not isinstance(labels, list):
@@ -454,48 +413,192 @@ class Structure(BaseRelatableNode):
454
413
  relationships = result
455
414
  return relationships
456
415
 
457
- def node_exist(self, node: BaseNode) -> bool:
458
- """
459
- Checks if a node exists in the structure.
416
+ def get_predecessors(self, node: BaseNode):
417
+ return self.graph.get_predecessors(node)
460
418
 
461
- Args:
462
- node (T): The node instance or node ID to check for existence.
463
-
464
- Returns:
465
- bool: True if the node exists, False otherwise.
466
- """
419
+ def get_successors(self, node: BaseNode):
420
+ return self.graph.get_successors(node)
467
421
 
422
+ def node_exist(self, node: BaseNode) -> bool:
468
423
  return self.graph.node_exist(node)
469
424
 
470
425
  def relationship_exist(self, relationship: Relationship) -> bool:
471
- """
472
- Checks if a relationship exists in the structure.
473
-
474
- Args:
475
- relationship (R): The relationship instance to check for existence.
476
-
477
- Returns:
478
- bool: True if the relationship exists, False otherwise.
479
- """
480
426
  return self.graph.relationship_exist(relationship)
481
427
 
482
428
  def remove_node(self, node: BaseNode) -> BaseNode:
483
- """
484
- Removes a node and its associated relationship from the structure.
485
-
486
- Args:
487
- node (T): The node instance or node ID to be removed.
488
- """
489
429
  return self.graph.remove_node(node)
490
430
 
491
431
  def remove_relationship(self, relationship: Relationship) -> Relationship:
492
- """
493
- Removes a relationship from the structure.
494
-
495
- Args:
496
- relationship (R): The relationship instance to be removed.
497
- """
498
432
  return self.graph.remove_relationship(relationship)
499
433
 
500
434
  def is_empty(self) -> bool:
501
435
  return self.graph.is_empty()
436
+
437
+ def get_heads(self):
438
+ heads = []
439
+ for key in self.graph.node_relationships:
440
+ if not self.graph.node_relationships[key]["in"]:
441
+ heads.append(self.graph.nodes[key])
442
+ return heads
443
+
444
+ @staticmethod
445
+ def parse_to_action(instruction: BaseNode, bundled_nodes: deque):
446
+ action_node = ActionNode(instruction)
447
+ while bundled_nodes:
448
+ node = bundled_nodes.popleft()
449
+ if isinstance(node, ActionSelection):
450
+ action_node.action = node.action
451
+ action_node.action_kwargs = node.action_kwargs
452
+ elif isinstance(node, Tool):
453
+ action_node.tools.append(node)
454
+ else:
455
+ raise ValueError("Invalid bundles nodes")
456
+ return action_node
457
+
458
+ async def check_condition(self, relationship, executable_id):
459
+ if relationship.condition.source_type == "structure":
460
+ return self.check_condition_structure(relationship)
461
+ elif relationship.condition.source_type == "executable":
462
+ self.send(
463
+ recipient_id=executable_id, category="condition", package=relationship
464
+ )
465
+ while self.condition_check_result is None:
466
+ await AsyncUtil.sleep(0.1)
467
+ self.process_relationship_condition(relationship.id_)
468
+ continue
469
+ check_result = self.condition_check_result
470
+ self.condition_check_result = None
471
+ return check_result
472
+ else:
473
+ raise ValueError(f"Invalid source_type.")
474
+
475
+ def check_condition_structure(self, relationship):
476
+ return relationship.condition(self)
477
+
478
+ async def get_next_step(self, current_node: BaseNode, executable_id):
479
+ next_nodes = []
480
+ next_relationships = self.get_node_relationships(current_node)
481
+ for relationship in next_relationships:
482
+ if relationship.bundle:
483
+ continue
484
+ if relationship.condition:
485
+ check = await self.check_condition(relationship, executable_id)
486
+ if not check:
487
+ continue
488
+ node = self.graph.nodes[relationship.target_node_id]
489
+ further_relationships = self.get_node_relationships(node)
490
+ bundled_nodes = deque()
491
+ for f_relationship in further_relationships:
492
+ if f_relationship.bundle:
493
+ bundled_nodes.append(
494
+ self.graph.nodes[f_relationship.target_node_id]
495
+ )
496
+ if bundled_nodes:
497
+ node = self.parse_to_action(node, bundled_nodes)
498
+ next_nodes.append(node)
499
+ return next_nodes
500
+
501
+ def acyclic(self):
502
+ check_deque = deque(self.graph.nodes.keys())
503
+ check_dict = {
504
+ key: 0 for key in self.graph.nodes.keys()
505
+ } # 0: not visited, 1: temp, 2: perm
506
+
507
+ def visit(key):
508
+ if check_dict[key] == 2:
509
+ return True
510
+ elif check_dict[key] == 1:
511
+ return False
512
+
513
+ check_dict[key] = 1
514
+
515
+ out_relationships = self.graph.get_node_relationships(self.graph.nodes[key])
516
+ for node in out_relationships:
517
+ check = visit(node.target_node_id)
518
+ if not check:
519
+ return False
520
+
521
+ check_dict[key] = 2
522
+ return True
523
+
524
+ while check_deque:
525
+ key = check_deque.pop()
526
+ check = visit(key)
527
+ if not check:
528
+ return False
529
+ return True
530
+
531
+ def send(self, recipient_id: str, category: str, package: Any) -> None:
532
+ mail = BaseMail(
533
+ sender_id=self.id_,
534
+ recipient_id=recipient_id,
535
+ category=category,
536
+ package=package,
537
+ )
538
+ self.pending_outs.append(mail)
539
+
540
+ def process_relationship_condition(self, relationship_id):
541
+ for key in list(self.pending_ins.keys()):
542
+ skipped_requests = deque()
543
+ while self.pending_ins[key]:
544
+ mail = self.pending_ins[key].popleft()
545
+ if (
546
+ mail.category == "condition"
547
+ and mail.package["relationship_id"] == relationship_id
548
+ ):
549
+ self.condition_check_result = mail.package["check_result"]
550
+ else:
551
+ skipped_requests.append(mail)
552
+ self.pending_ins[key] = skipped_requests
553
+
554
+ async def process(self) -> None:
555
+ for key in list(self.pending_ins.keys()):
556
+ while self.pending_ins[key]:
557
+ mail = self.pending_ins[key].popleft()
558
+ if mail.category == "start":
559
+ next_nodes = self.get_heads()
560
+ elif mail.category == "end":
561
+ self.execute_stop = True
562
+ return
563
+ elif mail.category == "node_id":
564
+ if mail.package not in self.graph.nodes:
565
+ raise ValueError(
566
+ f"Node {mail.package} does not exist in the structure {self.id_}"
567
+ )
568
+ next_nodes = await self.get_next_step(
569
+ self.graph.nodes[mail.package], mail.sender_id
570
+ )
571
+ elif mail.category == "node" and isinstance(mail.package, BaseNode):
572
+ if not self.node_exist(mail.package):
573
+ raise ValueError(
574
+ f"Node {mail.package} does not exist in the structure {self.id_}"
575
+ )
576
+ next_nodes = await self.get_next_step(mail.package, mail.sender_id)
577
+ else:
578
+ raise ValueError(f"Invalid mail type for structure")
579
+
580
+ if not next_nodes: # tail
581
+ self.send(
582
+ recipient_id=mail.sender_id, category="end", package="end"
583
+ )
584
+ else:
585
+ if len(next_nodes) == 1:
586
+ self.send(
587
+ recipient_id=mail.sender_id,
588
+ category="node",
589
+ package=next_nodes[0],
590
+ )
591
+ else:
592
+ self.send(
593
+ recipient_id=mail.sender_id,
594
+ category="node_list",
595
+ package=next_nodes,
596
+ )
597
+
598
+ async def execute(self, refresh_time=1):
599
+ if not self.acyclic():
600
+ raise ValueError("Structure is not acyclic")
601
+
602
+ while not self.execute_stop:
603
+ await self.process()
604
+ await AsyncUtil.sleep(refresh_time)