Spaces:
Sleeping
Sleeping
Commit
·
96b2ae3
1
Parent(s):
05e52e1
initial commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +3 -0
- .gitignore +45 -0
- Dockerfile +62 -0
- README.md +86 -7
- backend/Dockerfile.dev +25 -0
- backend/README.md +352 -0
- backend/app/api/__init__.py +5 -0
- backend/app/api/dependencies.py +34 -0
- backend/app/api/endpoints/leaderboard.py +49 -0
- backend/app/api/endpoints/models.py +116 -0
- backend/app/api/endpoints/votes.py +126 -0
- backend/app/api/router.py +9 -0
- backend/app/asgi.py +106 -0
- backend/app/config/__init__.py +6 -0
- backend/app/config/base.py +38 -0
- backend/app/config/hf_config.py +30 -0
- backend/app/config/logging_config.py +38 -0
- backend/app/core/cache.py +109 -0
- backend/app/core/fastapi_cache.py +76 -0
- backend/app/core/formatting.py +104 -0
- backend/app/main.py +18 -0
- backend/app/services/__init__.py +3 -0
- backend/app/services/hf_service.py +50 -0
- backend/app/services/leaderboard.py +208 -0
- backend/app/services/models.py +668 -0
- backend/app/services/rate_limiter.py +72 -0
- backend/app/services/votes.py +441 -0
- backend/app/utils/__init__.py +3 -0
- backend/app/utils/logging.py +3 -0
- backend/app/utils/model_validation.py +266 -0
- backend/pyproject.toml +31 -0
- backend/utils/analyze_prod_datasets.py +170 -0
- backend/utils/analyze_prod_models.py +106 -0
- backend/utils/fix_wrong_model_size.py +110 -0
- backend/utils/last_activity.py +164 -0
- backend/utils/sync_datasets_locally.py +130 -0
- docker-compose.yml +33 -0
- frontend/Dockerfile.dev +15 -0
- frontend/README.md +80 -0
- frontend/package.json +55 -0
- frontend/public/index.html +96 -0
- frontend/public/logo256.png +0 -0
- frontend/public/logo32.png +0 -0
- frontend/public/og-image.jpg +0 -0
- frontend/public/robots.txt +3 -0
- frontend/server.js +85 -0
- frontend/src/App.js +121 -0
- frontend/src/components/Footer/Footer.js +30 -0
- frontend/src/components/Logo/HFLogo.js +19 -0
- frontend/src/components/Logo/Logo.js +56 -0
.env.example
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
ENVIRONMENT=development
|
2 |
+
HF_TOKEN=xxx
|
3 |
+
HF_HOME=.cache
|
.gitignore
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
__pycache__
|
4 |
+
.cache/
|
5 |
+
|
6 |
+
# dependencies
|
7 |
+
|
8 |
+
frontend/node_modules
|
9 |
+
/.pnp
|
10 |
+
.pnp.js
|
11 |
+
|
12 |
+
# testing
|
13 |
+
|
14 |
+
/coverage
|
15 |
+
|
16 |
+
# production
|
17 |
+
|
18 |
+
/build
|
19 |
+
|
20 |
+
# misc
|
21 |
+
|
22 |
+
.DS_Store
|
23 |
+
.env.local
|
24 |
+
.env.development.local
|
25 |
+
.env.test.local
|
26 |
+
.env.production.local
|
27 |
+
|
28 |
+
npm-debug.log*
|
29 |
+
yarn-debug.log*
|
30 |
+
yarn-error.log\*
|
31 |
+
|
32 |
+
src/dataframe.json
|
33 |
+
|
34 |
+
yarn.lock
|
35 |
+
package-lock.json
|
36 |
+
|
37 |
+
/public
|
38 |
+
|
39 |
+
.claudesync/
|
40 |
+
|
41 |
+
# Environment variables
|
42 |
+
.env
|
43 |
+
.env.*
|
44 |
+
!.env.example
|
45 |
+
|
Dockerfile
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Build frontend
|
2 |
+
FROM node:18 as frontend-build
|
3 |
+
WORKDIR /app
|
4 |
+
COPY frontend/package*.json ./
|
5 |
+
RUN npm install
|
6 |
+
COPY frontend/ ./
|
7 |
+
|
8 |
+
RUN npm run build
|
9 |
+
|
10 |
+
# Build backend
|
11 |
+
FROM python:3.12-slim
|
12 |
+
WORKDIR /app
|
13 |
+
|
14 |
+
# Create non-root user
|
15 |
+
RUN useradd -m -u 1000 user
|
16 |
+
|
17 |
+
# Install poetry
|
18 |
+
RUN pip install poetry
|
19 |
+
|
20 |
+
# Create and configure cache directory
|
21 |
+
RUN mkdir -p /app/.cache && \
|
22 |
+
chown -R user:user /app
|
23 |
+
|
24 |
+
# Copy and install backend dependencies
|
25 |
+
COPY backend/pyproject.toml backend/poetry.lock* ./
|
26 |
+
RUN poetry config virtualenvs.create false \
|
27 |
+
&& poetry install --no-interaction --no-ansi --no-root --only main
|
28 |
+
|
29 |
+
# Copy backend code
|
30 |
+
COPY backend/ .
|
31 |
+
|
32 |
+
# Install Node.js and npm
|
33 |
+
RUN apt-get update && apt-get install -y \
|
34 |
+
curl \
|
35 |
+
netcat-openbsd \
|
36 |
+
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
37 |
+
&& apt-get install -y nodejs \
|
38 |
+
&& rm -rf /var/lib/apt/lists/*
|
39 |
+
|
40 |
+
# Copy frontend server and build
|
41 |
+
COPY --from=frontend-build /app/build ./frontend/build
|
42 |
+
COPY --from=frontend-build /app/package*.json ./frontend/
|
43 |
+
COPY --from=frontend-build /app/server.js ./frontend/
|
44 |
+
|
45 |
+
# Install frontend production dependencies
|
46 |
+
WORKDIR /app/frontend
|
47 |
+
RUN npm install --production
|
48 |
+
WORKDIR /app
|
49 |
+
|
50 |
+
# Environment variables
|
51 |
+
ENV HF_HOME=/app/.cache \
|
52 |
+
HF_DATASETS_CACHE=/app/.cache \
|
53 |
+
INTERNAL_API_PORT=7861 \
|
54 |
+
PORT=7860 \
|
55 |
+
NODE_ENV=production
|
56 |
+
|
57 |
+
# Note: HF_TOKEN should be provided at runtime, not build time
|
58 |
+
USER user
|
59 |
+
EXPOSE 7860
|
60 |
+
|
61 |
+
# Start both servers with wait-for
|
62 |
+
CMD ["sh", "-c", "uvicorn app.asgi:app --host 0.0.0.0 --port 7861 & while ! nc -z localhost 7861; do sleep 1; done && cd frontend && npm run serve"]
|
README.md
CHANGED
@@ -1,12 +1,91 @@
|
|
1 |
---
|
2 |
-
title: Open
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
-
|
|
|
8 |
license: apache-2.0
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Open LLM Leaderboard
|
3 |
+
emoji: 🏆
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: red
|
6 |
sdk: docker
|
7 |
+
hf_oauth: true
|
8 |
+
pinned: true
|
9 |
license: apache-2.0
|
10 |
+
duplicated_from: open-llm-leaderboard/open_llm_leaderboard
|
11 |
+
tags:
|
12 |
+
- leaderboard
|
13 |
+
- modality:text
|
14 |
+
- submission:automatic
|
15 |
+
- test:public
|
16 |
+
- language:english
|
17 |
+
- eval:code
|
18 |
+
- eval:math
|
19 |
+
short_description: Track, rank and evaluate open LLMs and chatbots
|
20 |
---
|
21 |
|
22 |
+
# Open LLM Leaderboard
|
23 |
+
|
24 |
+
Modern React interface for comparing Large Language Models (LLMs) in an open and reproducible way.
|
25 |
+
|
26 |
+
## Features
|
27 |
+
|
28 |
+
- 📊 Interactive table with advanced sorting and filtering
|
29 |
+
- 🔍 Semantic model search
|
30 |
+
- 📌 Pin models for comparison
|
31 |
+
- 📱 Responsive and modern interface
|
32 |
+
- 🎨 Dark/Light mode
|
33 |
+
- ⚡️ Optimized performance with virtualization
|
34 |
+
|
35 |
+
## Architecture
|
36 |
+
|
37 |
+
The project is split into two main parts:
|
38 |
+
|
39 |
+
### Frontend (React)
|
40 |
+
|
41 |
+
```
|
42 |
+
frontend/
|
43 |
+
├── src/
|
44 |
+
│ ├── components/ # Reusable UI components
|
45 |
+
│ ├── pages/ # Application pages
|
46 |
+
│ ├── hooks/ # Custom React hooks
|
47 |
+
│ ├── context/ # React contexts
|
48 |
+
│ └── constants/ # Constants and configurations
|
49 |
+
├── public/ # Static assets
|
50 |
+
└── server.js # Express server for production
|
51 |
+
```
|
52 |
+
|
53 |
+
### Backend (FastAPI)
|
54 |
+
|
55 |
+
```
|
56 |
+
backend/
|
57 |
+
├── app/
|
58 |
+
│ ├── api/ # API router and endpoints
|
59 |
+
│ │ └── endpoints/ # Specific API endpoints
|
60 |
+
│ ├── core/ # Core functionality
|
61 |
+
│ ├── config/ # Configuration
|
62 |
+
│ └── services/ # Business logic services
|
63 |
+
│ ├── leaderboard.py
|
64 |
+
│ ├── models.py
|
65 |
+
│ ├── votes.py
|
66 |
+
│ └── hf_service.py
|
67 |
+
└── utils/ # Utility functions
|
68 |
+
```
|
69 |
+
|
70 |
+
## Technologies
|
71 |
+
|
72 |
+
### Frontend
|
73 |
+
|
74 |
+
- React
|
75 |
+
- Material-UI
|
76 |
+
- TanStack Table & Virtual
|
77 |
+
- Express.js
|
78 |
+
|
79 |
+
### Backend
|
80 |
+
|
81 |
+
- FastAPI
|
82 |
+
- Hugging Face API
|
83 |
+
- Docker
|
84 |
+
|
85 |
+
## Development
|
86 |
+
|
87 |
+
The application is containerized using Docker and can be run using:
|
88 |
+
|
89 |
+
```bash
|
90 |
+
docker-compose up
|
91 |
+
```
|
backend/Dockerfile.dev
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.12-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
# Install required system dependencies
|
6 |
+
RUN apt-get update && apt-get install -y \
|
7 |
+
build-essential \
|
8 |
+
&& rm -rf /var/lib/apt/lists/*
|
9 |
+
|
10 |
+
# Install poetry
|
11 |
+
RUN pip install poetry
|
12 |
+
|
13 |
+
# Copy Poetry configuration files
|
14 |
+
COPY pyproject.toml poetry.lock* ./
|
15 |
+
|
16 |
+
# Install dependencies
|
17 |
+
RUN poetry config virtualenvs.create false && \
|
18 |
+
poetry install --no-interaction --no-ansi --no-root
|
19 |
+
|
20 |
+
# Environment variables configuration for logs
|
21 |
+
ENV PYTHONUNBUFFERED=1
|
22 |
+
ENV LOG_LEVEL=INFO
|
23 |
+
|
24 |
+
# In dev, mount volume directly
|
25 |
+
CMD ["uvicorn", "app.asgi:app", "--host", "0.0.0.0", "--port", "7860", "--reload", "--log-level", "warning", "--no-access-log"]
|
backend/README.md
ADDED
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Backend - Open LLM Leaderboard 🏆
|
2 |
+
|
3 |
+
FastAPI backend for the Open LLM Leaderboard. This service is part of a larger architecture that includes a React frontend. For complete project installation, see the [main README](../README.md).
|
4 |
+
|
5 |
+
## ✨ Features
|
6 |
+
|
7 |
+
- 📊 REST API for LLM models leaderboard management
|
8 |
+
- 🗳️ Voting and ranking system
|
9 |
+
- 🔄 HuggingFace Hub integration
|
10 |
+
- 🚀 Caching and performance optimizations
|
11 |
+
|
12 |
+
## 🏗 Architecture
|
13 |
+
|
14 |
+
```mermaid
|
15 |
+
flowchart TD
|
16 |
+
Client(["**Frontend**<br><br>React Application"]) --> API["**API Server**<br><br>FastAPI REST Endpoints"]
|
17 |
+
|
18 |
+
subgraph Backend
|
19 |
+
API --> Core["**Core Layer**<br><br>• Middleware<br>• Cache<br>• Rate Limiting"]
|
20 |
+
Core --> Services["**Services Layer**<br><br>• Business Logic<br>• Data Processing"]
|
21 |
+
|
22 |
+
subgraph Services Layer
|
23 |
+
Services --> Models["**Model Service**<br><br>• Model Submission<br>• Evaluation Pipeline"]
|
24 |
+
Services --> Votes["**Vote Service**<br><br>• Vote Management<br>• Data Synchronization"]
|
25 |
+
Services --> Board["**Leaderboard Service**<br><br>• Rankings<br>• Performance Metrics"]
|
26 |
+
end
|
27 |
+
|
28 |
+
Models --> Cache["**Cache Layer**<br><br>• In-Memory Store<br>• Auto Invalidation"]
|
29 |
+
Votes --> Cache
|
30 |
+
Board --> Cache
|
31 |
+
|
32 |
+
Models --> HF["**HuggingFace Hub**<br><br>• Models Repository<br>• Datasets Access"]
|
33 |
+
Votes --> HF
|
34 |
+
Board --> HF
|
35 |
+
end
|
36 |
+
|
37 |
+
style Client fill:#f9f,stroke:#333,stroke-width:2px
|
38 |
+
style Models fill:#bbf,stroke:#333,stroke-width:2px
|
39 |
+
style Votes fill:#bbf,stroke:#333,stroke-width:2px
|
40 |
+
style Board fill:#bbf,stroke:#333,stroke-width:2px
|
41 |
+
style HF fill:#bfb,stroke:#333,stroke-width:2px
|
42 |
+
```
|
43 |
+
|
44 |
+
## 🛠️ HuggingFace Datasets
|
45 |
+
|
46 |
+
The application uses several datasets on the HuggingFace Hub:
|
47 |
+
|
48 |
+
### 1. Requests Dataset (`{HF_ORGANIZATION}/requests`)
|
49 |
+
|
50 |
+
- **Operations**:
|
51 |
+
- 📤 `POST /api/models/submit`: Adds a JSON file for each new model submission
|
52 |
+
- 📥 `GET /api/models/status`: Reads files to get models status
|
53 |
+
- **Format**: One JSON file per model with submission details
|
54 |
+
- **Updates**: On each new model submission
|
55 |
+
|
56 |
+
### 2. Votes Dataset (`{HF_ORGANIZATION}/votes`)
|
57 |
+
|
58 |
+
- **Operations**:
|
59 |
+
- 📤 `POST /api/votes/{model_id}`: Adds a new vote
|
60 |
+
- 📥 `GET /api/votes/model/{provider}/{model}`: Reads model votes
|
61 |
+
- 📥 `GET /api/votes/user/{user_id}`: Reads user votes
|
62 |
+
- **Format**: JSONL with one vote per line
|
63 |
+
- **Sync**: Bidirectional between local cache and Hub
|
64 |
+
|
65 |
+
### 3. Contents Dataset (`{HF_ORGANIZATION}/contents`)
|
66 |
+
|
67 |
+
- **Operations**:
|
68 |
+
- 📥 `GET /api/leaderboard`: Reads raw data
|
69 |
+
- 📥 `GET /api/leaderboard/formatted`: Reads and formats data
|
70 |
+
- **Format**: Main dataset containing all scores and metrics
|
71 |
+
- **Updates**: Automatic after model evaluations
|
72 |
+
|
73 |
+
### 4. Official Providers Dataset (`{HF_ORGANIZATION}/official-providers`)
|
74 |
+
|
75 |
+
- **Operations**:
|
76 |
+
- 📥 Read-only access for highlighted models
|
77 |
+
- **Format**: List of models selected by maintainers
|
78 |
+
- **Updates**: Manual by maintainers
|
79 |
+
|
80 |
+
## 🛠 Local Development
|
81 |
+
|
82 |
+
### Prerequisites
|
83 |
+
|
84 |
+
- Python 3.9+
|
85 |
+
- [Poetry](https://python-poetry.org/docs/#installation)
|
86 |
+
|
87 |
+
### Standalone Installation (without Docker)
|
88 |
+
|
89 |
+
```bash
|
90 |
+
# Install dependencies
|
91 |
+
poetry install
|
92 |
+
|
93 |
+
# Setup configuration
|
94 |
+
cp .env.example .env
|
95 |
+
|
96 |
+
# Start development server
|
97 |
+
poetry run uvicorn app.asgi:app --host 0.0.0.0 --port 7860 --reload
|
98 |
+
```
|
99 |
+
|
100 |
+
Server will be available at http://localhost:7860
|
101 |
+
|
102 |
+
## ⚙️ Configuration
|
103 |
+
|
104 |
+
| Variable | Description | Default |
|
105 |
+
| ------------ | ------------------------------------ | ----------- |
|
106 |
+
| ENVIRONMENT | Environment (development/production) | development |
|
107 |
+
| HF_TOKEN | HuggingFace authentication token | - |
|
108 |
+
| PORT | Server port | 7860 |
|
109 |
+
| LOG_LEVEL | Logging level (INFO/DEBUG/WARNING) | INFO |
|
110 |
+
| CORS_ORIGINS | Allowed CORS origins | ["*"] |
|
111 |
+
| CACHE_TTL | Cache Time To Live in seconds | 300 |
|
112 |
+
|
113 |
+
## 🔧 Middleware
|
114 |
+
|
115 |
+
The backend uses several middleware layers for optimal performance and security:
|
116 |
+
|
117 |
+
- **CORS Middleware**: Handles Cross-Origin Resource Sharing
|
118 |
+
- **GZIP Middleware**: Compresses responses > 500 bytes
|
119 |
+
- **Rate Limiting**: Prevents API abuse
|
120 |
+
- **Caching**: In-memory caching with automatic invalidation
|
121 |
+
|
122 |
+
## 📝 Logging
|
123 |
+
|
124 |
+
The application uses a structured logging system with:
|
125 |
+
|
126 |
+
- Formatted console output
|
127 |
+
- Different log levels per component
|
128 |
+
- Request/Response logging
|
129 |
+
- Performance metrics
|
130 |
+
- Error tracking
|
131 |
+
|
132 |
+
## 📁 File Structure
|
133 |
+
|
134 |
+
```
|
135 |
+
backend/
|
136 |
+
├── app/ # Source code
|
137 |
+
│ ├── api/ # Routes and endpoints
|
138 |
+
│ │ └── endpoints/ # Endpoint handlers
|
139 |
+
│ ├── core/ # Configurations
|
140 |
+
│ ├── services/ # Business logic
|
141 |
+
│ └── utils/ # Utilities
|
142 |
+
└── tests/ # Tests
|
143 |
+
```
|
144 |
+
|
145 |
+
## 📚 API
|
146 |
+
|
147 |
+
Swagger documentation available at http://localhost:7860/docs
|
148 |
+
|
149 |
+
### Main Endpoints & Data Structures
|
150 |
+
|
151 |
+
#### Leaderboard
|
152 |
+
|
153 |
+
- `GET /api/leaderboard/formatted` - Formatted data with computed fields and metadata
|
154 |
+
|
155 |
+
```typescript
|
156 |
+
Response {
|
157 |
+
models: [{
|
158 |
+
id: string, // eval_name
|
159 |
+
model: {
|
160 |
+
name: string, // fullname
|
161 |
+
sha: string, // Model sha
|
162 |
+
precision: string, // e.g. "fp16", "int8"
|
163 |
+
type: string, // e.g. "fined-tuned-on-domain-specific-dataset"
|
164 |
+
weight_type: string,
|
165 |
+
architecture: string,
|
166 |
+
average_score: number,
|
167 |
+
has_chat_template: boolean
|
168 |
+
},
|
169 |
+
evaluations: {
|
170 |
+
ifeval: {
|
171 |
+
name: "IFEval",
|
172 |
+
value: number, // Raw score
|
173 |
+
normalized_score: number
|
174 |
+
},
|
175 |
+
bbh: {
|
176 |
+
name: "BBH",
|
177 |
+
value: number,
|
178 |
+
normalized_score: number
|
179 |
+
},
|
180 |
+
math: {
|
181 |
+
name: "MATH Level 5",
|
182 |
+
value: number,
|
183 |
+
normalized_score: number
|
184 |
+
},
|
185 |
+
gpqa: {
|
186 |
+
name: "GPQA",
|
187 |
+
value: number,
|
188 |
+
normalized_score: number
|
189 |
+
},
|
190 |
+
musr: {
|
191 |
+
name: "MUSR",
|
192 |
+
value: number,
|
193 |
+
normalized_score: number
|
194 |
+
},
|
195 |
+
mmlu_pro: {
|
196 |
+
name: "MMLU-PRO",
|
197 |
+
value: number,
|
198 |
+
normalized_score: number
|
199 |
+
}
|
200 |
+
},
|
201 |
+
features: {
|
202 |
+
is_not_available_on_hub: boolean,
|
203 |
+
is_merged: boolean,
|
204 |
+
is_moe: boolean,
|
205 |
+
is_flagged: boolean,
|
206 |
+
is_official_provider: boolean
|
207 |
+
},
|
208 |
+
metadata: {
|
209 |
+
upload_date: string,
|
210 |
+
submission_date: string,
|
211 |
+
generation: string,
|
212 |
+
base_model: string,
|
213 |
+
hub_license: string,
|
214 |
+
hub_hearts: number,
|
215 |
+
params_billions: number,
|
216 |
+
co2_cost: number // CO₂ cost in kg
|
217 |
+
}
|
218 |
+
}]
|
219 |
+
}
|
220 |
+
```
|
221 |
+
|
222 |
+
- `GET /api/leaderboard` - Raw data from the HuggingFace dataset
|
223 |
+
```typescript
|
224 |
+
Response {
|
225 |
+
models: [{
|
226 |
+
eval_name: string,
|
227 |
+
Precision: string,
|
228 |
+
Type: string,
|
229 |
+
"Weight type": string,
|
230 |
+
Architecture: string,
|
231 |
+
Model: string,
|
232 |
+
fullname: string,
|
233 |
+
"Model sha": string,
|
234 |
+
"Average ⬆️": number,
|
235 |
+
"Hub License": string,
|
236 |
+
"Hub ❤️": number,
|
237 |
+
"#Params (B)": number,
|
238 |
+
"Available on the hub": boolean,
|
239 |
+
Merged: boolean,
|
240 |
+
MoE: boolean,
|
241 |
+
Flagged: boolean,
|
242 |
+
"Chat Template": boolean,
|
243 |
+
"CO₂ cost (kg)": number,
|
244 |
+
"IFEval Raw": number,
|
245 |
+
IFEval: number,
|
246 |
+
"BBH Raw": number,
|
247 |
+
BBH: number,
|
248 |
+
"MATH Lvl 5 Raw": number,
|
249 |
+
"MATH Lvl 5": number,
|
250 |
+
"GPQA Raw": number,
|
251 |
+
GPQA: number,
|
252 |
+
"MUSR Raw": number,
|
253 |
+
MUSR: number,
|
254 |
+
"MMLU-PRO Raw": number,
|
255 |
+
"MMLU-PRO": number,
|
256 |
+
"Maintainer's Highlight": boolean,
|
257 |
+
"Upload To Hub Date": string,
|
258 |
+
"Submission Date": string,
|
259 |
+
Generation: string,
|
260 |
+
"Base Model": string
|
261 |
+
}]
|
262 |
+
}
|
263 |
+
```
|
264 |
+
|
265 |
+
#### Models
|
266 |
+
|
267 |
+
- `GET /api/models/status` - Get all models grouped by status
|
268 |
+
```typescript
|
269 |
+
Response {
|
270 |
+
pending: [{
|
271 |
+
name: string,
|
272 |
+
submitter: string,
|
273 |
+
revision: string,
|
274 |
+
wait_time: string,
|
275 |
+
submission_time: string,
|
276 |
+
status: "PENDING" | "EVALUATING" | "FINISHED",
|
277 |
+
precision: string
|
278 |
+
}],
|
279 |
+
evaluating: Array<Model>,
|
280 |
+
finished: Array<Model>
|
281 |
+
}
|
282 |
+
```
|
283 |
+
- `GET /api/models/pending` - Get pending models only
|
284 |
+
- `POST /api/models/submit` - Submit model
|
285 |
+
|
286 |
+
```typescript
|
287 |
+
Request {
|
288 |
+
user_id: string,
|
289 |
+
model_id: string,
|
290 |
+
base_model?: string,
|
291 |
+
precision?: string,
|
292 |
+
model_type: string
|
293 |
+
}
|
294 |
+
|
295 |
+
Response {
|
296 |
+
status: string,
|
297 |
+
message: string
|
298 |
+
}
|
299 |
+
```
|
300 |
+
|
301 |
+
- `GET /api/models/{model_id}/status` - Get model status
|
302 |
+
|
303 |
+
#### Votes
|
304 |
+
|
305 |
+
- `POST /api/votes/{model_id}` - Vote
|
306 |
+
|
307 |
+
```typescript
|
308 |
+
Request {
|
309 |
+
vote_type: "up" | "down",
|
310 |
+
user_id: string // HuggingFace username
|
311 |
+
}
|
312 |
+
|
313 |
+
Response {
|
314 |
+
success: boolean,
|
315 |
+
message: string
|
316 |
+
}
|
317 |
+
```
|
318 |
+
|
319 |
+
- `GET /api/votes/model/{provider}/{model}` - Get model votes
|
320 |
+
```typescript
|
321 |
+
Response {
|
322 |
+
total_votes: number,
|
323 |
+
up_votes: number,
|
324 |
+
down_votes: number
|
325 |
+
}
|
326 |
+
```
|
327 |
+
- `GET /api/votes/user/{user_id}` - Get user votes
|
328 |
+
```typescript
|
329 |
+
Response Array<{
|
330 |
+
model_id: string,
|
331 |
+
vote_type: string,
|
332 |
+
timestamp: string
|
333 |
+
}>
|
334 |
+
```
|
335 |
+
|
336 |
+
## 🔒 Authentication
|
337 |
+
|
338 |
+
The backend uses HuggingFace token-based authentication for secure API access. Make sure to:
|
339 |
+
|
340 |
+
1. Set your HF_TOKEN in the .env file
|
341 |
+
2. Include the token in API requests via Bearer authentication
|
342 |
+
3. Keep your token secure and never commit it to version control
|
343 |
+
|
344 |
+
## 🚀 Performance
|
345 |
+
|
346 |
+
The backend implements several optimizations:
|
347 |
+
|
348 |
+
- In-memory caching with configurable TTL (Time To Live)
|
349 |
+
- Batch processing for model evaluations
|
350 |
+
- Rate limiting for API endpoints
|
351 |
+
- Efficient database queries with proper indexing
|
352 |
+
- Automatic cache invalidation for votes
|
backend/app/api/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
API package initialization
|
3 |
+
"""
|
4 |
+
|
5 |
+
__all__ = ["endpoints"]
|
backend/app/api/dependencies.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import Depends, HTTPException
|
2 |
+
import logging
|
3 |
+
from app.services.models import ModelService
|
4 |
+
from app.services.votes import VoteService
|
5 |
+
from app.core.formatting import LogFormatter
|
6 |
+
|
7 |
+
logger = logging.getLogger(__name__)
|
8 |
+
|
9 |
+
model_service = ModelService()
|
10 |
+
vote_service = VoteService()
|
11 |
+
|
12 |
+
async def get_model_service() -> ModelService:
|
13 |
+
"""Dependency to get ModelService instance"""
|
14 |
+
try:
|
15 |
+
logger.info(LogFormatter.info("Initializing model service dependency"))
|
16 |
+
await model_service.initialize()
|
17 |
+
logger.info(LogFormatter.success("Model service initialized"))
|
18 |
+
return model_service
|
19 |
+
except Exception as e:
|
20 |
+
error_msg = "Failed to initialize model service"
|
21 |
+
logger.error(LogFormatter.error(error_msg, e))
|
22 |
+
raise HTTPException(status_code=500, detail=str(e))
|
23 |
+
|
24 |
+
async def get_vote_service() -> VoteService:
|
25 |
+
"""Dependency to get VoteService instance"""
|
26 |
+
try:
|
27 |
+
logger.info(LogFormatter.info("Initializing vote service dependency"))
|
28 |
+
await vote_service.initialize()
|
29 |
+
logger.info(LogFormatter.success("Vote service initialized"))
|
30 |
+
return vote_service
|
31 |
+
except Exception as e:
|
32 |
+
error_msg = "Failed to initialize vote service"
|
33 |
+
logger.error(LogFormatter.error(error_msg, e))
|
34 |
+
raise HTTPException(status_code=500, detail=str(e))
|
backend/app/api/endpoints/leaderboard.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter
|
2 |
+
from typing import List, Dict, Any
|
3 |
+
from app.services.leaderboard import LeaderboardService
|
4 |
+
from app.core.fastapi_cache import cached, build_cache_key
|
5 |
+
import logging
|
6 |
+
from app.core.formatting import LogFormatter
|
7 |
+
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
router = APIRouter()
|
10 |
+
leaderboard_service = LeaderboardService()
|
11 |
+
|
12 |
+
def leaderboard_key_builder(func, namespace: str = "leaderboard", **kwargs):
|
13 |
+
"""Build cache key for leaderboard data"""
|
14 |
+
key_type = "raw" if func.__name__ == "get_leaderboard" else "formatted"
|
15 |
+
key = build_cache_key(namespace, key_type)
|
16 |
+
logger.debug(LogFormatter.info(f"Built leaderboard cache key: {key}"))
|
17 |
+
return key
|
18 |
+
|
19 |
+
@router.get("")
|
20 |
+
@cached(expire=300, key_builder=leaderboard_key_builder)
|
21 |
+
async def get_leaderboard() -> List[Dict[str, Any]]:
|
22 |
+
"""
|
23 |
+
Get raw leaderboard data
|
24 |
+
Response will be automatically GZIP compressed if size > 500 bytes
|
25 |
+
"""
|
26 |
+
try:
|
27 |
+
logger.info(LogFormatter.info("Fetching raw leaderboard data"))
|
28 |
+
data = await leaderboard_service.fetch_raw_data()
|
29 |
+
logger.info(LogFormatter.success(f"Retrieved {len(data)} leaderboard entries"))
|
30 |
+
return data
|
31 |
+
except Exception as e:
|
32 |
+
logger.error(LogFormatter.error("Failed to fetch raw leaderboard data", e))
|
33 |
+
raise
|
34 |
+
|
35 |
+
@router.get("/formatted")
|
36 |
+
@cached(expire=300, key_builder=leaderboard_key_builder)
|
37 |
+
async def get_formatted_leaderboard() -> List[Dict[str, Any]]:
|
38 |
+
"""
|
39 |
+
Get formatted leaderboard data with restructured objects
|
40 |
+
Response will be automatically GZIP compressed if size > 500 bytes
|
41 |
+
"""
|
42 |
+
try:
|
43 |
+
logger.info(LogFormatter.info("Fetching formatted leaderboard data"))
|
44 |
+
data = await leaderboard_service.get_formatted_data()
|
45 |
+
logger.info(LogFormatter.success(f"Retrieved {len(data)} formatted entries"))
|
46 |
+
return data
|
47 |
+
except Exception as e:
|
48 |
+
logger.error(LogFormatter.error("Failed to fetch formatted leaderboard data", e))
|
49 |
+
raise
|
backend/app/api/endpoints/models.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, Depends, Query
|
2 |
+
from typing import Dict, Any, List
|
3 |
+
import logging
|
4 |
+
from app.services.models import ModelService
|
5 |
+
from app.api.dependencies import get_model_service
|
6 |
+
from app.core.fastapi_cache import cached
|
7 |
+
from app.core.formatting import LogFormatter
|
8 |
+
|
9 |
+
logger = logging.getLogger(__name__)
|
10 |
+
router = APIRouter(tags=["models"])
|
11 |
+
|
12 |
+
@router.get("/status")
|
13 |
+
@cached(expire=300)
|
14 |
+
async def get_models_status(
|
15 |
+
model_service: ModelService = Depends(get_model_service)
|
16 |
+
) -> Dict[str, List[Dict[str, Any]]]:
|
17 |
+
"""Get all models grouped by status"""
|
18 |
+
try:
|
19 |
+
logger.info(LogFormatter.info("Fetching status for all models"))
|
20 |
+
result = await model_service.get_models()
|
21 |
+
stats = {
|
22 |
+
status: len(models) for status, models in result.items()
|
23 |
+
}
|
24 |
+
for line in LogFormatter.stats(stats, "Models by Status"):
|
25 |
+
logger.info(line)
|
26 |
+
return result
|
27 |
+
except Exception as e:
|
28 |
+
logger.error(LogFormatter.error("Failed to get models status", e))
|
29 |
+
raise HTTPException(status_code=500, detail=str(e))
|
30 |
+
|
31 |
+
@router.get("/pending")
|
32 |
+
@cached(expire=60)
|
33 |
+
async def get_pending_models(
|
34 |
+
model_service: ModelService = Depends(get_model_service)
|
35 |
+
) -> List[Dict[str, Any]]:
|
36 |
+
"""Get all models waiting for evaluation"""
|
37 |
+
try:
|
38 |
+
logger.info(LogFormatter.info("Fetching pending models"))
|
39 |
+
models = await model_service.get_models()
|
40 |
+
pending = models.get("pending", [])
|
41 |
+
logger.info(LogFormatter.success(f"Found {len(pending)} pending models"))
|
42 |
+
return pending
|
43 |
+
except Exception as e:
|
44 |
+
logger.error(LogFormatter.error("Failed to get pending models", e))
|
45 |
+
raise HTTPException(status_code=500, detail=str(e))
|
46 |
+
|
47 |
+
@router.post("/submit")
|
48 |
+
async def submit_model(
|
49 |
+
model_data: Dict[str, Any],
|
50 |
+
model_service: ModelService = Depends(get_model_service)
|
51 |
+
) -> Dict[str, Any]:
|
52 |
+
try:
|
53 |
+
logger.info(LogFormatter.section("MODEL SUBMISSION"))
|
54 |
+
|
55 |
+
user_id = model_data.pop('user_id', None)
|
56 |
+
if not user_id:
|
57 |
+
error_msg = "user_id is required"
|
58 |
+
logger.error(LogFormatter.error("Validation failed", error_msg))
|
59 |
+
raise ValueError(error_msg)
|
60 |
+
|
61 |
+
# Log submission details
|
62 |
+
submission_info = {
|
63 |
+
"Model_ID": model_data.get("model_id"),
|
64 |
+
"User": user_id,
|
65 |
+
"Base_Model": model_data.get("base_model"),
|
66 |
+
"Precision": model_data.get("precision"),
|
67 |
+
"Model_Type": model_data.get("model_type")
|
68 |
+
}
|
69 |
+
for line in LogFormatter.tree(submission_info, "Submission Details"):
|
70 |
+
logger.info(line)
|
71 |
+
|
72 |
+
result = await model_service.submit_model(model_data, user_id)
|
73 |
+
logger.info(LogFormatter.success("Model submitted successfully"))
|
74 |
+
return result
|
75 |
+
|
76 |
+
except ValueError as e:
|
77 |
+
logger.error(LogFormatter.error("Invalid submission data", e))
|
78 |
+
raise HTTPException(status_code=400, detail=str(e))
|
79 |
+
except Exception as e:
|
80 |
+
logger.error(LogFormatter.error("Submission failed", e))
|
81 |
+
raise HTTPException(status_code=500, detail=str(e))
|
82 |
+
|
83 |
+
@router.get("/organization/{organization}/submissions")
|
84 |
+
async def get_organization_submissions(
|
85 |
+
organization: str,
|
86 |
+
days: int = Query(default=7, ge=1, le=30),
|
87 |
+
model_service: ModelService = Depends(get_model_service)
|
88 |
+
) -> List[Dict[str, Any]]:
|
89 |
+
"""Get all submissions from an organization in the last n days"""
|
90 |
+
try:
|
91 |
+
submissions = await model_service.get_organization_submissions(organization, days)
|
92 |
+
return submissions
|
93 |
+
except Exception as e:
|
94 |
+
raise HTTPException(status_code=500, detail=str(e))
|
95 |
+
|
96 |
+
@router.get("/{model_id}/status")
|
97 |
+
async def get_model_status(
|
98 |
+
model_id: str,
|
99 |
+
model_service: ModelService = Depends(get_model_service)
|
100 |
+
) -> Dict[str, Any]:
|
101 |
+
try:
|
102 |
+
logger.info(LogFormatter.info(f"Checking status for model: {model_id}"))
|
103 |
+
status = await model_service.get_model_status(model_id)
|
104 |
+
|
105 |
+
if status["status"] != "not_found":
|
106 |
+
logger.info(LogFormatter.success("Status found"))
|
107 |
+
for line in LogFormatter.tree(status, "Model Status"):
|
108 |
+
logger.info(line)
|
109 |
+
else:
|
110 |
+
logger.warning(LogFormatter.warning(f"No status found for model: {model_id}"))
|
111 |
+
|
112 |
+
return status
|
113 |
+
|
114 |
+
except Exception as e:
|
115 |
+
logger.error(LogFormatter.error("Failed to get model status", e))
|
116 |
+
raise HTTPException(status_code=500, detail=str(e))
|
backend/app/api/endpoints/votes.py
ADDED
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, Query, Depends, Response
|
2 |
+
from typing import Dict, Any, List
|
3 |
+
from app.services.votes import VoteService
|
4 |
+
from app.core.fastapi_cache import cached, build_cache_key, invalidate_cache_key
|
5 |
+
import logging
|
6 |
+
from app.core.formatting import LogFormatter
|
7 |
+
from datetime import datetime, timezone
|
8 |
+
|
9 |
+
logger = logging.getLogger(__name__)
|
10 |
+
router = APIRouter()
|
11 |
+
vote_service = VoteService()
|
12 |
+
|
13 |
+
CACHE_TTL = 30 # 30 seconds cache
|
14 |
+
|
15 |
+
def model_votes_key_builder(func, namespace: str = "model_votes", **kwargs):
|
16 |
+
"""Build cache key for model votes"""
|
17 |
+
provider = kwargs.get('provider')
|
18 |
+
model = kwargs.get('model')
|
19 |
+
key = build_cache_key(namespace, provider, model)
|
20 |
+
logger.debug(LogFormatter.info(f"Built model votes cache key: {key}"))
|
21 |
+
return key
|
22 |
+
|
23 |
+
def user_votes_key_builder(func, namespace: str = "user_votes", **kwargs):
|
24 |
+
"""Build cache key for user votes"""
|
25 |
+
user_id = kwargs.get('user_id')
|
26 |
+
key = build_cache_key(namespace, user_id)
|
27 |
+
logger.debug(LogFormatter.info(f"Built user votes cache key: {key}"))
|
28 |
+
return key
|
29 |
+
|
30 |
+
@router.post("/{model_id:path}")
|
31 |
+
async def add_vote(
|
32 |
+
response: Response,
|
33 |
+
model_id: str,
|
34 |
+
vote_type: str = Query(..., description="Type of vote (up/down)"),
|
35 |
+
user_id: str = Query(..., description="HuggingFace username"),
|
36 |
+
vote_data: Dict[str, Any] = None
|
37 |
+
) -> Dict[str, Any]:
|
38 |
+
try:
|
39 |
+
logger.info(LogFormatter.section("ADDING VOTE"))
|
40 |
+
stats = {
|
41 |
+
"Model": model_id,
|
42 |
+
"User": user_id,
|
43 |
+
"Type": vote_type,
|
44 |
+
"Config": vote_data or {}
|
45 |
+
}
|
46 |
+
for line in LogFormatter.tree(stats, "Vote Details"):
|
47 |
+
logger.info(line)
|
48 |
+
|
49 |
+
await vote_service.initialize()
|
50 |
+
result = await vote_service.add_vote(model_id, user_id, vote_type, vote_data)
|
51 |
+
|
52 |
+
# Invalidate affected caches
|
53 |
+
try:
|
54 |
+
logger.info(LogFormatter.subsection("CACHE INVALIDATION"))
|
55 |
+
provider, model = model_id.split('/', 1)
|
56 |
+
|
57 |
+
# Build and invalidate cache keys
|
58 |
+
model_cache_key = build_cache_key("model_votes", provider, model)
|
59 |
+
user_cache_key = build_cache_key("user_votes", user_id)
|
60 |
+
|
61 |
+
await invalidate_cache_key(model_cache_key)
|
62 |
+
await invalidate_cache_key(user_cache_key)
|
63 |
+
|
64 |
+
cache_stats = {
|
65 |
+
"Model_Cache": model_cache_key,
|
66 |
+
"User_Cache": user_cache_key
|
67 |
+
}
|
68 |
+
for line in LogFormatter.tree(cache_stats, "Invalidated Caches"):
|
69 |
+
logger.info(line)
|
70 |
+
|
71 |
+
except Exception as e:
|
72 |
+
logger.error(LogFormatter.error("Failed to invalidate cache", e))
|
73 |
+
|
74 |
+
# Add cache control headers
|
75 |
+
response.headers["Cache-Control"] = "no-cache"
|
76 |
+
|
77 |
+
return result
|
78 |
+
except Exception as e:
|
79 |
+
logger.error(LogFormatter.error("Failed to add vote", e))
|
80 |
+
raise HTTPException(status_code=400, detail=str(e))
|
81 |
+
|
82 |
+
@router.get("/model/{provider}/{model}")
|
83 |
+
@cached(expire=CACHE_TTL, key_builder=model_votes_key_builder)
|
84 |
+
async def get_model_votes(
|
85 |
+
response: Response,
|
86 |
+
provider: str,
|
87 |
+
model: str
|
88 |
+
) -> Dict[str, Any]:
|
89 |
+
"""Get all votes for a specific model"""
|
90 |
+
try:
|
91 |
+
logger.info(LogFormatter.info(f"Fetching votes for model: {provider}/{model}"))
|
92 |
+
await vote_service.initialize()
|
93 |
+
model_id = f"{provider}/{model}"
|
94 |
+
result = await vote_service.get_model_votes(model_id)
|
95 |
+
|
96 |
+
# Add cache control headers
|
97 |
+
response.headers["Cache-Control"] = f"max-age={CACHE_TTL}"
|
98 |
+
response.headers["Last-Modified"] = vote_service._last_sync.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
99 |
+
|
100 |
+
logger.info(LogFormatter.success(f"Found {result.get('total_votes', 0)} votes"))
|
101 |
+
return result
|
102 |
+
except Exception as e:
|
103 |
+
logger.error(LogFormatter.error("Failed to get model votes", e))
|
104 |
+
raise HTTPException(status_code=400, detail=str(e))
|
105 |
+
|
106 |
+
@router.get("/user/{user_id}")
|
107 |
+
@cached(expire=CACHE_TTL, key_builder=user_votes_key_builder)
|
108 |
+
async def get_user_votes(
|
109 |
+
response: Response,
|
110 |
+
user_id: str
|
111 |
+
) -> List[Dict[str, Any]]:
|
112 |
+
"""Get all votes from a specific user"""
|
113 |
+
try:
|
114 |
+
logger.info(LogFormatter.info(f"Fetching votes for user: {user_id}"))
|
115 |
+
await vote_service.initialize()
|
116 |
+
votes = await vote_service.get_user_votes(user_id)
|
117 |
+
|
118 |
+
# Add cache control headers
|
119 |
+
response.headers["Cache-Control"] = f"max-age={CACHE_TTL}"
|
120 |
+
response.headers["Last-Modified"] = vote_service._last_sync.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
121 |
+
|
122 |
+
logger.info(LogFormatter.success(f"Found {len(votes)} votes"))
|
123 |
+
return votes
|
124 |
+
except Exception as e:
|
125 |
+
logger.error(LogFormatter.error("Failed to get user votes", e))
|
126 |
+
raise HTTPException(status_code=400, detail=str(e))
|
backend/app/api/router.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter
|
2 |
+
|
3 |
+
from app.api.endpoints import leaderboard, votes, models
|
4 |
+
|
5 |
+
router = APIRouter()
|
6 |
+
|
7 |
+
router.include_router(leaderboard.router, prefix="/leaderboard", tags=["leaderboard"])
|
8 |
+
router.include_router(votes.router, prefix="/votes", tags=["votes"])
|
9 |
+
router.include_router(models.router, prefix="/models", tags=["models"])
|
backend/app/asgi.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
ASGI entry point for the Open LLM Leaderboard API.
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import uvicorn
|
6 |
+
import logging
|
7 |
+
import logging.config
|
8 |
+
from fastapi import FastAPI
|
9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
10 |
+
from fastapi.middleware.gzip import GZipMiddleware
|
11 |
+
import sys
|
12 |
+
|
13 |
+
from app.api.router import router
|
14 |
+
from app.core.fastapi_cache import setup_cache
|
15 |
+
from app.core.formatting import LogFormatter
|
16 |
+
from app.config import hf_config
|
17 |
+
|
18 |
+
# Configure logging before anything else
|
19 |
+
LOGGING_CONFIG = {
|
20 |
+
"version": 1,
|
21 |
+
"disable_existing_loggers": True,
|
22 |
+
"formatters": {
|
23 |
+
"default": {
|
24 |
+
"format": "%(name)s - %(levelname)s - %(message)s",
|
25 |
+
}
|
26 |
+
},
|
27 |
+
"handlers": {
|
28 |
+
"default": {
|
29 |
+
"formatter": "default",
|
30 |
+
"class": "logging.StreamHandler",
|
31 |
+
"stream": "ext://sys.stdout",
|
32 |
+
}
|
33 |
+
},
|
34 |
+
"loggers": {
|
35 |
+
"uvicorn": {
|
36 |
+
"handlers": ["default"],
|
37 |
+
"level": "WARNING",
|
38 |
+
"propagate": False,
|
39 |
+
},
|
40 |
+
"uvicorn.error": {
|
41 |
+
"level": "WARNING",
|
42 |
+
"handlers": ["default"],
|
43 |
+
"propagate": False,
|
44 |
+
},
|
45 |
+
"uvicorn.access": {
|
46 |
+
"handlers": ["default"],
|
47 |
+
"level": "WARNING",
|
48 |
+
"propagate": False,
|
49 |
+
},
|
50 |
+
"app": {
|
51 |
+
"handlers": ["default"],
|
52 |
+
"level": "WARNING",
|
53 |
+
"propagate": False,
|
54 |
+
}
|
55 |
+
},
|
56 |
+
"root": {
|
57 |
+
"handlers": ["default"],
|
58 |
+
"level": "WARNING",
|
59 |
+
}
|
60 |
+
}
|
61 |
+
|
62 |
+
# Apply logging configuration
|
63 |
+
logging.config.dictConfig(LOGGING_CONFIG)
|
64 |
+
logger = logging.getLogger("app")
|
65 |
+
|
66 |
+
# Create FastAPI application
|
67 |
+
app = FastAPI(
|
68 |
+
title="Open LLM Leaderboard",
|
69 |
+
version="1.0.0",
|
70 |
+
docs_url="/docs",
|
71 |
+
)
|
72 |
+
|
73 |
+
# Add CORS middleware
|
74 |
+
app.add_middleware(
|
75 |
+
CORSMiddleware,
|
76 |
+
allow_origins=["*"],
|
77 |
+
allow_credentials=True,
|
78 |
+
allow_methods=["*"],
|
79 |
+
allow_headers=["*"],
|
80 |
+
)
|
81 |
+
|
82 |
+
# Add GZIP compression
|
83 |
+
app.add_middleware(GZipMiddleware, minimum_size=500)
|
84 |
+
|
85 |
+
# Include API router
|
86 |
+
app.include_router(router, prefix="/api")
|
87 |
+
|
88 |
+
@app.on_event("startup")
|
89 |
+
async def startup_event():
|
90 |
+
"""Initialize services on startup"""
|
91 |
+
logger.info("\n")
|
92 |
+
logger.info(LogFormatter.section("APPLICATION STARTUP"))
|
93 |
+
|
94 |
+
# Log HF configuration
|
95 |
+
logger.info(LogFormatter.section("HUGGING FACE CONFIGURATION"))
|
96 |
+
logger.info(LogFormatter.info(f"Organization: {hf_config.HF_ORGANIZATION}"))
|
97 |
+
logger.info(LogFormatter.info(f"Token Status: {'Present' if hf_config.HF_TOKEN else 'Missing'}"))
|
98 |
+
logger.info(LogFormatter.info(f"Using repositories:"))
|
99 |
+
logger.info(LogFormatter.info(f" - Queue: {hf_config.QUEUE_REPO}"))
|
100 |
+
logger.info(LogFormatter.info(f" - Aggregated: {hf_config.AGGREGATED_REPO}"))
|
101 |
+
logger.info(LogFormatter.info(f" - Votes: {hf_config.VOTES_REPO}"))
|
102 |
+
logger.info(LogFormatter.info(f" - Official Providers: {hf_config.OFFICIAL_PROVIDERS_REPO}"))
|
103 |
+
|
104 |
+
# Setup cache
|
105 |
+
setup_cache()
|
106 |
+
logger.info(LogFormatter.success("FastAPI Cache initialized with in-memory backend"))
|
backend/app/config/__init__.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Configuration module for the Open LLM Leaderboard backend.
|
3 |
+
All configuration values are imported from base.py to avoid circular dependencies.
|
4 |
+
"""
|
5 |
+
|
6 |
+
from .base import *
|
backend/app/config/base.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
# Server configuration
|
5 |
+
HOST = "0.0.0.0"
|
6 |
+
PORT = 7860
|
7 |
+
WORKERS = 4
|
8 |
+
RELOAD = True if os.environ.get("ENVIRONMENT") == "development" else False
|
9 |
+
|
10 |
+
# CORS configuration
|
11 |
+
ORIGINS = ["http://localhost:3000"] if os.getenv("ENVIRONMENT") == "development" else ["*"]
|
12 |
+
|
13 |
+
# Cache configuration
|
14 |
+
CACHE_TTL = int(os.environ.get("CACHE_TTL", 300)) # 5 minutes default
|
15 |
+
|
16 |
+
# Rate limiting
|
17 |
+
RATE_LIMIT_PERIOD = 7 # days
|
18 |
+
RATE_LIMIT_QUOTA = 5
|
19 |
+
HAS_HIGHER_RATE_LIMIT = []
|
20 |
+
|
21 |
+
# HuggingFace configuration
|
22 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
23 |
+
HF_ORGANIZATION = "open-llm-leaderboard"
|
24 |
+
API = {
|
25 |
+
"INFERENCE": "https://api-inference.huggingface.co/models",
|
26 |
+
"HUB": "https://huggingface.co"
|
27 |
+
}
|
28 |
+
|
29 |
+
# Cache paths
|
30 |
+
CACHE_ROOT = Path(os.environ.get("HF_HOME", ".cache"))
|
31 |
+
DATASETS_CACHE = CACHE_ROOT / "datasets"
|
32 |
+
MODELS_CACHE = CACHE_ROOT / "models"
|
33 |
+
VOTES_CACHE = CACHE_ROOT / "votes"
|
34 |
+
EVAL_CACHE = CACHE_ROOT / "eval-queue"
|
35 |
+
|
36 |
+
# Repository configuration
|
37 |
+
QUEUE_REPO = f"{HF_ORGANIZATION}/requests"
|
38 |
+
EVAL_REQUESTS_PATH = EVAL_CACHE / "eval_requests.jsonl"
|
backend/app/config/hf_config.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
from typing import Optional
|
4 |
+
from huggingface_hub import HfApi
|
5 |
+
from pathlib import Path
|
6 |
+
from app.core.cache import cache_config
|
7 |
+
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
|
10 |
+
# Organization or user who owns the datasets
|
11 |
+
HF_ORGANIZATION = "open-llm-leaderboard"
|
12 |
+
|
13 |
+
# Get HF token directly from environment
|
14 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
15 |
+
if not HF_TOKEN:
|
16 |
+
logger.warning("HF_TOKEN not found in environment variables. Some features may be limited.")
|
17 |
+
|
18 |
+
# Initialize HF API
|
19 |
+
API = HfApi(token=HF_TOKEN)
|
20 |
+
|
21 |
+
# Repository configuration
|
22 |
+
QUEUE_REPO = f"{HF_ORGANIZATION}/requests"
|
23 |
+
AGGREGATED_REPO = f"{HF_ORGANIZATION}/contents"
|
24 |
+
VOTES_REPO = f"{HF_ORGANIZATION}/votes"
|
25 |
+
OFFICIAL_PROVIDERS_REPO = f"{HF_ORGANIZATION}/official-providers"
|
26 |
+
|
27 |
+
# File paths from cache config
|
28 |
+
VOTES_PATH = cache_config.votes_file
|
29 |
+
EVAL_REQUESTS_PATH = cache_config.eval_requests_file
|
30 |
+
MODEL_CACHE_DIR = cache_config.models_cache
|
backend/app/config/logging_config.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import sys
|
3 |
+
from tqdm import tqdm
|
4 |
+
|
5 |
+
def get_tqdm_handler():
|
6 |
+
"""
|
7 |
+
Creates a special handler for tqdm that doesn't interfere with other logs.
|
8 |
+
"""
|
9 |
+
class TqdmLoggingHandler(logging.Handler):
|
10 |
+
def emit(self, record):
|
11 |
+
try:
|
12 |
+
msg = self.format(record)
|
13 |
+
tqdm.write(msg)
|
14 |
+
self.flush()
|
15 |
+
except Exception:
|
16 |
+
self.handleError(record)
|
17 |
+
|
18 |
+
return TqdmLoggingHandler()
|
19 |
+
|
20 |
+
def setup_service_logger(service_name: str) -> logging.Logger:
|
21 |
+
"""
|
22 |
+
Configure a specific logger for a given service.
|
23 |
+
"""
|
24 |
+
logger = logging.getLogger(f"app.services.{service_name}")
|
25 |
+
|
26 |
+
# If the logger already has handlers, don't reconfigure it
|
27 |
+
if logger.handlers:
|
28 |
+
return logger
|
29 |
+
|
30 |
+
# Add tqdm handler for this service
|
31 |
+
tqdm_handler = get_tqdm_handler()
|
32 |
+
tqdm_handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
|
33 |
+
logger.addHandler(tqdm_handler)
|
34 |
+
|
35 |
+
# Don't propagate logs to parent loggers
|
36 |
+
logger.propagate = False
|
37 |
+
|
38 |
+
return logger
|
backend/app/core/cache.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
from pathlib import Path
|
4 |
+
from datetime import timedelta
|
5 |
+
import logging
|
6 |
+
from app.core.formatting import LogFormatter
|
7 |
+
from app.config.base import (
|
8 |
+
CACHE_ROOT,
|
9 |
+
DATASETS_CACHE,
|
10 |
+
MODELS_CACHE,
|
11 |
+
VOTES_CACHE,
|
12 |
+
EVAL_CACHE,
|
13 |
+
CACHE_TTL
|
14 |
+
)
|
15 |
+
|
16 |
+
logger = logging.getLogger(__name__)
|
17 |
+
|
18 |
+
class CacheConfig:
|
19 |
+
def __init__(self):
|
20 |
+
# Get cache paths from config
|
21 |
+
self.cache_root = CACHE_ROOT
|
22 |
+
self.datasets_cache = DATASETS_CACHE
|
23 |
+
self.models_cache = MODELS_CACHE
|
24 |
+
self.votes_cache = VOTES_CACHE
|
25 |
+
self.eval_cache = EVAL_CACHE
|
26 |
+
|
27 |
+
# Specific files
|
28 |
+
self.votes_file = self.votes_cache / "votes_data.jsonl"
|
29 |
+
self.eval_requests_file = self.eval_cache / "eval_requests.jsonl"
|
30 |
+
|
31 |
+
# Cache TTL
|
32 |
+
self.cache_ttl = timedelta(seconds=CACHE_TTL)
|
33 |
+
|
34 |
+
self._initialize_cache_dirs()
|
35 |
+
self._setup_environment()
|
36 |
+
|
37 |
+
def _initialize_cache_dirs(self):
|
38 |
+
"""Initialize all necessary cache directories"""
|
39 |
+
try:
|
40 |
+
logger.info(LogFormatter.section("CACHE INITIALIZATION"))
|
41 |
+
|
42 |
+
cache_dirs = {
|
43 |
+
"Root": self.cache_root,
|
44 |
+
"Datasets": self.datasets_cache,
|
45 |
+
"Models": self.models_cache,
|
46 |
+
"Votes": self.votes_cache,
|
47 |
+
"Eval": self.eval_cache
|
48 |
+
}
|
49 |
+
|
50 |
+
for name, cache_dir in cache_dirs.items():
|
51 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
52 |
+
logger.info(LogFormatter.success(f"{name} cache directory: {cache_dir}"))
|
53 |
+
|
54 |
+
except Exception as e:
|
55 |
+
logger.error(LogFormatter.error("Failed to create cache directories", e))
|
56 |
+
raise
|
57 |
+
|
58 |
+
def _setup_environment(self):
|
59 |
+
"""Configure HuggingFace environment variables"""
|
60 |
+
logger.info(LogFormatter.subsection("ENVIRONMENT SETUP"))
|
61 |
+
|
62 |
+
env_vars = {
|
63 |
+
"HF_HOME": str(self.cache_root),
|
64 |
+
"HF_DATASETS_CACHE": str(self.datasets_cache)
|
65 |
+
}
|
66 |
+
|
67 |
+
for var, value in env_vars.items():
|
68 |
+
os.environ[var] = value
|
69 |
+
logger.info(LogFormatter.info(f"Set {var}={value}"))
|
70 |
+
|
71 |
+
|
72 |
+
def get_cache_path(self, cache_type: str) -> Path:
|
73 |
+
"""Returns the path for a specific cache type"""
|
74 |
+
cache_paths = {
|
75 |
+
"datasets": self.datasets_cache,
|
76 |
+
"models": self.models_cache,
|
77 |
+
"votes": self.votes_cache,
|
78 |
+
"eval": self.eval_cache
|
79 |
+
}
|
80 |
+
return cache_paths.get(cache_type, self.cache_root)
|
81 |
+
|
82 |
+
def flush_cache(self, cache_type: str = None):
|
83 |
+
"""Flush specified cache or all caches if no type is specified"""
|
84 |
+
try:
|
85 |
+
if cache_type:
|
86 |
+
logger.info(LogFormatter.section(f"FLUSHING {cache_type.upper()} CACHE"))
|
87 |
+
cache_dir = self.get_cache_path(cache_type)
|
88 |
+
if cache_dir.exists():
|
89 |
+
stats = {
|
90 |
+
"Cache_Type": cache_type,
|
91 |
+
"Directory": str(cache_dir)
|
92 |
+
}
|
93 |
+
for line in LogFormatter.tree(stats, "Cache Details"):
|
94 |
+
logger.info(line)
|
95 |
+
shutil.rmtree(cache_dir)
|
96 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
97 |
+
logger.info(LogFormatter.success("Cache cleared successfully"))
|
98 |
+
else:
|
99 |
+
logger.info(LogFormatter.section("FLUSHING ALL CACHES"))
|
100 |
+
for cache_type in ["datasets", "models", "votes", "eval"]:
|
101 |
+
self.flush_cache(cache_type)
|
102 |
+
logger.info(LogFormatter.success("All caches cleared successfully"))
|
103 |
+
|
104 |
+
except Exception as e:
|
105 |
+
logger.error(LogFormatter.error("Failed to flush cache", e))
|
106 |
+
raise
|
107 |
+
|
108 |
+
# Singleton instance of cache configuration
|
109 |
+
cache_config = CacheConfig()
|
backend/app/core/fastapi_cache.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi_cache import FastAPICache
|
2 |
+
from fastapi_cache.backends.inmemory import InMemoryBackend
|
3 |
+
from fastapi_cache.decorator import cache
|
4 |
+
from datetime import timedelta
|
5 |
+
from app.config import CACHE_TTL
|
6 |
+
import logging
|
7 |
+
from app.core.formatting import LogFormatter
|
8 |
+
from typing import Optional, Any
|
9 |
+
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
+
|
12 |
+
class CustomInMemoryBackend(InMemoryBackend):
|
13 |
+
def __init__(self):
|
14 |
+
"""Initialize the cache backend"""
|
15 |
+
super().__init__()
|
16 |
+
self.cache = {}
|
17 |
+
|
18 |
+
async def delete(self, key: str) -> bool:
|
19 |
+
"""Delete a key from the cache"""
|
20 |
+
try:
|
21 |
+
if key in self.cache:
|
22 |
+
del self.cache[key]
|
23 |
+
return True
|
24 |
+
return False
|
25 |
+
except Exception as e:
|
26 |
+
logger.error(LogFormatter.error(f"Failed to delete key {key} from cache", e))
|
27 |
+
return False
|
28 |
+
|
29 |
+
async def get(self, key: str) -> Any:
|
30 |
+
"""Get a value from the cache"""
|
31 |
+
return self.cache.get(key)
|
32 |
+
|
33 |
+
async def set(self, key: str, value: Any, expire: Optional[int] = None) -> None:
|
34 |
+
"""Set a value in the cache"""
|
35 |
+
self.cache[key] = value
|
36 |
+
|
37 |
+
def setup_cache():
|
38 |
+
"""Initialize FastAPI Cache with in-memory backend"""
|
39 |
+
try:
|
40 |
+
logger.info(LogFormatter.section("CACHE INITIALIZATION"))
|
41 |
+
FastAPICache.init(
|
42 |
+
backend=CustomInMemoryBackend(),
|
43 |
+
prefix="fastapi-cache"
|
44 |
+
)
|
45 |
+
logger.info(LogFormatter.success("Cache initialized successfully"))
|
46 |
+
except Exception as e:
|
47 |
+
logger.error(LogFormatter.error("Failed to initialize cache", e))
|
48 |
+
raise
|
49 |
+
|
50 |
+
async def invalidate_cache_key(key: str):
|
51 |
+
"""Invalidate a specific cache key"""
|
52 |
+
try:
|
53 |
+
backend = FastAPICache.get_backend()
|
54 |
+
if hasattr(backend, 'delete'):
|
55 |
+
await backend.delete(key)
|
56 |
+
logger.info(LogFormatter.success(f"Cache invalidated for key: {key}"))
|
57 |
+
else:
|
58 |
+
logger.warning(LogFormatter.warning("Cache backend does not support deletion"))
|
59 |
+
except Exception as e:
|
60 |
+
logger.error(LogFormatter.error(f"Failed to invalidate cache key: {key}", e))
|
61 |
+
|
62 |
+
def build_cache_key(*args) -> str:
|
63 |
+
"""Build a cache key from multiple arguments"""
|
64 |
+
return ":".join(str(arg) for arg in args if arg is not None)
|
65 |
+
|
66 |
+
def cached(expire: int = CACHE_TTL, key_builder=None):
|
67 |
+
"""Decorator for caching endpoint responses
|
68 |
+
|
69 |
+
Args:
|
70 |
+
expire (int): Cache TTL in seconds
|
71 |
+
key_builder (callable, optional): Custom key builder function
|
72 |
+
"""
|
73 |
+
return cache(
|
74 |
+
expire=expire,
|
75 |
+
key_builder=key_builder
|
76 |
+
)
|
backend/app/core/formatting.py
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from typing import Dict, Any, List, Optional
|
3 |
+
|
4 |
+
logger = logging.getLogger(__name__)
|
5 |
+
|
6 |
+
class LogFormatter:
|
7 |
+
"""Utility class for consistent log formatting across the application"""
|
8 |
+
|
9 |
+
@staticmethod
|
10 |
+
def section(title: str) -> str:
|
11 |
+
"""Create a section header"""
|
12 |
+
return f"\n{'='*20} {title.upper()} {'='*20}"
|
13 |
+
|
14 |
+
@staticmethod
|
15 |
+
def subsection(title: str) -> str:
|
16 |
+
"""Create a subsection header"""
|
17 |
+
return f"\n{'─'*20} {title} {'─'*20}"
|
18 |
+
|
19 |
+
@staticmethod
|
20 |
+
def tree(items: Dict[str, Any], title: str = None) -> List[str]:
|
21 |
+
"""Create a tree view of dictionary data"""
|
22 |
+
lines = []
|
23 |
+
if title:
|
24 |
+
lines.append(f"📊 {title}:")
|
25 |
+
|
26 |
+
# Get the maximum length for alignment
|
27 |
+
max_key_length = max(len(str(k)) for k in items.keys())
|
28 |
+
|
29 |
+
# Format each item
|
30 |
+
for i, (key, value) in enumerate(items.items()):
|
31 |
+
prefix = "└──" if i == len(items) - 1 else "├──"
|
32 |
+
if isinstance(value, (int, float)):
|
33 |
+
value = f"{value:,}" # Add thousand separators
|
34 |
+
lines.append(f"{prefix} {str(key):<{max_key_length}}: {value}")
|
35 |
+
|
36 |
+
return lines
|
37 |
+
|
38 |
+
@staticmethod
|
39 |
+
def stats(stats: Dict[str, int], title: str = None) -> List[str]:
|
40 |
+
"""Format statistics with icons"""
|
41 |
+
lines = []
|
42 |
+
if title:
|
43 |
+
lines.append(f"📊 {title}:")
|
44 |
+
|
45 |
+
# Get the maximum length for alignment
|
46 |
+
max_key_length = max(len(str(k)) for k in stats.keys())
|
47 |
+
|
48 |
+
# Format each stat with an appropriate icon
|
49 |
+
icons = {
|
50 |
+
"total": "📌",
|
51 |
+
"success": "✅",
|
52 |
+
"error": "❌",
|
53 |
+
"pending": "⏳",
|
54 |
+
"processing": "⚙️",
|
55 |
+
"finished": "✨",
|
56 |
+
"evaluating": "🔄",
|
57 |
+
"downloads": "⬇️",
|
58 |
+
"files": "📁",
|
59 |
+
"cached": "💾",
|
60 |
+
"size": "📏",
|
61 |
+
"time": "⏱️",
|
62 |
+
"rate": "🚀"
|
63 |
+
}
|
64 |
+
|
65 |
+
# Format each item
|
66 |
+
for i, (key, value) in enumerate(stats.items()):
|
67 |
+
prefix = "└──" if i == len(stats) - 1 else "├──"
|
68 |
+
icon = icons.get(key.lower().split('_')[0], "•")
|
69 |
+
if isinstance(value, (int, float)):
|
70 |
+
value = f"{value:,}" # Add thousand separators
|
71 |
+
lines.append(f"{prefix} {icon} {str(key):<{max_key_length}}: {value}")
|
72 |
+
|
73 |
+
return lines
|
74 |
+
|
75 |
+
@staticmethod
|
76 |
+
def progress_bar(current: int, total: int, width: int = 20) -> str:
|
77 |
+
"""Create a progress bar"""
|
78 |
+
percentage = (current * 100) // total
|
79 |
+
filled = "█" * (percentage * width // 100)
|
80 |
+
empty = "░" * (width - len(filled))
|
81 |
+
return f"{filled}{empty} {percentage:3d}%"
|
82 |
+
|
83 |
+
@staticmethod
|
84 |
+
def error(message: str, error: Optional[Exception] = None) -> str:
|
85 |
+
"""Format error message"""
|
86 |
+
error_msg = f"\n❌ Error: {message}"
|
87 |
+
if error:
|
88 |
+
error_msg += f"\n └── Details: {str(error)}"
|
89 |
+
return error_msg
|
90 |
+
|
91 |
+
@staticmethod
|
92 |
+
def success(message: str) -> str:
|
93 |
+
"""Format success message"""
|
94 |
+
return f"✅ {message}"
|
95 |
+
|
96 |
+
@staticmethod
|
97 |
+
def warning(message: str) -> str:
|
98 |
+
"""Format warning message"""
|
99 |
+
return f"⚠️ {message}"
|
100 |
+
|
101 |
+
@staticmethod
|
102 |
+
def info(message: str) -> str:
|
103 |
+
"""Format info message"""
|
104 |
+
return f"ℹ️ {message}"
|
backend/app/main.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from app.config.logging_config import setup_logging
|
3 |
+
import logging
|
4 |
+
|
5 |
+
# Initialize logging configuration
|
6 |
+
setup_logging()
|
7 |
+
logger = logging.getLogger(__name__)
|
8 |
+
|
9 |
+
app = FastAPI(title="Open LLM Leaderboard API")
|
10 |
+
|
11 |
+
@app.on_event("startup")
|
12 |
+
async def startup_event():
|
13 |
+
logger.info("Starting up the application...")
|
14 |
+
|
15 |
+
# Import and include routers after app initialization
|
16 |
+
from app.api import models, votes
|
17 |
+
app.include_router(models.router, prefix="/api", tags=["models"])
|
18 |
+
app.include_router(votes.router, prefix="/api", tags=["votes"])
|
backend/app/services/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from . import hf_service, leaderboard, votes, models
|
2 |
+
|
3 |
+
__all__ = ["hf_service", "leaderboard", "votes", "models"]
|
backend/app/services/hf_service.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional
|
2 |
+
from huggingface_hub import HfApi
|
3 |
+
from app.config import HF_TOKEN, API
|
4 |
+
from app.core.cache import cache_config
|
5 |
+
from app.core.formatting import LogFormatter
|
6 |
+
import logging
|
7 |
+
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
|
10 |
+
class HuggingFaceService:
|
11 |
+
def __init__(self):
|
12 |
+
self.api = API
|
13 |
+
self.token = HF_TOKEN
|
14 |
+
self.cache_dir = cache_config.models_cache
|
15 |
+
|
16 |
+
async def check_authentication(self) -> bool:
|
17 |
+
"""Check if the HF token is valid"""
|
18 |
+
if not self.token:
|
19 |
+
return False
|
20 |
+
try:
|
21 |
+
logger.info(LogFormatter.info("Checking HF token validity..."))
|
22 |
+
self.api.get_token_permission()
|
23 |
+
logger.info(LogFormatter.success("HF token is valid"))
|
24 |
+
return True
|
25 |
+
except Exception as e:
|
26 |
+
logger.error(LogFormatter.error("HF token validation failed", e))
|
27 |
+
return False
|
28 |
+
|
29 |
+
async def get_user_info(self) -> Optional[dict]:
|
30 |
+
"""Get information about the authenticated user"""
|
31 |
+
try:
|
32 |
+
logger.info(LogFormatter.info("Fetching user information..."))
|
33 |
+
info = self.api.get_token_permission()
|
34 |
+
logger.info(LogFormatter.success(f"User info retrieved for: {info.get('user', 'Unknown')}"))
|
35 |
+
return info
|
36 |
+
except Exception as e:
|
37 |
+
logger.error(LogFormatter.error("Failed to get user info", e))
|
38 |
+
return None
|
39 |
+
|
40 |
+
def _log_repo_operation(self, operation: str, repo: str, details: str = None):
|
41 |
+
"""Helper to log repository operations"""
|
42 |
+
logger.info(LogFormatter.section(f"HF REPOSITORY OPERATION - {operation.upper()}"))
|
43 |
+
stats = {
|
44 |
+
"Operation": operation,
|
45 |
+
"Repository": repo,
|
46 |
+
}
|
47 |
+
if details:
|
48 |
+
stats["Details"] = details
|
49 |
+
for line in LogFormatter.tree(stats):
|
50 |
+
logger.info(line)
|
backend/app/services/leaderboard.py
ADDED
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.core.cache import cache_config
|
2 |
+
from datetime import datetime
|
3 |
+
from typing import List, Dict, Any
|
4 |
+
import datasets
|
5 |
+
from fastapi import HTTPException
|
6 |
+
import logging
|
7 |
+
from app.config.base import HF_ORGANIZATION
|
8 |
+
from app.core.formatting import LogFormatter
|
9 |
+
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
+
|
12 |
+
class LeaderboardService:
|
13 |
+
def __init__(self):
|
14 |
+
pass
|
15 |
+
|
16 |
+
async def fetch_raw_data(self) -> List[Dict[str, Any]]:
|
17 |
+
"""Fetch raw leaderboard data from HuggingFace dataset"""
|
18 |
+
try:
|
19 |
+
logger.info(LogFormatter.section("FETCHING LEADERBOARD DATA"))
|
20 |
+
logger.info(LogFormatter.info(f"Loading dataset from {HF_ORGANIZATION}/contents"))
|
21 |
+
|
22 |
+
dataset = datasets.load_dataset(
|
23 |
+
f"{HF_ORGANIZATION}/contents",
|
24 |
+
cache_dir=cache_config.get_cache_path("datasets")
|
25 |
+
)["train"]
|
26 |
+
|
27 |
+
df = dataset.to_pandas()
|
28 |
+
data = df.to_dict('records')
|
29 |
+
|
30 |
+
stats = {
|
31 |
+
"Total_Entries": len(data),
|
32 |
+
"Dataset_Size": f"{df.memory_usage(deep=True).sum() / 1024 / 1024:.1f}MB"
|
33 |
+
}
|
34 |
+
for line in LogFormatter.stats(stats, "Dataset Statistics"):
|
35 |
+
logger.info(line)
|
36 |
+
|
37 |
+
return data
|
38 |
+
|
39 |
+
except Exception as e:
|
40 |
+
logger.error(LogFormatter.error("Failed to fetch leaderboard data", e))
|
41 |
+
raise HTTPException(status_code=500, detail=str(e))
|
42 |
+
|
43 |
+
async def get_formatted_data(self) -> List[Dict[str, Any]]:
|
44 |
+
"""Get formatted leaderboard data"""
|
45 |
+
try:
|
46 |
+
logger.info(LogFormatter.section("FORMATTING LEADERBOARD DATA"))
|
47 |
+
|
48 |
+
raw_data = await self.fetch_raw_data()
|
49 |
+
formatted_data = []
|
50 |
+
type_counts = {}
|
51 |
+
error_count = 0
|
52 |
+
|
53 |
+
# Initialize progress tracking
|
54 |
+
total_items = len(raw_data)
|
55 |
+
logger.info(LogFormatter.info(f"Processing {total_items:,} entries..."))
|
56 |
+
|
57 |
+
for i, item in enumerate(raw_data, 1):
|
58 |
+
try:
|
59 |
+
formatted_item = await self.transform_data(item)
|
60 |
+
formatted_data.append(formatted_item)
|
61 |
+
|
62 |
+
# Count model types
|
63 |
+
model_type = formatted_item["model"]["type"]
|
64 |
+
type_counts[model_type] = type_counts.get(model_type, 0) + 1
|
65 |
+
|
66 |
+
except Exception as e:
|
67 |
+
error_count += 1
|
68 |
+
logger.error(LogFormatter.error(f"Failed to format entry {i}/{total_items}", e))
|
69 |
+
continue
|
70 |
+
|
71 |
+
# Log progress every 10%
|
72 |
+
if i % max(1, total_items // 10) == 0:
|
73 |
+
progress = (i / total_items) * 100
|
74 |
+
logger.info(LogFormatter.info(f"Progress: {LogFormatter.progress_bar(i, total_items)}"))
|
75 |
+
|
76 |
+
# Log final statistics
|
77 |
+
stats = {
|
78 |
+
"Total_Processed": total_items,
|
79 |
+
"Successful": len(formatted_data),
|
80 |
+
"Failed": error_count
|
81 |
+
}
|
82 |
+
logger.info(LogFormatter.section("PROCESSING SUMMARY"))
|
83 |
+
for line in LogFormatter.stats(stats, "Processing Statistics"):
|
84 |
+
logger.info(line)
|
85 |
+
|
86 |
+
# Log model type distribution
|
87 |
+
type_stats = {f"Type_{k}": v for k, v in type_counts.items()}
|
88 |
+
logger.info(LogFormatter.subsection("MODEL TYPE DISTRIBUTION"))
|
89 |
+
for line in LogFormatter.stats(type_stats):
|
90 |
+
logger.info(line)
|
91 |
+
|
92 |
+
return formatted_data
|
93 |
+
|
94 |
+
except Exception as e:
|
95 |
+
logger.error(LogFormatter.error("Failed to format leaderboard data", e))
|
96 |
+
raise HTTPException(status_code=500, detail=str(e))
|
97 |
+
|
98 |
+
async def transform_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
99 |
+
"""Transform raw data into the format expected by the frontend"""
|
100 |
+
try:
|
101 |
+
# Extract model name for logging
|
102 |
+
model_name = data.get("fullname", "Unknown")
|
103 |
+
logger.debug(LogFormatter.info(f"Transforming data for model: {model_name}"))
|
104 |
+
|
105 |
+
# Create unique ID combining model name, precision, sha and chat template status
|
106 |
+
unique_id = f"{data.get('fullname', 'Unknown')}_{data.get('Precision', 'Unknown')}_{data.get('Model sha', 'Unknown')}_{str(data.get('Chat Template', False))}"
|
107 |
+
|
108 |
+
evaluations = {
|
109 |
+
"ifeval": {
|
110 |
+
"name": "IFEval",
|
111 |
+
"value": data.get("IFEval Raw", 0),
|
112 |
+
"normalized_score": data.get("IFEval", 0)
|
113 |
+
},
|
114 |
+
"bbh": {
|
115 |
+
"name": "BBH",
|
116 |
+
"value": data.get("BBH Raw", 0),
|
117 |
+
"normalized_score": data.get("BBH", 0)
|
118 |
+
},
|
119 |
+
"math": {
|
120 |
+
"name": "MATH Level 5",
|
121 |
+
"value": data.get("MATH Lvl 5 Raw", 0),
|
122 |
+
"normalized_score": data.get("MATH Lvl 5", 0)
|
123 |
+
},
|
124 |
+
"gpqa": {
|
125 |
+
"name": "GPQA",
|
126 |
+
"value": data.get("GPQA Raw", 0),
|
127 |
+
"normalized_score": data.get("GPQA", 0)
|
128 |
+
},
|
129 |
+
"musr": {
|
130 |
+
"name": "MUSR",
|
131 |
+
"value": data.get("MUSR Raw", 0),
|
132 |
+
"normalized_score": data.get("MUSR", 0)
|
133 |
+
},
|
134 |
+
"mmlu_pro": {
|
135 |
+
"name": "MMLU-PRO",
|
136 |
+
"value": data.get("MMLU-PRO Raw", 0),
|
137 |
+
"normalized_score": data.get("MMLU-PRO", 0)
|
138 |
+
}
|
139 |
+
}
|
140 |
+
|
141 |
+
features = {
|
142 |
+
"is_not_available_on_hub": data.get("Available on the hub", False),
|
143 |
+
"is_merged": data.get("Merged", False),
|
144 |
+
"is_moe": data.get("MoE", False),
|
145 |
+
"is_flagged": data.get("Flagged", False),
|
146 |
+
"is_official_provider": data.get("Official Providers", False)
|
147 |
+
}
|
148 |
+
|
149 |
+
metadata = {
|
150 |
+
"upload_date": data.get("Upload To Hub Date"),
|
151 |
+
"submission_date": data.get("Submission Date"),
|
152 |
+
"generation": data.get("Generation"),
|
153 |
+
"base_model": data.get("Base Model"),
|
154 |
+
"hub_license": data.get("Hub License"),
|
155 |
+
"hub_hearts": data.get("Hub ❤️"),
|
156 |
+
"params_billions": data.get("#Params (B)"),
|
157 |
+
"co2_cost": data.get("CO₂ cost (kg)", 0)
|
158 |
+
}
|
159 |
+
|
160 |
+
# Clean model type by removing emojis if present
|
161 |
+
original_type = data.get("Type", "")
|
162 |
+
model_type = original_type.lower().strip()
|
163 |
+
|
164 |
+
# Remove emojis and parentheses
|
165 |
+
if "(" in model_type:
|
166 |
+
model_type = model_type.split("(")[0].strip()
|
167 |
+
model_type = ''.join(c for c in model_type if not c in '🔶🟢🟩💬🤝🌸 ')
|
168 |
+
|
169 |
+
# Map old model types to new ones
|
170 |
+
model_type_mapping = {
|
171 |
+
"fine-tuned": "fined-tuned-on-domain-specific-dataset",
|
172 |
+
"fine tuned": "fined-tuned-on-domain-specific-dataset",
|
173 |
+
"finetuned": "fined-tuned-on-domain-specific-dataset",
|
174 |
+
"fine_tuned": "fined-tuned-on-domain-specific-dataset",
|
175 |
+
"ft": "fined-tuned-on-domain-specific-dataset",
|
176 |
+
"finetuning": "fined-tuned-on-domain-specific-dataset",
|
177 |
+
"fine tuning": "fined-tuned-on-domain-specific-dataset",
|
178 |
+
"fine-tuning": "fined-tuned-on-domain-specific-dataset"
|
179 |
+
}
|
180 |
+
|
181 |
+
mapped_type = model_type_mapping.get(model_type.lower().strip(), model_type)
|
182 |
+
|
183 |
+
if mapped_type != model_type:
|
184 |
+
logger.debug(LogFormatter.info(f"Model type mapped: {original_type} -> {mapped_type}"))
|
185 |
+
|
186 |
+
transformed_data = {
|
187 |
+
"id": unique_id,
|
188 |
+
"model": {
|
189 |
+
"name": data.get("fullname"),
|
190 |
+
"sha": data.get("Model sha"),
|
191 |
+
"precision": data.get("Precision"),
|
192 |
+
"type": mapped_type,
|
193 |
+
"weight_type": data.get("Weight type"),
|
194 |
+
"architecture": data.get("Architecture"),
|
195 |
+
"average_score": data.get("Average ⬆️"),
|
196 |
+
"has_chat_template": data.get("Chat Template", False)
|
197 |
+
},
|
198 |
+
"evaluations": evaluations,
|
199 |
+
"features": features,
|
200 |
+
"metadata": metadata
|
201 |
+
}
|
202 |
+
|
203 |
+
logger.debug(LogFormatter.success(f"Successfully transformed data for {model_name}"))
|
204 |
+
return transformed_data
|
205 |
+
|
206 |
+
except Exception as e:
|
207 |
+
logger.error(LogFormatter.error(f"Failed to transform data for {data.get('fullname', 'Unknown')}", e))
|
208 |
+
raise
|
backend/app/services/models.py
ADDED
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone, timedelta
|
2 |
+
from typing import Dict, Any, Optional, List
|
3 |
+
import json
|
4 |
+
import os
|
5 |
+
from pathlib import Path
|
6 |
+
import logging
|
7 |
+
import aiohttp
|
8 |
+
import asyncio
|
9 |
+
import time
|
10 |
+
from huggingface_hub import HfApi, CommitOperationAdd
|
11 |
+
from huggingface_hub.utils import build_hf_headers
|
12 |
+
from datasets import disable_progress_bar
|
13 |
+
import sys
|
14 |
+
import contextlib
|
15 |
+
from concurrent.futures import ThreadPoolExecutor
|
16 |
+
import tempfile
|
17 |
+
|
18 |
+
from app.config import (
|
19 |
+
QUEUE_REPO,
|
20 |
+
HF_TOKEN,
|
21 |
+
EVAL_REQUESTS_PATH
|
22 |
+
)
|
23 |
+
from app.config.hf_config import HF_ORGANIZATION
|
24 |
+
from app.services.hf_service import HuggingFaceService
|
25 |
+
from app.utils.model_validation import ModelValidator
|
26 |
+
from app.services.votes import VoteService
|
27 |
+
from app.core.cache import cache_config
|
28 |
+
from app.core.formatting import LogFormatter
|
29 |
+
|
30 |
+
# Disable datasets progress bars globally
|
31 |
+
disable_progress_bar()
|
32 |
+
|
33 |
+
logger = logging.getLogger(__name__)
|
34 |
+
|
35 |
+
# Context manager to temporarily disable stdout and stderr
|
36 |
+
@contextlib.contextmanager
|
37 |
+
def suppress_output():
|
38 |
+
stdout = sys.stdout
|
39 |
+
stderr = sys.stderr
|
40 |
+
devnull = open(os.devnull, 'w')
|
41 |
+
try:
|
42 |
+
sys.stdout = devnull
|
43 |
+
sys.stderr = devnull
|
44 |
+
yield
|
45 |
+
finally:
|
46 |
+
sys.stdout = stdout
|
47 |
+
sys.stderr = stderr
|
48 |
+
devnull.close()
|
49 |
+
|
50 |
+
class ProgressTracker:
|
51 |
+
def __init__(self, total: int, desc: str = "Progress", update_frequency: int = 10):
|
52 |
+
self.total = total
|
53 |
+
self.current = 0
|
54 |
+
self.desc = desc
|
55 |
+
self.start_time = time.time()
|
56 |
+
self.update_frequency = update_frequency # Percentage steps
|
57 |
+
self.last_update = -1
|
58 |
+
|
59 |
+
# Initial log with fancy formatting
|
60 |
+
logger.info(LogFormatter.section(desc))
|
61 |
+
logger.info(LogFormatter.info(f"Starting processing of {total:,} items..."))
|
62 |
+
sys.stdout.flush()
|
63 |
+
|
64 |
+
def update(self, n: int = 1):
|
65 |
+
self.current += n
|
66 |
+
current_percentage = (self.current * 100) // self.total
|
67 |
+
|
68 |
+
# Only update on frequency steps (e.g., 0%, 10%, 20%, etc.)
|
69 |
+
if current_percentage >= self.last_update + self.update_frequency or current_percentage == 100:
|
70 |
+
elapsed = time.time() - self.start_time
|
71 |
+
rate = self.current / elapsed if elapsed > 0 else 0
|
72 |
+
remaining = (self.total - self.current) / rate if rate > 0 else 0
|
73 |
+
|
74 |
+
# Create progress stats
|
75 |
+
stats = {
|
76 |
+
"Progress": LogFormatter.progress_bar(self.current, self.total),
|
77 |
+
"Items": f"{self.current:,}/{self.total:,}",
|
78 |
+
"Time": f"⏱️ {elapsed:.1f}s elapsed, {remaining:.1f}s remaining",
|
79 |
+
"Rate": f"🚀 {rate:.1f} items/s"
|
80 |
+
}
|
81 |
+
|
82 |
+
# Log progress using tree format
|
83 |
+
for line in LogFormatter.tree(stats):
|
84 |
+
logger.info(line)
|
85 |
+
sys.stdout.flush()
|
86 |
+
|
87 |
+
self.last_update = (current_percentage // self.update_frequency) * self.update_frequency
|
88 |
+
|
89 |
+
def close(self):
|
90 |
+
elapsed = time.time() - self.start_time
|
91 |
+
rate = self.total / elapsed if elapsed > 0 else 0
|
92 |
+
|
93 |
+
# Final summary with fancy formatting
|
94 |
+
logger.info(LogFormatter.section("COMPLETED"))
|
95 |
+
stats = {
|
96 |
+
"Total": f"{self.total:,} items",
|
97 |
+
"Time": f"{elapsed:.1f}s",
|
98 |
+
"Rate": f"{rate:.1f} items/s"
|
99 |
+
}
|
100 |
+
for line in LogFormatter.stats(stats):
|
101 |
+
logger.info(line)
|
102 |
+
logger.info("="*50)
|
103 |
+
sys.stdout.flush()
|
104 |
+
|
105 |
+
class ModelService(HuggingFaceService):
|
106 |
+
_instance: Optional['ModelService'] = None
|
107 |
+
_initialized = False
|
108 |
+
|
109 |
+
def __new__(cls):
|
110 |
+
if cls._instance is None:
|
111 |
+
logger.info(LogFormatter.info("Creating new ModelService instance"))
|
112 |
+
cls._instance = super(ModelService, cls).__new__(cls)
|
113 |
+
return cls._instance
|
114 |
+
|
115 |
+
def __init__(self):
|
116 |
+
if not hasattr(self, '_init_done'):
|
117 |
+
logger.info(LogFormatter.section("MODEL SERVICE INITIALIZATION"))
|
118 |
+
super().__init__()
|
119 |
+
self.validator = ModelValidator()
|
120 |
+
self.vote_service = VoteService()
|
121 |
+
self.eval_requests_path = cache_config.eval_requests_file
|
122 |
+
logger.info(LogFormatter.info(f"Using eval requests path: {self.eval_requests_path}"))
|
123 |
+
|
124 |
+
self.eval_requests_path.parent.mkdir(parents=True, exist_ok=True)
|
125 |
+
self.hf_api = HfApi(token=HF_TOKEN)
|
126 |
+
self.cached_models = None
|
127 |
+
self.last_cache_update = 0
|
128 |
+
self.cache_ttl = cache_config.cache_ttl.total_seconds()
|
129 |
+
self._init_done = True
|
130 |
+
logger.info(LogFormatter.success("Initialization complete"))
|
131 |
+
|
132 |
+
async def _download_and_process_file(self, file: str, session: aiohttp.ClientSession, progress: ProgressTracker) -> Optional[Dict]:
|
133 |
+
"""Download and process a file asynchronously"""
|
134 |
+
try:
|
135 |
+
# Build file URL
|
136 |
+
url = f"https://huggingface.co/datasets/{QUEUE_REPO}/resolve/main/{file}"
|
137 |
+
headers = build_hf_headers(token=self.token)
|
138 |
+
|
139 |
+
# Download file
|
140 |
+
async with session.get(url, headers=headers) as response:
|
141 |
+
if response.status != 200:
|
142 |
+
logger.error(LogFormatter.error(f"Failed to download {file}", f"HTTP {response.status}"))
|
143 |
+
progress.update()
|
144 |
+
return None
|
145 |
+
|
146 |
+
try:
|
147 |
+
# First read content as text
|
148 |
+
text_content = await response.text()
|
149 |
+
# Then parse JSON
|
150 |
+
content = json.loads(text_content)
|
151 |
+
except json.JSONDecodeError as e:
|
152 |
+
logger.error(LogFormatter.error(f"Failed to decode JSON from {file}", e))
|
153 |
+
progress.update()
|
154 |
+
return None
|
155 |
+
|
156 |
+
# Get status and determine target status
|
157 |
+
status = content.get("status", "PENDING").upper()
|
158 |
+
target_status = None
|
159 |
+
status_map = {
|
160 |
+
"PENDING": ["PENDING"],
|
161 |
+
"EVALUATING": ["RUNNING"],
|
162 |
+
"FINISHED": ["FINISHED"]
|
163 |
+
}
|
164 |
+
|
165 |
+
for target, source_statuses in status_map.items():
|
166 |
+
if status in source_statuses:
|
167 |
+
target_status = target
|
168 |
+
break
|
169 |
+
|
170 |
+
if not target_status:
|
171 |
+
progress.update()
|
172 |
+
return None
|
173 |
+
|
174 |
+
# Calculate wait time
|
175 |
+
try:
|
176 |
+
submit_time = datetime.fromisoformat(content["submitted_time"].replace("Z", "+00:00"))
|
177 |
+
if submit_time.tzinfo is None:
|
178 |
+
submit_time = submit_time.replace(tzinfo=timezone.utc)
|
179 |
+
current_time = datetime.now(timezone.utc)
|
180 |
+
wait_time = current_time - submit_time
|
181 |
+
|
182 |
+
model_info = {
|
183 |
+
"name": content["model"],
|
184 |
+
"submitter": content.get("sender", "Unknown"),
|
185 |
+
"revision": content["revision"],
|
186 |
+
"wait_time": f"{wait_time.total_seconds():.1f}s",
|
187 |
+
"submission_time": content["submitted_time"],
|
188 |
+
"status": target_status,
|
189 |
+
"precision": content.get("precision", "Unknown")
|
190 |
+
}
|
191 |
+
|
192 |
+
progress.update()
|
193 |
+
return model_info
|
194 |
+
|
195 |
+
except (ValueError, TypeError) as e:
|
196 |
+
logger.error(LogFormatter.error(f"Failed to process {file}", e))
|
197 |
+
progress.update()
|
198 |
+
return None
|
199 |
+
|
200 |
+
except Exception as e:
|
201 |
+
logger.error(LogFormatter.error(f"Failed to load {file}", e))
|
202 |
+
progress.update()
|
203 |
+
return None
|
204 |
+
|
205 |
+
async def _refresh_models_cache(self):
|
206 |
+
"""Refresh the models cache"""
|
207 |
+
try:
|
208 |
+
logger.info(LogFormatter.section("CACHE REFRESH"))
|
209 |
+
self._log_repo_operation("read", f"{HF_ORGANIZATION}/requests", "Refreshing models cache")
|
210 |
+
|
211 |
+
# Initialize models dictionary
|
212 |
+
models = {
|
213 |
+
"finished": [],
|
214 |
+
"evaluating": [],
|
215 |
+
"pending": []
|
216 |
+
}
|
217 |
+
|
218 |
+
try:
|
219 |
+
logger.info(LogFormatter.subsection("DATASET LOADING"))
|
220 |
+
logger.info(LogFormatter.info("Loading dataset..."))
|
221 |
+
|
222 |
+
# Download entire dataset snapshot
|
223 |
+
with suppress_output():
|
224 |
+
local_dir = self.hf_api.snapshot_download(
|
225 |
+
repo_id=QUEUE_REPO,
|
226 |
+
repo_type="dataset",
|
227 |
+
token=self.token
|
228 |
+
)
|
229 |
+
|
230 |
+
# List JSON files in local directory
|
231 |
+
local_path = Path(local_dir)
|
232 |
+
json_files = list(local_path.glob("**/*.json"))
|
233 |
+
total_files = len(json_files)
|
234 |
+
|
235 |
+
# Log repository stats
|
236 |
+
stats = {
|
237 |
+
"Total_Files": total_files,
|
238 |
+
"Local_Dir": str(local_path),
|
239 |
+
}
|
240 |
+
for line in LogFormatter.stats(stats, "Repository Statistics"):
|
241 |
+
logger.info(line)
|
242 |
+
|
243 |
+
if not json_files:
|
244 |
+
raise Exception("No JSON files found in repository")
|
245 |
+
|
246 |
+
# Initialize progress tracker
|
247 |
+
progress = ProgressTracker(total_files, "PROCESSING FILES")
|
248 |
+
|
249 |
+
# Process local files
|
250 |
+
model_submissions = {} # Dict to track latest submission for each (model_id, revision, precision)
|
251 |
+
for file_path in json_files:
|
252 |
+
try:
|
253 |
+
with open(file_path, 'r') as f:
|
254 |
+
content = json.load(f)
|
255 |
+
|
256 |
+
# Get status and determine target status
|
257 |
+
status = content.get("status", "PENDING").upper()
|
258 |
+
target_status = None
|
259 |
+
status_map = {
|
260 |
+
"PENDING": ["PENDING"],
|
261 |
+
"EVALUATING": ["RUNNING"],
|
262 |
+
"FINISHED": ["FINISHED"]
|
263 |
+
}
|
264 |
+
|
265 |
+
for target, source_statuses in status_map.items():
|
266 |
+
if status in source_statuses:
|
267 |
+
target_status = target
|
268 |
+
break
|
269 |
+
|
270 |
+
if not target_status:
|
271 |
+
progress.update()
|
272 |
+
continue
|
273 |
+
|
274 |
+
# Calculate wait time
|
275 |
+
try:
|
276 |
+
submit_time = datetime.fromisoformat(content["submitted_time"].replace("Z", "+00:00"))
|
277 |
+
if submit_time.tzinfo is None:
|
278 |
+
submit_time = submit_time.replace(tzinfo=timezone.utc)
|
279 |
+
current_time = datetime.now(timezone.utc)
|
280 |
+
wait_time = current_time - submit_time
|
281 |
+
|
282 |
+
model_info = {
|
283 |
+
"name": content["model"],
|
284 |
+
"submitter": content.get("sender", "Unknown"),
|
285 |
+
"revision": content["revision"],
|
286 |
+
"wait_time": f"{wait_time.total_seconds():.1f}s",
|
287 |
+
"submission_time": content["submitted_time"],
|
288 |
+
"status": target_status,
|
289 |
+
"precision": content.get("precision", "Unknown")
|
290 |
+
}
|
291 |
+
|
292 |
+
# Use (model_id, revision, precision) as key to track latest submission
|
293 |
+
key = (content["model"], content["revision"], content.get("precision", "Unknown"))
|
294 |
+
if key not in model_submissions or submit_time > datetime.fromisoformat(model_submissions[key]["submission_time"].replace("Z", "+00:00")):
|
295 |
+
model_submissions[key] = model_info
|
296 |
+
|
297 |
+
except (ValueError, TypeError) as e:
|
298 |
+
logger.error(LogFormatter.error(f"Failed to process {file_path.name}", e))
|
299 |
+
|
300 |
+
except Exception as e:
|
301 |
+
logger.error(LogFormatter.error(f"Failed to load {file_path.name}", e))
|
302 |
+
finally:
|
303 |
+
progress.update()
|
304 |
+
|
305 |
+
# Populate models dict with deduplicated submissions
|
306 |
+
for model_info in model_submissions.values():
|
307 |
+
models[model_info["status"].lower()].append(model_info)
|
308 |
+
|
309 |
+
progress.close()
|
310 |
+
|
311 |
+
# Final summary with fancy formatting
|
312 |
+
logger.info(LogFormatter.section("CACHE SUMMARY"))
|
313 |
+
stats = {
|
314 |
+
"Finished": len(models["finished"]),
|
315 |
+
"Evaluating": len(models["evaluating"]),
|
316 |
+
"Pending": len(models["pending"])
|
317 |
+
}
|
318 |
+
for line in LogFormatter.stats(stats, "Models by Status"):
|
319 |
+
logger.info(line)
|
320 |
+
logger.info("="*50)
|
321 |
+
|
322 |
+
except Exception as e:
|
323 |
+
logger.error(LogFormatter.error("Error processing files", e))
|
324 |
+
raise
|
325 |
+
|
326 |
+
# Update cache
|
327 |
+
self.cached_models = models
|
328 |
+
self.last_cache_update = time.time()
|
329 |
+
logger.info(LogFormatter.success("Cache updated successfully"))
|
330 |
+
|
331 |
+
return models
|
332 |
+
|
333 |
+
except Exception as e:
|
334 |
+
logger.error(LogFormatter.error("Cache refresh failed", e))
|
335 |
+
raise
|
336 |
+
|
337 |
+
async def initialize(self):
|
338 |
+
"""Initialize the model service"""
|
339 |
+
if self._initialized:
|
340 |
+
logger.info(LogFormatter.info("Service already initialized, using cached data"))
|
341 |
+
return
|
342 |
+
|
343 |
+
try:
|
344 |
+
logger.info(LogFormatter.section("MODEL SERVICE INITIALIZATION"))
|
345 |
+
|
346 |
+
# Check if cache already exists
|
347 |
+
cache_path = cache_config.get_cache_path("datasets")
|
348 |
+
if not cache_path.exists() or not any(cache_path.iterdir()):
|
349 |
+
logger.info(LogFormatter.info("No existing cache found, initializing datasets cache..."))
|
350 |
+
cache_config.flush_cache("datasets")
|
351 |
+
else:
|
352 |
+
logger.info(LogFormatter.info("Using existing datasets cache"))
|
353 |
+
|
354 |
+
# Ensure eval requests directory exists
|
355 |
+
self.eval_requests_path.parent.mkdir(parents=True, exist_ok=True)
|
356 |
+
logger.info(LogFormatter.info(f"Eval requests directory: {self.eval_requests_path}"))
|
357 |
+
|
358 |
+
# List existing files
|
359 |
+
if self.eval_requests_path.exists():
|
360 |
+
files = list(self.eval_requests_path.glob("**/*.json"))
|
361 |
+
stats = {
|
362 |
+
"Total_Files": len(files),
|
363 |
+
"Directory": str(self.eval_requests_path)
|
364 |
+
}
|
365 |
+
for line in LogFormatter.stats(stats, "Eval Requests"):
|
366 |
+
logger.info(line)
|
367 |
+
|
368 |
+
# Load initial cache
|
369 |
+
await self._refresh_models_cache()
|
370 |
+
|
371 |
+
self._initialized = True
|
372 |
+
logger.info(LogFormatter.success("Model service initialization complete"))
|
373 |
+
|
374 |
+
except Exception as e:
|
375 |
+
logger.error(LogFormatter.error("Initialization failed", e))
|
376 |
+
raise
|
377 |
+
|
378 |
+
async def get_models(self) -> Dict[str, List[Dict[str, Any]]]:
|
379 |
+
"""Get all models with their status"""
|
380 |
+
if not self._initialized:
|
381 |
+
logger.info(LogFormatter.info("Service not initialized, initializing now..."))
|
382 |
+
await self.initialize()
|
383 |
+
|
384 |
+
current_time = time.time()
|
385 |
+
cache_age = current_time - self.last_cache_update
|
386 |
+
|
387 |
+
# Check if cache needs refresh
|
388 |
+
if not self.cached_models:
|
389 |
+
logger.info(LogFormatter.info("No cached data available, refreshing cache..."))
|
390 |
+
return await self._refresh_models_cache()
|
391 |
+
elif cache_age > self.cache_ttl:
|
392 |
+
logger.info(LogFormatter.info(f"Cache expired ({cache_age:.1f}s old, TTL: {self.cache_ttl}s)"))
|
393 |
+
return await self._refresh_models_cache()
|
394 |
+
else:
|
395 |
+
logger.info(LogFormatter.info(f"Using cached data ({cache_age:.1f}s old)"))
|
396 |
+
return self.cached_models
|
397 |
+
|
398 |
+
async def submit_model(
|
399 |
+
self,
|
400 |
+
model_data: Dict[str, Any],
|
401 |
+
user_id: str
|
402 |
+
) -> Dict[str, Any]:
|
403 |
+
logger.info(LogFormatter.section("MODEL SUBMISSION"))
|
404 |
+
self._log_repo_operation("write", f"{HF_ORGANIZATION}/requests", f"Submitting model {model_data['model_id']} by {user_id}")
|
405 |
+
stats = {
|
406 |
+
"Model": model_data["model_id"],
|
407 |
+
"User": user_id,
|
408 |
+
"Revision": model_data["revision"],
|
409 |
+
"Precision": model_data["precision"],
|
410 |
+
"Type": model_data["model_type"]
|
411 |
+
}
|
412 |
+
for line in LogFormatter.tree(stats, "Submission Details"):
|
413 |
+
logger.info(line)
|
414 |
+
|
415 |
+
# Validate required fields
|
416 |
+
required_fields = [
|
417 |
+
"model_id", "base_model", "revision", "precision",
|
418 |
+
"weight_type", "model_type", "use_chat_template"
|
419 |
+
]
|
420 |
+
for field in required_fields:
|
421 |
+
if field not in model_data:
|
422 |
+
raise ValueError(f"Missing required field: {field}")
|
423 |
+
|
424 |
+
# Get model info and validate it exists on HuggingFace
|
425 |
+
try:
|
426 |
+
logger.info(LogFormatter.subsection("MODEL VALIDATION"))
|
427 |
+
|
428 |
+
# Get the model info to check if it exists
|
429 |
+
model_info = self.hf_api.model_info(
|
430 |
+
model_data["model_id"],
|
431 |
+
revision=model_data["revision"],
|
432 |
+
token=self.token
|
433 |
+
)
|
434 |
+
|
435 |
+
if not model_info:
|
436 |
+
raise Exception(f"Model {model_data['model_id']} not found on HuggingFace Hub")
|
437 |
+
|
438 |
+
logger.info(LogFormatter.success("Model exists on HuggingFace Hub"))
|
439 |
+
|
440 |
+
except Exception as e:
|
441 |
+
logger.error(LogFormatter.error("Model validation failed", e))
|
442 |
+
raise
|
443 |
+
|
444 |
+
# Update model revision with commit sha
|
445 |
+
model_data["revision"] = model_info.sha
|
446 |
+
|
447 |
+
# Check if model already exists in the system
|
448 |
+
try:
|
449 |
+
logger.info(LogFormatter.subsection("CHECKING EXISTING SUBMISSIONS"))
|
450 |
+
existing_models = await self.get_models()
|
451 |
+
|
452 |
+
# Call the official provider status check
|
453 |
+
is_valid, error_message = await self.validator.check_official_provider_status(
|
454 |
+
model_data["model_id"],
|
455 |
+
existing_models
|
456 |
+
)
|
457 |
+
if not is_valid:
|
458 |
+
raise ValueError(error_message)
|
459 |
+
|
460 |
+
# Check in all statuses (pending, evaluating, finished)
|
461 |
+
for status, models in existing_models.items():
|
462 |
+
for model in models:
|
463 |
+
if model["name"] == model_data["model_id"] and model["revision"] == model_data["revision"]:
|
464 |
+
error_msg = f"Model {model_data['model_id']} revision {model_data['revision']} is already in the system with status: {status}"
|
465 |
+
logger.error(LogFormatter.error("Submission rejected", error_msg))
|
466 |
+
raise ValueError(error_msg)
|
467 |
+
|
468 |
+
logger.info(LogFormatter.success("No existing submission found"))
|
469 |
+
except ValueError:
|
470 |
+
raise
|
471 |
+
except Exception as e:
|
472 |
+
logger.error(LogFormatter.error("Failed to check existing submissions", e))
|
473 |
+
raise
|
474 |
+
|
475 |
+
# Check that model on hub and valid
|
476 |
+
valid, error, model_config = await self.validator.is_model_on_hub(
|
477 |
+
model_data["model_id"],
|
478 |
+
model_data["revision"],
|
479 |
+
test_tokenizer=True
|
480 |
+
)
|
481 |
+
if not valid:
|
482 |
+
logger.error(LogFormatter.error("Model on hub validation failed", error))
|
483 |
+
raise Exception(error)
|
484 |
+
logger.info(LogFormatter.success("Model on hub validation passed"))
|
485 |
+
|
486 |
+
# Validate model card
|
487 |
+
valid, error, model_card = await self.validator.check_model_card(
|
488 |
+
model_data["model_id"]
|
489 |
+
)
|
490 |
+
if not valid:
|
491 |
+
logger.error(LogFormatter.error("Model card validation failed", error))
|
492 |
+
raise Exception(error)
|
493 |
+
logger.info(LogFormatter.success("Model card validation passed"))
|
494 |
+
|
495 |
+
# Check size limits
|
496 |
+
model_size, error = await self.validator.get_model_size(
|
497 |
+
model_info,
|
498 |
+
model_data["precision"],
|
499 |
+
model_data["base_model"],
|
500 |
+
revision=model_data["revision"]
|
501 |
+
)
|
502 |
+
if model_size is None:
|
503 |
+
logger.error(LogFormatter.error("Model size validation failed", error))
|
504 |
+
raise Exception(error)
|
505 |
+
logger.info(LogFormatter.success(f"Model size validation passed: {model_size:.1f}B"))
|
506 |
+
|
507 |
+
# Size limits based on precision
|
508 |
+
if model_data["precision"] in ["float16", "bfloat16"] and model_size > 100:
|
509 |
+
error_msg = f"Model too large for {model_data['precision']} (limit: 100B)"
|
510 |
+
logger.error(LogFormatter.error("Size limit exceeded", error_msg))
|
511 |
+
raise Exception(error_msg)
|
512 |
+
|
513 |
+
# Chat template validation if requested
|
514 |
+
if model_data["use_chat_template"]:
|
515 |
+
valid, error = await self.validator.check_chat_template(
|
516 |
+
model_data["model_id"],
|
517 |
+
model_data["revision"]
|
518 |
+
)
|
519 |
+
if not valid:
|
520 |
+
logger.error(LogFormatter.error("Chat template validation failed", error))
|
521 |
+
raise Exception(error)
|
522 |
+
logger.info(LogFormatter.success("Chat template validation passed"))
|
523 |
+
|
524 |
+
|
525 |
+
architectures = model_info.config.get("architectures", "")
|
526 |
+
if architectures:
|
527 |
+
architectures = ";".join(architectures)
|
528 |
+
|
529 |
+
# Create eval entry
|
530 |
+
eval_entry = {
|
531 |
+
"model": model_data["model_id"],
|
532 |
+
"base_model": model_data["base_model"],
|
533 |
+
"revision": model_info.sha,
|
534 |
+
"precision": model_data["precision"],
|
535 |
+
"params": model_size,
|
536 |
+
"architectures": architectures,
|
537 |
+
"weight_type": model_data["weight_type"],
|
538 |
+
"status": "PENDING",
|
539 |
+
"submitted_time": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
540 |
+
"model_type": model_data["model_type"],
|
541 |
+
"job_id": -1,
|
542 |
+
"job_start_time": None,
|
543 |
+
"use_chat_template": model_data["use_chat_template"],
|
544 |
+
"sender": user_id
|
545 |
+
}
|
546 |
+
|
547 |
+
logger.info(LogFormatter.subsection("EVALUATION ENTRY"))
|
548 |
+
for line in LogFormatter.tree(eval_entry):
|
549 |
+
logger.info(line)
|
550 |
+
|
551 |
+
# Upload to HF dataset
|
552 |
+
try:
|
553 |
+
logger.info(LogFormatter.subsection("UPLOADING TO HUGGINGFACE"))
|
554 |
+
logger.info(LogFormatter.info(f"Uploading to {HF_ORGANIZATION}/requests..."))
|
555 |
+
|
556 |
+
# Construct the path in the dataset
|
557 |
+
org_or_user = model_data["model_id"].split("/")[0] if "/" in model_data["model_id"] else ""
|
558 |
+
model_path = model_data["model_id"].split("/")[-1]
|
559 |
+
relative_path = f"{org_or_user}/{model_path}_eval_request_False_{model_data['precision']}_{model_data['weight_type']}.json"
|
560 |
+
|
561 |
+
# Create a temporary file with the request
|
562 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as temp_file:
|
563 |
+
json.dump(eval_entry, temp_file, indent=2)
|
564 |
+
temp_file.flush()
|
565 |
+
temp_path = temp_file.name
|
566 |
+
|
567 |
+
# Upload file directly
|
568 |
+
self.hf_api.upload_file(
|
569 |
+
path_or_fileobj=temp_path,
|
570 |
+
path_in_repo=relative_path,
|
571 |
+
repo_id=f"{HF_ORGANIZATION}/requests",
|
572 |
+
repo_type="dataset",
|
573 |
+
commit_message=f"Add {model_data['model_id']} to eval queue",
|
574 |
+
token=self.token
|
575 |
+
)
|
576 |
+
|
577 |
+
# Clean up temp file
|
578 |
+
os.unlink(temp_path)
|
579 |
+
|
580 |
+
logger.info(LogFormatter.success("Upload successful"))
|
581 |
+
|
582 |
+
except Exception as e:
|
583 |
+
logger.error(LogFormatter.error("Upload failed", e))
|
584 |
+
raise
|
585 |
+
|
586 |
+
# Add automatic vote
|
587 |
+
try:
|
588 |
+
logger.info(LogFormatter.subsection("AUTOMATIC VOTE"))
|
589 |
+
logger.info(LogFormatter.info(f"Adding upvote for {model_data['model_id']} by {user_id}"))
|
590 |
+
await self.vote_service.add_vote(
|
591 |
+
model_data["model_id"],
|
592 |
+
user_id,
|
593 |
+
"up",
|
594 |
+
{
|
595 |
+
"precision": model_data["precision"],
|
596 |
+
"revision": model_data["revision"]
|
597 |
+
}
|
598 |
+
)
|
599 |
+
logger.info(LogFormatter.success("Vote recorded successfully"))
|
600 |
+
except Exception as e:
|
601 |
+
logger.error(LogFormatter.error("Failed to record vote", e))
|
602 |
+
# Don't raise here as the main submission was successful
|
603 |
+
|
604 |
+
return {
|
605 |
+
"status": "success",
|
606 |
+
"message": "The model was submitted successfully, and the vote has been recorded"
|
607 |
+
}
|
608 |
+
|
609 |
+
async def get_model_status(self, model_id: str) -> Dict[str, Any]:
|
610 |
+
"""Get evaluation status of a model"""
|
611 |
+
logger.info(LogFormatter.info(f"Checking status for model: {model_id}"))
|
612 |
+
eval_path = self.eval_requests_path
|
613 |
+
|
614 |
+
for user_folder in eval_path.iterdir():
|
615 |
+
if user_folder.is_dir():
|
616 |
+
for file in user_folder.glob("*.json"):
|
617 |
+
with open(file, "r") as f:
|
618 |
+
data = json.load(f)
|
619 |
+
if data["model"] == model_id:
|
620 |
+
status = {
|
621 |
+
"status": data["status"],
|
622 |
+
"submitted_time": data["submitted_time"],
|
623 |
+
"job_id": data.get("job_id", -1)
|
624 |
+
}
|
625 |
+
logger.info(LogFormatter.success("Status found"))
|
626 |
+
for line in LogFormatter.tree(status, "Model Status"):
|
627 |
+
logger.info(line)
|
628 |
+
return status
|
629 |
+
|
630 |
+
logger.warning(LogFormatter.warning(f"No status found for model: {model_id}"))
|
631 |
+
return {"status": "not_found"}
|
632 |
+
|
633 |
+
async def get_organization_submissions(self, organization: str, days: int = 7) -> List[Dict[str, Any]]:
|
634 |
+
"""Get all submissions from a user in the last n days"""
|
635 |
+
try:
|
636 |
+
# Get all models
|
637 |
+
all_models = await self.get_models()
|
638 |
+
current_time = datetime.now(timezone.utc)
|
639 |
+
cutoff_time = current_time - timedelta(days=days)
|
640 |
+
|
641 |
+
# Filter models by submitter and submission time
|
642 |
+
user_submissions = []
|
643 |
+
for status, models in all_models.items():
|
644 |
+
for model in models:
|
645 |
+
# Check if model was submitted by the user
|
646 |
+
if model["submitter"] == organization:
|
647 |
+
# Parse submission time
|
648 |
+
submit_time = datetime.fromisoformat(
|
649 |
+
model["submission_time"].replace("Z", "+00:00")
|
650 |
+
)
|
651 |
+
# Check if within time window
|
652 |
+
if submit_time > cutoff_time:
|
653 |
+
user_submissions.append({
|
654 |
+
"name": model["name"],
|
655 |
+
"status": status,
|
656 |
+
"submission_time": model["submission_time"],
|
657 |
+
"precision": model["precision"]
|
658 |
+
})
|
659 |
+
|
660 |
+
return sorted(
|
661 |
+
user_submissions,
|
662 |
+
key=lambda x: x["submission_time"],
|
663 |
+
reverse=True
|
664 |
+
)
|
665 |
+
|
666 |
+
except Exception as e:
|
667 |
+
logger.error(LogFormatter.error(f"Failed to get submissions for {organization}", e))
|
668 |
+
raise
|
backend/app/services/rate_limiter.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
import logging
|
3 |
+
from datetime import datetime, timedelta, timezone
|
4 |
+
from typing import Tuple, Dict, List
|
5 |
+
|
6 |
+
logger = logging.getLogger(__name__)
|
7 |
+
|
8 |
+
class RateLimiter:
|
9 |
+
def __init__(self, period_days: int = 7, quota: int = 5):
|
10 |
+
self.period_days = period_days
|
11 |
+
self.quota = quota
|
12 |
+
self.submission_history: Dict[str, List[datetime]] = {}
|
13 |
+
self.higher_quota_users = set() # Users with higher quotas
|
14 |
+
self.unlimited_users = set() # Users with no quota limits
|
15 |
+
|
16 |
+
def add_unlimited_user(self, user_id: str):
|
17 |
+
"""Add a user to the unlimited users list"""
|
18 |
+
self.unlimited_users.add(user_id)
|
19 |
+
|
20 |
+
def add_higher_quota_user(self, user_id: str):
|
21 |
+
"""Add a user to the higher quota users list"""
|
22 |
+
self.higher_quota_users.add(user_id)
|
23 |
+
|
24 |
+
def record_submission(self, user_id: str):
|
25 |
+
"""Record a new submission for a user"""
|
26 |
+
current_time = datetime.now(timezone.utc)
|
27 |
+
if user_id not in self.submission_history:
|
28 |
+
self.submission_history[user_id] = []
|
29 |
+
self.submission_history[user_id].append(current_time)
|
30 |
+
|
31 |
+
def clean_old_submissions(self, user_id: str):
|
32 |
+
"""Remove submissions older than the period"""
|
33 |
+
if user_id not in self.submission_history:
|
34 |
+
return
|
35 |
+
|
36 |
+
current_time = datetime.now(timezone.utc)
|
37 |
+
cutoff_time = current_time - timedelta(days=self.period_days)
|
38 |
+
|
39 |
+
self.submission_history[user_id] = [
|
40 |
+
time for time in self.submission_history[user_id]
|
41 |
+
if time > cutoff_time
|
42 |
+
]
|
43 |
+
|
44 |
+
async def check_rate_limit(self, user_id: str) -> Tuple[bool, str]:
|
45 |
+
"""Check if a user has exceeded their rate limit
|
46 |
+
|
47 |
+
Returns:
|
48 |
+
Tuple[bool, str]: (is_allowed, error_message)
|
49 |
+
"""
|
50 |
+
# Unlimited users bypass all checks
|
51 |
+
if user_id in self.unlimited_users:
|
52 |
+
return True, ""
|
53 |
+
|
54 |
+
# Clean old submissions
|
55 |
+
self.clean_old_submissions(user_id)
|
56 |
+
|
57 |
+
# Get current submission count
|
58 |
+
submission_count = len(self.submission_history.get(user_id, []))
|
59 |
+
|
60 |
+
# Calculate user's quota
|
61 |
+
user_quota = self.quota * 2 if user_id in self.higher_quota_users else self.quota
|
62 |
+
|
63 |
+
# Check if user has exceeded their quota
|
64 |
+
if submission_count >= user_quota:
|
65 |
+
error_msg = (
|
66 |
+
f"User '{user_id}' has reached the limit of {user_quota} submissions "
|
67 |
+
f"in the last {self.period_days} days. Please wait before submitting again."
|
68 |
+
)
|
69 |
+
return False, error_msg
|
70 |
+
|
71 |
+
return True, ""
|
72 |
+
"""
|
backend/app/services/votes.py
ADDED
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone
|
2 |
+
from typing import Dict, Any, List, Set, Tuple, Optional
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
import asyncio
|
6 |
+
from pathlib import Path
|
7 |
+
import aiohttp
|
8 |
+
from huggingface_hub import HfApi
|
9 |
+
import tempfile
|
10 |
+
import os
|
11 |
+
|
12 |
+
from app.services.hf_service import HuggingFaceService
|
13 |
+
from app.config import HF_TOKEN
|
14 |
+
from app.config.hf_config import HF_ORGANIZATION
|
15 |
+
from app.core.cache import cache_config
|
16 |
+
from app.core.formatting import LogFormatter
|
17 |
+
|
18 |
+
logger = logging.getLogger(__name__)
|
19 |
+
|
20 |
+
class VoteService(HuggingFaceService):
|
21 |
+
_instance: Optional['VoteService'] = None
|
22 |
+
_initialized = False
|
23 |
+
|
24 |
+
def __new__(cls):
|
25 |
+
if cls._instance is None:
|
26 |
+
cls._instance = super(VoteService, cls).__new__(cls)
|
27 |
+
return cls._instance
|
28 |
+
|
29 |
+
def __init__(self):
|
30 |
+
if not hasattr(self, '_init_done'):
|
31 |
+
super().__init__()
|
32 |
+
self.votes_file = cache_config.votes_file
|
33 |
+
self.votes_to_upload: List[Dict[str, Any]] = []
|
34 |
+
self.vote_check_set: Set[Tuple[str, str, str, str]] = set()
|
35 |
+
self._votes_by_model: Dict[str, List[Dict[str, Any]]] = {}
|
36 |
+
self._votes_by_user: Dict[str, List[Dict[str, Any]]] = {}
|
37 |
+
self._last_sync = None
|
38 |
+
self._sync_interval = 300 # 5 minutes
|
39 |
+
self._total_votes = 0
|
40 |
+
self._last_vote_timestamp = None
|
41 |
+
self._max_retries = 3
|
42 |
+
self._retry_delay = 1 # seconds
|
43 |
+
self.hf_api = HfApi(token=HF_TOKEN)
|
44 |
+
self._init_done = True
|
45 |
+
|
46 |
+
async def initialize(self):
|
47 |
+
"""Initialize the vote service"""
|
48 |
+
if self._initialized:
|
49 |
+
await self._check_for_new_votes()
|
50 |
+
return
|
51 |
+
|
52 |
+
try:
|
53 |
+
logger.info(LogFormatter.section("VOTE SERVICE INITIALIZATION"))
|
54 |
+
|
55 |
+
# Ensure votes directory exists
|
56 |
+
self.votes_file.parent.mkdir(parents=True, exist_ok=True)
|
57 |
+
|
58 |
+
# Load remote votes
|
59 |
+
remote_votes = await self._fetch_remote_votes()
|
60 |
+
if remote_votes:
|
61 |
+
logger.info(LogFormatter.info(f"Loaded {len(remote_votes)} votes from hub"))
|
62 |
+
|
63 |
+
# Save to local file
|
64 |
+
with open(self.votes_file, 'w') as f:
|
65 |
+
for vote in remote_votes:
|
66 |
+
json.dump(vote, f)
|
67 |
+
f.write('\n')
|
68 |
+
|
69 |
+
# Load into memory
|
70 |
+
await self._load_existing_votes()
|
71 |
+
else:
|
72 |
+
logger.warning(LogFormatter.warning("No votes found on hub"))
|
73 |
+
|
74 |
+
self._initialized = True
|
75 |
+
self._last_sync = datetime.now(timezone.utc)
|
76 |
+
|
77 |
+
# Final summary
|
78 |
+
stats = {
|
79 |
+
"Total_Votes": self._total_votes,
|
80 |
+
"Last_Sync": self._last_sync.strftime("%Y-%m-%d %H:%M:%S UTC")
|
81 |
+
}
|
82 |
+
logger.info(LogFormatter.section("INITIALIZATION COMPLETE"))
|
83 |
+
for line in LogFormatter.stats(stats):
|
84 |
+
logger.info(line)
|
85 |
+
|
86 |
+
except Exception as e:
|
87 |
+
logger.error(LogFormatter.error("Initialization failed", e))
|
88 |
+
raise
|
89 |
+
|
90 |
+
async def _fetch_remote_votes(self) -> List[Dict[str, Any]]:
|
91 |
+
"""Fetch votes from HF hub"""
|
92 |
+
url = f"https://huggingface.co/datasets/{HF_ORGANIZATION}/votes/raw/main/votes_data.jsonl"
|
93 |
+
headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
94 |
+
|
95 |
+
try:
|
96 |
+
async with aiohttp.ClientSession() as session:
|
97 |
+
async with session.get(url, headers=headers) as response:
|
98 |
+
if response.status == 200:
|
99 |
+
votes = []
|
100 |
+
async for line in response.content:
|
101 |
+
if line.strip():
|
102 |
+
try:
|
103 |
+
vote = json.loads(line.decode())
|
104 |
+
votes.append(vote)
|
105 |
+
except json.JSONDecodeError:
|
106 |
+
continue
|
107 |
+
return votes
|
108 |
+
else:
|
109 |
+
logger.error(f"Failed to get remote votes: HTTP {response.status}")
|
110 |
+
return []
|
111 |
+
except Exception as e:
|
112 |
+
logger.error(f"Error fetching remote votes: {str(e)}")
|
113 |
+
return []
|
114 |
+
|
115 |
+
async def _check_for_new_votes(self):
|
116 |
+
"""Check for new votes on the hub and sync if needed"""
|
117 |
+
try:
|
118 |
+
remote_votes = await self._fetch_remote_votes()
|
119 |
+
if len(remote_votes) != self._total_votes:
|
120 |
+
logger.info(f"Vote count changed: Local ({self._total_votes}) ≠ Remote ({len(remote_votes)})")
|
121 |
+
# Save to local file
|
122 |
+
with open(self.votes_file, 'w') as f:
|
123 |
+
for vote in remote_votes:
|
124 |
+
json.dump(vote, f)
|
125 |
+
f.write('\n')
|
126 |
+
|
127 |
+
# Reload into memory
|
128 |
+
await self._load_existing_votes()
|
129 |
+
else:
|
130 |
+
logger.info("Votes are in sync")
|
131 |
+
|
132 |
+
except Exception as e:
|
133 |
+
logger.error(f"Error checking for new votes: {str(e)}")
|
134 |
+
|
135 |
+
async def _sync_with_hub(self):
|
136 |
+
"""Sync votes with HuggingFace hub"""
|
137 |
+
try:
|
138 |
+
logger.info(LogFormatter.section("VOTE SYNC"))
|
139 |
+
|
140 |
+
# Get current remote votes
|
141 |
+
remote_votes = await self._fetch_remote_votes()
|
142 |
+
logger.info(LogFormatter.info(f"Loaded {len(remote_votes)} votes from hub"))
|
143 |
+
|
144 |
+
# If we have pending votes to upload
|
145 |
+
if self.votes_to_upload:
|
146 |
+
logger.info(LogFormatter.info(f"Adding {len(self.votes_to_upload)} pending votes..."))
|
147 |
+
|
148 |
+
# Add new votes to remote votes
|
149 |
+
remote_votes.extend(self.votes_to_upload)
|
150 |
+
|
151 |
+
# Create temporary file with all votes
|
152 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as temp_file:
|
153 |
+
for vote in remote_votes:
|
154 |
+
json.dump(vote, temp_file)
|
155 |
+
temp_file.write('\n')
|
156 |
+
temp_path = temp_file.name
|
157 |
+
|
158 |
+
try:
|
159 |
+
# Upload JSONL file directly
|
160 |
+
self.hf_api.upload_file(
|
161 |
+
path_or_fileobj=temp_path,
|
162 |
+
path_in_repo="votes_data.jsonl",
|
163 |
+
repo_id=f"{HF_ORGANIZATION}/votes",
|
164 |
+
repo_type="dataset",
|
165 |
+
commit_message=f"Update votes: +{len(self.votes_to_upload)} new votes",
|
166 |
+
token=self.token
|
167 |
+
)
|
168 |
+
|
169 |
+
# Clear pending votes only if upload succeeded
|
170 |
+
self.votes_to_upload.clear()
|
171 |
+
logger.info(LogFormatter.success("Pending votes uploaded successfully"))
|
172 |
+
|
173 |
+
except Exception as e:
|
174 |
+
logger.error(LogFormatter.error("Failed to upload votes to hub", e))
|
175 |
+
raise
|
176 |
+
finally:
|
177 |
+
# Clean up temp file
|
178 |
+
os.unlink(temp_path)
|
179 |
+
|
180 |
+
# Update local state
|
181 |
+
with open(self.votes_file, 'w') as f:
|
182 |
+
for vote in remote_votes:
|
183 |
+
json.dump(vote, f)
|
184 |
+
f.write('\n')
|
185 |
+
|
186 |
+
# Reload votes in memory
|
187 |
+
await self._load_existing_votes()
|
188 |
+
logger.info(LogFormatter.success("Sync completed successfully"))
|
189 |
+
|
190 |
+
self._last_sync = datetime.now(timezone.utc)
|
191 |
+
|
192 |
+
except Exception as e:
|
193 |
+
logger.error(LogFormatter.error("Sync failed", e))
|
194 |
+
raise
|
195 |
+
|
196 |
+
async def _load_existing_votes(self):
|
197 |
+
"""Load existing votes from file"""
|
198 |
+
if not self.votes_file.exists():
|
199 |
+
logger.warning(LogFormatter.warning("No votes file found"))
|
200 |
+
return
|
201 |
+
|
202 |
+
try:
|
203 |
+
logger.info(LogFormatter.section("LOADING VOTES"))
|
204 |
+
|
205 |
+
# Clear existing data structures
|
206 |
+
self.vote_check_set.clear()
|
207 |
+
self._votes_by_model.clear()
|
208 |
+
self._votes_by_user.clear()
|
209 |
+
|
210 |
+
vote_count = 0
|
211 |
+
latest_timestamp = None
|
212 |
+
|
213 |
+
with open(self.votes_file, "r") as f:
|
214 |
+
for line in f:
|
215 |
+
try:
|
216 |
+
vote = json.loads(line.strip())
|
217 |
+
vote_count += 1
|
218 |
+
|
219 |
+
# Track latest timestamp
|
220 |
+
try:
|
221 |
+
vote_timestamp = datetime.fromisoformat(vote["timestamp"].replace("Z", "+00:00"))
|
222 |
+
if not latest_timestamp or vote_timestamp > latest_timestamp:
|
223 |
+
latest_timestamp = vote_timestamp
|
224 |
+
vote["timestamp"] = vote_timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
225 |
+
except (KeyError, ValueError) as e:
|
226 |
+
logger.warning(LogFormatter.warning(f"Invalid timestamp in vote: {str(e)}"))
|
227 |
+
continue
|
228 |
+
|
229 |
+
if vote_count % 1000 == 0:
|
230 |
+
logger.info(LogFormatter.info(f"Processed {vote_count:,} votes..."))
|
231 |
+
|
232 |
+
self._add_vote_to_memory(vote)
|
233 |
+
|
234 |
+
except json.JSONDecodeError as e:
|
235 |
+
logger.error(LogFormatter.error("Vote parsing failed", e))
|
236 |
+
continue
|
237 |
+
except Exception as e:
|
238 |
+
logger.error(LogFormatter.error("Vote processing failed", e))
|
239 |
+
continue
|
240 |
+
|
241 |
+
self._total_votes = vote_count
|
242 |
+
self._last_vote_timestamp = latest_timestamp
|
243 |
+
|
244 |
+
# Final summary
|
245 |
+
stats = {
|
246 |
+
"Total_Votes": vote_count,
|
247 |
+
"Latest_Vote": latest_timestamp.strftime("%Y-%m-%d %H:%M:%S UTC") if latest_timestamp else "None",
|
248 |
+
"Unique_Models": len(self._votes_by_model),
|
249 |
+
"Unique_Users": len(self._votes_by_user)
|
250 |
+
}
|
251 |
+
|
252 |
+
logger.info(LogFormatter.section("VOTE SUMMARY"))
|
253 |
+
for line in LogFormatter.stats(stats):
|
254 |
+
logger.info(line)
|
255 |
+
|
256 |
+
except Exception as e:
|
257 |
+
logger.error(LogFormatter.error("Failed to load votes", e))
|
258 |
+
raise
|
259 |
+
|
260 |
+
def _add_vote_to_memory(self, vote: Dict[str, Any]):
|
261 |
+
"""Add vote to memory structures"""
|
262 |
+
try:
|
263 |
+
# Create a unique identifier tuple that includes precision
|
264 |
+
check_tuple = (
|
265 |
+
vote["model"],
|
266 |
+
vote.get("revision", "main"),
|
267 |
+
vote["username"],
|
268 |
+
vote.get("precision", "unknown")
|
269 |
+
)
|
270 |
+
|
271 |
+
# Skip if we already have this vote
|
272 |
+
if check_tuple in self.vote_check_set:
|
273 |
+
return
|
274 |
+
|
275 |
+
self.vote_check_set.add(check_tuple)
|
276 |
+
|
277 |
+
# Update model votes
|
278 |
+
if vote["model"] not in self._votes_by_model:
|
279 |
+
self._votes_by_model[vote["model"]] = []
|
280 |
+
self._votes_by_model[vote["model"]].append(vote)
|
281 |
+
|
282 |
+
# Update user votes
|
283 |
+
if vote["username"] not in self._votes_by_user:
|
284 |
+
self._votes_by_user[vote["username"]] = []
|
285 |
+
self._votes_by_user[vote["username"]].append(vote)
|
286 |
+
|
287 |
+
except KeyError as e:
|
288 |
+
logger.error(LogFormatter.error("Malformed vote data, missing key", str(e)))
|
289 |
+
except Exception as e:
|
290 |
+
logger.error(LogFormatter.error("Error adding vote to memory", str(e)))
|
291 |
+
|
292 |
+
async def get_user_votes(self, user_id: str) -> List[Dict[str, Any]]:
|
293 |
+
"""Get all votes from a specific user"""
|
294 |
+
logger.info(LogFormatter.info(f"Fetching votes for user: {user_id}"))
|
295 |
+
|
296 |
+
# Check if we need to refresh votes
|
297 |
+
if (datetime.now(timezone.utc) - self._last_sync).total_seconds() > self._sync_interval:
|
298 |
+
logger.info(LogFormatter.info("Cache expired, refreshing votes..."))
|
299 |
+
await self._check_for_new_votes()
|
300 |
+
|
301 |
+
votes = self._votes_by_user.get(user_id, [])
|
302 |
+
logger.info(LogFormatter.success(f"Found {len(votes):,} votes"))
|
303 |
+
return votes
|
304 |
+
|
305 |
+
async def get_model_votes(self, model_id: str) -> Dict[str, Any]:
|
306 |
+
"""Get all votes for a specific model"""
|
307 |
+
logger.info(LogFormatter.info(f"Fetching votes for model: {model_id}"))
|
308 |
+
|
309 |
+
# Check if we need to refresh votes
|
310 |
+
if (datetime.now(timezone.utc) - self._last_sync).total_seconds() > self._sync_interval:
|
311 |
+
logger.info(LogFormatter.info("Cache expired, refreshing votes..."))
|
312 |
+
await self._check_for_new_votes()
|
313 |
+
|
314 |
+
votes = self._votes_by_model.get(model_id, [])
|
315 |
+
|
316 |
+
# Group votes by revision and precision
|
317 |
+
votes_by_config = {}
|
318 |
+
for vote in votes:
|
319 |
+
revision = vote.get("revision", "main")
|
320 |
+
precision = vote.get("precision", "unknown")
|
321 |
+
config_key = f"{revision}_{precision}"
|
322 |
+
if config_key not in votes_by_config:
|
323 |
+
votes_by_config[config_key] = {
|
324 |
+
"revision": revision,
|
325 |
+
"precision": precision,
|
326 |
+
"count": 0
|
327 |
+
}
|
328 |
+
votes_by_config[config_key]["count"] += 1
|
329 |
+
|
330 |
+
stats = {
|
331 |
+
"Total_Votes": len(votes),
|
332 |
+
**{f"Config_{k}": v["count"] for k, v in votes_by_config.items()}
|
333 |
+
}
|
334 |
+
|
335 |
+
logger.info(LogFormatter.section("VOTE STATISTICS"))
|
336 |
+
for line in LogFormatter.stats(stats):
|
337 |
+
logger.info(line)
|
338 |
+
|
339 |
+
return {
|
340 |
+
"total_votes": len(votes),
|
341 |
+
"votes_by_config": votes_by_config,
|
342 |
+
"votes": votes
|
343 |
+
}
|
344 |
+
|
345 |
+
async def _get_model_revision(self, model_id: str) -> str:
|
346 |
+
"""Get current revision of a model with retries"""
|
347 |
+
logger.info(f"Getting revision for model: {model_id}")
|
348 |
+
for attempt in range(self._max_retries):
|
349 |
+
try:
|
350 |
+
model_info = await asyncio.to_thread(self.hf_api.model_info, model_id)
|
351 |
+
logger.info(f"Successfully got revision {model_info.sha} for model {model_id}")
|
352 |
+
return model_info.sha
|
353 |
+
except Exception as e:
|
354 |
+
logger.error(f"Error getting model revision for {model_id} (attempt {attempt + 1}): {str(e)}")
|
355 |
+
if attempt < self._max_retries - 1:
|
356 |
+
retry_delay = self._retry_delay * (attempt + 1)
|
357 |
+
logger.info(f"Retrying in {retry_delay} seconds...")
|
358 |
+
await asyncio.sleep(retry_delay)
|
359 |
+
else:
|
360 |
+
logger.warning(f"Using 'main' as fallback revision for {model_id} after {self._max_retries} failed attempts")
|
361 |
+
return "main"
|
362 |
+
|
363 |
+
async def add_vote(self, model_id: str, user_id: str, vote_type: str, vote_data: Dict[str, Any] = None) -> Dict[str, Any]:
|
364 |
+
"""Add a vote for a model"""
|
365 |
+
try:
|
366 |
+
self._log_repo_operation("add", f"{HF_ORGANIZATION}/votes", f"Adding {vote_type} vote for {model_id} by {user_id}")
|
367 |
+
logger.info(LogFormatter.section("NEW VOTE"))
|
368 |
+
stats = {
|
369 |
+
"Model": model_id,
|
370 |
+
"User": user_id,
|
371 |
+
"Type": vote_type,
|
372 |
+
"Config": vote_data or {}
|
373 |
+
}
|
374 |
+
for line in LogFormatter.tree(stats, "Vote Details"):
|
375 |
+
logger.info(line)
|
376 |
+
|
377 |
+
# Use provided configuration or fallback to model info
|
378 |
+
precision = None
|
379 |
+
revision = None
|
380 |
+
|
381 |
+
if vote_data:
|
382 |
+
precision = vote_data.get("precision")
|
383 |
+
revision = vote_data.get("revision")
|
384 |
+
|
385 |
+
# If any info is missing, try to get it from model info
|
386 |
+
if not all([precision, revision]):
|
387 |
+
try:
|
388 |
+
model_info = await asyncio.to_thread(self.hf_api.model_info, model_id)
|
389 |
+
model_card_data = model_info.cardData if hasattr(model_info, 'cardData') else {}
|
390 |
+
|
391 |
+
if not precision:
|
392 |
+
precision = model_card_data.get("precision", "unknown")
|
393 |
+
if not revision:
|
394 |
+
revision = model_info.sha
|
395 |
+
except Exception as e:
|
396 |
+
logger.warning(LogFormatter.warning(f"Failed to get model info: {str(e)}. Using default values."))
|
397 |
+
precision = precision or "unknown"
|
398 |
+
revision = revision or "main"
|
399 |
+
|
400 |
+
# Check if vote already exists with this configuration
|
401 |
+
check_tuple = (model_id, revision, user_id, precision)
|
402 |
+
|
403 |
+
if check_tuple in self.vote_check_set:
|
404 |
+
raise ValueError(f"Vote already recorded for this model configuration (precision: {precision}, revision: {revision[:7] if revision else 'unknown'})")
|
405 |
+
|
406 |
+
vote = {
|
407 |
+
"model": model_id,
|
408 |
+
"revision": revision,
|
409 |
+
"username": user_id,
|
410 |
+
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
411 |
+
"vote_type": vote_type,
|
412 |
+
"precision": precision
|
413 |
+
}
|
414 |
+
|
415 |
+
# Update local storage
|
416 |
+
with open(self.votes_file, "a") as f:
|
417 |
+
f.write(json.dumps(vote) + "\n")
|
418 |
+
|
419 |
+
self._add_vote_to_memory(vote)
|
420 |
+
self.votes_to_upload.append(vote)
|
421 |
+
|
422 |
+
stats = {
|
423 |
+
"Status": "Success",
|
424 |
+
"Queue_Size": len(self.votes_to_upload),
|
425 |
+
"Model_Config": {
|
426 |
+
"Precision": precision,
|
427 |
+
"Revision": revision[:7] if revision else "unknown"
|
428 |
+
}
|
429 |
+
}
|
430 |
+
for line in LogFormatter.stats(stats):
|
431 |
+
logger.info(line)
|
432 |
+
|
433 |
+
# Force immediate sync
|
434 |
+
logger.info(LogFormatter.info("Forcing immediate sync with hub"))
|
435 |
+
await self._sync_with_hub()
|
436 |
+
|
437 |
+
return {"status": "success", "message": "Vote added successfully"}
|
438 |
+
|
439 |
+
except Exception as e:
|
440 |
+
logger.error(LogFormatter.error("Failed to add vote", e))
|
441 |
+
raise
|
backend/app/utils/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from . import model_validation
|
2 |
+
|
3 |
+
__all__ = ["model_validation"]
|
backend/app/utils/logging.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from app.core.formatting import LogFormatter
|
2 |
+
|
3 |
+
__all__ = ['LogFormatter']
|
backend/app/utils/model_validation.py
ADDED
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
import asyncio
|
4 |
+
from typing import Tuple, Optional, Dict, Any
|
5 |
+
from datasets import load_dataset
|
6 |
+
from huggingface_hub import HfApi, ModelCard, hf_hub_download
|
7 |
+
from huggingface_hub import hf_api
|
8 |
+
from transformers import AutoConfig, AutoTokenizer
|
9 |
+
from app.config.base import HF_TOKEN
|
10 |
+
from app.config.hf_config import OFFICIAL_PROVIDERS_REPO
|
11 |
+
from app.core.formatting import LogFormatter
|
12 |
+
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
class ModelValidator:
|
16 |
+
def __init__(self):
|
17 |
+
self.token = HF_TOKEN
|
18 |
+
self.api = HfApi(token=self.token)
|
19 |
+
self.headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
20 |
+
|
21 |
+
async def check_model_card(self, model_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
|
22 |
+
"""Check if model has a valid model card"""
|
23 |
+
try:
|
24 |
+
logger.info(LogFormatter.info(f"Checking model card for {model_id}"))
|
25 |
+
|
26 |
+
# Get model card content using ModelCard.load
|
27 |
+
try:
|
28 |
+
model_card = await asyncio.to_thread(
|
29 |
+
ModelCard.load,
|
30 |
+
model_id
|
31 |
+
)
|
32 |
+
logger.info(LogFormatter.success("Model card found"))
|
33 |
+
except Exception as e:
|
34 |
+
error_msg = "Please add a model card to your model to explain how you trained/fine-tuned it."
|
35 |
+
logger.error(LogFormatter.error(error_msg, e))
|
36 |
+
return False, error_msg, None
|
37 |
+
|
38 |
+
# Check license in model card data
|
39 |
+
if model_card.data.license is None and not ("license_name" in model_card.data and "license_link" in model_card.data):
|
40 |
+
error_msg = "License not found. Please add a license to your model card using the `license` metadata or a `license_name`/`license_link` pair."
|
41 |
+
logger.warning(LogFormatter.warning(error_msg))
|
42 |
+
return False, error_msg, None
|
43 |
+
|
44 |
+
# Enforce card content length
|
45 |
+
if len(model_card.text) < 200:
|
46 |
+
error_msg = "Please add a description to your model card, it is too short."
|
47 |
+
logger.warning(LogFormatter.warning(error_msg))
|
48 |
+
return False, error_msg, None
|
49 |
+
|
50 |
+
logger.info(LogFormatter.success("Model card validation passed"))
|
51 |
+
return True, "", model_card
|
52 |
+
|
53 |
+
except Exception as e:
|
54 |
+
error_msg = "Failed to validate model card"
|
55 |
+
logger.error(LogFormatter.error(error_msg, e))
|
56 |
+
return False, str(e), None
|
57 |
+
|
58 |
+
async def get_safetensors_metadata(self, model_id: str, is_adapter: bool = False, revision: str = "main") -> Optional[Dict]:
|
59 |
+
"""Get metadata from a safetensors file"""
|
60 |
+
try:
|
61 |
+
if is_adapter:
|
62 |
+
metadata = await asyncio.to_thread(
|
63 |
+
hf_api.parse_safetensors_file_metadata,
|
64 |
+
model_id,
|
65 |
+
"adapter_model.safetensors",
|
66 |
+
token=self.token,
|
67 |
+
revision=revision,
|
68 |
+
)
|
69 |
+
else:
|
70 |
+
metadata = await asyncio.to_thread(
|
71 |
+
hf_api.get_safetensors_metadata,
|
72 |
+
repo_id=model_id,
|
73 |
+
token=self.token,
|
74 |
+
revision=revision,
|
75 |
+
)
|
76 |
+
return metadata
|
77 |
+
|
78 |
+
except Exception as e:
|
79 |
+
logger.error(f"Failed to get safetensors metadata: {str(e)}")
|
80 |
+
return None
|
81 |
+
|
82 |
+
async def get_model_size(
|
83 |
+
self,
|
84 |
+
model_info: Any,
|
85 |
+
precision: str,
|
86 |
+
base_model: str,
|
87 |
+
revision: str
|
88 |
+
) -> Tuple[Optional[float], Optional[str]]:
|
89 |
+
"""Get model size in billions of parameters"""
|
90 |
+
try:
|
91 |
+
logger.info(LogFormatter.info(f"Checking model size for {model_info.modelId}"))
|
92 |
+
|
93 |
+
# Check if model is adapter
|
94 |
+
is_adapter = any(s.rfilename == "adapter_config.json" for s in model_info.siblings if hasattr(s, 'rfilename'))
|
95 |
+
|
96 |
+
# Try to get size from safetensors first
|
97 |
+
model_size = None
|
98 |
+
|
99 |
+
if is_adapter and base_model:
|
100 |
+
# For adapters, we need both adapter and base model sizes
|
101 |
+
adapter_meta = await self.get_safetensors_metadata(model_info.id, is_adapter=True, revision=revision)
|
102 |
+
base_meta = await self.get_safetensors_metadata(base_model, revision="main")
|
103 |
+
|
104 |
+
if adapter_meta and base_meta:
|
105 |
+
adapter_size = sum(adapter_meta.parameter_count.values())
|
106 |
+
base_size = sum(base_meta.parameter_count.values())
|
107 |
+
model_size = adapter_size + base_size
|
108 |
+
else:
|
109 |
+
# For regular models, just get the model size
|
110 |
+
meta = await self.get_safetensors_metadata(model_info.id, revision=revision)
|
111 |
+
if meta:
|
112 |
+
model_size = sum(meta.parameter_count.values()) # total params
|
113 |
+
|
114 |
+
if model_size is None:
|
115 |
+
# If model size could not be determined, return an error
|
116 |
+
return None, "Model size could not be determined"
|
117 |
+
|
118 |
+
# Adjust size for GPTQ models
|
119 |
+
size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.id.lower()) else 1
|
120 |
+
model_size = model_size / 1e9 # Convert to billions, assuming float16
|
121 |
+
model_size = round(size_factor * model_size, 3)
|
122 |
+
|
123 |
+
logger.info(LogFormatter.success(f"Model size: {model_size}B parameters"))
|
124 |
+
return model_size, None
|
125 |
+
|
126 |
+
except Exception as e:
|
127 |
+
logger.error(LogFormatter.error(f"Error while determining model size: {e}"))
|
128 |
+
return None, str(e)
|
129 |
+
|
130 |
+
|
131 |
+
async def check_chat_template(
|
132 |
+
self,
|
133 |
+
model_id: str,
|
134 |
+
revision: str
|
135 |
+
) -> Tuple[bool, Optional[str]]:
|
136 |
+
"""Check if model has a valid chat template"""
|
137 |
+
try:
|
138 |
+
logger.info(LogFormatter.info(f"Checking chat template for {model_id}"))
|
139 |
+
|
140 |
+
try:
|
141 |
+
config_file = await asyncio.to_thread(
|
142 |
+
hf_hub_download,
|
143 |
+
repo_id=model_id,
|
144 |
+
filename="tokenizer_config.json",
|
145 |
+
revision=revision,
|
146 |
+
repo_type="model"
|
147 |
+
)
|
148 |
+
|
149 |
+
with open(config_file, 'r') as f:
|
150 |
+
tokenizer_config = json.load(f)
|
151 |
+
|
152 |
+
if 'chat_template' not in tokenizer_config:
|
153 |
+
error_msg = f"The model {model_id} doesn't have a chat_template in its tokenizer_config.json. Please add a chat_template before submitting or submit without it."
|
154 |
+
logger.error(LogFormatter.error(error_msg))
|
155 |
+
return False, error_msg
|
156 |
+
|
157 |
+
logger.info(LogFormatter.success("Valid chat template found"))
|
158 |
+
return True, None
|
159 |
+
|
160 |
+
except Exception as e:
|
161 |
+
error_msg = f"Error checking chat_template: {str(e)}"
|
162 |
+
logger.error(LogFormatter.error(error_msg))
|
163 |
+
return False, error_msg
|
164 |
+
|
165 |
+
except Exception as e:
|
166 |
+
error_msg = "Failed to check chat template"
|
167 |
+
logger.error(LogFormatter.error(error_msg, e))
|
168 |
+
return False, str(e)
|
169 |
+
|
170 |
+
async def is_model_on_hub(
|
171 |
+
self,
|
172 |
+
model_name: str,
|
173 |
+
revision: str,
|
174 |
+
test_tokenizer: bool = False,
|
175 |
+
trust_remote_code: bool = False
|
176 |
+
) -> Tuple[bool, Optional[str], Optional[Any]]:
|
177 |
+
"""Check if model exists and is properly configured on the Hub"""
|
178 |
+
try:
|
179 |
+
config = await asyncio.to_thread(
|
180 |
+
AutoConfig.from_pretrained,
|
181 |
+
model_name,
|
182 |
+
revision=revision,
|
183 |
+
trust_remote_code=trust_remote_code,
|
184 |
+
token=self.token,
|
185 |
+
force_download=True
|
186 |
+
)
|
187 |
+
|
188 |
+
if test_tokenizer:
|
189 |
+
try:
|
190 |
+
await asyncio.to_thread(
|
191 |
+
AutoTokenizer.from_pretrained,
|
192 |
+
model_name,
|
193 |
+
revision=revision,
|
194 |
+
trust_remote_code=trust_remote_code,
|
195 |
+
token=self.token
|
196 |
+
)
|
197 |
+
except ValueError as e:
|
198 |
+
return False, f"The tokenizer is not available in an official Transformers release: {e}", None
|
199 |
+
except Exception:
|
200 |
+
return False, "The tokenizer cannot be loaded. Ensure the tokenizer class is part of a stable Transformers release and correctly configured.", None
|
201 |
+
|
202 |
+
return True, None, config
|
203 |
+
|
204 |
+
except ValueError:
|
205 |
+
return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None
|
206 |
+
except Exception as e:
|
207 |
+
if "You are trying to access a gated repo." in str(e):
|
208 |
+
return True, "The model is gated and requires special access permissions.", None
|
209 |
+
return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None
|
210 |
+
|
211 |
+
async def check_official_provider_status(
|
212 |
+
self,
|
213 |
+
model_id: str,
|
214 |
+
existing_models: Dict[str, list]
|
215 |
+
) -> Tuple[bool, Optional[str]]:
|
216 |
+
"""
|
217 |
+
Check if model is from official provider and has finished submission.
|
218 |
+
|
219 |
+
Args:
|
220 |
+
model_id: The model identifier (org/model-name)
|
221 |
+
existing_models: Dictionary of models by status from get_models()
|
222 |
+
|
223 |
+
Returns:
|
224 |
+
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
225 |
+
"""
|
226 |
+
try:
|
227 |
+
logger.info(LogFormatter.info(f"Checking official provider status for {model_id}"))
|
228 |
+
|
229 |
+
# Get model organization
|
230 |
+
model_org = model_id.split('/')[0] if '/' in model_id else None
|
231 |
+
|
232 |
+
if not model_org:
|
233 |
+
return True, None
|
234 |
+
|
235 |
+
# Load official providers dataset
|
236 |
+
dataset = load_dataset(OFFICIAL_PROVIDERS_REPO)
|
237 |
+
official_providers = dataset["train"][0]["CURATED_SET"]
|
238 |
+
|
239 |
+
# Check if model org is in official providers
|
240 |
+
is_official = model_org in official_providers
|
241 |
+
|
242 |
+
if is_official:
|
243 |
+
logger.info(LogFormatter.info(f"Model organization '{model_org}' is an official provider"))
|
244 |
+
|
245 |
+
# Check for finished submissions
|
246 |
+
if "finished" in existing_models:
|
247 |
+
for model in existing_models["finished"]:
|
248 |
+
if model["name"] == model_id:
|
249 |
+
error_msg = (
|
250 |
+
f"Model {model_id} is an official provider model "
|
251 |
+
f"with a completed evaluation. "
|
252 |
+
f"To re-evaluate, please open a discussion."
|
253 |
+
)
|
254 |
+
logger.error(LogFormatter.error("Validation failed", error_msg))
|
255 |
+
return False, error_msg
|
256 |
+
|
257 |
+
logger.info(LogFormatter.success("No finished submission found for this official provider model"))
|
258 |
+
else:
|
259 |
+
logger.info(LogFormatter.info(f"Model organization '{model_org}' is not an official provider"))
|
260 |
+
|
261 |
+
return True, None
|
262 |
+
|
263 |
+
except Exception as e:
|
264 |
+
error_msg = f"Failed to check official provider status: {str(e)}"
|
265 |
+
logger.error(LogFormatter.error(error_msg))
|
266 |
+
return False, error_msg
|
backend/pyproject.toml
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[tool.poetry]
|
2 |
+
name = "llm-leaderboard-backend"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Backend for the Open LLM Leaderboard"
|
5 |
+
authors = ["Your Name <[email protected]>"]
|
6 |
+
|
7 |
+
[tool.poetry.dependencies]
|
8 |
+
python = "^3.12"
|
9 |
+
fastapi = "^0.115.6"
|
10 |
+
uvicorn = {extras = ["standard"], version = "^0.34.0"}
|
11 |
+
numpy = "^2.2.0"
|
12 |
+
pandas = "^2.2.3"
|
13 |
+
datasets = "^3.3.2"
|
14 |
+
pyarrow = "^18.1.0"
|
15 |
+
python-multipart = "^0.0.20"
|
16 |
+
huggingface-hub = "0.29.1"
|
17 |
+
transformers = "4.49.0"
|
18 |
+
safetensors = "^0.5.3"
|
19 |
+
aiofiles = "^24.1.0"
|
20 |
+
fastapi-cache2 = "^0.2.1"
|
21 |
+
python-dotenv = "^1.0.1"
|
22 |
+
|
23 |
+
[tool.poetry.group.dev.dependencies]
|
24 |
+
pytest = "^8.3.4"
|
25 |
+
black = "^24.10.0"
|
26 |
+
isort = "^5.13.2"
|
27 |
+
flake8 = "^6.1.0"
|
28 |
+
|
29 |
+
[build-system]
|
30 |
+
requires = ["poetry-core>=1.0.0"]
|
31 |
+
build-backend = "poetry.core.masonry.api"
|
backend/utils/analyze_prod_datasets.py
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
from datetime import datetime
|
5 |
+
from pathlib import Path
|
6 |
+
from typing import Dict, Any, List
|
7 |
+
from huggingface_hub import HfApi
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
from app.config.hf_config import HF_ORGANIZATION
|
10 |
+
|
11 |
+
# Get the backend directory path
|
12 |
+
BACKEND_DIR = Path(__file__).parent.parent
|
13 |
+
ROOT_DIR = BACKEND_DIR.parent
|
14 |
+
|
15 |
+
# Load environment variables from .env file in root directory
|
16 |
+
load_dotenv(ROOT_DIR / ".env")
|
17 |
+
|
18 |
+
# Configure logging
|
19 |
+
logging.basicConfig(
|
20 |
+
level=logging.INFO,
|
21 |
+
format='%(message)s'
|
22 |
+
)
|
23 |
+
logger = logging.getLogger(__name__)
|
24 |
+
|
25 |
+
# Initialize Hugging Face API
|
26 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
27 |
+
if not HF_TOKEN:
|
28 |
+
raise ValueError("HF_TOKEN not found in environment variables")
|
29 |
+
api = HfApi(token=HF_TOKEN)
|
30 |
+
|
31 |
+
def analyze_dataset(repo_id: str) -> Dict[str, Any]:
|
32 |
+
"""Analyze a dataset and return statistics"""
|
33 |
+
try:
|
34 |
+
# Get dataset info
|
35 |
+
dataset_info = api.dataset_info(repo_id=repo_id)
|
36 |
+
|
37 |
+
# Get file list
|
38 |
+
files = api.list_repo_files(repo_id, repo_type="dataset")
|
39 |
+
|
40 |
+
# Get last commit info
|
41 |
+
commits = api.list_repo_commits(repo_id, repo_type="dataset")
|
42 |
+
last_commit = next(commits, None)
|
43 |
+
|
44 |
+
# Count lines in jsonl files
|
45 |
+
total_entries = 0
|
46 |
+
for file in files:
|
47 |
+
if file.endswith('.jsonl'):
|
48 |
+
try:
|
49 |
+
# Download file content
|
50 |
+
content = api.hf_hub_download(
|
51 |
+
repo_id=repo_id,
|
52 |
+
filename=file,
|
53 |
+
repo_type="dataset"
|
54 |
+
)
|
55 |
+
|
56 |
+
# Count lines
|
57 |
+
with open(content, 'r') as f:
|
58 |
+
for _ in f:
|
59 |
+
total_entries += 1
|
60 |
+
|
61 |
+
except Exception as e:
|
62 |
+
logger.error(f"Error processing file {file}: {str(e)}")
|
63 |
+
continue
|
64 |
+
|
65 |
+
# Special handling for requests dataset
|
66 |
+
if repo_id == f"{HF_ORGANIZATION}/requests":
|
67 |
+
pending_count = 0
|
68 |
+
completed_count = 0
|
69 |
+
|
70 |
+
try:
|
71 |
+
content = api.hf_hub_download(
|
72 |
+
repo_id=repo_id,
|
73 |
+
filename="eval_requests.jsonl",
|
74 |
+
repo_type="dataset"
|
75 |
+
)
|
76 |
+
|
77 |
+
with open(content, 'r') as f:
|
78 |
+
for line in f:
|
79 |
+
try:
|
80 |
+
entry = json.loads(line)
|
81 |
+
if entry.get("status") == "pending":
|
82 |
+
pending_count += 1
|
83 |
+
elif entry.get("status") == "completed":
|
84 |
+
completed_count += 1
|
85 |
+
except json.JSONDecodeError:
|
86 |
+
continue
|
87 |
+
|
88 |
+
except Exception as e:
|
89 |
+
logger.error(f"Error analyzing requests: {str(e)}")
|
90 |
+
|
91 |
+
# Build response
|
92 |
+
response = {
|
93 |
+
"id": repo_id,
|
94 |
+
"last_modified": last_commit.created_at if last_commit else None,
|
95 |
+
"total_entries": total_entries,
|
96 |
+
"file_count": len(files),
|
97 |
+
"size_bytes": dataset_info.size_in_bytes,
|
98 |
+
"downloads": dataset_info.downloads,
|
99 |
+
}
|
100 |
+
|
101 |
+
# Add request-specific info if applicable
|
102 |
+
if repo_id == f"{HF_ORGANIZATION}/requests":
|
103 |
+
response.update({
|
104 |
+
"pending_requests": pending_count,
|
105 |
+
"completed_requests": completed_count
|
106 |
+
})
|
107 |
+
|
108 |
+
return response
|
109 |
+
|
110 |
+
except Exception as e:
|
111 |
+
logger.error(f"Error analyzing dataset {repo_id}: {str(e)}")
|
112 |
+
return {
|
113 |
+
"id": repo_id,
|
114 |
+
"error": str(e)
|
115 |
+
}
|
116 |
+
|
117 |
+
def main():
|
118 |
+
"""Main function to analyze all datasets"""
|
119 |
+
try:
|
120 |
+
# List of datasets to analyze
|
121 |
+
datasets = [
|
122 |
+
{
|
123 |
+
"id": f"{HF_ORGANIZATION}/contents",
|
124 |
+
"description": "Aggregated results"
|
125 |
+
},
|
126 |
+
{
|
127 |
+
"id": f"{HF_ORGANIZATION}/requests",
|
128 |
+
"description": "Evaluation requests"
|
129 |
+
},
|
130 |
+
{
|
131 |
+
"id": f"{HF_ORGANIZATION}/votes",
|
132 |
+
"description": "User votes"
|
133 |
+
},
|
134 |
+
{
|
135 |
+
"id": f"{HF_ORGANIZATION}/official-providers",
|
136 |
+
"description": "Highlighted models"
|
137 |
+
}
|
138 |
+
]
|
139 |
+
|
140 |
+
# Analyze each dataset
|
141 |
+
results = []
|
142 |
+
for dataset in datasets:
|
143 |
+
logger.info(f"\nAnalyzing {dataset['description']} ({dataset['id']})...")
|
144 |
+
result = analyze_dataset(dataset['id'])
|
145 |
+
results.append(result)
|
146 |
+
|
147 |
+
if 'error' in result:
|
148 |
+
logger.error(f"❌ Error: {result['error']}")
|
149 |
+
else:
|
150 |
+
logger.info(f"✓ {result['total_entries']} entries")
|
151 |
+
logger.info(f"✓ {result['file_count']} files")
|
152 |
+
logger.info(f"✓ {result['size_bytes'] / 1024:.1f} KB")
|
153 |
+
logger.info(f"✓ {result['downloads']} downloads")
|
154 |
+
|
155 |
+
if 'pending_requests' in result:
|
156 |
+
logger.info(f"✓ {result['pending_requests']} pending requests")
|
157 |
+
logger.info(f"✓ {result['completed_requests']} completed requests")
|
158 |
+
|
159 |
+
if result['last_modified']:
|
160 |
+
last_modified = datetime.fromisoformat(result['last_modified'].replace('Z', '+00:00'))
|
161 |
+
logger.info(f"✓ Last modified: {last_modified.strftime('%Y-%m-%d %H:%M:%S')}")
|
162 |
+
|
163 |
+
return results
|
164 |
+
|
165 |
+
except Exception as e:
|
166 |
+
logger.error(f"Global error: {str(e)}")
|
167 |
+
return []
|
168 |
+
|
169 |
+
if __name__ == "__main__":
|
170 |
+
main()
|
backend/utils/analyze_prod_models.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
from datetime import datetime
|
5 |
+
from pathlib import Path
|
6 |
+
from huggingface_hub import HfApi
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
from app.config.hf_config import HF_ORGANIZATION
|
9 |
+
|
10 |
+
# Get the backend directory path
|
11 |
+
BACKEND_DIR = Path(__file__).parent.parent
|
12 |
+
ROOT_DIR = BACKEND_DIR.parent
|
13 |
+
|
14 |
+
# Load environment variables from .env file in root directory
|
15 |
+
load_dotenv(ROOT_DIR / ".env")
|
16 |
+
|
17 |
+
# Configure logging
|
18 |
+
logging.basicConfig(
|
19 |
+
level=logging.INFO,
|
20 |
+
format='%(message)s'
|
21 |
+
)
|
22 |
+
logger = logging.getLogger(__name__)
|
23 |
+
|
24 |
+
# Initialize Hugging Face API
|
25 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
26 |
+
if not HF_TOKEN:
|
27 |
+
raise ValueError("HF_TOKEN not found in environment variables")
|
28 |
+
api = HfApi(token=HF_TOKEN)
|
29 |
+
|
30 |
+
def count_evaluated_models():
|
31 |
+
"""Count the number of evaluated models"""
|
32 |
+
try:
|
33 |
+
# Get dataset info
|
34 |
+
dataset_info = api.dataset_info(repo_id=f"{HF_ORGANIZATION}/contents", repo_type="dataset")
|
35 |
+
|
36 |
+
# Get file list
|
37 |
+
files = api.list_repo_files(f"{HF_ORGANIZATION}/contents", repo_type="dataset")
|
38 |
+
|
39 |
+
# Get last commit info
|
40 |
+
commits = api.list_repo_commits(f"{HF_ORGANIZATION}/contents", repo_type="dataset")
|
41 |
+
last_commit = next(commits, None)
|
42 |
+
|
43 |
+
# Count lines in jsonl files
|
44 |
+
total_entries = 0
|
45 |
+
for file in files:
|
46 |
+
if file.endswith('.jsonl'):
|
47 |
+
try:
|
48 |
+
# Download file content
|
49 |
+
content = api.hf_hub_download(
|
50 |
+
repo_id=f"{HF_ORGANIZATION}/contents",
|
51 |
+
filename=file,
|
52 |
+
repo_type="dataset"
|
53 |
+
)
|
54 |
+
|
55 |
+
# Count lines
|
56 |
+
with open(content, 'r') as f:
|
57 |
+
for _ in f:
|
58 |
+
total_entries += 1
|
59 |
+
|
60 |
+
except Exception as e:
|
61 |
+
logger.error(f"Error processing file {file}: {str(e)}")
|
62 |
+
continue
|
63 |
+
|
64 |
+
# Build response
|
65 |
+
response = {
|
66 |
+
"total_models": total_entries,
|
67 |
+
"last_modified": last_commit.created_at if last_commit else None,
|
68 |
+
"file_count": len(files),
|
69 |
+
"size_bytes": dataset_info.size_in_bytes,
|
70 |
+
"downloads": dataset_info.downloads
|
71 |
+
}
|
72 |
+
|
73 |
+
return response
|
74 |
+
|
75 |
+
except Exception as e:
|
76 |
+
logger.error(f"Error counting evaluated models: {str(e)}")
|
77 |
+
return {
|
78 |
+
"error": str(e)
|
79 |
+
}
|
80 |
+
|
81 |
+
def main():
|
82 |
+
"""Main function to count evaluated models"""
|
83 |
+
try:
|
84 |
+
logger.info("\nAnalyzing evaluated models...")
|
85 |
+
result = count_evaluated_models()
|
86 |
+
|
87 |
+
if 'error' in result:
|
88 |
+
logger.error(f"❌ Error: {result['error']}")
|
89 |
+
else:
|
90 |
+
logger.info(f"✓ {result['total_models']} models evaluated")
|
91 |
+
logger.info(f"✓ {result['file_count']} files")
|
92 |
+
logger.info(f"✓ {result['size_bytes'] / 1024:.1f} KB")
|
93 |
+
logger.info(f"✓ {result['downloads']} downloads")
|
94 |
+
|
95 |
+
if result['last_modified']:
|
96 |
+
last_modified = datetime.fromisoformat(result['last_modified'].replace('Z', '+00:00'))
|
97 |
+
logger.info(f"✓ Last modified: {last_modified.strftime('%Y-%m-%d %H:%M:%S')}")
|
98 |
+
|
99 |
+
return result
|
100 |
+
|
101 |
+
except Exception as e:
|
102 |
+
logger.error(f"Global error: {str(e)}")
|
103 |
+
return {"error": str(e)}
|
104 |
+
|
105 |
+
if __name__ == "__main__":
|
106 |
+
main()
|
backend/utils/fix_wrong_model_size.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import pytz
|
4 |
+
import logging
|
5 |
+
import asyncio
|
6 |
+
from datetime import datetime
|
7 |
+
from pathlib import Path
|
8 |
+
import huggingface_hub
|
9 |
+
from huggingface_hub.errors import RepositoryNotFoundError, RevisionNotFoundError
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
from git import Repo
|
12 |
+
from datetime import datetime
|
13 |
+
from tqdm.auto import tqdm
|
14 |
+
from tqdm.contrib.logging import logging_redirect_tqdm
|
15 |
+
|
16 |
+
from app.config.hf_config import HF_TOKEN, API
|
17 |
+
|
18 |
+
from app.utils.model_validation import ModelValidator
|
19 |
+
|
20 |
+
huggingface_hub.logging.set_verbosity_error()
|
21 |
+
huggingface_hub.utils.disable_progress_bars()
|
22 |
+
|
23 |
+
logging.basicConfig(
|
24 |
+
level=logging.ERROR,
|
25 |
+
format='%(message)s'
|
26 |
+
)
|
27 |
+
logger = logging.getLogger(__name__)
|
28 |
+
load_dotenv()
|
29 |
+
|
30 |
+
validator = ModelValidator()
|
31 |
+
|
32 |
+
def get_changed_files(repo_path, start_date, end_date):
|
33 |
+
repo = Repo(repo_path)
|
34 |
+
start = datetime.strptime(start_date, '%Y-%m-%d')
|
35 |
+
end = datetime.strptime(end_date, '%Y-%m-%d')
|
36 |
+
|
37 |
+
changed_files = set()
|
38 |
+
pbar = tqdm(repo.iter_commits(), desc=f"Reading commits from {end_date} to {start_date}")
|
39 |
+
for commit in pbar:
|
40 |
+
commit_date = datetime.fromtimestamp(commit.committed_date)
|
41 |
+
pbar.set_postfix_str(f"Commit date: {commit_date}")
|
42 |
+
if start <= commit_date <= end:
|
43 |
+
changed_files.update(item.a_path for item in commit.diff(commit.parents[0]))
|
44 |
+
|
45 |
+
if commit_date < start:
|
46 |
+
break
|
47 |
+
|
48 |
+
return changed_files
|
49 |
+
|
50 |
+
|
51 |
+
def read_json(repo_path, file):
|
52 |
+
with open(f"{repo_path}/{file}") as file:
|
53 |
+
return json.load(file)
|
54 |
+
|
55 |
+
|
56 |
+
def write_json(repo_path, file, content):
|
57 |
+
with open(f"{repo_path}/{file}", "w") as file:
|
58 |
+
json.dump(content, file, indent=2)
|
59 |
+
|
60 |
+
|
61 |
+
def main():
|
62 |
+
requests_path = "/requests"
|
63 |
+
start_date = "2024-12-09"
|
64 |
+
end_date = "2025-01-07"
|
65 |
+
|
66 |
+
changed_files = get_changed_files(requests_path, start_date, end_date)
|
67 |
+
|
68 |
+
for file in tqdm(changed_files):
|
69 |
+
try:
|
70 |
+
request_data = read_json(requests_path, file)
|
71 |
+
except FileNotFoundError as e:
|
72 |
+
tqdm.write(f"File {file} not found")
|
73 |
+
continue
|
74 |
+
|
75 |
+
try:
|
76 |
+
model_info = API.model_info(
|
77 |
+
repo_id=request_data["model"],
|
78 |
+
revision=request_data["revision"],
|
79 |
+
token=HF_TOKEN
|
80 |
+
)
|
81 |
+
except (RepositoryNotFoundError, RevisionNotFoundError) as e:
|
82 |
+
tqdm.write(f"Model info for {request_data["model"]} not found")
|
83 |
+
continue
|
84 |
+
|
85 |
+
with logging_redirect_tqdm():
|
86 |
+
new_model_size, error = asyncio.run(validator.get_model_size(
|
87 |
+
model_info=model_info,
|
88 |
+
precision=request_data["precision"],
|
89 |
+
base_model=request_data["base_model"],
|
90 |
+
revision=request_data["revision"]
|
91 |
+
))
|
92 |
+
|
93 |
+
if error:
|
94 |
+
tqdm.write(f"Error getting model size info for {request_data["model"]}, {error}")
|
95 |
+
continue
|
96 |
+
|
97 |
+
old_model_size = request_data["params"]
|
98 |
+
if old_model_size != new_model_size:
|
99 |
+
if new_model_size > 100:
|
100 |
+
tqdm.write(f"Model: {request_data["model"]}, size is more 100B: {new_model_size}")
|
101 |
+
|
102 |
+
tqdm.write(f"Model: {request_data["model"]}, old size: {request_data["params"]} new size: {new_model_size}")
|
103 |
+
tqdm.write(f"Updating request file {file}")
|
104 |
+
|
105 |
+
request_data["params"] = new_model_size
|
106 |
+
write_json(requests_path, file, content=request_data)
|
107 |
+
|
108 |
+
|
109 |
+
if __name__ == "__main__":
|
110 |
+
main()
|
backend/utils/last_activity.py
ADDED
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
from datetime import datetime
|
5 |
+
from pathlib import Path
|
6 |
+
from typing import Dict, Any, List, Tuple
|
7 |
+
from huggingface_hub import HfApi
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
|
10 |
+
# Get the backend directory path
|
11 |
+
BACKEND_DIR = Path(__file__).parent.parent
|
12 |
+
ROOT_DIR = BACKEND_DIR.parent
|
13 |
+
|
14 |
+
# Load environment variables from .env file in root directory
|
15 |
+
load_dotenv(ROOT_DIR / ".env")
|
16 |
+
|
17 |
+
# Configure logging
|
18 |
+
logging.basicConfig(
|
19 |
+
level=logging.INFO,
|
20 |
+
format='%(message)s'
|
21 |
+
)
|
22 |
+
logger = logging.getLogger(__name__)
|
23 |
+
|
24 |
+
# Initialize Hugging Face API
|
25 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
26 |
+
if not HF_TOKEN:
|
27 |
+
raise ValueError("HF_TOKEN not found in environment variables")
|
28 |
+
api = HfApi(token=HF_TOKEN)
|
29 |
+
|
30 |
+
# Default organization
|
31 |
+
HF_ORGANIZATION = os.getenv('HF_ORGANIZATION', 'open-llm-leaderboard')
|
32 |
+
|
33 |
+
def get_last_votes(limit: int = 5) -> List[Dict]:
|
34 |
+
"""Get the last votes from the votes dataset"""
|
35 |
+
try:
|
36 |
+
logger.info("\nFetching last votes...")
|
37 |
+
|
38 |
+
# Download and read votes file
|
39 |
+
logger.info("Downloading votes file...")
|
40 |
+
votes_file = api.hf_hub_download(
|
41 |
+
repo_id=f"{HF_ORGANIZATION}/votes",
|
42 |
+
filename="votes_data.jsonl",
|
43 |
+
repo_type="dataset"
|
44 |
+
)
|
45 |
+
|
46 |
+
logger.info("Reading votes file...")
|
47 |
+
votes = []
|
48 |
+
with open(votes_file, 'r') as f:
|
49 |
+
for line in f:
|
50 |
+
try:
|
51 |
+
vote = json.loads(line)
|
52 |
+
votes.append(vote)
|
53 |
+
except json.JSONDecodeError:
|
54 |
+
continue
|
55 |
+
|
56 |
+
# Sort by timestamp and get last n votes
|
57 |
+
logger.info("Sorting votes...")
|
58 |
+
votes.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
59 |
+
last_votes = votes[:limit]
|
60 |
+
|
61 |
+
logger.info(f"✓ Found {len(last_votes)} recent votes")
|
62 |
+
return last_votes
|
63 |
+
|
64 |
+
except Exception as e:
|
65 |
+
logger.error(f"Error reading votes: {str(e)}")
|
66 |
+
return []
|
67 |
+
|
68 |
+
def get_last_models(limit: int = 5) -> List[Dict]:
|
69 |
+
"""Get the last models from the requests dataset using commit history"""
|
70 |
+
try:
|
71 |
+
logger.info("\nFetching last model submissions...")
|
72 |
+
|
73 |
+
# Get commit history
|
74 |
+
logger.info("Getting commit history...")
|
75 |
+
commits = list(api.list_repo_commits(
|
76 |
+
repo_id=f"{HF_ORGANIZATION}/requests",
|
77 |
+
repo_type="dataset"
|
78 |
+
))
|
79 |
+
logger.info(f"Found {len(commits)} commits")
|
80 |
+
|
81 |
+
# Track processed files to avoid duplicates
|
82 |
+
processed_files = set()
|
83 |
+
models = []
|
84 |
+
|
85 |
+
# Process commits until we have enough models
|
86 |
+
for i, commit in enumerate(commits):
|
87 |
+
logger.info(f"Processing commit {i+1}/{len(commits)} ({commit.created_at})")
|
88 |
+
|
89 |
+
# Look at added/modified files in this commit
|
90 |
+
files_to_process = [f for f in (commit.added + commit.modified) if f.endswith('.json')]
|
91 |
+
if files_to_process:
|
92 |
+
logger.info(f"Found {len(files_to_process)} JSON files in commit")
|
93 |
+
|
94 |
+
for file in files_to_process:
|
95 |
+
if file in processed_files:
|
96 |
+
continue
|
97 |
+
|
98 |
+
processed_files.add(file)
|
99 |
+
logger.info(f"Downloading {file}...")
|
100 |
+
|
101 |
+
try:
|
102 |
+
# Download and read the file
|
103 |
+
content = api.hf_hub_download(
|
104 |
+
repo_id=f"{HF_ORGANIZATION}/requests",
|
105 |
+
filename=file,
|
106 |
+
repo_type="dataset"
|
107 |
+
)
|
108 |
+
|
109 |
+
with open(content, 'r') as f:
|
110 |
+
model_data = json.load(f)
|
111 |
+
models.append(model_data)
|
112 |
+
logger.info(f"✓ Added model {model_data.get('model', 'Unknown')}")
|
113 |
+
|
114 |
+
if len(models) >= limit:
|
115 |
+
logger.info("Reached desired number of models")
|
116 |
+
break
|
117 |
+
|
118 |
+
except Exception as e:
|
119 |
+
logger.error(f"Error reading file {file}: {str(e)}")
|
120 |
+
continue
|
121 |
+
|
122 |
+
if len(models) >= limit:
|
123 |
+
break
|
124 |
+
|
125 |
+
logger.info(f"✓ Found {len(models)} recent model submissions")
|
126 |
+
return models
|
127 |
+
|
128 |
+
except Exception as e:
|
129 |
+
logger.error(f"Error reading models: {str(e)}")
|
130 |
+
return []
|
131 |
+
|
132 |
+
def main():
|
133 |
+
"""Display last activities from the leaderboard"""
|
134 |
+
try:
|
135 |
+
# Get last votes
|
136 |
+
logger.info("\n=== Last Votes ===")
|
137 |
+
last_votes = get_last_votes()
|
138 |
+
if last_votes:
|
139 |
+
for vote in last_votes:
|
140 |
+
logger.info(f"\nModel: {vote.get('model')}")
|
141 |
+
logger.info(f"User: {vote.get('username')}")
|
142 |
+
logger.info(f"Timestamp: {vote.get('timestamp')}")
|
143 |
+
else:
|
144 |
+
logger.info("No votes found")
|
145 |
+
|
146 |
+
# Get last model submissions
|
147 |
+
logger.info("\n=== Last Model Submissions ===")
|
148 |
+
last_models = get_last_models()
|
149 |
+
if last_models:
|
150 |
+
for model in last_models:
|
151 |
+
logger.info(f"\nModel: {model.get('model')}")
|
152 |
+
logger.info(f"Submitter: {model.get('sender', 'Unknown')}")
|
153 |
+
logger.info(f"Status: {model.get('status', 'Unknown')}")
|
154 |
+
logger.info(f"Submission Time: {model.get('submitted_time', 'Unknown')}")
|
155 |
+
logger.info(f"Precision: {model.get('precision', 'Unknown')}")
|
156 |
+
logger.info(f"Weight Type: {model.get('weight_type', 'Unknown')}")
|
157 |
+
else:
|
158 |
+
logger.info("No models found")
|
159 |
+
|
160 |
+
except Exception as e:
|
161 |
+
logger.error(f"Global error: {str(e)}")
|
162 |
+
|
163 |
+
if __name__ == "__main__":
|
164 |
+
main()
|
backend/utils/sync_datasets_locally.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
import tempfile
|
4 |
+
import logging
|
5 |
+
from pathlib import Path
|
6 |
+
from huggingface_hub import HfApi, snapshot_download, upload_folder, create_repo
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
|
9 |
+
# Configure source and destination usernames
|
10 |
+
SOURCE_USERNAME = "open-llm-leaderboard"
|
11 |
+
DESTINATION_USERNAME = "tfrere"
|
12 |
+
|
13 |
+
# Get the backend directory path
|
14 |
+
BACKEND_DIR = Path(__file__).parent.parent
|
15 |
+
ROOT_DIR = BACKEND_DIR.parent
|
16 |
+
|
17 |
+
# Load environment variables from .env file in root directory
|
18 |
+
load_dotenv(ROOT_DIR / ".env")
|
19 |
+
|
20 |
+
# Configure logging
|
21 |
+
logging.basicConfig(
|
22 |
+
level=logging.INFO,
|
23 |
+
format='%(message)s'
|
24 |
+
)
|
25 |
+
logger = logging.getLogger(__name__)
|
26 |
+
|
27 |
+
# List of dataset names to sync
|
28 |
+
DATASET_NAMES = [
|
29 |
+
"votes",
|
30 |
+
"results",
|
31 |
+
"requests",
|
32 |
+
"contents",
|
33 |
+
"official-providers",
|
34 |
+
]
|
35 |
+
|
36 |
+
# Build list of datasets with their source and destination paths
|
37 |
+
DATASETS = [
|
38 |
+
(name, f"{SOURCE_USERNAME}/{name}", f"{DESTINATION_USERNAME}/{name}")
|
39 |
+
for name in DATASET_NAMES
|
40 |
+
]
|
41 |
+
|
42 |
+
# Initialize Hugging Face API
|
43 |
+
api = HfApi()
|
44 |
+
|
45 |
+
def ensure_repo_exists(repo_id, token):
|
46 |
+
"""Ensure the repository exists, create it if it doesn't"""
|
47 |
+
try:
|
48 |
+
api.repo_info(repo_id=repo_id, repo_type="dataset")
|
49 |
+
logger.info(f"✓ Repository {repo_id} already exists")
|
50 |
+
except Exception:
|
51 |
+
logger.info(f"Creating repository {repo_id}...")
|
52 |
+
create_repo(
|
53 |
+
repo_id=repo_id,
|
54 |
+
repo_type="dataset",
|
55 |
+
token=token,
|
56 |
+
private=True
|
57 |
+
)
|
58 |
+
logger.info(f"✓ Repository {repo_id} created")
|
59 |
+
|
60 |
+
def process_dataset(dataset_info, token):
|
61 |
+
"""Process a single dataset"""
|
62 |
+
name, source_dataset, destination_dataset = dataset_info
|
63 |
+
try:
|
64 |
+
logger.info(f"\n📥 Processing dataset: {name}")
|
65 |
+
|
66 |
+
# Ensure destination repository exists
|
67 |
+
ensure_repo_exists(destination_dataset, token)
|
68 |
+
|
69 |
+
# Create a temporary directory for this dataset
|
70 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
71 |
+
try:
|
72 |
+
# List files in source dataset
|
73 |
+
logger.info(f"Listing files in {source_dataset}...")
|
74 |
+
files = api.list_repo_files(source_dataset, repo_type="dataset")
|
75 |
+
logger.info(f"Detected structure: {len(files)} files")
|
76 |
+
|
77 |
+
# Download dataset
|
78 |
+
logger.info(f"Downloading from {source_dataset}...")
|
79 |
+
local_dir = snapshot_download(
|
80 |
+
repo_id=source_dataset,
|
81 |
+
repo_type="dataset",
|
82 |
+
local_dir=temp_dir,
|
83 |
+
token=token
|
84 |
+
)
|
85 |
+
logger.info(f"✓ Download complete")
|
86 |
+
|
87 |
+
# Upload to destination while preserving structure
|
88 |
+
logger.info(f"📤 Uploading to {destination_dataset}...")
|
89 |
+
api.upload_folder(
|
90 |
+
folder_path=local_dir,
|
91 |
+
repo_id=destination_dataset,
|
92 |
+
repo_type="dataset",
|
93 |
+
token=token
|
94 |
+
)
|
95 |
+
logger.info(f"✅ {name} copied successfully!")
|
96 |
+
return True
|
97 |
+
|
98 |
+
except Exception as e:
|
99 |
+
logger.error(f"❌ Error processing {name}: {str(e)}")
|
100 |
+
return False
|
101 |
+
|
102 |
+
except Exception as e:
|
103 |
+
logger.error(f"❌ Error for {name}: {str(e)}")
|
104 |
+
return False
|
105 |
+
|
106 |
+
def copy_datasets():
|
107 |
+
try:
|
108 |
+
logger.info("🔑 Checking authentication...")
|
109 |
+
# Get token from .env file
|
110 |
+
token = os.getenv("HF_TOKEN")
|
111 |
+
if not token:
|
112 |
+
raise ValueError("HF_TOKEN not found in .env file")
|
113 |
+
|
114 |
+
# Process datasets sequentially
|
115 |
+
results = []
|
116 |
+
for dataset_info in DATASETS:
|
117 |
+
success = process_dataset(dataset_info, token)
|
118 |
+
results.append((dataset_info[0], success))
|
119 |
+
|
120 |
+
# Print final summary
|
121 |
+
logger.info("\n📊 Final summary:")
|
122 |
+
for dataset, success in results:
|
123 |
+
status = "✅ Success" if success else "❌ Failure"
|
124 |
+
logger.info(f"{dataset}: {status}")
|
125 |
+
|
126 |
+
except Exception as e:
|
127 |
+
logger.error(f"❌ Global error: {str(e)}")
|
128 |
+
|
129 |
+
if __name__ == "__main__":
|
130 |
+
copy_datasets()
|
docker-compose.yml
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
backend:
|
3 |
+
build:
|
4 |
+
context: ./backend
|
5 |
+
dockerfile: Dockerfile.dev
|
6 |
+
args:
|
7 |
+
- HF_TOKEN=${HF_TOKEN}
|
8 |
+
ports:
|
9 |
+
- "${BACKEND_PORT:-8000}:8000"
|
10 |
+
volumes:
|
11 |
+
- ./backend:/app
|
12 |
+
environment:
|
13 |
+
- ENVIRONMENT=${ENVIRONMENT:-development}
|
14 |
+
- HF_TOKEN=${HF_TOKEN}
|
15 |
+
- HF_HOME=${HF_HOME:-/.cache}
|
16 |
+
command: uvicorn app.asgi:app --host 0.0.0.0 --port 8000 --reload
|
17 |
+
|
18 |
+
frontend:
|
19 |
+
build:
|
20 |
+
context: ./frontend
|
21 |
+
dockerfile: Dockerfile.dev
|
22 |
+
ports:
|
23 |
+
- "${FRONTEND_PORT:-7860}:7860"
|
24 |
+
volumes:
|
25 |
+
- ./frontend:/app
|
26 |
+
- /app/node_modules
|
27 |
+
environment:
|
28 |
+
- NODE_ENV=${ENVIRONMENT:-development}
|
29 |
+
- CHOKIDAR_USEPOLLING=true
|
30 |
+
- PORT=${FRONTEND_PORT:-7860}
|
31 |
+
command: npm start
|
32 |
+
stdin_open: true
|
33 |
+
tty: true
|
frontend/Dockerfile.dev
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:18
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
# Install required global dependencies
|
6 |
+
RUN npm install -g react-scripts
|
7 |
+
|
8 |
+
# Copy package.json and package-lock.json
|
9 |
+
COPY package*.json ./
|
10 |
+
|
11 |
+
# Install project dependencies
|
12 |
+
RUN npm install
|
13 |
+
|
14 |
+
# Volume will be mounted here, no need for COPY
|
15 |
+
CMD ["npm", "start"]
|
frontend/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Frontend - Open LLM Leaderboard 🏆
|
2 |
+
|
3 |
+
React interface for exploring and comparing open-source language models.
|
4 |
+
|
5 |
+
## 🏗 Architecture
|
6 |
+
|
7 |
+
```mermaid
|
8 |
+
flowchart TD
|
9 |
+
Client(["User Browser"]) --> Components["React Components"]
|
10 |
+
|
11 |
+
subgraph Frontend
|
12 |
+
Components --> Context["Context Layer<br>• LeaderboardContext<br>• Global State"]
|
13 |
+
|
14 |
+
API["API Layer<br>• /api/leaderboard/formatted<br>• TanStack Query"] --> |Data Feed| Context
|
15 |
+
|
16 |
+
Context --> Hooks["Hooks Layer<br>• Data Processing<br>• Filtering<br>• Caching"]
|
17 |
+
|
18 |
+
Hooks --> Features["Features<br>• Table Management<br>• Search & Filters<br>• Display Options"]
|
19 |
+
Features --> Cache["Cache Layer<br>• LocalStorage<br>• URL State"]
|
20 |
+
end
|
21 |
+
|
22 |
+
API --> Backend["Backend Server"]
|
23 |
+
|
24 |
+
style Backend fill:#f96,stroke:#333,stroke-width:2px
|
25 |
+
```
|
26 |
+
|
27 |
+
## ✨ Core Features
|
28 |
+
|
29 |
+
- 🔍 **Search & Filters**: Real-time filtering, regex search, advanced filters
|
30 |
+
- 📊 **Data Visualization**: Interactive table, customizable columns, sorting
|
31 |
+
- 🔄 **State Management**: URL sync, client-side caching (5min TTL)
|
32 |
+
- 📱 **Responsive Design**: Mobile-friendly, dark/light themes
|
33 |
+
|
34 |
+
## 🛠 Tech Stack
|
35 |
+
|
36 |
+
- React 18 + Material-UI
|
37 |
+
- TanStack Query & Table
|
38 |
+
- React Router v6
|
39 |
+
|
40 |
+
## 📁 Project Structure
|
41 |
+
|
42 |
+
```
|
43 |
+
src/
|
44 |
+
├── pages/
|
45 |
+
│ └── LeaderboardPage/
|
46 |
+
│ ├── components/ # UI Components
|
47 |
+
│ ├── context/ # Global State
|
48 |
+
│ └── hooks/ # Data Processing
|
49 |
+
├── components/ # Shared Components
|
50 |
+
└── utils/ # Helper Functions
|
51 |
+
```
|
52 |
+
|
53 |
+
## 🚀 Development
|
54 |
+
|
55 |
+
```bash
|
56 |
+
# Install dependencies
|
57 |
+
npm install
|
58 |
+
|
59 |
+
# Start development server
|
60 |
+
npm start
|
61 |
+
|
62 |
+
# Production build
|
63 |
+
npm run build
|
64 |
+
```
|
65 |
+
|
66 |
+
## 🔧 Environment Variables
|
67 |
+
|
68 |
+
```env
|
69 |
+
# API Configuration
|
70 |
+
REACT_APP_API_URL=http://localhost:8000
|
71 |
+
REACT_APP_CACHE_DURATION=300000 # 5 minutes
|
72 |
+
```
|
73 |
+
|
74 |
+
## 🔄 Data Flow
|
75 |
+
|
76 |
+
1. API fetches leaderboard data from backend
|
77 |
+
2. Context stores and manages global state
|
78 |
+
3. Hooks handle data processing and filtering
|
79 |
+
4. Components render based on processed data
|
80 |
+
5. Cache maintains user preferences and URL state
|
frontend/package.json
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "open-llm-leaderboard",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"dependencies": {
|
6 |
+
"@emotion/react": "^11.13.3",
|
7 |
+
"@emotion/styled": "^11.13.0",
|
8 |
+
"@huggingface/hub": "^0.14.0",
|
9 |
+
"@mui/icons-material": "^6.1.7",
|
10 |
+
"@mui/lab": "^6.0.0-beta.16",
|
11 |
+
"@mui/material": "^6.1.6",
|
12 |
+
"@mui/x-data-grid": "^7.22.2",
|
13 |
+
"@tanstack/react-query": "^5.62.2",
|
14 |
+
"@tanstack/react-table": "^8.20.5",
|
15 |
+
"@tanstack/react-virtual": "^3.10.9",
|
16 |
+
"@testing-library/jest-dom": "^5.17.0",
|
17 |
+
"@testing-library/react": "^13.4.0",
|
18 |
+
"@testing-library/user-event": "^13.5.0",
|
19 |
+
"compression": "^1.7.4",
|
20 |
+
"cors": "^2.8.5",
|
21 |
+
"express": "^4.18.2",
|
22 |
+
"react": "^18.3.1",
|
23 |
+
"react-dom": "^18.3.1",
|
24 |
+
"react-router-dom": "^6.28.0",
|
25 |
+
"react-scripts": "5.0.1",
|
26 |
+
"serve-static": "^1.15.0",
|
27 |
+
"web-vitals": "^2.1.4"
|
28 |
+
},
|
29 |
+
"scripts": {
|
30 |
+
"start": "react-scripts start",
|
31 |
+
"build": "react-scripts build",
|
32 |
+
"test": "react-scripts test",
|
33 |
+
"eject": "react-scripts eject",
|
34 |
+
"serve": "node server.js"
|
35 |
+
},
|
36 |
+
"eslintConfig": {
|
37 |
+
"extends": [
|
38 |
+
"react-app",
|
39 |
+
"react-app/jest"
|
40 |
+
]
|
41 |
+
},
|
42 |
+
"browserslist": {
|
43 |
+
"production": [
|
44 |
+
">0.2%",
|
45 |
+
"not dead",
|
46 |
+
"not op_mini all"
|
47 |
+
],
|
48 |
+
"development": [
|
49 |
+
"last 1 chrome version",
|
50 |
+
"last 1 firefox version",
|
51 |
+
"last 1 safari version"
|
52 |
+
]
|
53 |
+
},
|
54 |
+
"proxy": "http://backend:8000"
|
55 |
+
}
|
frontend/public/index.html
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" href="%PUBLIC_URL%/logo32.png" />
|
6 |
+
<meta
|
7 |
+
name="viewport"
|
8 |
+
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
9 |
+
/>
|
10 |
+
<meta
|
11 |
+
name="description"
|
12 |
+
content="Interactive leaderboard tracking and comparing open-source Large Language Models across multiple benchmarks: IFEval, BBH, MATH, GPQA, MUSR, and MMLU-PRO."
|
13 |
+
/>
|
14 |
+
|
15 |
+
<!-- Open Graph / Facebook -->
|
16 |
+
<meta property="og:type" content="website" />
|
17 |
+
<meta
|
18 |
+
property="og:url"
|
19 |
+
content="https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard"
|
20 |
+
/>
|
21 |
+
<meta
|
22 |
+
property="og:title"
|
23 |
+
content="Open LLM Leaderboard - Compare Open Source Large Language Models"
|
24 |
+
/>
|
25 |
+
<meta
|
26 |
+
property="og:description"
|
27 |
+
content="Interactive leaderboard for comparing LLM performance across multiple benchmarks. Features real-time filtering, community voting, and comprehensive model analysis with benchmarks like IFEval, BBH, MATH, GPQA, MUSR, and MMLU-PRO."
|
28 |
+
/>
|
29 |
+
<meta property="og:image" content="%PUBLIC_URL%/og-image.png" />
|
30 |
+
|
31 |
+
<!-- Twitter -->
|
32 |
+
<meta property="twitter:card" content="summary_large_image" />
|
33 |
+
<meta
|
34 |
+
property="twitter:url"
|
35 |
+
content="https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard"
|
36 |
+
/>
|
37 |
+
<meta
|
38 |
+
property="twitter:title"
|
39 |
+
content="Open LLM Leaderboard - Compare Open Source Large Language Models"
|
40 |
+
/>
|
41 |
+
<meta
|
42 |
+
property="twitter:description"
|
43 |
+
content="Interactive leaderboard for comparing LLM performance across multiple benchmarks. Features real-time filtering, community voting, and comprehensive model analysis with benchmarks like IFEval, BBH, MATH, GPQA, MUSR, and MMLU-PRO."
|
44 |
+
/>
|
45 |
+
<meta property="twitter:image" content="%PUBLIC_URL%/og-image.png" />
|
46 |
+
<!--
|
47 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
48 |
+
It will be replaced with the URL of the `public` folder during the build.
|
49 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
50 |
+
|
51 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
52 |
+
work correctly both with client-side routing and a non-root public URL.
|
53 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
54 |
+
-->
|
55 |
+
<title>
|
56 |
+
Open LLM Leaderboard - Compare Open Source Large Language Models
|
57 |
+
</title>
|
58 |
+
<link
|
59 |
+
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
60 |
+
rel="stylesheet"
|
61 |
+
/>
|
62 |
+
<style>
|
63 |
+
html,
|
64 |
+
body {
|
65 |
+
position: fixed;
|
66 |
+
width: 100%;
|
67 |
+
height: 100%;
|
68 |
+
overflow: hidden;
|
69 |
+
-webkit-overflow-scrolling: touch;
|
70 |
+
}
|
71 |
+
#root {
|
72 |
+
position: absolute;
|
73 |
+
top: 0;
|
74 |
+
left: 0;
|
75 |
+
right: 0;
|
76 |
+
bottom: 0;
|
77 |
+
overflow-y: auto;
|
78 |
+
-webkit-overflow-scrolling: touch;
|
79 |
+
}
|
80 |
+
</style>
|
81 |
+
</head>
|
82 |
+
<body>
|
83 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
84 |
+
<div id="root"></div>
|
85 |
+
<!--
|
86 |
+
This HTML file is a template.
|
87 |
+
If you open it directly in the browser, you will see an empty page.
|
88 |
+
|
89 |
+
You can add webfonts, meta tags, or analytics to this file.
|
90 |
+
The build step will place the bundled scripts into the <body> tag.
|
91 |
+
|
92 |
+
To begin the development, run `npm start` or `yarn start`.
|
93 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
94 |
+
-->
|
95 |
+
</body>
|
96 |
+
</html>
|
frontend/public/logo256.png
ADDED
![]() |
frontend/public/logo32.png
ADDED
![]() |
frontend/public/og-image.jpg
ADDED
![]() |
frontend/public/robots.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# https://www.robotstxt.org/robotstxt.html
|
2 |
+
User-agent: *
|
3 |
+
Disallow:
|
frontend/server.js
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require("express");
|
2 |
+
const cors = require("cors");
|
3 |
+
const compression = require("compression");
|
4 |
+
const path = require("path");
|
5 |
+
const serveStatic = require("serve-static");
|
6 |
+
const { createProxyMiddleware } = require("http-proxy-middleware");
|
7 |
+
|
8 |
+
const app = express();
|
9 |
+
const port = process.env.PORT || 7860;
|
10 |
+
const apiPort = process.env.INTERNAL_API_PORT || 7861;
|
11 |
+
|
12 |
+
// Enable CORS for all routes
|
13 |
+
app.use(cors());
|
14 |
+
|
15 |
+
// Enable GZIP compression
|
16 |
+
app.use(compression());
|
17 |
+
|
18 |
+
// Proxy all API requests to the Python backend
|
19 |
+
app.use(
|
20 |
+
"/api",
|
21 |
+
createProxyMiddleware({
|
22 |
+
target: `http://127.0.0.1:${apiPort}`,
|
23 |
+
changeOrigin: true,
|
24 |
+
onError: (err, req, res) => {
|
25 |
+
console.error("Proxy Error:", err);
|
26 |
+
res.status(500).json({ error: "Proxy Error", details: err.message });
|
27 |
+
},
|
28 |
+
})
|
29 |
+
);
|
30 |
+
|
31 |
+
// Serve static files from the build directory
|
32 |
+
app.use(
|
33 |
+
express.static(path.join(__dirname, "build"), {
|
34 |
+
// Don't cache HTML files
|
35 |
+
setHeaders: (res, path) => {
|
36 |
+
if (path.endsWith(".html")) {
|
37 |
+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
38 |
+
res.setHeader("Pragma", "no-cache");
|
39 |
+
res.setHeader("Expires", "0");
|
40 |
+
} else {
|
41 |
+
// Cache other static resources for 1 year
|
42 |
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
43 |
+
}
|
44 |
+
},
|
45 |
+
})
|
46 |
+
);
|
47 |
+
|
48 |
+
// Middleware to preserve URL parameters
|
49 |
+
app.use((req, res, next) => {
|
50 |
+
// Don't interfere with API requests
|
51 |
+
if (req.url.startsWith("/api")) {
|
52 |
+
return next();
|
53 |
+
}
|
54 |
+
|
55 |
+
// Preserve original URL parameters
|
56 |
+
req.originalUrl = req.url;
|
57 |
+
next();
|
58 |
+
});
|
59 |
+
|
60 |
+
// Handle all other routes by serving index.html
|
61 |
+
app.get("*", (req, res) => {
|
62 |
+
// Don't interfere with API requests
|
63 |
+
if (req.url.startsWith("/api")) {
|
64 |
+
return next();
|
65 |
+
}
|
66 |
+
|
67 |
+
// Headers for client-side routing
|
68 |
+
res.set({
|
69 |
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
70 |
+
Pragma: "no-cache",
|
71 |
+
Expires: "0",
|
72 |
+
});
|
73 |
+
|
74 |
+
// Send index.html for all other routes
|
75 |
+
res.sendFile(path.join(__dirname, "build", "index.html"));
|
76 |
+
});
|
77 |
+
|
78 |
+
app.listen(port, "0.0.0.0", () => {
|
79 |
+
console.log(
|
80 |
+
`Frontend server is running on port ${port} in ${
|
81 |
+
process.env.NODE_ENV || "development"
|
82 |
+
} mode`
|
83 |
+
);
|
84 |
+
console.log(`API proxy target: http://127.0.0.1:${apiPort}`);
|
85 |
+
});
|
frontend/src/App.js
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useEffect } from "react";
|
2 |
+
import {
|
3 |
+
HashRouter as Router,
|
4 |
+
Routes,
|
5 |
+
Route,
|
6 |
+
useSearchParams,
|
7 |
+
useLocation,
|
8 |
+
} from "react-router-dom";
|
9 |
+
import { ThemeProvider } from "@mui/material/styles";
|
10 |
+
import { Box, CssBaseline } from "@mui/material";
|
11 |
+
import Navigation from "./components/Navigation/Navigation";
|
12 |
+
import LeaderboardPage from "./pages/LeaderboardPage/LeaderboardPage";
|
13 |
+
import QuotePage from "./pages/QuotePage/QuotePage";
|
14 |
+
import Footer from "./components/Footer/Footer";
|
15 |
+
import getTheme from "./config/theme";
|
16 |
+
import { useThemeMode } from "./hooks/useThemeMode";
|
17 |
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
18 |
+
import LeaderboardProvider from "./pages/LeaderboardPage/components/Leaderboard/context/LeaderboardContext";
|
19 |
+
|
20 |
+
const queryClient = new QueryClient({
|
21 |
+
defaultOptions: {
|
22 |
+
queries: {
|
23 |
+
retry: 1,
|
24 |
+
refetchOnWindowFocus: false,
|
25 |
+
},
|
26 |
+
},
|
27 |
+
});
|
28 |
+
|
29 |
+
function UrlHandler() {
|
30 |
+
const location = useLocation();
|
31 |
+
const [searchParams] = useSearchParams();
|
32 |
+
|
33 |
+
// Synchroniser l'URL avec la page parente HF
|
34 |
+
useEffect(() => {
|
35 |
+
// Vérifier si nous sommes dans un iframe HF Space
|
36 |
+
const isHFSpace = window.location !== window.parent.location;
|
37 |
+
if (!isHFSpace) return;
|
38 |
+
|
39 |
+
// Sync query and hash from this embedded app to the parent page URL
|
40 |
+
const queryString = window.location.search;
|
41 |
+
const hash = window.location.hash;
|
42 |
+
|
43 |
+
// HF Spaces' special message type to update the query string and the hash in the parent page URL
|
44 |
+
window.parent.postMessage(
|
45 |
+
{
|
46 |
+
queryString,
|
47 |
+
hash,
|
48 |
+
},
|
49 |
+
"https://huggingface.co"
|
50 |
+
);
|
51 |
+
}, [location, searchParams]);
|
52 |
+
|
53 |
+
// Read the updated hash reactively
|
54 |
+
useEffect(() => {
|
55 |
+
const handleHashChange = (event) => {
|
56 |
+
console.log("hash change event", event);
|
57 |
+
};
|
58 |
+
|
59 |
+
window.addEventListener("hashchange", handleHashChange);
|
60 |
+
return () => window.removeEventListener("hashchange", handleHashChange);
|
61 |
+
}, []);
|
62 |
+
|
63 |
+
return null;
|
64 |
+
}
|
65 |
+
|
66 |
+
function App() {
|
67 |
+
const { mode, toggleTheme } = useThemeMode();
|
68 |
+
const theme = getTheme(mode);
|
69 |
+
|
70 |
+
return (
|
71 |
+
<div
|
72 |
+
className="App"
|
73 |
+
style={{
|
74 |
+
height: "100%",
|
75 |
+
width: "100%",
|
76 |
+
WebkitOverflowScrolling: "touch",
|
77 |
+
overflow: "auto",
|
78 |
+
}}
|
79 |
+
>
|
80 |
+
<QueryClientProvider client={queryClient}>
|
81 |
+
<ThemeProvider theme={theme}>
|
82 |
+
<CssBaseline />
|
83 |
+
<Router>
|
84 |
+
<LeaderboardProvider>
|
85 |
+
<UrlHandler />
|
86 |
+
<Box
|
87 |
+
sx={{
|
88 |
+
minHeight: "100vh",
|
89 |
+
display: "flex",
|
90 |
+
flexDirection: "column",
|
91 |
+
bgcolor: "background.default",
|
92 |
+
color: "text.primary",
|
93 |
+
}}
|
94 |
+
>
|
95 |
+
<Navigation onToggleTheme={toggleTheme} mode={mode} />
|
96 |
+
<Box
|
97 |
+
sx={{
|
98 |
+
flex: 1,
|
99 |
+
display: "flex",
|
100 |
+
flexDirection: "column",
|
101 |
+
width: "100%",
|
102 |
+
px: 4,
|
103 |
+
pb: 4,
|
104 |
+
}}
|
105 |
+
>
|
106 |
+
<Routes>
|
107 |
+
<Route path="/" element={<LeaderboardPage />} />
|
108 |
+
<Route path="/quote" element={<QuotePage />} />
|
109 |
+
</Routes>
|
110 |
+
</Box>
|
111 |
+
<Footer />
|
112 |
+
</Box>
|
113 |
+
</LeaderboardProvider>
|
114 |
+
</Router>
|
115 |
+
</ThemeProvider>
|
116 |
+
</QueryClientProvider>
|
117 |
+
</div>
|
118 |
+
);
|
119 |
+
}
|
120 |
+
|
121 |
+
export default App;
|
frontend/src/components/Footer/Footer.js
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { Box, Typography, Link } from "@mui/material";
|
3 |
+
|
4 |
+
const Footer = () => {
|
5 |
+
return (
|
6 |
+
<Box
|
7 |
+
component="footer"
|
8 |
+
sx={{
|
9 |
+
width: "100%",
|
10 |
+
py: 4,
|
11 |
+
textAlign: "center",
|
12 |
+
}}
|
13 |
+
>
|
14 |
+
<Typography variant="body2" color="text.secondary" sx={{ mx: 4 }}>
|
15 |
+
© 2024 Hugging Face - Open LLM Leaderboard - Made with 🤗 by the HF team
|
16 |
+
-{" "}
|
17 |
+
<Link
|
18 |
+
href="https://huggingface.co"
|
19 |
+
target="_blank"
|
20 |
+
rel="noopener noreferrer"
|
21 |
+
color="inherit"
|
22 |
+
>
|
23 |
+
huggingface.co
|
24 |
+
</Link>
|
25 |
+
</Typography>
|
26 |
+
</Box>
|
27 |
+
);
|
28 |
+
};
|
29 |
+
|
30 |
+
export default Footer;
|
frontend/src/components/Logo/HFLogo.js
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
|
3 |
+
const HFLogo = () => (
|
4 |
+
<svg width="100%" viewBox="0 0 236 220" version="1.1" xmlns="http://www.w3.org/2000/svg" >
|
5 |
+
<title>hg-logo</title>
|
6 |
+
<g id="hg-logo" stroke="none" strokeWidth="1" fill="none">
|
7 |
+
<g id="Group" transform="translate(-1.000000, 0.000000)">
|
8 |
+
<path d="M236.188357,161.726225 C235.570415,159.393906 234.569281,157.181253 233.22638,155.176863 C233.514062,154.120588 233.732701,153.048205 233.879994,151.965466 C234.832798,145.089325 232.449638,138.794251 227.956041,133.922501 C225.522249,131.262254 222.913547,129.506398 220.150646,128.428262 C221.964195,120.669591 222.882477,112.729122 222.88708,104.761037 C222.88708,101.122758 222.681099,97.5581193 222.335881,94.046409 C222.155216,92.2928551 221.937728,90.5427531 221.683417,88.7984042 C220.891716,83.6516545 219.717972,78.5709507 218.171392,73.5986359 C217.1576,70.3316637 215.985007,67.1160095 214.658216,63.9632945 C212.668606,59.2945148 210.345284,54.7746261 207.706662,50.4388375 C205.974815,47.5549087 204.089921,44.7659066 202.058884,42.0841428 C201.063504,40.7298561 200.026697,39.4075568 198.947313,38.1190859 C195.750588,34.2338824 192.277687,30.5855928 188.552777,27.2030978 C187.316894,26.0660493 186.045339,24.9682371 184.739261,23.9111571 C183.453897,22.8390039 182.139764,21.8011393 180.799165,20.798714 C178.100706,18.7906417 175.311338,16.9066068 172.44142,15.1525926 C156.583223,5.52185376 137.986291,0 118.109749,0 C60.2385495,0 13.3336831,46.9018135 13.3336831,104.76564 C13.3321871,112.833829 14.2670394,120.874403 16.1195981,128.726274 C13.6340233,129.805561 11.2508635,131.486626 9.04261448,133.920199 C4.55016831,138.788498 2.16585774,145.06171 3.11981211,151.9367 C3.26365324,153.029795 3.48229176,154.111383 3.77227548,155.174561 C2.4301802,157.180102 1.42939115,159.393906 0.810298929,161.726225 C-0.570575919,166.97423 -0.116037948,171.706754 1.63882384,175.85246 C-0.267934182,181.273058 0.208467641,187.044598 2.69289164,192.062477 C4.49953623,195.727221 7.08522438,198.561213 10.2715931,201.096041 C14.0609438,204.107229 18.8042489,206.667372 24.5268244,209.121657 C31.3529491,212.032741 39.6842274,214.767779 43.4735781,215.771124 C53.2616793,218.305953 62.6470253,219.912227 72.1599872,219.989319 C85.7109724,220.115888 97.3816663,216.928654 105.738261,208.774168 C109.842911,209.276992 113.974028,209.528979 118.109749,209.527828 C122.479067,209.518623 126.843782,209.242473 131.179729,208.70398 C139.51561,216.910244 151.231182,220.126243 164.8328,219.996223 C174.343575,219.921432 183.728921,218.315158 193.491706,215.776877 C197.306373,214.773532 205.63535,212.038494 212.464927,209.128561 C218.187502,206.668523 222.929657,204.109531 226.745474,201.101795 C229.907678,198.568116 232.491064,195.732974 234.29886,192.068231 C236.8086,187.050351 237.260836,181.278811 235.378244,175.858213 C237.116995,171.712507 237.568081,166.969627 236.188357,161.726225 Z M226.477354,175.501519 C228.400223,179.150153 228.523351,183.272846 226.826025,187.112485 C224.252995,192.932351 217.861846,197.515294 205.448932,202.436521 C197.730992,205.498336 190.662064,207.4544 190.599924,207.47281 C180.390656,210.1204 171.157207,211.464332 163.164243,211.464332 C149.928557,211.464332 140.080618,207.813396 133.834461,200.601272 C123.271919,202.399701 112.486136,202.460684 101.904031,200.781921 C95.6509699,207.874379 85.857115,211.464332 72.7330503,211.464332 C64.7390507,211.464332 55.5067517,210.1204 45.2974836,207.47281 C45.2341935,207.4544 38.1698679,205.498336 30.4484761,202.436521 C18.0355619,197.515294 11.6432621,192.934652 9.07138271,187.112485 C7.37405737,183.272846 7.49718538,179.150153 9.4200536,175.501519 C9.59841661,175.163235 9.7882869,174.831854 9.99196594,174.513131 C8.83939573,172.78259 8.06645104,170.826526 7.72364885,168.77611 C7.38096175,166.725695 7.47635718,164.624652 8.00350616,162.613358 C8.76759024,159.711479 10.3463905,157.297466 12.489048,155.563473 C11.4573043,153.86745 10.7801003,151.980424 10.4982867,150.015155 C9.88149595,145.74173 11.2991941,141.474059 14.4913165,137.995716 C16.9757405,135.288294 20.4889162,133.798233 24.3795311,133.798233 L24.4830967,133.798233 C21.5502336,124.39877 20.0630314,114.608094 20.0723523,104.762188 C20.0723523,51.0601755 63.612487,7.52279222 117.324951,7.52279222 C171.038681,7.52279222 214.577665,51.0567236 214.577665,104.762188 C214.58457,114.634558 213.087471,124.450548 210.137002,133.871873 C210.606499,133.825848 211.066791,133.801685 211.517877,133.801685 C215.407341,133.801685 218.921668,135.290595 221.406092,137.998018 C224.595912,141.474059 226.017063,145.745182 225.399121,150.017456 C225.117193,151.982725 224.440564,153.870902 223.40836,155.566925 C225.551018,157.299767 227.129818,159.71378 227.893902,162.61681 C228.419785,164.628104 228.515296,166.727996 228.172378,168.779562 C227.829461,170.829977 227.057322,172.784891 225.905442,174.516583 C226.109121,174.831854 226.301293,175.163235 226.477354,175.501519 Z" id="Shape" fill="#FFFFFF" fillRule="nonzero"></path>
|
9 |
+
<path d="M226.52977,174.037682 C227.682419,172.305523 228.455074,170.350082 228.798221,168.299114 C229.141367,166.246994 229.045793,164.146536 228.519558,162.134699 C227.754964,159.232038 226.175108,156.817373 224.031019,155.082913 C225.062761,153.386432 225.740993,151.498897 226.02311,149.533098 C226.640313,145.258521 225.221668,140.989698 222.027412,137.510418 C219.541328,134.802265 216.024653,133.311802 212.13259,133.311802 C211.680051,133.311802 211.220603,133.334821 210.750792,133.382009 C213.699779,123.958143 215.195575,114.139506 215.186363,104.265624 C215.186363,50.5501622 171.617132,7 117.873265,7 C64.1293973,7 20.5555606,50.5455585 20.5555606,104.265624 C20.5462334,114.114185 22.0344295,123.907502 24.9692525,133.3095 L24.8656177,133.3095 C20.9735543,133.3095 17.4580309,134.799963 14.9719466,137.508116 C11.7799941,140.985094 10.3590456,145.256219 10.9762485,149.530796 C11.2580201,151.496595 11.9356766,153.384131 12.9683401,155.080611 C10.8242508,156.815072 9.24439546,159.229736 8.48095227,162.133548 C7.95379648,164.145385 7.85868274,166.246994 8.20205945,168.299114 C8.54543616,170.350082 9.31935798,172.306674 10.4730439,174.037682 C10.2669257,174.356491 10.07808,174.687961 9.90074934,175.026336 C7.97774764,178.675955 7.85338585,182.79976 9.55184544,186.640434 C12.1254435,192.460719 18.522015,197.0472 30.9432242,201.968603 C38.6663215,205.031245 45.7399738,206.987836 45.8021547,207.006251 C56.0182452,209.654556 65.2567139,211 73.2550191,211 C86.3890056,211 96.1882538,207.409079 102.446646,200.313557 C113.035821,201.992773 123.828812,201.931773 134.398413,200.13286 C140.647592,207.346928 150.503264,211 163.747794,211 C171.7461,211 180.98572,209.654556 191.20181,207.006251 C191.263991,206.987836 198.334189,205.031245 206.060741,201.968603 C218.48195,197.0472 224.878521,192.460719 227.45212,186.640434 C229.150579,182.79976 229.027369,178.675955 227.103216,175.026336 C226.927036,174.684508 226.733585,174.354189 226.52977,174.037682 Z M97.9684697,189.207022 C97.4295686,190.149639 96.8526681,191.069237 96.2377682,191.963514 C94.6199135,194.33099 92.4919451,196.139111 90.0231334,197.484555 C85.30084,200.056898 79.3257167,200.95693 73.2538676,200.95693 C63.6641921,200.95693 53.832702,198.713755 48.3216324,197.284293 C48.0510304,197.214085 14.5435894,187.75454 18.7857081,179.70259 C19.4996369,178.349089 20.6753163,177.808149 22.1538398,177.808149 C28.1266601,177.808149 39.0026741,186.697981 43.677756,186.697981 C44.7210132,186.697981 45.4578568,186.252568 45.7583978,185.169537 C47.7504894,178.027978 15.4820603,175.026336 18.1995956,164.686325 C18.6797703,162.856336 19.9798115,162.113982 21.8095419,162.113982 C29.7053639,162.112831 47.4292214,175.993123 51.1358936,175.993123 C51.4203136,175.993123 51.6241287,175.910255 51.7346726,175.735313 L51.7830355,175.655898 C53.5217975,172.784312 52.5246002,170.696514 40.6042927,163.399578 L39.4597036,162.703262 C26.3441411,154.767556 17.1367629,149.993472 22.3737759,144.296338 C22.9760094,143.638002 23.8292694,143.346815 24.8667692,143.346815 C26.0977205,143.346815 27.5866075,143.758851 29.2263407,144.448261 C36.1537528,147.368187 45.7549433,155.331515 49.7656109,158.80504 C50.9481994,159.833977 51.6448557,160.462389 51.6448557,160.462389 C51.6448557,160.462389 56.722962,165.740582 59.7940072,165.740582 C60.501027,165.740582 61.099806,165.463207 61.5062848,164.773796 C63.684919,161.104611 41.282525,144.137509 40.0193317,137.137514 C39.1637686,132.393355 40.6204136,129.991351 43.3160705,129.991351 C44.5965363,129.991351 46.1602706,130.535744 47.8863662,131.630284 C53.240832,135.027848 63.5789812,152.784493 67.3639552,159.691261 C68.6329061,162.006945 70.7988738,162.985241 72.750663,162.985241 C76.6231508,162.985241 79.6504392,159.137661 73.1053244,154.248484 C63.2680768,146.890548 66.7202678,134.865566 71.4149253,134.125514 C71.6152859,134.094439 71.8179496,134.078326 72.0194617,134.077175 C76.2892164,134.077175 78.1730672,141.431658 78.1730672,141.431658 C78.1730672,141.431658 83.6921972,155.286628 93.1747834,164.756532 C101.779928,173.352875 102.980941,180.408114 97.9684697,189.207022 Z M128.631711,190.829842 L128.140021,190.88854 L127.300579,190.985218 C126.859555,191.030105 126.418531,191.07384 125.975205,191.115274 L125.542241,191.154406 L125.148429,191.187783 L124.58765,191.23267 C124.381531,191.247632 124.175413,191.263745 123.969295,191.276405 L123.352092,191.317839 L123.216215,191.325896 L122.730283,191.354669 L122.524165,191.365027 L121.948416,191.393801 L121.279396,191.421423 L120.671405,191.44214 L120.266077,191.453649 L120.061111,191.453649 C119.934446,191.453649 119.808933,191.460555 119.682268,191.461706 L119.480756,191.461706 C119.354091,191.461706 119.228578,191.461706 119.101913,191.468612 L118.587193,191.474366 L117.866356,191.474366 C117.30097,191.474366 116.737888,191.468612 116.174805,191.458253 L115.718812,191.447895 C115.589844,191.447895 115.459725,191.440989 115.330757,191.437536 L114.848279,191.422574 L114.247197,191.399555 L113.707145,191.376537 L113.566662,191.370782 L113.051942,191.34316 C112.909157,191.335103 112.766371,191.328197 112.624737,191.31899 L112.291954,191.299424 C111.87396,191.272952 111.455966,191.243028 111.037972,191.210802 L110.600403,191.176274 L110.047684,191.129085 L109.401694,191.069237 C109.041275,191.03586 108.680856,190.99903 108.320437,190.958747 L108.303164,190.958747 C113.56551,179.224952 110.904399,168.266887 100.270314,157.646048 C93.2968422,150.685185 88.6563052,140.403871 87.6948043,138.146885 C85.7464697,131.4657 80.58891,124.038709 72.0252192,124.038709 C71.300927,124.03986 70.5789377,124.097406 69.8638574,124.210198 C66.1111254,124.799478 62.831659,126.958634 60.491815,130.206576 C57.9642769,127.063369 55.5058286,124.564687 53.2834374,123.152488 C49.9325781,121.030161 46.5897794,119.952885 43.33104,119.952885 C39.2662519,119.952885 35.6309727,121.621743 33.097677,124.648705 L33.0343446,124.725818 L32.8915589,124.12618 L32.8858014,124.100859 C32.4040146,122.040683 31.9992631,119.962092 31.6721225,117.871992 C31.6721225,117.85703 31.6721225,117.843219 31.6652135,117.829408 L31.5938206,117.356373 C31.5552454,117.100865 31.5175914,116.844206 31.4809738,116.588698 C31.4510349,116.375775 31.4210959,116.162852 31.39346,115.95108 C31.365824,115.739307 31.3358851,115.526384 31.3105521,115.313461 C31.2829161,115.099387 31.2575832,114.886464 31.2322502,114.67354 C31.2079536,114.470976 31.1845782,114.268411 31.1620089,114.065846 L31.155215,114.014054 C31.0513499,113.079494 30.9623391,112.143782 30.8879523,111.20692 L30.8476499,110.664829 C30.8361349,110.516359 30.8257714,110.366737 30.8165594,110.215964 C30.8165594,110.181436 30.8119535,110.145757 30.8096505,110.113531 L30.7717662,109.512742 C30.7579482,109.295215 30.7474696,109.077688 30.7359546,108.860161 C30.7244396,108.643785 30.7129246,108.426258 30.7037126,108.20758 L30.6806827,107.637867 L30.6737737,107.465226 L30.6565012,106.938098 L30.6439499,106.491534 C30.6439499,106.313139 30.6357743,106.133593 30.6323198,105.955198 L30.6231078,105.39239 C30.6208048,105.204788 30.6231078,105.018336 30.6185018,104.830733 C30.6127443,104.64198 30.6185018,104.454377 30.6185018,104.265624 C30.6185018,56.0965241 69.6899812,17.0441057 117.887083,17.0441057 C166.084184,17.0441057 205.154512,56.0942222 205.154512,104.265624 L205.154512,105.39239 C205.154512,105.579993 205.149906,105.768746 205.1453,105.955198 C205.1453,106.111725 205.139542,106.2648 205.134936,106.421327 C205.134936,106.560591 205.129179,106.698703 205.123421,106.833362 C205.123421,107.009456 205.113058,107.184398 205.1073,107.360491 L205.1073,107.375453 C205.092331,107.757564 205.07621,108.139675 205.060089,108.521786 L205.05318,108.648389 L205.023241,109.219253 L204.995605,109.671571 C204.931121,110.743093 204.847062,111.814615 204.744579,112.883834 L204.744579,112.898797 C204.726155,113.09906 204.705428,113.300473 204.683549,113.500736 C204.632883,113.966865 204.581066,114.432995 204.529248,114.899124 L204.470522,115.367555 L204.397978,115.917702 C204.372645,116.119116 204.345009,116.319379 204.316221,116.519642 C204.285131,116.744075 204.251737,116.967356 204.219495,117.190638 L204.138891,117.717767 L204.044468,118.316254 C204.012226,118.515366 203.979984,118.713327 203.941984,118.912439 C203.902833,119.1104 203.872894,119.308361 203.83835,119.507473 C203.76926,119.903395 203.697867,120.298166 203.620716,120.692937 C203.467567,121.479026 203.307509,122.262814 203.138239,123.045451 C203.095633,123.239959 203.051876,123.435618 203.006968,123.630126 C200.550823,121.244235 197.298992,119.944828 193.710924,119.944828 C190.45564,119.944828 187.109386,121.020954 183.759679,123.144431 C181.536136,124.555479 179.079991,127.055313 176.550149,130.19852 C174.206851,126.949427 170.927385,124.791421 167.179258,124.200991 C166.464178,124.08935 165.741037,124.031803 165.016745,124.030652 C156.450751,124.030652 151.296646,131.457644 149.34716,138.138829 C148.381053,140.395815 143.740516,150.675978 136.758984,157.648349 C126.13296,168.234661 123.451121,179.144386 128.631711,190.829842 Z M218.724916,167.341535 L218.690371,167.443968 C218.66619,167.509571 218.642008,167.575175 218.615524,167.639627 C218.57407,167.737457 218.530313,167.832984 218.484253,167.928512 C218.372558,168.156398 218.245893,168.377377 218.106562,168.58915 C217.914261,168.875733 217.703537,169.149656 217.474389,169.407465 C217.42142,169.467314 217.370754,169.526012 217.310876,169.584709 C217.230272,169.673332 217.143909,169.759652 217.058698,169.845972 C215.507631,171.382472 213.144757,172.727916 210.473281,173.964022 C210.170437,174.100983 209.864139,174.237945 209.553234,174.374906 L209.244633,174.511868 C209.038515,174.602792 208.833548,174.693716 208.617066,174.782338 C208.406342,174.872111 208.194467,174.960733 207.982591,175.048204 L207.340055,175.31407 C205.83735,175.932123 204.297797,176.520251 202.795092,177.102625 L202.153708,177.351227 L201.518081,177.59983 C201.096633,177.764414 200.67979,177.928997 200.268706,178.093581 L199.65726,178.339882 L199.05733,178.586182 L198.761395,178.709332 C198.56564,178.7922 198.372188,178.872765 198.18104,178.955633 C193.76159,180.850074 190.583456,182.777892 191.251325,185.170688 C191.269749,185.238594 191.290476,185.303046 191.313506,185.365197 C191.373383,185.542441 191.45514,185.710477 191.556472,185.867005 C191.61635,185.961382 191.685439,186.050004 191.76259,186.130569 C192.547911,186.945432 193.97692,186.816527 195.779015,186.169701 C195.925255,186.116758 196.07495,186.060362 196.225796,186.000513 C196.329431,185.961382 196.431914,185.919948 196.535549,185.878514 L196.691001,185.812911 C197.103238,185.637968 197.539655,185.44346 197.986437,185.230537 C198.098132,185.178745 198.210979,185.128104 198.323826,185.068255 C200.526641,183.99213 203.02424,182.540799 205.502264,181.220675 C205.882259,181.014658 206.263404,180.817847 206.642247,180.622188 C207.261753,180.301077 207.887017,179.991475 208.518038,179.695685 C210.86479,178.593088 213.069909,177.810451 214.844368,177.810451 C215.675749,177.810451 216.411556,177.98079 217.023002,178.372108 L217.125485,178.440013 C217.435238,178.658691 217.704689,178.926859 217.924625,179.23531 C217.975291,179.307819 218.02826,179.38263 218.073168,179.459743 C218.121531,179.539157 218.167591,179.620874 218.211348,179.703741 C219.087638,181.365693 218.354134,183.088645 216.638402,184.777068 C214.990608,186.397586 212.427373,187.987029 209.512932,189.459077 C209.297602,189.568416 209.079969,189.677755 208.861184,189.783641 C200.189252,194.039803 188.835482,197.245161 188.676575,197.285443 C185.650438,198.069231 181.323109,199.099319 176.448818,199.869295 L175.726828,199.980936 L175.609375,199.9982 C174.503937,200.163935 173.395045,200.310104 172.283849,200.436707 L172.181366,200.447065 C170.160487,200.677253 168.08779,200.844138 166.011639,200.914346 L165.980549,200.914346 C165.234378,200.941968 164.489359,200.954628 163.743188,200.954628 L162.884171,200.954628 C161.746491,200.938515 160.609962,200.887874 159.475737,200.800403 C159.449252,200.800403 159.421616,200.800403 159.39398,200.794648 C158.988653,200.763573 158.582174,200.725592 158.177998,200.680705 C157.482494,200.605895 156.78814,200.51382 156.09609,200.405632 C155.826639,200.360745 155.538765,200.313557 155.261254,200.265217 C155.134589,200.242199 155.009076,200.21918 154.883563,200.193859 L154.853624,200.188105 C154.454054,200.112143 154.056787,200.028125 153.660672,199.937201 C153.431524,199.885408 153.201224,199.833616 152.974379,199.772617 L152.838502,199.736938 C152.725655,199.709315 152.615111,199.679391 152.504568,199.649467 L152.443538,199.633353 L152.087725,199.53092 C151.958758,199.49409 151.830941,199.456109 151.701974,199.418128 L151.655914,199.404317 L151.320828,199.301884 C151.194163,199.262752 151.06865,199.221318 150.943136,199.181036 C150.839501,199.146507 150.737018,199.110828 150.633383,199.077451 L150.406538,198.998037 C150.187754,198.922075 149.971272,198.841509 149.75479,198.758642 L149.549824,198.679227 L149.380554,198.611322 C149.05468,198.480115 148.729957,198.343154 148.408689,198.199287 L148.194511,198.096854 L148.158814,198.08074 C148.082815,198.045061 148.007968,198.009382 147.931969,197.977156 C147.783426,197.905798 147.636034,197.832138 147.489794,197.757327 L147.446037,197.73546 L147.234161,197.623819 C146.857621,197.42816 146.48799,197.218689 146.12642,196.995408 L145.928362,196.874559 C145.825879,196.812409 145.732608,196.749107 145.63473,196.685806 L145.376795,196.514316 C145.283524,196.452166 145.190252,196.388864 145.099284,196.323261 L144.933468,196.205865 C144.75844,196.080413 144.586867,195.952659 144.417597,195.820302 C144.32778,195.751245 144.239115,195.679887 144.150449,195.614284 C143.953543,195.456606 143.761243,195.294324 143.571246,195.127438 L143.563185,195.120532 C143.377794,194.960552 143.197009,194.794817 143.02083,194.62563 L143.011618,194.61412 C142.919498,194.526649 142.829681,194.439178 142.739864,194.348254 C142.651199,194.25733 142.561382,194.167557 142.47502,194.076633 C142.387506,193.984558 142.299992,193.891332 142.214781,193.795805 C142.131873,193.706032 142.051269,193.615108 141.970664,193.523033 L141.943028,193.491958 C141.527337,193.009715 141.138131,192.504454 140.777712,191.978476 L140.634926,191.767855 C140.540503,191.628592 140.447232,191.488178 140.355112,191.347763 L140.083358,190.929973 C139.949785,190.726258 139.819665,190.52024 139.693001,190.313072 C139.602032,190.165752 139.512215,190.018432 139.42355,189.869961 C139.348703,189.743358 139.273855,189.617906 139.202462,189.493605 C139.16216,189.4257 139.124161,189.356644 139.086161,189.287587 C139.048162,189.216229 139.003253,189.14372 138.962951,189.071211 L138.898467,188.957269 C138.876589,188.919288 138.86968,188.907778 138.858165,188.882458 C138.779863,188.745496 138.703864,188.606233 138.630168,188.464668 C138.596775,188.403668 138.563381,188.341518 138.527685,188.280518 L138.42405,188.082557 L138.321566,187.885747 L138.123509,187.486372 C138.091267,187.420769 138.060176,187.355165 138.029086,187.289562 C137.961148,187.147997 137.896664,187.008734 137.83218,186.86947 C137.806847,186.813074 137.780362,186.756679 137.757332,186.700283 C137.668667,186.508076 137.584608,186.313568 137.505154,186.116758 C137.451034,185.986702 137.40152,185.857797 137.348551,185.728892 C137.200007,185.349083 137.060676,184.965822 136.93286,184.577956 C136.89486,184.462863 136.856861,184.34892 136.823468,184.233826 C136.74056,183.972564 136.665712,183.708999 136.598925,183.443133 C136.517169,183.133531 136.444625,182.820477 136.383595,182.506271 C136.344444,182.32097 136.309899,182.134518 136.281112,181.946916 C136.250022,181.761615 136.223537,181.576314 136.201659,181.392165 C136.154447,181.025016 136.121054,180.656716 136.101478,180.287266 C136.101478,180.226266 136.095721,180.164116 136.094569,180.103116 C136.088812,179.981117 136.085357,179.859118 136.084206,179.737118 C136.019722,174.820319 138.510412,170.091121 143.833788,164.772645 C153.315222,155.303892 158.835504,141.447771 158.835504,141.447771 C158.835504,141.447771 158.984047,140.866548 159.2938,140.030968 C159.397435,139.753592 159.505676,139.477368 159.619674,139.204596 C159.749793,138.896145 159.889124,138.591147 160.038819,138.291903 L160.082576,138.204432 C160.274876,137.822321 160.483297,137.450569 160.710143,137.088024 C160.76196,137.004006 160.812626,136.919987 160.872504,136.83712 C161.040622,136.586216 161.219105,136.343368 161.406799,136.107426 C161.551888,135.926729 161.706188,135.751787 161.86855,135.586052 C162.418966,135.019792 163.046532,134.557115 163.757006,134.309664 L163.846823,134.280891 C163.906701,134.261325 163.965427,134.24291 164.026457,134.225646 C164.09785,134.207231 164.169243,134.189967 164.240636,134.175004 L164.272877,134.16925 C164.423724,134.139325 164.57457,134.118608 164.727719,134.10825 L164.741537,134.10825 C164.820991,134.10825 164.900444,134.100194 164.9822,134.100194 C165.186015,134.101344 165.388679,134.117458 165.590191,134.148533 C166.444603,134.283192 167.258712,134.791906 167.958823,135.576845 L168.053245,135.687335 C168.281242,135.954352 168.488512,136.239784 168.672751,136.539027 C168.810931,136.761158 168.94105,136.994798 169.064261,137.239948 C169.113775,137.343532 169.160987,137.437909 169.208198,137.539191 C169.231228,137.590983 169.254258,137.641624 169.276136,137.694567 C169.322196,137.797001 169.364802,137.900585 169.407407,138.008773 C169.64807,138.625675 169.834613,139.262143 169.965884,139.911271 C170.105215,140.612191 170.18582,141.32347 170.20885,142.038201 C170.21691,142.352407 170.21691,142.670065 170.20885,142.992327 C170.203092,143.114326 170.197335,143.237477 170.188123,143.360627 C170.0937,144.72909 169.795462,146.075685 169.302621,147.356677 C169.253106,147.48328 169.198986,147.612185 169.14832,147.74109 C168.900748,148.332671 168.612873,148.905838 168.288151,149.458287 C168.10276,149.773644 167.902399,150.086698 167.688221,150.397451 L167.55695,150.583903 C167.049139,151.292879 166.482602,151.958121 165.863096,152.572721 C165.252802,153.183868 164.598751,153.748977 163.906701,154.265748 C163.212348,154.777914 162.561751,155.346477 161.960669,155.96453 C160.229968,157.780707 159.826944,159.381659 160.21615,160.595897 C160.26221,160.732859 160.31633,160.867518 160.378511,160.997574 C160.522448,161.279553 160.708991,161.538514 160.932382,161.764098 L160.993411,161.825097 C161.056743,161.886097 161.12353,161.944795 161.191469,162.00119 L161.260559,162.057586 C161.428677,162.186491 161.606008,162.301585 161.791399,162.401716 C161.844368,162.43049 161.893883,162.459263 161.954912,162.486886 C162.209393,162.613489 162.475389,162.714771 162.749446,162.791884 L162.825444,162.81145 L162.927928,162.839072 L163.017745,162.86094 L163.113319,162.881657 L163.208893,162.901223 L163.299862,162.916185 C163.3678,162.928845 163.43689,162.939203 163.504828,162.948411 L163.571615,162.957618 L163.690219,162.970279 L163.763915,162.977184 L163.885974,162.986392 L163.956215,162.986392 L164.08288,162.992147 L164.48245,162.992147 L164.596448,162.986392 L164.727719,162.978335 L164.887777,162.963373 L165.037472,162.944958 C165.070866,162.944958 165.105411,162.935751 165.139955,162.929996 C165.667342,162.852883 166.18091,162.704412 166.666842,162.488036 C166.770477,162.444301 166.87296,162.395962 166.967383,162.34532 C167.170046,162.244038 167.366953,162.130095 167.555798,162.004643 C167.819491,161.832003 168.068215,161.637495 168.301969,161.425722 L168.46433,161.271497 C168.489663,161.246176 168.516148,161.222007 168.540329,161.195535 C168.699236,161.036706 168.848931,160.868669 168.988262,160.692576 C169.234682,160.381823 169.453467,160.051504 169.643464,159.703922 C171.896946,155.60889 174.258668,151.573708 176.726329,147.604129 L177.064869,147.062038 L177.406864,146.518796 C177.578437,146.244873 177.75001,145.973252 177.921584,145.705084 L178.09546,145.434614 L178.442061,144.894825 C178.907266,144.17549 179.373623,143.464211 179.843434,142.759838 L180.194641,142.235011 C180.899357,141.192263 181.602923,140.177137 182.298428,139.211501 L182.64618,138.735014 C183.402714,137.689964 184.19149,136.669083 185.010205,135.672372 L185.336079,135.283356 C185.389048,135.217753 185.438563,135.154451 185.497289,135.09115 L185.816254,134.721699 C185.869223,134.660699 185.919889,134.600851 185.975161,134.542153 L186.283762,134.193419 L186.439214,134.025383 C186.588909,133.863101 186.740907,133.703121 186.895208,133.544291 C186.998842,133.441858 187.094417,133.338274 187.194597,133.242746 C187.778407,132.64541 188.422094,132.110225 189.116447,131.646397 L189.276506,131.543964 L189.438867,131.44038 C189.542502,131.375927 189.647288,131.313777 189.754377,131.255079 C192.476519,129.711674 194.731152,129.597731 196.027738,130.893685 C196.814212,131.679775 197.248326,132.981483 197.225296,134.791906 C197.225296,134.872472 197.225296,134.951886 197.218387,135.034754 L197.218387,135.124527 C197.218387,135.206243 197.211478,135.290262 197.205721,135.37428 C197.205721,135.476713 197.193054,135.580298 197.182691,135.682731 C197.172327,135.786315 197.167722,135.864579 197.157358,135.955503 C197.157358,135.981974 197.151601,136.008446 197.148146,136.034917 C197.140086,136.115483 197.129722,136.197199 197.119359,136.277765 C197.119359,136.303086 197.119359,136.327255 197.108995,136.352576 C197.09748,136.461915 197.081359,136.571254 197.061784,136.679442 C197.049117,136.779573 197.032996,136.879705 197.013421,136.979836 C197.00536,137.037383 196.993845,137.093779 196.984633,137.151326 C196.954694,137.314759 196.915543,137.477041 196.868332,137.63587 C196.779667,137.932811 196.671426,138.223998 196.54361,138.507128 C196.488338,138.630279 196.429611,138.753429 196.369733,138.874277 C196.245372,139.12403 196.104889,139.379538 195.950588,139.644253 C195.873438,139.774309 195.793984,139.908969 195.708774,140.043628 L195.579806,140.248495 C195.448535,140.454513 195.311507,140.662832 195.168721,140.873453 C195.025935,141.084075 194.872786,141.2993 194.708122,141.516827 C194.495095,141.806863 194.270552,142.102653 194.033344,142.401897 L193.85371,142.628631 C193.3459,143.260495 192.825422,143.882001 192.29343,144.493148 L191.870831,144.970787 L191.6555,145.212483 C191.361868,145.5405 191.067085,145.866215 190.769998,146.189628 L190.542002,146.435928 C190.391156,146.599361 190.2334,146.765096 190.082554,146.92968 C189.928253,147.096566 189.77165,147.262301 189.615046,147.428035 L189.140629,147.927542 L188.660454,148.428199 C188.500396,148.595085 188.339186,148.764272 188.176825,148.931158 L187.199203,149.938227 C182.529879,154.724971 177.609528,159.495602 175.944461,162.469621 C175.833918,162.66413 175.733737,162.863242 175.642769,163.066957 C175.406712,163.607897 175.307683,164.070574 175.37447,164.444628 C175.396348,164.567778 175.440105,164.686325 175.503438,164.793362 C175.597861,164.955644 175.711859,165.105266 175.84313,165.238774 C175.904159,165.298623 175.967491,165.355019 176.036581,165.40566 C176.378576,165.649658 176.793115,165.77511 177.213412,165.76245 L177.344683,165.76245 L177.479408,165.753243 L177.615285,165.73713 L177.72698,165.721016 C177.74195,165.719865 177.756919,165.716413 177.771889,165.71296 L177.874372,165.693394 L177.902008,165.686488 L178.014855,165.662319 L178.055157,165.65196 L178.176065,165.620885 L178.320002,165.5783 C178.414425,165.549527 178.508848,165.517301 178.605573,165.481622 C178.813994,165.407962 179.01781,165.322792 179.217019,165.227265 C179.267685,165.203095 179.319502,165.180076 179.369017,165.153605 L179.522166,165.077643 C179.722526,164.976361 179.921736,164.867022 180.122096,164.748475 C180.282154,164.654099 180.441061,164.55742 180.599968,164.454987 L180.754269,164.352553 C180.804935,164.320327 180.856752,164.28695 180.907418,164.248969 L181.060567,164.146536 L181.141172,164.09014 L181.36226,163.935914 C181.568378,163.793198 181.760678,163.647029 181.956433,163.498559 L181.973705,163.485898 L182.282307,163.246503 C182.704906,162.912732 183.106779,162.576659 183.469501,162.260151 L183.711316,162.047228 L183.734346,162.026511 L183.859859,161.91487 C184.156945,161.649004 184.42179,161.400402 184.642877,161.194384 L184.732694,161.106913 L184.950327,160.900895 L185.080447,160.774292 L185.125355,160.728255 L185.138022,160.715595 C185.248565,160.605105 185.248565,160.605105 185.359109,160.493464 L185.368321,160.484256 L185.409775,160.447426 C185.422442,160.433615 185.439714,160.419804 185.45929,160.40254 L185.475411,160.387578 L185.519167,160.348446 L185.752922,160.142428 L185.884192,160.023882 C185.948676,159.968637 186.018918,159.905335 186.090311,159.83743 L186.248066,159.699318 C186.275702,159.676299 186.303338,159.649828 186.332125,159.624507 L186.496789,159.48064 L186.739755,159.268868 L186.867572,159.157227 C187.368473,158.721022 187.978767,158.195044 188.677727,157.602312 L188.96445,157.360615 L189.438867,156.963542 L189.922496,156.558413 C190.55582,156.033586 191.237507,155.475382 191.958344,154.89531 L192.43161,154.515501 C192.834634,154.195541 193.249174,153.867524 193.668319,153.538356 C193.836438,153.405998 194.008011,153.273641 194.183039,153.142434 C194.612547,152.810964 195.044359,152.480646 195.477322,152.154931 C196.465308,151.409124 197.484383,150.662166 198.514974,149.933624 L198.958301,149.626324 C199.266902,149.41225 199.575504,149.199326 199.885257,148.991007 L200.165071,148.801102 C200.718941,148.428199 201.277418,148.063352 201.8405,147.706562 L202.120315,147.530469 L202.397826,147.357828 C202.675337,147.186339 202.952848,147.016 203.226904,146.851416 L203.502112,146.687983 L204.05368,146.366872 C204.234465,146.262137 204.414099,146.159704 204.593732,146.058421 L204.701973,145.997422 L205.13033,145.763782 C205.307661,145.667103 205.486143,145.573877 205.662322,145.482953 L205.927167,145.347143 L206.1828,145.217087 C206.767761,144.923598 207.363085,144.651977 207.968773,144.403375 C208.051681,144.368847 208.133437,144.33547 208.215194,144.299791 C208.377555,144.231885 208.538764,144.169735 208.698823,144.11449 C208.858881,144.060396 209.016636,144.004 209.172088,143.948755 C209.313723,143.901567 209.450751,143.857831 209.584325,143.816397 L209.629233,143.802586 C209.700626,143.780718 209.772019,143.760002 209.843412,143.741587 L209.864139,143.735832 C210.009227,143.694398 210.154316,143.656417 210.300556,143.621889 L210.312071,143.621889 C210.527402,143.570097 210.736974,143.526362 210.941941,143.491833 C211.012182,143.480324 211.081272,143.468815 211.148059,143.459607 C211.347268,143.429683 211.547629,143.408966 211.747989,143.395155 C211.874654,143.385947 212.002471,143.382494 212.129135,143.382494 L212.225861,143.382494 C212.354829,143.382494 212.479191,143.390551 212.602401,143.403211 C212.658824,143.403211 212.715248,143.41357 212.77052,143.421626 L212.79355,143.421626 C212.848822,143.427381 212.905245,143.436588 212.959365,143.448098 C213.013486,143.459607 213.068758,143.467664 213.121727,143.480324 L213.140151,143.480324 C213.19312,143.491833 213.242634,143.506796 213.297906,143.521758 C213.737778,143.640304 214.141954,143.863586 214.47704,144.172037 L214.506979,144.201961 L214.565705,144.259508 L214.62328,144.320508 C215.085031,144.806203 215.467328,145.362105 215.755203,145.967497 L215.800111,146.069931 C215.897988,146.300118 215.975139,146.538362 216.029259,146.78236 C216.178954,147.462564 216.138651,148.17154 215.911806,148.829876 C215.813929,149.116459 215.694173,149.394986 215.552539,149.662003 L215.458116,149.832341 C215.098849,150.434281 214.675097,150.994787 214.19262,151.503501 L214.088985,151.61284 C213.86214,151.853385 213.626083,152.087025 213.383117,152.311458 C213.305966,152.383967 213.226513,152.456476 213.145908,152.528985 L213.023849,152.639475 C212.77052,152.863907 212.511433,153.082585 212.248891,153.296659 C212.202831,153.333489 212.15562,153.370319 212.110711,153.409451 C211.927623,153.556771 211.738777,153.706393 211.543023,153.856014 C210.868245,154.371634 210.180801,154.868838 209.48069,155.348779 C209.068454,155.630758 208.642399,155.915039 208.202527,156.203924 C207.38266,156.741412 206.557036,157.26739 205.723352,157.783009 C205.482689,157.93148 205.240874,158.082252 204.994454,158.231874 C202.729457,159.624507 200.21804,161.113819 197.539655,162.733186 L196.846454,163.154429 C196.087616,163.617105 195.371385,164.059064 194.697758,164.479156 L194.358066,164.69323 L193.71553,165.105266 C193.461049,165.266397 193.213477,165.425226 192.972814,165.581753 C192.790878,165.697998 192.613547,165.813091 192.439671,165.927034 L192.096524,166.152618 C191.929557,166.261956 191.763741,166.371295 191.597926,166.482936 L191.436716,166.58537 C191.266294,166.699312 191.100478,166.810953 190.939269,166.921443 L190.675576,167.100989 L190.365823,167.316214 L190.080251,167.515326 C189.628864,167.831834 189.183234,168.155247 188.741059,168.484414 L188.586758,168.60181 C188.412882,168.733017 188.244763,168.864223 188.082402,168.991977 L187.870526,169.161165 C187.513562,169.447748 187.189991,169.722822 186.897511,169.985235 L186.754725,170.116442 C186.672969,170.191253 186.592364,170.266064 186.516365,170.339724 C186.462244,170.390365 186.41273,170.442157 186.35861,170.492798 L186.283762,170.566458 C186.119098,170.731042 185.95904,170.90023 185.803588,171.074021 L185.727589,171.161492 C185.558318,171.357151 185.410927,171.544754 185.280807,171.727753 L185.223232,171.81062 C185.073538,172.029298 184.941115,172.260636 184.828269,172.501182 L184.789118,172.5852 L184.764936,172.643898 L184.745361,172.694539 L184.718876,172.764746 C184.710816,172.788916 184.694695,172.831501 184.685483,172.868331 C184.638272,173.005292 184.603727,173.146857 184.580697,173.290724 L184.569182,173.367837 L184.55997,173.438044 L184.554212,173.505949 L184.554212,173.867343 C184.554212,173.897268 184.554212,173.928343 184.561121,173.959418 C184.562273,173.977833 184.564576,173.996248 184.566879,174.014663 C184.566879,174.044587 184.574939,174.074512 184.578394,174.105587 L184.600272,174.23219 L184.600272,174.237945 C184.608333,174.278228 184.616393,174.31851 184.626757,174.358793 C184.63712,174.399076 184.647483,174.443962 184.66015,174.486547 C184.696998,174.616603 184.741906,174.745508 184.793724,174.87096 C184.793724,174.880167 184.801784,174.890526 184.805239,174.899733 C184.819057,174.930808 184.831723,174.963035 184.846693,174.99411 C184.903116,175.125317 184.966448,175.253071 185.035538,175.378523 L185.108083,175.508578 L185.184082,175.639785 L185.263535,175.770992 C185.277353,175.79286 185.292322,175.812426 185.309595,175.831992 L185.336079,175.859614 L185.363715,175.884935 L185.394806,175.909104 C185.415533,175.924066 185.437411,175.936727 185.460441,175.949387 C185.483471,175.959745 185.508804,175.971255 185.534137,175.980462 C185.561773,175.98967 185.589409,175.997726 185.617045,176.004632 C186.281459,176.154254 187.647136,175.604106 189.460745,174.646527 L189.786619,174.473887 L190.338187,174.173492 L190.608789,174.023871 C190.799937,173.920286 190.996844,173.805193 191.196053,173.69125 L191.561078,173.485232 C193.959648,172.10526 196.825727,170.331667 199.78738,168.600659 C200.063739,168.438377 200.342402,168.277246 200.621064,168.116115 L201.180692,167.759325 C201.460506,167.598193 201.739169,167.439364 202.018983,167.282837 C203.209632,166.606086 204.416402,165.955807 205.635838,165.333151 L206.1828,165.055775 C206.546673,164.876229 206.907092,164.701287 207.262905,164.530948 C207.952652,164.204082 208.649308,163.894481 209.352874,163.600992 L209.738625,163.444465 L209.785837,163.42605 C211.828594,162.616941 213.681355,162.116284 215.180605,162.116284 C215.505328,162.113982 215.828898,162.140454 216.149015,162.195699 L216.158227,162.195699 C216.261862,162.214114 216.355133,162.23483 216.449556,162.259 L216.466828,162.259 C216.605008,162.292377 216.740885,162.334962 216.872156,162.387905 C216.976942,162.425886 217.078274,162.470772 217.176151,162.523715 C217.60566,162.741242 217.972988,163.062354 218.247044,163.458276 C218.270074,163.491653 218.291953,163.526181 218.31268,163.56186 C218.414012,163.716085 218.501525,163.878367 218.572918,164.048706 C218.615524,164.146536 218.654675,164.247818 218.691523,164.357157 C218.728371,164.467647 218.760612,164.573533 218.794006,164.687476 C219.038123,165.559885 219.013942,166.482936 218.724916,167.341535 Z" id="Shape" fill="#FF9D00" fillRule="nonzero"></path>
|
10 |
+
<path d="M205.169417,104.827718 L205.169417,104.263487 C205.169417,56.0689579 166.09852,17 117.882512,17 C69.6688088,17 30.5830519,56.0712609 30.5830519,104.263487 L30.5805699,104.452331 C30.5805699,104.577844 30.5805699,104.702205 30.5805699,104.827718 C30.5874292,105.015411 30.5893875,105.203104 30.5876596,105.390797 L30.5945712,105.805334 L30.596875,105.955028 L30.6026346,106.159993 C30.6049385,106.270536 30.6083942,106.379928 30.6083942,106.490471 L30.6199134,106.938402 L30.6383442,107.464633 L30.6441038,107.637357 L30.6671422,108.182012 L30.6671422,108.207345 L30.6993961,108.84412 L30.6993961,108.860241 C30.7109153,109.077873 30.7224345,109.295505 30.7362576,109.511985 C30.7477768,109.713496 30.7604479,109.913856 30.774271,110.114215 L30.7777268,110.149912 C30.7878637,110.321484 30.7993829,110.494208 30.8122844,110.66578 L30.8157402,110.710688 C30.8272594,110.876503 30.8399306,111.043469 30.8537536,111.208132 L30.8560575,111.240374 L30.8790959,111.528247 C30.9480961,112.358472 31.0287306,113.188698 31.1209996,114.01662 L31.1267592,114.068437 L31.1958745,114.676425 L31.2753572,115.316654 C31.3286912,115.742706 31.3855961,116.167607 31.4458417,116.592507 C31.4817816,116.84929 31.5191039,117.104921 31.5575781,117.360552 L31.5633377,117.391642 C31.6493863,117.969691 31.7422312,118.54774 31.8421029,119.123486 C32.1285859,120.7943 32.4650624,122.457054 32.8511867,124.108293 L32.8569463,124.134777 L32.8938079,124.290228 C32.9283655,124.437619 32.9629232,124.58501 32.9997847,124.733552 L33.0631404,124.656403 C35.5973693,121.62798 39.2328357,119.958317 43.300273,119.958317 C46.5602128,119.958317 49.9053949,121.034962 53.2563365,123.159464 C55.4795464,124.571192 57.9377483,127.072232 60.4673695,130.216955 C62.8080754,126.966295 66.0886346,124.807248 69.8415971,124.217684 C70.5580927,124.104838 71.2814999,124.047264 72.004907,124.046112 C80.5717524,124.046112 85.7312119,131.477839 87.6802643,138.161096 C88.6421193,140.420322 93.2843658,150.704294 100.283445,157.661606 C110.921447,168.286419 113.583539,179.250921 108.319254,190.990378 L108.336533,190.990378 C108.695933,191.029529 109.056484,191.066377 109.418188,191.09977 C109.633597,191.120497 109.849007,191.141224 110.064416,191.159647 L110.140443,191.166556 C110.298257,191.181526 110.457222,191.195344 110.616187,191.206859 L111.053918,191.242555 C111.470913,191.274797 111.889061,191.303584 112.309513,191.33122 L112.641266,191.350795 L112.905057,191.365764 L113.069781,191.374976 L113.583539,191.402612 L113.724073,191.40837 L114.265477,191.431399 L114.865628,191.454429 L115.349435,191.468247 L115.43007,191.471702 C115.531439,191.475156 115.635112,191.479762 115.737633,191.479762 L115.847066,191.482065 C116.5267,191.498186 117.205182,191.506246 117.884816,191.506246 L118.605919,191.506246 L119.121981,191.500489 C119.248692,191.49358 119.374252,191.49358 119.500963,191.49358 L119.701397,191.49358 L119.875338,191.490126 C119.943301,191.487823 120.012416,191.48552 120.08038,191.48552 L120.286574,191.48552 L120.690899,191.474005 L121.299114,191.453278 L121.969532,191.424491 C122.161903,191.41643 122.353122,191.407218 122.545493,191.396855 L122.751687,191.38534 L123.056947,191.368067 L123.237799,191.357704 L123.372574,191.349644 L123.991156,191.30819 C124.19735,191.295523 124.403544,191.279403 124.609738,191.263282 L125.169572,191.219525 L125.564681,191.186132 L125.996652,191.146981 C126.440142,191.105527 126.88248,191.061771 127.322515,191.015711 C127.603584,190.984621 127.882349,190.95353 128.162266,190.920137 L128.654137,190.861411 C123.471639,179.170317 126.154466,168.255328 136.749846,157.671969 C143.732798,150.697385 148.375045,140.41111 149.342659,138.153035 C151.292864,131.468627 156.447716,124.038052 165.016865,124.038052 C165.741424,124.039203 166.464831,124.095626 167.180175,124.209624 C170.929681,124.799188 174.211508,126.959386 176.555669,130.208895 C179.086443,127.064171 181.543493,124.563132 183.766702,123.151403 C187.117644,121.026901 190.46513,119.950256 193.721614,119.950256 C197.311004,119.950256 200.565184,121.251442 203.022234,123.637333 C203.157009,123.036254 203.287176,122.435175 203.411584,121.831794 C203.48761,121.455256 203.562485,121.077567 203.636208,120.698726 C203.726058,120.233523 203.811301,119.766018 203.889631,119.298512 C203.910366,119.171848 203.932252,119.045184 203.956443,118.917369 C203.994456,118.71816 204.027862,118.521255 204.058964,118.320896 L204.071635,118.244897 L204.153421,117.723272 L204.234056,117.194737 L204.235208,117.18898 C204.267462,116.967893 204.300867,116.745655 204.330817,116.523418 C204.355008,116.3553 204.378046,116.187182 204.399933,116.017913 L204.413756,115.921187 L204.485175,115.370774 L204.543923,114.902117 C204.579633,114.593518 204.61419,114.284918 204.647596,113.975167 C204.662571,113.851957 204.675242,113.727596 204.687913,113.603235 L204.699433,113.503055 C204.721319,113.296938 204.740902,113.091973 204.759333,112.885856 L204.805409,112.406835 C204.901019,111.34516 204.978198,110.283485 205.038098,109.219506 L205.038098,109.212597 L205.0692,108.648367 L205.074959,108.521703 L205.097998,107.986259 C205.107213,107.784748 205.115276,107.580934 205.122188,107.374817 L205.122188,107.359847 L205.127948,107.211305 C205.133707,107.084641 205.138315,106.957977 205.138315,106.832464 L205.140619,106.780647 C205.145226,106.663195 205.149834,106.541137 205.149834,106.42023 L205.15329,106.316596 C205.156746,106.19569 205.160201,106.075934 205.160201,105.955028 L205.162505,105.852545 C205.165961,105.698245 205.169417,105.545097 205.169417,105.390797 L205.169417,104.827718 Z M96.2309827,192.003691 C103.143668,181.868262 102.654101,174.261508 93.1680216,164.784733 C83.6819423,155.307957 78.1607792,141.448604 78.1607792,141.448604 C78.1607792,141.448604 76.0999904,133.397375 71.4001479,134.137784 C66.7003053,134.877042 63.2527174,146.906674 73.0923216,154.270463 C82.932041,161.634252 71.1352058,166.627119 67.3476856,159.717018 C63.5591287,152.805766 53.219475,135.041705 47.8572772,131.643653 C42.4962313,128.245602 38.7259899,130.150169 39.9896486,137.153541 C40.6174462,140.629894 46.4553879,146.562378 51.9097395,152.105659 C57.4435737,157.730695 62.5822986,162.95271 61.4845168,164.800854 C59.3039281,168.471809 51.6240628,160.488518 51.6240628,160.488518 C51.6240628,160.488518 27.576535,138.613632 22.3410486,144.314667 C17.5156465,149.568923 24.9570639,154.035559 36.4256013,160.919175 C37.4012794,161.505285 38.4057556,162.108666 39.4344221,162.730472 C52.5548159,170.671158 53.575419,172.765721 51.7139127,175.769962 C51.0262152,176.88 46.6339358,174.244236 41.2314207,171.001636 C32.0218028,165.474476 19.877087,158.186686 18.1630268,164.709886 C16.6781991,170.355649 25.6125077,173.81473 33.7151284,176.952544 C40.4653925,179.56643 46.6396954,181.956927 45.7319807,185.20183 C44.7908603,188.565337 39.6889969,185.760303 34.1113896,182.69273 C27.8506925,179.248619 20.9909958,175.475181 18.7470514,179.733396 C14.50337,187.782323 48.0173944,197.257947 48.2927038,197.327036 C59.1207724,200.134373 86.6193439,206.082978 96.2309827,192.003691 Z M140.768903,192.003691 C133.855066,181.868262 134.345784,174.261508 143.831864,164.784733 C153.317943,155.307957 158.840258,141.448604 158.840258,141.448604 C158.840258,141.448604 160.899895,133.397375 165.599737,134.137784 C170.29958,134.877042 173.748435,146.906674 163.907564,154.270463 C154.066692,161.634252 165.86468,166.627119 169.653352,159.717018 C173.440872,152.805766 183.775918,135.041705 189.136964,131.643653 C194.49801,128.245602 198.269403,130.150169 197.005744,137.153541 C196.377947,140.629894 190.540005,146.56353 185.084501,152.10681 C179.551819,157.730695 174.413094,162.95271 175.510876,164.800854 C177.690313,168.471809 185.37709,160.483912 185.37709,160.483912 C185.37709,160.483912 209.423465,138.609026 214.658952,144.310061 C219.484354,149.564317 212.041785,154.032104 200.573247,160.916873 C199.597569,161.50183 198.594245,162.10406 197.565578,162.727017 C184.444033,170.666552 183.424581,172.762266 185.286088,175.765356 C185.973785,176.875394 190.366065,174.23963 195.767428,170.998181 C204.978198,165.471022 217.122913,158.18208 218.836974,164.70528 C220.321801,170.351043 211.387493,173.811275 203.28372,176.94909 C196.533456,179.562976 190.360305,181.952321 191.266868,185.198376 C192.207988,188.561882 197.307548,185.755697 202.882851,182.688124 C209.143548,179.244013 216.004397,175.469423 218.249493,179.72879 C222.493175,187.783474 188.974543,197.248735 188.702689,197.318976 C177.87462,200.134373 150.375934,206.082978 140.768903,192.003691 Z" id="Shape" fill="#FFD21E"></path>
|
11 |
+
<path d="M146.614758,80.6109193 C147.976342,81.0911 148.989747,82.5534685 149.953577,83.9434653 C151.256362,85.8239816 152.46922,87.5723908 154.330012,86.5867567 C156.5263,85.424214 158.325988,83.6390445 159.503106,81.456405 C160.680224,79.2743398 161.180586,76.793253 160.941934,74.3272149 C160.764386,72.4833439 160.178709,70.7015059 159.226408,69.1104765 C158.274107,67.5193323 156.979392,66.1585139 155.436803,65.1258957 C153.89306,64.0933923 152.138336,63.414936 150.300602,63.139349 C148.461715,62.8637621 146.584783,62.9980518 144.804694,63.532339 C142.423941,64.2467514 140.30951,65.6481209 138.727721,67.5595388 C137.147086,69.4708418 136.169421,71.806381 135.91924,74.2709257 C135.670212,76.7360448 136.160198,79.2193143 137.326939,81.4070084 C138.189314,83.0233104 140.10314,82.2616841 142.123033,81.456405 C143.707127,80.825737 145.355784,80.1686476 146.614758,80.6109193 Z M87.3830077,80.6120681 C86.0214245,81.0922488 85.0068663,82.5546173 84.043036,83.9446141 C82.7402511,85.8239816 81.5273929,87.5735396 79.666601,86.5879055 C78.0260143,85.7182959 76.5998684,84.4960178 75.4896189,83.0106741 C74.3793695,81.5253304 73.6138392,79.8136815 73.2472148,77.9977254 C72.8805903,76.1821139 72.922095,74.3082604 73.3694229,72.5107992 C73.8179037,70.7133381 74.6595259,69.036956 75.8343381,67.6023875 C77.0114562,66.168623 78.4917888,65.0125133 80.1704214,64.2172284 C81.849054,63.4219434 83.684482,63.0074717 85.5429681,63.0033066 C87.4014542,62.9993155 89.238035,63.4058608 90.9201264,64.1936788 C92.6022177,64.9816117 94.0883149,66.1312884 95.2711974,67.5598834 C96.8529859,69.4711864 97.830651,71.8071852 98.0808318,74.2723042 C98.3298598,76.7374233 97.8398743,79.2208077 96.6719795,81.4081571 C95.8072992,83.0244592 93.8934736,82.2616841 91.8735805,81.456405 C90.2906392,80.825737 88.6419821,80.1686476 87.3830077,80.6120681 Z M137.451453,134.317638 C146.839575,126.947209 150.28792,114.91627 150.28792,107.504485 C150.28792,101.645821 146.331143,103.489577 139.998225,106.613049 L139.641977,106.789958 C133.827867,109.658406 126.089555,113.476876 117.594936,113.476876 C109.099164,113.476876 101.360852,109.657257 95.549048,106.788809 C89.0109124,103.561949 84.9100221,101.537838 84.9100221,107.505634 C84.9100221,115.150616 88.5785722,127.705389 98.649215,135 C99.9773639,132.311907 101.847379,129.925937 104.14397,127.991429 C106.439408,126.056922 109.111846,124.616379 111.992961,123.758257 C112.998296,123.45958 114.033606,125.183866 115.094281,126.948357 C116.116909,128.651965 117.161443,130.393482 118.225576,130.393482 C119.357731,130.393482 120.471439,128.677238 121.55863,127.0012 C122.694244,125.250494 123.801034,123.54344 124.872085,123.884621 C130.263078,125.608906 134.772098,129.348112 137.451453,134.317638 Z" id="Shape" fill="#32343D"></path>
|
12 |
+
<path d="M137,134.589063 C132.085163,138.426668 125.552521,141 117.046296,141 C109.053457,141 102.801266,138.72867 98,135.271151 C99.3361959,132.585286 101.217068,130.200275 103.524411,128.267693 C105.831753,126.333962 108.518052,124.892851 111.41295,124.033925 C113.398122,123.447146 115.500342,130.666483 117.67673,130.666483 C120.00725,130.666483 122.252013,123.493078 124.358868,124.160238 C129.775503,125.884979 134.306748,129.622682 137,134.589063 Z" id="Path" fill="#FF323D" fillRule="nonzero"></path>
|
13 |
+
<path d="M64.7091655,90.6371141 C63.8188025,91.2251504 62.8188988,91.6348571 61.7668713,91.8425321 C60.7147288,92.0502071 59.6311738,92.0524645 58.5782258,91.8493041 C57.5252778,91.6450151 56.5236482,91.2398231 55.6307538,90.6563014 C54.7378594,90.0716511 53.971305,89.3210865 53.3749298,88.4452396 C52.7785546,87.5705215 52.3642101,86.5874511 52.1555996,85.555848 C51.9469891,84.5231163 51.9481398,83.4599105 52.1590515,82.4271787 C52.3699633,81.3955756 52.786609,80.4147626 53.3848253,79.5400444 C53.9831565,78.6662292 54.7514369,77.9166804 55.645597,77.3345131 C57.4440428,76.1637452 59.642335,75.7399301 61.7587018,76.1560702 C63.8750686,76.5722103 65.7370296,77.7943327 66.9366838,79.5547171 C68.1356477,81.3154401 68.5751911,83.4700685 68.1575098,85.5468187 C67.7398285,87.6246976 66.4994417,89.455398 64.7091655,90.6371141 Z M181.39746,90.6371141 C180.506867,91.2251504 179.506963,91.6348571 178.455281,91.8425321 C177.403599,92.0502071 176.319699,92.0524645 175.266866,91.8493041 C174.214033,91.6450151 173.211828,91.2398231 172.318933,90.6563014 C171.426039,90.0716511 170.659715,89.3210865 170.063685,88.4452396 C169.466504,87.5705215 169.052275,86.5874511 168.844009,85.555848 C168.635744,84.5231163 168.636895,83.4599105 168.847461,82.4271787 C169.058028,81.3955756 169.474559,80.4147626 170.07289,79.5400444 C170.671221,78.6662292 171.439847,77.9166804 172.333892,77.3345131 C174.132338,76.1637452 176.331205,75.7399301 178.447227,76.1560702 C180.563248,76.5722103 182.424979,77.7943327 183.625094,79.5547171 C184.824057,81.3154401 185.263601,83.4700685 184.84592,85.5468187 C184.428238,87.6246976 183.187852,89.455398 181.39746,90.6371141 Z" id="Shape" fill="#FFAD03"></path>
|
14 |
+
</g>
|
15 |
+
</g>
|
16 |
+
</svg>
|
17 |
+
);
|
18 |
+
|
19 |
+
export default HFLogo;
|
frontend/src/components/Logo/Logo.js
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
|
3 |
+
import { Box } from "@mui/material";
|
4 |
+
import HFLogo from "./HFLogo";
|
5 |
+
import { useLeaderboard } from "../../pages/LeaderboardPage/components/Leaderboard/context/LeaderboardContext";
|
6 |
+
|
7 |
+
const Logo = ({ height = "40px" }) => {
|
8 |
+
const navigate = useNavigate();
|
9 |
+
const [searchParams, setSearchParams] = useSearchParams();
|
10 |
+
const location = useLocation();
|
11 |
+
const { actions } = useLeaderboard();
|
12 |
+
|
13 |
+
const handleReset = () => {
|
14 |
+
// Reset all leaderboard state first
|
15 |
+
actions.resetAll();
|
16 |
+
|
17 |
+
// Then clean URL in one go
|
18 |
+
if (
|
19 |
+
location.pathname !== "/" ||
|
20 |
+
searchParams.toString() !== "" ||
|
21 |
+
location.hash !== ""
|
22 |
+
) {
|
23 |
+
window.history.replaceState(null, "", "/");
|
24 |
+
navigate("/", { replace: true, state: { skipUrlSync: true } });
|
25 |
+
setSearchParams({}, { replace: true, state: { skipUrlSync: true } });
|
26 |
+
}
|
27 |
+
};
|
28 |
+
|
29 |
+
return (
|
30 |
+
<Box
|
31 |
+
onClick={handleReset}
|
32 |
+
sx={{
|
33 |
+
height,
|
34 |
+
display: "flex",
|
35 |
+
alignItems: "center",
|
36 |
+
justifyContent: "center",
|
37 |
+
cursor: "pointer",
|
38 |
+
transition: "opacity 0.2s ease",
|
39 |
+
"&:hover": {
|
40 |
+
opacity: 0.8,
|
41 |
+
},
|
42 |
+
}}
|
43 |
+
>
|
44 |
+
<Box
|
45 |
+
sx={{
|
46 |
+
height: "100%",
|
47 |
+
aspectRatio: "95/88", // Ratio du SVG original (width/height)
|
48 |
+
}}
|
49 |
+
>
|
50 |
+
<HFLogo />
|
51 |
+
</Box>
|
52 |
+
</Box>
|
53 |
+
);
|
54 |
+
};
|
55 |
+
|
56 |
+
export default Logo;
|