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