abstractflow 0.3.0__py3-none-any.whl → 0.3.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.
- abstractflow/__init__.py +2 -2
- abstractflow/adapters/agent_adapter.py +2 -121
- abstractflow/adapters/control_adapter.py +2 -612
- abstractflow/adapters/effect_adapter.py +2 -642
- abstractflow/adapters/event_adapter.py +2 -304
- abstractflow/adapters/function_adapter.py +2 -94
- abstractflow/adapters/subflow_adapter.py +2 -71
- abstractflow/adapters/variable_adapter.py +2 -314
- abstractflow/cli.py +73 -28
- abstractflow/compiler.py +18 -2022
- abstractflow/core/flow.py +4 -240
- abstractflow/runner.py +59 -5
- abstractflow/visual/agent_ids.py +2 -26
- abstractflow/visual/builtins.py +2 -786
- abstractflow/visual/code_executor.py +2 -211
- abstractflow/visual/executor.py +319 -2140
- abstractflow/visual/interfaces.py +103 -10
- abstractflow/visual/models.py +26 -1
- abstractflow/visual/session_runner.py +23 -9
- abstractflow/visual/workspace_scoped_tools.py +11 -243
- abstractflow/workflow_bundle.py +290 -0
- abstractflow-0.3.1.dist-info/METADATA +186 -0
- abstractflow-0.3.1.dist-info/RECORD +33 -0
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/WHEEL +1 -1
- abstractflow-0.3.0.dist-info/METADATA +0 -413
- abstractflow-0.3.0.dist-info/RECORD +0 -32
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/entry_points.txt +0 -0
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -1,645 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
This adapter creates node handlers that produce AbstractRuntime Effects,
|
|
4
|
-
enabling visual flows to pause and wait for external input (user prompts,
|
|
5
|
-
events, delays, etc.).
|
|
6
|
-
"""
|
|
1
|
+
"""Re-export: AbstractRuntime VisualFlow compiler adapter."""
|
|
7
2
|
|
|
8
3
|
from __future__ import annotations
|
|
9
4
|
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from abstractruntime.core.models import RunState, StepPlan
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def create_ask_user_handler(
|
|
17
|
-
node_id: str,
|
|
18
|
-
next_node: Optional[str],
|
|
19
|
-
input_key: Optional[str] = None,
|
|
20
|
-
output_key: Optional[str] = None,
|
|
21
|
-
allow_free_text: bool = True,
|
|
22
|
-
) -> Callable:
|
|
23
|
-
"""Create a node handler that asks the user for input.
|
|
24
|
-
|
|
25
|
-
This handler produces an ASK_USER effect that pauses the flow
|
|
26
|
-
until the user provides a response.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
node_id: Unique identifier for this node
|
|
30
|
-
next_node: ID of the next node to transition to after response
|
|
31
|
-
input_key: Key in run.vars to read prompt/choices from
|
|
32
|
-
output_key: Key in run.vars to write the response to
|
|
33
|
-
allow_free_text: Whether to allow free text response
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
A node handler that produces ASK_USER effect
|
|
37
|
-
"""
|
|
38
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
39
|
-
|
|
40
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
41
|
-
"""Ask user and wait for response."""
|
|
42
|
-
# Get input from vars
|
|
43
|
-
if input_key:
|
|
44
|
-
input_data = run.vars.get(input_key, {})
|
|
45
|
-
else:
|
|
46
|
-
input_data = run.vars
|
|
47
|
-
|
|
48
|
-
# Extract prompt and choices
|
|
49
|
-
if isinstance(input_data, dict):
|
|
50
|
-
prompt = input_data.get("prompt", "Please respond:")
|
|
51
|
-
choices = input_data.get("choices", [])
|
|
52
|
-
else:
|
|
53
|
-
prompt = str(input_data) if input_data else "Please respond:"
|
|
54
|
-
choices = []
|
|
55
|
-
|
|
56
|
-
# Ensure choices is a list
|
|
57
|
-
if not isinstance(choices, list):
|
|
58
|
-
choices = []
|
|
59
|
-
|
|
60
|
-
# Create the effect
|
|
61
|
-
effect = Effect(
|
|
62
|
-
type=EffectType.ASK_USER,
|
|
63
|
-
payload={
|
|
64
|
-
"prompt": prompt,
|
|
65
|
-
"choices": choices,
|
|
66
|
-
"allow_free_text": allow_free_text,
|
|
67
|
-
},
|
|
68
|
-
result_key=output_key or "_temp.user_response",
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
return StepPlan(
|
|
72
|
-
node_id=node_id,
|
|
73
|
-
effect=effect,
|
|
74
|
-
next_node=next_node,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return handler
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def create_answer_user_handler(
|
|
81
|
-
node_id: str,
|
|
82
|
-
next_node: Optional[str],
|
|
83
|
-
input_key: Optional[str] = None,
|
|
84
|
-
output_key: Optional[str] = None,
|
|
85
|
-
) -> Callable:
|
|
86
|
-
"""Create a node handler that requests the host UI to display a message.
|
|
87
|
-
|
|
88
|
-
This handler produces an ANSWER_USER effect that completes immediately.
|
|
89
|
-
"""
|
|
90
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
91
|
-
|
|
92
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
93
|
-
if input_key:
|
|
94
|
-
input_data = run.vars.get(input_key, {})
|
|
95
|
-
else:
|
|
96
|
-
input_data = run.vars
|
|
97
|
-
|
|
98
|
-
if isinstance(input_data, dict):
|
|
99
|
-
message = input_data.get("message") or input_data.get("text") or ""
|
|
100
|
-
else:
|
|
101
|
-
message = str(input_data) if input_data is not None else ""
|
|
102
|
-
|
|
103
|
-
effect = Effect(
|
|
104
|
-
type=EffectType.ANSWER_USER,
|
|
105
|
-
payload={"message": str(message)},
|
|
106
|
-
result_key=output_key or "_temp.answer_user",
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
return StepPlan(
|
|
110
|
-
node_id=node_id,
|
|
111
|
-
effect=effect,
|
|
112
|
-
next_node=next_node,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
return handler
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def create_wait_until_handler(
|
|
119
|
-
node_id: str,
|
|
120
|
-
next_node: Optional[str],
|
|
121
|
-
input_key: Optional[str] = None,
|
|
122
|
-
output_key: Optional[str] = None,
|
|
123
|
-
duration_type: str = "seconds",
|
|
124
|
-
) -> Callable:
|
|
125
|
-
"""Create a node handler that waits until a specified time.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
node_id: Unique identifier for this node
|
|
129
|
-
next_node: ID of the next node to transition to after waiting
|
|
130
|
-
input_key: Key in run.vars to read duration from
|
|
131
|
-
output_key: Key in run.vars to write the completion info to
|
|
132
|
-
duration_type: How to interpret duration (seconds/minutes/hours/timestamp)
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
A node handler that produces WAIT_UNTIL effect
|
|
136
|
-
"""
|
|
137
|
-
from datetime import datetime, timedelta, timezone
|
|
138
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
139
|
-
|
|
140
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
141
|
-
"""Wait until time and then continue."""
|
|
142
|
-
# Get input from vars
|
|
143
|
-
if input_key:
|
|
144
|
-
input_data = run.vars.get(input_key, {})
|
|
145
|
-
else:
|
|
146
|
-
input_data = run.vars
|
|
147
|
-
|
|
148
|
-
# Extract duration
|
|
149
|
-
if isinstance(input_data, dict):
|
|
150
|
-
duration = input_data.get("duration", 0)
|
|
151
|
-
else:
|
|
152
|
-
duration = input_data
|
|
153
|
-
|
|
154
|
-
# Convert to seconds
|
|
155
|
-
try:
|
|
156
|
-
amount = float(duration) if duration else 0
|
|
157
|
-
except (TypeError, ValueError):
|
|
158
|
-
amount = 0
|
|
159
|
-
|
|
160
|
-
# Calculate target time
|
|
161
|
-
now = datetime.now(timezone.utc)
|
|
162
|
-
|
|
163
|
-
if duration_type == "timestamp":
|
|
164
|
-
# Already an ISO timestamp
|
|
165
|
-
until = str(duration)
|
|
166
|
-
elif duration_type == "minutes":
|
|
167
|
-
until = (now + timedelta(minutes=amount)).isoformat()
|
|
168
|
-
elif duration_type == "hours":
|
|
169
|
-
until = (now + timedelta(hours=amount)).isoformat()
|
|
170
|
-
else: # seconds
|
|
171
|
-
until = (now + timedelta(seconds=amount)).isoformat()
|
|
172
|
-
|
|
173
|
-
# Create the effect
|
|
174
|
-
effect = Effect(
|
|
175
|
-
type=EffectType.WAIT_UNTIL,
|
|
176
|
-
payload={"until": until},
|
|
177
|
-
result_key=output_key or "_temp.wait_result",
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
return StepPlan(
|
|
181
|
-
node_id=node_id,
|
|
182
|
-
effect=effect,
|
|
183
|
-
next_node=next_node,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
return handler
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def create_wait_event_handler(
|
|
190
|
-
node_id: str,
|
|
191
|
-
next_node: Optional[str],
|
|
192
|
-
input_key: Optional[str] = None,
|
|
193
|
-
output_key: Optional[str] = None,
|
|
194
|
-
) -> Callable:
|
|
195
|
-
"""Create a node handler that waits for an external event.
|
|
196
|
-
|
|
197
|
-
Args:
|
|
198
|
-
node_id: Unique identifier for this node
|
|
199
|
-
next_node: ID of the next node to transition to after event
|
|
200
|
-
input_key: Key in run.vars to read event_key from
|
|
201
|
-
output_key: Key in run.vars to write the event data to
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
A node handler that produces WAIT_EVENT effect
|
|
205
|
-
"""
|
|
206
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
207
|
-
|
|
208
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
209
|
-
"""Wait for event and then continue."""
|
|
210
|
-
# Get input from vars
|
|
211
|
-
if input_key:
|
|
212
|
-
input_data = run.vars.get(input_key, {})
|
|
213
|
-
else:
|
|
214
|
-
input_data = run.vars
|
|
215
|
-
|
|
216
|
-
# Extract event key + optional host UX fields (prompt/choices).
|
|
217
|
-
if isinstance(input_data, dict):
|
|
218
|
-
event_key = input_data.get("event_key")
|
|
219
|
-
if event_key is None:
|
|
220
|
-
event_key = input_data.get("wait_key")
|
|
221
|
-
if not event_key:
|
|
222
|
-
event_key = "default"
|
|
223
|
-
prompt = input_data.get("prompt")
|
|
224
|
-
choices = input_data.get("choices")
|
|
225
|
-
allow_free_text = input_data.get("allow_free_text")
|
|
226
|
-
if allow_free_text is None:
|
|
227
|
-
allow_free_text = input_data.get("allowFreeText")
|
|
228
|
-
else:
|
|
229
|
-
event_key = str(input_data) if input_data else "default"
|
|
230
|
-
prompt = None
|
|
231
|
-
choices = None
|
|
232
|
-
allow_free_text = None
|
|
233
|
-
|
|
234
|
-
# Create the effect
|
|
235
|
-
effect = Effect(
|
|
236
|
-
type=EffectType.WAIT_EVENT,
|
|
237
|
-
payload={
|
|
238
|
-
"wait_key": str(event_key),
|
|
239
|
-
**({"prompt": prompt} if isinstance(prompt, str) and prompt.strip() else {}),
|
|
240
|
-
**({"choices": choices} if isinstance(choices, list) else {}),
|
|
241
|
-
**({"allow_free_text": bool(allow_free_text)} if allow_free_text is not None else {}),
|
|
242
|
-
},
|
|
243
|
-
result_key=output_key or "_temp.event_data",
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
return StepPlan(
|
|
247
|
-
node_id=node_id,
|
|
248
|
-
effect=effect,
|
|
249
|
-
next_node=next_node,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
return handler
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def create_memory_note_handler(
|
|
256
|
-
node_id: str,
|
|
257
|
-
next_node: Optional[str],
|
|
258
|
-
input_key: Optional[str] = None,
|
|
259
|
-
output_key: Optional[str] = None,
|
|
260
|
-
) -> Callable:
|
|
261
|
-
"""Create a node handler that stores a memory note.
|
|
262
|
-
|
|
263
|
-
Args:
|
|
264
|
-
node_id: Unique identifier for this node
|
|
265
|
-
next_node: ID of the next node to transition to after storing
|
|
266
|
-
input_key: Key in run.vars to read note content from
|
|
267
|
-
output_key: Key in run.vars to write the note_id to
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
A node handler that produces MEMORY_NOTE effect
|
|
271
|
-
"""
|
|
272
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
273
|
-
|
|
274
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
275
|
-
"""Store memory note and continue."""
|
|
276
|
-
# Get input from vars
|
|
277
|
-
if input_key:
|
|
278
|
-
input_data = run.vars.get(input_key, {})
|
|
279
|
-
else:
|
|
280
|
-
input_data = run.vars
|
|
281
|
-
|
|
282
|
-
# Extract content
|
|
283
|
-
if isinstance(input_data, dict):
|
|
284
|
-
content = input_data.get("content", "")
|
|
285
|
-
tags = input_data.get("tags") if isinstance(input_data.get("tags"), dict) else {}
|
|
286
|
-
sources = input_data.get("sources") if isinstance(input_data.get("sources"), dict) else None
|
|
287
|
-
scope = input_data.get("scope") if isinstance(input_data.get("scope"), str) else None
|
|
288
|
-
else:
|
|
289
|
-
content = str(input_data) if input_data else ""
|
|
290
|
-
tags = {}
|
|
291
|
-
sources = None
|
|
292
|
-
scope = None
|
|
293
|
-
|
|
294
|
-
# Create the effect
|
|
295
|
-
payload: Dict[str, Any] = {"note": content, "tags": tags}
|
|
296
|
-
if sources is not None:
|
|
297
|
-
payload["sources"] = sources
|
|
298
|
-
if scope:
|
|
299
|
-
payload["scope"] = scope
|
|
300
|
-
|
|
301
|
-
effect = Effect(
|
|
302
|
-
type=EffectType.MEMORY_NOTE,
|
|
303
|
-
payload=payload,
|
|
304
|
-
result_key=output_key or "_temp.note_id",
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
return StepPlan(
|
|
308
|
-
node_id=node_id,
|
|
309
|
-
effect=effect,
|
|
310
|
-
next_node=next_node,
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
return handler
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def create_memory_query_handler(
|
|
317
|
-
node_id: str,
|
|
318
|
-
next_node: Optional[str],
|
|
319
|
-
input_key: Optional[str] = None,
|
|
320
|
-
output_key: Optional[str] = None,
|
|
321
|
-
) -> Callable:
|
|
322
|
-
"""Create a node handler that queries memory.
|
|
323
|
-
|
|
324
|
-
Args:
|
|
325
|
-
node_id: Unique identifier for this node
|
|
326
|
-
next_node: ID of the next node to transition to after query
|
|
327
|
-
input_key: Key in run.vars to read query from
|
|
328
|
-
output_key: Key in run.vars to write results to
|
|
329
|
-
|
|
330
|
-
Returns:
|
|
331
|
-
A node handler that produces MEMORY_QUERY effect
|
|
332
|
-
"""
|
|
333
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
334
|
-
|
|
335
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
336
|
-
"""Query memory and continue."""
|
|
337
|
-
# Get input from vars
|
|
338
|
-
if input_key:
|
|
339
|
-
input_data = run.vars.get(input_key, {})
|
|
340
|
-
else:
|
|
341
|
-
input_data = run.vars
|
|
342
|
-
|
|
343
|
-
# Extract query params
|
|
344
|
-
if isinstance(input_data, dict):
|
|
345
|
-
query = input_data.get("query", "")
|
|
346
|
-
limit = input_data.get("limit", 10)
|
|
347
|
-
tags = input_data.get("tags") if isinstance(input_data.get("tags"), dict) else None
|
|
348
|
-
since = input_data.get("since")
|
|
349
|
-
until = input_data.get("until")
|
|
350
|
-
scope = input_data.get("scope") if isinstance(input_data.get("scope"), str) else None
|
|
351
|
-
else:
|
|
352
|
-
query = str(input_data) if input_data else ""
|
|
353
|
-
limit = 10
|
|
354
|
-
tags = None
|
|
355
|
-
since = None
|
|
356
|
-
until = None
|
|
357
|
-
scope = None
|
|
358
|
-
|
|
359
|
-
# Create the effect
|
|
360
|
-
payload: Dict[str, Any] = {"query": query, "limit_spans": limit, "return": "both"}
|
|
361
|
-
if tags is not None:
|
|
362
|
-
payload["tags"] = tags
|
|
363
|
-
if since is not None:
|
|
364
|
-
payload["since"] = since
|
|
365
|
-
if until is not None:
|
|
366
|
-
payload["until"] = until
|
|
367
|
-
if scope:
|
|
368
|
-
payload["scope"] = scope
|
|
369
|
-
|
|
370
|
-
effect = Effect(
|
|
371
|
-
type=EffectType.MEMORY_QUERY,
|
|
372
|
-
payload=payload,
|
|
373
|
-
result_key=output_key or "_temp.memory_results",
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
return StepPlan(
|
|
377
|
-
node_id=node_id,
|
|
378
|
-
effect=effect,
|
|
379
|
-
next_node=next_node,
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
return handler
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def create_memory_rehydrate_handler(
|
|
386
|
-
node_id: str,
|
|
387
|
-
next_node: Optional[str],
|
|
388
|
-
input_key: Optional[str] = None,
|
|
389
|
-
output_key: Optional[str] = None,
|
|
390
|
-
) -> Callable:
|
|
391
|
-
"""Create a node handler that rehydrates recalled spans into context.messages.
|
|
392
|
-
|
|
393
|
-
This produces a runtime-owned `EffectType.MEMORY_REHYDRATE` so rehydration is durable and host-agnostic.
|
|
394
|
-
"""
|
|
395
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
396
|
-
|
|
397
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
398
|
-
del ctx
|
|
399
|
-
if input_key:
|
|
400
|
-
input_data = run.vars.get(input_key, {})
|
|
401
|
-
else:
|
|
402
|
-
input_data = run.vars
|
|
403
|
-
|
|
404
|
-
span_ids = []
|
|
405
|
-
placement = "after_summary"
|
|
406
|
-
max_messages = None
|
|
407
|
-
if isinstance(input_data, dict):
|
|
408
|
-
raw = input_data.get("span_ids")
|
|
409
|
-
if raw is None:
|
|
410
|
-
raw = input_data.get("span_id")
|
|
411
|
-
if isinstance(raw, list):
|
|
412
|
-
span_ids = list(raw)
|
|
413
|
-
elif raw is not None:
|
|
414
|
-
span_ids = [raw]
|
|
415
|
-
if isinstance(input_data.get("placement"), str):
|
|
416
|
-
placement = str(input_data.get("placement") or "").strip() or placement
|
|
417
|
-
if input_data.get("max_messages") is not None:
|
|
418
|
-
max_messages = input_data.get("max_messages")
|
|
419
|
-
|
|
420
|
-
payload: Dict[str, Any] = {"span_ids": span_ids, "placement": placement}
|
|
421
|
-
if max_messages is not None:
|
|
422
|
-
payload["max_messages"] = max_messages
|
|
423
|
-
|
|
424
|
-
return StepPlan(
|
|
425
|
-
node_id=node_id,
|
|
426
|
-
effect=Effect(
|
|
427
|
-
type=EffectType.MEMORY_REHYDRATE,
|
|
428
|
-
payload=payload,
|
|
429
|
-
result_key=output_key or "_temp.memory_rehydrate",
|
|
430
|
-
),
|
|
431
|
-
next_node=next_node,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
return handler
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def create_llm_call_handler(
|
|
438
|
-
node_id: str,
|
|
439
|
-
next_node: Optional[str],
|
|
440
|
-
input_key: Optional[str] = None,
|
|
441
|
-
output_key: Optional[str] = None,
|
|
442
|
-
provider: Optional[str] = None,
|
|
443
|
-
model: Optional[str] = None,
|
|
444
|
-
temperature: float = 0.7,
|
|
445
|
-
) -> Callable:
|
|
446
|
-
"""Create a node handler that makes an LLM call.
|
|
447
|
-
|
|
448
|
-
Args:
|
|
449
|
-
node_id: Unique identifier for this node
|
|
450
|
-
next_node: ID of the next node to transition to after LLM response
|
|
451
|
-
input_key: Key in run.vars to read prompt/system from
|
|
452
|
-
output_key: Key in run.vars to write response to
|
|
453
|
-
provider: LLM provider to use
|
|
454
|
-
model: Model name to use
|
|
455
|
-
temperature: Temperature parameter
|
|
456
|
-
|
|
457
|
-
Returns:
|
|
458
|
-
A node handler that produces LLM_CALL effect
|
|
459
|
-
"""
|
|
460
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
461
|
-
|
|
462
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
463
|
-
"""Make LLM call and continue."""
|
|
464
|
-
# Get input from vars
|
|
465
|
-
if input_key:
|
|
466
|
-
input_data = run.vars.get(input_key, {})
|
|
467
|
-
else:
|
|
468
|
-
input_data = run.vars
|
|
469
|
-
|
|
470
|
-
# Extract prompt and system
|
|
471
|
-
if isinstance(input_data, dict):
|
|
472
|
-
prompt = input_data.get("prompt", "")
|
|
473
|
-
system = input_data.get("system", "")
|
|
474
|
-
else:
|
|
475
|
-
prompt = str(input_data) if input_data else ""
|
|
476
|
-
system = ""
|
|
477
|
-
|
|
478
|
-
# Build messages for LLM
|
|
479
|
-
messages = []
|
|
480
|
-
if system:
|
|
481
|
-
messages.append({"role": "system", "content": system})
|
|
482
|
-
messages.append({"role": "user", "content": prompt})
|
|
483
|
-
|
|
484
|
-
# Create the effect
|
|
485
|
-
effect = Effect(
|
|
486
|
-
type=EffectType.LLM_CALL,
|
|
487
|
-
payload={
|
|
488
|
-
"messages": messages,
|
|
489
|
-
"provider": provider,
|
|
490
|
-
"model": model,
|
|
491
|
-
"params": {
|
|
492
|
-
"temperature": temperature,
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
result_key=output_key or "_temp.llm_response",
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
return StepPlan(
|
|
499
|
-
node_id=node_id,
|
|
500
|
-
effect=effect,
|
|
501
|
-
next_node=next_node,
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
return handler
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def create_tool_calls_handler(
|
|
508
|
-
node_id: str,
|
|
509
|
-
next_node: Optional[str],
|
|
510
|
-
input_key: Optional[str] = None,
|
|
511
|
-
output_key: Optional[str] = None,
|
|
512
|
-
allowed_tools: Optional[List[str]] = None,
|
|
513
|
-
) -> Callable:
|
|
514
|
-
"""Create a node handler that executes tool calls via AbstractRuntime.
|
|
515
|
-
|
|
516
|
-
This produces a durable `EffectType.TOOL_CALLS` so tool execution stays runtime-owned.
|
|
517
|
-
|
|
518
|
-
Inputs:
|
|
519
|
-
- `tool_calls`: list[dict] (or a single dict) in the common shape
|
|
520
|
-
`{name, arguments, call_id?}`.
|
|
521
|
-
- Optional `allowed_tools`: list[str] allowlist. If provided as a list, the
|
|
522
|
-
runtime effect handler enforces it (empty list => allow none).
|
|
523
|
-
"""
|
|
524
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
525
|
-
|
|
526
|
-
def _normalize_tool_calls(raw: Any) -> list[Dict[str, Any]]:
|
|
527
|
-
if raw is None:
|
|
528
|
-
return []
|
|
529
|
-
if isinstance(raw, dict):
|
|
530
|
-
return [dict(raw)]
|
|
531
|
-
if isinstance(raw, list):
|
|
532
|
-
out: list[Dict[str, Any]] = []
|
|
533
|
-
for x in raw:
|
|
534
|
-
if isinstance(x, dict):
|
|
535
|
-
out.append(dict(x))
|
|
536
|
-
return out
|
|
537
|
-
return []
|
|
538
|
-
|
|
539
|
-
def _normalize_str_list(raw: Any) -> list[str]:
|
|
540
|
-
if not isinstance(raw, list):
|
|
541
|
-
return []
|
|
542
|
-
out: list[str] = []
|
|
543
|
-
for x in raw:
|
|
544
|
-
if isinstance(x, str) and x.strip():
|
|
545
|
-
out.append(x.strip())
|
|
546
|
-
return out
|
|
547
|
-
|
|
548
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
549
|
-
del ctx
|
|
550
|
-
if input_key:
|
|
551
|
-
input_data = run.vars.get(input_key, {})
|
|
552
|
-
else:
|
|
553
|
-
input_data = run.vars
|
|
554
|
-
|
|
555
|
-
tool_calls: list[Dict[str, Any]] = []
|
|
556
|
-
allowlist: Optional[list[str]] = list(allowed_tools) if isinstance(allowed_tools, list) else None
|
|
557
|
-
|
|
558
|
-
if isinstance(input_data, dict):
|
|
559
|
-
raw_calls = input_data.get("tool_calls")
|
|
560
|
-
if raw_calls is None:
|
|
561
|
-
raw_calls = input_data.get("toolCalls")
|
|
562
|
-
tool_calls = _normalize_tool_calls(raw_calls)
|
|
563
|
-
|
|
564
|
-
# Optional override when the input explicitly provides an allowlist.
|
|
565
|
-
if "allowed_tools" in input_data or "allowedTools" in input_data:
|
|
566
|
-
raw_allowed = input_data.get("allowed_tools")
|
|
567
|
-
if raw_allowed is None:
|
|
568
|
-
raw_allowed = input_data.get("allowedTools")
|
|
569
|
-
allowlist = _normalize_str_list(raw_allowed)
|
|
570
|
-
else:
|
|
571
|
-
tool_calls = _normalize_tool_calls(input_data)
|
|
572
|
-
|
|
573
|
-
payload: Dict[str, Any] = {"tool_calls": tool_calls}
|
|
574
|
-
if isinstance(allowlist, list):
|
|
575
|
-
payload["allowed_tools"] = _normalize_str_list(allowlist)
|
|
576
|
-
|
|
577
|
-
return StepPlan(
|
|
578
|
-
node_id=node_id,
|
|
579
|
-
effect=Effect(
|
|
580
|
-
type=EffectType.TOOL_CALLS,
|
|
581
|
-
payload=payload,
|
|
582
|
-
result_key=output_key or "_temp.tool_calls",
|
|
583
|
-
),
|
|
584
|
-
next_node=next_node,
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
return handler
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def create_start_subworkflow_handler(
|
|
591
|
-
node_id: str,
|
|
592
|
-
next_node: Optional[str],
|
|
593
|
-
input_key: Optional[str] = None,
|
|
594
|
-
output_key: Optional[str] = None,
|
|
595
|
-
workflow_id: Optional[str] = None,
|
|
596
|
-
) -> Callable:
|
|
597
|
-
"""Create a node handler that starts a subworkflow by workflow id.
|
|
598
|
-
|
|
599
|
-
This is the effect-level equivalent of `create_subflow_node_handler`, but it
|
|
600
|
-
defers lookup/execution to the runtime's workflow registry.
|
|
601
|
-
"""
|
|
602
|
-
from abstractruntime.core.models import StepPlan, Effect, EffectType
|
|
603
|
-
|
|
604
|
-
def handler(run: "RunState", ctx: Any) -> "StepPlan":
|
|
605
|
-
if not workflow_id:
|
|
606
|
-
return StepPlan(
|
|
607
|
-
node_id=node_id,
|
|
608
|
-
complete_output={
|
|
609
|
-
"success": False,
|
|
610
|
-
"error": "start_subworkflow requires workflow_id (node config missing)",
|
|
611
|
-
},
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
if input_key:
|
|
615
|
-
input_data = run.vars.get(input_key, {})
|
|
616
|
-
else:
|
|
617
|
-
input_data = run.vars
|
|
618
|
-
|
|
619
|
-
sub_vars: Dict[str, Any] = {}
|
|
620
|
-
if isinstance(input_data, dict):
|
|
621
|
-
# Prefer explicit "vars" field, else pass through common "input" field.
|
|
622
|
-
if isinstance(input_data.get("vars"), dict):
|
|
623
|
-
sub_vars = dict(input_data["vars"])
|
|
624
|
-
elif isinstance(input_data.get("input"), dict):
|
|
625
|
-
sub_vars = dict(input_data["input"])
|
|
626
|
-
else:
|
|
627
|
-
sub_vars = dict(input_data)
|
|
628
|
-
else:
|
|
629
|
-
sub_vars = {"input": input_data}
|
|
630
|
-
|
|
631
|
-
return StepPlan(
|
|
632
|
-
node_id=node_id,
|
|
633
|
-
effect=Effect(
|
|
634
|
-
type=EffectType.START_SUBWORKFLOW,
|
|
635
|
-
payload={
|
|
636
|
-
"workflow_id": workflow_id,
|
|
637
|
-
"vars": sub_vars,
|
|
638
|
-
"async": False,
|
|
639
|
-
},
|
|
640
|
-
result_key=output_key or f"_temp.effects.{node_id}",
|
|
641
|
-
),
|
|
642
|
-
next_node=next_node,
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
return handler
|
|
5
|
+
from abstractruntime.visualflow_compiler.adapters.effect_adapter import * # noqa: F401,F403
|