File size: 11,368 Bytes
2bd66df 75ce203 bc32a44 2bd66df d6382b1 75ce203 d6382b1 75ce203 d6382b1 75ce203 2bd66df 75ce203 19c6bad bc32a44 19c6bad 75ce203 658d200 75ce203 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """
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,
}
|