feat: Add new API endpoints and HTML pages for ML model management
- Implemented HTML pages for datasets, models, training, testing, and results. - Created API endpoints for managing repositories, results, tests, and training sessions. - Added functionality for streaming training progress via Server-Sent Events (SSE). - Introduced a Dockerfile for the ML runner with necessary dependencies. - Developed an SDK for user code execution within the runner container. - Enhanced CSS styles for improved UI layout and navigation. - Established a layout template for consistent HTML structure across pages. - Added JavaScript for dynamic interactions on the models page. - Implemented WebSocket handling for real-time communication with kiosk devices and controllers. - Implemented model registration and management API at /api/models - Added Gitea proxy API for repository interactions at /api/repos - Created results API for listing and comparing training results at /api/results - Developed training management API for enqueueing and retrieving training jobs at /api/trainings - Introduced SSE endpoint for live training progress updates - Added HTML pages for models, datasets, and training management - Created a Dockerfile for the ML runner with necessary dependencies - Developed SDK for user code execution within the runner container - Enhanced CSS styles for improved UI/UX - Implemented WebSocket communication for real-time device and controller interactions in the kiosk system
This commit is contained in:
131
ml/routers/models.py
Normal file
131
ml/routers/models.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""API /api/models — registro modelli (repo Gitea + metadata)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from core import db
|
||||
from core.auth import require_auth
|
||||
from core.model_spec import fetch_and_parse_spec
|
||||
|
||||
router = APIRouter(prefix="/api/models", tags=["models"])
|
||||
|
||||
|
||||
def _row(r) -> Optional[dict]:
|
||||
if r is None:
|
||||
return None
|
||||
d = dict(r)
|
||||
for k in ("created_at", "updated_at"):
|
||||
if d.get(k) is not None and hasattr(d[k], "isoformat"):
|
||||
d[k] = d[k].isoformat()
|
||||
return d
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_models(user=Depends(require_auth)):
|
||||
rows = await db.fetch("SELECT * FROM models ORDER BY created_at DESC LIMIT 500")
|
||||
return {"count": len(rows), "models": [_row(r) for r in rows]}
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_model(body: dict, user=Depends(require_auth)):
|
||||
required = ("name", "type", "gitea_repo")
|
||||
for k in required:
|
||||
if not body.get(k):
|
||||
raise HTTPException(400, f"missing field: {k}")
|
||||
|
||||
# prova a pre-caricare model.yml dal default branch (non fatale)
|
||||
spec = None
|
||||
try:
|
||||
spec = await fetch_and_parse_spec(body["gitea_repo"], body.get("default_branch") or "main")
|
||||
except Exception:
|
||||
spec = None
|
||||
|
||||
row = await db.fetchrow(
|
||||
"""
|
||||
INSERT INTO models (name, type, gitea_repo, default_branch, spec, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)
|
||||
RETURNING *
|
||||
""",
|
||||
body["name"],
|
||||
body["type"],
|
||||
body["gitea_repo"],
|
||||
body.get("default_branch") or "main",
|
||||
spec,
|
||||
user.get("username") or "unknown",
|
||||
)
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/{model_id}")
|
||||
async def get_model(model_id: str, user=Depends(require_auth)):
|
||||
row = await db.fetchrow("SELECT * FROM models WHERE id = $1", uuid.UUID(model_id))
|
||||
if not row:
|
||||
raise HTTPException(404, "not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.patch("/{model_id}")
|
||||
async def patch_model(model_id: str, body: dict, user=Depends(require_auth)):
|
||||
allowed = {"name", "type", "default_branch"}
|
||||
sets = []
|
||||
args: list = []
|
||||
for k, v in body.items():
|
||||
if k in allowed:
|
||||
args.append(v)
|
||||
sets.append(f"{k} = ${len(args)}")
|
||||
if not sets:
|
||||
raise HTTPException(400, "no fields to update")
|
||||
args.append(uuid.UUID(model_id))
|
||||
row = await db.fetchrow(
|
||||
f"UPDATE models SET {', '.join(sets)} WHERE id = ${len(args)} RETURNING *",
|
||||
*args,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(404, "not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.delete("/{model_id}", status_code=204)
|
||||
async def delete_model(model_id: str, user=Depends(require_auth)):
|
||||
await db.execute("DELETE FROM models WHERE id = $1", uuid.UUID(model_id))
|
||||
return None
|
||||
|
||||
|
||||
# ── Notes ──────────────────────────────────────────────────────────────────
|
||||
@router.get("/{model_id}/notes")
|
||||
async def list_notes(model_id: str, user=Depends(require_auth)):
|
||||
rows = await db.fetch(
|
||||
"SELECT id, author, text, created_at FROM model_notes WHERE model_id = $1 ORDER BY created_at DESC",
|
||||
uuid.UUID(model_id),
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(r["id"]),
|
||||
"author": r["author"],
|
||||
"text": r["text"],
|
||||
"created_at": r["created_at"].isoformat(),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{model_id}/notes", status_code=201)
|
||||
async def add_note(model_id: str, body: dict, user=Depends(require_auth)):
|
||||
text = (body.get("text") or "").strip()
|
||||
if not text:
|
||||
raise HTTPException(400, "text required")
|
||||
row = await db.fetchrow(
|
||||
"INSERT INTO model_notes (model_id, author, text) VALUES ($1, $2, $3) RETURNING *",
|
||||
uuid.UUID(model_id),
|
||||
user.get("username") or "unknown",
|
||||
text,
|
||||
)
|
||||
return {
|
||||
"id": str(row["id"]),
|
||||
"author": row["author"],
|
||||
"text": row["text"],
|
||||
"created_at": row["created_at"].isoformat(),
|
||||
}
|
||||
Reference in New Issue
Block a user