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
479 lines
16 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|