simbiot-harness/harness/aum.py

227 lines
No EOL
12 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")
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 {}
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)
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,
"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 ""
return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов "
f"на длинной дистанции). {part}; burn 9router 7д="
f"{m.get('burn_7d', 0)} у.е. (прямой минус к AUM). Фильтр любого "
f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; "
f"короткий доход, съедающий долгосрочную базу — ниже приоритетом."
f"{tail}")