tktm8 commited on
Commit
e0e0864
·
verified ·
1 Parent(s): 0cf6ba8

Upload app.py

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