flatagents 0.4.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.
@@ -0,0 +1,166 @@
1
+ """
2
+ Simple expression engine for flatmachines.
3
+
4
+ Supports:
5
+ - Comparisons: ==, !=, <, <=, >, >=
6
+ - Boolean operators: and, or, not
7
+ - Field access: context.field, input.field, output.field
8
+ - Literals: strings, numbers, booleans, null
9
+
10
+ Examples:
11
+ context.score >= 8
12
+ context.current == context.target
13
+ context.score >= 8 and context.round < 4
14
+ not context.failed
15
+ """
16
+
17
+ import ast
18
+ import operator
19
+ import re
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+
23
+ class SimpleExpressionEngine:
24
+ """
25
+ Simple expression parser and evaluator.
26
+
27
+ Uses Python's ast module for safe parsing, then evaluates
28
+ with a restricted set of operations.
29
+ """
30
+
31
+ # Supported comparison operators
32
+ COMPARISON_OPS = {
33
+ ast.Eq: operator.eq,
34
+ ast.NotEq: operator.ne,
35
+ ast.Lt: operator.lt,
36
+ ast.LtE: operator.le,
37
+ ast.Gt: operator.gt,
38
+ ast.GtE: operator.ge,
39
+ }
40
+
41
+ # Supported boolean operators
42
+ BOOL_OPS = {
43
+ ast.And: lambda values: all(values),
44
+ ast.Or: lambda values: any(values),
45
+ }
46
+
47
+ def __init__(self):
48
+ pass
49
+
50
+ def evaluate(self, expression: str, variables: Dict[str, Any]) -> Any:
51
+ """
52
+ Evaluate an expression with the given variables.
53
+
54
+ Args:
55
+ expression: The expression string to evaluate
56
+ variables: Dictionary of variable names to values
57
+
58
+ Returns:
59
+ The result of evaluating the expression
60
+
61
+ Raises:
62
+ ValueError: If expression syntax is invalid
63
+ KeyError: If referenced variable doesn't exist
64
+ """
65
+ if not expression or not expression.strip():
66
+ return True # Empty expression is always true (default transition)
67
+
68
+ try:
69
+ tree = ast.parse(expression, mode='eval')
70
+ except SyntaxError as e:
71
+ raise ValueError(f"Invalid expression syntax: {expression}") from e
72
+
73
+ return self._eval_node(tree.body, variables)
74
+
75
+ def _eval_node(self, node: ast.AST, variables: Dict[str, Any]) -> Any:
76
+ """Recursively evaluate an AST node."""
77
+
78
+ # Literals
79
+ if isinstance(node, ast.Constant):
80
+ return node.value
81
+
82
+ # Name (variable reference like 'context', 'input', 'output')
83
+ if isinstance(node, ast.Name):
84
+ name = node.id
85
+ # Handle boolean literals
86
+ if name == 'true' or name == 'True':
87
+ return True
88
+ if name == 'false' or name == 'False':
89
+ return False
90
+ if name == 'null' or name == 'None':
91
+ return None
92
+ if name not in variables:
93
+ raise KeyError(f"Unknown variable: {name}")
94
+ return variables[name]
95
+
96
+ # Attribute access (e.g., context.score, context.nested.field)
97
+ if isinstance(node, ast.Attribute):
98
+ value = self._eval_node(node.value, variables)
99
+ attr = node.attr
100
+ if isinstance(value, dict):
101
+ if attr not in value:
102
+ return None # Missing fields return None
103
+ return value[attr]
104
+ return getattr(value, attr, None)
105
+
106
+ # Comparison (e.g., context.score >= 8)
107
+ if isinstance(node, ast.Compare):
108
+ left = self._eval_node(node.left, variables)
109
+ for op, comparator in zip(node.ops, node.comparators):
110
+ right = self._eval_node(comparator, variables)
111
+ op_func = self.COMPARISON_OPS.get(type(op))
112
+ if op_func is None:
113
+ raise ValueError(f"Unsupported comparison operator: {type(op).__name__}")
114
+ if not op_func(left, right):
115
+ return False
116
+ left = right
117
+ return True
118
+
119
+ # Boolean operators (and, or)
120
+ if isinstance(node, ast.BoolOp):
121
+ op_func = self.BOOL_OPS.get(type(node.op))
122
+ if op_func is None:
123
+ raise ValueError(f"Unsupported boolean operator: {type(node.op).__name__}")
124
+ # Short-circuit evaluation
125
+ if isinstance(node.op, ast.And):
126
+ for value_node in node.values:
127
+ if not self._eval_node(value_node, variables):
128
+ return False
129
+ return True
130
+ else: # Or
131
+ for value_node in node.values:
132
+ if self._eval_node(value_node, variables):
133
+ return True
134
+ return False
135
+
136
+ # Unary operators (not)
137
+ if isinstance(node, ast.UnaryOp):
138
+ operand = self._eval_node(node.operand, variables)
139
+ if isinstance(node.op, ast.Not):
140
+ return not operand
141
+ raise ValueError(f"Unsupported unary operator: {type(node.op).__name__}")
142
+
143
+ # Binary operators (for arithmetic in expressions like context.round + 1)
144
+ if isinstance(node, ast.BinOp):
145
+ left = self._eval_node(node.left, variables)
146
+ right = self._eval_node(node.right, variables)
147
+ if isinstance(node.op, ast.Add):
148
+ return left + right
149
+ if isinstance(node.op, ast.Sub):
150
+ return left - right
151
+ if isinstance(node.op, ast.Mult):
152
+ return left * right
153
+ if isinstance(node.op, ast.Div):
154
+ return left / right
155
+ raise ValueError(f"Unsupported binary operator: {type(node.op).__name__}")
156
+
157
+ # Subscript (e.g., context["key"])
158
+ if isinstance(node, ast.Subscript):
159
+ value = self._eval_node(node.value, variables)
160
+ index = self._eval_node(node.slice, variables)
161
+ return value[index]
162
+
163
+ raise ValueError(f"Unsupported expression type: {type(node).__name__}")
164
+
165
+
166
+ __all__ = ["SimpleExpressionEngine"]