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,324 @@
1
+ """Unit tests for tool enforcement service."""
2
+
3
+ import pytest
4
+ import asyncio
5
+ from unittest.mock import AsyncMock, Mock, patch
6
+ from datetime import datetime, timezone
7
+
8
+ from control_plane_api.worker.services.tool_enforcement import ToolEnforcementService
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_enforcer_client():
13
+ """Mock policy enforcer client."""
14
+ client = AsyncMock()
15
+ client.evaluation = AsyncMock()
16
+ return client
17
+
18
+
19
+ @pytest.fixture
20
+ def enforcement_service(mock_enforcer_client):
21
+ """Create enforcement service with mock client."""
22
+ return ToolEnforcementService(mock_enforcer_client)
23
+
24
+
25
+ @pytest.fixture
26
+ def enforcement_context():
27
+ """Sample enforcement context."""
28
+ return {
29
+ "user_email": "test@example.com",
30
+ "user_id": "user-123",
31
+ "user_roles": ["developer"],
32
+ "organization_id": "org-456",
33
+ "team_id": "team-789",
34
+ "agent_id": "agent-xyz",
35
+ "execution_id": "exec-abc",
36
+ "environment": "production"
37
+ }
38
+
39
+
40
+ class TestToolEnforcementService:
41
+ """Test suite for ToolEnforcementService."""
42
+
43
+ @pytest.mark.asyncio
44
+ async def test_enforce_tool_allowed(self, enforcement_service, enforcement_context, mock_enforcer_client):
45
+ """Test enforcement check when tool is allowed."""
46
+ # Mock enforcer response
47
+ mock_enforcer_client.evaluation.enforce.return_value = {
48
+ "allow": True,
49
+ "id": "enforcement-123",
50
+ "policies": ["role_based_access"]
51
+ }
52
+
53
+ allow, violation, metadata = await enforcement_service.enforce_tool_execution(
54
+ tool_name="Read",
55
+ tool_args={"file_path": "/tmp/test.txt"},
56
+ enforcement_context=enforcement_context
57
+ )
58
+
59
+ assert allow is True
60
+ assert violation is None
61
+ assert metadata["enforcer"] == "allowed"
62
+ assert "role_based_access" in metadata["policies"]
63
+ assert "enforcement_id" in metadata
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_enforce_tool_blocked(self, enforcement_service, enforcement_context, mock_enforcer_client):
67
+ """Test enforcement check when tool is blocked."""
68
+ mock_enforcer_client.evaluation.enforce.return_value = {
69
+ "allow": False,
70
+ "id": "enforcement-456",
71
+ "policies": ["production_safeguards"]
72
+ }
73
+
74
+ allow, violation, metadata = await enforcement_service.enforce_tool_execution(
75
+ tool_name="Bash",
76
+ tool_args={"command": "rm -rf /tmp/*"},
77
+ enforcement_context=enforcement_context
78
+ )
79
+
80
+ assert allow is False
81
+ assert violation is not None
82
+ assert "blocked by policy enforcement" in violation.lower()
83
+ assert "Bash" in violation
84
+ assert metadata["enforcer"] == "blocked"
85
+ assert "production_safeguards" in metadata["policies"]
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_enforce_timeout_fails_open(self, enforcement_service, enforcement_context, mock_enforcer_client):
89
+ """Test that enforcement timeout fails open (allows execution)."""
90
+ # Mock timeout
91
+ async def slow_enforce(*args, **kwargs):
92
+ await asyncio.sleep(5) # Longer than timeout
93
+ return {"allow": False}
94
+
95
+ mock_enforcer_client.evaluation.enforce = slow_enforce
96
+
97
+ allow, violation, metadata = await enforcement_service.enforce_tool_execution(
98
+ tool_name="Bash",
99
+ tool_args={"command": "ls"},
100
+ enforcement_context=enforcement_context,
101
+ timeout=0.1 # Very short timeout
102
+ )
103
+
104
+ assert allow is True # Fails open
105
+ assert violation is None
106
+ assert metadata["enforcer"] == "timeout"
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_enforce_error_fails_open(self, enforcement_service, enforcement_context, mock_enforcer_client):
110
+ """Test that enforcement errors fail open (allows execution)."""
111
+ # Mock error
112
+ mock_enforcer_client.evaluation.enforce.side_effect = Exception("Enforcer unavailable")
113
+
114
+ allow, violation, metadata = await enforcement_service.enforce_tool_execution(
115
+ tool_name="Bash",
116
+ tool_args={"command": "ls"},
117
+ enforcement_context=enforcement_context
118
+ )
119
+
120
+ assert allow is True # Fails open
121
+ assert violation is None
122
+ assert metadata["enforcer"] == "error"
123
+ assert "error" in metadata
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_disabled_enforcer(self):
127
+ """Test that disabled enforcer allows all tools."""
128
+ service = ToolEnforcementService(None)
129
+
130
+ allow, violation, metadata = await service.enforce_tool_execution(
131
+ tool_name="Bash",
132
+ tool_args={"command": "rm -rf /"},
133
+ enforcement_context={}
134
+ )
135
+
136
+ assert allow is True
137
+ assert violation is None
138
+ assert metadata["enforcer"] == "disabled"
139
+
140
+ def test_build_enforcement_payload(self, enforcement_service, enforcement_context):
141
+ """Test enforcement payload construction."""
142
+ payload = enforcement_service._build_enforcement_payload(
143
+ tool_name="Bash",
144
+ tool_args={"command": "kubectl get pods"},
145
+ context=enforcement_context
146
+ )
147
+
148
+ assert payload["action"] == "tool_execution"
149
+ assert payload["tool"]["name"] == "Bash"
150
+ assert payload["tool"]["source"] == "builtin"
151
+ assert payload["tool"]["category"] == "command_execution"
152
+ assert payload["tool"]["risk_level"] == "high"
153
+ assert payload["user"]["email"] == "test@example.com"
154
+ assert payload["organization"]["id"] == "org-456"
155
+ assert payload["execution"]["environment"] == "production"
156
+ assert "timestamp" in payload["execution"]
157
+
158
+ def test_determine_tool_source(self, enforcement_service):
159
+ """Test tool source detection."""
160
+ assert enforcement_service._determine_tool_source("mcp__github__list_repos") == "mcp"
161
+ assert enforcement_service._determine_tool_source("Bash") == "builtin"
162
+ assert enforcement_service._determine_tool_source("Read") == "builtin"
163
+ assert enforcement_service._determine_tool_source("custom_tool") == "skill"
164
+
165
+ def test_determine_tool_category(self, enforcement_service):
166
+ """Test tool category detection."""
167
+ assert enforcement_service._determine_tool_category("Bash") == "command_execution"
168
+ assert enforcement_service._determine_tool_category("Read") == "file_operation"
169
+ assert enforcement_service._determine_tool_category("Write") == "file_operation"
170
+ assert enforcement_service._determine_tool_category("Grep") == "file_search"
171
+ assert enforcement_service._determine_tool_category("WebFetch") == "network"
172
+ assert enforcement_service._determine_tool_category("custom_tool") == "general"
173
+
174
+ def test_determine_risk_level_critical(self, enforcement_service):
175
+ """Test risk level assessment for critical commands."""
176
+ # Critical risk - destructive commands
177
+ assert enforcement_service._determine_risk_level(
178
+ "Bash",
179
+ {"command": "rm -rf /"}
180
+ ) == "critical"
181
+
182
+ assert enforcement_service._determine_risk_level(
183
+ "Bash",
184
+ {"command": "dd if=/dev/zero of=/dev/sda"}
185
+ ) == "critical"
186
+
187
+ def test_determine_risk_level_high(self, enforcement_service):
188
+ """Test risk level assessment for high-risk operations."""
189
+ # High risk - command execution
190
+ assert enforcement_service._determine_risk_level(
191
+ "Bash",
192
+ {"command": "kubectl delete deployment"}
193
+ ) == "high"
194
+
195
+ # High risk - sensitive file access
196
+ assert enforcement_service._determine_risk_level(
197
+ "Read",
198
+ {"file_path": "/etc/passwd"}
199
+ ) == "high"
200
+
201
+ assert enforcement_service._determine_risk_level(
202
+ "Read",
203
+ {"file_path": "~/.ssh/id_rsa"}
204
+ ) == "high"
205
+
206
+ def test_determine_risk_level_medium(self, enforcement_service):
207
+ """Test risk level assessment for medium-risk operations."""
208
+ assert enforcement_service._determine_risk_level(
209
+ "Write",
210
+ {"file_path": "/tmp/test.txt", "content": "test"}
211
+ ) == "medium"
212
+
213
+ assert enforcement_service._determine_risk_level(
214
+ "Edit",
215
+ {"file_path": "/tmp/config.yaml"}
216
+ ) == "medium"
217
+
218
+ def test_determine_risk_level_low(self, enforcement_service):
219
+ """Test risk level assessment for low-risk operations."""
220
+ assert enforcement_service._determine_risk_level(
221
+ "Read",
222
+ {"file_path": "/tmp/test.txt"}
223
+ ) == "low"
224
+
225
+ assert enforcement_service._determine_risk_level(
226
+ "Grep",
227
+ {"pattern": "error", "path": "/var/log"}
228
+ ) == "low"
229
+
230
+ def test_format_violation_message(self, enforcement_service):
231
+ """Test violation message formatting."""
232
+ enforcement_result = {
233
+ "id": "enf-123",
234
+ "allow": False,
235
+ "policies": ["policy1", "policy2"]
236
+ }
237
+
238
+ message = enforcement_service._format_violation_message(
239
+ tool_name="Bash",
240
+ policies=["policy1", "policy2"],
241
+ enforcement_result=enforcement_result
242
+ )
243
+
244
+ assert "Tool execution blocked" in message
245
+ assert "Bash" in message
246
+ assert "policy1, policy2" in message
247
+ assert "enf-123" in message
248
+ assert "administrator" in message.lower()
249
+
250
+
251
+ class TestToolEnforcementPayloadValidation:
252
+ """Test payload structure validation."""
253
+
254
+ def test_payload_has_all_required_fields(self, enforcement_service, enforcement_context):
255
+ """Verify payload contains all required fields."""
256
+ payload = enforcement_service._build_enforcement_payload(
257
+ tool_name="Bash",
258
+ tool_args={"command": "ls"},
259
+ context=enforcement_context
260
+ )
261
+
262
+ # Top-level fields
263
+ assert "action" in payload
264
+ assert "tool" in payload
265
+ assert "user" in payload
266
+ assert "organization" in payload
267
+ assert "team" in payload
268
+ assert "execution" in payload
269
+
270
+ # Tool fields
271
+ assert "name" in payload["tool"]
272
+ assert "arguments" in payload["tool"]
273
+ assert "source" in payload["tool"]
274
+ assert "category" in payload["tool"]
275
+ assert "risk_level" in payload["tool"]
276
+
277
+ # User fields
278
+ assert "email" in payload["user"]
279
+ assert "id" in payload["user"]
280
+ assert "roles" in payload["user"]
281
+
282
+ # Organization fields
283
+ assert "id" in payload["organization"]
284
+
285
+ # Execution fields
286
+ assert "execution_id" in payload["execution"]
287
+ assert "agent_id" in payload["execution"]
288
+ assert "environment" in payload["execution"]
289
+ assert "timestamp" in payload["execution"]
290
+
291
+ def test_payload_timestamp_format(self, enforcement_service, enforcement_context):
292
+ """Verify timestamp is in ISO format."""
293
+ payload = enforcement_service._build_enforcement_payload(
294
+ tool_name="Test",
295
+ tool_args={},
296
+ context=enforcement_context
297
+ )
298
+
299
+ timestamp = payload["execution"]["timestamp"]
300
+ # Should be able to parse back
301
+ parsed = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
302
+ assert isinstance(parsed, datetime)
303
+
304
+ def test_payload_handles_missing_context_fields(self, enforcement_service):
305
+ """Test payload construction with minimal context."""
306
+ minimal_context = {
307
+ "organization_id": "org-123",
308
+ "agent_id": "agent-456"
309
+ }
310
+
311
+ payload = enforcement_service._build_enforcement_payload(
312
+ tool_name="Test",
313
+ tool_args={},
314
+ context=minimal_context
315
+ )
316
+
317
+ # Should not crash, just have None values
318
+ assert payload["user"]["email"] is None
319
+ assert payload["user"]["roles"] == []
320
+ assert payload["organization"]["id"] == "org-123"
321
+
322
+
323
+ if __name__ == "__main__":
324
+ pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1 @@
1
+ """Utility functions and helpers"""
@@ -0,0 +1,330 @@
1
+ """
2
+ Smart chunk batching for streaming to reduce HTTP requests.
3
+
4
+ Instead of sending one POST per chunk (50-70 requests), batch chunks
5
+ with configurable time/size windows (5-10 requests).
6
+
7
+ Batching Strategy:
8
+ - Time window: Flush after X ms (default: 100ms)
9
+ - Size window: Flush when batch reaches Y bytes (default: 100 bytes)
10
+ - Immediate flush: On tool events, errors, or completion
11
+
12
+ This provides:
13
+ - 90%+ reduction in HTTP requests
14
+ - Still feels real-time (100ms is imperceptible)
15
+ - Lower latency (fewer round trips)
16
+ - Better serverless performance (fewer cold starts)
17
+ - Lower costs (fewer invocations)
18
+ """
19
+
20
+ import asyncio
21
+ import time
22
+ from typing import Dict, Any, Optional, Callable
23
+ from dataclasses import dataclass, field
24
+ import structlog
25
+
26
+ logger = structlog.get_logger()
27
+
28
+
29
+ @dataclass
30
+ class BatchConfig:
31
+ """Configuration for chunk batching behavior."""
32
+
33
+ # Time-based batching: flush after this many milliseconds
34
+ time_window_ms: int = 100
35
+
36
+ # Size-based batching: flush when accumulated content reaches this size
37
+ size_window_bytes: int = 100
38
+
39
+ # Maximum batch size before forced flush (safety limit)
40
+ max_batch_size_bytes: int = 1000
41
+
42
+ # Enable/disable batching (for testing/debugging)
43
+ enabled: bool = True
44
+
45
+ @classmethod
46
+ def from_env(cls) -> "BatchConfig":
47
+ """
48
+ Create configuration from environment variables.
49
+
50
+ Environment variables:
51
+ CHUNK_BATCHING_ENABLED: Enable/disable batching (default: true)
52
+ CHUNK_BATCHING_TIME_WINDOW_MS: Time window in ms (default: 100)
53
+ CHUNK_BATCHING_SIZE_WINDOW_BYTES: Size window in bytes (default: 100)
54
+ CHUNK_BATCHING_MAX_SIZE_BYTES: Max batch size in bytes (default: 1000)
55
+
56
+ Returns:
57
+ BatchConfig instance with values from environment
58
+ """
59
+ import os
60
+
61
+ return cls(
62
+ enabled=os.getenv("CHUNK_BATCHING_ENABLED", "true").lower() == "true",
63
+ time_window_ms=int(os.getenv("CHUNK_BATCHING_TIME_WINDOW_MS", "100")),
64
+ size_window_bytes=int(os.getenv("CHUNK_BATCHING_SIZE_WINDOW_BYTES", "100")),
65
+ max_batch_size_bytes=int(os.getenv("CHUNK_BATCHING_MAX_SIZE_BYTES", "1000")),
66
+ )
67
+
68
+
69
+ @dataclass
70
+ class ContentBatch:
71
+ """Accumulated content chunks waiting to be flushed."""
72
+
73
+ chunks: list[str] = field(default_factory=list)
74
+ total_size: int = 0
75
+ first_chunk_time: Optional[float] = None
76
+
77
+ def add(self, content: str) -> None:
78
+ """Add content to the batch."""
79
+ self.chunks.append(content)
80
+ self.total_size += len(content.encode('utf-8'))
81
+
82
+ if self.first_chunk_time is None:
83
+ self.first_chunk_time = time.time()
84
+
85
+ def get_combined_content(self) -> str:
86
+ """Get all chunks combined into single string."""
87
+ return ''.join(self.chunks)
88
+
89
+ def clear(self) -> None:
90
+ """Clear the batch after flushing."""
91
+ self.chunks.clear()
92
+ self.total_size = 0
93
+ self.first_chunk_time = None
94
+
95
+ def is_empty(self) -> bool:
96
+ """Check if batch is empty."""
97
+ return len(self.chunks) == 0
98
+
99
+ def age_ms(self) -> float:
100
+ """Get age of batch in milliseconds."""
101
+ if self.first_chunk_time is None:
102
+ return 0
103
+ return (time.time() - self.first_chunk_time) * 1000
104
+
105
+
106
+ class ChunkBatcher:
107
+ """
108
+ Smart batching for streaming chunks to reduce HTTP requests.
109
+
110
+ Usage:
111
+ batcher = ChunkBatcher(
112
+ publish_func=control_plane.publish_event,
113
+ execution_id=execution_id,
114
+ message_id=message_id,
115
+ config=BatchConfig(time_window_ms=100, size_window_bytes=100)
116
+ )
117
+
118
+ # Add chunks as they arrive
119
+ await batcher.add_chunk("Hello")
120
+ await batcher.add_chunk(" world")
121
+
122
+ # Flush remaining chunks when done
123
+ await batcher.flush()
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ publish_func: Callable,
129
+ execution_id: str,
130
+ message_id: str,
131
+ config: Optional[BatchConfig] = None
132
+ ):
133
+ self.publish_func = publish_func
134
+ self.execution_id = execution_id
135
+ self.message_id = message_id
136
+ self.config = config or BatchConfig()
137
+
138
+ self.batch = ContentBatch()
139
+ self._flush_task: Optional[asyncio.Task] = None
140
+ self._stats = {
141
+ "chunks_received": 0,
142
+ "batches_sent": 0,
143
+ "bytes_sent": 0,
144
+ "flushes_by_time": 0,
145
+ "flushes_by_size": 0,
146
+ "flushes_manual": 0,
147
+ }
148
+
149
+ async def add_chunk(self, content: str) -> None:
150
+ """
151
+ Add a chunk to the batch.
152
+
153
+ Automatically flushes if:
154
+ - Batch size exceeds size_window_bytes
155
+ - Batch age exceeds time_window_ms
156
+ - Max batch size is reached (safety)
157
+ """
158
+ if not self.config.enabled:
159
+ # Batching disabled - send immediately
160
+ await self._publish_batch([content])
161
+ return
162
+
163
+ self._stats["chunks_received"] += 1
164
+ self.batch.add(content)
165
+
166
+ # Check if we should flush immediately due to size
167
+ should_flush_size = self.batch.total_size >= self.config.size_window_bytes
168
+ should_flush_max = self.batch.total_size >= self.config.max_batch_size_bytes
169
+
170
+ if should_flush_max:
171
+ # Safety: flush immediately if max size reached
172
+ logger.debug(
173
+ "Flushing batch (max size reached)",
174
+ execution_id=self.execution_id[:8],
175
+ batch_size=self.batch.total_size,
176
+ chunk_count=len(self.batch.chunks),
177
+ )
178
+ await self.flush(reason="max_size")
179
+ elif should_flush_size:
180
+ # Size threshold reached - flush now
181
+ await self.flush(reason="size")
182
+ else:
183
+ # Start/reset timer for time-based flush
184
+ await self._schedule_time_flush()
185
+
186
+ async def _schedule_time_flush(self) -> None:
187
+ """Schedule a time-based flush if not already scheduled."""
188
+ if self._flush_task is not None and not self._flush_task.done():
189
+ # Timer already running
190
+ return
191
+
192
+ self._flush_task = asyncio.create_task(self._time_based_flush())
193
+
194
+ async def _time_based_flush(self) -> None:
195
+ """Wait for time window, then flush."""
196
+ await asyncio.sleep(self.config.time_window_ms / 1000.0)
197
+
198
+ if not self.batch.is_empty():
199
+ await self.flush(reason="time")
200
+
201
+ async def flush(self, reason: str = "manual") -> None:
202
+ """
203
+ Flush current batch immediately.
204
+
205
+ Args:
206
+ reason: Why flush was triggered (for stats/debugging)
207
+ """
208
+ if self.batch.is_empty():
209
+ return
210
+
211
+ # Cancel pending timer if any
212
+ if self._flush_task is not None and not self._flush_task.done():
213
+ self._flush_task.cancel()
214
+ try:
215
+ await self._flush_task
216
+ except asyncio.CancelledError:
217
+ pass
218
+
219
+ # Publish the batch
220
+ chunks = self.batch.chunks.copy()
221
+ await self._publish_batch(chunks)
222
+
223
+ # Update stats
224
+ if reason == "time":
225
+ self._stats["flushes_by_time"] += 1
226
+ elif reason == "size" or reason == "max_size":
227
+ self._stats["flushes_by_size"] += 1
228
+ else:
229
+ self._stats["flushes_manual"] += 1
230
+
231
+ # Clear batch
232
+ self.batch.clear()
233
+
234
+ async def _publish_batch(self, chunks: list[str]) -> None:
235
+ """Publish a batch of chunks as single event with retry logic."""
236
+ combined_content = ''.join(chunks)
237
+ max_retries = 3
238
+ base_delay = 0.1 # 100ms
239
+
240
+ for attempt in range(max_retries):
241
+ try:
242
+ # CRITICAL: Always await async functions immediately
243
+ # publish_func is an async function, so call it with await
244
+ await self.publish_func(
245
+ execution_id=self.execution_id,
246
+ event_type="message_chunk",
247
+ data={
248
+ "role": "assistant",
249
+ "content": combined_content,
250
+ "is_chunk": True,
251
+ "message_id": self.message_id,
252
+ # Metadata for debugging
253
+ "batch_info": {
254
+ "chunk_count": len(chunks),
255
+ "batch_size": len(combined_content.encode('utf-8')),
256
+ } if len(chunks) > 1 else None,
257
+ }
258
+ )
259
+
260
+ self._stats["batches_sent"] += 1
261
+ self._stats["bytes_sent"] += len(combined_content.encode('utf-8'))
262
+
263
+ # Success - exit retry loop
264
+ if attempt > 0:
265
+ logger.debug(
266
+ "Batch published after retry",
267
+ execution_id=self.execution_id[:8],
268
+ attempt=attempt + 1,
269
+ chunk_count=len(chunks),
270
+ )
271
+ return
272
+
273
+ except Exception as e:
274
+ is_last_attempt = attempt == max_retries - 1
275
+
276
+ if is_last_attempt:
277
+ logger.error(
278
+ "Failed to publish batch after all retries",
279
+ execution_id=self.execution_id[:8],
280
+ error=str(e),
281
+ chunk_count=len(chunks),
282
+ attempts=max_retries,
283
+ )
284
+ else:
285
+ # Exponential backoff
286
+ delay = base_delay * (2 ** attempt)
287
+ logger.debug(
288
+ "Retrying batch publish",
289
+ execution_id=self.execution_id[:8],
290
+ error=str(e),
291
+ attempt=attempt + 1,
292
+ next_delay_ms=int(delay * 1000),
293
+ )
294
+ await asyncio.sleep(delay)
295
+
296
+ def get_stats(self) -> Dict[str, Any]:
297
+ """
298
+ Get batching statistics.
299
+
300
+ Returns:
301
+ Dict with stats about batching performance
302
+ """
303
+ chunks_received = self._stats["chunks_received"]
304
+ batches_sent = self._stats["batches_sent"]
305
+
306
+ return {
307
+ **self._stats,
308
+ "reduction_percent": round(
309
+ (1 - batches_sent / max(chunks_received, 1)) * 100, 1
310
+ ) if chunks_received > 0 else 0,
311
+ "avg_batch_size": round(
312
+ chunks_received / max(batches_sent, 1), 1
313
+ ) if batches_sent > 0 else 0,
314
+ }
315
+
316
+ async def close(self) -> None:
317
+ """
318
+ Close the batcher and flush remaining chunks.
319
+
320
+ Call this when streaming is complete.
321
+ """
322
+ await self.flush(reason="close")
323
+
324
+ # Log stats
325
+ stats = self.get_stats()
326
+ logger.info(
327
+ "Chunk batching stats",
328
+ execution_id=self.execution_id[:8],
329
+ **stats
330
+ )