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,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: BaseNode, executable_id, request_source):
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: dict[Edge] = self.get_node_edges(current_node, node_as="out")
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: dict[Edge] = self.get_node_edges(node, node_as="out")
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. If further actions are needed, invoke tool usage.
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 steps based on the provided tools and parameters.
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 steps.
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 steps.
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=False,
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 auto and not self.branch._is_invoked():
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
- _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
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
@@ -250,4 +250,4 @@ class MonoChatMixin(MonoChatConfigMixin, MonoChatInvokeMixin, ABC):
250
250
  Mixin class that combines MonoChatConfigMixin and MonoChatInvokeMixin.
251
251
  """
252
252
 
253
- pass
253
+ pass
@@ -150,7 +150,7 @@ class MonoFollowup(MonoChat):
150
150
  system=None,
151
151
  tools=None,
152
152
  max_followup: int = 1,
153
- auto=False,
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 auto and not self.branch._is_invoked():
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 auto:
205
- if not self.branch._is_invoked():
206
- return _out if out else None
204
+ if not self.branch._is_invoked():
205
+ return _out if out else None
207
206
 
208
- _prompt = self._get_prompt(
209
- prompt=output_prompt,
210
- default=self.OUTPUT_PROMPT,
211
- instruction=instruction,
212
- )
213
- _out = await self.chat(_prompt, sender=sender, **kwargs)
214
- return _out if out else None
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.
@@ -35,7 +35,7 @@ class Condition(BaseModel, ABC):
35
35
  extra = "allow"
36
36
 
37
37
  @abstractmethod
38
- def __call__(self, *args, **kwargs) -> bool:
38
+ def __call__(self, executable) -> bool:
39
39
  """Evaluates the condition based on implemented logic.
40
40
 
41
41
  Returns:
@@ -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
- self.add_sources(sources)
23
+ if sources:
24
+ self.add_sources(sources)
24
25
  self.execute_stop = False
25
26
 
26
27
  def add_sources(self, sources):
@@ -982,4 +982,4 @@ class Session:
982
982
  if system:
983
983
  self.default_branch.add_message(system=system, sender=sender)
984
984
 
985
- self.llmconfig = self.default_branch.llmconfig
985
+ self.llmconfig = self.default_branch.llmconfig
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
+ )