Muyeong Kim Claude commited on
Commit
21480cd
·
1 Parent(s): adf1fbc

Upgrade to OpenAI + Supabase RAG Chatbot with enhanced capabilities

Browse files

## Major Features Added:
- OpenAI GPT-4o-mini integration for advanced responses
- Supabase pgvector cloud database for scalable vector storage
- Hybrid support: Choose between FAISS (local) or Supabase (cloud)
- OpenAI text-embedding-3-small for improved embeddings
- Environment-based configuration system

## New Files:
- openai_chatbot.py: OpenAI-powered RAG chatbot
- supabase_vector_store.py: Cloud vector database integration
- supabase_setup*.sql: Database setup scripts (3 variants)
- .env.example: Configuration template
- .gitignore: Python and application-specific ignores

## Enhanced Configuration:
- Support for multiple vector database backends
- OpenAI API key and Supabase credentials
- Flexible embedding model selection
- Environment variable-based settings

## Bug Fixes:
- Fixed LangChain import compatibility issues
- Resolved PyMuPDF import problems
- Updated to latest package versions

🚀 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

.gitignore ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # PyInstaller
27
+ *.manifest
28
+ *.spec
29
+
30
+ # Installer logs
31
+ pip-log.txt
32
+ pip-delete-this-directory.txt
33
+
34
+ # Unit test / coverage reports
35
+ htmlcov/
36
+ .tox/
37
+ .nox/
38
+ .coverage
39
+ .coverage.*
40
+ .cache
41
+ nosetests.xml
42
+ coverage.xml
43
+ *.cover
44
+ *.py,cover
45
+ .hypothesis/
46
+ .pytest_cache/
47
+
48
+ # Jupyter Notebook
49
+ .ipynb_checkpoints
50
+
51
+ # IPython
52
+ profile_default/
53
+ ipython_config.py
54
+
55
+ # pyenv
56
+ .python-version
57
+
58
+ # pipenv
59
+ Pipfile.lock
60
+
61
+ # PEP 582
62
+ __pypackages__/
63
+
64
+ # Celery stuff
65
+ celerybeat-schedule
66
+ celerybeat.pid
67
+
68
+ # SageMath parsed files
69
+ *.sage.py
70
+
71
+ # Environments
72
+ .env
73
+ .env.venv
74
+ .env.local
75
+ .env.*.local
76
+
77
+ # Spyder project settings
78
+ .spyderproject
79
+ .spyproject
80
+
81
+ # Rope project settings
82
+ .ropeproject
83
+
84
+ # mkdocs documentation
85
+ /site
86
+
87
+ # mypy
88
+ .mypy_cache/
89
+ .dmypy.json
90
+ dmypy.json
91
+
92
+ # Pyre type checker
93
+ .pyre/
94
+
95
+ # IDEs
96
+ .vscode/
97
+ .idea/
98
+
99
+ # OS
100
+ .DS_Store
101
+ Thumbs.db
102
+
103
+ # Application specific
104
+ faiss_index/
105
+ *.bin
106
+ *.pkl
107
+
108
+ # Vector databases
109
+ faiss_index/
README.md CHANGED
@@ -16,9 +16,9 @@ pinned: false
16
  ## 📋 프로젝트 개요
17
 
18
  - **목적**: 소방업무 복무관리 관련 문서들을 활용한 지능형 질의응답 시스템
19
- - **기술**: RAG, Sentence-BERT, FAISS, Gradio, Hugging Face
20
  - **문서 유형**: PDF, Word, TXT, Excel, CSV
21
- - **특징**: 한국어 최적화, 실시간 검색, 웹 인터페이스
22
 
23
  ## 🎯 주요 기능
24
 
@@ -78,7 +78,28 @@ source venv/bin/activate
78
  pip install -r requirements.txt
79
  ```
80
 
81
- ### 2. 문서 준비
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  ```bash
84
  # documents 폴더 생성
@@ -108,40 +129,72 @@ python app.py
108
 
109
  ```
110
  119_chatbot/
111
- ├── app.py # 허깅페이스 배포용 메인 파일
112
- ├── config.py # 시스템 설정
113
- ├── requirements.txt # 라이브러리 목록
114
- ├── README.md # 프로젝트 설명서
115
- ├── document_processor.py # 문서 처리 모듈
116
- ├── vector_store.py # 벡터 데이터베이스
117
- ├── rag_chatbot.py # RAG 챗봇 핵심 로직
118
- ├── gradio_interface.py # Gradio 인터페이스
119
- ├── documents/ # 문서 폴더
 
 
 
 
120
  │ ├── 복무관리규정.txt
121
  │ ├── 인사평가규정.txt
122
  │ └── ...
123
- └── faiss_index/ # 벡터 인덱스 캐시
124
  ```
125
 
126
  ## ⚙️ 설정 옵션
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  ### config.py 주요 설정
129
 
130
  ```python
131
- # 모델 설정
132
- EMBEDDING_MODEL = "jhgan/ko-sbert-nli" # 한국어 임베딩 모델
133
- LLM_MODEL = "beomi/Llama-3-Open-Ko-8B" # 한국어 LLM
134
-
135
  # RAG 파라미터
136
  CHUNK_SIZE = 500 # 문서 청크 크기
137
  CHUNK_OVERLAP = 50 # 청크 중복 크기
138
  MAX_RETRIEVE_DOCS = 3 # 검색할 문서 수
139
 
 
 
 
 
140
  # 경로 설정
141
  DOCS_FOLDER = "documents" # 문서 폴더
142
  VECTOR_DB_PATH = "faiss_index" # 벡터 DB 경로
143
  ```
144
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  ## 🧪 사용 예시
146
 
147
  ### 기본 질문
 
16
  ## 📋 프로젝트 개요
17
 
18
  - **목적**: 소방업무 복무관리 관련 문서들을 활용한 지능형 질의응답 시스템
19
+ - **기술**: RAG, OpenAI GPT, Supabase pgvector, Sentence-BERT, Gradio
20
  - **문서 유형**: PDF, Word, TXT, Excel, CSV
21
+ - **특징**: 한국어 최적화, 실시간 검색, 클라우드 벡터 DB, 웹 인터페이스
22
 
23
  ## 🎯 주요 기능
24
 
 
78
  pip install -r requirements.txt
