kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.
Potentially problematic release.
This version of kubiya-control-plane-api might be problematic. Click here for more details.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request ID middleware for request tracking and correlation.
|
|
3
|
+
|
|
4
|
+
Adds a unique request ID to each request for tracking through logs and services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import Request, Response
|
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
+
from starlette.types import ASGIApp
|
|
10
|
+
import structlog
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
import contextvars
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger()
|
|
17
|
+
|
|
18
|
+
# Context variable to store request ID
|
|
19
|
+
request_id_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
20
|
+
"request_id", default=None
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RequestIDMiddleware(BaseHTTPMiddleware):
|
|
25
|
+
"""
|
|
26
|
+
Middleware to add request ID to all requests.
|
|
27
|
+
|
|
28
|
+
- Checks for existing X-Request-ID header
|
|
29
|
+
- Generates new ID if not present
|
|
30
|
+
- Adds ID to response headers
|
|
31
|
+
- Makes ID available in context for logging
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
REQUEST_ID_HEADER = "X-Request-ID"
|
|
35
|
+
|
|
36
|
+
def __init__(self, app: ASGIApp, header_name: str = None):
|
|
37
|
+
super().__init__(app)
|
|
38
|
+
if header_name:
|
|
39
|
+
self.REQUEST_ID_HEADER = header_name
|
|
40
|
+
|
|
41
|
+
async def dispatch(self, request: Request, call_next):
|
|
42
|
+
"""Process the request and add request ID."""
|
|
43
|
+
|
|
44
|
+
# Get or generate request ID
|
|
45
|
+
request_id = (
|
|
46
|
+
request.headers.get(self.REQUEST_ID_HEADER) or
|
|
47
|
+
request.headers.get(self.REQUEST_ID_HEADER.lower()) or
|
|
48
|
+
self._generate_request_id()
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Validate request ID format (basic security check)
|
|
52
|
+
if not self._is_valid_request_id(request_id):
|
|
53
|
+
request_id = self._generate_request_id()
|
|
54
|
+
|
|
55
|
+
# Store in request state for easy access
|
|
56
|
+
request.state.request_id = request_id
|
|
57
|
+
|
|
58
|
+
# Set context variable for logging
|
|
59
|
+
token = request_id_context.set(request_id)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Log request start
|
|
63
|
+
logger.info(
|
|
64
|
+
"request_started",
|
|
65
|
+
request_id=request_id,
|
|
66
|
+
method=request.method,
|
|
67
|
+
path=str(request.url.path),
|
|
68
|
+
query=str(request.url.query) if request.url.query else None,
|
|
69
|
+
client=request.client.host if request.client else None,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Process request
|
|
73
|
+
response = await call_next(request)
|
|
74
|
+
|
|
75
|
+
# Add request ID to response headers
|
|
76
|
+
response.headers[self.REQUEST_ID_HEADER] = request_id
|
|
77
|
+
|
|
78
|
+
# Log request completion
|
|
79
|
+
logger.info(
|
|
80
|
+
"request_completed",
|
|
81
|
+
request_id=request_id,
|
|
82
|
+
status_code=response.status_code,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return response
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# Log request failure
|
|
89
|
+
logger.error(
|
|
90
|
+
"request_failed",
|
|
91
|
+
request_id=request_id,
|
|
92
|
+
error=str(e),
|
|
93
|
+
error_type=type(e).__name__,
|
|
94
|
+
)
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
finally:
|
|
98
|
+
# Reset context variable
|
|
99
|
+
request_id_context.reset(token)
|
|
100
|
+
|
|
101
|
+
def _generate_request_id(self) -> str:
|
|
102
|
+
"""Generate a new request ID."""
|
|
103
|
+
return str(uuid.uuid4())
|
|
104
|
+
|
|
105
|
+
def _is_valid_request_id(self, request_id: str) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
Validate request ID format.
|
|
108
|
+
|
|
109
|
+
Accepts UUIDs and alphanumeric strings up to 128 characters.
|
|
110
|
+
"""
|
|
111
|
+
if not request_id or len(request_id) > 128:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Allow UUIDs, alphanumeric, hyphens, and underscores
|
|
115
|
+
import re
|
|
116
|
+
return bool(re.match(r'^[a-zA-Z0-9\-_]+$', request_id))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_request_id() -> Optional[str]:
|
|
120
|
+
"""
|
|
121
|
+
Get the current request ID from context.
|
|
122
|
+
|
|
123
|
+
This can be used anywhere in the application to get the current request ID.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Request ID if in request context, None otherwise
|
|
127
|
+
"""
|
|
128
|
+
return request_id_context.get()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def set_request_id(request_id: str) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Set the request ID in context.
|
|
134
|
+
|
|
135
|
+
This is useful for background tasks or other contexts where you want
|
|
136
|
+
to maintain the request ID.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
request_id: Request ID to set
|
|
140
|
+
"""
|
|
141
|
+
request_id_context.set(request_id)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class RequestIDLogProcessor:
|
|
145
|
+
"""
|
|
146
|
+
Structlog processor to add request ID to all log entries.
|
|
147
|
+
|
|
148
|
+
Use this in your structlog configuration:
|
|
149
|
+
|
|
150
|
+
structlog.configure(
|
|
151
|
+
processors=[
|
|
152
|
+
RequestIDLogProcessor(),
|
|
153
|
+
structlog.processors.add_log_level,
|
|
154
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
155
|
+
structlog.processors.JSONRenderer(),
|
|
156
|
+
],
|
|
157
|
+
...
|
|
158
|
+
)
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __call__(self, logger, name, event_dict):
|
|
162
|
+
"""Add request ID to log entry if available."""
|
|
163
|
+
request_id = get_request_id()
|
|
164
|
+
if request_id:
|
|
165
|
+
event_dict["request_id"] = request_id
|
|
166
|
+
return event_dict
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def setup_request_id_logging():
|
|
170
|
+
"""
|
|
171
|
+
Configure structlog to include request ID in all logs.
|
|
172
|
+
|
|
173
|
+
Call this in your app initialization.
|
|
174
|
+
"""
|
|
175
|
+
import structlog
|
|
176
|
+
|
|
177
|
+
structlog.configure(
|
|
178
|
+
processors=[
|
|
179
|
+
RequestIDLogProcessor(), # Add request ID
|
|
180
|
+
structlog.contextvars.merge_contextvars,
|
|
181
|
+
structlog.processors.add_log_level,
|
|
182
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
183
|
+
structlog.processors.JSONRenderer(),
|
|
184
|
+
],
|
|
185
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
|
186
|
+
logging.INFO
|
|
187
|
+
),
|
|
188
|
+
logger_factory=structlog.PrintLoggerFactory(),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# For FastAPI dependency injection
|
|
193
|
+
async def get_request_id_from_request(request: Request) -> str:
|
|
194
|
+
"""
|
|
195
|
+
FastAPI dependency to get request ID.
|
|
196
|
+
|
|
197
|
+
Usage:
|
|
198
|
+
@app.get("/example")
|
|
199
|
+
async def example(request_id: str = Depends(get_request_id_from_request)):
|
|
200
|
+
return {"request_id": request_id}
|
|
201
|
+
"""
|
|
202
|
+
return getattr(request.state, "request_id", None) or str(uuid.uuid4())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Database Models
|
|
2
|
+
from control_plane_api.app.models.project import Project, ProjectStatus
|
|
3
|
+
from control_plane_api.app.models.agent import Agent, AgentStatus
|
|
4
|
+
from control_plane_api.app.models.team import Team, TeamStatus
|
|
5
|
+
from control_plane_api.app.models.workflow import Workflow, WorkflowStatus
|
|
6
|
+
from control_plane_api.app.models.session import Session
|
|
7
|
+
from control_plane_api.app.models.execution import Execution, ExecutionStatus, ExecutionType, ExecutionTriggerSource
|
|
8
|
+
from control_plane_api.app.models.presence import UserPresence
|
|
9
|
+
from control_plane_api.app.models.environment import Environment, EnvironmentStatus
|
|
10
|
+
from control_plane_api.app.models.associations import AgentEnvironment, TeamEnvironment, ExecutionParticipant, ParticipantRole
|
|
11
|
+
from control_plane_api.app.models.job import Job, JobExecution, JobStatus, JobTriggerType, ExecutorType, PlanningMode
|
|
12
|
+
from control_plane_api.app.models.llm_model import LLMModel
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Project", "ProjectStatus",
|
|
16
|
+
"Agent", "AgentStatus",
|
|
17
|
+
"Team", "TeamStatus",
|
|
18
|
+
"Workflow", "WorkflowStatus",
|
|
19
|
+
"Session",
|
|
20
|
+
"Execution", "ExecutionStatus", "ExecutionType", "ExecutionTriggerSource",
|
|
21
|
+
"UserPresence",
|
|
22
|
+
"Environment", "EnvironmentStatus",
|
|
23
|
+
"AgentEnvironment", "TeamEnvironment",
|
|
24
|
+
"ExecutionParticipant", "ParticipantRole",
|
|
25
|
+
"Job", "JobExecution", "JobStatus", "JobTriggerType", "ExecutorType", "PlanningMode",
|
|
26
|
+
"LLMModel"
|
|
27
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, DateTime, Text, JSON, ForeignKey, Enum
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import enum
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from control_plane_api.app.database import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentStatus(str, enum.Enum):
|
|
11
|
+
"""Agent status enumeration"""
|
|
12
|
+
|
|
13
|
+
IDLE = "idle"
|
|
14
|
+
RUNNING = "running"
|
|
15
|
+
PAUSED = "paused"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
STOPPED = "stopped"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RuntimeType(str, enum.Enum):
|
|
22
|
+
"""Agent runtime type enumeration"""
|
|
23
|
+
|
|
24
|
+
DEFAULT = "default" # Agno-based runtime (current implementation)
|
|
25
|
+
CLAUDE_CODE = "claude_code" # Claude Code SDK runtime
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Agent(Base):
|
|
29
|
+
"""Agent model for storing agent information"""
|
|
30
|
+
|
|
31
|
+
__tablename__ = "agents"
|
|
32
|
+
|
|
33
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
34
|
+
name = Column(String, nullable=False, index=True)
|
|
35
|
+
description = Column(Text, nullable=True)
|
|
36
|
+
status = Column(Enum(AgentStatus, values_callable=lambda x: [e.value for e in x]), default=AgentStatus.IDLE, nullable=False)
|
|
37
|
+
capabilities = Column(JSON, default=list, nullable=False)
|
|
38
|
+
configuration = Column(JSON, default=dict, nullable=False)
|
|
39
|
+
|
|
40
|
+
# LiteLLM configuration
|
|
41
|
+
model_id = Column(String, nullable=True) # LiteLLM model identifier
|
|
42
|
+
model_config = Column(JSON, default=dict, nullable=False) # Model-specific config (temperature, top_p, etc.)
|
|
43
|
+
|
|
44
|
+
# Runtime configuration
|
|
45
|
+
runtime = Column(
|
|
46
|
+
Enum(RuntimeType, values_callable=lambda x: [e.value for e in x]),
|
|
47
|
+
default=RuntimeType.DEFAULT,
|
|
48
|
+
server_default="default",
|
|
49
|
+
nullable=False,
|
|
50
|
+
index=True
|
|
51
|
+
) # Runtime type for agent execution (default: Agno, claude_code: Claude Code SDK)
|
|
52
|
+
|
|
53
|
+
# Foreign keys
|
|
54
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
55
|
+
team_id = Column(String, ForeignKey("teams.id"), nullable=True)
|
|
56
|
+
|
|
57
|
+
# Metadata
|
|
58
|
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
59
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
60
|
+
last_active_at = Column(DateTime, nullable=True)
|
|
61
|
+
|
|
62
|
+
# State management
|
|
63
|
+
state = Column(JSON, default=dict, nullable=False)
|
|
64
|
+
error_message = Column(Text, nullable=True)
|
|
65
|
+
|
|
66
|
+
# Relationships
|
|
67
|
+
team = relationship("Team", back_populates="agents")
|
|
68
|
+
sessions = relationship("Session", back_populates="agent", cascade="all, delete-orphan")
|
|
69
|
+
|
|
70
|
+
# Many-to-many relationship with environments
|
|
71
|
+
environment_associations = relationship(
|
|
72
|
+
"AgentEnvironment",
|
|
73
|
+
foreign_keys="AgentEnvironment.agent_id",
|
|
74
|
+
cascade="all, delete-orphan",
|
|
75
|
+
lazy="select"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __repr__(self):
|
|
79
|
+
return f"<Agent(id={self.id}, name={self.name}, status={self.status})>"
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytics models for comprehensive execution tracking and reporting.
|
|
3
|
+
|
|
4
|
+
This module provides production-grade analytics tables to track:
|
|
5
|
+
- Per-turn LLM metrics (tokens, duration, cost)
|
|
6
|
+
- Tool execution details (success/failure, timing)
|
|
7
|
+
- Task completion tracking
|
|
8
|
+
- Organization-level reporting
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import Column, String, DateTime, Integer, Float, Boolean, Text, JSON, ForeignKey, Index
|
|
12
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
13
|
+
from sqlalchemy.sql import func
|
|
14
|
+
from sqlalchemy.orm import relationship
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
import uuid
|
|
17
|
+
import enum
|
|
18
|
+
|
|
19
|
+
from control_plane_api.app.database import Base
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExecutionTurn(Base):
|
|
23
|
+
"""
|
|
24
|
+
Tracks each LLM interaction turn within an execution.
|
|
25
|
+
|
|
26
|
+
This provides granular metrics for each agent reasoning step,
|
|
27
|
+
enabling detailed performance analysis and cost tracking.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__tablename__ = "execution_turns"
|
|
31
|
+
|
|
32
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
33
|
+
|
|
34
|
+
# Organization and execution context
|
|
35
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
36
|
+
execution_id = Column(UUID(as_uuid=True), ForeignKey("executions.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
37
|
+
|
|
38
|
+
# Turn identification
|
|
39
|
+
turn_number = Column(Integer, nullable=False) # Sequential turn number in conversation
|
|
40
|
+
turn_id = Column(String, nullable=True) # Runtime-specific turn identifier
|
|
41
|
+
|
|
42
|
+
# Model information
|
|
43
|
+
model = Column(String, nullable=False) # e.g., "claude-sonnet-4", "gpt-4"
|
|
44
|
+
model_provider = Column(String, nullable=True) # e.g., "anthropic", "openai"
|
|
45
|
+
|
|
46
|
+
# Timing metrics
|
|
47
|
+
started_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
|
48
|
+
completed_at = Column(DateTime(timezone=True), nullable=True)
|
|
49
|
+
duration_ms = Column(Integer, nullable=True) # Duration in milliseconds
|
|
50
|
+
|
|
51
|
+
# Token usage metrics
|
|
52
|
+
input_tokens = Column(Integer, nullable=True, default=0)
|
|
53
|
+
output_tokens = Column(Integer, nullable=True, default=0)
|
|
54
|
+
cache_read_tokens = Column(Integer, nullable=True, default=0) # Cached tokens (Anthropic)
|
|
55
|
+
cache_creation_tokens = Column(Integer, nullable=True, default=0) # Cache creation tokens
|
|
56
|
+
total_tokens = Column(Integer, nullable=True, default=0)
|
|
57
|
+
|
|
58
|
+
# Cost metrics (in USD)
|
|
59
|
+
input_cost = Column(Float, nullable=True, default=0.0)
|
|
60
|
+
output_cost = Column(Float, nullable=True, default=0.0)
|
|
61
|
+
cache_read_cost = Column(Float, nullable=True, default=0.0)
|
|
62
|
+
cache_creation_cost = Column(Float, nullable=True, default=0.0)
|
|
63
|
+
total_cost = Column(Float, nullable=True, default=0.0)
|
|
64
|
+
|
|
65
|
+
# Agentic Engineering Minutes (AEM) metrics
|
|
66
|
+
runtime_minutes = Column(Float, nullable=True, default=0.0) # duration_ms / 60000
|
|
67
|
+
model_weight = Column(Float, nullable=True, default=1.0) # Model family weight (opus=2.0, sonnet=1.0, haiku=0.5)
|
|
68
|
+
tool_calls_weight = Column(Float, nullable=True, default=1.0) # Tool complexity weight
|
|
69
|
+
aem_value = Column(Float, nullable=True, default=0.0) # Calculated: runtime_minutes × model_weight × tool_calls_weight
|
|
70
|
+
aem_cost = Column(Float, nullable=True, default=0.0) # AEM × price per AEM ($0.15/min default)
|
|
71
|
+
|
|
72
|
+
# Turn result
|
|
73
|
+
finish_reason = Column(String, nullable=True) # "stop", "length", "tool_use", "error"
|
|
74
|
+
response_preview = Column(Text, nullable=True) # First 500 chars of response
|
|
75
|
+
|
|
76
|
+
# Tool usage in this turn
|
|
77
|
+
tools_called_count = Column(Integer, default=0)
|
|
78
|
+
tools_called_names = Column(JSON, default=list) # List of tool names called
|
|
79
|
+
|
|
80
|
+
# Error tracking
|
|
81
|
+
error_message = Column(Text, nullable=True)
|
|
82
|
+
|
|
83
|
+
# Additional metrics (JSON for flexibility)
|
|
84
|
+
metrics = Column(JSON, default=dict) # Custom metrics, latencies, etc.
|
|
85
|
+
|
|
86
|
+
# Timestamps
|
|
87
|
+
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
88
|
+
|
|
89
|
+
# Indexes for fast querying
|
|
90
|
+
__table_args__ = (
|
|
91
|
+
Index('ix_execution_turns_org_execution', 'organization_id', 'execution_id'),
|
|
92
|
+
Index('ix_execution_turns_org_model', 'organization_id', 'model'),
|
|
93
|
+
Index('ix_execution_turns_org_created', 'organization_id', 'created_at'),
|
|
94
|
+
Index('ix_execution_turns_org_cost', 'organization_id', 'total_cost'),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __repr__(self):
|
|
98
|
+
return f"<ExecutionTurn {self.id} turn={self.turn_number} model={self.model} tokens={self.total_tokens} cost=${self.total_cost}>"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ExecutionToolCall(Base):
|
|
102
|
+
"""
|
|
103
|
+
Tracks individual tool/function calls within an execution.
|
|
104
|
+
|
|
105
|
+
This enables detailed tool usage analytics, error tracking,
|
|
106
|
+
and performance monitoring at the tool level.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
__tablename__ = "execution_tool_calls"
|
|
110
|
+
|
|
111
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
112
|
+
|
|
113
|
+
# Organization and execution context
|
|
114
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
115
|
+
execution_id = Column(UUID(as_uuid=True), ForeignKey("executions.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
116
|
+
turn_id = Column(UUID(as_uuid=True), ForeignKey("execution_turns.id", ondelete="CASCADE"), nullable=True, index=True)
|
|
117
|
+
|
|
118
|
+
# Tool identification
|
|
119
|
+
tool_name = Column(String, nullable=False, index=True) # e.g., "Read", "Bash", "WebFetch"
|
|
120
|
+
tool_use_id = Column(String, nullable=True) # Runtime-specific tool call ID
|
|
121
|
+
|
|
122
|
+
# Timing
|
|
123
|
+
started_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
|
124
|
+
completed_at = Column(DateTime(timezone=True), nullable=True)
|
|
125
|
+
duration_ms = Column(Integer, nullable=True) # Duration in milliseconds
|
|
126
|
+
|
|
127
|
+
# Tool execution details
|
|
128
|
+
tool_input = Column(JSON, nullable=True) # Tool parameters
|
|
129
|
+
tool_output = Column(Text, nullable=True) # Tool result (truncated if large)
|
|
130
|
+
tool_output_size = Column(Integer, nullable=True) # Size in bytes
|
|
131
|
+
|
|
132
|
+
# Status
|
|
133
|
+
success = Column(Boolean, nullable=False, default=True)
|
|
134
|
+
error_message = Column(Text, nullable=True)
|
|
135
|
+
error_type = Column(String, nullable=True) # e.g., "TimeoutError", "PermissionError"
|
|
136
|
+
|
|
137
|
+
# Additional metadata
|
|
138
|
+
custom_metadata = Column(JSON, default=dict) # Custom metrics, context, etc.
|
|
139
|
+
|
|
140
|
+
# Timestamps
|
|
141
|
+
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
142
|
+
|
|
143
|
+
# Indexes for fast querying
|
|
144
|
+
__table_args__ = (
|
|
145
|
+
Index('ix_execution_tool_calls_org_execution', 'organization_id', 'execution_id'),
|
|
146
|
+
Index('ix_execution_tool_calls_org_tool', 'organization_id', 'tool_name'),
|
|
147
|
+
Index('ix_execution_tool_calls_org_success', 'organization_id', 'success'),
|
|
148
|
+
Index('ix_execution_tool_calls_org_created', 'organization_id', 'created_at'),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def __repr__(self):
|
|
152
|
+
status = "✓" if self.success else "✗"
|
|
153
|
+
return f"<ExecutionToolCall {status} {self.tool_name} duration={self.duration_ms}ms>"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ExecutionTask(Base):
|
|
157
|
+
"""
|
|
158
|
+
Tracks high-level tasks/subtasks within an execution.
|
|
159
|
+
|
|
160
|
+
Some runtimes (like Claude Code) break work into tasks with status tracking.
|
|
161
|
+
This table captures that information for progress monitoring and analytics.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
__tablename__ = "execution_tasks"
|
|
165
|
+
|
|
166
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
167
|
+
|
|
168
|
+
# Organization and execution context
|
|
169
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
170
|
+
execution_id = Column(UUID(as_uuid=True), ForeignKey("executions.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
171
|
+
|
|
172
|
+
# Task identification
|
|
173
|
+
task_number = Column(Integer, nullable=True) # Sequential task number
|
|
174
|
+
task_id = Column(String, nullable=True) # Runtime-specific task ID
|
|
175
|
+
|
|
176
|
+
# Task details
|
|
177
|
+
task_description = Column(Text, nullable=False) # What is the task
|
|
178
|
+
task_type = Column(String, nullable=True) # e.g., "coding", "analysis", "research"
|
|
179
|
+
|
|
180
|
+
# Status tracking
|
|
181
|
+
status = Column(String, nullable=False, default="pending") # pending, in_progress, completed, failed
|
|
182
|
+
|
|
183
|
+
# Timing
|
|
184
|
+
started_at = Column(DateTime(timezone=True), nullable=True)
|
|
185
|
+
completed_at = Column(DateTime(timezone=True), nullable=True)
|
|
186
|
+
duration_ms = Column(Integer, nullable=True)
|
|
187
|
+
|
|
188
|
+
# Result
|
|
189
|
+
result = Column(Text, nullable=True) # Task outcome/result
|
|
190
|
+
error_message = Column(Text, nullable=True)
|
|
191
|
+
|
|
192
|
+
# Additional metadata
|
|
193
|
+
custom_metadata = Column(JSON, default=dict)
|
|
194
|
+
|
|
195
|
+
# Timestamps
|
|
196
|
+
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
197
|
+
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
198
|
+
|
|
199
|
+
# Indexes
|
|
200
|
+
__table_args__ = (
|
|
201
|
+
Index('ix_execution_tasks_org_execution', 'organization_id', 'execution_id'),
|
|
202
|
+
Index('ix_execution_tasks_org_status', 'organization_id', 'status'),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def __repr__(self):
|
|
206
|
+
return f"<ExecutionTask {self.id} status={self.status} desc={self.task_description[:50]}>"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Association tables for many-to-many relationships"""
|
|
2
|
+
from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum
|
|
3
|
+
from sqlalchemy.orm import relationship
|
|
4
|
+
from sqlalchemy.sql import func
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import uuid
|
|
7
|
+
import enum
|
|
8
|
+
|
|
9
|
+
from control_plane_api.app.database import Base
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ParticipantRole(str, enum.Enum):
|
|
13
|
+
"""Role of a participant in an execution"""
|
|
14
|
+
OWNER = "owner" # User who created the execution
|
|
15
|
+
COLLABORATOR = "collaborator" # User actively participating
|
|
16
|
+
VIEWER = "viewer" # User with read-only access
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentEnvironment(Base):
|
|
20
|
+
"""Many-to-many association between agents and environments"""
|
|
21
|
+
|
|
22
|
+
__tablename__ = "agent_environments"
|
|
23
|
+
|
|
24
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
25
|
+
agent_id = Column(String, ForeignKey("agents.id", ondelete="CASCADE"), nullable=False)
|
|
26
|
+
environment_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
|
27
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
28
|
+
|
|
29
|
+
# Assignment metadata
|
|
30
|
+
assigned_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
31
|
+
assigned_by = Column(String, nullable=True)
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
return f"<AgentEnvironment(agent_id={self.agent_id}, environment_id={self.environment_id})>"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TeamEnvironment(Base):
|
|
38
|
+
"""Many-to-many association between teams and environments"""
|
|
39
|
+
|
|
40
|
+
__tablename__ = "team_environments"
|
|
41
|
+
|
|
42
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
43
|
+
team_id = Column(String, ForeignKey("teams.id", ondelete="CASCADE"), nullable=False)
|
|
44
|
+
environment_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
|
45
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
46
|
+
|
|
47
|
+
# Assignment metadata
|
|
48
|
+
assigned_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
49
|
+
assigned_by = Column(String, nullable=True)
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return f"<TeamEnvironment(team_id={self.team_id}, environment_id={self.environment_id})>"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ExecutionParticipant(Base):
|
|
56
|
+
"""Many-to-many association between executions and users (multiplayer support)"""
|
|
57
|
+
|
|
58
|
+
__tablename__ = "execution_participants"
|
|
59
|
+
|
|
60
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
61
|
+
execution_id = Column(String, ForeignKey("executions.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
62
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
63
|
+
|
|
64
|
+
# User information
|
|
65
|
+
user_id = Column(String, nullable=False, index=True)
|
|
66
|
+
user_email = Column(String, nullable=True)
|
|
67
|
+
user_name = Column(String, nullable=True)
|
|
68
|
+
user_avatar = Column(String, nullable=True)
|
|
69
|
+
|
|
70
|
+
# Participant role and status
|
|
71
|
+
role = Column(SQLEnum(ParticipantRole, values_callable=lambda x: [e.value for e in x]), default=ParticipantRole.COLLABORATOR, nullable=False)
|
|
72
|
+
|
|
73
|
+
# Timestamps
|
|
74
|
+
joined_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
75
|
+
last_active_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
76
|
+
|
|
77
|
+
# Relationships
|
|
78
|
+
execution = relationship("Execution", back_populates="participants")
|
|
79
|
+
|
|
80
|
+
def __repr__(self):
|
|
81
|
+
return f"<ExecutionParticipant(execution_id={self.execution_id}, user_id={self.user_id}, role={self.role})>"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Environment model for execution environments"""
|
|
2
|
+
from sqlalchemy import Column, String, DateTime, JSON, Enum as SQLEnum
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
import enum
|
|
6
|
+
|
|
7
|
+
from control_plane_api.app.database import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EnvironmentStatus(str, enum.Enum):
|
|
11
|
+
"""Environment status"""
|
|
12
|
+
PENDING = "pending"
|
|
13
|
+
PROVISIONING = "provisioning"
|
|
14
|
+
ACTIVE = "active"
|
|
15
|
+
INACTIVE = "inactive"
|
|
16
|
+
ERROR = "error"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Environment(Base):
|
|
20
|
+
"""
|
|
21
|
+
Execution environment - represents a worker queue environment.
|
|
22
|
+
Maps to task queues in Temporal.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__tablename__ = "environments"
|
|
26
|
+
|
|
27
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
28
|
+
organization_id = Column(String, nullable=False, index=True)
|
|
29
|
+
|
|
30
|
+
# Basic info
|
|
31
|
+
name = Column(String, nullable=False, index=True) # Environment name (e.g., "default", "production")
|
|
32
|
+
display_name = Column(String, nullable=True) # User-friendly display name
|
|
33
|
+
description = Column(String, nullable=True)
|
|
34
|
+
|
|
35
|
+
# Configuration
|
|
36
|
+
tags = Column(JSON, nullable=False, default=list) # Tags for categorization
|
|
37
|
+
settings = Column(JSON, nullable=False, default=dict) # Environment-specific settings
|
|
38
|
+
status = Column(
|
|
39
|
+
SQLEnum(EnvironmentStatus),
|
|
40
|
+
nullable=False,
|
|
41
|
+
default=EnvironmentStatus.PENDING,
|
|
42
|
+
index=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Temporal Cloud provisioning
|
|
46
|
+
worker_token = Column(String, nullable=True) # JWT token for worker registration
|
|
47
|
+
provisioning_workflow_id = Column(String, nullable=True) # Temporal workflow ID for provisioning
|
|
48
|
+
provisioned_at = Column(DateTime, nullable=True) # When namespace was provisioned
|
|
49
|
+
error_message = Column(String, nullable=True) # Error message if provisioning failed
|
|
50
|
+
temporal_namespace_id = Column(String, nullable=True) # Temporal Cloud namespace ID
|
|
51
|
+
|
|
52
|
+
# Execution environment configuration (env vars, secrets, integrations)
|
|
53
|
+
execution_environment = Column(JSON, nullable=False, default=dict)
|
|
54
|
+
|
|
55
|
+
# Timestamps and audit
|
|
56
|
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
57
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
58
|
+
created_by = Column(String, nullable=True) # User email or ID who created
|
|
59
|
+
updated_by = Column(String, nullable=True) # User email or ID who last updated
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f"<Environment(id={self.id}, name={self.name}, organization_id={self.organization_id}, status={self.status})>"
|
|
63
|
+
|