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.
@@ -1,645 +1,5 @@
1
- """Adapter for creating effect nodes in visual flows.
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 typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
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