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,457 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytics collector that integrates with runtime execution to track metrics.
|
|
3
|
+
|
|
4
|
+
This module provides hooks into the execution lifecycle to automatically
|
|
5
|
+
collect and submit analytics data without blocking execution.
|
|
6
|
+
|
|
7
|
+
Key features:
|
|
8
|
+
- Leverages LiteLLM and Agno native usage tracking
|
|
9
|
+
- Async fire-and-forget submission (doesn't block execution)
|
|
10
|
+
- Comprehensive error handling (failures don't break execution)
|
|
11
|
+
- Per-turn, tool call, and task tracking
|
|
12
|
+
- Automatic cost calculation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Dict, Any, Optional, List
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
import structlog
|
|
18
|
+
import asyncio
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
from control_plane_api.worker.services.analytics_service import (
|
|
23
|
+
AnalyticsService,
|
|
24
|
+
TurnMetrics,
|
|
25
|
+
ToolCallMetrics,
|
|
26
|
+
TaskMetrics,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = structlog.get_logger()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ExecutionContext:
|
|
34
|
+
"""Context for tracking execution metrics"""
|
|
35
|
+
execution_id: str
|
|
36
|
+
organization_id: str
|
|
37
|
+
turn_number: int = 0
|
|
38
|
+
turn_start_time: Optional[float] = None
|
|
39
|
+
current_turn_id: Optional[str] = None
|
|
40
|
+
tools_in_turn: List[str] = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
if self.tools_in_turn is None:
|
|
44
|
+
self.tools_in_turn = []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AnalyticsCollector:
|
|
48
|
+
"""
|
|
49
|
+
Collects and submits analytics during execution.
|
|
50
|
+
|
|
51
|
+
This collector integrates with the runtime to automatically track:
|
|
52
|
+
- LLM turns with token usage and costs
|
|
53
|
+
- Tool executions with timing and results
|
|
54
|
+
- Task progress
|
|
55
|
+
|
|
56
|
+
All submissions are async and failures are logged but don't break execution.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, analytics_service: AnalyticsService):
|
|
60
|
+
self.analytics = analytics_service
|
|
61
|
+
self._submission_tasks: List[asyncio.Task] = []
|
|
62
|
+
|
|
63
|
+
def start_turn(self, ctx: ExecutionContext) -> ExecutionContext:
|
|
64
|
+
"""
|
|
65
|
+
Mark the start of a new LLM turn.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
ctx: Execution context
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Updated context with turn tracking
|
|
72
|
+
"""
|
|
73
|
+
ctx.turn_number += 1
|
|
74
|
+
ctx.turn_start_time = time.time()
|
|
75
|
+
ctx.tools_in_turn = []
|
|
76
|
+
|
|
77
|
+
logger.debug(
|
|
78
|
+
"turn_started",
|
|
79
|
+
execution_id=ctx.execution_id[:8],
|
|
80
|
+
turn_number=ctx.turn_number,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return ctx
|
|
84
|
+
|
|
85
|
+
def record_turn_from_litellm(
|
|
86
|
+
self,
|
|
87
|
+
ctx: ExecutionContext,
|
|
88
|
+
response: Any,
|
|
89
|
+
model: str,
|
|
90
|
+
finish_reason: str = "stop",
|
|
91
|
+
error_message: Optional[str] = None,
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Record turn metrics from LiteLLM response.
|
|
95
|
+
|
|
96
|
+
LiteLLM responses have `usage` attribute with token counts.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
ctx: Execution context
|
|
100
|
+
response: LiteLLM completion response
|
|
101
|
+
model: Model identifier
|
|
102
|
+
finish_reason: Why the turn finished
|
|
103
|
+
error_message: Error if turn failed
|
|
104
|
+
"""
|
|
105
|
+
if not ctx.turn_start_time:
|
|
106
|
+
logger.warning("record_turn_called_without_start", execution_id=ctx.execution_id[:8])
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
turn_end_time = time.time()
|
|
110
|
+
duration_ms = int((turn_end_time - ctx.turn_start_time) * 1000)
|
|
111
|
+
|
|
112
|
+
# Extract usage from LiteLLM response
|
|
113
|
+
usage = getattr(response, "usage", None)
|
|
114
|
+
if not usage:
|
|
115
|
+
logger.warning("litellm_response_missing_usage", execution_id=ctx.execution_id[:8])
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
input_tokens = getattr(usage, "prompt_tokens", 0)
|
|
119
|
+
output_tokens = getattr(usage, "completion_tokens", 0)
|
|
120
|
+
total_tokens = getattr(usage, "total_tokens", 0)
|
|
121
|
+
|
|
122
|
+
# Anthropic-specific: cache tokens
|
|
123
|
+
cache_read_tokens = getattr(usage, "prompt_tokens_details", {}).get("cached_tokens", 0) if hasattr(usage, "prompt_tokens_details") else 0
|
|
124
|
+
cache_creation_tokens = 0 # LiteLLM doesn't expose this directly
|
|
125
|
+
|
|
126
|
+
# Extract response content
|
|
127
|
+
response_content = ""
|
|
128
|
+
if hasattr(response, "choices") and response.choices:
|
|
129
|
+
message = response.choices[0].message
|
|
130
|
+
response_content = getattr(message, "content", "")
|
|
131
|
+
|
|
132
|
+
# Calculate costs
|
|
133
|
+
costs = AnalyticsService.calculate_token_cost(
|
|
134
|
+
input_tokens=input_tokens,
|
|
135
|
+
output_tokens=output_tokens,
|
|
136
|
+
cache_read_tokens=cache_read_tokens,
|
|
137
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
138
|
+
model=model,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Determine model provider from model string
|
|
142
|
+
model_provider = self._extract_provider(model)
|
|
143
|
+
|
|
144
|
+
turn = TurnMetrics(
|
|
145
|
+
execution_id=ctx.execution_id,
|
|
146
|
+
turn_number=ctx.turn_number,
|
|
147
|
+
model=model,
|
|
148
|
+
model_provider=model_provider,
|
|
149
|
+
started_at=datetime.fromtimestamp(ctx.turn_start_time, timezone.utc).isoformat(),
|
|
150
|
+
completed_at=datetime.fromtimestamp(turn_end_time, timezone.utc).isoformat(),
|
|
151
|
+
duration_ms=duration_ms,
|
|
152
|
+
input_tokens=input_tokens,
|
|
153
|
+
output_tokens=output_tokens,
|
|
154
|
+
cache_read_tokens=cache_read_tokens,
|
|
155
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
156
|
+
total_tokens=total_tokens,
|
|
157
|
+
input_cost=costs["input_cost"],
|
|
158
|
+
output_cost=costs["output_cost"],
|
|
159
|
+
cache_read_cost=costs["cache_read_cost"],
|
|
160
|
+
cache_creation_cost=costs["cache_creation_cost"],
|
|
161
|
+
total_cost=costs["total_cost"],
|
|
162
|
+
finish_reason=finish_reason,
|
|
163
|
+
response_preview=response_content[:500] if response_content else None,
|
|
164
|
+
tools_called_count=len(ctx.tools_in_turn),
|
|
165
|
+
tools_called_names=list(set(ctx.tools_in_turn)), # Unique tool names
|
|
166
|
+
error_message=error_message,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Submit async (fire-and-forget)
|
|
170
|
+
self._submit_async(turn, "turn")
|
|
171
|
+
|
|
172
|
+
# Reset turn context
|
|
173
|
+
ctx.turn_start_time = None
|
|
174
|
+
ctx.tools_in_turn = []
|
|
175
|
+
|
|
176
|
+
def record_turn_from_agno(
|
|
177
|
+
self,
|
|
178
|
+
ctx: ExecutionContext,
|
|
179
|
+
result: Any,
|
|
180
|
+
model: str,
|
|
181
|
+
finish_reason: str = "stop",
|
|
182
|
+
error_message: Optional[str] = None,
|
|
183
|
+
):
|
|
184
|
+
"""
|
|
185
|
+
Record turn metrics from Agno/phidata result.
|
|
186
|
+
|
|
187
|
+
Agno results have `metrics` attribute with token counts.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
ctx: Execution context
|
|
191
|
+
result: Agno RunResponse
|
|
192
|
+
model: Model identifier
|
|
193
|
+
finish_reason: Why the turn finished
|
|
194
|
+
error_message: Error if turn failed
|
|
195
|
+
"""
|
|
196
|
+
if not ctx.turn_start_time:
|
|
197
|
+
logger.warning("record_turn_called_without_start", execution_id=ctx.execution_id[:8])
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
turn_end_time = time.time()
|
|
201
|
+
duration_ms = int((turn_end_time - ctx.turn_start_time) * 1000)
|
|
202
|
+
|
|
203
|
+
# Extract usage from Agno result
|
|
204
|
+
metrics = getattr(result, "metrics", None)
|
|
205
|
+
if not metrics:
|
|
206
|
+
logger.warning("agno_result_missing_metrics", execution_id=ctx.execution_id[:8])
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
input_tokens = getattr(metrics, "input_tokens", 0)
|
|
210
|
+
output_tokens = getattr(metrics, "output_tokens", 0)
|
|
211
|
+
total_tokens = getattr(metrics, "total_tokens", 0)
|
|
212
|
+
|
|
213
|
+
# Anthropic-specific: Agno exposes cache tokens
|
|
214
|
+
cache_read_tokens = getattr(metrics, "input_token_details", {}).get("cache_read", 0) if hasattr(metrics, "input_token_details") else 0
|
|
215
|
+
cache_creation_tokens = getattr(metrics, "input_token_details", {}).get("cache_creation", 0) if hasattr(metrics, "input_token_details") else 0
|
|
216
|
+
|
|
217
|
+
# Extract response content
|
|
218
|
+
response_content = getattr(result, "content", "")
|
|
219
|
+
|
|
220
|
+
# Calculate costs
|
|
221
|
+
costs = AnalyticsService.calculate_token_cost(
|
|
222
|
+
input_tokens=input_tokens,
|
|
223
|
+
output_tokens=output_tokens,
|
|
224
|
+
cache_read_tokens=cache_read_tokens,
|
|
225
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
226
|
+
model=model,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Determine model provider
|
|
230
|
+
model_provider = self._extract_provider(model)
|
|
231
|
+
|
|
232
|
+
turn = TurnMetrics(
|
|
233
|
+
execution_id=ctx.execution_id,
|
|
234
|
+
turn_number=ctx.turn_number,
|
|
235
|
+
model=model,
|
|
236
|
+
model_provider=model_provider,
|
|
237
|
+
started_at=datetime.fromtimestamp(ctx.turn_start_time, timezone.utc).isoformat(),
|
|
238
|
+
completed_at=datetime.fromtimestamp(turn_end_time, timezone.utc).isoformat(),
|
|
239
|
+
duration_ms=duration_ms,
|
|
240
|
+
input_tokens=input_tokens,
|
|
241
|
+
output_tokens=output_tokens,
|
|
242
|
+
cache_read_tokens=cache_read_tokens,
|
|
243
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
244
|
+
total_tokens=total_tokens,
|
|
245
|
+
input_cost=costs["input_cost"],
|
|
246
|
+
output_cost=costs["output_cost"],
|
|
247
|
+
cache_read_cost=costs["cache_read_cost"],
|
|
248
|
+
cache_creation_cost=costs["cache_creation_cost"],
|
|
249
|
+
total_cost=costs["total_cost"],
|
|
250
|
+
finish_reason=finish_reason,
|
|
251
|
+
response_preview=response_content[:500] if response_content else None,
|
|
252
|
+
tools_called_count=len(ctx.tools_in_turn),
|
|
253
|
+
tools_called_names=list(set(ctx.tools_in_turn)),
|
|
254
|
+
error_message=error_message,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Submit async (fire-and-forget)
|
|
258
|
+
self._submit_async(turn, "turn")
|
|
259
|
+
|
|
260
|
+
# Reset turn context
|
|
261
|
+
ctx.turn_start_time = None
|
|
262
|
+
ctx.tools_in_turn = []
|
|
263
|
+
|
|
264
|
+
def record_tool_call(
|
|
265
|
+
self,
|
|
266
|
+
ctx: ExecutionContext,
|
|
267
|
+
tool_name: str,
|
|
268
|
+
tool_input: Optional[Dict[str, Any]],
|
|
269
|
+
tool_output: Optional[str],
|
|
270
|
+
start_time: float,
|
|
271
|
+
end_time: float,
|
|
272
|
+
success: bool = True,
|
|
273
|
+
error_message: Optional[str] = None,
|
|
274
|
+
error_type: Optional[str] = None,
|
|
275
|
+
):
|
|
276
|
+
"""
|
|
277
|
+
Record a tool call.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
ctx: Execution context
|
|
281
|
+
tool_name: Name of the tool
|
|
282
|
+
tool_input: Tool parameters
|
|
283
|
+
tool_output: Tool result
|
|
284
|
+
start_time: Start timestamp
|
|
285
|
+
end_time: End timestamp
|
|
286
|
+
success: Whether tool call succeeded
|
|
287
|
+
error_message: Error if failed
|
|
288
|
+
error_type: Type of error
|
|
289
|
+
"""
|
|
290
|
+
# Track tool in current turn
|
|
291
|
+
if ctx.tools_in_turn is not None:
|
|
292
|
+
ctx.tools_in_turn.append(tool_name)
|
|
293
|
+
|
|
294
|
+
duration_ms = int((end_time - start_time) * 1000)
|
|
295
|
+
|
|
296
|
+
tool_call = ToolCallMetrics(
|
|
297
|
+
execution_id=ctx.execution_id,
|
|
298
|
+
turn_id=ctx.current_turn_id,
|
|
299
|
+
tool_name=tool_name,
|
|
300
|
+
started_at=datetime.fromtimestamp(start_time, timezone.utc).isoformat(),
|
|
301
|
+
completed_at=datetime.fromtimestamp(end_time, timezone.utc).isoformat(),
|
|
302
|
+
duration_ms=duration_ms,
|
|
303
|
+
tool_input=tool_input,
|
|
304
|
+
tool_output=tool_output,
|
|
305
|
+
tool_output_size=len(tool_output) if tool_output else 0,
|
|
306
|
+
success=success,
|
|
307
|
+
error_message=error_message,
|
|
308
|
+
error_type=error_type,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Submit async (fire-and-forget)
|
|
312
|
+
self._submit_async(tool_call, "tool_call")
|
|
313
|
+
|
|
314
|
+
def record_task(
|
|
315
|
+
self,
|
|
316
|
+
ctx: ExecutionContext,
|
|
317
|
+
task_description: str,
|
|
318
|
+
task_number: Optional[int] = None,
|
|
319
|
+
task_type: Optional[str] = None,
|
|
320
|
+
status: str = "pending",
|
|
321
|
+
started_at: Optional[str] = None,
|
|
322
|
+
) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Record a task creation.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
ctx: Execution context
|
|
328
|
+
task_description: Task description
|
|
329
|
+
task_number: Sequential task number
|
|
330
|
+
task_type: Type of task
|
|
331
|
+
status: Initial status
|
|
332
|
+
started_at: Start timestamp
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Task ID for later updates
|
|
336
|
+
"""
|
|
337
|
+
task = TaskMetrics(
|
|
338
|
+
execution_id=ctx.execution_id,
|
|
339
|
+
task_number=task_number,
|
|
340
|
+
task_description=task_description,
|
|
341
|
+
task_type=task_type,
|
|
342
|
+
status=status,
|
|
343
|
+
started_at=started_at or datetime.now(timezone.utc).isoformat(),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Submit async (fire-and-forget)
|
|
347
|
+
self._submit_async(task, "task")
|
|
348
|
+
|
|
349
|
+
# Return a synthetic task ID (in practice, you'd get this from the API response)
|
|
350
|
+
return f"{ctx.execution_id}:{task_number or 0}"
|
|
351
|
+
|
|
352
|
+
def _submit_async(self, metric: Any, metric_type: str):
|
|
353
|
+
"""
|
|
354
|
+
Submit metric asynchronously without blocking execution.
|
|
355
|
+
|
|
356
|
+
This uses fire-and-forget pattern - failures are logged but don't
|
|
357
|
+
affect the execution flow.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
metric: Metric data (TurnMetrics, ToolCallMetrics, or TaskMetrics)
|
|
361
|
+
metric_type: Type of metric for logging
|
|
362
|
+
"""
|
|
363
|
+
async def submit():
|
|
364
|
+
try:
|
|
365
|
+
if isinstance(metric, TurnMetrics):
|
|
366
|
+
await self.analytics.record_turn(metric)
|
|
367
|
+
elif isinstance(metric, ToolCallMetrics):
|
|
368
|
+
await self.analytics.record_tool_call(metric)
|
|
369
|
+
elif isinstance(metric, TaskMetrics):
|
|
370
|
+
await self.analytics.record_task(metric)
|
|
371
|
+
else:
|
|
372
|
+
logger.error("unknown_metric_type", type=type(metric).__name__)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
# Log error but don't re-raise - analytics failures shouldn't break execution
|
|
375
|
+
logger.error(
|
|
376
|
+
"analytics_submission_failed",
|
|
377
|
+
metric_type=metric_type,
|
|
378
|
+
error=str(e),
|
|
379
|
+
execution_id=getattr(metric, "execution_id", "unknown")[:8],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Create task and track it (for cleanup)
|
|
383
|
+
task = asyncio.create_task(submit())
|
|
384
|
+
self._submission_tasks.append(task)
|
|
385
|
+
|
|
386
|
+
# Clean up completed tasks
|
|
387
|
+
self._submission_tasks = [t for t in self._submission_tasks if not t.done()]
|
|
388
|
+
|
|
389
|
+
async def wait_for_submissions(self, timeout: float = 5.0):
|
|
390
|
+
"""
|
|
391
|
+
Wait for all pending analytics submissions to complete.
|
|
392
|
+
|
|
393
|
+
Call this at the end of execution to ensure all analytics are submitted
|
|
394
|
+
before the worker shuts down.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
timeout: Maximum time to wait in seconds
|
|
398
|
+
"""
|
|
399
|
+
if not self._submission_tasks:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
await asyncio.wait_for(
|
|
404
|
+
asyncio.gather(*self._submission_tasks, return_exceptions=True),
|
|
405
|
+
timeout=timeout
|
|
406
|
+
)
|
|
407
|
+
logger.info(
|
|
408
|
+
"analytics_submissions_completed",
|
|
409
|
+
count=len(self._submission_tasks)
|
|
410
|
+
)
|
|
411
|
+
except asyncio.TimeoutError:
|
|
412
|
+
logger.warning(
|
|
413
|
+
"analytics_submissions_timeout",
|
|
414
|
+
pending=len([t for t in self._submission_tasks if not t.done()])
|
|
415
|
+
)
|
|
416
|
+
except Exception as e:
|
|
417
|
+
logger.error("analytics_wait_error", error=str(e))
|
|
418
|
+
finally:
|
|
419
|
+
self._submission_tasks.clear()
|
|
420
|
+
|
|
421
|
+
def _extract_provider(self, model: str) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Extract provider from model identifier.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
model: Model string like "claude-sonnet-4", "gpt-4", "openai/gpt-4"
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Provider name
|
|
430
|
+
"""
|
|
431
|
+
model_lower = model.lower()
|
|
432
|
+
|
|
433
|
+
if "claude" in model_lower or "anthropic" in model_lower:
|
|
434
|
+
return "anthropic"
|
|
435
|
+
elif "gpt" in model_lower or "openai" in model_lower:
|
|
436
|
+
return "openai"
|
|
437
|
+
elif "gemini" in model_lower or "google" in model_lower:
|
|
438
|
+
return "google"
|
|
439
|
+
elif "llama" in model_lower or "meta" in model_lower:
|
|
440
|
+
return "meta"
|
|
441
|
+
else:
|
|
442
|
+
return "unknown"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def create_analytics_collector(control_plane_url: str, api_key: str) -> AnalyticsCollector:
|
|
446
|
+
"""
|
|
447
|
+
Create an analytics collector instance.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
control_plane_url: Control Plane API URL
|
|
451
|
+
api_key: Kubiya API key
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Analytics collector instance
|
|
455
|
+
"""
|
|
456
|
+
analytics_service = AnalyticsService(control_plane_url, api_key)
|
|
457
|
+
return AnalyticsCollector(analytics_service)
|