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,166 @@
1
+ """
2
+ LiteLLM Pricing Integration - Fetch and cache model pricing data
3
+ """
4
+
5
+ import httpx
6
+ import structlog
7
+ from typing import Dict, Optional
8
+ from datetime import datetime, timedelta
9
+ import asyncio
10
+
11
+ logger = structlog.get_logger()
12
+
13
+ # Cache for pricing data
14
+ _pricing_cache: Optional[Dict] = None
15
+ _cache_timestamp: Optional[datetime] = None
16
+ _cache_lock = asyncio.Lock()
17
+
18
+ PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/refs/heads/main/model_prices_and_context_window.json"
19
+ CACHE_TTL_HOURS = 24 # Refresh pricing data daily
20
+
21
+
22
+ async def get_litellm_pricing() -> Dict:
23
+ """
24
+ Fetch LiteLLM pricing data with caching
25
+
26
+ Returns:
27
+ Dict containing model pricing information
28
+ """
29
+ global _pricing_cache, _cache_timestamp
30
+
31
+ async with _cache_lock:
32
+ # Check if cache is valid
33
+ if _pricing_cache and _cache_timestamp:
34
+ age = datetime.utcnow() - _cache_timestamp
35
+ if age < timedelta(hours=CACHE_TTL_HOURS):
36
+ logger.debug("litellm_pricing_cache_hit", age_hours=age.total_seconds() / 3600)
37
+ return _pricing_cache
38
+
39
+ # Fetch fresh pricing data
40
+ logger.info("fetching_litellm_pricing", url=PRICING_URL)
41
+ try:
42
+ async with httpx.AsyncClient(timeout=30.0) as client:
43
+ response = await client.get(PRICING_URL)
44
+ response.raise_for_status()
45
+ pricing_data = response.json()
46
+
47
+ # Update cache
48
+ _pricing_cache = pricing_data
49
+ _cache_timestamp = datetime.utcnow()
50
+
51
+ logger.info(
52
+ "litellm_pricing_fetched_successfully",
53
+ models_count=len(pricing_data),
54
+ cached_until=(_cache_timestamp + timedelta(hours=CACHE_TTL_HOURS)).isoformat()
55
+ )
56
+
57
+ return pricing_data
58
+ except Exception as e:
59
+ logger.error("litellm_pricing_fetch_failed", error=str(e), error_type=type(e).__name__)
60
+ # Return empty dict on failure
61
+ return {}
62
+
63
+
64
+ def get_model_pricing(model_id: str, pricing_data: Dict) -> Optional[Dict]:
65
+ """
66
+ Get pricing information for a specific model
67
+
68
+ Args:
69
+ model_id: Model identifier (e.g., "claude-sonnet-4", "gpt-4o")
70
+ pricing_data: Full pricing data from LiteLLM
71
+
72
+ Returns:
73
+ Dict with pricing info or None if not found
74
+ """
75
+ # Try exact match first
76
+ if model_id in pricing_data:
77
+ return pricing_data[model_id]
78
+
79
+ # Try common variations
80
+ variations = [
81
+ model_id,
82
+ f"openai/{model_id}",
83
+ f"anthropic/{model_id}",
84
+ f"anthropic.{model_id}",
85
+ f"bedrock/{model_id}",
86
+ ]
87
+
88
+ for variation in variations:
89
+ if variation in pricing_data:
90
+ return pricing_data[variation]
91
+
92
+ logger.warning("model_pricing_not_found", model_id=model_id, tried_variations=variations)
93
+ return None
94
+
95
+
96
+ def calculate_llm_cost(
97
+ model_id: str,
98
+ estimated_input_tokens: int,
99
+ estimated_output_tokens: int,
100
+ pricing_data: Dict
101
+ ) -> tuple[float, float, float]:
102
+ """
103
+ Calculate LLM cost for a model
104
+
105
+ Args:
106
+ model_id: Model identifier
107
+ estimated_input_tokens: Expected input tokens
108
+ estimated_output_tokens: Expected output tokens
109
+ pricing_data: Full pricing data from LiteLLM
110
+
111
+ Returns:
112
+ Tuple of (cost_per_1k_input, cost_per_1k_output, total_cost)
113
+ """
114
+ model_pricing = get_model_pricing(model_id, pricing_data)
115
+
116
+ if not model_pricing:
117
+ # Fallback defaults
118
+ logger.warning("using_default_pricing", model_id=model_id)
119
+ return (0.003, 0.015, 0.0)
120
+
121
+ input_cost_per_token = model_pricing.get("input_cost_per_token", 0.000003)
122
+ output_cost_per_token = model_pricing.get("output_cost_per_token", 0.000015)
123
+
124
+ # Convert to per-1k pricing for display
125
+ input_cost_per_1k = input_cost_per_token * 1000
126
+ output_cost_per_1k = output_cost_per_token * 1000
127
+
128
+ # Calculate total cost
129
+ total_cost = (
130
+ (estimated_input_tokens * input_cost_per_token) +
131
+ (estimated_output_tokens * output_cost_per_token)
132
+ )
133
+
134
+ return (input_cost_per_1k, output_cost_per_1k, total_cost)
135
+
136
+
137
+ def get_model_display_name(model_id: str) -> str:
138
+ """
139
+ Get human-readable model name
140
+
141
+ Args:
142
+ model_id: Model identifier
143
+
144
+ Returns:
145
+ Display name
146
+ """
147
+ # Map of common model IDs to display names
148
+ display_names = {
149
+ "claude-sonnet-4": "Claude Sonnet 4",
150
+ "claude-3-5-sonnet-20241022": "Claude 3.5 Sonnet",
151
+ "claude-3-5-sonnet-20240620": "Claude 3.5 Sonnet (Legacy)",
152
+ "claude-3-opus-20240229": "Claude 3 Opus",
153
+ "claude-3-haiku-20240307": "Claude 3 Haiku",
154
+ "gpt-4o": "GPT-4o",
155
+ "gpt-4o-mini": "GPT-4o Mini",
156
+ "gpt-4-turbo": "GPT-4 Turbo",
157
+ "gpt-4": "GPT-4",
158
+ "gpt-3.5-turbo": "GPT-3.5 Turbo",
159
+ "o1": "OpenAI o1",
160
+ "o1-mini": "OpenAI o1-mini",
161
+ "gemini-2.0-flash-exp": "Gemini 2.0 Flash",
162
+ "gemini-1.5-pro": "Gemini 1.5 Pro",
163
+ "gemini-1.5-flash": "Gemini 1.5 Flash",
164
+ }
165
+
166
+ return display_names.get(model_id, model_id.replace("-", " ").title())
@@ -0,0 +1,22 @@
1
+ """
2
+ Planning Tools - Modular tools for the task planning agent
3
+
4
+ This package provides decoupled, maintainable tools organized by category:
5
+ - agents: Agent-related context and operations
6
+ - teams: Team-related context and operations
7
+ - environments: Environment and infrastructure context
8
+ - resources: General resource and capability queries
9
+ - workflows: Workflow and execution context
10
+ """
11
+
12
+ from control_plane_api.app.lib.planning_tools.agents import AgentsContextTools
13
+ from control_plane_api.app.lib.planning_tools.teams import TeamsContextTools
14
+ from control_plane_api.app.lib.planning_tools.environments import EnvironmentsContextTools
15
+ from control_plane_api.app.lib.planning_tools.resources import ResourcesContextTools
16
+
17
+ __all__ = [
18
+ "AgentsContextTools",
19
+ "TeamsContextTools",
20
+ "EnvironmentsContextTools",
21
+ "ResourcesContextTools",
22
+ ]
@@ -0,0 +1,155 @@
1
+ """
2
+ Agent Context Tools - Fetch agent information for task planning
3
+ """
4
+
5
+ from typing import Optional
6
+ from control_plane_api.app.lib.planning_tools.base import BasePlanningTools
7
+
8
+
9
+ class AgentsContextTools(BasePlanningTools):
10
+ """
11
+ Tools for fetching agent context and capabilities
12
+
13
+ Provides methods to:
14
+ - List all available agents
15
+ - Get detailed agent information
16
+ - Query agent capabilities
17
+ - Check agent availability
18
+ """
19
+
20
+ def __init__(self, base_url: str = "http://localhost:8000", organization_id: Optional[str] = None):
21
+ super().__init__(base_url=base_url, organization_id=organization_id)
22
+ self.name = "agent_context_tools"
23
+
24
+ async def list_agents(self, limit: int = 50) -> str:
25
+ """
26
+ List all available agents with their basic information
27
+
28
+ Args:
29
+ limit: Maximum number of agents to return
30
+
31
+ Returns:
32
+ Formatted string with agent information including:
33
+ - Agent name and ID
34
+ - Model being used
35
+ - Description/capabilities
36
+ - Current status
37
+ """
38
+ try:
39
+ params = {"limit": limit}
40
+ if self.organization_id:
41
+ params["organization_id"] = self.organization_id
42
+
43
+ response = await self._make_request("GET", "/agents", params=params)
44
+
45
+ agents = response if isinstance(response, list) else response.get("agents", [])
46
+
47
+ return self._format_list_response(
48
+ items=agents,
49
+ title="Available Agents",
50
+ key_fields=["model_id", "description", "status", "runner_name"],
51
+ )
52
+
53
+ except Exception as e:
54
+ return f"Error listing agents: {str(e)}"
55
+
56
+ async def get_agent_details(self, agent_id: str) -> str:
57
+ """
58
+ Get detailed information about a specific agent
59
+
60
+ Args:
61
+ agent_id: ID of the agent to fetch
62
+
63
+ Returns:
64
+ Detailed agent information including:
65
+ - Full configuration
66
+ - Available tools/capabilities
67
+ - Model details
68
+ - Resource requirements
69
+ """
70
+ try:
71
+ response = await self._make_request("GET", f"/agents/{agent_id}")
72
+
73
+ return self._format_detail_response(
74
+ item=response,
75
+ title=f"Agent Details: {response.get('name', 'Unknown')}",
76
+ )
77
+
78
+ except Exception as e:
79
+ return f"Error fetching agent {agent_id}: {str(e)}"
80
+
81
+ async def search_agents_by_capability(self, capability: str) -> str:
82
+ """
83
+ Search for agents that have a specific capability
84
+
85
+ Args:
86
+ capability: Capability to search for (e.g., "kubernetes", "aws", "python")
87
+
88
+ Returns:
89
+ List of agents matching the capability
90
+ """
91
+ try:
92
+ # First get all agents
93
+ params = {}
94
+ if self.organization_id:
95
+ params["organization_id"] = self.organization_id
96
+
97
+ response = await self._make_request("GET", "/agents", params=params)
98
+ agents = response if isinstance(response, list) else response.get("agents", [])
99
+
100
+ # Filter by capability (search in description and tools)
101
+ matching_agents = []
102
+ for agent in agents:
103
+ agent_text = f"{agent.get('name', '')} {agent.get('description', '')}".lower()
104
+ if capability.lower() in agent_text:
105
+ matching_agents.append(agent)
106
+
107
+ return self._format_list_response(
108
+ items=matching_agents,
109
+ title=f"Agents with '{capability}' capability",
110
+ key_fields=["model_id", "description"],
111
+ )
112
+
113
+ except Exception as e:
114
+ return f"Error searching agents: {str(e)}"
115
+
116
+ async def get_agent_execution_history(self, agent_id: str, limit: int = 10) -> str:
117
+ """
118
+ Get recent execution history for an agent
119
+
120
+ Args:
121
+ agent_id: ID of the agent
122
+ limit: Number of recent executions to fetch
123
+
124
+ Returns:
125
+ Recent execution history with success rates
126
+ """
127
+ try:
128
+ params = {"limit": limit, "entity_id": agent_id}
129
+ response = await self._make_request("GET", "/executions", params=params)
130
+
131
+ executions = response if isinstance(response, list) else response.get("executions", [])
132
+
133
+ if not executions:
134
+ return f"No execution history found for agent {agent_id}"
135
+
136
+ # Calculate success rate
137
+ completed = sum(1 for e in executions if e.get("status") == "completed")
138
+ total = len(executions)
139
+ success_rate = (completed / total * 100) if total > 0 else 0
140
+
141
+ output = [
142
+ f"Execution History for Agent (Last {total} runs):",
143
+ f"Success Rate: {success_rate:.1f}% ({completed}/{total})",
144
+ "\nRecent Executions:",
145
+ ]
146
+
147
+ for idx, execution in enumerate(executions[:5], 1):
148
+ status = execution.get("status", "unknown")
149
+ prompt = execution.get("prompt", "No description")[:50]
150
+ output.append(f"{idx}. Status: {status} | Task: {prompt}...")
151
+
152
+ return "\n".join(output)
153
+
154
+ except Exception as e:
155
+ return f"Error fetching execution history: {str(e)}"
@@ -0,0 +1,189 @@
1
+ """
2
+ Base classes and utilities for planning tools
3
+ """
4
+
5
+ from typing import Optional, Dict, Any, List
6
+ from agno.tools.toolkit import Toolkit
7
+ import structlog
8
+ import httpx
9
+ import json
10
+
11
+ logger = structlog.get_logger()
12
+
13
+
14
+ class BasePlanningTools(Toolkit):
15
+ """
16
+ Base class for all planning tools with common utilities
17
+
18
+ Provides:
19
+ - HTTP client for API calls
20
+ - Common error handling
21
+ - Response formatting
22
+ - Logging utilities
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ base_url: str = "http://localhost:8000",
28
+ organization_id: Optional[str] = None,
29
+ timeout: int = 30,
30
+ ):
31
+ """
32
+ Initialize base planning tools
33
+
34
+ Args:
35
+ base_url: Base URL for the control plane API
36
+ organization_id: Organization context for filtering
37
+ timeout: HTTP request timeout in seconds
38
+ """
39
+ super().__init__(name="base_planning_tools")
40
+ self.base_url = base_url.rstrip("/")
41
+ self.organization_id = organization_id
42
+ self.timeout = timeout
43
+ self._client: Optional[httpx.AsyncClient] = None
44
+
45
+ async def _get_client(self) -> httpx.AsyncClient:
46
+ """Get or create HTTP client"""
47
+ if self._client is None:
48
+ self._client = httpx.AsyncClient(
49
+ base_url=self.base_url,
50
+ timeout=self.timeout,
51
+ headers={"Content-Type": "application/json"},
52
+ )
53
+ return self._client
54
+
55
+ async def _make_request(
56
+ self,
57
+ method: str,
58
+ endpoint: str,
59
+ params: Optional[Dict[str, Any]] = None,
60
+ data: Optional[Dict[str, Any]] = None,
61
+ ) -> Dict[str, Any]:
62
+ """
63
+ Make HTTP request with error handling
64
+
65
+ Args:
66
+ method: HTTP method (GET, POST, etc.)
67
+ endpoint: API endpoint (will be appended to base_url)
68
+ params: Query parameters
69
+ data: Request body data
70
+
71
+ Returns:
72
+ Parsed JSON response
73
+
74
+ Raises:
75
+ Exception: If request fails
76
+ """
77
+ client = await self._get_client()
78
+
79
+ try:
80
+ logger.info(
81
+ "planning_tool_request",
82
+ method=method,
83
+ endpoint=endpoint,
84
+ has_params=bool(params),
85
+ has_data=bool(data),
86
+ )
87
+
88
+ response = await client.request(
89
+ method=method,
90
+ url=endpoint,
91
+ params=params,
92
+ json=data,
93
+ )
94
+ response.raise_for_status()
95
+
96
+ result = response.json()
97
+ logger.info(
98
+ "planning_tool_response",
99
+ endpoint=endpoint,
100
+ status_code=response.status_code,
101
+ )
102
+
103
+ return result
104
+
105
+ except httpx.HTTPStatusError as e:
106
+ logger.error(
107
+ "planning_tool_http_error",
108
+ endpoint=endpoint,
109
+ status_code=e.response.status_code,
110
+ error=str(e),
111
+ )
112
+ raise Exception(f"HTTP {e.response.status_code}: {str(e)}")
113
+ except Exception as e:
114
+ logger.error(
115
+ "planning_tool_error",
116
+ endpoint=endpoint,
117
+ error=str(e),
118
+ error_type=type(e).__name__,
119
+ )
120
+ raise
121
+
122
+ def _format_list_response(
123
+ self,
124
+ items: List[Dict[str, Any]],
125
+ title: str,
126
+ key_fields: List[str],
127
+ ) -> str:
128
+ """
129
+ Format a list of items as a readable string
130
+
131
+ Args:
132
+ items: List of items to format
133
+ title: Title for the list
134
+ key_fields: Fields to include in the output
135
+
136
+ Returns:
137
+ Formatted string representation
138
+ """
139
+ if not items:
140
+ return f"{title}: None available"
141
+
142
+ output = [f"{title} ({len(items)} total):"]
143
+ for idx, item in enumerate(items, 1):
144
+ output.append(f"\n{idx}. {item.get('name', 'Unnamed')} (ID: {item.get('id', 'N/A')})")
145
+ for field in key_fields:
146
+ if field in item and item[field]:
147
+ value = item[field]
148
+ # Format nested objects
149
+ if isinstance(value, dict):
150
+ value = json.dumps(value, indent=2)
151
+ elif isinstance(value, list):
152
+ value = f"{len(value)} items"
153
+ output.append(f" - {field}: {value}")
154
+
155
+ return "\n".join(output)
156
+
157
+ def _format_detail_response(
158
+ self,
159
+ item: Dict[str, Any],
160
+ title: str,
161
+ ) -> str:
162
+ """
163
+ Format a single item as a readable string
164
+
165
+ Args:
166
+ item: Item to format
167
+ title: Title for the item
168
+
169
+ Returns:
170
+ Formatted string representation
171
+ """
172
+ if not item:
173
+ return f"{title}: Not found"
174
+
175
+ output = [f"{title}:"]
176
+ for key, value in item.items():
177
+ if isinstance(value, dict):
178
+ value = json.dumps(value, indent=2)
179
+ elif isinstance(value, list):
180
+ value = f"{len(value)} items: {', '.join([str(v) for v in value[:3]])}{'...' if len(value) > 3 else ''}"
181
+ output.append(f" {key}: {value}")
182
+
183
+ return "\n".join(output)
184
+
185
+ async def close(self):
186
+ """Close HTTP client"""
187
+ if self._client:
188
+ await self._client.aclose()
189
+ self._client = None