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.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses ā control_plane_api}/LICENSE +0 -0
- {kubiya_control_plane_api-0.1.0.dist-info ā kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
"""Team-related Temporal activities"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import httpx
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, List, Any, Dict
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from temporalio import activity
|
|
9
|
+
import structlog
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from agno.agent import Agent
|
|
13
|
+
from agno.team import Team
|
|
14
|
+
from agno.models.litellm import LiteLLM
|
|
15
|
+
from agno.tools.shell import ShellTools
|
|
16
|
+
from agno.tools.python import PythonTools
|
|
17
|
+
from agno.tools.file import FileTools
|
|
18
|
+
|
|
19
|
+
from control_plane_api.worker.activities.agent_activities import update_execution_status, ActivityUpdateExecutionInput
|
|
20
|
+
from control_plane_api.worker.control_plane_client import get_control_plane_client
|
|
21
|
+
|
|
22
|
+
logger = structlog.get_logger()
|
|
23
|
+
|
|
24
|
+
# Global registry for active Team instances to support cancellation
|
|
25
|
+
# Key: execution_id, Value: {team: Team, run_id: str}
|
|
26
|
+
_active_teams: Dict[str, Dict[str, Any]] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def instantiate_skill(skill_data: dict) -> Optional[Any]:
|
|
30
|
+
"""
|
|
31
|
+
Instantiate an Agno toolkit based on skill configuration from Control Plane.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
skill_data: Skill data from Control Plane API containing:
|
|
35
|
+
- type: Skill type (file_system, shell, python, docker, etc.)
|
|
36
|
+
- name: Skill name
|
|
37
|
+
- configuration: Dict with skill-specific config
|
|
38
|
+
- enabled: Whether skill is enabled
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Instantiated Agno toolkit or None if type not supported/enabled
|
|
42
|
+
"""
|
|
43
|
+
if not skill_data.get("enabled", True):
|
|
44
|
+
print(f" ā Skipping disabled skill: {skill_data.get('name')}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
skill_type = skill_data.get("type", "").lower()
|
|
48
|
+
config = skill_data.get("configuration", {})
|
|
49
|
+
name = skill_data.get("name", "Unknown")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Map Control Plane skill types to Agno toolkit classes
|
|
53
|
+
if skill_type in ["file_system", "file", "file_generation"]:
|
|
54
|
+
# FileTools: file operations (read, write, list, search)
|
|
55
|
+
# Note: file_generation is mapped to FileTools (save_file functionality)
|
|
56
|
+
base_dir = config.get("base_dir")
|
|
57
|
+
toolkit = FileTools(
|
|
58
|
+
base_dir=Path(base_dir) if base_dir else None,
|
|
59
|
+
enable_save_file=config.get("enable_save_file", True),
|
|
60
|
+
enable_read_file=config.get("enable_read_file", True),
|
|
61
|
+
enable_list_files=config.get("enable_list_files", True),
|
|
62
|
+
enable_search_files=config.get("enable_search_files", True),
|
|
63
|
+
)
|
|
64
|
+
print(f" ā Instantiated FileTools: {name}")
|
|
65
|
+
if skill_type == "file_generation":
|
|
66
|
+
print(f" - Type: File Generation (using FileTools.save_file)")
|
|
67
|
+
print(f" - Base Dir: {base_dir or 'Current directory'}")
|
|
68
|
+
print(f" - Read: {config.get('enable_read_file', True)}, Write: {config.get('enable_save_file', True)}")
|
|
69
|
+
return toolkit
|
|
70
|
+
|
|
71
|
+
elif skill_type in ["shell", "bash"]:
|
|
72
|
+
# ShellTools: shell command execution
|
|
73
|
+
base_dir = config.get("base_dir")
|
|
74
|
+
toolkit = ShellTools(
|
|
75
|
+
base_dir=Path(base_dir) if base_dir else None,
|
|
76
|
+
enable_run_shell_command=config.get("enable_run_shell_command", True),
|
|
77
|
+
)
|
|
78
|
+
print(f" ā Instantiated ShellTools: {name}")
|
|
79
|
+
print(f" - Base Dir: {base_dir or 'Current directory'}")
|
|
80
|
+
print(f" - Run Commands: {config.get('enable_run_shell_command', True)}")
|
|
81
|
+
return toolkit
|
|
82
|
+
|
|
83
|
+
elif skill_type == "python":
|
|
84
|
+
# PythonTools: Python code execution
|
|
85
|
+
base_dir = config.get("base_dir")
|
|
86
|
+
toolkit = PythonTools(
|
|
87
|
+
base_dir=Path(base_dir) if base_dir else None,
|
|
88
|
+
safe_globals=config.get("safe_globals"),
|
|
89
|
+
safe_locals=config.get("safe_locals"),
|
|
90
|
+
)
|
|
91
|
+
print(f" ā Instantiated PythonTools: {name}")
|
|
92
|
+
print(f" - Base Dir: {base_dir or 'Current directory'}")
|
|
93
|
+
return toolkit
|
|
94
|
+
|
|
95
|
+
elif skill_type == "docker":
|
|
96
|
+
# DockerTools requires docker package and running Docker daemon
|
|
97
|
+
try:
|
|
98
|
+
from agno.tools.docker import DockerTools
|
|
99
|
+
import docker
|
|
100
|
+
|
|
101
|
+
# Check if Docker daemon is accessible
|
|
102
|
+
try:
|
|
103
|
+
docker_client = docker.from_env()
|
|
104
|
+
docker_client.ping()
|
|
105
|
+
|
|
106
|
+
# Docker is available, instantiate toolkit
|
|
107
|
+
toolkit = DockerTools()
|
|
108
|
+
print(f" ā Instantiated DockerTools: {name}")
|
|
109
|
+
print(f" - Docker daemon: Connected")
|
|
110
|
+
docker_client.close()
|
|
111
|
+
return toolkit
|
|
112
|
+
|
|
113
|
+
except Exception as docker_error:
|
|
114
|
+
print(f" ā Docker daemon not available - skipping: {name}")
|
|
115
|
+
print(f" Error: {str(docker_error)}")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
except ImportError:
|
|
119
|
+
print(f" ā Docker skill requires 'docker' package - skipping: {name}")
|
|
120
|
+
print(f" Install with: pip install docker")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
print(f" ā Unsupported skill type '{skill_type}': {name}")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f" ā Error instantiating skill '{name}' (type: {skill_type}): {str(e)}")
|
|
129
|
+
logger.error(
|
|
130
|
+
f"Error instantiating skill",
|
|
131
|
+
extra={
|
|
132
|
+
"skill_name": name,
|
|
133
|
+
"skill_type": skill_type,
|
|
134
|
+
"error": str(e)
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class ActivityGetTeamAgentsInput:
|
|
142
|
+
"""Input for get_team_agents activity"""
|
|
143
|
+
team_id: str
|
|
144
|
+
organization_id: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class ActivityExecuteTeamInput:
|
|
149
|
+
"""Input for execute_team_coordination activity"""
|
|
150
|
+
execution_id: str
|
|
151
|
+
team_id: str
|
|
152
|
+
organization_id: str
|
|
153
|
+
prompt: str
|
|
154
|
+
system_prompt: Optional[str] = None
|
|
155
|
+
agents: List[dict] = None
|
|
156
|
+
team_config: dict = None
|
|
157
|
+
mcp_servers: dict = None # MCP servers configuration
|
|
158
|
+
session_id: Optional[str] = None # Session ID for Agno session management
|
|
159
|
+
user_id: Optional[str] = None # User ID for multi-user support
|
|
160
|
+
# Note: control_plane_url and api_key are read from worker environment variables (CONTROL_PLANE_URL, KUBIYA_API_KEY)
|
|
161
|
+
|
|
162
|
+
def __post_init__(self):
|
|
163
|
+
if self.agents is None:
|
|
164
|
+
self.agents = []
|
|
165
|
+
if self.team_config is None:
|
|
166
|
+
self.team_config = {}
|
|
167
|
+
if self.mcp_servers is None:
|
|
168
|
+
self.mcp_servers = {}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@activity.defn
|
|
172
|
+
async def get_team_agents(input: ActivityGetTeamAgentsInput) -> dict:
|
|
173
|
+
"""
|
|
174
|
+
Get all agents in a team via Control Plane API.
|
|
175
|
+
|
|
176
|
+
This activity fetches team details including member agents from the Control Plane.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
input: Activity input with team details
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dict with agents list
|
|
183
|
+
"""
|
|
184
|
+
print(f"\n\n=== GET_TEAM_AGENTS START ===")
|
|
185
|
+
print(f"team_id: {input.team_id} (type: {type(input.team_id).__name__})")
|
|
186
|
+
print(f"organization_id: {input.organization_id} (type: {type(input.organization_id).__name__})")
|
|
187
|
+
print(f"================================\n")
|
|
188
|
+
|
|
189
|
+
activity.logger.info(
|
|
190
|
+
f"[DEBUG] Getting team agents START",
|
|
191
|
+
extra={
|
|
192
|
+
"team_id": input.team_id,
|
|
193
|
+
"team_id_type": type(input.team_id).__name__,
|
|
194
|
+
"organization_id": input.organization_id,
|
|
195
|
+
"organization_id_type": type(input.organization_id).__name__,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Get Control Plane URL and Kubiya API key from environment
|
|
201
|
+
control_plane_url = os.getenv("CONTROL_PLANE_URL")
|
|
202
|
+
kubiya_api_key = os.getenv("KUBIYA_API_KEY")
|
|
203
|
+
|
|
204
|
+
if not control_plane_url:
|
|
205
|
+
raise ValueError("CONTROL_PLANE_URL environment variable not set")
|
|
206
|
+
if not kubiya_api_key:
|
|
207
|
+
raise ValueError("KUBIYA_API_KEY environment variable not set")
|
|
208
|
+
|
|
209
|
+
print(f"Fetching team from Control Plane API: {control_plane_url}")
|
|
210
|
+
|
|
211
|
+
# Call Control Plane API to get team with agents
|
|
212
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
213
|
+
response = await client.get(
|
|
214
|
+
f"{control_plane_url}/api/v1/teams/{input.team_id}",
|
|
215
|
+
headers={
|
|
216
|
+
"Authorization": f"Bearer {kubiya_api_key}",
|
|
217
|
+
"Content-Type": "application/json",
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if response.status_code == 404:
|
|
222
|
+
print(f"Team not found!")
|
|
223
|
+
activity.logger.error(
|
|
224
|
+
f"[DEBUG] Team not found",
|
|
225
|
+
extra={
|
|
226
|
+
"team_id": input.team_id,
|
|
227
|
+
"organization_id": input.organization_id,
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
return {"agents": [], "count": 0}
|
|
231
|
+
elif response.status_code != 200:
|
|
232
|
+
raise Exception(f"Failed to get team: {response.status_code} - {response.text}")
|
|
233
|
+
|
|
234
|
+
team_data = response.json()
|
|
235
|
+
|
|
236
|
+
# Extract agents from the API response
|
|
237
|
+
# The API returns a TeamWithAgentsResponse which includes the agents array
|
|
238
|
+
agents = team_data.get("agents", [])
|
|
239
|
+
|
|
240
|
+
print(f"Query executed. Agents found: {len(agents)}")
|
|
241
|
+
|
|
242
|
+
activity.logger.info(
|
|
243
|
+
f"[DEBUG] Query executed, processing results",
|
|
244
|
+
extra={
|
|
245
|
+
"agents_found": len(agents),
|
|
246
|
+
"agent_ids": [a.get("id") for a in agents],
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
print(f"Agents found: {len(agents)}")
|
|
251
|
+
if agents:
|
|
252
|
+
for agent in agents:
|
|
253
|
+
print(f" - {agent.get('name')} (ID: {agent.get('id')})")
|
|
254
|
+
|
|
255
|
+
activity.logger.info(
|
|
256
|
+
f"[DEBUG] Retrieved team agents via API",
|
|
257
|
+
extra={
|
|
258
|
+
"team_id": input.team_id,
|
|
259
|
+
"agent_count": len(agents),
|
|
260
|
+
"agent_names": [a.get("name") for a in agents],
|
|
261
|
+
"agent_ids": [a.get("id") for a in agents],
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not agents:
|
|
266
|
+
print(f"\n!!! NO AGENTS FOUND - Team may have no members !!!")
|
|
267
|
+
activity.logger.warning(
|
|
268
|
+
f"[DEBUG] WARNING: No agents found for team",
|
|
269
|
+
extra={
|
|
270
|
+
"team_id": input.team_id,
|
|
271
|
+
"organization_id": input.organization_id,
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
print(f"\n=== GET_TEAM_AGENTS END: Returning {len(agents)} agents ===\n\n")
|
|
276
|
+
return {
|
|
277
|
+
"agents": agents,
|
|
278
|
+
"count": len(agents),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"\n!!! EXCEPTION in get_team_agents: {type(e).__name__}: {str(e)} !!!\n")
|
|
283
|
+
activity.logger.error(
|
|
284
|
+
f"[DEBUG] EXCEPTION in get_team_agents",
|
|
285
|
+
extra={
|
|
286
|
+
"team_id": input.team_id,
|
|
287
|
+
"organization_id": input.organization_id,
|
|
288
|
+
"error": str(e),
|
|
289
|
+
"error_type": type(e).__name__,
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@activity.defn
|
|
296
|
+
async def execute_team_coordination(input: ActivityExecuteTeamInput) -> dict:
|
|
297
|
+
"""
|
|
298
|
+
Execute team coordination using Agno Teams.
|
|
299
|
+
|
|
300
|
+
This activity creates an Agno Team with member Agents and executes
|
|
301
|
+
the team run, allowing Agno to handle coordination.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
input: Activity input with team execution details
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dict with aggregated response, usage, success flag
|
|
308
|
+
"""
|
|
309
|
+
print("\n" + "="*80)
|
|
310
|
+
print("š TEAM EXECUTION START")
|
|
311
|
+
print("="*80)
|
|
312
|
+
print(f"Execution ID: {input.execution_id}")
|
|
313
|
+
print(f"Team ID: {input.team_id}")
|
|
314
|
+
print(f"Organization: {input.organization_id}")
|
|
315
|
+
print(f"Agent Count: {len(input.agents)}")
|
|
316
|
+
print(f"MCP Servers: {len(input.mcp_servers)} configured" if input.mcp_servers else "MCP Servers: None")
|
|
317
|
+
print(f"Session ID: {input.session_id}")
|
|
318
|
+
print(f"Prompt: {input.prompt[:100]}..." if len(input.prompt) > 100 else f"Prompt: {input.prompt}")
|
|
319
|
+
print("="*80 + "\n")
|
|
320
|
+
|
|
321
|
+
activity.logger.info(
|
|
322
|
+
f"Executing team coordination with Agno Teams",
|
|
323
|
+
extra={
|
|
324
|
+
"execution_id": input.execution_id,
|
|
325
|
+
"team_id": input.team_id,
|
|
326
|
+
"organization_id": input.organization_id,
|
|
327
|
+
"agent_count": len(input.agents),
|
|
328
|
+
"has_mcp_servers": bool(input.mcp_servers),
|
|
329
|
+
"mcp_server_count": len(input.mcp_servers) if input.mcp_servers else 0,
|
|
330
|
+
"mcp_server_ids": list(input.mcp_servers.keys()) if input.mcp_servers else [],
|
|
331
|
+
"session_id": input.session_id,
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
# Get Control Plane client for session management
|
|
337
|
+
control_plane = get_control_plane_client()
|
|
338
|
+
|
|
339
|
+
# STEP 1: Load existing session history from Control Plane (if this is a continuation)
|
|
340
|
+
# This enables conversation continuity across multiple execution turns
|
|
341
|
+
# IMPORTANT: Must be non-blocking with proper timeout/retry
|
|
342
|
+
session_history = []
|
|
343
|
+
if input.session_id:
|
|
344
|
+
print(f"\nš„ Loading session history from Control Plane...")
|
|
345
|
+
|
|
346
|
+
# Try up to 3 times with exponential backoff
|
|
347
|
+
max_retries = 3
|
|
348
|
+
for attempt in range(max_retries):
|
|
349
|
+
try:
|
|
350
|
+
if attempt > 0:
|
|
351
|
+
print(f" š Retry attempt {attempt + 1}/{max_retries}...")
|
|
352
|
+
|
|
353
|
+
session_data = control_plane.get_session(
|
|
354
|
+
execution_id=input.execution_id,
|
|
355
|
+
session_id=input.session_id
|
|
356
|
+
)
|
|
357
|
+
if session_data and session_data.get("messages"):
|
|
358
|
+
session_history = session_data["messages"]
|
|
359
|
+
print(f" ā
Loaded {len(session_history)} messages from previous turns")
|
|
360
|
+
|
|
361
|
+
activity.logger.info(
|
|
362
|
+
"Team session history loaded from Control Plane",
|
|
363
|
+
extra={
|
|
364
|
+
"execution_id": input.execution_id,
|
|
365
|
+
"session_id": input.session_id,
|
|
366
|
+
"message_count": len(session_history),
|
|
367
|
+
"attempt": attempt + 1,
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
break
|
|
371
|
+
else:
|
|
372
|
+
print(f" ā¹ļø No previous session found - starting new conversation")
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
except httpx.TimeoutException as e:
|
|
376
|
+
print(f" ā±ļø Timeout loading session (attempt {attempt + 1}/{max_retries})")
|
|
377
|
+
activity.logger.warning(
|
|
378
|
+
"Team session load timeout",
|
|
379
|
+
extra={"error": str(e), "execution_id": input.execution_id, "attempt": attempt + 1}
|
|
380
|
+
)
|
|
381
|
+
if attempt < max_retries - 1:
|
|
382
|
+
import time
|
|
383
|
+
time.sleep(2 ** attempt)
|
|
384
|
+
continue
|
|
385
|
+
else:
|
|
386
|
+
print(f" ā ļø Session load failed after {max_retries} attempts - continuing without history")
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
error_type = type(e).__name__
|
|
390
|
+
print(f" ā ļø Failed to load session history ({error_type}): {str(e)[:100]}")
|
|
391
|
+
activity.logger.warning(
|
|
392
|
+
"Failed to load team session history",
|
|
393
|
+
extra={
|
|
394
|
+
"error": str(e),
|
|
395
|
+
"error_type": error_type,
|
|
396
|
+
"execution_id": input.execution_id,
|
|
397
|
+
"attempt": attempt + 1
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
print(f" ā Continuing with {len(session_history)} messages in context\n")
|
|
403
|
+
|
|
404
|
+
# Get LiteLLM credentials from environment (set by worker from registration)
|
|
405
|
+
litellm_api_base = os.getenv("LITELLM_API_BASE", "https://llm-proxy.kubiya.ai")
|
|
406
|
+
litellm_api_key = os.getenv("LITELLM_API_KEY")
|
|
407
|
+
|
|
408
|
+
if not litellm_api_key:
|
|
409
|
+
raise ValueError("LITELLM_API_KEY environment variable not set")
|
|
410
|
+
|
|
411
|
+
# Get Control Plane URL and API key from environment (worker has these set on startup)
|
|
412
|
+
control_plane_url = os.getenv("CONTROL_PLANE_URL")
|
|
413
|
+
api_key = os.getenv("KUBIYA_API_KEY")
|
|
414
|
+
|
|
415
|
+
# Fetch resolved skills from Control Plane if available
|
|
416
|
+
skills = []
|
|
417
|
+
if control_plane_url and api_key and input.team_id:
|
|
418
|
+
print(f"š§ Fetching skills for TEAM from Control Plane...")
|
|
419
|
+
try:
|
|
420
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
421
|
+
response = await client.get(
|
|
422
|
+
f"{control_plane_url}/api/v1/skills/associations/teams/{input.team_id}/skills/resolved",
|
|
423
|
+
headers={"Authorization": f"Bearer {api_key}"}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if response.status_code == 200:
|
|
427
|
+
skills = response.json()
|
|
428
|
+
print(f"ā
Resolved {len(skills)} skills from Control Plane for TEAM")
|
|
429
|
+
print(f" Skill Types: {[t.get('type') for t in skills]}")
|
|
430
|
+
print(f" Skill Sources: {[t.get('source') for t in skills]}")
|
|
431
|
+
print(f" Skill Names: {[t.get('name') for t in skills]}\n")
|
|
432
|
+
|
|
433
|
+
activity.logger.info(
|
|
434
|
+
f"Resolved skills for team from Control Plane",
|
|
435
|
+
extra={
|
|
436
|
+
"team_id": input.team_id,
|
|
437
|
+
"skill_count": len(skills),
|
|
438
|
+
"skill_types": [t.get("type") for t in skills],
|
|
439
|
+
"skill_sources": [t.get("source") for t in skills],
|
|
440
|
+
"skill_names": [t.get("name") for t in skills],
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
print(f"ā ļø Failed to fetch skills for team: HTTP {response.status_code}")
|
|
445
|
+
print(f" Response: {response.text[:200]}\n")
|
|
446
|
+
activity.logger.warning(
|
|
447
|
+
f"Failed to fetch skills for team from Control Plane: {response.status_code}",
|
|
448
|
+
extra={
|
|
449
|
+
"status_code": response.status_code,
|
|
450
|
+
"response_text": response.text[:500]
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
except Exception as e:
|
|
454
|
+
print(f"ā Error fetching skills for team: {str(e)}\n")
|
|
455
|
+
activity.logger.error(
|
|
456
|
+
f"Error fetching skills for team from Control Plane: {str(e)}",
|
|
457
|
+
extra={"error": str(e)}
|
|
458
|
+
)
|
|
459
|
+
# Continue execution without skills
|
|
460
|
+
else:
|
|
461
|
+
print(f"ā¹ļø No Control Plane URL/API key in environment for team - skipping skill resolution\n")
|
|
462
|
+
|
|
463
|
+
# Instantiate Agno toolkits from Control Plane skills
|
|
464
|
+
print(f"\nš§ Instantiating Skills:")
|
|
465
|
+
agno_toolkits = []
|
|
466
|
+
if skills:
|
|
467
|
+
for skill in skills:
|
|
468
|
+
toolkit = instantiate_skill(skill)
|
|
469
|
+
if toolkit:
|
|
470
|
+
agno_toolkits.append(toolkit)
|
|
471
|
+
|
|
472
|
+
if agno_toolkits:
|
|
473
|
+
print(f"\nā
Successfully instantiated {len(agno_toolkits)} skill(s)")
|
|
474
|
+
else:
|
|
475
|
+
print(f"\nā¹ļø No skills instantiated\n")
|
|
476
|
+
|
|
477
|
+
print(f"š¦ Total Tools Available:")
|
|
478
|
+
print(f" MCP Servers: {len(input.mcp_servers)}")
|
|
479
|
+
print(f" OS-Level Skills: {len(agno_toolkits)}\n")
|
|
480
|
+
|
|
481
|
+
# Create Agno Agent objects for each team member
|
|
482
|
+
print("\nš Creating Team Members:")
|
|
483
|
+
member_agents = []
|
|
484
|
+
for i, agent_data in enumerate(input.agents, 1):
|
|
485
|
+
# Get model ID (default to kubiya/claude-sonnet-4 if not specified)
|
|
486
|
+
model_id = agent_data.get("model_id") or "kubiya/claude-sonnet-4"
|
|
487
|
+
|
|
488
|
+
print(f" {i}. {agent_data['name']}")
|
|
489
|
+
print(f" Model: {model_id}")
|
|
490
|
+
print(f" Role: {agent_data.get('description', agent_data['name'])[:60]}...")
|
|
491
|
+
|
|
492
|
+
# Create Agno Agent with explicit LiteLLM proxy configuration
|
|
493
|
+
# IMPORTANT: Use openai/ prefix for custom proxy compatibility
|
|
494
|
+
member_agent = Agent(
|
|
495
|
+
name=agent_data["name"],
|
|
496
|
+
role=agent_data.get("description", agent_data["name"]),
|
|
497
|
+
model=LiteLLM(
|
|
498
|
+
id=f"openai/{model_id}", # e.g., "openai/kubiya/claude-sonnet-4"
|
|
499
|
+
api_base=litellm_api_base,
|
|
500
|
+
api_key=litellm_api_key,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
member_agents.append(member_agent)
|
|
504
|
+
|
|
505
|
+
activity.logger.info(
|
|
506
|
+
f"Created Agno Agent",
|
|
507
|
+
extra={
|
|
508
|
+
"agent_name": agent_data["name"],
|
|
509
|
+
"model": model_id,
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Create Agno Team with member agents and LiteLLM model for coordination
|
|
514
|
+
# Get coordinator model from team configuration (if specified by user in UI)
|
|
515
|
+
# Falls back to default if not configured
|
|
516
|
+
team_model = (
|
|
517
|
+
input.team_config.get("llm", {}).get("model")
|
|
518
|
+
or "kubiya/claude-sonnet-4" # Default coordinator model
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
print(f"\nš¤ Creating Agno Team:")
|
|
522
|
+
print(f" Coordinator Model: {team_model}")
|
|
523
|
+
print(f" Members: {len(member_agents)}")
|
|
524
|
+
print(f" Skills: {len(agno_toolkits)}")
|
|
525
|
+
|
|
526
|
+
# Send heartbeat: Creating team
|
|
527
|
+
activity.heartbeat({"status": "Creating team with agents and skills..."})
|
|
528
|
+
|
|
529
|
+
# Track tool executions for real-time streaming
|
|
530
|
+
tool_execution_messages = []
|
|
531
|
+
|
|
532
|
+
# Create tool hook to capture tool execution for real-time streaming
|
|
533
|
+
# Agno inspects the signature and passes matching parameters
|
|
534
|
+
def tool_hook(name: str = None, function_name: str = None, function=None, arguments: dict = None, **kwargs):
|
|
535
|
+
"""Hook to capture tool execution and add to messages for streaming
|
|
536
|
+
|
|
537
|
+
Agno passes these parameters based on our signature:
|
|
538
|
+
- name or function_name: The tool function name
|
|
539
|
+
- function: The callable being executed (this is the NEXT function in the chain)
|
|
540
|
+
- arguments: Dict of arguments passed to the tool
|
|
541
|
+
|
|
542
|
+
The hook must CALL the function and return its result.
|
|
543
|
+
"""
|
|
544
|
+
# Get tool name from Agno's parameters
|
|
545
|
+
tool_name = name or function_name or "unknown"
|
|
546
|
+
tool_args = arguments or {}
|
|
547
|
+
|
|
548
|
+
# Generate unique tool execution ID (tool_name + timestamp)
|
|
549
|
+
import time
|
|
550
|
+
tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
|
|
551
|
+
|
|
552
|
+
print(f" š§ Tool Starting: {tool_name} (ID: {tool_execution_id})")
|
|
553
|
+
if tool_args:
|
|
554
|
+
args_preview = str(tool_args)[:200]
|
|
555
|
+
print(f" Args: {args_preview}{'...' if len(str(tool_args)) > 200 else ''}")
|
|
556
|
+
|
|
557
|
+
# Publish streaming event to Control Plane (real-time UI update)
|
|
558
|
+
control_plane.publish_event(
|
|
559
|
+
execution_id=input.execution_id,
|
|
560
|
+
event_type="tool_started",
|
|
561
|
+
data={
|
|
562
|
+
"tool_name": tool_name,
|
|
563
|
+
"tool_execution_id": tool_execution_id, # Unique ID for this execution
|
|
564
|
+
"tool_arguments": tool_args,
|
|
565
|
+
"message": f"š§ Executing tool: {tool_name}",
|
|
566
|
+
}
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
tool_execution_messages.append({
|
|
570
|
+
"role": "system",
|
|
571
|
+
"content": f"š§ Executing tool: **{tool_name}**",
|
|
572
|
+
"tool_name": tool_name,
|
|
573
|
+
"tool_event": "started",
|
|
574
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
# CRITICAL: Actually call the function and handle completion
|
|
578
|
+
result = None
|
|
579
|
+
error = None
|
|
580
|
+
try:
|
|
581
|
+
# Call the actual function (next in the hook chain)
|
|
582
|
+
if function and callable(function):
|
|
583
|
+
result = function(**tool_args) if tool_args else function()
|
|
584
|
+
else:
|
|
585
|
+
raise ValueError(f"Function not callable: {function}")
|
|
586
|
+
|
|
587
|
+
status = "success"
|
|
588
|
+
icon = "ā
"
|
|
589
|
+
print(f" {icon} Tool Success: {tool_name}")
|
|
590
|
+
|
|
591
|
+
except Exception as e:
|
|
592
|
+
error = e
|
|
593
|
+
status = "failed"
|
|
594
|
+
icon = "ā"
|
|
595
|
+
print(f" {icon} Tool Failed: {tool_name} - {str(e)}")
|
|
596
|
+
|
|
597
|
+
# Publish completion event to Control Plane (real-time UI update)
|
|
598
|
+
control_plane.publish_event(
|
|
599
|
+
execution_id=input.execution_id,
|
|
600
|
+
event_type="tool_completed",
|
|
601
|
+
data={
|
|
602
|
+
"tool_name": tool_name,
|
|
603
|
+
"tool_execution_id": tool_execution_id, # Same ID to match the started event
|
|
604
|
+
"status": status,
|
|
605
|
+
"error": str(error) if error else None,
|
|
606
|
+
"tool_output": result if result is not None else None, # Include tool output for UI display
|
|
607
|
+
"message": f"{icon} Tool {status}: {tool_name}",
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
tool_execution_messages.append({
|
|
612
|
+
"role": "system",
|
|
613
|
+
"content": f"{icon} Tool {status}: **{tool_name}**",
|
|
614
|
+
"tool_name": tool_name,
|
|
615
|
+
"tool_event": "completed",
|
|
616
|
+
"tool_status": status,
|
|
617
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
# If there was an error, re-raise it so Agno knows the tool failed
|
|
621
|
+
if error:
|
|
622
|
+
raise error
|
|
623
|
+
|
|
624
|
+
# Return the result to continue the chain
|
|
625
|
+
return result
|
|
626
|
+
|
|
627
|
+
# Create PERSISTENT database for Team based on session_id
|
|
628
|
+
# This allows Agno to automatically manage conversation history across turns
|
|
629
|
+
# Database persists across executions within the same session
|
|
630
|
+
from agno.db.sqlite import SqliteDb
|
|
631
|
+
import tempfile
|
|
632
|
+
|
|
633
|
+
# Use session_id (not execution_id) for persistent database across conversation turns
|
|
634
|
+
session_id_for_db = input.session_id or input.execution_id
|
|
635
|
+
db_path = os.path.join(tempfile.gettempdir(), f"team_session_{session_id_for_db}.db")
|
|
636
|
+
team_db = SqliteDb(db_file=db_path)
|
|
637
|
+
|
|
638
|
+
print(f"š Using persistent team database: {db_path}")
|
|
639
|
+
|
|
640
|
+
# Create Team with openai/ prefix for custom proxy compatibility
|
|
641
|
+
team = Team(
|
|
642
|
+
members=member_agents,
|
|
643
|
+
name=f"Team {input.team_id}",
|
|
644
|
+
model=LiteLLM(
|
|
645
|
+
id=f"openai/{team_model}", # e.g., "openai/kubiya/claude-sonnet-4"
|
|
646
|
+
api_base=litellm_api_base,
|
|
647
|
+
api_key=litellm_api_key,
|
|
648
|
+
),
|
|
649
|
+
tools=agno_toolkits if agno_toolkits else None, # Add skills to team
|
|
650
|
+
tool_hooks=[tool_hook], # Add hook for real-time tool updates
|
|
651
|
+
db=team_db, # PERSISTENT database per session
|
|
652
|
+
add_history_to_context=True, # Agno automatically adds conversation history from DB
|
|
653
|
+
num_history_runs=10, # Include last 10 turns in context
|
|
654
|
+
share_member_interactions=True, # Members see each other's work during current run
|
|
655
|
+
store_member_responses=True, # Enable member responses to be stored
|
|
656
|
+
show_members_responses=True, # Show member responses in logs
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
print(f" š Team configured with automatic history (last 10 runs)\n")
|
|
660
|
+
|
|
661
|
+
# Register team for cancellation support
|
|
662
|
+
_active_teams[input.execution_id] = {
|
|
663
|
+
"team": team,
|
|
664
|
+
"run_id": None, # Will be set when run starts
|
|
665
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
666
|
+
}
|
|
667
|
+
print(f"ā
Team registered for cancellation support (execution_id: {input.execution_id})\n")
|
|
668
|
+
|
|
669
|
+
activity.logger.info(
|
|
670
|
+
f"Created Agno Team with {len(member_agents)} members",
|
|
671
|
+
extra={
|
|
672
|
+
"coordinator_model": team_model,
|
|
673
|
+
"member_count": len(member_agents),
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Cache execution metadata in Redis for fast SSE lookups (avoid DB queries)
|
|
678
|
+
control_plane = get_control_plane_client()
|
|
679
|
+
control_plane.cache_metadata(input.execution_id, "TEAM")
|
|
680
|
+
|
|
681
|
+
# Execute team run with streaming in a thread pool
|
|
682
|
+
# This prevents blocking the async event loop in Temporal
|
|
683
|
+
print("\nā” Executing Team Run with Streaming...")
|
|
684
|
+
print(f" Prompt: {input.prompt}\n")
|
|
685
|
+
|
|
686
|
+
# Send heartbeat: Starting execution
|
|
687
|
+
activity.heartbeat({"status": "Team is processing your request..."})
|
|
688
|
+
|
|
689
|
+
import asyncio
|
|
690
|
+
|
|
691
|
+
# Stream the response and collect chunks + tool messages
|
|
692
|
+
response_chunks = []
|
|
693
|
+
full_response = ""
|
|
694
|
+
|
|
695
|
+
# Generate unique message ID for this turn (execution_id + timestamp)
|
|
696
|
+
import time
|
|
697
|
+
message_id = f"{input.execution_id}_{int(time.time() * 1000000)}"
|
|
698
|
+
|
|
699
|
+
def stream_team_run():
|
|
700
|
+
"""Run team with streaming and collect response + member events"""
|
|
701
|
+
nonlocal full_response, message_id
|
|
702
|
+
try:
|
|
703
|
+
# Run with streaming enabled AND stream_events to capture member events
|
|
704
|
+
# Agno Team automatically loads conversation history from persistent database
|
|
705
|
+
# via add_history_to_context=True and num_history_runs=10
|
|
706
|
+
run_kwargs = {
|
|
707
|
+
"stream": True,
|
|
708
|
+
"stream_events": True, # CRITICAL: Stream ALL events including member events
|
|
709
|
+
"stream_member_events": True, # Stream member agent events
|
|
710
|
+
"session_id": input.session_id or input.execution_id,
|
|
711
|
+
}
|
|
712
|
+
if input.user_id:
|
|
713
|
+
run_kwargs["user_id"] = input.user_id
|
|
714
|
+
|
|
715
|
+
print(f" š Agno will automatically load conversation history from persistent database\n")
|
|
716
|
+
run_response = team.run(input.prompt, **run_kwargs)
|
|
717
|
+
|
|
718
|
+
# Track member message IDs for streaming
|
|
719
|
+
member_message_ids = {}
|
|
720
|
+
active_streaming_member = None # Track which member is currently streaming
|
|
721
|
+
tool_execution_ids = {} # Track tool_execution_id to match start/complete events (key: tool_name_timestamp)
|
|
722
|
+
run_id_published = False # Track if we've captured and published run_id
|
|
723
|
+
|
|
724
|
+
# Iterate over streaming events (not just chunks)
|
|
725
|
+
for event in run_response:
|
|
726
|
+
# Capture and publish run_id from first event for cancellation support
|
|
727
|
+
if not run_id_published and hasattr(event, 'run_id') and event.run_id:
|
|
728
|
+
agno_run_id = event.run_id
|
|
729
|
+
print(f"\nš Agno run_id: {agno_run_id}")
|
|
730
|
+
|
|
731
|
+
# Store run_id in registry for cancellation
|
|
732
|
+
if input.execution_id in _active_teams:
|
|
733
|
+
_active_teams[input.execution_id]["run_id"] = agno_run_id
|
|
734
|
+
|
|
735
|
+
# Publish run_id to Redis for Control Plane cancellation access
|
|
736
|
+
# This allows users to cancel via STOP button in UI
|
|
737
|
+
control_plane.publish_event(
|
|
738
|
+
execution_id=input.execution_id,
|
|
739
|
+
event_type="run_started",
|
|
740
|
+
data={
|
|
741
|
+
"run_id": agno_run_id,
|
|
742
|
+
"team_id": input.team_id,
|
|
743
|
+
"cancellable": True,
|
|
744
|
+
}
|
|
745
|
+
)
|
|
746
|
+
run_id_published = True
|
|
747
|
+
|
|
748
|
+
event_type = getattr(event, 'event', None)
|
|
749
|
+
|
|
750
|
+
# Handle TEAM LEADER content chunks
|
|
751
|
+
if event_type == "TeamRunContent":
|
|
752
|
+
# If a member was streaming, mark them as complete (leader took control back)
|
|
753
|
+
if active_streaming_member and active_streaming_member in member_message_ids:
|
|
754
|
+
control_plane.publish_event(
|
|
755
|
+
execution_id=input.execution_id,
|
|
756
|
+
event_type="member_message_complete",
|
|
757
|
+
data={
|
|
758
|
+
"message_id": member_message_ids[active_streaming_member],
|
|
759
|
+
"member_name": active_streaming_member,
|
|
760
|
+
"source": "team_member",
|
|
761
|
+
}
|
|
762
|
+
)
|
|
763
|
+
active_streaming_member = None
|
|
764
|
+
if hasattr(event, 'content') and event.content:
|
|
765
|
+
content = str(event.content)
|
|
766
|
+
full_response += content
|
|
767
|
+
response_chunks.append(content)
|
|
768
|
+
print(content, end='', flush=True)
|
|
769
|
+
|
|
770
|
+
# Stream team leader chunk to Control Plane
|
|
771
|
+
control_plane.publish_event(
|
|
772
|
+
execution_id=input.execution_id,
|
|
773
|
+
event_type="message_chunk",
|
|
774
|
+
data={
|
|
775
|
+
"role": "assistant",
|
|
776
|
+
"content": content,
|
|
777
|
+
"is_chunk": True,
|
|
778
|
+
"message_id": message_id, # Team leader message ID
|
|
779
|
+
"source": "team_leader",
|
|
780
|
+
}
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
# Handle MEMBER content chunks (from team members)
|
|
784
|
+
elif event_type == "RunContent":
|
|
785
|
+
# Member agent content chunk
|
|
786
|
+
member_name = getattr(event, 'agent_name', getattr(event, 'member_name', 'Unknown Member'))
|
|
787
|
+
|
|
788
|
+
# If switching to a different member, mark the previous one as complete
|
|
789
|
+
if active_streaming_member and active_streaming_member != member_name and active_streaming_member in member_message_ids:
|
|
790
|
+
control_plane.publish_event(
|
|
791
|
+
execution_id=input.execution_id,
|
|
792
|
+
event_type="member_message_complete",
|
|
793
|
+
data={
|
|
794
|
+
"message_id": member_message_ids[active_streaming_member],
|
|
795
|
+
"member_name": active_streaming_member,
|
|
796
|
+
"source": "team_member",
|
|
797
|
+
}
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# Generate unique message ID for this member
|
|
801
|
+
if member_name not in member_message_ids:
|
|
802
|
+
member_message_ids[member_name] = f"{input.execution_id}_{member_name}_{int(time.time() * 1000000)}"
|
|
803
|
+
# Print member name header once when they start
|
|
804
|
+
print(f"\n[{member_name}] ", end='', flush=True)
|
|
805
|
+
|
|
806
|
+
# Track that this member is now actively streaming
|
|
807
|
+
active_streaming_member = member_name
|
|
808
|
+
|
|
809
|
+
if hasattr(event, 'content') and event.content:
|
|
810
|
+
content = str(event.content)
|
|
811
|
+
# Print content without the repeated member name prefix
|
|
812
|
+
print(content, end='', flush=True)
|
|
813
|
+
|
|
814
|
+
# Stream member chunk to Control Plane
|
|
815
|
+
control_plane.publish_event(
|
|
816
|
+
execution_id=input.execution_id,
|
|
817
|
+
event_type="member_message_chunk",
|
|
818
|
+
data={
|
|
819
|
+
"role": "assistant",
|
|
820
|
+
"content": content,
|
|
821
|
+
"is_chunk": True,
|
|
822
|
+
"message_id": member_message_ids[member_name],
|
|
823
|
+
"source": "team_member",
|
|
824
|
+
"member_name": member_name,
|
|
825
|
+
}
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Handle tool calls (team leader or members)
|
|
829
|
+
elif event_type in ["TeamToolCallStarted", "ToolCallStarted"]:
|
|
830
|
+
# Extract tool name properly (event.tool might be a ToolExecution object)
|
|
831
|
+
tool_obj = getattr(event, 'tool', None)
|
|
832
|
+
if tool_obj and hasattr(tool_obj, 'tool_name'):
|
|
833
|
+
# It's a ToolExecution object
|
|
834
|
+
tool_name = tool_obj.tool_name
|
|
835
|
+
tool_args = getattr(tool_obj, 'tool_args', {})
|
|
836
|
+
else:
|
|
837
|
+
# Fallback to string name
|
|
838
|
+
tool_name = str(tool_obj) if tool_obj else getattr(event, 'tool_name', 'unknown')
|
|
839
|
+
tool_args = {}
|
|
840
|
+
|
|
841
|
+
is_member_tool = event_type == "ToolCallStarted"
|
|
842
|
+
member_name = getattr(event, 'agent_name', getattr(event, 'member_name', None)) if is_member_tool else None
|
|
843
|
+
|
|
844
|
+
# Generate unique tool_execution_id and message_id
|
|
845
|
+
tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
|
|
846
|
+
message_id = f"{input.execution_id}_tool_{tool_execution_id}"
|
|
847
|
+
|
|
848
|
+
# Store the tool_execution_id so we can match it with the completion event
|
|
849
|
+
# Use a composite key to handle multiple tools with same name
|
|
850
|
+
tool_key = f"{member_name or 'leader'}_{tool_name}_{int(time.time())}"
|
|
851
|
+
tool_execution_ids[tool_key] = {
|
|
852
|
+
"tool_execution_id": tool_execution_id,
|
|
853
|
+
"message_id": message_id,
|
|
854
|
+
"tool_name": tool_name,
|
|
855
|
+
"member_name": member_name,
|
|
856
|
+
"parent_message_id": member_message_ids.get(member_name) if is_member_tool and member_name else None,
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
print(f"\n š§ Tool Starting: {tool_name} (ID: {tool_execution_id})")
|
|
860
|
+
if tool_args:
|
|
861
|
+
args_preview = str(tool_args)[:200]
|
|
862
|
+
print(f" Args: {args_preview}{'...' if len(str(tool_args)) > 200 else ''}")
|
|
863
|
+
|
|
864
|
+
control_plane.publish_event(
|
|
865
|
+
execution_id=input.execution_id,
|
|
866
|
+
event_type="tool_started" if not is_member_tool else "member_tool_started",
|
|
867
|
+
data={
|
|
868
|
+
"tool_name": tool_name,
|
|
869
|
+
"tool_execution_id": tool_execution_id,
|
|
870
|
+
"message_id": message_id,
|
|
871
|
+
"tool_arguments": tool_args if tool_args else None,
|
|
872
|
+
"source": "team_member" if is_member_tool else "team_leader",
|
|
873
|
+
"member_name": member_name,
|
|
874
|
+
"parent_message_id": member_message_ids.get(member_name) if is_member_tool and member_name else None,
|
|
875
|
+
"message": f"š§ Executing tool: {tool_name}",
|
|
876
|
+
}
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
elif event_type in ["TeamToolCallCompleted", "ToolCallCompleted", "TeamToolCallFailed", "ToolCallFailed"]:
|
|
880
|
+
# Extract tool name properly (event.tool might be a ToolExecution object)
|
|
881
|
+
tool_obj = getattr(event, 'tool', None)
|
|
882
|
+
if tool_obj and hasattr(tool_obj, 'tool_name'):
|
|
883
|
+
# It's a ToolExecution object
|
|
884
|
+
tool_name = tool_obj.tool_name
|
|
885
|
+
tool_output = getattr(tool_obj, 'result', None) or getattr(event, 'result', None)
|
|
886
|
+
else:
|
|
887
|
+
# Fallback to string name
|
|
888
|
+
tool_name = str(tool_obj) if tool_obj else getattr(event, 'tool_name', 'unknown')
|
|
889
|
+
tool_output = getattr(event, 'result', None)
|
|
890
|
+
|
|
891
|
+
is_member_tool = event_type in ["ToolCallCompleted", "ToolCallFailed"]
|
|
892
|
+
member_name = getattr(event, 'agent_name', getattr(event, 'member_name', None)) if is_member_tool else None
|
|
893
|
+
|
|
894
|
+
# Determine if this is a failure event
|
|
895
|
+
is_failure = event_type in ["TeamToolCallFailed", "ToolCallFailed"]
|
|
896
|
+
tool_error = getattr(event, 'error', None) if is_failure else None
|
|
897
|
+
|
|
898
|
+
# Find the stored tool info from the start event
|
|
899
|
+
tool_key_pattern = f"{member_name or 'leader'}_{tool_name}"
|
|
900
|
+
matching_tool = None
|
|
901
|
+
for key, tool_info in list(tool_execution_ids.items()):
|
|
902
|
+
if key.startswith(tool_key_pattern):
|
|
903
|
+
matching_tool = tool_info
|
|
904
|
+
# Remove from tracking dict
|
|
905
|
+
del tool_execution_ids[key]
|
|
906
|
+
break
|
|
907
|
+
|
|
908
|
+
if matching_tool:
|
|
909
|
+
tool_execution_id = matching_tool["tool_execution_id"]
|
|
910
|
+
message_id = matching_tool["message_id"]
|
|
911
|
+
parent_message_id = matching_tool["parent_message_id"]
|
|
912
|
+
else:
|
|
913
|
+
# Fallback if start event wasn't captured
|
|
914
|
+
tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
|
|
915
|
+
message_id = f"{input.execution_id}_tool_{tool_execution_id}"
|
|
916
|
+
parent_message_id = member_message_ids.get(member_name) if is_member_tool and member_name else None
|
|
917
|
+
print(f" ā ļø Warning: Tool completion without matching start event: {tool_name}")
|
|
918
|
+
|
|
919
|
+
status = "failed" if is_failure else "success"
|
|
920
|
+
icon = "ā" if is_failure else "ā
"
|
|
921
|
+
print(f"\n {icon} Tool {status.capitalize()}: {tool_name}")
|
|
922
|
+
if tool_error:
|
|
923
|
+
print(f" Error: {str(tool_error)[:200]}")
|
|
924
|
+
|
|
925
|
+
control_plane.publish_event(
|
|
926
|
+
execution_id=input.execution_id,
|
|
927
|
+
event_type="tool_completed" if not is_member_tool else "member_tool_completed",
|
|
928
|
+
data={
|
|
929
|
+
"tool_name": tool_name,
|
|
930
|
+
"tool_execution_id": tool_execution_id,
|
|
931
|
+
"message_id": message_id,
|
|
932
|
+
"status": status,
|
|
933
|
+
"tool_output": str(tool_output)[:1000] if tool_output else None, # Limit output size
|
|
934
|
+
"tool_error": str(tool_error) if tool_error else None,
|
|
935
|
+
"source": "team_member" if is_member_tool else "team_leader",
|
|
936
|
+
"member_name": member_name,
|
|
937
|
+
"parent_message_id": parent_message_id,
|
|
938
|
+
"message": f"{icon} Tool {status}: {tool_name}",
|
|
939
|
+
}
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# Rotate message_id after tool completion so subsequent responses are grouped separately
|
|
943
|
+
# This helps the UI show responses before and after tool execution as distinct sections
|
|
944
|
+
if is_member_tool and member_name and member_name in member_message_ids:
|
|
945
|
+
# Mark the previous message_id as complete before rotating
|
|
946
|
+
old_message_id = member_message_ids[member_name]
|
|
947
|
+
control_plane.publish_event(
|
|
948
|
+
execution_id=input.execution_id,
|
|
949
|
+
event_type="member_message_complete",
|
|
950
|
+
data={
|
|
951
|
+
"message_id": old_message_id,
|
|
952
|
+
"member_name": member_name,
|
|
953
|
+
"source": "team_member",
|
|
954
|
+
}
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
# Generate new message_id for this member's next response
|
|
958
|
+
member_message_ids[member_name] = f"{input.execution_id}_{member_name}_{int(time.time() * 1000000)}"
|
|
959
|
+
print(f" š Rotated message_id for {member_name}")
|
|
960
|
+
|
|
961
|
+
# Handle reasoning events (if model supports reasoning)
|
|
962
|
+
elif event_type in ["TeamReasoningStep", "ReasoningStep"]:
|
|
963
|
+
if hasattr(event, 'content') and event.content:
|
|
964
|
+
reasoning_content = str(event.content)
|
|
965
|
+
is_member = event_type == "ReasoningStep"
|
|
966
|
+
member_name = getattr(event, 'agent_name', getattr(event, 'member_name', None)) if is_member else None
|
|
967
|
+
|
|
968
|
+
print(f"\n š {'[' + member_name + '] ' if member_name else ''}Reasoning: {reasoning_content[:100]}...")
|
|
969
|
+
|
|
970
|
+
control_plane.publish_event(
|
|
971
|
+
execution_id=input.execution_id,
|
|
972
|
+
event_type="reasoning_step",
|
|
973
|
+
data={
|
|
974
|
+
"content": reasoning_content,
|
|
975
|
+
"source": "team_member" if is_member else "team_leader",
|
|
976
|
+
"member_name": member_name,
|
|
977
|
+
}
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
# Mark any remaining active member as complete (stream ended)
|
|
981
|
+
if active_streaming_member and active_streaming_member in member_message_ids:
|
|
982
|
+
control_plane.publish_event(
|
|
983
|
+
execution_id=input.execution_id,
|
|
984
|
+
event_type="member_message_complete",
|
|
985
|
+
data={
|
|
986
|
+
"message_id": member_message_ids[active_streaming_member],
|
|
987
|
+
"member_name": active_streaming_member,
|
|
988
|
+
"source": "team_member",
|
|
989
|
+
}
|
|
990
|
+
)
|
|
991
|
+
print(f"\n ā {active_streaming_member} completed")
|
|
992
|
+
|
|
993
|
+
print() # New line after streaming
|
|
994
|
+
|
|
995
|
+
# Return the iterator's final result
|
|
996
|
+
return run_response
|
|
997
|
+
except Exception as e:
|
|
998
|
+
print(f"\nā Streaming error: {str(e)}")
|
|
999
|
+
import traceback
|
|
1000
|
+
traceback.print_exc()
|
|
1001
|
+
# Fall back to non-streaming
|
|
1002
|
+
run_kwargs_fallback = {
|
|
1003
|
+
"stream": False,
|
|
1004
|
+
"session_id": input.session_id or input.execution_id,
|
|
1005
|
+
}
|
|
1006
|
+
if input.user_id:
|
|
1007
|
+
run_kwargs_fallback["user_id"] = input.user_id
|
|
1008
|
+
if conversation_context:
|
|
1009
|
+
run_kwargs_fallback["messages"] = conversation_context
|
|
1010
|
+
return team.run(input.prompt, **run_kwargs_fallback)
|
|
1011
|
+
|
|
1012
|
+
# Execute in thread pool (NO TIMEOUT - tasks can run as long as needed)
|
|
1013
|
+
# Control Plane can cancel via Agno's cancel_run API if user requests it
|
|
1014
|
+
result = await asyncio.to_thread(stream_team_run)
|
|
1015
|
+
|
|
1016
|
+
# Send heartbeat: Completed
|
|
1017
|
+
activity.heartbeat({"status": "Team execution completed, preparing response..."})
|
|
1018
|
+
|
|
1019
|
+
print("\nā
Team Execution Completed!")
|
|
1020
|
+
print(f" Response Length: {len(full_response)} chars")
|
|
1021
|
+
|
|
1022
|
+
activity.logger.info(
|
|
1023
|
+
f"Agno Team execution completed",
|
|
1024
|
+
extra={
|
|
1025
|
+
"execution_id": input.execution_id,
|
|
1026
|
+
"has_content": bool(full_response),
|
|
1027
|
+
}
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
# Use the streamed response content
|
|
1031
|
+
response_content = full_response if full_response else (result.content if hasattr(result, "content") else str(result))
|
|
1032
|
+
|
|
1033
|
+
# Extract tool call messages for UI streaming
|
|
1034
|
+
tool_messages = []
|
|
1035
|
+
if hasattr(result, "messages") and result.messages:
|
|
1036
|
+
for msg in result.messages:
|
|
1037
|
+
# Check if message has tool calls
|
|
1038
|
+
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
|
1039
|
+
for tool_call in msg.tool_calls:
|
|
1040
|
+
tool_name = getattr(tool_call, "function", {}).get("name") if hasattr(tool_call, "function") else str(tool_call)
|
|
1041
|
+
tool_args = getattr(tool_call, "function", {}).get("arguments") if hasattr(tool_call, "function") else {}
|
|
1042
|
+
|
|
1043
|
+
print(f" š§ Tool Call: {tool_name}")
|
|
1044
|
+
|
|
1045
|
+
tool_messages.append({
|
|
1046
|
+
"role": "tool",
|
|
1047
|
+
"content": f"Executing {tool_name}...",
|
|
1048
|
+
"tool_name": tool_name,
|
|
1049
|
+
"tool_input": tool_args,
|
|
1050
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
if tool_messages:
|
|
1054
|
+
print(f"\nš§ Tool Calls Captured: {len(tool_messages)}")
|
|
1055
|
+
|
|
1056
|
+
# Extract usage metrics if available
|
|
1057
|
+
usage = {}
|
|
1058
|
+
if hasattr(result, "metrics") and result.metrics:
|
|
1059
|
+
metrics = result.metrics
|
|
1060
|
+
usage = {
|
|
1061
|
+
"input_tokens": getattr(metrics, "input_tokens", 0),
|
|
1062
|
+
"output_tokens": getattr(metrics, "output_tokens", 0),
|
|
1063
|
+
"total_tokens": getattr(metrics, "total_tokens", 0),
|
|
1064
|
+
}
|
|
1065
|
+
print(f"\nš Token Usage:")
|
|
1066
|
+
print(f" Input Tokens: {usage.get('input_tokens', 0)}")
|
|
1067
|
+
print(f" Output Tokens: {usage.get('output_tokens', 0)}")
|
|
1068
|
+
print(f" Total Tokens: {usage.get('total_tokens', 0)}")
|
|
1069
|
+
|
|
1070
|
+
print(f"\nš Response Preview:")
|
|
1071
|
+
print(f" {response_content[:200]}..." if len(response_content) > 200 else f" {response_content}")
|
|
1072
|
+
|
|
1073
|
+
# CRITICAL: Persist COMPLETE session history to Control Plane API
|
|
1074
|
+
# This includes previous history + current turn for conversation continuity
|
|
1075
|
+
print("\nš¾ Persisting session history to Control Plane...")
|
|
1076
|
+
try:
|
|
1077
|
+
# Build complete session: previous history + current turn's messages
|
|
1078
|
+
updated_session_messages = list(session_history) # Start with loaded history
|
|
1079
|
+
|
|
1080
|
+
# Add current turn messages (user prompt + assistant response)
|
|
1081
|
+
# Streaming results don't have result.messages, so we manually build them
|
|
1082
|
+
current_turn_messages = [
|
|
1083
|
+
{
|
|
1084
|
+
"role": "user",
|
|
1085
|
+
"content": input.prompt,
|
|
1086
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1087
|
+
"user_id": input.user_id,
|
|
1088
|
+
"user_name": getattr(input, "user_name", None),
|
|
1089
|
+
"user_email": getattr(input, "user_email", None),
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
"role": "assistant",
|
|
1093
|
+
"content": response_content,
|
|
1094
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1095
|
+
}
|
|
1096
|
+
]
|
|
1097
|
+
|
|
1098
|
+
print(f" š Adding {len(current_turn_messages)} messages from current turn (user + assistant)...")
|
|
1099
|
+
updated_session_messages.extend(current_turn_messages)
|
|
1100
|
+
|
|
1101
|
+
if updated_session_messages:
|
|
1102
|
+
success = control_plane.persist_session(
|
|
1103
|
+
execution_id=input.execution_id,
|
|
1104
|
+
session_id=input.session_id or input.execution_id,
|
|
1105
|
+
user_id=input.user_id,
|
|
1106
|
+
messages=updated_session_messages, # Complete conversation history
|
|
1107
|
+
metadata={
|
|
1108
|
+
"team_id": input.team_id,
|
|
1109
|
+
"organization_id": input.organization_id,
|
|
1110
|
+
"turn_count": len(updated_session_messages),
|
|
1111
|
+
}
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
if success:
|
|
1115
|
+
print(f" ā
Complete session history persisted ({len(updated_session_messages)} total messages)")
|
|
1116
|
+
else:
|
|
1117
|
+
print(f" ā ļø Session persistence failed")
|
|
1118
|
+
else:
|
|
1119
|
+
print(" ā¹ļø No messages - skipping session persistence")
|
|
1120
|
+
|
|
1121
|
+
except Exception as session_error:
|
|
1122
|
+
print(f" ā ļø Session persistence error: {str(session_error)}")
|
|
1123
|
+
logger.warning("session_persistence_error", error=str(session_error), execution_id=input.execution_id)
|
|
1124
|
+
# Don't fail the execution if session persistence fails
|
|
1125
|
+
|
|
1126
|
+
print("\n" + "="*80)
|
|
1127
|
+
print("š TEAM EXECUTION END")
|
|
1128
|
+
print("="*80 + "\n")
|
|
1129
|
+
|
|
1130
|
+
# Cleanup: Remove team from registry
|
|
1131
|
+
if input.execution_id in _active_teams:
|
|
1132
|
+
del _active_teams[input.execution_id]
|
|
1133
|
+
print(f"ā
Team unregistered (execution_id: {input.execution_id})\n")
|
|
1134
|
+
|
|
1135
|
+
# Database persists across session for conversation history
|
|
1136
|
+
print(f"š¾ Database persisted for future turns: {db_path}\n")
|
|
1137
|
+
|
|
1138
|
+
return {
|
|
1139
|
+
"success": True,
|
|
1140
|
+
"response": response_content,
|
|
1141
|
+
"usage": usage,
|
|
1142
|
+
"coordination_type": "agno_team",
|
|
1143
|
+
"tool_messages": tool_messages, # Include tool call messages for UI
|
|
1144
|
+
"tool_execution_messages": tool_execution_messages, # Include real-time tool execution status
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
except Exception as e:
|
|
1148
|
+
# Cleanup on error
|
|
1149
|
+
if input.execution_id in _active_teams:
|
|
1150
|
+
del _active_teams[input.execution_id]
|
|
1151
|
+
|
|
1152
|
+
# Database persists even on error for potential recovery and history
|
|
1153
|
+
print(f"š¾ Database preserved for future turns despite error\n")
|
|
1154
|
+
|
|
1155
|
+
print("\n" + "="*80)
|
|
1156
|
+
print("ā TEAM EXECUTION FAILED")
|
|
1157
|
+
print("="*80)
|
|
1158
|
+
print(f"Error: {str(e)}")
|
|
1159
|
+
print("="*80 + "\n")
|
|
1160
|
+
|
|
1161
|
+
activity.logger.error(
|
|
1162
|
+
f"Team coordination failed",
|
|
1163
|
+
extra={
|
|
1164
|
+
"execution_id": input.execution_id,
|
|
1165
|
+
"error": str(e),
|
|
1166
|
+
}
|
|
1167
|
+
)
|
|
1168
|
+
return {
|
|
1169
|
+
"success": False,
|
|
1170
|
+
"error": str(e),
|
|
1171
|
+
"coordination_type": "agno_team",
|
|
1172
|
+
"usage": {},
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
@dataclass
|
|
1177
|
+
class ActivityCancelTeamInput:
|
|
1178
|
+
execution_id: str
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
@activity.defn(name="cancel_team_run")
|
|
1182
|
+
async def cancel_team_run(input: ActivityCancelTeamInput) -> dict:
|
|
1183
|
+
"""Cancel an active team run using Agno's cancel_run API."""
|
|
1184
|
+
print("\n" + "="*80)
|
|
1185
|
+
print("š CANCEL TEAM RUN")
|
|
1186
|
+
print("="*80)
|
|
1187
|
+
print(f"Execution ID: {input.execution_id}\n")
|
|
1188
|
+
|
|
1189
|
+
try:
|
|
1190
|
+
if input.execution_id not in _active_teams:
|
|
1191
|
+
print(f"ā ļø Team not found in registry - may have already completed")
|
|
1192
|
+
return {"success": False, "error": "Team not found or already completed", "execution_id": input.execution_id}
|
|
1193
|
+
|
|
1194
|
+
team_info = _active_teams[input.execution_id]
|
|
1195
|
+
team = team_info["team"]
|
|
1196
|
+
run_id = team_info.get("run_id")
|
|
1197
|
+
|
|
1198
|
+
if not run_id:
|
|
1199
|
+
print(f"ā ļø No run_id found - execution may not have started yet")
|
|
1200
|
+
return {"success": False, "error": "Execution not started yet", "execution_id": input.execution_id}
|
|
1201
|
+
|
|
1202
|
+
print(f"š Found run_id: {run_id}")
|
|
1203
|
+
print(f"š Calling team.cancel_run()...")
|
|
1204
|
+
|
|
1205
|
+
success = team.cancel_run(run_id)
|
|
1206
|
+
|
|
1207
|
+
if success:
|
|
1208
|
+
print(f"ā
Team run cancelled successfully!\n")
|
|
1209
|
+
del _active_teams[input.execution_id]
|
|
1210
|
+
return {"success": True, "execution_id": input.execution_id, "run_id": run_id, "cancelled_at": datetime.now(timezone.utc).isoformat()}
|
|
1211
|
+
else:
|
|
1212
|
+
print(f"ā ļø Cancel failed - run may have already completed\n")
|
|
1213
|
+
return {"success": False, "error": "Cancel failed - run may be completed", "execution_id": input.execution_id, "run_id": run_id}
|
|
1214
|
+
|
|
1215
|
+
except Exception as e:
|
|
1216
|
+
print(f"ā Error cancelling run: {str(e)}\n")
|
|
1217
|
+
return {"success": False, "error": str(e), "execution_id": input.execution_id}
|