refactor: rename packages to shorter names
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Integration Tests / test-service-integration (push) Failing after 3s
Python Tests / test-python (push) Failing after 32s
Security Scanning / security-scan (push) Successful in 32s

- Renamed aitbc-ai-service to aitbc-ai
- Renamed aitbc-edge-api to aitbc-edge
- Updated pyproject.toml files with new package names
- Renamed directories and package modules
- Updated references in documentation and scripts
- Updated systemd service references
This commit is contained in:
aitbc
2026-05-20 08:30:30 +02:00
parent 56938fb2c4
commit bd08848e0d
38 changed files with 17 additions and 11 deletions

153
apps/aitbc-edge/README.md Normal file
View File

@@ -0,0 +1,153 @@
# Edge API Service
REST API for AITBC island and edge operations, providing HTTP equivalents for CLI commands.
## Overview
The Edge API Service exposes island and edge operations via REST API, following the coordinator-api pattern. It integrates with the existing GPU service and blockchain node RPC.
## Architecture
- **Port:** 8103
- **Database:** PostgreSQL (aitbc_edge)
- **Communication:**
- Blockchain node RPC: localhost:8006
- GPU service: localhost:8101
- **Authentication:** JWT tokens (same as coordinator-api)
## Installation
```bash
cd /opt/aitbc/apps/edge-api
pip install -e .
```
## Database Setup
Create PostgreSQL database and user:
```bash
sudo -u postgres psql
CREATE DATABASE aitbc_edge;
CREATE USER aitbc_edge WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE aitbc_edge TO aitbc_edge;
\q
```
## Running
### Development
```bash
cd /opt/aitbc/apps/edge-api
python -m edge_api.main
```
### Production (systemd)
```bash
# Install service
sudo cp edge-api.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable edge-api
sudo systemctl start edge-api
```
## API Endpoints
### Health Checks
- `GET /health` - Health check
- `GET /ready` - Readiness check
### Island Operations (Phase 2)
- `POST /v1/islands/join` - Join an island
- `POST /v1/islands/leave` - Leave an island
- `GET /v1/islands` - List all islands
- `GET /v1/islands/{island_id}` - Get island details
- `POST /v1/islands/bridge` - Request bridge
### GPU Operations (Phase 3)
- `POST /v1/gpu/listings` - List GPU on island
- `GET /v1/gpu/listings` - Get GPU listings
- `DELETE /v1/gpu/listings/{listing_id}` - Remove GPU listing
- `GET /v1/gpu/scan` - Scan GPUs
### Edge Database Operations (Phase 4)
- `POST /v1/database/init` - Initialize edge database
- `GET /v1/database` - Get edge database status
- `DELETE /v1/database` - Delete edge database
- `POST /v1/database/sync` - Sync edge database
### Edge Serve Operations (Phase 5)
- `POST /v1/serve/start` - Start serving
- `POST /v1/serve/stop` - Stop serving
- `GET /v1/serve/status` - Get serve status
- `GET /v1/serve/requests` - Get pending requests
- `POST /v1/serve/requests/{request_id}/complete` - Complete request
### Edge Metrics (Phase 6)
- `GET /v1/metrics` - Get edge metrics
- `GET /v1/metrics/gpu` - Get GPU metrics
- `GET /v1/metrics/database` - Get database metrics
## Implementation Status
**Phase 1: Foundation**
- Service structure
- FastAPI application
- Database configuration
- Basic stub endpoints
**Phase 2: Island Operations**
- Island join/leave endpoints
- Blockchain RPC integration
- Island listing
**Phase 3: GPU Operations**
- GPU listing endpoints
- GPU service integration
- GPU metrics
**Phase 4: Edge Database**
- Database initialization
- Database sync
- Database management
**Phase 5: Edge Serve**
- Serve start/stop
- Request queue
- Request processing
**Phase 6: Metrics**
- Metrics collection
- Metrics endpoints
- Performance monitoring
## CLI Integration
CLI commands will be updated to use REST API instead of placeholder implementations:
- `aitbc island join``POST /v1/islands/join`
- `aitbc gpu list-island``POST /v1/gpu/listings`
- `aitbc database init-edge``POST /v1/database/init`
- `aitbc edge serve``POST /v1/serve/start`
- `aitbc edge metrics``GET /v1/metrics`
## Agent SDK Integration
Agent SDK will be updated to use REST API for island and edge operations.
## Dependencies
- FastAPI
- SQLAlchemy + SQLModel
- PostgreSQL
- httpx
- PyJWT
- uvicorn

View File

