68106d7a5a
Read-only natural-language query over the curated nl_query endpoint, answered in-thread. Two entry points (room-per-purpose model): a dedicated Q&A room (MATRIX_QUERY_ROOM) where every top-level message is a question, plus the ?/@bot trigger in the intake room as a cross-room convenience. Both routes hit the same handle_query -> crm_client.nl_query -> POST /api/query/nl; translation runs on the box's local model, nothing leaves the box, and there is no write path so no approval gate applies. Pure logic (trigger parsing, answer rendering) in query.py with offline tests; async room wiring in bot.py (live-smoke only, per the bot's convention). Bot-side only, ships on the Spark via git pull + restart. Depends on the box-side /api/query/nl endpoint, which lands with the v93 s9pk (reminders + W2): until v93 is installed the Q&A surface 404s, so the bot deploy is staged to follow that install.
81 lines
3.6 KiB
Python
81 lines
3.6 KiB
Python
"""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()
|
|
|
|
|
|
# Dedicated Q&A room for read-only natural-language queries (W2). In this room EVERY top-level
|
|
# message is treated as a question — no '?'/'@bot' trigger needed (the trigger only exists to
|
|
# disambiguate question-vs-note when Q&A shares the intake room; here that's unnecessary). The
|
|
# '?'/'@bot' trigger still works in the intake room too, as a cross-room convenience. Unset/empty
|
|
# just means no dedicated room (questions then go through the intake-room trigger). The bot must be
|
|
# a member of this room. Read-only — no approval gate, no redaction, no special power level needed.
|
|
def query_room():
|
|
return os.environ.get("MATRIX_QUERY_ROOM", "").strip()
|