Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -104,7 +104,7 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
|
| 104 |
if not client: return 8, "NO_API_KEY_DEFAULT"
|
| 105 |
with st.spinner(f"Getting AI decision for {echelon_name}..."):
|
| 106 |
try:
|
| 107 |
-
temp = 0.1 if 'rational' in prompt else 0.7
|
| 108 |
response = client.chat.completions.create(
|
| 109 |
model=OPENAI_MODEL,
|
| 110 |
messages=[
|
|
@@ -122,13 +122,12 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
|
|
| 122 |
st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 4.")
|
| 123 |
return 4, f"API_ERROR: {e}"
|
| 124 |
|
| 125 |
-
# =============== PROMPT FUNCTION (
|
| 126 |
def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_decision_point: dict) -> str:
|
| 127 |
# This function's logic is updated for "human_like" to follow a flawed Sterman heuristic.
|
| 128 |
e_state = echelon_state_decision_point
|
| 129 |
base_info = f"Your Current Status at the **{e_state['name']}** for **Week {week}** (Before Shipping):\n- On-hand inventory: {e_state['inventory']} units.\n- Backlog (total unfilled orders): {e_state['backlog']} units.\n- Incoming order this week (just received): {e_state['incoming_order']} units.\n"
|
| 130 |
|
| 131 |
-
# --- PROMPT FIX: Get correct demand (current, not future) ---
|
| 132 |
current_stable_demand = get_customer_demand(week) # Use current week's demand
|
| 133 |
|
| 134 |
if e_state['name'] == 'Factory':
|
|
@@ -141,60 +140,97 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
|
|
| 141 |
# --- PERFECT RATIONAL (NORMATIVE) PROMPTS ---
|
| 142 |
|
| 143 |
if llm_personality == 'perfect_rational' and info_sharing == 'full':
|
| 144 |
-
stable_demand = current_stable_demand
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
safety_stock = 4
|
| 151 |
target_inventory_level = (stable_demand * total_lead_time) + safety_stock
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
if e_state['name'] == 'Factory':
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
optimal_order = max(0, int(target_inventory_level - inventory_position))
|
| 160 |
return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
|
| 161 |
|
| 162 |
elif llm_personality == 'perfect_rational' and info_sharing == 'local':
|
| 163 |
-
safety_stock = 4
|
|
|
|
| 164 |
inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
|
|
|
|
|
|
|
| 165 |
if e_state['name'] == 'Factory':
|
|
|
|
| 166 |
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 167 |
supply_line_desc = "In Production"
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
calculated_order = anchor_demand + inventory_correction - supply_line
|
| 173 |
rational_local_order = max(0, int(calculated_order))
|
| 174 |
-
|
|
|
|
| 175 |
|
| 176 |
# --- HUMAN-LIKE (DESCRIPTIVE) PROMPTS ---
|
| 177 |
|
| 178 |
-
else:
|
| 179 |
-
|
| 180 |
-
# This is the flawed Sterman heuristic
|
| 181 |
DESIRED_INVENTORY = 12 # Matches initial inventory
|
| 182 |
-
anchor_demand = e_state['incoming_order']
|
| 183 |
net_inventory = e_state['inventory'] - e_state['backlog']
|
| 184 |
stock_correction = DESIRED_INVENTORY - net_inventory
|
| 185 |
-
panicky_order = max(0, int(anchor_demand + stock_correction))
|
| 186 |
-
panicky_order_calc = f"{anchor_demand} (Your Incoming Order) + {stock_correction} (Your Stock Correction)"
|
| 187 |
|
| 188 |
# Get supply line info *just to show* the AI it's being ignored
|
| 189 |
if e_state['name'] == 'Factory':
|
| 190 |
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 191 |
supply_line_desc = "In Production"
|
| 192 |
else:
|
|
|
|
| 193 |
order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
|
| 194 |
-
supply_line = sum(e_state['incoming_shipments']) + order_in_transit_to_supplier
|
| 195 |
supply_line_desc = "Supply Line"
|
| 196 |
|
| 197 |
if info_sharing == 'local':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
return f"""
|
| 199 |
**You are a reactive supply chain manager for the {e_state['name']}.** You have a limited (local) view.
|
| 200 |
You tend to make **reactive, 'gut-instinct' decisions** (like the classic Sterman 1989 model) that cause the Bullwhip Effect.
|
|
@@ -217,6 +253,11 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
|
|
| 217 |
"""
|
| 218 |
|
| 219 |
elif info_sharing == 'full':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
# Build the "Full Info" string just for context
|
| 221 |
full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {current_stable_demand} units.\n"
|
| 222 |
for name, other_e_state in all_echelons_state_decision_point.items():
|
|
@@ -230,14 +271,14 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
|
|
| 230 |
**A "Human-like" Flawed Decision:**
|
| 231 |
Even though you have full information, you are judged by *your own* performance (your inventory, your backlog).
|
| 232 |
You tend to react to your *local* situation (like the classic Sterman 1989 model) instead of using the complex full-system data.
|
| 233 |
-
A 'rational' player would use
|
| 234 |
|
| 235 |
**Your 'Panic' Calculation (Ignoring Full Info and Your Supply Line):**
|
| 236 |
-
1. **Anchor on *
|
| 237 |
2. **Correct for *Your* Stock:** Your desired 'safe' inventory is {DESIRED_INVENTORY}. Your current net inventory is {net_inventory}. You need to order **{stock_correction}** more units.
|
| 238 |
3. **Ignore *Your* Supply Line:** You'll ignore the **{supply_line} units** in your own pipeline ({supply_line_desc}).
|
| 239 |
|
| 240 |
-
**Final Panic Order:** (
|
| 241 |
* Order = {panicky_order_calc} = **{panicky_order} units**.
|
| 242 |
|
| 243 |
**Your Task:** Confirm this 'gut-instinct', locally-focused {task_word}. Respond with a single integer.
|
|
|
|
| 104 |
if not client: return 8, "NO_API_KEY_DEFAULT"
|
| 105 |
with st.spinner(f"Getting AI decision for {echelon_name}..."):
|
| 106 |
try:
|
| 107 |
+
temp = 0.1 if 'perfectly rational' in prompt else 0.7
|
| 108 |
response = client.chat.completions.create(
|
| 109 |
model=OPENAI_MODEL,
|
| 110 |
messages=[
|
|
|
|
| 122 |
st.error(f"API call failed for {echelon_name}: {e}. Defaulting to 4.")
|
| 123 |
return 4, f"API_ERROR: {e}"
|
| 124 |
|
| 125 |
+
# =============== PROMPT FUNCTION (v4 - FIXES FOR OSCILLATION AND HUMAN-LIKE) ===============
|
| 126 |
def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state_decision_point: dict) -> str:
|
| 127 |
# This function's logic is updated for "human_like" to follow a flawed Sterman heuristic.
|
| 128 |
e_state = echelon_state_decision_point
|
| 129 |
base_info = f"Your Current Status at the **{e_state['name']}** for **Week {week}** (Before Shipping):\n- On-hand inventory: {e_state['inventory']} units.\n- Backlog (total unfilled orders): {e_state['backlog']} units.\n- Incoming order this week (just received): {e_state['incoming_order']} units.\n"
|
| 130 |
|
|
|
|
| 131 |
current_stable_demand = get_customer_demand(week) # Use current week's demand
|
| 132 |
|
| 133 |
if e_state['name'] == 'Factory':
|
|
|
|
| 140 |
# --- PERFECT RATIONAL (NORMATIVE) PROMPTS ---
|
| 141 |
|
| 142 |
if llm_personality == 'perfect_rational' and info_sharing == 'full':
|
| 143 |
+
stable_demand = current_stable_demand
|
| 144 |
+
|
| 145 |
+
# 1. CALCULATE CORRECT LEAD TIME (UNCHANGED)
|
| 146 |
+
if e_state['name'] == 'Factory':
|
| 147 |
+
total_lead_time = FACTORY_LEAD_TIME # 1
|
| 148 |
+
elif e_state['name'] == 'Distributor':
|
| 149 |
+
total_lead_time = ORDER_PASSING_DELAY + FACTORY_LEAD_TIME + FACTORY_SHIPPING_DELAY # 1+1+1 = 3
|
| 150 |
+
else:
|
| 151 |
+
total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY # 1+2 = 3
|
| 152 |
+
|
| 153 |
safety_stock = 4
|
| 154 |
target_inventory_level = (stable_demand * total_lead_time) + safety_stock
|
| 155 |
+
|
| 156 |
+
# 2. OSCILLATION FIX: Calculate CORRECT Inventory Position
|
| 157 |
+
order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0) # Order Delay (1 week)
|
| 158 |
+
|
| 159 |
if e_state['name'] == 'Factory':
|
| 160 |
+
# Factory pipeline: In Production (1 week)
|
| 161 |
+
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 162 |
+
inventory_position = (e_state['inventory'] - e_state['backlog'] + supply_line)
|
| 163 |
+
inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InProd={supply_line})"
|
| 164 |
+
|
| 165 |
+
elif e_state['name'] == 'Distributor':
|
| 166 |
+
# Distributor pipeline: In Shipping (1 week) + In Production (1 week) + Order Delay (1 week)
|
| 167 |
+
in_shipping = sum(e_state['incoming_shipments'])
|
| 168 |
+
in_production = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 169 |
+
supply_line = in_shipping + in_production + order_in_transit_to_supplier
|
| 170 |
+
inventory_position = (e_state['inventory'] - e_state['backlog'] + supply_line)
|
| 171 |
+
inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={in_shipping} + InProd={in_production} + OrderToSupplier={order_in_transit_to_supplier})"
|
| 172 |
+
|
| 173 |
+
else: # Retailer and Wholesaler
|
| 174 |
+
# R/W pipeline: In Shipping (2 weeks) + Order Delay (1 week)
|
| 175 |
+
in_shipping = sum(e_state['incoming_shipments'])
|
| 176 |
+
supply_line = in_shipping + order_in_transit_to_supplier
|
| 177 |
+
inventory_position = (e_state['inventory'] - e_state['backlog'] + supply_line)
|
| 178 |
+
inv_pos_components = f"(Inv={e_state['inventory']} - Backlog={e_state['backlog']} + InTransitShip={in_shipping} + OrderToSupplier={order_in_transit_to_supplier})"
|
| 179 |
+
|
| 180 |
optimal_order = max(0, int(target_inventory_level - inventory_position))
|
| 181 |
return f"**You are a perfectly rational supply chain AI with full system visibility.**\nYour only goal is to maintain stability and minimize costs based on mathematical optimization.\n**System Analysis:**\n* **Known Stable End-Customer Demand:** {stable_demand} units/week.\n* **Your Current Total Inventory Position:** {inventory_position} units. {inv_pos_components}\n* **Optimal Target Inventory Level:** {target_inventory_level} units (Target for {total_lead_time} weeks lead time).\n* **Mathematically Optimal {task_word.title()}:** The optimal decision is **{optimal_order} units**.\n**Your Task:** Confirm this optimal {task_word}. Respond with a single integer."
|
| 182 |
|
| 183 |
elif llm_personality == 'perfect_rational' and info_sharing == 'local':
|
| 184 |
+
safety_stock = 4
|
| 185 |
+
anchor_demand = e_state['incoming_order']
|
| 186 |
inventory_correction = safety_stock - (e_state['inventory'] - e_state['backlog'])
|
| 187 |
+
|
| 188 |
+
# 2. OSCILLATION FIX: Calculate CORRECT *Local* Supply Line
|
| 189 |
if e_state['name'] == 'Factory':
|
| 190 |
+
# Factory can see its full (local) pipeline
|
| 191 |
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 192 |
supply_line_desc = "In Production"
|
| 193 |
+
|
| 194 |
+
elif e_state['name'] == 'Distributor':
|
| 195 |
+
# Distributor can *only* see its shipping queue (1 week)
|
| 196 |
+
# It CANNOT see the factory pipeline or its own order delay
|
| 197 |
+
# This is a weak heuristic, but it's *locally* correct and won't oscillate.
|
| 198 |
+
supply_line = sum(e_state['incoming_shipments'])
|
| 199 |
+
supply_line_desc = "Supply Line (In Transit Shipments)"
|
| 200 |
+
|
| 201 |
+
else: # Retailer and Wholesaler
|
| 202 |
+
# R/W can see their full (local) pipeline: Shipping (2 weeks)
|
| 203 |
+
supply_line = sum(e_state['incoming_shipments'])
|
| 204 |
+
supply_line_desc = "Supply Line (In Transit Shipments)"
|
| 205 |
+
|
| 206 |
calculated_order = anchor_demand + inventory_correction - supply_line
|
| 207 |
rational_local_order = max(0, int(calculated_order))
|
| 208 |
+
|
| 209 |
+
return f"**You are a perfectly rational supply chain AI with ONLY LOCAL information.**\nYou must use a logical heuristic to make a stable decision. A proven method is \"Anchoring and Adjustment\".\n\n{base_info}\n\n**Rational Calculation (Anchoring & Adjustment):**\n1. **Anchor on Demand:** Your best guess for future demand is your last incoming order: **{anchor_demand} units**.\n2. **Adjust for Inventory:** You want to hold a safety stock of {safety_stock} units. Your current stock (before shipping) is {e_state['inventory'] - e_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for {supply_line_desc}:** You already have **{supply_line} units** being processed (that you can see). These should be subtracted from your new decision.\n\n**Final Calculation:**\n* Decision = (Anchor Demand) + (Inventory Adjustment) - ({supply_line_desc})\n* Decision = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n**Your Task:** Confirm this locally rational {task_word}. Respond with a single integer."
|
| 210 |
|
| 211 |
# --- HUMAN-LIKE (DESCRIPTIVE) PROMPTS ---
|
| 212 |
|
| 213 |
+
else:
|
|
|
|
|
|
|
| 214 |
DESIRED_INVENTORY = 12 # Matches initial inventory
|
|
|
|
| 215 |
net_inventory = e_state['inventory'] - e_state['backlog']
|
| 216 |
stock_correction = DESIRED_INVENTORY - net_inventory
|
|
|
|
|
|
|
| 217 |
|
| 218 |
# Get supply line info *just to show* the AI it's being ignored
|
| 219 |
if e_state['name'] == 'Factory':
|
| 220 |
supply_line = sum(st.session_state.game_state['factory_production_pipeline'])
|
| 221 |
supply_line_desc = "In Production"
|
| 222 |
else:
|
| 223 |
+
# This is just for display, not calculation
|
| 224 |
order_in_transit_to_supplier = st.session_state.game_state['last_week_orders'].get(e_state['name'], 0)
|
| 225 |
+
supply_line = sum(e_state['incoming_shipments']) + order_in_transit_to_supplier
|
| 226 |
supply_line_desc = "Supply Line"
|
| 227 |
|
| 228 |
if info_sharing == 'local':
|
| 229 |
+
# 1. HUMAN-LIKE / LOCAL (Unchanged): Anchors on *local* incoming order
|
| 230 |
+
anchor_demand = e_state['incoming_order']
|
| 231 |
+
panicky_order = max(0, int(anchor_demand + stock_correction))
|
| 232 |
+
panicky_order_calc = f"{anchor_demand} (Your Incoming Order) + {stock_correction} (Your Stock Correction)"
|
| 233 |
+
|
| 234 |
return f"""
|
| 235 |
**You are a reactive supply chain manager for the {e_state['name']}.** You have a limited (local) view.
|
| 236 |
You tend to make **reactive, 'gut-instinct' decisions** (like the classic Sterman 1989 model) that cause the Bullwhip Effect.
|
|
|
|
| 253 |
"""
|
| 254 |
|
| 255 |
elif info_sharing == 'full':
|
| 256 |
+
# 1. HUMAN-LIKE / FULL (FIXED): Anchors on *global* customer demand
|
| 257 |
+
anchor_demand = current_stable_demand
|
| 258 |
+
panicky_order = max(0, int(anchor_demand + stock_correction))
|
| 259 |
+
panicky_order_calc = f"{anchor_demand} (End-Customer Demand) + {stock_correction} (Your Stock Correction)"
|
| 260 |
+
|
| 261 |
# Build the "Full Info" string just for context
|
| 262 |
full_info_str = f"\n**Full Supply Chain Information (State Before Shipping):**\n- End-Customer Demand this week: {current_stable_demand} units.\n"
|
| 263 |
for name, other_e_state in all_echelons_state_decision_point.items():
|
|
|
|
| 271 |
**A "Human-like" Flawed Decision:**
|
| 272 |
Even though you have full information, you are judged by *your own* performance (your inventory, your backlog).
|
| 273 |
You tend to react to your *local* situation (like the classic Sterman 1989 model) instead of using the complex full-system data.
|
| 274 |
+
A 'rational' player would use a complex formula, but your gut-instinct is to panic about *your* numbers.
|
| 275 |
|
| 276 |
**Your 'Panic' Calculation (Ignoring Full Info and Your Supply Line):**
|
| 277 |
+
1. **Anchor on *End-Customer* Demand:** You can see the real demand is **{anchor_demand}** units. You'll use that as your base.
|
| 278 |
2. **Correct for *Your* Stock:** Your desired 'safe' inventory is {DESIRED_INVENTORY}. Your current net inventory is {net_inventory}. You need to order **{stock_correction}** more units.
|
| 279 |
3. **Ignore *Your* Supply Line:** You'll ignore the **{supply_line} units** in your own pipeline ({supply_line_desc}).
|
| 280 |
|
| 281 |
+
**Final Panic Order:** (End-Customer Demand) + (Your Stock Correction)
|
| 282 |
* Order = {panicky_order_calc} = **{panicky_order} units**.
|
| 283 |
|
| 284 |
**Your Task:** Confirm this 'gut-instinct', locally-focused {task_word}. Respond with a single integer.
|