| """SimMart environment — multi-agent tier-2 Indian retail simulation (OpenEnv). |
| |
| 1 step = 1 week (7 daily ticks inside). 1 episode = 13 weeks = 90 days = 1 quarter. |
| |
| Flow per step(action): |
| 1. Score this week's decisions against rogue tells (mark caught rogues) |
| 2. Execute approved proposals (mutate ledger; buffer next-week multipliers) |
| 3. Consume pending weekly effects (revenue/margin/NPS multipliers) |
| 4. Tick 7 days: |
| • competitor events (once at week start) |
| • for each day: crisis activation → demand/supply/SLA → daily ledger tick |
| 5. Aggregate weekly KPIs, update NPS/basket/footfall/repeat |
| 6. Compute weekly reward (grader.weekly_reward) |
| 7. Record in state.history + cache next-week's inbox, crises, etc. |
| 8. Return the next-week's SimMartObservation |
| |
| At reset: |
| • Seed RNG |
| • Create initial ledger |
| • Sample per-dept drifts (base ± jitter) |
| • Schedule crises + rogues for the episode |
| • Generate week-1 inbox |
| • Return week-1 SimMartObservation |
| """ |
|
|
| from __future__ import annotations |
|
|
| import random |
| import uuid |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| from openenv.core.env_server import Environment |
|
|
| try: |
| from ..models import ( |
| CompanyLedger, |
| CompetitorEvent, |
| Complaint, |
| CrisisEvent, |
| ExecutiveDiligenceFinding, |
| ExecutiveDiligenceRequest, |
| KPISnapshot, |
| PnLSnapshot, |
| Proposal, |
| ProposalDecision, |
| RogueIncident, |
| SimMartAction, |
| SimMartObservation, |
| SimMartState, |
| WeeklyDecision, |
| ) |
| from . import crises as CR |
| from . import demand as DMD |
| from . import departments as DEP |
| from . import economics as E |
| from . import grader as GR |
| from . import ledger as LD |
| from . import proposals as PROP |
| from . import rogue as RG |
| except (ImportError, ModuleNotFoundError): |
| from models import ( |
| CompanyLedger, |
| CompetitorEvent, |
| Complaint, |
| CrisisEvent, |
| ExecutiveDiligenceFinding, |
| ExecutiveDiligenceRequest, |
| KPISnapshot, |
| PnLSnapshot, |
| Proposal, |
| ProposalDecision, |
| RogueIncident, |
| SimMartAction, |
| SimMartObservation, |
| SimMartState, |
| WeeklyDecision, |
| ) |
| from server import crises as CR |
| from server import demand as DMD |
| from server import departments as DEP |
| from server import economics as E |
| from server import grader as GR |
| from server import ledger as LD |
| from server import proposals as PROP |
| from server import rogue as RG |
|
|
|
|
| class SimMartEnvironment( |
| Environment[SimMartAction, SimMartObservation, SimMartState] |
| ): |
| SUPPORTS_CONCURRENT_SESSIONS = True |
| MAX_WEEKS: int = E.WEEKS_PER_QUARTER |
| DAYS_PER_QUARTER: int = E.DAYS_PER_QUARTER |
|
|
| def __init__(self): |
| super().__init__() |
| self._rng: random.Random = random.Random(0) |
| self._rng_seed: int = 0 |
| self._state: SimMartState = SimMartState() |
| self._episode_index: int = 0 |
| self._min_cash_reached: float = 0.0 |
|
|
| |
| self._competitor_events_window: List[CompetitorEvent] = [] |
| self._pending_complaints: List[Complaint] = [] |
| self._last_journal_entry: str = "" |
| self._last_kpi_snapshot: Optional[KPISnapshot] = None |
| self._current_inbox: List[Proposal] = [] |
| self._current_active_crises: List[CrisisEvent] = [] |
| self._pending_diligence_findings: List[ExecutiveDiligenceFinding] = [] |
| self._schema_hash_cache: str = PROP.schema_hash() |
|
|
| |
| |
| |
|
|
| def reset( |
| self, |
| seed: Optional[int] = None, |
| episode_id: Optional[str] = None, |
| **kwargs: Any, |
| ) -> SimMartObservation: |
| self._rng_seed = int(seed) if seed is not None else random.randint(0, 2**31 - 1) |
| self._rng = random.Random(self._rng_seed) |
|
|
| |
| self._episode_index = int(kwargs.get("episode_index", self._episode_index + 1)) |
| phase = E.curriculum_for_episode(self._episode_index) |
|
|
| |
| ledger = LD.create_initial_ledger(self._rng) |
|
|
| |
| drifts: Dict[str, float] = {} |
| for dept, base in E.DEPT_BASE_DRIFT.items(): |
| drifts[dept] = max( |
| 0.0, |
| min(1.0, base + self._rng.uniform(-E.DEPT_DRIFT_JITTER, E.DEPT_DRIFT_JITTER)), |
| ) |
|
|
| |
| crisis_queue = CR.schedule_crises( |
| self._rng, |
| crisis_prob=phase["crisis_prob_per_ep"], |
| dept_drifts=drifts, |
| cities=ledger.cities, |
| ) |
| rogues = RG.schedule_rogues( |
| self._rng, |
| rogue_prob=phase["rogue_prob_per_ep"], |
| dept_drifts=drifts, |
| cities=ledger.cities, |
| ) |
|
|
| |
| self._state = SimMartState( |
| episode_id=episode_id or str(uuid.uuid4()), |
| day=0, |
| week=0, |
| rng_seed=self._rng_seed, |
| company=ledger, |
| dept_drifts=drifts, |
| crisis_queue=crisis_queue, |
| rogue_incidents=rogues, |
| history=[], |
| pending_diligence_findings=[], |
| ) |
|
|
| |
| self._competitor_events_window = [] |
| self._pending_complaints = [] |
| self._last_journal_entry = "" |
| self._pending_diligence_findings = [] |
| self._last_kpi_snapshot = KPISnapshot( |
| revenue_inr=E.BASELINE_WEEKLY_REVENUE_INR, |
| gross_margin_pct=E.STARTING_BLENDED_MARGIN_PCT, |
| stockout_rate_pct=E.STARTING_STOCKOUT_PCT, |
| nps=E.STARTING_NPS, |
| cash_inr=ledger.cash_inr, |
| shrinkage_pct=E.STARTING_SHRINKAGE_PCT, |
| delivery_sla_hit_rate_pct=E.STARTING_SLA_HIT_RATE_PCT, |
| basket_size_inr=E.STARTING_BASKET_SIZE_INR, |
| footfall_per_store=E.STARTING_FOOTFALL_PER_STORE, |
| repeat_purchase_rate_pct=E.STARTING_REPEAT_PURCHASE_PCT, |
| ) |
| ledger.kpi_history.append(self._last_kpi_snapshot) |
| self._min_cash_reached = ledger.cash_inr |
| self._schema_hash_cache = PROP.schema_hash() |
|
|
| |
| self._state.week = 1 |
| self._state.day = 0 |
| inbox = self._generate_weekly_inbox(week=1) |
| self._current_inbox = inbox |
| self._current_active_crises = [] |
|
|
| return self._build_observation( |
| step_type="weekly_decision", |
| week=1, |
| inbox=inbox, |
| reward=None, |
| done=False, |
| message=self._narrative_for_week(1, crisis_queue, rogues), |
| ) |
|
|
| |
| |
| |
|
|
| def step( |
| self, |
| action: SimMartAction, |
| timeout_s: Optional[float] = None, |
| **kwargs: Any, |
| ) -> SimMartObservation: |
| ledger = self._state.company |
| prev_week = self._state.week |
| current_inbox = list(self._current_inbox) |
|
|
| |
| rogue_metrics = RG.mark_caught( |
| self._state.rogue_incidents, |
| prev_week, |
| action.decisions, |
| current_inbox, |
| ) |
|
|
| |
| exec_tel = LD.execute_approved_proposals( |
| ledger, current_inbox, action.decisions, self._rng, |
| ) |
|
|
| |
| |
| |
| diligence_tel = self._process_diligence_requests( |
| action.diligence_requests, |
| current_inbox, |
| prev_week, |
| ) |
| if diligence_tel["cost_inr"] > 0: |
| ledger.cash_inr -= diligence_tel["cost_inr"] |
| ledger.pnl_qtd.opex_qtd_inr += diligence_tel["cost_inr"] |
|
|
| |
| pending = LD.consume_pending_effects(ledger) |
| pending_rev_mult = pending["revenue_mult"] |
| pending_margin_delta = pending["margin_delta_pts"] |
| pending_nps_delta = pending["nps_delta"] |
| pending_sla_delta = pending["sla_delta_pts"] |
|
|
| |
| daily_tel_list: List[Dict[str, Any]] = [] |
|
|
| |
| new_comp = DMD.competitor_weekly_events(ledger, prev_week, self._rng) |
| self._competitor_events_window.extend(new_comp) |
| |
| self._competitor_events_window = [ |
| c for c in self._competitor_events_window if c.week >= prev_week - 3 |
| ] |
|
|
| week_start_day = (prev_week - 1) * 7 + 1 |
| for offset in range(7): |
| d = week_start_day + offset |
| if d > self.DAYS_PER_QUARTER: |
| break |
| |
| firing, expired = CR.tick_crisis_active(self._state.crisis_queue, d) |
| active = CR.active_crises_now(self._state.crisis_queue) |
| effects = CR.crisis_effects_today(active) |
|
|
| |
| for c in firing: |
| cash_bump = float((c.affected or {}).get("cash_bump_inr", 0.0)) |
| if cash_bump != 0.0: |
| ledger.cash_inr += cash_bump |
|
|
| |
| share_drain = DMD.active_share_drain_pct(self._competitor_events_window, prev_week) |
| |
| share_drain = min(15.0, share_drain + float(effects.get("share_drain_bump_pct", 0.0))) |
|
|
| cat_demand = DMD.customer_daily_demand( |
| ledger=ledger, |
| day_of_quarter=d, |
| nps=self._last_kpi_snapshot.nps, |
| share_drain_pct=share_drain, |
| active_crises=active, |
| rng=self._rng, |
| pending_revenue_mult=pending_rev_mult, |
| ) |
| sla_hit = DMD.rider_daily_sla_hit_rate(d, active, self._rng) |
|
|
| tel = LD.tick_one_day( |
| ledger=ledger, |
| day_of_quarter=d, |
| category_demand_units=cat_demand, |
| sla_hit_rate_pct=sla_hit, |
| crisis_extra_opex_inr=float(effects.get("opex_bump_inr", 0.0)), |
| rng=self._rng, |
| ) |
| daily_tel_list.append(tel) |
| self._min_cash_reached = min(self._min_cash_reached, ledger.cash_inr) |
| self._state.day = d |
|
|
| |
| weekly_revenue = sum(t["revenue_inr"] for t in daily_tel_list) |
| weekly_cogs = sum(t["cogs_inr"] for t in daily_tel_list) |
| weekly_opex = sum(t["opex_inr"] for t in daily_tel_list) |
| weekly_sla = ( |
| sum(t["sla_hit_rate_pct"] for t in daily_tel_list) / max(1, len(daily_tel_list)) |
| ) |
| weekly_stockout = ( |
| sum(t["stockout_rate_pct"] for t in daily_tel_list) / max(1, len(daily_tel_list)) |
| ) |
| weekly_shrinkage_value = sum(t["shrinkage_value_inr"] for t in daily_tel_list) |
| weekly_shrinkage_pct = ( |
| weekly_shrinkage_value / max(1.0, weekly_revenue) * 100.0 |
| if weekly_revenue > 0 |
| else E.STARTING_SHRINKAGE_PCT |
| ) |
|
|
| prev_nps = self._last_kpi_snapshot.nps |
| high_sev_complaints = sum(1 for c in self._pending_complaints if c.severity == "high") |
| new_nps = DMD.update_weekly_nps( |
| prev_nps=prev_nps, |
| stockout_rate_pct=weekly_stockout, |
| sla_hit_rate_pct=weekly_sla, |
| pending_nps_delta=pending_nps_delta, |
| high_severity_complaints=high_sev_complaints, |
| rng=self._rng, |
| ) |
| festival_weight = DMD.festival_weight_for_week(prev_week) |
| new_basket = DMD.update_weekly_basket_size( |
| self._last_kpi_snapshot.basket_size_inr, |
| weekly_stockout, |
| festival_weight, |
| self._rng, |
| ) |
| new_footfall = DMD.update_weekly_footfall( |
| self._last_kpi_snapshot.footfall_per_store, |
| DMD.active_share_drain_pct(self._competitor_events_window, prev_week), |
| festival_weight, |
| weekly_stockout, |
| self._rng, |
| ) |
| new_repeat = DMD.update_weekly_repeat_purchase( |
| self._last_kpi_snapshot.repeat_purchase_rate_pct, |
| new_nps, |
| pending_loyalty_boost=0.0, |
| rng=self._rng, |
| ) |
|
|
| snap = LD.snapshot_weekly_kpis( |
| ledger=ledger, |
| weekly_revenue=weekly_revenue, |
| weekly_cogs=weekly_cogs, |
| weekly_stockout_rate_pct=weekly_stockout, |
| weekly_shrinkage_pct=weekly_shrinkage_pct, |
| weekly_sla_hit_rate_pct=max(45.0, min(99.0, weekly_sla + pending_sla_delta)), |
| weekly_nps=new_nps, |
| weekly_basket_inr=new_basket, |
| weekly_footfall_per_store=new_footfall, |
| weekly_repeat_purchase_pct=new_repeat, |
| ) |
| |
| snap.margin_delta_pts = snap.margin_delta_pts + pending_margin_delta |
| self._last_kpi_snapshot = snap |
|
|
| |
| weekly_r, components = GR.weekly_reward( |
| kpi_snapshot=snap, |
| decisions=action.decisions, |
| inbox=current_inbox, |
| rogue_metrics=rogue_metrics, |
| journal_entry=action.journal_entry, |
| prev_journal_entry=self._last_journal_entry, |
| ) |
| self._last_journal_entry = action.journal_entry |
|
|
| |
| self._state.history.append(WeeklyDecision( |
| week=prev_week, |
| decisions=action.decisions, |
| budget_allocations=action.budget_allocations, |
| diligence_requests=action.diligence_requests, |
| diligence_findings=diligence_tel["findings"], |
| journal_entry=action.journal_entry, |
| weekly_reward=weekly_r, |
| reward_components={k: v for k, v in components.items() if k.startswith("weighted.") or k == "total"}, |
| kpi_snapshot=snap, |
| rogues_active=[r.rogue_id for r in RG.active_this_week(self._state.rogue_incidents, prev_week)], |
| rogues_caught=[r.rogue_id for r in self._state.rogue_incidents if r.caught and prev_week in r.active_weeks], |
| )) |
|
|
| |
| stockout_by_cat = {"aggregate": weekly_stockout} |
| self._pending_complaints = DMD.franchisee_weekly_complaints( |
| ledger=ledger, |
| week_of_quarter=prev_week + 1, |
| stockout_rate_by_category=stockout_by_cat, |
| sla_hit_rate_pct=weekly_sla, |
| rng=self._rng, |
| ) |
|
|
| |
| next_week = prev_week + 1 |
| done = next_week > self.MAX_WEEKS |
| self._state.week = next_week if not done else prev_week |
| self._state.step_count = prev_week |
|
|
| if done: |
| |
| term_r, term_components = GR.terminal_reward(ledger, self._min_cash_reached) |
| total_reward = weekly_r + term_r |
| self._current_inbox = [] |
| self._current_active_crises = CR.active_crises_now(self._state.crisis_queue) |
| return self._build_observation( |
| step_type="quarterly_close", |
| week=prev_week, |
| inbox=[], |
| reward=total_reward, |
| done=True, |
| message=self._terminal_narrative(ledger, term_components, rogue_metrics), |
| ) |
|
|
| |
| inbox_next = self._generate_weekly_inbox(next_week) |
| self._current_inbox = inbox_next |
| self._current_active_crises = CR.active_crises_now(self._state.crisis_queue) |
|
|
| return self._build_observation( |
| step_type="weekly_decision", |
| week=next_week, |
| inbox=inbox_next, |
| reward=weekly_r, |
| done=False, |
| message=self._narrative_for_week(next_week, self._state.crisis_queue, self._state.rogue_incidents), |
| ) |
|
|
| |
| |
| |
|
|
| @property |
| def state(self) -> SimMartState: |
| return self._state |
|
|
| def close(self) -> None: |
| pass |
|
|
| |
| |
| |
|
|
| def _generate_weekly_inbox(self, week: int) -> List[Proposal]: |
| active_crises = CR.active_crises_now(self._state.crisis_queue) |
| base = DEP.generate_weekly_proposals( |
| ledger=self._state.company, |
| active_crises=active_crises, |
| week=week, |
| dept_drifts=self._state.dept_drifts, |
| rng=self._rng, |
| crisis_queue=self._state.crisis_queue, |
| ) |
| rogues_now = RG.active_this_week(self._state.rogue_incidents, week) |
| overlaid = RG.inject_rogue_proposals( |
| base_proposals=base, |
| active_rogues=rogues_now, |
| week=week, |
| ledger=self._state.company, |
| rng=self._rng, |
| ) |
| return overlaid |
|
|
| |
| |
| |
|
|
| def _process_diligence_requests( |
| self, |
| requests: List[ExecutiveDiligenceRequest], |
| inbox: List[Proposal], |
| week: int, |
| ) -> Dict[str, Any]: |
| budget = E.EXECUTIVE_DILIGENCE_REQUESTS_PER_WEEK |
| cost_per = E.EXECUTIVE_DILIGENCE_COST_INR |
| findings: List[ExecutiveDiligenceFinding] = [] |
| inbox_by_id = {p.proposal_id: p for p in inbox} |
| active_rogues = RG.active_this_week(self._state.rogue_incidents, week) |
| rogue_pids = { |
| pid |
| for rogue in active_rogues |
| for pid in rogue.associated_proposal_ids |
| } |
|
|
| for idx, req in enumerate(requests): |
| request_id = req.request_id or f"DIL-W{week:02d}-{idx + 1}" |
| if idx >= budget: |
| findings.append(ExecutiveDiligenceFinding( |
| request_id=request_id, |
| request_type=req.request_type, |
| proposal_id=req.proposal_id, |
| dept=req.dept, |
| status="capacity_exceeded", |
| risk_level="med", |
| summary=( |
| "Escalation not completed: CEO diligence bandwidth was already " |
| f"used for {budget} request(s) this week." |
| ), |
| suggested_action="Prioritize the riskiest proposals for diligence next week.", |
| cost_inr=0.0, |
| )) |
| continue |
|
|
| finding = self._build_diligence_finding( |
| req=req, |
| request_id=request_id, |
| proposal=inbox_by_id.get(req.proposal_id), |
| rogue_pids=rogue_pids, |
| cost_inr=cost_per, |
| ) |
| findings.append(finding) |
|
|
| completed_cost = sum(f.cost_inr for f in findings if f.status == "completed") |
| self._pending_diligence_findings = findings |
| self._state.pending_diligence_findings = findings |
| return {"findings": findings, "cost_inr": completed_cost} |
|
|
| def _build_diligence_finding( |
| self, |
| req: ExecutiveDiligenceRequest, |
| request_id: str, |
| proposal: Optional[Proposal], |
| rogue_pids: set, |
| cost_inr: float, |
| ) -> ExecutiveDiligenceFinding: |
| ledger = self._state.company |
|
|
| if req.request_type in {"cashflow_stress_test", "cfo_variance_note"} and not req.proposal_id: |
| cash_cr = ledger.cash_inr / 1e7 |
| loc_used_pct = ( |
| ledger.line_of_credit_drawn / max(1.0, ledger.line_of_credit_limit) * 100.0 |
| ) |
| risk = "high" if ledger.cash_inr < 0.25 * E.STARTING_CASH_INR else "med" if ledger.cash_inr < 0.5 * E.STARTING_CASH_INR else "low" |
| return ExecutiveDiligenceFinding( |
| request_id=request_id, |
| request_type=req.request_type, |
| proposal_id="", |
| dept=req.dept or "finance", |
| status="completed", |
| risk_level=risk, |
| summary=( |
| f"Finance escalation complete: cash is ₹{cash_cr:+.2f} Cr, " |
| f"LoC utilization is {loc_used_pct:.0f}%, and QTD EBITDA margin is " |
| f"{ledger.pnl_qtd.ebitda_margin_pct:+.1f}%." |
| ), |
| evidence={ |
| "cash_inr": ledger.cash_inr, |
| "line_of_credit_drawn": ledger.line_of_credit_drawn, |
| "ebitda_margin_pct": ledger.pnl_qtd.ebitda_margin_pct, |
| }, |
| suggested_action="Preserve cash buffer before approving discretionary growth or capex proposals.", |
| cost_inr=cost_inr, |
| ) |
|
|
| if proposal is None: |
| return ExecutiveDiligenceFinding( |
| request_id=request_id, |
| request_type=req.request_type, |
| proposal_id=req.proposal_id, |
| dept=req.dept, |
| status="invalid_request", |
| risk_level="low", |
| summary="Escalation could not be completed because the proposal id was not in this week's CEO inbox.", |
| suggested_action="Use proposal IDs exactly as shown in the weekly inbox.", |
| cost_inr=0.0, |
| ) |
|
|
| risk_level, evidence, suggested = self._proposal_diligence_risk(proposal, proposal.proposal_id in rogue_pids) |
| summary = ( |
| f"{req.request_type} completed for {proposal.proposal_id} " |
| f"({proposal.dept}.{proposal.action}). Risk assessed as {risk_level}." |
| ) |
| if risk_level == "high": |
| summary += " Escalation found evidence that merits flagging or rejecting similar future proposals." |
| elif risk_level == "med": |
| summary += " Escalation found some pressure points; approve only with tighter controls." |
| else: |
| summary += " Escalation did not find a material control issue." |
|
|
| return ExecutiveDiligenceFinding( |
| request_id=request_id, |
| request_type=req.request_type, |
| proposal_id=proposal.proposal_id, |
| dept=proposal.dept, |
| status="completed", |
| risk_level=risk_level, |
| summary=summary, |
| evidence=evidence, |
| suggested_action=suggested, |
| cost_inr=cost_inr, |
| ) |
|
|
| def _proposal_diligence_risk( |
| self, |
| proposal: Proposal, |
| is_active_rogue: bool, |
| ) -> Tuple[str, Dict[str, Any], str]: |
| params = proposal.params or {} |
| evidence: Dict[str, Any] = { |
| "cost_inr": proposal.cost_inr, |
| "urgency": proposal.urgency, |
| } |
| if is_active_rogue: |
| evidence["rogue_pattern_detected"] = True |
| return ( |
| "high", |
| evidence, |
| "Flag or reject similar proposals and require finance/audit sign-off before future approval.", |
| ) |
|
|
| suspicious_vendor = str(params.get("vendor_id", "")).startswith("V-SUSPICIOUS-") |
| unit_cost = float(params.get("unit_cost", 0.0) or 0.0) |
| sku_id = params.get("sku_id") |
| sku_cost = 0.0 |
| if sku_id in self._state.company.sku_catalogue: |
| sku_cost = float(self._state.company.sku_catalogue[sku_id]["cost_inr"]) |
| cost_uplift = (unit_cost / sku_cost - 1.0) if sku_cost and unit_cost else 0.0 |
| qty = float(params.get("qty", 0.0) or 0.0) |
| baseline_qty = float(params.get("inventory_baseline_qty", 0.0) or 0.0) |
| qty_multiple = (qty / baseline_qty) if baseline_qty > 0 else 0.0 |
|
|
| evidence.update({ |
| "suspicious_vendor": suspicious_vendor, |
| "unit_cost_uplift_pct": round(cost_uplift * 100.0, 1), |
| "qty_multiple_vs_baseline": round(qty_multiple, 2) if qty_multiple else 0.0, |
| }) |
|
|
| if suspicious_vendor or cost_uplift > 0.15 or qty_multiple > 2.0: |
| return ( |
| "high", |
| evidence, |
| "Escalate to audit before approving future proposals with the same vendor, SKU, or cost pattern.", |
| ) |
| if proposal.urgency == "high" or abs(proposal.cost_inr) > 2e6: |
| return ( |
| "med", |
| evidence, |
| "Approve only if the department can tie the spend to stockout, SLA, or cash protection.", |
| ) |
| return ( |
| "low", |
| evidence, |
| "No special follow-up needed beyond normal weekly KPI review.", |
| ) |
|
|
| |
| |
| |
|
|
| def _build_observation( |
| self, |
| step_type: str, |
| week: int, |
| inbox: List[Proposal], |
| reward: Optional[float], |
| done: bool, |
| message: str, |
| ) -> SimMartObservation: |
| active = CR.active_crises_now(self._state.crisis_queue) |
| return SimMartObservation( |
| done=done, |
| reward=reward, |
| step_type=step_type, |
| day_of_quarter=self._state.day, |
| week_of_quarter=week, |
| kpi_snapshot=self._last_kpi_snapshot or KPISnapshot(), |
| pnl_snapshot=self._state.company.pnl_qtd, |
| inbox=inbox, |
| active_crises=active, |
| franchise_complaints=list(self._pending_complaints), |
| competitor_events=list(self._competitor_events_window), |
| executive_diligence_findings=list(self._pending_diligence_findings), |
| diligence_budget_remaining=E.EXECUTIVE_DILIGENCE_REQUESTS_PER_WEEK, |
| schema_hash=self._schema_hash_cache, |
| last_journal=self._last_journal_entry, |
| task_description=self._task_description(week, active), |
| message=message, |
| output=message, |
| ) |
|
|
| |
| |
| |
|
|
| def _task_description(self, week: int, active: List[CrisisEvent]) -> str: |
| head = f"Week {week}/{self.MAX_WEEKS} of SimMart's festive quarter in tier-2 India." |
| if active: |
| crisis_names = ", ".join(f"{c.crisis_id} {c.name}" for c in active) |
| return f"{head} Currently active: {crisis_names}. Review the inbox and decide." |
| return f"{head} Review the inbox, decide per proposal, allocate budget, log the journal." |
|
|
| def _narrative_for_week( |
| self, |
| week: int, |
| crisis_queue: List[CrisisEvent], |
| rogues: List[RogueIncident], |
| ) -> str: |
| upcoming = [ |
| c for c in crisis_queue |
| if c.started_day > self._state.day |
| and c.started_day <= self._state.day + 14 |
| and not c.active |
| ] |
| bits = [f"Week {week} begins."] |
| if upcoming: |
| bits.append( |
| "On the horizon: " + ", ".join(f"{c.name} (~day {c.started_day})" for c in upcoming[:2]) |
| + "." |
| ) |
| return " ".join(bits) |
|
|
| def _terminal_narrative( |
| self, |
| ledger: CompanyLedger, |
| term_components: Dict[str, float], |
| rogue_metrics: Dict[str, Any], |
| ) -> str: |
| pnl = ledger.pnl_qtd |
| caught_meta = RG.episode_accuracy(self._state.rogue_incidents) |
| return ( |
| f"Quarter closed. Revenue ₹{pnl.revenue_qtd_inr/1e7:.2f} Cr, " |
| f"EBITDA ₹{pnl.ebitda_qtd_inr/1e7:+.2f} Cr ({pnl.ebitda_margin_pct:+.1f}%), " |
| f"final cash ₹{ledger.cash_inr/1e7:+.2f} Cr, " |
| f"min cash reached ₹{self._min_cash_reached/1e7:+.2f} Cr. " |
| f"Rogue catches: {caught_meta['caught']}/{caught_meta['total_rogues']} " |
| f"(recall {caught_meta['recall']:.0%})." |
| ) |
|
|