Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| import logging | |
| import os | |
| import uuid | |
| from concurrent.futures import ThreadPoolExecutor | |
| from datetime import datetime | |
| import structlog | |
| from flask import Flask, Response, jsonify, render_template, request, stream_with_context | |
| from flask_limiter import Limiter | |
| from flask_limiter.util import get_remote_address | |
| from rag import answer, build_index, index_exists, load_index | |
| from rag.memory import create_session, get_recent_history, save_message | |
| logging.basicConfig(level=logging.INFO, format="%(message)s") | |
| structlog.configure( | |
| processors=[ | |
| structlog.stdlib.filter_by_level, | |
| structlog.stdlib.add_logger_name, | |
| structlog.stdlib.add_log_level, | |
| structlog.stdlib.PositionalArgumentsFormatter(), | |
| structlog.processors.TimeStamper(fmt="iso"), | |
| structlog.processors.JSONRenderer(), | |
| ], | |
| context_class=dict, | |
| logger_factory=structlog.stdlib.LoggerFactory(), | |
| cache_logger_on_first_use=True, | |
| ) | |
| logger = structlog.get_logger(__name__) | |
| os.environ.setdefault("HF_HUB_OFFLINE", "1") | |
| app = Flask(__name__) | |
| app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024 | |
| limiter = Limiter( | |
| get_remote_address, | |
| app=app, | |
| default_limits=["20 per minute"], | |
| storage_uri="memory://", | |
| ) | |
| _index_lock = ThreadPoolExecutor(max_workers=1) | |
| _index_future = None | |
| _vector_store = None | |
| if index_exists(): | |
| try: | |
| _vector_store = load_index() | |
| logger.info("vector_store_loaded") | |
| except Exception as e: | |
| logger.warning("vector_store_load_failed", error=str(e)) | |
| def _request_id() -> str: | |
| return request.headers.get("X-Request-Id") or str(uuid.uuid4()) | |
| def _before_request(): | |
| request.rid = _request_id() | |
| structlog.contextvars.clear_contextvars() | |
| structlog.contextvars.bind_contextvars(request_id=request.rid) | |
| def _after_request(response): | |
| response.headers["X-Request-Id"] = request.rid | |
| return response | |
| def index(): | |
| return render_template("index.html") | |
| def health(): | |
| return jsonify({ | |
| "status": "ok", | |
| "vector_store_ready": index_exists(), | |
| "vector_store_loaded": _vector_store is not None, | |
| "timestamp": datetime.now().isoformat(), | |
| }) | |
| def get_sessions(): | |
| conn = _db() | |
| rows = conn.execute( | |
| "SELECT id, title, updated_at FROM sessions ORDER BY updated_at DESC" | |
| ).fetchall() | |
| conn.close() | |
| return jsonify([{"id": r[0], "title": r[1], "updated_at": r[2]} for r in rows]) | |
| def create_session_api(): | |
| title = request.json.get("title", "New chat") if request.json else "New chat" | |
| sid = create_session(title) | |
| return jsonify({"id": sid, "title": title}) | |
| def delete_session_api(sid): | |
| conn = _db() | |
| conn.execute("DELETE FROM messages WHERE session_id=?", (sid,)) | |
| conn.execute("DELETE FROM sessions WHERE id=?", (sid,)) | |
| conn.commit() | |
| conn.close() | |
| return jsonify({"ok": True}) | |
| def get_messages_api(sid): | |
| msgs = get_recent_history(sid, max_turns=100) | |
| return jsonify(msgs) | |
| def chat(): | |
| global _vector_store | |
| data = request.json or {} | |
| sid = data.get("session_id") | |
| query = data.get("message", "").strip() | |
| if not sid or not query: | |
| return jsonify({"error": "session_id and message required"}), 400 | |
| save_message(sid, "user", query) | |
| msgs = get_recent_history(sid, max_turns=1) | |
| if len(msgs) <= 2: | |
| conn = _db() | |
| conn.execute("UPDATE sessions SET title=? WHERE id=?", (query[:50] + ("..." if len(query) > 50 else ""), sid)) | |
| conn.commit() | |
| conn.close() | |
| def generate(): | |
| try: | |
| if _vector_store is None: | |
| text = "No documents loaded. Upload PDFs and build the vector store first." | |
| confidence = 0.0 | |
| else: | |
| result = answer(query, _vector_store, session_id=sid) | |
| text = result.get("answer", "No answer generated.") | |
| confidence = result.get("confidence", 0.0) | |
| except Exception as e: | |
| logger.error("answer_failed", error=str(e)) | |
| text = f"Error: {str(e)[:200]}" | |
| confidence = 0.0 | |
| words = text.split(" ") | |
| for i, word in enumerate(words): | |
| chunk = word + (" " if i < len(words) - 1 else "") | |
| yield f"data: {json.dumps({'token': chunk})}\n\n" | |
| yield f"data: {json.dumps({'done': True, 'full': text, 'confidence': confidence})}\n\n" | |
| return Response( | |
| stream_with_context(generate()), | |
| mimetype="text/event-stream", | |
| headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, | |
| ) | |
| def upload(): | |
| files = request.files.getlist("files") | |
| if not files: | |
| return jsonify({"error": "No files"}), 400 | |
| os.makedirs("Artifacts", exist_ok=True) | |
| saved = [] | |
| for f in files: | |
| if f.filename and f.filename.endswith(".pdf"): | |
| path = os.path.join("Artifacts", f.filename) | |
| f.save(path) | |
| saved.append(f.filename) | |
| return jsonify({"saved": saved}) | |
| def vs_status(): | |
| global _index_future | |
| return jsonify({ | |
| "ready": index_exists(), | |
| "building": _index_future is not None and not _index_future.done(), | |
| }) | |
| def build_vs(): | |
| global _vector_store, _index_future | |
| if _index_future and not _index_future.done(): | |
| return jsonify({"error": "Build already in progress"}), 409 | |
| def _rebuild(): | |
| global _vector_store | |
| logger.info("rebuild_started") | |
| _vector_store = build_index() | |
| logger.info("rebuild_complete", chunks=len(_vector_store.chunks)) | |
| return _vector_store | |
| _index_future = _index_lock.submit(_rebuild) | |
| return jsonify({"ok": True, "message": "Build started in background"}) | |
| def build_status(): | |
| global _index_future | |
| if _index_future is None: | |
| return jsonify({"status": "idle"}) | |
| if _index_future.done(): | |
| exc = _index_future.exception() | |
| if exc: | |
| return jsonify({"status": "failed", "error": str(exc)}) | |
| return jsonify({"status": "completed"}) | |
| return jsonify({"status": "building"}) | |
| def _db(): | |
| import sqlite3 | |
| conn = sqlite3.connect("chat_history.db") | |
| conn.row_factory = sqlite3.Row | |
| conn.execute("PRAGMA journal_mode=WAL") | |
| conn.execute("""CREATE TABLE IF NOT EXISTS sessions ( | |
| id TEXT PRIMARY KEY, title TEXT NOT NULL, | |
| created_at TEXT NOT NULL, updated_at TEXT NOT NULL)""") | |
| conn.execute("""CREATE TABLE IF NOT EXISTS messages ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| session_id TEXT NOT NULL, | |
| role TEXT NOT NULL, | |
| content TEXT NOT NULL, | |
| created_at TEXT NOT NULL, | |
| FOREIGN KEY (session_id) REFERENCES sessions(id))""") | |
| conn.commit() | |
| return conn | |
| if __name__ == "__main__": | |
| import waitress | |
| port = int(os.environ.get("PORT", 7860)) | |
| logger.info("starting_server", port=port) | |
| waitress.serve(app, host="0.0.0.0", port=port, threads=8) | |