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,257 @@
1
+ """JIRA Tools - Project management integration with lazy validation"""
2
+
3
+ from typing import Optional, Dict, Any
4
+ import structlog
5
+
6
+ logger = structlog.get_logger()
7
+
8
+
9
+ class JiraTools:
10
+ """
11
+ JIRA tools for project management.
12
+
13
+ Validates JIRA authorization on first tool use (lazy validation).
14
+ This allows the LLM to see available tools and only validate when actually needed.
15
+ """
16
+
17
+ def __init__(self, control_plane_client=None, config: Dict[str, Any] = None):
18
+ """
19
+ Initialize JIRA tools.
20
+
21
+ Args:
22
+ control_plane_client: ControlPlaneClient for validation
23
+ config: JIRA configuration from skill
24
+ """
25
+ self.control_plane_client = control_plane_client
26
+ self.config = config or {}
27
+ self._validated = False
28
+ self._validation_result = None
29
+
30
+ def _ensure_validated(self) -> None:
31
+ """
32
+ Validate JIRA integration on first tool use.
33
+
34
+ Raises:
35
+ Exception: With OAuth URL if not authorized
36
+ """
37
+ if self._validated:
38
+ # Already validated this execution
39
+ return
40
+
41
+ if not self.control_plane_client:
42
+ raise Exception(
43
+ "❌ JIRA tools not available: No control plane client configured"
44
+ )
45
+
46
+ # Validate once per execution
47
+ self._validated = True
48
+ result = self.control_plane_client.validate_jira_integration()
49
+ self._validation_result = result
50
+
51
+ if not result.get("valid"):
52
+ message = result.get("message", "JIRA integration not configured")
53
+ oauth_url = result.get("oauth_url")
54
+
55
+ if oauth_url:
56
+ error_msg = (
57
+ f"❌ {message}\n\n"
58
+ f"🔗 Please authorize your JIRA account:\n"
59
+ f"{oauth_url}\n\n"
60
+ f"After authorizing, please retry your request."
61
+ )
62
+ logger.error(
63
+ "jira_not_authorized_on_tool_use",
64
+ message=message,
65
+ oauth_url=oauth_url,
66
+ )
67
+ raise Exception(error_msg)
68
+ else:
69
+ logger.error("jira_validation_failed_on_tool_use", message=message)
70
+ raise Exception(f"❌ {message}")
71
+
72
+ logger.info(
73
+ "jira_validated_on_first_tool_use",
74
+ jira_url=result.get("jira_url"),
75
+ email=result.get("email"),
76
+ )
77
+
78
+ def search_issues(
79
+ self,
80
+ project_key: Optional[str] = None,
81
+ jql: Optional[str] = None,
82
+ max_results: int = 50,
83
+ ) -> str:
84
+ """
85
+ Search JIRA issues.
86
+
87
+ Args:
88
+ project_key: Filter by project (e.g., "PROJ")
89
+ jql: JQL query string
90
+ max_results: Maximum number of results
91
+
92
+ Returns:
93
+ JSON string with issues or error message
94
+ """
95
+ try:
96
+ self._ensure_validated()
97
+
98
+ # TODO: Implement actual JIRA API call
99
+ logger.info(
100
+ "jira_search_issues_called",
101
+ project_key=project_key,
102
+ jql=jql,
103
+ max_results=max_results,
104
+ )
105
+
106
+ return "JIRA search not yet implemented. Coming soon!"
107
+
108
+ except Exception as e:
109
+ return str(e)
110
+
111
+ def get_issue(self, issue_key: str) -> str:
112
+ """
113
+ Get details of a specific JIRA issue.
114
+
115
+ Args:
116
+ issue_key: JIRA issue key (e.g., "PROJ-123")
117
+
118
+ Returns:
119
+ JSON string with issue details or error message
120
+ """
121
+ try:
122
+ self._ensure_validated()
123
+
124
+ # TODO: Implement actual JIRA API call
125
+ logger.info("jira_get_issue_called", issue_key=issue_key)
126
+
127
+ return f"JIRA get issue ({issue_key}) not yet implemented. Coming soon!"
128
+
129
+ except Exception as e:
130
+ return str(e)
131
+
132
+ def create_issue(
133
+ self,
134
+ project_key: str,
135
+ summary: str,
136
+ description: Optional[str] = None,
137
+ issue_type: str = "Task",
138
+ ) -> str:
139
+ """
140
+ Create a new JIRA issue.
141
+
142
+ Args:
143
+ project_key: Project key (e.g., "PROJ")
144
+ summary: Issue title
145
+ description: Issue description
146
+ issue_type: Issue type (Task, Bug, Story, etc.)
147
+
148
+ Returns:
149
+ JSON string with created issue or error message
150
+ """
151
+ try:
152
+ self._ensure_validated()
153
+
154
+ # Check if write is enabled
155
+ if not self.config.get("enable_write", False):
156
+ return "❌ JIRA write operations not enabled for this skill"
157
+
158
+ # TODO: Implement actual JIRA API call
159
+ logger.info(
160
+ "jira_create_issue_called",
161
+ project_key=project_key,
162
+ summary=summary,
163
+ issue_type=issue_type,
164
+ )
165
+
166
+ return "JIRA create issue not yet implemented. Coming soon!"
167
+
168
+ except Exception as e:
169
+ return str(e)
170
+
171
+ def update_issue(
172
+ self,
173
+ issue_key: str,
174
+ summary: Optional[str] = None,
175
+ description: Optional[str] = None,
176
+ ) -> str:
177
+ """
178
+ Update an existing JIRA issue.
179
+
180
+ Args:
181
+ issue_key: JIRA issue key (e.g., "PROJ-123")
182
+ summary: New summary
183
+ description: New description
184
+
185
+ Returns:
186
+ Success message or error
187
+ """
188
+ try:
189
+ self._ensure_validated()
190
+
191
+ # Check if write is enabled
192
+ if not self.config.get("enable_write", False):
193
+ return "❌ JIRA write operations not enabled for this skill"
194
+
195
+ # TODO: Implement actual JIRA API call
196
+ logger.info(
197
+ "jira_update_issue_called",
198
+ issue_key=issue_key,
199
+ has_summary=bool(summary),
200
+ has_description=bool(description),
201
+ )
202
+
203
+ return f"JIRA update issue ({issue_key}) not yet implemented. Coming soon!"
204
+
205
+ except Exception as e:
206
+ return str(e)
207
+
208
+ def transition_issue(self, issue_key: str, transition_name: str) -> str:
209
+ """
210
+ Transition JIRA issue to a new status.
211
+
212
+ Args:
213
+ issue_key: JIRA issue key (e.g., "PROJ-123")
214
+ transition_name: Transition name (e.g., "In Progress", "Done")
215
+
216
+ Returns:
217
+ Success message or error
218
+ """
219
+ try:
220
+ self._ensure_validated()
221
+
222
+ # Check if transitions are enabled
223
+ if not self.config.get("enable_transitions", False):
224
+ return "❌ JIRA transition operations not enabled for this skill"
225
+
226
+ # TODO: Implement actual JIRA API call
227
+ logger.info(
228
+ "jira_transition_issue_called",
229
+ issue_key=issue_key,
230
+ transition_name=transition_name,
231
+ )
232
+
233
+ return (
234
+ f"JIRA transition issue ({issue_key} → {transition_name}) "
235
+ f"not yet implemented. Coming soon!"
236
+ )
237
+
238
+ except Exception as e:
239
+ return str(e)
240
+
241
+ def list_projects(self) -> str:
242
+ """
243
+ List all accessible JIRA projects.
244
+
245
+ Returns:
246
+ JSON string with projects or error message
247
+ """
248
+ try:
249
+ self._ensure_validated()
250
+
251
+ # TODO: Implement actual JIRA API call
252
+ logger.info("jira_list_projects_called")
253
+
254
+ return "JIRA list projects not yet implemented. Coming soon!"
255
+
256
+ except Exception as e:
257
+ return str(e)
@@ -0,0 +1,328 @@
1
+ """
2
+ Runtime-agnostic analytics extraction.
3
+
4
+ This module extracts analytics data from RuntimeExecutionResult objects,
5
+ working with any runtime (Agno, Claude Code, LiteLLM, etc.) that follows
6
+ the standard runtime contract.
7
+ """
8
+
9
+ from typing import Dict, Any, Optional, List
10
+ from datetime import datetime, timezone
11
+ import structlog
12
+ import time
13
+
14
+ from control_plane_api.worker.runtimes.base import RuntimeExecutionResult
15
+ from control_plane_api.worker.services.analytics_service import (
16
+ TurnMetrics,
17
+ ToolCallMetrics,
18
+ AnalyticsService,
19
+ )
20
+
21
+ logger = structlog.get_logger()
22
+
23
+
24
+ class RuntimeAnalyticsExtractor:
25
+ """
26
+ Extracts analytics data from RuntimeExecutionResult.
27
+
28
+ This works with any runtime that populates the standard fields:
29
+ - usage: Token usage metrics
30
+ - model: Model identifier
31
+ - tool_execution_messages: Tool call tracking
32
+ - metadata: Runtime-specific data
33
+ """
34
+
35
+ @staticmethod
36
+ def extract_turn_metrics(
37
+ result: RuntimeExecutionResult,
38
+ execution_id: str,
39
+ turn_number: int,
40
+ turn_start_time: float,
41
+ turn_end_time: Optional[float] = None,
42
+ ) -> TurnMetrics:
43
+ """
44
+ Extract turn metrics from RuntimeExecutionResult.
45
+
46
+ Works with any runtime that populates the usage field.
47
+
48
+ Args:
49
+ result: Runtime execution result
50
+ execution_id: Execution ID
51
+ turn_number: Turn sequence number
52
+ turn_start_time: When turn started (timestamp)
53
+ turn_end_time: When turn ended (timestamp, defaults to now)
54
+
55
+ Returns:
56
+ TurnMetrics ready for submission
57
+ """
58
+ if turn_end_time is None:
59
+ turn_end_time = time.time()
60
+
61
+ duration_ms = int((turn_end_time - turn_start_time) * 1000)
62
+
63
+ # Extract usage - runtimes use different field names
64
+ usage = result.usage or {}
65
+
66
+ # Normalize field names from different providers
67
+ input_tokens = (
68
+ usage.get("input_tokens") or
69
+ usage.get("prompt_tokens") or
70
+ 0
71
+ )
72
+ output_tokens = (
73
+ usage.get("output_tokens") or
74
+ usage.get("completion_tokens") or
75
+ 0
76
+ )
77
+ total_tokens = (
78
+ usage.get("total_tokens") or
79
+ (input_tokens + output_tokens)
80
+ )
81
+
82
+ # Cache tokens (Anthropic-specific, but other providers may add support)
83
+ cache_read_tokens = usage.get("cache_read_tokens", 0)
84
+ cache_creation_tokens = usage.get("cache_creation_tokens", 0)
85
+
86
+ # Alternative: extract from prompt_tokens_details if present
87
+ if "prompt_tokens_details" in usage:
88
+ details = usage["prompt_tokens_details"]
89
+ if isinstance(details, dict):
90
+ cache_read_tokens = details.get("cached_tokens", cache_read_tokens)
91
+
92
+ # Extract tool names from tool_execution_messages (needed for AEM calculation)
93
+ tool_names = []
94
+ tools_count = 0
95
+ if result.tool_execution_messages:
96
+ tool_names = [msg.get("tool") for msg in result.tool_execution_messages if msg.get("tool")]
97
+ tools_count = len(tool_names)
98
+
99
+ # Calculate costs
100
+ model = result.model or "unknown"
101
+ costs = AnalyticsService.calculate_token_cost(
102
+ input_tokens=input_tokens,
103
+ output_tokens=output_tokens,
104
+ cache_read_tokens=cache_read_tokens,
105
+ cache_creation_tokens=cache_creation_tokens,
106
+ model=model,
107
+ )
108
+
109
+ # Calculate Agentic Engineering Minutes (AEM)
110
+ aem_metrics = AnalyticsService.calculate_aem(
111
+ duration_ms=duration_ms,
112
+ model=model,
113
+ tool_calls_count=tools_count,
114
+ )
115
+
116
+ # Extract model provider from metadata or infer from model name
117
+ metadata = result.metadata or {}
118
+ model_provider = metadata.get("model_provider") or RuntimeAnalyticsExtractor._infer_provider(model)
119
+
120
+ # Response preview (first 500 chars)
121
+ response_preview = result.response[:500] if result.response else None
122
+
123
+ return TurnMetrics(
124
+ execution_id=execution_id,
125
+ turn_number=turn_number,
126
+ model=model,
127
+ model_provider=model_provider,
128
+ started_at=datetime.fromtimestamp(turn_start_time, timezone.utc).isoformat(),
129
+ completed_at=datetime.fromtimestamp(turn_end_time, timezone.utc).isoformat(),
130
+ duration_ms=duration_ms,
131
+ input_tokens=input_tokens,
132
+ output_tokens=output_tokens,
133
+ cache_read_tokens=cache_read_tokens,
134
+ cache_creation_tokens=cache_creation_tokens,
135
+ total_tokens=total_tokens,
136
+ input_cost=costs["input_cost"],
137
+ output_cost=costs["output_cost"],
138
+ cache_read_cost=costs["cache_read_cost"],
139
+ cache_creation_cost=costs["cache_creation_cost"],
140
+ total_cost=costs["total_cost"],
141
+ finish_reason=result.finish_reason or "stop",
142
+ response_preview=response_preview,
143
+ tools_called_count=tools_count,
144
+ tools_called_names=list(set(tool_names)), # Unique tool names
145
+ error_message=result.error,
146
+ metrics=metadata, # Include runtime-specific metrics
147
+ # AEM metrics
148
+ runtime_minutes=aem_metrics["runtime_minutes"],
149
+ model_weight=aem_metrics["model_weight"],
150
+ tool_calls_weight=aem_metrics["tool_calls_weight"],
151
+ aem_value=aem_metrics["aem_value"],
152
+ aem_cost=aem_metrics["aem_cost"],
153
+ )
154
+
155
+ @staticmethod
156
+ def extract_tool_call_metrics(
157
+ result: RuntimeExecutionResult,
158
+ execution_id: str,
159
+ turn_id: Optional[str] = None,
160
+ ) -> List[ToolCallMetrics]:
161
+ """
162
+ Extract tool call metrics from RuntimeExecutionResult.
163
+
164
+ Works with any runtime that populates tool_execution_messages.
165
+
166
+ Args:
167
+ result: Runtime execution result
168
+ execution_id: Execution ID
169
+ turn_id: Turn ID to link tool calls to
170
+
171
+ Returns:
172
+ List of ToolCallMetrics ready for submission
173
+ """
174
+ if not result.tool_execution_messages:
175
+ return []
176
+
177
+ tool_calls = []
178
+
179
+ for tool_msg in result.tool_execution_messages:
180
+ # Extract timing information
181
+ # Runtimes should provide start_time/end_time or duration_ms
182
+ duration_ms = tool_msg.get("duration_ms")
183
+ start_time = tool_msg.get("start_time")
184
+ end_time = tool_msg.get("end_time")
185
+
186
+ # Calculate timestamps
187
+ if start_time and end_time:
188
+ started_at = datetime.fromtimestamp(start_time, timezone.utc).isoformat()
189
+ completed_at = datetime.fromtimestamp(end_time, timezone.utc).isoformat()
190
+ if duration_ms is None:
191
+ duration_ms = int((end_time - start_time) * 1000)
192
+ else:
193
+ # Fallback to current time if not provided
194
+ now = datetime.now(timezone.utc).isoformat()
195
+ started_at = now
196
+ completed_at = now
197
+
198
+ # Extract tool output
199
+ tool_output = tool_msg.get("output") or tool_msg.get("result")
200
+ if tool_output and not isinstance(tool_output, str):
201
+ tool_output = str(tool_output)
202
+
203
+ # Success status
204
+ success = tool_msg.get("success", True)
205
+
206
+ tool_call = ToolCallMetrics(
207
+ execution_id=execution_id,
208
+ turn_id=turn_id,
209
+ tool_name=tool_msg.get("tool", "unknown"),
210
+ tool_use_id=tool_msg.get("tool_use_id"),
211
+ started_at=started_at,
212
+ completed_at=completed_at,
213
+ duration_ms=duration_ms,
214
+ tool_input=tool_msg.get("input"),
215
+ tool_output=tool_output,
216
+ tool_output_size=len(tool_output) if tool_output else 0,
217
+ success=success,
218
+ error_message=tool_msg.get("error"),
219
+ error_type=tool_msg.get("error_type"),
220
+ metadata=tool_msg.get("metadata", {}),
221
+ )
222
+
223
+ tool_calls.append(tool_call)
224
+
225
+ return tool_calls
226
+
227
+ @staticmethod
228
+ def _infer_provider(model: str) -> str:
229
+ """
230
+ Infer provider from model identifier.
231
+
232
+ Args:
233
+ model: Model string
234
+
235
+ Returns:
236
+ Provider name
237
+ """
238
+ model_lower = model.lower()
239
+
240
+ if "claude" in model_lower or "anthropic" in model_lower:
241
+ return "anthropic"
242
+ elif "gpt" in model_lower or "openai" in model_lower:
243
+ return "openai"
244
+ elif "gemini" in model_lower or "google" in model_lower:
245
+ return "google"
246
+ elif "llama" in model_lower or "meta" in model_lower:
247
+ return "meta"
248
+ elif "mistral" in model_lower:
249
+ return "mistral"
250
+ elif "command" in model_lower or "cohere" in model_lower:
251
+ return "cohere"
252
+ else:
253
+ return "unknown"
254
+
255
+
256
+ async def submit_runtime_analytics(
257
+ result: RuntimeExecutionResult,
258
+ execution_id: str,
259
+ turn_number: int,
260
+ turn_start_time: float,
261
+ analytics_service: AnalyticsService,
262
+ turn_end_time: Optional[float] = None,
263
+ ) -> Dict[str, Any]:
264
+ """
265
+ Extract and submit all analytics from a RuntimeExecutionResult.
266
+
267
+ This is the main entry point for submitting analytics after a runtime execution.
268
+ It extracts turn metrics and tool call metrics and submits them asynchronously.
269
+
270
+ Args:
271
+ result: Runtime execution result
272
+ execution_id: Execution ID
273
+ turn_number: Turn sequence number
274
+ turn_start_time: When turn started
275
+ analytics_service: Analytics service instance
276
+ turn_end_time: When turn ended (defaults to now)
277
+
278
+ Returns:
279
+ Dict with submission status
280
+ """
281
+ try:
282
+ # Extract turn metrics
283
+ turn_metrics = RuntimeAnalyticsExtractor.extract_turn_metrics(
284
+ result=result,
285
+ execution_id=execution_id,
286
+ turn_number=turn_number,
287
+ turn_start_time=turn_start_time,
288
+ turn_end_time=turn_end_time,
289
+ )
290
+
291
+ # Submit turn metrics
292
+ await analytics_service.record_turn(turn_metrics)
293
+
294
+ # Extract and submit tool call metrics
295
+ tool_call_metrics = RuntimeAnalyticsExtractor.extract_tool_call_metrics(
296
+ result=result,
297
+ execution_id=execution_id,
298
+ turn_id=None, # Could link to turn ID if available
299
+ )
300
+
301
+ for tool_call in tool_call_metrics:
302
+ await analytics_service.record_tool_call(tool_call)
303
+
304
+ logger.info(
305
+ "runtime_analytics_submitted",
306
+ execution_id=execution_id[:8],
307
+ turn_number=turn_number,
308
+ tokens=turn_metrics.total_tokens,
309
+ cost=turn_metrics.total_cost,
310
+ tool_calls=len(tool_call_metrics),
311
+ )
312
+
313
+ return {
314
+ "success": True,
315
+ "turn_submitted": True,
316
+ "tool_calls_submitted": len(tool_call_metrics),
317
+ }
318
+
319
+ except Exception as e:
320
+ logger.error(
321
+ "runtime_analytics_submission_failed",
322
+ error=str(e),
323
+ execution_id=execution_id[:8],
324
+ )
325
+ return {
326
+ "success": False,
327
+ "error": str(e),
328
+ }