Lilli98 commited on
Commit
037c8c2
·
verified ·
1 Parent(s): e0ef505

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -91
app.py CHANGED
@@ -1,33 +1,32 @@
1
  # app.py
2
- # @title 啤酒游戏最终整合版 (v3 - 两阶段决策 + 信息隔离)
3
 
4
  # -----------------------------------------------------------------------------
5
- # 1. 导入必要的库
6
  # -----------------------------------------------------------------------------
7
  import streamlit as st
8
  import pandas as pd
9
  import matplotlib.pyplot as plt
10
- import numpy as np
11
  from collections import deque
12
  import time
13
  import openai
14
  import re
15
  import random
16
  import uuid
17
- import os
18
  from pathlib import Path
19
  from datetime import datetime
20
  from huggingface_hub import HfApi
21
 
22
  # -----------------------------------------------------------------------------
23
- # 0. 页面配置 (必须是第一个Streamlit命令)
24
  # -----------------------------------------------------------------------------
25
- st.set_page_config(page_title="啤酒游戏-人机协作版", layout="wide")
 
26
 
27
  # -----------------------------------------------------------------------------
28
- # 2. 配置游戏核心参数和API密钥
29
  # -----------------------------------------------------------------------------
30
- # --- 游戏参数 ---
31
  WEEKS = 24
32
  INITIAL_INVENTORY = 12
33
  INITIAL_BACKLOG = 0
@@ -38,31 +37,34 @@ FACTORY_SHIPPING_DELAY = 1
38
  HOLDING_COST = 0.5
39
  BACKLOG_COST = 1.0
40
 
41
- # --- 模型和日志配置 ---
42
  OPENAI_MODEL = "gpt-4o-mini"
43
  LOCAL_LOG_DIR = Path("logs")
44
  LOCAL_LOG_DIR.mkdir(exist_ok=True)
45
 
46
- # --- API & Secrets 配置 ---
47
  try:
48
  client = openai.OpenAI(api_key=st.secrets["OPENAI_API_KEY"])
49
  HF_TOKEN = st.secrets.get("HF_TOKEN")
50
  HF_REPO_ID = st.secrets.get("HF_REPO_ID")
51
  hf_api = HfApi() if HF_TOKEN else None
52
  except Exception as e:
53
- st.session_state.initialization_error = f"启动时读取Secrets出错: {e}"
54
  client = None
55
  else:
56
  st.session_state.initialization_error = None
57
 
 
58
  # -----------------------------------------------------------------------------
59
- # 3. 游戏核心逻辑函数
60
  # -----------------------------------------------------------------------------
61
 
62
  def get_customer_demand(week: int) -> int:
 
63
  return 4 if week <= 4 else 8
64
 
65
  def init_game_state(llm_personality: str, info_sharing: str):
 
66
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
67
  human_role = random.choice(roles)
68
  participant_id = str(uuid.uuid4())[:8]
@@ -72,8 +74,8 @@ def init_game_state(llm_personality: str, info_sharing: str):
72
  'human_role': human_role, 'llm_personality': llm_personality,
73
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
74
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
75
- 'decision_step': 'initial_order', # 新增:控制决策阶段
76
- 'human_initial_order': None, # 新增:储存玩家的初步订单
77
  }
78
 
79
  for i, name in enumerate(roles):
@@ -90,11 +92,12 @@ def init_game_state(llm_personality: str, info_sharing: str):
90
  'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
91
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
92
  }
93
- st.info(f"新游戏开始!AI模式: **{llm_personality} / {info_sharing}**。您的角色: **{human_role}**。")
94
 
95
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
 
96
  if not client: return 8, "NO_API_KEY_DEFAULT"
97
- with st.spinner(f"正在为 {echelon_name} 获取AI决策..."):
98
  try:
99
  temp = 0.1 if 'rational' in prompt else 0.7
