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,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
|
+
}
|