김무영 commited on
Commit
cb2a4bd
·
1 Parent(s): a6a02ef

Improve defaults, security, and index rebuild

Browse files
Files changed (6) hide show
  1. .env.example +6 -1
  2. README.md +4 -1
  3. app.py +27 -5
  4. config.py +3 -2
  5. supabase_vector_store.py +9 -1
  6. vector_store.py +22 -5
.env.example CHANGED
@@ -3,6 +3,8 @@
3
 
4
  # OpenAI API Key (필수)
5
  # OPENAI_API_KEY=your_openai_api_key_here
 
 
6
 
7
  # Supabase 설정 (필수)
8
  # SUPABASE_URL=https://your-project.supabase.co
@@ -14,4 +16,7 @@ VECTOR_DB_TYPE=faiss
14
 
15
  # 기존 FAISS 설정 (VECTOR_DB_TYPE=faiss 인 경우 사용)
16
  EMBEDDING_MODEL=jhgan/ko-sbert-nli
17
- LLM_MODEL=beomi/Llama-3-Open-Ko-8B
 
 
 
 
3
 
4
  # OpenAI API Key (필수)
5
  # OPENAI_API_KEY=your_openai_api_key_here
6
+ OPENAI_MODEL=gpt-4o-mini
7
+ OPENAI_EMBEDDING_MODEL=text-embedding-3-small
8
 
9
  # Supabase 설정 (필수)
10
  # SUPABASE_URL=https://your-project.supabase.co
 
16
 
17
  # 기존 FAISS 설정 (VECTOR_DB_TYPE=faiss 인 경우 사용)
18
  EMBEDDING_MODEL=jhgan/ko-sbert-nli
19
+ LLM_MODEL=beomi/Llama-3-Open-Ko-8B
20
+
21
+ # 관리자 보안 (필수)
22
+ ADMIN_PASSWORD=change-me
README.md CHANGED
@@ -164,6 +164,9 @@ SUPABASE_KEY=your-supabase-anon-key
164
 
165
  # 벡터 DB 타입 선택
166
  VECTOR_DB_TYPE=supabase # "supabase" 또는 "faiss"
 
 
 
167
  ```
168
 
169
  ### config.py 주요 설정
@@ -337,4 +340,4 @@ SYSTEM_PROMPT = """
337
 
338
  ---
339
 
340
- **⚠️ 본 챗봇은 보조 도구입니다. 중요한 업무 결정 시 반드시 관련 규정 원본과 담당자의 확인을 받으세요!**
 
164
 
165
  # 벡터 DB 타입 선택
166
  VECTOR_DB_TYPE=supabase # "supabase" 또는 "faiss"
167
+
168
+ # 관리자 보안 (필수)
169
+ ADMIN_PASSWORD=strong-password-here
170
  ```
171
 
172
  ### config.py 주요 설정
 
340
 
341
  ---
342
 
343
+ **⚠️ 본 챗봇은 보조 도구입니다. 중요한 업무 결정 시 반드시 관련 규정 원본과 담당자의 확인을 받으세요!**
app.py CHANGED
@@ -38,7 +38,9 @@ class AdminManager:
38
  """관리자 기능을 담당하는 클래스"""
39
 
40
  def __init__(self):
41
- self.admin_password = os.getenv("ADMIN_PASSWORD", "fireadmin123")
 
 
42
  self.authenticated = False
43
  self.auth_time = None
44
  self.session_timeout = 3600 # 1시간
@@ -214,7 +216,7 @@ class HuggingFaceApp:
214
 
215
  def __init__(self):
216
  # 벡터 DB 타입에 따라 챗봇 선택
217
- if Config.VECTOR_DB_TYPE == "supabase" and OPENAI_AVAILABLE and OpenAIRAGChatbot:
218
  try:
219
  self.chatbot = OpenAIRAGChatbot()
220
  except Exception as e:
@@ -222,8 +224,6 @@ class HuggingFaceApp:
222
  print("FAISS 모드로 전환됩니다.")
