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,429 @@
|
|
|
1
|
+
"""Team execution workflow for Temporal"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import Optional, List, Dict, Any
|
|
6
|
+
from temporalio import workflow
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
with workflow.unsafe.imports_passed_through():
|
|
10
|
+
from control_plane_api.worker.activities.team_activities import (
|
|
11
|
+
get_team_agents,
|
|
12
|
+
execute_team_coordination,
|
|
13
|
+
ActivityGetTeamAgentsInput,
|
|
14
|
+
ActivityExecuteTeamInput,
|
|
15
|
+
)
|
|
16
|
+
from control_plane_api.worker.activities.agent_activities import (
|
|
17
|
+
update_execution_status,
|
|
18
|
+
ActivityUpdateExecutionInput,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TeamExecutionInput:
|
|
24
|
+
"""Input for team execution workflow"""
|
|
25
|
+
execution_id: str
|
|
26
|
+
team_id: str
|
|
27
|
+
organization_id: str
|
|
28
|
+
prompt: str
|
|
29
|
+
system_prompt: Optional[str] = None
|
|
30
|
+
team_config: dict = None
|
|
31
|
+
user_metadata: dict = None
|
|
32
|
+
mcp_servers: dict = None # MCP servers configuration
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
if self.team_config is None:
|
|
36
|
+
self.team_config = {}
|
|
37
|
+
if self.user_metadata is None:
|
|
38
|
+
self.user_metadata = {}
|
|
39
|
+
if self.mcp_servers is None:
|
|
40
|
+
self.mcp_servers = {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ChatMessage:
|
|
45
|
+
"""Represents a message in the conversation"""
|
|
46
|
+
role: str # "user", "assistant", "system", "tool"
|
|
47
|
+
content: str
|
|
48
|
+
timestamp: str
|
|
49
|
+
tool_name: Optional[str] = None
|
|
50
|
+
tool_input: Optional[Dict[str, Any]] = None
|
|
51
|
+
tool_output: Optional[Dict[str, Any]] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ExecutionState:
|
|
56
|
+
"""Current state of the execution for queries"""
|
|
57
|
+
status: str # "pending", "running", "waiting_for_input", "completed", "failed"
|
|
58
|
+
messages: List[ChatMessage] = field(default_factory=list)
|
|
59
|
+
current_response: str = ""
|
|
60
|
+
error_message: Optional[str] = None
|
|
61
|
+
usage: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
is_waiting_for_input: bool = False
|
|
64
|
+
should_complete: bool = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@workflow.defn
|
|
68
|
+
class TeamExecutionWorkflow:
|
|
69
|
+
"""
|
|
70
|
+
Workflow for executing a team of agents with HITL support.
|
|
71
|
+
|
|
72
|
+
This workflow:
|
|
73
|
+
1. Gets team agents
|
|
74
|
+
2. Coordinates execution across agents
|
|
75
|
+
3. Aggregates results
|
|
76
|
+
4. Updates execution status
|
|
77
|
+
5. Supports queries for real-time state access
|
|
78
|
+
6. Supports signals for adding followup messages
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self) -> None:
|
|
82
|
+
"""Initialize workflow state"""
|
|
83
|
+
self._state = ExecutionState(status="pending")
|
|
84
|
+
self._lock = asyncio.Lock()
|
|
85
|
+
self._new_message_count = 0
|
|
86
|
+
self._processed_message_count = 0
|
|
87
|
+
|
|
88
|
+
@workflow.query
|
|
89
|
+
def get_state(self) -> ExecutionState:
|
|
90
|
+
"""Query handler: Get current execution state including messages and status"""
|
|
91
|
+
return self._state
|
|
92
|
+
|
|
93
|
+
@workflow.signal
|
|
94
|
+
async def add_message(self, message: ChatMessage) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Signal handler: Add a message to the conversation.
|
|
97
|
+
This allows clients to send followup messages while the workflow is running.
|
|
98
|
+
"""
|
|
99
|
+
async with self._lock:
|
|
100
|
+
self._state.messages.append(message)
|
|
101
|
+
self._new_message_count += 1
|
|
102
|
+
self._state.is_waiting_for_input = False
|
|
103
|
+
workflow.logger.info(
|
|
104
|
+
f"Message added to team conversation",
|
|
105
|
+
extra={
|
|
106
|
+
"role": message.role,
|
|
107
|
+
"content_preview": message.content[:100] if message.content else "",
|
|
108
|
+
"total_messages": len(self._state.messages)
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@workflow.signal
|
|
113
|
+
async def mark_as_done(self) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Signal handler: Mark the workflow as complete.
|
|
116
|
+
"""
|
|
117
|
+
async with self._lock:
|
|
118
|
+
self._state.should_complete = True
|
|
119
|
+
self._state.is_waiting_for_input = False
|
|
120
|
+
workflow.logger.info("Team workflow marked as done by user")
|
|
121
|
+
|
|
122
|
+
@workflow.signal
|
|
123
|
+
async def update_streaming_response(self, current_response: str) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Signal handler: Update current streaming response.
|
|
126
|
+
Activity sends this periodically during execution for state tracking.
|
|
127
|
+
"""
|
|
128
|
+
async with self._lock:
|
|
129
|
+
self._state.current_response = current_response
|
|
130
|
+
workflow.logger.info(
|
|
131
|
+
f"Streaming response updated",
|
|
132
|
+
extra={"response_length": len(current_response)}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@workflow.run
|
|
136
|
+
async def run(self, input: TeamExecutionInput) -> dict:
|
|
137
|
+
"""
|
|
138
|
+
Run the team execution workflow with HITL pattern.
|
|
139
|
+
|
|
140
|
+
This workflow implements a continuous conversation loop:
|
|
141
|
+
1. Process the initial user message
|
|
142
|
+
2. Execute team coordination and return response
|
|
143
|
+
3. Wait for user input (signals)
|
|
144
|
+
4. Process followup messages in a loop
|
|
145
|
+
5. Only complete when user explicitly marks as done
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
input: Workflow input with team execution details
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Team execution result dict
|
|
152
|
+
"""
|
|
153
|
+
workflow.logger.info(
|
|
154
|
+
f"Starting team execution workflow with HITL pattern",
|
|
155
|
+
extra={
|
|
156
|
+
"execution_id": input.execution_id,
|
|
157
|
+
"team_id": input.team_id,
|
|
158
|
+
"organization_id": input.organization_id,
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Initialize state with user's initial message
|
|
163
|
+
self._state.messages.append(ChatMessage(
|
|
164
|
+
role="user",
|
|
165
|
+
content=input.prompt,
|
|
166
|
+
timestamp=workflow.now().isoformat(),
|
|
167
|
+
))
|
|
168
|
+
self._state.status = "running"
|
|
169
|
+
self._new_message_count = 1
|
|
170
|
+
self._processed_message_count = 0
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
# Step 1: Update execution status to running
|
|
174
|
+
await workflow.execute_activity(
|
|
175
|
+
update_execution_status,
|
|
176
|
+
ActivityUpdateExecutionInput(
|
|
177
|
+
execution_id=input.execution_id,
|
|
178
|
+
status="running",
|
|
179
|
+
started_at=workflow.now().isoformat(),
|
|
180
|
+
execution_metadata={
|
|
181
|
+
"workflow_started": True,
|
|
182
|
+
"hitl_enabled": True,
|
|
183
|
+
},
|
|
184
|
+
),
|
|
185
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Step 2: Get team agents once at the beginning
|
|
189
|
+
workflow.logger.info(
|
|
190
|
+
f"[WORKFLOW] About to call get_team_agents",
|
|
191
|
+
extra={
|
|
192
|
+
"team_id": input.team_id,
|
|
193
|
+
"organization_id": input.organization_id,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
team_agents = await workflow.execute_activity(
|
|
198
|
+
get_team_agents,
|
|
199
|
+
ActivityGetTeamAgentsInput(
|
|
200
|
+
team_id=input.team_id,
|
|
201
|
+
organization_id=input.organization_id,
|
|
202
|
+
),
|
|
203
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
workflow.logger.info(
|
|
207
|
+
f"[WORKFLOW] get_team_agents returned",
|
|
208
|
+
extra={
|
|
209
|
+
"result": team_agents,
|
|
210
|
+
"agents_count": len(team_agents.get("agents", [])) if team_agents else 0,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if not team_agents.get("agents"):
|
|
215
|
+
workflow.logger.error(
|
|
216
|
+
f"[WORKFLOW] NO AGENTS RETURNED!",
|
|
217
|
+
extra={
|
|
218
|
+
"team_agents": team_agents,
|
|
219
|
+
"team_id": input.team_id,
|
|
220
|
+
"organization_id": input.organization_id,
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
raise ValueError("No agents found in team")
|
|
224
|
+
|
|
225
|
+
# HITL Conversation Loop - Continue until user marks as done
|
|
226
|
+
conversation_turn = 0
|
|
227
|
+
while not self._state.should_complete:
|
|
228
|
+
conversation_turn += 1
|
|
229
|
+
workflow.logger.info(
|
|
230
|
+
f"Starting team conversation turn {conversation_turn}",
|
|
231
|
+
extra={"turn": conversation_turn, "message_count": len(self._state.messages)}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Get the latest user message
|
|
235
|
+
latest_message = self._state.messages[-1] if self._state.messages else None
|
|
236
|
+
latest_prompt = latest_message.content if latest_message and latest_message.role == "user" else input.prompt
|
|
237
|
+
|
|
238
|
+
# Step 3: Execute team coordination
|
|
239
|
+
team_result = await workflow.execute_activity(
|
|
240
|
+
execute_team_coordination,
|
|
241
|
+
ActivityExecuteTeamInput(
|
|
242
|
+
execution_id=input.execution_id,
|
|
243
|
+
team_id=input.team_id,
|
|
244
|
+
organization_id=input.organization_id,
|
|
245
|
+
prompt=latest_prompt,
|
|
246
|
+
system_prompt=input.system_prompt,
|
|
247
|
+
agents=team_agents["agents"],
|
|
248
|
+
team_config=input.team_config,
|
|
249
|
+
mcp_servers=input.mcp_servers, # Pass MCP servers
|
|
250
|
+
session_id=input.execution_id, # Use execution_id as session_id
|
|
251
|
+
user_id=input.user_metadata.get("user_id") if input.user_metadata else None,
|
|
252
|
+
# Activity reads CONTROL_PLANE_URL and KUBIYA_API_KEY from worker environment
|
|
253
|
+
),
|
|
254
|
+
start_to_close_timeout=timedelta(minutes=30), # Teams can take longer
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Add tool execution status messages (real-time updates)
|
|
258
|
+
if team_result.get("tool_execution_messages"):
|
|
259
|
+
async with self._lock:
|
|
260
|
+
for tool_msg in team_result["tool_execution_messages"]:
|
|
261
|
+
self._state.messages.append(ChatMessage(
|
|
262
|
+
role="system",
|
|
263
|
+
content=tool_msg.get("content", ""),
|
|
264
|
+
timestamp=tool_msg.get("timestamp", workflow.now().isoformat()),
|
|
265
|
+
tool_name=tool_msg.get("tool_name"),
|
|
266
|
+
))
|
|
267
|
+
|
|
268
|
+
# Add tool messages to state (detailed tool info)
|
|
269
|
+
if team_result.get("tool_messages"):
|
|
270
|
+
async with self._lock:
|
|
271
|
+
for tool_msg in team_result["tool_messages"]:
|
|
272
|
+
self._state.messages.append(ChatMessage(
|
|
273
|
+
role="tool",
|
|
274
|
+
content=tool_msg.get("content", ""),
|
|
275
|
+
timestamp=tool_msg.get("timestamp", workflow.now().isoformat()),
|
|
276
|
+
tool_name=tool_msg.get("tool_name"),
|
|
277
|
+
tool_input=tool_msg.get("tool_input"),
|
|
278
|
+
))
|
|
279
|
+
|
|
280
|
+
# Update state with team response
|
|
281
|
+
if team_result.get("response"):
|
|
282
|
+
async with self._lock:
|
|
283
|
+
self._state.messages.append(ChatMessage(
|
|
284
|
+
role="assistant",
|
|
285
|
+
content=team_result["response"],
|
|
286
|
+
timestamp=workflow.now().isoformat(),
|
|
287
|
+
))
|
|
288
|
+
self._state.current_response = team_result["response"]
|
|
289
|
+
self._processed_message_count += 1
|
|
290
|
+
|
|
291
|
+
# Update usage and metadata (accumulate across turns)
|
|
292
|
+
if team_result.get("usage"):
|
|
293
|
+
current_usage = self._state.usage
|
|
294
|
+
new_usage = team_result.get("usage", {})
|
|
295
|
+
self._state.usage = {
|
|
296
|
+
"input_tokens": current_usage.get("input_tokens", 0) + new_usage.get("input_tokens", 0),
|
|
297
|
+
"output_tokens": current_usage.get("output_tokens", 0) + new_usage.get("output_tokens", 0),
|
|
298
|
+
"total_tokens": current_usage.get("total_tokens", 0) + new_usage.get("total_tokens", 0),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Update metadata
|
|
302
|
+
self._state.metadata.update({
|
|
303
|
+
"agent_count": len(team_agents["agents"]),
|
|
304
|
+
"coordination_type": team_result.get("coordination_type"),
|
|
305
|
+
"conversation_turns": conversation_turn,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
# Check if team execution failed
|
|
309
|
+
if not team_result.get("success"):
|
|
310
|
+
self._state.status = "failed"
|
|
311
|
+
self._state.error_message = team_result.get("error")
|
|
312
|
+
break
|
|
313
|
+
|
|
314
|
+
# Update execution status to waiting_for_input
|
|
315
|
+
self._state.status = "waiting_for_input"
|
|
316
|
+
self._state.is_waiting_for_input = True
|
|
317
|
+
|
|
318
|
+
# Update database to reflect waiting state
|
|
319
|
+
await workflow.execute_activity(
|
|
320
|
+
update_execution_status,
|
|
321
|
+
ActivityUpdateExecutionInput(
|
|
322
|
+
execution_id=input.execution_id,
|
|
323
|
+
status="waiting_for_input",
|
|
324
|
+
response=self._state.current_response,
|
|
325
|
+
usage=self._state.usage,
|
|
326
|
+
execution_metadata={
|
|
327
|
+
**self._state.metadata,
|
|
328
|
+
"conversation_turns": conversation_turn,
|
|
329
|
+
"waiting_for_user": True,
|
|
330
|
+
},
|
|
331
|
+
),
|
|
332
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
workflow.logger.info(
|
|
336
|
+
f"Waiting for user input after team turn {conversation_turn}",
|
|
337
|
+
extra={"turn": conversation_turn}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Wait for either new message or mark as done
|
|
341
|
+
await workflow.wait_condition(
|
|
342
|
+
lambda: self._new_message_count > self._processed_message_count or self._state.should_complete,
|
|
343
|
+
timeout=timedelta(hours=24)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if self._state.should_complete:
|
|
347
|
+
workflow.logger.info("User marked team workflow as done")
|
|
348
|
+
break
|
|
349
|
+
|
|
350
|
+
# Continue loop to process new message
|
|
351
|
+
self._state.status = "running"
|
|
352
|
+
|
|
353
|
+
# Conversation complete - finalize workflow
|
|
354
|
+
final_status = "failed" if self._state.status == "failed" else "completed"
|
|
355
|
+
self._state.status = final_status
|
|
356
|
+
|
|
357
|
+
await workflow.execute_activity(
|
|
358
|
+
update_execution_status,
|
|
359
|
+
ActivityUpdateExecutionInput(
|
|
360
|
+
execution_id=input.execution_id,
|
|
361
|
+
status=final_status,
|
|
362
|
+
completed_at=workflow.now().isoformat(),
|
|
363
|
+
response=self._state.current_response,
|
|
364
|
+
error_message=self._state.error_message,
|
|
365
|
+
usage=self._state.usage,
|
|
366
|
+
execution_metadata={
|
|
367
|
+
**self._state.metadata,
|
|
368
|
+
"workflow_completed": True,
|
|
369
|
+
"total_conversation_turns": conversation_turn,
|
|
370
|
+
},
|
|
371
|
+
),
|
|
372
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
workflow.logger.info(
|
|
376
|
+
f"Team execution workflow completed with HITL",
|
|
377
|
+
extra={
|
|
378
|
+
"execution_id": input.execution_id,
|
|
379
|
+
"status": final_status,
|
|
380
|
+
"conversation_turns": conversation_turn,
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
"success": final_status == "completed",
|
|
386
|
+
"execution_id": input.execution_id,
|
|
387
|
+
"status": final_status,
|
|
388
|
+
"response": self._state.current_response,
|
|
389
|
+
"usage": self._state.usage,
|
|
390
|
+
"conversation_turns": conversation_turn,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
except Exception as e:
|
|
394
|
+
# Update state with error
|
|
395
|
+
self._state.status = "failed"
|
|
396
|
+
self._state.error_message = str(e)
|
|
397
|
+
self._state.metadata["error_type"] = type(e).__name__
|
|
398
|
+
|
|
399
|
+
workflow.logger.error(
|
|
400
|
+
f"Team execution workflow failed",
|
|
401
|
+
extra={
|
|
402
|
+
"execution_id": input.execution_id,
|
|
403
|
+
"error": str(e),
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Update execution as failed
|
|
408
|
+
try:
|
|
409
|
+
await workflow.execute_activity(
|
|
410
|
+
update_execution_status,
|
|
411
|
+
ActivityUpdateExecutionInput(
|
|
412
|
+
execution_id=input.execution_id,
|
|
413
|
+
status="failed",
|
|
414
|
+
completed_at=workflow.now().isoformat(),
|
|
415
|
+
error_message=f"Workflow error: {str(e)}",
|
|
416
|
+
execution_metadata={
|
|
417
|
+
"workflow_error": True,
|
|
418
|
+
"error_type": type(e).__name__,
|
|
419
|
+
},
|
|
420
|
+
),
|
|
421
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
422
|
+
)
|
|
423
|
+
except Exception as update_error:
|
|
424
|
+
workflow.logger.error(
|
|
425
|
+
f"Failed to update status after error",
|
|
426
|
+
extra={"error": str(update_error)}
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
raise
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kubiya-control-plane-api
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Agent Control Plane API - Multi-tenant AI agent orchestration and management platform
|
|
5
|
+
Author-email: Kubiya <support@kubiya.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ai,agents,orchestration,temporal,fastapi,multi-tenant,workflow,automation
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
18
|
+
Requires-Python: <4.0.0,>=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: httpx<1.0.0,>=0.26.0
|
|
21
|
+
Requires-Dist: pydantic<3.0.0,>=2.5.3
|
|
22
|
+
Requires-Dist: fastapi<1.0.0,>=0.109.0
|
|
23
|
+
Requires-Dist: uvicorn[standard]<1.0.0,>=0.27.0
|
|
24
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.25
|
|
25
|
+
Requires-Dist: psycopg2-binary<3.0.0,>=2.9.9
|
|
26
|
+
Requires-Dist: pydantic-settings<3.0.0,>=2.1.0
|
|
27
|
+
Requires-Dist: alembic<2.0.0,>=1.13.1
|
|
28
|
+
Requires-Dist: supabase<3.0.0,>=2.3.0
|
|
29
|
+
Requires-Dist: temporalio<2.0.0,>=1.5.0
|
|
30
|
+
Requires-Dist: python-multipart<1.0.0,>=0.0.6
|
|
31
|
+
Requires-Dist: python-jose[cryptography]<4.0.0,>=3.3.0
|
|
32
|
+
Requires-Dist: PyJWT<3.0.0,>=2.8.0
|
|
33
|
+
Requires-Dist: passlib[bcrypt]<2.0.0,>=1.7.4
|
|
34
|
+
Requires-Dist: mangum<1.0.0,>=0.17.0
|
|
35
|
+
Requires-Dist: structlog<25.0.0,>=24.1.0
|
|
36
|
+
Requires-Dist: croniter<3.0.0,>=2.0.0
|
|
37
|
+
Requires-Dist: litellm<2.0.0,>=1.30.0
|
|
38
|
+
Requires-Dist: agno<3.0.0,>=2.0.10
|
|
39
|
+
Requires-Dist: mcp<2.0.0,>=1.0.0
|
|
40
|
+
Requires-Dist: redis<6.0.0,>=5.0.0
|
|
41
|
+
Requires-Dist: kubiya-sdk<3.0.0,>=2.0.3
|
|
42
|
+
Provides-Extra: worker
|
|
43
|
+
Requires-Dist: temporalio<2.0.0,>=1.5.0; extra == "worker"
|
|
44
|
+
Requires-Dist: structlog<25.0.0,>=24.1.0; extra == "worker"
|
|
45
|
+
Requires-Dist: agno<3.0.0,>=2.0.10; extra == "worker"
|
|
46
|
+
Requires-Dist: litellm<2.0.0,>=1.30.0; extra == "worker"
|
|
47
|
+
Requires-Dist: psutil<6.0.0,>=5.9.0; extra == "worker"
|
|
48
|
+
Requires-Dist: docker<8.0.0,>=7.0.0; extra == "worker"
|
|
49
|
+
Requires-Dist: redis<6.0.0,>=5.0.0; extra == "worker"
|
|
50
|
+
Requires-Dist: mcp<2.0.0,>=1.0.0; extra == "worker"
|
|
51
|
+
Requires-Dist: nest-asyncio<2.0.0,>=1.5.0; extra == "worker"
|
|
52
|
+
Requires-Dist: claude-agent-sdk<1.0.0,>=0.1.0; extra == "worker"
|
|
53
|
+
Provides-Extra: api
|
|
54
|
+
Requires-Dist: fastapi<1.0.0,>=0.109.0; extra == "api"
|
|
55
|
+
Requires-Dist: uvicorn[standard]<1.0.0,>=0.27.0; extra == "api"
|
|
56
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.25; extra == "api"
|
|
57
|
+
Requires-Dist: psycopg2-binary<3.0.0,>=2.9.9; extra == "api"
|
|
58
|
+
Requires-Dist: pydantic-settings<3.0.0,>=2.1.0; extra == "api"
|
|
59
|
+
Requires-Dist: alembic<2.0.0,>=1.13.1; extra == "api"
|
|
60
|
+
Requires-Dist: supabase<3.0.0,>=2.3.0; extra == "api"
|
|
61
|
+
Requires-Dist: temporalio<2.0.0,>=1.5.0; extra == "api"
|
|
62
|
+
Requires-Dist: python-multipart<1.0.0,>=0.0.6; extra == "api"
|
|
63
|
+
Requires-Dist: python-jose[cryptography]<4.0.0,>=3.3.0; extra == "api"
|
|
64
|
+
Requires-Dist: PyJWT<3.0.0,>=2.8.0; extra == "api"
|
|
65
|
+
Requires-Dist: passlib[bcrypt]<2.0.0,>=1.7.4; extra == "api"
|
|
66
|
+
Requires-Dist: mangum<1.0.0,>=0.17.0; extra == "api"
|
|
67
|
+
Requires-Dist: structlog<25.0.0,>=24.1.0; extra == "api"
|
|
68
|
+
Requires-Dist: croniter<3.0.0,>=2.0.0; extra == "api"
|
|
69
|
+
Requires-Dist: kubiya-sdk<3.0.0,>=2.0.3; extra == "api"
|
|
70
|
+
Provides-Extra: dev
|
|
71
|
+
Requires-Dist: pytest<9.0.0,>=7.4.0; extra == "dev"
|
|
72
|
+
Requires-Dist: pytest-asyncio<1.0.0,>=0.21.0; extra == "dev"
|
|
73
|
+
Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
|
|
74
|
+
Requires-Dist: pytest-mock<4.0.0,>=3.11.0; extra == "dev"
|
|
75
|
+
Requires-Dist: pytest-xdist<4.0.0,>=3.5.0; extra == "dev"
|
|
76
|
+
Requires-Dist: fakeredis<3.0.0,>=2.20.0; extra == "dev"
|
|
77
|
+
Requires-Dist: respx<1.0.0,>=0.20.0; extra == "dev"
|
|
78
|
+
Requires-Dist: black<25.0.0,>=23.0.0; extra == "dev"
|
|
79
|
+
Requires-Dist: ruff<1.0.0,>=0.1.0; extra == "dev"
|
|
80
|
+
Requires-Dist: mypy<2.0.0,>=1.0.0; extra == "dev"
|
|
81
|
+
Provides-Extra: test
|
|
82
|
+
Requires-Dist: pytest<9.0.0,>=7.4.0; extra == "test"
|
|
83
|
+
Requires-Dist: pytest-asyncio<1.0.0,>=0.21.0; extra == "test"
|
|
84
|
+
Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "test"
|
|
85
|
+
Requires-Dist: pytest-mock<4.0.0,>=3.11.0; extra == "test"
|
|
86
|
+
Requires-Dist: pytest-xdist<4.0.0,>=3.5.0; extra == "test"
|
|
87
|
+
Requires-Dist: fakeredis<3.0.0,>=2.20.0; extra == "test"
|
|
88
|
+
Requires-Dist: respx<1.0.0,>=0.20.0; extra == "test"
|
|
89
|
+
Provides-Extra: all
|
|
90
|
+
Requires-Dist: kubiya-control-plane-api[api,dev,test,worker]; extra == "all"
|
|
91
|
+
|
|
92
|
+
# Kubiya Control Plane API
|
|
93
|
+
|
|
94
|
+
Multi-tenant AI agent orchestration and management platform powered by Temporal workflows.
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
### Basic Installation
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install kubiya-control-plane-api
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Installation with Extras
|
|
105
|
+
|
|
106
|
+
Install with development dependencies:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install "kubiya-control-plane-api[dev]"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Install with test dependencies:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install "kubiya-control-plane-api[test]"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Install with all optional dependencies:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pip install "kubiya-control-plane-api[all]"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Running for Development
|
|
125
|
+
|
|
126
|
+
### 1. Clone the repository
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/kubiyabot/agent-control-plane.git
|
|
130
|
+
cd agent-control-plane
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Set up environment variables
|
|
134
|
+
|
|
135
|
+
Create a `.env` file with the required variables (see below).
|
|
136
|
+
|
|
137
|
+
### 3. Build an image and run.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
make build
|
|
141
|
+
|
|
142
|
+
make up
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The API will be available at `http://localhost:7777/api/docs`
|
|
146
|
+
|
|
147
|
+
## Running the Worker
|
|
148
|
+
|
|
149
|
+
The worker processes Temporal workflows for agent execution.
|
|
150
|
+
|
|
151
|
+
### Using the CLI command
|
|
152
|
+
|
|
153
|
+
After installing the package, run:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
kubiya-control-plane-worker
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Using Python module
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python -m control_plane_api.worker
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Required Environment Variables
|
|
166
|
+
|
|
167
|
+
The following environment variables **must** be set to run the worker:
|
|
168
|
+
|
|
169
|
+
### Temporal Configuration (Required)
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
TEMPORAL_HOST=localhost:7233
|
|
173
|
+
TEMPORAL_NAMESPACE=default
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `TEMPORAL_HOST`: Address of your Temporal server
|
|
177
|
+
- `TEMPORAL_NAMESPACE`: Temporal namespace to use
|
|
178
|
+
|
|
179
|
+
### Database Configuration (Required)
|
|
180
|
+
|
|
181
|
+
**Option 1: Direct PostgreSQL**
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/control_plane
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Option 2: Supabase**
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
SUPABASE_URL=https://your-project.supabase.co
|
|
191
|
+
SUPABASE_SERVICE_KEY=your-service-role-key
|
|
192
|
+
SUPABASE_POSTGRES_URL=postgresql://user:password@host:5432/database
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
You need **either** `DATABASE_URL` **or** all three Supabase variables.
|
|
196
|
+
|
|
197
|
+
### Temporal Cloud Authentication (If using Temporal Cloud)
|
|
198
|
+
|
|
199
|
+
If connecting to Temporal Cloud instead of a self-hosted server, you also need **one** of:
|
|
200
|
+
|
|
201
|
+
**Option A: API Key**
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
TEMPORAL_API_KEY=your-temporal-cloud-api-key
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Option B: mTLS Certificates**
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
TEMPORAL_CLIENT_CERT_PATH=/path/to/cert.pem
|
|
211
|
+
TEMPORAL_CLIENT_KEY_PATH=/path/to/key.pem
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Quick Start Example
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# 1. Install the package
|
|
218
|
+
pip install kubiya-control-plane-api
|
|
219
|
+
|
|
220
|
+
# 2. Set required environment variables
|
|
221
|
+
export TEMPORAL_HOST=localhost:7233
|
|
222
|
+
export TEMPORAL_NAMESPACE=default
|
|
223
|
+
export DATABASE_URL=postgresql://user:password@localhost:5432/control_plane
|
|
224
|
+
|
|
225
|
+
# 3. Run the worker
|
|
226
|
+
kubiya-control-plane-worker
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The worker will connect to Temporal and start processing agent execution workflows.
|