Files
aitbc/apps/pool-hub/src/app/routers/jobs.py
oib 329b3beeba ```
feat: add SQLModel relationships, fix ZK verifier circuit integration, and complete Stage 19-20 documentation

- Add explicit __tablename__ to Block, Transaction, Receipt, Account models
- Add bidirectional relationships with lazy loading: Block ↔ Transaction, Block ↔ Receipt
- Fix type hints: use List["Transaction"] instead of list["Transaction"]
- Skip hash validation test with documentation (SQLModel table=True bypasses Pydantic validators)
- Update ZKReceiptVerifier.sol to match receipt_simple circuit (
2026-01-24 18:34:37 +01:00

185 lines
4.7 KiB
Python

"""Job distribution routes for Pool Hub"""
from fastapi import APIRouter, HTTPException, Depends, Query
from typing import List, Optional
from datetime import datetime
from pydantic import BaseModel
from ..registry import MinerRegistry
from ..scoring import ScoringEngine
router = APIRouter(prefix="/jobs", tags=["jobs"])
class JobRequest(BaseModel):
"""Job request from coordinator"""
job_id: str
prompt: str
model: str
params: dict = {}
priority: int = 0
deadline: Optional[datetime] = None
reward: float = 0.0
class JobAssignment(BaseModel):
"""Job assignment response"""
job_id: str
miner_id: str
pool_id: str
assigned_at: datetime
deadline: Optional[datetime]
class JobResult(BaseModel):
"""Job result from miner"""
job_id: str
miner_id: str
status: str # completed, failed
result: Optional[str] = None
error: Optional[str] = None
metrics: dict = {}
def get_registry() -> MinerRegistry:
return MinerRegistry()
def get_scoring() -> ScoringEngine:
return ScoringEngine()
@router.post("/assign", response_model=JobAssignment)
async def assign_job(
job: JobRequest,
registry: MinerRegistry = Depends(get_registry),
scoring: ScoringEngine = Depends(get_scoring)
):
"""Assign a job to the best available miner."""
# Find available miners with required capability
available = await registry.list(
status="available",
capability=job.model,
limit=100
)
if not available:
raise HTTPException(
status_code=503,
detail="No miners available for this model"
)
# Score and rank miners
scored = await scoring.rank_miners(available, job)
# Select best miner
best_miner = scored[0]
# Assign job
assignment = await registry.assign_job(
job_id=job.job_id,
miner_id=best_miner.miner_id,
deadline=job.deadline
)
return JobAssignment(
job_id=job.job_id,
miner_id=best_miner.miner_id,
pool_id=best_miner.pool_id,
assigned_at=datetime.utcnow(),
deadline=job.deadline
)
@router.post("/result")
async def submit_result(
result: JobResult,
registry: MinerRegistry = Depends(get_registry),
scoring: ScoringEngine = Depends(get_scoring)
):
"""Submit job result and update miner stats."""
miner = await registry.get(result.miner_id)
if not miner:
raise HTTPException(status_code=404, detail="Miner not found")
# Update job status
await registry.complete_job(
job_id=result.job_id,
miner_id=result.miner_id,
status=result.status,
metrics=result.metrics
)
# Update miner score based on result
if result.status == "completed":
await scoring.record_success(result.miner_id, result.metrics)
else:
await scoring.record_failure(result.miner_id, result.error)
return {"status": "recorded"}
@router.get("/pending")
async def get_pending_jobs(
pool_id: Optional[str] = Query(None),
limit: int = Query(50, le=100),
registry: MinerRegistry = Depends(get_registry)
):
"""Get pending jobs waiting for assignment."""
return await registry.get_pending_jobs(pool_id=pool_id, limit=limit)
@router.get("/{job_id}")
async def get_job_status(
job_id: str,
registry: MinerRegistry = Depends(get_registry)
):
"""Get job assignment status."""
job = await registry.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
return job
@router.post("/{job_id}/reassign")
async def reassign_job(
job_id: str,
registry: MinerRegistry = Depends(get_registry),
scoring: ScoringEngine = Depends(get_scoring)
):
"""Reassign a failed or timed-out job to another miner."""
job = await registry.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if job.status not in ["failed", "timeout"]:
raise HTTPException(
status_code=400,
detail="Can only reassign failed or timed-out jobs"
)
# Find new miner (exclude previous)
available = await registry.list(
status="available",
capability=job.model,
exclude_miner=job.miner_id,
limit=100
)
if not available:
raise HTTPException(
status_code=503,
detail="No alternative miners available"
)
scored = await scoring.rank_miners(available, job)
new_miner = scored[0]
await registry.reassign_job(job_id, new_miner.miner_id)
return {
"job_id": job_id,
"new_miner_id": new_miner.miner_id,
"status": "reassigned"
}