223
  self.chatbot = RAGChatbot()
224
  else:
225
- if Config.VECTOR_DB_TYPE == "supabase" and not OPENAI_AVAILABLE:
226
- print("OpenAI 모듈을 사용할 수 없어 FAISS 모드로 실행됩니다.")
227
  self.chatbot = RAGChatbot()
228
  self.is_initialized = False
229
 
@@ -245,6 +245,28 @@ class HuggingFaceApp:
245
  # 앱 초기화
246
  self._initialize_app()
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  def _initialize_app(self):
249
  """앱 초기화"""
250
  try:
@@ -1080,4 +1102,4 @@ def main():
1080
  )
1081
 
1082
  if __name__ == "__main__":
1083
- main()
 
38
  """관리자 기능을 담당하는 클래스"""
39
 
40
  def __init__(self):
41
+ self.admin_password = os.getenv("ADMIN_PASSWORD")
42
+ if not self.admin_password:
43
+ raise RuntimeError("ADMIN_PASSWORD 환경 변수가 설정되지 않았습니다. 관리자 기능 보안을 위해 반드시 설정하세요.")
44
  self.authenticated = False
45
  self.auth_time = None
46
  self.session_timeout = 3600 # 1시간
 
216
 
217
  def __init__(self):
218
  # 벡터 DB 타입에 따라 챗봇 선택
219
+ if self._should_use_supabase():
220
  try:
221
  self.chatbot = OpenAIRAGChatbot()
222
  except Exception as e:
 
224
  print("FAISS 모드로 전환됩니다.")
225
  self.chatbot = RAGChatbot()
226
  else:
 
 
227
  self.chatbot = RAGChatbot()
228
  self.is_initialized = False
229
 
 
245
  # 앱 초기화
246
  self._initialize_app()
247
 
248
+ def _should_use_supabase(self) -> bool:
249
+ """Supabase 경로 사용 여부 판단"""
250
+ has_supabase_env = bool(Config.SUPABASE_URL and Config.SUPABASE_KEY)
251
+ has_openai = bool(Config.OPENAI_API_KEY)
252
+
253
+ if Config.VECTOR_DB_TYPE != "supabase":
254
+ return False
255
+
256
+ if not has_supabase_env:
257
+ print("Supabase 환경 변수가 없어 FAISS 모드로 실행됩니다.")
258
+ return False
259
+
260
+ if not has_openai:
261
+ print("OpenAI API Key가 없어 FAISS 모드로 실행됩니다.")
262
+ return False
263
+
264
+ if not (OPENAI_AVAILABLE and OpenAIRAGChatbot):
265
+ print("OpenAI 모듈을 사용할 수 없어 FAISS 모드로 실행됩니다.")
266
+ return False
267
+
268
+ return True
269
+
270
  def _initialize_app(self):
271
  """앱 초기화"""
272
  try:
 
1102
  )
1103
 
1104
  if __name__ == "__main__":
1105
+ main()
config.py CHANGED
@@ -38,7 +38,8 @@ class Config:
38
  OPENAI_EMBEDDING_MODEL = "text-embedding-3-small"
39
 
40
  # 벡터 DB 타입 설정
41
- VECTOR_DB_TYPE = os.getenv("VECTOR_DB_TYPE", "supabase") # "faiss" 또는 "supabase"
 
42
 
43
  # 시스템 프롬프트
44
  SYSTEM_PROMPT = """
@@ -52,4 +53,4 @@ class Config:
52
  5. 모든 답변은 한국어로 제공
53
 
54
  사용자의 질문에 최대한 상세하고 정확한 정보를 제공하세요.
55
- """
 
38
  OPENAI_EMBEDDING_MODEL = "text-embedding-3-small"
39
 
40
  # 벡터 DB 타입 설정
41
+ # 기본은 로컬 FAISS로 두고, Supabase를 쓰려면 환경 변수를 명시적으로 설정하도록 강제
42
+ VECTOR_DB_TYPE = os.getenv("VECTOR_DB_TYPE", "faiss") # "faiss" 또는 "supabase"
43
 
