SOY NV AI commited on
Commit
665bcdc
Β·
1 Parent(s): fa87e9c

Add Gemini API integration with REST API support, improve error handling, and add markdown bold formatting for messages

Browse files
app/database.py CHANGED
@@ -155,4 +155,63 @@ class ParentChunk(db.Model):
155
  'updated_at': self.updated_at.isoformat() if self.updated_at else None
156
  }
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
 
155
  'updated_at': self.updated_at.isoformat() if self.updated_at else None
156
  }
157
 
158
+ # μ‹œμŠ€ν…œ μ„€μ • λͺ¨λΈ (API ν‚€ λ“±)
159
+ class SystemConfig(db.Model):
160
+ id = db.Column(db.Integer, primary_key=True)
161
+ key = db.Column(db.String(100), unique=True, nullable=False) # μ„€μ • ν‚€ (예: 'gemini_api_key')
162
+ value = db.Column(db.Text, nullable=True) # μ„€μ • κ°’ (μ•”ν˜Έν™”λœ API ν‚€)
163
+ description = db.Column(db.String(255), nullable=True) # μ„€μ • μ„€λͺ…
164
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
165
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
166
+
167
+ def to_dict(self):
168
+ return {
169
+ 'id': self.id,
170
+ 'key': self.key,
171
+ 'value': self.value, # μ‹€μ œ κ°’ λ°˜ν™˜ (λ³΄μ•ˆμ„ μœ„ν•΄ λ§ˆμŠ€ν‚Ή ν•„μš”ν•  수 있음)
172
+ 'description': self.description,
173
+ 'created_at': self.created_at.isoformat() if self.created_at else None,
174
+ 'updated_at': self.updated_at.isoformat() if self.updated_at else None
175
+ }
176
+
177
+ @staticmethod
178
+ def get_config(key, default=None):
179
+ """μ„€μ • κ°’ κ°€μ Έμ˜€κΈ°"""
180
+ try:
181
+ config = SystemConfig.query.filter_by(key=key).first()
182
+ return config.value if config else default
183
+ except Exception as e:
184
+ # ν…Œμ΄λΈ”μ΄ μ—†κ±°λ‚˜ 였λ₯˜ λ°œμƒ μ‹œ κΈ°λ³Έκ°’ λ°˜ν™˜
185
+ print(f"[SystemConfig.get_config] 였λ₯˜: {e}")
186
+ return default
187
+
188
+ @staticmethod
189
+ def set_config(key, value, description=None):
190
+ """μ„€μ • κ°’ μ €μž₯/μ—…λ°μ΄νŠΈ"""
191
+ try:
192
+ # ν…Œμ΄λΈ”μ΄ μ—†μœΌλ©΄ 생성 μ‹œλ„
193
+ from sqlalchemy import inspect
194
+ inspector = inspect(db.engine)
195
+ if 'system_config' not in inspector.get_table_names():
196
+ print("[SystemConfig.set_config] SystemConfig ν…Œμ΄λΈ” 생성 쀑...")
197
+ db.create_all()
198
+
199
+ config = SystemConfig.query.filter_by(key=key).first()
200
+ if config:
201
+ config.value = value
202
+ if description:
203
+ config.description = description
204
+ config.updated_at = datetime.utcnow()
205
+ else:
206
+ config = SystemConfig(key=key, value=value, description=description)
207
+ db.session.add(config)
208
+ db.session.commit()
209
+ return config
210
+ except Exception as e:
211
+ db.session.rollback()
212
+ print(f"[SystemConfig.set_config] 였λ₯˜: {e}")
213
+ import traceback
214
+ traceback.print_exc()
215
+ raise
216
+
217
 
