Files
michaelschiemer/src/Application/Api/MachineLearning/MLModelsController.php
Michael Schiemer c8b47e647d feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support
BREAKING CHANGE: Requires PHP 8.5.0RC3

Changes:
- Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm
- Enable ext-uri for native WHATWG URL parsing support
- Update composer.json PHP requirement from ^8.4 to ^8.5
- Add ext-uri as required extension in composer.json
- Move URL classes from Url.php85/ to Url/ directory (now compatible)
- Remove temporary PHP 8.4 compatibility workarounds

Benefits:
- Native URL parsing with Uri\WhatWg\Url class
- Better performance for URL operations
- Future-proof with latest PHP features
- Eliminates PHP version compatibility issues
2025-10-27 09:31:28 +01:00

479 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Application\Api\MachineLearning;
use App\Framework\Attributes\Route;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Core\ValueObjects\Version;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\MachineLearning\ModelManagement\ModelPerformanceMonitor;
use App\Framework\MachineLearning\ModelManagement\ModelRegistry;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelType;
use App\Framework\OpenApi\Attributes\ApiEndpoint;
use App\Framework\OpenApi\Attributes\ApiParameter;
use App\Framework\OpenApi\Attributes\ApiRequestBody;
use App\Framework\OpenApi\Attributes\ApiResponse;
use App\Framework\OpenApi\Attributes\ApiSecurity;
use App\Framework\Router\Result\JsonResult;
/**
* ML Models Management API Controller
*
* RESTful API endpoints for managing machine learning models:
* - Model registration
* - Performance metrics retrieval
* - Model listing and versioning
*/
#[ApiSecurity('bearerAuth')]
final readonly class MLModelsController
{
public function __construct(
private ModelRegistry $registry,
private ModelPerformanceMonitor $performanceMonitor
) {}
#[Route(path: '/api/ml/models', method: Method::GET)]
#[ApiEndpoint(
summary: 'List all ML models',
description: 'Retrieve a list of all registered machine learning models with their versions',
tags: ['Machine Learning'],
)]
#[ApiParameter(
name: 'type',
in: 'query',
description: 'Filter by model type (supervised, unsupervised, reinforcement)',
required: false,
type: 'string',
example: 'supervised',
)]
#[ApiResponse(
statusCode: 200,
description: 'List of ML models retrieved successfully',
example: [
'models' => [
[
'model_name' => 'fraud-detector',
'type' => 'supervised',
'versions' => [
[
'version' => '1.0.0',
'created_at' => '2024-01-01T00:00:00Z',
'is_latest' => true,
],
],
],
],
'total_models' => 5,
],
)]
public function listModels(HttpRequest $request): JsonResult
{
$typeFilter = $request->query->get('type');
// Get all model names
$modelNames = $this->registry->getAllModelNames();
// Get all versions for each model
$allModels = [];
foreach ($modelNames as $modelName) {
$versions = $this->registry->getAll($modelName);
$allModels = array_merge($allModels, $versions);
}
// Filter by type if specified
if ($typeFilter !== null) {
$allModels = array_filter($allModels, function (ModelMetadata $metadata) use ($typeFilter) {
return strtolower($metadata->modelType->value) === strtolower($typeFilter);
});
}
// Group by model name
$groupedModels = [];
foreach ($allModels as $metadata) {
$modelName = $metadata->modelName;
if (!isset($groupedModels[$modelName])) {
$groupedModels[$modelName] = [
'model_name' => $modelName,
'type' => $metadata->modelType->value,
'versions' => [],
];
}
$groupedModels[$modelName]['versions'][] = [
'version' => $metadata->version->toString(),
'created_at' => $metadata->createdAt->format('Y-m-d\TH:i:s\Z'),
'configuration' => $metadata->configuration,
];
}
return new JsonResult([
'models' => array_values($groupedModels),
'total_models' => count($groupedModels),
]);
}
#[Route(path: '/api/ml/models/{modelName}', method: Method::GET)]
#[ApiEndpoint(
summary: 'Get model details',
description: 'Retrieve detailed information about a specific ML model',
tags: ['Machine Learning'],
)]
#[ApiParameter(
name: 'modelName',
in: 'path',
description: 'Model identifier',
required: true,
type: 'string',
example: 'fraud-detector',
)]
#[ApiParameter(
name: 'version',
in: 'query',
description: 'Specific version (optional, defaults to latest)',
required: false,
type: 'string',
example: '1.0.0',
)]
#[ApiResponse(
statusCode: 200,
description: 'Model details retrieved successfully',
example: [
'model_name' => 'fraud-detector',
'type' => 'supervised',
'version' => '1.0.0',
'configuration' => [
'threshold' => 0.7,
'algorithm' => 'random_forest',
],
'created_at' => '2024-01-01T00:00:00Z',
],
)]
#[ApiResponse(
statusCode: 404,
description: 'Model not found',
)]
public function getModel(string $modelName, HttpRequest $request): JsonResult
{
$versionString = $request->query->get('version');
try {
if ($versionString !== null) {
$version = Version::fromString($versionString);
$metadata = $this->registry->get($modelName, $version);
} else {
$metadata = $this->registry->getLatest($modelName);
}
if ($metadata === null) {
return new JsonResult([
'error' => 'Model not found',
'model_name' => $modelName,
], Status::NOT_FOUND);
}
return new JsonResult([
'model_name' => $metadata->modelName,
'type' => $metadata->modelType->value,
'version' => $metadata->version->toString(),
'configuration' => $metadata->configuration,
'performance_metrics' => $metadata->performanceMetrics,
'created_at' => $metadata->createdAt->format('Y-m-d\TH:i:s\Z'),
]);
} catch (\InvalidArgumentException $e) {
return new JsonResult([
'error' => 'Invalid version format',
'message' => $e->getMessage(),
], Status::BAD_REQUEST);
}
}
#[Route(path: '/api/ml/models/{modelName}/metrics', method: Method::GET)]
#[ApiEndpoint(
summary: 'Get model performance metrics',
description: 'Retrieve real-time performance metrics for a specific model',
tags: ['Machine Learning'],
)]
#[ApiParameter(
name: 'modelName',
in: 'path',
description: 'Model identifier',
required: true,
type: 'string',
example: 'fraud-detector',
)]
#[ApiParameter(
name: 'version',
in: 'query',
description: 'Model version',
required: false,
type: 'string',
example: '1.0.0',
)]
#[ApiParameter(
name: 'timeWindow',
in: 'query',
description: 'Time window in hours (default: 1)',
required: false,
type: 'integer',
example: 24,
)]
#[ApiResponse(
statusCode: 200,
description: 'Performance metrics retrieved successfully',
example: [
'model_name' => 'fraud-detector',
'version' => '1.0.0',
'time_window_hours' => 24,
'metrics' => [
'accuracy' => 0.92,
'precision' => 0.89,
'recall' => 0.94,
'f1_score' => 0.91,
'total_predictions' => 1523,
'average_confidence' => 0.85,
],
'confusion_matrix' => [
'true_positive' => 1234,
'true_negative' => 156,
'false_positive' => 89,
'false_negative' => 44,
],
],
)]
#[ApiResponse(
statusCode: 404,
description: 'Model not found',
)]
public function getMetrics(string $modelName, HttpRequest $request): JsonResult
{
$versionString = $request->query->get('version');
$timeWindowHours = $request->query->getInt('timeWindow', 1);
try {
if ($versionString !== null) {
$version = Version::fromString($versionString);
} else {
$metadata = $this->registry->getLatest($modelName);
if ($metadata === null) {
return new JsonResult([
'error' => 'Model not found',
'model_name' => $modelName,
], Status::NOT_FOUND);
}
$version = $metadata->version;
}
$timeWindow = Duration::fromHours($timeWindowHours);
$metrics = $this->performanceMonitor->getCurrentMetrics(
$modelName,
$version,
$timeWindow
);
return new JsonResult([
'model_name' => $modelName,
'version' => $version->toString(),
'time_window_hours' => $timeWindowHours,
'metrics' => [
'accuracy' => $metrics['accuracy'],
'precision' => $metrics['precision'] ?? null,
'recall' => $metrics['recall'] ?? null,
'f1_score' => $metrics['f1_score'] ?? null,
'total_predictions' => $metrics['total_predictions'],
'average_confidence' => $metrics['average_confidence'] ?? null,
],
'confusion_matrix' => $metrics['confusion_matrix'] ?? null,
'timestamp' => date('c'),
]);
} catch (\InvalidArgumentException $e) {
return new JsonResult([
'error' => 'Invalid parameters',
'message' => $e->getMessage(),
], Status::BAD_REQUEST);
}
}
#[Route(path: '/api/ml/models', method: Method::POST)]
#[ApiEndpoint(
summary: 'Register a new ML model',
description: 'Register a new machine learning model or version in the system',
tags: ['Machine Learning'],
)]
#[ApiRequestBody(
description: 'Model metadata for registration',
required: true,
example: [
'model_name' => 'fraud-detector',
'type' => 'supervised',
'version' => '2.0.0',
'configuration' => [
'threshold' => 0.75,
'algorithm' => 'xgboost',
'features' => 30,
],
'performance_metrics' => [
'accuracy' => 0.94,
'precision' => 0.91,
'recall' => 0.96,
],
],
)]
#[ApiResponse(
statusCode: 201,
description: 'Model registered successfully',
example: [
'model_name' => 'fraud-detector',
'version' => '2.0.0',
'created_at' => '2024-01-01T00:00:00Z',
'message' => 'Model registered successfully',
],
)]
#[ApiResponse(
statusCode: 400,
description: 'Invalid model data',
)]
#[ApiResponse(
statusCode: 409,
description: 'Model version already exists',
)]
public function registerModel(HttpRequest $request): JsonResult
{
try {
$data = $request->parsedBody->toArray();
// Validate required fields
if (!isset($data['model_name'], $data['type'], $data['version'])) {
return new JsonResult([
'error' => 'Missing required fields',
'required' => ['model_name', 'type', 'version'],
], Status::BAD_REQUEST);
}
// Parse model type
$modelType = match (strtolower($data['type'])) {
'supervised' => ModelType::SUPERVISED,
'unsupervised' => ModelType::UNSUPERVISED,
'reinforcement' => ModelType::REINFORCEMENT,
default => throw new \InvalidArgumentException("Invalid model type: {$data['type']}")
};
// Create metadata
$metadata = new ModelMetadata(
modelName: $data['model_name'],
modelType: $modelType,
version: Version::fromString($data['version']),
configuration: $data['configuration'] ?? [],
createdAt: Timestamp::now(),
performanceMetrics: $data['performance_metrics'] ?? []
);
// Check if already exists
$existing = $this->registry->get($metadata->modelName, $metadata->version);
if ($existing !== null) {
return new JsonResult([
'error' => 'Model version already exists',
'model_name' => $metadata->modelName,
'version' => $metadata->version->toString(),
], Status::CONFLICT);
}
// Register model
$this->registry->register($metadata);
return new JsonResult([
'model_name' => $metadata->modelName,
'version' => $metadata->version->toString(),
'type' => $metadata->modelType->value,
'created_at' => $metadata->createdAt->format('Y-m-d\TH:i:s\Z'),
'message' => 'Model registered successfully',
], Status::CREATED);
} catch (\InvalidArgumentException $e) {
return new JsonResult([
'error' => 'Invalid model data',
'message' => $e->getMessage(),
], Status::BAD_REQUEST);
} catch (\Throwable $e) {
return new JsonResult([
'error' => 'Failed to register model',
'message' => $e->getMessage(),
], Status::INTERNAL_SERVER_ERROR);
}
}
#[Route(path: '/api/ml/models/{modelName}', method: Method::DELETE)]
#[ApiEndpoint(
summary: 'Unregister ML model',
description: 'Remove a specific version of an ML model from the registry',
tags: ['Machine Learning'],
)]
#[ApiParameter(
name: 'modelName',
in: 'path',
description: 'Model identifier',
required: true,
type: 'string',
example: 'fraud-detector',
)]
#[ApiParameter(
name: 'version',
in: 'query',
description: 'Model version to unregister',
required: true,
type: 'string',
example: '1.0.0',
)]
#[ApiResponse(
statusCode: 200,
description: 'Model unregistered successfully',
)]
#[ApiResponse(
statusCode: 404,
description: 'Model not found',
)]
public function unregisterModel(string $modelName, HttpRequest $request): JsonResult
{
$versionString = $request->query->get('version');
if ($versionString === null) {
return new JsonResult([
'error' => 'Version parameter is required',
], Status::BAD_REQUEST);
}
try {
$version = Version::fromString($versionString);
// Check if model exists
$metadata = $this->registry->get($modelName, $version);
if ($metadata === null) {
return new JsonResult([
'error' => 'Model not found',
'model_name' => $modelName,
'version' => $versionString,
], Status::NOT_FOUND);
}
// Unregister
$this->registry->unregister($modelName, $version);
return new JsonResult([
'message' => 'Model unregistered successfully',
'model_name' => $modelName,
'version' => $versionString,
]);
} catch (\InvalidArgumentException $e) {
return new JsonResult([
'error' => 'Invalid version format',
'message' => $e->getMessage(),
], Status::BAD_REQUEST);
}
}
}