44
  # 시스템 프롬프트
45
  SYSTEM_PROMPT = """
 
53
  5. 모든 답변은 한국어로 제공
54
 
55
  사용자의 질문에 최대한 상세하고 정확한 정보를 제공하세요.
56
+ """
supabase_vector_store.py CHANGED
@@ -128,6 +128,14 @@ class SupabaseVectorStore:
128
  # 임베딩 생성
129
  texts = [doc.page_content for doc in documents]
130
  embeddings = self.create_embeddings(texts, use_openai)
 
 
 
 
 
 
 
 
131
 
132
  # 문서 데이터 준비
133
  documents_data = []
@@ -321,4 +329,4 @@ def test_supabase_vector_store():
321
  print(f" 내용: {doc.page_content[:100]}...")
322
 
323
  if __name__ == "__main__":
324
- test_supabase_vector_store()
 
128
  # 임베딩 생성
129
  texts = [doc.page_content for doc in documents]
130
  embeddings = self.create_embeddings(texts, use_openai)
131
+ embedding_dim = len(embeddings[0]) if len(embeddings) else 0
132
+
133
+ # Supabase 테이블 스키마가 vector(1536)으로 고정되어 있어 차원 불일치 시 실패 방지
134
+ if embedding_dim and embedding_dim != 1536:
135
+ raise ValueError(
136
+ f"임베딩 차원({embedding_dim})이 Supabase 테이블 vector(1536)와 다릅니다. "
137
+ "OpenAI 임베딩을 사용하거나 테이블 스키마/임베딩 모델을 맞춰주세요."
138
+ )
139
 
140
  # 문서 데이터 준비
141
  documents_data = []
 
329
  print(f" 내용: {doc.page_content[:100]}...")
330
 
331
  if __name__ == "__main__":
332
+ test_supabase_vector_store()
vector_store.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import pickle
 
3
  import numpy as np
4
  from typing import List, Dict, Tuple
5
  from pathlib import Path
@@ -18,6 +19,7 @@ class VectorStore:
18
  self.index = None
19
  self.documents = []
20
  self.doc_ids = []
 
21
 
22
  # 캐시 디렉토리 생성
23
  Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
@@ -59,6 +61,7 @@ class VectorStore:
59
 
60
  # 문서 저장
61
  self.documents = documents
 
62
 
63
  # 텍스트 추출
64
  texts = [doc.page_content for doc in documents]
@@ -128,7 +131,8 @@ class VectorStore:
128
  'documents': self.documents,
129
  'doc_ids': self.doc_ids,
130
  'embedding_model': self.embedding_model_name,
131
- 'total_documents': len(self.documents)
 
132
  }
133
 
134
  with open(metadata_path, 'wb') as f:
@@ -158,6 +162,7 @@ class VectorStore:
158
  self.documents = metadata['documents']
159
  self.doc_ids = metadata['doc_ids']
160
  self.embedding_model_name = metadata.get('embedding_model', Config.EMBEDDING_MODEL)
 
161
 
162
  # 임베딩 모델 로드
163
  self.load_embedding_model()
@@ -184,12 +189,15 @@ class VectorStore:
184
 
185
  def rebuild_if_needed(self, documents: List[Document], force_rebuild: bool = False) -> bool:
186
  """필요시 인덱스 재구축"""
 
 
187
  # 기존 인덱스가 있고 강제 재구축이 없는 경우
188
  if not force_rebuild and self.load_index():
189
- # 문서 개수가 크게 변경되지 않았으면 재사용
190
- if abs(len(self.documents) - len(documents)) / max(len(self.documents), 1) < 0.1:
191
- print("📦 기존 인덱스 재사용")
192
  return True
 
 
193
 
194
  print("🔄 벡터 인덱스 재구축")
195
  return self.build_vector_index(documents)
@@ -214,6 +222,7 @@ class VectorStore:
214
  start_id = len(self.documents)
215
  self.documents.extend(new_documents)
