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,173 @@
1
+ """
2
+ Runtime factory using the registry system.
3
+
4
+ This module provides a simplified factory that delegates to RuntimeRegistry.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, List
8
+ import structlog
9
+
10
+ from .base import RuntimeType, BaseRuntime, RuntimeRegistry
11
+
12
+ if TYPE_CHECKING:
13
+ from control_plane_client import ControlPlaneClient
14
+ from services.cancellation_manager import CancellationManager
15
+
16
+ logger = structlog.get_logger(__name__)
17
+
18
+
19
+ class RuntimeFactory:
20
+ """
21
+ Factory for creating runtime instances.
22
+
23
+ This is a thin wrapper around RuntimeRegistry that provides
24
+ backward compatibility and convenience methods.
25
+ """
26
+
27
+ @staticmethod
28
+ def create_runtime(
29
+ runtime_type: RuntimeType,
30
+ control_plane_client: "ControlPlaneClient",
31
+ cancellation_manager: "CancellationManager",
32
+ **kwargs,
33
+ ) -> BaseRuntime:
34
+ """
35
+ Create a runtime instance using the registry.
36
+
37
+ Args:
38
+ runtime_type: Type of runtime to create
39
+ control_plane_client: Client for Control Plane API
40
+ cancellation_manager: Manager for execution cancellation
41
+ **kwargs: Additional runtime-specific configuration
42
+
43
+ Returns:
44
+ BaseRuntime instance
45
+
46
+ Raises:
47
+ ValueError: If runtime_type is not supported
48
+
49
+ Example:
50
+ >>> factory = RuntimeFactory()
51
+ >>> runtime = factory.create_runtime(
52
+ ... RuntimeType.CLAUDE_CODE,
53
+ ... control_plane_client,
54
+ ... cancellation_manager
55
+ ... )
56
+ """
57
+ logger.info(
58
+ "creating_runtime",
59
+ runtime_type=runtime_type.value,
60
+ has_kwargs=bool(kwargs),
61
+ )
62
+
63
+ return RuntimeRegistry.create(
64
+ runtime_type=runtime_type,
65
+ control_plane_client=control_plane_client,
66
+ cancellation_manager=cancellation_manager,
67
+ **kwargs,
68
+ )
69
+
70
+ @staticmethod
71
+ def get_default_runtime_type() -> RuntimeType:
72
+ """
73
+ Get the default runtime type.
74
+
75
+ This is used when no runtime is explicitly specified in agent config.
76
+
77
+ Returns:
78
+ Default RuntimeType (RuntimeType.DEFAULT)
79
+ """
80
+ return RuntimeType.DEFAULT
81
+
82
+ @staticmethod
83
+ def get_supported_runtimes() -> List[RuntimeType]:
84
+ """
85
+ Get list of supported runtimes from registry.
86
+
87
+ Returns:
88
+ List of RuntimeType enum values
89
+ """
90
+ return RuntimeRegistry.list_available()
91
+
92
+ @staticmethod
93
+ def parse_runtime_type(runtime_str: str) -> RuntimeType:
94
+ """
95
+ Parse runtime type from string with fallback to default.
96
+
97
+ Args:
98
+ runtime_str: Runtime type as string
99
+
100
+ Returns:
101
+ RuntimeType enum value, defaults to DEFAULT if invalid
102
+
103
+ Example:
104
+ >>> RuntimeFactory.parse_runtime_type("claude_code")
105
+ RuntimeType.CLAUDE_CODE
106
+ >>> RuntimeFactory.parse_runtime_type("invalid")
107
+ RuntimeType.DEFAULT
108
+ """
109
+ try:
110
+ return RuntimeType(runtime_str)
111
+ except ValueError:
112
+ logger.warning(
113
+ "invalid_runtime_type_fallback",
114
+ runtime_str=runtime_str,
115
+ default=RuntimeType.DEFAULT.value,
116
+ )
117
+ return RuntimeType.DEFAULT
118
+
119
+ @staticmethod
120
+ def get_runtime_info_all() -> dict:
121
+ """
122
+ Get information about all available runtimes.
123
+
124
+ Returns:
125
+ Dict mapping runtime type to info dict
126
+ """
127
+ return RuntimeRegistry.get_runtime_info_all()
128
+
129
+ @staticmethod
130
+ def validate_runtime_config(
131
+ runtime_type: RuntimeType, config: dict
132
+ ) -> tuple[bool, str]:
133
+ """
134
+ Validate runtime-specific configuration.
135
+
136
+ Args:
137
+ runtime_type: Type of runtime
138
+ config: Configuration dict to validate
139
+
140
+ Returns:
141
+ Tuple of (is_valid, error_message)
142
+
143
+ Example:
144
+ >>> is_valid, error = RuntimeFactory.validate_runtime_config(
145
+ ... RuntimeType.CLAUDE_CODE,
146
+ ... {"allowed_tools": ["Bash"]}
147
+ ... )
148
+ """
149
+ # Get runtime class from registry
150
+ try:
151
+ runtime_class = RuntimeRegistry.get(runtime_type)
152
+
153
+ # Create temporary instance to validate (with mocks)
154
+ from unittest.mock import MagicMock
155
+ temp_runtime = runtime_class(
156
+ control_plane_client=MagicMock(),
157
+ cancellation_manager=MagicMock(),
158
+ )
159
+
160
+ # Use runtime's validation if available
161
+ if hasattr(temp_runtime, 'validate_config'):
162
+ try:
163
+ temp_runtime.validate_config(config)
164
+ return True, ""
165
+ except ValueError as e:
166
+ return False, str(e)
167
+
168
+ return True, ""
169
+
170
+ except ValueError as e:
171
+ return False, f"Unknown runtime type: {str(e)}"
172
+ except Exception as e:
173
+ return False, f"Validation error: {str(e)}"
@@ -0,0 +1,93 @@
1
+ """
2
+ Runtime validation system with model compatibility and requirements checking.
3
+
4
+ This module re-exports validation functions from the shared validation module
5
+ and provides worker-specific validation helpers for RuntimeExecutionContext.
6
+ """
7
+
8
+ from typing import List, Optional, Dict, Any
9
+ from control_plane_api.worker.runtimes.base import RuntimeType, RuntimeExecutionContext
10
+
11
+ # Import shared validation logic that both API and worker can use
12
+ from control_plane_api.app.lib.validation import (
13
+ validate_agent_for_runtime as _validate_agent_for_runtime,
14
+ get_runtime_requirements_info as _get_runtime_requirements_info,
15
+ list_all_runtime_requirements as _list_all_runtime_requirements,
16
+ RUNTIME_REQUIREMENTS as _RUNTIME_REQUIREMENTS,
17
+ )
18
+
19
+
20
+ # Re-export for backward compatibility
21
+ validate_agent_for_runtime = _validate_agent_for_runtime
22
+ get_runtime_requirements_info = _get_runtime_requirements_info
23
+ list_all_runtime_requirements = _list_all_runtime_requirements
24
+
25
+
26
+ class ValidationError(Exception):
27
+ """Raised when validation fails."""
28
+
29
+ def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict[str, Any]] = None):
30
+ self.message = message
31
+ self.field = field
32
+ self.details = details or {}
33
+ super().__init__(message)
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ """Convert to dict for API responses."""
37
+ result = {"error": self.message}
38
+ if self.field:
39
+ result["field"] = self.field
40
+ if self.details:
41
+ result["details"] = self.details
42
+ return result
43
+
44
+
45
+ def validate_execution_context(context: RuntimeExecutionContext) -> tuple[bool, List[str]]:
46
+ """
47
+ Validate RuntimeExecutionContext for worker execution.
48
+
49
+ This is a worker-specific helper that validates the full execution context
50
+ including conversation history, skills, and runtime-specific requirements.
51
+
52
+ Args:
53
+ context: Runtime execution context
54
+
55
+ Returns:
56
+ Tuple of (is_valid, error_messages)
57
+ """
58
+ errors = []
59
+
60
+ # Get runtime requirements from shared module
61
+ runtime_type_str = context.runtime_type.value if isinstance(context.runtime_type, RuntimeType) else str(context.runtime_type)
62
+ requirements = _RUNTIME_REQUIREMENTS.get(runtime_type_str)
63
+
64
+ if not requirements:
65
+ # No requirements registered - allow by default
66
+ return True, []
67
+
68
+ # Validate model
69
+ is_valid, error = requirements.validate_model(context.model_id)
70
+ if not is_valid:
71
+ errors.append(error)
72
+
73
+ # Validate config
74
+ config_errors = requirements.validate_config(context.agent_config)
75
+ errors.extend(config_errors)
76
+
77
+ # Validate system prompt if required
78
+ if requirements.requires_system_prompt and not context.system_prompt:
79
+ errors.append("System prompt is required for this runtime")
80
+
81
+ # Validate tools if required
82
+ if requirements.requires_tools and not context.skills:
83
+ errors.append("At least one skill is required for this runtime")
84
+
85
+ # Validate history length
86
+ if requirements.max_history_length and context.conversation_history:
87
+ if len(context.conversation_history) > requirements.max_history_length:
88
+ errors.append(
89
+ f"Conversation history too long ({len(context.conversation_history)} messages). "
90
+ f"Maximum: {requirements.max_history_length}"
91
+ )
92
+
93
+ return len(errors) == 0, errors
@@ -0,0 +1 @@
1
+ """Business logic services for worker operations"""
@@ -0,0 +1,422 @@
1
+ """Agent executor service - handles agent execution business logic"""
2
+
3
+ from typing import Dict, Any, Optional, List
4
+ from datetime import datetime, timezone
5
+ import structlog
6
+ import asyncio
7
+ import os
8
+
9
+ from agno.agent import Agent
10
+ from agno.models.litellm import LiteLLM
11
+
12
+ from control_plane_api.worker.control_plane_client import ControlPlaneClient
13
+ from control_plane_api.worker.services.session_service import SessionService
14
+ from control_plane_api.worker.services.cancellation_manager import CancellationManager
15
+ from control_plane_api.worker.services.skill_factory import SkillFactory
16
+ from control_plane_api.worker.utils.streaming_utils import StreamingHelper
17
+
18
+ logger = structlog.get_logger()
19
+
20
+
21
+ class AgentExecutorService:
22
+ """
23
+ Service for executing agents with full session management and cancellation support.
24
+
25
+ This service orchestrates:
26
+ - Session loading and restoration
27
+ - Agent creation with LiteLLM configuration
28
+ - Skill instantiation
29
+ - Streaming execution with real-time updates
30
+ - Session persistence
31
+ - Cancellation support via CancellationManager
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ control_plane: ControlPlaneClient,
37
+ session_service: SessionService,
38
+ cancellation_manager: CancellationManager
39
+ ):
40
+ self.control_plane = control_plane
41
+ self.session_service = session_service
42
+ self.cancellation_manager = cancellation_manager
43
+
44
+ async def execute(self, input: Any) -> Dict[str, Any]:
45
+ """
46
+ Execute an agent with full session management and streaming.
47
+
48
+ Args:
49
+ input: AgentExecutionInput with execution details
50
+
51
+ Returns:
52
+ Dict with response, usage, success flag, etc.
53
+ """
54
+ execution_id = input.execution_id
55
+
56
+ print("\n" + "="*80)
57
+ print("🤖 AGENT EXECUTION START")
58
+ print("="*80)
59
+ print(f"Execution ID: {execution_id}")
60
+ print(f"Agent ID: {input.agent_id}")
61
+ print(f"Organization: {input.organization_id}")
62
+ print(f"Model: {input.model_id or 'default'}")
63
+ print(f"Session ID: {input.session_id}")
64
+ print(f"Prompt: {input.prompt[:100]}..." if len(input.prompt) > 100 else f"Prompt: {input.prompt}")
65
+ print("="*80 + "\n")
66
+
67
+ logger.info(
68
+ "agent_execution_start",
69
+ execution_id=execution_id[:8],
70
+ agent_id=input.agent_id,
71
+ session_id=input.session_id
72
+ )
73
+
74
+ try:
75
+ # STEP 1: Load session history
76
+ session_history = self.session_service.load_session(
77
+ execution_id=execution_id,
78
+ session_id=input.session_id
79
+ )
80
+
81
+ if session_history:
82
+ print(f"✅ Loaded {len(session_history)} messages from previous session\n")
83
+ else:
84
+ print("ℹ️ Starting new conversation\n")
85
+
86
+ # STEP 2: Build conversation context for Agno
87
+ conversation_context = self.session_service.build_conversation_context(session_history)
88
+
89
+ # STEP 3: Get LiteLLM configuration
90
+ litellm_api_base = os.getenv("LITELLM_API_BASE", "https://llm-proxy.kubiya.ai")
91
+ litellm_api_key = os.getenv("LITELLM_API_KEY")
92
+
93
+ if not litellm_api_key:
94
+ raise ValueError("LITELLM_API_KEY environment variable not set")
95
+
96
+ model = input.model_id or os.environ.get("LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4")
97
+
98
+ # STEP 4: Fetch and instantiate skills
99
+ skills = []
100
+ if input.agent_id:
101
+ print(f"🔧 Fetching skills from Control Plane...")
102
+ try:
103
+ skill_configs = self.control_plane.get_skills(input.agent_id)
104
+ if skill_configs:
105
+ print(f"✅ Resolved {len(skill_configs)} skills")
106
+ print(f" Types: {[t.get('type') for t in skill_configs]}")
107
+ print(f" Names: {[t.get('name') for t in skill_configs]}\n")
108
+
109
+ skills = SkillFactory.create_skills_from_list(skill_configs)
110
+
111
+ if skills:
112
+ print(f"✅ Instantiated {len(skills)} skill(s)\n")
113
+ else:
114
+ print(f"⚠️ No skills found\n")
115
+ except Exception as e:
116
+ print(f"❌ Error fetching skills: {str(e)}\n")
117
+ logger.error("skill_fetch_error", error=str(e))
118
+
119
+ # STEP 5: Create agent with streaming helper
120
+ print(f"\n🤖 Creating Agno Agent:")
121
+ print(f" Model: {model}")
122
+ print(f" Skills: {len(skills)}")
123
+
124
+ # Create streaming helper for this execution
125
+ streaming_helper = StreamingHelper(
126
+ control_plane_client=self.control_plane,
127
+ execution_id=execution_id
128
+ )
129
+
130
+ # Create tool hook for real-time updates
131
+ def tool_hook(name: str = None, function_name: str = None, function=None, arguments: dict = None, **kwargs):
132
+ """Hook to capture tool execution for real-time streaming"""
133
+ tool_name = name or function_name or "unknown"
134
+ tool_args = arguments or {}
135
+
136
+ # Generate unique tool execution ID
137
+ import time
138
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
139
+
140
+ print(f" 🔧 Tool Starting: {tool_name} (ID: {tool_execution_id})")
141
+
142
+ # Publish tool start event
143
+ streaming_helper.publish_tool_start(
144
+ tool_name=tool_name,
145
+ tool_execution_id=tool_execution_id,
146
+ tool_args=tool_args,
147
+ source="agent"
148
+ )
149
+
150
+ # Execute the tool
151
+ result = None
152
+ error = None
153
+ try:
154
+ if function and callable(function):
155
+ result = function(**tool_args) if tool_args else function()
156
+ else:
157
+ raise ValueError(f"Function not callable: {function}")
158
+
159
+ status = "success"
160
+ print(f" ✅ Tool Success: {tool_name}")
161
+
162
+ except Exception as e:
163
+ error = e
164
+ status = "failed"
165
+ print(f" ❌ Tool Failed: {tool_name} - {str(e)}")
166
+
167
+ # Publish tool completion event
168
+ streaming_helper.publish_tool_complete(
169
+ tool_name=tool_name,
170
+ tool_execution_id=tool_execution_id,
171
+ status=status,
172
+ output=str(result)[:1000] if result else None,
173
+ error=str(error) if error else None,
174
+ source="agent"
175
+ )
176
+
177
+ if error:
178
+ raise error
179
+
180
+ return result
181
+
182
+ # Create Agno Agent
183
+ agent = Agent(
184
+ name=f"Agent {input.agent_id}",
185
+ role=input.system_prompt or "You are a helpful AI assistant",
186
+ model=LiteLLM(
187
+ id=f"openai/{model}",
188
+ api_base=litellm_api_base,
189
+ api_key=litellm_api_key,
190
+ ),
191
+ tools=skills if skills else None,
192
+ tool_hooks=[tool_hook],
193
+ )
194
+
195
+ # STEP 6: Register for cancellation
196
+ self.cancellation_manager.register(
197
+ execution_id=execution_id,
198
+ instance=agent,
199
+ instance_type="agent"
200
+ )
201
+ print(f"✅ Agent registered for cancellation support\n")
202
+
203
+ # Cache execution metadata in Redis
204
+ self.control_plane.cache_metadata(execution_id, "AGENT")
205
+
206
+ # STEP 7: Execute with streaming
207
+ print("⚡ Executing Agent Run with Streaming...\n")
208
+
209
+ # Generate unique message ID for this turn
210
+ import time
211
+ message_id = f"{execution_id}_{int(time.time() * 1000000)}"
212
+
213
+ def stream_agent_run():
214
+ """Run agent with streaming and collect response"""
215
+ # Create event loop for this thread (needed for async streaming)
216
+ loop = asyncio.new_event_loop()
217
+ asyncio.set_event_loop(loop)
218
+
219
+ async def _async_stream():
220
+ """Async wrapper for streaming execution"""
221
+ import time as time_module
222
+ last_heartbeat_time = time_module.time()
223
+ last_persistence_time = time_module.time()
224
+ heartbeat_interval = 10 # Send heartbeat every 10 seconds
225
+ persistence_interval = 60 # Persist to database every 60 seconds
226
+
227
+ try:
228
+ # Execute with conversation context
229
+ if conversation_context:
230
+ run_response = agent.run(
231
+ input.prompt,
232
+ stream=True,
233
+ messages=conversation_context,
234
+ )
235
+ else:
236
+ run_response = agent.run(input.prompt, stream=True)
237
+
238
+ # Process streaming chunks (sync iteration in async context)
239
+ for chunk in run_response:
240
+ # Periodic maintenance: heartbeats and persistence
241
+ current_time = time_module.time()
242
+
243
+ # Send heartbeat every 10s (Temporal liveness)
244
+ if current_time - last_heartbeat_time >= heartbeat_interval:
245
+ current_response = streaming_helper.get_full_response()
246
+ activity.heartbeat({
247
+ "status": "Streaming in progress...",
248
+ "response_length": len(current_response),
249
+ "execution_id": execution_id,
250
+ })
251
+ last_heartbeat_time = current_time
252
+
253
+ # Persist snapshot every 60s (resilience against crashes)
254
+ if current_time - last_persistence_time >= persistence_interval:
255
+ current_response = streaming_helper.get_full_response()
256
+ if current_response:
257
+ print(f"\n💾 Periodic persistence ({len(current_response)} chars)...")
258
+ snapshot_messages = session_history + [{
259
+ "role": "assistant",
260
+ "content": current_response,
261
+ "timestamp": datetime.now(timezone.utc).isoformat(),
262
+ }]
263
+ try:
264
+ # Best effort - don't fail execution if persistence fails
265
+ self.session_service.persist_session(
266
+ execution_id=execution_id,
267
+ session_id=input.session_id or execution_id,
268
+ user_id=input.user_id,
269
+ messages=snapshot_messages,
270
+ metadata={
271
+ "agent_id": input.agent_id,
272
+ "organization_id": input.organization_id,
273
+ "snapshot": True,
274
+ }
275
+ )
276
+ print(f" ✅ Session snapshot persisted")
277
+ except Exception as e:
278
+ print(f" ⚠️ Session persistence error: {str(e)} (non-fatal)")
279
+ last_persistence_time = current_time
280
+
281
+ # Handle run_id capture
282
+ streaming_helper.handle_run_id(
283
+ chunk=chunk,
284
+ on_run_id=lambda run_id: self.cancellation_manager.set_run_id(execution_id, run_id)
285
+ )
286
+
287
+ # Handle content chunk
288
+ streaming_helper.handle_content_chunk(
289
+ chunk=chunk,
290
+ message_id=message_id,
291
+ print_to_console=True
292
+ )
293
+
294
+ print() # New line after streaming
295
+ return run_response
296
+
297
+ except Exception as e:
298
+ print(f"\n❌ Streaming error: {str(e)}")
299
+ # Fall back to non-streaming
300
+ if conversation_context:
301
+ return agent.run(input.prompt, stream=False, messages=conversation_context)
302
+ else:
303
+ return agent.run(input.prompt, stream=False)
304
+
305
+ # Run the async function in the event loop
306
+ try:
307
+ return loop.run_until_complete(_async_stream())
308
+ finally:
309
+ loop.close()
310
+
311
+ # Execute in thread pool (no timeout - user controls via STOP button)
312
+ # Wrap in try-except to handle Temporal cancellation
313
+ try:
314
+ result = await asyncio.to_thread(stream_agent_run)
315
+ except asyncio.CancelledError:
316
+ # Temporal cancelled the activity - cancel the running agent
317
+ print("\n🛑 Cancellation signal received - stopping agent execution...")
318
+ cancel_result = self.cancellation_manager.cancel(execution_id)
319
+ if cancel_result["success"]:
320
+ print(f"✅ Agent execution cancelled successfully")
321
+ else:
322
+ print(f"⚠️ Cancellation completed with warning: {cancel_result.get('error', 'Unknown')}")
323
+ # Re-raise to let Temporal know we're cancelled
324
+ raise
325
+
326
+ print("✅ Agent Execution Completed!")
327
+ full_response = streaming_helper.get_full_response()
328
+ print(f" Response Length: {len(full_response)} chars\n")
329
+
330
+ logger.info(
331
+ "agent_execution_completed",
332
+ execution_id=execution_id[:8],
333
+ response_length=len(full_response)
334
+ )
335
+
336
+ # Use the streamed response content
337
+ response_content = full_response if full_response else (result.content if hasattr(result, "content") else str(result))
338
+
339
+ # STEP 8: Extract usage metrics
340
+ usage = {}
341
+ if hasattr(result, "metrics") and result.metrics:
342
+ metrics = result.metrics
343
+ usage = {
344
+ "prompt_tokens": getattr(metrics, "input_tokens", 0),
345
+ "completion_tokens": getattr(metrics, "output_tokens", 0),
346
+ "total_tokens": getattr(metrics, "total_tokens", 0),
347
+ }
348
+ print(f"📊 Token Usage:")
349
+ print(f" Input: {usage.get('prompt_tokens', 0)}")
350
+ print(f" Output: {usage.get('completion_tokens', 0)}")
351
+ print(f" Total: {usage.get('total_tokens', 0)}\n")
352
+
353
+ # STEP 9: Persist complete session history
354
+ print("\n💾 Persisting session history to Control Plane...")
355
+
356
+ # Extract messages from result
357
+ new_messages = self.session_service.extract_messages_from_result(
358
+ result=result,
359
+ user_id=input.user_id
360
+ )
361
+
362
+ # Combine with previous history
363
+ complete_session = session_history + new_messages
364
+
365
+ if complete_session:
366
+ success = self.session_service.persist_session(
367
+ execution_id=execution_id,
368
+ session_id=input.session_id or execution_id,
369
+ user_id=input.user_id,
370
+ messages=complete_session,
371
+ metadata={
372
+ "agent_id": input.agent_id,
373
+ "organization_id": input.organization_id,
374
+ "turn_count": len(complete_session),
375
+ }
376
+ )
377
+
378
+ if success:
379
+ print(f" ✅ Session persisted ({len(complete_session)} total messages)")
380
+ else:
381
+ print(f" ⚠️ Session persistence failed")
382
+ else:
383
+ print(" ℹ️ No messages to persist")
384
+
385
+ print("\n" + "="*80)
386
+ print("🏁 AGENT EXECUTION END")
387
+ print("="*80 + "\n")
388
+
389
+ # STEP 10: Cleanup
390
+ self.cancellation_manager.unregister(execution_id)
391
+
392
+ return {
393
+ "success": True,
394
+ "response": response_content,
395
+ "usage": usage,
396
+ "model": model,
397
+ "finish_reason": "stop",
398
+ }
399
+
400
+ except Exception as e:
401
+ # Cleanup on error
402
+ self.cancellation_manager.unregister(execution_id)
403
+
404
+ print("\n" + "="*80)
405
+ print("❌ AGENT EXECUTION FAILED")
406
+ print("="*80)
407
+ print(f"Error: {str(e)}")
408
+ print("="*80 + "\n")
409
+
410
+ logger.error(
411
+ "agent_execution_failed",
412
+ execution_id=execution_id[:8],
413
+ error=str(e)
414
+ )
415
+
416
+ return {
417
+ "success": False,
418
+ "error": str(e),
419
+ "model": input.model_id,
420
+ "usage": None,
421
+ "finish_reason": "error",
422
+ }