GT AI OS Community v2.0.33 - Add NVIDIA NIM and Nemotron agents

- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2
- Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2
  - Made more general-purpose (flexible targets, expanded tools)
- Added nemotron-mini-agent.csv for fast local inference via Ollama
- Added nemotron-agent.csv for advanced reasoning via Ollama
- Added wiki page: Projects for NVIDIA NIMs and Nemotron
This commit is contained in:
HackWeasel
2025-12-12 17:47:14 -05:00
commit 310491a557
750 changed files with 232701 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
"""
Analytics and Dremio SQL Federation Endpoints
"""
from typing import List, Dict, Any, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from app.core.database import get_db
from app.services.dremio_service import DremioService
from app.core.auth import get_current_user
from app.models.user import User
router = APIRouter(prefix="/api/v1/analytics", tags=["Analytics"])
class TenantDashboardResponse(BaseModel):
"""Response model for tenant dashboard data"""
tenant: Dict[str, Any]
metrics: Dict[str, Any]
analytics: Dict[str, Any]
alerts: List[Dict[str, Any]]
class CustomQueryRequest(BaseModel):
"""Request model for custom analytics queries"""
query_type: str
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
class DatasetCreationResponse(BaseModel):
"""Response model for dataset creation"""
tenant_id: int
datasets_created: List[str]
status: str
@router.get("/dashboard/{tenant_id}", response_model=TenantDashboardResponse)
async def get_tenant_dashboard(
tenant_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get comprehensive dashboard data for a tenant using Dremio SQL federation"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to view dashboard"
)
service = DremioService(db)
try:
dashboard_data = await service.get_tenant_dashboard_data(tenant_id)
return TenantDashboardResponse(**dashboard_data)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch dashboard data: {str(e)}"
)
@router.post("/query/{tenant_id}")
async def execute_custom_analytics(
tenant_id: int,
request: CustomQueryRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Execute custom analytics queries for a tenant"""
# Check permissions (only admins)
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions for analytics queries"
)
service = DremioService(db)
try:
results = await service.get_custom_analytics(
tenant_id=tenant_id,
query_type=request.query_type,
start_date=request.start_date,
end_date=request.end_date
)
return {
"query_type": request.query_type,
"results": results,
"count": len(results)
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Query execution failed: {str(e)}"
)
@router.post("/datasets/create/{tenant_id}", response_model=DatasetCreationResponse)
async def create_virtual_datasets(
tenant_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create Dremio virtual datasets for tenant analytics"""
# Check permissions (only GT admin)
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only GT admins can create virtual datasets"
)
service = DremioService(db)
try:
result = await service.create_virtual_datasets(tenant_id)
return DatasetCreationResponse(**result)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create datasets: {str(e)}"
)
@router.get("/metrics/performance/{tenant_id}")
async def get_performance_metrics(
tenant_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get real-time performance metrics for a tenant"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to view metrics"
)
if current_user.user_type == 'tenant_admin' and current_user.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot view metrics for other tenants"
)
service = DremioService(db)
try:
metrics = await service._get_performance_metrics(tenant_id)
return metrics
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch metrics: {str(e)}"
)
@router.get("/alerts/{tenant_id}")
async def get_security_alerts(
tenant_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get security and operational alerts for a tenant"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to view alerts"
)
if current_user.user_type == 'tenant_admin' and current_user.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot view alerts for other tenants"
)
service = DremioService(db)
try:
alerts = await service._get_security_alerts(tenant_id)
return {
"tenant_id": tenant_id,
"alerts": alerts,
"total": len(alerts),
"critical": len([a for a in alerts if a.get('severity') == 'critical']),
"warning": len([a for a in alerts if a.get('severity') == 'warning']),
"info": len([a for a in alerts if a.get('severity') == 'info'])
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch alerts: {str(e)}"
)
@router.get("/query-types")
async def get_available_query_types(
current_user: User = Depends(get_current_user)
):
"""Get list of available analytics query types"""
return {
"query_types": [
{
"id": "user_activity",
"name": "User Activity Analysis",
"description": "Analyze user activity, token usage, and costs"
},
{
"id": "resource_trends",
"name": "Resource Usage Trends",
"description": "View resource usage trends over time"
},
{
"id": "cost_optimization",
"name": "Cost Optimization Report",
"description": "Identify cost optimization opportunities"
}
]
}

View File

