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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. lionagi/core/agent/base_agent.py +2 -3
  2. lionagi/core/branch/base.py +1 -1
  3. lionagi/core/branch/branch.py +2 -1
  4. lionagi/core/branch/flow_mixin.py +1 -1
  5. lionagi/core/branch/util.py +1 -1
  6. lionagi/core/execute/base_executor.py +1 -4
  7. lionagi/core/execute/branch_executor.py +66 -3
  8. lionagi/core/execute/instruction_map_executor.py +48 -0
  9. lionagi/core/execute/neo4j_executor.py +381 -0
  10. lionagi/core/execute/structure_executor.py +99 -3
  11. lionagi/core/flow/monoflow/ReAct.py +18 -18
  12. lionagi/core/flow/monoflow/chat_mixin.py +1 -1
  13. lionagi/core/flow/monoflow/followup.py +11 -12
  14. lionagi/core/flow/polyflow/__init__.py +1 -1
  15. lionagi/core/generic/component.py +0 -2
  16. lionagi/core/generic/condition.py +1 -1
  17. lionagi/core/generic/edge.py +52 -0
  18. lionagi/core/mail/mail_manager.py +3 -2
  19. lionagi/core/session/session.py +1 -1
  20. lionagi/experimental/__init__.py +0 -0
  21. lionagi/experimental/directive/__init__.py +0 -0
  22. lionagi/experimental/directive/evaluator/__init__.py +0 -0
  23. lionagi/experimental/directive/evaluator/ast_evaluator.py +115 -0
  24. lionagi/experimental/directive/evaluator/base_evaluator.py +202 -0
  25. lionagi/experimental/directive/evaluator/sandbox_.py +14 -0
  26. lionagi/experimental/directive/evaluator/script_engine.py +83 -0
  27. lionagi/experimental/directive/parser/__init__.py +0 -0
  28. lionagi/experimental/directive/parser/base_parser.py +215 -0
  29. lionagi/experimental/directive/schema.py +36 -0
  30. lionagi/experimental/directive/template_/__init__.py +0 -0
  31. lionagi/experimental/directive/template_/base_template.py +63 -0
  32. lionagi/experimental/tool/__init__.py +0 -0
  33. lionagi/experimental/tool/function_calling.py +43 -0
  34. lionagi/experimental/tool/manual.py +66 -0
  35. lionagi/experimental/tool/schema.py +59 -0
  36. lionagi/experimental/tool/tool_manager.py +138 -0
  37. lionagi/experimental/tool/util.py +16 -0
  38. lionagi/experimental/work/__init__.py +0 -0
  39. lionagi/experimental/work/_logger.py +25 -0
  40. lionagi/experimental/work/exchange.py +0 -0
  41. lionagi/experimental/work/schema.py +30 -0
  42. lionagi/experimental/work/tests.py +72 -0
  43. lionagi/experimental/work/util.py +0 -0
  44. lionagi/experimental/work/work_function.py +89 -0
  45. lionagi/experimental/work/worker.py +12 -0
  46. lionagi/integrations/bridge/autogen_/__init__.py +0 -0
  47. lionagi/integrations/bridge/autogen_/autogen_.py +124 -0
  48. lionagi/integrations/bridge/llamaindex_/get_index.py +294 -0
  49. lionagi/integrations/bridge/llamaindex_/llama_pack.py +227 -0
  50. lionagi/integrations/bridge/transformers_/__init__.py +0 -0
  51. lionagi/integrations/bridge/transformers_/install_.py +36 -0
  52. lionagi/integrations/config/oai_configs.py +1 -1
  53. lionagi/integrations/config/ollama_configs.py +1 -1
  54. lionagi/integrations/config/openrouter_configs.py +1 -1
  55. lionagi/integrations/storage/__init__.py +3 -0
  56. lionagi/integrations/storage/neo4j.py +673 -0
  57. lionagi/integrations/storage/storage_util.py +289 -0
  58. lionagi/integrations/storage/to_csv.py +63 -0
  59. lionagi/integrations/storage/to_excel.py +67 -0
  60. lionagi/libs/ln_knowledge_graph.py +405 -0
  61. lionagi/libs/ln_queue.py +101 -0
  62. lionagi/libs/ln_tokenizer.py +57 -0
  63. lionagi/libs/sys_util.py +1 -1
  64. lionagi/lions/__init__.py +0 -0
  65. lionagi/lions/coder/__init__.py +0 -0
  66. lionagi/lions/coder/add_feature.py +20 -0
  67. lionagi/lions/coder/base_prompts.py +22 -0
  68. lionagi/lions/coder/coder.py +121 -0
  69. lionagi/lions/coder/util.py +91 -0
  70. lionagi/lions/researcher/__init__.py +0 -0
  71. lionagi/lions/researcher/data_source/__init__.py +0 -0
  72. lionagi/lions/researcher/data_source/finhub_.py +191 -0
  73. lionagi/lions/researcher/data_source/google_.py +199 -0
  74. lionagi/lions/researcher/data_source/wiki_.py +96 -0
  75. lionagi/lions/researcher/data_source/yfinance_.py +21 -0
  76. lionagi/tests/libs/test_queue.py +67 -0
  77. lionagi/tests/test_core/test_branch.py +0 -1
  78. lionagi/version.py +1 -1
  79. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/METADATA +1 -1
  80. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/RECORD +83 -29
  81. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/LICENSE +0 -0
  82. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/WHEEL +0 -0
  83. {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/top_level.txt +0 -0
@@ -19,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
+ )