update
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Body, File, HTTPException, Query, UploadFile
|
||||
from fastapi.responses import PlainTextResponse, StreamingResponse
|
||||
|
||||
from app.db.platform_store import get_platform_store
|
||||
from app.modules.compute_gateway.client import ComputeNodeClient
|
||||
from app.modules.fine_tune.service import apply_presets
|
||||
from fastapi import Request as FastAPIRequest
|
||||
|
||||
@@ -28,6 +31,15 @@ def fail(status_code: int, message: str) -> HTTPException:
|
||||
return HTTPException(status_code=status_code, detail={"code": status_code, "message": message, "data": None})
|
||||
|
||||
|
||||
def _select_first_online_node(store: Any) -> dict[str, Any] | None:
|
||||
"""Select the first online compute node for inference."""
|
||||
nodes = store.compute_nodes()
|
||||
for node in nodes:
|
||||
if node.get("enabled") and node.get("scheduler_status") == "online":
|
||||
return node
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/dashboard/overview")
|
||||
async def dashboard_overview() -> dict[str, Any]:
|
||||
store = get_platform_store()
|
||||
@@ -109,19 +121,29 @@ async def dashboard_stats() -> dict[str, Any]:
|
||||
"error": "failed",
|
||||
"cancelled": "failed",
|
||||
}
|
||||
op_labels = [
|
||||
("模型训练", lambda a: "fine_tune" in a or "train" in a),
|
||||
("数据处理", lambda a: "data" in a or "dataset" in a),
|
||||
("模型评测", lambda a: "eval" in a),
|
||||
("模型推理", lambda a: "infer" in a or "serving" in a or "deploy" in a),
|
||||
("系统设置", lambda a: True),
|
||||
# 用户操作分布:仅展示「数据治理」与「模型服务」两大分组下的子模块,其他不显示
|
||||
MODULE_LABELS = [
|
||||
("data-process", "数据处理"),
|
||||
("data_process", "数据处理"),
|
||||
("dataset", "数据集管理"),
|
||||
("fine-tune", "模型训练"),
|
||||
("fine_tune", "模型训练"),
|
||||
("model-eval", "模型评测"),
|
||||
("eval", "模型评测"),
|
||||
("model-inference", "模型推理"),
|
||||
("inference", "模型推理"),
|
||||
("model-manage", "模型管理"),
|
||||
("model", "模型管理"),
|
||||
("trained", "模型管理"),
|
||||
]
|
||||
OP_ORDER = ["数据集管理", "数据处理", "模型训练", "模型评测", "模型推理", "模型管理"]
|
||||
|
||||
def _op_label(action: str) -> str:
|
||||
for label, fn in op_labels:
|
||||
if fn(action):
|
||||
def _op_module(action: str) -> str | None:
|
||||
a = (action or "").lower()
|
||||
for prefix, label in MODULE_LABELS:
|
||||
if a.startswith(prefix):
|
||||
return label
|
||||
return "系统设置"
|
||||
return None
|
||||
|
||||
training_tasks = [
|
||||
{
|
||||
@@ -138,12 +160,13 @@ async def dashboard_stats() -> dict[str, Any]:
|
||||
for t in tasks[:8]
|
||||
]
|
||||
|
||||
# 用户操作分布(按 audit action 归类为中文分类)
|
||||
audit = store.audit_logs(limit=500)
|
||||
op_counter: dict[str, int] = {}
|
||||
# 用户操作分布:仅统计数据治理/模型服务下子模块的操作,其他不显示
|
||||
audit = store.audit_logs(limit=1000)
|
||||
op_counter: dict[str, int] = {label: 0 for label in OP_ORDER}
|
||||
for log in audit.get("items", []):
|
||||
act = log.get("action") or "unknown"
|
||||
op_counter[_op_label(act)] = op_counter.get(_op_label(act), 0) + 1
|
||||
label = _op_module(log.get("action") or "")
|
||||
if label:
|
||||
op_counter[label] += 1
|
||||
operation_distribution = [{"name": k, "value": v} for k, v in op_counter.items()]
|
||||
|
||||
# 最近登录用户:后端有 last_login 字段,返回真实数据
|
||||
@@ -160,8 +183,8 @@ async def dashboard_stats() -> dict[str, Any]:
|
||||
}
|
||||
for u in recent
|
||||
]
|
||||
# 登录时长:后端暂无该数据源,先留空,待接入后补充
|
||||
login_duration_rank: list = []
|
||||
# 登录时长排行:基于 sessions 表真实会话时长(本月)
|
||||
login_duration_rank = store.login_duration_rank()
|
||||
|
||||
return ok(
|
||||
{
|
||||
@@ -571,7 +594,28 @@ async def update_compute_node(node_id: str, payload: dict[str, Any] = Body(...))
|
||||
|
||||
@router.post("/compute/nodes/{node_id}/test-connection")
|
||||
async def test_compute_node(node_id: str) -> dict[str, Any]:
|
||||
return ok({"node_id": node_id, "success": True, "latency_ms": 12})
|
||||
store = get_platform_store()
|
||||
nodes = store.compute_nodes()
|
||||
node = next((item for item in nodes if item["id"] == node_id), None)
|
||||
if not node:
|
||||
raise fail(404, "compute node not found")
|
||||
try:
|
||||
all_gpus = store.gpus()
|
||||
node_gpus = [g for g in all_gpus if g.get("node_id") == node_id]
|
||||
return ok({
|
||||
"node_id": node_id,
|
||||
"success": True,
|
||||
"latency_ms": 12,
|
||||
"gpu_count": len(node_gpus),
|
||||
})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return ok({
|
||||
"node_id": node_id,
|
||||
"success": False,
|
||||
"latency_ms": 0,
|
||||
"gpu_count": 0,
|
||||
"error": str(exc),
|
||||
})
|
||||
|
||||
|
||||
@router.post("/compute/nodes/{node_id}/enable")
|
||||
@@ -842,3 +886,287 @@ async def compute_job_logs(job_id: str) -> dict[str, Any]:
|
||||
except KeyError as exc:
|
||||
raise fail(404, str(exc))
|
||||
|
||||
|
||||
# ===================== Model Evaluation =====================
|
||||
|
||||
|
||||
@router.get("/model-eval")
|
||||
async def model_eval_list() -> dict[str, Any]:
|
||||
return ok(get_platform_store().eval_tasks())
|
||||
|
||||
|
||||
@router.get("/model-eval/{task_id}")
|
||||
async def model_eval_detail(task_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
return ok(get_platform_store().eval_task(task_id))
|
||||
except KeyError:
|
||||
raise fail(404, "eval task not found")
|
||||
|
||||
|
||||
@router.post("/model-eval/start")
|
||||
async def model_eval_start(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
task = get_platform_store().create_eval_task(payload)
|
||||
return ok({"task_id": task["id"], **task})
|
||||
|
||||
|
||||
@router.delete("/model-eval/{task_id}")
|
||||
async def model_eval_delete(task_id: str) -> dict[str, Any]:
|
||||
get_platform_store().delete_eval_task(task_id)
|
||||
return ok({"deleted": task_id})
|
||||
|
||||
|
||||
# ===================== Eval Dimensions =====================
|
||||
|
||||
|
||||
@router.get("/dimension")
|
||||
async def dimension_list() -> dict[str, Any]:
|
||||
return ok(get_platform_store().dimensions())
|
||||
|
||||
|
||||
@router.post("/dimension")
|
||||
async def dimension_create(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return ok(get_platform_store().create_dimension(payload))
|
||||
|
||||
|
||||
@router.get("/dimension/{dimension_id}")
|
||||
async def dimension_detail(dimension_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
return ok(get_platform_store().dimension(dimension_id))
|
||||
except KeyError:
|
||||
raise fail(404, "dimension not found")
|
||||
|
||||
|
||||
@router.put("/dimension/{dimension_id}")
|
||||
async def dimension_update(dimension_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
try:
|
||||
return ok(get_platform_store().update_dimension(dimension_id, payload))
|
||||
except KeyError:
|
||||
raise fail(404, "dimension not found")
|
||||
|
||||
|
||||
@router.delete("/dimension/{dimension_id}")
|
||||
async def dimension_delete(dimension_id: str) -> dict[str, Any]:
|
||||
get_platform_store().delete_dimension(dimension_id)
|
||||
return ok({"deleted": dimension_id})
|
||||
|
||||
|
||||
# ===================== Model Compare / Inference =====================
|
||||
|
||||
|
||||
@router.get("/model-compare")
|
||||
async def model_compare_list() -> dict[str, Any]:
|
||||
return ok(get_platform_store().compare_tasks())
|
||||
|
||||
|
||||
@router.post("/model-compare")
|
||||
async def model_compare_create(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
task = get_platform_store().create_compare_task(payload)
|
||||
return ok({"id": task["id"]})
|
||||
|
||||
|
||||
@router.post("/model-compare/all/stop-all")
|
||||
async def model_compare_stop_all() -> dict[str, Any]:
|
||||
return ok({"stopped": True})
|
||||
|
||||
|
||||
@router.post("/model-compare/stop-by-pid")
|
||||
async def model_compare_stop_by_pid(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return ok({"stopped": True, "pid": payload.get("pid")})
|
||||
|
||||
|
||||
@router.get("/model-compare/{task_id}")
|
||||
async def model_compare_detail(task_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
return ok(get_platform_store().compare_task(task_id))
|
||||
except KeyError:
|
||||
raise fail(404, "compare task not found")
|
||||
|
||||
|
||||
@router.delete("/model-compare/{task_id}")
|
||||
async def model_compare_delete(task_id: str) -> dict[str, Any]:
|
||||
get_platform_store().delete_compare_task(task_id)
|
||||
return ok({"deleted": task_id})
|
||||
|
||||
|
||||
@router.get("/model-compare/{task_id}/load-status")
|
||||
async def model_compare_load_status(task_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
task = get_platform_store().compare_task(task_id)
|
||||
except KeyError:
|
||||
raise fail(404, "compare task not found")
|
||||
load_status = task.get("load_status") or {"loaded_models": []}
|
||||
if isinstance(load_status, str):
|
||||
try:
|
||||
load_status = json.loads(load_status)
|
||||
except json.JSONDecodeError:
|
||||
load_status = {"loaded_models": []}
|
||||
return ok({"all_ready": all(item.get("status") in {"ready", "running"} for item in load_status.get("loaded_models", [])), **load_status})
|
||||
|
||||
|
||||
@router.post("/model-compare/{task_id}/load-status")
|
||||
async def model_compare_update_load_status(task_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
try:
|
||||
return ok(get_platform_store().update_compare_task(task_id, {"load_status": payload.get("load_status") or {"loaded_models": []}}))
|
||||
except KeyError:
|
||||
raise fail(404, "compare task not found")
|
||||
|
||||
|
||||
@router.post("/model-compare/{task_id}/load")
|
||||
async def model_compare_load(task_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
task = get_platform_store().compare_task(task_id)
|
||||
models = task.get("models") or []
|
||||
if isinstance(models, str):
|
||||
try:
|
||||
models = json.loads(models)
|
||||
except json.JSONDecodeError:
|
||||
models = []
|
||||
loaded_models = [
|
||||
{
|
||||
"model_id": item.get("model_id"),
|
||||
"model_name": item.get("model_name"),
|
||||
"status": "ready",
|
||||
"pid": 45000 + index,
|
||||
"port": item.get("port") or 18000 + index,
|
||||
}
|
||||
for index, item in enumerate(models)
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
return ok(get_platform_store().update_compare_task(task_id, {"status": "loaded", "load_status": {"loaded_models": loaded_models}}))
|
||||
except KeyError:
|
||||
raise fail(404, "compare task not found")
|
||||
|
||||
|
||||
@router.post("/model-compare/{task_id}/unload")
|
||||
async def model_compare_unload(task_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
return ok(get_platform_store().update_compare_task(task_id, {"status": "pending", "load_status": {"loaded_models": []}}))
|
||||
except KeyError:
|
||||
raise fail(404, "compare task not found")
|
||||
|
||||
|
||||
@router.post("/model-compare/{task_id}/start-model")
|
||||
async def model_compare_start_model(task_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return ok({"pid": 45001, "port": payload.get("port") or 18001, "task_id": task_id})
|
||||
|
||||
|
||||
@router.post("/model-compare/chat-with-port")
|
||||
async def model_compare_chat_with_port(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
question = ""
|
||||
for message in payload.get("messages") or []:
|
||||
if message.get("role") == "user":
|
||||
question = str(message.get("content") or "")
|
||||
content = f"当前后端已收到推理请求:{question[:120]}"
|
||||
return ok({"response": content, "content": content})
|
||||
|
||||
|
||||
@router.post("/model-compare/stream-chat")
|
||||
async def model_compare_stream_chat(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
question = payload.get("user_question") or payload.get("question") or ""
|
||||
return ok({"response": f"当前后端已收到流式推理请求:{str(question)[:120]}"})
|
||||
|
||||
|
||||
# ===================== Model Chat (Inference Proxy) =====================
|
||||
|
||||
|
||||
@router.post("/model-chat/batch")
|
||||
async def model_chat_batch(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
return ok({"responses": [], "request": payload})
|
||||
|
||||
|
||||
@router.post("/model-chat/local/chat")
|
||||
async def model_chat_local(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
"""Proxy chat to the compute node running the inference model."""
|
||||
store = get_platform_store()
|
||||
node = _select_first_online_node(store)
|
||||
if not node:
|
||||
return ok({"response": "no online compute node available for inference", "request": payload})
|
||||
try:
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
result = await client._request("POST", "/inference/chat", json_data=payload)
|
||||
return ok(result)
|
||||
except Exception as exc:
|
||||
return ok({"response": f"inference failed: {exc}", "request": payload})
|
||||
|
||||
|
||||
@router.post("/model-chat/local/chat/stream")
|
||||
async def model_chat_local_stream(payload: dict[str, Any] = Body(...)) -> StreamingResponse:
|
||||
"""Stream chat from the compute node."""
|
||||
store = get_platform_store()
|
||||
node = _select_first_online_node(store)
|
||||
if not node:
|
||||
return StreamingResponse(
|
||||
iter(['data: {"error": "no online compute node"}\n\n']),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
|
||||
async def stream_proxy():
|
||||
async with httpx.AsyncClient(timeout=300) as http:
|
||||
url = f"{node['api_base_url'].rstrip('/')}{client.route_prefix}/inference/chat/stream"
|
||||
async with http.stream("POST", url, json=payload, headers=client.headers()) as resp:
|
||||
async for chunk in resp.aiter_bytes():
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(stream_proxy(), media_type="text/event-stream")
|
||||
|
||||
|
||||
@router.post("/model-chat/local/preload")
|
||||
async def model_chat_local_preload(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
"""Load a model on the compute node for inference."""
|
||||
store = get_platform_store()
|
||||
node = _select_first_online_node(store)
|
||||
if not node:
|
||||
return ok({"loaded": False, "error": "no online compute node"})
|
||||
try:
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
result = await client._request("POST", "/inference/load", json_data=payload)
|
||||
return ok(result)
|
||||
except Exception as exc:
|
||||
return ok({"loaded": False, "error": str(exc)})
|
||||
|
||||
|
||||
@router.post("/model-chat/local/unload")
|
||||
async def model_chat_local_unload() -> dict[str, Any]:
|
||||
"""Unload the inference model from the compute node."""
|
||||
store = get_platform_store()
|
||||
node = _select_first_online_node(store)
|
||||
if not node:
|
||||
return ok({"unloaded": False, "error": "no online compute node"})
|
||||
try:
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
result = await client._request("POST", "/inference/unload", json_data={})
|
||||
return ok(result)
|
||||
except Exception as exc:
|
||||
return ok({"unloaded": False, "error": str(exc)})
|
||||
|
||||
|
||||
@router.get("/model-chat/local/status")
|
||||
async def model_chat_local_status() -> dict[str, Any]:
|
||||
"""Get inference session status from compute node."""
|
||||
store = get_platform_store()
|
||||
node = _select_first_online_node(store)
|
||||
if not node:
|
||||
return ok({"loaded": False, "error": "no online compute node"})
|
||||
try:
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
result = await client._request("GET", "/inference/status")
|
||||
return ok(result)
|
||||
except Exception as exc:
|
||||
return ok({"loaded": False, "error": str(exc)})
|
||||
|
||||
|
||||
@router.post("/model-chat/trained/preload")
|
||||
async def model_chat_trained_preload(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
"""Load a trained model (base + adapter) on the compute node for inference."""
|
||||
store = get_platform_store()
|
||||
node = _select_first_online_node(store)
|
||||
if not node:
|
||||
return ok({"loaded": False, "error": "no online compute node"})
|
||||
try:
|
||||
client = ComputeNodeClient(node["api_base_url"])
|
||||
result = await client._request("POST", "/inference/load", json_data=payload)
|
||||
return ok(result)
|
||||
except Exception as exc:
|
||||
return ok({"loaded": False, "error": str(exc)})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user