79
  ```
80
 
81
+ ### 2. OpenAI API 설정 (권장)
82
+
83
+ ```bash
84
+ # OpenAI API Key 발급: https://platform.openai.com/api-keys
85
+ # .env 파일에 설정
86
+ echo "OPENAI_API_KEY=sk-your-api-key-here" >> .env
87
+ ```
88
+
89
+ ### 3. Supabase 설정 (권장 - 클라우드 벡터 DB)
90
+
91
+ ```bash
92
+ # 1. Supabase 프로젝트 생성: https://supabase.com
93
+ # 2. SQL Editor에서 supabase_setup.sql 실행
94
+ # 3. Settings > API에서 URL과 Key 확인
95
+
96
+ # .env 파일에 Supabase 설정 추가
97
+ echo "SUPABASE_URL=https://your-project.supabase.co" >> .env
98
+ echo "SUPABASE_KEY=your-supabase-anon-key" >> .env
99
+ echo "VECTOR_DB_TYPE=supabase" >> .env
100
+ ```
101
+
102
+ ### 4. 문서 준비
103
 
104
  ```bash
105
  # documents 폴더 생성
 
129
 
130
  ```
131
  119_chatbot/
132
+ ├── app.py # 허깅페이스 배포용 메인 파일
133
+ ├── config.py # 시스템 설정
134
+ ├── requirements.txt # 라이브러리 목록
135
+ ├── README.md # 프로젝트 설명서
136
+ ├── .env.example # 환경 변수 템플릿
137
+ ├── supabase_setup.sql # Supabase 설정 SQL
138
+ ├── document_processor.py # 문서 처리 모듈
139
+ ├── vector_store.py # FAISS 벡터 데이터베이스
140
+ ├── supabase_vector_store.py # Supabase 벡터 데이터베이스
141
+ ├── rag_chatbot.py # 기존 RAG 챗봇
142
+ ├── openai_chatbot.py # OpenAI 기반 RAG 챗봇
143
+ ├── gradio_interface.py # Gradio 웹 인터페이스
144
+ ├── documents/ # 문서 폴더
145
  │ ├── 복무관리규정.txt
146
  │ ├── 인사평가규정.txt
147
  │ └── ...
148
+ └── faiss_index/ # 벡터 인덱스 캐시 (FAISS 사용시)
149
  ```
150
 
151
  ## ⚙️ 설정 옵션
152
 
153
+ ### 환경 변수 설정 (.env 파일)
154
+
155
+ ```bash
156
+ # OpenAI API (필수)
157
+ OPENAI_API_KEY=sk-your-openai-api-key
158
+ OPENAI_MODEL=gpt-4o-mini
159
+ OPENAI_EMBEDDING_MODEL=text-embedding-3-small
160
+
161
+ # Supabase (클라우드 벡터 DB 사용시 필수)
162
+ SUPABASE_URL=https://your-project.supabase.co
163
+ SUPABASE_KEY=your-supabase-anon-key
164
+
165
+ # 벡터 DB 타입 선택
166
+ VECTOR_DB_TYPE=supabase # "supabase" 또는 "faiss"
167
+ ```
168
+
169
  ### config.py 주요 설정
170
 
171
  ```python
 
 
 
 
172
  # RAG 파라미터
173
  CHUNK_SIZE = 500 # 문서 청크 크기
174
  CHUNK_OVERLAP = 50 # 청크 중복 크기
175
  MAX_RETRIEVE_DOCS = 3 # 검색할 문서 수
176
 
177
+ # 기존 로컬 모델 설정 (FAISS 사용시)
178
+ EMBEDDING_MODEL = "jhgan/ko-sbert-nli" # 한국어 임베딩 모델
179
+ LLM_MODEL = "beomi/Llama-3-Open-Ko-8B" # 한국어 LLM
180
+
181
  # 경로 설정
182
  DOCS_FOLDER = "documents" # 문서 폴더
183
  VECTOR_DB_PATH = "faiss_index" # 벡터 DB 경로
184
  ```
185
 
186
+ ### 🔄 벡터 DB 옵션
187
+
188
+ #### 1. Supabase (권장) - 클라우드 벡터 DB
189
+ - **장점**: 클라우드 저장, 확장성, 동시성, 실시간 동기화
190
+ - **필요사항**: OpenAI API Key, Supabase 프로젝트
191
+ - **설정**: `VECTOR_DB_TYPE=supabase`
192
+
193
+ #### 2. FAISS (로컬) - 오프라인 벡터 DB
194
+ - **장점**: 로컬 실행, 무료, 빠른 초기 설정
195
+ - **단점**: 로컬 저장 공간 필요, 확장성 제한
196
+ - **설정**: `VECTOR_DB_TYPE=faiss`
197
+
198
  ## 🧪 사용 예시
199
 
200
  ### 기본 질문
app.py CHANGED
@@ -19,13 +19,18 @@ sys.path.append(current_dir)
19
  # 모듈 임포트
20
  from config import Config
21
  from rag_chatbot import RAGChatbot
 
22
  from document_processor import DocumentProcessor
23
 
24
  class HuggingFaceApp:
25
  """허깅페이스 Spaces 배포용 앱 클래스"""
26
 
27
  def __init__(self):
28
- self.chatbot = RAGChatbot()
 
 
 
 
29
  self.is_initialized = False
30
 
31
  # 예시 질문 (허깅페이스 환경 최적화)
@@ -44,7 +49,7 @@ class HuggingFaceApp:
44
  def _initialize_app(self):
45
  """앱 초기화"""
46
  try:
47
- print("🚀 소방 복무관리 RAG 챗봇 시작 중...")
48
 
49
  # 문서 폴더 확인 및 샘플 데이터 생성
50
  self._ensure_documents()
@@ -53,12 +58,12 @@ class HuggingFaceApp:
53
  success = self.chatbot.initialize()
54
  if success:
55
  self.is_initialized = True
56
- print(" 챗봇 초기화 완료")
57
  else:
58
- print("⚠️ 챗봇 초기화 실패 - 템플릿 모드로 동작")
59
 
60
  except Exception as e:
61
- print(f" 초기화 오류: {str(e)}")
62
  # 오류가 있어도 앱은 계속 실행
63
 
64
  def _ensure_documents(self):
@@ -69,7 +74,7 @@ class HuggingFaceApp:
69
  # 샘플 문서가 없으면 생성
70
  sample_files = list(docs_folder.glob("*.txt"))
71
  if not sample_files:
72
- print("📝 샘플 문서 생성 중...")
73
  self._create_sample_documents()
74
 
75
  def _create_sample_documents(self):
 
19
  # 모듈 임포트
20
  from config import Config
21
  from rag_chatbot import RAGChatbot
22
+ from openai_chatbot import OpenAIRAGChatbot
23
  from document_processor import DocumentProcessor
