Spaces:
Sleeping
Sleeping
init
Browse files- app_part2.py +0 -209
- app_part3.py +0 -163
- app_revised.py +0 -268
app_part2.py
DELETED
@@ -1,209 +0,0 @@
|
|
1 |
-
# --- μλ² λ© κ΄λ ¨ ν¬νΌ ν¨μ ---
|
2 |
-
def save_embeddings(base_retriever, file_path):
|
3 |
-
"""μλ² λ© λ°μ΄ν°λ₯Ό μμΆνμ¬ νμΌμ μ μ₯"""
|
4 |
-
try:
|
5 |
-
# μ μ₯ λλ ν λ¦¬κ° μμΌλ©΄ μμ±
|
6 |
-
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
7 |
-
|
8 |
-
# νμμ€ν¬ν μΆκ°
|
9 |
-
save_data = {
|
10 |
-
'timestamp': datetime.now().isoformat(),
|
11 |
-
'retriever': base_retriever
|
12 |
-
}
|
13 |
-
|
14 |
-
# μμΆνμ¬ μ μ₯ (μ©λ μ€μ΄κΈ°)
|
15 |
-
with gzip.open(file_path, 'wb') as f:
|
16 |
-
pickle.dump(save_data, f)
|
17 |
-
|
18 |
-
logger.info(f"μλ² λ© λ°μ΄ν°λ₯Ό {file_path}μ μμΆνμ¬ μ μ₯νμ΅λλ€.")
|
19 |
-
return True
|
20 |
-
except Exception as e:
|
21 |
-
logger.error(f"μλ² λ© μ μ₯ μ€ μ€λ₯ λ°μ: {e}")
|
22 |
-
return False
|
23 |
-
|
24 |
-
def load_embeddings(file_path, max_age_days=30):
|
25 |
-
"""μ μ₯λ μλ² λ© λ°μ΄ν°λ₯Ό νμΌμμ λ‘λ"""
|
26 |
-
try:
|
27 |
-
if not os.path.exists(file_path):
|
28 |
-
logger.info(f"μ μ₯λ μλ² λ© νμΌ({file_path})μ΄ μμ΅λλ€.")
|
29 |
-
return None
|
30 |
-
|
31 |
-
# μμΆ νμΌ λ‘λ
|
32 |
-
with gzip.open(file_path, 'rb') as f:
|
33 |
-
data = pickle.load(f)
|
34 |
-
|
35 |
-
# νμμ€ν¬ν νμΈ (λ무 μ€λλ λ°μ΄ν°λ μ¬μ©νμ§ μμ)
|
36 |
-
saved_time = datetime.fromisoformat(data['timestamp'])
|
37 |
-
age = (datetime.now() - saved_time).days
|
38 |
-
|
39 |
-
if age > max_age_days:
|
40 |
-
logger.info(f"μ μ₯λ μλ² λ©μ΄ {age}μΌλ‘ λ무 μ€λλμμ΅λλ€. μλ‘ μμ±ν©λλ€.")
|
41 |
-
return None
|
42 |
-
|
43 |
-
logger.info(f"{file_path}μμ μλ² λ© λ°μ΄ν°λ₯Ό λ‘λνμ΅λλ€. (μμ±μΌ: {saved_time})")
|
44 |
-
return data['retriever']
|
45 |
-
except Exception as e:
|
46 |
-
logger.error(f"μλ² λ© λ‘λ μ€ μ€λ₯ λ°μ: {e}")
|
47 |
-
return None
|
48 |
-
|
49 |
-
def init_retriever():
|
50 |
-
"""κ²μκΈ° κ°μ²΄ μ΄κΈ°ν λλ λ‘λ"""
|
51 |
-
global base_retriever, retriever
|
52 |
-
|
53 |
-
# μλ² λ© μΊμ νμΌ κ²½λ‘
|
54 |
-
cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
|
55 |
-
|
56 |
-
# λ¨Όμ μ μ₯λ μλ² λ© λ°μ΄ν° λ‘λ μλ
|
57 |
-
cached_retriever = load_embeddings(cache_path)
|
58 |
-
|
59 |
-
if cached_retriever:
|
60 |
-
logger.info("μΊμλ μλ² λ© λ°μ΄ν°λ₯Ό μ±κ³΅μ μΌλ‘ λ‘λνμ΅λλ€.")
|
61 |
-
base_retriever = cached_retriever
|
62 |
-
else:
|
63 |
-
# μΊμλ λ°μ΄ν°κ° μμΌλ©΄ κΈ°μ‘΄ λ°©μμΌλ‘ μ΄κΈ°ν
|
64 |
-
index_path = app.config['INDEX_PATH']
|
65 |
-
|
66 |
-
# VectorRetriever λ‘λ λλ μ΄κΈ°ν
|
67 |
-
if os.path.exists(os.path.join(index_path, "documents.json")):
|
68 |
-
try:
|
69 |
-
logger.info(f"κΈ°μ‘΄ λ²‘ν° μΈλ±μ€λ₯Ό '{index_path}'μμ λ‘λν©λλ€...")
|
70 |
-
base_retriever = VectorRetriever.load(index_path)
|
71 |
-
logger.info(f"{len(base_retriever.documents) if hasattr(base_retriever, 'documents') else 0}κ° λ¬Έμκ° λ‘λλμμ΅λλ€.")
|
72 |
-
except Exception as e:
|
73 |
-
logger.error(f"μΈλ±μ€ λ‘λ μ€ μ€λ₯ λ°μ: {e}. μ κ²μκΈ°λ₯Ό μ΄κΈ°νν©λλ€.")
|
74 |
-
base_retriever = VectorRetriever()
|
75 |
-
else:
|
76 |
-
logger.info("κΈ°μ‘΄ μΈλ±μ€λ₯Ό μ°Ύμ μ μμ΄ μ κ²μκΈ°λ₯Ό μ΄κΈ°νν©λλ€...")
|
77 |
-
base_retriever = VectorRetriever()
|
78 |
-
|
79 |
-
# λ°μ΄ν° ν΄λμ λ¬Έμ λ‘λ
|
80 |
-
data_path = app.config['DATA_FOLDER']
|
81 |
-
if (not hasattr(base_retriever, 'documents') or not base_retriever.documents) and os.path.exists(data_path):
|
82 |
-
logger.info(f"{data_path}μμ λ¬Έμλ₯Ό λ‘λν©λλ€...")
|
83 |
-
try:
|
84 |
-
docs = DocumentProcessor.load_documents_from_directory(
|
85 |
-
data_path,
|
86 |
-
extensions=[".txt", ".md", ".csv"],
|
87 |
-
recursive=True
|
88 |
-
)
|
89 |
-
if docs and hasattr(base_retriever, 'add_documents'):
|
90 |
-
logger.info(f"{len(docs)}κ° λ¬Έμλ₯Ό κ²μκΈ°μ μΆκ°ν©λλ€...")
|
91 |
-
base_retriever.add_documents(docs)
|
92 |
-
|
93 |
-
if hasattr(base_retriever, 'save'):
|
94 |
-
logger.info(f"κ²μκΈ° μνλ₯Ό '{index_path}'μ μ μ₯ν©λλ€...")
|
95 |
-
try:
|
96 |
-
base_retriever.save(index_path)
|
97 |
-
logger.info("μΈλ±μ€ μ μ₯ μλ£")
|
98 |
-
|
99 |
-
# μλ‘ μμ±λ κ²μκΈ° μΊμ±
|
100 |
-
if hasattr(base_retriever, 'documents') and base_retriever.documents:
|
101 |
-
save_embeddings(base_retriever, cache_path)
|
102 |
-
logger.info(f"κ²μκΈ°λ₯Ό μΊμ νμΌ {cache_path}μ μ μ₯ μλ£")
|
103 |
-
except Exception as e:
|
104 |
-
logger.error(f"μΈλ±μ€ μ μ₯ μ€ μ€λ₯ λ°μ: {e}")
|
105 |
-
except Exception as e:
|
106 |
-
logger.error(f"DATA_FOLDERμμ λ¬Έμ λ‘λ μ€ μ€λ₯: {e}")
|
107 |
-
|
108 |
-
# μ¬μμν κ²μκΈ° μ΄κΈ°ν
|
109 |
-
logger.info("μ¬μμν κ²μκΈ°λ₯Ό μ΄κΈ°νν©λλ€...")
|
110 |
-
try:
|
111 |
-
# μ체 ꡬνλ μ¬μμν ν¨μ
|
112 |
-
def custom_rerank_fn(query, results):
|
113 |
-
query_terms = set(query.lower().split())
|
114 |
-
for result in results:
|
115 |
-
if isinstance(result, dict) and "text" in result:
|
116 |
-
text = result["text"].lower()
|
117 |
-
term_freq = sum(1 for term in query_terms if term in text)
|
118 |
-
normalized_score = term_freq / (len(text.split()) + 1) * 10
|
119 |
-
result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
|
120 |
-
elif isinstance(result, dict):
|
121 |
-
result["rerank_score"] = result.get("score", 0)
|
122 |
-
results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
|
123 |
-
return results
|
124 |
-
|
125 |
-
# ReRanker ν΄λμ€ μ¬μ©
|
126 |
-
retriever = ReRanker(
|
127 |
-
base_retriever=base_retriever,
|
128 |
-
rerank_fn=custom_rerank_fn,
|
129 |
-
rerank_field="text"
|
130 |
-
)
|
131 |
-
logger.info("μ¬μμν κ²μκΈ° μ΄κΈ°ν μλ£")
|
132 |
-
except Exception as e:
|
133 |
-
logger.error(f"μ¬μμν κ²μκΈ° μ΄κΈ°ν μ€ν¨: {e}")
|
134 |
-
retriever = base_retriever # μ€ν¨ μ κΈ°λ³Έ κ²μκΈ° μ¬μ©
|
135 |
-
|
136 |
-
return retriever
|
137 |
-
|
138 |
-
def background_init():
|
139 |
-
"""λ°±κ·ΈλΌμ΄λμμ κ²μκΈ° μ΄κΈ°ν μν"""
|
140 |
-
global app_ready, retriever, base_retriever
|
141 |
-
|
142 |
-
# μ¦μ μ± μ¬μ© κ°λ₯ μνλ‘ μ€μ
|
143 |
-
app_ready = True
|
144 |
-
logger.info("μ±μ μ¦μ μ¬μ© κ°λ₯ μνλ‘ μ€μ (app_ready=True)")
|
145 |
-
|
146 |
-
try:
|
147 |
-
# κΈ°λ³Έ κ²μκΈ° μ΄κΈ°ν (보ν)
|
148 |
-
if base_retriever is None:
|
149 |
-
base_retriever = MockComponent()
|
150 |
-
if hasattr(base_retriever, 'documents'):
|
151 |
-
base_retriever.documents = []
|
152 |
-
|
153 |
-
# μμ retriever μ€μ
|
154 |
-
if retriever is None:
|
155 |
-
retriever = MockComponent()
|
156 |
-
if not hasattr(retriever, 'search'):
|
157 |
-
retriever.search = lambda query, **kwargs: []
|
158 |
-
|
159 |
-
# μΊμλ μλ² λ© λ‘λ μλ
|
160 |
-
cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
|
161 |
-
cached_retriever = load_embeddings(cache_path)
|
162 |
-
|
163 |
-
if cached_retriever:
|
164 |
-
# μΊμλ λ°μ΄ν°κ° μμΌλ©΄ λ°λ‘ μ¬μ©
|
165 |
-
base_retriever = cached_retriever
|
166 |
-
|
167 |
-
# κ°λ¨ν μ¬μμν ν¨μ
|
168 |
-
def simple_rerank(query, results):
|
169 |
-
if results:
|
170 |
-
for result in results:
|
171 |
-
if isinstance(result, dict):
|
172 |
-
result["rerank_score"] = result.get("score", 0)
|
173 |
-
results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
|
174 |
-
return results
|
175 |
-
|
176 |
-
# μ¬μμν κ²μκΈ° μ΄κΈ°ν
|
177 |
-
retriever = ReRanker(
|
178 |
-
base_retriever=base_retriever,
|
179 |
-
rerank_fn=simple_rerank,
|
180 |
-
rerank_field="text"
|
181 |
-
)
|
182 |
-
|
183 |
-
logger.info("μΊμλ μλ² λ©μΌλ‘ κ²μκΈ° μ΄κΈ°ν μλ£ (λΉ λ₯Έ μμ)")
|
184 |
-
else:
|
185 |
-
# μΊμλ λ°μ΄ν°κ° μμΌλ©΄ μ 체 μ΄κΈ°ν μ§ν
|
186 |
-
logger.info("μΊμλ μλ² λ©μ΄ μμ΄ μ 체 μ΄κΈ°ν μμ")
|
187 |
-
retriever = init_retriever()
|
188 |
-
logger.info("μ 체 μ΄κΈ°ν μλ£")
|
189 |
-
|
190 |
-
logger.info("μ± μ΄κΈ°ν μλ£ (λͺ¨λ μ»΄ν¬λνΈ μ€λΉλ¨)")
|
191 |
-
except Exception as e:
|
192 |
-
logger.error(f"μ± λ°±κ·ΈλΌμ΄λ μ΄κΈ°ν μ€ μ¬κ°ν μ€λ₯ λ°μ: {e}", exc_info=True)
|
193 |
-
# μ΄κΈ°ν μ€ν¨ μ κΈ°λ³Έ κ°μ²΄ μμ±
|
194 |
-
if base_retriever is None:
|
195 |
-
base_retriever = MockComponent()
|
196 |
-
if hasattr(base_retriever, 'documents'):
|
197 |
-
base_retriever.documents = []
|
198 |
-
if retriever is None:
|
199 |
-
retriever = MockComponent()
|
200 |
-
if not hasattr(retriever, 'search'):
|
201 |
-
retriever.search = lambda query, **kwargs: []
|
202 |
-
|
203 |
-
logger.warning("μ΄κΈ°ν μ€ μ€λ₯κ° μμ§λ§ μ±μ κ³μ μ¬μ© κ°λ₯ν©λλ€.")
|
204 |
-
|
205 |
-
# λ°±κ·ΈλΌμ΄λ μ€λ λ μμ
|
206 |
-
init_thread = threading.Thread(target=background_init)
|
207 |
-
init_thread.daemon = True
|
208 |
-
init_thread.start()
|
209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app_part3.py
DELETED
@@ -1,163 +0,0 @@
|
|
1 |
-
# --- Flask λΌμ°νΈ μ μ ---
|
2 |
-
|
3 |
-
@app.route('/login', methods=['GET', 'POST'])
|
4 |
-
def login():
|
5 |
-
error = None
|
6 |
-
next_url = request.args.get('next')
|
7 |
-
logger.info(f"-------------- λ‘κ·ΈμΈ νμ΄μ§ μ μ (Next: {next_url}) --------------")
|
8 |
-
logger.info(f"Method: {request.method}")
|
9 |
-
|
10 |
-
if request.method == 'POST':
|
11 |
-
logger.info("λ‘κ·ΈμΈ μλ λ°μ")
|
12 |
-
username = request.form.get('username', '')
|
13 |
-
password = request.form.get('password', '')
|
14 |
-
logger.info(f"μ
λ ₯λ μ¬μ©μλͺ
: {username}")
|
15 |
-
logger.info(f"λΉλ°λ²νΈ μ
λ ₯ μ¬λΆ: {len(password) > 0}")
|
16 |
-
|
17 |
-
# νκ²½ λ³μ λλ κΈ°λ³Έκ°κ³Ό λΉκ΅
|
18 |
-
valid_username = ADMIN_USERNAME
|
19 |
-
valid_password = ADMIN_PASSWORD
|
20 |
-
logger.info(f"κ²μ¦μ© μ¬μ©μλͺ
: {valid_username}")
|
21 |
-
logger.info(f"κ²μ¦μ© λΉλ°λ²νΈ μ‘΄μ¬ μ¬λΆ: {valid_password is not None and len(valid_password) > 0}")
|
22 |
-
|
23 |
-
if username == valid_username and password == valid_password:
|
24 |
-
logger.info(f"λ‘κ·ΈμΈ μ±κ³΅: {username}")
|
25 |
-
# μΈμ
μ€μ μ νμ¬ μΈμ
μν λ‘κΉ
|
26 |
-
logger.debug(f"μΈμ
μ€μ μ : {session}")
|
27 |
-
|
28 |
-
# μΈμ
μ λ‘κ·ΈμΈ μ 보 μ μ₯
|
29 |
-
session.permanent = True
|
30 |
-
session['logged_in'] = True
|
31 |
-
session['username'] = username
|
32 |
-
session.modified = True
|
33 |
-
|
34 |
-
logger.info(f"μΈμ
μ€μ ν: {session}")
|
35 |
-
logger.info("μΈμ
μ€μ μλ£, 리λλ μ
μλ")
|
36 |
-
|
37 |
-
# λ‘κ·ΈμΈ μ±κ³΅ ν 리λλ μ
|
38 |
-
redirect_to = next_url or url_for('index')
|
39 |
-
logger.info(f"리λλ μ
λμ: {redirect_to}")
|
40 |
-
response = redirect(redirect_to)
|
41 |
-
return response
|
42 |
-
else:
|
43 |
-
logger.warning("λ‘κ·ΈμΈ μ€ν¨: μμ΄λ λλ λΉλ°λ²νΈ λΆμΌμΉ")
|
44 |
-
if username != valid_username: logger.warning("μ¬μ©μλͺ
λΆμΌμΉ")
|
45 |
-
if password != valid_password: logger.warning("λΉλ°λ²νΈ λΆμΌμΉ")
|
46 |
-
error = 'μμ΄λ λλ λΉλ°λ²νΈκ° μ¬λ°λ₯΄μ§ μμ΅λλ€.'
|
47 |
-
else:
|
48 |
-
logger.info("λ‘κ·ΈμΈ νμ΄μ§ GET μμ²")
|
49 |
-
if 'logged_in' in session:
|
50 |
-
logger.info("μ΄λ―Έ λ‘κ·ΈμΈλ μ¬μ©μ, λ©μΈ νμ΄μ§λ‘ 리λλ μ
")
|
51 |
-
return redirect(url_for('index'))
|
52 |
-
|
53 |
-
logger.info("---------- λ‘κ·ΈμΈ νμ΄μ§ λ λλ§ ----------")
|
54 |
-
return render_template('login.html', error=error, next=next_url)
|
55 |
-
|
56 |
-
|
57 |
-
@app.route('/logout')
|
58 |
-
def logout():
|
59 |
-
logger.info("-------------- λ‘κ·Έμμ μμ² --------------")
|
60 |
-
logger.info(f"λ‘κ·Έμμ μ μΈμ
μν: {session}")
|
61 |
-
|
62 |
-
if 'logged_in' in session:
|
63 |
-
username = session.get('username', 'unknown')
|
64 |
-
logger.info(f"μ¬μ©μ {username} λ‘κ·Έμμ μ²λ¦¬ μμ")
|
65 |
-
session.pop('logged_in', None)
|
66 |
-
session.pop('username', None)
|
67 |
-
session.modified = True
|
68 |
-
logger.info(f"μΈμ
μ 보 μμ μλ£. νμ¬ μΈμ
: {session}")
|
69 |
-
else:
|
70 |
-
logger.warning("λ‘κ·ΈμΈλμ§ μμ μνμμ λ‘κ·Έμμ μλ")
|
71 |
-
|
72 |
-
logger.info("λ‘κ·ΈμΈ νμ΄μ§λ‘ 리λλ μ
")
|
73 |
-
response = redirect(url_for('login'))
|
74 |
-
return response
|
75 |
-
|
76 |
-
|
77 |
-
@app.route('/')
|
78 |
-
@login_required
|
79 |
-
def index():
|
80 |
-
"""λ©μΈ νμ΄μ§"""
|
81 |
-
global app_ready
|
82 |
-
|
83 |
-
# μ± μ€λΉ μν νμΈ - 30μ΄ μ΄μ μ§λ¬μΌλ©΄ κ°μ λ‘ ready μνλ‘ λ³κ²½
|
84 |
-
current_time = datetime.now()
|
85 |
-
start_time = datetime.fromtimestamp(os.path.getmtime(__file__))
|
86 |
-
time_diff = (current_time - start_time).total_seconds()
|
87 |
-
|
88 |
-
if not app_ready and time_diff > 30:
|
89 |
-
logger.warning(f"μ±μ΄ 30μ΄ μ΄μ μ΄κΈ°ν μ€ μνμ
λλ€. κ°μ λ‘ ready μνλ‘ λ³κ²½ν©λλ€.")
|
90 |
-
app_ready = True
|
91 |
-
|
92 |
-
if not app_ready:
|
93 |
-
logger.info("μ±μ΄ μμ§ μ€λΉλμ§ μμ λ‘λ© νμ΄μ§ νμ")
|
94 |
-
return render_template('loading.html'), 503 # μλΉμ€ μ€λΉ μλ¨ μν μ½λ
|
95 |
-
|
96 |
-
logger.info("λ©μΈ νμ΄μ§ μμ²")
|
97 |
-
return render_template('index.html')
|
98 |
-
|
99 |
-
|
100 |
-
@app.route('/api/status')
|
101 |
-
@login_required
|
102 |
-
def app_status():
|
103 |
-
"""μ± μ΄κΈ°ν μν νμΈ API"""
|
104 |
-
logger.info(f"μ± μν νμΈ μμ²: {'Ready' if app_ready else 'Not Ready'}")
|
105 |
-
return jsonify({"ready": app_ready})
|
106 |
-
|
107 |
-
|
108 |
-
@app.route('/api/llm', methods=['GET', 'POST'])
|
109 |
-
@login_required
|
110 |
-
def llm_api():
|
111 |
-
"""μ¬μ© κ°λ₯ν LLM λͺ©λ‘ λ° μ ν API"""
|
112 |
-
global llm_interface
|
113 |
-
|
114 |
-
if not app_ready:
|
115 |
-
return jsonify({"error": "μ±μ΄ μμ§ μ΄κΈ°ν μ€μ
λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ."}), 503
|
116 |
-
|
117 |
-
if request.method == 'GET':
|
118 |
-
logger.info("LLM λͺ©λ‘ μμ²")
|
119 |
-
try:
|
120 |
-
current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
|
121 |
-
supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
|
122 |
-
supported_list = [{
|
123 |
-
"name": name, "id": id, "current": id == current_details.get("id")
|
124 |
-
} for name, id in supported_llms_dict.items()]
|
125 |
-
|
126 |
-
return jsonify({
|
127 |
-
"supported_llms": supported_list,
|
128 |
-
"current_llm": current_details
|
129 |
-
})
|
130 |
-
except Exception as e:
|
131 |
-
logger.error(f"LLM μ 보 μ‘°ν μ€λ₯: {e}")
|
132 |
-
return jsonify({"error": "LLM μ 보 μ‘°ν μ€ μ€λ₯ λ°μ"}), 500
|
133 |
-
|
134 |
-
elif request.method == 'POST':
|
135 |
-
data = request.get_json()
|
136 |
-
if not data or 'llm_id' not in data:
|
137 |
-
return jsonify({"error": "LLM IDκ° μ 곡λμ§ μμμ΅λλ€."}), 400
|
138 |
-
|
139 |
-
llm_id = data['llm_id']
|
140 |
-
logger.info(f"LLM λ³κ²½ μμ²: {llm_id}")
|
141 |
-
|
142 |
-
try:
|
143 |
-
if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
|
144 |
-
raise NotImplementedError("LLM μΈν°νμ΄μ€μ νμν λ©μλ/μμ± μμ")
|
145 |
-
|
146 |
-
if llm_id not in llm_interface.llm_clients:
|
147 |
-
return jsonify({"error": f"μ§μλμ§ μλ LLM ID: {llm_id}"}), 400
|
148 |
-
|
149 |
-
success = llm_interface.set_llm(llm_id)
|
150 |
-
if success:
|
151 |
-
new_details = llm_interface.get_current_llm_details()
|
152 |
-
logger.info(f"LLMμ΄ '{new_details.get('name', llm_id)}'λ‘ λ³κ²½λμμ΅λλ€.")
|
153 |
-
return jsonify({
|
154 |
-
"success": True,
|
155 |
-
"message": f"LLMμ΄ '{new_details.get('name', llm_id)}'λ‘ λ³κ²½λμμ΅λλ€.",
|
156 |
-
"current_llm": new_details
|
157 |
-
})
|
158 |
-
else:
|
159 |
-
logger.error(f"LLM λ³κ²½ μ€ν¨ (ID: {llm_id})")
|
160 |
-
return jsonify({"error": "LLM λ³κ²½ μ€ λ΄λΆ μ€λ₯ λ°μ"}), 500
|
161 |
-
except Exception as e:
|
162 |
-
logger.error(f"LLM λ³κ²½ μ²λ¦¬ μ€ μ€λ₯: {e}", exc_info=True)
|
163 |
-
return jsonify({"error": f"LLM λ³κ²½ μ€ μ€λ₯ λ°μ: {str(e)}"}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app_revised.py
DELETED
@@ -1,268 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
RAG κ²μ μ±λ΄ μΉ μ ν리μΌμ΄μ
(μ₯μΉ κ΄λ¦¬ κΈ°λ₯ ν΅ν©)
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import logging
|
7 |
-
import threading
|
8 |
-
from datetime import datetime, timedelta
|
9 |
-
from flask import Flask, send_from_directory, jsonify
|
10 |
-
from dotenv import load_dotenv
|
11 |
-
from functools import wraps
|
12 |
-
from flask_cors import CORS
|
13 |
-
|
14 |
-
# λ‘κ±° μ€μ
|
15 |
-
logging.basicConfig(
|
16 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
17 |
-
level=logging.DEBUG
|
18 |
-
)
|
19 |
-
logger = logging.getLogger(__name__)
|
20 |
-
|
21 |
-
# νκ²½ λ³μ λ‘λ
|
22 |
-
load_dotenv()
|
23 |
-
|
24 |
-
# νκ²½ λ³μ λ‘λ μν νμΈ λ° λ‘κΉ
|
25 |
-
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
|
26 |
-
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
|
27 |
-
DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', 'http://localhost:5050')
|
28 |
-
|
29 |
-
logger.info(f"==== νκ²½ λ³μ λ‘λ μν ====")
|
30 |
-
logger.info(f"ADMIN_USERNAME μ€μ μ¬λΆ: {ADMIN_USERNAME is not None}")
|
31 |
-
logger.info(f"ADMIN_PASSWORD μ€μ μ¬λΆ: {ADMIN_PASSWORD is not None}")
|
32 |
-
logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL}")
|
33 |
-
|
34 |
-
# νκ²½ λ³μκ° μμΌλ©΄ κΈ°λ³Έκ° μ€μ
|
35 |
-
if not ADMIN_USERNAME:
|
36 |
-
ADMIN_USERNAME = 'admin'
|
37 |
-
logger.warning("ADMIN_USERNAME νκ²½λ³μκ° μμ΄ κΈ°λ³Έκ° 'admin'μΌλ‘ μ€μ ν©λλ€.")
|
38 |
-
|
39 |
-
if not ADMIN_PASSWORD:
|
40 |
-
ADMIN_PASSWORD = 'rag12345'
|
41 |
-
logger.warning("ADMIN_PASSWORD νκ²½λ³μκ° μμ΄ κΈ°λ³Έκ° 'rag12345'λ‘ μ€μ ν©λλ€.")
|
42 |
-
|
43 |
-
class MockComponent: pass
|
44 |
-
|
45 |
-
# --- λ‘컬 λͺ¨λ μν¬νΈ ---
|
46 |
-
try:
|
47 |
-
from utils.vito_stt import VitoSTT
|
48 |
-
from utils.llm_interface import LLMInterface
|
49 |
-
from utils.document_processor import DocumentProcessor
|
50 |
-
from retrieval.vector_retriever import VectorRetriever
|
51 |
-
from retrieval.reranker import ReRanker
|
52 |
-
except ImportError as e:
|
53 |
-
logger.error(f"λ‘컬 λͺ¨λ μν¬νΈ μ€ν¨: {e}. utils λ° retrieval ν¨ν€μ§κ° μ¬λ°λ₯Έ κ²½λ‘μ μλμ§ νμΈνμΈμ.")
|
54 |
-
VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
|
55 |
-
# --- λ‘컬 λͺ¨λ μν¬νΈ λ ---
|
56 |
-
|
57 |
-
|
58 |
-
# Flask μ± μ΄κΈ°ν
|
59 |
-
app = Flask(__name__)
|
60 |
-
|
61 |
-
# CORS μ€μ - λͺ¨λ λλ©μΈμμμ μμ² νμ©
|
62 |
-
CORS(app, supports_credentials=True)
|
63 |
-
|
64 |
-
# μΈμ
μ€μ
|
65 |
-
app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345')
|
66 |
-
|
67 |
-
# --- μΈμ
μΏ ν€ μ€μ ---
|
68 |
-
app.config['SESSION_COOKIE_SECURE'] = True
|
69 |
-
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
70 |
-
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
71 |
-
app.config['SESSION_COOKIE_DOMAIN'] = None
|
72 |
-
app.config['SESSION_COOKIE_PATH'] = '/'
|
73 |
-
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=1)
|
74 |
-
# --- μΈμ
μΏ ν€ μ€μ λ ---
|
75 |
-
|
76 |
-
# μ΅λ νμΌ ν¬κΈ° μ€μ (10MB)
|
77 |
-
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
|
78 |
-
# μ ν리μΌμ΄μ
νμΌ κΈ°μ€ μλ κ²½λ‘ μ€μ
|
79 |
-
APP_ROOT = os.path.dirname(os.path.abspath(__file__))
|
80 |
-
app.config['UPLOAD_FOLDER'] = os.path.join(APP_ROOT, 'uploads')
|
81 |
-
app.config['DATA_FOLDER'] = os.path.join(APP_ROOT, '..', 'data')
|
82 |
-
app.config['INDEX_PATH'] = os.path.join(APP_ROOT, '..', 'data', 'index')
|
83 |
-
|
84 |
-
# νμν ν΄λ μμ±
|
85 |
-
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
86 |
-
os.makedirs(app.config['DATA_FOLDER'], exist_ok=True)
|
87 |
-
os.makedirs(app.config['INDEX_PATH'], exist_ok=True)
|
88 |
-
|
89 |
-
# --- μ μ κ°μ²΄ μ΄κΈ°ν ---
|
90 |
-
try:
|
91 |
-
llm_interface = LLMInterface(default_llm="openai")
|
92 |
-
stt_client = VitoSTT()
|
93 |
-
except NameError:
|
94 |
-
logger.warning("LLM λλ STT μΈν°νμ΄μ€ μ΄κΈ°ν μ€ν¨. Mock κ°μ²΄λ₯Ό μ¬μ©ν©λλ€.")
|
95 |
-
llm_interface = MockComponent()
|
96 |
-
stt_client = MockComponent()
|
97 |
-
|
98 |
-
base_retriever = None
|
99 |
-
retriever = None
|
100 |
-
app_ready = False # μ± μ΄κΈ°ν μν νλκ·Έ
|
101 |
-
# --- μ μ κ°μ²΄ μ΄κΈ°ν λ ---
|
102 |
-
|
103 |
-
|
104 |
-
# --- μΈμ¦ λ°μ½λ μ΄ν° ---
|
105 |
-
def login_required(f):
|
106 |
-
@wraps(f)
|
107 |
-
def decorated_function(*args, **kwargs):
|
108 |
-
from flask import request, session, redirect, url_for
|
109 |
-
|
110 |
-
logger.info(f"----------- μΈμ¦ νμ νμ΄μ§ μ κ·Ό μλ: {request.path} -----------")
|
111 |
-
logger.info(f"νμ¬ νλΌμ€ν¬ μΈμ
κ°μ²΄: {session}")
|
112 |
-
logger.info(f"νμ¬ μΈμ
μν: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
|
113 |
-
logger.info(f"μμ²μ μΈμ
μΏ ν€ κ°: {request.cookies.get('session', 'None')}")
|
114 |
-
|
115 |
-
# API μμ²μ΄κ³ ν΄λΌμ΄μΈνΈμμ μ€λ κ²½μ° μΈμ¦ 무μ (μμ μ‘°μΉ)
|
116 |
-
if request.path.startswith('/api/device/'):
|
117 |
-
logger.info(f"μ₯μΉ API μμ²: {request.path} - μΈμ¦ μ μΈ")
|
118 |
-
return f(*args, **kwargs)
|
119 |
-
|
120 |
-
# Flask μΈμ
μ 'logged_in' ν€κ° μλμ§ μ§μ νμΈ
|
121 |
-
if 'logged_in' not in session:
|
122 |
-
logger.warning(f"νλΌμ€ν¬ μΈμ
μ 'logged_in' μμ. λ‘κ·ΈμΈ νμ΄μ§λ‘ 리λλ μ
.")
|
123 |
-
return redirect(url_for('login', next=request.url))
|
124 |
-
|
125 |
-
logger.info(f"μΈμ¦ μ±κ³΅: {session.get('username', 'unknown')} μ¬μ©μκ° {request.path} μ κ·Ό")
|
126 |
-
return f(*args, **kwargs)
|
127 |
-
return decorated_function
|
128 |
-
# --- μΈμ¦ λ°μ½λ μ΄ν° λ ---
|
129 |
-
|
130 |
-
# --- μ€λ₯ νΈλ€λ¬ μΆκ° ---
|
131 |
-
@app.errorhandler(404)
|
132 |
-
def not_found(e):
|
133 |
-
# ν΄λΌμ΄μΈνΈκ° JSONμ κΈ°λνλ API νΈμΆμΈ κ²½μ° JSON μλ΅
|
134 |
-
if request.path.startswith('/api/'):
|
135 |
-
return jsonify({"success": False, "error": "μμ²ν API μλν¬μΈνΈλ₯Ό μ°Ύμ μ μμ΅λλ€."}), 404
|
136 |
-
# μΌλ° μΉ νμ΄μ§ μμ²μΈ κ²½μ° HTML μλ΅
|
137 |
-
return "νμ΄μ§λ₯Ό μ°Ύμ μ μμ΅λλ€.", 404
|
138 |
-
|
139 |
-
@app.errorhandler(500)
|
140 |
-
def internal_error(e):
|
141 |
-
# ν΄λΌμ΄μΈνΈκ° JSONμ κΈ°λνλ API νΈμΆμΈ κ²½μ° JSON μλ΅
|
142 |
-
if request.path.startswith('/api/'):
|
143 |
-
return jsonify({"success": False, "error": "μλ² λ΄λΆ μ€λ₯κ° λ°μνμ΅λλ€."}), 500
|
144 |
-
# μΌλ° μΉ νμ΄μ§ μμ²μΈ κ²½μ° HTML μλ΅
|
145 |
-
return "μλ² μ€λ₯κ° λ°μνμ΅λλ€.", 500
|
146 |
-
# --- μ€λ₯ νΈλ€λ¬ λ ---
|
147 |
-
|
148 |
-
|
149 |
-
# --- μ μ νμΌ μλΉ ---
|
150 |
-
@app.route('/static/<path:path>')
|
151 |
-
def send_static(path):
|
152 |
-
return send_from_directory('static', path)
|
153 |
-
|
154 |
-
|
155 |
-
# --- λ°±κ·ΈλΌμ΄λ μ΄κΈ°ν ν¨μ ---
|
156 |
-
def background_init():
|
157 |
-
"""λ°±κ·ΈλΌμ΄λμμ κ²μκΈ° μ΄κΈ°ν μν"""
|
158 |
-
global app_ready, retriever, base_retriever
|
159 |
-
|
160 |
-
# μ¦μ μ± μ¬μ© κ°λ₯ μνλ‘ μ€μ
|
161 |
-
app_ready = True
|
162 |
-
logger.info("μ±μ μ¦μ μ¬μ© κ°λ₯ μνλ‘ μ€μ (app_ready=True)")
|
163 |
-
|
164 |
-
try:
|
165 |
-
from app.init_retriever import init_retriever
|
166 |
-
|
167 |
-
# κΈ°λ³Έ κ²μκΈ° μ΄κΈ°ν (보ν)
|
168 |
-
if base_retriever is None:
|
169 |
-
base_retriever = MockComponent()
|
170 |
-
if hasattr(base_retriever, 'documents'):
|
171 |
-
base_retriever.documents = []
|
172 |
-
|
173 |
-
# μμ retriever μ€μ
|
174 |
-
if retriever is None:
|
175 |
-
retriever = MockComponent()
|
176 |
-
if not hasattr(retriever, 'search'):
|
177 |
-
retriever.search = lambda query, **kwargs: []
|
178 |
-
|
179 |
-
# μλ² λ© μΊμ νμΌ κ²½λ‘
|
180 |
-
cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
|
181 |
-
|
182 |
-
# μΊμλ μλ² λ© λ‘λ μλ
|
183 |
-
try:
|
184 |
-
from app.init_retriever import load_embeddings
|
185 |
-
cached_retriever = load_embeddings(cache_path)
|
186 |
-
|
187 |
-
if cached_retriever:
|
188 |
-
# μΊμλ λ°μ΄ν°κ° μμΌλ©΄ λ°λ‘ μ¬μ©
|
189 |
-
base_retriever = cached_retriever
|
190 |
-
|
191 |
-
# μ¬μμν κ²μκΈ° μ΄κΈ°ν
|
192 |
-
retriever = ReRanker(
|
193 |
-
base_retriever=base_retriever,
|
194 |
-
rerank_fn=lambda query, results: results,
|
195 |
-
rerank_field="text"
|
196 |
-
)
|
197 |
-
|
198 |
-
logger.info("μΊμλ μλ² λ©μΌλ‘ κ²μκΈ° μ΄κΈ°ν μλ£ (λΉ λ₯Έ μμ)")
|
199 |
-
else:
|
200 |
-
# μΊμλ λ°μ΄ν°κ° μμΌλ©΄ μ 체 μ΄κΈ°ν μ§ν
|
201 |
-
logger.info("μΊμλ μλ² λ©μ΄ μμ΄ μ 체 μ΄κΈ°ν μμ")
|
202 |
-
retriever = init_retriever(app, base_retriever, retriever, ReRanker)
|
203 |
-
logger.info("μ 체 μ΄κΈ°ν μλ£")
|
204 |
-
except ImportError:
|
205 |
-
logger.warning("μλ² λ© μΊμ λͺ¨λμ μ°Ύμ μ μμ΅λλ€. μ 체 μ΄κΈ°νλ₯Ό μ§νν©λλ€.")
|
206 |
-
retriever = init_retriever(app, base_retriever, retriever, ReRanker)
|
207 |
-
|
208 |
-
logger.info("μ± μ΄κΈ°ν μλ£ (λͺ¨λ μ»΄ν¬λνΈ μ€λΉλ¨)")
|
209 |
-
except Exception as e:
|
210 |
-
logger.error(f"μ± λ°±κ·ΈλΌμ΄λ μ΄κΈ°ν μ€ μ¬κ°ν μ€λ₯ λ°μ: {e}", exc_info=True)
|
211 |
-
# μ΄κΈ°ν μ€ν¨ μ κΈ°λ³Έ κ°μ²΄ μμ±
|
212 |
-
if base_retriever is None:
|
213 |
-
base_retriever = MockComponent()
|
214 |
-
if hasattr(base_retriever, 'documents'):
|
215 |
-
base_retriever.documents = []
|
216 |
-
if retriever is None:
|
217 |
-
retriever = MockComponent()
|
218 |
-
if not hasattr(retriever, 'search'):
|
219 |
-
retriever.search = lambda query, **kwargs: []
|
220 |
-
|
221 |
-
logger.warning("μ΄κΈ°ν μ€ μ€λ₯κ° μμ§λ§ μ±μ κ³μ μ¬μ© κ°λ₯ν©λλ€.")
|
222 |
-
|
223 |
-
|
224 |
-
# --- λΌμ°νΈ λ±λ‘ ---
|
225 |
-
def register_all_routes():
|
226 |
-
try:
|
227 |
-
# κΈ°λ³Έ λΌμ°νΈ λ±λ‘
|
228 |
-
from app.app_routes import register_routes
|
229 |
-
register_routes(
|
230 |
-
app, login_required, llm_interface, retriever, stt_client,
|
231 |
-
DocumentProcessor, base_retriever, app_ready,
|
232 |
-
ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL
|
233 |
-
)
|
234 |
-
|
235 |
-
# μ₯μΉ κ΄λ¦¬ λΌμ°νΈ λ±λ‘
|
236 |
-
from app.app_device_routes import register_device_routes
|
237 |
-
register_device_routes(app, login_required, DEVICE_SERVER_URL)
|
238 |
-
|
239 |
-
logger.info("λͺ¨λ λΌμ°νΈ λ±λ‘ μλ£")
|
240 |
-
except ImportError as e:
|
241 |
-
logger.error(f"λΌμ°νΈ λͺ¨λ μν¬νΈ μ€ν¨: {e}")
|
242 |
-
except Exception as e:
|
243 |
-
logger.error(f"λΌμ°νΈ λ±λ‘ μ€ μ€λ₯ λ°μ: {e}", exc_info=True)
|
244 |
-
|
245 |
-
|
246 |
-
# --- μ± μ΄κΈ°ν λ° μ€ν ---
|
247 |
-
def initialize_app():
|
248 |
-
# λ°±κ·ΈλΌμ΄λ μ΄κΈ°ν μ€λ λ μμ
|
249 |
-
init_thread = threading.Thread(target=background_init)
|
250 |
-
init_thread.daemon = True
|
251 |
-
init_thread.start()
|
252 |
-
|
253 |
-
# λΌμ°νΈ λ±λ‘
|
254 |
-
register_all_routes()
|
255 |
-
|
256 |
-
logger.info("μ± μ΄κΈ°ν μλ£")
|
257 |
-
|
258 |
-
|
259 |
-
# μ± μ΄κΈ°ν μ€ν
|
260 |
-
initialize_app()
|
261 |
-
|
262 |
-
|
263 |
-
# --- μ± μ€ν (μ§μ μ€ν μ) ---
|
264 |
-
if __name__ == '__main__':
|
265 |
-
logger.info("Flask μ±μ μ§μ μ€νν©λλ€ (κ°λ°μ© μλ²).")
|
266 |
-
port = int(os.environ.get("PORT", 7860))
|
267 |
-
logger.info(f"μλ²λ₯Ό http://0.0.0.0:{port} μμ μμν©λλ€.")
|
268 |
-
app.run(debug=True, host='0.0.0.0', port=port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|