# Panduan Classification Runner (Laya / laya.cpp) Dokumen ini menjelaskan cara pakai `classify_runner.py` — runner klasifikasi teks pakai **Laya**, model keputusan (*decision model*) multilingual 322M parameter, yang dijalankan lewat runtime native **laya.cpp** di CPU. Untuk ringkasan proyek dan setup runner lain, lihat [README.md](README.md). --- ## Daftar isi 1. [Cara kerja singkat](#1-cara-kerja-singkat) 2. [Menjalankan](#2-menjalankan) 3. [Cara baca output](#3-cara-baca-output) 4. [Tiga tipe pertanyaan](#4-tiga-tipe-pertanyaan) 5. [Pertanyaan sendiri](#5-pertanyaan-sendiri) 6. [Mode server (lebih cepat)](#6-mode-server-lebih-cepat) 7. [Output JSON](#7-output-json) 8. [Semua opsi CLI](#8-semua-opsi-cli) 9. [Konfigurasi](#9-konfigurasi) 10. [Setup dari nol](#10-setup-dari-nol) 11. [Catatan penting](#11-catatan-penting) --- ## 1. Cara kerja singkat Laya **bukan** classifier satu-label biasa. Kamu memberi dia sebuah *state* (teks, email, ticket, atau JSON) plus beberapa **pertanyaan bertipe**. Tiap pertanyaan mendeklarasikan ruang jawabannya sendiri, jadi **tidak perlu training/finetune** untuk memakainya di kasus baru. Semua pertanyaan dijawab dalam **satu forward pass**, dengan probabilitas — jadi tidak ada teks yang perlu diparse dan tidak ada risiko hallucination seperti pada LLM generatif. ``` state + pertanyaan → satu forward pass → jawaban bertipe + probabilitas ``` Runner mendukung tiga mode pemanggilan: | Mode | Kapan dipakai | Kondisi | Kecepatan | |---|---|---|---| | CLI | satu kali jalan | tanpa setup | ~5–7 detik | | Server | dipakai berulang | harus nyalakan server | ~0,6 detik | --- ## 2. Menjalankan ### Cara paling dasar ```bash ./usage-classify.sh "Saya dikenakan biaya dua kali untuk invoice 4411, tolong kembalikan uang saya." ``` ### Kalau teksnya panjang, pakai file ```bash ./usage-classify.sh --state-file ticket.txt ``` ### Kalau state-nya punya beberapa kolom Kalau teks diawali `{` atau `[`, runner otomatis mengirimnya sebagai **struktur JSON**, bukan teks biasa. Jadi email dengan kolom subject/body bisa langsung: ```bash ./usage-classify.sh '{"subject":"Refund not received","body":"I cancelled two weeks ago and still have no refund."}' ``` Atau dari file JSON: ```bash ./usage-classify.sh --state-file ticket.json ``` --- ## 3. Cara baca output Contoh output nyata: ``` State : Saya dikenakan biaya dua kali untuk invoice 4411, tolong kembalikan uang saya. Questions : 3 (cli) Backend : CPU (12th Gen Intel(R) Core(TM) i7-12700H) Elapsed : 6.953 s Inference : 0.732 s Tokens : 176 in / 0 out department : billing billing=1.0000 technical=0.0000 sales=0.0000 other=0.0000 urgency : 1.7297 (immediate) 0=0.0306 1=0.2091 2=0.7603 refund : 0.7751 act_probability=1.0000 ``` ### Bagian atas | Baris | Arti | |---|---| | `State` | State yang dikirim (dipotong 120 karakter kalau panjang) | | `Questions` | Berapa pertanyaan dijawab, dan mode yang dipakai (`cli` atau `server`) | | `Backend` | Backend dan perangkat yang dipakai. Hanya muncul di mode CLI | | `Elapsed` | Total waktu wall-clock, **termasuk load bobot model** | | `Inference` | Waktu forward pass saja. Hanya muncul di mode CLI | | `Tokens` | Jumlah token input yang diproses (output selalu 0, karena model ini tidak generatif) | > **Kenapa `Elapsed` jauh lebih besar dari `Inference`?** > Model ini tidak generatif, jadi forward pass-nya cuma ~0,6 detik. Sisanya (~5 detik) adalah > waktu baca bobot 644MB dari disk. Ini normal. Kalau kamu pemakai berulang, pakai > [mode server](#6-mode-server-lebih-cepat). ### Bagian jawaban Nama di kiri adalah **ID pertanyaan** yang kamu tentukan sendiri. Formatnya berbeda per tipe: **`choice`** — jawaban berupa salah satu label. Baris kedua menampilkan probabilitas tiap opsi, diurutkan dari yang tertinggi: ``` department : billing billing=1.0000 technical=0.0000 sales=0.0000 other=0.0000 ``` **`score`** — jawaban berupa angka desimal (rata-rata tertimbang dari distribusi), diikuti label level yang paling dekat dalam kurung. Baris kedua menampilkan probabilitas tiap level **dalam urutan legend** (tidak diurutkan): ``` urgency : 1.7297 (immediate) 0=0.0306 1=0.2091 2=0.7603 ``` **`noul`** — jawaban berupa peluang pernyataan itu benar (0.0 = pasti salah, 1.0 = pasti benar). Tipe ini tidak punya objek `probabilities`, jadi baris kedua menampilkan `act_probability`, yaitu peluang bahwa kasus ini perlu di-*escalate* ke manusia: ``` refund : 0.7751 act_probability=1.0000 ``` --- ## 4. Tiga tipe pertanyaan | Tipe | Isi `criteria` | Output | Kapan pakai | |---|---|---|---| | `choice` | objek label → deskripsi | label terpilih + peluang tiap label | klasifikasi ke kategori | | `score` | **array** berurutan | angka desimal + label level | intensitas, prioritas, sentimen | | `noul` | tidak ada | peluang pernyataan `true` | deteksi ya/tidak | Contoh lengkap ketiga tipe (ini isi preset `triage` di `config/classify.py`): ```json { "department": { "type": "choice", "instructions": "Which team should handle this?", "criteria": { "billing": "invoices, payments, refunds", "technical": "bugs, outages, system errors", "sales": "pricing, plans, new contracts", "other": "everything else" } }, "urgency": { "type": "score", "instructions": "How urgent is the request?", "criteria": ["not urgent", "soon", "immediate"] }, "refund": { "type": "noul", "instructions": "Does the customer ask for money back?" } } ``` ### Aturan penting - **`choice` jangan lebih dari ~20 opsi.** Semua opsi berbagi satu budget token yang tetap, jadi kalau label terlalu banyak, tiap label cuma kebagian sedikit token dan akurasinya jatuh tajam. - **`score` pakai array, bukan objek.** Urutan array itu penting — itulah yang menentukan level 0, 1, 2, dan seterusnya. - **`instructions` ditulis dalam bahasa Inggris** walau isinya bisa bahasa apa pun. Instruksi bagian kepala (256 token) dan state diletakkan terpisah oleh `[SEP]` dalam model, jadi instruksi tidak ikut mengotori bahasa input. - **Nama kolom output bebas kamu pilih.** Kolom di kiri output itu key yang kamu pakai di objek `questions`, bukan dari `instructions`. ### Multibahasa Checkpoint-nya memang varian multilingual (100+ bahasa), jadi input non-Inggris diharapkan berfungsi: ```bash $ ./usage-classify.sh "二重に請求されました。返金をお願いします。" refund : 0.9976 $ ./usage-classify.sh "双重收费了,请退款。" refund : 0.9892 ``` --- ## 5. Pertanyaan sendiri Preset `triage` cuma comprise 3 pertanyaan default. Untuk kasus kamu, bikin file JSON: ```bash cat > my_questions.json <<'EOF' { "bahasa": { "type": "choice", "instructions": "What language is this text written in?", "criteria": { "id": "Indonesian", "en": "English", "fr": "French", "zh": "Chinese" } }, "komplain": { "type": "noul", "instructions": "Does the customer complain or express dissatisfaction?" }, "sentimen": { "type": "score", "instructions": "How positive is the tone?", "criteria": ["very negative", "neutral", "positive", "very positive"] } } EOF ./usage-classify.sh --questions my_questions.json "produk ini error terus sejak kemarin" ``` File-nya boleh berisi **objek questions langsung** (seperti di atas), **atau** objek request lengkap yang punya key `"questions"`. Runner mendeteksi keduanya. Kamu juga bisa simpan pertanyaan sebagai preset permanen di `config/classify.py` supaya bisa dipakai cuma dengan `--preset nama`: ```python PRESETS = { "triage": { ... }, # bawaan "bahasa": { ... }, # preset kamu } DEFAULT_PRESET = "triage" ``` ```bash ./usage-classify.sh --preset bahasa "produk ini error terus sejak kemarin" ``` --- ## 6. Mode server (lebih cepat) Tiap kali runner dipanggil dalam mode CLI, ia me-*spawn* `laya-cli` dan **load ulang bobot 644MB dari disk**. Itu yang bikin total ~5–7 detik, padahal inferensinya cuma 0,6 detik. Kalau kamu classifies berulang, biarkan model tetap *resident* di memory lewat HTTP server bawaan laya.cpp: ```bash third_party/laya.cpp/build-cpu/bin/laya-cli --server --port 8080 \ --model models/convaiinnovations/laya --variant multilingual --cpu & ``` Lalu arahkan runner ke sana: ```bash LAYA_URL=http://127.0.0.1:8080 ./usage-classify.sh "teks kamu" ``` Hasilnya **~0,6 detik bukan ~6 detik** — sekitar 8x lebih cepat, dengan jawaban yang identik (bisa diverifikasi langsung: bandingkan output CLI dan server untuk input yang sama). ### Urutan prioritas URL 1. `--url` di CLI 2. environment variable `LAYA_URL` 3. `LAYA_URL` di `config/classify.py` ### Kalau server mati Runner **otomatis fallback** ke mode CLI, dengan informasi di stderr: ``` laya.cpp server at http://127.0.0.1:8080 unavailable (Connection refused). Falling back to the CLI. ``` Jadi tidak ada risiko kalau lupa nyalakan server atau server-nya crash. ### Endpoint lain yang tersedia Server laya.cpp juga menyediakan `POST /predict` (bisa batch beberapa request sekaligus, dan mengembalikan envelope CLI lengkap termasuk `elapsed_ms`), `GET /health`, dan `GET /v1/models`. Runner memakai `/v1/systemone` yang bentuk balasannya lebih ringkas. --- ## 7. Output JSON ```bash ./usage-classify.sh "teks" --json ``` Menghasilkan envelope mentah laya.cpp dengan probabilitas presisi penuh (bukan 4 desimal) — berguna kalau mau pipe ke program lain: ```json { "results": [ { "model": "laya-rl-agent", "answers": { "refund": { "type": "noul", "confidence": 0.7751, "action": { "act_probability": 1.0 }, "noul": 0.7751 } }, "usage": { "input_tokens": 176, "output_tokens": 0 } } ], "elapsed_ms": 732.14, "backend": "CPU", "device": "12th Gen Intel(R) Core(TM) i7-12700H" } ``` Bentuk tiap tipe jawaban: | Tipe | Field yang ada | |---|---| | `choice` | `type`, `choice`, `probabilities`, `confidence`, `action.act_probability` | | `score` | `type`, `score`, `legend`, `probabilities`, `confidence`, `action.act_probability` | | `noul` | `type`, `noul`, `confidence`, `action.act_probability` | --- ## 8. Semua opsi CLI ``` usage: classify_runner.py [-h] [--state-file STATE_FILE] [--preset PRESET] [--questions QUESTIONS] [--url URL] [--json] [state] positional arguments: state State yang akan diklasifikasi (teks biasa atau JSON). Hilangkan kalau mau baca dari --state-file. options: --state-file STATE_FILE File yang berisi state (teks biasa atau JSON) --preset PRESET Nama preset pertanyaan dari config/classify.py --questions QUESTIONS File JSON berisi objek questions, mengoverride --preset --url URL Base URL server laya.cpp, mengoverride LAYA_URL dan config --json Cetak envelope respons mentah sebagai JSON -h, --help Tampilkan bantuan ``` > **Catatan:** `--state` **bukan** sebuah flag — state diberikan sebagai argumen posisi. Writer > `--state` akan menghasilkan error, bukan diam-diam diperlakukan sebagai nama file. Exit code: | Kode | Arti | |---|---| | `0` | Berhasil | | `1` | Gagal — preset tidak dikenal, file tidak ada, state kosong, atau inference error | Contoh pemakaian error yang umum: ```bash $ ./usage-classify.sh --preset salah "teks" Unknown preset: salah. Available: triage $ ./usage-classify.sh Usage: ./usage-classify.sh [--preset ] [--questions ] [--json] ``` --- ## 9. Konfigurasi Semua default ada di `config/classify.py`: ```python LAYACPP = /third_party/laya.cpp CLI = LAYACPP / "build-cpu" / "bin" / "laya-cli" MODEL_ROOT = MODEL_DIR / "convaiinnovations" / "laya" VARIANT = "multilingual" CHECKPOINT = MODEL_ROOT / VARIANT / "model.safetensors" BACKEND = "cpu" # cpu | cuda | vulkan | coreml ALLOW_TRUNCATION = False LAYA_URL = "" # "" = spawn CLI per jalan TIMEOUT = 120 PRESETS = { "triage": {...} } DEFAULT_PRESET = "triage" ``` | Variabel | Fungsi | |---|---| | `CLI` | Lokasi binary `laya-cli` hasil build | | `MODEL_ROOT` | Direktori induk checkpoint | | `VARIANT` | Varian checkpoint: `multilingual`, `english`, atau `typed-decisions` | | `BACKEND` | Backend komputasi. `cpu` = pure CPU, tanpa GPU sama sekali | | `ALLOW_TRUNCATION` | Kalau `True`, input kepanjangan dipotong diam-diam. Default: request ditolak | | `LAYA_URL` | Fallback URL server kalau env dan flag tidak di-set | | `PRESETS` | Kumpulan pertanyaan siap pakai | | `DEFAULT_PRESET` | Preset yang dipakai kalau `--preset` tidak diberikan | ### Ganti ke GPU nanti Ubah `BACKEND` jadi `"cuda"` atau `"vulkan"`. Tapi kamu harus pakai binary laya.cpp yang dikompilasi dengan backend tersebut — build CPU-only (yang sekarang) tidak bisa. Detail di [README.md](README.md#build-layacpp-cpu-only). --- ## 10. Setup dari nol Di mesin ini **sudah selesai**. Kalau perlu diulang (misal di mesin lain): ```bash ./build-laya.sh ``` Script-nya **tidak butuh `sudo`** sama sekali: - `cmake` dan `ninja` diambil dari pip wheel - `nlohmann/json` diekstrak ke prefix lokal `third_party/nlohmann-install` - satu-satunya kebutuhan sistem adalah `libicu-dev` Script aman dijalankan berulang — setiap langkah dilewati kalau output-nya sudah ada. Yang dilakukan script: 1. Clone `laya.cpp` beserta submodulnya (`ggml`, `cpp-httplib`) ke `third_party/laya.cpp` 2. Pasang `cmake` (versi 3.x) dan `ninja` lewat pip kalau belum ada 3. Ekstrak `nlohmann/json` ke prefix lokal 4. Build CPU-only ke `third_party/laya.cpp/build-cpu` ### Tiga hal yang wajib | Flag | Kenapa wajib | |---|---| | `-DLAYA_CUDA=OFF` | Default-nya **ON**, dan konfigurasi akan gagal mencari `nvcc` | | `cmake` versi 3.x | ggml yang di-pin laya.cpp deklarasikan `cmake_minimum_required(3.14...3.28)` dan **tidak bisa dikonfigurasi di CMake 4** | | `--cpu` saat runtime | Default `laya-cli` adalah backend CUDA, jadi build CPU-only akan gagal start tanpa flag ini | ### Kenapa harus build dari sumber? Upstream laya.cpp **tidak menyediakan** binary prebuilt khusus CPU — rilis Linux hanya CUDA 12, CUDA 13, dan Vulkan. Kalau di mesin dengan GPU NVIDIA, kamu bisa unduh binary prebuilt dan menghemat langkah build, tapi build CPU-only ini nol dependensi GPU. ### Struktur checkpoint Checkpoint ada di `models/convaiinnovations/laya/multilingual/`. > **Penting:** `laya-cli` menambahkan nama varian ke path `--model` untuk semua varian non-English. > Jadi checkpoint multilingual **harus** berada di `/multilingual/`. > > Ada salinan bobot yang identik di `models/convaiinnovations/laya-multilingual/`, tapi layout itu > **tidak bisa** dipakai laya.cpp — direktori itu untuk implementasi referensi Python. --- ## 11. Catatan penting Dua keterbatasan dari model card upstream yang belum diatasi runner ini. Keduanya penting sebelum kamu mempercayai angkanya: ### 1. Probabilitasnya belum dikalibrasi Model ini di-ship **uncalibrated** dengan `temperature = [1.0, 1.0, 1.0]` tanpa bucket per jumlah opsi. Akibatnya dia **sistematis over-confident** — rata-rata confidence 0,75–0,83 padahal akurasinya jauh lebih rendah. Jadi nilai `confidence=1.0` di output **belum tentu akurat**. Untuk membaikkannya, refit satu temperatur per kombinasi (tipe pertanyaan, jumlah opsi) di data held-out milikmu sendiri. Dari model card, ini memindahkan mean ECE dari **0,314 → 0,106**. Lakukan ini sebelum memperlakukan probabilitas sebagai ambang keputusan. ### 2. Tipe `noul` bisa under-report `true` Pada input yang jelas positif, satu pengukuran menempatkan `P(true)` di kisaran 0,5 sementara kasus negatifnya benar-benar dekat 0. Kalau jawaban `noul` terasa lemah, **cek ulang** dengan versi dua opsi: ```json { "cek": { "type": "choice", "instructions": "Does the text ask for a refund?", "criteria": { "A": "no, it does not ask for a refund", "B": "yes, it asks for a refund" } } } ``` ### 3. Bahasa dengan sumber daya rendah masih lemah SWE, Tamil, Amharic masih berada di rentang 0,110–0,250. Check akurasi di bahasa-bahasa yang memang kamu pakai, jangan diasumsikan seragam. ### 4. Model multilingual lebih lemah untuk teks Inggris Makro accuracy 0,619 (multilingual) vs 0,684 (checkpoint Inggris). Kalau mostly English, routing ke checkpoint `laya` (Inggris) lebih akurat daripada memaksakan pakai multilingual. --- ## Ringkasan perintah ```bash # Klasifikasi teks ./usage-classify.sh "teks kamu" # Dari file ./usage-classify.sh --state-file ticket.txt # Pertanyaan sendiri ./usage-classify.sh --questions my_questions.json "teks kamu" # Output JSON untuk di-pipe ./usage-classify.sh "teks kamu" --json # Mode cepat: nyalakan server sekali third_party/laya.cpp/build-cpu/bin/laya-cli --server --port 8080 \ --model models/convaiinnovations/laya --variant multilingual --cpu & LAYA_URL=http://127.0.0.1:8080 ./usage-classify.sh "teks kamu" # Setup (sekali saja, tanpa sudo) ./build-laya.sh ```