lionagi 0.0.316__py3-none-any.whl → 0.1.1__py3-none-any.whl

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