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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)