Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| import shutil | |
| import time | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| class AppConfig: | |
| openai_api_key: str | |
| chat_model: str | |
| embedding_model: str | |
| reranker_model: str | |
| data_dir: Path | |
| max_papers: int = 5 | |
| max_pdf_mb: int = 25 | |
| chunk_target_tokens: int = 900 | |
| chunk_overlap_tokens: int = 170 | |
| dense_results_per_query: int = 22 | |
| lexical_results: int = 35 | |
| candidate_limit: int = 70 | |
| final_evidence_count: int = 10 | |
| query_count: int = 5 | |
| citation_check_limit: int = 24 | |
| session_ttl_hours: int = 6 | |
| max_analyses_per_session: int = 15 | |
| def _streamlit_secret(name: str) -> Any | None: | |
| try: | |
| import streamlit as st | |
| return st.secrets.get(name) | |
| except Exception: | |
| return None | |
| def _get_setting(name: str, default: str = "") -> str: | |
| secret = _streamlit_secret(name) | |
| if secret not in (None, ""): | |
| return str(secret) | |
| return os.getenv(name, default) | |
| def load_config() -> AppConfig: | |
| data_dir = Path(_get_setting("PAPERMIND_DATA_DIR", "data")).resolve() | |
| data_dir.mkdir(parents=True, exist_ok=True) | |
| return AppConfig( | |
| openai_api_key=_get_setting("OPENAI_API_KEY"), | |
| chat_model=_get_setting("OPENAI_CHAT_MODEL", "gpt-5.4"), | |
| embedding_model=_get_setting( | |
| "OPENAI_EMBEDDING_MODEL", "text-embedding-3-large" | |
| ), | |
| reranker_model=_get_setting( | |
| "RERANKER_MODEL", "BAAI/bge-reranker-v2-m3" | |
| ), | |
| data_dir=data_dir, | |
| max_papers=int(_get_setting("MAX_PAPERS", "5")), | |
| max_pdf_mb=int(_get_setting("MAX_PDF_MB", "25")), | |
| chunk_target_tokens=int(_get_setting("CHUNK_TARGET_TOKENS", "900")), | |
| chunk_overlap_tokens=int(_get_setting("CHUNK_OVERLAP_TOKENS", "170")), | |
| dense_results_per_query=int( | |
| _get_setting("DENSE_RESULTS_PER_QUERY", "22") | |
| ), | |
| lexical_results=int(_get_setting("LEXICAL_RESULTS", "35")), | |
| candidate_limit=int(_get_setting("CANDIDATE_LIMIT", "70")), | |
| final_evidence_count=int(_get_setting("FINAL_EVIDENCE_COUNT", "10")), | |
| query_count=int(_get_setting("QUERY_COUNT", "5")), | |
| citation_check_limit=int(_get_setting("CITATION_CHECK_LIMIT", "24")), | |
| session_ttl_hours=int(_get_setting("SESSION_TTL_HOURS", "6")), | |
| max_analyses_per_session=int( | |
| _get_setting("MAX_ANALYSES_PER_SESSION", "15") | |
| ), | |
| ) | |
| def session_paths(config: AppConfig, session_id: str) -> dict[str, Path]: | |
| root = config.data_dir / "runtime" / session_id | |
| paths = { | |
| "root": root, | |
| "uploads": root / "uploads", | |
| "chroma": root / "chroma", | |
| "exports": root / "exports", | |
| } | |
| for path in paths.values(): | |
| path.mkdir(parents=True, exist_ok=True) | |
| (root / ".last_access").touch() | |
| return paths | |
| def clear_session(config: AppConfig, session_id: str) -> None: | |
| root = config.data_dir / "runtime" / session_id | |
| if root.exists(): | |
| shutil.rmtree(root, ignore_errors=True) | |
| def cleanup_expired_sessions(config: AppConfig, active_session_id: str = "") -> None: | |
| runtime = config.data_dir / "runtime" | |
| runtime.mkdir(parents=True, exist_ok=True) | |
| cutoff = time.time() - (config.session_ttl_hours * 3600) | |
| for child in runtime.iterdir(): | |
| if not child.is_dir() or child.name == active_session_id: | |
| continue | |
| marker = child / ".last_access" | |
| timestamp = marker.stat().st_mtime if marker.exists() else child.stat().st_mtime | |
| if timestamp < cutoff: | |
| shutil.rmtree(child, ignore_errors=True) | |