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.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {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
+