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
@@ -0,0 +1,362 @@
1
+ """
2
+ This module provides the BaseStructure class, which represents a node with
3
+ internal edges and nodes, including methods for managing them.
4
+
5
+ The BaseStructure class inherits from the Node class and provides functionality
6
+ for adding, removing, and querying nodes and edges within the structure, as
7
+ well as checking if the structure is empty.
8
+ """
9
+
10
+ from functools import singledispatchmethod
11
+ from typing import Any
12
+ from pydantic import Field
13
+
14
+ from lionagi.libs import convert, func_call
15
+ from .condition import Condition
16
+ from .edge import Edge
17
+ from .node import Node
18
+ from .action import ActionSelection
19
+ from lionagi.core.tool import Tool
20
+
21
+
22
+ class BaseStructure(Node):
23
+ """
24
+ Represents a node with internal edges and nodes, including methods for
25
+ managing them.
26
+
27
+ Provides functionality for adding, removing, and querying nodes and edges
28
+ within the structure, as well as checking if the structure is empty.
29
+ """
30
+
31
+ internal_nodes: dict[str, Node] = Field(
32
+ default_factory=dict,
33
+ description="A dictionary of all nodes within the structure, keyed by \
34
+ node ID.",
35
+ )
36
+
37
+ @property
38
+ def internal_edges(self) -> dict[str, Edge]:
39
+ """
40
+ Gets all internal edges indexed by their ID.
41
+
42
+ Returns:
43
+ dict[str, Edge]: A dictionary of all internal edges within the
44
+ structure.
45
+ """
46
+ edges = {}
47
+ for i in self.internal_nodes.values():
48
+ for _, edge in i.edges.items():
49
+ if edge.id_ not in edges:
50
+ edges[edge.id_] = edge
51
+ return edges
52
+
53
+ @property
54
+ def is_empty(self) -> bool:
55
+ """
56
+ Checks if the structure is empty (contains no nodes).
57
+
58
+ Returns:
59
+ bool: True if the structure has no nodes, False otherwise.
60
+ """
61
+ return len(self.internal_nodes) == 0
62
+
63
+ def clear(self):
64
+ """Clears all nodes and edges from the structure."""
65
+ self.internal_nodes.clear()
66
+
67
+ def get_node_edges(
68
+ self,
69
+ node: Node | str,
70
+ node_as: str = "both",
71
+ label: list | str = None,
72
+ ) -> dict[str, list[Edge]]:
73
+ """
74
+ Retrieves edges associated with a specific node.
75
+
76
+ Args:
77
+ node (Node | str): The node or its ID to query for edges.
78
+ node_as (str, optional): The role of the node in the edges.
79
+ Defaults to "both".
80
+ - "both" or "all": Retrieve edges where the node is either the
81
+ head or tail.
82
+ - "head", "predecessor", "outgoing", "out", or "predecessors":
83
+ Retrieve edges where the node is the head.
84
+ - "tail", "successor", "incoming", "in", or "successors":
85
+ Retrieve edges where the node is the tail.
86
+ label (list | str, optional): Filter edges by label(s). Defaults to
87
+ None.
88
+
89
+ Returns:
90
+ dict[str, list[Edge]]: A dictionary mapping related node IDs to
91
+ lists of associated edges based on the specified criteria.
92
+
93
+ Raises:
94
+ ValueError: If an invalid role is specified for the node.
95
+ """
96
+
97
+ node = self.get_node(node)
98
+
99
+ if node_as in ["all", "both"]:
100
+ all_node_edges = {i: [] for i in node.related_nodes}
101
+
102
+ for k, v in node.node_relations["points_to"].items():
103
+ all_node_edges[k].append(v)
104
+
105
+ for k, v in node.node_relations["pointed_by"].items():
106
+ all_node_edges[k].append(v)
107
+
108
+ for k, v in all_node_edges.items():
109
+ all_node_edges[k] = convert.to_list(v, dropna=True, flatten=True)
110
+
111
+ if label:
112
+ for k, v in all_node_edges.items():
113
+ all_node_edges[k] = (
114
+ v
115
+ if v.label in convert.to_list(label, dropna=True, flatten=True)
116
+ else []
117
+ )
118
+
119
+ return {k: v for k, v in all_node_edges.items() if len(v) > 0}
120
+
121
+ elif node_as in ["head", "predecessor", "outgoing", "out", "predecessors"]:
122
+ return node.node_relations["points_to"]
123
+
124
+ elif node_as in ["tail", "successor", "incoming", "in", "successors"]:
125
+ return node.node_relations["pointed_by"]
126
+
127
+ else:
128
+ raise ValueError(
129
+ f"Invalid value for self_as: {node_as}, must be 'head' or 'tail'"
130
+ )
131
+
132
+ def relate_nodes(
133
+ self,
134
+ head: Node,
135
+ tail: Node,
136
+ condition: Condition | None = None,
137
+ bundle=False,
138
+ label=None,
139
+ **kwargs,
140
+ ):
141
+ """
142
+ Relates two nodes within the structure with an edge.
143
+
144
+ Args:
145
+ head (Node): The head node of the edge.
146
+ tail (Node): The tail node of the edge.
147
+ condition (Condition | None, optional): The condition associated
148
+ with the edge. Defaults to None.
149
+ label (optional): The label for the edge.
150
+ **kwargs: Additional keyword arguments for the edge creation.
151
+
152
+ Raises:
153
+ ValueError: If there is an error adding the edge.
154
+ """
155
+ try:
156
+ if isinstance(head, Tool) or isinstance(head, ActionSelection):
157
+ raise ValueError(
158
+ f"type {type(tail)} should not be the head of the relationship, "
159
+ f"please switch position and attach it to the tail of the relationship"
160
+ )
161
+
162
+ if isinstance(tail, Tool) or isinstance(tail, ActionSelection):
163
+ bundle = True
164
+
165
+ if head.id_ not in self.internal_nodes:
166
+ self.add_node(head)
167
+
168
+ if tail.id_ not in self.internal_nodes:
169
+ self.add_node(tail)
170
+
171
+ head.relate(
172
+ tail,
173
+ node_as="head",
174
+ condition=condition,
175
+ label=label,
176
+ bundle=bundle,
177
+ **kwargs,
178
+ )
179
+ except Exception as e:
180
+ raise ValueError(f"Error adding edge: {e}") from e
181
+
182
+ def remove_edge(
183
+ self, edge: Edge | str | list[str | Edge] | dict[str, Edge]
184
+ ) -> bool:
185
+ """
186
+ Removes one or more edges from the structure.
187
+
188
+ Args:
189
+ edge: The edge(s) to be removed, specified as single edge, its ID,
190
+ a list, or a dictionary of edges.
191
+
192
+ Returns:
193
+ bool: True if all specified edges were successfully removed, False
194
+ otherwise.
195
+ """
196
+ if isinstance(edge, list):
197
+ for i in edge:
198
+ self._remove_edge(i)
199
+
200
+ elif isinstance(edge, dict):
201
+ for i in edge.values():
202
+ self._remove_edge(i)
203
+
204
+ elif isinstance(edge, (Edge, str)):
205
+ self._remove_edge(edge)
206
+
207
+ @singledispatchmethod
208
+ def add_node(self, node: Any) -> None:
209
+ """
210
+ Method placeholder for adding a node. Will be implemented by register
211
+ decorators.
212
+
213
+ Args:
214
+ node (Any): The node to be added.
215
+
216
+ Raises:
217
+ NotImplementedError: If the method is not implemented for the type
218
+ of node.
219
+ """
220
+ raise NotImplementedError
221
+
222
+ @add_node.register(Node)
223
+ def _(self, node) -> None:
224
+ if node.id_ not in self.internal_nodes:
225
+ self.internal_nodes[node.id_] = node
226
+ else:
227
+ raise ValueError(f"Node {node.id_} already exists in structure.")
228
+
229
+ @add_node.register(list)
230
+ def _(self, node) -> None:
231
+ for _node in node:
232
+ self.add_node(_node)
233
+
234
+ @add_node.register(dict)
235
+ def _(self, node) -> None:
236
+ self.add_node(list(node.values()))
237
+
238
+ def get_node(self, node, default="undefined"):
239
+ """
240
+ Retrieves one or more nodes by ID, node instance, or a collection of
241
+ IDs/nodes.
242
+
243
+ Args:
244
+ node: A single node, node ID, or a collection of nodes/IDs to
245
+ retrieve.
246
+ default: The default value to return if a node is not found.
247
+
248
+ Returns:
249
+ The node(s) corresponding to the input, or the default value for
250
+ any not found.
251
+ """
252
+ if not isinstance(node, (list, dict, set)):
253
+ return self._get_node(node, default=default)
254
+ else:
255
+ node = list(node) if isinstance(node, set) else node
256
+ node = list(node.values()) if isinstance(node, dict) else node
257
+
258
+ return func_call.lcall(node, self._get_node, default=default)
259
+
260
+ @singledispatchmethod
261
+ def remove_node(self, node: Any) -> bool:
262
+ """
263
+ Method placeholder for removing a node. Will be implemented by register
264
+ decorators.
265
+
266
+ Args:
267
+ node (Any): The node to be removed.
268
+
269
+ Returns:
270
+ bool: True if the node was successfully removed, False otherwise.
271
+
272
+ Raises:
273
+ NotImplementedError: If the method is not implemented for the type
274
+ of node.
275
+ """
276
+ return NotImplementedError
277
+
278
+ @remove_node.register(Node)
279
+ def _(self, node: Node) -> bool:
280
+ return self._remove_node(node)
281
+
282
+ @remove_node.register(str)
283
+ def _(self, node: str) -> bool:
284
+ return self._remove_node(node)
285
+
286
+ @remove_node.register(list)
287
+ def _(self, node: list[str | Node]) -> bool:
288
+ for i in node:
289
+ self.remove_node(i)
290
+
291
+ @remove_node.register(dict)
292
+ def _(self, node: dict[str, Node]) -> bool:
293
+ self.remove_node(list(node.values()))
294
+
295
+ def _pop_node(self, node: Node | str, default="undefined"):
296
+ node = self.get_node(node, default=default)
297
+ self.remove_node(node)
298
+ return node
299
+
300
+ @singledispatchmethod
301
+ def pop_node(self, node: Any, default="undefined") -> Node:
302
+ return NotImplementedError
303
+
304
+ @pop_node.register(Node)
305
+ def _(self, node: Node, default="undefined") -> Node:
306
+ return self._pop_node(node, default=default)
307
+
308
+ @pop_node.register(str)
309
+ def _(self, node: str, default="undefined") -> Node:
310
+ return self._pop_node(node, default=default)
311
+
312
+ @pop_node.register(list)
313
+ def _(self, node: list[str | Node], default="undefined") -> list[Node]:
314
+ nodes = []
315
+ for i in node:
316
+ nodes.append(self.pop_node(i, default=default))
317
+ return nodes
318
+
319
+ @pop_node.register(dict)
320
+ def _(self, node: dict[str, Node], default="undefined") -> list[Node]:
321
+ return self.pop_node(list(node.values()), default=default)
322
+
323
+ def _remove_edge(self, edge: Edge | str) -> bool:
324
+ """Helper method to remove an edge by ID or edge instance.
325
+
326
+ Args:
327
+ edge (Edge | str): The edge or its ID to remove.
328
+
329
+ Returns:
330
+ bool: True if the edge was successfully removed, False otherwise.
331
+
332
+ Raises:
333
+ ValueError: If the edge is not found within the structure.
334
+ """
335
+ edge_id = edge.id_ if isinstance(edge, Edge) else edge
336
+ if not edge_id in self.internal_edges:
337
+ raise ValueError(f"Edge {edge_id} not found in structure.")
338
+
339
+ edge = self.internal_edges[edge_id]
340
+
341
+ head: Node = self.get_node(edge.head)
342
+ tail: Node = self.get_node(edge.tail)
343
+
344
+ head.unrelate(tail, edge=edge)
345
+
346
+ def _get_node(self, node: Node | str, default="undefined") -> Node:
347
+ node_id_ = node.id_ if isinstance(node, Node) else node
348
+ if not node_id_ in self.internal_nodes:
349
+ if default == "undefined":
350
+ raise KeyError(f"Node {node_id_} not found in structure.")
351
+ return default
352
+ return self.internal_nodes[node_id_]
353
+
354
+ def _remove_node(self, node: Node | str) -> bool:
355
+ try:
356
+ node = self.get_node(node)
357
+ related_nodes = self.get_node(node.related_nodes)
358
+ func_call.lcall(related_nodes, node.unrelate)
359
+ self.internal_nodes.pop(node.id_)
360
+ return True
361
+ except Exception as e:
362
+ raise ValueError(f"Error removing node: {e}") from e
@@ -0,0 +1,20 @@
1
+ from collections import deque
2
+ from pydantic import Field
3
+ from pydantic.dataclasses import dataclass
4
+
5
+ from lionagi.core.generic.mail import Mail
6
+ from lionagi.core.generic.signal import Signal
7
+
8
+
9
+ @dataclass
10
+ class Transfer:
11
+
12
+ schedule: dict[str, deque[Mail | Signal]] = Field(
13
+ default_factory=dict,
14
+ description="The sequence of all pending mails - {direction: deque[mail_id]}",
15
+ )
16
+
17
+ @property
18
+ def is_empty(self) -> bool:
19
+ """Returns a flag indicating whether the transfer is empty."""
20
+ return not any(self.schedule.values())
@@ -0,0 +1,40 @@
1
+ from pydantic.dataclasses import dataclass
2
+ from typing import Any
3
+ from abc import ABC, abstractmethod
4
+ from pydantic import Field
5
+
6
+ from .node import Node
7
+
8
+
9
+ @dataclass
10
+ class Work:
11
+
12
+ work_completed: bool = Field(
13
+ default=False,
14
+ description="A flag indicating whether the work is already completed.",
15
+ )
16
+
17
+ work_result: Any | None = Field(
18
+ default=None,
19
+ description="The result of the work.",
20
+ )
21
+
22
+ context: dict | str | None = Field(
23
+ None, description="The context buffer for the next instruction."
24
+ )
25
+
26
+
27
+ class Worker(Node, ABC):
28
+ stopped: bool = False
29
+
30
+ @abstractmethod
31
+ def perform(self, *args, **kwargs):
32
+ pass
33
+
34
+ @abstractmethod
35
+ def forward(self, *args, **kwargs):
36
+ pass
37
+
38
+ def stop(self):
39
+ self.stopped = True
40
+ return True
@@ -0,0 +1,126 @@
1
+ """
2
+ This module provides the Graph class, which represents a graph structure
3
+ extending the capabilities of BaseStructure to include graph-specific
4
+ properties and methods, such as detecting heads, acyclicity, and conversion to
5
+ NetworkX for visualization.
6
+ """
7
+
8
+ from collections import deque
9
+ from typing import Any
10
+
11
+ from lionagi.core.generic import Node
12
+ from lionagi.core.generic import BaseStructure
13
+
14
+
15
+ class Graph(BaseStructure):
16
+ """
17
+ Represents a graph structure extending the capabilities of BaseStructure.
18
+
19
+ Includes graph-specific properties and methods, such as detecting heads,
20
+ acyclicity, and conversion to NetworkX for visualization.
21
+ """
22
+
23
+ def get_heads(self):
24
+ return [
25
+ node
26
+ for node in self.internal_nodes.values()
27
+ if not node.relations.pointed_by
28
+ ]
29
+
30
+ @property
31
+ def acyclic(self) -> bool:
32
+ """
33
+ Checks if the graph is acyclic.
34
+
35
+ An acyclic graph contains no cycles and can be represented as a directed
36
+ acyclic graph (DAG).
37
+
38
+ Returns:
39
+ bool: True if the graph is acyclic, False otherwise.
40
+ """
41
+ node_ids = list(self.internal_nodes.keys())
42
+ check_deque = deque(node_ids)
43
+ check_dict = {key: 0 for key in node_ids} # 0: not visited, 1: temp, 2: perm
44
+
45
+ def visit(key):
46
+ if check_dict[key] == 2:
47
+ return True
48
+ elif check_dict[key] == 1:
49
+ return False
50
+
51
+ check_dict[key] = 1
52
+
53
+ points_to_edges = self.internal_nodes[key].relations.points_to
54
+
55
+ for _, edge in points_to_edges.items():
56
+ check = visit(edge.tail)
57
+ if not check:
58
+ return False
59
+
60
+ check_dict[key] = 2
61
+ return True
62
+
63
+ while check_deque:
64
+ key = check_deque.pop()
65
+ check = visit(key)
66
+ if not check:
67
+ return False
68
+ return True
69
+
70
+ def to_networkx(self, **kwargs) -> Any:
71
+ """
72
+ Converts the graph into a NetworkX graph object.
73
+
74
+ The NetworkX graph object can be used for further analysis or
75
+ visualization.
76
+
77
+ Args:
78
+ **kwargs: Additional keyword arguments to pass to the NetworkX graph
79
+ constructor.
80
+
81
+ Returns:
82
+ Any: A NetworkX graph object representing the current graph
83
+ structure.
84
+ """
85
+ from lionagi.libs import SysUtil
86
+
87
+ SysUtil.check_import("networkx")
88
+
89
+ from networkx import DiGraph
90
+
91
+ g = DiGraph(**kwargs)
92
+ for node_id, node in self.internal_nodes.items():
93
+ node_info = node.to_dict()
94
+ node_info.pop("id_")
95
+ node_info.update({"class_name": node.class_name()})
96
+ g.add_node(node_id, **node_info)
97
+
98
+ for _edge in list(self.internal_edges.values()):
99
+ edge_info = _edge.to_dict()
100
+ edge_info.pop("id_")
101
+ edge_info.update({"class_name": _edge.__class__.__name__})
102
+ source_node_id = edge_info.pop("head")
103
+ target_node_id = edge_info.pop("tail")
104
+ g.add_edge(source_node_id, target_node_id, **edge_info)
105
+
106
+ return g
107
+
108
+ def display(self, **kwargs):
109
+ """
110
+ Displays the graph using NetworkX's drawing capabilities.
111
+
112
+ This method requires NetworkX and a compatible plotting library (like
113
+ matplotlib) to be installed.
114
+
115
+ Args:
116
+ **kwargs: Additional keyword arguments to pass to the NetworkX graph
117
+ constructor.
118
+ """
119
+ from lionagi.libs import SysUtil
120
+
121
+ SysUtil.check_import("networkx")
122
+
123
+ from networkx import draw
124
+
125
+ g = self.to_networkx(**kwargs)
126
+ draw(g)