Spaces:
Running
Running
| """Load config.yaml and initialize shared settings. | |
| Loads, in order: | |
| 1. config.yaml (model, paths, logging, auto_restart) | |
| 2. .env (API keys for local dev; HF Spaces use Secrets instead) | |
| 3. Three prompt markdown files (manager / main / perfect_answer) | |
| 4. OpenAI client + HF API client | |
| All callers downstream import their dependencies from this module, so | |
| changing config / prompts is a single point of editing. | |
| """ | |
| import os | |
| import platform | |
| import threading | |
| import yaml | |
| from openai import OpenAI | |
| from dotenv import load_dotenv | |
| from huggingface_hub import HfApi | |
| from pathlib import Path | |
| BASE_DIR = Path(__file__).resolve().parent.parent # project root (one level up from core/) | |
| with open(BASE_DIR / "config.yaml") as f: | |
| CONFIG = yaml.safe_load(f) | |
| MODEL = CONFIG["model"] | |
| PATHS = CONFIG["paths"] | |
| REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium") | |
| VERBOSITY = CONFIG.get("verbosity", "medium") | |
| # Load API key: .env for local dev, HF Secrets for Spaces. | |
| env_path = BASE_DIR / PATHS["env_file"] | |
| if env_path.exists(): | |
| load_dotenv(env_path) | |
| # OpenAI client pool for round-robin per-session key assignment. | |
| # Loads OPENAI_KEY_01 .. OPENAI_KEY_10 from environment. Falls back to API_KEY | |
| # (single client, no rotation) if no pool keys are present, so local dev still | |
| # works with just one key in .env. | |
| _pool_pairs = [] | |
| for _i in range(1, 11): | |
| _k = os.getenv(f"OPENAI_KEY_{_i:02d}") | |
| if _k: | |
| _pool_pairs.append((_i, _k)) | |
| KEY_POOL = [] # list of (key_idx, OpenAI client) tuples | |
| if _pool_pairs: | |
| KEY_POOL = [(idx, OpenAI(api_key=k, max_retries=5)) for idx, k in _pool_pairs] | |
| print(f"[key_pool] loaded {len(KEY_POOL)} OPENAI_KEY_NN secrets for per-session rotation", flush=True) | |
| else: | |
| _fallback = os.getenv("API_KEY") | |
| if _fallback: | |
| KEY_POOL = [(0, OpenAI(api_key=_fallback, max_retries=5))] | |
| print("[key_pool] no OPENAI_KEY_NN secrets found; using single API_KEY (no rotation)", flush=True) | |
| else: | |
| print("[key_pool] WARNING: no OpenAI key configured (no API_KEY or OPENAI_KEY_NN)", flush=True) | |
| _counter = 0 | |
| _counter_lock = threading.Lock() | |
| def get_next_client(): | |
| """Round-robin OpenAI client picker. Call once per session at session start; | |
| pass the returned client through to all downstream calls in that session. | |
| Returns (key_idx, client). key_idx is 1-10 for pool keys, 0 for fallback API_KEY. | |
| Raises RuntimeError if no client is configured. | |
| """ | |
| global _counter | |
| if not KEY_POOL: | |
| raise RuntimeError("no OpenAI client configured (set OPENAI_KEY_01..10 or API_KEY)") | |
| with _counter_lock: | |
| idx_in_pool = _counter % len(KEY_POOL) | |
| _counter += 1 | |
| return KEY_POOL[idx_in_pool] | |
| # Backward-compat: module-level `client` for code paths that have not yet been | |
| # threaded with per-session clients. Equals the first pool entry. | |
| client = KEY_POOL[0][1] if KEY_POOL else None | |
| # HuggingFace logging setup. RUN_MODE env var picks the bucket | |
| # (test|pilot|prod) appended to mode_prefix to form the final HF dataset path. | |
| LOG_CONFIG = CONFIG.get("logging", {}) | |
| HF_TOKEN = os.getenv(LOG_CONFIG.get("hf_token_env", "HF_access")) | |
| HF_DATASET = LOG_CONFIG.get("hf_dataset") | |
| hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN and HF_DATASET else None | |
| RUN_MODE = os.getenv("RUN_MODE", "test") | |
| LOG_MODE_PREFIX = LOG_CONFIG.get("mode_prefix", "HOT/socratic") | |
| LOG_PATH = f"{LOG_MODE_PREFIX}/{RUN_MODE}" | |
| CONDITION = LOG_CONFIG.get("condition", "socratic") | |
| SCHEMA_VERSION = LOG_CONFIG.get("schema_version", "1.1") | |
| CASE_ID = LOG_CONFIG.get("case_id", "unknown") | |
| # OS detection (still here for tests / utilities; main app no longer uses shell tool). | |
| IS_WINDOWS = platform.system() == "Windows" | |
| def _load_prompt(key, default_relpath): | |
| """Read a prompt markdown file. Returns "" if the file is missing so the | |
| app still boots (with reduced behaviour) rather than crashing.""" | |
| relpath = PATHS.get(key, default_relpath) | |
| path = BASE_DIR / relpath | |
| if not path.exists(): | |
| print(f"[config_loader] Warning: prompt file missing at {path}", flush=True) | |
| return "" | |
| return path.read_text(encoding="utf-8") | |
| MANAGER_PROMPT = _load_prompt("manager_prompt", "prompts/manager_prompt.md") | |
| MAIN_PROMPT = _load_prompt("main_prompt", "prompts/main_prompt.md") | |
| PERFECT_ANSWER_PROMPT = _load_prompt("perfect_answer_prompt", "prompts/perfect_answer_prompt.md") | |