File size: 4,030 Bytes
ef271cc
8c7fe70
 
 
ef271cc
 
8c7fe70
 
 
ef271cc
 
 
 
 
 
 
 
8c7fe70
 
ef271cc
 
 
 
 
 
 
 
8c7fe70
ef271cc
 
8c7fe70
ef271cc
8c7fe70
ef271cc
 
 
 
8c7fe70
ef271cc
 
8c7fe70
 
 
ef271cc
8c7fe70
 
 
 
ef271cc
8c7fe70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef271cc
8c7fe70
 
 
 
ef271cc
 
 
 
8c7fe70
ef271cc
8c7fe70
 
 
ef271cc
8c7fe70
ef271cc
 
 
 
 
8c7fe70
ef271cc
 
 
8c7fe70
ef271cc
 
 
 
8c7fe70
ef271cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""
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)