Implement BDEF v1.1 grading: scoring core, per-deck pipeline, ledger, dashboard, StartOS layer
- Deterministic scoring.py (quant 60 / qual 40 / flags -15, profitability heaviest) - Per-company JSON ledger with forecast-target chaining deck N-1 -> N - Single-shot sandbox agent with guided-JSON fallback ladder (no tool loop) - Portfolio dashboard with sparklines, KPI hit rates, BDEF category bars - 48 unit tests green; endpoints smoke-tested; npm check+build green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1dde915540
commit
b1d7aed9f4
+45
-15
@@ -12,13 +12,23 @@ 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")
|
||||
|
||||
DEFAULT_RUBRIC = (
|
||||
"Review the attached document(s). Produce a structured report: a 3-5 sentence "
|
||||
"summary, the key findings and insights, risks or red flags, open questions, "
|
||||
"and concrete recommendations. Cite the document and section for each point. "
|
||||
"Be honest about uncertainty; never invent facts not present in the documents."
|
||||
)
|
||||
# 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
|
||||
@@ -39,22 +49,29 @@ CONFIG_DEFAULTS = {
|
||||
"proxyPort": 4000,
|
||||
"maxConcurrentModels": 1,
|
||||
"models": [
|
||||
{"alias": "reviewer-a", "hfModel": "Qwen/Qwen3-32B-FP8", "spark": "primary", "port": 8001},
|
||||
{"alias": "grader-a", "hfModel": "Qwen/Qwen3-32B-FP8", "spark": "primary", "port": 8001},
|
||||
],
|
||||
# Review panel
|
||||
"reviewers": [
|
||||
{"name": "reviewer-1", "model": "reviewer-a", "persona": "", "temperature": None},
|
||||
# Grading panel
|
||||
"graders": [
|
||||
{"name": "munger-lens", "model": "grader-a", "persona": "", "temperature": None},
|
||||
],
|
||||
# Review job settings
|
||||
"reviewInstructions": DEFAULT_RUBRIC,
|
||||
# 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": "",
|
||||
"synthesisEnabled": True,
|
||||
"synthesisModel": "",
|
||||
"synthesisPersona": "",
|
||||
"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,
|
||||
}
|
||||
@@ -68,9 +85,22 @@ def load() -> dict:
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user