@@ -0,0 +1,22 @@
[Unit]
Description=AITBC Edge API Service
After=network.target postgresql.service
[Service]
Type=simple
WorkingDirectory=/opt/aitbc/apps/edge-api
Environment="PATH=/opt/aitbc/venv/bin"
Environment="PYTHONPATH=/opt/aitbc/packages/py/aitbc-core/src:/opt/aitbc/apps/edge-api/src:/opt/aitbc"
Environment="DATABASE_URL=postgresql+asyncpg://aitbc_edge:password@localhost:5432/aitbc_edge"
Environment="BLOCKCHAIN_RPC_HOST=localhost"
Environment="BLOCKCHAIN_RPC_PORT=8006"
Environment="GPU_SERVICE_HOST=localhost"
Environment="GPU_SERVICE_PORT=8101"
Environment="JWT_SECRET_KEY=your-secret-key-change-in-production"
Environment="API_PORT=8103"
ExecStart=/opt/aitbc/venv/bin/python -m edge_api.main
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,24 @@
[project]
name = "aitbc-edge"
version = "0.1.0"
description = "Edge API Service for AITBC island and edge operations"
requires-python = ">=3.13"
dependencies = [
"fastapi>=0.115.6",
"uvicorn>=0.34.0",
"sqlmodel>=0.0.38",
"sqlalchemy[asyncio]>=2.0.49",
"asyncpg>=0.30.0",
"httpx>=0.28.1",
"pydantic>=2.11.0",
"pydantic-settings>=2.13.1",
"python-jose[cryptography]>=3.3.0",
"passlib[bcrypt]>=1.7.4",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/aitbc_edge"]

View File

@@ -0,0 +1,3 @@
"""Edge API Service for AITBC island and edge operations"""
__version__ = "0.1.0"

View File

@@ -0,0 +1,9 @@
"""Clients for Edge API Service"""
from .blockchain_rpc import BlockchainRPCClient
from .gpu_service import GPUServiceClient
__all__ = [
"BlockchainRPCClient",
"GPUServiceClient",
]

View File

@@ -0,0 +1,65 @@
"""Blockchain RPC client for Edge API Service"""
import httpx
from typing import Dict, Optional, Any
from ..config import settings
class BlockchainRPCClient:
"""Client for blockchain node RPC communication"""
def __init__(self):
self.base_url = f"http://{settings.blockchain_rpc_host}:{settings.blockchain_rpc_port}"
self.client = httpx.AsyncClient(timeout=30.0)
async def close(self):
"""Close the HTTP client"""
await self.client.aclose()
async def join_island(self, island_id: str, island_name: str, chain_id: str, role: str = "compute-provider", is_hub: bool = False) -> Dict[str, Any]:
"""Join island via blockchain RPC"""
response = await self.client.post(
f"{self.base_url}/rpc/islands/join",
json={
"island_id": island_id,
"island_name": island_name,
"chain_id": chain_id,
"role": role,
"is_hub": is_hub
}
)
response.raise_for_status()
return response.json()
async def leave_island(self, island_id: str) -> Dict[str, Any]:
"""Leave island via blockchain RPC"""
response = await self.client.post(
f"{self.base_url}/rpc/islands/leave",
json={"island_id": island_id}
)
response.raise_for_status()
return response.json()
async def get_island_info(self, island_id: str) -> Optional[Dict[str, Any]]:
"""Get island info via blockchain RPC"""
response = await self.client.get(f"{self.base_url}/rpc/islands/{island_id}")
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
async def list_islands(self) -> Dict[str, Any]:
"""List all islands via blockchain RPC"""
response = await self.client.get(f"{self.base_url}/rpc/islands")
response.raise_for_status()
return response.json()
async def request_bridge(self, target_island_id: str) -> Dict[str, Any]:
"""Request bridge via blockchain RPC"""
response = await self.client.post(
f"{self.base_url}/rpc/islands/bridge",
json={"target_island_id": target_island_id}
)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,50 @@
"""GPU service client for Edge API Service"""
import httpx
from typing import Dict, Optional, Any, List
from ..config import settings
class GPUServiceClient:
"""Client for GPU service communication"""
def __init__(self):
self.base_url = f"http://{settings.gpu_service_host}:{settings.gpu_service_port}"
self.client = httpx.AsyncClient(timeout=30.0)
async def close(self):
"""Close the HTTP client"""
await self.client.aclose()
async def scan_gpus(self, miner_id: str) -> Dict[str, Any]:
"""Scan GPUs via GPU service"""
response = await self.client.post(f"{self.base_url}/v1/marketplace/edge-gpu/scan/{miner_id}")
response.raise_for_status()
return response.json()
async def get_gpu_profiles(self, architecture: str = None, edge_optimized: bool = None, min_memory_gb: int = None) -> List[Dict[str, Any]]:
"""Get GPU profiles via GPU service"""
params = {}
if architecture:
params["architecture"] = architecture
if edge_optimized is not None:
params["edge_optimized"] = edge_optimized
if min_memory_gb is not None:
params["min_memory_gb"] = min_memory_gb
response = await self.client.get(f"{self.base_url}/v1/marketplace/edge-gpu/profiles", params=params)
response.raise_for_status()
return response.json()
async def get_gpu_metrics(self, gpu_id: str, limit: int = 100) -> List[Dict[str, Any]]:
"""Get GPU metrics via GPU service"""
response = await self.client.get(f"{self.base_url}/v1/marketplace/edge-gpu/metrics/{gpu_id}", params={"limit": limit})
response.raise_for_status()
return response.json()
async def get_miner_gpus(self, miner_id: str) -> List[Dict[str, Any]]:
"""Get GPUs registered by a miner"""
response = await self.client.get(f"{self.base_url}/v1/miners/{miner_id}/gpus")
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,43 @@
"""Configuration for Edge API Service"""
import os
from typing import Optional
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Edge API settings"""
# API settings
api_host: str = "0.0.0.0"
api_port: int = 8103
api_prefix: str = "/v1"
# Database settings
database_url: str = "postgresql+asyncpg://aitbc_edge:password@localhost:5432/aitbc_edge"
# Blockchain node RPC settings
blockchain_rpc_host: str = "localhost"
blockchain_rpc_port: int = 8006
# GPU service settings
gpu_service_host: str = "localhost"
gpu_service_port: int = 8101
# JWT settings
jwt_secret_key: str = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
jwt_algorithm: str = "HS256"
jwt_expiration_hours: int = 24
# CORS settings
cors_origins: list[str] = ["http://localhost:3000", "http://localhost:8080"]
# Logging
log_level: str = "INFO"
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()

View File

@@ -0,0 +1,104 @@
"""Main FastAPI application for Edge API Service"""
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from aitbc import get_logger
from .config import settings
from .storage import init_db
from .routers import islands_router as islands, gpu_router as gpu, database_router as database, serve_router as serve, metrics_router as metrics
logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan context manager for startup/shutdown"""
# Startup
logger.info("Starting Edge API Service")
await init_db()
logger.info("Database initialized")
yield
# Shutdown
logger.info("Shutting down Edge API Service")
# Create FastAPI application
app = FastAPI(
title="Edge API Service",
description="REST API for AITBC island and edge operations",
version="0.1.0",
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Health check endpoint
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "edge-api",
"version": "0.1.0"
}
# Readiness check endpoint
@app.get("/ready")
async def readiness_check():
"""Readiness check endpoint"""
# TODO: Check database connection, blockchain RPC, GPU service
return {
"status": "ready",
"service": "edge-api",
"version": "0.1.0"
}
# Include routers
app.include_router(islands, prefix=f"{settings.api_prefix}/islands", tags=["islands"])
app.include_router(gpu, prefix=f"{settings.api_prefix}/gpu", tags=["gpu"])
app.include_router(database, prefix=f"{settings.api_prefix}/database", tags=["database"])
app.include_router(serve, prefix=f"{settings.api_prefix}/serve", tags=["serve"])
app.include_router(metrics, prefix=f"{settings.api_prefix}/metrics", tags=["metrics"])
# Global exception handler
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Global exception handler"""
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": "Internal server error",
"detail": str(exc)
}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"edge_api.main:app",
host=settings.api_host,
port=settings.api_port,
reload=True,
log_level=settings.log_level.lower()
)

View File

@@ -0,0 +1,15 @@
"""Routers for Edge API Service"""
from .islands import router as islands_router
from .gpu import router as gpu_router
from .database import router as database_router
from .serve import router as serve_router
from .metrics import router as metrics_router
__all__ = [
"islands_router",
"gpu_router",
"database_router",
"serve_router",
"metrics_router",
]

View File

@@ -0,0 +1,60 @@
"""Database operations router for Edge API Service"""
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from ..services.database_service import DatabaseService
router = APIRouter()
class InitDatabaseRequest(BaseModel):
"""Request model for initializing a database"""
database_id: str
island_id: str
capacity_gb: int
def get_database_service() -> DatabaseService:
"""Dependency injection for database service"""
return DatabaseService()
@router.post("/init")
async def init_database(request: InitDatabaseRequest, svc: DatabaseService = Depends(get_database_service)):
"""Initialize edge database"""
result = await svc.init_database(request.database_id, request.island_id, request.capacity_gb)
return result
@router.get("/")
async def list_databases(island_id: str = Query(None), svc: DatabaseService = Depends(get_database_service)):
"""List databases, optionally filtered by island_id"""
databases = await svc.list_databases(island_id)
return {"databases": databases, "total": len(databases)}
@router.get("/{database_id}")
async def get_database(database_id: str, svc: DatabaseService = Depends(get_database_service)):
"""Get database details"""
db = await svc.get_database(database_id)
if db is None:
raise HTTPException(status_code=404, detail=f"Database {database_id} not found")
return db
@router.delete("/{database_id}")
async def delete_database(database_id: str, svc: DatabaseService = Depends(get_database_service)):
"""Delete database"""
success = await svc.delete_database(database_id)
if success:
return {"message": f"Database {database_id} deleted"}
else:
raise HTTPException(status_code=404, detail=f"Database {database_id} not found")
@router.post("/{database_id}/sync")
async def sync_database(database_id: str, svc: DatabaseService = Depends(get_database_service)):
"""Sync database from source"""
result = await svc.sync_database(database_id)
return result

View File

@@ -0,0 +1,67 @@
"""GPU operations router for Edge API Service"""
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from ..services.gpu_service import GPUService
router = APIRouter()
class ScanGPUsRequest(BaseModel):
"""Request model for scanning GPUs"""
miner_id: str
def get_gpu_service() -> GPUService:
"""Dependency injection for GPU service"""
return GPUService()
@router.get("/")
async def list_gpus(
architecture: str = Query(None),
edge_optimized: bool = Query(None),
min_memory_gb: int = Query(None),
svc: GPUService = Depends(get_gpu_service)
):
"""List all GPUs"""
gpus = await svc.list_gpus(architecture=architecture, edge_optimized=edge_optimized, min_memory_gb=min_memory_gb)
return {"gpus": gpus, "total": len(gpus)}
@router.get("/{gpu_id}")
async def get_gpu_listing(gpu_id: str, svc: GPUService = Depends(get_gpu_service)):
"""Get GPU listing details"""
gpu = await svc.get_gpu_listing(gpu_id)
if gpu is None:
raise HTTPException(status_code=404, detail=f"GPU {gpu_id} not found")
return gpu
@router.delete("/{gpu_id}")
async def remove_gpu_listing(gpu_id: str, svc: GPUService = Depends(get_gpu_service)):
"""Remove GPU listing"""
success = await svc.remove_gpu_listing(gpu_id)
if success:
return {"message": f"GPU {gpu_id} removed"}
else:
raise HTTPException(status_code=404, detail=f"GPU {gpu_id} not found")
@router.post("/scan")
async def scan_gpus(request: ScanGPUsRequest, svc: GPUService = Depends(get_gpu_service)):
"""Scan GPUs for a miner"""
result = await svc.scan_gpus(request.miner_id)
return result
@router.get("/{gpu_id}/metrics")
async def get_gpu_metrics(
gpu_id: str,
limit: int = Query(100),
svc: GPUService = Depends(get_gpu_service)
):
"""Get GPU metrics"""
metrics = await svc.get_gpu_metrics(gpu_id, limit)
return {"gpu_id": gpu_id, "metrics": metrics, "total": len(metrics)}

View File

@@ -0,0 +1,75 @@
"""Island operations router for Edge API Service"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ..services.island_service import IslandService
router = APIRouter()
class JoinIslandRequest(BaseModel):
"""Request model for joining an island"""
island_id: str
island_name: str
chain_id: str
role: str = Field(default="compute-provider")
is_hub: bool = Field(default=False)
class LeaveIslandRequest(BaseModel):
"""Request model for leaving an island"""
island_id: str
class BridgeRequestRequest(BaseModel):
"""Request model for requesting a bridge"""
target_island_id: str
def get_island_service() -> IslandService:
"""Dependency injection for island service"""
return IslandService()
@router.post("/join")
async def join_island(request: JoinIslandRequest, svc: IslandService = Depends(get_island_service)):
"""Join an island"""
result = await svc.join_island(
island_id=request.island_id,
island_name=request.island_name,
chain_id=request.chain_id,
role=request.role,
is_hub=request.is_hub
)
return result
@router.post("/leave")
async def leave_island(request: LeaveIslandRequest, svc: IslandService = Depends(get_island_service)):
"""Leave an island"""
result = await svc.leave_island(request.island_id)
return result
@router.get("/")
async def list_islands(svc: IslandService = Depends(get_island_service)):
"""List all islands"""
islands = await svc.list_islands()
return {"islands": islands, "total": len(islands)}
@router.get("/{island_id}")
async def get_island(island_id: str, svc: IslandService = Depends(get_island_service)):
"""Get island details"""
island = await svc.get_island(island_id)
if island is None:
raise HTTPException(status_code=404, detail=f"Island {island_id} not found")
return island
@router.post("/bridge")
async def request_bridge(request: BridgeRequestRequest, svc: IslandService = Depends(get_island_service)):
"""Request bridge to another island"""
result = await svc.request_bridge(request.target_island_id)
return result

View File

@@ -0,0 +1,52 @@
"""Metrics operations router for Edge API Service"""
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from ..services.metrics_service import MetricsService
router = APIRouter()
class RecordMetricsRequest(BaseModel):
"""Request model for recording metrics"""
gpu_id: str
metrics: dict
def get_metrics_service() -> MetricsService:
"""Dependency injection for metrics service"""
return MetricsService()
@router.post("/")
async def record_metrics(request: RecordMetricsRequest, svc: MetricsService = Depends(get_metrics_service)):
"""Record edge metrics"""
result = await svc.record_metrics(request.gpu_id, request.metrics)
return result
@router.get("/")
async def list_metrics(gpu_id: str = Query(None), limit: int = Query(100), svc: MetricsService = Depends(get_metrics_service)):
"""List metrics, optionally filtered by gpu_id"""
metrics = await svc.list_metrics(gpu_id, limit)
return {"metrics": metrics, "total": len(metrics)}
@router.get("/{metric_id}")
async def get_metrics(metric_id: str, svc: MetricsService = Depends(get_metrics_service)):
"""Get metric details"""
metric = await svc.get_metrics(metric_id)
if metric is None:
raise HTTPException(status_code=404, detail=f"Metric {metric_id} not found")
return metric
@router.delete("/{metric_id}")
async def delete_metrics(metric_id: str, svc: MetricsService = Depends(get_metrics_service)):
"""Delete metric"""
success = await svc.delete_metrics(metric_id)
if success:
return {"message": f"Metric {metric_id} deleted"}
else:
raise HTTPException(status_code=404, detail=f"Metric {metric_id} not found")

View File

@@ -0,0 +1,63 @@
"""Edge serve operations router for Edge API Service"""
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from ..services.serve_service import ServeService
router = APIRouter()
class SubmitComputeRequest(BaseModel):
"""Request model for submitting compute request"""
gpu_id: str
model_name: str
input_data: dict
priority: str = Field(default="normal")
def get_serve_service() -> ServeService:
"""Dependency injection for serve service"""
return ServeService()
@router.post("/requests")
async def submit_compute_request(request: SubmitComputeRequest, svc: ServeService = Depends(get_serve_service)):
"""Submit compute request"""
result = await svc.submit_compute_request(request.gpu_id, request.model_name, request.input_data, request.priority)
return result
@router.get("/requests")
async def list_compute_requests(gpu_id: str = Query(None), status: str = Query(None), svc: ServeService = Depends(get_serve_service)):
"""List compute requests, optionally filtered"""
requests = await svc.list_compute_requests(gpu_id, status)
return {"requests": requests, "total": len(requests)}
@router.get("/requests/{request_id}")
async def get_compute_request(request_id: str, svc: ServeService = Depends(get_serve_service)):
"""Get compute request details"""
req = await svc.get_compute_request(request_id)
if req is None:
raise HTTPException(status_code=404, detail=f"Request {request_id} not found")
return req
@router.post("/requests/{request_id}/cancel")
async def cancel_compute_request(request_id: str, svc: ServeService = Depends(get_serve_service)):
"""Cancel compute request"""
success = await svc.cancel_compute_request(request_id)
if success:
return {"message": f"Request {request_id} cancelled"}
else:
raise HTTPException(status_code=400, detail=f"Request {request_id} cannot be cancelled")
@router.get("/requests/{request_id}/result")
async def get_compute_result(request_id: str, svc: ServeService = Depends(get_serve_service)):
"""Get compute result"""
result = await svc.get_compute_result(request_id)
if result is None:
raise HTTPException(status_code=404, detail=f"Result for request {request_id} not found")
return result

View File

@@ -0,0 +1,17 @@
"""Schemas for Edge API Service"""
from .island import IslandMembership, BridgeRequest
from .gpu import GPUListing
from .database import EdgeDatabase
from .serve import ComputeRequest, ComputeResult
from .metrics import EdgeMetrics
__all__ = [
"IslandMembership",
"BridgeRequest",
"GPUListing",
"EdgeDatabase",
"ComputeRequest",
"ComputeResult",
"EdgeMetrics",
]

View File

@@ -0,0 +1,31 @@
"""Edge database-related schemas for Edge API Service"""
from datetime import datetime
from uuid import uuid4
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class EdgeDatabase(SQLModel, table=True):
"""Edge database instance"""
__tablename__ = "edge_databases"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"edge_db_{uuid4().hex[:8]}", primary_key=True)
database_id: str = Field(index=True)
island_id: str = Field(index=True)
capacity_gb: int
used_gb: int = Field(default=0)
status: str = Field(default="initialized", index=True) # initialized, active, syncing, error
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
# Sync information
last_sync_at: datetime | None = Field(default=None)
sync_status: str = Field(default="idle") # idle, syncing, error
records_synced: int = Field(default=0)
# Database metadata
extra_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=True))