100
  response = client.chat.completions.create(
@@ -110,11 +113,12 @@ def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
110
  if match: return int(match.group(0)), raw_text
111
  return 8, raw_text
112
  except Exception as e:
113
- st.error(f"API调用失败 for {echelon_name}: {e}")
114
  return 8, f"API_ERROR: {e}"
115
 
116
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
117
- # (此函数内容与上一版完全相同,为简洁省略,实际代码中应完整保留)
 
118
  base_info = f"Your Current Status at the **{echelon_state['name']}** for **Week {week}**:\n- On-hand inventory: {echelon_state['inventory']} units.\n- Backlog (unfilled orders): {echelon_state['backlog']} units.\n- Incoming order this week (from your customer): {echelon_state['incoming_order']} units.\n- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}\n- Orders you have placed being processed by your supplier: {list(echelon_state['order_pipeline'])}"
119
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
120
  stable_demand = 8; total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY; safety_stock = 4
@@ -137,16 +141,15 @@ def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sh
137
  elif llm_personality == 'human_like' and info_sharing == 'local':
138
  return f"**You are a reactive supply chain manager for the {echelon_state['name']}.** You have a limited view and tend to over-correct based on fear.\n\n**Your Mindset: **Your top priority is try to not have a backlog.\n\n{base_info}\n\n**Your Task:** You just saw your own inventory and a new order coming. Your gut instinct is to panic and order enough to ensure you are never caught with a backlog again.\n\n**React emotionally.** What is your knee-jerk order quantity? Respond with a single integer."
139
 
140
-
141
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
142
- """推进一周的游戏进程,并记录包括两阶段决策在内的详细日志"""
143
  state = st.session_state.game_state
144
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
145
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
146
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
147
  llm_raw_responses = {}
148
 
149
- # 游戏流程 (与之前相同)
150
  factory_state = echelons["Factory"]
151
  if state['factory_production_pipeline']: factory_state['inventory'] += state['factory_production_pipeline'].popleft()
152
  for name in ["Retailer", "Wholesaler", "Distributor"]:
@@ -164,7 +167,7 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
164
  receiver = echelons[sender]['downstream_name']
165
  if receiver: echelons[receiver]['incoming_shipments'].append(echelons[sender]['shipment_sent'])
166
 
167
- # 各环节下订单
168
  for name in echelon_order:
169
  e = echelons[name]
170
  if name == human_role:
@@ -178,9 +181,9 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
178
 
179
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
180
 
181
- # 更新成本和记录日志
182
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
183
- del log_entry['echelons'], log_entry['factory_production_pipeline'] # 移除复杂对象
184
  for name in echelon_order:
185
  e = echelons[name]
186
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
@@ -188,20 +191,21 @@ def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: i
188
  log_entry[f'{name}.{key}'] = e[key]
189
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
190
 
191
- # 新增:记录两阶段决策数据
192
  log_entry[f'{human_role}.initial_order'] = human_initial_order
193
  log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
194
 
195
  state['logs'].append(log_entry)
196
 
197
- # 推进周数并重置决策步骤
198
  state['week'] += 1
199
  state['decision_step'] = 'initial_order'
200
  if state['week'] > WEEKS: state['game_running'] = False
201
 
202
 
203
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
204
- fig, axes = plt.subplots(4, 1, figsize=(12, 22)) # 增加一个子图
 
205
  fig.suptitle(title, fontsize=16)
206
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
207
 
@@ -213,140 +217,172 @@ def plot_results(df: pd.DataFrame, title: str, human_role: str):
213
  'total_cost': row[f'{e}.total_cost']})
214
  plot_df = pd.DataFrame(plot_data)
215
 
216
- # 1: 库存
217
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
218
- inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels'); axes[0].grid(True, linestyle='--')
219
 
220
- # 2: 订单 (牛鞭效应)
221
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
222
- order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5); axes[1].set_title('Order Quantities (Bullwhip Effect)'); axes[1].grid(True, linestyle='--'); axes[1].legend()
223
 
224
- # 3: 成本
225
  total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
226
- total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost')
227
 
