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,1010 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-tenant skills router.
|
|
3
|
+
|
|
4
|
+
This router handles skill CRUD operations and associations with agents/teams/environments.
|
|
5
|
+
All operations are scoped to the authenticated organization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
import structlog
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from control_plane_api.app.middleware.auth import get_current_organization
|
|
16
|
+
from control_plane_api.app.database import get_db
|
|
17
|
+
from sqlalchemy.orm import Session
|
|
18
|
+
from sqlalchemy import desc
|
|
19
|
+
from sqlalchemy.inspection import inspect
|
|
20
|
+
from control_plane_api.app.models.skill import Skill, SkillAssociation
|
|
21
|
+
from control_plane_api.app.models.agent import Agent
|
|
22
|
+
from control_plane_api.app.models.team import Team
|
|
23
|
+
from control_plane_api.app.models.environment import Environment
|
|
24
|
+
from control_plane_api.app.models.associations import AgentEnvironment, TeamEnvironment
|
|
25
|
+
from control_plane_api.app.lib.kubiya_client import get_kubiya_client
|
|
26
|
+
from control_plane_api.app.skills import get_all_skills, get_skill, SkillType
|
|
27
|
+
|
|
28
|
+
logger = structlog.get_logger()
|
|
29
|
+
|
|
30
|
+
router = APIRouter()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Pydantic schemas
|
|
34
|
+
class ToolSetConfiguration(BaseModel):
|
|
35
|
+
"""Configuration for a skill"""
|
|
36
|
+
# File System
|
|
37
|
+
base_dir: Optional[str] = None
|
|
38
|
+
enable_save_file: Optional[bool] = None
|
|
39
|
+
enable_read_file: Optional[bool] = None
|
|
40
|
+
enable_list_files: Optional[bool] = None
|
|
41
|
+
enable_search_files: Optional[bool] = None
|
|
42
|
+
|
|
43
|
+
# Shell
|
|
44
|
+
allowed_commands: Optional[List[str]] = None
|
|
45
|
+
blocked_commands: Optional[List[str]] = None
|
|
46
|
+
timeout: Optional[int] = None
|
|
47
|
+
|
|
48
|
+
# Docker
|
|
49
|
+
enable_container_management: Optional[bool] = None
|
|
50
|
+
enable_image_management: Optional[bool] = None
|
|
51
|
+
enable_volume_management: Optional[bool] = None
|
|
52
|
+
enable_network_management: Optional[bool] = None
|
|
53
|
+
|
|
54
|
+
# Python
|
|
55
|
+
enable_code_execution: Optional[bool] = None
|
|
56
|
+
allowed_imports: Optional[List[str]] = None
|
|
57
|
+
blocked_imports: Optional[List[str]] = None
|
|
58
|
+
|
|
59
|
+
# File Generation
|
|
60
|
+
enable_json_generation: Optional[bool] = None
|
|
61
|
+
enable_csv_generation: Optional[bool] = None
|
|
62
|
+
enable_pdf_generation: Optional[bool] = None
|
|
63
|
+
enable_txt_generation: Optional[bool] = None
|
|
64
|
+
output_directory: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
# Data Visualization
|
|
67
|
+
max_diagram_size: Optional[int] = None
|
|
68
|
+
enable_flowchart: Optional[bool] = None
|
|
69
|
+
enable_sequence: Optional[bool] = None
|
|
70
|
+
enable_class_diagram: Optional[bool] = None
|
|
71
|
+
enable_er_diagram: Optional[bool] = None
|
|
72
|
+
enable_gantt: Optional[bool] = None
|
|
73
|
+
enable_pie_chart: Optional[bool] = None
|
|
74
|
+
enable_state_diagram: Optional[bool] = None
|
|
75
|
+
enable_git_graph: Optional[bool] = None
|
|
76
|
+
enable_user_journey: Optional[bool] = None
|
|
77
|
+
enable_quadrant_chart: Optional[bool] = None
|
|
78
|
+
|
|
79
|
+
# Workflow Executor
|
|
80
|
+
workflow_type: Optional[str] = Field(None, description="Workflow type: 'json' or 'python_dsl'")
|
|
81
|
+
workflow_definition: Optional[str] = Field(None, description="JSON workflow definition as string")
|
|
82
|
+
python_dsl_code: Optional[str] = Field(None, description="Python DSL code for workflow")
|
|
83
|
+
validation_enabled: Optional[bool] = Field(None, description="Enable workflow validation")
|
|
84
|
+
default_runner: Optional[str] = Field(None, description="Default runner/environment name")
|
|
85
|
+
|
|
86
|
+
# Custom
|
|
87
|
+
custom_class: Optional[str] = None
|
|
88
|
+
custom_config: Optional[dict] = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ToolSetCreate(BaseModel):
|
|
92
|
+
name: str = Field(..., description="Skill name")
|
|
93
|
+
type: str = Field(..., description="Skill type (file_system, shell, docker, python, etc.)")
|
|
94
|
+
description: Optional[str] = Field(None, description="Skill description")
|
|
95
|
+
icon: Optional[str] = Field("Wrench", description="Icon name")
|
|
96
|
+
enabled: bool = Field(True, description="Whether skill is enabled")
|
|
97
|
+
configuration: ToolSetConfiguration = Field(default_factory=ToolSetConfiguration)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ToolSetUpdate(BaseModel):
|
|
101
|
+
name: Optional[str] = None
|
|
102
|
+
description: Optional[str] = None
|
|
103
|
+
icon: Optional[str] = None
|
|
104
|
+
enabled: Optional[bool] = None
|
|
105
|
+
configuration: Optional[ToolSetConfiguration] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ToolSetResponse(BaseModel):
|
|
109
|
+
id: str
|
|
110
|
+
organization_id: str
|
|
111
|
+
name: str
|
|
112
|
+
type: str # Aliased from skill_type in SQL query
|
|
113
|
+
description: Optional[str]
|
|
114
|
+
icon: str
|
|
115
|
+
enabled: bool
|
|
116
|
+
configuration: dict
|
|
117
|
+
created_at: datetime
|
|
118
|
+
updated_at: datetime
|
|
119
|
+
|
|
120
|
+
@field_validator("id", mode="before")
|
|
121
|
+
def ensure_id_is_string(cls, v):
|
|
122
|
+
if v is None:
|
|
123
|
+
return None
|
|
124
|
+
return str(v)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ToolSetAssociationCreate(BaseModel):
|
|
128
|
+
skill_id: str = Field(..., description="Skill ID to associate")
|
|
129
|
+
configuration_override: Optional[ToolSetConfiguration] = Field(None, description="Entity-specific config overrides")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ResolvedToolSet(BaseModel):
|
|
133
|
+
id: str
|
|
134
|
+
name: str
|
|
135
|
+
type: str
|
|
136
|
+
description: Optional[str]
|
|
137
|
+
icon: str
|
|
138
|
+
enabled: bool
|
|
139
|
+
configuration: dict
|
|
140
|
+
source: str # "environment", "team", "agent"
|
|
141
|
+
inherited: bool
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Helper functions
|
|
145
|
+
def get_skill_by_id(db: Session, organization_id: str, skill_id: str) -> dict:
|
|
146
|
+
"""Get a skill by ID, scoped to organization"""
|
|
147
|
+
skill = db.query(Skill).filter(
|
|
148
|
+
Skill.organization_id == organization_id,
|
|
149
|
+
Skill.id == skill_id
|
|
150
|
+
).first()
|
|
151
|
+
|
|
152
|
+
if not skill:
|
|
153
|
+
raise HTTPException(status_code=404, detail=f"Skill {skill_id} not found")
|
|
154
|
+
|
|
155
|
+
# Convert to dict and alias skill_type as type
|
|
156
|
+
skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
|
|
157
|
+
# Convert UUID fields to strings for Pydantic validation
|
|
158
|
+
if "id" in skill_dict and skill_dict["id"] is not None:
|
|
159
|
+
skill_dict["id"] = str(skill_dict["id"])
|
|
160
|
+
skill_dict["type"] = skill_dict.pop("skill_type")
|
|
161
|
+
return skill_dict
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_entity_skills(db: Session, organization_id: str, entity_type: str, entity_id: str) -> List[dict]:
|
|
165
|
+
"""Get skills associated with an entity"""
|
|
166
|
+
# Get associations with joined skills
|
|
167
|
+
associations = db.query(SkillAssociation).join(Skill).filter(
|
|
168
|
+
SkillAssociation.organization_id == organization_id,
|
|
169
|
+
SkillAssociation.entity_type == entity_type,
|
|
170
|
+
SkillAssociation.entity_id == entity_id,
|
|
171
|
+
Skill.enabled == True
|
|
172
|
+
).all()
|
|
173
|
+
|
|
174
|
+
skills = []
|
|
175
|
+
for assoc in associations:
|
|
176
|
+
skill = assoc.skill
|
|
177
|
+
# Convert skill to dict and alias skill_type as type
|
|
178
|
+
skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
|
|
179
|
+
# Convert UUID fields to strings for Pydantic validation
|
|
180
|
+
if "id" in skill_dict and skill_dict["id"] is not None:
|
|
181
|
+
skill_dict["id"] = str(skill_dict["id"])
|
|
182
|
+
skill_dict["type"] = skill_dict.pop("skill_type")
|
|
183
|
+
|
|
184
|
+
# Merge configuration with override
|
|
185
|
+
config = skill_dict.get("configuration", {})
|
|
186
|
+
override = assoc.configuration_override
|
|
187
|
+
if override:
|
|
188
|
+
config = {**config, **override}
|
|
189
|
+
|
|
190
|
+
skill_dict["configuration"] = config
|
|
191
|
+
skills.append(skill_dict)
|
|
192
|
+
|
|
193
|
+
return skills
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def merge_configurations(base: dict, override: dict) -> dict:
|
|
197
|
+
"""Merge two configuration dictionaries, with override taking precedence"""
|
|
198
|
+
result = base.copy()
|
|
199
|
+
for key, value in override.items():
|
|
200
|
+
if value is not None:
|
|
201
|
+
result[key] = value
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def validate_workflow_runner(config: dict, token: str, org_id: str) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Validate that runners specified in workflow configuration exist.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
config: Workflow executor configuration
|
|
211
|
+
token: Kubiya API token
|
|
212
|
+
org_id: Organization ID
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
HTTPException: If runner validation fails
|
|
216
|
+
"""
|
|
217
|
+
import json as json_lib
|
|
218
|
+
|
|
219
|
+
# Extract runners to validate
|
|
220
|
+
runners_to_check = []
|
|
221
|
+
|
|
222
|
+
# Check default_runner
|
|
223
|
+
if config.get("default_runner"):
|
|
224
|
+
runners_to_check.append(("default_runner", config["default_runner"]))
|
|
225
|
+
|
|
226
|
+
# Check workflow-level runner in JSON workflows
|
|
227
|
+
if config.get("workflow_type") == "json" and config.get("workflow_definition"):
|
|
228
|
+
try:
|
|
229
|
+
workflow_data = json_lib.loads(config["workflow_definition"])
|
|
230
|
+
if workflow_data.get("runner"):
|
|
231
|
+
runners_to_check.append(("workflow.runner", workflow_data["runner"]))
|
|
232
|
+
except json_lib.JSONDecodeError:
|
|
233
|
+
# Invalid JSON - will be caught by skill validation
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
if not runners_to_check:
|
|
237
|
+
# No runners specified, will use default
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
# Fetch available runners from Kubiya API
|
|
241
|
+
try:
|
|
242
|
+
kubiya_client = get_kubiya_client()
|
|
243
|
+
available_runners = await kubiya_client.get_runners(token, org_id)
|
|
244
|
+
|
|
245
|
+
if not available_runners:
|
|
246
|
+
logger.warning(
|
|
247
|
+
"no_runners_available_skipping_validation",
|
|
248
|
+
org_id=org_id
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# Extract runner names/IDs from the response
|
|
253
|
+
runner_names = set()
|
|
254
|
+
for runner in available_runners:
|
|
255
|
+
if isinstance(runner, dict):
|
|
256
|
+
# Add both 'name' and 'id' to the set
|
|
257
|
+
if runner.get("name"):
|
|
258
|
+
runner_names.add(runner["name"])
|
|
259
|
+
if runner.get("id"):
|
|
260
|
+
runner_names.add(runner["id"])
|
|
261
|
+
|
|
262
|
+
# Validate each runner
|
|
263
|
+
for field_name, runner_value in runners_to_check:
|
|
264
|
+
if runner_value not in runner_names:
|
|
265
|
+
available_list = sorted(list(runner_names))
|
|
266
|
+
raise HTTPException(
|
|
267
|
+
status_code=400,
|
|
268
|
+
detail=(
|
|
269
|
+
f"Invalid runner '{runner_value}' specified in {field_name}. "
|
|
270
|
+
f"Available runners: {', '.join(available_list) if available_list else 'none'}"
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
logger.info(
|
|
275
|
+
"workflow_runners_validated",
|
|
276
|
+
runners_checked=[r[1] for r in runners_to_check],
|
|
277
|
+
available_count=len(runner_names)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
except HTTPException:
|
|
281
|
+
raise
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(
|
|
284
|
+
"runner_validation_failed",
|
|
285
|
+
error=str(e),
|
|
286
|
+
org_id=org_id
|
|
287
|
+
)
|
|
288
|
+
# Don't fail skill creation if runner validation fails
|
|
289
|
+
# This allows offline/testing scenarios
|
|
290
|
+
logger.warning("skipping_runner_validation_due_to_error")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# API Endpoints
|
|
294
|
+
|
|
295
|
+
@router.post("", response_model=ToolSetResponse, status_code=status.HTTP_201_CREATED)
|
|
296
|
+
async def create_skill(
|
|
297
|
+
skill_data: ToolSetCreate,
|
|
298
|
+
request: Request,
|
|
299
|
+
organization: dict = Depends(get_current_organization),
|
|
300
|
+
db: Session = Depends(get_db),
|
|
301
|
+
):
|
|
302
|
+
"""Create a new skill in the organization"""
|
|
303
|
+
try:
|
|
304
|
+
skill_id = str(uuid.uuid4())
|
|
305
|
+
now = datetime.utcnow()
|
|
306
|
+
|
|
307
|
+
# Validate skill type
|
|
308
|
+
valid_types = ["file_system", "shell", "python", "docker", "sleep", "file_generation", "data_visualization", "workflow_executor", "custom"]
|
|
309
|
+
if skill_data.type not in valid_types:
|
|
310
|
+
raise HTTPException(
|
|
311
|
+
status_code=400,
|
|
312
|
+
detail=f"Invalid skill type. Must be one of: {', '.join(valid_types)}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Validate workflow_executor runner if applicable
|
|
316
|
+
if skill_data.type == "workflow_executor":
|
|
317
|
+
config_dict = skill_data.configuration.dict(exclude_none=True)
|
|
318
|
+
token = request.state.kubiya_token
|
|
319
|
+
await validate_workflow_runner(config_dict, token, organization["id"])
|
|
320
|
+
|
|
321
|
+
skill = Skill(
|
|
322
|
+
id=skill_id,
|
|
323
|
+
organization_id=organization["id"],
|
|
324
|
+
name=skill_data.name,
|
|
325
|
+
skill_type=skill_data.type,
|
|
326
|
+
description=skill_data.description,
|
|
327
|
+
icon=skill_data.icon,
|
|
328
|
+
enabled=skill_data.enabled,
|
|
329
|
+
configuration=skill_data.configuration.dict(exclude_none=True),
|
|
330
|
+
created_at=now,
|
|
331
|
+
updated_at=now,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
db.add(skill)
|
|
335
|
+
db.commit()
|
|
336
|
+
db.refresh(skill)
|
|
337
|
+
|
|
338
|
+
logger.info(
|
|
339
|
+
"skill_created",
|
|
340
|
+
skill_id=skill_id,
|
|
341
|
+
name=skill_data.name,
|
|
342
|
+
type=skill_data.type,
|
|
343
|
+
organization_id=organization["id"]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Convert to dict and alias skill_type as type
|
|
347
|
+
skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
|
|
348
|
+
skill_dict["type"] = skill_dict.pop("skill_type")
|
|
349
|
+
|
|
350
|
+
return ToolSetResponse(**skill_dict)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error("skill_creation_failed", error=str(e))
|
|
354
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@router.get("", response_model=List[ToolSetResponse])
|
|
358
|
+
async def list_skills(
|
|
359
|
+
organization: dict = Depends(get_current_organization),
|
|
360
|
+
db: Session = Depends(get_db),
|
|
361
|
+
):
|
|
362
|
+
"""List all skills for the organization"""
|
|
363
|
+
try:
|
|
364
|
+
skills = db.query(Skill).filter(
|
|
365
|
+
Skill.organization_id == organization["id"]
|
|
366
|
+
).order_by(desc(Skill.created_at)).all()
|
|
367
|
+
|
|
368
|
+
# Convert to list of dicts with aliased skill_type as type
|
|
369
|
+
skills_data = []
|
|
370
|
+
for skill in skills:
|
|
371
|
+
skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
|
|
372
|
+
skill_dict["type"] = skill_dict.pop("skill_type")
|
|
373
|
+
skills_data.append(ToolSetResponse(**skill_dict))
|
|
374
|
+
|
|
375
|
+
return skills_data
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logger.error("skill_list_failed", error=str(e))
|
|
379
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@router.get("/{skill_id}", response_model=ToolSetResponse)
|
|
383
|
+
async def get_skill_endpoint(
|
|
384
|
+
skill_id: str,
|
|
385
|
+
organization: dict = Depends(get_current_organization),
|
|
386
|
+
db: Session = Depends(get_db),
|
|
387
|
+
):
|
|
388
|
+
"""Get a specific skill"""
|
|
389
|
+
try:
|
|
390
|
+
skill = get_skill_by_id(db, organization["id"], skill_id)
|
|
391
|
+
return ToolSetResponse(**skill)
|
|
392
|
+
|
|
393
|
+
except HTTPException:
|
|
394
|
+
raise
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.error("skill_get_failed", error=str(e), skill_id=skill_id)
|
|
397
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@router.patch("/{skill_id}", response_model=ToolSetResponse)
|
|
401
|
+
async def update_skill(
|
|
402
|
+
skill_id: str,
|
|
403
|
+
skill_data: ToolSetUpdate,
|
|
404
|
+
request: Request,
|
|
405
|
+
organization: dict = Depends(get_current_organization),
|
|
406
|
+
db: Session = Depends(get_db),
|
|
407
|
+
):
|
|
408
|
+
"""Update a skill"""
|
|
409
|
+
try:
|
|
410
|
+
# Verify skill exists
|
|
411
|
+
skill = db.query(Skill).filter(
|
|
412
|
+
Skill.id == skill_id,
|
|
413
|
+
Skill.organization_id == organization["id"]
|
|
414
|
+
).first()
|
|
415
|
+
|
|
416
|
+
if not skill:
|
|
417
|
+
raise HTTPException(status_code=404, detail=f"Skill {skill_id} not found")
|
|
418
|
+
|
|
419
|
+
# Build update dict
|
|
420
|
+
update_data = skill_data.dict(exclude_none=True)
|
|
421
|
+
if "configuration" in update_data:
|
|
422
|
+
update_data["configuration"] = update_data["configuration"]
|
|
423
|
+
update_data["updated_at"] = datetime.utcnow()
|
|
424
|
+
|
|
425
|
+
# Validate workflow_executor runner if updating configuration
|
|
426
|
+
if skill.skill_type == "workflow_executor" and "configuration" in update_data:
|
|
427
|
+
# Merge existing config with updates for complete validation
|
|
428
|
+
merged_config = {**(skill.configuration or {}), **update_data["configuration"]}
|
|
429
|
+
token = request.state.kubiya_token
|
|
430
|
+
await validate_workflow_runner(merged_config, token, organization["id"])
|
|
431
|
+
|
|
432
|
+
# Apply updates
|
|
433
|
+
for key, value in update_data.items():
|
|
434
|
+
setattr(skill, key, value)
|
|
435
|
+
|
|
436
|
+
db.commit()
|
|
437
|
+
db.refresh(skill)
|
|
438
|
+
|
|
439
|
+
logger.info("skill_updated", skill_id=skill_id, organization_id=organization["id"])
|
|
440
|
+
|
|
441
|
+
# Convert to dict and alias skill_type as type
|
|
442
|
+
skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
|
|
443
|
+
skill_dict["type"] = skill_dict.pop("skill_type")
|
|
444
|
+
|
|
445
|
+
return ToolSetResponse(**skill_dict)
|
|
446
|
+
|
|
447
|
+
except HTTPException:
|
|
448
|
+
raise
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.error("skill_update_failed", error=str(e), skill_id=skill_id)
|
|
451
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
455
|
+
async def delete_skill(
|
|
456
|
+
skill_id: str,
|
|
457
|
+
organization: dict = Depends(get_current_organization),
|
|
458
|
+
db: Session = Depends(get_db),
|
|
459
|
+
):
|
|
460
|
+
"""Delete a skill"""
|
|
461
|
+
try:
|
|
462
|
+
# Verify skill exists
|
|
463
|
+
skill = db.query(Skill).filter(
|
|
464
|
+
Skill.id == skill_id,
|
|
465
|
+
Skill.organization_id == organization["id"]
|
|
466
|
+
).first()
|
|
467
|
+
|
|
468
|
+
if not skill:
|
|
469
|
+
raise HTTPException(status_code=404, detail=f"Skill {skill_id} not found")
|
|
470
|
+
|
|
471
|
+
# Delete skill (cascade will handle associations)
|
|
472
|
+
db.delete(skill)
|
|
473
|
+
db.commit()
|
|
474
|
+
|
|
475
|
+
logger.info("skill_deleted", skill_id=skill_id, organization_id=organization["id"])
|
|
476
|
+
|
|
477
|
+
except HTTPException:
|
|
478
|
+
raise
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.error("skill_delete_failed", error=str(e), skill_id=skill_id)
|
|
481
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# Association endpoints for agents
|
|
485
|
+
@router.post("/associations/{entity_type}/{entity_id}/skills", status_code=status.HTTP_201_CREATED)
|
|
486
|
+
async def associate_skill(
|
|
487
|
+
entity_type: str,
|
|
488
|
+
entity_id: str,
|
|
489
|
+
association_data: ToolSetAssociationCreate,
|
|
490
|
+
organization: dict = Depends(get_current_organization),
|
|
491
|
+
db: Session = Depends(get_db),
|
|
492
|
+
):
|
|
493
|
+
"""Associate a skill with an entity (agent, team, environment)"""
|
|
494
|
+
try:
|
|
495
|
+
# Validate entity type
|
|
496
|
+
if entity_type not in ["agent", "team", "environment"]:
|
|
497
|
+
raise HTTPException(status_code=400, detail="Invalid entity type. Must be: agent, team, or environment")
|
|
498
|
+
|
|
499
|
+
# Verify skill exists
|
|
500
|
+
get_skill_by_id(db, organization["id"], association_data.skill_id)
|
|
501
|
+
|
|
502
|
+
# Verify entity exists (check appropriate table)
|
|
503
|
+
entity_model = None
|
|
504
|
+
if entity_type == "agent":
|
|
505
|
+
entity_model = Agent
|
|
506
|
+
elif entity_type == "team":
|
|
507
|
+
entity_model = Team
|
|
508
|
+
elif entity_type == "environment":
|
|
509
|
+
entity_model = Environment
|
|
510
|
+
|
|
511
|
+
entity = db.query(entity_model).filter(
|
|
512
|
+
entity_model.id == entity_id,
|
|
513
|
+
entity_model.organization_id == organization["id"]
|
|
514
|
+
).first()
|
|
515
|
+
|
|
516
|
+
if not entity:
|
|
517
|
+
raise HTTPException(status_code=404, detail=f"{entity_type.capitalize()} {entity_id} not found")
|
|
518
|
+
|
|
519
|
+
# Create association
|
|
520
|
+
association_id = str(uuid.uuid4())
|
|
521
|
+
association = SkillAssociation(
|
|
522
|
+
id=association_id,
|
|
523
|
+
organization_id=organization["id"],
|
|
524
|
+
skill_id=association_data.skill_id,
|
|
525
|
+
entity_type=entity_type,
|
|
526
|
+
entity_id=entity_id,
|
|
527
|
+
configuration_override=association_data.configuration_override.dict(exclude_none=True) if association_data.configuration_override else {},
|
|
528
|
+
created_at=datetime.utcnow(),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
db.add(association)
|
|
532
|
+
|
|
533
|
+
# Also update denormalized skill_ids array (only for teams)
|
|
534
|
+
# Agents and environments don't have a skill_ids column - they only use the skill_associations junction table
|
|
535
|
+
# Teams have a denormalized skill_ids array for performance
|
|
536
|
+
if entity_type == "team":
|
|
537
|
+
current_ids = entity.skill_ids or []
|
|
538
|
+
if association_data.skill_id not in current_ids:
|
|
539
|
+
updated_ids = current_ids + [association_data.skill_id]
|
|
540
|
+
entity.skill_ids = updated_ids
|
|
541
|
+
|
|
542
|
+
db.commit()
|
|
543
|
+
|
|
544
|
+
logger.info(
|
|
545
|
+
"skill_associated",
|
|
546
|
+
skill_id=association_data.skill_id,
|
|
547
|
+
entity_type=entity_type,
|
|
548
|
+
entity_id=entity_id,
|
|
549
|
+
organization_id=organization["id"]
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return {"message": "Skill associated successfully"}
|
|
553
|
+
|
|
554
|
+
except HTTPException:
|
|
555
|
+
raise
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.error("skill_association_failed", error=str(e))
|
|
558
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@router.get("/associations/{entity_type}/{entity_id}/skills", response_model=List[ToolSetResponse])
|
|
562
|
+
async def list_entity_skills(
|
|
563
|
+
entity_type: str,
|
|
564
|
+
entity_id: str,
|
|
565
|
+
organization: dict = Depends(get_current_organization),
|
|
566
|
+
db: Session = Depends(get_db),
|
|
567
|
+
):
|
|
568
|
+
"""List skills associated with an entity"""
|
|
569
|
+
try:
|
|
570
|
+
if entity_type not in ["agent", "team", "environment"]:
|
|
571
|
+
raise HTTPException(status_code=400, detail="Invalid entity type")
|
|
572
|
+
|
|
573
|
+
skills = get_entity_skills(db, organization["id"], entity_type, entity_id)
|
|
574
|
+
return [ToolSetResponse(**skill) for skill in skills]
|
|
575
|
+
|
|
576
|
+
except HTTPException:
|
|
577
|
+
raise
|
|
578
|
+
except Exception as e:
|
|
579
|
+
logger.error("list_entity_skills_failed", error=str(e))
|
|
580
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@router.delete("/associations/{entity_type}/{entity_id}/skills/{skill_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
584
|
+
async def dissociate_skill(
|
|
585
|
+
entity_type: str,
|
|
586
|
+
entity_id: str,
|
|
587
|
+
skill_id: str,
|
|
588
|
+
organization: dict = Depends(get_current_organization),
|
|
589
|
+
db: Session = Depends(get_db),
|
|
590
|
+
):
|
|
591
|
+
"""Remove a skill association from an entity"""
|
|
592
|
+
try:
|
|
593
|
+
if entity_type not in ["agent", "team", "environment"]:
|
|
594
|
+
raise HTTPException(status_code=400, detail="Invalid entity type")
|
|
595
|
+
|
|
596
|
+
# Delete association
|
|
597
|
+
db.query(SkillAssociation).filter(
|
|
598
|
+
SkillAssociation.skill_id == skill_id,
|
|
599
|
+
SkillAssociation.entity_type == entity_type,
|
|
600
|
+
SkillAssociation.entity_id == entity_id
|
|
601
|
+
).delete()
|
|
602
|
+
|
|
603
|
+
# Update denormalized skill_ids array (only for teams)
|
|
604
|
+
# Agents and environments don't have a skill_ids column - they only use the skill_associations junction table
|
|
605
|
+
# Teams have a denormalized skill_ids array for performance
|
|
606
|
+
if entity_type == "team":
|
|
607
|
+
team = db.query(Team).filter(Team.id == entity_id).first()
|
|
608
|
+
if team:
|
|
609
|
+
current_ids = team.skill_ids or []
|
|
610
|
+
updated_ids = [tid for tid in current_ids if tid != skill_id]
|
|
611
|
+
team.skill_ids = updated_ids
|
|
612
|
+
|
|
613
|
+
db.commit()
|
|
614
|
+
|
|
615
|
+
logger.info(
|
|
616
|
+
"skill_dissociated",
|
|
617
|
+
skill_id=skill_id,
|
|
618
|
+
entity_type=entity_type,
|
|
619
|
+
entity_id=entity_id,
|
|
620
|
+
organization_id=organization["id"]
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error("skill_dissociation_failed", error=str(e))
|
|
625
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@router.get("/associations/agents/{agent_id}/skills/resolved", response_model=List[ResolvedToolSet])
|
|
629
|
+
async def resolve_agent_skills(
|
|
630
|
+
agent_id: str,
|
|
631
|
+
organization: dict = Depends(get_current_organization),
|
|
632
|
+
db: Session = Depends(get_db),
|
|
633
|
+
):
|
|
634
|
+
"""
|
|
635
|
+
Resolve all skills for an agent (including inherited from ALL environments and team).
|
|
636
|
+
|
|
637
|
+
Inheritance order (with deduplication):
|
|
638
|
+
1. All agent environments
|
|
639
|
+
2. All team environments (if agent has team)
|
|
640
|
+
3. Team skills
|
|
641
|
+
4. Agent skills
|
|
642
|
+
|
|
643
|
+
Later layers override earlier ones if there are conflicts.
|
|
644
|
+
"""
|
|
645
|
+
try:
|
|
646
|
+
# Get agent details
|
|
647
|
+
agent = db.query(Agent).filter(
|
|
648
|
+
Agent.id == agent_id,
|
|
649
|
+
Agent.organization_id == organization["id"]
|
|
650
|
+
).first()
|
|
651
|
+
|
|
652
|
+
if not agent:
|
|
653
|
+
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
654
|
+
|
|
655
|
+
resolved_skills = []
|
|
656
|
+
seen_ids = set()
|
|
657
|
+
|
|
658
|
+
# 1. Load skills from ALL agent environments (many-to-many)
|
|
659
|
+
agent_envs = db.query(AgentEnvironment).filter(
|
|
660
|
+
AgentEnvironment.agent_id == agent_id
|
|
661
|
+
).all()
|
|
662
|
+
|
|
663
|
+
agent_environment_ids = [str(env.environment_id) for env in agent_envs]
|
|
664
|
+
|
|
665
|
+
for environment_id in agent_environment_ids:
|
|
666
|
+
env_skills = get_entity_skills(db, organization["id"], "environment", environment_id)
|
|
667
|
+
for skill in env_skills:
|
|
668
|
+
if skill["id"] not in seen_ids:
|
|
669
|
+
resolved_skills.append(ResolvedToolSet(
|
|
670
|
+
**skill,
|
|
671
|
+
source="environment",
|
|
672
|
+
inherited=True
|
|
673
|
+
))
|
|
674
|
+
seen_ids.add(skill["id"])
|
|
675
|
+
|
|
676
|
+
# 2. Load skills from ALL team environments (if agent has team)
|
|
677
|
+
team_id = agent.team_id
|
|
678
|
+
team_environment_ids = []
|
|
679
|
+
if team_id:
|
|
680
|
+
team_envs = db.query(TeamEnvironment).filter(
|
|
681
|
+
TeamEnvironment.team_id == team_id
|
|
682
|
+
).all()
|
|
683
|
+
|
|
684
|
+
team_environment_ids = [str(env.environment_id) for env in team_envs]
|
|
685
|
+
|
|
686
|
+
for environment_id in team_environment_ids:
|
|
687
|
+
env_skills = get_entity_skills(db, organization["id"], "environment", environment_id)
|
|
688
|
+
for skill in env_skills:
|
|
689
|
+
if skill["id"] not in seen_ids:
|
|
690
|
+
resolved_skills.append(ResolvedToolSet(
|
|
691
|
+
**skill,
|
|
692
|
+
source="environment",
|
|
693
|
+
inherited=True
|
|
694
|
+
))
|
|
695
|
+
seen_ids.add(skill["id"])
|
|
696
|
+
|
|
697
|
+
# 3. Load team skills
|
|
698
|
+
team_skills = get_entity_skills(db, organization["id"], "team", str(team_id))
|
|
699
|
+
for skill in team_skills:
|
|
700
|
+
if skill["id"] not in seen_ids:
|
|
701
|
+
resolved_skills.append(ResolvedToolSet(
|
|
702
|
+
**skill,
|
|
703
|
+
source="team",
|
|
704
|
+
inherited=True
|
|
705
|
+
))
|
|
706
|
+
seen_ids.add(skill["id"])
|
|
707
|
+
|
|
708
|
+
# 4. Load agent skills (highest priority)
|
|
709
|
+
agent_skills = get_entity_skills(db, organization["id"], "agent", agent_id)
|
|
710
|
+
for skill in agent_skills:
|
|
711
|
+
if skill["id"] not in seen_ids:
|
|
712
|
+
resolved_skills.append(ResolvedToolSet(
|
|
713
|
+
**skill,
|
|
714
|
+
source="agent",
|
|
715
|
+
inherited=False
|
|
716
|
+
))
|
|
717
|
+
seen_ids.add(skill["id"])
|
|
718
|
+
|
|
719
|
+
logger.info(
|
|
720
|
+
"agent_skills_resolved",
|
|
721
|
+
agent_id=agent_id,
|
|
722
|
+
skill_count=len(resolved_skills),
|
|
723
|
+
agent_env_count=len(agent_environment_ids),
|
|
724
|
+
team_env_count=len(team_environment_ids) if team_id else 0,
|
|
725
|
+
organization_id=organization["id"]
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
return resolved_skills
|
|
729
|
+
|
|
730
|
+
except HTTPException:
|
|
731
|
+
raise
|
|
732
|
+
except Exception as e:
|
|
733
|
+
logger.error("resolve_agent_skills_failed", error=str(e), agent_id=agent_id)
|
|
734
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@router.get("/associations/agents/{agent_id}/toolsets/resolved", response_model=List[ResolvedToolSet])
|
|
738
|
+
async def resolve_agent_toolsets_legacy(
|
|
739
|
+
agent_id: str,
|
|
740
|
+
organization: dict = Depends(get_current_organization),
|
|
741
|
+
db: Session = Depends(get_db),
|
|
742
|
+
):
|
|
743
|
+
"""
|
|
744
|
+
DEPRECATED: Legacy endpoint for backward compatibility.
|
|
745
|
+
Use /associations/agents/{agent_id}/skills/resolved instead.
|
|
746
|
+
|
|
747
|
+
This endpoint redirects to the new skills endpoint.
|
|
748
|
+
"""
|
|
749
|
+
logger.warning(
|
|
750
|
+
"deprecated_toolsets_endpoint_used",
|
|
751
|
+
agent_id=agent_id,
|
|
752
|
+
endpoint="/associations/agents/{agent_id}/toolsets/resolved",
|
|
753
|
+
new_endpoint="/associations/agents/{agent_id}/skills/resolved"
|
|
754
|
+
)
|
|
755
|
+
return await resolve_agent_skills(agent_id, organization, db)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
@router.get("/associations/teams/{team_id}/skills/resolved", response_model=List[ResolvedToolSet])
|
|
759
|
+
async def resolve_team_skills(
|
|
760
|
+
team_id: str,
|
|
761
|
+
organization: dict = Depends(get_current_organization),
|
|
762
|
+
db: Session = Depends(get_db),
|
|
763
|
+
):
|
|
764
|
+
"""
|
|
765
|
+
Resolve all skills for a team (including inherited from ALL environments).
|
|
766
|
+
|
|
767
|
+
Inheritance order (with deduplication):
|
|
768
|
+
1. All team environments
|
|
769
|
+
2. Team skills
|
|
770
|
+
|
|
771
|
+
Later layers override earlier ones if there are conflicts.
|
|
772
|
+
"""
|
|
773
|
+
try:
|
|
774
|
+
# Get team details
|
|
775
|
+
team = db.query(Team).filter(
|
|
776
|
+
Team.id == team_id,
|
|
777
|
+
Team.organization_id == organization["id"]
|
|
778
|
+
).first()
|
|
779
|
+
|
|
780
|
+
if not team:
|
|
781
|
+
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
|
782
|
+
|
|
783
|
+
resolved_skills = []
|
|
784
|
+
seen_ids = set()
|
|
785
|
+
|
|
786
|
+
# 1. Load skills from ALL team environments (many-to-many)
|
|
787
|
+
team_envs = db.query(TeamEnvironment).filter(
|
|
788
|
+
TeamEnvironment.team_id == team_id
|
|
789
|
+
).all()
|
|
790
|
+
|
|
791
|
+
team_environment_ids = [str(env.environment_id) for env in team_envs]
|
|
792
|
+
|
|
793
|
+
for environment_id in team_environment_ids:
|
|
794
|
+
env_skills = get_entity_skills(db, organization["id"], "environment", environment_id)
|
|
795
|
+
for skill in env_skills:
|
|
796
|
+
if skill["id"] not in seen_ids:
|
|
797
|
+
resolved_skills.append(ResolvedToolSet(
|
|
798
|
+
**skill,
|
|
799
|
+
source="environment",
|
|
800
|
+
inherited=True
|
|
801
|
+
))
|
|
802
|
+
seen_ids.add(skill["id"])
|
|
803
|
+
|
|
804
|
+
# 2. Load team skills (highest priority)
|
|
805
|
+
team_skills = get_entity_skills(db, organization["id"], "team", team_id)
|
|
806
|
+
for skill in team_skills:
|
|
807
|
+
if skill["id"] not in seen_ids:
|
|
808
|
+
resolved_skills.append(ResolvedToolSet(
|
|
809
|
+
**skill,
|
|
810
|
+
source="team",
|
|
811
|
+
inherited=False
|
|
812
|
+
))
|
|
813
|
+
seen_ids.add(skill["id"])
|
|
814
|
+
|
|
815
|
+
logger.info(
|
|
816
|
+
"team_skills_resolved",
|
|
817
|
+
team_id=team_id,
|
|
818
|
+
skill_count=len(resolved_skills),
|
|
819
|
+
team_env_count=len(team_environment_ids),
|
|
820
|
+
organization_id=organization["id"]
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
return resolved_skills
|
|
824
|
+
|
|
825
|
+
except HTTPException:
|
|
826
|
+
raise
|
|
827
|
+
except Exception as e:
|
|
828
|
+
logger.error("resolve_team_skills_failed", error=str(e), team_id=team_id)
|
|
829
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
@router.get("/associations/teams/{team_id}/toolsets/resolved", response_model=List[ResolvedToolSet])
|
|
833
|
+
async def resolve_team_toolsets_legacy(
|
|
834
|
+
team_id: str,
|
|
835
|
+
organization: dict = Depends(get_current_organization),
|
|
836
|
+
db: Session = Depends(get_db),
|
|
837
|
+
):
|
|
838
|
+
"""
|
|
839
|
+
DEPRECATED: Legacy endpoint for backward compatibility.
|
|
840
|
+
Use /associations/teams/{team_id}/skills/resolved instead.
|
|
841
|
+
|
|
842
|
+
This endpoint redirects to the new skills endpoint.
|
|
843
|
+
"""
|
|
844
|
+
logger.warning(
|
|
845
|
+
"deprecated_toolsets_endpoint_used",
|
|
846
|
+
team_id=team_id,
|
|
847
|
+
endpoint="/associations/teams/{team_id}/toolsets/resolved",
|
|
848
|
+
new_endpoint="/associations/teams/{team_id}/skills/resolved"
|
|
849
|
+
)
|
|
850
|
+
return await resolve_team_skills(team_id, organization, db)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
@router.get("/types")
|
|
854
|
+
async def get_skill_types():
|
|
855
|
+
"""Get available skill types and their descriptions"""
|
|
856
|
+
return {
|
|
857
|
+
"types": [
|
|
858
|
+
{
|
|
859
|
+
"type": "file_system",
|
|
860
|
+
"name": "File System",
|
|
861
|
+
"description": "Read, write, list, and search files",
|
|
862
|
+
"icon": "FileText"
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
"type": "shell",
|
|
866
|
+
"name": "Shell",
|
|
867
|
+
"description": "Execute shell commands",
|
|
868
|
+
"icon": "Terminal"
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
"type": "docker",
|
|
872
|
+
"name": "Docker",
|
|
873
|
+
"description": "Manage containers, images, volumes, and networks",
|
|
874
|
+
"icon": "Container"
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
"type": "python",
|
|
878
|
+
"name": "Python",
|
|
879
|
+
"description": "Execute Python code",
|
|
880
|
+
"icon": "Code"
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
"type": "file_generation",
|
|
884
|
+
"name": "File Generation",
|
|
885
|
+
"description": "Generate JSON, CSV, PDF, and TXT files",
|
|
886
|
+
"icon": "FileOutput"
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
"type": "sleep",
|
|
890
|
+
"name": "Sleep",
|
|
891
|
+
"description": "Pause execution for a specified duration",
|
|
892
|
+
"icon": "Clock"
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"type": "workflow_executor",
|
|
896
|
+
"name": "Workflow Executor",
|
|
897
|
+
"description": "Execute workflows defined via JSON or Python DSL",
|
|
898
|
+
"icon": "Workflow"
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
"type": "custom",
|
|
902
|
+
"name": "Custom",
|
|
903
|
+
"description": "User-defined custom skill",
|
|
904
|
+
"icon": "Wrench"
|
|
905
|
+
}
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@router.get("/templates")
|
|
911
|
+
async def get_skill_templates(
|
|
912
|
+
request: Request,
|
|
913
|
+
organization: dict = Depends(get_current_organization),
|
|
914
|
+
):
|
|
915
|
+
"""
|
|
916
|
+
Get detailed skill templates with variants and configuration schemas.
|
|
917
|
+
|
|
918
|
+
This endpoint returns all available skill templates including their variants,
|
|
919
|
+
configuration schemas, default values, and available runners for workflow executor skills.
|
|
920
|
+
Useful for UI forms and skill creation.
|
|
921
|
+
"""
|
|
922
|
+
templates = []
|
|
923
|
+
|
|
924
|
+
# Get all registered skills from the skill system
|
|
925
|
+
all_skills = get_all_skills()
|
|
926
|
+
|
|
927
|
+
# Fetch available runners for workflow executor validation
|
|
928
|
+
runners_list = []
|
|
929
|
+
try:
|
|
930
|
+
kubiya_client = get_kubiya_client()
|
|
931
|
+
token = request.state.kubiya_token
|
|
932
|
+
available_runners = await kubiya_client.get_runners(token, organization["id"])
|
|
933
|
+
|
|
934
|
+
if available_runners:
|
|
935
|
+
for runner in available_runners:
|
|
936
|
+
if isinstance(runner, dict):
|
|
937
|
+
runners_list.append({
|
|
938
|
+
"id": runner.get("id"),
|
|
939
|
+
"name": runner.get("name"),
|
|
940
|
+
"status": runner.get("status"),
|
|
941
|
+
"capabilities": runner.get("capabilities", []),
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
logger.info(
|
|
945
|
+
"runners_fetched_for_templates",
|
|
946
|
+
org_id=organization["id"],
|
|
947
|
+
runner_count=len(runners_list)
|
|
948
|
+
)
|
|
949
|
+
except Exception as e:
|
|
950
|
+
logger.warning(
|
|
951
|
+
"failed_to_fetch_runners_for_templates",
|
|
952
|
+
error=str(e),
|
|
953
|
+
org_id=organization["id"]
|
|
954
|
+
)
|
|
955
|
+
# Continue without runners - they're optional
|
|
956
|
+
|
|
957
|
+
for skill_def in all_skills:
|
|
958
|
+
try:
|
|
959
|
+
# Get skill metadata
|
|
960
|
+
template = {
|
|
961
|
+
"type": skill_def.type.value,
|
|
962
|
+
"name": skill_def.name,
|
|
963
|
+
"description": skill_def.description,
|
|
964
|
+
"icon": skill_def.icon,
|
|
965
|
+
"icon_type": skill_def.icon_type,
|
|
966
|
+
"category": skill_def.get_category().value,
|
|
967
|
+
"default_configuration": skill_def.get_default_configuration(),
|
|
968
|
+
"requirements": {
|
|
969
|
+
"supported_os": skill_def.get_requirements().supported_os,
|
|
970
|
+
"min_python_version": skill_def.get_requirements().min_python_version,
|
|
971
|
+
"python_packages": skill_def.get_requirements().python_packages,
|
|
972
|
+
"required_env_vars": skill_def.get_requirements().required_env_vars,
|
|
973
|
+
"notes": skill_def.get_requirements().notes,
|
|
974
|
+
} if skill_def.get_requirements() else None,
|
|
975
|
+
"variants": []
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
# Add available runners for workflow executor skills
|
|
979
|
+
if skill_def.type.value == "workflow_executor":
|
|
980
|
+
template["available_runners"] = runners_list
|
|
981
|
+
|
|
982
|
+
# Get variants for this skill
|
|
983
|
+
variants = skill_def.get_variants()
|
|
984
|
+
for variant in variants:
|
|
985
|
+
template["variants"].append({
|
|
986
|
+
"id": variant.id,
|
|
987
|
+
"name": variant.name,
|
|
988
|
+
"description": variant.description,
|
|
989
|
+
"category": variant.category.value,
|
|
990
|
+
"icon": variant.icon,
|
|
991
|
+
"is_default": variant.is_default,
|
|
992
|
+
"configuration": variant.configuration,
|
|
993
|
+
"tags": variant.tags,
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
templates.append(template)
|
|
997
|
+
|
|
998
|
+
except Exception as e:
|
|
999
|
+
logger.error(
|
|
1000
|
+
"failed_to_build_skill_template",
|
|
1001
|
+
skill_type=skill_def.type.value if hasattr(skill_def, 'type') else 'unknown',
|
|
1002
|
+
error=str(e)
|
|
1003
|
+
)
|
|
1004
|
+
continue
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
"templates": templates,
|
|
1008
|
+
"count": len(templates),
|
|
1009
|
+
"runners": runners_list # Also return at root level for easy access
|
|
1010
|
+
}
|