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,619 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agno Service
|
|
3
|
+
|
|
4
|
+
This service provides integration with Agno for agent execution with MCP server support.
|
|
5
|
+
Agno enables dynamic MCP configuration at runtime, allowing agents to use different
|
|
6
|
+
MCP servers based on their configuration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Any, Iterator
|
|
13
|
+
import logging
|
|
14
|
+
from agno.agent import Agent
|
|
15
|
+
from agno.team import Team
|
|
16
|
+
from agno.models.litellm import LiteLLM
|
|
17
|
+
from agno.tools.mcp import MCPTools
|
|
18
|
+
from agno.run.team import TeamRunOutputEvent
|
|
19
|
+
from agno.db.postgres import PostgresDb
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgnoService:
|
|
25
|
+
"""Service for executing agents with Agno and MCP support"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize Agno service with persistent session storage using Supabase PostgreSQL"""
|
|
29
|
+
self.model_mapping = self._load_model_mapping()
|
|
30
|
+
|
|
31
|
+
# Get PostgreSQL connection string from environment
|
|
32
|
+
# This should be the direct database connection string from Supabase
|
|
33
|
+
db_url = os.environ.get("DATABASE_URL") or os.environ.get("SUPABASE_DB_URL")
|
|
34
|
+
|
|
35
|
+
if db_url:
|
|
36
|
+
self.db = PostgresDb(
|
|
37
|
+
db_url=db_url,
|
|
38
|
+
db_schema="agno", # Use "agno" schema for Agno session data
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger.info(
|
|
42
|
+
"Agno Service initialized with PostgreSQL session storage",
|
|
43
|
+
extra={
|
|
44
|
+
"model_mappings": len(self.model_mapping),
|
|
45
|
+
"db_schema": "agno",
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"DATABASE_URL or SUPABASE_DB_URL not set, Agno sessions will not persist",
|
|
51
|
+
extra={"model_mappings": len(self.model_mapping)}
|
|
52
|
+
)
|
|
53
|
+
self.db = None
|
|
54
|
+
|
|
55
|
+
def _load_model_mapping(self) -> Dict[str, str]:
|
|
56
|
+
"""
|
|
57
|
+
Load model mapping from models.json.
|
|
58
|
+
Maps kubiya/ prefix models to actual LiteLLM provider models.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
models_file = Path(__file__).parent.parent.parent / "models.json"
|
|
62
|
+
if models_file.exists():
|
|
63
|
+
with open(models_file, "r") as f:
|
|
64
|
+
mapping = json.load(f)
|
|
65
|
+
logger.info(f"Loaded model mapping from {models_file}", extra={"mappings": mapping})
|
|
66
|
+
return mapping
|
|
67
|
+
else:
|
|
68
|
+
logger.warning(f"Model mapping file not found at {models_file}, using empty mapping")
|
|
69
|
+
return {}
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Failed to load model mapping: {str(e)}")
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
def _resolve_model(self, model: str) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Resolve kubiya/ prefixed models to actual LiteLLM provider models.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
model: Model identifier (e.g., "kubiya/claude-sonnet-4")
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Resolved model identifier (e.g., "anthropic/claude-sonnet-4-20250514")
|
|
83
|
+
"""
|
|
84
|
+
if model in self.model_mapping:
|
|
85
|
+
resolved = self.model_mapping[model]
|
|
86
|
+
logger.info(f"Resolved model: {model} -> {resolved}")
|
|
87
|
+
return resolved
|
|
88
|
+
|
|
89
|
+
# If no mapping found, return as-is (for backward compatibility)
|
|
90
|
+
logger.info(f"No mapping found for model: {model}, using as-is")
|
|
91
|
+
return model
|
|
92
|
+
|
|
93
|
+
async def _build_mcp_tools_async(self, mcp_config: Dict[str, Any]) -> List[Any]:
|
|
94
|
+
"""
|
|
95
|
+
Build and connect to MCP tools from agent configuration (async).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
mcp_config: MCP servers configuration from agent.configuration.mcpServers
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of connected MCP tool instances
|
|
102
|
+
"""
|
|
103
|
+
mcp_tools = []
|
|
104
|
+
|
|
105
|
+
if not mcp_config:
|
|
106
|
+
logger.info("No MCP servers configured, agent will run without MCP tools")
|
|
107
|
+
return mcp_tools
|
|
108
|
+
|
|
109
|
+
logger.info(
|
|
110
|
+
f"Building MCP tools from {len(mcp_config)} MCP server configurations",
|
|
111
|
+
extra={
|
|
112
|
+
"mcp_server_count": len(mcp_config),
|
|
113
|
+
"mcp_server_ids": list(mcp_config.keys()),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for server_id, server_config in mcp_config.items():
|
|
118
|
+
try:
|
|
119
|
+
# Determine transport type
|
|
120
|
+
if "url" in server_config:
|
|
121
|
+
# SSE/HTTP transport
|
|
122
|
+
mcp_tool = MCPTools(
|
|
123
|
+
url=server_config["url"],
|
|
124
|
+
headers=server_config.get("headers", {}),
|
|
125
|
+
)
|
|
126
|
+
logger.info(
|
|
127
|
+
f"Configured MCP server '{server_id}' with SSE transport",
|
|
128
|
+
extra={"url": server_config["url"]}
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
# stdio transport - build full command with args
|
|
132
|
+
command = server_config.get("command")
|
|
133
|
+
args = server_config.get("args", [])
|
|
134
|
+
env = server_config.get("env", {})
|
|
135
|
+
|
|
136
|
+
# Build full command string: "command arg1 arg2 ..."
|
|
137
|
+
# Args can be a list or already a string
|
|
138
|
+
if isinstance(args, list) and args:
|
|
139
|
+
full_command = f"{command} {' '.join(str(arg) for arg in args)}"
|
|
140
|
+
elif isinstance(args, str):
|
|
141
|
+
full_command = f"{command} {args}"
|
|
142
|
+
else:
|
|
143
|
+
full_command = command
|
|
144
|
+
|
|
145
|
+
mcp_tool = MCPTools(
|
|
146
|
+
command=full_command,
|
|
147
|
+
env=env,
|
|
148
|
+
)
|
|
149
|
+
logger.info(
|
|
150
|
+
f"Configured MCP server '{server_id}' with stdio transport",
|
|
151
|
+
extra={"command": full_command}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Connect to the MCP server
|
|
155
|
+
await mcp_tool.connect()
|
|
156
|
+
mcp_tools.append(mcp_tool)
|
|
157
|
+
logger.info(f"Successfully connected to MCP server '{server_id}'")
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
import traceback
|
|
161
|
+
error_details = {
|
|
162
|
+
"server_id": server_id,
|
|
163
|
+
"error": str(e),
|
|
164
|
+
"error_type": type(e).__name__,
|
|
165
|
+
"traceback": traceback.format_exc(),
|
|
166
|
+
"config": {
|
|
167
|
+
"command": server_config.get("command") if "command" in server_config else None,
|
|
168
|
+
"url": server_config.get("url") if "url" in server_config else None,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
logger.error(
|
|
172
|
+
f"Failed to configure MCP server '{server_id}': {str(e)}",
|
|
173
|
+
extra=error_details
|
|
174
|
+
)
|
|
175
|
+
# Continue with other servers even if one fails
|
|
176
|
+
|
|
177
|
+
logger.info(
|
|
178
|
+
f"Built {len(mcp_tools)} MCP tools from {len(mcp_config)} server configurations",
|
|
179
|
+
extra={
|
|
180
|
+
"mcp_tool_count": len(mcp_tools),
|
|
181
|
+
"total_servers_configured": len(mcp_config),
|
|
182
|
+
"connection_success_rate": f"{len(mcp_tools)}/{len(mcp_config)}"
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return mcp_tools
|
|
187
|
+
|
|
188
|
+
async def _build_skill_tools(self, skill_defs: List[Dict[str, Any]]) -> List[Any]:
|
|
189
|
+
"""
|
|
190
|
+
Build OS-level skill tools from skill definitions.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
skill_defs: List of resolved skill definitions with configurations
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of instantiated skill tool instances
|
|
197
|
+
"""
|
|
198
|
+
# Import agno OS-level tools
|
|
199
|
+
try:
|
|
200
|
+
from agno.tools.file import FileTools
|
|
201
|
+
from agno.tools.shell import ShellTools
|
|
202
|
+
from agno.tools.docker import DockerTools
|
|
203
|
+
from agno.tools.sleep import SleepTools
|
|
204
|
+
from agno.tools.file_generation import FileGenerationTools
|
|
205
|
+
except ImportError as e:
|
|
206
|
+
logger.error(f"Failed to import agno tools: {str(e)}")
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
# Tool registry mapping skill types to agno tool classes
|
|
210
|
+
SKILL_REGISTRY = {
|
|
211
|
+
"file_system": FileTools,
|
|
212
|
+
"shell": ShellTools,
|
|
213
|
+
"docker": DockerTools,
|
|
214
|
+
"sleep": SleepTools,
|
|
215
|
+
"file_generation": FileGenerationTools,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
tools = []
|
|
219
|
+
|
|
220
|
+
if not skill_defs:
|
|
221
|
+
logger.info("No skill definitions provided, agent will run without OS-level tools")
|
|
222
|
+
return tools
|
|
223
|
+
|
|
224
|
+
logger.info(
|
|
225
|
+
f"Building skill tools from {len(skill_defs)} skill definitions",
|
|
226
|
+
extra={
|
|
227
|
+
"skill_count": len(skill_defs),
|
|
228
|
+
"skill_names": [t.get("name") for t in skill_defs],
|
|
229
|
+
"skill_types": [t.get("type") for t in skill_defs],
|
|
230
|
+
"skill_sources": [t.get("source") for t in skill_defs],
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
for skill_def in skill_defs:
|
|
235
|
+
if not skill_def.get("enabled", True):
|
|
236
|
+
logger.debug(
|
|
237
|
+
f"Skipping disabled skill",
|
|
238
|
+
extra={"skill_name": skill_def.get("name")}
|
|
239
|
+
)
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
skill_type = skill_def.get("type")
|
|
243
|
+
tool_class = SKILL_REGISTRY.get(skill_type)
|
|
244
|
+
|
|
245
|
+
if not tool_class:
|
|
246
|
+
logger.warning(
|
|
247
|
+
f"Unknown skill type: {skill_type}",
|
|
248
|
+
extra={
|
|
249
|
+
"skill_type": skill_type,
|
|
250
|
+
"skill_name": skill_def.get("name")
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Get configuration from skill definition
|
|
256
|
+
config = skill_def.get("configuration", {})
|
|
257
|
+
|
|
258
|
+
# Instantiate tool with configuration
|
|
259
|
+
try:
|
|
260
|
+
tool_instance = tool_class(**config)
|
|
261
|
+
tools.append(tool_instance)
|
|
262
|
+
|
|
263
|
+
logger.info(
|
|
264
|
+
f"Skill instantiated: {skill_def.get('name')}",
|
|
265
|
+
extra={
|
|
266
|
+
"skill_name": skill_def.get("name"),
|
|
267
|
+
"skill_type": skill_type,
|
|
268
|
+
"source": skill_def.get("source"),
|
|
269
|
+
"configuration": config
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
import traceback
|
|
274
|
+
logger.error(
|
|
275
|
+
f"Failed to instantiate skill '{skill_def.get('name')}': {str(e)}",
|
|
276
|
+
extra={
|
|
277
|
+
"skill_name": skill_def.get("name"),
|
|
278
|
+
"skill_type": skill_type,
|
|
279
|
+
"error": str(e),
|
|
280
|
+
"error_type": type(e).__name__,
|
|
281
|
+
"traceback": traceback.format_exc(),
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
# Continue with other tools even if one fails
|
|
285
|
+
|
|
286
|
+
logger.info(
|
|
287
|
+
f"Built {len(tools)} skill tools",
|
|
288
|
+
extra={
|
|
289
|
+
"tool_count": len(tools),
|
|
290
|
+
"tool_types": [type(t).__name__ for t in tools]
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return tools
|
|
295
|
+
|
|
296
|
+
async def execute_agent_async(
|
|
297
|
+
self,
|
|
298
|
+
prompt: Optional[str] = None,
|
|
299
|
+
model: Optional[str] = None,
|
|
300
|
+
system_prompt: Optional[str] = None,
|
|
301
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
302
|
+
skills: Optional[List[Dict[str, Any]]] = None,
|
|
303
|
+
temperature: float = 0.7,
|
|
304
|
+
max_tokens: Optional[int] = None,
|
|
305
|
+
stream: bool = False,
|
|
306
|
+
conversation_history: Optional[List[Dict[str, str]]] = None,
|
|
307
|
+
session_id: Optional[str] = None,
|
|
308
|
+
user_id: Optional[str] = None,
|
|
309
|
+
**kwargs: Any,
|
|
310
|
+
) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Execute an agent using Agno Teams with MCP and OS-level skill support and session management.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
prompt: The user prompt (for single-turn conversations)
|
|
316
|
+
model: Model identifier
|
|
317
|
+
system_prompt: System prompt for the agent
|
|
318
|
+
mcp_servers: MCP servers configuration dict
|
|
319
|
+
skills: List of resolved skill definitions (OS-level tools)
|
|
320
|
+
temperature: Temperature for response generation
|
|
321
|
+
max_tokens: Maximum tokens to generate
|
|
322
|
+
stream: Whether to stream the response
|
|
323
|
+
conversation_history: Full conversation history (for multi-turn conversations) - DEPRECATED, use session_id instead
|
|
324
|
+
session_id: Session ID for multi-turn conversations (enables Agno session management)
|
|
325
|
+
user_id: User ID for multi-user support
|
|
326
|
+
**kwargs: Additional parameters
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Dict containing the response and metadata including session messages
|
|
330
|
+
"""
|
|
331
|
+
mcp_tools = []
|
|
332
|
+
skill_tools = []
|
|
333
|
+
try:
|
|
334
|
+
# Use default model if not specified
|
|
335
|
+
if not model:
|
|
336
|
+
model = os.environ.get("LITELLM_DEFAULT_MODEL", "claude-sonnet-4")
|
|
337
|
+
|
|
338
|
+
# Build and connect to MCP tools from configuration
|
|
339
|
+
mcp_tools = await self._build_mcp_tools_async(mcp_servers or {})
|
|
340
|
+
|
|
341
|
+
# Build OS-level skill tools
|
|
342
|
+
skill_tools = await self._build_skill_tools(skills or [])
|
|
343
|
+
|
|
344
|
+
# Create LiteLLM model instance
|
|
345
|
+
# IMPORTANT: Use openai/ prefix for custom proxy compatibility
|
|
346
|
+
litellm_model = LiteLLM(
|
|
347
|
+
id=f"openai/{model}",
|
|
348
|
+
api_base=os.environ.get("LITELLM_API_BASE", "https://llm-proxy.kubiya.ai"),
|
|
349
|
+
api_key=os.environ.get("LITELLM_API_KEY"),
|
|
350
|
+
temperature=temperature,
|
|
351
|
+
max_tokens=max_tokens,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Create specialized agent members for MCP tools
|
|
355
|
+
members = []
|
|
356
|
+
if mcp_tools:
|
|
357
|
+
# Create a specialized agent for each MCP tool
|
|
358
|
+
for idx, tool in enumerate(mcp_tools):
|
|
359
|
+
member = Agent(
|
|
360
|
+
name=f"MCP Agent {idx+1}",
|
|
361
|
+
role="Execute MCP tool operations",
|
|
362
|
+
tools=[tool],
|
|
363
|
+
model=litellm_model,
|
|
364
|
+
)
|
|
365
|
+
members.append(member)
|
|
366
|
+
logger.info(
|
|
367
|
+
f"Created {len(members)} specialized MCP agents",
|
|
368
|
+
extra={
|
|
369
|
+
"mcp_agent_count": len(members),
|
|
370
|
+
"mcp_tools_per_agent": [1] * len(members)
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Combine MCP tools and OS-level skill tools for the main team
|
|
375
|
+
all_tools = mcp_tools + skill_tools
|
|
376
|
+
logger.info(
|
|
377
|
+
f"Total tools available: {len(all_tools)} (MCP: {len(mcp_tools)}, Skills: {len(skill_tools)})",
|
|
378
|
+
extra={
|
|
379
|
+
"mcp_tool_count": len(mcp_tools),
|
|
380
|
+
"skill_tool_count": len(skill_tools),
|
|
381
|
+
"total_tools": len(all_tools)
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Create the team with database for session management
|
|
386
|
+
# The team itself gets all tools (both MCP and OS-level skills)
|
|
387
|
+
logger.info(
|
|
388
|
+
f"Creating Agent Execution Team with {len(members)} MCP agents and {len(all_tools)} total tools",
|
|
389
|
+
extra={
|
|
390
|
+
"team_members": len(members),
|
|
391
|
+
"total_tools": len(all_tools),
|
|
392
|
+
"mcp_tools": len(mcp_tools),
|
|
393
|
+
"skill_tools": len(skill_tools),
|
|
394
|
+
"session_enabled": bool(session_id),
|
|
395
|
+
"session_id": session_id,
|
|
396
|
+
"user_id": user_id,
|
|
397
|
+
"model": model,
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
team = Team(
|
|
402
|
+
name="Agent Execution Team",
|
|
403
|
+
members=members,
|
|
404
|
+
tools=all_tools, # Add all tools to the team
|
|
405
|
+
model=litellm_model,
|
|
406
|
+
instructions=system_prompt or ["You are a helpful AI assistant."],
|
|
407
|
+
markdown=True,
|
|
408
|
+
db=self.db, # Enable session storage
|
|
409
|
+
add_history_to_context=True, # Automatically add history to context
|
|
410
|
+
num_history_runs=5, # Include last 5 runs in context
|
|
411
|
+
read_team_history=True, # Enable reading team history
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
logger.info(
|
|
415
|
+
f"Team created successfully with session management enabled",
|
|
416
|
+
extra={
|
|
417
|
+
"team_name": team.name,
|
|
418
|
+
"team_id": id(team),
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# For session-based conversations, just use the current prompt
|
|
423
|
+
# Agno will automatically handle the conversation history through sessions
|
|
424
|
+
if not prompt:
|
|
425
|
+
raise ValueError("'prompt' is required for session-based conversations")
|
|
426
|
+
|
|
427
|
+
input_text = prompt
|
|
428
|
+
logger.info(
|
|
429
|
+
f"Executing team with Agno. Model: {model}, Members: {len(members)}, MCP tools: {len(mcp_tools)}, Session: {session_id}, User: {user_id}"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Execute team with session support (use arun for async)
|
|
433
|
+
run_kwargs = {}
|
|
434
|
+
if session_id:
|
|
435
|
+
run_kwargs["session_id"] = session_id
|
|
436
|
+
if user_id:
|
|
437
|
+
run_kwargs["user_id"] = user_id
|
|
438
|
+
|
|
439
|
+
if stream:
|
|
440
|
+
# For streaming, collect all chunks AND publish to Redis for real-time UI
|
|
441
|
+
response_stream: Iterator[TeamRunOutputEvent] = team.arun(
|
|
442
|
+
input_text,
|
|
443
|
+
stream=True,
|
|
444
|
+
stream_intermediate_steps=True,
|
|
445
|
+
**run_kwargs
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Generate unique message ID for this streaming turn
|
|
449
|
+
import time
|
|
450
|
+
message_id = f"{session_id}_{int(time.time() * 1000000)}" if session_id else f"msg_{int(time.time() * 1000000)}"
|
|
451
|
+
|
|
452
|
+
# Publish chunks to Redis for real-time UI updates
|
|
453
|
+
from control_plane_api.app.lib.redis_client import get_redis_client
|
|
454
|
+
from datetime import datetime, timezone
|
|
455
|
+
import json as json_lib
|
|
456
|
+
|
|
457
|
+
redis_client = get_redis_client()
|
|
458
|
+
|
|
459
|
+
content_chunks = []
|
|
460
|
+
async for chunk in response_stream:
|
|
461
|
+
if chunk.event == "TeamRunContent" and chunk.content:
|
|
462
|
+
content_chunks.append(chunk.content)
|
|
463
|
+
|
|
464
|
+
# Publish chunk to Redis immediately for real-time UI
|
|
465
|
+
if redis_client and session_id: # session_id acts as execution_id
|
|
466
|
+
try:
|
|
467
|
+
redis_key = f"execution:{session_id}:events"
|
|
468
|
+
event_data = {
|
|
469
|
+
"event_type": "message_chunk",
|
|
470
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
471
|
+
"data": {
|
|
472
|
+
"role": "assistant",
|
|
473
|
+
"content": chunk.content, # Delta chunk, not accumulated
|
|
474
|
+
"is_chunk": True,
|
|
475
|
+
"message_id": message_id,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
await redis_client.lpush(redis_key, json_lib.dumps(event_data))
|
|
479
|
+
await redis_client.ltrim(redis_key, 0, 999) # Keep last 1000 events
|
|
480
|
+
await redis_client.expire(redis_key, 3600) # 1 hour TTL
|
|
481
|
+
except Exception as redis_error:
|
|
482
|
+
# Don't fail execution if Redis publish fails
|
|
483
|
+
logger.debug("redis_chunk_publish_failed", error=str(redis_error), session_id=session_id)
|
|
484
|
+
|
|
485
|
+
content = "".join(content_chunks)
|
|
486
|
+
|
|
487
|
+
# Get the final response object
|
|
488
|
+
response = await team.arun(input_text, stream=False, **run_kwargs)
|
|
489
|
+
else:
|
|
490
|
+
# Non-streaming execution with session
|
|
491
|
+
response = await team.arun(input_text, **run_kwargs)
|
|
492
|
+
content = response.content
|
|
493
|
+
|
|
494
|
+
# Extract usage from metrics if available
|
|
495
|
+
usage = {}
|
|
496
|
+
if hasattr(response, 'metrics') and response.metrics:
|
|
497
|
+
usage = {
|
|
498
|
+
"prompt_tokens": getattr(response.metrics, 'input_tokens', 0),
|
|
499
|
+
"completion_tokens": getattr(response.metrics, 'output_tokens', 0),
|
|
500
|
+
"total_tokens": getattr(response.metrics, 'total_tokens', 0),
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# Get session messages if session_id was provided
|
|
504
|
+
messages = []
|
|
505
|
+
if session_id and self.db:
|
|
506
|
+
try:
|
|
507
|
+
# Fetch all messages for this session from Agno's database
|
|
508
|
+
session_messages = team.get_messages_for_session(session_id=session_id)
|
|
509
|
+
messages = [
|
|
510
|
+
{
|
|
511
|
+
"role": msg.role,
|
|
512
|
+
"content": msg.content,
|
|
513
|
+
"timestamp": msg.created_at if hasattr(msg, 'created_at') else None,
|
|
514
|
+
}
|
|
515
|
+
for msg in session_messages
|
|
516
|
+
]
|
|
517
|
+
logger.info(f"Retrieved {len(messages)} messages from session {session_id}")
|
|
518
|
+
except Exception as e:
|
|
519
|
+
logger.warning(f"Failed to retrieve session messages: {str(e)}")
|
|
520
|
+
|
|
521
|
+
result = {
|
|
522
|
+
"success": True,
|
|
523
|
+
"response": content,
|
|
524
|
+
"model": model,
|
|
525
|
+
"usage": usage,
|
|
526
|
+
"finish_reason": "stop",
|
|
527
|
+
"mcp_tools_used": len(mcp_tools),
|
|
528
|
+
"run_id": getattr(response, 'run_id', None),
|
|
529
|
+
"session_id": session_id,
|
|
530
|
+
"messages": messages, # Include full session history
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
logger.info(
|
|
534
|
+
f"Team execution successful",
|
|
535
|
+
extra={
|
|
536
|
+
"mcp_tools": len(mcp_tools),
|
|
537
|
+
"members": len(members),
|
|
538
|
+
"session_messages": len(messages)
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
return result
|
|
542
|
+
|
|
543
|
+
except Exception as e:
|
|
544
|
+
import traceback
|
|
545
|
+
error_traceback = traceback.format_exc()
|
|
546
|
+
error_type = type(e).__name__
|
|
547
|
+
|
|
548
|
+
logger.error(f"Error executing team with Agno: {str(e)}")
|
|
549
|
+
logger.error(f"Error type: {error_type}")
|
|
550
|
+
logger.error(f"Traceback: {error_traceback}")
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
"success": False,
|
|
554
|
+
"error": str(e),
|
|
555
|
+
"error_type": error_type,
|
|
556
|
+
"error_traceback": error_traceback,
|
|
557
|
+
"model": model or os.environ.get("LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4"),
|
|
558
|
+
"mcp_tools_used": 0,
|
|
559
|
+
}
|
|
560
|
+
finally:
|
|
561
|
+
# Close all MCP connections
|
|
562
|
+
for mcp_tool in mcp_tools:
|
|
563
|
+
try:
|
|
564
|
+
await mcp_tool.close()
|
|
565
|
+
except Exception as e:
|
|
566
|
+
logger.warning(f"Failed to close MCP tool: {str(e)}")
|
|
567
|
+
|
|
568
|
+
def execute_agent(
|
|
569
|
+
self,
|
|
570
|
+
prompt: str,
|
|
571
|
+
model: Optional[str] = None,
|
|
572
|
+
system_prompt: Optional[str] = None,
|
|
573
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
574
|
+
temperature: float = 0.7,
|
|
575
|
+
max_tokens: Optional[int] = None,
|
|
576
|
+
stream: bool = False,
|
|
577
|
+
**kwargs: Any,
|
|
578
|
+
) -> Dict[str, Any]:
|
|
579
|
+
"""
|
|
580
|
+
Sync wrapper for execute_agent_async.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
prompt: The user prompt
|
|
584
|
+
model: Model identifier
|
|
585
|
+
system_prompt: System prompt for the agent
|
|
586
|
+
mcp_servers: MCP servers configuration dict
|
|
587
|
+
temperature: Temperature for response generation
|
|
588
|
+
max_tokens: Maximum tokens to generate
|
|
589
|
+
stream: Whether to stream the response
|
|
590
|
+
**kwargs: Additional parameters
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
Dict containing the response and metadata
|
|
594
|
+
"""
|
|
595
|
+
import asyncio
|
|
596
|
+
|
|
597
|
+
# Always create a new event loop for isolation
|
|
598
|
+
loop = asyncio.new_event_loop()
|
|
599
|
+
asyncio.set_event_loop(loop)
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
return loop.run_until_complete(
|
|
603
|
+
self.execute_agent_async(
|
|
604
|
+
prompt=prompt,
|
|
605
|
+
model=model,
|
|
606
|
+
system_prompt=system_prompt,
|
|
607
|
+
mcp_servers=mcp_servers,
|
|
608
|
+
temperature=temperature,
|
|
609
|
+
max_tokens=max_tokens,
|
|
610
|
+
stream=stream,
|
|
611
|
+
**kwargs,
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
finally:
|
|
615
|
+
loop.close()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# Singleton instance
|
|
619
|
+
agno_service = AgnoService()
|