tktm8 commited on
Commit
cdf7866
·
verified ·
1 Parent(s): 003c2e5

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -660
app.py DELETED
@@ -1,660 +0,0 @@
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-4o-mini",
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
- </div>
482
- """,
483
- unsafe_allow_html=True
484
- )
485
- st.stop()
486
-
487
- # ウェルカムメッセージ(初回のみ)
488
- if len(st.session_state.messages) == 0:
489
- st.markdown(
490
- """
491
- <div style="text-align: center; padding: 3rem 0; color: #6B7280;">
492
- <p style="font-size: 0.95rem;">こんにちは、KurageSan®だよ!何か英語学習に関して困っていることはありますか?</p>
493
- </div>
494
- """,
495
- unsafe_allow_html=True
496
- )
497
-
498
- # チャット履歴の表示
499
- for message in st.session_state.messages:
500
- if message["role"] == "user":
501
- # ユーザーメッセージ(右側)
502
- st.markdown(
503
- f"""
504
- <div style="display: flex; justify-content: flex-end; margin: 1rem 0; padding-right: 1rem;">
505
- <div style="background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%);
506
- color: white;
507
- padding: 0.75rem 1.25rem;
508
- border-radius: 18px 18px 4px 18px;
509
- max-width: 60%;
510
- box-shadow: 0 2px 10px rgba(79, 70, 229, 0.2);
511
- word-wrap: break-word;">
512
- <pre class="bubble-content">{html.escape(message['content'])}</pre>
513
- <div style="font-size: 0.7rem; opacity: 0.8; margin-top: 0.3rem; text-align: right;">
514
- {message.get('timestamp', '')}
515
- </div>
516
- </div>
517
- </div>
518
- """,
519
- unsafe_allow_html=True
520
- )
521
- else:
522
- # アシスタントメッセージ(左側)
523
- st.markdown(
524
- f"""
525
- <div style="display: flex; justify-content: flex-start; margin: 1rem 0; padding-left: 1rem;">
526
- <div style="background: #F3F4F6;
527
- color: #111827;
528
- padding: 0.75rem 1.25rem;
529
- border-radius: 18px 18px 18px 4px;
530
- max-width: 60%;
531
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
532
- word-wrap: break-word;">
533
- <pre class="bubble-content">{html.escape(message['content'])}</pre>
534
- <div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem;">
535
- {message.get('timestamp', '')}
536
- </div>
537
- </div>
538
- </div>
539
- """,
540
- unsafe_allow_html=True
541
- )
542
-
543
- # チャット入力
544
- if prompt := st.chat_input("質問を入力してください...", key="chat_input"):
545
- # タイムスタンプを追加
546
- timestamp = datetime.now().strftime("%H:%M")
547
-
548
- # ユーザーメッセージを追加
549
- st.session_state.messages.append({
550
- "role": "user",
551
- "content": prompt,
552
- "timestamp": timestamp
553
- })
554
-
555
- # ユーザーメッセージを表示
556
- st.markdown(
557
- f"""
558
- <div style="display: flex; justify-content: flex-end; margin: 1rem 0; padding-right: 1rem;">
559
- <div style="background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%);
560
- color: white;
561
- padding: 0.75rem 1.25rem;
562
- border-radius: 18px 18px 4px 18px;
563
- max-width: 60%;
564
- box-shadow: 0 2px 10px rgba(79, 70, 229, 0.2);
565
- word-wrap: break-word;">
566
- <pre class="bubble-content">{html.escape(prompt)}</pre>
567
- <div style="font-size: 0.7rem; opacity: 0.8; margin-top: 0.3rem; text-align: right;">
568
- {timestamp}
569
- </div>
570
- </div>
571
- </div>
572
- """,
573
- unsafe_allow_html=True
574
- )
575
-
576
- # アシスタントの応答を生成
577
- with st.spinner("考えています..."):
578
- response_timestamp = datetime.now().strftime("%H:%M")
579
- response_data = bot.ask_question(prompt)
580
-
581
- if response_data:
582
- answer = response_data["answer"]
583
-
584
- # メッセージ履歴に追加
585
- st.session_state.messages.append({
586
- "role": "assistant",
587
- "content": answer,
588
- "timestamp": response_timestamp,
589
- "metadata": {
590
- "source_count": response_data.get("source_count", 0)
591
- }
592
- })
593
-
594
- # アシスタントメッセージを表示
595
- st.markdown(
596
- f"""
597
- <div style="display: flex; justify-content: flex-start; margin: 1rem 0; padding-left: 1rem;">
598
- <div style="background: #F3F4F6;
599
- color: #111827;
600
- padding: 0.75rem 1.25rem;
601
- border-radius: 18px 18px 18px 4px;
602
- max-width: 60%;
603
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
604
- word-wrap: break-word;">
605
- <pre class="bubble-content">{html.escape(answer)}</pre>
606
- <div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem;">
607
- {response_timestamp}
608
- </div>
609
- </div>
610
- </div>
611
- """,
612
- unsafe_allow_html=True
613
- )
614
- else:
615
- # エラーの場合
616
- error_message = "申し訳ございません。回答の生成に失敗しました。もう一度お試しください。"
617
-
618
- st.session_state.messages.append({
619
- "role": "assistant",
620
- "content": error_message,
621
- "timestamp": response_timestamp
622
- })
623
-
624
- # エラーメッセージを表示
625
- st.markdown(
626
- f"""
627
- <div style="display: flex; justify-content: flex-start; margin: 1rem 0; padding-left: 1rem;">
628
- <div style="background: #F3F4F6;
629
- color: #111827;
630
- padding: 0.75rem 1.25rem;
631
- border-radius: 18px 18px 18px 4px;
632
- max-width: 60%;
633
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
634
- word-wrap: break-word;">
635
- <pre class="bubble-content">{html.escape(error_message)}</pre>
636
- <div style="font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem;">
637
- {response_timestamp}
638
- </div>
639
- </div>
640
- </div>
641
- """,
642
- unsafe_allow_html=True
643
- )
644
-
645
- # アクティビティを更新
646
- st.session_state.last_activity = datetime.now()
647
-
648
- # フッター
649
- st.markdown(
650
- f"""
651
- <div style="text-align: center; margin-top: 3rem; padding: 1rem 0;
652
- border-top: 1px solid #E5E7EB; color: #9CA3AF; font-size: 0.8rem;">
653
- EmpathemeBot · セッション: {st.session_state.session_id[:8]}
654
- </div>
655
- """,
656
- unsafe_allow_html=True
657
- )
658
-
659
- if __name__ == "__main__":
660
- main()