- Pinned target's profitability flag now overrides the extractor's bucket guess - Extractor/target periods canonicalized so ledger forecast chaining matches - autoRunOnDrop no longer error-loops on ungradeable inbox content; failed batches count as seen - Ship 2 default graders (pipeline requires >=2 valid reports per deck) - UI styles 'failed' deck chips as errors; .markdown discoverable - Unknown adjudicator model disables adjudication loudly instead of silently - Reserved rids extractor/adjudicator; teardown also clears bm-grader-* containers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
"""Config loading for the Boardroom Map orchestrator.
|
|
|
|
Defaults mirror startos/file-models/config.ts. The StartOS actions only persist
|
|
the fields the user actually touched, and Python (unlike the zod schema) does not
|
|
auto-fill defaults — so we apply them here. Keep in sync with the zod schema.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import spark_client as sc
|
|
|
|
DATA_DIR = os.environ.get("BM_DATA_DIR", "/data")
|
|
HF_TOKEN_PATH = os.path.join(DATA_DIR, "secrets", "hf_token")
|
|
BDEF_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bdef.md")
|
|
|
|
# Scoring weights (composite 0-100 = quant 60 + qual 40 - penalties).
|
|
# Every knob the deterministic scorer uses lives here so the user can retune
|
|
# without a rebuild. Keep flat: StartOS action inputs are flat number fields.
|
|
WEIGHTS_DEFAULTS = {
|
|
"profitabilityKpi": 30, # profitability KPI attainment bucket
|
|
"otherKpi": 20, # non-profitability measurable KPI bucket
|
|
"forecastIntegrity": 10, # deck N actuals vs deck N-1 stated targets
|
|
"qualCategoryMax": 5, # each BDEF category A-H maxes at this (8x5=40)
|
|
"redFlagCap": 15, # max total penalty
|
|
"kpiCreditFloor": 0.5, # actual/target ratio below which credit = 0
|
|
"droppedKpiPenalty": 2, # severity of a KPI that silently disappeared
|
|
"droppedKpiMax": 3, # count at most this many dropped-KPI flags
|
|
"evidenceFullCredit": 400, # quote chars for full qualitative weight
|
|
"singleSourceFlagFactor": 0.5, # damping for flags raised by one source only
|
|
}
|
|
|
|
CONFIG_DEFAULTS = {
|
|
# Spark connection
|
|
"primarySparkHost": "",
|
|
"primarySparkUser": "nvidia",
|
|
"sshPort": 22,
|
|
"secondarySparkHost": None,
|
|
"useBothSparks": False,
|
|
"headInternalHost": "127.0.0.1",
|
|
"remoteWorkDir": "/home/nvidia/boardroom-map",
|
|
# Images
|
|
"servingImage": "boardroom-vllm:latest",
|
|
"graderImage": "boardroom-grader:latest",
|
|
# Serving
|
|
"gpuMemoryUtilization": "0.85",
|
|
"maxModelLen": 32768,
|
|
"toolCallParser": "hermes",
|
|
"proxyPort": 4000,
|
|
"maxConcurrentModels": 1,
|
|
"models": [
|
|
{"alias": "grader-a", "hfModel": "Qwen/Qwen3-32B-FP8", "spark": "primary", "port": 8001},
|
|
],
|
|
# Grading panel (>= 2 required: every deck needs >= 2 valid grade reports)
|
|
"graders": [
|
|
{"name": "munger-lens", "model": "grader-a", "persona": "", "temperature": None},
|
|
{"name": "girdley-operator", "model": "grader-a", "persona": "", "temperature": None},
|
|
],
|
|
# Which catalog model runs the stage-1 structured extractor ("" = first model)
|
|
"extractorModel": "",
|
|
# Grading job settings
|
|
"bdefOverride": "", # non-empty replaces the baked-in bdef.md rubric
|
|
"weights": dict(WEIGHTS_DEFAULTS),
|
|
"networkMode": "airgapped",
|
|
"searxngUrl": "",
|
|
"adjudicatorEnabled": True,
|
|
"adjudicatorModel": "",
|
|
"adjudicatorPersona": "",
|
|
"wipeRemoteDocs": True,
|
|
"autoRunOnDrop": False,
|
|
"networkName": "boardroom-net",
|
|
# Portfolio companies (authoritative source of pinned targets / aliases).
|
|
# pinnedTargets: [{kpi, target, unit, direction: gte|lte, profitability}]
|
|
# kpiAliases: newline-separated "canonical=alias1;alias2" lines.
|
|
"companies": [],
|
|
# Flags
|
|
"hfTokenSet": False,
|
|
}
|
|
|
|
|
|
def load() -> dict:
|
|
"""Return the merged config (defaults <- saved), or just defaults if unset."""
|
|
merged = dict(CONFIG_DEFAULTS)
|
|
try:
|
|
saved = sc.load_config()
|
|
except FileNotFoundError:
|
|
return merged
|
|
merged.update({k: v for k, v in saved.items() if v is not None})
|
|
# weights merge key-by-key so a partially-saved weights object keeps defaults
|
|
w = dict(WEIGHTS_DEFAULTS)
|
|
w.update({k: v for k, v in (merged.get("weights") or {}).items() if v is not None})
|
|
merged["weights"] = w
|
|
return merged
|
|
|
|
|
|
def bdef_text(cfg: dict) -> str:
|
|
"""The grading rubric: config override if set, else the baked-in spec."""
|
|
override = (cfg.get("bdefOverride") or "").strip()
|
|
if override:
|
|
return override
|
|
with open(BDEF_PATH, encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def hf_token() -> str | None:
|
|
if os.path.exists(HF_TOKEN_PATH):
|
|
t = open(HF_TOKEN_PATH).read().strip()
|
|
return t or None
|
|
return None
|