app/gemini_client.py ADDED
@@ -0,0 +1,662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ else:
418
+ # 404κ°€ μ•„λ‹Œ λ‹€λ₯Έ μ—λŸ¬
419
+ error_text = json.dumps(error_info)
420
+ raise Exception(f"REST API 였λ₯˜ {error_code}: {error_text}")
421
+ except ValueError:
422
+ # JSON νŒŒμ‹± μ‹€νŒ¨
423
+ pass
424
+
425
+ # 이미 μ—λŸ¬κ°€ μ²˜λ¦¬λ˜μ§€ μ•Šμ€ κ²½μš°μ—λ§Œ μΆ”κ°€ μ—λŸ¬ 처리
426
+ # (response_has_errorκ°€ Trueλ©΄ 이미 μœ„μ—μ„œ μ²˜λ¦¬λ˜μ—ˆκ±°λ‚˜ Exception이 λ°œμƒν–ˆμ„ 것)
427
+ if rest_response.status_code != 200 and not response_has_error:
428
+ error_text = rest_response.text[:1000] if rest_response.text else '상세 정보 μ—†μŒ'
429
+
430
+ # API ν‚€ 였λ₯˜μΈ 경우 더 μƒμ„Έν•œ μ•ˆλ‚΄ 제곡
431
+ if rest_response.status_code == 400 and ('API key' in error_text or 'API_KEY' in error_text):
432
+ print(f"[Gemini] ❌ API ν‚€ 였λ₯˜ 감지")
433
+ print(f"[Gemini] ν˜„μž¬ API ν‚€ 정보:")
434
+ print(f"[Gemini] - 길이: {len(api_key_clean)}자")
435
+ print(f"[Gemini] - μ‹œμž‘: {api_key_clean[:15]}...")
436
+ print(f"[Gemini] - 끝: ...{api_key_clean[-10:]}")
437
+ print(f"[Gemini] - ν˜•μ‹ 확인: {'AIza둜 μ‹œμž‘' if api_key_clean.startswith('AIza') else 'AIza둜 μ‹œμž‘ν•˜μ§€ μ•ŠμŒ (비정상)'}")
438
+ raise Exception(f"""REST API 였λ₯˜ 400: API ν‚€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
439
+
440
+ 확인 사항:
441
+ 1. 관리 νŽ˜μ΄μ§€μ—μ„œ API ν‚€κ°€ μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.
442
+ 2. Google AI Studio (https://aistudio.google.com/app/apikey)μ—μ„œ API ν‚€κ°€ ν™œμ„±ν™”λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.
443
+ 3. API ν‚€ ν˜•μ‹μ΄ μ˜¬λ°”λ₯Έμ§€ ν™•μΈν•˜μ„Έμš” (일반적으둜 'AIza'둜 μ‹œμž‘).
444
+ 4. API 킀에 λΆˆν•„μš”ν•œ κ³΅λ°±μ΄λ‚˜ μ€„λ°”κΏˆμ΄ ν¬ν•¨λ˜μ§€ μ•Šμ•˜λŠ”μ§€ ν™•μΈν•˜μ„Έμš”.
445
+
446
+ 였λ₯˜ 상세: {error_text[:300]}""")
447
+
448
+ raise Exception(f"REST API 였λ₯˜ {rest_response.status_code}: {error_text}")
449
+
450
+ # μ—λŸ¬κ°€ μžˆμ—ˆμ§€λ§Œ μ²˜λ¦¬λ˜μ§€ μ•Šμ€ 경우 (정상 응닡이 μ•„λ‹˜)
451
+ if response_has_error:
452
+ # 이미 μœ„μ—μ„œ Exception이 λ°œμƒν–ˆμ–΄μ•Ό ν•˜μ§€λ§Œ, ν˜Ήμ‹œ λͺ¨λ₯΄λ‹ˆ 확인
453
+ error_text = rest_response.text[:1000] if rest_response.text else '상세 정보 μ—†μŒ'
454
+ raise Exception(f"REST API 였λ₯˜: 응닡에 μ—λŸ¬κ°€ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. {error_text}")
455
+
456
+ # REST API 응닡 νŒŒμ‹±
457
+ response_data = rest_response.json()
458
+
459
+ # μ‘λ‹΅μ—μ„œ ν…μŠ€νŠΈ μΆ”μΆœ
460
+ if 'candidates' in response_data and len(response_data['candidates']) > 0:
461
+ candidate = response_data['candidates'][0]
462
+ if 'content' in candidate and 'parts' in candidate['content']:
463
+ parts = candidate['content']['parts']
464
+ if len(parts) > 0 and 'text' in parts[0]:
465
+ response_text = parts[0]['text']
466
+ print(f"[Gemini] REST API 응닡 μˆ˜μ‹  성곡 (길이: {len(response_text)}자)")
467
+
468
+ # genai 라이브러리 ν˜•μ‹μœΌλ‘œ λ³€ν™˜ (ν˜Έν™˜μ„±μ„ μœ„ν•΄)
469
+ class MockResponse:
470
+ def __init__(self, text):
471
+ self.text = text
472
+
473
+ response = MockResponse(response_text)
474
+ break
475
+ else:
476
+ raise Exception("REST API 응닡에 ν…μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
477
+ else:
478
+ raise Exception("REST API 응닡 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
479
+ else:
480
+ raise Exception("REST API 응닡에 candidatesκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
481
+ else:
482
+ # κΈ°μ‘΄ genai 라이브러리 μ‚¬μš© (fallback)
483
+ genai.configure(api_key=self.api_key)
484
+ print(f"[Gemini] genai 라이브러리 μ‚¬μš© (fallback)")
485
+ response = model.generate_content(
486
+ prompt,
487
+ generation_config=generation_config
488
+ )
489
+ print(f"[Gemini] Gemini API 응닡 μˆ˜μ‹  성곡")
490
+ break
491
+ # 성곡 μ‹œ 루프 μ’…λ£Œ
492
+ if retry_count > 0:
493
+ print(f"[Gemini] μž¬μ‹œλ„ 성곡 (총 {retry_count}회 μž¬μ‹œλ„)")
494
+ break
495
+ except Exception as e:
496
+ last_error = e
497
+ error_str = str(e).lower()
498
+
499
+ # μž¬μ‹œλ„ κ°€λŠ₯ν•œ 였λ₯˜μΈμ§€ 확인 (νƒ€μž„μ•„μ›ƒ, λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±)
500
+ retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
501
+ is_retryable = any(err in error_str for err in retryable_errors)
502
+
503
+ # deadline 확인
504
+ if time.time() >= deadline_time:
505
+ print(f"[Gemini] μž¬μ‹œλ„ deadline 초과 ({deadline}초), λ§ˆμ§€λ§‰ 였λ₯˜ λ°˜ν™˜")
506
+ raise
507
+
508
+ if is_retryable:
509
+ retry_count += 1
510
+ print(f"[Gemini] μž¬μ‹œλ„ {retry_count} - {wait_time:.1f}초 ν›„ μž¬μ‹œλ„ (였λ₯˜: {str(e)[:100]})")
511
+ time.sleep(wait_time)
512
+ wait_time = min(wait_time * multiplier, max_wait) # 배수둜 증가, μ΅œλŒ€κ°’ μ œν•œ
513
+ else:
514
+ # μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜
515
+ print(f"[Gemini] μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜: {str(e)[:100]}")
516
+ raise
517
+
518
+ # 응닡 μ‹œκ°„ 확인
519
+ elapsed_time = time.time() - start_time
520
+ print(f"[Gemini] 응닡 μˆ˜μ‹  μ™„λ£Œ (κ²½κ³Ό μ‹œκ°„: {elapsed_time:.2f}초)")
521
+
522
+ # 응닡 ν…μŠ€νŠΈ μΆ”μΆœ
523
+ response_text = response.text if hasattr(response, 'text') else str(response)
524
+
525
+ print(f"[Gemini] 응닡 생성 μ™„λ£Œ: {len(response_text)}자")
526
+ return {
527
+ 'response': response_text,
528
+ 'error': None
529
+ }
530
+
531
+ except Exception as e:
532
+ error_msg = f'Gemini API 였λ₯˜: {str(e)}'
533
+ print(f"[Gemini] {error_msg}")
534
+ return {
535
+ 'response': None,
536
+ 'error': error_msg
537
+ }
538
+
539
+ def generate_chat_response(self, messages: List[Dict], model_name: str = 'gemini-1.5-flash', **kwargs) -> Dict:
540
+ """
541
+ Gemini APIλ₯Ό μ‚¬μš©ν•˜μ—¬ μ±„νŒ… 응닡 생성
542
+
543
+ Args:
544
+ messages: λ©”μ‹œμ§€ 리슀트 [{'role': 'user', 'content': '...'}, ...]
545
+ model_name: μ‚¬μš©ν•  λͺ¨λΈ 이름
546
+ **kwargs: μΆ”κ°€ νŒŒλΌλ―Έν„°
547
+
548
+ Returns:
549
+ Dict: {'response': str, 'error': str or None}
550
+ """
551
+ if not self.is_configured():
552
+ return {
553
+ 'response': None,
554
+ 'error': 'Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. GEMINI_API_KEY ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”.'
555
+ }
556
+
557
+ try:
558
+ # λͺ¨λΈ 생성
559
+ model = genai.GenerativeModel(model_name)
560
+
561
+ # μ±„νŒ… μ„Έμ…˜ μ‹œμž‘
562
+ chat = model.start_chat(history=[])
563
+
564
+ # 이전 λŒ€ν™” λ‚΄μ—­ μΆ”κ°€ (user와 assistant λ©”μ‹œμ§€)
565
+ for msg in messages[:-1]: # λ§ˆμ§€λ§‰ λ©”μ‹œμ§€ μ œμ™Έ
566
+ if msg['role'] == 'user':
567
+ chat.send_message(msg['content'])
568
+ elif msg['role'] == 'assistant' or msg['role'] == 'ai':
569
+ # GeminiλŠ” μ‚¬μš©μž λ©”μ‹œμ§€λ§Œ 직접 λ³΄λ‚΄λ―€λ‘œ, assistant λ©”μ‹œμ§€λŠ” νžˆμŠ€ν† λ¦¬λ‘œ μ²˜λ¦¬ν•˜μ§€ μ•ŠμŒ
570
+ pass
571
+
572
+ # λ§ˆμ§€λ§‰ μ‚¬μš©μž λ©”μ‹œμ§€λ‘œ 응닡 생성 (νƒ€μž„μ•„μ›ƒ μ„€μ •)
573
+ last_message = messages[-1] if messages else {'content': ''}
574
+ timeout_seconds = getattr(self, 'timeout', 600) # 5λΆ„ νƒ€μž„μ•„μ›ƒ
575
+ print(f"[Gemini] μ±„νŒ… 응닡 생성 쀑... (νƒ€μž„μ•„μ›ƒ: {timeout_seconds}초)")
576
+
577
+ # νƒ€μž„μ•„μ›ƒμ„ μœ„ν•œ μ‹œμž‘ μ‹œκ°„ 기둝
578
+ start_time = time.time()
579
+
580
+ # google-generativeaiλŠ” retry νŒŒλΌλ―Έν„°λ₯Ό μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ
581
+ # μž¬μ‹œλ„ 정책을 μˆ˜λ™μœΌλ‘œ κ΅¬ν˜„ν•˜μ—¬ 적용
582
+ print(f"[Gemini] μ±„νŒ… API 호좜 μ‹œμž‘ (νƒ€μž„μ•„μ›ƒ: {timeout_seconds}초, μž¬μ‹œλ„ μ •μ±… 적용)")
583
+
584
+ # μž¬μ‹œλ„ 둜직 κ΅¬ν˜„ (μž¬μ‹œλ„ μ •μ±… 객체의 μ„€μ • μ‚¬μš©)
585
+ initial_wait = 10.0 # 초기 λŒ€κΈ° μ‹œκ°„
586
+ max_wait = 60.0 # μ΅œλŒ€ λŒ€κΈ° μ‹œκ°„
587
+ multiplier = 2.0 # λŒ€κΈ° μ‹œκ°„ 배수
588
+ deadline = 600.0 # 전체 μž¬μ‹œλ„ κΈ°κ°„ (10λΆ„)
589
+
590
+ wait_time = initial_wait
591
+ deadline_time = time.time() + deadline
592
+ retry_count = 0
593
+ last_error = None
594
+
595
+ while True:
596
+ try:
597
+ response = chat.send_message(last_message['content'])
598
+ # 성곡 μ‹œ 루프 μ’…λ£Œ
599
+ if retry_count > 0:
600
+ print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ 성곡 (총 {retry_count}회 μž¬μ‹œλ„)")
601
+ break
602
+ except Exception as e:
603
+ last_error = e
604
+ error_str = str(e).lower()
605
+
606
+ # μž¬μ‹œλ„ κ°€λŠ₯ν•œ 였λ₯˜μΈμ§€ 확인 (νƒ€μž„μ•„μ›ƒ, λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±)
607
+ retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
608
+ is_retryable = any(err in error_str for err in retryable_errors)
609
+
610
+ # deadline 확인
611
+ if time.time() >= deadline_time:
612
+ print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ deadline 초과 ({deadline}초), λ§ˆμ§€λ§‰ 였λ₯˜ λ°˜ν™˜")
613
+ raise
614
+
615
+ if is_retryable:
616
+ retry_count += 1
617
+ print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ {retry_count} - {wait_time:.1f}초 ν›„ μž¬μ‹œλ„ (였λ₯˜: {str(e)[:100]})")
618
+ time.sleep(wait_time)
619
+ wait_time = min(wait_time * multiplier, max_wait) # 배수둜 증가, μ΅œλŒ€κ°’ μ œν•œ
620
+ else:
621
+ # μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜
622
+ print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜: {str(e)[:100]}")
623
+ raise
624
+
625
+ elapsed_time = time.time() - start_time
626
+ print(f"[Gemini] μ±„νŒ… 응닡 μˆ˜μ‹  μ™„λ£Œ (κ²½κ³Ό μ‹œκ°„: {elapsed_time:.2f}초)")
627
+
628
+ response_text = response.text if hasattr(response, 'text') else str(response)
629
+
630
+ print(f"[Gemini] μ±„νŒ… 응닡 생성 μ™„λ£Œ: {len(response_text)}자")
631
+ return {
632
+ 'response': response_text,
633
+ 'error': None
634
+ }
635
+
636
+ except Exception as e:
637
+ error_msg = f'Gemini API 였λ₯˜: {str(e)}'
638
+ print(f"[Gemini] {error_msg}")
639
+ return {
640
+ 'response': None,
641
+ 'error': error_msg
642
+ }
643
+
644
+ # μ „μ—­ Gemini ν΄λΌμ΄μ–ΈνŠΈ μΈμŠ€ν„΄μŠ€
645
+ _gemini_client = None
646
+
647
+ def get_gemini_client() -> GeminiClient:
648
+ """Gemini ν΄λΌμ΄μ–ΈνŠΈ 싱글톀 μΈμŠ€ν„΄μŠ€ λ°˜ν™˜"""
649
+ global _gemini_client
650
+ if _gemini_client is None:
651
+ _gemini_client = GeminiClient()
652
+ else:
653
+ # API ν‚€κ°€ λ³€κ²½λ˜μ—ˆμ„ 수 μžˆμœΌλ―€λ‘œ μž¬λ‘œλ“œ μ‹œλ„
654
+ _gemini_client.reload_api_key()
655
+ return _gemini_client
656
+
657
+ def reset_gemini_client():
658
+ """Gemini ν΄λΌμ΄μ–ΈνŠΈ 리셋 (API ν‚€ λ³€κ²½ ν›„ 호좜)"""
659
+ global _gemini_client
660
+ _gemini_client = None
661
+ return get_gemini_client()
662
+
app/routes.py CHANGED
@@ -1,8 +1,9 @@
1
  from flask import Blueprint, render_template, request, jsonify, send_from_directory, redirect, url_for, flash
2
  from flask_login import login_user, logout_user, login_required, current_user
3
  from werkzeug.utils import secure_filename
4
- from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk
5
  from app.vector_db import get_vector_db
 
6
  import requests
7
  import os
8
  from datetime import datetime
@@ -19,6 +20,9 @@ def admin_required(f):
19
  @login_required
20
  def decorated_function(*args, **kwargs):
21
  if not current_user.is_admin:
 
 
 
22
  flash('κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.', 'error')
23
  return redirect(url_for('main.index'))
24
  return f(*args, **kwargs)
@@ -262,6 +266,11 @@ def create_parent_chunk_with_ai(file_id, content, model_name):
262
  print(f"[Parent Chunk 생성] μ‚¬μš© λͺ¨λΈ: {model_name}")
263
  print(f"[Parent Chunk 생성] 원본 ν…μŠ€νŠΈ 길이: {len(content)}자")
264
 
 
 
 
 
 
265
  # ν…μŠ€νŠΈκ°€ λ„ˆλ¬΄ κΈΈλ©΄ μΌλΆ€λ§Œ μ‚¬μš© (μ΅œλŒ€ 50000자)
266
  content_preview = content[:50000] if len(content) > 50000 else content
267
  if len(content) > 50000:
@@ -292,35 +301,95 @@ def create_parent_chunk_with_ai(file_id, content, model_name):
292
 
293
  각 ν•­λͺ©μ„ λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„ν•˜μ—¬ μž‘μ„±ν•΄μ£Όμ„Έμš”."""
294
 
295
- print(f"[Parent Chunk 생성] Ollama API에 뢄석 μš”μ²­ 전솑 쀑...")
296
-
297
- # Ollama API 호좜
298
- ollama_response = requests.post(
299
- f'{OLLAMA_BASE_URL}/api/chat',
300
- json={
301
- 'model': model_name,
302
- 'messages': [
303
- {
304
- 'role': 'user',
305
- 'content': analysis_prompt
306
- }
307
- ],
308
- 'stream': False
309
- },
310
- timeout=300 # 5λΆ„ νƒ€μž„μ•„μ›ƒ
311
- )
312
 
313
- if ollama_response.status_code != 200:
314
- error_detail = ollama_response.text if ollama_response.text else '상세 정보 μ—†μŒ'
315
- if ollama_response.status_code == 404:
316
- error_msg = f'Ollama API 였λ₯˜ 404: λͺ¨λΈ "{model_name}"을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. λͺ¨λΈμ΄ Ollama에 μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.'
317
- else:
318
- error_msg = f'Ollama API 였λ₯˜: {ollama_response.status_code} - {error_detail[:200]}'
319
- print(f"[Parent Chunk 생성] ❌ 였λ₯˜: {error_msg}")
320
- return None
321
 
322
- response_data = ollama_response.json()
323
- analysis_result = response_data.get('message', {}).get('content', '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
  if not analysis_result:
326
  print(f"[Parent Chunk 생성] ⚠️ κ²½κ³ : 뢄석 κ²°κ³Όκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
@@ -849,24 +918,107 @@ def delete_user(user_id):
849
  db.session.rollback()
850
  return jsonify({'error': f'μ‚¬μš©μž μ‚­μ œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}'}), 500
851
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  @main_bp.route('/api/ollama/models', methods=['GET'])
853
  @login_required
854
  def get_ollama_models():
855
- """Ollamaμ—μ„œ μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ λͺ©λ‘ κ°€μ Έμ˜€κΈ° (μ‹€μ œ μ„€μΉ˜λœ λͺ¨λΈλ§Œ λ°˜ν™˜)"""
856
  try:
857
- response = requests.get(f'{OLLAMA_BASE_URL}/api/tags', timeout=5)
858
- if response.status_code == 200:
859
- data = response.json()
860
- ollama_models = [{'name': model['name']} for model in data.get('models', [])]
861
-
862
- # μ‹€μ œ μ„€μΉ˜λœ λͺ¨λΈλ§Œ λ°˜ν™˜
863
- return jsonify({'models': ollama_models})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
  else:
865
- return jsonify({'error': 'Ollama μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€.', 'models': []}), 500
866
- except requests.exceptions.ConnectionError:
867
- return jsonify({'error': 'Ollama μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. Ollamaκ°€ μ‹€ν–‰ 쀑인지 ν™•μΈν•˜μ„Έμš”.', 'models': []}), 503
868
- except requests.exceptions.Timeout:
869
- return jsonify({'error': 'Ollama μ„œλ²„ 응닡 μ‹œκ°„μ΄ μ΄ˆκ³Όλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'models': []}), 504
870
  except Exception as e:
871
  return jsonify({'error': f'λͺ¨λΈ λͺ©λ‘μ„ κ°€μ Έμ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}', 'models': []}), 500
872
 
@@ -1098,111 +1250,134 @@ def chat():
1098
  # ν”„λ‘¬ν”„νŠΈ ꡬ성
1099
  full_prompt = context + message if context else message
1100
 
1101
- # Ollama API 호좜
1102
- ollama_response = requests.post(
1103
- f'{OLLAMA_BASE_URL}/api/generate',
1104
- json={
1105
- 'model': model,
1106
- 'prompt': full_prompt,
1107
- 'stream': False
1108
- },
1109
- timeout=120 # 파일이 λ§Žμ„ 수 μžˆμœΌλ―€λ‘œ νƒ€μž„μ•„μ›ƒ 증가
1110
- )
1111
 
1112
- if ollama_response.status_code == 200:
1113
- ollama_data = ollama_response.json()
1114
- response_text = ollama_data.get('response', '응닡을 생성할 수 μ—†μŠ΅λ‹ˆλ‹€.')
 
 
 
 
 
 
 
 
 
 
 
 
1115
 
1116
- # λŒ€ν™” μ„Έμ…˜μ— λ©”μ‹œμ§€ μ €μž₯
1117
- session_id = data.get('session_id')
1118
- if session_id:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1119
  try:
1120
- session = ChatSession.query.filter_by(
1121
- id=session_id,
1122
- user_id=current_user.id
1123
- ).first()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
 
1125
- if session:
1126
- # μ‚¬μš©μž λ©”μ‹œμ§€κ°€ 이미 μ €μž₯λ˜μ–΄ μžˆλŠ”μ§€ 확인 (쀑볡 λ°©μ§€)
1127
- # κ°€μž₯ 졜근 λ©”μ‹œμ§€λ₯Ό ν™•μΈν•˜μ—¬ 쀑볡 μ €μž₯ λ°©μ§€
1128
- latest_user_msg = ChatMessage.query.filter_by(
1129
- session_id=session_id,
1130
- role='user'
1131
- ).order_by(ChatMessage.created_at.desc()).first()
1132
-
1133
- # 졜근 10초 이내에 같은 λ‚΄μš©μ˜ λ©”μ‹œμ§€κ°€ μ—†μœΌλ©΄ μ €μž₯
1134
- should_save = True
1135
- if latest_user_msg:
1136
- time_diff = (datetime.utcnow() - latest_user_msg.created_at).total_seconds()
1137
- if latest_user_msg.content == message and time_diff < 10:
1138
- should_save = False
1139
- print(f"[쀑볡 λ°©μ§€] 졜근 {time_diff:.2f}초 전에 같은 λ©”μ‹œμ§€κ°€ μ €μž₯λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ €μž₯을 κ±΄λ„ˆλœλ‹ˆλ‹€.")
1140
-
1141
- if should_save:
1142
- user_msg = ChatMessage(
1143
- session_id=session_id,
1144
- role='user',
1145
- content=message
1146
- )
1147
- db.session.add(user_msg)
1148
- print(f"[λ©”μ‹œμ§€ μ €μž₯] μ‚¬μš©μž λ©”μ‹œμ§€ μ €μž₯: {message[:50]}...")
1149
-
1150
- # μ„Έμ…˜ 제λͺ© μ—…λ°μ΄νŠΈ (첫 μ‚¬μš©μž λ©”μ‹œμ§€μΈ 경우)
1151
- title_needs_update = (
1152
- not session.title or
1153
- session.title.strip() == '' or
1154
- session.title == 'μƒˆ λŒ€ν™”'
1155
- )
1156
-
1157
- if title_needs_update and message.strip():
1158
- # λ©”μ‹œμ§€ λ‚΄μš©μ„ 제λͺ©μœΌλ‘œ μ‚¬μš© (μ΅œλŒ€ 30자)
1159
- title = message.strip()[:30]
1160
- if len(message.strip()) > 30:
1161
- title += '...'
1162
- session.title = title
1163
- print(f"[μ„Έμ…˜ 제λͺ©] μ—…λ°μ΄νŠΈ: '{title}' (원본 길이: {len(message.strip())}자)")
1164
- elif title_needs_update:
1165
- print(f"[μ„Έμ…˜ 제λͺ©] λ©”μ‹œμ§€κ°€ λΉ„μ–΄μžˆμ–΄ 제λͺ©μ„ μ—…λ°μ΄νŠΈν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
1166
- else:
1167
- print(f"[λ©”μ‹œμ§€ μ €μž₯] 쀑볡 λ©”μ‹œμ§€λ‘œ 인해 μ €μž₯을 κ±΄λ„ˆλœλ‹ˆλ‹€.")
1168
-
1169
- # AI 응닡 μ €μž₯
1170
- ai_msg = ChatMessage(
1171
  session_id=session_id,
1172
- role='ai',
1173
- content=response_text
1174
  )
1175
- db.session.add(ai_msg)
 
1176
 
1177
- session.updated_at = datetime.utcnow()
1178
- db.session.commit()
 
 
 
 
1179
 
1180
- # μ„Έμ…˜ 정보λ₯Ό 응닡에 포함 (제λͺ© μ—…λ°μ΄νŠΈ 반영)
1181
- session_dict = session.to_dict()
1182
- except Exception as e:
1183
- print(f"λ©”μ‹œμ§€ μ €μž₯ 였λ₯˜: {str(e)}")
1184
- db.session.rollback()
1185
- session_dict = None
1186
-
1187
- response_data = {'response': response_text, 'session_id': session_id}
1188
- if session_dict:
1189
- response_data['session'] = session_dict
1190
-
1191
- return jsonify(response_data)
1192
- else:
1193
- # 였λ₯˜ 상세 정보 κ°€μ Έμ˜€κΈ°
1194
- try:
1195
- error_detail = ollama_response.json().get('error', ollama_response.text[:200])
1196
- except:
1197
- error_detail = ollama_response.text[:200] if ollama_response.text else '상세 정보 μ—†μŒ'
1198
-
1199
- if ollama_response.status_code == 404:
1200
- error_msg = f'λͺ¨λΈ "{model}"을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. λͺ¨λΈμ΄ Ollama에 μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”. (였λ₯˜: {error_detail})'
1201
- else:
1202
- error_msg = f'Ollama μ„œλ²„ 였λ₯˜ {ollama_response.status_code}: {error_detail}'
1203
-
1204
- print(f"[μ±„νŒ… API] 였λ₯˜ λ°œμƒ: {error_msg}")
1205
- return jsonify({'error': error_msg}), ollama_response.status_code
 
 
 
 
 
 
 
 
 
1206
 
1207
  except requests.exceptions.ConnectionError:
1208
  return jsonify({'error': 'Ollama μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. Ollamaκ°€ μ‹€ν–‰ 쀑인지 ν™•μΈν•˜μ„Έμš”.'}), 503
 
1
  from flask import Blueprint, render_template, request, jsonify, send_from_directory, redirect, url_for, flash
2
  from flask_login import login_user, logout_user, login_required, current_user
3
  from werkzeug.utils import secure_filename
4
+ from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk, SystemConfig
5
  from app.vector_db import get_vector_db
6
+ from app.gemini_client import get_gemini_client
7
  import requests
8
  import os
9
  from datetime import datetime
 
20
  @login_required
21
  def decorated_function(*args, **kwargs):
22
  if not current_user.is_admin:
23
+ # API μš”μ²­μΈ 경우 JSON 응닡 λ°˜ν™˜
24
+ if request.path.startswith('/api/'):
25
+ return jsonify({'error': 'κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.'}), 403
26
  flash('κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.', 'error')
27
  return redirect(url_for('main.index'))
28
  return f(*args, **kwargs)
 
266
  print(f"[Parent Chunk 생성] μ‚¬μš© λͺ¨λΈ: {model_name}")
267
  print(f"[Parent Chunk 생성] 원본 ν…μŠ€νŠΈ 길이: {len(content)}자")
268
 
269
+ # λͺ¨λΈλͺ…이 Noneμ΄κ±°λ‚˜ 빈 λ¬Έμžμ—΄μΈ 경우 처리
270
+ if not model_name or not model_name.strip():
271
+ print(f"[Parent Chunk 생성] ❌ 였λ₯˜: λͺ¨λΈλͺ…이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
272
+ return None
273
+
274
  # ν…μŠ€νŠΈκ°€ λ„ˆλ¬΄ κΈΈλ©΄ μΌλΆ€λ§Œ μ‚¬μš© (μ΅œλŒ€ 50000자)
275
  content_preview = content[:50000] if len(content) > 50000 else content
276
  if len(content) > 50000:
 
301
 
302
  각 ν•­λͺ©μ„ λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„ν•˜μ—¬ μž‘μ„±ν•΄μ£Όμ„Έμš”."""
303
 
304
+ # λͺ¨λΈ νƒ€μž… 확인 (Gemini λ˜λŠ” Ollama)
305
+ # Gemini λͺ¨λΈλͺ… ν˜•μ‹: "gemini:λͺ¨λΈλͺ…" λ˜λŠ” "gemini-1.5-flash" (접두사 μ—†λŠ” κ²½μš°λ„ 지원)
306
+ model_name_lower = model_name.lower().strip()
307
+ is_gemini = model_name_lower.startswith('gemini:') or model_name_lower.startswith('gemini-')
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
+ print(f"[Parent Chunk 생성] λͺ¨λΈ νƒ€μž… 확인: is_gemini={is_gemini}, model_name={model_name}")
 
 
 
 
 
 
 
310
 
311
+ if is_gemini:
312
+ # Gemini API 호좜
313
+ # λͺ¨λΈλͺ…μ—μ„œ "gemini:" 접두사 제거 (λŒ€μ†Œλ¬Έμž ꡬ뢄 없이)
314
+ gemini_model_name = model_name.strip()
315
+ if gemini_model_name.lower().startswith('gemini:'):
316
+ gemini_model_name = gemini_model_name.split(':', 1)[1].strip()
317
+ # "gemini-"둜 μ‹œμž‘ν•˜λŠ” 경우 (예: "gemini-1.5-flash") κ·ΈλŒ€λ‘œ μ‚¬μš©
318
+
319
+ print(f"[Parent Chunk 생성] Gemini API에 뢄석 μš”μ²­ 전솑 쀑... (λͺ¨λΈ: {gemini_model_name})")
320
+ print(f"[Parent Chunk 생성] 원본 λͺ¨λΈλͺ…: {model_name} -> Gemini λͺ¨λΈλͺ…: {gemini_model_name}")
321
+
322
+ gemini_client = get_gemini_client()
323
+ if not gemini_client.is_configured():
324
+ print(f"[Parent Chunk 생성] ❌ 였λ₯˜: Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
325
+ print(f"[Parent Chunk 생성] 디버그: Gemini ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ 확인 쀑...")
326
+ # API ν‚€ μƒνƒœ λ‹€μ‹œ 확인
327
+ from app.gemini_client import get_gemini_api_key
328
+ api_key = get_gemini_api_key()
329
+ if api_key:
330
+ print(f"[Parent Chunk 생성] 디버그: API ν‚€λŠ” μ‘΄μž¬ν•˜μ§€λ§Œ ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. (길이: {len(api_key)})")
331
+ else:
332
+ print(f"[Parent Chunk 생성] 디버그: API ν‚€κ°€ λ°μ΄ν„°λ² μ΄μŠ€μ— μ—†μŠ΅λ‹ˆλ‹€.")
333
+ return None
334
+
335
+ print(f"[Parent Chunk 생성] Gemini API ν‚€ 확인 μ™„λ£Œ. API 호좜 μ‹œμž‘...")
336
+ result = gemini_client.generate_response(
337
+ prompt=analysis_prompt,
338
+ model_name=gemini_model_name,
339
+ temperature=0.7,
340
+ max_output_tokens=8192
341
+ )
342
+
343
+ if result['error']:
344
+ print(f"[Parent Chunk 생성] ❌ 였λ₯˜: Gemini API 호좜 μ‹€νŒ¨ - {result['error']}")
345
+ print(f"[Parent Chunk 생성] 디버그: result 객체 λ‚΄μš©: {result}")
346
+ return None
347
+
348
+ if not result.get('response'):
349
+ print(f"[Parent Chunk 생성] ❌ 였λ₯˜: Gemini API 응닡이 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
350
+ print(f"[Parent Chunk 생성] 디버그: result 객체 λ‚΄μš©: {result}")
351
+ return None
352
+
353
+ analysis_result = result['response']
354
+ print(f"[Parent Chunk 생성] Gemini API 응닡 μˆ˜μ‹  성곡: {len(analysis_result)}자")
355
+ else:
356
+ # Ollama API 호좜
357
+ print(f"[Parent Chunk 생성] Ollama API에 뢄석 μš”μ²­ 전솑 쀑... (λͺ¨λΈ: {model_name})")
358
+
359
+ try:
360
+ ollama_response = requests.post(
361
+ f'{OLLAMA_BASE_URL}/api/chat',
362
+ json={
363
+ 'model': model_name,
364
+ 'messages': [
365
+ {
366
+ 'role': 'user',
367
+ 'content': analysis_prompt
368
+ }
369
+ ],
370
+ 'stream': False
371
+ },
372
+ timeout=300 # 5λΆ„ νƒ€μž„μ•„μ›ƒ
373
+ )
374
+
375
+ if ollama_response.status_code != 200:
376
+ error_detail = ollama_response.text if ollama_response.text else '상세 정보 μ—†μŒ'
377
+ if ollama_response.status_code == 404:
378
+ error_msg = f'Ollama API 였λ₯˜ 404: λͺ¨λΈ "{model_name}"을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. λͺ¨λΈμ΄ Ollama에 μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.'
379
+ print(f"[Parent Chunk 생성] ❌ 였λ₯˜: {error_msg}")
380
+ print(f"[Parent Chunk 생성] 디버그: λ§Œμ•½ Gemini λͺ¨λΈμ„ μ‚¬μš©ν•˜λ €λ©΄ λͺ¨λΈλͺ…이 'gemini:' λ˜λŠ” 'gemini-'둜 μ‹œμž‘ν•΄μ•Ό ν•©λ‹ˆλ‹€.")
381
+ else:
382
+ error_msg = f'Ollama API 였λ₯˜: {ollama_response.status_code} - {error_detail[:200]}'
383
+ print(f"[Parent Chunk 생성] ❌ 였λ₯˜: {error_msg}")
384
+ return None
385
+
386
+ response_data = ollama_response.json()
387
+ analysis_result = response_data.get('message', {}).get('content', '')
388
+ print(f"[Parent Chunk 생성] Ollama API 응닡 μˆ˜μ‹  성곡: {len(analysis_result)}자")
389
+ except requests.exceptions.RequestException as e:
390
+ print(f"[Parent Chunk 생성] ❌ Ollama API μ—°κ²° 였λ₯˜: {str(e)}")
391
+ print(f"[Parent Chunk 생성] 디버그: Ollama URL: {OLLAMA_BASE_URL}")
392
+ raise
393
 
394
  if not analysis_result:
395
  print(f"[Parent Chunk 생성] ⚠️ κ²½κ³ : 뢄석 κ²°κ³Όκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
 
918
  db.session.rollback()
919
  return jsonify({'error': f'μ‚¬μš©μž μ‚­μ œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}'}), 500
920
 
921
+ @main_bp.route('/api/admin/gemini-api-key', methods=['GET'])
922
+ @admin_required
923
+ def get_gemini_api_key():
924
+ """Gemini API ν‚€ 쑰회"""
925
+ try:
926
+ # SystemConfigμ—μ„œ API ν‚€ κ°€μ Έμ˜€κΈ° (ν…Œμ΄λΈ”μ΄ μ—†μœΌλ©΄ 빈 λ¬Έμžμ—΄ λ°˜ν™˜)
927
+ api_key = SystemConfig.get_config('gemini_api_key', '')
928
+
929
+ # λ³΄μ•ˆμ„ μœ„ν•΄ λ§ˆμŠ€ν‚Ήλœ κ°’ λ°˜ν™˜ (처음 8자만 ν‘œμ‹œ)
930
+ masked_key = api_key[:8] + '...' if api_key and len(api_key) > 8 else ''
931
+ return jsonify({
932
+ 'has_api_key': bool(api_key),
933
+ 'masked_key': masked_key
934
+ }), 200
935
+ except Exception as e:
936
+ print(f"[Gemini API ν‚€ 쑰회] 였λ₯˜: {e}")
937
+ import traceback
938
+ traceback.print_exc()
939
+ return jsonify({'error': f'API ν‚€ 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}'}), 500
940
+
941
+ @main_bp.route('/api/admin/gemini-api-key', methods=['POST'])
942
+ @admin_required
943
+ def set_gemini_api_key():
944
+ """Gemini API ν‚€ μ €μž₯/μ—…λ°μ΄νŠΈ"""
945
+
946
+ try:
947
+ if not request.is_json:
948
+ return jsonify({'error': 'Content-Type이 application/json이 μ•„λ‹™λ‹ˆλ‹€.'}), 400
949
+
950
+ data = request.json
951
+ if not data:
952
+ return jsonify({'error': 'μš”μ²­ 데이터가 μ—†μŠ΅λ‹ˆλ‹€.'}), 400
953
+
954
+ api_key = data.get('api_key', '').strip()
955
+
956
+ if not api_key:
957
+ return jsonify({'error': 'API ν‚€λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.'}), 400
958
+
959
+ # API ν‚€ μ €μž₯ (SystemConfig.set_config λ‚΄λΆ€μ—μ„œ ν…Œμ΄λΈ” 생성 처리)
960
+ SystemConfig.set_config(
961
+ key='gemini_api_key',
962
+ value=api_key,
963
+ description='Google Gemini API ν‚€'
964
+ )
965
+
966
+ # Gemini ν΄λΌμ΄μ–ΈνŠΈμ— API ν‚€ μž¬λ‘œλ“œ μ•Œλ¦Ό
967
+ try:
968
+ from app.gemini_client import reset_gemini_client
969
+ reset_gemini_client()
970
+ print(f"[Gemini] API ν‚€κ°€ μ—…λ°μ΄νŠΈλ˜μ–΄ ν΄λΌμ΄μ–ΈνŠΈκ°€ μž¬λ‘œλ“œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
971
+ except Exception as e:
972
+ print(f"[Gemini] API ν‚€ μž¬λ‘œλ“œ μ‹€νŒ¨: {e}")
973
+
974
+ return jsonify({
975
+ 'message': 'Gemini API ν‚€κ°€ μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.',
976
+ 'has_api_key': True
977
+ }), 200
978
+
979
+ except Exception as e:
980
+ db.session.rollback()
981
+ print(f"[Gemini API ν‚€ μ €μž₯] 였λ₯˜: {e}")
982
+ import traceback
983
+ traceback.print_exc()
984
+ return jsonify({'error': f'API ν‚€ μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}'}), 500
985
+
986
  @main_bp.route('/api/ollama/models', methods=['GET'])
987
  @login_required
988
  def get_ollama_models():
989
+ """Ollama 및 Geminiμ—μ„œ μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ λͺ©λ‘ κ°€μ Έμ˜€κΈ°"""
990
  try:
991
+ all_models = []
992
+
993
+ # 1. Ollama λͺ¨λΈ λͺ©λ‘ κ°€μ Έμ˜€κΈ°
994
+ try:
995
+ response = requests.get(f'{OLLAMA_BASE_URL}/api/tags', timeout=5)
996
+ if response.status_code == 200:
997
+ data = response.json()
998
+ ollama_models = [{'name': model['name'], 'type': 'ollama'} for model in data.get('models', [])]
999
+ all_models.extend(ollama_models)
1000
+ print(f"[λͺ¨λΈ λͺ©λ‘] Ollama λͺ¨λΈ {len(ollama_models)}개 μΆ”κ°€")
1001
+ except Exception as e:
1002
+ print(f"[λͺ¨λΈ λͺ©λ‘] Ollama λͺ¨λΈ λͺ©λ‘ 쑰회 μ‹€νŒ¨: {e}")
1003
+
1004
+ # 2. Gemini λͺ¨λΈ λͺ©λ‘ κ°€μ Έμ˜€κΈ°
1005
+ try:
1006
+ gemini_client = get_gemini_client()
1007
+ if gemini_client.is_configured():
1008
+ gemini_models = gemini_client.get_available_models()
1009
+ gemini_model_list = [{'name': f'gemini:{model_name}', 'type': 'gemini'} for model_name in gemini_models]
1010
+ all_models.extend(gemini_model_list)
1011
+ print(f"[λͺ¨λΈ λͺ©λ‘] Gemini λͺ¨λΈ {len(gemini_model_list)}개 μΆ”κ°€")
1012
+ else:
1013
+ print(f"[λͺ¨λΈ λͺ©λ‘] Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•„ Gemini λͺ¨λΈμ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.")
1014
+ except Exception as e:
1015
+ print(f"[λͺ¨λΈ λͺ©λ‘] Gemini λͺ¨λΈ λͺ©λ‘ 쑰회 μ‹€νŒ¨: {e}")
1016
+
1017
+ if all_models:
1018
+ return jsonify({'models': all_models})
1019
  else:
1020
+ return jsonify({'error': 'μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈμ΄ μ—†μŠ΅λ‹ˆλ‹€. Ollamaκ°€ μ‹€ν–‰ 쀑인지, λ˜λŠ” Gemini API ν‚€κ°€ μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.', 'models': []}), 500
1021
+
 
 
 
1022
  except Exception as e:
1023
  return jsonify({'error': f'λͺ¨λΈ λͺ©λ‘μ„ κ°€μ Έμ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}', 'models': []}), 500
1024
 
 
1250
  # ν”„λ‘¬ν”„νŠΈ ꡬ성
1251
  full_prompt = context + message if context else message
1252
 
1253
+ # λͺ¨λΈ νƒ€μž… 확인 (Gemini λ˜λŠ” Ollama)
1254
+ is_gemini = model.startswith('gemini:')
 
 
 
 
 
 
 
 
1255
 
1256
+ if is_gemini:
1257
+ # Gemini API ��좜
1258
+ gemini_model_name = model.replace('gemini:', '')
1259
+ print(f"[Gemini] λͺ¨λΈ: {gemini_model_name}, 질문: {message[:50]}...")
1260
+
1261
+ gemini_client = get_gemini_client()
1262
+ if not gemini_client.is_configured():
1263
+ return jsonify({'error': 'Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. GEMINI_API_KEY ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”.'}), 500
1264
+
1265
+ result = gemini_client.generate_response(
1266
+ prompt=full_prompt,
1267
+ model_name=gemini_model_name,
1268
+ temperature=0.7,
1269
+ max_output_tokens=8192
1270
+ )
1271
 
1272
+ if result['error']:
1273
+ return jsonify({'error': result['error']}), 500
1274
+
1275
+ response_text = result['response']
1276
+ else:
1277
+ # Ollama API 호좜
1278
+ ollama_response = requests.post(
1279
+ f'{OLLAMA_BASE_URL}/api/generate',
1280
+ json={
1281
+ 'model': model,
1282
+ 'prompt': full_prompt,
1283
+ 'stream': False
1284
+ },
1285
+ timeout=120 # 파일이 λ§Žμ„ 수 μžˆμœΌλ―€λ‘œ νƒ€μž„μ•„μ›ƒ 증가
1286
+ )
1287
+
1288
+ if ollama_response.status_code != 200:
1289
+ # 였λ₯˜ 상세 정보 κ°€μ Έμ˜€κΈ°
1290
  try:
1291
+ error_detail = ollama_response.json().get('error', ollama_response.text[:200])
1292
+ except:
1293
+ error_detail = ollama_response.text[:200] if ollama_response.text else '상세 정보 μ—†μŒ'
1294
+
1295
+ if ollama_response.status_code == 404:
1296
+ error_msg = f'λͺ¨λΈ "{model}"을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. λͺ¨λΈμ΄ Ollama에 μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”. (였λ₯˜: {error_detail})'
1297
+ else:
1298
+ error_msg = f'Ollama μ„œλ²„ 였λ₯˜: {ollama_response.status_code} (였λ₯˜: {error_detail})'
1299
+ return jsonify({'error': error_msg}), ollama_response.status_code
1300
+
1301
+ ollama_data = ollama_response.json()
1302
+ response_text = ollama_data.get('response', '응닡을 생성할 수 μ—†μŠ΅λ‹ˆλ‹€.')
1303
+
1304
+ # λŒ€ν™” μ„Έμ…˜μ— λ©”μ‹œμ§€ μ €μž₯ (Gemini와 Ollama 곡톡)
1305
+ session_id = data.get('session_id')
1306
+ session_dict = None
1307
+ if session_id:
1308
+ try:
1309
+ session = ChatSession.query.filter_by(
1310
+ id=session_id,
1311
+ user_id=current_user.id
1312
+ ).first()
1313
+
1314
+ if session:
1315
+ # μ‚¬μš©μž λ©”μ‹œμ§€κ°€ 이미 μ €μž₯λ˜μ–΄ μžˆλŠ”μ§€ 확인 (쀑볡 λ°©μ§€)
1316
+ # κ°€μž₯ 졜근 λ©”μ‹œμ§€λ₯Ό ν™•μΈν•˜μ—¬ 쀑볡 μ €μž₯ λ°©μ§€
1317
+ latest_user_msg = ChatMessage.query.filter_by(
1318
+ session_id=session_id,
1319
+ role='user'
1320
+ ).order_by(ChatMessage.created_at.desc()).first()
1321
 
1322
+ # 졜근 10초 이내에 같은 λ‚΄μš©μ˜ λ©”μ‹œμ§€κ°€ μ—†μœΌλ©΄ μ €μž₯
1323
+ should_save = True
1324
+ if latest_user_msg:
1325
+ time_diff = (datetime.utcnow() - latest_user_msg.created_at).total_seconds()
1326
+ if latest_user_msg.content == message and time_diff < 10:
1327
+ should_save = False
1328
+ print(f"[쀑볡 λ°©μ§€] 졜근 {time_diff:.2f}초 전에 같은 λ©”μ‹œμ§€κ°€ μ €μž₯λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ €μž₯을 κ±΄λ„ˆλœλ‹ˆλ‹€.")
1329
+
1330
+ if should_save:
1331
+ user_msg = ChatMessage(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1332
  session_id=session_id,
1333
+ role='user',
1334
+ content=message
1335
  )
1336
+ db.session.add(user_msg)
1337
+ print(f"[λ©”μ‹œμ§€ μ €μž₯] μ‚¬μš©μž λ©”μ‹œμ§€ μ €μž₯: {message[:50]}...")
1338
 
1339
+ # μ„Έμ…˜ 제λͺ© μ—…λ°μ΄νŠΈ (첫 μ‚¬μš©μž λ©”μ‹œμ§€μΈ 경우)
1340
+ title_needs_update = (
1341
+ not session.title or
1342
+ session.title.strip() == '' or
1343
+ session.title == 'μƒˆ λŒ€ν™”'
1344
+ )
1345
 
1346
+ if title_needs_update and message.strip():
1347
+ # λ©”μ‹œμ§€ λ‚΄μš©μ„ 제λͺ©μœΌλ‘œ μ‚¬μš© (μ΅œλŒ€ 30자)
1348
+ title = message.strip()[:30]
1349
+ if len(message.strip()) > 30:
1350
+ title += '...'
1351
+ session.title = title
1352
+ print(f"[μ„Έμ…˜ 제λͺ©] μ—…λ°μ΄νŠΈ: '{title}' (원본 길이: {len(message.strip())}자)")
1353
+ elif title_needs_update:
1354
+ print(f"[μ„Έμ…˜ 제λͺ©] λ©”μ‹œμ§€κ°€ λΉ„μ–΄μžˆμ–΄ 제λͺ©μ„ μ—…λ°μ΄νŠΈν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
1355
+ else:
1356
+ print(f"[λ©”μ‹œμ§€ μ €μž₯] 쀑볡 λ©”μ‹œμ§€λ‘œ 인해 μ €μž₯을 κ±΄λ„ˆλœλ‹ˆλ‹€.")
1357
+
1358
+ # AI 응닡 μ €μž₯
1359
+ ai_msg = ChatMessage(
1360
+ session_id=session_id,
1361
+ role='ai',
1362
+ content=response_text
1363
+ )
1364
+ db.session.add(ai_msg)
1365
+
1366
+ session.updated_at = datetime.utcnow()
1367
+ db.session.commit()
1368
+
1369
+ # μ„Έμ…˜ 정보λ₯Ό 응닡에 포함 (제λͺ© μ—…λ°μ΄νŠΈ 반영)
1370
+ session_dict = session.to_dict()
1371
+ except Exception as e:
1372
+ print(f"λ©”μ‹œμ§€ μ €μž₯ 였λ₯˜: {str(e)}")
1373
+ db.session.rollback()
1374
+ session_dict = None
1375
+
1376
+ response_data = {'response': response_text, 'session_id': session_id}
1377
+ if session_dict:
1378
+ response_data['session'] = session_dict
1379
+
1380
+ return jsonify(response_data)
1381
 
1382
  except requests.exceptions.ConnectionError:
1383
  return jsonify({'error': 'Ollama μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. Ollamaκ°€ μ‹€ν–‰ 쀑인지 ν™•μΈν•˜μ„Έμš”.'}), 503
requirements.txt CHANGED
@@ -7,5 +7,6 @@ werkzeug==3.0.1
7
  chromadb==0.4.22
8
  sentence-transformers==2.3.1
9
  numpy==1.24.3
 
10
 
11
 
 
7
  chromadb==0.4.22
8
  sentence-transformers==2.3.1
9
  numpy==1.24.3
10
+ google-generativeai==0.3.2
11
 
12
 
templates/admin.html CHANGED
@@ -460,6 +460,27 @@
460
 
461
  <div id="alertContainer"></div>
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  <div class="card">
464
  <div class="card-header">
465
  <div class="card-title">μ‚¬μš©μž λͺ©λ‘</div>
@@ -668,6 +689,88 @@
668
  }
669
  });
670
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  </script>
672
  </body>
673
  </html>
 
460
 
461
  <div id="alertContainer"></div>
462
 
463
+ <!-- Gemini API ν‚€ μ„€μ • μ„Ήμ…˜ -->
464
+ <div class="card">
465
+ <div class="card-header">
466
+ <div class="card-title">Gemini API ν‚€ μ„€μ •</div>
467
+ </div>
468
+ <div style="padding: 16px 0;">
469
+ <div class="form-group">
470
+ <label for="geminiApiKey">Gemini API ν‚€</label>
471
+ <div style="display: flex; gap: 8px;">
472
+ <input type="password" id="geminiApiKey" placeholder="Gemini API ν‚€λ₯Ό μž…λ ₯ν•˜μ„Έμš”" style="flex: 1; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;">
473
+ <button class="btn btn-primary" onclick="saveGeminiApiKey()">μ €μž₯</button>
474
+ <button class="btn btn-secondary" onclick="loadGeminiApiKey()">ν˜„μž¬ μƒνƒœ 확인</button>
475
+ </div>
476
+ <small style="color: #5f6368; font-size: 12px; display: block; margin-top: 4px;">
477
+ Google AI Studio(<a href="https://aistudio.google.com/app/apikey" target="_blank">https://aistudio.google.com/app/apikey</a>)μ—μ„œ API ν‚€λ₯Ό λ°œκΈ‰λ°›μ„ 수 μžˆμŠ΅λ‹ˆλ‹€.
478
+ </small>
479
+ <div id="geminiApiKeyStatus" style="margin-top: 8px; font-size: 13px;"></div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+
484
  <div class="card">
485
  <div class="card-header">
486
  <div class="card-title">μ‚¬μš©μž λͺ©λ‘</div>
 
689
  }
690
  });
691
 
692
+ // Gemini API ν‚€ κ΄€λ ¨ ν•¨μˆ˜
693
+ async function loadGeminiApiKey() {
694
+ try {
695
+ const response = await fetch('/api/admin/gemini-api-key');
696
+
697
+ // 응닡이 JSON인지 확인
698
+ const contentType = response.headers.get('content-type');
699
+ if (!contentType || !contentType.includes('application/json')) {
700
+ const text = await response.text();
701
+ console.error('Non-JSON response:', text.substring(0, 200));
702
+ const statusDiv = document.getElementById('geminiApiKeyStatus');
703
+ statusDiv.innerHTML = `<span style="color: #ea4335;">μ„œλ²„ 였λ₯˜: 응닡 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.</span>`;
704
+ return;
705
+ }
706
+
707
+ const data = await response.json();
708
+
709
+ const statusDiv = document.getElementById('geminiApiKeyStatus');
710
+ if (!response.ok) {
711
+ statusDiv.innerHTML = `<span style="color: #ea4335;">였λ₯˜: ${data.error || 'μ•Œ 수 μ—†λŠ” 였λ₯˜'}</span>`;
712
+ return;
713
+ }
714
+
715
+ if (data.has_api_key) {
716
+ statusDiv.innerHTML = `<span style="color: #137333;">βœ“ API ν‚€κ°€ μ„€μ •λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€ (${data.masked_key})</span>`;
717
+ } else {
718
+ statusDiv.innerHTML = `<span style="color: #ea4335;">⚠ API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€</span>`;
719
+ }
720
+ } catch (error) {
721
+ console.error('Gemini API ν‚€ λ‘œλ“œ 였λ₯˜:', error);
722
+ const statusDiv = document.getElementById('geminiApiKeyStatus');
723
+ statusDiv.innerHTML = `<span style="color: #ea4335;">였λ₯˜: ${error.message}</span>`;
724
+ }
725
+ }
726
+
727
+ async function saveGeminiApiKey() {
728
+ const apiKeyInput = document.getElementById('geminiApiKey');
729
+ const apiKey = apiKeyInput.value.trim();
730
+
731
+ if (!apiKey) {
732
+ showAlert('API ν‚€λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.', 'error');
733
+ return;
734
+ }
735
+
736
+ try {
737
+ const response = await fetch('/api/admin/gemini-api-key', {
738
+ method: 'POST',
739
+ headers: {
740
+ 'Content-Type': 'application/json',
741
+ },
742
+ body: JSON.stringify({ api_key: apiKey })
743
+ });
744
+
745
+ // 응닡이 JSON인지 확인
746
+ const contentType = response.headers.get('content-type');
747
+ if (!contentType || !contentType.includes('application/json')) {
748
+ const text = await response.text();
749
+ console.error('Non-JSON response:', text.substring(0, 200));
750
+ showAlert('μ„œλ²„ 였λ₯˜: 응닡 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', 'error');
751
+ return;
752
+ }
753
+
754
+ const data = await response.json();
755
+
756
+ if (response.ok) {
757
+ showAlert(data.message, 'success');
758
+ apiKeyInput.value = ''; // λ³΄μ•ˆμ„ μœ„ν•΄ μž…λ ₯ ν•„λ“œ μ΄ˆκΈ°ν™”
759
+ loadGeminiApiKey(); // μƒνƒœ μ—…λ°μ΄νŠΈ
760
+ } else {
761
+ showAlert(data.error || 'API ν‚€ μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', 'error');
762
+ }
763
+ } catch (error) {
764
+ console.error('Gemini API ν‚€ μ €μž₯ 였λ₯˜:', error);
765
+ showAlert(`였λ₯˜: ${error.message}`, 'error');
766
+ }
767
+ }
768
+
769
+ // νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ API ν‚€ μƒνƒœ 확인
770
+ window.addEventListener('load', () => {
771
+ loadGeminiApiKey();
772
+ });
773
+
774
  </script>
775
  </body>
776
  </html>
templates/index.html CHANGED
@@ -552,6 +552,10 @@
552
  word-wrap: break-word;
553
  white-space: pre-wrap;
554
  }
 
 
 
 
555
 
556
  .message.user .message-bubble {
557
  background: var(--accent);
@@ -1215,6 +1219,26 @@
1215
  }
1216
  });
