@q1k-oss/btree-workflows 0.0.1
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.
- package/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- package/workflows/process-order-workflow.yaml +142 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python Activity Implementations for btree workflows
|
|
3
|
+
|
|
4
|
+
These activities handle data processing operations that benefit from Python's
|
|
5
|
+
superior data libraries (pandas, openpyxl, rapidfuzz).
|
|
6
|
+
|
|
7
|
+
Activities:
|
|
8
|
+
- parse_file: Parse CSV/Excel files into structured data
|
|
9
|
+
- generate_file: Generate CSV/Excel/JSON files from data
|
|
10
|
+
- execute_python_script: Execute Python code with blackboard access
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import tempfile
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
from temporalio import activity
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import numpy as np
|
|
20
|
+
from rapidfuzz import fuzz, process
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
# ParseFile Activity
|
|
25
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
@activity.defn
|
|
28
|
+
async def parse_file(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
Parse CSV/Excel file into structured data.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
request: Dictionary containing:
|
|
34
|
+
- file: Path to file
|
|
35
|
+
- format: File format (csv, xlsx, xls, auto)
|
|
36
|
+
- sheetName: Sheet name for Excel (optional)
|
|
37
|
+
- columnMapping: Column rename mapping (optional)
|
|
38
|
+
- options: Parse options (optional)
|
|
39
|
+
- skipRows: Number of rows to skip
|
|
40
|
+
- trim: Trim whitespace from values
|
|
41
|
+
- emptyAsNull: Treat empty strings as None
|
|
42
|
+
- dateColumns: Columns to parse as dates
|
|
43
|
+
- dateFormat: Date format string
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary containing:
|
|
47
|
+
- data: List of row dictionaries
|
|
48
|
+
- rowCount: Number of rows parsed
|
|
49
|
+
- columns: List of column names
|
|
50
|
+
"""
|
|
51
|
+
file_path = request.get("file")
|
|
52
|
+
format_type = request.get("format", "auto")
|
|
53
|
+
sheet_name = request.get("sheetName")
|
|
54
|
+
column_mapping = request.get("columnMapping", {})
|
|
55
|
+
options = request.get("options", {})
|
|
56
|
+
|
|
57
|
+
activity.logger.info(f"Parsing file: {file_path} (format: {format_type})")
|
|
58
|
+
|
|
59
|
+
# Auto-detect format from file extension
|
|
60
|
+
if format_type == "auto":
|
|
61
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
62
|
+
if ext in [".xlsx", ".xls"]:
|
|
63
|
+
format_type = "xlsx" if ext == ".xlsx" else "xls"
|
|
64
|
+
else:
|
|
65
|
+
format_type = "csv"
|
|
66
|
+
|
|
67
|
+
# Read file into DataFrame
|
|
68
|
+
try:
|
|
69
|
+
if format_type == "csv":
|
|
70
|
+
df = pd.read_csv(
|
|
71
|
+
file_path,
|
|
72
|
+
skiprows=options.get("skipRows", 0),
|
|
73
|
+
skipinitialspace=options.get("trim", True),
|
|
74
|
+
)
|
|
75
|
+
else: # xlsx or xls
|
|
76
|
+
df = pd.read_excel(
|
|
77
|
+
file_path,
|
|
78
|
+
sheet_name=sheet_name or 0,
|
|
79
|
+
skiprows=options.get("skipRows", 0),
|
|
80
|
+
engine="openpyxl" if format_type == "xlsx" else None,
|
|
81
|
+
)
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
raise ValueError(f"File not found: {file_path}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ValueError(f"Failed to read file {file_path}: {str(e)}")
|
|
86
|
+
|
|
87
|
+
# Apply column mapping (rename columns)
|
|
88
|
+
if column_mapping:
|
|
89
|
+
df = df.rename(columns=column_mapping)
|
|
90
|
+
|
|
91
|
+
# Trim whitespace from string columns
|
|
92
|
+
if options.get("trim", True):
|
|
93
|
+
for col in df.select_dtypes(include=["object"]).columns:
|
|
94
|
+
df[col] = df[col].apply(lambda x: x.strip() if isinstance(x, str) else x)
|
|
95
|
+
|
|
96
|
+
# Convert empty strings to None
|
|
97
|
+
if options.get("emptyAsNull", False):
|
|
98
|
+
df = df.replace("", None)
|
|
99
|
+
|
|
100
|
+
# Parse date columns
|
|
101
|
+
date_columns = options.get("dateColumns", [])
|
|
102
|
+
date_format = options.get("dateFormat")
|
|
103
|
+
for col in date_columns:
|
|
104
|
+
if col in df.columns:
|
|
105
|
+
try:
|
|
106
|
+
df[col] = pd.to_datetime(df[col], format=date_format)
|
|
107
|
+
# Convert to ISO format string for JSON serialization
|
|
108
|
+
df[col] = df[col].dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
activity.logger.warning(f"Failed to parse date column {col}: {e}")
|
|
111
|
+
|
|
112
|
+
# Replace NaN/NaT with None for JSON serialization
|
|
113
|
+
df = df.where(pd.notnull(df), None)
|
|
114
|
+
|
|
115
|
+
# Convert to list of dictionaries
|
|
116
|
+
data = df.to_dict("records")
|
|
117
|
+
|
|
118
|
+
activity.logger.info(f"Parsed {len(data)} rows, columns: {list(df.columns)}")
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"data": data,
|
|
122
|
+
"rowCount": len(data),
|
|
123
|
+
"columns": list(df.columns),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
# GenerateFile Activity
|
|
129
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
@activity.defn
|
|
132
|
+
async def generate_file(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Generate CSV/Excel/JSON file from data.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
request: Dictionary containing:
|
|
138
|
+
- format: Output format (csv, xlsx, json)
|
|
139
|
+
- data: List of row dictionaries
|
|
140
|
+
- columns: Column definitions (optional)
|
|
141
|
+
- header: Display header name
|
|
142
|
+
- key: Data key
|
|
143
|
+
- width: Column width (Excel only)
|
|
144
|
+
- filename: Output filename
|
|
145
|
+
- storage: Storage type (temp, persistent)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dictionary containing:
|
|
149
|
+
- filename: Generated filename
|
|
150
|
+
- contentType: MIME type
|
|
151
|
+
- size: File size in bytes
|
|
152
|
+
- path: Full file path
|
|
153
|
+
- url: Download URL (if persistent)
|
|
154
|
+
"""
|
|
155
|
+
format_type = request.get("format")
|
|
156
|
+
data = request.get("data", [])
|
|
157
|
+
columns = request.get("columns")
|
|
158
|
+
filename = request.get("filename")
|
|
159
|
+
storage = request.get("storage", "temp")
|
|
160
|
+
|
|
161
|
+
activity.logger.info(f"Generating {format_type} file: {filename} ({len(data)} rows)")
|
|
162
|
+
|
|
163
|
+
# Create DataFrame from data
|
|
164
|
+
df = pd.DataFrame(data)
|
|
165
|
+
|
|
166
|
+
# Reorder and rename columns if specified
|
|
167
|
+
if columns:
|
|
168
|
+
col_order = [c["key"] for c in columns if c["key"] in df.columns]
|
|
169
|
+
df = df[col_order]
|
|
170
|
+
header_map = {c["key"]: c["header"] for c in columns}
|
|
171
|
+
df = df.rename(columns=header_map)
|
|
172
|
+
|
|
173
|
+
# Determine output directory
|
|
174
|
+
if storage == "temp":
|
|
175
|
+
output_dir = tempfile.gettempdir()
|
|
176
|
+
else:
|
|
177
|
+
output_dir = os.environ.get("PERSISTENT_STORAGE", "/tmp/persistent")
|
|
178
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
file_path = os.path.join(output_dir, filename)
|
|
181
|
+
|
|
182
|
+
# Write file based on format
|
|
183
|
+
if format_type == "csv":
|
|
184
|
+
df.to_csv(file_path, index=False)
|
|
185
|
+
content_type = "text/csv"
|
|
186
|
+
elif format_type == "xlsx":
|
|
187
|
+
# Write with openpyxl for xlsx support
|
|
188
|
+
with pd.ExcelWriter(file_path, engine="openpyxl") as writer:
|
|
189
|
+
df.to_excel(writer, index=False, sheet_name="Sheet1")
|
|
190
|
+
|
|
191
|
+
# Apply column widths if specified
|
|
192
|
+
if columns:
|
|
193
|
+
worksheet = writer.sheets["Sheet1"]
|
|
194
|
+
for i, col_def in enumerate(columns):
|
|
195
|
+
if "width" in col_def:
|
|
196
|
+
# openpyxl uses 1-based column indexing
|
|
197
|
+
col_letter = chr(65 + i) # A, B, C, etc.
|
|
198
|
+
worksheet.column_dimensions[col_letter].width = col_def["width"]
|
|
199
|
+
|
|
200
|
+
content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
201
|
+
else: # json
|
|
202
|
+
df.to_json(file_path, orient="records", indent=2)
|
|
203
|
+
content_type = "application/json"
|
|
204
|
+
|
|
205
|
+
# Get file size
|
|
206
|
+
size = os.path.getsize(file_path)
|
|
207
|
+
|
|
208
|
+
# Generate URL for persistent storage
|
|
209
|
+
url = None
|
|
210
|
+
if storage == "persistent":
|
|
211
|
+
# In a real implementation, this would upload to S3/GCS and return a signed URL
|
|
212
|
+
url = f"/files/{filename}"
|
|
213
|
+
|
|
214
|
+
activity.logger.info(f"Generated file: {file_path} ({size} bytes)")
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"filename": filename,
|
|
218
|
+
"contentType": content_type,
|
|
219
|
+
"size": size,
|
|
220
|
+
"path": file_path,
|
|
221
|
+
"url": url,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
# PythonScript Activity
|
|
227
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
@activity.defn
|
|
230
|
+
async def execute_python_script(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
231
|
+
"""
|
|
232
|
+
Execute Python code with blackboard access.
|
|
233
|
+
|
|
234
|
+
The code has access to:
|
|
235
|
+
- bb: dict - Blackboard state (read/write)
|
|
236
|
+
- input: dict - Workflow input (read-only)
|
|
237
|
+
- env: dict - Environment variables
|
|
238
|
+
- pd: pandas module
|
|
239
|
+
- np: numpy module
|
|
240
|
+
- fuzz: rapidfuzz.fuzz module (for fuzzy string matching)
|
|
241
|
+
- process: rapidfuzz.process module (for fuzzy extraction)
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
request: Dictionary containing:
|
|
245
|
+
- code: Python code to execute
|
|
246
|
+
- blackboard: Current blackboard state
|
|
247
|
+
- input: Workflow input (optional)
|
|
248
|
+
- env: Environment variables (optional)
|
|
249
|
+
- timeout: Execution timeout in ms (optional)
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dictionary containing:
|
|
253
|
+
- blackboard: Modified blackboard state
|
|
254
|
+
- stdout: Captured stdout (if any)
|
|
255
|
+
- stderr: Captured stderr (if any)
|
|
256
|
+
"""
|
|
257
|
+
code = request.get("code", "")
|
|
258
|
+
bb = request.get("blackboard", {})
|
|
259
|
+
input_data = request.get("input", {})
|
|
260
|
+
env = request.get("env", {})
|
|
261
|
+
|
|
262
|
+
activity.logger.info(f"Executing Python script ({len(code)} chars)")
|
|
263
|
+
|
|
264
|
+
# Create execution context with available libraries
|
|
265
|
+
local_vars: Dict[str, Any] = {
|
|
266
|
+
"bb": bb,
|
|
267
|
+
"input": input_data,
|
|
268
|
+
"env": env,
|
|
269
|
+
"pd": pd,
|
|
270
|
+
"np": np,
|
|
271
|
+
"fuzz": fuzz,
|
|
272
|
+
"process": process,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Capture stdout/stderr
|
|
276
|
+
import io
|
|
277
|
+
import sys
|
|
278
|
+
|
|
279
|
+
stdout_capture = io.StringIO()
|
|
280
|
+
stderr_capture = io.StringIO()
|
|
281
|
+
|
|
282
|
+
old_stdout = sys.stdout
|
|
283
|
+
old_stderr = sys.stderr
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
sys.stdout = stdout_capture
|
|
287
|
+
sys.stderr = stderr_capture
|
|
288
|
+
|
|
289
|
+
# Execute user code
|
|
290
|
+
# Note: In production, consider using a sandboxed execution environment
|
|
291
|
+
exec(code, {"__builtins__": __builtins__}, local_vars)
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
activity.logger.error(f"Python script execution failed: {e}")
|
|
295
|
+
raise ValueError(f"Script execution failed: {str(e)}")
|
|
296
|
+
finally:
|
|
297
|
+
sys.stdout = old_stdout
|
|
298
|
+
sys.stderr = old_stderr
|
|
299
|
+
|
|
300
|
+
stdout_output = stdout_capture.getvalue()
|
|
301
|
+
stderr_output = stderr_capture.getvalue()
|
|
302
|
+
|
|
303
|
+
if stdout_output:
|
|
304
|
+
activity.logger.info(f"Script stdout: {stdout_output[:500]}")
|
|
305
|
+
if stderr_output:
|
|
306
|
+
activity.logger.warning(f"Script stderr: {stderr_output[:500]}")
|
|
307
|
+
|
|
308
|
+
# Convert any numpy/pandas types to JSON-serializable types
|
|
309
|
+
result_bb = _make_json_serializable(local_vars["bb"])
|
|
310
|
+
|
|
311
|
+
activity.logger.info(f"Script completed, {len(result_bb)} blackboard keys")
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
"blackboard": result_bb,
|
|
315
|
+
"stdout": stdout_output,
|
|
316
|
+
"stderr": stderr_output,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _make_json_serializable(obj: Any) -> Any:
|
|
321
|
+
"""Convert numpy/pandas types to JSON-serializable Python types."""
|
|
322
|
+
if isinstance(obj, dict):
|
|
323
|
+
return {k: _make_json_serializable(v) for k, v in obj.items()}
|
|
324
|
+
elif isinstance(obj, list):
|
|
325
|
+
return [_make_json_serializable(item) for item in obj]
|
|
326
|
+
elif isinstance(obj, pd.DataFrame):
|
|
327
|
+
return obj.to_dict("records")
|
|
328
|
+
elif isinstance(obj, pd.Series):
|
|
329
|
+
return obj.tolist()
|
|
330
|
+
elif isinstance(obj, np.ndarray):
|
|
331
|
+
return obj.tolist()
|
|
332
|
+
elif isinstance(obj, (np.integer, np.floating)):
|
|
333
|
+
return obj.item()
|
|
334
|
+
elif isinstance(obj, np.bool_):
|
|
335
|
+
return bool(obj)
|
|
336
|
+
elif pd.isna(obj):
|
|
337
|
+
return None
|
|
338
|
+
else:
|
|
339
|
+
return obj
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Python Worker for btree workflows
|
|
4
|
+
|
|
5
|
+
This worker handles data processing activities that benefit from Python's
|
|
6
|
+
superior data libraries (pandas, openpyxl, rapidfuzz).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
pip install -r requirements.txt
|
|
10
|
+
python worker.py
|
|
11
|
+
|
|
12
|
+
Environment variables:
|
|
13
|
+
TEMPORAL_HOST: Temporal server host (default: localhost:7233)
|
|
14
|
+
TEMPORAL_NAMESPACE: Temporal namespace (default: default)
|
|
15
|
+
TASK_QUEUE: Task queue name (default: btree-workflows)
|
|
16
|
+
PERSISTENT_STORAGE: Path for persistent file storage (default: /tmp/persistent)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import signal
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from temporalio.client import Client
|
|
26
|
+
from temporalio.worker import Worker
|
|
27
|
+
|
|
28
|
+
from activities import parse_file, generate_file, execute_python_script
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Configure logging
|
|
32
|
+
logging.basicConfig(
|
|
33
|
+
level=logging.INFO,
|
|
34
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
35
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
36
|
+
)
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
"""Main entry point for the Python worker."""
|
|
42
|
+
# Configuration from environment
|
|
43
|
+
temporal_host = os.environ.get("TEMPORAL_HOST", "localhost:7233")
|
|
44
|
+
namespace = os.environ.get("TEMPORAL_NAMESPACE", "default")
|
|
45
|
+
task_queue = os.environ.get("TASK_QUEUE", "btree-workflows")
|
|
46
|
+
|
|
47
|
+
logger.info(f"Connecting to Temporal at {temporal_host} (namespace: {namespace})")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Connect to Temporal
|
|
51
|
+
client = await Client.connect(temporal_host, namespace=namespace)
|
|
52
|
+
logger.info("Connected to Temporal server")
|
|
53
|
+
|
|
54
|
+
# Create worker with Python activities
|
|
55
|
+
worker = Worker(
|
|
56
|
+
client,
|
|
57
|
+
task_queue=task_queue,
|
|
58
|
+
activities=[
|
|
59
|
+
parse_file,
|
|
60
|
+
generate_file,
|
|
61
|
+
execute_python_script,
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.info(f"Starting Python worker on task queue: {task_queue}")
|
|
66
|
+
logger.info("Registered activities:")
|
|
67
|
+
logger.info(" - parse_file: Parse CSV/Excel files into structured data")
|
|
68
|
+
logger.info(" - generate_file: Generate CSV/Excel/JSON files from data")
|
|
69
|
+
logger.info(" - execute_python_script: Execute Python code with pandas/numpy")
|
|
70
|
+
|
|
71
|
+
# Handle graceful shutdown
|
|
72
|
+
shutdown_event = asyncio.Event()
|
|
73
|
+
|
|
74
|
+
def signal_handler(signum, frame):
|
|
75
|
+
logger.info(f"Received signal {signum}, initiating graceful shutdown...")
|
|
76
|
+
shutdown_event.set()
|
|
77
|
+
|
|
78
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
79
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
80
|
+
|
|
81
|
+
# Run worker until shutdown
|
|
82
|
+
async with worker:
|
|
83
|
+
logger.info("Python worker started successfully")
|
|
84
|
+
await shutdown_event.wait()
|
|
85
|
+
|
|
86
|
+
logger.info("Python worker shut down gracefully")
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Worker failed: {e}")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
print("""
|
|
95
|
+
╔═══════════════════════════════════════════════════════════════════════════════╗
|
|
96
|
+
║ btree Python Worker ║
|
|
97
|
+
║ ║
|
|
98
|
+
║ Activities: ║
|
|
99
|
+
║ - parse_file: Parse CSV/Excel files into structured data ║
|
|
100
|
+
║ - generate_file: Generate CSV/Excel/JSON files from data ║
|
|
101
|
+
║ - execute_python_script: Execute Python code with pandas/numpy/rapidfuzz ║
|
|
102
|
+
║ ║
|
|
103
|
+
║ Press Ctrl+C to stop ║
|
|
104
|
+
╚═══════════════════════════════════════════════════════════════════════════════╝
|
|
105
|
+
""")
|
|
106
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal Worker
|
|
3
|
+
* Registers and runs behavior tree workflows with activity support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NativeConnection, Worker, bundleWorkflowCode } from "@temporalio/worker";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
|
|
10
|
+
// Import activities
|
|
11
|
+
import * as activities from "./activities.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
async function run() {
|
|
17
|
+
console.log("🚀 Starting Temporal worker for behavior tree workflows...");
|
|
18
|
+
|
|
19
|
+
const connection = await NativeConnection.connect({
|
|
20
|
+
address: "localhost:7233",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Bundle workflows ahead of time with better control
|
|
24
|
+
console.log("📦 Bundling workflows...");
|
|
25
|
+
const { code } = await bundleWorkflowCode({
|
|
26
|
+
workflowsPath: join(__dirname, "workflows.ts"),
|
|
27
|
+
// Ignore modules that are used by btree but not needed in workflow context
|
|
28
|
+
// Note: 'vm' is used by js-interpreter but not at runtime in the workflow
|
|
29
|
+
ignoreModules: ["fs", "fs/promises", "path", "vm"],
|
|
30
|
+
webpackConfigHook: (config) => {
|
|
31
|
+
config.target = "webworker";
|
|
32
|
+
if (config.output) {
|
|
33
|
+
config.output.publicPath = "";
|
|
34
|
+
config.output.globalObject = "globalThis";
|
|
35
|
+
}
|
|
36
|
+
// Force single bundle without code splitting
|
|
37
|
+
config.optimization = {
|
|
38
|
+
minimize: false,
|
|
39
|
+
splitChunks: false,
|
|
40
|
+
runtimeChunk: false,
|
|
41
|
+
};
|
|
42
|
+
return config;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log("✅ Workflows bundled successfully");
|
|
47
|
+
|
|
48
|
+
const worker = await Worker.create({
|
|
49
|
+
connection,
|
|
50
|
+
namespace: "default",
|
|
51
|
+
workflowBundle: { code },
|
|
52
|
+
taskQueue: "btree-workflows",
|
|
53
|
+
activities, // Register activity implementations
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log("✅ Worker started successfully!");
|
|
57
|
+
console.log("📋 Task Queue: btree-workflows");
|
|
58
|
+
console.log("🔄 Listening for workflow tasks...\n");
|
|
59
|
+
|
|
60
|
+
await worker.run();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
run().catch((err) => {
|
|
64
|
+
console.error("❌ Worker error:", err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal YAML Workflow Loader
|
|
3
|
+
* Executes any YAML-defined workflow in Temporal with activity support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { proxyActivities } from "@temporalio/workflow";
|
|
7
|
+
import {
|
|
8
|
+
BehaviorTree,
|
|
9
|
+
Registry,
|
|
10
|
+
registerStandardNodes,
|
|
11
|
+
loadTreeFromYaml,
|
|
12
|
+
type WorkflowArgs,
|
|
13
|
+
type WorkflowResult,
|
|
14
|
+
type BtreeActivities,
|
|
15
|
+
type TokenProvider,
|
|
16
|
+
type PieceAuth,
|
|
17
|
+
} from "../../dist/index.js";
|
|
18
|
+
|
|
19
|
+
// Import activity types for proxy creation
|
|
20
|
+
import type * as activitiesModule from "./activities.js";
|
|
21
|
+
|
|
22
|
+
// Create activity proxies - these route calls to the activity worker
|
|
23
|
+
const activities = proxyActivities<typeof activitiesModule>({
|
|
24
|
+
startToCloseTimeout: "30s",
|
|
25
|
+
retry: {
|
|
26
|
+
maximumAttempts: 3,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Create the BtreeActivities object that nodes expect
|
|
31
|
+
const btreeActivities: BtreeActivities = {
|
|
32
|
+
executePieceAction: activities.executePieceActionActivity,
|
|
33
|
+
executePythonScript: activities.executePythonScriptActivity,
|
|
34
|
+
parseFile: activities.parseFileActivity,
|
|
35
|
+
generateFile: activities.generateFileActivity,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mock token provider for testing
|
|
40
|
+
* In production, this would fetch real OAuth tokens from controlplane
|
|
41
|
+
*/
|
|
42
|
+
const mockTokenProvider: TokenProvider = async (
|
|
43
|
+
_context,
|
|
44
|
+
provider,
|
|
45
|
+
_connectionId
|
|
46
|
+
): Promise<PieceAuth> => {
|
|
47
|
+
// For testing, return mock tokens
|
|
48
|
+
// In production, this would call controlplane to get real tokens
|
|
49
|
+
console.log(`[TokenProvider] Fetching token for provider: ${provider}`);
|
|
50
|
+
return {
|
|
51
|
+
access_token: `mock_token_for_${provider}_${Date.now()}`,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extended workflow args with YAML content
|
|
57
|
+
*/
|
|
58
|
+
export interface YamlWorkflowArgs extends WorkflowArgs {
|
|
59
|
+
yamlContent: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Universal YAML workflow executor
|
|
64
|
+
* Loads and executes any YAML workflow definition
|
|
65
|
+
*
|
|
66
|
+
* Usage:
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const result = await client.workflow.execute(yamlWorkflow, {
|
|
69
|
+
* args: [{
|
|
70
|
+
* input: {},
|
|
71
|
+
* treeRegistry: new Registry(),
|
|
72
|
+
* yamlContent: readFileSync('./my-workflow.yaml', 'utf-8')
|
|
73
|
+
* }]
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export async function yamlWorkflow(
|
|
78
|
+
args: YamlWorkflowArgs,
|
|
79
|
+
): Promise<WorkflowResult> {
|
|
80
|
+
if (!args.yamlContent) {
|
|
81
|
+
throw new Error("yamlContent is required in workflow arguments");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create registry and register all standard built-in nodes
|
|
85
|
+
const registry = new Registry();
|
|
86
|
+
registerStandardNodes(registry);
|
|
87
|
+
|
|
88
|
+
// Users can register custom nodes here:
|
|
89
|
+
// registry.register("MyCustomNode", MyCustomNode, { category: "action" });
|
|
90
|
+
|
|
91
|
+
// Parse and validate YAML
|
|
92
|
+
const root = loadTreeFromYaml(args.yamlContent, registry);
|
|
93
|
+
|
|
94
|
+
// Convert to Temporal workflow
|
|
95
|
+
const tree = new BehaviorTree(root);
|
|
96
|
+
const workflow = tree.toWorkflow();
|
|
97
|
+
|
|
98
|
+
// Execute with original args (without yamlContent), activities, and tokenProvider
|
|
99
|
+
return workflow({
|
|
100
|
+
input: args.input,
|
|
101
|
+
treeRegistry: args.treeRegistry,
|
|
102
|
+
activities: btreeActivities,
|
|
103
|
+
tokenProvider: mockTokenProvider,
|
|
104
|
+
});
|
|
105
|
+
}
|