simbiot-harness/harness/aum.py

269 lines
No EOL
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Simbiot Harness — North Star metric: долгосрочная AUM (t-260929161138).
Формализует главную цель (фиксация Alex, m-516173142): максимизация
суммарных измеримых ресурсов кооператива на длинной дистанции.
Состав метрики:
- AUM_t — балансы кооператива в у.е. (USD-эквивалент). Источник уровня
день: docs/aum/balances.json (ручной ввод Alex, схема
{"accounts":[{"name":..., "amount_usd":...}]}); пока файла нет —
AUM = None (не замерен), но драйверы считаются уже сейчас.
- burn_t — расход на LLM-инфраструктуру (9router, живая БД usageHistory
/root/.9router/db/data.sqlite, env SIMBIOT_ROUTER_DB переопределяет;
на тестах БД роутера не требуется — ошибка чтения → пустой замер).
- ΔAUM(t) = revenue_t − burn_t; фильтр решений: «даёт ≥ +1% к AUM в
горизонте год+?» (README, раздел «Главная цель»).
Петля оптимизации (пилот v0.1, песочница): симуляция сценариев
(бутстрап исторических суточных burn 9router) + генетический отбор
конфигураций расхода по fitness = E[ΔAUM горизонта]. Результат —
АДВОКАТ-отчёт (рекомендация дню/Alex'у), НЕ авто-применение.
Модель продуктивности — честный плейсхолдер (см. docs/aum.md): бюджет
дня покупает токены по ставкам моделей (glm-5.3-flash / glm-5.3 из
дайджеста расходов 25.09–02.10), выручка = P·√(полезность токенов),
полезность flash-токенов слегка ниже (q_flash=0.95). P калибруется так,
чтобы бейзлайн «как сейчас» давал −5% в день (лёгкое давление к
оптимизации). В v0.2 P заменяется реальными выручочными ставками
проектов кооператива.
"""
import json
import os
import random
import sqlite3
import statistics
from datetime import datetime, timezone
# ────────────────────────── источники данных ──────────────────────────
ROUTER_DB = os.environ.get(
"SIMBIOT_ROUTER_DB", "/root/.9router/db/data.sqlite")
STORE_DB = os.environ.get(
"SIMBIOT_DB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "harness.db"))
HARNESS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BALANCES_JSON = os.path.join(HARNESS_ROOT, "docs", "aum", "balances.json")
COOP_JSON = os.environ.get("SIMBIOT_COOP_JSON", "/root/coop_data.json")
RATE_FLASH = 0.001022 # у.е./токен glm-5.3-flash (дайджест 25.09–02.10)
RATE_FULL = 0.001058 # у.е./токен glm-5.3
Q_FLASH = 0.95 # полезность flash-токена относительно полного
BASELINE_NET = 0.95 # выручка бейзлайна = 95% расхода (−5%/день)
HORIZON_DAYS = 365
def _fmt(v):
return None if v is None else round(float(v), 2)
# ────────────────────────── чтение данных ──────────────────────────
def read_balances():
"""Балансы кооператива из docs/aum/balances.json. total=None если нет файла."""
try:
path = os.environ.get("SIMBIOT_BALANCES_JSON", BALANCES_JSON)
with open(path) as f:
data = json.load(f)
accs = [a for a in data.get("accounts", [])
if a.get("amount_usd") is not None]
return {"total": _fmt(sum(float(a["amount_usd"]) for a in accs))
if accs else None, "accounts": accs, "src": "balances.json"}
except Exception:
return {"total": None, "accounts": [], "src": "balances.json (нет файла)"}
def read_router_spend(days: int = 7):
"""Суточный расход 9router за N дней: {date: {cost, reqs}}. Ошибка → {}."""
try:
db = sqlite3.connect(f"file:{ROUTER_DB}?mode=ro", timeout=5, uri=True)
rows = db.execute(
"SELECT substr(timestamp,1,10) d, ROUND(SUM(cost),4) c, COUNT(*) r "
"FROM usageHistory WHERE timestamp >= datetime('now', ?) "
"GROUP BY d ORDER BY d", (f"-{int(days)} days",)).fetchall()
db.close()
return {d: {"cost": c, "reqs": r} for d, c, r in rows}
except Exception:
return {}
# Блендед-ставка токен→у.е., откалибрована на окне 25.09–02.10
# (docs/digests/9router-spend-2026-09-25_10-02.md): 2491.47 у.е. /
# 2.642 млрд токенов флота ≈ 0.000943 у.е./токен. Порядок величины, не счёт.
BURN_RATE_PER_TOKEN = float(os.environ.get(
"SIMBIOT_BURN_RATE_PER_TOKEN", "0.000943"))
def read_store_spend(days: int = 7):
"""Fallback-расход из событий BudgetSpent стора харнеса (только СВОИ тики).
Используется, когда живая БД 9router недоступна (миграция хоста унесла
/root/.9router): метрика burn не должна молча зануляться — иначе
policy.aum_target врёт «burn=0» в контексте каждого тика. Оценка
блендед-ставкой; в событиях нет разбивки по моделям.
"""
try:
db = sqlite3.connect(f"file:{STORE_DB}?mode=ro", timeout=5, uri=True)
rows = db.execute(
"SELECT substr(timestamp,1,10) d, COUNT(*) r, "
"SUM(COALESCE(json_extract(data,'$.prompt_tokens'),0)"
"+COALESCE(json_extract(data,'$.completion_tokens'),0)) t "
"FROM events WHERE event_type='BudgetSpent' "
"AND timestamp >= datetime('now', ?) "
"GROUP BY d ORDER BY d", (f"-{int(days)} days",)).fetchall()
db.close()
return {d: {"cost": round(t * BURN_RATE_PER_TOKEN, 4), "reqs": r}
for d, r, t in rows}
except Exception:
return {}
def read_coop_structure():
"""Структура кооператива (проекты/выручочные доли) — множитель метрики."""
try:
with open(COOP_JSON) as f:
data = json.load(f)
return {"title": (data.get("mission") or {}).get("title"),
"projects": [{"name": p.get("name"),
"revenue_pct": p.get("revenue_pct")}
for p in data.get("projects", [])]}
except Exception:
return {"title": None, "projects": []}
# ────────────────────────── замер ──────────────────────────
def measure():
"""Снапшот метрики: AUM (если замерен) + burn 24ч/7д + структура кооператива."""
bal = read_balances()
spend = read_router_spend(days=7)
spend_src = "9router-db"
if not spend: # БД роутера недоступна → не молчим, а считаем из стора
spend = read_store_spend(days=7)
spend_src = "store-BudgetSpent-оценка" if spend else "недоступен"
days_iter = list(spend.values())
return {
"ts": datetime.now(timezone.utc).isoformat(),
"aum": bal["total"], "balances_src": bal["src"],
"burn_24h": _fmt(days_iter[-1]["cost"]) if days_iter else 0.0,
"burn_7d": _fmt(sum(v["cost"] for v in days_iter)),
"spend_days": spend,
"burn_src": spend_src,
"coop": read_coop_structure(),
}
# ────────────────────────── симуляция сценариев ──────────────────────────
def _model_params(config, productivity):
volume = min(3.0, max(0.05, float(config.get("volume_mult", 1.0))))
flash = min(1.0, max(0.0, float(config.get("flash_share", 0.84))))
rate = flash * RATE_FLASH + (1 - flash) * RATE_FULL
quality = flash * Q_FLASH + (1 - flash) * 1.0
return volume, rate, quality, productivity
def calibrate(burn_hist: list, base_config: dict) -> float:
"""P бейзлайна: выручка = BASELINE_NET × средний burn (давление к оптимизации)."""
mean_b = statistics.mean(burn_hist) or 100.0
_, rate, quality, _ = _model_params(base_config, 0.0)
utility = (mean_b / rate) * quality
return BASELINE_NET * mean_b / max(utility ** 0.5, 1e-9)
def simulate(config: dict, burn_hist: list, productivity: float,
days: int = HORIZON_DAYS, seed: int = 42,
trials: int = 120) -> dict:
"""E[ΔAUM в горизонте days]: бюджет дня B=volume·burn_t покупает токены
B/rate; полезность токенов×quality → выручка P·√польз; Δ = выручка − B."""
volume, rate, quality, P = _model_params(config, productivity)
rng = random.Random(seed)
finals = []
for _ in range(trials):
aum = 0.0
for _d in range(days):
B = volume * rng.choice(burn_hist)
utility = (B / rate) * quality
aum += P * (utility ** 0.5) - B
finals.append(aum)
return {"mean": statistics.mean(finals),
"p10": statistics.quantiles(finals, n=10)[0]}
def score(config, burn_hist, productivity, seed=42):
return simulate(config, burn_hist, productivity, seed=seed)["mean"]
# ────────────────────────── генетический отбор ──────────────────────────
def evolve(burn_hist: list, pop_size: int = 12, generations: int = 6,
seed: int = 7) -> dict:
"""Генетический отбор по fitness=E[ΔAUM]; песочница — отчёт-адвокат."""
rng = random.Random(seed)
base_cfg = {"volume_mult": 1.0, "flash_share": 0.84} # «как сейчас»
P = calibrate(burn_hist, base_cfg)
base_fit = score(base_cfg, burn_hist, P, seed=seed)
def mutate(c):
return {"volume_mult": round(min(3.0, max(0.05, c["volume_mult"] + rng.gauss(0, 0.2))), 3),
"flash_share": round(min(1.0, max(0.0, c["flash_share"] + rng.gauss(0, 0.12))), 3)}
def crossover(a, b):
return {"volume_mult": round((a["volume_mult"] + b["volume_mult"]) / 2, 3),
"flash_share": round((a["flash_share"] + b["flash_share"]) / 2, 3)}
pop = [{"volume_mult": round(rng.uniform(0.3, 1.8), 3),
"flash_share": round(rng.uniform(0.2, 1.0), 3)}
for _ in range(pop_size)]
best, best_fit = base_cfg, base_fit
history = []
for g in range(generations):
scored = sorted(((score(c, burn_hist, P, seed=seed + g), c) for c in pop),
reverse=True)
if scored[0][0] > best_fit:
best_fit, best = scored[0][0], dict(scored[0][1])
history.append(round(scored[0][0], 2))
elite = [c for _, c in scored[: max(2, pop_size // 3)]]
pop = list(elite)
while len(pop) < pop_size:
pop.append(mutate(crossover(rng.choice(elite), rng.choice(elite))))
gap = (best_fit - base_fit) / abs(base_fit) * 100 if base_fit else 0.0
return {
"baseline": base_cfg, "baseline_fitness": round(base_fit, 2),
"best": best, "best_fitness": round(best_fit, 2),
"improves": bool(best_fit > base_fit + 1e-9),
"improvement_pct": round(gap, 2),
"generations": generations, "pop_size": pop_size,
"productivity_P": round(P, 4),
"burn_mean_day": round(statistics.mean(burn_hist), 2),
"fitness_history": history,
"verdict": ("конфигурация лучше статус-кво — вынести дню/Alex'у "
"(адвокат, не авто-применение)")
if best_fit > base_fit + 1e-9 else "статус-кво остаётся",
}
def run_pilot(m: dict) -> dict:
"""Пилот на свежем замере: история burn → симуляции → отбор → вывод."""
hist = [v["cost"] for v in m.get("spend_days", {}).values()] or [100.0]
return {"ts": m["ts"], "aum": m["aum"], "burn_7d": m.get("burn_7d"),
"evolution": evolve(hist),
"note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"}
def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
"""Однострочник для policy/контекста всех уровней."""
aum = m.get("aum")
part = (f"AUM={aum} у.е." if aum is not None
else "AUM пока не замерен (когда будет баланс — занеси в "
"docs/aum/balances.json)")
ev = (pilot or {}).get("evolution") or {}
tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else ""
src = m.get("burn_src") or "9router-db"
burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; "
f"живая БД 9router недоступна)" if src.startswith("store")
else f"{m.get('burn_7d', 0)}")
return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов "
f"на длинной дистанции). {part}; burn 9router 7д="
f"{burn} у.е. (прямой минус к AUM). Фильтр любого "
f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; "
f"короткий доход, съедающий долгосрочную базу — ниже приоритетом."
f"{tail}")