kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kubiya-control-plane-api might be problematic. Click here for more details.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,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
|
+
}
|