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