Sazid2 commited on
Commit
d9036af
·
verified ·
1 Parent(s): 8e95fa0

Update app.pyo

Browse files
Files changed (1) hide show
  1. app.pyo +456 -184
app.pyo CHANGED
@@ -7,126 +7,270 @@ import hashlib
7
  import json
8
 
9
  # ===============================
10
- # HUGGING FACE API KEY HANDLING
11
  # ===============================
12
- # Try to get API key from Hugging Face secrets first
13
- try:
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
- In-memory only cache for Hugging Face Spaces
34
- - No file system access needed
35
- - Works within session only
36
  """
37
- self.ttl_hours = ttl_hours
38
- self.max_entries = max_entries
39
- self.cache = {} # Simple dictionary for in-memory storage
 
 
 
 
 
 
 
 
 
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def get(self, cache_key):
42
- """Get cached answer if not expired"""
43
- if cache_key in self.cache:
44
- entry = self.cache[cache_key]
45
- created_at = entry.get('created_at')
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
- else:
57
- # Remove expired entry
58
- del self.cache[cache_key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  return None
61
 
62
  def set(self, cache_key, data):
63
- """Add or update cache entry"""
64
- data['created_at'] = datetime.now().isoformat()
65
- data['last_accessed'] = data['created_at']
66
- data['access_count'] = 1
 
 
 
 
 
 
 
 
67
 
68
- self.cache[cache_key] = data
 
69
 
70
- # Enforce max entries limit
71
- if len(self.cache) > self.max_entries:
72
- # Remove oldest entries (by access time)
73
- sorted_keys = sorted(
74
- self.cache.keys(),
75
- key=lambda k: self.cache[k].get('last_accessed', ''),
76
- reverse=True
77
- )
78
- for key in sorted_keys[self.max_entries:]:
79
- del self.cache[key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  def clear_expired(self):
82
- """Manually clear expired entries"""
83
- current_time = datetime.now()
84
  expired_keys = []
85
-
86
- for key, entry in self.cache.items():
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.cache[key]
96
 
97
  return len(expired_keys)
98
 
99
  def clear_all(self):
100
  """Clear all cache entries"""
101
- self.cache = {}
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  def get_stats(self):
104
  """Get cache statistics"""
105
- total_saved_tokens = sum(entry.get('tokens', 0) for entry in self.cache.values())
106
- total_access_count = sum(entry.get('access_count', 0) for entry in self.cache.values())
107
- avg_age_hours = 0
108
 
109
- if self.cache:
110
- current_time = datetime.now()
111
- ages = []
112
- for entry in self.cache.values():
113
- created_at = entry.get('created_at')
114
- if isinstance(created_at, str):
115
- created_at = datetime.fromisoformat(created_at)
116
- if created_at:
117
- ages.append((current_time - created_at).seconds / 3600) # in hours
118
- avg_age_hours = sum(ages) / len(ages) if ages else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  return {
121
- 'total_entries': len(self.cache),
122
- 'total_saved_tokens': total_saved_tokens,
123
- 'total_access_count': total_access_count,
124
- 'average_age_hours': round(avg_age_hours, 1),
125
- 'ttl_hours': self.ttl_hours,
126
- 'max_entries': self.max_entries,
127
- 'storage_mode': 'In-Memory (Hugging Face)'
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 caching
581
  normalized_question = question.strip().lower()
582
- # Remove extra whitespace and special characters
 
583
  normalized_question = re.sub(r'\s+', ' ', normalized_question)
584
- normalized_question = normalized_question[:200] # Limit length
585
 
586
- # Create a hash-based key for better performance
587
- key_string = f"{subject}|{chapter_name}|{normalized_question}"
 
 
 
 
 
 
 
 
 
 
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 - Streamlit will auto-render LaTeX
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 - Streamlit will handle LaTeX automatically
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 # Not cached (new response)
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 = HybridCache(ttl_hours=24, max_entries=100)
 
 
 
 
 
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 (SKIPPED - ADD FROM YOUR SIDE)
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
- "📘 হিন্দ (Hindi)": {
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
- # DISPLAY CACHED ANSWER
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1680
  # ===============================
1681
- if hasattr(st.session_state, 'show_cached_answer') and st.session_state.show_cached_answer:
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
- cached_data = st.session_state.cached_answer_data
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> Cache Hit
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 # $0.14 per 100K tokens
 
 
 
 
 
 
 
 
 
 
1732
  st.info(f"""