228
- # 新增图4: 人类玩家决策分析
229
  human_df = df[['week', f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']].copy()
230
  human_df.rename(columns={
231
- f'{human_role}.initial_order': 'Your Initial Order',
232
- f'{human_role}.ai_suggestion': 'AI Suggestion',
233
- f'{human_role}.order_placed': 'Your Final Order'
234
  }, inplace=True)
235
- human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-')
236
- axes[3].set_title(f'Analysis of Your ({human_role}) Decisions')
237
- axes[3].set_ylabel('Order Quantity')
238
- axes[3].grid(True, linestyle='--')
239
 
240
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
241
 
242
-
243
  def save_logs_and_upload(state: dict):
244
- # (此函数内容与上一版完全相同)
245
  if not state.get('logs'): return
246
  participant_id = state['participant_id']
247
  df = pd.json_normalize(state['logs'])
248
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
249
  df.to_csv(fname, index=False)
250
- st.success(f"日志已成功保存到本地: `{fname}`")
251
  with open(fname, "rb") as f:
252
- st.download_button("📥 下载日志CSV文件", data=f, file_name=fname.name, mime="text/csv")
253
  if HF_TOKEN and HF_REPO_ID and hf_api:
254
- with st.spinner("正在上传日志到 Hugging Face Hub..."):
255
  try:
256
  url = hf_api.upload_file(path_or_fileobj=str(fname), path_in_repo=f"logs/{fname.name}", repo_id=HF_REPO_ID, repo_type="dataset", token=HF_TOKEN)
257
- st.success(f"✅ 日志已成功上传到 Hugging Face! [查看文件]({url})")
258
  except Exception as e:
259
- st.error(f"上传到 Hugging Face 失败: {e}")
260
 
261
  # -----------------------------------------------------------------------------
262
- # 4. Streamlit UI 界面
263
  # -----------------------------------------------------------------------------
264
- st.title("🍺 啤酒游戏:人机协作挑战")
265
 
266
  if st.session_state.get('initialization_error'):
267
  st.error(st.session_state.initialization_error)
268
  else:
269
- # --- 游戏设置和初始化 ---
270
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
271
- st.markdown("你将扮演供应链中的一个角色,与另外三个由大语言模型(LLM)驱动的AI代理合作。")
272
- st.header("🎮 开始新游戏")
 
 
 
 
 
 
 
 
273
  col1, col2 = st.columns(2)
274
- with col1: llm_personality = st.selectbox("AI '性格'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title())
275
- with col2: info_sharing = st.selectbox("信息共享", ('local', 'full'), format_func=lambda x: x.title())
276
- if st.button("🚀 开始游戏", type="primary", disabled=(client is None)):
277
- init_game_state(llm_personality, info_sharing); st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
- # --- 游戏主界面 ---
280
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
281
  state = st.session_state.game_state
282
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
283
 
284
- st.header(f" {week} / {WEEKS}")
285
- st.subheader(f"你的角色: **{human_role}** | AI模式: **{state['llm_personality'].replace('_', ' ')}** | 信息: **{state['info_sharing']}**")
286
 
287
- # --- 核心改动:根据信息模式显示面板 ---
288
  st.markdown("---")
289
- st.subheader("供应链状态")
290
  if info_sharing == 'full':
291
  cols = st.columns(4)
292
  for i, name in enumerate(["Retailer", "Wholesaler", "Distributor", "Factory"]):
293
  with cols[i]:
294
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
295
- st.markdown(f"##### {icon} {name} {'()' if name == human_role else ''}")
296
- st.metric("库存", e['inventory']); st.metric("缺货", e['backlog'])
297
- st.write(f"收到订单: **{e['incoming_order']}**")
298
- st.write(f"下周到货: **{list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}**")
299
  else: # local information
300
- st.info("在本地信息模式下,你只能看到你自己的状态。")
301
  e = echelons[human_role]
302
- st.markdown(f"### 👤 {human_role} (你的面板)")
303
  col1, col2, col3, col4 = st.columns(4)
304
- col1.metric("当前库存", e['inventory'])
305
- col2.metric("当前缺货/积压", e['backlog'])
306
- col3.write(f"**本周收到订单:**\n# {e['incoming_order']}")
307
- col4.write(f"**下周预计到货:**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
308
  st.markdown("---")
309
 
310
- # --- 核心改动:两阶段决策流程 ---
311
- st.header("你的决策")
312
  human_echelon_state = echelons[human_role]
313
 
314
- # 阶段一:提交初步订单
315
  if state['decision_step'] == 'initial_order':
316
  with st.form(key="initial_order_form"):
317
- st.markdown("#### **第一步**: 请根据你看到的信息,提交你的 **初步** 订单。")
318
- initial_order = st.number_input("你的初步订单数量:", min_value=0, step=1, value=human_echelon_state['incoming_order'])
319
- if st.form_submit_button("提交初步订单,查看AI建议", type="primary"):
320
  state['human_initial_order'] = int(initial_order)
321
  state['decision_step'] = 'final_order'
322
  st.rerun()
323
-
324
- # 阶段二:结合AI建议,提交最终订单
325
  elif state['decision_step'] == 'final_order':
326
- st.success(f"你提交的初步订单是: **{state['human_initial_order']}** 单位。")
327
  prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
328
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
329
 
330
  with st.form(key="final_order_form"):
331
- st.markdown(f"#### **第二步**: AI的建议订单是 **{ai_suggestion}** 单位。")
332
- st.markdown("请结合AI建议,提交你的 **最终** 订单。这将结束本周。")
333
- final_order = st.number_input("你的最终订单数量:", min_value=0, step=1, value=ai_suggestion)
334
- if st.form_submit_button("提交最终订单并进入下一周"):
335
  step_game(int(final_order), state['human_initial_order'], ai_suggestion)
336
  st.rerun()
337
 
338
- st.sidebar.header("游戏信息")
339
- st.sidebar.markdown(f"**游戏ID**: `{state['participant_id']}` | **当前周**: {week}")
340
- if st.sidebar.button("🔄 重置游戏"):
341
  del st.session_state.game_state; st.rerun()
342
 
343
- # --- 游戏结束界面 ---
344
  if 'game_state' in st.session_state and not st.session_state.game_state.get('game_running', False) and st.session_state.game_state['week'] > WEEKS:
345
- st.header("🎉 游戏结束!")
346
  state = st.session_state.game_state
347
  logs_df = pd.json_normalize(state['logs'])
348
  fig = plot_results(logs_df, f"Beer Game (Human: {state['human_role']})\n(AI: {state['llm_personality']} | Info: {state['info_sharing']})", state['human_role'])
349
  st.pyplot(fig)
350
  save_logs_and_upload(state)
351
- if st.button("✨ 开始一局新游戏"):
352
  del st.session_state.game_state; st.rerun()
 
1
  # app.py
2
+ # @title Beer Game Final Version (v4 - English UI & Introduction)
3
 
4
  # -----------------------------------------------------------------------------
5
+ # 1. Import Libraries
6
  # -----------------------------------------------------------------------------
7
  import streamlit as st
8
  import pandas as pd
9
  import matplotlib.pyplot as plt
 
10
  from collections import deque
11
  import time
12
  import openai
13
  import re
14
  import random
15
  import uuid
 
16
  from pathlib import Path
17
  from datetime import datetime
18
  from huggingface_hub import HfApi
19
 
20
  # -----------------------------------------------------------------------------
21
+ # 0. Page Configuration (Must be the first Streamlit command)
22
  # -----------------------------------------------------------------------------
23
+ st.set_page_config(page_title="Beer Game: Human-AI Collaboration", layout="wide")
24
+
25
 
26
  # -----------------------------------------------------------------------------
27
+ # 2. Game Parameters & API Configuration
28
  # -----------------------------------------------------------------------------
29
+ # --- Game Parameters ---
30
  WEEKS = 24
31
  INITIAL_INVENTORY = 12
32
  INITIAL_BACKLOG = 0
 
37
  HOLDING_COST = 0.5
38
  BACKLOG_COST = 1.0
39
 
40
+ # --- Model & Log Configuration ---
41
  OPENAI_MODEL = "gpt-4o-mini"
42
  LOCAL_LOG_DIR = Path("logs")
43
  LOCAL_LOG_DIR.mkdir(exist_ok=True)
44
 
45
+ # --- API & Secrets Configuration ---
46
  try:
47
  client = openai.OpenAI(api_key=st.secrets["OPENAI_API_KEY"])
48
  HF_TOKEN = st.secrets.get("HF_TOKEN")
49
  HF_REPO_ID = st.secrets.get("HF_REPO_ID")
50
  hf_api = HfApi() if HF_TOKEN else None
51
  except Exception as e:
52
+ st.session_state.initialization_error = f"Error reading secrets on startup: {e}."
53
  client = None
54
  else:
55
  st.session_state.initialization_error = None
56
 
57
+
58
  # -----------------------------------------------------------------------------
59
+ # 3. Core Game Logic Functions
60
  # -----------------------------------------------------------------------------
61
 
62
  def get_customer_demand(week: int) -> int:
63
+ """Defines the end-customer demand pattern."""
64
  return 4 if week <= 4 else 8
65
 
66
  def init_game_state(llm_personality: str, info_sharing: str):
67
+ """Initializes or resets the game state in st.session_state."""
68
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
69
  human_role = random.choice(roles)
70
  participant_id = str(uuid.uuid4())[:8]
 
74
  'human_role': human_role, 'llm_personality': llm_personality,
75
  'info_sharing': info_sharing, 'logs': [], 'echelons': {},
76
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
77
+ 'decision_step': 'initial_order', # Controls the two-step decision process
78
+ 'human_initial_order': None, # Stores the player's initial order
79
  }
80
 
81
  for i, name in enumerate(roles):
 
92
  'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
93
  'weekly_cost': 0, 'total_cost': 0, 'upstream_name': upstream, 'downstream_name': downstream,
94
  }
95
+ st.info(f"New game started! AI Mode: **{llm_personality} / {info_sharing}**. You have been randomly assigned the role of: **{human_role}**.")
96
 
97
  def get_llm_order_decision(prompt: str, echelon_name: str) -> (int, str):
98
+ """Calls the OpenAI API to get a decision and returns the integer order and raw text."""
99
  if not client: return 8, "NO_API_KEY_DEFAULT"
100
+ with st.spinner(f"Getting AI decision for {echelon_name}..."):
101
  try:
102
  temp = 0.1 if 'rational' in prompt else 0.7
103
  response = client.chat.completions.create(
 
113
  if match: return int(match.group(0)), raw_text
114
  return 8, raw_text
115
  except Exception as e:
116
+ st.error(f"API call failed for {echelon_name}: {e}")
117
  return 8, f"API_ERROR: {e}"
118
 
119
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
120
+ """Generates the prompt for the LLM based on the game scenario."""
121
+ # This function's logic is complex and correct, so it remains unchanged.
122
  base_info = f"Your Current Status at the **{echelon_state['name']}** for **Week {week}**:\n- On-hand inventory: {echelon_state['inventory']} units.\n- Backlog (unfilled orders): {echelon_state['backlog']} units.\n- Incoming order this week (from your customer): {echelon_state['incoming_order']} units.\n- Shipments on the way to you: {list(echelon_state['incoming_shipments'])}\n- Orders you have placed being processed by your supplier: {list(echelon_state['order_pipeline'])}"
123
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
124
  stable_demand = 8; total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY; safety_stock = 4
 
141
  elif llm_personality == 'human_like' and info_sharing == 'local':
142
  return f"**You are a reactive supply chain manager for the {echelon_state['name']}.** You have a limited view and tend to over-correct based on fear.\n\n**Your Mindset: **Your top priority is try to not have a backlog.\n\n{base_info}\n\n**Your Task:** You just saw your own inventory and a new order coming. Your gut instinct is to panic and order enough to ensure you are never caught with a backlog again.\n\n**React emotionally.** What is your knee-jerk order quantity? Respond with a single integer."
143
 
 
144
  def step_game(human_final_order: int, human_initial_order: int, ai_suggestion: int):
145
+ """Processes one week of the game and records detailed logs, including two-step decision data."""
146
  state = st.session_state.game_state
147
  week, echelons, human_role = state['week'], state['echelons'], state['human_role']
148
  llm_personality, info_sharing = state['llm_personality'], state['info_sharing']
149
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
150
  llm_raw_responses = {}
151
 
152
+ # Core game simulation steps (unchanged)
153
  factory_state = echelons["Factory"]
154
  if state['factory_production_pipeline']: factory_state['inventory'] += state['factory_production_pipeline'].popleft()
155
  for name in ["Retailer", "Wholesaler", "Distributor"]:
 
167
  receiver = echelons[sender]['downstream_name']
168
  if receiver: echelons[receiver]['incoming_shipments'].append(echelons[sender]['shipment_sent'])
169
 
170
+ # Agents place orders
171
  for name in echelon_order:
172
  e = echelons[name]
173
  if name == human_role:
 
181
 
182
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
183
 
184
+ # Update costs and record logs
185
  log_entry = {'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week, **state}
186
+ del log_entry['echelons'], log_entry['factory_production_pipeline'], log_entry['logs'] # Clean up complex objects
187
  for name in echelon_order:
188
  e = echelons[name]
189
  e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST); e['total_cost'] += e['weekly_cost']
 
191
  log_entry[f'{name}.{key}'] = e[key]
192
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
193
 
194
+ # Record two-step decision data
195
  log_entry[f'{human_role}.initial_order'] = human_initial_order
196
  log_entry[f'{human_role}.ai_suggestion'] = ai_suggestion
197
 
198
  state['logs'].append(log_entry)
199
 
200
+ # Advance week and reset decision step
201
  state['week'] += 1
202
  state['decision_step'] = 'initial_order'
203
  if state['week'] > WEEKS: state['game_running'] = False
204
 
205
 
206
  def plot_results(df: pd.DataFrame, title: str, human_role: str):
207
+ """Generates and returns the end-of-game plots."""
208
+ fig, axes = plt.subplots(4, 1, figsize=(12, 22))
209
  fig.suptitle(title, fontsize=16)
210
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
211
 
 
217
  'total_cost': row[f'{e}.total_cost']})
218
  plot_df = pd.DataFrame(plot_data)
219
 
220
+ # Plot 1: Inventory
221
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
222
+ inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels'); axes[0].grid(True, linestyle='--'); axes[0].set_ylabel('Stock (Units)')
223
 
224
+ # Plot 2: Orders (Bullwhip Effect)
225
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
226
+ order_pivot.plot(ax=axes[1], style='--'); axes[1].plot(range(1, WEEKS + 1), [get_customer_demand(w) for w in range(1, WEEKS + 1)], label='Customer Demand', color='black', lw=2.5); axes[1].set_title('Order Quantities (The Bullwhip Effect)'); axes[1].grid(True, linestyle='--'); axes[1].legend(); axes[1].set_ylabel('Ordered (Units)')
227
 
228
+ # Plot 3: Costs
229
  total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
230
+ total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost'); axes[2].set_ylabel('Cost ($)')
231
 
232
+ # Plot 4: Human Decision Analysis
233
  human_df = df[['week', f'{human_role}.initial_order', f'{human_role}.ai_suggestion', f'{human_role}.order_placed']].copy()
234
  human_df.rename(columns={
235
+ f'{human_role}.initial_order': 'Your Initial Order', f'{human_role}.ai_suggestion': 'AI Suggestion', f'{human_role}.order_placed': 'Your Final Order'
 
 
236
  }, inplace=True)
237
+ human_df.plot(x='week', ax=axes[3], marker='o', linestyle='-'); axes[3].set_title(f'Analysis of Your ({human_role}) Decisions'); axes[3].set_ylabel('Order Quantity'); axes[3].grid(True, linestyle='--'); axes[3].set_xlabel('Week')
 
 
 
238
 
239
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
240
 
 
241
  def save_logs_and_upload(state: dict):
242
+ """Saves logs locally and uploads to Hugging Face Hub at the end of the game."""
243
  if not state.get('logs'): return
244
  participant_id = state['participant_id']
245
  df = pd.json_normalize(state['logs'])
246
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
247
  df.to_csv(fname, index=False)
248
+ st.success(f"Log successfully saved locally: `{fname}`")
249
  with open(fname, "rb") as f:
250
+ st.download_button("📥 Download Log CSV", data=f, file_name=fname.name, mime="text/csv")
251
  if HF_TOKEN and HF_REPO_ID and hf_api:
252
+ with st.spinner("Uploading log to Hugging Face Hub..."):
253
  try:
254
  url = hf_api.upload_file(path_or_fileobj=str(fname), path_in_repo=f"logs/{fname.name}", repo_id=HF_REPO_ID, repo_type="dataset", token=HF_TOKEN)
255
+ st.success(f"✅ Log successfully uploaded to Hugging Face! [View File]({url})")
256
  except Exception as e:
257
+ st.error(f"Upload to Hugging Face failed: {e}")
258
 
259
  # -----------------------------------------------------------------------------
260
+ # 4. Streamlit UI
261
  # -----------------------------------------------------------------------------
262
+ st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
263
 
264
  if st.session_state.get('initialization_error'):
265
  st.error(st.session_state.initialization_error)
266
  else:
267
+ # --- Game Setup & Instructions ---
268
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
269
+
270
+ # --- NEW: Introduction Section ---
271
+ st.markdown("---")
272
+ st.header("📖 Welcome to the Beer Game!")
273
+ st.markdown("""
274
+ The Beer Game is a classic supply chain simulation that demonstrates a phenomenon called the **"Bullwhip Effect."** Even with stable customer demand, small variations in orders can amplify as they move up the supply chain, causing massive shortages and overstocks.
275
+
276
+ **Your Goal:** Minimize the total cost for your position in the supply chain. Costs are incurred for holding inventory (**$0.50**/unit/week) and for having a backlog of unfilled orders (**$1.00**/unit/week).
277
+ """)
278
+
279
  col1, col2 = st.columns(2)
280
+ with col1:
281
+ st.subheader("🔗 The Supply Chain")
282
+ st.markdown("""
283
+ You will be randomly assigned one of four roles. The other three will be controlled by AI agents.
284
+ - **Retailer:** Fulfills end-customer demand. Orders from the Wholesaler.
285
+ - **Wholesaler:** Fulfills Retailer orders. Orders from the Distributor.
286
+ - **Distributor:** Fulfills Wholesaler orders. Orders from the Factory.
287
+ - **Factory:** Fulfills Distributor orders. Produces new beer.
288
+ """)
289
+ with col2:
290
+ st.subheader("⏳ The Challenge: Delays!")
291
+ st.markdown("""
292
+ The key challenge is managing delays. There is a **communication delay** for orders to reach your supplier and a **shipping delay** for goods to arrive. You must order enough to meet future demand without creating a huge pile of expensive inventory.
293
+ """)
294
+
295
+ st.subheader("🎮 How to Play This Version")
296
+ st.markdown("""
297
+ 1. **Configure the Game:** Choose the AI's behavior and the level of information sharing.
298
+ 2. **Get Your Role:** You will be randomly assigned a role.
299
+ 3. **Make Your Decision (Two Steps):**
300
+ - **Step 1:** Based on your current status, submit your **initial order**.
301
+ - **Step 2:** After submitting, you will see an **AI suggestion**. You can either follow it or stick to your own judgment when submitting your **final order**.
302
+ 4. **Advance:** Once you submit your final order, the week advances, and all AI agents make their moves.
303
+ """)
304
+ st.markdown("---")
305
+
306
+ # --- Game Configuration ---
307
+ st.header("⚙️ Game Configuration")
308
+ c1, c2 = st.columns(2)
309
+ with c1:
310
+ llm_personality = st.selectbox("AI Agent 'Personality'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title(), help="**Human-like:** Tends to react emotionally, potentially over-ordering. **Perfect Rational:** Uses a mathematical heuristic to make stable, logical decisions.")
311
+ with c2:
312
+ info_sharing = st.selectbox("Information Sharing Level", ('local', 'full'), format_func=lambda x: x.title(), help="**Local:** You and the AI agents can only see your own inventory and incoming orders. **Full:** Everyone can see the entire supply chain's status and the true end-customer demand.")
313
+
314
+ if st.button("🚀 Start Game", type="primary", disabled=(client is None)):
315
+ init_game_state(llm_personality, info_sharing)
316
+ st.rerun()
317
 
318
+ # --- Main Game Interface ---
319
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
320
  state = st.session_state.game_state
321
  week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
322
 
323
+ st.header(f"Week {week} / {WEEKS}")
324
+ st.subheader(f"Your Role: **{human_role}** | AI Mode: **{state['llm_personality'].replace('_', ' ')}** | Information: **{state['info_sharing']}**")
325
 
 
326
  st.markdown("---")
327
+ st.subheader("Supply Chain Status")
328
  if info_sharing == 'full':
329
  cols = st.columns(4)
330
  for i, name in enumerate(["Retailer", "Wholesaler", "Distributor", "Factory"]):
331
  with cols[i]:
332
  e, icon = echelons[name], "👤" if name == human_role else "🤖"
333
+ st.markdown(f"##### {icon} {name} {'(You)' if name == human_role else ''}")
334
+ st.metric("Inventory", e['inventory']); st.metric("Backlog", e['backlog'])
335
+ st.write(f"Incoming Order: **{e['incoming_order']}**")
336
+ st.write(f"Arriving Next Week: **{list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}**")
337
  else: # local information
338
+ st.info("In Local Information mode, you can only see your own status dashboard.")
339
  e = echelons[human_role]
340
+ st.markdown(f"### 👤 {human_role} (Your Dashboard)")
341
  col1, col2, col3, col4 = st.columns(4)
342
+ col1.metric("Current Inventory", e['inventory'])
343
+ col2.metric("Current Backlog", e['backlog'])
344
+ col3.write(f"**Incoming Order (This Week):**\n# {e['incoming_order']}")
345
+ col4.write(f"**Shipment Arriving (Next Week):**\n# {list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}")
346
  st.markdown("---")
347
 
348
+ # --- Two-Step Decision UI ---
349
+ st.header("Your Decision")
350
  human_echelon_state = echelons[human_role]
351
 
 
352
  if state['decision_step'] == 'initial_order':
353
  with st.form(key="initial_order_form"):
354
+ st.markdown("#### **Step 1:** Based on the information available, submit your **initial** order.")
355
+ initial_order = st.number_input("Your Initial Order Quantity:", min_value=0, step=1, value=human_echelon_state['incoming_order'])
356
+ if st.form_submit_button("Submit Initial Order & See AI Suggestion", type="primary"):
357
  state['human_initial_order'] = int(initial_order)
358
  state['decision_step'] = 'final_order'
359
  st.rerun()
360
+
 
361
  elif state['decision_step'] == 'final_order':
362
+ st.success(f"Your initial order was: **{state['human_initial_order']}** units.")
363
  prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
364
  ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)")
365
 
366
  with st.form(key="final_order_form"):
367
+ st.markdown(f"#### **Step 2:** The AI suggests ordering **{ai_suggestion}** units.")
368
+ st.markdown("Considering the AI's advice, submit your **final** order to end the week.")
369
+ final_order = st.number_input("Your Final Order Quantity:", min_value=0, step=1, value=ai_suggestion)
370
+ if st.form_submit_button("Submit Final Order & Advance to Next Week"):
371
  step_game(int(final_order), state['human_initial_order'], ai_suggestion)
372
  st.rerun()
373
 
374
+ st.sidebar.header("Game Info")
375
+ st.sidebar.markdown(f"**Game ID**: `{state['participant_id']}`\n\n**Current Week**: {week}")
376
+ if st.sidebar.button("🔄 Reset Game"):
377
  del st.session_state.game_state; st.rerun()
378
 
379
+ # --- Game Over Interface ---
380
  if 'game_state' in st.session_state and not st.session_state.game_state.get('game_running', False) and st.session_state.game_state['week'] > WEEKS:
381
+ st.header("🎉 Game Over!")
382
  state = st.session_state.game_state
383
  logs_df = pd.json_normalize(state['logs'])
384
  fig = plot_results(logs_df, f"Beer Game (Human: {state['human_role']})\n(AI: {state['llm_personality']} | Info: {state['info_sharing']})", state['human_role'])
385
  st.pyplot(fig)
386
  save_logs_and_upload(state)
387
+ if st.button("✨ Start a New Game"):
388
  del st.session_state.game_state; st.rerun()