Spaces:
Sleeping
Sleeping
| import sqlite3 | |
| import threading | |
| import time | |
| import os | |
| from huggingface_hub import hf_hub_download, HfApi | |
| import config | |
| class DatabaseManager: | |
| def __init__(self): | |
| self.api = HfApi(token=config.HF_TOKEN) | |
| self.lock = threading.Lock() | |
| self._last_sync = 0 | |
| self._needs_sync = False | |
| self._sync_thread = None | |
| self._stop_event = threading.Event() | |
| def initialize(self): | |
| """啟動時下載資料庫或建立新的""" | |
| with self.lock: | |
| try: | |
| # 確保目錄存在 | |
| config.LOCAL_DB_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| print(f"正在從 {config.DB_REPO_ID} 下載資料庫...") | |
| hf_hub_download( | |
| repo_id=config.DB_REPO_ID, | |
| filename=config.DB_FILENAME, | |
| repo_type="dataset", | |
| token=config.HF_TOKEN, | |
| local_dir=str(config.RUNTIME_ROOT), | |
| local_dir_use_symlinks=False | |
| ) | |
| print("資料庫下載成功。") | |
| except Exception as e: | |
| print(f"無法下載資料庫 (可能是首次建立): {e}") | |
| self._create_empty_db() | |
| self._ensure_tables() | |
| # 啟動背景同步執行緒 | |
| if not self._sync_thread or not self._sync_thread.is_alive(): | |
| self._stop_event.clear() | |
| self._sync_thread = threading.Thread(target=self._sync_worker, daemon=True) | |
| self._sync_thread.start() | |
| def _sync_worker(self): | |
| """背景同步工作執行緒,負責彙整變更並重試""" | |
| print("資料庫同步執行緒已啟動。") | |
| while not self._stop_event.is_set(): | |
| if self._needs_sync: | |
| # 等待一小段時間(例如 5 秒),彙整多個連續變更 | |
| time.sleep(5) | |
| self.sync_to_hf() | |
| else: | |
| time.sleep(1) | |
| def _create_empty_db(self): | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| conn.close() | |
| def _ensure_tables(self): | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS knowledge ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| key TEXT UNIQUE, | |
| content TEXT, | |
| updated_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| """) | |
| # 新增 personas 表,支援儲存多種人設 | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS personas ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| name TEXT UNIQUE, | |
| content TEXT, | |
| is_active INTEGER DEFAULT 0, | |
| updated_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| """) | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS chat_history ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| role TEXT, | |
| content TEXT, | |
| timestamp DATETIME DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| def sync_to_hf(self, max_retries=3): | |
| if not config.HF_TOKEN: | |
| self._needs_sync = False | |
| return | |
| with self.lock: | |
| for attempt in range(max_retries): | |
| try: | |
| print(f"正在同步資料庫到 {config.DB_REPO_ID} (嘗試 {attempt + 1}/{max_retries})...") | |
| self.api.upload_file( | |
| path_or_fileobj=str(config.LOCAL_DB_PATH), | |
| path_in_repo=config.DB_FILENAME, | |
| repo_id=config.DB_REPO_ID, | |
| repo_type="dataset", | |
| commit_message=f"Auto-sync database at {time.strftime('%H:%M:%S')}" | |
| ) | |
| self._last_sync = time.time() | |
| self._needs_sync = False | |
| print("同步成功。") | |
| return | |
| except Exception as e: | |
| print(f"同步嘗試 {attempt + 1} 失敗: {e}") | |
| if "503" in str(e) or "Service Temporarily Unavailable" in str(e): | |
| # 針對 HF 503 錯誤,等待較長時間後重試 | |
| wait_time = (attempt + 1) * 10 | |
| print(f"HF 服務忙碌中,等待 {wait_time} 秒後重試...") | |
| time.sleep(wait_time) | |
| else: | |
| # 其他錯誤,稍微等待後重試 | |
| time.sleep(2) | |
| print("所有同步嘗試皆失敗,將於下一次週期再次嘗試。") | |
| def _request_sync(self): | |
| """標記需要同步,由背景執行緒處理""" | |
| self._needs_sync = True | |
| def query_knowledge(self, user_input): | |
| if not user_input or len(user_input.strip()) < 2: | |
| return [] | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| # 1. 先嘗試原有的關鍵字比對 (User Input 包含 Key) | |
| cursor.execute(""" | |
| SELECT content FROM knowledge | |
| WHERE ? LIKE '%' || key || '%' | |
| AND length(key) > 1 | |
| ORDER BY length(key) DESC | |
| LIMIT 3 | |
| """, (user_input,)) | |
| results = cursor.fetchall() | |
| # 2. 如果結果不夠,嘗試「反向比對」(Key 包含 User Input 中的關鍵詞,或 Content 包含 User Input 中的關鍵詞) | |
| if len(results) < 5: | |
| # 簡單提取關鍵詞 (移除常見廢話) | |
| clean_input = user_input | |
| for stop in ["什麼是", "請問", "你知道", "嗎", "關於", "我想問", "的"]: | |
| clean_input = clean_input.replace(stop, "") | |
| clean_input = clean_input.strip("??!! ") | |
| if len(clean_input) >= 2: | |
| # 搜尋內容或 Key 中包含這個關鍵詞的內容 | |
| cursor.execute(""" | |
| SELECT content FROM knowledge | |
| WHERE (content LIKE '%' || ? || '%' OR key LIKE '%' || ? || '%') | |
| AND id NOT IN (SELECT id FROM ( | |
| SELECT id FROM knowledge WHERE ? LIKE '%' || key || '%' | |
| )) | |
| LIMIT ? | |
| """, (clean_input, clean_input, user_input, 5 - len(results))) | |
| results.extend(cursor.fetchall()) | |
| conn.close() | |
| # 確保回傳值是字串列表,且去除重複 | |
| final_results = [] | |
| seen = set() | |
| for r in results: | |
| content = r[0] | |
| if content not in seen: | |
| final_results.append(content) | |
| seen.add(content) | |
| return final_results[:5] | |
| def add_knowledge(self, key, content): | |
| with self.lock: | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| cursor.execute("INSERT OR REPLACE INTO knowledge (key, content) VALUES (?, ?)", (key, content)) | |
| conn.commit() | |
| conn.close() | |
| self._request_sync() | |
| def add_persona(self, name, content, set_active=False): | |
| with self.lock: | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| if set_active: | |
| cursor.execute("UPDATE personas SET is_active = 0") | |
| cursor.execute(""" | |
| INSERT OR REPLACE INTO personas (name, content, is_active) | |
| VALUES (?, ?, ?) | |
| """, (name, content, 1 if set_active else 0)) | |
| conn.commit() | |
| conn.close() | |
| self._request_sync() | |
| def get_all_personas(self): | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| cursor.execute("SELECT name, is_active FROM personas") | |
| rows = cursor.fetchall() | |
| conn.close() | |
| return [{"name": r[0], "active": bool(r[1])} for r in rows] | |
| def switch_persona(self, name): | |
| with self.lock: | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| cursor.execute("UPDATE personas SET is_active = 0") | |
| cursor.execute("UPDATE personas SET is_active = 1 WHERE name = ?", (name,)) | |
| conn.commit() | |
| conn.close() | |
| self._request_sync() | |
| def get_active_persona(self): | |
| try: | |
| conn = sqlite3.connect(config.LOCAL_DB_PATH) | |
| cursor = conn.cursor() | |
| cursor.execute("SELECT content FROM personas WHERE is_active = 1") | |
| row = cursor.fetchone() | |
| if not row: | |
| # 備用:檢查舊有的 knowledge 表 | |
| cursor.execute("SELECT content FROM knowledge WHERE key = 'persona'") | |
| row = cursor.fetchone() | |
| conn.close() | |
| return row[0] if row else None | |
| except Exception: | |
| return None | |
| db_mgr = DatabaseManager() | |