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
@@ -19,10 +19,32 @@ from lionagi.core.graph.graph import Graph
|
|
19
19
|
|
20
20
|
|
21
21
|
class StructureExecutor(BaseExecutor, Graph):
|
22
|
+
"""
|
23
|
+
Executes tasks within a graph structure, handling dynamic node flows and conditional edge logic.
|
24
|
+
|
25
|
+
Attributes:
|
26
|
+
condition_check_result (bool | None): Result of the last condition check performed during execution,
|
27
|
+
used to control flow based on dynamic conditions.
|
28
|
+
"""
|
22
29
|
|
23
30
|
condition_check_result: bool | None = None
|
24
31
|
|
25
32
|
async def check_edge_condition(self, edge: Edge, executable_id, request_source):
|
33
|
+
"""
|
34
|
+
Evaluates the condition associated with an edge, determining if execution should proceed along that edge based
|
35
|
+
on the condition's source type.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
edge (Edge): The edge whose condition needs to be checked.
|
39
|
+
executable_id (str): ID of the executor handling this edge's condition.
|
40
|
+
request_source (str): Origin of the request prompting this condition check.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
bool: Result of the condition evaluation.
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
ValueError: If the source_type of the condition is invalid.
|
47
|
+
"""
|
26
48
|
if edge.condition.source_type == "structure":
|
27
49
|
return edge.condition(self)
|
28
50
|
|
@@ -59,6 +81,17 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
59
81
|
async def _check_executable_condition(
|
60
82
|
self, edge: Edge, executable_id, request_source
|
61
83
|
):
|
84
|
+
"""
|
85
|
+
Sends the edge's condition to an external executable for evaluation and waits for the result.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
edge (Edge): The edge containing the condition to be checked.
|
89
|
+
executable_id (str): ID of the executable that will evaluate the condition.
|
90
|
+
request_source (str): Source of the request for condition evaluation.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
bool: The result of the condition check.
|
94
|
+
"""
|
62
95
|
self.send(
|
63
96
|
recipient_id=executable_id,
|
64
97
|
category="condition",
|
@@ -73,6 +106,16 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
73
106
|
return check_result
|
74
107
|
|
75
108
|
async def _handle_node_id(self, mail: BaseMail):
|
109
|
+
"""
|
110
|
+
Processes the node identified by its ID in the mail's package, ensuring it exists and retrieving the next set of
|
111
|
+
nodes based on the current node.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
mail (BaseMail): The mail containing the node ID and related execution details.
|
115
|
+
|
116
|
+
Raises:
|
117
|
+
ValueError: If the node does not exist within the structure.
|
118
|
+
"""
|
76
119
|
if mail.package["package"] not in self.internal_nodes:
|
77
120
|
raise ValueError(
|
78
121
|
f"Node {mail.package} does not exist in the structure {self.id_}"
|
@@ -84,6 +127,15 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
84
127
|
)
|
85
128
|
|
86
129
|
async def _handle_node(self, mail: BaseMail):
|
130
|
+
"""
|
131
|
+
Processes the node specified in the mail's package, ensuring it exists within the structure.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
mail (BaseMail): The mail containing the node details to be processed.
|
135
|
+
|
136
|
+
Raises:
|
137
|
+
ValueError: If the node does not exist within the structure.
|
138
|
+
"""
|
87
139
|
if not self.node_exist(mail.package["package"]):
|
88
140
|
raise ValueError(
|
89
141
|
f"Node {mail.package} does not exist in the structure {self.id_}"
|
@@ -93,7 +145,15 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
93
145
|
)
|
94
146
|
|
95
147
|
async def _handle_mail(self, mail: BaseMail):
|
148
|
+
"""
|
149
|
+
Processes incoming mail based on its category, initiating node execution or structure operations accordingly.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
mail (BaseMail): The mail to be processed, containing category and package information.
|
96
153
|
|
154
|
+
Raises:
|
155
|
+
ValueError: If the mail type is invalid for the current structure or an error occurs in handling the node ID.
|
156
|
+
"""
|
97
157
|
if mail.category == "start":
|
98
158
|
return self.get_heads()
|
99
159
|
|
@@ -116,7 +176,7 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
116
176
|
else:
|
117
177
|
raise ValueError(f"Invalid mail type for structure")
|
118
178
|
|
119
|
-
async def _next_node(self, current_node:
|
179
|
+
async def _next_node(self, current_node: Node, executable_id, request_source):
|
120
180
|
"""
|
121
181
|
Get the next step nodes based on the current node.
|
122
182
|
|
@@ -128,7 +188,7 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
128
188
|
list[Node]: The next step nodes.
|
129
189
|
"""
|
130
190
|
next_nodes = []
|
131
|
-
next_edges
|
191
|
+
next_edges = self.get_node_edges(current_node, node_as="out")
|
132
192
|
for edge in convert.to_list(list(next_edges.values())):
|
133
193
|
if edge.bundle:
|
134
194
|
continue
|
@@ -139,7 +199,7 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
139
199
|
if not check:
|
140
200
|
continue
|
141
201
|
node = self.internal_nodes[edge.tail]
|
142
|
-
further_edges
|
202
|
+
further_edges = self.get_node_edges(node, node_as="out")
|
143
203
|
bundled_nodes = deque()
|
144
204
|
for f_edge in convert.to_list(list(further_edges.values())):
|
145
205
|
if f_edge.bundle:
|
@@ -150,6 +210,13 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
150
210
|
return next_nodes
|
151
211
|
|
152
212
|
def _send_mail(self, next_nodes: list | None, mail: BaseMail):
|
213
|
+
"""
|
214
|
+
Sends mails to the next nodes or signals the end of execution if no next nodes exist.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
next_nodes (list | None): List of next nodes to process or None if no further nodes are available.
|
218
|
+
mail (BaseMail): The base mail used for sending follow-up actions.
|
219
|
+
"""
|
153
220
|
if not next_nodes: # tail
|
154
221
|
self.send(
|
155
222
|
recipient_id=mail.sender_id,
|
@@ -181,6 +248,26 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
181
248
|
|
182
249
|
@staticmethod
|
183
250
|
def parse_bundled_to_action(instruction: Node, bundled_nodes: deque):
|
251
|
+
"""
|
252
|
+
Constructs an action node from a bundle of nodes, combining various types of nodes like ActionSelection or Tool
|
253
|
+
into a single actionable unit.
|
254
|
+
|
255
|
+
This method takes a bundle of nodes and systematically integrates their functionalities into a single `ActionNode`.
|
256
|
+
This is crucial in scenarios where multiple actions or tools need to be executed sequentially or in a coordinated
|
257
|
+
manner as part of a larger instruction flow.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
instruction (Node): The initial instruction node leading to this action.
|
261
|
+
bundled_nodes (deque): A deque containing nodes to be bundled into the action. These nodes typically represent
|
262
|
+
either actions to be taken or tools to be utilized.
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
ActionNode: An `ActionNode` that encapsulates the combined functionality of the bundled nodes, ready for execution.
|
266
|
+
|
267
|
+
Raises:
|
268
|
+
ValueError: If an unrecognized node type is encountered within the bundled nodes. Only `ActionSelection` and
|
269
|
+
`Tool` nodes are valid for bundling into an `ActionNode`.
|
270
|
+
"""
|
184
271
|
action_node = ActionNode(instruction=instruction)
|
185
272
|
while bundled_nodes:
|
186
273
|
node = bundled_nodes.popleft()
|
@@ -210,6 +297,15 @@ class StructureExecutor(BaseExecutor, Graph):
|
|
210
297
|
raise ValueError(f"Error handling mail: {e}") from e
|
211
298
|
|
212
299
|
async def execute(self, refresh_time=1):
|
300
|
+
"""
|
301
|
+
Executes the forward processing loop, checking conditions and processing nodes at defined intervals.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
refresh_time (int): The delay between execution cycles, allowing for asynchronous operations to complete.
|
305
|
+
|
306
|
+
Raises:
|
307
|
+
ValueError: If the graph structure is found to be cyclic, which is unsupported.
|
308
|
+
"""
|
213
309
|
if not self.acyclic:
|
214
310
|
raise ValueError("Structure is not acyclic")
|
215
311
|
|
@@ -46,8 +46,7 @@ class MonoReAct(MonoChat):
|
|
46
46
|
"""
|
47
47
|
|
48
48
|
ACTION_PROMPT = """
|
49
|
-
You have {num_steps} steps left in the current task.
|
50
|
-
If you are done, present the final result to the user without further tool usage.
|
49
|
+
You have {num_steps} steps left in the current task. invoke tool usage.
|
51
50
|
"""
|
52
51
|
|
53
52
|
OUTPUT_PROMPT = "Notice: Present the final output to the user. Original user instruction: {instruction}"
|
@@ -127,15 +126,15 @@ class MonoReAct(MonoChat):
|
|
127
126
|
|
128
127
|
def _create_followup_config(self, tools, tool_choice="auto", **kwargs):
|
129
128
|
"""
|
130
|
-
Creates the configuration for the followup
|
129
|
+
Creates the configuration for the followup chat based on the provided tools and parameters.
|
131
130
|
|
132
131
|
Args:
|
133
|
-
tools (Optional[Any]): Specifies tools to be invoked during the followup
|
132
|
+
tools (Optional[Any]): Specifies tools to be invoked during the followup chat.
|
134
133
|
tool_choice (str): The choice of tools to use (default: "auto").
|
135
|
-
**kwargs: Additional keyword arguments for the followup configuration.
|
134
|
+
**kwargs: Additional keyword arguments for the followup chat configuration.
|
136
135
|
|
137
136
|
Returns:
|
138
|
-
dict: The configuration for the followup
|
137
|
+
dict: The configuration for the followup chat.
|
139
138
|
|
140
139
|
Raises:
|
141
140
|
ValueError: If no tools are found and registered.
|
@@ -159,7 +158,7 @@ class MonoReAct(MonoChat):
|
|
159
158
|
system=None,
|
160
159
|
tools=None,
|
161
160
|
num_rounds: int = 1,
|
162
|
-
auto=
|
161
|
+
auto=True,
|
163
162
|
reason_prompt=None,
|
164
163
|
action_prompt=None,
|
165
164
|
output_prompt=None,
|
@@ -188,6 +187,7 @@ class MonoReAct(MonoChat):
|
|
188
187
|
The result of the reasoning and action steps.
|
189
188
|
"""
|
190
189
|
config = self._create_followup_config(tools, **kwargs)
|
190
|
+
kwargs.pop("tools", None)
|
191
191
|
|
192
192
|
i = 0
|
193
193
|
_out = ""
|
@@ -220,19 +220,19 @@ class MonoReAct(MonoChat):
|
|
220
220
|
|
221
221
|
_out = await self.chat(_prompt, sender=sender, **config)
|
222
222
|
|
223
|
-
if
|
223
|
+
if not self.branch._is_invoked():
|
224
224
|
return _out if out else None
|
225
225
|
|
226
226
|
i += 1
|
227
227
|
|
228
|
-
if auto:
|
229
|
-
if not self.branch._is_invoked():
|
230
|
-
return _out if out else None
|
231
228
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
229
|
+
if not self.branch._is_invoked():
|
230
|
+
return _out if out else None
|
231
|
+
|
232
|
+
_prompt = self._get_prompt(
|
233
|
+
prompt=output_prompt,
|
234
|
+
default=self.OUTPUT_PROMPT,
|
235
|
+
instruction=instruction,
|
236
|
+
)
|
237
|
+
_out = await self.chat(_prompt, sender=sender, **kwargs)
|
238
|
+
return _out if out else None
|
@@ -150,7 +150,7 @@ class MonoFollowup(MonoChat):
|
|
150
150
|
system=None,
|
151
151
|
tools=None,
|
152
152
|
max_followup: int = 1,
|
153
|
-
auto=
|
153
|
+
auto=True,
|
154
154
|
followup_prompt=None,
|
155
155
|
output_prompt=None,
|
156
156
|
out=True,
|
@@ -196,19 +196,18 @@ class MonoFollowup(MonoChat):
|
|
196
196
|
else:
|
197
197
|
_out = await self.chat(_prompt, sender=sender, **config)
|
198
198
|
|
199
|
-
if
|
199
|
+
if not self.branch._is_invoked():
|
200
200
|
return _out if out else None
|
201
201
|
|
202
202
|
i += 1
|
203
203
|
|
204
|
-
if
|
205
|
-
if
|
206
|
-
return _out if out else None
|
204
|
+
if not self.branch._is_invoked():
|
205
|
+
return _out if out else None
|
207
206
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
207
|
+
_prompt = self._get_prompt(
|
208
|
+
prompt=output_prompt,
|
209
|
+
default=self.OUTPUT_PROMPT,
|
210
|
+
instruction=instruction,
|
211
|
+
)
|
212
|
+
_out = await self.chat(_prompt, sender=sender, **kwargs)
|
213
|
+
return _out if out else None
|
@@ -1 +1 @@
|
|
1
|
-
from .chat import PolyChat
|
1
|
+
from .chat import PolyChat
|
@@ -150,7 +150,6 @@ class BaseComponent(BaseModel, ABC):
|
|
150
150
|
dict_.update(self._get_field_annotation(i))
|
151
151
|
return dict_
|
152
152
|
|
153
|
-
|
154
153
|
@_get_field_annotation.register(tuple)
|
155
154
|
def _(self, field_name) -> dict[str, Any]:
|
156
155
|
"""
|
@@ -168,7 +167,6 @@ class BaseComponent(BaseModel, ABC):
|
|
168
167
|
dict_.update(self._get_field_annotation(i))
|
169
168
|
return dict_
|
170
169
|
|
171
|
-
|
172
170
|
def _field_has_attr(self, k: str, attr: str) -> bool:
|
173
171
|
"""
|
174
172
|
Check if a field has a specific attribute.
|
lionagi/core/generic/edge.py
CHANGED
@@ -85,6 +85,58 @@ class Edge(BaseComponent):
|
|
85
85
|
raise ValueError("The condition for the edge is not set.")
|
86
86
|
return self.condition(obj)
|
87
87
|
|
88
|
+
def string_condition(self):
|
89
|
+
"""
|
90
|
+
Retrieves the source code of the condition class associated with this edge as a string.
|
91
|
+
|
92
|
+
This method is useful for serialization and debugging, allowing the condition logic to be inspected or stored
|
93
|
+
in a human-readable format. It employs advanced introspection techniques to locate and extract the exact class
|
94
|
+
definition, handling edge cases like dynamically defined classes or classes defined in interactive environments.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
str: The source code of the condition's class, if available. If the condition is None or the source code
|
98
|
+
cannot be located, this method returns None.
|
99
|
+
|
100
|
+
Raises:
|
101
|
+
TypeError: If the source code of the condition's class cannot be found due to the class being defined in a
|
102
|
+
non-standard manner or in the interactive interpreter (__main__ context).
|
103
|
+
"""
|
104
|
+
if self.condition is None:
|
105
|
+
return
|
106
|
+
|
107
|
+
import inspect, sys
|
108
|
+
|
109
|
+
def new_getfile(object, _old_getfile=inspect.getfile):
|
110
|
+
if not inspect.isclass(object):
|
111
|
+
return _old_getfile(object)
|
112
|
+
|
113
|
+
# Lookup by parent module (as in current inspect)
|
114
|
+
if hasattr(object, "__module__"):
|
115
|
+
object_ = sys.modules.get(object.__module__)
|
116
|
+
if hasattr(object_, "__file__"):
|
117
|
+
return object_.__file__
|
118
|
+
|
119
|
+
# If parent module is __main__, lookup by methods (NEW)
|
120
|
+
for name, member in inspect.getmembers(object):
|
121
|
+
if (
|
122
|
+
inspect.isfunction(member)
|
123
|
+
and object.__qualname__ + "." + member.__name__
|
124
|
+
== member.__qualname__
|
125
|
+
):
|
126
|
+
return inspect.getfile(member)
|
127
|
+
else:
|
128
|
+
raise TypeError("Source for {!r} not found".format(object))
|
129
|
+
|
130
|
+
inspect.getfile = new_getfile
|
131
|
+
|
132
|
+
import inspect
|
133
|
+
from IPython.core.magics.code import extract_symbols
|
134
|
+
|
135
|
+
obj = self.condition.__class__
|
136
|
+
cell_code = "".join(inspect.linecache.getlines(new_getfile(obj)))
|
137
|
+
class_code = extract_symbols(cell_code, obj.__name__)[0][0]
|
138
|
+
return class_code
|
139
|
+
|
88
140
|
def __str__(self) -> str:
|
89
141
|
"""
|
90
142
|
Returns a simple string representation of the Relationship.
|
@@ -17,10 +17,11 @@ class MailManager:
|
|
17
17
|
and sender.
|
18
18
|
"""
|
19
19
|
|
20
|
-
def __init__(self, sources):
|
20
|
+
def __init__(self, sources=None):
|
21
21
|
self.sources = {}
|
22
22
|
self.mails = {}
|
23
|
-
|
23
|
+
if sources:
|
24
|
+
self.add_sources(sources)
|
24
25
|
self.execute_stop = False
|
25
26
|
|
26
27
|
def add_sources(self, sources):
|
lionagi/core/session/session.py
CHANGED
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import ast
|
2
|
+
import operator
|
3
|
+
|
4
|
+
|
5
|
+
class ASTEvaluator:
|
6
|
+
"""
|
7
|
+
Safely evaluates expressions using AST parsing to prevent unsafe operations.
|
8
|
+
"""
|
9
|
+
|
10
|
+
def __init__(self):
|
11
|
+
self.allowed_operators = {
|
12
|
+
ast.Eq: operator.eq,
|
13
|
+
ast.NotEq: operator.ne,
|
14
|
+
ast.Lt: operator.lt,
|
15
|
+
ast.LtE: operator.le,
|
16
|
+
ast.Gt: operator.gt,
|
17
|
+
ast.GtE: operator.ge,
|
18
|
+
# Additional operators can be added here as needed
|
19
|
+
}
|
20
|
+
|
21
|
+
def evaluate(self, expression, context):
|
22
|
+
"""
|
23
|
+
Evaluate a condition expression within a given context using AST parsing.
|
24
|
+
"""
|
25
|
+
try:
|
26
|
+
tree = ast.parse(expression, mode="eval")
|
27
|
+
return self._evaluate_node(tree.body, context)
|
28
|
+
except Exception as e:
|
29
|
+
raise ValueError(f"Failed to evaluate expression: {expression}. Error: {e}")
|
30
|
+
|
31
|
+
def _evaluate_node(self, node, context):
|
32
|
+
if isinstance(node, ast.Compare):
|
33
|
+
left = self._evaluate_node(node.left, context)
|
34
|
+
for operation, comparator in zip(node.ops, node.comparators):
|
35
|
+
op_func = self.allowed_operators.get(type(operation))
|
36
|
+
if not op_func:
|
37
|
+
raise ValueError(
|
38
|
+
f"Operation {type(operation).__name__} is not allowed."
|
39
|
+
)
|
40
|
+
right = self._evaluate_node(comparator, context)
|
41
|
+
if not op_func(left, right):
|
42
|
+
return False
|
43
|
+
return True
|
44
|
+
elif isinstance(node, ast.Name):
|
45
|
+
return context.get(node.id)
|
46
|
+
elif isinstance(node, ast.Constant):
|
47
|
+
return node.n
|
48
|
+
else:
|
49
|
+
raise ValueError(
|
50
|
+
"Unsupported AST node type encountered in condition evaluation."
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
class ASTEvaluationEngine:
|
55
|
+
"""
|
56
|
+
Executes scripts safely using the SafeEvaluator for expression evaluation.
|
57
|
+
"""
|
58
|
+
|
59
|
+
def __init__(self):
|
60
|
+
self.variables = {}
|
61
|
+
self.safe_evaluator = ASTEvaluator()
|
62
|
+
self.functions = {
|
63
|
+
"processData": self.process_data,
|
64
|
+
}
|
65
|
+
|
66
|
+
def process_data(self, data):
|
67
|
+
# Example placeholder function for data processing
|
68
|
+
return data * 2
|
69
|
+
|
70
|
+
def _evaluate_expression(self, expression):
|
71
|
+
"""
|
72
|
+
Evaluates expressions within scripts using SafeEvaluator.
|
73
|
+
"""
|
74
|
+
# Here, 'self.variables' serves as the context for the evaluation
|
75
|
+
return self.safe_evaluator.evaluate(expression, self.variables)
|
76
|
+
|
77
|
+
def _assign_variable(self, var_name, value):
|
78
|
+
"""
|
79
|
+
Assigns a value to a variable within the script's context.
|
80
|
+
"""
|
81
|
+
self.variables[var_name] = value
|
82
|
+
|
83
|
+
def _execute_function(self, func_name, arg):
|
84
|
+
"""
|
85
|
+
Executes a predefined function with the given argument.
|
86
|
+
"""
|
87
|
+
if func_name in self.functions:
|
88
|
+
function = self.functions[func_name]
|
89
|
+
return function(arg)
|
90
|
+
else:
|
91
|
+
raise ValueError(f"Function '{func_name}' is not defined.")
|
92
|
+
|
93
|
+
def execute(self, script):
|
94
|
+
"""
|
95
|
+
Parses and executes a script, handling variable assignments and function calls.
|
96
|
+
"""
|
97
|
+
tree = ast.parse(script, mode="exec")
|
98
|
+
for stmt in tree.body:
|
99
|
+
if isinstance(stmt, ast.Assign):
|
100
|
+
var_name = stmt.targets[
|
101
|
+
0
|
102
|
+
].id # Assumes single target assignment for simplicity
|
103
|
+
# Convert the AST node back to a string for evaluation
|
104
|
+
value_expr = ast.unparse(stmt.value)
|
105
|
+
value = self._evaluate_expression(value_expr)
|
106
|
+
self._assign_variable(var_name, value)
|
107
|
+
elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
108
|
+
func_name = stmt.value.func.id
|
109
|
+
arg_expr = ast.unparse(stmt.value.args[0])
|
110
|
+
arg = self._evaluate_expression(arg_expr)
|
111
|
+
self._execute_function(func_name, arg)
|
112
|
+
else:
|
113
|
+
raise ValueError(
|
114
|
+
"Unsupported statement type encountered in script execution."
|
115
|
+
)
|