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,464 @@
1
+ """
2
+ Analytics service for collecting and sending execution metrics to Control Plane.
3
+
4
+ This service provides a clean interface for workers to track:
5
+ - Per-turn LLM metrics (tokens, cost, duration)
6
+ - Tool execution details
7
+ - Task progress
8
+ - Batch submission for efficient network usage
9
+ """
10
+
11
+ from typing import List, Dict, Any, Optional
12
+ from datetime import datetime, timezone
13
+ import structlog
14
+ import httpx
15
+ from dataclasses import dataclass, field, asdict
16
+ import time
17
+
18
+ logger = structlog.get_logger()
19
+
20
+
21
+ @dataclass
22
+ class TurnMetrics:
23
+ """Metrics for a single LLM turn/interaction"""
24
+ execution_id: str
25
+ turn_number: int
26
+ model: str
27
+ started_at: str # ISO timestamp
28
+ completed_at: Optional[str] = None
29
+ turn_id: Optional[str] = None
30
+ model_provider: Optional[str] = None
31
+ duration_ms: Optional[int] = None
32
+ input_tokens: int = 0
33
+ output_tokens: int = 0
34
+ cache_read_tokens: int = 0
35
+ cache_creation_tokens: int = 0
36
+ total_tokens: int = 0
37
+ input_cost: float = 0.0
38
+ output_cost: float = 0.0
39
+ cache_read_cost: float = 0.0
40
+ cache_creation_cost: float = 0.0
41
+ total_cost: float = 0.0
42
+ finish_reason: Optional[str] = None
43
+ response_preview: Optional[str] = None
44
+ tools_called_count: int = 0
45
+ tools_called_names: List[str] = field(default_factory=list)
46
+ error_message: Optional[str] = None
47
+ metrics: Dict[str, Any] = field(default_factory=dict)
48
+ # Agentic Engineering Minutes (AEM) fields
49
+ runtime_minutes: float = 0.0
50
+ model_weight: float = 1.0
51
+ tool_calls_weight: float = 1.0
52
+ aem_value: float = 0.0
53
+ aem_cost: float = 0.0
54
+
55
+
56
+ @dataclass
57
+ class ToolCallMetrics:
58
+ """Metrics for a single tool execution"""
59
+ execution_id: str
60
+ tool_name: str
61
+ started_at: str # ISO timestamp
62
+ completed_at: Optional[str] = None
63
+ turn_id: Optional[str] = None
64
+ tool_use_id: Optional[str] = None
65
+ duration_ms: Optional[int] = None
66
+ tool_input: Optional[Dict[str, Any]] = None
67
+ tool_output: Optional[str] = None
68
+ tool_output_size: Optional[int] = None
69
+ success: bool = True
70
+ error_message: Optional[str] = None
71
+ error_type: Optional[str] = None
72
+ metadata: Dict[str, Any] = field(default_factory=dict)
73
+
74
+
75
+ @dataclass
76
+ class TaskMetrics:
77
+ """Metrics for a task"""
78
+ execution_id: str
79
+ task_description: str
80
+ task_number: Optional[int] = None
81
+ task_id: Optional[str] = None
82
+ task_type: Optional[str] = None
83
+ status: str = "pending"
84
+ started_at: Optional[str] = None
85
+ completed_at: Optional[str] = None
86
+ duration_ms: Optional[int] = None
87
+ result: Optional[str] = None
88
+ error_message: Optional[str] = None
89
+ metadata: Dict[str, Any] = field(default_factory=dict)
90
+
91
+
92
+ class AnalyticsService:
93
+ """
94
+ Service for collecting and submitting execution analytics to Control Plane.
95
+
96
+ Workers use this service to track detailed execution metrics that are
97
+ persisted to the database for reporting and analysis.
98
+
99
+ Usage:
100
+ analytics = AnalyticsService(control_plane_url, api_key)
101
+
102
+ # Track a turn
103
+ turn = TurnMetrics(
104
+ execution_id=exec_id,
105
+ turn_number=1,
106
+ model="claude-sonnet-4",
107
+ started_at=datetime.now(timezone.utc).isoformat(),
108
+ input_tokens=100,
109
+ output_tokens=200,
110
+ ...
111
+ )
112
+ await analytics.record_turn(turn)
113
+
114
+ # Or collect all metrics and send in batch at the end
115
+ analytics.add_turn(turn)
116
+ analytics.add_tool_call(tool_call)
117
+ await analytics.flush()
118
+ """
119
+
120
+ def __init__(self, control_plane_url: str, api_key: str):
121
+ self.control_plane_url = control_plane_url.rstrip("/")
122
+ self.api_key = api_key
123
+ self.headers = {
124
+ "Authorization": f"UserKey {api_key}",
125
+ "Content-Type": "application/json",
126
+ }
127
+
128
+ # Buffered metrics for batch submission
129
+ self._turns: List[TurnMetrics] = []
130
+ self._tool_calls: List[ToolCallMetrics] = []
131
+ self._tasks: List[TaskMetrics] = []
132
+
133
+ # HTTP client with reasonable timeouts
134
+ self._client = httpx.AsyncClient(
135
+ timeout=httpx.Timeout(30.0, connect=5.0),
136
+ limits=httpx.Limits(max_connections=10),
137
+ )
138
+
139
+ async def aclose(self):
140
+ """Cleanup HTTP client"""
141
+ await self._client.aclose()
142
+
143
+ def add_turn(self, turn: TurnMetrics):
144
+ """Add a turn to the buffer for batch submission"""
145
+ self._turns.append(turn)
146
+
147
+ def add_tool_call(self, tool_call: ToolCallMetrics):
148
+ """Add a tool call to the buffer for batch submission"""
149
+ self._tool_calls.append(tool_call)
150
+
151
+ def add_task(self, task: TaskMetrics):
152
+ """Add a task to the buffer for batch submission"""
153
+ self._tasks.append(task)
154
+
155
+ async def record_turn(self, turn: TurnMetrics) -> bool:
156
+ """
157
+ Immediately submit a turn to Control Plane.
158
+
159
+ Returns:
160
+ True if successful, False otherwise
161
+ """
162
+ try:
163
+ url = f"{self.control_plane_url}/api/v1/analytics/turns"
164
+ payload = self._dataclass_to_dict(turn)
165
+
166
+ response = await self._client.post(url, json=payload, headers=self.headers)
167
+
168
+ if response.status_code not in (200, 201):
169
+ # Try to get error details from response
170
+ try:
171
+ error_detail = response.json() if response.text else "No response body"
172
+ except:
173
+ error_detail = response.text[:500] if response.text else "No response body"
174
+
175
+ logger.warning(
176
+ "turn_submission_failed",
177
+ status=response.status_code,
178
+ execution_id=turn.execution_id[:8],
179
+ error_detail=error_detail,
180
+ )
181
+ return False
182
+
183
+ logger.info(
184
+ "turn_submitted",
185
+ execution_id=turn.execution_id[:8],
186
+ turn_number=turn.turn_number,
187
+ tokens=turn.total_tokens,
188
+ cost=turn.total_cost,
189
+ )
190
+ return True
191
+
192
+ except Exception as e:
193
+ logger.error(
194
+ "turn_submission_error",
195
+ error=str(e),
196
+ execution_id=turn.execution_id[:8],
197
+ )
198
+ return False
199
+
200
+ async def record_tool_call(self, tool_call: ToolCallMetrics) -> bool:
201
+ """
202
+ Immediately submit a tool call to Control Plane.
203
+
204
+ Returns:
205
+ True if successful, False otherwise
206
+ """
207
+ try:
208
+ url = f"{self.control_plane_url}/api/v1/analytics/tool-calls"
209
+ payload = self._dataclass_to_dict(tool_call)
210
+
211
+ response = await self._client.post(url, json=payload, headers=self.headers)
212
+
213
+ if response.status_code not in (200, 201):
214
+ logger.warning(
215
+ "tool_call_submission_failed",
216
+ status=response.status_code,
217
+ execution_id=tool_call.execution_id[:8],
218
+ )
219
+ return False
220
+
221
+ logger.info(
222
+ "tool_call_submitted",
223
+ execution_id=tool_call.execution_id[:8],
224
+ tool_name=tool_call.tool_name,
225
+ success=tool_call.success,
226
+ )
227
+ return True
228
+
229
+ except Exception as e:
230
+ logger.error(
231
+ "tool_call_submission_error",
232
+ error=str(e),
233
+ execution_id=tool_call.execution_id[:8],
234
+ )
235
+ return False
236
+
237
+ async def record_task(self, task: TaskMetrics) -> bool:
238
+ """
239
+ Immediately submit a task to Control Plane.
240
+
241
+ Returns:
242
+ True if successful, False otherwise
243
+ """
244
+ try:
245
+ url = f"{self.control_plane_url}/api/v1/analytics/tasks"
246
+ payload = self._dataclass_to_dict(task)
247
+
248
+ response = await self._client.post(url, json=payload, headers=self.headers)
249
+
250
+ if response.status_code not in (200, 201):
251
+ logger.warning(
252
+ "task_submission_failed",
253
+ status=response.status_code,
254
+ execution_id=task.execution_id[:8],
255
+ )
256
+ return False
257
+
258
+ logger.info(
259
+ "task_submitted",
260
+ execution_id=task.execution_id[:8],
261
+ task_description=task.task_description[:50],
262
+ )
263
+ return True
264
+
265
+ except Exception as e:
266
+ logger.error(
267
+ "task_submission_error",
268
+ error=str(e),
269
+ execution_id=task.execution_id[:8],
270
+ )
271
+ return False
272
+
273
+ async def flush(self, execution_id: str) -> Dict[str, Any]:
274
+ """
275
+ Submit all buffered metrics in a single batch request.
276
+
277
+ This is more efficient than individual submissions when collecting
278
+ multiple metrics throughout an execution.
279
+
280
+ Args:
281
+ execution_id: Execution ID (used for logging)
282
+
283
+ Returns:
284
+ Dict with submission results
285
+ """
286
+ if not self._turns and not self._tool_calls and not self._tasks:
287
+ logger.info("analytics_flush_skipped_no_data", execution_id=execution_id[:8])
288
+ return {
289
+ "success": True,
290
+ "turns_created": 0,
291
+ "tool_calls_created": 0,
292
+ "tasks_created": 0,
293
+ }
294
+
295
+ try:
296
+ url = f"{self.control_plane_url}/api/v1/analytics/batch"
297
+
298
+ payload = {
299
+ "execution_id": execution_id,
300
+ "turns": [self._dataclass_to_dict(t) for t in self._turns],
301
+ "tool_calls": [self._dataclass_to_dict(tc) for tc in self._tool_calls],
302
+ "tasks": [self._dataclass_to_dict(task) for task in self._tasks],
303
+ }
304
+
305
+ response = await self._client.post(url, json=payload, headers=self.headers)
306
+
307
+ if response.status_code not in (200, 201):
308
+ logger.warning(
309
+ "analytics_batch_submission_failed",
310
+ status=response.status_code,
311
+ execution_id=execution_id[:8],
312
+ )
313
+ return {
314
+ "success": False,
315
+ "error": f"HTTP {response.status_code}",
316
+ }
317
+
318
+ result = response.json()
319
+
320
+ logger.info(
321
+ "analytics_batch_submitted",
322
+ execution_id=execution_id[:8],
323
+ turns=result.get("turns_created", 0),
324
+ tool_calls=result.get("tool_calls_created", 0),
325
+ tasks=result.get("tasks_created", 0),
326
+ )
327
+
328
+ # Clear buffers after successful submission
329
+ self._turns.clear()
330
+ self._tool_calls.clear()
331
+ self._tasks.clear()
332
+
333
+ return result
334
+
335
+ except Exception as e:
336
+ logger.error(
337
+ "analytics_batch_submission_error",
338
+ error=str(e),
339
+ execution_id=execution_id[:8],
340
+ )
341
+ return {
342
+ "success": False,
343
+ "error": str(e),
344
+ }
345
+
346
+ def _dataclass_to_dict(self, obj) -> Dict[str, Any]:
347
+ """Convert dataclass to dict, handling nested objects"""
348
+ return asdict(obj)
349
+
350
+ @staticmethod
351
+ def calculate_duration_ms(start_time: float, end_time: float) -> int:
352
+ """Calculate duration in milliseconds from timestamps"""
353
+ return int((end_time - start_time) * 1000)
354
+
355
+ @staticmethod
356
+ def calculate_token_cost(
357
+ input_tokens: int,
358
+ output_tokens: int,
359
+ cache_read_tokens: int = 0,
360
+ cache_creation_tokens: int = 0,
361
+ model: str = "claude-sonnet-4",
362
+ ) -> Dict[str, float]:
363
+ """
364
+ Calculate token costs based on model pricing.
365
+
366
+ Uses centralized pricing configuration from model_pricing.py
367
+
368
+ Returns dict with input_cost, output_cost, cache_read_cost, cache_creation_cost, total_cost
369
+ """
370
+ try:
371
+ from control_plane_api.app.config.model_pricing import calculate_token_cost as calc_cost
372
+ return calc_cost(model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens)
373
+ except ImportError:
374
+ # Fallback to simple calculation if config not available
375
+ logger.warning("model_pricing_not_available_using_fallback", model=model)
376
+
377
+ # Simplified pricing (per 1M tokens)
378
+ pricing = {
379
+ "input": 3.00,
380
+ "output": 15.00,
381
+ "cache_read": 0.30,
382
+ "cache_creation": 3.75,
383
+ }
384
+
385
+ input_cost = (input_tokens / 1_000_000) * pricing["input"]
386
+ output_cost = (output_tokens / 1_000_000) * pricing["output"]
387
+ cache_read_cost = (cache_read_tokens / 1_000_000) * pricing["cache_read"]
388
+ cache_creation_cost = (cache_creation_tokens / 1_000_000) * pricing["cache_creation"]
389
+
390
+ return {
391
+ "input_cost": round(input_cost, 6),
392
+ "output_cost": round(output_cost, 6),
393
+ "cache_read_cost": round(cache_read_cost, 6),
394
+ "cache_creation_cost": round(cache_creation_cost, 6),
395
+ "total_cost": round(input_cost + output_cost + cache_read_cost + cache_creation_cost, 6),
396
+ }
397
+
398
+ @staticmethod
399
+ def calculate_aem(
400
+ duration_ms: int,
401
+ model: str,
402
+ tool_calls_count: int,
403
+ tool_calls_weight: float = 1.0,
404
+ ) -> Dict[str, float]:
405
+ """
406
+ Calculate Agentic Engineering Minutes (AEM).
407
+
408
+ Formula: Runtime (minutes) × Model Weight × Tool Calls Weight
409
+
410
+ Args:
411
+ duration_ms: Turn duration in milliseconds
412
+ model: Model identifier
413
+ tool_calls_count: Number of tool calls
414
+ tool_calls_weight: Optional weight override
415
+
416
+ Returns:
417
+ Dict with AEM metrics (runtime_minutes, model_weight, tool_calls_weight, aem_value, aem_cost)
418
+ """
419
+ try:
420
+ from control_plane_api.app.config.model_pricing import calculate_aem as calc_aem
421
+ return calc_aem(duration_ms, model, tool_calls_count, tool_calls_weight)
422
+ except ImportError:
423
+ # Fallback calculation if config not available
424
+ logger.warning("model_pricing_not_available_using_fallback_aem", model=model)
425
+
426
+ runtime_minutes = duration_ms / 60_000.0
427
+ model_weight = 1.0 # Default weight
428
+ calculated_tool_weight = max(1.0, tool_calls_count / 50.0) if tool_calls_count > 0 else 1.0
429
+ final_tool_weight = tool_calls_weight if tool_calls_weight != 1.0 else calculated_tool_weight
430
+
431
+ aem_value = runtime_minutes * model_weight * final_tool_weight
432
+ aem_cost = aem_value * 0.15 # $0.15/min default
433
+
434
+ return {
435
+ "runtime_minutes": round(runtime_minutes, 4),
436
+ "model_weight": round(model_weight, 2),
437
+ "tool_calls_weight": round(final_tool_weight, 2),
438
+ "aem_value": round(aem_value, 4),
439
+ "aem_cost": round(aem_cost, 4),
440
+ }
441
+
442
+
443
+ # Singleton for convenience
444
+ _analytics_service: Optional[AnalyticsService] = None
445
+
446
+
447
+ def get_analytics_service(control_plane_url: str, api_key: str) -> AnalyticsService:
448
+ """
449
+ Get or create the analytics service singleton.
450
+
451
+ Args:
452
+ control_plane_url: Control Plane API URL
453
+ api_key: Kubiya API key for authentication
454
+
455
+ Returns:
456
+ AnalyticsService instance
457
+ """
458
+ global _analytics_service
459
+
460
+ if _analytics_service is None:
461
+ _analytics_service = AnalyticsService(control_plane_url, api_key)
462
+ logger.info("analytics_service_initialized")
463
+
464
+ return _analytics_service