jeongsoo commited on
Commit
27a9fab
ยท
1 Parent(s): 6575706
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: RAG AgenticServer Small
3
  emoji: ๐Ÿ”ฅ
4
- colorFrom: purple
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.29.0
8
  app_file: app.py
 
1
  ---
2
+ title: RAG AgenticServer
3
  emoji: ๐Ÿ”ฅ
4
+ colorFrom: pink
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 5.29.0
8
  app_file: app.py
app/app.py CHANGED
@@ -1,32 +1,919 @@
1
  """
2
- RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
3
  """
4
 
5
  import os
 
6
  import logging
7
- from flask import Flask
 
 
 
 
 
8
  from dotenv import load_dotenv
 
9
 
10
  # ๋กœ๊ฑฐ ์„ค์ •
11
  logging.basicConfig(
12
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
13
- level=logging.DEBUG
14
  )
15
  logger = logging.getLogger(__name__)
16
 
17
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
18
  load_dotenv()
19
 
20
- # ๋กœ๊น… ์‹œ์ž‘
21
- logger.info("===== RAG ์ฑ—๋ด‡ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ =====")
 
22
 
23
- # app_revised.py์˜ ๋‚ด์šฉ์„ ์ž„ํฌํŠธํ•ด์„œ ์‚ฌ์šฉ
24
- from app_revised import app, initialize_app
 
25
 
26
- # ์•ฑ์ด ์ด๋ฏธ initialize_app์—์„œ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Œ
 
 
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  if __name__ == '__main__':
29
  logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
 
 
30
  port = int(os.environ.get("PORT", 7860))
31
  logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
32
- app.run(debug=True, host='0.0.0.0', port=port)
 
 
 
 
1
  """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์„ธ์…˜ ์„ค์ • ์ˆ˜์ • ์ ์šฉ ๋ฐ ์ค‘๋ณต ๋ผ์šฐํŠธ ๋“ฑ๋ก ๋ฐฉ์ง€)
3
  """
4
 
5
  import os
6
+ import json
7
  import logging
8
+ import tempfile
9
+ import threading
10
+ import datetime
11
+ from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
12
+ from flask_cors import CORS
13
+ from werkzeug.utils import secure_filename
14
  from dotenv import load_dotenv
15
+ from functools import wraps
16
 
17
  # ๋กœ๊ฑฐ ์„ค์ •
18
  logging.basicConfig(
19
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
20
+ level=logging.DEBUG # INFO์—์„œ DEBUG๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋” ์ƒ์„ธํ•œ ๋กœ๊ทธ ํ™•์ธ
21
  )
22
  logger = logging.getLogger(__name__)
23
 
24
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
25
  load_dotenv()
26
 
27
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
28
+ ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
29
+ ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
30
 
31
+ # ์žฅ์น˜ ์„œ๋ฒ„ URL ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ถ”๊ฐ€
32
+ DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', 'http://localhost:5050')
33
+ logger.info(f"์žฅ์น˜ ์„œ๋ฒ„ URL: {DEVICE_SERVER_URL}")
34
 
35
+ logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
36
+ logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
37
+ # ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๊ธฐ๋ก (๋ณด์•ˆ)
38
+ logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
39
 
