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,1282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slack Tools - Integration with OAuth support and lazy validation
|
|
3
|
+
|
|
4
|
+
Provides comprehensive Slack API access with configuration-driven capabilities.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional, Dict, Any, List
|
|
10
|
+
from control_plane_api.worker.skills.builtin.schema_fix_mixin import SchemaFixMixin
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from agno.tools import Toolkit
|
|
16
|
+
except ImportError:
|
|
17
|
+
# Fallback for testing
|
|
18
|
+
class Toolkit:
|
|
19
|
+
def __init__(self, name: str):
|
|
20
|
+
self.name = name
|
|
21
|
+
self._tools = []
|
|
22
|
+
|
|
23
|
+
def register(self, func):
|
|
24
|
+
self._tools.append(func)
|
|
25
|
+
|
|
26
|
+
# Fix: Rebuild function schemas with proper parameters
|
|
27
|
+
self._rebuild_function_schemas()
|
|
28
|
+
class SlackTools(SchemaFixMixin, Toolkit):
|
|
29
|
+
"""
|
|
30
|
+
Slack integration toolkit with OAuth support via Integration API.
|
|
31
|
+
|
|
32
|
+
Authentication (priority order):
|
|
33
|
+
1. Integration API (fetch token via /integrations/slack/{id}/token)
|
|
34
|
+
2. Secret from vault (secret_name resolved as environment variable)
|
|
35
|
+
3. SLACK_BOT_TOKEN environment variable (fallback)
|
|
36
|
+
|
|
37
|
+
Lazy validation pattern: Don't fail on __init__, validate on first tool use.
|
|
38
|
+
|
|
39
|
+
Capabilities are configuration-driven with fine-grained permission control.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
integration_id: Optional[str] = None,
|
|
45
|
+
secret_name: Optional[str] = None, # Deprecated, use secrets
|
|
46
|
+
secrets: Optional[List[str]] = None,
|
|
47
|
+
use_env_fallback: bool = True,
|
|
48
|
+
enable_read_messages: bool = True,
|
|
49
|
+
enable_write_messages: bool = False,
|
|
50
|
+
enable_reactions: bool = False,
|
|
51
|
+
enable_channel_management: bool = False,
|
|
52
|
+
# Fine-grained read permissions
|
|
53
|
+
read_channels: bool = True,
|
|
54
|
+
read_messages: bool = True,
|
|
55
|
+
search_messages: bool = True,
|
|
56
|
+
read_threads: bool = True,
|
|
57
|
+
read_users: bool = True,
|
|
58
|
+
# Fine-grained write permissions
|
|
59
|
+
send_messages: bool = True,
|
|
60
|
+
post_messages: bool = True,
|
|
61
|
+
reply_to_threads: bool = True,
|
|
62
|
+
update_messages: bool = True,
|
|
63
|
+
# Fine-grained reaction permissions
|
|
64
|
+
add_reactions: bool = True,
|
|
65
|
+
remove_reactions: bool = True,
|
|
66
|
+
# Fine-grained channel management permissions
|
|
67
|
+
create_channels: bool = False,
|
|
68
|
+
invite_to_channels: bool = False,
|
|
69
|
+
archive_channels: bool = False,
|
|
70
|
+
set_channel_topic: bool = False,
|
|
71
|
+
# General settings
|
|
72
|
+
timeout: int = 30,
|
|
73
|
+
default_channel: Optional[str] = None,
|
|
74
|
+
# Channel scoping
|
|
75
|
+
allowed_channels: Optional[List[str]] = None,
|
|
76
|
+
blocked_channels: Optional[List[str]] = None,
|
|
77
|
+
**kwargs
|
|
78
|
+
):
|
|
79
|
+
super().__init__(name="slack")
|
|
80
|
+
|
|
81
|
+
# Store authentication config
|
|
82
|
+
self.integration_id = integration_id
|
|
83
|
+
# Handle backwards compatibility: secret_name -> secrets list
|
|
84
|
+
if secret_name:
|
|
85
|
+
self.secrets = [secret_name]
|
|
86
|
+
elif secrets:
|
|
87
|
+
self.secrets = secrets if isinstance(secrets, list) else [secrets]
|
|
88
|
+
else:
|
|
89
|
+
self.secrets = []
|
|
90
|
+
self.use_env_fallback = use_env_fallback
|
|
91
|
+
self.timeout = timeout
|
|
92
|
+
self.default_channel = default_channel
|
|
93
|
+
|
|
94
|
+
# Store channel scoping
|
|
95
|
+
self.allowed_channels = allowed_channels or []
|
|
96
|
+
self.blocked_channels = blocked_channels or []
|
|
97
|
+
|
|
98
|
+
# Store capability flags
|
|
99
|
+
self.capabilities = {
|
|
100
|
+
"read_messages": enable_read_messages,
|
|
101
|
+
"write_messages": enable_write_messages,
|
|
102
|
+
"reactions": enable_reactions,
|
|
103
|
+
"channel_management": enable_channel_management,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Store fine-grained permissions
|
|
107
|
+
self.permissions = {
|
|
108
|
+
"read_channels": read_channels,
|
|
109
|
+
"read_messages": read_messages,
|
|
110
|
+
"search_messages": search_messages,
|
|
111
|
+
"read_threads": read_threads,
|
|
112
|
+
"read_users": read_users,
|
|
113
|
+
"send_messages": send_messages,
|
|
114
|
+
"post_messages": post_messages,
|
|
115
|
+
"reply_to_threads": reply_to_threads,
|
|
116
|
+
"update_messages": update_messages,
|
|
117
|
+
"add_reactions": add_reactions,
|
|
118
|
+
"remove_reactions": remove_reactions,
|
|
119
|
+
"create_channels": create_channels,
|
|
120
|
+
"invite_to_channels": invite_to_channels,
|
|
121
|
+
"archive_channels": archive_channels,
|
|
122
|
+
"set_channel_topic": set_channel_topic,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Lazy initialization - don't fetch token or create client yet
|
|
126
|
+
self._client = None
|
|
127
|
+
self._token = None
|
|
128
|
+
self._auth_checked = False
|
|
129
|
+
|
|
130
|
+
# Register tools based on enabled capabilities
|
|
131
|
+
self._register_tools()
|
|
132
|
+
|
|
133
|
+
def _is_channel_allowed(self, channel_id: str, channel_name: str = None) -> tuple[bool, Optional[str]]:
|
|
134
|
+
"""
|
|
135
|
+
Check if a channel is allowed based on scoping configuration.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
channel_id: Channel ID (e.g., "C01234567")
|
|
139
|
+
channel_name: Optional channel name (e.g., "general", "#general")
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (is_allowed, error_message)
|
|
143
|
+
"""
|
|
144
|
+
# Normalize channel name (remove # prefix if present)
|
|
145
|
+
if channel_name:
|
|
146
|
+
channel_name = channel_name.lstrip('#')
|
|
147
|
+
|
|
148
|
+
# If no restrictions, allow all
|
|
149
|
+
if not self.allowed_channels and not self.blocked_channels:
|
|
150
|
+
return True, None
|
|
151
|
+
|
|
152
|
+
# Check blocked channels first
|
|
153
|
+
if self.blocked_channels:
|
|
154
|
+
if channel_id in self.blocked_channels:
|
|
155
|
+
return False, f"Channel {channel_id} is blocked by configuration"
|
|
156
|
+
if channel_name and channel_name in self.blocked_channels:
|
|
157
|
+
return False, f"Channel {channel_name} is blocked by configuration"
|
|
158
|
+
|
|
159
|
+
# Check allowed channels
|
|
160
|
+
if self.allowed_channels:
|
|
161
|
+
if channel_id not in self.allowed_channels:
|
|
162
|
+
if not channel_name or channel_name not in self.allowed_channels:
|
|
163
|
+
return False, f"Channel not in allowed list. Allowed channels: {', '.join(self.allowed_channels)}"
|
|
164
|
+
|
|
165
|
+
return True, None
|
|
166
|
+
|
|
167
|
+
def _register_tools(self):
|
|
168
|
+
"""Register tools based on enabled capabilities and permissions."""
|
|
169
|
+
# Always register get_available_channels (useful for discovery)
|
|
170
|
+
self.register(self.get_available_channels)
|
|
171
|
+
|
|
172
|
+
# Read Messages Group
|
|
173
|
+
if self.capabilities["read_messages"]:
|
|
174
|
+
if self.permissions["read_channels"]:
|
|
175
|
+
self.register(self.list_channels)
|
|
176
|
+
if self.permissions["read_messages"]:
|
|
177
|
+
self.register(self.get_channel_history)
|
|
178
|
+
if self.permissions["search_messages"]:
|
|
179
|
+
self.register(self.search_messages)
|
|
180
|
+
if self.permissions["read_threads"]:
|
|
181
|
+
self.register(self.get_thread_replies)
|
|
182
|
+
if self.permissions["read_users"]:
|
|
183
|
+
self.register(self.get_user_info)
|
|
184
|
+
self.register(self.list_users)
|
|
185
|
+
|
|
186
|
+
# Write Messages Group
|
|
187
|
+
if self.capabilities["write_messages"]:
|
|
188
|
+
if self.permissions["send_messages"]:
|
|
189
|
+
self.register(self.send_message)
|
|
190
|
+
if self.permissions["post_messages"]:
|
|
191
|
+
self.register(self.post_message)
|
|
192
|
+
if self.permissions["reply_to_threads"]:
|
|
193
|
+
self.register(self.reply_to_thread)
|
|
194
|
+
if self.permissions["update_messages"]:
|
|
195
|
+
self.register(self.update_message)
|
|
196
|
+
self.register(self.delete_message)
|
|
197
|
+
|
|
198
|
+
# Reactions Group
|
|
199
|
+
if self.capabilities["reactions"]:
|
|
200
|
+
if self.permissions["add_reactions"]:
|
|
201
|
+
self.register(self.add_reaction)
|
|
202
|
+
if self.permissions["remove_reactions"]:
|
|
203
|
+
self.register(self.remove_reaction)
|
|
204
|
+
self.register(self.list_reactions)
|
|
205
|
+
|
|
206
|
+
# Channel Management Group
|
|
207
|
+
if self.capabilities["channel_management"]:
|
|
208
|
+
if self.permissions["create_channels"]:
|
|
209
|
+
self.register(self.create_channel)
|
|
210
|
+
if self.permissions["invite_to_channels"]:
|
|
211
|
+
self.register(self.invite_to_channel)
|
|
212
|
+
if self.permissions["archive_channels"]:
|
|
213
|
+
self.register(self.archive_channel)
|
|
214
|
+
if self.permissions["set_channel_topic"]:
|
|
215
|
+
self.register(self.set_channel_topic)
|
|
216
|
+
self.register(self.set_channel_description)
|
|
217
|
+
|
|
218
|
+
def _get_client(self):
|
|
219
|
+
"""Lazy initialization of Slack client with authentication."""
|
|
220
|
+
if self._client is not None:
|
|
221
|
+
return self._client
|
|
222
|
+
|
|
223
|
+
# Get authentication token
|
|
224
|
+
token = self._get_auth_token()
|
|
225
|
+
|
|
226
|
+
# Import slack_sdk
|
|
227
|
+
try:
|
|
228
|
+
from slack_sdk import WebClient
|
|
229
|
+
except ImportError:
|
|
230
|
+
raise ImportError(
|
|
231
|
+
"slack_sdk is required for Slack skill. "
|
|
232
|
+
"Install with: pip install slack-sdk"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Create client
|
|
236
|
+
self._client = WebClient(token=token, timeout=self.timeout)
|
|
237
|
+
logger.info("Slack WebClient initialized successfully")
|
|
238
|
+
return self._client
|
|
239
|
+
|
|
240
|
+
def _get_auth_token(self) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Get Slack authentication token.
|
|
243
|
+
|
|
244
|
+
Priority:
|
|
245
|
+
1. Integration API (if integration_id provided)
|
|
246
|
+
2. Secrets from vault (if secrets list provided, resolved as env vars)
|
|
247
|
+
3. Environment variable SLACK_BOT_TOKEN (if use_env_fallback=True)
|
|
248
|
+
|
|
249
|
+
Returns OAuth URL if integration exists but not authenticated.
|
|
250
|
+
Raises exception if no authentication available.
|
|
251
|
+
"""
|
|
252
|
+
if self._token:
|
|
253
|
+
return self._token
|
|
254
|
+
|
|
255
|
+
# Try Integration API first
|
|
256
|
+
if self.integration_id:
|
|
257
|
+
logger.info(f"Attempting to fetch Slack token from Integration API for integration_id: {self.integration_id}")
|
|
258
|
+
token = self._fetch_from_integration_api()
|
|
259
|
+
if token:
|
|
260
|
+
self._token = token
|
|
261
|
+
return token
|
|
262
|
+
|
|
263
|
+
# If integration exists but no token, return OAuth URL
|
|
264
|
+
oauth_url = self._get_oauth_url()
|
|
265
|
+
if oauth_url:
|
|
266
|
+
logger.warning(f"Slack integration not authenticated, OAuth URL: {oauth_url}")
|
|
267
|
+
raise Exception(
|
|
268
|
+
f"❌ Slack integration not authenticated.\n\n"
|
|
269
|
+
f"🔗 Please complete OAuth flow:\n{oauth_url}\n\n"
|
|
270
|
+
f"After authorizing, please retry your request."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Try secrets from vault (resolved as environment variables)
|
|
274
|
+
# Check each secret in order, use first one found
|
|
275
|
+
if self.secrets:
|
|
276
|
+
for secret_name in self.secrets:
|
|
277
|
+
# Secrets from the vault are injected as environment variables
|
|
278
|
+
# by the execution environment controller with the secret name as the key
|
|
279
|
+
token = os.environ.get(secret_name)
|
|
280
|
+
if token:
|
|
281
|
+
logger.info(f"Using Slack token from secret: {secret_name}")
|
|
282
|
+
self._token = token
|
|
283
|
+
return token
|
|
284
|
+
|
|
285
|
+
# None of the secrets were found
|
|
286
|
+
logger.warning(f"None of the configured secrets found in environment: {self.secrets}")
|
|
287
|
+
|
|
288
|
+
# Try environment variable fallback
|
|
289
|
+
if self.use_env_fallback:
|
|
290
|
+
token = os.environ.get("SLACK_BOT_TOKEN")
|
|
291
|
+
if token:
|
|
292
|
+
logger.info("Using Slack token from SLACK_BOT_TOKEN environment variable")
|
|
293
|
+
self._token = token
|
|
294
|
+
return token
|
|
295
|
+
|
|
296
|
+
# No authentication available
|
|
297
|
+
logger.error("No Slack authentication found")
|
|
298
|
+
raise Exception(
|
|
299
|
+
"❌ No Slack authentication found.\n\n"
|
|
300
|
+
"Either provide integration_id with authenticated OAuth, "
|
|
301
|
+
"provide secrets referencing secrets in the vault, "
|
|
302
|
+
"or set SLACK_BOT_TOKEN environment variable."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _fetch_from_integration_api(self) -> Optional[str]:
|
|
306
|
+
"""Fetch token from Integration API."""
|
|
307
|
+
try:
|
|
308
|
+
import httpx
|
|
309
|
+
|
|
310
|
+
# Get integration API base URL from environment
|
|
311
|
+
integration_api_base = os.environ.get(
|
|
312
|
+
"INTEGRATION_API_BASE",
|
|
313
|
+
"https://api.kubiya.ai"
|
|
314
|
+
)
|
|
315
|
+
api_key = os.environ.get("KUBIYA_API_KEY")
|
|
316
|
+
|
|
317
|
+
if not api_key:
|
|
318
|
+
logger.warning("KUBIYA_API_KEY not set, cannot fetch from Integration API")
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
# Call Integration API
|
|
322
|
+
url = f"{integration_api_base}/integrations/slack/{self.integration_id}/token"
|
|
323
|
+
headers = {
|
|
324
|
+
"Authorization": f"Bearer {api_key}",
|
|
325
|
+
"Accept": "application/json"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
logger.debug(f"Fetching Slack token from: {url}")
|
|
329
|
+
response = httpx.get(url, headers=headers, timeout=self.timeout)
|
|
330
|
+
response.raise_for_status()
|
|
331
|
+
|
|
332
|
+
data = response.json()
|
|
333
|
+
token = data.get("access_token")
|
|
334
|
+
|
|
335
|
+
if token:
|
|
336
|
+
logger.info("Successfully fetched Slack token from Integration API")
|
|
337
|
+
return token
|
|
338
|
+
|
|
339
|
+
logger.warning("No access_token in Integration API response")
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(f"Failed to fetch Slack token from Integration API: {e}")
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
def _get_oauth_url(self) -> Optional[str]:
|
|
347
|
+
"""Get OAuth URL for Slack integration."""
|
|
348
|
+
try:
|
|
349
|
+
import httpx
|
|
350
|
+
|
|
351
|
+
integration_api_base = os.environ.get(
|
|
352
|
+
"INTEGRATION_API_BASE",
|
|
353
|
+
"https://api.kubiya.ai"
|
|
354
|
+
)
|
|
355
|
+
api_key = os.environ.get("KUBIYA_API_KEY")
|
|
356
|
+
|
|
357
|
+
if not api_key:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
url = f"{integration_api_base}/integrations/slack/{self.integration_id}/oauth-url"
|
|
361
|
+
headers = {
|
|
362
|
+
"Authorization": f"Bearer {api_key}",
|
|
363
|
+
"Accept": "application/json"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
response = httpx.get(url, headers=headers, timeout=self.timeout)
|
|
367
|
+
response.raise_for_status()
|
|
368
|
+
|
|
369
|
+
data = response.json()
|
|
370
|
+
return data.get("oauth_url")
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(f"Failed to get OAuth URL: {e}")
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
# ========================================================================
|
|
377
|
+
# Channel Discovery & Scoping
|
|
378
|
+
# ========================================================================
|
|
379
|
+
|
|
380
|
+
def get_available_channels(
|
|
381
|
+
self,
|
|
382
|
+
types: str = "public_channel,private_channel",
|
|
383
|
+
limit: int = 100
|
|
384
|
+
) -> str:
|
|
385
|
+
"""
|
|
386
|
+
Get list of channels available to this skill based on configuration.
|
|
387
|
+
|
|
388
|
+
This tool respects the allowed_channels and blocked_channels configuration,
|
|
389
|
+
returning only channels that the skill is permitted to access.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
types: Channel types (public_channel,private_channel,mpim,im)
|
|
393
|
+
limit: Maximum channels to return (default: 100)
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
JSON string with available channels and scoping info
|
|
397
|
+
"""
|
|
398
|
+
try:
|
|
399
|
+
client = self._get_client()
|
|
400
|
+
response = client.conversations_list(
|
|
401
|
+
types=types,
|
|
402
|
+
limit=min(limit, 1000)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
all_channels = response.get("channels", [])
|
|
406
|
+
|
|
407
|
+
# Filter channels based on scoping configuration
|
|
408
|
+
available_channels = []
|
|
409
|
+
blocked_count = 0
|
|
410
|
+
|
|
411
|
+
for ch in all_channels:
|
|
412
|
+
channel_id = ch["id"]
|
|
413
|
+
channel_name = ch.get("name")
|
|
414
|
+
|
|
415
|
+
# Check if channel is allowed
|
|
416
|
+
is_allowed, error_msg = self._is_channel_allowed(channel_id, channel_name)
|
|
417
|
+
|
|
418
|
+
if is_allowed:
|
|
419
|
+
available_channels.append({
|
|
420
|
+
"id": ch["id"],
|
|
421
|
+
"name": ch.get("name"),
|
|
422
|
+
"is_private": ch.get("is_private", False),
|
|
423
|
+
"is_archived": ch.get("is_archived", False),
|
|
424
|
+
"is_member": ch.get("is_member", False),
|
|
425
|
+
"num_members": ch.get("num_members", 0),
|
|
426
|
+
"topic": ch.get("topic", {}).get("value", ""),
|
|
427
|
+
"purpose": ch.get("purpose", {}).get("value", "")
|
|
428
|
+
})
|
|
429
|
+
else:
|
|
430
|
+
blocked_count += 1
|
|
431
|
+
|
|
432
|
+
result = {
|
|
433
|
+
"success": True,
|
|
434
|
+
"available_channels": available_channels,
|
|
435
|
+
"available_count": len(available_channels),
|
|
436
|
+
"total_channels": len(all_channels),
|
|
437
|
+
"blocked_count": blocked_count,
|
|
438
|
+
"scoping_active": bool(self.allowed_channels or self.blocked_channels),
|
|
439
|
+
"allowed_channels_config": self.allowed_channels,
|
|
440
|
+
"blocked_channels_config": self.blocked_channels,
|
|
441
|
+
"message": (
|
|
442
|
+
f"Found {len(available_channels)} available channels "
|
|
443
|
+
f"(blocked {blocked_count} due to configuration)"
|
|
444
|
+
if blocked_count > 0
|
|
445
|
+
else f"Found {len(available_channels)} available channels"
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
return json.dumps(result, indent=2)
|
|
449
|
+
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Error getting available channels: {e}")
|
|
452
|
+
return json.dumps({
|
|
453
|
+
"success": False,
|
|
454
|
+
"error": str(e)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
# ========================================================================
|
|
458
|
+
# Read Messages Group
|
|
459
|
+
# ========================================================================
|
|
460
|
+
|
|
461
|
+
def list_channels(
|
|
462
|
+
self,
|
|
463
|
+
types: str = "public_channel,private_channel",
|
|
464
|
+
limit: int = 100
|
|
465
|
+
) -> str:
|
|
466
|
+
"""
|
|
467
|
+
List Slack channels.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
types: Channel types (public_channel,private_channel,mpim,im)
|
|
471
|
+
limit: Maximum channels to return (default: 100)
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
JSON string with channel list
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
client = self._get_client()
|
|
478
|
+
response = client.conversations_list(
|
|
479
|
+
types=types,
|
|
480
|
+
limit=min(limit, 1000)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
channels = response.get("channels", [])
|
|
484
|
+
result = {
|
|
485
|
+
"success": True,
|
|
486
|
+
"channels": [
|
|
487
|
+
{
|
|
488
|
+
"id": ch["id"],
|
|
489
|
+
"name": ch.get("name"),
|
|
490
|
+
"is_private": ch.get("is_private", False),
|
|
491
|
+
"is_archived": ch.get("is_archived", False),
|
|
492
|
+
"is_member": ch.get("is_member", False),
|
|
493
|
+
"num_members": ch.get("num_members", 0),
|
|
494
|
+
"topic": ch.get("topic", {}).get("value", ""),
|
|
495
|
+
"purpose": ch.get("purpose", {}).get("value", "")
|
|
496
|
+
}
|
|
497
|
+
for ch in channels
|
|
498
|
+
],
|
|
499
|
+
"count": len(channels)
|
|
500
|
+
}
|
|
501
|
+
return json.dumps(result, indent=2)
|
|
502
|
+
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.error(f"Error listing channels: {e}")
|
|
505
|
+
return json.dumps({
|
|
506
|
+
"success": False,
|
|
507
|
+
"error": str(e)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
def get_channel_history(
|
|
511
|
+
self,
|
|
512
|
+
channel_id: str,
|
|
513
|
+
limit: int = 50,
|
|
514
|
+
oldest: Optional[str] = None,
|
|
515
|
+
latest: Optional[str] = None
|
|
516
|
+
) -> str:
|
|
517
|
+
"""
|
|
518
|
+
Get message history from a Slack channel.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
channel_id: Channel ID (required)
|
|
522
|
+
limit: Number of messages (default: 50, max: 1000)
|
|
523
|
+
oldest: Timestamp for oldest message
|
|
524
|
+
latest: Timestamp for latest message
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
JSON string with message history
|
|
528
|
+
"""
|
|
529
|
+
try:
|
|
530
|
+
client = self._get_client()
|
|
531
|
+
kwargs = {
|
|
532
|
+
"channel": channel_id,
|
|
533
|
+
"limit": min(limit, 1000)
|
|
534
|
+
}
|
|
535
|
+
if oldest:
|
|
536
|
+
kwargs["oldest"] = oldest
|
|
537
|
+
if latest:
|
|
538
|
+
kwargs["latest"] = latest
|
|
539
|
+
|
|
540
|
+
response = client.conversations_history(**kwargs)
|
|
541
|
+
|
|
542
|
+
messages = response.get("messages", [])
|
|
543
|
+
result = {
|
|
544
|
+
"success": True,
|
|
545
|
+
"channel_id": channel_id,
|
|
546
|
+
"messages": [
|
|
547
|
+
{
|
|
548
|
+
"ts": msg.get("ts"),
|
|
549
|
+
"user": msg.get("user"),
|
|
550
|
+
"text": msg.get("text"),
|
|
551
|
+
"type": msg.get("type"),
|
|
552
|
+
"thread_ts": msg.get("thread_ts"),
|
|
553
|
+
"reply_count": msg.get("reply_count", 0),
|
|
554
|
+
"reactions": msg.get("reactions", [])
|
|
555
|
+
}
|
|
556
|
+
for msg in messages
|
|
557
|
+
],
|
|
558
|
+
"count": len(messages),
|
|
559
|
+
"has_more": response.get("has_more", False)
|
|
560
|
+
}
|
|
561
|
+
return json.dumps(result, indent=2)
|
|
562
|
+
|
|
563
|
+
except Exception as e:
|
|
564
|
+
logger.error(f"Error getting channel history: {e}")
|
|
565
|
+
return json.dumps({
|
|
566
|
+
"success": False,
|
|
567
|
+
"error": str(e)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
def search_messages(
|
|
571
|
+
self,
|
|
572
|
+
query: str,
|
|
573
|
+
count: int = 20,
|
|
574
|
+
sort: str = "score"
|
|
575
|
+
) -> str:
|
|
576
|
+
"""
|
|
577
|
+
Search for messages in Slack.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
query: Search query (required)
|
|
581
|
+
count: Number of results (default: 20, max: 100)
|
|
582
|
+
sort: Sort by 'score' or 'timestamp' (default: score)
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
JSON string with search results
|
|
586
|
+
"""
|
|
587
|
+
try:
|
|
588
|
+
client = self._get_client()
|
|
589
|
+
response = client.search_messages(
|
|
590
|
+
query=query,
|
|
591
|
+
count=min(count, 100),
|
|
592
|
+
sort=sort
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
messages = response.get("messages", {}).get("matches", [])
|
|
596
|
+
result = {
|
|
597
|
+
"success": True,
|
|
598
|
+
"query": query,
|
|
599
|
+
"matches": [
|
|
600
|
+
{
|
|
601
|
+
"text": msg.get("text"),
|
|
602
|
+
"user": msg.get("user"),
|
|
603
|
+
"username": msg.get("username"),
|
|
604
|
+
"channel": msg.get("channel", {}).get("name"),
|
|
605
|
+
"channel_id": msg.get("channel", {}).get("id"),
|
|
606
|
+
"ts": msg.get("ts"),
|
|
607
|
+
"permalink": msg.get("permalink"),
|
|
608
|
+
"type": msg.get("type")
|
|
609
|
+
}
|
|
610
|
+
for msg in messages
|
|
611
|
+
],
|
|
612
|
+
"count": len(messages),
|
|
613
|
+
"total": response.get("messages", {}).get("total", 0)
|
|
614
|
+
}
|
|
615
|
+
return json.dumps(result, indent=2)
|
|
616
|
+
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error(f"Error searching messages: {e}")
|
|
619
|
+
return json.dumps({
|
|
620
|
+
"success": False,
|
|
621
|
+
"error": str(e)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
def get_thread_replies(
|
|
625
|
+
self,
|
|
626
|
+
channel_id: str,
|
|
627
|
+
thread_ts: str,
|
|
628
|
+
limit: int = 50
|
|
629
|
+
) -> str:
|
|
630
|
+
"""
|
|
631
|
+
Get replies from a Slack thread.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
channel_id: Channel ID (required)
|
|
635
|
+
thread_ts: Thread timestamp (required)
|
|
636
|
+
limit: Number of replies (default: 50, max: 1000)
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
JSON string with thread replies
|
|
640
|
+
"""
|
|
641
|
+
try:
|
|
642
|
+
client = self._get_client()
|
|
643
|
+
response = client.conversations_replies(
|
|
644
|
+
channel=channel_id,
|
|
645
|
+
ts=thread_ts,
|
|
646
|
+
limit=min(limit, 1000)
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
messages = response.get("messages", [])
|
|
650
|
+
result = {
|
|
651
|
+
"success": True,
|
|
652
|
+
"channel_id": channel_id,
|
|
653
|
+
"thread_ts": thread_ts,
|
|
654
|
+
"messages": [
|
|
655
|
+
{
|
|
656
|
+
"ts": msg.get("ts"),
|
|
657
|
+
"user": msg.get("user"),
|
|
658
|
+
"text": msg.get("text"),
|
|
659
|
+
"type": msg.get("type"),
|
|
660
|
+
"reactions": msg.get("reactions", [])
|
|
661
|
+
}
|
|
662
|
+
for msg in messages
|
|
663
|
+
],
|
|
664
|
+
"count": len(messages)
|
|
665
|
+
}
|
|
666
|
+
return json.dumps(result, indent=2)
|
|
667
|
+
|
|
668
|
+
except Exception as e:
|
|
669
|
+
logger.error(f"Error getting thread replies: {e}")
|
|
670
|
+
return json.dumps({
|
|
671
|
+
"success": False,
|
|
672
|
+
"error": str(e)
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
def get_user_info(
|
|
676
|
+
self,
|
|
677
|
+
user_id: str
|
|
678
|
+
) -> str:
|
|
679
|
+
"""
|
|
680
|
+
Get information about a Slack user.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
user_id: User ID (required)
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
JSON string with user information
|
|
687
|
+
"""
|
|
688
|
+
try:
|
|
689
|
+
client = self._get_client()
|
|
690
|
+
response = client.users_info(user=user_id)
|
|
691
|
+
|
|
692
|
+
user = response.get("user", {})
|
|
693
|
+
result = {
|
|
694
|
+
"success": True,
|
|
695
|
+
"user": {
|
|
696
|
+
"id": user.get("id"),
|
|
697
|
+
"name": user.get("name"),
|
|
698
|
+
"real_name": user.get("real_name"),
|
|
699
|
+
"display_name": user.get("profile", {}).get("display_name"),
|
|
700
|
+
"email": user.get("profile", {}).get("email"),
|
|
701
|
+
"title": user.get("profile", {}).get("title"),
|
|
702
|
+
"is_bot": user.get("is_bot", False),
|
|
703
|
+
"is_admin": user.get("is_admin", False),
|
|
704
|
+
"is_owner": user.get("is_owner", False),
|
|
705
|
+
"tz": user.get("tz"),
|
|
706
|
+
"status_text": user.get("profile", {}).get("status_text", ""),
|
|
707
|
+
"status_emoji": user.get("profile", {}).get("status_emoji", "")
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return json.dumps(result, indent=2)
|
|
711
|
+
|
|
712
|
+
except Exception as e:
|
|
713
|
+
logger.error(f"Error getting user info: {e}")
|
|
714
|
+
return json.dumps({
|
|
715
|
+
"success": False,
|
|
716
|
+
"error": str(e)
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
def list_users(
|
|
720
|
+
self,
|
|
721
|
+
limit: int = 100
|
|
722
|
+
) -> str:
|
|
723
|
+
"""
|
|
724
|
+
List users in the Slack workspace.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
limit: Maximum users to return (default: 100, max: 1000)
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
JSON string with user list
|
|
731
|
+
"""
|
|
732
|
+
try:
|
|
733
|
+
client = self._get_client()
|
|
734
|
+
response = client.users_list(limit=min(limit, 1000))
|
|
735
|
+
|
|
736
|
+
members = response.get("members", [])
|
|
737
|
+
result = {
|
|
738
|
+
"success": True,
|
|
739
|
+
"users": [
|
|
740
|
+
{
|
|
741
|
+
"id": user.get("id"),
|
|
742
|
+
"name": user.get("name"),
|
|
743
|
+
"real_name": user.get("real_name"),
|
|
744
|
+
"display_name": user.get("profile", {}).get("display_name"),
|
|
745
|
+
"is_bot": user.get("is_bot", False),
|
|
746
|
+
"is_admin": user.get("is_admin", False),
|
|
747
|
+
"deleted": user.get("deleted", False)
|
|
748
|
+
}
|
|
749
|
+
for user in members
|
|
750
|
+
if not user.get("deleted", False) # Filter out deleted users
|
|
751
|
+
],
|
|
752
|
+
"count": len([u for u in members if not u.get("deleted", False)])
|
|
753
|
+
}
|
|
754
|
+
return json.dumps(result, indent=2)
|
|
755
|
+
|
|
756
|
+
except Exception as e:
|
|
757
|
+
logger.error(f"Error listing users: {e}")
|
|
758
|
+
return json.dumps({
|
|
759
|
+
"success": False,
|
|
760
|
+
"error": str(e)
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
# ========================================================================
|
|
764
|
+
# Write Messages Group
|
|
765
|
+
# ========================================================================
|
|
766
|
+
|
|
767
|
+
def send_message(
|
|
768
|
+
self,
|
|
769
|
+
channel: str,
|
|
770
|
+
text: str,
|
|
771
|
+
thread_ts: Optional[str] = None,
|
|
772
|
+
blocks: Optional[str] = None
|
|
773
|
+
) -> str:
|
|
774
|
+
"""
|
|
775
|
+
Send a message to a Slack channel.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
channel: Channel ID or name (required)
|
|
779
|
+
text: Message text (required)
|
|
780
|
+
thread_ts: Thread timestamp (to reply in thread)
|
|
781
|
+
blocks: Optional Block Kit blocks as JSON string
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
JSON string with message details
|
|
785
|
+
"""
|
|
786
|
+
try:
|
|
787
|
+
# Check channel scoping
|
|
788
|
+
is_allowed, error_msg = self._is_channel_allowed(channel, channel)
|
|
789
|
+
if not is_allowed:
|
|
790
|
+
return json.dumps({
|
|
791
|
+
"success": False,
|
|
792
|
+
"error": f"❌ Channel access denied: {error_msg}"
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
client = self._get_client()
|
|
796
|
+
kwargs = {
|
|
797
|
+
"channel": channel,
|
|
798
|
+
"text": text
|
|
799
|
+
}
|
|
800
|
+
if thread_ts:
|
|
801
|
+
kwargs["thread_ts"] = thread_ts
|
|
802
|
+
if blocks:
|
|
803
|
+
kwargs["blocks"] = json.loads(blocks) if isinstance(blocks, str) else blocks
|
|
804
|
+
|
|
805
|
+
response = client.chat_postMessage(**kwargs)
|
|
806
|
+
|
|
807
|
+
result = {
|
|
808
|
+
"success": True,
|
|
809
|
+
"channel": response.get("channel"),
|
|
810
|
+
"ts": response.get("ts"),
|
|
811
|
+
"message": {
|
|
812
|
+
"text": response.get("message", {}).get("text"),
|
|
813
|
+
"user": response.get("message", {}).get("user"),
|
|
814
|
+
"ts": response.get("message", {}).get("ts")
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return json.dumps(result, indent=2)
|
|
818
|
+
|
|
819
|
+
except Exception as e:
|
|
820
|
+
logger.error(f"Error sending message: {e}")
|
|
821
|
+
return json.dumps({
|
|
822
|
+
"success": False,
|
|
823
|
+
"error": str(e)
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
def post_message(
|
|
827
|
+
self,
|
|
828
|
+
channel: str,
|
|
829
|
+
text: str,
|
|
830
|
+
blocks: Optional[str] = None
|
|
831
|
+
) -> str:
|
|
832
|
+
"""
|
|
833
|
+
Post a message to a Slack channel (alias for send_message without thread).
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
channel: Channel ID or name (required)
|
|
837
|
+
text: Message text (required)
|
|
838
|
+
blocks: Optional Block Kit blocks as JSON string
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
JSON string with message details
|
|
842
|
+
"""
|
|
843
|
+
return self.send_message(channel=channel, text=text, blocks=blocks)
|
|
844
|
+
|
|
845
|
+
def reply_to_thread(
|
|
846
|
+
self,
|
|
847
|
+
channel: str,
|
|
848
|
+
thread_ts: str,
|
|
849
|
+
text: str
|
|
850
|
+
) -> str:
|
|
851
|
+
"""
|
|
852
|
+
Reply to a Slack thread.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
channel: Channel ID or name (required)
|
|
856
|
+
thread_ts: Thread timestamp (required)
|
|
857
|
+
text: Reply text (required)
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
JSON string with reply details
|
|
861
|
+
"""
|
|
862
|
+
return self.send_message(channel=channel, text=text, thread_ts=thread_ts)
|
|
863
|
+
|
|
864
|
+
def update_message(
|
|
865
|
+
self,
|
|
866
|
+
channel: str,
|
|
867
|
+
ts: str,
|
|
868
|
+
text: str,
|
|
869
|
+
blocks: Optional[str] = None
|
|
870
|
+
) -> str:
|
|
871
|
+
"""
|
|
872
|
+
Update an existing Slack message.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
channel: Channel ID (required)
|
|
876
|
+
ts: Message timestamp (required)
|
|
877
|
+
text: New message text (required)
|
|
878
|
+
blocks: Optional Block Kit blocks as JSON string
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
JSON string with update confirmation
|
|
882
|
+
"""
|
|
883
|
+
try:
|
|
884
|
+
client = self._get_client()
|
|
885
|
+
kwargs = {
|
|
886
|
+
"channel": channel,
|
|
887
|
+
"ts": ts,
|
|
888
|
+
"text": text
|
|
889
|
+
}
|
|
890
|
+
if blocks:
|
|
891
|
+
kwargs["blocks"] = json.loads(blocks) if isinstance(blocks, str) else blocks
|
|
892
|
+
|
|
893
|
+
response = client.chat_update(**kwargs)
|
|
894
|
+
|
|
895
|
+
result = {
|
|
896
|
+
"success": True,
|
|
897
|
+
"channel": response.get("channel"),
|
|
898
|
+
"ts": response.get("ts"),
|
|
899
|
+
"text": response.get("text")
|
|
900
|
+
}
|
|
901
|
+
return json.dumps(result, indent=2)
|
|
902
|
+
|
|
903
|
+
except Exception as e:
|
|
904
|
+
logger.error(f"Error updating message: {e}")
|
|
905
|
+
return json.dumps({
|
|
906
|
+
"success": False,
|
|
907
|
+
"error": str(e)
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
def delete_message(
|
|
911
|
+
self,
|
|
912
|
+
channel: str,
|
|
913
|
+
ts: str
|
|
914
|
+
) -> str:
|
|
915
|
+
"""
|
|
916
|
+
Delete a Slack message.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
channel: Channel ID (required)
|
|
920
|
+
ts: Message timestamp (required)
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
JSON string with deletion confirmation
|
|
924
|
+
"""
|
|
925
|
+
try:
|
|
926
|
+
client = self._get_client()
|
|
927
|
+
response = client.chat_delete(
|
|
928
|
+
channel=channel,
|
|
929
|
+
ts=ts
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
result = {
|
|
933
|
+
"success": True,
|
|
934
|
+
"channel": response.get("channel"),
|
|
935
|
+
"ts": response.get("ts")
|
|
936
|
+
}
|
|
937
|
+
return json.dumps(result, indent=2)
|
|
938
|
+
|
|
939
|
+
except Exception as e:
|
|
940
|
+
logger.error(f"Error deleting message: {e}")
|
|
941
|
+
return json.dumps({
|
|
942
|
+
"success": False,
|
|
943
|
+
"error": str(e)
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
# ========================================================================
|
|
947
|
+
# Reactions Group
|
|
948
|
+
# ========================================================================
|
|
949
|
+
|
|
950
|
+
def add_reaction(
|
|
951
|
+
self,
|
|
952
|
+
channel: str,
|
|
953
|
+
ts: str,
|
|
954
|
+
reaction: str
|
|
955
|
+
) -> str:
|
|
956
|
+
"""
|
|
957
|
+
Add a reaction to a Slack message.
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
channel: Channel ID (required)
|
|
961
|
+
ts: Message timestamp (required)
|
|
962
|
+
reaction: Reaction emoji name without colons (e.g., 'thumbsup')
|
|
963
|
+
|
|
964
|
+
Returns:
|
|
965
|
+
JSON string with confirmation
|
|
966
|
+
"""
|
|
967
|
+
try:
|
|
968
|
+
client = self._get_client()
|
|
969
|
+
# Remove colons if present
|
|
970
|
+
reaction = reaction.strip(':')
|
|
971
|
+
|
|
972
|
+
response = client.reactions_add(
|
|
973
|
+
channel=channel,
|
|
974
|
+
timestamp=ts,
|
|
975
|
+
name=reaction
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
result = {
|
|
979
|
+
"success": True,
|
|
980
|
+
"reaction": reaction,
|
|
981
|
+
"channel": channel,
|
|
982
|
+
"ts": ts
|
|
983
|
+
}
|
|
984
|
+
return json.dumps(result, indent=2)
|
|
985
|
+
|
|
986
|
+
except Exception as e:
|
|
987
|
+
logger.error(f"Error adding reaction: {e}")
|
|
988
|
+
return json.dumps({
|
|
989
|
+
"success": False,
|
|
990
|
+
"error": str(e)
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
def remove_reaction(
|
|
994
|
+
self,
|
|
995
|
+
channel: str,
|
|
996
|
+
ts: str,
|
|
997
|
+
reaction: str
|
|
998
|
+
) -> str:
|
|
999
|
+
"""
|
|
1000
|
+
Remove a reaction from a Slack message.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
channel: Channel ID (required)
|
|
1004
|
+
ts: Message timestamp (required)
|
|
1005
|
+
reaction: Reaction emoji name without colons (e.g., 'thumbsup')
|
|
1006
|
+
|
|
1007
|
+
Returns:
|
|
1008
|
+
JSON string with confirmation
|
|
1009
|
+
"""
|
|
1010
|
+
try:
|
|
1011
|
+
client = self._get_client()
|
|
1012
|
+
# Remove colons if present
|
|
1013
|
+
reaction = reaction.strip(':')
|
|
1014
|
+
|
|
1015
|
+
response = client.reactions_remove(
|
|
1016
|
+
channel=channel,
|
|
1017
|
+
timestamp=ts,
|
|
1018
|
+
name=reaction
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
result = {
|
|
1022
|
+
"success": True,
|
|
1023
|
+
"reaction": reaction,
|
|
1024
|
+
"channel": channel,
|
|
1025
|
+
"ts": ts
|
|
1026
|
+
}
|
|
1027
|
+
return json.dumps(result, indent=2)
|
|
1028
|
+
|
|
1029
|
+
except Exception as e:
|
|
1030
|
+
logger.error(f"Error removing reaction: {e}")
|
|
1031
|
+
return json.dumps({
|
|
1032
|
+
"success": False,
|
|
1033
|
+
"error": str(e)
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
def list_reactions(
|
|
1037
|
+
self,
|
|
1038
|
+
channel: str,
|
|
1039
|
+
ts: str
|
|
1040
|
+
) -> str:
|
|
1041
|
+
"""
|
|
1042
|
+
List reactions on a Slack message.
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
channel: Channel ID (required)
|
|
1046
|
+
ts: Message timestamp (required)
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
JSON string with reactions list
|
|
1050
|
+
"""
|
|
1051
|
+
try:
|
|
1052
|
+
client = self._get_client()
|
|
1053
|
+
response = client.reactions_get(
|
|
1054
|
+
channel=channel,
|
|
1055
|
+
timestamp=ts
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
message = response.get("message", {})
|
|
1059
|
+
reactions = message.get("reactions", [])
|
|
1060
|
+
|
|
1061
|
+
result = {
|
|
1062
|
+
"success": True,
|
|
1063
|
+
"channel": channel,
|
|
1064
|
+
"ts": ts,
|
|
1065
|
+
"reactions": [
|
|
1066
|
+
{
|
|
1067
|
+
"name": r.get("name"),
|
|
1068
|
+
"count": r.get("count"),
|
|
1069
|
+
"users": r.get("users", [])
|
|
1070
|
+
}
|
|
1071
|
+
for r in reactions
|
|
1072
|
+
],
|
|
1073
|
+
"count": len(reactions)
|
|
1074
|
+
}
|
|
1075
|
+
return json.dumps(result, indent=2)
|
|
1076
|
+
|
|
1077
|
+
except Exception as e:
|
|
1078
|
+
logger.error(f"Error listing reactions: {e}")
|
|
1079
|
+
return json.dumps({
|
|
1080
|
+
"success": False,
|
|
1081
|
+
"error": str(e)
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
# ========================================================================
|
|
1085
|
+
# Channel Management Group
|
|
1086
|
+
# ========================================================================
|
|
1087
|
+
|
|
1088
|
+
def create_channel(
|
|
1089
|
+
self,
|
|
1090
|
+
name: str,
|
|
1091
|
+
is_private: bool = False,
|
|
1092
|
+
description: Optional[str] = None
|
|
1093
|
+
) -> str:
|
|
1094
|
+
"""
|
|
1095
|
+
Create a new Slack channel.
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
name: Channel name (required, lowercase, no spaces)
|
|
1099
|
+
is_private: Create as private channel (default: False)
|
|
1100
|
+
description: Optional channel description
|
|
1101
|
+
|
|
1102
|
+
Returns:
|
|
1103
|
+
JSON string with new channel details
|
|
1104
|
+
"""
|
|
1105
|
+
try:
|
|
1106
|
+
client = self._get_client()
|
|
1107
|
+
response = client.conversations_create(
|
|
1108
|
+
name=name,
|
|
1109
|
+
is_private=is_private
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
channel = response.get("channel", {})
|
|
1113
|
+
|
|
1114
|
+
# Set description if provided
|
|
1115
|
+
if description and channel.get("id"):
|
|
1116
|
+
try:
|
|
1117
|
+
client.conversations_setPurpose(
|
|
1118
|
+
channel=channel["id"],
|
|
1119
|
+
purpose=description
|
|
1120
|
+
)
|
|
1121
|
+
except:
|
|
1122
|
+
pass # Description setting is optional
|
|
1123
|
+
|
|
1124
|
+
result = {
|
|
1125
|
+
"success": True,
|
|
1126
|
+
"channel": {
|
|
1127
|
+
"id": channel.get("id"),
|
|
1128
|
+
"name": channel.get("name"),
|
|
1129
|
+
"is_private": channel.get("is_private", False),
|
|
1130
|
+
"created": channel.get("created")
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return json.dumps(result, indent=2)
|
|
1134
|
+
|
|
1135
|
+
except Exception as e:
|
|
1136
|
+
logger.error(f"Error creating channel: {e}")
|
|
1137
|
+
return json.dumps({
|
|
1138
|
+
"success": False,
|
|
1139
|
+
"error": str(e)
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
def invite_to_channel(
|
|
1143
|
+
self,
|
|
1144
|
+
channel: str,
|
|
1145
|
+
user_ids: str
|
|
1146
|
+
) -> str:
|
|
1147
|
+
"""
|
|
1148
|
+
Invite users to a Slack channel.
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
channel: Channel ID (required)
|
|
1152
|
+
user_ids: Comma-separated user IDs (required)
|
|
1153
|
+
|
|
1154
|
+
Returns:
|
|
1155
|
+
JSON string with confirmation
|
|
1156
|
+
"""
|
|
1157
|
+
try:
|
|
1158
|
+
client = self._get_client()
|
|
1159
|
+
# Parse user IDs
|
|
1160
|
+
users = [uid.strip() for uid in user_ids.split(',')]
|
|
1161
|
+
|
|
1162
|
+
response = client.conversations_invite(
|
|
1163
|
+
channel=channel,
|
|
1164
|
+
users=users
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
result = {
|
|
1168
|
+
"success": True,
|
|
1169
|
+
"channel": response.get("channel", {}).get("id"),
|
|
1170
|
+
"invited_users": users,
|
|
1171
|
+
"count": len(users)
|
|
1172
|
+
}
|
|
1173
|
+
return json.dumps(result, indent=2)
|
|
1174
|
+
|
|
1175
|
+
except Exception as e:
|
|
1176
|
+
logger.error(f"Error inviting to channel: {e}")
|
|
1177
|
+
return json.dumps({
|
|
1178
|
+
"success": False,
|
|
1179
|
+
"error": str(e)
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
def archive_channel(
|
|
1183
|
+
self,
|
|
1184
|
+
channel: str
|
|
1185
|
+
) -> str:
|
|
1186
|
+
"""
|
|
1187
|
+
Archive a Slack channel.
|
|
1188
|
+
|
|
1189
|
+
Args:
|
|
1190
|
+
channel: Channel ID (required)
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
JSON string with confirmation
|
|
1194
|
+
"""
|
|
1195
|
+
try:
|
|
1196
|
+
client = self._get_client()
|
|
1197
|
+
response = client.conversations_archive(channel=channel)
|
|
1198
|
+
|
|
1199
|
+
result = {
|
|
1200
|
+
"success": True,
|
|
1201
|
+
"channel": channel
|
|
1202
|
+
}
|
|
1203
|
+
return json.dumps(result, indent=2)
|
|
1204
|
+
|
|
1205
|
+
except Exception as e:
|
|
1206
|
+
logger.error(f"Error archiving channel: {e}")
|
|
1207
|
+
return json.dumps({
|
|
1208
|
+
"success": False,
|
|
1209
|
+
"error": str(e)
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
def set_channel_topic(
|
|
1213
|
+
self,
|
|
1214
|
+
channel: str,
|
|
1215
|
+
topic: str
|
|
1216
|
+
) -> str:
|
|
1217
|
+
"""
|
|
1218
|
+
Set a Slack channel's topic.
|
|
1219
|
+
|
|
1220
|
+
Args:
|
|
1221
|
+
channel: Channel ID (required)
|
|
1222
|
+
topic: New topic text (required)
|
|
1223
|
+
|
|
1224
|
+
Returns:
|
|
1225
|
+
JSON string with confirmation
|
|
1226
|
+
"""
|
|
1227
|
+
try:
|
|
1228
|
+
client = self._get_client()
|
|
1229
|
+
response = client.conversations_setTopic(
|
|
1230
|
+
channel=channel,
|
|
1231
|
+
topic=topic
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
result = {
|
|
1235
|
+
"success": True,
|
|
1236
|
+
"channel": response.get("channel"),
|
|
1237
|
+
"topic": response.get("topic")
|
|
1238
|
+
}
|
|
1239
|
+
return json.dumps(result, indent=2)
|
|
1240
|
+
|
|
1241
|
+
except Exception as e:
|
|
1242
|
+
logger.error(f"Error setting channel topic: {e}")
|
|
1243
|
+
return json.dumps({
|
|
1244
|
+
"success": False,
|
|
1245
|
+
"error": str(e)
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
def set_channel_description(
|
|
1249
|
+
self,
|
|
1250
|
+
channel: str,
|
|
1251
|
+
description: str
|
|
1252
|
+
) -> str:
|
|
1253
|
+
"""
|
|
1254
|
+
Set a Slack channel's description (purpose).
|
|
1255
|
+
|
|
1256
|
+
Args:
|
|
1257
|
+
channel: Channel ID (required)
|
|
1258
|
+
description: New description text (required)
|
|
1259
|
+
|
|
1260
|
+
Returns:
|
|
1261
|
+
JSON string with confirmation
|
|
1262
|
+
"""
|
|
1263
|
+
try:
|
|
1264
|
+
client = self._get_client()
|
|
1265
|
+
response = client.conversations_setPurpose(
|
|
1266
|
+
channel=channel,
|
|
1267
|
+
purpose=description
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
result = {
|
|
1271
|
+
"success": True,
|
|
1272
|
+
"channel": response.get("channel"),
|
|
1273
|
+
"purpose": response.get("purpose")
|
|
1274
|
+
}
|
|
1275
|
+
return json.dumps(result, indent=2)
|
|
1276
|
+
|
|
1277
|
+
except Exception as e:
|
|
1278
|
+
logger.error(f"Error setting channel description: {e}")
|
|
1279
|
+
return json.dumps({
|
|
1280
|
+
"success": False,
|
|
1281
|
+
"error": str(e)
|
|
1282
|
+
})
|