wiizm commited on
Commit
372ce50
·
verified ·
1 Parent(s): 9554911

Upload app\gemini_client.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app//gemini_client.py +724 -0
app//gemini_client.py ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Google Gemini API 클라이언트
3
+ """
4
+
5
+ import os
6
+ import time
7
+ import requests
8
+ import google.generativeai as genai
9
+ from google.api_core import retry
10
+ from typing import Optional, List, Dict
11
+ import functools
12
+ import json
13
+
14
+ # Gemini API 키 (환경 변수 또는 데이터베이스에서 가져오기)
15
+ def get_gemini_api_key():
16
+ """Gemini API 키 가져오기 (환경 변수 우선, 없으면 DB에서)"""
17
+ # 환경 변수에서 먼저 확인
18
+ api_key = os.getenv('GEMINI_API_KEY', '').strip()
19
+ if api_key:
20
+ print(f"[Gemini] 환경 변수에서 API 키 가져옴 (길이: {len(api_key)}자)")
21
+ return api_key
22
+
23
+ # DB에서 가져오기 (순환 참조 방지를 위해 여기서 임포트)
24
+ try:
25
+ from app.database import SystemConfig
26
+ api_key = SystemConfig.get_config('gemini_api_key', '').strip()
27
+ if api_key:
28
+ print(f"[Gemini] DB에서 API 키 가져옴 (길이: {len(api_key)}자)")
29
+ else:
30
+ print(f"[Gemini] DB에 API 키가 없거나 비어있음")
31
+ return api_key
32
+ except Exception as e:
33
+ print(f"[Gemini] DB에서 API 키 조회 실패: {e}")
34
+ return ''
35
+
36
+ GEMINI_API_KEY = get_gemini_api_key()
37
+
38
+ # 사용 가능한 Gemini 모델 목록 (최신 버전 우선)
39
+ AVAILABLE_GEMINI_MODELS = [
40
+ 'gemini-2.0-flash-exp',
41
+ 'gemini-1.5-pro',
42
+ 'gemini-1.5-flash',
43
+ 'gemini-1.5-pro-latest',
44
+ 'gemini-1.5-flash-latest',
45
+ 'gemini-pro',
46
+ 'gemini-pro-vision'
47
+ ]
48
+
49
+ class GeminiClient:
50
+ """Google Gemini API 클라이언트 클래스"""
51
+
52
+ def __init__(self, api_key: Optional[str] = None):
53
+ """Gemini 클라이언트 초기화"""
54
+ if api_key:
55
+ self.api_key = api_key
56
+ else:
57
+ # 최신 API 키 가져오기 (DB에서 동적으로)
58
+ self.api_key = get_gemini_api_key()
59
+
60
+ if not self.api_key:
61
+ print("[Gemini] 경고: GEMINI_API_KEY가 설정되지 않았습니다. 환경 변수나 관리 페이지에서 설정하세요.")
62
+ return
63
+
64
+ try:
65
+ # API 키 설정 및 타임아웃 설정 (기본 60초 -> 300초(5분)로 증가)
66
+ # 전역 타임아웃 설정
67
+ self.request_timeout = 300 # 5분(300초) 타임아웃
68
+
69
+ # 재시도 정책 설정
70
+ self.retry_policy = retry.Retry(
71
+ initial=10.0, # 초기 대기 시간 (10초)
72
+ maximum=60.0, # 최대 대기 시간 (60초)
73
+ multiplier=2.0, # 대기 시간 배수
74
+ deadline=600.0 # 전체 재시도 기간 (600초 = 10분)
75
+ )
76
+
77
+ # REST API 사용을 위한 설정
78
+ # 환경 변수를 통해 HTTP 클라이언트 타임아웃 설정
79
+ os.environ.setdefault('HTTPX_TIMEOUT', str(self.request_timeout))
80
+ os.environ.setdefault('GOOGLE_API_TIMEOUT', str(self.request_timeout))
81
+
82
+ # REST API 엔드포인트 설정 (v1 사용)
83
+ self.rest_base_url = 'https://generativelanguage.googleapis.com/v1'
84
+ self.use_rest_api = True # REST API 강제 사용
85
+
86
+ # API 키 설정 (fallback용, REST API가 실패할 경우)
87
+ try:
88
+ genai.configure(api_key=self.api_key)
89
+ print(f"[Gemini] API 키 설정 완료 (REST API 모드, 타임아웃: {self.request_timeout}초)")
90
+ except Exception as e:
91
+ print(f"[Gemini] API 키 설정 오류: {e}")
92
+
93
+ # API 키가 실제로 설정되었는지 확인
94
+ try:
95
+ # genai 모듈의 전역 API 키 확인
96
+ configured_key = getattr(genai, '_api_key', None) or getattr(genai, 'api_key', None)
97
+ if configured_key:
98
+ print(f"[Gemini] 전역 API 키 확인: 설정됨 (길이: {len(str(configured_key))}자)")
99
+ else:
100
+ print(f"[Gemini] 경고: 전역 API 키가 확인되지 않습니다. API 호출이 실패할 수 있습니다.")
101
+ except Exception as e:
102
+ print(f"[Gemini] API 키 확인 오류: {e}")
103
+
104
+ print(f"[Gemini] 재시도 정책 적용됨")
105
+ except Exception as e:
106
+ print(f"[Gemini] API 키 설정 오류: {e}")
107
+
108
+ def reload_api_key(self):
109
+ """API 키를 다시 로드 (DB에서 최신 값 가져오기)"""
110
+ self.api_key = get_gemini_api_key()
111
+ if self.api_key:
112
+ try:
113
+ genai.configure(api_key=self.api_key)
114
+ print(f"[Gemini] API 키 재로드 완료")
115
+ return True
116
+ except Exception as e:
117
+ print(f"[Gemini] API 키 재로드 오류: {e}")
118
+ return False
119
+ return False
120
+
121
+ def is_configured(self) -> bool:
122
+ """Gemini API가 제대로 설정되었는지 확인"""
123
+ return bool(self.api_key)
124
+
125
+ def get_available_models(self) -> List[str]:
126
+ """사용 가능한 Gemini 모델 목록 반환"""
127
+ if not self.is_configured():
128
+ return []
129
+
130
+ try:
131
+ # 실제 사용 가능한 모델 확인
132
+ available_models = []
133
+ for model_name in AVAILABLE_GEMINI_MODELS:
134
+ try:
135
+ model = genai.GenerativeModel(model_name)
136
+ # 모델 접근 가능 여부 확인
137
+ available_models.append(model_name)
138
+ except Exception as e:
139
+ # 모델을 찾을 수 없으면 건너뛰기
140
+ continue
141
+
142
+ # 모델을 찾지 못한 경우 기본 모델 시도
143
+ if not available_models:
144
+ try:
145
+ # 기본적으로 gemini-1.5-flash 시도
146
+ model = genai.GenerativeModel('gemini-1.5-flash')
147
+ available_models.append('gemini-1.5-flash')
148
+ except:
149
+ pass
150
+
151
+ print(f"[Gemini] 사용 가능한 모델: {available_models}")
152
+ return available_models
153
+ except Exception as e:
154
+ print(f"[Gemini] 모델 목록 조회 오류: {e}")
155
+ return []
156
+
157
+ def generate_response(self, prompt: str, model_name: str = 'gemini-1.5-flash', **kwargs) -> Dict:
158
+ """
159
+ Gemini API를 사용하여 응답 생성
160
+
161
+ Args:
162
+ prompt: 입력 프롬프트
163
+ model_name: 사용할 모델 이름
164
+ **kwargs: 추가 파라미터 (temperature, max_tokens 등)
165
+
166
+ Returns:
167
+ Dict: {'response': str, 'error': str or None}
168
+ """
169
+ if not self.is_configured():
170
+ return {
171
+ 'response': None,
172
+ 'error': 'Gemini API 키가 설정되지 않았습니다. GEMINI_API_KEY 환경 변수를 설정하세요.'
173
+ }
174
+
175
+ try:
176
+ # API 키를 항상 최신으로 다시 가져와서 설정 (DB에서 변경되었을 수 있음)
177
+ current_api_key = get_gemini_api_key()
178
+ if not current_api_key:
179
+ return {
180
+ 'response': None,
181
+ 'error': 'Gemini API 키가 설정되지 않았습니다. 관리 페이지에서 API 키를 설정하세요.'
182
+ }
183
+
184
+ # API 키가 변경되었거나 없는 경우 재설정
185
+ if not self.api_key or self.api_key != current_api_key:
186
+ self.api_key = current_api_key
187
+ genai.configure(api_key=self.api_key)
188
+ print(f"[Gemini] API 키 재설정 완료 (길이: {len(self.api_key)}자)")
189
+ else:
190
+ # API 키가 이미 설정되어 있어도 매번 재설정하여 확실히 함
191
+ genai.configure(api_key=self.api_key)
192
+
193
+ # 모델 생성
194
+ model = genai.GenerativeModel(model_name)
195
+ print(f"[Gemini] 모델 생성 완료: {model_name}")
196
+
197
+ # 생성 설정
198
+ generation_config = {
199
+ 'temperature': kwargs.get('temperature', 0.7),
200
+ 'top_p': kwargs.get('top_p', 0.95),
201
+ 'top_k': kwargs.get('top_k', 40),
202
+ 'max_output_tokens': kwargs.get('max_output_tokens', 8192),
203
+ }
204
+
205
+ # 응답 생성 (타임아웃 설정)
206
+ timeout_seconds = getattr(self, 'request_timeout', 300) # 5분 타임아웃
207
+ print(f"[Gemini] 모델 {model_name}로 응답 생성 중... (타임아웃: {timeout_seconds}초)")
208
+ print(f"[Gemini] API 키 확인: 설정됨 (길이: {len(self.api_key)}자)")
209
+ print(f"[Gemini] 프롬프트 길이: {len(prompt)}자")
210
+
211
+ # 타임아웃을 위한 시작 시간 기록
212
+ start_time = time.time()
213
+
214
+ # google-generativeai는 retry 파라미터를 지원하지 않으므로
215
+ # 재시도 정책을 수동으로 구현하여 적용
216
+ retry_policy = getattr(self, 'retry_policy', None)
217
+ print(f"[Gemini] API 호출 시작 (타임아웃: {timeout_seconds}초, 재시도 정책 적용)")
218
+
219
+ # 재시도 로직 구현 (재시도 정책 객체의 설정 사용)
220
+ # retry.Retry 객체를 생성했지만, 실제로는 그 설정값들을 직접 사용
221
+ initial_wait = 10.0 # 초기 대기 시간
222
+ max_wait = 60.0 # 최대 대기 시간
223
+ multiplier = 2.0 # 대기 시간 배수
224
+ deadline = 600.0 # 전체 재시도 기간 (10분)
225
+
226
+ wait_time = initial_wait
227
+ deadline_time = time.time() + deadline
228
+ retry_count = 0
229
+ last_error = None
230
+
231
+ while True:
232
+ try:
233
+ # API 호출 전 API 키 재확인 및 재설정
234
+ current_key = get_gemini_api_key()
235
+ if not current_key:
236
+ raise Exception("API 키가 설정되지 않았습니다. 관리 페이지에서 API 키를 설정하세요.")
237
+
238
+ # API 키가 변경되었거나 설정되지 않은 경우 재설정
239
+ if not self.api_key or self.api_key != current_key:
240
+ self.api_key = current_key.strip() if current_key else None # 공백 제거
241
+ print(f"[Gemini] API 키 재설정 완료 (길이: {len(self.api_key) if self.api_key else 0}자)")
242
+
243
+ # API 키 유효성 검사
244
+ if not self.api_key or not self.api_key.strip():
245
+ raise Exception("API 키가 비어있습니다. 관리 페이지에서 API 키를 설정하세요.")
246
+
247
+ # API 키 앞뒤 공백 제거
248
+ api_key_clean = self.api_key.strip()
249
+ if not api_key_clean:
250
+ raise Exception("API 키가 유효하지 않습니다 (공백만 포함).")
251
+
252
+ # API 키 형식 확인 (Google API 키는 보통 AIza로 시작)
253
+ if not api_key_clean.startswith('AIza'):
254
+ print(f"[Gemini] 경고: API 키가 일반적인 Google API 키 형식이 아닙니다 (AIza로 시작하지 않음)")
255
+
256
+ # API 키 길이 확인 (일반적으로 39자 이상)
257
+ if len(api_key_clean) < 20:
258
+ raise Exception(f"API 키 길이가 너무 짧습니다 ({len(api_key_clean)}자). 올바른 API 키인지 확인하세요.")
259
+
260
+ print(f"[Gemini] API 키 검증 완료 (길이: {len(api_key_clean)}자, 시작: {api_key_clean[:10]}..., 끝: ...{api_key_clean[-5:]})")
261
+
262
+ # REST API를 직접 사용하여 호출
263
+ use_rest = getattr(self, 'use_rest_api', True)
264
+ if use_rest:
265
+ print(f"[Gemini] REST API 직접 호출 모드")
266
+
267
+ # API 버전 및 모델 이름 정규화
268
+ # 모델 이름에서 'gemini:' 접두사 제거 및 정규화
269
+ model_name_clean = model_name.strip()
270
+ if ':' in model_name_clean:
271
+ # "gemini:gemini-1.5-flash" 형식인 경우
272
+ model_name_clean = model_name_clean.split(':', 1)[1].strip()
273
+ elif model_name_clean.startswith('gemini-'):
274
+ # "gemini-1.5-flash" 형식인 경우 그대로 사용
275
+ pass
276
+
277
+ # REST API 베이스 URL (v1 사용)
278
+ rest_base_url = 'https://generativelanguage.googleapis.com/v1'
279
+ url = f"{rest_base_url}/models/{model_name_clean}:generateContent"
280
+
281
+ print(f"[Gemini] - API 버전: v1")
282
+ print(f"[Gemini] - 원본 모델 이름: {model_name}")
283
+ print(f"[Gemini] - 정규화된 모델 이름: {model_name_clean}")
284
+ print(f"[Gemini] - 전체 URL: {url}")
285
+
286
+ # REST API 요청 본문 구성
287
+ request_body = {
288
+ "contents": [{
289
+ "parts": [{
290
+ "text": prompt
291
+ }]
292
+ }],
293
+ "generationConfig": generation_config
294
+ }
295
+
296
+ # REST API 헤더 (API 키를 헤더로 전달 시도)
297
+ headers = {
298
+ "Content-Type": "application/json",
299
+ "x-goog-api-key": api_key_clean
300
+ }
301
+
302
+ print(f"[Gemini] REST API 호출 전송 중...")
303
+ print(f"[Gemini] - URL: {url}")
304
+ print(f"[Gemini] - 모델: {model_name}")
305
+ print(f"[Gemini] - API 키: 설정됨 (길이: {len(api_key_clean)}자)")
306
+ print(f"[Gemini] - API 키 시작: {api_key_clean[:15]}...")
307
+ print(f"[Gemini] - API 키 끝: ...{api_key_clean[-10:]}")
308
+ print(f"[Gemini] - 프롬프트 길이: {len(prompt)}자")
309
+
310
+ # REST API 호출 (API 키를 헤더와 params 양쪽으로 전달 시도)
311
+ # Google Gemini API는 헤더 또는 params 중 하나만 필요하지만, 양쪽 모두 시도
312
+ api_params = {"key": api_key_clean}
313
+ print(f"[Gemini] - API 키 전달 방식: 헤더 (x-goog-api-key) 및 파라미터 (key)")
314
+
315
+ rest_response = requests.post(
316
+ url,
317
+ headers=headers,
318
+ json=request_body,
319
+ params=api_params,
320
+ timeout=timeout_seconds
321
+ )
322
+
323
+ # 요청 URL에서 API 키 부분만 제거하여 로깅 (보안)
324
+ request_url = rest_response.request.url
325
+ if 'key=' in request_url:
326
+ # API 키 부분을 마스킹
327
+ from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
328
+ parsed = urlparse(request_url)
329
+ params = parse_qs(parsed.query)
330
+ if 'key' in params:
331
+ masked_params = params.copy()
332
+ masked_key = masked_params['key'][0][:10] + '...' + masked_params['key'][0][-5:]
333
+ masked_params['key'] = [masked_key]
334
+ masked_query = urlencode(masked_params, doseq=True)
335
+ masked_url = urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, masked_query, parsed.fragment))
336
+ print(f"[Gemini] - 요청 URL (마스킹됨): {masked_url[:150]}...")
337
+ else:
338
+ print(f"[Gemini] - 요청 URL: {request_url[:150]}...")
339
+
340
+ print(f"[Gemini] REST API 응답 상태 코드: {rest_response.status_code}")
341
+
342
+ # 응답 본문 확인 (상태 코드가 200이어도 에러가 있을 수 있음)
343
+ response_has_error = False
344
+ try:
345
+ response_data_check = rest_response.json()
346
+ # 응답에 error 필드가 있는지 확인
347
+ if 'error' in response_data_check:
348
+ response_has_error = True
349
+ error_info = response_data_check['error']
350
+ error_code = error_info.get('code', rest_response.status_code)
351
+ error_message = error_info.get('message', '알 수 없는 오류')
352
+ print(f"[Gemini] 응답 본문에 에러 감지: code={error_code}, message={error_message}")
353
+
354
+ # 에러 코드에 따라 처리
355
+ if error_code == 404:
356
+ # 404 오류인 경우 v1beta로 재시도
357
+ print(f"[Gemini] v1에서 모델을 찾을 수 없음, v1beta로 재시도...")
358
+ rest_base_url_v1beta = 'https://generativelanguage.googleapis.com/v1beta'
359
+ url_v1beta = f"{rest_base_url_v1beta}/models/{model_name_clean}:generateContent"
360
+
361
+ rest_response = requests.post(
362
+ url_v1beta,
363
+ headers=headers,
364
+ json=request_body,
365
+ params=api_params,
366
+ timeout=timeout_seconds
367
+ )
368
+
369
+ print(f"[Gemini] v1beta REST API 응답 상태 코드: {rest_response.status_code}")
370
+
371
+ # v1beta 응답도 확인
372
+ if rest_response.status_code == 200:
373
+ response_data_check_v1beta = rest_response.json()
374
+ if 'error' in response_data_check_v1beta:
375
+ # v1beta에서도 에러 발생
376
+ error_info_v1beta = response_data_check_v1beta['error']
377
+ error_code_v1beta = error_info_v1beta.get('code', 404)
378
+ error_message_v1beta = error_info_v1beta.get('message', '알 수 없는 오류')
379
+
380
+ # 모델 목록 조회 시도
381
+ available_models_str = "확인 불가"
382
+ try:
383
+ list_models_url_v1 = f"{rest_base_url}/models"
384
+ list_response = requests.get(
385
+ list_models_url_v1,
386
+ headers={"x-goog-api-key": api_key_clean},
387
+ params={"key": api_key_clean},
388
+ timeout=10
389
+ )
390
+ if list_response.status_code == 200:
391
+ models_data = list_response.json()
392
+ available_models = []
393
+ for m in models_data.get('models', []):
394
+ model_name_full = m.get('name', '')
395
+ if '/' in model_name_full:
396
+ model_name_short = model_name_full.split('/')[-1]
397
+ else:
398
+ model_name_short = model_name_full
399
+ # generateContent를 지원하는 모델만 필터링
400
+ supported_methods = m.get('supportedGenerationMethods', [])
401
+ if 'generateContent' in supported_methods:
402
+ available_models.append(model_name_short)
403
+ available_models_str = ', '.join(available_models[:10]) if available_models else '없음'
404
+ print(f"[Gemini] 사용 가능한 모델 목록 (v1): {available_models[:10]}")
405
+ except Exception as list_error:
406
+ print(f"[Gemini] 모델 목록 조회 실패: {list_error}")
407
+
408
+ error_text_v1beta = json.dumps(error_info_v1beta)
409
+ raise Exception(f"REST API 오류 {error_code_v1beta}: {error_text_v1beta}\n사용 가능한 모델: {available_models_str}")
410
+ else:
411
+ # v1beta에서 성공
412
+ response_has_error = False
413
+ print(f"[Gemini] v1beta에서 정상 응답 받음")
414
+ elif rest_response.status_code != 200:
415
+ error_text_v1beta = rest_response.text[:1000] if rest_response.text else '상세 정보 없음'
416
+ raise Exception(f"REST API 오류 {rest_response.status_code}: {error_text_v1beta}")
417
+ elif error_code == 429:
418
+ # 429 오류: 할당량 초과 (재시도 불가능)
419
+ print(f"[Gemini] ❌ 할당량 초과 오류 (429) 감지")
420
+ print(f"[Gemini] 재시도 불가능한 오류: REST API 오류 {error_code}: {json.dumps(error_info)}")
421
+
422
+ # 에러 메시지에서 권장 모델 추출
423
+ recommended_model = None
424
+ if 'gemini-2.0-flash' in error_message.lower() or 'gemini 2.0' in error_message.lower():
425
+ recommended_model = "gemini-2.0-flash-exp"
426
+
427
+ # 사용자 친화적인 에러 메시지 생성
428
+ quota_error_msg = f"""Gemini API 할당량 초과 (429)
429
+
430
+ 현재 사용 중인 모델 '{model_name_clean}'의 일일 요청 한도를 초과했습니다.
431
+
432
+ 해결 방법:
433
+ 1. 내일까지 대기 (할당량이 자동으로 재설정됩니다)
434
+ 2. 다른 Gemini 모델로 변경:
435
+ - gemini-2.0-flash-exp (더 높은 할당량 제공)
436
+ - gemini-1.5-pro
437
+ - gemini-1.5-flash
438
+ 3. Google AI Studio에서 할당량 확인:
439
+ https://aistudio.google.com/app/apikey
440
+
441
+ 상세 오류: {error_message[:200]}"""
442
+
443
+ if recommended_model:
444
+ quota_error_msg += f"\n\n권장: {recommended_model} 모델로 변경을 고려해보세요."
445
+
446
+ raise Exception(quota_error_msg)
447
+ else:
448
+ # 404, 429가 아닌 다른 에러
449
+ error_text = json.dumps(error_info)
450
+ raise Exception(f"REST API 오류 {error_code}: {error_text}")
451
+ except ValueError:
452
+ # JSON 파싱 실패
453
+ pass
454
+
455
+ # 이미 에러가 처리되지 않은 경우에만 추가 에러 처리
456
+ # (response_has_error가 True면 이미 위에서 처리되었거나 Exception이 발생했을 것)
457
+ if rest_response.status_code != 200 and not response_has_error:
458
+ error_text = rest_response.text[:1000] if rest_response.text else '상세 정보 없음'
459
+
460
+ # API 키 오류인 경우 더 상세한 안내 제공
461
+ if rest_response.status_code == 400 and ('API key' in error_text or 'API_KEY' in error_text):
462
+ print(f"[Gemini] ❌ API 키 오류 감지")
463
+ print(f"[Gemini] 현재 API 키 정보:")
464
+ print(f"[Gemini] - 길이: {len(api_key_clean)}자")
465
+ print(f"[Gemini] - 시작: {api_key_clean[:15]}...")
466
+ print(f"[Gemini] - 끝: ...{api_key_clean[-10:]}")
467
+ print(f"[Gemini] - 형식 확인: {'AIza로 시작' if api_key_clean.startswith('AIza') else 'AIza로 시작하지 않음 (비정상)'}")
468
+ raise Exception(f"""REST API 오류 400: API 키가 유효하지 않습니다.
469
+
470
+ 확인 사항:
471
+ 1. 관리 페이지에서 API 키가 올바르게 설정되었는지 확인하세요.
472
+ 2. Google AI Studio (https://aistudio.google.com/app/apikey)에서 API 키가 활성화되어 있는지 확인하세요.
473
+ 3. API 키 형식이 올바른지 확인하세요 (일반적으로 'AIza'로 시작).
474
+ 4. API 키에 불필요한 공백이나 줄바꿈이 포함되지 않았는지 확인하세요.
475
+
476
+ 오류 상세: {error_text[:300]}""")
477
+
478
+ raise Exception(f"REST API 오류 {rest_response.status_code}: {error_text}")
479
+
480
+ # 에러가 있었지만 처리되지 않은 경우 (정상 응답이 아님)
481
+ if response_has_error:
482
+ # 이미 위에서 Exception이 발생했어야 하지만, 혹시 모르니 확인
483
+ error_text = rest_response.text[:1000] if rest_response.text else '상세 정보 없음'
484
+ raise Exception(f"REST API 오류: 응답에 에러가 포함되어 있습니다. {error_text}")
485
+
486
+ # REST API 응답 파싱
487
+ response_data = rest_response.json()
488
+
489
+ # 토큰 사용량 정보 추출
490
+ input_tokens = None
491
+ output_tokens = None
492
+ if 'usageMetadata' in response_data:
493
+ usage = response_data['usageMetadata']
494
+ input_tokens = usage.get('promptTokenCount')
495
+ output_tokens = usage.get('candidatesTokenCount')
496
+ total_tokens = usage.get('totalTokenCount')
497
+ print(f"[Gemini] 토큰 사용량: 입력={input_tokens}, 출력={output_tokens}, 총={total_tokens}")
498
+
499
+ # 응답에서 텍스트 추출
500
+ if 'candidates' in response_data and len(response_data['candidates']) > 0:
501
+ candidate = response_data['candidates'][0]
502
+ if 'content' in candidate and 'parts' in candidate['content']:
503
+ parts = candidate['content']['parts']
504
+ if len(parts) > 0 and 'text' in parts[0]:
505
+ response_text = parts[0]['text']
506
+ print(f"[Gemini] REST API 응답 수신 성공 (길이: {len(response_text)}자)")
507
+
508
+ # genai 라이브러리 형식으로 변환 (호환성을 위해)
509
+ class MockResponse:
510
+ def __init__(self, text, input_tokens=None, output_tokens=None):
511
+ self.text = text
512
+ self.input_tokens = input_tokens
513
+ self.output_tokens = output_tokens
514
+
515
+ response = MockResponse(response_text, input_tokens, output_tokens)
516
+ break
517
+ else:
518
+ raise Exception("REST API 응답에 텍스트가 없습니다.")
519
+ else:
520
+ raise Exception("REST API 응답 형식이 올바르지 않습니다.")
521
+ else:
522
+ raise Exception("REST API 응답에 candidates가 없습니다.")
523
+ else:
524
+ # 기존 genai 라이브러리 사용 (fallback)
525
+ genai.configure(api_key=self.api_key)
526
+ print(f"[Gemini] genai 라이브러리 사용 (fallback)")
527
+ response = model.generate_content(
528
+ prompt,
529
+ generation_config=generation_config
530
+ )
531
+ print(f"[Gemini] Gemini API 응답 수신 성공")
532
+ break
533
+ # 성공 시 루프 종료
534
+ if retry_count > 0:
535
+ print(f"[Gemini] 재시도 성공 (총 {retry_count}회 재시도)")
536
+ break
537
+ except Exception as e:
538
+ last_error = e
539
+ error_str = str(e).lower()
540
+
541
+ # 재시도 가능한 오류인지 확인 (타임아웃, 네트워크 오류 등)
542
+ # 429(할당량 초과), 400(잘못된 요청), 401(인증 실패), 403(권한 없음)은 재시도 불가
543
+ non_retryable_errors = ['429', 'quota', 'exceeded', '400', '401', '403', 'api key', 'invalid', 'unauthorized', 'forbidden']
544
+ is_non_retryable = any(err in error_str for err in non_retryable_errors)
545
+
546
+ retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
547
+ is_retryable = any(err in error_str for err in retryable_errors) and not is_non_retryable
548
+
549
+ # deadline 확인
550
+ if time.time() >= deadline_time:
551
+ print(f"[Gemini] 재시도 deadline 초과 ({deadline}초), 마지막 오류 반환")
552
+ raise
553
+
554
+ if is_retryable:
555
+ retry_count += 1
556
+ print(f"[Gemini] 재시도 {retry_count} - {wait_time:.1f}초 후 재시도 (오류: {str(e)[:100]})")
557
+ time.sleep(wait_time)
558
+ wait_time = min(wait_time * multiplier, max_wait) # 배수로 증가, 최대값 제한
559
+ else:
560
+ # 재시도 불가능한 오류
561
+ if is_non_retryable:
562
+ print(f"[Gemini] 재시도 불가능한 오류 (할당량/인증 오류): {str(e)[:200]}")
563
+ else:
564
+ print(f"[Gemini] 재시도 불가능한 오류: {str(e)[:100]}")
565
+ raise
566
+
567
+ # 응답 시간 확인
568
+ elapsed_time = time.time() - start_time
569
+ print(f"[Gemini] 응답 수신 완료 (경과 시간: {elapsed_time:.2f}초)")
570
+
571
+ # 응답 텍스트 추출
572
+ response_text = response.text if hasattr(response, 'text') else str(response)
573
+
574
+ # 토큰 정보 추출
575
+ input_tokens = getattr(response, 'input_tokens', None)
576
+ output_tokens = getattr(response, 'output_tokens', None)
577
+
578
+ print(f"[Gemini] 응답 생성 완료: {len(response_text)}자, 입력 토큰: {input_tokens}, 출력 토큰: {output_tokens}")
579
+ return {
580
+ 'response': response_text,
581
+ 'error': None,
582
+ 'input_tokens': input_tokens,
583
+ 'output_tokens': output_tokens
584
+ }
585
+
586
+ except Exception as e:
587
+ error_msg = f'Gemini API 오류: {str(e)}'
588
+ print(f"[Gemini] {error_msg}")
589
+ return {
590
+ 'response': None,
591
+ 'error': error_msg
592
+ }
593
+
594
+ def generate_chat_response(self, messages: List[Dict], model_name: str = 'gemini-1.5-flash', **kwargs) -> Dict:
595
+ """
596
+ Gemini API를 사용하여 채팅 응답 생성
597
+
598
+ Args:
599
+ messages: 메시지 리스트 [{'role': 'user', 'content': '...'}, ...]
600
+ model_name: 사용할 모델 이름
601
+ **kwargs: 추가 파라미터
602
+
603
+ Returns:
604
+ Dict: {'response': str, 'error': str or None}
605
+ """
606
+ if not self.is_configured():
607
+ return {
608
+ 'response': None,
609
+ 'error': 'Gemini API 키가 설정되지 않았습니다. GEMINI_API_KEY 환경 변수를 설정하세요.'
610
+ }
611
+
612
+ try:
613
+ # 모델 생성
614
+ model = genai.GenerativeModel(model_name)
615
+
616
+ # 채팅 세션 시작
617
+ chat = model.start_chat(history=[])
618
+
619
+ # 이전 대화 내역 추가 (user와 assistant 메시지)
620
+ for msg in messages[:-1]: # 마지막 메시지 제외
621
+ if msg['role'] == 'user':
622
+ chat.send_message(msg['content'])
623
+ elif msg['role'] == 'assistant' or msg['role'] == 'ai':
624
+ # Gemini는 사용자 메시지만 직접 보내므로, assistant 메시지는 히스토리로 처리하지 않음
625
+ pass
626
+
627
+ # 마지막 사용자 메시지로 응답 생성 (타임아웃 설정)
628
+ last_message = messages[-1] if messages else {'content': ''}
629
+ timeout_seconds = getattr(self, 'timeout', 600) # 5분 타임아웃
630
+ print(f"[Gemini] 채팅 응답 생성 중... (타임아웃: {timeout_seconds}초)")
631
+
632
+ # 타임아웃을 위한 시작 시간 기록
633
+ start_time = time.time()
634
+
635
+ # google-generativeai는 retry 파라미터를 지원하지 않으므로
636
+ # 재시도 정책을 수동으로 구현하여 적용
637
+ print(f"[Gemini] 채팅 API 호출 시작 (타임아웃: {timeout_seconds}초, 재시도 정책 적용)")
638
+
639
+ # 재시도 로직 구현 (재시도 정책 객체의 설정 사용)
640
+ initial_wait = 10.0 # 초기 대기 시간
641
+ max_wait = 60.0 # 최대 대기 시간
642
+ multiplier = 2.0 # 대기 시간 배수
643
+ deadline = 600.0 # 전체 재시도 기간 (10분)
644
+
645
+ wait_time = initial_wait
646
+ deadline_time = time.time() + deadline
647
+ retry_count = 0
648
+ last_error = None
649
+
650
+ while True:
651
+ try:
652
+ response = chat.send_message(last_message['content'])
653
+ # 성공 시 루프 종료
654
+ if retry_count > 0:
655
+ print(f"[Gemini] 채팅 재시도 성공 (총 {retry_count}회 재시도)")
656
+ break
657
+ except Exception as e:
658
+ last_error = e
659
+ error_str = str(e).lower()
660
+
661
+ # 재시도 가능한 오류인지 확인 (타임아웃, 네트워크 오류 등)
662
+ # 429(할당량 초과), 400(잘못된 요청), 401(인증 실패), 403(권한 없음)은 재시도 불가
663
+ non_retryable_errors = ['429', 'quota', 'exceeded', '400', '401', '403', 'api key', 'invalid', 'unauthorized', 'forbidden']
664
+ is_non_retryable = any(err in error_str for err in non_retryable_errors)
665
+
666
+ retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
667
+ is_retryable = any(err in error_str for err in retryable_errors) and not is_non_retryable
668
+
669
+ # deadline 확인
670
+ if time.time() >= deadline_time:
671
+ print(f"[Gemini] 채팅 재시도 deadline 초과 ({deadline}초), 마지막 오류 반환")
672
+ raise
673
+
674
+ if is_retryable:
675
+ retry_count += 1
676
+ print(f"[Gemini] 채팅 재시도 {retry_count} - {wait_time:.1f}초 후 재시도 (오류: {str(e)[:100]})")
677
+ time.sleep(wait_time)
678
+ wait_time = min(wait_time * multiplier, max_wait) # 배수로 증가, 최대값 제한
679
+ else:
680
+ # 재시도 불가능한 오류
681
+ if is_non_retryable:
682
+ print(f"[Gemini] 채팅 재시도 불가능한 오류 (할당량/인증 오류): {str(e)[:200]}")
683
+ else:
684
+ print(f"[Gemini] 채팅 재시도 불가능한 오류: {str(e)[:100]}")
685
+ raise
686
+
687
+ elapsed_time = time.time() - start_time
688
+ print(f"[Gemini] 채팅 응답 수신 완료 (경과 시간: {elapsed_time:.2f}초)")
689
+
690
+ response_text = response.text if hasattr(response, 'text') else str(response)
691
+
692
+ print(f"[Gemini] 채팅 응답 생성 완료: {len(response_text)}자")
693
+ return {
694
+ 'response': response_text,
695
+ 'error': None
696
+ }
697
+
698
+ except Exception as e:
699
+ error_msg = f'Gemini API 오류: {str(e)}'
700
+ print(f"[Gemini] {error_msg}")
701
+ return {
702
+ 'response': None,
703
+ 'error': error_msg
704
+ }
705
+
706
+ # 전역 Gemini ��라이언트 인스턴스
707
+ _gemini_client = None
708
+
709
+ def get_gemini_client() -> GeminiClient:
710
+ """Gemini 클라이언트 싱글톤 인스턴스 반환"""
711
+ global _gemini_client
712
+ if _gemini_client is None:
713
+ _gemini_client = GeminiClient()
714
+ else:
715
+ # API 키가 변경되었을 수 있으므로 재로드 시도
716
+ _gemini_client.reload_api_key()
717
+ return _gemini_client
718
+
719
+ def reset_gemini_client():
720
+ """Gemini 클라이언트 리셋 (API 키 변경 후 호출)"""
721
+ global _gemini_client
722
+ _gemini_client = None
723
+ return get_gemini_client()
724
+