Update Banyak
This commit is contained in:
@@ -0,0 +1,818 @@
|
||||
<?php
|
||||
include_once __DIR__ . "/ResponAI_Helper.php";
|
||||
|
||||
class ResponAI {
|
||||
|
||||
private $pdo;
|
||||
private $helper;
|
||||
|
||||
public function __construct(PDO $pdo) { // Terima PDO di sini juga
|
||||
$this->pdo = $pdo;
|
||||
$this->helper = new ResponAI_Helper($this->pdo); // Teruskan PDO ke helper
|
||||
}
|
||||
|
||||
// Tambahan properti untuk whitelist & alias agar tidak undefined
|
||||
private $ALLOWED_COLUMNS = [
|
||||
'v_tiket_gangguan' => [
|
||||
'id','id_pelanggan','permasalahan','status','jenis_tiket','start_time'
|
||||
],
|
||||
'v_pelanggan' => [
|
||||
'akun','nama','status_koneksi','alamat','status','nama_paket','harga_paket', 'nomor_whatsapp'
|
||||
],
|
||||
'v_tagihan' => [
|
||||
'id','id_pelanggan','akun','nomor_whatsapp',
|
||||
'total_tagihan','limit_pembayaran','status'
|
||||
],
|
||||
'rekening_bank' => [
|
||||
'id_rekening','nama_bank','no_rekening','pemilik'
|
||||
],
|
||||
'v_order_list' => [
|
||||
'nama','jenis_layanan','start_time','alamat','status','nomor_whatsapp'
|
||||
],
|
||||
'profile_paket' => [
|
||||
'jenis_profile','nama','harga'
|
||||
],
|
||||
'promo' => [
|
||||
'id_promo','nama_promo','deskripsi','tanggal_mulai','tanggal_akhir'
|
||||
],
|
||||
'setting_mikrotik' => [
|
||||
'nama','status'
|
||||
],
|
||||
'cs_contact' => [
|
||||
'id_contact','nama','nomor_wa','email','jabatan'
|
||||
],
|
||||
'teknisi' => [
|
||||
'id_teknisi','nama','nomor_wa','area'
|
||||
],
|
||||
'v_jumlah_transaksi_bulanan' => [
|
||||
'bulan','pemasukan','pengeluaran','saldo'
|
||||
]
|
||||
];
|
||||
|
||||
private $ALIASES = [
|
||||
'v_tiket_gangguan' => [
|
||||
'ticket'=>'id','id_tiket'=>'id',
|
||||
'pelanggan_id'=>'id_pelanggan',
|
||||
'problem'=>'permasalahan','issue'=>'permasalahan',
|
||||
'jenis'=>'jenis_tiket','type'=>'jenis_tiket',
|
||||
'mulai'=>'start_time','waktu'=>'start_time'
|
||||
],
|
||||
'v_pelanggan' => [
|
||||
'id'=>'akun','pppoe'=>'akun','username'=>'akun','customer_id'=>'akun','id_pelanggan'=>'akun',
|
||||
'name'=>'nama','customer'=>'nama','pelanggan'=>'nama',
|
||||
'paket'=>'nama_paket','package'=>'nama_paket','plan'=>'nama_paket','layanan'=>'nama_paket',
|
||||
'harga'=>'harga_paket','biaya'=>'harga_paket',
|
||||
'koneksi'=>'status_koneksi','status_layanan'=>'status_koneksi','status_internet'=>'status_koneksi',
|
||||
'aktif'=>'status','nonaktif'=>'status','isolir'=>'status',
|
||||
'alamat_pelanggan'=>'alamat','alamat_lengkap'=>'alamat'
|
||||
],
|
||||
'v_tagihan' => [
|
||||
'id'=>'id_tagihan','invoice'=>'id_tagihan',
|
||||
'pppoe'=>'akun','username'=>'akun','customer_id'=>'akun',
|
||||
'wa'=>'nomor_whatsapp','no_wa'=>'nomor_whatsapp','hp'=>'nomor_whatsapp',
|
||||
'amount'=>'total_tagihan','tagihan'=>'total_tagihan','nominal'=>'total_tagihan','total'=>'total_tagihan',
|
||||
'due'=>'limit_pembayaran','jatuh_tempo'=>'limit_pembayaran','tempo'=>'limit_pembayaran',
|
||||
'status_tagihan'=>'status','pending'=>'status','lunas'=>'status','belum'=>'status'
|
||||
],
|
||||
'rekening_bank' => [
|
||||
'bank'=>'nama_bank','rekening'=>'no_rekening','account_number'=>'no_rekening','atas_nama'=>'pemilik'
|
||||
],
|
||||
'v_order_list' => [
|
||||
'customer'=>'nama','layanan'=>'jenis_layanan',
|
||||
'tanggal'=>'start_time','mulai'=>'start_time',
|
||||
'alamat_pemasangan'=>'alamat','lokasi'=>'alamat',
|
||||
'wa'=>'nomor_whatsapp','nohp'=>'nomor_whatsapp'
|
||||
],
|
||||
'profile_paket' => [
|
||||
'nama_profile'=>'nama','jenis'=>'jenis_profile',
|
||||
'harga_paket'=>'harga','biaya'=>'harga'
|
||||
],
|
||||
'promo' => [
|
||||
'promo_id'=>'id_promo','kode'=>'id_promo',
|
||||
'nama_promo'=>'nama_promo','promo'=>'nama_promo',
|
||||
'desc'=>'deskripsi','keterangan'=>'deskripsi',
|
||||
'mulai'=>'tanggal_mulai','start'=>'tanggal_mulai',
|
||||
'akhir'=>'tanggal_akhir','selesai'=>'tanggal_akhir'
|
||||
],
|
||||
'setting_mikrotik' => [
|
||||
'device'=>'nama','router'=>'nama',
|
||||
'status_router'=>'status','aktif'=>'status','nonaktif'=>'status'
|
||||
],
|
||||
'cs_contact' => [
|
||||
'contact_id'=>'id_contact','id_cs'=>'id_contact',
|
||||
'cs'=>'nama','customer_service'=>'nama','admin'=>'nama',
|
||||
'wa'=>'nomor_wa','nohp'=>'nomor_wa',
|
||||
'posisi'=>'jabatan','jabatan_cs'=>'jabatan'
|
||||
],
|
||||
'teknisi' => [
|
||||
'id'=>'id_teknisi','id_tech'=>'id_teknisi',
|
||||
'nama_teknisi'=>'nama','technician'=>'nama',
|
||||
'wa'=>'nomor_wa','nohp'=>'nomor_wa',
|
||||
'daerah'=>'area','wilayah'=>'area'
|
||||
],
|
||||
'v_jumlah_transaksi_bulanan' => [
|
||||
'month'=>'bulan','periode'=>'bulan',
|
||||
'income'=>'pemasukan','debit'=>'pemasukan',
|
||||
'expense'=>'pengeluaran','credit'=>'pengeluaran',
|
||||
'balance'=>'saldo','cash'=>'saldo'
|
||||
]
|
||||
];
|
||||
|
||||
private $DB_KEYWORDS = [
|
||||
'pelanggan' => ['nama','akun','id','username','status','paket','alamat','koneksi','harga'],
|
||||
'tagihan' => ['tagihan','invoice','pembayaran','bayar','periode','tempo','tanggal','total','status'],
|
||||
'paket' => ['paket','profile_paket','layanan','harga','jenis_profile','upgrade','downgrade'],
|
||||
'promo' => ['promo','diskon','bonus','voucher','penawaran','potongan','gratis'],
|
||||
'gangguan' => ['gangguan','trouble','tiket','keluhan','permasalahan','lemot','putus','down'],
|
||||
'pemasangan'=> ['pemasangan','registrasi','order','survey','baru','langganan','daftar'],
|
||||
'info_jaringan'=> ['jaringan','mikrotik','status','router','perangkat','down area','maintenance'],
|
||||
'keuangan' => ['keuangan','saldo','pemasukan','pengeluaran','kas','laporan','transaksi','bulan']
|
||||
];
|
||||
|
||||
private function getTableMappingByTopic(string $topic): array {
|
||||
$mapping = [
|
||||
'gangguan' => [
|
||||
'tables' => [
|
||||
'v_tiket_gangguan' => [
|
||||
'columns' => ['id', 'id_pelanggan', 'permasalahan', 'status', 'jenis_tiket', 'start_time'],
|
||||
'where' => [
|
||||
'id_pelanggan' => 'From Value: id_pelanggan_db',
|
||||
'jenis_tiket' => 'Value List DB: Pelanggan, Jalur Utama, Lainnya'
|
||||
]
|
||||
],
|
||||
'v_pelanggan' => [
|
||||
'columns' => ['akun', 'nama', 'status_koneksi', 'alamat', 'status'],
|
||||
'where' => [
|
||||
'akun' => 'From Value: id_pelanggan',
|
||||
'status_koneksi' => 'Value List DB: On, Off',
|
||||
'status' => 'Value List DB: register, aktif, tidak aktif, sampah',
|
||||
'nama'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'tagihan' => [
|
||||
'tables' => [
|
||||
'v_tagihan' => [
|
||||
'columns' => ['id', 'akun', 'nomor_whatsapp', 'verifikasi_tagihan', 'limit_pembayaran', 'status', 'transaction_link'],
|
||||
'where' => [
|
||||
'akun' => 'From Value: id_pelanggan',
|
||||
'limit_pembayaran' => 'Format Y-m-d',
|
||||
'nomor_whatsapp',
|
||||
'verifikasi_tagihan' => 'Sama dengan Jumlah Tagihan',
|
||||
]
|
||||
]
|
||||
// ,
|
||||
// 'v_rekening_bank' => [
|
||||
// 'columns' => ['nama_bank', 'no_rekening', 'pemilik'],
|
||||
// 'where' => [],
|
||||
// ]
|
||||
]
|
||||
],
|
||||
'pemasangan' => [
|
||||
'tables' => [
|
||||
'v_order_list' => [
|
||||
'columns' => ['nama', 'jenis_layanan', 'start_time', 'alamat', 'status', 'nomor_whatsapp'],
|
||||
'where' => [
|
||||
'nama'
|
||||
]
|
||||
],
|
||||
'master_desa' => [
|
||||
'columns' => ['nama'],
|
||||
'where' => []
|
||||
],
|
||||
'v_master_topologi' => [
|
||||
'columns' => ['nama', 'titik_koordinat', 'sisa_port'],
|
||||
'where' => [
|
||||
'id_data_server'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'paket' => [
|
||||
'tables' => [
|
||||
'profile_paket' => [
|
||||
'columns' => ['jenis_profile', 'nama', 'harga_paket'],
|
||||
'where' => []
|
||||
]
|
||||
]
|
||||
],
|
||||
'promo' => [
|
||||
'tables' => [
|
||||
'master_promo' => [
|
||||
'columns' => ['nama', 'deskripsi', 'tanggal_mulai', 'tanggal_akhir', 'gambar', 'status'],
|
||||
'where' => [
|
||||
'status'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'info_jaringan' => [
|
||||
'tables' => [
|
||||
'setting_mikrotik' => [
|
||||
'columns' => ['nama', 'status'],
|
||||
'where' => [
|
||||
'id_data_server'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'keuangan' => [
|
||||
'tables' => [
|
||||
'v_jumlah_transaksi_bulanan' => [
|
||||
'columns' => ['bulan', 'pemasukan', 'pengeluaran', 'saldo'],
|
||||
'where' => [
|
||||
'id_data_server' => 'Wajib ambil dari id jika Owner',
|
||||
'bulan' => 'Format Y-m-d'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'umum' => [
|
||||
'tables' => []
|
||||
]
|
||||
];
|
||||
|
||||
return $mapping[$topic] ?? $mapping['umum'];
|
||||
}
|
||||
|
||||
private function deteksi_topik(string $message): string {
|
||||
$rules = [
|
||||
'gangguan' => [
|
||||
'lambat', 'tidak konek', 'jaringan', 'trouble', 'gangguan',
|
||||
'internet mati', 'lemot', 'putus', 'disconnect', 'down',
|
||||
'error', 'tidak stabil', 'signal hilang', 'tidak bisa akses',
|
||||
'modem rusak', 'router error', 'kabel putus', 'wifi putus'
|
||||
],
|
||||
'tagihan' => [
|
||||
'tagihan', 'bayar', 'invoice', 'pembayaran', 'rekening',
|
||||
'saldo', 'jatuhtempo', 'denda', 'transfer', 'virtual account',
|
||||
'lunas', 'belum bayar', 'auto debit', 'cicilan', 'konfirmasi pembayaran'
|
||||
],
|
||||
'pemasangan' => [
|
||||
'pasang', 'berlangganan', 'pemasangan', 'registrasi', 'daftar',
|
||||
'masang', 'order', 'baru', 'paket baru', 'apply', 'subscribe',
|
||||
'survey lokasi', 'coverage', 'jangkauan', 'titik koordinat'
|
||||
],
|
||||
'paket' => [
|
||||
'paket', 'upgrade', 'downgrade', 'speed', 'kecepatan',
|
||||
'ganti paket', 'ubah paket', 'langganan paket',
|
||||
'upgrade speed', 'add-on', 'kuota tambahan'
|
||||
],
|
||||
'promo' => [
|
||||
'promo', 'diskon', 'gratis', 'voucher', 'bonus', 'penawaran',
|
||||
'paket murah', 'potongan harga'
|
||||
],
|
||||
'info_jaringan' => [
|
||||
'pemeliharaan', 'maintenance', 'perbaikan', 'upgrade jaringan', 'down area'
|
||||
],
|
||||
'cs' => [
|
||||
'hubungi admin', 'nomor wa', 'teknisi', 'kontak', 'call center', 'customer service'
|
||||
],
|
||||
'keuangan' => [
|
||||
'keuangan', 'laporan', 'saldo', 'kas', 'arus kas', 'transaksi',
|
||||
'pemasukan', 'pengeluaran', 'neraca', 'pembukuan', 'rekap bulanan', 'omzet', 'omset', 'laba'
|
||||
],
|
||||
'umum' => []
|
||||
];
|
||||
|
||||
$message = strtolower($message);
|
||||
|
||||
foreach ($rules as $topik => $keywords) {
|
||||
foreach ($keywords as $keyword) {
|
||||
if (strpos($message, $keyword) !== false) {
|
||||
return $topik;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'umum';
|
||||
}
|
||||
|
||||
private function simpan_sesi(string $sender, string $topik): void {
|
||||
$stmt = $this->pdo->prepare("INSERT INTO chat_session (nomor_whatsapp, topik_terakhir)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE topik_terakhir = VALUES(topik_terakhir)");
|
||||
$stmt->execute([$sender, $topik]);
|
||||
}
|
||||
|
||||
private function ambil_sesi(string $sender): ?string {
|
||||
$stmt = $this->pdo->prepare("SELECT topik_terakhir FROM chat_session WHERE nomor_whatsapp = ?");
|
||||
$stmt->execute([$sender]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $row['topik_terakhir'] ?? null;
|
||||
}
|
||||
|
||||
private function simpan_log(string $sender, string $role, string $pesan, $id = null): void {
|
||||
$stmt = $this->pdo->prepare("INSERT INTO chat_log (nomor_whatsapp, role, pesan, id_data_server)
|
||||
VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$sender, $role, $pesan, $id]);
|
||||
}
|
||||
|
||||
|
||||
private function get_topik($id_servers, string $topik) {
|
||||
// Pastikan topik huruf pertama kapital tetap dicocokkan case-insensitive
|
||||
$topik_final = trim($topik);
|
||||
|
||||
// jika $id_servers berupa string "3,4,5", ubah ke array
|
||||
if (is_string($id_servers)) {
|
||||
$id_servers = array_map('trim', explode(',', $id_servers));
|
||||
}
|
||||
|
||||
// jika $id_servers cuma satu angka, tetap bikin array
|
||||
if (!is_array($id_servers)) {
|
||||
$id_servers = [$id_servers];
|
||||
}
|
||||
|
||||
// buat placeholders sesuai jumlah ID
|
||||
$placeholders = implode(',', array_fill(0, count($id_servers), '?'));
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT metadata
|
||||
FROM chat_topik
|
||||
WHERE id_data_server IN ($placeholders)
|
||||
AND LOWER(topik) = LOWER(?)
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmt->execute(array_merge($id_servers, [$topik_final]));
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $result ? $result['metadata'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whitelist tabel+kolom berdasarkan ROLE (global), nanti masih akan dipersempit oleh TOPIK
|
||||
*/
|
||||
private function getAllowedMappingByRole(string $role): array {
|
||||
// Daftar izin setiap role. Owner = semua (gunakan '*' sebagai wildcard).
|
||||
$roleMapping = [
|
||||
'umum' => [
|
||||
'profile_paket' => ['jenis_profile', 'nama', 'harga'],
|
||||
'promo' => ['id_promo', 'nama_promo', 'deskripsi', 'tanggal_mulai', 'tanggal_akhir'],
|
||||
'v_order_list' => ['nama', 'jenis_layanan', 'start_time', 'alamat', 'status', 'nomor_whatsapp'],
|
||||
// 'master_desa' bisa ditambahkan jika ada di DB & ingin diekspos
|
||||
],
|
||||
'pelanggan' => [
|
||||
'profile_paket' => ['jenis_profile', 'nama', 'harga'],
|
||||
'promo' => ['id_promo', 'nama_promo', 'deskripsi', 'tanggal_mulai', 'tanggal_akhir'],
|
||||
'v_order_list' => ['nama', 'jenis_layanan', 'start_time', 'alamat', 'status', 'nomor_whatsapp'],
|
||||
'v_pelanggan' => ['akun', 'nama', 'status_koneksi', 'alamat', 'status', 'nama_paket', 'harga_paket'],
|
||||
'v_tagihan' => ['id', 'id_pelanggan', 'akun', 'nomor_whatsapp', 'total_tagihan', 'limit_pembayaran', 'status'],
|
||||
'v_tiket_gangguan' => ['id', 'id_pelanggan', 'permasalahan', 'status', 'jenis_tiket', 'start_time'],
|
||||
'rekening_bank' => ['id_rekening', 'nama_bank', 'no_rekening', 'pemilik'] // jika memang boleh dilihat pelanggan
|
||||
],
|
||||
'karyawan' => [
|
||||
'profile_paket' => ['jenis_profile', 'nama', 'harga'],
|
||||
'promo' => ['id_promo', 'nama_promo', 'deskripsi', 'tanggal_mulai', 'tanggal_akhir'],
|
||||
'v_order_list' => ['nama', 'jenis_layanan', 'start_time', 'alamat', 'status', 'nomor_whatsapp'],
|
||||
'v_pelanggan' => ['akun', 'nama', 'status_koneksi', 'alamat', 'status', 'nama_paket', 'harga_paket'],
|
||||
'v_tagihan' => ['id', 'id_pelanggan', 'akun', 'nomor_whatsapp', 'total_tagihan', 'limit_pembayaran', 'status'],
|
||||
'v_tiket_gangguan' => ['id', 'id_pelanggan', 'permasalahan', 'status', 'jenis_tiket', 'start_time'],
|
||||
'setting_mikrotik' => ['nama', 'status'],
|
||||
'rekening_bank' => ['id_rekening', 'nama_bank', 'no_rekening', 'pemilik']
|
||||
],
|
||||
'owner' => ['*'] // akses penuh
|
||||
];
|
||||
|
||||
return $roleMapping[$role] ?? $roleMapping['umum'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Persempit mapping topik dengan role (tabel+kolom)
|
||||
*/
|
||||
private function narrowMappingByTopicAndRole(array $topicMapping, array $roleMapping): array {
|
||||
// Owner full pass
|
||||
if ($roleMapping === ['*']) {
|
||||
return $topicMapping;
|
||||
}
|
||||
|
||||
$filtered = ['tables' => []];
|
||||
|
||||
foreach ($topicMapping['tables'] as $table => $cols) {
|
||||
if (!isset($roleMapping[$table])) {
|
||||
continue; // tabel ini tidak diizinkan role
|
||||
}
|
||||
$allowedCols = $roleMapping[$table];
|
||||
$finalCols = array_values(array_intersect($cols, $allowedCols));
|
||||
if (!empty($finalCols)) {
|
||||
$filtered['tables'][$table] = $finalCols;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format mapping (tabel+kolom) untuk dikirim ke AI Stage 1 (string JSON yang rapi)
|
||||
*/
|
||||
private function mappingToString(array $mapping): string {
|
||||
// Misal: {"v_tagihan":["id_tagihan","akun"],"promo":["id_promo","nama_promo"]}
|
||||
return json_encode($mapping['tables'], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set $this->ALLOWED_COLUMNS dari mapping final agar validator DB bisa pakai
|
||||
*/
|
||||
private function hydrateAllowedColumns(array $finalMapping): void {
|
||||
$this->ALLOWED_COLUMNS = [];
|
||||
foreach ($finalMapping['tables'] as $table => $cols) {
|
||||
$this->ALLOWED_COLUMNS[strtolower($table)] = array_map('strtolower', $cols);
|
||||
}
|
||||
}
|
||||
|
||||
public function respon_chat(string $body, string $sender, string $apikey): array {
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "=== Mulai respon_chat ===");
|
||||
$this->log_to_file($sender, "DEBUG", "Body pesan: $body");
|
||||
$this->log_to_file($sender, "DEBUG", "API Key: $apikey");
|
||||
|
||||
$idServers = $this->helper->get_id_server($apikey);
|
||||
$this->log_to_file($sender, "DEBUG", "ID Server hasil lookup: " . json_encode($idServers));
|
||||
|
||||
$this->log_to_file($sender, "Terima Pesan", "dari $sender: $body");
|
||||
|
||||
$roless = $this->helper->cek_role($sender);
|
||||
$this->log_to_file($sender, "DEBUG", "Role hasil cek: " . json_encode($roless));
|
||||
$role = $roless['role'];
|
||||
|
||||
$sesi_lama = $this->ambil_sesi($sender);
|
||||
$topik_baru = $this->deteksi_topik($body);
|
||||
$topik_final = $topik_baru ?: $sesi_lama ?: 'umum';
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "Topik lama: " . ($sesi_lama ?: 'null'));
|
||||
$this->log_to_file($sender, "DEBUG", "Topik baru: $topik_baru");
|
||||
$this->log_to_file($sender, "DEBUG", "Topik final: $topik_final");
|
||||
|
||||
$this->simpan_sesi($sender, $topik_final);
|
||||
|
||||
$roleInstructions = [
|
||||
"umum" => "Role umum hanya boleh mengakses informasi publik: daftar paket internet (profile_paket) dan informasi wilayah (master_desa). Role ini tidak boleh mengakses data pelanggan (v_pelanggan) maupun data tagihan (v_tagihan).",
|
||||
|
||||
"pelanggan" => "Role pelanggan mewarisi hak akses role umum. Selain itu, pelanggan boleh mengakses data tagihan miliknya sendiri di v_tagihan (difilter berdasarkan nomor_whatsapp). Jika pelanggan menanyakan informasi terkait dirinya (nama, alamat, server, paket, dll), ambil dari Database Statis, bukan langsung dari tabel dinamis.",
|
||||
|
||||
"karyawan" => "Role karyawan mewarisi akses role pelanggan dan umum. Selain itu, karyawan boleh mengakses data pelanggan (v_pelanggan) dan data tagihan (v_tagihan) tanpa batasan nomor_whatsapp. Cocok untuk kebutuhan pelayanan atau pengecekan teknis.",
|
||||
|
||||
"owner" => "Role owner memiliki akses penuh ke semua tabel (profile_paket, master_desa, v_pelanggan, v_tagihan) dan tambahan hak untuk menjalankan query agregasi, misalnya menghitung total omset, jumlah pelanggan, serta statistik penggunaan layanan."
|
||||
];
|
||||
|
||||
$roleRule = $roleInstructions[$role] ?? "Default: hanya boleh menjawab pertanyaan umum tanpa akses database.";
|
||||
|
||||
$waktu_batas = date('Y-m-d H:i:s', strtotime('-15 minutes'));
|
||||
$stmt = $this->pdo->prepare("SELECT role, pesan FROM chat_log WHERE nomor_whatsapp = ? AND waktu > ? ORDER BY id DESC LIMIT 5");
|
||||
$stmt->execute([$sender, $waktu_batas]);
|
||||
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- PEMBATASAN TABEL+KOLOM UNTUK STAGE 1 (TOPIK + ROLE) ---
|
||||
$topicMapping = $this->getTableMappingByTopic($topik_final); // tabel/kolom sesuai topik
|
||||
$roleMapping = $this->getAllowedMappingByRole($role); // whitelisting global per role
|
||||
$finalMapping = $this->narrowMappingByTopicAndRole($topicMapping, $roleMapping); // gabungan topik+role
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "TopicMapping: " . json_encode($topicMapping));
|
||||
$this->log_to_file($sender, "DEBUG", "RoleMapping: " . json_encode($roleMapping));
|
||||
$this->log_to_file($sender, "DEBUG", "FinalMapping: " . json_encode($finalMapping));
|
||||
|
||||
// Fallback: kalau hasil kosong, pakai topik 'umum' lalu persempit lagi
|
||||
if (empty($finalMapping['tables'])) {
|
||||
$topicMapping = $this->getTableMappingByTopic('umum');
|
||||
$finalMapping = $this->narrowMappingByTopicAndRole($topicMapping, $roleMapping);
|
||||
}
|
||||
|
||||
// Set allowed columns untuk validator query di tahap eksekusi
|
||||
$this->hydrateAllowedColumns($finalMapping);
|
||||
|
||||
// String tabel yang boleh diakses untuk dimasukkan ke prompt Stage 1
|
||||
$tableMappingString = $this->mappingToString($finalMapping);
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "TableMapping string (Stage 1): $tableMappingString");
|
||||
|
||||
// ========== BLOK ROLE LAMA DIPERTAHANKAN (dibetulkan operatornya) ==========
|
||||
// Catatan: sekarang variabel $tableMapping diisi string JSON whitelist agar AI tidak ngarang.
|
||||
if ($role == 'umum') {
|
||||
$mapping = $this->getTableMappingByTopic($topik_final);
|
||||
$tableMapping = $tableMappingString; // hanya boleh pemasangan, paket, promo sesuai finalMapping
|
||||
}
|
||||
elseif ($role == 'pelanggan') {
|
||||
$mapping = $this->getTableMappingByTopic($topik_final);
|
||||
$tableMapping = $tableMappingString; // pemasangan, paket, promo, pelanggan, tagihan, gangguan (tergantung topik)
|
||||
}
|
||||
elseif ($role == 'karyawan') {
|
||||
$mapping = $this->getTableMappingByTopic($topik_final);
|
||||
$tableMapping = $tableMappingString; // + info_jaringan bila topiknya sesuai
|
||||
}
|
||||
elseif ($role == 'owner') {
|
||||
$mapping = $this->getTableMappingByTopic($topik_final);
|
||||
$tableMapping = $tableMappingString; // semua (karena owner di roleMapping = '*')
|
||||
}
|
||||
// ==========================================================================
|
||||
|
||||
$userContext = json_encode($roless['data'], JSON_PRETTY_PRINT);
|
||||
|
||||
$messages_stage1 = [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "Tugas: ubah pertanyaan user menjadi instruksi query database dalam format JSON.
|
||||
Gunakan hanya tabel & kolom berikut (strict whitelist): $tableMapping
|
||||
|
||||
📌 Data user yang tersedia untuk WHERE:
|
||||
$userContext
|
||||
|
||||
📌 Aturan pembuatan WHERE:
|
||||
- Ikuti aturan WHERE di strict whitelist
|
||||
- Gunakan kolom dari data user di atas (contoh: nomor_whatsapp, akun, id_pelanggan).
|
||||
- Jangan menebak isi kolom. Jika field tidak ada di data user, kosongkan WHERE.
|
||||
- Jika pertanyaan terkait data pribadi, gunakan nomor_whatsapp dari data user.
|
||||
- Jika query umum (tanpa filter), kosongkan WHERE: {}
|
||||
|
||||
📌 Contoh format JSON:
|
||||
[
|
||||
{\"action\": \"GET\", \"table\": \"v_pelanggan\", \"where\": {\"nomor_whatsapp\": \"{{sender}}\"}, \"columns\": [\"nama\", \"nama_paket\", \"status_koneksi\"]},
|
||||
{\"action\": \"COUNT\", \"table\": \"v_tagihan\", \"where\": {\"status\": \"belum bayar\"}},
|
||||
{\"action\": \"GET\", \"table\": \"profile_paket\", \"where\": {}, \"columns\": [\"nama\", \"jenis_profile\", \"harga\"]}
|
||||
]
|
||||
|
||||
📌 Jika tidak ada query DB diperlukan:
|
||||
- Kembalikan JSON kosong: {}
|
||||
- atau {\"action\": \"NONE\"}."
|
||||
]
|
||||
];
|
||||
|
||||
// foreach (array_reverse($history) as $h) {
|
||||
// $messages_stage1[] = ["role" => $h['role'], "content" => $h['pesan']];
|
||||
// }
|
||||
// $messages_stage1[] = ["role" => "user", "content" => $body];
|
||||
|
||||
file_put_contents(__DIR__ . "/result_stage1.log", json_encode($messages_stage1, JSON_PRETTY_PRINT) . "\n", FILE_APPEND);
|
||||
|
||||
$payload_stage1 = [
|
||||
"model" => "gpt-4.1-nano",
|
||||
"messages" => $messages_stage1,
|
||||
"temperature" => 0.3,
|
||||
"max_tokens" => 500 // Cukup untuk JSON
|
||||
];
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "Payload Stage 1: " . json_encode($payload_stage1));
|
||||
|
||||
$reply_stage1 = $this->call_openai($payload_stage1);
|
||||
$this->log_to_file($sender, "Proses AI Tahap 1 (JSON)", $reply_stage1);
|
||||
|
||||
$this->simpan_log($sender, "user", $body, $idServers); // Log pesan user
|
||||
|
||||
$db_results = [];
|
||||
$needsDB = false;
|
||||
$replySanitized = $this->sanitize_json($reply_stage1);
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "Reply Stage 1 (sanitized): $replySanitized");
|
||||
// Coba decode JSON dari tahap 1
|
||||
$jsonAI = json_decode($replySanitized, true);
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "Parsed JSON Stage 1: " . json_encode($jsonAI));
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($jsonAI) && !empty($jsonAI)) {
|
||||
// Cek apakah ini array of objects atau single object
|
||||
$queries = isset($jsonAI[0]) ? $jsonAI : [$jsonAI];
|
||||
|
||||
// Filter out "NONE" action if it's the only one
|
||||
if (count($queries) === 1 && isset($queries[0]['action']) && strtoupper($queries[0]['action']) === 'NONE') {
|
||||
$needsDB = false;
|
||||
} else {
|
||||
$needsDB = true;
|
||||
foreach ($queries as $q) {
|
||||
$this->log_to_file($sender, "DEBUG", "Eksekusi DB Query: " . json_encode($q));
|
||||
if (!isset($q['action'], $q['table'])) {
|
||||
$db_results[] = ["error" => "Format query tidak valid: " . json_encode($q)];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validasi tabel
|
||||
$table = strtolower(trim($q['table']));
|
||||
if (!isset($this->ALLOWED_COLUMNS[$table])) {
|
||||
$db_results[] = ["error" => "Tabel tidak diizinkan atau tidak valid: {$q['table']}"];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validasi columns: jika tidak diset, izinkan semua kolom yang di-whitelist untuk tabel tsb
|
||||
if (isset($q['columns'])) {
|
||||
if (!is_array($q['columns'])) {
|
||||
$db_results[] = ["error" => "Field 'columns' harus array untuk tabel {$q['table']}"];
|
||||
continue;
|
||||
}
|
||||
$requestedCols = array_map('strtolower', $q['columns']);
|
||||
$diff = array_diff($requestedCols, $this->ALLOWED_COLUMNS[$table]);
|
||||
if (!empty($diff)) {
|
||||
$db_results[] = ["error" => "Kolom tidak diizinkan pada tabel {$q['table']}: " . implode(', ', $diff)];
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Isi default: semua kolom yang diizinkan
|
||||
$q['columns'] = $this->ALLOWED_COLUMNS[$table];
|
||||
}
|
||||
|
||||
// Mapping alias untuk where (opsional)
|
||||
if (isset($q['where']) && is_array($q['where'])) {
|
||||
$q['where'] = $this->map_aliases($table, $q['where']);
|
||||
|
||||
// Validasi key 'where' agar hanya kolom yang diizinkan
|
||||
foreach ($q['where'] as $k => $v) {
|
||||
if (!in_array(strtolower($k), $this->ALLOWED_COLUMNS[$table], true)) {
|
||||
$db_results[] = ["error" => "WHERE mengandung kolom tidak diizinkan pada {$q['table']}: {$k}"];
|
||||
continue 2; // skip query ini
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log_to_file($sender, "Cek DB Query", json_encode($q, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// Eksekusi aman via helper
|
||||
$dbResult = $this->helper->process_db_action($q, $this->ALLOWED_COLUMNS, $idServers);
|
||||
$db_results[] = ["query" => $q, "result" => $dbResult];
|
||||
$this->log_to_file($sender, "DEBUG", "Hasil DB Query: " . json_encode($dbResult));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Jika bukan JSON valid atau JSON kosong, berarti tidak perlu query DB
|
||||
$needsDB = false;
|
||||
}
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "ID Servers sebelum get_topik: " . json_encode($idServers));
|
||||
$this->log_to_file($sender, "DEBUG", "Topik final sebelum get_topik: " . $topik_final);
|
||||
|
||||
$data_topik = $this->get_topik($idServers, $topik_final);
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "Hasil get_topik: " . json_encode($data_topik));
|
||||
|
||||
|
||||
// --- Proses AI Tahap 2: Menghasilkan respons natural ---
|
||||
$messages_stage2 = [
|
||||
["role" => "system", "content" => "Kamu adalah AI Customer Service yang membantu menjawab pertanyaan pelanggan. Berikan respons yang natural, ramah, dan informatif berdasarkan pesan pengguna dan hasil query database (jika ada).
|
||||
|
||||
Hal yang WAJIB DIPATUHI:
|
||||
- selalu manggil 'Kak' Contoh Maaf Kak, Baik Kak
|
||||
- sesekali sapa nama nya jika ada nama di hasil query
|
||||
- selalu menggunakan kata 'kami' jangan menggunakan kata saya,aku,gue,gua
|
||||
- Jika informasi tidak tersedia, katakan dengan sopan akan kami cek terlebih dahulu dan wajib membuat kalimat di akhir '#manualcheck#'
|
||||
- Fokus hanya memberikan informasi yang tersedia dari database dan pengetahuan yang kamu miliki.
|
||||
- Jangan berikan saran apapun kecuali topik nya umum.
|
||||
|
||||
Aturan akses database untuk role '$role': $roleRule
|
||||
|
||||
Topik percakapan saat ini: $topik_final
|
||||
|
||||
Data dan atauran atau langkah-langkah: $data_topik
|
||||
"]
|
||||
];
|
||||
|
||||
foreach (array_reverse($history) as $h) {
|
||||
$messages_stage2[] = ["role" => $h['role'], "content" => $h['pesan']];
|
||||
}
|
||||
$messages_stage2[] = ["role" => "user", "content" => $body];
|
||||
|
||||
// Gabungkan data statis dan query database ke satu entry untuk AI
|
||||
$databaseInfo = [];
|
||||
|
||||
|
||||
if (!empty($roless['data'])) {
|
||||
$databaseInfo[] = "Database Statis: " . json_encode($roless['data'], JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$databaseInfo[] = "Database Statis: Tidak ada query database statis yang dilakukan atau hasil query kosong.";
|
||||
}
|
||||
|
||||
if (!empty($needsDB) && !empty($db_results)) {
|
||||
$databaseInfo[] = "Database Dinamis: " . json_encode($db_results, JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$databaseInfo[] = "Database Dinamis: Tidak ada query database yang dilakukan atau hasil query kosong.";
|
||||
}
|
||||
|
||||
|
||||
// Masukkan ke messages_stage2 sebagai satu sistem content
|
||||
$messages_stage2[] = [
|
||||
"role" => "system",
|
||||
"content" => implode("\n", $databaseInfo)
|
||||
];
|
||||
|
||||
$maxTokens = strlen($body) < 30 ? 100 : (strlen($body) < 100 ? 200 : 300);
|
||||
|
||||
file_put_contents(__DIR__ . "/result_stage2.log", json_encode($messages_stage2, JSON_PRETTY_PRINT) . "\n", FILE_APPEND);
|
||||
|
||||
$payload_stage2 = [
|
||||
"model" => "gpt-4.1-nano",
|
||||
"messages" => $messages_stage2,
|
||||
"temperature" => 0.7, // Suhu lebih tinggi untuk respons natural
|
||||
"max_tokens" => $maxTokens
|
||||
];
|
||||
|
||||
$this->log_to_file($sender, "DEBUG", "Payload Stage 2: " . json_encode($payload_stage2));
|
||||
|
||||
$reply_final = $this->call_openai($payload_stage2);
|
||||
$this->log_to_file($sender, "Proses AI Tahap 2 (Natural Response)", $reply_final);
|
||||
|
||||
// Log hasil akhir ke DB
|
||||
$this->simpan_log($sender, "assistant", $reply_final, $idServers);
|
||||
$this->log_to_file($sender, "DEBUG", "=== Selesai respon_chat ===");
|
||||
|
||||
return $this->make_response($sender, $reply_final);
|
||||
}
|
||||
|
||||
private function should_query_db(string $message): bool {
|
||||
// Fungsi ini tidak lagi digunakan secara langsung untuk decision making utama,
|
||||
// karena keputusan query DB sekarang ada di AI Tahap 1.
|
||||
// Namun, tetap dipertahankan jika ada kebutuhan lain.
|
||||
$message = strtolower($message);
|
||||
foreach ($this->DB_KEYWORDS as $group => $keywords) {
|
||||
foreach ($keywords as $k) {
|
||||
if (strpos($message, $k) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function call_openai(array $payload): string {
|
||||
$ch = curl_init("https://api.openai.com/v1/chat/completions");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: " . "Bearer " . OPENAI_API_KEY
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
$error_msg = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return "⚠️ Gagal menghubungi AI: $error_msg";
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return "⚠️ Respon AI tidak valid JSON.";
|
||||
}
|
||||
return $data['choices'][0]['message']['content'] ?? "⚠️ Tidak ada respon dari AI.";
|
||||
}
|
||||
|
||||
private function sanitize_json(string $reply): string {
|
||||
$clean = trim($reply);
|
||||
// Menghapus markdown code block jika ada
|
||||
if (preg_match('/^```(?:json)?\s*(.*?)\s*```$/s', $clean, $matches)) {
|
||||
$clean = trim($matches[1]);
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function map_aliases(string $table, array $data): array {
|
||||
$result = [];
|
||||
foreach ($data as $key => $val) {
|
||||
$keyLower = strtolower($key);
|
||||
if (isset($this->ALIASES[$table][$keyLower])) {
|
||||
$result[$this->ALIASES[$table][$keyLower]] = $val;
|
||||
} else {
|
||||
$result[$keyLower] = $val;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function make_response(string $sender, string $text): array {
|
||||
return [
|
||||
"id" => 78,
|
||||
"nomor" => $sender,
|
||||
"respon" => trim($text)
|
||||
];
|
||||
}
|
||||
|
||||
public function get_replied(string $sender): array {
|
||||
$stmt = $this->pdo->prepare("SELECT update_time, replied FROM chat_replied WHERE nomor_whatsapp = ?");
|
||||
$stmt->execute([$sender]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ?: [];
|
||||
}
|
||||
|
||||
public function simpan_replied(string $sender, string $replied): void {
|
||||
$update_time = date('Y-m-d H:i:s', strtotime('+5 minutes'));
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO chat_replied (nomor_whatsapp, update_time, replied)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
update_time = VALUES(update_time),
|
||||
replied = VALUES(replied)
|
||||
");
|
||||
$stmt->execute([$sender, $update_time, $replied]);
|
||||
}
|
||||
|
||||
private function write_flow_log($step, $detail) {
|
||||
// Fungsi ini tidak lagi digunakan secara langsung, diganti dengan log_to_file
|
||||
// $sender = $this->currentSender !== null ? $this->currentSender : '-';
|
||||
// $this->log_to_file($sender, $step, $detail);
|
||||
}
|
||||
|
||||
private function log_to_file(string $sender, string $step, $data): void {
|
||||
$logFile = __DIR__ . "/percakapan_log.log";
|
||||
$time = date('Y-m-d H:i:s');
|
||||
$entry = "[$time][$sender][$step] " . (is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data) . PHP_EOL;
|
||||
file_put_contents($logFile, $entry, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user