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,803 @@
|
|
|
1
|
+
"""Redis client for caching authentication tokens and user data.
|
|
2
|
+
|
|
3
|
+
Supports two modes:
|
|
4
|
+
1. Upstash REST API (serverless-friendly) - uses KV_REST_API_URL/TOKEN or UPSTASH_* env vars
|
|
5
|
+
2. Standard Redis (TCP connection) - uses REDIS_URL env var (e.g., redis://localhost:6379)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
from typing import Optional, Any
|
|
11
|
+
import httpx
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger()
|
|
15
|
+
|
|
16
|
+
# Redis configuration cache
|
|
17
|
+
_redis_client: Optional[Any] = None
|
|
18
|
+
_redis_client_type: Optional[str] = None # "upstash" or "standard"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UpstashRedisClient:
|
|
22
|
+
"""Upstash Redis client using direct HTTP REST API calls (serverless-friendly)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, url: str, token: str):
|
|
25
|
+
self.url = url.rstrip('/')
|
|
26
|
+
self.token = token
|
|
27
|
+
self.headers = {
|
|
28
|
+
"Authorization": f"Bearer {token}",
|
|
29
|
+
"Content-Type": "application/json"
|
|
30
|
+
}
|
|
31
|
+
# Use a shared async client for connection reuse and better performance
|
|
32
|
+
# This avoids creating a new connection for every Redis operation
|
|
33
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
34
|
+
|
|
35
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
36
|
+
"""Get or create the shared HTTP client."""
|
|
37
|
+
if self._client is None or self._client.is_closed:
|
|
38
|
+
self._client = httpx.AsyncClient(
|
|
39
|
+
timeout=httpx.Timeout(5.0, connect=2.0),
|
|
40
|
+
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
|
|
41
|
+
)
|
|
42
|
+
return self._client
|
|
43
|
+
|
|
44
|
+
async def close(self) -> None:
|
|
45
|
+
"""Close the HTTP client."""
|
|
46
|
+
if self._client is not None and not self._client.is_closed:
|
|
47
|
+
await self._client.aclose()
|
|
48
|
+
self._client = None
|
|
49
|
+
|
|
50
|
+
async def get(self, key: str) -> Optional[str]:
|
|
51
|
+
"""Get value from Redis."""
|
|
52
|
+
try:
|
|
53
|
+
client = self._get_client()
|
|
54
|
+
response = await client.post(
|
|
55
|
+
f"{self.url}/get/{key}",
|
|
56
|
+
headers=self.headers
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if response.status_code == 200:
|
|
60
|
+
result = response.json()
|
|
61
|
+
return result.get("result")
|
|
62
|
+
|
|
63
|
+
logger.warning("redis_get_failed", status=response.status_code, key=key[:20])
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.warning("redis_get_error", error=str(e), key=key[:20])
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
async def mget(self, keys: list[str]) -> dict[str, Optional[str]]:
|
|
71
|
+
"""
|
|
72
|
+
Get multiple values from Redis in a single request using pipeline.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
keys: List of Redis keys to fetch
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dict mapping keys to their values (None if key doesn't exist)
|
|
79
|
+
"""
|
|
80
|
+
if not keys:
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Build pipeline commands for MGET
|
|
85
|
+
commands = [["GET", key] for key in keys]
|
|
86
|
+
|
|
87
|
+
client = self._get_client()
|
|
88
|
+
response = await client.post(
|
|
89
|
+
f"{self.url}/pipeline",
|
|
90
|
+
headers=self.headers,
|
|
91
|
+
json=commands
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if response.status_code == 200:
|
|
95
|
+
results = response.json()
|
|
96
|
+
# Map keys to their results
|
|
97
|
+
return {
|
|
98
|
+
key: results[i].get("result") if i < len(results) else None
|
|
99
|
+
for i, key in enumerate(keys)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.warning("redis_mget_failed", status=response.status_code, key_count=len(keys))
|
|
103
|
+
return {key: None for key in keys}
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning("redis_mget_error", error=str(e), key_count=len(keys))
|
|
107
|
+
return {key: None for key in keys}
|
|
108
|
+
|
|
109
|
+
async def set(self, key: str, value: str, ex: Optional[int] = None) -> bool:
|
|
110
|
+
"""Set value in Redis with optional expiry (seconds)."""
|
|
111
|
+
try:
|
|
112
|
+
# Build command
|
|
113
|
+
if ex:
|
|
114
|
+
command = ["SET", key, value, "EX", str(ex)]
|
|
115
|
+
else:
|
|
116
|
+
command = ["SET", key, value]
|
|
117
|
+
|
|
118
|
+
client = self._get_client()
|
|
119
|
+
response = await client.post(
|
|
120
|
+
f"{self.url}/pipeline",
|
|
121
|
+
headers=self.headers,
|
|
122
|
+
json=[command]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if response.status_code == 200:
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
logger.warning("redis_set_failed", status=response.status_code, key=key[:20])
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.warning("redis_set_error", error=str(e), key=key[:20])
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
async def setex(self, key: str, seconds: int, value: str) -> bool:
|
|
136
|
+
"""Set value in Redis with expiry (seconds). Alias for set with ex parameter."""
|
|
137
|
+
return await self.set(key, value, ex=seconds)
|
|
138
|
+
|
|
139
|
+
async def delete(self, key: str) -> bool:
|
|
140
|
+
"""Delete a key from Redis."""
|
|
141
|
+
try:
|
|
142
|
+
command = ["DEL", key]
|
|
143
|
+
|
|
144
|
+
client = self._get_client()
|
|
145
|
+
response = await client.post(
|
|
146
|
+
f"{self.url}/pipeline",
|
|
147
|
+
headers=self.headers,
|
|
148
|
+
json=[command]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if response.status_code == 200:
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
logger.warning("redis_delete_failed", status=response.status_code, key=key[:20])
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.warning("redis_delete_error", error=str(e), key=key[:20])
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
async def hset(self, key: str, mapping: dict) -> bool:
|
|
162
|
+
"""Set hash fields in Redis."""
|
|
163
|
+
try:
|
|
164
|
+
# Convert dict to list of field-value pairs
|
|
165
|
+
fields = []
|
|
166
|
+
for k, v in mapping.items():
|
|
167
|
+
fields.extend([k, str(v)])
|
|
168
|
+
|
|
169
|
+
command = ["HSET", key] + fields
|
|
170
|
+
|
|
171
|
+
client = self._get_client()
|
|
172
|
+
response = await client.post(
|
|
173
|
+
f"{self.url}/pipeline",
|
|
174
|
+
headers=self.headers,
|
|
175
|
+
json=[command]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if response.status_code == 200:
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
logger.warning("redis_hset_failed", status=response.status_code, key=key[:20])
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.warning("redis_hset_error", error=str(e), key=key[:20])
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
async def hgetall(self, key: str) -> Optional[dict]:
|
|
189
|
+
"""Get all hash fields from Redis."""
|
|
190
|
+
try:
|
|
191
|
+
client = self._get_client()
|
|
192
|
+
response = await client.post(
|
|
193
|
+
f"{self.url}/pipeline",
|
|
194
|
+
headers=self.headers,
|
|
195
|
+
json=[["HGETALL", key]]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if response.status_code == 200:
|
|
199
|
+
result = response.json()
|
|
200
|
+
if result and isinstance(result, list) and len(result) > 0:
|
|
201
|
+
data = result[0].get("result", [])
|
|
202
|
+
# Convert list to dict [k1, v1, k2, v2] -> {k1: v1, k2: v2}
|
|
203
|
+
return {data[i]: data[i+1] for i in range(0, len(data), 2)} if data else {}
|
|
204
|
+
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.warning("redis_hgetall_error", error=str(e), key=key[:20])
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
async def expire(self, key: str, seconds: int) -> bool:
|
|
212
|
+
"""Set expiry on a key."""
|
|
213
|
+
try:
|
|
214
|
+
client = self._get_client()
|
|
215
|
+
response = await client.post(
|
|
216
|
+
f"{self.url}/pipeline",
|
|
217
|
+
headers=self.headers,
|
|
218
|
+
json=[["EXPIRE", key, str(seconds)]]
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return response.status_code == 200
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.warning("redis_expire_error", error=str(e), key=key[:20])
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
async def sadd(self, key: str, *members: str) -> bool:
|
|
228
|
+
"""Add members to a set."""
|
|
229
|
+
try:
|
|
230
|
+
command = ["SADD", key] + list(members)
|
|
231
|
+
|
|
232
|
+
client = self._get_client()
|
|
233
|
+
response = await client.post(
|
|
234
|
+
f"{self.url}/pipeline",
|
|
235
|
+
headers=self.headers,
|
|
236
|
+
json=[command]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return response.status_code == 200
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.warning("redis_sadd_error", error=str(e), key=key[:20])
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
async def scard(self, key: str) -> int:
|
|
246
|
+
"""Get count of set members."""
|
|
247
|
+
try:
|
|
248
|
+
client = self._get_client()
|
|
249
|
+
response = await client.post(
|
|
250
|
+
f"{self.url}/pipeline",
|
|
251
|
+
headers=self.headers,
|
|
252
|
+
json=[["SCARD", key]]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if response.status_code == 200:
|
|
256
|
+
result = response.json()
|
|
257
|
+
if result and isinstance(result, list):
|
|
258
|
+
return result[0].get("result", 0)
|
|
259
|
+
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.warning("redis_scard_error", error=str(e), key=key[:20])
|
|
264
|
+
return 0
|
|
265
|
+
|
|
266
|
+
async def lpush(self, key: str, *values: str) -> bool:
|
|
267
|
+
"""Push values to start of list."""
|
|
268
|
+
try:
|
|
269
|
+
command = ["LPUSH", key] + list(values)
|
|
270
|
+
|
|
271
|
+
client = self._get_client()
|
|
272
|
+
response = await client.post(
|
|
273
|
+
f"{self.url}/pipeline",
|
|
274
|
+
headers=self.headers,
|
|
275
|
+
json=[command]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return response.status_code == 200
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.warning("redis_lpush_error", error=str(e), key=key[:20])
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
async def ltrim(self, key: str, start: int, stop: int) -> bool:
|
|
285
|
+
"""Trim list to specified range."""
|
|
286
|
+
try:
|
|
287
|
+
client = self._get_client()
|
|
288
|
+
response = await client.post(
|
|
289
|
+
f"{self.url}/pipeline",
|
|
290
|
+
headers=self.headers,
|
|
291
|
+
json=[["LTRIM", key, str(start), str(stop)]]
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return response.status_code == 200
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.warning("redis_ltrim_error", error=str(e), key=key[:20])
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
async def lrange(self, key: str, start: int, stop: int) -> list:
|
|
301
|
+
"""Get range of list elements."""
|
|
302
|
+
try:
|
|
303
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
304
|
+
response = await client.post(
|
|
305
|
+
f"{self.url}/pipeline",
|
|
306
|
+
headers=self.headers,
|
|
307
|
+
json=[["LRANGE", key, str(start), str(stop)]]
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if response.status_code == 200:
|
|
311
|
+
result = response.json()
|
|
312
|
+
if result and isinstance(result, list):
|
|
313
|
+
return result[0].get("result", [])
|
|
314
|
+
|
|
315
|
+
return []
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.warning("redis_lrange_error", error=str(e), key=key[:20])
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
async def llen(self, key: str) -> int:
|
|
322
|
+
"""Get length of list."""
|
|
323
|
+
try:
|
|
324
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
325
|
+
response = await client.post(
|
|
326
|
+
f"{self.url}/pipeline",
|
|
327
|
+
headers=self.headers,
|
|
328
|
+
json=[["LLEN", key]]
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if response.status_code == 200:
|
|
332
|
+
result = response.json()
|
|
333
|
+
if result and isinstance(result, list):
|
|
334
|
+
return result[0].get("result", 0)
|
|
335
|
+
|
|
336
|
+
return 0
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.warning("redis_llen_error", error=str(e), key=key[:20])
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
async def publish(self, channel: str, message: str) -> bool:
|
|
343
|
+
"""Publish message to Redis pub/sub channel."""
|
|
344
|
+
try:
|
|
345
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
346
|
+
response = await client.post(
|
|
347
|
+
f"{self.url}/pipeline",
|
|
348
|
+
headers=self.headers,
|
|
349
|
+
json=[["PUBLISH", channel, message]]
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if response.status_code == 200:
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
logger.warning("redis_publish_failed", status=response.status_code, channel=channel[:20])
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.warning("redis_publish_error", error=str(e), channel=channel[:20])
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
async def ttl(self, key: str) -> int:
|
|
363
|
+
"""Get TTL of a key in seconds."""
|
|
364
|
+
try:
|
|
365
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
366
|
+
response = await client.post(
|
|
367
|
+
f"{self.url}/pipeline",
|
|
368
|
+
headers=self.headers,
|
|
369
|
+
json=[["TTL", key]]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if response.status_code == 200:
|
|
373
|
+
result = response.json()
|
|
374
|
+
if result and isinstance(result, list):
|
|
375
|
+
return result[0].get("result", -2)
|
|
376
|
+
|
|
377
|
+
return -2
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.warning("redis_ttl_error", error=str(e), key=key[:20])
|
|
381
|
+
return -2
|
|
382
|
+
|
|
383
|
+
async def ping(self) -> bool:
|
|
384
|
+
"""Ping Redis to check connection."""
|
|
385
|
+
try:
|
|
386
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
387
|
+
response = await client.post(
|
|
388
|
+
f"{self.url}/pipeline",
|
|
389
|
+
headers=self.headers,
|
|
390
|
+
json=[["PING"]]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if response.status_code == 200:
|
|
394
|
+
result = response.json()
|
|
395
|
+
if result and isinstance(result, list):
|
|
396
|
+
return result[0].get("result") == "PONG"
|
|
397
|
+
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.warning("redis_ping_error", error=str(e))
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class StandardRedisClient:
|
|
406
|
+
"""Standard Redis client using redis-py with async support.
|
|
407
|
+
|
|
408
|
+
This client provides the same interface as UpstashRedisClient but uses
|
|
409
|
+
standard Redis protocol (TCP connection) instead of REST API.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
def __init__(self, url: str):
|
|
413
|
+
"""Initialize standard Redis client.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
url: Redis URL (e.g., redis://localhost:6379, redis://:password@host:port/db)
|
|
417
|
+
"""
|
|
418
|
+
try:
|
|
419
|
+
import redis.asyncio as aioredis
|
|
420
|
+
except ImportError:
|
|
421
|
+
raise ImportError(
|
|
422
|
+
"redis package is required for standard Redis connections. "
|
|
423
|
+
"Install it with: pip install redis"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
self.url = url
|
|
427
|
+
self._redis = aioredis.from_url(
|
|
428
|
+
url,
|
|
429
|
+
encoding="utf-8",
|
|
430
|
+
decode_responses=True,
|
|
431
|
+
socket_timeout=5.0,
|
|
432
|
+
socket_connect_timeout=5.0,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
async def get(self, key: str) -> Optional[str]:
|
|
436
|
+
"""Get value from Redis."""
|
|
437
|
+
try:
|
|
438
|
+
return await self._redis.get(key)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.warning("redis_get_error", error=str(e), key=key[:20])
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
async def mget(self, keys: list[str]) -> dict[str, Optional[str]]:
|
|
444
|
+
"""Get multiple values from Redis."""
|
|
445
|
+
if not keys:
|
|
446
|
+
return {}
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
values = await self._redis.mget(keys)
|
|
450
|
+
return {key: value for key, value in zip(keys, values)}
|
|
451
|
+
except Exception as e:
|
|
452
|
+
logger.warning("redis_mget_error", error=str(e), key_count=len(keys))
|
|
453
|
+
return {key: None for key in keys}
|
|
454
|
+
|
|
455
|
+
async def set(self, key: str, value: str, ex: Optional[int] = None) -> bool:
|
|
456
|
+
"""Set value in Redis with optional expiry (seconds)."""
|
|
457
|
+
try:
|
|
458
|
+
result = await self._redis.set(key, value, ex=ex)
|
|
459
|
+
return bool(result)
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.warning("redis_set_error", error=str(e), key=key[:20])
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
async def setex(self, key: str, seconds: int, value: str) -> bool:
|
|
465
|
+
"""Set value in Redis with expiry (seconds)."""
|
|
466
|
+
return await self.set(key, value, ex=seconds)
|
|
467
|
+
|
|
468
|
+
async def delete(self, key: str) -> bool:
|
|
469
|
+
"""Delete a key from Redis."""
|
|
470
|
+
try:
|
|
471
|
+
result = await self._redis.delete(key)
|
|
472
|
+
return result > 0
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.warning("redis_delete_error", error=str(e), key=key[:20])
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
async def hset(self, key: str, mapping: dict) -> bool:
|
|
478
|
+
"""Set hash fields in Redis."""
|
|
479
|
+
try:
|
|
480
|
+
await self._redis.hset(key, mapping=mapping)
|
|
481
|
+
return True
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.warning("redis_hset_error", error=str(e), key=key[:20])
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
async def hgetall(self, key: str) -> Optional[dict]:
|
|
487
|
+
"""Get all hash fields from Redis."""
|
|
488
|
+
try:
|
|
489
|
+
result = await self._redis.hgetall(key)
|
|
490
|
+
return result if result else {}
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.warning("redis_hgetall_error", error=str(e), key=key[:20])
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
async def expire(self, key: str, seconds: int) -> bool:
|
|
496
|
+
"""Set expiry on a key."""
|
|
497
|
+
try:
|
|
498
|
+
return await self._redis.expire(key, seconds)
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.warning("redis_expire_error", error=str(e), key=key[:20])
|
|
501
|
+
return False
|
|
502
|
+
|
|
503
|
+
async def sadd(self, key: str, *members: str) -> bool:
|
|
504
|
+
"""Add members to a set."""
|
|
505
|
+
try:
|
|
506
|
+
await self._redis.sadd(key, *members)
|
|
507
|
+
return True
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.warning("redis_sadd_error", error=str(e), key=key[:20])
|
|
510
|
+
return False
|
|
511
|
+
|
|
512
|
+
async def scard(self, key: str) -> int:
|
|
513
|
+
"""Get count of set members."""
|
|
514
|
+
try:
|
|
515
|
+
return await self._redis.scard(key) or 0
|
|
516
|
+
except Exception as e:
|
|
517
|
+
logger.warning("redis_scard_error", error=str(e), key=key[:20])
|
|
518
|
+
return 0
|
|
519
|
+
|
|
520
|
+
async def lpush(self, key: str, *values: str) -> bool:
|
|
521
|
+
"""Push values to start of list."""
|
|
522
|
+
try:
|
|
523
|
+
await self._redis.lpush(key, *values)
|
|
524
|
+
return True
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.warning("redis_lpush_error", error=str(e), key=key[:20])
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
async def ltrim(self, key: str, start: int, stop: int) -> bool:
|
|
530
|
+
"""Trim list to specified range."""
|
|
531
|
+
try:
|
|
532
|
+
await self._redis.ltrim(key, start, stop)
|
|
533
|
+
return True
|
|
534
|
+
except Exception as e:
|
|
535
|
+
logger.warning("redis_ltrim_error", error=str(e), key=key[:20])
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
async def lrange(self, key: str, start: int, stop: int) -> list:
|
|
539
|
+
"""Get range of list elements."""
|
|
540
|
+
try:
|
|
541
|
+
return await self._redis.lrange(key, start, stop) or []
|
|
542
|
+
except Exception as e:
|
|
543
|
+
logger.warning("redis_lrange_error", error=str(e), key=key[:20])
|
|
544
|
+
return []
|
|
545
|
+
|
|
546
|
+
async def llen(self, key: str) -> int:
|
|
547
|
+
"""Get length of list."""
|
|
548
|
+
try:
|
|
549
|
+
return await self._redis.llen(key) or 0
|
|
550
|
+
except Exception as e:
|
|
551
|
+
logger.warning("redis_llen_error", error=str(e), key=key[:20])
|
|
552
|
+
return 0
|
|
553
|
+
|
|
554
|
+
async def publish(self, channel: str, message: str) -> bool:
|
|
555
|
+
"""Publish message to Redis pub/sub channel."""
|
|
556
|
+
try:
|
|
557
|
+
await self._redis.publish(channel, message)
|
|
558
|
+
return True
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.warning("redis_publish_error", error=str(e), channel=channel[:20])
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
async def ttl(self, key: str) -> int:
|
|
564
|
+
"""Get TTL of a key in seconds."""
|
|
565
|
+
try:
|
|
566
|
+
return await self._redis.ttl(key)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
logger.warning("redis_ttl_error", error=str(e), key=key[:20])
|
|
569
|
+
return -2
|
|
570
|
+
|
|
571
|
+
async def ping(self) -> bool:
|
|
572
|
+
"""Ping Redis to check connection."""
|
|
573
|
+
try:
|
|
574
|
+
return await self._redis.ping()
|
|
575
|
+
except Exception as e:
|
|
576
|
+
logger.warning("redis_ping_error", error=str(e))
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# Type alias for either Redis client
|
|
581
|
+
RedisClient = UpstashRedisClient | StandardRedisClient
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _normalize_redis_url(url: str) -> str:
|
|
585
|
+
"""
|
|
586
|
+
Normalize a Redis URL to include the redis:// scheme if missing.
|
|
587
|
+
|
|
588
|
+
Handles various URL formats:
|
|
589
|
+
- redis://host:port -> redis://host:port (no change)
|
|
590
|
+
- rediss://host:port -> rediss://host:port (no change)
|
|
591
|
+
- host:port -> redis://host:port (add scheme)
|
|
592
|
+
- host:port/db -> redis://host:port/db (add scheme)
|
|
593
|
+
- :password@host:port -> redis://:password@host:port (add scheme)
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
url: Redis URL, possibly without scheme
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Normalized Redis URL with scheme
|
|
600
|
+
"""
|
|
601
|
+
if not url:
|
|
602
|
+
return url
|
|
603
|
+
|
|
604
|
+
url = url.strip()
|
|
605
|
+
|
|
606
|
+
# Already has scheme
|
|
607
|
+
if url.startswith(("redis://", "rediss://")):
|
|
608
|
+
return url
|
|
609
|
+
|
|
610
|
+
# Check if URL looks like a Redis connection string (has host:port pattern)
|
|
611
|
+
# Patterns: "host:port", "host:port/db", ":password@host:port"
|
|
612
|
+
if ":" in url:
|
|
613
|
+
# Default to redis:// scheme (non-TLS)
|
|
614
|
+
# If the URL has a password marker at the start, add scheme correctly
|
|
615
|
+
if url.startswith(":"):
|
|
616
|
+
# Format: :password@host:port
|
|
617
|
+
return f"redis://{url}"
|
|
618
|
+
else:
|
|
619
|
+
# Format: host:port or host:port/db
|
|
620
|
+
return f"redis://{url}"
|
|
621
|
+
|
|
622
|
+
return url
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def get_redis_client() -> Optional[RedisClient]:
|
|
626
|
+
"""
|
|
627
|
+
Get or create Redis client.
|
|
628
|
+
|
|
629
|
+
Supports two modes (checked in order):
|
|
630
|
+
1. Standard Redis URL (REDIS_URL) - e.g., redis://localhost:6379
|
|
631
|
+
2. Upstash REST API (KV_REST_API_URL + KV_REST_API_TOKEN)
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Redis client instance or None if not configured
|
|
635
|
+
"""
|
|
636
|
+
global _redis_client, _redis_client_type
|
|
637
|
+
|
|
638
|
+
# Return cached client if available
|
|
639
|
+
if _redis_client is not None:
|
|
640
|
+
return _redis_client
|
|
641
|
+
|
|
642
|
+
# Priority 1: Check for standard Redis URL
|
|
643
|
+
redis_url = os.getenv("REDIS_URL")
|
|
644
|
+
|
|
645
|
+
# Normalize URL format (add redis:// scheme if missing)
|
|
646
|
+
if redis_url:
|
|
647
|
+
original_url = redis_url
|
|
648
|
+
redis_url = _normalize_redis_url(redis_url)
|
|
649
|
+
if redis_url != original_url:
|
|
650
|
+
logger.info(
|
|
651
|
+
"redis_url_normalized",
|
|
652
|
+
original=original_url[:30] + "..." if len(original_url) > 30 else original_url,
|
|
653
|
+
normalized=redis_url[:30] + "..." if len(redis_url) > 30 else redis_url,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
if redis_url and redis_url.startswith(("redis://", "rediss://")):
|
|
657
|
+
try:
|
|
658
|
+
_redis_client = StandardRedisClient(url=redis_url)
|
|
659
|
+
_redis_client_type = "standard"
|
|
660
|
+
# Mask password in log
|
|
661
|
+
log_url = redis_url
|
|
662
|
+
if "@" in redis_url:
|
|
663
|
+
# redis://:password@host:port -> redis://***@host:port
|
|
664
|
+
parts = redis_url.split("@")
|
|
665
|
+
log_url = parts[0].rsplit(":", 1)[0] + ":***@" + parts[1]
|
|
666
|
+
logger.info("redis_client_created", type="standard", url=log_url[:50])
|
|
667
|
+
return _redis_client
|
|
668
|
+
except ImportError as e:
|
|
669
|
+
logger.warning(
|
|
670
|
+
"redis_standard_client_unavailable",
|
|
671
|
+
error=str(e),
|
|
672
|
+
message="Falling back to Upstash REST API if configured"
|
|
673
|
+
)
|
|
674
|
+
except Exception as e:
|
|
675
|
+
logger.error("redis_standard_client_init_failed", error=str(e))
|
|
676
|
+
|
|
677
|
+
# Priority 2: Check for Upstash REST API
|
|
678
|
+
upstash_url = (
|
|
679
|
+
os.getenv("KV_REST_API_URL") or
|
|
680
|
+
os.getenv("UPSTASH_REDIS_REST_URL") or
|
|
681
|
+
os.getenv("UPSTASH_REDIS_URL")
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
upstash_token = (
|
|
685
|
+
os.getenv("KV_REST_API_TOKEN") or
|
|
686
|
+
os.getenv("UPSTASH_REDIS_REST_TOKEN") or
|
|
687
|
+
os.getenv("UPSTASH_REDIS_TOKEN")
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
if upstash_url and upstash_token:
|
|
691
|
+
try:
|
|
692
|
+
_redis_client = UpstashRedisClient(url=upstash_url, token=upstash_token)
|
|
693
|
+
_redis_client_type = "upstash"
|
|
694
|
+
logger.info("redis_client_created", type="upstash", url=upstash_url[:30] + "...")
|
|
695
|
+
return _redis_client
|
|
696
|
+
except Exception as e:
|
|
697
|
+
logger.error("redis_upstash_client_init_failed", error=str(e))
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
# No Redis configured
|
|
701
|
+
logger.warning(
|
|
702
|
+
"redis_not_configured",
|
|
703
|
+
message="No Redis configuration found, caching disabled",
|
|
704
|
+
checked_vars=["REDIS_URL", "KV_REST_API_URL", "KV_REST_API_TOKEN", "UPSTASH_*"]
|
|
705
|
+
)
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# Worker-specific caching functions
|
|
710
|
+
|
|
711
|
+
async def cache_worker_heartbeat(
|
|
712
|
+
worker_id: str,
|
|
713
|
+
queue_id: str,
|
|
714
|
+
organization_id: str,
|
|
715
|
+
status: str,
|
|
716
|
+
last_heartbeat: str,
|
|
717
|
+
tasks_processed: int,
|
|
718
|
+
system_info: Optional[dict] = None,
|
|
719
|
+
ttl: int = 60
|
|
720
|
+
) -> bool:
|
|
721
|
+
"""
|
|
722
|
+
Cache worker heartbeat data in Redis.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
worker_id: Worker UUID
|
|
726
|
+
queue_id: Queue UUID
|
|
727
|
+
organization_id: Organization ID
|
|
728
|
+
status: Worker status
|
|
729
|
+
last_heartbeat: ISO timestamp
|
|
730
|
+
tasks_processed: Task count
|
|
731
|
+
system_info: Optional system metrics
|
|
732
|
+
ttl: Cache TTL in seconds
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
True if cached successfully
|
|
736
|
+
"""
|
|
737
|
+
client = get_redis_client()
|
|
738
|
+
if not client:
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
try:
|
|
742
|
+
data = {
|
|
743
|
+
"worker_id": worker_id,
|
|
744
|
+
"queue_id": queue_id,
|
|
745
|
+
"organization_id": organization_id,
|
|
746
|
+
"status": status,
|
|
747
|
+
"last_heartbeat": last_heartbeat,
|
|
748
|
+
"tasks_processed": tasks_processed,
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if system_info:
|
|
752
|
+
data["system_info"] = json.dumps(system_info)
|
|
753
|
+
|
|
754
|
+
# Cache worker status
|
|
755
|
+
await client.hset(f"worker:{worker_id}:status", data)
|
|
756
|
+
await client.expire(f"worker:{worker_id}:status", ttl)
|
|
757
|
+
|
|
758
|
+
# Add to queue workers set
|
|
759
|
+
await client.sadd(f"queue:{queue_id}:workers", worker_id)
|
|
760
|
+
await client.expire(f"queue:{queue_id}:workers", ttl)
|
|
761
|
+
|
|
762
|
+
logger.debug("worker_heartbeat_cached", worker_id=worker_id[:8])
|
|
763
|
+
return True
|
|
764
|
+
|
|
765
|
+
except Exception as e:
|
|
766
|
+
logger.error("cache_worker_heartbeat_failed", error=str(e), worker_id=worker_id[:8])
|
|
767
|
+
return False
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
async def cache_worker_logs(worker_id: str, logs: list, ttl: int = 300) -> bool:
|
|
771
|
+
"""Cache worker logs in Redis."""
|
|
772
|
+
client = get_redis_client()
|
|
773
|
+
if not client or not logs:
|
|
774
|
+
return False
|
|
775
|
+
|
|
776
|
+
try:
|
|
777
|
+
# Add logs to list
|
|
778
|
+
await client.lpush(f"worker:{worker_id}:logs", *logs)
|
|
779
|
+
# Keep only last 100 logs
|
|
780
|
+
await client.ltrim(f"worker:{worker_id}:logs", 0, 99)
|
|
781
|
+
# Set expiry
|
|
782
|
+
await client.expire(f"worker:{worker_id}:logs", ttl)
|
|
783
|
+
|
|
784
|
+
logger.debug("worker_logs_cached", worker_id=worker_id[:8], count=len(logs))
|
|
785
|
+
return True
|
|
786
|
+
|
|
787
|
+
except Exception as e:
|
|
788
|
+
logger.error("cache_worker_logs_failed", error=str(e), worker_id=worker_id[:8])
|
|
789
|
+
return False
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
async def get_queue_worker_count_cached(queue_id: str) -> Optional[int]:
|
|
793
|
+
"""Get active worker count for queue from cache."""
|
|
794
|
+
client = get_redis_client()
|
|
795
|
+
if not client:
|
|
796
|
+
return None
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
count = await client.scard(f"queue:{queue_id}:workers")
|
|
800
|
+
return count
|
|
801
|
+
except Exception as e:
|
|
802
|
+
logger.error("get_queue_worker_count_failed", error=str(e), queue_id=queue_id[:8])
|
|
803
|
+
return None
|