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.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {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
+ }