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,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime-agnostic analytics extraction.
|
|
3
|
+
|
|
4
|
+
This module extracts analytics data from RuntimeExecutionResult objects,
|
|
5
|
+
working with any runtime (Agno, Claude Code, LiteLLM, etc.) that follows
|
|
6
|
+
the standard runtime contract.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
import structlog
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from control_plane_api.worker.runtimes.base import RuntimeExecutionResult
|
|
15
|
+
from control_plane_api.worker.services.analytics_service import (
|
|
16
|
+
TurnMetrics,
|
|
17
|
+
ToolCallMetrics,
|
|
18
|
+
AnalyticsService,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = structlog.get_logger()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RuntimeAnalyticsExtractor:
|
|
25
|
+
"""
|
|
26
|
+
Extracts analytics data from RuntimeExecutionResult.
|
|
27
|
+
|
|
28
|
+
This works with any runtime that populates the standard fields:
|
|
29
|
+
- usage: Token usage metrics
|
|
30
|
+
- model: Model identifier
|
|
31
|
+
- tool_execution_messages: Tool call tracking
|
|
32
|
+
- metadata: Runtime-specific data
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def extract_turn_metrics(
|
|
37
|
+
result: RuntimeExecutionResult,
|
|
38
|
+
execution_id: str,
|
|
39
|
+
turn_number: int,
|
|
40
|
+
turn_start_time: float,
|
|
41
|
+
turn_end_time: Optional[float] = None,
|
|
42
|
+
) -> TurnMetrics:
|
|
43
|
+
"""
|
|
44
|
+
Extract turn metrics from RuntimeExecutionResult.
|
|
45
|
+
|
|
46
|
+
Works with any runtime that populates the usage field.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
result: Runtime execution result
|
|
50
|
+
execution_id: Execution ID
|
|
51
|
+
turn_number: Turn sequence number
|
|
52
|
+
turn_start_time: When turn started (timestamp)
|
|
53
|
+
turn_end_time: When turn ended (timestamp, defaults to now)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
TurnMetrics ready for submission
|
|
57
|
+
"""
|
|
58
|
+
if turn_end_time is None:
|
|
59
|
+
turn_end_time = time.time()
|
|
60
|
+
|
|
61
|
+
duration_ms = int((turn_end_time - turn_start_time) * 1000)
|
|
62
|
+
|
|
63
|
+
# Extract usage - runtimes use different field names
|
|
64
|
+
usage = result.usage or {}
|
|
65
|
+
|
|
66
|
+
# Normalize field names from different providers
|
|
67
|
+
input_tokens = (
|
|
68
|
+
usage.get("input_tokens") or
|
|
69
|
+
usage.get("prompt_tokens") or
|
|
70
|
+
0
|
|
71
|
+
)
|
|
72
|
+
output_tokens = (
|
|
73
|
+
usage.get("output_tokens") or
|
|
74
|
+
usage.get("completion_tokens") or
|
|
75
|
+
0
|
|
76
|
+
)
|
|
77
|
+
total_tokens = (
|
|
78
|
+
usage.get("total_tokens") or
|
|
79
|
+
(input_tokens + output_tokens)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Cache tokens (Anthropic-specific, but other providers may add support)
|
|
83
|
+
cache_read_tokens = usage.get("cache_read_tokens", 0)
|
|
84
|
+
cache_creation_tokens = usage.get("cache_creation_tokens", 0)
|
|
85
|
+
|
|
86
|
+
# Alternative: extract from prompt_tokens_details if present
|
|
87
|
+
if "prompt_tokens_details" in usage:
|
|
88
|
+
details = usage["prompt_tokens_details"]
|
|
89
|
+
if isinstance(details, dict):
|
|
90
|
+
cache_read_tokens = details.get("cached_tokens", cache_read_tokens)
|
|
91
|
+
|
|
92
|
+
# Extract tool names from tool_execution_messages (needed for AEM calculation)
|
|
93
|
+
tool_names = []
|
|
94
|
+
tools_count = 0
|
|
95
|
+
if result.tool_execution_messages:
|
|
96
|
+
tool_names = [msg.get("tool") for msg in result.tool_execution_messages if msg.get("tool")]
|
|
97
|
+
tools_count = len(tool_names)
|
|
98
|
+
|
|
99
|
+
# Calculate costs
|
|
100
|
+
model = result.model or "unknown"
|
|
101
|
+
costs = AnalyticsService.calculate_token_cost(
|
|
102
|
+
input_tokens=input_tokens,
|
|
103
|
+
output_tokens=output_tokens,
|
|
104
|
+
cache_read_tokens=cache_read_tokens,
|
|
105
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
106
|
+
model=model,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Calculate Agentic Engineering Minutes (AEM)
|
|
110
|
+
aem_metrics = AnalyticsService.calculate_aem(
|
|
111
|
+
duration_ms=duration_ms,
|
|
112
|
+
model=model,
|
|
113
|
+
tool_calls_count=tools_count,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Extract model provider from metadata or infer from model name
|
|
117
|
+
metadata = result.metadata or {}
|
|
118
|
+
model_provider = metadata.get("model_provider") or RuntimeAnalyticsExtractor._infer_provider(model)
|
|
119
|
+
|
|
120
|
+
# Response preview (first 500 chars)
|
|
121
|
+
response_preview = result.response[:500] if result.response else None
|
|
122
|
+
|
|
123
|
+
return TurnMetrics(
|
|
124
|
+
execution_id=execution_id,
|
|
125
|
+
turn_number=turn_number,
|
|
126
|
+
model=model,
|
|
127
|
+
model_provider=model_provider,
|
|
128
|
+
started_at=datetime.fromtimestamp(turn_start_time, timezone.utc).isoformat(),
|
|
129
|
+
completed_at=datetime.fromtimestamp(turn_end_time, timezone.utc).isoformat(),
|
|
130
|
+
duration_ms=duration_ms,
|
|
131
|
+
input_tokens=input_tokens,
|
|
132
|
+
output_tokens=output_tokens,
|
|
133
|
+
cache_read_tokens=cache_read_tokens,
|
|
134
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
135
|
+
total_tokens=total_tokens,
|
|
136
|
+
input_cost=costs["input_cost"],
|
|
137
|
+
output_cost=costs["output_cost"],
|
|
138
|
+
cache_read_cost=costs["cache_read_cost"],
|
|
139
|
+
cache_creation_cost=costs["cache_creation_cost"],
|
|
140
|
+
total_cost=costs["total_cost"],
|
|
141
|
+
finish_reason=result.finish_reason or "stop",
|
|
142
|
+
response_preview=response_preview,
|
|
143
|
+
tools_called_count=tools_count,
|
|
144
|
+
tools_called_names=list(set(tool_names)), # Unique tool names
|
|
145
|
+
error_message=result.error,
|
|
146
|
+
metrics=metadata, # Include runtime-specific metrics
|
|
147
|
+
# AEM metrics
|
|
148
|
+
runtime_minutes=aem_metrics["runtime_minutes"],
|
|
149
|
+
model_weight=aem_metrics["model_weight"],
|
|
150
|
+
tool_calls_weight=aem_metrics["tool_calls_weight"],
|
|
151
|
+
aem_value=aem_metrics["aem_value"],
|
|
152
|
+
aem_cost=aem_metrics["aem_cost"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def extract_tool_call_metrics(
|
|
157
|
+
result: RuntimeExecutionResult,
|
|
158
|
+
execution_id: str,
|
|
159
|
+
turn_id: Optional[str] = None,
|
|
160
|
+
) -> List[ToolCallMetrics]:
|
|
161
|
+
"""
|
|
162
|
+
Extract tool call metrics from RuntimeExecutionResult.
|
|
163
|
+
|
|
164
|
+
Works with any runtime that populates tool_execution_messages.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
result: Runtime execution result
|
|
168
|
+
execution_id: Execution ID
|
|
169
|
+
turn_id: Turn ID to link tool calls to
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of ToolCallMetrics ready for submission
|
|
173
|
+
"""
|
|
174
|
+
if not result.tool_execution_messages:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
tool_calls = []
|
|
178
|
+
|
|
179
|
+
for tool_msg in result.tool_execution_messages:
|
|
180
|
+
# Extract timing information
|
|
181
|
+
# Runtimes should provide start_time/end_time or duration_ms
|
|
182
|
+
duration_ms = tool_msg.get("duration_ms")
|
|
183
|
+
start_time = tool_msg.get("start_time")
|
|
184
|
+
end_time = tool_msg.get("end_time")
|
|
185
|
+
|
|
186
|
+
# Calculate timestamps
|
|
187
|
+
if start_time and end_time:
|
|
188
|
+
started_at = datetime.fromtimestamp(start_time, timezone.utc).isoformat()
|
|
189
|
+
completed_at = datetime.fromtimestamp(end_time, timezone.utc).isoformat()
|
|
190
|
+
if duration_ms is None:
|
|
191
|
+
duration_ms = int((end_time - start_time) * 1000)
|
|
192
|
+
else:
|
|
193
|
+
# Fallback to current time if not provided
|
|
194
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
195
|
+
started_at = now
|
|
196
|
+
completed_at = now
|
|
197
|
+
|
|
198
|
+
# Extract tool output
|
|
199
|
+
tool_output = tool_msg.get("output") or tool_msg.get("result")
|
|
200
|
+
if tool_output and not isinstance(tool_output, str):
|
|
201
|
+
tool_output = str(tool_output)
|
|
202
|
+
|
|
203
|
+
# Success status
|
|
204
|
+
success = tool_msg.get("success", True)
|
|
205
|
+
|
|
206
|
+
tool_call = ToolCallMetrics(
|
|
207
|
+
execution_id=execution_id,
|
|
208
|
+
turn_id=turn_id,
|
|
209
|
+
tool_name=tool_msg.get("tool", "unknown"),
|
|
210
|
+
tool_use_id=tool_msg.get("tool_use_id"),
|
|
211
|
+
started_at=started_at,
|
|
212
|
+
completed_at=completed_at,
|
|
213
|
+
duration_ms=duration_ms,
|
|
214
|
+
tool_input=tool_msg.get("input"),
|
|
215
|
+
tool_output=tool_output,
|
|
216
|
+
tool_output_size=len(tool_output) if tool_output else 0,
|
|
217
|
+
success=success,
|
|
218
|
+
error_message=tool_msg.get("error"),
|
|
219
|
+
error_type=tool_msg.get("error_type"),
|
|
220
|
+
metadata=tool_msg.get("metadata", {}),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
tool_calls.append(tool_call)
|
|
224
|
+
|
|
225
|
+
return tool_calls
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _infer_provider(model: str) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Infer provider from model identifier.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
model: Model string
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Provider name
|
|
237
|
+
"""
|
|
238
|
+
model_lower = model.lower()
|
|
239
|
+
|
|
240
|
+
if "claude" in model_lower or "anthropic" in model_lower:
|
|
241
|
+
return "anthropic"
|
|
242
|
+
elif "gpt" in model_lower or "openai" in model_lower:
|
|
243
|
+
return "openai"
|
|
244
|
+
elif "gemini" in model_lower or "google" in model_lower:
|
|
245
|
+
return "google"
|
|
246
|
+
elif "llama" in model_lower or "meta" in model_lower:
|
|
247
|
+
return "meta"
|
|
248
|
+
elif "mistral" in model_lower:
|
|
249
|
+
return "mistral"
|
|
250
|
+
elif "command" in model_lower or "cohere" in model_lower:
|
|
251
|
+
return "cohere"
|
|
252
|
+
else:
|
|
253
|
+
return "unknown"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def submit_runtime_analytics(
|
|
257
|
+
result: RuntimeExecutionResult,
|
|
258
|
+
execution_id: str,
|
|
259
|
+
turn_number: int,
|
|
260
|
+
turn_start_time: float,
|
|
261
|
+
analytics_service: AnalyticsService,
|
|
262
|
+
turn_end_time: Optional[float] = None,
|
|
263
|
+
) -> Dict[str, Any]:
|
|
264
|
+
"""
|
|
265
|
+
Extract and submit all analytics from a RuntimeExecutionResult.
|
|
266
|
+
|
|
267
|
+
This is the main entry point for submitting analytics after a runtime execution.
|
|
268
|
+
It extracts turn metrics and tool call metrics and submits them asynchronously.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
result: Runtime execution result
|
|
272
|
+
execution_id: Execution ID
|
|
273
|
+
turn_number: Turn sequence number
|
|
274
|
+
turn_start_time: When turn started
|
|
275
|
+
analytics_service: Analytics service instance
|
|
276
|
+
turn_end_time: When turn ended (defaults to now)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dict with submission status
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
# Extract turn metrics
|
|
283
|
+
turn_metrics = RuntimeAnalyticsExtractor.extract_turn_metrics(
|
|
284
|
+
result=result,
|
|
285
|
+
execution_id=execution_id,
|
|
286
|
+
turn_number=turn_number,
|
|
287
|
+
turn_start_time=turn_start_time,
|
|
288
|
+
turn_end_time=turn_end_time,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Submit turn metrics
|
|
292
|
+
await analytics_service.record_turn(turn_metrics)
|
|
293
|
+
|
|
294
|
+
# Extract and submit tool call metrics
|
|
295
|
+
tool_call_metrics = RuntimeAnalyticsExtractor.extract_tool_call_metrics(
|
|
296
|
+
result=result,
|
|
297
|
+
execution_id=execution_id,
|
|
298
|
+
turn_id=None, # Could link to turn ID if available
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for tool_call in tool_call_metrics:
|
|
302
|
+
await analytics_service.record_tool_call(tool_call)
|
|
303
|
+
|
|
304
|
+
logger.info(
|
|
305
|
+
"runtime_analytics_submitted",
|
|
306
|
+
execution_id=execution_id[:8],
|
|
307
|
+
turn_number=turn_number,
|
|
308
|
+
tokens=turn_metrics.total_tokens,
|
|
309
|
+
cost=turn_metrics.total_cost,
|
|
310
|
+
tool_calls=len(tool_call_metrics),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
"success": True,
|
|
315
|
+
"turn_submitted": True,
|
|
316
|
+
"tool_calls_submitted": len(tool_call_metrics),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.error(
|
|
321
|
+
"runtime_analytics_submission_failed",
|
|
322
|
+
error=str(e),
|
|
323
|
+
execution_id=execution_id[:8],
|
|
324
|
+
)
|
|
325
|
+
return {
|
|
326
|
+
"success": False,
|
|
327
|
+
"error": str(e),
|
|
328
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Session management service - handles loading and persisting conversation history"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import structlog
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from control_plane_api.worker.control_plane_client import ControlPlaneClient
|
|
9
|
+
from control_plane_api.worker.utils.retry_utils import retry_with_backoff
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _safe_timestamp_to_iso(timestamp: Any) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Safely convert a timestamp (int, float, datetime, or str) to ISO format string.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
timestamp: Can be Unix timestamp (int/float), datetime object, or ISO string
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
ISO format timestamp string
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(timestamp, str):
|
|
25
|
+
# Already a string, return as-is
|
|
26
|
+
return timestamp
|
|
27
|
+
elif isinstance(timestamp, datetime):
|
|
28
|
+
# datetime object, call isoformat()
|
|
29
|
+
return timestamp.isoformat()
|
|
30
|
+
elif isinstance(timestamp, (int, float)):
|
|
31
|
+
# Unix timestamp, convert to datetime first
|
|
32
|
+
return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat()
|
|
33
|
+
else:
|
|
34
|
+
# Fallback to current time
|
|
35
|
+
return datetime.now(timezone.utc).isoformat()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionService:
|
|
39
|
+
"""
|
|
40
|
+
Manages session history loading and persistence via Control Plane API.
|
|
41
|
+
|
|
42
|
+
Workers don't have database access, so all session operations go through
|
|
43
|
+
the Control Plane which provides Redis caching for hot loads.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, control_plane: ControlPlaneClient):
|
|
47
|
+
self.control_plane = control_plane
|
|
48
|
+
|
|
49
|
+
@retry_with_backoff(max_retries=3, initial_delay=1.0)
|
|
50
|
+
def load_session(
|
|
51
|
+
self,
|
|
52
|
+
execution_id: str,
|
|
53
|
+
session_id: Optional[str] = None
|
|
54
|
+
) -> List[Dict[str, Any]]:
|
|
55
|
+
"""
|
|
56
|
+
Load session history from Control Plane (with retry).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of message dicts with role, content, timestamp, etc.
|
|
60
|
+
Empty list if session not found or on error.
|
|
61
|
+
"""
|
|
62
|
+
if not session_id:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
session_data = self.control_plane.get_session(
|
|
67
|
+
execution_id=execution_id,
|
|
68
|
+
session_id=session_id
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if session_data and session_data.get("messages"):
|
|
72
|
+
messages = session_data["messages"]
|
|
73
|
+
logger.info(
|
|
74
|
+
"session_loaded",
|
|
75
|
+
execution_id=execution_id[:8],
|
|
76
|
+
message_count=len(messages)
|
|
77
|
+
)
|
|
78
|
+
return messages
|
|
79
|
+
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
except httpx.TimeoutException:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"session_load_timeout",
|
|
85
|
+
execution_id=execution_id[:8]
|
|
86
|
+
)
|
|
87
|
+
raise # Let retry decorator handle it
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(
|
|
90
|
+
"session_load_error",
|
|
91
|
+
execution_id=execution_id[:8],
|
|
92
|
+
error=str(e)
|
|
93
|
+
)
|
|
94
|
+
return [] # Don't retry on non-timeout errors
|
|
95
|
+
|
|
96
|
+
@retry_with_backoff(max_retries=3, initial_delay=1.0)
|
|
97
|
+
def persist_session(
|
|
98
|
+
self,
|
|
99
|
+
execution_id: str,
|
|
100
|
+
session_id: str,
|
|
101
|
+
user_id: Optional[str],
|
|
102
|
+
messages: List[Dict[str, Any]],
|
|
103
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
104
|
+
) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Persist session history to Control Plane (with retry).
|
|
107
|
+
|
|
108
|
+
IMPORTANT: Applies defensive deduplication before persisting to prevent
|
|
109
|
+
duplicate messages from reaching the database, even if caller didn't deduplicate.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if successful, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
if not messages:
|
|
115
|
+
logger.info("session_persist_skipped_no_messages", execution_id=execution_id[:8])
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
# DEFENSIVE: Apply deduplication before persisting (defense-in-depth)
|
|
119
|
+
# This ensures duplicates never reach the database, even if callers forget to deduplicate
|
|
120
|
+
original_count = len(messages)
|
|
121
|
+
messages = self.deduplicate_messages(messages)
|
|
122
|
+
|
|
123
|
+
if len(messages) < original_count:
|
|
124
|
+
logger.info(
|
|
125
|
+
"defensive_deduplication_applied",
|
|
126
|
+
execution_id=execution_id[:8],
|
|
127
|
+
original_count=original_count,
|
|
128
|
+
deduplicated_count=len(messages),
|
|
129
|
+
removed=original_count - len(messages)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
success = self.control_plane.persist_session(
|
|
134
|
+
execution_id=execution_id,
|
|
135
|
+
session_id=session_id or execution_id,
|
|
136
|
+
user_id=user_id,
|
|
137
|
+
messages=messages,
|
|
138
|
+
metadata=metadata or {}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if success:
|
|
142
|
+
logger.info(
|
|
143
|
+
"session_persisted",
|
|
144
|
+
execution_id=execution_id[:8],
|
|
145
|
+
message_count=len(messages)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return success
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(
|
|
152
|
+
"session_persist_error",
|
|
153
|
+
execution_id=execution_id[:8],
|
|
154
|
+
error=str(e)
|
|
155
|
+
)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def build_conversation_context(
|
|
159
|
+
self,
|
|
160
|
+
session_messages: List[Dict[str, Any]]
|
|
161
|
+
) -> List[Dict[str, str]]:
|
|
162
|
+
"""
|
|
163
|
+
Convert Control Plane session messages to Agno format.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
session_messages: Messages from Control Plane
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of dicts with 'role' and 'content' for Agno
|
|
170
|
+
"""
|
|
171
|
+
context = []
|
|
172
|
+
for msg in session_messages:
|
|
173
|
+
context.append({
|
|
174
|
+
"role": msg.get("role", "user"),
|
|
175
|
+
"content": msg.get("content", ""),
|
|
176
|
+
})
|
|
177
|
+
return context
|
|
178
|
+
|
|
179
|
+
def extract_messages_from_result(
|
|
180
|
+
self,
|
|
181
|
+
result: Any,
|
|
182
|
+
user_id: Optional[str] = None,
|
|
183
|
+
execution_id: Optional[str] = None,
|
|
184
|
+
message_ids: Optional[Dict[int, str]] = None
|
|
185
|
+
) -> List[Dict[str, Any]]:
|
|
186
|
+
"""
|
|
187
|
+
Extract messages from Agno Agent/Team result.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
result: Agno RunResponse object
|
|
191
|
+
user_id: Optional user ID to attach
|
|
192
|
+
execution_id: Optional execution ID for generating message_ids
|
|
193
|
+
message_ids: Optional dict mapping message index to message_id.
|
|
194
|
+
Format: {0: "exec_123_user_1", 1: "exec_123_assistant_1"}
|
|
195
|
+
When provided, these deterministic IDs are used instead of
|
|
196
|
+
generating new ones, ensuring streaming and persisted messages
|
|
197
|
+
have the SAME message_id (fixes duplicate message issue).
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of message dicts ready for persistence
|
|
201
|
+
"""
|
|
202
|
+
messages = []
|
|
203
|
+
|
|
204
|
+
if hasattr(result, "messages") and result.messages:
|
|
205
|
+
for idx, msg in enumerate(result.messages):
|
|
206
|
+
# IMPORTANT: Skip Agno's internal "tool" role messages
|
|
207
|
+
# These are empty placeholders that Agno uses for tool calls
|
|
208
|
+
# We use StreamingHelper's tool messages instead (role="system" with complete data)
|
|
209
|
+
if msg.role == "tool":
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Use provided message_id if available, otherwise generate
|
|
213
|
+
message_id = None
|
|
214
|
+
if message_ids and idx in message_ids:
|
|
215
|
+
# Use pre-generated deterministic ID (preferred - prevents duplicates)
|
|
216
|
+
message_id = message_ids[idx]
|
|
217
|
+
elif execution_id:
|
|
218
|
+
# Fallback: Generate ID (for backward compatibility)
|
|
219
|
+
message_id = f"{execution_id}_{msg.role}_{idx}"
|
|
220
|
+
|
|
221
|
+
messages.append({
|
|
222
|
+
"role": msg.role,
|
|
223
|
+
"content": msg.content,
|
|
224
|
+
"timestamp": (
|
|
225
|
+
_safe_timestamp_to_iso(msg.created_at)
|
|
226
|
+
if hasattr(msg, "created_at") and msg.created_at is not None
|
|
227
|
+
else datetime.now(timezone.utc).isoformat()
|
|
228
|
+
),
|
|
229
|
+
"message_id": message_id,
|
|
230
|
+
"user_id": getattr(msg, "user_id", user_id),
|
|
231
|
+
"user_name": getattr(msg, "user_name", None),
|
|
232
|
+
"user_email": getattr(msg, "user_email", None),
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return messages
|
|
236
|
+
|
|
237
|
+
def deduplicate_messages(
|
|
238
|
+
self,
|
|
239
|
+
messages: List[Dict[str, Any]]
|
|
240
|
+
) -> List[Dict[str, Any]]:
|
|
241
|
+
"""
|
|
242
|
+
Remove duplicate messages based on message_id AND content.
|
|
243
|
+
Keeps first occurrence of each unique message.
|
|
244
|
+
|
|
245
|
+
Two-level deduplication:
|
|
246
|
+
1. Primary: message_id uniqueness
|
|
247
|
+
2. Fallback: Content signature (role + normalized content + timestamp proximity)
|
|
248
|
+
|
|
249
|
+
This is a defense-in-depth measure to prevent duplicate messages
|
|
250
|
+
from appearing in the UI, even if they slip through earlier checks.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
messages: List of message dicts to deduplicate
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Deduplicated list of messages (preserves order, keeps first occurrence)
|
|
257
|
+
"""
|
|
258
|
+
seen_ids = set()
|
|
259
|
+
seen_content_sigs = {} # Track content signatures for assistant messages
|
|
260
|
+
deduplicated = []
|
|
261
|
+
duplicates_by_id = 0
|
|
262
|
+
duplicates_by_content = 0
|
|
263
|
+
|
|
264
|
+
for msg in messages:
|
|
265
|
+
msg_id = msg.get("message_id")
|
|
266
|
+
if not msg_id:
|
|
267
|
+
# No ID - include it (shouldn't happen in normal flow)
|
|
268
|
+
deduplicated.append(msg)
|
|
269
|
+
logger.warning(
|
|
270
|
+
"message_without_id_in_deduplication",
|
|
271
|
+
role=msg.get("role"),
|
|
272
|
+
content_preview=(msg.get("content", "") or "")[:50]
|
|
273
|
+
)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Level 1: Check message_id (existing logic)
|
|
277
|
+
if msg_id in seen_ids:
|
|
278
|
+
# Log duplicate for monitoring
|
|
279
|
+
logger.debug(
|
|
280
|
+
"duplicate_message_id_filtered",
|
|
281
|
+
message_id=msg_id,
|
|
282
|
+
role=msg.get("role")
|
|
283
|
+
)
|
|
284
|
+
duplicates_by_id += 1
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# Level 2: Check content signature (NEW - for assistant messages only)
|
|
288
|
+
if msg.get("role") == "assistant":
|
|
289
|
+
content = msg.get("content", "")
|
|
290
|
+
timestamp = msg.get("timestamp", "")
|
|
291
|
+
|
|
292
|
+
# Create content signature from first 200 chars of normalized content
|
|
293
|
+
content_normalized = content.strip().lower()[:200]
|
|
294
|
+
|
|
295
|
+
# Check if similar content exists recently
|
|
296
|
+
if content_normalized and content_normalized in seen_content_sigs:
|
|
297
|
+
prev_msg = seen_content_sigs[content_normalized]
|
|
298
|
+
prev_timestamp = prev_msg.get("timestamp", "")
|
|
299
|
+
|
|
300
|
+
# Check timestamp proximity (within 5 seconds = likely duplicate)
|
|
301
|
+
if self._timestamps_close(timestamp, prev_timestamp, threshold_seconds=5):
|
|
302
|
+
logger.info(
|
|
303
|
+
"duplicate_content_filtered",
|
|
304
|
+
message_id=msg_id,
|
|
305
|
+
prev_message_id=prev_msg.get("message_id"),
|
|
306
|
+
content_preview=content[:50],
|
|
307
|
+
timestamp=timestamp,
|
|
308
|
+
prev_timestamp=prev_timestamp
|
|
309
|
+
)
|
|
310
|
+
duplicates_by_content += 1
|
|
311
|
+
continue # Skip duplicate content
|
|
312
|
+
|
|
313
|
+
# Store content signature for future checks
|
|
314
|
+
if content_normalized:
|
|
315
|
+
seen_content_sigs[content_normalized] = msg
|
|
316
|
+
|
|
317
|
+
# Message is unique - add it
|
|
318
|
+
seen_ids.add(msg_id)
|
|
319
|
+
deduplicated.append(msg)
|
|
320
|
+
|
|
321
|
+
if len(deduplicated) < len(messages):
|
|
322
|
+
logger.info(
|
|
323
|
+
"messages_deduplicated",
|
|
324
|
+
original_count=len(messages),
|
|
325
|
+
deduplicated_count=len(deduplicated),
|
|
326
|
+
duplicates_removed=len(messages) - len(deduplicated),
|
|
327
|
+
duplicates_by_id=duplicates_by_id,
|
|
328
|
+
duplicates_by_content=duplicates_by_content
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return deduplicated
|
|
332
|
+
|
|
333
|
+
def _timestamps_close(self, ts1: str, ts2: str, threshold_seconds: int = 5) -> bool:
|
|
334
|
+
"""
|
|
335
|
+
Check if two timestamps are within threshold_seconds of each other.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
ts1: First timestamp (ISO format string)
|
|
339
|
+
ts2: Second timestamp (ISO format string)
|
|
340
|
+
threshold_seconds: Maximum difference in seconds to consider timestamps close
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
True if timestamps are within threshold, False otherwise
|
|
344
|
+
"""
|
|
345
|
+
if not ts1 or not ts2:
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
# Parse timestamps (handle both with and without 'Z' suffix)
|
|
350
|
+
t1 = datetime.fromisoformat(ts1.replace('Z', '+00:00'))
|
|
351
|
+
t2 = datetime.fromisoformat(ts2.replace('Z', '+00:00'))
|
|
352
|
+
|
|
353
|
+
# Calculate absolute difference in seconds
|
|
354
|
+
diff = abs((t1 - t2).total_seconds())
|
|
355
|
+
|
|
356
|
+
return diff <= threshold_seconds
|
|
357
|
+
except Exception as e:
|
|
358
|
+
# If can't parse timestamps, assume they're not close
|
|
359
|
+
logger.debug(
|
|
360
|
+
"timestamp_comparison_failed",
|
|
361
|
+
ts1=ts1,
|
|
362
|
+
ts2=ts2,
|
|
363
|
+
error=str(e)
|
|
364
|
+
)
|
|
365
|
+
return False
|