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,576 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket Endpoint for Trace Streaming.
|
|
3
|
+
|
|
4
|
+
Provides persistent WebSocket connections for frontend clients to receive
|
|
5
|
+
real-time trace and span updates for the observability UI.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Organization-scoped live trace streaming
|
|
9
|
+
- Single trace detail streaming (for waterfall view)
|
|
10
|
+
- Authentication via JWT tokens
|
|
11
|
+
- Heartbeat/keepalive mechanism
|
|
12
|
+
- Redis pub/sub for event distribution
|
|
13
|
+
|
|
14
|
+
Architecture:
|
|
15
|
+
Browser → WebSocket → Control Plane API → Redis Pub/Sub → Trace Events
|
|
16
|
+
↓
|
|
17
|
+
LocalStorageSpanProcessor
|
|
18
|
+
|
|
19
|
+
This enables real-time updates in the Observability UI without polling.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
|
|
23
|
+
from typing import Optional, Dict, Any, Set
|
|
24
|
+
import structlog
|
|
25
|
+
import json
|
|
26
|
+
import asyncio
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
|
|
29
|
+
from control_plane_api.app.lib.redis_client import get_redis_client
|
|
30
|
+
from control_plane_api.app.database import get_db
|
|
31
|
+
from control_plane_api.app.models.trace import Trace
|
|
32
|
+
from sqlalchemy.orm import Session
|
|
33
|
+
|
|
34
|
+
logger = structlog.get_logger()
|
|
35
|
+
|
|
36
|
+
router = APIRouter()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TraceConnectionManager:
|
|
40
|
+
"""
|
|
41
|
+
Manages WebSocket connections for trace streaming.
|
|
42
|
+
|
|
43
|
+
Features:
|
|
44
|
+
- Per-organization trace list streaming
|
|
45
|
+
- Per-trace detail streaming
|
|
46
|
+
- Connection tracking and cleanup
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
# organization_id -> Set[WebSocket]
|
|
51
|
+
self._org_connections: Dict[str, Set[WebSocket]] = {}
|
|
52
|
+
|
|
53
|
+
# trace_id -> Set[WebSocket]
|
|
54
|
+
self._trace_connections: Dict[str, Set[WebSocket]] = {}
|
|
55
|
+
|
|
56
|
+
# WebSocket -> organization_id
|
|
57
|
+
self._ws_to_org: Dict[WebSocket, str] = {}
|
|
58
|
+
|
|
59
|
+
# WebSocket -> trace_id
|
|
60
|
+
self._ws_to_trace: Dict[WebSocket, str] = {}
|
|
61
|
+
|
|
62
|
+
# Statistics
|
|
63
|
+
self._stats = {
|
|
64
|
+
"total_connections": 0,
|
|
65
|
+
"active_connections": 0,
|
|
66
|
+
"messages_sent": 0,
|
|
67
|
+
"errors": 0,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async def connect_org(
|
|
71
|
+
self,
|
|
72
|
+
organization_id: str,
|
|
73
|
+
websocket: WebSocket,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Register a new organization-level WebSocket connection."""
|
|
76
|
+
await websocket.accept()
|
|
77
|
+
|
|
78
|
+
if organization_id not in self._org_connections:
|
|
79
|
+
self._org_connections[organization_id] = set()
|
|
80
|
+
self._org_connections[organization_id].add(websocket)
|
|
81
|
+
self._ws_to_org[websocket] = organization_id
|
|
82
|
+
|
|
83
|
+
self._stats["total_connections"] += 1
|
|
84
|
+
self._stats["active_connections"] += 1
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
"trace_websocket_org_connected",
|
|
88
|
+
organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
|
|
89
|
+
active_connections=self._stats["active_connections"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def connect_trace(
|
|
93
|
+
self,
|
|
94
|
+
trace_id: str,
|
|
95
|
+
organization_id: str,
|
|
96
|
+
websocket: WebSocket,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Register a new trace-level WebSocket connection."""
|
|
99
|
+
await websocket.accept()
|
|
100
|
+
|
|
101
|
+
if trace_id not in self._trace_connections:
|
|
102
|
+
self._trace_connections[trace_id] = set()
|
|
103
|
+
self._trace_connections[trace_id].add(websocket)
|
|
104
|
+
self._ws_to_trace[websocket] = trace_id
|
|
105
|
+
self._ws_to_org[websocket] = organization_id
|
|
106
|
+
|
|
107
|
+
self._stats["total_connections"] += 1
|
|
108
|
+
self._stats["active_connections"] += 1
|
|
109
|
+
|
|
110
|
+
logger.info(
|
|
111
|
+
"trace_websocket_trace_connected",
|
|
112
|
+
trace_id=trace_id[:8],
|
|
113
|
+
organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
|
|
114
|
+
active_connections=self._stats["active_connections"],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def disconnect(self, websocket: WebSocket) -> None:
|
|
118
|
+
"""Unregister a WebSocket connection."""
|
|
119
|
+
# Remove from org connections
|
|
120
|
+
org_id = self._ws_to_org.pop(websocket, None)
|
|
121
|
+
if org_id and org_id in self._org_connections:
|
|
122
|
+
self._org_connections[org_id].discard(websocket)
|
|
123
|
+
if not self._org_connections[org_id]:
|
|
124
|
+
del self._org_connections[org_id]
|
|
125
|
+
|
|
126
|
+
# Remove from trace connections
|
|
127
|
+
trace_id = self._ws_to_trace.pop(websocket, None)
|
|
128
|
+
if trace_id and trace_id in self._trace_connections:
|
|
129
|
+
self._trace_connections[trace_id].discard(websocket)
|
|
130
|
+
if not self._trace_connections[trace_id]:
|
|
131
|
+
del self._trace_connections[trace_id]
|
|
132
|
+
|
|
133
|
+
self._stats["active_connections"] = max(0, self._stats["active_connections"] - 1)
|
|
134
|
+
|
|
135
|
+
logger.info(
|
|
136
|
+
"trace_websocket_disconnected",
|
|
137
|
+
organization_id=org_id[:8] if org_id and len(org_id) > 8 else org_id,
|
|
138
|
+
trace_id=trace_id[:8] if trace_id else None,
|
|
139
|
+
active_connections=self._stats["active_connections"],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_org_connections(self, organization_id: str) -> Set[WebSocket]:
|
|
143
|
+
"""Get all WebSocket connections for an organization."""
|
|
144
|
+
return self._org_connections.get(organization_id, set())
|
|
145
|
+
|
|
146
|
+
def get_trace_connections(self, trace_id: str) -> Set[WebSocket]:
|
|
147
|
+
"""Get all WebSocket connections for a trace."""
|
|
148
|
+
return self._trace_connections.get(trace_id, set())
|
|
149
|
+
|
|
150
|
+
def get_stats(self) -> Dict[str, int]:
|
|
151
|
+
"""Get connection statistics."""
|
|
152
|
+
return self._stats.copy()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Global connection manager
|
|
156
|
+
trace_manager = TraceConnectionManager()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def send_json(websocket: WebSocket, event_type: str, data: Any) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Send JSON message via WebSocket.
|
|
162
|
+
|
|
163
|
+
Returns True if successful, False if failed.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
message = {
|
|
167
|
+
"type": event_type,
|
|
168
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
169
|
+
**(data if isinstance(data, dict) else {"data": data})
|
|
170
|
+
}
|
|
171
|
+
await websocket.send_text(json.dumps(message, default=str))
|
|
172
|
+
trace_manager._stats["messages_sent"] += 1
|
|
173
|
+
return True
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error("failed_to_send_websocket_message", error=str(e), event_type=event_type)
|
|
176
|
+
trace_manager._stats["errors"] += 1
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def handle_auth(websocket: WebSocket, token: str) -> Optional[str]:
|
|
181
|
+
"""
|
|
182
|
+
Handle authentication message.
|
|
183
|
+
|
|
184
|
+
Returns organization_id if authentication successful, None otherwise.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
from control_plane_api.app.middleware.auth import decode_jwt_token
|
|
188
|
+
|
|
189
|
+
decoded = decode_jwt_token(token)
|
|
190
|
+
|
|
191
|
+
if not decoded:
|
|
192
|
+
logger.error("jwt_decode_failed", reason="Invalid token format")
|
|
193
|
+
await send_json(websocket, "auth_error", {
|
|
194
|
+
"error": "Invalid authentication token",
|
|
195
|
+
"code": "INVALID_TOKEN",
|
|
196
|
+
})
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
organization_id = (
|
|
200
|
+
decoded.get('https://kubiya.ai/org_id') or
|
|
201
|
+
decoded.get('org_id') or
|
|
202
|
+
decoded.get('organization_id')
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if not organization_id:
|
|
206
|
+
logger.error("org_id_missing", decoded_claims=list(decoded.keys()))
|
|
207
|
+
await send_json(websocket, "auth_error", {
|
|
208
|
+
"error": "Organization ID not found in token",
|
|
209
|
+
"code": "ORG_ID_MISSING",
|
|
210
|
+
})
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
user_id = decoded.get('sub')
|
|
214
|
+
|
|
215
|
+
logger.info(
|
|
216
|
+
"trace_websocket_authenticated",
|
|
217
|
+
organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
|
|
218
|
+
user_id=user_id[:8] if user_id and len(user_id) > 8 else user_id,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
await send_json(websocket, "auth_success", {
|
|
222
|
+
"organization_id": organization_id,
|
|
223
|
+
"user_id": user_id,
|
|
224
|
+
"authenticated_at": datetime.now(timezone.utc).isoformat(),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return organization_id
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error("authentication_failed", error=str(e), error_type=type(e).__name__)
|
|
231
|
+
await send_json(websocket, "auth_error", {
|
|
232
|
+
"error": "Authentication failed",
|
|
233
|
+
"code": "AUTH_FAILED",
|
|
234
|
+
})
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def subscribe_to_redis_channel(
|
|
239
|
+
websocket: WebSocket,
|
|
240
|
+
channel: str,
|
|
241
|
+
organization_id: str,
|
|
242
|
+
trace_id: Optional[str] = None,
|
|
243
|
+
):
|
|
244
|
+
"""
|
|
245
|
+
Subscribe to Redis pub/sub channel and forward events to WebSocket.
|
|
246
|
+
|
|
247
|
+
This runs until the WebSocket disconnects or an error occurs.
|
|
248
|
+
"""
|
|
249
|
+
redis_client = get_redis_client()
|
|
250
|
+
if not redis_client:
|
|
251
|
+
logger.warning("redis_not_available", channel=channel)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Get the underlying redis connection for pub/sub
|
|
256
|
+
# Note: This depends on the type of redis client
|
|
257
|
+
if hasattr(redis_client, '_redis'):
|
|
258
|
+
# StandardRedisClient
|
|
259
|
+
pubsub = redis_client._redis.pubsub()
|
|
260
|
+
await pubsub.subscribe(channel)
|
|
261
|
+
|
|
262
|
+
logger.info(
|
|
263
|
+
"redis_channel_subscribed",
|
|
264
|
+
channel=channel,
|
|
265
|
+
organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Listen for messages
|
|
269
|
+
while True:
|
|
270
|
+
try:
|
|
271
|
+
message = await pubsub.get_message(
|
|
272
|
+
ignore_subscribe_messages=True,
|
|
273
|
+
timeout=1.0
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if message and message.get("type") == "message":
|
|
277
|
+
data = message.get("data")
|
|
278
|
+
if isinstance(data, bytes):
|
|
279
|
+
data = data.decode("utf-8")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
event = json.loads(data)
|
|
283
|
+
event_type = event.get("type", "trace_event")
|
|
284
|
+
|
|
285
|
+
# Filter by trace_id if this is a trace-specific connection
|
|
286
|
+
if trace_id:
|
|
287
|
+
event_trace_id = event.get("data", {}).get("trace_id")
|
|
288
|
+
if event_trace_id != trace_id:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
success = await send_json(websocket, event_type, event.get("data", {}))
|
|
292
|
+
if not success:
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
except json.JSONDecodeError:
|
|
296
|
+
logger.warning("invalid_redis_message", data=str(data)[:100])
|
|
297
|
+
|
|
298
|
+
except asyncio.TimeoutError:
|
|
299
|
+
# Send keepalive ping
|
|
300
|
+
try:
|
|
301
|
+
await send_json(websocket, "ping", {})
|
|
302
|
+
except Exception:
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
else:
|
|
306
|
+
# UpstashRedisClient doesn't support pub/sub subscription
|
|
307
|
+
# Fall back to polling approach
|
|
308
|
+
logger.warning(
|
|
309
|
+
"redis_pubsub_not_supported",
|
|
310
|
+
client_type=type(redis_client).__name__,
|
|
311
|
+
message="Upstash REST API doesn't support pub/sub subscription",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Simple keepalive loop until disconnect
|
|
315
|
+
while True:
|
|
316
|
+
await asyncio.sleep(30)
|
|
317
|
+
try:
|
|
318
|
+
await send_json(websocket, "ping", {})
|
|
319
|
+
except Exception:
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
except asyncio.CancelledError:
|
|
323
|
+
logger.info("redis_subscription_cancelled", channel=channel)
|
|
324
|
+
raise
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error("redis_subscription_error", error=str(e), channel=channel)
|
|
327
|
+
finally:
|
|
328
|
+
# Cleanup
|
|
329
|
+
if hasattr(redis_client, '_redis') and 'pubsub' in dir():
|
|
330
|
+
try:
|
|
331
|
+
await pubsub.unsubscribe(channel)
|
|
332
|
+
await pubsub.close()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@router.websocket("/ws/traces/live")
|
|
338
|
+
async def websocket_trace_list_stream(
|
|
339
|
+
websocket: WebSocket,
|
|
340
|
+
db: Session = Depends(get_db),
|
|
341
|
+
):
|
|
342
|
+
"""
|
|
343
|
+
WebSocket endpoint for live trace list streaming.
|
|
344
|
+
|
|
345
|
+
Streams all new traces and trace updates for the authenticated organization.
|
|
346
|
+
|
|
347
|
+
Flow:
|
|
348
|
+
1. Accept WebSocket connection
|
|
349
|
+
2. Wait for auth message with JWT token
|
|
350
|
+
3. Validate token and extract organization_id
|
|
351
|
+
4. Send 'connected' event
|
|
352
|
+
5. Subscribe to Redis channel for trace events
|
|
353
|
+
6. Stream events until client disconnects
|
|
354
|
+
"""
|
|
355
|
+
organization_id: Optional[str] = None
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
await websocket.accept()
|
|
359
|
+
|
|
360
|
+
logger.info("trace_list_websocket_started")
|
|
361
|
+
|
|
362
|
+
# Wait for authentication message
|
|
363
|
+
try:
|
|
364
|
+
auth_message = await asyncio.wait_for(websocket.receive_text(), timeout=5.0)
|
|
365
|
+
auth_data = json.loads(auth_message)
|
|
366
|
+
|
|
367
|
+
if auth_data.get("type") == "auth" and "token" in auth_data:
|
|
368
|
+
organization_id = await handle_auth(websocket, auth_data["token"])
|
|
369
|
+
if not organization_id:
|
|
370
|
+
await websocket.close(code=4001, reason="Authentication failed")
|
|
371
|
+
return
|
|
372
|
+
else:
|
|
373
|
+
await websocket.close(code=4003, reason="Invalid authentication message")
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
except asyncio.TimeoutError:
|
|
377
|
+
logger.error("trace_auth_timeout")
|
|
378
|
+
await websocket.close(code=4002, reason="Authentication timeout")
|
|
379
|
+
return
|
|
380
|
+
except json.JSONDecodeError:
|
|
381
|
+
logger.error("trace_invalid_auth_message")
|
|
382
|
+
await websocket.close(code=4003, reason="Invalid authentication message")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
# Track connection
|
|
386
|
+
if organization_id not in trace_manager._org_connections:
|
|
387
|
+
trace_manager._org_connections[organization_id] = set()
|
|
388
|
+
trace_manager._org_connections[organization_id].add(websocket)
|
|
389
|
+
trace_manager._ws_to_org[websocket] = organization_id
|
|
390
|
+
trace_manager._stats["active_connections"] += 1
|
|
391
|
+
|
|
392
|
+
# Send connected event
|
|
393
|
+
await send_json(websocket, "connected", {
|
|
394
|
+
"organization_id": organization_id,
|
|
395
|
+
"connected_at": datetime.now(timezone.utc).isoformat(),
|
|
396
|
+
"subscription": "trace_list",
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
# Create tasks for Redis subscription and client message handling
|
|
400
|
+
redis_task = asyncio.create_task(
|
|
401
|
+
subscribe_to_redis_channel(
|
|
402
|
+
websocket,
|
|
403
|
+
f"traces:{organization_id}",
|
|
404
|
+
organization_id,
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Handle incoming messages (ping/pong, etc.)
|
|
409
|
+
try:
|
|
410
|
+
while True:
|
|
411
|
+
try:
|
|
412
|
+
message = await asyncio.wait_for(websocket.receive_text(), timeout=60.0)
|
|
413
|
+
data = json.loads(message)
|
|
414
|
+
|
|
415
|
+
if data.get("type") == "ping":
|
|
416
|
+
await send_json(websocket, "pong", {
|
|
417
|
+
"timestamp": int(datetime.now(timezone.utc).timestamp() * 1000)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
except asyncio.TimeoutError:
|
|
421
|
+
# Send ping to keep connection alive
|
|
422
|
+
await send_json(websocket, "ping", {})
|
|
423
|
+
|
|
424
|
+
except WebSocketDisconnect:
|
|
425
|
+
logger.info("trace_list_websocket_disconnected")
|
|
426
|
+
|
|
427
|
+
finally:
|
|
428
|
+
redis_task.cancel()
|
|
429
|
+
try:
|
|
430
|
+
await redis_task
|
|
431
|
+
except asyncio.CancelledError:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
except WebSocketDisconnect:
|
|
435
|
+
logger.info("trace_list_websocket_disconnected_early")
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error("trace_list_websocket_error", error=str(e), exc_info=True)
|
|
438
|
+
finally:
|
|
439
|
+
if organization_id:
|
|
440
|
+
await trace_manager.disconnect(websocket)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@router.websocket("/ws/traces/{trace_id}")
|
|
444
|
+
async def websocket_trace_detail_stream(
|
|
445
|
+
websocket: WebSocket,
|
|
446
|
+
trace_id: str,
|
|
447
|
+
db: Session = Depends(get_db),
|
|
448
|
+
):
|
|
449
|
+
"""
|
|
450
|
+
WebSocket endpoint for single trace detail streaming.
|
|
451
|
+
|
|
452
|
+
Streams span updates for a specific trace (for waterfall view).
|
|
453
|
+
|
|
454
|
+
Flow:
|
|
455
|
+
1. Accept WebSocket connection
|
|
456
|
+
2. Wait for auth message with JWT token
|
|
457
|
+
3. Validate token and verify trace belongs to organization
|
|
458
|
+
4. Send 'connected' event with initial trace data
|
|
459
|
+
5. Subscribe to Redis channel filtered by trace_id
|
|
460
|
+
6. Stream span events until client disconnects or trace completes
|
|
461
|
+
"""
|
|
462
|
+
organization_id: Optional[str] = None
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
await websocket.accept()
|
|
466
|
+
|
|
467
|
+
logger.info("trace_detail_websocket_started", trace_id=trace_id[:8])
|
|
468
|
+
|
|
469
|
+
# Wait for authentication message
|
|
470
|
+
try:
|
|
471
|
+
auth_message = await asyncio.wait_for(websocket.receive_text(), timeout=5.0)
|
|
472
|
+
auth_data = json.loads(auth_message)
|
|
473
|
+
|
|
474
|
+
if auth_data.get("type") == "auth" and "token" in auth_data:
|
|
475
|
+
organization_id = await handle_auth(websocket, auth_data["token"])
|
|
476
|
+
if not organization_id:
|
|
477
|
+
await websocket.close(code=4001, reason="Authentication failed")
|
|
478
|
+
return
|
|
479
|
+
else:
|
|
480
|
+
await websocket.close(code=4003, reason="Invalid authentication message")
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
except asyncio.TimeoutError:
|
|
484
|
+
logger.error("trace_detail_auth_timeout", trace_id=trace_id[:8])
|
|
485
|
+
await websocket.close(code=4002, reason="Authentication timeout")
|
|
486
|
+
return
|
|
487
|
+
except json.JSONDecodeError:
|
|
488
|
+
logger.error("trace_detail_invalid_auth_message", trace_id=trace_id[:8])
|
|
489
|
+
await websocket.close(code=4003, reason="Invalid authentication message")
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
# Verify trace exists and belongs to organization
|
|
493
|
+
trace = db.query(Trace).filter(
|
|
494
|
+
Trace.trace_id == trace_id,
|
|
495
|
+
Trace.organization_id == organization_id,
|
|
496
|
+
).first()
|
|
497
|
+
|
|
498
|
+
if not trace:
|
|
499
|
+
await send_json(websocket, "error", {
|
|
500
|
+
"error": f"Trace {trace_id} not found",
|
|
501
|
+
"code": "TRACE_NOT_FOUND",
|
|
502
|
+
})
|
|
503
|
+
await websocket.close(code=4004, reason="Trace not found")
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# Track connection
|
|
507
|
+
if trace_id not in trace_manager._trace_connections:
|
|
508
|
+
trace_manager._trace_connections[trace_id] = set()
|
|
509
|
+
trace_manager._trace_connections[trace_id].add(websocket)
|
|
510
|
+
trace_manager._ws_to_trace[websocket] = trace_id
|
|
511
|
+
trace_manager._ws_to_org[websocket] = organization_id
|
|
512
|
+
trace_manager._stats["active_connections"] += 1
|
|
513
|
+
|
|
514
|
+
# Send connected event with trace info
|
|
515
|
+
await send_json(websocket, "connected", {
|
|
516
|
+
"trace_id": trace_id,
|
|
517
|
+
"organization_id": organization_id,
|
|
518
|
+
"trace_name": trace.name,
|
|
519
|
+
"trace_status": trace.status.value if trace.status else "running",
|
|
520
|
+
"connected_at": datetime.now(timezone.utc).isoformat(),
|
|
521
|
+
"subscription": "trace_detail",
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
# Create tasks for Redis subscription and client message handling
|
|
525
|
+
redis_task = asyncio.create_task(
|
|
526
|
+
subscribe_to_redis_channel(
|
|
527
|
+
websocket,
|
|
528
|
+
f"traces:{organization_id}",
|
|
529
|
+
organization_id,
|
|
530
|
+
trace_id=trace_id, # Filter by this trace
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Handle incoming messages (ping/pong, etc.)
|
|
535
|
+
try:
|
|
536
|
+
while True:
|
|
537
|
+
try:
|
|
538
|
+
message = await asyncio.wait_for(websocket.receive_text(), timeout=60.0)
|
|
539
|
+
data = json.loads(message)
|
|
540
|
+
|
|
541
|
+
if data.get("type") == "ping":
|
|
542
|
+
await send_json(websocket, "pong", {
|
|
543
|
+
"timestamp": int(datetime.now(timezone.utc).timestamp() * 1000)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
except asyncio.TimeoutError:
|
|
547
|
+
# Check if trace is still running
|
|
548
|
+
db.refresh(trace)
|
|
549
|
+
if trace.status and trace.status.value != "running":
|
|
550
|
+
await send_json(websocket, "trace_completed", {
|
|
551
|
+
"trace_id": trace_id,
|
|
552
|
+
"status": trace.status.value,
|
|
553
|
+
"duration_ms": trace.duration_ms,
|
|
554
|
+
})
|
|
555
|
+
break
|
|
556
|
+
|
|
557
|
+
# Send ping to keep connection alive
|
|
558
|
+
await send_json(websocket, "ping", {})
|
|
559
|
+
|
|
560
|
+
except WebSocketDisconnect:
|
|
561
|
+
logger.info("trace_detail_websocket_disconnected", trace_id=trace_id[:8])
|
|
562
|
+
|
|
563
|
+
finally:
|
|
564
|
+
redis_task.cancel()
|
|
565
|
+
try:
|
|
566
|
+
await redis_task
|
|
567
|
+
except asyncio.CancelledError:
|
|
568
|
+
pass
|
|
569
|
+
|
|
570
|
+
except WebSocketDisconnect:
|
|
571
|
+
logger.info("trace_detail_websocket_disconnected_early", trace_id=trace_id[:8])
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.error("trace_detail_websocket_error", error=str(e), trace_id=trace_id[:8], exc_info=True)
|
|
574
|
+
finally:
|
|
575
|
+
if organization_id:
|
|
576
|
+
await trace_manager.disconnect(websocket)
|