Component Details

Deep dive into the configuration and assets associated with this component.

Basic Information
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
Controller Code
<?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);
        }
    }
}

Routes Code
<?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');
});

Views (1)
  • index.blade.php Length: 18634 chars