kailash 0.1.4__py3-none-any.whl → 0.2.0__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/__init__.py +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +38 -0
- kailash/nodes/ai/a2a.py +1790 -0
- kailash/nodes/ai/agents.py +116 -2
- kailash/nodes/ai/ai_providers.py +206 -8
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +1623 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +116 -53
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/async_operations.py +48 -9
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +212 -27
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/processors.py +119 -4
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.4.dist-info/RECORD +0 -85
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,285 @@
|
|
1
|
+
"""
|
2
|
+
Custom Node API endpoints for Kailash Workflow Studio.
|
3
|
+
|
4
|
+
This module provides endpoints for users to:
|
5
|
+
- Create custom nodes with visual configuration
|
6
|
+
- Implement nodes using Python code, workflows, or API calls
|
7
|
+
- Manage and version custom nodes
|
8
|
+
- Share nodes within a tenant
|
9
|
+
"""
|
10
|
+
|
11
|
+
from datetime import datetime
|
12
|
+
from typing import Any, Dict, List, Optional
|
13
|
+
|
14
|
+
from pydantic import BaseModel, Field
|
15
|
+
|
16
|
+
|
17
|
+
class CustomNodeCreate(BaseModel):
|
18
|
+
"""Request model for creating a custom node"""
|
19
|
+
|
20
|
+
name: str = Field(..., min_length=1, max_length=255)
|
21
|
+
category: str = Field(default="custom", max_length=100)
|
22
|
+
description: Optional[str] = None
|
23
|
+
icon: Optional[str] = Field(None, max_length=50)
|
24
|
+
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
|
25
|
+
|
26
|
+
# Node configuration
|
27
|
+
parameters: List[Dict[str, Any]] = Field(default_factory=list)
|
28
|
+
inputs: List[Dict[str, Any]] = Field(default_factory=list)
|
29
|
+
outputs: List[Dict[str, Any]] = Field(default_factory=list)
|
30
|
+
|
31
|
+
# Implementation
|
32
|
+
implementation_type: str = Field(..., pattern="^(python|workflow|api)$")
|
33
|
+
implementation: Dict[str, Any]
|
34
|
+
|
35
|
+
|
36
|
+
class CustomNodeUpdate(BaseModel):
|
37
|
+
"""Request model for updating a custom node"""
|
38
|
+
|
39
|
+
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
40
|
+
category: Optional[str] = Field(None, max_length=100)
|
41
|
+
description: Optional[str] = None
|
42
|
+
icon: Optional[str] = Field(None, max_length=50)
|
43
|
+
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
|
44
|
+
|
45
|
+
# Node configuration
|
46
|
+
parameters: Optional[List[Dict[str, Any]]] = None
|
47
|
+
inputs: Optional[List[Dict[str, Any]]] = None
|
48
|
+
outputs: Optional[List[Dict[str, Any]]] = None
|
49
|
+
|
50
|
+
# Implementation
|
51
|
+
implementation_type: Optional[str] = Field(None, pattern="^(python|workflow|api)$")
|
52
|
+
implementation: Optional[Dict[str, Any]] = None
|
53
|
+
|
54
|
+
# Publishing
|
55
|
+
is_published: Optional[bool] = None
|
56
|
+
|
57
|
+
|
58
|
+
class CustomNodeResponse(BaseModel):
|
59
|
+
"""Response model for custom node"""
|
60
|
+
|
61
|
+
id: str
|
62
|
+
tenant_id: str
|
63
|
+
name: str
|
64
|
+
category: str
|
65
|
+
description: Optional[str]
|
66
|
+
icon: Optional[str]
|
67
|
+
color: Optional[str]
|
68
|
+
|
69
|
+
# Node configuration
|
70
|
+
parameters: List[Dict[str, Any]]
|
71
|
+
inputs: List[Dict[str, Any]]
|
72
|
+
outputs: List[Dict[str, Any]]
|
73
|
+
|
74
|
+
# Implementation
|
75
|
+
implementation_type: str
|
76
|
+
implementation: Dict[str, Any]
|
77
|
+
|
78
|
+
# Metadata
|
79
|
+
is_published: bool
|
80
|
+
created_by: Optional[str]
|
81
|
+
created_at: datetime
|
82
|
+
updated_at: datetime
|
83
|
+
|
84
|
+
|
85
|
+
def setup_custom_node_routes(app, SessionLocal, tenant_id: str):
|
86
|
+
"""Setup custom node API routes"""
|
87
|
+
from fastapi import HTTPException
|
88
|
+
|
89
|
+
from .database import CustomNodeRepository, get_db_session
|
90
|
+
|
91
|
+
@app.get("/api/custom-nodes", response_model=List[CustomNodeResponse])
|
92
|
+
async def list_custom_nodes():
|
93
|
+
"""List all custom nodes for the tenant"""
|
94
|
+
with get_db_session(SessionLocal) as session:
|
95
|
+
repo = CustomNodeRepository(session)
|
96
|
+
nodes = repo.list(tenant_id)
|
97
|
+
|
98
|
+
return [
|
99
|
+
CustomNodeResponse(
|
100
|
+
id=node.id,
|
101
|
+
tenant_id=node.tenant_id,
|
102
|
+
name=node.name,
|
103
|
+
category=node.category,
|
104
|
+
description=node.description,
|
105
|
+
icon=node.icon,
|
106
|
+
color=node.color,
|
107
|
+
parameters=node.parameters or [],
|
108
|
+
inputs=node.inputs or [],
|
109
|
+
outputs=node.outputs or [],
|
110
|
+
implementation_type=node.implementation_type,
|
111
|
+
implementation=node.implementation or {},
|
112
|
+
is_published=node.is_published,
|
113
|
+
created_by=node.created_by,
|
114
|
+
created_at=node.created_at,
|
115
|
+
updated_at=node.updated_at,
|
116
|
+
)
|
117
|
+
for node in nodes
|
118
|
+
]
|
119
|
+
|
120
|
+
@app.post("/api/custom-nodes", response_model=CustomNodeResponse)
|
121
|
+
async def create_custom_node(request: CustomNodeCreate):
|
122
|
+
"""Create a new custom node"""
|
123
|
+
with get_db_session(SessionLocal) as session:
|
124
|
+
repo = CustomNodeRepository(session)
|
125
|
+
|
126
|
+
# Check if node name already exists
|
127
|
+
existing_nodes = repo.list(tenant_id)
|
128
|
+
if any(node.name == request.name for node in existing_nodes):
|
129
|
+
raise HTTPException(
|
130
|
+
status_code=400,
|
131
|
+
detail=f"Custom node with name '{request.name}' already exists",
|
132
|
+
)
|
133
|
+
|
134
|
+
# Create node
|
135
|
+
node_data = request.dict()
|
136
|
+
node = repo.create(tenant_id, node_data)
|
137
|
+
|
138
|
+
return CustomNodeResponse(
|
139
|
+
id=node.id,
|
140
|
+
tenant_id=node.tenant_id,
|
141
|
+
name=node.name,
|
142
|
+
category=node.category,
|
143
|
+
description=node.description,
|
144
|
+
icon=node.icon,
|
145
|
+
color=node.color,
|
146
|
+
parameters=node.parameters or [],
|
147
|
+
inputs=node.inputs or [],
|
148
|
+
outputs=node.outputs or [],
|
149
|
+
implementation_type=node.implementation_type,
|
150
|
+
implementation=node.implementation or {},
|
151
|
+
is_published=node.is_published,
|
152
|
+
created_by=node.created_by,
|
153
|
+
created_at=node.created_at,
|
154
|
+
updated_at=node.updated_at,
|
155
|
+
)
|
156
|
+
|
157
|
+
@app.get("/api/custom-nodes/{node_id}", response_model=CustomNodeResponse)
|
158
|
+
async def get_custom_node(node_id: str):
|
159
|
+
"""Get a specific custom node"""
|
160
|
+
with get_db_session(SessionLocal) as session:
|
161
|
+
repo = CustomNodeRepository(session)
|
162
|
+
node = repo.get(node_id)
|
163
|
+
|
164
|
+
if not node or node.tenant_id != tenant_id:
|
165
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
166
|
+
|
167
|
+
return CustomNodeResponse(
|
168
|
+
id=node.id,
|
169
|
+
tenant_id=node.tenant_id,
|
170
|
+
name=node.name,
|
171
|
+
category=node.category,
|
172
|
+
description=node.description,
|
173
|
+
icon=node.icon,
|
174
|
+
color=node.color,
|
175
|
+
parameters=node.parameters or [],
|
176
|
+
inputs=node.inputs or [],
|
177
|
+
outputs=node.outputs or [],
|
178
|
+
implementation_type=node.implementation_type,
|
179
|
+
implementation=node.implementation or {},
|
180
|
+
is_published=node.is_published,
|
181
|
+
created_by=node.created_by,
|
182
|
+
created_at=node.created_at,
|
183
|
+
updated_at=node.updated_at,
|
184
|
+
)
|
185
|
+
|
186
|
+
@app.put("/api/custom-nodes/{node_id}", response_model=CustomNodeResponse)
|
187
|
+
async def update_custom_node(node_id: str, request: CustomNodeUpdate):
|
188
|
+
"""Update a custom node"""
|
189
|
+
with get_db_session(SessionLocal) as session:
|
190
|
+
repo = CustomNodeRepository(session)
|
191
|
+
node = repo.get(node_id)
|
192
|
+
|
193
|
+
if not node or node.tenant_id != tenant_id:
|
194
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
195
|
+
|
196
|
+
# Update node
|
197
|
+
updates = request.dict(exclude_unset=True)
|
198
|
+
node = repo.update(node_id, updates)
|
199
|
+
|
200
|
+
return CustomNodeResponse(
|
201
|
+
id=node.id,
|
202
|
+
tenant_id=node.tenant_id,
|
203
|
+
name=node.name,
|
204
|
+
category=node.category,
|
205
|
+
description=node.description,
|
206
|
+
icon=node.icon,
|
207
|
+
color=node.color,
|
208
|
+
parameters=node.parameters or [],
|
209
|
+
inputs=node.inputs or [],
|
210
|
+
outputs=node.outputs or [],
|
211
|
+
implementation_type=node.implementation_type,
|
212
|
+
implementation=node.implementation or {},
|
213
|
+
is_published=node.is_published,
|
214
|
+
created_by=node.created_by,
|
215
|
+
created_at=node.created_at,
|
216
|
+
updated_at=node.updated_at,
|
217
|
+
)
|
218
|
+
|
219
|
+
@app.delete("/api/custom-nodes/{node_id}")
|
220
|
+
async def delete_custom_node(node_id: str):
|
221
|
+
"""Delete a custom node"""
|
222
|
+
with get_db_session(SessionLocal) as session:
|
223
|
+
repo = CustomNodeRepository(session)
|
224
|
+
node = repo.get(node_id)
|
225
|
+
|
226
|
+
if not node or node.tenant_id != tenant_id:
|
227
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
228
|
+
|
229
|
+
repo.delete(node_id)
|
230
|
+
return {"message": "Custom node deleted successfully"}
|
231
|
+
|
232
|
+
@app.post("/api/custom-nodes/{node_id}/test")
|
233
|
+
async def test_custom_node(node_id: str, test_data: Dict[str, Any]):
|
234
|
+
"""Test a custom node with sample data"""
|
235
|
+
with get_db_session(SessionLocal) as session:
|
236
|
+
repo = CustomNodeRepository(session)
|
237
|
+
node = repo.get(node_id)
|
238
|
+
|
239
|
+
if not node or node.tenant_id != tenant_id:
|
240
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
241
|
+
|
242
|
+
# Execute node based on implementation type
|
243
|
+
try:
|
244
|
+
if node.implementation_type == "python":
|
245
|
+
# Execute Python code
|
246
|
+
result = _execute_python_node(node, test_data)
|
247
|
+
elif node.implementation_type == "workflow":
|
248
|
+
# Execute workflow
|
249
|
+
result = _execute_workflow_node(node, test_data)
|
250
|
+
elif node.implementation_type == "api":
|
251
|
+
# Execute API call
|
252
|
+
result = _execute_api_node(node, test_data)
|
253
|
+
else:
|
254
|
+
raise ValueError(
|
255
|
+
f"Unknown implementation type: {node.implementation_type}"
|
256
|
+
)
|
257
|
+
|
258
|
+
return {
|
259
|
+
"success": True,
|
260
|
+
"result": result,
|
261
|
+
"execution_time_ms": 0, # TODO: Track actual execution time
|
262
|
+
}
|
263
|
+
except Exception as e:
|
264
|
+
return {"success": False, "error": str(e), "execution_time_ms": 0}
|
265
|
+
|
266
|
+
|
267
|
+
def _execute_python_node(node, test_data):
|
268
|
+
"""Execute a Python-based custom node"""
|
269
|
+
# This would execute the Python code in a sandboxed environment
|
270
|
+
# For now, return mock result
|
271
|
+
return {"output": f"Executed {node.name} with Python implementation"}
|
272
|
+
|
273
|
+
|
274
|
+
def _execute_workflow_node(node, test_data):
|
275
|
+
"""Execute a workflow-based custom node"""
|
276
|
+
# This would create and execute a workflow from the stored definition
|
277
|
+
# For now, return mock result
|
278
|
+
return {"output": f"Executed {node.name} with Workflow implementation"}
|
279
|
+
|
280
|
+
|
281
|
+
def _execute_api_node(node, test_data):
|
282
|
+
"""Execute an API-based custom node"""
|
283
|
+
# This would make HTTP requests based on the API configuration
|
284
|
+
# For now, return mock result
|
285
|
+
return {"output": f"Executed {node.name} with API implementation"}
|
@@ -0,0 +1,377 @@
|
|
1
|
+
"""
|
2
|
+
Custom Node API endpoints for Kailash Workflow Studio with authentication.
|
3
|
+
|
4
|
+
This module provides secure endpoints for users to:
|
5
|
+
- Create custom nodes with visual configuration
|
6
|
+
- Implement nodes using Python code, workflows, or API calls
|
7
|
+
- Manage and version custom nodes
|
8
|
+
- Share nodes within a tenant
|
9
|
+
"""
|
10
|
+
|
11
|
+
from datetime import datetime
|
12
|
+
from typing import Any, Dict, List, Optional
|
13
|
+
|
14
|
+
from fastapi import Depends, HTTPException
|
15
|
+
from pydantic import BaseModel, Field
|
16
|
+
from sqlalchemy.orm import Session
|
17
|
+
|
18
|
+
from .auth import Tenant, User, get_current_tenant, require_permission
|
19
|
+
from .database import CustomNode, CustomNodeRepository, get_db_session
|
20
|
+
|
21
|
+
|
22
|
+
class CustomNodeCreate(BaseModel):
|
23
|
+
"""Request model for creating a custom node"""
|
24
|
+
|
25
|
+
name: str = Field(..., min_length=1, max_length=255)
|
26
|
+
category: str = Field(default="custom", max_length=100)
|
27
|
+
description: Optional[str] = None
|
28
|
+
icon: Optional[str] = Field(None, max_length=50)
|
29
|
+
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
|
30
|
+
|
31
|
+
# Node configuration
|
32
|
+
parameters: List[Dict[str, Any]] = Field(default_factory=list)
|
33
|
+
inputs: List[Dict[str, Any]] = Field(default_factory=list)
|
34
|
+
outputs: List[Dict[str, Any]] = Field(default_factory=list)
|
35
|
+
|
36
|
+
# Implementation
|
37
|
+
implementation_type: str = Field(..., pattern="^(python|workflow|api)$")
|
38
|
+
implementation: Dict[str, Any]
|
39
|
+
|
40
|
+
|
41
|
+
class CustomNodeUpdate(BaseModel):
|
42
|
+
"""Request model for updating a custom node"""
|
43
|
+
|
44
|
+
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
45
|
+
category: Optional[str] = Field(None, max_length=100)
|
46
|
+
description: Optional[str] = None
|
47
|
+
icon: Optional[str] = Field(None, max_length=50)
|
48
|
+
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
|
49
|
+
|
50
|
+
# Node configuration
|
51
|
+
parameters: Optional[List[Dict[str, Any]]] = None
|
52
|
+
inputs: Optional[List[Dict[str, Any]]] = None
|
53
|
+
outputs: Optional[List[Dict[str, Any]]] = None
|
54
|
+
|
55
|
+
# Implementation
|
56
|
+
implementation_type: Optional[str] = Field(None, pattern="^(python|workflow|api)$")
|
57
|
+
implementation: Optional[Dict[str, Any]] = None
|
58
|
+
|
59
|
+
# Publishing
|
60
|
+
is_published: Optional[bool] = None
|
61
|
+
|
62
|
+
|
63
|
+
class CustomNodeResponse(BaseModel):
|
64
|
+
"""Response model for custom node"""
|
65
|
+
|
66
|
+
id: str
|
67
|
+
tenant_id: str
|
68
|
+
name: str
|
69
|
+
category: str
|
70
|
+
description: Optional[str]
|
71
|
+
icon: Optional[str]
|
72
|
+
color: Optional[str]
|
73
|
+
|
74
|
+
# Node configuration
|
75
|
+
parameters: List[Dict[str, Any]]
|
76
|
+
inputs: List[Dict[str, Any]]
|
77
|
+
outputs: List[Dict[str, Any]]
|
78
|
+
|
79
|
+
# Implementation
|
80
|
+
implementation_type: str
|
81
|
+
implementation: Dict[str, Any]
|
82
|
+
|
83
|
+
# Metadata
|
84
|
+
is_published: bool
|
85
|
+
created_by: Optional[str]
|
86
|
+
created_at: datetime
|
87
|
+
updated_at: datetime
|
88
|
+
|
89
|
+
|
90
|
+
def setup_custom_node_routes(app, SessionLocal):
|
91
|
+
"""Setup custom node API routes with authentication"""
|
92
|
+
|
93
|
+
@app.get("/api/custom-nodes", response_model=List[CustomNodeResponse])
|
94
|
+
async def list_custom_nodes(
|
95
|
+
user: User = Depends(require_permission("read:nodes")),
|
96
|
+
tenant: Tenant = Depends(get_current_tenant),
|
97
|
+
session: Session = Depends(get_db_session),
|
98
|
+
):
|
99
|
+
"""List all custom nodes for the tenant"""
|
100
|
+
repo = CustomNodeRepository(session)
|
101
|
+
nodes = repo.list(tenant.id)
|
102
|
+
|
103
|
+
return [
|
104
|
+
CustomNodeResponse(
|
105
|
+
id=node.id,
|
106
|
+
tenant_id=node.tenant_id,
|
107
|
+
name=node.name,
|
108
|
+
category=node.category,
|
109
|
+
description=node.description,
|
110
|
+
icon=node.icon,
|
111
|
+
color=node.color,
|
112
|
+
parameters=node.parameters or [],
|
113
|
+
inputs=node.inputs or [],
|
114
|
+
outputs=node.outputs or [],
|
115
|
+
implementation_type=node.implementation_type,
|
116
|
+
implementation=node.implementation or {},
|
117
|
+
is_published=node.is_published,
|
118
|
+
created_by=node.created_by,
|
119
|
+
created_at=node.created_at,
|
120
|
+
updated_at=node.updated_at,
|
121
|
+
)
|
122
|
+
for node in nodes
|
123
|
+
]
|
124
|
+
|
125
|
+
@app.post("/api/custom-nodes", response_model=CustomNodeResponse)
|
126
|
+
async def create_custom_node(
|
127
|
+
request: CustomNodeCreate,
|
128
|
+
user: User = Depends(require_permission("write:nodes")),
|
129
|
+
tenant: Tenant = Depends(get_current_tenant),
|
130
|
+
session: Session = Depends(get_db_session),
|
131
|
+
):
|
132
|
+
"""Create a new custom node"""
|
133
|
+
repo = CustomNodeRepository(session)
|
134
|
+
|
135
|
+
# Check if node name already exists for this tenant
|
136
|
+
existing_nodes = repo.list(tenant.id)
|
137
|
+
if any(node.name == request.name for node in existing_nodes):
|
138
|
+
raise HTTPException(
|
139
|
+
status_code=400,
|
140
|
+
detail=f"Custom node with name '{request.name}' already exists",
|
141
|
+
)
|
142
|
+
|
143
|
+
# Create node
|
144
|
+
node_data = request.dict()
|
145
|
+
node_data["created_by"] = user.email
|
146
|
+
node = repo.create(tenant.id, node_data)
|
147
|
+
|
148
|
+
return CustomNodeResponse(
|
149
|
+
id=node.id,
|
150
|
+
tenant_id=node.tenant_id,
|
151
|
+
name=node.name,
|
152
|
+
category=node.category,
|
153
|
+
description=node.description,
|
154
|
+
icon=node.icon,
|
155
|
+
color=node.color,
|
156
|
+
parameters=node.parameters or [],
|
157
|
+
inputs=node.inputs or [],
|
158
|
+
outputs=node.outputs or [],
|
159
|
+
implementation_type=node.implementation_type,
|
160
|
+
implementation=node.implementation or {},
|
161
|
+
is_published=node.is_published,
|
162
|
+
created_by=node.created_by,
|
163
|
+
created_at=node.created_at,
|
164
|
+
updated_at=node.updated_at,
|
165
|
+
)
|
166
|
+
|
167
|
+
@app.get("/api/custom-nodes/{node_id}", response_model=CustomNodeResponse)
|
168
|
+
async def get_custom_node(
|
169
|
+
node_id: str,
|
170
|
+
user: User = Depends(require_permission("read:nodes")),
|
171
|
+
tenant: Tenant = Depends(get_current_tenant),
|
172
|
+
session: Session = Depends(get_db_session),
|
173
|
+
):
|
174
|
+
"""Get a specific custom node"""
|
175
|
+
repo = CustomNodeRepository(session)
|
176
|
+
node = repo.get(node_id)
|
177
|
+
|
178
|
+
if not node or node.tenant_id != tenant.id:
|
179
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
180
|
+
|
181
|
+
return CustomNodeResponse(
|
182
|
+
id=node.id,
|
183
|
+
tenant_id=node.tenant_id,
|
184
|
+
name=node.name,
|
185
|
+
category=node.category,
|
186
|
+
description=node.description,
|
187
|
+
icon=node.icon,
|
188
|
+
color=node.color,
|
189
|
+
parameters=node.parameters or [],
|
190
|
+
inputs=node.inputs or [],
|
191
|
+
outputs=node.outputs or [],
|
192
|
+
implementation_type=node.implementation_type,
|
193
|
+
implementation=node.implementation or {},
|
194
|
+
is_published=node.is_published,
|
195
|
+
created_by=node.created_by,
|
196
|
+
created_at=node.created_at,
|
197
|
+
updated_at=node.updated_at,
|
198
|
+
)
|
199
|
+
|
200
|
+
@app.put("/api/custom-nodes/{node_id}", response_model=CustomNodeResponse)
|
201
|
+
async def update_custom_node(
|
202
|
+
node_id: str,
|
203
|
+
request: CustomNodeUpdate,
|
204
|
+
user: User = Depends(require_permission("write:nodes")),
|
205
|
+
tenant: Tenant = Depends(get_current_tenant),
|
206
|
+
session: Session = Depends(get_db_session),
|
207
|
+
):
|
208
|
+
"""Update a custom node"""
|
209
|
+
repo = CustomNodeRepository(session)
|
210
|
+
node = repo.get(node_id)
|
211
|
+
|
212
|
+
if not node or node.tenant_id != tenant.id:
|
213
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
214
|
+
|
215
|
+
# Update node
|
216
|
+
updates = request.dict(exclude_unset=True)
|
217
|
+
node = repo.update(node_id, updates)
|
218
|
+
|
219
|
+
return CustomNodeResponse(
|
220
|
+
id=node.id,
|
221
|
+
tenant_id=node.tenant_id,
|
222
|
+
name=node.name,
|
223
|
+
category=node.category,
|
224
|
+
description=node.description,
|
225
|
+
icon=node.icon,
|
226
|
+
color=node.color,
|
227
|
+
parameters=node.parameters or [],
|
228
|
+
inputs=node.inputs or [],
|
229
|
+
outputs=node.outputs or [],
|
230
|
+
implementation_type=node.implementation_type,
|
231
|
+
implementation=node.implementation or {},
|
232
|
+
is_published=node.is_published,
|
233
|
+
created_by=node.created_by,
|
234
|
+
created_at=node.created_at,
|
235
|
+
updated_at=node.updated_at,
|
236
|
+
)
|
237
|
+
|
238
|
+
@app.delete("/api/custom-nodes/{node_id}")
|
239
|
+
async def delete_custom_node(
|
240
|
+
node_id: str,
|
241
|
+
user: User = Depends(require_permission("delete:nodes")),
|
242
|
+
tenant: Tenant = Depends(get_current_tenant),
|
243
|
+
session: Session = Depends(get_db_session),
|
244
|
+
):
|
245
|
+
"""Delete a custom node"""
|
246
|
+
repo = CustomNodeRepository(session)
|
247
|
+
node = repo.get(node_id)
|
248
|
+
|
249
|
+
if not node or node.tenant_id != tenant.id:
|
250
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
251
|
+
|
252
|
+
repo.delete(node_id)
|
253
|
+
return {"message": "Custom node deleted successfully"}
|
254
|
+
|
255
|
+
@app.post("/api/custom-nodes/{node_id}/test")
|
256
|
+
async def test_custom_node(
|
257
|
+
node_id: str,
|
258
|
+
test_data: Dict[str, Any],
|
259
|
+
user: User = Depends(require_permission("execute:nodes")),
|
260
|
+
tenant: Tenant = Depends(get_current_tenant),
|
261
|
+
session: Session = Depends(get_db_session),
|
262
|
+
):
|
263
|
+
"""Test a custom node with sample data"""
|
264
|
+
repo = CustomNodeRepository(session)
|
265
|
+
node = repo.get(node_id)
|
266
|
+
|
267
|
+
if not node or node.tenant_id != tenant.id:
|
268
|
+
raise HTTPException(status_code=404, detail="Custom node not found")
|
269
|
+
|
270
|
+
# Execute node based on implementation type
|
271
|
+
try:
|
272
|
+
if node.implementation_type == "python":
|
273
|
+
# Execute Python code in sandboxed environment
|
274
|
+
result = await _execute_python_node(node, test_data, tenant.id)
|
275
|
+
elif node.implementation_type == "workflow":
|
276
|
+
# Execute workflow
|
277
|
+
result = await _execute_workflow_node(node, test_data, tenant.id)
|
278
|
+
elif node.implementation_type == "api":
|
279
|
+
# Execute API call
|
280
|
+
result = await _execute_api_node(node, test_data, tenant.id)
|
281
|
+
else:
|
282
|
+
raise ValueError(
|
283
|
+
f"Unknown implementation type: {node.implementation_type}"
|
284
|
+
)
|
285
|
+
|
286
|
+
return {
|
287
|
+
"success": True,
|
288
|
+
"result": result,
|
289
|
+
"execution_time_ms": 0, # TODO: Track actual execution time
|
290
|
+
}
|
291
|
+
except Exception as e:
|
292
|
+
return {"success": False, "error": str(e), "execution_time_ms": 0}
|
293
|
+
|
294
|
+
|
295
|
+
async def _execute_python_node(
|
296
|
+
node: CustomNode, test_data: Dict[str, Any], tenant_id: str
|
297
|
+
) -> Dict[str, Any]:
|
298
|
+
"""Execute a Python-based custom node with security sandboxing"""
|
299
|
+
from kailash.nodes.code.python import PythonCodeNode
|
300
|
+
from kailash.security import SecurityConfig, TenantContext
|
301
|
+
|
302
|
+
# Create security config for tenant
|
303
|
+
security_config = SecurityConfig(
|
304
|
+
allowed_directories=[f"tenants/{tenant_id}/sandbox"],
|
305
|
+
execution_timeout=30.0, # 30 seconds max
|
306
|
+
memory_limit=256 * 1024 * 1024, # 256MB
|
307
|
+
)
|
308
|
+
|
309
|
+
# Execute in tenant context
|
310
|
+
with TenantContext(tenant_id):
|
311
|
+
# Create Python code node with custom implementation
|
312
|
+
python_node = PythonCodeNode(
|
313
|
+
code=node.implementation.get("code", ""),
|
314
|
+
inputs=node.implementation.get("inputs", []),
|
315
|
+
outputs=node.implementation.get("outputs", []),
|
316
|
+
security_config=security_config,
|
317
|
+
)
|
318
|
+
|
319
|
+
# Run the node
|
320
|
+
result = python_node.run(**test_data)
|
321
|
+
|
322
|
+
return result
|
323
|
+
|
324
|
+
|
325
|
+
async def _execute_workflow_node(
|
326
|
+
node: CustomNode, test_data: Dict[str, Any], tenant_id: str
|
327
|
+
) -> Dict[str, Any]:
|
328
|
+
"""Execute a workflow-based custom node"""
|
329
|
+
from kailash.runtime.local import LocalRuntime
|
330
|
+
from kailash.security import TenantContext
|
331
|
+
from kailash.workflow import Workflow
|
332
|
+
|
333
|
+
# Execute in tenant context
|
334
|
+
with TenantContext(tenant_id):
|
335
|
+
# Create workflow from stored definition
|
336
|
+
workflow_def = node.implementation.get("workflow", {})
|
337
|
+
workflow = Workflow.from_dict(workflow_def)
|
338
|
+
|
339
|
+
# Create tenant-isolated runtime
|
340
|
+
runtime = LocalRuntime()
|
341
|
+
|
342
|
+
# Execute workflow
|
343
|
+
result, run_id = runtime.execute(workflow, parameters=test_data)
|
344
|
+
|
345
|
+
return result
|
346
|
+
|
347
|
+
|
348
|
+
async def _execute_api_node(
|
349
|
+
node: CustomNode, test_data: Dict[str, Any], tenant_id: str
|
350
|
+
) -> Dict[str, Any]:
|
351
|
+
"""Execute an API-based custom node"""
|
352
|
+
|
353
|
+
from kailash.nodes.api.http import HTTPClientNode
|
354
|
+
from kailash.security import TenantContext
|
355
|
+
|
356
|
+
# Execute in tenant context
|
357
|
+
with TenantContext(tenant_id):
|
358
|
+
# Get API configuration
|
359
|
+
api_config = node.implementation.get("api", {})
|
360
|
+
|
361
|
+
# Create HTTP client node
|
362
|
+
http_node = HTTPClientNode(
|
363
|
+
url=api_config.get("url", ""),
|
364
|
+
method=api_config.get("method", "GET"),
|
365
|
+
headers=api_config.get("headers", {}),
|
366
|
+
timeout=api_config.get("timeout", 30),
|
367
|
+
)
|
368
|
+
|
369
|
+
# Prepare request data
|
370
|
+
if api_config.get("method") in ["POST", "PUT", "PATCH"]:
|
371
|
+
# Include test data in body
|
372
|
+
result = await http_node.run(json_data=test_data)
|
373
|
+
else:
|
374
|
+
# Include test data as query params
|
375
|
+
result = await http_node.run(params=test_data)
|
376
|
+
|
377
|
+
return result
|