Files
manjapro_v6/whapi_webhook/ai/ResponAI.php
T
2025-09-21 21:40:17 +07:00

819 lines
36 KiB
PHP

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