jeongsoo commited on
Commit
a90b938
Β·
1 Parent(s): 6a3db9b

Add application file

Browse files
Files changed (1) hide show
  1. app.py +277 -0
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG 챗봇 μ›Ή ν΄λΌμ΄μ–ΈνŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜
3
+
4
+ Docker 기반 RAG API와 ν†΅μ‹ ν•˜λŠ” μ›Ή μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
5
+ VITO STT κΈ°λŠ₯을 ν†΅ν•œ μŒμ„± μ§ˆμ˜λ„ μ§€μ›ν•©λ‹ˆλ‹€.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import logging
11
+ import tempfile
12
+ import requests
13
+ from flask import Flask, request, jsonify, render_template, send_from_directory
14
+ from werkzeug.utils import secure_filename
15
+ from dotenv import load_dotenv
16
+
17
+ from utils.vito_stt import VitoSTT
18
+
19
+ # 둜거 μ„€μ •
20
+ logging.basicConfig(
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
22
+ level=logging.INFO
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ
27
+ load_dotenv()
28
+
29
+ # Flask μ•± μ΄ˆκΈ°ν™”
30
+ app = Flask(__name__)
31
+
32
+ # μ΅œλŒ€ 파일 크기 μ„€μ • (10MB)
33
+ app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
34
+ app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
35
+
36
+ # μ—…λ‘œλ“œ 폴더가 μ—†μœΌλ©΄ 생성
37
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
38
+
39
+ # RAG API μ„€μ •
40
+ API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:8000')
41
+
42
+ # ν—ˆμš©λ˜λŠ” μ˜€λ””μ˜€ 파일 ν™•μž₯자
43
+ ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
44
+
45
+ # ν—ˆμš©λ˜λŠ” λ¬Έμ„œ 파일 ν™•μž₯자
46
+ ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
47
+
48
+ # VITO STT ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”
49
+ stt_client = VitoSTT()
50
+
51
+ def allowed_audio_file(filename):
52
+ """파일이 ν—ˆμš©λœ μ˜€λ””μ˜€ ν™•μž₯자λ₯Ό κ°€μ§€λŠ”μ§€ 확인"""
53
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
54
+
55
+ def allowed_doc_file(filename):
56
+ """파일이 ν—ˆμš©λœ λ¬Έμ„œ ν™•μž₯자λ₯Ό κ°€μ§€λŠ”μ§€ 확인"""
57
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
58
+
59
+ @app.route('/')
60
+ def index():
61
+ """메인 νŽ˜μ΄μ§€"""
62
+ return render_template('index.html')
63
+
64
+ @app.route('/chat')
65
+ def chat():
66
+ """μ±„νŒ… νŽ˜μ΄μ§€"""
67
+ return render_template('chat.html')
68
+
69
+ @app.route('/knowledge')
70
+ def knowledge():
71
+ """μ§€μ‹λ² μ΄μŠ€ 관리 νŽ˜μ΄μ§€"""
72
+ return render_template('knowledge.html')
73
+
74
+ @app.route('/api/chat', methods=['POST'])
75
+ def api_chat():
76
+ """ν…μŠ€νŠΈ μ±„νŒ… API"""
77
+ try:
78
+ data = request.get_json()
79
+ if not data or 'query' not in data:
80
+ return jsonify({"error": "쿼리가 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
81
+
82
+ query = data['query']
83
+ logger.info(f"쿼리 처리 쀑: {query}")
84
+
85
+ # RAG API 호좜
86
+ response = requests.post(
87
+ f"{API_BASE_URL}/rag",
88
+ json={
89
+ "query": query,
90
+ "retriever_type": data.get("retriever_type", "reranker"),
91
+ "top_k": data.get("top_k", 3),
92
+ "temperature": data.get("temperature", 0.7)
93
+ }
94
+ )
95
+
96
+ # API 응닡 확인
97
+ if response.status_code != 200:
98
+ logger.error(f"API 였λ₯˜: {response.status_code} - {response.text}")
99
+ return jsonify({"error": f"API 였λ₯˜: {response.text}"}), response.status_code
100
+
101
+ # API 응닡 λ°˜ν™˜
102
+ result = response.json()
103
+ logger.info(f"API 응닡 성곡: {len(result['answer'])} 문자")
104
+ return jsonify(result)
105
+
106
+ except requests.RequestException as e:
107
+ logger.error(f"API 톡신 였λ₯˜: {str(e)}")
108
+ return jsonify({"error": f"API μ„œλ²„ μ—°κ²° 였λ₯˜: {str(e)}"}), 503
109
+
110
+ except Exception as e:
111
+ logger.error(f"μ±„νŒ… 처리 쀑 였λ₯˜: {str(e)}", exc_info=True)
112
+ return jsonify({"error": f"처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}"}), 500
113
+
114
+ @app.route('/api/voice', methods=['POST'])
115
+ def api_voice():
116
+ """μŒμ„± μ±„νŒ… API - μ˜€λ””μ˜€ νŒŒμΌμ„ ν…μŠ€νŠΈλ‘œ λ³€ν™˜ν•˜κ³  RAG API에 질의"""
117
+ logger.info("μŒμ„± μ±„νŒ… μš”μ²­ 처리 쀑...")
118
+
119
+ # μ˜€λ””μ˜€ 파일 확인
120
+ if 'audio' not in request.files:
121
+ logger.error("μ˜€λ””μ˜€ 파일이 μ œκ³΅λ˜μ§€ μ•ŠμŒ")
122
+ return jsonify({"error": "μ˜€λ””μ˜€ 파일이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
123
+
124
+ audio_file = request.files['audio']
125
+
126
+ if audio_file.filename == '':
127
+ logger.error("μ˜€λ””μ˜€ 파일λͺ…이 λΉ„μ–΄μžˆμŒ")
128
+ return jsonify({"error": "μ˜€λ””μ˜€ 파일이 μ„ νƒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
129
+
130
+ if not allowed_audio_file(audio_file.filename):
131
+ logger.error(f"ν—ˆμš©λ˜μ§€ μ•ŠλŠ” μ˜€λ””μ˜€ 파일 ν˜•μ‹: {audio_file.filename}")
132
+ return jsonify({"error": "ν—ˆμš©λ˜μ§€ μ•ŠλŠ” μ˜€λ””μ˜€ 파일 ν˜•μ‹μž…λ‹ˆλ‹€."}), 400
133
+
134
+ try:
135
+ # μ˜€λ””μ˜€ 파일 μž„μ‹œ μ €μž₯
136
+ filename = secure_filename(audio_file.filename)
137
+ temp_dir = tempfile.mkdtemp()
138
+ temp_path = os.path.join(temp_dir, filename)
139
+ audio_file.save(temp_path)
140
+
141
+ logger.info(f"μ˜€λ””μ˜€ 파일 μž„μ‹œ μ €μž₯: {temp_path}")
142
+
143
+ # νŒŒμΌμ„ λ°”μ΄νŠΈλ‘œ 읽기
144
+ with open(temp_path, 'rb') as f:
145
+ audio_bytes = f.read()
146
+
147
+ # VITO STTλ₯Ό μ‚¬μš©ν•˜μ—¬ μŒμ„± 인식
148
+ stt_result = stt_client.transcribe_audio(audio_bytes)
149
+
150
+ # μž„μ‹œ 파일 μ‚­μ œ
151
+ os.unlink(temp_path)
152
+ os.rmdir(temp_dir)
153
+
154
+ if not stt_result["success"]:
155
+ logger.error(f"STT 였λ₯˜: {stt_result.get('error', 'μ•Œ 수 μ—†λŠ” 였λ₯˜')}")
156
+ return jsonify({"error": f"μŒμ„± 인식 였λ₯˜: {stt_result.get('error', 'μ•Œ 수 μ—†λŠ” 였λ₯˜')}"}), 500
157
+
158
+ transcription = stt_result["text"]
159
+ if not transcription:
160
+ logger.warning("STT κ²°κ³Όκ°€ λΉ„μ–΄μžˆμŒ")
161
+ return jsonify({"error": "μŒμ„±μ—μ„œ ν…μŠ€νŠΈλ₯Ό μΈμ‹ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."}), 400
162
+
163
+ logger.info(f"μŒμ„± 인식 κ²°κ³Ό: {transcription}")
164
+
165
+ # RAG API 호좜
166
+ try:
167
+ response = requests.post(
168
+ f"{API_BASE_URL}/rag",
169
+ json={
170
+ "query": transcription,
171
+ "retriever_type": "reranker",
172
+ "top_k": 3
173
+ }
174
+ )
175
+
176
+ if response.status_code != 200:
177
+ logger.error(f"RAG API 였λ₯˜: {response.status_code} - {response.text}")
178
+ return jsonify({
179
+ "transcription": transcription,
180
+ "error": f"RAG API 였λ₯˜: {response.text}"
181
+ }), response.status_code
182
+
183
+ # API 결과에 μŒμ„± 인식 κ²°κ³Ό μΆ”κ°€
184
+ result = response.json()
185
+ result["transcription"] = transcription
186
+
187
+ logger.info("μŒμ„± 처리 및 RAG 응닡 성곡")
188
+ return jsonify(result)
189
+
190
+ except requests.RequestException as e:
191
+ logger.error(f"RAG API 톡신 였λ₯˜: {str(e)}")
192
+ return jsonify({
193
+ "transcription": transcription,
194
+ "error": f"RAG API μ„œλ²„ μ—°κ²° 였λ₯˜: {str(e)}"
195
+ }), 503
196
+
197
+ except Exception as e:
198
+ logger.error(f"μŒμ„± 처리 쀑 였λ₯˜: {str(e)}", exc_info=True)
199
+ return jsonify({"error": f"μŒμ„± 처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}"}), 500
200
+
201
+ @app.route('/api/documents', methods=['GET'])
202
+ def api_get_documents():
203
+ """λ¬Έμ„œ λͺ©λ‘ 쑰회 API"""
204
+ try:
205
+ # RAG API 호좜
206
+ response = requests.get(f"{API_BASE_URL}/cache/stats")
207
+
208
+ if response.status_code != 200:
209
+ logger.error(f"λ¬Έμ„œ λͺ©λ‘ 쑰회 API 였λ₯˜: {response.status_code} - {response.text}")
210
+ return jsonify({"error": f"λ¬Έμ„œ λͺ©λ‘ 쑰회 API 였λ₯˜: {response.text}"}), response.status_code
211
+
212
+ # μΊμ‹œ 톡계 정보 λ°˜ν™˜
213
+ return jsonify(response.json())
214
+
215
+ except requests.RequestException as e:
216
+ logger.error(f"API 톡신 였λ₯˜: {str(e)}")
217
+ return jsonify({"error": f"API μ„œλ²„ μ—°κ²° 였λ₯˜: {str(e)}"}), 503
218
+
219
+ except Exception as e:
220
+ logger.error(f"λ¬Έμ„œ λͺ©λ‘ 쑰회 쀑 였λ₯˜: {str(e)}", exc_info=True)
221
+ return jsonify({"error": f"처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}"}), 500
222
+
223
+ @app.route('/api/upload', methods=['POST'])
224
+ def api_upload_document():
225
+ """λ¬Έμ„œ μ—…λ‘œλ“œ API"""
226
+ # 파일 확인
227
+ if 'document' not in request.files:
228
+ return jsonify({"error": "λ¬Έμ„œ 파일이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
229
+
230
+ doc_file = request.files['document']
231
+
232
+ if doc_file.filename == '':
233
+ return jsonify({"error": "λ¬Έμ„œ 파일이 μ„ νƒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
234
+
235
+ if not allowed_doc_file(doc_file.filename):
236
+ return jsonify({"error": "ν—ˆμš©λ˜μ§€ μ•ŠλŠ” λ¬Έμ„œ 파일 ν˜•μ‹μž…λ‹ˆλ‹€."}), 400
237
+
238
+ try:
239
+ # 파일 μž„μ‹œ μ €μž₯
240
+ filename = secure_filename(doc_file.filename)
241
+ temp_dir = tempfile.mkdtemp()
242
+ temp_path = os.path.join(temp_dir, filename)
243
+ doc_file.save(temp_path)
244
+
245
+ logger.info(f"λ¬Έμ„œ 파일 μž„μ‹œ μ €μž₯: {temp_path}")
246
+
247
+ # νŒŒμΌμ„ λ©€ν‹°νŒŒνŠΈ 폼으둜 μ—…λ‘œλ“œ
248
+ with open(temp_path, 'rb') as f:
249
+ files = {'document': (filename, f)}
250
+ response = requests.post(f"{API_BASE_URL}/documents/upload", files=files)
251
+
252
+ # μž„μ‹œ 파일 μ‚­μ œ
253
+ os.unlink(temp_path)
254
+ os.rmdir(temp_dir)
255
+
256
+ if response.status_code not in (200, 201):
257
+ logger.error(f"λ¬Έμ„œ μ—…λ‘œλ“œ API 였λ₯˜: {response.status_code} - {response.text}")
258
+ return jsonify({"error": f"λ¬Έμ„œ μ—…λ‘œλ“œ API 였λ₯˜: {response.text}"}), response.status_code
259
+
260
+ return jsonify(response.json())
261
+
262
+ except requests.RequestException as e:
263
+ logger.error(f"API 톡신 였λ₯˜: {str(e)}")
264
+ return jsonify({"error": f"API μ„œλ²„ μ—°κ²° 였λ₯˜: {str(e)}"}), 503
265
+
266
+ except Exception as e:
267
+ logger.error(f"λ¬Έμ„œ μ—…λ‘œλ“œ 쀑 였λ₯˜: {str(e)}", exc_info=True)
268
+ return jsonify({"error": f"처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}"}), 500
269
+
270
+ # 정적 파일 οΏ½οΏ½οΏ½λΉ™
271
+ @app.route('/static/<path:path>')
272
+ def send_static(path):
273
+ return send_from_directory('static', path)
274
+
275
+ if __name__ == '__main__':
276
+ port = int(os.getenv('PORT', 5000))
277
+ app.run(debug=True, host='0.0.0.0', port=port)