Живая петля AUM-метрики: router_usage.json (недельное окно, накопительно) + часовые точки AumMeasured + env-доктор ключей + Δburn нед/нед в policy + спарклайн на публичный дашборд — t-261019001211-d6a921

This commit is contained in:
simbiot 2026-10-19 01:08:18 +00:00
parent cbf03ffac2
commit 69ac2cecaa
4 changed files with 281 additions and 1 deletions

View file

@ -0,0 +1,15 @@
{
"weeks": [
{
"week_start": "2026-10-19T01:06:57.981175+00:00",
"start_day": "2026-10-19",
"burn_usd": 7.7078,
"reqs": 243,
"days_seen": 2,
"src": "недоступен",
"active": true,
"updated_at": "2026-10-19T01:08:13.330295+00:00"
}
],
"method": "BudgetSpent стора × блендед-ставка 9.43e-07 у.е./токен; окно 7д, накопительно в рамках недели (t-261019001211)"
}

View file

@ -238,6 +238,128 @@ def read_coop_structure():
return {"title": None, "projects": []} return {"title": None, "projects": []}
# ───────────── живая петля burn-7д: router_usage.json (t-261019001211) ─────────────
# Слепота AUM-метрики: burn живёт только в тексте policy и теряет историю
# при недельном скольжении. Файл docs/aum/router_usage.json — компактный реестр
# «неделя → замер» с ежегодным сбросом окна: каждая запись — сумма расхода
# с момента прошлого замера (окно кратно ~7д, ровно одна активная неделя).
# Идёмпотентность: замер того же дня не создаёт новую запись, а дополняет
# активное окно (докупка ΔB = sum_7d − уже_записанное). Так скользящее окно
# BudgetSpent-стора превращается в стабильный накопительный реестр: виден
# и фактический burn недели, и Δ-тренд неделя-к-неделе.
ROUTER_USAGE_JSON = os.environ.get(
"SIMBIOT_ROUTER_USAGE_JSON",
os.path.join(os.path.dirname(BALANCES_JSON), "router_usage.json"))
def update_router_usage(snap: dict = None, path: str = None):
"""Дополнить реестр router_usage.json свежим замером burn (7д).
Вход — снапшот aum.measure() (или собирается сам). Логика:
- если активной записи нет или её окно старше 21д — новая запись;
- если окно той же недели — досчитать Δburn от уже записанного
(скользящее окно BudgetSpent суммирует 7д заново, реестр — накопительно);
- окно ровно одно активное; история капится 260 записями (5 лет).
Ошибки не бросает (best effort, как и весь AUM-контур) — возвращает dict.
"""
path = path or os.environ.get("SIMBIOT_ROUTER_USAGE_JSON", ROUTER_USAGE_JSON)
snap = snap or measure()
days = snap.get("spend_days") or {}
sum7 = round(sum(v.get("cost", 0) for v in days.values()), 4)
today = snap.get("ts", "")[:10]
try:
from datetime import date
try:
today_d = date.fromisoformat(today)
except ValueError:
today_d = date.today()
try:
with open(path, encoding="utf-8") as f:
reg = json.load(f)
except Exception:
reg = {"weeks": []}
weeks = reg.setdefault("weeks", [])
cur = weeks[-1] if weeks else None
stale = True
if cur and cur.get("active") and cur.get("start_day"):
try:
stale = (today_d - date.fromisoformat(cur["start_day"])).days > 21
except ValueError:
stale = True
fresh = False
if (not cur or not cur.get("active") or stale):
# закрываем прошлое окно и заводим новое (сброс недели)
if cur and cur.get("active"):
cur["active"] = False
cur["closed_at"] = snap.get("ts")
cur = {"week_start": snap.get("ts"), "start_day": today,
"burn_usd": 0.0, "reqs": 0, "days_seen": 0,
"src": snap.get("burn_src"), "active": True}
weeks.append(cur)
fresh = True
# накопительная досумма поверх уже записанного в этом окне
prev = float(cur.get("burn_usd") or 0)
delta = round(sum7 - prev, 4)
if delta < 0: # скользящее окно выпало из старых дней — не отнимаем
delta = 0.0
cur["burn_usd"] = round(prev + delta, 4)
cur["reqs"] = max(int(cur.get("reqs") or 0),
sum(v.get("reqs", 0) for v in days.values()))
cur["days_seen"] = max(int(cur.get("days_seen") or 0), len(days))
cur["src"] = snap.get("burn_src") or cur.get("src")
cur["updated_at"] = snap.get("ts")
if len(weeks) > 260:
reg["weeks"] = weeks[-260:]
reg["method"] = ("BudgetSpent стора × блендед-ставка "
f"{BURN_RATE_PER_TOKEN:.2e} у.е./токен; окно 7д, "
"накопительно в рамках недели (t-261019001211)")
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(reg, f, ensure_ascii=False, indent=2)
f.write("\n")
os.replace(tmp, path) # атомарно: читатели не увидят полуфайл
return {"path": path, "week_start": cur.get("week_start"),
"burn_usd": cur["burn_usd"], "delta_usd": delta,
"fresh_week": fresh}
except Exception as e:
return {"path": path, "error": str(e)[:200]}
# ────────── env-доктор авто-источников AUM (t-261019001211, пункт 2) ──────────
# balances.json сейчас слеп: OPENROUTER_KEY и STRIPE_LIVE_KEY живут на другом
# хосте (KE-SREGISTRY). Чтобы запрос к Alex не был «сделай что-то», доктор
# собирает точный список: каких переменных в каком .env не хватает и что
# именно каждая открывает в метрике.
ENV_KEYS_NEEDED = (
# (переменная, файл, что открывает в AUM-петле)
("OPENROUTER_KEY", "simbiot-harness/.env",
"кредиты OpenRouter (auto-баланс №1, сейчас 26.7 фикс с 2026-10-04)"),
("STRIPE_LIVE_KEY", "/root/coop/.env",
"Stripe live available (auto-баланс, подписки $99/мес)"),
("SIMBIOT_ROUTER_DB", "simbiot-harness/.env",
"живая БД 9router (usageHistory): точный burn вместо блендед-оценки"),
)
def env_doctor(env_path: str = None, coop_env: str = "/root/coop/.env"):
"""Какие ключи отсутствуют + готовый шаблон запроса к Alex (без значений)."""
env_path = env_path or _paths.env_path()
missing = []
for var, where, why in ENV_KEYS_NEEDED:
path = coop_env if var == "STRIPE_LIVE_KEY" else env_path
present = _env_file_value(path, var) is not None
if not present:
missing.append({"var": var, "env_file": path, "opens": why})
request = ("Чтобы петля AUM видела деньги без ручного ввода, добавь в .env:\n"
+ "\n".join(f" {m['var']}=… # в {m['env_file']} — {m['opens']}"
for m in missing)) if missing else \
"Все ключи авто-источников на месте — петля AUM не слепа."
return {"missing": missing, "request_to_alex": request,
"checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds")}
# ────────────────────────── замер ────────────────────────── # ────────────────────────── замер ──────────────────────────
def measure(): def measure():
@ -258,6 +380,15 @@ def measure():
if not spend: # БД роутера недоступна → не молчим, а считаем из стора if not spend: # БД роутера недоступна → не молчим, а считаем из стора
spend = read_store_spend(days=7) spend = read_store_spend(days=7)
spend_src = "store-BudgetSpent-оценка" if spend else "недоступен" spend_src = "store-BudgetSpent-оценка" if spend else "недоступен"
# живая петля burn-7д (t-261019001211): каждый замер дополняет реестр
# router_usage.json (накопительное окно недели) — контур не теряет историю
if os.environ.get("SIMBIOT_AUM_USAGE_REGISTRY", "1") not in ("0", "false", "no"):
_snap = {"ts": datetime.now(timezone.utc).isoformat(),
"spend_days": spend, "burn_src": spend_src}
try:
update_router_usage(_snap)
except Exception:
pass # реестр — best effort: замер важнее и идёт всегда
days_iter = list(spend.values()) days_iter = list(spend.values())
return { return {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
@ -368,6 +499,25 @@ def run_pilot(m: dict) -> dict:
"note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"} "note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"}
def weekly_burn_delta(path: str = None):
"""Δ burn последняя-прошлая неделя из router_usage.json (у.е. или None).
Пункт 4 петли (t-261019001211): манифесту дня нужен не только абсолют
burn, но и тренд — растём или падаем относительно прошлой недели.
"""
path = path or os.environ.get("SIMBIOT_ROUTER_USAGE_JSON", ROUTER_USAGE_JSON)
try:
with open(path, encoding="utf-8") as f:
weeks = json.load(f).get("weeks", [])
if len(weeks) < 2:
return None
cur, prev = weeks[-1], weeks[-2]
return round(float(cur.get("burn_usd") or 0)
- float(prev.get("burn_usd") or 0), 2)
except Exception:
return None
def build_policy_aum_summary(m: dict, pilot: dict = None) -> str: def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
"""Однострочник для policy/контекста всех уровней.""" """Однострочник для policy/контекста всех уровней."""
aum = m.get("aum") aum = m.get("aum")
@ -376,13 +526,16 @@ def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
"docs/aum/balances.json)") "docs/aum/balances.json)")
ev = (pilot or {}).get("evolution") or {} ev = (pilot or {}).get("evolution") or {}
tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else "" tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else ""
wd = weekly_burn_delta()
trend = (f"; Δburn к прошлой неделе: {'+' if wd >= 0 else ''}{wd} у.е."
if wd is not None else "")
src = m.get("burn_src") or "9router-db" src = m.get("burn_src") or "9router-db"
burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; " burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; "
f"живая БД 9router недоступна)" if src.startswith("store") f"живая БД 9router недоступна)" if src.startswith("store")
else f"{m.get('burn_7d', 0)}") else f"{m.get('burn_7d', 0)}")
return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов " return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов "
f"на длинной дистанции). {part}; burn 9router 7д=" f"на длинной дистанции). {part}; burn 9router 7д="
f"{burn} у.е. (прямой минус к AUM). Фильтр любого " f"{burn} у.е.{trend} (прямой минус к AUM). Фильтр любого "
f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; " f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; "
f"короткий доход, съедающий долгосрочную базу — ниже приоритетом." f"короткий доход, съедающий долгосрочную базу — ниже приоритетом."
f"{tail}") f"{tail}")

