applied-cli 0.5.7__tar.gz → 0.5.9__tar.gz
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.
- {applied_cli-0.5.7 → applied_cli-0.5.9}/PKG-INFO +1 -1
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli/__init__.py +1 -1
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli/client.py +113 -44
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli/tools.py +543 -41
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.5.7 → applied_cli-0.5.9}/pyproject.toml +1 -1
- {applied_cli-0.5.7 → applied_cli-0.5.9}/README.md +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli/cli.py +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.7 → applied_cli-0.5.9}/setup.cfg +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""Applied Labs API client."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
3
6
|
from typing import Any
|
|
4
7
|
|
|
5
8
|
import httpx
|
|
@@ -250,6 +253,19 @@ class AppliedClient:
|
|
|
250
253
|
# Flow Nodes
|
|
251
254
|
# -------------------------------------------------------------------------
|
|
252
255
|
|
|
256
|
+
async def get_node(self, flow_id: str, node_id: str) -> dict | None:
|
|
257
|
+
"""Get a single node with full configuration.
|
|
258
|
+
|
|
259
|
+
Returns None if node not found.
|
|
260
|
+
"""
|
|
261
|
+
flow = await self.get_flow(flow_id)
|
|
262
|
+
graph = flow.get("graph", {})
|
|
263
|
+
nodes = graph.get("nodes", [])
|
|
264
|
+
for node in nodes:
|
|
265
|
+
if node.get("id") == node_id:
|
|
266
|
+
return node
|
|
267
|
+
return None
|
|
268
|
+
|
|
253
269
|
async def create_node(
|
|
254
270
|
self,
|
|
255
271
|
flow_id: str,
|
|
@@ -341,10 +357,44 @@ class AppliedClient:
|
|
|
341
357
|
self,
|
|
342
358
|
flow_id: str,
|
|
343
359
|
trigger_data: dict[str, Any] | None = None,
|
|
360
|
+
wait: bool = False,
|
|
361
|
+
wait_timeout: float = 60.0,
|
|
362
|
+
poll_interval: float = 1.0,
|
|
344
363
|
) -> dict:
|
|
345
|
-
"""Execute a flow with trigger data.
|
|
364
|
+
"""Execute a flow with trigger data.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
flow_id: The flow UUID
|
|
368
|
+
trigger_data: Trigger input data
|
|
369
|
+
wait: If True, poll until the run completes and include spans
|
|
370
|
+
wait_timeout: Max seconds to wait for completion (default 60)
|
|
371
|
+
poll_interval: Seconds between polls (default 1)
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Flow run with spans if wait=True
|
|
375
|
+
"""
|
|
346
376
|
body = {"trigger": trigger_data or {}}
|
|
347
|
-
|
|
377
|
+
run = await self._request("POST", f"/v1/flows/{flow_id}/run/", body=body)
|
|
378
|
+
|
|
379
|
+
if not wait:
|
|
380
|
+
return run
|
|
381
|
+
|
|
382
|
+
# Poll until completion
|
|
383
|
+
run_id = run.get("id")
|
|
384
|
+
if not run_id:
|
|
385
|
+
return run
|
|
386
|
+
|
|
387
|
+
elapsed = 0.0
|
|
388
|
+
while elapsed < wait_timeout:
|
|
389
|
+
run = await self.get_flow_run(run_id)
|
|
390
|
+
status = run.get("status", "")
|
|
391
|
+
if status in ("Success", "Failed", "Cancelled"):
|
|
392
|
+
return run
|
|
393
|
+
await asyncio.sleep(poll_interval)
|
|
394
|
+
elapsed += poll_interval
|
|
395
|
+
|
|
396
|
+
# Timeout - return current state
|
|
397
|
+
return run
|
|
348
398
|
|
|
349
399
|
async def list_flow_runs(
|
|
350
400
|
self,
|
|
@@ -395,6 +445,31 @@ class AppliedClient:
|
|
|
395
445
|
# Conversational Testing (via streaming /complete endpoint)
|
|
396
446
|
# -------------------------------------------------------------------------
|
|
397
447
|
|
|
448
|
+
async def create_conversation(
|
|
449
|
+
self,
|
|
450
|
+
agent_id: str,
|
|
451
|
+
is_test: bool = True,
|
|
452
|
+
metadata: dict[str, Any] | None = None,
|
|
453
|
+
) -> dict[str, Any]:
|
|
454
|
+
"""Create a new conversation for an agent.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
agent_id: The agent UUID
|
|
458
|
+
is_test: Whether this is a test conversation (default True)
|
|
459
|
+
metadata: Optional metadata for the conversation
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Dict with conversation id
|
|
463
|
+
"""
|
|
464
|
+
body: dict[str, Any] = {
|
|
465
|
+
"agent_id": agent_id,
|
|
466
|
+
"is_test": is_test,
|
|
467
|
+
}
|
|
468
|
+
if metadata:
|
|
469
|
+
body["metadata"] = metadata
|
|
470
|
+
|
|
471
|
+
return await self._request("POST", "/v1/c/", body=body)
|
|
472
|
+
|
|
398
473
|
async def send_message(
|
|
399
474
|
self,
|
|
400
475
|
agent_id: str,
|
|
@@ -410,6 +485,8 @@ class AppliedClient:
|
|
|
410
485
|
This calls the /complete endpoint (streaming) and consumes the full
|
|
411
486
|
response. Use this for testing conversational flows.
|
|
412
487
|
|
|
488
|
+
If no conversation_id is provided, a new conversation is created first.
|
|
489
|
+
|
|
413
490
|
Args:
|
|
414
491
|
agent_id: The agent UUID
|
|
415
492
|
message: The user message to send
|
|
@@ -420,46 +497,51 @@ class AppliedClient:
|
|
|
420
497
|
metadata: Optional metadata dict for the conversation
|
|
421
498
|
|
|
422
499
|
Returns:
|
|
423
|
-
Dict with conversation_id,
|
|
500
|
+
Dict with conversation_id, response, and status
|
|
424
501
|
"""
|
|
425
|
-
|
|
426
|
-
|
|
502
|
+
# Build metadata with contact info
|
|
503
|
+
meta = metadata.copy() if metadata else {}
|
|
504
|
+
if contact_email:
|
|
505
|
+
meta["email"] = contact_email
|
|
506
|
+
if contact_name:
|
|
507
|
+
meta["name"] = contact_name
|
|
508
|
+
if contact_phone:
|
|
509
|
+
meta["phone"] = contact_phone
|
|
510
|
+
|
|
511
|
+
# Create conversation if needed
|
|
512
|
+
result_conversation_id = conversation_id
|
|
513
|
+
if not result_conversation_id:
|
|
514
|
+
conv = await self.create_conversation(
|
|
515
|
+
agent_id=agent_id,
|
|
516
|
+
is_test=True,
|
|
517
|
+
metadata=meta if meta else None,
|
|
518
|
+
)
|
|
519
|
+
result_conversation_id = conv["id"]
|
|
520
|
+
|
|
521
|
+
# /complete endpoint uses AllowAny - no auth header needed
|
|
522
|
+
headers: dict[str, str] = {
|
|
427
523
|
"Content-Type": "application/json",
|
|
428
|
-
"Accept": "text/event-stream",
|
|
429
524
|
}
|
|
430
|
-
if self.shop_id:
|
|
431
|
-
headers["X-Shop-Id"] = self.shop_id
|
|
432
525
|
|
|
433
526
|
body: dict[str, Any] = {
|
|
434
|
-
"
|
|
527
|
+
"conversation_id": result_conversation_id,
|
|
435
528
|
"transcript": [
|
|
436
529
|
{
|
|
530
|
+
"id": str(uuid.uuid4()),
|
|
437
531
|
"role": "user",
|
|
438
532
|
"content": message,
|
|
439
533
|
"text": message,
|
|
440
|
-
"format": "
|
|
534
|
+
"format": "MARKDOWN",
|
|
441
535
|
}
|
|
442
536
|
],
|
|
443
537
|
}
|
|
444
538
|
|
|
445
|
-
if conversation_id:
|
|
446
|
-
body["conversation_id"] = conversation_id
|
|
447
|
-
|
|
448
|
-
# Build metadata with contact info
|
|
449
|
-
meta = metadata or {}
|
|
450
|
-
if contact_email:
|
|
451
|
-
meta["email"] = contact_email
|
|
452
|
-
if contact_name:
|
|
453
|
-
meta["name"] = contact_name
|
|
454
|
-
if contact_phone:
|
|
455
|
-
meta["phone"] = contact_phone
|
|
456
539
|
if meta:
|
|
457
540
|
body["metadata"] = meta
|
|
458
541
|
|
|
459
542
|
url = f"{self.base_url}/v1/agents/{agent_id}/complete/"
|
|
460
543
|
|
|
461
544
|
response_text = ""
|
|
462
|
-
result_conversation_id = conversation_id
|
|
463
545
|
|
|
464
546
|
async with (
|
|
465
547
|
httpx.AsyncClient(timeout=120.0) as client,
|
|
@@ -467,32 +549,19 @@ class AppliedClient:
|
|
|
467
549
|
):
|
|
468
550
|
response.raise_for_status()
|
|
469
551
|
|
|
552
|
+
# v1 streaming format: JSON objects on each line
|
|
470
553
|
async for line in response.aiter_lines():
|
|
471
554
|
if not line:
|
|
472
555
|
continue
|
|
473
556
|
|
|
474
|
-
#
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
data = json.loads(data_str)
|
|
484
|
-
|
|
485
|
-
# Extract conversation_id from first chunk
|
|
486
|
-
if not result_conversation_id:
|
|
487
|
-
result_conversation_id = data.get("conversation_id")
|
|
488
|
-
|
|
489
|
-
# Accumulate response content
|
|
490
|
-
if "content" in data:
|
|
491
|
-
response_text += data["content"]
|
|
492
|
-
|
|
493
|
-
except (json.JSONDecodeError, KeyError):
|
|
494
|
-
# Skip malformed chunks
|
|
495
|
-
pass
|
|
557
|
+
# Try to parse as JSON directly (v1 format)
|
|
558
|
+
try:
|
|
559
|
+
data = json.loads(line)
|
|
560
|
+
if "content" in data:
|
|
561
|
+
response_text += data["content"]
|
|
562
|
+
except json.JSONDecodeError:
|
|
563
|
+
# Skip non-JSON lines
|
|
564
|
+
pass
|
|
496
565
|
|
|
497
566
|
return {
|
|
498
567
|
"conversation_id": result_conversation_id,
|
|
@@ -256,6 +256,7 @@ async def flow_list(
|
|
|
256
256
|
async def flow_get(
|
|
257
257
|
client: AppliedClient,
|
|
258
258
|
flow_id: str,
|
|
259
|
+
output_format: str = "summary",
|
|
259
260
|
) -> str:
|
|
260
261
|
"""
|
|
261
262
|
Get a flow with its full graph (nodes and edges).
|
|
@@ -263,15 +264,20 @@ async def flow_get(
|
|
|
263
264
|
Args:
|
|
264
265
|
client: Authenticated AppliedClient
|
|
265
266
|
flow_id: The flow UUID
|
|
267
|
+
output_format: 'summary' (default), 'full' (includes node metadata/prompts),
|
|
268
|
+
or 'json' (raw JSON)
|
|
266
269
|
|
|
267
270
|
Returns:
|
|
268
|
-
Flow metadata, nodes
|
|
271
|
+
Flow metadata, nodes, and edges. Use 'full' to see node configurations.
|
|
269
272
|
"""
|
|
270
273
|
flow = await client.get_flow(flow_id)
|
|
271
274
|
graph = flow.get("graph", {})
|
|
272
275
|
nodes = graph.get("nodes", [])
|
|
273
276
|
edges = graph.get("edges", [])
|
|
274
277
|
|
|
278
|
+
if output_format == "json":
|
|
279
|
+
return to_json(flow)
|
|
280
|
+
|
|
275
281
|
# Format header
|
|
276
282
|
result = f"# Flow: {flow.get('name')}\n"
|
|
277
283
|
result += f"id: {flow.get('id')}\n"
|
|
@@ -285,15 +291,48 @@ async def flow_get(
|
|
|
285
291
|
|
|
286
292
|
# Format nodes
|
|
287
293
|
result += f"\n## Nodes ({len(nodes)})\n"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
|
|
295
|
+
if output_format == "full":
|
|
296
|
+
# Full output: include metadata, prompt, schemas for each node
|
|
297
|
+
for n in nodes:
|
|
298
|
+
data = n.get("data", {})
|
|
299
|
+
node_name = data.get("name", n.get("id", ""))
|
|
300
|
+
node_type = n.get("type", data.get("executor_type", ""))
|
|
301
|
+
|
|
302
|
+
result += f"\n### {node_name}"
|
|
303
|
+
if node_type and node_type != node_name:
|
|
304
|
+
result += f" ({node_type})"
|
|
305
|
+
result += "\n"
|
|
306
|
+
result += f"id: {n.get('id')}\n"
|
|
307
|
+
|
|
308
|
+
if data.get("description"):
|
|
309
|
+
result += f"description: {data.get('description')}\n"
|
|
310
|
+
|
|
311
|
+
if data.get("prompt"):
|
|
312
|
+
prompt_preview = data.get("prompt", "")
|
|
313
|
+
if len(prompt_preview) > 300:
|
|
314
|
+
prompt_preview = prompt_preview[:300] + "..."
|
|
315
|
+
result += f"prompt: {prompt_preview}\n"
|
|
316
|
+
|
|
317
|
+
metadata = data.get("metadata", {})
|
|
318
|
+
if metadata:
|
|
319
|
+
result += f"metadata: {json.dumps(metadata)}\n"
|
|
320
|
+
|
|
321
|
+
outputs = data.get("outputs", {})
|
|
322
|
+
if outputs and outputs.get("properties"):
|
|
323
|
+
props = list(outputs.get("properties", {}).keys())
|
|
324
|
+
result += f"output_fields: {', '.join(props)}\n"
|
|
325
|
+
else:
|
|
326
|
+
# Summary output: table format
|
|
327
|
+
node_rows = [
|
|
328
|
+
{
|
|
329
|
+
"id": n.get("id"),
|
|
330
|
+
"type": n.get("type", n.get("data", {}).get("name", "")),
|
|
331
|
+
"description": n.get("data", {}).get("description", "")[:50],
|
|
332
|
+
}
|
|
333
|
+
for n in nodes
|
|
334
|
+
]
|
|
335
|
+
result += to_csv(node_rows, ["id", "type", "description"])
|
|
297
336
|
|
|
298
337
|
# Format edges
|
|
299
338
|
result += f"\n## Edges ({len(edges)})\n"
|
|
@@ -415,6 +454,60 @@ async def flow_delete(
|
|
|
415
454
|
# -----------------------------------------------------------------------------
|
|
416
455
|
|
|
417
456
|
|
|
457
|
+
async def flow_node_get(
|
|
458
|
+
client: AppliedClient,
|
|
459
|
+
flow_id: str,
|
|
460
|
+
node_id: str,
|
|
461
|
+
) -> str:
|
|
462
|
+
"""
|
|
463
|
+
Get a single node's full configuration.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
client: Authenticated AppliedClient
|
|
467
|
+
flow_id: The flow UUID
|
|
468
|
+
node_id: The node UUID
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Full node config including metadata, prompt, executor type, and schemas
|
|
472
|
+
"""
|
|
473
|
+
node = await client.get_node(flow_id, node_id)
|
|
474
|
+
|
|
475
|
+
if not node:
|
|
476
|
+
return f"Node {node_id} not found in flow {flow_id}"
|
|
477
|
+
|
|
478
|
+
data = node.get("data", {})
|
|
479
|
+
|
|
480
|
+
result = f"# Node: {data.get('name', node_id)}\n"
|
|
481
|
+
result += f"id: {node.get('id')}\n"
|
|
482
|
+
result += f"type: {node.get('type', data.get('executor_type', ''))}\n"
|
|
483
|
+
|
|
484
|
+
if data.get("description"):
|
|
485
|
+
result += f"description: {data.get('description')}\n"
|
|
486
|
+
|
|
487
|
+
if data.get("prompt"):
|
|
488
|
+
result += f"\n## Prompt\n{data.get('prompt')}\n"
|
|
489
|
+
|
|
490
|
+
# Full metadata
|
|
491
|
+
metadata = data.get("metadata", {})
|
|
492
|
+
if metadata:
|
|
493
|
+
result += "\n## Metadata\n"
|
|
494
|
+
result += json.dumps(metadata, indent=2) + "\n"
|
|
495
|
+
|
|
496
|
+
# Input schema
|
|
497
|
+
inputs = data.get("inputs", {})
|
|
498
|
+
if inputs:
|
|
499
|
+
result += "\n## Input Schema\n"
|
|
500
|
+
result += json.dumps(inputs, indent=2) + "\n"
|
|
501
|
+
|
|
502
|
+
# Output schema
|
|
503
|
+
outputs = data.get("outputs", {})
|
|
504
|
+
if outputs:
|
|
505
|
+
result += "\n## Output Schema\n"
|
|
506
|
+
result += json.dumps(outputs, indent=2) + "\n"
|
|
507
|
+
|
|
508
|
+
return result
|
|
509
|
+
|
|
510
|
+
|
|
418
511
|
async def flow_node_create(
|
|
419
512
|
client: AppliedClient,
|
|
420
513
|
flow_id: str,
|
|
@@ -616,6 +709,8 @@ async def flow_run(
|
|
|
616
709
|
client: AppliedClient,
|
|
617
710
|
flow_id: str,
|
|
618
711
|
trigger_data: dict | None = None,
|
|
712
|
+
wait: bool = True,
|
|
713
|
+
wait_timeout: float = 60.0,
|
|
619
714
|
) -> str:
|
|
620
715
|
"""
|
|
621
716
|
Execute a flow with test input.
|
|
@@ -627,17 +722,76 @@ async def flow_run(
|
|
|
627
722
|
{"message": "Hello"}
|
|
628
723
|
{"message": "Help me", "contact": {"email": "user@example.com"}}
|
|
629
724
|
{"conversation_id": "uuid-of-existing-conversation"}
|
|
725
|
+
wait: If True (default), wait for completion and return full trace.
|
|
726
|
+
If False, return immediately with just the run ID.
|
|
727
|
+
wait_timeout: Max seconds to wait (default 60)
|
|
630
728
|
|
|
631
729
|
Returns:
|
|
632
|
-
Flow run
|
|
730
|
+
Flow run with execution trace (if wait=True) or just run ID
|
|
633
731
|
"""
|
|
634
|
-
run = await client.run_flow(
|
|
732
|
+
run = await client.run_flow(
|
|
733
|
+
flow_id,
|
|
734
|
+
trigger_data,
|
|
735
|
+
wait=wait,
|
|
736
|
+
wait_timeout=wait_timeout,
|
|
737
|
+
)
|
|
635
738
|
|
|
636
739
|
result = f"# Flow Run: {run.get('id')}\n"
|
|
637
740
|
result += f"status: {run.get('status')}\n"
|
|
638
741
|
result += f"started_at: {run.get('started_at')}\n"
|
|
639
|
-
result += f"ended_at: {run.get('ended_at')}\n"
|
|
640
742
|
|
|
743
|
+
if run.get("ended_at"):
|
|
744
|
+
result += f"ended_at: {run.get('ended_at')}\n"
|
|
745
|
+
if run.get("duration"):
|
|
746
|
+
result += f"duration: {run.get('duration')}s\n"
|
|
747
|
+
|
|
748
|
+
# Execution trace from spans
|
|
749
|
+
spans = run.get("spans", [])
|
|
750
|
+
if spans:
|
|
751
|
+
result += f"\n## Execution Trace ({len(spans)} spans)\n"
|
|
752
|
+
for i, span in enumerate(spans, 1):
|
|
753
|
+
name = span.get("span_name", "")
|
|
754
|
+
duration = span.get("duration", 0)
|
|
755
|
+
status = span.get("status_code", "")
|
|
756
|
+
status_msg = span.get("status_message", "")
|
|
757
|
+
attrs = span.get("span_attributes", {})
|
|
758
|
+
|
|
759
|
+
status_str = "OK" if "OK" in str(status) else "ERROR"
|
|
760
|
+
result += f"{i}. [{name}] {duration:.2f}s - {status_str}"
|
|
761
|
+
if status_msg:
|
|
762
|
+
result += f" - {status_msg}"
|
|
763
|
+
result += "\n"
|
|
764
|
+
|
|
765
|
+
# Show output for executor_run spans
|
|
766
|
+
if name == "executor_run" and attrs.get("output"):
|
|
767
|
+
try:
|
|
768
|
+
output_data = json.loads(attrs["output"])
|
|
769
|
+
for k, v in output_data.items():
|
|
770
|
+
if k == "success":
|
|
771
|
+
continue
|
|
772
|
+
result += f" output.{k}: {str(v)[:300]}\n"
|
|
773
|
+
except (json.JSONDecodeError, TypeError):
|
|
774
|
+
result += f" output: {str(attrs['output'])[:500]}\n"
|
|
775
|
+
|
|
776
|
+
# completion: show the LLM text output
|
|
777
|
+
elif name == "completion" and attrs.get("content"):
|
|
778
|
+
result += f" completion: {str(attrs['content'])[:500]}\n"
|
|
779
|
+
|
|
780
|
+
# structured_completion: show parsed output
|
|
781
|
+
elif name == "structured_completion" and attrs.get("output"):
|
|
782
|
+
try:
|
|
783
|
+
out = json.loads(attrs["output"])
|
|
784
|
+
result += f" output: {json.dumps(out, indent=None)[:400]}\n"
|
|
785
|
+
except (json.JSONDecodeError, TypeError):
|
|
786
|
+
result += f" output: {str(attrs['output'])[:400]}\n"
|
|
787
|
+
|
|
788
|
+
# branch: show routing decision
|
|
789
|
+
elif name == "branch":
|
|
790
|
+
selected = attrs.get("selected.path", "")
|
|
791
|
+
if selected:
|
|
792
|
+
result += f" selected: {selected}\n"
|
|
793
|
+
|
|
794
|
+
# Actions summary
|
|
641
795
|
actions = run.get("actions", [])
|
|
642
796
|
if actions:
|
|
643
797
|
result += f"\n## Actions ({len(actions)})\n"
|
|
@@ -650,6 +804,21 @@ async def flow_run(
|
|
|
650
804
|
]
|
|
651
805
|
result += to_csv(action_rows, ["node", "status"])
|
|
652
806
|
|
|
807
|
+
# Errors
|
|
808
|
+
errors = [s for s in spans if "ERROR" in str(s.get("status_code", ""))]
|
|
809
|
+
if errors:
|
|
810
|
+
result += "\n## Errors\n"
|
|
811
|
+
for err in errors:
|
|
812
|
+
attrs = err.get("span_attributes", {})
|
|
813
|
+
node_name = attrs.get("tool_name") or err.get("span_name") or "unknown"
|
|
814
|
+
error_msg = err.get("status_message", "")
|
|
815
|
+
result += f"**Node:** {node_name}\n"
|
|
816
|
+
if error_msg:
|
|
817
|
+
result += f"**Error:** {error_msg}\n"
|
|
818
|
+
if attrs.get("output"):
|
|
819
|
+
result += f"**Output:** {str(attrs.get('output'))[:500]}\n"
|
|
820
|
+
result += "\n"
|
|
821
|
+
|
|
653
822
|
return result
|
|
654
823
|
|
|
655
824
|
|
|
@@ -949,82 +1118,415 @@ async def send_message(
|
|
|
949
1118
|
|
|
950
1119
|
async def executor_list(
|
|
951
1120
|
client: AppliedClient,
|
|
952
|
-
output_format: str = "
|
|
1121
|
+
output_format: str = "full",
|
|
953
1122
|
) -> str:
|
|
954
1123
|
"""
|
|
955
|
-
List available node types (executors).
|
|
1124
|
+
List available node types (executors) with their metadata schemas.
|
|
956
1125
|
|
|
957
1126
|
Args:
|
|
958
1127
|
client: Authenticated AppliedClient (not used, list is hardcoded)
|
|
959
|
-
output_format: 'csv'
|
|
1128
|
+
output_format: 'full' (default, includes schemas), 'csv' (names only), 'json'
|
|
960
1129
|
|
|
961
1130
|
Returns:
|
|
962
|
-
Available executor types with descriptions
|
|
1131
|
+
Available executor types with descriptions and metadata field schemas
|
|
963
1132
|
"""
|
|
964
|
-
# Hardcoded list of
|
|
1133
|
+
# Hardcoded list of executors with metadata schemas
|
|
965
1134
|
executors = [
|
|
966
1135
|
{
|
|
967
1136
|
"name": "completion",
|
|
968
|
-
"description": "LLM completion
|
|
969
|
-
"use_case": "
|
|
1137
|
+
"description": "LLM completion - generates free-form text",
|
|
1138
|
+
"use_case": "Responses, summaries, any text output",
|
|
1139
|
+
"metadata_fields": {
|
|
1140
|
+
"model": "string - LLM model (default: agent's model)",
|
|
1141
|
+
"temperature": "float - 0.0-1.0 (default: 0.7)",
|
|
1142
|
+
"max_tokens": "int - max output tokens",
|
|
1143
|
+
},
|
|
1144
|
+
"output_fields": ["completion"],
|
|
970
1145
|
},
|
|
971
1146
|
{
|
|
972
1147
|
"name": "structured_completion",
|
|
973
1148
|
"description": "LLM completion with structured JSON output",
|
|
974
|
-
"use_case": "Extract data, classify,
|
|
1149
|
+
"use_case": "Extract data, classify, typed responses",
|
|
1150
|
+
"metadata_fields": {
|
|
1151
|
+
"model": "string - LLM model",
|
|
1152
|
+
"output_schema": "object - JSON Schema for output structure",
|
|
1153
|
+
},
|
|
1154
|
+
"output_fields": ["(defined by output_schema)"],
|
|
975
1155
|
},
|
|
976
1156
|
{
|
|
977
1157
|
"name": "branch",
|
|
978
|
-
"description": "Conditional branching based on LLM
|
|
979
|
-
"use_case": "Route flow based on
|
|
1158
|
+
"description": "Conditional branching based on comparison or LLM",
|
|
1159
|
+
"use_case": "Route flow based on conditions",
|
|
1160
|
+
"metadata_fields": {
|
|
1161
|
+
"branch_type": "'comparison' or 'llm'",
|
|
1162
|
+
"branches": "array - branch definitions with conditions",
|
|
1163
|
+
},
|
|
1164
|
+
"output_fields": ["selected_branch"],
|
|
980
1165
|
},
|
|
981
1166
|
{
|
|
982
|
-
"name": "
|
|
983
|
-
"description": "
|
|
984
|
-
"use_case": "
|
|
1167
|
+
"name": "code",
|
|
1168
|
+
"description": "Execute Python code in sandbox",
|
|
1169
|
+
"use_case": "Data transformation, calculations, API calls",
|
|
1170
|
+
"metadata_fields": {
|
|
1171
|
+
"language": "string - 'python' (required)",
|
|
1172
|
+
"code": "string - Python code to execute (required)",
|
|
1173
|
+
"packages": "array - pip packages to install, e.g. ['requests']",
|
|
1174
|
+
},
|
|
1175
|
+
"output_fields": ["result", "stdout", "stderr"],
|
|
985
1176
|
},
|
|
986
1177
|
{
|
|
987
1178
|
"name": "http_request",
|
|
988
1179
|
"description": "Make HTTP API calls",
|
|
989
1180
|
"use_case": "Integrate with external APIs",
|
|
1181
|
+
"metadata_fields": {
|
|
1182
|
+
"method": "string - GET, POST, PUT, DELETE",
|
|
1183
|
+
"url": "string - endpoint URL (supports {var} interpolation)",
|
|
1184
|
+
"headers": "object - request headers",
|
|
1185
|
+
"body": "object - request body for POST/PUT",
|
|
1186
|
+
},
|
|
1187
|
+
"output_fields": ["status_code", "data", "headers"],
|
|
990
1188
|
},
|
|
991
1189
|
{
|
|
992
|
-
"name": "
|
|
993
|
-
"description": "
|
|
994
|
-
"use_case": "
|
|
1190
|
+
"name": "loop",
|
|
1191
|
+
"description": "Loop over a list and execute subflow",
|
|
1192
|
+
"use_case": "Process multiple items, batch operations",
|
|
1193
|
+
"metadata_fields": {
|
|
1194
|
+
"items_path": "string - variable path to iterate, e.g. {trigger.items}",
|
|
1195
|
+
"max_iterations": "int - limit iterations",
|
|
1196
|
+
},
|
|
1197
|
+
"output_fields": ["results", "count"],
|
|
995
1198
|
},
|
|
996
1199
|
{
|
|
997
1200
|
"name": "memory",
|
|
998
1201
|
"description": "Store/retrieve data in conversation memory",
|
|
999
|
-
"use_case": "Remember user preferences, context",
|
|
1202
|
+
"use_case": "Remember user preferences, context across turns",
|
|
1203
|
+
"metadata_fields": {
|
|
1204
|
+
"operation": "'get' or 'set'",
|
|
1205
|
+
"key": "string - memory key",
|
|
1206
|
+
"value": "any - value to store (for set)",
|
|
1207
|
+
},
|
|
1208
|
+
"output_fields": ["value"],
|
|
1000
1209
|
},
|
|
1001
1210
|
{
|
|
1002
|
-
"name": "
|
|
1003
|
-
"description": "
|
|
1004
|
-
"use_case": "
|
|
1211
|
+
"name": "search",
|
|
1212
|
+
"description": "Search knowledge base",
|
|
1213
|
+
"use_case": "Find relevant documentation or Q&A",
|
|
1214
|
+
"metadata_fields": {
|
|
1215
|
+
"query": "string - search query (supports {var} interpolation)",
|
|
1216
|
+
"limit": "int - max results",
|
|
1217
|
+
},
|
|
1218
|
+
"output_fields": ["results", "count"],
|
|
1005
1219
|
},
|
|
1006
1220
|
{
|
|
1007
|
-
"name": "
|
|
1008
|
-
"description": "
|
|
1009
|
-
"use_case": "
|
|
1221
|
+
"name": "global",
|
|
1222
|
+
"description": "Global handler for escalation or failure",
|
|
1223
|
+
"use_case": "Catch-all for errors, default handler",
|
|
1224
|
+
"metadata_fields": {},
|
|
1225
|
+
"output_fields": [],
|
|
1010
1226
|
},
|
|
1011
1227
|
{
|
|
1012
1228
|
"name": "_escalate_conversation",
|
|
1013
1229
|
"description": "Escalate conversation to human agent",
|
|
1014
1230
|
"use_case": "Hand off to support team",
|
|
1231
|
+
"metadata_fields": {
|
|
1232
|
+
"reason": "string - escalation reason",
|
|
1233
|
+
},
|
|
1234
|
+
"output_fields": [],
|
|
1015
1235
|
},
|
|
1016
1236
|
{
|
|
1017
1237
|
"name": "handoff",
|
|
1018
1238
|
"description": "Hand off to another flow or agent",
|
|
1019
1239
|
"use_case": "Transfer to specialized flow",
|
|
1240
|
+
"metadata_fields": {
|
|
1241
|
+
"target_flow_id": "string - flow to hand off to",
|
|
1242
|
+
},
|
|
1243
|
+
"output_fields": [],
|
|
1020
1244
|
},
|
|
1021
1245
|
{
|
|
1022
|
-
"name": "
|
|
1023
|
-
"description": "
|
|
1024
|
-
"use_case": "
|
|
1246
|
+
"name": "end_flow",
|
|
1247
|
+
"description": "Explicitly end the flow",
|
|
1248
|
+
"use_case": "Terminate flow execution early",
|
|
1249
|
+
"metadata_fields": {},
|
|
1250
|
+
"output_fields": [],
|
|
1025
1251
|
},
|
|
1026
1252
|
]
|
|
1027
1253
|
|
|
1028
1254
|
if output_format == "csv":
|
|
1029
|
-
|
|
1030
|
-
|
|
1255
|
+
simple = [
|
|
1256
|
+
{"name": e["name"], "description": e["description"]} for e in executors
|
|
1257
|
+
]
|
|
1258
|
+
return to_csv(simple, ["name", "description"])
|
|
1259
|
+
|
|
1260
|
+
if output_format == "json":
|
|
1261
|
+
return to_json(executors)
|
|
1262
|
+
|
|
1263
|
+
# Full output with schemas
|
|
1264
|
+
result = "# Available Node Types (Executors)\n\n"
|
|
1265
|
+
result += "Use these with flow_node_create(executor_type=...)\n\n"
|
|
1266
|
+
|
|
1267
|
+
for ex in executors:
|
|
1268
|
+
result += f"## {ex['name']}\n"
|
|
1269
|
+
result += f"{ex['description']}\n"
|
|
1270
|
+
result += f"Use case: {ex['use_case']}\n"
|
|
1271
|
+
|
|
1272
|
+
if ex.get("metadata_fields"):
|
|
1273
|
+
result += "\n**Metadata fields:**\n"
|
|
1274
|
+
for field, desc in ex["metadata_fields"].items():
|
|
1275
|
+
result += f" - `{field}`: {desc}\n"
|
|
1276
|
+
|
|
1277
|
+
if ex.get("output_fields"):
|
|
1278
|
+
result += f"\n**Output fields:** {', '.join(ex['output_fields'])}\n"
|
|
1279
|
+
|
|
1280
|
+
result += "\n"
|
|
1281
|
+
|
|
1282
|
+
result += "## Variable Reference Syntax\n"
|
|
1283
|
+
result += "Reference previous node outputs in prompts and metadata:\n"
|
|
1284
|
+
result += "- `{trigger.message}` - user's input message\n"
|
|
1285
|
+
result += "- `{trigger.contact.email}` - contact email\n"
|
|
1286
|
+
result += "- `{node_name.field}` - output from previous node\n"
|
|
1287
|
+
result += "- `{code-abc123.result}` - code node output\n"
|
|
1288
|
+
result += "- `{http_request-xyz.data.items}` - nested API response\n"
|
|
1289
|
+
|
|
1290
|
+
return result
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
# -----------------------------------------------------------------------------
|
|
1294
|
+
# Flow Variables
|
|
1295
|
+
# -----------------------------------------------------------------------------
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
def _extract_schema_paths(
|
|
1299
|
+
schema: dict,
|
|
1300
|
+
prefix: str = "",
|
|
1301
|
+
defs: dict | None = None,
|
|
1302
|
+
max_depth: int = 4,
|
|
1303
|
+
) -> list[dict]:
|
|
1304
|
+
"""
|
|
1305
|
+
Recursively extract variable paths from a JSON Schema.
|
|
1306
|
+
|
|
1307
|
+
Returns list of {"path": "field.subfield", "type": "string", "description": "..."}
|
|
1308
|
+
"""
|
|
1309
|
+
if max_depth <= 0:
|
|
1310
|
+
return []
|
|
1311
|
+
|
|
1312
|
+
defs = defs or schema.get("$defs", {})
|
|
1313
|
+
paths = []
|
|
1314
|
+
|
|
1315
|
+
# Handle $ref
|
|
1316
|
+
if "$ref" in schema:
|
|
1317
|
+
ref_name = schema["$ref"].split("/")[-1]
|
|
1318
|
+
if ref_name in defs:
|
|
1319
|
+
return _extract_schema_paths(defs[ref_name], prefix, defs, max_depth)
|
|
1320
|
+
return []
|
|
1321
|
+
|
|
1322
|
+
# Handle anyOf/oneOf (take first option)
|
|
1323
|
+
if "anyOf" in schema:
|
|
1324
|
+
for option in schema["anyOf"]:
|
|
1325
|
+
if option.get("type") != "null":
|
|
1326
|
+
return _extract_schema_paths(option, prefix, defs, max_depth)
|
|
1327
|
+
return []
|
|
1328
|
+
|
|
1329
|
+
schema_type = schema.get("type", "")
|
|
1330
|
+
|
|
1331
|
+
if schema_type == "object":
|
|
1332
|
+
props = schema.get("properties", {})
|
|
1333
|
+
for prop_name, prop_schema in props.items():
|
|
1334
|
+
path = f"{prefix}.{prop_name}" if prefix else prop_name
|
|
1335
|
+
prop_type = prop_schema.get("type", "any")
|
|
1336
|
+
desc = prop_schema.get("description", "")
|
|
1337
|
+
|
|
1338
|
+
# Add this path
|
|
1339
|
+
paths.append({"path": path, "type": prop_type, "description": desc[:60]})
|
|
1340
|
+
|
|
1341
|
+
# Recurse for nested objects/arrays
|
|
1342
|
+
if prop_type == "object":
|
|
1343
|
+
paths.extend(
|
|
1344
|
+
_extract_schema_paths(prop_schema, path, defs, max_depth - 1)
|
|
1345
|
+
)
|
|
1346
|
+
elif prop_type == "array":
|
|
1347
|
+
items = prop_schema.get("items", {})
|
|
1348
|
+
if items:
|
|
1349
|
+
# Add [0] and [*] access patterns
|
|
1350
|
+
paths.append(
|
|
1351
|
+
{
|
|
1352
|
+
"path": f"{path}[0]",
|
|
1353
|
+
"type": "item",
|
|
1354
|
+
"description": "First item",
|
|
1355
|
+
}
|
|
1356
|
+
)
|
|
1357
|
+
paths.append(
|
|
1358
|
+
{
|
|
1359
|
+
"path": f"{path}[*]",
|
|
1360
|
+
"type": "list",
|
|
1361
|
+
"description": "Map over all",
|
|
1362
|
+
}
|
|
1363
|
+
)
|
|
1364
|
+
# Recurse into array items
|
|
1365
|
+
paths.extend(
|
|
1366
|
+
_extract_schema_paths(items, f"{path}[0]", defs, max_depth - 1)
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
elif schema_type == "array":
|
|
1370
|
+
items = schema.get("items", {})
|
|
1371
|
+
if items and prefix:
|
|
1372
|
+
paths.append(
|
|
1373
|
+
{"path": f"{prefix}[0]", "type": "item", "description": "First item"}
|
|
1374
|
+
)
|
|
1375
|
+
paths.extend(
|
|
1376
|
+
_extract_schema_paths(items, f"{prefix}[0]", defs, max_depth - 1)
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
return paths
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def _topological_sort(nodes: list[dict], edges: list[dict]) -> list[dict]:
|
|
1383
|
+
"""Sort nodes in topological order based on edges."""
|
|
1384
|
+
# Build adjacency and in-degree
|
|
1385
|
+
node_map = {n["id"]: n for n in nodes}
|
|
1386
|
+
in_degree = {n["id"]: 0 for n in nodes}
|
|
1387
|
+
children = {n["id"]: [] for n in nodes}
|
|
1388
|
+
|
|
1389
|
+
for e in edges:
|
|
1390
|
+
src, tgt = e.get("source"), e.get("target")
|
|
1391
|
+
if src in node_map and tgt in node_map:
|
|
1392
|
+
children[src].append(tgt)
|
|
1393
|
+
in_degree[tgt] = in_degree.get(tgt, 0) + 1
|
|
1394
|
+
|
|
1395
|
+
# Kahn's algorithm
|
|
1396
|
+
queue = [nid for nid, deg in in_degree.items() if deg == 0]
|
|
1397
|
+
result = []
|
|
1398
|
+
|
|
1399
|
+
while queue:
|
|
1400
|
+
nid = queue.pop(0)
|
|
1401
|
+
result.append(node_map[nid])
|
|
1402
|
+
for child in children.get(nid, []):
|
|
1403
|
+
in_degree[child] -= 1
|
|
1404
|
+
if in_degree[child] == 0:
|
|
1405
|
+
queue.append(child)
|
|
1406
|
+
|
|
1407
|
+
return result
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
async def flow_variables(
|
|
1411
|
+
client: AppliedClient,
|
|
1412
|
+
flow_id: str,
|
|
1413
|
+
node_id: str | None = None,
|
|
1414
|
+
) -> str:
|
|
1415
|
+
"""
|
|
1416
|
+
List available variables for a flow, showing what can be referenced in prompts
|
|
1417
|
+
and node configurations using {node_name.field} syntax.
|
|
1418
|
+
|
|
1419
|
+
Args:
|
|
1420
|
+
client: Authenticated AppliedClient
|
|
1421
|
+
flow_id: The flow UUID
|
|
1422
|
+
node_id: Optional - show only variables available to this specific node
|
|
1423
|
+
(predecessors in the graph). If omitted, shows all variables.
|
|
1424
|
+
|
|
1425
|
+
Returns:
|
|
1426
|
+
Formatted list of available variables grouped by source node,
|
|
1427
|
+
with paths, types, and descriptions.
|
|
1428
|
+
|
|
1429
|
+
Example output:
|
|
1430
|
+
## 1. trigger
|
|
1431
|
+
{trigger.message} string User's message
|
|
1432
|
+
{trigger.contact.email} string Contact email
|
|
1433
|
+
{trigger.shop.is_open} boolean Business hours check
|
|
1434
|
+
|
|
1435
|
+
## 2. http_request-abc123
|
|
1436
|
+
{http_request-abc123.data} object Response data
|
|
1437
|
+
{http_request-abc123.status_code} integer HTTP status
|
|
1438
|
+
"""
|
|
1439
|
+
flow = await client.get_flow(flow_id)
|
|
1440
|
+
graph = flow.get("graph", {})
|
|
1441
|
+
nodes = graph.get("nodes", [])
|
|
1442
|
+
edges = graph.get("edges", [])
|
|
1443
|
+
|
|
1444
|
+
# Sort nodes topologically
|
|
1445
|
+
sorted_nodes = _topological_sort(nodes, edges)
|
|
1446
|
+
|
|
1447
|
+
# If node_id specified, find its predecessors
|
|
1448
|
+
if node_id:
|
|
1449
|
+
# Build predecessor set via BFS backwards
|
|
1450
|
+
parents = {n["id"]: set() for n in nodes}
|
|
1451
|
+
for e in edges:
|
|
1452
|
+
tgt = e.get("target")
|
|
1453
|
+
src = e.get("source")
|
|
1454
|
+
if tgt and src:
|
|
1455
|
+
parents[tgt].add(src)
|
|
1456
|
+
|
|
1457
|
+
# BFS to find all ancestors
|
|
1458
|
+
ancestors = set()
|
|
1459
|
+
queue = list(parents.get(node_id, set()))
|
|
1460
|
+
while queue:
|
|
1461
|
+
p = queue.pop(0)
|
|
1462
|
+
if p not in ancestors:
|
|
1463
|
+
ancestors.add(p)
|
|
1464
|
+
queue.extend(parents.get(p, set()))
|
|
1465
|
+
|
|
1466
|
+
# Filter to only ancestor nodes
|
|
1467
|
+
sorted_nodes = [n for n in sorted_nodes if n["id"] in ancestors]
|
|
1468
|
+
|
|
1469
|
+
result = "# Available Variables for Flow\n"
|
|
1470
|
+
result += "Use {node_name.field} syntax in prompts and metadata.\n\n"
|
|
1471
|
+
|
|
1472
|
+
for idx, node in enumerate(sorted_nodes, 1):
|
|
1473
|
+
node_data = node.get("data", {})
|
|
1474
|
+
node_name = node_data.get("name", node.get("id", ""))
|
|
1475
|
+
node_type = node.get("type", node_data.get("executor_type", ""))
|
|
1476
|
+
outputs = node_data.get("outputs", {})
|
|
1477
|
+
|
|
1478
|
+
# Skip nodes without output schema
|
|
1479
|
+
if not outputs or not outputs.get("properties"):
|
|
1480
|
+
continue
|
|
1481
|
+
|
|
1482
|
+
result += f"## {idx}. {node_name}"
|
|
1483
|
+
if node_type and node_type != node_name:
|
|
1484
|
+
result += f" ({node_type})"
|
|
1485
|
+
result += "\n"
|
|
1486
|
+
|
|
1487
|
+
# Extract paths from schema
|
|
1488
|
+
paths = _extract_schema_paths(outputs)
|
|
1489
|
+
|
|
1490
|
+
for p in paths[:20]: # Limit to avoid overwhelming output
|
|
1491
|
+
var_path = f"{{{node_name}.{p['path']}}}"
|
|
1492
|
+
type_str = p["type"][:10].ljust(10)
|
|
1493
|
+
desc = p["description"][:40] if p["description"] else ""
|
|
1494
|
+
result += f" {var_path:<45} {type_str} {desc}\n"
|
|
1495
|
+
|
|
1496
|
+
if len(paths) > 20:
|
|
1497
|
+
result += f" ... and {len(paths) - 20} more fields\n"
|
|
1498
|
+
|
|
1499
|
+
result += "\n"
|
|
1500
|
+
|
|
1501
|
+
if not result.strip().endswith("\n\n"):
|
|
1502
|
+
result += "\n"
|
|
1503
|
+
|
|
1504
|
+
result += "## Built-in Trigger Context (llm.call trigger)\n"
|
|
1505
|
+
result += "The trigger node always provides these fields:\n\n"
|
|
1506
|
+
result += "**{trigger.shop}** - Workspace/shop object:\n"
|
|
1507
|
+
result += " - {trigger.shop.is_open} (boolean) - whether shop is currently open\n"
|
|
1508
|
+
result += " - {trigger.shop.name} (string)\n"
|
|
1509
|
+
result += " - {trigger.shop.timezone} (string)\n"
|
|
1510
|
+
result += " - {trigger.shop.address} (string)\n"
|
|
1511
|
+
result += "\n"
|
|
1512
|
+
result += "**{trigger.contact}** - Customer contact:\n"
|
|
1513
|
+
result += " - {trigger.contact.email} (string)\n"
|
|
1514
|
+
result += " - {trigger.contact.name} (string)\n"
|
|
1515
|
+
result += " - {trigger.contact.phone} (string)\n"
|
|
1516
|
+
result += "\n"
|
|
1517
|
+
result += "**{trigger.conversation}** - Current conversation:\n"
|
|
1518
|
+
result += " - {trigger.conversation.id} (string)\n"
|
|
1519
|
+
result += " - {trigger.conversation.title} (string)\n"
|
|
1520
|
+
result += " - {trigger.conversation.resolution} (string)\n"
|
|
1521
|
+
result += "\n"
|
|
1522
|
+
result += "**{trigger.messages}** - Message history (array)\n"
|
|
1523
|
+
result += "**{trigger.attachments}** - Uploaded files (array)\n"
|
|
1524
|
+
result += "**{trigger.current_time}** - ISO timestamp (string)\n"
|
|
1525
|
+
result += "\n"
|
|
1526
|
+
result += "## Tips\n"
|
|
1527
|
+
result += "- Use {trigger.shop.is_open} directly in branch conditions\n"
|
|
1528
|
+
result += "- Reference previous nodes by full name: {completion-abc.completion}\n"
|
|
1529
|
+
result += "- Access nested fields with dots: {trigger.contact.email}\n"
|
|
1530
|
+
result += "- Access array items: {trigger.messages[0].text}\n"
|
|
1531
|
+
|
|
1532
|
+
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|