269 lines
No EOL
14 KiB
Python
269 lines
No EOL
14 KiB
Python
"""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}") |