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,562 @@
1
+ """
2
+ Unified Context Management System for Agent Control Plane.
3
+
4
+ Manages contextual settings (knowledge, resources, policies) across all entity types:
5
+ - Environments
6
+ - Teams
7
+ - Projects
8
+ - Agents
9
+
10
+ Provides layered context resolution: Environment → Team → Project → Agent
11
+ """
12
+
13
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
14
+ from typing import List, Optional, Dict, Any, Literal
15
+ from datetime import datetime
16
+ from pydantic import BaseModel, Field
17
+ import structlog
18
+ import uuid
19
+
20
+ from control_plane_api.app.middleware.auth import get_current_organization
21
+ from control_plane_api.app.lib.supabase import get_supabase
22
+
23
+ logger = structlog.get_logger()
24
+
25
+ router = APIRouter()
26
+
27
+ # Entity types that support context
28
+ EntityType = Literal["environment", "team", "project", "agent"]
29
+
30
+ # Pydantic schemas
31
+ class ContextData(BaseModel):
32
+ """Generic context data structure"""
33
+ knowledge_uuids: List[str] = Field(default_factory=list, description="Knowledge base UUIDs")
34
+ resource_ids: List[str] = Field(default_factory=list, description="Resource IDs from Meilisearch")
35
+ policy_ids: List[str] = Field(default_factory=list, description="OPA policy IDs")
36
+
37
+
38
+ class UpdateContextRequest(BaseModel):
39
+ """Request to update context for any entity"""
40
+ knowledge_uuids: List[str] = Field(default_factory=list)
41
+ resource_ids: List[str] = Field(default_factory=list)
42
+ policy_ids: List[str] = Field(default_factory=list)
43
+
44
+
45
+ class ContextResponse(BaseModel):
46
+ """Generic context response"""
47
+ id: str
48
+ entity_type: str
49
+ entity_id: str
50
+ organization_id: str
51
+ knowledge_uuids: List[str]
52
+ resource_ids: List[str]
53
+ policy_ids: List[str]
54
+ created_at: str
55
+ updated_at: str
56
+
57
+
58
+ class ResolvedContextResponse(BaseModel):
59
+ """Resolved context with inheritance from all layers"""
60
+ entity_id: str
61
+ entity_type: str
62
+ environment_id: Optional[str] = None
63
+ team_id: Optional[str] = None
64
+ project_id: Optional[str] = None
65
+
66
+ # Aggregated context from all layers
67
+ knowledge_uuids: List[str] = Field(description="Merged knowledge from all layers")
68
+ resource_ids: List[str] = Field(description="Merged resources from all layers")
69
+ policy_ids: List[str] = Field(description="Merged policies from all layers")
70
+
71
+ # Layer breakdown for debugging
72
+ layers: Dict[str, ContextData] = Field(description="Context breakdown by layer")
73
+
74
+
75
+ # Table name mapping
76
+ CONTEXT_TABLE_MAP = {
77
+ "environment": "environment_contexts",
78
+ "team": "team_contexts",
79
+ "project": "project_contexts",
80
+ "agent": "agent_contexts",
81
+ }
82
+
83
+ # Entity table mapping (for validation)
84
+ ENTITY_TABLE_MAP = {
85
+ "environment": "environments",
86
+ "team": "teams",
87
+ "project": "projects",
88
+ "agent": "agents",
89
+ }
90
+
91
+
92
+ async def _verify_entity_exists(
93
+ client, entity_type: EntityType, entity_id: str, org_id: str
94
+ ) -> bool:
95
+ """Verify that an entity exists for the organization"""
96
+ table_name = ENTITY_TABLE_MAP.get(entity_type)
97
+ if not table_name:
98
+ return False
99
+
100
+ result = (
101
+ client.table(table_name)
102
+ .select("id")
103
+ .eq("id", entity_id)
104
+ .eq("organization_id", org_id)
105
+ .single()
106
+ .execute()
107
+ )
108
+
109
+ return bool(result.data)
110
+
111
+
112
+ async def _get_or_create_context(
113
+ client, entity_type: EntityType, entity_id: str, org_id: str
114
+ ) -> Dict[str, Any]:
115
+ """Get existing context or create a default one"""
116
+ table_name = CONTEXT_TABLE_MAP.get(entity_type)
117
+ if not table_name:
118
+ raise ValueError(f"Invalid entity type: {entity_type}")
119
+
120
+ # Try to get existing context
121
+ result = (
122
+ client.table(table_name)
123
+ .select("*")
124
+ .eq(f"{entity_type}_id", entity_id)
125
+ .eq("organization_id", org_id)
126
+ .single()
127
+ .execute()
128
+ )
129
+
130
+ if result.data:
131
+ return result.data
132
+
133
+ # Create default context
134
+ context_id = str(uuid.uuid4())
135
+ now = datetime.utcnow().isoformat()
136
+
137
+ default_context = {
138
+ "id": context_id,
139
+ f"{entity_type}_id": entity_id,
140
+ "entity_type": entity_type,
141
+ "organization_id": org_id,
142
+ "knowledge_uuids": [],
143
+ "resource_ids": [],
144
+ "policy_ids": [],
145
+ "created_at": now,
146
+ "updated_at": now,
147
+ }
148
+
149
+ insert_result = client.table(table_name).insert(default_context).execute()
150
+
151
+ if not insert_result.data:
152
+ raise Exception(f"Failed to create {entity_type} context")
153
+
154
+ logger.info(
155
+ "context_created",
156
+ entity_type=entity_type,
157
+ entity_id=entity_id,
158
+ org_id=org_id,
159
+ )
160
+
161
+ return insert_result.data[0]
162
+
163
+
164
+ @router.get("/context/{entity_type}/{entity_id}", response_model=ContextResponse)
165
+ async def get_context(
166
+ entity_type: EntityType,
167
+ entity_id: str,
168
+ request: Request,
169
+ organization: dict = Depends(get_current_organization),
170
+ ):
171
+ """Get context configuration for any entity type"""
172
+ try:
173
+ client = get_supabase()
174
+ org_id = organization["id"]
175
+
176
+ # Verify entity exists
177
+ if not await _verify_entity_exists(client, entity_type, entity_id, org_id):
178
+ raise HTTPException(
179
+ status_code=status.HTTP_404_NOT_FOUND,
180
+ detail=f"{entity_type.capitalize()} not found"
181
+ )
182
+
183
+ # Get or create context
184
+ context_data = await _get_or_create_context(client, entity_type, entity_id, org_id)
185
+
186
+ return ContextResponse(**context_data)
187
+
188
+ except HTTPException:
189
+ raise
190
+ except Exception as e:
191
+ logger.error("get_context_failed", error=str(e), entity_type=entity_type, entity_id=entity_id)
192
+ raise HTTPException(
193
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
194
+ detail=f"Failed to get {entity_type} context: {str(e)}"
195
+ )
196
+
197
+
198
+ @router.put("/context/{entity_type}/{entity_id}", response_model=ContextResponse)
199
+ async def update_context(
200
+ entity_type: EntityType,
201
+ entity_id: str,
202
+ context_data: UpdateContextRequest,
203
+ request: Request,
204
+ organization: dict = Depends(get_current_organization),
205
+ ):
206
+ """Update context configuration for any entity type"""
207
+ try:
208
+ client = get_supabase()
209
+ org_id = organization["id"]
210
+
211
+ # Verify entity exists
212
+ if not await _verify_entity_exists(client, entity_type, entity_id, org_id):
213
+ raise HTTPException(
214
+ status_code=status.HTTP_404_NOT_FOUND,
215
+ detail=f"{entity_type.capitalize()} not found"
216
+ )
217
+
218
+ table_name = CONTEXT_TABLE_MAP[entity_type]
219
+ now = datetime.utcnow().isoformat()
220
+
221
+ # Check if context exists
222
+ existing = (
223
+ client.table(table_name)
224
+ .select("id")
225
+ .eq(f"{entity_type}_id", entity_id)
226
+ .eq("organization_id", org_id)
227
+ .single()
228
+ .execute()
229
+ )
230
+
231
+ update_data = {
232
+ "knowledge_uuids": context_data.knowledge_uuids,
233
+ "resource_ids": context_data.resource_ids,
234
+ "policy_ids": context_data.policy_ids,
235
+ "updated_at": now,
236
+ }
237
+
238
+ if existing.data:
239
+ # Update existing
240
+ result = (
241
+ client.table(table_name)
242
+ .update(update_data)
243
+ .eq("id", existing.data["id"])
244
+ .execute()
245
+ )
246
+ else:
247
+ # Create new
248
+ context_id = str(uuid.uuid4())
249
+ new_context = {
250
+ "id": context_id,
251
+ f"{entity_type}_id": entity_id,
252
+ "entity_type": entity_type,
253
+ "organization_id": org_id,
254
+ **update_data,
255
+ "created_at": now,
256
+ }
257
+ result = client.table(table_name).insert(new_context).execute()
258
+
259
+ if not result.data:
260
+ raise Exception(f"Failed to update {entity_type} context")
261
+
262
+ logger.info(
263
+ "context_updated",
264
+ entity_type=entity_type,
265
+ entity_id=entity_id,
266
+ knowledge_count=len(context_data.knowledge_uuids),
267
+ resource_count=len(context_data.resource_ids),
268
+ policy_count=len(context_data.policy_ids),
269
+ org_id=org_id,
270
+ )
271
+
272
+ return ContextResponse(**result.data[0])
273
+
274
+ except HTTPException:
275
+ raise
276
+ except Exception as e:
277
+ logger.error("update_context_failed", error=str(e), entity_type=entity_type, entity_id=entity_id)
278
+ raise HTTPException(
279
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
280
+ detail=f"Failed to update {entity_type} context: {str(e)}"
281
+ )
282
+
283
+
284
+ @router.delete("/context/{entity_type}/{entity_id}", status_code=status.HTTP_204_NO_CONTENT)
285
+ async def clear_context(
286
+ entity_type: EntityType,
287
+ entity_id: str,
288
+ request: Request,
289
+ organization: dict = Depends(get_current_organization),
290
+ ):
291
+ """Clear all context for an entity"""
292
+ try:
293
+ client = get_supabase()
294
+ org_id = organization["id"]
295
+
296
+ table_name = CONTEXT_TABLE_MAP[entity_type]
297
+ now = datetime.utcnow().isoformat()
298
+
299
+ update_data = {
300
+ "knowledge_uuids": [],
301
+ "resource_ids": [],
302
+ "policy_ids": [],
303
+ "updated_at": now,
304
+ }
305
+
306
+ client.table(table_name).update(update_data).eq(f"{entity_type}_id", entity_id).eq("organization_id", org_id).execute()
307
+
308
+ logger.info("context_cleared", entity_type=entity_type, entity_id=entity_id, org_id=org_id)
309
+ return None
310
+
311
+ except Exception as e:
312
+ logger.error("clear_context_failed", error=str(e), entity_type=entity_type, entity_id=entity_id)
313
+ raise HTTPException(
314
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
315
+ detail=f"Failed to clear {entity_type} context: {str(e)}"
316
+ )
317
+
318
+
319
+ @router.get("/context/resolve/{entity_type}/{entity_id}", response_model=ResolvedContextResponse)
320
+ async def resolve_context(
321
+ entity_type: EntityType,
322
+ entity_id: str,
323
+ request: Request,
324
+ organization: dict = Depends(get_current_organization),
325
+ ):
326
+ """
327
+ Resolve context with inheritance from all layers.
328
+
329
+ Resolution order (each layer adds to the previous):
330
+ 1. ALL Environments (many-to-many for agents/teams)
331
+ 2. Team (if member of a team)
332
+ 3. ALL Team Environments (if agent is part of team)
333
+ 4. Project (if assigned to a project)
334
+ 5. Agent/Entity itself
335
+
336
+ Returns merged context with full layer breakdown.
337
+ """
338
+ try:
339
+ client = get_supabase()
340
+ org_id = organization["id"]
341
+
342
+ # Verify entity exists
343
+ if not await _verify_entity_exists(client, entity_type, entity_id, org_id):
344
+ raise HTTPException(
345
+ status_code=status.HTTP_404_NOT_FOUND,
346
+ detail=f"{entity_type.capitalize()} not found"
347
+ )
348
+
349
+ layers: Dict[str, ContextData] = {}
350
+ team_id: Optional[str] = None
351
+ project_id: Optional[str] = None
352
+ environment_ids: List[str] = []
353
+
354
+ # Collect context from all layers
355
+ all_knowledge: List[str] = []
356
+ all_resources: List[str] = []
357
+ all_policies: List[str] = []
358
+
359
+ # 1. Get entity relationships (team, project)
360
+ entity_table = ENTITY_TABLE_MAP[entity_type]
361
+ entity_result = (
362
+ client.table(entity_table)
363
+ .select("team_id, project_id")
364
+ .eq("id", entity_id)
365
+ .eq("organization_id", org_id)
366
+ .single()
367
+ .execute()
368
+ )
369
+
370
+ entity_data = entity_result.data if entity_result.data else {}
371
+
372
+ # Extract relationships
373
+ team_id = entity_data.get("team_id")
374
+ project_id = entity_data.get("project_id")
375
+
376
+ # 2. Layer 1: Get ALL agent/team environments (many-to-many)
377
+ if entity_type == "agent":
378
+ # Get all agent environments
379
+ agent_env_result = (
380
+ client.table("agent_environments")
381
+ .select("environment_id")
382
+ .eq("agent_id", entity_id)
383
+ .execute()
384
+ )
385
+ environment_ids = [env["environment_id"] for env in (agent_env_result.data or [])]
386
+
387
+ elif entity_type == "team":
388
+ # Get all team environments
389
+ team_env_result = (
390
+ client.table("team_environments")
391
+ .select("environment_id")
392
+ .eq("team_id", entity_id)
393
+ .execute()
394
+ )
395
+ environment_ids = [env["environment_id"] for env in (team_env_result.data or [])]
396
+
397
+ # Merge context from ALL environments
398
+ for idx, env_id in enumerate(environment_ids):
399
+ try:
400
+ env_context = await _get_or_create_context(client, "environment", env_id, org_id)
401
+ layer_key = f"environment_{idx+1}" if len(environment_ids) > 1 else "environment"
402
+ layers[layer_key] = ContextData(
403
+ knowledge_uuids=env_context.get("knowledge_uuids", []),
404
+ resource_ids=env_context.get("resource_ids", []),
405
+ policy_ids=env_context.get("policy_ids", []),
406
+ )
407
+ all_knowledge.extend(layers[layer_key].knowledge_uuids)
408
+ all_resources.extend(layers[layer_key].resource_ids)
409
+ all_policies.extend(layers[layer_key].policy_ids)
410
+ except Exception as e:
411
+ logger.warning("failed_to_get_environment_context", error=str(e), environment_id=env_id)
412
+
413
+ # 3. Layer 2: Team context
414
+ if team_id:
415
+ try:
416
+ team_context = await _get_or_create_context(client, "team", team_id, org_id)
417
+ layers["team"] = ContextData(
418
+ knowledge_uuids=team_context.get("knowledge_uuids", []),
419
+ resource_ids=team_context.get("resource_ids", []),
420
+ policy_ids=team_context.get("policy_ids", []),
421
+ )
422
+ all_knowledge.extend(layers["team"].knowledge_uuids)
423
+ all_resources.extend(layers["team"].resource_ids)
424
+ all_policies.extend(layers["team"].policy_ids)
425
+ except Exception as e:
426
+ logger.warning("failed_to_get_team_context", error=str(e), team_id=team_id)
427
+
428
+ # 3b. Get ALL team environments (if agent has team)
429
+ if entity_type == "agent":
430
+ team_env_result = (
431
+ client.table("team_environments")
432
+ .select("environment_id")
433
+ .eq("team_id", team_id)
434
+ .execute()
435
+ )
436
+ team_environment_ids = [env["environment_id"] for env in (team_env_result.data or [])]
437
+
438
+ for idx, env_id in enumerate(team_environment_ids):
439
+ # Skip if already processed in agent environments
440
+ if env_id in environment_ids:
441
+ continue
442
+ try:
443
+ env_context = await _get_or_create_context(client, "environment", env_id, org_id)
444
+ layer_key = f"team_environment_{idx+1}"
445
+ layers[layer_key] = ContextData(
446
+ knowledge_uuids=env_context.get("knowledge_uuids", []),
447
+ resource_ids=env_context.get("resource_ids", []),
448
+ policy_ids=env_context.get("policy_ids", []),
449
+ )
450
+ all_knowledge.extend(layers[layer_key].knowledge_uuids)
451
+ all_resources.extend(layers[layer_key].resource_ids)
452
+ all_policies.extend(layers[layer_key].policy_ids)
453
+ except Exception as e:
454
+ logger.warning("failed_to_get_team_environment_context", error=str(e), environment_id=env_id)
455
+
456
+ # 4. Layer 3: Project context
457
+ if project_id:
458
+ try:
459
+ project_context = await _get_or_create_context(client, "project", project_id, org_id)
460
+ layers["project"] = ContextData(
461
+ knowledge_uuids=project_context.get("knowledge_uuids", []),
462
+ resource_ids=project_context.get("resource_ids", []),
463
+ policy_ids=project_context.get("policy_ids", []),
464
+ )
465
+ all_knowledge.extend(layers["project"].knowledge_uuids)
466
+ all_resources.extend(layers["project"].resource_ids)
467
+ all_policies.extend(layers["project"].policy_ids)
468
+ except Exception as e:
469
+ logger.warning("failed_to_get_project_context", error=str(e), project_id=project_id)
470
+
471
+ # 5. Layer 4: Entity's own context
472
+ try:
473
+ entity_context = await _get_or_create_context(client, entity_type, entity_id, org_id)
474
+ layers[entity_type] = ContextData(
475
+ knowledge_uuids=entity_context.get("knowledge_uuids", []),
476
+ resource_ids=entity_context.get("resource_ids", []),
477
+ policy_ids=entity_context.get("policy_ids", []),
478
+ )
479
+ all_knowledge.extend(layers[entity_type].knowledge_uuids)
480
+ all_resources.extend(layers[entity_type].resource_ids)
481
+ all_policies.extend(layers[entity_type].policy_ids)
482
+ except Exception as e:
483
+ logger.warning("failed_to_get_entity_context", error=str(e), entity_type=entity_type, entity_id=entity_id)
484
+
485
+ # Deduplicate while preserving order
486
+ unique_knowledge = list(dict.fromkeys(all_knowledge))
487
+ unique_resources = list(dict.fromkeys(all_resources))
488
+ unique_policies = list(dict.fromkeys(all_policies))
489
+
490
+ logger.info(
491
+ "context_resolved",
492
+ entity_type=entity_type,
493
+ entity_id=entity_id,
494
+ layers_count=len(layers),
495
+ environment_count=len(environment_ids),
496
+ total_knowledge=len(unique_knowledge),
497
+ total_resources=len(unique_resources),
498
+ total_policies=len(unique_policies),
499
+ org_id=org_id,
500
+ )
501
+
502
+ return ResolvedContextResponse(
503
+ entity_id=entity_id,
504
+ entity_type=entity_type,
505
+ environment_id=environment_ids[0] if environment_ids else None,
506
+ team_id=team_id,
507
+ project_id=project_id,
508
+ knowledge_uuids=unique_knowledge,
509
+ resource_ids=unique_resources,
510
+ policy_ids=unique_policies,
511
+ layers=layers,
512
+ )
513
+
514
+ except HTTPException:
515
+ raise
516
+ except Exception as e:
517
+ logger.error("resolve_context_failed", error=str(e), entity_type=entity_type, entity_id=entity_id)
518
+ raise HTTPException(
519
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
520
+ detail=f"Failed to resolve {entity_type} context: {str(e)}"
521
+ )
522
+
523
+
524
+ # Convenience endpoints for workers
525
+ @router.get("/agents/{agent_id}/context/resolved", response_model=ResolvedContextResponse)
526
+ async def resolve_agent_context(
527
+ agent_id: str,
528
+ request: Request,
529
+ organization: dict = Depends(get_current_organization),
530
+ ):
531
+ """
532
+ Convenience endpoint to resolve full context for an agent.
533
+
534
+ Fetches and merges context from:
535
+ 1. ALL agent environments (many-to-many)
536
+ 2. Team (if agent belongs to a team)
537
+ 3. ALL team environments
538
+ 4. Project (if assigned)
539
+ 5. Agent's own context
540
+
541
+ Workers should call this endpoint to get all knowledge, resources, and policies.
542
+ """
543
+ return await resolve_context("agent", agent_id, request, organization)
544
+
545
+
546
+ @router.get("/teams/{team_id}/context/resolved", response_model=ResolvedContextResponse)
547
+ async def resolve_team_context(
548
+ team_id: str,
549
+ request: Request,
550
+ organization: dict = Depends(get_current_organization),
551
+ ):
552
+ """
553
+ Convenience endpoint to resolve full context for a team.
554
+
555
+ Fetches and merges context from:
556
+ 1. ALL team environments (many-to-many)
557
+ 2. Project (if assigned)
558
+ 3. Team's own context
559
+
560
+ Workers should call this endpoint to get all knowledge, resources, and policies.
561
+ """
562
+ return await resolve_context("team", team_id, request, organization)