gemini-claw / src /gemini_client.py
Ricky01anjay's picture
Upload files using @huggingface/hub
8c7fe70 verified
"""
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)