View File

@@ -0,0 +1,35 @@
"""GPU-related schemas for Edge API Service"""
from datetime import datetime
from uuid import uuid4
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class GPUListing(SQLModel, table=True):
"""GPU listing on island"""
__tablename__ = "gpu_listings"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"gpu_listing_{uuid4().hex[:8]}", primary_key=True)
listing_id: str = Field(index=True)
island_id: str = Field(index=True)
miner_id: str = Field(index=True)
gpu_type: str = Field(index=True)
price_per_hour: float
status: str = Field(default="active", index=True) # active, inactive, booked
created_at: datetime = Field(default_factory=lambda: datetime.utcnow())
updated_at: datetime = Field(default_factory=lambda: datetime.utcnow())
# GPU specifications
memory_gb: int | None = Field(default=None)
cuda_version: str = Field(default="")
region: str = Field(default="")
# Capabilities
capabilities: list = Field(default_factory=list, sa_column=Column(JSON, nullable=True))
# Listing metadata
extra_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=True))

View File

@@ -0,0 +1,54 @@
"""Island-related schemas for Edge API Service"""
from datetime import datetime
from enum import StrEnum
from uuid import uuid4
from sqlalchemy import JSON, Column, Enum as SQLEnum
from sqlmodel import Field, SQLModel
class IslandStatus(StrEnum):
"""Island membership status"""
ACTIVE = "active"
INACTIVE = "inactive"
BRIDGING = "bridging"
JOINED = "joined"
class IslandMembership(SQLModel, table=True):
"""Island membership in edge API database"""
__tablename__ = "island_memberships"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"membership_{uuid4().hex[:8]}", primary_key=True)
island_id: str = Field(sa_column=Column(index=True))
island_name: str
chain_id: str = Field(sa_column=Column(index=True))
status: IslandStatus = Field(
default=IslandStatus.ACTIVE,
sa_column=Column(SQLEnum(IslandStatus, values_only=True), index=True)
)
role: str = Field(default="compute-provider") # compute-provider, consumer, hub
joined_at: datetime = Field(default_factory=lambda: datetime.utcnow())
peer_count: int = Field(default=0)
# Additional metadata
extra_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=True))
class BridgeRequest(SQLModel, table=True):
"""Bridge request for island connectivity"""
__tablename__ = "bridge_requests"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"bridge_req_{uuid4().hex[:8]}", primary_key=True)
request_id: str = Field(index=True)
source_island_id: str
target_island_id: str
source_node_id: str
status: str = Field(default="pending", index=True) # pending, approved, rejected
created_at: datetime = Field(default_factory=lambda: datetime.utcnow())
updated_at: datetime = Field(default_factory=lambda: datetime.utcnow())

