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 @@
|
||||
"""
|
||||
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