40
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ถŒ์žฅ)
41
+ if not ADMIN_USERNAME:
42
+ ADMIN_USERNAME = 'admin'
43
+ logger.warning("ADMIN_USERNAME ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'admin'์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
44
+
45
+ if not ADMIN_PASSWORD:
46
+ ADMIN_PASSWORD = 'rag12345'
47
+ logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
48
+ class MockComponent: pass
49
+
50
+ # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
51
+ # ์‹ค์ œ ๊ฒฝ๋กœ์— ๋งž๊ฒŒ utils, retrieval ํด๋”๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
52
+ try:
53
+ from utils.vito_stt import VitoSTT
54
+ from utils.llm_interface import LLMInterface
55
+ from utils.document_processor import DocumentProcessor
56
+ from retrieval.vector_retriever import VectorRetriever
57
+ from retrieval.reranker import ReRanker
58
+ # ์žฅ์น˜ ๋ผ์šฐํŠธ ๋“ฑ๋ก ํ•จ์ˆ˜ ์ž„ํฌํŠธ
59
+ from app.app_device_routes import register_device_routes
60
+ except ImportError as e:
61
+ logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils, retrieval, app ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
62
+ # ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ž„์‹œ ํด๋ž˜์Šค/ํ•จ์ˆ˜ ์ •์˜ (์‹ค์ œ ์‚ฌ์šฉ ์‹œ ์ œ๊ฑฐ)
63
+
64
+ VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
65
+ def register_device_routes(*args, **kwargs):
66
+ logger.warning("Mock register_device_routes ํ•จ์ˆ˜ ํ˜ธ์ถœ๋จ.")
67
+ pass
68
+ # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
69
+
70
+
71
+ # Flask ์•ฑ ์ดˆ๊ธฐํ™”
72
+ app = Flask(__name__)
73
+
74
+ # CORS ์„ค์ • - ๋ชจ๋“  ๋„๋ฉ”์ธ์—์„œ์˜ ์š”์ฒญ ํ—ˆ์šฉ
75
+ CORS(app, supports_credentials=True)
76
+
77
+ # ์„ธ์…˜ ์„ค์ • - ๊ณ ์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค ์‚ฌ์šฉ (์‹ค์ œ ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ ๊ถŒ์žฅ)
78
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345') # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์šฐ์„  ์‚ฌ์šฉ
79
+
80
+ # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ์ˆ˜์ • (ํ—ˆ๊น…ํŽ˜์ด์Šค ํ™˜๊ฒฝ ๊ณ ๋ ค) ---
81
+ # ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ HTTPS๋กœ ์„œ๋น„์Šค๋˜๋ฏ€๋กœ Secure=True ์„ค์ •
82
+ app.config['SESSION_COOKIE_SECURE'] = True
83
+ app.config['SESSION_COOKIE_HTTPONLY'] = True # JavaScript์—์„œ ์ฟ ํ‚ค ์ ‘๊ทผ ๋ฐฉ์ง€ (๋ณด์•ˆ ๊ฐ•ํ™”)
84
+ # SameSite='Lax'๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ๋” ์•ˆ์ „ํ•˜๊ณ  ํ˜ธํ™˜์„ฑ์ด ์ข‹์Œ.
85
+ # ๋งŒ์•ฝ ์•ฑ์ด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ iframe ๋‚ด์—์„œ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด 'None'์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ.
86
+ # (๋‹จ, 'None'์œผ๋กœ ์„ค์ • ์‹œ ๋ฐ˜๋“œ์‹œ Secure=True์—ฌ์•ผ ํ•จ)
87
+ # ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ iframe ํ™˜๊ฒฝ์œผ๋กœ ํ™•์ธ๋˜์–ด 'None'์œผ๋กœ ๋ณ€๊ฒฝ
88
+ app.config['SESSION_COOKIE_SAMESITE'] = 'None' # <--- ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
89
+ app.config['SESSION_COOKIE_DOMAIN'] = None # ํŠน์ • ๋„๋ฉ”์ธ ์ œํ•œ ์—†์Œ
90
+ app.config['SESSION_COOKIE_PATH'] = '/' # ์•ฑ ์ „์ฒด ๊ฒฝ๋กœ์— ์ฟ ํ‚ค ์ ์šฉ
91
+ app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1) # ์„ธ์…˜ ์œ ํšจ ์‹œ๊ฐ„ ์ฆ๊ฐ€
92
+ # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
93
+
94
+ # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
95
+ app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
96
+ # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ ์„ค์ •
97
+ APP_ROOT = os.path.dirname(os.path.abspath(__file__))
98
+ app.config['UPLOAD_FOLDER'] = os.path.join(APP_ROOT, 'uploads')
99
+ app.config['DATA_FOLDER'] = os.path.join(APP_ROOT, '..', 'data')
100
+ app.config['INDEX_PATH'] = os.path.join(APP_ROOT, '..', 'data', 'index')
101
+
102
+ # ํ•„์š”ํ•œ ํด๋” ์ƒ์„ฑ
103
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
104
+ os.makedirs(app.config['DATA_FOLDER'], exist_ok=True)
105
+ os.makedirs(app.config['INDEX_PATH'], exist_ok=True)
106
+
107
+ # ํ—ˆ์šฉ๋˜๋Š” ์˜ค๋””์˜ค/๋ฌธ์„œ ํŒŒ์ผ ํ™•์žฅ์ž
108
+ ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
109
+ ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
110
+
111
+ # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ---
112
+ try:
113
+ llm_interface = LLMInterface(default_llm="openai")
114
+ stt_client = VitoSTT()
115
+ except NameError:
116
+ logger.warning("LLM ๋˜๋Š” STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์‹คํŒจ. Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
117
+ llm_interface = MockComponent()
118
+ stt_client = MockComponent()
119
+
120
+ base_retriever = None
121
+ retriever = None
122
+ app_ready = False # ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
123
+ DEVICE_ROUTES_REGISTERED = False # ์žฅ์น˜ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
124
+ # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
125
+
126
+
127
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ (์ˆ˜์ •๋จ) ---
128
+ def login_required(f):
129
+ @wraps(f)
130
+ def decorated_function(*args, **kwargs):
131
+ logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
132
+ logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
133
+ logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
134
+ # ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณด๋‚ธ ์‹ค์ œ ์ฟ ํ‚ค ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
135
+ logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
136
+
137
+ # API ์š”์ฒญ์ด๊ณ  ํด๋ผ์ด์–ธํŠธ์—์„œ ์˜ค๋Š” ๊ฒฝ์šฐ ์ธ์ฆ ๋ฌด์‹œ (์ž„์‹œ ์กฐ์น˜)
138
+ # ---> ์ฃผ์˜: ์ด ๋ถ€๋ถ„์€ ๋ณด์•ˆ ๊ฒ€ํ†  ํ›„ ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ๋” ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
139
+ if request.path.startswith('/api/device/'):
140
+ logger.info(f"์žฅ์น˜ API ์š”์ฒญ: {request.path} - ์ธ์ฆ ์ œ์™ธ (์ฃผ์˜: ์ž„์‹œ ์กฐ์น˜)")
141
+ return f(*args, **kwargs)
142
+
143
+ # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
144
+ if 'logged_in' not in session:
145
+ logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
146
+ # ์ˆ˜๋™ ์ฟ ํ‚ค ํ™•์ธ ๋กœ์ง ์ œ๊ฑฐ๋จ
147
+ return redirect(url_for('login', next=request.url)) # ๋กœ๊ทธ์ธ ํ›„ ์›๋ž˜ ํŽ˜์ด์ง€๋กœ ๋Œ์•„๊ฐ€๋„๋ก next ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
148
+
149
+ logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
150
+ return f(*args, **kwargs)
151
+ return decorated_function
152
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
153
+
154
+ # --- ์˜ค๋ฅ˜ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€ ---
155
+ @app.errorhandler(404)
156
+ def not_found(e):
157
+ # ํด๋ผ์ด์–ธํŠธ๊ฐ€ JSON์„ ๊ธฐ๋Œ€ํ•˜๋Š” API ํ˜ธ์ถœ์ธ ๊ฒฝ์šฐ JSON ์‘๋‹ต
158
+ if request.path.startswith('/api/'):
159
+ return jsonify({"success": False, "error": "์š”์ฒญํ•œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}), 404
160
+ # ์ผ๋ฐ˜ ์›น ํŽ˜์ด์ง€ ์š”์ฒญ์ธ ๊ฒฝ์šฐ HTML ์‘๋‹ต
161
+ return "ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", 404
162
+
163
+ @app.errorhandler(500)
164
+ def internal_error(e):
165
+ # ํด๋ผ์ด์–ธํŠธ๊ฐ€ JSON์„ ๊ธฐ๋Œ€ํ•˜๋Š” API ํ˜ธ์ถœ์ธ ๊ฒฝ์šฐ JSON ์‘๋‹ต
166
+ if request.path.startswith('/api/'):
167
+ return jsonify({"success": False, "error": "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}), 500
168
+ # ์ผ๋ฐ˜ ์›น ํŽ˜์ด์ง€ ์š”์ฒญ์ธ ๊ฒฝ์šฐ HTML ์‘๋‹ต
169
+ return "์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", 500
170
+ # --- ์˜ค๋ฅ˜ ํ•ธ๋“ค๋Ÿฌ ๋ ---
171
+
172
+ # --- ์žฅ์น˜ ๊ด€๋ จ ๋ผ์šฐํŠธ ๋“ฑ๋ก (์ˆ˜์ •๋จ: ์ค‘๋ณต ๋ฐฉ์ง€) ---
173
+ # ์ „์—ญ ํ”Œ๋ž˜๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•œ ๋ฒˆ๋งŒ ๋“ฑ๋ก๋˜๋„๋ก ํ•จ
174
+ if not DEVICE_ROUTES_REGISTERED:
175
+ try:
176
+ # ์ž„ํฌํŠธ๋œ register_device_routes ํ•จ์ˆ˜ ์‚ฌ์šฉ
177
+ # ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ(login_required)์™€ ์„œ๋ฒ„ URL ์ „๋‹ฌ
178
+ register_device_routes(app, login_required, DEVICE_SERVER_URL)
179
+ DEVICE_ROUTES_REGISTERED = True # ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ ํ”Œ๋ž˜๊ทธ ์„ค์ •
180
+ logger.info("์žฅ์น˜ ๊ด€๋ จ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์™„๋ฃŒ")
181
+ except NameError:
182
+ logger.error("register_device_routes ํ•จ์ˆ˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. app.app_device_routes ๋ชจ๋“ˆ ํ™•์ธ ํ•„์š”.")
183
+ except Exception as e:
184
+ logger.error(f"์žฅ์น˜ ๊ด€๋ จ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์‹คํŒจ: {e}", exc_info=True)
185
+ else:
186
+ logger.info("์žฅ์น˜ ๊ด€๋ จ ๋ผ์šฐํŠธ๊ฐ€ ์ด๋ฏธ ๋“ฑ๋ก๋˜์–ด ์žˆ์–ด ๊ฑด๋„ˆ<0xEB>๋œ๋‹ˆ๋‹ค.")
187
+ # --- ์žฅ์น˜ ๊ด€๋ จ ๋ผ์šฐํŠธ ๋“ฑ๋ก ๋ ---
188
+
189
+
190
+ # --- ํ—ฌํผ ํ•จ์ˆ˜ ---
191
+ def allowed_audio_file(filename):
192
+ """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
193
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
194
+
195
+ def allowed_doc_file(filename):
196
+ """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ๋ฌธ์„œ ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
197
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
198
+ # --- ํ—ฌํผ ํ•จ์ˆ˜ ๋ ---
199
+
200
+
201
+ # init_retriever ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
202
+ # --- ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๊ด€๋ จ ํ•จ์ˆ˜ ---
203
+ def init_retriever():
204
+ """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
205
+ global base_retriever, retriever
206
+
207
+ index_path = app.config['INDEX_PATH']
208
+ data_path = app.config['DATA_FOLDER'] # data_path ์ •์˜ ํ™•์ธ
209
+ logger.info("--- init_retriever ์‹œ์ž‘ ---")
210
+
211
+ # 1. ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
212
+ if os.path.exists(os.path.join(index_path, "documents.json")):
213
+ try:
214
+ logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹œ๋„: {index_path}")
215
+ base_retriever = VectorRetriever.load(index_path)
216
+ logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์„ฑ๊ณต. ๋ฌธ์„œ {len(getattr(base_retriever, 'documents', []))}๊ฐœ")
217
+ except Exception as e:
218
+ logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
219
+ logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
220
+ try:
221
+ base_retriever = VectorRetriever()
222
+ logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
223
+ except Exception as e_init:
224
+ logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
225
+ base_retriever = None
226
+ else:
227
+ logger.info("์ธ๋ฑ์Šค ํŒŒ์ผ ์—†์Œ. ์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
228
+ try:
229
+ base_retriever = VectorRetriever()
230
+ logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
231
+ except Exception as e_init:
232
+ logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
233
+ base_retriever = None
234
+
235
+ if base_retriever is None:
236
+ logger.error("base_retriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ์— ์‹คํŒจํ•˜์—ฌ init_retriever ์ค‘๋‹จ.")
237
+ return None
238
+
239
+ # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
240
+ needs_loading = (not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', None)) # None ์ฒดํฌ ์ถ”๊ฐ€
241
+ if needs_loading and os.path.exists(data_path):
242
+ logger.info(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์–ด {data_path}์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์‹œ๋„...")
243
+ try:
244
+ docs = DocumentProcessor.load_documents_from_directory(
245
+ directory=data_path,
246
+ extensions=[".txt", ".md", ".csv"],
247
+ recursive=True
248
+ )
249
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ๋กœ๋“œ ์„ฑ๊ณต.")
250
+ if docs and hasattr(base_retriever, 'add_documents'):
251
+ logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ์‹œ๋„...")
252
+ base_retriever.add_documents(docs)
253
+ logger.info("๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ.")
254
+
255
+ if hasattr(base_retriever, 'save'):
256
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ ์ €์žฅ ์‹œ๋„: {index_path}")
257
+ try:
258
+ base_retriever.save(index_path)
259
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ.")
260
+ except Exception as e_save:
261
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์‹คํŒจ: {e_save}", exc_info=True)
262
+ except Exception as e_load_add:
263
+ logger.error(f"DATA_FOLDER ๋ฌธ์„œ ๋กœ๋“œ/์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜: {e_load_add}", exc_info=True)
264
+
265
+ # 3. ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
266
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
267
+ try:
268
+ def custom_rerank_fn(query, results):
269
+ query_terms = set(query.lower().split())
270
+ for result in results:
271
+ if isinstance(result, dict) and "text" in result:
272
+ text = result["text"].lower()
273
+ term_freq = sum(1 for term in query_terms if term in text)
274
+ normalized_score = term_freq / (len(text.split()) + 1) * 10
275
+ result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
276
+ elif isinstance(result, dict):
277
+ result["rerank_score"] = result.get("score", 0)
278
+ results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
279
+ return results
280
+
281
+ # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
282
+ retriever = ReRanker(
283
+ base_retriever=base_retriever,
284
+ rerank_fn=custom_rerank_fn,
285
+ rerank_field="text"
286
+ )
287
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
288
+ except Exception as e_rerank:
289
+ logger.error(f"์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_rerank}", exc_info=True)
290
+ logger.warning("์žฌ์ˆœ์œ„ํ™” ์‹คํŒจ, ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ retriever๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
291
+ retriever = base_retriever # fallback
292
+
293
+ logger.info("--- init_retriever ์ข…๋ฃŒ ---")
294
+ return retriever
295
+
296
+ def background_init():
297
+ """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
298
+ global app_ready, retriever, base_retriever, llm_interface, stt_client
299
+
300
+ temp_app_ready = False # ์ž„์‹œ ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
301
+ try:
302
+ logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘...")
303
+
304
+ # 1. LLM, STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” (ํ•„์š” ์‹œ)
305
+ if llm_interface is None or isinstance(llm_interface, MockComponent):
306
+ if 'LLMInterface' in globals() and LLMInterface != MockComponent:
307
+ llm_interface = LLMInterface(default_llm="openai")
308
+ logger.info("LLM ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
309
+ else:
310
+ logger.warning("LLMInterface ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
311
+ llm_interface = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
312
+ if stt_client is None or isinstance(stt_client, MockComponent):
313
+ if 'VitoSTT' in globals() and VitoSTT != MockComponent:
314
+ stt_client = VitoSTT()
315
+ logger.info("STT ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
316
+ else:
317
+ logger.warning("VitoSTT ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
318
+ stt_client = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
319
+
320
+
321
+ # 2. ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
322
+ if 'VectorRetriever' in globals() and VectorRetriever != MockComponent:
323
+ logger.info("์‹ค์ œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
324
+ retriever = init_retriever()
325
+ if hasattr(retriever, 'base_retriever') and base_retriever is None:
326
+ base_retriever = retriever.base_retriever
327
+ elif base_retriever is None:
328
+ logger.warning("init_retriever ํ›„ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ. ํ™•์ธ ํ•„์š”.")
329
+ if isinstance(retriever, VectorRetriever):
330
+ base_retriever = retriever
331
+
332
+ if retriever is not None and base_retriever is not None:
333
+ logger.info("๊ฒ€์ƒ‰๊ธฐ (Retriever, Base Retriever) ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
334
+ temp_app_ready = True
335
+ else:
336
+ logger.error("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ํ›„์—๋„ retriever ๋˜๋Š” base_retriever๊ฐ€ None์ž…๋‹ˆ๋‹ค.")
337
+ if base_retriever is None: base_retriever = MockComponent()
338
+ if retriever is None: retriever = MockComponent()
339
+ if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
340
+ if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
341
+ temp_app_ready = True
342
+
343
+ else:
344
+ logger.warning("VectorRetriever ํด๋ž˜์Šค ์—†์Œ. Mock ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ.")
345
+ base_retriever = MockComponent()
346
+ retriever = MockComponent()
347
+ if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
348
+ if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
349
+ temp_app_ready = True
350
+
351
+ logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ. ์ตœ์ข… ์ƒํƒœ: {'Ready' if temp_app_ready else 'Not Ready (Error during init)'}")
352
+
353
+ except Exception as e:
354
+ logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
355
+ if base_retriever is None: base_retriever = MockComponent()
356
+ if retriever is None: retriever = MockComponent()
357
+ if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
358
+ if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
359
+ temp_app_ready = True
360
+ logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์ง€๋งŒ Mock ๊ฐ์ฒด๋กœ ๋Œ€์ฒด ํ›„ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ •.")
361
+
362
+ finally:
363
+ # ์ตœ์ข…์ ์œผ๋กœ app_ready ์ƒํƒœ ์—…๋ฐ์ดํŠธ
364
+ app_ready = temp_app_ready
365
+ # ์žฅ์น˜ ๋ผ์šฐํŠธ ๋“ฑ๋ก ํ˜ธ์ถœ์€ ์—ฌ๊ธฐ์„œ ์ œ๊ฑฐ๋จ (๋ฉ”์ธ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ)
366
+
367
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
368
+ init_thread = threading.Thread(target=background_init)
369
+ init_thread.daemon = True
370
+ init_thread.start()
371
+
372
+
373
+ # --- Flask ๋ผ์šฐํŠธ ์ •์˜ ---
374
+
375
+ @app.route('/login', methods=['GET', 'POST'])
376
+ def login():
377
+ error = None
378
+ next_url = request.args.get('next') # ๋ฆฌ๋””๋ ‰์…˜ํ•  URL ๊ฐ€์ ธ์˜ค๊ธฐ
379
+ logger.info(f"-------------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† (Next: {next_url}) --------------")
380
+ logger.info(f"Method: {request.method}")
381
+
382
+ if request.method == 'POST':
383
+ logger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐ›์Œ")
384
+ username = request.form.get('username', '')
385
+ password = request.form.get('password', '')
386
+ logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
387
+ logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}")
388
+
389
+ valid_username = ADMIN_USERNAME
390
+ valid_password = ADMIN_PASSWORD
391
+ logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
392
+ logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
393
+
394
+
395
+ if username == valid_username and password == valid_password:
396
+ logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
397
+ logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}")
398
+
399
+ session.permanent = True
400
+ session['logged_in'] = True
401
+ session['username'] = username
402
+ session.modified = True
403
+
404
+ logger.info(f"์„ธ์…˜ ์„ค์ • ํ›„: {session}")
405
+ logger.info("์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ, ๋ฆฌ๋””๋ ‰์…˜ ์‹œ๋„")
406
+
407
+ redirect_to = next_url or url_for('index')
408
+ logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
409
+ response = redirect(redirect_to)
410
+ return response
411
+ else:
412
+ logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
413
+ if username != valid_username: logger.warning("์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜")
414
+ if password != valid_password: logger.warning("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
415
+ error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
416
+ else:
417
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
418
+ if 'logged_in' in session:
419
+ logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
420
+ return redirect(url_for('index'))
421
+
422
+ logger.info("---------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ----------")
423
+ return render_template('login.html', error=error, next=next_url)
424
+
425
+
426
+ @app.route('/logout')
427
+ def logout():
428
+ logger.info("-------------- ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ --------------")
429
+ logger.info(f"๋กœ๊ทธ์•„์›ƒ ์ „ ์„ธ์…˜ ์ƒํƒœ: {session}")
430
+
431
+ if 'logged_in' in session:
432
+ username = session.get('username', 'unknown')
433
+ logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
434
+ session.pop('logged_in', None)
435
+ session.pop('username', None)
436
+ session.modified = True
437
+ logger.info(f"์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
438
+ else:
439
+ logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
440
+
441
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
442
+ response = redirect(url_for('login'))
443
+ return response
444
+
445
+
446
+ @app.route('/')
447
+ @login_required
448
+ def index():
449
+ """๋ฉ”์ธ ํŽ˜์ด์ง€"""
450
+ global app_ready
451
+
452
+ current_time = datetime.datetime.now()
453
+ try:
454
+ start_time = datetime.datetime.fromtimestamp(os.path.getmtime(__file__))
455
+ time_diff = (current_time - start_time).total_seconds()
456
+ if not app_ready and time_diff > 30:
457
+ logger.warning(f"์•ฑ์ด 30์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.")
458
+ app_ready = True
459
+ except FileNotFoundError:
460
+ logger.warning("__file__ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด ์‹œ๊ฐ„ ๋น„๊ต ๋กœ์ง์„ ๊ฑด๋„ˆ<0xEB>๋œ๋‹ˆ๋‹ค.")
461
+ if not app_ready: # ๊ธฐ๋ณธ ํƒ€์ž„์•„์›ƒ ๋Œ€์‹  ๊ฐ„๋‹จํ•œ ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
462
+ logger.warning("์•ฑ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ (์‹œ๊ฐ„ ๋น„๊ต ๋ถˆ๊ฐ€)")
463
+ # ํ•„์š”์‹œ ๋‹ค๋ฅธ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ ๋กœ์ง ์ถ”๊ฐ€
464
+ pass # ์ž„์‹œ๋กœ ํ†ต๊ณผ
465
+
466
+ if not app_ready:
467
+ logger.info("์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ")
468
+ return render_template('loading.html'), 503
469
+
470
+ logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
471
+ return render_template('index.html')
472
+
473
+
474
+ @app.route('/api/status')
475
+ @login_required
476
+ def app_status():
477
+ """์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ API"""
478
+ logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if app_ready else 'Not Ready'}")
479
+ return jsonify({"ready": app_ready})
480
+
481
+
482
+ @app.route('/api/llm', methods=['GET', 'POST'])
483
+ @login_required
484
+ def llm_api():
485
+ """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
486
+ global llm_interface
487
+
488
+ if not app_ready:
489
+ return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
490
+
491
+ if request.method == 'GET':
492
+ logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
493
+ try:
494
+ current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
495
+ supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
496
+ supported_list = [{
497
+ "name": name, "id": id, "current": id == current_details.get("id")
498
+ } for name, id in supported_llms_dict.items()]
499
+
500
+ return jsonify({
501
+ "supported_llms": supported_list,
502
+ "current_llm": current_details
503
+ })
504
+ except Exception as e:
505
+ logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}")
506
+ return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
507
+
508
+ elif request.method == 'POST':
509
+ data = request.get_json()
510
+ if not data or 'llm_id' not in data:
511
+ return jsonify({"error": "LLM ID๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
512
+
513
+ llm_id = data['llm_id']
514
+ logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
515
+
516
+ try:
517
+ if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
518
+ raise NotImplementedError("LLM ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ ์—†์Œ")
519
+
520
+ if llm_id not in llm_interface.llm_clients:
521
+ return jsonify({"error": f"์ง€์›๋˜์ง€ ์•Š๋Š” LLM ID: {llm_id}"}), 400
522
+
523
+ success = llm_interface.set_llm(llm_id)
524
+ if success:
525
+ new_details = llm_interface.get_current_llm_details()
526
+ logger.info(f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
527
+ return jsonify({
528
+ "success": True,
529
+ "message": f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
530
+ "current_llm": new_details
531
+ })
532
+ else:
533
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์‹คํŒจ (ID: {llm_id})")
534
+ return jsonify({"error": "LLM ๋ณ€๊ฒฝ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
535
+ except Exception as e:
536
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
537
+ return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
538
+
539
+
540
+ @app.route('/api/chat', methods=['POST'])
541
+ @login_required
542
+ def chat():
543
+ """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡ API"""
544
+ global retriever
545
+
546
+ if not app_ready or retriever is None:
547
+ return jsonify({"error": "์•ฑ/๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
548
+
549
+ try:
550
+ data = request.get_json()
551
+ if not data or 'query' not in data:
552
+ return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
553
+
554
+ query = data['query']
555
+ logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
556
+
557
+ if not hasattr(retriever, 'search'):
558
+ raise NotImplementedError("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
559
+ search_results = retriever.search(query, top_k=5, first_stage_k=6)
560
+
561
+ if not hasattr(DocumentProcessor, 'prepare_rag_context'):
562
+ raise NotImplementedError("DocumentProcessor์— prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
563
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
564
+
565
+ if not context:
566
+ logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•จ.")
567
+ pass
568
+
569
+ llm_id = data.get('llm_id', None)
570
+ if not hasattr(llm_interface, 'rag_generate'):
571
+ raise NotImplementedError("LLMInterface์— rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
572
+
573
+ if not context:
574
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
575
+ logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
576
+ else:
577
+ answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
578
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
579
+
580
+
581
+ sources = []
582
+ if search_results:
583
+ for result in search_results:
584
+ if not isinstance(result, dict):
585
+ logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
586
+ continue
587
+
588
+ if "source" in result:
589
+ source_info = {
590
+ "source": result.get("source", "Unknown"),
591
+ "score": result.get("rerank_score", result.get("score", 0))
592
+ }
593
+
594
+ if "text" in result and result.get("filetype") == "csv":
595
+ try:
596
+ text_lines = result["text"].strip().split('\n')
597
+ if text_lines:
598
+ first_line = text_lines[0].strip()
599
+ if ',' in first_line:
600
+ first_column = first_line.split(',')[0].strip()
601
+ source_info["id"] = first_column
602
+ logger.debug(f"CSV ์†Œ์Šค ID ์ถ”์ถœ: {first_column} from {source_info['source']}")
603
+ except Exception as e:
604
+ logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({result.get('source')}): {e}")
605
+
606
+ sources.append(source_info)
607
+
608
+ response_data = {
609
+ "answer": answer,
610
+ "sources": sources,
611
+ "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
612
+ }
613
+ return jsonify(response_data)
614
+
615
+ except Exception as e:
616
+ logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
617
+ return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
618
+
619
+
620
+ @app.route('/api/voice', methods=['POST'])
621
+ @login_required
622
+ def voice_chat():
623
+ """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
624
+ global retriever, stt_client
625
+
626
+ if not app_ready:
627
+ logger.warning("์•ฑ ์ดˆ๊ธฐํ™”๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์ง€๋งŒ ์Œ์„ฑ API ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๋„")
628
+
629
+ if retriever is None:
630
+ logger.error("retriever๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
631
+ return jsonify({
632
+ "transcription": "(์Œ์„ฑ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ–ˆ์ง€๋งŒ ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค)",
633
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
634
+ "sources": []
635
+ })
636
+ if stt_client is None:
637
+ return jsonify({
638
+ "transcription": "(์Œ์„ฑ ์ธ์‹ ๊ธฐ๋Šฅ์ด ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค)",
639
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์Œ์„ฑ ์ธ์‹ ์„œ๋น„์Šค๊ฐ€ ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
640
+ "sources": []
641
+ })
642
+
643
+ logger.info("์Œ์„ฑ ์ฑ— ์š”์ฒญ ์ˆ˜์‹ ")
644
+
645
+ if 'audio' not in request.files:
646
+ logger.error("์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์Œ")
647
+ return jsonify({"error": "์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
648
+
649
+ audio_file = request.files['audio']
650
+ logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
651
+
652
+ try:
653
+ with tempfile.NamedTemporaryFile(delete=True) as temp_audio:
654
+ audio_file.save(temp_audio.name)
655
+ logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
656
+
657
+ if not hasattr(stt_client, 'transcribe_audio'):
658
+ raise NotImplementedError("STT ํด๋ผ์ด์–ธํŠธ์— transcribe_audio ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
659
+
660
+ with open(temp_audio.name, 'rb') as f_bytes:
661
+ audio_bytes = f_bytes.read()
662
+ stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
663
+
664
+
665
+ if not isinstance(stt_result, dict) or not stt_result.get("success"):
666
+ error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
667
+ logger.error(f"์Œ์„ฑ์ธ์‹ ์‹คํŒจ: {error_msg}")
668
+ return jsonify({
669
+ "error": "์Œ์„ฑ์ธ์‹ ์‹คํŒจ",
670
+ "details": error_msg
671
+ }), 500
672
+
673
+ transcription = stt_result.get("text", "")
674
+ if not transcription:
675
+ logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
676
+ return jsonify({"error": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.", "transcription": ""}), 400
677
+
678
+ logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
679
+ if retriever is None:
680
+ logger.error("STT ์„ฑ๊ณต ํ›„ ๊ฒ€์ƒ‰ ์‹œ๋„ ์ค‘ retriever๊ฐ€ None์ž„")
681
+ return jsonify({
682
+ "transcription": transcription,
683
+ "answer": "์Œ์„ฑ์„ ์ธ์‹ํ–ˆ์ง€๋งŒ, ํ˜„์žฌ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์ด ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
684
+ "sources": []
685
+ })
686
+
687
+ search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
688
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
689
+
690
+ if not context:
691
+ logger.warning("์Œ์„ฑ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
692
+ pass
693
+
694
+ llm_id = request.form.get('llm_id', None)
695
+ if not context:
696
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
697
+ logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
698
+ else:
699
+ answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
700
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
701
+
702
+ enhanced_sources = []
703
+ if search_results:
704
+ for doc in search_results:
705
+ if not isinstance(doc, dict): continue
706
+ if "source" in doc:
707
+ source_info = {
708
+ "source": doc.get("source", "Unknown"),
709
+ "score": doc.get("rerank_score", doc.get("score", 0))
710
+ }
711
+ if "text" in doc and doc.get("filetype") == "csv":
712
+ try:
713
+ text_lines = doc["text"].strip().split('\n')
714
+ if text_lines:
715
+ first_line = text_lines[0].strip()
716
+ if ',' in first_line:
717
+ first_column = first_line.split(',')[0].strip()
718
+ source_info["id"] = first_column
719
+ except Exception as e:
720
+ logger.warning(f"[์Œ์„ฑ์ฑ—] CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({doc.get('source')}): {e}")
721
+ enhanced_sources.append(source_info)
722
+
723
+ response_data = {
724
+ "transcription": transcription,
725
+ "answer": answer,
726
+ "sources": enhanced_sources,
727
+ "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
728
+ }
729
+ return jsonify(response_data)
730
+
731
+ except Exception as e:
732
+ logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
733
+ return jsonify({
734
+ "error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ",
735
+ "details": str(e)
736
+ }), 500
737
+
738
+
739
+ @app.route('/api/upload', methods=['POST'])
740
+ @login_required
741
+ def upload_document():
742
+ """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ์—…๋กœ๋“œ API"""
743
+ global base_retriever, retriever
744
+
745
+ if not app_ready or base_retriever is None:
746
+ return jsonify({"error": "์•ฑ/๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
747
+
748
+ if 'document' not in request.files:
749
+ return jsonify({"error": "๋ฌธ์„œ ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
750
+
751
+ doc_file = request.files['document']
752
+ if doc_file.filename == '':
753
+ return jsonify({"error": "์„ ํƒ๋œ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
754
+
755
+ if not allowed_doc_file(doc_file.filename):
756
+ logger.error(f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: {doc_file.filename}")
757
+ return jsonify({"error": f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
758
+
759
+ try:
760
+ filename = secure_filename(doc_file.filename)
761
+ filepath = os.path.join(app.config['DATA_FOLDER'], filename)
762
+ doc_file.save(filepath)
763
+ logger.info(f"๋ฌธ์„œ ์ €์žฅ ์™„๋ฃŒ: {filepath}")
764
+
765
+ try:
766
+ with open(filepath, 'r', encoding='utf-8') as f:
767
+ content = f.read()
768
+ except UnicodeDecodeError:
769
+ logger.info(f"UTF-8 ๋””์ฝ”๋”ฉ ์‹คํŒจ, CP949๋กœ ์‹œ๋„: {filename}")
770
+ try:
771
+ with open(filepath, 'r', encoding='cp949') as f:
772
+ content = f.read()
773
+ except Exception as e_cp949:
774
+ logger.error(f"CP949 ๋””์ฝ”๋”ฉ ์‹คํŒจ ({filename}): {e_cp949}")
775
+ return jsonify({"error": "ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์„ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (UTF-8, CP949 ์‹œ๋„ ์‹คํŒจ)."}), 400
776
+ except Exception as e_read:
777
+ logger.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ ({filename}): {e_read}")
778
+ return jsonify({"error": f"ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e_read)}"}), 500
779
+
780
+ metadata = {
781
+ "source": filename, "filename": filename,
782
+ "filetype": filename.rsplit('.', 1)[1].lower(),
783
+ "filepath": filepath
784
+ }
785
+ file_ext = metadata["filetype"]
786
+ docs = []
787
+
788
+ if not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
789
+ raise NotImplementedError("DocumentProcessor์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ ์—†์Œ")
790
+
791
+ if file_ext == 'csv':
792
+ logger.info(f"CSV ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
793
+ docs = DocumentProcessor.csv_to_documents(content, metadata)
794
+ else:
795
+ logger.info(f"์ผ๋ฐ˜ ํ…์ŠคํŠธ ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
796
+ if file_ext in ['pdf', 'docx']:
797
+ logger.warning(f".{file_ext} ํŒŒ์ผ ์ฒ˜๋ฆฌ๋Š” ํ˜„์žฌ ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ์ถ”์ถœ ๋กœ์ง ์ถ”๊ฐ€ ํ•„์š”.")
798
+ content = ""
799
+
800
+ if content:
801
+ docs = DocumentProcessor.text_to_documents(
802
+ content, metadata=metadata,
803
+ chunk_size=512, chunk_overlap=50
804
+ )
805
+
806
+ if docs:
807
+ if not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
808
+ raise NotImplementedError("๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ์— add_documents ๋˜๋Š” save ๋ฉ”์†Œ๋“œ ์—†์Œ")
809
+
810
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
811
+ base_retriever.add_documents(docs)
812
+
813
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
814
+ index_path = app.config['INDEX_PATH']
815
+ try:
816
+ base_retriever.save(index_path)
817
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
818
+ # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์—…๋ฐ์ดํŠธ ๋กœ์ง ํ•„์š” ์‹œ ์ถ”๊ฐ€
819
+ # ์˜ˆ: if retriever != base_retriever and hasattr(retriever, 'update_base_retriever'): retriever.update_base_retriever(base_retriever)
820
+ return jsonify({
821
+ "success": True,
822
+ "message": f"ํŒŒ์ผ '{filename}' ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ({len(docs)}๊ฐœ ์ฒญํฌ ์ถ”๊ฐ€)."
823
+ })
824
+ except Exception as e_save:
825
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e_save}")
826
+ return jsonify({"error": f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e_save)}"}), 500
827
+ else:
828
+ logger.warning(f"ํŒŒ์ผ '{filename}'์—์„œ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")
829
+ return jsonify({
830
+ "warning": True,
831
+ "message": f"ํŒŒ์ผ '{filename}'์ด ์ €์žฅ๋˜์—ˆ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."
832
+ })
833
+
834
+ except Exception as e:
835
+ logger.error(f"ํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
836
+ return jsonify({"error": f"ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
837
+
838
+
839
+ @app.route('/api/documents', methods=['GET'])
840
+ @login_required
841
+ def list_documents():
842
+ """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ๋ชฉ๋ก API"""
843
+ global base_retriever
844
+
845
+ if not app_ready or base_retriever is None:
846
+ return jsonify({"error": "์•ฑ/๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
847
+
848
+ try:
849
+ sources = {}
850
+ total_chunks = 0
851
+ if hasattr(base_retriever, 'documents') and base_retriever.documents:
852
+ logger.info(f"์ด {len(base_retriever.documents)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ์—์„œ ์†Œ์Šค ๋ชฉ๋ก ์ƒ์„ฑ ์ค‘...")
853
+ for doc in base_retriever.documents:
854
+ if not isinstance(doc, dict): continue
855
+
856
+ source = doc.get("source", "unknown")
857
+ if source == "unknown" and "metadata" in doc and isinstance(doc["metadata"], dict):
858
+ source = doc["metadata"].get("source", "unknown")
859
+
860
+ if source != "unknown":
861
+ if source in sources:
862
+ sources[source]["chunks"] += 1
863
+ else:
864
+ filename = doc.get("filename", source)
865
+ filetype = doc.get("filetype", "unknown")
866
+ if "metadata" in doc and isinstance(doc["metadata"], dict):
867
+ filename = doc["metadata"].get("filename", filename)
868
+ filetype = doc["metadata"].get("filetype", filetype)
869
+
870
+ sources[source] = {
871
+ "filename": filename,
872
+ "chunks": 1,
873
+ "filetype": filetype
874
+ }
875
+ total_chunks += 1
876
+ else:
877
+ logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ๊ฐ€ ์—†๊ฑฐ๋‚˜ documents ์†์„ฑ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
878
+
879
+ documents = [{"source": src, **info} for src, info in sources.items()]
880
+ documents.sort(key=lambda x: x["chunks"], reverse=True)
881
+
882
+ logger.info(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ: {len(documents)}๊ฐœ ์†Œ์Šค ํŒŒ์ผ, {total_chunks}๊ฐœ ์ฒญํฌ")
883
+ return jsonify({
884
+ "documents": documents,
885
+ "total_documents": len(documents),
886
+ "total_chunks": total_chunks
887
+ })
888
+
889
+ except Exception as e:
890
+ logger.error(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
891
+ return jsonify({"error": f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
892
+
893
+
894
+ # ์ •์  ํŒŒ์ผ ์„œ๋น™
895
+ @app.route('/static/<path:path>')
896
+ def send_static(path):
897
+ return send_from_directory('static', path)
898
+
899
+
900
+ # --- ์š”์ฒญ ์ฒ˜๋ฆฌ ํ›… ---
901
+
902
+ @app.after_request
903
+ def after_request_func(response):
904
+ """๋ชจ๋“  ์‘๋‹ต์— ๋Œ€ํ•ด ํ›„์ฒ˜๋ฆฌ ์ˆ˜ํ–‰"""
905
+ # logger.debug(f"[After Request] ์‘๋‹ต ํ—ค๋”: {response.headers}") # ๋””๋ฒ„๊น… ์‹œ Set-Cookie ํ™•์ธ
906
+ return response
907
+
908
+
909
+ # ์•ฑ ์‹คํ–‰ (๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ)
910
  if __name__ == '__main__':
911
  logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
912
+ # ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋Š” ๊ฐœ๋ฐœ ์ค‘์—๋งŒ True๋กœ ์„ค์ •ํ•˜๊ณ , ์‹ค์ œ ๋ฐฐํฌ ์‹œ์—๋Š” False๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
913
+ # host='0.0.0.0' ์€ ๋ชจ๋“  ๋„คํŠธ์›Œํฌ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ ‘์† ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
914
  port = int(os.environ.get("PORT", 7860))
915
  logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
916
+ # debug=True ์‚ฌ์šฉ ์‹œ werkzeug reloader๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์ฝ”๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ์„œ๋ฒ„๊ฐ€ ์žฌ์‹œ์ž‘๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ,
917
+ # ์ด ๊ณผ์ •์—์„œ ์ „์—ญ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
918
+ # DEVICE_ROUTES_REGISTERED ํ”Œ๋ž˜๊ทธ๊ฐ€ ์ด๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
919
+ app.run(debug=True, host='0.0.0.0', port=port)
app/app/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- """
2
- RAG ์ฑ—๋ด‡ ์•ฑ ํŒจํ‚ค์ง€
3
- """
 
 
 
 
app/{app/app_device_routes.py โ†’ app_device_routes.py} RENAMED
File without changes
app/{app/app_routes.py โ†’ app_routes.py} RENAMED
File without changes
app/docs/cleanup_plan.md DELETED
@@ -1,31 +0,0 @@
1
- # ํ”„๋กœ์ ํŠธ ์ •๋ฆฌ ๊ณ„ํš
2
-
3
- ## ์ œ๊ฑฐํ•  ํŒŒ์ผ๋“ค
4
- 1. `app.py` - `app_revised.py`๊ฐ€ ์ด ํŒŒ์ผ์˜ ๊ธฐ๋Šฅ์„ ๋Œ€์ฒดํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋ชจ๋“ˆํ™”๊ฐ€ ๋” ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
5
- 2. `app_part2.py` - ์ด ํŒŒ์ผ์˜ ๊ธฐ๋Šฅ์€ `init_retriever.py`๋กœ ์ด๋ฏธ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
6
- 3. `app_part3.py` - ์ด ํŒŒ์ผ์˜ ๋ผ์šฐํŠธ ํ•จ์ˆ˜๋“ค์€ `app_routes.py`์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
7
-
8
- ## ๊ตฌ์กฐ ๊ฐœ์„ 
9
- 1. `app_revised.py` - ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ
10
- - ์•ฑ ์ดˆ๊ธฐํ™” ๋ฐ ์‹คํ–‰
11
- - ๋ผ์šฐํŠธ ๋“ฑ๋ก ํ•จ์ˆ˜ ํ˜ธ์ถœ
12
-
13
- 2. `app_routes.py` - RAG ์ฑ—๋ด‡ ๊ธฐ๋ณธ ๋ผ์šฐํŠธ
14
- - ๋ชจ๋“  ๊ธฐ๋ณธ ๋ผ์šฐํŠธ (๋กœ๊ทธ์ธ, ๊ฒ€์ƒ‰, ๋ฌธ์„œ ๊ด€๋ฆฌ ๋“ฑ)
15
-
16
- 3. `app_device_routes.py` - ์žฅ์น˜ ๊ด€๋ฆฌ ๊ด€๋ จ ๋ผ์šฐํŠธ
17
- - LocalPCAgent ์—ฐ๊ฒฐ ๋ฐ ์ œ์–ด ๊ด€๋ จ API
18
-
19
- 4. `init_retriever.py` - ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
20
- - ๊ฒ€์ƒ‰๊ธฐ ๋กœ๋“œ, ์ดˆ๊ธฐํ™”, ์ž„๋ฒ ๋”ฉ ๊ด€๋ฆฌ
21
-
22
- ## ํ”„๋ŸฐํŠธ์—”๋“œ ํŒŒ์ผ ๊ฐœ์„  ๊ณ„ํš
23
- 1. `templates/index.html` - ๊ธฐ์กด ํŒŒ์ผ์— ์žฅ์น˜ ์ œ์–ด UI ์ถ”๊ฐ€
24
- 2. `static/js/device.js` - ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ์žฅ์น˜ ์ œ์–ด JavaScript ํŒŒ์ผ
25
- 3. `static/css/device.css` - ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ์žฅ์น˜ ์ œ์–ด ์Šคํƒ€์ผ ํŒŒ์ผ (ํ•„์š” ์‹œ)
26
-
27
- ## ์ •๋ฆฌ ์ˆœ์„œ
28
- 1. ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ ์ œ๊ฑฐ
29
- 2. ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํŒŒ์ผ ๊ฐ„ ์ฝ”๋“œ ํ†ตํ•ฉ
30
- 3. ์ƒˆ๋กœ์šด ํ”„๋ŸฐํŠธ์—”๋“œ ํŒŒ์ผ ์ถ”๊ฐ€
31
- 4. ์ „์ฒด ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/docs/cleanup_summary.md DELETED
@@ -1,44 +0,0 @@
1
- # ํ”„๋กœ์ ํŠธ ์ •๋ฆฌ ์š”์•ฝ
2
-
3
- ## ๊ฐœ์š”
4
- RAG ์ฑ—๋ด‡๊ณผ LocalPCAgent ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ์˜ ์ฝ”๋“œ ์ •๋ฆฌ ์ž‘์—…์„ ์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ์„ ์ œ๊ฑฐํ•˜์—ฌ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
5
-
6
- ## ์ˆ˜ํ–‰ํ•œ ์ž‘์—…
7
-
8
- ### 1. ์ฝ”๋“œ ๊ตฌ์กฐ ๋ชจ๋“ˆํ™”
9
- - `app/` ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ ๋ฐ ๋ชจ๋“ˆ ํŒŒ์ผ ์ด๋™:
10
- - `app_device_routes.py` โ†’ `app/app_device_routes.py`
11
- - `app_routes.py` โ†’ `app/app_routes.py`
12
- - `init_retriever.py` โ†’ `app/init_retriever.py`
13
- - `app/__init__.py` ์ถ”๊ฐ€ํ•˜์—ฌ ํŒจํ‚ค์ง€ํ™”
14
-
15
- ### 2. ํŒŒ์ผ ์ •๋ฆฌ
16
- - ์ค‘๋ณต ํŒŒ์ผ ๋ฐ ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ ์ •๋ฆฌ:
17
- - `app_part2.py` ๋ฐ `app_part3.py` ์ œ๊ฑฐ ์˜ˆ์ • (์ด๋ฏธ ๋‹ค๋ฅธ ํŒŒ์ผ๋กœ ํ†ตํ•ฉ๋จ)
18
- - `app.py` ํŒŒ์ผ์„ ๊ฐ„์†Œํ™”ํ•˜์—ฌ `app_revised.py`๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฆฌํŒฉํ† ๋ง
19
-
20
- ### 3. ํ”„๋ŸฐํŠธ์—”๋“œ ํ†ตํ•ฉ ํ™•์ธ
21
- - ์žฅ์น˜ ์ œ์–ด UI ๊ตฌํ˜„ ํ™•์ธ:
22
- - `templates/index.html`์— ์žฅ์น˜ ์ œ์–ด UI ์„น์…˜ ์ด๋ฏธ ๊ตฌํ˜„๋จ
23
- - `static/js/app-device.js`์— ์žฅ์น˜ ์ œ์–ด JavaScript ๋กœ์ง ๊ตฌํ˜„๋จ
24
- - `static/css/device-style.css`์— ์žฅ์น˜ ์ œ์–ด ์Šคํƒ€์ผ ๊ตฌํ˜„๋จ
25
-
26
- ### 4. ํ”„๋กœ์ ํŠธ ๋ฌธ์„œํ™”
27
- - `docs/project_plan.md` ์—…๋ฐ์ดํŠธ: ์ตœ์‹  ํ”„๋กœ์ ํŠธ ์ƒํƒœ ๋ฐ˜์˜
28
- - `docs/cleanup_plan.md` ์ž‘์„ฑ: ์ •๋ฆฌ ์ž‘์—… ๊ณ„ํš ๋ฌธ์„œํ™”
29
- - `docs/cleanup_summary.md` ์ž‘์„ฑ: ์ •๋ฆฌ ์ž‘์—… ๊ฒฐ๊ณผ ์š”์•ฝ
30
-
31
- ## ํ”„๋กœ์ ํŠธ ํ˜„์žฌ ์ƒํƒœ
32
- LocalPCAgent ์ œ์–ด ๊ธฐ๋Šฅ์ด RAG ์ฑ—๋ด‡ ์›น ์ธํ„ฐํŽ˜์ด์Šค์— ์„ฑ๊ณต์ ์œผ๋กœ ํ†ตํ•ฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. UI๋Š” ํƒญ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์‰ฝ๊ฒŒ ๋Œ€ํ™”, ๋ฌธ์„œ ๊ด€๋ฆฌ, ์žฅ์น˜ ์ œ์–ด ๊ธฐ๋Šฅ์„ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
33
-
34
- ์žฅ์น˜ ์ œ์–ด ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์ƒํ™ฉ:
35
- 1. **์„œ๋ฒ„ ์—ฐ๊ฒฐ**: ngrok URL ์ž…๋ ฅ ๋ฐ ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ
36
- 2. **์ƒํƒœ ํ™•์ธ**: ์žฅ์น˜ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ ๊ธฐ๋Šฅ
37
- 3. **ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ**: ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
38
- 4. **ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰**: ์„ ํƒํ•œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ๊ธฐ๋Šฅ
39
- 5. **์‚ฌ์šฉ์ž ์ •์˜ ์‹คํ–‰**: ์‚ฌ์šฉ์ž ์ง€์ • ๋ช…๋ น์–ด ์‹คํ–‰ ๊ธฐ๋Šฅ
40
-
41
- ## ๋‹ค์Œ ๋‹จ๊ณ„
42
- 1. **ํ…Œ์ŠคํŠธ**: ํ†ตํ•ฉ๋œ ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ ์ง„ํ–‰
43
- 2. **์ตœ์ ํ™”**: ํ•„์š” ์‹œ ์ถ”๊ฐ€ ์ฝ”๋“œ ์ตœ์ ํ™”
44
- 3. **๋ฐฐํฌ**: ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ์˜ ๋ฐฐํฌ ํ…Œ์ŠคํŠธ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/docs/project_plan.md CHANGED
@@ -1,75 +1,44 @@
1
  # RAG ์ฑ—๋ด‡ + LocalPCAgent ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ ๊ณ„ํš
2
 
3
  ## ํ”„๋กœ์ ํŠธ ๊ฐœ์š”
4
- ๊ธฐ์กด RAG ์ฑ—๋ด‡(Flask ๊ธฐ๋ฐ˜)์— LocalPCAgent ์ œ์–ด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ํ†ตํ•ฉ ์ž‘์—…์ž…๋‹ˆ๋‹ค. Gradio UI ์ฝ”๋“œ์˜ ๊ธฐ๋Šฅ์„ Flask+HTML/JS ๊ตฌ์กฐ์— ์ด์‹ํ•ฉ๋‹ˆ๋‹ค.
 
5
 
6
  ## ์™„๋ฃŒ๋œ ์ž‘์—…
7
- - [x] ๊ธฐ๋ณธ Flask ๊ธฐ๋ฐ˜ RAG ์ฑ—๋ด‡ ๋ฐฑ์—”๋“œ ๊ตฌํ˜„
8
- - [x] LocalPCAgent ์ œ์–ด๋ฅผ ์œ„ํ•œ API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ (`app_device_routes.py`)
9
- - [x] Flask ๋ฐฑ์—”๋“œ์™€ LocalPCAgent ๊ฐ„ ํ†ต์‹  ๋กœ์ง ๊ตฌํ˜„
10
- - [x] ์›น UI์— LocalPCAgent ์ œ์–ด ํ™”๋ฉด ์ถ”๊ฐ€ (HTML/CSS)
11
- - [x] ์›น UI์—์„œ LocalPCAgent ์ œ์–ด ๊ธฐ๋Šฅ์„ ์œ„ํ•œ JavaScript ๋กœ์ง ๊ตฌํ˜„
12
- - [x] ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ์ตœ์ ํ™”
13
- - ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ ๋ฐ ์ฝ”๋“œ ์ œ๊ฑฐ
14
- - ๋ชจ๋“ˆํ™”๋œ ์ฝ”๋“œ ๊ตฌ์กฐ ๊ฐœ์„ 
15
 
16
- ## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์ •๋ฆฌ
17
- - ์ฃผ์š” ์‹คํ–‰ ํŒŒ์ผ์„ ํ™•์ธํ•˜๊ณ  `app_revised.py`๋ฅผ ์ฃผ ํŒŒ์ผ๋กœ ์„ ํƒ
18
- - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ๋ฅผ 'app' ํŒจํ‚ค์ง€๋กœ ๋ชจ๋“ˆํ™”:
19
- - ์žฅ์น˜ ์ œ์–ด ๋ผ์šฐํ„ฐ: `app/app_device_routes.py`
20
- - ๊ธฐ๋ณธ API ๋ผ์šฐํ„ฐ: `app/app_routes.py`
21
- - ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”: `app/init_retriever.py`
22
- - ํ”„๋ŸฐํŠธ์—”๋“œ ์ฝ”๋“œ ํ†ตํ•ฉ:
23
- - `templates/index.html`์— ์žฅ์น˜ ์ œ์–ด UI ์„น์…˜
24
- - `static/js/app-device.js`์— ์žฅ์น˜ ์ œ์–ด JavaScript ์ฝ”๋“œ
25
- - `static/css/device-style.css`์— ์žฅ์น˜ ์ œ์–ด ์Šคํƒ€์ผ
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- ## ๋‚จ์€ ์ž‘์—…
28
- - [ ] ์ „์ฒด ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…
29
- - ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ, ๋ฐฐํฌ ํ™˜๊ฒฝ)
30
- - ๋‹ค์–‘ํ•œ ์žฅ์น˜ ์—ฐ๊ฒฐ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
31
 
32
- ## ํŒŒ์ผ ๊ตฌ์กฐ
33
- - `app_revised.py`: ๋ฉ”์ธ Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
34
- - `app.py`: ๋‹จ์ˆœํ™”๋œ ์ง„์ž…์  (app_revised.py ํ˜ธ์ถœ)
35
- - `app/`: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ชจ๋“ˆ
36
- - `__init__.py`: ํŒจํ‚ค์ง€ ์ดˆ๊ธฐํ™”
37
- - `app_device_routes.py`: LocalPCAgent ์ œ์–ด API ๋ผ์šฐํŠธ
38
- - `app_routes.py`: ๊ธฐ๋ณธ RAG ์ฑ—๋ด‡ API ๋ผ์šฐํŠธ
39
- - `init_retriever.py`: RAG ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ
40
- - `static/`: ์ •์  ํŒŒ์ผ
41
- - `css/`: ์Šคํƒ€์ผ์‹œํŠธ
42
- - `style.css`: ๊ธฐ๋ณธ ์Šคํƒ€์ผ
43
- - `device-style.css`: ์žฅ์น˜ ์ œ์–ด ์Šคํƒ€์ผ
44
- - `js/`: JavaScript ํŒŒ์ผ
45
- - `app.js`: ๋ฉ”์ธ JavaScript
46
- - `app-core.js`: ํ•ต์‹ฌ ๊ธฐ๋Šฅ
47
- - `app-device.js`: ์žฅ์น˜ ์ œ์–ด ๊ธฐ๋Šฅ
48
- - `app-docs.js`: ๋ฌธ์„œ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ
49
- - `app-llm.js`: LLM ๊ด€๋ จ ๊ธฐ๋Šฅ
50
- - `templates/`: HTML ํ…œํ”Œ๋ฆฟ
51
- - `index.html`: ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŽ˜์ด์ง€
52
- - `login.html`: ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
53
- - `loading.html`: ๋กœ๋”ฉ ํŽ˜์ด์ง€
54
- - `docs/`: ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ
55
- - `project_plan.md`: ํ”„๋กœ์ ํŠธ ๊ณ„ํš ๋ฌธ์„œ
56
- - `cleanup_plan.md`: ์ฝ”๋“œ ์ •๋ฆฌ ๊ณ„ํš ๋ฌธ์„œ
57
 
58
- ## LocalPCAgent ํ†ตํ•ฉ ์š”์•ฝ
59
- - **๋ฐฑ์—”๋“œ API**: `app/app_device_routes.py`์— ๊ตฌํ˜„
60
- - `/api/device/status`: ์žฅ์น˜ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ
61
- - `/api/device/connect`: ์‚ฌ์šฉ์ž ์ง€์ • URL ์—ฐ๊ฒฐ
62
- - `/api/device/list`: ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ
63
- - `/api/device/programs`: ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
64
- - `/api/device/programs/<program_id>/execute`: ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
65
- - `/api/device/execute-custom`: ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
66
 
67
- - **ํ”„๋ŸฐํŠธ์—”๋“œ**: ๊ธฐ๋Šฅ ์™„์„ฑ
68
- - ์žฅ์น˜ ์ œ์–ด ํƒญ UI: `templates/index.html`์— ๊ตฌํ˜„
69
- - JavaScript ๋กœ์ง: `static/js/app-device.js`์— ๊ตฌํ˜„
70
- - ์Šคํƒ€์ผ: `static/css/device-style.css`์— ๊ตฌํ˜„
71
 
72
- ## ์ฃผ์˜์‚ฌํ•ญ
73
- - JavaScript๋Š” ์ง์ ‘ LocalPCAgent์™€ ํ†ต์‹ ํ•˜์ง€ ์•Š๊ณ  Flask ๋ฐฑ์—”๋“œ๋ฅผ ํ†ตํ•ด ํ†ต์‹ 
74
- - ๋ชจ๋“  ์žฅ์น˜ ๊ด€๋ จ API๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ (`login_required` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ ์šฉ)
75
- - ๋กœ๊น… ์ฝ”๋“œ ์œ ์ง€ํ•˜์—ฌ ๋””๋ฒ„๊น… ์šฉ์ดํ•˜๊ฒŒ ํ•จ
 
1
  # RAG ์ฑ—๋ด‡ + LocalPCAgent ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ ๊ณ„ํš
2
 
3
  ## ํ”„๋กœ์ ํŠธ ๊ฐœ์š”
4
+
5
+ ์ด ํ”„๋กœ์ ํŠธ๋Š” RAG ์ฑ—๋ด‡ ๋ฐฑ์—”๋“œ(Flask ๊ธฐ๋ฐ˜)์™€ LocalPCAgent ์ œ์–ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” RAG ์ฑ—๋ด‡ ์ธํ„ฐํŽ˜์ด์Šค ๋‚ด์—์„œ LocalPCAgent๋ฅผ ํ†ตํ•ด ์›๊ฒฉ์œผ๋กœ PC๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
6
 
7
  ## ์™„๋ฃŒ๋œ ์ž‘์—…
 
 
 
 
 
 
 
 
8
 
9
+ ### 1. ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ ๊ฐœ์„  (2025-05-02)
10
+
11
+ - `app-device.js` ํŒŒ์ผ์—์„œ `connectServer()` ํ•จ์ˆ˜ ์ˆ˜์ •:
12
+ - ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ €์žฅ๋œ URL์„ ์šฐ์„ ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
13
+ - ํ…์ŠคํŠธ๋ฐ•์Šค์— ์ž…๋ ฅ๋œ URL ์ฃผ์†Œ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ๋ฐฑ์—…์œผ๋กœ ์‚ฌ์šฉ
14
+ - ์—ฐ๊ฒฐ ์ƒํƒœ ๋ฐ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๊ฐœ์„ 
15
+
16
+ - `app_device_routes.py` ํŒŒ์ผ์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€:
17
+ - ์‚ฌ์šฉ์ž ์ง€์ • URL ์ €์žฅ์„ ์œ„ํ•œ `custom_device_url` ๋ณ€์ˆ˜ ์ถ”๊ฐ€
18
+ - URL ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ `get_device_url()` ํ•จ์ˆ˜ ๊ตฌํ˜„
19
+ - `/api/device/connect` ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ง€์ • URL ์„ค์ • ๊ธฐ๋Šฅ ๊ตฌํ˜„
20
+ - ๋ชจ๋“  API ์—”๋“œํฌ์ธํŠธ์—์„œ `get_device_url()` ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์—…๋ฐ์ดํŠธ
21
+
22
+ ## ์˜ˆ์ •๋œ ์ž‘์—…
23
+
24
+ ### 1. ์ถ”๊ฐ€ UI ๊ฐœ์„ 
25
+
26
+ - ์žฅ์น˜ ์„œ๋ฒ„ URL ์ž…๋ ฅ ํ•„๋“œ์— ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์ถ”๊ฐ€ (์˜ˆ: "ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ €์žฅ๋œ URL ์‚ฌ์šฉ, ๋˜๋Š” ์ง์ ‘ ์ž…๋ ฅ")
27
+ - ์—ฐ๊ฒฐ ์„ฑ๊ณต/์‹คํŒจ ์‹œ UI ํ”ผ๋“œ๋ฐฑ ๊ฐœ์„ 
28
+
29
+ ### 2. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ฐ•ํ™”
30
 
31
+ - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๊ฐœ์„  ๋ฐ ๋” ๊ตฌ์ฒด์ ์ธ ๊ฐ€์ด๋“œ ์ œ๊ณต
32
+ - ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ ๊ธฐ๋Šฅ
 
 
33
 
34
+ ### 3. ํ…Œ์ŠคํŠธ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ - ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ๋ฐ ์‚ฌ์šฉ์ž ์ง€์ • URL ์ „ํ™˜ ํ…Œ์ŠคํŠธ
37
+ - ๋‹ค์–‘ํ•œ ์˜ค๋ฅ˜ ์ƒํ™ฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฐ ๋ณต๊ตฌ ํ…Œ์ŠคํŠธ
 
 
 
 
 
 
38
 
39
+ ## ๊ธฐ์ˆ  ์Šคํƒ
 
 
 
40
 
41
+ - ํ”„๋ก ํŠธ์—”๋“œ: JavaScript, HTML, CSS
42
+ - ๋ฐฑ์—”๋“œ: Flask (Python)
43
+ - ํ†ต์‹ : RESTful API
44
+ - ์žฅ์น˜ ์ œ์–ด: LocalPCAgent API
app/{app/init_retriever.py โ†’ init_retriever.py} RENAMED
File without changes