CoolFace
Apppublic

OMNIP0TENT/Syntax_Squad_FinopsENV

sourceHugging Faceupdated 6mo agoView on Hugging Face
1likes
environment.py424 linesDownload Raw Back to root
1"""FinOps RL Environment – Phase 2 Core Logic.2 3Implements FinOpsEnv with reset / step / state and full trajectory tracking.4All math follows the locked blueprint exactly, with the weekly-savings time-scale5bug fix applied (÷ 4.33).6"""7 8from __future__ import annotations9 10import copy11import random12import time13from typing import Optional14 15from schemas import (16    ActionEnvelope,17    BudgetState,18    LLMTierStats,19    ModifySaaSSeats,20    NoOp,21    Observation,22    RewardBreakdown,23    SaaSToolStats,24    StepResult,25    SwitchLLMRoutingTier,26)27 28# ── Reward constants ────────────────────────────────────────────────────────29SAVINGS_WEIGHT: float = 0.0130SLA_PENALTY: float = -75.031CHURN_PENALTY: float = -150.032INVALID_PENALTY: float = -50.033BANKRUPTCY_PENALTY: float = -500.034IDLE_PENALTY: float = -5.035 36# ── Operational constants ───────────────────────────────────────────────────37OVERHEAD_WEEKLY: float = 1_200.038MONTHLY_TO_WEEKLY: float = 4.3339SLA_THRESHOLD_MS: float = 800.040BUDGET_TOLERANCE: float = 0.0141 42 43class FinOpsEnv:44    """OpenEnv-compliant FinOps simulation environment."""45 46    def __init__(self) -> None:47        self.task: Optional[str] = None48        self.week: int = 149        self.saas_tools: list[SaaSToolStats] = []50        self.llm_tiers: list[LLMTierStats] = []51        self.budget: Optional[BudgetState] = None52        self.active_sla_breaches: int = 053        self.cumulative_savings: float = 0.054        self.episode_done: bool = False55        self.trajectory: list[dict] = []56        self._rng: Optional[random.Random] = None57 58    # ── helpers ──────────────────────────────────────────────────────────────59 60    def _compute_weekly_burn(self) -> float:61        saas = sum(t.monthly_cost_usd / MONTHLY_TO_WEEKLY for t in self.saas_tools)62        llm = sum(t.weekly_spend_usd for t in self.llm_tiers)63        return saas + llm + OVERHEAD_WEEKLY64 65    def _build_observation(self) -> Observation:66        burn = self._compute_weekly_burn()67        remaining_weeks = max(1, 52 - self.week + 1)68        remaining = self.budget.annual_budget_usd - self.budget.spent_to_date_usd69        projected = max(70            0.0,71            self.budget.spent_to_date_usd + burn * remaining_weeks - self.budget.annual_budget_usd,72        )73        self.budget.fiscal_week = self.week74        self.budget.weekly_burn_rate_usd = round(burn, 2)75        self.budget.remaining_budget_usd = round(remaining, 2)76        self.budget.projected_overrun_usd = round(projected, 2)77 78        return Observation(79            week=self.week,80            budget=self.budget.model_copy(),81            saas_tools=[t.model_copy() for t in self.saas_tools],82            llm_tiers=[t.model_copy() for t in self.llm_tiers],83            active_sla_breaches=self.active_sla_breaches,84            cumulative_savings_usd=round(self.cumulative_savings, 2),85            episode_done=self.episode_done,86        )87 88    def _count_sla_breaches(self) -> int:89        return sum(1 for t in self.llm_tiers if t.p95_latency_ms > t.sla_latency_threshold_ms)90 91    # ── reset ────────────────────────────────────────────────────────────────92 93    def reset(self, task: str) -> Observation:94        self.task = task95        self.week = 196        self.cumulative_savings = 0.097        self.active_sla_breaches = 098        self.episode_done = False99        self.trajectory = []100        self._rng = None101 102        if task == "easy":103            self._init_easy()104        elif task == "medium":105            self._init_medium()106        elif task == "hard":107            self._init_hard()108        else:109            raise ValueError(f"Unknown task: {task}")110 111        obs = self._build_observation()112        self.trajectory.append(113            {"step": 0, "action": None, "observation": obs, "reward": None, "reward_breakdown": None}114        )115        return obs116 117    # ── deterministic starting states ────────────────────────────────────────118 119    def _init_easy(self) -> None:120        self.budget = BudgetState(121            fiscal_week=1,122            annual_budget_usd=500_000.0,123            spent_to_date_usd=0.0,124            remaining_budget_usd=500_000.0,125            weekly_burn_rate_usd=0.0,126            projected_overrun_usd=0.0,127        )128        self.saas_tools = [129            SaaSToolStats(130                tool_name="linkedin_learning", total_seats=150, active_seats=50,131                inactive_seats=100, cost_per_seat_usd=30.0, monthly_cost_usd=4_500.0,132            ),133            SaaSToolStats(134                tool_name="slack", total_seats=200, active_seats=150,135                inactive_seats=50, cost_per_seat_usd=12.50, monthly_cost_usd=2_500.0,136            ),137            SaaSToolStats(138                tool_name="salesforce", total_seats=80, active_seats=80,139                inactive_seats=0, cost_per_seat_usd=75.0, monthly_cost_usd=6_000.0,140            ),141        ]142        self.llm_tiers = []143 144    def _init_medium(self) -> None:145        self.budget = BudgetState(146            fiscal_week=1,147            annual_budget_usd=200_000.0,148            spent_to_date_usd=180_000.0,149            remaining_budget_usd=20_000.0,150            weekly_burn_rate_usd=0.0,151            projected_overrun_usd=0.0,152        )153        self.saas_tools = []154        self.llm_tiers = [155            LLMTierStats(156                tier_name="premium", model_id="claude-opus-4",157                requests_this_week=50_000, cost_per_1k_tokens_usd=0.075,158                weekly_spend_usd=5_200.0, p95_latency_ms=420.0,159                sla_latency_threshold_ms=800.0,160            ),161            LLMTierStats(162                tier_name="standard", model_id="gpt-4o-mini",163                requests_this_week=30_000, cost_per_1k_tokens_usd=0.002,164                weekly_spend_usd=600.0, p95_latency_ms=210.0,165                sla_latency_threshold_ms=800.0,166            ),167            LLMTierStats(168                tier_name="opensource", model_id="llama-3-70b",169                requests_this_week=10_000, cost_per_1k_tokens_usd=0.0004,170                weekly_spend_usd=80.0, p95_latency_ms=680.0,171                sla_latency_threshold_ms=800.0,172            ),173        ]174 175    def _init_hard(self) -> None:176        self._rng = random.Random(time.time())177        self.budget = BudgetState(178            fiscal_week=1,179            annual_budget_usd=1_200_000.0,180            spent_to_date_usd=0.0,181            remaining_budget_usd=1_200_000.0,182            weekly_burn_rate_usd=0.0,183            projected_overrun_usd=0.0,184        )185        self.saas_tools = [186            SaaSToolStats(187                tool_name="salesforce", total_seats=300, active_seats=100,188                inactive_seats=200, cost_per_seat_usd=75.0, monthly_cost_usd=22_500.0,189            ),190            SaaSToolStats(191                tool_name="linkedin_learning", total_seats=1000, active_seats=500,192                inactive_seats=500, cost_per_seat_usd=30.0, monthly_cost_usd=500_000.0,193            ),194            SaaSToolStats(195                tool_name="slack", total_seats=1_000, active_seats=420,196                inactive_seats=580, cost_per_seat_usd=12.50, monthly_cost_usd=12_500.0,197            ),198            SaaSToolStats(199                tool_name="zoom", total_seats=400, active_seats=190,200                inactive_seats=210, cost_per_seat_usd=20.0, monthly_cost_usd=8_000.0,201            ),202            SaaSToolStats(203                tool_name="github", total_seats=250, active_seats=148,204                inactive_seats=102, cost_per_seat_usd=21.0, monthly_cost_usd=5_250.0,205            ),206        ]207        self.llm_tiers = [208            LLMTierStats(209                tier_name="premium", model_id="claude-opus-4",210                requests_this_week=80_000, cost_per_1k_tokens_usd=0.575,211                weekly_spend_usd=8_320.0, p95_latency_ms=430.0,212                sla_latency_threshold_ms=1000.0,213            ),214            LLMTierStats(215                tier_name="standard", model_id="gpt-4o-mini",216                requests_this_week=60_000, cost_per_1k_tokens_usd=0.042,217                weekly_spend_usd=1_200.0, p95_latency_ms=215.0,218                sla_latency_threshold_ms=1000.0,219            ),220            LLMTierStats(221                tier_name="opensource", model_id="llama-3-70b",222                requests_this_week=20_000, cost_per_1k_tokens_usd=0.0004,223                weekly_spend_usd=160.0, p95_latency_ms=690.0,224                sla_latency_threshold_ms=800.0,225            ),226        ]227 228    # ── hard-mode perturbation (deterministic via seeded RNG) ────────────────229 230    def _apply_perturbation(self) -> None:231        if self.task != "hard" or self._rng is None:232            return233        roll = self._rng.random()234        if roll < 0.30:235            tool = self._rng.choice(self.saas_tools)236            n = self._rng.randint(5, 25)237            tool.total_seats += n238            tool.inactive_seats += n239            tool.monthly_cost_usd = tool.total_seats * tool.cost_per_seat_usd240        elif roll < 0.50:241            for tier in self.llm_tiers:242                if tier.tier_name == "premium":243                    old_reqs = tier.requests_this_week244                    tier.requests_this_week += 10_000245                    if old_reqs > 0:246                        tier.weekly_spend_usd *= tier.requests_this_week / old_reqs247                    break248        elif roll < 0.60:249            per_tool = 25 // len(self.saas_tools)250            remainder = 25 % len(self.saas_tools)251            for i, tool in enumerate(self.saas_tools):252                add = per_tool + (1 if i < remainder else 0)253                tool.total_seats += add254                tool.inactive_seats += add255                tool.monthly_cost_usd = tool.total_seats * tool.cost_per_seat_usd256        # else (≥0.60): no event – 40 %257 258    # ── step ─────────────────────────────────────────────────────────────────259 260    def step(self, action: ActionEnvelope) -> StepResult:261        if self.episode_done:262            raise ValueError("Episode is done. Call reset() to start a new episode.")263 264        # 1. Hard-mode perturbation at TOP of step265        self._apply_perturbation()266 267        # 2. Snapshot pre-action burn268        pre_burn = self._compute_weekly_burn()269 270        # 3. Process action271        weekly_savings = 0.0272        sla_breaches = 0273        churn_events = 0274        invalid = False275 276        act = action.action277        if isinstance(act, ModifySaaSSeats):278            r = self._handle_modify_saas(act)279            weekly_savings = r["savings"]280            churn_events = r["churn_events"]281            invalid = r["invalid"]282        elif isinstance(act, SwitchLLMRoutingTier):283            r = self._handle_switch_llm(act)284            sla_breaches = r["sla_breaches"]285            invalid = r["invalid"]286        # NoOp → nothing287 288        # 4. Clock: deduct weekly burn (pre-action) then credit savings289        self.budget.spent_to_date_usd += pre_burn290        self.budget.spent_to_date_usd -= weekly_savings291        self.cumulative_savings += weekly_savings292        self.budget.remaining_budget_usd = (293            self.budget.annual_budget_usd - self.budget.spent_to_date_usd294        )295 296        # 5. Advance week297        self.week += 1298        bankrupt = self.budget.remaining_budget_usd <= BUDGET_TOLERANCE299        if bankrupt or self.week > 52:300            self.week = min(self.week, 52)301            self.episode_done = True302 303        # 6. Recount SLA breaches for observation304        self.active_sla_breaches = self._count_sla_breaches()305 306        # 7. Reward307        rb = self._compute_reward(308            weekly_savings=weekly_savings,309            sla_breach_count=sla_breaches,310            churn_count=churn_events,311            invalid=invalid,312            bankrupt=bankrupt,313            is_noop=isinstance(act, NoOp),314        )315 316        obs = self._build_observation()317        self.trajectory.append(318            {319                "step": len(self.trajectory),320                "action": action,321                "observation": obs,322                "reward": rb.net_reward,323                "reward_breakdown": rb,324            }325        )326        return StepResult(327            observation=obs,328            reward=rb.net_reward,329            reward_breakdown=rb,330            done=self.episode_done,331            info={"week": self.week, "cumulative_savings": round(self.cumulative_savings, 2)},332        )333 334    # ── action handlers ──────────────────────────────────────────────────────335 336    def _handle_modify_saas(self, act: ModifySaaSSeats) -> dict:337        tool = next((t for t in self.saas_tools if t.tool_name == act.tool_name), None)338        if tool is None:339            return {"savings": 0.0, "churn_events": 0, "invalid": True}340 341        new_total = tool.total_seats + act.delta_seats342        churn = 0343        if new_total < tool.active_seats:344            churn = tool.active_seats - new_total345            new_total = tool.active_seats346        if new_total < 0:347            new_total = 0348 349        seats_removed = max(0, tool.total_seats - new_total)350        # BUG-FIX: monthly cost ÷ 4.33 to convert to weekly351        weekly_savings = (seats_removed * tool.cost_per_seat_usd) / MONTHLY_TO_WEEKLY352 353        tool.total_seats = new_total354        tool.inactive_seats = max(0, tool.total_seats - tool.active_seats)355        tool.monthly_cost_usd = tool.total_seats * tool.cost_per_seat_usd356 357        return {"savings": weekly_savings, "churn_events": 1 if churn > 0 else 0, "invalid": False}358 359    def _handle_switch_llm(self, act: SwitchLLMRoutingTier) -> dict:360        from_tier = next((t for t in self.llm_tiers if t.tier_name == act.from_tier), None)361        to_tier = next((t for t in self.llm_tiers if t.tier_name == act.to_tier), None)362        if from_tier is None or to_tier is None or from_tier.tier_name == to_tier.tier_name:363            return {"sla_breaches": 0, "invalid": True}364 365        requests_to_move = int(from_tier.requests_this_week * (act.traffic_shift_pct / 100.0))366        old_from_reqs = from_tier.requests_this_week367        old_to_reqs = to_tier.requests_this_week368 369        from_tier.requests_this_week -= requests_to_move370        to_tier.requests_this_week += requests_to_move371 372        # Proportional spend update373        if old_from_reqs > 0:374            from_tier.weekly_spend_usd *= from_tier.requests_this_week / old_from_reqs375        else:376            from_tier.weekly_spend_usd = 0.0377        if old_to_reqs > 0:378            to_tier.weekly_spend_usd *= to_tier.requests_this_week / old_to_reqs379        elif to_tier.requests_this_week > 0:380            to_tier.weekly_spend_usd = (381                to_tier.requests_this_week * to_tier.cost_per_1k_tokens_usd / 1_000.0382            )383 384        # Latency pressure on destination tier385        new_to_reqs = to_tier.requests_this_week386        if new_to_reqs > 0 and requests_to_move > 0:387            latency_pressure_factor = 1.0 + (requests_to_move / new_to_reqs) * 0.4388            to_tier.p95_latency_ms *= latency_pressure_factor389 390        sla_breaches = self._count_sla_breaches()391        self.active_sla_breaches = sla_breaches392        return {"sla_breaches": sla_breaches, "invalid": False}393 394    # ── reward ───────────────────────────────────────────────────────────────395 396    def _compute_reward(397        self,398        weekly_savings: float,399        sla_breach_count: int,400        churn_count: int,401        invalid: bool,402        bankrupt: bool,403        is_noop: bool,404    ) -> RewardBreakdown:405        sav = weekly_savings * SAVINGS_WEIGHT406        sla = sla_breach_count * SLA_PENALTY407        churn = churn_count * CHURN_PENALTY408        inv = INVALID_PENALTY if invalid else 0.0409        bank = BANKRUPTCY_PENALTY if bankrupt else 0.0410        idle = IDLE_PENALTY if is_noop else 0.0411        net = sav + sla + churn + inv + bank + idle412        return RewardBreakdown(413            savings_reward=round(sav, 4),414            sla_penalty=round(sla + bank + idle, 4),415            churn_penalty=round(churn, 4),416            invalid_action_penalty=round(inv, 4),417            net_reward=round(net, 4),418        )419 420    # ── state accessor ───────────────────────────────────────────────────────421 422    def state(self) -> Observation:423        return self._build_observation()424