File size: 13,473 Bytes
3c8c274
 
 
 
af289ca
 
bc83228
 
6c134c0
3705270
3c8c274
6c134c0
bc83228
 
6c134c0
bc83228
 
 
 
 
 
6c134c0
 
 
 
 
bc83228
 
6c134c0
3c8c274
eab288c
 
3705270
6c134c0
 
 
 
 
 
 
 
 
44013a5
 
 
af289ca
 
 
 
 
 
 
44013a5
 
 
 
 
6c134c0
 
 
 
 
44013a5
6c134c0
44013a5
 
 
 
 
6c134c0
 
 
 
96d59ac
3c8c274
906da16
 
 
 
6c134c0
 
 
 
906da16
 
 
 
3c8c274
eab288c
44013a5
6c134c0
44013a5
eab288c
44013a5
 
6c134c0
44013a5
 
6c134c0
af289ca
6c134c0
af289ca
6c134c0
dd1ea60
96d59ac
dd1ea60
440a20d
 
 
0d5bd79
6c134c0
 
 
440a20d
6c134c0
 
 
 
9d94525
0d5bd79
6c134c0
 
 
440a20d
0d5bd79
 
 
6c134c0
 
 
 
 
0d5bd79
bc83228
6c134c0
 
 
bc83228
 
0d5bd79
6c134c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d5bd79
 
bc83228
6c134c0
 
96d59ac
6c134c0
 
1ca0b43
fd5dbb4
6c134c0
 
1ca0b43
 
0d5bd79
 
6c134c0
 
 
 
 
1ca0b43
 
6c134c0
 
1ca0b43
440a20d
bc83228
 
 
 
eab288c
 
6c134c0
eab288c
6c134c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44013a5
6c134c0
44013a5
6c134c0
 
 
 
 
bc83228
6c134c0
eab288c
3c8c274
 
44013a5
 
 
 
 
 
 
3c8c274
6c134c0
 
 
eab288c
44013a5
6c134c0
44013a5
eab288c
44013a5
6c134c0
44013a5
 
6c134c0
44013a5
 
6c134c0
96d59ac
6c134c0
 
 
eab288c
 
6c134c0
eab288c
6c134c0
8812f42
96d59ac
6c134c0
 
 
 
eab288c
 
6c134c0
eab288c
 
 
6c134c0
8812f42
 
6c134c0
44013a5
6c134c0
 
 
 
 
 
 
44013a5
 
 
 
 
 
6c134c0
 
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
from google.generativeai.embedding import embed_content
from google.generativeai.client import configure
from google.generativeai.generative_models import GenerativeModel
from loguru import logger
from typing import Dict, List, Optional
from google.generativeai.types import GenerationConfig, HarmCategory, HarmBlockThreshold

from .request_limit_manager import RequestLimitManager
from .utils import _safe_truncate
from .config import get_settings


class GeminiResponseError(Exception):
    """Custom exception for non-retriable Gemini response issues like safety or token limits."""

    def __init__(self, message, finish_reason=None, usage_metadata=None):
        super().__init__(message)
        self.finish_reason = finish_reason
        self.usage_metadata = usage_metadata

    def __str__(self):
        usage_str = (
            f"Prompt: {self.usage_metadata.prompt_token_count}, Candidates: {self.usage_metadata.candidates_token_count}, Total: {self.usage_metadata.total_token_count}"
            if self.usage_metadata
            else "N/A"
        )
        return f"{super().__str__()} (Finish Reason: {self.finish_reason}, Usage: {usage_str})"


