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

Delete app.py

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