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.
- 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)
|