RAG_AgenticServer_Small / app /app_routes.py
jeongsoo's picture
init
27a9fab
"""
RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - API ๋ผ์šฐํŠธ ์ •์˜
"""
import os
import json
import logging
import tempfile
import requests
from flask import request, jsonify, render_template, send_from_directory, session, redirect, url_for
from datetime import datetime
from werkzeug.utils import secure_filename
# ๋กœ๊ฑฐ ๊ฐ€์ ธ์˜ค๊ธฐ
logger = logging.getLogger(__name__)
def register_routes(app, login_required, llm_interface, retriever, stt_client, DocumentProcessor, base_retriever, app_ready, ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL):
"""Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ผ์šฐํŠธ ๋“ฑ๋ก"""
# ํ—ฌํผ ํ•จ์ˆ˜
def allowed_audio_file(filename):
"""ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
def allowed_doc_file(filename):
"""ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ๋ฌธ์„œ ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
# ์ž„๋ฒ ๋”ฉ ์ €์žฅ ํ•จ์ˆ˜
def save_embeddings(base_retriever, file_path):
"""์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ํŒŒ์ผ์— ์ €์žฅ"""
import pickle
import gzip
try:
# ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€
save_data = {
'timestamp': datetime.now().isoformat(),
'retriever': base_retriever
}
# ์••์ถ•ํ•˜์—ฌ ์ €์žฅ (์šฉ๋Ÿ‰ ์ค„์ด๊ธฐ)
with gzip.open(file_path, 'wb') as f:
pickle.dump(save_data, f)
logger.info(f"์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ {file_path}์— ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
return True
except Exception as e:
logger.error(f"์ž„๋ฒ ๋”ฉ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
return False
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
next_url = request.args.get('next')
logger.info(f"-------------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† (Next: {next_url}) --------------")
logger.info(f"Method: {request.method}")
if request.method == 'POST':
logger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐ›์Œ")
username = request.form.get('username', '')
password = request.form.get('password', '')
logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}")
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋น„๊ต
valid_username = ADMIN_USERNAME
valid_password = ADMIN_PASSWORD
logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
if username == valid_username and password == valid_password:
logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
# ์„ธ์…˜ ์„ค์ • ์ „ ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ ๋กœ๊น…
logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}")
# ์„ธ์…˜์— ๋กœ๊ทธ์ธ ์ •๋ณด ์ €์žฅ
session.permanent = True
session['logged_in'] = True
session['username'] = username
session.modified = True
logger.info(f"์„ธ์…˜ ์„ค์ • ํ›„: {session}")
logger.info("์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ, ๋ฆฌ๋””๋ ‰์…˜ ์‹œ๋„")
# ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋ฆฌ๋””๋ ‰์…˜
redirect_to = next_url or url_for('index')
logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
response = redirect(redirect_to)
return response
else:
logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
if username != valid_username: logger.warning("์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜")
if password != valid_password: logger.warning("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
else:
logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
if 'logged_in' in session:
logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
return redirect(url_for('index'))
logger.info("---------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ----------")
return render_template('login.html', error=error, next=next_url)
@app.route('/logout')
def logout():
logger.info("-------------- ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ --------------")
logger.info(f"๋กœ๊ทธ์•„์›ƒ ์ „ ์„ธ์…˜ ์ƒํƒœ: {session}")
if 'logged_in' in session:
username = session.get('username', 'unknown')
logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
session.pop('logged_in', None)
session.pop('username', None)
session.modified = True
logger.info(f"์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
else:
logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
response = redirect(url_for('login'))
return response
@app.route('/')
@login_required
def index():
"""๋ฉ”์ธ ํŽ˜์ด์ง€"""
nonlocal app_ready
# ์•ฑ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ - 30์ดˆ ์ด์ƒ ์ง€๋‚ฌ์œผ๋ฉด ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
current_time = datetime.now()
start_time = datetime.fromtimestamp(os.path.getmtime(__file__))
time_diff = (current_time - start_time).total_seconds()
if not app_ready and time_diff > 30:
logger.warning(f"์•ฑ์ด 30์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.")
app_ready = True
if not app_ready:
logger.info("์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ")
return render_template('loading.html'), 503 # ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ ์ƒํƒœ ์ฝ”๋“œ
logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
return render_template('index.html')
@app.route('/api/status')
@login_required
def app_status():
"""์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ API"""
logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if app_ready else 'Not Ready'}")
return jsonify({"ready": app_ready})
@app.route('/api/llm', methods=['GET', 'POST'])
@login_required
def llm_api():
"""์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
if not app_ready:
return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
if request.method == 'GET':
logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
try:
current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
supported_list = [{
"name": name, "id": id, "current": id == current_details.get("id")
} for name, id in supported_llms_dict.items()]
return jsonify({
"supported_llms": supported_list,
"current_llm": current_details
})
except Exception as e:
logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}")
return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
elif request.method == 'POST':
data = request.get_json()
if not data or 'llm_id' not in data:
return jsonify({"error": "LLM ID๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
llm_id = data['llm_id']
logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
try:
if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
raise NotImplementedError("LLM ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ ์—†์Œ")
if llm_id not in llm_interface.llm_clients:
return jsonify({"error": f"์ง€์›๋˜์ง€ ์•Š๋Š” LLM ID: {llm_id}"}), 400
success = llm_interface.set_llm(llm_id)
if success:
new_details = llm_interface.get_current_llm_details()
logger.info(f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
return jsonify({
"success": True,
"message": f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
"current_llm": new_details
})
else:
logger.error(f"LLM ๋ณ€๊ฒฝ ์‹คํŒจ (ID: {llm_id})")
return jsonify({"error": "LLM ๋ณ€๊ฒฝ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
except Exception as e:
logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
@app.route('/api/chat', methods=['POST'])
@login_required
def chat():
"""ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡ API"""
if not app_ready or retriever is None:
return jsonify({"error": "์•ฑ/๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
try:
data = request.get_json()
if not data or 'query' not in data:
return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
query = data['query']
logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
# RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
if not hasattr(retriever, 'search'):
raise NotImplementedError("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
search_results = retriever.search(query, top_k=5, first_stage_k=6)
# ์ปจํ…์ŠคํŠธ ์ค€๋น„
if not hasattr(DocumentProcessor, 'prepare_rag_context'):
raise NotImplementedError("DocumentProcessor์— prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
context = DocumentProcessor.prepare_rag_context(search_results, field="text")
if not context:
logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•จ.")
# LLM์— ์งˆ์˜
llm_id = data.get('llm_id', None)
if not hasattr(llm_interface, 'rag_generate'):
raise NotImplementedError("LLMInterface์— rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
if not context:
answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
else:
answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
# ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (CSV ID ์ถ”์ถœ ๋กœ์ง ํฌํ•จ)
sources = []
if search_results:
for result in search_results:
if not isinstance(result, dict):
logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
continue
if "source" in result:
source_info = {
"source": result.get("source", "Unknown"),
"score": result.get("rerank_score", result.get("score", 0))
}
# CSV ํŒŒ์ผ ํŠน์ • ์ฒ˜๋ฆฌ
if "text" in result and result.get("filetype") == "csv":
try:
text_lines = result["text"].strip().split('\n')
if text_lines:
first_line = text_lines[0].strip()
if ',' in first_line:
first_column = first_line.split(',')[0].strip()
source_info["id"] = first_column
logger.debug(f"CSV ์†Œ์Šค ID ์ถ”์ถœ: {first_column} from {source_info['source']}")
except Exception as e:
logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({result.get('source')}): {e}")
sources.append(source_info)
# ์ตœ์ข… ์‘๋‹ต
response_data = {
"answer": answer,
"sources": sources,
"llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
}
return jsonify(response_data)
except Exception as e:
logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500