View file

@ -1256,6 +1256,26 @@ def process_level(level_cfg, model: dict, seq: int = None):
except Exception: except Exception:
pass # замер — best effort, не валить тик pass # замер — best effort, не валить тик
# живая петля AUM-метрики (t-261019001211): дневной снапшот — раз в сутки
# (см. выше), часовой дописывает внутрисуточные точки в aum_history —
# спарклайн дашборда оживает, а не обновляется раз в день. Идемпотентно
# по aggregate_id=aum:hour:<UTC-час>: повторный тик часа не задублирует.
if level_cfg.name == "hour":
try:
from . import aum as _aum
snap = _aum.measure()
hour_key = snap["ts"][:13] # 2026-10-19T01
ev = es.append_event("harness", f"hour:{hour_key}", "AumMeasured", {
"ts": snap["ts"], "aum": snap["aum"],
"burn_24h": snap["burn_24h"], "burn_7d": snap["burn_7d"],
"policy": {"aum_target": _aum.build_policy_aum_summary(snap)},
})
if ev:
_log(f"aum: часовая точка {hour_key} записана "
f"(burn_24h={snap['burn_24h']})")
except Exception:
pass # замер — best effort, не валить тик
# ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается # ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается
# только если есть событие ≥ порога уровня; тихий такт отрабатывает # только если есть событие ≥ порога уровня; тихий такт отрабатывает
# детерминированной механикой (как sec) — без расхода кредитов роутера. # детерминированной механикой (как sec) — без расхода кредитов роутера.

