lionagi 0.1.0__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 (83) hide show
  1. lionagi/core/agent/base_agent.py +2 -3
  2. lionagi/core/branch/base.py +1 -1
  3. lionagi/core/branch/branch.py +2 -1
  4. lionagi/core/branch/flow_mixin.py +1 -1
  5. lionagi/core/branch/util.py +1 -1
  6. lionagi/core/execute/base_executor.py +1 -4
  7. lionagi/core/execute/branch_executor.py +66 -3
  8. lionagi/core/execute/instruction_map_executor.py +48 -0
  9. lionagi/core/execute/neo4j_executor.py +381 -0
  10. lionagi/core/execute/structure_executor.py +99 -3
  11. lionagi/core/flow/monoflow/ReAct.py +18 -18
  12. lionagi/core/flow/monoflow/chat_mixin.py +1 -1
  13. lionagi/core/flow/monoflow/followup.py +11 -12
  14. lionagi/core/flow/polyflow/__init__.py +1 -1
  15. lionagi/core/generic/component.py +0 -2
  16. lionagi/core/generic/condition.py +1 -1
  17. lionagi/core/generic/edge.py +52 -0
  18. lionagi/core/mail/mail_manager.py +3 -2
  19. lionagi/core/session/session.py +1 -1
  20. lionagi/experimental/__init__.py +0 -0
  21. lionagi/experimental/directive/__init__.py +0 -0
  22. lionagi/experimental/directive/evaluator/__init__.py +0 -0
  23. lionagi/experimental/directive/evaluator/ast_evaluator.py +115 -0
  24. lionagi/experimental/directive/evaluator/base_evaluator.py +202 -0
  25. lionagi/experimental/directive/evaluator/sandbox_.py +14 -0
  26. lionagi/experimental/directive/evaluator/script_engine.py +83 -0
  27. lionagi/experimental/directive/parser/__init__.py +0 -0
  28. lionagi/experimental/directive/parser/base_parser.py +215 -0
  29. lionagi/experimental/directive/schema.py +36 -0
  30. lionagi/experimental/directive/template_/__init__.py +0 -0
  31. lionagi/experimental/directive/template_/base_template.py +63 -0
  32. lionagi/experimental/tool/__init__.py +0 -0
  33. lionagi/experimental/tool/function_calling.py +43 -0
  34. lionagi/experimental/tool/manual.py +66 -0
  35. lionagi/experimental/tool/schema.py +59 -0
  36. lionagi/experimental/tool/tool_manager.py +138 -0
  37. lionagi/experimental/tool/util.py +16 -0
  38. lionagi/experimental/work/__init__.py +0 -0
  39. lionagi/experimental/work/_logger.py +25 -0
  40. lionagi/experimental/work/exchange.py +0 -0
  41. lionagi/experimental/work/schema.py +30 -0
  42. lionagi/experimental/work/tests.py +72 -0
  43. lionagi/experimental/work/util.py +0 -0
  44. lionagi/experimental/work/work_function.py +89 -0
  45. lionagi/experimental/work/worker.py +12 -0
  46. lionagi/integrations/bridge/autogen_/__init__.py +0 -0
  47. lionagi/integrations/bridge/autogen_/autogen_.py +124 -0
  48. lionagi/integrations/bridge/llamaindex_/get_index.py +294 -0
  49. lionagi/integrations/bridge/llamaindex_/llama_pack.py +227 -0
  50. lionagi/integrations/bridge/transformers_/__init__.py +0 -0
  51. lionagi/integrations/bridge/transformers_/install_.py +36 -0
  52. lionagi/integrations/config/oai_configs.py +1 -1
  53. lionagi/integrations/config/ollama_configs.py +1 -1
  54. lionagi/integrations/config/openrouter_configs.py +1 -1
  55. lionagi/integrations/storage/__init__.py +3 -0
  56. lionagi/integrations/storage/neo4j.py +673 -0
  57. lionagi/integrations/storage/storage_util.py +289 -0
  58. lionagi/integrations/storage/to_csv.py +63 -0
  59. lionagi/integrations/storage/to_excel.py +67 -0
  60. lionagi/libs/ln_knowledge_graph.py +405 -0
  61. lionagi/libs/ln_queue.py +101 -0
  62. lionagi/libs/ln_tokenizer.py +57 -0
  63. lionagi/libs/sys_util.py +1 -1
  64. lionagi/lions/__init__.py +0 -0
  65. lionagi/lions/coder/__init__.py +0 -0
  66. lionagi/lions/coder/add_feature.py +20 -0
  67. lionagi/lions/coder/base_prompts.py +22 -0
  68. lionagi/lions/coder/coder.py +121 -0
  69. lionagi/lions/coder/util.py +91 -0
  70. lionagi/lions/researcher/__init__.py +0 -0
  71. lionagi/lions/researcher/data_source/__init__.py +0 -0
  72. lionagi/lions/researcher/data_source/finhub_.py +191 -0
  73. lionagi/lions/researcher/data_source/google_.py +199 -0
  74. lionagi/lions/researcher/data_source/wiki_.py +96 -0
  75. lionagi/lions/researcher/data_source/yfinance_.py +21 -0
  76. lionagi/tests/libs/test_queue.py +67 -0
  77. lionagi/tests/test_core/test_branch.py +0 -1
  78. lionagi/version.py +1 -1
  79. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/METADATA +1 -1
  80. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/RECORD +83 -29
  81. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/LICENSE +0 -0
  82. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/WHEEL +0 -0
  83. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/top_level.txt +0 -0