1733
  **Cache Benefits:**
1734
- - ⚡ Instant response (no API call)
1735
- - 💰 No token cost (saved {cached_data.get('tokens', 0):,} tokens)
1736
- - 💵 Estimated savings: ${estimated_cost:.4f}
1737
- - 🌿 Environmentally friendly
1738
 
1739
- **Cache Details:**
1740
- - Cache entries: {cache_stats['total_entries']}
 
1741
  - Total tokens saved: {cache_stats['total_saved_tokens']:,}
1742
- - Cache hit count: {cached_data.get('access_count', 1)}
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 this cached answer", type="secondary", use_container_width=True):
1751
- if st.session_state.current_cache_key in st.session_state.cache_manager.cache:
1752
- st.session_state.cache_manager.cache.pop(st.session_state.current_cache_key)
1753
- st.success("Cache entry cleared!")
1754
- st.session_state.show_cached_answer = False
1755
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
1756
 
1757
  with col2:
1758
- if st.button("🧹 Clear all cache", type="secondary", use_container_width=True):
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
- del st.session_state.cached_answer_data
1771
- del st.session_state.current_cache_key
 
 
1772
 
1773
  # ===============================
1774
  # PROCESS QUESTION WITH STREAMING
1775
  # ===============================
1776
- if st.session_state.get('processing') and question and api_key and not st.session_state.get('show_cached_answer', False):
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
- st.caption(f"📊 ট'কেন ব্যৱহৃত: {st.session_state.tokens_used:,}")
 
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[-3:]), 1):
1855
- cache_indicator = " ⚡" if item.get('cached') else ""
1856
- with st.expander(f"প্ৰশ্ন {i}: {item['question']} ({item['timestamp']}{cache_indicator})"):
 
 
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
- st.metric("Avg Age", f"{cache_stats['average_age_hours']}h")
 
1881
 
1882
  with col2:
1883
- st.metric("Tokens Saved", f"{cache_stats['total_saved_tokens']:,}")
1884
- st.metric("TTL", f"{cache_stats['ttl_hours']}h")
 
1885
 
1886
- # Cost savings
1887
- estimated_cost = cache_stats['total_saved_tokens'] * 0.0000014 # $0.14 per 100K tokens
1888
- st.metric("💰 Cost Saved", f"${estimated_cost:.4f}")
 
 
 
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("All cache cleared!")
1907
  st.rerun()
1908
 
1909
- # View recent cached questions
1910
- with st.expander("📋 Recent Cached Questions"):
1911
- recent_entries = list(st.session_state.cache_manager.cache.items())[:5]
1912
- for key, entry in recent_entries:
1913
- question_text = entry.get('question', 'N/A')[:40] + "..."
1914
- age_hours = round((datetime.now() - datetime.fromisoformat(entry.get('created_at', datetime.now().isoformat()))).seconds / 3600, 1)
1915
- st.caption(f"• {question_text}")
1916
- st.write(f" 📚 {entry.get('subject', 'N/A').split()[0]} 🔢 {entry.get('access_count', 1)}x • ⏳ {age_hours}h ago")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1917
  else:
1918
  st.info("Cache is empty. Ask some questions to build cache!")
1919
-
1920
- # Cache performance note
1921
- st.markdown("---")
1922
- st.caption("""
1923
- **💡 Cache Performance:**
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