View file

@ -206,6 +206,98 @@ finally:
aum._http_json = _saved_http aum._http_json = _saved_http
check("stripe: livemode=False → None (не актив)", v_test is None) check("stripe: livemode=False → None (не актив)", v_test is None)
# ── 10. живая петля burn-7д: router_usage.json (t-261019001211) ──
RU = os.path.join(tmpdir, "router_usage.json")
for p in (RU,):
if os.path.exists(p):
os.unlink(p)
# 10a: первый замер → новая активная неделя
r1 = aum.update_router_usage({"ts": "2026-10-12T00:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-10-11": {"cost": 2.0, "reqs": 10},
"2026-10-12": {"cost": 1.0, "reqs": 5}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: первая неделя создана",
r1.get("fresh_week") and len(w) == 1 and w[0]["active"]
and abs(w[0]["burn_usd"] - 3.0) < 0.001, str(r1))
# 10b: тот же день, окно подросло → накопительная досумма (не дубль полной суммы)
r2 = aum.update_router_usage({"ts": "2026-10-12T12:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-10-11": {"cost": 2.0, "reqs": 10},
"2026-10-12": {"cost": 2.5, "reqs": 12}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: досумма Δ в той же неделе",
not r2.get("fresh_week") and abs(r2.get("delta_usd", -1) - 1.5) < 0.001
and abs(w[0]["burn_usd"] - 4.5) < 0.001, f"r2={r2}")
# 10c: скользящее окно сжалось (выпал старый день) — не отнимаем
r3 = aum.update_router_usage({"ts": "2026-10-13T00:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-10-12": {"cost": 2.5, "reqs": 12},
"2026-10-13": {"cost": 1.0, "reqs": 4}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: сжатие окна не отнимает burn",
abs(r3.get("delta_usd", -1)) < 0.001 and abs(w[0]["burn_usd"] - 4.5) < 0.001,
f"r3={r3}")
# 10d: окно старше 21д → сброс, новое окно, старое закрыто
r4 = aum.update_router_usage({"ts": "2026-11-15T00:00:00+00:00",
"burn_src": "store-BudgetSpent-оценка",
"spend_days": {"2026-11-14": {"cost": 7.0, "reqs": 30}}}, path=RU)
w = json.load(open(RU))["weeks"]
check("router_usage: недельный сброс окна",
r4.get("fresh_week") and len(w) == 2 and not w[0]["active"]
and w[1]["active"] and abs(w[1]["burn_usd"] - 7.0) < 0.001, str(r4))
# 10e: битый путь → ошибка в dict, без исключения
r5 = aum.update_router_usage({"ts": "2026-11-15T01:00:00+00:00",
"spend_days": {"2026-11-14": {"cost": 1.0, "reqs": 3}}},
path="/nonexistent-dir/ru.json")
check("router_usage: ошибка тихо в dict", "error" in r5)
# 10f: measure() ведёт реестр сам (интеграция: hermetic, реестр в tmp)
os.environ["SIMBIOT_ROUTER_USAGE_JSON"] = os.path.join(tmpdir, "ru2.json")
_m = aum.measure()
check("measure: router_usage пишется из замера",
os.path.exists(os.environ["SIMBIOT_ROUTER_USAGE_JSON"]))
# ── 11. env-доктор авто-источников (t-261019001211) ──
_empty = os.path.join(tmpdir, "empty.env")
with open(_empty, "w") as f:
f.write("# пусто\n")
doc = aum.env_doctor(env_path=_empty, coop_env=os.path.join(tmpdir, "no-coop.env"))
check("env_doctor: все три ключа отсутствуют на пустых .env",
[m["var"] for m in doc["missing"]] ==
["OPENROUTER_KEY", "STRIPE_LIVE_KEY", "SIMBIOT_ROUTER_DB"], str(doc["missing"]))
check("env_doctor: шаблон запроса к Alex готов",
"OPENROUTER_KEY" in doc["request_to_alex"]
and "STRIPE_LIVE_KEY" in doc["request_to_alex"])
_full = os.path.join(tmpdir, "full.env")
with open(_full, "w") as f:
f.write("OPENROUTER_KEY=sk-test\nSIMBIOT_ROUTER_DB=/tmp/x.sqlite\n")
_coop_full = os.path.join(tmpdir, "coop.env")
with open(_coop_full, "w") as f:
f.write("STRIPE_LIVE_KEY=sk-live-test\n")
doc2 = aum.env_doctor(env_path=_full, coop_env=_coop_full)
check("env_doctor: полный .env → missing=[]",
doc2["missing"] == [] and "не слепа" in doc2["request_to_alex"])
# ── 12. Δ burn неделя-к-неделе (t-261019001211, пункт 4) ──
RU2 = os.path.join(tmpdir, "ru_delta.json")
with open(RU2, "w") as f:
json.dump({"weeks": [{"burn_usd": 10.0, "active": False},
{"burn_usd": 7.5, "active": True}]}, f)
check("weekly_burn_delta: −2.5", aum.weekly_burn_delta(path=RU2) == -2.5)
os.environ["SIMBIOT_ROUTER_USAGE_JSON"] = RU2
check("weekly_burn_delta: одна неделя → None",
aum.weekly_burn_delta(path=os.path.join(tmpdir, "ru-one.json")) is None)
s_wd = aum.build_policy_aum_summary({"aum": 26.7, "burn_7d": 7.5,
"burn_src": "store-BudgetSpent-оценка"})
check("summary: Δburn неделя-к-неделе в контексте", "Δburn" in s_wd and "-2.5" in s_wd,
s_wd)
print() print()
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}") print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)