Aurelius / config.py
murtaza-2007
Aurelius improvement pass: domain-aware recs, finance/research surfaces, 2D graph
658d200
Raw
History Blame Contribute Delete
11.4 kB
"""
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,
}