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.
Files changed (479) hide show
  1. control_plane_api/LICENSE +676 -0
  2. control_plane_api/README.md +350 -0
  3. control_plane_api/__init__.py +4 -0
  4. control_plane_api/__version__.py +8 -0
  5. control_plane_api/alembic/README +1 -0
  6. control_plane_api/alembic/env.py +121 -0
  7. control_plane_api/alembic/script.py.mako +28 -0
  8. control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
  9. control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
  10. control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
  11. control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
  12. control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
  13. control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
  14. control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
  15. control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
  16. control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
  17. control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
  18. control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
  19. control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
  20. control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
  21. control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
  22. control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
  23. control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
  24. control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
  25. control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -0
  26. control_plane_api/alembic.ini +148 -0
  27. control_plane_api/api/index.py +12 -0
  28. control_plane_api/app/__init__.py +11 -0
  29. control_plane_api/app/activities/__init__.py +20 -0
  30. control_plane_api/app/activities/agent_activities.py +384 -0
  31. control_plane_api/app/activities/plan_generation_activities.py +499 -0
  32. control_plane_api/app/activities/team_activities.py +424 -0
  33. control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
  34. control_plane_api/app/config/__init__.py +35 -0
  35. control_plane_api/app/config/api_config.py +469 -0
  36. control_plane_api/app/config/config_loader.py +224 -0
  37. control_plane_api/app/config/model_pricing.py +323 -0
  38. control_plane_api/app/config/storage_config.py +159 -0
  39. control_plane_api/app/config.py +115 -0
  40. control_plane_api/app/controllers/__init__.py +0 -0
  41. control_plane_api/app/controllers/execution_environment_controller.py +1315 -0
  42. control_plane_api/app/database.py +135 -0
  43. control_plane_api/app/exceptions.py +408 -0
  44. control_plane_api/app/lib/__init__.py +11 -0
  45. control_plane_api/app/lib/environment.py +65 -0
  46. control_plane_api/app/lib/event_bus/__init__.py +17 -0
  47. control_plane_api/app/lib/event_bus/base.py +136 -0
  48. control_plane_api/app/lib/event_bus/manager.py +335 -0
  49. control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
  50. control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
  51. control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
  52. control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
  53. control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
  54. control_plane_api/app/lib/job_executor.py +330 -0
  55. control_plane_api/app/lib/kubiya_client.py +293 -0
  56. control_plane_api/app/lib/litellm_pricing.py +166 -0
  57. control_plane_api/app/lib/mcp_validation.py +163 -0
  58. control_plane_api/app/lib/nats/__init__.py +13 -0
  59. control_plane_api/app/lib/nats/credentials_manager.py +288 -0
  60. control_plane_api/app/lib/nats/listener.py +374 -0
  61. control_plane_api/app/lib/planning_prompt_builder.py +153 -0
  62. control_plane_api/app/lib/planning_tools/__init__.py +41 -0
  63. control_plane_api/app/lib/planning_tools/agents.py +409 -0
  64. control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
  65. control_plane_api/app/lib/planning_tools/base.py +119 -0
  66. control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
  67. control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
  68. control_plane_api/app/lib/planning_tools/environments.py +218 -0
  69. control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
  70. control_plane_api/app/lib/planning_tools/models.py +93 -0
  71. control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
  72. control_plane_api/app/lib/planning_tools/resources.py +242 -0
  73. control_plane_api/app/lib/planning_tools/teams.py +334 -0
  74. control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
  75. control_plane_api/app/lib/redis_client.py +803 -0
  76. control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
  77. control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
  78. control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
  79. control_plane_api/app/lib/storage/__init__.py +20 -0
  80. control_plane_api/app/lib/storage/base_provider.py +274 -0
  81. control_plane_api/app/lib/storage/provider_factory.py +157 -0
  82. control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
  83. control_plane_api/app/lib/supabase.py +71 -0
  84. control_plane_api/app/lib/supabase_utils.py +138 -0
  85. control_plane_api/app/lib/task_planning/__init__.py +138 -0
  86. control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
  87. control_plane_api/app/lib/task_planning/agents.py +389 -0
  88. control_plane_api/app/lib/task_planning/cache.py +218 -0
  89. control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
  90. control_plane_api/app/lib/task_planning/helpers.py +293 -0
  91. control_plane_api/app/lib/task_planning/hooks.py +474 -0
  92. control_plane_api/app/lib/task_planning/models.py +503 -0
  93. control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
  94. control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
  95. control_plane_api/app/lib/task_planning/runner.py +656 -0
  96. control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
  97. control_plane_api/app/lib/task_planning/workflow.py +424 -0
  98. control_plane_api/app/lib/templating/__init__.py +88 -0
  99. control_plane_api/app/lib/templating/compiler.py +278 -0
  100. control_plane_api/app/lib/templating/engine.py +178 -0
  101. control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
  102. control_plane_api/app/lib/templating/parsers/base.py +96 -0
  103. control_plane_api/app/lib/templating/parsers/env.py +85 -0
  104. control_plane_api/app/lib/templating/parsers/graph.py +112 -0
  105. control_plane_api/app/lib/templating/parsers/secret.py +87 -0
  106. control_plane_api/app/lib/templating/parsers/simple.py +81 -0
  107. control_plane_api/app/lib/templating/resolver.py +366 -0
  108. control_plane_api/app/lib/templating/types.py +214 -0
  109. control_plane_api/app/lib/templating/validator.py +201 -0
  110. control_plane_api/app/lib/temporal_client.py +232 -0
  111. control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
  112. control_plane_api/app/lib/temporal_credentials_service.py +203 -0
  113. control_plane_api/app/lib/validation/__init__.py +24 -0
  114. control_plane_api/app/lib/validation/runtime_validation.py +388 -0
  115. control_plane_api/app/main.py +531 -0
  116. control_plane_api/app/middleware/__init__.py +10 -0
  117. control_plane_api/app/middleware/auth.py +645 -0
  118. control_plane_api/app/middleware/exception_handler.py +267 -0
  119. control_plane_api/app/middleware/prometheus_middleware.py +173 -0
  120. control_plane_api/app/middleware/rate_limiting.py +384 -0
  121. control_plane_api/app/middleware/request_id.py +202 -0
  122. control_plane_api/app/models/__init__.py +40 -0
  123. control_plane_api/app/models/agent.py +90 -0
  124. control_plane_api/app/models/analytics.py +206 -0
  125. control_plane_api/app/models/associations.py +107 -0
  126. control_plane_api/app/models/auth_user.py +73 -0
  127. control_plane_api/app/models/context.py +161 -0
  128. control_plane_api/app/models/custom_integration.py +99 -0
  129. control_plane_api/app/models/environment.py +64 -0
  130. control_plane_api/app/models/execution.py +125 -0
  131. control_plane_api/app/models/execution_transition.py +50 -0
  132. control_plane_api/app/models/job.py +159 -0
  133. control_plane_api/app/models/llm_model.py +78 -0
  134. control_plane_api/app/models/orchestration.py +66 -0
  135. control_plane_api/app/models/plan_execution.py +102 -0
  136. control_plane_api/app/models/presence.py +49 -0
  137. control_plane_api/app/models/project.py +61 -0
  138. control_plane_api/app/models/project_management.py +85 -0
  139. control_plane_api/app/models/session.py +29 -0
  140. control_plane_api/app/models/skill.py +155 -0
  141. control_plane_api/app/models/system_tables.py +43 -0
  142. control_plane_api/app/models/task_planning.py +372 -0
  143. control_plane_api/app/models/team.py +86 -0
  144. control_plane_api/app/models/trace.py +257 -0
  145. control_plane_api/app/models/user_profile.py +54 -0
  146. control_plane_api/app/models/worker.py +221 -0
  147. control_plane_api/app/models/workflow.py +161 -0
  148. control_plane_api/app/models/workspace.py +50 -0
  149. control_plane_api/app/observability/__init__.py +177 -0
  150. control_plane_api/app/observability/context_logging.py +475 -0
  151. control_plane_api/app/observability/decorators.py +337 -0
  152. control_plane_api/app/observability/local_span_processor.py +702 -0
  153. control_plane_api/app/observability/metrics.py +303 -0
  154. control_plane_api/app/observability/middleware.py +246 -0
  155. control_plane_api/app/observability/optional.py +115 -0
  156. control_plane_api/app/observability/tracing.py +382 -0
  157. control_plane_api/app/policies/README.md +149 -0
  158. control_plane_api/app/policies/approved_users.rego +62 -0
  159. control_plane_api/app/policies/business_hours.rego +51 -0
  160. control_plane_api/app/policies/rate_limiting.rego +100 -0
  161. control_plane_api/app/policies/tool_enforcement/README.md +336 -0
  162. control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
  163. control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
  164. control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
  165. control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
  166. control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -0
  167. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  168. control_plane_api/app/routers/__init__.py +4 -0
  169. control_plane_api/app/routers/agents.py +382 -0
  170. control_plane_api/app/routers/agents_v2.py +1598 -0
  171. control_plane_api/app/routers/analytics.py +1310 -0
  172. control_plane_api/app/routers/auth.py +59 -0
  173. control_plane_api/app/routers/client_config.py +57 -0
  174. control_plane_api/app/routers/context_graph.py +561 -0
  175. control_plane_api/app/routers/context_manager.py +577 -0
  176. control_plane_api/app/routers/custom_integrations.py +490 -0
  177. control_plane_api/app/routers/enforcer.py +132 -0
  178. control_plane_api/app/routers/environment_context.py +252 -0
  179. control_plane_api/app/routers/environments.py +761 -0
  180. control_plane_api/app/routers/execution_environment.py +847 -0
  181. control_plane_api/app/routers/executions/__init__.py +28 -0
  182. control_plane_api/app/routers/executions/router.py +286 -0
  183. control_plane_api/app/routers/executions/services/__init__.py +22 -0
  184. control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
  185. control_plane_api/app/routers/executions/services/status_service.py +420 -0
  186. control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
  187. control_plane_api/app/routers/executions/services/worker_health.py +514 -0
  188. control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
  189. control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
  190. control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
  191. control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
  192. control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
  193. control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
  194. control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
  195. control_plane_api/app/routers/executions.py +4888 -0
  196. control_plane_api/app/routers/health.py +165 -0
  197. control_plane_api/app/routers/health_v2.py +394 -0
  198. control_plane_api/app/routers/integration_templates.py +496 -0
  199. control_plane_api/app/routers/integrations.py +287 -0
  200. control_plane_api/app/routers/jobs.py +1809 -0
  201. control_plane_api/app/routers/metrics.py +517 -0
  202. control_plane_api/app/routers/models.py +82 -0
  203. control_plane_api/app/routers/models_v2.py +628 -0
  204. control_plane_api/app/routers/plan_executions.py +1481 -0
  205. control_plane_api/app/routers/plan_generation_async.py +304 -0
  206. control_plane_api/app/routers/policies.py +669 -0
  207. control_plane_api/app/routers/presence.py +234 -0
  208. control_plane_api/app/routers/projects.py +987 -0
  209. control_plane_api/app/routers/runners.py +379 -0
  210. control_plane_api/app/routers/runtimes.py +172 -0
  211. control_plane_api/app/routers/secrets.py +171 -0
  212. control_plane_api/app/routers/skills.py +1010 -0
  213. control_plane_api/app/routers/skills_definitions.py +140 -0
  214. control_plane_api/app/routers/storage.py +456 -0
  215. control_plane_api/app/routers/task_planning.py +611 -0
  216. control_plane_api/app/routers/task_queues.py +650 -0
  217. control_plane_api/app/routers/team_context.py +274 -0
  218. control_plane_api/app/routers/teams.py +1747 -0
  219. control_plane_api/app/routers/templates.py +248 -0
  220. control_plane_api/app/routers/traces.py +571 -0
  221. control_plane_api/app/routers/websocket_client.py +479 -0
  222. control_plane_api/app/routers/websocket_executions_status.py +437 -0
  223. control_plane_api/app/routers/websocket_gateway.py +323 -0
  224. control_plane_api/app/routers/websocket_traces.py +576 -0
  225. control_plane_api/app/routers/worker_queues.py +2555 -0
  226. control_plane_api/app/routers/worker_websocket.py +419 -0
  227. control_plane_api/app/routers/workers.py +1004 -0
  228. control_plane_api/app/routers/workflows.py +204 -0
  229. control_plane_api/app/runtimes/__init__.py +6 -0
  230. control_plane_api/app/runtimes/validation.py +344 -0
  231. control_plane_api/app/schemas/__init__.py +1 -0
  232. control_plane_api/app/schemas/job_schemas.py +302 -0
  233. control_plane_api/app/schemas/mcp_schemas.py +311 -0
  234. control_plane_api/app/schemas/template_schemas.py +133 -0
  235. control_plane_api/app/schemas/trace_schemas.py +168 -0
  236. control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
  237. control_plane_api/app/services/__init__.py +1 -0
  238. control_plane_api/app/services/agno_planning_strategy.py +233 -0
  239. control_plane_api/app/services/agno_service.py +838 -0
  240. control_plane_api/app/services/claude_code_planning_service.py +203 -0
  241. control_plane_api/app/services/context_graph_client.py +224 -0
  242. control_plane_api/app/services/custom_integration_service.py +415 -0
  243. control_plane_api/app/services/integration_resolution_service.py +345 -0
  244. control_plane_api/app/services/litellm_service.py +394 -0
  245. control_plane_api/app/services/plan_generator.py +79 -0
  246. control_plane_api/app/services/planning_strategy.py +66 -0
  247. control_plane_api/app/services/planning_strategy_factory.py +118 -0
  248. control_plane_api/app/services/policy_service.py +615 -0
  249. control_plane_api/app/services/state_transition_service.py +755 -0
  250. control_plane_api/app/services/storage_service.py +593 -0
  251. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  252. control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
  253. control_plane_api/app/services/trace_retention.py +354 -0
  254. control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
  255. control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
  256. control_plane_api/app/services/workflow_operations_service.py +611 -0
  257. control_plane_api/app/skills/__init__.py +100 -0
  258. control_plane_api/app/skills/base.py +239 -0
  259. control_plane_api/app/skills/builtin/__init__.py +37 -0
  260. control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
  261. control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
  262. control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
  263. control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
  264. control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
  265. control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
  266. control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
  267. control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
  268. control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
  269. control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
  270. control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
  271. control_plane_api/app/skills/builtin/docker/skill.py +104 -0
  272. control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
  273. control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
  274. control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
  275. control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
  276. control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
  277. control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
  278. control_plane_api/app/skills/builtin/python/__init__.py +4 -0
  279. control_plane_api/app/skills/builtin/python/skill.py +92 -0
  280. control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
  281. control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
  282. control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
  283. control_plane_api/app/skills/builtin/shell/skill.py +161 -0
  284. control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
  285. control_plane_api/app/skills/builtin/slack/skill.py +302 -0
  286. control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
  287. control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
  288. control_plane_api/app/skills/business_intelligence.py +189 -0
  289. control_plane_api/app/skills/config.py +63 -0
  290. control_plane_api/app/skills/loaders/__init__.py +14 -0
  291. control_plane_api/app/skills/loaders/base.py +73 -0
  292. control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
  293. control_plane_api/app/skills/registry.py +125 -0
  294. control_plane_api/app/utils/helpers.py +12 -0
  295. control_plane_api/app/utils/workflow_executor.py +354 -0
  296. control_plane_api/app/workflows/__init__.py +11 -0
  297. control_plane_api/app/workflows/agent_execution.py +520 -0
  298. control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
  299. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  300. control_plane_api/app/workflows/plan_generation.py +254 -0
  301. control_plane_api/app/workflows/team_execution.py +442 -0
  302. control_plane_api/scripts/seed_models.py +240 -0
  303. control_plane_api/scripts/validate_existing_tool_names.py +492 -0
  304. control_plane_api/shared/__init__.py +8 -0
  305. control_plane_api/shared/version.py +17 -0
  306. control_plane_api/test_deduplication.py +274 -0
  307. control_plane_api/test_executor_deduplication_e2e.py +309 -0
  308. control_plane_api/test_job_execution_e2e.py +283 -0
  309. control_plane_api/test_real_integration.py +193 -0
  310. control_plane_api/version.py +38 -0
  311. control_plane_api/worker/__init__.py +0 -0
  312. control_plane_api/worker/activities/__init__.py +0 -0
  313. control_plane_api/worker/activities/agent_activities.py +1585 -0
  314. control_plane_api/worker/activities/approval_activities.py +234 -0
  315. control_plane_api/worker/activities/job_activities.py +199 -0
  316. control_plane_api/worker/activities/runtime_activities.py +1167 -0
  317. control_plane_api/worker/activities/skill_activities.py +282 -0
  318. control_plane_api/worker/activities/team_activities.py +479 -0
  319. control_plane_api/worker/agent_runtime_server.py +370 -0
  320. control_plane_api/worker/binary_manager.py +333 -0
  321. control_plane_api/worker/config/__init__.py +31 -0
  322. control_plane_api/worker/config/worker_config.py +273 -0
  323. control_plane_api/worker/control_plane_client.py +1491 -0
  324. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  325. control_plane_api/worker/health_monitor.py +159 -0
  326. control_plane_api/worker/metrics.py +237 -0
  327. control_plane_api/worker/models/__init__.py +1 -0
  328. control_plane_api/worker/models/error_events.py +105 -0
  329. control_plane_api/worker/models/inputs.py +89 -0
  330. control_plane_api/worker/runtimes/__init__.py +35 -0
  331. control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
  332. control_plane_api/worker/runtimes/agno/__init__.py +34 -0
  333. control_plane_api/worker/runtimes/agno/config.py +248 -0
  334. control_plane_api/worker/runtimes/agno/hooks.py +385 -0
  335. control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
  336. control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
  337. control_plane_api/worker/runtimes/agno/utils.py +163 -0
  338. control_plane_api/worker/runtimes/base.py +979 -0
  339. control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
  340. control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
  341. control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
  342. control_plane_api/worker/runtimes/claude_code/config.py +829 -0
  343. control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
  344. control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
  345. control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
  346. control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
  347. control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
  348. control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
  349. control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
  350. control_plane_api/worker/runtimes/factory.py +173 -0
  351. control_plane_api/worker/runtimes/model_utils.py +107 -0
  352. control_plane_api/worker/runtimes/validation.py +93 -0
  353. control_plane_api/worker/services/__init__.py +1 -0
  354. control_plane_api/worker/services/agent_communication_tools.py +908 -0
  355. control_plane_api/worker/services/agent_executor.py +485 -0
  356. control_plane_api/worker/services/agent_executor_v2.py +793 -0
  357. control_plane_api/worker/services/analytics_collector.py +457 -0
  358. control_plane_api/worker/services/analytics_service.py +464 -0
  359. control_plane_api/worker/services/approval_tools.py +310 -0
  360. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  361. control_plane_api/worker/services/cancellation_manager.py +177 -0
  362. control_plane_api/worker/services/code_ingestion_tools.py +465 -0
  363. control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
  364. control_plane_api/worker/services/data_visualization.py +834 -0
  365. control_plane_api/worker/services/event_publisher.py +531 -0
  366. control_plane_api/worker/services/jira_tools.py +257 -0
  367. control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
  368. control_plane_api/worker/services/runtime_analytics.py +328 -0
  369. control_plane_api/worker/services/session_service.py +365 -0
  370. control_plane_api/worker/services/skill_context_enhancement.py +181 -0
  371. control_plane_api/worker/services/skill_factory.py +471 -0
  372. control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
  373. control_plane_api/worker/services/team_executor.py +715 -0
  374. control_plane_api/worker/services/team_executor_v2.py +1866 -0
  375. control_plane_api/worker/services/tool_enforcement.py +254 -0
  376. control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
  377. control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
  378. control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
  379. control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
  380. control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
  381. control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
  382. control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
  383. control_plane_api/worker/services/workflow_executor/models.py +142 -0
  384. control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
  385. control_plane_api/worker/skills/__init__.py +12 -0
  386. control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
  387. control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
  388. control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
  389. control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
  390. control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
  391. control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
  392. control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
  393. control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
  394. control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
  395. control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
  396. control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
  397. control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
  398. control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
  399. control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
  400. control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
  401. control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
  402. control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
  403. control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
  404. control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
  405. control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
  406. control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
  407. control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
  408. control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
  409. control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
  410. control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
  411. control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
  412. control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
  413. control_plane_api/worker/skills/loaders/__init__.py +5 -0
  414. control_plane_api/worker/skills/loaders/base.py +23 -0
  415. control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
  416. control_plane_api/worker/skills/registry.py +208 -0
  417. control_plane_api/worker/tests/__init__.py +1 -0
  418. control_plane_api/worker/tests/conftest.py +12 -0
  419. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  420. control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
  421. control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
  422. control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
  423. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  424. control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
  425. control_plane_api/worker/tests/integration/__init__.py +0 -0
  426. control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
  427. control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
  428. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  429. control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
  430. control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
  431. control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
  432. control_plane_api/worker/tests/unit/__init__.py +0 -0
  433. control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
  434. control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
  435. control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
  436. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  437. control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
  438. control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
  439. control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
  440. control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
  441. control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
  442. control_plane_api/worker/utils/__init__.py +1 -0
  443. control_plane_api/worker/utils/chunk_batcher.py +330 -0
  444. control_plane_api/worker/utils/environment.py +65 -0
  445. control_plane_api/worker/utils/error_publisher.py +260 -0
  446. control_plane_api/worker/utils/event_batcher.py +256 -0
  447. control_plane_api/worker/utils/logging_config.py +335 -0
  448. control_plane_api/worker/utils/logging_helper.py +326 -0
  449. control_plane_api/worker/utils/parameter_validator.py +120 -0
  450. control_plane_api/worker/utils/retry_utils.py +60 -0
  451. control_plane_api/worker/utils/streaming_utils.py +665 -0
  452. control_plane_api/worker/utils/tool_validation.py +332 -0
  453. control_plane_api/worker/utils/workspace_manager.py +163 -0
  454. control_plane_api/worker/websocket_client.py +393 -0
  455. control_plane_api/worker/worker.py +1297 -0
  456. control_plane_api/worker/workflows/__init__.py +0 -0
  457. control_plane_api/worker/workflows/agent_execution.py +909 -0
  458. control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
  459. control_plane_api/worker/workflows/team_execution.py +611 -0
  460. kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
  461. kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
  462. kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
  463. kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
  464. kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
  465. kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
  466. scripts/__init__.py +1 -0
  467. scripts/migrations.py +39 -0
  468. scripts/seed_worker_queues.py +128 -0
  469. scripts/setup_agent_runtime.py +142 -0
  470. worker_internal/__init__.py +1 -0
  471. worker_internal/planner/__init__.py +1 -0
  472. worker_internal/planner/activities.py +1499 -0
  473. worker_internal/planner/agent_tools.py +197 -0
  474. worker_internal/planner/event_models.py +148 -0
  475. worker_internal/planner/event_publisher.py +67 -0
  476. worker_internal/planner/models.py +199 -0
  477. worker_internal/planner/retry_logic.py +134 -0
  478. worker_internal/planner/worker.py +300 -0
  479. worker_internal/planner/workflows.py +970 -0