216
  self.doc_ids.extend(range(start_id, start_id + len(new_documents)))
 
217
 
218
  print(f"➕ {len(new_documents)}개 문서 추가 완료")
219
 
@@ -230,6 +239,14 @@ class VectorStore:
230
  remaining_docs = [doc for i, doc in enumerate(self.documents) if i != doc_id]
231
  return self.build_vector_index(remaining_docs)
232
 
 
 
 
 
 
 
 
 
233
  # 테스트용 함수
234
  def test_vector_store():
235
  """벡터 데이터베이스 테스트"""
@@ -263,4 +280,4 @@ def test_vector_store():
263
  print(f" 내용: {doc.page_content[:100]}...")
264
 
265
  if __name__ == "__main__":
266
- test_vector_store()
 
1
  import os
2
  import pickle
3
+ import hashlib
4
  import numpy as np
5
  from typing import List, Dict, Tuple
6
  from pathlib import Path
 
19
  self.index = None
20
  self.documents = []
21
  self.doc_ids = []
22
+ self.documents_hash = None
23
 
24
  # 캐시 디렉토리 생성
25
  Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
 
61
 
62
  # 문서 저장
63
  self.documents = documents
64
+ self.documents_hash = self._compute_documents_hash(documents)
65
 
66
  # 텍스트 추출
67
  texts = [doc.page_content for doc in documents]
 
131
  'documents': self.documents,
132
  'doc_ids': self.doc_ids,
133
  'embedding_model': self.embedding_model_name,
134
+ 'total_documents': len(self.documents),
135
+ 'documents_hash': self.documents_hash
136
  }
137
 
138
  with open(metadata_path, 'wb') as f:
 
162
  self.documents = metadata['documents']
163
  self.doc_ids = metadata['doc_ids']
164
  self.embedding_model_name = metadata.get('embedding_model', Config.EMBEDDING_MODEL)
165
+ self.documents_hash = metadata.get('documents_hash')
166
 
167
  # 임베딩 모델 로드
168
  self.load_embedding_model()
 
189
 
190
  def rebuild_if_needed(self, documents: List[Document], force_rebuild: bool = False) -> bool:
191
  """필요시 인덱스 재구축"""
192
+ new_hash = self._compute_documents_hash(documents)
193
+
194
  # 기존 인덱스가 있고 강제 재구축이 없는 경우
195
  if not force_rebuild and self.load_index():
196
+ if self.documents_hash and self.documents_hash == new_hash:
197
+ print("📦 기존 인덱스 재사용 (문서 해시 일치)")
 
198
  return True
199
+ else:
200
+ print("🔄 문서 변경을 감지하여 인덱스를 재구축합니다.")
201
 
202
  print("🔄 벡터 인덱스 재구축")
203
  return self.build_vector_index(documents)
 
222
  start_id = len(self.documents)
223
  self.documents.extend(new_documents)
224
  self.doc_ids.extend(range(start_id, start_id + len(new_documents)))
225
+ self.documents_hash = self._compute_documents_hash(self.documents)
226
 
227
  print(f"➕ {len(new_documents)}개 문서 추가 완료")
228
 
 
239
  remaining_docs = [doc for i, doc in enumerate(self.documents) if i != doc_id]
240
  return self.build_vector_index(remaining_docs)
241
 
242
+ def _compute_documents_hash(self, documents: List[Document]) -> str:
243
+ """문서 내용과 메타데이터 기반 해시 생성 (내용 변경 감지)"""
244
+ hasher = hashlib.md5()
245
+ for doc in documents:
246
+ hasher.update(doc.page_content.encode("utf-8", errors="ignore"))
247
+ hasher.update(str(doc.metadata).encode("utf-8", errors="ignore"))
248
+ return hasher.hexdigest()
249
+
250
  # 테스트용 함수
251
  def test_vector_store():
252
  """벡터 데이터베이스 테스트"""
 
280
  print(f" 내용: {doc.page_content[:100]}...")
281
 
282
  if __name__ == "__main__":
283
+ test_vector_store()