Spaces:
Sleeping
Sleeping
Update app.pyo
Browse files
app.pyo
CHANGED
|
@@ -7,126 +7,270 @@ import hashlib
|
|
| 7 |
import json
|
| 8 |
|
| 9 |
# ===============================
|
| 10 |
-
#
|
| 11 |
# ===============================
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# For Hugging Face Spaces
|
| 15 |
-
from huggingface_hub import HfFolder
|
| 16 |
-
api_key = HfFolder.get_token()
|
| 17 |
-
if not api_key:
|
| 18 |
-
# Try environment variable
|
| 19 |
-
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
| 20 |
-
except ImportError:
|
| 21 |
-
# huggingface_hub not installed
|
| 22 |
-
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
| 23 |
-
except:
|
| 24 |
-
# Any other error
|
| 25 |
-
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
| 26 |
-
|
| 27 |
-
# ===============================
|
| 28 |
-
# HUGGING FACE COMPATIBLE CACHE CLASS
|
| 29 |
-
# ===============================
|
| 30 |
-
class HybridCache:
|
| 31 |
-
def __init__(self, ttl_hours=24, max_entries=100):
|
| 32 |
"""
|
| 33 |
-
|
| 34 |
-
- No file system access needed
|
| 35 |
-
- Works within session only
|
| 36 |
"""
|
| 37 |
-
self.
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
def get(self, cache_key):
|
| 42 |
-
"""Get cached answer
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
if isinstance(created_at, str):
|
| 48 |
-
created_at = datetime.fromisoformat(created_at)
|
| 49 |
-
|
| 50 |
-
# Check TTL (in hours for Hugging Face)
|
| 51 |
-
if created_at and (datetime.now() - created_at).seconds < (self.ttl_hours * 3600):
|
| 52 |
-
# Update access time
|
| 53 |
-
entry['last_accessed'] = datetime.now().isoformat()
|
| 54 |
entry['access_count'] = entry.get('access_count', 0) + 1
|
|
|
|
| 55 |
return entry
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
return None
|
| 61 |
|
| 62 |
def set(self, cache_key, data):
|
| 63 |
-
"""
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
|
|
|
| 69 |
|
| 70 |
-
#
|
| 71 |
-
if len(self.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
def clear_expired(self):
|
| 82 |
-
"""
|
| 83 |
-
current_time = datetime.now()
|
| 84 |
expired_keys = []
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
created_at = entry.get('created_at')
|
| 88 |
-
if isinstance(created_at, str):
|
| 89 |
-
created_at = datetime.fromisoformat(created_at)
|
| 90 |
-
|
| 91 |
-
if created_at and (current_time - created_at).seconds >= (self.ttl_hours * 3600):
|
| 92 |
expired_keys.append(key)
|
| 93 |
|
| 94 |
for key in expired_keys:
|
| 95 |
-
del self.
|
| 96 |
|
| 97 |
return len(expired_keys)
|
| 98 |
|
| 99 |
def clear_all(self):
|
| 100 |
"""Clear all cache entries"""
|
| 101 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
def get_stats(self):
|
| 104 |
"""Get cache statistics"""
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
return {
|
| 121 |
-
'total_entries':
|
| 122 |
-
'
|
| 123 |
-
'
|
| 124 |
-
'
|
| 125 |
-
'
|
| 126 |
-
'
|
| 127 |
-
'
|
| 128 |
}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
# Page config - must be first Streamlit command
|
| 131 |
st.set_page_config(
|
| 132 |
page_title="SEBA দশম শ্ৰেণীৰ AI টিউটাৰ",
|
|
@@ -470,16 +614,16 @@ SEBA_CURRICULUM = {
|
|
| 470 |
"পাঠ ১০": "সাহিত্যৰ ৰূপ"
|
| 471 |
},
|
| 472 |
"📘 হিন্দী (Hindi)": {
|
| 473 |
-
"
|
| 474 |
-
"
|
| 475 |
-
"
|
| 476 |
-
"
|
| 477 |
-
"
|
| 478 |
-
"
|
| 479 |
-
"
|
| 480 |
-
"
|
| 481 |
-
"
|
| 482 |
-
"
|
| 483 |
}
|
| 484 |
}
|
| 485 |
|
|
@@ -514,7 +658,7 @@ SUBJECT_PROMPTS = {
|
|
| 514 |
"base_prompt": """তুমি এজন বিজ্ঞান শিক্ষক। SEBA দশম শ্ৰেণীৰ বিজ্ঞানৰ {chapter_name} অধ্যায়ৰ সকলো বৈজ্ঞানিক ধাৰণা, প্ৰক্ৰয়া, আৰু নীতি তুমি জানা।
|
| 515 |
|
| 516 |
**বিজ্ঞানৰ বিশেষ নিৰ্দেশনা:**
|
| 517 |
-
১. **বৈজ্ঞানিক প্ৰক্ৰ
|
| 518 |
২. **ৰাসায়নিক সমীকৰণ সঠিকভাৱে দিবা**
|
| 519 |
৩. **জীৱবিজ্ঞানৰ চিত্ৰ/ৰেখাচিত্ৰৰ বৰ্ণনা দিবা**
|
| 520 |
৪. **পদাৰ্থবিজ্ঞানৰ সূত্ৰ LaTeX ফৰ্মেটত দিবা**
|
|
@@ -573,18 +717,28 @@ $F = ma$, $v = u + at$
|
|
| 573 |
}
|
| 574 |
|
| 575 |
# ===============================
|
| 576 |
-
# HELPER FUNCTIONS
|
| 577 |
# ===============================
|
| 578 |
def create_cache_key(question, subject, chapter_name):
|
| 579 |
"""Create a unique cache key for the question"""
|
| 580 |
-
# Normalize the question for better
|
| 581 |
normalized_question = question.strip().lower()
|
| 582 |
-
|
|
|
|
| 583 |
normalized_question = re.sub(r'\s+', ' ', normalized_question)
|
| 584 |
-
normalized_question = normalized_question[:200] # Limit length
|
| 585 |
|
| 586 |
-
#
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
cache_key = hashlib.md5(key_string.encode()).hexdigest()
|
| 589 |
|
| 590 |
return cache_key
|
|
@@ -706,7 +860,6 @@ def stream_deepseek_response(prompt, question, subject, chapter_name):
|
|
| 706 |
break
|
| 707 |
|
| 708 |
try:
|
| 709 |
-
import json
|
| 710 |
chunk = json.loads(data)
|
| 711 |
if 'choices' in chunk and len(chunk['choices']) > 0:
|
| 712 |
delta = chunk['choices'][0].get('delta', {})
|
|
@@ -714,7 +867,7 @@ def stream_deepseek_response(prompt, question, subject, chapter_name):
|
|
| 714 |
content = delta['content']
|
| 715 |
full_response += content
|
| 716 |
|
| 717 |
-
# Update streaming display
|
| 718 |
streaming_placeholder.markdown(
|
| 719 |
f"{full_response}<span style='animation: cursor-blink 1s infinite;'>▋</span>",
|
| 720 |
unsafe_allow_html=True
|
|
@@ -729,7 +882,7 @@ def stream_deepseek_response(prompt, question, subject, chapter_name):
|
|
| 729 |
# Clear streaming cursor after completion
|
| 730 |
streaming_placeholder.empty()
|
| 731 |
|
| 732 |
-
# Render the final answer
|
| 733 |
st.markdown(full_response)
|
| 734 |
|
| 735 |
# Store the complete response
|
|
@@ -753,7 +906,7 @@ def stream_deepseek_response(prompt, question, subject, chapter_name):
|
|
| 753 |
'question': question[:100],
|
| 754 |
'timestamp': datetime.now().strftime("%H:%M"),
|
| 755 |
'tokens': tokens_used,
|
| 756 |
-
'cached': False
|
| 757 |
}
|
| 758 |
st.session_state.history.append(history_entry)
|
| 759 |
|
|
@@ -764,7 +917,7 @@ def stream_deepseek_response(prompt, question, subject, chapter_name):
|
|
| 764 |
st.error(f"সংযোগ ত্ৰুটি: {str(e)}")
|
| 765 |
|
| 766 |
# ===============================
|
| 767 |
-
# INITIALIZE SESSION STATE
|
| 768 |
# ===============================
|
| 769 |
if 'history' not in st.session_state:
|
| 770 |
st.session_state.history = []
|
|
@@ -783,13 +936,20 @@ if 'streaming_answer' not in st.session_state:
|
|
| 783 |
if 'tokens_used' not in st.session_state:
|
| 784 |
st.session_state.tokens_used = 0
|
| 785 |
if 'cache_manager' not in st.session_state:
|
| 786 |
-
st.session_state.cache_manager =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
if 'show_cached_answer' not in st.session_state:
|
| 788 |
st.session_state.show_cached_answer = False
|
| 789 |
if 'cached_answer_data' not in st.session_state:
|
| 790 |
st.session_state.cached_answer_data = None
|
| 791 |
if 'current_cache_key' not in st.session_state:
|
| 792 |
st.session_state.current_cache_key = None
|
|
|
|
|
|
|
| 793 |
|
| 794 |
# ===============================
|
| 795 |
# HEADER SECTION
|
|
@@ -874,9 +1034,8 @@ st.info(f"""
|
|
| 874 |
""")
|
| 875 |
|
| 876 |
# ===============================
|
| 877 |
-
# SAMPLE QUESTIONS SECTION
|
| 878 |
# ===============================
|
| 879 |
-
# Sample questions
|
| 880 |
SAMPLE_QUESTIONS = {
|
| 881 |
"📐 গণিত (Mathematics)": {
|
| 882 |
"অধ্যায় ১": [
|
|
@@ -1126,7 +1285,7 @@ SAMPLE_QUESTIONS = {
|
|
| 1126 |
"অধ্যায় ৮": [
|
| 1127 |
"ভাৰতৰ ৰাজনৈতিক দলসমূহৰ শ্ৰেণীবিভাজন কৰক।",
|
| 1128 |
"ৰাষ্ট্ৰীয় দল আৰু ৰাজ্যিক দলৰ মাজৰ পাৰ্থক্য লিখক。",
|
| 1129 |
-
"ভাৰতত বহুদলীয় গণতন্ত্ৰৰ গুৰুত্ব লিখক
|
| 1130 |
"ৰাজনৈতিক দলৰ কাৰ্যবোৰ লিখক।",
|
| 1131 |
"নিৰ্বাচন আয়োগৰ কাৰ্যবোৰ লিখক。"
|
| 1132 |
],
|
|
@@ -1299,7 +1458,7 @@ SAMPLE_QUESTIONS = {
|
|
| 1299 |
]
|
| 1300 |
},
|
| 1301 |
|
| 1302 |
-
"📘 হিন্দ
|
| 1303 |
"पाठ १": [
|
| 1304 |
"साखी पाठ का मुख्य संदेश क्या है?",
|
| 1305 |
"कबीरदास की साखियों की भाषा-शैली पर प्रकाश डालिए।",
|
|
@@ -1337,7 +1496,7 @@ SAMPLE_QUESTIONS = {
|
|
| 1337 |
],
|
| 1338 |
"पाठ ६": [
|
| 1339 |
"मधुर-मधुर मेरे दीपक जल कविता की व्याख्या कीजिए।",
|
| 1340 |
-
"महादेवी वर्मा की कविता 'मधुर-म
|
| 1341 |
"कविता में दीपक किसका प्रतीक है?",
|
| 1342 |
"महादेवी वर्मा की काव्य शैली की विशेषताएँ बताइए।",
|
| 1343 |
"कविता से हमें क्या संदेश मिलता है?"
|
|
@@ -1593,7 +1752,6 @@ div[role="listbox"] div:first-child {
|
|
| 1593 |
</style>
|
| 1594 |
""", unsafe_allow_html=True)
|
| 1595 |
|
| 1596 |
-
|
| 1597 |
# ===============================
|
| 1598 |
# QUESTION INPUT AREA
|
| 1599 |
# ===============================
|
|
@@ -1628,11 +1786,18 @@ if not api_key:
|
|
| 1628 |
```
|
| 1629 |
""")
|
| 1630 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1631 |
# ===============================
|
| 1632 |
-
# CACHE CHECK AND SUBMIT BUTTON
|
| 1633 |
# ===============================
|
| 1634 |
submit_disabled = not (question.strip() and api_key)
|
| 1635 |
col1, col2, col3 = st.columns([1, 2, 1])
|
|
|
|
| 1636 |
with col2:
|
| 1637 |
if st.button(
|
| 1638 |
"🚀 উত্তৰ দিবলৈ দিয়ক!",
|
|
@@ -1647,9 +1812,21 @@ with col2:
|
|
| 1647 |
else:
|
| 1648 |
# Check cache first
|
| 1649 |
cache_key = create_cache_key(question, selected_subject, current_chapter_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1650 |
cached_entry = st.session_state.cache_manager.get(cache_key)
|
| 1651 |
|
| 1652 |
if cached_entry:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1653 |
# Load from cache
|
| 1654 |
st.session_state.last_answer = cached_entry['answer']
|
| 1655 |
st.session_state.tokens_used = cached_entry['tokens']
|
|
@@ -1661,7 +1838,8 @@ with col2:
|
|
| 1661 |
'question': question[:100],
|
| 1662 |
'timestamp': datetime.now().strftime("%H:%M"),
|
| 1663 |
'tokens': cached_entry['tokens'],
|
| 1664 |
-
'cached': True
|
|
|
|
| 1665 |
}
|
| 1666 |
st.session_state.history.append(history_entry)
|
| 1667 |
|
|
@@ -1671,30 +1849,66 @@ with col2:
|
|
| 1671 |
st.session_state.current_cache_key = cache_key
|
| 1672 |
st.session_state.processing = False
|
| 1673 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1674 |
# Not in cache, proceed with API call
|
| 1675 |
st.session_state.processing = True
|
| 1676 |
st.session_state.current_cache_key = cache_key
|
| 1677 |
|
| 1678 |
# ===============================
|
| 1679 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1680 |
# ===============================
|
| 1681 |
-
if
|
| 1682 |
st.markdown("---")
|
| 1683 |
|
|
|
|
|
|
|
| 1684 |
# User question
|
| 1685 |
st.markdown(f"""
|
| 1686 |
<div style="margin-bottom: 1rem;">
|
| 1687 |
<div style="display: flex; justify-content: flex-end; margin-bottom: 0.3rem;">
|
| 1688 |
<div class="user-bubble">
|
| 1689 |
<div style="font-weight: 600; margin-bottom: 0.2rem;">👤 আপুনি:</div>
|
| 1690 |
-
<div>{question}</div>
|
| 1691 |
</div>
|
| 1692 |
</div>
|
| 1693 |
</div>
|
| 1694 |
""", unsafe_allow_html=True)
|
| 1695 |
|
| 1696 |
# Cached answer with indicator
|
| 1697 |
-
|
| 1698 |
|
| 1699 |
st.markdown(f"""
|
| 1700 |
<div style="margin-bottom: 0.5rem;">
|
|
@@ -1706,14 +1920,14 @@ if hasattr(st.session_state, 'show_cached_answer') and st.session_state.show_cac
|
|
| 1706 |
<div style="display: flex; align-items: center;">
|
| 1707 |
<div style="background: #4CAF50; color: white; padding: 0.2rem 0.5rem; border-radius: 8px;
|
| 1708 |
font-weight: 600; font-size: 0.8rem; margin-right: 0.5rem;">
|
| 1709 |
-
<span style="margin-right: 0.3rem;">⚡</span> Cached
|
| 1710 |
</div>
|
| 1711 |
<div style="font-weight: 600; color: #0d47a1; font-size: 0.9rem;">
|
| 1712 |
-
{selected_subject} • {current_chapter_name}
|
| 1713 |
</div>
|
| 1714 |
</div>
|
| 1715 |
<div style="font-size: 0.75rem; color: #666; background: #f1f8e9; padding: 0.2rem 0.5rem; border-radius: 4px;">
|
| 1716 |
-
<span style="margin-right: 0.3rem;">💾</span>
|
| 1717 |
</div>
|
| 1718 |
</div>
|
| 1719 |
<div style="color: #333; line-height: 1.5; font-size: 0.95rem;">
|
|
@@ -1728,52 +1942,77 @@ if hasattr(st.session_state, 'show_cached_answer') and st.session_state.show_cac
|
|
| 1728 |
# Show cache info
|
| 1729 |
with st.expander("📊 Cache Information"):
|
| 1730 |
cache_stats = st.session_state.cache_manager.get_stats()
|
| 1731 |
-
estimated_cost = cached_data.get('tokens', 0) * 0.0000014
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
st.info(f"""
|
| 1733 |
**Cache Benefits:**
|
| 1734 |
-
- ⚡ Instant response (no API call)
|
| 1735 |
-
- 💰 No token cost
|
| 1736 |
-
-
|
| 1737 |
-
-
|
| 1738 |
|
| 1739 |
-
**Cache
|
| 1740 |
-
-
|
|
|
|
| 1741 |
- Total tokens saved: {cache_stats['total_saved_tokens']:,}
|
| 1742 |
-
- Cache
|
| 1743 |
-
- Cache TTL: {cache_stats['ttl_hours']} hours
|
| 1744 |
-
- Storage: {cache_stats['storage_mode']}
|
| 1745 |
""")
|
| 1746 |
|
| 1747 |
# Cache management buttons
|
|
|
|
|
|
|
|
|
|
| 1748 |
col1, col2 = st.columns(2)
|
| 1749 |
with col1:
|
| 1750 |
-
if st.button("🗑️ Clear
|
| 1751 |
-
if st.session_state.current_cache_key in st.session_state.cache_manager.
|
| 1752 |
-
st.session_state.cache_manager.
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1756 |
|
| 1757 |
with col2:
|
| 1758 |
-
if st.button("🧹 Clear
|
| 1759 |
st.session_state.cache_manager.clear_all()
|
| 1760 |
-
st.success("All cache cleared!")
|
| 1761 |
st.session_state.show_cached_answer = False
|
| 1762 |
st.rerun()
|
| 1763 |
|
| 1764 |
# Show token usage
|
| 1765 |
if cached_data.get('tokens', 0) > 0:
|
| 1766 |
-
st.caption(f"📊 Original token cost (saved): {cached_data['tokens']:,}")
|
| 1767 |
|
| 1768 |
# Reset flag
|
| 1769 |
st.session_state.show_cached_answer = False
|
| 1770 |
-
|
| 1771 |
-
|
|
|
|
|
|
|
| 1772 |
|
| 1773 |
# ===============================
|
| 1774 |
# PROCESS QUESTION WITH STREAMING
|
| 1775 |
# ===============================
|
| 1776 |
-
if st.session_state.get('processing') and question and api_key
|
| 1777 |
st.markdown("---")
|
| 1778 |
|
| 1779 |
# User question
|
|
@@ -1782,7 +2021,7 @@ if st.session_state.get('processing') and question and api_key and not st.sessio
|
|
| 1782 |
<div style="display: flex; justify-content: flex-end; margin-bottom: 0.3rem;">
|
| 1783 |
<div class="user-bubble">
|
| 1784 |
<div style="font-weight: 600; margin-bottom: 0.2rem;">👤 আপুনি:</div>
|
| 1785 |
-
<div>{question}</div>
|
| 1786 |
</div>
|
| 1787 |
</div>
|
| 1788 |
</div>
|
|
@@ -1840,7 +2079,8 @@ if st.session_state.get('processing') and question and api_key and not st.sessio
|
|
| 1840 |
|
| 1841 |
# Show token usage
|
| 1842 |
if st.session_state.tokens_used > 0:
|
| 1843 |
-
|
|
|
|
| 1844 |
|
| 1845 |
st.session_state.processing = False
|
| 1846 |
|
|
@@ -1851,14 +2091,16 @@ if st.session_state.history:
|
|
| 1851 |
st.markdown("---")
|
| 1852 |
st.markdown("#### 📜 আজিৰ প্ৰশ্নাৱলী")
|
| 1853 |
|
| 1854 |
-
for i, item in enumerate(reversed(st.session_state.history[-
|
| 1855 |
-
cache_indicator = " ⚡" if item.get('cached') else ""
|
| 1856 |
-
|
|
|
|
|
|
|
| 1857 |
st.write(f"**বিষয়:** {item['subject']}")
|
| 1858 |
st.write(f"**অধ্যায়:** {item['chapter']}")
|
| 1859 |
st.write(f"**ট'কেন:** {item.get('tokens', 0):,}")
|
| 1860 |
if item.get('cached'):
|
| 1861 |
-
st.caption("⚡ This answer was served from cache")
|
| 1862 |
|
| 1863 |
# ===============================
|
| 1864 |
# CACHE STATISTICS SIDEBAR
|
|
@@ -1869,23 +2111,33 @@ with st.sidebar:
|
|
| 1869 |
|
| 1870 |
cache_stats = st.session_state.cache_manager.get_stats()
|
| 1871 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1872 |
if cache_stats['total_entries'] > 0:
|
| 1873 |
-
# Cache info
|
| 1874 |
-
st.caption(f"⚡ **Storage:** {cache_stats['storage_mode']}")
|
| 1875 |
-
|
| 1876 |
# Cache stats metrics
|
| 1877 |
col1, col2 = st.columns(2)
|
| 1878 |
with col1:
|
| 1879 |
-
st.metric("Entries", cache_stats['total_entries'])
|
| 1880 |
-
|
|
|
|
| 1881 |
|
| 1882 |
with col2:
|
| 1883 |
-
st.metric("
|
| 1884 |
-
|
|
|
|
| 1885 |
|
| 1886 |
-
# Cost savings
|
| 1887 |
-
|
| 1888 |
-
|
|
|
|
|
|
|
|
|
|
| 1889 |
|
| 1890 |
# Cache management
|
| 1891 |
st.markdown("#### 🛠️ Cache Management")
|
|
@@ -1901,31 +2153,51 @@ with st.sidebar:
|
|
| 1901 |
st.rerun()
|
| 1902 |
|
| 1903 |
with col2:
|
| 1904 |
-
if st.button("🧹 Clear All", use_container_width=True
|
|
|
|
| 1905 |
st.session_state.cache_manager.clear_all()
|
| 1906 |
-
st.success("
|
| 1907 |
st.rerun()
|
| 1908 |
|
| 1909 |
-
#
|
| 1910 |
-
|
| 1911 |
-
|
| 1912 |
-
|
| 1913 |
-
|
| 1914 |
-
|
| 1915 |
-
|
| 1916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1917 |
else:
|
| 1918 |
st.info("Cache is empty. Ask some questions to build cache!")
|
| 1919 |
-
|
| 1920 |
-
|
| 1921 |
-
|
| 1922 |
-
|
| 1923 |
-
|
| 1924 |
-
- Cache persists for 24 hours
|
| 1925 |
-
- In-memory storage (no file access)
|
| 1926 |
-
- Automatic cleanup of old entries
|
| 1927 |
-
- Works within browser session
|
| 1928 |
-
""")
|
| 1929 |
|
| 1930 |
# ===============================
|
| 1931 |
# FOOTER
|
|
|
|
| 7 |
import json
|
| 8 |
|
| 9 |
# ===============================
|
| 10 |
+
# SUPABASE CACHE CLASS - FIXED VERSION
|
| 11 |
# ===============================
|
| 12 |
+
class SupabaseCache:
|
| 13 |
+
def __init__(self, ttl_days=7):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
+
Supabase-based cache for multi-user, persistent storage
|
|
|
|
|
|
|
| 16 |
"""
|
| 17 |
+
self.ttl_days = ttl_days
|
| 18 |
+
|
| 19 |
+
# Get Supabase credentials from environment
|
| 20 |
+
self.supabase_url = os.environ.get("SUPABASE_URL", "")
|
| 21 |
+
self.supabase_key = os.environ.get("SUPABASE_KEY", "")
|
| 22 |
+
|
| 23 |
+
# Initialize Supabase client
|
| 24 |
+
self.supabase = None
|
| 25 |
+
self._init_supabase()
|
| 26 |
+
|
| 27 |
+
# In-memory fallback cache
|
| 28 |
+
self.memory_cache = {}
|
| 29 |
+
self.max_memory_entries = 100
|
| 30 |
|
| 31 |
+
def _init_supabase(self):
|
| 32 |
+
"""Initialize Supabase client"""
|
| 33 |
+
if self.supabase_url and self.supabase_key:
|
| 34 |
+
try:
|
| 35 |
+
from supabase import create_client
|
| 36 |
+
self.supabase = create_client(self.supabase_url, self.supabase_key)
|
| 37 |
+
# Test connection
|
| 38 |
+
self.supabase.table("seba_cache").select("count", count="exact").limit(1).execute()
|
| 39 |
+
except ImportError:
|
| 40 |
+
# supabase-py not installed
|
| 41 |
+
self.supabase = None
|
| 42 |
+
except Exception as e:
|
| 43 |
+
# Connection failed
|
| 44 |
+
print(f"Supabase connection error: {e}")
|
| 45 |
+
self.supabase = None
|
| 46 |
+
else:
|
| 47 |
+
self.supabase = None
|
| 48 |
+
|
| 49 |
def get(self, cache_key):
|
| 50 |
+
"""Get cached answer - try Supabase first, then memory"""
|
| 51 |
+
# First check memory cache (fastest)
|
| 52 |
+
if cache_key in self.memory_cache:
|
| 53 |
+
entry = self.memory_cache[cache_key]
|
| 54 |
+
if self._is_valid(entry):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
entry['access_count'] = entry.get('access_count', 0) + 1
|
| 56 |
+
entry['last_accessed'] = datetime.now().isoformat()
|
| 57 |
return entry
|
| 58 |
+
|
| 59 |
+
# Try Supabase if available - FIXED: Removed TTL filter from query
|
| 60 |
+
if self.supabase:
|
| 61 |
+
try:
|
| 62 |
+
# Get entry without TTL filter - we'll check TTL in Python
|
| 63 |
+
response = self.supabase.table("seba_cache") \
|
| 64 |
+
.select("*") \
|
| 65 |
+
.eq("key_hash", cache_key) \
|
| 66 |
+
.execute()
|
| 67 |
+
|
| 68 |
+
if response.data and len(response.data) > 0:
|
| 69 |
+
entry = response.data[0]
|
| 70 |
+
|
| 71 |
+
# Check if entry is expired
|
| 72 |
+
created_at_str = entry.get('created_at')
|
| 73 |
+
is_expired = False
|
| 74 |
+
|
| 75 |
+
if created_at_str:
|
| 76 |
+
try:
|
| 77 |
+
# Parse the timestamp
|
| 78 |
+
if 'Z' in created_at_str:
|
| 79 |
+
created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
|
| 80 |
+
else:
|
| 81 |
+
created_at = datetime.fromisoformat(created_at_str)
|
| 82 |
+
|
| 83 |
+
# Check TTL
|
| 84 |
+
if (datetime.now() - created_at).days >= self.ttl_days:
|
| 85 |
+
# Entry expired, delete it
|
| 86 |
+
is_expired = True
|
| 87 |
+
try:
|
| 88 |
+
self.supabase.table("seba_cache") \
|
| 89 |
+
.delete() \
|
| 90 |
+
.eq("key_hash", cache_key) \
|
| 91 |
+
.execute()
|
| 92 |
+
except:
|
| 93 |
+
pass
|
| 94 |
+
except Exception:
|
| 95 |
+
# If we can't parse date, assume not expired
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
if not is_expired:
|
| 99 |
+
# Convert to standard format
|
| 100 |
+
cached_data = {
|
| 101 |
+
'answer': entry['answer'],
|
| 102 |
+
'tokens': entry.get('tokens', 0),
|
| 103 |
+
'subject': entry.get('subject', ''),
|
| 104 |
+
'chapter': entry.get('chapter', ''),
|
| 105 |
+
'question': entry.get('question', ''),
|
| 106 |
+
'access_count': entry.get('access_count', 0) + 1,
|
| 107 |
+
'created_at': entry.get('created_at'),
|
| 108 |
+
'last_accessed': datetime.now().isoformat()
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
# Update access count in Supabase
|
| 112 |
+
try:
|
| 113 |
+
self.supabase.table("seba_cache") \
|
| 114 |
+
.update({
|
| 115 |
+
"last_accessed": datetime.now().isoformat(),
|
| 116 |
+
"access_count": entry.get('access_count', 0) + 1
|
| 117 |
+
}) \
|
| 118 |
+
.eq("key_hash", cache_key) \
|
| 119 |
+
.execute()
|
| 120 |
+
except:
|
| 121 |
+
pass
|
| 122 |
+
|
| 123 |
+
# Store in memory cache for faster access
|
| 124 |
+
self.memory_cache[cache_key] = cached_data
|
| 125 |
+
|
| 126 |
+
# Limit memory cache size
|
| 127 |
+
if len(self.memory_cache) > self.max_memory_entries:
|
| 128 |
+
oldest_key = min(self.memory_cache.keys(),
|
| 129 |
+
key=lambda k: self.memory_cache[k].get('last_accessed', ''))
|
| 130 |
+
del self.memory_cache[oldest_key]
|
| 131 |
+
|
| 132 |
+
return cached_data
|
| 133 |
+
except Exception as e:
|
| 134 |
+
# Silently fail - fall back to memory cache
|
| 135 |
+
print(f"Supabase get error: {e}")
|
| 136 |
+
pass
|
| 137 |
|
| 138 |
return None
|
| 139 |
|
| 140 |
def set(self, cache_key, data):
|
| 141 |
+
"""Store answer in both Supabase and memory cache"""
|
| 142 |
+
# Prepare data
|
| 143 |
+
cache_data = {
|
| 144 |
+
'answer': data['answer'],
|
| 145 |
+
'tokens': data.get('tokens', 0),
|
| 146 |
+
'subject': data.get('subject', ''),
|
| 147 |
+
'chapter': data.get('chapter', ''),
|
| 148 |
+
'question': data.get('question', '')[:200],
|
| 149 |
+
'access_count': 1,
|
| 150 |
+
'created_at': datetime.now().isoformat(),
|
| 151 |
+
'last_accessed': datetime.now().isoformat()
|
| 152 |
+
}
|
| 153 |
|
| 154 |
+
# Store in memory cache
|
| 155 |
+
self.memory_cache[cache_key] = cache_data
|
| 156 |
|
| 157 |
+
# Limit memory cache size
|
| 158 |
+
if len(self.memory_cache) > self.max_memory_entries:
|
| 159 |
+
oldest_key = min(self.memory_cache.keys(),
|
| 160 |
+
key=lambda k: self.memory_cache[k].get('last_accessed', ''))
|
| 161 |
+
del self.memory_cache[oldest_key]
|
| 162 |
+
|
| 163 |
+
# Store in Supabase if available
|
| 164 |
+
if self.supabase:
|
| 165 |
+
try:
|
| 166 |
+
self.supabase.table("seba_cache").upsert({
|
| 167 |
+
"key_hash": cache_key,
|
| 168 |
+
"question": cache_data['question'],
|
| 169 |
+
"answer": cache_data['answer'],
|
| 170 |
+
"subject": cache_data['subject'],
|
| 171 |
+
"chapter": cache_data['chapter'],
|
| 172 |
+
"tokens": cache_data['tokens'],
|
| 173 |
+
"created_at": cache_data['created_at'],
|
| 174 |
+
"last_accessed": cache_data['last_accessed'],
|
| 175 |
+
"access_count": cache_data['access_count']
|
| 176 |
+
}).execute()
|
| 177 |
+
except Exception as e:
|
| 178 |
+
# Silently fail - at least we have memory cache
|
| 179 |
+
print(f"Supabase set error: {e}")
|
| 180 |
+
pass
|
| 181 |
+
|
| 182 |
+
def _is_valid(self, entry):
|
| 183 |
+
"""Check if cache entry is not expired"""
|
| 184 |
+
created_at = entry.get('created_at')
|
| 185 |
+
if isinstance(created_at, str):
|
| 186 |
+
try:
|
| 187 |
+
if 'Z' in created_at:
|
| 188 |
+
created_at = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
|
| 189 |
+
else:
|
| 190 |
+
created_at = datetime.fromisoformat(created_at)
|
| 191 |
+
except:
|
| 192 |
+
return True # If we can't parse, assume valid
|
| 193 |
+
|
| 194 |
+
if created_at and (datetime.now() - created_at).days < self.ttl_days:
|
| 195 |
+
return True
|
| 196 |
+
return False
|
| 197 |
|
| 198 |
def clear_expired(self):
|
| 199 |
+
"""Clear expired entries from memory cache"""
|
|
|
|
| 200 |
expired_keys = []
|
| 201 |
+
for key, entry in self.memory_cache.items():
|
| 202 |
+
if not self._is_valid(entry):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
expired_keys.append(key)
|
| 204 |
|
| 205 |
for key in expired_keys:
|
| 206 |
+
del self.memory_cache[key]
|
| 207 |
|
| 208 |
return len(expired_keys)
|
| 209 |
|
| 210 |
def clear_all(self):
|
| 211 |
"""Clear all cache entries"""
|
| 212 |
+
self.memory_cache = {}
|
| 213 |
+
|
| 214 |
+
# Also clear Supabase cache if available
|
| 215 |
+
if self.supabase:
|
| 216 |
+
try:
|
| 217 |
+
# Delete entries older than 1 day (safer than deleting all)
|
| 218 |
+
self.supabase.table("seba_cache") \
|
| 219 |
+
.delete() \
|
| 220 |
+
.lt("created_at", f"now() - interval '1 day'") \
|
| 221 |
+
.execute()
|
| 222 |
+
except:
|
| 223 |
+
pass
|
| 224 |
|
| 225 |
def get_stats(self):
|
| 226 |
"""Get cache statistics"""
|
| 227 |
+
# Memory cache stats
|
| 228 |
+
memory_entries = len(self.memory_cache)
|
| 229 |
+
memory_tokens = sum(entry.get('tokens', 0) for entry in self.memory_cache.values())
|
| 230 |
|
| 231 |
+
# Try to get Supabase stats
|
| 232 |
+
supabase_entries = 0
|
| 233 |
+
supabase_tokens = 0
|
| 234 |
+
|
| 235 |
+
if self.supabase:
|
| 236 |
+
try:
|
| 237 |
+
# Get count from Supabase
|
| 238 |
+
response = self.supabase.table("seba_cache") \
|
| 239 |
+
.select("count", count="exact") \
|
| 240 |
+
.execute()
|
| 241 |
+
|
| 242 |
+
supabase_entries = response.count or 0
|
| 243 |
+
|
| 244 |
+
# Get total tokens (might be heavy, so approximate)
|
| 245 |
+
if supabase_entries > 0:
|
| 246 |
+
response = self.supabase.table("seba_cache") \
|
| 247 |
+
.select("tokens") \
|
| 248 |
+
.limit(100) \
|
| 249 |
+
.execute()
|
| 250 |
+
|
| 251 |
+
supabase_tokens = sum(entry.get('tokens', 0) for entry in response.data)
|
| 252 |
+
except:
|
| 253 |
+
pass
|
| 254 |
+
|
| 255 |
+
total_entries = memory_entries + supabase_entries
|
| 256 |
+
total_tokens = memory_tokens + supabase_tokens
|
| 257 |
|
| 258 |
return {
|
| 259 |
+
'total_entries': total_entries,
|
| 260 |
+
'memory_entries': memory_entries,
|
| 261 |
+
'supabase_entries': supabase_entries,
|
| 262 |
+
'total_saved_tokens': total_tokens,
|
| 263 |
+
'ttl_days': self.ttl_days,
|
| 264 |
+
'storage_mode': 'Supabase + Memory' if self.supabase else 'Memory Only',
|
| 265 |
+
'supabase_connected': self.supabase is not None
|
| 266 |
}
|
| 267 |
|
| 268 |
+
# ===============================
|
| 269 |
+
# API KEY HANDLING
|
| 270 |
+
# ===============================
|
| 271 |
+
# Get API key from environment variable
|
| 272 |
+
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
| 273 |
+
|
| 274 |
# Page config - must be first Streamlit command
|
| 275 |
st.set_page_config(
|
| 276 |
page_title="SEBA দশম শ্ৰেণীৰ AI টিউটাৰ",
|
|
|
|
| 614 |
"পাঠ ১০": "সাহিত্যৰ ৰূপ"
|
| 615 |
},
|
| 616 |
"📘 হিন্দী (Hindi)": {
|
| 617 |
+
"পাঠ ১": "साखी",
|
| 618 |
+
"পাঠ ২": "पद",
|
| 619 |
+
"পাঠ ৩": "दोहे",
|
| 620 |
+
"পাঠ ৪": "मनुष्यता",
|
| 621 |
+
"পাঠ ৫": "पर्वत प्रदेश में पावस",
|
| 622 |
+
"পাঠ ৬": "मधुर-मधुर मेरे दीपक जल",
|
| 623 |
+
"পাঠ ৭": "तोप",
|
| 624 |
+
"পাঠ ৮": "कर चले हम फ़िदा",
|
| 625 |
+
"পাঠ ৯": "आत्मत्राण",
|
| 626 |
+
"পাঠ ১০": "बड़े भाई साहब"
|
| 627 |
}
|
| 628 |
}
|
| 629 |
|
|
|
|
| 658 |
"base_prompt": """তুমি এজন বিজ্ঞান শিক্ষক। SEBA দশম শ্ৰেণীৰ বিজ্ঞানৰ {chapter_name} অধ্যায়ৰ সকলো বৈজ্ঞানিক ধাৰণা, প্ৰক্ৰয়া, আৰু নীতি তুমি জানা।
|
| 659 |
|
| 660 |
**বিজ্ঞানৰ বিশেষ নিৰ্দেশনা:**
|
| 661 |
+
১. **বৈজ্ঞানিক প্ৰক্ৰয়া ধাপে ধাপে বুজাবা**
|
| 662 |
২. **ৰাসায়নিক সমীকৰণ সঠিকভাৱে দিবা**
|
| 663 |
৩. **জীৱবিজ্ঞানৰ চিত্ৰ/ৰেখাচিত্ৰৰ বৰ্ণনা দিবা**
|
| 664 |
৪. **পদাৰ্থবিজ্ঞানৰ সূত্ৰ LaTeX ফৰ্মেটত দিবা**
|
|
|
|
| 717 |
}
|
| 718 |
|
| 719 |
# ===============================
|
| 720 |
+
# HELPER FUNCTIONS - FIXED CACHE KEY
|
| 721 |
# ===============================
|
| 722 |
def create_cache_key(question, subject, chapter_name):
|
| 723 |
"""Create a unique cache key for the question"""
|
| 724 |
+
# Normalize the question more aggressively for better cache matching
|
| 725 |
normalized_question = question.strip().lower()
|
| 726 |
+
|
| 727 |
+
# Remove extra whitespace
|
| 728 |
normalized_question = re.sub(r'\s+', ' ', normalized_question)
|
|
|
|
| 729 |
|
| 730 |
+
# Remove punctuation that might vary
|
| 731 |
+
normalized_question = re.sub(r'[^\w\s\u0980-\u09FF]', '', normalized_question)
|
| 732 |
+
|
| 733 |
+
normalized_question = normalized_question[:200]
|
| 734 |
+
|
| 735 |
+
# Normalize subject and chapter
|
| 736 |
+
# Take only the main subject name (before parentheses)
|
| 737 |
+
normalized_subject = subject.split('(')[0].strip() if '(' in subject else subject
|
| 738 |
+
# Take only chapter number/name before colon
|
| 739 |
+
normalized_chapter = chapter_name.split(':')[0].strip() if ':' in chapter_name else chapter_name
|
| 740 |
+
|
| 741 |
+
key_string = f"{normalized_subject}|{normalized_chapter}|{normalized_question}"
|
| 742 |
cache_key = hashlib.md5(key_string.encode()).hexdigest()
|
| 743 |
|
| 744 |
return cache_key
|
|
|
|
| 860 |
break
|
| 861 |
|
| 862 |
try:
|
|
|
|
| 863 |
chunk = json.loads(data)
|
| 864 |
if 'choices' in chunk and len(chunk['choices']) > 0:
|
| 865 |
delta = chunk['choices'][0].get('delta', {})
|
|
|
|
| 867 |
content = delta['content']
|
| 868 |
full_response += content
|
| 869 |
|
| 870 |
+
# Update streaming display
|
| 871 |
streaming_placeholder.markdown(
|
| 872 |
f"{full_response}<span style='animation: cursor-blink 1s infinite;'>▋</span>",
|
| 873 |
unsafe_allow_html=True
|
|
|
|
| 882 |
# Clear streaming cursor after completion
|
| 883 |
streaming_placeholder.empty()
|
| 884 |
|
| 885 |
+
# Render the final answer
|
| 886 |
st.markdown(full_response)
|
| 887 |
|
| 888 |
# Store the complete response
|
|
|
|
| 906 |
'question': question[:100],
|
| 907 |
'timestamp': datetime.now().strftime("%H:%M"),
|
| 908 |
'tokens': tokens_used,
|
| 909 |
+
'cached': False
|
| 910 |
}
|
| 911 |
st.session_state.history.append(history_entry)
|
| 912 |
|
|
|
|
| 917 |
st.error(f"সংযোগ ত্ৰুটি: {str(e)}")
|
| 918 |
|
| 919 |
# ===============================
|
| 920 |
+
# INITIALIZE SESSION STATE - FIXED
|
| 921 |
# ===============================
|
| 922 |
if 'history' not in st.session_state:
|
| 923 |
st.session_state.history = []
|
|
|
|
| 936 |
if 'tokens_used' not in st.session_state:
|
| 937 |
st.session_state.tokens_used = 0
|
| 938 |
if 'cache_manager' not in st.session_state:
|
| 939 |
+
st.session_state.cache_manager = SupabaseCache(ttl_days=7)
|
| 940 |
+
# Pre-warm cache by checking Supabase connection on startup
|
| 941 |
+
cache_stats = st.session_state.cache_manager.get_stats()
|
| 942 |
+
if cache_stats['supabase_connected'] and cache_stats['supabase_entries'] > 0:
|
| 943 |
+
st.toast(f"📦 Cache loaded: {cache_stats['supabase_entries']} entries available", icon="✅")
|
| 944 |
+
|
| 945 |
if 'show_cached_answer' not in st.session_state:
|
| 946 |
st.session_state.show_cached_answer = False
|
| 947 |
if 'cached_answer_data' not in st.session_state:
|
| 948 |
st.session_state.cached_answer_data = None
|
| 949 |
if 'current_cache_key' not in st.session_state:
|
| 950 |
st.session_state.current_cache_key = None
|
| 951 |
+
if 'cache_debug' not in st.session_state:
|
| 952 |
+
st.session_state.cache_debug = False
|
| 953 |
|
| 954 |
# ===============================
|
| 955 |
# HEADER SECTION
|
|
|
|
| 1034 |
""")
|
| 1035 |
|
| 1036 |
# ===============================
|
| 1037 |
+
# SAMPLE QUESTIONS SECTION
|
| 1038 |
# ===============================
|
|
|
|
| 1039 |
SAMPLE_QUESTIONS = {
|
| 1040 |
"📐 গণিত (Mathematics)": {
|
| 1041 |
"অধ্যায় ১": [
|
|
|
|
| 1285 |
"অধ্যায় ৮": [
|
| 1286 |
"ভাৰতৰ ৰাজনৈতিক দলসমূহৰ শ্ৰেণীবিভাজন কৰক।",
|
| 1287 |
"ৰাষ্ট্ৰীয় দল আৰু ৰাজ্যিক দলৰ মাজৰ পাৰ্থক্য লিখক。",
|
| 1288 |
+
"ভাৰতত বহুদলীয় গণতন্ত্ৰৰ গুৰুত্ব লিখক।",
|
| 1289 |
"ৰাজনৈতিক দলৰ কাৰ্যবোৰ লিখক।",
|
| 1290 |
"নিৰ্বাচন আয়োগৰ কাৰ্যবোৰ লিখক。"
|
| 1291 |
],
|
|
|
|
| 1458 |
]
|
| 1459 |
},
|
| 1460 |
|
| 1461 |
+
"📘 হিন্দী (Hindi)": {
|
| 1462 |
"पाठ १": [
|
| 1463 |
"साखी पाठ का मुख्य संदेश क्या है?",
|
| 1464 |
"कबीरदास की साखियों की भाषा-शैली पर प्रकाश डालिए।",
|
|
|
|
| 1496 |
],
|
| 1497 |
"पाठ ६": [
|
| 1498 |
"मधुर-मधुर मेरे दीपक जल कविता की व्याख्या कीजिए।",
|
| 1499 |
+
"महादेवी वर्मा की कविता 'मधुर-मধुर मेरे दीপक जल' का सार लिखिए।",
|
| 1500 |
"कविता में दीपक किसका प्रतीक है?",
|
| 1501 |
"महादेवी वर्मा की काव्य शैली की विशेषताएँ बताइए।",
|
| 1502 |
"कविता से हमें क्या संदेश मिलता है?"
|
|
|
|
| 1752 |
</style>
|
| 1753 |
""", unsafe_allow_html=True)
|
| 1754 |
|
|
|
|
| 1755 |
# ===============================
|
| 1756 |
# QUESTION INPUT AREA
|
| 1757 |
# ===============================
|
|
|
|
| 1786 |
```
|
| 1787 |
""")
|
| 1788 |
|
| 1789 |
+
# Show Supabase status
|
| 1790 |
+
if not os.environ.get("SUPABASE_URL") and not os.environ.get("SUPABASE_KEY"):
|
| 1791 |
+
with st.sidebar:
|
| 1792 |
+
st.warning("⚠️ **Supabase Not Configured**")
|
| 1793 |
+
st.caption("Add `SUPABASE_URL` and `SUPABASE_KEY` in secrets for multi-user cache")
|
| 1794 |
+
|
| 1795 |
# ===============================
|
| 1796 |
+
# CACHE CHECK AND SUBMIT BUTTON - FIXED VERSION
|
| 1797 |
# ===============================
|
| 1798 |
submit_disabled = not (question.strip() and api_key)
|
| 1799 |
col1, col2, col3 = st.columns([1, 2, 1])
|
| 1800 |
+
|
| 1801 |
with col2:
|
| 1802 |
if st.button(
|
| 1803 |
"🚀 উত্তৰ দিবলৈ দিয়ক!",
|
|
|
|
| 1812 |
else:
|
| 1813 |
# Check cache first
|
| 1814 |
cache_key = create_cache_key(question, selected_subject, current_chapter_name)
|
| 1815 |
+
|
| 1816 |
+
# Get cache stats for debugging
|
| 1817 |
+
cache_stats = st.session_state.cache_manager.get_stats()
|
| 1818 |
+
|
| 1819 |
+
# Check if we should show debug info
|
| 1820 |
+
if cache_stats['supabase_connected']:
|
| 1821 |
+
st.toast(f"🔍 Checking Supabase cache ({cache_stats['supabase_entries']} entries)", icon="🔍")
|
| 1822 |
+
|
| 1823 |
cached_entry = st.session_state.cache_manager.get(cache_key)
|
| 1824 |
|
| 1825 |
if cached_entry:
|
| 1826 |
+
# Determine cache source
|
| 1827 |
+
cache_source = "Memory" if cache_key in st.session_state.cache_manager.memory_cache else "Supabase"
|
| 1828 |
+
st.toast(f"🎯 Cache hit from {cache_source}!", icon="⚡")
|
| 1829 |
+
|
| 1830 |
# Load from cache
|
| 1831 |
st.session_state.last_answer = cached_entry['answer']
|
| 1832 |
st.session_state.tokens_used = cached_entry['tokens']
|
|
|
|
| 1838 |
'question': question[:100],
|
| 1839 |
'timestamp': datetime.now().strftime("%H:%M"),
|
| 1840 |
'tokens': cached_entry['tokens'],
|
| 1841 |
+
'cached': True,
|
| 1842 |
+
'cache_source': cache_source
|
| 1843 |
}
|
| 1844 |
st.session_state.history.append(history_entry)
|
| 1845 |
|
|
|
|
| 1849 |
st.session_state.current_cache_key = cache_key
|
| 1850 |
st.session_state.processing = False
|
| 1851 |
else:
|
| 1852 |
+
# Cache miss
|
| 1853 |
+
if cache_stats['supabase_connected']:
|
| 1854 |
+
st.toast("❌ Cache miss - calling API...", icon="🤖")
|
| 1855 |
+
else:
|
| 1856 |
+
st.toast("🤖 Calling DeepSeek API...", icon="🤖")
|
| 1857 |
+
|
| 1858 |
# Not in cache, proceed with API call
|
| 1859 |
st.session_state.processing = True
|
| 1860 |
st.session_state.current_cache_key = cache_key
|
| 1861 |
|
| 1862 |
# ===============================
|
| 1863 |
+
# CACHE DEBUG PANEL (Optional)
|
| 1864 |
+
# ===============================
|
| 1865 |
+
with st.sidebar:
|
| 1866 |
+
if st.checkbox("🔧 Show Cache Debug", value=st.session_state.cache_debug):
|
| 1867 |
+
st.session_state.cache_debug = True
|
| 1868 |
+
st.markdown("#### 🔍 Cache Debug")
|
| 1869 |
+
|
| 1870 |
+
cache_stats = st.session_state.cache_manager.get_stats()
|
| 1871 |
+
st.write(f"**Supabase Connected:** {cache_stats['supabase_connected']}")
|
| 1872 |
+
st.write(f"**Supabase Entries:** {cache_stats['supabase_entries']}")
|
| 1873 |
+
st.write(f"**Memory Entries:** {cache_stats['memory_entries']}")
|
| 1874 |
+
|
| 1875 |
+
if cache_stats['supabase_connected'] and cache_stats['supabase_entries'] > 0:
|
| 1876 |
+
try:
|
| 1877 |
+
# Show some sample cache entries
|
| 1878 |
+
sample = st.session_state.cache_manager.supabase.table("seba_cache") \
|
| 1879 |
+
.select("question, subject, chapter, created_at") \
|
| 1880 |
+
.limit(5) \
|
| 1881 |
+
.execute()
|
| 1882 |
+
|
| 1883 |
+
if sample.data:
|
| 1884 |
+
st.write("**Sample Cache Entries:**")
|
| 1885 |
+
for i, item in enumerate(sample.data, 1):
|
| 1886 |
+
st.write(f"{i}. {item['question'][:30]}... ({item['subject']})")
|
| 1887 |
+
except Exception as e:
|
| 1888 |
+
st.write(f"Error: {e}")
|
| 1889 |
+
|
| 1890 |
+
# ===============================
|
| 1891 |
+
# DISPLAY CACHED ANSWER - FIXED VERSION
|
| 1892 |
# ===============================
|
| 1893 |
+
if st.session_state.get('show_cached_answer') and st.session_state.get('cached_answer_data'):
|
| 1894 |
st.markdown("---")
|
| 1895 |
|
| 1896 |
+
cached_data = st.session_state.cached_answer_data
|
| 1897 |
+
|
| 1898 |
# User question
|
| 1899 |
st.markdown(f"""
|
| 1900 |
<div style="margin-bottom: 1rem;">
|
| 1901 |
<div style="display: flex; justify-content: flex-end; margin-bottom: 0.3rem;">
|
| 1902 |
<div class="user-bubble">
|
| 1903 |
<div style="font-weight: 600; margin-bottom: 0.2rem;">👤 আপুনি:</div>
|
| 1904 |
+
<div>{question[:200]}{'...' if len(question) > 200 else ''}</div>
|
| 1905 |
</div>
|
| 1906 |
</div>
|
| 1907 |
</div>
|
| 1908 |
""", unsafe_allow_html=True)
|
| 1909 |
|
| 1910 |
# Cached answer with indicator
|
| 1911 |
+
cache_source = "Memory" if st.session_state.current_cache_key in st.session_state.cache_manager.memory_cache else "Supabase"
|
| 1912 |
|
| 1913 |
st.markdown(f"""
|
| 1914 |
<div style="margin-bottom: 0.5rem;">
|
|
|
|
| 1920 |
<div style="display: flex; align-items: center;">
|
| 1921 |
<div style="background: #4CAF50; color: white; padding: 0.2rem 0.5rem; border-radius: 8px;
|
| 1922 |
font-weight: 600; font-size: 0.8rem; margin-right: 0.5rem;">
|
| 1923 |
+
<span style="margin-right: 0.3rem;">⚡</span> Cached Answer
|
| 1924 |
</div>
|
| 1925 |
<div style="font-weight: 600; color: #0d47a1; font-size: 0.9rem;">
|
| 1926 |
+
{cached_data.get('subject', selected_subject)} • {cached_data.get('chapter', current_chapter_name)}
|
| 1927 |
</div>
|
| 1928 |
</div>
|
| 1929 |
<div style="font-size: 0.75rem; color: #666; background: #f1f8e9; padding: 0.2rem 0.5rem; border-radius: 4px;">
|
| 1930 |
+
<span style="margin-right: 0.3rem;">💾</span> From {cache_source}
|
| 1931 |
</div>
|
| 1932 |
</div>
|
| 1933 |
<div style="color: #333; line-height: 1.5; font-size: 0.95rem;">
|
|
|
|
| 1942 |
# Show cache info
|
| 1943 |
with st.expander("📊 Cache Information"):
|
| 1944 |
cache_stats = st.session_state.cache_manager.get_stats()
|
| 1945 |
+
estimated_cost = cached_data.get('tokens', 0) * 0.0000014
|
| 1946 |
+
|
| 1947 |
+
col1, col2 = st.columns(2)
|
| 1948 |
+
with col1:
|
| 1949 |
+
st.metric("Tokens Saved", f"{cached_data.get('tokens', 0):,}")
|
| 1950 |
+
st.metric("Cache Source", cache_source)
|
| 1951 |
+
|
| 1952 |
+
with col2:
|
| 1953 |
+
st.metric("Access Count", cached_data.get('access_count', 1))
|
| 1954 |
+
st.metric("Cost Saved", f"${estimated_cost:.6f}")
|
| 1955 |
+
|
| 1956 |
st.info(f"""
|
| 1957 |
**Cache Benefits:**
|
| 1958 |
+
- ⚡ Instant response (no API call needed)
|
| 1959 |
+
- 💰 No token cost for this query
|
| 1960 |
+
- 🌿 Environmentally friendly (reduces API calls)
|
| 1961 |
+
- 🔄 Available for all users
|
| 1962 |
|
| 1963 |
+
**Cache Storage:**
|
| 1964 |
+
- Mode: {cache_stats['storage_mode']}
|
| 1965 |
+
- Total cached entries: {cache_stats['total_entries']}
|
| 1966 |
- Total tokens saved: {cache_stats['total_saved_tokens']:,}
|
| 1967 |
+
- Cache TTL: {cache_stats['ttl_days']} days
|
|
|
|
|
|
|
| 1968 |
""")
|
| 1969 |
|
| 1970 |
# Cache management buttons
|
| 1971 |
+
st.markdown("---")
|
| 1972 |
+
st.markdown("#### 🛠️ Cache Management")
|
| 1973 |
+
|
| 1974 |
col1, col2 = st.columns(2)
|
| 1975 |
with col1:
|
| 1976 |
+
if st.button("🗑️ Clear This Cache", use_container_width=True, type="secondary"):
|
| 1977 |
+
if st.session_state.current_cache_key in st.session_state.cache_manager.memory_cache:
|
| 1978 |
+
del st.session_state.cache_manager.memory_cache[st.session_state.current_cache_key]
|
| 1979 |
+
|
| 1980 |
+
# Also delete from Supabase if connected
|
| 1981 |
+
if st.session_state.cache_manager.supabase:
|
| 1982 |
+
try:
|
| 1983 |
+
st.session_state.cache_manager.supabase.table("seba_cache") \
|
| 1984 |
+
.delete() \
|
| 1985 |
+
.eq("key_hash", st.session_state.current_cache_key) \
|
| 1986 |
+
.execute()
|
| 1987 |
+
except:
|
| 1988 |
+
pass
|
| 1989 |
+
|
| 1990 |
+
st.success("✅ Cache entry cleared!")
|
| 1991 |
+
st.session_state.show_cached_answer = False
|
| 1992 |
+
st.rerun()
|
| 1993 |
|
| 1994 |
with col2:
|
| 1995 |
+
if st.button("🧹 Clear All Cache", use_container_width=True, type="secondary"):
|
| 1996 |
st.session_state.cache_manager.clear_all()
|
| 1997 |
+
st.success("✅ All cache cleared!")
|
| 1998 |
st.session_state.show_cached_answer = False
|
| 1999 |
st.rerun()
|
| 2000 |
|
| 2001 |
# Show token usage
|
| 2002 |
if cached_data.get('tokens', 0) > 0:
|
| 2003 |
+
st.caption(f"📊 Original token cost (saved): {cached_data['tokens']:,} tokens")
|
| 2004 |
|
| 2005 |
# Reset flag
|
| 2006 |
st.session_state.show_cached_answer = False
|
| 2007 |
+
if 'cached_answer_data' in st.session_state:
|
| 2008 |
+
del st.session_state.cached_answer_data
|
| 2009 |
+
if 'current_cache_key' in st.session_state:
|
| 2010 |
+
del st.session_state.current_cache_key
|
| 2011 |
|
| 2012 |
# ===============================
|
| 2013 |
# PROCESS QUESTION WITH STREAMING
|
| 2014 |
# ===============================
|
| 2015 |
+
if st.session_state.get('processing') and question and api_key:
|
| 2016 |
st.markdown("---")
|
| 2017 |
|
| 2018 |
# User question
|
|
|
|
| 2021 |
<div style="display: flex; justify-content: flex-end; margin-bottom: 0.3rem;">
|
| 2022 |
<div class="user-bubble">
|
| 2023 |
<div style="font-weight: 600; margin-bottom: 0.2rem;">👤 আপুনি:</div>
|
| 2024 |
+
<div>{question[:200]}{'...' if len(question) > 200 else ''}</div>
|
| 2025 |
</div>
|
| 2026 |
</div>
|
| 2027 |
</div>
|
|
|
|
| 2079 |
|
| 2080 |
# Show token usage
|
| 2081 |
if st.session_state.tokens_used > 0:
|
| 2082 |
+
estimated_cost = st.session_state.tokens_used * 0.0000014
|
| 2083 |
+
st.caption(f"📊 ট'কেন ব্যৱহৃত: {st.session_state.tokens_used:,} (Cost: ${estimated_cost:.6f})")
|
| 2084 |
|
| 2085 |
st.session_state.processing = False
|
| 2086 |
|
|
|
|
| 2091 |
st.markdown("---")
|
| 2092 |
st.markdown("#### 📜 আজিৰ প্ৰশ্নাৱলী")
|
| 2093 |
|
| 2094 |
+
for i, item in enumerate(reversed(st.session_state.history[-5:]), 1):
|
| 2095 |
+
cache_indicator = " ⚡" if item.get('cached') else " 🤖"
|
| 2096 |
+
cache_source = f" ({item.get('cache_source', 'API')})" if item.get('cached') else ""
|
| 2097 |
+
|
| 2098 |
+
with st.expander(f"প্ৰশ্ন {i}: {item['question']} ({item['timestamp']}{cache_indicator}{cache_source})"):
|
| 2099 |
st.write(f"**বিষয়:** {item['subject']}")
|
| 2100 |
st.write(f"**অধ্যায়:** {item['chapter']}")
|
| 2101 |
st.write(f"**ট'কেন:** {item.get('tokens', 0):,}")
|
| 2102 |
if item.get('cached'):
|
| 2103 |
+
st.caption(f"⚡ This answer was served from {item.get('cache_source', 'cache')}")
|
| 2104 |
|
| 2105 |
# ===============================
|
| 2106 |
# CACHE STATISTICS SIDEBAR
|
|
|
|
| 2111 |
|
| 2112 |
cache_stats = st.session_state.cache_manager.get_stats()
|
| 2113 |
|
| 2114 |
+
# Show connection status
|
| 2115 |
+
if cache_stats['supabase_connected']:
|
| 2116 |
+
st.success("✅ Connected to Supabase")
|
| 2117 |
+
st.caption(f"🔗 **Storage:** {cache_stats['storage_mode']}")
|
| 2118 |
+
else:
|
| 2119 |
+
st.warning("⚠️ Memory Cache Only")
|
| 2120 |
+
st.caption("🔗 **Storage:** Memory Only (Supabase not configured)")
|
| 2121 |
+
|
| 2122 |
if cache_stats['total_entries'] > 0:
|
|
|
|
|
|
|
|
|
|
| 2123 |
# Cache stats metrics
|
| 2124 |
col1, col2 = st.columns(2)
|
| 2125 |
with col1:
|
| 2126 |
+
st.metric("Total Entries", cache_stats['total_entries'])
|
| 2127 |
+
if cache_stats['supabase_connected']:
|
| 2128 |
+
st.metric("Supabase", cache_stats['supabase_entries'])
|
| 2129 |
|
| 2130 |
with col2:
|
| 2131 |
+
st.metric("Memory", cache_stats['memory_entries'])
|
| 2132 |
+
estimated_savings = cache_stats['total_saved_tokens'] * 0.0000014
|
| 2133 |
+
st.metric("💰 Savings", f"${estimated_savings:.4f}")
|
| 2134 |
|
| 2135 |
+
# Cost savings breakdown
|
| 2136 |
+
with st.expander("📈 Savings Details"):
|
| 2137 |
+
st.write(f"**Total tokens saved:** {cache_stats['total_saved_tokens']:,}")
|
| 2138 |
+
st.write(f"**Estimated cost savings:** ${estimated_savings:.6f}")
|
| 2139 |
+
st.write(f"**Average per entry:** {cache_stats['total_saved_tokens'] // max(1, cache_stats['total_entries']):,} tokens")
|
| 2140 |
+
st.write(f"**Cache TTL:** {cache_stats['ttl_days']} days")
|
| 2141 |
|
| 2142 |
# Cache management
|
| 2143 |
st.markdown("#### 🛠️ Cache Management")
|
|
|
|
| 2153 |
st.rerun()
|
| 2154 |
|
| 2155 |
with col2:
|
| 2156 |
+
if st.button("🧹 Clear All", use_container_width=True,
|
| 2157 |
+
help="Clear memory cache and old Supabase entries"):
|
| 2158 |
st.session_state.cache_manager.clear_all()
|
| 2159 |
+
st.success("Cache cleared successfully!")
|
| 2160 |
st.rerun()
|
| 2161 |
|
| 2162 |
+
# Supabase setup guide
|
| 2163 |
+
if not cache_stats['supabase_connected']:
|
| 2164 |
+
with st.expander("🚀 Enable Supabase Cache (Multi-User)"):
|
| 2165 |
+
st.markdown("""
|
| 2166 |
+
**Benefits:**
|
| 2167 |
+
- ✅ Cache shared across ALL users
|
| 2168 |
+
- ✅ Persistent storage (7 days)
|
| 2169 |
+
- ✅ No data loss on app restart
|
| 2170 |
+
|
| 2171 |
+
**Setup:**
|
| 2172 |
+
1. **Create Supabase account** at [supabase.com](https://supabase.com)
|
| 2173 |
+
2. **Create new project** and get URL + anon key
|
| 2174 |
+
3. **Add to Hugging Face Secrets:**
|
| 2175 |
+
- `SUPABASE_URL` = your-project-url
|
| 2176 |
+
- `SUPABASE_KEY` = your-anon-key
|
| 2177 |
+
4. **Create table** (SQL below)
|
| 2178 |
+
|
| 2179 |
+
**SQL for cache table:**
|
| 2180 |
+
```sql
|
| 2181 |
+
CREATE TABLE seba_cache (
|
| 2182 |
+
key_hash VARCHAR(64) PRIMARY KEY,
|
| 2183 |
+
question TEXT,
|
| 2184 |
+
answer TEXT,
|
| 2185 |
+
subject VARCHAR(100),
|
| 2186 |
+
chapter VARCHAR(100),
|
| 2187 |
+
tokens INTEGER DEFAULT 0,
|
| 2188 |
+
created_at TIMESTAMP DEFAULT NOW(),
|
| 2189 |
+
last_accessed TIMESTAMP DEFAULT NOW(),
|
| 2190 |
+
access_count INTEGER DEFAULT 1
|
| 2191 |
+
);
|
| 2192 |
+
```
|
| 2193 |
+
""")
|
| 2194 |
else:
|
| 2195 |
st.info("Cache is empty. Ask some questions to build cache!")
|
| 2196 |
+
|
| 2197 |
+
if not cache_stats['supabase_connected']:
|
| 2198 |
+
st.markdown("---")
|
| 2199 |
+
st.markdown("#### 🚀 Upgrade to Multi-User Cache")
|
| 2200 |
+
st.caption("Enable Supabase to share cache across all users")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2201 |
|
| 2202 |
# ===============================
|
| 2203 |
# FOOTER
|