View File

@@ -0,0 +1,21 @@
"""Edge metrics-related schemas for Edge API Service"""
from datetime import datetime
from uuid import uuid4
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class EdgeMetrics(SQLModel, table=True):
"""Edge performance metrics"""
__tablename__ = "edge_metrics"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"edge_metric_{uuid4().hex[:8]}", primary_key=True)
metric_id: str = Field(index=True)
gpu_id: str = Field(index=True)
metrics_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=False))
created_at: datetime = Field(default_factory=datetime.utcnow)
extra_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=True))

View File

@@ -0,0 +1,45 @@
"""Edge serve-related schemas for Edge API Service"""
from datetime import datetime
from uuid import uuid4
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class ComputeRequest(SQLModel, table=True):
"""Compute request in edge serve queue"""
__tablename__ = "compute_requests"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"compute_req_{uuid4().hex[:8]}", primary_key=True)
request_id: str = Field(index=True)
gpu_id: str = Field(index=True)
model_name: str
input_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=True))
priority: str = Field(default="normal")
status: str = Field(default="queued", index=True) # queued, running, completed, failed, cancelled
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
started_at: datetime | None = Field(default=None)
completed_at: datetime | None = Field(default=None)
error: str | None = Field(default=None)
extra_data: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=True))
class ComputeResult(SQLModel, table=True):
"""Compute result cache"""
__tablename__ = "compute_results"
__table_args__ = {"extend_existing": True}
id: str = Field(default_factory=lambda: f"compute_res_{uuid4().hex[:8]}", primary_key=True)
result_id: str = Field(index=True)
request_id: str = Field(index=True)
island_id: str = Field(index=True)
gpu_id: str
result: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=False))
cache_ttl: int = Field(default=3600) # 1 hour default
created_at: datetime = Field(default_factory=datetime.utcnow)
expires_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,15 @@
"""Services for Edge API Service"""
from .island_service import IslandService
from .gpu_service import GPUService
from .database_service import DatabaseService
from .serve_service import ServeService
from .metrics_service import MetricsService
__all__ = [
"IslandService",
"GPUService",
"DatabaseService",
"ServeService",
"MetricsService",
]

