Spaces:
Running
Running
yangzhitao
commited on
Commit
·
0b237ab
1
Parent(s):
b4929ca
feat: add backend health status indicator and update functionality
Browse files- Implemented a backend health status indicator in the Gradio interface.
- Added JavaScript to automatically update the backend status every 30 seconds.
- Enhanced CSS for visual representation of backend health states (undefined, healthy, unhealthy).
- Updated app.py to include health check logic and integrate the new status indicator.
- Introduced a new backend_status.js file for managing status updates.
- app.py +88 -6
- src/assets/css/custom.css +42 -0
- src/assets/js/backend_status.js +26 -0
- src/backend/routes/hf.py +34 -3
- src/backend/schemas.py +52 -1
- src/display/css_html_js.py +1 -0
- src/leaderboard/read_evals.py +2 -1
app.py
CHANGED
|
@@ -3,6 +3,7 @@ import threading
|
|
| 3 |
import gradio as gr
|
| 4 |
import gradio.components as grc
|
| 5 |
import pandas as pd
|
|
|
|
| 6 |
import uvicorn
|
| 7 |
from apscheduler.schedulers.background import BackgroundScheduler
|
| 8 |
from huggingface_hub import snapshot_download
|
|
@@ -18,7 +19,7 @@ from src.about import (
|
|
| 18 |
TITLE,
|
| 19 |
)
|
| 20 |
from src.backend.app import create_app
|
| 21 |
-
from src.display.css_html_js import custom_css
|
| 22 |
from src.display.utils import (
|
| 23 |
BASE_COLS,
|
| 24 |
BENCHMARK_COLS,
|
|
@@ -39,6 +40,58 @@ def restart_space():
|
|
| 39 |
API.restart_space(repo_id=settings.REPO_ID)
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
print("///// --- Settings --- /////", settings.model_dump())
|
| 43 |
|
| 44 |
# Space initialisation
|
|
@@ -282,6 +335,35 @@ with demo:
|
|
| 282 |
gr.Markdown(LLM_BENCHMARKS_TEXT, elem_classes="markdown-text")
|
| 283 |
|
| 284 |
with gr.TabItem("🚀 Submit here! ", elem_id="submit-tab", id=len(BENCHMARKS) + 1):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
with gr.Column():
|
| 286 |
with gr.Row():
|
| 287 |
gr.Markdown(EVALUATION_QUEUE_TEXT, elem_classes="markdown-text")
|
|
@@ -413,13 +495,13 @@ if __name__ == "__main__":
|
|
| 413 |
# Backend server - 在单独的线程中运行
|
| 414 |
app = create_app()
|
| 415 |
|
| 416 |
-
def run_fastapi():
|
| 417 |
-
print("Starting FastAPI server on http://
|
| 418 |
uvicorn.run(
|
| 419 |
app,
|
| 420 |
-
host=
|
| 421 |
-
port=
|
| 422 |
-
log_level="
|
| 423 |
access_log=True,
|
| 424 |
)
|
| 425 |
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
import gradio.components as grc
|
| 5 |
import pandas as pd
|
| 6 |
+
import requests
|
| 7 |
import uvicorn
|
| 8 |
from apscheduler.schedulers.background import BackgroundScheduler
|
| 9 |
from huggingface_hub import snapshot_download
|
|
|
|
| 19 |
TITLE,
|
| 20 |
)
|
| 21 |
from src.backend.app import create_app
|
| 22 |
+
from src.display.css_html_js import backend_status_js, custom_css
|
| 23 |
from src.display.utils import (
|
| 24 |
BASE_COLS,
|
| 25 |
BENCHMARK_COLS,
|
|
|
|
| 40 |
API.restart_space(repo_id=settings.REPO_ID)
|
| 41 |
|
| 42 |
|
| 43 |
+
def get_backend_status_undefined_html() -> str:
|
| 44 |
+
"""
|
| 45 |
+
返回未定义状态(首次检查前)的 HTML
|
| 46 |
+
"""
|
| 47 |
+
return """
|
| 48 |
+
<div id="backend-status-indicator">
|
| 49 |
+
<span class="backend-status-light undefined"></span>
|
| 50 |
+
<span>Backend Status: Checking...</span>
|
| 51 |
+
</div>
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def check_backend_health() -> tuple[bool, str]:
|
| 56 |
+
"""
|
| 57 |
+
查询后端健康状态
|
| 58 |
+
返回: (is_healthy, status_html)
|
| 59 |
+
"""
|
| 60 |
+
try:
|
| 61 |
+
response = requests.get("http://localhost:8000/api/v1/health/", timeout=2)
|
| 62 |
+
if response.status_code == 200:
|
| 63 |
+
data = response.json()
|
| 64 |
+
if data.get("code") == 0:
|
| 65 |
+
return (
|
| 66 |
+
True,
|
| 67 |
+
"""
|
| 68 |
+
<div id="backend-status-indicator">
|
| 69 |
+
<span class="backend-status-light healthy"></span>
|
| 70 |
+
<span>Backend Status: Healthy</span>
|
| 71 |
+
</div>
|
| 72 |
+
""",
|
| 73 |
+
)
|
| 74 |
+
return (
|
| 75 |
+
False,
|
| 76 |
+
"""
|
| 77 |
+
<div id="backend-status-indicator">
|
| 78 |
+
<span class="backend-status-light unhealthy"></span>
|
| 79 |
+
<span>Backend Status: Unhealthy</span>
|
| 80 |
+
</div>
|
| 81 |
+
""",
|
| 82 |
+
)
|
| 83 |
+
except Exception:
|
| 84 |
+
return (
|
| 85 |
+
False,
|
| 86 |
+
"""
|
| 87 |
+
<div id="backend-status-indicator">
|
| 88 |
+
<span class="backend-status-light unhealthy"></span>
|
| 89 |
+
<span>Backend Status: Unavailable</span>
|
| 90 |
+
</div>
|
| 91 |
+
""",
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
print("///// --- Settings --- /////", settings.model_dump())
|
| 96 |
|
| 97 |
# Space initialisation
|
|
|
|
| 335 |
gr.Markdown(LLM_BENCHMARKS_TEXT, elem_classes="markdown-text")
|
| 336 |
|
| 337 |
with gr.TabItem("🚀 Submit here! ", elem_id="submit-tab", id=len(BENCHMARKS) + 1):
|
| 338 |
+
# Backend status indicator - 初始状态为 undefined
|
| 339 |
+
backend_status = gr.HTML(value=get_backend_status_undefined_html(), elem_id="backend-status-container")
|
| 340 |
+
|
| 341 |
+
# 定时更新后端状态
|
| 342 |
+
def update_backend_status():
|
| 343 |
+
return check_backend_health()[1]
|
| 344 |
+
|
| 345 |
+
# 创建一个隐藏的按钮用于定时触发更新
|
| 346 |
+
status_trigger = gr.Button(visible=False, elem_id="backend-status-trigger-btn")
|
| 347 |
+
|
| 348 |
+
# 绑定按钮点击事件来更新状态
|
| 349 |
+
status_trigger.click(
|
| 350 |
+
fn=update_backend_status,
|
| 351 |
+
inputs=None,
|
| 352 |
+
outputs=backend_status,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
# 加载外部 JavaScript 文件
|
| 356 |
+
js_content = backend_status_js.read_text(encoding="utf-8")
|
| 357 |
+
status_trigger_js_html = f'<script>{js_content}</script>'
|
| 358 |
+
gr.HTML(status_trigger_js_html)
|
| 359 |
+
|
| 360 |
+
# 页面加载时立即检查一次
|
| 361 |
+
demo.load(
|
| 362 |
+
fn=update_backend_status,
|
| 363 |
+
inputs=None,
|
| 364 |
+
outputs=backend_status,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
with gr.Column():
|
| 368 |
with gr.Row():
|
| 369 |
gr.Markdown(EVALUATION_QUEUE_TEXT, elem_classes="markdown-text")
|
|
|
|
| 495 |
# Backend server - 在单独的线程中运行
|
| 496 |
app = create_app()
|
| 497 |
|
| 498 |
+
def run_fastapi(host: str = "127.0.0.1", port: int = 8000):
|
| 499 |
+
print(f"Starting FastAPI server on http://{host}:{port}")
|
| 500 |
uvicorn.run(
|
| 501 |
app,
|
| 502 |
+
host=host,
|
| 503 |
+
port=port,
|
| 504 |
+
log_level="debug",
|
| 505 |
access_log=True,
|
| 506 |
)
|
| 507 |
|
src/assets/css/custom.css
CHANGED
|
@@ -105,3 +105,45 @@
|
|
| 105 |
overflow: hidden !important;
|
| 106 |
text-overflow: ellipsis !important;
|
| 107 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
overflow: hidden !important;
|
| 106 |
text-overflow: ellipsis !important;
|
| 107 |
}
|
| 108 |
+
|
| 109 |
+
/* Backend status indicator - breathing light animation */
|
| 110 |
+
#backend-status-indicator {
|
| 111 |
+
display: flex;
|
| 112 |
+
align-items: center;
|
| 113 |
+
gap: 10px;
|
| 114 |
+
padding: 10px;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.backend-status-light {
|
| 118 |
+
width: 16px;
|
| 119 |
+
height: 16px;
|
| 120 |
+
border-radius: 50%;
|
| 121 |
+
display: inline-block;
|
| 122 |
+
animation: breathing 2s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.backend-status-light.undefined {
|
| 126 |
+
background-color: #6b7280;
|
| 127 |
+
box-shadow: 0 0 10px rgba(107, 114, 128, 0.5);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.backend-status-light.healthy {
|
| 131 |
+
background-color: #10b981;
|
| 132 |
+
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.backend-status-light.unhealthy {
|
| 136 |
+
background-color: #ef4444;
|
| 137 |
+
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
@keyframes breathing {
|
| 141 |
+
0%, 100% {
|
| 142 |
+
opacity: 1;
|
| 143 |
+
transform: scale(1);
|
| 144 |
+
}
|
| 145 |
+
50% {
|
| 146 |
+
opacity: 0.6;
|
| 147 |
+
transform: scale(1.1);
|
| 148 |
+
}
|
| 149 |
+
}
|
src/assets/js/backend_status.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Backend status update script
|
| 3 |
+
* Automatically updates backend health status every 5 seconds
|
| 4 |
+
*/
|
| 5 |
+
(function() {
|
| 6 |
+
function startStatusUpdate() {
|
| 7 |
+
const trigger = document.getElementById('backend-status-trigger-btn');
|
| 8 |
+
if (trigger) {
|
| 9 |
+
// 立即执行一次
|
| 10 |
+
trigger.click();
|
| 11 |
+
// 然后每5秒执行一次
|
| 12 |
+
setInterval(function() {
|
| 13 |
+
trigger.click();
|
| 14 |
+
}, 30_000);
|
| 15 |
+
} else {
|
| 16 |
+
// 如果按钮还没加载,等待一下再试
|
| 17 |
+
setTimeout(startStatusUpdate, 1_500);
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
// 页面加载完成后开始
|
| 21 |
+
if (document.readyState === 'complete') {
|
| 22 |
+
startStatusUpdate();
|
| 23 |
+
} else {
|
| 24 |
+
window.addEventListener('load', startStatusUpdate);
|
| 25 |
+
}
|
| 26 |
+
})();
|
src/backend/routes/hf.py
CHANGED
|
@@ -1,17 +1,30 @@
|
|
| 1 |
import io
|
| 2 |
from dataclasses import asdict
|
|
|
|
| 3 |
|
| 4 |
-
from fastapi import APIRouter
|
| 5 |
from loguru import logger
|
| 6 |
|
| 7 |
from src.backend.config import settings
|
| 8 |
-
from src.backend.schemas import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
router = APIRouter(tags=["huggingface"])
|
| 11 |
|
| 12 |
|
| 13 |
@router.post("/community/submit/")
|
| 14 |
-
async def submit(params: Submit_Params) -> ResponseData[Submit_RespData]:
|
| 15 |
"""Submit a new evaluation request to the Hugging Face repository."""
|
| 16 |
file_obj = io.BytesIO(params.content.encode("utf-8"))
|
| 17 |
commit_info = settings.hf_api.upload_file(
|
|
@@ -45,3 +58,21 @@ async def submit(params: Submit_Params) -> ResponseData[Submit_RespData]:
|
|
| 45 |
msg=msg,
|
| 46 |
data=Submit_RespData.model_validate(data_dict),
|
| 47 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import io
|
| 2 |
from dataclasses import asdict
|
| 3 |
+
from typing import TYPE_CHECKING, Annotated
|
| 4 |
|
| 5 |
+
from fastapi import APIRouter, Depends
|
| 6 |
from loguru import logger
|
| 7 |
|
| 8 |
from src.backend.config import settings
|
| 9 |
+
from src.backend.schemas import (
|
| 10 |
+
CommitInfo,
|
| 11 |
+
GetModelInfo_QueryParams,
|
| 12 |
+
GetModelInfo_RespData,
|
| 13 |
+
HfRepoUrl,
|
| 14 |
+
ResponseData,
|
| 15 |
+
Submit_Params,
|
| 16 |
+
Submit_RespData,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
if TYPE_CHECKING:
|
| 20 |
+
from huggingface_hub import ModelInfo
|
| 21 |
+
|
| 22 |
|
| 23 |
router = APIRouter(tags=["huggingface"])
|
| 24 |
|
| 25 |
|
| 26 |
@router.post("/community/submit/")
|
| 27 |
+
async def submit(params: Annotated[Submit_Params, Depends()]) -> ResponseData[Submit_RespData]:
|
| 28 |
"""Submit a new evaluation request to the Hugging Face repository."""
|
| 29 |
file_obj = io.BytesIO(params.content.encode("utf-8"))
|
| 30 |
commit_info = settings.hf_api.upload_file(
|
|
|
|
| 58 |
msg=msg,
|
| 59 |
data=Submit_RespData.model_validate(data_dict),
|
| 60 |
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Get model info
|
| 64 |
+
@router.get("/models/info/")
|
| 65 |
+
async def get_model_info(
|
| 66 |
+
params: Annotated[GetModelInfo_QueryParams, Depends()],
|
| 67 |
+
) -> ResponseData[GetModelInfo_RespData]:
|
| 68 |
+
# Get model info
|
| 69 |
+
model: ModelInfo = settings.hf_api.model_info(params.model_id, revision=params.revision or None)
|
| 70 |
+
# Get model commit history
|
| 71 |
+
commit_infos = settings.hf_api.list_repo_commits(repo_id=params.model_id, repo_type="model")
|
| 72 |
+
commits = [CommitInfo.model_validate(asdict(c)) for c in commit_infos]
|
| 73 |
+
# Response data
|
| 74 |
+
data = GetModelInfo_RespData.model_validate({
|
| 75 |
+
**asdict(model),
|
| 76 |
+
"commits": commits or None,
|
| 77 |
+
})
|
| 78 |
+
return ResponseData(data=data)
|
src/backend/schemas.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
from
|
|
|
|
| 2 |
|
| 3 |
from pydantic import BaseModel, ConfigDict, Field
|
| 4 |
|
|
@@ -42,3 +43,53 @@ class HfRepoUrl(BaseModel):
|
|
| 42 |
repo_id: str
|
| 43 |
repo_type: Literal["model", "dataset", "space"]
|
| 44 |
url: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import Annotated, Any, Generic, Literal, TypeVar
|
| 3 |
|
| 4 |
from pydantic import BaseModel, ConfigDict, Field
|
| 5 |
|
|
|
|
| 43 |
repo_id: str
|
| 44 |
repo_type: Literal["model", "dataset", "space"]
|
| 45 |
url: str
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# --- Get Model Info ---
|
| 49 |
+
class GetModelInfo_QueryParams(BaseModel):
|
| 50 |
+
"""Parameters for model info query."""
|
| 51 |
+
|
| 52 |
+
model_id: str
|
| 53 |
+
revision: str | None = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class GetModelInfo_RespData(BaseModel):
|
| 57 |
+
model_config = ConfigDict(extra="allow")
|
| 58 |
+
|
| 59 |
+
id: str
|
| 60 |
+
author: str | None = None
|
| 61 |
+
downloads: int | None = None
|
| 62 |
+
likes: int | None = None
|
| 63 |
+
tags: list[str] | None = None
|
| 64 |
+
pipeline_tag: str | None = None
|
| 65 |
+
sha: str | None = None
|
| 66 |
+
created_at: datetime | None = None
|
| 67 |
+
last_modified: datetime | None = None
|
| 68 |
+
private: bool | None = None
|
| 69 |
+
disabled: bool | None = None
|
| 70 |
+
gated: bool | None = None
|
| 71 |
+
# ... others
|
| 72 |
+
safetensors: "SafetensorsField | None" = None
|
| 73 |
+
# commits
|
| 74 |
+
commits: list["CommitInfo"] | None = None
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class SafetensorsField(BaseModel):
|
| 78 |
+
model_config = ConfigDict(extra="allow")
|
| 79 |
+
|
| 80 |
+
parameters: dict[str, Any] | None = None
|
| 81 |
+
total: int | None = None
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class CommitInfo(BaseModel):
|
| 85 |
+
model_config = ConfigDict(extra="allow")
|
| 86 |
+
|
| 87 |
+
commit_id: str
|
| 88 |
+
|
| 89 |
+
authors: list[str]
|
| 90 |
+
created_at: datetime
|
| 91 |
+
title: str
|
| 92 |
+
message: str
|
| 93 |
+
|
| 94 |
+
formatted_title: str | None = None
|
| 95 |
+
formatted_message: str | None = None
|
src/display/css_html_js.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
custom_css = Path("src/assets/css/custom.css")
|
|
|
|
| 4 |
|
| 5 |
# FIXME: seems deprecated
|
| 6 |
get_window_url_params = """
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
custom_css = Path("src/assets/css/custom.css")
|
| 4 |
+
backend_status_js = Path("src/assets/js/backend_status.js")
|
| 5 |
|
| 6 |
# FIXME: seems deprecated
|
| 7 |
get_window_url_params = """
|
src/leaderboard/read_evals.py
CHANGED
|
@@ -62,7 +62,8 @@ class EvalResult(BaseModel):
|
|
| 62 |
@classmethod
|
| 63 |
def init_from_json_file(cls, json_filepath: str) -> Self:
|
| 64 |
"""Inits the result from the specific model result file"""
|
| 65 |
-
|
|
|
|
| 66 |
config = data.config
|
| 67 |
|
| 68 |
# Precision
|
|
|
|
| 62 |
@classmethod
|
| 63 |
def init_from_json_file(cls, json_filepath: str) -> Self:
|
| 64 |
"""Inits the result from the specific model result file"""
|
| 65 |
+
json_content = Path(json_filepath).read_text(encoding="utf-8")
|
| 66 |
+
data = EvalResultJson.model_validate_json(json_content)
|
| 67 |
config = data.config
|
| 68 |
|
| 69 |
# Precision
|