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,588 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message history loader for streaming execution data.
|
|
3
|
+
|
|
4
|
+
This module provides a HistoryLoader class that retrieves historical messages from
|
|
5
|
+
the database (PostgreSQL Session table) with Temporal workflow state as fallback.
|
|
6
|
+
Messages are streamed progressively using an async generator for non-blocking
|
|
7
|
+
rendering in the UI.
|
|
8
|
+
|
|
9
|
+
Key Features:
|
|
10
|
+
- Primary source: PostgreSQL Session table (fast, reliable)
|
|
11
|
+
- Fallback source: Temporal workflow state (when DB is empty)
|
|
12
|
+
- Progressive streaming: yields messages one-at-a-time for instant UI rendering
|
|
13
|
+
- Message deduplication: integrates with MessageDeduplicator
|
|
14
|
+
- Message limiting: caps at last 200 messages for performance
|
|
15
|
+
- Chronological sorting: ensures proper conversation flow
|
|
16
|
+
- Timeout protection: 3-second timeout for Temporal queries
|
|
17
|
+
|
|
18
|
+
Architecture:
|
|
19
|
+
This class is part of the Resumable Execution Stream Architecture:
|
|
20
|
+
1. HistoryLoader: Loads and streams historical messages (this module)
|
|
21
|
+
2. MessageDeduplicator: Prevents duplicate messages across history/live streams
|
|
22
|
+
3. LiveStreamProcessor: Handles real-time Redis stream events (future)
|
|
23
|
+
|
|
24
|
+
Test Strategy:
|
|
25
|
+
- Unit test database loading with various message counts (0, 1, 100, 300)
|
|
26
|
+
- Test Temporal fallback when DB empty
|
|
27
|
+
- Test yielding behavior (progressive, not batched)
|
|
28
|
+
- Test timeout handling for Temporal queries (3s limit)
|
|
29
|
+
- Test message sorting and chronological order
|
|
30
|
+
- Test message limit enforcement (200 cap)
|
|
31
|
+
- Test edge cases: no messages, DB errors, empty session record
|
|
32
|
+
- Integration test with MessageDeduplicator
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import hashlib
|
|
37
|
+
import time
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
40
|
+
|
|
41
|
+
import structlog
|
|
42
|
+
from sqlalchemy.orm import Session as SQLAlchemySession
|
|
43
|
+
|
|
44
|
+
from control_plane_api.app.models.session import Session as SessionModel
|
|
45
|
+
from control_plane_api.app.workflows.agent_execution import (
|
|
46
|
+
AgentExecutionWorkflow,
|
|
47
|
+
ChatMessage,
|
|
48
|
+
)
|
|
49
|
+
from .deduplication import MessageDeduplicator
|
|
50
|
+
|
|
51
|
+
logger = structlog.get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Message limit for performance optimization
|
|
55
|
+
# Loading >200 messages can slow down initial rendering
|
|
56
|
+
# Most conversations don't exceed 100 messages
|
|
57
|
+
MAX_HISTORY_MESSAGES = 200
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HistoryLoader:
|
|
61
|
+
"""
|
|
62
|
+
Handles loading historical messages from database with Temporal fallback.
|
|
63
|
+
|
|
64
|
+
This class manages the initial message history load when a client connects
|
|
65
|
+
to the streaming execution endpoint. It attempts to load messages from the
|
|
66
|
+
PostgreSQL Session table first (fast, reliable), falling back to Temporal
|
|
67
|
+
workflow state if the database has no messages (e.g., new execution, DB lag).
|
|
68
|
+
|
|
69
|
+
The loader yields messages progressively as an async generator, allowing
|
|
70
|
+
the UI to render messages immediately without waiting for the entire history
|
|
71
|
+
to load. This provides instant feedback to users even for long conversations.
|
|
72
|
+
|
|
73
|
+
Message deduplication is handled via the provided MessageDeduplicator instance,
|
|
74
|
+
ensuring that messages aren't duplicated between history and live streams.
|
|
75
|
+
|
|
76
|
+
Example usage:
|
|
77
|
+
deduplicator = MessageDeduplicator()
|
|
78
|
+
loader = HistoryLoader(
|
|
79
|
+
execution_id="exec-123",
|
|
80
|
+
organization_id="org-456",
|
|
81
|
+
db_session=db,
|
|
82
|
+
temporal_client=temporal_client,
|
|
83
|
+
deduplicator=deduplicator,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async for message in loader.stream():
|
|
87
|
+
# Send message to client
|
|
88
|
+
yield format_sse_message(message)
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
execution_id: str,
|
|
94
|
+
organization_id: str,
|
|
95
|
+
db_session: SQLAlchemySession,
|
|
96
|
+
temporal_client: Any, # temporalio.client.Client
|
|
97
|
+
deduplicator: MessageDeduplicator,
|
|
98
|
+
workflow_id: Optional[str] = None,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Initialize the history loader.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
execution_id: Execution ID to load history for
|
|
105
|
+
organization_id: Organization ID for security filtering
|
|
106
|
+
db_session: SQLAlchemy database session
|
|
107
|
+
temporal_client: Temporal client for workflow queries
|
|
108
|
+
deduplicator: Message deduplicator instance
|
|
109
|
+
workflow_id: Workflow ID for Temporal queries (defaults to agent-execution-{execution_id})
|
|
110
|
+
"""
|
|
111
|
+
self.execution_id = execution_id
|
|
112
|
+
self.organization_id = organization_id
|
|
113
|
+
self.db_session = db_session
|
|
114
|
+
self.temporal_client = temporal_client
|
|
115
|
+
self.deduplicator = deduplicator
|
|
116
|
+
self.workflow_id = workflow_id or f"agent-execution-{execution_id}"
|
|
117
|
+
|
|
118
|
+
# Statistics for monitoring
|
|
119
|
+
self._stats = {
|
|
120
|
+
"db_messages_loaded": 0,
|
|
121
|
+
"temporal_messages_loaded": 0,
|
|
122
|
+
"messages_sent": 0,
|
|
123
|
+
"messages_skipped_empty": 0,
|
|
124
|
+
"messages_deduplicated": 0,
|
|
125
|
+
"db_load_duration_ms": 0,
|
|
126
|
+
"temporal_load_duration_ms": 0,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async def stream(self) -> AsyncGenerator[Dict[str, Any], None]:
|
|
130
|
+
"""
|
|
131
|
+
Stream historical messages progressively.
|
|
132
|
+
|
|
133
|
+
This method loads messages from the database first, falling back to
|
|
134
|
+
Temporal workflow state if no messages are found. Messages are yielded
|
|
135
|
+
one at a time for non-blocking progressive rendering.
|
|
136
|
+
|
|
137
|
+
The method performs the following steps:
|
|
138
|
+
1. Load messages from database (PostgreSQL Session table)
|
|
139
|
+
2. If no messages found, try Temporal workflow fallback
|
|
140
|
+
3. Sort messages chronologically
|
|
141
|
+
4. Limit to last 200 messages if needed
|
|
142
|
+
5. Yield messages one at a time, checking deduplication
|
|
143
|
+
|
|
144
|
+
Yields:
|
|
145
|
+
Message dictionaries with keys:
|
|
146
|
+
- message_id: Unique message identifier
|
|
147
|
+
- role: Message role (user, assistant, system, tool)
|
|
148
|
+
- content: Message content
|
|
149
|
+
- timestamp: ISO format timestamp
|
|
150
|
+
- tool_name, tool_input, tool_output: Tool data (if applicable)
|
|
151
|
+
- workflow_name, workflow_steps, etc.: Workflow data (if applicable)
|
|
152
|
+
- user_id, user_name, user_email, user_avatar: User attribution (if applicable)
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
Exception: If both database and Temporal loading fail (logged but not raised)
|
|
156
|
+
"""
|
|
157
|
+
import time
|
|
158
|
+
|
|
159
|
+
# Step 1: Try loading from database
|
|
160
|
+
t0 = time.time()
|
|
161
|
+
messages = await self._load_from_database()
|
|
162
|
+
self._stats["db_load_duration_ms"] = int((time.time() - t0) * 1000)
|
|
163
|
+
self._stats["db_messages_loaded"] = len(messages)
|
|
164
|
+
|
|
165
|
+
# Step 2: Fallback to Temporal if no messages in database
|
|
166
|
+
if not messages:
|
|
167
|
+
logger.info(
|
|
168
|
+
"no_database_history_attempting_temporal_fallback",
|
|
169
|
+
execution_id=self.execution_id,
|
|
170
|
+
)
|
|
171
|
+
t0 = time.time()
|
|
172
|
+
messages = await self._load_from_temporal()
|
|
173
|
+
self._stats["temporal_load_duration_ms"] = int((time.time() - t0) * 1000)
|
|
174
|
+
self._stats["temporal_messages_loaded"] = len(messages)
|
|
175
|
+
|
|
176
|
+
if not messages:
|
|
177
|
+
logger.info(
|
|
178
|
+
"no_history_messages_found",
|
|
179
|
+
execution_id=self.execution_id,
|
|
180
|
+
stats=self._stats,
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Step 3: Sort messages chronologically
|
|
185
|
+
# CRITICAL: Messages must be in exact order for proper conversation flow
|
|
186
|
+
messages.sort(key=lambda m: self._parse_timestamp(m.get("timestamp", "")))
|
|
187
|
+
|
|
188
|
+
logger.info(
|
|
189
|
+
"history_messages_loaded_and_sorted",
|
|
190
|
+
execution_id=self.execution_id,
|
|
191
|
+
message_count=len(messages),
|
|
192
|
+
first_timestamp=messages[0].get("timestamp") if messages else None,
|
|
193
|
+
last_timestamp=messages[-1].get("timestamp") if messages else None,
|
|
194
|
+
stats=self._stats,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Step 4: Limit to last N messages for performance
|
|
198
|
+
if len(messages) > MAX_HISTORY_MESSAGES:
|
|
199
|
+
original_count = len(messages)
|
|
200
|
+
messages = messages[-MAX_HISTORY_MESSAGES:]
|
|
201
|
+
logger.info(
|
|
202
|
+
"history_messages_limited_for_performance",
|
|
203
|
+
execution_id=self.execution_id,
|
|
204
|
+
original_count=original_count,
|
|
205
|
+
limited_count=len(messages),
|
|
206
|
+
limit=MAX_HISTORY_MESSAGES,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Step 5: Stream messages one at a time
|
|
210
|
+
for msg in messages:
|
|
211
|
+
# Skip messages with empty content UNLESS they have tool/workflow data
|
|
212
|
+
has_content = msg.get("content") and msg.get("content").strip()
|
|
213
|
+
has_tool_data = bool(
|
|
214
|
+
msg.get("tool_name")
|
|
215
|
+
or msg.get("tool_input")
|
|
216
|
+
or msg.get("tool_output")
|
|
217
|
+
or msg.get("tool_error")
|
|
218
|
+
)
|
|
219
|
+
has_workflow_data = bool(
|
|
220
|
+
msg.get("workflow_name")
|
|
221
|
+
or msg.get("workflow_steps")
|
|
222
|
+
or msg.get("workflow_status")
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if not has_content and not has_tool_data and not has_workflow_data:
|
|
226
|
+
self._stats["messages_skipped_empty"] += 1
|
|
227
|
+
logger.debug(
|
|
228
|
+
"skipping_empty_message",
|
|
229
|
+
execution_id=self.execution_id,
|
|
230
|
+
message_id=msg.get("message_id"),
|
|
231
|
+
role=msg.get("role"),
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Check deduplication
|
|
236
|
+
if self.deduplicator.is_sent(msg):
|
|
237
|
+
self._stats["messages_deduplicated"] += 1
|
|
238
|
+
logger.debug(
|
|
239
|
+
"skipping_duplicate_message",
|
|
240
|
+
execution_id=self.execution_id,
|
|
241
|
+
message_id=msg.get("message_id"),
|
|
242
|
+
role=msg.get("role"),
|
|
243
|
+
)
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Mark as sent for deduplication
|
|
247
|
+
self.deduplicator.mark_sent(msg)
|
|
248
|
+
self._stats["messages_sent"] += 1
|
|
249
|
+
|
|
250
|
+
# Yield the message
|
|
251
|
+
yield msg
|
|
252
|
+
|
|
253
|
+
logger.info(
|
|
254
|
+
"history_streaming_complete",
|
|
255
|
+
execution_id=self.execution_id,
|
|
256
|
+
stats=self._stats,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
async def _load_from_database(self) -> List[Dict[str, Any]]:
|
|
260
|
+
"""
|
|
261
|
+
Load messages from PostgreSQL Session table.
|
|
262
|
+
|
|
263
|
+
This is the primary source for message history. The Session table stores
|
|
264
|
+
messages as JSONB array, which is fast to query and doesn't require joins.
|
|
265
|
+
|
|
266
|
+
The method queries the Session table by execution_id and organization_id,
|
|
267
|
+
extracts the messages array, and converts to standard message dictionaries.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of message dictionaries, or empty list if no session found
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
# Query session record by execution_id only
|
|
274
|
+
# NOTE: We don't filter by organization_id here because:
|
|
275
|
+
# 1. execution_id is globally unique (UUID)
|
|
276
|
+
# 2. Authorization is already enforced at the WebSocket/API level
|
|
277
|
+
# 3. Worker may persist with different org_id format than UI queries with
|
|
278
|
+
# (e.g., 'kubiya-ai' vs 'org_lAowz6o1YKbB4YUt')
|
|
279
|
+
session_record = (
|
|
280
|
+
self.db_session.query(SessionModel)
|
|
281
|
+
.filter(
|
|
282
|
+
SessionModel.execution_id == self.execution_id,
|
|
283
|
+
)
|
|
284
|
+
.first()
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if not session_record:
|
|
288
|
+
logger.warning(
|
|
289
|
+
"no_session_record_found_in_database",
|
|
290
|
+
execution_id=self.execution_id,
|
|
291
|
+
queried_org_id=self.organization_id,
|
|
292
|
+
)
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
# Extract messages from JSONB array
|
|
296
|
+
messages_data = session_record.messages or []
|
|
297
|
+
|
|
298
|
+
if not messages_data:
|
|
299
|
+
logger.warning(
|
|
300
|
+
"session_record_found_but_no_messages",
|
|
301
|
+
execution_id=self.execution_id,
|
|
302
|
+
session_id=session_record.session_id,
|
|
303
|
+
created_at=str(session_record.created_at),
|
|
304
|
+
)
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
logger.info(
|
|
308
|
+
"loaded_messages_from_database",
|
|
309
|
+
execution_id=self.execution_id,
|
|
310
|
+
message_count=len(messages_data),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Convert to standard message dictionaries
|
|
314
|
+
# Messages are already dicts in JSONB, so just ensure they have required fields
|
|
315
|
+
messages = []
|
|
316
|
+
for msg_data in messages_data:
|
|
317
|
+
# Ensure message has required fields
|
|
318
|
+
message = {
|
|
319
|
+
"message_id": msg_data.get("message_id"),
|
|
320
|
+
"role": msg_data.get("role"),
|
|
321
|
+
"content": msg_data.get("content"),
|
|
322
|
+
"timestamp": msg_data.get("timestamp"),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Add optional fields if present
|
|
326
|
+
optional_fields = [
|
|
327
|
+
"tool_name",
|
|
328
|
+
"tool_input",
|
|
329
|
+
"tool_output",
|
|
330
|
+
"tool_error",
|
|
331
|
+
"workflow_name",
|
|
332
|
+
"workflow_status",
|
|
333
|
+
"workflow_steps",
|
|
334
|
+
"workflow_runner",
|
|
335
|
+
"workflow_type",
|
|
336
|
+
"workflow_duration",
|
|
337
|
+
"workflow_error",
|
|
338
|
+
"user_id",
|
|
339
|
+
"user_name",
|
|
340
|
+
"user_email",
|
|
341
|
+
"user_avatar",
|
|
342
|
+
"metadata",
|
|
343
|
+
]
|
|
344
|
+
for field in optional_fields:
|
|
345
|
+
if field in msg_data and msg_data[field] is not None:
|
|
346
|
+
message[field] = msg_data[field]
|
|
347
|
+
|
|
348
|
+
messages.append(message)
|
|
349
|
+
|
|
350
|
+
# Normalize old message ID formats for backward compatibility
|
|
351
|
+
self._normalize_message_ids(messages)
|
|
352
|
+
|
|
353
|
+
return messages
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.error(
|
|
357
|
+
"database_load_failed",
|
|
358
|
+
execution_id=self.execution_id,
|
|
359
|
+
error=str(e),
|
|
360
|
+
error_type=type(e).__name__,
|
|
361
|
+
)
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
async def _load_from_temporal(self) -> List[Dict[str, Any]]:
|
|
365
|
+
"""
|
|
366
|
+
Fallback: Load messages from Temporal workflow state.
|
|
367
|
+
|
|
368
|
+
This method is used when the database has no messages, which can happen
|
|
369
|
+
for new executions or if there's DB replication lag. It queries the
|
|
370
|
+
Temporal workflow state to get the current message history.
|
|
371
|
+
|
|
372
|
+
The method has a 3-second timeout to prevent blocking the stream if the
|
|
373
|
+
Temporal worker is down or slow. If the timeout is exceeded, an empty
|
|
374
|
+
list is returned and the stream continues without history.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List of message dictionaries, or empty list if query fails/times out
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
# Get workflow handle
|
|
381
|
+
workflow_handle = self.temporal_client.get_workflow_handle(
|
|
382
|
+
self.workflow_id
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Query workflow state with 3-second timeout
|
|
386
|
+
# This prevents 29-second hangs when worker is down
|
|
387
|
+
try:
|
|
388
|
+
state = await asyncio.wait_for(
|
|
389
|
+
workflow_handle.query(AgentExecutionWorkflow.get_state),
|
|
390
|
+
timeout=3.0,
|
|
391
|
+
)
|
|
392
|
+
except asyncio.TimeoutError:
|
|
393
|
+
logger.warning(
|
|
394
|
+
"temporal_fallback_timeout",
|
|
395
|
+
execution_id=self.execution_id,
|
|
396
|
+
workflow_id=self.workflow_id,
|
|
397
|
+
timeout_seconds=3.0,
|
|
398
|
+
)
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
if not state or not state.messages or len(state.messages) == 0:
|
|
402
|
+
logger.info(
|
|
403
|
+
"temporal_fallback_no_messages",
|
|
404
|
+
execution_id=self.execution_id,
|
|
405
|
+
workflow_id=self.workflow_id,
|
|
406
|
+
)
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
logger.info(
|
|
410
|
+
"loaded_messages_from_temporal",
|
|
411
|
+
execution_id=self.execution_id,
|
|
412
|
+
workflow_id=self.workflow_id,
|
|
413
|
+
message_count=len(state.messages),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Convert ChatMessage objects to dictionaries
|
|
417
|
+
messages = []
|
|
418
|
+
for i, msg in enumerate(state.messages):
|
|
419
|
+
# Generate message_id if missing
|
|
420
|
+
message_id = getattr(msg, "message_id", None)
|
|
421
|
+
if not message_id:
|
|
422
|
+
# Use index-based ID for temporal messages
|
|
423
|
+
message_id = f"{self.execution_id}_{msg.role}_{i}"
|
|
424
|
+
|
|
425
|
+
message = {
|
|
426
|
+
"message_id": message_id,
|
|
427
|
+
"role": msg.role,
|
|
428
|
+
"content": msg.content,
|
|
429
|
+
"timestamp": msg.timestamp,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# Add optional fields if present
|
|
433
|
+
if msg.tool_name:
|
|
434
|
+
message["tool_name"] = msg.tool_name
|
|
435
|
+
if hasattr(msg, "tool_input") and msg.tool_input:
|
|
436
|
+
message["tool_input"] = msg.tool_input
|
|
437
|
+
if hasattr(msg, "tool_output") and msg.tool_output:
|
|
438
|
+
message["tool_output"] = msg.tool_output
|
|
439
|
+
|
|
440
|
+
# Add user attribution if present
|
|
441
|
+
if hasattr(msg, "user_id") and msg.user_id:
|
|
442
|
+
message["user_id"] = msg.user_id
|
|
443
|
+
if hasattr(msg, "user_name") and msg.user_name:
|
|
444
|
+
message["user_name"] = msg.user_name
|
|
445
|
+
if hasattr(msg, "user_email") and msg.user_email:
|
|
446
|
+
message["user_email"] = msg.user_email
|
|
447
|
+
if hasattr(msg, "user_avatar") and msg.user_avatar:
|
|
448
|
+
message["user_avatar"] = msg.user_avatar
|
|
449
|
+
|
|
450
|
+
messages.append(message)
|
|
451
|
+
|
|
452
|
+
# Normalize message IDs
|
|
453
|
+
self._normalize_message_ids(messages)
|
|
454
|
+
|
|
455
|
+
return messages
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
logger.error(
|
|
459
|
+
"temporal_fallback_failed",
|
|
460
|
+
execution_id=self.execution_id,
|
|
461
|
+
workflow_id=self.workflow_id,
|
|
462
|
+
error=str(e),
|
|
463
|
+
error_type=type(e).__name__,
|
|
464
|
+
)
|
|
465
|
+
return []
|
|
466
|
+
|
|
467
|
+
def _normalize_message_ids(self, messages: List[Dict[str, Any]]) -> None:
|
|
468
|
+
"""
|
|
469
|
+
Normalize old message ID formats for backward compatibility.
|
|
470
|
+
|
|
471
|
+
This method handles legacy message ID formats to ensure consistent IDs
|
|
472
|
+
across database reloads. Old formats used timestamps or simple indices,
|
|
473
|
+
which change on each load. New format uses turn-based numbering which
|
|
474
|
+
is stable.
|
|
475
|
+
|
|
476
|
+
Message ID formats:
|
|
477
|
+
- New (turn-based): {execution_id}_{role}_{turn_number}
|
|
478
|
+
Example: "exec123_assistant_5"
|
|
479
|
+
- Old (timestamp-based): {execution_id}_{role}_{timestamp_micros}
|
|
480
|
+
Example: "exec123_assistant_1234567890123456"
|
|
481
|
+
- Old (index-based): {execution_id}_{role}_{idx}
|
|
482
|
+
Example: "exec123_assistant_42" (ambiguous with turn-based)
|
|
483
|
+
|
|
484
|
+
Detection heuristic:
|
|
485
|
+
- If last part is < 10000, assume turn-based (new format) - keep as-is
|
|
486
|
+
- If last part is >= 10000, assume timestamp-based (old format) - use content hash
|
|
487
|
+
- If can't parse, use content hash
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
messages: List of message dictionaries to normalize in-place
|
|
491
|
+
"""
|
|
492
|
+
normalized_count = 0
|
|
493
|
+
|
|
494
|
+
for msg in messages:
|
|
495
|
+
message_id = msg.get("message_id")
|
|
496
|
+
if not message_id:
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
parts = message_id.split("_")
|
|
500
|
+
|
|
501
|
+
# Check if format is: {execution_id}_{role}_{number}
|
|
502
|
+
if len(parts) >= 3 and parts[-2] in ["user", "assistant", "system"]:
|
|
503
|
+
try:
|
|
504
|
+
last_part = int(parts[-1])
|
|
505
|
+
|
|
506
|
+
# Turn numbers are small (1-100), timestamps are huge (1e15)
|
|
507
|
+
if last_part < 10000:
|
|
508
|
+
# New format (turn-based) - keep as-is
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
# Old format (timestamp-based) - normalize to content hash
|
|
512
|
+
normalized_count += 1
|
|
513
|
+
|
|
514
|
+
except (ValueError, IndexError):
|
|
515
|
+
# Can't parse as number - might be hash or other format
|
|
516
|
+
normalized_count += 1
|
|
517
|
+
|
|
518
|
+
# Generate stable ID based on content hash
|
|
519
|
+
content = msg.get("content", "") or ""
|
|
520
|
+
role = msg.get("role", "unknown")
|
|
521
|
+
execution_id = parts[0] if parts else self.execution_id
|
|
522
|
+
|
|
523
|
+
content_hash = hashlib.md5(content.encode()).hexdigest()[:8]
|
|
524
|
+
new_id = f"{execution_id}_{role}_{content_hash}"
|
|
525
|
+
|
|
526
|
+
old_id = message_id
|
|
527
|
+
msg["message_id"] = new_id
|
|
528
|
+
|
|
529
|
+
logger.debug(
|
|
530
|
+
"normalized_message_id",
|
|
531
|
+
execution_id=self.execution_id,
|
|
532
|
+
old_id=old_id,
|
|
533
|
+
new_id=new_id,
|
|
534
|
+
role=role,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
if normalized_count > 0:
|
|
538
|
+
logger.info(
|
|
539
|
+
"normalized_message_ids_for_backward_compatibility",
|
|
540
|
+
execution_id=self.execution_id,
|
|
541
|
+
normalized_count=normalized_count,
|
|
542
|
+
total_messages=len(messages),
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def _parse_timestamp(self, timestamp_str: str) -> datetime:
|
|
546
|
+
"""
|
|
547
|
+
Parse ISO format timestamp string.
|
|
548
|
+
|
|
549
|
+
Handles both with and without 'Z' suffix. Returns datetime.min for
|
|
550
|
+
invalid/missing timestamps to ensure they sort first.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
timestamp_str: ISO format timestamp (e.g., "2024-01-15T10:30:00Z")
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
datetime object, or datetime.min if parsing fails
|
|
557
|
+
"""
|
|
558
|
+
if not timestamp_str:
|
|
559
|
+
return datetime.min
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
# Handle 'Z' suffix for UTC timestamps
|
|
563
|
+
normalized = timestamp_str.replace("Z", "+00:00")
|
|
564
|
+
return datetime.fromisoformat(normalized)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
logger.warning(
|
|
567
|
+
"failed_to_parse_timestamp",
|
|
568
|
+
execution_id=self.execution_id,
|
|
569
|
+
timestamp=timestamp_str,
|
|
570
|
+
error=str(e),
|
|
571
|
+
)
|
|
572
|
+
return datetime.min
|
|
573
|
+
|
|
574
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
575
|
+
"""
|
|
576
|
+
Get history loading statistics.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
Dictionary with statistics:
|
|
580
|
+
- db_messages_loaded: Messages loaded from database
|
|
581
|
+
- temporal_messages_loaded: Messages loaded from Temporal
|
|
582
|
+
- messages_sent: Messages yielded to stream
|
|
583
|
+
- messages_skipped_empty: Messages skipped due to empty content
|
|
584
|
+
- messages_deduplicated: Messages skipped due to deduplication
|
|
585
|
+
- db_load_duration_ms: Database query duration
|
|
586
|
+
- temporal_load_duration_ms: Temporal query duration
|
|
587
|
+
"""
|
|
588
|
+
return self._stats.copy()
|