View File

@@ -0,0 +1,132 @@
"""Database service for Edge API Service"""
from typing import Dict, Optional, List
from datetime import datetime
from uuid import uuid4
from ..storage import get_session
from ..schemas.database import EdgeDatabase
from sqlmodel import select, delete
class DatabaseService:
"""Service for edge database operations"""
async def init_database(self, database_id: str, island_id: str, capacity_gb: int) -> Dict:
"""Initialize edge database"""
async with get_session() as session:
# Check if database already exists
result = await session.execute(select(EdgeDatabase).where(EdgeDatabase.database_id == database_id))
existing_db = result.scalar_one_or_none()
if existing_db:
return {
"success": False,
"message": f"Database {database_id} already exists",
"database": existing_db.database_id
}
# Create new database record
db = EdgeDatabase(
database_id=database_id,
island_id=island_id,
capacity_gb=capacity_gb,
used_gb=0,
status="initialized",
sync_status="idle",
records_synced=0
)
session.add(db)
await session.commit()
return {
"success": True,
"message": f"Database {database_id} initialized",
"database": database_id,
"id": db.id
}
async def get_database(self, database_id: str) -> Optional[Dict]:
"""Get database details"""
async with get_session() as session:
result = await session.execute(select(EdgeDatabase).where(EdgeDatabase.database_id == database_id))
db = result.scalar_one_or_none()
if db:
return {
"id": db.id,
"database_id": db.database_id,
"island_id": db.island_id,
"capacity_gb": db.capacity_gb,
"used_gb": db.used_gb,
"status": db.status,
"created_at": db.created_at.isoformat() if db.created_at else None,
"updated_at": db.updated_at.isoformat() if db.updated_at else None,
"last_sync_at": db.last_sync_at.isoformat() if db.last_sync_at else None,
"sync_status": db.sync_status,
"records_synced": db.records_synced,
"extra_data": db.extra_data
}
return None
async def delete_database(self, database_id: str) -> bool:
"""Delete database"""
async with get_session() as session:
stmt = delete(EdgeDatabase).where(EdgeDatabase.database_id == database_id)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
async def sync_database(self, database_id: str) -> Dict:
"""Sync database from source"""
async with get_session() as session:
result = await session.execute(select(EdgeDatabase).where(EdgeDatabase.database_id == database_id))
db = result.scalar_one_or_none()
if not db:
return {
"success": False,
"message": f"Database {database_id} not found"
}
# Update sync status in single transaction
db.sync_status = "syncing"
db.updated_at = datetime.utcnow()
# Simulate sync process (in production, this would actually sync data)
db.sync_status = "idle"
db.last_sync_at = datetime.utcnow()
db.records_synced = db.records_synced + 100 # Simulated
db.updated_at = datetime.utcnow()
await session.commit()
return {
"success": True,
"message": f"Database {database_id} synced",
"records_synced": db.records_synced
}
async def list_databases(self, island_id: str = None) -> List[Dict]:
"""List databases, optionally filtered by island_id"""
async with get_session() as session:
if island_id:
result = await session.execute(select(EdgeDatabase).where(EdgeDatabase.island_id == island_id))
else:
result = await session.execute(select(EdgeDatabase))
databases = result.scalars().all()
return [
{
"id": db.id,
"database_id": db.database_id,
"island_id": db.island_id,
"capacity_gb": db.capacity_gb,
"used_gb": db.used_gb,
"status": db.status,
"sync_status": db.sync_status,
"records_synced": db.records_synced,
"created_at": db.created_at.isoformat() if db.created_at else None
}
for db in databases
]

