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,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
|