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,381 @@
1
+ from collections import deque
2
+ import json
3
+ from typing import Callable
4
+
5
+ from lionagi.core.execute.base_executor import BaseExecutor
6
+ from lionagi.integrations.storage.neo4j import Neo4j
7
+ from lionagi.integrations.storage.storage_util import ParseNode
8
+ from lionagi.core.generic import ActionNode
9
+ from lionagi.core.agent.base_agent import BaseAgent
10
+ from lionagi.core.execute.instruction_map_executor import InstructionMapExecutor
11
+
12
+ from lionagi.core.mail.schema import BaseMail
13
+ from lionagi.core.tool import Tool
14
+ from lionagi.core.generic import ActionSelection, Edge
15
+
16
+ from lionagi.libs import AsyncUtil
17
+
18
+
19
+ class Neo4jExecutor(BaseExecutor):
20
+ """
21
+ Executes tasks within a Neo4j graph database, handling dynamic instruction flows and conditional logic across various nodes and agents.
22
+
23
+ Attributes:
24
+ driver (Neo4j | None): Connection driver to the Neo4j database.
25
+ structure_id (str | None): Identifier for the structure being executed within the graph.
26
+ structure_name (str | None): Name of the structure being executed.
27
+ middle_agents (list | None): List of agents operating within the structure.
28
+ default_agent_executable (BaseExecutor): Default executor for running tasks not handled by specific agents.
29
+ condition_check_result (bool | None): Result of the last condition check performed during execution.
30
+ """
31
+
32
+ driver: Neo4j | None
33
+ structure_id: str = None
34
+ structure_name: str = None
35
+ middle_agents: list | None = None
36
+ default_agent_executable: BaseExecutor = InstructionMapExecutor()
37
+ condition_check_result: bool | None = None
38
+
39
+ async def check_edge_condition(
40
+ self, condition, executable_id, request_source, head, tail
41
+ ):
42
+ """
43
+ Evaluates the condition associated with an edge in the graph, determining if execution should proceed along that edge.
44
+
45
+ Args:
46
+ condition: The condition object or logic to be evaluated.
47
+ executable_id (str): ID of the executor responsible for this condition check.
48
+ request_source (str): Origin of the request prompting this check.
49
+ head (str): ID of the head node in the edge.
50
+ tail (str): ID of the tail node in the edge.
51
+
52
+ Returns:
53
+ bool: Result of the condition check.
54
+ """
55
+ if condition.source_type == "structure":
56
+ return condition(self)
57
+ elif condition.source_type == "executable":
58
+ return await self._check_executable_condition(
59
+ condition, executable_id, head, tail, request_source
60
+ )
61
+
62
+ def _process_edge_condition(self, edge_id):
63
+ """
64
+ Process the condition of a edge.
65
+
66
+ Args:
67
+ edge_id (str): The ID of the edge.
68
+ """
69
+ for key in list(self.pending_ins.keys()):
70
+ skipped_requests = deque()
71
+ while self.pending_ins[key]:
72
+ mail: BaseMail = self.pending_ins[key].popleft()
73
+ if (
74
+ mail.category == "condition"
75
+ and mail.package["package"]["edge_id"] == edge_id
76
+ ):
77
+ self.condition_check_result = mail.package["package"][
78
+ "check_result"
79
+ ]
80
+ else:
81
+ skipped_requests.append(mail)
82
+ self.pending_ins[key] = skipped_requests
83
+
84
+ async def _check_executable_condition(
85
+ self, condition, executable_id, head, tail, request_source
86
+ ):
87
+ """
88
+ Sends a condition to be checked by an external executable and awaits the result.
89
+
90
+ Args:
91
+ condition: The condition object to be evaluated.
92
+ executable_id (str): ID of the executable that will evaluate the condition.
93
+ head (str): Starting node of the edge.
94
+ tail (str): Ending node of the edge.
95
+ request_source (str): Source of the request for condition evaluation.
96
+
97
+ Returns:
98
+ bool: The result of the condition check.
99
+ """
100
+ edge = Edge(head=head, tail=tail, condition=condition)
101
+ self.send(
102
+ recipient_id=executable_id,
103
+ category="condition",
104
+ package={"request_source": request_source, "package": edge},
105
+ )
106
+ while self.condition_check_result is None:
107
+ await AsyncUtil.sleep(0.1)
108
+ self._process_edge_condition(edge.id_)
109
+ continue
110
+ check_result = self.condition_check_result
111
+ self.condition_check_result = None
112
+ return check_result
113
+
114
+ @staticmethod
115
+ def parse_bundled_to_action(instruction, bundle_list):
116
+ """
117
+ Parses bundled actions and tools from a list of nodes, creating a composite action node from them.
118
+
119
+ Args:
120
+ instruction: The initial instruction leading to this bundle.
121
+ bundle_list (list): List of nodes bundled together.
122
+
123
+ Returns:
124
+ ActionNode: A node representing a composite action constructed from the bundled nodes.
125
+ """
126
+ bundled_nodes = deque()
127
+ for node_labels, node_properties in bundle_list:
128
+ try:
129
+ if "ActionSelection" in node_labels:
130
+ node = ParseNode.parse_actionSelection(node_properties)
131
+ bundled_nodes.append(node)
132
+ elif "Tool" in node_labels:
133
+ node = ParseNode.parse_tool(node_properties)
134
+ bundled_nodes.append(node)
135
+ else:
136
+ raise ValueError(
137
+ f"Invalid bundle node {node_properties.id}. Valid nodes are ActionSelection or Tool"
138
+ )
139
+ except Exception as e:
140
+ raise ValueError(
141
+ f"Failed to parse ActionSelection or Tool node {node_properties.id}. Error: {e}"
142
+ )
143
+
144
+ action_node = ActionNode(instruction=instruction)
145
+ while bundled_nodes:
146
+ node = bundled_nodes.popleft()
147
+ if isinstance(node, ActionSelection):
148
+ action_node.action = node.action
149
+ action_node.action_kwargs = node.action_kwargs
150
+ elif isinstance(node, Tool):
151
+ action_node.tools.append(node)
152
+ return action_node
153
+
154
+ def parse_agent(self, node_properties):
155
+ """
156
+ Parses agent properties and creates an agent executor.
157
+
158
+ Args:
159
+ node_properties (dict): Properties defining the agent.
160
+
161
+ Returns:
162
+ BaseAgent: An agent executor configured with the given properties.
163
+ """
164
+ output_parser = ParseNode.convert_to_def(node_properties["outputParser"])
165
+
166
+ structure = Neo4jExecutor(
167
+ driver=self.driver, structure_id=node_properties["structureId"]
168
+ )
169
+ agent = BaseAgent(
170
+ structure=structure,
171
+ executable=self.default_agent_executable,
172
+ output_parser=output_parser,
173
+ )
174
+ agent.id_ = node_properties["id"]
175
+ agent.timestamp = node_properties["timestamp"]
176
+ return agent
177
+
178
+ async def _next_node(
179
+ self, query_list, node_id=None, executable_id=None, request_source=None
180
+ ):
181
+ """
182
+ Processes the next set of nodes based on the results of a query list, applying conditions and preparing nodes
183
+ for further execution.
184
+
185
+ Args:
186
+ query_list (list): List of nodes and their properties.
187
+ node_id (str | None): Current node ID, if applicable.
188
+ executable_id (str | None): ID of the executor handling these nodes.
189
+ request_source (str | None): Source of the node processing request.
190
+
191
+ Returns:
192
+ list: Next nodes ready for processing.
193
+ """
194
+ next_nodes = []
195
+ for edge_properties, node_labels, node_properties in query_list:
196
+ if "condition" in edge_properties.keys():
197
+ try:
198
+ condition = json.loads(edge_properties["condition"])
199
+ condition_cls = await self.driver.get_condition_cls_code(
200
+ condition["class"]
201
+ )
202
+ condition_obj = ParseNode.parse_condition(condition, condition_cls)
203
+
204
+ head = node_id
205
+ tail = node_properties["id"]
206
+ check = await self.check_edge_condition(
207
+ condition_obj, executable_id, request_source, head, tail
208
+ )
209
+ if not check:
210
+ continue
211
+ except Exception as e:
212
+ raise ValueError(
213
+ f"Failed to use condition {edge_properties['condition']} from {node_id} to {node_properties['id']}, Error: {e}"
214
+ )
215
+
216
+ try:
217
+ if "System" in node_labels:
218
+ node = ParseNode.parse_system(node_properties)
219
+ elif "Instruction" in node_labels:
220
+ node = ParseNode.parse_instruction(node_properties)
221
+ elif "Agent" in node_labels:
222
+ node = self.parse_agent(node_properties)
223
+
224
+ else:
225
+ raise ValueError(
226
+ f"Invalid start node {node_properties.id}. Valid nodes are System or Instruction"
227
+ )
228
+ except Exception as e:
229
+ raise ValueError(
230
+ f"Failed to parse System or Instruction node {node_properties.id}. Error: {e}"
231
+ )
232
+
233
+ bundle_list = await self.driver.get_bundle(node.id_)
234
+
235
+ if bundle_list and "System" in node_labels:
236
+ raise ValueError("System node does not support bundle edge")
237
+ if bundle_list:
238
+ node = self.parse_bundled_to_action(node, bundle_list)
239
+ next_nodes.append(node)
240
+ return next_nodes
241
+
242
+ async def _handle_start(self):
243
+ """
244
+ Handles the start of execution, fetching and processing head nodes from the structure.
245
+
246
+ Raises:
247
+ ValueError: If there is an issue with finding or starting the structure.
248
+ """
249
+ try:
250
+ id, head_list = await self.driver.get_heads(
251
+ self.structure_name, self.structure_id
252
+ )
253
+ self.structure_id = id
254
+ return await self._next_node(head_list)
255
+ except Exception as e:
256
+ raise ValueError(f"Error in searching for structure in Neo4j. Error: {e}")
257
+
258
+ async def _handle_node_id(self, node_id, executable_id, request_source):
259
+ """
260
+ Handles the processing of a specific node ID, fetching its forward connections and conditions.
261
+
262
+ Args:
263
+ node_id (str): The node ID to process.
264
+ executable_id (str): ID of the executor handling this node.
265
+ request_source (str): Source of the node processing request.
266
+
267
+ Returns:
268
+ list: Next nodes derived from the given node ID.
269
+ """
270
+ check = await self.driver.node_exist(node_id)
271
+ if not check:
272
+ raise ValueError(f"Node {node_id} if not found in the database")
273
+ node_list = await self.driver.get_forwards(node_id)
274
+ return await self._next_node(node_list, node_id, executable_id, request_source)
275
+
276
+ async def _handle_mail(self, mail: BaseMail):
277
+ """
278
+ Processes incoming mail, determining the next action based on the mail's category and content.
279
+
280
+ Args:
281
+ mail (BaseMail): The incoming mail to be processed.
282
+
283
+ Raises:
284
+ ValueError: If there is an error processing the mail.
285
+ """
286
+ if mail.category == "start":
287
+ try:
288
+ return await self._handle_start()
289
+ except Exception as e:
290
+ raise ValueError(f"Error in start. Error: {e}")
291
+
292
+ elif mail.category == "end":
293
+ self.execute_stop = True
294
+ return None
295
+
296
+ elif mail.category == "node_id":
297
+ try:
298
+ node_id = mail.package["package"]
299
+ executable_id = mail.sender_id
300
+ request_source = mail.package["request_source"]
301
+ return await self._handle_node_id(
302
+ node_id, executable_id, request_source
303
+ )
304
+ except Exception as e:
305
+ raise ValueError(f"Error in handling node_id: {e}")
306
+ elif mail.category == "node":
307
+ try:
308
+ node_id = mail.package["package"].id_
309
+ executable_id = mail.sender_id
310
+ request_source = mail.package["request_source"]
311
+ return await self._handle_node_id(
312
+ node_id, executable_id, request_source
313
+ )
314
+ except Exception as e:
315
+ raise ValueError(f"Error in handling node: {e}")
316
+ else:
317
+ raise ValueError(f"Invalid mail type for structure")
318
+
319
+ def _send_mail(self, next_nodes: list | None, mail: BaseMail):
320
+ """
321
+ Sends out mail to the next nodes or marks the execution as ended if there are no next nodes.
322
+
323
+ Args:
324
+ next_nodes (list | None): List of next nodes to which mail should be sent.
325
+ mail (BaseMail): The current mail being processed.
326
+ """
327
+ if not next_nodes: # tail
328
+ self.send(
329
+ recipient_id=mail.sender_id,
330
+ category="end",
331
+ package={
332
+ "request_source": mail.package["request_source"],
333
+ "package": "end",
334
+ },
335
+ )
336
+ else:
337
+ if len(next_nodes) == 1:
338
+ self.send(
339
+ recipient_id=mail.sender_id,
340
+ category="node",
341
+ package={
342
+ "request_source": mail.package["request_source"],
343
+ "package": next_nodes[0],
344
+ },
345
+ )
346
+ else:
347
+ self.send(
348
+ recipient_id=mail.sender_id,
349
+ category="node_list",
350
+ package={
351
+ "request_source": mail.package["request_source"],
352
+ "package": next_nodes,
353
+ },
354
+ )
355
+
356
+ async def forward(self) -> None:
357
+ """
358
+ Forwards execution by processing all pending mails and advancing to next nodes or actions.
359
+ """
360
+ for key in list(self.pending_ins.keys()):
361
+ while self.pending_ins[key]:
362
+ mail: BaseMail = self.pending_ins[key].popleft()
363
+ try:
364
+ if mail == "end":
365
+ self.execute_stop = True
366
+ return
367
+ next_nodes = await self._handle_mail(mail)
368
+ self._send_mail(next_nodes, mail)
369
+ except Exception as e:
370
+ raise ValueError(f"Error handling mail: {e}") from e
371
+
372
+ async def execute(self, refresh_time=1):
373
+ """
374
+ Continuously executes the forward process at specified intervals until instructed to stop.
375
+
376
+ Args:
377
+ refresh_time (int): The time in seconds between execution cycles.
378
+ """
379
+ while not self.execute_stop:
380
+ await self.forward()
381
+ await AsyncUtil.sleep(refresh_time)
@@ -0,0 +1,314 @@
1
+ from typing import overload
2
+
3
+ from abc import ABC
4
+ from collections import deque
5
+
6
+ from lionagi.libs import AsyncUtil, convert
7
+
8
+ from lionagi.core.generic import BaseNode, ActionNode, ActionSelection, Edge
9
+ from lionagi.core.tool import Tool
10
+ from lionagi.core.mail.schema import BaseMail
11
+ from lionagi.core.execute.base_executor import BaseExecutor
12
+
13
+ from lionagi.libs import AsyncUtil
14
+ from lionagi.core.generic import Node, ActionSelection, Edge
15
+ from lionagi.core.tool import Tool
16
+
17
+ from lionagi.core.mail.schema import BaseMail
18
+ from lionagi.core.graph.graph import Graph
19
+
20
+
21
+ class StructureExecutor(BaseExecutor, Graph):
22
+ """
23
+ Executes tasks within a graph structure, handling dynamic node flows and conditional edge logic.
24
+
25
+ Attributes:
26
+ condition_check_result (bool | None): Result of the last condition check performed during execution,
27
+ used to control flow based on dynamic conditions.
28
+ """
29
+
30
+ condition_check_result: bool | None = None
31
+
32
+ async def check_edge_condition(self, edge: Edge, executable_id, request_source):
33
+ """
34
+ Evaluates the condition associated with an edge, determining if execution should proceed along that edge based
35
+ on the condition's source type.
36
+
37
+ Args:
38
+ edge (Edge): The edge whose condition needs to be checked.
39
+ executable_id (str): ID of the executor handling this edge's condition.
40
+ request_source (str): Origin of the request prompting this condition check.
41
+
42
+ Returns:
43
+ bool: Result of the condition evaluation.
44
+
45
+ Raises:
46
+ ValueError: If the source_type of the condition is invalid.
47
+ """
48
+ if edge.condition.source_type == "structure":
49
+ return edge.condition(self)
50
+
51
+ elif edge.condition.source_type == "executable":
52
+ return await self._check_executable_condition(
53
+ edge, executable_id, request_source
54
+ )
55
+
56
+ else:
57
+ raise ValueError("Invalid source_type.")
58
+
59
+ def _process_edge_condition(self, edge_id):
60
+ """
61
+ Process the condition of a edge.
62
+
63
+ Args:
64
+ edge_id (str): The ID of the edge.
65
+ """
66
+ for key in list(self.pending_ins.keys()):
67
+ skipped_requests = deque()
68
+ while self.pending_ins[key]:
69
+ mail: BaseMail = self.pending_ins[key].popleft()
70
+ if (
71
+ mail.category == "condition"
72
+ and mail.package["package"]["edge_id"] == edge_id
73
+ ):
74
+ self.condition_check_result = mail.package["package"][
75
+ "check_result"
76
+ ]
77
+ else:
78
+ skipped_requests.append(mail)
79
+ self.pending_ins[key] = skipped_requests
80
+
81
+ async def _check_executable_condition(
82
+ self, edge: Edge, executable_id, request_source
83
+ ):
84
+ """
85
+ Sends the edge's condition to an external executable for evaluation and waits for the result.
86
+
87
+ Args:
88
+ edge (Edge): The edge containing the condition to be checked.
89
+ executable_id (str): ID of the executable that will evaluate the condition.
90
+ request_source (str): Source of the request for condition evaluation.
91
+
92
+ Returns:
93
+ bool: The result of the condition check.
94
+ """
95
+ self.send(
96
+ recipient_id=executable_id,
97
+ category="condition",
98
+ package={"request_source": request_source, "package": edge},
99
+ )
100
+ while self.condition_check_result is None:
101
+ await AsyncUtil.sleep(0.1)
102
+ self._process_edge_condition(edge.id_)
103
+ continue
104
+ check_result = self.condition_check_result
105
+ self.condition_check_result = None
106
+ return check_result
107
+
108
+ async def _handle_node_id(self, mail: BaseMail):
109
+ """
110
+ Processes the node identified by its ID in the mail's package, ensuring it exists and retrieving the next set of
111
+ nodes based on the current node.
112
+
113
+ Args:
114
+ mail (BaseMail): The mail containing the node ID and related execution details.
115
+
116
+ Raises:
117
+ ValueError: If the node does not exist within the structure.
118
+ """
119
+ if mail.package["package"] not in self.internal_nodes:
120
+ raise ValueError(
121
+ f"Node {mail.package} does not exist in the structure {self.id_}"
122
+ )
123
+ return await self._next_node(
124
+ self.internal_nodes[mail.package["package"]],
125
+ mail.sender_id,
126
+ mail.package["request_source"],
127
+ )
128
+
129
+ async def _handle_node(self, mail: BaseMail):
130
+ """
131
+ Processes the node specified in the mail's package, ensuring it exists within the structure.
132
+
133
+ Args:
134
+ mail (BaseMail): The mail containing the node details to be processed.
135
+
136
+ Raises:
137
+ ValueError: If the node does not exist within the structure.
138
+ """
139
+ if not self.node_exist(mail.package["package"]):
140
+ raise ValueError(
141
+ f"Node {mail.package} does not exist in the structure {self.id_}"
142
+ )
143
+ return await self._next_node(
144
+ mail.package["package"], mail.sender_id, mail.package["request_source"]
145
+ )
146
+
147
+ async def _handle_mail(self, mail: BaseMail):
148
+ """
149
+ Processes incoming mail based on its category, initiating node execution or structure operations accordingly.
150
+
151
+ Args:
152
+ mail (BaseMail): The mail to be processed, containing category and package information.
153
+
154
+ Raises:
155
+ ValueError: If the mail type is invalid for the current structure or an error occurs in handling the node ID.
156
+ """
157
+ if mail.category == "start":
158
+ return self.get_heads()
159
+
160
+ elif mail.category == "end":
161
+ self.execute_stop = True
162
+ return None
163
+
164
+ elif mail.category == "node_id":
165
+ try:
166
+ return await self._handle_node_id(mail)
167
+ except Exception as e:
168
+ raise ValueError(f"Error handling node id: {e}") from e
169
+
170
+ elif mail.category == "node" and isinstance(mail.package["package"], BaseNode):
171
+ try:
172
+ return await self._handle_node(mail)
173
+ except Exception as e:
174
+ raise ValueError(f"Error handling node: {e}") from e
175
+
176
+ else:
177
+ raise ValueError(f"Invalid mail type for structure")
178
+
179
+ async def _next_node(self, current_node: Node, executable_id, request_source):
180
+ """
181
+ Get the next step nodes based on the current node.
182
+
183
+ Args:
184
+ current_node (Node): The current node.
185
+ executable_id (str): The ID of the executable.
186
+
187
+ Returns:
188
+ list[Node]: The next step nodes.
189
+ """
190
+ next_nodes = []
191
+ next_edges = self.get_node_edges(current_node, node_as="out")
192
+ for edge in convert.to_list(list(next_edges.values())):
193
+ if edge.bundle:
194
+ continue
195
+ if edge.condition:
196
+ check = await self.check_edge_condition(
197
+ edge, executable_id, request_source
198
+ )
199
+ if not check:
200
+ continue
201
+ node = self.internal_nodes[edge.tail]
202
+ further_edges = self.get_node_edges(node, node_as="out")
203
+ bundled_nodes = deque()
204
+ for f_edge in convert.to_list(list(further_edges.values())):
205
+ if f_edge.bundle:
206
+ bundled_nodes.append(self.internal_nodes[f_edge.tail])
207
+ if bundled_nodes:
208
+ node = self.parse_bundled_to_action(node, bundled_nodes)
209
+ next_nodes.append(node)
210
+ return next_nodes
211
+
212
+ def _send_mail(self, next_nodes: list | None, mail: BaseMail):
213
+ """
214
+ Sends mails to the next nodes or signals the end of execution if no next nodes exist.
215
+
216
+ Args:
217
+ next_nodes (list | None): List of next nodes to process or None if no further nodes are available.
218
+ mail (BaseMail): The base mail used for sending follow-up actions.
219
+ """
220
+ if not next_nodes: # tail
221
+ self.send(
222
+ recipient_id=mail.sender_id,
223
+ category="end",
224
+ package={
225
+ "request_source": mail.package["request_source"],
226
+ "package": "end",
227
+ },
228
+ )
229
+ else:
230
+ if len(next_nodes) == 1:
231
+ self.send(
232
+ recipient_id=mail.sender_id,
233
+ category="node",
234
+ package={
235
+ "request_source": mail.package["request_source"],
236
+ "package": next_nodes[0],
237
+ },
238
+ )
239
+ else:
240
+ self.send(
241
+ recipient_id=mail.sender_id,
242
+ category="node_list",
243
+ package={
244
+ "request_source": mail.package["request_source"],
245
+ "package": next_nodes,
246
+ },
247
+ )
248
+
249
+ @staticmethod
250
+ def parse_bundled_to_action(instruction: Node, bundled_nodes: deque):
251
+ """
252
+ Constructs an action node from a bundle of nodes, combining various types of nodes like ActionSelection or Tool
253
+ into a single actionable unit.
254
+
255
+ This method takes a bundle of nodes and systematically integrates their functionalities into a single `ActionNode`.
256
+ This is crucial in scenarios where multiple actions or tools need to be executed sequentially or in a coordinated
257
+ manner as part of a larger instruction flow.
258
+
259
+ Args:
260
+ instruction (Node): The initial instruction node leading to this action.
261
+ bundled_nodes (deque): A deque containing nodes to be bundled into the action. These nodes typically represent
262
+ either actions to be taken or tools to be utilized.
263
+
264
+ Returns:
265
+ ActionNode: An `ActionNode` that encapsulates the combined functionality of the bundled nodes, ready for execution.
266
+
267
+ Raises:
268
+ ValueError: If an unrecognized node type is encountered within the bundled nodes. Only `ActionSelection` and
269
+ `Tool` nodes are valid for bundling into an `ActionNode`.
270
+ """
271
+ action_node = ActionNode(instruction=instruction)
272
+ while bundled_nodes:
273
+ node = bundled_nodes.popleft()
274
+ if isinstance(node, ActionSelection):
275
+ action_node.action = node.action
276
+ action_node.action_kwargs = node.action_kwargs
277
+ elif isinstance(node, Tool):
278
+ action_node.tools.append(node)
279
+ else:
280
+ raise ValueError("Invalid bundles nodes")
281
+ return action_node
282
+
283
+ async def forward(self) -> None:
284
+ """
285
+ Process the pending incoming mails and perform the corresponding actions.
286
+ """
287
+ for key in list(self.pending_ins.keys()):
288
+ while self.pending_ins[key]:
289
+ mail: BaseMail = self.pending_ins[key].popleft()
290
+ try:
291
+ if mail == "end":
292
+ self.execute_stop = True
293
+ return
294
+ next_nodes = await self._handle_mail(mail)
295
+ self._send_mail(next_nodes, mail)
296
+ except Exception as e:
297
+ raise ValueError(f"Error handling mail: {e}") from e
298
+
299
+ async def execute(self, refresh_time=1):
300
+ """
301
+ Executes the forward processing loop, checking conditions and processing nodes at defined intervals.
302
+
303
+ Args:
304
+ refresh_time (int): The delay between execution cycles, allowing for asynchronous operations to complete.
305
+
306
+ Raises:
307
+ ValueError: If the graph structure is found to be cyclic, which is unsupported.
308
+ """
309
+ if not self.acyclic:
310
+ raise ValueError("Structure is not acyclic")
311
+
312
+ while not self.execute_stop:
313
+ await self.forward()
314
+ await AsyncUtil.sleep(refresh_time)