"""Config for the Matrix intake bot — Matrix creds + the dedicated intake room. Spark settings (SPARK_CONTROL_URL, CHAT_MODEL, …) are NOT read here; they come from the reused ingest client (see spark.py), which loads the same repo .env. This module only owns the Matrix connection and the CRM API target for the write-back leg (M2). """ import os REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def load_env(path=None): """Populate os.environ from the repo .env (setdefault — never clobber a real env var).""" path = path or os.path.join(REPO_ROOT, ".env") if not os.path.exists(path): return with open(path, "r", encoding="utf-8") as fh: for line in fh: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) os.environ.setdefault(k.strip(), v.strip()) load_env() def _require(name): val = os.environ.get(name, "").strip() if not val: raise RuntimeError(f"matrix_intake: required env var {name} is not set (see .env.example)") return val # Matrix connection (resolved lazily so importing this module for tests never requires creds). def matrix_settings(): return { "homeserver": _require("MATRIX_HOMESERVER"), "user_id": _require("MATRIX_USER"), "token": _require("MATRIX_ACCESS_TOKEN"), "device_id": os.environ.get("MATRIX_DEVICE_ID", "ten31-intake-bot"), "intake_room": _require("MATRIX_INTAKE_ROOM"), } # CRM API target for the write-back leg (M2). The CRM has no service-key auth path — auth is # Bearer-JWT via /api/auth/login — so the bot logs in as a DEDICATED service user (a normal # CRM user, created by an admin) and reuses the existing auth. Creds live in .env, never code. def crm_settings(): return { "base": os.environ.get("CRM_API_BASE", "http://127.0.0.1:8080").rstrip("/"), "username": os.environ.get("CRM_BOT_USERNAME", "").strip(), "password": os.environ.get("CRM_BOT_PASSWORD", ""), "verify_tls": os.environ.get("CRM_API_VERIFY_TLS", "true").lower() in ("1", "true", "yes", "on"), } # Team-member names (comma-separated in INTAKE_TEAM_ROSTER), fed to the parser so a teammate's # name reads as the person DOING outreach, not the investor (see parse.build_system). Optional — # unset/empty just means no roster framing, i.e. the prior behavior. def team_roster(): return [n.strip() for n in os.environ.get("INTAKE_TEAM_ROSTER", "").split(",") if n.strip()] # Dedicated room for reviewing CRM-drafted email-activity proposals (the CRM→Matrix push leg). # Separate from the intake room so high-volume email proposals don't drown the conversational # intake flow. Unset/empty disables the whole email-review poll loop (the bot just does intake). def email_review_room(): return os.environ.get("MATRIX_EMAIL_REVIEW_ROOM", "").strip()