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

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -650
app.py DELETED
@@ -1,650 +0,0 @@
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()