First commit
This commit is contained in:
109
app/Console/Commands/HealthCheckCommand.php
Normal file
109
app/Console/Commands/HealthCheckCommand.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Ollama\OllamaService;
|
||||
use App\Services\Qdrant\QdrantService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* php artisan kb:health-check
|
||||
*
|
||||
* Semak status semua perkhidmatan yang diperlukan.
|
||||
*/
|
||||
class HealthCheckCommand extends Command
|
||||
{
|
||||
protected $signature = 'kb:health-check';
|
||||
protected $description = 'Semak status Ollama, Qdrant, dan MySQL';
|
||||
|
||||
public function handle(OllamaService $ollama, QdrantService $qdrant): int
|
||||
{
|
||||
$this->info('════════════════════════════════════════');
|
||||
$this->info(' PEMERIKSAAN KESIHATAN SISTEM');
|
||||
$this->info('════════════════════════════════════════');
|
||||
|
||||
$allOk = true;
|
||||
|
||||
// ── MySQL ─────────────────────────────────────────────────────────
|
||||
$this->line('');
|
||||
$this->info('📦 MySQL');
|
||||
try {
|
||||
DB::connection()->getPdo();
|
||||
$dbName = DB::getDatabaseName();
|
||||
$this->line(" ✅ Berjaya sambung ke: {$dbName}");
|
||||
} catch (\Exception $e) {
|
||||
$this->error(" ❌ Tidak dapat sambung: {$e->getMessage()}");
|
||||
$allOk = false;
|
||||
}
|
||||
|
||||
// ── Ollama ────────────────────────────────────────────────────────
|
||||
$this->line('');
|
||||
$this->info('🤖 Ollama');
|
||||
$ollamaStatus = $ollama->healthCheck();
|
||||
|
||||
if ($ollamaStatus['online']) {
|
||||
$this->line(" ✅ Online");
|
||||
$chatOk = $ollamaStatus['chat_model'] ? '✅' : '❌';
|
||||
$embedOk = $ollamaStatus['embed_model'] ? '✅' : '❌';
|
||||
$this->line(" {$chatOk} Model Chat: " . config('ollama.chat_model'));
|
||||
$this->line(" {$embedOk} Model Embed: " . config('ollama.embedding_model'));
|
||||
|
||||
if (!$ollamaStatus['chat_model'] || !$ollamaStatus['embed_model']) {
|
||||
$allOk = false;
|
||||
}
|
||||
} else {
|
||||
$this->error(" ❌ Offline");
|
||||
if ($ollamaStatus['error']) {
|
||||
$this->line(" " . $ollamaStatus['error']);
|
||||
}
|
||||
$allOk = false;
|
||||
}
|
||||
|
||||
// ── Qdrant ────────────────────────────────────────────────────────
|
||||
$this->line('');
|
||||
$this->info('🗄️ Qdrant');
|
||||
$qdrantStatus = $qdrant->healthCheck();
|
||||
|
||||
if ($qdrantStatus['online']) {
|
||||
$this->line(" ✅ Online");
|
||||
$collOk = $qdrantStatus['collection_exists'] ? '✅' : '⚠️ ';
|
||||
$this->line(" {$collOk} Collection: " . config('qdrant.collection'));
|
||||
if ($qdrantStatus['collection_exists']) {
|
||||
$this->line(" 📊 Vectors: " . number_format($qdrantStatus['points_count'] ?? 0));
|
||||
} else {
|
||||
$this->warn(" Collection belum wujud. Akan dibuat secara automatik semasa pertama embed.");
|
||||
}
|
||||
} else {
|
||||
$this->error(" ❌ Offline");
|
||||
if ($qdrantStatus['error']) {
|
||||
$this->line(" " . $qdrantStatus['error']);
|
||||
}
|
||||
$allOk = false;
|
||||
}
|
||||
|
||||
// ── Queue ─────────────────────────────────────────────────────────
|
||||
$this->line('');
|
||||
$this->info('⏳ Queue');
|
||||
$driver = config('queue.default');
|
||||
$this->line(" ℹ️ Driver: {$driver}");
|
||||
if ($driver === 'sync') {
|
||||
$this->warn(" ⚠️ Queue driver adalah 'sync' — job akan dijalankan secara synchronous.");
|
||||
$this->warn(" Tukar kepada 'database' atau 'redis' untuk production.");
|
||||
} else {
|
||||
$this->line(" ✅ Driver konfigurasi untuk async.");
|
||||
}
|
||||
|
||||
// ── Ringkasan ─────────────────────────────────────────────────────
|
||||
$this->line('');
|
||||
$this->info('════════════════════════════════════════');
|
||||
if ($allOk) {
|
||||
$this->info(' ✅ SEMUA PERKHIDMATAN OK');
|
||||
} else {
|
||||
$this->error(' ❌ ADA PERKHIDMATAN YANG BERMASALAH');
|
||||
}
|
||||
$this->info('════════════════════════════════════════');
|
||||
|
||||
return $allOk ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
96
app/Console/Commands/ReindexCategoryCommand.php
Normal file
96
app/Console/Commands/ReindexCategoryCommand.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ReindexDocumentJob;
|
||||
use App\Jobs\ReindexKnowledgeItemJob;
|
||||
use App\Models\Category;
|
||||
use App\Models\DocumentVersion;
|
||||
use App\Models\KnowledgeItem;
|
||||
use App\Services\KnowledgeBase\AuditService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* php artisan kb:reindex-category {--category_id=} {--slug=} {--dry-run}
|
||||
*
|
||||
* Contoh:
|
||||
* php artisan kb:reindex-category --category_id=1
|
||||
* php artisan kb:reindex-category --slug=pelesenan
|
||||
* php artisan kb:reindex-category --slug=pelesenan --dry-run
|
||||
*/
|
||||
class ReindexCategoryCommand extends Command
|
||||
{
|
||||
protected $signature = 'kb:reindex-category
|
||||
{--category_id= : ID kategori}
|
||||
{--slug= : Slug kategori}
|
||||
{--dry-run : Papar sahaja tanpa dispatch}';
|
||||
|
||||
protected $description = 'Reindex semua dokumen dan knowledge items dalam satu kategori';
|
||||
|
||||
public function handle(AuditService $auditService): int
|
||||
{
|
||||
$category = null;
|
||||
|
||||
if ($id = $this->option('category_id')) {
|
||||
$category = Category::find((int) $id);
|
||||
} elseif ($slug = $this->option('slug')) {
|
||||
$category = Category::where('slug', $slug)->first();
|
||||
}
|
||||
|
||||
if (!$category) {
|
||||
$this->error('Kategori tidak dijumpai.');
|
||||
$this->listCategories();
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info("Kategori: {$category->name}");
|
||||
$this->line(str_repeat('─', 50));
|
||||
|
||||
// Dokumen
|
||||
$versions = DocumentVersion::whereHas('document', fn($q) => $q->where('category_id', $category->id))
|
||||
->where('is_current', true)
|
||||
->where('processing_status', DocumentVersion::STATUS_INDEXED)
|
||||
->with('document')
|
||||
->get();
|
||||
|
||||
$this->info("Dokumen (versi semasa): {$versions->count()}");
|
||||
foreach ($versions as $v) {
|
||||
$this->line(" → {$v->document->title} v{$v->version_number}");
|
||||
if (!$dryRun) {
|
||||
ReindexDocumentJob::dispatch($v->id);
|
||||
}
|
||||
}
|
||||
|
||||
// Knowledge Items
|
||||
$items = KnowledgeItem::where('category_id', $category->id)
|
||||
->where('is_active', true)
|
||||
->get();
|
||||
|
||||
$this->info("\nKnowledge Items: {$items->count()}");
|
||||
foreach ($items as $item) {
|
||||
$this->line(" → [{$item->item_type}] {$item->title}");
|
||||
if (!$dryRun) {
|
||||
ReindexKnowledgeItemJob::dispatch($item->id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn("\n[DRY RUN] Tiada job dihantar. Buang --dry-run untuk reindex sebenar.");
|
||||
} else {
|
||||
$auditService->systemReindexStarted("category:{$category->slug}");
|
||||
$this->info("\n✓ Semua job telah dijadualkan.");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function listCategories(): void
|
||||
{
|
||||
$this->info("\nKategori tersedia:");
|
||||
Category::orderBy('name')->get()->each(function ($c) {
|
||||
$this->line(" ID: {$c->id} Slug: {$c->slug} Nama: {$c->name}");
|
||||
});
|
||||
}
|
||||
}
|
||||
107
app/Console/Commands/ReindexDocumentCommand.php
Normal file
107
app/Console/Commands/ReindexDocumentCommand.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ReindexDocumentJob;
|
||||
use App\Models\Document;
|
||||
use App\Models\DocumentVersion;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* php artisan kb:reindex-document {--document_id=} {--version_id=} {--all-failed}
|
||||
*
|
||||
* Contoh:
|
||||
* php artisan kb:reindex-document --document_id=5
|
||||
* php artisan kb:reindex-document --version_id=12
|
||||
* php artisan kb:reindex-document --all-failed
|
||||
*/
|
||||
class ReindexDocumentCommand extends Command
|
||||
{
|
||||
protected $signature = 'kb:reindex-document
|
||||
{--document_id= : ID dokumen untuk reindex versi semasa}
|
||||
{--version_id= : ID versi spesifik untuk reindex}
|
||||
{--all-failed : Reindex semua versi yang gagal}';
|
||||
|
||||
protected $description = 'Reindex (re-embed) dokumen dalam Qdrant';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if ($this->option('all-failed')) {
|
||||
return $this->reindexAllFailed();
|
||||
}
|
||||
|
||||
if ($versionId = $this->option('version_id')) {
|
||||
return $this->reindexVersion((int) $versionId);
|
||||
}
|
||||
|
||||
if ($documentId = $this->option('document_id')) {
|
||||
return $this->reindexDocument((int) $documentId);
|
||||
}
|
||||
|
||||
$this->error('Sila nyatakan --document_id, --version_id, atau --all-failed');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
private function reindexDocument(int $documentId): int
|
||||
{
|
||||
$document = Document::find($documentId);
|
||||
|
||||
if (!$document) {
|
||||
$this->error("Dokumen ID {$documentId} tidak dijumpai.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$currentVersion = $document->currentVersion;
|
||||
|
||||
if (!$currentVersion) {
|
||||
$this->error("Dokumen '{$document->title}' tiada versi semasa.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
ReindexDocumentJob::dispatch($currentVersion->id);
|
||||
|
||||
$this->info("✓ Reindex dijadualkan untuk dokumen: {$document->title} (v{$currentVersion->version_number})");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function reindexVersion(int $versionId): int
|
||||
{
|
||||
$version = DocumentVersion::with('document')->find($versionId);
|
||||
|
||||
if (!$version) {
|
||||
$this->error("Version ID {$versionId} tidak dijumpai.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
ReindexDocumentJob::dispatch($version->id);
|
||||
|
||||
$this->info("✓ Reindex dijadualkan untuk: {$version->document->title} v{$version->version_number}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function reindexAllFailed(): int
|
||||
{
|
||||
$failedVersions = DocumentVersion::whereIn('processing_status', [
|
||||
DocumentVersion::STATUS_FAILED,
|
||||
DocumentVersion::STATUS_EXTRACTION_FAILED,
|
||||
])->with('document')->get();
|
||||
|
||||
if ($failedVersions->isEmpty()) {
|
||||
$this->info('Tiada versi yang gagal ditemui.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($failedVersions as $version) {
|
||||
ReindexDocumentJob::dispatch($version->id);
|
||||
$this->line(" → {$version->document->title} v{$version->version_number}");
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->info("✓ {$count} versi telah dijadualkan untuk reindex.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user