File size: 8,403 Bytes
470bcea 37cb069 470bcea 37cb069 470bcea 37cb069 470bcea 37cb069 470bcea 98ce58d 470bcea 37cb069 4f41d03 37cb069 4f41d03 37cb069 4f41d03 37cb069 4f41d03 37cb069 470bcea 37cb069 470bcea 37cb069 470bcea 37cb069 470bcea 37cb069 470bcea 37cb069 470bcea | 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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """ICSAC Editorial System β Configuration.
Copy this file to config.py. Secrets are loaded from environment variables.
Set them in /etc/icsac/editorial.env (loaded by systemd EnvironmentFile=).
For manual runs: source /etc/icsac/editorial.env && export ZENODO_TOKEN TELEGRAM_TOKEN TELEGRAM_CHAT_ID
"""
import os
import os as _os
def _load_env_file(path: str = "/etc/icsac/editorial.env") -> None:
"""Self-load env file if vars not already set. Lets Python invocations
work without ceremony β systemd EnvironmentFile= still wins when present.
"""
p = _os.path.expanduser(path)
if not _os.path.isfile(p):
return
with open(p) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
k = k.strip()
v = v.strip().strip('"').strip("'")
_os.environ.setdefault(k, v)
_load_env_file()
ZENODO_TOKEN = os.environ.get("ZENODO_TOKEN", "")
ZENODO_API = "https://zenodo.org/api"
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
# Optional: Telegram supergroup-thread routing. When set, editorial-system
# messages pin to a specific topic so notifications can share a bot/supergroup
# with other operator monitoring without crossing streams.
TELEGRAM_THREAD_ID = os.environ.get("TELEGRAM_THREAD_ID", "")
# Optional: Tier-3 test routing. The submission worker (when test_mode and
# tier=3) and apply_decision (when applying a verdict to a test sub_id)
# route curator-facing Telegram to this chat instead of TELEGRAM_CHAT_ID.
# Leave unset to skip the curator Telegram in T3 entirely (panel/RQC/email
# draft still run); the worker logs a single warning when this happens.
TELEGRAM_TEST_CHAT_ID = os.environ.get("TELEGRAM_TEST_CHAT_ID", "")
TELEGRAM_TEST_THREAD_ID = os.environ.get("TELEGRAM_TEST_THREAD_ID", "")
# Optional: IMAP draft-save mode. When email_send is invoked with draft=True,
# the rendered MIME message is APPENDed to Gmail's Drafts folder via IMAP
# (operator manually reviews + sends from Gmail UI). Leave unset to disable
# draft mode entirely.
IMAP_HOST = os.environ.get("IMAP_HOST", "imap.gmail.com")
IMAP_PORT = int(os.environ.get("IMAP_PORT", "993"))
IMAP_USER = os.environ.get("IMAP_USER", "")
IMAP_PASSWORD = os.environ.get("IMAP_PASSWORD", "")
IMAP_DRAFTS_FOLDER = os.environ.get("IMAP_DRAFTS_FOLDER", "[Gmail]/Drafts")
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
HF_TOKEN = os.environ.get("HF_TOKEN", "")
# Panel slot chains. Entries are tagged with backend prefix:
# "hf|<model>:<provider>" β HuggingFace Inference Providers Router
# (custom provider keys live in HF settings;
# billing routes through the upstream provider)
# "or|<model>" β OpenRouter direct
# Untagged entries fall through to OR for backward compatibility.
# Consecutive OR entries are batched into a single OR call (`models` array,
# max 3 per OR's cap). HF entries fire one HTTP request each because HF's
# explicit provider pin does not auto-failover within the call β the panel
# chain dispatcher is responsible for trying the next entry on failure.
#
# Cross-provider redundancy (2026-04-27): every slot's chain spans Groq +
# Cerebras + OR-free so a single-provider outage can't take more than one
# chain entry per slot. Cerebras free-tier 8K context cap forces
# Qwen3-235B-A22B-Instruct-2507 (the 64K-context exempt model) anywhere
# Cerebras appears in a slot.
OPENROUTER_MODELS = [
# Slot 1: Groq Llama-3.3-70B β Cerebras Qwen3-235B β OR cross-family.
[
"hf|meta-llama/Llama-3.3-70B-Instruct:groq",
"hf|Qwen/Qwen3-235B-A22B-Instruct-2507:cerebras",
"or|openai/gpt-oss-120b:free",
"or|z-ai/glm-4.5-air:free",
"gemini",
],
# Slot 2: Groq gpt-oss-120b β Cerebras Qwen3-235B β OR Nvidia/Hermes.
# nemotron-3-super-120b-a12b excluded (won't emit JSON reliably).
[
"hf|openai/gpt-oss-120b:groq",
"hf|Qwen/Qwen3-235B-A22B-Instruct-2507:cerebras",
"or|nvidia/nemotron-nano-12b-v2-vl:free",
"or|nousresearch/hermes-3-llama-3.1-405b:free",
"gemini",
],
# Slot 3: Cerebras primary β Groq Llama β OR Google/cross-family.
[
"hf|Qwen/Qwen3-235B-A22B-Instruct-2507:cerebras",
"hf|meta-llama/Llama-3.3-70B-Instruct:groq",
"or|google/gemma-4-26b-a4b-it:free",
"or|z-ai/glm-4.5-air:free",
"gemini",
],
# Slot 4: HF Groq primary, HF Cerebras fallback, OR tail. Reordered
# 2026-04-27 after qwen3-next-80b-a3b-instruct:free failed all 4
# consecutive panel passes (SUB-00003 pass 0+1, SUB-00004 pass 0+1).
# Kept minimax + gemma-4-31b as the OR tail so slot 4 still has a
# full OR-only fallback path with model-family diversity from slots
# 1-3 OR tails (gpt-oss/z-ai, nemotron/hermes, gemma-4-26b/z-ai).
# NB: this puts slot 4 on the same primary (HF Groq llama-3.3) as
# slot 1 β accepted trade-off; total Groq-outage now drops the panel
# to 4/5 via Cerebras fallback rather than staying functional, but a
# CHRONIC slot-4 failure (which is what we had) was permanently below
# MIN_REVIEWERS=4 in pass 1. Reliability beats slot-level diversity.
[
"hf|meta-llama/Llama-3.3-70B-Instruct:groq",
"hf|Qwen/Qwen3-235B-A22B-Instruct-2507:cerebras",
"or|minimax/minimax-m2.5:free",
"or|google/gemma-4-31b-it:free",
"gemini",
],
]
OPENROUTER_MODELS_API_URL = "https://openrouter.ai/api/v1/models"
# Self-heal thresholds (claude + 4 OR slots = 5 total panelists per pass).
# MIN_REVIEWERS=4 tolerates 1 slot failure per pass after self-heal retry.
# Combined with REVIEW_PASSES below, a paper yields 8-10 valid reviews in
# the aggregate. Tightened from MIN_REVIEWERS=3 + 3 passes 2026-04-26
# after observing pass-to-pass stdev was uniformly tiny (β€0.41 on the
# noisiest dim, β€0.09 on most) β 3rd pass added marginal stderr at 33%
# more compute. Two passes captures essentially the same signal; pairing
# with MIN_REVIEWERS=4 keeps each pass closer to full panel.
MIN_REVIEWERS = 4
MAX_SLOT_RETRIES = 1 # per failed slot, after the initial attempt
RETRY_COOLDOWN_SEC = 30 # wait between initial pass and retry pass
# Multi-pass aggregation: run the full panel N times, aggregate mean+stdev
# across passes. 2 passes balances stability against compute. Set to 1
# to disable multi-pass; 3+ for noise-reduction at compute cost.
REVIEW_PASSES = 2
SMTP_HOST = os.environ.get("SMTP_HOST", "smtp.gmail.com")
SMTP_PORT = int(os.environ.get("SMTP_PORT", "465"))
SMTP_USER = os.environ.get("SMTP_USER", "")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
FROM_EMAIL = os.environ.get("FROM_EMAIL", "info@icsacinstitute.org")
REPLY_TO_EMAIL = os.environ.get("REPLY_TO_EMAIL", "info@icsacinstitute.org")
NTFY_PAIN_URL = os.environ.get("NTFY_PAIN_URL", "")
NTFY_BACKUPS_URL = os.environ.get("NTFY_BACKUPS_URL", "")
BRAIN_URL = os.environ.get("BRAIN_URL", "")
KUMA_PUSH_URL = os.environ.get("KUMA_PUSH_URL", "")
COMMUNITY_ID = "icsac"
GOOGLE_FORM_URL = os.environ.get("GOOGLE_FORM_URL", "https://example.com/your-community-signup-form")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# Runtime output directory β created on first run, gitignored.
REVIEWS_DIR = os.path.join(BASE_DIR, "reviews")
# Runtime output directory β created on first run, gitignored.
DOWNLOADS_DIR = os.path.join(BASE_DIR, "downloads")
RUBRICS_DIR = os.path.join(BASE_DIR, "rubrics")
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
# Site base URL used to build share-target landing pages (icsacinstitute.org/accepted/<id>)
SITE_BASE_URL = "https://icsacinstitute.org"
CLAUDE_CMD = "claude"
GEMINI_CMD = "gemini"
RUBRIC_DIMENSIONS = [
"domain_fit",
"methodological_transparency",
"internal_consistency",
"citation_integrity",
"novelty_signal",
"ai_provenance_signal",
]
# Path to the institute's website repo (used by publications.py to commit
# accepted-paper landing pages + redacted reviews). Empty disables the
# website-registry push; the Zenodo accept itself still proceeds.
ICSAC_WEBSITE_REPO = os.environ.get("ICSAC_WEBSITE_REPO", "")
|