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,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)
|