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,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"
|
||||
)
|
||||
Reference in New Issue
Block a user