kailash 0.1.1__py3-none-any.whl → 0.1.3__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.
- kailash/api/__init__.py +7 -0
- kailash/api/workflow_api.py +383 -0
- kailash/nodes/__init__.py +2 -1
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/ai_providers.py +1272 -0
- kailash/nodes/ai/embedding_generator.py +853 -0
- kailash/nodes/ai/llm_agent.py +1166 -0
- kailash/nodes/api/auth.py +3 -3
- kailash/nodes/api/graphql.py +2 -2
- kailash/nodes/api/http.py +391 -48
- kailash/nodes/api/rate_limiting.py +2 -2
- kailash/nodes/api/rest.py +465 -57
- kailash/nodes/base.py +71 -12
- kailash/nodes/code/python.py +2 -1
- kailash/nodes/data/__init__.py +7 -0
- kailash/nodes/data/readers.py +28 -26
- kailash/nodes/data/retrieval.py +178 -0
- kailash/nodes/data/sharepoint_graph.py +7 -7
- kailash/nodes/data/sources.py +65 -0
- kailash/nodes/data/sql.py +7 -5
- kailash/nodes/data/vector_db.py +2 -2
- kailash/nodes/data/writers.py +6 -3
- kailash/nodes/logic/__init__.py +2 -1
- kailash/nodes/logic/operations.py +2 -1
- kailash/nodes/logic/workflow.py +439 -0
- kailash/nodes/mcp/__init__.py +11 -0
- kailash/nodes/mcp/client.py +558 -0
- kailash/nodes/mcp/resource.py +682 -0
- kailash/nodes/mcp/server.py +577 -0
- kailash/nodes/transform/__init__.py +16 -1
- kailash/nodes/transform/chunkers.py +78 -0
- kailash/nodes/transform/formatters.py +96 -0
- kailash/nodes/transform/processors.py +5 -3
- kailash/runtime/docker.py +8 -6
- kailash/sdk_exceptions.py +24 -10
- kailash/tracking/metrics_collector.py +2 -1
- kailash/tracking/models.py +0 -20
- kailash/tracking/storage/database.py +4 -4
- kailash/tracking/storage/filesystem.py +0 -1
- kailash/utils/templates.py +6 -6
- kailash/visualization/performance.py +7 -7
- kailash/visualization/reports.py +1 -1
- kailash/workflow/graph.py +4 -4
- kailash/workflow/mock_registry.py +1 -1
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/METADATA +441 -47
- kailash-0.1.3.dist-info/RECORD +83 -0
- kailash-0.1.1.dist-info/RECORD +0 -69
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/WHEEL +0 -0
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,439 @@
|
|
1
|
+
"""Workflow node for wrapping workflows as reusable components.
|
2
|
+
|
3
|
+
This module provides the WorkflowNode class that enables hierarchical workflow
|
4
|
+
composition by wrapping entire workflows as single nodes. This allows complex
|
5
|
+
workflows to be reused as building blocks in larger workflows.
|
6
|
+
|
7
|
+
Design Philosophy:
|
8
|
+
- Workflows as first-class components
|
9
|
+
- Hierarchical composition patterns
|
10
|
+
- Clean abstraction of complexity
|
11
|
+
- Consistent node interface
|
12
|
+
|
13
|
+
Key Features:
|
14
|
+
- Dynamic parameter discovery from entry nodes
|
15
|
+
- Multiple loading methods (instance, file, dict)
|
16
|
+
- Automatic output mapping from exit nodes
|
17
|
+
- Full compatibility with existing runtime
|
18
|
+
"""
|
19
|
+
|
20
|
+
import json
|
21
|
+
from pathlib import Path
|
22
|
+
from typing import Any, Dict, Optional
|
23
|
+
|
24
|
+
import yaml
|
25
|
+
|
26
|
+
from kailash.nodes.base import Node, NodeParameter, register_node
|
27
|
+
from kailash.sdk_exceptions import NodeConfigurationError, NodeExecutionError
|
28
|
+
from kailash.workflow.graph import Workflow
|
29
|
+
|
30
|
+
|
31
|
+
@register_node()
|
32
|
+
class WorkflowNode(Node):
|
33
|
+
"""A node that encapsulates and executes an entire workflow.
|
34
|
+
|
35
|
+
This node allows workflows to be composed hierarchically, where a complex
|
36
|
+
workflow can be used as a single node within another workflow. This enables
|
37
|
+
powerful composition patterns and reusability.
|
38
|
+
|
39
|
+
Design Philosophy:
|
40
|
+
- Workflows become reusable components
|
41
|
+
- Complex logic hidden behind simple interface
|
42
|
+
- Hierarchical composition of workflows
|
43
|
+
- Consistent with standard node behavior
|
44
|
+
|
45
|
+
Upstream Components:
|
46
|
+
- Parent workflows that use this node
|
47
|
+
- Workflow builders creating composite workflows
|
48
|
+
- CLI/API creating nested workflow structures
|
49
|
+
|
50
|
+
Downstream Usage:
|
51
|
+
- The wrapped workflow and all its nodes
|
52
|
+
- Runtime executing the inner workflow
|
53
|
+
- Results passed to subsequent nodes
|
54
|
+
|
55
|
+
Usage Patterns:
|
56
|
+
1. Direct workflow wrapping:
|
57
|
+
```python
|
58
|
+
inner_workflow = Workflow("data_processing")
|
59
|
+
# ... build workflow ...
|
60
|
+
node = WorkflowNode(workflow=inner_workflow)
|
61
|
+
```
|
62
|
+
|
63
|
+
2. Loading from file:
|
64
|
+
```python
|
65
|
+
node = WorkflowNode(workflow_path="workflows/processor.yaml")
|
66
|
+
```
|
67
|
+
|
68
|
+
3. Loading from dictionary:
|
69
|
+
```python
|
70
|
+
workflow_dict = {"nodes": {...}, "connections": [...]}
|
71
|
+
node = WorkflowNode(workflow_dict=workflow_dict)
|
72
|
+
```
|
73
|
+
|
74
|
+
Implementation Details:
|
75
|
+
- Parameters derived from workflow entry nodes
|
76
|
+
- Outputs mapped from workflow exit nodes
|
77
|
+
- Uses LocalRuntime for execution
|
78
|
+
- Validates workflow structure on load
|
79
|
+
|
80
|
+
Error Handling:
|
81
|
+
- Configuration errors for invalid workflows
|
82
|
+
- Execution errors wrapped with context
|
83
|
+
- Clear error messages for debugging
|
84
|
+
|
85
|
+
Side Effects:
|
86
|
+
- Executes entire workflow when run
|
87
|
+
- May create temporary files/state
|
88
|
+
- Logs execution progress
|
89
|
+
"""
|
90
|
+
|
91
|
+
def __init__(self, workflow: Optional[Workflow] = None, **kwargs):
|
92
|
+
"""Initialize the WorkflowNode.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
workflow: Optional workflow instance to wrap
|
96
|
+
**kwargs: Additional configuration including:
|
97
|
+
- workflow_path: Path to load workflow from file
|
98
|
+
- workflow_dict: Dictionary representation of workflow
|
99
|
+
- name: Display name for the node
|
100
|
+
- description: Node description
|
101
|
+
- input_mapping: Map node inputs to workflow inputs
|
102
|
+
- output_mapping: Map workflow outputs to node outputs
|
103
|
+
|
104
|
+
Raises:
|
105
|
+
NodeConfigurationError: If no workflow source provided or
|
106
|
+
if workflow loading fails
|
107
|
+
"""
|
108
|
+
# Store workflow configuration before parent init
|
109
|
+
self._workflow = workflow
|
110
|
+
self._workflow_path = kwargs.get("workflow_path")
|
111
|
+
self._workflow_dict = kwargs.get("workflow_dict")
|
112
|
+
self._input_mapping = kwargs.get("input_mapping", {})
|
113
|
+
self._output_mapping = kwargs.get("output_mapping", {})
|
114
|
+
|
115
|
+
# Initialize parent
|
116
|
+
super().__init__(**kwargs)
|
117
|
+
|
118
|
+
# Runtime will be created lazily to avoid circular imports
|
119
|
+
self._runtime = None
|
120
|
+
|
121
|
+
# Load workflow if not provided directly
|
122
|
+
if not self._workflow:
|
123
|
+
self._load_workflow()
|
124
|
+
|
125
|
+
def _validate_config(self):
|
126
|
+
"""Override validation for WorkflowNode.
|
127
|
+
|
128
|
+
WorkflowNode has dynamic parameters based on the wrapped workflow,
|
129
|
+
so we skip the strict validation that base Node does.
|
130
|
+
"""
|
131
|
+
# Skip parameter validation for WorkflowNode since parameters
|
132
|
+
# are dynamically determined from the wrapped workflow
|
133
|
+
pass
|
134
|
+
|
135
|
+
def _load_workflow(self):
|
136
|
+
"""Load workflow from path or dictionary.
|
137
|
+
|
138
|
+
Attempts to load the workflow from configured sources:
|
139
|
+
1. From file path (JSON or YAML)
|
140
|
+
2. From dictionary representation
|
141
|
+
|
142
|
+
Raises:
|
143
|
+
NodeConfigurationError: If no valid source or loading fails
|
144
|
+
"""
|
145
|
+
if self._workflow_path:
|
146
|
+
path = Path(self._workflow_path)
|
147
|
+
if not path.exists():
|
148
|
+
raise NodeConfigurationError(
|
149
|
+
f"Workflow file not found: {self._workflow_path}"
|
150
|
+
)
|
151
|
+
|
152
|
+
try:
|
153
|
+
if path.suffix == ".json":
|
154
|
+
with open(path, "r") as f:
|
155
|
+
data = json.load(f)
|
156
|
+
self._workflow = Workflow.from_dict(data)
|
157
|
+
elif path.suffix in [".yaml", ".yml"]:
|
158
|
+
with open(path, "r") as f:
|
159
|
+
data = yaml.safe_load(f)
|
160
|
+
self._workflow = Workflow.from_dict(data)
|
161
|
+
else:
|
162
|
+
raise NodeConfigurationError(
|
163
|
+
f"Unsupported workflow file format: {path.suffix}"
|
164
|
+
)
|
165
|
+
except Exception as e:
|
166
|
+
raise NodeConfigurationError(
|
167
|
+
f"Failed to load workflow from {path}: {e}"
|
168
|
+
) from e
|
169
|
+
|
170
|
+
elif self._workflow_dict:
|
171
|
+
try:
|
172
|
+
self._workflow = Workflow.from_dict(self._workflow_dict)
|
173
|
+
except Exception as e:
|
174
|
+
raise NodeConfigurationError(
|
175
|
+
f"Failed to load workflow from dictionary: {e}"
|
176
|
+
) from e
|
177
|
+
else:
|
178
|
+
raise NodeConfigurationError(
|
179
|
+
"WorkflowNode requires either 'workflow', 'workflow_path', "
|
180
|
+
"or 'workflow_dict' parameter"
|
181
|
+
)
|
182
|
+
|
183
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
184
|
+
"""Define parameters based on workflow entry nodes.
|
185
|
+
|
186
|
+
Analyzes the wrapped workflow to determine required inputs:
|
187
|
+
1. Finds entry nodes (no incoming connections)
|
188
|
+
2. Aggregates their parameters
|
189
|
+
3. Adds generic 'inputs' parameter for overrides
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Dictionary of parameters derived from workflow structure
|
193
|
+
"""
|
194
|
+
if not self._workflow:
|
195
|
+
# Default parameters if workflow not loaded yet
|
196
|
+
return {
|
197
|
+
"inputs": NodeParameter(
|
198
|
+
name="inputs",
|
199
|
+
type=dict,
|
200
|
+
required=False,
|
201
|
+
default={},
|
202
|
+
description="Input data for the workflow",
|
203
|
+
)
|
204
|
+
}
|
205
|
+
|
206
|
+
params = {}
|
207
|
+
|
208
|
+
# Find entry nodes (nodes with no incoming edges)
|
209
|
+
entry_nodes = []
|
210
|
+
for node_id in self._workflow.nodes:
|
211
|
+
if self._workflow.graph.in_degree(node_id) == 0:
|
212
|
+
entry_nodes.append(node_id)
|
213
|
+
|
214
|
+
# If custom input mapping provided, use that
|
215
|
+
if self._input_mapping:
|
216
|
+
for param_name, mapping in self._input_mapping.items():
|
217
|
+
params[param_name] = NodeParameter(
|
218
|
+
name=param_name,
|
219
|
+
type=mapping.get("type", Any),
|
220
|
+
required=mapping.get("required", True),
|
221
|
+
default=mapping.get("default"),
|
222
|
+
description=mapping.get("description", f"Input for {param_name}"),
|
223
|
+
)
|
224
|
+
else:
|
225
|
+
# Auto-discover from entry nodes
|
226
|
+
for node_id in entry_nodes:
|
227
|
+
node = self._workflow.get_node(node_id)
|
228
|
+
if node:
|
229
|
+
node_params = node.get_parameters()
|
230
|
+
for param_name, param_def in node_params.items():
|
231
|
+
# Create flattened parameter name
|
232
|
+
full_param_name = f"{node_id}_{param_name}"
|
233
|
+
params[full_param_name] = NodeParameter(
|
234
|
+
name=full_param_name,
|
235
|
+
type=param_def.type,
|
236
|
+
required=False, # Make all workflow parameters optional
|
237
|
+
default=param_def.default,
|
238
|
+
description=f"{node_id}: {param_def.description}",
|
239
|
+
)
|
240
|
+
|
241
|
+
# Always include generic inputs parameter
|
242
|
+
params["inputs"] = NodeParameter(
|
243
|
+
name="inputs",
|
244
|
+
type=dict,
|
245
|
+
required=False,
|
246
|
+
default={},
|
247
|
+
description="Additional input overrides for workflow nodes",
|
248
|
+
)
|
249
|
+
|
250
|
+
return params
|
251
|
+
|
252
|
+
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
253
|
+
"""Define output schema based on workflow exit nodes.
|
254
|
+
|
255
|
+
Analyzes the wrapped workflow to determine outputs:
|
256
|
+
1. Finds exit nodes (no outgoing connections)
|
257
|
+
2. Aggregates their output schemas
|
258
|
+
3. Includes general 'results' output
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
Dictionary of output parameters from workflow structure
|
262
|
+
"""
|
263
|
+
if not self._workflow:
|
264
|
+
return {
|
265
|
+
"results": NodeParameter(
|
266
|
+
name="results",
|
267
|
+
type=dict,
|
268
|
+
required=True,
|
269
|
+
description="Workflow execution results",
|
270
|
+
)
|
271
|
+
}
|
272
|
+
|
273
|
+
output_schema = {
|
274
|
+
"results": NodeParameter(
|
275
|
+
name="results",
|
276
|
+
type=dict,
|
277
|
+
required=True,
|
278
|
+
description="Complete workflow execution results by node",
|
279
|
+
)
|
280
|
+
}
|
281
|
+
|
282
|
+
# If custom output mapping provided, use that
|
283
|
+
if self._output_mapping:
|
284
|
+
for output_name, mapping in self._output_mapping.items():
|
285
|
+
output_schema[output_name] = NodeParameter(
|
286
|
+
name=output_name,
|
287
|
+
type=mapping.get("type", Any),
|
288
|
+
required=mapping.get("required", False),
|
289
|
+
description=mapping.get("description", f"Output {output_name}"),
|
290
|
+
)
|
291
|
+
else:
|
292
|
+
# Auto-discover from exit nodes
|
293
|
+
exit_nodes = []
|
294
|
+
for node_id in self._workflow.nodes:
|
295
|
+
if self._workflow.graph.out_degree(node_id) == 0:
|
296
|
+
exit_nodes.append(node_id)
|
297
|
+
|
298
|
+
for node_id in exit_nodes:
|
299
|
+
node = self._workflow.get_node(node_id)
|
300
|
+
if node and hasattr(node, "get_output_schema"):
|
301
|
+
try:
|
302
|
+
node_outputs = node.get_output_schema()
|
303
|
+
for output_name, output_def in node_outputs.items():
|
304
|
+
full_output_name = f"{node_id}_{output_name}"
|
305
|
+
output_schema[full_output_name] = NodeParameter(
|
306
|
+
name=full_output_name,
|
307
|
+
type=output_def.type,
|
308
|
+
required=False,
|
309
|
+
description=f"{node_id}: {output_def.description}",
|
310
|
+
)
|
311
|
+
except Exception:
|
312
|
+
# Skip nodes that fail to provide output schema
|
313
|
+
pass
|
314
|
+
|
315
|
+
return output_schema
|
316
|
+
|
317
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
318
|
+
"""Execute the wrapped workflow.
|
319
|
+
|
320
|
+
Executes the inner workflow with proper input mapping:
|
321
|
+
1. Maps node inputs to workflow node inputs
|
322
|
+
2. Executes workflow using LocalRuntime
|
323
|
+
3. Maps workflow outputs to node outputs
|
324
|
+
|
325
|
+
Args:
|
326
|
+
**kwargs: Input parameters for the workflow
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
Dictionary containing:
|
330
|
+
- results: Complete workflow execution results
|
331
|
+
- Mapped outputs from exit nodes
|
332
|
+
|
333
|
+
Raises:
|
334
|
+
NodeExecutionError: If workflow execution fails
|
335
|
+
"""
|
336
|
+
if not self._workflow:
|
337
|
+
raise NodeExecutionError("No workflow loaded")
|
338
|
+
|
339
|
+
# Prepare inputs for the workflow
|
340
|
+
workflow_inputs = {}
|
341
|
+
|
342
|
+
# Handle custom input mapping
|
343
|
+
if self._input_mapping:
|
344
|
+
for param_name, mapping in self._input_mapping.items():
|
345
|
+
if param_name in kwargs:
|
346
|
+
# mapping should specify target node and parameter
|
347
|
+
target_node = mapping.get("node")
|
348
|
+
target_param = mapping.get("parameter", param_name)
|
349
|
+
if target_node:
|
350
|
+
workflow_inputs.setdefault(target_node, {})[target_param] = (
|
351
|
+
kwargs[param_name]
|
352
|
+
)
|
353
|
+
else:
|
354
|
+
# Auto-map inputs based on parameter names
|
355
|
+
for key, value in kwargs.items():
|
356
|
+
if "_" in key and key != "inputs":
|
357
|
+
# Split node_id and param_name
|
358
|
+
parts = key.split("_", 1)
|
359
|
+
if len(parts) == 2:
|
360
|
+
node_id, param_name = parts
|
361
|
+
if node_id in self._workflow.nodes:
|
362
|
+
workflow_inputs.setdefault(node_id, {})[param_name] = value
|
363
|
+
|
364
|
+
# Add any additional inputs
|
365
|
+
if "inputs" in kwargs and isinstance(kwargs["inputs"], dict):
|
366
|
+
for node_id, node_inputs in kwargs["inputs"].items():
|
367
|
+
if node_id in self._workflow.nodes:
|
368
|
+
workflow_inputs.setdefault(node_id, {}).update(node_inputs)
|
369
|
+
|
370
|
+
try:
|
371
|
+
# Create runtime lazily to avoid circular imports
|
372
|
+
if self._runtime is None:
|
373
|
+
from kailash.runtime.local import LocalRuntime
|
374
|
+
|
375
|
+
self._runtime = LocalRuntime()
|
376
|
+
|
377
|
+
# Execute the workflow
|
378
|
+
self.logger.info(f"Executing wrapped workflow: {self._workflow.name}")
|
379
|
+
results, _ = self._runtime.execute(
|
380
|
+
self._workflow, parameters=workflow_inputs
|
381
|
+
)
|
382
|
+
|
383
|
+
# Process results
|
384
|
+
output = {"results": results}
|
385
|
+
|
386
|
+
# Handle custom output mapping
|
387
|
+
if self._output_mapping:
|
388
|
+
for output_name, mapping in self._output_mapping.items():
|
389
|
+
source_node = mapping.get("node")
|
390
|
+
source_output = mapping.get("output", output_name)
|
391
|
+
if source_node and source_node in results:
|
392
|
+
node_results = results[source_node]
|
393
|
+
if (
|
394
|
+
isinstance(node_results, dict)
|
395
|
+
and source_output in node_results
|
396
|
+
):
|
397
|
+
output[output_name] = node_results[source_output]
|
398
|
+
else:
|
399
|
+
# Auto-map outputs from exit nodes
|
400
|
+
for node_id in self._workflow.nodes:
|
401
|
+
if self._workflow.graph.out_degree(node_id) == 0:
|
402
|
+
if node_id in results:
|
403
|
+
node_results = results[node_id]
|
404
|
+
if isinstance(node_results, dict):
|
405
|
+
for key, value in node_results.items():
|
406
|
+
output[f"{node_id}_{key}"] = value
|
407
|
+
|
408
|
+
return output
|
409
|
+
|
410
|
+
except Exception as e:
|
411
|
+
self.logger.error(f"Workflow execution failed: {e}")
|
412
|
+
raise NodeExecutionError(f"Failed to execute wrapped workflow: {e}") from e
|
413
|
+
|
414
|
+
def to_dict(self) -> Dict[str, Any]:
|
415
|
+
"""Convert node to dictionary representation.
|
416
|
+
|
417
|
+
Serializes the WorkflowNode including its wrapped workflow
|
418
|
+
for persistence and export.
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
Dictionary containing node configuration and workflow
|
422
|
+
"""
|
423
|
+
base_dict = super().to_dict()
|
424
|
+
|
425
|
+
# Add workflow information
|
426
|
+
if self._workflow:
|
427
|
+
base_dict["wrapped_workflow"] = self._workflow.to_dict()
|
428
|
+
elif self._workflow_path:
|
429
|
+
base_dict["workflow_path"] = str(self._workflow_path)
|
430
|
+
elif self._workflow_dict:
|
431
|
+
base_dict["workflow_dict"] = self._workflow_dict
|
432
|
+
|
433
|
+
# Add mappings if present
|
434
|
+
if self._input_mapping:
|
435
|
+
base_dict["input_mapping"] = self._input_mapping
|
436
|
+
if self._output_mapping:
|
437
|
+
base_dict["output_mapping"] = self._output_mapping
|
438
|
+
|
439
|
+
return base_dict
|