lionagi 0.0.305__py3-none-any.whl → 0.0.307__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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)