tktm8 commited on
Commit
73c4275
·
verified ·
1 Parent(s): 1194bfe

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +650 -0
app.py ADDED
@@ -0,0 +1,650 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EmpathemeBot - Hugging Face Spaces用統合版Streamlitアプリ(セキュア版)
3
+ APIキーをセッション単位で管理し、ユーザー間で共有されないようにする
4
+ """
5
+
6
+ import html
7
+ import logging
8
+ import re
9
+ import time
10
+ import uuid
11
+ from datetime import datetime
12
+ from typing import List, Dict, Optional
13
+ from pathlib import Path
14
+ import sys
15
+
16
+ import streamlit as st
17
+ from dotenv import load_dotenv
18
+ import os
19
+
20
+ # 環境変数の読み込み
21
+ load_dotenv()
22
+
23
+ # srcディレクトリをパスに追加
24
+ sys.path.append(str(Path(__file__).parent))
25
+
26
+ # ロギング設定
27
+ logging.basicConfig(
28
+ level=logging.INFO,
29
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # 設定
34
+ st.set_page_config(
35
+ page_title="EmpathemeBot QA System",
36
+ page_icon="🤖",
37
+ layout="wide",
38
+ initial_sidebar_state="collapsed",
39
+ menu_items={}
40
+ )
41
+
42
+ class SecureQAChain:
43
+ """APIキーを直接渡すQAChainラッパー"""
44
+
45
+ def __init__(self, api_key: str, persist_dir: str = "data/vector_store", **kwargs):
46
+ """
47
+ APIキーを直接受け取ってQAChainを初期化
48
+
49
+ Args:
50
+ api_key: OpenAI APIキー
51
+ persist_dir: ベクトルストアのパス
52
+ **kwargs: その他のQAChain用パラメータ
53
+ """
54
+ self.api_key = api_key
55
+ self.persist_dir = persist_dir
56
+ self.conversation_history = []
57
+ self.qa_chain_initialized = False
58
+
59
+ # QAChainの遅延初期化用のパラメータを保存
60
+ self.chain_params = kwargs
61
+
62
+ # ベクトルストアの存在確認
63
+ self.vector_store_exists = Path(persist_dir).exists()
64
+
65
+ if not self.vector_store_exists:
66
+ logger.warning("ベクトルストアが見つかりません。デモモードで動作します。")
67
+
68
+ def _init_qa_chain(self):
69
+ """実際のQAChainを初期化(遅延初期化)"""
70
+ if not self.qa_chain_initialized:
71
+ try:
72
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
73
+ from langchain_chroma import Chroma
74
+
75
+ if self.vector_store_exists:
76
+ # ベクトルストアがある場合
77
+ embeddings = OpenAIEmbeddings(
78
+ model="text-embedding-3-small",
79
+ openai_api_key=self.api_key
80
+ )
81
+ self.db = Chroma(
82
+ persist_directory=self.persist_dir,
83
+ embedding_function=embeddings
84
+ )
85
+ self.retriever = self.db.as_retriever(
86
+ search_type='similarity',
87
+ search_kwargs={'k': 5}
88
+ )
89
+
90
+ # LLMを初期化(APIキーを直接渡す)
91
+ self.llm = ChatOpenAI(
92
+ model_name="gpt-4o-mini",
93
+ temperature=0.7,
94
+ openai_api_key=self.api_key
95
+ )
96
+
97
+ self.qa_chain_initialized = True
98
+ logger.info("QAChain初期化完了")
99
+
100
+ except Exception as e:
101
+ logger.error(f"QAChain初期化エラー: {e}")
102
+ raise
103
+
104
+ def ask_with_history(self, question: str):
105
+ """質問に回答(会話履歴を考慮)"""
106
+ self._init_qa_chain() # 遅延初期化
107
+
108
+ try:
109
+ from langchain.schema import HumanMessage, SystemMessage
110
+
111
+ # 会話履歴を追加
112
+ self.conversation_history.append(f"Q: {question}")
113
+
114
+ # システムメッセージ
115
+ messages = [
116
+ SystemMessage(content="あなたは英語学習をサポートするKurageSan®という親切なアシスタントです。"),
117
+ ]
118
+
119
+ # ベクトルストアが使える場合はコンテキストを追加
120
+ if self.vector_store_exists and hasattr(self, 'retriever'):
121
+ try:
122
+ # 関連文書を検索
123
+ docs = self.retriever.invoke(question)
124
+ if docs:
125
+ context = "\n".join([doc.page_content[:500] for doc in docs[:3]]) # 上位3件を使用
126
+ messages.append(SystemMessage(content=f"以下の情報を参考にして回答してください:\n{context}"))
127
+ except Exception as e:
128
+ logger.warning(f"文書検索エラー: {e}")
129
+
130
+ # 質問を追加
131
+ messages.append(HumanMessage(content=question))
132
+
133
+ # 回答を生成
134
+ response = self.llm.invoke(messages)
135
+ answer = response.content
136
+
137
+ # 会話履歴に追加
138
+ self.conversation_history.append(f"A: {answer}")
139
+
140
+ # 文書情報(ダミー)
141
+ source_docs = []
142
+ if self.vector_store_exists and hasattr(self, 'retriever'):
143
+ try:
144
+ source_docs = self.retriever.invoke(question)[:3]
145
+ except:
146
+ pass
147
+
148
+ return answer, source_docs
149
+
150
+ except Exception as e:
151
+ logger.error(f"質問処理エラー: {e}")
152
+ # エラー時のフォールバック
153
+ answer = f"申し訳ございません。エラーが発生しました: {str(e)}"
154
+ self.conversation_history.append(f"A: {answer}")
155
+ return answer, []
156
+
157
+ def clear_history(self):
158
+ """会話履歴をクリア"""
159
+ self.conversation_history = []
160
+
161
+ def get_history(self):
162
+ """会話履歴を取得"""
163
+ return "\n".join(self.conversation_history)
164
+
165
+
166
+ class EmpathemeBotUI:
167
+ """Hugging Face Spaces用セキュア版EmpathemeBot UIクラス"""
168
+
169
+ def __init__(self):
170
+ # セッション状態の初期化(APIキーは保存しない)
171
+ if 'session_id' not in st.session_state:
172
+ st.session_state.session_id = str(uuid.uuid4())
173
+ if 'messages' not in st.session_state:
174
+ st.session_state.messages = []
175
+ if 'qa_chain' not in st.session_state:
176
+ st.session_state.qa_chain = None
177
+ if 'last_activity' not in st.session_state:
178
+ st.session_state.last_activity = datetime.now()
179
+ if 'vector_store_initialized' not in st.session_state:
180
+ st.session_state.vector_store_initialized = False
181
+ # APIキー関連の状態(セッション単位)
182
+ if 'current_api_key' not in st.session_state:
183
+ st.session_state.current_api_key = "" # 現在のセッションのAPIキー
184
+
185
+ def initialize_qa_chain(self, api_key: str) -> bool:
186
+ """
187
+ QAChainを初期化(APIキーを直接渡す)
188
+
189
+ Args:
190
+ api_key: OpenAI APIキー
191
+
192
+ Returns:
193
+ 初期化成功の場合True
194
+ """
195
+ try:
196
+ # ベクトルストアのパスを確認
197
+ vector_store_path = Path("data/vector_store")
198
+
199
+ # QAChainの初期化(APIキーを直接渡す)
200
+ logger.info("QAChainを初期化中...")
201
+
202
+ # SecureQAChainを使用(APIキーを直接渡す)
203
+ st.session_state.qa_chain = SecureQAChain(
204
+ api_key=api_key,
205
+ persist_dir=str(vector_store_path),
206
+ verbose=False,
207
+ max_history_turns=10,
208
+ max_history_chars=10000
209
+ )
210
+
211
+ st.session_state.vector_store_initialized = True
212
+ st.session_state.current_api_key = api_key # 現在のセッションのAPIキーを記録
213
+ logger.info("QAChain初期化完了")
214
+ return True
215
+
216
+ except Exception as e:
217
+ logger.error(f"QAChain初期化エラー: {e}")
218
+ st.error(f"初期化エラー: {str(e)}")
219
+ return False
220
+
221
+ def ask_question(self, question: str) -> Optional[Dict]:
222
+ """
223
+ 質問を処理して回答を取得
224
+
225
+ Args:
226
+ question: ユーザーの質問
227
+
228
+ Returns:
229
+ 回答データ
230
+ """
231
+ try:
232
+ if st.session_state.qa_chain is None:
233
+ st.error("システムが初期化されていません。APIキーを入力してください。")
234
+ return None
235
+
236
+ # 質問処理
237
+ logger.info(f"質問処理開始: {question[:100]}...")
238
+ answer, source_docs = st.session_state.qa_chain.ask_with_history(question)
239
+
240
+ # ソースURLを抽出(重複除去)
241
+ source_urls = []
242
+ for doc in source_docs:
243
+ if hasattr(doc, 'metadata'):
244
+ url = doc.metadata.get('source_url', '')
245
+ if url and url not in source_urls:
246
+ source_urls.append(url)
247
+
248
+ result = {
249
+ "answer": answer,
250
+ "source_count": len(source_docs),
251
+ "source_urls": source_urls
252
+ }
253
+
254
+ logger.info(f"回答生成成功: {len(source_docs)}件のソース参照")
255
+ return result
256
+
257
+ except Exception as e:
258
+ logger.error(f"エラー発生: {e}")
259
+ st.error(f"予期しないエラーが発生しました: {str(e)}")
260
+ return None
261
+
262
+ def clear_history(self):
263
+ """会話履歴をクリア"""
264
+ try:
265
+ if st.session_state.qa_chain:
266
+ st.session_state.qa_chain.clear_history()
267
+ st.session_state.messages = []
268
+ st.success("会話履歴をクリアしました")
269
+ logger.info("履歴クリア成功")
270
+ except Exception as e:
271
+ logger.error(f"履歴クリアエラー: {e}")
272
+ st.error("エラーが発生しました")
273
+
274
+ def create_new_session(self):
275
+ """新しいセッションIDを生成"""
276
+ st.session_state.session_id = str(uuid.uuid4())
277
+ st.session_state.messages = []
278
+ st.session_state.qa_chain = None # QAChainもリセット
279
+ st.session_state.current_api_key = "" # APIキーもクリア
280
+ st.session_state.vector_store_initialized = False
281
+ logger.info(f"新しいセッション作成: {st.session_state.session_id}")
282
+
283
+ def main():
284
+ """メイン関数"""
285
+
286
+ # カスタムCSS
287
+ st.markdown("""
288
+ <style>
289
+ /* メインコンテナのスタイル */
290
+ .main {
291
+ padding-top: 1rem;
292
+ max-width: 1000px;
293
+ margin: 0 auto;
294
+ }
295
+
296
+ .block-container {
297
+ padding: 1rem 2rem;
298
+ max-width: 100%;
299
+ }
300
+
301
+ /* チャット入力のスタイル */
302
+ .stChatInput {
303
+ border: none !important;
304
+ box-shadow: none !important;
305
+ background: transparent !important;
306
+ position: fixed;
307
+ bottom: 0;
308
+ padding-bottom: 1rem;
309
+ background: white !important;
310
+ z-index: 999;
311
+ }
312
+
313
+ /* チャット入力のテキストエリア */
314
+ .stChatInput textarea {
315
+ font-size: 14px;
316
+ border: 1px solid #E5E7EB !important;
317
+ border-radius: 8px !important;
318
+ padding: 0.6rem 1rem !important;
319
+ background: #FAFAFA !important;
320
+ transition: all 0.2s ease;
321
+ }
322
+
323
+ .stChatInput textarea:focus {
324
+ background: white !important;
325
+ border-color: #4F46E5 !important;
326
+ outline: none !important;
327
+ box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1) !important;
328
+ }
329
+
330
+ /* ボタンのスタイル */
331
+ .stButton > button {
332
+ background: #4F46E5;
333
+ color: white;
334
+ border: none;
335
+ border-radius: 6px;
336
+ padding: 0.5rem 1rem;
337
+ font-weight: 500;
338
+ font-size: 13px;
339
+ transition: all 0.15s ease;
340
+ }
341
+
342
+ .stButton > button:hover {
343
+ background: #4338CA;
344
+ }
345
+
346
+ /* タイトルのスタイル */
347
+ h1 {
348
+ color: #111827;
349
+ font-weight: 600;
350
+ text-align: center;
351
+ font-size: 1.75rem;
352
+ margin-bottom: 0.5rem;
353
+ }
354
+
355
+ /* サイドバーのスタイル */
356
+ section[data-testid="stSidebar"] {
357
+ background: #FAFAFB;
358
+ }
359
+
360
+ /* 吹き出し内のコンテンツスタイル */
361
+ .bubble-content {
362
+ font-family: inherit;
363
+ font-size: inherit;
364
+ white-space: pre-wrap;
365
+ word-wrap: break-word;
366
+ margin: 0;
367
+ padding: 0;
368
+ color: inherit;
369
+ }
370
+ </style>
371
+ """, unsafe_allow_html=True)
372
+
373
+ # UIインスタンス作成
374
+ bot = EmpathemeBotUI()
375
+
376
+ # サイドバー設定
377
+ with st.sidebar:
378
+ st.markdown("## 設定")
379
+
380
+ # APIキー入力欄(セッションごとに必要)
381
+ st.markdown("### OpenAI API キー")
382
+
383
+ # 現在のAPIキー状態を表示
384
+ if st.session_state.current_api_key:
385
+ st.success("✅ APIキー設定済み")
386
+ # APIキーの一部を表示(セキュリティのため)
387
+ masked_key = st.session_state.current_api_key[:7] + "..." + st.session_state.current_api_key[-4:]
388
+ st.caption(f"現在のキー: {masked_key}")
389
+ else:
390
+ st.info("⚠️ APIキーを入力してください")
391
+
392
+ # APIキー入力フォーム
393
+ with st.form("api_key_form"):
394
+ api_key_input = st.text_input(
395
+ "APIキーを入力",
396
+ type="password",
397
+ placeholder="sk-...",
398
+ help="このセッション専用のAPIキーです。ブラウザを閉じると消去されます。",
399
+ key="api_key_input_field"
400
+ )
401
+
402
+ submit_button = st.form_submit_button("APIキーを設定", use_container_width=True)
403
+
404
+ if submit_button and api_key_input:
405
+ if api_key_input.startswith("sk-"):
406
+ # QAChainを初期化
407
+ if bot.initialize_qa_chain(api_key_input):
408
+ st.success("✅ APIキーが設定されました")
409
+ st.rerun()
410
+ else:
411
+ st.error("❌ 初期化に失敗しました")
412
+ else:
413
+ st.error("有効なAPIキー(sk-で始まる)を入力してください")
414
+
415
+ st.markdown("---")
416
+
417
+ # コントロールボタン
418
+ st.markdown("### コントロール")
419
+ if st.button("新しいチャット", use_container_width=True):
420
+ bot.create_new_session()
421
+ st.rerun()
422
+
423
+ if st.button("履歴クリア", use_container_width=True):
424
+ bot.clear_history()
425
+ st.rerun()
426
+
427
+ st.markdown("---")
428
+
429
+ # ステータス
430
+ st.markdown("### ステータス")
431
+ if st.session_state.vector_store_initialized:
432
+ st.success("システム準備完了")
433
+ else:
434
+ st.info("システム待機中")
435
+
436
+ # セッション情報
437
+ st.markdown("---")
438
+ st.caption(f"セッションID: {st.session_state.session_id[:8]}...")
439
+
440
+ # メインヘッダー
441
+ st.markdown(
442
+ """
443
+ <div style='text-align: center; margin-bottom: 2rem;'>
444
+ <h1 style='margin-bottom: 0.25rem;'>🤖 EmpathemeBot</h1>
445
+ <p style='color: #6B7280; font-size: 0.9rem;'>Potionベースの質問応答システム</p>
446
+ </div>
447
+ """,
448
+ unsafe_allow_html=True
449
+ )
450
+
451
+ # APIキー未入力時の警告メッセージ
452
+ if not st.session_state.current_api_key:
453
+ st.markdown(
454
+ """
455
+ <div style="background: #FEF3C7; border: 2px solid #F59E0B; border-radius: 12px; padding: 1.5rem; margin: 2rem 0;">
456
+ <h3 style="color: #92400E; margin-top: 0;">APIキーの入力が必要です</h3>
457
+ <p style="color: #78350F; margin-bottom: 1rem;">
458
+ EmpathemeBotを使用するには、OpenAI APIキーが必要です。
459
+ </p>
460
+ <ol style="color: #78350F; margin-left: 1.5rem;">
461
+ <li>左上の「>」ボタンをクリックしてサイドバーを開く</li>
462
+ <li>「OpenAI API キー」セクションにAPIキー(sk-...)を入力</li>
463
+ <li>「APIキーを設定」ボタンをクリック</li>
464
+ </ol>
465
+ <p style="color: #78350F; font-size: 0.9rem; margin-top: 1rem;">
466
+ APIキーは <a href="https://platform.openai.com/api-keys" target="__blank" style="color: #F59E0B;">OpenAIのダッシュボード</a> から取得できます。
467
+ </p>
468
+ <p style="color: #78350F; font-size: 0.85rem; margin-top: 0.5rem; font-style: italic;">
469
+ ※ APIキーは各ブラウザセッション専用です。他のユーザーと共有されません。
470
+ </p>
471
+ </div>
472
+ """,
473
+ unsafe_allow_html=True
474
+ )
475
+ st.stop()
476
+
477
+ # ウェルカムメッセージ(初回のみ)
478
+ if len(st.session_state.messages) == 0:
479
+ st.markdown(
480
+ """
481
+ <div style="text-align: center; padding: 3rem 0; color: #6B7280;">
482
+ <p style="font-size: 0.95rem;">こんにちは、KurageSan®だよ!何か英語学習に関して困っていることはありますか?</p>
483
+ </div>
484
+ """,
485
+ unsafe_allow_html=True
486
+ )
487
+
488
+ # チャット履歴の表示
489
+ for message in st.session_state.messages:
490
+ if message["role"] == "user":
491
+ # ユーザーメッセージ(右側)
492
+ st.markdown(
493
+ f"""
494
+ <div style="display: flex; justify-content: flex-end; margin: 1rem 0; padding-right: 1rem;">
495
+ <div style="background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%);
496
+ color: white;
497
+ padding: 0.75rem 1.25rem;
498
+ border-radius: 18px 18px 4px 18px;
499
+ max-width: 60%;
500
+ box-shadow: 0 2px 10px rgba(79, 70, 229, 0.2);
501
+ word-wrap: break-word;">
502
+ <pre class="bubble-content">{html.escape(message['content'])}</pre>
503
+ <div style="font-size: 0.7rem; opacity: 0.8; margin-top: 0.3rem; text-align: right;">
504
+ {message.get('timestamp', '')}
505
+ </div>
506
+ </div>
507
+ </div>
508
+ """,
509
+ unsafe_allow_html=True
510
+ )
511
+ else:
512
+ # アシスタントメッセージ(左側)
513
+ st.markdown(
514
+ f"""
515
+ <div style="display: flex; justify-content: flex-start; margin: 1rem 0; padding-left: 1rem;">
516
+ <div style="background: #F3F4F6;
517
+ color: #111827;
518
+ padding: 0.75rem 1.25rem;
519
+ border-radius: 18px 18px 18px 4px;
520
+ max-width: 60%;
521
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
522
+ word-wrap: break-word;">
523
+ <pre class="bubble-content">{html.escape(message['content'])}</pre>
524
+ <div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem;">
525
+ {message.get('timestamp', '')}
526
+ </div>
527
+ </div>
528
+ </div>
529
+ """,
530
+ unsafe_allow_html=True
531
+ )
532
+
533
+ # チャット入力
534
+ if prompt := st.chat_input("質問を入力してください...", key="chat_input"):
535
+ # タイムスタンプを追加
536
+ timestamp = datetime.now().strftime("%H:%M")
537
+
538
+ # ユーザーメッセージを追加
539
+ st.session_state.messages.append({
540
+ "role": "user",
541
+ "content": prompt,
542
+ "timestamp": timestamp
543
+ })
544
+
545
+ # ユーザーメッセージを表示
546
+ st.markdown(
547
+ f"""
548
+ <div style="display: flex; justify-content: flex-end; margin: 1rem 0; padding-right: 1rem;">
549
+ <div style="background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%);
550
+ color: white;
551
+ padding: 0.75rem 1.25rem;
552
+ border-radius: 18px 18px 4px 18px;
553
+ max-width: 60%;
554
+ box-shadow: 0 2px 10px rgba(79, 70, 229, 0.2);
555
+ word-wrap: break-word;">
556
+ <pre class="bubble-content">{html.escape(prompt)}</pre>
557
+ <div style="font-size: 0.7rem; opacity: 0.8; margin-top: 0.3rem; text-align: right;">
558
+ {timestamp}
559
+ </div>
560
+ </div>
561
+ </div>
562
+ """,
563
+ unsafe_allow_html=True
564
+ )
565
+
566
+ # アシスタントの応答を生成
567
+ with st.spinner("考えています..."):
568
+ response_timestamp = datetime.now().strftime("%H:%M")
569
+ response_data = bot.ask_question(prompt)
570
+
571
+ if response_data:
572
+ answer = response_data["answer"]
573
+
574
+ # メッセージ履歴に追加
575
+ st.session_state.messages.append({
576
+ "role": "assistant",
577
+ "content": answer,
578
+ "timestamp": response_timestamp,
579
+ "metadata": {
580
+ "source_count": response_data.get("source_count", 0)
581
+ }
582
+ })
583
+
584
+ # アシスタントメッセージを表示
585
+ st.markdown(
586
+ f"""
587
+ <div style="display: flex; justify-content: flex-start; margin: 1rem 0; padding-left: 1rem;">
588
+ <div style="background: #F3F4F6;
589
+ color: #111827;
590
+ padding: 0.75rem 1.25rem;
591
+ border-radius: 18px 18px 18px 4px;
592
+ max-width: 60%;
593
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
594
+ word-wrap: break-word;">
595
+ <pre class="bubble-content">{html.escape(answer)}</pre>
596
+ <div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem;">
597
+ {response_timestamp}
598
+ </div>
599
+ </div>
600
+ </div>
601
+ """,
602
+ unsafe_allow_html=True
603
+ )
604
+ else:
605
+ # エラーの場合
606
+ error_message = "申し訳ございません。回答の生成に失敗しました。もう一度お試しください。"
607
+
608
+ st.session_state.messages.append({
609
+ "role": "assistant",
610
+ "content": error_message,
611
+ "timestamp": response_timestamp
612
+ })
613
+
614
+ # エラーメッセージを表示
615
+ st.markdown(
616
+ f"""
617
+ <div style="display: flex; justify-content: flex-start; margin: 1rem 0; padding-left: 1rem;">
618
+ <div style="background: #F3F4F6;
619
+ color: #111827;
620
+ padding: 0.75rem 1.25rem;
621
+ border-radius: 18px 18px 18px 4px;
622
+ max-width: 60%;
623
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
624
+ word-wrap: break-word;">
625
+ <pre class="bubble-content">{html.escape(error_message)}</pre>
626
+ <div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem;">
627
+ {response_timestamp}
628
+ </div>
629
+ </div>
630
+ </div>
631
+ """,
632
+ unsafe_allow_html=True
633
+ )
634
+
635
+ # アクティビティを更新
636
+ st.session_state.last_activity = datetime.now()
637
+
638
+ # フッター
639
+ st.markdown(
640
+ f"""
641
+ <div style="text-align: center; margin-top: 3rem; padding: 1rem 0;
642
+ border-top: 1px solid #E5E7EB; color: #9CA3AF; font-size: 0.8rem;">
643
+ EmpathemeBot · セッション: {st.session_state.session_id[:8]}
644
+ </div>
645
+ """,
646
+ unsafe_allow_html=True
647
+ )
648
+
649
+ if __name__ == "__main__":
650
+ main()