kailash 0.1.5__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 +2 -0
- kailash/nodes/ai/a2a.py +714 -67
- kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +5 -6
- 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 +16 -6
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +187 -27
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- 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.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
- 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.5.dist-info/RECORD +0 -88
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
kailash/api/database.py
ADDED
@@ -0,0 +1,620 @@
|
|
1
|
+
"""
|
2
|
+
Database models and storage backend for Kailash Workflow Studio.
|
3
|
+
|
4
|
+
This module provides:
|
5
|
+
- SQLAlchemy models for workflows, nodes, executions, and user data
|
6
|
+
- Database initialization and migration support
|
7
|
+
- Repository classes for data access
|
8
|
+
"""
|
9
|
+
|
10
|
+
import uuid
|
11
|
+
from contextlib import contextmanager
|
12
|
+
from datetime import datetime, timezone
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any, Dict, List, Optional
|
15
|
+
|
16
|
+
from sqlalchemy import (
|
17
|
+
JSON,
|
18
|
+
Boolean,
|
19
|
+
Column,
|
20
|
+
DateTime,
|
21
|
+
ForeignKey,
|
22
|
+
Index,
|
23
|
+
Integer,
|
24
|
+
String,
|
25
|
+
Text,
|
26
|
+
create_engine,
|
27
|
+
)
|
28
|
+
from sqlalchemy.engine import Engine
|
29
|
+
from sqlalchemy.ext.declarative import declarative_base
|
30
|
+
from sqlalchemy.orm import Session, relationship, sessionmaker
|
31
|
+
from sqlalchemy.sql import func
|
32
|
+
|
33
|
+
Base = declarative_base()
|
34
|
+
|
35
|
+
|
36
|
+
class Workflow(Base):
|
37
|
+
"""Workflow database model"""
|
38
|
+
|
39
|
+
__tablename__ = "workflows"
|
40
|
+
|
41
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
42
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
43
|
+
name = Column(String(255), nullable=False)
|
44
|
+
description = Column(Text)
|
45
|
+
definition = Column(JSON, nullable=False)
|
46
|
+
version = Column(Integer, default=1)
|
47
|
+
is_published = Column(Boolean, default=False)
|
48
|
+
created_by = Column(String(255))
|
49
|
+
created_at = Column(DateTime, default=func.now())
|
50
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
51
|
+
|
52
|
+
# Relationships
|
53
|
+
executions = relationship(
|
54
|
+
"WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan"
|
55
|
+
)
|
56
|
+
versions = relationship(
|
57
|
+
"WorkflowVersion", back_populates="workflow", cascade="all, delete-orphan"
|
58
|
+
)
|
59
|
+
|
60
|
+
__table_args__ = (
|
61
|
+
Index("idx_workflow_tenant_created", "tenant_id", "created_at"),
|
62
|
+
Index("idx_workflow_tenant_name", "tenant_id", "name"),
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
class WorkflowVersion(Base):
|
67
|
+
"""Workflow version history"""
|
68
|
+
|
69
|
+
__tablename__ = "workflow_versions"
|
70
|
+
|
71
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
72
|
+
workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
|
73
|
+
version = Column(Integer, nullable=False)
|
74
|
+
definition = Column(JSON, nullable=False)
|
75
|
+
change_message = Column(Text)
|
76
|
+
created_by = Column(String(255))
|
77
|
+
created_at = Column(DateTime, default=func.now())
|
78
|
+
|
79
|
+
# Relationships
|
80
|
+
workflow = relationship("Workflow", back_populates="versions")
|
81
|
+
|
82
|
+
__table_args__ = (
|
83
|
+
Index("idx_version_workflow_version", "workflow_id", "version", unique=True),
|
84
|
+
)
|
85
|
+
|
86
|
+
|
87
|
+
class CustomNode(Base):
|
88
|
+
"""Custom node definitions created by users"""
|
89
|
+
|
90
|
+
__tablename__ = "custom_nodes"
|
91
|
+
|
92
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
93
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
94
|
+
name = Column(String(255), nullable=False)
|
95
|
+
category = Column(String(100), default="custom")
|
96
|
+
description = Column(Text)
|
97
|
+
icon = Column(String(50))
|
98
|
+
color = Column(String(7)) # Hex color
|
99
|
+
|
100
|
+
# Node configuration
|
101
|
+
parameters = Column(JSON) # Parameter definitions
|
102
|
+
inputs = Column(JSON) # Input port definitions
|
103
|
+
outputs = Column(JSON) # Output port definitions
|
104
|
+
|
105
|
+
# Implementation
|
106
|
+
implementation_type = Column(String(50)) # 'python', 'workflow', 'api'
|
107
|
+
implementation = Column(JSON) # Implementation details
|
108
|
+
|
109
|
+
# Metadata
|
110
|
+
is_published = Column(Boolean, default=False)
|
111
|
+
created_by = Column(String(255))
|
112
|
+
created_at = Column(DateTime, default=func.now())
|
113
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
114
|
+
|
115
|
+
__table_args__ = (
|
116
|
+
Index("idx_node_tenant_name", "tenant_id", "name", unique=True),
|
117
|
+
Index("idx_node_tenant_category", "tenant_id", "category"),
|
118
|
+
)
|
119
|
+
|
120
|
+
|
121
|
+
class WorkflowExecution(Base):
|
122
|
+
"""Workflow execution history"""
|
123
|
+
|
124
|
+
__tablename__ = "workflow_executions"
|
125
|
+
|
126
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
127
|
+
workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
|
128
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
129
|
+
status = Column(
|
130
|
+
String(50), nullable=False
|
131
|
+
) # 'pending', 'running', 'completed', 'failed'
|
132
|
+
|
133
|
+
# Execution details
|
134
|
+
parameters = Column(JSON)
|
135
|
+
result = Column(JSON)
|
136
|
+
error = Column(Text)
|
137
|
+
|
138
|
+
# Performance metrics
|
139
|
+
started_at = Column(DateTime)
|
140
|
+
completed_at = Column(DateTime)
|
141
|
+
execution_time_ms = Column(Integer)
|
142
|
+
|
143
|
+
# Node execution details
|
144
|
+
node_executions = Column(JSON) # Detailed per-node execution data
|
145
|
+
|
146
|
+
# Relationships
|
147
|
+
workflow = relationship("Workflow", back_populates="executions")
|
148
|
+
|
149
|
+
__table_args__ = (
|
150
|
+
Index("idx_execution_tenant_started", "tenant_id", "started_at"),
|
151
|
+
Index("idx_execution_workflow_started", "workflow_id", "started_at"),
|
152
|
+
Index("idx_execution_status", "status"),
|
153
|
+
)
|
154
|
+
|
155
|
+
|
156
|
+
class UserPreferences(Base):
|
157
|
+
"""User preferences and settings"""
|
158
|
+
|
159
|
+
__tablename__ = "user_preferences"
|
160
|
+
|
161
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
162
|
+
tenant_id = Column(String(36), nullable=False)
|
163
|
+
user_id = Column(String(255), nullable=False)
|
164
|
+
|
165
|
+
# UI preferences
|
166
|
+
theme = Column(String(20), default="light")
|
167
|
+
canvas_settings = Column(JSON) # Zoom level, grid settings, etc.
|
168
|
+
|
169
|
+
# Workflow preferences
|
170
|
+
default_parameters = Column(JSON)
|
171
|
+
favorite_nodes = Column(JSON)
|
172
|
+
recent_workflows = Column(JSON)
|
173
|
+
|
174
|
+
created_at = Column(DateTime, default=func.now())
|
175
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
176
|
+
|
177
|
+
__table_args__ = (
|
178
|
+
Index("idx_pref_tenant_user", "tenant_id", "user_id", unique=True),
|
179
|
+
)
|
180
|
+
|
181
|
+
|
182
|
+
class WorkflowTemplate(Base):
|
183
|
+
"""Pre-built workflow templates"""
|
184
|
+
|
185
|
+
__tablename__ = "workflow_templates"
|
186
|
+
|
187
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
188
|
+
tenant_id = Column(String(36), nullable=True) # Null for global templates
|
189
|
+
name = Column(String(255), nullable=False)
|
190
|
+
category = Column(String(100))
|
191
|
+
description = Column(Text)
|
192
|
+
thumbnail = Column(String(255))
|
193
|
+
|
194
|
+
# Template definition
|
195
|
+
definition = Column(JSON, nullable=False)
|
196
|
+
default_parameters = Column(JSON)
|
197
|
+
|
198
|
+
# Metadata
|
199
|
+
is_public = Column(Boolean, default=False)
|
200
|
+
usage_count = Column(Integer, default=0)
|
201
|
+
created_by = Column(String(255))
|
202
|
+
created_at = Column(DateTime, default=func.now())
|
203
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
204
|
+
|
205
|
+
__table_args__ = (
|
206
|
+
Index("idx_template_category", "category"),
|
207
|
+
Index("idx_template_public", "is_public"),
|
208
|
+
)
|
209
|
+
|
210
|
+
|
211
|
+
class WorkflowPermission(Base):
|
212
|
+
"""Workflow-level permissions"""
|
213
|
+
|
214
|
+
__tablename__ = "workflow_permissions"
|
215
|
+
|
216
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
217
|
+
workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
|
218
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
219
|
+
|
220
|
+
# Who does this permission apply to?
|
221
|
+
user_id = Column(String(36), nullable=True) # Specific user
|
222
|
+
role = Column(String(50), nullable=True) # Role-based
|
223
|
+
group_id = Column(String(36), nullable=True) # Group-based
|
224
|
+
|
225
|
+
# What permission?
|
226
|
+
permission = Column(
|
227
|
+
String(50), nullable=False
|
228
|
+
) # view, execute, modify, delete, share, admin
|
229
|
+
effect = Column(
|
230
|
+
String(20), nullable=False, default="allow"
|
231
|
+
) # allow, deny, conditional
|
232
|
+
|
233
|
+
# Conditions (JSON object for flexibility)
|
234
|
+
conditions = Column(JSON)
|
235
|
+
|
236
|
+
# Metadata
|
237
|
+
created_by = Column(String(255))
|
238
|
+
created_at = Column(DateTime, default=func.now())
|
239
|
+
expires_at = Column(DateTime, nullable=True)
|
240
|
+
|
241
|
+
# Relationships
|
242
|
+
workflow = relationship("Workflow", backref="permissions")
|
243
|
+
|
244
|
+
__table_args__ = (
|
245
|
+
Index("idx_workflow_perm_workflow", "workflow_id"),
|
246
|
+
Index("idx_workflow_perm_user", "user_id"),
|
247
|
+
Index("idx_workflow_perm_tenant", "tenant_id"),
|
248
|
+
)
|
249
|
+
|
250
|
+
|
251
|
+
class NodePermission(Base):
|
252
|
+
"""Node-level permissions"""
|
253
|
+
|
254
|
+
__tablename__ = "node_permissions"
|
255
|
+
|
256
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
257
|
+
workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
|
258
|
+
node_id = Column(String(255), nullable=False) # Node ID within workflow
|
259
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
260
|
+
|
261
|
+
# Who does this permission apply to?
|
262
|
+
user_id = Column(String(36), nullable=True)
|
263
|
+
role = Column(String(50), nullable=True)
|
264
|
+
group_id = Column(String(36), nullable=True)
|
265
|
+
|
266
|
+
# What permission?
|
267
|
+
permission = Column(
|
268
|
+
String(50), nullable=False
|
269
|
+
) # execute, read_output, write_input, skip, mask_output
|
270
|
+
effect = Column(String(20), nullable=False, default="allow")
|
271
|
+
|
272
|
+
# Conditions and special handling
|
273
|
+
conditions = Column(JSON)
|
274
|
+
masked_fields = Column(JSON) # Fields to mask in output
|
275
|
+
redirect_node = Column(String(255)) # Alternative node if access denied
|
276
|
+
|
277
|
+
# Metadata
|
278
|
+
created_by = Column(String(255))
|
279
|
+
created_at = Column(DateTime, default=func.now())
|
280
|
+
expires_at = Column(DateTime, nullable=True)
|
281
|
+
|
282
|
+
# Relationships
|
283
|
+
workflow = relationship("Workflow", backref="node_permissions")
|
284
|
+
|
285
|
+
__table_args__ = (
|
286
|
+
Index("idx_node_perm_workflow", "workflow_id"),
|
287
|
+
Index("idx_node_perm_node", "workflow_id", "node_id"),
|
288
|
+
Index("idx_node_perm_user", "user_id"),
|
289
|
+
Index("idx_node_perm_tenant", "tenant_id"),
|
290
|
+
)
|
291
|
+
|
292
|
+
|
293
|
+
class AccessLog(Base):
|
294
|
+
"""Audit log for access attempts"""
|
295
|
+
|
296
|
+
__tablename__ = "access_logs"
|
297
|
+
|
298
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
299
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
300
|
+
user_id = Column(String(36), nullable=False, index=True)
|
301
|
+
|
302
|
+
# What was accessed?
|
303
|
+
resource_type = Column(String(50), nullable=False) # workflow, node
|
304
|
+
resource_id = Column(String(255), nullable=False)
|
305
|
+
permission = Column(String(50), nullable=False)
|
306
|
+
|
307
|
+
# Result
|
308
|
+
allowed = Column(Boolean, nullable=False)
|
309
|
+
reason = Column(Text)
|
310
|
+
|
311
|
+
# Context
|
312
|
+
ip_address = Column(String(50))
|
313
|
+
user_agent = Column(String(255))
|
314
|
+
session_id = Column(String(36))
|
315
|
+
|
316
|
+
# Timestamp
|
317
|
+
timestamp = Column(DateTime, default=func.now(), index=True)
|
318
|
+
|
319
|
+
__table_args__ = (
|
320
|
+
Index("idx_access_log_user_time", "user_id", "timestamp"),
|
321
|
+
Index("idx_access_log_resource", "resource_type", "resource_id", "timestamp"),
|
322
|
+
)
|
323
|
+
|
324
|
+
|
325
|
+
class UserGroup(Base):
|
326
|
+
"""User groups for permission management"""
|
327
|
+
|
328
|
+
__tablename__ = "user_groups"
|
329
|
+
|
330
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
331
|
+
tenant_id = Column(String(36), nullable=False, index=True)
|
332
|
+
name = Column(String(255), nullable=False)
|
333
|
+
description = Column(Text)
|
334
|
+
|
335
|
+
# Group permissions (can be inherited)
|
336
|
+
permissions = Column(JSON)
|
337
|
+
|
338
|
+
# Metadata
|
339
|
+
created_by = Column(String(255))
|
340
|
+
created_at = Column(DateTime, default=func.now())
|
341
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
342
|
+
|
343
|
+
# Relationships
|
344
|
+
members = relationship(
|
345
|
+
"UserGroupMember", back_populates="group", cascade="all, delete-orphan"
|
346
|
+
)
|
347
|
+
|
348
|
+
__table_args__ = (Index("idx_group_tenant_name", "tenant_id", "name", unique=True),)
|
349
|
+
|
350
|
+
|
351
|
+
class UserGroupMember(Base):
|
352
|
+
"""User membership in groups"""
|
353
|
+
|
354
|
+
__tablename__ = "user_group_members"
|
355
|
+
|
356
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
357
|
+
group_id = Column(String(36), ForeignKey("user_groups.id"), nullable=False)
|
358
|
+
user_id = Column(String(36), nullable=False)
|
359
|
+
|
360
|
+
# Membership details
|
361
|
+
role = Column(String(50), default="member") # member, admin
|
362
|
+
joined_at = Column(DateTime, default=func.now())
|
363
|
+
added_by = Column(String(255))
|
364
|
+
|
365
|
+
# Relationships
|
366
|
+
group = relationship("UserGroup", back_populates="members")
|
367
|
+
|
368
|
+
__table_args__ = (
|
369
|
+
Index("idx_group_member", "group_id", "user_id", unique=True),
|
370
|
+
Index("idx_member_user", "user_id"),
|
371
|
+
)
|
372
|
+
|
373
|
+
|
374
|
+
# Repository classes for data access
|
375
|
+
class WorkflowRepository:
|
376
|
+
"""Repository for workflow operations"""
|
377
|
+
|
378
|
+
def __init__(self, session: Session):
|
379
|
+
self.session = session
|
380
|
+
|
381
|
+
def create(
|
382
|
+
self,
|
383
|
+
tenant_id: str,
|
384
|
+
name: str,
|
385
|
+
description: str,
|
386
|
+
definition: Dict[str, Any],
|
387
|
+
created_by: str = None,
|
388
|
+
) -> Workflow:
|
389
|
+
"""Create a new workflow"""
|
390
|
+
workflow = Workflow(
|
391
|
+
tenant_id=tenant_id,
|
392
|
+
name=name,
|
393
|
+
description=description,
|
394
|
+
definition=definition,
|
395
|
+
created_by=created_by,
|
396
|
+
)
|
397
|
+
self.session.add(workflow)
|
398
|
+
self.session.commit()
|
399
|
+
|
400
|
+
# Create initial version
|
401
|
+
self.create_version(workflow.id, 1, definition, "Initial version", created_by)
|
402
|
+
|
403
|
+
return workflow
|
404
|
+
|
405
|
+
def update(
|
406
|
+
self, workflow_id: str, updates: Dict[str, Any], updated_by: str = None
|
407
|
+
) -> Workflow:
|
408
|
+
"""Update a workflow"""
|
409
|
+
workflow = self.session.query(Workflow).filter_by(id=workflow_id).first()
|
410
|
+
if not workflow:
|
411
|
+
raise ValueError(f"Workflow {workflow_id} not found")
|
412
|
+
|
413
|
+
# Update fields
|
414
|
+
for key, value in updates.items():
|
415
|
+
if hasattr(workflow, key):
|
416
|
+
setattr(workflow, key, value)
|
417
|
+
|
418
|
+
# If definition changed, create new version
|
419
|
+
if "definition" in updates:
|
420
|
+
workflow.version += 1
|
421
|
+
self.create_version(
|
422
|
+
workflow_id,
|
423
|
+
workflow.version,
|
424
|
+
updates["definition"],
|
425
|
+
updates.get("change_message", "Updated workflow"),
|
426
|
+
updated_by,
|
427
|
+
)
|
428
|
+
|
429
|
+
self.session.commit()
|
430
|
+
return workflow
|
431
|
+
|
432
|
+
def create_version(
|
433
|
+
self,
|
434
|
+
workflow_id: str,
|
435
|
+
version: int,
|
436
|
+
definition: Dict[str, Any],
|
437
|
+
change_message: str,
|
438
|
+
created_by: str = None,
|
439
|
+
):
|
440
|
+
"""Create a workflow version"""
|
441
|
+
version_record = WorkflowVersion(
|
442
|
+
workflow_id=workflow_id,
|
443
|
+
version=version,
|
444
|
+
definition=definition,
|
445
|
+
change_message=change_message,
|
446
|
+
created_by=created_by,
|
447
|
+
)
|
448
|
+
self.session.add(version_record)
|
449
|
+
self.session.commit()
|
450
|
+
|
451
|
+
def get(self, workflow_id: str) -> Optional[Workflow]:
|
452
|
+
"""Get a workflow by ID"""
|
453
|
+
return self.session.query(Workflow).filter_by(id=workflow_id).first()
|
454
|
+
|
455
|
+
def list(self, tenant_id: str, limit: int = 100, offset: int = 0) -> List[Workflow]:
|
456
|
+
"""List workflows for a tenant"""
|
457
|
+
return (
|
458
|
+
self.session.query(Workflow)
|
459
|
+
.filter_by(tenant_id=tenant_id)
|
460
|
+
.order_by(Workflow.created_at.desc())
|
461
|
+
.limit(limit)
|
462
|
+
.offset(offset)
|
463
|
+
.all()
|
464
|
+
)
|
465
|
+
|
466
|
+
def delete(self, workflow_id: str):
|
467
|
+
"""Delete a workflow"""
|
468
|
+
workflow = self.get(workflow_id)
|
469
|
+
if workflow:
|
470
|
+
self.session.delete(workflow)
|
471
|
+
self.session.commit()
|
472
|
+
|
473
|
+
|
474
|
+
class CustomNodeRepository:
|
475
|
+
"""Repository for custom node operations"""
|
476
|
+
|
477
|
+
def __init__(self, session: Session):
|
478
|
+
self.session = session
|
479
|
+
|
480
|
+
def create(self, tenant_id: str, node_data: Dict[str, Any]) -> CustomNode:
|
481
|
+
"""Create a custom node"""
|
482
|
+
node = CustomNode(tenant_id=tenant_id, **node_data)
|
483
|
+
self.session.add(node)
|
484
|
+
self.session.commit()
|
485
|
+
return node
|
486
|
+
|
487
|
+
def update(self, node_id: str, updates: Dict[str, Any]) -> CustomNode:
|
488
|
+
"""Update a custom node"""
|
489
|
+
node = self.session.query(CustomNode).filter_by(id=node_id).first()
|
490
|
+
if not node:
|
491
|
+
raise ValueError(f"Custom node {node_id} not found")
|
492
|
+
|
493
|
+
for key, value in updates.items():
|
494
|
+
if hasattr(node, key):
|
495
|
+
setattr(node, key, value)
|
496
|
+
|
497
|
+
self.session.commit()
|
498
|
+
return node
|
499
|
+
|
500
|
+
def list(self, tenant_id: str) -> List[CustomNode]:
|
501
|
+
"""List custom nodes for a tenant"""
|
502
|
+
return (
|
503
|
+
self.session.query(CustomNode)
|
504
|
+
.filter_by(tenant_id=tenant_id)
|
505
|
+
.order_by(CustomNode.category, CustomNode.name)
|
506
|
+
.all()
|
507
|
+
)
|
508
|
+
|
509
|
+
def get(self, node_id: str) -> Optional[CustomNode]:
|
510
|
+
"""Get a custom node by ID"""
|
511
|
+
return self.session.query(CustomNode).filter_by(id=node_id).first()
|
512
|
+
|
513
|
+
def delete(self, node_id: str):
|
514
|
+
"""Delete a custom node"""
|
515
|
+
node = self.get(node_id)
|
516
|
+
if node:
|
517
|
+
self.session.delete(node)
|
518
|
+
self.session.commit()
|
519
|
+
|
520
|
+
|
521
|
+
class ExecutionRepository:
|
522
|
+
"""Repository for execution operations"""
|
523
|
+
|
524
|
+
def __init__(self, session: Session):
|
525
|
+
self.session = session
|
526
|
+
|
527
|
+
def create(
|
528
|
+
self, workflow_id: str, tenant_id: str, parameters: Dict[str, Any] = None
|
529
|
+
) -> WorkflowExecution:
|
530
|
+
"""Create an execution record"""
|
531
|
+
execution = WorkflowExecution(
|
532
|
+
workflow_id=workflow_id,
|
533
|
+
tenant_id=tenant_id,
|
534
|
+
status="pending",
|
535
|
+
parameters=parameters,
|
536
|
+
started_at=datetime.now(timezone.utc),
|
537
|
+
)
|
538
|
+
self.session.add(execution)
|
539
|
+
self.session.commit()
|
540
|
+
return execution
|
541
|
+
|
542
|
+
def update_status(
|
543
|
+
self,
|
544
|
+
execution_id: str,
|
545
|
+
status: str,
|
546
|
+
result: Dict[str, Any] = None,
|
547
|
+
error: str = None,
|
548
|
+
):
|
549
|
+
"""Update execution status"""
|
550
|
+
execution = (
|
551
|
+
self.session.query(WorkflowExecution).filter_by(id=execution_id).first()
|
552
|
+
)
|
553
|
+
if not execution:
|
554
|
+
raise ValueError(f"Execution {execution_id} not found")
|
555
|
+
|
556
|
+
execution.status = status
|
557
|
+
if result is not None:
|
558
|
+
execution.result = result
|
559
|
+
if error is not None:
|
560
|
+
execution.error = error
|
561
|
+
|
562
|
+
if status in ["completed", "failed"]:
|
563
|
+
execution.completed_at = datetime.now(timezone.utc)
|
564
|
+
if execution.started_at:
|
565
|
+
execution.execution_time_ms = int(
|
566
|
+
(execution.completed_at - execution.started_at).total_seconds()
|
567
|
+
* 1000
|
568
|
+
)
|
569
|
+
|
570
|
+
self.session.commit()
|
571
|
+
|
572
|
+
def get(self, execution_id: str) -> Optional[WorkflowExecution]:
|
573
|
+
"""Get execution by ID"""
|
574
|
+
return self.session.query(WorkflowExecution).filter_by(id=execution_id).first()
|
575
|
+
|
576
|
+
def list_for_workflow(
|
577
|
+
self, workflow_id: str, limit: int = 50
|
578
|
+
) -> List[WorkflowExecution]:
|
579
|
+
"""List executions for a workflow"""
|
580
|
+
return (
|
581
|
+
self.session.query(WorkflowExecution)
|
582
|
+
.filter_by(workflow_id=workflow_id)
|
583
|
+
.order_by(WorkflowExecution.started_at.desc())
|
584
|
+
.limit(limit)
|
585
|
+
.all()
|
586
|
+
)
|
587
|
+
|
588
|
+
|
589
|
+
# Database initialization
|
590
|
+
def init_database(db_path: str = None) -> tuple[sessionmaker, Engine]:
|
591
|
+
"""Initialize the database"""
|
592
|
+
if db_path is None:
|
593
|
+
db_path = Path.home() / ".kailash" / "studio.db"
|
594
|
+
|
595
|
+
db_path = Path(db_path)
|
596
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
597
|
+
|
598
|
+
engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
599
|
+
Base.metadata.create_all(engine)
|
600
|
+
|
601
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
602
|
+
|
603
|
+
return SessionLocal, engine
|
604
|
+
|
605
|
+
|
606
|
+
# Context manager for database sessions
|
607
|
+
|
608
|
+
|
609
|
+
@contextmanager
|
610
|
+
def get_db_session(SessionLocal):
|
611
|
+
"""Provide a transactional scope for database operations"""
|
612
|
+
session = SessionLocal()
|
613
|
+
try:
|
614
|
+
yield session
|
615
|
+
session.commit()
|
616
|
+
except Exception:
|
617
|
+
session.rollback()
|
618
|
+
raise
|
619
|
+
finally:
|
620
|
+
session.close()
|