""" Aurelius — central configuration. All tunables live here so they can be adjusted (or env-overridden) without touching the search/embedding/wiki/server logic. See CLAUDE.md and the architect plan (Production Refactor + Algorithm + UI Pass, §2) for context. """ import os from pathlib import Path # ── Embedding model ────────────────────────────────────────────────────── # Default is all-MiniLM-L6-v2: 80 MB / 384-dim, fits free-tier deployment # (Render free web service) without OOMing or blowing the cold-start budget. # Qwen/Qwen3-Embedding-0.6B (plan §3) is a better-quality drop-in if you're # running on a host with more RAM — set AURELIUS_EMBED_MODEL to switch. EMBED_MODEL_NAME = os.getenv("AURELIUS_EMBED_MODEL", "all-MiniLM-L6-v2") EMBED_DEVICE = os.getenv("AURELIUS_EMBED_DEVICE", "cpu") EMBED_BATCH_SIZE = int(os.getenv("AURELIUS_EMBED_BATCH", "64")) # smaller for 0.6B vs 128 for MiniLM # ── Wikipedia ──────────────────────────────────────────────────────────── WIKI_API = "https://en.wikipedia.org/w/api.php" MAX_FETCH = 500 # max links fetched from Wikipedia (paginated) MAX_PAGES = 6 # safety cap on pagination pages when hunting for a specific goal link # Wikimedia's robot policy (https://w.wiki/4wJS) 403s requests whose # User-Agent has no contact info — this was dropped during the module split # and the bare "Aurelius-WikiNavigator/7.0" string started getting blocked. WIKI_HEADERS = { "User-Agent": "Aurelius-WikiNavigator/7.0 (educational project; contact: murtaza.vali.ug25@plaksha.edu.in)", "Accept": "application/json", } # ── Search ─────────────────────────────────────────────────────────────── MAX_HOPS = 100 # enforced as expansion-count cap, not path-length cap (see search.py) TOP_DISPLAY = 35 # max neighbours sent to frontend per step # ── Scoring ────────────────────────────────────────────────────────────── # Architect plan (Honest Bidirectional Meeting-Check Rewrite): the target's # backlinks (depth-1) are now the actual goal zone, not just a score hint — # see search.py's meeting check. Depth-2 frontier, its bonus, and the # category bonus were removed: none of them could contribute a real, # verifiable edge, and depth-2 specifically could not be stitched into a # real path without bridge bookkeeping it never had. The stagnation/ # frontier-jump escape valve was removed entirely — it fabricated a # came_from pointer with no corresponding Wikipedia link (see the Marcus # Aurelius -> A* search algorithm incident), which is no longer needed now # that the meeting check gives the search an honest way to converge. PRUNE_TOP_K = 15 # keep this many candidates per expansion after scoring # NOTE: tried raising this to 0.22 as part of the Semantic Drift fix (see # search.py docstring) on the theory that richer "{title}. {short_desc}" # embeddings would make raw cosine trustworthy enough to support a higher # floor. Verified live against Marcus Aurelius -> A* search algorithm: the # best of Marcus Aurelius's 500 direct links scores only ~0.186 raw cosine # to the (also-enriched) target embedding — there is no semantically close # 1-hop neighbour for a niche CS topic from a Roman-emperor article, full # stop, regardless of embedding quality. 0.22 killed the search at step 1 # (0/500 candidates survived). Kept at 0.15. The actual drift fix is the # embedding enrichment improving *relative* ranking among survivors, plus # DEPTH_TIEBREAK_EPSILON preventing runaway commitment to one irrelevant # cluster — not an absolute floor, which hard multi-hop cases can't clear # early on by construction. PRUNE_FLOOR = 0.15 # soft floor on raw cosine-to-target; frontier members exempt PRUNE_MIN_SURVIVORS = 5 # ALWAYS keep at least this many top-ranked candidates per step, # even if they fall below PRUNE_FLOOR — prevents the search from # killing itself at step 1 for semantically distant pairs # (Cleopatra → Time complexity: 0/500 candidates cleared 0.15, # search died immediately) FRONTIER_D1_BONUS = 0.30 # candidate is a direct backlink of the target (goal-zone member) # Depth tie-breaker (Semantic Drift fix): heap priority is h + DEPTH_EPSILON*g # instead of pure h. This is a deliberate, documented reversal of the prior # "heap priority = h only, no g-cost" design (see CLAUDE.md) — pure-greedy # let one noisy high-scoring title (e.g. a proper noun with no domain # context) drag the search arbitrarily deep into its cluster with nothing # to prefer a shallower, more recently-improving alternative. 0.01 is small # enough that it only breaks ties/near-ties; it doesn't override a genuinely # strong h advantage at any reasonable depth (60 hops -> max 0.6 penalty). DEPTH_TIEBREAK_EPSILON = 0.01 # ── Active Backward Expansion (goal-zone depth-2) ─────────────────────── GOAL_ZONE_EXPAND_INTERVAL = 10 # expand goal zone every N steps GOAL_ZONE_EXPAND_BATCH = 5 # d1 members to expand per interval GOAL_ZONE_D2_BACKLINK_LIMIT = 100 # max backlinks fetched per d1 member FRONTIER_D2_BONUS = 0.15 # score bonus for d2 goal-zone members (half of d1's 0.30) # ── Stagnation Detection / Cluster Escape ──────────────────────────────── STAGNATION_WINDOW = 8 # sliding window size for h-improvement tracking STAGNATION_DELTA = 0.02 # min improvement over window to count as progress PRUNE_TOP_K_STAGNANT = 25 # widened beam when stagnant (vs 15 normal) DIVERSITY_WEIGHT = 0.10 # weight for centroid-distance diversity bonus # ── Security / abuse limits ────────────────────────────────────────────── # This backend is public and unauthenticated. None of the limits below # change search behaviour for a normal user — they only bound what a single # abusive client can make the server do (open WS handshakes, CPU-bound # embedding, and Wikipedia API traffic on the shared free tier). # # ALLOWED_ORIGINS: browser Origins permitted to call the API (CORS for the # REST endpoints + an explicit Origin check on the WebSocket handshake). # Defaults cover the deployed Vercel frontend, localhost dev, and the # "null" origin used when index.html is opened directly via file:// (the # documented local workflow). Override in production with the env var, e.g. # ALLOWED_ORIGINS="https://aurelius-psi.vercel.app". Non-browser clients # (curl, scripts) send no Origin and are unaffected — Origin checks only # defend against other websites' JS driving this backend from a user's browser. ALLOWED_ORIGINS = [ o.strip() for o in os.getenv( "ALLOWED_ORIGINS", "https://aurelius-psi.vercel.app," "http://localhost:8000,http://127.0.0.1:8000," "http://localhost:8081,http://127.0.0.1:8081,null", ).split(",") if o.strip() ] MAX_QUERY_LEN = int(os.getenv("AURELIUS_MAX_QUERY_LEN", "200")) # reject absurd start/end strings MAX_CONCURRENT_SEARCHES = int(os.getenv("AURELIUS_MAX_CONCURRENT", "4")) # parallel searches the box will run MAX_SEARCH_SECONDS = int(os.getenv("AURELIUS_MAX_SEARCH_SECONDS", "300")) # hard wall-clock cap per connection # The two knobs above bound a single WS connection; the sliding-window rate # limit below bounds total request volume per client IP across connections # (a concurrency cap alone can't stop rapid connect/abandon loops). RATE_LIMIT_WINDOW_S = int(os.getenv("AURELIUS_RATE_WINDOW", "60")) RATE_LIMIT_MAX_REQUESTS = int(os.getenv("AURELIUS_RATE_MAX", "30")) # per IP per window # ── Files / store ──────────────────────────────────────────────────────── _THIS_DIR = Path(__file__).resolve().parent SEED_FILE = _THIS_DIR / "common_wiki_searches.txt" # Persistent graph + vector store (SQLite; the interface is pgvector-shaped # so a Postgres swap is config-level — see core/store.py). AURELIUS_DB = Path(os.getenv("AURELIUS_DB", str(_THIS_DIR / "data" / "aurelius.db"))) # ── Adapters ───────────────────────────────────────────────────────────── # Contact email: the Wikimedia robot-policy lesson applies to every # upstream API — identify yourself or get throttled. OpenAlex's "polite # pool" is explicitly faster with a mailto. CONTACT_EMAIL = os.getenv("AURELIUS_CONTACT", "murtaza.vali.ug25@plaksha.edu.in") OPENALEX_API = "https://api.openalex.org" NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "") # optional; GDELT fallback when absent # News Intelligence scheduled refresh: minutes between pipeline runs. # 0 (default) = off — refresh manually via /api/news/refresh or the CLI. NEWS_REFRESH_MINUTES = int(os.getenv("NEWS_REFRESH_MINUTES", "0")) HETIONET_URL = ("https://github.com/hetio/hetionet/raw/main/" "hetnet/json/hetionet-v1.0.json.bz2") # ── Optional LLM layer (Google Gemini via Google AI Studio) ────────────── # Purely additive: every AI feature is a lazy call made AFTER the fast # non-LLM result is already on screen, and the whole app runs with no key. # The key is a backend-only secret — never committed, never sent to the # browser (only /api/llm/status's booleans are). See core/llm.py. GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash") GEMINI_RPM = int(os.getenv("GEMINI_RPM", "12")) # soft calls/min budget — stay under the free-tier limit GEMINI_COOLDOWN_S = int(os.getenv("GEMINI_COOLDOWN_S", "60")) # pause after an upstream 429/5xx GEMINI_TIMEOUT_S = float(os.getenv("GEMINI_TIMEOUT_S", "12")) GEMINI_MAX_CONCURRENCY = int(os.getenv("GEMINI_MAX_CONCURRENCY", "3")) # ── Structural embeddings (node2vec — core/representation.py) ──────────── N2V_DIM = int(os.getenv("AURELIUS_N2V_DIM", "64")) N2V_WALKS_PER_NODE = 10 N2V_WALK_LENGTH = 20 N2V_WINDOW = 5 N2V_EPOCHS = 2 N2V_NEGATIVES = 5 # Fusion weight for [text ; α·struct] — how much structure counts relative # to text, per source. Text dominates for encyclopedic sources; structure # carries sources whose node text is thin (tickers, genes, file paths). FUSION_ALPHA = { "wikipedia": 0.3, "openalex": 0.5, "biomed": 0.8, "news": 0.5, "finance": 0.8, }