First commit

This commit is contained in:
Saufi
2026-05-18 08:56:23 +08:00
commit fd3d3a4d2b
147 changed files with 22099 additions and 0 deletions

View 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;
}
}

View 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}");
});
}
}

View 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;
}
}