File size: 3,815 Bytes
3eae4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# ── Path bootstrap ─────────────────────────────────────────────────────────────
from __future__ import annotations
from pathlib import Path

# Load .env file if it exists β€” must happen before Pydantic Settings reads env vars
try:
    from dotenv import load_dotenv
except (ImportError, AttributeError):
    # Keep runtime functional even when python-dotenv is not installed
    # or when a conflicting `dotenv` package is present.
    def load_dotenv(*args, **kwargs):  # type: ignore[no-redef]
        return False
_ENV_FILE = Path(__file__).resolve().parent.parent / ".env"
load_dotenv(dotenv_path=_ENV_FILE, override=False)
# override=False means real environment variables always win over .env values
# ──────────────────────────────────────────────────────────────────────────────

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class ServerSettings(BaseSettings):
    """
    HTTP-server configuration.
    Read from environment variables prefixed SERVER_.
    Example: SERVER_PORT=8080  SERVER_LOG_LEVEL=debug

    Intentionally isolated from EnvSettings β€” changing server bind
    options never affects simulation behaviour, and vice-versa.
    Both classes are instantiated once at import and treated as
    read-only singletons for the lifetime of the process.
    """

    host: str = Field("0.0.0.0", description="Bind host")
    port: int = Field(7860, description="Bind port β€” HF Spaces default is 7860")
    log_level: str = Field(
        "info", description="Uvicorn log level: debug | info | warning | error"
    )
    cors_origins: list[str] = Field(
        default=["*"],
        description="Allowed CORS origins. '*' is required for HF Spaces embedding.",
    )
    # NOTE: Keep at 1 when using the in-memory session store.
    # Multiple workers do NOT share process memory.
    # Use Redis + a shared store before increasing workers in production.
    workers: int = Field(
        1, description="Uvicorn worker count β€” keep at 1 for in-memory sessions"
    )

    model_config = SettingsConfigDict(env_prefix="SERVER_", extra="ignore")


class EnvSettings(BaseSettings):
    """
    Simulation-environment defaults.
    Read from environment variables prefixed ENV_.
    Example: ENV_DEFAULT_TASK_ID=mixed_urgency_medium  ENV_MAX_SESSIONS=50

    Controls the environment kernel only. No effect on network
    binding, logging, or CORS β€” those belong to ServerSettings.
    """

    default_task_id: str = Field(
        "district_backlog_easy",
        description="Task used when POST /reset is called without an explicit task_id",
    )
    default_seed: int = Field(
        11,
        description="Seed used when POST /reset is called without an explicit seed",
    )
    max_steps_per_episode: int = Field(
        500,
        description="Hard cap on step() calls per session before episode is truncated",
    )
    max_sessions: int = Field(
        100,
        description="Maximum concurrent in-memory sessions. Oldest is evicted when exceeded.",
    )

    model_config = SettingsConfigDict(env_prefix="ENV_", extra="ignore")


# ── Singletons ────────────────────────────────────────────────────────────────
# Loaded exactly once at import time. Never mutated at runtime.
# Tests may monkeypatch individual fields after import if needed.
server_settings = ServerSettings()
env_settings = EnvSettings()