@@ -0,0 +1,803 @@
1
+ """Redis client for caching authentication tokens and user data.
2
+
3
+ Supports two modes:
4
+ 1. Upstash REST API (serverless-friendly) - uses KV_REST_API_URL/TOKEN or UPSTASH_* env vars
5
+ 2. Standard Redis (TCP connection) - uses REDIS_URL env var (e.g., redis://localhost:6379)
6
+ """
7
+
8
+ import os
9
+ import json
10
+ from typing import Optional, Any
11
+ import httpx
12
+ import structlog
13
+
14
+ logger = structlog.get_logger()
15
+
16
+ # Redis configuration cache
17
+ _redis_client: Optional[Any] = None
18
+ _redis_client_type: Optional[str] = None # "upstash" or "standard"
19
+
20
+
21
+ class UpstashRedisClient:
22
+ """Upstash Redis client using direct HTTP REST API calls (serverless-friendly)."""
23
+
24
+ def __init__(self, url: str, token: str):
25
+ self.url = url.rstrip('/')
26
+ self.token = token
27
+ self.headers = {
28
+ "Authorization": f"Bearer {token}",
29
+ "Content-Type": "application/json"
30
+ }
31
+ # Use a shared async client for connection reuse and better performance
32
+ # This avoids creating a new connection for every Redis operation
33
+ self._client: Optional[httpx.AsyncClient] = None
34
+
35
+ def _get_client(self) -> httpx.AsyncClient:
36
+ """Get or create the shared HTTP client."""
37
+ if self._client is None or self._client.is_closed:
38
+ self._client = httpx.AsyncClient(
39
+ timeout=httpx.Timeout(5.0, connect=2.0),
40
+ limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
41
+ )
42
+ return self._client
43
+
44
+ async def close(self) -> None:
45
+ """Close the HTTP client."""
46
+ if self._client is not None and not self._client.is_closed:
47
+ await self._client.aclose()
48
+ self._client = None
49
+
50
+ async def get(self, key: str) -> Optional[str]:
51
+ """Get value from Redis."""
52
+ try:
53
+ client = self._get_client()
54
+ response = await client.post(
55
+ f"{self.url}/get/{key}",
56
+ headers=self.headers
57
+ )
58
+
59
+ if response.status_code == 200:
60
+ result = response.json()
61
+ return result.get("result")
62
+
63
+ logger.warning("redis_get_failed", status=response.status_code, key=key[:20])
64
+ return None
65
+
66
+ except Exception as e:
67
+ logger.warning("redis_get_error", error=str(e), key=key[:20])
68
+ return None
69
+
70
+ async def mget(self, keys: list[str]) -> dict[str, Optional[str]]:
71
+ """
72
+ Get multiple values from Redis in a single request using pipeline.
73
+
74
+ Args:
75
+ keys: List of Redis keys to fetch
76
+
77
+ Returns:
78
+ Dict mapping keys to their values (None if key doesn't exist)
79
+ """
80
+ if not keys:
81
+ return {}
82
+
83
+ try:
84
+ # Build pipeline commands for MGET
85
+ commands = [["GET", key] for key in keys]
86
+
87
+ client = self._get_client()
88
+ response = await client.post(
89
+ f"{self.url}/pipeline",
90
+ headers=self.headers,
91
+ json=commands
92
+ )
93
+
94
+ if response.status_code == 200:
95
+ results = response.json()
96
+ # Map keys to their results
97
+ return {
98
+ key: results[i].get("result") if i < len(results) else None
99
+ for i, key in enumerate(keys)
100
+ }
101
+
102
+ logger.warning("redis_mget_failed", status=response.status_code, key_count=len(keys))
103
+ return {key: None for key in keys}
104
+
105
+ except Exception as e:
106
+ logger.warning("redis_mget_error", error=str(e), key_count=len(keys))
107
+ return {key: None for key in keys}
108
+
109
+ async def set(self, key: str, value: str, ex: Optional[int] = None) -> bool:
110
+ """Set value in Redis with optional expiry (seconds)."""
111
+ try:
112
+ # Build command
113
+ if ex:
114
+ command = ["SET", key, value, "EX", str(ex)]
115
+ else:
116
+ command = ["SET", key, value]
117
+
118
+ client = self._get_client()
119
+ response = await client.post(
120
+ f"{self.url}/pipeline",
121
+ headers=self.headers,
122
+ json=[command]
123
+ )
124
+
125
+ if response.status_code == 200:
126
+ return True
127
+
128
+ logger.warning("redis_set_failed", status=response.status_code, key=key[:20])
129
+ return False
130
+
131
+ except Exception as e:
132
+ logger.warning("redis_set_error", error=str(e), key=key[:20])
133
+ return False
134
+
135
+ async def setex(self, key: str, seconds: int, value: str) -> bool:
136
+ """Set value in Redis with expiry (seconds). Alias for set with ex parameter."""
137
+ return await self.set(key, value, ex=seconds)
138
+
139
+ async def delete(self, key: str) -> bool:
140
+ """Delete a key from Redis."""
141
+ try:
142
+ command = ["DEL", key]
143
+
144
+ client = self._get_client()
145
+ response = await client.post(
146
+ f"{self.url}/pipeline",
147
+ headers=self.headers,
148
+ json=[command]
149
+ )
150
+
151
+ if response.status_code == 200:
152
+ return True
153
+
154
+ logger.warning("redis_delete_failed", status=response.status_code, key=key[:20])
155
+ return False
156
+
157
+ except Exception as e:
158
+ logger.warning("redis_delete_error", error=str(e), key=key[:20])
159
+ return False
160
+
161
+ async def hset(self, key: str, mapping: dict) -> bool:
162
+ """Set hash fields in Redis."""
163
+ try:
164
+ # Convert dict to list of field-value pairs
165
+ fields = []
166
+ for k, v in mapping.items():
167
+ fields.extend([k, str(v)])
168
+
169
+ command = ["HSET", key] + fields
170
+
171
+ client = self._get_client()
172
+ response = await client.post(
173
+ f"{self.url}/pipeline",
174
+ headers=self.headers,
175
+ json=[command]
176
+ )
177
+
178
+ if response.status_code == 200:
179
+ return True
180
+
181
+ logger.warning("redis_hset_failed", status=response.status_code, key=key[:20])
182
+ return False
183
+
184
+ except Exception as e:
185
+ logger.warning("redis_hset_error", error=str(e), key=key[:20])
186
+ return False
187
+
188
+ async def hgetall(self, key: str) -> Optional[dict]:
189
+ """Get all hash fields from Redis."""
190
+ try:
191
+ client = self._get_client()
192
+ response = await client.post(
193
+ f"{self.url}/pipeline",
194
+ headers=self.headers,
195
+ json=[["HGETALL", key]]
196
+ )
197
+
198
+ if response.status_code == 200:
199
+ result = response.json()
200
+ if result and isinstance(result, list) and len(result) > 0:
201
+ data = result[0].get("result", [])
202
+ # Convert list to dict [k1, v1, k2, v2] -> {k1: v1, k2: v2}
203
+ return {data[i]: data[i+1] for i in range(0, len(data), 2)} if data else {}
204
+
205
+ return None
206
+
207
+ except Exception as e:
208
+ logger.warning("redis_hgetall_error", error=str(e), key=key[:20])
209
+ return None
210
+
211
+ async def expire(self, key: str, seconds: int) -> bool:
212
+ """Set expiry on a key."""
213
+ try:
214
+ client = self._get_client()
215
+ response = await client.post(
216
+ f"{self.url}/pipeline",
217
+ headers=self.headers,
218
+ json=[["EXPIRE", key, str(seconds)]]
219
+ )
220
+
221
+ return response.status_code == 200
222
+
223
+ except Exception as e:
224
+ logger.warning("redis_expire_error", error=str(e), key=key[:20])
225
+ return False
226
+
227
+ async def sadd(self, key: str, *members: str) -> bool:
228
+ """Add members to a set."""
229
+ try:
230
+ command = ["SADD", key] + list(members)
231
+
232
+ client = self._get_client()
233
+ response = await client.post(
234
+ f"{self.url}/pipeline",
235
+ headers=self.headers,
236
+ json=[command]
237
+ )
238
+
239
+ return response.status_code == 200
240
+
241
+ except Exception as e:
242
+ logger.warning("redis_sadd_error", error=str(e), key=key[:20])
243
+ return False
244
+
245
+ async def scard(self, key: str) -> int:
246
+ """Get count of set members."""
247
+ try:
248
+ client = self._get_client()
249
+ response = await client.post(
250
+ f"{self.url}/pipeline",
251
+ headers=self.headers,
252
+ json=[["SCARD", key]]
253
+ )
254
+
255
+ if response.status_code == 200:
256
+ result = response.json()
257
+ if result and isinstance(result, list):
258
+ return result[0].get("result", 0)
259
+
260
+ return 0
261
+
262
+ except Exception as e:
263
+ logger.warning("redis_scard_error", error=str(e), key=key[:20])
264
+ return 0
265
+
266
+ async def lpush(self, key: str, *values: str) -> bool:
267
+ """Push values to start of list."""
268
+ try:
269
+ command = ["LPUSH", key] + list(values)
270
+
271
+ client = self._get_client()
272
+ response = await client.post(
273
+ f"{self.url}/pipeline",
274
+ headers=self.headers,
275
+ json=[command]
276
+ )
277
+
278
+ return response.status_code == 200
279
+
280
+ except Exception as e:
281
+ logger.warning("redis_lpush_error", error=str(e), key=key[:20])
282
+ return False
283
+
284
+ async def ltrim(self, key: str, start: int, stop: int) -> bool:
285
+ """Trim list to specified range."""
286
+ try:
287
+ client = self._get_client()
288
+ response = await client.post(
289
+ f"{self.url}/pipeline",
290
+ headers=self.headers,
291
+ json=[["LTRIM", key, str(start), str(stop)]]
292
+ )
293
+
294
+ return response.status_code == 200
295
+
296
+ except Exception as e:
297
+ logger.warning("redis_ltrim_error", error=str(e), key=key[:20])
298
+ return False
299
+
300
+ async def lrange(self, key: str, start: int, stop: int) -> list:
301
+ """Get range of list elements."""
302
+ try:
303
+ async with httpx.AsyncClient(timeout=5.0) as client:
304
+ response = await client.post(
305
+ f"{self.url}/pipeline",
306
+ headers=self.headers,
307
+ json=[["LRANGE", key, str(start), str(stop)]]
308
+ )
309
+
310
+ if response.status_code == 200:
311
+ result = response.json()
312
+ if result and isinstance(result, list):
313
+ return result[0].get("result", [])
314
+
315
+ return []
316
+
317
+ except Exception as e:
318
+ logger.warning("redis_lrange_error", error=str(e), key=key[:20])
319
+ return []
320
+
321
+ async def llen(self, key: str) -> int:
322
+ """Get length of list."""
323
+ try:
324
+ async with httpx.AsyncClient(timeout=5.0) as client:
325
+ response = await client.post(
326
+ f"{self.url}/pipeline",
327
+ headers=self.headers,
328
+ json=[["LLEN", key]]
329
+ )
330
+
331
+ if response.status_code == 200:
332
+ result = response.json()
333
+ if result and isinstance(result, list):
334
+ return result[0].get("result", 0)
335
+
336
+ return 0
337
+
338
+ except Exception as e:
339
+ logger.warning("redis_llen_error", error=str(e), key=key[:20])
340
+ return 0
341
+
342
+ async def publish(self, channel: str, message: str) -> bool:
343
+ """Publish message to Redis pub/sub channel."""
344
+ try:
345
+ async with httpx.AsyncClient(timeout=5.0) as client:
346
+ response = await client.post(
347
+ f"{self.url}/pipeline",
348
+ headers=self.headers,
349
+ json=[["PUBLISH", channel, message]]
350
+ )
351
+
352
+ if response.status_code == 200:
353
+ return True
354
+
355
+ logger.warning("redis_publish_failed", status=response.status_code, channel=channel[:20])
356
+ return False
357
+
358
+ except Exception as e:
359
+ logger.warning("redis_publish_error", error=str(e), channel=channel[:20])
360
+ return False
361
+
362
+ async def ttl(self, key: str) -> int:
363
+ """Get TTL of a key in seconds."""
364
+ try:
365
+ async with httpx.AsyncClient(timeout=5.0) as client:
366
+ response = await client.post(
367
+ f"{self.url}/pipeline",
368
+ headers=self.headers,
369
+ json=[["TTL", key]]
370
+ )
371
+
372
+ if response.status_code == 200:
373
+ result = response.json()
374
+ if result and isinstance(result, list):
375
+ return result[0].get("result", -2)
376
+
377
+ return -2
378
+
379
+ except Exception as e:
380
+ logger.warning("redis_ttl_error", error=str(e), key=key[:20])
381
+ return -2
382
+
383
+ async def ping(self) -> bool:
384
+ """Ping Redis to check connection."""
385
+ try:
386
+ async with httpx.AsyncClient(timeout=5.0) as client:
387
+ response = await client.post(
388
+ f"{self.url}/pipeline",
389
+ headers=self.headers,
390
+ json=[["PING"]]
391
+ )
392
+
393
+ if response.status_code == 200:
394
+ result = response.json()
395
+ if result and isinstance(result, list):
396
+ return result[0].get("result") == "PONG"
397
+
398
+ return False
399
+
400
+ except Exception as e:
401
+ logger.warning("redis_ping_error", error=str(e))
402
+ return False
403
+
404
+
405
+ class StandardRedisClient:
406
+ """Standard Redis client using redis-py with async support.
407
+
408
+ This client provides the same interface as UpstashRedisClient but uses
409
+ standard Redis protocol (TCP connection) instead of REST API.
410
+ """
411
+
412
+ def __init__(self, url: str):
413
+ """Initialize standard Redis client.
414
+
415
+ Args:
416
+ url: Redis URL (e.g., redis://localhost:6379, redis://:password@host:port/db)
417
+ """
418
+ try:
419
+ import redis.asyncio as aioredis
420
+ except ImportError:
421
+ raise ImportError(
422
+ "redis package is required for standard Redis connections. "
423
+ "Install it with: pip install redis"
424
+ )
425
+
426
+ self.url = url
427
+ self._redis = aioredis.from_url(
428
+ url,
429
+ encoding="utf-8",
430
+ decode_responses=True,
431
+ socket_timeout=5.0,
432
+ socket_connect_timeout=5.0,
433
+ )
434
+
435
+ async def get(self, key: str) -> Optional[str]:
436
+ """Get value from Redis."""
437
+ try:
438
+ return await self._redis.get(key)
439
+ except Exception as e:
440
+ logger.warning("redis_get_error", error=str(e), key=key[:20])
441
+ return None
442
+
443
+ async def mget(self, keys: list[str]) -> dict[str, Optional[str]]:
444
+ """Get multiple values from Redis."""
445
+ if not keys:
446
+ return {}
447
+
448
+ try:
449
+ values = await self._redis.mget(keys)
450
+ return {key: value for key, value in zip(keys, values)}
451
+ except Exception as e:
452
+ logger.warning("redis_mget_error", error=str(e), key_count=len(keys))
453
+ return {key: None for key in keys}
454
+
455
+ async def set(self, key: str, value: str, ex: Optional[int] = None) -> bool:
456
+ """Set value in Redis with optional expiry (seconds)."""
457
+ try:
458
+ result = await self._redis.set(key, value, ex=ex)
459
+ return bool(result)
460
+ except Exception as e:
461
+ logger.warning("redis_set_error", error=str(e), key=key[:20])
462
+ return False
463
+
464
+ async def setex(self, key: str, seconds: int, value: str) -> bool:
465
+ """Set value in Redis with expiry (seconds)."""
466
+ return await self.set(key, value, ex=seconds)
467
+
468
+ async def delete(self, key: str) -> bool:
469
+ """Delete a key from Redis."""
470
+ try:
471
+ result = await self._redis.delete(key)
472
+ return result > 0
473
+ except Exception as e:
474
+ logger.warning("redis_delete_error", error=str(e), key=key[:20])
475
+ return False
476
+
477
+ async def hset(self, key: str, mapping: dict) -> bool:
478
+ """Set hash fields in Redis."""
479
+ try:
480
+ await self._redis.hset(key, mapping=mapping)
481
+ return True
482
+ except Exception as e:
483
+ logger.warning("redis_hset_error", error=str(e), key=key[:20])
484
+ return False
485
+
486
+ async def hgetall(self, key: str) -> Optional[dict]:
487
+ """Get all hash fields from Redis."""
488
+ try:
489
+ result = await self._redis.hgetall(key)
490
+ return result if result else {}
491
+ except Exception as e:
492
+ logger.warning("redis_hgetall_error", error=str(e), key=key[:20])
493
+ return None
494
+
495
+ async def expire(self, key: str, seconds: int) -> bool:
496
+ """Set expiry on a key."""
497
+ try:
498
+ return await self._redis.expire(key, seconds)
499
+ except Exception as e:
500
+ logger.warning("redis_expire_error", error=str(e), key=key[:20])
501
+ return False
502
+
503
+ async def sadd(self, key: str, *members: str) -> bool:
504
+ """Add members to a set."""
505
+ try:
506
+ await self._redis.sadd(key, *members)
507
+ return True
508
+ except Exception as e:
509
+ logger.warning("redis_sadd_error", error=str(e), key=key[:20])
510
+ return False
511
+
512
+ async def scard(self, key: str) -> int:
513
+ """Get count of set members."""
514
+ try:
515
+ return await self._redis.scard(key) or 0
516
+ except Exception as e:
517
+ logger.warning("redis_scard_error", error=str(e), key=key[:20])
518
+ return 0
519
+
520
+ async def lpush(self, key: str, *values: str) -> bool:
521
+ """Push values to start of list."""
522
+ try:
523
+ await self._redis.lpush(key, *values)
524
+ return True
525
+ except Exception as e:
526
+ logger.warning("redis_lpush_error", error=str(e), key=key[:20])
527
+ return False
528
+
529
+ async def ltrim(self, key: str, start: int, stop: int) -> bool:
530
+ """Trim list to specified range."""
531
+ try:
532
+ await self._redis.ltrim(key, start, stop)
533
+ return True
534
+ except Exception as e:
535
+ logger.warning("redis_ltrim_error", error=str(e), key=key[:20])
536
+ return False
537
+
538
+ async def lrange(self, key: str, start: int, stop: int) -> list:
539
+ """Get range of list elements."""
540
+ try:
541
+ return await self._redis.lrange(key, start, stop) or []
542
+ except Exception as e:
543
+ logger.warning("redis_lrange_error", error=str(e), key=key[:20])
544
+ return []
545
+
546
+ async def llen(self, key: str) -> int:
547
+ """Get length of list."""
548
+ try:
549
+ return await self._redis.llen(key) or 0
550
+ except Exception as e:
551
+ logger.warning("redis_llen_error", error=str(e), key=key[:20])
552
+ return 0
553
+
554
+ async def publish(self, channel: str, message: str) -> bool:
555
+ """Publish message to Redis pub/sub channel."""
556
+ try:
557
+ await self._redis.publish(channel, message)
558
+ return True
559
+ except Exception as e:
560
+ logger.warning("redis_publish_error", error=str(e), channel=channel[:20])
561
+ return False
562
+
563
+ async def ttl(self, key: str) -> int:
564
+ """Get TTL of a key in seconds."""
565
+ try:
566
+ return await self._redis.ttl(key)
567
+ except Exception as e:
568
+ logger.warning("redis_ttl_error", error=str(e), key=key[:20])
569
+ return -2
570
+
571
+ async def ping(self) -> bool:
572
+ """Ping Redis to check connection."""
573
+ try:
574
+ return await self._redis.ping()
575
+ except Exception as e:
576
+ logger.warning("redis_ping_error", error=str(e))
577
+ return False
578
+
579
+
580
+ # Type alias for either Redis client
581
+ RedisClient = UpstashRedisClient | StandardRedisClient
582
+
583
+
584
+ def _normalize_redis_url(url: str) -> str:
585
+ """
586
+ Normalize a Redis URL to include the redis:// scheme if missing.
587
+
588
+ Handles various URL formats:
589
+ - redis://host:port -> redis://host:port (no change)
590
+ - rediss://host:port -> rediss://host:port (no change)
591
+ - host:port -> redis://host:port (add scheme)
592
+ - host:port/db -> redis://host:port/db (add scheme)
593
+ - :password@host:port -> redis://:password@host:port (add scheme)
594
+
595
+ Args:
596
+ url: Redis URL, possibly without scheme
597
+
598
+ Returns:
599
+ Normalized Redis URL with scheme
600
+ """
601
+ if not url:
602
+ return url
603
+
604
+ url = url.strip()
605
+
606
+ # Already has scheme
607
+ if url.startswith(("redis://", "rediss://")):
608
+ return url
609
+
610
+ # Check if URL looks like a Redis connection string (has host:port pattern)
611
+ # Patterns: "host:port", "host:port/db", ":password@host:port"
612
+ if ":" in url:
613
+ # Default to redis:// scheme (non-TLS)
614
+ # If the URL has a password marker at the start, add scheme correctly
615
+ if url.startswith(":"):
616
+ # Format: :password@host:port
617
+ return f"redis://{url}"
618
+ else:
619
+ # Format: host:port or host:port/db
620
+ return f"redis://{url}"
621
+
622
+ return url
623
+
624
+
625
+ def get_redis_client() -> Optional[RedisClient]:
626
+ """
627
+ Get or create Redis client.
628
+
629
+ Supports two modes (checked in order):
630
+ 1. Standard Redis URL (REDIS_URL) - e.g., redis://localhost:6379
631
+ 2. Upstash REST API (KV_REST_API_URL + KV_REST_API_TOKEN)
632
+
633
+ Returns:
634
+ Redis client instance or None if not configured
635
+ """
636
+ global _redis_client, _redis_client_type
637
+
638
+ # Return cached client if available
639
+ if _redis_client is not None:
640
+ return _redis_client
641
+
642
+ # Priority 1: Check for standard Redis URL
643
+ redis_url = os.getenv("REDIS_URL")
644
+
645
+ # Normalize URL format (add redis:// scheme if missing)
646
+ if redis_url:
647
+ original_url = redis_url
648
+ redis_url = _normalize_redis_url(redis_url)
649
+ if redis_url != original_url:
650
+ logger.info(
651
+ "redis_url_normalized",
652
+ original=original_url[:30] + "..." if len(original_url) > 30 else original_url,
653
+ normalized=redis_url[:30] + "..." if len(redis_url) > 30 else redis_url,
654
+ )
655
+
656
+ if redis_url and redis_url.startswith(("redis://", "rediss://")):
657
+ try:
658
+ _redis_client = StandardRedisClient(url=redis_url)
659
+ _redis_client_type = "standard"
660
+ # Mask password in log
661
+ log_url = redis_url
662
+ if "@" in redis_url:
663
+ # redis://:password@host:port -> redis://***@host:port
664
+ parts = redis_url.split("@")
665
+ log_url = parts[0].rsplit(":", 1)[0] + ":***@" + parts[1]
666
+ logger.info("redis_client_created", type="standard", url=log_url[:50])
667
+ return _redis_client
668
+ except ImportError as e:
669
+ logger.warning(
670
+ "redis_standard_client_unavailable",
671
+ error=str(e),
672
+ message="Falling back to Upstash REST API if configured"
673
+ )
674
+ except Exception as e:
675
+ logger.error("redis_standard_client_init_failed", error=str(e))
676
+
677
+ # Priority 2: Check for Upstash REST API
678
+ upstash_url = (
679
+ os.getenv("KV_REST_API_URL") or
680
+ os.getenv("UPSTASH_REDIS_REST_URL") or
681
+ os.getenv("UPSTASH_REDIS_URL")
682
+ )
683
+
684
+ upstash_token = (
685
+ os.getenv("KV_REST_API_TOKEN") or
686
+ os.getenv("UPSTASH_REDIS_REST_TOKEN") or
687
+ os.getenv("UPSTASH_REDIS_TOKEN")
688
+ )
689
+
690
+ if upstash_url and upstash_token:
691
+ try:
692
+ _redis_client = UpstashRedisClient(url=upstash_url, token=upstash_token)
693
+ _redis_client_type = "upstash"
694
+ logger.info("redis_client_created", type="upstash", url=upstash_url[:30] + "...")
695
+ return _redis_client
696
+ except Exception as e:
697
+ logger.error("redis_upstash_client_init_failed", error=str(e))
698
+ return None
699
+
700
+ # No Redis configured
701
+ logger.warning(
702
+ "redis_not_configured",
703
+ message="No Redis configuration found, caching disabled",
704
+ checked_vars=["REDIS_URL", "KV_REST_API_URL", "KV_REST_API_TOKEN", "UPSTASH_*"]
705
+ )
706
+ return None
707
+
708
+
709
+ # Worker-specific caching functions
710
+
711
+ async def cache_worker_heartbeat(
712
+ worker_id: str,
713
+ queue_id: str,
714
+ organization_id: str,
715
+ status: str,
716
+ last_heartbeat: str,
717
+ tasks_processed: int,
718
+ system_info: Optional[dict] = None,
719
+ ttl: int = 60
720
+ ) -> bool:
721
+ """
722
+ Cache worker heartbeat data in Redis.
723
+
724
+ Args:
725
+ worker_id: Worker UUID
726
+ queue_id: Queue UUID
727
+ organization_id: Organization ID
728
+ status: Worker status
729
+ last_heartbeat: ISO timestamp
730
+ tasks_processed: Task count
731
+ system_info: Optional system metrics
732
+ ttl: Cache TTL in seconds
733
+
734
+ Returns:
735
+ True if cached successfully
736
+ """
737
+ client = get_redis_client()
738
+ if not client:
739
+ return False
740
+
741
+ try:
742
+ data = {
743
+ "worker_id": worker_id,
744
+ "queue_id": queue_id,
745
+ "organization_id": organization_id,
746
+ "status": status,
747
+ "last_heartbeat": last_heartbeat,
748
+ "tasks_processed": tasks_processed,
749
+ }
750
+
751
+ if system_info:
752
+ data["system_info"] = json.dumps(system_info)
753
+
754
+ # Cache worker status
755
+ await client.hset(f"worker:{worker_id}:status", data)
756
+ await client.expire(f"worker:{worker_id}:status", ttl)
757
+
758
+ # Add to queue workers set
759
+ await client.sadd(f"queue:{queue_id}:workers", worker_id)
760
+ await client.expire(f"queue:{queue_id}:workers", ttl)
761
+
762
+ logger.debug("worker_heartbeat_cached", worker_id=worker_id[:8])
763
+ return True
764
+
765
+ except Exception as e:
766
+ logger.error("cache_worker_heartbeat_failed", error=str(e), worker_id=worker_id[:8])
767
+ return False
768
+
769
+
770
+ async def cache_worker_logs(worker_id: str, logs: list, ttl: int = 300) -> bool:
771
+ """Cache worker logs in Redis."""
772
+ client = get_redis_client()
773
+ if not client or not logs:
774
+ return False
775
+
776
+ try:
777
+ # Add logs to list
778
+ await client.lpush(f"worker:{worker_id}:logs", *logs)
779
+ # Keep only last 100 logs
780
+ await client.ltrim(f"worker:{worker_id}:logs", 0, 99)
781
+ # Set expiry
782
+ await client.expire(f"worker:{worker_id}:logs", ttl)
783
+
784
+ logger.debug("worker_logs_cached", worker_id=worker_id[:8], count=len(logs))
785
+ return True
786
+
787
+ except Exception as e:
788
+ logger.error("cache_worker_logs_failed", error=str(e), worker_id=worker_id[:8])
789
+ return False
790
+
791
+
792
+ async def get_queue_worker_count_cached(queue_id: str) -> Optional[int]:
793
+ """Get active worker count for queue from cache."""
794
+ client = get_redis_client()
795
+ if not client:
796
+ return None
797
+
798
+ try:
799
+ count = await client.scard(f"queue:{queue_id}:workers")
800
+ return count
801
+ except Exception as e:
802
+ logger.error("get_queue_worker_count_failed", error=str(e), queue_id=queue_id[:8])
803
+ return None