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,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Approval workflow activities for Temporal.
|
|
3
|
+
|
|
4
|
+
Activities for creating approval requests and waiting for approval/rejection.
|
|
5
|
+
|
|
6
|
+
Two approaches:
|
|
7
|
+
1. Signal-based (recommended): create_approval_request + workflow waits for signal
|
|
8
|
+
2. Polling-based (legacy): wait_for_approval_activity polls for status
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import httpx
|
|
12
|
+
import structlog
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import List, Optional, Dict, Any
|
|
15
|
+
from temporalio import activity
|
|
16
|
+
|
|
17
|
+
from control_plane_api.worker.services.approval_tools import ApprovalTools
|
|
18
|
+
|
|
19
|
+
logger = structlog.get_logger()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ActivityCreateApprovalInput:
|
|
24
|
+
"""Input for create_approval_request activity"""
|
|
25
|
+
execution_id: str
|
|
26
|
+
organization_id: str
|
|
27
|
+
title: str
|
|
28
|
+
message: Optional[str] = None
|
|
29
|
+
approver_user_emails: Optional[List[str]] = None
|
|
30
|
+
approver_group_id: Optional[str] = None
|
|
31
|
+
timeout_minutes: int = 1440 # 24 hours default
|
|
32
|
+
context: Optional[Dict[str, Any]] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ActivityCreateApprovalOutput:
|
|
37
|
+
"""Output from create_approval_request activity"""
|
|
38
|
+
approval_id: str
|
|
39
|
+
status: str # "pending"
|
|
40
|
+
expires_at: Optional[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@activity.defn
|
|
44
|
+
async def create_approval_request(
|
|
45
|
+
input: ActivityCreateApprovalInput,
|
|
46
|
+
) -> ActivityCreateApprovalOutput:
|
|
47
|
+
"""
|
|
48
|
+
Create an approval request via Control Plane API and return immediately.
|
|
49
|
+
|
|
50
|
+
This is the signal-based approach where:
|
|
51
|
+
1. Activity creates the approval request
|
|
52
|
+
2. Activity returns approval_id to workflow
|
|
53
|
+
3. Workflow waits for approval_response signal
|
|
54
|
+
4. Control Plane sends signal when user approves/rejects
|
|
55
|
+
|
|
56
|
+
This is more durable than polling because workflow state is preserved
|
|
57
|
+
across worker restarts.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
input: Approval request parameters
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
ActivityCreateApprovalOutput with approval_id
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
Exception: If approval request creation fails
|
|
67
|
+
"""
|
|
68
|
+
activity.logger.info(
|
|
69
|
+
"creating_approval_request",
|
|
70
|
+
title=input.title,
|
|
71
|
+
execution_id=input.execution_id,
|
|
72
|
+
approver_emails=input.approver_user_emails,
|
|
73
|
+
approver_group_id=input.approver_group_id,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
control_plane_url = os.getenv("CONTROL_PLANE_URL")
|
|
77
|
+
api_key = os.getenv("KUBIYA_API_KEY")
|
|
78
|
+
|
|
79
|
+
if not control_plane_url or not api_key:
|
|
80
|
+
raise ValueError("CONTROL_PLANE_URL and KUBIYA_API_KEY must be set")
|
|
81
|
+
|
|
82
|
+
# Validate at least one approver is specified
|
|
83
|
+
if not input.approver_user_emails and not input.approver_group_id:
|
|
84
|
+
raise ValueError("At least one of approver_user_emails or approver_group_id must be provided")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
88
|
+
# Create approval request
|
|
89
|
+
approval_request = {
|
|
90
|
+
"execution_id": input.execution_id,
|
|
91
|
+
"title": input.title,
|
|
92
|
+
"message": input.message,
|
|
93
|
+
"approver_user_ids": [],
|
|
94
|
+
"approver_user_emails": input.approver_user_emails or [],
|
|
95
|
+
"approver_group_id": input.approver_group_id,
|
|
96
|
+
"timeout_minutes": input.timeout_minutes,
|
|
97
|
+
"context": input.context or {},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
response = await client.post(
|
|
101
|
+
f"{control_plane_url.rstrip('/')}/api/v1/approvals",
|
|
102
|
+
json=approval_request,
|
|
103
|
+
headers={
|
|
104
|
+
"Authorization": f"Bearer {api_key}",
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if response.status_code != 201:
|
|
110
|
+
error_detail = response.text
|
|
111
|
+
activity.logger.error(
|
|
112
|
+
"approval_request_creation_failed",
|
|
113
|
+
status_code=response.status_code,
|
|
114
|
+
error=error_detail,
|
|
115
|
+
)
|
|
116
|
+
raise Exception(f"Failed to create approval request: {error_detail}")
|
|
117
|
+
|
|
118
|
+
approval_data = response.json()
|
|
119
|
+
approval_id = approval_data["id"]
|
|
120
|
+
|
|
121
|
+
activity.logger.info(
|
|
122
|
+
"approval_request_created_signal_mode",
|
|
123
|
+
approval_id=approval_id,
|
|
124
|
+
title=input.title,
|
|
125
|
+
execution_id=input.execution_id,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return ActivityCreateApprovalOutput(
|
|
129
|
+
approval_id=approval_id,
|
|
130
|
+
status=approval_data.get("status", "pending"),
|
|
131
|
+
expires_at=approval_data.get("expires_at"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
activity.logger.error(
|
|
136
|
+
"create_approval_request_failed",
|
|
137
|
+
error=str(e),
|
|
138
|
+
title=input.title,
|
|
139
|
+
execution_id=input.execution_id,
|
|
140
|
+
)
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class ActivityWaitForApprovalInput:
|
|
146
|
+
"""Input for wait_for_approval activity"""
|
|
147
|
+
execution_id: str
|
|
148
|
+
organization_id: str
|
|
149
|
+
title: str
|
|
150
|
+
message: Optional[str] = None
|
|
151
|
+
approver_user_ids: Optional[List[str]] = None
|
|
152
|
+
approver_user_emails: Optional[List[str]] = None
|
|
153
|
+
approver_group_id: Optional[str] = None
|
|
154
|
+
context: Optional[Dict[str, Any]] = None
|
|
155
|
+
config: Optional[Dict[str, Any]] = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@activity.defn
|
|
159
|
+
async def wait_for_approval_activity(input: ActivityWaitForApprovalInput) -> Dict[str, Any]:
|
|
160
|
+
"""
|
|
161
|
+
Activity to create approval request and wait for approval/rejection.
|
|
162
|
+
|
|
163
|
+
This activity:
|
|
164
|
+
1. Creates an approval request via control plane API
|
|
165
|
+
2. Publishes approval_request event for UI streaming
|
|
166
|
+
3. Polls control plane for approval status changes
|
|
167
|
+
4. Returns result when approved/rejected/expired
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
input: Approval request configuration
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dict with approval result:
|
|
174
|
+
{
|
|
175
|
+
"approved": bool,
|
|
176
|
+
"status": "approved" | "rejected" | "expired",
|
|
177
|
+
"approval_id": str,
|
|
178
|
+
"approved_by_email": str (if approved),
|
|
179
|
+
"rejection_reason": str (if rejected)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
Exception: If approval request creation fails
|
|
184
|
+
"""
|
|
185
|
+
activity.logger.info(
|
|
186
|
+
"wait_for_approval_activity_started",
|
|
187
|
+
execution_id=input.execution_id,
|
|
188
|
+
title=input.title,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Get control plane configuration from environment
|
|
193
|
+
control_plane_url = os.getenv("CONTROL_PLANE_URL")
|
|
194
|
+
api_key = os.getenv("KUBIYA_API_KEY")
|
|
195
|
+
|
|
196
|
+
if not control_plane_url or not api_key:
|
|
197
|
+
raise ValueError("CONTROL_PLANE_URL and KUBIYA_API_KEY must be set")
|
|
198
|
+
|
|
199
|
+
# Initialize approval tools
|
|
200
|
+
approval_tools = ApprovalTools(
|
|
201
|
+
control_plane_url=control_plane_url,
|
|
202
|
+
api_key=api_key,
|
|
203
|
+
execution_id=input.execution_id,
|
|
204
|
+
organization_id=input.organization_id,
|
|
205
|
+
config=input.config or {},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Wait for approval (this polls until approved/rejected/expired)
|
|
209
|
+
result = await approval_tools.wait_for_approval(
|
|
210
|
+
title=input.title,
|
|
211
|
+
message=input.message,
|
|
212
|
+
approver_user_ids=input.approver_user_ids,
|
|
213
|
+
approver_user_emails=input.approver_user_emails,
|
|
214
|
+
approver_group_id=input.approver_group_id,
|
|
215
|
+
context=input.context,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
activity.logger.info(
|
|
219
|
+
"wait_for_approval_activity_completed",
|
|
220
|
+
execution_id=input.execution_id,
|
|
221
|
+
approval_id=result.get("approval_id"),
|
|
222
|
+
status=result.get("status"),
|
|
223
|
+
approved=result.get("approved"),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
activity.logger.error(
|
|
230
|
+
"wait_for_approval_activity_failed",
|
|
231
|
+
execution_id=input.execution_id,
|
|
232
|
+
error=str(e),
|
|
233
|
+
)
|
|
234
|
+
raise
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Runtime-based execution activities for Temporal workflows.
|
|
2
|
+
|
|
3
|
+
This module provides activities that use the RuntimeFactory/RuntimeRegistry system
|
|
4
|
+
for agent execution, supporting multiple runtimes (Agno/Default, Claude Code, etc.)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional, List, Dict, Any
|
|
9
|
+
from temporalio import activity
|
|
10
|
+
import structlog
|
|
11
|
+
import os
|
|
12
|
+
import asyncio
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from control_plane_api.worker.runtimes.base import (
|
|
16
|
+
RuntimeType,
|
|
17
|
+
RuntimeExecutionContext,
|
|
18
|
+
RuntimeExecutionResult,
|
|
19
|
+
)
|
|
20
|
+
from control_plane_api.worker.runtimes.factory import RuntimeFactory
|
|
21
|
+
from control_plane_api.worker.control_plane_client import get_control_plane_client
|
|
22
|
+
from control_plane_api.worker.services.cancellation_manager import CancellationManager
|
|
23
|
+
from control_plane_api.worker.services.runtime_analytics import submit_runtime_analytics
|
|
24
|
+
from control_plane_api.worker.services.analytics_service import AnalyticsService
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ActivityRuntimeExecuteInput:
|
|
31
|
+
"""Input for runtime-based execution activity"""
|
|
32
|
+
execution_id: str
|
|
33
|
+
agent_id: str
|
|
34
|
+
organization_id: str
|
|
35
|
+
prompt: str
|
|
36
|
+
runtime_type: str = "default" # "default", "claude_code", etc.
|
|
37
|
+
system_prompt: Optional[str] = None
|
|
38
|
+
model_id: Optional[str] = None
|
|
39
|
+
model_config: Optional[Dict[str, Any]] = None
|
|
40
|
+
agent_config: Optional[Dict[str, Any]] = None
|
|
41
|
+
skills: Optional[List[Dict[str, Any]]] = None
|
|
42
|
+
mcp_servers: Optional[Dict[str, Any]] = None
|
|
43
|
+
conversation_history: Optional[List[Dict[str, Any]]] = None
|
|
44
|
+
user_metadata: Optional[Dict[str, Any]] = None
|
|
45
|
+
runtime_config: Optional[Dict[str, Any]] = None
|
|
46
|
+
stream: bool = False
|
|
47
|
+
|
|
48
|
+
def __post_init__(self):
|
|
49
|
+
if self.model_config is None:
|
|
50
|
+
self.model_config = {}
|
|
51
|
+
if self.agent_config is None:
|
|
52
|
+
self.agent_config = {}
|
|
53
|
+
if self.skills is None:
|
|
54
|
+
self.skills = []
|
|
55
|
+
if self.mcp_servers is None:
|
|
56
|
+
self.mcp_servers = {}
|
|
57
|
+
if self.conversation_history is None:
|
|
58
|
+
self.conversation_history = []
|
|
59
|
+
if self.user_metadata is None:
|
|
60
|
+
self.user_metadata = {}
|
|
61
|
+
if self.runtime_config is None:
|
|
62
|
+
self.runtime_config = {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@activity.defn
|
|
66
|
+
async def execute_with_runtime(input: ActivityRuntimeExecuteInput) -> Dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Execute agent using the RuntimeFactory/RuntimeRegistry system.
|
|
69
|
+
|
|
70
|
+
This activity:
|
|
71
|
+
1. Creates a runtime based on runtime_type (default, claude_code, etc.)
|
|
72
|
+
2. Builds execution context
|
|
73
|
+
3. Executes (streaming or non-streaming)
|
|
74
|
+
4. Returns results
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
input: Activity input with execution details and runtime_type
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dict with response, usage, success flag, etc.
|
|
81
|
+
"""
|
|
82
|
+
print("\n" + "="*80)
|
|
83
|
+
print("🚀 RUNTIME-BASED EXECUTION START")
|
|
84
|
+
print("="*80)
|
|
85
|
+
print(f"Execution ID: {input.execution_id}")
|
|
86
|
+
print(f"Agent ID: {input.agent_id}")
|
|
87
|
+
print(f"Organization: {input.organization_id}")
|
|
88
|
+
print(f"Runtime Type: {input.runtime_type}")
|
|
89
|
+
print(f"Model: {input.model_id or 'default'}")
|
|
90
|
+
print(f"Stream: {input.stream}")
|
|
91
|
+
print(f"Skills: {len(input.skills)}")
|
|
92
|
+
print(f"MCP Servers: {len(input.mcp_servers)}")
|
|
93
|
+
print(f"Prompt: {input.prompt[:100]}..." if len(input.prompt) > 100 else f"Prompt: {input.prompt}")
|
|
94
|
+
print("="*80 + "\n")
|
|
95
|
+
|
|
96
|
+
activity.logger.info(
|
|
97
|
+
"Executing with Runtime system",
|
|
98
|
+
extra={
|
|
99
|
+
"execution_id": input.execution_id,
|
|
100
|
+
"agent_id": input.agent_id,
|
|
101
|
+
"organization_id": input.organization_id,
|
|
102
|
+
"runtime_type": input.runtime_type,
|
|
103
|
+
"model_id": input.model_id,
|
|
104
|
+
"stream": input.stream,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Track execution start time for analytics
|
|
110
|
+
turn_start_time = time.time()
|
|
111
|
+
|
|
112
|
+
# Get Control Plane client and cancellation manager
|
|
113
|
+
control_plane = get_control_plane_client()
|
|
114
|
+
cancellation_manager = CancellationManager()
|
|
115
|
+
|
|
116
|
+
# Initialize analytics service for submission
|
|
117
|
+
analytics_service = AnalyticsService(
|
|
118
|
+
control_plane_url=control_plane.base_url if hasattr(control_plane, 'base_url') else "http://localhost:8000",
|
|
119
|
+
api_key=os.environ.get("KUBIYA_API_KEY", ""),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Parse runtime type
|
|
123
|
+
try:
|
|
124
|
+
runtime_type_enum = RuntimeType(input.runtime_type)
|
|
125
|
+
except ValueError:
|
|
126
|
+
logger.error(f"Invalid runtime_type: {input.runtime_type}, falling back to DEFAULT")
|
|
127
|
+
runtime_type_enum = RuntimeType.DEFAULT
|
|
128
|
+
|
|
129
|
+
# Create runtime using factory
|
|
130
|
+
factory = RuntimeFactory()
|
|
131
|
+
runtime = factory.create_runtime(
|
|
132
|
+
runtime_type=runtime_type_enum,
|
|
133
|
+
control_plane_client=control_plane,
|
|
134
|
+
cancellation_manager=cancellation_manager,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
logger.info(
|
|
138
|
+
f"Created runtime",
|
|
139
|
+
extra={
|
|
140
|
+
"runtime_type": runtime_type_enum,
|
|
141
|
+
"runtime_class": runtime.__class__.__name__,
|
|
142
|
+
"capabilities": runtime.get_capabilities(),
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Fetch and instantiate skills if runtime supports tools
|
|
147
|
+
skills = input.skills or []
|
|
148
|
+
if runtime.supports_tools():
|
|
149
|
+
print(f"🔧 Fetching skills from Control Plane...")
|
|
150
|
+
try:
|
|
151
|
+
skill_configs = control_plane.get_skills(input.agent_id)
|
|
152
|
+
if skill_configs:
|
|
153
|
+
print(f"✅ Resolved {len(skill_configs)} skill configs")
|
|
154
|
+
print(f" Types: {[t.get('type') for t in skill_configs]}")
|
|
155
|
+
print(f" Names: {[t.get('name') for t in skill_configs]}")
|
|
156
|
+
print(f" Enabled: {[t.get('enabled', True) for t in skill_configs]}\n")
|
|
157
|
+
|
|
158
|
+
# DEBUG: Show full config for workflow_executor skills
|
|
159
|
+
for cfg in skill_configs:
|
|
160
|
+
if cfg.get('type') in ['workflow_executor', 'workflow']:
|
|
161
|
+
print(f"🔍 Workflow Executor Skill Config:")
|
|
162
|
+
print(f" Name: {cfg.get('name')}")
|
|
163
|
+
print(f" Type: {cfg.get('type')}")
|
|
164
|
+
print(f" Enabled: {cfg.get('enabled', True)}")
|
|
165
|
+
print(f" Config Keys: {list(cfg.get('configuration', {}).keys())}\n")
|
|
166
|
+
|
|
167
|
+
# Import here to avoid circular dependency
|
|
168
|
+
from worker.services.skill_factory import SkillFactory
|
|
169
|
+
|
|
170
|
+
skills = SkillFactory.create_skills_from_list(skill_configs)
|
|
171
|
+
|
|
172
|
+
if skills:
|
|
173
|
+
print(f"✅ Instantiated {len(skills)} skill(s)")
|
|
174
|
+
# Show types of instantiated skills
|
|
175
|
+
skill_types = [type(s).__name__ for s in skills]
|
|
176
|
+
print(f" Skill classes: {skill_types}\n")
|
|
177
|
+
else:
|
|
178
|
+
print(f"⚠️ No skills were instantiated (all disabled or failed)\n")
|
|
179
|
+
else:
|
|
180
|
+
print(f"⚠️ No skills found for agent\n")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f"❌ Error fetching skills: {str(e)}\n")
|
|
183
|
+
logger.error("skill_fetch_error", error=str(e), exc_info=True)
|
|
184
|
+
|
|
185
|
+
# Build execution context
|
|
186
|
+
context = RuntimeExecutionContext(
|
|
187
|
+
execution_id=input.execution_id,
|
|
188
|
+
agent_id=input.agent_id,
|
|
189
|
+
organization_id=input.organization_id,
|
|
190
|
+
prompt=input.prompt,
|
|
191
|
+
system_prompt=input.system_prompt,
|
|
192
|
+
conversation_history=input.conversation_history,
|
|
193
|
+
model_id=input.model_id,
|
|
194
|
+
model_config=input.model_config,
|
|
195
|
+
agent_config=input.agent_config,
|
|
196
|
+
skills=skills, # Use fetched skills
|
|
197
|
+
mcp_servers=input.mcp_servers,
|
|
198
|
+
user_metadata=input.user_metadata,
|
|
199
|
+
runtime_config=input.runtime_config,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Execute based on streaming preference
|
|
203
|
+
if input.stream:
|
|
204
|
+
# Streaming execution
|
|
205
|
+
logger.info("Starting streaming execution")
|
|
206
|
+
accumulated_response = ""
|
|
207
|
+
final_result = None
|
|
208
|
+
|
|
209
|
+
# Generate unique message ID for this turn (execution_id + timestamp)
|
|
210
|
+
message_id = f"{input.execution_id}_{int(time.time() * 1000000)}"
|
|
211
|
+
|
|
212
|
+
# Track tool events published
|
|
213
|
+
tool_events_published = {"start": 0, "complete": 0}
|
|
214
|
+
|
|
215
|
+
# Define event callback for publishing tool events to Control Plane
|
|
216
|
+
def event_callback(event: Dict):
|
|
217
|
+
"""Callback to publish events (tool start/complete, content chunks) to Control Plane SSE"""
|
|
218
|
+
event_type = event.get("type")
|
|
219
|
+
|
|
220
|
+
if event_type == "content_chunk":
|
|
221
|
+
# Content chunks are already handled below via result.response
|
|
222
|
+
pass
|
|
223
|
+
elif event_type == "tool_start":
|
|
224
|
+
# Publish tool start event (synchronous - this runs in async context via callback)
|
|
225
|
+
try:
|
|
226
|
+
print(f"\n🔧 TOOL START EVENT: {event.get('tool_name')} (ID: {event.get('tool_execution_id')})")
|
|
227
|
+
control_plane.publish_event(
|
|
228
|
+
execution_id=input.execution_id,
|
|
229
|
+
event_type="tool_started", # Match default runtime event type
|
|
230
|
+
data={
|
|
231
|
+
"tool_name": event.get("tool_name"),
|
|
232
|
+
"tool_execution_id": event.get("tool_execution_id"),
|
|
233
|
+
"tool_arguments": event.get("tool_args", {}),
|
|
234
|
+
"message": f"🔧 Executing tool: {event.get('tool_name')}",
|
|
235
|
+
"source": "agent",
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
tool_events_published["start"] += 1
|
|
239
|
+
print(f"📡 Published tool_started event #{tool_events_published['start']}: {event.get('tool_name')}")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"❌ Failed to publish tool_start event: {e}", exc_info=True)
|
|
242
|
+
print(f"❌ Failed to publish tool_start event: {e}")
|
|
243
|
+
elif event_type == "tool_complete":
|
|
244
|
+
# Publish tool complete event
|
|
245
|
+
try:
|
|
246
|
+
status = event.get("status", "success")
|
|
247
|
+
icon = "✅" if status == "success" else "❌"
|
|
248
|
+
print(f"\n{icon} TOOL COMPLETE EVENT: {event.get('tool_name')} ({status})")
|
|
249
|
+
control_plane.publish_event(
|
|
250
|
+
execution_id=input.execution_id,
|
|
251
|
+
event_type="tool_completed", # Match default runtime event type
|
|
252
|
+
data={
|
|
253
|
+
"tool_name": event.get("tool_name"),
|
|
254
|
+
"tool_execution_id": event.get("tool_execution_id"),
|
|
255
|
+
"status": status,
|
|
256
|
+
"tool_output": event.get("output"),
|
|
257
|
+
"tool_error": event.get("error"),
|
|
258
|
+
"message": f"{icon} Tool {status}: {event.get('tool_name')}",
|
|
259
|
+
"source": "agent",
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
tool_events_published["complete"] += 1
|
|
263
|
+
print(f"📡 Published tool_completed event #{tool_events_published['complete']}: {event.get('tool_name')}\n")
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(f"❌ Failed to publish tool_complete event: {e}", exc_info=True)
|
|
266
|
+
print(f"❌ Failed to publish tool_complete event: {e}")
|
|
267
|
+
|
|
268
|
+
# Stream execution with event callback
|
|
269
|
+
async for result in runtime.stream_execute(context, event_callback):
|
|
270
|
+
if result.response:
|
|
271
|
+
accumulated_response += result.response
|
|
272
|
+
|
|
273
|
+
# Publish streaming chunk to control plane for real-time UI updates
|
|
274
|
+
try:
|
|
275
|
+
await control_plane.publish_event_async(
|
|
276
|
+
execution_id=input.execution_id,
|
|
277
|
+
event_type="message_chunk",
|
|
278
|
+
data={
|
|
279
|
+
"role": "assistant",
|
|
280
|
+
"content": result.response,
|
|
281
|
+
"is_chunk": True,
|
|
282
|
+
"message_id": message_id,
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning(f"Failed to publish streaming chunk: {e}")
|
|
287
|
+
|
|
288
|
+
if result.finish_reason:
|
|
289
|
+
final_result = result
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
if not final_result:
|
|
293
|
+
raise RuntimeError("Streaming execution did not provide final result")
|
|
294
|
+
|
|
295
|
+
# Log tool event summary
|
|
296
|
+
print(f"\n📊 Tool Events Summary:")
|
|
297
|
+
print(f" tool_started events published: {tool_events_published['start']}")
|
|
298
|
+
print(f" tool_completed events published: {tool_events_published['complete']}")
|
|
299
|
+
print(f" tool_messages in result: {len(final_result.tool_messages or [])}\n")
|
|
300
|
+
|
|
301
|
+
# Submit analytics (fire-and-forget)
|
|
302
|
+
try:
|
|
303
|
+
asyncio.create_task(
|
|
304
|
+
submit_runtime_analytics(
|
|
305
|
+
result=final_result,
|
|
306
|
+
execution_id=input.execution_id,
|
|
307
|
+
turn_number=1, # TODO: Track turn number across conversation
|
|
308
|
+
turn_start_time=turn_start_time,
|
|
309
|
+
analytics_service=analytics_service,
|
|
310
|
+
turn_end_time=time.time(),
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
logger.info(
|
|
314
|
+
"analytics_submission_started",
|
|
315
|
+
execution_id=input.execution_id,
|
|
316
|
+
tokens=final_result.usage.get("total_tokens", 0) if final_result.usage else 0,
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.warning("analytics_submission_failed", error=str(e), execution_id=input.execution_id)
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"success": final_result.success,
|
|
323
|
+
"response": accumulated_response,
|
|
324
|
+
"usage": final_result.usage or {},
|
|
325
|
+
"model": final_result.model,
|
|
326
|
+
"finish_reason": final_result.finish_reason,
|
|
327
|
+
"tool_messages": final_result.tool_messages or [],
|
|
328
|
+
"metadata": final_result.metadata or {},
|
|
329
|
+
"error": final_result.error,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
# Non-streaming execution
|
|
334
|
+
logger.info("Starting non-streaming execution")
|
|
335
|
+
result = await runtime.execute(context)
|
|
336
|
+
|
|
337
|
+
# Submit analytics (fire-and-forget)
|
|
338
|
+
try:
|
|
339
|
+
asyncio.create_task(
|
|
340
|
+
submit_runtime_analytics(
|
|
341
|
+
result=result,
|
|
342
|
+
execution_id=input.execution_id,
|
|
343
|
+
turn_number=1, # TODO: Track turn number across conversation
|
|
344
|
+
turn_start_time=turn_start_time,
|
|
345
|
+
analytics_service=analytics_service,
|
|
346
|
+
turn_end_time=time.time(),
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
logger.info(
|
|
350
|
+
"analytics_submission_started",
|
|
351
|
+
execution_id=input.execution_id,
|
|
352
|
+
tokens=result.usage.get("total_tokens", 0) if result.usage else 0,
|
|
353
|
+
)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.warning("analytics_submission_failed", error=str(e), execution_id=input.execution_id)
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"success": result.success,
|
|
359
|
+
"response": result.response,
|
|
360
|
+
"usage": result.usage or {},
|
|
361
|
+
"model": result.model,
|
|
362
|
+
"finish_reason": result.finish_reason,
|
|
363
|
+
"tool_messages": result.tool_messages or [],
|
|
364
|
+
"metadata": result.metadata or {},
|
|
365
|
+
"error": result.error,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.error(
|
|
370
|
+
"Runtime execution failed",
|
|
371
|
+
extra={
|
|
372
|
+
"execution_id": input.execution_id,
|
|
373
|
+
"runtime_type": input.runtime_type,
|
|
374
|
+
"error": str(e),
|
|
375
|
+
},
|
|
376
|
+
exc_info=True,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
"success": False,
|
|
381
|
+
"response": "",
|
|
382
|
+
"usage": {},
|
|
383
|
+
"model": input.model_id,
|
|
384
|
+
"finish_reason": "error",
|
|
385
|
+
"tool_messages": [],
|
|
386
|
+
"metadata": {},
|
|
387
|
+
"error": f"{type(e).__name__}: {str(e)}",
|
|
388
|
+
}
|