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