feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution - Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments - Add force_deploy flag support in Ansible playbook to override stale locks - Use PHP deployment module as orchestrator (php console.php deploy:production) - Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal Architecture: - BuildStage → AnsibleDeployStage → HealthCheckStage for production - Process module provides timeout, error handling, and output capture - Ansible playbook supports rollback via rollback-git-based.yml - Zero-downtime deployments with health checks
This commit is contained in:
478
src/Application/Api/MachineLearning/MLModelsController.php
Normal file
478
src/Application/Api/MachineLearning/MLModelsController.php
Normal file
@@ -0,0 +1,478 @@
|
||||
<?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->queryParameters['type'] ?? null;
|
||||
|
||||
// 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->queryParameters['version'] ?? null;
|
||||
|
||||
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->queryParameters['version'] ?? null;
|
||||
$timeWindowHours = (int) ($request->queryParameters['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->queryParameters['version'] ?? null;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user