class GeminiClient:
    def __init__(self):
        self.limit_manager = RequestLimitManager("gemini")
        settings = get_settings()
        num_keys = (
            len(settings.gemini_api_keys.split(",")) if settings.gemini_api_keys else 0
        )
        num_models = (
            len(settings.gemini_models.split(",")) if settings.gemini_models else 0
        )
        logger.info(
            f"[GEMINI_INIT] Limiter is considering {num_keys} API keys and {num_models} models."
        )
        self._cached_model = None
        self._cached_key = None
        self._cached_model_instance = None
        # Thêm cấu hình safety_settings để bỏ chặn các phản hồi bị coi là không an toàn
        self.safety_settings: Dict[HarmCategory, HarmBlockThreshold] = {
            HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
            HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
            HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
            HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
        }

    def _get_model_instance(self, key: str, model: str):
        """
        Cache model instance để tránh recreate mỗi lần.
        """
        if (
            self._cached_key == key
            and self._cached_model == model
            and self._cached_model_instance is not None
        ):
            return self._cached_model_instance

        # Configure và tạo model instance mới
        configure(api_key=key)
        self._cached_model_instance = GenerativeModel(model)
        self._cached_key = key
        self._cached_model = model

        logger.info(
            f"[GEMINI] Created new model instance for key={key[:5]}...{key[-5:]} model={model}"
        )
        return self._cached_model_instance  # noqa

    def _clear_cache_if_needed(self, new_key: str, new_model: str):
        """
        Chỉ clear cache khi key/model thực sự thay đổi.
        """
        if self._cached_key != new_key or self._cached_model != new_model:
            logger.info(
                f"[GEMINI] Clearing cache due to key/model change: {self._cached_key}->{new_key}, {self._cached_model}->{new_model}"
            )
            self._cached_model_instance = None
            self._cached_key = None
            self._cached_model = None

    def generate_text(self, prompt: str, **kwargs) -> str:
        last_error = None
        max_retries = 3

        for attempt in range(max_retries):
            try:
                # Lấy current key/model từ manager
                key, model = self.limit_manager.get_current_key_model()

                # Sử dụng cached model instance
                _model = self._get_model_instance(key, model)

                response = _model.generate_content(
                    prompt, safety_settings=self.safety_settings, **kwargs
                )

                # Log toàn bộ nội dung response ở mức INFO để tiện gỡ lỗi
                logger.debug(f"[GEMINI][RAW_RESPONSE] {response}")

                # --- START: Cải tiến logic xử lý response ---
                # 1. Kiểm tra response có hợp lệ không
                if not response.candidates:
                    # Lỗi này nên được coi là lỗi tạm thời, thử lại với key/model khác
                    raise ValueError(
                        "Gemini response is missing 'candidates' field. Retrying..."
                    )

                candidate = response.candidates[0]
                finish_reason_name = getattr(
                    getattr(candidate, "finish_reason", None), "name", "UNKNOWN"
                )
                # Kiểm tra xem có nội dung thực sự không
                # Sửa: Dùng getattr để tránh AttributeError nếu 'parts' không tồn tại
                has_content = bool(
                    candidate.content and getattr(candidate.content, "parts", None)
                )

                # 2. Phân loại lỗi và xử lý
                # Case 1: Lỗi nội dung không thể thử lại (SAFETY, MAX_TOKENS, etc.)
                if finish_reason_name != "STOP":
                    usage_metadata = (
                        response.usage_metadata
                        if hasattr(response, "usage_metadata")
                        else None
                    )
                    error_message = f"Gemini response finished with non-OK reason: {finish_reason_name}."
                    raise GeminiResponseError(
                        error_message,
                        finish_reason=finish_reason_name,
                        usage_metadata=usage_metadata,
                    )

                # Case 2: Lỗi có thể thử lại (STOP nhưng không có nội dung)
                if (
                    not has_content
                ):  # Tại đây, ta biết chắc chắn finish_reason_name là "STOP"
                    usage_metadata = (
                        response.usage_metadata
                        if hasattr(response, "usage_metadata")
                        else None
                    )
                    last_error = GeminiResponseError(
                        "Gemini response finished with STOP but has no content parts.",
                        finish_reason="STOP_NO_CONTENT",
                        usage_metadata=usage_metadata,
                    )
                    logger.warning(
                        f"[GEMINI] Model returned STOP with no content. Retrying with another key/model... (Attempt {attempt + 1}/{max_retries})"
                    )
                    self.limit_manager.log_request(
                        key, model, success=False, retry_delay=5
                    )
                    continue  # Thử lại vòng lặp với key/model mới

                # Case 3: Thành công (STOP và có nội dung)
                self.limit_manager.log_request(key, model, success=True)
                if hasattr(response, "usage_metadata"):
                    logger.info(
                        f"[GEMINI][USAGE] Prompt Token Count: {response.usage_metadata.prompt_token_count} - Candidate Token Count: {response.usage_metadata.candidates_token_count} - Total Token Count: {response.usage_metadata.total_token_count}"  # noqa
                    )

                try:
                    logger.debug(
                        f"[GEMINI][TEXT_RESPONSE] {_safe_truncate(response.text)}"
                    )
                    return response.text
                except ValueError as ve:
                    # Safety net: Nếu truy cập .text thất bại dù các kiểm tra trước đó đã qua,
                    # coi như đây là lỗi STOP_NO_CONTENT và ném ra để tầng trên xử lý.
                    usage_metadata = (
                        response.usage_metadata
                        if hasattr(response, "usage_metadata")
                        else None
                    )
                    raise GeminiResponseError(
                        f"Gemini response has no valid content part. Original error: {ve}",
                        finish_reason="STOP_NO_CONTENT",
                        usage_metadata=usage_metadata,
                    ) from ve
                # --- END: Cải tiến logic xử lý response ---
            except GeminiResponseError as e:
                # Lỗi nội dung, không thể retry bằng cách đổi key. Propagate lên.
                logger.error(f"[GEMINI] Non-retriable content error: {e}")
                raise e
            except Exception as e:
                import re

                msg = str(e)
                # Kiểm tra lỗi rate limit hoặc lỗi server (5xx)
                is_rate_limit = "429" in msg or "rate limit" in msg.lower()
                is_server_error = any(
                    code in msg for code in ["500", "502", "503", "504"]
                )

                if is_rate_limit or is_server_error:
                    retry_delay = 60  # Mặc định cho lỗi server
                    if is_rate_limit:
                        m = re.search(r"retry_delay.*?seconds: (\d+)", msg)
                        if m:
                            retry_delay = int(m.group(1))

                    # Log lỗi và chặn cặp key/model hiện tại trong một khoảng thời gian
                    self.limit_manager.log_request(
                        key, model, success=False, retry_delay=retry_delay
                    )

                    error_type = "Rate limit" if is_rate_limit else "Server"
                    logger.warning(
                        f"[GEMINI] {error_type} error hit, will retry with new key/model "
                        f"(attempt {attempt + 1}/{max_retries}). Error: {e}"
                    )
                    last_error = e
                    continue  # Tiếp tục vòng lặp để thử key/model mới
                else:
                    # Các lỗi khác không phải rate limit hoặc server error (vd: network timeout, invalid argument)
                    # sẽ được propagate lên để lớp llm.py/reranker.py xử lý retry với backoff.
                    logger.error(
                        f"[GEMINI] Unhandled error generating text, propagating up: {e}"
                    )
                    raise e

        raise last_error or RuntimeError("No available Gemini API key/model")

    def count_tokens(self, prompt: str) -> int:
        try:
            key, model = self.limit_manager.get_current_key_model()
            _model = self._get_model_instance(key, model)
            return _model.count_tokens(prompt).total_tokens
        except Exception as e:
            logger.error(f"[GEMINI] Error counting tokens: {e}")
            return 0

    def create_embedding(
        self, text: str, model: Optional[str] = None, task_type: str = "retrieval_query"
    ) -> list:
        last_error = None
        max_retries = 3

        for attempt in range(max_retries):
            try:
                key, default_model = self.limit_manager.get_current_key_model()

                # Ưu tiên model được truyền vào parameter, chỉ fallback về default_model nếu không có
                use_model = model if model and model.strip() else default_model

                if not use_model:
                    raise ValueError("No model specified for embedding")

                logger.debug(
                    f"[GEMINI][EMBEDDING] Using model={use_model} (requested={model}, default={default_model}), task_type={task_type}"
                )

                configure(api_key=key)
                response = embed_content(
                    model=use_model, content=text, task_type=task_type
                )

                self.limit_manager.log_request(key, use_model, success=True)
                logger.debug(
                    f"[GEMINI][EMBEDDING][RAW_RESPONSE] {response['embedding'][:10]} ..... {response['embedding'][-10:]}"
                )
                return response["embedding"]

            except Exception as e:
                import re

                msg = str(e)
                if "429" in msg or "rate limit" in msg.lower():
                    retry_delay = 60
                    m_retry = re.search(r"retry_delay.*?seconds: (\d+)", msg)
                    if m_retry:
                        retry_delay = int(m_retry.group(1))

                    # Log failure và trigger scan cho key/model mới
                    self.limit_manager.log_request(
                        key, use_model, success=False, retry_delay=retry_delay
                    )

                    logger.warning(
                        f"[GEMINI] Rate limit hit in embedding, will retry with new key/model (attempt {attempt + 1}/{max_retries})"
                    )
                    last_error = e
                    continue
                else:
                    logger.error(f"[GEMINI] Error creating embedding: {e}")
                    last_error = e
                    break

        raise last_error or RuntimeError("No available Gemini API key/model")