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 +59 -0
- app/gemini_client.py +662 -0
- app/routes.py +313 -138
- requirements.txt +1 -0
- templates/admin.html +103 -0
- templates/index.html +25 -1
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 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 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 |
-
|
| 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 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
else:
|
| 865 |
-
return jsonify({'error': 'Ollama
|
| 866 |
-
|
| 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 |
-
#
|
| 1102 |
-
|
| 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
|
| 1113 |
-
|
| 1114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1119 |
try:
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1124 |
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 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='
|
| 1173 |
-
content=
|
| 1174 |
)
|
| 1175 |
-
db.session.add(
|
|
|
|
| 1176 |
|
| 1177 |
-
|
| 1178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1179 |
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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, '&')
|
| 1229 |
+
.replace(/</g, '<')
|
| 1230 |
+
.replace(/>/g, '>');
|
| 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';
|