@@ -19,7 +19,7 @@ class BaseAgent(Node):
19
19
 
20
20
  def __init__(
21
21
  self,
22
- structure: StructureExecutor,
22
+ structure: BaseExecutor,
23
23
  executable: BaseExecutor,
24
24
  output_parser=None,
25
25
  **kwargs,
@@ -33,7 +33,7 @@ class BaseAgent(Node):
33
33
  output_parser: A function for parsing the agent's output (optional).
34
34
  """
35
35
  super().__init__()
36
- self.structure: StructureExecutor = structure
36
+ self.structure: BaseExecutor = structure
37
37
  self.executable: BaseExecutor = executable
38
38
  for v, k in kwargs.items():
39
39
  executable.__setattr__(v, k)
@@ -87,4 +87,3 @@ class BaseAgent(Node):
87
87
 
88
88
  if self.output_parser:
89
89
  return self.output_parser(self)
90
-
@@ -650,4 +650,4 @@ class BaseBranch(BaseNode, ABC):
650
650
  messages = self.messages["sender"] if use_sender else self.messages["role"]
651
651
  result = messages.value_counts().to_dict()
652
652
  result["total"] = len(self.messages)
653
- return result
653
+ return result
@@ -470,4 +470,5 @@ class Branch(BaseBranch, BranchFlowMixin):
470
470
  }:
471
471
  return True
472
472
  except Exception:
473
- return False
473
+ return False
474
+ return False
@@ -93,4 +93,4 @@ class BranchFlowMixin(ABC):
93
93
  output_prompt=output_prompt,
94
94
  out=out,
95
95
  **kwargs,
96
- )
96
+ )
@@ -320,4 +320,4 @@ class MessageUtil:
320
320
  else:
321
321
  with contextlib.suppress(Exception):
322
322
  answers.append(nested.nget(content, ["system_info"]))
323
- return "\n".join(answers)
323
+ return "\n".join(answers)
@@ -18,7 +18,7 @@ class BaseExecutor(BaseComponent, ABC):
18
18
  execute_stop: bool = Field(
19
19
  False, description="A flag indicating whether to stop execution."
20
20
  )
21
- context: dict | str | None = Field(
21
+ context: dict | str | list | None = Field(
22
22
  None, description="The context buffer for the next instruction."
23
23
  )
24
24
  execution_responses: list = Field(
@@ -28,9 +28,6 @@ class BaseExecutor(BaseComponent, ABC):
28
28
  verbose: bool = Field(
29
29
  True, description="A flag indicating whether to provide verbose output."
30
30
  )
31
- execute_stop: bool = Field(
32
- False, description="A flag indicating whether to stop execution."
33
- )
34
31
 
35
32
  def send(self, recipient_id: str, category: str, package: Any) -> None:
36
33
  """
@@ -11,6 +11,10 @@ from lionagi.core.execute.base_executor import BaseExecutor
11
11
  class BranchExecutor(Branch, BaseExecutor):
12
12
 
13
13
  async def forward(self) -> None:
14
+ """
15
+ Forwards the execution by processing all pending incoming mails in each branch. Depending on the category of the mail,
16
+ it processes starts, nodes, node lists, conditions, or ends, accordingly executing different functions.
17
+ """
14
18
  for key in list(self.pending_ins.keys()):
15
19
  while self.pending_ins[key]:
16
20
  mail = self.pending_ins[key].popleft()
@@ -26,11 +30,27 @@ class BranchExecutor(Branch, BaseExecutor):
26
30
  self._process_end(mail)
27
31
 
28
32
  async def execute(self, refresh_time=1) -> None:
33
+ """
34
+ Executes the forward process repeatedly at specified time intervals until execution is instructed to stop.
35
+
36
+ Args:
37
+ refresh_time (int): The interval, in seconds, at which the forward method is called repeatedly.
38
+ """
29
39
  while not self.execute_stop:
30
40
  await self.forward()
31
41
  await AsyncUtil.sleep(refresh_time)
32
42
 
33
43
  async def _process_node(self, mail: BaseMail):
44
+ """
45
+ Processes a single node based on the node type specified in the mail's package. It handles different types of nodes such as System,
46
+ Instruction, ActionNode, and generic nodes through separate processes.
47
+
48
+ Args:
49
+ mail (BaseMail): The mail containing the node to be processed along with associated details.
50
+
51
+ Raises:
52
+ ValueError: If an invalid mail is encountered or the process encounters errors.
53
+ """
34
54
  if isinstance(mail.package["package"], System):
35
55
  self._system_process(mail.package["package"], verbose=self.verbose)
36
56
  self.send(
@@ -74,11 +94,26 @@ class BranchExecutor(Branch, BaseExecutor):
74
94
  raise ValueError(f"Invalid mail to process. Mail:{mail}")
75
95
 
76
96
  def _process_node_list(self, mail: BaseMail):
97
+ """
98
+ Processes a list of nodes provided in the mail, but currently only sends an end signal as multiple path selection is not supported.
99
+
100
+ Args:
101
+ mail (BaseMail): The mail containing a list of nodes to be processed.
102
+
103
+ Raises:
104
+ ValueError: When trying to process multiple paths which is currently unsupported.
105
+ """
77
106
  self.send(mail.sender_id, "end", {"request_source": self.id_, "package": "end"})
78
107
  self.execute_stop = True
79
108
  raise ValueError("Multiple path selection is currently not supported")
80
109
 
81
110
  def _process_condition(self, mail: BaseMail):
111
+ """
112
+ Processes a condition associated with an edge based on the mail's package, setting up the result of the condition check.
113
+
114
+ Args:
115
+ mail (BaseMail): The mail containing the condition to be processed.
116
+ """
82
117
  relationship: Edge = mail.package["package"]
83
118
  check_result = relationship.condition(self)
84
119
  back_mail = {
@@ -93,6 +128,14 @@ class BranchExecutor(Branch, BaseExecutor):
93
128
  )
94
129
 
95
130
  def _system_process(self, system: System, verbose=True, context_verbose=False):
131
+ """
132
+ Processes a system node, possibly displaying its content and context if verbose is enabled.
133
+
134
+ Args:
135
+ system (System): The system node to process.
136
+ verbose (bool): Flag to enable verbose output.
137
+ context_verbose (bool): Flag to enable verbose output specifically for context.
138
+ """
96
139
  from lionagi.libs import SysUtil
97
140
 
98
141
  SysUtil.check_import("IPython")
@@ -111,6 +154,14 @@ class BranchExecutor(Branch, BaseExecutor):
111
154
  async def _instruction_process(
112
155
  self, instruction: Instruction, verbose=True, **kwargs
113
156
  ):
157
+ """
158
+ Processes an instruction node, possibly displaying its content if verbose is enabled, and handling any additional keyword arguments.
159
+
160
+ Args:
161
+ instruction (Instruction): The instruction node to process.
162
+ verbose (bool): Flag to enable verbose output.
163
+ **kwargs: Additional keyword arguments that might affect how instructions are processed.
164
+ """
114
165
  from lionagi.libs import SysUtil
115
166
 
116
167
  SysUtil.check_import("IPython")
@@ -146,6 +197,13 @@ class BranchExecutor(Branch, BaseExecutor):
146
197
  self.execution_responses.append(result)
147
198
 
148
199
  async def _action_process(self, action: ActionNode, verbose=True):
200
+ """
201
+ Processes an action node, executing the defined action along with any tools specified within the node.
202
+
203
+ Args:
204
+ action (ActionNode): The action node to process.
205
+ verbose (bool): Flag to enable verbose output of the action results.
206
+ """
149
207
  from lionagi.libs import SysUtil
150
208
 
151
209
  SysUtil.check_import("IPython")
@@ -196,7 +254,7 @@ class BranchExecutor(Branch, BaseExecutor):
196
254
  agent: The agent to process.
197
255
  verbose (bool): A flag indicating whether to provide verbose output (default: True).
198
256
  """
199
- context = self.responses
257
+ context = list(self.messages["content"])
200
258
  if verbose:
201
259
  print("*****************************************************")
202
260
  result = await agent.execute(context)
@@ -204,8 +262,13 @@ class BranchExecutor(Branch, BaseExecutor):
204
262
  if verbose:
205
263
  print("*****************************************************")
206
264
 
207
- self.context = result
208
- self.responses.append(result)
265
+ from pandas import DataFrame
266
+
267
+ if isinstance(result, DataFrame):
268
+ self.context = list(result["content"])
269
+ else:
270
+ self.context = result
271
+ self.execution_responses.append(result)
209
272
 
210
273
  def _process_start(self, mail):
211
274
  """
@@ -10,6 +10,18 @@ from lionagi.core.execute.branch_executor import BranchExecutor
10
10
 
11
11
 
12
12
  class InstructionMapExecutor(BaseExecutor):
13
+ """
14
+ Manages the execution of a mapped set of instructions across multiple branches within an executable structure.
15
+
16
+ Attributes:
17
+ branches (dict[str, BranchExecutor]): A dictionary of branch executors managing individual instruction flows.
18
+ structure_id (str): The identifier for the structure within which these branches operate.
19
+ mail_transfer (MailTransfer): Handles the transfer of mail between branches and other components.
20
+ branch_kwargs (dict): Keyword arguments used for initializing branches.
21
+ num_end_branches (int): Tracks the number of branches that have completed execution.
22
+ mail_manager (MailManager): Manages the distribution and collection of mails across branches.
23
+ """
24
+
13
25
  branches: dict[str, BranchExecutor] = Field(
14
26
  default_factory=dict, description="The branches of the instruction mapping."
15
27
  )
@@ -27,10 +39,20 @@ class InstructionMapExecutor(BaseExecutor):
27
39
  )
28
40
 
29
41
  def __init__(self, **kwargs):
42
+ """
43
+ Initializes an InstructionMapExecutor with the given parameters.
44
+
45
+ Args:
46
+ **kwargs: Arbitrary keyword arguments passed to the base executor and used for initializing branch executors.
47
+ """
30
48
  super().__init__(**kwargs)
31
49
  self.mail_manager = MailManager([self.mail_transfer])
32
50
 
33
51
  def transfer_ins(self):
52
+ """
53
+ Processes incoming mails, directing them appropriately based on their categories, and handles the initial setup
54
+ of branches or the routing of node and condition mails.
55
+ """
34
56
  for key in list(self.pending_ins.keys()):
35
57
  while self.pending_ins[key]:
36
58
  mail: BaseMail = self.pending_ins[key].popleft()
@@ -48,6 +70,10 @@ class InstructionMapExecutor(BaseExecutor):
48
70
  self.mail_transfer.pending_outs.append(mail)
49
71
 
50
72
  def transfer_outs(self):
73
+ """
74
+ Processes outgoing mails from the central mail transfer, handling end-of-execution notifications and routing
75
+ other mails to appropriate recipients.
76
+ """
51
77
  for key in list(self.mail_transfer.pending_ins.keys()):
52
78
  while self.mail_transfer.pending_ins[key]:
53
79
  mail: BaseMail = self.mail_transfer.pending_ins[key].popleft()
@@ -66,6 +92,12 @@ class InstructionMapExecutor(BaseExecutor):
66
92
  self.pending_outs.append(mail)
67
93
 
68
94
  def _process_start(self, start_mail: BaseMail):
95
+ """
96
+ Processes a start mail to initialize a new branch executor and configures it based on the mail's package content.
97
+
98
+ Args:
99
+ start_mail (BaseMail): The mail initiating the start of a new branch execution.
100
+ """
69
101
  branch = BranchExecutor(verbose=self.verbose, **self.branch_kwargs)
70
102
  branch.context = start_mail.package["context"]
71
103
  self.branches[branch.id_] = branch
@@ -80,6 +112,13 @@ class InstructionMapExecutor(BaseExecutor):
80
112
  self.pending_outs.append(mail)
81
113
 
82
114
  def _process_node_list(self, nl_mail: BaseMail):
115
+ """
116
+ Processes a node list mail, setting up new branches or propagating the execution context based on the node list
117
+ provided in the mail.
118
+
119
+ Args:
120
+ nl_mail (BaseMail): The mail containing a list of nodes to be processed in subsequent branches.
121
+ """
83
122
  source_branch_id = nl_mail.package["request_source"]
84
123
  node_list = nl_mail.package["package"]
85
124
  shared_context = self.branches[source_branch_id].context
@@ -115,6 +154,9 @@ class InstructionMapExecutor(BaseExecutor):
115
154
  self.mail_transfer.pending_outs.append(node_mail)
116
155
 
117
156
  async def forward(self):
157
+ """
158
+ Forwards the execution by processing all incoming and outgoing mails and advancing the state of all active branches.
159
+ """
118
160
  self.transfer_ins()
119
161
  self.transfer_outs()
120
162
  self.mail_manager.collect_all()
@@ -126,6 +168,12 @@ class InstructionMapExecutor(BaseExecutor):
126
168
  return
127
169
 
128
170
  async def execute(self, refresh_time=1):
171
+ """
172
+ Continuously executes the forward process at specified intervals until instructed to stop.
173
+
174
+ Args:
175
+ refresh_time (int): The time in seconds between execution cycles.
176
+ """
129
177
  while not self.execute_stop:
130
178
  await self.forward()
131
179
  await asyncio.sleep(refresh_time)
@@ -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)