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,379 @@
|
|
|
1
|
+
"""Runners endpoint - proxies to Kubiya API"""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, Request, HTTPException, Query
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
import structlog
|
|
6
|
+
import httpx
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
from control_plane_api.app.middleware.auth import get_current_organization
|
|
10
|
+
from control_plane_api.app.lib.kubiya_client import get_kubiya_client
|
|
11
|
+
from control_plane_api.app.config import settings
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger()
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
# Valid component names for filtering
|
|
18
|
+
VALID_COMPONENTS = [
|
|
19
|
+
"workflow_engine",
|
|
20
|
+
"workflow-engine",
|
|
21
|
+
"workflowEngine",
|
|
22
|
+
"tool-manager",
|
|
23
|
+
"tool_manager",
|
|
24
|
+
"toolManager",
|
|
25
|
+
"agent-manager",
|
|
26
|
+
"agent_manager",
|
|
27
|
+
"agentManager",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("")
|
|
32
|
+
async def list_runners(
|
|
33
|
+
request: Request,
|
|
34
|
+
organization: dict = Depends(get_current_organization),
|
|
35
|
+
component: Optional[str] = Query(
|
|
36
|
+
None,
|
|
37
|
+
description="Filter runners by healthy component (e.g., 'workflow_engine', 'tool-manager'). If not specified, returns all runners with health data.",
|
|
38
|
+
),
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
List available runners for the organization.
|
|
42
|
+
Fetches health data for each runner and optionally filters by component health.
|
|
43
|
+
|
|
44
|
+
Query Parameters:
|
|
45
|
+
- component: Optional component name to filter by (workflow_engine, tool-manager, etc.)
|
|
46
|
+
|
|
47
|
+
Returns only runners with the specified healthy component, or all runners if no filter specified.
|
|
48
|
+
"""
|
|
49
|
+
# Validate component parameter if provided
|
|
50
|
+
if component and component not in VALID_COMPONENTS:
|
|
51
|
+
raise HTTPException(
|
|
52
|
+
status_code=400,
|
|
53
|
+
detail={
|
|
54
|
+
"error": f"Invalid component '{component}'",
|
|
55
|
+
"valid_components": VALID_COMPONENTS,
|
|
56
|
+
"message": f"Valid choices are: {', '.join(VALID_COMPONENTS)}",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
kubiya_client = get_kubiya_client()
|
|
62
|
+
token = request.state.kubiya_token
|
|
63
|
+
|
|
64
|
+
runners = await kubiya_client.get_runners(token, organization["id"])
|
|
65
|
+
|
|
66
|
+
# Fetch health for each runner in parallel
|
|
67
|
+
kubiya_api_base = settings.kubiya_api_base
|
|
68
|
+
|
|
69
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
70
|
+
# Create health check tasks for all runners
|
|
71
|
+
health_tasks = []
|
|
72
|
+
for runner in runners:
|
|
73
|
+
runner_name = runner.get("name")
|
|
74
|
+
if runner_name:
|
|
75
|
+
health_tasks.append(_fetch_runner_health(client, kubiya_api_base, token, runner_name))
|
|
76
|
+
else:
|
|
77
|
+
health_tasks.append(None)
|
|
78
|
+
|
|
79
|
+
# Execute all health checks in parallel
|
|
80
|
+
health_results = await asyncio.gather(*health_tasks, return_exceptions=True)
|
|
81
|
+
|
|
82
|
+
# Combine runners with their health data and optionally filter by component
|
|
83
|
+
filtered_runners = []
|
|
84
|
+
for runner, health_data in zip(runners, health_results):
|
|
85
|
+
runner_name = runner.get("name")
|
|
86
|
+
|
|
87
|
+
# Determine if we should include this runner
|
|
88
|
+
should_include = False
|
|
89
|
+
status = "unknown"
|
|
90
|
+
|
|
91
|
+
# If no component filter specified, include all runners
|
|
92
|
+
if not component:
|
|
93
|
+
should_include = True
|
|
94
|
+
# Determine status from health data if available
|
|
95
|
+
if health_data and not isinstance(health_data, Exception):
|
|
96
|
+
runner["health_data"] = health_data
|
|
97
|
+
status = _determine_runner_status_from_health(runner, health_data)
|
|
98
|
+
else:
|
|
99
|
+
# No health data or error - mark as unknown or active
|
|
100
|
+
status = "unknown" if isinstance(health_data, Exception) else "active"
|
|
101
|
+
else:
|
|
102
|
+
# Component filter specified - only include if component is healthy
|
|
103
|
+
if health_data and not isinstance(health_data, Exception):
|
|
104
|
+
runner["health_data"] = health_data
|
|
105
|
+
should_include = _has_healthy_component(health_data, component)
|
|
106
|
+
if should_include:
|
|
107
|
+
status = _determine_runner_status_from_health(runner, health_data)
|
|
108
|
+
else:
|
|
109
|
+
should_include = False
|
|
110
|
+
logger.debug(
|
|
111
|
+
"runner_health_check_failed",
|
|
112
|
+
runner_name=runner_name,
|
|
113
|
+
component=component,
|
|
114
|
+
error=str(health_data) if isinstance(health_data, Exception) else "No health data",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Add runner to filtered list if it should be included
|
|
118
|
+
if should_include:
|
|
119
|
+
normalized_runner = {
|
|
120
|
+
**runner, # Keep all original fields
|
|
121
|
+
"id": runner.get("name") or runner.get("id"),
|
|
122
|
+
"status": status,
|
|
123
|
+
}
|
|
124
|
+
filtered_runners.append(normalized_runner)
|
|
125
|
+
|
|
126
|
+
logger.debug(
|
|
127
|
+
"runner_included",
|
|
128
|
+
runner_name=runner_name,
|
|
129
|
+
status=status,
|
|
130
|
+
component_filter=component,
|
|
131
|
+
)
|
|
132
|
+
elif component:
|
|
133
|
+
logger.debug(
|
|
134
|
+
"runner_filtered_out",
|
|
135
|
+
runner_name=runner_name,
|
|
136
|
+
component=component,
|
|
137
|
+
reason="component_not_healthy",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
logger.info(
|
|
141
|
+
"runners_listed",
|
|
142
|
+
org_id=organization["id"],
|
|
143
|
+
total_runners=len(runners),
|
|
144
|
+
filtered_runners=len(filtered_runners),
|
|
145
|
+
component_filter=component,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"runners": filtered_runners,
|
|
150
|
+
"count": len(filtered_runners),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error("runners_list_failed", error=str(e), org_id=organization["id"])
|
|
155
|
+
# Return empty list if Kubiya API fails
|
|
156
|
+
return {
|
|
157
|
+
"runners": [],
|
|
158
|
+
"count": 0,
|
|
159
|
+
"error": "Failed to fetch runners from Kubiya API"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@router.get("/{runner_name}/health")
|
|
164
|
+
async def get_runner_health(
|
|
165
|
+
runner_name: str,
|
|
166
|
+
request: Request,
|
|
167
|
+
organization: dict = Depends(get_current_organization),
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Get health status for a specific runner.
|
|
171
|
+
|
|
172
|
+
Proxies to Kubiya API /api/v3/runners/{runner_name}/health
|
|
173
|
+
Checks workflow_engine component health specifically.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
token = request.state.kubiya_token
|
|
177
|
+
kubiya_api_base = settings.kubiya_api_base
|
|
178
|
+
|
|
179
|
+
# Determine auth method from request state
|
|
180
|
+
auth_type = getattr(request.state, "kubiya_auth_type", "Bearer")
|
|
181
|
+
auth_header = f"{auth_type} {token}"
|
|
182
|
+
|
|
183
|
+
# Call Kubiya API health endpoint
|
|
184
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
185
|
+
# Try Bearer first, fallback to UserKey
|
|
186
|
+
response = await client.get(
|
|
187
|
+
f"{kubiya_api_base}/api/v3/runners/{runner_name}/health",
|
|
188
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if response.status_code == 401:
|
|
192
|
+
# Try UserKey if Bearer fails
|
|
193
|
+
response = await client.get(
|
|
194
|
+
f"{kubiya_api_base}/api/v3/runners/{runner_name}/health",
|
|
195
|
+
headers={"Authorization": f"UserKey {token}"},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if response.status_code == 200:
|
|
199
|
+
health_data = response.json()
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
"runner_health_fetched",
|
|
203
|
+
runner_name=runner_name,
|
|
204
|
+
status=health_data.get("status"),
|
|
205
|
+
org_id=organization["id"],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return health_data
|
|
209
|
+
else:
|
|
210
|
+
logger.warning(
|
|
211
|
+
"runner_health_failed",
|
|
212
|
+
runner_name=runner_name,
|
|
213
|
+
status_code=response.status_code,
|
|
214
|
+
)
|
|
215
|
+
raise HTTPException(
|
|
216
|
+
status_code=response.status_code,
|
|
217
|
+
detail=f"Failed to fetch runner health: {response.status_code}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
except httpx.TimeoutException:
|
|
221
|
+
logger.error("runner_health_timeout", runner_name=runner_name)
|
|
222
|
+
raise HTTPException(status_code=504, detail="Health check timeout")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error("runner_health_error", runner_name=runner_name, error=str(e))
|
|
225
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def _fetch_runner_health(
|
|
229
|
+
client: httpx.AsyncClient,
|
|
230
|
+
kubiya_api_base: str,
|
|
231
|
+
token: str,
|
|
232
|
+
runner_name: str,
|
|
233
|
+
) -> Optional[Dict[str, Any]]:
|
|
234
|
+
"""
|
|
235
|
+
Fetch health data for a specific runner from Kubiya API.
|
|
236
|
+
Tries Bearer auth first, then UserKey.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
# Try Bearer first
|
|
240
|
+
response = await client.get(
|
|
241
|
+
f"{kubiya_api_base}/api/v3/runners/{runner_name}/health",
|
|
242
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Fallback to UserKey if Bearer fails
|
|
246
|
+
if response.status_code == 401:
|
|
247
|
+
response = await client.get(
|
|
248
|
+
f"{kubiya_api_base}/api/v3/runners/{runner_name}/health",
|
|
249
|
+
headers={"Authorization": f"UserKey {token}"},
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if response.status_code == 200:
|
|
253
|
+
return response.json()
|
|
254
|
+
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.debug(
|
|
259
|
+
"runner_health_fetch_error",
|
|
260
|
+
runner_name=runner_name,
|
|
261
|
+
error=str(e),
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _has_healthy_component(health_data: Dict[str, Any], component_name: str) -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Check if the runner has a healthy component with the specified name.
|
|
269
|
+
|
|
270
|
+
Health data structure:
|
|
271
|
+
{
|
|
272
|
+
"checks": [
|
|
273
|
+
{"name": "workflow-engine", "status": "ok", ...},
|
|
274
|
+
{"name": "tool-manager", "status": "ok", ...},
|
|
275
|
+
...
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
health_data: Health data from Kubiya API
|
|
281
|
+
component_name: Component name to check (supports multiple naming conventions)
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
True if component is found and healthy, False otherwise
|
|
285
|
+
"""
|
|
286
|
+
checks = health_data.get("checks", [])
|
|
287
|
+
|
|
288
|
+
# Normalize component name to check against multiple naming conventions
|
|
289
|
+
component_variants = _get_component_name_variants(component_name)
|
|
290
|
+
|
|
291
|
+
for check in checks:
|
|
292
|
+
check_name = check.get("name", "")
|
|
293
|
+
if check_name in component_variants:
|
|
294
|
+
status = check.get("status", "")
|
|
295
|
+
# Consider "ok" or "healthy" as valid
|
|
296
|
+
if status in ["ok", "healthy"]:
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _get_component_name_variants(component_name: str) -> List[str]:
|
|
303
|
+
"""
|
|
304
|
+
Get all possible naming variants for a component name.
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
- "workflow_engine" → ["workflow_engine", "workflow-engine", "workflowEngine"]
|
|
308
|
+
- "tool-manager" → ["tool-manager", "tool_manager", "toolManager"]
|
|
309
|
+
- "workflowEngine" → ["workflow_engine", "workflow-engine", "workflowEngine"]
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
component_name: Component name in any format
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of possible naming variants
|
|
316
|
+
"""
|
|
317
|
+
import re
|
|
318
|
+
|
|
319
|
+
# Detect if input is camelCase and split it
|
|
320
|
+
# Insert underscore before uppercase letters (except first char)
|
|
321
|
+
snake_from_camel = re.sub(r"(?<!^)(?=[A-Z])", "_", component_name)
|
|
322
|
+
|
|
323
|
+
# Convert to lowercase base
|
|
324
|
+
base = snake_from_camel.lower().replace("-", "_").replace(" ", "_")
|
|
325
|
+
|
|
326
|
+
# Generate camelCase: first word lowercase, rest capitalized
|
|
327
|
+
words = base.split("_")
|
|
328
|
+
if len(words) > 1:
|
|
329
|
+
camel_case = words[0] + "".join(word.capitalize() for word in words[1:])
|
|
330
|
+
else:
|
|
331
|
+
camel_case = words[0]
|
|
332
|
+
|
|
333
|
+
# Generate variants
|
|
334
|
+
variants = [
|
|
335
|
+
base, # workflow_engine
|
|
336
|
+
base.replace("_", "-"), # workflow-engine
|
|
337
|
+
camel_case, # workflowEngine
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
# Also include the original input
|
|
341
|
+
variants.append(component_name)
|
|
342
|
+
|
|
343
|
+
return list(set(variants))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _determine_runner_status_from_health(runner: Dict[str, Any], health_data: Dict[str, Any]) -> str:
|
|
347
|
+
"""
|
|
348
|
+
Determine runner status based on health check data.
|
|
349
|
+
|
|
350
|
+
Returns: "active", "degraded", "unhealthy"
|
|
351
|
+
"""
|
|
352
|
+
# Check if managed cloud (always active)
|
|
353
|
+
if runner.get("runner_type") == "managed_cloud" or runner.get("isManagedCloud"):
|
|
354
|
+
return "active"
|
|
355
|
+
|
|
356
|
+
# Check overall health status
|
|
357
|
+
overall_status = health_data.get("status", "")
|
|
358
|
+
if overall_status == "ok":
|
|
359
|
+
return "active"
|
|
360
|
+
|
|
361
|
+
# Check individual component health
|
|
362
|
+
checks = health_data.get("checks", [])
|
|
363
|
+
healthy_count = 0
|
|
364
|
+
total_count = len(checks)
|
|
365
|
+
|
|
366
|
+
for check in checks:
|
|
367
|
+
if check.get("status") == "ok":
|
|
368
|
+
healthy_count += 1
|
|
369
|
+
|
|
370
|
+
# All components healthy
|
|
371
|
+
if total_count > 0 and healthy_count == total_count:
|
|
372
|
+
return "active"
|
|
373
|
+
|
|
374
|
+
# Some components healthy
|
|
375
|
+
if healthy_count > 0:
|
|
376
|
+
return "degraded"
|
|
377
|
+
|
|
378
|
+
# No healthy components
|
|
379
|
+
return "unhealthy"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Runtime types endpoint for agent execution frameworks"""
|
|
2
|
+
from fastapi import APIRouter, HTTPException
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
from control_plane_api.app.lib.validation import (
|
|
7
|
+
validate_agent_for_runtime,
|
|
8
|
+
get_runtime_requirements_info,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelRequirementInfo(BaseModel):
|
|
15
|
+
"""Model requirements for a runtime"""
|
|
16
|
+
description: str = Field(..., description="Human-readable description")
|
|
17
|
+
supported_providers: List[str] = Field(..., description="Supported model providers")
|
|
18
|
+
supported_families: List[str] = Field(..., description="Supported model families")
|
|
19
|
+
examples: List[str] = Field(..., description="Example valid model IDs")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RuntimeRequirementsInfo(BaseModel):
|
|
23
|
+
"""Requirements specification for a runtime"""
|
|
24
|
+
model_requirement: ModelRequirementInfo
|
|
25
|
+
required_config_fields: List[str] = Field(default_factory=list)
|
|
26
|
+
recommended_config_fields: List[str] = Field(default_factory=list)
|
|
27
|
+
max_history_length: Optional[int] = None
|
|
28
|
+
requires_system_prompt: bool = False
|
|
29
|
+
requires_tools: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RuntimeInfo(BaseModel):
|
|
33
|
+
"""Information about an agent runtime"""
|
|
34
|
+
id: str = Field(..., description="Runtime identifier")
|
|
35
|
+
name: str = Field(..., description="Display name")
|
|
36
|
+
description: str = Field(..., description="Description of the runtime")
|
|
37
|
+
icon: str = Field(..., description="Icon identifier for UI")
|
|
38
|
+
features: List[str] = Field(..., description="Key features of this runtime")
|
|
39
|
+
status: str = Field(..., description="Status: available, beta, coming_soon")
|
|
40
|
+
requirements: Optional[RuntimeRequirementsInfo] = Field(None, description="Runtime requirements and validation rules")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValidationRequest(BaseModel):
|
|
44
|
+
"""Request to validate agent configuration for a runtime"""
|
|
45
|
+
runtime_type: str = Field(..., description="Runtime type to validate for")
|
|
46
|
+
model_id: Optional[str] = Field(None, description="Model ID to validate")
|
|
47
|
+
agent_config: Optional[Dict[str, Any]] = Field(None, description="Agent configuration")
|
|
48
|
+
system_prompt: Optional[str] = Field(None, description="System prompt")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ValidationResponse(BaseModel):
|
|
52
|
+
"""Response from validation"""
|
|
53
|
+
valid: bool = Field(..., description="Whether configuration is valid")
|
|
54
|
+
errors: List[str] = Field(default_factory=list, description="Validation errors if any")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_runtime_requirements(runtime_id: str) -> Optional[RuntimeRequirementsInfo]:
|
|
58
|
+
"""Helper to get requirements for a runtime from shared validation module."""
|
|
59
|
+
try:
|
|
60
|
+
req_info = get_runtime_requirements_info(runtime_id)
|
|
61
|
+
if "error" in req_info:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
model_req = req_info.get("model_requirement", {})
|
|
65
|
+
return RuntimeRequirementsInfo(
|
|
66
|
+
model_requirement=ModelRequirementInfo(
|
|
67
|
+
description=model_req.get("description", ""),
|
|
68
|
+
supported_providers=model_req.get("supported_providers", []),
|
|
69
|
+
supported_families=model_req.get("supported_families", []),
|
|
70
|
+
examples=model_req.get("examples", []),
|
|
71
|
+
),
|
|
72
|
+
required_config_fields=req_info.get("required_config_fields", []),
|
|
73
|
+
recommended_config_fields=req_info.get("recommended_config_fields", []),
|
|
74
|
+
max_history_length=req_info.get("max_history_length"),
|
|
75
|
+
requires_system_prompt=req_info.get("requires_system_prompt", False),
|
|
76
|
+
requires_tools=req_info.get("requires_tools", False),
|
|
77
|
+
)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Error loading requirements for {runtime_id}: {e}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.get("/runtimes", response_model=List[RuntimeInfo], tags=["Runtimes"])
|
|
84
|
+
def list_runtimes():
|
|
85
|
+
"""
|
|
86
|
+
List available agent runtime types with requirements.
|
|
87
|
+
|
|
88
|
+
Returns information about different agent execution frameworks
|
|
89
|
+
that can be used when creating or configuring agents, including
|
|
90
|
+
model compatibility requirements and validation rules.
|
|
91
|
+
"""
|
|
92
|
+
return [
|
|
93
|
+
RuntimeInfo(
|
|
94
|
+
id="default",
|
|
95
|
+
name="Agno Runtime",
|
|
96
|
+
description="Production-ready agent framework with advanced reasoning and tool execution capabilities. Best for complex workflows and multi-step tasks. Supports most LiteLLM-compatible models.",
|
|
97
|
+
icon="agno",
|
|
98
|
+
features=[
|
|
99
|
+
"Advanced reasoning capabilities",
|
|
100
|
+
"Multi-step task execution",
|
|
101
|
+
"Built-in tool integration",
|
|
102
|
+
"Session management",
|
|
103
|
+
"Production-tested reliability",
|
|
104
|
+
"Supports GPT, Claude, Gemini, Mistral, and more"
|
|
105
|
+
],
|
|
106
|
+
status="available",
|
|
107
|
+
requirements=_get_runtime_requirements("default")
|
|
108
|
+
),
|
|
109
|
+
RuntimeInfo(
|
|
110
|
+
id="claude_code",
|
|
111
|
+
name="Claude Code SDK",
|
|
112
|
+
description="Specialized runtime for code generation and software development tasks. Requires Anthropic Claude models for optimal performance with extended context and advanced code understanding.",
|
|
113
|
+
icon="code",
|
|
114
|
+
features=[
|
|
115
|
+
"Code-first design",
|
|
116
|
+
"Advanced code generation",
|
|
117
|
+
"Built-in code review",
|
|
118
|
+
"Repository awareness",
|
|
119
|
+
"Development workflow optimization",
|
|
120
|
+
"Requires Claude models (claude-3-opus, claude-3-sonnet, etc.)"
|
|
121
|
+
],
|
|
122
|
+
status="beta",
|
|
123
|
+
requirements=_get_runtime_requirements("claude_code")
|
|
124
|
+
)
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post("/runtimes/validate", response_model=ValidationResponse, tags=["Runtimes"])
|
|
129
|
+
def validate_runtime_config(request: ValidationRequest):
|
|
130
|
+
"""
|
|
131
|
+
Validate agent/team configuration for a specific runtime.
|
|
132
|
+
|
|
133
|
+
This endpoint checks if the provided configuration is compatible with
|
|
134
|
+
the selected runtime, including model validation and required fields.
|
|
135
|
+
|
|
136
|
+
Use this before creating or updating agents to ensure compatibility.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
is_valid, errors = validate_agent_for_runtime(
|
|
140
|
+
runtime_type=request.runtime_type,
|
|
141
|
+
model_id=request.model_id,
|
|
142
|
+
agent_config=request.agent_config,
|
|
143
|
+
system_prompt=request.system_prompt,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return ValidationResponse(
|
|
147
|
+
valid=is_valid,
|
|
148
|
+
errors=errors
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=500,
|
|
154
|
+
detail=f"Validation error: {str(e)}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.get("/runtimes/{runtime_id}/requirements", response_model=RuntimeRequirementsInfo, tags=["Runtimes"])
|
|
159
|
+
def get_runtime_requirements(runtime_id: str):
|
|
160
|
+
"""
|
|
161
|
+
Get detailed requirements for a specific runtime.
|
|
162
|
+
|
|
163
|
+
Returns model compatibility requirements, required configuration fields,
|
|
164
|
+
and other validation rules for the specified runtime.
|
|
165
|
+
"""
|
|
166
|
+
requirements = _get_runtime_requirements(runtime_id)
|
|
167
|
+
if not requirements:
|
|
168
|
+
raise HTTPException(
|
|
169
|
+
status_code=404,
|
|
170
|
+
detail=f"Runtime '{runtime_id}' not found or has no requirements"
|
|
171
|
+
)
|
|
172
|
+
return requirements
|