kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1260 @@
1
+ """
2
+ Multi-tenant agents router with Temporal workflow integration.
3
+
4
+ This router handles agent CRUD operations and execution submissions.
5
+ All operations are scoped to the authenticated organization.
6
+ """
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
9
+ from typing import List, Optional
10
+ from datetime import datetime
11
+ from pydantic import BaseModel, Field
12
+ import structlog
13
+ import uuid
14
+ import httpx
15
+
16
+ from control_plane_api.app.middleware.auth import get_current_organization
17
+ from control_plane_api.app.lib.supabase import get_supabase
18
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
19
+ from control_plane_api.app.workflows.agent_execution import AgentExecutionWorkflow, AgentExecutionInput
20
+ from control_plane_api.app.routers.projects import get_default_project_id
21
+ from control_plane_api.app.lib.validation import validate_agent_for_runtime
22
+
23
+ logger = structlog.get_logger()
24
+
25
+ router = APIRouter()
26
+
27
+
28
+ class ExecutionEnvironment(BaseModel):
29
+ """Execution environment configuration for agents/teams"""
30
+ env_vars: dict[str, str] = Field(default_factory=dict, description="Environment variables (key-value pairs)")
31
+ secrets: list[str] = Field(default_factory=list, description="Secret names from Kubiya vault")
32
+ integration_ids: list[str] = Field(default_factory=list, description="Integration UUIDs for delegated credentials")
33
+
34
+
35
+ def get_agent_projects(client, agent_id: str) -> list[dict]:
36
+ """Get all projects an agent belongs to"""
37
+ try:
38
+ # Query project_agents join table
39
+ result = (
40
+ client.table("project_agents")
41
+ .select("project_id, projects(id, name, key, description)")
42
+ .eq("agent_id", agent_id)
43
+ .execute()
44
+ )
45
+
46
+ projects = []
47
+ for item in result.data:
48
+ project_data = item.get("projects")
49
+ if project_data:
50
+ projects.append({
51
+ "id": project_data["id"],
52
+ "name": project_data["name"],
53
+ "key": project_data["key"],
54
+ "description": project_data.get("description"),
55
+ })
56
+
57
+ return projects
58
+ except Exception as e:
59
+ logger.warning("failed_to_fetch_agent_projects", error=str(e), agent_id=agent_id)
60
+ return []
61
+
62
+
63
+ def get_agent_environments(client, agent_id: str) -> list[dict]:
64
+ """Get all environments an agent is assigned to"""
65
+ try:
66
+ # Query agent_environments join table
67
+ result = (
68
+ client.table("agent_environments")
69
+ .select("environment_id, environments(id, name, display_name, status)")
70
+ .eq("agent_id", agent_id)
71
+ .execute()
72
+ )
73
+
74
+ environments = []
75
+ for item in result.data:
76
+ env_data = item.get("environments")
77
+ if env_data:
78
+ environments.append({
79
+ "id": env_data["id"],
80
+ "name": env_data["name"],
81
+ "display_name": env_data.get("display_name"),
82
+ "status": env_data.get("status"),
83
+ })
84
+
85
+ return environments
86
+ except Exception as e:
87
+ logger.warning("failed_to_fetch_agent_environments", error=str(e), agent_id=agent_id)
88
+ return []
89
+
90
+
91
+ def get_entity_skills(client, organization_id: str, entity_type: str, entity_id: str) -> list[dict]:
92
+ """Get skills associated with an entity"""
93
+ try:
94
+ # Get associations
95
+ result = (
96
+ client.table("skill_associations")
97
+ .select("skill_id, configuration_override, skills(*)")
98
+ .eq("organization_id", organization_id)
99
+ .eq("entity_type", entity_type)
100
+ .eq("entity_id", entity_id)
101
+ .execute()
102
+ )
103
+
104
+ skills = []
105
+ for item in result.data:
106
+ skill_data = item.get("skills")
107
+ if skill_data and skill_data.get("enabled", True):
108
+ # Merge configuration with override
109
+ config = skill_data.get("configuration", {})
110
+ override = item.get("configuration_override")
111
+ if override:
112
+ config = {**config, **override}
113
+
114
+ skills.append({
115
+ "id": skill_data["id"],
116
+ "name": skill_data["name"],
117
+ "type": skill_data["skill_type"],
118
+ "description": skill_data.get("description"),
119
+ "enabled": skill_data.get("enabled", True),
120
+ "configuration": config,
121
+ })
122
+
123
+ return skills
124
+ except Exception as e:
125
+ logger.warning("failed_to_fetch_entity_skills", error=str(e), entity_type=entity_type, entity_id=entity_id)
126
+ return []
127
+
128
+
129
+ def get_agent_skills_with_inheritance(client, organization_id: str, agent_id: str, team_id: str | None) -> list[dict]:
130
+ """
131
+ Get all skills for an agent, including those inherited from the team.
132
+ Team skills are inherited by all team members.
133
+
134
+ Inheritance order (later overrides earlier):
135
+ 1. Team skills (if agent is part of a team)
136
+ 2. Agent skills
137
+ """
138
+ seen_ids = set()
139
+ skills = []
140
+
141
+ # 1. Get team skills first (if agent is part of a team)
142
+ if team_id:
143
+ try:
144
+ team_skills = get_entity_skills(client, organization_id, "team", team_id)
145
+ for skill in team_skills:
146
+ if skill["id"] not in seen_ids:
147
+ skills.append(skill)
148
+ seen_ids.add(skill["id"])
149
+ except Exception as e:
150
+ logger.warning("failed_to_fetch_team_skills_for_agent", error=str(e), team_id=team_id, agent_id=agent_id)
151
+
152
+ # 2. Get agent-specific skills (these override team skills if there's a conflict)
153
+ try:
154
+ agent_skills = get_entity_skills(client, organization_id, "agent", agent_id)
155
+ for skill in agent_skills:
156
+ if skill["id"] not in seen_ids:
157
+ skills.append(skill)
158
+ seen_ids.add(skill["id"])
159
+ except Exception as e:
160
+ logger.warning("failed_to_fetch_agent_skills", error=str(e), agent_id=agent_id)
161
+
162
+ return skills
163
+
164
+
165
+ # Pydantic schemas
166
+ class AgentCreate(BaseModel):
167
+ name: str = Field(..., description="Agent name")
168
+ description: str | None = Field(None, description="Agent description")
169
+ system_prompt: str | None = Field(None, description="System prompt for the agent")
170
+ capabilities: list = Field(default_factory=list, description="Agent capabilities")
171
+ configuration: dict = Field(default_factory=dict, description="Agent configuration")
172
+ model_id: str | None = Field(None, description="LiteLLM model identifier")
173
+ model: str | None = Field(None, description="Model identifier (alias for model_id)")
174
+ llm_config: dict = Field(default_factory=dict, description="Model-specific configuration")
175
+ runtime: str | None = Field(None, description="Runtime type: 'default' (Agno) or 'claude_code' (Claude Code SDK)")
176
+ runner_name: str | None = Field(None, description="Preferred runner for this agent")
177
+ team_id: str | None = Field(None, description="Team ID to assign this agent to")
178
+ environment_ids: list[str] = Field(default_factory=list, description="Environment IDs to deploy this agent to")
179
+ skill_ids: list[str] = Field(default_factory=list, description="Tool set IDs to associate with this agent")
180
+ skill_configurations: dict[str, dict] = Field(default_factory=dict, description="Tool set configurations keyed by skill ID")
181
+ execution_environment: ExecutionEnvironment | None = Field(None, description="Execution environment: env vars, secrets, integrations")
182
+
183
+
184
+ class AgentUpdate(BaseModel):
185
+ name: str | None = None
186
+ description: str | None = None
187
+ system_prompt: str | None = None
188
+ status: str | None = None
189
+ capabilities: list | None = None
190
+ configuration: dict | None = None
191
+ state: dict | None = None
192
+ model_id: str | None = None
193
+ model: str | None = None # Alias for model_id
194
+ llm_config: dict | None = None
195
+ runtime: str | None = None
196
+ runner_name: str | None = None
197
+ team_id: str | None = None
198
+ environment_ids: list[str] | None = None
199
+ skill_ids: list[str] | None = None
200
+ skill_configurations: dict[str, dict] | None = None
201
+ execution_environment: ExecutionEnvironment | None = None
202
+
203
+
204
+ class AgentResponse(BaseModel):
205
+ id: str
206
+ organization_id: str
207
+ name: str
208
+ description: str | None
209
+ system_prompt: str | None
210
+ status: str
211
+ capabilities: list
212
+ configuration: dict
213
+ model_id: str | None
214
+ llm_config: dict
215
+ runtime: str | None
216
+ runner_name: str | None
217
+ team_id: str | None
218
+ created_at: str
219
+ updated_at: str
220
+ last_active_at: str | None
221
+ state: dict
222
+ error_message: str | None
223
+ projects: list[dict] = Field(default_factory=list, description="Projects this agent belongs to")
224
+ environments: list[dict] = Field(default_factory=list, description="Environments this agent is deployed to")
225
+ skill_ids: list[str] | None = Field(default_factory=list, description="IDs of associated skills")
226
+ skills: list[dict] | None = Field(default_factory=list, description="Associated skills with details")
227
+ execution_environment: ExecutionEnvironment | None = None
228
+
229
+
230
+ class AgentExecutionRequest(BaseModel):
231
+ prompt: str = Field(..., description="The prompt to execute")
232
+ system_prompt: str | None = Field(None, description="Optional system prompt")
233
+ stream: bool = Field(False, description="Whether to stream the response")
234
+ worker_queue_id: str = Field(..., description="Worker queue ID (UUID) to route execution to - REQUIRED")
235
+ user_metadata: dict | None = Field(None, description="User attribution metadata (optional, auto-filled from token)")
236
+
237
+
238
+ class AgentExecutionResponse(BaseModel):
239
+ execution_id: str
240
+ workflow_id: str
241
+ status: str
242
+ message: str
243
+
244
+
245
+ @router.post("", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
246
+ async def create_agent(
247
+ agent_data: AgentCreate,
248
+ request: Request,
249
+ organization: dict = Depends(get_current_organization),
250
+ ):
251
+ """Create a new agent in the organization"""
252
+ try:
253
+ client = get_supabase()
254
+
255
+ agent_id = str(uuid.uuid4())
256
+ now = datetime.utcnow().isoformat()
257
+
258
+ # Handle model field - prefer 'model' over 'model_id' for backward compatibility
259
+ model_id = agent_data.model or agent_data.model_id
260
+
261
+ # Validate model_id against runtime type
262
+ runtime = agent_data.runtime or "default"
263
+ is_valid, errors = validate_agent_for_runtime(
264
+ runtime_type=runtime,
265
+ model_id=model_id,
266
+ agent_config=agent_data.configuration,
267
+ system_prompt=agent_data.system_prompt
268
+ )
269
+ if not is_valid:
270
+ error_msg = "Agent validation failed:\n" + "\n".join(f" - {err}" for err in errors)
271
+ logger.error(
272
+ "agent_validation_failed",
273
+ runtime=runtime,
274
+ model_id=model_id,
275
+ errors=errors,
276
+ org_id=organization["id"]
277
+ )
278
+ raise HTTPException(
279
+ status_code=status.HTTP_400_BAD_REQUEST,
280
+ detail=error_msg
281
+ )
282
+
283
+ # Store system_prompt in configuration for persistence
284
+ configuration = agent_data.configuration.copy() if agent_data.configuration else {}
285
+ if agent_data.system_prompt is not None:
286
+ configuration["system_prompt"] = agent_data.system_prompt
287
+
288
+ # Insert agent into database
289
+ agent_record = {
290
+ "id": agent_id,
291
+ "organization_id": organization["id"],
292
+ "name": agent_data.name,
293
+ "description": agent_data.description,
294
+ "status": "idle",
295
+ "capabilities": agent_data.capabilities,
296
+ "configuration": configuration,
297
+ "model_id": model_id,
298
+ "model_config": agent_data.llm_config,
299
+ "runtime": agent_data.runtime or "default",
300
+ "runner_name": agent_data.runner_name,
301
+ "team_id": agent_data.team_id,
302
+ # Note: skill_ids is not stored in agents table - skills are tracked via skill_associations junction table
303
+ "execution_environment": agent_data.execution_environment.dict() if agent_data.execution_environment else {},
304
+ "state": {},
305
+ "created_at": now,
306
+ "updated_at": now,
307
+ }
308
+
309
+ result = client.table("agents").insert(agent_record).execute()
310
+
311
+ if not result.data:
312
+ raise HTTPException(
313
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
314
+ detail="Failed to create agent"
315
+ )
316
+
317
+ agent = result.data[0]
318
+
319
+ # Automatically assign agent to the default project
320
+ default_project_id = get_default_project_id(organization)
321
+ if default_project_id:
322
+ try:
323
+ project_agent_record = {
324
+ "id": str(uuid.uuid4()),
325
+ "project_id": default_project_id,
326
+ "agent_id": agent_id,
327
+ "role": None,
328
+ "added_at": now,
329
+ "added_by": organization.get("user_id"),
330
+ }
331
+ client.table("project_agents").insert(project_agent_record).execute()
332
+ logger.info(
333
+ "agent_added_to_default_project",
334
+ agent_id=agent_id,
335
+ project_id=default_project_id,
336
+ org_id=organization["id"]
337
+ )
338
+ except Exception as e:
339
+ logger.warning(
340
+ "failed_to_add_agent_to_default_project",
341
+ error=str(e),
342
+ agent_id=agent_id,
343
+ org_id=organization["id"]
344
+ )
345
+
346
+ # Create skill associations if skills were provided
347
+ if agent_data.skill_ids:
348
+ try:
349
+ for skill_id in agent_data.skill_ids:
350
+ association_id = str(uuid.uuid4())
351
+ config_override = agent_data.skill_configurations.get(skill_id, {})
352
+
353
+ association_record = {
354
+ "id": association_id,
355
+ "organization_id": organization["id"],
356
+ "skill_id": skill_id,
357
+ "entity_type": "agent",
358
+ "entity_id": agent_id,
359
+ "configuration_override": config_override,
360
+ "created_at": now,
361
+ }
362
+
363
+ client.table("skill_associations").insert(association_record).execute()
364
+
365
+ logger.info(
366
+ "agent_skills_associated",
367
+ agent_id=agent_id,
368
+ skill_count=len(agent_data.skill_ids),
369
+ org_id=organization["id"]
370
+ )
371
+ except Exception as e:
372
+ logger.warning(
373
+ "failed_to_associate_agent_skills",
374
+ error=str(e),
375
+ agent_id=agent_id,
376
+ org_id=organization["id"]
377
+ )
378
+
379
+ # Create environment associations if environments were provided
380
+ if agent_data.environment_ids:
381
+ try:
382
+ for environment_id in agent_data.environment_ids:
383
+ env_association_record = {
384
+ "id": str(uuid.uuid4()),
385
+ "agent_id": agent_id,
386
+ "environment_id": environment_id,
387
+ "organization_id": organization["id"],
388
+ "assigned_at": now,
389
+ "assigned_by": organization.get("user_id"),
390
+ }
391
+ client.table("agent_environments").insert(env_association_record).execute()
392
+
393
+ logger.info(
394
+ "agent_environments_associated",
395
+ agent_id=agent_id,
396
+ environment_count=len(agent_data.environment_ids),
397
+ org_id=organization["id"]
398
+ )
399
+ except Exception as e:
400
+ logger.warning(
401
+ "failed_to_associate_agent_environments",
402
+ error=str(e),
403
+ agent_id=agent_id,
404
+ org_id=organization["id"]
405
+ )
406
+
407
+ logger.info(
408
+ "agent_created",
409
+ agent_id=agent_id,
410
+ agent_name=agent_data.name,
411
+ org_id=organization["id"],
412
+ org_slug=organization["slug"]
413
+ )
414
+
415
+ # Get skills with team inheritance
416
+ team_id = agent.get("team_id")
417
+ skills = get_agent_skills_with_inheritance(client, organization["id"], agent_id, team_id)
418
+
419
+ # Extract system_prompt from configuration
420
+ configuration = agent["configuration"] or {}
421
+ system_prompt = configuration.get("system_prompt")
422
+
423
+ return AgentResponse(
424
+ id=agent["id"],
425
+ organization_id=agent["organization_id"],
426
+ name=agent["name"],
427
+ description=agent["description"],
428
+ system_prompt=system_prompt,
429
+ status=agent["status"],
430
+ capabilities=agent["capabilities"],
431
+ configuration=agent["configuration"],
432
+ model_id=agent["model_id"],
433
+ llm_config=agent["model_config"] or {},
434
+ runtime=agent.get("runtime"),
435
+ runner_name=agent.get("runner_name"),
436
+ team_id=agent.get("team_id"),
437
+ created_at=agent["created_at"],
438
+ updated_at=agent["updated_at"],
439
+ last_active_at=agent.get("last_active_at"),
440
+ state=agent.get("state", {}),
441
+ error_message=agent.get("error_message"),
442
+ projects=get_agent_projects(client, agent_id),
443
+ environments=get_agent_environments(client, agent_id),
444
+ skill_ids=[ts["id"] for ts in skills],
445
+ skills=skills,
446
+ )
447
+
448
+ except Exception as e:
449
+ logger.error("agent_creation_failed", error=str(e), org_id=organization["id"])
450
+ raise HTTPException(
451
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
452
+ detail=f"Failed to create agent: {str(e)}"
453
+ )
454
+
455
+
456
+ @router.get("", response_model=List[AgentResponse])
457
+ async def list_agents(
458
+ request: Request,
459
+ skip: int = 0,
460
+ limit: int = 100,
461
+ status_filter: str | None = None,
462
+ organization: dict = Depends(get_current_organization),
463
+ ):
464
+ """List all agents in the organization"""
465
+ try:
466
+ client = get_supabase()
467
+
468
+ # Query agents for this organization
469
+ query = client.table("agents").select("*").eq("organization_id", organization["id"])
470
+
471
+ if status_filter:
472
+ query = query.eq("status", status_filter)
473
+
474
+ query = query.order("created_at", desc=True).range(skip, skip + limit - 1)
475
+
476
+ result = query.execute()
477
+
478
+ if not result.data:
479
+ return []
480
+
481
+ # Batch fetch all agent-project relationships in one query
482
+ agent_ids = [agent["id"] for agent in result.data]
483
+ agent_projects_result = (
484
+ client.table("project_agents")
485
+ .select("agent_id, projects(id, name, key, description)")
486
+ .in_("agent_id", agent_ids)
487
+ .execute()
488
+ )
489
+
490
+ # Group projects by agent_id
491
+ projects_by_agent = {}
492
+ for item in agent_projects_result.data or []:
493
+ agent_id = item["agent_id"]
494
+ project_data = item.get("projects")
495
+ if project_data:
496
+ if agent_id not in projects_by_agent:
497
+ projects_by_agent[agent_id] = []
498
+ projects_by_agent[agent_id].append({
499
+ "id": project_data["id"],
500
+ "name": project_data["name"],
501
+ "key": project_data["key"],
502
+ "description": project_data.get("description"),
503
+ })
504
+
505
+ # Batch fetch environments for all agents
506
+ agent_environments_result = (
507
+ client.table("agent_environments")
508
+ .select("agent_id, environments(id, name, display_name, status)")
509
+ .in_("agent_id", agent_ids)
510
+ .execute()
511
+ )
512
+
513
+ # Group environments by agent_id
514
+ environments_by_agent = {}
515
+ for item in agent_environments_result.data or []:
516
+ agent_id = item["agent_id"]
517
+ env_data = item.get("environments")
518
+ if env_data:
519
+ if agent_id not in environments_by_agent:
520
+ environments_by_agent[agent_id] = []
521
+ environments_by_agent[agent_id].append({
522
+ "id": env_data["id"],
523
+ "name": env_data["name"],
524
+ "display_name": env_data.get("display_name"),
525
+ "status": env_data.get("status"),
526
+ })
527
+
528
+ # Batch fetch skills for all agents (including team inheritance)
529
+ # Collect all unique team IDs
530
+ team_ids = set()
531
+ for agent in result.data:
532
+ if agent.get("team_id"):
533
+ team_ids.add(agent["team_id"])
534
+
535
+ # BATCH FETCH: Get all team skills in one query
536
+ team_skills = {}
537
+ if team_ids:
538
+ team_skills_result = (
539
+ client.table("skill_associations")
540
+ .select("entity_id, skill_id, configuration_override, skills(*)")
541
+ .eq("organization_id", organization["id"])
542
+ .eq("entity_type", "team")
543
+ .in_("entity_id", list(team_ids))
544
+ .execute()
545
+ )
546
+
547
+ for item in team_skills_result.data or []:
548
+ team_id = item["entity_id"]
549
+ skill_data = item.get("skills")
550
+ if skill_data and skill_data.get("enabled", True):
551
+ if team_id not in team_skills:
552
+ team_skills[team_id] = []
553
+
554
+ config = skill_data.get("configuration", {})
555
+ override = item.get("configuration_override")
556
+ if override:
557
+ config = {**config, **override}
558
+
559
+ team_skills[team_id].append({
560
+ "id": skill_data["id"],
561
+ "name": skill_data["name"],
562
+ "type": skill_data["skill_type"],
563
+ "description": skill_data.get("description"),
564
+ "enabled": skill_data.get("enabled", True),
565
+ "configuration": config,
566
+ })
567
+
568
+ # BATCH FETCH: Get all agent skills in one query
569
+ agent_skills_result = (
570
+ client.table("skill_associations")
571
+ .select("entity_id, skill_id, configuration_override, skills(*)")
572
+ .eq("organization_id", organization["id"])
573
+ .eq("entity_type", "agent")
574
+ .in_("entity_id", agent_ids)
575
+ .execute()
576
+ )
577
+
578
+ agent_direct_skills = {}
579
+ for item in agent_skills_result.data or []:
580
+ agent_id = item["entity_id"]
581
+ skill_data = item.get("skills")
582
+ if skill_data and skill_data.get("enabled", True):
583
+ if agent_id not in agent_direct_skills:
584
+ agent_direct_skills[agent_id] = []
585
+
586
+ config = skill_data.get("configuration", {})
587
+ override = item.get("configuration_override")
588
+ if override:
589
+ config = {**config, **override}
590
+
591
+ agent_direct_skills[agent_id].append({
592
+ "id": skill_data["id"],
593
+ "name": skill_data["name"],
594
+ "type": skill_data["skill_type"],
595
+ "description": skill_data.get("description"),
596
+ "enabled": skill_data.get("enabled", True),
597
+ "configuration": config,
598
+ })
599
+
600
+ # Combine team and agent skills with proper inheritance
601
+ skills_by_agent = {}
602
+ for agent in result.data:
603
+ agent_id = agent["id"]
604
+ team_id = agent.get("team_id")
605
+
606
+ # Start with empty list
607
+ combined_skills = []
608
+ seen_ids = set()
609
+
610
+ # Add team skills first (if agent is part of a team)
611
+ if team_id and team_id in team_skills:
612
+ for skill in team_skills[team_id]:
613
+ if skill["id"] not in seen_ids:
614
+ combined_skills.append(skill)
615
+ seen_ids.add(skill["id"])
616
+
617
+ # Add agent-specific skills (these override team skills)
618
+ if agent_id in agent_direct_skills:
619
+ for skill in agent_direct_skills[agent_id]:
620
+ if skill["id"] not in seen_ids:
621
+ combined_skills.append(skill)
622
+ seen_ids.add(skill["id"])
623
+
624
+ skills_by_agent[agent_id] = combined_skills
625
+
626
+ agents = []
627
+ for agent in result.data:
628
+ # Extract system_prompt from configuration
629
+ configuration = agent["configuration"] or {}
630
+ system_prompt = configuration.get("system_prompt")
631
+
632
+ agents.append(AgentResponse(
633
+ id=agent["id"],
634
+ organization_id=agent["organization_id"],
635
+ name=agent["name"],
636
+ description=agent["description"],
637
+ system_prompt=system_prompt,
638
+ status=agent["status"],
639
+ capabilities=agent["capabilities"],
640
+ configuration=agent["configuration"],
641
+ model_id=agent["model_id"],
642
+ llm_config=agent["model_config"] or {},
643
+ runtime=agent.get("runtime"),
644
+ runner_name=agent.get("runner_name"),
645
+ team_id=agent.get("team_id"),
646
+ created_at=agent["created_at"],
647
+ updated_at=agent["updated_at"],
648
+ last_active_at=agent.get("last_active_at"),
649
+ state=agent.get("state", {}),
650
+ error_message=agent.get("error_message"),
651
+ projects=projects_by_agent.get(agent["id"], []),
652
+ environments=environments_by_agent.get(agent["id"], []),
653
+ skill_ids=[ts["id"] for ts in skills_by_agent.get(agent["id"], [])],
654
+ skills=skills_by_agent.get(agent["id"], []),
655
+ execution_environment=(
656
+ ExecutionEnvironment(**agent["execution_environment"])
657
+ if agent.get("execution_environment")
658
+ else None
659
+ ),
660
+ ))
661
+
662
+ logger.info(
663
+ "agents_listed",
664
+ count=len(agents),
665
+ org_id=organization["id"],
666
+ org_slug=organization["slug"]
667
+ )
668
+
669
+ return agents
670
+
671
+ except Exception as e:
672
+ logger.error("agents_list_failed", error=str(e), org_id=organization["id"])
673
+ raise HTTPException(
674
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
675
+ detail=f"Failed to list agents: {str(e)}"
676
+ )
677
+
678
+
679
+ @router.get("/{agent_id}", response_model=AgentResponse)
680
+ async def get_agent(
681
+ agent_id: str,
682
+ request: Request,
683
+ organization: dict = Depends(get_current_organization),
684
+ ):
685
+ """Get a specific agent by ID"""
686
+ try:
687
+ client = get_supabase()
688
+
689
+ result = (
690
+ client.table("agents")
691
+ .select("*")
692
+ .eq("id", agent_id)
693
+ .eq("organization_id", organization["id"])
694
+ .single()
695
+ .execute()
696
+ )
697
+
698
+ if not result.data:
699
+ raise HTTPException(status_code=404, detail="Agent not found")
700
+
701
+ agent = result.data
702
+
703
+ # Get skills with team inheritance
704
+ team_id = agent.get("team_id")
705
+ skills = get_agent_skills_with_inheritance(client, organization["id"], agent_id, team_id)
706
+
707
+ # Parse execution_environment if it exists
708
+ execution_env = None
709
+ if agent.get("execution_environment"):
710
+ try:
711
+ execution_env = ExecutionEnvironment(**agent["execution_environment"])
712
+ except Exception:
713
+ execution_env = None
714
+
715
+ # Extract system_prompt from configuration
716
+ configuration = agent["configuration"] or {}
717
+ system_prompt = configuration.get("system_prompt")
718
+
719
+ return AgentResponse(
720
+ id=agent["id"],
721
+ organization_id=agent["organization_id"],
722
+ name=agent["name"],
723
+ description=agent["description"],
724
+ system_prompt=system_prompt,
725
+ status=agent["status"],
726
+ capabilities=agent["capabilities"],
727
+ configuration=agent["configuration"],
728
+ model_id=agent["model_id"],
729
+ llm_config=agent["model_config"] or {},
730
+ runtime=agent.get("runtime"),
731
+ runner_name=agent.get("runner_name"),
732
+ team_id=agent.get("team_id"),
733
+ created_at=agent["created_at"],
734
+ updated_at=agent["updated_at"],
735
+ last_active_at=agent.get("last_active_at"),
736
+ state=agent.get("state", {}),
737
+ error_message=agent.get("error_message"),
738
+ projects=get_agent_projects(client, agent_id),
739
+ environments=get_agent_environments(client, agent_id),
740
+ skill_ids=[ts["id"] for ts in skills],
741
+ skills=skills,
742
+ execution_environment=execution_env,
743
+ )
744
+
745
+ except HTTPException:
746
+ raise
747
+ except Exception as e:
748
+ logger.error("agent_get_failed", error=str(e), agent_id=agent_id)
749
+ raise HTTPException(
750
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
751
+ detail=f"Failed to get agent: {str(e)}"
752
+ )
753
+
754
+
755
+ @router.patch("/{agent_id}", response_model=AgentResponse)
756
+ async def update_agent(
757
+ agent_id: str,
758
+ agent_data: AgentUpdate,
759
+ request: Request,
760
+ organization: dict = Depends(get_current_organization),
761
+ ):
762
+ """Update an agent"""
763
+ try:
764
+ client = get_supabase()
765
+
766
+ # Check if agent exists and belongs to organization
767
+ existing = (
768
+ client.table("agents")
769
+ .select("id")
770
+ .eq("id", agent_id)
771
+ .eq("organization_id", organization["id"])
772
+ .execute()
773
+ )
774
+
775
+ if not existing.data:
776
+ raise HTTPException(status_code=404, detail="Agent not found")
777
+
778
+ # Build update dict
779
+ update_data = agent_data.model_dump(exclude_unset=True)
780
+
781
+ # Extract skill data before database update
782
+ skill_ids = update_data.pop("skill_ids", None)
783
+ skill_configurations = update_data.pop("skill_configurations", None)
784
+
785
+ # Extract environment data before database update (many-to-many via junction table)
786
+ environment_ids = update_data.pop("environment_ids", None)
787
+
788
+ # Extract system_prompt and store it in configuration
789
+ system_prompt = update_data.pop("system_prompt", None)
790
+ if system_prompt is not None:
791
+ # Get existing agent to merge with existing configuration
792
+ existing_agent = (
793
+ client.table("agents")
794
+ .select("configuration")
795
+ .eq("id", agent_id)
796
+ .eq("organization_id", organization["id"])
797
+ .single()
798
+ .execute()
799
+ )
800
+ existing_config = existing_agent.data.get("configuration", {}) if existing_agent.data else {}
801
+
802
+ # Merge system_prompt into configuration
803
+ merged_config = {**existing_config, "system_prompt": system_prompt}
804
+ update_data["configuration"] = merged_config
805
+
806
+ # Handle model field - prefer 'model' over 'model_id' for backward compatibility
807
+ if "model" in update_data and update_data["model"]:
808
+ update_data["model_id"] = update_data.pop("model")
809
+ elif "model" in update_data:
810
+ # Remove null model field
811
+ update_data.pop("model")
812
+
813
+ # Map llm_config to model_config for database
814
+ if "llm_config" in update_data:
815
+ update_data["model_config"] = update_data.pop("llm_config")
816
+
817
+ # Validate model_id and runtime if being updated
818
+ if "model_id" in update_data or "runtime" in update_data:
819
+ # Get current agent to merge with updates
820
+ existing_agent = (
821
+ client.table("agents")
822
+ .select("model_id, runtime, configuration")
823
+ .eq("id", agent_id)
824
+ .eq("organization_id", organization["id"])
825
+ .single()
826
+ .execute()
827
+ )
828
+
829
+ if existing_agent.data:
830
+ # Merge updates with existing values
831
+ final_model_id = update_data.get("model_id", existing_agent.data.get("model_id"))
832
+ final_runtime = update_data.get("runtime", existing_agent.data.get("runtime", "default"))
833
+ final_config = update_data.get("configuration", existing_agent.data.get("configuration", {}))
834
+
835
+ is_valid, errors = validate_agent_for_runtime(
836
+ runtime_type=final_runtime,
837
+ model_id=final_model_id,
838
+ agent_config=final_config,
839
+ system_prompt=system_prompt
840
+ )
841
+ if not is_valid:
842
+ error_msg = "Agent validation failed:\n" + "\n".join(f" - {err}" for err in errors)
843
+ logger.error(
844
+ "agent_validation_failed",
845
+ runtime=final_runtime,
846
+ model_id=final_model_id,
847
+ errors=errors,
848
+ org_id=organization["id"]
849
+ )
850
+ raise HTTPException(
851
+ status_code=status.HTTP_400_BAD_REQUEST,
852
+ detail=error_msg
853
+ )
854
+
855
+ # Handle execution_environment - convert to dict if present
856
+ if "execution_environment" in update_data and update_data["execution_environment"]:
857
+ if isinstance(update_data["execution_environment"], ExecutionEnvironment):
858
+ update_data["execution_environment"] = update_data["execution_environment"].dict()
859
+ # If None, keep as None to preserve existing value
860
+
861
+ # Note: skill_ids is not stored in agents table - skills are tracked via skill_associations junction table
862
+ # The skill associations will be updated separately below if skill_ids was provided
863
+
864
+ update_data["updated_at"] = datetime.utcnow().isoformat()
865
+
866
+ # Update agent
867
+ result = (
868
+ client.table("agents")
869
+ .update(update_data)
870
+ .eq("id", agent_id)
871
+ .eq("organization_id", organization["id"])
872
+ .execute()
873
+ )
874
+
875
+ if not result.data:
876
+ raise HTTPException(
877
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
878
+ detail="Failed to update agent"
879
+ )
880
+
881
+ agent = result.data[0]
882
+
883
+ # Update skill associations if skill_ids was provided
884
+ if skill_ids is not None:
885
+ try:
886
+ # Delete existing associations (scoped to organization)
887
+ client.table("skill_associations").delete().eq("organization_id", organization["id"]).eq("entity_type", "agent").eq("entity_id", agent_id).execute()
888
+
889
+ # Create new associations
890
+ now = datetime.utcnow().isoformat()
891
+ for skill_id in skill_ids:
892
+ association_id = str(uuid.uuid4())
893
+ config_override = (skill_configurations or {}).get(skill_id, {})
894
+
895
+ association_record = {
896
+ "id": association_id,
897
+ "organization_id": organization["id"],
898
+ "skill_id": skill_id,
899
+ "entity_type": "agent",
900
+ "entity_id": agent_id,
901
+ "configuration_override": config_override,
902
+ "created_at": now,
903
+ }
904
+
905
+ client.table("skill_associations").insert(association_record).execute()
906
+
907
+ logger.info(
908
+ "agent_skills_updated",
909
+ agent_id=agent_id,
910
+ skill_count=len(skill_ids),
911
+ org_id=organization["id"]
912
+ )
913
+ except Exception as e:
914
+ logger.error(
915
+ "failed_to_update_agent_skills",
916
+ error=str(e),
917
+ agent_id=agent_id,
918
+ org_id=organization["id"]
919
+ )
920
+ raise HTTPException(
921
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
922
+ detail=f"Failed to update agent skills: {str(e)}"
923
+ )
924
+
925
+ # Update environment associations if environment_ids was provided
926
+ if environment_ids is not None:
927
+ try:
928
+ # Delete existing environment associations
929
+ client.table("agent_environments").delete().eq("agent_id", agent_id).execute()
930
+
931
+ # Create new environment associations
932
+ for environment_id in environment_ids:
933
+ env_association_record = {
934
+ "id": str(uuid.uuid4()),
935
+ "agent_id": agent_id,
936
+ "environment_id": environment_id,
937
+ "organization_id": organization["id"],
938
+ "assigned_at": datetime.utcnow().isoformat(),
939
+ }
940
+ client.table("agent_environments").insert(env_association_record).execute()
941
+
942
+ logger.info(
943
+ "agent_environments_updated",
944
+ agent_id=agent_id,
945
+ environment_count=len(environment_ids),
946
+ org_id=organization["id"]
947
+ )
948
+ except Exception as e:
949
+ logger.warning(
950
+ "failed_to_update_agent_environments",
951
+ error=str(e),
952
+ agent_id=agent_id,
953
+ org_id=organization["id"]
954
+ )
955
+
956
+ logger.info(
957
+ "agent_updated",
958
+ agent_id=agent_id,
959
+ org_id=organization["id"],
960
+ fields_updated=list(update_data.keys())
961
+ )
962
+
963
+ # Get skills with team inheritance
964
+ team_id = agent.get("team_id")
965
+ skills = get_agent_skills_with_inheritance(client, organization["id"], agent_id, team_id)
966
+
967
+ # Parse execution_environment if it exists
968
+ execution_env = None
969
+ if agent.get("execution_environment"):
970
+ try:
971
+ execution_env = ExecutionEnvironment(**agent["execution_environment"])
972
+ except Exception:
973
+ execution_env = None
974
+
975
+ # Extract system_prompt from configuration
976
+ configuration = agent["configuration"] or {}
977
+ system_prompt = configuration.get("system_prompt")
978
+
979
+ return AgentResponse(
980
+ id=agent["id"],
981
+ organization_id=agent["organization_id"],
982
+ name=agent["name"],
983
+ description=agent["description"],
984
+ system_prompt=system_prompt,
985
+ status=agent["status"],
986
+ capabilities=agent["capabilities"],
987
+ configuration=agent["configuration"],
988
+ model_id=agent["model_id"],
989
+ llm_config=agent["model_config"] or {},
990
+ runtime=agent.get("runtime"),
991
+ runner_name=agent.get("runner_name"),
992
+ team_id=agent.get("team_id"),
993
+ created_at=agent["created_at"],
994
+ updated_at=agent["updated_at"],
995
+ last_active_at=agent.get("last_active_at"),
996
+ state=agent.get("state", {}),
997
+ error_message=agent.get("error_message"),
998
+ projects=get_agent_projects(client, agent_id),
999
+ environments=get_agent_environments(client, agent_id),
1000
+ skill_ids=[ts["id"] for ts in skills],
1001
+ skills=skills,
1002
+ execution_environment=execution_env,
1003
+ )
1004
+
1005
+ except HTTPException:
1006
+ raise
1007
+ except Exception as e:
1008
+ logger.error("agent_update_failed", error=str(e), agent_id=agent_id)
1009
+ raise HTTPException(
1010
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1011
+ detail=f"Failed to update agent: {str(e)}"
1012
+ )
1013
+
1014
+
1015
+ @router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT)
1016
+ async def delete_agent(
1017
+ agent_id: str,
1018
+ request: Request,
1019
+ organization: dict = Depends(get_current_organization),
1020
+ ):
1021
+ """Delete an agent"""
1022
+ try:
1023
+ client = get_supabase()
1024
+
1025
+ result = (
1026
+ client.table("agents")
1027
+ .delete()
1028
+ .eq("id", agent_id)
1029
+ .eq("organization_id", organization["id"])
1030
+ .execute()
1031
+ )
1032
+
1033
+ if not result.data:
1034
+ raise HTTPException(status_code=404, detail="Agent not found")
1035
+
1036
+ logger.info("agent_deleted", agent_id=agent_id, org_id=organization["id"])
1037
+
1038
+ return None
1039
+
1040
+ except HTTPException:
1041
+ raise
1042
+ except Exception as e:
1043
+ logger.error("agent_delete_failed", error=str(e), agent_id=agent_id)
1044
+ raise HTTPException(
1045
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1046
+ detail=f"Failed to delete agent: {str(e)}"
1047
+ )
1048
+
1049
+
1050
+ @router.post("/{agent_id}/execute", response_model=AgentExecutionResponse)
1051
+ async def execute_agent(
1052
+ agent_id: str,
1053
+ execution_request: AgentExecutionRequest,
1054
+ request: Request,
1055
+ organization: dict = Depends(get_current_organization),
1056
+ ):
1057
+ """
1058
+ Execute an agent by submitting to Temporal workflow.
1059
+
1060
+ This creates an execution record and starts a Temporal workflow.
1061
+ The actual execution happens asynchronously on the Temporal worker.
1062
+
1063
+ The runner_name should come from the Composer UI where user selects
1064
+ from available runners (fetched from Kubiya API /api/v1/runners).
1065
+ """
1066
+ try:
1067
+ client = get_supabase()
1068
+
1069
+ # Get agent details
1070
+ agent_result = (
1071
+ client.table("agents")
1072
+ .select("*")
1073
+ .eq("id", agent_id)
1074
+ .eq("organization_id", organization["id"])
1075
+ .single()
1076
+ .execute()
1077
+ )
1078
+
1079
+ if not agent_result.data:
1080
+ raise HTTPException(status_code=404, detail="Agent not found")
1081
+
1082
+ agent = agent_result.data
1083
+
1084
+ # Create execution record
1085
+ execution_id = str(uuid.uuid4())
1086
+ now = datetime.utcnow().isoformat()
1087
+
1088
+ # Validate and get worker queue
1089
+ worker_queue_id = execution_request.worker_queue_id
1090
+
1091
+ queue_result = (
1092
+ client.table("worker_queues")
1093
+ .select("*")
1094
+ .eq("id", worker_queue_id)
1095
+ .eq("organization_id", organization["id"])
1096
+ .maybe_single()
1097
+ .execute()
1098
+ )
1099
+
1100
+ if not queue_result.data:
1101
+ raise HTTPException(
1102
+ status_code=status.HTTP_404_NOT_FOUND,
1103
+ detail=f"Worker queue '{worker_queue_id}' not found. Please select a valid worker queue."
1104
+ )
1105
+
1106
+ worker_queue = queue_result.data
1107
+
1108
+ # Check if queue has active workers
1109
+ if worker_queue.get("status") != "active":
1110
+ raise HTTPException(
1111
+ status_code=status.HTTP_400_BAD_REQUEST,
1112
+ detail=f"Worker queue '{worker_queue.get('name')}' is not active"
1113
+ )
1114
+
1115
+ # Extract user metadata - ALWAYS use JWT-decoded organization data as source of truth
1116
+ user_metadata = execution_request.user_metadata or {}
1117
+ # Override with JWT data (user can't spoof their identity)
1118
+ user_metadata["user_id"] = organization.get("user_id")
1119
+ user_metadata["user_email"] = organization.get("user_email")
1120
+ user_metadata["user_name"] = organization.get("user_name")
1121
+ # Keep user_avatar from request if provided (not in JWT)
1122
+ if not user_metadata.get("user_avatar"):
1123
+ user_metadata["user_avatar"] = None
1124
+
1125
+ execution_record = {
1126
+ "id": execution_id,
1127
+ "organization_id": organization["id"],
1128
+ "execution_type": "AGENT",
1129
+ "entity_id": agent_id,
1130
+ "entity_name": agent["name"],
1131
+ "prompt": execution_request.prompt,
1132
+ "system_prompt": execution_request.system_prompt,
1133
+ "status": "PENDING",
1134
+ "worker_queue_id": worker_queue_id,
1135
+ "runner_name": worker_queue.get("name"), # Store queue name for display
1136
+ "user_id": user_metadata.get("user_id"),
1137
+ "user_name": user_metadata.get("user_name"),
1138
+ "user_email": user_metadata.get("user_email"),
1139
+ "user_avatar": user_metadata.get("user_avatar"),
1140
+ "usage": {},
1141
+ "execution_metadata": {
1142
+ "kubiya_org_id": organization["id"],
1143
+ "kubiya_org_name": organization["name"],
1144
+ "worker_queue_name": worker_queue.get("display_name") or worker_queue.get("name"),
1145
+ },
1146
+ "created_at": now,
1147
+ "updated_at": now,
1148
+ }
1149
+
1150
+ exec_result = client.table("executions").insert(execution_record).execute()
1151
+
1152
+ if not exec_result.data:
1153
+ raise HTTPException(
1154
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1155
+ detail="Failed to create execution record"
1156
+ )
1157
+
1158
+ # Add creator as the first participant (owner role) for multiplayer support
1159
+ user_id = user_metadata.get("user_id")
1160
+ if user_id:
1161
+ try:
1162
+ import uuid as uuid_lib
1163
+ client.table("execution_participants").insert({
1164
+ "id": str(uuid_lib.uuid4()),
1165
+ "execution_id": execution_id,
1166
+ "organization_id": organization["id"],
1167
+ "user_id": user_id,
1168
+ "user_name": user_metadata.get("user_name"),
1169
+ "user_email": user_metadata.get("user_email"),
1170
+ "user_avatar": user_metadata.get("user_avatar"),
1171
+ "role": "owner",
1172
+ }).execute()
1173
+ logger.info(
1174
+ "owner_participant_added",
1175
+ execution_id=execution_id,
1176
+ user_id=user_id,
1177
+ )
1178
+ except Exception as participant_error:
1179
+ logger.warning(
1180
+ "failed_to_add_owner_participant",
1181
+ error=str(participant_error),
1182
+ execution_id=execution_id,
1183
+ )
1184
+ # Don't fail execution creation if participant tracking fails
1185
+
1186
+ # Extract MCP servers from agent configuration
1187
+ agent_configuration = agent.get("configuration", {})
1188
+ mcp_servers = agent_configuration.get("mcpServers", {})
1189
+
1190
+ # Submit to Temporal workflow
1191
+ # Task queue is the worker queue UUID
1192
+ task_queue = worker_queue_id
1193
+
1194
+ # Get Temporal client
1195
+ temporal_client = await get_temporal_client()
1196
+
1197
+ # Start workflow
1198
+ # Use agent's stored system_prompt from configuration as fallback
1199
+ system_prompt = execution_request.system_prompt or agent_configuration.get("system_prompt")
1200
+
1201
+ # Get API key from Authorization header
1202
+ auth_header = request.headers.get("authorization", "")
1203
+ api_key = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None
1204
+
1205
+ # Get control plane URL from request
1206
+ control_plane_url = str(request.base_url).rstrip("/")
1207
+
1208
+ workflow_input = AgentExecutionInput(
1209
+ execution_id=execution_id,
1210
+ agent_id=agent_id,
1211
+ organization_id=organization["id"],
1212
+ prompt=execution_request.prompt,
1213
+ system_prompt=system_prompt,
1214
+ model_id=agent.get("model_id"),
1215
+ model_config=agent.get("model_config", {}),
1216
+ agent_config=agent_configuration,
1217
+ mcp_servers=mcp_servers,
1218
+ user_metadata=user_metadata,
1219
+ runtime_type=agent.get("runtime", "default"),
1220
+ )
1221
+
1222
+ workflow_handle = await temporal_client.start_workflow(
1223
+ AgentExecutionWorkflow.run,
1224
+ workflow_input,
1225
+ id=f"agent-execution-{execution_id}",
1226
+ task_queue=task_queue,
1227
+ )
1228
+
1229
+ logger.info(
1230
+ "agent_execution_submitted",
1231
+ execution_id=execution_id,
1232
+ agent_id=agent_id,
1233
+ workflow_id=workflow_handle.id,
1234
+ task_queue=task_queue,
1235
+ worker_queue_id=worker_queue_id,
1236
+ worker_queue_name=worker_queue.get("name"),
1237
+ org_id=organization["id"],
1238
+ org_name=organization["name"],
1239
+ )
1240
+
1241
+ return AgentExecutionResponse(
1242
+ execution_id=execution_id,
1243
+ workflow_id=workflow_handle.id,
1244
+ status="PENDING",
1245
+ message=f"Execution submitted to worker queue: {worker_queue.get('name')}",
1246
+ )
1247
+
1248
+ except HTTPException:
1249
+ raise
1250
+ except Exception as e:
1251
+ logger.error(
1252
+ "agent_execution_failed",
1253
+ error=str(e),
1254
+ agent_id=agent_id,
1255
+ org_id=organization["id"]
1256
+ )
1257
+ raise HTTPException(
1258
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1259
+ detail=f"Failed to execute agent: {str(e)}"
1260
+ )