Spaces:
Runtime error
Runtime error
File size: 9,177 Bytes
a663682 8961dba a663682 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | """
World Model — Hidden dynamics engine for the Cashflow Multi-Agent Environment.
Manages probabilistic events that are NOT directly visible to agents:
- Future inflow uncertainty (customer payment delays)
- Cash shocks (equipment failure, tax audit, fraud)
- Vendor behavior shifts (trust decay/growth)
- Market conditions (interest rate changes)
The world model is updated AFTER every environment step (Step 8 in workflow).
"""
import random
from typing import List, Dict, Any, Optional
from uuid import uuid4
class WorldEvent:
"""A single hidden event that may trigger on a specific day."""
def __init__(self, day: int, event_type: str, severity: float,
description: str, target_id: Optional[str] = None,
amount: float = 0.0, probability: float = 1.0):
self.day = day
self.event_type = event_type # cash_shock, payment_delay, vendor_shift, revenue_miss, fraud
self.severity = severity # 0.0 to 1.0
self.description = description
self.target_id = target_id # affected invoice/receivable/vendor ID
self.amount = amount
self.probability = probability
self.triggered = False
class WorldModel:
"""
Hidden state tracker that evolves probabilistically each day.
The environment calls:
- initialize() on reset
- update(day) after each step to check for triggered events
Agents get PARTIAL views — they don't see the full event list.
"""
def __init__(self):
self.events: List[WorldEvent] = []
self.triggered_log: List[Dict[str, Any]] = []
self.market_stress: float = 0.0 # 0.0 = calm, 1.0 = crisis
self.vendor_mood: Dict[str, float] = {} # vendor_id -> mood modifier
self.day_effects: Dict[str, Any] = {} # per-day cache of effects
def initialize(self, scenario: Dict[str, Any], max_days: int = 10):
"""Generate hidden events from scenario data."""
self.events = []
self.triggered_log = []
self.market_stress = random.uniform(0.0, 0.3)
self.vendor_mood = {}
self.day_effects = {}
vendors = scenario.get("vendors", [])
for v in vendors:
self.vendor_mood[v["id"]] = 0.0
# --- Generate Cash Shocks ---
num_shocks = random.randint(4, 7)
shock_types = [
("Equipment Failure", -200000, -400000),
("Tax Audit Penalty", -150000, -300000),
("Emergency Repair", -80000, -200000),
("Regulatory Fine", -100000, -250000),
("Supplier Price Hike", -50000, -150000),
]
for _ in range(num_shocks):
shock = random.choice(shock_types)
self.events.append(WorldEvent(
day=random.randint(1, max_days - 1),
event_type="cash_shock",
severity=random.uniform(0.6, 1.0),
description=shock[0],
amount=random.uniform(shock[1], shock[2]),
probability=random.uniform(0.6, 0.95),
))
# --- Generate Payment Delays ---
receivables = scenario.get("initial_receivables", [])
for rec in receivables:
if random.random() < 0.7: # 70% chance any receivable gets delayed
self.events.append(WorldEvent(
day=random.randint(1, max_days),
event_type="payment_delay",
severity=random.uniform(0.5, 0.9),
description=f"Customer {rec['customer_id']} payment delayed",
target_id=rec["id"],
amount=random.randint(2, 5), # delay in days
probability=random.uniform(0.7, 1.0),
))
# --- Generate Revenue Miss ---
if random.random() < 0.6:
self.events.append(WorldEvent(
day=random.randint(1, max_days),
event_type="revenue_miss",
severity=random.uniform(0.6, 0.9),
description="Quarterly revenue target missed — board review triggered",
probability=random.uniform(0.7, 0.9),
))
# --- Generate Vendor Mood Shifts ---
for v in vendors:
if random.random() < 0.6:
self.events.append(WorldEvent(
day=random.randint(1, max_days),
event_type="vendor_shift",
severity=random.uniform(0.4, 0.8),
description=f"Vendor {v['name']} mood shift",
target_id=v["id"],
amount=random.uniform(-0.3, -0.1), # negative trust modifier
probability=random.uniform(0.7, 0.9),
))
# --- Fraud anomaly (rare) ---
if random.random() < 0.4:
self.events.append(WorldEvent(
day=random.randint(1, max_days),
event_type="fraud",
severity=0.9,
description="Suspicious transaction detected — investigation required",
amount=-random.uniform(200000, 500000),
probability=0.7,
))
def update(self, day: int) -> Dict[str, Any]:
"""
Check and trigger events for the given day.
Returns a dict of effects to apply to the environment.
"""
effects = {
"cash_delta": 0.0,
"payment_delays": [], # list of (receivable_id, extra_days)
"vendor_trust_deltas": {}, # vendor_id -> trust_delta
"shock_occurred": False,
"shock_description": None,
"fraud_alert": False,
"revenue_miss": False,
"events_triggered": [],
}
for event in self.events:
if event.day == day and not event.triggered:
# Roll the dice
if random.random() < event.probability:
event.triggered = True
if event.event_type == "cash_shock":
effects["cash_delta"] += event.amount
effects["shock_occurred"] = True
effects["shock_description"] = event.description
elif event.event_type == "payment_delay":
effects["payment_delays"].append(
(event.target_id, int(event.amount))
)
elif event.event_type == "vendor_shift":
vid = event.target_id
effects["vendor_trust_deltas"][vid] = event.amount
self.vendor_mood[vid] = self.vendor_mood.get(vid, 0) + event.amount
elif event.event_type == "revenue_miss":
effects["revenue_miss"] = True
self.market_stress = min(1.0, self.market_stress + 0.2)
elif event.event_type == "fraud":
effects["fraud_alert"] = True
effects["cash_delta"] += event.amount
effects["events_triggered"].append({
"type": event.event_type,
"description": event.description,
"severity": event.severity,
})
self.triggered_log.append({
"day": day,
"type": event.event_type,
"description": event.description,
"amount": event.amount,
})
# Market stress naturally decays
self.market_stress = max(0.0, self.market_stress - 0.02)
self.day_effects[day] = effects
return effects
def get_risk_hints(self, day: int) -> Dict[str, Any]:
"""
Partial information for the Risk Agent.
Reveals SOME upcoming threats but not exact amounts/days.
"""
hints = {
"market_stress": round(self.market_stress, 2),
"upcoming_risk_level": "low",
"vendor_sentiment": {},
}
# Give a vague warning about upcoming shocks (within 2 days)
upcoming_threats = 0
for event in self.events:
if not event.triggered and abs(event.day - day) <= 2:
if event.event_type in ("cash_shock", "fraud", "payment_delay", "revenue_miss"):
upcoming_threats += 1
if upcoming_threats >= 2:
hints["upcoming_risk_level"] = "critical"
elif upcoming_threats == 1:
hints["upcoming_risk_level"] = "elevated"
# Vendor sentiment (partial view)
for vid, mood in self.vendor_mood.items():
if mood < -0.1:
hints["vendor_sentiment"][vid] = "negative"
elif mood > 0.05:
hints["vendor_sentiment"][vid] = "positive"
else:
hints["vendor_sentiment"][vid] = "neutral"
return hints
def get_triggered_events(self) -> List[Dict]:
"""Full log of all triggered events (for grading/logging)."""
return self.triggered_log
|