Deep dive into the configuration and assets associated with this component.
| Name | ml-training |
|---|---|
| Display Name | ML Training |
| Version | 1.0.0 |
| Status | Active |
| Downloads | 78 |
| Description | Component: ML Training |
| Created | 2025-11-11 20:42:10 |
<?php
namespace App\Http\Controllers;
use App\Services\MLCommandResolver;
use App\Services\TrainingDataLoader;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class MLTrainingController extends Controller
{
/**
* Show the training interface
*/
public function index()
{
$resolver = new MLCommandResolver();
$stats = $resolver->getStats();
return view('components.ml-training.index', [
'stats' => $stats,
]);
}
/**
* Get current model statistics
*/
public function stats()
{
$resolver = new MLCommandResolver();
$stats = $resolver->getStats();
return response()->json($stats);
}
/**
* Train the model from the dataset file
*/
public function train(Request $request)
{
$force = $request->input('force', false);
try {
$trainingFile = app_path('Services/trainingdataset.txt');
if (!file_exists($trainingFile)) {
return response()->json([
'success' => false,
'message' => "Training dataset file not found: {$trainingFile}"
], 404);
}
$examples = TrainingDataLoader::loadTrainingExamples($trainingFile);
if (empty($examples)) {
return response()->json([
'success' => false,
'message' => 'No training examples found in the dataset file.'
], 400);
}
$resolver = new MLCommandResolver();
// Check if model already exists
$stats = $resolver->getStats();
if ($stats['total_examples'] > 0 && !$force) {
return response()->json([
'success' => false,
'message' => "Model already exists with {$stats['total_examples']} examples. Use force=true to retrain.",
'stats' => $stats
], 409);
}
if ($stats['total_examples'] > 0 && $force) {
$resolver->clear();
}
// Train in chunks
$chunkSize = 500;
$chunks = array_chunk($examples, $chunkSize);
$totalChunks = count($chunks);
$processed = 0;
foreach ($chunks as $chunk) {
$resolver->batchTrain($chunk, $chunkSize);
$processed += count($chunk);
}
$newStats = $resolver->getStats();
return response()->json([
'success' => true,
'message' => "Model trained successfully with {$processed} examples!",
'stats' => $newStats
]);
} catch (\Exception $e) {
Log::error('ML Training Error: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
return response()->json([
'success' => false,
'message' => 'Training failed: ' . $e->getMessage()
], 500);
}
}
/**
* Add a single training example
*/
public function addExample(Request $request)
{
$request->validate([
'input' => 'required|string|max:500',
'command' => 'required|string|max:200',
'weight' => 'nullable|numeric|min:0|max:10',
]);
try {
// Normalize command - remove "php console" or "./console" prefix and clean up
$command = $request->input('command');
$command = preg_replace('/^(php\s+console|\.\/console|console)\s+/i', '', $command);
$command = trim($command);
// Remove extra spaces in parameters (e.g., "--force-idsites=5, 7" -> "--force-idsites=5,7")
$command = preg_replace('/,\s+/', ',', $command);
$input = $request->input('input');
$weight = $request->input('weight', 1.0);
// Train the model
$resolver = new MLCommandResolver();
$resolver->train($input, $command, $weight);
$resolver->saveModel();
// Also append to training dataset file so it persists after retraining
$trainingFile = app_path('Services/trainingdataset.txt');
$appendSuccess = false;
if (file_exists($trainingFile)) {
if (is_writable($trainingFile)) {
try {
// Append as JSON line format (same format as the end of the file)
$jsonLine = json_encode([
'utterance' => $input,
'command' => './console ' . $command
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Append to end of file with newline
$result = @file_put_contents($trainingFile, "\n" . $jsonLine, FILE_APPEND | LOCK_EX);
$appendSuccess = $result !== false;
} catch (\Exception $e) {
Log::warning('Failed to append to training dataset file', [
'file' => $trainingFile,
'error' => $e->getMessage()
]);
}
} else {
// Try to make it writable (if we have permission)
@chmod($trainingFile, 0664);
if (is_writable($trainingFile)) {
try {
$jsonLine = json_encode([
'utterance' => $input,
'command' => './console ' . $command
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$result = @file_put_contents($trainingFile, "\n" . $jsonLine, FILE_APPEND | LOCK_EX);
$appendSuccess = $result !== false;
} catch (\Exception $e) {
Log::warning('Failed to append to training dataset file after chmod', [
'file' => $trainingFile,
'error' => $e->getMessage()
]);
}
} else {
Log::warning('Training dataset file is not writable', [
'file' => $trainingFile,
'permissions' => substr(sprintf('%o', fileperms($trainingFile)), -4),
'owner' => posix_getpwuid(fileowner($trainingFile))['name'] ?? 'unknown'
]);
}
}
}
if (!$appendSuccess) {
// Still return success for the model training, but log the warning
Log::warning('Could not append new example to training dataset file. Example was added to model but will be lost on retrain.', [
'file' => $trainingFile,
'suggestion' => 'Run: chmod 664 ' . $trainingFile . ' && chown beonline:beonline ' . $trainingFile
]);
}
return response()->json([
'success' => true,
'message' => 'Training example added successfully!',
'stats' => $resolver->getStats()
]);
} catch (\Exception $e) {
Log::error('Add Training Example Error: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
// Check if it's a permission error
if (strpos($e->getMessage(), 'Permission denied') !== false || strpos($e->getMessage(), 'Failed to open stream') !== false) {
return response()->json([
'success' => false,
'message' => 'Permission denied. Please ensure the storage/app directory is writable by the web server user.'
], 500);
}
return response()->json([
'success' => false,
'message' => 'Failed to add example: ' . $e->getMessage()
], 500);
}
}
/**
* Test a prediction
*/
public function test(Request $request)
{
// Start output buffering to catch any unexpected output
ob_start();
// Register shutdown function to catch fatal errors
register_shutdown_function(function() {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
ob_clean();
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Fatal error: ' . $error['message'],
'error_type' => 'FatalError',
'error_details' => config('app.debug') ? [
'file' => $error['file'],
'line' => $error['line']
] : null
]);
exit;
}
});
try {
$request->validate([
'input' => 'required|string|max:500',
]);
} catch (\Illuminate\Validation\ValidationException $e) {
ob_end_clean();
return response()->json([
'success' => false,
'message' => 'Validation failed: ' . $e->getMessage(),
'errors' => $e->errors()
], 422);
} catch (\Exception $e) {
ob_end_clean();
return response()->json([
'success' => false,
'message' => 'Request validation error: ' . $e->getMessage()
], 400);
}
try {
// Set execution time limit
set_time_limit(60);
$resolver = new MLCommandResolver();
$stats = $resolver->getStats();
// Check if model has training data
if ($stats['total_examples'] == 0) {
ob_end_clean();
return response()->json([
'success' => false,
'message' => 'Model has no training data. Please train the model first.'
], 400);
}
$userInput = $request->input('input');
$siteIds = $this->extractSiteIds($userInput);
// If site IDs are detected and input contains "archive", force core:archive
// This handles cases where ML doesn't match well but the intent is clear
$forceArchive = !empty($siteIds) && stripos($userInput, 'archive') !== false;
// Lower threshold for testing (0.2 instead of 0.3 to catch more matches)
$prediction = $resolver->predict($userInput, 0.2);
// If we should force archive and prediction is not core:archive, create one
if ($forceArchive && (!$prediction || strpos($prediction->command, 'core:archive') !== 0)) {
// Check if core:archive exists in alternatives
$foundInAlternatives = false;
if ($prediction && isset($prediction->alternatives)) {
foreach ($prediction->alternatives as $alt) {
$altCommand = is_array($alt) ? ($alt['command'] ?? null) : $alt;
if ($altCommand && strpos($altCommand, 'core:archive') === 0) {
// Use the alternative
$altScore = is_array($alt) ? ($alt['score'] ?? 0.5) : 0.5;
$prediction = new \App\Services\CommandPrediction(
$altCommand,
max(0.5, $altScore), // Minimum 50% confidence for forced matches
[]
);
$foundInAlternatives = true;
break;
}
}
}
// If not found in alternatives, create a new prediction for core:archive
if (!$foundInAlternatives) {
$prediction = new \App\Services\CommandPrediction(
'core:archive',
0.5, // 50% confidence for pattern-based match
[]
);
}
}
ob_end_clean();
if ($prediction) {
// Post-process: Extract site IDs from input and add to command if it's an archive command
$command = $prediction->command;
// If we found site IDs and the command is core:archive, add them
if (!empty($siteIds) && strpos($command, 'core:archive') === 0) {
// Check if command already has force-idsites
if (strpos($command, '--force-idsites') === false) {
$siteIdsStr = implode(',', $siteIds);
$command .= ' --force-idsites=' . $siteIdsStr;
}
}
return response()->json([
'success' => true,
'prediction' => [
'command' => $command,
'confidence' => round($prediction->confidence * 100, 2),
'score' => $prediction->confidence, // Use confidence as score
'alternatives' => array_map(function($alt) use ($userInput) {
$altCommand = $alt['command'] ?? $alt;
// Also add site IDs to alternatives if applicable
if (is_array($alt) && isset($alt['command']) && strpos($alt['command'], 'core:archive') === 0) {
$siteIds = $this->extractSiteIds($userInput);
if (!empty($siteIds) && strpos($altCommand, '--force-idsites') === false) {
$siteIdsStr = implode(',', $siteIds);
$altCommand .= ' --force-idsites=' . $siteIdsStr;
}
}
return [
'command' => $altCommand,
'score' => $alt['score'] ?? 0
];
}, $prediction->alternatives ?? [])
]
]);
} else {
return response()->json([
'success' => false,
'message' => 'No prediction found (confidence too low or no matching training examples)'
]);
}
} catch (\Error $e) {
ob_end_clean();
Log::error('Prediction Test Fatal Error: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
'input' => $request->input('input')
]);
return response()->json([
'success' => false,
'message' => 'Prediction failed: ' . $e->getMessage(),
'error_type' => get_class($e),
'error_details' => config('app.debug') ? [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
] : null
], 500);
} catch (\Exception $e) {
ob_end_clean();
Log::error('Prediction Test Error: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString(),
'input' => $request->input('input')
]);
return response()->json([
'success' => false,
'message' => 'Prediction failed: ' . $e->getMessage(),
'error_type' => get_class($e),
'error_details' => config('app.debug') ? $e->getTraceAsString() : null
], 500);
}
}
/**
* Extract site IDs from user input
*/
private function extractSiteIds(string $input): array
{
$siteIds = [];
// Pattern 1: "for sites 5 and 7" or "for site 5 and 7 and 8" - handles "for" prefix and multiple "and" conjunctions
if (preg_match('/for\s+(?:\w+\s+)?sites?\s+((?:\d+\s+and\s+)+\d+)/i', $input, $matches)) {
// Extract all numbers from the matched string
preg_match_all('/\d+/', $matches[1], $numbers);
$siteIds = array_map('intval', $numbers[0]);
}
// Pattern 2: "sites 5 and 7" or "site 5 and 7 and 8" - handles multiple "and" conjunctions (without "for")
// Also handles mixed formats like "site 3 and 12 and 1 and 5, 9"
if (empty($siteIds) && preg_match('/sites?\s+([\d\s,and]+)/i', $input, $matches)) {
// Extract all numbers from the matched string (handles both "and" and comma-separated)
preg_match_all('/\d+/', $matches[1], $numbers);
$siteIds = array_map('intval', $numbers[0]);
}
// Pattern 3: "sites 2, 3, 4" or "sites 2,3,4" - comma-separated (fallback if pattern 2 didn't match)
if (empty($siteIds) && preg_match('/sites?\s+(\d+(?:\s*,\s*\d+)+)/i', $input, $matches)) {
preg_match_all('/\d+/', $matches[1], $numbers);
$siteIds = array_map('intval', $numbers[0]);
}
// Pattern 4: "idsite=5,6" or "--idsite=5,6"
if (empty($siteIds) && preg_match('/idsite[=:](\d+(?:[,\s]+\d+)*)/i', $input, $matches)) {
preg_match_all('/\d+/', $matches[1], $numbers);
$siteIds = array_map('intval', $numbers[0]);
}
// Pattern 5: Single "site 5" or "site5" (fallback)
if (empty($siteIds)) {
if (preg_match('/sites?[^\d]*(\d+)/i', $input, $matches)) {
$siteIds = [(int) $matches[1]];
} elseif (preg_match('/site\s*id\s*(\d+)/i', $input, $matches)) {
$siteIds = [(int) $matches[1]];
}
}
return array_values(array_unique($siteIds));
}
/**
* Clear the model
*/
public function clear()
{
try {
$resolver = new MLCommandResolver();
$resolver->clear();
return response()->json([
'success' => true,
'message' => 'Model cleared successfully!',
'stats' => $resolver->getStats()
]);
} catch (\Exception $e) {
Log::error('Clear Model Error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to clear model: ' . $e->getMessage()
], 500);
}
}
}
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\MLTrainingController;
Route::middleware(['auth', 'component.visible'])->group(function () {
Route::get('/ml-training', [MLTrainingController::class, 'index'])->name('dashboard.ml-training');
Route::get('/ml-training/stats', [MLTrainingController::class, 'stats'])->name('ml-training.stats');
Route::post('/ml-training/train', [MLTrainingController::class, 'train'])->name('ml-training.train');
Route::post('/ml-training/add-example', [MLTrainingController::class, 'addExample'])->name('ml-training.add-example');
Route::post('/ml-training/test', [MLTrainingController::class, 'test'])->name('ml-training.test');
Route::post('/ml-training/clear', [MLTrainingController::class, 'clear'])->name('ml-training.clear');
});
index.blade.php
Length: 18634 chars