Spaces:
Paused
Paused
| """ | |
| gemini_client.py β API client menggunakan PuruBoy NoteGPT API (SSE streaming). | |
| Endpoint: https://puruboy-api.vercel.app/api/ai/notegpt | |
| Payload : {"prompt": "<teks>"} | |
| 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) | |