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,693 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LiveEventSource for real-time event streaming from Redis.
|
|
3
|
+
|
|
4
|
+
This module provides the LiveEventSource class that handles real-time event streaming
|
|
5
|
+
from Redis with proper polling, deduplication, and workflow completion detection.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Redis polling with explicit 50ms sleep interval to prevent CPU spinning
|
|
9
|
+
- Integration with MessageDeduplicator to filter duplicate events
|
|
10
|
+
- Workflow completion detection via Temporal status monitoring
|
|
11
|
+
- Graceful degradation when Redis or Temporal unavailable
|
|
12
|
+
- Keepalive and timeout handling for long-running streams
|
|
13
|
+
- Support for both Upstash REST API and standard redis-py clients
|
|
14
|
+
|
|
15
|
+
Test Strategy:
|
|
16
|
+
- Test Redis polling with mock events at 50ms intervals
|
|
17
|
+
- Test deduplication of overlapping events (history + live)
|
|
18
|
+
- Test completion detection stops streaming when workflow reaches terminal state
|
|
19
|
+
- Test timeout behavior (0 = no timeout, streams until task completes)
|
|
20
|
+
- Test keepalive events sent every 15 seconds
|
|
21
|
+
- Test graceful degradation when Redis unavailable
|
|
22
|
+
- Test graceful degradation when Temporal unavailable
|
|
23
|
+
- Test various event types: message, status, tool_started, tool_completed, etc.
|
|
24
|
+
- Test Redis LLEN and LRANGE operations with both client types
|
|
25
|
+
- Test terminal states: COMPLETED, FAILED, TERMINATED, CANCELLED
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import json
|
|
30
|
+
import time
|
|
31
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
import structlog
|
|
34
|
+
|
|
35
|
+
from .deduplication import MessageDeduplicator
|
|
36
|
+
|
|
37
|
+
logger = structlog.get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LiveEventSource:
|
|
41
|
+
"""
|
|
42
|
+
Handles real-time event streaming from Redis with polling and completion detection.
|
|
43
|
+
|
|
44
|
+
This class polls Redis for new events at 50ms intervals, deduplicates events
|
|
45
|
+
against historical data, and monitors workflow status to detect completion.
|
|
46
|
+
|
|
47
|
+
The streaming stops when:
|
|
48
|
+
1. Workflow reaches terminal state (COMPLETED, FAILED, CANCELLED, TERMINATED)
|
|
49
|
+
2. Timeout is reached (default 0 = no timeout, streams until task completes)
|
|
50
|
+
3. An exception occurs that cannot be recovered
|
|
51
|
+
|
|
52
|
+
Events are yielded as dictionaries with the following structure:
|
|
53
|
+
{
|
|
54
|
+
"event_type": "message" | "status" | "tool_started" | "tool_completed" | ...,
|
|
55
|
+
"data": {...}, # Event-specific data
|
|
56
|
+
"timestamp": "2024-01-15T10:30:00Z"
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# Terminal workflow states that indicate streaming should stop
|
|
61
|
+
TERMINAL_STATES = {"COMPLETED", "FAILED", "CANCELLED", "TERMINATED"}
|
|
62
|
+
|
|
63
|
+
# Terminal database execution states (lowercase, as stored in DB)
|
|
64
|
+
TERMINAL_DB_STATES = {"completed", "failed", "cancelled", "terminated", "interrupted"}
|
|
65
|
+
|
|
66
|
+
# Temporal status cache TTL (seconds) to reduce API load
|
|
67
|
+
TEMPORAL_STATUS_CACHE_TTL = 1.0
|
|
68
|
+
|
|
69
|
+
# Database status poll interval (seconds) - check for status changes
|
|
70
|
+
DB_STATUS_POLL_INTERVAL = 2.0
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
execution_id: str,
|
|
75
|
+
organization_id: str,
|
|
76
|
+
redis_client,
|
|
77
|
+
workflow_handle,
|
|
78
|
+
deduplicator: MessageDeduplicator,
|
|
79
|
+
timeout_seconds: int = 0, # 0 = no timeout, stream until task completes
|
|
80
|
+
keepalive_interval: int = 15,
|
|
81
|
+
db_session=None, # SQLAlchemy session for status polling
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Initialize LiveEventSource.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
execution_id: Execution ID to stream events for
|
|
88
|
+
organization_id: Organization ID for authorization
|
|
89
|
+
redis_client: Redis client instance (UpstashRedisClient or StandardRedisClient)
|
|
90
|
+
workflow_handle: Temporal workflow handle for status checks (can be None)
|
|
91
|
+
deduplicator: MessageDeduplicator instance for filtering duplicates
|
|
92
|
+
timeout_seconds: Maximum streaming duration in seconds (default: 0 = no timeout)
|
|
93
|
+
keepalive_interval: Seconds between keepalive messages (default: 15)
|
|
94
|
+
db_session: Database session for polling execution status (optional)
|
|
95
|
+
"""
|
|
96
|
+
self.execution_id = execution_id
|
|
97
|
+
self.organization_id = organization_id
|
|
98
|
+
self.redis_client = redis_client
|
|
99
|
+
self.workflow_handle = workflow_handle
|
|
100
|
+
self.deduplicator = deduplicator
|
|
101
|
+
self.timeout_seconds = timeout_seconds
|
|
102
|
+
self.keepalive_interval = keepalive_interval
|
|
103
|
+
self.db_session = db_session
|
|
104
|
+
|
|
105
|
+
# Streaming state
|
|
106
|
+
self._start_time = None
|
|
107
|
+
self._last_keepalive = None
|
|
108
|
+
self._last_redis_index = -1 # Track last processed Redis event index
|
|
109
|
+
self._stopped = False
|
|
110
|
+
self._is_workflow_running = True # Assume running until proven otherwise
|
|
111
|
+
|
|
112
|
+
# Temporal status caching
|
|
113
|
+
self._cached_temporal_status = None
|
|
114
|
+
self._cached_workflow_description = None
|
|
115
|
+
self._last_temporal_check = 0
|
|
116
|
+
|
|
117
|
+
# Database status caching
|
|
118
|
+
self._cached_db_status = None
|
|
119
|
+
self._last_db_status_check = 0
|
|
120
|
+
|
|
121
|
+
# Redis key for events
|
|
122
|
+
self._redis_key = f"execution:{execution_id}:events"
|
|
123
|
+
|
|
124
|
+
logger.info(
|
|
125
|
+
"live_event_source_initialized",
|
|
126
|
+
execution_id=execution_id[:8],
|
|
127
|
+
timeout_seconds=timeout_seconds,
|
|
128
|
+
keepalive_interval=keepalive_interval,
|
|
129
|
+
has_workflow_handle=workflow_handle is not None,
|
|
130
|
+
has_redis_client=redis_client is not None,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def stream(self) -> AsyncGenerator[Dict[str, Any], None]:
|
|
134
|
+
"""
|
|
135
|
+
Stream live events from Redis until workflow completes or timeout.
|
|
136
|
+
|
|
137
|
+
This is the main entry point for streaming. It polls Redis every 50ms
|
|
138
|
+
for new events, deduplicates them, and yields them as dictionaries.
|
|
139
|
+
|
|
140
|
+
Yields:
|
|
141
|
+
Event dictionaries with event_type, data, and timestamp
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
```python
|
|
145
|
+
async for event in live_source.stream():
|
|
146
|
+
print(f"Event: {event['event_type']}")
|
|
147
|
+
# Process event...
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
self._start_time = time.time()
|
|
151
|
+
self._last_keepalive = self._start_time
|
|
152
|
+
|
|
153
|
+
logger.info(
|
|
154
|
+
"live_streaming_started",
|
|
155
|
+
execution_id=self.execution_id[:8],
|
|
156
|
+
timeout_seconds=self.timeout_seconds,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
while not self.should_stop:
|
|
161
|
+
current_time = time.time()
|
|
162
|
+
|
|
163
|
+
# Check timeout (skip if timeout_seconds is 0 = no timeout)
|
|
164
|
+
elapsed = current_time - self._start_time
|
|
165
|
+
if self.timeout_seconds > 0 and elapsed >= self.timeout_seconds:
|
|
166
|
+
logger.warning(
|
|
167
|
+
"live_streaming_timeout",
|
|
168
|
+
execution_id=self.execution_id[:8],
|
|
169
|
+
elapsed_seconds=int(elapsed),
|
|
170
|
+
timeout_seconds=self.timeout_seconds,
|
|
171
|
+
)
|
|
172
|
+
self._stopped = True
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
# Send keepalive if needed
|
|
176
|
+
if current_time - self._last_keepalive >= self.keepalive_interval:
|
|
177
|
+
yield self._create_keepalive_event()
|
|
178
|
+
self._last_keepalive = current_time
|
|
179
|
+
|
|
180
|
+
# Check workflow completion (if handle available)
|
|
181
|
+
if self.workflow_handle is not None:
|
|
182
|
+
try:
|
|
183
|
+
is_complete = await self._check_completion()
|
|
184
|
+
if is_complete:
|
|
185
|
+
logger.info(
|
|
186
|
+
"workflow_completed_stopping_stream",
|
|
187
|
+
execution_id=self.execution_id[:8],
|
|
188
|
+
status=self._cached_temporal_status,
|
|
189
|
+
)
|
|
190
|
+
self._stopped = True
|
|
191
|
+
break
|
|
192
|
+
except Exception as e:
|
|
193
|
+
# Don't fail streaming if status check fails
|
|
194
|
+
logger.warning(
|
|
195
|
+
"workflow_status_check_failed",
|
|
196
|
+
execution_id=self.execution_id[:8],
|
|
197
|
+
error=str(e),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Check database status for changes (every 2 seconds)
|
|
201
|
+
db_status_event = await self._check_db_status()
|
|
202
|
+
if db_status_event:
|
|
203
|
+
# Emit status change event
|
|
204
|
+
yield db_status_event
|
|
205
|
+
|
|
206
|
+
# Poll Redis for new events
|
|
207
|
+
if self.redis_client:
|
|
208
|
+
try:
|
|
209
|
+
new_events = await self._poll_redis(self._last_redis_index)
|
|
210
|
+
for event in new_events:
|
|
211
|
+
# Deduplicate event
|
|
212
|
+
if self.deduplicator.is_sent(event):
|
|
213
|
+
logger.debug(
|
|
214
|
+
"duplicate_event_skipped",
|
|
215
|
+
execution_id=self.execution_id[:8],
|
|
216
|
+
event_type=event.get("event_type"),
|
|
217
|
+
message_id=event.get("message_id"),
|
|
218
|
+
)
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
# Mark as sent and yield
|
|
222
|
+
self.deduplicator.mark_sent(event)
|
|
223
|
+
yield event
|
|
224
|
+
except Exception as redis_error:
|
|
225
|
+
# Log error but don't fail streaming
|
|
226
|
+
logger.error(
|
|
227
|
+
"redis_poll_error",
|
|
228
|
+
execution_id=self.execution_id[:8],
|
|
229
|
+
error=str(redis_error),
|
|
230
|
+
)
|
|
231
|
+
# Yield degraded state notification
|
|
232
|
+
yield self._create_degraded_event(str(redis_error))
|
|
233
|
+
|
|
234
|
+
# Sleep 50ms before next poll - reduced from 200ms for faster event delivery
|
|
235
|
+
# This 4x improvement significantly reduces perceived latency
|
|
236
|
+
await asyncio.sleep(0.05)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(
|
|
240
|
+
"live_streaming_error",
|
|
241
|
+
execution_id=self.execution_id[:8],
|
|
242
|
+
error=str(e),
|
|
243
|
+
)
|
|
244
|
+
raise
|
|
245
|
+
finally:
|
|
246
|
+
elapsed = time.time() - self._start_time
|
|
247
|
+
logger.info(
|
|
248
|
+
"live_streaming_stopped",
|
|
249
|
+
execution_id=self.execution_id[:8],
|
|
250
|
+
elapsed_seconds=int(elapsed),
|
|
251
|
+
events_processed=self._last_redis_index + 1,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
async def _poll_redis(self, last_index: int) -> List[Dict[str, Any]]:
|
|
255
|
+
"""
|
|
256
|
+
Poll Redis for new events since last_index.
|
|
257
|
+
|
|
258
|
+
This method:
|
|
259
|
+
1. Uses LLEN to get total event count
|
|
260
|
+
2. Uses LRANGE to get new events since last index
|
|
261
|
+
3. Parses events from JSON
|
|
262
|
+
4. Reverses them to chronological order (Redis uses LPUSH, newest first)
|
|
263
|
+
5. Extracts message_id for deduplication
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
last_index: Last processed event index (0-based)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of new event dictionaries in chronological order
|
|
270
|
+
|
|
271
|
+
Note:
|
|
272
|
+
Redis stores events in reverse chronological order (LPUSH adds to head).
|
|
273
|
+
We reverse them here to get chronological order (oldest first).
|
|
274
|
+
"""
|
|
275
|
+
if not self.redis_client:
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
# Get total event count
|
|
280
|
+
total_events = await self.redis_client.llen(self._redis_key)
|
|
281
|
+
|
|
282
|
+
if total_events is None or total_events == 0:
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
# Check if there are new events
|
|
286
|
+
if total_events <= (last_index + 1):
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
logger.debug(
|
|
290
|
+
"redis_new_events_found",
|
|
291
|
+
execution_id=self.execution_id[:8],
|
|
292
|
+
total=total_events,
|
|
293
|
+
last_index=last_index,
|
|
294
|
+
new_count=total_events - (last_index + 1),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Get all events (they're in reverse chronological order from LPUSH)
|
|
298
|
+
all_redis_events = await self.redis_client.lrange(self._redis_key, 0, -1)
|
|
299
|
+
|
|
300
|
+
if not all_redis_events:
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
# Reverse to get chronological order (oldest first)
|
|
304
|
+
chronological_events = list(reversed(all_redis_events))
|
|
305
|
+
|
|
306
|
+
# Extract only NEW events we haven't processed yet
|
|
307
|
+
new_events = []
|
|
308
|
+
for i in range(last_index + 1, len(chronological_events)):
|
|
309
|
+
event_json = chronological_events[i]
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
event_data = json.loads(event_json)
|
|
313
|
+
event_type = event_data.get("event_type", "message")
|
|
314
|
+
|
|
315
|
+
# Extract payload based on event structure
|
|
316
|
+
# Two formats:
|
|
317
|
+
# 1. Message events: flat structure {role, content, timestamp, message_id}
|
|
318
|
+
# 2. Other events: nested {event_type, data: {...}, timestamp}
|
|
319
|
+
if "data" in event_data and isinstance(event_data["data"], dict):
|
|
320
|
+
if event_type == "message" and "role" in event_data["data"]:
|
|
321
|
+
# Message events expect flat structure
|
|
322
|
+
payload = event_data["data"].copy()
|
|
323
|
+
else:
|
|
324
|
+
# Chunk events and others expect nested structure
|
|
325
|
+
payload = {
|
|
326
|
+
"data": event_data["data"],
|
|
327
|
+
"timestamp": event_data.get("timestamp"),
|
|
328
|
+
}
|
|
329
|
+
else:
|
|
330
|
+
# Fallback for legacy format
|
|
331
|
+
payload = event_data.copy()
|
|
332
|
+
|
|
333
|
+
# Ensure message_id exists for deduplication
|
|
334
|
+
if event_type == "message" and isinstance(payload, dict):
|
|
335
|
+
if not payload.get("message_id"):
|
|
336
|
+
# Generate stable message_id
|
|
337
|
+
timestamp = payload.get("timestamp") or event_data.get("timestamp")
|
|
338
|
+
role = payload.get("role", "unknown")
|
|
339
|
+
if timestamp:
|
|
340
|
+
try:
|
|
341
|
+
from datetime import datetime
|
|
342
|
+
timestamp_micros = int(
|
|
343
|
+
datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()
|
|
344
|
+
* 1000000
|
|
345
|
+
)
|
|
346
|
+
except Exception:
|
|
347
|
+
timestamp_micros = int(time.time() * 1000000)
|
|
348
|
+
else:
|
|
349
|
+
timestamp_micros = int(time.time() * 1000000)
|
|
350
|
+
|
|
351
|
+
generated_id = f"{self.execution_id}_{role}_{timestamp_micros}"
|
|
352
|
+
payload["message_id"] = generated_id
|
|
353
|
+
|
|
354
|
+
logger.debug(
|
|
355
|
+
"generated_message_id_for_redis_event",
|
|
356
|
+
execution_id=self.execution_id[:8],
|
|
357
|
+
role=role,
|
|
358
|
+
generated_id=generated_id,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Store event type and payload
|
|
362
|
+
event = {
|
|
363
|
+
"event_type": event_type,
|
|
364
|
+
**payload, # Merge payload into event
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
new_events.append(event)
|
|
368
|
+
|
|
369
|
+
# Update last processed index
|
|
370
|
+
self._last_redis_index = i
|
|
371
|
+
|
|
372
|
+
logger.debug(
|
|
373
|
+
"redis_event_parsed",
|
|
374
|
+
execution_id=self.execution_id[:8],
|
|
375
|
+
event_type=event_type,
|
|
376
|
+
index=i,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
except json.JSONDecodeError as e:
|
|
380
|
+
logger.warning(
|
|
381
|
+
"invalid_redis_event_json",
|
|
382
|
+
execution_id=self.execution_id[:8],
|
|
383
|
+
event=event_json[:100],
|
|
384
|
+
error=str(e),
|
|
385
|
+
)
|
|
386
|
+
# Update index even for invalid events
|
|
387
|
+
self._last_redis_index = i
|
|
388
|
+
continue
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.error(
|
|
391
|
+
"redis_event_processing_error",
|
|
392
|
+
execution_id=self.execution_id[:8],
|
|
393
|
+
event=event_json[:100],
|
|
394
|
+
error=str(e),
|
|
395
|
+
)
|
|
396
|
+
# Update index even for failed events
|
|
397
|
+
self._last_redis_index = i
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
return new_events
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(
|
|
404
|
+
"redis_poll_failed",
|
|
405
|
+
execution_id=self.execution_id[:8],
|
|
406
|
+
error=str(e),
|
|
407
|
+
)
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
async def _check_completion(self) -> bool:
|
|
411
|
+
"""
|
|
412
|
+
Check if workflow is in terminal state (completed, failed, cancelled).
|
|
413
|
+
|
|
414
|
+
This method uses cached status if within TTL (1 second) to reduce
|
|
415
|
+
Temporal API load. Fresh status is fetched if cache expired.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
True if workflow is in terminal state, False otherwise
|
|
419
|
+
|
|
420
|
+
Note:
|
|
421
|
+
Temporal execution status enum values:
|
|
422
|
+
- RUNNING: Workflow is actively processing
|
|
423
|
+
- COMPLETED: Workflow completed successfully
|
|
424
|
+
- FAILED: Workflow failed with error
|
|
425
|
+
- CANCELLED: Workflow was cancelled by user
|
|
426
|
+
- TERMINATED: Workflow was terminated
|
|
427
|
+
- TIMED_OUT: Workflow exceeded timeout
|
|
428
|
+
- CONTINUED_AS_NEW: Workflow continued as new execution
|
|
429
|
+
"""
|
|
430
|
+
if not self.workflow_handle:
|
|
431
|
+
# No workflow handle - can't check completion
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
current_time = time.time()
|
|
436
|
+
|
|
437
|
+
# Use cached status if within TTL
|
|
438
|
+
if (
|
|
439
|
+
self._cached_temporal_status
|
|
440
|
+
and (current_time - self._last_temporal_check) < self.TEMPORAL_STATUS_CACHE_TTL
|
|
441
|
+
):
|
|
442
|
+
temporal_status = self._cached_temporal_status
|
|
443
|
+
logger.debug(
|
|
444
|
+
"using_cached_temporal_status",
|
|
445
|
+
execution_id=self.execution_id[:8],
|
|
446
|
+
status=temporal_status,
|
|
447
|
+
)
|
|
448
|
+
else:
|
|
449
|
+
# Cache expired or not set - fetch fresh status
|
|
450
|
+
t0 = time.time()
|
|
451
|
+
description = await self.workflow_handle.describe()
|
|
452
|
+
temporal_status = description.status.name # Get enum name (e.g., "RUNNING")
|
|
453
|
+
describe_duration = int((time.time() - t0) * 1000)
|
|
454
|
+
|
|
455
|
+
# Update cache
|
|
456
|
+
self._cached_temporal_status = temporal_status
|
|
457
|
+
self._cached_workflow_description = description
|
|
458
|
+
self._last_temporal_check = t0
|
|
459
|
+
|
|
460
|
+
logger.debug(
|
|
461
|
+
"temporal_status_fetched",
|
|
462
|
+
execution_id=self.execution_id[:8],
|
|
463
|
+
status=temporal_status,
|
|
464
|
+
duration_ms=describe_duration,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Log slow describe calls (>100ms)
|
|
468
|
+
if describe_duration > 100:
|
|
469
|
+
logger.warning(
|
|
470
|
+
"slow_temporal_describe",
|
|
471
|
+
execution_id=self.execution_id[:8],
|
|
472
|
+
duration_ms=describe_duration,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Update running state
|
|
476
|
+
previous_running = self._is_workflow_running
|
|
477
|
+
self._is_workflow_running = temporal_status == "RUNNING"
|
|
478
|
+
|
|
479
|
+
# Log state changes
|
|
480
|
+
if previous_running != self._is_workflow_running:
|
|
481
|
+
logger.info(
|
|
482
|
+
"workflow_running_state_changed",
|
|
483
|
+
execution_id=self.execution_id[:8],
|
|
484
|
+
temporal_status=temporal_status,
|
|
485
|
+
is_running=self._is_workflow_running,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Check if terminal state reached
|
|
489
|
+
return temporal_status in self.TERMINAL_STATES
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
error_msg = str(e).lower()
|
|
493
|
+
|
|
494
|
+
# Check if workflow is not found - could be:
|
|
495
|
+
# 1. Not started yet (race condition) - keep waiting
|
|
496
|
+
# 2. Already completed and cleaned up - check DB
|
|
497
|
+
if "workflow not found" in error_msg or "not found" in error_msg:
|
|
498
|
+
# Track how many times we've seen "not found"
|
|
499
|
+
if not hasattr(self, '_not_found_count'):
|
|
500
|
+
self._not_found_count = 0
|
|
501
|
+
self._not_found_count += 1
|
|
502
|
+
|
|
503
|
+
# First few times: might not be started yet, wait and retry
|
|
504
|
+
if self._not_found_count <= 10: # ~10 seconds of retries
|
|
505
|
+
logger.debug(
|
|
506
|
+
"workflow_not_found_waiting",
|
|
507
|
+
execution_id=self.execution_id[:8],
|
|
508
|
+
attempt=self._not_found_count,
|
|
509
|
+
)
|
|
510
|
+
return False # Keep streaming, check again later
|
|
511
|
+
|
|
512
|
+
# After 10 attempts: workflow probably doesn't exist
|
|
513
|
+
logger.info(
|
|
514
|
+
"workflow_not_found_stopping_temporal_polling",
|
|
515
|
+
execution_id=self.execution_id[:8],
|
|
516
|
+
attempts=self._not_found_count,
|
|
517
|
+
)
|
|
518
|
+
# Disable further workflow checks by clearing the handle
|
|
519
|
+
# Stream will continue from Redis/DB only
|
|
520
|
+
self.workflow_handle = None
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
logger.warning(
|
|
524
|
+
"temporal_status_check_error",
|
|
525
|
+
execution_id=self.execution_id[:8],
|
|
526
|
+
error=str(e),
|
|
527
|
+
)
|
|
528
|
+
# Don't treat transient errors as completion - keep streaming
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
async def _check_db_status(self) -> Optional[Dict[str, Any]]:
|
|
532
|
+
"""
|
|
533
|
+
Check database execution status and emit status event if changed.
|
|
534
|
+
|
|
535
|
+
This polls the database every 2 seconds to detect status changes
|
|
536
|
+
(running, waiting_for_input, completed, failed, etc.) and returns
|
|
537
|
+
a status event if the status has changed since last check.
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Status event dict if status changed, None otherwise
|
|
541
|
+
"""
|
|
542
|
+
if not self.db_session:
|
|
543
|
+
return None
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
current_time = time.time()
|
|
547
|
+
|
|
548
|
+
# Check if we should poll (respect poll interval)
|
|
549
|
+
if (current_time - self._last_db_status_check) < self.DB_STATUS_POLL_INTERVAL:
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
# Query execution status from database
|
|
553
|
+
from control_plane_api.app.models.execution import Execution
|
|
554
|
+
import uuid as uuid_module
|
|
555
|
+
|
|
556
|
+
execution = self.db_session.query(Execution).filter(
|
|
557
|
+
Execution.id == uuid_module.UUID(self.execution_id),
|
|
558
|
+
Execution.organization_id == self.organization_id
|
|
559
|
+
).first()
|
|
560
|
+
|
|
561
|
+
if not execution:
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
db_status = execution.status
|
|
565
|
+
self._last_db_status_check = current_time
|
|
566
|
+
|
|
567
|
+
# Check if status changed
|
|
568
|
+
status_event = None
|
|
569
|
+
if db_status != self._cached_db_status:
|
|
570
|
+
logger.info(
|
|
571
|
+
"execution_status_changed",
|
|
572
|
+
execution_id=self.execution_id[:8],
|
|
573
|
+
old_status=self._cached_db_status,
|
|
574
|
+
new_status=db_status,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
self._cached_db_status = db_status
|
|
578
|
+
|
|
579
|
+
# Return status event
|
|
580
|
+
status_event = {
|
|
581
|
+
"event_type": "status",
|
|
582
|
+
"status": db_status,
|
|
583
|
+
"data": {
|
|
584
|
+
"execution_id": self.execution_id,
|
|
585
|
+
"status": db_status,
|
|
586
|
+
"source": "database_poll",
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
# Check if DB status is a terminal state - signal completion
|
|
591
|
+
# This is a fallback when Temporal workflow handle is unavailable
|
|
592
|
+
if db_status in self.TERMINAL_DB_STATES:
|
|
593
|
+
logger.info(
|
|
594
|
+
"db_terminal_state_detected",
|
|
595
|
+
execution_id=self.execution_id[:8],
|
|
596
|
+
db_status=db_status,
|
|
597
|
+
)
|
|
598
|
+
self._stopped = True
|
|
599
|
+
|
|
600
|
+
return status_event
|
|
601
|
+
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.warning(
|
|
604
|
+
"db_status_check_error",
|
|
605
|
+
execution_id=self.execution_id[:8],
|
|
606
|
+
error=str(e),
|
|
607
|
+
)
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
@property
|
|
611
|
+
def should_stop(self) -> bool:
|
|
612
|
+
"""
|
|
613
|
+
Whether streaming should stop.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
True if streaming should stop (completed or timeout), False otherwise
|
|
617
|
+
"""
|
|
618
|
+
return self._stopped
|
|
619
|
+
|
|
620
|
+
def _create_keepalive_event(self) -> Dict[str, Any]:
|
|
621
|
+
"""
|
|
622
|
+
Create a keepalive event.
|
|
623
|
+
|
|
624
|
+
Keepalive events are sent periodically to prevent connection timeout
|
|
625
|
+
and to inform the client that streaming is still active.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
Keepalive event dictionary
|
|
629
|
+
"""
|
|
630
|
+
elapsed = time.time() - self._start_time
|
|
631
|
+
remaining = max(0, self.timeout_seconds - elapsed)
|
|
632
|
+
|
|
633
|
+
event = {
|
|
634
|
+
"event_type": "keepalive",
|
|
635
|
+
"data": {
|
|
636
|
+
"execution_id": self.execution_id,
|
|
637
|
+
"elapsed_seconds": int(elapsed),
|
|
638
|
+
"remaining_seconds": int(remaining),
|
|
639
|
+
},
|
|
640
|
+
"timestamp": self._current_timestamp(),
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
logger.debug(
|
|
644
|
+
"keepalive_sent",
|
|
645
|
+
execution_id=self.execution_id[:8],
|
|
646
|
+
elapsed_seconds=int(elapsed),
|
|
647
|
+
remaining_seconds=int(remaining),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return event
|
|
651
|
+
|
|
652
|
+
def _create_degraded_event(self, reason: str) -> Dict[str, Any]:
|
|
653
|
+
"""
|
|
654
|
+
Create a degraded state event.
|
|
655
|
+
|
|
656
|
+
Degraded events inform the client that streaming quality is reduced
|
|
657
|
+
due to Redis unavailability or other issues.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
reason: Reason for degraded state
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
Degraded event dictionary
|
|
664
|
+
"""
|
|
665
|
+
event = {
|
|
666
|
+
"event_type": "degraded",
|
|
667
|
+
"data": {
|
|
668
|
+
"reason": "redis_unavailable",
|
|
669
|
+
"fallback": "temporal_polling",
|
|
670
|
+
"message": f"Real-time events unavailable: {reason}",
|
|
671
|
+
"execution_id": self.execution_id,
|
|
672
|
+
},
|
|
673
|
+
"timestamp": self._current_timestamp(),
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
logger.warning(
|
|
677
|
+
"degraded_state_notification",
|
|
678
|
+
execution_id=self.execution_id[:8],
|
|
679
|
+
reason=reason,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return event
|
|
683
|
+
|
|
684
|
+
def _current_timestamp(self) -> str:
|
|
685
|
+
"""
|
|
686
|
+
Get current timestamp in ISO format.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
ISO format timestamp string (e.g., "2024-01-15T10:30:00Z")
|
|
690
|
+
"""
|
|
691
|
+
from datetime import datetime, timezone
|
|
692
|
+
|
|
693
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|