diff --git a/ai_brain.py b/ai_brain.py index 2f6c23a..fe3ee35 100644 --- a/ai_brain.py +++ b/ai_brain.py @@ -13,21 +13,25 @@ def get_system_instruction(): categories_str = " | ".join([f'"{cat}"' for cat in config.TRANSACTION_CATEGORIES]) instruction = f""" -Kamu adalah asisten AI spesialis pencatatan keuangan pribadi. Tugasmu adalah mengekstrak data dari pesan pengguna ke format JSON murni. +Kamu adalah asisten AI spesialis pencatatan keuangan pribadi. Tugasmu adalah mengekstrak data dari pesan pengguna ke format JSON MURNI dalam bentuk LIST (ARRAY). SKEMA JSON: -{{ - "tipe": "pemasukan" | "pengeluaran" | "investasi", - "item": "deskripsi singkat transaksi", - "nominal": angka integer, - "kategori": {categories_str} -}} +[ + {{ + "tipe": "pemasukan" | "pengeluaran" | "investasi", + "item": "deskripsi singkat transaksi", + "nominal": angka integer, + "kategori": {categories_str} + }}, + ... +] ATURAN KETAT: -1. Konversi singkatan angka: "k" / "rb" = 1000, "jt" / "juta" = 1000000. -2. Jika nominal dalam teks (misal: "setengah juta", "seratus ribu"), ubah ke angka integer (misal: 500000, 100000). -3. Fokus pada uang masuk, keluar, atau investasi. -4. Jawaban HANYA berupa JSON murni tanpa teks pengantar. Jika error, kembalikan {{"error": true}}. +1. Gunakan format LIST [ ... ] meskipun hanya ada satu transaksi. +2. Konversi singkatan angka: "k" / "rb" = 1000, "jt" / "juta" = 1000000. +3. Jika nominal dalam teks (misal: "setengah juta", "seratus ribu"), ubah ke angka integer (misal: 500000, 100000). +4. Fokus pada uang masuk, keluar, atau investasi. +5. Jawaban HANYA berupa JSON murni tanpa teks pengantar. Jika tidak ada transaksi sama sekali, kembalikan [ {{ "error": true }} ]. """ return instruction @@ -123,11 +127,17 @@ def extract_json_from_text(text: str) -> str: return text.split("```")[1].split("```")[0].strip() except: pass - # 3. Cari dari kurung kurawal pertama sampai terakhir (paling robust) - start_index = text.find('{') - end_index = text.rfind('}') - if start_index != -1 and end_index != -1 and end_index > start_index: - return text[start_index:end_index+1].strip() + # 3. Cari dari kurung siku pertama sampai terakhir (untuk list) + start_array = text.find('[') + end_array = text.rfind(']') + if start_array != -1 and end_array != -1 and end_array > start_array: + return text[start_array:end_array+1].strip() + + # 4. Cari dari kurung kurawal pertama sampai terakhir (fallback jika AI mengembalikan single object) + start_obj = text.find('{') + end_obj = text.rfind('}') + if start_obj != -1 and end_obj != -1 and end_obj > start_obj: + return text[start_obj:end_obj+1].strip() return text @@ -167,12 +177,26 @@ def get_json_data_from_text(text: str) -> dict: data = json.loads(json_text) - # Validasi minimal struktur data - if not any(k in data for k in ["tipe", "nominal", "item"]): - raise ValueError("JSON berhasil diparsing tapi struktur tidak sesuai") + # Normalisasi ke format LIST jika AI mengembalikan single object + if isinstance(data, dict): + data = [data] + + if not isinstance(data, list): + raise ValueError("Format respons AI bukan merupakan list JSON") + + # Validasi minimal struktur data untuk setiap item + valid_items = [] + for item in data: + if item.get("error"): + continue + if any(k in item for k in ["tipe", "nominal", "item"]): + valid_items.append(item) + + if not valid_items and any(item.get("error") for item in data): + return {"error": True} - print(f"[ai_brain] Memanfaatkan tenaga {provider.capitalize()} berhasil!") - return data + print(f"[ai_brain] Memanfaatkan tenaga {provider.capitalize()} berhasil! ({len(valid_items)} transaksi)") + return valid_items except Exception as e: # Jika KeyError / Network Error / Rate Limit / Parsing Error dll diff --git a/handlers/nlp_message.py b/handlers/nlp_message.py index 54c2f4a..fddeaf5 100644 --- a/handlers/nlp_message.py +++ b/handlers/nlp_message.py @@ -21,43 +21,49 @@ def handle_text(message): bot.send_chat_action(message.chat.id, 'typing') # Process text with AI - data = get_json_data_from_text(text) + results = get_json_data_from_text(text) - if data.get("error"): + # Jika hasil bukan list (misal {"error": true}), handle error + if isinstance(results, dict) and results.get("error"): bot.reply_to(message, "❌ Maaf, saya tidak menangkap adanya transaksi keuangan. Pastikan formatnya jelas (contoh: 'beli makan 20rb' atau 'gajian 4 juta').") return - tipe = data.get("tipe", "").lower() - item = data.get("item", "") - nominal = float(data.get("nominal", 0)) - kategori = data.get("kategori", "") + success_count = 0 + summary_lines = [] - # Save to database based on type try: - # Sekarang hanya fokus pada pemasukan, pengeluaran, dan investasi - if tipe in ["pemasukan", "pengeluaran", "investasi"]: - last_id = database.insert_transaction(user_id, tipe, item, nominal, kategori) + for data in results: + tipe = data.get("tipe", "").lower() + item = data.get("item", "") + nominal = float(data.get("nominal", 0)) + kategori = data.get("kategori", "") - if tipe == "pemasukan": - icon = "🟢" - elif tipe == "investasi": - icon = "🔵" - else: - icon = "🔴" - - reply = ( - f"{icon} *Berhasil dicatat!*\n\n" - f"🔹 *Tipe:* {tipe.capitalize()}\n" - f"🔹 *Item:* {item}\n" - f"🔹 *Nominal:* Rp {nominal:,.0f}\n" - f"🔹 *Kategori:* {kategori.replace('_', ' ').capitalize()}\n\n" - f"🆔 *ID:* `T-{last_id}`" - ) + # Save to database based on type + try: + if tipe in ["pemasukan", "pengeluaran", "investasi"]: + last_id = database.insert_transaction(user_id, tipe, item, nominal, kategori) + + if tipe == "pemasukan": + icon = "🟢" + elif tipe == "investasi": + icon = "🔵" + else: + icon = "🔴" + + summary_lines.append( + f"{icon} *{tipe.capitalize()}:* {item} — Rp {nominal:,.0f} (`T-{last_id}`)" + ) + success_count += 1 + except Exception as e: + print(f"Error inserting transaction: {e}") + + if success_count > 0: + reply = "✅ *Berhasil mencatat " + (f"{success_count} transaksi" if success_count > 1 else "transaksi") + ":*\n\n" + reply += "\n".join(summary_lines) bot.reply_to(message, reply, parse_mode='Markdown') - else: - bot.reply_to(message, "❌ Tipe data tidak dikenali dari hasil AI. Pastikan AI mengembalikan format yang benar.") - + bot.reply_to(message, "❌ Gagal mencatat transaksi. Pastikan format pesan sudah benar.") + except Exception as e: print(f"Error in handle_text: {e}") bot.reply_to(message, "❌ Terjadi kesalahan sistem saat memproses permintaan Anda. Silakan coba lagi nanti.") \ No newline at end of file