AbstractRuntime 0.4.0__py3-none-any.whl → 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.
- abstractruntime/__init__.py +76 -1
- abstractruntime/core/config.py +68 -1
- abstractruntime/core/models.py +5 -0
- abstractruntime/core/policy.py +74 -3
- abstractruntime/core/runtime.py +1002 -126
- abstractruntime/core/vars.py +8 -2
- abstractruntime/evidence/recorder.py +1 -1
- abstractruntime/history_bundle.py +772 -0
- abstractruntime/integrations/abstractcore/__init__.py +3 -0
- abstractruntime/integrations/abstractcore/default_tools.py +127 -3
- abstractruntime/integrations/abstractcore/effect_handlers.py +2440 -99
- abstractruntime/integrations/abstractcore/embeddings_client.py +69 -0
- abstractruntime/integrations/abstractcore/factory.py +68 -20
- abstractruntime/integrations/abstractcore/llm_client.py +447 -15
- abstractruntime/integrations/abstractcore/mcp_worker.py +1 -0
- abstractruntime/integrations/abstractcore/session_attachments.py +946 -0
- abstractruntime/integrations/abstractcore/tool_executor.py +31 -10
- abstractruntime/integrations/abstractcore/workspace_scoped_tools.py +561 -0
- abstractruntime/integrations/abstractmemory/__init__.py +3 -0
- abstractruntime/integrations/abstractmemory/effect_handlers.py +946 -0
- abstractruntime/memory/active_context.py +6 -1
- abstractruntime/memory/kg_packets.py +164 -0
- abstractruntime/memory/memact_composer.py +175 -0
- abstractruntime/memory/recall_levels.py +163 -0
- abstractruntime/memory/token_budget.py +86 -0
- abstractruntime/storage/__init__.py +4 -1
- abstractruntime/storage/artifacts.py +158 -30
- abstractruntime/storage/base.py +17 -1
- abstractruntime/storage/commands.py +339 -0
- abstractruntime/storage/in_memory.py +41 -1
- abstractruntime/storage/json_files.py +195 -12
- abstractruntime/storage/observable.py +38 -1
- abstractruntime/storage/offloading.py +433 -0
- abstractruntime/storage/sqlite.py +836 -0
- abstractruntime/visualflow_compiler/__init__.py +29 -0
- abstractruntime/visualflow_compiler/adapters/__init__.py +11 -0
- abstractruntime/visualflow_compiler/adapters/agent_adapter.py +126 -0
- abstractruntime/visualflow_compiler/adapters/context_adapter.py +109 -0
- abstractruntime/visualflow_compiler/adapters/control_adapter.py +615 -0
- abstractruntime/visualflow_compiler/adapters/effect_adapter.py +1051 -0
- abstractruntime/visualflow_compiler/adapters/event_adapter.py +307 -0
- abstractruntime/visualflow_compiler/adapters/function_adapter.py +97 -0
- abstractruntime/visualflow_compiler/adapters/memact_adapter.py +114 -0
- abstractruntime/visualflow_compiler/adapters/subflow_adapter.py +74 -0
- abstractruntime/visualflow_compiler/adapters/variable_adapter.py +316 -0
- abstractruntime/visualflow_compiler/compiler.py +3832 -0
- abstractruntime/visualflow_compiler/flow.py +247 -0
- abstractruntime/visualflow_compiler/visual/__init__.py +13 -0
- abstractruntime/visualflow_compiler/visual/agent_ids.py +29 -0
- abstractruntime/visualflow_compiler/visual/builtins.py +1376 -0
- abstractruntime/visualflow_compiler/visual/code_executor.py +214 -0
- abstractruntime/visualflow_compiler/visual/executor.py +2804 -0
- abstractruntime/visualflow_compiler/visual/models.py +211 -0
- abstractruntime/workflow_bundle/__init__.py +52 -0
- abstractruntime/workflow_bundle/models.py +236 -0
- abstractruntime/workflow_bundle/packer.py +317 -0
- abstractruntime/workflow_bundle/reader.py +87 -0
- abstractruntime/workflow_bundle/registry.py +587 -0
- abstractruntime-0.4.1.dist-info/METADATA +177 -0
- abstractruntime-0.4.1.dist-info/RECORD +86 -0
- abstractruntime-0.4.0.dist-info/METADATA +0 -167
- abstractruntime-0.4.0.dist-info/RECORD +0 -49
- {abstractruntime-0.4.0.dist-info → abstractruntime-0.4.1.dist-info}/WHEEL +0 -0
- {abstractruntime-0.4.0.dist-info → abstractruntime-0.4.1.dist-info}/entry_points.txt +0 -0
- {abstractruntime-0.4.0.dist-info → abstractruntime-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1376 @@
|
|
|
1
|
+
"""Built-in function handlers for visual nodes.
|
|
2
|
+
|
|
3
|
+
These are intentionally pure and JSON-friendly so visual workflows can run in
|
|
4
|
+
any host that can compile the VisualFlow JSON to a WorkflowSpec.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import json
|
|
12
|
+
import locale
|
|
13
|
+
import os
|
|
14
|
+
import random
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from abstractruntime.rendering import render_agent_trace_markdown as runtime_render_agent_trace_markdown
|
|
18
|
+
from abstractruntime.rendering import stringify_json as runtime_stringify_json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_builtin_handler(node_type: str) -> Optional[Callable[[Any], Any]]:
|
|
22
|
+
"""Get a built-in handler function for a node type."""
|
|
23
|
+
return BUILTIN_HANDLERS.get(node_type)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _path_tokens(path: str) -> list[Any]:
|
|
27
|
+
"""Parse a dotted/bracket path into tokens.
|
|
28
|
+
|
|
29
|
+
Supported:
|
|
30
|
+
- `a.b.c`
|
|
31
|
+
- `a[0].b`
|
|
32
|
+
|
|
33
|
+
Returns tokens as str keys and int indices.
|
|
34
|
+
"""
|
|
35
|
+
import re
|
|
36
|
+
|
|
37
|
+
p = str(path or "").strip()
|
|
38
|
+
if not p:
|
|
39
|
+
return []
|
|
40
|
+
token_re = re.compile(r"([^\.\[\]]+)|\[(\d+)\]")
|
|
41
|
+
out: list[Any] = []
|
|
42
|
+
for m in token_re.finditer(p):
|
|
43
|
+
key = m.group(1)
|
|
44
|
+
if key is not None:
|
|
45
|
+
k = key.strip()
|
|
46
|
+
if k:
|
|
47
|
+
out.append(k)
|
|
48
|
+
continue
|
|
49
|
+
idx = m.group(2)
|
|
50
|
+
if idx is not None:
|
|
51
|
+
try:
|
|
52
|
+
out.append(int(idx))
|
|
53
|
+
except Exception:
|
|
54
|
+
continue
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_path(value: Any, path: str) -> Any:
|
|
59
|
+
"""Best-effort nested lookup (dict keys + list indices)."""
|
|
60
|
+
tokens = _path_tokens(path)
|
|
61
|
+
if not tokens:
|
|
62
|
+
return None
|
|
63
|
+
current: Any = value
|
|
64
|
+
for tok in tokens:
|
|
65
|
+
if isinstance(current, dict) and isinstance(tok, str):
|
|
66
|
+
current = current.get(tok)
|
|
67
|
+
continue
|
|
68
|
+
if isinstance(current, list):
|
|
69
|
+
idx: Optional[int] = None
|
|
70
|
+
if isinstance(tok, int):
|
|
71
|
+
idx = tok
|
|
72
|
+
elif isinstance(tok, str) and tok.isdigit():
|
|
73
|
+
idx = int(tok)
|
|
74
|
+
if idx is None:
|
|
75
|
+
return None
|
|
76
|
+
if idx < 0 or idx >= len(current):
|
|
77
|
+
return None
|
|
78
|
+
current = current[idx]
|
|
79
|
+
continue
|
|
80
|
+
return None
|
|
81
|
+
return current
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Math operations
|
|
85
|
+
def math_add(inputs: Dict[str, Any]) -> float:
|
|
86
|
+
"""Add two numbers."""
|
|
87
|
+
return float(inputs.get("a", 0)) + float(inputs.get("b", 0))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def math_subtract(inputs: Dict[str, Any]) -> float:
|
|
91
|
+
"""Subtract b from a."""
|
|
92
|
+
return float(inputs.get("a", 0)) - float(inputs.get("b", 0))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def math_multiply(inputs: Dict[str, Any]) -> float:
|
|
96
|
+
"""Multiply two numbers."""
|
|
97
|
+
return float(inputs.get("a", 0)) * float(inputs.get("b", 0))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def math_divide(inputs: Dict[str, Any]) -> float:
|
|
101
|
+
"""Divide a by b."""
|
|
102
|
+
b = float(inputs.get("b", 1))
|
|
103
|
+
if b == 0:
|
|
104
|
+
raise ValueError("Division by zero")
|
|
105
|
+
return float(inputs.get("a", 0)) / b
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def math_modulo(inputs: Dict[str, Any]) -> float:
|
|
109
|
+
"""Get remainder of a divided by b."""
|
|
110
|
+
b = float(inputs.get("b", 1))
|
|
111
|
+
if b == 0:
|
|
112
|
+
raise ValueError("Modulo by zero")
|
|
113
|
+
return float(inputs.get("a", 0)) % b
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def math_power(inputs: Dict[str, Any]) -> float:
|
|
117
|
+
"""Raise base to exponent power."""
|
|
118
|
+
return float(inputs.get("base", 0)) ** float(inputs.get("exp", 1))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def math_abs(inputs: Dict[str, Any]) -> float:
|
|
122
|
+
"""Get absolute value."""
|
|
123
|
+
return abs(float(inputs.get("value", 0)))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def math_round(inputs: Dict[str, Any]) -> float:
|
|
127
|
+
"""Round to specified decimal places."""
|
|
128
|
+
value = float(inputs.get("value", 0))
|
|
129
|
+
decimals = int(inputs.get("decimals", 0))
|
|
130
|
+
return round(value, decimals)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def math_random_int(inputs: Dict[str, Any]) -> int:
|
|
134
|
+
"""Random integer in [min, max] (inclusive)."""
|
|
135
|
+
raw_min = inputs.get("min", 0)
|
|
136
|
+
raw_max = inputs.get("max", 100)
|
|
137
|
+
try:
|
|
138
|
+
min_v = int(float(raw_min))
|
|
139
|
+
except Exception:
|
|
140
|
+
min_v = 0
|
|
141
|
+
try:
|
|
142
|
+
max_v = int(float(raw_max))
|
|
143
|
+
except Exception:
|
|
144
|
+
max_v = 100
|
|
145
|
+
|
|
146
|
+
if max_v < min_v:
|
|
147
|
+
min_v, max_v = max_v, min_v
|
|
148
|
+
return random.randint(min_v, max_v)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def math_random_float(inputs: Dict[str, Any]) -> float:
|
|
152
|
+
"""Random float in [min, max]."""
|
|
153
|
+
raw_min = inputs.get("min", 0)
|
|
154
|
+
raw_max = inputs.get("max", 1)
|
|
155
|
+
try:
|
|
156
|
+
min_v = float(raw_min)
|
|
157
|
+
except Exception:
|
|
158
|
+
min_v = 0.0
|
|
159
|
+
try:
|
|
160
|
+
max_v = float(raw_max)
|
|
161
|
+
except Exception:
|
|
162
|
+
max_v = 1.0
|
|
163
|
+
|
|
164
|
+
if max_v < min_v:
|
|
165
|
+
min_v, max_v = max_v, min_v
|
|
166
|
+
if max_v == min_v:
|
|
167
|
+
return float(min_v)
|
|
168
|
+
return min_v + (max_v - min_v) * random.random()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# String operations
|
|
172
|
+
def string_concat(inputs: Dict[str, Any]) -> str:
|
|
173
|
+
"""Concatenate two strings."""
|
|
174
|
+
return str(inputs.get("a", "")) + str(inputs.get("b", ""))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def string_split(inputs: Dict[str, Any]) -> List[str]:
|
|
178
|
+
"""Split a string by a delimiter (defaults are tuned for real-world workflow usage).
|
|
179
|
+
|
|
180
|
+
Notes:
|
|
181
|
+
- Visual workflows often use human-edited / LLM-generated text where trailing
|
|
182
|
+
delimiters are common (e.g. "A@@B@@"). A strict `str.split` would produce an
|
|
183
|
+
empty last element and create a spurious downstream loop iteration.
|
|
184
|
+
- We therefore support optional normalization flags with sensible defaults:
|
|
185
|
+
- `trim` (default True): strip whitespace around parts
|
|
186
|
+
- `drop_empty` (default True): drop empty parts after trimming
|
|
187
|
+
- Delimiters may be entered as escape sequences (e.g. "\\n") from the UI.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
raw_text = inputs.get("text", "")
|
|
191
|
+
text = "" if raw_text is None else str(raw_text)
|
|
192
|
+
|
|
193
|
+
raw_delim = inputs.get("delimiter", ",")
|
|
194
|
+
delimiter = "" if raw_delim is None else str(raw_delim)
|
|
195
|
+
delimiter = delimiter.replace("\\n", "\n").replace("\\t", "\t").replace("\\r", "\r")
|
|
196
|
+
|
|
197
|
+
trim = bool(inputs.get("trim", True))
|
|
198
|
+
drop_empty = bool(inputs.get("drop_empty", True))
|
|
199
|
+
|
|
200
|
+
# Avoid ValueError from Python's `split("")` and keep behavior predictable.
|
|
201
|
+
if delimiter == "":
|
|
202
|
+
parts = [text] if text else []
|
|
203
|
+
else:
|
|
204
|
+
raw_maxsplit = inputs.get("maxsplit")
|
|
205
|
+
maxsplit: Optional[int] = None
|
|
206
|
+
if raw_maxsplit is not None:
|
|
207
|
+
try:
|
|
208
|
+
maxsplit = int(raw_maxsplit)
|
|
209
|
+
except Exception:
|
|
210
|
+
maxsplit = None
|
|
211
|
+
if maxsplit is not None and maxsplit >= 0:
|
|
212
|
+
parts = text.split(delimiter, maxsplit)
|
|
213
|
+
else:
|
|
214
|
+
parts = text.split(delimiter)
|
|
215
|
+
|
|
216
|
+
if trim:
|
|
217
|
+
parts = [p.strip() for p in parts]
|
|
218
|
+
|
|
219
|
+
if drop_empty:
|
|
220
|
+
parts = [p for p in parts if p != ""]
|
|
221
|
+
|
|
222
|
+
return parts
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def string_join(inputs: Dict[str, Any]) -> str:
|
|
226
|
+
"""Join array items with delimiter."""
|
|
227
|
+
items = inputs.get("items")
|
|
228
|
+
# Visual workflows frequently pass optional pins; treat `null` as empty.
|
|
229
|
+
if items is None:
|
|
230
|
+
items_list: list[Any] = []
|
|
231
|
+
elif isinstance(items, list):
|
|
232
|
+
items_list = items
|
|
233
|
+
elif isinstance(items, tuple):
|
|
234
|
+
items_list = list(items)
|
|
235
|
+
else:
|
|
236
|
+
# Defensive: if a non-array leaks in, treat it as a single element instead of
|
|
237
|
+
# iterating characters (strings) or keys (dicts).
|
|
238
|
+
items_list = [items]
|
|
239
|
+
|
|
240
|
+
delimiter = str(inputs.get("delimiter", ","))
|
|
241
|
+
# UI often stores escape sequences (e.g. "\\n") in JSON.
|
|
242
|
+
delimiter = delimiter.replace("\\n", "\n").replace("\\t", "\t").replace("\\r", "\r")
|
|
243
|
+
return delimiter.join("" if item is None else str(item) for item in items_list)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def string_format(inputs: Dict[str, Any]) -> str:
|
|
247
|
+
"""Format string with values."""
|
|
248
|
+
template = str(inputs.get("template", ""))
|
|
249
|
+
values = inputs.get("values", {})
|
|
250
|
+
if isinstance(values, dict):
|
|
251
|
+
return template.format(**values)
|
|
252
|
+
return template
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def string_uppercase(inputs: Dict[str, Any]) -> str:
|
|
256
|
+
"""Convert to uppercase."""
|
|
257
|
+
return str(inputs.get("text", "")).upper()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def string_lowercase(inputs: Dict[str, Any]) -> str:
|
|
261
|
+
"""Convert to lowercase."""
|
|
262
|
+
return str(inputs.get("text", "")).lower()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def string_trim(inputs: Dict[str, Any]) -> str:
|
|
266
|
+
"""Trim whitespace."""
|
|
267
|
+
return str(inputs.get("text", "")).strip()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def string_substring(inputs: Dict[str, Any]) -> str:
|
|
271
|
+
"""Get substring."""
|
|
272
|
+
text = str(inputs.get("text", ""))
|
|
273
|
+
start = int(inputs.get("start", 0))
|
|
274
|
+
end = inputs.get("end")
|
|
275
|
+
if end is not None:
|
|
276
|
+
return text[start : int(end)]
|
|
277
|
+
return text[start:]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def string_length(inputs: Dict[str, Any]) -> int:
|
|
281
|
+
"""Get string length."""
|
|
282
|
+
return len(str(inputs.get("text", "")))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def string_contains(inputs: Dict[str, Any]) -> bool:
|
|
286
|
+
"""Return True if `text` contains `pattern` (substring match)."""
|
|
287
|
+
pattern_raw = inputs.get("pattern")
|
|
288
|
+
if pattern_raw is None:
|
|
289
|
+
return False
|
|
290
|
+
pattern = str(pattern_raw)
|
|
291
|
+
# Avoid surprising behavior where "" is "contained" in every string.
|
|
292
|
+
if pattern == "":
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
text_raw = inputs.get("text")
|
|
296
|
+
text = "" if text_raw is None else str(text_raw)
|
|
297
|
+
return pattern in text
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def string_replace(inputs: Dict[str, Any]) -> str:
|
|
301
|
+
"""Replace `pattern` in `text` with `replacement`.
|
|
302
|
+
|
|
303
|
+
mode:
|
|
304
|
+
- "first": replace the first occurrence
|
|
305
|
+
- "all" (default): replace all occurrences
|
|
306
|
+
"""
|
|
307
|
+
text_raw = inputs.get("text")
|
|
308
|
+
text = "" if text_raw is None else str(text_raw)
|
|
309
|
+
|
|
310
|
+
pattern_raw = inputs.get("pattern")
|
|
311
|
+
if pattern_raw is None:
|
|
312
|
+
return text
|
|
313
|
+
pattern = str(pattern_raw)
|
|
314
|
+
if pattern == "":
|
|
315
|
+
return text
|
|
316
|
+
|
|
317
|
+
replacement_raw = inputs.get("replacement")
|
|
318
|
+
replacement = "" if replacement_raw is None else str(replacement_raw)
|
|
319
|
+
|
|
320
|
+
mode_raw = inputs.get("mode")
|
|
321
|
+
mode = str(mode_raw).strip().lower() if mode_raw is not None else "all"
|
|
322
|
+
|
|
323
|
+
if mode in {"first", "once", "1"}:
|
|
324
|
+
return text.replace(pattern, replacement, 1)
|
|
325
|
+
if mode in {"all", "*", "global"}:
|
|
326
|
+
return text.replace(pattern, replacement)
|
|
327
|
+
|
|
328
|
+
# Best-effort support for numeric counts (e.g. mode="2").
|
|
329
|
+
try:
|
|
330
|
+
n = int(mode_raw) # type: ignore[arg-type]
|
|
331
|
+
if n < 0:
|
|
332
|
+
return text.replace(pattern, replacement)
|
|
333
|
+
return text.replace(pattern, replacement, n)
|
|
334
|
+
except Exception:
|
|
335
|
+
return text.replace(pattern, replacement)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def string_template(inputs: Dict[str, Any]) -> str:
|
|
339
|
+
"""Render a template with placeholders like `{{path.to.value}}`.
|
|
340
|
+
|
|
341
|
+
Supported filters:
|
|
342
|
+
- `| json` -> json.dumps(value)
|
|
343
|
+
- `| join(", ")` -> join array values with delimiter
|
|
344
|
+
- `| trim` / `| lower` / `| upper`
|
|
345
|
+
"""
|
|
346
|
+
import re
|
|
347
|
+
|
|
348
|
+
template = str(inputs.get("template", "") or "")
|
|
349
|
+
vars_raw = inputs.get("vars")
|
|
350
|
+
vars_obj = vars_raw if isinstance(vars_raw, dict) else {}
|
|
351
|
+
|
|
352
|
+
pat = re.compile(r"\{\{\s*(.*?)\s*\}\}")
|
|
353
|
+
|
|
354
|
+
def _apply_filters(value: Any, filters: list[str]) -> Any:
|
|
355
|
+
cur = value
|
|
356
|
+
for f in filters:
|
|
357
|
+
f = f.strip()
|
|
358
|
+
if not f:
|
|
359
|
+
continue
|
|
360
|
+
if f == "json":
|
|
361
|
+
cur = json.dumps(cur, ensure_ascii=False, sort_keys=True)
|
|
362
|
+
continue
|
|
363
|
+
if f.startswith("join"):
|
|
364
|
+
m = re.match(r"join\((.*)\)$", f)
|
|
365
|
+
delim = ", "
|
|
366
|
+
if m:
|
|
367
|
+
raw = m.group(1).strip()
|
|
368
|
+
if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")):
|
|
369
|
+
raw = raw[1:-1]
|
|
370
|
+
delim = raw
|
|
371
|
+
if isinstance(cur, list):
|
|
372
|
+
cur = delim.join("" if x is None else str(x) for x in cur)
|
|
373
|
+
else:
|
|
374
|
+
cur = "" if cur is None else str(cur)
|
|
375
|
+
continue
|
|
376
|
+
if f == "trim":
|
|
377
|
+
cur = ("" if cur is None else str(cur)).strip()
|
|
378
|
+
continue
|
|
379
|
+
if f == "lower":
|
|
380
|
+
cur = ("" if cur is None else str(cur)).lower()
|
|
381
|
+
continue
|
|
382
|
+
if f == "upper":
|
|
383
|
+
cur = ("" if cur is None else str(cur)).upper()
|
|
384
|
+
continue
|
|
385
|
+
# Unknown filters are ignored (best-effort, stable).
|
|
386
|
+
return cur
|
|
387
|
+
|
|
388
|
+
def _render_expr(expr: str) -> str:
|
|
389
|
+
parts = [p.strip() for p in str(expr or "").split("|")]
|
|
390
|
+
path = parts[0] if parts else ""
|
|
391
|
+
filters = parts[1:] if len(parts) > 1 else []
|
|
392
|
+
value = _get_path(vars_obj, path)
|
|
393
|
+
if value is None:
|
|
394
|
+
return ""
|
|
395
|
+
value = _apply_filters(value, filters)
|
|
396
|
+
return "" if value is None else str(value)
|
|
397
|
+
|
|
398
|
+
return pat.sub(lambda m: _render_expr(m.group(1)), template)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Control flow helpers (these return decision values, not execution control)
|
|
402
|
+
def control_compare(inputs: Dict[str, Any]) -> bool:
|
|
403
|
+
"""Compare two values."""
|
|
404
|
+
a = inputs.get("a")
|
|
405
|
+
b = inputs.get("b")
|
|
406
|
+
op = str(inputs.get("op", "=="))
|
|
407
|
+
|
|
408
|
+
if op == "==":
|
|
409
|
+
return a == b
|
|
410
|
+
if op == "!=":
|
|
411
|
+
return a != b
|
|
412
|
+
if op == "<":
|
|
413
|
+
try:
|
|
414
|
+
return a < b
|
|
415
|
+
except Exception:
|
|
416
|
+
return False
|
|
417
|
+
if op == "<=":
|
|
418
|
+
try:
|
|
419
|
+
return a <= b
|
|
420
|
+
except Exception:
|
|
421
|
+
return False
|
|
422
|
+
if op == ">":
|
|
423
|
+
try:
|
|
424
|
+
return a > b
|
|
425
|
+
except Exception:
|
|
426
|
+
return False
|
|
427
|
+
if op == ">=":
|
|
428
|
+
try:
|
|
429
|
+
return a >= b
|
|
430
|
+
except Exception:
|
|
431
|
+
return False
|
|
432
|
+
raise ValueError(f"Unknown comparison operator: {op}")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def control_not(inputs: Dict[str, Any]) -> bool:
|
|
436
|
+
"""Logical NOT."""
|
|
437
|
+
return not bool(inputs.get("value", False))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def control_and(inputs: Dict[str, Any]) -> bool:
|
|
441
|
+
"""Logical AND."""
|
|
442
|
+
return bool(inputs.get("a", False)) and bool(inputs.get("b", False))
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def control_or(inputs: Dict[str, Any]) -> bool:
|
|
446
|
+
"""Logical OR."""
|
|
447
|
+
return bool(inputs.get("a", False)) or bool(inputs.get("b", False))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def control_coalesce(inputs: Dict[str, Any]) -> Any:
|
|
451
|
+
"""Return the first non-None input in pin order.
|
|
452
|
+
|
|
453
|
+
Pin order is injected by the visual executor as `_pin_order` based on the node's
|
|
454
|
+
input pin list, so selection is deterministic and matches the visual layout.
|
|
455
|
+
"""
|
|
456
|
+
order = inputs.get("_pin_order")
|
|
457
|
+
pin_order: list[str] = []
|
|
458
|
+
if isinstance(order, list):
|
|
459
|
+
for x in order:
|
|
460
|
+
if isinstance(x, str) and x:
|
|
461
|
+
pin_order.append(x)
|
|
462
|
+
if not pin_order:
|
|
463
|
+
pin_order = ["a", "b"]
|
|
464
|
+
|
|
465
|
+
for pid in pin_order:
|
|
466
|
+
if pid not in inputs:
|
|
467
|
+
continue
|
|
468
|
+
v = inputs.get(pid)
|
|
469
|
+
if v is not None:
|
|
470
|
+
return v
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Data operations
|
|
475
|
+
def data_get(inputs: Dict[str, Any]) -> Any:
|
|
476
|
+
"""Get property from object."""
|
|
477
|
+
obj = inputs.get("object", {})
|
|
478
|
+
key = str(inputs.get("key", ""))
|
|
479
|
+
default = inputs.get("default")
|
|
480
|
+
|
|
481
|
+
value = _get_path(obj, key)
|
|
482
|
+
if value is None:
|
|
483
|
+
return {"value": default}
|
|
484
|
+
return {"value": value}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def data_set(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
488
|
+
"""Set property on object (returns new object)."""
|
|
489
|
+
obj = dict(inputs.get("object", {}))
|
|
490
|
+
key = str(inputs.get("key", ""))
|
|
491
|
+
value = inputs.get("value")
|
|
492
|
+
|
|
493
|
+
# Support dot notation
|
|
494
|
+
parts = key.split(".")
|
|
495
|
+
current = obj
|
|
496
|
+
for part in parts[:-1]:
|
|
497
|
+
nxt = current.get(part)
|
|
498
|
+
if not isinstance(nxt, dict):
|
|
499
|
+
nxt = {}
|
|
500
|
+
current[part] = nxt
|
|
501
|
+
current = nxt
|
|
502
|
+
current[parts[-1]] = value
|
|
503
|
+
return obj
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def data_merge(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
507
|
+
"""Merge two objects."""
|
|
508
|
+
a = dict(inputs.get("a", {}))
|
|
509
|
+
b = dict(inputs.get("b", {}))
|
|
510
|
+
return {**a, **b}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def data_make_object(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
514
|
+
"""Build a flat JSON object from the provided inputs.
|
|
515
|
+
|
|
516
|
+
VisualFlow's executor injects helper keys like `_pin_order` / `_literalValue`;
|
|
517
|
+
this node ignores keys starting with "_" to keep output stable.
|
|
518
|
+
"""
|
|
519
|
+
out: Dict[str, Any] = {}
|
|
520
|
+
for k, v in (inputs or {}).items():
|
|
521
|
+
if not isinstance(k, str):
|
|
522
|
+
continue
|
|
523
|
+
if k.startswith("_"):
|
|
524
|
+
continue
|
|
525
|
+
out[k] = v
|
|
526
|
+
return out
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def data_make_context(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
530
|
+
"""Build a context object {task, messages, ...context_extra}."""
|
|
531
|
+
context_extra = inputs.get("context_extra")
|
|
532
|
+
out: Dict[str, Any] = dict(context_extra) if isinstance(context_extra, dict) else {}
|
|
533
|
+
|
|
534
|
+
task = inputs.get("task")
|
|
535
|
+
if task is None:
|
|
536
|
+
out["task"] = ""
|
|
537
|
+
elif isinstance(task, str):
|
|
538
|
+
out["task"] = task
|
|
539
|
+
else:
|
|
540
|
+
out["task"] = str(task)
|
|
541
|
+
|
|
542
|
+
messages = inputs.get("messages")
|
|
543
|
+
if isinstance(messages, list):
|
|
544
|
+
out["messages"] = list(messages)
|
|
545
|
+
elif isinstance(messages, tuple):
|
|
546
|
+
out["messages"] = list(messages)
|
|
547
|
+
elif messages is None:
|
|
548
|
+
out["messages"] = []
|
|
549
|
+
else:
|
|
550
|
+
out["messages"] = [messages]
|
|
551
|
+
|
|
552
|
+
return {"context": out}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def data_add_message(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
556
|
+
"""Build a canonical message object for context.messages.
|
|
557
|
+
|
|
558
|
+
Shape:
|
|
559
|
+
{
|
|
560
|
+
"role": "...",
|
|
561
|
+
"content": "...",
|
|
562
|
+
"timestamp": "<utc iso>",
|
|
563
|
+
"metadata": {"message_id": "msg_<hex>"}
|
|
564
|
+
}
|
|
565
|
+
"""
|
|
566
|
+
role_raw = inputs.get("role")
|
|
567
|
+
role = str(role_raw) if role_raw is not None else "user"
|
|
568
|
+
|
|
569
|
+
content_raw = inputs.get("content")
|
|
570
|
+
content = str(content_raw) if content_raw is not None else ""
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
from datetime import timezone
|
|
574
|
+
|
|
575
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
576
|
+
except Exception:
|
|
577
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
578
|
+
|
|
579
|
+
import uuid
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
"message": {
|
|
583
|
+
"role": role,
|
|
584
|
+
"content": content,
|
|
585
|
+
"timestamp": timestamp,
|
|
586
|
+
"metadata": {"message_id": f"msg_{uuid.uuid4().hex}"},
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def data_make_meta(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
592
|
+
"""Build a host-facing meta envelope (portable, no strict schema enforcement)."""
|
|
593
|
+
extra = inputs.get("extra")
|
|
594
|
+
out: Dict[str, Any] = dict(extra) if isinstance(extra, dict) else {}
|
|
595
|
+
|
|
596
|
+
schema = inputs.get("schema")
|
|
597
|
+
schema_str = schema.strip() if isinstance(schema, str) and schema.strip() else "abstractcode.agent.v1.meta"
|
|
598
|
+
|
|
599
|
+
version = inputs.get("version")
|
|
600
|
+
version_int = 1
|
|
601
|
+
try:
|
|
602
|
+
if isinstance(version, bool):
|
|
603
|
+
version_int = 1
|
|
604
|
+
elif isinstance(version, (int, float)):
|
|
605
|
+
version_int = int(version)
|
|
606
|
+
elif isinstance(version, str) and version.strip():
|
|
607
|
+
version_int = int(float(version.strip()))
|
|
608
|
+
except Exception:
|
|
609
|
+
version_int = 1
|
|
610
|
+
|
|
611
|
+
out["schema"] = schema_str
|
|
612
|
+
out["version"] = version_int
|
|
613
|
+
|
|
614
|
+
output_mode = inputs.get("output_mode")
|
|
615
|
+
if isinstance(output_mode, str) and output_mode.strip():
|
|
616
|
+
out["output_mode"] = output_mode.strip()
|
|
617
|
+
|
|
618
|
+
provider = inputs.get("provider")
|
|
619
|
+
if isinstance(provider, str) and provider.strip():
|
|
620
|
+
out["provider"] = provider.strip()
|
|
621
|
+
elif provider is not None and str(provider).strip():
|
|
622
|
+
out["provider"] = str(provider).strip()
|
|
623
|
+
|
|
624
|
+
model = inputs.get("model")
|
|
625
|
+
if isinstance(model, str) and model.strip():
|
|
626
|
+
out["model"] = model.strip()
|
|
627
|
+
elif model is not None and str(model).strip():
|
|
628
|
+
out["model"] = str(model).strip()
|
|
629
|
+
|
|
630
|
+
sub_run_id = inputs.get("sub_run_id")
|
|
631
|
+
if isinstance(sub_run_id, str) and sub_run_id.strip():
|
|
632
|
+
out["sub_run_id"] = sub_run_id.strip()
|
|
633
|
+
|
|
634
|
+
iterations = inputs.get("iterations")
|
|
635
|
+
try:
|
|
636
|
+
if isinstance(iterations, bool):
|
|
637
|
+
pass
|
|
638
|
+
elif isinstance(iterations, (int, float)):
|
|
639
|
+
out["iterations"] = int(iterations)
|
|
640
|
+
elif isinstance(iterations, str) and iterations.strip():
|
|
641
|
+
out["iterations"] = int(float(iterations.strip()))
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
tool_calls = inputs.get("tool_calls")
|
|
646
|
+
try:
|
|
647
|
+
if isinstance(tool_calls, bool):
|
|
648
|
+
pass
|
|
649
|
+
elif isinstance(tool_calls, (int, float)):
|
|
650
|
+
out["tool_calls"] = int(tool_calls)
|
|
651
|
+
elif isinstance(tool_calls, str) and tool_calls.strip():
|
|
652
|
+
out["tool_calls"] = int(float(tool_calls.strip()))
|
|
653
|
+
except Exception:
|
|
654
|
+
pass
|
|
655
|
+
|
|
656
|
+
tool_results = inputs.get("tool_results")
|
|
657
|
+
try:
|
|
658
|
+
if isinstance(tool_results, bool):
|
|
659
|
+
pass
|
|
660
|
+
elif isinstance(tool_results, (int, float)):
|
|
661
|
+
out["tool_results"] = int(tool_results)
|
|
662
|
+
elif isinstance(tool_results, str) and tool_results.strip():
|
|
663
|
+
out["tool_results"] = int(float(tool_results.strip()))
|
|
664
|
+
except Exception:
|
|
665
|
+
pass
|
|
666
|
+
|
|
667
|
+
finish_reason = inputs.get("finish_reason")
|
|
668
|
+
if isinstance(finish_reason, str) and finish_reason.strip():
|
|
669
|
+
out["finish_reason"] = finish_reason.strip()
|
|
670
|
+
|
|
671
|
+
gen_time = inputs.get("gen_time")
|
|
672
|
+
if gen_time is not None and not isinstance(gen_time, bool):
|
|
673
|
+
out["gen_time"] = gen_time
|
|
674
|
+
|
|
675
|
+
ttft_ms = inputs.get("ttft_ms")
|
|
676
|
+
if ttft_ms is not None and not isinstance(ttft_ms, bool):
|
|
677
|
+
out["ttft_ms"] = ttft_ms
|
|
678
|
+
|
|
679
|
+
usage = inputs.get("usage")
|
|
680
|
+
if isinstance(usage, dict):
|
|
681
|
+
out["usage"] = dict(usage)
|
|
682
|
+
|
|
683
|
+
trace = inputs.get("trace")
|
|
684
|
+
if isinstance(trace, dict):
|
|
685
|
+
out["trace"] = dict(trace)
|
|
686
|
+
elif isinstance(trace, str) and trace.strip():
|
|
687
|
+
out["trace"] = {"trace_id": trace.strip()}
|
|
688
|
+
|
|
689
|
+
trace_id = inputs.get("trace_id")
|
|
690
|
+
trace_id_str = trace_id.strip() if isinstance(trace_id, str) and trace_id.strip() else ""
|
|
691
|
+
if trace_id_str:
|
|
692
|
+
trace = out.get("trace")
|
|
693
|
+
trace_obj: Dict[str, Any] = dict(trace) if isinstance(trace, dict) else {}
|
|
694
|
+
trace_obj["trace_id"] = trace_id_str
|
|
695
|
+
out["trace"] = trace_obj
|
|
696
|
+
|
|
697
|
+
warnings = inputs.get("warnings")
|
|
698
|
+
if isinstance(warnings, list):
|
|
699
|
+
out["warnings"] = [str(w) for w in warnings if str(w).strip()]
|
|
700
|
+
elif isinstance(warnings, tuple):
|
|
701
|
+
out["warnings"] = [str(w) for w in list(warnings) if str(w).strip()]
|
|
702
|
+
elif isinstance(warnings, str) and warnings.strip():
|
|
703
|
+
out["warnings"] = [warnings.strip()]
|
|
704
|
+
|
|
705
|
+
debug = inputs.get("debug")
|
|
706
|
+
if isinstance(debug, dict):
|
|
707
|
+
out["debug"] = dict(debug)
|
|
708
|
+
elif debug is not None:
|
|
709
|
+
out["debug"] = debug
|
|
710
|
+
|
|
711
|
+
return {"meta": out}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def data_make_scratchpad(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
715
|
+
"""Build a scratchpad/trace envelope (commonly from Agent outputs)."""
|
|
716
|
+
out: Dict[str, Any] = {}
|
|
717
|
+
|
|
718
|
+
sub_run_id = inputs.get("sub_run_id")
|
|
719
|
+
if isinstance(sub_run_id, str) and sub_run_id.strip():
|
|
720
|
+
out["sub_run_id"] = sub_run_id.strip()
|
|
721
|
+
|
|
722
|
+
workflow_id = inputs.get("workflow_id")
|
|
723
|
+
if isinstance(workflow_id, str) and workflow_id.strip():
|
|
724
|
+
out["workflow_id"] = workflow_id.strip()
|
|
725
|
+
|
|
726
|
+
task = inputs.get("task")
|
|
727
|
+
if task is None:
|
|
728
|
+
out["task"] = ""
|
|
729
|
+
elif isinstance(task, str):
|
|
730
|
+
out["task"] = task
|
|
731
|
+
else:
|
|
732
|
+
out["task"] = str(task)
|
|
733
|
+
|
|
734
|
+
messages = inputs.get("messages")
|
|
735
|
+
if isinstance(messages, list):
|
|
736
|
+
out["messages"] = list(messages)
|
|
737
|
+
elif isinstance(messages, tuple):
|
|
738
|
+
out["messages"] = list(messages)
|
|
739
|
+
elif messages is None:
|
|
740
|
+
out["messages"] = []
|
|
741
|
+
else:
|
|
742
|
+
out["messages"] = [messages]
|
|
743
|
+
|
|
744
|
+
# Optional nested context extras (kept for backwards compatibility).
|
|
745
|
+
context_extra = inputs.get("context_extra")
|
|
746
|
+
if isinstance(context_extra, dict):
|
|
747
|
+
out["context_extra"] = dict(context_extra)
|
|
748
|
+
|
|
749
|
+
node_traces = inputs.get("node_traces")
|
|
750
|
+
out["node_traces"] = dict(node_traces) if isinstance(node_traces, dict) else {}
|
|
751
|
+
|
|
752
|
+
steps = inputs.get("steps")
|
|
753
|
+
if isinstance(steps, list):
|
|
754
|
+
out["steps"] = list(steps)
|
|
755
|
+
elif isinstance(steps, tuple):
|
|
756
|
+
out["steps"] = list(steps)
|
|
757
|
+
elif steps is None:
|
|
758
|
+
out["steps"] = []
|
|
759
|
+
else:
|
|
760
|
+
out["steps"] = [steps]
|
|
761
|
+
|
|
762
|
+
tool_calls = inputs.get("tool_calls")
|
|
763
|
+
if isinstance(tool_calls, list):
|
|
764
|
+
out["tool_calls"] = list(tool_calls)
|
|
765
|
+
elif isinstance(tool_calls, tuple):
|
|
766
|
+
out["tool_calls"] = list(tool_calls)
|
|
767
|
+
elif tool_calls is None:
|
|
768
|
+
out["tool_calls"] = []
|
|
769
|
+
else:
|
|
770
|
+
out["tool_calls"] = [tool_calls]
|
|
771
|
+
|
|
772
|
+
tool_results = inputs.get("tool_results")
|
|
773
|
+
if isinstance(tool_results, list):
|
|
774
|
+
out["tool_results"] = list(tool_results)
|
|
775
|
+
elif isinstance(tool_results, tuple):
|
|
776
|
+
out["tool_results"] = list(tool_results)
|
|
777
|
+
elif tool_results is None:
|
|
778
|
+
out["tool_results"] = []
|
|
779
|
+
else:
|
|
780
|
+
out["tool_results"] = [tool_results]
|
|
781
|
+
|
|
782
|
+
return {"scratchpad": out}
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def data_get_element(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
786
|
+
"""Get an array element by index with an optional default.
|
|
787
|
+
|
|
788
|
+
- Supports list/tuple as arrays; other inputs are treated as empty.
|
|
789
|
+
- Index is coerced to int (floats truncated).
|
|
790
|
+
- Negative indices are supported (Python-style).
|
|
791
|
+
"""
|
|
792
|
+
arr_raw = inputs.get("array")
|
|
793
|
+
if isinstance(arr_raw, list):
|
|
794
|
+
arr = arr_raw
|
|
795
|
+
elif isinstance(arr_raw, tuple):
|
|
796
|
+
arr = list(arr_raw)
|
|
797
|
+
else:
|
|
798
|
+
arr = []
|
|
799
|
+
|
|
800
|
+
default = inputs.get("default")
|
|
801
|
+
raw_index = inputs.get("index", 0)
|
|
802
|
+
try:
|
|
803
|
+
if isinstance(raw_index, bool):
|
|
804
|
+
idx = 0
|
|
805
|
+
elif isinstance(raw_index, (int, float)):
|
|
806
|
+
idx = int(raw_index)
|
|
807
|
+
elif isinstance(raw_index, str) and raw_index.strip():
|
|
808
|
+
idx = int(float(raw_index.strip()))
|
|
809
|
+
else:
|
|
810
|
+
idx = 0
|
|
811
|
+
except Exception:
|
|
812
|
+
idx = 0
|
|
813
|
+
|
|
814
|
+
if idx < 0:
|
|
815
|
+
idx = len(arr) + idx
|
|
816
|
+
|
|
817
|
+
if 0 <= idx < len(arr):
|
|
818
|
+
return {"result": arr[idx], "found": True}
|
|
819
|
+
return {"result": default, "found": False}
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def data_get_random_element(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
823
|
+
"""Pick a random element from an array with an optional default."""
|
|
824
|
+
arr_raw = inputs.get("array")
|
|
825
|
+
if isinstance(arr_raw, list):
|
|
826
|
+
arr = arr_raw
|
|
827
|
+
elif isinstance(arr_raw, tuple):
|
|
828
|
+
arr = list(arr_raw)
|
|
829
|
+
else:
|
|
830
|
+
arr = []
|
|
831
|
+
|
|
832
|
+
default = inputs.get("default")
|
|
833
|
+
if not arr:
|
|
834
|
+
return {"result": default, "found": False}
|
|
835
|
+
|
|
836
|
+
# Use global RNG; tests only assert membership/non-empty, so this stays non-flaky.
|
|
837
|
+
return {"result": random.choice(arr), "found": True}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def data_array_map(inputs: Dict[str, Any]) -> List[Any]:
|
|
841
|
+
"""Map array items (extract property from each)."""
|
|
842
|
+
items = inputs.get("items", [])
|
|
843
|
+
key = str(inputs.get("key", ""))
|
|
844
|
+
|
|
845
|
+
result: list[Any] = []
|
|
846
|
+
for item in items:
|
|
847
|
+
if isinstance(item, dict):
|
|
848
|
+
result.append(item.get(key))
|
|
849
|
+
else:
|
|
850
|
+
result.append(item)
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def data_array_filter(inputs: Dict[str, Any]) -> List[Any]:
|
|
855
|
+
"""Filter array by condition."""
|
|
856
|
+
items = inputs.get("items", [])
|
|
857
|
+
key = str(inputs.get("key", ""))
|
|
858
|
+
value = inputs.get("value")
|
|
859
|
+
|
|
860
|
+
result: list[Any] = []
|
|
861
|
+
for item in items:
|
|
862
|
+
if isinstance(item, dict):
|
|
863
|
+
if item.get(key) == value:
|
|
864
|
+
result.append(item)
|
|
865
|
+
elif item == value:
|
|
866
|
+
result.append(item)
|
|
867
|
+
return result
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def data_array_length(inputs: Dict[str, Any]) -> int:
|
|
871
|
+
"""Return array length (0 if not an array)."""
|
|
872
|
+
items = inputs.get("array")
|
|
873
|
+
if isinstance(items, list):
|
|
874
|
+
return len(items)
|
|
875
|
+
if isinstance(items, tuple):
|
|
876
|
+
return len(list(items))
|
|
877
|
+
return 0
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def data_has_tools(inputs: Dict[str, Any]) -> bool:
|
|
881
|
+
"""Convenience: return True if the input array has at least one element.
|
|
882
|
+
|
|
883
|
+
Intended for checking LLM `tool_calls` arrays, but works with any array-like input.
|
|
884
|
+
Non-array / null inputs are treated as empty.
|
|
885
|
+
"""
|
|
886
|
+
items = inputs.get("array")
|
|
887
|
+
if isinstance(items, list):
|
|
888
|
+
return len(items) > 0
|
|
889
|
+
if isinstance(items, tuple):
|
|
890
|
+
return len(items) > 0
|
|
891
|
+
return False
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def data_array_append(inputs: Dict[str, Any]) -> List[Any]:
|
|
895
|
+
"""Append an item to an array (returns a new array)."""
|
|
896
|
+
items = inputs.get("array")
|
|
897
|
+
item = inputs.get("item")
|
|
898
|
+
out: list[Any]
|
|
899
|
+
if isinstance(items, list):
|
|
900
|
+
out = list(items)
|
|
901
|
+
elif isinstance(items, tuple):
|
|
902
|
+
out = list(items)
|
|
903
|
+
elif items is None:
|
|
904
|
+
out = []
|
|
905
|
+
else:
|
|
906
|
+
out = [items]
|
|
907
|
+
out.append(item)
|
|
908
|
+
return out
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def data_array_dedup(inputs: Dict[str, Any]) -> List[Any]:
|
|
912
|
+
"""Stable-order dedup for arrays.
|
|
913
|
+
|
|
914
|
+
If `key` is provided (string path), dedup objects by that path value.
|
|
915
|
+
"""
|
|
916
|
+
items = inputs.get("array")
|
|
917
|
+
if not isinstance(items, list):
|
|
918
|
+
if isinstance(items, tuple):
|
|
919
|
+
items = list(items)
|
|
920
|
+
else:
|
|
921
|
+
return []
|
|
922
|
+
|
|
923
|
+
key = inputs.get("key")
|
|
924
|
+
key_path = str(key or "").strip()
|
|
925
|
+
|
|
926
|
+
def _fingerprint(v: Any) -> str:
|
|
927
|
+
if v is None or isinstance(v, (bool, int, float, str)):
|
|
928
|
+
return f"{type(v).__name__}:{v}"
|
|
929
|
+
try:
|
|
930
|
+
return json.dumps(v, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
931
|
+
except Exception:
|
|
932
|
+
return str(v)
|
|
933
|
+
|
|
934
|
+
seen: set[str] = set()
|
|
935
|
+
out: list[Any] = []
|
|
936
|
+
for item in items:
|
|
937
|
+
if key_path:
|
|
938
|
+
k = _get_path(item, key_path)
|
|
939
|
+
fp = _fingerprint(k) if k is not None else _fingerprint(item)
|
|
940
|
+
else:
|
|
941
|
+
fp = _fingerprint(item)
|
|
942
|
+
if fp in seen:
|
|
943
|
+
continue
|
|
944
|
+
seen.add(fp)
|
|
945
|
+
out.append(item)
|
|
946
|
+
return out
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def system_datetime(_: Dict[str, Any]) -> Dict[str, Any]:
|
|
950
|
+
"""Return current system date/time and best-effort locale metadata.
|
|
951
|
+
|
|
952
|
+
All values are JSON-serializable and stable-keyed.
|
|
953
|
+
"""
|
|
954
|
+
now = datetime.now().astimezone()
|
|
955
|
+
offset = now.utcoffset()
|
|
956
|
+
offset_minutes = int(offset.total_seconds() // 60) if offset is not None else 0
|
|
957
|
+
|
|
958
|
+
tzname = now.tzname() or ""
|
|
959
|
+
|
|
960
|
+
# Avoid deprecated locale.getdefaultlocale() in Python 3.12+.
|
|
961
|
+
lang = os.environ.get("LC_ALL") or os.environ.get("LANG") or os.environ.get("LC_CTYPE") or ""
|
|
962
|
+
env_locale = lang.split(".", 1)[0] if lang else ""
|
|
963
|
+
|
|
964
|
+
loc = locale.getlocale()[0] or env_locale
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
"iso": now.isoformat(),
|
|
968
|
+
"timezone": tzname,
|
|
969
|
+
"utc_offset_minutes": offset_minutes,
|
|
970
|
+
"locale": loc or "",
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def data_parse_json(inputs: Dict[str, Any]) -> Any:
|
|
975
|
+
"""Parse JSON (or JSON-ish) text into a JSON-serializable Python value.
|
|
976
|
+
|
|
977
|
+
Primary use-case: turn an LLM string response into an object/array that can be
|
|
978
|
+
fed into `Break Object` (dynamic pins) or other data nodes.
|
|
979
|
+
|
|
980
|
+
Behavior:
|
|
981
|
+
- If the input is already a dict/list, returns it unchanged (idempotent).
|
|
982
|
+
- Tries strict `json.loads` first.
|
|
983
|
+
- If that fails, tries to extract the first JSON object/array substring and parse it.
|
|
984
|
+
- As a last resort, tries `ast.literal_eval` to handle Python-style dicts/lists
|
|
985
|
+
(common in LLM output), then converts to JSON-friendly types.
|
|
986
|
+
- If the parsed value is a scalar, wraps it as `{ "value": <scalar> }` by default,
|
|
987
|
+
so `Break Object` can still expose it.
|
|
988
|
+
"""
|
|
989
|
+
|
|
990
|
+
def _strip_code_fence(text: str) -> str:
|
|
991
|
+
s = text.strip()
|
|
992
|
+
if not s.startswith("```"):
|
|
993
|
+
return s
|
|
994
|
+
# Opening fence line can be ```json / ```js etc; drop it.
|
|
995
|
+
nl = s.find("\n")
|
|
996
|
+
if nl == -1:
|
|
997
|
+
return s.strip("`").strip()
|
|
998
|
+
body = s[nl + 1 :]
|
|
999
|
+
end = body.rfind("```")
|
|
1000
|
+
if end != -1:
|
|
1001
|
+
body = body[:end]
|
|
1002
|
+
return body.strip()
|
|
1003
|
+
|
|
1004
|
+
def _jsonify(value: Any) -> Any:
|
|
1005
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
1006
|
+
return value
|
|
1007
|
+
if isinstance(value, dict):
|
|
1008
|
+
return {str(k): _jsonify(v) for k, v in value.items()}
|
|
1009
|
+
if isinstance(value, list):
|
|
1010
|
+
return [_jsonify(v) for v in value]
|
|
1011
|
+
if isinstance(value, tuple):
|
|
1012
|
+
return [_jsonify(v) for v in value]
|
|
1013
|
+
return str(value)
|
|
1014
|
+
|
|
1015
|
+
raw = inputs.get("text")
|
|
1016
|
+
if isinstance(raw, (dict, list)):
|
|
1017
|
+
parsed: Any = raw
|
|
1018
|
+
else:
|
|
1019
|
+
if raw is None:
|
|
1020
|
+
raise ValueError("parse_json requires a non-empty 'text' input.")
|
|
1021
|
+
text = _strip_code_fence(str(raw))
|
|
1022
|
+
if not text.strip():
|
|
1023
|
+
raise ValueError("parse_json requires a non-empty 'text' input.")
|
|
1024
|
+
|
|
1025
|
+
parsed = None
|
|
1026
|
+
text_stripped = text.strip()
|
|
1027
|
+
|
|
1028
|
+
try:
|
|
1029
|
+
parsed = json.loads(text_stripped)
|
|
1030
|
+
except Exception:
|
|
1031
|
+
# Best-effort: find and parse the first JSON object/array substring.
|
|
1032
|
+
decoder = json.JSONDecoder()
|
|
1033
|
+
starts: list[int] = []
|
|
1034
|
+
for i, ch in enumerate(text_stripped):
|
|
1035
|
+
if ch in "{[":
|
|
1036
|
+
starts.append(i)
|
|
1037
|
+
if len(starts) >= 64:
|
|
1038
|
+
break
|
|
1039
|
+
for i in starts:
|
|
1040
|
+
try:
|
|
1041
|
+
parsed, _end = decoder.raw_decode(text_stripped[i:])
|
|
1042
|
+
break
|
|
1043
|
+
except Exception:
|
|
1044
|
+
continue
|
|
1045
|
+
|
|
1046
|
+
if parsed is None:
|
|
1047
|
+
# Last resort: tolerate Python-literal dict/list output.
|
|
1048
|
+
try:
|
|
1049
|
+
parsed = ast.literal_eval(text_stripped)
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
1052
|
+
|
|
1053
|
+
parsed = _jsonify(parsed)
|
|
1054
|
+
|
|
1055
|
+
wrap_scalar = bool(inputs.get("wrap_scalar", True))
|
|
1056
|
+
if wrap_scalar and not isinstance(parsed, (dict, list)):
|
|
1057
|
+
return {"value": parsed}
|
|
1058
|
+
return parsed
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def data_stringify_json(inputs: Dict[str, Any]) -> str:
|
|
1062
|
+
"""Render a JSON-like value into a string (runtime-owned implementation).
|
|
1063
|
+
|
|
1064
|
+
The core stringify logic lives in AbstractRuntime so multiple hosts can reuse it.
|
|
1065
|
+
|
|
1066
|
+
Supported inputs (backward compatible):
|
|
1067
|
+
- `value`: JSON value (dict/list/scalar) OR a JSON-ish string.
|
|
1068
|
+
- `mode`: none | beautify | minified
|
|
1069
|
+
- Legacy: `indent` (<=0 => minified; >0 => beautify with that indent)
|
|
1070
|
+
- Legacy: `sort_keys` (bool)
|
|
1071
|
+
"""
|
|
1072
|
+
|
|
1073
|
+
value = inputs.get("value")
|
|
1074
|
+
|
|
1075
|
+
raw_mode = inputs.get("mode")
|
|
1076
|
+
mode = str(raw_mode).strip().lower() if isinstance(raw_mode, str) else ""
|
|
1077
|
+
|
|
1078
|
+
raw_indent = inputs.get("indent")
|
|
1079
|
+
indent_n: Optional[int] = None
|
|
1080
|
+
if raw_indent is not None:
|
|
1081
|
+
try:
|
|
1082
|
+
indent_n = int(raw_indent)
|
|
1083
|
+
except Exception:
|
|
1084
|
+
indent_n = None
|
|
1085
|
+
|
|
1086
|
+
raw_sort_keys = inputs.get("sort_keys")
|
|
1087
|
+
sort_keys = bool(raw_sort_keys) if isinstance(raw_sort_keys, bool) else False
|
|
1088
|
+
|
|
1089
|
+
# If mode not provided, infer from legacy indent.
|
|
1090
|
+
if not mode:
|
|
1091
|
+
if indent_n is not None and indent_n <= 0:
|
|
1092
|
+
mode = "minified"
|
|
1093
|
+
elif indent_n is not None and indent_n > 0:
|
|
1094
|
+
mode = "beautify"
|
|
1095
|
+
else:
|
|
1096
|
+
mode = "beautify"
|
|
1097
|
+
|
|
1098
|
+
return runtime_stringify_json(
|
|
1099
|
+
value,
|
|
1100
|
+
mode=mode,
|
|
1101
|
+
beautify_indent=indent_n if isinstance(indent_n, int) and indent_n > 0 else 2,
|
|
1102
|
+
sort_keys=sort_keys,
|
|
1103
|
+
parse_strings=True,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def data_agent_trace_report(inputs: Dict[str, Any]) -> str:
|
|
1108
|
+
"""Render an agent scratchpad (runtime-owned node traces) into Markdown."""
|
|
1109
|
+
scratchpad = inputs.get("scratchpad")
|
|
1110
|
+
return runtime_render_agent_trace_markdown(scratchpad)
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def data_format_tool_results(inputs: Dict[str, Any]) -> str:
|
|
1114
|
+
"""Render Tool Calls `results` into a condensed, human-readable digest string.
|
|
1115
|
+
|
|
1116
|
+
Expected input shape (Tool Calls node output):
|
|
1117
|
+
results: [{call_id, name, success, output, error}, ...]
|
|
1118
|
+
|
|
1119
|
+
The Tool Calls node does not currently include the original arguments, so this
|
|
1120
|
+
formatter best-effort derives a “primary” argument from:
|
|
1121
|
+
- `result.arguments` (when present)
|
|
1122
|
+
- the tool output object (e.g. execute_command.output.command)
|
|
1123
|
+
|
|
1124
|
+
When available, tool specs (AbstractCore ToolDefinitions) are used to pick a
|
|
1125
|
+
stable primary key without hardcoding tool parameter names.
|
|
1126
|
+
"""
|
|
1127
|
+
|
|
1128
|
+
raw = inputs.get("tool_results")
|
|
1129
|
+
if raw is None:
|
|
1130
|
+
raw = inputs.get("results")
|
|
1131
|
+
|
|
1132
|
+
items: list[Any]
|
|
1133
|
+
if raw is None:
|
|
1134
|
+
items = []
|
|
1135
|
+
elif isinstance(raw, list):
|
|
1136
|
+
items = raw
|
|
1137
|
+
else:
|
|
1138
|
+
items = [raw]
|
|
1139
|
+
|
|
1140
|
+
tool_specs_by_name: Dict[str, Dict[str, Any]] = {}
|
|
1141
|
+
try:
|
|
1142
|
+
from abstractruntime.integrations.abstractcore.default_tools import list_default_tool_specs
|
|
1143
|
+
|
|
1144
|
+
for s in list_default_tool_specs():
|
|
1145
|
+
if not isinstance(s, dict):
|
|
1146
|
+
continue
|
|
1147
|
+
name = s.get("name")
|
|
1148
|
+
if isinstance(name, str) and name.strip():
|
|
1149
|
+
tool_specs_by_name[name.strip()] = dict(s)
|
|
1150
|
+
except Exception:
|
|
1151
|
+
tool_specs_by_name = {}
|
|
1152
|
+
|
|
1153
|
+
def _clamp(text: str, max_len: int) -> str:
|
|
1154
|
+
s = str(text or "").strip()
|
|
1155
|
+
if len(s) <= max_len:
|
|
1156
|
+
return s
|
|
1157
|
+
return f"{s[: max(0, max_len - 1)]}…"
|
|
1158
|
+
|
|
1159
|
+
def _one_line(text: str) -> str:
|
|
1160
|
+
s = str(text or "").replace("\r\n", "\n").replace("\r", "\n")
|
|
1161
|
+
s = " ".join([p for p in s.split("\n") if p.strip()])
|
|
1162
|
+
return s.strip()
|
|
1163
|
+
|
|
1164
|
+
def _format_value(value: Any, max_len: int = 160) -> str:
|
|
1165
|
+
if value is None:
|
|
1166
|
+
return ""
|
|
1167
|
+
if isinstance(value, bool):
|
|
1168
|
+
return "true" if value else "false"
|
|
1169
|
+
if isinstance(value, (int, float)):
|
|
1170
|
+
return str(value)
|
|
1171
|
+
if isinstance(value, str):
|
|
1172
|
+
return _clamp(_one_line(value), max_len=max_len)
|
|
1173
|
+
try:
|
|
1174
|
+
return _clamp(_one_line(json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)), max_len=max_len)
|
|
1175
|
+
except Exception:
|
|
1176
|
+
return _clamp(_one_line(str(value)), max_len=max_len)
|
|
1177
|
+
|
|
1178
|
+
def _primary_key_order(name: str, args_obj: Dict[str, Any], out_obj: Dict[str, Any]) -> list[str]:
|
|
1179
|
+
spec = tool_specs_by_name.get(str(name or "").strip()) or {}
|
|
1180
|
+
order: list[str] = []
|
|
1181
|
+
required = spec.get("required_args")
|
|
1182
|
+
if isinstance(required, list):
|
|
1183
|
+
for k in required:
|
|
1184
|
+
if isinstance(k, str) and k.strip():
|
|
1185
|
+
order.append(k.strip())
|
|
1186
|
+
if not order:
|
|
1187
|
+
params = spec.get("parameters")
|
|
1188
|
+
if isinstance(params, dict):
|
|
1189
|
+
for k in params.keys():
|
|
1190
|
+
if isinstance(k, str) and k.strip():
|
|
1191
|
+
order.append(k.strip())
|
|
1192
|
+
if not order:
|
|
1193
|
+
for k in list(args_obj.keys()) + list(out_obj.keys()):
|
|
1194
|
+
if isinstance(k, str) and k.strip() and k not in order:
|
|
1195
|
+
order.append(k.strip())
|
|
1196
|
+
return order
|
|
1197
|
+
|
|
1198
|
+
def _signature(name: str, args_obj: Dict[str, Any], out_obj: Dict[str, Any]) -> str:
|
|
1199
|
+
tool = str(name or "").strip() or "tool"
|
|
1200
|
+
order = _primary_key_order(tool, args_obj, out_obj)
|
|
1201
|
+
primary_val: Any = None
|
|
1202
|
+
for k in order:
|
|
1203
|
+
if k in args_obj and args_obj.get(k) is not None:
|
|
1204
|
+
primary_val = args_obj.get(k)
|
|
1205
|
+
break
|
|
1206
|
+
if primary_val is None:
|
|
1207
|
+
for k in order:
|
|
1208
|
+
if k in out_obj and out_obj.get(k) is not None:
|
|
1209
|
+
primary_val = out_obj.get(k)
|
|
1210
|
+
break
|
|
1211
|
+
if primary_val is None:
|
|
1212
|
+
return f"{tool}()"
|
|
1213
|
+
return f"{tool}({_format_value(primary_val)})"
|
|
1214
|
+
|
|
1215
|
+
lines: list[str] = []
|
|
1216
|
+
for it in items:
|
|
1217
|
+
if not isinstance(it, dict):
|
|
1218
|
+
continue
|
|
1219
|
+
name = str(it.get("name") or it.get("tool") or "").strip() or "tool"
|
|
1220
|
+
success_raw = it.get("success")
|
|
1221
|
+
success = success_raw if isinstance(success_raw, bool) else None
|
|
1222
|
+
error_raw = it.get("error")
|
|
1223
|
+
error = str(error_raw).strip() if isinstance(error_raw, str) else (str(error_raw).strip() if error_raw is not None else "")
|
|
1224
|
+
output = it.get("output")
|
|
1225
|
+
args_obj = it.get("arguments") if isinstance(it.get("arguments"), dict) else {}
|
|
1226
|
+
out_obj = output if isinstance(output, dict) else {}
|
|
1227
|
+
|
|
1228
|
+
sig = _signature(name, args_obj, out_obj)
|
|
1229
|
+
status = "SUCCESS" if success is True else "FAILURE" if success is False else "DONE"
|
|
1230
|
+
|
|
1231
|
+
if success is False and error:
|
|
1232
|
+
obs = error
|
|
1233
|
+
else:
|
|
1234
|
+
if isinstance(output, dict):
|
|
1235
|
+
rendered = output.get("rendered")
|
|
1236
|
+
if isinstance(rendered, str) and rendered.strip():
|
|
1237
|
+
obs = rendered.strip()
|
|
1238
|
+
else:
|
|
1239
|
+
obs = _format_value(output, max_len=420)
|
|
1240
|
+
elif isinstance(output, str):
|
|
1241
|
+
obs = _clamp(_one_line(output), max_len=420)
|
|
1242
|
+
elif output is None:
|
|
1243
|
+
obs = "(no output)"
|
|
1244
|
+
else:
|
|
1245
|
+
obs = _clamp(_one_line(str(output)), max_len=420)
|
|
1246
|
+
|
|
1247
|
+
if not obs.strip():
|
|
1248
|
+
obs = "(no output)"
|
|
1249
|
+
|
|
1250
|
+
lines.append(f"- {sig} [{status}] : {obs}")
|
|
1251
|
+
|
|
1252
|
+
return "\n".join(lines).strip()
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
# Literal value handlers - return configured constant values
|
|
1256
|
+
def literal_string(inputs: Dict[str, Any]) -> str:
|
|
1257
|
+
"""Return string literal value."""
|
|
1258
|
+
return str(inputs.get("_literalValue", ""))
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def literal_number(inputs: Dict[str, Any]) -> float:
|
|
1262
|
+
"""Return number literal value."""
|
|
1263
|
+
value = inputs.get("_literalValue", 0)
|
|
1264
|
+
try:
|
|
1265
|
+
return float(value)
|
|
1266
|
+
except (TypeError, ValueError):
|
|
1267
|
+
return 0.0
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def literal_boolean(inputs: Dict[str, Any]) -> bool:
|
|
1271
|
+
"""Return boolean literal value."""
|
|
1272
|
+
return bool(inputs.get("_literalValue", False))
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def literal_json(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
1276
|
+
"""Return JSON literal value."""
|
|
1277
|
+
value = inputs.get("_literalValue", {})
|
|
1278
|
+
if isinstance(value, (dict, list)):
|
|
1279
|
+
return value # type: ignore[return-value]
|
|
1280
|
+
return {}
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def literal_array(inputs: Dict[str, Any]) -> List[Any]:
|
|
1284
|
+
"""Return array literal value."""
|
|
1285
|
+
value = inputs.get("_literalValue", [])
|
|
1286
|
+
if isinstance(value, list):
|
|
1287
|
+
return value
|
|
1288
|
+
return []
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def tools_allowlist(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
1292
|
+
"""Return a workflow-scope tool allowlist as a named output.
|
|
1293
|
+
|
|
1294
|
+
The visual editor stores the selected tools as a JSON array of strings in the
|
|
1295
|
+
node's `literalValue`. The executor injects it as `_literalValue`.
|
|
1296
|
+
"""
|
|
1297
|
+
value = inputs.get("_literalValue", [])
|
|
1298
|
+
if not isinstance(value, list):
|
|
1299
|
+
return {"tools": []}
|
|
1300
|
+
out: list[str] = []
|
|
1301
|
+
for x in value:
|
|
1302
|
+
if isinstance(x, str) and x.strip():
|
|
1303
|
+
out.append(x.strip())
|
|
1304
|
+
# Preserve order; remove duplicates.
|
|
1305
|
+
seen: set[str] = set()
|
|
1306
|
+
uniq: list[str] = []
|
|
1307
|
+
for t in out:
|
|
1308
|
+
if t in seen:
|
|
1309
|
+
continue
|
|
1310
|
+
seen.add(t)
|
|
1311
|
+
uniq.append(t)
|
|
1312
|
+
return {"tools": uniq}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
# Handler registry
|
|
1316
|
+
BUILTIN_HANDLERS: Dict[str, Callable[[Dict[str, Any]], Any]] = {
|
|
1317
|
+
# Math
|
|
1318
|
+
"add": math_add,
|
|
1319
|
+
"subtract": math_subtract,
|
|
1320
|
+
"multiply": math_multiply,
|
|
1321
|
+
"divide": math_divide,
|
|
1322
|
+
"modulo": math_modulo,
|
|
1323
|
+
"power": math_power,
|
|
1324
|
+
"abs": math_abs,
|
|
1325
|
+
"round": math_round,
|
|
1326
|
+
"random_int": math_random_int,
|
|
1327
|
+
"random_float": math_random_float,
|
|
1328
|
+
# String
|
|
1329
|
+
"concat": string_concat,
|
|
1330
|
+
"split": string_split,
|
|
1331
|
+
"join": string_join,
|
|
1332
|
+
"format": string_format,
|
|
1333
|
+
"string_template": string_template,
|
|
1334
|
+
"uppercase": string_uppercase,
|
|
1335
|
+
"lowercase": string_lowercase,
|
|
1336
|
+
"trim": string_trim,
|
|
1337
|
+
"substring": string_substring,
|
|
1338
|
+
"length": string_length,
|
|
1339
|
+
"contains": string_contains,
|
|
1340
|
+
"replace": string_replace,
|
|
1341
|
+
# Control
|
|
1342
|
+
"compare": control_compare,
|
|
1343
|
+
"not": control_not,
|
|
1344
|
+
"and": control_and,
|
|
1345
|
+
"or": control_or,
|
|
1346
|
+
"coalesce": control_coalesce,
|
|
1347
|
+
# Data
|
|
1348
|
+
"get": data_get,
|
|
1349
|
+
"set": data_set,
|
|
1350
|
+
"merge": data_merge,
|
|
1351
|
+
"make_object": data_make_object,
|
|
1352
|
+
"make_context": data_make_context,
|
|
1353
|
+
"add_message": data_add_message,
|
|
1354
|
+
"make_meta": data_make_meta,
|
|
1355
|
+
"make_scratchpad": data_make_scratchpad,
|
|
1356
|
+
"get_element": data_get_element,
|
|
1357
|
+
"get_random_element": data_get_random_element,
|
|
1358
|
+
"array_map": data_array_map,
|
|
1359
|
+
"array_filter": data_array_filter,
|
|
1360
|
+
"array_length": data_array_length,
|
|
1361
|
+
"has_tools": data_has_tools,
|
|
1362
|
+
"array_append": data_array_append,
|
|
1363
|
+
"array_dedup": data_array_dedup,
|
|
1364
|
+
"parse_json": data_parse_json,
|
|
1365
|
+
"stringify_json": data_stringify_json,
|
|
1366
|
+
"format_tool_results": data_format_tool_results,
|
|
1367
|
+
"agent_trace_report": data_agent_trace_report,
|
|
1368
|
+
"system_datetime": system_datetime,
|
|
1369
|
+
# Literals
|
|
1370
|
+
"literal_string": literal_string,
|
|
1371
|
+
"literal_number": literal_number,
|
|
1372
|
+
"literal_boolean": literal_boolean,
|
|
1373
|
+
"literal_json": literal_json,
|
|
1374
|
+
"literal_array": literal_array,
|
|
1375
|
+
"tools_allowlist": tools_allowlist,
|
|
1376
|
+
}
|