View File

@@ -0,0 +1,81 @@
"""GPU service for Edge API Service"""
from typing import Dict, List, Optional
from ..clients.gpu_service import GPUServiceClient
from ..storage import get_session
from ..schemas.gpu import GPUListing
class GPUService:
"""Service for GPU operations"""
def __init__(self):
self.gpu_client = GPUServiceClient()
async def list_gpus(self, architecture: str = None, edge_optimized: bool = None, min_memory_gb: int = None) -> List[Dict]:
"""List GPUs via GPU service"""
profiles = await self.gpu_client.get_gpu_profiles(architecture, edge_optimized, min_memory_gb)
# Store GPU listings in edge-api database
async with get_session() as session:
for profile in profiles:
gpu_listing = GPUListing(
gpu_id=profile.get("id", ""),
model=profile.get("model", "Unknown"),
memory_gb=profile.get("memory_gb", 0),
cuda_version=profile.get("cuda_version", ""),
region=profile.get("region", ""),
capabilities=profile.get("capabilities", []),
extra_data=profile
)
session.add(gpu_listing)
await session.commit()
return profiles
async def get_gpu_listing(self, gpu_id: str) -> Optional[Dict]:
"""Get GPU listing details"""
# Get from GPU service
try:
profiles = await self.gpu_client.get_gpu_profiles()
for profile in profiles:
if profile.get("id") == gpu_id:
return profile
return None
except Exception as e:
# Fall back to database
from sqlmodel import select
async with get_session() as session:
result = await session.execute(select(GPUListing).where(GPUListing.gpu_id == gpu_id))
gpu = result.scalar_one_or_none()
if gpu:
return {
"id": gpu.gpu_id,
"model": gpu.model,
"memory_gb": gpu.memory_gb,
"cuda_version": gpu.cuda_version,
"region": gpu.region,
"capabilities": gpu.capabilities,
"extra_data": gpu.extra_data
}
return None
async def remove_gpu_listing(self, gpu_id: str) -> bool:
"""Remove GPU listing from database"""
from sqlmodel import delete
async with get_session() as session:
stmt = delete(GPUListing).where(GPUListing.gpu_id == gpu_id)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
async def scan_gpus(self, miner_id: str) -> Dict:
"""Scan GPUs via GPU service"""
result = await self.gpu_client.scan_gpus(miner_id)
return result
async def get_gpu_metrics(self, gpu_id: str, limit: int = 100) -> List[Dict]:
"""Get GPU metrics via GPU service"""
metrics = await self.gpu_client.get_gpu_metrics(gpu_id, limit)
return metrics

