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,1400 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
2
|
+
from fastapi.responses import StreamingResponse
|
|
3
|
+
from sqlalchemy.orm import Session
|
|
4
|
+
from sqlalchemy.exc import IntegrityError
|
|
5
|
+
from typing import List, Union, Dict, Any, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
import structlog
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
from control_plane_api.app.database import get_db
|
|
12
|
+
from control_plane_api.app.models.team import Team
|
|
13
|
+
from control_plane_api.app.models.agent import Agent
|
|
14
|
+
from control_plane_api.app.middleware.auth import get_current_organization
|
|
15
|
+
from control_plane_api.app.lib.supabase import get_supabase
|
|
16
|
+
from control_plane_api.app.lib.temporal_client import get_temporal_client
|
|
17
|
+
from control_plane_api.app.workflows.agent_execution import AgentExecutionWorkflow, TeamExecutionInput
|
|
18
|
+
from control_plane_api.app.workflows.team_execution import TeamExecutionWorkflow
|
|
19
|
+
from control_plane_api.app.routers.projects import get_default_project_id
|
|
20
|
+
from control_plane_api.app.routers.agents_v2 import ExecutionEnvironment
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
logger = structlog.get_logger()
|
|
24
|
+
|
|
25
|
+
router = APIRouter()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Team status enum
|
|
29
|
+
class TeamStatus(str, Enum):
|
|
30
|
+
"""Team status enumeration"""
|
|
31
|
+
ACTIVE = "active"
|
|
32
|
+
INACTIVE = "inactive"
|
|
33
|
+
ARCHIVED = "archived"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_entity_skills(client, organization_id: str, entity_type: str, entity_id: str) -> List[dict]:
|
|
37
|
+
"""Get skills associated with an entity"""
|
|
38
|
+
# Get associations
|
|
39
|
+
result = (
|
|
40
|
+
client.table("skill_associations")
|
|
41
|
+
.select("skill_id, configuration_override, skills(*)")
|
|
42
|
+
.eq("organization_id", organization_id)
|
|
43
|
+
.eq("entity_type", entity_type)
|
|
44
|
+
.eq("entity_id", entity_id)
|
|
45
|
+
.execute()
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
skills = []
|
|
49
|
+
for item in result.data:
|
|
50
|
+
skill_data = item.get("skills")
|
|
51
|
+
if skill_data and skill_data.get("enabled", True):
|
|
52
|
+
# Merge configuration with override
|
|
53
|
+
config = skill_data.get("configuration", {})
|
|
54
|
+
override = item.get("configuration_override")
|
|
55
|
+
if override:
|
|
56
|
+
config = {**config, **override}
|
|
57
|
+
|
|
58
|
+
skills.append({
|
|
59
|
+
"id": skill_data["id"],
|
|
60
|
+
"name": skill_data["name"],
|
|
61
|
+
"type": skill_data["skill_type"],
|
|
62
|
+
"description": skill_data.get("description"),
|
|
63
|
+
"enabled": skill_data.get("enabled", True),
|
|
64
|
+
"configuration": config,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return skills
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_team_projects(db, team_id: str) -> list[dict]:
|
|
71
|
+
"""Get all projects a team belongs to"""
|
|
72
|
+
try:
|
|
73
|
+
from control_plane_api.app.lib.supabase import get_supabase
|
|
74
|
+
client = get_supabase()
|
|
75
|
+
|
|
76
|
+
# Query project_teams join table
|
|
77
|
+
result = (
|
|
78
|
+
client.table("project_teams")
|
|
79
|
+
.select("project_id, projects(id, name, key, description)")
|
|
80
|
+
.eq("team_id", team_id)
|
|
81
|
+
.execute()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
projects = []
|
|
85
|
+
for item in result.data:
|
|
86
|
+
project_data = item.get("projects")
|
|
87
|
+
if project_data:
|
|
88
|
+
projects.append({
|
|
89
|
+
"id": project_data["id"],
|
|
90
|
+
"name": project_data["name"],
|
|
91
|
+
"key": project_data["key"],
|
|
92
|
+
"description": project_data.get("description"),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return projects
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning("failed_to_fetch_team_projects", error=str(e), team_id=team_id)
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Enhanced Pydantic schemas aligned with Agno Team capabilities
|
|
102
|
+
|
|
103
|
+
class ReasoningConfig(BaseModel):
|
|
104
|
+
"""Reasoning configuration for the team"""
|
|
105
|
+
enabled: bool = Field(False, description="Enable reasoning for the team")
|
|
106
|
+
model: Optional[str] = Field(None, description="Model to use for reasoning")
|
|
107
|
+
agent_id: Optional[str] = Field(None, description="Agent ID to use for reasoning")
|
|
108
|
+
min_steps: Optional[int] = Field(1, description="Minimum reasoning steps", ge=1)
|
|
109
|
+
max_steps: Optional[int] = Field(10, description="Maximum reasoning steps", ge=1, le=100)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class LLMConfig(BaseModel):
|
|
113
|
+
"""LLM configuration for the team"""
|
|
114
|
+
model: Optional[str] = Field(None, description="Default model for the team")
|
|
115
|
+
temperature: Optional[float] = Field(None, description="Temperature for generation", ge=0.0, le=2.0)
|
|
116
|
+
max_tokens: Optional[int] = Field(None, description="Maximum tokens to generate", ge=1)
|
|
117
|
+
top_p: Optional[float] = Field(None, description="Top-p sampling", ge=0.0, le=1.0)
|
|
118
|
+
top_k: Optional[int] = Field(None, description="Top-k sampling", ge=0)
|
|
119
|
+
stop: Optional[List[str]] = Field(None, description="Stop sequences")
|
|
120
|
+
frequency_penalty: Optional[float] = Field(None, description="Frequency penalty", ge=-2.0, le=2.0)
|
|
121
|
+
presence_penalty: Optional[float] = Field(None, description="Presence penalty", ge=-2.0, le=2.0)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SessionConfig(BaseModel):
|
|
125
|
+
"""Session configuration for the team"""
|
|
126
|
+
user_id: Optional[str] = Field(None, description="User ID for the session")
|
|
127
|
+
session_id: Optional[str] = Field(None, description="Session ID")
|
|
128
|
+
auto_save: bool = Field(True, description="Auto-save session state")
|
|
129
|
+
persist: bool = Field(True, description="Persist session across runs")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TeamConfiguration(BaseModel):
|
|
133
|
+
"""
|
|
134
|
+
Comprehensive team configuration aligned with Agno's Team capabilities.
|
|
135
|
+
This allows full control over team behavior, reasoning, tools, and LLM settings.
|
|
136
|
+
"""
|
|
137
|
+
# Members
|
|
138
|
+
member_ids: List[str] = Field(default_factory=list, description="List of agent IDs in the team")
|
|
139
|
+
|
|
140
|
+
# Instructions
|
|
141
|
+
instructions: Union[str, List[str]] = Field(
|
|
142
|
+
default="",
|
|
143
|
+
description="Instructions for the team - can be a single string or list of instructions"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Reasoning
|
|
147
|
+
reasoning: Optional[ReasoningConfig] = Field(None, description="Reasoning configuration")
|
|
148
|
+
|
|
149
|
+
# LLM Configuration
|
|
150
|
+
llm: Optional[LLMConfig] = Field(None, description="LLM configuration for the team")
|
|
151
|
+
|
|
152
|
+
# Tools & Knowledge
|
|
153
|
+
tools: List[Dict[str, Any]] = Field(
|
|
154
|
+
default_factory=list,
|
|
155
|
+
description="Tools available to the team - list of tool configurations"
|
|
156
|
+
)
|
|
157
|
+
knowledge_base: Optional[Dict[str, Any]] = Field(
|
|
158
|
+
None,
|
|
159
|
+
description="Knowledge base configuration (vector store, embeddings, etc.)"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Session & State
|
|
163
|
+
session: Optional[SessionConfig] = Field(None, description="Session configuration")
|
|
164
|
+
dependencies: Dict[str, Any] = Field(
|
|
165
|
+
default_factory=dict,
|
|
166
|
+
description="External dependencies (databases, APIs, services)"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Advanced Options
|
|
170
|
+
markdown: bool = Field(True, description="Enable markdown formatting in responses")
|
|
171
|
+
add_datetime_to_instructions: bool = Field(
|
|
172
|
+
False,
|
|
173
|
+
description="Automatically add current datetime to instructions"
|
|
174
|
+
)
|
|
175
|
+
structured_outputs: bool = Field(False, description="Enable structured outputs")
|
|
176
|
+
response_model: Optional[str] = Field(None, description="Response model schema name")
|
|
177
|
+
|
|
178
|
+
# Monitoring & Debugging
|
|
179
|
+
debug_mode: bool = Field(False, description="Enable debug mode with verbose logging")
|
|
180
|
+
monitoring: bool = Field(False, description="Enable monitoring and telemetry")
|
|
181
|
+
|
|
182
|
+
# Custom Metadata
|
|
183
|
+
metadata: Dict[str, Any] = Field(
|
|
184
|
+
default_factory=dict,
|
|
185
|
+
description="Additional custom metadata for the team"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TeamCreate(BaseModel):
|
|
190
|
+
"""Create a new team with full Agno capabilities"""
|
|
191
|
+
name: str = Field(..., description="Team name", min_length=1, max_length=255)
|
|
192
|
+
description: Optional[str] = Field(None, description="Team description")
|
|
193
|
+
configuration: TeamConfiguration = Field(
|
|
194
|
+
default_factory=TeamConfiguration,
|
|
195
|
+
description="Team configuration aligned with Agno Team"
|
|
196
|
+
)
|
|
197
|
+
skill_ids: list[str] = Field(default_factory=list, description="Tool set IDs to associate with this team")
|
|
198
|
+
skill_configurations: dict[str, dict] = Field(default_factory=dict, description="Tool set configurations keyed by skill ID")
|
|
199
|
+
execution_environment: ExecutionEnvironment | None = Field(None, description="Execution environment: env vars, secrets, integrations")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TeamUpdate(BaseModel):
|
|
203
|
+
"""Update an existing team"""
|
|
204
|
+
name: Optional[str] = Field(None, description="Team name", min_length=1, max_length=255)
|
|
205
|
+
description: Optional[str] = Field(None, description="Team description")
|
|
206
|
+
status: Optional[TeamStatus] = Field(None, description="Team status")
|
|
207
|
+
runtime: Optional[str] = Field(None, description="Runtime type: 'default' (Agno) or 'claude_code' (Claude Code SDK)")
|
|
208
|
+
configuration: Optional[TeamConfiguration] = Field(None, description="Team configuration")
|
|
209
|
+
skill_ids: list[str] | None = None
|
|
210
|
+
skill_configurations: dict[str, dict] | None = None
|
|
211
|
+
environment_ids: list[str] | None = None
|
|
212
|
+
execution_environment: ExecutionEnvironment | None = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TeamResponse(BaseModel):
|
|
216
|
+
"""Team response with structured configuration"""
|
|
217
|
+
id: str
|
|
218
|
+
organization_id: str
|
|
219
|
+
name: str
|
|
220
|
+
description: Optional[str]
|
|
221
|
+
status: TeamStatus
|
|
222
|
+
configuration: TeamConfiguration
|
|
223
|
+
created_at: datetime
|
|
224
|
+
updated_at: datetime
|
|
225
|
+
projects: List[dict] = Field(default_factory=list, description="Projects this team belongs to")
|
|
226
|
+
skill_ids: Optional[List[str]] = Field(default_factory=list, description="IDs of associated skills")
|
|
227
|
+
skills: Optional[List[dict]] = Field(default_factory=list, description="Associated skills with details")
|
|
228
|
+
execution_environment: ExecutionEnvironment | None = None
|
|
229
|
+
|
|
230
|
+
class Config:
|
|
231
|
+
from_attributes = True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TeamWithAgentsResponse(TeamResponse):
|
|
235
|
+
"""Team response including member agents"""
|
|
236
|
+
agents: List[dict]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class TeamExecutionRequest(BaseModel):
|
|
240
|
+
prompt: str = Field(..., description="The prompt/task to execute")
|
|
241
|
+
system_prompt: str | None = Field(None, description="Optional system prompt for team coordination")
|
|
242
|
+
stream: bool = Field(False, description="Whether to stream the response")
|
|
243
|
+
worker_queue_id: str = Field(..., description="Worker queue ID (UUID) to route execution to - REQUIRED")
|
|
244
|
+
user_metadata: dict | None = Field(None, description="User attribution metadata (optional, auto-filled from token)")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TeamExecutionResponse(BaseModel):
|
|
248
|
+
execution_id: str
|
|
249
|
+
workflow_id: str
|
|
250
|
+
status: str
|
|
251
|
+
message: str
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@router.post("", response_model=TeamResponse, status_code=status.HTTP_201_CREATED)
|
|
255
|
+
def create_team(
|
|
256
|
+
team_data: TeamCreate,
|
|
257
|
+
request: Request,
|
|
258
|
+
db: Session = Depends(get_db),
|
|
259
|
+
organization: dict = Depends(get_current_organization),
|
|
260
|
+
):
|
|
261
|
+
"""
|
|
262
|
+
Create a new team with full Agno capabilities.
|
|
263
|
+
|
|
264
|
+
Supports comprehensive configuration including:
|
|
265
|
+
- Member agents
|
|
266
|
+
- Instructions and reasoning
|
|
267
|
+
- Tools and knowledge bases
|
|
268
|
+
- LLM settings
|
|
269
|
+
- Session management
|
|
270
|
+
"""
|
|
271
|
+
try:
|
|
272
|
+
logger.info(
|
|
273
|
+
"create_team_request",
|
|
274
|
+
team_name=team_data.name,
|
|
275
|
+
org_id=organization["id"],
|
|
276
|
+
org_name=organization.get("name"),
|
|
277
|
+
member_count=len(team_data.configuration.member_ids) if team_data.configuration.member_ids else 0,
|
|
278
|
+
skill_count=len(team_data.skill_ids) if team_data.skill_ids else 0,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Check if team name already exists in this organization
|
|
282
|
+
existing_team = db.query(Team).filter(
|
|
283
|
+
Team.name == team_data.name,
|
|
284
|
+
Team.organization_id == organization["id"]
|
|
285
|
+
).first()
|
|
286
|
+
if existing_team:
|
|
287
|
+
logger.warning(
|
|
288
|
+
"team_name_already_exists",
|
|
289
|
+
team_name=team_data.name,
|
|
290
|
+
org_id=organization["id"],
|
|
291
|
+
)
|
|
292
|
+
raise HTTPException(status_code=400, detail="Team with this name already exists in your organization")
|
|
293
|
+
|
|
294
|
+
# Validate member_ids if provided
|
|
295
|
+
if team_data.configuration.member_ids:
|
|
296
|
+
client = get_supabase()
|
|
297
|
+
logger.info(
|
|
298
|
+
"validating_team_members",
|
|
299
|
+
member_ids=team_data.configuration.member_ids,
|
|
300
|
+
org_id=organization["id"],
|
|
301
|
+
)
|
|
302
|
+
for agent_id in team_data.configuration.member_ids:
|
|
303
|
+
try:
|
|
304
|
+
# Query Supabase instead of local DB for consistency with GET /agents
|
|
305
|
+
result = (
|
|
306
|
+
client.table("agents")
|
|
307
|
+
.select("id")
|
|
308
|
+
.eq("id", agent_id)
|
|
309
|
+
.eq("organization_id", organization["id"])
|
|
310
|
+
.maybe_single()
|
|
311
|
+
.execute()
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
logger.debug(
|
|
315
|
+
"agent_validation_result",
|
|
316
|
+
agent_id=agent_id,
|
|
317
|
+
result_type=type(result).__name__,
|
|
318
|
+
has_data=hasattr(result, 'data') if result else False,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if not result or not hasattr(result, 'data') or not result.data:
|
|
322
|
+
logger.warning(
|
|
323
|
+
"agent_not_found",
|
|
324
|
+
agent_id=agent_id,
|
|
325
|
+
org_id=organization["id"],
|
|
326
|
+
result_is_none=result is None,
|
|
327
|
+
)
|
|
328
|
+
raise HTTPException(
|
|
329
|
+
status_code=400,
|
|
330
|
+
detail=f"Agent with ID '{agent_id}' not found. Please create the agent first."
|
|
331
|
+
)
|
|
332
|
+
except HTTPException:
|
|
333
|
+
raise
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(
|
|
336
|
+
"agent_validation_failed",
|
|
337
|
+
agent_id=agent_id,
|
|
338
|
+
error=str(e),
|
|
339
|
+
error_type=type(e).__name__,
|
|
340
|
+
org_id=organization["id"],
|
|
341
|
+
)
|
|
342
|
+
raise HTTPException(
|
|
343
|
+
status_code=500,
|
|
344
|
+
detail=f"Failed to validate agent '{agent_id}': {str(e)}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Convert TeamConfiguration to dict for JSON storage
|
|
348
|
+
configuration_dict = team_data.configuration.model_dump(exclude_none=True)
|
|
349
|
+
|
|
350
|
+
team = Team(
|
|
351
|
+
organization_id=organization["id"],
|
|
352
|
+
name=team_data.name,
|
|
353
|
+
description=team_data.description,
|
|
354
|
+
configuration=configuration_dict,
|
|
355
|
+
skill_ids=team_data.skill_ids,
|
|
356
|
+
execution_environment=team_data.execution_environment.model_dump() if team_data.execution_environment else {},
|
|
357
|
+
)
|
|
358
|
+
db.add(team)
|
|
359
|
+
db.commit()
|
|
360
|
+
db.refresh(team)
|
|
361
|
+
|
|
362
|
+
logger.info(
|
|
363
|
+
"team_created",
|
|
364
|
+
team_id=str(team.id),
|
|
365
|
+
team_name=team.name,
|
|
366
|
+
org_id=organization["id"],
|
|
367
|
+
)
|
|
368
|
+
except HTTPException:
|
|
369
|
+
raise
|
|
370
|
+
except IntegrityError as e:
|
|
371
|
+
db.rollback()
|
|
372
|
+
logger.error(
|
|
373
|
+
"database_integrity_error",
|
|
374
|
+
error=str(e),
|
|
375
|
+
team_name=team_data.name,
|
|
376
|
+
org_id=organization["id"],
|
|
377
|
+
)
|
|
378
|
+
raise HTTPException(
|
|
379
|
+
status_code=400,
|
|
380
|
+
detail=f"Database constraint violation: {str(e.orig) if hasattr(e, 'orig') else str(e)}"
|
|
381
|
+
)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
db.rollback()
|
|
384
|
+
logger.error(
|
|
385
|
+
"team_creation_failed",
|
|
386
|
+
error=str(e),
|
|
387
|
+
error_type=type(e).__name__,
|
|
388
|
+
team_name=team_data.name,
|
|
389
|
+
org_id=organization["id"],
|
|
390
|
+
)
|
|
391
|
+
raise HTTPException(
|
|
392
|
+
status_code=500,
|
|
393
|
+
detail=f"Failed to create team: {str(e)}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Sync agent.team_id relationship for initial members in Supabase
|
|
397
|
+
if team_data.configuration.member_ids:
|
|
398
|
+
client = get_supabase()
|
|
399
|
+
for agent_id in team_data.configuration.member_ids:
|
|
400
|
+
try:
|
|
401
|
+
client.table("agents").update({"team_id": str(team.id)}).eq("id", agent_id).execute()
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.warning(
|
|
404
|
+
"failed_to_sync_agent_team_id",
|
|
405
|
+
error=str(e),
|
|
406
|
+
agent_id=agent_id,
|
|
407
|
+
team_id=str(team.id)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Automatically assign team to the default project
|
|
411
|
+
default_project_id = get_default_project_id(organization)
|
|
412
|
+
if default_project_id:
|
|
413
|
+
try:
|
|
414
|
+
client = get_supabase()
|
|
415
|
+
project_team_record = {
|
|
416
|
+
"id": str(uuid.uuid4()),
|
|
417
|
+
"project_id": default_project_id,
|
|
418
|
+
"team_id": str(team.id),
|
|
419
|
+
"role": None,
|
|
420
|
+
"added_at": datetime.utcnow().isoformat(),
|
|
421
|
+
"added_by": organization.get("user_id"),
|
|
422
|
+
}
|
|
423
|
+
client.table("project_teams").insert(project_team_record).execute()
|
|
424
|
+
logger.info(
|
|
425
|
+
"team_added_to_default_project",
|
|
426
|
+
team_id=str(team.id),
|
|
427
|
+
project_id=default_project_id,
|
|
428
|
+
org_id=organization["id"]
|
|
429
|
+
)
|
|
430
|
+
except Exception as e:
|
|
431
|
+
logger.warning(
|
|
432
|
+
"failed_to_add_team_to_default_project",
|
|
433
|
+
error=str(e),
|
|
434
|
+
team_id=str(team.id),
|
|
435
|
+
org_id=organization["id"]
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Create skill associations if skills were provided
|
|
439
|
+
if team_data.skill_ids:
|
|
440
|
+
try:
|
|
441
|
+
client = get_supabase()
|
|
442
|
+
now = datetime.utcnow().isoformat()
|
|
443
|
+
|
|
444
|
+
for skill_id in team_data.skill_ids:
|
|
445
|
+
association_id = str(uuid.uuid4())
|
|
446
|
+
config_override = team_data.skill_configurations.get(skill_id, {})
|
|
447
|
+
|
|
448
|
+
association_record = {
|
|
449
|
+
"id": association_id,
|
|
450
|
+
"organization_id": organization["id"],
|
|
451
|
+
"skill_id": skill_id,
|
|
452
|
+
"entity_type": "team",
|
|
453
|
+
"entity_id": str(team.id),
|
|
454
|
+
"configuration_override": config_override,
|
|
455
|
+
"created_at": now,
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
client.table("skill_associations").insert(association_record).execute()
|
|
459
|
+
|
|
460
|
+
logger.info(
|
|
461
|
+
"team_skills_associated",
|
|
462
|
+
team_id=str(team.id),
|
|
463
|
+
skill_count=len(team_data.skill_ids),
|
|
464
|
+
org_id=organization["id"]
|
|
465
|
+
)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.warning(
|
|
468
|
+
"failed_to_associate_team_skills",
|
|
469
|
+
error=str(e),
|
|
470
|
+
team_id=str(team.id),
|
|
471
|
+
org_id=organization["id"]
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Parse configuration back to TeamConfiguration for response
|
|
475
|
+
response_team = TeamResponse(
|
|
476
|
+
id=str(team.id),
|
|
477
|
+
organization_id=team.organization_id,
|
|
478
|
+
name=team.name,
|
|
479
|
+
description=team.description,
|
|
480
|
+
status=team.status,
|
|
481
|
+
configuration=TeamConfiguration(**team.configuration),
|
|
482
|
+
created_at=team.created_at,
|
|
483
|
+
updated_at=team.updated_at,
|
|
484
|
+
projects=get_team_projects(db, str(team.id)),
|
|
485
|
+
)
|
|
486
|
+
return response_team
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@router.get("", response_model=List[TeamWithAgentsResponse])
|
|
490
|
+
def list_teams(
|
|
491
|
+
skip: int = 0,
|
|
492
|
+
limit: int = 100,
|
|
493
|
+
status_filter: Optional[TeamStatus] = None,
|
|
494
|
+
db: Session = Depends(get_db),
|
|
495
|
+
organization: dict = Depends(get_current_organization),
|
|
496
|
+
):
|
|
497
|
+
"""
|
|
498
|
+
List all teams with their configurations and member agents.
|
|
499
|
+
|
|
500
|
+
Supports filtering by status and pagination.
|
|
501
|
+
Only returns teams belonging to the current organization.
|
|
502
|
+
"""
|
|
503
|
+
try:
|
|
504
|
+
query = db.query(Team).filter(Team.organization_id == organization["id"])
|
|
505
|
+
if status_filter:
|
|
506
|
+
query = query.filter(Team.status == status_filter)
|
|
507
|
+
teams = query.offset(skip).limit(limit).all()
|
|
508
|
+
|
|
509
|
+
if not teams:
|
|
510
|
+
return []
|
|
511
|
+
|
|
512
|
+
client = get_supabase()
|
|
513
|
+
team_ids = [str(team.id) for team in teams]
|
|
514
|
+
|
|
515
|
+
# BATCH 1: Fetch all projects for all teams in one query
|
|
516
|
+
try:
|
|
517
|
+
projects_result = (
|
|
518
|
+
client.table("project_teams")
|
|
519
|
+
.select("team_id, projects(id, name, key, description)")
|
|
520
|
+
.in_("team_id", team_ids)
|
|
521
|
+
.execute()
|
|
522
|
+
)
|
|
523
|
+
except Exception as project_error:
|
|
524
|
+
logger.error("failed_to_fetch_projects", error=str(project_error), org_id=organization["id"])
|
|
525
|
+
projects_result = type('obj', (object,), {'data': []}) # Empty result object
|
|
526
|
+
|
|
527
|
+
# Group projects by team_id
|
|
528
|
+
projects_by_team = {}
|
|
529
|
+
for item in projects_result.data or []:
|
|
530
|
+
team_id = item["team_id"]
|
|
531
|
+
project_data = item.get("projects")
|
|
532
|
+
if project_data:
|
|
533
|
+
if team_id not in projects_by_team:
|
|
534
|
+
projects_by_team[team_id] = []
|
|
535
|
+
projects_by_team[team_id].append({
|
|
536
|
+
"id": project_data["id"],
|
|
537
|
+
"name": project_data["name"],
|
|
538
|
+
"key": project_data["key"],
|
|
539
|
+
"description": project_data.get("description"),
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
# BATCH 2: Fetch all skill associations for all teams in one query
|
|
543
|
+
try:
|
|
544
|
+
skills_result = (
|
|
545
|
+
client.table("skill_associations")
|
|
546
|
+
.select("entity_id, skill_id, configuration_override, skills(*)")
|
|
547
|
+
.eq("organization_id", organization["id"])
|
|
548
|
+
.eq("entity_type", "team")
|
|
549
|
+
.in_("entity_id", team_ids)
|
|
550
|
+
.execute()
|
|
551
|
+
)
|
|
552
|
+
except Exception as skill_error:
|
|
553
|
+
logger.error("failed_to_fetch_skills", error=str(skill_error), org_id=organization["id"])
|
|
554
|
+
skills_result = type('obj', (object,), {'data': []}) # Empty result object
|
|
555
|
+
|
|
556
|
+
# Group skills by team_id
|
|
557
|
+
skills_by_team = {}
|
|
558
|
+
for item in skills_result.data or []:
|
|
559
|
+
team_id = item["entity_id"]
|
|
560
|
+
skill_data = item.get("skills")
|
|
561
|
+
if skill_data and skill_data.get("enabled", True):
|
|
562
|
+
if team_id not in skills_by_team:
|
|
563
|
+
skills_by_team[team_id] = []
|
|
564
|
+
|
|
565
|
+
# Merge configuration with override
|
|
566
|
+
config = skill_data.get("configuration", {})
|
|
567
|
+
override = item.get("configuration_override")
|
|
568
|
+
if override:
|
|
569
|
+
config = {**config, **override}
|
|
570
|
+
|
|
571
|
+
skills_by_team[team_id].append({
|
|
572
|
+
"id": skill_data["id"],
|
|
573
|
+
"name": skill_data["name"],
|
|
574
|
+
"type": skill_data["skill_type"],
|
|
575
|
+
"description": skill_data.get("description"),
|
|
576
|
+
"enabled": skill_data.get("enabled", True),
|
|
577
|
+
"configuration": config,
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
# BATCH 3: Collect all unique agent IDs from all teams
|
|
581
|
+
all_agent_ids = set()
|
|
582
|
+
for team in teams:
|
|
583
|
+
try:
|
|
584
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
585
|
+
if team_config.member_ids:
|
|
586
|
+
all_agent_ids.update(team_config.member_ids)
|
|
587
|
+
except Exception as config_error:
|
|
588
|
+
logger.warning("failed_to_parse_team_config", error=str(config_error), team_id=str(team.id))
|
|
589
|
+
|
|
590
|
+
# Fetch all agents in one query
|
|
591
|
+
agents_by_id = {}
|
|
592
|
+
if all_agent_ids:
|
|
593
|
+
try:
|
|
594
|
+
db_agents = db.query(Agent).filter(Agent.id.in_(list(all_agent_ids))).all()
|
|
595
|
+
agents_by_id = {
|
|
596
|
+
str(agent.id): {
|
|
597
|
+
"id": str(agent.id),
|
|
598
|
+
"name": agent.name,
|
|
599
|
+
"status": agent.status,
|
|
600
|
+
"capabilities": agent.capabilities,
|
|
601
|
+
"description": agent.description,
|
|
602
|
+
}
|
|
603
|
+
for agent in db_agents
|
|
604
|
+
}
|
|
605
|
+
except Exception as agent_error:
|
|
606
|
+
logger.error("failed_to_fetch_agents", error=str(agent_error), org_id=organization["id"])
|
|
607
|
+
|
|
608
|
+
# Build response for each team
|
|
609
|
+
result = []
|
|
610
|
+
for team in teams:
|
|
611
|
+
try:
|
|
612
|
+
team_id = str(team.id)
|
|
613
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
614
|
+
|
|
615
|
+
# Get agents for this team from the batched data
|
|
616
|
+
agents = []
|
|
617
|
+
if team_config.member_ids:
|
|
618
|
+
agents = [agents_by_id[agent_id] for agent_id in team_config.member_ids if agent_id in agents_by_id]
|
|
619
|
+
|
|
620
|
+
# Get skills from batched data
|
|
621
|
+
skills = skills_by_team.get(team_id, [])
|
|
622
|
+
skill_ids = [ts["id"] for ts in skills]
|
|
623
|
+
|
|
624
|
+
result.append(TeamWithAgentsResponse(
|
|
625
|
+
id=team_id,
|
|
626
|
+
organization_id=team.organization_id,
|
|
627
|
+
name=team.name,
|
|
628
|
+
description=team.description,
|
|
629
|
+
status=team.status,
|
|
630
|
+
configuration=team_config,
|
|
631
|
+
created_at=team.created_at,
|
|
632
|
+
updated_at=team.updated_at,
|
|
633
|
+
projects=projects_by_team.get(team_id, []),
|
|
634
|
+
agents=agents,
|
|
635
|
+
skill_ids=skill_ids,
|
|
636
|
+
skills=skills,
|
|
637
|
+
))
|
|
638
|
+
except Exception as team_error:
|
|
639
|
+
logger.error("failed_to_build_team_response", error=str(team_error), team_id=str(team.id))
|
|
640
|
+
# Skip this team and continue with others
|
|
641
|
+
|
|
642
|
+
logger.info(
|
|
643
|
+
"teams_listed_successfully",
|
|
644
|
+
count=len(result),
|
|
645
|
+
org_id=organization["id"],
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
return result
|
|
649
|
+
|
|
650
|
+
except Exception as e:
|
|
651
|
+
logger.error(
|
|
652
|
+
"teams_list_failed",
|
|
653
|
+
error=str(e),
|
|
654
|
+
error_type=type(e).__name__,
|
|
655
|
+
org_id=organization["id"]
|
|
656
|
+
)
|
|
657
|
+
raise HTTPException(
|
|
658
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
659
|
+
detail=f"Failed to list teams: {str(e)}"
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@router.get("/{team_id}", response_model=TeamWithAgentsResponse)
|
|
664
|
+
def get_team(
|
|
665
|
+
team_id: str,
|
|
666
|
+
db: Session = Depends(get_db),
|
|
667
|
+
organization: dict = Depends(get_current_organization),
|
|
668
|
+
):
|
|
669
|
+
"""
|
|
670
|
+
Get a specific team by ID with full configuration and member agents.
|
|
671
|
+
|
|
672
|
+
Returns the team with structured configuration and list of member agents.
|
|
673
|
+
Only returns teams belonging to the current organization.
|
|
674
|
+
"""
|
|
675
|
+
team = db.query(Team).filter(
|
|
676
|
+
Team.id == team_id,
|
|
677
|
+
Team.organization_id == organization["id"]
|
|
678
|
+
).first()
|
|
679
|
+
if not team:
|
|
680
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
681
|
+
|
|
682
|
+
# Parse configuration
|
|
683
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
684
|
+
|
|
685
|
+
# Get agents from configuration.member_ids (source of truth)
|
|
686
|
+
# instead of team.agents relationship to avoid ghost agents
|
|
687
|
+
member_ids = team_config.member_ids
|
|
688
|
+
agents = []
|
|
689
|
+
if member_ids:
|
|
690
|
+
# Query agents that actually exist in the database
|
|
691
|
+
db_agents = db.query(Agent).filter(Agent.id.in_(member_ids)).all()
|
|
692
|
+
agents = [
|
|
693
|
+
{
|
|
694
|
+
"id": str(agent.id),
|
|
695
|
+
"name": agent.name,
|
|
696
|
+
"status": agent.status,
|
|
697
|
+
"capabilities": agent.capabilities,
|
|
698
|
+
"description": agent.description,
|
|
699
|
+
}
|
|
700
|
+
for agent in db_agents
|
|
701
|
+
]
|
|
702
|
+
|
|
703
|
+
# Get skills for this team
|
|
704
|
+
client = get_supabase()
|
|
705
|
+
skills = get_entity_skills(client, organization["id"], "team", team_id)
|
|
706
|
+
skill_ids = [ts["id"] for ts in skills]
|
|
707
|
+
|
|
708
|
+
# Include agents in response
|
|
709
|
+
return TeamWithAgentsResponse(
|
|
710
|
+
id=str(team.id),
|
|
711
|
+
organization_id=team.organization_id,
|
|
712
|
+
name=team.name,
|
|
713
|
+
description=team.description,
|
|
714
|
+
status=team.status,
|
|
715
|
+
configuration=team_config,
|
|
716
|
+
created_at=team.created_at,
|
|
717
|
+
updated_at=team.updated_at,
|
|
718
|
+
projects=get_team_projects(db, team_id),
|
|
719
|
+
agents=agents,
|
|
720
|
+
skill_ids=skill_ids,
|
|
721
|
+
skills=skills,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
@router.patch("/{team_id}", response_model=TeamResponse)
|
|
726
|
+
def update_team(
|
|
727
|
+
team_id: str,
|
|
728
|
+
team_data: TeamUpdate,
|
|
729
|
+
db: Session = Depends(get_db),
|
|
730
|
+
organization: dict = Depends(get_current_organization),
|
|
731
|
+
):
|
|
732
|
+
"""
|
|
733
|
+
Update a team's configuration, name, description, or status.
|
|
734
|
+
|
|
735
|
+
Supports partial updates - only provided fields are updated.
|
|
736
|
+
Validates member_ids if configuration is being updated.
|
|
737
|
+
Only allows updating teams belonging to the current organization.
|
|
738
|
+
"""
|
|
739
|
+
team = db.query(Team).filter(
|
|
740
|
+
Team.id == team_id,
|
|
741
|
+
Team.organization_id == organization["id"]
|
|
742
|
+
).first()
|
|
743
|
+
if not team:
|
|
744
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
745
|
+
|
|
746
|
+
update_data = team_data.model_dump(exclude_unset=True)
|
|
747
|
+
|
|
748
|
+
# Extract skill data before processing
|
|
749
|
+
skill_ids = update_data.pop("skill_ids", None)
|
|
750
|
+
skill_configurations = update_data.pop("skill_configurations", None)
|
|
751
|
+
|
|
752
|
+
# Extract environment data before processing (many-to-many via junction table)
|
|
753
|
+
environment_ids = update_data.pop("environment_ids", None)
|
|
754
|
+
|
|
755
|
+
# Handle execution_environment - convert to dict if present
|
|
756
|
+
if "execution_environment" in update_data and update_data["execution_environment"]:
|
|
757
|
+
if isinstance(update_data["execution_environment"], ExecutionEnvironment):
|
|
758
|
+
update_data["execution_environment"] = update_data["execution_environment"].model_dump()
|
|
759
|
+
# If None, keep as None to preserve existing value
|
|
760
|
+
|
|
761
|
+
logger.info(
|
|
762
|
+
"team_update_request",
|
|
763
|
+
team_id=team_id,
|
|
764
|
+
has_skill_ids=skill_ids is not None,
|
|
765
|
+
skill_count=len(skill_ids) if skill_ids else 0,
|
|
766
|
+
skill_ids=skill_ids,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Check if name is being updated and if it already exists
|
|
770
|
+
if "name" in update_data and update_data["name"] != team.name:
|
|
771
|
+
existing_team = db.query(Team).filter(Team.name == update_data["name"]).first()
|
|
772
|
+
if existing_team:
|
|
773
|
+
raise HTTPException(status_code=400, detail="Team with this name already exists")
|
|
774
|
+
|
|
775
|
+
# Handle configuration update specially
|
|
776
|
+
if "configuration" in update_data and update_data["configuration"]:
|
|
777
|
+
new_config = update_data["configuration"]
|
|
778
|
+
|
|
779
|
+
# new_config is already a dict from model_dump(exclude_unset=True)
|
|
780
|
+
# Validate member_ids if provided and sync the agent.team_id relationship
|
|
781
|
+
if isinstance(new_config, dict) and 'member_ids' in new_config:
|
|
782
|
+
new_member_ids = set(new_config.get('member_ids', []))
|
|
783
|
+
client = get_supabase()
|
|
784
|
+
|
|
785
|
+
# Validate all agent IDs exist in Supabase
|
|
786
|
+
for agent_id in new_member_ids:
|
|
787
|
+
result = (
|
|
788
|
+
client.table("agents")
|
|
789
|
+
.select("id")
|
|
790
|
+
.eq("id", agent_id)
|
|
791
|
+
.eq("organization_id", organization["id"])
|
|
792
|
+
.maybe_single()
|
|
793
|
+
.execute()
|
|
794
|
+
)
|
|
795
|
+
if not result.data:
|
|
796
|
+
raise HTTPException(
|
|
797
|
+
status_code=400,
|
|
798
|
+
detail=f"Agent with ID '{agent_id}' not found. Please create the agent first."
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# Sync the agent.team_id relationship in Supabase
|
|
802
|
+
# Get current team members from configuration
|
|
803
|
+
current_config = TeamConfiguration(**(team.configuration or {}))
|
|
804
|
+
current_member_ids = set(current_config.member_ids or [])
|
|
805
|
+
|
|
806
|
+
# Remove agents that are no longer in the team
|
|
807
|
+
agents_to_remove = current_member_ids - new_member_ids
|
|
808
|
+
for agent_id in agents_to_remove:
|
|
809
|
+
try:
|
|
810
|
+
client.table("agents").update({"team_id": None}).eq("id", agent_id).execute()
|
|
811
|
+
except Exception as e:
|
|
812
|
+
logger.warning("failed_to_remove_agent_from_team", error=str(e), agent_id=agent_id)
|
|
813
|
+
|
|
814
|
+
# Add agents that are newly added to the team
|
|
815
|
+
agents_to_add = new_member_ids - current_member_ids
|
|
816
|
+
for agent_id in agents_to_add:
|
|
817
|
+
try:
|
|
818
|
+
client.table("agents").update({"team_id": team_id}).eq("id", agent_id).execute()
|
|
819
|
+
except Exception as e:
|
|
820
|
+
logger.warning("failed_to_add_agent_to_team", error=str(e), agent_id=agent_id)
|
|
821
|
+
|
|
822
|
+
# new_config is already a dict, just assign it
|
|
823
|
+
team.configuration = new_config
|
|
824
|
+
del update_data["configuration"]
|
|
825
|
+
|
|
826
|
+
# Update other fields
|
|
827
|
+
for field, value in update_data.items():
|
|
828
|
+
if hasattr(team, field):
|
|
829
|
+
setattr(team, field, value)
|
|
830
|
+
|
|
831
|
+
# Update skill_ids if provided
|
|
832
|
+
if skill_ids is not None:
|
|
833
|
+
team.skill_ids = skill_ids
|
|
834
|
+
|
|
835
|
+
team.updated_at = datetime.utcnow()
|
|
836
|
+
db.commit()
|
|
837
|
+
db.refresh(team)
|
|
838
|
+
|
|
839
|
+
# Update skill associations if skill_ids was provided
|
|
840
|
+
if skill_ids is not None:
|
|
841
|
+
try:
|
|
842
|
+
client = get_supabase()
|
|
843
|
+
|
|
844
|
+
# Delete existing associations
|
|
845
|
+
client.table("skill_associations").delete().eq("entity_type", "team").eq("entity_id", team_id).execute()
|
|
846
|
+
|
|
847
|
+
# Create new associations
|
|
848
|
+
now = datetime.utcnow().isoformat()
|
|
849
|
+
for skill_id in skill_ids:
|
|
850
|
+
association_id = str(uuid.uuid4())
|
|
851
|
+
config_override = (skill_configurations or {}).get(skill_id, {})
|
|
852
|
+
|
|
853
|
+
association_record = {
|
|
854
|
+
"id": association_id,
|
|
855
|
+
"organization_id": organization["id"],
|
|
856
|
+
"skill_id": skill_id,
|
|
857
|
+
"entity_type": "team",
|
|
858
|
+
"entity_id": team_id,
|
|
859
|
+
"configuration_override": config_override,
|
|
860
|
+
"created_at": now,
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
client.table("skill_associations").insert(association_record).execute()
|
|
864
|
+
|
|
865
|
+
logger.info(
|
|
866
|
+
"team_skills_updated",
|
|
867
|
+
team_id=team_id,
|
|
868
|
+
skill_count=len(skill_ids),
|
|
869
|
+
org_id=organization["id"]
|
|
870
|
+
)
|
|
871
|
+
except Exception as e:
|
|
872
|
+
logger.warning(
|
|
873
|
+
"failed_to_update_team_skills",
|
|
874
|
+
error=str(e),
|
|
875
|
+
team_id=team_id,
|
|
876
|
+
org_id=organization["id"]
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
# Update environment associations if environment_ids was provided
|
|
880
|
+
if environment_ids is not None:
|
|
881
|
+
try:
|
|
882
|
+
client = get_supabase()
|
|
883
|
+
|
|
884
|
+
# Delete existing environment associations
|
|
885
|
+
client.table("team_environments").delete().eq("team_id", team_id).execute()
|
|
886
|
+
|
|
887
|
+
# Create new environment associations
|
|
888
|
+
for environment_id in environment_ids:
|
|
889
|
+
env_association_record = {
|
|
890
|
+
"id": str(uuid.uuid4()),
|
|
891
|
+
"team_id": team_id,
|
|
892
|
+
"environment_id": environment_id,
|
|
893
|
+
"organization_id": organization["id"],
|
|
894
|
+
"assigned_at": datetime.utcnow().isoformat(),
|
|
895
|
+
}
|
|
896
|
+
client.table("team_environments").insert(env_association_record).execute()
|
|
897
|
+
|
|
898
|
+
logger.info(
|
|
899
|
+
"team_environments_updated",
|
|
900
|
+
team_id=team_id,
|
|
901
|
+
environment_count=len(environment_ids),
|
|
902
|
+
org_id=organization["id"]
|
|
903
|
+
)
|
|
904
|
+
except Exception as e:
|
|
905
|
+
logger.warning(
|
|
906
|
+
"failed_to_update_team_environments",
|
|
907
|
+
error=str(e),
|
|
908
|
+
team_id=team_id,
|
|
909
|
+
org_id=organization["id"]
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# Return with parsed configuration
|
|
913
|
+
return TeamResponse(
|
|
914
|
+
id=str(team.id),
|
|
915
|
+
organization_id=team.organization_id,
|
|
916
|
+
name=team.name,
|
|
917
|
+
description=team.description,
|
|
918
|
+
status=team.status,
|
|
919
|
+
configuration=TeamConfiguration(**(team.configuration or {})),
|
|
920
|
+
created_at=team.created_at,
|
|
921
|
+
updated_at=team.updated_at,
|
|
922
|
+
projects=get_team_projects(db, team_id),
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@router.delete("/{team_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
927
|
+
def delete_team(
|
|
928
|
+
team_id: str,
|
|
929
|
+
db: Session = Depends(get_db),
|
|
930
|
+
organization: dict = Depends(get_current_organization),
|
|
931
|
+
):
|
|
932
|
+
"""Delete a team - only if it belongs to the current organization"""
|
|
933
|
+
team = db.query(Team).filter(
|
|
934
|
+
Team.id == team_id,
|
|
935
|
+
Team.organization_id == organization["id"]
|
|
936
|
+
).first()
|
|
937
|
+
if not team:
|
|
938
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
939
|
+
|
|
940
|
+
db.delete(team)
|
|
941
|
+
db.commit()
|
|
942
|
+
return None
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
@router.post("/{team_id}/agents/{agent_id}", response_model=TeamWithAgentsResponse)
|
|
946
|
+
def add_agent_to_team(
|
|
947
|
+
team_id: str,
|
|
948
|
+
agent_id: str,
|
|
949
|
+
db: Session = Depends(get_db),
|
|
950
|
+
organization: dict = Depends(get_current_organization),
|
|
951
|
+
):
|
|
952
|
+
"""
|
|
953
|
+
Add an agent to a team.
|
|
954
|
+
|
|
955
|
+
This sets the agent's team_id foreign key. You can also manage members
|
|
956
|
+
through the team's configuration.member_ids field.
|
|
957
|
+
Only allows adding agents to teams belonging to the current organization.
|
|
958
|
+
"""
|
|
959
|
+
team = db.query(Team).filter(
|
|
960
|
+
Team.id == team_id,
|
|
961
|
+
Team.organization_id == organization["id"]
|
|
962
|
+
).first()
|
|
963
|
+
if not team:
|
|
964
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
965
|
+
|
|
966
|
+
agent = db.query(Agent).filter(
|
|
967
|
+
Agent.id == agent_id,
|
|
968
|
+
Agent.organization_id == organization["id"]
|
|
969
|
+
).first()
|
|
970
|
+
if not agent:
|
|
971
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
972
|
+
|
|
973
|
+
agent.team_id = team_id
|
|
974
|
+
db.commit()
|
|
975
|
+
db.refresh(team)
|
|
976
|
+
|
|
977
|
+
# Parse configuration
|
|
978
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
979
|
+
|
|
980
|
+
# Get agents from configuration.member_ids (source of truth)
|
|
981
|
+
member_ids = team_config.member_ids
|
|
982
|
+
agents = []
|
|
983
|
+
if member_ids:
|
|
984
|
+
db_agents = db.query(Agent).filter(Agent.id.in_(member_ids)).all()
|
|
985
|
+
agents = [
|
|
986
|
+
{
|
|
987
|
+
"id": str(a.id),
|
|
988
|
+
"name": a.name,
|
|
989
|
+
"status": a.status,
|
|
990
|
+
"capabilities": a.capabilities,
|
|
991
|
+
"description": a.description,
|
|
992
|
+
}
|
|
993
|
+
for a in db_agents
|
|
994
|
+
]
|
|
995
|
+
|
|
996
|
+
# Return team with agents
|
|
997
|
+
return TeamWithAgentsResponse(
|
|
998
|
+
id=str(team.id),
|
|
999
|
+
organization_id=team.organization_id,
|
|
1000
|
+
name=team.name,
|
|
1001
|
+
description=team.description,
|
|
1002
|
+
status=team.status,
|
|
1003
|
+
configuration=team_config,
|
|
1004
|
+
created_at=team.created_at,
|
|
1005
|
+
updated_at=team.updated_at,
|
|
1006
|
+
agents=agents,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@router.delete("/{team_id}/agents/{agent_id}", response_model=TeamWithAgentsResponse)
|
|
1011
|
+
def remove_agent_from_team(
|
|
1012
|
+
team_id: str,
|
|
1013
|
+
agent_id: str,
|
|
1014
|
+
db: Session = Depends(get_db),
|
|
1015
|
+
organization: dict = Depends(get_current_organization),
|
|
1016
|
+
):
|
|
1017
|
+
"""
|
|
1018
|
+
Remove an agent from a team.
|
|
1019
|
+
|
|
1020
|
+
This clears the agent's team_id foreign key.
|
|
1021
|
+
Only allows removing agents from teams belonging to the current organization.
|
|
1022
|
+
"""
|
|
1023
|
+
team = db.query(Team).filter(
|
|
1024
|
+
Team.id == team_id,
|
|
1025
|
+
Team.organization_id == organization["id"]
|
|
1026
|
+
).first()
|
|
1027
|
+
if not team:
|
|
1028
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
1029
|
+
|
|
1030
|
+
agent = db.query(Agent).filter(
|
|
1031
|
+
Agent.id == agent_id,
|
|
1032
|
+
Agent.team_id == team_id,
|
|
1033
|
+
Agent.organization_id == organization["id"]
|
|
1034
|
+
).first()
|
|
1035
|
+
if not agent:
|
|
1036
|
+
raise HTTPException(status_code=404, detail="Agent not found in this team")
|
|
1037
|
+
|
|
1038
|
+
agent.team_id = None
|
|
1039
|
+
db.commit()
|
|
1040
|
+
db.refresh(team)
|
|
1041
|
+
|
|
1042
|
+
# Parse configuration
|
|
1043
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
1044
|
+
|
|
1045
|
+
# Get agents from configuration.member_ids (source of truth)
|
|
1046
|
+
member_ids = team_config.member_ids
|
|
1047
|
+
agents = []
|
|
1048
|
+
if member_ids:
|
|
1049
|
+
db_agents = db.query(Agent).filter(Agent.id.in_(member_ids)).all()
|
|
1050
|
+
agents = [
|
|
1051
|
+
{
|
|
1052
|
+
"id": str(a.id),
|
|
1053
|
+
"name": a.name,
|
|
1054
|
+
"status": a.status,
|
|
1055
|
+
"capabilities": a.capabilities,
|
|
1056
|
+
"description": a.description,
|
|
1057
|
+
}
|
|
1058
|
+
for a in db_agents
|
|
1059
|
+
]
|
|
1060
|
+
|
|
1061
|
+
# Return team with agents
|
|
1062
|
+
return TeamWithAgentsResponse(
|
|
1063
|
+
id=str(team.id),
|
|
1064
|
+
organization_id=team.organization_id,
|
|
1065
|
+
name=team.name,
|
|
1066
|
+
description=team.description,
|
|
1067
|
+
status=team.status,
|
|
1068
|
+
configuration=team_config,
|
|
1069
|
+
created_at=team.created_at,
|
|
1070
|
+
updated_at=team.updated_at,
|
|
1071
|
+
agents=agents,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
@router.post("/{team_id}/execute", response_model=TeamExecutionResponse)
|
|
1076
|
+
async def execute_team(
|
|
1077
|
+
team_id: str,
|
|
1078
|
+
execution_request: TeamExecutionRequest,
|
|
1079
|
+
request: Request,
|
|
1080
|
+
db: Session = Depends(get_db),
|
|
1081
|
+
organization: dict = Depends(get_current_organization),
|
|
1082
|
+
):
|
|
1083
|
+
"""
|
|
1084
|
+
Execute a team task by submitting to Temporal workflow.
|
|
1085
|
+
|
|
1086
|
+
This creates an execution record and starts a Temporal workflow.
|
|
1087
|
+
The actual execution happens asynchronously on the Temporal worker.
|
|
1088
|
+
|
|
1089
|
+
The runner_name should come from the Composer UI where user selects
|
|
1090
|
+
from available runners (fetched from Kubiya API /api/v1/runners).
|
|
1091
|
+
"""
|
|
1092
|
+
try:
|
|
1093
|
+
# Get team details from local DB
|
|
1094
|
+
team = db.query(Team).filter(
|
|
1095
|
+
Team.id == team_id,
|
|
1096
|
+
Team.organization_id == organization["id"]
|
|
1097
|
+
).first()
|
|
1098
|
+
|
|
1099
|
+
if not team:
|
|
1100
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
1101
|
+
|
|
1102
|
+
# Parse team configuration
|
|
1103
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
1104
|
+
|
|
1105
|
+
# Validate and get worker queue
|
|
1106
|
+
worker_queue_id = execution_request.worker_queue_id
|
|
1107
|
+
|
|
1108
|
+
client = get_supabase()
|
|
1109
|
+
queue_result = (
|
|
1110
|
+
client.table("worker_queues")
|
|
1111
|
+
.select("*")
|
|
1112
|
+
.eq("id", worker_queue_id)
|
|
1113
|
+
.eq("organization_id", organization["id"])
|
|
1114
|
+
.maybe_single()
|
|
1115
|
+
.execute()
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
if not queue_result.data:
|
|
1119
|
+
raise HTTPException(
|
|
1120
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
1121
|
+
detail=f"Worker queue '{worker_queue_id}' not found. Please select a valid worker queue."
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
worker_queue = queue_result.data
|
|
1125
|
+
|
|
1126
|
+
# Check if queue has active workers
|
|
1127
|
+
if worker_queue.get("status") != "active":
|
|
1128
|
+
raise HTTPException(
|
|
1129
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
1130
|
+
detail=f"Worker queue '{worker_queue.get('name')}' is not active"
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# Extract user metadata - ALWAYS use JWT-decoded organization data as source of truth
|
|
1134
|
+
user_metadata = execution_request.user_metadata or {}
|
|
1135
|
+
# Override with JWT data (user can't spoof their identity)
|
|
1136
|
+
user_metadata["user_id"] = organization.get("user_id")
|
|
1137
|
+
user_metadata["user_email"] = organization.get("user_email")
|
|
1138
|
+
user_metadata["user_name"] = organization.get("user_name")
|
|
1139
|
+
# Keep user_avatar from request if provided (not in JWT)
|
|
1140
|
+
if not user_metadata.get("user_avatar"):
|
|
1141
|
+
user_metadata["user_avatar"] = None
|
|
1142
|
+
|
|
1143
|
+
logger.info(
|
|
1144
|
+
"execution_user_metadata",
|
|
1145
|
+
user_id=user_metadata.get("user_id"),
|
|
1146
|
+
user_name=user_metadata.get("user_name"),
|
|
1147
|
+
user_email=user_metadata.get("user_email"),
|
|
1148
|
+
org_id=organization.get("id"),
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
# Create execution record in Supabase
|
|
1152
|
+
execution_id = str(uuid.uuid4())
|
|
1153
|
+
now = datetime.utcnow().isoformat()
|
|
1154
|
+
|
|
1155
|
+
execution_record = {
|
|
1156
|
+
"id": execution_id,
|
|
1157
|
+
"organization_id": organization["id"],
|
|
1158
|
+
"execution_type": "TEAM",
|
|
1159
|
+
"entity_id": team_id,
|
|
1160
|
+
"entity_name": team.name,
|
|
1161
|
+
"prompt": execution_request.prompt,
|
|
1162
|
+
"system_prompt": execution_request.system_prompt,
|
|
1163
|
+
"status": "PENDING",
|
|
1164
|
+
"worker_queue_id": worker_queue_id,
|
|
1165
|
+
"runner_name": worker_queue.get("name"), # Store queue name for display
|
|
1166
|
+
"user_id": user_metadata.get("user_id"),
|
|
1167
|
+
"user_name": user_metadata.get("user_name"),
|
|
1168
|
+
"user_email": user_metadata.get("user_email"),
|
|
1169
|
+
"user_avatar": user_metadata.get("user_avatar"),
|
|
1170
|
+
"usage": {},
|
|
1171
|
+
"execution_metadata": {
|
|
1172
|
+
"kubiya_org_id": organization["id"],
|
|
1173
|
+
"kubiya_org_name": organization["name"],
|
|
1174
|
+
"worker_queue_name": worker_queue.get("display_name") or worker_queue.get("name"),
|
|
1175
|
+
"team_execution": True,
|
|
1176
|
+
},
|
|
1177
|
+
"created_at": now,
|
|
1178
|
+
"updated_at": now,
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
exec_result = client.table("executions").insert(execution_record).execute()
|
|
1182
|
+
|
|
1183
|
+
if not exec_result.data:
|
|
1184
|
+
raise HTTPException(
|
|
1185
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1186
|
+
detail="Failed to create execution record"
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
# Add creator as the first participant (owner role) for multiplayer support
|
|
1190
|
+
user_id = user_metadata.get("user_id")
|
|
1191
|
+
if user_id:
|
|
1192
|
+
try:
|
|
1193
|
+
import uuid as uuid_lib
|
|
1194
|
+
client.table("execution_participants").insert({
|
|
1195
|
+
"id": str(uuid_lib.uuid4()),
|
|
1196
|
+
"execution_id": execution_id,
|
|
1197
|
+
"organization_id": organization["id"],
|
|
1198
|
+
"user_id": user_id,
|
|
1199
|
+
"user_name": user_metadata.get("user_name"),
|
|
1200
|
+
"user_email": user_metadata.get("user_email"),
|
|
1201
|
+
"user_avatar": user_metadata.get("user_avatar"),
|
|
1202
|
+
"role": "owner",
|
|
1203
|
+
}).execute()
|
|
1204
|
+
logger.info(
|
|
1205
|
+
"owner_participant_added",
|
|
1206
|
+
execution_id=execution_id,
|
|
1207
|
+
user_id=user_id,
|
|
1208
|
+
)
|
|
1209
|
+
except Exception as participant_error:
|
|
1210
|
+
logger.warning(
|
|
1211
|
+
"failed_to_add_owner_participant",
|
|
1212
|
+
error=str(participant_error),
|
|
1213
|
+
execution_id=execution_id,
|
|
1214
|
+
)
|
|
1215
|
+
# Don't fail execution creation if participant tracking fails
|
|
1216
|
+
|
|
1217
|
+
# Extract MCP servers from team configuration if available
|
|
1218
|
+
mcp_servers = team_config.metadata.get("mcpServers", {}) if team_config.metadata else {}
|
|
1219
|
+
|
|
1220
|
+
# Use LLM config from team configuration if available
|
|
1221
|
+
model_id = team_config.llm.model if team_config.llm and team_config.llm.model else "kubiya/claude-sonnet-4"
|
|
1222
|
+
|
|
1223
|
+
# Build model config from LLM configuration
|
|
1224
|
+
model_config = {}
|
|
1225
|
+
if team_config.llm:
|
|
1226
|
+
if team_config.llm.temperature is not None:
|
|
1227
|
+
model_config["temperature"] = team_config.llm.temperature
|
|
1228
|
+
if team_config.llm.max_tokens is not None:
|
|
1229
|
+
model_config["max_tokens"] = team_config.llm.max_tokens
|
|
1230
|
+
if team_config.llm.top_p is not None:
|
|
1231
|
+
model_config["top_p"] = team_config.llm.top_p
|
|
1232
|
+
if team_config.llm.stop is not None:
|
|
1233
|
+
model_config["stop"] = team_config.llm.stop
|
|
1234
|
+
if team_config.llm.frequency_penalty is not None:
|
|
1235
|
+
model_config["frequency_penalty"] = team_config.llm.frequency_penalty
|
|
1236
|
+
if team_config.llm.presence_penalty is not None:
|
|
1237
|
+
model_config["presence_penalty"] = team_config.llm.presence_penalty
|
|
1238
|
+
|
|
1239
|
+
# Submit to Temporal workflow
|
|
1240
|
+
# Task queue is the worker queue UUID
|
|
1241
|
+
task_queue = worker_queue_id
|
|
1242
|
+
|
|
1243
|
+
# Get Temporal client
|
|
1244
|
+
temporal_client = await get_temporal_client()
|
|
1245
|
+
|
|
1246
|
+
# Start workflow
|
|
1247
|
+
# Use team instructions as fallback system prompt if not provided in request
|
|
1248
|
+
system_prompt = execution_request.system_prompt
|
|
1249
|
+
if not system_prompt and team_config.instructions:
|
|
1250
|
+
# Convert instructions to string if it's a list
|
|
1251
|
+
if isinstance(team_config.instructions, list):
|
|
1252
|
+
system_prompt = "\n".join(team_config.instructions)
|
|
1253
|
+
else:
|
|
1254
|
+
system_prompt = team_config.instructions
|
|
1255
|
+
|
|
1256
|
+
# Get API key from Authorization header
|
|
1257
|
+
auth_header = request.headers.get("authorization", "")
|
|
1258
|
+
api_key = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None
|
|
1259
|
+
|
|
1260
|
+
# Get control plane URL from request
|
|
1261
|
+
control_plane_url = str(request.base_url).rstrip("/")
|
|
1262
|
+
|
|
1263
|
+
workflow_input = TeamExecutionInput(
|
|
1264
|
+
execution_id=execution_id,
|
|
1265
|
+
team_id=team_id,
|
|
1266
|
+
organization_id=organization["id"],
|
|
1267
|
+
prompt=execution_request.prompt,
|
|
1268
|
+
system_prompt=system_prompt,
|
|
1269
|
+
model_id=model_id,
|
|
1270
|
+
model_config=model_config,
|
|
1271
|
+
team_config=team.configuration,
|
|
1272
|
+
mcp_servers=mcp_servers,
|
|
1273
|
+
user_metadata=user_metadata,
|
|
1274
|
+
runtime_type=team.runtime.value if team.runtime else "default",
|
|
1275
|
+
control_plane_url=control_plane_url,
|
|
1276
|
+
api_key=api_key,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
workflow_handle = await temporal_client.start_workflow(
|
|
1280
|
+
TeamExecutionWorkflow.run,
|
|
1281
|
+
workflow_input, # Pass TeamExecutionInput directly
|
|
1282
|
+
id=f"team-execution-{execution_id}",
|
|
1283
|
+
task_queue=task_queue,
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
logger.info(
|
|
1287
|
+
"team_execution_submitted",
|
|
1288
|
+
execution_id=execution_id,
|
|
1289
|
+
team_id=team_id,
|
|
1290
|
+
workflow_id=workflow_handle.id,
|
|
1291
|
+
task_queue=task_queue,
|
|
1292
|
+
worker_queue_id=worker_queue_id,
|
|
1293
|
+
worker_queue_name=worker_queue.get("name"),
|
|
1294
|
+
org_id=organization["id"],
|
|
1295
|
+
org_name=organization["name"],
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
return TeamExecutionResponse(
|
|
1299
|
+
execution_id=execution_id,
|
|
1300
|
+
workflow_id=workflow_handle.id,
|
|
1301
|
+
status="PENDING",
|
|
1302
|
+
message=f"Execution submitted to worker queue: {worker_queue.get('name')}",
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
except HTTPException:
|
|
1306
|
+
raise
|
|
1307
|
+
except Exception as e:
|
|
1308
|
+
logger.error(
|
|
1309
|
+
"team_execution_failed",
|
|
1310
|
+
error=str(e),
|
|
1311
|
+
team_id=team_id,
|
|
1312
|
+
org_id=organization["id"]
|
|
1313
|
+
)
|
|
1314
|
+
raise HTTPException(
|
|
1315
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1316
|
+
detail=f"Failed to execute team: {str(e)}"
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
@router.post("/{team_id}/execute/stream")
|
|
1321
|
+
def execute_team_stream(
|
|
1322
|
+
team_id: str,
|
|
1323
|
+
execution_request: TeamExecutionRequest,
|
|
1324
|
+
db: Session = Depends(get_db),
|
|
1325
|
+
):
|
|
1326
|
+
"""
|
|
1327
|
+
Execute a team task with streaming response.
|
|
1328
|
+
|
|
1329
|
+
The team leader coordinates and delegates the task to appropriate team members.
|
|
1330
|
+
Results are streamed back in real-time.
|
|
1331
|
+
"""
|
|
1332
|
+
from control_plane_api.app.services.litellm_service import litellm_service
|
|
1333
|
+
|
|
1334
|
+
team = db.query(Team).filter(Team.id == team_id).first()
|
|
1335
|
+
if not team:
|
|
1336
|
+
raise HTTPException(status_code=404, detail="Team not found")
|
|
1337
|
+
|
|
1338
|
+
# Get team agents
|
|
1339
|
+
agents = team.agents
|
|
1340
|
+
if not agents:
|
|
1341
|
+
raise HTTPException(
|
|
1342
|
+
status_code=400,
|
|
1343
|
+
detail="Team has no agents. Add agents to the team before executing tasks."
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
# Build team coordination prompt
|
|
1347
|
+
agent_descriptions = []
|
|
1348
|
+
for agent in agents:
|
|
1349
|
+
caps = ", ".join(agent.capabilities) if agent.capabilities else "general"
|
|
1350
|
+
agent_descriptions.append(
|
|
1351
|
+
f"- {agent.name}: {agent.description or 'No description'} (Capabilities: {caps})"
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
# Create a coordination system prompt
|
|
1355
|
+
coordination_prompt = f"""You are a Team Coordinator managing a team with the following agents:
|
|
1356
|
+
|
|
1357
|
+
{chr(10).join(agent_descriptions)}
|
|
1358
|
+
|
|
1359
|
+
Your task is to:
|
|
1360
|
+
1. Analyze the user's request
|
|
1361
|
+
2. Determine which agent(s) are best suited for the task
|
|
1362
|
+
3. Delegate or route the task appropriately
|
|
1363
|
+
4. Synthesize and present the results
|
|
1364
|
+
|
|
1365
|
+
User Request: {execution_request.prompt}
|
|
1366
|
+
|
|
1367
|
+
Please coordinate the team to complete this request effectively."""
|
|
1368
|
+
|
|
1369
|
+
# Parse team configuration
|
|
1370
|
+
team_config = TeamConfiguration(**(team.configuration or {}))
|
|
1371
|
+
|
|
1372
|
+
# Use LLM config from team configuration if available
|
|
1373
|
+
model = team_config.llm.model if team_config.llm and team_config.llm.model else "kubiya/claude-sonnet-4"
|
|
1374
|
+
|
|
1375
|
+
# Build LLM kwargs from configuration
|
|
1376
|
+
llm_kwargs = {}
|
|
1377
|
+
if team_config.llm:
|
|
1378
|
+
if team_config.llm.temperature is not None:
|
|
1379
|
+
llm_kwargs["temperature"] = team_config.llm.temperature
|
|
1380
|
+
if team_config.llm.max_tokens is not None:
|
|
1381
|
+
llm_kwargs["max_tokens"] = team_config.llm.max_tokens
|
|
1382
|
+
if team_config.llm.top_p is not None:
|
|
1383
|
+
llm_kwargs["top_p"] = team_config.llm.top_p
|
|
1384
|
+
if team_config.llm.stop is not None:
|
|
1385
|
+
llm_kwargs["stop"] = team_config.llm.stop
|
|
1386
|
+
if team_config.llm.frequency_penalty is not None:
|
|
1387
|
+
llm_kwargs["frequency_penalty"] = team_config.llm.frequency_penalty
|
|
1388
|
+
if team_config.llm.presence_penalty is not None:
|
|
1389
|
+
llm_kwargs["presence_penalty"] = team_config.llm.presence_penalty
|
|
1390
|
+
|
|
1391
|
+
# Execute coordination using LiteLLM (streaming)
|
|
1392
|
+
return StreamingResponse(
|
|
1393
|
+
litellm_service.execute_agent_stream(
|
|
1394
|
+
prompt=coordination_prompt,
|
|
1395
|
+
model=model,
|
|
1396
|
+
system_prompt=execution_request.system_prompt or "You are an expert team coordinator. Delegate tasks efficiently and synthesize results clearly.",
|
|
1397
|
+
**llm_kwargs,
|
|
1398
|
+
),
|
|
1399
|
+
media_type="text/event-stream",
|
|
1400
|
+
)
|