24
 
25
  class HuggingFaceApp:
26
  """허깅페이스 Spaces 배포용 앱 클래스"""
27
 
28
  def __init__(self):
29
+ # 벡터 DB 타입에 따라 챗봇 선택
30
+ if Config.VECTOR_DB_TYPE == "supabase":
31
+ self.chatbot = OpenAIRAGChatbot()
32
+ else:
33
+ self.chatbot = RAGChatbot()
34
  self.is_initialized = False
35
 
36
  # 예시 질문 (허깅페이스 환경 최적화)
 
49
  def _initialize_app(self):
50
  """앱 초기화"""
51
  try:
52
+ print("Starting Fire Service Management RAG Chatbot...")
53
 
54
  # 문서 폴더 확인 및 샘플 데이터 생성
55
  self._ensure_documents()
 
58
  success = self.chatbot.initialize()
59
  if success:
60
  self.is_initialized = True
61
+ print("Chatbot initialization completed")
62
  else:
63
+ print("Chatbot initialization failed - running in template mode")
64
 
65
  except Exception as e:
66
+ print(f"Initialization error: {str(e)}")
67
  # 오류가 있어도 앱은 계속 실행
68
 
69
  def _ensure_documents(self):
 
74
  # 샘플 문서가 없으면 생성
75
  sample_files = list(docs_folder.glob("*.txt"))
76
  if not sample_files:
77
+ print("Creating sample documents...")
78
  self._create_sample_documents()
79
 
80
  def _create_sample_documents(self):
config.py CHANGED
@@ -28,6 +28,18 @@ class Config:
28
  # 토큰 설정 (필요시)
29
  HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_TOKEN", "")
30
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # 시스템 프롬프트
32
  SYSTEM_PROMPT = """
33
  당신은 소방업무 복무관리 전문가입니다. 복무관리 규정, 인사운영, 근무절차 등에 대한 질문에 정확하고 친절하게 답변해 주세요.
 
28
  # 토큰 설정 (필요시)
29
  HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_TOKEN", "")
30
 
31
+ # Supabase 설정
32
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
33
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
34
+
35
+ # OpenAI 설정
36
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
37
+ OPENAI_MODEL = "gpt-4o-mini" # 또는 "gpt-3.5-turbo"
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 = """
45
  당신은 소방업무 복무관리 전문가입니다. 복무관리 규정, 인사운영, 근무절차 등에 대한 질문에 정확하고 친절하게 답변해 주세요.
document_processor.py CHANGED
@@ -2,11 +2,11 @@ import os
2
  import re
3
  from typing import List, Dict
4
  from pathlib import Path
5
- import PyMuPDF
6
  import docx
7
  import pandas as pd
8
- from langchain.text_splitter import RecursiveCharacterTextSplitter
9
- from langchain.schema import Document
10
 
11
  class DocumentProcessor:
12
  """복무관리 문서 처리 클래스"""
@@ -63,7 +63,7 @@ class DocumentProcessor:
63
  documents = []
64
 
65
  try:
66
- with PyMuPDF.open(file_path) as doc:
67
  full_text = ""
68
 
69
  for page_num in range(len(doc)):
 
2
  import re
3
  from typing import List, Dict
4
  from pathlib import Path
5
+ import fitz # PyMuPDF
6
  import docx
7
  import pandas as pd
8
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
9
+ from langchain_core.documents import Document
10
 
11
  class DocumentProcessor:
12
  """복무관리 문서 처리 클래스"""
 
63
  documents = []
64
 
65
  try:
66
+ with fitz.open(file_path) as doc:
67
  full_text = ""
68
 
69
  for page_num in range(len(doc)):
