kubiya-control-plane-api 0.9.15__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.
- control_plane_api/LICENSE +676 -0
- control_plane_api/README.md +350 -0
- control_plane_api/__init__.py +4 -0
- control_plane_api/__version__.py +8 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +121 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
- control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
- control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
- control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
- control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
- control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
- control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
- control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
- control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
- control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
- control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
- control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
- control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
- control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
- control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
- control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
- control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
- control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -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 +384 -0
- control_plane_api/app/activities/plan_generation_activities.py +499 -0
- control_plane_api/app/activities/team_activities.py +424 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +469 -0
- control_plane_api/app/config/config_loader.py +224 -0
- control_plane_api/app/config/model_pricing.py +323 -0
- control_plane_api/app/config/storage_config.py +159 -0
- control_plane_api/app/config.py +115 -0
- control_plane_api/app/controllers/__init__.py +0 -0
- control_plane_api/app/controllers/execution_environment_controller.py +1315 -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/environment.py +65 -0
- control_plane_api/app/lib/event_bus/__init__.py +17 -0
- control_plane_api/app/lib/event_bus/base.py +136 -0
- control_plane_api/app/lib/event_bus/manager.py +335 -0
- control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
- control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
- control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
- control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
- control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
- control_plane_api/app/lib/job_executor.py +330 -0
- control_plane_api/app/lib/kubiya_client.py +293 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/mcp_validation.py +163 -0
- control_plane_api/app/lib/nats/__init__.py +13 -0
- control_plane_api/app/lib/nats/credentials_manager.py +288 -0
- control_plane_api/app/lib/nats/listener.py +374 -0
- control_plane_api/app/lib/planning_prompt_builder.py +153 -0
- control_plane_api/app/lib/planning_tools/__init__.py +41 -0
- control_plane_api/app/lib/planning_tools/agents.py +409 -0
- control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
- control_plane_api/app/lib/planning_tools/base.py +119 -0
- control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
- control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
- control_plane_api/app/lib/planning_tools/environments.py +218 -0
- control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
- control_plane_api/app/lib/planning_tools/models.py +93 -0
- control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
- control_plane_api/app/lib/planning_tools/resources.py +242 -0
- control_plane_api/app/lib/planning_tools/teams.py +334 -0
- control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
- control_plane_api/app/lib/redis_client.py +803 -0
- control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
- control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
- control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
- control_plane_api/app/lib/storage/__init__.py +20 -0
- control_plane_api/app/lib/storage/base_provider.py +274 -0
- control_plane_api/app/lib/storage/provider_factory.py +157 -0
- control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/supabase_utils.py +138 -0
- control_plane_api/app/lib/task_planning/__init__.py +138 -0
- control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
- control_plane_api/app/lib/task_planning/agents.py +389 -0
- control_plane_api/app/lib/task_planning/cache.py +218 -0
- control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
- control_plane_api/app/lib/task_planning/helpers.py +293 -0
- control_plane_api/app/lib/task_planning/hooks.py +474 -0
- control_plane_api/app/lib/task_planning/models.py +503 -0
- control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
- control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
- control_plane_api/app/lib/task_planning/runner.py +656 -0
- control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
- control_plane_api/app/lib/task_planning/workflow.py +424 -0
- control_plane_api/app/lib/templating/__init__.py +88 -0
- control_plane_api/app/lib/templating/compiler.py +278 -0
- control_plane_api/app/lib/templating/engine.py +178 -0
- control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
- control_plane_api/app/lib/templating/parsers/base.py +96 -0
- control_plane_api/app/lib/templating/parsers/env.py +85 -0
- control_plane_api/app/lib/templating/parsers/graph.py +112 -0
- control_plane_api/app/lib/templating/parsers/secret.py +87 -0
- control_plane_api/app/lib/templating/parsers/simple.py +81 -0
- control_plane_api/app/lib/templating/resolver.py +366 -0
- control_plane_api/app/lib/templating/types.py +214 -0
- control_plane_api/app/lib/templating/validator.py +201 -0
- control_plane_api/app/lib/temporal_client.py +232 -0
- control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
- control_plane_api/app/lib/temporal_credentials_service.py +203 -0
- control_plane_api/app/lib/validation/__init__.py +24 -0
- control_plane_api/app/lib/validation/runtime_validation.py +388 -0
- control_plane_api/app/main.py +531 -0
- control_plane_api/app/middleware/__init__.py +10 -0
- control_plane_api/app/middleware/auth.py +645 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/prometheus_middleware.py +173 -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 +40 -0
- control_plane_api/app/models/agent.py +90 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +107 -0
- control_plane_api/app/models/auth_user.py +73 -0
- control_plane_api/app/models/context.py +161 -0
- control_plane_api/app/models/custom_integration.py +99 -0
- control_plane_api/app/models/environment.py +64 -0
- control_plane_api/app/models/execution.py +125 -0
- control_plane_api/app/models/execution_transition.py +50 -0
- control_plane_api/app/models/job.py +159 -0
- control_plane_api/app/models/llm_model.py +78 -0
- control_plane_api/app/models/orchestration.py +66 -0
- control_plane_api/app/models/plan_execution.py +102 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +61 -0
- control_plane_api/app/models/project_management.py +85 -0
- control_plane_api/app/models/session.py +29 -0
- control_plane_api/app/models/skill.py +155 -0
- control_plane_api/app/models/system_tables.py +43 -0
- control_plane_api/app/models/task_planning.py +372 -0
- control_plane_api/app/models/team.py +86 -0
- control_plane_api/app/models/trace.py +257 -0
- control_plane_api/app/models/user_profile.py +54 -0
- control_plane_api/app/models/worker.py +221 -0
- control_plane_api/app/models/workflow.py +161 -0
- control_plane_api/app/models/workspace.py +50 -0
- control_plane_api/app/observability/__init__.py +177 -0
- control_plane_api/app/observability/context_logging.py +475 -0
- control_plane_api/app/observability/decorators.py +337 -0
- control_plane_api/app/observability/local_span_processor.py +702 -0
- control_plane_api/app/observability/metrics.py +303 -0
- control_plane_api/app/observability/middleware.py +246 -0
- control_plane_api/app/observability/optional.py +115 -0
- control_plane_api/app/observability/tracing.py +382 -0
- control_plane_api/app/policies/README.md +149 -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_enforcement/README.md +336 -0
- control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
- control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
- control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
- control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
- control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -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 +382 -0
- control_plane_api/app/routers/agents_v2.py +1598 -0
- control_plane_api/app/routers/analytics.py +1310 -0
- control_plane_api/app/routers/auth.py +59 -0
- control_plane_api/app/routers/client_config.py +57 -0
- control_plane_api/app/routers/context_graph.py +561 -0
- control_plane_api/app/routers/context_manager.py +577 -0
- control_plane_api/app/routers/custom_integrations.py +490 -0
- control_plane_api/app/routers/enforcer.py +132 -0
- control_plane_api/app/routers/environment_context.py +252 -0
- control_plane_api/app/routers/environments.py +761 -0
- control_plane_api/app/routers/execution_environment.py +847 -0
- control_plane_api/app/routers/executions/__init__.py +28 -0
- control_plane_api/app/routers/executions/router.py +286 -0
- control_plane_api/app/routers/executions/services/__init__.py +22 -0
- control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
- control_plane_api/app/routers/executions/services/status_service.py +420 -0
- control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
- control_plane_api/app/routers/executions/services/worker_health.py +514 -0
- control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
- control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
- control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
- control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
- control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
- control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
- control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
- control_plane_api/app/routers/executions.py +4888 -0
- control_plane_api/app/routers/health.py +165 -0
- control_plane_api/app/routers/health_v2.py +394 -0
- control_plane_api/app/routers/integration_templates.py +496 -0
- control_plane_api/app/routers/integrations.py +287 -0
- control_plane_api/app/routers/jobs.py +1809 -0
- control_plane_api/app/routers/metrics.py +517 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +628 -0
- control_plane_api/app/routers/plan_executions.py +1481 -0
- control_plane_api/app/routers/plan_generation_async.py +304 -0
- control_plane_api/app/routers/policies.py +669 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +987 -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 +171 -0
- control_plane_api/app/routers/skills.py +1010 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/storage.py +456 -0
- control_plane_api/app/routers/task_planning.py +611 -0
- control_plane_api/app/routers/task_queues.py +650 -0
- control_plane_api/app/routers/team_context.py +274 -0
- control_plane_api/app/routers/teams.py +1747 -0
- control_plane_api/app/routers/templates.py +248 -0
- control_plane_api/app/routers/traces.py +571 -0
- control_plane_api/app/routers/websocket_client.py +479 -0
- control_plane_api/app/routers/websocket_executions_status.py +437 -0
- control_plane_api/app/routers/websocket_gateway.py +323 -0
- control_plane_api/app/routers/websocket_traces.py +576 -0
- control_plane_api/app/routers/worker_queues.py +2555 -0
- control_plane_api/app/routers/worker_websocket.py +419 -0
- control_plane_api/app/routers/workers.py +1004 -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/__init__.py +1 -0
- control_plane_api/app/schemas/job_schemas.py +302 -0
- control_plane_api/app/schemas/mcp_schemas.py +311 -0
- control_plane_api/app/schemas/template_schemas.py +133 -0
- control_plane_api/app/schemas/trace_schemas.py +168 -0
- control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_planning_strategy.py +233 -0
- control_plane_api/app/services/agno_service.py +838 -0
- control_plane_api/app/services/claude_code_planning_service.py +203 -0
- control_plane_api/app/services/context_graph_client.py +224 -0
- control_plane_api/app/services/custom_integration_service.py +415 -0
- control_plane_api/app/services/integration_resolution_service.py +345 -0
- control_plane_api/app/services/litellm_service.py +394 -0
- control_plane_api/app/services/plan_generator.py +79 -0
- control_plane_api/app/services/planning_strategy.py +66 -0
- control_plane_api/app/services/planning_strategy_factory.py +118 -0
- control_plane_api/app/services/policy_service.py +615 -0
- control_plane_api/app/services/state_transition_service.py +755 -0
- control_plane_api/app/services/storage_service.py +593 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
- control_plane_api/app/services/trace_retention.py +354 -0
- control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
- control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
- control_plane_api/app/services/workflow_operations_service.py +611 -0
- control_plane_api/app/skills/__init__.py +100 -0
- control_plane_api/app/skills/base.py +239 -0
- control_plane_api/app/skills/builtin/__init__.py +37 -0
- control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
- control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
- control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
- control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
- control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
- control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
- control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
- control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
- control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
- control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
- control_plane_api/app/skills/builtin/docker/skill.py +104 -0
- control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
- control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
- control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
- control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
- control_plane_api/app/skills/builtin/python/__init__.py +4 -0
- control_plane_api/app/skills/builtin/python/skill.py +92 -0
- control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
- control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
- control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
- control_plane_api/app/skills/builtin/shell/skill.py +161 -0
- control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/app/skills/builtin/slack/skill.py +302 -0
- control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
- control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/config.py +63 -0
- control_plane_api/app/skills/loaders/__init__.py +14 -0
- control_plane_api/app/skills/loaders/base.py +73 -0
- control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
- control_plane_api/app/skills/registry.py +125 -0
- control_plane_api/app/utils/helpers.py +12 -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 +520 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/plan_generation.py +254 -0
- control_plane_api/app/workflows/team_execution.py +442 -0
- control_plane_api/scripts/seed_models.py +240 -0
- control_plane_api/scripts/validate_existing_tool_names.py +492 -0
- control_plane_api/shared/__init__.py +8 -0
- control_plane_api/shared/version.py +17 -0
- control_plane_api/test_deduplication.py +274 -0
- control_plane_api/test_executor_deduplication_e2e.py +309 -0
- control_plane_api/test_job_execution_e2e.py +283 -0
- control_plane_api/test_real_integration.py +193 -0
- control_plane_api/version.py +38 -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 +1585 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/job_activities.py +199 -0
- control_plane_api/worker/activities/runtime_activities.py +1167 -0
- control_plane_api/worker/activities/skill_activities.py +282 -0
- control_plane_api/worker/activities/team_activities.py +479 -0
- control_plane_api/worker/agent_runtime_server.py +370 -0
- control_plane_api/worker/binary_manager.py +333 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +273 -0
- control_plane_api/worker/control_plane_client.py +1491 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/health_monitor.py +159 -0
- control_plane_api/worker/metrics.py +237 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/error_events.py +105 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +35 -0
- control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
- control_plane_api/worker/runtimes/agno/__init__.py +34 -0
- control_plane_api/worker/runtimes/agno/config.py +248 -0
- control_plane_api/worker/runtimes/agno/hooks.py +385 -0
- control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
- control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
- control_plane_api/worker/runtimes/agno/utils.py +163 -0
- control_plane_api/worker/runtimes/base.py +979 -0
- control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
- control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
- control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
- control_plane_api/worker/runtimes/claude_code/config.py +829 -0
- control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
- control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
- control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
- control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
- control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
- control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
- control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/model_utils.py +107 -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_communication_tools.py +908 -0
- control_plane_api/worker/services/agent_executor.py +485 -0
- control_plane_api/worker/services/agent_executor_v2.py +793 -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/code_ingestion_tools.py +465 -0
- control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
- control_plane_api/worker/services/data_visualization.py +834 -0
- control_plane_api/worker/services/event_publisher.py +531 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +365 -0
- control_plane_api/worker/services/skill_context_enhancement.py +181 -0
- control_plane_api/worker/services/skill_factory.py +471 -0
- control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
- control_plane_api/worker/services/team_executor.py +715 -0
- control_plane_api/worker/services/team_executor_v2.py +1866 -0
- control_plane_api/worker/services/tool_enforcement.py +254 -0
- control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
- control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
- control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
- control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
- control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
- control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/models.py +142 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
- control_plane_api/worker/skills/__init__.py +12 -0
- control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
- control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
- control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
- control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
- control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
- control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
- control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
- control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
- control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
- control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
- control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
- control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
- control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
- control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
- control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
- control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
- control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
- control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
- control_plane_api/worker/skills/loaders/__init__.py +5 -0
- control_plane_api/worker/skills/loaders/base.py +23 -0
- control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
- control_plane_api/worker/skills/registry.py +208 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/conftest.py +12 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
- control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
- control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
- control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
- control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
- control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
- control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
- control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
- control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
- control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
- control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
- control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +330 -0
- control_plane_api/worker/utils/environment.py +65 -0
- control_plane_api/worker/utils/error_publisher.py +260 -0
- control_plane_api/worker/utils/event_batcher.py +256 -0
- control_plane_api/worker/utils/logging_config.py +335 -0
- control_plane_api/worker/utils/logging_helper.py +326 -0
- control_plane_api/worker/utils/parameter_validator.py +120 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +665 -0
- control_plane_api/worker/utils/tool_validation.py +332 -0
- control_plane_api/worker/utils/workspace_manager.py +163 -0
- control_plane_api/worker/websocket_client.py +393 -0
- control_plane_api/worker/worker.py +1297 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +909 -0
- control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
- control_plane_api/worker/workflows/team_execution.py +611 -0
- kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
- kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
- kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
- kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
- scripts/__init__.py +1 -0
- scripts/migrations.py +39 -0
- scripts/seed_worker_queues.py +128 -0
- scripts/setup_agent_runtime.py +142 -0
- worker_internal/__init__.py +1 -0
- worker_internal/planner/__init__.py +1 -0
- worker_internal/planner/activities.py +1499 -0
- worker_internal/planner/agent_tools.py +197 -0
- worker_internal/planner/event_models.py +148 -0
- worker_internal/planner/event_publisher.py +67 -0
- worker_internal/planner/models.py +199 -0
- worker_internal/planner/retry_logic.py +134 -0
- worker_internal/planner/worker.py +300 -0
- worker_internal/planner/workflows.py +970 -0
|
@@ -0,0 +1,1598 @@
|
|
|
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 Dict, List, Optional
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
import structlog
|
|
13
|
+
import uuid
|
|
14
|
+
import httpx
|
|
15
|
+
import os
|
|
16
|
+
from sqlalchemy.orm import Session, joinedload
|
|
17
|
+
from sqlalchemy import and_
|
|
18
|
+
|
|
19
|
+
from control_plane_api.app.middleware.auth import get_current_organization
|
|
20
|
+
from control_plane_api.app.database import get_db
|
|
21
|
+
from control_plane_api.app.lib.temporal_client import get_temporal_client
|
|
22
|
+
from control_plane_api.app.lib.mcp_validation import validate_execution_environment_mcp, MCPValidationError
|
|
23
|
+
from control_plane_api.app.workflows.agent_execution import AgentExecutionWorkflow, AgentExecutionInput
|
|
24
|
+
from control_plane_api.app.routers.projects import get_default_project_id
|
|
25
|
+
from control_plane_api.app.lib.validation import validate_agent_for_runtime
|
|
26
|
+
from control_plane_api.app.schemas.mcp_schemas import MCPServerConfig
|
|
27
|
+
from control_plane_api.app.models import (
|
|
28
|
+
Agent, AgentStatus, Execution, ExecutionStatus, ExecutionType,
|
|
29
|
+
ExecutionTriggerSource, ExecutionParticipant, ParticipantRole,
|
|
30
|
+
Skill, SkillAssociation, AgentEnvironment, Environment, Project,
|
|
31
|
+
ProjectAgent, WorkerQueue
|
|
32
|
+
)
|
|
33
|
+
from control_plane_api.app.lib.sqlalchemy_utils import model_to_dict, models_to_dict_list
|
|
34
|
+
from control_plane_api.app.observability import (
|
|
35
|
+
instrument_endpoint,
|
|
36
|
+
create_span_with_context,
|
|
37
|
+
add_span_event,
|
|
38
|
+
add_span_error,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = structlog.get_logger()
|
|
42
|
+
|
|
43
|
+
router = APIRouter()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ExecutionEnvironment(BaseModel):
|
|
47
|
+
"""Execution environment configuration for agents/teams"""
|
|
48
|
+
working_dir: str | None = Field(None, description="Working directory for execution (overrides default workspace)")
|
|
49
|
+
env_vars: dict[str, str] = Field(default_factory=dict, description="Environment variables (key-value pairs)")
|
|
50
|
+
secrets: list[str] = Field(default_factory=list, description="Secret names from Kubiya vault")
|
|
51
|
+
integration_ids: list[str] = Field(default_factory=list, description="Integration UUIDs for delegated credentials")
|
|
52
|
+
mcp_servers: Dict[str, MCPServerConfig] = Field(
|
|
53
|
+
default_factory=dict,
|
|
54
|
+
description="MCP (Model Context Protocol) server configurations. Supports stdio, HTTP, and SSE transports."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_agent_projects(db: Session, agent_id: str) -> list[dict]:
|
|
59
|
+
"""Get all projects an agent belongs to"""
|
|
60
|
+
try:
|
|
61
|
+
# Query project_agents join table with Project relationship
|
|
62
|
+
project_agents = (
|
|
63
|
+
db.query(ProjectAgent)
|
|
64
|
+
.join(Project, ProjectAgent.project_id == Project.id)
|
|
65
|
+
.filter(ProjectAgent.agent_id == agent_id)
|
|
66
|
+
.all()
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
projects = []
|
|
70
|
+
for pa in project_agents:
|
|
71
|
+
if pa.project:
|
|
72
|
+
projects.append({
|
|
73
|
+
"id": str(pa.project.id),
|
|
74
|
+
"name": pa.project.name,
|
|
75
|
+
"key": pa.project.key,
|
|
76
|
+
"description": pa.project.description,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return projects
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning("failed_to_fetch_agent_projects", error=str(e), agent_id=agent_id)
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_agent_environments(db: Session, agent_id: str) -> list[dict]:
|
|
86
|
+
"""Get all environments an agent is assigned to"""
|
|
87
|
+
try:
|
|
88
|
+
# Query agent_environments join table with Environment relationship
|
|
89
|
+
agent_envs = (
|
|
90
|
+
db.query(AgentEnvironment)
|
|
91
|
+
.join(Environment, AgentEnvironment.environment_id == Environment.id)
|
|
92
|
+
.filter(AgentEnvironment.agent_id == agent_id)
|
|
93
|
+
.all()
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
environments = []
|
|
97
|
+
for ae in agent_envs:
|
|
98
|
+
env = db.query(Environment).filter(Environment.id == ae.environment_id).first()
|
|
99
|
+
if env:
|
|
100
|
+
environments.append({
|
|
101
|
+
"id": str(env.id),
|
|
102
|
+
"name": env.name,
|
|
103
|
+
"display_name": env.display_name,
|
|
104
|
+
"status": env.status,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return environments
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning("failed_to_fetch_agent_environments", error=str(e), agent_id=agent_id)
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_entity_skills(db: Session, organization_id: str, entity_type: str, entity_id: str) -> list[dict]:
|
|
114
|
+
"""Get skills associated with an entity"""
|
|
115
|
+
try:
|
|
116
|
+
# Get associations with joined skills
|
|
117
|
+
skill_associations = (
|
|
118
|
+
db.query(SkillAssociation)
|
|
119
|
+
.join(Skill, SkillAssociation.skill_id == Skill.id)
|
|
120
|
+
.filter(
|
|
121
|
+
SkillAssociation.organization_id == organization_id,
|
|
122
|
+
SkillAssociation.entity_type == entity_type,
|
|
123
|
+
SkillAssociation.entity_id == entity_id
|
|
124
|
+
)
|
|
125
|
+
.all()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
skills = []
|
|
129
|
+
for assoc in skill_associations:
|
|
130
|
+
skill = db.query(Skill).filter(Skill.id == assoc.skill_id).first()
|
|
131
|
+
if skill and skill.enabled:
|
|
132
|
+
# Merge configuration with override
|
|
133
|
+
config = skill.configuration or {}
|
|
134
|
+
override = assoc.configuration_override
|
|
135
|
+
if override:
|
|
136
|
+
config = {**config, **override}
|
|
137
|
+
|
|
138
|
+
skills.append({
|
|
139
|
+
"id": str(skill.id),
|
|
140
|
+
"name": skill.name,
|
|
141
|
+
"type": skill.skill_type,
|
|
142
|
+
"description": skill.description,
|
|
143
|
+
"enabled": skill.enabled,
|
|
144
|
+
"configuration": config,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return skills
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.warning("failed_to_fetch_entity_skills", error=str(e), entity_type=entity_type, entity_id=entity_id)
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_agent_skills_with_inheritance(db: Session, organization_id: str, agent_id: str, team_id: str | None) -> list[dict]:
|
|
154
|
+
"""
|
|
155
|
+
Get all skills for an agent, including those inherited from the team.
|
|
156
|
+
Team skills are inherited by all team members.
|
|
157
|
+
|
|
158
|
+
Inheritance order (later overrides earlier):
|
|
159
|
+
1. Team skills (if agent is part of a team)
|
|
160
|
+
2. Agent skills
|
|
161
|
+
"""
|
|
162
|
+
seen_ids = set()
|
|
163
|
+
skills = []
|
|
164
|
+
|
|
165
|
+
# 1. Get team skills first (if agent is part of a team)
|
|
166
|
+
if team_id:
|
|
167
|
+
try:
|
|
168
|
+
team_skills = get_entity_skills(db, organization_id, "team", team_id)
|
|
169
|
+
for skill in team_skills:
|
|
170
|
+
if skill["id"] not in seen_ids:
|
|
171
|
+
skills.append(skill)
|
|
172
|
+
seen_ids.add(skill["id"])
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.warning("failed_to_fetch_team_skills_for_agent", error=str(e), team_id=team_id, agent_id=agent_id)
|
|
175
|
+
|
|
176
|
+
# 2. Get agent-specific skills (these override team skills if there's a conflict)
|
|
177
|
+
try:
|
|
178
|
+
agent_skills = get_entity_skills(db, organization_id, "agent", agent_id)
|
|
179
|
+
for skill in agent_skills:
|
|
180
|
+
if skill["id"] not in seen_ids:
|
|
181
|
+
skills.append(skill)
|
|
182
|
+
seen_ids.add(skill["id"])
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning("failed_to_fetch_agent_skills", error=str(e), agent_id=agent_id)
|
|
185
|
+
|
|
186
|
+
return skills
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Pydantic schemas
|
|
190
|
+
class AgentCreate(BaseModel):
|
|
191
|
+
name: str = Field(..., description="Agent name")
|
|
192
|
+
description: str | None = Field(None, description="Agent description")
|
|
193
|
+
system_prompt: str | None = Field(None, description="System prompt for the agent")
|
|
194
|
+
capabilities: list = Field(default_factory=list, description="Agent capabilities")
|
|
195
|
+
configuration: dict = Field(default_factory=dict, description="Agent configuration")
|
|
196
|
+
model_id: str | None = Field(None, description="LiteLLM model identifier")
|
|
197
|
+
model: str | None = Field(None, description="Model identifier (alias for model_id)")
|
|
198
|
+
llm_config: dict = Field(default_factory=dict, description="Model-specific configuration")
|
|
199
|
+
runtime: str | None = Field(None, description="Runtime type: 'default' (Agno) or 'claude_code' (Claude Code SDK)")
|
|
200
|
+
runner_name: str | None = Field(None, description="Preferred runner for this agent")
|
|
201
|
+
team_id: str | None = Field(None, description="Team ID to assign this agent to")
|
|
202
|
+
environment_ids: list[str] = Field(default_factory=list, description="Environment IDs to deploy this agent to")
|
|
203
|
+
skill_ids: list[str] = Field(default_factory=list, description="Tool set IDs to associate with this agent")
|
|
204
|
+
skill_configurations: dict[str, dict] = Field(default_factory=dict, description="Tool set configurations keyed by skill ID")
|
|
205
|
+
execution_environment: ExecutionEnvironment | None = Field(None, description="Execution environment: env vars, secrets, integrations")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class AgentUpdate(BaseModel):
|
|
209
|
+
name: str | None = None
|
|
210
|
+
description: str | None = None
|
|
211
|
+
system_prompt: str | None = None
|
|
212
|
+
status: str | None = None
|
|
213
|
+
capabilities: list | None = None
|
|
214
|
+
configuration: dict | None = None
|
|
215
|
+
state: dict | None = None
|
|
216
|
+
model_id: str | None = None
|
|
217
|
+
model: str | None = None # Alias for model_id
|
|
218
|
+
llm_config: dict | None = None
|
|
219
|
+
runtime: str | None = None
|
|
220
|
+
runner_name: str | None = None
|
|
221
|
+
team_id: str | None = None
|
|
222
|
+
environment_ids: list[str] | None = None
|
|
223
|
+
skill_ids: list[str] | None = None
|
|
224
|
+
skill_configurations: dict[str, dict] | None = None
|
|
225
|
+
execution_environment: ExecutionEnvironment | None = None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class AgentResponse(BaseModel):
|
|
229
|
+
id: str
|
|
230
|
+
organization_id: str
|
|
231
|
+
name: str
|
|
232
|
+
description: str | None
|
|
233
|
+
system_prompt: str | None
|
|
234
|
+
status: str
|
|
235
|
+
capabilities: list
|
|
236
|
+
configuration: dict
|
|
237
|
+
model_id: str | None
|
|
238
|
+
llm_config: dict
|
|
239
|
+
runtime: str | None
|
|
240
|
+
runner_name: str | None
|
|
241
|
+
team_id: str | None
|
|
242
|
+
created_at: str
|
|
243
|
+
updated_at: str
|
|
244
|
+
last_active_at: str | None
|
|
245
|
+
state: dict
|
|
246
|
+
error_message: str | None
|
|
247
|
+
projects: list[dict] = Field(default_factory=list, description="Projects this agent belongs to")
|
|
248
|
+
environments: list[dict] = Field(default_factory=list, description="Environments this agent is deployed to")
|
|
249
|
+
skill_ids: list[str] | None = Field(default_factory=list, description="IDs of associated skills")
|
|
250
|
+
skills: list[dict] | None = Field(default_factory=list, description="Associated skills with details")
|
|
251
|
+
execution_environment: ExecutionEnvironment | None = None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class AgentExecutionRequest(BaseModel):
|
|
255
|
+
prompt: str = Field(..., description="The prompt to execute")
|
|
256
|
+
system_prompt: str | None = Field(None, description="Optional system prompt")
|
|
257
|
+
stream: bool = Field(False, description="Whether to stream the response")
|
|
258
|
+
worker_queue_id: str = Field(..., description="Worker queue ID (UUID) to route execution to - REQUIRED")
|
|
259
|
+
user_metadata: dict | None = Field(None, description="User attribution metadata (optional, auto-filled from token)")
|
|
260
|
+
execution_environment: ExecutionEnvironment | None = Field(None, description="Optional execution environment settings (working_dir, etc.)")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class AgentExecutionResponse(BaseModel):
|
|
264
|
+
execution_id: str
|
|
265
|
+
workflow_id: str
|
|
266
|
+
status: str
|
|
267
|
+
message: str
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_or_create_default_shell_skill(db: Session, organization_id: str) -> Optional[str]:
|
|
271
|
+
"""
|
|
272
|
+
Get or create the default shell skill for an organization.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
db: Database session
|
|
276
|
+
organization_id: Organization ID
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Shell skill ID if found/created, None if failed
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
# First, try to find existing shell skill
|
|
283
|
+
existing_skill = (
|
|
284
|
+
db.query(Skill)
|
|
285
|
+
.filter(
|
|
286
|
+
Skill.organization_id == organization_id,
|
|
287
|
+
Skill.skill_type == "shell",
|
|
288
|
+
Skill.enabled == True
|
|
289
|
+
)
|
|
290
|
+
.first()
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if existing_skill:
|
|
294
|
+
logger.info(
|
|
295
|
+
"found_existing_shell_skill",
|
|
296
|
+
skill_id=str(existing_skill.id),
|
|
297
|
+
org_id=organization_id
|
|
298
|
+
)
|
|
299
|
+
return str(existing_skill.id)
|
|
300
|
+
|
|
301
|
+
# Create default shell skill if none exists
|
|
302
|
+
skill_id = uuid.uuid4()
|
|
303
|
+
now = datetime.utcnow()
|
|
304
|
+
|
|
305
|
+
skill = Skill(
|
|
306
|
+
id=skill_id,
|
|
307
|
+
organization_id=organization_id,
|
|
308
|
+
name="Shell",
|
|
309
|
+
skill_type="shell",
|
|
310
|
+
description="Execute shell commands on the system",
|
|
311
|
+
icon="Terminal",
|
|
312
|
+
enabled=True,
|
|
313
|
+
configuration={},
|
|
314
|
+
created_at=now,
|
|
315
|
+
updated_at=now,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
db.add(skill)
|
|
319
|
+
db.commit()
|
|
320
|
+
db.refresh(skill)
|
|
321
|
+
|
|
322
|
+
logger.info(
|
|
323
|
+
"default_shell_skill_created",
|
|
324
|
+
skill_id=str(skill_id),
|
|
325
|
+
org_id=organization_id
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return str(skill_id)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
db.rollback()
|
|
331
|
+
logger.error(
|
|
332
|
+
"failed_to_get_or_create_shell_skill",
|
|
333
|
+
error=str(e),
|
|
334
|
+
org_id=organization_id
|
|
335
|
+
)
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@router.post("", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
|
|
340
|
+
@instrument_endpoint("agents_v2.create_agent")
|
|
341
|
+
async def create_agent(
|
|
342
|
+
agent_data: AgentCreate,
|
|
343
|
+
request: Request,
|
|
344
|
+
organization: dict = Depends(get_current_organization),
|
|
345
|
+
db: Session = Depends(get_db),
|
|
346
|
+
):
|
|
347
|
+
"""Create a new agent in the organization"""
|
|
348
|
+
try:
|
|
349
|
+
|
|
350
|
+
agent_id = str(uuid.uuid4())
|
|
351
|
+
now = datetime.utcnow().isoformat()
|
|
352
|
+
|
|
353
|
+
# Handle model field - prefer 'model' over 'model_id' for backward compatibility
|
|
354
|
+
model_id = agent_data.model or agent_data.model_id
|
|
355
|
+
|
|
356
|
+
# Validate model_id against runtime type
|
|
357
|
+
runtime = agent_data.runtime or "default"
|
|
358
|
+
is_valid, errors = validate_agent_for_runtime(
|
|
359
|
+
runtime_type=runtime,
|
|
360
|
+
model_id=model_id,
|
|
361
|
+
agent_config=agent_data.configuration,
|
|
362
|
+
system_prompt=agent_data.system_prompt
|
|
363
|
+
)
|
|
364
|
+
if not is_valid:
|
|
365
|
+
error_msg = "Agent validation failed:\n" + "\n".join(f" - {err}" for err in errors)
|
|
366
|
+
logger.error(
|
|
367
|
+
"agent_validation_failed",
|
|
368
|
+
runtime=runtime,
|
|
369
|
+
model_id=model_id,
|
|
370
|
+
errors=errors,
|
|
371
|
+
org_id=organization["id"]
|
|
372
|
+
)
|
|
373
|
+
raise HTTPException(
|
|
374
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
375
|
+
detail=error_msg
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Validate MCP server configuration if present
|
|
379
|
+
if agent_data.execution_environment and agent_data.execution_environment.mcp_servers:
|
|
380
|
+
try:
|
|
381
|
+
mcp_validation = validate_execution_environment_mcp(
|
|
382
|
+
agent_data.execution_environment.model_dump(by_alias=True),
|
|
383
|
+
strict=False # Non-strict: allow warnings for missing secrets/env vars
|
|
384
|
+
)
|
|
385
|
+
if not mcp_validation["valid"]:
|
|
386
|
+
error_msg = "MCP configuration validation failed:\n" + "\n".join(f" - {err}" for err in mcp_validation["errors"])
|
|
387
|
+
logger.error(
|
|
388
|
+
"mcp_validation_failed",
|
|
389
|
+
errors=mcp_validation["errors"],
|
|
390
|
+
warnings=mcp_validation["warnings"],
|
|
391
|
+
org_id=organization["id"]
|
|
392
|
+
)
|
|
393
|
+
raise HTTPException(
|
|
394
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
395
|
+
detail=error_msg
|
|
396
|
+
)
|
|
397
|
+
# Log warnings if any
|
|
398
|
+
if mcp_validation["warnings"]:
|
|
399
|
+
logger.warning(
|
|
400
|
+
"mcp_validation_warnings",
|
|
401
|
+
warnings=mcp_validation["warnings"],
|
|
402
|
+
required_secrets=mcp_validation["required_secrets"],
|
|
403
|
+
required_env_vars=mcp_validation["required_env_vars"],
|
|
404
|
+
org_id=organization["id"]
|
|
405
|
+
)
|
|
406
|
+
except MCPValidationError as e:
|
|
407
|
+
raise HTTPException(
|
|
408
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
409
|
+
detail=str(e)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Store system_prompt in configuration for persistence
|
|
413
|
+
configuration = agent_data.configuration.copy() if agent_data.configuration else {}
|
|
414
|
+
if agent_data.system_prompt is not None:
|
|
415
|
+
configuration["system_prompt"] = agent_data.system_prompt
|
|
416
|
+
|
|
417
|
+
# Create Agent object
|
|
418
|
+
agent = Agent(
|
|
419
|
+
id=agent_id,
|
|
420
|
+
organization_id=organization["id"],
|
|
421
|
+
name=agent_data.name,
|
|
422
|
+
description=agent_data.description,
|
|
423
|
+
status=AgentStatus.IDLE,
|
|
424
|
+
capabilities=agent_data.capabilities,
|
|
425
|
+
configuration=configuration,
|
|
426
|
+
model_id=model_id,
|
|
427
|
+
model_config=agent_data.llm_config,
|
|
428
|
+
runtime=agent_data.runtime or "default",
|
|
429
|
+
runner_name=agent_data.runner_name,
|
|
430
|
+
team_id=agent_data.team_id,
|
|
431
|
+
execution_environment=agent_data.execution_environment.model_dump(by_alias=True) if agent_data.execution_environment else {},
|
|
432
|
+
state={},
|
|
433
|
+
created_at=now,
|
|
434
|
+
updated_at=now,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
db.add(agent)
|
|
438
|
+
db.commit()
|
|
439
|
+
db.refresh(agent)
|
|
440
|
+
|
|
441
|
+
# Automatically assign agent to the default project
|
|
442
|
+
default_project_id = get_default_project_id(db, organization)
|
|
443
|
+
if default_project_id:
|
|
444
|
+
try:
|
|
445
|
+
project_agent = ProjectAgent(
|
|
446
|
+
id=uuid.uuid4(),
|
|
447
|
+
project_id=default_project_id,
|
|
448
|
+
agent_id=agent_id,
|
|
449
|
+
role=None,
|
|
450
|
+
added_at=now,
|
|
451
|
+
added_by=organization.get("user_id"),
|
|
452
|
+
)
|
|
453
|
+
db.add(project_agent)
|
|
454
|
+
db.commit()
|
|
455
|
+
logger.info(
|
|
456
|
+
"agent_added_to_default_project",
|
|
457
|
+
agent_id=str(agent_id),
|
|
458
|
+
project_id=default_project_id,
|
|
459
|
+
org_id=organization["id"]
|
|
460
|
+
)
|
|
461
|
+
except Exception as e:
|
|
462
|
+
db.rollback()
|
|
463
|
+
logger.warning(
|
|
464
|
+
"failed_to_add_agent_to_default_project",
|
|
465
|
+
error=str(e),
|
|
466
|
+
agent_id=str(agent_id),
|
|
467
|
+
org_id=organization["id"]
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# VALIDATION: Ensure at least one skill is associated with the agent
|
|
471
|
+
# Agents without skills are non-functional
|
|
472
|
+
if not agent_data.skill_ids or len(agent_data.skill_ids) == 0:
|
|
473
|
+
# Auto-add shell skill as default
|
|
474
|
+
shell_skill_id = get_or_create_default_shell_skill(db, organization["id"])
|
|
475
|
+
|
|
476
|
+
if shell_skill_id:
|
|
477
|
+
agent_data.skill_ids = [shell_skill_id]
|
|
478
|
+
logger.info(
|
|
479
|
+
"auto_added_shell_skill",
|
|
480
|
+
agent_id=str(agent_id),
|
|
481
|
+
org_id=organization["id"],
|
|
482
|
+
reason="no_skills_provided"
|
|
483
|
+
)
|
|
484
|
+
else:
|
|
485
|
+
db.rollback()
|
|
486
|
+
raise HTTPException(
|
|
487
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
488
|
+
detail="At least one skill is required to create an agent. Unable to add default shell skill. Please add a skill manually."
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Create skill associations if skills were provided
|
|
492
|
+
if agent_data.skill_ids:
|
|
493
|
+
try:
|
|
494
|
+
for skill_id in agent_data.skill_ids:
|
|
495
|
+
config_override = agent_data.skill_configurations.get(skill_id, {})
|
|
496
|
+
|
|
497
|
+
skill_association = SkillAssociation(
|
|
498
|
+
id=uuid.uuid4(),
|
|
499
|
+
organization_id=organization["id"],
|
|
500
|
+
skill_id=skill_id,
|
|
501
|
+
entity_type="agent",
|
|
502
|
+
entity_id=agent_id,
|
|
503
|
+
configuration_override=config_override,
|
|
504
|
+
created_at=now,
|
|
505
|
+
)
|
|
506
|
+
db.add(skill_association)
|
|
507
|
+
|
|
508
|
+
db.commit()
|
|
509
|
+
logger.info(
|
|
510
|
+
"agent_skills_associated",
|
|
511
|
+
agent_id=str(agent_id),
|
|
512
|
+
skill_count=len(agent_data.skill_ids),
|
|
513
|
+
org_id=organization["id"]
|
|
514
|
+
)
|
|
515
|
+
except Exception as e:
|
|
516
|
+
db.rollback()
|
|
517
|
+
logger.warning(
|
|
518
|
+
"failed_to_associate_agent_skills",
|
|
519
|
+
error=str(e),
|
|
520
|
+
agent_id=str(agent_id),
|
|
521
|
+
org_id=organization["id"]
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Create environment associations if environments were provided
|
|
525
|
+
if agent_data.environment_ids:
|
|
526
|
+
try:
|
|
527
|
+
for environment_id in agent_data.environment_ids:
|
|
528
|
+
agent_env = AgentEnvironment(
|
|
529
|
+
id=uuid.uuid4(),
|
|
530
|
+
agent_id=agent_id,
|
|
531
|
+
environment_id=environment_id,
|
|
532
|
+
organization_id=organization["id"],
|
|
533
|
+
assigned_at=now,
|
|
534
|
+
assigned_by=organization.get("user_id"),
|
|
535
|
+
)
|
|
536
|
+
db.add(agent_env)
|
|
537
|
+
|
|
538
|
+
db.commit()
|
|
539
|
+
logger.info(
|
|
540
|
+
"agent_environments_associated",
|
|
541
|
+
agent_id=str(agent_id),
|
|
542
|
+
environment_count=len(agent_data.environment_ids),
|
|
543
|
+
org_id=organization["id"]
|
|
544
|
+
)
|
|
545
|
+
except Exception as e:
|
|
546
|
+
db.rollback()
|
|
547
|
+
logger.warning(
|
|
548
|
+
"failed_to_associate_agent_environments",
|
|
549
|
+
error=str(e),
|
|
550
|
+
agent_id=str(agent_id),
|
|
551
|
+
org_id=organization["id"]
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
logger.info(
|
|
555
|
+
"agent_created",
|
|
556
|
+
agent_id=str(agent_id),
|
|
557
|
+
agent_name=agent_data.name,
|
|
558
|
+
org_id=organization["id"],
|
|
559
|
+
org_slug=organization["slug"]
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Get skills with team inheritance
|
|
563
|
+
team_id = str(agent.team_id) if agent.team_id else None
|
|
564
|
+
skills = get_agent_skills_with_inheritance(db, organization["id"], str(agent_id), team_id)
|
|
565
|
+
|
|
566
|
+
# Extract system_prompt from configuration
|
|
567
|
+
configuration = agent.configuration or {}
|
|
568
|
+
system_prompt = configuration.get("system_prompt")
|
|
569
|
+
|
|
570
|
+
return AgentResponse(
|
|
571
|
+
id=str(agent.id),
|
|
572
|
+
organization_id=agent.organization_id,
|
|
573
|
+
name=agent.name,
|
|
574
|
+
description=agent.description,
|
|
575
|
+
system_prompt=system_prompt,
|
|
576
|
+
status=agent.status,
|
|
577
|
+
capabilities=agent.capabilities,
|
|
578
|
+
configuration=agent.configuration,
|
|
579
|
+
model_id=agent.model_id,
|
|
580
|
+
llm_config=agent.model_config or {},
|
|
581
|
+
runtime=agent.runtime,
|
|
582
|
+
runner_name=agent.runner_name,
|
|
583
|
+
team_id=str(agent.team_id) if agent.team_id else None,
|
|
584
|
+
created_at=agent.created_at.isoformat() if agent.created_at else None,
|
|
585
|
+
updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
|
|
586
|
+
last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
|
|
587
|
+
state=agent.state or {},
|
|
588
|
+
error_message=agent.error_message,
|
|
589
|
+
projects=get_agent_projects(db, str(agent_id)),
|
|
590
|
+
environments=get_agent_environments(db, str(agent_id)),
|
|
591
|
+
skill_ids=[ts["id"] for ts in skills],
|
|
592
|
+
skills=skills,
|
|
593
|
+
execution_environment=(
|
|
594
|
+
ExecutionEnvironment(**agent.execution_environment)
|
|
595
|
+
if agent.execution_environment
|
|
596
|
+
else None
|
|
597
|
+
),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
except HTTPException:
|
|
601
|
+
raise
|
|
602
|
+
except Exception as e:
|
|
603
|
+
db.rollback()
|
|
604
|
+
logger.error("agent_creation_failed", error=str(e), org_id=organization["id"])
|
|
605
|
+
raise HTTPException(
|
|
606
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
607
|
+
detail=f"Failed to create agent: {str(e)}"
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@router.get("", response_model=List[AgentResponse])
|
|
612
|
+
@instrument_endpoint("agents_v2.list_agents")
|
|
613
|
+
async def list_agents(
|
|
614
|
+
request: Request,
|
|
615
|
+
skip: int = 0,
|
|
616
|
+
limit: int = 100,
|
|
617
|
+
status_filter: str | None = None,
|
|
618
|
+
organization: dict = Depends(get_current_organization),
|
|
619
|
+
db: Session = Depends(get_db),
|
|
620
|
+
):
|
|
621
|
+
"""List all agents in the organization"""
|
|
622
|
+
try:
|
|
623
|
+
# Query agents for this organization
|
|
624
|
+
query = db.query(Agent).filter(Agent.organization_id == organization["id"])
|
|
625
|
+
|
|
626
|
+
if status_filter:
|
|
627
|
+
query = query.filter(Agent.status == status_filter)
|
|
628
|
+
|
|
629
|
+
agents_list = query.order_by(Agent.created_at.desc()).offset(skip).limit(limit).all()
|
|
630
|
+
|
|
631
|
+
if not agents_list:
|
|
632
|
+
return []
|
|
633
|
+
|
|
634
|
+
# Batch fetch all agent-project relationships in one query
|
|
635
|
+
agent_ids = [str(agent.id) for agent in agents_list]
|
|
636
|
+
from sqlalchemy import UUID as SQLUUID
|
|
637
|
+
agent_project_associations = (
|
|
638
|
+
db.query(ProjectAgent)
|
|
639
|
+
.join(Project, ProjectAgent.project_id == Project.id)
|
|
640
|
+
.filter(ProjectAgent.agent_id.in_([agent.id for agent in agents_list]))
|
|
641
|
+
.all()
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Group projects by agent_id
|
|
645
|
+
projects_by_agent = {}
|
|
646
|
+
for pa in agent_project_associations:
|
|
647
|
+
agent_id = str(pa.agent_id)
|
|
648
|
+
if pa.project:
|
|
649
|
+
if agent_id not in projects_by_agent:
|
|
650
|
+
projects_by_agent[agent_id] = []
|
|
651
|
+
projects_by_agent[agent_id].append({
|
|
652
|
+
"id": str(pa.project.id),
|
|
653
|
+
"name": pa.project.name,
|
|
654
|
+
"key": pa.project.key,
|
|
655
|
+
"description": pa.project.description,
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
# Batch fetch environments for all agents
|
|
659
|
+
agent_env_associations = (
|
|
660
|
+
db.query(AgentEnvironment)
|
|
661
|
+
.join(Environment, AgentEnvironment.environment_id == Environment.id)
|
|
662
|
+
.filter(AgentEnvironment.agent_id.in_([agent.id for agent in agents_list]))
|
|
663
|
+
.all()
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Group environments by agent_id
|
|
667
|
+
environments_by_agent = {}
|
|
668
|
+
for ae in agent_env_associations:
|
|
669
|
+
agent_id = str(ae.agent_id)
|
|
670
|
+
env = db.query(Environment).filter(Environment.id == ae.environment_id).first()
|
|
671
|
+
if env:
|
|
672
|
+
if agent_id not in environments_by_agent:
|
|
673
|
+
environments_by_agent[agent_id] = []
|
|
674
|
+
environments_by_agent[agent_id].append({
|
|
675
|
+
"id": str(env.id),
|
|
676
|
+
"name": env.name,
|
|
677
|
+
"display_name": env.display_name,
|
|
678
|
+
"status": env.status,
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
# Batch fetch skills for all agents (including team inheritance)
|
|
682
|
+
# Collect all unique team IDs
|
|
683
|
+
team_ids = set()
|
|
684
|
+
for agent in agents_list:
|
|
685
|
+
if agent.team_id:
|
|
686
|
+
team_ids.add(agent.team_id)
|
|
687
|
+
|
|
688
|
+
# BATCH FETCH: Get all team skills in one query
|
|
689
|
+
team_skills = {}
|
|
690
|
+
if team_ids:
|
|
691
|
+
team_skill_associations = (
|
|
692
|
+
db.query(SkillAssociation)
|
|
693
|
+
.join(Skill, SkillAssociation.skill_id == Skill.id)
|
|
694
|
+
.filter(
|
|
695
|
+
SkillAssociation.organization_id == organization["id"],
|
|
696
|
+
SkillAssociation.entity_type == "team",
|
|
697
|
+
SkillAssociation.entity_id.in_(team_ids)
|
|
698
|
+
)
|
|
699
|
+
.all()
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
for assoc in team_skill_associations:
|
|
703
|
+
team_id = str(assoc.entity_id)
|
|
704
|
+
skill = db.query(Skill).filter(Skill.id == assoc.skill_id).first()
|
|
705
|
+
if skill and skill.enabled:
|
|
706
|
+
if team_id not in team_skills:
|
|
707
|
+
team_skills[team_id] = []
|
|
708
|
+
|
|
709
|
+
config = skill.configuration or {}
|
|
710
|
+
override = assoc.configuration_override
|
|
711
|
+
if override:
|
|
712
|
+
config = {**config, **override}
|
|
713
|
+
|
|
714
|
+
team_skills[team_id].append({
|
|
715
|
+
"id": str(skill.id),
|
|
716
|
+
"name": skill.name,
|
|
717
|
+
"type": skill.skill_type,
|
|
718
|
+
"description": skill.description,
|
|
719
|
+
"enabled": skill.enabled,
|
|
720
|
+
"configuration": config,
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
# BATCH FETCH: Get all agent skills in one query
|
|
724
|
+
agent_skill_associations = (
|
|
725
|
+
db.query(SkillAssociation)
|
|
726
|
+
.join(Skill, SkillAssociation.skill_id == Skill.id)
|
|
727
|
+
.filter(
|
|
728
|
+
SkillAssociation.organization_id == organization["id"],
|
|
729
|
+
SkillAssociation.entity_type == "agent",
|
|
730
|
+
SkillAssociation.entity_id.in_([agent.id for agent in agents_list])
|
|
731
|
+
)
|
|
732
|
+
.all()
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
agent_direct_skills = {}
|
|
736
|
+
for assoc in agent_skill_associations:
|
|
737
|
+
agent_id = str(assoc.entity_id)
|
|
738
|
+
skill = db.query(Skill).filter(Skill.id == assoc.skill_id).first()
|
|
739
|
+
if skill and skill.enabled:
|
|
740
|
+
if agent_id not in agent_direct_skills:
|
|
741
|
+
agent_direct_skills[agent_id] = []
|
|
742
|
+
|
|
743
|
+
config = skill.configuration or {}
|
|
744
|
+
override = assoc.configuration_override
|
|
745
|
+
if override:
|
|
746
|
+
config = {**config, **override}
|
|
747
|
+
|
|
748
|
+
agent_direct_skills[agent_id].append({
|
|
749
|
+
"id": str(skill.id),
|
|
750
|
+
"name": skill.name,
|
|
751
|
+
"type": skill.skill_type,
|
|
752
|
+
"description": skill.description,
|
|
753
|
+
"enabled": skill.enabled,
|
|
754
|
+
"configuration": config,
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
# Combine team and agent skills with proper inheritance
|
|
758
|
+
skills_by_agent = {}
|
|
759
|
+
for agent in agents_list:
|
|
760
|
+
agent_id = str(agent.id)
|
|
761
|
+
team_id = str(agent.team_id) if agent.team_id else None
|
|
762
|
+
|
|
763
|
+
# Start with empty list
|
|
764
|
+
combined_skills = []
|
|
765
|
+
seen_ids = set()
|
|
766
|
+
|
|
767
|
+
# Add team skills first (if agent is part of a team)
|
|
768
|
+
if team_id and team_id in team_skills:
|
|
769
|
+
for skill in team_skills[team_id]:
|
|
770
|
+
if skill["id"] not in seen_ids:
|
|
771
|
+
combined_skills.append(skill)
|
|
772
|
+
seen_ids.add(skill["id"])
|
|
773
|
+
|
|
774
|
+
# Add agent-specific skills (these override team skills)
|
|
775
|
+
if agent_id in agent_direct_skills:
|
|
776
|
+
for skill in agent_direct_skills[agent_id]:
|
|
777
|
+
if skill["id"] not in seen_ids:
|
|
778
|
+
combined_skills.append(skill)
|
|
779
|
+
seen_ids.add(skill["id"])
|
|
780
|
+
|
|
781
|
+
skills_by_agent[agent_id] = combined_skills
|
|
782
|
+
|
|
783
|
+
agents = []
|
|
784
|
+
for agent in agents_list:
|
|
785
|
+
# Extract system_prompt from configuration
|
|
786
|
+
configuration = agent.configuration or {}
|
|
787
|
+
system_prompt = configuration.get("system_prompt")
|
|
788
|
+
|
|
789
|
+
agent_id = str(agent.id)
|
|
790
|
+
|
|
791
|
+
agents.append(AgentResponse(
|
|
792
|
+
id=agent_id,
|
|
793
|
+
organization_id=agent.organization_id,
|
|
794
|
+
name=agent.name,
|
|
795
|
+
description=agent.description,
|
|
796
|
+
system_prompt=system_prompt,
|
|
797
|
+
status=agent.status,
|
|
798
|
+
capabilities=agent.capabilities,
|
|
799
|
+
configuration=agent.configuration,
|
|
800
|
+
model_id=agent.model_id,
|
|
801
|
+
llm_config=agent.model_config or {},
|
|
802
|
+
runtime=agent.runtime,
|
|
803
|
+
runner_name=agent.runner_name,
|
|
804
|
+
team_id=str(agent.team_id) if agent.team_id else None,
|
|
805
|
+
created_at=agent.created_at.isoformat() if agent.created_at else None,
|
|
806
|
+
updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
|
|
807
|
+
last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
|
|
808
|
+
state=agent.state or {},
|
|
809
|
+
error_message=agent.error_message,
|
|
810
|
+
projects=projects_by_agent.get(agent_id, []),
|
|
811
|
+
environments=environments_by_agent.get(agent_id, []),
|
|
812
|
+
skill_ids=[ts["id"] for ts in skills_by_agent.get(agent_id, [])],
|
|
813
|
+
skills=skills_by_agent.get(agent_id, []),
|
|
814
|
+
execution_environment=(
|
|
815
|
+
ExecutionEnvironment(**agent.execution_environment)
|
|
816
|
+
if agent.execution_environment
|
|
817
|
+
else None
|
|
818
|
+
),
|
|
819
|
+
))
|
|
820
|
+
|
|
821
|
+
logger.info(
|
|
822
|
+
"agents_listed",
|
|
823
|
+
count=len(agents),
|
|
824
|
+
org_id=organization["id"],
|
|
825
|
+
org_slug=organization["slug"]
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
return agents
|
|
829
|
+
|
|
830
|
+
except Exception as e:
|
|
831
|
+
logger.error("agents_list_failed", error=str(e), org_id=organization["id"])
|
|
832
|
+
raise HTTPException(
|
|
833
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
834
|
+
detail=f"Failed to list agents: {str(e)}"
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
@router.get("/{agent_id}", response_model=AgentResponse)
|
|
839
|
+
@instrument_endpoint("agents_v2.get_agent")
|
|
840
|
+
async def get_agent(
|
|
841
|
+
agent_id: str,
|
|
842
|
+
request: Request,
|
|
843
|
+
organization: dict = Depends(get_current_organization),
|
|
844
|
+
db: Session = Depends(get_db),
|
|
845
|
+
):
|
|
846
|
+
"""Get a specific agent by ID"""
|
|
847
|
+
try:
|
|
848
|
+
# Query agent
|
|
849
|
+
agent = db.query(Agent).filter(
|
|
850
|
+
Agent.id == agent_id,
|
|
851
|
+
Agent.organization_id == organization["id"]
|
|
852
|
+
).first()
|
|
853
|
+
|
|
854
|
+
if not agent:
|
|
855
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
856
|
+
|
|
857
|
+
# DIAGNOSTIC: Log raw agent data from database
|
|
858
|
+
logger.info(
|
|
859
|
+
"get_agent_database_result",
|
|
860
|
+
agent_id=agent_id,
|
|
861
|
+
has_execution_environment=agent.execution_environment is not None,
|
|
862
|
+
execution_environment_type=type(agent.execution_environment).__name__ if agent.execution_environment else None,
|
|
863
|
+
execution_environment_keys=list(agent.execution_environment.keys()) if isinstance(agent.execution_environment, dict) else None,
|
|
864
|
+
has_mcp_servers=bool(agent.execution_environment.get("mcp_servers")) if isinstance(agent.execution_environment, dict) else False,
|
|
865
|
+
org_id=organization["id"]
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
# Get skills with team inheritance
|
|
869
|
+
team_id = str(agent.team_id) if agent.team_id else None
|
|
870
|
+
skills = get_agent_skills_with_inheritance(db, organization["id"], str(agent_id), team_id)
|
|
871
|
+
|
|
872
|
+
# Parse execution_environment if it exists
|
|
873
|
+
execution_env = None
|
|
874
|
+
if agent.execution_environment:
|
|
875
|
+
try:
|
|
876
|
+
execution_env = ExecutionEnvironment(**agent.execution_environment)
|
|
877
|
+
# DIAGNOSTIC: Log parsed execution_environment
|
|
878
|
+
logger.info(
|
|
879
|
+
"get_agent_execution_env_parsed",
|
|
880
|
+
agent_id=agent_id,
|
|
881
|
+
has_mcp_servers=bool(execution_env.mcp_servers) if execution_env else False,
|
|
882
|
+
mcp_server_count=len(execution_env.mcp_servers) if execution_env and execution_env.mcp_servers else 0,
|
|
883
|
+
org_id=organization["id"]
|
|
884
|
+
)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
logger.error(
|
|
887
|
+
"get_agent_execution_env_parse_failed",
|
|
888
|
+
agent_id=agent_id,
|
|
889
|
+
error=str(e),
|
|
890
|
+
raw_value=agent.execution_environment,
|
|
891
|
+
org_id=organization["id"]
|
|
892
|
+
)
|
|
893
|
+
execution_env = None
|
|
894
|
+
|
|
895
|
+
# Extract system_prompt from configuration
|
|
896
|
+
configuration = agent.configuration or {}
|
|
897
|
+
system_prompt = configuration.get("system_prompt")
|
|
898
|
+
|
|
899
|
+
return AgentResponse(
|
|
900
|
+
id=str(agent.id),
|
|
901
|
+
organization_id=agent.organization_id,
|
|
902
|
+
name=agent.name,
|
|
903
|
+
description=agent.description,
|
|
904
|
+
system_prompt=system_prompt,
|
|
905
|
+
status=agent.status,
|
|
906
|
+
capabilities=agent.capabilities,
|
|
907
|
+
configuration=agent.configuration,
|
|
908
|
+
model_id=agent.model_id,
|
|
909
|
+
llm_config=agent.model_config or {},
|
|
910
|
+
runtime=agent.runtime,
|
|
911
|
+
runner_name=agent.runner_name,
|
|
912
|
+
team_id=str(agent.team_id) if agent.team_id else None,
|
|
913
|
+
created_at=agent.created_at.isoformat() if agent.created_at else None,
|
|
914
|
+
updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
|
|
915
|
+
last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
|
|
916
|
+
state=agent.state or {},
|
|
917
|
+
error_message=agent.error_message,
|
|
918
|
+
projects=get_agent_projects(db, str(agent_id)),
|
|
919
|
+
environments=get_agent_environments(db, str(agent_id)),
|
|
920
|
+
skill_ids=[ts["id"] for ts in skills],
|
|
921
|
+
skills=skills,
|
|
922
|
+
execution_environment=execution_env,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
except HTTPException:
|
|
926
|
+
raise
|
|
927
|
+
except Exception as e:
|
|
928
|
+
logger.error("agent_get_failed", error=str(e), agent_id=agent_id)
|
|
929
|
+
raise HTTPException(
|
|
930
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
931
|
+
detail=f"Failed to get agent: {str(e)}"
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
@router.patch("/{agent_id}", response_model=AgentResponse)
|
|
936
|
+
@instrument_endpoint("agents_v2.update_agent")
|
|
937
|
+
async def update_agent(
|
|
938
|
+
agent_id: str,
|
|
939
|
+
agent_data: AgentUpdate,
|
|
940
|
+
request: Request,
|
|
941
|
+
organization: dict = Depends(get_current_organization),
|
|
942
|
+
db: Session = Depends(get_db),
|
|
943
|
+
):
|
|
944
|
+
"""Update an agent"""
|
|
945
|
+
try:
|
|
946
|
+
# Check if agent exists and belongs to organization
|
|
947
|
+
agent = db.query(Agent).filter(
|
|
948
|
+
Agent.id == agent_id,
|
|
949
|
+
Agent.organization_id == organization["id"]
|
|
950
|
+
).first()
|
|
951
|
+
|
|
952
|
+
if not agent:
|
|
953
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
954
|
+
|
|
955
|
+
# Build update dict
|
|
956
|
+
update_data = agent_data.model_dump(exclude_unset=True)
|
|
957
|
+
|
|
958
|
+
# DIAGNOSTIC: Log full request data
|
|
959
|
+
logger.info(
|
|
960
|
+
"update_agent_request",
|
|
961
|
+
agent_id=agent_id,
|
|
962
|
+
has_execution_environment="execution_environment" in update_data,
|
|
963
|
+
execution_environment_keys=list(update_data.get("execution_environment", {}).keys()) if isinstance(update_data.get("execution_environment"), dict) else None,
|
|
964
|
+
has_mcp_servers=bool(update_data.get("execution_environment", {}).get("mcp_servers")) if isinstance(update_data.get("execution_environment"), dict) else False,
|
|
965
|
+
mcp_server_count=len(update_data.get("execution_environment", {}).get("mcp_servers", {})) if isinstance(update_data.get("execution_environment"), dict) else 0,
|
|
966
|
+
org_id=organization["id"]
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
# Extract skill data before database update
|
|
970
|
+
skill_ids = update_data.pop("skill_ids", None)
|
|
971
|
+
skill_configurations = update_data.pop("skill_configurations", None)
|
|
972
|
+
|
|
973
|
+
# Extract environment data before database update (many-to-many via junction table)
|
|
974
|
+
environment_ids = update_data.pop("environment_ids", None)
|
|
975
|
+
|
|
976
|
+
# Extract system_prompt and store it in configuration
|
|
977
|
+
system_prompt = update_data.pop("system_prompt", None)
|
|
978
|
+
if system_prompt is not None:
|
|
979
|
+
# Merge system_prompt into existing configuration
|
|
980
|
+
existing_config = agent.configuration or {}
|
|
981
|
+
merged_config = {**existing_config, "system_prompt": system_prompt}
|
|
982
|
+
update_data["configuration"] = merged_config
|
|
983
|
+
|
|
984
|
+
# Handle model field - prefer 'model' over 'model_id' for backward compatibility
|
|
985
|
+
if "model" in update_data and update_data["model"]:
|
|
986
|
+
update_data["model_id"] = update_data.pop("model")
|
|
987
|
+
elif "model" in update_data:
|
|
988
|
+
# Remove null model field
|
|
989
|
+
update_data.pop("model")
|
|
990
|
+
|
|
991
|
+
# Map llm_config to model_config for database
|
|
992
|
+
if "llm_config" in update_data:
|
|
993
|
+
update_data["model_config"] = update_data.pop("llm_config")
|
|
994
|
+
|
|
995
|
+
# Validate model_id and runtime if being updated
|
|
996
|
+
if "model_id" in update_data or "runtime" in update_data:
|
|
997
|
+
# Merge updates with existing values
|
|
998
|
+
final_model_id = update_data.get("model_id", agent.model_id)
|
|
999
|
+
final_runtime = update_data.get("runtime", agent.runtime or "default")
|
|
1000
|
+
final_config = update_data.get("configuration", agent.configuration or {})
|
|
1001
|
+
|
|
1002
|
+
is_valid, errors = validate_agent_for_runtime(
|
|
1003
|
+
runtime_type=final_runtime,
|
|
1004
|
+
model_id=final_model_id,
|
|
1005
|
+
agent_config=final_config,
|
|
1006
|
+
system_prompt=system_prompt
|
|
1007
|
+
)
|
|
1008
|
+
if not is_valid:
|
|
1009
|
+
error_msg = "Agent validation failed:\n" + "\n".join(f" - {err}" for err in errors)
|
|
1010
|
+
logger.error(
|
|
1011
|
+
"agent_validation_failed",
|
|
1012
|
+
runtime=final_runtime,
|
|
1013
|
+
model_id=final_model_id,
|
|
1014
|
+
errors=errors,
|
|
1015
|
+
org_id=organization["id"]
|
|
1016
|
+
)
|
|
1017
|
+
raise HTTPException(
|
|
1018
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
1019
|
+
detail=error_msg
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Handle execution_environment - convert to dict if present
|
|
1023
|
+
if "execution_environment" in update_data and update_data["execution_environment"]:
|
|
1024
|
+
if isinstance(update_data["execution_environment"], ExecutionEnvironment):
|
|
1025
|
+
update_data["execution_environment"] = update_data["execution_environment"].model_dump(by_alias=True)
|
|
1026
|
+
# If None, keep as None to preserve existing value
|
|
1027
|
+
|
|
1028
|
+
# Validate MCP server configuration if being updated
|
|
1029
|
+
if "execution_environment" in update_data and update_data["execution_environment"]:
|
|
1030
|
+
exec_env_dict = update_data["execution_environment"]
|
|
1031
|
+
if exec_env_dict and exec_env_dict.get("mcp_servers"):
|
|
1032
|
+
try:
|
|
1033
|
+
mcp_validation = validate_execution_environment_mcp(
|
|
1034
|
+
exec_env_dict,
|
|
1035
|
+
strict=False
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
if not mcp_validation["valid"]:
|
|
1039
|
+
error_msg = "MCP configuration validation failed:\n" + "\n".join(
|
|
1040
|
+
f" - {err}" for err in mcp_validation["errors"]
|
|
1041
|
+
)
|
|
1042
|
+
logger.error(
|
|
1043
|
+
"mcp_validation_failed",
|
|
1044
|
+
agent_id=agent_id,
|
|
1045
|
+
errors=mcp_validation["errors"],
|
|
1046
|
+
org_id=organization["id"]
|
|
1047
|
+
)
|
|
1048
|
+
raise HTTPException(status_code=400, detail=error_msg)
|
|
1049
|
+
|
|
1050
|
+
if mcp_validation["warnings"]:
|
|
1051
|
+
logger.warning(
|
|
1052
|
+
"mcp_validation_warnings",
|
|
1053
|
+
agent_id=agent_id,
|
|
1054
|
+
warnings=mcp_validation["warnings"],
|
|
1055
|
+
required_secrets=mcp_validation.get("required_secrets", []),
|
|
1056
|
+
required_env_vars=mcp_validation.get("required_env_vars", []),
|
|
1057
|
+
org_id=organization["id"]
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
logger.info(
|
|
1061
|
+
"mcp_validation_passed",
|
|
1062
|
+
agent_id=agent_id,
|
|
1063
|
+
server_count=len(exec_env_dict.get("mcp_servers", {})),
|
|
1064
|
+
required_secrets=mcp_validation.get("required_secrets", []),
|
|
1065
|
+
required_env_vars=mcp_validation.get("required_env_vars", []),
|
|
1066
|
+
org_id=organization["id"]
|
|
1067
|
+
)
|
|
1068
|
+
except MCPValidationError as e:
|
|
1069
|
+
logger.error(
|
|
1070
|
+
"mcp_validation_error",
|
|
1071
|
+
agent_id=agent_id,
|
|
1072
|
+
error=str(e),
|
|
1073
|
+
org_id=organization["id"]
|
|
1074
|
+
)
|
|
1075
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1076
|
+
|
|
1077
|
+
# Note: skill_ids is not stored in agents table - skills are tracked via skill_associations junction table
|
|
1078
|
+
# The skill associations will be updated separately below if skill_ids was provided
|
|
1079
|
+
|
|
1080
|
+
update_data["updated_at"] = datetime.utcnow()
|
|
1081
|
+
|
|
1082
|
+
# DIAGNOSTIC: Log what's being sent to database
|
|
1083
|
+
logger.info(
|
|
1084
|
+
"update_agent_database_update",
|
|
1085
|
+
agent_id=agent_id,
|
|
1086
|
+
update_keys=list(update_data.keys()),
|
|
1087
|
+
has_execution_environment="execution_environment" in update_data,
|
|
1088
|
+
execution_environment_value=update_data.get("execution_environment"),
|
|
1089
|
+
org_id=organization["id"]
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
# Update agent fields
|
|
1093
|
+
for key, value in update_data.items():
|
|
1094
|
+
setattr(agent, key, value)
|
|
1095
|
+
|
|
1096
|
+
db.commit()
|
|
1097
|
+
db.refresh(agent)
|
|
1098
|
+
|
|
1099
|
+
# DIAGNOSTIC: Log database result
|
|
1100
|
+
logger.info(
|
|
1101
|
+
"update_agent_database_result",
|
|
1102
|
+
agent_id=agent_id,
|
|
1103
|
+
success=True,
|
|
1104
|
+
returned_execution_environment=agent.execution_environment,
|
|
1105
|
+
org_id=organization["id"]
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
# Update skill associations if skill_ids was provided
|
|
1109
|
+
if skill_ids is not None:
|
|
1110
|
+
# VALIDATION: Prevent removing all skills from an agent
|
|
1111
|
+
if len(skill_ids) == 0:
|
|
1112
|
+
raise HTTPException(
|
|
1113
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
1114
|
+
detail="Cannot remove all skills from an agent. At least one skill is required for agent functionality."
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
try:
|
|
1118
|
+
# Delete existing associations (scoped to organization)
|
|
1119
|
+
db.query(SkillAssociation).filter(
|
|
1120
|
+
SkillAssociation.organization_id == organization["id"],
|
|
1121
|
+
SkillAssociation.entity_type == "agent",
|
|
1122
|
+
SkillAssociation.entity_id == agent_id
|
|
1123
|
+
).delete()
|
|
1124
|
+
|
|
1125
|
+
# Create new associations
|
|
1126
|
+
now = datetime.utcnow()
|
|
1127
|
+
for skill_id in skill_ids:
|
|
1128
|
+
config_override = (skill_configurations or {}).get(skill_id, {})
|
|
1129
|
+
|
|
1130
|
+
skill_association = SkillAssociation(
|
|
1131
|
+
id=uuid.uuid4(),
|
|
1132
|
+
organization_id=organization["id"],
|
|
1133
|
+
skill_id=skill_id,
|
|
1134
|
+
entity_type="agent",
|
|
1135
|
+
entity_id=agent_id,
|
|
1136
|
+
configuration_override=config_override,
|
|
1137
|
+
created_at=now,
|
|
1138
|
+
)
|
|
1139
|
+
db.add(skill_association)
|
|
1140
|
+
|
|
1141
|
+
db.commit()
|
|
1142
|
+
logger.info(
|
|
1143
|
+
"agent_skills_updated",
|
|
1144
|
+
agent_id=agent_id,
|
|
1145
|
+
skill_count=len(skill_ids),
|
|
1146
|
+
org_id=organization["id"]
|
|
1147
|
+
)
|
|
1148
|
+
except Exception as e:
|
|
1149
|
+
db.rollback()
|
|
1150
|
+
logger.error(
|
|
1151
|
+
"failed_to_update_agent_skills",
|
|
1152
|
+
error=str(e),
|
|
1153
|
+
agent_id=agent_id,
|
|
1154
|
+
org_id=organization["id"]
|
|
1155
|
+
)
|
|
1156
|
+
raise HTTPException(
|
|
1157
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1158
|
+
detail=f"Failed to update agent skills: {str(e)}"
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
# Update environment associations if environment_ids was provided
|
|
1162
|
+
if environment_ids is not None:
|
|
1163
|
+
try:
|
|
1164
|
+
# Delete existing environment associations
|
|
1165
|
+
db.query(AgentEnvironment).filter(
|
|
1166
|
+
AgentEnvironment.agent_id == agent_id
|
|
1167
|
+
).delete()
|
|
1168
|
+
|
|
1169
|
+
# Create new environment associations
|
|
1170
|
+
for environment_id in environment_ids:
|
|
1171
|
+
agent_env = AgentEnvironment(
|
|
1172
|
+
id=uuid.uuid4(),
|
|
1173
|
+
agent_id=agent_id,
|
|
1174
|
+
environment_id=environment_id,
|
|
1175
|
+
organization_id=organization["id"],
|
|
1176
|
+
assigned_at=datetime.utcnow(),
|
|
1177
|
+
)
|
|
1178
|
+
db.add(agent_env)
|
|
1179
|
+
|
|
1180
|
+
db.commit()
|
|
1181
|
+
logger.info(
|
|
1182
|
+
"agent_environments_updated",
|
|
1183
|
+
agent_id=agent_id,
|
|
1184
|
+
environment_count=len(environment_ids),
|
|
1185
|
+
org_id=organization["id"]
|
|
1186
|
+
)
|
|
1187
|
+
except Exception as e:
|
|
1188
|
+
db.rollback()
|
|
1189
|
+
logger.warning(
|
|
1190
|
+
"failed_to_update_agent_environments",
|
|
1191
|
+
error=str(e),
|
|
1192
|
+
agent_id=agent_id,
|
|
1193
|
+
org_id=organization["id"]
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
logger.info(
|
|
1197
|
+
"agent_updated",
|
|
1198
|
+
agent_id=agent_id,
|
|
1199
|
+
org_id=organization["id"],
|
|
1200
|
+
fields_updated=list(update_data.keys())
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
# Get skills with team inheritance
|
|
1204
|
+
team_id = str(agent.team_id) if agent.team_id else None
|
|
1205
|
+
skills = get_agent_skills_with_inheritance(db, organization["id"], agent_id, team_id)
|
|
1206
|
+
|
|
1207
|
+
# Parse execution_environment if it exists
|
|
1208
|
+
execution_env = None
|
|
1209
|
+
if agent.execution_environment:
|
|
1210
|
+
try:
|
|
1211
|
+
execution_env = ExecutionEnvironment(**agent.execution_environment)
|
|
1212
|
+
except Exception:
|
|
1213
|
+
execution_env = None
|
|
1214
|
+
|
|
1215
|
+
# Extract system_prompt from configuration
|
|
1216
|
+
configuration = agent.configuration or {}
|
|
1217
|
+
system_prompt = configuration.get("system_prompt")
|
|
1218
|
+
|
|
1219
|
+
return AgentResponse(
|
|
1220
|
+
id=str(agent.id),
|
|
1221
|
+
organization_id=agent.organization_id,
|
|
1222
|
+
name=agent.name,
|
|
1223
|
+
description=agent.description,
|
|
1224
|
+
system_prompt=system_prompt,
|
|
1225
|
+
status=agent.status,
|
|
1226
|
+
capabilities=agent.capabilities,
|
|
1227
|
+
configuration=agent.configuration,
|
|
1228
|
+
model_id=agent.model_id,
|
|
1229
|
+
llm_config=agent.model_config or {},
|
|
1230
|
+
runtime=agent.runtime,
|
|
1231
|
+
runner_name=agent.runner_name,
|
|
1232
|
+
team_id=str(agent.team_id) if agent.team_id else None,
|
|
1233
|
+
created_at=agent.created_at.isoformat() if agent.created_at else None,
|
|
1234
|
+
updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
|
|
1235
|
+
last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
|
|
1236
|
+
state=agent.state or {},
|
|
1237
|
+
error_message=agent.error_message,
|
|
1238
|
+
projects=get_agent_projects(db, agent_id),
|
|
1239
|
+
environments=get_agent_environments(db, agent_id),
|
|
1240
|
+
skill_ids=[ts["id"] for ts in skills],
|
|
1241
|
+
skills=skills,
|
|
1242
|
+
execution_environment=execution_env,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
except HTTPException:
|
|
1246
|
+
raise
|
|
1247
|
+
except Exception as e:
|
|
1248
|
+
db.rollback()
|
|
1249
|
+
logger.error("agent_update_failed", error=str(e), agent_id=agent_id)
|
|
1250
|
+
raise HTTPException(
|
|
1251
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1252
|
+
detail=f"Failed to update agent: {str(e)}"
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
@router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
1257
|
+
@instrument_endpoint("agents_v2.delete_agent")
|
|
1258
|
+
async def delete_agent(
|
|
1259
|
+
agent_id: str,
|
|
1260
|
+
request: Request,
|
|
1261
|
+
organization: dict = Depends(get_current_organization),
|
|
1262
|
+
db: Session = Depends(get_db),
|
|
1263
|
+
):
|
|
1264
|
+
"""Delete an agent"""
|
|
1265
|
+
try:
|
|
1266
|
+
# Find the agent first
|
|
1267
|
+
agent = db.query(Agent).filter(
|
|
1268
|
+
Agent.id == agent_id,
|
|
1269
|
+
Agent.organization_id == organization["id"]
|
|
1270
|
+
).first()
|
|
1271
|
+
|
|
1272
|
+
if not agent:
|
|
1273
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
1274
|
+
|
|
1275
|
+
# Delete the agent (cascading deletes will handle related records)
|
|
1276
|
+
db.delete(agent)
|
|
1277
|
+
db.commit()
|
|
1278
|
+
|
|
1279
|
+
logger.info("agent_deleted", agent_id=agent_id, org_id=organization["id"])
|
|
1280
|
+
|
|
1281
|
+
return None
|
|
1282
|
+
|
|
1283
|
+
except HTTPException:
|
|
1284
|
+
raise
|
|
1285
|
+
except Exception as e:
|
|
1286
|
+
logger.error("agent_delete_failed", error=str(e), agent_id=agent_id)
|
|
1287
|
+
raise HTTPException(
|
|
1288
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1289
|
+
detail=f"Failed to delete agent: {str(e)}"
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
@router.post("/{agent_id}/execute", response_model=AgentExecutionResponse)
|
|
1294
|
+
@instrument_endpoint("agents_v2.execute_agent")
|
|
1295
|
+
async def execute_agent(
|
|
1296
|
+
agent_id: str,
|
|
1297
|
+
execution_request: AgentExecutionRequest,
|
|
1298
|
+
request: Request,
|
|
1299
|
+
organization: dict = Depends(get_current_organization),
|
|
1300
|
+
db: Session = Depends(get_db),
|
|
1301
|
+
):
|
|
1302
|
+
"""
|
|
1303
|
+
Execute an agent by submitting to Temporal workflow.
|
|
1304
|
+
|
|
1305
|
+
This creates an execution record and starts a Temporal workflow.
|
|
1306
|
+
The actual execution happens asynchronously on the Temporal worker.
|
|
1307
|
+
|
|
1308
|
+
The runner_name should come from the Composer UI where user selects
|
|
1309
|
+
from available runners (fetched from Kubiya API /api/v1/runners).
|
|
1310
|
+
"""
|
|
1311
|
+
try:
|
|
1312
|
+
# Get agent details
|
|
1313
|
+
agent = db.query(Agent).filter(
|
|
1314
|
+
Agent.id == agent_id,
|
|
1315
|
+
Agent.organization_id == organization["id"]
|
|
1316
|
+
).first()
|
|
1317
|
+
|
|
1318
|
+
if not agent:
|
|
1319
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
1320
|
+
|
|
1321
|
+
# Create execution record
|
|
1322
|
+
execution_id = uuid.uuid4()
|
|
1323
|
+
now = datetime.utcnow()
|
|
1324
|
+
|
|
1325
|
+
# Validate and get worker queue
|
|
1326
|
+
worker_queue_id = execution_request.worker_queue_id
|
|
1327
|
+
|
|
1328
|
+
worker_queue = db.query(WorkerQueue).filter(
|
|
1329
|
+
WorkerQueue.id == worker_queue_id,
|
|
1330
|
+
WorkerQueue.organization_id == organization["id"]
|
|
1331
|
+
).first()
|
|
1332
|
+
|
|
1333
|
+
if not worker_queue:
|
|
1334
|
+
raise HTTPException(
|
|
1335
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
1336
|
+
detail=f"Worker queue '{worker_queue_id}' not found. Please select a valid worker queue."
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# Check if queue has active workers
|
|
1340
|
+
if worker_queue.status != "active":
|
|
1341
|
+
raise HTTPException(
|
|
1342
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
1343
|
+
detail=f"Worker queue '{worker_queue.name}' is not active"
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
# Extract user metadata - ALWAYS use JWT-decoded organization data as source of truth
|
|
1347
|
+
user_metadata = execution_request.user_metadata or {}
|
|
1348
|
+
# Override with JWT data (user can't spoof their identity)
|
|
1349
|
+
user_metadata["user_id"] = organization.get("user_id")
|
|
1350
|
+
user_metadata["user_email"] = organization.get("user_email")
|
|
1351
|
+
user_metadata["user_name"] = organization.get("user_name")
|
|
1352
|
+
# Keep user_avatar from request if provided (not in JWT)
|
|
1353
|
+
if not user_metadata.get("user_avatar"):
|
|
1354
|
+
user_metadata["user_avatar"] = None
|
|
1355
|
+
|
|
1356
|
+
execution = Execution(
|
|
1357
|
+
id=execution_id,
|
|
1358
|
+
organization_id=organization["id"],
|
|
1359
|
+
execution_type=ExecutionType.AGENT,
|
|
1360
|
+
entity_id=agent_id,
|
|
1361
|
+
entity_name=agent.name,
|
|
1362
|
+
prompt=execution_request.prompt,
|
|
1363
|
+
system_prompt=execution_request.system_prompt,
|
|
1364
|
+
status=ExecutionStatus.PENDING,
|
|
1365
|
+
worker_queue_id=worker_queue_id,
|
|
1366
|
+
runner_name=worker_queue.name, # Store queue name for display
|
|
1367
|
+
user_id=user_metadata.get("user_id"),
|
|
1368
|
+
user_name=user_metadata.get("user_name"),
|
|
1369
|
+
user_email=user_metadata.get("user_email"),
|
|
1370
|
+
user_avatar=user_metadata.get("user_avatar"),
|
|
1371
|
+
usage={},
|
|
1372
|
+
execution_metadata={
|
|
1373
|
+
"kubiya_org_id": organization["id"],
|
|
1374
|
+
"kubiya_org_name": organization["name"],
|
|
1375
|
+
"worker_queue_name": worker_queue.display_name or worker_queue.name,
|
|
1376
|
+
},
|
|
1377
|
+
created_at=now,
|
|
1378
|
+
updated_at=now,
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
db.add(execution)
|
|
1382
|
+
db.commit()
|
|
1383
|
+
db.refresh(execution)
|
|
1384
|
+
|
|
1385
|
+
# Add creator as the first participant (owner role) for multiplayer support
|
|
1386
|
+
user_id = user_metadata.get("user_id")
|
|
1387
|
+
if user_id:
|
|
1388
|
+
try:
|
|
1389
|
+
participant = ExecutionParticipant(
|
|
1390
|
+
id=uuid.uuid4(),
|
|
1391
|
+
execution_id=execution_id,
|
|
1392
|
+
organization_id=organization["id"],
|
|
1393
|
+
user_id=user_id,
|
|
1394
|
+
user_name=user_metadata.get("user_name"),
|
|
1395
|
+
user_email=user_metadata.get("user_email"),
|
|
1396
|
+
user_avatar=user_metadata.get("user_avatar"),
|
|
1397
|
+
role=ParticipantRole.OWNER,
|
|
1398
|
+
)
|
|
1399
|
+
db.add(participant)
|
|
1400
|
+
db.commit()
|
|
1401
|
+
logger.info(
|
|
1402
|
+
"owner_participant_added",
|
|
1403
|
+
execution_id=str(execution_id),
|
|
1404
|
+
user_id=user_id,
|
|
1405
|
+
)
|
|
1406
|
+
except Exception as participant_error:
|
|
1407
|
+
db.rollback()
|
|
1408
|
+
logger.warning(
|
|
1409
|
+
"failed_to_add_owner_participant",
|
|
1410
|
+
error=str(participant_error),
|
|
1411
|
+
execution_id=str(execution_id),
|
|
1412
|
+
)
|
|
1413
|
+
# Don't fail execution creation if participant tracking fails
|
|
1414
|
+
|
|
1415
|
+
# Get resolved execution environment with templates compiled
|
|
1416
|
+
# This includes MCP servers with all {{.secret.x}} and {{.env.X}} resolved
|
|
1417
|
+
# Call internal function directly to avoid HTTP/auth issues
|
|
1418
|
+
resolved_env = {} # Initialize to empty dict to avoid UnboundLocalError
|
|
1419
|
+
try:
|
|
1420
|
+
from control_plane_api.app.routers.execution_environment import resolve_agent_execution_environment_internal
|
|
1421
|
+
|
|
1422
|
+
# Get token from request
|
|
1423
|
+
token = request.state.kubiya_token
|
|
1424
|
+
|
|
1425
|
+
resolved_env = await resolve_agent_execution_environment_internal(
|
|
1426
|
+
agent_id=agent_id,
|
|
1427
|
+
org_id=organization["id"],
|
|
1428
|
+
db=db,
|
|
1429
|
+
token=token
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
mcp_servers = resolved_env.get("mcp_servers", {})
|
|
1433
|
+
resolved_system_prompt = resolved_env.get("system_prompt")
|
|
1434
|
+
resolved_description = resolved_env.get("description")
|
|
1435
|
+
|
|
1436
|
+
# DEBUG: Log detailed MCP server info
|
|
1437
|
+
logger.info(
|
|
1438
|
+
"🔍 DEBUG: execution_environment_resolved_for_execution",
|
|
1439
|
+
agent_id=agent_id[:8],
|
|
1440
|
+
mcp_server_count=len(mcp_servers),
|
|
1441
|
+
mcp_server_names=list(mcp_servers.keys()) if mcp_servers else [],
|
|
1442
|
+
has_resolved_prompt=bool(resolved_system_prompt)
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
if mcp_servers:
|
|
1446
|
+
for server_name, server_config in mcp_servers.items():
|
|
1447
|
+
logger.info(
|
|
1448
|
+
"🔍 DEBUG: MCP server config from API",
|
|
1449
|
+
server_name=server_name,
|
|
1450
|
+
has_url="url" in server_config,
|
|
1451
|
+
has_headers="headers" in server_config,
|
|
1452
|
+
has_transport="transport_type" in server_config or "type" in server_config
|
|
1453
|
+
)
|
|
1454
|
+
else:
|
|
1455
|
+
logger.warning(
|
|
1456
|
+
"🔍 DEBUG: NO MCP SERVERS returned from execution env resolution",
|
|
1457
|
+
agent_id=agent_id[:8],
|
|
1458
|
+
resolved_env_keys=list(resolved_env.keys())
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
except Exception as e:
|
|
1462
|
+
logger.error(
|
|
1463
|
+
"execution_environment_resolution_error",
|
|
1464
|
+
agent_id=agent_id[:8],
|
|
1465
|
+
error=str(e),
|
|
1466
|
+
exc_info=True
|
|
1467
|
+
)
|
|
1468
|
+
# Don't fallback to old configuration.mcpServers format
|
|
1469
|
+
# MCP servers should only come from execution_environment.mcp_servers
|
|
1470
|
+
agent_configuration = agent.configuration or {}
|
|
1471
|
+
if "mcpServers" in agent_configuration:
|
|
1472
|
+
logger.warning(
|
|
1473
|
+
"ignoring_legacy_mcp_servers_in_configuration",
|
|
1474
|
+
agent_id=agent_id[:8],
|
|
1475
|
+
legacy_servers=list(agent_configuration.get("mcpServers", {}).keys()),
|
|
1476
|
+
recommendation="Move MCP servers to execution_environment.mcp_servers"
|
|
1477
|
+
)
|
|
1478
|
+
mcp_servers = {} # Don't use old format
|
|
1479
|
+
resolved_system_prompt = None
|
|
1480
|
+
resolved_description = None
|
|
1481
|
+
|
|
1482
|
+
# Use resolved system prompt if available, otherwise use original
|
|
1483
|
+
agent_configuration = agent.configuration or {}
|
|
1484
|
+
|
|
1485
|
+
# Override agent_config with execution_environment.working_dir if provided
|
|
1486
|
+
if execution_request.execution_environment and execution_request.execution_environment.working_dir:
|
|
1487
|
+
agent_configuration = agent_configuration.copy()
|
|
1488
|
+
agent_configuration["cwd"] = execution_request.execution_environment.working_dir
|
|
1489
|
+
logger.info(
|
|
1490
|
+
"execution_working_dir_override",
|
|
1491
|
+
execution_id=str(execution_id),
|
|
1492
|
+
working_dir=execution_request.execution_environment.working_dir,
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
# Submit to Temporal workflow
|
|
1496
|
+
# Task queue is the worker queue UUID
|
|
1497
|
+
task_queue = str(worker_queue_id)
|
|
1498
|
+
|
|
1499
|
+
# Use shared agent execution Temporal client (from env vars)
|
|
1500
|
+
# Agent executions run in a shared namespace (agent-control-plane.lpagu in us-east-1)
|
|
1501
|
+
# NOT in org-specific namespaces like the plan worker
|
|
1502
|
+
from control_plane_api.app.lib.temporal_client import get_temporal_client
|
|
1503
|
+
|
|
1504
|
+
temporal_client = await get_temporal_client()
|
|
1505
|
+
|
|
1506
|
+
# Start workflow
|
|
1507
|
+
# Use resolved system prompt (with templates compiled) if available
|
|
1508
|
+
# Priority: request > resolved > configuration > agent.system_prompt
|
|
1509
|
+
system_prompt = (
|
|
1510
|
+
execution_request.system_prompt or
|
|
1511
|
+
resolved_system_prompt or
|
|
1512
|
+
agent_configuration.get("system_prompt") or
|
|
1513
|
+
agent.system_prompt
|
|
1514
|
+
)
|
|
1515
|
+
|
|
1516
|
+
# Get API key from Authorization header
|
|
1517
|
+
auth_header = request.headers.get("authorization", "")
|
|
1518
|
+
api_key = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None
|
|
1519
|
+
|
|
1520
|
+
# Get control plane URL from request
|
|
1521
|
+
control_plane_url = str(request.base_url).rstrip("/")
|
|
1522
|
+
|
|
1523
|
+
# CRITICAL: Use real-time timestamp for initial message to ensure chronological ordering
|
|
1524
|
+
# This prevents timestamp mismatches between initial and follow-up messages
|
|
1525
|
+
initial_timestamp = datetime.now(timezone.utc).isoformat()
|
|
1526
|
+
|
|
1527
|
+
workflow_input = AgentExecutionInput(
|
|
1528
|
+
execution_id=str(execution_id),
|
|
1529
|
+
agent_id=str(agent_id),
|
|
1530
|
+
organization_id=organization["id"],
|
|
1531
|
+
prompt=execution_request.prompt,
|
|
1532
|
+
system_prompt=system_prompt,
|
|
1533
|
+
model_id=agent.model_id,
|
|
1534
|
+
model_config=agent.model_config or {},
|
|
1535
|
+
agent_config=agent_configuration,
|
|
1536
|
+
mcp_servers=mcp_servers,
|
|
1537
|
+
user_metadata=user_metadata,
|
|
1538
|
+
runtime_type=agent.runtime or "default",
|
|
1539
|
+
control_plane_url=control_plane_url,
|
|
1540
|
+
api_key=api_key,
|
|
1541
|
+
initial_message_timestamp=initial_timestamp,
|
|
1542
|
+
graph_api_url=resolved_env.get("graph_api_url"),
|
|
1543
|
+
dataset_name=resolved_env.get("dataset_name"),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
# DEBUG: Log workflow input MCP servers
|
|
1547
|
+
logger.info(
|
|
1548
|
+
"🔍 DEBUG: Starting workflow with MCP servers",
|
|
1549
|
+
execution_id=str(execution_id),
|
|
1550
|
+
mcp_servers_count=len(mcp_servers),
|
|
1551
|
+
mcp_servers_type=str(type(mcp_servers)),
|
|
1552
|
+
mcp_server_names=list(mcp_servers.keys()) if mcp_servers else []
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
workflow_handle = await temporal_client.start_workflow(
|
|
1556
|
+
AgentExecutionWorkflow.run,
|
|
1557
|
+
workflow_input,
|
|
1558
|
+
id=f"agent-execution-{execution_id}",
|
|
1559
|
+
task_queue=task_queue,
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
# Get namespace from env for logging (agent execution uses shared namespace)
|
|
1563
|
+
temporal_namespace = os.getenv("TEMPORAL_NAMESPACE", "agent-control-plane.lpagu")
|
|
1564
|
+
|
|
1565
|
+
logger.info(
|
|
1566
|
+
"agent_execution_submitted",
|
|
1567
|
+
execution_id=str(execution_id),
|
|
1568
|
+
agent_id=str(agent_id),
|
|
1569
|
+
workflow_id=workflow_handle.id,
|
|
1570
|
+
task_queue=task_queue,
|
|
1571
|
+
temporal_namespace=temporal_namespace,
|
|
1572
|
+
worker_queue_id=str(worker_queue_id),
|
|
1573
|
+
worker_queue_name=worker_queue.name,
|
|
1574
|
+
org_id=organization["id"],
|
|
1575
|
+
org_name=organization["name"],
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
return AgentExecutionResponse(
|
|
1579
|
+
execution_id=str(execution_id),
|
|
1580
|
+
workflow_id=workflow_handle.id,
|
|
1581
|
+
status="PENDING",
|
|
1582
|
+
message=f"Execution submitted to worker queue: {worker_queue.name}",
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
except HTTPException:
|
|
1586
|
+
raise
|
|
1587
|
+
except Exception as e:
|
|
1588
|
+
db.rollback()
|
|
1589
|
+
logger.error(
|
|
1590
|
+
"agent_execution_failed",
|
|
1591
|
+
error=str(e),
|
|
1592
|
+
agent_id=str(agent_id),
|
|
1593
|
+
org_id=organization["id"]
|
|
1594
|
+
)
|
|
1595
|
+
raise HTTPException(
|
|
1596
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1597
|
+
detail=f"Failed to execute agent: {str(e)}"
|
|
1598
|
+
)
|