View File

@@ -0,0 +1,87 @@
"""Island service for Edge API Service"""
from typing import Dict, List, Optional
from ..clients.blockchain_rpc import BlockchainRPCClient
from ..storage import get_session
from ..schemas.island import IslandMembership, BridgeRequest, IslandStatus
class IslandService:
"""Service for island operations"""
def __init__(self):
self.rpc_client = BlockchainRPCClient()
async def join_island(self, island_id: str, island_name: str, chain_id: str, role: str = "compute-provider", is_hub: bool = False) -> Dict:
"""Join an island via blockchain RPC"""
# Call blockchain RPC to join island
result = await self.rpc_client.join_island(island_id, island_name, chain_id, role, is_hub)
# Store membership in edge-api database
if result.get("success"):
async with get_session() as session:
# Map blockchain status string to IslandStatus enum
# PostgreSQL enum only has: active, inactive, bridging
# Map "joined" to "active"
raw_status = result.get("status", "active").lower()
if raw_status == "joined":
raw_status = "active"
try:
status = IslandStatus(raw_status)
except ValueError:
status = IslandStatus.ACTIVE
membership = IslandMembership(
island_id=island_id,
island_name=island_name,
chain_id=chain_id,
role=role,
status=status
)
session.add(membership)
await session.commit()
return result
async def leave_island(self, island_id: str) -> Dict:
"""Leave an island via blockchain RPC"""
# Call blockchain RPC to leave island
result = await self.rpc_client.leave_island(island_id)
# Remove membership from edge-api database
if result.get("success"):
async with get_session() as session:
from sqlmodel import delete
stmt = delete(IslandMembership).where(IslandMembership.island_id == island_id)
await session.execute(stmt)
await session.commit()
return result
async def list_islands(self) -> List[Dict]:
"""List all islands via blockchain RPC"""
result = await self.rpc_client.list_islands()
return result.get("islands", [])
async def get_island(self, island_id: str) -> Optional[Dict]:
"""Get island details via blockchain RPC"""
result = await self.rpc_client.get_island_info(island_id)
return result
async def request_bridge(self, target_island_id: str) -> Dict:
"""Request bridge to another island via blockchain RPC"""
result = await self.rpc_client.request_bridge(target_island_id)
# Store bridge request in edge-api database
if result.get("success"):
async with get_session() as session:
bridge_req = BridgeRequest(
request_id=result.get("request_id"),
target_island_id=target_island_id,
source_node_id="edge-api", # TODO: Get actual node ID
status=result.get("status", "pending")
)
session.add(bridge_req)
await session.commit()
return result

View File

