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
|