Spaces:
Paused
Paused
Add greeting function to app.py
Browse files- .gitignore +34 -0
- .idea/.gitignore +3 -0
- .idea/RAG3_voice.iml +14 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +7 -0
- .idea/modules.xml +8 -0
- .idea/vcs.xml +7 -0
- app.py +1454 -0
- autorag.log +493 -0
- clova_stt.py +92 -0
- config.py +402 -0
- custom_rag_chain.py +224 -0
- deepseek_utils.py +170 -0
- dir +154 -0
- direct_deepseek.py +306 -0
- fallback_rag_chain.py +230 -0
- optimized_document_processor.py +346 -0
- rag_chain.py +255 -0
- requirements.txt +16 -0
- reranker.py +58 -0
- simple_rag_chain.py +123 -0
- vector_store.py +349 -0
.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 환경 변수
|
2 |
+
.env
|
3 |
+
|
4 |
+
# 캐시 및 임시 파일
|
5 |
+
__pycache__/
|
6 |
+
*.py[cod]
|
7 |
+
*.so
|
8 |
+
.Python
|
9 |
+
env/
|
10 |
+
build/
|
11 |
+
develop-eggs/
|
12 |
+
dist/
|
13 |
+
downloads/
|
14 |
+
eggs/
|
15 |
+
.eggs/
|
16 |
+
lib/
|
17 |
+
lib64/
|
18 |
+
parts/
|
19 |
+
sdist/
|
20 |
+
var/
|
21 |
+
*.egg-info/
|
22 |
+
.installed.cfg
|
23 |
+
*.egg
|
24 |
+
|
25 |
+
# 폴더
|
26 |
+
documents/
|
27 |
+
faiss_index/
|
28 |
+
cached_data/
|
29 |
+
preprocessed_index/
|
30 |
+
**/__pycache__/
|
31 |
+
|
32 |
+
# 프로젝트 특화 파일
|
33 |
+
parts_extraction_cache.json
|
34 |
+
.venv/
|
.idea/.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Default ignored files
|
2 |
+
/shelf/
|
3 |
+
/workspace.xml
|
.idea/RAG3_voice.iml
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<module type="PYTHON_MODULE" version="4">
|
3 |
+
<component name="NewModuleRootManager">
|
4 |
+
<content url="file://$MODULE_DIR$">
|
5 |
+
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
6 |
+
</content>
|
7 |
+
<orderEntry type="jdk" jdkName="Python 3.10 (RAG3_voice)" jdkType="Python SDK" />
|
8 |
+
<orderEntry type="sourceFolder" forTests="false" />
|
9 |
+
</component>
|
10 |
+
<component name="PyDocumentationSettings">
|
11 |
+
<option name="format" value="PLAIN" />
|
12 |
+
<option name="myDocStringFormat" value="Plain" />
|
13 |
+
</component>
|
14 |
+
</module>
|
.idea/inspectionProfiles/profiles_settings.xml
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<component name="InspectionProjectProfileManager">
|
2 |
+
<settings>
|
3 |
+
<option name="USE_PROJECT_PROFILE" value="false" />
|
4 |
+
<version value="1.0" />
|
5 |
+
</settings>
|
6 |
+
</component>
|
.idea/misc.xml
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="Black">
|
4 |
+
<option name="sdkName" value="Python 3.10 (RAG3_voice)" />
|
5 |
+
</component>
|
6 |
+
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (RAG3_voice)" project-jdk-type="Python SDK" />
|
7 |
+
</project>
|
.idea/modules.xml
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="ProjectModuleManager">
|
4 |
+
<modules>
|
5 |
+
<module fileurl="file://$PROJECT_DIR$/.idea/RAG3_voice.iml" filepath="$PROJECT_DIR$/.idea/RAG3_voice.iml" />
|
6 |
+
</modules>
|
7 |
+
</component>
|
8 |
+
</project>
|
.idea/vcs.xml
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="VcsDirectoryMappings">
|
4 |
+
<mapping directory="" vcs="Git" />
|
5 |
+
<mapping directory="$PROJECT_DIR$/RAG3_Voice" vcs="Git" />
|
6 |
+
</component>
|
7 |
+
</project>
|
app.py
ADDED
@@ -0,0 +1,1454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
디버깅을 위한 코드 추가 - 경로 관련 문제 해결
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import time
|
6 |
+
import hashlib
|
7 |
+
import pickle
|
8 |
+
import json
|
9 |
+
import logging
|
10 |
+
import glob
|
11 |
+
from typing import List, Dict, Tuple, Any, Optional
|
12 |
+
from logging.handlers import RotatingFileHandler
|
13 |
+
from pathlib import Path
|
14 |
+
from langchain.schema import Document
|
15 |
+
|
16 |
+
from config import (
|
17 |
+
PDF_DIRECTORY, CACHE_DIRECTORY, CHUNK_SIZE, CHUNK_OVERLAP,
|
18 |
+
LLM_MODEL, LOG_LEVEL, LOG_FILE, print_config, validate_config
|
19 |
+
)
|
20 |
+
from optimized_document_processor import OptimizedDocumentProcessor
|
21 |
+
from vector_store import VectorStore
|
22 |
+
|
23 |
+
import sys
|
24 |
+
print("===== Script starting =====")
|
25 |
+
sys.stdout.flush() # 즉시 출력 강제
|
26 |
+
|
27 |
+
# 주요 함수/메서드 호출 전후에도 디버깅 출력 추가
|
28 |
+
print("Loading config...")
|
29 |
+
sys.stdout.flush()
|
30 |
+
# from config import ... 등의 코드
|
31 |
+
print("Config loaded!")
|
32 |
+
sys.stdout.flush()
|
33 |
+
|
34 |
+
# 로깅 설정 개선
|
35 |
+
def setup_logging():
|
36 |
+
"""애플리케이션 로깅 설정"""
|
37 |
+
# 로그 레벨 설정
|
38 |
+
log_level = getattr(logging, LOG_LEVEL.upper(), logging.INFO)
|
39 |
+
|
40 |
+
# 로그 포맷 설정
|
41 |
+
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
42 |
+
formatter = logging.Formatter(log_format)
|
43 |
+
|
44 |
+
# 루트 로거 설정
|
45 |
+
root_logger = logging.getLogger()
|
46 |
+
root_logger.setLevel(log_level)
|
47 |
+
|
48 |
+
# 핸들러 초기화
|
49 |
+
# 콘솔 핸들러
|
50 |
+
console_handler = logging.StreamHandler()
|
51 |
+
console_handler.setFormatter(formatter)
|
52 |
+
root_logger.addHandler(console_handler)
|
53 |
+
|
54 |
+
# 파일 핸들러 (회전식)
|
55 |
+
try:
|
56 |
+
file_handler = RotatingFileHandler(
|
57 |
+
LOG_FILE,
|
58 |
+
maxBytes=10*1024*1024, # 10 MB
|
59 |
+
backupCount=5
|
60 |
+
)
|
61 |
+
file_handler.setFormatter(formatter)
|
62 |
+
root_logger.addHandler(file_handler)
|
63 |
+
except Exception as e:
|
64 |
+
console_handler.warning(f"로그 파일 설정 실패: {e}, 콘솔 로깅만 사용합니다.")
|
65 |
+
|
66 |
+
return logging.getLogger("AutoRAG")
|
67 |
+
|
68 |
+
# 로거 설정
|
69 |
+
logger = setup_logging()
|
70 |
+
|
71 |
+
# 현재 작업 디렉토리 확인을 위한 디버깅 코드
|
72 |
+
current_dir = os.getcwd()
|
73 |
+
logger.info(f"현재 작업 디렉토리: {current_dir}")
|
74 |
+
|
75 |
+
# 설정된 PDF 디렉토리 확인
|
76 |
+
abs_pdf_dir = os.path.abspath(PDF_DIRECTORY)
|
77 |
+
logger.info(f"설정된 PDF 디렉토리: {PDF_DIRECTORY}")
|
78 |
+
logger.info(f"절대 경로로 변환된 PDF 디렉토리: {abs_pdf_dir}")
|
79 |
+
|
80 |
+
# PDF 디렉토리 존재 확인
|
81 |
+
if os.path.exists(abs_pdf_dir):
|
82 |
+
logger.info(f"PDF 디렉토리가 존재합니다: {abs_pdf_dir}")
|
83 |
+
# 디렉토리 내용 확인
|
84 |
+
pdf_files = glob.glob(os.path.join(abs_pdf_dir, "*.pdf"))
|
85 |
+
logger.info(f"디렉토리 내 PDF 파일 목록: {pdf_files}")
|
86 |
+
else:
|
87 |
+
logger.error(f"PDF 디렉토리가 존재하지 않습니다: {abs_pdf_dir}")
|
88 |
+
# 상위 디렉토리 내용 확인
|
89 |
+
parent_dir = os.path.dirname(abs_pdf_dir)
|
90 |
+
logger.info(f"상위 디렉토리: {parent_dir}")
|
91 |
+
if os.path.exists(parent_dir):
|
92 |
+
dir_contents = os.listdir(parent_dir)
|
93 |
+
logger.info(f"상위 디렉토리 내용: {dir_contents}")
|
94 |
+
|
95 |
+
# 설정 상태 확인
|
96 |
+
logger.info("애플리케이션 설정 검증 중...")
|
97 |
+
config_status = validate_config()
|
98 |
+
if config_status["status"] != "valid":
|
99 |
+
for warning in config_status["warnings"]:
|
100 |
+
logger.warning(f"설정 경고: {warning}")
|
101 |
+
|
102 |
+
|
103 |
+
|
104 |
+
|
105 |
+
# 안전한 임포트
|
106 |
+
try:
|
107 |
+
from rag_chain import RAGChain
|
108 |
+
RAG_CHAIN_AVAILABLE = True
|
109 |
+
print("RAG 체인 모듈 로드 성공!")
|
110 |
+
except ImportError as e:
|
111 |
+
logger.warning(f"RAG 체인 모듈을 로드할 수 없습니다: {e}")
|
112 |
+
RAG_CHAIN_AVAILABLE = False
|
113 |
+
except Exception as e:
|
114 |
+
logger.warning(f"RAG 체인 모듈 로드 중 예상치 못한 오류: {e}")
|
115 |
+
RAG_CHAIN_AVAILABLE = False
|
116 |
+
|
117 |
+
# 폴백 RAG 관련 모듈도 미리 확인
|
118 |
+
try:
|
119 |
+
from fallback_rag_chain import FallbackRAGChain
|
120 |
+
FALLBACK_AVAILABLE = True
|
121 |
+
print("폴백 RAG 체인 모듈 로드 성공!")
|
122 |
+
except ImportError as e:
|
123 |
+
logger.warning(f"폴백 RAG 체인 모듈을 로드할 수 없습니다: {e}")
|
124 |
+
FALLBACK_AVAILABLE = False
|
125 |
+
|
126 |
+
try:
|
127 |
+
from offline_fallback_rag import OfflineFallbackRAG
|
128 |
+
OFFLINE_FALLBACK_AVAILABLE = True
|
129 |
+
print("오프라인 폴백 RAG 모듈 로드 성공!")
|
130 |
+
except ImportError as e:
|
131 |
+
logger.warning(f"오프라인 폴백 RAG 모듈을 로드할 수 없습니다: {e}")
|
132 |
+
OFFLINE_FALLBACK_AVAILABLE = False
|
133 |
+
|
134 |
+
|
135 |
+
class DocumentProcessingError(Exception):
|
136 |
+
"""문서 처리 중 발생하는 예외"""
|
137 |
+
pass
|
138 |
+
|
139 |
+
|
140 |
+
class VectorStoreError(Exception):
|
141 |
+
"""벡터 스토어 작업 중 발생하는 예외"""
|
142 |
+
pass
|
143 |
+
|
144 |
+
|
145 |
+
class RAGInitializationError(Exception):
|
146 |
+
"""RAG 체인 초기화 중 발생하는 예외"""
|
147 |
+
pass
|
148 |
+
|
149 |
+
|
150 |
+
class ConfigurationError(Exception):
|
151 |
+
"""설정 관련 오류"""
|
152 |
+
pass
|
153 |
+
|
154 |
+
|
155 |
+
class AutoRAGChatApp:
|
156 |
+
"""
|
157 |
+
documents 폴더의 PDF 파일을 자동으로 처리하는 RAG 챗봇
|
158 |
+
"""
|
159 |
+
|
160 |
+
def __init__(self):
|
161 |
+
"""
|
162 |
+
RAG 챗봇 애플리케이션 초기화
|
163 |
+
"""
|
164 |
+
try:
|
165 |
+
logger.info("AutoRAGChatApp 초기화 시작")
|
166 |
+
|
167 |
+
# 데이터 디렉토리 정의 (설정에서 가져옴)
|
168 |
+
# 절대 경로로 변환하여 사용
|
169 |
+
self.pdf_directory = os.path.abspath(PDF_DIRECTORY)
|
170 |
+
self.cache_directory = os.path.abspath(CACHE_DIRECTORY)
|
171 |
+
self.index_file = os.path.join(self.cache_directory, "file_index.json")
|
172 |
+
self.chunks_dir = os.path.join(self.cache_directory, "chunks")
|
173 |
+
self.vector_index_dir = os.path.join(self.cache_directory, "vector_index")
|
174 |
+
|
175 |
+
logger.info(f"설정된 PDF 디렉토리 (절대 경로): {self.pdf_directory}")
|
176 |
+
|
177 |
+
# 디렉토리 검증
|
178 |
+
self._verify_pdf_directory()
|
179 |
+
|
180 |
+
# 디렉토리 생성
|
181 |
+
self._ensure_directories_exist()
|
182 |
+
|
183 |
+
logger.info(f"PDF 문서 디렉토리: '{self.pdf_directory}'")
|
184 |
+
logger.info(f"캐시 디렉토리: '{self.cache_directory}'")
|
185 |
+
|
186 |
+
# 컴포넌트 초기화
|
187 |
+
try:
|
188 |
+
self.document_processor = OptimizedDocumentProcessor(
|
189 |
+
chunk_size=CHUNK_SIZE,
|
190 |
+
chunk_overlap=CHUNK_OVERLAP
|
191 |
+
)
|
192 |
+
except Exception as e:
|
193 |
+
logger.error(f"문서 처리기 초기화 실패: {e}")
|
194 |
+
raise DocumentProcessingError(f"문서 처리기 초기화 실패: {str(e)}")
|
195 |
+
|
196 |
+
# 벡터 저장소 초기화
|
197 |
+
try:
|
198 |
+
self.vector_store = VectorStore(use_milvus=False)
|
199 |
+
except Exception as e:
|
200 |
+
logger.error(f"벡터 저장소 초기화 실패: {e}")
|
201 |
+
raise VectorStoreError(f"벡터 저장소 초기화 실패: {str(e)}")
|
202 |
+
|
203 |
+
# 문서 인덱스 로드
|
204 |
+
self.file_index = self._load_file_index()
|
205 |
+
|
206 |
+
# 기본 변수 초기화
|
207 |
+
self.documents = []
|
208 |
+
self.processed_files = []
|
209 |
+
self.is_initialized = False
|
210 |
+
|
211 |
+
# 시작 시 자동으로 문서 로드 및 처리
|
212 |
+
logger.info("문서 자동 로드 및 처리 시작...")
|
213 |
+
self.auto_process_documents()
|
214 |
+
|
215 |
+
logger.info("AutoRAGChatApp 초기화 완료")
|
216 |
+
|
217 |
+
except Exception as e:
|
218 |
+
logger.critical(f"애플리케이션 초기화 중 심각한 오류: {e}", exc_info=True)
|
219 |
+
# 기본 상태 설정으로 최소한의 기능 유지
|
220 |
+
self.pdf_directory = os.path.abspath(PDF_DIRECTORY)
|
221 |
+
self.documents = []
|
222 |
+
self.processed_files = []
|
223 |
+
self.is_initialized = False
|
224 |
+
self.file_index = {}
|
225 |
+
|
226 |
+
def _ensure_directories_exist(self) -> None:
|
227 |
+
"""
|
228 |
+
필요한 디렉토리가 존재하는지 확인하고 생성
|
229 |
+
"""
|
230 |
+
directories = [
|
231 |
+
self.pdf_directory,
|
232 |
+
self.cache_directory,
|
233 |
+
self.chunks_dir,
|
234 |
+
self.vector_index_dir
|
235 |
+
]
|
236 |
+
|
237 |
+
for directory in directories:
|
238 |
+
try:
|
239 |
+
os.makedirs(directory, exist_ok=True)
|
240 |
+
except Exception as e:
|
241 |
+
logger.error(f"디렉토리 생성 실패 '{directory}': {e}")
|
242 |
+
raise OSError(f"디렉토리 생성 실패 '{directory}': {str(e)}")
|
243 |
+
|
244 |
+
def _process_pdf_file(self, file_path: str) -> List[Document]:
|
245 |
+
"""
|
246 |
+
PDF 파일 처리 - docling 실패 시 PyPDFLoader 사용
|
247 |
+
|
248 |
+
Args:
|
249 |
+
file_path: 처리할 PDF 파일 경로
|
250 |
+
|
251 |
+
Returns:
|
252 |
+
처리된 문서 청크 리스트
|
253 |
+
"""
|
254 |
+
if not os.path.exists(file_path):
|
255 |
+
logger.error(f"파일이 존재하지 않음: {file_path}")
|
256 |
+
raise FileNotFoundError(f"파일이 존재하지 않음: {file_path}")
|
257 |
+
|
258 |
+
try:
|
259 |
+
logger.info(f"docling으로 처리 시도: {file_path}")
|
260 |
+
|
261 |
+
# docling 사용 시도
|
262 |
+
try:
|
263 |
+
# 10초 타임아웃 설정 (옵션)
|
264 |
+
import signal
|
265 |
+
|
266 |
+
def timeout_handler(signum, frame):
|
267 |
+
raise TimeoutError("docling 처리 시간 초과 (60초)")
|
268 |
+
|
269 |
+
# 리눅스/맥에서만 작동 (윈도우에서는 무시됨)
|
270 |
+
try:
|
271 |
+
signal.signal(signal.SIGALRM, timeout_handler)
|
272 |
+
signal.alarm(60) # 60초 타임아웃
|
273 |
+
except (AttributeError, ValueError) as se:
|
274 |
+
logger.warning(f"시그널 설정 실패 (윈도우 환경일 수 있음): {se}")
|
275 |
+
|
276 |
+
# docling으로 처리 시도
|
277 |
+
chunks = self.document_processor.process_pdf(file_path, use_docling=True)
|
278 |
+
|
279 |
+
# 타임아웃 취소
|
280 |
+
try:
|
281 |
+
signal.alarm(0)
|
282 |
+
except (AttributeError, ValueError):
|
283 |
+
pass
|
284 |
+
|
285 |
+
return chunks
|
286 |
+
|
287 |
+
except TimeoutError as te:
|
288 |
+
logger.warning(f"docling 처리 시간 초과: {te}")
|
289 |
+
logger.info("PyPDFLoader로 대체합니다.")
|
290 |
+
|
291 |
+
# PyPDFLoader로 대체
|
292 |
+
try:
|
293 |
+
return self.document_processor.process_pdf(file_path, use_docling=False)
|
294 |
+
except Exception as inner_e:
|
295 |
+
logger.error(f"PyPDFLoader 처리 오류: {inner_e}", exc_info=True)
|
296 |
+
raise DocumentProcessingError(f"PDF 로딩 실패 (PyPDFLoader): {str(inner_e)}")
|
297 |
+
|
298 |
+
except Exception as e:
|
299 |
+
# docling 오류 확인
|
300 |
+
error_str = str(e)
|
301 |
+
if "Invalid code point" in error_str or "RuntimeError" in error_str:
|
302 |
+
logger.warning(f"docling 처리 오류 (코드 포인트 문제): {error_str}")
|
303 |
+
logger.info("PyPDFLoader로 대체합니다.")
|
304 |
+
else:
|
305 |
+
logger.warning(f"docling 처리 오류: {error_str}")
|
306 |
+
logger.info("PyPDFLoader로 대체합니다.")
|
307 |
+
|
308 |
+
# PyPDFLoader로 대체
|
309 |
+
try:
|
310 |
+
return self.document_processor.process_pdf(file_path, use_docling=False)
|
311 |
+
except Exception as inner_e:
|
312 |
+
logger.error(f"PyPDFLoader 처리 오류: {inner_e}", exc_info=True)
|
313 |
+
raise DocumentProcessingError(f"PDF 로딩 실패 (PyPDFLoader): {str(inner_e)}")
|
314 |
+
|
315 |
+
except DocumentProcessingError:
|
316 |
+
# 이미 래핑된 예외는 그대로 전달
|
317 |
+
raise
|
318 |
+
except Exception as e:
|
319 |
+
logger.error(f"PDF 처리 중 심각한 오류: {e}", exc_info=True)
|
320 |
+
# 빈 청크라도 반환하여 전체 처리가 중단되지 않도록 함
|
321 |
+
logger.warning(f"'{file_path}' 처리 실패로 빈 청크 목록 반환")
|
322 |
+
return []
|
323 |
+
|
324 |
+
def _load_file_index(self) -> Dict[str, Dict[str, Any]]:
|
325 |
+
"""
|
326 |
+
파일 인덱스 로드
|
327 |
+
|
328 |
+
Returns:
|
329 |
+
파일 경로 -> 메타데이터 매핑
|
330 |
+
"""
|
331 |
+
if os.path.exists(self.index_file):
|
332 |
+
try:
|
333 |
+
with open(self.index_file, 'r', encoding='utf-8') as f:
|
334 |
+
return json.load(f)
|
335 |
+
except json.JSONDecodeError as e:
|
336 |
+
logger.error(f"인덱스 파일 JSON 파싱 실패: {e}")
|
337 |
+
logger.warning("손상된 인덱스 파일, 새로운 인덱스를 생성합니다.")
|
338 |
+
return {}
|
339 |
+
except Exception as e:
|
340 |
+
logger.error(f"인덱스 파일 로드 실패: {e}")
|
341 |
+
return {}
|
342 |
+
return {}
|
343 |
+
|
344 |
+
def _save_file_index(self) -> None:
|
345 |
+
"""
|
346 |
+
파일 인덱스 저장
|
347 |
+
"""
|
348 |
+
try:
|
349 |
+
with open(self.index_file, 'w', encoding='utf-8') as f:
|
350 |
+
json.dump(self.file_index, f, ensure_ascii=False, indent=2)
|
351 |
+
logger.debug("파일 인덱스 저장 완료")
|
352 |
+
except Exception as e:
|
353 |
+
logger.error(f"파일 인덱스 저장 실패: {e}")
|
354 |
+
raise IOError(f"파일 인덱스 저장 실패: {str(e)}")
|
355 |
+
|
356 |
+
def _calculate_file_hash(self, file_path: str) -> str:
|
357 |
+
"""
|
358 |
+
파일 해시 계산
|
359 |
+
|
360 |
+
Args:
|
361 |
+
file_path: 파일 경로
|
362 |
+
|
363 |
+
Returns:
|
364 |
+
MD5 해시값
|
365 |
+
"""
|
366 |
+
if not os.path.exists(file_path):
|
367 |
+
logger.error(f"해시 계산 실패 - 파일이 존재하지 않음: {file_path}")
|
368 |
+
raise FileNotFoundError(f"파일이 존재하지 않음: {file_path}")
|
369 |
+
|
370 |
+
try:
|
371 |
+
hasher = hashlib.md5()
|
372 |
+
with open(file_path, 'rb') as f:
|
373 |
+
buf = f.read(65536)
|
374 |
+
while len(buf) > 0:
|
375 |
+
hasher.update(buf)
|
376 |
+
buf = f.read(65536)
|
377 |
+
return hasher.hexdigest()
|
378 |
+
except Exception as e:
|
379 |
+
logger.error(f"파일 해시 계산 중 오류: {e}")
|
380 |
+
raise IOError(f"파일 해시 계산 실패: {str(e)}")
|
381 |
+
|
382 |
+
def _is_file_processed(self, file_path: str) -> bool:
|
383 |
+
"""
|
384 |
+
파일이 이미 처리되었고 변경되지 않았는지 확인
|
385 |
+
|
386 |
+
Args:
|
387 |
+
file_path: 파일 경로
|
388 |
+
|
389 |
+
Returns:
|
390 |
+
처리 여부
|
391 |
+
"""
|
392 |
+
# 파일 존재 확인
|
393 |
+
if not os.path.exists(file_path):
|
394 |
+
logger.warning(f"파일이 존재하지 않음: {file_path}")
|
395 |
+
return False
|
396 |
+
|
397 |
+
# 인덱스에 파일 존재 여부 확인
|
398 |
+
if file_path not in self.file_index:
|
399 |
+
return False
|
400 |
+
|
401 |
+
try:
|
402 |
+
# 현재 해시값 계산
|
403 |
+
current_hash = self._calculate_file_hash(file_path)
|
404 |
+
|
405 |
+
# 저장된 해시값과 비교
|
406 |
+
if self.file_index[file_path]['hash'] != current_hash:
|
407 |
+
logger.info(f"파일 변경 감지: {file_path}")
|
408 |
+
return False
|
409 |
+
|
410 |
+
# 청크 파일 존재 확인
|
411 |
+
chunks_path = self.file_index[file_path]['chunks_path']
|
412 |
+
if not os.path.exists(chunks_path):
|
413 |
+
logger.warning(f"청크 파일이 존재하지 않음: {chunks_path}")
|
414 |
+
return False
|
415 |
+
|
416 |
+
return True
|
417 |
+
except Exception as e:
|
418 |
+
logger.error(f"파일 처리 상태 확인 중 오류: {e}")
|
419 |
+
return False
|
420 |
+
|
421 |
+
def _get_chunks_path(self, file_hash: str) -> str:
|
422 |
+
"""
|
423 |
+
청크 파일 경로 생성
|
424 |
+
|
425 |
+
Args:
|
426 |
+
file_hash: 파일 해시값
|
427 |
+
|
428 |
+
Returns:
|
429 |
+
청크 파일 경로
|
430 |
+
"""
|
431 |
+
return os.path.join(self.chunks_dir, f"{file_hash}.pkl")
|
432 |
+
|
433 |
+
def _save_chunks(self, file_path: str, chunks: List[Document]) -> None:
|
434 |
+
"""
|
435 |
+
청크 데이터 저장
|
436 |
+
|
437 |
+
Args:
|
438 |
+
file_path: 원본 파일 경로
|
439 |
+
chunks: 문서 청크 리스트
|
440 |
+
"""
|
441 |
+
try:
|
442 |
+
# 해시 계산
|
443 |
+
file_hash = self._calculate_file_hash(file_path)
|
444 |
+
|
445 |
+
# 청크 파일 경로
|
446 |
+
chunks_path = self._get_chunks_path(file_hash)
|
447 |
+
|
448 |
+
# 청크 데이터 저장
|
449 |
+
with open(chunks_path, 'wb') as f:
|
450 |
+
pickle.dump(chunks, f)
|
451 |
+
|
452 |
+
# 인덱스 업데이트
|
453 |
+
self.file_index[file_path] = {
|
454 |
+
'hash': file_hash,
|
455 |
+
'chunks_path': chunks_path,
|
456 |
+
'last_processed': time.time(),
|
457 |
+
'chunks_count': len(chunks),
|
458 |
+
'file_size': os.path.getsize(file_path),
|
459 |
+
'file_name': os.path.basename(file_path)
|
460 |
+
}
|
461 |
+
|
462 |
+
# 인덱스 저장
|
463 |
+
self._save_file_index()
|
464 |
+
|
465 |
+
logger.info(f"청크 저장 완료: {file_path} ({len(chunks)}개 청크)")
|
466 |
+
except Exception as e:
|
467 |
+
logger.error(f"청크 저장 실패: {e}", exc_info=True)
|
468 |
+
raise IOError(f"청크 저장 실패: {str(e)}")
|
469 |
+
|
470 |
+
def _load_chunks(self, file_path: str) -> List[Document]:
|
471 |
+
"""
|
472 |
+
저장된 청크 데이터 로드
|
473 |
+
|
474 |
+
Args:
|
475 |
+
file_path: 파일 경로
|
476 |
+
|
477 |
+
Returns:
|
478 |
+
문서 청크 리스트
|
479 |
+
"""
|
480 |
+
if file_path not in self.file_index:
|
481 |
+
logger.error(f"인덱스에 파일이 존재하지 않음: {file_path}")
|
482 |
+
raise KeyError(f"인덱스에 파일이 존재하지 않음: {file_path}")
|
483 |
+
|
484 |
+
chunks_path = self.file_index[file_path]['chunks_path']
|
485 |
+
|
486 |
+
if not os.path.exists(chunks_path):
|
487 |
+
logger.error(f"청크 파일이 존재하지 않음: {chunks_path}")
|
488 |
+
raise FileNotFoundError(f"청크 파일이 존재하지 않음: {chunks_path}")
|
489 |
+
|
490 |
+
try:
|
491 |
+
with open(chunks_path, 'rb') as f:
|
492 |
+
chunks = pickle.load(f)
|
493 |
+
|
494 |
+
logger.info(f"청크 로드 완료: {file_path} ({len(chunks)}개 청크)")
|
495 |
+
return chunks
|
496 |
+
except pickle.UnpicklingError as e:
|
497 |
+
logger.error(f"청크 파일 역직렬화 실패: {e}")
|
498 |
+
raise IOError(f"청크 파일 손상: {str(e)}")
|
499 |
+
except Exception as e:
|
500 |
+
logger.error(f"청크 로드 실패: {e}", exc_info=True)
|
501 |
+
raise IOError(f"청크 로드 실패: {str(e)}")
|
502 |
+
|
503 |
+
def _verify_pdf_directory(self):
|
504 |
+
"""PDF 디렉토리 검증 및 파일 존재 확인"""
|
505 |
+
try:
|
506 |
+
# 디렉토리 존재 확인
|
507 |
+
if not os.path.exists(self.pdf_directory):
|
508 |
+
try:
|
509 |
+
logger.warning(f"PDF 디렉토리가 존재하지 않아 생성합니다: {self.pdf_directory}")
|
510 |
+
os.makedirs(self.pdf_directory, exist_ok=True)
|
511 |
+
except Exception as e:
|
512 |
+
logger.error(f"PDF 디렉토리 생성 실패: {e}")
|
513 |
+
raise
|
514 |
+
|
515 |
+
# 디렉토리인지 확인
|
516 |
+
if not os.path.isdir(self.pdf_directory):
|
517 |
+
logger.error(f"PDF 경로가 디렉토리가 아닙니다: {self.pdf_directory}")
|
518 |
+
raise ConfigurationError(f"PDF 경로가 디렉토리가 아닙니다: {self.pdf_directory}")
|
519 |
+
|
520 |
+
# PDF 파일 존재 확인
|
521 |
+
pdf_files = [f for f in os.listdir(self.pdf_directory) if f.lower().endswith('.pdf')]
|
522 |
+
|
523 |
+
if pdf_files:
|
524 |
+
logger.info(f"PDF 디렉토리에서 {len(pdf_files)}개의 PDF 파일을 찾았습니다: {pdf_files}")
|
525 |
+
else:
|
526 |
+
# 여러 경로에서 PDF 파일 탐색 시도
|
527 |
+
alternative_paths = [
|
528 |
+
"./documents",
|
529 |
+
"../documents",
|
530 |
+
"documents",
|
531 |
+
os.path.join(os.getcwd(), "documents")
|
532 |
+
]
|
533 |
+
|
534 |
+
found_pdfs = False
|
535 |
+
for alt_path in alternative_paths:
|
536 |
+
if os.path.exists(alt_path) and os.path.isdir(alt_path):
|
537 |
+
alt_pdf_files = [f for f in os.listdir(alt_path) if f.lower().endswith('.pdf')]
|
538 |
+
if alt_pdf_files:
|
539 |
+
logger.warning(f"대체 경로 '{alt_path}'에서 PDF 파일을 찾았습니다. 이 경로를 사용합니다.")
|
540 |
+
self.pdf_directory = os.path.abspath(alt_path)
|
541 |
+
found_pdfs = True
|
542 |
+
break
|
543 |
+
|
544 |
+
if not found_pdfs:
|
545 |
+
logger.warning(f"PDF 디렉토리에 PDF 파일이 없습니다: {self.pdf_directory}")
|
546 |
+
logger.info("PDF 파일을 디렉토리에 추가해주세요.")
|
547 |
+
|
548 |
+
except Exception as e:
|
549 |
+
logger.error(f"PDF 디렉토리 검증 중 오류: {e}", exc_info=True)
|
550 |
+
raise
|
551 |
+
|
552 |
+
def auto_process_documents(self) -> str:
|
553 |
+
"""
|
554 |
+
documents 폴더의 PDF 파일 자동 처리
|
555 |
+
|
556 |
+
Returns:
|
557 |
+
처리 결과 메시지
|
558 |
+
"""
|
559 |
+
try:
|
560 |
+
start_time = time.time()
|
561 |
+
|
562 |
+
# PDF 파일 목록 수집을 개선하여 다양한 경로 처리
|
563 |
+
try:
|
564 |
+
pdf_files = []
|
565 |
+
|
566 |
+
# 설정된 디렉토리에서 PDF 파일 찾기
|
567 |
+
logger.info(f"PDF 파일 검색 경로: {self.pdf_directory}")
|
568 |
+
|
569 |
+
if os.path.exists(self.pdf_directory) and os.path.isdir(self.pdf_directory):
|
570 |
+
# 디렉토리 내용 출력 (디버깅용)
|
571 |
+
dir_contents = os.listdir(self.pdf_directory)
|
572 |
+
logger.info(f"디렉토리 내용: {dir_contents}")
|
573 |
+
|
574 |
+
# PDF 파일만 필터링
|
575 |
+
for filename in os.listdir(self.pdf_directory):
|
576 |
+
if filename.lower().endswith('.pdf'):
|
577 |
+
file_path = os.path.join(self.pdf_directory, filename)
|
578 |
+
if os.path.isfile(file_path): # 실제 파일인지 확인
|
579 |
+
pdf_files.append(file_path)
|
580 |
+
logger.info(f"PDF 파일 찾음: {file_path}")
|
581 |
+
|
582 |
+
# 발견된 모든 파일 로그
|
583 |
+
logger.info(f"발견된 모든 PDF 파일: {pdf_files}")
|
584 |
+
|
585 |
+
except FileNotFoundError:
|
586 |
+
logger.error(f"PDF 디렉토리를 찾을 수 없음: {self.pdf_directory}")
|
587 |
+
return f"'{self.pdf_directory}' 디렉토리를 찾을 수 없습니다. 디렉토리가 존재하는지 확인하세요."
|
588 |
+
except PermissionError:
|
589 |
+
logger.error(f"PDF 디렉토리 접근 권한 없음: {self.pdf_directory}")
|
590 |
+
return f"'{self.pdf_directory}' 디렉토리에 접근할 수 없습니다. 권한을 확인하세요."
|
591 |
+
|
592 |
+
if not pdf_files:
|
593 |
+
logger.warning(f"'{self.pdf_directory}' 폴더에 PDF 파일이 없습니다.")
|
594 |
+
return f"'{self.pdf_directory}' 폴더에 PDF 파일이 없습니다."
|
595 |
+
|
596 |
+
logger.info(f"발견된 PDF 파일: {len(pdf_files)}개")
|
597 |
+
|
598 |
+
# 폴더 내 PDF 파일 처리
|
599 |
+
new_files = []
|
600 |
+
updated_files = []
|
601 |
+
cached_files = []
|
602 |
+
failed_files = []
|
603 |
+
all_chunks = []
|
604 |
+
|
605 |
+
for file_path in pdf_files:
|
606 |
+
try:
|
607 |
+
if self._is_file_processed(file_path):
|
608 |
+
# 캐시에서 청크 로드
|
609 |
+
try:
|
610 |
+
chunks = self._load_chunks(file_path)
|
611 |
+
all_chunks.extend(chunks)
|
612 |
+
cached_files.append(file_path)
|
613 |
+
self.processed_files.append(os.path.basename(file_path))
|
614 |
+
except Exception as e:
|
615 |
+
logger.error(f"캐시된 청크 로드 실패: {e}")
|
616 |
+
# 파일을 다시 처리
|
617 |
+
logger.info(f"캐시 실패로 파일 재처리: {file_path}")
|
618 |
+
chunks = self._process_pdf_file(file_path)
|
619 |
+
if chunks:
|
620 |
+
self._save_chunks(file_path, chunks)
|
621 |
+
all_chunks.extend(chunks)
|
622 |
+
updated_files.append(file_path)
|
623 |
+
self.processed_files.append(os.path.basename(file_path))
|
624 |
+
else:
|
625 |
+
failed_files.append(file_path)
|
626 |
+
else:
|
627 |
+
# 새 파일 또는 변경된 파일 처리
|
628 |
+
logger.info(f"처리 중: {file_path}")
|
629 |
+
|
630 |
+
try:
|
631 |
+
# 개선된 PDF 처리 메서드 사용
|
632 |
+
chunks = self._process_pdf_file(file_path)
|
633 |
+
|
634 |
+
if chunks: # 청크가 있는 경우에만 저장
|
635 |
+
# 청크 저장
|
636 |
+
self._save_chunks(file_path, chunks)
|
637 |
+
|
638 |
+
all_chunks.extend(chunks)
|
639 |
+
if file_path in self.file_index:
|
640 |
+
updated_files.append(file_path)
|
641 |
+
else:
|
642 |
+
new_files.append(file_path)
|
643 |
+
|
644 |
+
self.processed_files.append(os.path.basename(file_path))
|
645 |
+
else:
|
646 |
+
logger.warning(f"'{file_path}' 처리 실패: 추출된 청크 없음")
|
647 |
+
failed_files.append(file_path)
|
648 |
+
except Exception as e:
|
649 |
+
logger.error(f"'{file_path}' 처리 중 오류: {e}", exc_info=True)
|
650 |
+
failed_files.append(file_path)
|
651 |
+
except Exception as e:
|
652 |
+
logger.error(f"'{file_path}' 파일 처리 루프 중 오류: {e}", exc_info=True)
|
653 |
+
failed_files.append(file_path)
|
654 |
+
|
655 |
+
# 모든 청크 저장
|
656 |
+
self.documents = all_chunks
|
657 |
+
|
658 |
+
processing_time = time.time() - start_time
|
659 |
+
logger.info(f"문서 처리 완료: {len(all_chunks)}개 청크, {processing_time:.2f}초")
|
660 |
+
|
661 |
+
# 벡터 인덱스 처리
|
662 |
+
try:
|
663 |
+
self._process_vector_index(new_files, updated_files)
|
664 |
+
except Exception as e:
|
665 |
+
logger.error(f"벡터 인덱스 처리 실패: {e}", exc_info=True)
|
666 |
+
return f"문서는 처리되었으나 벡터 인덱스 생성에 실패했습니다: {str(e)}"
|
667 |
+
|
668 |
+
# RAG 체인 초기화
|
669 |
+
if RAG_CHAIN_AVAILABLE:
|
670 |
+
try:
|
671 |
+
logger.info("RAGChain으로 초기화를 시도합니다.")
|
672 |
+
self.rag_chain = RAGChain(self.vector_store)
|
673 |
+
self.is_initialized = True
|
674 |
+
logger.info("RAG 체인 초기화 성공")
|
675 |
+
except Exception as e:
|
676 |
+
logger.error(f"RAG 체인 초기화 실패: {e}", exc_info=True)
|
677 |
+
|
678 |
+
# FallbackRAGChain으로 대체 시도
|
679 |
+
try:
|
680 |
+
logger.info("FallbackRAGChain으로 대체합니다...")
|
681 |
+
from fallback_rag_chain import FallbackRAGChain
|
682 |
+
self.rag_chain = FallbackRAGChain(self.vector_store)
|
683 |
+
self.is_initialized = True
|
684 |
+
logger.info("폴백 RAG 체인 초기화 성공")
|
685 |
+
except Exception as fallback_e:
|
686 |
+
logger.error(f"폴백 RAG 체인 초기화 실패: {fallback_e}", exc_info=True)
|
687 |
+
|
688 |
+
# SimpleRAGChain 시도 (최후의 수단)
|
689 |
+
try:
|
690 |
+
logger.info("SimpleRAGChain으로 대체합니다...")
|
691 |
+
from simple_rag_chain import SimpleRAGChain
|
692 |
+
|
693 |
+
# API 정보 가져오기
|
694 |
+
try:
|
695 |
+
from config import DEEPSEEK_API_KEY, DEEPSEEK_MODEL, DEEPSEEK_ENDPOINT
|
696 |
+
logger.info(f"설정 파일에서 DeepSeek API 정보를 로드했습니다: 모델={DEEPSEEK_MODEL}")
|
697 |
+
except ImportError:
|
698 |
+
# 설정 파일에서 가져올 수 없는 경우 환경 변수 확인
|
699 |
+
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
700 |
+
DEEPSEEK_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
|
701 |
+
DEEPSEEK_ENDPOINT = os.environ.get("DEEPSEEK_ENDPOINT",
|
702 |
+
"https://api.deepseek.com/v1/chat/completions")
|
703 |
+
logger.info(f"환경 변수에서 DeepSeek API 정보를 로드했습니다: 모델={DEEPSEEK_MODEL}")
|
704 |
+
|
705 |
+
# SimpleRAGChain 초기화 시도
|
706 |
+
self.rag_chain = SimpleRAGChain(self.vector_store)
|
707 |
+
self.is_initialized = True
|
708 |
+
logger.info("SimpleRAGChain 초기화 성공")
|
709 |
+
except Exception as simple_e:
|
710 |
+
logger.error(f"모든 RAG 체인 초기화 실패: {simple_e}", exc_info=True)
|
711 |
+
return f"문서와 벡터 인덱스는 처리되었으나 RAG 체인 초기화에 실패했습니다: {str(e)}"
|
712 |
+
else:
|
713 |
+
# RAGChain을 사용할 수 없는 경우
|
714 |
+
try:
|
715 |
+
logger.info("기본 RAG Chain을 사용할 수 없어 대체 버전을 시도합니다...")
|
716 |
+
|
717 |
+
# FallbackRAGChain 시도
|
718 |
+
try:
|
719 |
+
from fallback_rag_chain import FallbackRAGChain
|
720 |
+
self.rag_chain = FallbackRAGChain(self.vector_store)
|
721 |
+
self.is_initialized = True
|
722 |
+
logger.info("폴백 RAG 체인 초기화 성공")
|
723 |
+
except Exception as fallback_e:
|
724 |
+
logger.error(f"폴백 RAG 체인 초기화 실패: {fallback_e}", exc_info=True)
|
725 |
+
|
726 |
+
# SimpleRAGChain 시도 (최후의 수단)
|
727 |
+
try:
|
728 |
+
from simple_rag_chain import SimpleRAGChain
|
729 |
+
self.rag_chain = SimpleRAGChain(self.vector_store)
|
730 |
+
self.is_initialized = True
|
731 |
+
logger.info("SimpleRAGChain 초기화 성공")
|
732 |
+
except Exception as simple_e:
|
733 |
+
logger.error(f"모든 RAG 체인 초기화 실패: {simple_e}", exc_info=True)
|
734 |
+
return f"문서와 벡터 인덱스는 처리되었으나 RAG 체인 초기화에 실패했습니다"
|
735 |
+
except Exception as e:
|
736 |
+
logger.error(f"RAG 체인 초기화 실패: {e}", exc_info=True)
|
737 |
+
return f"문서와 벡터 인덱스는 처리되었으나 RAG 체인 초기화에 실패했습니다: {str(e)}"
|
738 |
+
|
739 |
+
# 성공 메시지 생성
|
740 |
+
result_message = f"""문서 처리 완료!
|
741 |
+
- 처리된 파일: {len(pdf_files)}개
|
742 |
+
- 캐시된 파일: {len(cached_files)}개
|
743 |
+
- 새 파일: {len(new_files)}개
|
744 |
+
- 업데이트된 파일: {len(updated_files)}개
|
745 |
+
- 실패한 파일: {len(failed_files)}개
|
746 |
+
- 총 청크 수: {len(all_chunks)}개
|
747 |
+
- 처리 시간: {processing_time:.2f}초
|
748 |
+
이제 질문할 준비가 되었습니다!"""
|
749 |
+
|
750 |
+
return result_message
|
751 |
+
|
752 |
+
except Exception as e:
|
753 |
+
error_message = f"문서 처리 중 오류 발생: {str(e)}"
|
754 |
+
logger.error(error_message, exc_info=True)
|
755 |
+
return error_message
|
756 |
+
|
757 |
+
def _process_vector_index(self, new_files: List[str], updated_files: List[str]) -> None:
|
758 |
+
"""
|
759 |
+
벡터 인덱스 처리
|
760 |
+
|
761 |
+
Args:
|
762 |
+
new_files: 새로 추가된 파일 목록
|
763 |
+
updated_files: 업데이트된 파일 목록
|
764 |
+
"""
|
765 |
+
# 벡터 인덱스 저장 경로 확인
|
766 |
+
if os.path.exists(self.vector_index_dir) and any(os.listdir(self.vector_index_dir)):
|
767 |
+
# 기존 벡터 인덱스 로드
|
768 |
+
try:
|
769 |
+
logger.info("저장된 벡터 인덱스 로드 중...")
|
770 |
+
vector_store_loaded = self.vector_store.load_local(self.vector_index_dir)
|
771 |
+
|
772 |
+
# 인덱스 로드 성공 확인
|
773 |
+
if self.vector_store.vector_store is not None:
|
774 |
+
# 새 문서나 변경된 문서가 있으면 인덱스 업데이트
|
775 |
+
if new_files or updated_files:
|
776 |
+
logger.info("벡터 인덱스 업데이트 중...")
|
777 |
+
self.vector_store.add_documents(self.documents)
|
778 |
+
|
779 |
+
logger.info("벡터 인덱스 로드 완료")
|
780 |
+
else:
|
781 |
+
logger.warning("벡터 인덱스를 로드했으나 유효하지 않음, 새로 생성합니다.")
|
782 |
+
self.vector_store.create_or_load(self.documents)
|
783 |
+
|
784 |
+
except Exception as e:
|
785 |
+
logger.error(f"벡터 인덱스 로드 실패, 새로 생성합니다: {e}", exc_info=True)
|
786 |
+
# 새 벡터 인덱스 생성
|
787 |
+
self.vector_store.create_or_load(self.documents)
|
788 |
+
else:
|
789 |
+
# 새 벡터 인덱스 생성
|
790 |
+
logger.info("새 벡터 인덱스 생성 중...")
|
791 |
+
self.vector_store.create_or_load(self.documents)
|
792 |
+
|
793 |
+
# 벡터 인덱스 저장
|
794 |
+
if self.vector_store and self.vector_store.vector_store is not None:
|
795 |
+
try:
|
796 |
+
logger.info(f"벡터 인덱스 저장 중: {self.vector_index_dir}")
|
797 |
+
save_result = self.vector_store.save_local(self.vector_index_dir)
|
798 |
+
logger.info(f"벡터 인덱스 저장 완료: {self.vector_index_dir}")
|
799 |
+
except Exception as e:
|
800 |
+
logger.error(f"벡터 인덱스 저장 실패: {e}", exc_info=True)
|
801 |
+
raise VectorStoreError(f"벡터 인덱스 저장 실패: {str(e)}")
|
802 |
+
else:
|
803 |
+
logger.warning("벡터 인덱스가 초기화되지 않아 저장하지 않습니다.")
|
804 |
+
|
805 |
+
def reset_cache(self) -> str:
|
806 |
+
"""
|
807 |
+
캐시 초기화
|
808 |
+
|
809 |
+
Returns:
|
810 |
+
결과 메시지
|
811 |
+
"""
|
812 |
+
try:
|
813 |
+
# 청크 파일 삭제
|
814 |
+
try:
|
815 |
+
for filename in os.listdir(self.chunks_dir):
|
816 |
+
file_path = os.path.join(self.chunks_dir, filename)
|
817 |
+
if os.path.isfile(file_path):
|
818 |
+
os.remove(file_path)
|
819 |
+
logger.info("청크 캐시 파일 삭제 완료")
|
820 |
+
except Exception as e:
|
821 |
+
logger.error(f"청크 파일 삭제 중 오류: {e}")
|
822 |
+
return f"청크 파일 삭제 중 오류 발생: {str(e)}"
|
823 |
+
|
824 |
+
# 인덱스 초기화
|
825 |
+
self.file_index = {}
|
826 |
+
try:
|
827 |
+
self._save_file_index()
|
828 |
+
logger.info("파일 인덱스 초기화 완료")
|
829 |
+
except Exception as e:
|
830 |
+
logger.error(f"인덱스 파일 초기화 중 오류: {e}")
|
831 |
+
return f"인덱스 파일 초기화 중 ��류 발생: {str(e)}"
|
832 |
+
|
833 |
+
# 벡터 인덱스 삭제
|
834 |
+
try:
|
835 |
+
for filename in os.listdir(self.vector_index_dir):
|
836 |
+
file_path = os.path.join(self.vector_index_dir, filename)
|
837 |
+
if os.path.isfile(file_path):
|
838 |
+
os.remove(file_path)
|
839 |
+
logger.info("벡터 인덱스 파일 삭제 완료")
|
840 |
+
except Exception as e:
|
841 |
+
logger.error(f"벡터 인덱스 파일 삭제 중 오류: {e}")
|
842 |
+
return f"벡터 인덱스 파일 삭제 중 오류 발생: {str(e)}"
|
843 |
+
|
844 |
+
self.documents = []
|
845 |
+
self.processed_files = []
|
846 |
+
self.is_initialized = False
|
847 |
+
|
848 |
+
logger.info("캐시 초기화 완료")
|
849 |
+
return "캐시가 초기화되었습니다. 다음 실행 시 모든 문서가 다시 처리됩니다."
|
850 |
+
except Exception as e:
|
851 |
+
error_msg = f"캐시 초기화 중 오류 발생: {str(e)}"
|
852 |
+
logger.error(error_msg, exc_info=True)
|
853 |
+
return error_msg
|
854 |
+
|
855 |
+
def process_query(self, query: str, chat_history: List[Tuple[str, str]]) -> Tuple[str, List[Tuple[str, str]]]:
|
856 |
+
"""
|
857 |
+
사용자 쿼리 처리
|
858 |
+
|
859 |
+
Args:
|
860 |
+
query: 사용자 질문
|
861 |
+
chat_history: 대화 기록
|
862 |
+
|
863 |
+
Returns:
|
864 |
+
응답 및 업데이트된 대화 기록
|
865 |
+
"""
|
866 |
+
if not query or not query.strip():
|
867 |
+
response = "질문이 비어 있습니다. 질문을 입력해 주세요."
|
868 |
+
chat_history.append((query, response))
|
869 |
+
return "", chat_history
|
870 |
+
|
871 |
+
if not self.is_initialized:
|
872 |
+
response = "문서 로드가 초기화되지 않았습니다. 자동 로드를 시도합니다."
|
873 |
+
chat_history.append((query, response))
|
874 |
+
|
875 |
+
# 자동 로드 시도
|
876 |
+
try:
|
877 |
+
init_result = self.auto_process_documents()
|
878 |
+
if not self.is_initialized:
|
879 |
+
response = f"문서를 로드할 수 없습니다. 'documents' 폴더에 PDF 파일이 있는지 확인하세요. 초기화 결과: {init_result}"
|
880 |
+
chat_history.append((query, response))
|
881 |
+
return "", chat_history
|
882 |
+
except Exception as e:
|
883 |
+
response = f"문서 로드 중 오류 발생: {str(e)}"
|
884 |
+
logger.error(f"자동 로드 실패: {e}", exc_info=True)
|
885 |
+
chat_history.append((query, response))
|
886 |
+
return "", chat_history
|
887 |
+
|
888 |
+
try:
|
889 |
+
# RAG 체인 실행 및 응답 생성
|
890 |
+
start_time = time.time()
|
891 |
+
logger.info(f"쿼리 처리 시작: {query}")
|
892 |
+
|
893 |
+
# rag_chain이 초기화되었는지 확인
|
894 |
+
if not hasattr(self, 'rag_chain') or self.rag_chain is None:
|
895 |
+
raise RAGInitializationError("RAG 체인이 초기화되지 않았습니다")
|
896 |
+
|
897 |
+
# 1. 먼저 표준 RAG 체인으로 시도
|
898 |
+
try:
|
899 |
+
response = self.rag_chain.run(query)
|
900 |
+
logger.info(f"기본 RAG 체인으로 응답 생성 성공")
|
901 |
+
except Exception as rag_error:
|
902 |
+
logger.error(f"기본 RAG 체인 실행 실패: {rag_error}, 대안 시도")
|
903 |
+
|
904 |
+
# 2. DeepSeek API 직접 호출 시도 (RAG 체인 우회)
|
905 |
+
try:
|
906 |
+
# DeepSeek API 정보 가져오기
|
907 |
+
try:
|
908 |
+
from config import DEEPSEEK_API_KEY, DEEPSEEK_MODEL, DEEPSEEK_ENDPOINT
|
909 |
+
except ImportError:
|
910 |
+
# 설정 모듈에서 가져올 수 없는 경우 기본값 설정
|
911 |
+
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
912 |
+
DEEPSEEK_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
|
913 |
+
DEEPSEEK_ENDPOINT = os.environ.get("DEEPSEEK_ENDPOINT",
|
914 |
+
"https://api.deepseek.com/v1/chat/completions")
|
915 |
+
|
916 |
+
# 직접 API 호출 함수 정의 (외부 모듈 의존성 제거)
|
917 |
+
def direct_api_call(query, context, api_key, model_name, endpoint, max_retries=3, timeout=60):
|
918 |
+
"""DeepSeek API 직접 호출 함수"""
|
919 |
+
import requests
|
920 |
+
import json
|
921 |
+
import time
|
922 |
+
|
923 |
+
# 프롬프트 길이 제한
|
924 |
+
if len(context) > 6000:
|
925 |
+
context = context[:2500] + "\n...(중략)...\n" + context[-2500:]
|
926 |
+
|
927 |
+
# 프롬프트 구성
|
928 |
+
prompt = f"""
|
929 |
+
다음 정보를 기반으로 질문에 정확하게 답변해주세요.
|
930 |
+
|
931 |
+
질문: {query}
|
932 |
+
|
933 |
+
참고 정보:
|
934 |
+
{context}
|
935 |
+
|
936 |
+
참고 정보에 답이 있으면 반드시 그 정보를 기반으로 답변하세요.
|
937 |
+
참고 정보에 ��이 없는 경우에는 일반적인 지식을 활용하여 답변할 수 있지만, "제공된 문서에는 이 정보가 없으나, 일반적으로는..." 식으로 시작하세요.
|
938 |
+
답변은 정확하고 간결하게 제공하되, 가능한 참고 정보에서 근거를 찾아 설명해주세요.
|
939 |
+
참고 정보의 출처도 함께 알려주세요.
|
940 |
+
"""
|
941 |
+
|
942 |
+
# API 요청 시도
|
943 |
+
headers = {
|
944 |
+
"Content-Type": "application/json",
|
945 |
+
"Authorization": f"Bearer {api_key}"
|
946 |
+
}
|
947 |
+
|
948 |
+
payload = {
|
949 |
+
"model": model_name,
|
950 |
+
"messages": [{"role": "user", "content": prompt}],
|
951 |
+
"temperature": 0.3,
|
952 |
+
"max_tokens": 1000
|
953 |
+
}
|
954 |
+
|
955 |
+
# 재시도 로직
|
956 |
+
retry_delay = 1.0
|
957 |
+
for attempt in range(max_retries):
|
958 |
+
try:
|
959 |
+
logger.info(f"DeepSeek API 직접 호출 시도 ({attempt + 1}/{max_retries})...")
|
960 |
+
response = requests.post(
|
961 |
+
endpoint,
|
962 |
+
headers=headers,
|
963 |
+
json=payload,
|
964 |
+
timeout=timeout
|
965 |
+
)
|
966 |
+
|
967 |
+
if response.status_code == 200:
|
968 |
+
result = response.json()
|
969 |
+
content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
970 |
+
logger.info(f"DeepSeek API 직접 호출 성공")
|
971 |
+
return content
|
972 |
+
else:
|
973 |
+
logger.warning(f"API 오류: 상태 코드 {response.status_code}")
|
974 |
+
# 요청 한도인 경우 더 오래 대기
|
975 |
+
if response.status_code == 429:
|
976 |
+
retry_delay = min(retry_delay * 3, 15)
|
977 |
+
else:
|
978 |
+
retry_delay = min(retry_delay * 2, 10)
|
979 |
+
|
980 |
+
if attempt < max_retries - 1:
|
981 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
982 |
+
time.sleep(retry_delay)
|
983 |
+
except Exception as e:
|
984 |
+
logger.error(f"API 호출 오류: {e}")
|
985 |
+
if attempt < max_retries - 1:
|
986 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
987 |
+
time.sleep(retry_delay)
|
988 |
+
retry_delay = min(retry_delay * 2, 10)
|
989 |
+
|
990 |
+
# 모든 시도 실패
|
991 |
+
raise Exception("최대 재시도 횟수 초과")
|
992 |
+
|
993 |
+
# 벡터 검색 수행
|
994 |
+
if self.vector_store and hasattr(self.vector_store, "similarity_search"):
|
995 |
+
logger.info("벡터 검색 수행...")
|
996 |
+
docs = self.vector_store.similarity_search(query, k=5)
|
997 |
+
|
998 |
+
# 검색 결과 컨텍스트 구성
|
999 |
+
context_parts = []
|
1000 |
+
for i, doc in enumerate(docs, 1):
|
1001 |
+
source = doc.metadata.get("source", "알 수 없는 출처")
|
1002 |
+
page = doc.metadata.get("page", "")
|
1003 |
+
source_info = f"{source}"
|
1004 |
+
if page:
|
1005 |
+
source_info += f" (페이지: {page})"
|
1006 |
+
context_parts.append(f"[참고자료 {i}] - 출처: {source_info}\n{doc.page_content}\n")
|
1007 |
+
context = "\n".join(context_parts)
|
1008 |
+
|
1009 |
+
# 직접 API 호출
|
1010 |
+
logger.info("DeepSeek API 직접 호출 시도...")
|
1011 |
+
response = direct_api_call(
|
1012 |
+
query,
|
1013 |
+
context,
|
1014 |
+
DEEPSEEK_API_KEY,
|
1015 |
+
DEEPSEEK_MODEL,
|
1016 |
+
DEEPSEEK_ENDPOINT,
|
1017 |
+
max_retries=3,
|
1018 |
+
timeout=120
|
1019 |
+
)
|
1020 |
+
logger.info("DeepSeek API 직접 호출 성공")
|
1021 |
+
else:
|
1022 |
+
raise Exception("벡터 스토어가 초기화되지 않았습니다")
|
1023 |
+
|
1024 |
+
except Exception as direct_api_error:
|
1025 |
+
logger.error(f"DeepSeek API 직접 호출 실패: {direct_api_error}, 검색 결과 반환")
|
1026 |
+
|
1027 |
+
# 3. 검색 결과만이라도 반환
|
1028 |
+
try:
|
1029 |
+
# 벡터 검색 수행
|
1030 |
+
if self.vector_store and hasattr(self.vector_store, "similarity_search"):
|
1031 |
+
docs = self.vector_store.similarity_search(query, k=5)
|
1032 |
+
|
1033 |
+
# 검색 결과 컨텍스트 구성
|
1034 |
+
context_parts = []
|
1035 |
+
for i, doc in enumerate(docs, 1):
|
1036 |
+
source = doc.metadata.get("source", "알 수 없는 출처")
|
1037 |
+
page = doc.metadata.get("page", "")
|
1038 |
+
source_info = f"{source}"
|
1039 |
+
if page:
|
1040 |
+
source_info += f" (페이지: {page})"
|
1041 |
+
context_parts.append(f"[참고자료 {i}] - 출처: {source_info}\n{doc.page_content}\n")
|
1042 |
+
context = "\n".join(context_parts)
|
1043 |
+
|
1044 |
+
# 간단한 응답 생성
|
1045 |
+
predefined_answers = {
|
1046 |
+
"대한민국의 수도": "대한민국의 수도는 서울입니다.",
|
1047 |
+
"수도": "대한민국의 수도는 서울입니다.",
|
1048 |
+
"누구야": "저는 RAG 기반 질의응답 시스템입니다. 문서를 검색하고 관련 정보를 찾아드립니다.",
|
1049 |
+
"안녕": "안녕하세요! 무엇을 도와드릴까요?",
|
1050 |
+
"뭐해": "사용자의 질문에 답변하기 위해 문서를 검색하고 있습니다. 무엇을 알려드릴까요?"
|
1051 |
+
}
|
1052 |
+
|
1053 |
+
# 질문에 맞는 미리 정의된 응답이 있는지 확인
|
1054 |
+
for key, answer in predefined_answers.items():
|
1055 |
+
if key in query.lower():
|
1056 |
+
response = answer
|
1057 |
+
logger.info(f"미리 정의된 응답 제공: {key}")
|
1058 |
+
break
|
1059 |
+
else:
|
1060 |
+
# 미리 정의된 응답이 없으면 검색 결과만 표시
|
1061 |
+
response = f"""
|
1062 |
+
API 서버 연결에 문제가 있어 검색 결과만 표시합니다.
|
1063 |
+
|
1064 |
+
질문: {query}
|
1065 |
+
|
1066 |
+
검색된 관련 문서:
|
1067 |
+
{context}
|
1068 |
+
|
1069 |
+
[참고] API 연결 문제로 인해 자동 요약이 제공되지 않습니다. 다시 시도하거나 다른 질문을 해보세요.
|
1070 |
+
"""
|
1071 |
+
logger.info("검색 결과만 표시")
|
1072 |
+
else:
|
1073 |
+
response = f"API 연결 및 벡터 검색에 모두 실패했습니다. 시스템 관리자에게 문의하세요."
|
1074 |
+
except Exception as fallback_error:
|
1075 |
+
logger.error(f"최종 폴백 응답 생성 실패: {fallback_error}")
|
1076 |
+
|
1077 |
+
# 4. 최후의 방법: 오류 메시지를 응답으로 반환
|
1078 |
+
if "Connection error" in str(rag_error) or "timeout" in str(rag_error).lower():
|
1079 |
+
response = f"""
|
1080 |
+
API 서버 연결에 문제가 있습니다. 잠시 후 다시 시도해주세요.
|
1081 |
+
|
1082 |
+
질문: {query}
|
1083 |
+
|
1084 |
+
[참고] 현재 DeepSeek API 서버와의 연결이 원활하지 않습니다. 이로 인해 질문에 대한 응답을 제공할 수 없습니다.
|
1085 |
+
"""
|
1086 |
+
else:
|
1087 |
+
response = f"쿼리 처리 중 오류가 발생했습니다: {str(rag_error)}"
|
1088 |
+
|
1089 |
+
end_time = time.time()
|
1090 |
+
query_time = end_time - start_time
|
1091 |
+
logger.info(f"쿼리 처리 완료: {query_time:.2f}초")
|
1092 |
+
|
1093 |
+
chat_history.append((query, response))
|
1094 |
+
return "", chat_history
|
1095 |
+
except RAGInitializationError as e:
|
1096 |
+
error_msg = f"RAG 시스템 초기화 오류: {str(e)}. 'documents' 폴더에 PDF 파일이 있는지 확인하고, 재시작해 보세요."
|
1097 |
+
logger.error(f"쿼리 처리 중 RAG 초기화 오류: {e}", exc_info=True)
|
1098 |
+
chat_history.append((query, error_msg))
|
1099 |
+
return "", chat_history
|
1100 |
+
except (VectorStoreError, DocumentProcessingError) as e:
|
1101 |
+
error_msg = f"문서 처리 시스템 오류: {str(e)}. 문서 형식이 올바른지 확인해 보세요."
|
1102 |
+
logger.error(f"쿼리 처리 중 문서/벡터 스토어 오류: {e}", exc_info=True)
|
1103 |
+
chat_history.append((query, error_msg))
|
1104 |
+
return "", chat_history
|
1105 |
+
except Exception as e:
|
1106 |
+
error_msg = f"쿼리 처리 중 오류 발생: {str(e)}"
|
1107 |
+
logger.error(f"쿼리 처리 중 예상치 못한 오류: {e}", exc_info=True)
|
1108 |
+
chat_history.append((query, error_msg))
|
1109 |
+
return "", chat_history
|
1110 |
+
|
1111 |
+
def launch_app(self) -> None:
|
1112 |
+
"""
|
1113 |
+
Gradio 앱 실행
|
1114 |
+
"""
|
1115 |
+
try:
|
1116 |
+
import gradio as gr
|
1117 |
+
except ImportError:
|
1118 |
+
logger.error("Gradio 라이브러리를 찾을 수 없습니다. pip install gradio로 설치하세요.")
|
1119 |
+
print("Gradio 라이브러리를 찾을 수 없습니다. pip install gradio로 설치하세요.")
|
1120 |
+
return
|
1121 |
+
# 내부 함수들이 현재 인스턴스(self)에 접근할 수 있도록 클로저 변수로 정의
|
1122 |
+
app_instance = self
|
1123 |
+
try:
|
1124 |
+
with gr.Blocks(title="PDF 문서 기반 RAG 챗봇") as app:
|
1125 |
+
gr.Markdown("# PDF 문서 기반 RAG 챗봇")
|
1126 |
+
gr.Markdown(f"* 사용 중인 LLM 모델: **{LLM_MODEL}**")
|
1127 |
+
|
1128 |
+
# 여기를 수정: 실제 경로 표시
|
1129 |
+
actual_pdf_dir = self.pdf_directory.replace('\\', '\\\\') if os.name == 'nt' else self.pdf_directory
|
1130 |
+
gr.Markdown(f"* PDF 문서 폴더: **{actual_pdf_dir}**")
|
1131 |
+
with gr.Row():
|
1132 |
+
with gr.Column(scale=1):
|
1133 |
+
# 문서 상태 섹션
|
1134 |
+
status_box = gr.Textbox(
|
1135 |
+
label="문서 처리 상태",
|
1136 |
+
value=self._get_status_message(),
|
1137 |
+
lines=5,
|
1138 |
+
interactive=False
|
1139 |
+
)
|
1140 |
+
|
1141 |
+
# 캐시 관리 버튼
|
1142 |
+
refresh_button = gr.Button("문서 새로 읽기", variant="primary")
|
1143 |
+
reset_button = gr.Button("캐시 초기화", variant="stop")
|
1144 |
+
|
1145 |
+
# 상태 및 오류 표시
|
1146 |
+
status_info = gr.Markdown(
|
1147 |
+
value=f"시스템 상태: {'초기화됨' if self.is_initialized else '초기화되지 않음'}"
|
1148 |
+
)
|
1149 |
+
|
1150 |
+
# 처리된 파일 정보
|
1151 |
+
with gr.Accordion("캐시 세부 정보", open=False):
|
1152 |
+
cache_info = gr.Textbox(
|
1153 |
+
label="캐시된 파일 정보",
|
1154 |
+
value=self._get_cache_info(),
|
1155 |
+
lines=5,
|
1156 |
+
interactive=False
|
1157 |
+
)
|
1158 |
+
|
1159 |
+
with gr.Column(scale=2):
|
1160 |
+
# 채팅 인터페이스
|
1161 |
+
chatbot = gr.Chatbot(
|
1162 |
+
label="대화 내용",
|
1163 |
+
bubble_full_width=False,
|
1164 |
+
height=500,
|
1165 |
+
show_copy_button=True
|
1166 |
+
)
|
1167 |
+
|
1168 |
+
# 음성 녹음 UI 추가
|
1169 |
+
with gr.Row():
|
1170 |
+
with gr.Column(scale=4):
|
1171 |
+
# 질문 입력과 전송 버튼
|
1172 |
+
query_box = gr.Textbox(
|
1173 |
+
label="질문",
|
1174 |
+
placeholder="처리된 문서 내용에 대해 질문하세요...",
|
1175 |
+
lines=2
|
1176 |
+
)
|
1177 |
+
with gr.Column(scale=1):
|
1178 |
+
# 음성 녹음 컴포넌트
|
1179 |
+
audio_input = gr.Audio(
|
1180 |
+
sources=["microphone"],
|
1181 |
+
type="numpy",
|
1182 |
+
label="음성으로 질문하기"
|
1183 |
+
)
|
1184 |
+
|
1185 |
+
with gr.Row():
|
1186 |
+
submit_btn = gr.Button("전송", variant="primary")
|
1187 |
+
clear_chat_button = gr.Button("대화 초기화")
|
1188 |
+
|
1189 |
+
# 음성 인식 처리 함수
|
1190 |
+
# app.py 내 process_audio 함수 보강
|
1191 |
+
# Gradio 앱 내에 있는 음성 인식 처리 함수 (원본)
|
1192 |
+
def process_audio(audio):
|
1193 |
+
logger.info("음성 인식 처리 시작...")
|
1194 |
+
try:
|
1195 |
+
from clova_stt import ClovaSTT
|
1196 |
+
import numpy as np
|
1197 |
+
import soundfile as sf
|
1198 |
+
import tempfile
|
1199 |
+
import os
|
1200 |
+
|
1201 |
+
if audio is None:
|
1202 |
+
return "음성이 녹음되지 않았습니다."
|
1203 |
+
|
1204 |
+
# 오디오 데이터를 임시 파일로 저장
|
1205 |
+
sr, y = audio
|
1206 |
+
logger.info(f"오디오 녹음 데이터 수신: 샘플레이트={sr}Hz, 길이={len(y)}샘플")
|
1207 |
+
if len(y) / sr < 1.0:
|
1208 |
+
return "녹음된 음성이 너무 짧습니다. 다시 시도해주세요."
|
1209 |
+
|
1210 |
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
|
1211 |
+
temp_path = temp_file.name
|
1212 |
+
sf.write(temp_path, y, sr, format="WAV")
|
1213 |
+
logger.info(f"임시 WAV 파일 저장됨: {temp_path}")
|
1214 |
+
|
1215 |
+
# 음성 인식 실행
|
1216 |
+
stt_client = ClovaSTT()
|
1217 |
+
with open(temp_path, "rb") as f:
|
1218 |
+
audio_bytes = f.read()
|
1219 |
+
result = stt_client.recognize(audio_bytes)
|
1220 |
+
|
1221 |
+
# 임시 파일 삭제
|
1222 |
+
try:
|
1223 |
+
os.unlink(temp_path)
|
1224 |
+
logger.info("임시 오디오 파일 삭제됨")
|
1225 |
+
except Exception as e:
|
1226 |
+
logger.warning(f"임시 파일 삭제 실패: {e}")
|
1227 |
+
|
1228 |
+
if result["success"]:
|
1229 |
+
recognized_text = result["text"]
|
1230 |
+
logger.info(f"음성인식 성공: {recognized_text}")
|
1231 |
+
return recognized_text
|
1232 |
+
else:
|
1233 |
+
error_msg = f"음성 인식 실패: {result.get('error', '알 수 없는 오류')}"
|
1234 |
+
logger.error(error_msg)
|
1235 |
+
return error_msg
|
1236 |
+
|
1237 |
+
except ImportError as e:
|
1238 |
+
logger.error(f"필요한 라이브러리 누락: {e}")
|
1239 |
+
return "음성인식에 필요한 라이브러리가 설치되지 않았습니다. pip install soundfile numpy requests를 실행해주세요."
|
1240 |
+
except Exception as e:
|
1241 |
+
logger.error(f"음성 처리 중 오류 발생: {e}", exc_info=True)
|
1242 |
+
return f"음성 처리 중 오류 발생: {str(e)}"
|
1243 |
+
|
1244 |
+
# 새로 추가할 process_audio_and_submit 함수
|
1245 |
+
def process_audio_and_submit(audio, chat_history):
|
1246 |
+
"""
|
1247 |
+
녹음 정지 시 음성 인식 후 자동으로 질문을 처리하는 함수.
|
1248 |
+
입력:
|
1249 |
+
- audio: 녹음 데이터 (gr.Audio의 값)
|
1250 |
+
- chat_history: 현재 대화 기록 (gr.Chatbot의 값)
|
1251 |
+
출력:
|
1252 |
+
- query_box: 빈 문자열 (질문 입력란 초기화)
|
1253 |
+
- chatbot: 업데이트된 대화 기록
|
1254 |
+
"""
|
1255 |
+
recognized_text = process_audio(audio)
|
1256 |
+
|
1257 |
+
# 음성 인식 결과가 오류 메시지인 경우 그대로 반환
|
1258 |
+
if not recognized_text or recognized_text.startswith("음성 인식 실패") or recognized_text.startswith(
|
1259 |
+
"음성 처리 중 오류"):
|
1260 |
+
return recognized_text, chat_history
|
1261 |
+
|
1262 |
+
# 인식된 텍스트를 사용하여 질문 처리
|
1263 |
+
return app_instance.process_query(recognized_text, chat_history)
|
1264 |
+
|
1265 |
+
# 기존 update_ui_after_refresh 함수 수정 (self 대신 app_instance 사용)
|
1266 |
+
def update_ui_after_refresh(result):
|
1267 |
+
return (
|
1268 |
+
result, # 상태 메시지
|
1269 |
+
app_instance._get_status_message(), # 상태 박스 업데이트
|
1270 |
+
f"시스템 상태: {'초기화됨' if app_instance.is_initialized else '초기화되지 않음'}", # 상태 정보 업데이트
|
1271 |
+
app_instance._get_cache_info() # 캐시 정보 업데이트
|
1272 |
+
)
|
1273 |
+
|
1274 |
+
# --- Gradio 이벤트 핸들러 설정 ---
|
1275 |
+
# 예: audio_input 컴포넌트의 stop_recording 이벤트를 아래와 같이 수정
|
1276 |
+
audio_input.stop_recording(
|
1277 |
+
fn=process_audio_and_submit,
|
1278 |
+
inputs=[audio_input, chatbot],
|
1279 |
+
outputs=[query_box, chatbot]
|
1280 |
+
)
|
1281 |
+
|
1282 |
+
# 음성 인식 결과를 질문 상자에 업데이트
|
1283 |
+
audio_input.stop_recording(
|
1284 |
+
fn=process_audio,
|
1285 |
+
inputs=[audio_input],
|
1286 |
+
outputs=[query_box]
|
1287 |
+
)
|
1288 |
+
|
1289 |
+
# 문서 새로 읽기 버튼
|
1290 |
+
refresh_button.click(
|
1291 |
+
fn=lambda: update_ui_after_refresh(self.auto_process_documents()),
|
1292 |
+
inputs=[],
|
1293 |
+
outputs=[status_box, status_box, status_info, cache_info]
|
1294 |
+
)
|
1295 |
+
|
1296 |
+
# 캐시 초기화 버튼
|
1297 |
+
def reset_and_process():
|
1298 |
+
reset_result = self.reset_cache()
|
1299 |
+
process_result = self.auto_process_documents()
|
1300 |
+
return update_ui_after_refresh(f"{reset_result}\n\n{process_result}")
|
1301 |
+
|
1302 |
+
reset_button.click(
|
1303 |
+
fn=reset_and_process,
|
1304 |
+
inputs=[],
|
1305 |
+
outputs=[status_box, status_box, status_info, cache_info]
|
1306 |
+
)
|
1307 |
+
|
1308 |
+
# 전송 버튼 클릭 이벤트
|
1309 |
+
submit_btn.click(
|
1310 |
+
fn=self.process_query,
|
1311 |
+
inputs=[query_box, chatbot],
|
1312 |
+
outputs=[query_box, chatbot]
|
1313 |
+
)
|
1314 |
+
|
1315 |
+
# 엔터키 입력 이벤트
|
1316 |
+
query_box.submit(
|
1317 |
+
fn=self.process_query,
|
1318 |
+
inputs=[query_box, chatbot],
|
1319 |
+
outputs=[query_box, chatbot]
|
1320 |
+
)
|
1321 |
+
|
1322 |
+
# 대화 초기화 버튼
|
1323 |
+
clear_chat_button.click(
|
1324 |
+
fn=lambda: [],
|
1325 |
+
outputs=[chatbot]
|
1326 |
+
)
|
1327 |
+
|
1328 |
+
# 앱 실행
|
1329 |
+
app.launch(share=False)
|
1330 |
+
except Exception as e:
|
1331 |
+
logger.error(f"Gradio 앱 실행 중 오류 발생: {e}", exc_info=True)
|
1332 |
+
print(f"Gradio 앱 실행 중 오류 발생: {e}")
|
1333 |
+
|
1334 |
+
|
1335 |
+
|
1336 |
+
def _get_status_message(self) -> str:
|
1337 |
+
"""
|
1338 |
+
현재 처리 상태 메시지 생성
|
1339 |
+
|
1340 |
+
Returns:
|
1341 |
+
상태 메시지
|
1342 |
+
"""
|
1343 |
+
if not self.processed_files:
|
1344 |
+
return "처리된 문서가 없습니다. '문서 새로 읽기' 버튼을 클릭하세요."
|
1345 |
+
|
1346 |
+
# DeepSeek API 상태 확인
|
1347 |
+
from config import USE_DEEPSEEK, DEEPSEEK_API_KEY, DEEPSEEK_MODEL
|
1348 |
+
|
1349 |
+
model_info = ""
|
1350 |
+
if USE_DEEPSEEK and DEEPSEEK_API_KEY:
|
1351 |
+
# DeepSeek API 테스트 수행
|
1352 |
+
try:
|
1353 |
+
# 테스트 함수 가져오기 시도
|
1354 |
+
try:
|
1355 |
+
from deepseek_utils import test_deepseek_api
|
1356 |
+
|
1357 |
+
# DeepSeek 설정 가져오기
|
1358 |
+
from config import DEEPSEEK_ENDPOINT
|
1359 |
+
|
1360 |
+
# API 테스트
|
1361 |
+
test_result = test_deepseek_api(DEEPSEEK_API_KEY, DEEPSEEK_ENDPOINT, DEEPSEEK_MODEL)
|
1362 |
+
|
1363 |
+
if test_result["success"]:
|
1364 |
+
model_info = f"\nDeepSeek API 상태: 정상 ({DEEPSEEK_MODEL})"
|
1365 |
+
else:
|
1366 |
+
model_info = f"\nDeepSeek API 상태: 오류 - {test_result['message']}"
|
1367 |
+
|
1368 |
+
except ImportError:
|
1369 |
+
# 직접 테스트 실행
|
1370 |
+
import requests
|
1371 |
+
import json
|
1372 |
+
|
1373 |
+
# DeepSeek 설정 가져오기
|
1374 |
+
from config import DEEPSEEK_ENDPOINT
|
1375 |
+
|
1376 |
+
# 테스트용 간단한 프롬프트
|
1377 |
+
test_prompt = "Hello, please respond with a short greeting."
|
1378 |
+
|
1379 |
+
# API 요청 헤더 및 데이터
|
1380 |
+
headers = {
|
1381 |
+
"Content-Type": "application/json",
|
1382 |
+
"Authorization": f"Bearer {DEEPSEEK_API_KEY}"
|
1383 |
+
}
|
1384 |
+
|
1385 |
+
payload = {
|
1386 |
+
"model": DEEPSEEK_MODEL,
|
1387 |
+
"messages": [{"role": "user", "content": test_prompt}],
|
1388 |
+
"temperature": 0.7,
|
1389 |
+
"max_tokens": 50
|
1390 |
+
}
|
1391 |
+
|
1392 |
+
# API 요청 전송
|
1393 |
+
try:
|
1394 |
+
response = requests.post(
|
1395 |
+
DEEPSEEK_ENDPOINT,
|
1396 |
+
headers=headers,
|
1397 |
+
data=json.dumps(payload),
|
1398 |
+
timeout=5 # 5초 타임아웃 (UI 반응성 유지)
|
1399 |
+
)
|
1400 |
+
|
1401 |
+
# 응답 확인
|
1402 |
+
if response.status_code == 200:
|
1403 |
+
model_info = f"\nDeepSeek API 상태: 정상 ({DEEPSEEK_MODEL})"
|
1404 |
+
else:
|
1405 |
+
error_message = response.text[:100]
|
1406 |
+
model_info = f"\nDeepSeek API 상태: 오류 (상태 코드: {response.status_code})"
|
1407 |
+
except Exception as e:
|
1408 |
+
model_info = f"\nDeepSeek API 상태: 연결 실패 ({str(e)[:100]})"
|
1409 |
+
except Exception as e:
|
1410 |
+
model_info = f"\nDeepSeek API 상태 확인 실패: {str(e)[:100]}"
|
1411 |
+
|
1412 |
+
return f"처리된 문서 ({len(self.processed_files)}개): {', '.join(self.processed_files)}{model_info}"
|
1413 |
+
|
1414 |
+
def _get_cache_info(self) -> str:
|
1415 |
+
"""
|
1416 |
+
캐시 세부 정보 메시지 생성
|
1417 |
+
|
1418 |
+
Returns:
|
1419 |
+
캐시 정보 메시지
|
1420 |
+
"""
|
1421 |
+
if not self.file_index:
|
1422 |
+
return "캐시된 파일이 없습니다."
|
1423 |
+
|
1424 |
+
file_info = ""
|
1425 |
+
for file_path, info in self.file_index.items():
|
1426 |
+
file_name = info.get('file_name', os.path.basename(file_path))
|
1427 |
+
chunks_count = info.get('chunks_count', 0)
|
1428 |
+
file_size = info.get('file_size', 0)
|
1429 |
+
last_processed = info.get('last_processed', 0)
|
1430 |
+
|
1431 |
+
# 파일 크기를 사람이 읽기 쉬운 형태로 변환
|
1432 |
+
if file_size < 1024:
|
1433 |
+
size_str = f"{file_size} bytes"
|
1434 |
+
elif file_size < 1024 * 1024:
|
1435 |
+
size_str = f"{file_size / 1024:.1f} KB"
|
1436 |
+
else:
|
1437 |
+
size_str = f"{file_size / (1024 * 1024):.1f} MB"
|
1438 |
+
|
1439 |
+
# 마지막 처리 시간을 날짜/시간 형식으로 변환
|
1440 |
+
if last_processed:
|
1441 |
+
from datetime import datetime
|
1442 |
+
last_time = datetime.fromtimestamp(last_processed).strftime('%Y-%m-%d %H:%M:%S')
|
1443 |
+
else:
|
1444 |
+
last_time = "알 수 없음"
|
1445 |
+
|
1446 |
+
file_info += f"- {file_name}: {chunks_count}개 청크, {size_str}, 마지막 처리: {last_time}\n"
|
1447 |
+
|
1448 |
+
return file_info
|
1449 |
+
|
1450 |
+
|
1451 |
+
|
1452 |
+
if __name__ == "__main__":
|
1453 |
+
app = AutoRAGChatApp()
|
1454 |
+
app.launch_app()
|
autorag.log
ADDED
@@ -0,0 +1,493 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
2025-03-29 23:17:05,934 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3
|
2 |
+
2025-03-29 23:17:05,934 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3\documents
|
3 |
+
2025-03-29 23:17:05,934 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3\documents
|
4 |
+
2025-03-29 23:17:05,934 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3\documents
|
5 |
+
2025-03-29 23:17:05,934 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
6 |
+
2025-03-29 23:17:05,934 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
7 |
+
2025-03-29 23:17:07,025 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
8 |
+
2025-03-29 23:17:07,025 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3\documents
|
9 |
+
2025-03-29 23:17:07,026 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
10 |
+
2025-03-29 23:17:07,026 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3\documents'
|
11 |
+
2025-03-29 23:17:07,026 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data'
|
12 |
+
2025-03-29 23:17:07,849 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
13 |
+
2025-03-29 23:17:07,851 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
14 |
+
2025-03-29 23:17:12,304 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
15 |
+
2025-03-29 23:17:12,304 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
16 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3\documents
|
17 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - ���丮 ����: ['RAG �Ʒÿ� Q.pdf']
|
18 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
19 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
20 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
21 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - ó�� ��: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
22 |
+
2025-03-29 23:17:12,305 - AutoRAG - INFO - docling���� ó�� �õ�: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
23 |
+
2025-03-29 23:17:12,305 - AutoRAG - WARNING - �ñ׳� ���� ���� (������ ȯ���� �� ����): module 'signal' has no attribute 'SIGALRM'
|
24 |
+
2025-03-29 23:17:13,014 - docling.document_converter - INFO - Going to convert document batch...
|
25 |
+
2025-03-29 23:17:13,025 - docling.models.factories.base_factory - INFO - Loading plugin 'docling_defaults'
|
26 |
+
2025-03-29 23:17:13,026 - docling.models.factories - INFO - Registered ocr engines: ['easyocr', 'ocrmac', 'rapidocr', 'tesserocr', 'tesseract']
|
27 |
+
2025-03-29 23:17:13,150 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cuda:0'
|
28 |
+
2025-03-29 23:17:15,247 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cuda:0'
|
29 |
+
2025-03-29 23:17:16,731 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cuda:0'
|
30 |
+
2025-03-29 23:17:17,206 - docling.models.factories.base_factory - INFO - Loading plugin 'docling_defaults'
|
31 |
+
2025-03-29 23:17:17,207 - docling.models.factories - INFO - Registered picture descriptions: ['vlm', 'api']
|
32 |
+
2025-03-29 23:17:17,207 - docling.pipeline.base_pipeline - INFO - Processing document RAG �Ʒÿ� Q.pdf
|
33 |
+
2025-03-29 23:17:18,062 - docling.document_converter - INFO - Finished converting document RAG �Ʒÿ� Q.pdf in 5.76 sec.
|
34 |
+
2025-03-29 23:17:18,110 - AutoRAG - INFO - ûũ ���� �Ϸ�: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
35 |
+
2025-03-29 23:17:18,111 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 5.81��
|
36 |
+
2025-03-29 23:17:18,111 - AutoRAG - INFO - �� ���� ��� ���� ��...
|
37 |
+
2025-03-29 23:17:18,111 - VectorStore - INFO - FAISS ��� ���� ��: 1�� ����
|
38 |
+
2025-03-29 23:17:18,331 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
39 |
+
2025-03-29 23:17:18,517 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
40 |
+
2025-03-29 23:17:18,523 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
41 |
+
2025-03-29 23:17:18,523 - VectorStore - INFO - FAISS �ε��� ���� �Ϸ�
|
42 |
+
2025-03-29 23:17:18,523 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
43 |
+
2025-03-29 23:17:18,525 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
44 |
+
2025-03-29 23:17:18,525 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
45 |
+
2025-03-29 23:17:18,525 - RAGChain - INFO - RAGChain �ʱ�ȭ ����...
|
46 |
+
2025-03-29 23:17:18,525 - RAGChain - INFO - ����Ŀ ��� ����: True
|
47 |
+
2025-03-29 23:17:20,596 - sentence_transformers.cross_encoder.CrossEncoder - INFO - Use pytorch device: cuda
|
48 |
+
2025-03-29 23:17:20,800 - RAGChain - INFO - ����Ŀ �ʱ�ȭ ����
|
49 |
+
2025-03-29 23:17:20,800 - RAGChain - INFO - Ollama �� �ʱ�ȭ: gemma3:latest
|
50 |
+
2025-03-29 23:17:20,810 - RAGChain - INFO - Ollama �� �ʱ�ȭ ����
|
51 |
+
2025-03-29 23:17:20,810 - RAGChain - INFO - RAG ü�� ���� ����...
|
52 |
+
2025-03-29 23:17:20,810 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
53 |
+
2025-03-29 23:17:20,810 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
54 |
+
2025-03-29 23:17:20,810 - AutoRAG - INFO - ���� ó�� �Ϸ�!
|
55 |
+
- �� ����: 1��
|
56 |
+
- ij�õ� ����: 0��
|
57 |
+
- �� ����: 0��
|
58 |
+
- ������Ʈ�� ����: 1��
|
59 |
+
- ������ ����: 0��
|
60 |
+
- �� ûũ ��: 1��
|
61 |
+
- ó�� �ð�: 8.51��
|
62 |
+
���� ������ �غ� �Ǿ����ϴ�!
|
63 |
+
2025-03-29 23:17:20,810 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
64 |
+
2025-03-29 23:20:20,300 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3
|
65 |
+
2025-03-29 23:20:20,300 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3\documents
|
66 |
+
2025-03-29 23:20:20,300 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3\documents
|
67 |
+
2025-03-29 23:20:20,300 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3\documents
|
68 |
+
2025-03-29 23:20:20,300 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
69 |
+
2025-03-29 23:20:20,300 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
70 |
+
2025-03-29 23:20:21,048 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
71 |
+
2025-03-29 23:20:21,048 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3\documents
|
72 |
+
2025-03-29 23:20:21,048 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
73 |
+
2025-03-29 23:20:21,048 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3\documents'
|
74 |
+
2025-03-29 23:20:21,048 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data'
|
75 |
+
2025-03-29 23:20:21,794 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
76 |
+
2025-03-29 23:20:21,796 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
77 |
+
2025-03-29 23:20:25,626 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
78 |
+
2025-03-29 23:20:25,626 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
79 |
+
2025-03-29 23:20:25,626 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3\documents
|
80 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - ���丮 ����: ['RAG �Ʒÿ� Q.pdf']
|
81 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
82 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
83 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
84 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - ûũ �ε� �Ϸ�: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
85 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 0.00��
|
86 |
+
2025-03-29 23:20:25,627 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
87 |
+
2025-03-29 23:20:25,627 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
88 |
+
2025-03-29 23:20:25,629 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
89 |
+
2025-03-29 23:20:25,641 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
90 |
+
2025-03-29 23:20:25,644 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
91 |
+
2025-03-29 23:20:25,645 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
92 |
+
2025-03-29 23:20:25,645 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
93 |
+
2025-03-29 23:20:25,645 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
94 |
+
2025-03-29 23:20:25,645 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
95 |
+
2025-03-29 23:20:25,645 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
96 |
+
2025-03-29 23:20:25,645 - RAGChain - INFO - RAGChain �ʱ�ȭ ����...
|
97 |
+
2025-03-29 23:20:25,645 - RAGChain - INFO - ����Ŀ ��� ����: True
|
98 |
+
2025-03-29 23:20:27,424 - sentence_transformers.cross_encoder.CrossEncoder - INFO - Use pytorch device: cuda
|
99 |
+
2025-03-29 23:20:27,622 - RAGChain - INFO - ����Ŀ �ʱ�ȭ ����
|
100 |
+
2025-03-29 23:20:27,622 - RAGChain - INFO - Ollama �� �ʱ�ȭ: gemma3:latest
|
101 |
+
2025-03-29 23:20:27,623 - RAGChain - INFO - Ollama �� �ʱ�ȭ ����
|
102 |
+
2025-03-29 23:20:27,623 - RAGChain - INFO - RAG ü�� ���� ����...
|
103 |
+
2025-03-29 23:20:27,623 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
104 |
+
2025-03-29 23:20:27,623 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
105 |
+
2025-03-29 23:20:27,624 - AutoRAG - INFO - ���� ó�� �Ϸ�!
|
106 |
+
- �� ����: 1��
|
107 |
+
- ij�õ� ����: 1��
|
108 |
+
- �� ����: 0��
|
109 |
+
- ������Ʈ�� ����: 0��
|
110 |
+
- ������ ����: 0��
|
111 |
+
- �� ûũ ��: 1��
|
112 |
+
- ó�� �ð�: 2.00��
|
113 |
+
���� ������ �غ� �Ǿ����ϴ�!
|
114 |
+
2025-03-29 23:20:27,624 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
115 |
+
2025-03-29 23:20:29,758 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
116 |
+
2025-03-29 23:20:30,349 - AutoRAG - ERROR - Gradio �� ���� �� ���� ��: 'AutoRAGChatApp' object has no attribute '_get_status_message'
|
117 |
+
Traceback (most recent call last):
|
118 |
+
File "C:\Users\USER\PycharmProjects\RagPipeline\RAG3\app.py", line 855, in launch_app
|
119 |
+
value=self._get_status_message(),
|
120 |
+
^^^^^^^^^^^^^^^^^^^^^^^^
|
121 |
+
AttributeError: 'AutoRAGChatApp' object has no attribute '_get_status_message'
|
122 |
+
2025-03-29 23:20:31,087 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
123 |
+
2025-03-29 23:22:28,187 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3
|
124 |
+
2025-03-29 23:22:28,188 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3\documents
|
125 |
+
2025-03-29 23:22:28,188 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3\documents
|
126 |
+
2025-03-29 23:22:28,188 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3\documents
|
127 |
+
2025-03-29 23:22:28,188 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
128 |
+
2025-03-29 23:22:28,188 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
129 |
+
2025-03-29 23:22:28,930 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
130 |
+
2025-03-29 23:22:28,930 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3\documents
|
131 |
+
2025-03-29 23:22:28,930 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
132 |
+
2025-03-29 23:22:28,931 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3\documents'
|
133 |
+
2025-03-29 23:22:28,931 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data'
|
134 |
+
2025-03-29 23:22:29,680 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
135 |
+
2025-03-29 23:22:29,681 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
136 |
+
2025-03-29 23:22:33,510 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
137 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
138 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3\documents
|
139 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - ���丮 ����: ['RAG �Ʒÿ� Q.pdf']
|
140 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
141 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
142 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
143 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - ûũ �ε� �Ϸ�: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
144 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 0.00��
|
145 |
+
2025-03-29 23:22:33,510 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
146 |
+
2025-03-29 23:22:33,510 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
147 |
+
2025-03-29 23:22:33,512 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
148 |
+
2025-03-29 23:22:33,523 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
149 |
+
2025-03-29 23:22:33,526 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
150 |
+
2025-03-29 23:22:33,532 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
151 |
+
2025-03-29 23:22:33,532 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
152 |
+
2025-03-29 23:22:33,532 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
153 |
+
2025-03-29 23:22:33,533 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
154 |
+
2025-03-29 23:22:33,533 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
155 |
+
2025-03-29 23:22:33,533 - RAGChain - INFO - RAGChain �ʱ�ȭ ����...
|
156 |
+
2025-03-29 23:22:33,533 - RAGChain - INFO - ����Ŀ ��� ����: True
|
157 |
+
2025-03-29 23:22:35,580 - sentence_transformers.cross_encoder.CrossEncoder - INFO - Use pytorch device: cuda
|
158 |
+
2025-03-29 23:22:35,782 - RAGChain - INFO - ����Ŀ �ʱ�ȭ ����
|
159 |
+
2025-03-29 23:22:35,782 - RAGChain - INFO - Ollama �� �ʱ�ȭ: gemma3:latest
|
160 |
+
2025-03-29 23:22:35,783 - RAGChain - INFO - Ollama �� �ʱ�ȭ ����
|
161 |
+
2025-03-29 23:22:35,783 - RAGChain - INFO - RAG ü�� ���� ����...
|
162 |
+
2025-03-29 23:22:35,783 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
163 |
+
2025-03-29 23:22:35,783 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
164 |
+
2025-03-29 23:22:35,783 - AutoRAG - INFO - ���� ó�� �Ϸ�!
|
165 |
+
- �� ����: 1��
|
166 |
+
- ij�õ� ����: 1��
|
167 |
+
- �� ����: 0��
|
168 |
+
- ������Ʈ�� ����: 0��
|
169 |
+
- ������ ����: 0��
|
170 |
+
- �� ûũ ��: 1��
|
171 |
+
- ó�� �ð�: 2.27��
|
172 |
+
���� ������ �غ� �Ǿ����ϴ�!
|
173 |
+
2025-03-29 23:22:35,783 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
174 |
+
2025-03-29 23:22:37,712 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
175 |
+
2025-03-29 23:22:38,171 - httpx - INFO - HTTP Request: GET http://127.0.0.1:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
|
176 |
+
2025-03-29 23:22:38,259 - httpx - INFO - HTTP Request: HEAD http://127.0.0.1:7860/ "HTTP/1.1 200 OK"
|
177 |
+
2025-03-29 23:22:38,510 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
178 |
+
2025-03-29 23:23:06,606 - AutoRAG - INFO - ���� ó�� ����: �ȳ�?
|
179 |
+
2025-03-29 23:23:06,606 - RAGChain - INFO - RAG ü�� ����: '�ȳ�?'
|
180 |
+
2025-03-29 23:23:06,616 - RAGChain - INFO - ���� �˻� ����: '�ȳ�?'
|
181 |
+
2025-03-29 23:23:06,616 - VectorStore - INFO - �˻� ���� ����: '�ȳ�?', ���� 5�� ��� ��û
|
182 |
+
2025-03-29 23:23:06,674 - VectorStore - INFO - �˻� �Ϸ�: 1�� ��� ã��
|
183 |
+
2025-03-29 23:23:06,674 - RAGChain - INFO - ����ŷ ����: 1�� ����
|
184 |
+
2025-03-29 23:23:06,992 - RAGChain - INFO - ����ŷ �Ϸ�: 1�� ���� ���õ�
|
185 |
+
2025-03-29 23:23:06,992 - RAGChain - INFO - ���ؽ�Ʈ ���� �Ϸ�: 1�� ����, 683 ����
|
186 |
+
2025-03-29 23:23:13,887 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
187 |
+
2025-03-29 23:23:13,887 - AutoRAG - INFO - ���� ó�� �Ϸ�: 7.28��
|
188 |
+
2025-03-29 23:23:44,199 - AutoRAG - INFO - ���� ó�� ����: ���ѹα� ������?
|
189 |
+
2025-03-29 23:23:44,199 - RAGChain - INFO - RAG ü�� ����: '���ѹα� ������?'
|
190 |
+
2025-03-29 23:23:44,200 - RAGChain - INFO - ���� �˻� ����: '���ѹα� ������?'
|
191 |
+
2025-03-29 23:23:44,201 - VectorStore - INFO - �˻� ���� ����: '���ѹα� ������?', ���� 5�� ��� ��û
|
192 |
+
2025-03-29 23:23:44,237 - VectorStore - INFO - �˻� �Ϸ�: 1�� ��� ã��
|
193 |
+
2025-03-29 23:23:44,238 - RAGChain - INFO - ����ŷ ����: 1�� ����
|
194 |
+
2025-03-29 23:23:44,358 - RAGChain - INFO - ����ŷ �Ϸ�: 1�� ���� ���õ�
|
195 |
+
2025-03-29 23:23:44,358 - RAGChain - INFO - ���ؽ�Ʈ ���� �Ϸ�: 1�� ����, 683 ����
|
196 |
+
2025-03-29 23:23:47,486 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
197 |
+
2025-03-29 23:23:47,486 - AutoRAG - INFO - ���� ó�� �Ϸ�: 3.29��
|
198 |
+
2025-03-29 23:24:29,670 - AutoRAG - INFO - ���� ó�� ����: �� ������?
|
199 |
+
2025-03-29 23:24:29,670 - RAGChain - INFO - RAG ü�� ����: '�� ������?'
|
200 |
+
2025-03-29 23:24:29,673 - RAGChain - INFO - ���� �˻� ����: '�� ������?'
|
201 |
+
2025-03-29 23:24:29,673 - VectorStore - INFO - �˻� ���� ����: '�� ������?', ���� 5�� ��� ��û
|
202 |
+
2025-03-29 23:24:29,707 - VectorStore - INFO - �˻� �Ϸ�: 1�� ��� ã��
|
203 |
+
2025-03-29 23:24:29,708 - RAGChain - INFO - ����ŷ ����: 1�� ����
|
204 |
+
2025-03-29 23:24:29,827 - RAGChain - INFO - ����ŷ �Ϸ�: 1�� ���� ���õ�
|
205 |
+
2025-03-29 23:24:29,827 - RAGChain - INFO - ���ؽ�Ʈ ���� �Ϸ�: 1�� ����, 683 ����
|
206 |
+
2025-03-29 23:24:32,394 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
207 |
+
2025-03-29 23:24:32,394 - AutoRAG - INFO - ���� ó�� �Ϸ�: 2.72��
|
208 |
+
2025-03-30 00:09:22,627 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3
|
209 |
+
2025-03-30 00:09:22,627 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3\documents
|
210 |
+
2025-03-30 00:09:22,627 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3\documents
|
211 |
+
2025-03-30 00:09:22,627 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3\documents
|
212 |
+
2025-03-30 00:09:22,627 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
213 |
+
2025-03-30 00:09:22,628 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
214 |
+
2025-03-30 00:09:23,685 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
215 |
+
2025-03-30 00:09:23,685 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3\documents
|
216 |
+
2025-03-30 00:09:23,685 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
217 |
+
2025-03-30 00:09:23,686 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3\documents'
|
218 |
+
2025-03-30 00:09:23,686 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data'
|
219 |
+
2025-03-30 00:09:24,553 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
220 |
+
2025-03-30 00:09:24,560 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
221 |
+
2025-03-30 00:09:28,468 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
222 |
+
2025-03-30 00:09:28,473 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
223 |
+
2025-03-30 00:09:28,473 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3\documents
|
224 |
+
2025-03-30 00:09:28,473 - AutoRAG - INFO - ���丮 ����: ['.gitkeep', 'RAG �Ʒÿ� Q.pdf']
|
225 |
+
2025-03-30 00:09:28,473 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
226 |
+
2025-03-30 00:09:28,473 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
227 |
+
2025-03-30 00:09:28,473 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
228 |
+
2025-03-30 00:09:28,479 - AutoRAG - INFO - ûũ �ε� �Ϸ�: C:\Users\USER\RAG3\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
229 |
+
2025-03-30 00:09:28,479 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 0.01��
|
230 |
+
2025-03-30 00:09:28,479 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
231 |
+
2025-03-30 00:09:28,479 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
232 |
+
2025-03-30 00:09:28,480 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
233 |
+
2025-03-30 00:09:28,625 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
234 |
+
2025-03-30 00:09:28,630 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
235 |
+
2025-03-30 00:09:28,635 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
236 |
+
2025-03-30 00:09:28,636 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
237 |
+
2025-03-30 00:09:28,636 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
238 |
+
2025-03-30 00:09:28,636 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
239 |
+
2025-03-30 00:09:28,636 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
240 |
+
2025-03-30 00:09:28,636 - RAGChain - INFO - RAGChain �ʱ�ȭ ����...
|
241 |
+
2025-03-30 00:09:28,636 - RAGChain - INFO - ����Ŀ ��� ����: True
|
242 |
+
2025-03-30 00:09:30,453 - sentence_transformers.cross_encoder.CrossEncoder - INFO - Use pytorch device: cuda
|
243 |
+
2025-03-30 00:09:30,651 - RAGChain - INFO - ����Ŀ �ʱ�ȭ ����
|
244 |
+
2025-03-30 00:09:30,652 - RAGChain - INFO - Ollama �� �ʱ�ȭ: gemma3:latest
|
245 |
+
2025-03-30 00:09:30,653 - RAGChain - INFO - Ollama �� �ʱ�ȭ ����
|
246 |
+
2025-03-30 00:09:30,653 - RAGChain - INFO - RAG ü�� ���� ����...
|
247 |
+
2025-03-30 00:09:30,653 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
248 |
+
2025-03-30 00:09:30,653 - RAGChain - INFO - RAG ü�� ���� �Ϸ�
|
249 |
+
2025-03-30 00:09:30,653 - AutoRAG - INFO - ���� ó�� �Ϸ�!
|
250 |
+
- �� ����: 1��
|
251 |
+
- ij�õ� ����: 1��
|
252 |
+
- �� ����: 0��
|
253 |
+
- ������Ʈ�� ����: 0��
|
254 |
+
- ������ ����: 0��
|
255 |
+
- �� ûũ ��: 1��
|
256 |
+
- ó�� �ð�: 2.18��
|
257 |
+
���� ������ �غ� �Ǿ����ϴ�!
|
258 |
+
2025-03-30 00:09:30,653 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
259 |
+
2025-03-30 00:09:33,004 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
260 |
+
2025-03-30 00:09:33,980 - httpx - INFO - HTTP Request: GET http://127.0.0.1:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
|
261 |
+
2025-03-30 00:09:34,074 - httpx - INFO - HTTP Request: HEAD http://127.0.0.1:7860/ "HTTP/1.1 200 OK"
|
262 |
+
2025-03-30 00:09:34,318 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
263 |
+
2025-03-30 00:32:10,853 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3
|
264 |
+
2025-03-30 00:32:10,854 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents
|
265 |
+
2025-03-30 00:32:10,854 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents
|
266 |
+
2025-03-30 00:32:10,854 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents
|
267 |
+
2025-03-30 00:32:10,854 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\PycharmProjects\\RagPipeline\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
268 |
+
2025-03-30 00:32:10,854 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
269 |
+
2025-03-30 00:32:11,619 - AutoRAG - WARNING - RAG ü�� ����� �ε��� �� �����ϴ�.
|
270 |
+
2025-03-30 00:32:11,620 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
271 |
+
2025-03-30 00:32:11,620 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents
|
272 |
+
2025-03-30 00:32:11,620 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
273 |
+
2025-03-30 00:32:11,620 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents'
|
274 |
+
2025-03-30 00:32:11,620 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data'
|
275 |
+
2025-03-30 00:32:12,436 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
276 |
+
2025-03-30 00:32:12,514 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
277 |
+
2025-03-30 00:32:16,937 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
278 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
279 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents
|
280 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - ���丮 ����: ['RAG �Ʒÿ� Q.pdf']
|
281 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
282 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\PycharmProjects\\RagPipeline\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
283 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
284 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - ó�� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
285 |
+
2025-03-30 00:32:16,938 - AutoRAG - INFO - docling���� ó�� �õ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
286 |
+
2025-03-30 00:32:16,938 - AutoRAG - WARNING - �ñ׳� ���� ���� (������ ȯ���� �� ����): module 'signal' has no attribute 'SIGALRM'
|
287 |
+
2025-03-30 00:32:17,580 - docling.document_converter - INFO - Going to convert document batch...
|
288 |
+
2025-03-30 00:32:17,596 - docling.models.factories.base_factory - INFO - Loading plugin 'docling_defaults'
|
289 |
+
2025-03-30 00:32:17,596 - docling.models.factories - INFO - Registered ocr engines: ['easyocr', 'ocrmac', 'rapidocr', 'tesserocr', 'tesseract']
|
290 |
+
2025-03-30 00:32:17,693 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cuda:0'
|
291 |
+
2025-03-30 00:32:19,673 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cuda:0'
|
292 |
+
2025-03-30 00:32:20,999 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cuda:0'
|
293 |
+
2025-03-30 00:32:21,412 - docling.models.factories.base_factory - INFO - Loading plugin 'docling_defaults'
|
294 |
+
2025-03-30 00:32:21,412 - docling.models.factories - INFO - Registered picture descriptions: ['vlm', 'api']
|
295 |
+
2025-03-30 00:32:21,412 - docling.pipeline.base_pipeline - INFO - Processing document RAG �Ʒÿ� Q.pdf
|
296 |
+
2025-03-30 00:32:22,019 - docling.document_converter - INFO - Finished converting document RAG �Ʒÿ� Q.pdf in 5.08 sec.
|
297 |
+
2025-03-30 00:32:22,079 - AutoRAG - INFO - ûũ ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
298 |
+
2025-03-30 00:32:22,079 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 5.14��
|
299 |
+
2025-03-30 00:32:22,079 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
300 |
+
2025-03-30 00:32:22,079 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
301 |
+
2025-03-30 00:32:22,082 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
302 |
+
2025-03-30 00:32:22,104 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
303 |
+
2025-03-30 00:32:22,108 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
304 |
+
2025-03-30 00:32:22,113 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
305 |
+
2025-03-30 00:32:22,113 - AutoRAG - INFO - ���� �ε��� ������Ʈ ��...
|
306 |
+
2025-03-30 00:32:22,113 - VectorStore - INFO - 1�� ������ ���� ���� ���� �߰��մϴ�
|
307 |
+
2025-03-30 00:32:22,324 - VectorStore - INFO - 1�� ���� �߰� �Ϸ�
|
308 |
+
2025-03-30 00:32:22,324 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
309 |
+
2025-03-30 00:32:22,324 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
310 |
+
2025-03-30 00:32:22,325 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
311 |
+
2025-03-30 00:32:22,325 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
312 |
+
2025-03-30 00:32:22,325 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
313 |
+
2025-03-30 00:32:24,654 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
314 |
+
2025-03-30 00:32:24,990 - httpx - INFO - HTTP Request: GET http://127.0.0.1:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
|
315 |
+
2025-03-30 00:32:25,035 - httpx - INFO - HTTP Request: HEAD http://127.0.0.1:7860/ "HTTP/1.1 200 OK"
|
316 |
+
2025-03-30 00:32:25,293 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
317 |
+
2025-03-30 00:32:45,649 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents
|
318 |
+
2025-03-30 00:32:45,649 - AutoRAG - INFO - ���丮 ����: ['RAG �Ʒÿ� Q.pdf']
|
319 |
+
2025-03-30 00:32:45,650 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents\RAG �Ʒÿ� Q.pdf
|
320 |
+
2025-03-30 00:32:45,650 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\PycharmProjects\\RagPipeline\\RAG3\\documents\\RAG �Ʒÿ� Q.pdf']
|
321 |
+
2025-03-30 00:32:45,650 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
322 |
+
2025-03-30 00:32:45,658 - AutoRAG - INFO - ûũ �ε� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
323 |
+
2025-03-30 00:32:45,658 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 0.01��
|
324 |
+
2025-03-30 00:32:45,658 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
325 |
+
2025-03-30 00:32:45,658 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
326 |
+
2025-03-30 00:32:45,666 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
327 |
+
2025-03-30 00:32:45,666 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
328 |
+
2025-03-30 00:32:45,666 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
329 |
+
2025-03-30 00:32:45,667 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
330 |
+
2025-03-30 00:32:45,667 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\PycharmProjects\RagPipeline\RAG3\cached_data\vector_index
|
331 |
+
2025-04-10 23:37:57,365 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\RAG3_voice
|
332 |
+
2025-04-10 23:37:57,365 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3_voice\documents
|
333 |
+
2025-04-10 23:37:57,365 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3_voice\documents
|
334 |
+
2025-04-10 23:37:57,366 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3_voice\documents
|
335 |
+
2025-04-10 23:37:57,366 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3_voice\\documents\\RAG �Ʒÿ� Q.pdf']
|
336 |
+
2025-04-10 23:37:57,366 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
337 |
+
2025-04-10 23:37:57,366 - Config - INFO - DeepSeek API ���� ��Ʈ ����: https://api.deepseek.com/v1/chat/completions, ��: deepseek-chat
|
338 |
+
2025-04-10 23:38:04,670 - Config - INFO - DeepSeek API ���� ����
|
339 |
+
2025-04-10 23:38:04,674 - AutoRAG - WARNING - RAG ü�� ����� �ε��� �� �����ϴ�: cannot import name 'RAGChain' from 'rag_chain' (C:\Users\USER\RAG3_voice\rag_chain.py)
|
340 |
+
2025-04-10 23:38:04,678 - AutoRAG - WARNING - �������� ���� RAG ����� �ε��� �� �����ϴ�: No module named 'offline_fallback_rag'
|
341 |
+
2025-04-10 23:38:04,678 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
342 |
+
2025-04-10 23:38:04,679 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3_voice\documents
|
343 |
+
2025-04-10 23:38:04,679 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
344 |
+
2025-04-10 23:38:04,679 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3_voice\documents'
|
345 |
+
2025-04-10 23:38:04,679 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\RAG3_voice\cached_data'
|
346 |
+
2025-04-10 23:38:05,611 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
347 |
+
2025-04-10 23:38:05,823 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
348 |
+
2025-04-10 23:38:09,891 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
349 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
350 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3_voice\documents
|
351 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - ���丮 ����: ['.gitkeep', 'RAG �Ʒÿ� Q.pdf']
|
352 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf
|
353 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3_voice\\documents\\RAG �Ʒÿ� Q.pdf']
|
354 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
355 |
+
2025-04-10 23:38:09,891 - AutoRAG - INFO - ó�� ��: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf
|
356 |
+
2025-04-10 23:38:09,892 - AutoRAG - INFO - docling���� ó�� �õ�: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf
|
357 |
+
2025-04-10 23:38:09,892 - AutoRAG - WARNING - �ñ׳� ���� ���� (������ ȯ���� �� ����): module 'signal' has no attribute 'SIGALRM'
|
358 |
+
2025-04-10 23:38:09,911 - docling.document_converter - INFO - Going to convert document batch...
|
359 |
+
2025-04-10 23:38:09,911 - docling.document_converter - INFO - Initializing pipeline for StandardPdfPipeline with options hash 3d2abd0e021741887551c73bd132b421
|
360 |
+
2025-04-10 23:38:09,920 - docling.models.factories.base_factory - INFO - Loading plugin 'docling_defaults'
|
361 |
+
2025-04-10 23:38:09,921 - docling.models.factories - INFO - Registered ocr engines: ['easyocr', 'ocrmac', 'rapidocr', 'tesserocr', 'tesseract']
|
362 |
+
2025-04-10 23:38:09,951 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cpu'
|
363 |
+
2025-04-10 23:38:11,882 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cpu'
|
364 |
+
2025-04-10 23:38:12,840 - docling.utils.accelerator_utils - INFO - Accelerator device: 'cpu'
|
365 |
+
2025-04-10 23:38:13,366 - docling.models.factories.base_factory - INFO - Loading plugin 'docling_defaults'
|
366 |
+
2025-04-10 23:38:13,367 - docling.models.factories - INFO - Registered picture descriptions: ['vlm', 'api']
|
367 |
+
2025-04-10 23:38:13,367 - docling.pipeline.base_pipeline - INFO - Processing document RAG �Ʒÿ� Q.pdf
|
368 |
+
2025-04-10 23:38:14,803 - docling.document_converter - INFO - Finished converting document RAG �Ʒÿ� Q.pdf in 4.92 sec.
|
369 |
+
2025-04-10 23:38:14,825 - AutoRAG - INFO - ûũ ���� �Ϸ�: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
370 |
+
2025-04-10 23:38:14,825 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 4.93��
|
371 |
+
2025-04-10 23:38:14,825 - AutoRAG - INFO - �� ���� ��� ���� ��...
|
372 |
+
2025-04-10 23:38:14,826 - VectorStore - INFO - FAISS ��� ���� ��: 1�� ����
|
373 |
+
2025-04-10 23:38:15,340 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
374 |
+
2025-04-10 23:38:15,355 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
375 |
+
2025-04-10 23:38:15,359 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
376 |
+
2025-04-10 23:38:15,359 - VectorStore - INFO - FAISS �ε��� ���� �Ϸ�
|
377 |
+
2025-04-10 23:38:15,359 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
378 |
+
2025-04-10 23:38:15,360 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
379 |
+
2025-04-10 23:38:15,360 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
380 |
+
2025-04-10 23:38:15,360 - AutoRAG - INFO - �⺻ RAG Chain�� ����� �� ���� ��ü ������ �õ��մϴ�...
|
381 |
+
2025-04-10 23:38:15,360 - FallbackRAGChain - INFO - ���� RAG ü�� �ʱ�ȭ...
|
382 |
+
2025-04-10 23:38:15,360 - FallbackRAGChain - INFO - DeepSeek �� ���� �ʱ�ȭ: deepseek-chat
|
383 |
+
2025-04-10 23:38:15,360 - DirectDeepSeek - INFO - DirectDeepSeekClient �ʱ�ȭ: ��=deepseek-chat, ��������Ʈ=https://api.deepseek.com/v1/chat/completions
|
384 |
+
2025-04-10 23:38:15,360 - FallbackRAGChain - INFO - DeepSeek �� ���� �ʱ�ȭ ����
|
385 |
+
2025-04-10 23:38:15,361 - FallbackRAGChain - INFO - ���� RAG ü�� �ʱ�ȭ �Ϸ�
|
386 |
+
2025-04-10 23:38:15,361 - AutoRAG - INFO - ���� RAG ü�� �ʱ�ȭ ����
|
387 |
+
2025-04-10 23:38:15,361 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
388 |
+
2025-04-10 23:38:16,336 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
389 |
+
2025-04-10 23:38:16,681 - DeepSeekUtils - INFO - DeepSeek API ���� ��Ʈ ����: https://api.deepseek.com/v1/chat/completions, ��: deepseek-chat
|
390 |
+
2025-04-10 23:38:17,260 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
391 |
+
2025-04-10 23:38:20,420 - DeepSeekUtils - INFO - DeepSeek API ���� ����
|
392 |
+
2025-04-10 23:38:20,534 - httpx - INFO - HTTP Request: GET http://127.0.0.1:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
|
393 |
+
2025-04-10 23:38:20,545 - httpx - INFO - HTTP Request: HEAD http://127.0.0.1:7860/ "HTTP/1.1 200 OK"
|
394 |
+
2025-04-10 23:44:10,873 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\RAG3_voice
|
395 |
+
2025-04-10 23:44:10,873 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3_voice\documents
|
396 |
+
2025-04-10 23:44:10,874 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3_voice\documents
|
397 |
+
2025-04-10 23:44:10,874 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3_voice\documents
|
398 |
+
2025-04-10 23:44:10,874 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3_voice\\documents\\RAG �Ʒÿ� Q.pdf']
|
399 |
+
2025-04-10 23:44:10,875 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
400 |
+
2025-04-10 23:44:10,875 - Config - INFO - DeepSeek API ���� ��Ʈ ����: https://api.deepseek.com/v1/chat/completions, ��: deepseek-chat
|
401 |
+
2025-04-10 23:44:17,355 - Config - INFO - DeepSeek API ���� ����
|
402 |
+
2025-04-10 23:44:17,358 - AutoRAG - WARNING - RAG ü�� ����� �ε��� �� �����ϴ�: cannot import name 'RAGChain' from 'rag_chain' (C:\Users\USER\RAG3_voice\rag_chain.py)
|
403 |
+
2025-04-10 23:44:17,363 - AutoRAG - WARNING - �������� ���� RAG ����� �ε��� �� �����ϴ�: No module named 'offline_fallback_rag'
|
404 |
+
2025-04-10 23:44:17,363 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
405 |
+
2025-04-10 23:44:17,364 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3_voice\documents
|
406 |
+
2025-04-10 23:44:17,364 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
407 |
+
2025-04-10 23:44:17,365 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3_voice\documents'
|
408 |
+
2025-04-10 23:44:17,366 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\RAG3_voice\cached_data'
|
409 |
+
2025-04-10 23:44:18,213 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
410 |
+
2025-04-10 23:44:18,460 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
411 |
+
2025-04-10 23:44:23,069 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
412 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
413 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3_voice\documents
|
414 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - ���丮 ����: ['.gitkeep', 'RAG �Ʒÿ� Q.pdf']
|
415 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf
|
416 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3_voice\\documents\\RAG �Ʒÿ� Q.pdf']
|
417 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
418 |
+
2025-04-10 23:44:23,070 - AutoRAG - INFO - ûũ �ε� �Ϸ�: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
419 |
+
2025-04-10 23:44:23,071 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 0.00��
|
420 |
+
2025-04-10 23:44:23,071 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
421 |
+
2025-04-10 23:44:23,071 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
422 |
+
2025-04-10 23:44:23,072 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
423 |
+
2025-04-10 23:44:23,083 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
424 |
+
2025-04-10 23:44:23,085 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
425 |
+
2025-04-10 23:44:23,086 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
426 |
+
2025-04-10 23:44:23,086 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
427 |
+
2025-04-10 23:44:23,086 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
428 |
+
2025-04-10 23:44:23,087 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
429 |
+
2025-04-10 23:44:23,087 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
430 |
+
2025-04-10 23:44:23,087 - AutoRAG - INFO - �⺻ RAG Chain�� ����� �� ���� ��ü ������ �õ��մϴ�...
|
431 |
+
2025-04-10 23:44:23,087 - FallbackRAGChain - INFO - ���� RAG ü�� �ʱ�ȭ...
|
432 |
+
2025-04-10 23:44:23,087 - FallbackRAGChain - INFO - DeepSeek �� ���� �ʱ�ȭ: deepseek-chat
|
433 |
+
2025-04-10 23:44:23,087 - DirectDeepSeek - INFO - DirectDeepSeekClient �ʱ�ȭ: ��=deepseek-chat, ��������Ʈ=https://api.deepseek.com/v1/chat/completions
|
434 |
+
2025-04-10 23:44:23,087 - FallbackRAGChain - INFO - DeepSeek �� ���� �ʱ�ȭ ����
|
435 |
+
2025-04-10 23:44:23,087 - FallbackRAGChain - INFO - ���� RAG ü�� �ʱ�ȭ �Ϸ�
|
436 |
+
2025-04-10 23:44:23,087 - AutoRAG - INFO - ���� RAG ü�� �ʱ�ȭ ����
|
437 |
+
2025-04-10 23:44:23,087 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
438 |
+
2025-04-10 23:44:23,988 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
439 |
+
2025-04-10 23:44:24,635 - DeepSeekUtils - INFO - DeepSeek API ���� ��Ʈ ����: https://api.deepseek.com/v1/chat/completions, ��: deepseek-chat
|
440 |
+
2025-04-10 23:44:25,196 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
441 |
+
2025-04-10 23:44:28,870 - DeepSeekUtils - INFO - DeepSeek API ���� ����
|
442 |
+
2025-04-10 23:44:29,004 - httpx - INFO - HTTP Request: GET http://127.0.0.1:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
|
443 |
+
2025-04-10 23:44:29,016 - httpx - INFO - HTTP Request: HEAD http://127.0.0.1:7860/ "HTTP/1.1 200 OK"
|
444 |
+
2025-04-11 00:59:57,522 - AutoRAG - INFO - ���� �۾� ���丮: C:\Users\USER\RAG3_voice
|
445 |
+
2025-04-11 00:59:57,523 - AutoRAG - INFO - ������ PDF ���丮: C:\Users\USER\RAG3_voice\documents
|
446 |
+
2025-04-11 00:59:57,523 - AutoRAG - INFO - ���� ��η� ��ȯ�� PDF ���丮: C:\Users\USER\RAG3_voice\documents
|
447 |
+
2025-04-11 00:59:57,523 - AutoRAG - INFO - PDF ���丮�� �����մϴ�: C:\Users\USER\RAG3_voice\documents
|
448 |
+
2025-04-11 00:59:57,524 - AutoRAG - INFO - ���丮 �� PDF ���� ���: ['C:\\Users\\USER\\RAG3_voice\\documents\\RAG �Ʒÿ� Q.pdf']
|
449 |
+
2025-04-11 00:59:57,524 - AutoRAG - INFO - ���ø����̼� ���� ���� ��...
|
450 |
+
2025-04-11 00:59:57,524 - Config - INFO - DeepSeek API ���� ��Ʈ ����: https://api.deepseek.com/v1/chat/completions, ��: deepseek-chat
|
451 |
+
2025-04-11 01:00:01,647 - Config - INFO - DeepSeek API ���� ����
|
452 |
+
2025-04-11 01:00:01,661 - AutoRAG - WARNING - RAG ü�� ����� �ε��� �� �����ϴ�: cannot import name 'RAGChain' from 'rag_chain' (C:\Users\USER\RAG3_voice\rag_chain.py)
|
453 |
+
2025-04-11 01:00:01,666 - AutoRAG - WARNING - �������� ���� RAG ����� �ε��� �� �����ϴ�: No module named 'offline_fallback_rag'
|
454 |
+
2025-04-11 01:00:01,667 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ ����
|
455 |
+
2025-04-11 01:00:01,668 - AutoRAG - INFO - ������ PDF ���丮 (���� ���): C:\Users\USER\RAG3_voice\documents
|
456 |
+
2025-04-11 01:00:01,668 - AutoRAG - INFO - PDF ���丮���� 1���� PDF ������ ã�ҽ��ϴ�: ['RAG �Ʒÿ� Q.pdf']
|
457 |
+
2025-04-11 01:00:01,669 - AutoRAG - INFO - PDF ���� ���丮: 'C:\Users\USER\RAG3_voice\documents'
|
458 |
+
2025-04-11 01:00:01,669 - AutoRAG - INFO - ij�� ���丮: 'C:\Users\USER\RAG3_voice\cached_data'
|
459 |
+
2025-04-11 01:00:02,471 - VectorStore - INFO - �Ӻ��� �� �ε� ��: Alibaba-NLP/gte-multilingual-base
|
460 |
+
2025-04-11 01:00:02,744 - sentence_transformers.SentenceTransformer - INFO - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
|
461 |
+
2025-04-11 01:00:07,061 - VectorStore - INFO - �Ӻ��� �� �ʱ�ȭ �Ϸ�: Alibaba-NLP/gte-multilingual-base
|
462 |
+
2025-04-11 01:00:07,061 - AutoRAG - INFO - ���� �ڵ� �ε� �� ó�� ����...
|
463 |
+
2025-04-11 01:00:07,061 - AutoRAG - INFO - PDF ���� �˻� ���: C:\Users\USER\RAG3_voice\documents
|
464 |
+
2025-04-11 01:00:07,061 - AutoRAG - INFO - ���丮 ����: ['.gitkeep', 'RAG �Ʒÿ� Q.pdf']
|
465 |
+
2025-04-11 01:00:07,061 - AutoRAG - INFO - PDF ���� ã��: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf
|
466 |
+
2025-04-11 01:00:07,061 - AutoRAG - INFO - �߰ߵ� ��� PDF ����: ['C:\\Users\\USER\\RAG3_voice\\documents\\RAG �Ʒÿ� Q.pdf']
|
467 |
+
2025-04-11 01:00:07,062 - AutoRAG - INFO - �߰ߵ� PDF ����: 1��
|
468 |
+
2025-04-11 01:00:07,062 - AutoRAG - INFO - ûũ �ε� �Ϸ�: C:\Users\USER\RAG3_voice\documents\RAG �Ʒÿ� Q.pdf (1�� ûũ)
|
469 |
+
2025-04-11 01:00:07,062 - AutoRAG - INFO - ���� ó�� �Ϸ�: 1�� ûũ, 0.00��
|
470 |
+
2025-04-11 01:00:07,062 - AutoRAG - INFO - ����� ���� �ε��� �ε� ��...
|
471 |
+
2025-04-11 01:00:07,062 - VectorStore - INFO - FAISS �ε��� �ε� ��: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
472 |
+
2025-04-11 01:00:07,065 - faiss.loader - INFO - Loading faiss with AVX2 support.
|
473 |
+
2025-04-11 01:00:07,173 - faiss.loader - INFO - Successfully loaded faiss with AVX2 support.
|
474 |
+
2025-04-11 01:00:07,177 - faiss - INFO - Failed to load GPU Faiss: name 'GpuIndexIVFFlat' is not defined. Will not load constructor refs for GPU indexes.
|
475 |
+
2025-04-11 01:00:07,185 - VectorStore - INFO - FAISS �ε��� �ε� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
476 |
+
2025-04-11 01:00:07,185 - AutoRAG - INFO - ���� �ε��� �ε� �Ϸ�
|
477 |
+
2025-04-11 01:00:07,186 - AutoRAG - INFO - ���� ��� ���� ��: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
478 |
+
2025-04-11 01:00:07,187 - VectorStore - INFO - FAISS �ε��� ���� ���� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
479 |
+
2025-04-11 01:00:07,187 - AutoRAG - INFO - ���� �ε��� ���� �Ϸ�: C:\Users\USER\RAG3_voice\cached_data\vector_index
|
480 |
+
2025-04-11 01:00:07,187 - AutoRAG - INFO - �⺻ RAG Chain�� ����� �� ���� ��ü ������ �õ��մϴ�...
|
481 |
+
2025-04-11 01:00:07,188 - FallbackRAGChain - INFO - ���� RAG ü�� �ʱ�ȭ...
|
482 |
+
2025-04-11 01:00:07,188 - FallbackRAGChain - INFO - DeepSeek �� ���� �ʱ�ȭ: deepseek-chat
|
483 |
+
2025-04-11 01:00:07,188 - DirectDeepSeek - INFO - DirectDeepSeekClient �ʱ�ȭ: ��=deepseek-chat, ��������Ʈ=https://api.deepseek.com/v1/chat/completions
|
484 |
+
2025-04-11 01:00:07,188 - FallbackRAGChain - INFO - DeepSeek �� ���� �ʱ�ȭ ����
|
485 |
+
2025-04-11 01:00:07,189 - FallbackRAGChain - INFO - ���� RAG ü�� �ʱ�ȭ �Ϸ�
|
486 |
+
2025-04-11 01:00:07,189 - AutoRAG - INFO - ���� RAG ü�� �ʱ�ȭ ����
|
487 |
+
2025-04-11 01:00:07,189 - AutoRAG - INFO - AutoRAGChatApp �ʱ�ȭ �Ϸ�
|
488 |
+
2025-04-11 01:00:08,133 - httpx - INFO - HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK"
|
489 |
+
2025-04-11 01:00:09,395 - DeepSeekUtils - INFO - DeepSeek API ���� ��Ʈ ����: https://api.deepseek.com/v1/chat/completions, ��: deepseek-chat
|
490 |
+
2025-04-11 01:00:09,953 - httpx - INFO - HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK"
|
491 |
+
2025-04-11 01:00:13,592 - DeepSeekUtils - INFO - DeepSeek API ���� ����
|
492 |
+
2025-04-11 01:00:13,714 - httpx - INFO - HTTP Request: GET http://127.0.0.1:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
|
493 |
+
2025-04-11 01:00:13,789 - httpx - INFO - HTTP Request: HEAD http://127.0.0.1:7860/ "HTTP/1.1 200 OK"
|
clova_stt.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
네이버 클로바 음성인식(STT) API 연동 모듈
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import json
|
6 |
+
import requests
|
7 |
+
import logging
|
8 |
+
from typing import Dict, Any
|
9 |
+
from dotenv import load_dotenv
|
10 |
+
|
11 |
+
# .env 파일 로드
|
12 |
+
load_dotenv()
|
13 |
+
|
14 |
+
# 로깅 설정
|
15 |
+
logger = logging.getLogger("ClovaSTT")
|
16 |
+
|
17 |
+
class ClovaSTT:
|
18 |
+
"""
|
19 |
+
네이버 클로바 음성인식(STT) API 클래스
|
20 |
+
"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""
|
24 |
+
클로바 STT 클라이언트 초기화
|
25 |
+
"""
|
26 |
+
# .env 파일에서 설정 가져오기
|
27 |
+
self.client_id = os.getenv("NAVER_CLIENT_ID", "")
|
28 |
+
self.client_secret = os.getenv("NAVER_CLIENT_SECRET", "")
|
29 |
+
|
30 |
+
# 클라이언트 ID와 Secret 검증
|
31 |
+
if not self.client_id or not self.client_secret:
|
32 |
+
logger.warning("네이버 클로바 API 키가 설정되지 않았습니다.")
|
33 |
+
logger.warning(".env 파일에 NAVER_CLIENT_ID와 NAVER_CLIENT_SECRET를 설정해주세요.")
|
34 |
+
else:
|
35 |
+
logger.info("네이버 클로바 STT API 설정 완료")
|
36 |
+
|
37 |
+
def recognize(self, audio_bytes, language="Kor") -> Dict[str, Any]:
|
38 |
+
"""
|
39 |
+
오디오 데이터를 텍스트로 변환
|
40 |
+
|
41 |
+
Args:
|
42 |
+
audio_bytes: 오디오 파일 바이트 데이터
|
43 |
+
language: 언어 코드 (기본값: 'Kor')
|
44 |
+
|
45 |
+
Returns:
|
46 |
+
인식된 텍스트 또는 오류 메시지
|
47 |
+
"""
|
48 |
+
if not self.client_id or not self.client_secret:
|
49 |
+
logger.error("API 키가 설정되지 않았습니다.")
|
50 |
+
return {"success": False, "error": "API 키가 설정되지 않았습니다."}
|
51 |
+
|
52 |
+
try:
|
53 |
+
# API 엔드포인트 URL
|
54 |
+
url = f"https://naveropenapi.apigw.ntruss.com/recog/v1/stt?lang={language}"
|
55 |
+
|
56 |
+
# 요청 헤더 설정
|
57 |
+
headers = {
|
58 |
+
"X-NCP-APIGW-API-KEY-ID": self.client_id,
|
59 |
+
"X-NCP-APIGW-API-KEY": self.client_secret,
|
60 |
+
"Content-Type": "application/octet-stream"
|
61 |
+
}
|
62 |
+
|
63 |
+
logger.info("네이버 클로바 STT 요청 전송 중...")
|
64 |
+
|
65 |
+
# API 요청 전송
|
66 |
+
response = requests.post(url, headers=headers, data=audio_bytes, timeout=30)
|
67 |
+
|
68 |
+
# 응답 처리
|
69 |
+
if response.status_code == 200:
|
70 |
+
result = response.json()
|
71 |
+
recognized_text = result.get("text", "")
|
72 |
+
logger.info(f"인식 성공: {recognized_text[:50]}...")
|
73 |
+
return {
|
74 |
+
"success": True,
|
75 |
+
"text": recognized_text,
|
76 |
+
"result": result
|
77 |
+
}
|
78 |
+
else:
|
79 |
+
logger.error(f"API 오류 응답: {response.status_code}, {response.text}")
|
80 |
+
return {
|
81 |
+
"success": False,
|
82 |
+
"error": f"API 오류: {response.status_code}",
|
83 |
+
"details": response.text
|
84 |
+
}
|
85 |
+
|
86 |
+
except Exception as e:
|
87 |
+
logger.error(f"음성인식 처리 중 오류 발생: {str(e)}")
|
88 |
+
return {
|
89 |
+
"success": False,
|
90 |
+
"error": "음성인식 처리 실패",
|
91 |
+
"details": str(e)
|
92 |
+
}
|
config.py
ADDED
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
벡터 스토어, 임베딩 모델, LLM 등 구성 요소 설정
|
3 |
+
환경 변수 및 .env 파일 활용 개선 버전 - HuggingFace 환경 지원 추가
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import logging
|
7 |
+
import sys
|
8 |
+
import re
|
9 |
+
import requests
|
10 |
+
import json
|
11 |
+
from pathlib import Path
|
12 |
+
from typing import Dict, Any
|
13 |
+
from dotenv import load_dotenv
|
14 |
+
|
15 |
+
# 로깅 설정
|
16 |
+
logger = logging.getLogger("Config")
|
17 |
+
|
18 |
+
# 현재 실행 위치 확인 (디버깅용)
|
19 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
20 |
+
logger.info(f"스크립트 디렉토리: {script_dir}")
|
21 |
+
logger.info(f"현재 작업 디렉토리: {os.getcwd()}")
|
22 |
+
logger.info(f"운영 체제: {os.name}")
|
23 |
+
|
24 |
+
# 환경 감지 - HuggingFace Space 환경인지 확인
|
25 |
+
IS_HUGGINGFACE = False
|
26 |
+
if os.getenv('SPACE_ID') is not None or os.getenv('SYSTEM') == 'spaces':
|
27 |
+
IS_HUGGINGFACE = True
|
28 |
+
logger.info("HuggingFace Spaces 환경이 감지되었습니다.")
|
29 |
+
else:
|
30 |
+
# 로컬 환경인 경우 .env 파일 로드
|
31 |
+
# .env 파일 위치 후보들
|
32 |
+
env_paths = [
|
33 |
+
".env", # 현재 디렉토리
|
34 |
+
os.path.join(script_dir, ".env"), # 스크립트 디렉토리
|
35 |
+
os.path.join(script_dir, "config", ".env"), # config 하위 디렉토리
|
36 |
+
os.path.join(os.path.dirname(script_dir), ".env"), # 상위 디렉토리
|
37 |
+
]
|
38 |
+
|
39 |
+
# .env 파일 찾아서 로드
|
40 |
+
env_loaded = False
|
41 |
+
for env_path in env_paths:
|
42 |
+
if os.path.isfile(env_path):
|
43 |
+
logger.info(f".env 파일 발견: {env_path}")
|
44 |
+
env_loaded = load_dotenv(env_path, verbose=True)
|
45 |
+
if env_loaded:
|
46 |
+
logger.info(f".env 파일 로드 성공: {env_path}")
|
47 |
+
break
|
48 |
+
|
49 |
+
if not env_loaded:
|
50 |
+
logger.warning(".env 파일을 찾을 수 없습니다. 기본값 또는 시스템 환경 변수를 사용합니다.")
|
51 |
+
|
52 |
+
logger.info(f"로컬 환경에서 실행 중입니다. (OS: {'Windows' if os.name == 'nt' else 'Unix/Linux/MacOS'})")
|
53 |
+
|
54 |
+
# Windows 환경 감지
|
55 |
+
IS_WINDOWS = os.name == 'nt'
|
56 |
+
|
57 |
+
# 유틸리티 함수: 환경 변수 가져오기 (HuggingFace 환경과 로컬 환경 구분)
|
58 |
+
def get_env(key: str, default: Any = None, required: bool = False) -> Any:
|
59 |
+
"""
|
60 |
+
환경 변수를 가져오는 유틸리티 함수 (HuggingFace 환경 지원)
|
61 |
+
|
62 |
+
Args:
|
63 |
+
key: 환경 변수 키
|
64 |
+
default: 환경 변수가 없을 경우 기본값
|
65 |
+
required: 환경 변수가 필수적인지 여부
|
66 |
+
|
67 |
+
Returns:
|
68 |
+
환경 변수 값 또는 기본값
|
69 |
+
"""
|
70 |
+
# HuggingFace Spaces 환경에서는 내부 환경변수 활용
|
71 |
+
if IS_HUGGINGFACE:
|
72 |
+
# HuggingFace Spaces에서는 시크릿 값을 직접 사용
|
73 |
+
# HF_SECRET_<KEY> 형식으로 저장된 시크릿 확인
|
74 |
+
hf_secret_key = f"HF_SECRET_{key.upper()}"
|
75 |
+
value = os.getenv(hf_secret_key)
|
76 |
+
|
77 |
+
# 시크릿이 없으면 일반 환경변수 확인
|
78 |
+
if value is None:
|
79 |
+
value = os.getenv(key, default)
|
80 |
+
else:
|
81 |
+
# 로컬 환경에서는 일반적인 방식으로 환경변수 가져오기
|
82 |
+
value = os.getenv(key, default)
|
83 |
+
|
84 |
+
if required and value is None:
|
85 |
+
if IS_HUGGINGFACE:
|
86 |
+
error_msg = f"필수 환경 변수 {key}가 설정되지 않았습니다. HuggingFace Space에서 시크릿을 설정해주세요."
|
87 |
+
logger.error(error_msg)
|
88 |
+
raise ValueError(error_msg)
|
89 |
+
else:
|
90 |
+
error_msg = f"필수 환경 변수 {key}가 설정되지 않았습니다. .env 파일에 추가해주세요."
|
91 |
+
logger.error(error_msg)
|
92 |
+
raise ValueError(error_msg)
|
93 |
+
|
94 |
+
return value
|
95 |
+
|
96 |
+
# 경로 생성 유틸리티 함수
|
97 |
+
def ensure_absolute_path(path_str: str) -> str:
|
98 |
+
"""
|
99 |
+
상대 경로를 절대 경로로 변환 (Windows 경로 지원)
|
100 |
+
|
101 |
+
Args:
|
102 |
+
path_str: 변환할 경로 문자열
|
103 |
+
|
104 |
+
Returns:
|
105 |
+
절대 경로
|
106 |
+
"""
|
107 |
+
# Windows 드라이브 문자(C:\ 등)로 시작하는 경로 확인
|
108 |
+
if IS_WINDOWS and re.match(r'^[a-zA-Z]:\\', path_str):
|
109 |
+
logger.info(f"Windows 절대 경로 감지: {path_str}")
|
110 |
+
# Windows 절대 경로는 그대로 사용
|
111 |
+
return path_str
|
112 |
+
|
113 |
+
path = Path(path_str)
|
114 |
+
if path.is_absolute():
|
115 |
+
return str(path)
|
116 |
+
|
117 |
+
# 스크립트 디렉토리 기준 경로
|
118 |
+
script_based_path = Path(script_dir) / path
|
119 |
+
|
120 |
+
# 현재 작업 디렉토리 기준 경로
|
121 |
+
cwd_based_path = Path.cwd() / path
|
122 |
+
|
123 |
+
# 두 경로 중 존재하는 경로 우선 사용
|
124 |
+
if script_based_path.exists():
|
125 |
+
return str(script_based_path)
|
126 |
+
elif cwd_based_path.exists():
|
127 |
+
return str(cwd_based_path)
|
128 |
+
else:
|
129 |
+
# 기본적으로 현재 작업 디렉토리 기준 경로 반환
|
130 |
+
return str(cwd_based_path)
|
131 |
+
|
132 |
+
# Windows 경로 처리를 위한 유틸리티 함수
|
133 |
+
def normalize_path(path_str: str) -> str:
|
134 |
+
"""
|
135 |
+
경로 문자열을 정규화하여 OS에 맞게 변환
|
136 |
+
|
137 |
+
Args:
|
138 |
+
path_str: 변환할 경로 문자열
|
139 |
+
|
140 |
+
Returns:
|
141 |
+
정규화된 경로
|
142 |
+
"""
|
143 |
+
# Windows 경로 형식('\')��� OS에 맞게 변환
|
144 |
+
return os.path.normpath(path_str)
|
145 |
+
|
146 |
+
# 기본 디렉토리 설정 (절대 경로로 변환)
|
147 |
+
PDF_DIRECTORY_RAW = get_env("PDF_DIRECTORY", "documents")
|
148 |
+
# Windows 백슬래시 이중 처리를 위해 정규화
|
149 |
+
PDF_DIRECTORY_RAW = normalize_path(PDF_DIRECTORY_RAW)
|
150 |
+
PDF_DIRECTORY = ensure_absolute_path(PDF_DIRECTORY_RAW)
|
151 |
+
|
152 |
+
CACHE_DIRECTORY_RAW = get_env("CACHE_DIRECTORY", "cached_data")
|
153 |
+
CACHE_DIRECTORY_RAW = normalize_path(CACHE_DIRECTORY_RAW)
|
154 |
+
CACHE_DIRECTORY = ensure_absolute_path(CACHE_DIRECTORY_RAW)
|
155 |
+
|
156 |
+
logger.info(f"PDF 디렉토리 (원본): {PDF_DIRECTORY_RAW}")
|
157 |
+
logger.info(f"PDF 디렉토리 (절대): {PDF_DIRECTORY}")
|
158 |
+
logger.info(f"캐시 디렉토리 (원본): {CACHE_DIRECTORY_RAW}")
|
159 |
+
logger.info(f"캐시 디렉토리 (절대): {CACHE_DIRECTORY}")
|
160 |
+
|
161 |
+
# 청킹 설정
|
162 |
+
CHUNK_SIZE = int(get_env("CHUNK_SIZE", "1000"))
|
163 |
+
CHUNK_OVERLAP = int(get_env("CHUNK_OVERLAP", "200"))
|
164 |
+
|
165 |
+
# API 키 및 환경 설정
|
166 |
+
OPENAI_API_KEY = get_env("OPENAI_API_KEY", "")
|
167 |
+
LANGFUSE_PUBLIC_KEY = get_env("LANGFUSE_PUBLIC_KEY", "")
|
168 |
+
LANGFUSE_SECRET_KEY = get_env("LANGFUSE_SECRET_KEY", "")
|
169 |
+
LANGFUSE_HOST = get_env("LANGFUSE_HOST", "https://cloud.langfuse.com")
|
170 |
+
|
171 |
+
# DeepSeek 관련 설정 추가
|
172 |
+
DEEPSEEK_API_KEY = get_env("DEEPSEEK_API_KEY", "")
|
173 |
+
DEEPSEEK_ENDPOINT = get_env("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions")
|
174 |
+
DEEPSEEK_MODEL = get_env("DEEPSEEK_MODEL", "deepseek-chat")
|
175 |
+
|
176 |
+
# 허깅페이스 환경에서 API 키 확인 및 로그 출력
|
177 |
+
if IS_HUGGINGFACE:
|
178 |
+
logger.info(f"허깅페이스 환경에서 DeepSeek API 키 존재 여부: {bool(DEEPSEEK_API_KEY)}")
|
179 |
+
# 보안을 위해 API 키 첫 4자리와 마지막 4자리만 표시 (키가 존재하는 경우)
|
180 |
+
if DEEPSEEK_API_KEY:
|
181 |
+
masked_key = DEEPSEEK_API_KEY[:4] + "****" + DEEPSEEK_API_KEY[-4:] if len(DEEPSEEK_API_KEY) > 8 else "****"
|
182 |
+
logger.info(f"DeepSeek API 키: {masked_key}")
|
183 |
+
|
184 |
+
logger.info(f"DeepSeek 모델: {DEEPSEEK_MODEL}")
|
185 |
+
logger.info(f"DeepSeek 엔드포인트: {DEEPSEEK_ENDPOINT}")
|
186 |
+
|
187 |
+
# Milvus 벡터 DB 설정
|
188 |
+
MILVUS_HOST = get_env("MILVUS_HOST", "localhost")
|
189 |
+
MILVUS_PORT = get_env("MILVUS_PORT", "19530")
|
190 |
+
MILVUS_COLLECTION = get_env("MILVUS_COLLECTION", "pdf_documents")
|
191 |
+
|
192 |
+
# 임베딩 모델 설정
|
193 |
+
EMBEDDING_MODEL = get_env("EMBEDDING_MODEL", "Alibaba-NLP/gte-multilingual-base") # 다국어 지원 모델
|
194 |
+
RERANKER_MODEL = get_env("RERANKER_MODEL", "Alibaba-NLP/gte-multilingual-reranker-base") # 다국어 지원 리랭커
|
195 |
+
|
196 |
+
# LLM 모델 설정 (환경에 따라 자동 선택)
|
197 |
+
USE_OPENAI = get_env("USE_OPENAI", "False").lower() == "true"
|
198 |
+
USE_DEEPSEEK = get_env("USE_DEEPSEEK", "False").lower() == "true"
|
199 |
+
|
200 |
+
# 허깅페이스 환경에서는 DeepSeek 우선 사용
|
201 |
+
if IS_HUGGINGFACE:
|
202 |
+
# 허깅페이스 환경에서 DeepSeek API 키가 있는지 확인
|
203 |
+
if DEEPSEEK_API_KEY:
|
204 |
+
USE_DEEPSEEK = True
|
205 |
+
USE_OPENAI = False
|
206 |
+
LLM_MODEL = DEEPSEEK_MODEL
|
207 |
+
logger.info("HuggingFace Spaces 환경: DeepSeek 모델 사용")
|
208 |
+
else:
|
209 |
+
logger.warning("HuggingFace Spaces 환경에서 DeepSeek API 키가 설정되지 않았습니다.")
|
210 |
+
USE_DEEPSEEK = False
|
211 |
+
USE_OPENAI = False # 기본적으로 API 키가 없으면 비활성화
|
212 |
+
LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest") # 대체 모델 설정
|
213 |
+
logger.info(f"HuggingFace Spaces 환경: DeepSeek API 키 없음, LLM 모델: {LLM_MODEL}")
|
214 |
+
else:
|
215 |
+
# 로컬 환경에서는 설정에 따라 LLM 선택
|
216 |
+
if USE_DEEPSEEK:
|
217 |
+
LLM_MODEL = DEEPSEEK_MODEL
|
218 |
+
logger.info(f"로컬 환경: DeepSeek 모델 사용 ({DEEPSEEK_MODEL})")
|
219 |
+
elif USE_OPENAI:
|
220 |
+
LLM_MODEL = get_env("LLM_MODEL", "gpt-3.5-turbo")
|
221 |
+
logger.info(f"로컬 환경: OpenAI 모델 사용 ({LLM_MODEL})")
|
222 |
+
else:
|
223 |
+
LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest")
|
224 |
+
OLLAMA_HOST = get_env("OLLAMA_HOST", "http://localhost:11434")
|
225 |
+
logger.info(f"로컬 환경: Ollama 모델 사용 ({LLM_MODEL})")
|
226 |
+
|
227 |
+
# API 키 검증 (로컬 환경만)
|
228 |
+
if not IS_HUGGINGFACE:
|
229 |
+
if USE_DEEPSEEK and not DEEPSEEK_API_KEY:
|
230 |
+
logger.warning("DeepSeek 모델이 선택되었지만 API 키가 설정되지 않았습니다.")
|
231 |
+
USE_DEEPSEEK = False
|
232 |
+
USE_OPENAI = False
|
233 |
+
LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest")
|
234 |
+
logger.info("DeepSeek API 키가 없어 Ollama로 폴백합니다.")
|
235 |
+
elif USE_OPENAI and not OPENAI_API_KEY:
|
236 |
+
logger.warning("OpenAI 모델이 선택되었지만 API 키가 설정되지 않았습니다.")
|
237 |
+
logger.warning("OpenAI API 키가 없어 Ollama로 폴백합니다.")
|
238 |
+
USE_OPENAI = False
|
239 |
+
LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest")
|
240 |
+
|
241 |
+
# DeepSeek API 테스트 함수
|
242 |
+
def test_deepseek_connection():
|
243 |
+
"""
|
244 |
+
DeepSeek API 연결 테스트
|
245 |
+
|
246 |
+
Returns:
|
247 |
+
테스트 결과 딕셔너리 (성공 여부 및 메시지)
|
248 |
+
"""
|
249 |
+
if not DEEPSEEK_API_KEY:
|
250 |
+
logger.warning("DeepSeek API 키가 설정되지 않아 테스트를 건너뜁니다.")
|
251 |
+
return {
|
252 |
+
"success": False,
|
253 |
+
"message": "API 키가 설정되지 않았습니다.",
|
254 |
+
"status_code": None
|
255 |
+
}
|
256 |
+
|
257 |
+
try:
|
258 |
+
logger.info(f"DeepSeek API 연결 테스트 시작: {DEEPSEEK_ENDPOINT}, 모델: {DEEPSEEK_MODEL}")
|
259 |
+
|
260 |
+
# 테스트용 간단한 프롬프트
|
261 |
+
test_prompt = "Hello, please respond with a short greeting."
|
262 |
+
|
263 |
+
# API 요청 헤더 및 데이터
|
264 |
+
headers = {
|
265 |
+
"Content-Type": "application/json",
|
266 |
+
"Authorization": f"Bearer {DEEPSEEK_API_KEY}"
|
267 |
+
}
|
268 |
+
|
269 |
+
payload = {
|
270 |
+
"model": DEEPSEEK_MODEL,
|
271 |
+
"messages": [{"role": "user", "content": test_prompt}],
|
272 |
+
"temperature": 0.7,
|
273 |
+
"max_tokens": 50
|
274 |
+
}
|
275 |
+
|
276 |
+
# API 요청 전송
|
277 |
+
response = requests.post(
|
278 |
+
DEEPSEEK_ENDPOINT,
|
279 |
+
headers=headers,
|
280 |
+
json=payload,
|
281 |
+
timeout=10 # 10초 타임아웃
|
282 |
+
)
|
283 |
+
|
284 |
+
# 응답 확인
|
285 |
+
if response.status_code == 200:
|
286 |
+
logger.info("DeepSeek API 연결 성공")
|
287 |
+
return {
|
288 |
+
"success": True,
|
289 |
+
"message": "API 연결 성공",
|
290 |
+
"status_code": response.status_code
|
291 |
+
}
|
292 |
+
else:
|
293 |
+
logger.error(f"DeepSeek API 오류: 상태 코드 {response.status_code}")
|
294 |
+
error_message = ""
|
295 |
+
try:
|
296 |
+
error_data = response.json()
|
297 |
+
error_message = error_data.get("error", {}).get("message", str(error_data))
|
298 |
+
except:
|
299 |
+
error_message = response.text
|
300 |
+
|
301 |
+
return {
|
302 |
+
"success": False,
|
303 |
+
"message": f"API 오류: {error_message}",
|
304 |
+
"status_code": response.status_code
|
305 |
+
}
|
306 |
+
|
307 |
+
except requests.exceptions.Timeout:
|
308 |
+
logger.error("DeepSeek API 요청 시간 초과")
|
309 |
+
return {
|
310 |
+
"success": False,
|
311 |
+
"message": "API 요청 시간 초과",
|
312 |
+
"status_code": None
|
313 |
+
}
|
314 |
+
except requests.exceptions.ConnectionError:
|
315 |
+
logger.error("DeepSeek API 연결 실패")
|
316 |
+
return {
|
317 |
+
"success": False,
|
318 |
+
"message": "API 서버 연결 실패",
|
319 |
+
"status_code": None
|
320 |
+
}
|
321 |
+
except Exception as e:
|
322 |
+
logger.error(f"DeepSeek API 테스트 중 예상치 못한 오류: {e}", exc_info=True)
|
323 |
+
return {
|
324 |
+
"success": False,
|
325 |
+
"message": f"예상치 못한 오류: {str(e)}",
|
326 |
+
"status_code": None
|
327 |
+
}
|
328 |
+
|
329 |
+
# 벡터 검색 설정
|
330 |
+
TOP_K_RETRIEVAL = int(get_env("TOP_K_RETRIEVAL", "5")) # 벡터 검색 결과 수
|
331 |
+
TOP_K_RERANK = int(get_env("TOP_K_RERANK", "3")) # 리랭킹 후 선택할 결과 수
|
332 |
+
|
333 |
+
# 로깅 설정
|
334 |
+
LOG_LEVEL = get_env("LOG_LEVEL", "INFO")
|
335 |
+
LOG_FILE = get_env("LOG_FILE", "autorag.log")
|
336 |
+
|
337 |
+
# 설정 정보 출력 (디버깅용)
|
338 |
+
def print_config():
|
339 |
+
"""현재 설정 정보를 로그에 출력"""
|
340 |
+
logger.info("===== 현재 설정 정보 =====")
|
341 |
+
logger.info(f"실행 환경: {'HuggingFace Spaces' if IS_HUGGINGFACE else '로컬'}")
|
342 |
+
logger.info(f"문서 디렉토리: {PDF_DIRECTORY}")
|
343 |
+
logger.info(f"캐시 디렉토리: {CACHE_DIRECTORY}")
|
344 |
+
logger.info(f"청크 크기: {CHUNK_SIZE}, 오버랩: {CHUNK_OVERLAP}")
|
345 |
+
logger.info(f"OpenAI 사용: {USE_OPENAI}")
|
346 |
+
logger.info(f"DeepSeek 사용: {USE_DEEPSEEK}")
|
347 |
+
logger.info(f"LLM 모델: {LLM_MODEL}")
|
348 |
+
if not USE_OPENAI and not USE_DEEPSEEK and not IS_HUGGINGFACE:
|
349 |
+
logger.info(f"Ollama 호스트: {OLLAMA_HOST}")
|
350 |
+
logger.info(f"임베딩 모델: {EMBEDDING_MODEL}")
|
351 |
+
logger.info(f"리랭커 모델: {RERANKER_MODEL}")
|
352 |
+
logger.info(f"TOP_K 검색: {TOP_K_RETRIEVAL}, 리랭킹: {TOP_K_RERANK}")
|
353 |
+
logger.info("=========================")
|
354 |
+
|
355 |
+
# 설정 유효성 검사
|
356 |
+
def validate_config() -> Dict[str, Any]:
|
357 |
+
"""
|
358 |
+
현재 설정의 유효성을 검사하고 경고나 오류를 로그에 기록
|
359 |
+
|
360 |
+
Returns:
|
361 |
+
검증 결과 (status: 상태, warnings: 경고 목록)
|
362 |
+
"""
|
363 |
+
warnings = []
|
364 |
+
|
365 |
+
# 디렉토리 확인
|
366 |
+
if not os.path.exists(PDF_DIRECTORY):
|
367 |
+
warnings.append(f"PDF 디렉토리({PDF_DIRECTORY})가 존재하지 않습니다.")
|
368 |
+
|
369 |
+
# API 키 확인 (허깅페이스와 로컬 환경 구분)
|
370 |
+
if IS_HUGGINGFACE:
|
371 |
+
if USE_DEEPSEEK and not DEEPSEEK_API_KEY:
|
372 |
+
warnings.append("허깅페이스 환경에서 DeepSeek 사용이 설정되었지만 API 키가 제공되지 않았습니다.")
|
373 |
+
else:
|
374 |
+
if USE_OPENAI and not OPENAI_API_KEY:
|
375 |
+
warnings.append("OpenAI 사용이 설정되었지만 API 키가 제공되지 않았습니다.")
|
376 |
+
|
377 |
+
if USE_DEEPSEEK and not DEEPSEEK_API_KEY:
|
378 |
+
warnings.append("DeepSeek 사용이 설정되었지만 API 키가 제공되지 않았습니다.")
|
379 |
+
|
380 |
+
# 모델 및 설정 값 확인
|
381 |
+
if CHUNK_SIZE <= CHUNK_OVERLAP:
|
382 |
+
warnings.append(f"청크 크기({CHUNK_SIZE})가 오버랩({CHUNK_OVERLAP})보다 작거나 같습니다.")
|
383 |
+
|
384 |
+
# DeepSeek API 연결 확인 (설정된 경우)
|
385 |
+
if USE_DEEPSEEK and DEEPSEEK_API_KEY:
|
386 |
+
deepseek_test_result = test_deepseek_connection()
|
387 |
+
if not deepseek_test_result["success"]:
|
388 |
+
warnings.append(f"DeepSeek API 연결 테스트 실패: {deepseek_test_result['message']}")
|
389 |
+
|
390 |
+
# 결과 기록
|
391 |
+
if warnings:
|
392 |
+
for warning in warnings:
|
393 |
+
logger.warning(warning)
|
394 |
+
|
395 |
+
return {
|
396 |
+
"status": "valid" if not warnings else "warnings",
|
397 |
+
"warnings": warnings
|
398 |
+
}
|
399 |
+
|
400 |
+
# 설정 로드 시 실행
|
401 |
+
print_config()
|
402 |
+
config_status = validate_config()
|
custom_rag_chain.py
ADDED
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
DeepSeek API를 활용한 커스텀 RAG 체인 구현
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import logging
|
6 |
+
import time
|
7 |
+
from typing import List, Dict, Any, Optional, Tuple
|
8 |
+
|
9 |
+
from langchain.schema import Document
|
10 |
+
from langchain.prompts import PromptTemplate
|
11 |
+
from langchain_core.output_parsers import StrOutputParser
|
12 |
+
from langchain_core.runnables import RunnablePassthrough
|
13 |
+
|
14 |
+
# DeepSeek 커스텀 LLM 임포트
|
15 |
+
from deepseek_llm import DeepSeekLLM, DeepSeekChat
|
16 |
+
|
17 |
+
# 설정 가져오기
|
18 |
+
try:
|
19 |
+
from config import (
|
20 |
+
DEEPSEEK_API_KEY, DEEPSEEK_MODEL, DEEPSEEK_ENDPOINT,
|
21 |
+
TOP_K_RETRIEVAL, TOP_K_RERANK
|
22 |
+
)
|
23 |
+
except ImportError:
|
24 |
+
# 설정 모듈을 가져올 수 없는 경우 기본값 설정
|
25 |
+
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
26 |
+
DEEPSEEK_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
|
27 |
+
DEEPSEEK_ENDPOINT = os.environ.get("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions")
|
28 |
+
TOP_K_RETRIEVAL = int(os.environ.get("TOP_K_RETRIEVAL", "5"))
|
29 |
+
TOP_K_RERANK = int(os.environ.get("TOP_K_RERANK", "3"))
|
30 |
+
|
31 |
+
# 로깅 설정
|
32 |
+
logger = logging.getLogger("CustomRAGChain")
|
33 |
+
|
34 |
+
|
35 |
+
class CustomRAGChain:
|
36 |
+
"""
|
37 |
+
DeepSeek API를 활용한 커스텀 RAG 체인
|
38 |
+
"""
|
39 |
+
|
40 |
+
def __init__(self, vector_store, use_reranker=False):
|
41 |
+
"""
|
42 |
+
RAG 체인 초기화
|
43 |
+
|
44 |
+
Args:
|
45 |
+
vector_store: 벡터 스토어 인스턴스
|
46 |
+
use_reranker: 리랭커 사용 여부 (현재 미지원)
|
47 |
+
"""
|
48 |
+
logger.info("커스텀 RAG 체인 초기화...")
|
49 |
+
self.vector_store = vector_store
|
50 |
+
self.use_reranker = use_reranker
|
51 |
+
|
52 |
+
# API 키 확인
|
53 |
+
if not DEEPSEEK_API_KEY:
|
54 |
+
logger.error("DeepSeek API 키가 설정되지 않았습니다.")
|
55 |
+
raise ValueError("DeepSeek API 키가 설정되지 않았습니다.")
|
56 |
+
|
57 |
+
# DeepSeek LLM 초기화
|
58 |
+
try:
|
59 |
+
self.llm = DeepSeekLLM(
|
60 |
+
api_key=DEEPSEEK_API_KEY,
|
61 |
+
model=DEEPSEEK_MODEL,
|
62 |
+
endpoint=DEEPSEEK_ENDPOINT,
|
63 |
+
temperature=0.3,
|
64 |
+
max_tokens=1000,
|
65 |
+
request_timeout=120,
|
66 |
+
max_retries=5
|
67 |
+
)
|
68 |
+
logger.info(f"DeepSeek LLM 초기화 성공: {DEEPSEEK_MODEL}")
|
69 |
+
except Exception as e:
|
70 |
+
logger.error(f"DeepSeek LLM 초기화 실패: {e}")
|
71 |
+
raise ValueError(f"DeepSeek LLM 초기화 실패: {str(e)}")
|
72 |
+
|
73 |
+
# 챗 인터페이스 초기화 (대체용)
|
74 |
+
self.chat = DeepSeekChat(
|
75 |
+
api_key=DEEPSEEK_API_KEY,
|
76 |
+
model=DEEPSEEK_MODEL,
|
77 |
+
endpoint=DEEPSEEK_ENDPOINT
|
78 |
+
)
|
79 |
+
|
80 |
+
# RAG 프롬프트 템플릿
|
81 |
+
self.prompt = PromptTemplate.from_template("""
|
82 |
+
다음 정보를 기반으로 질문에 정확하게 답변해주세요.
|
83 |
+
|
84 |
+
질문: {question}
|
85 |
+
|
86 |
+
참고 정보:
|
87 |
+
{context}
|
88 |
+
|
89 |
+
참고 정보에 답이 있으면 반드시 그 정보를 기반으로 답변하세요.
|
90 |
+
참고 정보에 답이 없는 경우에는 일반적인 지식을 활용하여 답변할 수 있지만, "제공된 문서에는 이 정보가 없으나, 일반적으로는..." 식으로 시작하세요.
|
91 |
+
답변은 정확하고 간결하게 제공하되, 가능한 참고 정보에서 근거를 찾아 설명해주세요.
|
92 |
+
참고 정보의 출처도 함께 알려주세요.
|
93 |
+
""")
|
94 |
+
|
95 |
+
# RAG 체인 구성
|
96 |
+
self.chain = (
|
97 |
+
{"context": self._retrieve, "question": RunnablePassthrough()}
|
98 |
+
| self.prompt
|
99 |
+
| self.llm
|
100 |
+
| StrOutputParser()
|
101 |
+
)
|
102 |
+
|
103 |
+
logger.info("커스텀 RAG 체인 초기화 완료")
|
104 |
+
|
105 |
+
def _retrieve(self, query: str) -> str:
|
106 |
+
"""
|
107 |
+
쿼리에 대한 관련 문서 검색 및 컨텍스트 구성
|
108 |
+
|
109 |
+
Args:
|
110 |
+
query: 사용자 질문
|
111 |
+
|
112 |
+
Returns:
|
113 |
+
검색 결과를 포함한 컨텍스트 문자열
|
114 |
+
"""
|
115 |
+
if not query or not query.strip():
|
116 |
+
logger.warning("빈 쿼리로 검색 시도")
|
117 |
+
return "검색 쿼리가 비어있습니다."
|
118 |
+
|
119 |
+
try:
|
120 |
+
# 벡터 검색 수행
|
121 |
+
logger.info(f"벡터 검색 수행: '{query[:50]}{'...' if len(query) > 50 else ''}'")
|
122 |
+
docs = self.vector_store.similarity_search(query, k=TOP_K_RETRIEVAL)
|
123 |
+
|
124 |
+
if not docs:
|
125 |
+
logger.warning("검색 결과가 없습니다")
|
126 |
+
return "관련 문서를 찾을 수 없습니다."
|
127 |
+
|
128 |
+
# 검색 결과 컨텍스트 구성
|
129 |
+
context_parts = []
|
130 |
+
for i, doc in enumerate(docs, 1):
|
131 |
+
source = doc.metadata.get("source", "알 수 없는 출처")
|
132 |
+
page = doc.metadata.get("page", "")
|
133 |
+
source_info = f"{source}"
|
134 |
+
if page:
|
135 |
+
source_info += f" (페이지: {page})"
|
136 |
+
|
137 |
+
context_parts.append(f"[참고자료 {i}] - 출처: {source_info}\n{doc.page_content}\n")
|
138 |
+
|
139 |
+
context = "\n".join(context_parts)
|
140 |
+
|
141 |
+
# ��텍스트 길이 제한 (토큰 수 제한)
|
142 |
+
if len(context) > 6000:
|
143 |
+
logger.warning(f"컨텍스트가 너무 깁니다 ({len(context)} 문자). 제한합니다.")
|
144 |
+
context = context[:2500] + "\n...(중략)...\n" + context[-2500:]
|
145 |
+
|
146 |
+
logger.info(f"컨텍스트 생성 완료: {len(context_parts)}개 문서, {len(context)} 문자")
|
147 |
+
return context
|
148 |
+
|
149 |
+
except Exception as e:
|
150 |
+
logger.error(f"검색 중 오류: {e}")
|
151 |
+
return f"검색 중 오류 발생: {str(e)}"
|
152 |
+
|
153 |
+
def run(self, query: str) -> str:
|
154 |
+
"""
|
155 |
+
사용자 쿼리에 대한 RAG 파이프라인 실행
|
156 |
+
|
157 |
+
Args:
|
158 |
+
query: 사용자 질문
|
159 |
+
|
160 |
+
Returns:
|
161 |
+
모델 응답 문자열
|
162 |
+
"""
|
163 |
+
if not query or not query.strip():
|
164 |
+
logger.warning("빈 쿼리로 실행 시도")
|
165 |
+
return "질문이 비어있습니다. 질문을 입력해 주세요."
|
166 |
+
|
167 |
+
try:
|
168 |
+
logger.info(f"RAG 체인 실행: '{query[:50]}{'...' if len(query) > 50 else ''}'")
|
169 |
+
start_time = time.time()
|
170 |
+
|
171 |
+
# 벡터 검색 실행
|
172 |
+
context = self._retrieve(query)
|
173 |
+
|
174 |
+
# 직접 LLM 호출 (체인 사용)
|
175 |
+
try:
|
176 |
+
response = self.chain.invoke(query)
|
177 |
+
logger.info(f"LangChain 체인 호출 성공")
|
178 |
+
except Exception as chain_error:
|
179 |
+
logger.error(f"체인 호출 실패: {chain_error}, 대체 방식 시도")
|
180 |
+
|
181 |
+
# 대체 방식: 직접 채팅 API 호출
|
182 |
+
try:
|
183 |
+
prompt = self.prompt.format(question=query, context=context)
|
184 |
+
response = self.chat.generate([{"role": "user", "content": prompt}])
|
185 |
+
logger.info("대체 채팅 API 호출 성공")
|
186 |
+
except Exception as chat_error:
|
187 |
+
logger.error(f"대체 채팅 API 호출 실패: {chat_error}")
|
188 |
+
|
189 |
+
# 미리 정의된 응답으로 폴백
|
190 |
+
predefined_answers = {
|
191 |
+
"대한민국의 수도": "대한민국의 수도는 서울입니다.",
|
192 |
+
"수도": "대한민국의 수도는 서울입니다.",
|
193 |
+
"누구야": "저는 RAG 기반 질의응답 시스템입니다. 문서를 검색하고 관련 정보를 찾아드립니다.",
|
194 |
+
"안녕": "안녕하세요! 무엇을 도와드릴까요?",
|
195 |
+
"뭐해": "사용자의 질문에 답변하기 위해 문서를 검색하고 있습니다. 무엇을 알려드릴까요?"
|
196 |
+
}
|
197 |
+
|
198 |
+
# 질문에 맞는 미리 정의된 응답이 있는지 확인
|
199 |
+
for key, answer in predefined_answers.items():
|
200 |
+
if key in query.lower():
|
201 |
+
response = answer
|
202 |
+
logger.info(f"미리 정의된 응답 제공: {key}")
|
203 |
+
break
|
204 |
+
else:
|
205 |
+
# 검색 결과만 표시
|
206 |
+
response = f"""
|
207 |
+
API 연결 오류로 인해 검색 결과만 표시합니다.
|
208 |
+
|
209 |
+
질문: {query}
|
210 |
+
|
211 |
+
검색된 관련 문서:
|
212 |
+
{context}
|
213 |
+
|
214 |
+
[참고] API 연결 문제로 인해 자동 요약이 제공되지 않습니다. 다시 시도하거나 다른 질문을 해보세요.
|
215 |
+
"""
|
216 |
+
logger.info("검색 결과만 표시")
|
217 |
+
|
218 |
+
end_time = time.time()
|
219 |
+
logger.info(f"RAG 체인 실행 완료: {end_time - start_time:.2f}초")
|
220 |
+
return response
|
221 |
+
|
222 |
+
except Exception as e:
|
223 |
+
logger.error(f"RAG 체인 실행 중 오류: {e}")
|
224 |
+
return f"질문 처리 중 오류가 발생했습니다: {str(e)}"
|
deepseek_utils.py
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
DeepSeek API 테스트 및 유틸리티 기능
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import logging
|
6 |
+
import requests
|
7 |
+
import json
|
8 |
+
from typing import Dict, Any, Optional
|
9 |
+
|
10 |
+
# 로깅 설정
|
11 |
+
logger = logging.getLogger("DeepSeekUtils")
|
12 |
+
|
13 |
+
|
14 |
+
class DeepSeekError(Exception):
|
15 |
+
"""DeepSeek API 관련 오류"""
|
16 |
+
pass
|
17 |
+
|
18 |
+
|
19 |
+
def test_deepseek_api(api_key: str, endpoint: str, model: str) -> Dict[str, Any]:
|
20 |
+
"""
|
21 |
+
DeepSeek API 연결 테스트
|
22 |
+
|
23 |
+
Args:
|
24 |
+
api_key: DeepSeek API 키
|
25 |
+
endpoint: DeepSeek API 엔드포인트
|
26 |
+
model: 사용할 모델명
|
27 |
+
|
28 |
+
Returns:
|
29 |
+
테스트 결과 딕셔너리 (성공 여부 및 메시지)
|
30 |
+
"""
|
31 |
+
if not api_key:
|
32 |
+
logger.error("DeepSeek API 키가 제공되지 않았습니다.")
|
33 |
+
return {
|
34 |
+
"success": False,
|
35 |
+
"message": "API 키가 제공되지 않았습니다.",
|
36 |
+
"status_code": None,
|
37 |
+
"response": None
|
38 |
+
}
|
39 |
+
|
40 |
+
try:
|
41 |
+
logger.info(f"DeepSeek API 연결 테스트 시작: {endpoint}, 모델: {model}")
|
42 |
+
|
43 |
+
# 테스트용 간단한 프롬프트
|
44 |
+
test_prompt = "Hello, please respond with a short greeting."
|
45 |
+
|
46 |
+
# API 요청 헤더 및 데이터
|
47 |
+
headers = {
|
48 |
+
"Content-Type": "application/json",
|
49 |
+
"Authorization": f"Bearer {api_key}"
|
50 |
+
}
|
51 |
+
|
52 |
+
payload = {
|
53 |
+
"model": model,
|
54 |
+
"messages": [{"role": "user", "content": test_prompt}],
|
55 |
+
"temperature": 0.7,
|
56 |
+
"max_tokens": 50
|
57 |
+
}
|
58 |
+
|
59 |
+
# API 요청 전송
|
60 |
+
response = requests.post(
|
61 |
+
endpoint,
|
62 |
+
headers=headers,
|
63 |
+
data=json.dumps(payload),
|
64 |
+
timeout=10 # 10초 타임아웃
|
65 |
+
)
|
66 |
+
|
67 |
+
# 응답 확인
|
68 |
+
if response.status_code == 200:
|
69 |
+
logger.info("DeepSeek API 연결 성공")
|
70 |
+
response_data = response.json()
|
71 |
+
|
72 |
+
# 응답 내용 확인
|
73 |
+
if "choices" in response_data and len(response_data["choices"]) > 0:
|
74 |
+
message_content = response_data["choices"][0].get("message", {}).get("content", "")
|
75 |
+
return {
|
76 |
+
"success": True,
|
77 |
+
"message": "API 연결 성공",
|
78 |
+
"status_code": response.status_code,
|
79 |
+
"response": message_content[:100] + "..." if len(message_content) > 100 else message_content
|
80 |
+
}
|
81 |
+
else:
|
82 |
+
return {
|
83 |
+
"success": True,
|
84 |
+
"message": "API 연결 성공했으나 응답 형식이 예상과 다릅니다.",
|
85 |
+
"status_code": response.status_code,
|
86 |
+
"response": response_data
|
87 |
+
}
|
88 |
+
else:
|
89 |
+
logger.error(f"DeepSeek API 오류: 상태 코드 {response.status_code}")
|
90 |
+
error_message = ""
|
91 |
+
try:
|
92 |
+
error_data = response.json()
|
93 |
+
error_message = error_data.get("error", {}).get("message", str(error_data))
|
94 |
+
except:
|
95 |
+
error_message = response.text
|
96 |
+
|
97 |
+
return {
|
98 |
+
"success": False,
|
99 |
+
"message": f"API 오류: {error_message}",
|
100 |
+
"status_code": response.status_code,
|
101 |
+
"response": error_message
|
102 |
+
}
|
103 |
+
|
104 |
+
except requests.exceptions.Timeout:
|
105 |
+
logger.error("DeepSeek API 요청 시간 초과")
|
106 |
+
return {
|
107 |
+
"success": False,
|
108 |
+
"message": "API 요청 시간 초과",
|
109 |
+
"status_code": None,
|
110 |
+
"response": None
|
111 |
+
}
|
112 |
+
except requests.exceptions.ConnectionError:
|
113 |
+
logger.error("DeepSeek API 연결 실패")
|
114 |
+
return {
|
115 |
+
"success": False,
|
116 |
+
"message": "API 서버 연결 실패",
|
117 |
+
"status_code": None,
|
118 |
+
"response": None
|
119 |
+
}
|
120 |
+
except Exception as e:
|
121 |
+
logger.error(f"DeepSeek API 테스트 중 예상치 못한 오류: {e}", exc_info=True)
|
122 |
+
return {
|
123 |
+
"success": False,
|
124 |
+
"message": f"예상치 못한 오류: {str(e)}",
|
125 |
+
"status_code": None,
|
126 |
+
"response": None
|
127 |
+
}
|
128 |
+
|
129 |
+
|
130 |
+
def create_deepseek_client(api_key: str, endpoint: str, model: str):
|
131 |
+
"""
|
132 |
+
DeepSeek 클라이언트 생성 (LangChain 호환)
|
133 |
+
|
134 |
+
Args:
|
135 |
+
api_key: DeepSeek API 키
|
136 |
+
endpoint: DeepSeek API 엔드포인트
|
137 |
+
model: 사용할 모델명
|
138 |
+
|
139 |
+
Returns:
|
140 |
+
DeepSeek 클라이언트 객체 또는 None
|
141 |
+
"""
|
142 |
+
# LangChain과 DeepSeek 통합 시도
|
143 |
+
try:
|
144 |
+
from langchain_openai import ChatOpenAI
|
145 |
+
|
146 |
+
# API 연결 테스트 먼저 수행
|
147 |
+
test_result = test_deepseek_api(api_key, endpoint, model)
|
148 |
+
|
149 |
+
if not test_result["success"]:
|
150 |
+
logger.error(f"DeepSeek API 연결 테스트 실패: {test_result['message']}")
|
151 |
+
return None
|
152 |
+
|
153 |
+
# 정상 연결 시 클라이언트 생성
|
154 |
+
# DeepSeek는 OpenAI 호환 API를 제공하므로 ChatOpenAI를 사용
|
155 |
+
client = ChatOpenAI(
|
156 |
+
model=model,
|
157 |
+
temperature=0.2,
|
158 |
+
api_key=api_key,
|
159 |
+
base_url=endpoint.rstrip("/v1/chat/completions"), # OpenAI 호환 베이스 URL
|
160 |
+
)
|
161 |
+
|
162 |
+
logger.info(f"DeepSeek 클라이언트 생성 성공: {model}")
|
163 |
+
return client
|
164 |
+
|
165 |
+
except ImportError as e:
|
166 |
+
logger.error(f"필요한 라이브러리 임포트 실패: {e}")
|
167 |
+
return None
|
168 |
+
except Exception as e:
|
169 |
+
logger.error(f"DeepSeek 클라이언트 생성 중 오류: {e}", exc_info=True)
|
170 |
+
return None
|
dir
ADDED
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
동의어 처리 모듈
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import sys
|
6 |
+
import re
|
7 |
+
from typing import Dict, List, Optional, Set
|
8 |
+
|
9 |
+
# 기본 동의어 사전 (MP_synonyms.py 파일이 없을 경우 사용)
|
10 |
+
DEFAULT_SYNONYMS = {
|
11 |
+
"엑츄레이터": "액츄에이터",
|
12 |
+
"액츄에이터": "액츄에이터",
|
13 |
+
"모터": "액츄에이터",
|
14 |
+
"컨박": "컨트롤박스"
|
15 |
+
}
|
16 |
+
|
17 |
+
|
18 |
+
class SynonymsHandler:
|
19 |
+
"""
|
20 |
+
부품명의 동의어를 처리하는 클래스
|
21 |
+
"""
|
22 |
+
|
23 |
+
def __init__(self, synonyms_file: Optional[str] = None):
|
24 |
+
"""
|
25 |
+
동의어 핸들러 초기화
|
26 |
+
|
27 |
+
Args:
|
28 |
+
synonyms_file: 동의어 파일 경로 (선택적)
|
29 |
+
"""
|
30 |
+
self.synonyms = {}
|
31 |
+
self.loaded = False
|
32 |
+
|
33 |
+
# 1. 기본 제공된 파일 경로 확인
|
34 |
+
if synonyms_file and os.path.exists(synonyms_file):
|
35 |
+
self._load_from_file(synonyms_file)
|
36 |
+
|
37 |
+
# 2. 일반적인 위치 확인 (.venv/SYNONYMS/MP_synonyms.py)
|
38 |
+
elif os.path.exists(".venv/SYNONYMS/MP_synonyms.py"):
|
39 |
+
self._load_from_file(".venv/SYNONYMS/MP_synonyms.py")
|
40 |
+
|
41 |
+
# 3. 현재 디렉토리 확인
|
42 |
+
elif os.path.exists("MP_synonyms.py"):
|
43 |
+
self._load_from_file("MP_synonyms.py")
|
44 |
+
|
45 |
+
# 4. 기본 동의어 사용
|
46 |
+
else:
|
47 |
+
print("동의어 파일을 찾을 수 없어 기본 동의어 사전을 사용합니다.")
|
48 |
+
self.synonyms = DEFAULT_SYNONYMS
|
49 |
+
self.loaded = True
|
50 |
+
|
51 |
+
def _load_from_file(self, file_path: str) -> None:
|
52 |
+
"""
|
53 |
+
파일에서 동의어 사전 로드
|
54 |
+
|
55 |
+
Args:
|
56 |
+
file_path: 동의어 파일 경로
|
57 |
+
"""
|
58 |
+
try:
|
59 |
+
# 파일 내용 읽기
|
60 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
61 |
+
content = f.read()
|
62 |
+
|
63 |
+
# SYNONYMS 딕셔너리 추출
|
64 |
+
synonyms_match = re.search(r'SYNONYMS\s*=\s*\{(.*?)\}', content, re.DOTALL)
|
65 |
+
if synonyms_match:
|
66 |
+
# 실행하지 않고 변환하는 방법
|
67 |
+
synonyms_str = "{" + synonyms_match.group(1) + "}"
|
68 |
+
|
69 |
+
# 정규식을 사용하여 딕셔너리 형태로 파싱
|
70 |
+
pattern = r'"([^"]*)"\s*:\s*"([^"]*)"'
|
71 |
+
matches = re.findall(pattern, synonyms_str)
|
72 |
+
|
73 |
+
self.synonyms = {key: value for key, value in matches}
|
74 |
+
self.loaded = True
|
75 |
+
print(f"동의어 사전 로드 완료: {file_path}, {len(self.synonyms)}개 항목")
|
76 |
+
else:
|
77 |
+
print(f"파일에서 SYNONYMS 딕셔너리를 찾을 수 없습니다: {file_path}")
|
78 |
+
self.synonyms = DEFAULT_SYNONYMS
|
79 |
+
self.loaded = True
|
80 |
+
|
81 |
+
except Exception as e:
|
82 |
+
print(f"동의어 사전 로드 중 오류: {e}")
|
83 |
+
self.synonyms = DEFAULT_SYNONYMS
|
84 |
+
self.loaded = True
|
85 |
+
|
86 |
+
def find_in_text(self, text: str) -> List[str]:
|
87 |
+
"""
|
88 |
+
텍스트에서 동의어 찾기
|
89 |
+
|
90 |
+
Args:
|
91 |
+
text: 검색할 텍스트
|
92 |
+
|
93 |
+
Returns:
|
94 |
+
찾은 표준화된 부품명 리스트
|
95 |
+
"""
|
96 |
+
if not text or not self.loaded:
|
97 |
+
return []
|
98 |
+
|
99 |
+
# 공백 제거 및 소문자 변환
|
100 |
+
text = text.lower()
|
101 |
+
|
102 |
+
found_parts = set()
|
103 |
+
|
104 |
+
# 동의어 키워드가 텍스트에 포함되어 있는지 확인
|
105 |
+
for keyword, standard_name in self.synonyms.items():
|
106 |
+
if keyword.lower() in text:
|
107 |
+
found_parts.add(standard_name)
|
108 |
+
|
109 |
+
return list(found_parts)
|
110 |
+
|
111 |
+
def standardize(self, part_name: str) -> str:
|
112 |
+
"""
|
113 |
+
부품명을 표준화
|
114 |
+
|
115 |
+
Args:
|
116 |
+
part_name: 표준화할 부품명
|
117 |
+
|
118 |
+
Returns:
|
119 |
+
표준화된 부품명
|
120 |
+
"""
|
121 |
+
if not part_name or not self.loaded:
|
122 |
+
return part_name
|
123 |
+
|
124 |
+
# 소문자 변환하여 비교
|
125 |
+
part_lower = part_name.lower().strip()
|
126 |
+
|
127 |
+
# 동의어 사전에서 검색
|
128 |
+
for keyword, standard_name in self.synonyms.items():
|
129 |
+
if part_lower == keyword.lower():
|
130 |
+
return standard_name
|
131 |
+
|
132 |
+
# 매칭되지 않으면 원래 이름 반환
|
133 |
+
return part_name
|
134 |
+
|
135 |
+
def standardize_parts_list(self, parts: List[str]) -> List[str]:
|
136 |
+
"""
|
137 |
+
부품명 리스트를 표준화
|
138 |
+
|
139 |
+
Args:
|
140 |
+
parts: 표준화할 부품명 리스트
|
141 |
+
|
142 |
+
Returns:
|
143 |
+
표준화된 부품명 리스트
|
144 |
+
"""
|
145 |
+
if not parts or not self.loaded:
|
146 |
+
return parts
|
147 |
+
|
148 |
+
standardized = set()
|
149 |
+
|
150 |
+
for part in parts:
|
151 |
+
if part:
|
152 |
+
standardized.add(self.standardize(part))
|
153 |
+
|
154 |
+
return list(standardized)
|
direct_deepseek.py
ADDED
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
직접 DeepSeek API 호출을 위한 클라이언트 구현 - 허깅페이스 환경 지원
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import time
|
6 |
+
import logging
|
7 |
+
import requests
|
8 |
+
import json
|
9 |
+
from typing import Dict, Any, Optional, List
|
10 |
+
|
11 |
+
# 로깅 설정
|
12 |
+
logger = logging.getLogger("DirectDeepSeek")
|
13 |
+
|
14 |
+
# 환경 감지
|
15 |
+
IS_HUGGINGFACE = os.getenv('SPACE_ID') is not None or os.getenv('SYSTEM') == 'spaces'
|
16 |
+
|
17 |
+
class DirectDeepSeekClient:
|
18 |
+
"""
|
19 |
+
DeepSeek API를 직접 호출하는 클라이언트
|
20 |
+
OpenAI 클라이언트를 우회하고 직접 HTTP 요청 사용
|
21 |
+
허깅페이스 환경 지원
|
22 |
+
"""
|
23 |
+
def __init__(self, api_key: Optional[str] = None, model_name: str = "deepseek-chat"):
|
24 |
+
"""
|
25 |
+
클라이언트 초기화
|
26 |
+
|
27 |
+
Args:
|
28 |
+
api_key: DeepSeek API 키 (None인 경우 환경변수에서 가져옴)
|
29 |
+
model_name: 사용할 모델 이름 (기본값: "deepseek-chat")
|
30 |
+
"""
|
31 |
+
# API 키 설정 (허깅페이스 환경 확인)
|
32 |
+
if api_key is None:
|
33 |
+
if IS_HUGGINGFACE:
|
34 |
+
# 허깅페이스 환경에서는 시크릿에서 가져오기 시도
|
35 |
+
api_key = os.getenv('HF_SECRET_DEEPSEEK_API_KEY')
|
36 |
+
if not api_key:
|
37 |
+
# 시크릿이 없으면 일반 환경변수 확인
|
38 |
+
api_key = os.getenv("DEEPSEEK_API_KEY", "")
|
39 |
+
else:
|
40 |
+
# 로컬 환경에서는 환경변수 사용
|
41 |
+
api_key = os.getenv("DEEPSEEK_API_KEY", "")
|
42 |
+
|
43 |
+
self.api_key = api_key
|
44 |
+
self.model_name = model_name
|
45 |
+
|
46 |
+
# 엔드포인트 설정 (허깅페이스 환경 확인)
|
47 |
+
if IS_HUGGINGFACE:
|
48 |
+
# 허깅페이스 환경에서는 시크릿에서 가져오기 시도
|
49 |
+
self.endpoint = os.getenv('HF_SECRET_DEEPSEEK_ENDPOINT')
|
50 |
+
if not self.endpoint:
|
51 |
+
# 시크릿이 없으면 일반 환경변수 확인
|
52 |
+
self.endpoint = os.getenv("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions")
|
53 |
+
else:
|
54 |
+
# 로컬 환경에서는 환경변수 사용
|
55 |
+
self.endpoint = os.getenv("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions")
|
56 |
+
|
57 |
+
logger.info(f"DirectDeepSeekClient 초기화: 모델={model_name}, 엔드포인트={self.endpoint}")
|
58 |
+
|
59 |
+
# API 키 확인
|
60 |
+
if not self.api_key:
|
61 |
+
if IS_HUGGINGFACE:
|
62 |
+
logger.warning("허깅페이스 환경에서 DeepSeek API 키가 설정되지 않았습니다. Space 시크릿을 확인하세요.")
|
63 |
+
else:
|
64 |
+
logger.warning("DeepSeek API 키가 설정되지 않았습니다. .env 파일이나 환경변수를 확인하세요.")
|
65 |
+
|
66 |
+
def generate(self,
|
67 |
+
prompt: str,
|
68 |
+
temperature: float = 0.3,
|
69 |
+
max_tokens: int = 1000,
|
70 |
+
max_retries: int = 3,
|
71 |
+
timeout: int = 60) -> Dict[str, Any]:
|
72 |
+
"""
|
73 |
+
텍스트 생성 요청
|
74 |
+
|
75 |
+
Args:
|
76 |
+
prompt: 입력 프롬프트
|
77 |
+
temperature: 생성 온도 (0.0 ~ 1.0)
|
78 |
+
max_tokens: 최대 생성 토큰 수
|
79 |
+
max_retries: 재시도 횟수
|
80 |
+
timeout: 요청 타임아웃 (초)
|
81 |
+
|
82 |
+
Returns:
|
83 |
+
생성 결과 딕셔너리 (success, response, message 등)
|
84 |
+
"""
|
85 |
+
# 메시지 구성 (단일 사용자 메시지)
|
86 |
+
messages = [{"role": "user", "content": prompt}]
|
87 |
+
return self.chat(messages, temperature, max_tokens, max_retries, timeout)
|
88 |
+
|
89 |
+
def chat(self,
|
90 |
+
messages: List[Dict[str, str]],
|
91 |
+
temperature: float = 0.3,
|
92 |
+
max_tokens: int = 1000,
|
93 |
+
max_retries: int = 3,
|
94 |
+
timeout: int = 60) -> Dict[str, Any]:
|
95 |
+
"""
|
96 |
+
채팅 API 호출
|
97 |
+
|
98 |
+
Args:
|
99 |
+
messages: 채팅 메시지 리스트 (role, content 키를 가진 딕셔너리 리스트)
|
100 |
+
temperature: 생성 온도 (0.0 ~ 1.0)
|
101 |
+
max_tokens: 최대 생성 토큰 수
|
102 |
+
max_retries: 재시도 횟수
|
103 |
+
timeout: 요청 타임아웃 (초)
|
104 |
+
|
105 |
+
Returns:
|
106 |
+
생성 결과 딕셔너리 (success, response, message 등)
|
107 |
+
"""
|
108 |
+
# API 키 확인
|
109 |
+
if not self.api_key:
|
110 |
+
error_msg = "DeepSeek API 키가 설정되지 않았습니다."
|
111 |
+
logger.error(error_msg)
|
112 |
+
return {
|
113 |
+
"success": False,
|
114 |
+
"message": error_msg,
|
115 |
+
"status_code": None
|
116 |
+
}
|
117 |
+
|
118 |
+
# API 요청 헤더 및 데이터
|
119 |
+
headers = {
|
120 |
+
"Content-Type": "application/json",
|
121 |
+
"Authorization": f"Bearer {self.api_key}"
|
122 |
+
}
|
123 |
+
|
124 |
+
payload = {
|
125 |
+
"model": self.model_name,
|
126 |
+
"messages": messages,
|
127 |
+
"temperature": temperature,
|
128 |
+
"max_tokens": max_tokens
|
129 |
+
}
|
130 |
+
|
131 |
+
# 재시도 로직
|
132 |
+
retry_delay = 1.0
|
133 |
+
attempt = 0
|
134 |
+
|
135 |
+
while attempt < max_retries:
|
136 |
+
attempt += 1
|
137 |
+
try:
|
138 |
+
logger.info(f"DeepSeek API 요청 시도 ({attempt}/{max_retries})...")
|
139 |
+
|
140 |
+
# API 요청 전송
|
141 |
+
response = requests.post(
|
142 |
+
self.endpoint,
|
143 |
+
headers=headers,
|
144 |
+
json=payload,
|
145 |
+
timeout=timeout
|
146 |
+
)
|
147 |
+
|
148 |
+
# 응답 확인
|
149 |
+
if response.status_code == 200:
|
150 |
+
result = response.json()
|
151 |
+
|
152 |
+
# 응답 내용 추출
|
153 |
+
if "choices" in result and len(result["choices"]) > 0:
|
154 |
+
message_content = result["choices"][0].get("message", {}).get("content", "")
|
155 |
+
logger.info(f"DeepSeek API 응답 성공 (길이: {len(message_content)})")
|
156 |
+
|
157 |
+
return {
|
158 |
+
"success": True,
|
159 |
+
"response": message_content,
|
160 |
+
"status_code": response.status_code,
|
161 |
+
"raw_response": result
|
162 |
+
}
|
163 |
+
else:
|
164 |
+
logger.warning(f"DeepSeek API 응답은 성공했으나 예상치 못한 응답 형식: {result}")
|
165 |
+
return {
|
166 |
+
"success": False,
|
167 |
+
"message": "응답에서 메시지를 찾을 수 없습니다",
|
168 |
+
"status_code": response.status_code,
|
169 |
+
"raw_response": result
|
170 |
+
}
|
171 |
+
else:
|
172 |
+
logger.error(f"DeepSeek API 오류: 상태 코드 {response.status_code}")
|
173 |
+
|
174 |
+
# 오류 메시지 추출
|
175 |
+
error_message = ""
|
176 |
+
try:
|
177 |
+
error_data = response.json()
|
178 |
+
error_message = error_data.get("error", {}).get("message", str(error_data))
|
179 |
+
except:
|
180 |
+
error_message = response.text
|
181 |
+
|
182 |
+
# 요청 한도 초과시 더 오래 대기
|
183 |
+
if response.status_code == 429:
|
184 |
+
retry_delay = min(retry_delay * 3, 15)
|
185 |
+
else:
|
186 |
+
retry_delay = min(retry_delay * 2, 10)
|
187 |
+
|
188 |
+
if attempt < max_retries:
|
189 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
190 |
+
time.sleep(retry_delay)
|
191 |
+
else:
|
192 |
+
# 모든 시도 실패
|
193 |
+
return {
|
194 |
+
"success": False,
|
195 |
+
"message": f"API 오류: {error_message}",
|
196 |
+
"status_code": response.status_code
|
197 |
+
}
|
198 |
+
|
199 |
+
except requests.exceptions.Timeout:
|
200 |
+
logger.error("DeepSeek API 요청 시간 초과")
|
201 |
+
|
202 |
+
if attempt < max_retries:
|
203 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
204 |
+
time.sleep(retry_delay)
|
205 |
+
retry_delay = min(retry_delay * 2, 10)
|
206 |
+
else:
|
207 |
+
return {
|
208 |
+
"success": False,
|
209 |
+
"message": "API 요청 시간 초과",
|
210 |
+
"status_code": None
|
211 |
+
}
|
212 |
+
|
213 |
+
except requests.exceptions.ConnectionError:
|
214 |
+
logger.error("DeepSeek API 연결 실패")
|
215 |
+
|
216 |
+
if attempt < max_retries:
|
217 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
218 |
+
time.sleep(retry_delay)
|
219 |
+
retry_delay = min(retry_delay * 2, 10)
|
220 |
+
else:
|
221 |
+
return {
|
222 |
+
"success": False,
|
223 |
+
"message": "API 서버 연결 실패",
|
224 |
+
"status_code": None
|
225 |
+
}
|
226 |
+
|
227 |
+
except Exception as e:
|
228 |
+
logger.error(f"DeepSeek API 요청 중 예상치 못한 오류: {e}")
|
229 |
+
|
230 |
+
if attempt < max_retries:
|
231 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
232 |
+
time.sleep(retry_delay)
|
233 |
+
retry_delay = min(retry_delay * 2, 10)
|
234 |
+
else:
|
235 |
+
return {
|
236 |
+
"success": False,
|
237 |
+
"message": f"예상치 못한 오류: {str(e)}",
|
238 |
+
"status_code": None
|
239 |
+
}
|
240 |
+
|
241 |
+
# 모든 시도 실패
|
242 |
+
return {
|
243 |
+
"success": False,
|
244 |
+
"message": "최대 재시도 횟수 초과",
|
245 |
+
"status_code": None
|
246 |
+
}
|
247 |
+
|
248 |
+
def system_prompt_chat(self,
|
249 |
+
system_prompt: str,
|
250 |
+
user_prompt: str,
|
251 |
+
temperature: float = 0.3,
|
252 |
+
max_tokens: int = 1000,
|
253 |
+
max_retries: int = 3,
|
254 |
+
timeout: int = 60) -> Dict[str, Any]:
|
255 |
+
"""
|
256 |
+
시스템 프롬프트와 사용자 프롬프트를 이용한 채팅 API 호출
|
257 |
+
|
258 |
+
Args:
|
259 |
+
system_prompt: 시스템 프롬프트
|
260 |
+
user_prompt: 사용자 프롬프트
|
261 |
+
temperature: 생성 온도 (0.0 ~ 1.0)
|
262 |
+
max_tokens: 최대 생성 토큰 수
|
263 |
+
max_retries: 재시도 횟수
|
264 |
+
timeout: 요청 타임아웃 (초)
|
265 |
+
|
266 |
+
Returns:
|
267 |
+
생성 결과 딕셔너리
|
268 |
+
"""
|
269 |
+
messages = [
|
270 |
+
{"role": "system", "content": system_prompt},
|
271 |
+
{"role": "user", "content": user_prompt}
|
272 |
+
]
|
273 |
+
|
274 |
+
return self.chat(messages, temperature, max_tokens, max_retries, timeout)
|
275 |
+
|
276 |
+
|
277 |
+
# 단독 실행을 위한 테스트 코드
|
278 |
+
if __name__ == "__main__":
|
279 |
+
# 로깅 설정
|
280 |
+
logging.basicConfig(level=logging.INFO)
|
281 |
+
|
282 |
+
# 허깅페이스 환경 확인
|
283 |
+
if IS_HUGGINGFACE:
|
284 |
+
print("허깅페이스 환경에서 실행 중입니다.")
|
285 |
+
print("HF_SECRET_DEEPSEEK_API_KEY 시크릿 설정이 필요합니다.")
|
286 |
+
else:
|
287 |
+
print("로컬 환경에서 실행 중입니다.")
|
288 |
+
print("DEEPSEEK_API_KEY 환경변수 설정이 필요합니다.")
|
289 |
+
|
290 |
+
# 클라이언트 생성
|
291 |
+
client = DirectDeepSeekClient()
|
292 |
+
|
293 |
+
# API 키 확인
|
294 |
+
if not client.api_key:
|
295 |
+
print("DeepSeek API 키가 설정되지 않았습니다.")
|
296 |
+
exit(1)
|
297 |
+
|
298 |
+
# 간단한 테스트
|
299 |
+
response = client.generate("Hello, what can you do?")
|
300 |
+
|
301 |
+
# 결과 출력
|
302 |
+
if response["success"]:
|
303 |
+
print("응답 성공!")
|
304 |
+
print(response["response"])
|
305 |
+
else:
|
306 |
+
print(f"응답 실패: {response['message']}")
|
fallback_rag_chain.py
ADDED
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
폴백 RAG 체인 구현 (기본적인 기능만 포함) - 직접 DeepSeek API 호출 방식
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import logging
|
6 |
+
import time
|
7 |
+
from typing import List, Dict, Any, Optional, Tuple
|
8 |
+
from langchain.schema import Document
|
9 |
+
|
10 |
+
# 직접 DeepSeek 클라이언트 사용
|
11 |
+
from direct_deepseek import DirectDeepSeekClient
|
12 |
+
|
13 |
+
# 설정 가져오기
|
14 |
+
from config import (
|
15 |
+
LLM_MODEL, USE_OPENAI, USE_DEEPSEEK,
|
16 |
+
DEEPSEEK_API_KEY, DEEPSEEK_ENDPOINT, DEEPSEEK_MODEL,
|
17 |
+
TOP_K_RETRIEVAL
|
18 |
+
)
|
19 |
+
|
20 |
+
# 로깅 설정
|
21 |
+
logger = logging.getLogger("FallbackRAGChain")
|
22 |
+
|
23 |
+
class FallbackRAGChain:
|
24 |
+
"""
|
25 |
+
기본적인 RAG 체인 구현 (단순화된 버전, 문제 해결용)
|
26 |
+
직접 DeepSeek API 호출 방식 사용
|
27 |
+
"""
|
28 |
+
|
29 |
+
def __init__(self, vector_store):
|
30 |
+
"""
|
31 |
+
RAG 체인 초기화
|
32 |
+
|
33 |
+
Args:
|
34 |
+
vector_store: 벡터 스토어 인스턴스
|
35 |
+
"""
|
36 |
+
logger.info("폴백 RAG 체인 초기화...")
|
37 |
+
self.vector_store = vector_store
|
38 |
+
|
39 |
+
# DeepSeek 모델 직접 초기화
|
40 |
+
if USE_DEEPSEEK and DEEPSEEK_API_KEY:
|
41 |
+
logger.info(f"DeepSeek 모델 직접 초기화: {DEEPSEEK_MODEL}")
|
42 |
+
try:
|
43 |
+
self.client = DirectDeepSeekClient(
|
44 |
+
api_key=DEEPSEEK_API_KEY,
|
45 |
+
model_name=DEEPSEEK_MODEL
|
46 |
+
)
|
47 |
+
logger.info("DeepSeek 모델 직접 초기화 성공")
|
48 |
+
except Exception as e:
|
49 |
+
logger.error(f"DeepSeek 모델 초기화 실패: {e}")
|
50 |
+
# 오프라인 모드로 폴백
|
51 |
+
self.client = None
|
52 |
+
logger.warning("LLM이 초기화되지 않아 오프라인 모드로 동작합니다.")
|
53 |
+
else:
|
54 |
+
# LLM이 설정되지 않음
|
55 |
+
logger.warning("LLM이 설정되지 않아 오프라인 모드로 동작합니다.")
|
56 |
+
self.client = None
|
57 |
+
|
58 |
+
logger.info("폴백 RAG 체인 초기화 완료")
|
59 |
+
|
60 |
+
def _retrieve(self, query: str) -> str:
|
61 |
+
"""
|
62 |
+
쿼리에 대한 관련 문서 검색 및 컨텍스트 구성
|
63 |
+
|
64 |
+
Args:
|
65 |
+
query: 사용자 질문
|
66 |
+
|
67 |
+
Returns:
|
68 |
+
검색 결과를 포함한 컨텍스트 문자열
|
69 |
+
"""
|
70 |
+
if not query or not query.strip():
|
71 |
+
return "검색 쿼리가 비어있습니다."
|
72 |
+
|
73 |
+
try:
|
74 |
+
# 벡터 검색 수행
|
75 |
+
logger.info(f"벡터 검색: '{query[:30]}...'")
|
76 |
+
docs = self.vector_store.similarity_search(query, k=TOP_K_RETRIEVAL)
|
77 |
+
|
78 |
+
if not docs:
|
79 |
+
return "관련 문서를 찾을 수 없습니다."
|
80 |
+
|
81 |
+
# 검색 결과 컨텍스트 구성
|
82 |
+
context_parts = []
|
83 |
+
for i, doc in enumerate(docs, 1):
|
84 |
+
source = doc.metadata.get("source", "알 수 없는 출처")
|
85 |
+
page = doc.metadata.get("page", "")
|
86 |
+
source_info = f"{source}"
|
87 |
+
if page:
|
88 |
+
source_info += f" (페이지: {page})"
|
89 |
+
|
90 |
+
context_parts.append(f"[참고자료 {i}] - 출처: {source_info}\n{doc.page_content}\n")
|
91 |
+
|
92 |
+
context = "\n".join(context_parts)
|
93 |
+
|
94 |
+
# 컨텍스트 길이 제한 (토큰 수 제한)
|
95 |
+
if len(context) > 6000:
|
96 |
+
logger.warning(f"컨텍스트가 너무 깁니다 ({len(context)} 문자). 제한합니다.")
|
97 |
+
context = context[:2500] + "\n...(중략)...\n" + context[-2500:]
|
98 |
+
|
99 |
+
return context
|
100 |
+
|
101 |
+
except Exception as e:
|
102 |
+
logger.error(f"검색 중 오류: {e}")
|
103 |
+
return f"검색 중 오류 발생: {str(e)}"
|
104 |
+
|
105 |
+
def _generate_prompt(self, query: str, context: str) -> List[Dict[str, str]]:
|
106 |
+
"""
|
107 |
+
프롬프트 생성 (DeepSeek API 형식)
|
108 |
+
|
109 |
+
Args:
|
110 |
+
query: 사용자 질문
|
111 |
+
context: 검색 결과 컨텍스트
|
112 |
+
|
113 |
+
Returns:
|
114 |
+
DeepSeek API용 messages 형식
|
115 |
+
"""
|
116 |
+
# 시스템 프롬프트
|
117 |
+
system_prompt = """다음 정보를 기반으로 질문에 정확하게 답변해주세요.
|
118 |
+
참고 정보에 답이 있으면 반드시 그 정보를 기반으로 답변하세요.
|
119 |
+
참고 정보에 답이 없는 경우에는 일반적인 지식을 활용하여 답변할 수 있지만, "제공된 문서에는 이 정보가 없으나, 일반적으로는..." 식으로 시작하세요.
|
120 |
+
답변은 정확하고 간결하게 제공하되, 가능한 참고 정보에서 근거를 찾아 설명해주세요.
|
121 |
+
참고 정보의 출처도 함께 알려주세요."""
|
122 |
+
|
123 |
+
# 사용자 프롬프트 (질문과 컨텍스트 포함)
|
124 |
+
user_prompt = f"""질문: {query}
|
125 |
+
|
126 |
+
참고 정보:
|
127 |
+
{context}"""
|
128 |
+
|
129 |
+
# DeepSeek API에 맞는 메시지 포맷
|
130 |
+
messages = [
|
131 |
+
{"role": "system", "content": system_prompt},
|
132 |
+
{"role": "user", "content": user_prompt}
|
133 |
+
]
|
134 |
+
|
135 |
+
return messages
|
136 |
+
|
137 |
+
def _generate_simple_response(self, query: str, context: str) -> str:
|
138 |
+
"""
|
139 |
+
간단한 오프라인 응답 생성 (LLM이 없을 때 사용)
|
140 |
+
"""
|
141 |
+
# 기본 제공 응답 (일반적인 질문에 대한 정해진 응답)
|
142 |
+
predefined_answers = {
|
143 |
+
"대한민국의 수도": "대한민국의 수도는 서울입니다.",
|
144 |
+
"수도": "대한민국의 수도는 서울입니다.",
|
145 |
+
"누구야": "저는 RAG 기반 질의응답 시스템입니다. 문서를 검색하고 관련 정보를 찾아드립니다.",
|
146 |
+
"안녕": "안녕하세요! 무엇을 도와드릴까요?",
|
147 |
+
"뭐해": "사용자의 질문에 답변하기 위해 문서를 검색하고 있습니다. 무엇을 알려드릴까요?"
|
148 |
+
}
|
149 |
+
|
150 |
+
# 질문에 맞는 미리 정의된 응답이 있는지 확인
|
151 |
+
for key, answer in predefined_answers.items():
|
152 |
+
if key in query.lower():
|
153 |
+
return answer
|
154 |
+
|
155 |
+
# 미리 정의된 응답이 없으면 검색 결과만 표시
|
156 |
+
return f"""
|
157 |
+
현재 LLM API 연결에 문제가 있어 검색 결과만 표시합니다.
|
158 |
+
|
159 |
+
질문: {query}
|
160 |
+
|
161 |
+
검색된 관련 문서:
|
162 |
+
{context}
|
163 |
+
|
164 |
+
[참고] 관련 정보를 찾으셨나요? API 연결 문제로 인해 자동 요약이 제공되지 않습니다. 다시 시도하거나 다른 질문을 해보세요.
|
165 |
+
"""
|
166 |
+
|
167 |
+
def run(self, query: str) -> str:
|
168 |
+
"""
|
169 |
+
사용자 쿼리에 대한 RAG 파이프라인 실행
|
170 |
+
|
171 |
+
Args:
|
172 |
+
query: 사용자 질문
|
173 |
+
|
174 |
+
Returns:
|
175 |
+
모델 응답 문자열
|
176 |
+
"""
|
177 |
+
if not query or not query.strip():
|
178 |
+
return "질문이 비어있습니다. 질문을 입력해 주세요."
|
179 |
+
|
180 |
+
try:
|
181 |
+
logger.info(f"RAG 체인 실행: '{query[:30]}...'")
|
182 |
+
|
183 |
+
# 문서 검색
|
184 |
+
context = self._retrieve(query)
|
185 |
+
|
186 |
+
# LLM이 초기화되지 않은 경우 오프라인 응답
|
187 |
+
if self.client is None:
|
188 |
+
logger.warning("LLM이 초기화되지 않아 오프라인 응답 생성")
|
189 |
+
return self._generate_simple_response(query, context)
|
190 |
+
|
191 |
+
# 프롬프트 구성
|
192 |
+
messages = self._generate_prompt(query, context)
|
193 |
+
|
194 |
+
# 응답 생성 (최대 3회 시도)
|
195 |
+
max_retries = 3
|
196 |
+
retry_delay = 1.0
|
197 |
+
|
198 |
+
for attempt in range(max_retries):
|
199 |
+
try:
|
200 |
+
logger.info(f"응답 생성 시도 ({attempt+1}/{max_retries})")
|
201 |
+
|
202 |
+
# 직접 DeepSeek API 호출
|
203 |
+
response = self.client.chat(messages)
|
204 |
+
|
205 |
+
if response["success"]:
|
206 |
+
logger.info(f"응답 생성 성공 (길이: {len(response['response'])})")
|
207 |
+
return response["response"]
|
208 |
+
else:
|
209 |
+
logger.error(f"응답 생성 실패: {response['message']}")
|
210 |
+
if attempt < max_retries - 1:
|
211 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
212 |
+
time.sleep(retry_delay)
|
213 |
+
retry_delay *= 2
|
214 |
+
else:
|
215 |
+
# 모든 시도 실패 시 오프라인 응답
|
216 |
+
logger.warning("최대 재시도 횟수 초과, 오프라인 응답 생성")
|
217 |
+
return self._generate_simple_response(query, context)
|
218 |
+
except Exception as e:
|
219 |
+
logger.error(f"응답 생성 중 오류: {e}")
|
220 |
+
if attempt < max_retries - 1:
|
221 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
222 |
+
time.sleep(retry_delay)
|
223 |
+
retry_delay *= 2
|
224 |
+
else:
|
225 |
+
# 모든 시도 실패 시 오프라인 응답 생성
|
226 |
+
return self._generate_simple_response(query, context)
|
227 |
+
|
228 |
+
except Exception as e:
|
229 |
+
logger.error(f"RAG 체인 실행 중 오류: {e}")
|
230 |
+
return f"질문 처리 중 오류가 발생했습니다: {str(e)}"
|
optimized_document_processor.py
ADDED
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
CPU에 최적화된 문서 처리 모듈 - 병렬 처리 적용
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import time
|
6 |
+
from typing import List, Dict, Any, Optional
|
7 |
+
from langchain.schema import Document
|
8 |
+
from concurrent.futures import ThreadPoolExecutor
|
9 |
+
|
10 |
+
# 멀티프로세싱 가져오기
|
11 |
+
import multiprocessing
|
12 |
+
|
13 |
+
try:
|
14 |
+
CPU_COUNT = multiprocessing.cpu_count()
|
15 |
+
except:
|
16 |
+
CPU_COUNT = 4
|
17 |
+
|
18 |
+
print(f"CPU 코어 수: {CPU_COUNT}")
|
19 |
+
|
20 |
+
# docling 라이브러리 존재 여부 확인
|
21 |
+
try:
|
22 |
+
from docling.datamodel.base_models import InputFormat
|
23 |
+
from docling.document_converter import DocumentConverter, PdfFormatOption
|
24 |
+
from docling.datamodel.pipeline_options import PdfPipelineOptions, TableFormerMode
|
25 |
+
from docling.chunking import HybridChunker
|
26 |
+
|
27 |
+
DOCLING_AVAILABLE = True
|
28 |
+
print("docling 라이브러리 사용 가능")
|
29 |
+
except ImportError:
|
30 |
+
print("docling 라이브러리를 찾을 수 없습니다. PyPDFLoader만 사용합니다.")
|
31 |
+
DOCLING_AVAILABLE = False
|
32 |
+
|
33 |
+
# LangChain 문서 로더
|
34 |
+
from langchain_community.document_loaders import PyPDFLoader
|
35 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
36 |
+
|
37 |
+
|
38 |
+
class OptimizedDocumentProcessor:
|
39 |
+
"""
|
40 |
+
CPU에 최적화된 병렬 처리 문서 처리 클래스
|
41 |
+
"""
|
42 |
+
|
43 |
+
def __init__(self,
|
44 |
+
chunk_size: int = 1000,
|
45 |
+
chunk_overlap: int = 200,
|
46 |
+
tokenizer: str = "Alibaba-NLP/gte-multilingual-base", # 올바른 모델 경로로 수정
|
47 |
+
max_workers: int = CPU_COUNT):
|
48 |
+
"""
|
49 |
+
문서 처리기 초기화
|
50 |
+
|
51 |
+
Args:
|
52 |
+
chunk_size: 텍스트 청크 크기
|
53 |
+
chunk_overlap: 청크 간 겹침 크기
|
54 |
+
tokenizer: HybridChunker에서 사용할 토크나이저
|
55 |
+
max_workers: 병렬 처리시 최대 작업자 수
|
56 |
+
"""
|
57 |
+
self.chunk_size = chunk_size
|
58 |
+
self.chunk_overlap = chunk_overlap
|
59 |
+
self.tokenizer = tokenizer
|
60 |
+
self.max_workers = max(1, min(max_workers, CPU_COUNT)) # CPU 코어 수 초과하지 않도록
|
61 |
+
|
62 |
+
print(f"병렬 처리 작업자 수: {self.max_workers}")
|
63 |
+
|
64 |
+
# LangChain 텍스트 스플리터
|
65 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
66 |
+
chunk_size=chunk_size,
|
67 |
+
chunk_overlap=chunk_overlap,
|
68 |
+
separators=["\n\n", "\n", ". ", " ", ""],
|
69 |
+
)
|
70 |
+
|
71 |
+
# docling 관련 컴포넌트 초기화
|
72 |
+
if DOCLING_AVAILABLE:
|
73 |
+
# 파이프라인 옵션 설정
|
74 |
+
self.pipeline_options = PdfPipelineOptions(do_table_structure=True)
|
75 |
+
self.pipeline_options.table_structure_options.mode = TableFormerMode.ACCURATE
|
76 |
+
|
77 |
+
# 문서 변환기 초기화
|
78 |
+
self.doc_converter = DocumentConverter(
|
79 |
+
format_options={
|
80 |
+
InputFormat.PDF: PdfFormatOption(pipeline_options=self.pipeline_options)
|
81 |
+
}
|
82 |
+
)
|
83 |
+
|
84 |
+
# HybridChunker 초기화 (trust_remote_code=True 추가)
|
85 |
+
self.hybrid_chunker = HybridChunker(
|
86 |
+
tokenizer=tokenizer,
|
87 |
+
chunk_size=chunk_size,
|
88 |
+
overlap=chunk_overlap,
|
89 |
+
tokenizer_kwargs={"trust_remote_code": True} # 원격 코드 실행 허용
|
90 |
+
)
|
91 |
+
|
92 |
+
print(f"docling 초기화 완료: HybridChunker(청크 크기={chunk_size}, 오버랩={chunk_overlap})")
|
93 |
+
|
94 |
+
def process_with_docling(self, pdf_path: str) -> Dict[str, Any]:
|
95 |
+
"""
|
96 |
+
docling을 사용하여 PDF 문서 처리
|
97 |
+
|
98 |
+
Args:
|
99 |
+
pdf_path: PDF 파일 경로
|
100 |
+
|
101 |
+
Returns:
|
102 |
+
처리된 문서 데이터
|
103 |
+
"""
|
104 |
+
if not DOCLING_AVAILABLE:
|
105 |
+
raise ImportError("docling 라이브러리가 설치되지 않았습니다.")
|
106 |
+
|
107 |
+
try:
|
108 |
+
start_time = time.time()
|
109 |
+
|
110 |
+
# 문서 변환
|
111 |
+
conv_res = self.doc_converter.convert(pdf_path)
|
112 |
+
doc = conv_res.document
|
113 |
+
|
114 |
+
# 성능 측정
|
115 |
+
conversion_time = time.time() - start_time
|
116 |
+
print(f"PDF 변환 시간: {conversion_time:.2f}초")
|
117 |
+
|
118 |
+
# 메타데이터 추출
|
119 |
+
metadata = {
|
120 |
+
"source": pdf_path,
|
121 |
+
"title": os.path.basename(pdf_path),
|
122 |
+
"processing_time": conversion_time
|
123 |
+
}
|
124 |
+
|
125 |
+
return {
|
126 |
+
"content": doc.export_to_markdown(),
|
127 |
+
"metadata": metadata,
|
128 |
+
"raw_document": doc,
|
129 |
+
}
|
130 |
+
|
131 |
+
except Exception as e:
|
132 |
+
print(f"docling으로 문서 처리 중 오류 발생: {e}")
|
133 |
+
raise
|
134 |
+
|
135 |
+
def chunk_with_hybrid_chunker(self, doc: Any) -> List[Dict[str, Any]]:
|
136 |
+
"""
|
137 |
+
HybridChunker를 사용하여 문서를 청크로 분할
|
138 |
+
|
139 |
+
Args:
|
140 |
+
doc: docling 문서 객체
|
141 |
+
|
142 |
+
Returns:
|
143 |
+
청크 리스트
|
144 |
+
"""
|
145 |
+
start_time = time.time()
|
146 |
+
|
147 |
+
# 청킹 수행
|
148 |
+
chunk_iter = self.hybrid_chunker.chunk(doc)
|
149 |
+
chunks = list(chunk_iter)
|
150 |
+
|
151 |
+
chunking_time = time.time() - start_time
|
152 |
+
print(f"청킹 시간: {chunking_time:.2f}초 (청크 수: {len(chunks)})")
|
153 |
+
|
154 |
+
return chunks
|
155 |
+
|
156 |
+
def create_langchain_documents_from_chunks(self,
|
157 |
+
chunks: List[Dict[str, Any]],
|
158 |
+
metadata: Dict[str, Any]) -> List[Document]:
|
159 |
+
"""
|
160 |
+
docling 청크를 LangChain Document 객체로 변환
|
161 |
+
|
162 |
+
Args:
|
163 |
+
chunks: docling HybridChunker로 생성한 청크 리스트
|
164 |
+
metadata: 문서 메타데이터
|
165 |
+
|
166 |
+
Returns:
|
167 |
+
LangChain Document 객체 리스트
|
168 |
+
"""
|
169 |
+
documents = []
|
170 |
+
|
171 |
+
for i, chunk in enumerate(chunks):
|
172 |
+
# 각 청크에 대한 메타데이터
|
173 |
+
chunk_metadata = metadata.copy()
|
174 |
+
chunk_metadata["chunk_id"] = i
|
175 |
+
|
176 |
+
# 청크 내용 추출
|
177 |
+
if hasattr(chunk, "text"):
|
178 |
+
content = chunk.text
|
179 |
+
elif hasattr(chunk, "content"):
|
180 |
+
content = chunk.content
|
181 |
+
else:
|
182 |
+
content = str(chunk)
|
183 |
+
|
184 |
+
document = Document(
|
185 |
+
page_content=content,
|
186 |
+
metadata=chunk_metadata
|
187 |
+
)
|
188 |
+
documents.append(document)
|
189 |
+
|
190 |
+
return documents
|
191 |
+
|
192 |
+
def process_with_langchain(self, pdf_path: str) -> List[Document]:
|
193 |
+
"""
|
194 |
+
LangChain의 PyPDFLoader를 사용하여 PDF 문서 로드
|
195 |
+
|
196 |
+
Args:
|
197 |
+
pdf_path: PDF 파일 경로
|
198 |
+
|
199 |
+
Returns:
|
200 |
+
LangChain Document 객체 리스트
|
201 |
+
"""
|
202 |
+
start_time = time.time()
|
203 |
+
|
204 |
+
try:
|
205 |
+
loader = PyPDFLoader(pdf_path)
|
206 |
+
documents = loader.load()
|
207 |
+
|
208 |
+
processing_time = time.time() - start_time
|
209 |
+
print(f"PyPDFLoader 처리 시간: {processing_time:.2f}초")
|
210 |
+
|
211 |
+
return documents
|
212 |
+
except Exception as e:
|
213 |
+
print(f"PyPDFLoader로 문서 처리 중 오류 발생: {e}")
|
214 |
+
raise
|
215 |
+
|
216 |
+
def process_pdf(self, pdf_path: str, use_docling: bool = True) -> List[Document]:
|
217 |
+
"""
|
218 |
+
PDF 파일 처리
|
219 |
+
|
220 |
+
Args:
|
221 |
+
pdf_path: PDF 파일 경로
|
222 |
+
use_docling: docling 사용 여부
|
223 |
+
|
224 |
+
Returns:
|
225 |
+
처리된 문서의 청크 리스트
|
226 |
+
"""
|
227 |
+
total_start_time = time.time()
|
228 |
+
|
229 |
+
# docling 사용 가능 여부 확인
|
230 |
+
can_use_docling = use_docling and DOCLING_AVAILABLE
|
231 |
+
|
232 |
+
if can_use_docling:
|
233 |
+
try:
|
234 |
+
# 1. docling으로 PDF 처리
|
235 |
+
docling_result = self.process_with_docling(pdf_path)
|
236 |
+
doc = docling_result["raw_document"]
|
237 |
+
metadata = docling_result["metadata"]
|
238 |
+
|
239 |
+
# 2. HybridChunker로 청크 생성
|
240 |
+
chunks = self.chunk_with_hybrid_chunker(doc)
|
241 |
+
|
242 |
+
# 3. 청크를 LangChain Document로 변환
|
243 |
+
documents = self.create_langchain_documents_from_chunks(chunks, metadata)
|
244 |
+
|
245 |
+
total_time = time.time() - total_start_time
|
246 |
+
print(f"docling 처리 완료: '{pdf_path}', {len(documents)} 청크, 총 {total_time:.2f}초")
|
247 |
+
|
248 |
+
return documents
|
249 |
+
except Exception as e:
|
250 |
+
print(f"docling 처리 실패, PyPDFLoader로 대체: {e}")
|
251 |
+
can_use_docling = False
|
252 |
+
|
253 |
+
if not can_use_docling:
|
254 |
+
# PyPDFLoader로 처리 (대체 방안)
|
255 |
+
documents = self.process_with_langchain(pdf_path)
|
256 |
+
chunks = self.text_splitter.split_documents(documents)
|
257 |
+
|
258 |
+
total_time = time.time() - total_start_time
|
259 |
+
print(f"PyPDFLoader 처리 완료: '{pdf_path}', {len(chunks)} 청크, 총 {total_time:.2f}초")
|
260 |
+
|
261 |
+
return chunks
|
262 |
+
|
263 |
+
def process_directory_parallel(self, directory: str, use_docling: bool = True) -> List[Document]:
|
264 |
+
"""
|
265 |
+
디렉토리 내 모든 PDF 파일 병렬 처리 (멀티스레딩)
|
266 |
+
|
267 |
+
Args:
|
268 |
+
directory: PDF 파일 디렉토리 경로
|
269 |
+
use_docling: docling 사용 여부
|
270 |
+
|
271 |
+
Returns:
|
272 |
+
처리된 모든 문서의 청크 리스트
|
273 |
+
"""
|
274 |
+
all_documents = []
|
275 |
+
pdf_files = []
|
276 |
+
|
277 |
+
# PDF 파일 목록 수집
|
278 |
+
for file in os.listdir(directory):
|
279 |
+
if file.endswith(".pdf"):
|
280 |
+
pdf_path = os.path.join(directory, file)
|
281 |
+
pdf_files.append(pdf_path)
|
282 |
+
|
283 |
+
if not pdf_files:
|
284 |
+
print(f"'{directory}' 디렉토리에 PDF 파일이 없습니다.")
|
285 |
+
return []
|
286 |
+
|
287 |
+
print(f"총 {len(pdf_files)}개 PDF 파일 병렬 처리 시작 (최대 {self.max_workers} 작업자)")
|
288 |
+
start_time = time.time()
|
289 |
+
|
290 |
+
# 병렬 처리 실행
|
291 |
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
292 |
+
# 각 PDF 파일에 대해 process_pdf 함수 병렬 실행
|
293 |
+
future_to_pdf = {executor.submit(self.process_pdf, pdf_path, use_docling): pdf_path
|
294 |
+
for pdf_path in pdf_files}
|
295 |
+
|
296 |
+
# 결과 수집
|
297 |
+
for future in future_to_pdf:
|
298 |
+
pdf_path = future_to_pdf[future]
|
299 |
+
try:
|
300 |
+
# 결과 가져오기
|
301 |
+
chunks = future.result()
|
302 |
+
all_documents.extend(chunks)
|
303 |
+
print(f"'{os.path.basename(pdf_path)}' 처리 완료: {len(chunks)} 청크")
|
304 |
+
except Exception as e:
|
305 |
+
print(f"'{pdf_path}' 처리 중 오류 발생: {e}")
|
306 |
+
|
307 |
+
total_time = time.time() - start_time
|
308 |
+
print(f"병렬 처리 완료: 총 {len(all_documents)} 청크, 처리 시간: {total_time:.2f}초")
|
309 |
+
|
310 |
+
return all_documents
|
311 |
+
|
312 |
+
def process_directory(self, directory: str, use_docling: bool = True, parallel: bool = True) -> List[Document]:
|
313 |
+
"""
|
314 |
+
디렉토리 내 모든 PDF 파일 처리
|
315 |
+
|
316 |
+
Args:
|
317 |
+
directory: PDF 파일 디렉토리 경로
|
318 |
+
use_docling: docling 사용 여부
|
319 |
+
parallel: 병렬 처리 사용 여부
|
320 |
+
|
321 |
+
Returns:
|
322 |
+
처리된 모든 문서의 청크 리스트
|
323 |
+
"""
|
324 |
+
# 병렬 처리 사용
|
325 |
+
if parallel:
|
326 |
+
return self.process_directory_parallel(directory, use_docling)
|
327 |
+
|
328 |
+
# 순차 처리
|
329 |
+
all_documents = []
|
330 |
+
start_time = time.time()
|
331 |
+
|
332 |
+
for file in os.listdir(directory):
|
333 |
+
if file.endswith(".pdf"):
|
334 |
+
pdf_path = os.path.join(directory, file)
|
335 |
+
print(f"처리 중: {pdf_path}")
|
336 |
+
|
337 |
+
try:
|
338 |
+
chunks = self.process_pdf(pdf_path, use_docling=use_docling)
|
339 |
+
all_documents.extend(chunks)
|
340 |
+
except Exception as e:
|
341 |
+
print(f"'{pdf_path}' 처리 중 오류 발생: {e}")
|
342 |
+
|
343 |
+
total_time = time.time() - start_time
|
344 |
+
print(f"순차 처리 완료: 총 {len(all_documents)} 청크, 처리 시간: {total_time:.2f}초")
|
345 |
+
|
346 |
+
return all_documents
|
rag_chain.py
ADDED
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
직접 DeepSeek API 호출을 위한 클라이언트 구현
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import time
|
6 |
+
import logging
|
7 |
+
import requests
|
8 |
+
import json
|
9 |
+
from typing import Dict, Any, Optional, List
|
10 |
+
|
11 |
+
# 로깅 설정
|
12 |
+
logger = logging.getLogger("DirectDeepSeek")
|
13 |
+
|
14 |
+
class DirectDeepSeekClient:
|
15 |
+
"""
|
16 |
+
DeepSeek API를 직접 호출하는 클라이언트
|
17 |
+
OpenAI 클라이언트를 우회하고 직접 HTTP 요청 사용
|
18 |
+
"""
|
19 |
+
def __init__(self, api_key: str, model_name: str = "deepseek-chat"):
|
20 |
+
"""
|
21 |
+
클라이언트 초기화
|
22 |
+
|
23 |
+
Args:
|
24 |
+
api_key: DeepSeek API 키
|
25 |
+
model_name: 사용할 모델 이름 (기본값: "deepseek-chat")
|
26 |
+
"""
|
27 |
+
self.api_key = api_key
|
28 |
+
self.model_name = model_name
|
29 |
+
self.endpoint = os.getenv("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions")
|
30 |
+
logger.info(f"DirectDeepSeekClient 초기화: 모델={model_name}, 엔드포인트={self.endpoint}")
|
31 |
+
|
32 |
+
def generate(self,
|
33 |
+
prompt: str,
|
34 |
+
temperature: float = 0.3,
|
35 |
+
max_tokens: int = 1000,
|
36 |
+
max_retries: int = 3,
|
37 |
+
timeout: int = 60) -> Dict[str, Any]:
|
38 |
+
"""
|
39 |
+
텍스트 생성 요청
|
40 |
+
|
41 |
+
Args:
|
42 |
+
prompt: 입력 프롬프트
|
43 |
+
temperature: 생성 온도 (0.0 ~ 1.0)
|
44 |
+
max_tokens: 최대 생성 토큰 수
|
45 |
+
max_retries: 재시도 횟수
|
46 |
+
timeout: 요청 타임아웃 (초)
|
47 |
+
|
48 |
+
Returns:
|
49 |
+
생성 결과 딕셔너리 (success, response, message 등)
|
50 |
+
"""
|
51 |
+
# 메시지 구성 (단일 사용자 메시지)
|
52 |
+
messages = [{"role": "user", "content": prompt}]
|
53 |
+
return self.chat(messages, temperature, max_tokens, max_retries, timeout)
|
54 |
+
|
55 |
+
def chat(self,
|
56 |
+
messages: List[Dict[str, str]],
|
57 |
+
temperature: float = 0.3,
|
58 |
+
max_tokens: int = 1000,
|
59 |
+
max_retries: int = 3,
|
60 |
+
timeout: int = 60) -> Dict[str, Any]:
|
61 |
+
"""
|
62 |
+
채팅 API 호출
|
63 |
+
|
64 |
+
Args:
|
65 |
+
messages: 채팅 메시지 리스트 (role, content 키를 가진 딕셔너리 리스트)
|
66 |
+
temperature: 생성 온도 (0.0 ~ 1.0)
|
67 |
+
max_tokens: 최대 생성 토큰 수
|
68 |
+
max_retries: 재시도 횟수
|
69 |
+
timeout: 요청 타임아웃 (초)
|
70 |
+
|
71 |
+
Returns:
|
72 |
+
생성 결과 딕셔너리 (success, response, message 등)
|
73 |
+
"""
|
74 |
+
# API 요청 헤더 및 데이터
|
75 |
+
headers = {
|
76 |
+
"Content-Type": "application/json",
|
77 |
+
"Authorization": f"Bearer {self.api_key}"
|
78 |
+
}
|
79 |
+
|
80 |
+
payload = {
|
81 |
+
"model": self.model_name,
|
82 |
+
"messages": messages,
|
83 |
+
"temperature": temperature,
|
84 |
+
"max_tokens": max_tokens
|
85 |
+
}
|
86 |
+
|
87 |
+
# 재시도 로직
|
88 |
+
retry_delay = 1.0
|
89 |
+
attempt = 0
|
90 |
+
|
91 |
+
while attempt < max_retries:
|
92 |
+
attempt += 1
|
93 |
+
try:
|
94 |
+
logger.info(f"DeepSeek API 요청 시도 ({attempt}/{max_retries})...")
|
95 |
+
|
96 |
+
# API 요청 전송
|
97 |
+
response = requests.post(
|
98 |
+
self.endpoint,
|
99 |
+
headers=headers,
|
100 |
+
json=payload,
|
101 |
+
timeout=timeout
|
102 |
+
)
|
103 |
+
|
104 |
+
# 응답 확인
|
105 |
+
if response.status_code == 200:
|
106 |
+
result = response.json()
|
107 |
+
|
108 |
+
# 응답 내용 추출
|
109 |
+
if "choices" in result and len(result["choices"]) > 0:
|
110 |
+
message_content = result["choices"][0].get("message", {}).get("content", "")
|
111 |
+
logger.info(f"DeepSeek API 응답 성공 (길이: {len(message_content)})")
|
112 |
+
|
113 |
+
return {
|
114 |
+
"success": True,
|
115 |
+
"response": message_content,
|
116 |
+
"status_code": response.status_code,
|
117 |
+
"raw_response": result
|
118 |
+
}
|
119 |
+
else:
|
120 |
+
logger.warning(f"DeepSeek API 응답은 성공했으나 예상치 못한 응답 형식: {result}")
|
121 |
+
return {
|
122 |
+
"success": False,
|
123 |
+
"message": "응답에서 메시지를 찾을 수 없습니다",
|
124 |
+
"status_code": response.status_code,
|
125 |
+
"raw_response": result
|
126 |
+
}
|
127 |
+
else:
|
128 |
+
logger.error(f"DeepSeek API 오류: 상태 코드 {response.status_code}")
|
129 |
+
|
130 |
+
# 오류 메시지 추출
|
131 |
+
error_message = ""
|
132 |
+
try:
|
133 |
+
error_data = response.json()
|
134 |
+
error_message = error_data.get("error", {}).get("message", str(error_data))
|
135 |
+
except:
|
136 |
+
error_message = response.text
|
137 |
+
|
138 |
+
# 요청 한도 ��과시 더 오래 대기
|
139 |
+
if response.status_code == 429:
|
140 |
+
retry_delay = min(retry_delay * 3, 15)
|
141 |
+
else:
|
142 |
+
retry_delay = min(retry_delay * 2, 10)
|
143 |
+
|
144 |
+
if attempt < max_retries:
|
145 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
146 |
+
time.sleep(retry_delay)
|
147 |
+
else:
|
148 |
+
# 모든 시도 실패
|
149 |
+
return {
|
150 |
+
"success": False,
|
151 |
+
"message": f"API 오류: {error_message}",
|
152 |
+
"status_code": response.status_code
|
153 |
+
}
|
154 |
+
|
155 |
+
except requests.exceptions.Timeout:
|
156 |
+
logger.error("DeepSeek API 요청 시간 초과")
|
157 |
+
|
158 |
+
if attempt < max_retries:
|
159 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
160 |
+
time.sleep(retry_delay)
|
161 |
+
retry_delay = min(retry_delay * 2, 10)
|
162 |
+
else:
|
163 |
+
return {
|
164 |
+
"success": False,
|
165 |
+
"message": "API 요청 시간 초과",
|
166 |
+
"status_code": None
|
167 |
+
}
|
168 |
+
|
169 |
+
except requests.exceptions.ConnectionError:
|
170 |
+
logger.error("DeepSeek API 연결 실패")
|
171 |
+
|
172 |
+
if attempt < max_retries:
|
173 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
174 |
+
time.sleep(retry_delay)
|
175 |
+
retry_delay = min(retry_delay * 2, 10)
|
176 |
+
else:
|
177 |
+
return {
|
178 |
+
"success": False,
|
179 |
+
"message": "API 서버 연결 실패",
|
180 |
+
"status_code": None
|
181 |
+
}
|
182 |
+
|
183 |
+
except Exception as e:
|
184 |
+
logger.error(f"DeepSeek API 요청 중 예상치 못한 오류: {e}")
|
185 |
+
|
186 |
+
if attempt < max_retries:
|
187 |
+
logger.info(f"{retry_delay}초 후 재시도...")
|
188 |
+
time.sleep(retry_delay)
|
189 |
+
retry_delay = min(retry_delay * 2, 10)
|
190 |
+
else:
|
191 |
+
return {
|
192 |
+
"success": False,
|
193 |
+
"message": f"예상치 못한 오류: {str(e)}",
|
194 |
+
"status_code": None
|
195 |
+
}
|
196 |
+
|
197 |
+
# 모든 시도 실패
|
198 |
+
return {
|
199 |
+
"success": False,
|
200 |
+
"message": "최대 재시도 횟수 초과",
|
201 |
+
"status_code": None
|
202 |
+
}
|
203 |
+
|
204 |
+
def system_prompt_chat(self,
|
205 |
+
system_prompt: str,
|
206 |
+
user_prompt: str,
|
207 |
+
temperature: float = 0.3,
|
208 |
+
max_tokens: int = 1000,
|
209 |
+
max_retries: int = 3,
|
210 |
+
timeout: int = 60) -> Dict[str, Any]:
|
211 |
+
"""
|
212 |
+
시스템 프롬프트와 사용자 프롬프트를 이용한 채팅 API 호출
|
213 |
+
|
214 |
+
Args:
|
215 |
+
system_prompt: 시스템 프롬프트
|
216 |
+
user_prompt: 사용자 프롬프트
|
217 |
+
temperature: 생성 온도 (0.0 ~ 1.0)
|
218 |
+
max_tokens: 최대 생성 토큰 수
|
219 |
+
max_retries: 재시도 횟수
|
220 |
+
timeout: 요청 타임아웃 (초)
|
221 |
+
|
222 |
+
Returns:
|
223 |
+
생성 결과 딕셔너리
|
224 |
+
"""
|
225 |
+
messages = [
|
226 |
+
{"role": "system", "content": system_prompt},
|
227 |
+
{"role": "user", "content": user_prompt}
|
228 |
+
]
|
229 |
+
|
230 |
+
return self.chat(messages, temperature, max_tokens, max_retries, timeout)
|
231 |
+
|
232 |
+
|
233 |
+
# 단독 실행을 위한 테스트 코드
|
234 |
+
if __name__ == "__main__":
|
235 |
+
# 로깅 설정
|
236 |
+
logging.basicConfig(level=logging.INFO)
|
237 |
+
|
238 |
+
# API 키 확인
|
239 |
+
api_key = os.environ.get("DEEPSEEK_API_KEY")
|
240 |
+
if not api_key:
|
241 |
+
print("환경 변수 DEEPSEEK_API_KEY가 설정되지 않았습니다.")
|
242 |
+
exit(1)
|
243 |
+
|
244 |
+
# 클라이언트 생성
|
245 |
+
client = DirectDeepSeekClient(api_key)
|
246 |
+
|
247 |
+
# 간단한 테스트
|
248 |
+
response = client.generate("Hello, what can you do?")
|
249 |
+
|
250 |
+
# 결과 출력
|
251 |
+
if response["success"]:
|
252 |
+
print("응답 성공!")
|
253 |
+
print(response["response"])
|
254 |
+
else:
|
255 |
+
print(f"응답 실패: {response['message']}")
|
requirements.txt
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
langchain>=0.1.0
|
2 |
+
langchain-community>=0.0.10
|
3 |
+
langchain-huggingface>=0.0.1
|
4 |
+
sentence-transformers>=2.2.2
|
5 |
+
faiss-cpu>=1.7.4
|
6 |
+
pypdf>=3.15.1
|
7 |
+
gradio>=4.0.0
|
8 |
+
python-dotenv>=1.0.0
|
9 |
+
torch>=2.0.0
|
10 |
+
transformers>=4.34.0
|
11 |
+
langchain-openai>=0.0.2
|
12 |
+
openai>=1.0.0
|
13 |
+
docling>=0.1.3
|
14 |
+
soundfile>=0.12.1
|
15 |
+
numpy>=1.20.0
|
16 |
+
requests>=2.25.1
|
reranker.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
원격 코드 실행 옵션이 추가된 리랭커 모듈
|
3 |
+
"""
|
4 |
+
from typing import List, Dict, Tuple
|
5 |
+
import numpy as np
|
6 |
+
from sentence_transformers import CrossEncoder
|
7 |
+
from langchain.schema import Document
|
8 |
+
from config import RERANKER_MODEL
|
9 |
+
|
10 |
+
class Reranker:
|
11 |
+
def __init__(self, model_name: str = RERANKER_MODEL):
|
12 |
+
"""
|
13 |
+
Cross-Encoder 리랭커 초기화
|
14 |
+
|
15 |
+
Args:
|
16 |
+
model_name: 사용할 Cross-Encoder 모델 이름
|
17 |
+
"""
|
18 |
+
print(f"리랭커 모델 로드 중: {model_name}")
|
19 |
+
|
20 |
+
# 원격 코드 실행 허용 옵션 추가
|
21 |
+
self.model = CrossEncoder(
|
22 |
+
model_name,
|
23 |
+
trust_remote_code=True # 원격 코드 실행 허용 (필수)
|
24 |
+
)
|
25 |
+
|
26 |
+
print(f"리랭커 모델 로드 완료: {model_name}")
|
27 |
+
|
28 |
+
def rerank(self, query: str, documents: List[Document], top_k: int = 3) -> List[Document]:
|
29 |
+
"""
|
30 |
+
검색 결과 재정렬
|
31 |
+
|
32 |
+
Args:
|
33 |
+
query: 검색 쿼리
|
34 |
+
documents: 벡터 검색 결과 문서 리스트
|
35 |
+
top_k: 반환할 상위 결과 수
|
36 |
+
|
37 |
+
Returns:
|
38 |
+
재정렬된 상위 문서 리스트
|
39 |
+
"""
|
40 |
+
if not documents:
|
41 |
+
return []
|
42 |
+
|
43 |
+
# Cross-Encoder 입력 쌍 생성
|
44 |
+
document_texts = [doc.page_content for doc in documents]
|
45 |
+
query_doc_pairs = [(query, doc) for doc in document_texts]
|
46 |
+
|
47 |
+
# 점수 계산
|
48 |
+
print(f"리랭킹 수행 중: {len(documents)}개 문서")
|
49 |
+
scores = self.model.predict(query_doc_pairs)
|
50 |
+
|
51 |
+
# 점수에 따라 문서 재정렬
|
52 |
+
doc_score_pairs = list(zip(documents, scores))
|
53 |
+
doc_score_pairs.sort(key=lambda x: x[1], reverse=True)
|
54 |
+
|
55 |
+
print(f"리랭킹 완료: 상위 {top_k}개 문서 선택")
|
56 |
+
|
57 |
+
# 상위 k개 결과 반환
|
58 |
+
return [doc for doc, score in doc_score_pairs[:top_k]]
|
simple_rag_chain.py
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
간단한 RAG 체인 구현 (디버깅용) - 직접 DeepSeek API 호출 방식
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import logging
|
6 |
+
import time
|
7 |
+
from typing import Dict, Any, List
|
8 |
+
|
9 |
+
# 직접 DeepSeek 클라이언트 사용
|
10 |
+
from direct_deepseek import DirectDeepSeekClient
|
11 |
+
|
12 |
+
# 로깅 설정
|
13 |
+
logger = logging.getLogger("SimpleRAGChain")
|
14 |
+
|
15 |
+
class SimpleRAGChain:
|
16 |
+
def __init__(self, vector_store, api_key=None, model="deepseek-chat", endpoint=None):
|
17 |
+
"""간단한 RAG 체인 초기화"""
|
18 |
+
logger.info("간단한 RAG 체인 초기화 중...")
|
19 |
+
self.vector_store = vector_store
|
20 |
+
|
21 |
+
# DeepSeek API 키 확인
|
22 |
+
self.api_key = api_key or os.environ.get("DEEPSEEK_API_KEY", "")
|
23 |
+
self.model = model or os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
|
24 |
+
logger.info(f"API 키 설정됨: {bool(self.api_key)}")
|
25 |
+
|
26 |
+
# DeepSeek 클라이언트 초기화
|
27 |
+
if self.api_key:
|
28 |
+
try:
|
29 |
+
self.client = DirectDeepSeekClient(
|
30 |
+
api_key=self.api_key,
|
31 |
+
model_name=self.model
|
32 |
+
)
|
33 |
+
logger.info(f"DeepSeek 클라이언트 초기화 성공: {self.model}")
|
34 |
+
except Exception as e:
|
35 |
+
logger.error(f"DeepSeek 클라이언트 초기화 실패: {e}")
|
36 |
+
self.client = None
|
37 |
+
else:
|
38 |
+
logger.warning("API 키가 설정되지 않아 클라이언트를 초기화할 수 없습니다.")
|
39 |
+
self.client = None
|
40 |
+
|
41 |
+
logger.info("간단한 RAG 체인 초기화 완료")
|
42 |
+
|
43 |
+
def _retrieve(self, query: str) -> str:
|
44 |
+
"""문서 검색 및 컨텍스트 구성"""
|
45 |
+
try:
|
46 |
+
docs = self.vector_store.similarity_search(query, k=3)
|
47 |
+
if not docs:
|
48 |
+
return "관련 문서를 찾을 수 없습니다."
|
49 |
+
|
50 |
+
# 검색 결과 컨텍스트 구성
|
51 |
+
context_parts = []
|
52 |
+
for i, doc in enumerate(docs, 1):
|
53 |
+
source = doc.metadata.get("source", "알 수 없는 출처")
|
54 |
+
page = doc.metadata.get("page", "")
|
55 |
+
source_info = f"{source}"
|
56 |
+
if page:
|
57 |
+
source_info += f" (페이지: {page})"
|
58 |
+
|
59 |
+
context_parts.append(f"[참고자료 {i}] - 출처: {source_info}\n{doc.page_content}\n")
|
60 |
+
|
61 |
+
context = "\n".join(context_parts)
|
62 |
+
|
63 |
+
# 길이 제한
|
64 |
+
if len(context) > 6000:
|
65 |
+
context = context[:2500] + "\n...(중략)...\n" + context[-2500:]
|
66 |
+
|
67 |
+
return context
|
68 |
+
except Exception as e:
|
69 |
+
logger.error(f"검색 중 오류: {e}")
|
70 |
+
return "문서 검색 중 오류가 발생했습니다."
|
71 |
+
|
72 |
+
def _generate_prompt(self, query: str, context: str) -> List[Dict[str, str]]:
|
73 |
+
"""DeepSeek API용 프롬프트 생성"""
|
74 |
+
# 시스템 프롬프트
|
75 |
+
system_prompt = """다음 정보를 기반으로 질문에 정확하게 답변해주세요.
|
76 |
+
참고 정보에서 답을 찾을 수 없는 경우 "제공된 문서에서 해당 정보를 찾을 수 없습니다."라고 답변하세요.
|
77 |
+
정보 출처를 포함해서 대답하세요."""
|
78 |
+
|
79 |
+
# 사용자 프롬프트
|
80 |
+
user_prompt = f"""질문: {query}
|
81 |
+
|
82 |
+
참고 정보:
|
83 |
+
{context}"""
|
84 |
+
|
85 |
+
# DeepSeek API 프롬프트 포맷
|
86 |
+
messages = [
|
87 |
+
{"role": "system", "content": system_prompt},
|
88 |
+
{"role": "user", "content": user_prompt}
|
89 |
+
]
|
90 |
+
|
91 |
+
return messages
|
92 |
+
|
93 |
+
def run(self, query: str) -> str:
|
94 |
+
"""쿼리 처리"""
|
95 |
+
try:
|
96 |
+
logger.info(f"SimpleRAGChain 실행: {query[:50]}...")
|
97 |
+
|
98 |
+
# 문서 검색
|
99 |
+
context = self._retrieve(query)
|
100 |
+
|
101 |
+
# 클라이언트가 초기화되지 않은 경우
|
102 |
+
if self.client is None:
|
103 |
+
logger.warning("DeepSeek 클라이언트가 초기화되지 않음. 검색 결과만 반환.")
|
104 |
+
return f"API 연결이 설정되지 않았습니다. 검색 결과:\n\n{context}"
|
105 |
+
|
106 |
+
# 프롬프트 생성
|
107 |
+
messages = self._generate_prompt(query, context)
|
108 |
+
|
109 |
+
# API 호출
|
110 |
+
start_time = time.time()
|
111 |
+
response = self.client.chat(messages)
|
112 |
+
logger.info(f"API 응답 시간: {time.time() - start_time:.2f}초")
|
113 |
+
|
114 |
+
if response["success"]:
|
115 |
+
logger.info("응답 생성 성공")
|
116 |
+
return response["response"]
|
117 |
+
else:
|
118 |
+
logger.error(f"응답 생성 실패: {response['message']}")
|
119 |
+
return f"응답 생성 실패: {response['message']}\n\n검색 결과:\n{context}"
|
120 |
+
|
121 |
+
except Exception as e:
|
122 |
+
logger.error(f"실행 중 오류: {e}")
|
123 |
+
return f"오류 발생: {str(e)}"
|
vector_store.py
ADDED
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
개선된 벡터 스토어 모듈 - Milvus 설정 최적화 및 예외 처리 강화
|
3 |
+
"""
|
4 |
+
import os
|
5 |
+
import logging
|
6 |
+
from typing import List, Dict, Any, Optional
|
7 |
+
import uuid
|
8 |
+
from langchain.schema import Document
|
9 |
+
|
10 |
+
# 로깅 설정
|
11 |
+
logger = logging.getLogger("VectorStore")
|
12 |
+
|
13 |
+
# 벡터 스토어 관련 예외 클래스
|
14 |
+
class VectorStoreInitError(Exception):
|
15 |
+
"""벡터 스토어 초기화 중 발생한 오류"""
|
16 |
+
pass
|
17 |
+
|
18 |
+
class EmbeddingModelError(Exception):
|
19 |
+
"""임베딩 모델 초기화 중 발생한 오류"""
|
20 |
+
pass
|
21 |
+
|
22 |
+
class DocumentIndexError(Exception):
|
23 |
+
"""문서 인덱싱 중 발생한 오류"""
|
24 |
+
pass
|
25 |
+
|
26 |
+
class VectorSearchError(Exception):
|
27 |
+
"""벡터 검색 중 발생한 오류"""
|
28 |
+
pass
|
29 |
+
|
30 |
+
class PersistenceError(Exception):
|
31 |
+
"""인덱스 저장/로드 중 발생한 오류"""
|
32 |
+
pass
|
33 |
+
|
34 |
+
# 벡터 스토어 임포트
|
35 |
+
try:
|
36 |
+
# 최신 버전 임포트
|
37 |
+
from langchain_milvus import Milvus
|
38 |
+
from langchain_community.vectorstores import FAISS
|
39 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
40 |
+
MODERN_IMPORTS = True
|
41 |
+
logger.info("최신 langchain 패키지 임포트 성공")
|
42 |
+
except ImportError:
|
43 |
+
try:
|
44 |
+
# 이전 버전 임포트
|
45 |
+
from langchain_community.vectorstores import Milvus, FAISS
|
46 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
47 |
+
MODERN_IMPORTS = False
|
48 |
+
logger.info("레거시 langchain_community 패키지 사용")
|
49 |
+
except ImportError as e:
|
50 |
+
logger.error(f"필수 벡터 스토어 라이브러리를 임포트할 수 없습니다: {e}")
|
51 |
+
raise VectorStoreInitError(f"필수 벡터 스토어 라이브러리를 임포트할 수 없습니다: {str(e)}")
|
52 |
+
|
53 |
+
from config import MILVUS_HOST, MILVUS_PORT, MILVUS_COLLECTION, EMBEDDING_MODEL
|
54 |
+
|
55 |
+
class VectorStore:
|
56 |
+
def __init__(self, use_milvus: bool = True):
|
57 |
+
"""
|
58 |
+
벡터 스토어 초기화
|
59 |
+
|
60 |
+
Args:
|
61 |
+
use_milvus: Milvus 사용 여부 (False이면 FAISS 사용)
|
62 |
+
"""
|
63 |
+
self.use_milvus = use_milvus
|
64 |
+
self.vector_store = None
|
65 |
+
|
66 |
+
# 임베딩 모델 설정
|
67 |
+
logger.info(f"임베딩 모델 로드 중: {EMBEDDING_MODEL}")
|
68 |
+
model_kwargs = {
|
69 |
+
"device": "cpu",
|
70 |
+
"trust_remote_code": True # 원격 코드 실행 허용 (필수)
|
71 |
+
}
|
72 |
+
encode_kwargs = {"normalize_embeddings": True}
|
73 |
+
|
74 |
+
try:
|
75 |
+
self.embeddings = HuggingFaceEmbeddings(
|
76 |
+
model_name=EMBEDDING_MODEL,
|
77 |
+
model_kwargs=model_kwargs,
|
78 |
+
encode_kwargs=encode_kwargs
|
79 |
+
)
|
80 |
+
logger.info(f"임베딩 모델 초기화 완료: {EMBEDDING_MODEL}")
|
81 |
+
except Exception as e:
|
82 |
+
logger.error(f"임베딩 모델 초기화 실패: {e}", exc_info=True)
|
83 |
+
raise EmbeddingModelError(f"임베딩 모델 '{EMBEDDING_MODEL}' 초기화 실패: {str(e)}")
|
84 |
+
|
85 |
+
def init_milvus(self) -> Milvus:
|
86 |
+
"""
|
87 |
+
Milvus 벡터 스토어 초기화
|
88 |
+
|
89 |
+
Returns:
|
90 |
+
Milvus 벡터 스토어 인스턴스
|
91 |
+
"""
|
92 |
+
try:
|
93 |
+
connection_args = {
|
94 |
+
"host": MILVUS_HOST,
|
95 |
+
"port": MILVUS_PORT,
|
96 |
+
}
|
97 |
+
|
98 |
+
# 벡터 검색 인덱스 파라미터 (FLAT 인덱스 및 코사인 유사도 메트릭)
|
99 |
+
index_params = {
|
100 |
+
"index_type": "FLAT", # 정확도 우선 FLAT 인덱스
|
101 |
+
"metric_type": "COSINE", # 코사인 유사도 (정규화된 벡터에 적합)
|
102 |
+
"params": {} # FLAT 인덱스에는 추가 파라미터 없음
|
103 |
+
}
|
104 |
+
|
105 |
+
logger.info(f"Milvus 연결 시도 중: {MILVUS_HOST}:{MILVUS_PORT}")
|
106 |
+
milvus_store = Milvus(
|
107 |
+
embedding_function=self.embeddings,
|
108 |
+
collection_name=MILVUS_COLLECTION,
|
109 |
+
connection_args=connection_args,
|
110 |
+
index_params=index_params
|
111 |
+
)
|
112 |
+
logger.info(f"Milvus 연결 성공: {MILVUS_COLLECTION}")
|
113 |
+
return milvus_store
|
114 |
+
except Exception as e:
|
115 |
+
logger.error(f"Milvus 초기화 실패: {e}", exc_info=True)
|
116 |
+
raise VectorStoreInitError(f"Milvus 벡터 스토어 초기화 실패: {str(e)}")
|
117 |
+
|
118 |
+
def init_faiss(self) -> FAISS:
|
119 |
+
"""
|
120 |
+
FAISS 벡터 스토어 초기화 (로컬 대체용)
|
121 |
+
|
122 |
+
Returns:
|
123 |
+
FAISS 벡터 스토어 인스턴스
|
124 |
+
"""
|
125 |
+
try:
|
126 |
+
logger.info("FAISS 벡터 스토어 초기화 중")
|
127 |
+
faiss_store = FAISS.from_documents([], self.embeddings)
|
128 |
+
logger.info("FAISS 벡터 스토어 초기화 완료")
|
129 |
+
return faiss_store
|
130 |
+
except Exception as e:
|
131 |
+
logger.error(f"FAISS 초기화 실패: {e}", exc_info=True)
|
132 |
+
raise VectorStoreInitError(f"FAISS 벡터 스토어 초기화 실패: {str(e)}")
|
133 |
+
|
134 |
+
def create_or_load(self, documents: Optional[List[Document]] = None) -> Any:
|
135 |
+
"""
|
136 |
+
벡터 스토어 생성 또는 로드
|
137 |
+
|
138 |
+
Args:
|
139 |
+
documents: 저장할 문서 리스트 (None이면 빈 스토어 생성)
|
140 |
+
|
141 |
+
Returns:
|
142 |
+
벡터 스토어 인스턴스
|
143 |
+
"""
|
144 |
+
if self.use_milvus:
|
145 |
+
if documents:
|
146 |
+
# 문서가 제공된 경우 새 컬렉션 생성
|
147 |
+
try:
|
148 |
+
# 연결 설정
|
149 |
+
connection_args = {
|
150 |
+
"host": MILVUS_HOST,
|
151 |
+
"port": MILVUS_PORT,
|
152 |
+
}
|
153 |
+
|
154 |
+
# 검색 인덱스 설정
|
155 |
+
index_params = {
|
156 |
+
"index_type": "FLAT", # 정확도 우선
|
157 |
+
"metric_type": "COSINE", # 코사인 유사도
|
158 |
+
"params": {}
|
159 |
+
}
|
160 |
+
|
161 |
+
logger.info(f"Milvus 컬렉션 생성 중: {MILVUS_COLLECTION} (기존 컬렉션 삭제)")
|
162 |
+
|
163 |
+
# 문서로부터 Milvus 컬렉션 생성
|
164 |
+
self.vector_store = Milvus.from_documents(
|
165 |
+
documents=documents,
|
166 |
+
embedding=self.embeddings,
|
167 |
+
collection_name=MILVUS_COLLECTION,
|
168 |
+
connection_args=connection_args,
|
169 |
+
index_params=index_params,
|
170 |
+
drop_old=True # 기존 컬렉션 삭제 (재구축)
|
171 |
+
)
|
172 |
+
|
173 |
+
logger.info(f"Milvus 컬렉션 생성 완료: {len(documents)}개 문서 인덱싱됨")
|
174 |
+
|
175 |
+
except Exception as e:
|
176 |
+
logger.error(f"Milvus 컬렉션 생성 실패: {e}", exc_info=True)
|
177 |
+
# 대체 방안으로 FAISS 사용
|
178 |
+
logger.warning("Milvus 실패로 FAISS로 대체합니다")
|
179 |
+
self.use_milvus = False
|
180 |
+
try:
|
181 |
+
self.vector_store = FAISS.from_documents(documents, self.embeddings)
|
182 |
+
logger.info(f"FAISS로 대체 성공: {len(documents)}개 문서 인덱싱됨")
|
183 |
+
except Exception as faiss_err:
|
184 |
+
logger.error(f"FAISS 대체 실패: {faiss_err}", exc_info=True)
|
185 |
+
raise DocumentIndexError(f"문서 인덱싱 실패 (Milvus 및 FAISS): {str(e)} / {str(faiss_err)}")
|
186 |
+
else:
|
187 |
+
# 기존 컬렉션 로드
|
188 |
+
try:
|
189 |
+
self.vector_store = self.init_milvus()
|
190 |
+
except VectorStoreInitError as e:
|
191 |
+
logger.error(f"Milvus 컬렉션 로드 실패: {e}")
|
192 |
+
# 대체 방안으로 FAISS 사용
|
193 |
+
logger.warning("Milvus 실패로 FAISS로 대체합니다")
|
194 |
+
self.use_milvus = False
|
195 |
+
try:
|
196 |
+
self.vector_store = self.init_faiss()
|
197 |
+
except VectorStoreInitError as faiss_err:
|
198 |
+
logger.error(f"FAISS 대체 실패: {faiss_err}", exc_info=True)
|
199 |
+
raise VectorStoreInitError(f"벡터 스토어 초기화 실패 (Milvus 및 FAISS): {str(e)} / {str(faiss_err)}")
|
200 |
+
else:
|
201 |
+
# FAISS 사용
|
202 |
+
if documents:
|
203 |
+
try:
|
204 |
+
logger.info(f"FAISS 인덱스 생성 중: {len(documents)}개 문서")
|
205 |
+
self.vector_store = FAISS.from_documents(documents, self.embeddings)
|
206 |
+
logger.info("FAISS 인덱스 생성 완료")
|
207 |
+
except Exception as e:
|
208 |
+
logger.error(f"FAISS 인덱스 생성 실패: {e}", exc_info=True)
|
209 |
+
raise DocumentIndexError(f"FAISS 문서 인덱싱 실패: {str(e)}")
|
210 |
+
else:
|
211 |
+
try:
|
212 |
+
self.vector_store = self.init_faiss()
|
213 |
+
except VectorStoreInitError as e:
|
214 |
+
# 이미 로깅됨
|
215 |
+
raise
|
216 |
+
|
217 |
+
return self.vector_store
|
218 |
+
|
219 |
+
def add_documents(self, documents: List[Document]) -> None:
|
220 |
+
"""
|
221 |
+
벡터 스토어에 문서 추가
|
222 |
+
|
223 |
+
Args:
|
224 |
+
documents: 추가할 문서 리스트
|
225 |
+
"""
|
226 |
+
if not documents:
|
227 |
+
logger.warning("추가할 문서가 없습니다")
|
228 |
+
return
|
229 |
+
|
230 |
+
try:
|
231 |
+
if self.vector_store is None:
|
232 |
+
logger.info("벡터 스토어가 초기화되지 않았습니다. 새 벡터 스토어를 생성합니다.")
|
233 |
+
self.create_or_load(documents)
|
234 |
+
else:
|
235 |
+
logger.info(f"{len(documents)}개 문서를 기존 벡터 스토어에 추가합니다")
|
236 |
+
self.vector_store.add_documents(documents)
|
237 |
+
logger.info(f"{len(documents)}개 문서 추가 완료")
|
238 |
+
except Exception as e:
|
239 |
+
logger.error(f"문서 추가 실패: {e}", exc_info=True)
|
240 |
+
raise DocumentIndexError(f"벡터 스토어에 문서 추가 실패: {str(e)}")
|
241 |
+
|
242 |
+
def similarity_search(self, query: str, k: int = 5) -> List[Document]:
|
243 |
+
"""
|
244 |
+
벡터 유사도 검색 수행
|
245 |
+
|
246 |
+
Args:
|
247 |
+
query: 검색 쿼리
|
248 |
+
k: 반환할 결과 수
|
249 |
+
|
250 |
+
Returns:
|
251 |
+
유사도가 높은 문서 리스트
|
252 |
+
"""
|
253 |
+
if not query or not query.strip():
|
254 |
+
logger.warning("빈 쿼리로 검색 시도")
|
255 |
+
return []
|
256 |
+
|
257 |
+
if self.vector_store is None:
|
258 |
+
logger.error("벡터 스토어가 초기화되지 않았습니다")
|
259 |
+
raise VectorSearchError("벡터 스토어가 초기화되지 않았습니다")
|
260 |
+
|
261 |
+
try:
|
262 |
+
logger.info(f"검색 쿼리 실행: '{query[:50]}{'...' if len(query) > 50 else ''}', 상위 {k}개 결과 요청")
|
263 |
+
results = self.vector_store.similarity_search(query, k=k)
|
264 |
+
logger.info(f"검색 완료: {len(results)}개 결과 찾음")
|
265 |
+
return results
|
266 |
+
except Exception as e:
|
267 |
+
logger.error(f"검색 중 오류 발생: {e}", exc_info=True)
|
268 |
+
raise VectorSearchError(f"벡터 검색 실패: {str(e)}")
|
269 |
+
|
270 |
+
def save_local(self, path: str = "faiss_index") -> bool:
|
271 |
+
"""
|
272 |
+
FAISS 인덱스 로컬 저장 (Milvus 사용 안 할 경우)
|
273 |
+
|
274 |
+
Args:
|
275 |
+
path: 저장 경로
|
276 |
+
|
277 |
+
Returns:
|
278 |
+
저장 성공 여부
|
279 |
+
"""
|
280 |
+
if self.vector_store is None:
|
281 |
+
logger.error("저장할 벡터 스토어가 초기화되지 않았습니다")
|
282 |
+
raise PersistenceError("저장할 벡터 스토어가 초기화되지 않았습니다")
|
283 |
+
|
284 |
+
# FAISS만 로컬 저장 가능
|
285 |
+
if not self.use_milvus:
|
286 |
+
try:
|
287 |
+
# 저장 디렉토리가 존재하는지 확인
|
288 |
+
os.makedirs(os.path.dirname(path) if os.path.dirname(path) else path, exist_ok=True)
|
289 |
+
|
290 |
+
self.vector_store.save_local(path)
|
291 |
+
logger.info(f"FAISS 인덱스 로컬 저장 완료: {path}")
|
292 |
+
return True
|
293 |
+
except Exception as e:
|
294 |
+
logger.error(f"FAISS 인덱스 저장 실패: {e}", exc_info=True)
|
295 |
+
raise PersistenceError(f"벡터 인덱스 저장 실패: {str(e)}")
|
296 |
+
else:
|
297 |
+
logger.info("Milvus는 로컬 저장이 필요하지 않습니다")
|
298 |
+
return True
|
299 |
+
|
300 |
+
def load_local(self, path: str = "faiss_index") -> bool:
|
301 |
+
"""
|
302 |
+
FAISS 인덱스 로컬 로드 (Milvus 사용 안 할 경우)
|
303 |
+
|
304 |
+
Args:
|
305 |
+
path: 로드할 인덱스 경로
|
306 |
+
|
307 |
+
Returns:
|
308 |
+
로드 성공 여부
|
309 |
+
"""
|
310 |
+
if self.use_milvus:
|
311 |
+
logger.info("Milvus 사용 중이므로 로컬 로드를 건너뜁니다")
|
312 |
+
try:
|
313 |
+
# Milvus 연결 확인
|
314 |
+
self.vector_store = self.init_milvus()
|
315 |
+
return True
|
316 |
+
except Exception as e:
|
317 |
+
logger.error(f"Milvus 연결 실패, FAISS로 대체: {e}")
|
318 |
+
self.use_milvus = False
|
319 |
+
# FAISS로 계속 진행
|
320 |
+
|
321 |
+
if not os.path.exists(path):
|
322 |
+
logger.warning(f"인덱스 경로가 존재하지 않음: {path}")
|
323 |
+
raise FileNotFoundError(f"벡터 인덱스 경로가 존재하지 않음: {path}")
|
324 |
+
|
325 |
+
try:
|
326 |
+
logger.info(f"FAISS 인덱스 로드 중: {path}")
|
327 |
+
|
328 |
+
# 역직렬화 허용 옵션 추가 (보안 경고 확인 필요)
|
329 |
+
self.vector_store = FAISS.load_local(
|
330 |
+
path,
|
331 |
+
self.embeddings,
|
332 |
+
allow_dangerous_deserialization=True # 역직렬화 허용
|
333 |
+
)
|
334 |
+
logger.info(f"FAISS 인덱스 로드 완료: {path}")
|
335 |
+
return True
|
336 |
+
except FileNotFoundError as e:
|
337 |
+
logger.error(f"FAISS 인덱스 파일을 찾을 수 없음: {e}")
|
338 |
+
raise PersistenceError(f"벡터 인덱스 파일을 찾을 수 없음: {str(e)}")
|
339 |
+
except Exception as e:
|
340 |
+
logger.error(f"FAISS 인덱스 로드 실패: {e}", exc_info=True)
|
341 |
+
|
342 |
+
# 오류 세부 정보 출력
|
343 |
+
import traceback
|
344 |
+
logger.error(f"상세 오류: {traceback.format_exc()}")
|
345 |
+
|
346 |
+
# 새 인덱스 초기화
|
347 |
+
logger.warning("인덱스 로드 실패로 새 FAISS 인덱스 초기화")
|
348 |
+
self.vector_store = self.init_faiss()
|
349 |
+
return False
|