Files
hive/backend/app/api/ucxl_integration.py
anthonyrawlins 268214d971 Major WHOOSH system refactoring and feature enhancements
- 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>
2025-08-27 08:34:48 +10:00

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