- Migrated from HIVE branding to WHOOSH across all components - Enhanced backend API with new services: AI models, BZZZ integration, templates, members - Added comprehensive testing suite with security, performance, and integration tests - Improved frontend with new components for project setup, AI models, and team management - Updated MCP server implementation with WHOOSH-specific tools and resources - Enhanced deployment configurations with production-ready Docker setups - Added comprehensive documentation and setup guides - Implemented age encryption service and UCXL integration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
395 lines
14 KiB
Python
395 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
UCXL Integration API for WHOOSH
|
|
API endpoints for distributed artifact storage, retrieval, and temporal navigation
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File
|
|
from typing import Dict, List, Optional, Any, Union
|
|
from pydantic import BaseModel, Field
|
|
from datetime import datetime
|
|
|
|
from ..services.ucxl_integration_service import ucxl_service, UCXLAddress
|
|
from ..core.auth_deps import get_current_user
|
|
from ..models.user import User
|
|
|
|
router = APIRouter(prefix="/api/ucxl", tags=["UCXL Integration"])
|
|
|
|
# Pydantic models for API requests/responses
|
|
|
|
class StoreArtifactRequest(BaseModel):
|
|
project: str = Field(..., description="Project name")
|
|
component: str = Field(..., description="Component name")
|
|
path: str = Field(..., description="Artifact path")
|
|
content: str = Field(..., description="Artifact content")
|
|
content_type: str = Field("text/plain", description="Content MIME type")
|
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata")
|
|
|
|
class StoreArtifactResponse(BaseModel):
|
|
address: str
|
|
success: bool
|
|
message: str
|
|
|
|
class ArtifactInfo(BaseModel):
|
|
address: str
|
|
content_hash: str
|
|
content_type: str
|
|
size: int
|
|
created_at: str
|
|
modified_at: str
|
|
metadata: Dict[str, Any]
|
|
cached: Optional[bool] = None
|
|
|
|
class CreateProjectContextRequest(BaseModel):
|
|
project_name: str = Field(..., description="Project name")
|
|
description: str = Field(..., description="Project description")
|
|
components: List[str] = Field(..., description="List of project components")
|
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional project metadata")
|
|
|
|
class LinkArtifactsRequest(BaseModel):
|
|
source_address: str = Field(..., description="Source UCXL address")
|
|
target_address: str = Field(..., description="Target UCXL address")
|
|
relationship: str = Field(..., description="Relationship type (e.g., 'depends_on', 'implements', 'tests')")
|
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Link metadata")
|
|
|
|
class SystemStatusResponse(BaseModel):
|
|
ucxl_endpoints: int
|
|
dht_nodes: int
|
|
bzzz_gateways: int
|
|
cached_artifacts: int
|
|
cache_limit: int
|
|
system_health: float
|
|
last_update: str
|
|
|
|
@router.get("/status", response_model=SystemStatusResponse)
|
|
async def get_ucxl_status(
|
|
current_user: User = Depends(get_current_user)
|
|
) -> SystemStatusResponse:
|
|
"""Get UCXL integration system status"""
|
|
try:
|
|
status = await ucxl_service.get_system_status()
|
|
return SystemStatusResponse(**status)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get UCXL status: {str(e)}")
|
|
|
|
@router.post("/artifacts", response_model=StoreArtifactResponse)
|
|
async def store_artifact(
|
|
request: StoreArtifactRequest,
|
|
current_user: User = Depends(get_current_user)
|
|
) -> StoreArtifactResponse:
|
|
"""
|
|
Store an artifact in the distributed UCXL system
|
|
"""
|
|
try:
|
|
address = await ucxl_service.store_artifact(
|
|
project=request.project,
|
|
component=request.component,
|
|
path=request.path,
|
|
content=request.content,
|
|
content_type=request.content_type,
|
|
metadata=request.metadata
|
|
)
|
|
|
|
if address:
|
|
return StoreArtifactResponse(
|
|
address=address,
|
|
success=True,
|
|
message="Artifact stored successfully"
|
|
)
|
|
else:
|
|
raise HTTPException(status_code=500, detail="Failed to store artifact")
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to store artifact: {str(e)}")
|
|
|
|
@router.post("/artifacts/upload", response_model=StoreArtifactResponse)
|
|
async def upload_artifact(
|
|
project: str,
|
|
component: str,
|
|
path: str,
|
|
file: UploadFile = File(...),
|
|
metadata: Optional[str] = None,
|
|
current_user: User = Depends(get_current_user)
|
|
) -> StoreArtifactResponse:
|
|
"""
|
|
Upload and store a file artifact in the distributed UCXL system
|
|
"""
|
|
try:
|
|
# Read file content
|
|
content = await file.read()
|
|
|
|
# Parse metadata if provided
|
|
file_metadata = {}
|
|
if metadata:
|
|
import json
|
|
file_metadata = json.loads(metadata)
|
|
|
|
# Add file info to metadata
|
|
file_metadata.update({
|
|
"original_filename": file.filename,
|
|
"uploaded_by": current_user.username,
|
|
"upload_timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
address = await ucxl_service.store_artifact(
|
|
project=project,
|
|
component=component,
|
|
path=path,
|
|
content=content,
|
|
content_type=file.content_type or "application/octet-stream",
|
|
metadata=file_metadata
|
|
)
|
|
|
|
if address:
|
|
return StoreArtifactResponse(
|
|
address=address,
|
|
success=True,
|
|
message=f"File '{file.filename}' uploaded successfully"
|
|
)
|
|
else:
|
|
raise HTTPException(status_code=500, detail="Failed to upload file")
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to upload file: {str(e)}")
|
|
|
|
@router.get("/artifacts/{address:path}", response_model=Optional[ArtifactInfo])
|
|
async def retrieve_artifact(
|
|
address: str,
|
|
current_user: User = Depends(get_current_user)
|
|
) -> Optional[ArtifactInfo]:
|
|
"""
|
|
Retrieve an artifact from the distributed UCXL system
|
|
"""
|
|
try:
|
|
# Decode URL-encoded address
|
|
import urllib.parse
|
|
decoded_address = urllib.parse.unquote(address)
|
|
|
|
data = await ucxl_service.retrieve_artifact(decoded_address)
|
|
|
|
if data:
|
|
return ArtifactInfo(**data)
|
|
else:
|
|
raise HTTPException(status_code=404, detail=f"Artifact not found: {decoded_address}")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to retrieve artifact: {str(e)}")
|
|
|
|
@router.get("/artifacts", response_model=List[ArtifactInfo])
|
|
async def list_artifacts(
|
|
project: Optional[str] = Query(None, description="Filter by project"),
|
|
component: Optional[str] = Query(None, description="Filter by component"),
|
|
limit: int = Query(100, ge=1, le=1000, description="Maximum number of artifacts to return"),
|
|
current_user: User = Depends(get_current_user)
|
|
) -> List[ArtifactInfo]:
|
|
"""
|
|
List artifacts from the distributed UCXL system
|
|
"""
|
|
try:
|
|
artifacts = await ucxl_service.list_artifacts(
|
|
project=project,
|
|
component=component,
|
|
limit=limit
|
|
)
|
|
|
|
return [ArtifactInfo(**artifact) for artifact in artifacts]
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to list artifacts: {str(e)}")
|
|
|
|
@router.get("/artifacts/{address:path}/temporal", response_model=Optional[ArtifactInfo])
|
|
async def resolve_temporal_artifact(
|
|
address: str,
|
|
timestamp: Optional[str] = Query(None, description="ISO timestamp for temporal resolution"),
|
|
current_user: User = Depends(get_current_user)
|
|
) -> Optional[ArtifactInfo]:
|
|
"""
|
|
Resolve a UCXL address at a specific point in time using temporal navigation
|
|
"""
|
|
try:
|
|
# Decode URL-encoded address
|
|
import urllib.parse
|
|
decoded_address = urllib.parse.unquote(address)
|
|
|
|
# Parse timestamp if provided
|
|
target_time = None
|
|
if timestamp:
|
|
target_time = datetime.fromisoformat(timestamp)
|
|
|
|
data = await ucxl_service.resolve_temporal_address(decoded_address, target_time)
|
|
|
|
if data:
|
|
return ArtifactInfo(**data)
|
|
else:
|
|
raise HTTPException(status_code=404, detail=f"Artifact not found at specified time: {decoded_address}")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to resolve temporal artifact: {str(e)}")
|
|
|
|
@router.post("/projects", response_model=Dict[str, str])
|
|
async def create_project_context(
|
|
request: CreateProjectContextRequest,
|
|
current_user: User = Depends(get_current_user)
|
|
) -> Dict[str, str]:
|
|
"""
|
|
Create a project context in the UCXL system
|
|
"""
|
|
try:
|
|
address = await ucxl_service.create_project_context(
|
|
project_name=request.project_name,
|
|
description=request.description,
|
|
components=request.components,
|
|
metadata=request.metadata
|
|
)
|
|
|
|
if address:
|
|
return {
|
|
"address": address,
|
|
"project_name": request.project_name,
|
|
"status": "created"
|
|
}
|
|
else:
|
|
raise HTTPException(status_code=500, detail="Failed to create project context")
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to create project context: {str(e)}")
|
|
|
|
@router.post("/links", response_model=Dict[str, str])
|
|
async def link_artifacts(
|
|
request: LinkArtifactsRequest,
|
|
current_user: User = Depends(get_current_user)
|
|
) -> Dict[str, str]:
|
|
"""
|
|
Create a relationship link between two UCXL artifacts
|
|
"""
|
|
try:
|
|
success = await ucxl_service.link_artifacts(
|
|
source_address=request.source_address,
|
|
target_address=request.target_address,
|
|
relationship=request.relationship,
|
|
metadata=request.metadata
|
|
)
|
|
|
|
if success:
|
|
return {
|
|
"status": "linked",
|
|
"source": request.source_address,
|
|
"target": request.target_address,
|
|
"relationship": request.relationship
|
|
}
|
|
else:
|
|
raise HTTPException(status_code=500, detail="Failed to create artifact link")
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to link artifacts: {str(e)}")
|
|
|
|
@router.get("/artifacts/{address:path}/links", response_model=List[Dict[str, Any]])
|
|
async def get_artifact_links(
|
|
address: str,
|
|
current_user: User = Depends(get_current_user)
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get all links involving a specific artifact
|
|
"""
|
|
try:
|
|
# Decode URL-encoded address
|
|
import urllib.parse
|
|
decoded_address = urllib.parse.unquote(address)
|
|
|
|
links = await ucxl_service.get_artifact_links(decoded_address)
|
|
return links
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get artifact links: {str(e)}")
|
|
|
|
@router.get("/addresses/parse", response_model=Dict[str, Any])
|
|
async def parse_ucxl_address(
|
|
address: str = Query(..., description="UCXL address to parse"),
|
|
current_user: User = Depends(get_current_user)
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Parse a UCXL address into its components
|
|
"""
|
|
try:
|
|
ucxl_addr = UCXLAddress.parse(address)
|
|
|
|
return {
|
|
"original": address,
|
|
"protocol": ucxl_addr.protocol.value,
|
|
"user": ucxl_addr.user,
|
|
"password": "***" if ucxl_addr.password else None, # Hide password
|
|
"project": ucxl_addr.project,
|
|
"component": ucxl_addr.component,
|
|
"path": ucxl_addr.path,
|
|
"reconstructed": ucxl_addr.to_string()
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid UCXL address: {str(e)}")
|
|
|
|
@router.get("/addresses/generate", response_model=Dict[str, str])
|
|
async def generate_ucxl_address(
|
|
project: str = Query(..., description="Project name"),
|
|
component: str = Query(..., description="Component name"),
|
|
path: str = Query(..., description="Artifact path"),
|
|
user: Optional[str] = Query(None, description="User name"),
|
|
secure: bool = Query(False, description="Use secure protocol (ucxls)"),
|
|
current_user: User = Depends(get_current_user)
|
|
) -> Dict[str, str]:
|
|
"""
|
|
Generate a UCXL address from components
|
|
"""
|
|
try:
|
|
from ..services.ucxl_integration_service import UCXLProtocol
|
|
|
|
ucxl_addr = UCXLAddress(
|
|
protocol=UCXLProtocol.UCXL_SECURE if secure else UCXLProtocol.UCXL,
|
|
user=user,
|
|
project=project,
|
|
component=component,
|
|
path=path
|
|
)
|
|
|
|
return {
|
|
"address": ucxl_addr.to_string(),
|
|
"project": project,
|
|
"component": component,
|
|
"path": path
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Failed to generate address: {str(e)}")
|
|
|
|
@router.get("/health")
|
|
async def ucxl_health_check() -> Dict[str, Any]:
|
|
"""UCXL integration health check endpoint"""
|
|
try:
|
|
status = await ucxl_service.get_system_status()
|
|
|
|
health_status = "healthy"
|
|
if status.get("system_health", 0) < 0.5:
|
|
health_status = "degraded"
|
|
if status.get("dht_nodes", 0) == 0:
|
|
health_status = "offline"
|
|
|
|
return {
|
|
"status": health_status,
|
|
"ucxl_endpoints": status.get("ucxl_endpoints", 0),
|
|
"dht_nodes": status.get("dht_nodes", 0),
|
|
"bzzz_gateways": status.get("bzzz_gateways", 0),
|
|
"cached_artifacts": status.get("cached_artifacts", 0),
|
|
"system_health": status.get("system_health", 0),
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"status": "error",
|
|
"error": str(e),
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
# Note: Exception handlers are registered at the app level, not router level |