1217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1218
  // λ©”μ‹œμ§€ μΆ”κ°€
1219
  function addMessage(role, content, save = true) {
1220
  // 빈 μƒνƒœ 숨기기
@@ -1234,7 +1258,7 @@
1234
 
1235
  const bubble = document.createElement('div');
1236
  bubble.className = 'message-bubble';
1237
- bubble.textContent = content;
1238
 
1239
  const time = document.createElement('div');
1240
  time.className = 'message-time';
 
552
  word-wrap: break-word;
553
  white-space: pre-wrap;
554
  }
555
+
556
+ .message-bubble strong {
557
+ font-weight: 700;
558
+ }
559
 
560
  .message.user .message-bubble {
561
  background: var(--accent);
 
1219
  }
1220
  });
1221
 
1222
+ // Markdown μŠ€νƒ€μΌ κ°•μ‘° ν‘œμ‹œλ₯Ό HTML둜 λ³€ν™˜
1223
+ function formatMessageText(text) {
1224
+ if (!text) return '';
1225
+
1226
+ // HTML 특수문자 μ΄μŠ€μΌ€μ΄ν”„
1227
+ let html = text
1228
+ .replace(/&/g, '&amp;')
1229
+ .replace(/</g, '&lt;')
1230
+ .replace(/>/g, '&gt;');
1231
+
1232
+ // **ν…μŠ€νŠΈ** νŒ¨ν„΄μ„ <strong>ν…μŠ€νŠΈ</strong>둜 λ³€ν™˜
1233
+ // 단, ** 사이에 λ‚΄μš©μ΄ μžˆμ–΄μ•Ό 함
1234
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1235
+
1236
+ // μ€„λ°”κΏˆ 처리
1237
+ html = html.replace(/\n/g, '<br>');
1238
+
1239
+ return html;
1240
+ }
1241
+
1242
  // λ©”μ‹œμ§€ μΆ”κ°€
1243
  function addMessage(role, content, save = true) {
1244
  // 빈 μƒνƒœ 숨기기
 
1258
 
1259
  const bubble = document.createElement('div');
1260
  bubble.className = 'message-bubble';
1261
+ bubble.innerHTML = formatMessageText(content);
1262
 
1263
  const time = document.createElement('div');
1264
  time.className = 'message-time';