SOY NV AI commited on
Commit
d54e6a9
·
1 Parent(s): d5b7e8c

feat: Parent Chunk 湲곕뒫 異붽? 諛?RAG ?쒖뒪??媛쒖꽑

Browse files

- Parent Chunk ?앹꽦 湲곕뒫 異붽? (AI 遺꾩꽍: ?멸퀎愿€, 罹먮┃?? ?ㅽ넗由? ?먰뵾?뚮뱶, 湲고?)
- 2?④퀎 RAG 寃€???쒖뒪??援ы쁽 (Parent Chunk濡?臾몃㎘ ?뚯븙 + Child Chunk濡??뺣? 寃€??
- ?뚯씪 ??젣 ??Parent Chunk?€ Child Chunk 紐⑤몢 ??젣?섎룄濡?媛쒖꽑
- 愿€由??섏씠吏€?먯꽌 Parent Chunk ?댁슜 ?뺤씤 湲곕뒫 異붽?
- Ollama 404 ?ㅻ쪟 泥섎━ 媛쒖꽑 (紐⑤뜽 ?놁쓣 ??紐낇솗??硫붿떆吏€ ?쒓났)
- EXAONE 紐⑤뜽 愿€???ㅽ겕由쏀듃 諛?媛€?대뱶 臾몄꽌 異붽?
- ?ㅼ젣 ?ㅼ튂??紐⑤뜽留??쒖떆?섎룄濡?紐⑤뜽 紐⑸줉 API ?섏젙

EXAONE-3.0-7.8B-Instruct.modelfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
2
+
3
+ # 모델 설정
4
+ PARAMETER temperature 0.7
5
+ PARAMETER top_p 0.9
6
+ PARAMETER top_k 40
7
+ PARAMETER num_ctx 4096
8
+ PARAMETER num_predict 512
9
+
10
+ # 시스템 프롬프트
11
+ SYSTEM """You are EXAONE, a helpful AI assistant developed by LG AI Research.
12
+ You are designed to be helpful, harmless, and honest.
13
+ You can communicate in both Korean and English."""
14
+
15
+ # EXAONE 모델의 채팅 템플릿
16
+ TEMPLATE """{{ if .System }}<|im_start|>system
17
+ {{ .System }}<|im_end|>
18
+ {{ end }}{{ if .Prompt }}<|im_start|>user
19
+ {{ .Prompt }}<|im_end|>
20
+ {{ end }}<|im_start|>assistant
21
+ {{ .Response }}<|im_end|>
22
+ """
EXAONE_설치_가이드.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EXAONE-3.0-7.8B-Instruct Ollama 설치 가이드
2
+
3
+ Hugging Face의 EXAONE-3.0-7.8B-Instruct 모델을 Ollama에 추가하는 방법입니다.
4
+
5
+ ## 사전 준비
6
+
7
+ ### 1. Hugging Face 계정 및 액세스 권한
8
+
9
+ 1. [Hugging Face 계정 생성](https://huggingface.co/join) (없는 경우)
10
+ 2. [EXAONE-3.0-7.8B-Instruct 모델 페이지](https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct) 방문
11
+ 3. 사용 조건에 동의하고 액세스 권한 요청
12
+
13
+ ### 2. Hugging Face 토큰 설정
14
+
15
+ ```bash
16
+ # Hugging Face CLI로 로그인
17
+ huggingface-cli login
18
+
19
+ # 또는 환경 변수로 토큰 설정
20
+ export HUGGINGFACE_HUB_TOKEN=your_token_here
21
+ ```
22
+
23
+ Windows PowerShell:
24
+ ```powershell
25
+ $env:HUGGINGFACE_HUB_TOKEN="your_token_here"
26
+ ```
27
+
28
+ ### 3. 필요한 패키지 설치
29
+
30
+ ```bash
31
+ pip install huggingface_hub transformers torch
32
+ ```
33
+
34
+ ## 설치 방법
35
+
36
+ ### 방법 1: Ollama의 --from 옵션 사용 (가장 간단)
37
+
38
+ ```bash
39
+ ollama create EXAONE-3.0-7.8B-Instruct --from huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
40
+ ```
41
+
42
+ **주의사항:**
43
+ - Ollama 최신 버전이 필요합니다 (0.1.0 이상)
44
+ - 모델이 GGUF 형식으로 변환되어 있어야 합니다
45
+ - Hugging Face에 GGUF 버전이 없으면 이 방법은 작동하지 않을 수 있습니다
46
+
47
+ ### 방법 2: Modelfile 사용 (권장)
48
+
49
+ 1. `add_exaone_model.py` 스크립트 실행:
50
+ ```bash
51
+ python add_exaone_model.py
52
+ ```
53
+
54
+ 2. 생성된 Modelfile로 모델 생성:
55
+ ```bash
56
+ ollama create EXAONE-3.0-7.8B-Instruct -f EXAONE-3.0-7.8B-Instruct.modelfile
57
+ ```
58
+
59
+ ### 방법 3: 수동 Modelfile 생성
60
+
61
+ 1. `EXAONE-3.0-7.8B-Instruct.modelfile` 파일 생성:
62
+
63
+ ```modelfile
64
+ FROM huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
65
+
66
+ PARAMETER temperature 0.7
67
+ PARAMETER top_p 0.9
68
+ PARAMETER top_k 40
69
+ PARAMETER num_ctx 4096
70
+
71
+ SYSTEM """You are EXAONE, a helpful AI assistant developed by LG AI Research."""
72
+
73
+ TEMPLATE """{{ if .System }}<|im_start|>system
74
+ {{ .System }}<|im_end|>
75
+ {{ end }}{{ if .Prompt }}<|im_start|>user
76
+ {{ .Prompt }}<|im_end|>
77
+ {{ end }}<|im_start|>assistant
78
+ {{ .Response }}<|im_end|>
79
+ """
80
+ ```
81
+
82
+ 2. 모델 생성:
83
+ ```bash
84
+ ollama create EXAONE-3.0-7.8B-Instruct -f EXAONE-3.0-7.8B-Instruct.modelfile
85
+ ```
86
+
87
+ ### 방법 4: GGUF 변환 후 추가 (고급)
88
+
89
+ Hugging Face 모델이 GGUF 형식이 아닌 경우, llama.cpp를 사용하여 변환해야 합니다.
90
+
91
+ 1. llama.cpp 설치 및 빌드
92
+ 2. 모델을 GGUF 형식으로 변환
93
+ 3. 변환된 모델을 Ollama에 추가
94
+
95
+ 자세한 내용은 [llama.cpp 문서](https://github.com/ggerganov/llama.cpp)를 참고하세요.
96
+
97
+ ## 설치 확인
98
+
99
+ ```bash
100
+ # 모델 목록 확인
101
+ ollama list
102
+
103
+ # 모델 테스트
104
+ ollama run EXAONE-3.0-7.8B-Instruct "안녕하세요"
105
+ ```
106
+
107
+ ## 문제 해결
108
+
109
+ ### 1. "file does not exist" 오류
110
+
111
+ - Hugging Face 토큰이 올바르게 설정되었는지 확인
112
+ - 모델 액세스 권한이 승인되었는지 확인
113
+ - Ollama 버전이 최신인지 확인
114
+
115
+ ### 2. 모델이 너무 큼
116
+
117
+ - 모델 크기는 약 15GB입니다
118
+ - 충분한 디스크 공간과 메모리가 필요합니다
119
+ - GPU 메모리 최소 16GB 권장
120
+
121
+ ### 3. 변환 오류
122
+
123
+ - 모델이 GGUF 형식이 아닌 경우, llama.cpp로 변환 필요
124
+ - 또는 Ollama의 최신 버전에서 Hugging Face 모델 직접 지원 여부 확인
125
+
126
+ ## 참고 자료
127
+
128
+ - [EXAONE-3.0-7.8B-Instruct 모델 페이지](https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct)
129
+ - [Ollama 공식 문서](https://github.com/ollama/ollama)
130
+ - [EXAONE 기술 보고서](https://www.lgresearch.ai/data/upload/tech_report/ko/EXAONE_3.0_Technical_Report.pdf)
131
+
132
+ ## 대안: 직접 Hugging Face 모델 사용
133
+
134
+ Ollama에 추가하는 것이 어려운 경우, Python에서 직접 Hugging Face 모델을 사용할 수도 있습니다:
135
+
136
+ ```python
137
+ from transformers import AutoModelForCausalLM, AutoTokenizer
138
+
139
+ model = AutoModelForCausalLM.from_pretrained(
140
+ "LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct",
141
+ torch_dtype=torch.bfloat16,
142
+ trust_remote_code=True,
143
+ device_map="auto"
144
+ )
145
+ tokenizer = AutoTokenizer.from_pretrained("LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct")
146
+ ```
147
+
148
+ 하지만 이 경우 Ollama API와 통합하려면 추가 작업이 필요합니다.
149
+
EXAONE_추가_안내.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EXAONE-3.0-7.8B-Instruct Ollama 추가 안내
2
+
3
+ 현재 Ollama 버전(0.13.0)에서는 Hugging Face 모델을 직접 가져오는 기능이 제한적입니다.
4
+
5
+ ## 현재 상황
6
+
7
+ - **Ollama 버전**: 0.13.0
8
+ - **Hugging Face 토큰**: 설정 완료
9
+ - **문제**: `FROM huggingface:` 프리픽스가 지원되지 않음
10
+
11
+ ## 해결 방법
12
+
13
+ ### 방법 1: Ollama 업데이트 (가장 권장)
14
+
15
+ 최신 버전의 Ollama는 Hugging Face 모델을 더 잘 지원합니다.
16
+
17
+ 1. [Ollama 다운로드 페이지](https://ollama.ai/download)에서 최신 버전 다운로드
18
+ 2. 설치 후 재시작
19
+ 3. 다음 명령어로 모델 추가 시도:
20
+
21
+ ```powershell
22
+ $env:HUGGINGFACE_HUB_TOKEN="YOUR_HUGGINGFACE_TOKEN_HERE"
23
+ ollama create EXAONE-3.0-7.8B-Instruct --from huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
24
+ ```
25
+
26
+ ### 방법 2: GGUF 형식 모델 사용
27
+
28
+ EXAONE 모델의 GGUF 버전이 있다면:
29
+
30
+ 1. GGUF 모델 파일 다운로드
31
+ 2. 로컬 경로에 저장
32
+ 3. Modelfile 생성:
33
+
34
+ ```modelfile
35
+ FROM /path/to/exaone-model.gguf
36
+ PARAMETER temperature 0.7
37
+ PARAMETER top_p 0.9
38
+ ```
39
+
40
+ 4. 모델 생성:
41
+ ```bash
42
+ ollama create EXAONE-3.0-7.8B-Instruct -f Modelfile
43
+ ```
44
+
45
+ ### 방법 3: Python에서 직접 사용
46
+
47
+ Ollama를 거치지 않고 Python에서 직접 Hugging Face 모델을 사용할 수 있습니다.
48
+ 하지만 이 경우 웹 애플리케이션과의 통합이 필요합니다.
49
+
50
+ ## 현재 설정된 정보
51
+
52
+ - **Hugging Face 토큰**: `YOUR_HUGGINGFACE_TOKEN_HERE`
53
+ - **모델 이름**: `LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct`
54
+ - **생성된 Modelfile**: `EXAONE-3.0-7.8B-Instruct.modelfile`
55
+
56
+ ## 다음 단계
57
+
58
+ 1. Ollama를 최신 버전으로 업데이트
59
+ 2. 또는 EXAONE 모델의 GGUF 버전을 찾아서 사용
60
+ 3. 또는 Python에서 직접 모델을 사용하도록 애플리케이션 수정
61
+
62
+ ## 참고
63
+
64
+ - [EXAONE 모델 페이지](https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct)
65
+ - [Ollama 공식 문서](https://github.com/ollama/ollama)
66
+
README.md CHANGED
@@ -34,7 +34,21 @@ SOY NV AI/
34
  ├── venv/
35
  ├── requirements.txt
36
  ├── run.py
 
 
37
  └── README.md
38
  ```
39
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
 
34
  ├── venv/
35
  ├── requirements.txt
36
  ├── run.py
37
+ ├── add_exaone_model.py
38
+ ├── EXAONE_설치_가이드.md
39
  └── README.md
40
  ```
41
 
42
+ ## EXAONE-3.0-7.8B-Instruct 모델 추가
43
+
44
+ Hugging Face의 EXAONE 모델을 Ollama에 추가하려면:
45
+
46
+ 1. `EXAONE_설치_가이드.md` 파일을 참고하세요
47
+ 2. 또는 `add_exaone_model.py` 스크립트를 실행하세요:
48
+ ```bash
49
+ python add_exaone_model.py
50
+ ```
51
+
52
+ 자세한 내용은 `EXAONE_설치_가이드.md`를 참고하세요.
53
+
54
 
add_exaone_model.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EXAONE-3.0-7.8B-Instruct 모델을 Ollama에 추가하는 스크립트
3
+
4
+ 사용 방법:
5
+ 1. Hugging Face에서 모델 액세스 권한을 받아야 합니다.
6
+ 2. 필요한 패키지를 설치합니다:
7
+ pip install transformers torch huggingface_hub
8
+ 3. 이 스크립트를 실행합니다:
9
+ python add_exaone_model.py
10
+ """
11
+
12
+ import os
13
+ import subprocess
14
+ import json
15
+ from pathlib import Path
16
+
17
+ def create_ollama_modelfile():
18
+ """Ollama Modelfile 생성"""
19
+ # EXAONE 모델의 실제 채팅 템플릿 형식에 맞춰 수정
20
+ modelfile_content = """FROM huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
21
+
22
+ # 모델 설정
23
+ PARAMETER temperature 0.7
24
+ PARAMETER top_p 0.9
25
+ PARAMETER top_k 40
26
+ PARAMETER num_ctx 4096
27
+ PARAMETER num_predict 512
28
+
29
+ # 시스템 프롬프트
30
+ SYSTEM \"\"\"You are EXAONE, a helpful AI assistant developed by LG AI Research.
31
+ You are designed to be helpful, harmless, and honest.
32
+ You can communicate in both Korean and English.\"\"\"
33
+
34
+ # EXAONE 모델의 채팅 템플릿 형식
35
+ # 참고: 실제 모델의 토크나이저 템플릿을 확인하여 조정이 필요할 수 있습니다.
36
+ TEMPLATE \"\"\"{{ if .System }}<|im_start|>system
37
+ {{ .System }}<|im_end|>
38
+ {{ end }}{{ if .Prompt }}<|im_start|>user
39
+ {{ .Prompt }}<|im_end|>
40
+ {{ end }}<|im_start|>assistant
41
+ {{ .Response }}<|im_end|>
42
+ \"\"\"
43
+ """
44
+
45
+ modelfile_path = Path("EXAONE-3.0-7.8B-Instruct.modelfile")
46
+ modelfile_path.write_text(modelfile_content, encoding='utf-8')
47
+ print(f"✅ Modelfile 생성 완료: {modelfile_path}")
48
+ return modelfile_path
49
+
50
+ def create_ollama_model_from_huggingface():
51
+ """Hugging Face 모델을 사용하여 Ollama 모델 생성"""
52
+ print("=" * 60)
53
+ print("EXAONE-3.0-7.8B-Instruct 모델을 Ollama에 추가합니다")
54
+ print("=" * 60)
55
+
56
+ # 방법 1: Modelfile 사용 (권장)
57
+ print("\n[방법 1] Modelfile을 사용한 모델 생성")
58
+ print("-" * 60)
59
+
60
+ modelfile_path = create_ollama_modelfile()
61
+
62
+ print(f"\n다음 명령어를 실행하여 모델을 생성하세요:")
63
+ print(f" ollama create EXAONE-3.0-7.8B-Instruct -f {modelfile_path}")
64
+
65
+ # 방법 2: 직접 Hugging Face에서 가져오기
66
+ print("\n[방법 2] Hugging Face에서 직접 가져오기")
67
+ print("-" * 60)
68
+ print("다음 명령어를 실행하세요:")
69
+ print(" ollama create EXAONE-3.0-7.8B-Instruct --from huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct")
70
+
71
+ # 방법 3: Python 스크립트로 변환
72
+ print("\n[방법 3] GGUF 형식으로 변환 후 추가 (고급)")
73
+ print("-" * 60)
74
+ print("이 방법은 llama.cpp를 사용하여 모델을 GGUF 형식으로 변환한 후")
75
+ print("Ollama에 추가하는 방법입니다. 더 많은 설정이 필요합니다.")
76
+
77
+ print("\n" + "=" * 60)
78
+ print("⚠️ 중요 사항:")
79
+ print("=" * 60)
80
+ print("1. Hugging Face에서 모델 액세스 권한이 필요합니다.")
81
+ print(" https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct")
82
+ print("2. Hugging Face 토큰이 필요할 수 있습니다.")
83
+ print(" 환경 변수에 설정: export HUGGINGFACE_HUB_TOKEN=your_token")
84
+ print(" Windows PowerShell: $env:HUGGINGFACE_HUB_TOKEN='your_token'")
85
+ print("3. 모델 크기가 약 15GB이므로 충분한 디스크 공간이 필요합니다.")
86
+ print("4. GPU 메모리가 충분해야 합니다 (최소 16GB 권장).")
87
+ print("=" * 60)
88
+
89
+ def check_ollama_installation():
90
+ """Ollama 설치 확인"""
91
+ try:
92
+ result = subprocess.run(['ollama', '--version'],
93
+ capture_output=True, text=True, timeout=5)
94
+ if result.returncode == 0:
95
+ print(f"✅ Ollama 설치 확인: {result.stdout.strip()}")
96
+ return True
97
+ else:
98
+ print("❌ Ollama가 설치되어 있지 않습니다.")
99
+ return False
100
+ except FileNotFoundError:
101
+ print("❌ Ollama가 설치되어 있지 않습니다.")
102
+ print(" 설치 방법: https://ollama.ai/download")
103
+ return False
104
+ except Exception as e:
105
+ print(f"❌ Ollama 확인 중 오류: {e}")
106
+ return False
107
+
108
+ def check_huggingface_access():
109
+ """Hugging Face 액세스 확인"""
110
+ try:
111
+ from huggingface_hub import whoami
112
+ user_info = whoami()
113
+ print(f"✅ Hugging Face 로그인 확인: {user_info.get('name', 'Unknown')}")
114
+ return True
115
+ except Exception as e:
116
+ print(f"⚠️ Hugging Face 로그인 확인 실패: {e}")
117
+ print(" Hugging Face CLI로 로그인하세요: huggingface-cli login")
118
+ return False
119
+
120
+ if __name__ == "__main__":
121
+ print("\n" + "=" * 60)
122
+ print("EXAONE-3.0-7.8B-Instruct Ollama 추가 스크립트")
123
+ print("=" * 60 + "\n")
124
+
125
+ # 사전 확인
126
+ ollama_ok = check_ollama_installation()
127
+ hf_ok = check_huggingface_access()
128
+
129
+ if not ollama_ok:
130
+ print("\n⚠️ Ollama를 먼저 설치해주세요.")
131
+ exit(1)
132
+
133
+ # 모델 추가 방법 안내
134
+ create_ollama_model_from_huggingface()
135
+
136
+ print("\n" + "=" * 60)
137
+ print("다음 단계:")
138
+ print("=" * 60)
139
+ print("1. 위의 명령어 중 하나를 선택하여 실행하세요.")
140
+ print("2. 모델 생성이 완료되면 다음 명령어로 확인하세요:")
141
+ print(" ollama list")
142
+ print("3. 모델이 목록에 나타나면 웹 애플리케이션에서 사용할 수 있습니다.")
143
+ print("=" * 60 + "\n")
144
+
add_exaone_with_token.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EXAONE-3.0-7.8B-Instruct 모델을 Ollama에 추가하는 스크립트 (토큰 포함)
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ # Hugging Face 토큰
10
+ HF_TOKEN = "YOUR_HUGGINGFACE_TOKEN_HERE"
11
+
12
+ def set_huggingface_token():
13
+ """Hugging Face 토큰 설정"""
14
+ os.environ['HUGGINGFACE_HUB_TOKEN'] = HF_TOKEN
15
+ print(f"[OK] Hugging Face 토큰 설정 완료")
16
+
17
+ def create_ollama_modelfile():
18
+ """Ollama Modelfile 생성"""
19
+ modelfile_content = f"""FROM huggingface:LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
20
+
21
+ # 모델 설정
22
+ PARAMETER temperature 0.7
23
+ PARAMETER top_p 0.9
24
+ PARAMETER top_k 40
25
+ PARAMETER num_ctx 4096
26
+ PARAMETER num_predict 512
27
+
28
+ # 시스템 프롬프트
29
+ SYSTEM \"\"\"You are EXAONE, a helpful AI assistant developed by LG AI Research.
30
+ You are designed to be helpful, harmless, and honest.
31
+ You can communicate in both Korean and English.\"\"\"
32
+
33
+ # EXAONE 모델의 채팅 템플릿
34
+ TEMPLATE \"\"\"{{{{ if .System }}}}<|im_start|>system
35
+ {{{{ .System }}}}<|im_end|>
36
+ {{{{ end }}}}{{{{ if .Prompt }}}}<|im_start|>user
37
+ {{{{ .Prompt }}}}<|im_end|>
38
+ {{{{ end }}}}<|im_start|>assistant
39
+ {{{{ .Response }}}}<|im_end|>
40
+ \"\"\"
41
+ """
42
+
43
+ modelfile_path = Path("EXAONE-3.0-7.8B-Instruct.modelfile")
44
+ modelfile_path.write_text(modelfile_content, encoding='utf-8')
45
+ print(f"[OK] Modelfile 생성 완료: {modelfile_path.absolute()}")
46
+ return modelfile_path
47
+
48
+ def create_model_with_modelfile(modelfile_path):
49
+ """Modelfile을 사용하여 Ollama 모델 생성"""
50
+ try:
51
+ print("\n" + "=" * 60)
52
+ print("Ollama 모델 생성 시작...")
53
+ print("=" * 60)
54
+
55
+ # 환경 변수 설정
56
+ env = os.environ.copy()
57
+ env['HUGGINGFACE_HUB_TOKEN'] = HF_TOKEN
58
+
59
+ # ollama create 명령어 실행
60
+ cmd = ['ollama', 'create', 'EXAONE-3.0-7.8B-Instruct', '-f', str(modelfile_path)]
61
+ print(f"실행 명령어: {' '.join(cmd)}")
62
+
63
+ result = subprocess.run(
64
+ cmd,
65
+ env=env,
66
+ capture_output=True,
67
+ text=True,
68
+ timeout=3600 # 1시간 타임아웃
69
+ )
70
+
71
+ if result.returncode == 0:
72
+ print("[OK] 모델 생성 성공!")
73
+ print(result.stdout)
74
+ return True
75
+ else:
76
+ print("[ERROR] 모델 생성 실패")
77
+ print("오류 출력:")
78
+ print(result.stderr)
79
+ return False
80
+
81
+ except subprocess.TimeoutExpired:
82
+ print("[ERROR] 모델 생성 시간 초과 (1시간)")
83
+ return False
84
+ except Exception as e:
85
+ print(f"[ERROR] 오류 발생: {e}")
86
+ return False
87
+
88
+ def check_ollama_installation():
89
+ """Ollama 설치 확인"""
90
+ try:
91
+ result = subprocess.run(['ollama', '--version'],
92
+ capture_output=True, text=True, timeout=5)
93
+ if result.returncode == 0:
94
+ version = result.stdout.strip()
95
+ print(f"[OK] Ollama 설치 확인: {version}")
96
+ return True
97
+ else:
98
+ print("[ERROR] Ollama가 설치되어 있지 않습니다.")
99
+ return False
100
+ except FileNotFoundError:
101
+ print("[ERROR] Ollama가 설치되어 있지 않습니다.")
102
+ print(" 설치 방법: https://ollama.ai/download")
103
+ return False
104
+ except Exception as e:
105
+ print(f"[ERROR] Ollama 확인 중 오류: {e}")
106
+ return False
107
+
108
+ def verify_model():
109
+ """생성된 모델 확인"""
110
+ try:
111
+ result = subprocess.run(['ollama', 'list'],
112
+ capture_output=True, text=True, timeout=5)
113
+ if result.returncode == 0:
114
+ if 'EXAONE-3.0-7.8B-Instruct' in result.stdout:
115
+ print("\n[OK] 모델이 성공적으로 추가되었습니다!")
116
+ print("\n설치된 모델 목록:")
117
+ print(result.stdout)
118
+ return True
119
+ else:
120
+ print("\n[WARNING] 모델이 목록에 나타나지 않습니다.")
121
+ print("\n현재 설치된 모델:")
122
+ print(result.stdout)
123
+ return False
124
+ return False
125
+ except Exception as e:
126
+ print(f"[WARNING] 모델 확인 중 오류: {e}")
127
+ return False
128
+
129
+ if __name__ == "__main__":
130
+ print("\n" + "=" * 60)
131
+ print("EXAONE-3.0-7.8B-Instruct Ollama 추가 스크립트")
132
+ print("=" * 60 + "\n")
133
+
134
+ # 사전 확인
135
+ if not check_ollama_installation():
136
+ print("\n[WARNING] Ollama를 먼저 설치해주세요.")
137
+ exit(1)
138
+
139
+ # 토큰 설정
140
+ set_huggingface_token()
141
+
142
+ # Modelfile 생성
143
+ modelfile_path = create_ollama_modelfile()
144
+
145
+ # 모델 생성 확인
146
+ print("\n[WARNING] 주의사항:")
147
+ print("- 모델 크기가 약 15GB이므로 다운로드에 시간이 걸릴 수 있습니다.")
148
+ print("- 충분한 디스크 공간과 GPU 메모리가 필요합니다.")
149
+ print("- Hugging Face에서 모�� 액세스 권한이 필요합니다.")
150
+
151
+ # 모델 생성 시작
152
+ print("\n모델 생성을 시작합니다...")
153
+
154
+ # 모델 생성
155
+ success = create_model_with_modelfile(modelfile_path)
156
+
157
+ if success:
158
+ verify_model()
159
+ print("\n" + "=" * 60)
160
+ print("[OK] 모델 추가 완료!")
161
+ print("=" * 60)
162
+ print("\n이제 웹 애플리케이션에서 모델을 사용할 수 있습니다.")
163
+ print("모델 테스트:")
164
+ print(" ollama run EXAONE-3.0-7.8B-Instruct \"안녕하세요\"")
165
+ print("=" * 60)
166
+ else:
167
+ print("\n" + "=" * 60)
168
+ print("[ERROR] 모델 생성 실패")
169
+ print("=" * 60)
170
+ print("\n수동으로 모델을 생성하려면:")
171
+ print(f" 1. Hugging Face 토큰 설정:")
172
+ print(f" $env:HUGGINGFACE_HUB_TOKEN='{HF_TOKEN}'")
173
+ print(f" 2. Modelfile로 모델 생성:")
174
+ print(f" ollama create EXAONE-3.0-7.8B-Instruct -f {modelfile_path}")
175
+ print("=" * 60)
176
+
app/database.py CHANGED
@@ -49,6 +49,9 @@ class UploadedFile(db.Model):
49
  parent_file = db.relationship('UploadedFile', remote_side=[id], backref='child_files')
50
 
51
  def to_dict(self):
 
 
 
52
  return {
53
  'id': self.id,
54
  'filename': self.filename,
@@ -58,6 +61,7 @@ class UploadedFile(db.Model):
58
  'uploaded_at': self.uploaded_at.isoformat() if self.uploaded_at else None,
59
  'uploaded_by': self.uploaded_by,
60
  'parent_file_id': self.parent_file_id,
 
61
  'child_count': len(self.child_files) if self.child_files else 0
62
  }
63
 
@@ -123,4 +127,32 @@ class DocumentChunk(db.Model):
123
  'created_at': self.created_at.isoformat() if self.created_at else None
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
 
49
  parent_file = db.relationship('UploadedFile', remote_side=[id], backref='child_files')
50
 
51
  def to_dict(self):
52
+ # 청크 개수 계산
53
+ chunk_count = len(self.chunks) if hasattr(self, 'chunks') else 0
54
+
55
  return {
56
  'id': self.id,
57
  'filename': self.filename,
 
61
  'uploaded_at': self.uploaded_at.isoformat() if self.uploaded_at else None,
62
  'uploaded_by': self.uploaded_by,
63
  'parent_file_id': self.parent_file_id,
64
+ 'chunk_count': chunk_count,
65
  'child_count': len(self.child_files) if self.child_files else 0
66
  }
67
 
 
127
  'created_at': self.created_at.isoformat() if self.created_at else None
128
  }
129
 
130
+ # Parent Chunk 모델 (AI 분석 결과 저장)
131
+ class ParentChunk(db.Model):
132
+ id = db.Column(db.Integer, primary_key=True)
133
+ file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False, unique=True)
134
+ world_view = db.Column(db.Text, nullable=True) # 세계관 설명
135
+ characters = db.Column(db.Text, nullable=True) # 주요 캐릭터 분석
136
+ story = db.Column(db.Text, nullable=True) # 주요 스토리 분석
137
+ episodes = db.Column(db.Text, nullable=True) # 주요 에피소드 분석
138
+ others = db.Column(db.Text, nullable=True) # 기타
139
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
140
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
141
+
142
+ # 관계
143
+ file = db.relationship('UploadedFile', backref='parent_chunk')
144
+
145
+ def to_dict(self):
146
+ return {
147
+ 'id': self.id,
148
+ 'file_id': self.file_id,
149
+ 'world_view': self.world_view,
150
+ 'characters': self.characters,
151
+ 'story': self.story,
152
+ 'episodes': self.episodes,
153
+ 'others': self.others,
154
+ 'created_at': self.created_at.isoformat() if self.created_at else None,
155
+ 'updated_at': self.updated_at.isoformat() if self.updated_at else None
156
+ }
157
+
158
 
app/routes.py CHANGED
@@ -1,7 +1,7 @@
1
  from flask import Blueprint, render_template, request, jsonify, send_from_directory, redirect, url_for, flash
2
  from flask_login import login_user, logout_user, login_required, current_user
3
  from werkzeug.utils import secure_filename
4
- from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk
5
  import requests
6
  import os
7
  from datetime import datetime
@@ -179,33 +179,269 @@ def split_text_into_chunks(text, min_chunk_size=200, max_chunk_size=1000, overla
179
  def create_chunks_for_file(file_id, content):
180
  """파일 내용을 의미 기반 청크로 분할하여 저장"""
181
  try:
 
 
 
182
  # 기존 청크 삭제
183
- DocumentChunk.query.filter_by(file_id=file_id).delete()
 
 
 
 
184
 
185
  # 의미 기반 청킹 (문장과 문단 경계를 고려하여 분할)
186
  # min_chunk_size: 최소 200자, max_chunk_size: 최대 1000자, overlap: 150자
187
  chunks = split_text_into_chunks(content, min_chunk_size=200, max_chunk_size=1000, overlap=150)
 
 
 
 
 
188
 
189
  # 각 청크를 데이터베이스에 저장
 
190
  for idx, chunk_content in enumerate(chunks):
191
- chunk = DocumentChunk(
192
- file_id=file_id,
193
- chunk_index=idx,
194
- content=chunk_content
195
- )
196
- db.session.add(chunk)
 
 
 
 
 
 
 
 
 
197
 
198
  db.session.commit()
199
- return len(chunks)
 
 
 
 
 
 
 
 
 
200
  except Exception as e:
201
  db.session.rollback()
202
- print(f"청크 생성 오류: {str(e)}")
203
  import traceback
204
  traceback.print_exc()
205
  return 0
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  def search_relevant_chunks(query, file_ids=None, model_name=None, top_k=25, min_score=1):
208
- """질문과 관련된 청크 검색 (개선된 키워드 기반 검색)"""
209
  try:
210
  # 검색 쿼리 준비 - 한글과 영문 단어 모두 추출
211
  query_words = set(re.findall(r'[가-힣]+|\w+', query.lower()))
@@ -527,13 +763,15 @@ def delete_user(user_id):
527
  @main_bp.route('/api/ollama/models', methods=['GET'])
528
  @login_required
529
  def get_ollama_models():
530
- """Ollama에서 사용 가능한 모델 목록 가져오기"""
531
  try:
532
  response = requests.get(f'{OLLAMA_BASE_URL}/api/tags', timeout=5)
533
  if response.status_code == 200:
534
  data = response.json()
535
- models = [{'name': model['name']} for model in data.get('models', [])]
536
- return jsonify({'models': models})
 
 
537
  else:
538
  return jsonify({'error': 'Ollama 서버에 연결할 수 없습니다.', 'models': []}), 500
539
  except requests.exceptions.ConnectionError:
@@ -565,23 +803,60 @@ def chat():
565
  use_rag = True # RAG 사용 여부
566
 
567
  if use_rag:
568
- # 관련 청크 검색
569
  print(f"\n[RAG 검색] 모델: {model}, 질문: {message[:50]}...")
570
  print(f"[RAG 검색] 선택된 파일 ID: {file_ids if file_ids else '없음 (모든 파일 검색)'}")
571
 
572
- # 많은 청크를 검색하도록 top_k 증가 (기본 25개)
 
 
 
 
 
 
 
 
573
  relevant_chunks = search_relevant_chunks(
574
  query=message,
575
  file_ids=file_ids if file_ids else None,
576
  model_name=model,
577
- top_k=25, # 5개에서 25개로 증가
578
- min_score=0.5 # 최소 점수 임계값 낮춤 (더 많은 청크 포함)
579
  )
 
580
 
581
- print(f"[RAG 검색] 검색된 청크 수: {len(relevant_chunks)}")
 
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  if relevant_chunks:
584
- context_parts = []
585
  seen_files = set()
586
 
587
  for chunk in relevant_chunks:
@@ -590,28 +865,68 @@ def chat():
590
  seen_files.add(file.original_filename)
591
  print(f"[RAG 검색] 사용된 파일: {file.original_filename} (모델: {file.model_name})")
592
 
593
- context_parts.append(f"[{file.original_filename} - 청크 {chunk.chunk_index + 1}]\n{chunk.content}")
594
 
595
- if context_parts:
596
  # 컨텍스트 길이 확인 및 최적화
597
- full_context = "\n\n".join(context_parts)
598
- context_length = len(full_context)
599
 
600
- # 컨텍스트가 너무 길면 일부만 사용 (최대 15000자)
601
- if context_length > 15000:
602
- # 상위 점수 청크 우선 유지하면서 길이 조절
603
  truncated_parts = []
604
  current_length = 0
605
- for part in context_parts:
606
  if current_length + len(part) > 15000:
607
  break
608
  truncated_parts.append(part)
609
  current_length += len(part)
610
- full_context = "\n\n".join(truncated_parts)
611
- print(f"[RAG 검색] 컨텍스트 길이 조절: {context_length}자 → {len(full_context)}자")
612
 
613
- context = f"다음은 질문과 관련된 웹소설 내용입니다 (RAG 검색 결과, 총 {len(relevant_chunks)}개 청크):\n\n{full_context}\n\n위 내용을 충분히 참고하여 다음 질문에 정확하고 상세하게 답변해주세요. 웹소설의 맥락과 스토리를 고려하여 답변해주세요:\n\n"
614
- print(f"[RAG 검색] 컨텍스트 생성 완료 ({len(seen_files)}개 파일, {len(relevant_chunks)}개 청크, {len(full_context)})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
  else:
616
  # RAG 검색 결과가 없으면 기존 방식 사용
617
  print(f"[RAG 검색] 관련 청크를 찾지 못했습니다. 전체 파일 내용 사용")
@@ -763,7 +1078,18 @@ def chat():
763
 
764
  return jsonify(response_data)
765
  else:
766
- error_msg = f'Ollama 서버 오류: {ollama_response.status_code}'
 
 
 
 
 
 
 
 
 
 
 
767
  return jsonify({'error': error_msg}), ollama_response.status_code
768
 
769
  except requests.exceptions.ConnectionError:
@@ -971,33 +1297,65 @@ def upload_file():
971
  # 텍스트 파일인 경우 청크로 분할하여 저장 (RAG용)
972
  if original_filename.lower().endswith(('.txt', '.md')):
973
  try:
974
- print(f"텍스트 파일 청크 분할 시작: {original_filename}")
 
 
 
975
  encoding = 'utf-8'
976
  try:
977
  with open(file_path, 'r', encoding=encoding) as f:
978
  content = f.read()
 
979
  except UnicodeDecodeError:
980
- print(f"UTF-8 인코딩 실패, CP949 시도: {original_filename}")
981
  with open(file_path, 'r', encoding='cp949') as f:
982
  content = f.read()
 
983
 
 
 
984
  chunk_count = create_chunks_for_file(uploaded_file.id, content)
 
985
  if chunk_count > 0:
 
986
  print(f"파일 {original_filename}을 {chunk_count}개의 청크로 분할했습니다.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
987
  except Exception as e:
988
- print(f"청크 생성 중 오류 (무시): {str(e)}")
 
 
989
  import traceback
990
  traceback.print_exc()
991
 
992
- # 청크 개수 저장
993
  chunk_count = 0
994
  if original_filename.lower().endswith(('.txt', '.md')):
995
  chunk_count = DocumentChunk.query.filter_by(file_id=uploaded_file.id).count()
 
996
 
997
  db.session.commit()
998
  log_print(f"[8/8] 데이터베이스 커밋 완료: {original_filename}")
999
  log_print(f"[8/8] 연결된 모델: {model_name}")
1000
  log_print(f"[8/8] 생성된 청크 수: {chunk_count}")
 
 
 
 
 
 
1001
  log_print(f"{'='*60}")
1002
  log_print(f"=== 파일 업로드 성공 ===")
1003
  log_print(f"{'='*60}\n")
@@ -1058,9 +1416,19 @@ def get_files():
1058
  files_with_children = []
1059
  for file in files:
1060
  file_dict = file.to_dict()
 
 
 
 
1061
  # 이어서 업로드된 파일들도 조회
1062
  child_files = UploadedFile.query.filter_by(parent_file_id=file.id).order_by(UploadedFile.uploaded_at.asc()).all()
1063
- file_dict['child_files'] = [child.to_dict() for child in child_files]
 
 
 
 
 
 
1064
  files_with_children.append(file_dict)
1065
 
1066
  # 모델별 통계 정보 추가 (원본 파일만 카운트)
@@ -1092,6 +1460,70 @@ def get_files():
1092
  except Exception as e:
1093
  return jsonify({'error': f'파일 목록 조회 중 오류가 발생했습니다: {str(e)}'}), 500
1094
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
  @main_bp.route('/api/files/<int:file_id>', methods=['DELETE'])
1096
  @login_required
1097
  def delete_file(file_id):
@@ -1127,15 +1559,28 @@ def delete_file(file_id):
1127
  # 파일 시스템에서 삭제
1128
  if os.path.exists(file_to_delete.file_path):
1129
  os.remove(file_to_delete.file_path)
 
 
 
 
 
 
 
1130
 
1131
- # 관련 청크도 삭제
1132
- DocumentChunk.query.filter_by(file_id=file_to_delete.id).delete()
 
 
 
1133
 
1134
  deleted_files.append(file_to_delete.original_filename)
1135
  db.session.delete(file_to_delete)
1136
  deleted_count += 1
 
1137
  except Exception as e:
1138
  print(f"[파일 삭제 오류] {file_to_delete.original_filename}: {str(e)}")
 
 
1139
 
1140
  db.session.commit()
1141
 
 
1
  from flask import Blueprint, render_template, request, jsonify, send_from_directory, redirect, url_for, flash
2
  from flask_login import login_user, logout_user, login_required, current_user
3
  from werkzeug.utils import secure_filename
4
+ from app.database import db, UploadedFile, User, ChatSession, ChatMessage, DocumentChunk, ParentChunk
5
  import requests
6
  import os
7
  from datetime import datetime
 
179
  def create_chunks_for_file(file_id, content):
180
  """파일 내용을 의미 기반 청크로 분할하여 저장"""
181
  try:
182
+ print(f"[청크 생성] 파일 ID {file_id}에 대한 청크 생성 시작")
183
+ print(f"[청크 생성] 원본 텍스트 길이: {len(content)}자")
184
+
185
  # 기존 청크 삭제
186
+ existing_chunks = DocumentChunk.query.filter_by(file_id=file_id).count()
187
+ if existing_chunks > 0:
188
+ print(f"[청크 생성] 기존 청크 {existing_chunks}개 삭제")
189
+ DocumentChunk.query.filter_by(file_id=file_id).delete()
190
+ db.session.commit()
191
 
192
  # 의미 기반 청킹 (문장과 문단 경계를 고려하여 분할)
193
  # min_chunk_size: 최소 200자, max_chunk_size: 최대 1000자, overlap: 150자
194
  chunks = split_text_into_chunks(content, min_chunk_size=200, max_chunk_size=1000, overlap=150)
195
+ print(f"[청크 생성] 분할된 청크 수: {len(chunks)}개")
196
+
197
+ if len(chunks) == 0:
198
+ print(f"[청크 생성] 경고: 청크가 생성되지 않았습니다. 텍스트가 너무 짧거나 비어있을 수 있습니다.")
199
+ return 0
200
 
201
  # 각 청크를 데이터베이스에 저장
202
+ saved_count = 0
203
  for idx, chunk_content in enumerate(chunks):
204
+ try:
205
+ chunk = DocumentChunk(
206
+ file_id=file_id,
207
+ chunk_index=idx,
208
+ content=chunk_content
209
+ )
210
+ db.session.add(chunk)
211
+ saved_count += 1
212
+
213
+ # 진행 상황 출력 (10개마다)
214
+ if (idx + 1) % 10 == 0:
215
+ print(f"[청크 생성] 진행 중: {idx + 1}/{len(chunks)}개 청크 저장 중...")
216
+ except Exception as e:
217
+ print(f"[청크 생성] 경고: 청크 {idx} 저장 중 오류: {str(e)}")
218
+ continue
219
 
220
  db.session.commit()
221
+ print(f"[청크 생성] 완료: {saved_count}개 청크가 데이터베이스에 저장되었습니다.")
222
+
223
+ # 저장 확인
224
+ verified_count = DocumentChunk.query.filter_by(file_id=file_id).count()
225
+ if verified_count != saved_count:
226
+ print(f"[청크 생성] 경고: 저장된 청크 수({saved_count})와 확인된 청크 수({verified_count})가 일치하지 않습니다.")
227
+ else:
228
+ print(f"[청크 생성] 검증 완료: {verified_count}개 청크가 정상적으로 저장되었습니다.")
229
+
230
+ return saved_count
231
  except Exception as e:
232
  db.session.rollback()
233
+ print(f"[청크 생성] 오류: {str(e)}")
234
  import traceback
235
  traceback.print_exc()
236
  return 0
237
 
238
+ def create_parent_chunk_with_ai(file_id, content, model_name):
239
+ """AI를 사용하여 Parent Chunk 생성 (웹소설 분석)"""
240
+ try:
241
+ print(f"[Parent Chunk 생성] 파일 ID {file_id}에 대한 Parent Chunk 생성 시작")
242
+ print(f"[Parent Chunk 생성] 사용 모델: {model_name}")
243
+ print(f"[Parent Chunk 생성] 원본 텍스트 길이: {len(content)}자")
244
+
245
+ # 텍스트가 너무 길면 일부만 사용 (최대 50000자)
246
+ content_preview = content[:50000] if len(content) > 50000 else content
247
+ if len(content) > 50000:
248
+ print(f"[Parent Chunk 생성] 텍스트가 길어 일부만 사용: {len(content_preview)}자 (전체: {len(content)}자)")
249
+
250
+ # 분석 프롬프트 생성
251
+ analysis_prompt = f"""다음 웹소설 텍스트를 분석하여 다음 항목들을 작성해주세요. 각 항목은 명확하고 구체적으로 작성해주세요.
252
+
253
+ 텍스트 내용:
254
+ {content_preview}
255
+
256
+ 위 텍스트를 분석하여 다음 형식으로 답변해주세요:
257
+
258
+ ## 세계관 설명
259
+ [세계관에 대한 상세한 설명을 작성하세요. 배경, 설정, 규칙 등을 포함하세요.]
260
+
261
+ ## 주요 캐릭터 분석
262
+ [주요 등장인물들의 이름, 역할, 성격, 특징 등을 분석하여 작성하세요. 각 캐릭터별로 구분하여 작성하세요.]
263
+
264
+ ## 주요 스토리 분석
265
+ [전체적인 스토리 흐름, 주요 사건, 갈등 구조 등을 분석하여 작성하세요.]
266
+
267
+ ## 주요 에피소드 분석
268
+ [중요한 에피소드나 챕터별 주요 내용을 분석하여 작성하세요. 시간 순서대로 정리하면 좋습니다.]
269
+
270
+ ## 기타
271
+ [위 카테고리에 포함되지 않지만 중요한 정보나 특징 등을 작성하세요.]
272
+
273
+ 각 항목을 명확하게 구분하여 작성해주세요."""
274
+
275
+ print(f"[Parent Chunk 생성] Ollama API에 분석 요청 전송 중...")
276
+
277
+ # Ollama API 호출
278
+ ollama_response = requests.post(
279
+ f'{OLLAMA_BASE_URL}/api/chat',
280
+ json={
281
+ 'model': model_name,
282
+ 'messages': [
283
+ {
284
+ 'role': 'user',
285
+ 'content': analysis_prompt
286
+ }
287
+ ],
288
+ 'stream': False
289
+ },
290
+ timeout=300 # 5분 타임아웃
291
+ )
292
+
293
+ if ollama_response.status_code != 200:
294
+ error_detail = ollama_response.text if ollama_response.text else '상세 정보 없음'
295
+ if ollama_response.status_code == 404:
296
+ error_msg = f'Ollama API 오류 404: 모델 "{model_name}"을(를) 찾을 수 없습니다. 모델이 Ollama에 설치되어 있는지 확인하세요.'
297
+ else:
298
+ error_msg = f'Ollama API 오류: {ollama_response.status_code} - {error_detail[:200]}'
299
+ print(f"[Parent Chunk 생성] ❌ 오류: {error_msg}")
300
+ return None
301
+
302
+ response_data = ollama_response.json()
303
+ analysis_result = response_data.get('message', {}).get('content', '')
304
+
305
+ if not analysis_result:
306
+ print(f"[Parent Chunk 생성] ⚠️ 경고: 분석 결과가 비어있습니다.")
307
+ return None
308
+
309
+ print(f"[Parent Chunk 생성] 분석 결과 수신 완료: {len(analysis_result)}자")
310
+
311
+ # 분석 결과 파싱
312
+ world_view = ""
313
+ characters = ""
314
+ story = ""
315
+ episodes = ""
316
+ others = ""
317
+
318
+ # 각 섹션 추출
319
+ sections = {
320
+ 'world_view': ['## 세계관 설명', '## 세계관', '세계관 설명'],
321
+ 'characters': ['## 주요 캐릭터 분석', '## 주요 캐릭터', '주요 캐릭터 분석', '## 캐릭터'],
322
+ 'story': ['## 주요 스토리 분석', '## 주요 스토리', '주요 스토리 분석', '## 스토리'],
323
+ 'episodes': ['## 주요 에피소드 분석', '## 주요 에피소드', '주요 에피소드 분석', '## 에피소드'],
324
+ 'others': ['## 기타', '기타']
325
+ }
326
+
327
+ lines = analysis_result.split('\n')
328
+ current_section = None
329
+ current_content = []
330
+
331
+ for line in lines:
332
+ line_stripped = line.strip()
333
+
334
+ # 섹션 헤더 확인
335
+ section_found = False
336
+ for section_key, section_headers in sections.items():
337
+ for header in section_headers:
338
+ if header in line_stripped:
339
+ # 이전 섹션 저장
340
+ if current_section:
341
+ if current_section == 'world_view':
342
+ world_view = '\n'.join(current_content).strip()
343
+ elif current_section == 'characters':
344
+ characters = '\n'.join(current_content).strip()
345
+ elif current_section == 'story':
346
+ story = '\n'.join(current_content).strip()
347
+ elif current_section == 'episodes':
348
+ episodes = '\n'.join(current_content).strip()
349
+ elif current_section == 'others':
350
+ others = '\n'.join(current_content).strip()
351
+
352
+ current_section = section_key
353
+ current_content = []
354
+ section_found = True
355
+ break
356
+
357
+ if section_found:
358
+ break
359
+
360
+ if not section_found and current_section:
361
+ # 현재 섹션에 내용 추가
362
+ if line_stripped and not line_stripped.startswith('#'):
363
+ current_content.append(line)
364
+
365
+ # 마지막 섹션 저장
366
+ if current_section:
367
+ if current_section == 'world_view':
368
+ world_view = '\n'.join(current_content).strip()
369
+ elif current_section == 'characters':
370
+ characters = '\n'.join(current_content).strip()
371
+ elif current_section == 'story':
372
+ story = '\n'.join(current_content).strip()
373
+ elif current_section == 'episodes':
374
+ episodes = '\n'.join(current_content).strip()
375
+ elif current_section == 'others':
376
+ others = '\n'.join(current_content).strip()
377
+
378
+ # 파싱 실패 시 전체 내용을 "기타"에 저장
379
+ if not world_view and not characters and not story and not episodes:
380
+ print(f"[Parent Chunk 생성] 경고: 섹션 파싱 실패. 전체 내용을 '기타'에 저장합니다.")
381
+ others = analysis_result.strip()
382
+
383
+ # 기존 Parent Chunk 삭제 (있으면)
384
+ existing_parent = ParentChunk.query.filter_by(file_id=file_id).first()
385
+ if existing_parent:
386
+ db.session.delete(existing_parent)
387
+ db.session.commit()
388
+ print(f"[Parent Chunk 생성] 기존 Parent Chunk 삭제 완료")
389
+
390
+ # Parent Chunk 생성 및 저장
391
+ parent_chunk = ParentChunk(
392
+ file_id=file_id,
393
+ world_view=world_view if world_view else None,
394
+ characters=characters if characters else None,
395
+ story=story if story else None,
396
+ episodes=episodes if episodes else None,
397
+ others=others if others else None
398
+ )
399
+
400
+ db.session.add(parent_chunk)
401
+ db.session.commit()
402
+
403
+ print(f"[Parent Chunk 생성] ✅ 완료: Parent Chunk가 생성되었습니다.")
404
+ print(f"[Parent Chunk 생성] - 세계관: {len(world_view)}자")
405
+ print(f"[Parent Chunk 생성] - 캐릭터: {len(characters)}자")
406
+ print(f"[Parent Chunk 생성] - 스토리: {len(story)}자")
407
+ print(f"[Parent Chunk 생성] - 에피소드: {len(episodes)}자")
408
+ print(f"[Parent Chunk 생성] - 기타: {len(others)}자")
409
+
410
+ return parent_chunk
411
+
412
+ except requests.exceptions.RequestException as e:
413
+ error_msg = f'Ollama API 연결 오류: {str(e)}'
414
+ print(f"[Parent Chunk 생성] ❌ 오류: {error_msg}")
415
+ import traceback
416
+ traceback.print_exc()
417
+ return None
418
+ except Exception as e:
419
+ db.session.rollback()
420
+ error_msg = f'Parent Chunk 생성 중 오류: {str(e)}'
421
+ print(f"[Parent Chunk 생성] ❌ 오류: {error_msg}")
422
+ import traceback
423
+ traceback.print_exc()
424
+ return None
425
+
426
+ def get_parent_chunks_for_files(file_ids):
427
+ """파일 ID 목록에 대한 Parent Chunk 조회 (문맥 파악용)"""
428
+ try:
429
+ if not file_ids:
430
+ return []
431
+
432
+ parent_chunks = []
433
+ for file_id in file_ids:
434
+ parent_chunk = ParentChunk.query.filter_by(file_id=file_id).first()
435
+ if parent_chunk:
436
+ parent_chunks.append(parent_chunk)
437
+
438
+ return parent_chunks
439
+ except Exception as e:
440
+ print(f"[Parent Chunk 조회] 오류: {str(e)}")
441
+ return []
442
+
443
  def search_relevant_chunks(query, file_ids=None, model_name=None, top_k=25, min_score=1):
444
+ """질문과 관련된 청크 검색 (개선된 키워드 기반 검색) - Child Chunk 정밀 검색"""
445
  try:
446
  # 검색 쿼리 준비 - 한글과 영문 단어 모두 추출
447
  query_words = set(re.findall(r'[가-힣]+|\w+', query.lower()))
 
763
  @main_bp.route('/api/ollama/models', methods=['GET'])
764
  @login_required
765
  def get_ollama_models():
766
+ """Ollama에서 사용 가능한 모델 목록 가져오기 (실제 설치된 모델만 반환)"""
767
  try:
768
  response = requests.get(f'{OLLAMA_BASE_URL}/api/tags', timeout=5)
769
  if response.status_code == 200:
770
  data = response.json()
771
+ ollama_models = [{'name': model['name']} for model in data.get('models', [])]
772
+
773
+ # 실제 설치된 모델만 반환
774
+ return jsonify({'models': ollama_models})
775
  else:
776
  return jsonify({'error': 'Ollama 서버에 연결할 수 없습니다.', 'models': []}), 500
777
  except requests.exceptions.ConnectionError:
 
803
  use_rag = True # RAG 사용 여부
804
 
805
  if use_rag:
 
806
  print(f"\n[RAG 검색] 모델: {model}, 질문: {message[:50]}...")
807
  print(f"[RAG 검색] 선택된 파일 ID: {file_ids if file_ids else '없음 (모든 파일 검색)'}")
808
 
809
+ # 1단계: Parent Chunk로 문맥 파악
810
+ parent_chunks = []
811
+ if file_ids:
812
+ print(f"[RAG 검색 1단계] Parent Chunk 조회 시작...")
813
+ parent_chunks = get_parent_chunks_for_files(file_ids)
814
+ print(f"[RAG 검색 1단계] Parent Chunk 조회 완료: {len(parent_chunks)}개 파일")
815
+
816
+ # 2단계: Child Chunk로 정밀 검색
817
+ print(f"[RAG 검색 2단계] Child Chunk 정밀 검색 시작...")
818
  relevant_chunks = search_relevant_chunks(
819
  query=message,
820
  file_ids=file_ids if file_ids else None,
821
  model_name=model,
822
+ top_k=25, # 25 청크 검색
823
+ min_score=0.5 # 최소 점수 임계값
824
  )
825
+ print(f"[RAG 검색 2단계] Child Chunk 검색 완료: {len(relevant_chunks)}개 청크")
826
 
827
+ # 컨텍스트 구성
828
+ context_parts = []
829
 
830
+ # Parent Chunk 정보 추가 (문맥 파악용)
831
+ if parent_chunks:
832
+ parent_context_sections = []
833
+ for parent_chunk in parent_chunks:
834
+ file = parent_chunk.file
835
+ file_info = f"\n=== {file.original_filename} 전체 개요 ===\n"
836
+
837
+ sections = []
838
+ if parent_chunk.world_view:
839
+ sections.append(f"[세계관]\n{parent_chunk.world_view}")
840
+ if parent_chunk.characters:
841
+ sections.append(f"[주요 캐릭터]\n{parent_chunk.characters}")
842
+ if parent_chunk.story:
843
+ sections.append(f"[주요 스토리]\n{parent_chunk.story}")
844
+ if parent_chunk.episodes:
845
+ sections.append(f"[주요 에피소드]\n{parent_chunk.episodes}")
846
+ if parent_chunk.others:
847
+ sections.append(f"[기타 정보]\n{parent_chunk.others}")
848
+
849
+ if sections:
850
+ parent_context_sections.append(file_info + "\n\n".join(sections))
851
+
852
+ if parent_context_sections:
853
+ parent_context = "\n\n".join(parent_context_sections)
854
+ context_parts.append(f"다음은 웹소설의 전체적인 문맥과 개요입니다:\n\n{parent_context}")
855
+ print(f"[RAG 검색] Parent Chunk 컨텍스트 추가: {len(parent_context)}자")
856
+
857
+ # Child Chunk 정보 추가 (정밀 검색 결과)
858
  if relevant_chunks:
859
+ child_context_parts = []
860
  seen_files = set()
861
 
862
  for chunk in relevant_chunks:
 
865
  seen_files.add(file.original_filename)
866
  print(f"[RAG 검색] 사용된 파일: {file.original_filename} (모델: {file.model_name})")
867
 
868
+ child_context_parts.append(f"[{file.original_filename} - 청크 {chunk.chunk_index + 1}]\n{chunk.content}")
869
 
870
+ if child_context_parts:
871
  # 컨텍스트 길이 확인 및 최적화
872
+ full_child_context = "\n\n".join(child_context_parts)
873
+ child_context_length = len(full_child_context)
874
 
875
+ # Child Chunk 컨텍스트가 너무 길면 일부만 사용 (최대 15000자)
876
+ if child_context_length > 15000:
 
877
  truncated_parts = []
878
  current_length = 0
879
+ for part in child_context_parts:
880
  if current_length + len(part) > 15000:
881
  break
882
  truncated_parts.append(part)
883
  current_length += len(part)
884
+ full_child_context = "\n\n".join(truncated_parts)
885
+ print(f"[RAG 검색] Child Chunk 컨텍스트 길이 조절: {child_context_length}자 → {len(full_child_context)}자")
886
 
887
+ context_parts.append(f"다음은 질문과 관련된 웹소설의 구체적인 내용입니다 (정밀 검색 결과, 총 {len(relevant_chunks)}개 청크):\n\n{full_child_context}")
888
+ print(f"[RAG 검색] Child Chunk 컨텍스트 추가: {len(full_child_context)}자")
889
+
890
+ # 최종 컨텍스트 구성
891
+ if context_parts:
892
+ full_context = "\n\n" + "\n\n---\n\n".join(context_parts) + "\n\n"
893
+
894
+ # Parent Chunk와 Child Chunk 모두 있는 경우
895
+ if parent_chunks and relevant_chunks:
896
+ context = f"""다음은 질문에 답하기 위한 웹소설 정보입니다:
897
+
898
+ {full_context}
899
+
900
+ 위 정보를 참고하여 답변해주세요:
901
+ - 먼저 전체적인 문맥(Parent Chunk)을 이해하여 웹소설의 배경과 설정을 파악하세요.
902
+ - 그 다음 구체적인 내용(Child Chunk)을 통해 질문에 대한 정확한 답변을 제공하세요.
903
+ - 웹소설의 맥락과 스토리를 고려하여 일관성 있는 답변을 작성하세요.
904
+
905
+ 질문:
906
+ """
907
+ elif parent_chunks:
908
+ # Parent Chunk만 있는 경우
909
+ context = f"""다음은 웹소설의 전체적인 문맥과 개요입니다:
910
+
911
+ {full_context}
912
+
913
+ 위 정보를 참고하여 질문에 답변해주세요. 웹소설의 배경과 설정을 고려하여 답변하세요.
914
+
915
+ 질문:
916
+ """
917
+ else:
918
+ # Child Chunk만 있는 경우
919
+ context = f"""다음은 질문과 관련된 웹소설의 구체적인 내용입니다:
920
+
921
+ {full_context}
922
+
923
+ 위 내용을 충분히 참고하여 다음 질문에 정확하고 상세하게 답변해주세요. 웹소설의 맥락과 스토리를 고려하여 답변해주세요:
924
+
925
+ 질문:
926
+ """
927
+
928
+ context += message
929
+ print(f"[RAG 검색] 최종 컨텍스트 생성 완료 (Parent Chunk: {len(parent_chunks)}개, Child Chunk: {len(relevant_chunks)}개, 총 {len(context)}자)")
930
  else:
931
  # RAG 검색 결과가 없으면 기존 방식 사용
932
  print(f"[RAG 검색] 관련 청크를 찾지 못했습니다. 전체 파일 내용 사용")
 
1078
 
1079
  return jsonify(response_data)
1080
  else:
1081
+ # 오류 상세 정보 가져오기
1082
+ try:
1083
+ error_detail = ollama_response.json().get('error', ollama_response.text[:200])
1084
+ except:
1085
+ error_detail = ollama_response.text[:200] if ollama_response.text else '상세 정보 없음'
1086
+
1087
+ if ollama_response.status_code == 404:
1088
+ error_msg = f'모델 "{model}"을(를) 찾을 수 없습니다. 모델이 Ollama에 설치되어 있는지 확인하세요. (오류: {error_detail})'
1089
+ else:
1090
+ error_msg = f'Ollama 서버 오류 {ollama_response.status_code}: {error_detail}'
1091
+
1092
+ print(f"[채팅 API] 오류 발생: {error_msg}")
1093
  return jsonify({'error': error_msg}), ollama_response.status_code
1094
 
1095
  except requests.exceptions.ConnectionError:
 
1297
  # 텍스트 파일인 경우 청크로 분할하여 저장 (RAG용)
1298
  if original_filename.lower().endswith(('.txt', '.md')):
1299
  try:
1300
+ log_print(f"[7/8] 청크 생성 시작: {original_filename}")
1301
+ log_print(f"[7/8] 파일 ID: {uploaded_file.id}")
1302
+
1303
+ # 파일 내용 읽기
1304
  encoding = 'utf-8'
1305
  try:
1306
  with open(file_path, 'r', encoding=encoding) as f:
1307
  content = f.read()
1308
+ log_print(f"[7/8] UTF-8 인코딩으로 파일 읽기 성공: {len(content)}자")
1309
  except UnicodeDecodeError:
1310
+ log_print(f"[7/8] UTF-8 인코딩 실패, CP949 시도: {original_filename}")
1311
  with open(file_path, 'r', encoding='cp949') as f:
1312
  content = f.read()
1313
+ log_print(f"[7/8] CP949 인코딩으로 파일 읽기 성공: {len(content)}자")
1314
 
1315
+ # 청크 생성 및 저장
1316
+ log_print(f"[7/8] 청크 생성 함수 호출 중...")
1317
  chunk_count = create_chunks_for_file(uploaded_file.id, content)
1318
+
1319
  if chunk_count > 0:
1320
+ log_print(f"[7/8] ✅ 성공: 파일 {original_filename}을 {chunk_count}개의 청크로 분할했습니다.")
1321
  print(f"파일 {original_filename}을 {chunk_count}개의 청크로 분할했습니다.")
1322
+ else:
1323
+ log_print(f"[7/8] ⚠️ 경고: 청크가 생성되지 않았습니다. (파일이 너무 짧거나 비어있을 수 있습니다.)")
1324
+ print(f"경고: 파일 {original_filename}에 대한 청크가 생성되지 않았습니다.")
1325
+
1326
+ # Parent Chunk 생성 (AI 분석)
1327
+ log_print(f"[7/9] Parent Chunk 생성 시작 (AI 분석)...")
1328
+ parent_chunk = create_parent_chunk_with_ai(uploaded_file.id, content, model_name)
1329
+ if parent_chunk:
1330
+ log_print(f"[7/9] ✅ Parent Chunk 생성 완료: {original_filename}")
1331
+ print(f"Parent Chunk가 생성되었습니다: {original_filename}")
1332
+ else:
1333
+ log_print(f"[7/9] ⚠️ 경고: Parent Chunk 생성 실패: {original_filename}")
1334
+ print(f"경고: Parent Chunk 생성에 실패했습니다: {original_filename}")
1335
+
1336
  except Exception as e:
1337
+ error_msg = f"청크 생성 중 오류: {str(e)}"
1338
+ log_print(f"[7/8] ❌ 오류: {error_msg}")
1339
+ print(error_msg)
1340
  import traceback
1341
  traceback.print_exc()
1342
 
1343
+ # 최종 청크 개수 확인 및 저장
1344
  chunk_count = 0
1345
  if original_filename.lower().endswith(('.txt', '.md')):
1346
  chunk_count = DocumentChunk.query.filter_by(file_id=uploaded_file.id).count()
1347
+ log_print(f"[8/8] 최종 청크 개수 확인: {chunk_count}개")
1348
 
1349
  db.session.commit()
1350
  log_print(f"[8/8] 데이터베이스 커밋 완료: {original_filename}")
1351
  log_print(f"[8/8] 연결된 모델: {model_name}")
1352
  log_print(f"[8/8] 생성된 청크 수: {chunk_count}")
1353
+
1354
+ # 학습 상태 요약
1355
+ if chunk_count > 0:
1356
+ log_print(f"[8/8] ✅ AI 학습 준비 완료: {chunk_count}개 청크가 저장되어 RAG 검색에 사용 가능합니다.")
1357
+ else:
1358
+ log_print(f"[8/8] ⚠️ 경고: 청크가 생성되지 않아 RAG 검색이 불가능합니다.")
1359
  log_print(f"{'='*60}")
1360
  log_print(f"=== 파일 업로드 성공 ===")
1361
  log_print(f"{'='*60}\n")
 
1416
  files_with_children = []
1417
  for file in files:
1418
  file_dict = file.to_dict()
1419
+ # 청크 개수 추가
1420
+ chunk_count = DocumentChunk.query.filter_by(file_id=file.id).count()
1421
+ file_dict['chunk_count'] = chunk_count
1422
+
1423
  # 이어서 업로드된 파일들도 조회
1424
  child_files = UploadedFile.query.filter_by(parent_file_id=file.id).order_by(UploadedFile.uploaded_at.asc()).all()
1425
+ child_files_dict = []
1426
+ for child in child_files:
1427
+ child_dict = child.to_dict()
1428
+ child_chunk_count = DocumentChunk.query.filter_by(file_id=child.id).count()
1429
+ child_dict['chunk_count'] = child_chunk_count
1430
+ child_files_dict.append(child_dict)
1431
+ file_dict['child_files'] = child_files_dict
1432
  files_with_children.append(file_dict)
1433
 
1434
  # 모델별 통계 정보 추가 (원본 파일만 카운트)
 
1460
  except Exception as e:
1461
  return jsonify({'error': f'파일 목록 조회 중 오류가 발생했습니다: {str(e)}'}), 500
1462
 
1463
+ @main_bp.route('/api/files/<int:file_id>/chunks', methods=['GET'])
1464
+ @login_required
1465
+ def get_file_chunks(file_id):
1466
+ """파일의 청크 정보 조회 (학습 상태 확인용)"""
1467
+ try:
1468
+ file = UploadedFile.query.filter_by(id=file_id, uploaded_by=current_user.id).first()
1469
+ if not file:
1470
+ return jsonify({'error': '파일을 찾을 수 없습니다.'}), 404
1471
+
1472
+ chunks = DocumentChunk.query.filter_by(file_id=file_id).order_by(DocumentChunk.chunk_index.asc()).all()
1473
+ total_chunks = len(chunks)
1474
+
1475
+ # 샘플 청크 (처음 3개)
1476
+ sample_chunks = []
1477
+ for chunk in chunks[:3]:
1478
+ sample_chunks.append({
1479
+ 'index': chunk.chunk_index,
1480
+ 'content_preview': chunk.content[:100] + '...' if len(chunk.content) > 100 else chunk.content,
1481
+ 'content_length': len(chunk.content)
1482
+ })
1483
+
1484
+ return jsonify({
1485
+ 'file_id': file_id,
1486
+ 'filename': file.original_filename,
1487
+ 'model_name': file.model_name,
1488
+ 'total_chunks': total_chunks,
1489
+ 'sample_chunks': sample_chunks,
1490
+ 'learning_status': 'ready' if total_chunks > 0 else 'not_ready',
1491
+ 'message': f'{total_chunks}개 청크가 저장되어 RAG 검색에 사용 가능합니다.' if total_chunks > 0 else '청크가 생성되지 않아 RAG 검색이 불가능합니다.'
1492
+ }), 200
1493
+
1494
+ except Exception as e:
1495
+ return jsonify({'error': f'청크 정보 조회 중 오류가 발생했습니다: {str(e)}'}), 500
1496
+
1497
+ @main_bp.route('/api/files/<int:file_id>/parent-chunk', methods=['GET'])
1498
+ @login_required
1499
+ def get_file_parent_chunk(file_id):
1500
+ """파일의 Parent Chunk 조회"""
1501
+ try:
1502
+ file = UploadedFile.query.filter_by(id=file_id, uploaded_by=current_user.id).first()
1503
+ if not file:
1504
+ return jsonify({'error': '파일을 찾을 수 없습니다.'}), 404
1505
+
1506
+ parent_chunk = ParentChunk.query.filter_by(file_id=file_id).first()
1507
+
1508
+ if not parent_chunk:
1509
+ return jsonify({
1510
+ 'file_id': file_id,
1511
+ 'filename': file.original_filename,
1512
+ 'has_parent_chunk': False,
1513
+ 'message': 'Parent Chunk가 생성되지 않았습니다.'
1514
+ }), 200
1515
+
1516
+ return jsonify({
1517
+ 'file_id': file_id,
1518
+ 'filename': file.original_filename,
1519
+ 'has_parent_chunk': True,
1520
+ 'parent_chunk': parent_chunk.to_dict(),
1521
+ 'message': 'Parent Chunk가 존재합니다.'
1522
+ }), 200
1523
+
1524
+ except Exception as e:
1525
+ return jsonify({'error': f'Parent Chunk 조회 중 오류가 발생했습니다: {str(e)}'}), 500
1526
+
1527
  @main_bp.route('/api/files/<int:file_id>', methods=['DELETE'])
1528
  @login_required
1529
  def delete_file(file_id):
 
1559
  # 파일 시스템에서 삭제
1560
  if os.path.exists(file_to_delete.file_path):
1561
  os.remove(file_to_delete.file_path)
1562
+ print(f"[파일 삭제] 파일 시스템에서 삭제: {file_to_delete.file_path}")
1563
+
1564
+ # 관련 Child Chunk (DocumentChunk) 삭제
1565
+ child_chunk_count = DocumentChunk.query.filter_by(file_id=file_to_delete.id).count()
1566
+ if child_chunk_count > 0:
1567
+ DocumentChunk.query.filter_by(file_id=file_to_delete.id).delete()
1568
+ print(f"[파일 삭제] Child Chunk {child_chunk_count}개 삭제 완료")
1569
 
1570
+ # 관련 Parent Chunk 삭제
1571
+ parent_chunk = ParentChunk.query.filter_by(file_id=file_to_delete.id).first()
1572
+ if parent_chunk:
1573
+ db.session.delete(parent_chunk)
1574
+ print(f"[파일 삭제] Parent Chunk 삭제 완료")
1575
 
1576
  deleted_files.append(file_to_delete.original_filename)
1577
  db.session.delete(file_to_delete)
1578
  deleted_count += 1
1579
+ print(f"[파일 삭제] 데이터베이스에서 파일 삭제 완료: {file_to_delete.original_filename}")
1580
  except Exception as e:
1581
  print(f"[파일 삭제 오류] {file_to_delete.original_filename}: {str(e)}")
1582
+ import traceback
1583
+ traceback.print_exc()
1584
 
1585
  db.session.commit()
1586
 
download_exaone_model.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EXAONE-3.0-7.8B-Instruct 모델을 Hugging Face에서 다운로드하고
3
+ Ollama에서 사용할 수 있도록 준비하는 스크립트
4
+ """
5
+
6
+ import os
7
+ from huggingface_hub import snapshot_download, login
8
+
9
+ HF_TOKEN = "YOUR_HUGGINGFACE_TOKEN_HERE"
10
+ MODEL_NAME = "LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct"
11
+
12
+ def download_model():
13
+ """Hugging Face에서 모델 다운로드"""
14
+ print("=" * 60)
15
+ print("EXAONE-3.0-7.8B-Instruct 모델 다운로드")
16
+ print("=" * 60)
17
+
18
+ # Hugging Face 로그인
19
+ try:
20
+ login(token=HF_TOKEN)
21
+ print("[OK] Hugging Face 로그인 성공")
22
+ except Exception as e:
23
+ print(f"[ERROR] Hugging Face 로그인 실패: {e}")
24
+ return False
25
+
26
+ # 모델 다운로드
27
+ try:
28
+ print(f"\n모델 다운로드 시작: {MODEL_NAME}")
29
+ print("주의: 모델 크기가 약 15GB이므로 시간이 걸릴 수 있습니다...")
30
+
31
+ download_path = snapshot_download(
32
+ repo_id=MODEL_NAME,
33
+ token=HF_TOKEN,
34
+ local_dir="./models/EXAONE-3.0-7.8B-Instruct",
35
+ local_dir_use_symlinks=False
36
+ )
37
+
38
+ print(f"\n[OK] 모델 다운로드 완료!")
39
+ print(f"저장 위치: {download_path}")
40
+ return True
41
+
42
+ except Exception as e:
43
+ print(f"[ERROR] 모델 다운로드 실패: {e}")
44
+ return False
45
+
46
+ if __name__ == "__main__":
47
+ import sys
48
+
49
+ print("\n이 스크립트는 EXAONE 모델을 Hugging Face에서 다운로드합니다.")
50
+ print("다운로드된 모델은 Ollama에서 직접 사용할 수 없으며,")
51
+ print("GGUF 형식으로 변환하는 추가 작업이 필요합니다.")
52
+ print("\n참고: Ollama는 일반적으로 GGUF 형식의 모델만 지원합니다.")
53
+
54
+ response = input("\n계속하시겠습니까? (y/n): ")
55
+ if response.lower() != 'y':
56
+ print("다운로드를 취소했습니다.")
57
+ sys.exit(0)
58
+
59
+ success = download_model()
60
+
61
+ if success:
62
+ print("\n" + "=" * 60)
63
+ print("다운로드 완료!")
64
+ print("=" * 60)
65
+ print("\n다음 단계:")
66
+ print("1. llama.cpp를 사용하여 GGUF 형식으로 변환")
67
+ print("2. 변환된 모델을 Ollama에 추가")
68
+ print("\n자세한 내용은 EXAONE_설치_가이드.md를 참고하세요.")
69
+ print("=" * 60)
70
+ else:
71
+ print("\n다운로드에 실패했습니다.")
72
+
install_exaone_direct.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EXAONE-3.0-7.8B-Instruct를 Ollama에 직접 추가하는 스크립트
3
+ 토큰을 사용하여 모델을 추가합니다.
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+ import json
9
+
10
+ HF_TOKEN = "YOUR_HUGGINGFACE_TOKEN_HERE"
11
+
12
+ def create_simple_modelfile():
13
+ """간단한 Modelfile 생성 (로컬 경로 대신 모델 이름만 사용)"""
14
+ modelfile_content = """# EXAONE-3.0-7.8B-Instruct 모델 설정
15
+ # 참고: Ollama가 Hugging Face 모델을 직접 지원하지 않는 경우
16
+ # 다른 방법이 필요할 수 있습니다.
17
+
18
+ PARAMETER temperature 0.7
19
+ PARAMETER top_p 0.9
20
+ PARAMETER top_k 40
21
+ PARAMETER num_ctx 4096
22
+
23
+ SYSTEM \"\"\"You are EXAONE, a helpful AI assistant developed by LG AI Research.
24
+ You can communicate in both Korean and English.\"\"\"
25
+ """
26
+
27
+ with open("EXAONE-3.0-7.8B-Instruct.modelfile", "w", encoding="utf-8") as f:
28
+ f.write(modelfile_content)
29
+
30
+ print("[OK] Modelfile 생성 완료")
31
+
32
+ def check_model_availability():
33
+ """모델이 이미 설치되어 있는지 확인"""
34
+ try:
35
+ result = subprocess.run(['ollama', 'list'],
36
+ capture_output=True, text=True, timeout=5)
37
+ if 'EXAONE' in result.stdout or 'exaone' in result.stdout.lower():
38
+ print("[INFO] EXAONE 모델이 이미 설치되어 있습니다.")
39
+ return True
40
+ return False
41
+ except Exception as e:
42
+ print(f"[WARNING] 모델 확인 중 오류: {e}")
43
+ return False
44
+
45
+ def main():
46
+ print("\n" + "=" * 60)
47
+ print("EXAONE-3.0-7.8B-Instruct Ollama 추가 시도")
48
+ print("=" * 60)
49
+
50
+ # 토큰 설정
51
+ os.environ['HUGGINGFACE_HUB_TOKEN'] = HF_TOKEN
52
+ print(f"[OK] Hugging Face 토큰 설정 완료")
53
+
54
+ # 모델 확인
55
+ if check_model_availability():
56
+ print("\n모델이 이미 설치되어 있습니다.")
57
+ return
58
+
59
+ print("\n현재 Ollama 버전에서는 Hugging Face 모델을 직접 가져올 수 없습니다.")
60
+ print("\n다음 방법을 시도해보세요:")
61
+ print("\n1. Ollama를 최신 버전으로 업데이트")
62
+ print(" https://ollama.ai/download")
63
+
64
+ print("\n2. 수동으로 모델 정보 확인")
65
+ print(" 현재 Ollama 버전 확인: ollama --version")
66
+ print(" 사용 가능한 모델 확인: ollama list")
67
+
68
+ print("\n3. EXAONE 모델의 GGUF 버전 찾기")
69
+ print(" Hugging Face에서 GGUF 형식의 모델을 찾아보세요.")
70
+
71
+ print("\n" + "=" * 60)
72
+ print("참고: 현재 Ollama 0.13.0은 Hugging Face 모델 직접 지원이 제한적입니다.")
73
+ print("=" * 60)
74
+
75
+ if __name__ == "__main__":
76
+ main()
77
+
install_exaone_simple.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EXAONE-3.0-7.8B-Instruct 모델을 Ollama에 추가하는 간단한 스크립트
3
+ Ollama 0.13.0에서는 직접 Hugging Face 모델을 지원하지 않으므로
4
+ 다른 방법을 안내합니다.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ HF_TOKEN = "YOUR_HUGGINGFACE_TOKEN_HERE"
12
+
13
+ def main():
14
+ print("\n" + "=" * 60)
15
+ print("EXAONE-3.0-7.8B-Instruct Ollama 추가 가이드")
16
+ print("=" * 60)
17
+
18
+ print("\n현재 Ollama 버전(0.13.0)에서는 Hugging Face 모델을 직접 가져올 수 없습니다.")
19
+ print("\n다음 방법 중 하나를 선택하세요:")
20
+ print("\n[방법 1] Ollama 업데이트 (권장)")
21
+ print("-" * 60)
22
+ print("최신 버전의 Ollama는 Hugging Face 모델을 더 잘 지원합니다.")
23
+ print("https://ollama.ai/download 에서 최신 버전을 다운로드하세요.")
24
+
25
+ print("\n[방법 2] GGUF 모델 사용")
26
+ print("-" * 60)
27
+ print("EXAONE 모델의 GGUF 버전이 있다면 직접 다운로드하여 사용할 수 있습니다.")
28
+ print("하지만 현재 Hugging Face에는 GGUF 버전이 없는 것으로 보입니다.")
29
+
30
+ print("\n[방법 3] Python에서 직접 사용")
31
+ print("-" * 60)
32
+ print("Ollama를 통하지 않고 Python에서 직접 Hugging Face 모델을 사용할 수 있습니다.")
33
+ print("하지만 이 경우 Ollama API와의 통합이 필요합니다.")
34
+
35
+ print("\n[방법 4] Modelfile 수정")
36
+ print("-" * 60)
37
+ print("로컬에 모델을 다운로드한 후 Modelfile에서 로컬 경로를 참조할 수 있습니다.")
38
+
39
+ print("\n" + "=" * 60)
40
+ print("추천: Ollama를 최신 버전으로 업데이트하세요")
41
+ print("=" * 60)
42
+
43
+ print("\n현재 상태:")
44
+ print(f"- Hugging Face 토큰: 설정됨")
45
+ print(f"- Ollama 버전: 0.13.0 (최신 버전 권장)")
46
+
47
+ print("\n다음 명령어로 Ollama 버전을 확인하세요:")
48
+ print(" ollama --version")
49
+
50
+ print("\nOllama를 업데이트한 후 다시 시도하세요.")
51
+
52
+ if __name__ == "__main__":
53
+ main()
54
+
templates/admin_webnovels.html CHANGED
@@ -320,6 +320,119 @@
320
  display: flex;
321
  gap: 4px;
322
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  </style>
324
  </head>
325
  <body>
@@ -429,6 +542,19 @@
429
  </div>
430
  </div>
431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  <script>
433
  function showAlert(message, type = 'success') {
434
  const container = document.getElementById('alertContainer');
@@ -521,6 +647,7 @@
521
  <td>${uploadDate}</td>
522
  <td>
523
  <div class="file-actions">
 
524
  <button class="btn btn-primary" onclick="continueUpload(${file.id})" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">이어서 업로드</button>
525
  <button class="btn btn-secondary" onclick="deleteFile(${file.id})" style="padding: 4px 8px; font-size: 12px;">삭제</button>
526
  </div>
@@ -796,6 +923,108 @@
796
  }
797
  });
798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  // 페이지 로드 시 초기화
800
  window.addEventListener('load', () => {
801
  loadModelsForFiles();
 
320
  display: flex;
321
  gap: 4px;
322
  }
323
+
324
+ /* Parent Chunk 모달 스타일 */
325
+ .modal {
326
+ display: none;
327
+ position: fixed;
328
+ z-index: 1000;
329
+ left: 0;
330
+ top: 0;
331
+ width: 100%;
332
+ height: 100%;
333
+ background-color: rgba(0, 0, 0, 0.5);
334
+ overflow: auto;
335
+ }
336
+
337
+ .modal.active {
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ }
342
+
343
+ .modal-content {
344
+ background-color: white;
345
+ margin: auto;
346
+ padding: 0;
347
+ border-radius: 8px;
348
+ width: 90%;
349
+ max-width: 900px;
350
+ max-height: 90vh;
351
+ display: flex;
352
+ flex-direction: column;
353
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
354
+ }
355
+
356
+ .modal-header {
357
+ padding: 20px 24px;
358
+ border-bottom: 1px solid #dadce0;
359
+ display: flex;
360
+ justify-content: space-between;
361
+ align-items: center;
362
+ }
363
+
364
+ .modal-header h2 {
365
+ font-size: 20px;
366
+ font-weight: 600;
367
+ margin: 0;
368
+ }
369
+
370
+ .modal-close {
371
+ background: none;
372
+ border: none;
373
+ font-size: 24px;
374
+ cursor: pointer;
375
+ color: #5f6368;
376
+ padding: 0;
377
+ width: 32px;
378
+ height: 32px;
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ border-radius: 50%;
383
+ transition: background 0.2s;
384
+ }
385
+
386
+ .modal-close:hover {
387
+ background: #f1f3f4;
388
+ }
389
+
390
+ .modal-body {
391
+ padding: 24px;
392
+ overflow-y: auto;
393
+ flex: 1;
394
+ }
395
+
396
+ .parent-chunk-section {
397
+ margin-bottom: 24px;
398
+ }
399
+
400
+ .parent-chunk-section:last-child {
401
+ margin-bottom: 0;
402
+ }
403
+
404
+ .parent-chunk-section-title {
405
+ font-size: 16px;
406
+ font-weight: 600;
407
+ color: #1a73e8;
408
+ margin-bottom: 12px;
409
+ padding-bottom: 8px;
410
+ border-bottom: 2px solid #e8f0fe;
411
+ }
412
+
413
+ .parent-chunk-section-content {
414
+ font-size: 14px;
415
+ line-height: 1.8;
416
+ color: #202124;
417
+ white-space: pre-wrap;
418
+ word-wrap: break-word;
419
+ background: #f8f9fa;
420
+ padding: 16px;
421
+ border-radius: 6px;
422
+ border-left: 4px solid #1a73e8;
423
+ }
424
+
425
+ .parent-chunk-empty {
426
+ text-align: center;
427
+ padding: 40px;
428
+ color: #5f6368;
429
+ }
430
+
431
+ .parent-chunk-loading {
432
+ text-align: center;
433
+ padding: 40px;
434
+ color: #1a73e8;
435
+ }
436
  </style>
437
  </head>
438
  <body>
 
542
  </div>
543
  </div>
544
 
545
+ <!-- Parent Chunk 모달 -->
546
+ <div id="parentChunkModal" class="modal">
547
+ <div class="modal-content">
548
+ <div class="modal-header">
549
+ <h2 id="parentChunkModalTitle">Parent Chunk 확인</h2>
550
+ <button class="modal-close" onclick="closeParentChunkModal()">&times;</button>
551
+ </div>
552
+ <div class="modal-body" id="parentChunkModalBody">
553
+ <div class="parent-chunk-loading">로딩 중...</div>
554
+ </div>
555
+ </div>
556
+ </div>
557
+
558
  <script>
559
  function showAlert(message, type = 'success') {
560
  const container = document.getElementById('alertContainer');
 
647
  <td>${uploadDate}</td>
648
  <td>
649
  <div class="file-actions">
650
+ <button class="btn btn-primary" onclick="viewParentChunk(${file.id}, '${escapeHtml(file.original_filename)}')" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">Parent Chunk</button>
651
  <button class="btn btn-primary" onclick="continueUpload(${file.id})" style="padding: 4px 8px; font-size: 12px; margin-right: 4px;">이어서 업로드</button>
652
  <button class="btn btn-secondary" onclick="deleteFile(${file.id})" style="padding: 4px 8px; font-size: 12px;">삭제</button>
653
  </div>
 
923
  }
924
  });
925
 
926
+ // Parent Chunk 확인
927
+ async function viewParentChunk(fileId, fileName) {
928
+ const modal = document.getElementById('parentChunkModal');
929
+ const modalTitle = document.getElementById('parentChunkModalTitle');
930
+ const modalBody = document.getElementById('parentChunkModalBody');
931
+
932
+ modalTitle.textContent = `Parent Chunk 확인 - ${fileName}`;
933
+ modalBody.innerHTML = '<div class="parent-chunk-loading">로딩 중...</div>';
934
+ modal.classList.add('active');
935
+
936
+ try {
937
+ const response = await fetch(`/api/files/${fileId}/parent-chunk`);
938
+ const data = await response.json();
939
+
940
+ if (response.ok) {
941
+ if (data.has_parent_chunk && data.parent_chunk) {
942
+ const chunk = data.parent_chunk;
943
+ let html = '';
944
+
945
+ if (chunk.world_view) {
946
+ html += `
947
+ <div class="parent-chunk-section">
948
+ <div class="parent-chunk-section-title">🌍 세계관 설명</div>
949
+ <div class="parent-chunk-section-content">${escapeHtml(chunk.world_view)}</div>
950
+ </div>
951
+ `;
952
+ }
953
+
954
+ if (chunk.characters) {
955
+ html += `
956
+ <div class="parent-chunk-section">
957
+ <div class="parent-chunk-section-title">👥 주요 캐릭터 분석</div>
958
+ <div class="parent-chunk-section-content">${escapeHtml(chunk.characters)}</div>
959
+ </div>
960
+ `;
961
+ }
962
+
963
+ if (chunk.story) {
964
+ html += `
965
+ <div class="parent-chunk-section">
966
+ <div class="parent-chunk-section-title">📖 주요 스토리 분석</div>
967
+ <div class="parent-chunk-section-content">${escapeHtml(chunk.story)}</div>
968
+ </div>
969
+ `;
970
+ }
971
+
972
+ if (chunk.episodes) {
973
+ html += `
974
+ <div class="parent-chunk-section">
975
+ <div class="parent-chunk-section-title">📚 주요 에피소드 분석</div>
976
+ <div class="parent-chunk-section-content">${escapeHtml(chunk.episodes)}</div>
977
+ </div>
978
+ `;
979
+ }
980
+
981
+ if (chunk.others) {
982
+ html += `
983
+ <div class="parent-chunk-section">
984
+ <div class="parent-chunk-section-title">📝 기타</div>
985
+ <div class="parent-chunk-section-content">${escapeHtml(chunk.others)}</div>
986
+ </div>
987
+ `;
988
+ }
989
+
990
+ if (!html) {
991
+ html = '<div class="parent-chunk-empty">Parent Chunk 내용이 비어있습니다.</div>';
992
+ }
993
+
994
+ modalBody.innerHTML = html;
995
+ } else {
996
+ modalBody.innerHTML = '<div class="parent-chunk-empty">Parent Chunk가 생성되지 않았습니다.<br>파일 업로드 시 AI 분석을 통해 자동으로 생성됩니다.</div>';
997
+ }
998
+ } else {
999
+ modalBody.innerHTML = `<div class="parent-chunk-empty" style="color: #c5221f;">오류: ${data.error || 'Parent Chunk를 불러올 수 없습니다.'}</div>`;
1000
+ }
1001
+ } catch (error) {
1002
+ modalBody.innerHTML = `<div class="parent-chunk-empty" style="color: #c5221f;">오류: ${error.message}</div>`;
1003
+ console.error('Parent Chunk 조회 오류:', error);
1004
+ }
1005
+ }
1006
+
1007
+ // Parent Chunk 모달 닫기
1008
+ function closeParentChunkModal() {
1009
+ const modal = document.getElementById('parentChunkModal');
1010
+ modal.classList.remove('active');
1011
+ }
1012
+
1013
+ // 모달 외부 클릭 시 닫기
1014
+ window.addEventListener('click', (event) => {
1015
+ const modal = document.getElementById('parentChunkModal');
1016
+ if (event.target === modal) {
1017
+ closeParentChunkModal();
1018
+ }
1019
+ });
1020
+
1021
+ // ESC 키로 모달 닫기
1022
+ document.addEventListener('keydown', (event) => {
1023
+ if (event.key === 'Escape') {
1024
+ closeParentChunkModal();
1025
+ }
1026
+ });
1027
+
1028
  // 페이지 로드 시 초기화
1029
  window.addEventListener('load', () => {
1030
  loadModelsForFiles();