kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,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()