Spaces:
Sleeping
Sleeping
Commit ·
54f146a
0
Parent(s):
Clean push without binary history
Browse files- .gitattributes +2 -0
- .github/workflows/auto_sync.yml +20 -0
- .gitignore +16 -0
- LICENSE +21 -0
- README.md +69 -0
- main.py +269 -0
- requirements.txt +11 -0
- simple_notes.json +1 -0
- vault_logic.py +290 -0
.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fonts/dejavu-sans/*.ttf filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
fonts/dejavu-sans/*.pkl filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/auto_sync.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face hub
|
| 2 |
+
on:
|
| 3 |
+
push:
|
| 4 |
+
branches: [main]
|
| 5 |
+
workflow_dispatch:
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
sync-to-hub:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
steps:
|
| 11 |
+
- uses: actions/checkout@v3
|
| 12 |
+
with:
|
| 13 |
+
fetch-depth: 0
|
| 14 |
+
lfs: true
|
| 15 |
+
- name: Push to hub
|
| 16 |
+
env:
|
| 17 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 18 |
+
run: git push --force https://mubi0613:$HF_TOKEN@huggingface.co/spaces/mubi0613/SecureVault-AI main
|
| 19 |
+
|
| 20 |
+
----------------------------------------------------------------------------------------------------------------
|
.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Local Vault Data
|
| 2 |
+
notes.json
|
| 3 |
+
vault_config.json
|
| 4 |
+
usage_stats.json
|
| 5 |
+
feedback_log.json
|
| 6 |
+
.env
|
| 7 |
+
|
| 8 |
+
# Python Environment
|
| 9 |
+
__pycache__/
|
| 10 |
+
*.py[cod]
|
| 11 |
+
*$py.class
|
| 12 |
+
venv/
|
| 13 |
+
.env
|
| 14 |
+
|
| 15 |
+
# Streamlit logs
|
| 16 |
+
.streamlit/
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 mubi0613
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛡️ SecureVault AI: Privacy-First RAG Knowledge Base
|
| 2 |
+
|
| 3 |
+
A production-grade, secure note-taking application that integrates **Retrieval-Augmented Generation (RAG)** with **Zero-Knowledge Encryption**. Built for users who need AI-powered insights without sacrificing data privacy.
|
| 4 |
+
|
| 5 |
+
## 🚀 Key Engineering Features
|
| 6 |
+
* **Hybrid Security Model:** Uses **PBKDF2** key derivation and **AES-256 (Fernet)** encryption. Data is only decrypted in-memory during an active session.
|
| 7 |
+
* **Local RAG Engine:** Utilizes **FAISS** (Facebook AI Similarity Search) and `all-MiniLM-L6-v2` embeddings for lightning-fast, local semantic search—no private data ever leaves your machine.
|
| 8 |
+
* **AI Observability:** Integrated **Token & Cost Tracker** and a **Feedback Loop** (Correct/Wrong logging) to audit retrieval accuracy and identify gaps in the knowledge base.
|
| 9 |
+
* **Fail-Safe Recovery:** Implements a hashed **Recovery Key** system for account resets while maintaining the mathematical integrity of encrypted data.
|
| 10 |
+
* **Smart Exports:** Unicode-compliant PDF and DOCX generation with specialized font embedding for multi-language support.
|
| 11 |
+
|
| 12 |
+
## 🛠️ Technical Stack
|
| 13 |
+
* **Frontend:** Streamlit (Custom CSS & Session Management)
|
| 14 |
+
* **AI/NLP:** Sentence-Transformers, TextBlob, FAISS
|
| 15 |
+
* **Cryptography:** Python `cryptography` library (SHA-256 & PBKDF2)
|
| 16 |
+
* **Data:** JSON-based persistent storage
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 🔐 Security Architecture
|
| 21 |
+
1. **PIN Hashing:** The Master PIN is never stored; only its **SHA-256** hash is kept for authentication.
|
| 22 |
+
2. **Zero-Knowledge:** The PIN is used to derive the encryption key. If the PIN is lost, "Secret" notes remain mathematically inaccessible.
|
| 23 |
+
3. **Auto-Lock Engine:** A session-based activity monitor automatically wipes the memory and locks the vault after 150 seconds of inactivity.
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## 📦 Installation & Setup
|
| 30 |
+
|
| 31 |
+
### 1. Clone the Repository
|
| 32 |
+
```bash
|
| 33 |
+
git clone [https://github.com/mubi0613/SecureVault-AI.git](https://github.com/mubi0613/SecureVault-AI.git)
|
| 34 |
+
cd SecureVault-AI
|
| 35 |
+
|
| 36 |
+
2. Install Dependencies
|
| 37 |
+
Ensure you have Python 3.9+ installed, then run:
|
| 38 |
+
```bash
|
| 39 |
+
pip install -r requirements.txt
|
| 40 |
+
|
| 41 |
+
3. Run the Application
|
| 42 |
+
```bash
|
| 43 |
+
streamlit run main.py
|
| 44 |
+
|
| 45 |
+
📖 Usage Guide
|
| 46 |
+
Initialize: Set your Master PIN and save your Recovery Key.
|
| 47 |
+
|
| 48 |
+
Create Notes: Use the sidebar to add notes. Toggle "Mark as Secret" to apply AES-256 encryption.
|
| 49 |
+
|
| 50 |
+
AI Search: Open the "Ask Your Vault" expander to query your notes using natural language.
|
| 51 |
+
|
| 52 |
+
Manage: Edit or delete notes, and export them as PDF/DOCX for offline use.
|
| 53 |
+
|
| 54 |
+
Monitor: Check the "AI Resources" section in the sidebar to view estimated token usage and costs.
|
| 55 |
+
|
| 56 |
+
📂 Project Structure
|
| 57 |
+
Plaintext
|
| 58 |
+
SecureVault-AI/
|
| 59 |
+
├── main.py # Streamlit UI & Session Management
|
| 60 |
+
├── vault_logic.py # Cryptography, RAG, & File IO Logic
|
| 61 |
+
├── requirements.txt # Project Dependencies
|
| 62 |
+
├── .gitignore
|
| 63 |
+
├── fonts/ # Custom fonts for PDF Unicode support
|
| 64 |
+
└── README.md # Documentation
|
| 65 |
+
⚠️ Important Note on Security
|
| 66 |
+
This application is designed with a Zero-Knowledge philosophy. The developers cannot recover your notes if you lose both your Master PIN and your Recovery Key. Data integrity is the sole responsibility of the user.
|
| 67 |
+
|
| 68 |
+
📜 License
|
| 69 |
+
Distributed under the MIT License. See LICENSE for more information.
|
main.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import vault_logic as vl
|
| 4 |
+
import time
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# --- CONFIGURATION ---
|
| 9 |
+
AUTO_LOCK_SECONDS = 150 # 2 Minutes
|
| 10 |
+
|
| 11 |
+
st.set_page_config(page_title="AI Vault", layout="wide")
|
| 12 |
+
|
| 13 |
+
# --- 0. FIRST TIME SETUP ---
|
| 14 |
+
if not vl.is_vault_initialized():
|
| 15 |
+
st.title("🔒 Initialize Your Secure Vault")
|
| 16 |
+
|
| 17 |
+
# 1. Primary Action: The PIN Box
|
| 18 |
+
st.info("👋 Welcome! To get started, please set your Master PIN below.")
|
| 19 |
+
|
| 20 |
+
with st.form("setup_form"):
|
| 21 |
+
new_pin = st.text_input("Create Master PIN", type="password", help="Minimum 4 digits")
|
| 22 |
+
conf_pin = st.text_input("Confirm Master PIN", type="password")
|
| 23 |
+
submit = st.form_submit_button("Setup Vault", type="primary")
|
| 24 |
+
|
| 25 |
+
# 2. Professional Spacing
|
| 26 |
+
st.write("")
|
| 27 |
+
st.write("")
|
| 28 |
+
st.divider()
|
| 29 |
+
|
| 30 |
+
# 3. Security & Recovery Information
|
| 31 |
+
if 'generated_recovery' not in st.session_state:
|
| 32 |
+
st.session_state.generated_recovery = vl.generate_recovery_key()
|
| 33 |
+
|
| 34 |
+
st.warning("⚠️ **CRITICAL: Save Your Recovery Key**")
|
| 35 |
+
st.caption("RECOVERY - KEY : This is the ONLY way to reset your vault access if you forget your PIN.")
|
| 36 |
+
st.code(st.session_state.generated_recovery, language=None)
|
| 37 |
+
|
| 38 |
+
# Important Security Note (Liability Waiver)
|
| 39 |
+
st.markdown("""
|
| 40 |
+
<div style="background-color: #ff4b4b11; padding: 20px; border-radius: 10px; border: 1px solid #ff4b4b;">
|
| 41 |
+
<p style="color: #ff4b4b; font-weight: bold; margin-bottom: 5px;">🛑 Important Security Notice:</p>
|
| 42 |
+
<p style="font-size: 0.9rem; margin-bottom: 10px;">
|
| 43 |
+
This vault utilizes <b>Zero-Knowledge AES-Encryption</b>. Your PIN is never stored; it is only hashed.
|
| 44 |
+
While the Recovery Key can reset app access, it <b>cannot decrypt</b> existing secret data
|
| 45 |
+
locked with a different PIN.
|
| 46 |
+
</p>
|
| 47 |
+
<p style="font-size: 0.8rem; opacity: 0.8;">
|
| 48 |
+
<i>The developer holds no responsibility for data loss resulting from forgotten credentials.</i>
|
| 49 |
+
</p>
|
| 50 |
+
</div>
|
| 51 |
+
""", unsafe_allow_html=True)
|
| 52 |
+
|
| 53 |
+
if submit:
|
| 54 |
+
if new_pin == conf_pin and len(new_pin) >= 4:
|
| 55 |
+
vl.initialize_vault(new_pin, st.session_state.generated_recovery)
|
| 56 |
+
st.success("Vault Securely Initialized! Refreshing...")
|
| 57 |
+
time.sleep(1.5)
|
| 58 |
+
st.rerun()
|
| 59 |
+
else:
|
| 60 |
+
st.error("PINs must match and be at least 4 digits.")
|
| 61 |
+
st.stop()
|
| 62 |
+
|
| 63 |
+
# --- 1. SESSION STATE ---
|
| 64 |
+
if 'notes' not in st.session_state:
|
| 65 |
+
st.session_state.notes = vl.load_notes()
|
| 66 |
+
if 'edit_note_id' not in st.session_state:
|
| 67 |
+
st.session_state.edit_note_id = None
|
| 68 |
+
if 'vault_unlocked' not in st.session_state:
|
| 69 |
+
st.session_state.vault_unlocked = False
|
| 70 |
+
if 'master_pin' not in st.session_state:
|
| 71 |
+
st.session_state.master_pin = "" # We store the actual PIN in session only while unlocked
|
| 72 |
+
if 'temp_content' not in st.session_state:
|
| 73 |
+
st.session_state.temp_content = ""
|
| 74 |
+
if 'form_iteration' not in st.session_state:
|
| 75 |
+
st.session_state.form_iteration = 0
|
| 76 |
+
if 'last_activity' not in st.session_state:
|
| 77 |
+
st.session_state.last_activity = time.time()
|
| 78 |
+
if 'show_lock_alert' not in st.session_state:
|
| 79 |
+
st.session_state.show_lock_alert = False
|
| 80 |
+
|
| 81 |
+
# --- 2. AUTO-LOCK ENGINE ---
|
| 82 |
+
def update_activity():
|
| 83 |
+
st.session_state.last_activity = time.time()
|
| 84 |
+
st.session_state.show_lock_alert = False
|
| 85 |
+
|
| 86 |
+
if st.session_state.vault_unlocked:
|
| 87 |
+
elapsed_time = time.time() - st.session_state.last_activity
|
| 88 |
+
if elapsed_time > AUTO_LOCK_SECONDS:
|
| 89 |
+
st.session_state.vault_unlocked = False
|
| 90 |
+
st.session_state.edit_note_id = None
|
| 91 |
+
st.session_state.temp_content = ""
|
| 92 |
+
st.session_state.show_lock_alert = True
|
| 93 |
+
st.rerun()
|
| 94 |
+
|
| 95 |
+
# --- 3. CSS & ALERTS ---
|
| 96 |
+
st.markdown("""
|
| 97 |
+
<style>
|
| 98 |
+
.block-container {padding-top: 2rem !important;}
|
| 99 |
+
[data-testid="stSidebarNav"] {display: none;}
|
| 100 |
+
</style>
|
| 101 |
+
""", unsafe_allow_html=True)
|
| 102 |
+
|
| 103 |
+
if st.session_state.show_lock_alert:
|
| 104 |
+
st.error("🚨 **THE VAULT IS AUTO LOCKED**")
|
| 105 |
+
|
| 106 |
+
# --- 4. SIDEBAR ---
|
| 107 |
+
with st.sidebar:
|
| 108 |
+
st.title("➕ Add/Edit Note")
|
| 109 |
+
|
| 110 |
+
c_title, c_content, c_secret = "", "", False
|
| 111 |
+
if st.session_state.edit_note_id:
|
| 112 |
+
n = next((x for x in st.session_state.notes if x['id'] == st.session_state.edit_note_id), None)
|
| 113 |
+
if n:
|
| 114 |
+
c_title = n['title']
|
| 115 |
+
c_content = vl.decrypt_data(n['content'], st.session_state.master_pin) if (n.get('secret') and st.session_state.vault_unlocked) else n['content']
|
| 116 |
+
c_secret = n.get('secret', False)
|
| 117 |
+
|
| 118 |
+
default_content = st.session_state.temp_content if st.session_state.temp_content else c_content
|
| 119 |
+
|
| 120 |
+
with st.form(key=f"note_form_{st.session_state.form_iteration}", clear_on_submit=False):
|
| 121 |
+
new_t = st.text_input("Title", value=c_title)
|
| 122 |
+
new_c = st.text_area("Content", value=default_content, height=150)
|
| 123 |
+
m_secret = st.checkbox("🤫 Mark as Secret", value=c_secret) if st.session_state.vault_unlocked else False
|
| 124 |
+
|
| 125 |
+
if st.form_submit_button("✨ AI Summarize"):
|
| 126 |
+
update_activity()
|
| 127 |
+
if new_c:
|
| 128 |
+
summary_result = vl.ai_summarize_text(new_c)
|
| 129 |
+
st.session_state.temp_content = summary_result
|
| 130 |
+
st.rerun()
|
| 131 |
+
|
| 132 |
+
if st.form_submit_button("Save", type="primary", use_container_width=True):
|
| 133 |
+
update_activity()
|
| 134 |
+
if new_t or new_c:
|
| 135 |
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 136 |
+
final_content = vl.encrypt_data(new_c, st.session_state.master_pin) if m_secret else new_c
|
| 137 |
+
if st.session_state.edit_note_id:
|
| 138 |
+
for n in st.session_state.notes:
|
| 139 |
+
if n['id'] == st.session_state.edit_note_id:
|
| 140 |
+
n.update({"title": new_t, "content": final_content, "timestamp": ts, "secret": m_secret})
|
| 141 |
+
st.session_state.edit_note_id = None
|
| 142 |
+
else:
|
| 143 |
+
st.session_state.notes.insert(0, {"id": int(datetime.now().timestamp()), "title": new_t, "content": final_content, "timestamp": ts, "secret": m_secret})
|
| 144 |
+
|
| 145 |
+
vl.save_notes(st.session_state.notes)
|
| 146 |
+
st.session_state.temp_content = ""
|
| 147 |
+
st.session_state.form_iteration += 1
|
| 148 |
+
st.rerun()
|
| 149 |
+
|
| 150 |
+
st.divider()
|
| 151 |
+
st.write("### 🔐 Vault Security")
|
| 152 |
+
if not st.session_state.vault_unlocked:
|
| 153 |
+
pin_input = st.text_input("Enter Stealth PIN", type="password", key="pin_entry")
|
| 154 |
+
if pin_input:
|
| 155 |
+
if vl.verify_pin(pin_input):
|
| 156 |
+
update_activity()
|
| 157 |
+
st.session_state.vault_unlocked = True
|
| 158 |
+
st.session_state.master_pin = pin_input
|
| 159 |
+
st.rerun()
|
| 160 |
+
else: st.error("Incorrect PIN")
|
| 161 |
+
|
| 162 |
+
# NEW: Recovery Logic
|
| 163 |
+
with st.expander("Forgot PIN?"):
|
| 164 |
+
recovery_in = st.text_input("Enter Recovery Key")
|
| 165 |
+
if st.button("Reset Vault PIN"):
|
| 166 |
+
# Logic: If recovery key matches, delete config so they can start over
|
| 167 |
+
# Note: Old encrypted notes stay encrypted (this is the cost of security!)
|
| 168 |
+
if vl.verify_recovery_key(recovery_in): # You'll add this to vault_logic
|
| 169 |
+
os.remove("vault_config.json")
|
| 170 |
+
st.warning("Vault Identity Reset. Please refresh to set a new PIN.")
|
| 171 |
+
st.rerun()
|
| 172 |
+
|
| 173 |
+
else:
|
| 174 |
+
st.success("🔓 Vault is Open")
|
| 175 |
+
time_left = int(AUTO_LOCK_SECONDS - (time.time() - st.session_state.last_activity))
|
| 176 |
+
st.caption(f"Auto-locking in {max(0, time_left)}s")
|
| 177 |
+
if st.button("🔒 Close Vault", use_container_width=True):
|
| 178 |
+
st.session_state.vault_unlocked = False
|
| 179 |
+
st.session_state.master_pin = ""
|
| 180 |
+
st.rerun()
|
| 181 |
+
|
| 182 |
+
st.divider()
|
| 183 |
+
st.subheader("📊 AI Resources")
|
| 184 |
+
if os.path.exists("usage_stats.json"):
|
| 185 |
+
with open("usage_stats.json", "r") as f:
|
| 186 |
+
stats = json.load(f)
|
| 187 |
+
col1, col2 = st.columns(2)
|
| 188 |
+
col1.metric("Tokens", f"{int(stats['total_tokens'])}")
|
| 189 |
+
col2.metric("Cost", f"${stats['total_cost']:.5f}")
|
| 190 |
+
else:
|
| 191 |
+
st.info("No AI usage data yet.")
|
| 192 |
+
|
| 193 |
+
# --- 5. MAIN PAGE ---
|
| 194 |
+
st.title(vl.get_page_heading(st.session_state.vault_unlocked))
|
| 195 |
+
|
| 196 |
+
# --- 6. RAG CHAT INTERFACE ---
|
| 197 |
+
if st.session_state.vault_unlocked:
|
| 198 |
+
with st.expander("💬 Ask Your Vault (AI Search)", expanded=False):
|
| 199 |
+
user_query = st.text_input("Ask a question about your notes:", placeholder="e.g., What are my goals for 2026?")
|
| 200 |
+
|
| 201 |
+
if user_query:
|
| 202 |
+
update_activity()
|
| 203 |
+
# 1. Prepare Decrypted Context
|
| 204 |
+
decrypted_texts = []
|
| 205 |
+
for n in st.session_state.notes:
|
| 206 |
+
content = vl.decrypt_data(n['content'], st.session_state.master_pin) if n.get('secret') else n['content']
|
| 207 |
+
decrypted_texts.append(content)
|
| 208 |
+
|
| 209 |
+
# 2. RAG Logic
|
| 210 |
+
index, text_data = vl.create_vector_index([{"content": t} for t in decrypted_texts])
|
| 211 |
+
context_chunks = vl.query_vault(user_query, index, text_data)
|
| 212 |
+
|
| 213 |
+
# 3. AI Answer
|
| 214 |
+
context_str = "\n".join(context_chunks)
|
| 215 |
+
prompt = f"Using ONLY these notes:\n{context_str}\n\nAnswer this question: {user_query}"
|
| 216 |
+
|
| 217 |
+
with st.spinner("Searching Vault..."):
|
| 218 |
+
answer = vl.get_gemini_response(user_query, context_str)
|
| 219 |
+
st.info(f"**AI Answer:**\n{answer}")
|
| 220 |
+
|
| 221 |
+
# 4. Feedback System ---
|
| 222 |
+
st.write("---")
|
| 223 |
+
st.write("Was this answer helpful?")
|
| 224 |
+
f_col1, f_col2 = st.columns([1, 5])
|
| 225 |
+
|
| 226 |
+
if f_col1.button("✅ Yes", key="fb_yes"):
|
| 227 |
+
vl.log_feedback(user_query, answer, context_str, "Correct")
|
| 228 |
+
st.success("Thanks for the feedback!")
|
| 229 |
+
|
| 230 |
+
if f_col2.button("❌ No / Wrong", key="fb_no"):
|
| 231 |
+
vl.log_feedback(user_query, answer, context_str, "Wrong")
|
| 232 |
+
st.warning("Logged as a failure.")
|
| 233 |
+
|
| 234 |
+
st.divider()
|
| 235 |
+
search = st.text_input("🔍 Search...", placeholder="Filter notes...")
|
| 236 |
+
|
| 237 |
+
# --- 7. Display Grid ---
|
| 238 |
+
cols = st.columns(3)
|
| 239 |
+
filtered = vl.get_filtered_notes(st.session_state.notes, st.session_state.vault_unlocked, search)
|
| 240 |
+
|
| 241 |
+
for idx, note in enumerate(filtered):
|
| 242 |
+
with cols[idx % 3]:
|
| 243 |
+
with st.container(border=True):
|
| 244 |
+
display_content = vl.decrypt_data(note['content'], st.session_state.master_pin) if (note.get('secret') and st.session_state.vault_unlocked) else note['content']
|
| 245 |
+
|
| 246 |
+
st.subheader(f"🔒 {note['title']}" if note.get('secret') else note['title'])
|
| 247 |
+
st.write(display_content[:200] + "..." if len(display_content) > 200 else display_content)
|
| 248 |
+
st.caption(f"🕒 {note['timestamp']}")
|
| 249 |
+
|
| 250 |
+
eb, db = st.columns(2)
|
| 251 |
+
with eb:
|
| 252 |
+
if st.button("✏️ Edit", key=f"e_{note['id']}"):
|
| 253 |
+
update_activity()
|
| 254 |
+
st.session_state.edit_note_id = note['id']
|
| 255 |
+
st.rerun()
|
| 256 |
+
with db:
|
| 257 |
+
if st.button("🗑️ Delete", key=f"d_{note['id']}"):
|
| 258 |
+
st.session_state.notes = [x for x in st.session_state.notes if x['id'] != note['id']]
|
| 259 |
+
vl.save_notes(st.session_state.notes)
|
| 260 |
+
st.rerun()
|
| 261 |
+
|
| 262 |
+
st.divider()
|
| 263 |
+
p_col, d_col = st.columns(2)
|
| 264 |
+
with p_col:
|
| 265 |
+
pdf_bytes = vl.create_pdf(note['title'], display_content)
|
| 266 |
+
st.download_button("📄 Download PDF", data=pdf_bytes, file_name=f"{note['title']}.pdf", key=f"pdf_{note['id']}", use_container_width=True)
|
| 267 |
+
with d_col:
|
| 268 |
+
docx_bytes = vl.create_docx(note['title'], display_content)
|
| 269 |
+
st.download_button("📝 Download DOCX", data=docx_bytes, file_name=f"{note['title']}.docx", key=f"docx_{note['id']}", use_container_width=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.42.0
|
| 2 |
+
cryptography
|
| 3 |
+
textblob
|
| 4 |
+
fpdf2
|
| 5 |
+
python-docx
|
| 6 |
+
faiss-cpu
|
| 7 |
+
numpy
|
| 8 |
+
sentence-transformers
|
| 9 |
+
google-genai
|
| 10 |
+
python-dotenv
|
| 11 |
+
nltk
|
simple_notes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[]
|
vault_logic.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import base64
|
| 4 |
+
import hashlib
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from cryptography.fernet import Fernet
|
| 8 |
+
from cryptography.hazmat.primitives import hashes
|
| 9 |
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
| 10 |
+
from textblob import TextBlob
|
| 11 |
+
from fpdf import FPDF
|
| 12 |
+
from docx import Document
|
| 13 |
+
import io
|
| 14 |
+
import faiss
|
| 15 |
+
import numpy as np
|
| 16 |
+
from sentence_transformers import SentenceTransformer
|
| 17 |
+
import secrets
|
| 18 |
+
import string
|
| 19 |
+
from google import genai # Gemini
|
| 20 |
+
from dotenv import load_dotenv
|
| 21 |
+
import nltk
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
nltk.data.find('tokenizers/punkt_tab')
|
| 25 |
+
except LookupError:
|
| 26 |
+
nltk.download('punkt_tab')
|
| 27 |
+
|
| 28 |
+
# This line reads the .env file and makes the API key available
|
| 29 |
+
load_dotenv(override=True) # 'override=True' forces it to refresh the key if you changed .env
|
| 30 |
+
|
| 31 |
+
NOTES_FILE = "notes.json"
|
| 32 |
+
CONFIG_FILE = "vault_config.json"
|
| 33 |
+
|
| 34 |
+
# Load a small, fast model for embeddings (runs locally)
|
| 35 |
+
embed_model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 36 |
+
|
| 37 |
+
# --- 0. PIN & KEY LOGIC ---
|
| 38 |
+
def get_pin_hash(pin: str):
|
| 39 |
+
"""Creates a secure SHA-256 hash of the PIN"""
|
| 40 |
+
return hashlib.sha256(pin.encode()).hexdigest()
|
| 41 |
+
|
| 42 |
+
def is_vault_initialized():
|
| 43 |
+
"""Checks if a user has set a PIN yet"""
|
| 44 |
+
return os.path.exists(CONFIG_FILE)
|
| 45 |
+
|
| 46 |
+
def generate_recovery_key():
|
| 47 |
+
"""Generates a random 16-character recovery string"""
|
| 48 |
+
chars = string.ascii_uppercase + string.digits
|
| 49 |
+
return ''.join(secrets.choice(chars) for _ in range(16))
|
| 50 |
+
|
| 51 |
+
def initialize_vault(pin: str, recovery_key: str):
|
| 52 |
+
"""Saves the user's initial PIN hash and recovery key hash"""
|
| 53 |
+
with open(CONFIG_FILE, "w") as f:
|
| 54 |
+
json.dump({"pin_hash": get_pin_hash(pin),
|
| 55 |
+
"recovery_hash": get_pin_hash(recovery_key) # We hash the recovery key too!
|
| 56 |
+
}, f)
|
| 57 |
+
|
| 58 |
+
def verify_recovery_key(input_key: str):
|
| 59 |
+
"""Verifies the recovery key against the stored hash"""
|
| 60 |
+
if not is_vault_initialized():
|
| 61 |
+
return False
|
| 62 |
+
with open(CONFIG_FILE, "r") as f:
|
| 63 |
+
stored_recovery_hash = json.load(f).get("recovery_hash")
|
| 64 |
+
return get_pin_hash(input_key) == stored_recovery_hash
|
| 65 |
+
|
| 66 |
+
def verify_pin(input_pin: str):
|
| 67 |
+
"""Verifies input against stored hash"""
|
| 68 |
+
if not is_vault_initialized():
|
| 69 |
+
return False
|
| 70 |
+
with open(CONFIG_FILE, "r") as f:
|
| 71 |
+
stored_hash = json.load(f).get("pin_hash")
|
| 72 |
+
return get_pin_hash(input_pin) == stored_hash
|
| 73 |
+
|
| 74 |
+
# --- 1. KEY GENERATION (From your PIN) ---
|
| 75 |
+
def generate_key(pin: str):
|
| 76 |
+
"""Derives a functional encryption key from the user's PIN"""
|
| 77 |
+
password = pin.encode()
|
| 78 |
+
# We use a static salt for this local project (Professional apps use random salts)
|
| 79 |
+
salt = b'stable_salt_123'
|
| 80 |
+
kdf = PBKDF2HMAC(
|
| 81 |
+
algorithm=hashes.SHA256(),
|
| 82 |
+
length=32,
|
| 83 |
+
salt=salt,
|
| 84 |
+
iterations=100000,
|
| 85 |
+
)
|
| 86 |
+
key = base64.urlsafe_b64encode(kdf.derive(password))
|
| 87 |
+
return Fernet(key)
|
| 88 |
+
|
| 89 |
+
# --- 2. ENCRYPTION / DECRYPTION ---
|
| 90 |
+
def encrypt_data(data_string, pin):
|
| 91 |
+
"""Turns readable text into scrambled code"""
|
| 92 |
+
f = generate_key(pin)
|
| 93 |
+
return f.encrypt(data_string.encode()).decode()
|
| 94 |
+
|
| 95 |
+
def decrypt_data(encrypted_string, pin):
|
| 96 |
+
"""Turns scrambled code back into readable text"""
|
| 97 |
+
try:
|
| 98 |
+
f = generate_key(pin)
|
| 99 |
+
return f.decrypt(encrypted_string.encode()).decode()
|
| 100 |
+
except Exception:
|
| 101 |
+
return "[Decryption Error: Check PIN]"
|
| 102 |
+
|
| 103 |
+
# --- 3. UPDATED LOAD/SAVE ---
|
| 104 |
+
def load_notes():
|
| 105 |
+
if os.path.exists(NOTES_FILE):
|
| 106 |
+
with open(NOTES_FILE, "r") as f:
|
| 107 |
+
return json.load(f)
|
| 108 |
+
return []
|
| 109 |
+
|
| 110 |
+
def save_notes(notes_list):
|
| 111 |
+
with open(NOTES_FILE, "w") as f:
|
| 112 |
+
json.dump(notes_list, f, indent=4)
|
| 113 |
+
|
| 114 |
+
def get_page_heading(is_unlocked):
|
| 115 |
+
return "🛡️ Safe Vault" if is_unlocked else "📝 My Notes"
|
| 116 |
+
|
| 117 |
+
def get_filtered_notes(all_notes, is_unlocked, search_query):
|
| 118 |
+
visible = all_notes
|
| 119 |
+
if not is_unlocked:
|
| 120 |
+
visible = [n for n in all_notes if not n.get('secret', False)]
|
| 121 |
+
|
| 122 |
+
query = search_query.lower()
|
| 123 |
+
return [n for n in visible if query in n['title'].lower() or query in n['content'].lower()]
|
| 124 |
+
|
| 125 |
+
# --- 4. AI FEATURES ---
|
| 126 |
+
def ai_summarize_text(text):
|
| 127 |
+
"""Uses NLP to extract key points from long notes."""
|
| 128 |
+
if len(text) < 50:
|
| 129 |
+
return text # Don't summarize very short notes
|
| 130 |
+
|
| 131 |
+
blob = TextBlob(text)
|
| 132 |
+
# Extracting sentences and picking the top 2 for a summary
|
| 133 |
+
sentences = [str(s) for s in blob.sentences]
|
| 134 |
+
|
| 135 |
+
if len(sentences) > 2:
|
| 136 |
+
summary = "✨ AI Summary:\n" + "\n".join([f"• {s}" for s in sentences[:2]])
|
| 137 |
+
|
| 138 |
+
# NEW: Log the usage here!
|
| 139 |
+
track_usage(text, type="input")
|
| 140 |
+
track_usage(summary, type="output")
|
| 141 |
+
return summary
|
| 142 |
+
return text
|
| 143 |
+
|
| 144 |
+
# --- 5. EXPORTS ---
|
| 145 |
+
# The Logic Functions : These handle the actual file creation.
|
| 146 |
+
def create_pdf(title, content):
|
| 147 |
+
try:
|
| 148 |
+
pdf = FPDF()
|
| 149 |
+
pdf.add_page()
|
| 150 |
+
|
| 151 |
+
# Updated to match your folder: fonts -> dejavu-sans -> DejaVuSans.ttf
|
| 152 |
+
font_path = os.path.join("fonts", "dejavu-sans", "DejaVuSans.ttf")
|
| 153 |
+
|
| 154 |
+
if os.path.exists(font_path):
|
| 155 |
+
pdf.add_font('DejaVu', '', font_path, uni=True)
|
| 156 |
+
pdf.set_font('DejaVu', '', 14)
|
| 157 |
+
else:
|
| 158 |
+
# This is where it was falling back before, causing the "?" symbols
|
| 159 |
+
pdf.set_font("Arial", "B", 16)
|
| 160 |
+
|
| 161 |
+
# Write Title
|
| 162 |
+
pdf.multi_cell(0, 10, title)
|
| 163 |
+
pdf.ln(5)
|
| 164 |
+
|
| 165 |
+
# Write Content
|
| 166 |
+
if os.path.exists(font_path):
|
| 167 |
+
pdf.set_font('DejaVu', '', 12)
|
| 168 |
+
else:
|
| 169 |
+
pdf.set_font("Arial", "", 12)
|
| 170 |
+
|
| 171 |
+
pdf.multi_cell(0, 10, content)
|
| 172 |
+
|
| 173 |
+
# Output as bytes
|
| 174 |
+
return pdf.output(dest='S').encode('latin-1')
|
| 175 |
+
except Exception as e:
|
| 176 |
+
return f"PDF Error: {str(e)}".encode('latin-1')
|
| 177 |
+
|
| 178 |
+
def create_docx(title, content):
|
| 179 |
+
doc = Document()
|
| 180 |
+
doc.add_heading(title, 0)
|
| 181 |
+
doc.add_paragraph(content)
|
| 182 |
+
bio = io.BytesIO()
|
| 183 |
+
doc.save(bio)
|
| 184 |
+
return bio.getvalue()
|
| 185 |
+
|
| 186 |
+
# --- 6. AI & RESOURCE TRACKER ---
|
| 187 |
+
def track_usage(text, type="input"):
|
| 188 |
+
# Rough estimation: 1 token approx 4 characters
|
| 189 |
+
tokens = len(text) / 4
|
| 190 |
+
cost = (tokens / 1000) * 0.000125 # Estimate for Gemini 1.5 Flash
|
| 191 |
+
|
| 192 |
+
# Save to a local JSON file to persist data
|
| 193 |
+
usage_file = "usage_stats.json"
|
| 194 |
+
stats = {"total_tokens": 0, "total_cost": 0.0}
|
| 195 |
+
|
| 196 |
+
if os.path.exists(usage_file):
|
| 197 |
+
with open(usage_file, "r") as f:
|
| 198 |
+
stats = json.load(f)
|
| 199 |
+
|
| 200 |
+
stats["total_tokens"] += tokens
|
| 201 |
+
stats["total_cost"] += cost
|
| 202 |
+
|
| 203 |
+
with open(usage_file, "w") as f:
|
| 204 |
+
json.dump(stats, f)
|
| 205 |
+
|
| 206 |
+
return stats
|
| 207 |
+
|
| 208 |
+
# --- 7. RAG & FEEDBACK ---
|
| 209 |
+
def create_vector_index(notes):
|
| 210 |
+
"""Turns notes into a searchable mathematical index"""
|
| 211 |
+
if not notes:
|
| 212 |
+
return None, []
|
| 213 |
+
|
| 214 |
+
# Extract only the text content
|
| 215 |
+
text_data = [n['content'] for n in notes]
|
| 216 |
+
|
| 217 |
+
# Convert text to vectors (embeddings)
|
| 218 |
+
embeddings = embed_model.encode(text_data)
|
| 219 |
+
|
| 220 |
+
# Create the FAISS index
|
| 221 |
+
dimension = embeddings.shape[1]
|
| 222 |
+
index = faiss.IndexFlatL2(dimension)
|
| 223 |
+
index.add(np.array(embeddings).astype('float32'))
|
| 224 |
+
|
| 225 |
+
return index, text_data
|
| 226 |
+
|
| 227 |
+
def query_vault(query, index, text_data, top_k=2):
|
| 228 |
+
"""Finds the most relevant notes for a question"""
|
| 229 |
+
if index is None:
|
| 230 |
+
return "No notes found to search."
|
| 231 |
+
|
| 232 |
+
# Convert question to vector
|
| 233 |
+
query_vector = embed_model.encode([query])
|
| 234 |
+
|
| 235 |
+
# Search the index
|
| 236 |
+
distances, indices = index.search(np.array(query_vector).astype('float32'), top_k)
|
| 237 |
+
|
| 238 |
+
# Pull the relevant text chunks
|
| 239 |
+
results = [text_data[i] for i in indices[0] if i != -1]
|
| 240 |
+
return results
|
| 241 |
+
|
| 242 |
+
def log_feedback(query, answer, context, status):
|
| 243 |
+
log_file = "feedback_log.json"
|
| 244 |
+
entry = {
|
| 245 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 246 |
+
"query": query,
|
| 247 |
+
"answer": answer,
|
| 248 |
+
"context_used": context,
|
| 249 |
+
"status": status # "Correct" or "Wrong"
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
logs = []
|
| 253 |
+
if os.path.exists(log_file):
|
| 254 |
+
with open(log_file, "r") as f:
|
| 255 |
+
logs = json.load(f)
|
| 256 |
+
|
| 257 |
+
logs.append(entry)
|
| 258 |
+
with open(log_file, "w") as f:
|
| 259 |
+
json.dump(logs, f, indent=4)
|
| 260 |
+
|
| 261 |
+
# --- 8. GEMINI AI ENGINE ---
|
| 262 |
+
def get_gemini_response(user_query, context_str):
|
| 263 |
+
"""Connects to Google Gemini API for free AI logic"""
|
| 264 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 265 |
+
if not api_key:
|
| 266 |
+
return "⚠️ Error: GEMINI_API_KEY not found in Hugging Face Secrets."
|
| 267 |
+
|
| 268 |
+
try:
|
| 269 |
+
# New initialization style
|
| 270 |
+
client = genai.Client(api_key=api_key)
|
| 271 |
+
|
| 272 |
+
prompt = f"""
|
| 273 |
+
You are a secure vault assistant. Use the following retrieved notes to answer.
|
| 274 |
+
Notes Context: {context_str}
|
| 275 |
+
User Question: {user_query}
|
| 276 |
+
"""
|
| 277 |
+
|
| 278 |
+
# Inside your loop or before you call the model
|
| 279 |
+
time.sleep(2) # Wait 2 seconds between requests
|
| 280 |
+
# Simplified model call
|
| 281 |
+
response = client.models.generate_content(
|
| 282 |
+
model="gemini-2.5-flash-lite",
|
| 283 |
+
contents=prompt
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
track_usage(prompt, type="input")
|
| 287 |
+
track_usage(response.text, type="output")
|
| 288 |
+
return response.text
|
| 289 |
+
except Exception as e:
|
| 290 |
+
return f"❌ AI Engine Error: {str(e)}"
|