from flask import Flask, request, jsonify from flask_cors import CORS import subprocess import threading import queue import time import sys app = Flask(__name__) CORS(app, resources={r"/*": {"origins": "*"}}) # Thread-safe queue to handle database updates sequentially mine_queue = queue.Queue() def background_miner(): """Processes mining requests one at a time to prevent SQLite database locks.""" while True: # Wait until a request comes in _ = mine_queue.get() # Coalesce burst updates: wait a brief moment if more data is arriving time.sleep(1.5) while not mine_queue.empty(): mine_queue.get_nowait() mine_queue.task_done() try: print("[Worker] Initiating background mining on live_chat_memories.txt...") # Mine the text file safely subprocess.run( ['mempalace', 'mine', 'live_chat_memories.txt'], capture_output=True, text=True, check=True ) # Sync the palace map subprocess.run(['mempalace', 'sync'], capture_output=True) print("[Worker] Mining and sync complete.") except Exception as e: print(f"[Worker Error] Mining background task failed: {e}") finally: mine_queue.task_done() # Start the background worker thread worker = threading.Thread(target=background_miner, daemon=True) worker.start() @app.after_request def add_headers(response): response.headers['ngrok-skip-browser-warning'] = 'true' return response @app.route('/health', methods=['GET', 'OPTIONS']) def health_check(): return jsonify({"status": "healthy"}), 200 @app.route('/search/health', methods=['GET', 'OPTIONS']) def search_health_check(): return jsonify({"status": "healthy"}), 200 @app.route('/search', methods=['POST', 'OPTIONS']) def search_memory(): if request.method == 'OPTIONS': return jsonify({"status": "ok"}), 200 data = request.json or {} print(f"\n[Incoming Search Payload]: {data}") sys.stdout.flush() # Capture either 'query' or 'text' depending on how the frontend bundles it query = data.get('query') or data.get('text') or "" if not query: print("[Search Error] Extracted query string was completely empty.") return jsonify({"error": "No query or text provided"}), 400 try: print(f"[Executing Search CLI] Querying MemPalace for: '{query}'") result = subprocess.run( ['mempalace', 'search', query], capture_output=True, text=True, check=True ) raw_output = result.stdout # --- PARSING LOGIC FOR PERCHANCE --- # Split the CLI output by the divider line to separate the matches raw_segments = raw_output.split("────────────────────────────────────────────────────────") parsed_memories = [] for segment in raw_segments: lines = segment.split('\n') text_lines = [] # Extract only lines that are actually part of the story/memory snippet for line in lines: cleaned = line.strip() # Skip CLI decorative lines, headers, and metadata tags if not cleaned: continue if cleaned.startswith('====') or cleaned.startswith('Results for:') or cleaned.startswith('['): continue if cleaned.startswith('Source:') or cleaned.startswith('Match:'): continue text_lines.append(line) # Keep original layout structure if text_lines: # Merge the lines back into a cohesive snippet block snippet = "\n".join(text_lines).strip() if snippet: parsed_memories.append({"text": snippet}) print(f"[Parsed for UI]: Extracted {len(parsed_memories)} clean text blocks for Perchance UI.") sys.stdout.flush() # Return multiple key variants to ensure the frontend script catches the data array return jsonify({ "results": raw_output, # Raw text format "memories": parsed_memories, # Clean standard array format "matches": [m["text"] for m in parsed_memories] # Flat array fallback }) except subprocess.CalledProcessError as e: print(f"[Search CLI Exception]: {e.stderr}") return jsonify({"error": str(e.stderr)}), 500 @app.route('/memories/add', methods=['POST', 'OPTIONS']) def add_memory(): if request.method == 'OPTIONS': return jsonify({"status": "ok"}), 200 data = request.json or {} content = data.get('text') or data.get('content') or "" # Grab the thread details sent by the frontend thread_name = data.get('threadName') or "Unknown Thread" thread_id = data.get('threadId') or "Unknown_ID" if not content: return jsonify({"error": "No content provided"}), 400 try: # Prepend the thread/character name so MemPalace indexes who this memory belongs to contextualized_content = f"[{thread_name} (Thread {thread_id})]: {content.strip()}" # File writing is fast and won't lock up or 500 with open("live_chat_memories.txt", "a", encoding="utf-8") as f: f.write(f"\n{contextualized_content}\n") # Drop a ticket into the queue for the worker thread to index it smoothly mine_queue.put(True) return jsonify({"status": "success", "message": "Memory queued for processing"}), 200 except Exception as e: print(f"[Add Memory Error]: {e}") return jsonify({"error": str(e)}), 500 if __name__ == '__main__': print("Starting MemPalace Integration Server on port 5000...") app.run(host='0.0.0.0', port=5000)