kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,574 @@
1
+ """Team executor service - handles team execution business logic"""
2
+
3
+ from typing import Dict, Any, Optional, List
4
+ from datetime import datetime, timezone
5
+ import structlog
6
+ import asyncio
7
+ import os
8
+ from temporalio import activity
9
+
10
+ from agno.agent import Agent
11
+ from agno.team import Team
12
+ from agno.models.litellm import LiteLLM
13
+
14
+ from control_plane_api.worker.control_plane_client import ControlPlaneClient
15
+ from control_plane_api.worker.services.session_service import SessionService
16
+ from control_plane_api.worker.services.cancellation_manager import CancellationManager
17
+ from control_plane_api.worker.services.skill_factory import SkillFactory
18
+ from control_plane_api.worker.utils.streaming_utils import StreamingHelper
19
+
20
+ logger = structlog.get_logger()
21
+
22
+
23
+ class TeamExecutorService:
24
+ """
25
+ Service for executing teams with full session management and cancellation support.
26
+
27
+ This service orchestrates:
28
+ - Session loading and restoration
29
+ - Team and member agent creation with LiteLLM configuration
30
+ - Skill instantiation for team members
31
+ - Streaming execution with real-time updates
32
+ - Session persistence
33
+ - Cancellation support via CancellationManager
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ control_plane: ControlPlaneClient,
39
+ session_service: SessionService,
40
+ cancellation_manager: CancellationManager
41
+ ):
42
+ self.control_plane = control_plane
43
+ self.session_service = session_service
44
+ self.cancellation_manager = cancellation_manager
45
+
46
+ async def execute(self, input: Any) -> Dict[str, Any]:
47
+ """
48
+ Execute a team with full session management and streaming.
49
+
50
+ Args:
51
+ input: TeamExecutionInput with execution details
52
+
53
+ Returns:
54
+ Dict with response, usage, success flag, etc.
55
+ """
56
+ execution_id = input.execution_id
57
+
58
+ print("\n" + "="*80)
59
+ print("🚀 TEAM EXECUTION START")
60
+ print("="*80)
61
+ print(f"Execution ID: {execution_id}")
62
+ print(f"Team ID: {input.team_id}")
63
+ print(f"Organization: {input.organization_id}")
64
+ print(f"Agent Count: {len(input.agents)}")
65
+ print(f"Session ID: {input.session_id}")
66
+ print(f"Prompt: {input.prompt[:100]}..." if len(input.prompt) > 100 else f"Prompt: {input.prompt}")
67
+ print("="*80 + "\n")
68
+
69
+ logger.info(
70
+ "team_execution_start",
71
+ execution_id=execution_id[:8],
72
+ team_id=input.team_id,
73
+ session_id=input.session_id,
74
+ agent_count=len(input.agents)
75
+ )
76
+
77
+ try:
78
+ # STEP 1: Load session history
79
+ session_history = self.session_service.load_session(
80
+ execution_id=execution_id,
81
+ session_id=input.session_id
82
+ )
83
+
84
+ if session_history:
85
+ print(f"✅ Loaded {len(session_history)} messages from previous session\n")
86
+ else:
87
+ print("ℹ️ Starting new conversation\n")
88
+
89
+ # STEP 2: Build conversation context for Agno
90
+ conversation_context = self.session_service.build_conversation_context(session_history)
91
+
92
+ # STEP 3: Get LiteLLM configuration
93
+ litellm_api_base = os.getenv("LITELLM_API_BASE", "https://llm-proxy.kubiya.ai")
94
+ litellm_api_key = os.getenv("LITELLM_API_KEY")
95
+
96
+ if not litellm_api_key:
97
+ raise ValueError("LITELLM_API_KEY environment variable not set")
98
+
99
+ model = input.model_id or os.environ.get("LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4")
100
+
101
+ # STEP 4: Create member agents with skills
102
+ print(f"👥 Creating {len(input.agents)} member agents...\n")
103
+
104
+ team_members = []
105
+ for agent_data in input.agents:
106
+ agent_id = agent_data.get("id")
107
+ agent_name = agent_data.get("name", f"Agent {agent_id}")
108
+ agent_role = agent_data.get("role", "You are a helpful AI assistant")
109
+
110
+ print(f" 🤖 Creating Agent: {agent_name}")
111
+ print(f" ID: {agent_id}")
112
+ print(f" Role: {agent_role[:80]}...")
113
+
114
+ # Fetch skills for this agent
115
+ skills = []
116
+ if agent_id:
117
+ try:
118
+ skill_configs = self.control_plane.get_skills(agent_id)
119
+ if skill_configs:
120
+ print(f" Skills: {len(skill_configs)}")
121
+ skills = SkillFactory.create_skills_from_list(skill_configs)
122
+ if skills:
123
+ print(f" ✅ Instantiated {len(skills)} skill(s)")
124
+ except Exception as e:
125
+ print(f" ⚠️ Failed to fetch skills: {str(e)}")
126
+ logger.warning("skill_fetch_error_for_team_member", agent_id=agent_id, error=str(e))
127
+
128
+ # Create Agno Agent
129
+ member_agent = Agent(
130
+ name=agent_name,
131
+ role=agent_role,
132
+ model=LiteLLM(
133
+ id=f"openai/{model}",
134
+ api_base=litellm_api_base,
135
+ api_key=litellm_api_key,
136
+ ),
137
+ tools=skills if skills else None,
138
+ )
139
+
140
+ team_members.append(member_agent)
141
+ print(f" ✅ Agent created\n")
142
+
143
+ if not team_members:
144
+ raise ValueError("No team members available for team execution")
145
+
146
+ # STEP 5: Create Agno Team with streaming helper
147
+ print(f"\n🚀 Creating Agno Team:")
148
+ print(f" Team ID: {input.team_id}")
149
+ print(f" Members: {len(team_members)}")
150
+ print(f" Model: {model}")
151
+
152
+ # Create streaming helper for this execution
153
+ streaming_helper = StreamingHelper(
154
+ control_plane_client=self.control_plane,
155
+ execution_id=execution_id
156
+ )
157
+
158
+ # Create tool hook for real-time updates
159
+ def tool_hook(name: str = None, function_name: str = None, function=None, arguments: dict = None, **kwargs):
160
+ """Hook to capture tool execution for real-time streaming"""
161
+ tool_name = name or function_name or "unknown"
162
+ tool_args = arguments or {}
163
+
164
+ # Generate unique tool execution ID
165
+ import time
166
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
167
+
168
+ print(f" 🔧 Tool Starting: {tool_name} (ID: {tool_execution_id})")
169
+
170
+ # Publish tool start event
171
+ streaming_helper.publish_tool_start(
172
+ tool_name=tool_name,
173
+ tool_execution_id=tool_execution_id,
174
+ tool_args=tool_args,
175
+ source="team"
176
+ )
177
+
178
+ # Execute the tool
179
+ result = None
180
+ error = None
181
+ try:
182
+ if function and callable(function):
183
+ result = function(**tool_args) if tool_args else function()
184
+ else:
185
+ raise ValueError(f"Function not callable: {function}")
186
+
187
+ status = "success"
188
+ print(f" ✅ Tool Success: {tool_name}")
189
+
190
+ except Exception as e:
191
+ error = e
192
+ status = "failed"
193
+ print(f" ❌ Tool Failed: {tool_name} - {str(e)}")
194
+
195
+ # Publish tool completion event
196
+ streaming_helper.publish_tool_complete(
197
+ tool_name=tool_name,
198
+ tool_execution_id=tool_execution_id,
199
+ status=status,
200
+ output=str(result)[:1000] if result else None,
201
+ error=str(error) if error else None,
202
+ source="team"
203
+ )
204
+
205
+ if error:
206
+ raise error
207
+
208
+ return result
209
+
210
+ # Add tool hooks to all team members
211
+ for member in team_members:
212
+ if not hasattr(member, 'tool_hooks') or member.tool_hooks is None:
213
+ member.tool_hooks = []
214
+ member.tool_hooks.append(tool_hook)
215
+
216
+ # Create Agno Team
217
+ team = Team(
218
+ name=f"Team {input.team_id}",
219
+ members=team_members,
220
+ model=LiteLLM(
221
+ id=f"openai/{model}",
222
+ api_base=litellm_api_base,
223
+ api_key=litellm_api_key,
224
+ ),
225
+ )
226
+
227
+ # STEP 6: Register for cancellation
228
+ self.cancellation_manager.register(
229
+ execution_id=execution_id,
230
+ instance=team,
231
+ instance_type="team"
232
+ )
233
+ print(f"✅ Team registered for cancellation support\n")
234
+
235
+ # Cache execution metadata in Redis
236
+ self.control_plane.cache_metadata(execution_id, "TEAM")
237
+
238
+ # STEP 7: Execute with streaming
239
+ print("⚡ Executing Team Run with Streaming...\n")
240
+
241
+ # Generate unique message ID for this turn
242
+ import time
243
+ message_id = f"{execution_id}_{int(time.time() * 1000000)}"
244
+
245
+ def stream_team_run():
246
+ """Run team with streaming and collect response"""
247
+ # Create event loop for this thread (needed for async streaming)
248
+ loop = asyncio.new_event_loop()
249
+ asyncio.set_event_loop(loop)
250
+
251
+ async def _async_stream():
252
+ """Async wrapper for streaming execution"""
253
+ import time as time_module
254
+ last_heartbeat_time = time_module.time()
255
+ last_persistence_time = time_module.time()
256
+ heartbeat_interval = 10 # Send heartbeat every 10 seconds
257
+ persistence_interval = 60 # Persist to database every 60 seconds
258
+
259
+ try:
260
+ # Execute with conversation context
261
+ if conversation_context:
262
+ run_response = team.run(
263
+ input.prompt,
264
+ stream=True,
265
+ messages=conversation_context,
266
+ )
267
+ else:
268
+ run_response = team.run(input.prompt, stream=True)
269
+
270
+ # Process streaming events (sync iteration in async context)
271
+ for event in run_response:
272
+ # Periodic maintenance: heartbeats and persistence
273
+ current_time = time_module.time()
274
+
275
+ # Send heartbeat every 10s (Temporal liveness)
276
+ if current_time - last_heartbeat_time >= heartbeat_interval:
277
+ current_response = streaming_helper.get_full_response()
278
+ activity.heartbeat({
279
+ "status": "Streaming in progress...",
280
+ "response_length": len(current_response),
281
+ "execution_id": execution_id,
282
+ })
283
+ last_heartbeat_time = current_time
284
+
285
+ # Persist snapshot every 60s (resilience against crashes)
286
+ if current_time - last_persistence_time >= persistence_interval:
287
+ current_response = streaming_helper.get_full_response()
288
+ if current_response:
289
+ print(f"\n💾 Periodic persistence ({len(current_response)} chars)...")
290
+ snapshot_messages = session_history + [{
291
+ "role": "assistant",
292
+ "content": current_response,
293
+ "timestamp": datetime.now(timezone.utc).isoformat(),
294
+ }]
295
+ try:
296
+ # Best effort - don't fail execution if persistence fails
297
+ self.session_service.persist_session(
298
+ execution_id=execution_id,
299
+ session_id=input.session_id or execution_id,
300
+ user_id=input.user_id,
301
+ messages=snapshot_messages,
302
+ metadata={
303
+ "team_id": input.team_id,
304
+ "organization_id": input.organization_id,
305
+ "snapshot": True,
306
+ }
307
+ )
308
+ print(f" ✅ Session snapshot persisted")
309
+ except Exception as e:
310
+ print(f" ⚠️ Session persistence error: {str(e)} (non-fatal)")
311
+ last_persistence_time = current_time
312
+
313
+ # Handle run_id capture
314
+ streaming_helper.handle_run_id(
315
+ chunk=event,
316
+ on_run_id=lambda run_id: self.cancellation_manager.set_run_id(execution_id, run_id)
317
+ )
318
+
319
+ # Get event type
320
+ event_type = getattr(event, 'event', None)
321
+
322
+ # Handle TEAM LEADER content chunks
323
+ if event_type == "TeamRunContent":
324
+ streaming_helper.handle_content_chunk(
325
+ chunk=event,
326
+ message_id=message_id,
327
+ print_to_console=True
328
+ )
329
+
330
+ # Handle MEMBER content chunks (from team members)
331
+ elif event_type == "RunContent":
332
+ # Member agent content chunk
333
+ member_name = getattr(event, 'agent_name', getattr(event, 'member_name', 'Team Member'))
334
+
335
+ if hasattr(event, 'content') and event.content:
336
+ content = str(event.content)
337
+ streaming_helper.handle_member_content_chunk(
338
+ member_name=member_name,
339
+ content=content,
340
+ print_to_console=True
341
+ )
342
+
343
+ # Handle TEAM LEADER tool calls
344
+ elif event_type == "TeamToolCallStarted":
345
+ # Extract tool name properly
346
+ tool_obj = getattr(event, 'tool', None)
347
+ if tool_obj and hasattr(tool_obj, 'tool_name'):
348
+ tool_name = tool_obj.tool_name
349
+ tool_args = getattr(tool_obj, 'tool_args', {})
350
+ else:
351
+ tool_name = str(tool_obj) if tool_obj else getattr(event, 'tool_name', 'unknown')
352
+ tool_args = {}
353
+
354
+ import time
355
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
356
+
357
+ print(f"\n 🔧 Tool Starting: {tool_name} (Team Leader)")
358
+ streaming_helper.publish_tool_start(
359
+ tool_name=tool_name,
360
+ tool_execution_id=tool_execution_id,
361
+ tool_args=tool_args,
362
+ source="team_leader",
363
+ member_name=None
364
+ )
365
+
366
+ elif event_type == "TeamToolCallCompleted":
367
+ # Extract tool name and output
368
+ tool_obj = getattr(event, 'tool', None)
369
+ if tool_obj and hasattr(tool_obj, 'tool_name'):
370
+ tool_name = tool_obj.tool_name
371
+ tool_output = getattr(tool_obj, 'result', None) or getattr(event, 'result', None)
372
+ else:
373
+ tool_name = str(tool_obj) if tool_obj else getattr(event, 'tool_name', 'unknown')
374
+ tool_output = getattr(event, 'result', None)
375
+
376
+ import time
377
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
378
+
379
+ print(f"\n ✅ Tool Completed: {tool_name} (Team Leader)")
380
+ streaming_helper.publish_tool_complete(
381
+ tool_name=tool_name,
382
+ tool_execution_id=tool_execution_id,
383
+ status="success",
384
+ output=str(tool_output) if tool_output else None,
385
+ error=None,
386
+ source="team_leader",
387
+ member_name=None
388
+ )
389
+
390
+ # Handle MEMBER tool calls
391
+ elif event_type == "ToolCallStarted":
392
+ # Extract tool name properly
393
+ tool_obj = getattr(event, 'tool', None)
394
+ if tool_obj and hasattr(tool_obj, 'tool_name'):
395
+ tool_name = tool_obj.tool_name
396
+ tool_args = getattr(tool_obj, 'tool_args', {})
397
+ else:
398
+ tool_name = str(tool_obj) if tool_obj else getattr(event, 'tool_name', 'unknown')
399
+ tool_args = {}
400
+
401
+ member_name = getattr(event, 'agent_name', getattr(event, 'member_name', 'Team Member'))
402
+
403
+ import time
404
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
405
+
406
+ print(f"\n 🔧 Tool Starting: {tool_name} ({member_name})")
407
+ streaming_helper.publish_tool_start(
408
+ tool_name=tool_name,
409
+ tool_execution_id=tool_execution_id,
410
+ tool_args=tool_args,
411
+ source="team_member",
412
+ member_name=member_name
413
+ )
414
+
415
+ elif event_type == "ToolCallCompleted":
416
+ # Extract tool name and output
417
+ tool_obj = getattr(event, 'tool', None)
418
+ if tool_obj and hasattr(tool_obj, 'tool_name'):
419
+ tool_name = tool_obj.tool_name
420
+ tool_output = getattr(tool_obj, 'result', None) or getattr(event, 'result', None)
421
+ else:
422
+ tool_name = str(tool_obj) if tool_obj else getattr(event, 'tool_name', 'unknown')
423
+ tool_output = getattr(event, 'result', None)
424
+
425
+ member_name = getattr(event, 'agent_name', getattr(event, 'member_name', 'Team Member'))
426
+
427
+ import time
428
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
429
+
430
+ print(f"\n ✅ Tool Completed: {tool_name} ({member_name})")
431
+ streaming_helper.publish_tool_complete(
432
+ tool_name=tool_name,
433
+ tool_execution_id=tool_execution_id,
434
+ status="success",
435
+ output=str(tool_output) if tool_output else None,
436
+ error=None,
437
+ source="team_member",
438
+ member_name=member_name
439
+ )
440
+
441
+ # Finalize streaming (mark any active member as complete)
442
+ streaming_helper.finalize_streaming()
443
+
444
+ print() # New line after streaming
445
+ return run_response
446
+
447
+ except Exception as e:
448
+ print(f"\n❌ Streaming error: {str(e)}")
449
+ # Fall back to non-streaming
450
+ if conversation_context:
451
+ return team.run(input.prompt, stream=False, messages=conversation_context)
452
+ else:
453
+ return team.run(input.prompt, stream=False)
454
+
455
+ # Run the async function in the event loop
456
+ try:
457
+ return loop.run_until_complete(_async_stream())
458
+ finally:
459
+ loop.close()
460
+
461
+ # Execute in thread pool (no timeout - user controls via STOP button)
462
+ # Wrap in try-except to handle Temporal cancellation
463
+ try:
464
+ result = await asyncio.to_thread(stream_team_run)
465
+ except asyncio.CancelledError:
466
+ # Temporal cancelled the activity - cancel the running team
467
+ print("\n🛑 Cancellation signal received - stopping team execution...")
468
+ cancel_result = self.cancellation_manager.cancel(execution_id)
469
+ if cancel_result["success"]:
470
+ print(f"✅ Team execution cancelled successfully")
471
+ else:
472
+ print(f"⚠️ Cancellation completed with warning: {cancel_result.get('error', 'Unknown')}")
473
+ # Re-raise to let Temporal know we're cancelled
474
+ raise
475
+
476
+ print("✅ Team Execution Completed!")
477
+ full_response = streaming_helper.get_full_response()
478
+ print(f" Response Length: {len(full_response)} chars\n")
479
+
480
+ logger.info(
481
+ "team_execution_completed",
482
+ execution_id=execution_id[:8],
483
+ response_length=len(full_response)
484
+ )
485
+
486
+ # Use the streamed response content
487
+ response_content = full_response if full_response else (result.content if hasattr(result, "content") else str(result))
488
+
489
+ # STEP 8: Extract usage metrics
490
+ usage = {}
491
+ if hasattr(result, "metrics") and result.metrics:
492
+ metrics = result.metrics
493
+ usage = {
494
+ "prompt_tokens": getattr(metrics, "input_tokens", 0),
495
+ "completion_tokens": getattr(metrics, "output_tokens", 0),
496
+ "total_tokens": getattr(metrics, "total_tokens", 0),
497
+ }
498
+ print(f"📊 Token Usage:")
499
+ print(f" Input: {usage.get('prompt_tokens', 0)}")
500
+ print(f" Output: {usage.get('completion_tokens', 0)}")
501
+ print(f" Total: {usage.get('total_tokens', 0)}\n")
502
+
503
+ # STEP 9: Persist complete session history
504
+ print("\n💾 Persisting session history to Control Plane...")
505
+
506
+ # Extract messages from result
507
+ new_messages = self.session_service.extract_messages_from_result(
508
+ result=result,
509
+ user_id=input.user_id
510
+ )
511
+
512
+ # Combine with previous history
513
+ complete_session = session_history + new_messages
514
+
515
+ if complete_session:
516
+ success = self.session_service.persist_session(
517
+ execution_id=execution_id,
518
+ session_id=input.session_id or execution_id,
519
+ user_id=input.user_id,
520
+ messages=complete_session,
521
+ metadata={
522
+ "team_id": input.team_id,
523
+ "organization_id": input.organization_id,
524
+ "turn_count": len(complete_session),
525
+ "member_count": len(team_members),
526
+ }
527
+ )
528
+
529
+ if success:
530
+ print(f" ✅ Session persisted ({len(complete_session)} total messages)")
531
+ else:
532
+ print(f" ⚠️ Session persistence failed")
533
+ else:
534
+ print(" ℹ️ No messages to persist")
535
+
536
+ print("\n" + "="*80)
537
+ print("🏁 TEAM EXECUTION END")
538
+ print("="*80 + "\n")
539
+
540
+ # STEP 10: Cleanup
541
+ self.cancellation_manager.unregister(execution_id)
542
+
543
+ return {
544
+ "success": True,
545
+ "response": response_content,
546
+ "usage": usage,
547
+ "model": model,
548
+ "finish_reason": "stop",
549
+ "team_member_count": len(team_members),
550
+ }
551
+
552
+ except Exception as e:
553
+ # Cleanup on error
554
+ self.cancellation_manager.unregister(execution_id)
555
+
556
+ print("\n" + "="*80)
557
+ print("❌ TEAM EXECUTION FAILED")
558
+ print("="*80)
559
+ print(f"Error: {str(e)}")
560
+ print("="*80 + "\n")
561
+
562
+ logger.error(
563
+ "team_execution_failed",
564
+ execution_id=execution_id[:8],
565
+ error=str(e)
566
+ )
567
+
568
+ return {
569
+ "success": False,
570
+ "error": str(e),
571
+ "model": input.model_id,
572
+ "usage": None,
573
+ "finish_reason": "error",
574
+ }