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.
- lionagi/__init__.py +2 -5
- lionagi/core/__init__.py +7 -4
- lionagi/core/agent/__init__.py +3 -0
- lionagi/core/agent/base_agent.py +46 -0
- lionagi/core/branch/__init__.py +4 -0
- lionagi/core/branch/base/__init__.py +0 -0
- lionagi/core/branch/base_branch.py +100 -78
- lionagi/core/branch/branch.py +22 -34
- lionagi/core/branch/branch_flow_mixin.py +3 -7
- lionagi/core/branch/executable_branch.py +192 -0
- lionagi/core/branch/util.py +77 -162
- lionagi/core/direct/__init__.py +13 -0
- lionagi/core/direct/parallel_predict.py +127 -0
- lionagi/core/direct/parallel_react.py +0 -0
- lionagi/core/direct/parallel_score.py +0 -0
- lionagi/core/direct/parallel_select.py +0 -0
- lionagi/core/direct/parallel_sentiment.py +0 -0
- lionagi/core/direct/predict.py +174 -0
- lionagi/core/direct/react.py +33 -0
- lionagi/core/direct/score.py +163 -0
- lionagi/core/direct/select.py +144 -0
- lionagi/core/direct/sentiment.py +51 -0
- lionagi/core/direct/utils.py +83 -0
- lionagi/core/flow/__init__.py +0 -3
- lionagi/core/flow/monoflow/{mono_react.py → ReAct.py} +52 -9
- lionagi/core/flow/monoflow/__init__.py +9 -0
- lionagi/core/flow/monoflow/{mono_chat.py → chat.py} +11 -11
- lionagi/core/flow/monoflow/{mono_chat_mixin.py → chat_mixin.py} +33 -27
- lionagi/core/flow/monoflow/{mono_followup.py → followup.py} +7 -6
- lionagi/core/flow/polyflow/__init__.py +1 -0
- lionagi/core/flow/polyflow/{polychat.py → chat.py} +15 -3
- lionagi/core/mail/__init__.py +8 -0
- lionagi/core/mail/mail_manager.py +88 -40
- lionagi/core/mail/schema.py +32 -6
- lionagi/core/messages/__init__.py +3 -0
- lionagi/core/messages/schema.py +56 -25
- lionagi/core/prompt/__init__.py +0 -0
- lionagi/core/prompt/prompt_template.py +0 -0
- lionagi/core/schema/__init__.py +7 -5
- lionagi/core/schema/action_node.py +29 -0
- lionagi/core/schema/base_mixin.py +56 -59
- lionagi/core/schema/base_node.py +35 -38
- lionagi/core/schema/condition.py +24 -0
- lionagi/core/schema/data_logger.py +98 -98
- lionagi/core/schema/data_node.py +19 -19
- lionagi/core/schema/prompt_template.py +0 -0
- lionagi/core/schema/structure.py +293 -190
- lionagi/core/session/__init__.py +1 -3
- lionagi/core/session/session.py +196 -214
- lionagi/core/tool/tool_manager.py +95 -103
- lionagi/integrations/__init__.py +1 -3
- lionagi/integrations/bridge/langchain_/documents.py +17 -18
- lionagi/integrations/bridge/langchain_/langchain_bridge.py +14 -14
- lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py +22 -22
- lionagi/integrations/bridge/llamaindex_/node_parser.py +12 -12
- lionagi/integrations/bridge/llamaindex_/reader.py +11 -11
- lionagi/integrations/bridge/llamaindex_/textnode.py +7 -7
- lionagi/integrations/config/openrouter_configs.py +0 -1
- lionagi/integrations/provider/oai.py +26 -26
- lionagi/integrations/provider/services.py +38 -38
- lionagi/libs/__init__.py +34 -1
- lionagi/libs/ln_api.py +211 -221
- lionagi/libs/ln_async.py +53 -60
- lionagi/libs/ln_convert.py +118 -120
- lionagi/libs/ln_dataframe.py +32 -33
- lionagi/libs/ln_func_call.py +334 -342
- lionagi/libs/ln_nested.py +99 -107
- lionagi/libs/ln_parse.py +175 -158
- lionagi/libs/sys_util.py +52 -52
- lionagi/tests/test_core/test_base_branch.py +427 -427
- lionagi/tests/test_core/test_branch.py +292 -292
- lionagi/tests/test_core/test_mail_manager.py +57 -57
- lionagi/tests/test_core/test_session.py +254 -266
- lionagi/tests/test_core/test_session_base_util.py +299 -300
- lionagi/tests/test_core/test_tool_manager.py +70 -74
- lionagi/tests/test_libs/test_nested.py +2 -7
- lionagi/tests/test_libs/test_parse.py +2 -2
- lionagi/version.py +1 -1
- {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/METADATA +4 -2
- lionagi-0.0.307.dist-info/RECORD +115 -0
- lionagi-0.0.305.dist-info/RECORD +0 -94
- {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/LICENSE +0 -0
- {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/WHEEL +0 -0
- {lionagi-0.0.305.dist-info → lionagi-0.0.307.dist-info}/top_level.txt +0 -0
lionagi/core/schema/structure.py
CHANGED
@@ -1,10 +1,17 @@
|
|
1
|
-
|
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
|
5
|
-
from lionagi.libs import ln_func_call as func_call
|
6
|
+
from lionagi.libs import SysUtil, func_call, AsyncUtil
|
6
7
|
|
7
|
-
from
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
33
|
+
source_node_id: str
|
34
|
+
target_node_id: str
|
35
|
+
bundle: bool = False
|
36
|
+
condition: Callable = None
|
94
37
|
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
56
|
+
obj (Dict[str, Any]): The object to check.
|
118
57
|
|
119
58
|
Returns:
|
120
|
-
|
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
|
-
|
68
|
+
obj (Dict[str, Any]): The object to check.
|
130
69
|
|
131
70
|
Returns:
|
132
|
-
|
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
|
-
|
80
|
+
obj (Dict[str, Any]): The object to check.
|
142
81
|
|
143
82
|
Returns:
|
144
|
-
|
83
|
+
bool: True if both nodes exist.
|
145
84
|
|
146
85
|
Raises:
|
147
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
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
|
-
|
166
|
+
relationship (Relationship): The relationship to add.
|
228
167
|
|
229
168
|
Raises:
|
230
|
-
|
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
|
-
|
253
|
-
|
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
|
-
|
195
|
+
List[Relationship]: A list of relationship.
|
257
196
|
|
258
197
|
Raises:
|
259
|
-
|
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
|
-
|
234
|
+
node (BaseNode): The node to remove.
|
286
235
|
|
287
236
|
Returns:
|
288
|
-
|
237
|
+
BaseNode: The removed node.
|
289
238
|
|
290
239
|
Raises:
|
291
|
-
|
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
|
-
|
263
|
+
relationship (Relationship): The relationship to remove.
|
315
264
|
|
316
265
|
Returns:
|
317
|
-
|
266
|
+
Relationship: The removed relationship.
|
318
267
|
|
319
268
|
Raises:
|
320
|
-
|
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
|
-
|
286
|
+
node (BaseNode): The node to check.
|
338
287
|
|
339
288
|
Returns:
|
340
|
-
|
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
|
-
|
301
|
+
relationship (Relationship): The relationship to check.
|
353
302
|
|
354
303
|
Returns:
|
355
|
-
|
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
|
-
|
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
|
-
|
334
|
+
**kwargs: Additional keyword arguments to pass to the NetworkX DiGraph constructor.
|
386
335
|
|
387
336
|
Returns:
|
388
|
-
|
337
|
+
Any: A NetworkX directed graph representing the graph.
|
389
338
|
|
390
339
|
Examples:
|
391
|
-
|
392
|
-
|
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)
|
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(
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
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
|
-
|
462
|
-
|
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)
|