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.
- lionagi/core/agent/base_agent.py +2 -3
- lionagi/core/branch/base.py +1 -1
- lionagi/core/branch/branch.py +2 -1
- lionagi/core/branch/flow_mixin.py +1 -1
- lionagi/core/branch/util.py +1 -1
- lionagi/core/execute/base_executor.py +1 -4
- lionagi/core/execute/branch_executor.py +66 -3
- lionagi/core/execute/instruction_map_executor.py +48 -0
- lionagi/core/execute/neo4j_executor.py +381 -0
- lionagi/core/execute/structure_executor.py +99 -3
- lionagi/core/flow/monoflow/ReAct.py +18 -18
- lionagi/core/flow/monoflow/chat_mixin.py +1 -1
- lionagi/core/flow/monoflow/followup.py +11 -12
- lionagi/core/flow/polyflow/__init__.py +1 -1
- lionagi/core/generic/component.py +0 -2
- lionagi/core/generic/condition.py +1 -1
- lionagi/core/generic/edge.py +52 -0
- lionagi/core/mail/mail_manager.py +3 -2
- lionagi/core/session/session.py +1 -1
- lionagi/experimental/__init__.py +0 -0
- lionagi/experimental/directive/__init__.py +0 -0
- lionagi/experimental/directive/evaluator/__init__.py +0 -0
- lionagi/experimental/directive/evaluator/ast_evaluator.py +115 -0
- lionagi/experimental/directive/evaluator/base_evaluator.py +202 -0
- lionagi/experimental/directive/evaluator/sandbox_.py +14 -0
- lionagi/experimental/directive/evaluator/script_engine.py +83 -0
- lionagi/experimental/directive/parser/__init__.py +0 -0
- lionagi/experimental/directive/parser/base_parser.py +215 -0
- lionagi/experimental/directive/schema.py +36 -0
- lionagi/experimental/directive/template_/__init__.py +0 -0
- lionagi/experimental/directive/template_/base_template.py +63 -0
- lionagi/experimental/tool/__init__.py +0 -0
- lionagi/experimental/tool/function_calling.py +43 -0
- lionagi/experimental/tool/manual.py +66 -0
- lionagi/experimental/tool/schema.py +59 -0
- lionagi/experimental/tool/tool_manager.py +138 -0
- lionagi/experimental/tool/util.py +16 -0
- lionagi/experimental/work/__init__.py +0 -0
- lionagi/experimental/work/_logger.py +25 -0
- lionagi/experimental/work/exchange.py +0 -0
- lionagi/experimental/work/schema.py +30 -0
- lionagi/experimental/work/tests.py +72 -0
- lionagi/experimental/work/util.py +0 -0
- lionagi/experimental/work/work_function.py +89 -0
- lionagi/experimental/work/worker.py +12 -0
- lionagi/integrations/bridge/autogen_/__init__.py +0 -0
- lionagi/integrations/bridge/autogen_/autogen_.py +124 -0
- lionagi/integrations/bridge/llamaindex_/get_index.py +294 -0
- lionagi/integrations/bridge/llamaindex_/llama_pack.py +227 -0
- lionagi/integrations/bridge/transformers_/__init__.py +0 -0
- lionagi/integrations/bridge/transformers_/install_.py +36 -0
- lionagi/integrations/config/oai_configs.py +1 -1
- lionagi/integrations/config/ollama_configs.py +1 -1
- lionagi/integrations/config/openrouter_configs.py +1 -1
- lionagi/integrations/storage/__init__.py +3 -0
- lionagi/integrations/storage/neo4j.py +673 -0
- lionagi/integrations/storage/storage_util.py +289 -0
- lionagi/integrations/storage/to_csv.py +63 -0
- lionagi/integrations/storage/to_excel.py +67 -0
- lionagi/libs/ln_knowledge_graph.py +405 -0
- lionagi/libs/ln_queue.py +101 -0
- lionagi/libs/ln_tokenizer.py +57 -0
- lionagi/libs/sys_util.py +1 -1
- lionagi/lions/__init__.py +0 -0
- lionagi/lions/coder/__init__.py +0 -0
- lionagi/lions/coder/add_feature.py +20 -0
- lionagi/lions/coder/base_prompts.py +22 -0
- lionagi/lions/coder/coder.py +121 -0
- lionagi/lions/coder/util.py +91 -0
- lionagi/lions/researcher/__init__.py +0 -0
- lionagi/lions/researcher/data_source/__init__.py +0 -0
- lionagi/lions/researcher/data_source/finhub_.py +191 -0
- lionagi/lions/researcher/data_source/google_.py +199 -0
- lionagi/lions/researcher/data_source/wiki_.py +96 -0
- lionagi/lions/researcher/data_source/yfinance_.py +21 -0
- lionagi/tests/libs/test_queue.py +67 -0
- lionagi/tests/test_core/test_branch.py +0 -1
- lionagi/version.py +1 -1
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/METADATA +1 -1
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/RECORD +83 -29
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/LICENSE +0 -0
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/WHEEL +0 -0
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/top_level.txt +0 -0
lionagi/core/agent/base_agent.py
CHANGED
@@ -19,7 +19,7 @@ class BaseAgent(Node):
|
|
19
19
|
|
20
20
|
def __init__(
|
21
21
|
self,
|
22
|
-
structure:
|
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:
|
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
|
-
|
lionagi/core/branch/base.py
CHANGED
lionagi/core/branch/branch.py
CHANGED
lionagi/core/branch/util.py
CHANGED
@@ -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.
|
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
|
-
|
208
|
-
|
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)
|