docs: add comprehensive README and project scaffolding
- README completo con istruzioni di installazione, configurazione e utilizzo - API Swagger/OpenAPI documentata - File env.example con variabili di configurazione - Dockerfile multi-stage ottimizzato - Docker Compose con Ollama e LLM Monitor - Struttura completa dell'app FastAPI (main.py, config, api routes) - Servizio client Ollama reusabile - Dashboard web HTML con TailwindCSS - Test suite con pytest - Makefile per comandi comuni - CONTRIBUTING.md per i contributori - LICENSE MIT - .editorconfig e .dockerignore - requirements.txt e requirements-dev.txt
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.coverage
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.gitignore
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
CONTRIBUTING.md
|
||||||
|
|
||||||
|
# Development
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
Makefile
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Test
|
||||||
|
tests/
|
||||||
|
pytest.ini
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# Python files
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# JSON files
|
||||||
|
[*.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# YAML files
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Markdown files
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
# Dockerfile
|
||||||
|
[Dockerfile*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# LLM Monitor - Local Development Environment
|
||||||
|
# Copia questo file da env.example e personalizza per il tuo ambiente
|
||||||
|
|
||||||
|
OLLAMA_HOST=http://localhost:11434
|
||||||
|
OLLAMA_TIMEOUT=30
|
||||||
|
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
API_WORKERS=1
|
||||||
|
|
||||||
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8000
|
||||||
|
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
|
ENVIRONMENT=development
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node modules (per TailwindCSS)
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
app/web/static/css/output.css
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
uploads/
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
# Contribuire a LLM Monitor
|
||||||
|
|
||||||
|
Grazie per l'interesse nel contribuire a LLM Monitor! Questo documento fornisce linee guida per contribuire al progetto.
|
||||||
|
|
||||||
|
## Codice di Condotta
|
||||||
|
|
||||||
|
Questo progetto aderisce a un Codice di Condotta per garantire un ambiente inclusivo e rispettoso.
|
||||||
|
|
||||||
|
## Come Contribuire
|
||||||
|
|
||||||
|
### Segnalare Bug
|
||||||
|
|
||||||
|
- **Verificare prima** se il bug non è già stato segnalato
|
||||||
|
- **Includere dettagli**: sistema operativo, versione Python, stack trace
|
||||||
|
- **Fornire un esempio ripetibile** se possibile
|
||||||
|
|
||||||
|
### Suggerire Miglioramenti
|
||||||
|
|
||||||
|
- **Verificare prima** se il suggerimento non è già stato fatto
|
||||||
|
- **Spiegare chiaramente** il caso d'uso e i benefici
|
||||||
|
- **Fornire esempi** di come dovrebbe funzionare
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
|
||||||
|
1. **Fork il repository**
|
||||||
|
2. **Crea un branch**: `git checkout -b feature/my-feature`
|
||||||
|
3. **Installa le dipendenze di sviluppo**:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
```
|
||||||
|
4. **Effettua i tuoi cambiamenti** seguendo lo [Style Guide](#style-guide)
|
||||||
|
5. **Scrivi i test**: I test sono obbligatori per nuove funzionalità
|
||||||
|
6. **Esegui i test**: `make test`
|
||||||
|
7. **Formatta il codice**: `make format`
|
||||||
|
8. **Esegui il linting**: `make lint`
|
||||||
|
9. **Fai il commit**: `git commit -m "feat: descrizione della feature"`
|
||||||
|
10. **Push**: `git push origin feature/my-feature`
|
||||||
|
11. **Apri una PR** descrivendo i cambiamenti
|
||||||
|
|
||||||
|
## Style Guide
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
- Usa **Black** per la formattazione: `make format`
|
||||||
|
- Usa **isort** per l'organizzazione degli import
|
||||||
|
- Segui **PEP 8**
|
||||||
|
- Usa type hints per le funzioni nuove
|
||||||
|
- Documenta con docstring (formato Google):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def my_function(param1: str, param2: int) -> bool:
|
||||||
|
"""
|
||||||
|
Descrizione breve della funzione.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
param1: Descrizione del primo parametro
|
||||||
|
param2: Descrizione del secondo parametro
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Descrizione del valore ritornato
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Quando succede
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
Usa il formato Conventional Commits:
|
||||||
|
- `feat: aggiungi nuova feature`
|
||||||
|
- `fix: correggi un bug`
|
||||||
|
- `docs: aggiorna documentazione`
|
||||||
|
- `style: formattazione, without semantic change`
|
||||||
|
- `refactor: ristruttura codice`
|
||||||
|
- `perf: migliora le performance`
|
||||||
|
- `test: aggiungi o modifica test`
|
||||||
|
- `chore: aggiorna dipendenze, etc`
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
```
|
||||||
|
feat: aggiungi endpoint per ottenere statistiche modelli
|
||||||
|
|
||||||
|
- Nuovo endpoint GET /api/v1/models/stats
|
||||||
|
- Ritorna conteggio, spazio totale e ultimi aggiornamenti
|
||||||
|
- Include test di integrazione
|
||||||
|
```
|
||||||
|
|
||||||
|
### Codice
|
||||||
|
|
||||||
|
- Mantieni le funzioni piccole e ben definite
|
||||||
|
- Usa nomi descrittivi
|
||||||
|
- Aggiungi commenti per la logica complessa
|
||||||
|
- Evita magic numbers, usa costanti
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Tutti i PR devono includere test per nuove funzionalità
|
||||||
|
- La copertura del codice deve rimanere ≥ 80%
|
||||||
|
- Esegui i test prima di submitare:
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentazione
|
||||||
|
|
||||||
|
- Aggiorna il README se cambi il comportamento
|
||||||
|
- Aggiungi docstring a nuove funzioni
|
||||||
|
- Aggiorna il CHANGELOG.md
|
||||||
|
|
||||||
|
## Processo di Review
|
||||||
|
|
||||||
|
- I PR saranno reviewati il prima possibile
|
||||||
|
- I feedback saranno forniti in buona fede
|
||||||
|
- Le discussioni devono essere costruttive
|
||||||
|
|
||||||
|
## Licenza
|
||||||
|
|
||||||
|
Contribuendo, accetti che i tuoi contributi siano licensiati sotto la MIT License.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Domande? Apri un issue o contatta il maintainer!
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
# Multi-stage build per LLM Monitor
|
||||||
|
|
||||||
|
# Stage 1: Builder
|
||||||
|
FROM python:3.11-slim as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installare dipendenze di build
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copiare requirements
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Installare Python packages in un virtualenv
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Stage 2: Runtime
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installare dipendenze di runtime
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copiare il virtualenv dal builder
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
|
||||||
|
# Copiare codice dell'app
|
||||||
|
COPY app/ /app/app/
|
||||||
|
COPY main.py /app/
|
||||||
|
COPY .env* /app/
|
||||||
|
|
||||||
|
# Impostare PATH
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Esporre la porta
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8000/api/v1/health || exit 1
|
||||||
|
|
||||||
|
# Comando di avvio
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-2026 Luca Sacchi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
.PHONY: help install dev prod test lint format clean docker-build docker-up docker-down
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "LLM Monitor - Makefile Commands"
|
||||||
|
@echo "================================"
|
||||||
|
@echo "make install - Installa le dipendenze"
|
||||||
|
@echo "make dev - Avvia in modalità sviluppo"
|
||||||
|
@echo "make prod - Avvia in modalità produzione"
|
||||||
|
@echo "make test - Esegui i test"
|
||||||
|
@echo "make lint - Linting e type checking"
|
||||||
|
@echo "make format - Formatta il codice"
|
||||||
|
@echo "make clean - Pulisce cache e file temporanei"
|
||||||
|
@echo "make docker-build - Build dell'immagine Docker"
|
||||||
|
@echo "make docker-up - Avvia i container con Docker Compose"
|
||||||
|
@echo "make docker-down - Ferma i container con Docker Compose"
|
||||||
|
|
||||||
|
install:
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate && pip install -r requirements.txt -r requirements-dev.txt
|
||||||
|
|
||||||
|
dev:
|
||||||
|
. venv/bin/activate && uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
prod:
|
||||||
|
. venv/bin/activate && uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||||
|
|
||||||
|
test:
|
||||||
|
. venv/bin/activate && pytest tests/ -v --cov=app
|
||||||
|
|
||||||
|
lint:
|
||||||
|
. venv/bin/activate && flake8 app/ tests/ main.py && mypy app/
|
||||||
|
|
||||||
|
format:
|
||||||
|
. venv/bin/activate && black app/ tests/ main.py && isort app/ tests/ main.py
|
||||||
|
|
||||||
|
clean:
|
||||||
|
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
rm -rf .pytest_cache .mypy_cache .coverage htmlcov dist build *.egg-info
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
docker build -t llm-monitor:latest .
|
||||||
|
|
||||||
|
docker-up:
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
docker-down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
docker-logs:
|
||||||
|
docker compose logs -f llm-monitor
|
||||||
|
|
||||||
|
docker-shell:
|
||||||
|
docker compose exec llm-monitor /bin/bash
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
# LLM Monitor - Dashboard Ollama
|
||||||
|
|
||||||
|
Una dashboard web moderna e intuitiva per monitorare e gestire i modelli LLM caricati in **Ollama**. Visualizza i modelli disponibili, i dettagli dei caricamenti e accedi ai dati via API Ollama direttamente da una web app elegante.
|
||||||
|
|
||||||
|
## 🎯 Caratteristiche
|
||||||
|
|
||||||
|
- ✨ **Dashboard intuitiva** - Visualizza in tempo reale i modelli caricati in Ollama
|
||||||
|
- 📊 **Monitoraggio modelli** - Dettagli completi di ogni modello (nome, dimensione, memoria, stato)
|
||||||
|
- 🔌 **API REST documentata** - Documentazione interattiva con Swagger/OpenAPI
|
||||||
|
- 🎨 **UI moderna** - Interfaccia elegante realizzata con TailwindCSS
|
||||||
|
- 🐳 **Docker ready** - Container sempre acceso (until stopped)
|
||||||
|
- ⚡ **Performance** - Costruito su FastAPI e uVicorn
|
||||||
|
- 🔐 **Configurazione flessibile** - File `.env` per personalizzazione
|
||||||
|
|
||||||
|
## 📋 Requisiti
|
||||||
|
|
||||||
|
- **Python** 3.10+
|
||||||
|
- **Ollama** installato e in esecuzione
|
||||||
|
- **Docker** (opzionale, per containerizzazione)
|
||||||
|
- **Docker Compose** (opzionale)
|
||||||
|
|
||||||
|
## 🚀 Installazione Rapida
|
||||||
|
|
||||||
|
### 1. Clonare il repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/LucaSacchiNet/llm-monitor.git
|
||||||
|
cd llm-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurare l'ambiente
|
||||||
|
|
||||||
|
Copia il file di esempio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifica `.env` con i tuoi parametri (vedi [Configurazione](#configurazione)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Installare le dipendenze
|
||||||
|
|
||||||
|
#### Opzione A: Ambiente virtuale (Sviluppo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # Su Windows: venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Opzione B: Docker (Produzione)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Avviare l'applicazione
|
||||||
|
|
||||||
|
#### Modalità sviluppo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Modalità produzione
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Configurazione
|
||||||
|
|
||||||
|
Crea un file `.env` nella root del progetto (copia da `env.example`):
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Ollama Configuration
|
||||||
|
OLLAMA_HOST=http://localhost:11434
|
||||||
|
OLLAMA_TIMEOUT=30
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
API_WORKERS=4
|
||||||
|
|
||||||
|
# CORS Configuration
|
||||||
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
ENVIRONMENT=development
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variabili disponibili
|
||||||
|
|
||||||
|
| Variabile | Default | Descrizione |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `OLLAMA_HOST` | `http://localhost:11434` | URL della API Ollama |
|
||||||
|
| `OLLAMA_TIMEOUT` | `30` | Timeout (secondi) per le richieste |
|
||||||
|
| `API_HOST` | `0.0.0.0` | Host su cui esporre l'API |
|
||||||
|
| `API_PORT` | `8000` | Porta dell'API |
|
||||||
|
| `API_WORKERS` | `4` | Worker processes |
|
||||||
|
| `CORS_ORIGINS` | `http://localhost:3000` | Origini CORS consentite |
|
||||||
|
| `LOG_LEVEL` | `INFO` | Livello di logging |
|
||||||
|
| `ENVIRONMENT` | `development` | Ambiente (development/production) |
|
||||||
|
|
||||||
|
## 📚 API Swagger
|
||||||
|
|
||||||
|
La documentazione interattiva dell'API è disponibile automaticamente:
|
||||||
|
|
||||||
|
- **Swagger UI**: http://localhost:8000/docs
|
||||||
|
- **ReDoc**: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
### Endpoint principali
|
||||||
|
|
||||||
|
#### Recuperare modelli caricati
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risposta:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "abc123...",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dettagli di un modello specifico
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/v1/models/{model_name}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Health check API Ollama
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risposta:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"ollama_version": "0.1.0",
|
||||||
|
"uptime_seconds": 3600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test API con cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ottenere i modelli
|
||||||
|
curl http://localhost:8000/api/v1/models
|
||||||
|
|
||||||
|
# Ottenere info su un modello
|
||||||
|
curl http://localhost:8000/api/v1/models/llama2
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:8000/api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker
|
||||||
|
|
||||||
|
### Build dell'immagine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t llm-monitor:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eseguire il container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name llm-monitor \
|
||||||
|
-p 8000:8000 \
|
||||||
|
--env-file .env \
|
||||||
|
--network host \
|
||||||
|
llm-monitor:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Nota**: `--network host` consente al container di accedere a Ollama su localhost
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
Usa il file `docker-compose.yml` fornito:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Avviare i servizi
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Visualizzare i log
|
||||||
|
docker compose logs -f llm-monitor
|
||||||
|
|
||||||
|
# Fermare i servizi
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Riavviare
|
||||||
|
docker compose restart llm-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container sempre acceso
|
||||||
|
|
||||||
|
Il container Ollama rimarrà in esecuzione fino al suo arresto manuale:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fermare
|
||||||
|
docker compose stop ollama
|
||||||
|
# oppure
|
||||||
|
docker stop llm-monitor
|
||||||
|
|
||||||
|
# Riavviare
|
||||||
|
docker compose start ollama
|
||||||
|
# oppure
|
||||||
|
docker start llm-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Struttura del Progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
llm-monitor/
|
||||||
|
├── main.py # Entry point dell'applicazione
|
||||||
|
├── requirements.txt # Dipendenze Python
|
||||||
|
├── env.example # Esempio di configurazione
|
||||||
|
├── Dockerfile # Configurazione Docker
|
||||||
|
├── docker-compose.yml # Composizione servizi
|
||||||
|
├── README.md # Questo file
|
||||||
|
├── .gitignore
|
||||||
|
│
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── config.py # Configurazione (variabili ambiente)
|
||||||
|
│ ├── main.py # Inizializzazione FastAPI
|
||||||
|
│ │
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── models.py # Endpoint modelli
|
||||||
|
│ │ ├── health.py # Endpoint health
|
||||||
|
│ │ └── v1/
|
||||||
|
│ │ └── __init__.py
|
||||||
|
│ │
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── ollama.py # Client Ollama
|
||||||
|
│ │ └── cache.py # Cache in-memory (opzionale)
|
||||||
|
│ │
|
||||||
|
│ └── web/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── static/ # Assets statici (CSS compilato TailwindCSS)
|
||||||
|
│ └── templates/ # Template HTML
|
||||||
|
│
|
||||||
|
└── tests/
|
||||||
|
├── __init__.py
|
||||||
|
├── test_api.py
|
||||||
|
└── test_ollama.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Sviluppo
|
||||||
|
|
||||||
|
### Setup locale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clonare il repo
|
||||||
|
git clone https://github.com/LucaSacchiNet/llm-monitor.git
|
||||||
|
cd llm-monitor
|
||||||
|
|
||||||
|
# Ambiente virtuale
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Installare dipendenze + dev
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r requirements-dev.txt # black, pytest, flake8, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comandi utili
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Formattare codice
|
||||||
|
black app/ tests/ main.py
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
flake8 app/ tests/ main.py
|
||||||
|
|
||||||
|
# Test
|
||||||
|
pytest tests/ -v
|
||||||
|
|
||||||
|
# Test con coverage
|
||||||
|
pytest tests/ --cov=app
|
||||||
|
|
||||||
|
# Hot reload durante sviluppo
|
||||||
|
uvicorn main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilare TailwindCSS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installare dipendenze Node (opzionale)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Generare CSS in modalità développement
|
||||||
|
npm run tailwind:dev
|
||||||
|
|
||||||
|
# Build per produzione
|
||||||
|
npm run tailwind:build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Errore: "Cannot connect to Ollama"
|
||||||
|
|
||||||
|
- Verificare che Ollama sia in esecuzione: `curl http://localhost:11434/api/tags`
|
||||||
|
- Controllare che l'indirizzo in `.env` sia corretto (`OLLAMA_HOST`)
|
||||||
|
- Se usi Docker, assicurati che il container abbia accesso a Ollama (vedi [Docker](#docker))
|
||||||
|
|
||||||
|
### Errore: "Port 8000 already in use"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cambiare la porta in .env
|
||||||
|
API_PORT=8001
|
||||||
|
|
||||||
|
# Oppure liberare la porta
|
||||||
|
lsof -ti :8000 | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard lenta
|
||||||
|
|
||||||
|
- Verificare lo stato di Ollama
|
||||||
|
- Aumentare `OLLAMA_TIMEOUT` in `.env`
|
||||||
|
- Controllare i log: `docker compose logs -f llm-monitor`
|
||||||
|
|
||||||
|
## 📄 Dipendenze Principali
|
||||||
|
|
||||||
|
- **FastAPI** - Framework web moderno
|
||||||
|
- **uVicorn** - Server ASGI ad alte prestazioni
|
||||||
|
- **Pydantic** - Validazione dati
|
||||||
|
- **Requests** - Client HTTP
|
||||||
|
- **Jinja2** - Template HTML
|
||||||
|
- **TailwindCSS** - Utility-first CSS
|
||||||
|
|
||||||
|
## 📜 Licenza
|
||||||
|
|
||||||
|
Questo progetto è distribuito sotto licenza **MIT**. Vedi il file `LICENSE` per dettagli.
|
||||||
|
|
||||||
|
## 🤝 Contribuire
|
||||||
|
|
||||||
|
Le pull request sono benvenute! Per cambiamenti importanti, apri prima un issue per discutere i cambiamenti proposti.
|
||||||
|
|
||||||
|
### Processo di contribuzione
|
||||||
|
|
||||||
|
1. Fork il repository
|
||||||
|
2. Crea un branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit i cambiamenti (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push al branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Apri una Pull Request
|
||||||
|
|
||||||
|
## 📞 Supporto
|
||||||
|
|
||||||
|
Per domande o segnalazioni di bug, apri un **Issue** nel repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fatto con ❤️ da [LucaSacchi.Net](https://lucasacchi.net)**
|
||||||
|
|
||||||
|
**Versione**: 1.0.0
|
||||||
|
**Ultima modifica**: Aprile 2026
|
||||||
|
**Status**: 🟢 In Development
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""
|
||||||
|
LLM Monitor - Package principale
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
API routes
|
||||||
|
"""
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Health check endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class HealthResponse(BaseModel):
|
||||||
|
status: str
|
||||||
|
ollama_status: str
|
||||||
|
timestamp: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"status": "healthy",
|
||||||
|
"ollama_status": "online",
|
||||||
|
"timestamp": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/health", response_model=HealthResponse)
|
||||||
|
async def health_check():
|
||||||
|
"""
|
||||||
|
Health check dell'API e dello stato di Ollama
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HealthResponse: Status dell'API e di Ollama
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check Ollama
|
||||||
|
response = requests.get(
|
||||||
|
f"{settings.OLLAMA_HOST}/api/tags",
|
||||||
|
timeout=settings.OLLAMA_TIMEOUT
|
||||||
|
)
|
||||||
|
ollama_status = "online" if response.status_code == 200 else "offline"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Ollama health check failed: {e}")
|
||||||
|
ollama_status = "offline"
|
||||||
|
|
||||||
|
return HealthResponse(
|
||||||
|
status="healthy",
|
||||||
|
ollama_status=ollama_status,
|
||||||
|
timestamp=datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/ready")
|
||||||
|
async def ready():
|
||||||
|
"""
|
||||||
|
Readiness probe per Kubernetes/Docker
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{settings.OLLAMA_HOST}/api/tags",
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"status": "ready"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=503, detail="Service unavailable")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Readiness check failed: {e}")
|
||||||
|
raise HTTPException(status_code=503, detail="Service unavailable")
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
"""
|
||||||
|
Models endpoints - Gestione dei modelli Ollama
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class ModelInfo(BaseModel):
|
||||||
|
"""Informazioni su un modello"""
|
||||||
|
name: str
|
||||||
|
digest: str
|
||||||
|
size: int
|
||||||
|
modified_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "abc123def456...",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelsResponse(BaseModel):
|
||||||
|
"""Risposta con lista di modelli"""
|
||||||
|
models: List[ModelInfo]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "abc123def456...",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/models", response_model=ModelsResponse)
|
||||||
|
async def get_models():
|
||||||
|
"""
|
||||||
|
Recupera l'elenco di tutti i modelli caricati in Ollama
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelsResponse: Lista dei modelli disponibili
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Se Ollama non è disponibile
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{settings.OLLAMA_HOST}/api/tags",
|
||||||
|
timeout=settings.OLLAMA_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Ollama non disponibile"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
models_data = data.get("models", [])
|
||||||
|
|
||||||
|
models = [
|
||||||
|
ModelInfo(
|
||||||
|
name=model.get("name", "unknown"),
|
||||||
|
digest=model.get("digest", ""),
|
||||||
|
size=model.get("size", 0),
|
||||||
|
modified_at=datetime.fromisoformat(
|
||||||
|
model.get("modified_at", "").replace("Z", "+00:00")
|
||||||
|
) if model.get("modified_at") else datetime.utcnow()
|
||||||
|
)
|
||||||
|
for model in models_data
|
||||||
|
]
|
||||||
|
|
||||||
|
return ModelsResponse(
|
||||||
|
models=models,
|
||||||
|
total=len(models)
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=504,
|
||||||
|
detail="Timeout: Ollama non ha risposto in tempo"
|
||||||
|
)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Impossible connettersi a Ollama"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching models: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Errore nel recupero dei modelli"
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/models/{model_name}", response_model=ModelInfo)
|
||||||
|
async def get_model(model_name: str):
|
||||||
|
"""
|
||||||
|
Recupera le informazioni di un modello specifico
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Nome del modello da cercare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelInfo: Informazioni del modello
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Se il modello non esiste o Ollama non è disponibile
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{settings.OLLAMA_HOST}/api/tags",
|
||||||
|
timeout=settings.OLLAMA_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Ollama non disponibile"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
models_data = data.get("models", [])
|
||||||
|
|
||||||
|
# Cercare il modello
|
||||||
|
for model in models_data:
|
||||||
|
if model.get("name") == model_name:
|
||||||
|
return ModelInfo(
|
||||||
|
name=model.get("name", "unknown"),
|
||||||
|
digest=model.get("digest", ""),
|
||||||
|
size=model.get("size", 0),
|
||||||
|
modified_at=datetime.fromisoformat(
|
||||||
|
model.get("modified_at", "").replace("Z", "+00:00")
|
||||||
|
) if model.get("modified_at") else datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Modello '{model_name}' non trovato"
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching model: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Errore nel recupero del modello"
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/models/{model_name}/pull")
|
||||||
|
async def pull_model(model_name: str):
|
||||||
|
"""
|
||||||
|
Scarica/carica un modello in Ollama
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Nome del modello da scaricare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status del download
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{settings.OLLAMA_HOST}/api/pull",
|
||||||
|
json={"name": model_name},
|
||||||
|
timeout=None # Pull può essere lungo
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code not in [200, 201]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Errore nel pull del modello"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": "pulling", "model": model_name}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error pulling model: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Errore nel pull del modello"
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.delete("/models/{model_name}")
|
||||||
|
async def delete_model(model_name: str):
|
||||||
|
"""
|
||||||
|
Elimina un modello da Ollama
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Nome del modello da eliminare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Confirmazione eliminazione
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.delete(
|
||||||
|
f"{settings.OLLAMA_HOST}/api/delete",
|
||||||
|
json={"name": model_name},
|
||||||
|
timeout=settings.OLLAMA_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code not in [200, 204]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Errore nell'eliminazione del modello"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": "deleted", "model": model_name}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting model: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Errore nell'eliminazione del modello"
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Configurazione dell'applicazione tramite variabili di ambiente
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Configurazione dell'applicazione"""
|
||||||
|
|
||||||
|
# Ollama
|
||||||
|
OLLAMA_HOST: str = "http://localhost:11434"
|
||||||
|
OLLAMA_TIMEOUT: int = 30
|
||||||
|
|
||||||
|
# API
|
||||||
|
API_HOST: str = "0.0.0.0"
|
||||||
|
API_PORT: int = 8000
|
||||||
|
API_WORKERS: int = 4
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL: str = "INFO"
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
ENVIRONMENT: str = "development"
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
|
# Istanza globale della configurazione
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Services - Business logic
|
||||||
|
"""
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Ollama client service
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OllamaClient:
|
||||||
|
"""Client per interagire con l'API Ollama"""
|
||||||
|
|
||||||
|
def __init__(self, host: str = None, timeout: int = None):
|
||||||
|
self.host = host or settings.OLLAMA_HOST
|
||||||
|
self.timeout = timeout or settings.OLLAMA_TIMEOUT
|
||||||
|
|
||||||
|
def get_models(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Recupera l'elenco dei modelli da Ollama
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: Lista dei modelli
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.host}/api/tags",
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json().get("models", [])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting models from Ollama: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_model(self, model_name: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Recupera informazioni su un modello specifico
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Nome del modello
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Informazioni del modello, o None se non trovato
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
models = self.get_models()
|
||||||
|
for model in models:
|
||||||
|
if model.get("name") == model_name:
|
||||||
|
return model
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting model {model_name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""
|
||||||
|
Verifica se Ollama è disponibile
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True se disponibile, False altrimenti
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.host}/api/tags",
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pull_model(self, model_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Scarica/carica un modello
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Nome del modello
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True se ha successo
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.host}/api/pull",
|
||||||
|
json={"name": model_name},
|
||||||
|
timeout=None
|
||||||
|
)
|
||||||
|
return response.status_code in [200, 201]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error pulling model {model_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_model(self, model_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Elimina un modello
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Nome del modello
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True se ha successo
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.delete(
|
||||||
|
f"{self.host}/api/delete",
|
||||||
|
json={"name": model_name},
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
return response.status_code in [200, 204]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting model {model_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Istanza globale del client Ollama
|
||||||
|
ollama_client = OllamaClient()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Web templates and static files
|
||||||
|
"""
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LLM Monitor - Dashboard Ollama</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-white">
|
||||||
|
<div class="min-h-screen flex flex-col">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="bg-gray-800 border-b border-gray-700 sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center font-bold text-lg">
|
||||||
|
🦙
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold">LLM Monitor</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div id="health-status" class="flex items-center gap-2">
|
||||||
|
<div id="status-indicator" class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
||||||
|
<span id="status-text" class="text-sm text-gray-400">Controllo...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<div class="text-gray-400 text-sm font-medium">Modelli Caricati</div>
|
||||||
|
<div id="models-count" class="text-4xl font-bold mt-2">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<div class="text-gray-400 text-sm font-medium">Spazio Totale</div>
|
||||||
|
<div id="total-size" class="text-4xl font-bold mt-2">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<div class="text-gray-400 text-sm font-medium">Status Ollama</div>
|
||||||
|
<div id="ollama-status" class="text-4xl font-bold mt-2">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Models Section -->
|
||||||
|
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-xl font-bold">Modelli Disponibili</h2>
|
||||||
|
<button onclick="loadModels()" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
||||||
|
🔄 Aggiorna
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Models List -->
|
||||||
|
<div id="models-container" class="space-y-4">
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<div class="animate-spin inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full"></div>
|
||||||
|
<p class="text-gray-400 mt-4">Caricamento modelli...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Documentation Section -->
|
||||||
|
<div class="mt-8 bg-blue-900 bg-opacity-20 border border-blue-700 rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-bold mb-4">📚 Documentazione API</h3>
|
||||||
|
<p class="text-gray-300 mb-4">La API è documentata e testabile direttamente da:</p>
|
||||||
|
<div class="flex gap-3 flex-wrap">
|
||||||
|
<a href="/docs" target="_blank" class="inline-block bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
||||||
|
Swagger UI
|
||||||
|
</a>
|
||||||
|
<a href="/redoc" target="_blank" class="inline-block bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
||||||
|
ReDoc
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-gray-800 border-t border-gray-700 mt-12">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-gray-400 text-sm">
|
||||||
|
<p>LLM Monitor v1.0.0 • Fatto con ❤️ da <a href="https://lucasacchi.net" target="_blank" class="text-purple-400 hover:text-purple-300">LucaSacchi.Net</a></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = "/api/v1";
|
||||||
|
|
||||||
|
// Formattare bytes in formato leggibile
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formattare data
|
||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("it-IT", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificare health
|
||||||
|
async function checkHealth() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/health`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const statusEl = document.getElementById("status-indicator");
|
||||||
|
const statusText = document.getElementById("status-text");
|
||||||
|
const ollamaStatus = data.ollama_status;
|
||||||
|
|
||||||
|
if (ollamaStatus === "online") {
|
||||||
|
statusEl.className = "w-3 h-3 bg-green-500 rounded-full";
|
||||||
|
statusText.className = "text-sm text-green-400";
|
||||||
|
statusText.textContent = "Ollama Online";
|
||||||
|
document.getElementById("ollama-status").innerHTML = "🟢 Online";
|
||||||
|
} else {
|
||||||
|
statusEl.className = "w-3 h-3 bg-red-500 rounded-full";
|
||||||
|
statusText.className = "text-sm text-red-400";
|
||||||
|
statusText.textContent = "Ollama Offline";
|
||||||
|
document.getElementById("ollama-status").innerHTML = "🔴 Offline";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Health check error:", error);
|
||||||
|
document.getElementById("status-indicator").className = "w-3 h-3 bg-red-500 rounded-full";
|
||||||
|
document.getElementById("status-text").textContent = "Errore connessione";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caricare modelli
|
||||||
|
async function loadModels() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/models`);
|
||||||
|
if (!response.ok) throw new Error("Errore nel caricamento");
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const models = data.models || [];
|
||||||
|
|
||||||
|
// Aggiornare conteggio
|
||||||
|
document.getElementById("models-count").textContent = models.length;
|
||||||
|
|
||||||
|
// Calcolare spazio totale
|
||||||
|
const totalSize = models.reduce((sum, m) => sum + m.size, 0);
|
||||||
|
document.getElementById("total-size").textContent = formatBytes(totalSize);
|
||||||
|
|
||||||
|
// Renderizzare modelli
|
||||||
|
if (models.length === 0) {
|
||||||
|
document.getElementById("models-container").innerHTML = `
|
||||||
|
<div class="text-center py-8 text-gray-400">
|
||||||
|
<p>Nessun modello caricato</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
document.getElementById("models-container").innerHTML = models.map(model => `
|
||||||
|
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 transition">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<h3 class="text-lg font-semibold">${model.name}</h3>
|
||||||
|
<span class="bg-purple-600 px-3 py-1 rounded text-xs font-medium">Caricato</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400">Dimensione</p>
|
||||||
|
<p class="font-semibold">${formatBytes(model.size)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400">Ultimo aggiornamento</p>
|
||||||
|
<p class="font-semibold">${formatDate(model.modified_at)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-gray-400 text-xs">Digest</p>
|
||||||
|
<p class="font-mono text-xs bg-gray-800 p-2 rounded mt-1 break-all">${model.digest.substring(0, 64)}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading models:", error);
|
||||||
|
document.getElementById("models-container").innerHTML = `
|
||||||
|
<div class="text-center py-8 text-red-400">
|
||||||
|
<p>❌ Errore nel caricamento dei modelli</p>
|
||||||
|
<p class="text-sm mt-2">${error.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inizializzazione
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
checkHealth();
|
||||||
|
loadModels();
|
||||||
|
|
||||||
|
// Refresh ogni 30 secondi
|
||||||
|
setInterval(() => {
|
||||||
|
checkHealth();
|
||||||
|
loadModels();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Ollama Service
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: ollama-server
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
environment:
|
||||||
|
OLLAMA_HOST: 0.0.0.0:11434
|
||||||
|
volumes:
|
||||||
|
- ollama_data:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
# Keep container running until stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
networks:
|
||||||
|
- llm-monitor-network
|
||||||
|
|
||||||
|
# LLM Monitor Dashboard
|
||||||
|
llm-monitor:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: llm-monitor-app
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
# Carica variabili da .env
|
||||||
|
OLLAMA_HOST: http://ollama:11434
|
||||||
|
OLLAMA_TIMEOUT: 30
|
||||||
|
API_HOST: 0.0.0.0
|
||||||
|
API_PORT: 8000
|
||||||
|
API_WORKERS: 4
|
||||||
|
CORS_ORIGINS: http://localhost:3000,http://localhost:5173,http://localhost:8000
|
||||||
|
LOG_LEVEL: INFO
|
||||||
|
ENVIRONMENT: production
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
networks:
|
||||||
|
- llm-monitor-network
|
||||||
|
# Health check
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
llm-monitor-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
# Istruzioni di avvio:
|
||||||
|
# docker compose up -d # Avvia i servizi
|
||||||
|
# docker compose logs -f # Visualizza i log
|
||||||
|
# docker compose down # Ferma i servizi
|
||||||
|
# docker compose stop ollama # Ferma solo Ollama
|
||||||
|
# docker compose start ollama # Riavvia Ollama
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
# LLM Monitor - Environment Configuration Example
|
||||||
|
# Copy this file to .env and adjust values for your environment
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Ollama Configuration
|
||||||
|
# ===========================================
|
||||||
|
# URL base dell'API Ollama
|
||||||
|
OLLAMA_HOST=http://localhost:11434
|
||||||
|
|
||||||
|
# Timeout per le richieste a Ollama (secondi)
|
||||||
|
OLLAMA_TIMEOUT=30
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# API Configuration
|
||||||
|
# ===========================================
|
||||||
|
# Host su cui esporre l'API
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Porta su cui esporre l'API
|
||||||
|
API_PORT=8000
|
||||||
|
|
||||||
|
# Numero di worker processes per uVicorn
|
||||||
|
API_WORKERS=4
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# CORS Configuration
|
||||||
|
# ===========================================
|
||||||
|
# Origini CORS consentite (separare con virgola)
|
||||||
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8000
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Logging
|
||||||
|
# ===========================================
|
||||||
|
# Livello di logging (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Environment
|
||||||
|
# ===========================================
|
||||||
|
# Ambiente di esecuzione (development, production)
|
||||||
|
ENVIRONMENT=development
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Security (opzionale)
|
||||||
|
# ===========================================
|
||||||
|
# Secret key per sessioni (genera con: python -c "import secrets; print(secrets.token_hex(32))")
|
||||||
|
# SECRET_KEY=your-secret-key-here
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Database (se necessario in futuro)
|
||||||
|
# ===========================================
|
||||||
|
# DATABASE_URL=sqlite:///./llm-monitor.db
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
LLM Monitor - Dashboard per controllare i modelli caricati in Ollama
|
||||||
|
Entry point dell'applicazione FastAPI
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configurazione logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Importare le rotte
|
||||||
|
from app.api.health import router as health_router
|
||||||
|
from app.api.models import router as models_router
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
# Creare l'app FastAPI
|
||||||
|
app = FastAPI(
|
||||||
|
title="LLM Monitor API",
|
||||||
|
description="Dashboard per il monitoraggio dei modelli LLM in Ollama",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc",
|
||||||
|
openapi_url="/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configurare CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.CORS_ORIGINS.split(","),
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Registrare le rotte API
|
||||||
|
app.include_router(health_router, prefix="/api/v1", tags=["health"])
|
||||||
|
app.include_router(models_router, prefix="/api/v1", tags=["models"])
|
||||||
|
|
||||||
|
# Servire i file statici
|
||||||
|
static_path = Path(__file__).parent / "app" / "web" / "static"
|
||||||
|
if static_path.exists():
|
||||||
|
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||||
|
|
||||||
|
# Servire la dashboard web
|
||||||
|
templates_path = Path(__file__).parent / "app" / "web" / "templates"
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Redirect alla dashboard"""
|
||||||
|
return FileResponse(templates_path / "index.html")
|
||||||
|
|
||||||
|
@app.get("/dashboard")
|
||||||
|
async def dashboard():
|
||||||
|
"""Dashboard principale"""
|
||||||
|
return FileResponse(templates_path / "index.html")
|
||||||
|
|
||||||
|
# Event hooks
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
logger.info("🚀 LLM Monitor avviato")
|
||||||
|
logger.info(f"📊 Ollama host: {settings.OLLAMA_HOST}")
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
logger.info("🛑 LLM Monitor arrestato")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host=settings.API_HOST,
|
||||||
|
port=settings.API_PORT,
|
||||||
|
reload=settings.ENVIRONMENT == "development",
|
||||||
|
log_level=settings.LOG_LEVEL.lower()
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "llm-monitor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Dashboard per controllare i modelli caricati in Ollama",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"tailwind:dev": "tailwindcss -i app/web/static/css/input.css -o app/web/static/css/output.css --watch",
|
||||||
|
"tailwind:build": "tailwindcss -i app/web/static/css/input.css -o app/web/static/css/output.css --minify"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Development Dependencies
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
pytest==7.4.3
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
pytest-asyncio==0.21.1
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
black==23.12.0
|
||||||
|
flake8==6.1.0
|
||||||
|
isort==5.13.2
|
||||||
|
mypy==1.7.1
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
pylint==3.0.3
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
mkdocs==1.5.3
|
||||||
|
mkdocs-material==9.5.0
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
ipython==8.18.1
|
||||||
|
ipdb==0.13.13
|
||||||
|
|
||||||
|
# Pre-commit hooks
|
||||||
|
pre-commit==3.5.0
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# LLM Monitor Requirements
|
||||||
|
|
||||||
|
# Core Web Framework
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
|
||||||
|
# HTTP Client
|
||||||
|
requests==2.31.0
|
||||||
|
httpx==0.25.1
|
||||||
|
|
||||||
|
# Template Engine
|
||||||
|
jinja2==3.1.2
|
||||||
|
|
||||||
|
# Database & ORM (opzionale)
|
||||||
|
# sqlalchemy==2.0.23
|
||||||
|
# alembic==1.12.1
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
|
||||||
|
# Async
|
||||||
|
aiohttp==3.9.1
|
||||||
|
|
||||||
|
# Logging & Monitoring
|
||||||
|
python-json-logger==2.0.7
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
fastapi-cors==0.0.6
|
||||||
|
|
||||||
|
# API Documentation
|
||||||
|
# (Swagger/ReDoc sono inclusi di default in FastAPI)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Test suite for LLM Monitor
|
||||||
|
"""
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
Pytest configuration and fixtures
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
"""FastAPI test client"""
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_models_response():
|
||||||
|
"""Mock response from Ollama API"""
|
||||||
|
return {
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "91ab89b1b9117e34fb2ff4a5bff07b2e1fa1f1d2d3e4f5a6b7c8d9e0f1a2b3c",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-01-15T10:30:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mistral",
|
||||||
|
"digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
|
||||||
|
"size": 4096000000,
|
||||||
|
"modified_at": "2024-01-14T15:45:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
Test API endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
def test_health_check(client):
|
||||||
|
"""Test health endpoint"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
response = client.get("/api/v1/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "status" in data
|
||||||
|
assert data["status"] == "healthy"
|
||||||
|
|
||||||
|
def test_ready_endpoint(client):
|
||||||
|
"""Test readiness probe"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
response = client.get("/api/v1/ready")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"status": "ready"}
|
||||||
|
|
||||||
|
def test_get_models(client, mock_models_response):
|
||||||
|
"""Test getting models list"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_models_response
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
response = client.get("/api/v1/models")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "models" in data
|
||||||
|
assert "total" in data
|
||||||
|
assert data["total"] == 2
|
||||||
|
assert len(data["models"]) == 2
|
||||||
|
assert data["models"][0]["name"] == "llama2"
|
||||||
|
|
||||||
|
def test_get_models_ollama_offline(client):
|
||||||
|
"""Test getting models when Ollama is offline"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_get.side_effect = Exception("Connection refused")
|
||||||
|
|
||||||
|
response = client.get("/api/v1/models")
|
||||||
|
assert response.status_code == 500
|
||||||
|
|
||||||
|
def test_get_specific_model(client, mock_models_response):
|
||||||
|
"""Test getting specific model"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_models_response
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
response = client.get("/api/v1/models/llama2")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "llama2"
|
||||||
|
|
||||||
|
def test_get_nonexistent_model(client, mock_models_response):
|
||||||
|
"""Test getting nonexistent model"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_models_response
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
response = client.get("/api/v1/models/nonexistent")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_root_endpoint(client):
|
||||||
|
"""Test root endpoint redirects to dashboard"""
|
||||||
|
response = client.get("/", follow_redirects=False)
|
||||||
|
assert response.status_code in [200, 307]
|
||||||
|
|
||||||
|
def test_openapi_schema(client):
|
||||||
|
"""Test OpenAPI schema is available"""
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200
|
||||||
|
schema = response.json()
|
||||||
|
assert "info" in schema
|
||||||
|
assert "paths" in schema
|
||||||
|
assert "/api/v1/health" in schema["paths"]
|
||||||
|
assert "/api/v1/models" in schema["paths"]
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
Test Ollama client service
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from app.services.ollama import OllamaClient
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ollama_client():
|
||||||
|
"""Create OllamaClient instance"""
|
||||||
|
return OllamaClient(host="http://localhost:11434", timeout=30)
|
||||||
|
|
||||||
|
def test_get_models(ollama_client):
|
||||||
|
"""Test getting models from Ollama"""
|
||||||
|
mock_data = {
|
||||||
|
"models": [
|
||||||
|
{"name": "llama2", "digest": "abc123", "size": 3825922048},
|
||||||
|
{"name": "mistral", "digest": "def456", "size": 4096000000}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_data
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
models = ollama_client.get_models()
|
||||||
|
assert len(models) == 2
|
||||||
|
assert models[0]["name"] == "llama2"
|
||||||
|
|
||||||
|
def test_get_models_error(ollama_client):
|
||||||
|
"""Test get models when error occurs"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_get.side_effect = Exception("Connection error")
|
||||||
|
|
||||||
|
models = ollama_client.get_models()
|
||||||
|
assert models == []
|
||||||
|
|
||||||
|
def test_get_model(ollama_client):
|
||||||
|
"""Test getting specific model"""
|
||||||
|
mock_data = {
|
||||||
|
"models": [
|
||||||
|
{"name": "llama2", "digest": "abc123", "size": 3825922048}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_data
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
model = ollama_client.get_model("llama2")
|
||||||
|
assert model is not None
|
||||||
|
assert model["name"] == "llama2"
|
||||||
|
|
||||||
|
def test_get_nonexistent_model(ollama_client):
|
||||||
|
"""Test getting nonexistent model"""
|
||||||
|
mock_data = {"models": []}
|
||||||
|
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_data
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
model = ollama_client.get_model("nonexistent")
|
||||||
|
assert model is None
|
||||||
|
|
||||||
|
def test_is_available(ollama_client):
|
||||||
|
"""Test checking if Ollama is available"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
assert ollama_client.is_available() is True
|
||||||
|
|
||||||
|
def test_is_available_offline(ollama_client):
|
||||||
|
"""Test checking if Ollama is available when offline"""
|
||||||
|
with patch("requests.get") as mock_get:
|
||||||
|
mock_get.side_effect = Exception("Connection refused")
|
||||||
|
|
||||||
|
assert ollama_client.is_available() is False
|
||||||
|
|
||||||
|
def test_pull_model(ollama_client):
|
||||||
|
"""Test pulling a model"""
|
||||||
|
with patch("requests.post") as mock_post:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = ollama_client.pull_model("llama2")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_pull_model_error(ollama_client):
|
||||||
|
"""Test pull model when error occurs"""
|
||||||
|
with patch("requests.post") as mock_post:
|
||||||
|
mock_post.side_effect = Exception("Error")
|
||||||
|
|
||||||
|
result = ollama_client.pull_model("llama2")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_delete_model(ollama_client):
|
||||||
|
"""Test deleting a model"""
|
||||||
|
with patch("requests.delete") as mock_delete:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 204
|
||||||
|
mock_delete.return_value = mock_response
|
||||||
|
|
||||||
|
result = ollama_client.delete_model("llama2")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_delete_model_error(ollama_client):
|
||||||
|
"""Test delete model when error occurs"""
|
||||||
|
with patch("requests.delete") as mock_delete:
|
||||||
|
mock_delete.side_effect = Exception("Error")
|
||||||
|
|
||||||
|
result = ollama_client.delete_model("llama2")
|
||||||
|
assert result is False
|
||||||
Reference in New Issue
Block a user