""" gemini_client.py — API client menggunakan PuruBoy NoteGPT API (SSE streaming). Endpoint: https://puruboy-api.vercel.app/api/ai/notegpt Payload : {"prompt": ""} Format SSE: data: {"text": "..."} → akumulasi teks data: {"text": "", "done": true} → sinyal hampir selesai (mungkin ada teks terakhir) data: {"type": "finish"} → akhir stream sesungguhnya """ from __future__ import annotations import json import time import urllib.request import urllib.error GEMINI_API_URL = "https://puruboy-api.vercel.app/api/ai/notegpt" DEFAULT_MODEL = "notegpt" class GeminiAPIError(Exception): pass def _parse_sse_response(resp) -> str: """ Baca SSE stream, akumulasi semua chunk text. Validasi: - Jika done/finish diterima tapi teks kosong → raise GeminiAPIError (trigger retry). """ accumulated = [] for raw_line in resp: line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n") if not line.startswith("data:"): continue data_str = line[len("data:"):].strip() if not data_str: continue try: event = json.loads(data_str) except json.JSONDecodeError: continue # Akumulasi teks text_chunk = event.get("text", "") if text_chunk: accumulated.append(text_chunk) # Cek sinyal selesai is_done = event.get("done", False) is_finish = event.get("type") == "finish" if is_done or is_finish: if not accumulated: raise GeminiAPIError( "API mengirim sinyal selesai tapi tidak ada konten teks " f"({'done=true' if is_done else 'type=finish'})" ) if is_finish: break result = "".join(accumulated).strip() if not result: raise GeminiAPIError("Respons API kosong (tidak ada teks terakumulasi)") return result def call_gemini(prompt: str, model: str = DEFAULT_MODEL) -> str: """ Kirim prompt ke PuruBoy NoteGPT API (SSE) dengan Exponential Backoff. Retry jika: - Error jaringan / HTTP error - Respons kosong atau done/finish tanpa konten """ payload = json.dumps({"prompt": prompt}).encode("utf-8") max_retries = 5 delay = 4 for attempt in range(max_retries): req = urllib.request.Request( GEMINI_API_URL, data=payload, headers={ "Content-Type": "application/json", "User-Agent": "GeminiClaw/3.0", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=90) as resp: result = _parse_sse_response(resp) return result except GeminiAPIError as e: if attempt == max_retries - 1: raise GeminiAPIError(f"Gagal setelah {max_retries} percobaan: {e}") print(f"[Percobaan {attempt+1}] Gagal: {e}. Retry dalam {delay}s...") time.sleep(delay) delay = min(delay * 2, 60) except urllib.error.HTTPError as e: err_body = "" try: err_body = e.read().decode("utf-8", errors="replace")[:200] except Exception: pass msg = f"HTTP {e.code}: {err_body}" if attempt == max_retries - 1: raise GeminiAPIError(f"Gagal setelah {max_retries} percobaan: {msg}") print(f"[Percobaan {attempt+1}] {msg}. Retry dalam {delay}s...") time.sleep(delay) delay = min(delay * 2, 60) except urllib.error.URLError as e: msg = str(e.reason) if attempt == max_retries - 1: raise GeminiAPIError(f"Gagal setelah {max_retries} percobaan: {msg}") print(f"[Percobaan {attempt+1}] URLError: {msg}. Retry dalam {delay}s...") time.sleep(delay) delay = min(delay * 2, 60)