@@ -0,0 +1,79 @@
"""Edge metrics service for Edge API Service"""
from typing import Dict, Optional, List
from datetime import datetime
from uuid import uuid4
from ..storage import get_session
from ..schemas.metrics import EdgeMetrics
from sqlmodel import select, delete
class MetricsService:
"""Service for edge metrics operations"""
async def record_metrics(self, gpu_id: str, metrics: dict) -> Dict:
"""Record edge metrics"""
async with get_session() as session:
metric_id = f"metric_{uuid4().hex[:8]}"
metric = EdgeMetrics(
metric_id=metric_id,
gpu_id=gpu_id,
metrics_data=metrics
)
session.add(metric)
await session.commit()
return {
"success": True,
"metric_id": metric_id,
"message": f"Metrics {metric_id} recorded"
}
async def get_metrics(self, metric_id: str) -> Optional[Dict]:
"""Get metric details"""
async with get_session() as session:
result = await session.execute(select(EdgeMetrics).where(EdgeMetrics.metric_id == metric_id))
metric = result.scalar_one_or_none()
if metric:
return {
"metric_id": metric.metric_id,
"gpu_id": metric.gpu_id,
"metrics_data": metric.metrics_data,
"created_at": metric.created_at.isoformat() if metric.created_at else None,
"extra_data": metric.extra_data
}
return None
async def list_metrics(self, gpu_id: str = None, limit: int = 100) -> List[Dict]:
"""List metrics, optionally filtered by gpu_id"""
async with get_session() as session:
query = select(EdgeMetrics)
if gpu_id:
query = query.where(EdgeMetrics.gpu_id == gpu_id)
query = query.order_by(EdgeMetrics.created_at.desc()).limit(limit)
result = await session.execute(query)
metrics = result.scalars().all()
return [
{
"metric_id": metric.metric_id,
"gpu_id": metric.gpu_id,
"metrics_data": metric.metrics_data,
"created_at": metric.created_at.isoformat() if metric.created_at else None
}
for metric in metrics
]
async def delete_metrics(self, metric_id: str) -> bool:
"""Delete metric"""
async with get_session() as session:
stmt = delete(EdgeMetrics).where(EdgeMetrics.metric_id == metric_id)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0

View File

@@ -0,0 +1,114 @@
"""Edge serve service for Edge API Service"""
from typing import Dict, Optional, List
from datetime import datetime
from uuid import uuid4
from ..storage import get_session
from ..schemas.serve import ComputeRequest, ComputeResult
from sqlmodel import select, delete
class ServeService:
"""Service for edge serve operations"""
async def submit_compute_request(self, gpu_id: str, model_name: str, input_data: dict, priority: str = "normal") -> Dict:
"""Submit compute request"""
async with get_session() as session:
request_id = f"req_{uuid4().hex[:8]}"
request = ComputeRequest(
request_id=request_id,
gpu_id=gpu_id,
model_name=model_name,
input_data=input_data,
priority=priority,
status="queued"
)
session.add(request)
await session.commit()
return {
"success": True,
"request_id": request_id,
"status": "queued",
"message": f"Compute request {request_id} submitted"
}
async def get_compute_request(self, request_id: str) -> Optional[Dict]:
"""Get compute request details"""
async with get_session() as session:
result = await session.execute(select(ComputeRequest).where(ComputeRequest.request_id == request_id))
req = result.scalar_one_or_none()
if req:
return {
"request_id": req.request_id,
"gpu_id": req.gpu_id,
"model_name": req.model_name,
"input_data": req.input_data,
"priority": req.priority,
"status": req.status,
"created_at": req.created_at.isoformat() if req.created_at else None,
"started_at": req.started_at.isoformat() if req.started_at else None,
"completed_at": req.completed_at.isoformat() if req.completed_at else None,
"error": req.error,
"extra_data": req.extra_data
}
return None
async def cancel_compute_request(self, request_id: str) -> bool:
"""Cancel compute request"""
async with get_session() as session:
result = await session.execute(select(ComputeRequest).where(ComputeRequest.request_id == request_id))
req = result.scalar_one_or_none()
if req and req.status in ["queued", "running"]:
req.status = "cancelled"
req.completed_at = datetime.utcnow()
await session.commit()
return True
return False
async def list_compute_requests(self, gpu_id: str = None, status: str = None) -> List[Dict]:
"""List compute requests, optionally filtered"""
async with get_session() as session:
query = select(ComputeRequest)
if gpu_id:
query = query.where(ComputeRequest.gpu_id == gpu_id)
if status:
query = query.where(ComputeRequest.status == status)
result = await session.execute(query)
requests = result.scalars().all()
return [
{
"request_id": req.request_id,
"gpu_id": req.gpu_id,
"model_name": req.model_name,
"priority": req.priority,
"status": req.status,
"created_at": req.created_at.isoformat() if req.created_at else None
}
for req in requests
]
async def get_compute_result(self, request_id: str) -> Optional[Dict]:
"""Get compute result"""
async with get_session() as session:
result = await session.execute(select(ComputeResult).where(ComputeResult.request_id == request_id))
res = result.scalar_one_or_none()
if res:
return {
"result_id": res.result_id,
"request_id": res.request_id,
"output_data": res.output_data,
"metrics": res.metrics,
"status": res.status,
"created_at": res.created_at.isoformat() if res.created_at else None,
"extra_data": res.extra_data
}
return None

View File

@@ -0,0 +1,46 @@
"""Database session management for Edge API Service"""
import os
from contextlib import asynccontextmanager
from typing import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlmodel import SQLModel
from aitbc import get_logger
logger = get_logger(__name__)
# Database URL from environment variable or default
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://aitbc_edge:password@localhost:5432/aitbc_edge")
# Create async engine with proper connection pool settings
engine = create_async_engine(
DATABASE_URL,
echo=False,
pool_size=10,
max_overflow=20,
pool_pre_ping=True, # Verify connections before using
pool_recycle=3600, # Recycle connections after 1 hour
)
async def init_db() -> None:
"""Initialize database tables"""
from .schemas.island import IslandMembership, BridgeRequest
from .schemas.gpu import GPUListing
from .schemas.database import EdgeDatabase
from .schemas.serve import ComputeRequest, ComputeResult
from .schemas.metrics import EdgeMetrics
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
logger.info("Edge API database initialized")
@asynccontextmanager
async def get_session() -> AsyncIterator[AsyncSession]:
"""Get database session"""
async with AsyncSession(engine) as session:
yield session