@@ -0,0 +1,259 @@
"""
API Key Management Endpoints
"""
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from app.core.database import get_db
from app.services.api_key_service import APIKeyService
from app.core.auth import get_current_user
from app.models.user import User
router = APIRouter(prefix="/api/v1/api-keys", tags=["API Keys"])
class SetAPIKeyRequest(BaseModel):
"""Request model for setting an API key"""
tenant_id: int
provider: str
api_key: str
api_secret: Optional[str] = None
enabled: bool = True
metadata: Optional[Dict[str, Any]] = None
class APIKeyResponse(BaseModel):
"""Response model for API key operations"""
tenant_id: int
provider: str
enabled: bool
updated_at: str
class APIKeyStatusResponse(BaseModel):
"""Response model for API key status"""
configured: bool
enabled: bool
updated_at: Optional[str]
metadata: Optional[Dict[str, Any]]
class TestAPIKeyResponse(BaseModel):
"""Response model for API key testing"""
provider: str
valid: bool
message: str
status_code: Optional[int] = None
error: Optional[str] = None
error_type: Optional[str] = None # auth_failed, rate_limited, invalid_format, insufficient_permissions
rate_limit_remaining: Optional[int] = None
rate_limit_reset: Optional[str] = None
models_available: Optional[int] = None # Count of models accessible with this key
@router.post("/set", response_model=APIKeyResponse)
async def set_api_key(
request: SetAPIKeyRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Set or update an API key for a tenant"""
# Check permissions (must be GT admin or tenant admin)
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to manage API keys"
)
service = APIKeyService(db)
try:
result = await service.set_api_key(
tenant_id=request.tenant_id,
provider=request.provider,
api_key=request.api_key,
api_secret=request.api_secret,
enabled=request.enabled,
metadata=request.metadata
)
return APIKeyResponse(**result)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to set API key: {str(e)}"
)
@router.get("/tenant/{tenant_id}", response_model=Dict[str, APIKeyStatusResponse])
async def get_tenant_api_keys(
tenant_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all API keys for a tenant (without decryption)"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to view API keys"
)
service = APIKeyService(db)
try:
api_keys = await service.get_api_keys(tenant_id)
return {
provider: APIKeyStatusResponse(**info)
for provider, info in api_keys.items()
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
@router.post("/test/{tenant_id}/{provider}", response_model=TestAPIKeyResponse)
async def test_api_key(
tenant_id: int,
provider: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Test if an API key is valid"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to test API keys"
)
service = APIKeyService(db)
try:
result = await service.test_api_key(tenant_id, provider)
return TestAPIKeyResponse(**result)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Test failed: {str(e)}"
)
@router.put("/disable/{tenant_id}/{provider}")
async def disable_api_key(
tenant_id: int,
provider: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Disable an API key without removing it"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to manage API keys"
)
service = APIKeyService(db)
try:
success = await service.disable_api_key(tenant_id, provider)
return {"success": success, "provider": provider, "enabled": False}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
@router.delete("/remove/{tenant_id}/{provider}")
async def remove_api_key(
tenant_id: int,
provider: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Completely remove an API key"""
# Check permissions (only GT admin can remove)
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only GT admins can remove API keys"
)
service = APIKeyService(db)
try:
success = await service.remove_api_key(tenant_id, provider)
if success:
return {"success": True, "message": f"API key for {provider} removed"}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"API key for {provider} not found"
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
@router.get("/providers", response_model=List[Dict[str, Any]])
async def get_supported_providers(
current_user: User = Depends(get_current_user)
):
"""Get list of supported API key providers"""
return APIKeyService.get_supported_providers()
@router.get("/usage/{tenant_id}/{provider}")
async def get_api_key_usage(
tenant_id: int,
provider: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get usage statistics for an API key"""
# Check permissions
if current_user.user_type != 'super_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to view usage"
)
service = APIKeyService(db)
try:
usage = await service.get_api_key_usage(tenant_id, provider)
return usage
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,760 @@
"""
Resource Management API for GT 2.0 Control Panel
Provides comprehensive resource allocation and monitoring capabilities for admins.
"""
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.user import User
from app.services.resource_allocation import ResourceAllocationService, ResourceType
router = APIRouter(prefix="/resource-management", tags=["Resource Management"])
# Pydantic models
class ResourceAllocationRequest(BaseModel):
tenant_id: int
template: str = Field(..., description="Resource template (startup, standard, enterprise)")
class ResourceScalingRequest(BaseModel):
tenant_id: int
resource_type: str = Field(..., description="Resource type to scale")
scale_factor: float = Field(..., ge=0.1, le=10.0, description="Scaling factor (1.0 = no change)")
class ResourceUsageUpdateRequest(BaseModel):
tenant_id: int
resource_type: str
usage_delta: float = Field(..., description="Change in usage (positive or negative)")
class ResourceQuotaResponse(BaseModel):
id: int
tenant_id: int
resource_type: str
max_value: float
current_usage: float
usage_percentage: float
warning_threshold: float
critical_threshold: float
unit: str
cost_per_unit: float
is_active: bool
created_at: str
updated_at: str
class ResourceUsageResponse(BaseModel):
resource_type: str
current_usage: float
max_allowed: float
percentage_used: float
cost_accrued: float
last_updated: str
class ResourceAlertResponse(BaseModel):
id: int
tenant_id: int
resource_type: str
alert_level: str
message: str
current_usage: float
max_value: float
percentage_used: float
acknowledged: bool
acknowledged_by: Optional[str]
acknowledged_at: Optional[str]
created_at: str
class SystemResourceOverviewResponse(BaseModel):
timestamp: str
resource_overview: Dict[str, Any]
total_tenants: int
class TenantCostResponse(BaseModel):
tenant_id: int
period_start: str
period_end: str
total_cost: float
costs_by_resource: Dict[str, Any]
currency: str
@router.post("/allocate", status_code=status.HTTP_201_CREATED)
async def allocate_tenant_resources(
request: ResourceAllocationRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Allocate initial resources to a tenant based on template.
"""
# Check admin permissions
if current_user.user_type != "super_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Super admin privileges required"
)
try:
service = ResourceAllocationService(db)
success = await service.allocate_resources(request.tenant_id, request.template)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to allocate resources"
)
return {"message": "Resources allocated successfully", "tenant_id": request.tenant_id}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Resource allocation failed: {str(e)}"
)
@router.get("/tenant/{tenant_id}/usage", response_model=Dict[str, ResourceUsageResponse])
async def get_tenant_resource_usage(
tenant_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get current resource usage for a specific tenant.
"""
# Check permissions
if current_user.user_type != "super_admin":
# Regular users can only view their own tenant
if current_user.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
try:
service = ResourceAllocationService(db)
usage_data = await service.get_tenant_resource_usage(tenant_id)
# Convert to response format
response = {}
for resource_type, data in usage_data.items():
response[resource_type] = ResourceUsageResponse(
resource_type=data.resource_type.value,
current_usage=data.current_usage,
max_allowed=data.max_allowed,
percentage_used=data.percentage_used,
cost_accrued=data.cost_accrued,
last_updated=data.last_updated.isoformat()
)
return response
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get resource usage: {str(e)}"
)
@router.post("/usage/update")
async def update_resource_usage(
request: ResourceUsageUpdateRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Update resource usage for a tenant (usually called by services).
"""
# This endpoint is typically called by services, so we allow tenant users for their own tenant
if current_user.user_type != "super_admin":
if current_user.tenant_id != request.tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
try:
# Validate resource type
try:
resource_type = ResourceType(request.resource_type)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid resource type: {request.resource_type}"
)
service = ResourceAllocationService(db)
success = await service.update_resource_usage(
request.tenant_id,
resource_type,
request.usage_delta
)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to update resource usage (quota exceeded or not found)"
)
return {"message": "Resource usage updated successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update resource usage: {str(e)}"
)
@router.post("/scale")
async def scale_tenant_resources(
request: ResourceScalingRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Scale tenant resources up or down.
"""
# Check admin permissions
if current_user.user_type != "super_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Super admin privileges required"
)
try:
# Validate resource type
try:
resource_type = ResourceType(request.resource_type)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid resource type: {request.resource_type}"
)
service = ResourceAllocationService(db)
success = await service.scale_tenant_resources(
request.tenant_id,
resource_type,
request.scale_factor
)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to scale resources"
)
return {
"message": "Resources scaled successfully",
"tenant_id": request.tenant_id,
"resource_type": request.resource_type,
"scale_factor": request.scale_factor
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to scale resources: {str(e)}"
)
@router.get("/tenant/{tenant_id}/costs", response_model=TenantCostResponse)
async def get_tenant_costs(
tenant_id: int,
start_date: Optional[str] = Query(None, description="Start date (ISO format)"),
end_date: Optional[str] = Query(None, description="End date (ISO format)"),
days: int = Query(30, ge=1, le=365, description="Days back from now if dates not specified"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get cost breakdown for a tenant over a date range.
"""
# Check permissions
if current_user.user_type != "super_admin":
if current_user.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
try:
# Parse dates
if start_date and end_date:
start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
else:
end_dt = datetime.utcnow()
start_dt = end_dt - timedelta(days=days)
service = ResourceAllocationService(db)
cost_data = await service.get_tenant_costs(tenant_id, start_dt, end_dt)
if not cost_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No cost data found for tenant"
)
return TenantCostResponse(**cost_data)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get tenant costs: {str(e)}"
)
@router.get("/alerts", response_model=List[ResourceAlertResponse])
async def get_resource_alerts(
tenant_id: Optional[int] = Query(None, description="Filter by tenant ID"),
hours: int = Query(24, ge=1, le=168, description="Hours back to look for alerts"),
alert_level: Optional[str] = Query(None, description="Filter by alert level"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get resource alerts for tenant(s).
"""
# Check permissions
if current_user.user_type != "super_admin":
# Regular users can only see their own tenant alerts
if tenant_id and current_user.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
tenant_id = current_user.tenant_id
try:
service = ResourceAllocationService(db)
alerts = await service.get_resource_alerts(tenant_id, hours)
# Filter by alert level if specified
if alert_level:
alerts = [alert for alert in alerts if alert['alert_level'] == alert_level]
return [ResourceAlertResponse(**alert) for alert in alerts]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get resource alerts: {str(e)}"
)
@router.get("/system/overview", response_model=SystemResourceOverviewResponse)
async def get_system_resource_overview(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get system-wide resource usage overview (admin only).
"""
# Check admin permissions
if current_user.user_type != "super_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Super admin privileges required"
)
try:
service = ResourceAllocationService(db)
overview = await service.get_system_resource_overview()
if not overview:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No system resource data available"
)
return SystemResourceOverviewResponse(**overview)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get system overview: {str(e)}"
)
@router.post("/alerts/{alert_id}/acknowledge")
async def acknowledge_alert(
alert_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Acknowledge a resource alert.
"""
try:
from app.models.resource_usage import ResourceAlert
from sqlalchemy import select, update
# Get the alert
result = await db.execute(select(ResourceAlert).where(ResourceAlert.id == alert_id))
alert = result.scalar_one_or_none()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Alert not found"
)
# Check permissions
if current_user.user_type != "super_admin":
if current_user.tenant_id != alert.tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
# Acknowledge the alert
alert.acknowledge(current_user.email)
await db.commit()
return {"message": "Alert acknowledged successfully", "alert_id": alert_id}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to acknowledge alert: {str(e)}"
)
@router.get("/templates")
async def get_resource_templates(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get available resource allocation templates.
"""
try:
# Return hardcoded templates for now
templates = {
"startup": {
"name": "startup",
"display_name": "Startup",
"description": "Basic resources for small teams and development",
"monthly_cost": 99.0,
"resources": {
"cpu": {"limit": 2.0, "unit": "cores"},
"memory": {"limit": 4096, "unit": "MB"},
"storage": {"limit": 10240, "unit": "MB"},
"api_calls": {"limit": 10000, "unit": "calls/hour"},
"model_inference": {"limit": 1000, "unit": "tokens"}
}
},
"standard": {
"name": "standard",
"display_name": "Standard",
"description": "Standard resources for production workloads",
"monthly_cost": 299.0,
"resources": {
"cpu": {"limit": 4.0, "unit": "cores"},
"memory": {"limit": 8192, "unit": "MB"},
"storage": {"limit": 51200, "unit": "MB"},
"api_calls": {"limit": 50000, "unit": "calls/hour"},
"model_inference": {"limit": 10000, "unit": "tokens"}
}
},
"enterprise": {
"name": "enterprise",
"display_name": "Enterprise",
"description": "High-performance resources for large organizations",
"monthly_cost": 999.0,
"resources": {
"cpu": {"limit": 16.0, "unit": "cores"},
"memory": {"limit": 32768, "unit": "MB"},
"storage": {"limit": 102400, "unit": "MB"},
"api_calls": {"limit": 200000, "unit": "calls/hour"},
"model_inference": {"limit": 100000, "unit": "tokens"},
"gpu_time": {"limit": 1000, "unit": "minutes"}
}
}
}
return {"templates": templates}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get resource templates: {str(e)}"
)
# Agent Library Templates Endpoints
class AssistantTemplateRequest(BaseModel):
name: str
description: str
category: str
icon: str = "🤖"
system_prompt: str
capabilities: List[str] = []
tags: List[str] = []
access_groups: List[str] = []
class AssistantTemplateResponse(BaseModel):
id: str
template_id: str
name: str
description: str
category: str
icon: str
version: str
status: str
access_groups: List[str]
deployment_count: int
active_instances: int
popularity_score: int
last_updated: str
created_by: str
created_at: str
capabilities: List[str]
prompt_preview: str
tags: List[str]
compatibility: List[str]
@router.get("/templates/", response_model=dict)
async def list_agent_templates(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
category: Optional[str] = Query(None),
status: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
List agent templates for the agent library.
"""
try:
# Mock data for now - replace with actual database queries
mock_templates = [
{
"id": "1",
"template_id": "cybersec_analyst",
"name": "Cybersecurity Analyst",
"description": "AI agent specialized in cybersecurity analysis, threat detection, and incident response",
"category": "cybersecurity",
"icon": "🛡️",
"version": "1.2.0",
"status": "published",
"access_groups": ["security_team", "admin"],
"deployment_count": 15,
"active_instances": 8,
"popularity_score": 92,
"last_updated": "2024-01-15T10:30:00Z",
"created_by": "admin@gt2.com",
"created_at": "2024-01-10T14:20:00Z",
"capabilities": ["threat_analysis", "log_analysis", "incident_response", "compliance_check"],
"prompt_preview": "You are a cybersecurity analyst agent...",
"tags": ["security", "analysis", "incident"],
"compatibility": ["gpt-4", "claude-3"]
},
{
"id": "2",
"template_id": "research_assistant",
"name": "Research Agent",
"description": "Academic research helper for literature review, data analysis, and paper writing",
"category": "research",
"icon": "📚",
"version": "2.0.1",
"status": "published",
"access_groups": ["researchers", "academics"],
"deployment_count": 23,
"active_instances": 12,
"popularity_score": 88,
"last_updated": "2024-01-12T16:45:00Z",
"created_by": "research@gt2.com",
"created_at": "2024-01-05T09:15:00Z",
"capabilities": ["literature_search", "data_analysis", "citation_help", "writing_assistance"],
"prompt_preview": "You are an academic research agent...",
"tags": ["research", "academic", "writing"],
"compatibility": ["gpt-4", "claude-3", "llama-2"]
},
{
"id": "3",
"template_id": "code_reviewer",
"name": "Code Reviewer",
"description": "AI agent for code review, best practices, and security vulnerability detection",
"category": "development",
"icon": "💻",
"version": "1.5.0",
"status": "testing",
"access_groups": ["developers", "devops"],
"deployment_count": 7,
"active_instances": 4,
"popularity_score": 85,
"last_updated": "2024-01-18T11:20:00Z",
"created_by": "dev@gt2.com",
"created_at": "2024-01-15T13:30:00Z",
"capabilities": ["code_review", "security_scan", "best_practices", "refactoring"],
"prompt_preview": "You are a senior code reviewer...",
"tags": ["development", "code", "security"],
"compatibility": ["gpt-4", "codex"]
}
]
# Apply filters
filtered_templates = mock_templates
if category:
filtered_templates = [t for t in filtered_templates if t["category"] == category]
if status:
filtered_templates = [t for t in filtered_templates if t["status"] == status]
# Apply pagination
start = (page - 1) * limit
end = start + limit
paginated_templates = filtered_templates[start:end]
return {
"data": {
"templates": paginated_templates,
"total": len(filtered_templates),
"page": page,
"limit": limit
}
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to list agent templates: {str(e)}"
)
@router.get("/access-groups/", response_model=dict)
async def list_access_groups(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
List access groups for agent templates.
"""
try:
# Mock data for now
mock_access_groups = [
{
"id": "1",
"name": "security_team",
"description": "Cybersecurity team with access to security-focused agents",
"tenant_count": 8,
"permissions": ["deploy_security", "manage_policies", "view_logs"]
},
{
"id": "2",
"name": "researchers",
"description": "Academic researchers and data analysts",
"tenant_count": 12,
"permissions": ["deploy_research", "access_data", "export_results"]
},
{
"id": "3",
"name": "developers",
"description": "Software development teams",
"tenant_count": 15,
"permissions": ["deploy_code", "review_access", "ci_cd_integration"]
},
{
"id": "4",
"name": "admin",
"description": "System administrators with full access",
"tenant_count": 3,
"permissions": ["full_access", "manage_templates", "system_config"]
}
]
return {
"data": {
"access_groups": mock_access_groups
}
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to list access groups: {str(e)}"
)
@router.get("/deployments/", response_model=dict)
async def get_deployments(
template_id: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get deployment status for agent templates.
"""
try:
# Mock data for now
mock_deployments = [
{
"id": "1",
"template_id": "cybersec_analyst",
"tenant_name": "Acme Corp",
"tenant_id": "acme-corp",
"status": "completed",
"deployed_at": "2024-01-16T09:30:00Z",
"customizations": {"theme": "dark", "language": "en"}
},
{
"id": "2",
"template_id": "research_assistant",
"tenant_name": "University Lab",
"tenant_id": "uni-lab",
"status": "processing",
"customizations": {"domain": "biology", "access_level": "restricted"}
},
{
"id": "3",
"template_id": "code_reviewer",
"tenant_name": "DevTeam Inc",
"tenant_id": "devteam-inc",
"status": "failed",
"error_message": "Insufficient resources available",
"customizations": {"languages": ["python", "javascript"]}
}
]
# Filter by template_id if provided
if template_id:
mock_deployments = [d for d in mock_deployments if d["template_id"] == template_id]
return {
"data": {
"deployments": mock_deployments
}
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get deployments: {str(e)}"
)

View File

@@ -0,0 +1,531 @@
"""
GT 2.0 Control Panel - Resources API with CB-REST Standards
"""
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, Query, BackgroundTasks, Request
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field
import logging
import uuid
from datetime import datetime
from app.core.database import get_db
from app.core.api_standards import (
format_response,
format_error,
ErrorCode,
APIError,
require_capability
)
from app.services.resource_service import ResourceService
from app.services.groq_service import groq_service
from app.models.ai_resource import AIResource
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/resources", tags=["AI Resources"])
# Request/Response Models
class ResourceCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
resource_type: str
provider: str
model_name: Optional[str] = None
personalization_mode: str = "shared"
primary_endpoint: Optional[str] = None
api_endpoints: List[str] = []
failover_endpoints: List[str] = []
health_check_url: Optional[str] = None
max_requests_per_minute: int = 60
max_tokens_per_request: int = 4000
cost_per_1k_tokens: float = 0.0
configuration: Dict[str, Any] = {}
class ResourceUpdateRequest(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
personalization_mode: Optional[str] = None
primary_endpoint: Optional[str] = None
api_endpoints: Optional[List[str]] = None
failover_endpoints: Optional[List[str]] = None
health_check_url: Optional[str] = None
max_requests_per_minute: Optional[int] = None
max_tokens_per_request: Optional[int] = None
cost_per_1k_tokens: Optional[float] = None
configuration: Optional[Dict[str, Any]] = None
is_active: Optional[bool] = None
class BulkAssignRequest(BaseModel):
resource_ids: List[int]
tenant_ids: List[int]
usage_limits: Optional[Dict[str, Any]] = None
custom_config: Optional[Dict[str, Any]] = None
@router.get("")
async def list_resources(
request: Request,
db: AsyncSession = Depends(get_db),
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
provider: Optional[str] = Query(None, description="Filter by provider"),
is_active: Optional[bool] = Query(None, description="Filter by active status"),
search: Optional[str] = Query(None, description="Search in name and description"),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0)
):
"""
List all AI resources with filtering and pagination
CB-REST Capability Required: resource:*:read
"""
try:
service = ResourceService(db)
# Build filters
filters = {}
if resource_type:
filters['resource_type'] = resource_type
if provider:
filters['provider'] = provider
if is_active is not None:
filters['is_active'] = is_active
if search:
filters['search'] = search
resources = await service.list_resources(
filters=filters,
limit=limit,
offset=offset
)
# Get categories for easier filtering
categories = await service.get_resource_categories()
return format_response(
data={
"resources": [r.dict() for r in resources],
"categories": categories,
"total": len(resources),
"limit": limit,
"offset": offset
},
capability_used="resource:*:read",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to list resources: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used="resource:*:read",
request_id=getattr(request.state, 'request_id', None)
)
@router.post("")
async def create_resource(
request: Request,
resource: ResourceCreateRequest,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""
Create a new AI resource
CB-REST Capability Required: resource:*:create
"""
try:
service = ResourceService(db)
# Create resource
new_resource = await service.create_resource(
name=resource.name,
description=resource.description,
resource_type=resource.resource_type,
provider=resource.provider,
model_name=resource.model_name,
personalization_mode=resource.personalization_mode,
primary_endpoint=resource.primary_endpoint,
api_endpoints=resource.api_endpoints,
failover_endpoints=resource.failover_endpoints,
health_check_url=resource.health_check_url,
max_requests_per_minute=resource.max_requests_per_minute,
max_tokens_per_request=resource.max_tokens_per_request,
cost_per_1k_tokens=resource.cost_per_1k_tokens,
configuration=resource.configuration,
created_by=getattr(request.state, 'user_email', 'system')
)
# Schedule health check
if resource.health_check_url:
background_tasks.add_task(
service.perform_health_check,
new_resource.id
)
return format_response(
data={
"resource_id": new_resource.id,
"uuid": new_resource.uuid,
"health_check_scheduled": bool(resource.health_check_url)
},
capability_used="resource:*:create",
request_id=getattr(request.state, 'request_id', None)
)
except ValueError as e:
logger.error(f"Invalid request for resource creation: {e}", exc_info=True)
return format_error(
code=ErrorCode.INVALID_REQUEST,
message="Invalid request parameters",
capability_used="resource:*:create",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to create resource: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used="resource:*:create",
request_id=getattr(request.state, 'request_id', None)
)
@router.get("/{resource_id}")
async def get_resource(
request: Request,
resource_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Get a specific AI resource with full configuration and metrics
CB-REST Capability Required: resource:{resource_id}:read
"""
try:
service = ResourceService(db)
resource = await service.get_resource(resource_id)
if not resource:
return format_error(
code=ErrorCode.RESOURCE_NOT_FOUND,
message=f"Resource {resource_id} not found",
capability_used=f"resource:{resource_id}:read",
request_id=getattr(request.state, 'request_id', None)
)
# Get additional metrics
metrics = await service.get_resource_metrics(resource_id)
return format_response(
data={
**resource.dict(),
"metrics": metrics
},
capability_used=f"resource:{resource_id}:read",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to get resource {resource_id}: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used=f"resource:{resource_id}:read",
request_id=getattr(request.state, 'request_id', None)
)
@router.put("/{resource_id}")
async def update_resource(
request: Request,
resource_id: int,
update: ResourceUpdateRequest,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""
Update an AI resource configuration
CB-REST Capability Required: resource:{resource_id}:update
"""
try:
service = ResourceService(db)
# Update resource
updated_resource = await service.update_resource(
resource_id=resource_id,
**update.dict(exclude_unset=True)
)
if not updated_resource:
return format_error(
code=ErrorCode.RESOURCE_NOT_FOUND,
message=f"Resource {resource_id} not found",
capability_used=f"resource:{resource_id}:update",
request_id=getattr(request.state, 'request_id', None)
)
# Schedule health check if endpoint changed
if update.primary_endpoint or update.health_check_url:
background_tasks.add_task(
service.perform_health_check,
resource_id
)
return format_response(
data={
"resource_id": resource_id,
"updated_fields": list(update.dict(exclude_unset=True).keys()),
"health_check_required": bool(update.primary_endpoint or update.health_check_url)
},
capability_used=f"resource:{resource_id}:update",
request_id=getattr(request.state, 'request_id', None)
)
except ValueError as e:
logger.error(f"Invalid request for resource update: {e}", exc_info=True)
return format_error(
code=ErrorCode.INVALID_REQUEST,
message="Invalid request parameters",
capability_used=f"resource:{resource_id}:update",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to update resource {resource_id}: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used=f"resource:{resource_id}:update",
request_id=getattr(request.state, 'request_id', None)
)
@router.delete("/{resource_id}")
async def delete_resource(
request: Request,
resource_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Archive an AI resource (soft delete)
CB-REST Capability Required: resource:{resource_id}:delete
"""
try:
service = ResourceService(db)
# Get affected tenants before deletion
affected_tenants = await service.get_resource_tenants(resource_id)
# Archive resource
success = await service.archive_resource(resource_id)
if not success:
return format_error(
code=ErrorCode.RESOURCE_NOT_FOUND,
message=f"Resource {resource_id} not found",
capability_used=f"resource:{resource_id}:delete",
request_id=getattr(request.state, 'request_id', None)
)
return format_response(
data={
"archived": True,
"affected_tenants": len(affected_tenants)
},
capability_used=f"resource:{resource_id}:delete",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to delete resource {resource_id}: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used=f"resource:{resource_id}:delete",
request_id=getattr(request.state, 'request_id', None)
)
@router.post("/{resource_id}/health-check")
async def check_resource_health(
request: Request,
resource_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Perform health check on a resource
CB-REST Capability Required: resource:{resource_id}:health
"""
try:
service = ResourceService(db)
# Perform health check
health_result = await service.perform_health_check(resource_id)
if not health_result:
return format_error(
code=ErrorCode.RESOURCE_NOT_FOUND,
message=f"Resource {resource_id} not found",
capability_used=f"resource:{resource_id}:health",
request_id=getattr(request.state, 'request_id', None)
)
return format_response(
data=health_result,
capability_used=f"resource:{resource_id}:health",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to check health for resource {resource_id}: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used=f"resource:{resource_id}:health",
request_id=getattr(request.state, 'request_id', None)
)
@router.get("/types")
async def get_resource_types(request: Request):
"""
Get all available resource types and their access groups
CB-REST Capability Required: resource:*:read
"""
try:
resource_types = {
"ai_ml": {
"name": "AI/ML Models",
"subtypes": ["llm", "embedding", "image_generation", "function_calling", "custom_model"],
"access_groups": ["ai_advanced", "ai_basic"]
},
"rag_engine": {
"name": "RAG Engines",
"subtypes": ["document_processor", "vector_database", "retrieval_strategy"],
"access_groups": ["knowledge_management", "document_processing"]
},
"agentic_workflow": {
"name": "Agentic Workflows",
"subtypes": ["single_agent", "multi_agent", "workflow_chain", "collaborative_agent"],
"access_groups": ["advanced_workflows", "automation"]
},
"app_integration": {
"name": "App Integrations",
"subtypes": ["communication_app", "development_app", "project_management_app", "database_connector"],
"access_groups": ["integration_tools", "development_tools"]
},
"external_service": {
"name": "External Web Services",
"subtypes": ["educational_service", "cybersecurity_service", "development_service", "remote_access_service"],
"access_groups": ["external_platforms", "remote_labs"]
},
"ai_literacy": {
"name": "AI Literacy & Cognitive Skills",
"subtypes": ["strategic_game", "logic_puzzle", "philosophical_dilemma", "educational_content"],
"access_groups": ["ai_literacy", "educational_tools"]
}
}
return format_response(
data={
"resource_types": resource_types,
"access_groups": list(set(
group
for rt in resource_types.values()
for group in rt["access_groups"]
))
},
capability_used="resource:*:read",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to get resource types: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used="resource:*:read",
request_id=getattr(request.state, 'request_id', None)
)
@router.post("/bulk/assign")
async def bulk_assign_resources(
request: Request,
assignment: BulkAssignRequest,
db: AsyncSession = Depends(get_db)
):
"""
Bulk assign resources to tenants
CB-REST Capability Required: resource:*:assign
"""
try:
service = ResourceService(db)
results = await service.bulk_assign_resources(
resource_ids=assignment.resource_ids,
tenant_ids=assignment.tenant_ids,
usage_limits=assignment.usage_limits,
custom_config=assignment.custom_config,
assigned_by=getattr(request.state, 'user_email', 'system')
)
return format_response(
data={
"operation_id": str(uuid.uuid4()),
"assigned": results["assigned"],
"failed": results["failed"]
},
capability_used="resource:*:assign",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to bulk assign resources: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used="resource:*:assign",
request_id=getattr(request.state, 'request_id', None)
)
@router.post("/bulk/health-check")
async def bulk_health_check(
request: Request,
resource_ids: List[int],
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""
Schedule health checks for multiple resources
CB-REST Capability Required: resource:*:health
"""
try:
service = ResourceService(db)
# Schedule health checks
for resource_id in resource_ids:
background_tasks.add_task(
service.perform_health_check,
resource_id
)
return format_response(
data={
"operation_id": str(uuid.uuid4()),
"scheduled_checks": len(resource_ids)
},
capability_used="resource:*:health",
request_id=getattr(request.state, 'request_id', None)
)
except Exception as e:
logger.error(f"Failed to schedule bulk health checks: {e}")
return format_error(
code=ErrorCode.SYSTEM_ERROR,
message="Internal server error",
capability_used="resource:*:health",
request_id=getattr(request.state, 'request_id', None)
)

View File

@@ -0,0 +1,580 @@
"""
System Management API Endpoints
"""
import asyncio
import subprocess
import json
import shutil
import os
from datetime import datetime
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, text
from pydantic import BaseModel, Field
import structlog
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.user import User
from app.models.system import SystemVersion
from app.services.update_service import UpdateService
from app.services.backup_service import BackupService
logger = structlog.get_logger()
router = APIRouter(prefix="/api/v1/system", tags=["System Management"])
# Request/Response Models
class VersionResponse(BaseModel):
"""Response model for version information"""
version: str
installed_at: str
installed_by: Optional[str]
is_current: bool
git_commit: Optional[str]
class SystemInfoResponse(BaseModel):
"""Response model for system information"""
current_version: str
version: str = "" # Alias for frontend compatibility - will be set from current_version
installation_date: str
container_count: Optional[int] = None
database_status: str = "healthy"
class CheckUpdateResponse(BaseModel):
"""Response model for update check"""
update_available: bool
available: bool = False # Alias for frontend compatibility
current_version: str
latest_version: Optional[str]
update_type: Optional[str] = None # "major", "minor", or "patch"
release_notes: Optional[str]
published_at: Optional[str]
released_at: Optional[str] = None # Alias for frontend compatibility
download_url: Optional[str]
checked_at: str # Timestamp when the check was performed
class ValidationCheckResult(BaseModel):
"""Individual validation check result"""
name: str
passed: bool
message: str
details: Dict[str, Any] = {}
class ValidateUpdateResponse(BaseModel):
"""Response model for update validation"""
valid: bool
checks: List[ValidationCheckResult]
warnings: List[str] = []
errors: List[str] = []
class ValidateUpdateRequest(BaseModel):
"""Request model for validating an update"""
target_version: str = Field(..., description="Target version to validate")
class StartUpdateRequest(BaseModel):
"""Request model for starting an update"""
target_version: str = Field(..., description="Version to update to")
create_backup: bool = Field(default=True, description="Create backup before update")
class StartUpdateResponse(BaseModel):
"""Response model for starting an update"""
update_id: str
target_version: str
message: str = "Update initiated"
class UpdateStatusResponse(BaseModel):
"""Response model for update status"""
update_id: str
target_version: str
status: str
started_at: str
completed_at: Optional[str]
current_stage: Optional[str]
logs: List[Dict[str, Any]] = []
error_message: Optional[str]
backup_id: Optional[int]
class RollbackRequest(BaseModel):
"""Request model for rollback"""
reason: Optional[str] = Field(None, description="Reason for rollback")
class BackupResponse(BaseModel):
"""Response model for backup information"""
id: int
uuid: str
backup_type: str
created_at: str
size_mb: Optional[float] # Keep for backward compatibility
size: Optional[int] = None # Size in bytes for frontend
version: Optional[str]
description: Optional[str]
is_valid: bool
download_url: Optional[str] = None # Download URL if available
class CreateBackupRequest(BaseModel):
"""Request model for creating a backup"""
backup_type: str = Field(default="manual", description="Type of backup")
description: Optional[str] = Field(None, description="Backup description")
class RestoreBackupRequest(BaseModel):
"""Request model for restoring a backup"""
backup_id: str = Field(..., description="UUID of backup to restore")
components: Optional[List[str]] = Field(None, description="Components to restore")
class ContainerStatus(BaseModel):
"""Container status from Docker"""
name: str
cluster: str # "admin", "tenant", "resource"
state: str # "running", "exited", "paused"
health: str # "healthy", "unhealthy", "starting", "none"
uptime: str
ports: List[str] = []
class DatabaseStats(BaseModel):
"""PostgreSQL database statistics"""
connections_active: int
connections_max: int
cache_hit_ratio: float
database_size: str
transactions_committed: int
class ClusterSummary(BaseModel):
"""Cluster health summary"""
name: str
healthy: int
unhealthy: int
total: int
class SystemHealthDetailedResponse(BaseModel):
"""Detailed system health response"""
overall_status: str
containers: List[ContainerStatus]
clusters: List[ClusterSummary]
database: DatabaseStats
version: str
# Helper Functions
async def _get_container_status() -> List[ContainerStatus]:
"""Get container status from Docker Compose"""
try:
# Run docker compose ps with JSON format
process = await asyncio.create_subprocess_exec(
"docker", "compose", "ps", "--format", "json",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd="/Users/hackweasel/Documents/GT-2.0"
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
logger.error("docker_compose_ps_failed", stderr=stderr.decode())
return []
# Parse JSON output (one JSON object per line)
containers = []
for line in stdout.decode().strip().split('\n'):
if not line:
continue
try:
container_data = json.loads(line)
name = container_data.get("Name", "")
state = container_data.get("State", "unknown")
health = container_data.get("Health", "none")
# Map container name to cluster
cluster = "unknown"
if "controlpanel" in name.lower():
cluster = "admin"
elif "tenant" in name.lower() and "controlpanel" not in name.lower():
cluster = "tenant"
elif "resource" in name.lower() or "vllm" in name.lower():
cluster = "resource"
# Extract ports
ports = []
publishers = container_data.get("Publishers", [])
if publishers:
for pub in publishers:
if pub.get("PublishedPort"):
ports.append(f"{pub.get('PublishedPort')}:{pub.get('TargetPort')}")
# Get uptime from status
status_text = container_data.get("Status", "")
uptime = status_text if status_text else "unknown"
containers.append(ContainerStatus(
name=name,
cluster=cluster,
state=state,
health=health if health else "none",
uptime=uptime,
ports=ports
))
except json.JSONDecodeError as e:
logger.warning("failed_to_parse_container_json", line=line, error=str(e))
continue
return containers
except Exception as e:
# Docker is not available inside the container - this is expected behavior
logger.debug("docker_not_available", error=str(e))
return []
async def _get_database_stats(db: AsyncSession) -> DatabaseStats:
"""Get PostgreSQL database statistics"""
try:
# Get connection and transaction stats
stats_query = text("""
SELECT
numbackends as active_connections,
xact_commit as transactions_committed,
ROUND(100.0 * blks_hit / NULLIF(blks_read + blks_hit, 0), 1) as cache_hit_ratio
FROM pg_stat_database
WHERE datname = current_database()
""")
stats_result = await db.execute(stats_query)
stats = stats_result.fetchone()
# Get database size
size_query = text("SELECT pg_size_pretty(pg_database_size(current_database()))")
size_result = await db.execute(size_query)
size = size_result.scalar()
# Get max connections
max_conn_query = text("SELECT current_setting('max_connections')::int")
max_conn_result = await db.execute(max_conn_query)
max_connections = max_conn_result.scalar()
return DatabaseStats(
connections_active=stats[0] if stats else 0,
connections_max=max_connections if max_connections else 100,
cache_hit_ratio=float(stats[2]) if stats and stats[2] else 0.0,
database_size=size if size else "0 bytes",
transactions_committed=stats[1] if stats else 0
)
except Exception as e:
logger.error("failed_to_get_database_stats", error=str(e))
# Return default stats on error
return DatabaseStats(
connections_active=0,
connections_max=100,
cache_hit_ratio=0.0,
database_size="unknown",
transactions_committed=0
)
def _aggregate_clusters(containers: List[ContainerStatus]) -> List[ClusterSummary]:
"""Aggregate container health by cluster"""
cluster_data = {}
for container in containers:
cluster_name = container.cluster
if cluster_name not in cluster_data:
cluster_data[cluster_name] = {"healthy": 0, "unhealthy": 0, "total": 0}
cluster_data[cluster_name]["total"] += 1
# Consider container healthy if running and health is healthy/none
if container.state == "running" and container.health in ["healthy", "none"]:
cluster_data[cluster_name]["healthy"] += 1
else:
cluster_data[cluster_name]["unhealthy"] += 1
# Convert to ClusterSummary objects
summaries = []
for cluster_name, data in cluster_data.items():
summaries.append(ClusterSummary(
name=cluster_name,
healthy=data["healthy"],
unhealthy=data["unhealthy"],
total=data["total"]
))
return summaries
# Dependency for admin-only access
async def require_admin(current_user: User = Depends(get_current_user)):
"""Ensure user is a super admin"""
if current_user.user_type != "super_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Administrator access required"
)
return current_user
# Version Endpoints
@router.get("/version", response_model=SystemInfoResponse)
async def get_system_version(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Get current system version and information"""
# Get current version
stmt = select(SystemVersion).where(
SystemVersion.is_current == True
).order_by(desc(SystemVersion.installed_at)).limit(1)
result = await db.execute(stmt)
current = result.scalar_one_or_none()
if not current:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="System version not found. Please run database migrations: alembic upgrade head"
)
return SystemInfoResponse(
current_version=current.version,
version=current.version, # Set version same as current_version for frontend compatibility
installation_date=current.installed_at.isoformat(),
database_status="healthy"
)
@router.get("/health-detailed", response_model=SystemHealthDetailedResponse)
async def get_detailed_health(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Get comprehensive system health with real container and database metrics"""
# Get current version
stmt = select(SystemVersion).where(
SystemVersion.is_current == True
).order_by(desc(SystemVersion.installed_at)).limit(1)
result = await db.execute(stmt)
current_version = result.scalar_one_or_none()
version_str = current_version.version if current_version else "unknown"
# Gather system metrics concurrently
containers = await _get_container_status()
database_stats = await _get_database_stats(db)
cluster_summaries = _aggregate_clusters(containers)
# Determine overall status
unhealthy_count = sum(cluster.unhealthy for cluster in cluster_summaries)
overall_status = "healthy" if unhealthy_count == 0 else "degraded"
return SystemHealthDetailedResponse(
overall_status=overall_status,
containers=containers,
clusters=cluster_summaries,
database=database_stats,
version=version_str
)
# Update Endpoints
@router.get("/check-update", response_model=CheckUpdateResponse)
async def check_for_updates(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Check for available system updates"""
service = UpdateService(db)
return await service.check_for_updates()
@router.post("/validate-update", response_model=ValidateUpdateResponse)
async def validate_update(
request: ValidateUpdateRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Run pre-update validation checks"""
service = UpdateService(db)
return await service.validate_update(request.target_version)
@router.post("/update", response_model=StartUpdateResponse)
async def start_update(
request: StartUpdateRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Start system update process"""
service = UpdateService(db)
update_id = await service.execute_update(
target_version=request.target_version,
create_backup=request.create_backup,
started_by=current_user.email
)
return StartUpdateResponse(
update_id=update_id,
target_version=request.target_version
)
@router.get("/update/{update_id}/status", response_model=UpdateStatusResponse)
async def get_update_status(
update_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Get status of an update job"""
service = UpdateService(db)
status_data = await service.get_update_status(update_id)
return UpdateStatusResponse(
update_id=status_data["uuid"],
target_version=status_data["target_version"],
status=status_data["status"],
started_at=status_data["started_at"],
completed_at=status_data.get("completed_at"),
current_stage=status_data.get("current_stage"),
logs=status_data.get("logs", []),
error_message=status_data.get("error_message"),
backup_id=status_data.get("backup_id")
)
@router.post("/update/{update_id}/rollback")
async def rollback_update(
update_id: str,
request: RollbackRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Rollback a failed update"""
service = UpdateService(db)
return await service.rollback(update_id, request.reason)
# Backup Endpoints
@router.get("/backups", response_model=Dict[str, Any])
async def list_backups(
limit: int = Query(default=50, ge=1, le=100),
offset: int = Query(default=0, ge=0),
backup_type: Optional[str] = Query(default=None, description="Filter by backup type"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""List available backups with storage information"""
service = BackupService(db)
backup_data = await service.list_backups(limit=limit, offset=offset, backup_type=backup_type)
# Add storage information
backup_dir = service.BACKUP_DIR
try:
# Create backup directory if it doesn't exist
os.makedirs(backup_dir, exist_ok=True)
disk_usage = shutil.disk_usage(backup_dir)
storage = {
"used": backup_data.get("storage_used", 0), # From service
"total": disk_usage.total,
"available": disk_usage.free
}
except Exception as e:
logger.debug("backup_dir_unavailable", error=str(e))
storage = {"used": 0, "total": 0, "available": 0}
backup_data["storage"] = storage
return backup_data
@router.post("/backups", response_model=BackupResponse)
async def create_backup(
request: CreateBackupRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Create a new system backup"""
service = BackupService(db)
backup_data = await service.create_backup(
backup_type=request.backup_type,
description=request.description,
created_by=current_user.email
)
return BackupResponse(
id=backup_data["id"],
uuid=backup_data["uuid"],
backup_type=backup_data["backup_type"],
created_at=backup_data["created_at"],
size_mb=backup_data.get("size_mb"),
size=backup_data.get("size"),
version=backup_data.get("version"),
description=backup_data.get("description"),
is_valid=backup_data["is_valid"],
download_url=backup_data.get("download_url")
)
@router.get("/backups/{backup_id}", response_model=BackupResponse)
async def get_backup(
backup_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Get details of a specific backup"""
service = BackupService(db)
backup_data = await service.get_backup(backup_id)
return BackupResponse(
id=backup_data["id"],
uuid=backup_data["uuid"],
backup_type=backup_data["backup_type"],
created_at=backup_data["created_at"],
size_mb=backup_data.get("size_mb"),
size=backup_data.get("size"),
version=backup_data.get("version"),
description=backup_data.get("description"),
is_valid=backup_data["is_valid"],
download_url=backup_data.get("download_url")
)
@router.delete("/backups/{backup_id}")
async def delete_backup(
backup_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Delete a backup"""
service = BackupService(db)
return await service.delete_backup(backup_id)
@router.post("/restore")
async def restore_backup(
request: RestoreBackupRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""Restore system from a backup"""
service = BackupService(db)
return await service.restore_backup(
backup_id=request.backup_id,
components=request.components
)

View File

@@ -0,0 +1,133 @@
"""
GT 2.0 Tenant Templates API
Manage and apply tenant configuration templates
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete
from typing import List
from pydantic import BaseModel
from app.core.database import get_db
from app.models.tenant_template import TenantTemplate
from app.services.template_service import TemplateService
router = APIRouter(prefix="/api/v1/templates", tags=["templates"])
class CreateTemplateRequest(BaseModel):
tenant_id: int
name: str
description: str = ""
class ApplyTemplateRequest(BaseModel):
template_id: int
tenant_id: int
class TemplateResponse(BaseModel):
id: int
name: str
description: str
is_default: bool
resource_counts: dict
created_at: str
@router.get("/", response_model=List[TemplateResponse])
async def list_templates(
db: AsyncSession = Depends(get_db)
):
"""List all tenant templates"""
result = await db.execute(select(TenantTemplate).order_by(TenantTemplate.name))
templates = result.scalars().all()
return [TemplateResponse(**template.get_summary()) for template in templates]
@router.get("/{template_id}")
async def get_template(
template_id: int,
db: AsyncSession = Depends(get_db)
):
"""Get template details including full configuration"""
template = await db.get(TenantTemplate, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
return template.to_dict()
@router.post("/export")
async def export_template(
request: CreateTemplateRequest,
db: AsyncSession = Depends(get_db)
):
"""Export existing tenant configuration as a new template"""
try:
service = TemplateService()
template = await service.export_tenant_as_template(
tenant_id=request.tenant_id,
template_name=request.name,
template_description=request.description,
control_panel_db=db
)
return {
"success": True,
"message": f"Template '{request.name}' created successfully",
"template": template.get_summary()
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to export template: {str(e)}")
@router.post("/apply")
async def apply_template(
request: ApplyTemplateRequest,
db: AsyncSession = Depends(get_db)
):
"""Apply a template to an existing tenant"""
try:
service = TemplateService()
results = await service.apply_template(
template_id=request.template_id,
tenant_id=request.tenant_id,
control_panel_db=db
)
return {
"success": True,
"message": "Template applied successfully",
"results": results
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to apply template: {str(e)}")
@router.delete("/{template_id}")
async def delete_template(
template_id: int,
db: AsyncSession = Depends(get_db)
):
"""Delete a template"""
template = await db.get(TenantTemplate, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
await db.delete(template)
await db.commit()
return {
"success": True,
"message": f"Template '{template.name}' deleted successfully"
}

View File

@@ -0,0 +1,362 @@
"""
Tenant Model Management API for GT 2.0 Admin Control Panel
Provides endpoints for managing which models are available to which tenants,
with tenant-specific permissions and rate limits.
"""
from typing import Dict, Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field
import logging
from app.core.database import get_db
from app.services.model_management_service import get_model_management_service
from app.models.tenant_model_config import TenantModelConfig
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tenants", tags=["Tenant Model Management"])
# Request/Response Models
class TenantModelAssignRequest(BaseModel):
model_id: str = Field(..., description="Model ID to assign")
rate_limits: Optional[Dict[str, Any]] = Field(None, description="Custom rate limits")
capabilities: Optional[Dict[str, Any]] = Field(None, description="Tenant-specific capabilities")
usage_constraints: Optional[Dict[str, Any]] = Field(None, description="Usage restrictions")
priority: int = Field(1, ge=1, le=10, description="Priority level (1-10)")
model_config = {"protected_namespaces": ()}
class TenantModelUpdateRequest(BaseModel):
is_enabled: Optional[bool] = Field(None, description="Enable/disable model for tenant")
rate_limits: Optional[Dict[str, Any]] = Field(None, description="Updated rate limits")
tenant_capabilities: Optional[Dict[str, Any]] = Field(None, description="Updated capabilities")
usage_constraints: Optional[Dict[str, Any]] = Field(None, description="Updated usage restrictions")
priority: Optional[int] = Field(None, ge=1, le=10, description="Updated priority level")
class ModelAccessCheckRequest(BaseModel):
user_capabilities: Optional[List[str]] = Field(None, description="User capabilities")
user_id: Optional[str] = Field(None, description="User identifier")
class TenantModelResponse(BaseModel):
id: int
tenant_id: int
model_id: str
is_enabled: bool
tenant_capabilities: Dict[str, Any]
rate_limits: Dict[str, Any]
usage_constraints: Dict[str, Any]
priority: int
created_at: str
updated_at: str
class ModelWithTenantConfigResponse(BaseModel):
model_id: str
name: str
provider: str
model_type: str
endpoint: str
tenant_config: TenantModelResponse
@router.post("/{tenant_id}/models", response_model=TenantModelResponse)
async def assign_model_to_tenant(
tenant_id: int,
request: TenantModelAssignRequest,
db: AsyncSession = Depends(get_db)
):
"""Assign a model to a tenant with specific configuration"""
try:
service = get_model_management_service(db)
tenant_model_config = await service.assign_model_to_tenant(
tenant_id=tenant_id,
model_id=request.model_id,
rate_limits=request.rate_limits,
capabilities=request.capabilities,
usage_constraints=request.usage_constraints,
priority=request.priority
)
return TenantModelResponse(**tenant_model_config.to_dict())
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error assigning model to tenant: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{tenant_id}/models/{model_id:path}")
async def remove_model_from_tenant(
tenant_id: int,
model_id: str,
db: AsyncSession = Depends(get_db)
):
"""Remove model access from a tenant"""
try:
service = get_model_management_service(db)
success = await service.remove_model_from_tenant(tenant_id, model_id)
if not success:
raise HTTPException(status_code=404, detail="Model assignment not found")
return {"message": f"Model {model_id} removed from tenant {tenant_id}"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error removing model from tenant: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/{tenant_id}/models/{model_id:path}", response_model=TenantModelResponse)
async def update_tenant_model_config(
tenant_id: int,
model_id: str,
request: TenantModelUpdateRequest,
db: AsyncSession = Depends(get_db)
):
"""Update tenant-specific model configuration"""
try:
service = get_model_management_service(db)
# Convert request to dict, excluding None values
updates = {k: v for k, v in request.dict().items() if v is not None}
tenant_model_config = await service.update_tenant_model_config(
tenant_id=tenant_id,
model_id=model_id,
updates=updates
)
if not tenant_model_config:
raise HTTPException(status_code=404, detail="Tenant model configuration not found")
return TenantModelResponse(**tenant_model_config.to_dict())
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating tenant model config: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{tenant_id}/models", response_model=List[ModelWithTenantConfigResponse])
async def get_tenant_models(
tenant_id: int,
enabled_only: bool = Query(False, description="Only return enabled models"),
db: AsyncSession = Depends(get_db)
):
"""Get all models available to a tenant"""
try:
service = get_model_management_service(db)
models = await service.get_tenant_models(
tenant_id=tenant_id,
enabled_only=enabled_only
)
# Format response
response_models = []
for model in models:
tenant_config = model.pop("tenant_config")
response_models.append({
**model,
"tenant_config": TenantModelResponse(**tenant_config)
})
return response_models
except Exception as e:
logger.error(f"Error getting tenant models: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{tenant_id}/models/{model_id}/check-access")
async def check_tenant_model_access(
tenant_id: int,
model_id: str,
request: ModelAccessCheckRequest,
db: AsyncSession = Depends(get_db)
):
"""Check if a tenant/user can access a specific model"""
try:
service = get_model_management_service(db)
access_info = await service.check_tenant_model_access(
tenant_id=tenant_id,
model_id=model_id,
user_capabilities=request.user_capabilities,
user_id=request.user_id
)
return access_info
except Exception as e:
logger.error(f"Error checking tenant model access: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{tenant_id}/models/stats")
async def get_tenant_model_stats(
tenant_id: int,
db: AsyncSession = Depends(get_db)
):
"""Get statistics about models for a tenant"""
try:
service = get_model_management_service(db)
stats = await service.get_tenant_model_stats(tenant_id)
return stats
except Exception as e:
logger.error(f"Error getting tenant model stats: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Additional endpoints for model-centric views
@router.get("/models/{model_id:path}/tenants")
async def get_model_tenants(
model_id: str,
db: AsyncSession = Depends(get_db)
):
"""Get all tenants that have access to a model"""
try:
service = get_model_management_service(db)
tenants = await service.get_model_tenants(model_id)
return {
"model_id": model_id,
"tenants": tenants,
"total_tenants": len(tenants)
}
except Exception as e:
logger.error(f"Error getting model tenants: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Global tenant model configuration endpoints
@router.get("/all")
async def get_all_tenant_model_configs(
db: AsyncSession = Depends(get_db)
):
"""Get all tenant model configurations with joined tenant and model data"""
try:
service = get_model_management_service(db)
# This would need to be implemented in the service
configs = await service.get_all_tenant_model_configs()
return configs
except Exception as e:
logger.error(f"Error getting all tenant model configs: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Bulk operations
@router.post("/{tenant_id}/models/bulk-assign")
async def bulk_assign_models_to_tenant(
tenant_id: int,
model_ids: List[str],
default_config: Optional[TenantModelAssignRequest] = None,
db: AsyncSession = Depends(get_db)
):
"""Assign multiple models to a tenant with the same configuration"""
try:
service = get_model_management_service(db)
results = []
errors = []
for model_id in model_ids:
try:
config = default_config if default_config else TenantModelAssignRequest(model_id=model_id)
tenant_model_config = await service.assign_model_to_tenant(
tenant_id=tenant_id,
model_id=model_id,
rate_limits=config.rate_limits,
capabilities=config.capabilities,
usage_constraints=config.usage_constraints,
priority=config.priority
)
results.append({
"model_id": model_id,
"status": "success",
"config": tenant_model_config.to_dict()
})
except Exception as e:
errors.append({
"model_id": model_id,
"status": "error",
"error": str(e)
})
return {
"tenant_id": tenant_id,
"total_requested": len(model_ids),
"successful": len(results),
"failed": len(errors),
"results": results,
"errors": errors
}
except Exception as e:
logger.error(f"Error bulk assigning models: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{tenant_id}/models/bulk-remove")
async def bulk_remove_models_from_tenant(
tenant_id: int,
model_ids: List[str],
db: AsyncSession = Depends(get_db)
):
"""Remove multiple models from a tenant"""
try:
service = get_model_management_service(db)
results = []
for model_id in model_ids:
try:
success = await service.remove_model_from_tenant(tenant_id, model_id)
results.append({
"model_id": model_id,
"status": "success" if success else "not_found",
"removed": success
})
except Exception as e:
results.append({
"model_id": model_id,
"status": "error",
"error": str(e)
})
successful = sum(1 for r in results if r["status"] == "success")
return {
"tenant_id": tenant_id,
"total_requested": len(model_ids),
"successful": successful,
"results": results
}
except Exception as e:
logger.error(f"Error bulk removing models: {e}")
raise HTTPException(status_code=500, detail=str(e))