documents/복무관리규정.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 소방공무원 복무관리 규정
2
+
3
+ 제1장 총칙
4
+ 제1조 (목적)
5
+ 이 규정은 소방공무원의 복무에 관한 기본사항을 규정하여 직무수행의 효율성을 높이고 조직의 발전에 기여함을 목적으로 한다.
6
+
7
+ 제2조 (근무시간)
8
+ 1. 정규근무시간은 09:00부터 18:00까지로 한다.
9
+ 2. 점심시간은 12:00부터 13:00까지로 한다.
10
+ 3. 토요일, 일요일 및 법정공휴일은 휴무일로 한다.
11
+
12
+ 제3조 (연차휴가)
13
+ 1. 연차휴가는 1년간 정상 근무한 자에게 15일을 부여한다.
14
+ 2. 연차휴가 사용 시 3일 전까지 신청서를 제출해야 한다.
15
+ 3. 부서장의 승인을 받아 사용하며, 긴급한 경우에는 사후 승인도 가능하다.
16
+
17
+ 제4조 (당직근무)
18
+ 1. 당직근무는 정규근무시간 외에 수행하는 근무를 말한다.
19
+ 2. 당직자는 비상상황에 대비한 통신장비를 항시 점검해야 한다.
20
+ 3. 당직 중에는 음주를 엄금하며, 직무 수행에 지장이 없는 행위만 가능하다.
openai_chatbot.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ from typing import List, Dict, Tuple
4
+ from dataclasses import dataclass
5
+ import openai
6
+ from supabase_vector_store import SupabaseVectorStore
7
+ from document_processor import DocumentProcessor
8
+ from config import Config
9
+
10
+ @dataclass
11
+ class ChatResponse:
12
+ """챗봇 응답 결과 클래스"""
13
+ answer: str
14
+ sources: List[Dict]
15
+ confidence: float
16
+ response_time: float
17
+
18
+ class OpenAIRAGChatbot:
19
+ """OpenAI 기반 RAG 챗봇"""
20
+
21
+ def __init__(self):
22
+ self.document_processor = DocumentProcessor(
23
+ chunk_size=Config.CHUNK_SIZE,
24
+ chunk_overlap=Config.CHUNK_OVERLAP
25
+ )
26
+ self.vector_store = None
27
+ self.openai_client = None
28
+ self.is_initialized = False
29
+
30
+ def initialize(self, docs_folder: str = None, force_rebuild: bool = False) -> bool:
31
+ """챗봇 초기화"""
32
+ print("🤖 OpenAI 기반 소방 복무관리 RAG 챗봇 초기화 중...")
33
+
34
+ # 1. OpenAI 클라이언트 초기화
35
+ if not Config.OPENAI_API_KEY:
36
+ print("❌ OpenAI API Key가 설정되지 않았습니다.")
37
+ return False
38
+
39
+ self.openai_client = openai.OpenAI(api_key=Config.OPENAI_API_KEY)
40
+
41
+ # 2. 벡터 저장소 초기화
42
+ try:
43
+ self.vector_store = SupabaseVectorStore()
44
+ except Exception as e:
45
+ print(f"❌ 벡터 저장소 초기화 실패: {str(e)}")
46
+ return False
47
+
48
+ # 3. 문서 로드 및 처리
49
+ docs_folder = docs_folder or Config.DOCS_FOLDER
50
+ documents = self._load_documents(docs_folder)
51
+
52
+ if not documents:
53
+ print("❌ 처리할 문서가 없습니다. documents 폴더에 파일을 넣어주세요.")
54
+ return False
55
+
56
+ # 4. 벡터 데이터베이스 구축
57
+ success = self.vector_store.rebuild_index(documents, force_rebuild, use_openai=True)
58
+ if not success:
59
+ print("❌ 벡터 데이터베이스 구축 실패")
60
+ return False
61
+
62
+ self.is_initialized = True
63
+ print("✅ OpenAI RAG 챗봇 초기화 완료")
64
+ return True
65
+
66
+ def _load_documents(self, docs_folder: str) -> List:
67
+ """문서 로드 및 처리"""
68
+ if not os.path.exists(docs_folder):
69
+ print(f"⚠️ 문서 폴더가 존재하지 않습니다: {docs_folder}")
70
+ return []
71
+
72
+ print(f"📂 문서 폴더: {docs_folder}")
73
+ raw_documents = self.document_processor.load_documents_from_folder(docs_folder)
74
+ processed_documents = self.document_processor.process_documents(raw_documents)
75
+
76
+ print(f"✅ 총 {len(processed_documents)}개 문서 청크 생성 완료")
77
+ return processed_documents
78
+
79
+ def search_relevant_docs(self, query: str, k: int = 3) -> List[Tuple]:
80
+ """관련 문서 검색"""
81
+ if not self.is_initialized:
82
+ print("⚠️ 챗봇이 초기화되지 않았습니다.")
83
+ return []
84
+
85
+ # 쿼리 전처리
86
+ processed_query = self._preprocess_query(query)
87
+
88
+ # 벡터 검색
89
+ results = self.vector_store.search_similar(processed_query, k, use_openai=True)
90
+
91
+ # 유사도 필터링
92
+ filtered_results = [
93
+ (doc, similarity) for doc, similarity in results
94
+ if similarity > 0.3 # 최소 유사도 임계값
95
+ ]
96
+
97
+ return filtered_results
98
+
99
+ def _preprocess_query(self, query: str) -> str:
100
+ """쿼리 전처리"""
101
+ import re
102
+
103
+ # 불필요한 공백 제거
104
+ query = re.sub(r'\s+', ' ', query.strip())
105
+
106
+ # 복무관리 관련 키워드 강화
107
+ keyword_mappings = {
108
+ "연차": "연차휴가",
109
+ "휴가": "휴가사용",
110
+ "근무": "근무시간",
111
+ "당직": "당직근무",
112
+ "인사": "인사평가",
113
+ "승진": "승진시험"
114
+ }
115
+
116
+ for keyword, enhanced in keyword_mappings.items():
117
+ if keyword in query and enhanced not in query:
118
+ query = query.replace(keyword, enhanced)
119
+
120
+ return query
121
+
122
+ def generate_answer(self, query: str) -> ChatResponse:
123
+ """질문에 대한 답변 생성 (OpenAI 사용)"""
124
+ start_time = time.time()
125
+
126
+ if not self.is_initialized:
127
+ return ChatResponse(
128
+ answer="죄송합니다. 챗봇이 초기화되지 않았습니다. 관리자에게 문의해주세요.",
129
+ sources=[],
130
+ confidence=0.0,
131
+ response_time=time.time() - start_time
132
+ )
133
+
134
+ # 1. 관련 문서 검색
135
+ relevant_docs = self.search_relevant_docs(query, k=Config.MAX_RETRIEVE_DOCS)
136
+
137
+ if not relevant_docs:
138
+ return ChatResponse(
139
+ answer="죄송합니다. 질문과 관련된 정보를 찾을 수 없습니다. 다른 방식으로 질문해주시거나 관련 부서에 문의해주시기 바랍니다.",
140
+ sources=[],
141
+ confidence=0.0,
142
+ response_time=time.time() - start_time
143
+ )
144
+
145
+ # 2. OpenAI로 답변 생성
146
+ answer = self._generate_openai_answer(query, relevant_docs)
147
+
148
+ # 3. 출처 정보 준비
149
+ sources = [
150
+ {
151
+ "source": doc.metadata.get("source", "알 수 없음"),
152
+ "content": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content,
153
+ "similarity": f"{similarity:.4f}"
154
+ }
155
+ for doc, similarity in relevant_docs
156
+ ]
157
+
158
+ # 4. 신뢰도 계산
159
+ confidence = min(sum(similarity for _, similarity in relevant_docs) / len(relevant_docs), 1.0)
160
+
161
+ return ChatResponse(
162
+ answer=answer,
163
+ sources=sources,
164
+ confidence=confidence,
165
+ response_time=time.time() - start_time
166
+ )
167
+
168
+ def _generate_openai_answer(self, query: str, relevant_docs: List[Tuple]) -> str:
169
+ """OpenAI로 답변 생성"""
170
+ try:
171
+ # 문맥 구성
172
+ context = "\n\n".join([
173
+ f"[출처 {i+1}] {doc.page_content}"
174
+ for i, (doc, _) in enumerate(relevant_docs)
175
+ ])
176
+
177
+ # OpenAI API 호출
178
+ messages = [
179
+ {
180
+ "role": "system",
181
+ "content": f"""{Config.SYSTEM_PROMPT}
182
+
183
+ 답변 시 다음 지침을 따르세요:
184
+ 1. 반드시 아래 참고자료를 기반으로 답변하세요
185
+ 2. 규정 조문이나 구체적인 절차를 명시하세요
186
+ 3. 단계별 설명이 필요한 경우 번호로 구분해서 설명하세요
187
+ 4. 필요한 서류나 양식을 구체적으로 안내하세요
188
+ 5. 주의사항이나 중요 사항은 강조해주세요
189
+ 6. 답변 마지막에 참고한 출처를 표시하세요"""
190
+ },
191
+ {
192
+ "role": "user",
193
+ "content": f"""[참고자료]
194
+ {context}
195
+
196
+ [질문]
197
+ {query}
198
+
199
+ 위 참고자료를 바탕으로 질문에 답변해주세요. 정확하고 친절하게 설명해주세요."""
200
+ }
201
+ ]
202
+
203
+ response = self.openai_client.chat.completions.create(
204
+ model=Config.OPENAI_MODEL,
205
+ messages=messages,
206
+ max_tokens=2000,
207
+ temperature=0.3, # 더 일관된 답변을 위해 낮은 온도
208
+ top_p=0.9
209
+ )
210
+
211
+ answer = response.choices[0].message.content.strip()
212
+ return answer
213
+
214
+ except Exception as e:
215
+ print(f"⚠️ OpenAI 답변 생성 실패: {str(e)}")
216
+ return self._generate_fallback_answer(query, relevant_docs)
217
+
218
+ def _generate_fallback_answer(self, query: str, relevant_docs: List[Tuple]) -> str:
219
+ """OpenAI 실패 시 대체 답변 생성"""
220
+ top_doc, top_similarity = relevant_docs[0]
221
+
222
+ answer = f"""📋 소방 복무관리 안내
223
+
224
+ 질문: {query}
225
+
226
+ 관련 정보:
227
+ {top_doc.page_content[:800]}...
228
+
229
+ 📖 더 자세한 정보는 관련 규정 파일을 확인하시거나 담당 부서에 문의해주시기 바랍니다.
230
+
231
+ *참고자료 유사도: {top_similarity:.2%}*"""
232
+
233
+ return answer
234
+
235
+ def get_stats(self) -> Dict:
236
+ """챗봇 통계 정보"""
237
+ if not self.is_initialized:
238
+ return {"status": "not_initialized"}
239
+
240
+ vector_stats = self.vector_store.get_stats()
241
+
242
+ return {
243
+ "status": "initialized",
244
+ "vector_store": vector_stats,
245
+ "llm_provider": "openai",
246
+ "llm_model": Config.OPENAI_MODEL,
247
+ "embedding_model": Config.OPENAI_EMBEDDING_MODEL
248
+ }
249
+
250
+ def add_documents(self, documents: List) -> bool:
251
+ """새 문서 추가"""
252
+ if not self.is_initialized:
253
+ print("⚠️ 챗봇이 초기화되지 않았습니다.")
254
+ return False
255
+
256
+ return self.vector_store.add_documents(documents, use_openai=True)
257
+
258
+ # 테스트용 함수
259
+ def test_openai_chatbot():
260
+ """OpenAI RAG 챗봇 테스트"""
261
+ # 환경 변수 확인
262
+ if not Config.OPENAI_API_KEY:
263
+ print("❌ OPENAI_API_KEY 환경 변수가 필요합니다.")
264
+ return
265
+
266
+ if not Config.SUPABASE_URL or not Config.SUPABASE_KEY:
267
+ print("❌ SUPABASE_URL, SUPABASE_KEY 환경 변수가 필요합니다.")
268
+ return
269
+
270
+ # 챗봇 초기화
271
+ chatbot = OpenAIRAGChatbot()
272
+ success = chatbot.initialize()
273
+
274
+ if not success:
275
+ return
276
+
277
+ # 테스트 질문
278
+ test_questions = [
279
+ "연차휴가는 어떻게 사용하나요?",
280
+ "정규근무시간은 어떻게 되나요?",
281
+ "당직근무가 무엇인가요?",
282
+ "인사평가 절차가 궁금합니다."
283
+ ]
284
+
285
+ # 질문 테스트
286
+ for question in test_questions:
287
+ print(f"\n❓ 질문: {question}")
288
+ response = chatbot.generate_answer(question)
289
+
290
+ print(f"🤖 답변: {response.answer[:500]}...")
291
+ print(f"📊 신뢰도: {response.confidence:.4f}")
292
+ print(f"⏱️ 응답시간: {response.response_time:.4f}초")
293
+ print(f"📚 출처: {len(response.sources)}개")
294
+
295
+ # 통계 정보
296
+ print(f"\n📈 챗봇 통계: {chatbot.get_stats()}")
297
+
298
+ if __name__ == "__main__":
299
+ test_openai_chatbot()
rag_chatbot.py CHANGED
@@ -4,7 +4,7 @@ from typing import List, Dict, Tuple
4
  from dataclasses import dataclass
5
  import torch
6
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
7
- from langchain.schema import Document
8
  from document_processor import DocumentProcessor
9
  from vector_store import VectorStore
10
  from config import Config
 
4
  from dataclasses import dataclass
5
  import torch
6
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
7
+ from langchain_core.documents import Document
8
  from document_processor import DocumentProcessor
9
  from vector_store import VectorStore
10
  from config import Config
requirements.txt CHANGED
@@ -24,6 +24,10 @@ python-docx>=1.1.0
24
  openpyxl>=3.1.2
25
  PyMuPDF>=1.23.8
26
 
 
 
 
 
27
  # 유틸리티
28
  python-dotenv>=1.0.0
29
  tqdm>=4.66.0
 
24
  openpyxl>=3.1.2
25
  PyMuPDF>=1.23.8
26
 
27
+ # Supabase
28
+ supabase>=2.0.0
29
+ openai>=1.0.0
30
+
31
  # 유틸리티
32
  python-dotenv>=1.0.0
33
  tqdm>=4.66.0
supabase_setup.sql ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Supabase pgvector 설정 SQL
2
+ -- 이 SQL을 Supabase Dashboard > SQL Editor에서 실행하세요
3
+
4
+ -- 1. pgvector 확장 활성화
5
+ CREATE EXTENSION IF NOT EXISTS vector;
6
+
7
+ -- 2. documents 테이블 생성
8
+ CREATE TABLE IF NOT EXISTS documents (
9
+ id SERIAL PRIMARY KEY,
10
+ content TEXT NOT NULL,
11
+ metadata JSONB,
12
+ embedding vector(1536), -- OpenAI text-embedding-3-small 차원
13
+ source_file VARCHAR(255),
14
+ chunk_index INTEGER,
15
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
16
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
17
+ );
18
+
19
+ -- 3. 벡터 유사도 검색을 위한 인덱스 생성
20
+ CREATE INDEX IF NOT EXISTS documents_embedding_idx
21
+ ON documents
22
+ USING ivfflat (embedding vector_cosine_ops)
23
+ WITH (lists = 100);
24
+
25
+ -- 4. 전문 검색을 위한 인덱스 생성 (기본 영어 설정)
26
+ CREATE INDEX IF NOT EXISTS documents_content_idx
27
+ ON documents
28
+ USING gin(to_tsvector('english', content));
29
+
30
+ -- 5. 소스 파일별 검색 인덱스
31
+ CREATE INDEX IF NOT EXISTS documents_source_file_idx
32
+ ON documents (source_file);
33
+
34
+ -- 6. 벡터 유사도 검색 함수 생성
35
+ CREATE OR REPLACE FUNCTION search_similar_documents(
36
+ query_embedding vector(1536),
37
+ match_threshold float DEFAULT 0.5,
38
+ match_count int DEFAULT 10
39
+ )
40
+ RETURNS TABLE (
41
+ id int,
42
+ content text,
43
+ metadata jsonb,
44
+ source_file varchar(255),
45
+ similarity float
46
+ ) AS $$
47
+ BEGIN
48
+ RETURN QUERY
49
+ SELECT
50
+ d.id,
51
+ d.content,
52
+ d.metadata,
53
+ d.source_file,
54
+ 1 - (d.embedding <=> query_embedding) as similarity
55
+ FROM documents d
56
+ WHERE 1 - (d.embedding <=> query_embedding) > match_threshold
57
+ ORDER BY similarity DESC
58
+ LIMIT match_count;
59
+ END;
60
+ $$ LANGUAGE plpgsql;
61
+
62
+ -- 7. 자동 타임스탬프 업데이트 함수
63
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
64
+ RETURNS TRIGGER AS $$
65
+ BEGIN
66
+ NEW.updated_at = NOW();
67
+ RETURN NEW;
68
+ END;
69
+ $$ LANGUAGE plpgsql;
70
+
71
+ -- 8. documents 테이블에 트리거 추가
72
+ CREATE TRIGGER update_documents_updated_at
73
+ BEFORE UPDATE ON documents
74
+ FOR EACH ROW
75
+ EXECUTE FUNCTION update_updated_at_column();
76
+
77
+ -- 9. RLS (Row Level Security) 설정 (선택사항)
78
+ ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
79
+
80
+ -- 10. 읽기 권한 정책 (인증된 사용자에게 허용)
81
+ CREATE POLICY "Enable read access for all authenticated users" ON documents
82
+ FOR SELECT USING (auth.role() = 'authenticated');
83
+
84
+ -- 11. 쓰기 권한 정책 (서비스 롤에게 허용)
85
+ CREATE POLICY "Enable write access for service role" ON documents
86
+ FOR ALL USING (auth.role() = 'service_role')
87
+ WITH CHECK (auth.role() = 'service_role');
88
+
89
+ -- 12. 초기 데이터 확인용 쿼리
90
+ SELECT 'Setup completed successfully!' as status;
91
+
92
+ -- 13. 테이블 정보 확인
93
+ SELECT
94
+ column_name,
95
+ data_type,
96
+ is_nullable
97
+ FROM information_schema.columns
98
+ WHERE table_name = 'documents'
99
+ ORDER BY ordinal_position;
supabase_setup_minimal.sql ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- 가장 기본적인 Supabase 설정
2
+ -- 모든 복잡한 기능을 제거하고 최소한의 기능만 포함
3
+
4
+ -- 1. pgvector 확장 활성화
5
+ CREATE EXTENSION IF NOT EXISTS vector;
6
+
7
+ -- 2. 가장 간단한 documents 테이블 생성
8
+ CREATE TABLE IF NOT EXISTS documents (
9
+ id SERIAL PRIMARY KEY,
10
+ content TEXT NOT NULL,
11
+ metadata JSONB DEFAULT '{}',
12
+ embedding vector(1536), -- OpenAI text-embedding-3-small 차원
13
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
14
+ );
15
+
16
+ -- 3. 성공 메시지
17
+ SELECT 'Minimal Supabase setup completed!' as status;
18
+
19
+ -- 4. 테이블 구조 확인
20
+ SELECT
21
+ column_name,
22
+ data_type
23
+ FROM information_schema.columns
24
+ WHERE table_name = 'documents'
25
+ ORDER BY ordinal_position;
supabase_setup_simple.sql ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- 간단한 Supabase 설정 SQL
2
+ -- 이 버전은 기본 기능만 포함하여 호환성을 높입니다.
3
+
4
+ -- 1. pgvector 확장 활성화
5
+ CREATE EXTENSION IF NOT EXISTS vector;
6
+
7
+ -- 2. documents 테이블 생성 (필수 항목만)
8
+ CREATE TABLE IF NOT EXISTS documents (
9
+ id SERIAL PRIMARY KEY,
10
+ content TEXT NOT NULL,
11
+ metadata JSONB DEFAULT '{}',
12
+ embedding vector(1536), -- OpenAI text-embedding-3-small 차원
13
+ source_file VARCHAR(255),
14
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
15
+ );
16
+
17
+ -- 3. 기본 인덱스 생성 (테이블 생성 후)
18
+ CREATE INDEX IF NOT EXISTS documents_created_at_idx
19
+ ON documents (created_at);
20
+
21
+ -- 4. 벡터 유사도 검색 함수 (가장 간단한 버전)
22
+ CREATE OR REPLACE FUNCTION search_similar_documents(
23
+ query_embedding vector(1536),
24
+ match_count int DEFAULT 10
25
+ )
26
+ RETURNS TABLE (
27
+ id int,
28
+ content text,
29
+ metadata jsonb,
30
+ similarity float
31
+ ) AS $$
32
+ BEGIN
33
+ RETURN QUERY
34
+ SELECT
35
+ d.id,
36
+ d.content,
37
+ d.metadata,
38
+ 1 - (d.embedding <=> query_embedding) as similarity
39
+ FROM documents d
40
+ ORDER BY d.embedding <=> query_embedding
41
+ LIMIT match_count;
42
+ END;
43
+ $$ LANGUAGE plpgsql;
44
+
45
+ -- 5. 성공 메시지
46
+ SELECT 'Supabase setup completed successfully!' as status;
47
+
48
+ -- 6. 테이블 구조 확인
49
+ SELECT
50
+ column_name,
51
+ data_type,
52
+ is_nullable
53
+ FROM information_schema.columns
54
+ WHERE table_name = 'documents'
55
+ ORDER BY ordinal_position;
supabase_vector_store.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ from typing import List, Dict, Tuple, Optional
5
+ from supabase import create_client, Client
6
+ from langchain_core.documents import Document
7
+ from sentence_transformers import SentenceTransformer
8
+ import openai
9
+ from config import Config
10
+
11
+ class SupabaseVectorStore:
12
+ """Supabase pgvector 기반 벡터 데이터베이스 클래스"""
13
+
14
+ def __init__(self, embedding_model: str = None):
15
+ self.embedding_model_name = embedding_model or Config.EMBEDDING_MODEL
16
+ self.model = None
17
+ self.supabase: Optional[Client] = None
18
+ self.table_name = "documents"
19
+
20
+ # Supabase 클라이언트 초기화
21
+ self._init_supabase()
22
+
23
+ def _init_supabase(self):
24
+ """Supabase 클라이언트 초기화"""
25
+ try:
26
+ if not Config.SUPABASE_URL or not Config.SUPABASE_KEY:
27
+ raise ValueError("Supabase URL과 Key가 필요합니다.")
28
+
29
+ self.supabase = create_client(Config.SUPABASE_URL, Config.SUPABASE_KEY)
30
+ print("✅ Supabase 클라이언트 연결 성공")
31
+
32
+ # 테이블이 없으면 생성 (필요시)
33
+ self._create_table_if_not_exists()
34
+
35
+ except Exception as e:
36
+ print(f"❌ Supabase 연결 실패: {str(e)}")
37
+ raise
38
+
39
+ def _create_table_if_not_exists(self):
40
+ """테이블 생성 (SQL 실행 필요시 관리자에서 직접 실행)"""
41
+ # 아래 SQL은 Supabase SQL 에디터에서 직접 실행해야 함
42
+ create_table_sql = f"""
43
+ -- Enable pgvector extension
44
+ CREATE EXTENSION IF NOT EXISTS vector;
45
+
46
+ -- Create documents table
47
+ CREATE TABLE IF NOT EXISTS {self.table_name} (
48
+ id SERIAL PRIMARY KEY,
49
+ content TEXT NOT NULL,
50
+ metadata JSONB,
51
+ embedding vector(1536), -- OpenAI embedding 차원
52
+ source_file VARCHAR(255),
53
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
54
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
55
+ );
56
+
57
+ -- Create index for vector similarity search
58
+ CREATE INDEX IF NOT EXISTS documents_embedding_idx ON {self.table_name}
59
+ USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
60
+
61
+ -- Create full-text search index
62
+ CREATE INDEX IF NOT EXISTS documents_content_idx ON {self.table_name}
63
+ USING gin(to_tsvector('korean', content));
64
+ """
65
+
66
+ print(f"📝 아래 SQL을 Supabase SQL 에디터에서 실행해주세요:")
67
+ print(create_table_sql)
68
+
69
+ def load_embedding_model(self, use_openai: bool = True):
70
+ """임베딩 모델 로드"""
71
+ if self.model is not None:
72
+ return
73
+
74
+ if use_openai and Config.OPENAI_API_KEY:
75
+ print("📥 OpenAI 임베딩 모델 사용")
76
+ self.model = "openai"
77
+ else:
78
+ print(f"📥 임베딩 모델 로드: {self.embedding_model_name}")
79
+ try:
80
+ self.model = SentenceTransformer(self.embedding_model_name)
81
+ print("✅ 임베딩 모델 로드 완료")
82
+ except Exception as e:
83
+ print(f"❌ 임베딩 모델 로드 실패: {str(e)}")
84
+ print("🔄 다국어 모델로 대체 시도...")
85
+ self.model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
86
+
87
+ def create_embeddings(self, texts: List[str], use_openai: bool = True) -> np.ndarray:
88
+ """텍스트 목록에 대한 임베딩 생성"""
89
+ if self.model is None:
90
+ self.load_embedding_model(use_openai)
91
+
92
+ print(f"🔄 {len(texts)}개 텍스트 임베딩 생성 중...")
93
+
94
+ if self.model == "openai" and Config.OPENAI_API_KEY:
95
+ # OpenAI API 사용
96
+ client = openai.OpenAI(api_key=Config.OPENAI_API_KEY)
97
+ embeddings = []
98
+
99
+ for i in range(0, len(texts), 100): # 배치 사이즈 100
100
+ batch_texts = texts[i:i+100]
101
+ response = client.embeddings.create(
102
+ model=Config.OPENAI_EMBEDDING_MODEL,
103
+ input=batch_texts
104
+ )
105
+ batch_embeddings = [item.embedding for item in response.data]
106
+ embeddings.extend(batch_embeddings)
107
+
108
+ return np.array(embeddings)
109
+ else:
110
+ # 로컬 모델 사용
111
+ return self.model.encode(
112
+ texts,
113
+ batch_size=32,
114
+ show_progress_bar=True,
115
+ convert_to_numpy=True,
116
+ normalize_embeddings=True
117
+ )
118
+
119
+ def add_documents(self, documents: List[Document], use_openai: bool = True) -> bool:
120
+ """문서 추가"""
121
+ if not documents:
122
+ print("⚠️ 추가할 문서가 없습니다.")
123
+ return False
124
+
125
+ print(f"📝 {len(documents)}개 문서를 Supabase에 추가 중...")
126
+
127
+ try:
128
+ # 임베딩 생성
129
+ texts = [doc.page_content for doc in documents]
130
+ embeddings = self.create_embeddings(texts, use_openai)
131
+
132
+ # 문서 데이터 준비
133
+ documents_data = []
134
+ for i, doc in enumerate(documents):
135
+ doc_data = {
136
+ 'content': doc.page_content,
137
+ 'metadata': doc.metadata,
138
+ 'embedding': embeddings[i].tolist()
139
+ }
140
+ documents_data.append(doc_data)
141
+
142
+ # 배치로 데이터 삽입
143
+ batch_size = 100
144
+ for i in range(0, len(documents_data), batch_size):
145
+ batch = documents_data[i:i+batch_size]
146
+ result = self.supabase.table(self.table_name).insert(batch).execute()
147
+
148
+ if not result.data:
149
+ print(f"❌ 배치 삽입 실패 (배치 {i//batch_size + 1})")
150
+ return False
151
+
152
+ print(f"✅ {len(documents)}개 문서 추가 완료")
153
+ return True
154
+
155
+ except Exception as e:
156
+ print(f"❌ 문서 추가 실패: {str(e)}")
157
+ return False
158
+
159
+ def search_similar(self, query: str, k: int = 5, use_openai: bool = True) -> List[Tuple[Document, float]]:
160
+ """유사 문서 검색"""
161
+ if self.supabase is None:
162
+ print("⚠️ Supabase 클라이언트가 초기화되지 않았습니다.")
163
+ return []
164
+
165
+ if self.model is None:
166
+ self.load_embedding_model(use_openai)
167
+
168
+ try:
169
+ # 쿼리 임베딩 생성
170
+ if self.model == "openai" and Config.OPENAI_API_KEY:
171
+ client = openai.OpenAI(api_key=Config.OPENAI_API_KEY)
172
+ response = client.embeddings.create(
173
+ model=Config.OPENAI_EMBEDDING_MODEL,
174
+ input=[query]
175
+ )
176
+ query_embedding = response.data[0].embedding
177
+ else:
178
+ query_embedding = self.model.encode([query], normalize_embeddings=True)[0]
179
+ query_embedding = query_embedding.tolist()
180
+
181
+ # 유사도 검색 SQL
182
+ match_threshold = 0.5
183
+ match_count = k
184
+
185
+ search_sql = f"""
186
+ SELECT content, metadata, source_file, 1 - (embedding <=> '[{','.join(map(str, query_embedding))}]') as similarity
187
+ FROM {self.table_name}
188
+ WHERE 1 - (embedding <=> '[{','.join(map(str, query_embedding))}]') > {match_threshold}
189
+ ORDER BY similarity DESC
190
+ LIMIT {match_count}
191
+ """
192
+
193
+ # Supabase RPC 호출
194
+ result = self.supabase.rpc('search_similar_documents', {
195
+ 'query_embedding': query_embedding,
196
+ 'match_threshold': match_threshold,
197
+ 'match_count': match_count
198
+ }).execute()
199
+
200
+ if not result.data:
201
+ # RPC가 없으면 직접 SQL 실행 (권한 필요)
202
+ result = self.supabase.table(self.table_name).select(
203
+ "content, metadata, source_file"
204
+ ).execute()
205
+
206
+ # 클라이언트 측에서 유사도 계산
207
+ if result.data:
208
+ similarities = []
209
+ for row in result.data:
210
+ # 저장된 임베딩이 없으면 스킵
211
+ if not row.get('embedding'):
212
+ continue
213
+ similarity = self._cosine_similarity(query_embedding, row['embedding'])
214
+ if similarity > match_threshold:
215
+ similarities.append((row, similarity))
216
+
217
+ # 유사도로 정렬
218
+ similarities.sort(key=lambda x: x[1], reverse=True)
219
+ result.data = [item[0] for item in similarities[:k]]
220
+
221
+ # 결과 변환
222
+ results = []
223
+ for row in result.data[:k]:
224
+ doc = Document(
225
+ page_content=row['content'],
226
+ metadata=row.get('metadata', {}),
227
+ id=row.get('id')
228
+ )
229
+ similarity = row.get('similarity', 1.0) # 기본값 1.0
230
+ results.append((doc, float(similarity)))
231
+
232
+ return results
233
+
234
+ except Exception as e:
235
+ print(f"❌ 검색 실패: {str(e)}")
236
+ return []
237
+
238
+ def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
239
+ """코사인 유사도 계산"""
240
+ vec1 = np.array(vec1)
241
+ vec2 = np.array(vec2)
242
+
243
+ dot_product = np.dot(vec1, vec2)
244
+ norm1 = np.linalg.norm(vec1)
245
+ norm2 = np.linalg.norm(vec2)
246
+
247
+ if norm1 == 0 or norm2 == 0:
248
+ return 0.0
249
+
250
+ return dot_product / (norm1 * norm2)
251
+
252
+ def delete_all_documents(self) -> bool:
253
+ """모든 문서 삭제"""
254
+ try:
255
+ result = self.supabase.table(self.table_name).delete().execute()
256
+ print("✅ 모든 문서 삭제 완료")
257
+ return True
258
+ except Exception as e:
259
+ print(f"❌ 문서 삭제 실패: {str(e)}")
260
+ return False
261
+
262
+ def get_stats(self) -> Dict:
263
+ """벡터 데이터베이스 통계 정보"""
264
+ try:
265
+ result = self.supabase.table(self.table_name).select("count", count="exact").execute()
266
+ total_docs = result.count if hasattr(result, 'count') else 0
267
+
268
+ return {
269
+ "total_documents": total_docs,
270
+ "embedding_model": self.embedding_model_name,
271
+ "database_type": "supabase",
272
+ "table_name": self.table_name
273
+ }
274
+ except Exception as e:
275
+ print(f"❌ 통계 정보 조회 실패: {str(e)}")
276
+ return {"status": "error", "message": str(e)}
277
+
278
+ def rebuild_index(self, documents: List[Document], force_rebuild: bool = False, use_openai: bool = True) -> bool:
279
+ """인덱스 재구축"""
280
+ if force_rebuild:
281
+ print("🔄 기존 데이터 삭제 후 재구축...")
282
+ self.delete_all_documents()
283
+
284
+ return self.add_documents(documents, use_openai)
285
+
286
+ # 테스트용 함수
287
+ def test_supabase_vector_store():
288
+ """Supabase 벡터 데이터베이스 테스트"""
289
+ from document_processor import DocumentProcessor
290
+
291
+ # 문서 처리
292
+ processor = DocumentProcessor()
293
+ documents = processor.load_documents_from_folder("documents")
294
+
295
+ if not documents:
296
+ print("⚠️ 테스트할 문서가 없습니다.")
297
+ return
298
+
299
+ # 벡터 데이터베이스 생성
300
+ vector_store = SupabaseVectorStore()
301
+
302
+ # 문서 추가
303
+ success = vector_store.add_documents(documents[:5]) # 테스트용으로 5개만
304
+ if not success:
305
+ print("❌ 문서 추가 실패")
306
+ return
307
+
308
+ # 검색 테스트
309
+ test_queries = [
310
+ "연차휴가 사용 방법",
311
+ "근무시간은 어떻게 되나요?",
312
+ "당직근무 절차"
313
+ ]
314
+
315
+ for query in test_queries:
316
+ print(f"\n🔍 검색: {query}")
317
+ results = vector_store.search_similar(query, k=3)
318
+
319
+ for i, (doc, similarity) in enumerate(results):
320
+ print(f" {i+1}. 유사도: {similarity:.4f}")
321
+ print(f" 내용: {doc.page_content[:100]}...")
322
+
323
+ if __name__ == "__main__":
324
+ test_supabase_vector_store()
vector_store.py CHANGED
@@ -5,7 +5,7 @@ from typing import List, Dict, Tuple
5
  from pathlib import Path
6
  from sentence_transformers import SentenceTransformer
7
  import faiss
8
- from langchain.schema import Document
9
  from config import Config
10
 
11
  class VectorStore:
 
5
  from pathlib import Path
6
  from sentence_transformers import SentenceTransformer
7
  import faiss
8
+ from langchain_core.documents import Document
9
  from config import Config
10
 
11
  class VectorStore: