Lilli98 commited on
Commit
e0ef505
·
verified ·
1 Parent(s): 60d8a4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -168
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # @title 啤酒游戏最终整合版 (Streamlit 交互应用 + Hugging Face 日志上传) - 已修复版本
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. 导入必要的库
@@ -17,14 +17,13 @@ import uuid
17
  import os
18
  from pathlib import Path
19
  from datetime import datetime
20
- from huggingface_hub import HfApi, upload_file
21
 
22
  # -----------------------------------------------------------------------------
23
  # 0. 页面配置 (必须是第一个Streamlit命令)
24
  # -----------------------------------------------------------------------------
25
  st.set_page_config(page_title="啤酒游戏-人机协作版", layout="wide")
26
 
27
-
28
  # -----------------------------------------------------------------------------
29
  # 2. 配置游戏核心参数和API密钥
30
  # -----------------------------------------------------------------------------
@@ -42,57 +41,41 @@ BACKLOG_COST = 1.0
42
  # --- 模型和日志配置 ---
43
  OPENAI_MODEL = "gpt-4o-mini"
44
  LOCAL_LOG_DIR = Path("logs")
45
- LOCAL_LOG_DIR.mkdir(exist_ok=True) # Ensure the log directory exists
46
 
47
- # --- API & Secrets 配置 (从 Streamlit Secrets 读取) ---
48
  try:
49
- # OpenAI
50
  client = openai.OpenAI(api_key=st.secrets["OPENAI_API_KEY"])
51
- # Hugging Face
52
  HF_TOKEN = st.secrets.get("HF_TOKEN")
53
- HF_REPO_ID = st.secrets.get("HF_REPO_ID") # e.g., "YourUser/beer-game-logs"
54
- if HF_TOKEN:
55
- hf_api = HfApi()
56
- else:
57
- hf_api = None
58
  except Exception as e:
59
- # 将错误信息显示在主页面,而不是在加载配置时就调用st.error
60
- st.session_state.initialization_error = f"启动时读取Secrets出错: {e}. 请确保在Streamlit的Secrets中设置了 OPENAI_API_KEY。可选设置 HF_TOKEN 和 HF_REPO_ID 用于上传日志。"
61
  client = None
62
- HF_TOKEN = None
63
- HF_REPO_ID = None
64
- hf_api = None
65
  else:
66
  st.session_state.initialization_error = None
67
 
68
-
69
  # -----------------------------------------------------------------------------
70
- # 3. 游戏核心逻辑函数 (大部分源自代码1, 并为Streamlit适配)
71
  # -----------------------------------------------------------------------------
72
 
73
  def get_customer_demand(week: int) -> int:
74
- """定义终端客户需求函数"""
75
  return 4 if week <= 4 else 8
76
 
77
  def init_game_state(llm_personality: str, info_sharing: str):
78
- """初始化或重置游戏状态,并储存在 st.session_state 中"""
79
  roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
80
  human_role = random.choice(roles)
81
- participant_id = str(uuid.uuid4())[:8] # Generate a unique ID for this game session
82
 
83
  st.session_state.game_state = {
84
- 'game_running': True,
85
- 'participant_id': participant_id,
86
- 'week': 1,
87
- 'human_role': human_role,
88
- 'llm_personality': llm_personality,
89
- 'info_sharing': info_sharing,
90
- 'logs': [], # Changed from 'history' to 'logs' for more detailed logging
91
- 'echelons': {},
92
  'factory_production_pipeline': deque([0] * FACTORY_LEAD_TIME, maxlen=FACTORY_LEAD_TIME),
 
 
93
  }
94
 
95
- # 为每个角色初始化状态
96
  for i, name in enumerate(roles):
97
  upstream = roles[i + 1] if i + 1 < len(roles) else None
98
  downstream = roles[i - 1] if i - 1 >= 0 else None
@@ -101,64 +84,44 @@ def init_game_state(llm_personality: str, info_sharing: str):
101
  else: shipping_weeks = SHIPPING_DELAY
102
 
103
  st.session_state.game_state['echelons'][name] = {
104
- 'name': name, 'upstream_name': upstream, 'downstream_name': downstream,
105
- 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
106
  'order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
107
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
108
  'incoming_order': 0, 'order_placed': 0, 'shipment_sent': 0,
109
- 'weekly_cost': 0, 'total_cost': 0,
110
  }
111
- st.info(f"新游戏开始!AI模式: **{llm_personality} / {info_sharing}**。您的角色: **{human_role}**。本次游戏ID: `{participant_id}`")
112
-
113
-
114
- def get_llm_order_decision(prompt: str, echelon_name: str, current_week: int, personality: str) -> (int, str):
115
- """调用 OpenAI API 获取决策,并返回决策和原始文本"""
116
- if not client:
117
- st.warning("API Key未设置,LLM将使用默认值8。")
118
- return 8, "NO_API_KEY_DEFAULT"
119
 
 
 
120
  with st.spinner(f"正在为 {echelon_name} 获取AI决策..."):
121
- temp = 0.1 if personality == 'perfect_rational' else 0.7
122
  try:
 
123
  response = client.chat.completions.create(
124
  model=OPENAI_MODEL,
125
  messages=[
126
  {"role": "system", "content": "You are a supply chain manager playing the Beer Game. Your response must be only an integer number representing your order quantity and nothing else. For example: 8"},
127
  {"role": "user", "content": prompt}
128
  ],
129
- temperature=temp,
130
- max_tokens=10
131
  )
132
  raw_text = response.choices[0].message.content.strip()
133
  match = re.search(r'\d+', raw_text)
134
- if match:
135
- return int(match.group(0)), raw_text
136
- else:
137
- st.warning(f"LLM for {echelon_name} 未返回有效数字,将使用默认值 8。原始返回: '{raw_text}'")
138
- return 8, raw_text
139
  except Exception as e:
140
- st.error(f"API调用失败 for {echelon_name}。错误: {e}。将使用默认值 8。")
141
  return 8, f"API_ERROR: {e}"
142
 
143
  def get_llm_prompt(echelon_state: dict, week: int, llm_personality: str, info_sharing: str, all_echelons_state: dict) -> str:
144
- """生成LLM的提示词 (核心逻辑完全来自代码1)"""
145
  # (此函数内容与上一版完全相同,为简洁省略,实际代码中应完整保留)
146
- base_info = f"""
147
- Your Current Status at the **{echelon_state['name']}** for **Week {week}**:
148
- - On-hand inventory: {echelon_state['inventory']} units.
149
- - Backlog (unfilled orders): {echelon_state['backlog']} units.
150
- - Incoming order this week (from your customer): {echelon_state['incoming_order']} units.
151
- - Shipments on the way to you: {list(echelon_state['incoming_shipments'])}
152
- - Orders you have placed being processed by your supplier: {list(echelon_state['order_pipeline'])}
153
- """
154
- # 场景 1: 完美理性 x 完全信息
155
  if llm_personality == 'perfect_rational' and info_sharing == 'full':
156
  stable_demand = 8; total_lead_time = ORDER_PASSING_DELAY + SHIPPING_DELAY; safety_stock = 4
157
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
158
  inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline']))
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.\n* **Optimal Target Inventory Level:** {target_inventory_level} units.\n* **Mathematically Optimal Order:** The optimal order is **{optimal_order} units**.\n**Your Task:** Confirm this optimal quantity. Respond with a single integer."
161
- # 场景 2: 完美理性 x 本地信息
162
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
163
  safety_stock = 4; anchor_demand = echelon_state['incoming_order']
164
  inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
@@ -166,206 +129,214 @@ Your Current Status at the **{echelon_state['name']}** for **Week {week}**:
166
  calculated_order = anchor_demand + inventory_correction - supply_line
167
  rational_local_order = max(0, int(calculated_order))
168
  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 is {echelon_state['inventory'] - echelon_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for Supply Line:** You already have **{supply_line} units** in transit or being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Order = (Anchor Demand) + (Inventory Adjustment) - (Supply Line)\n* Order = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n\n**Your Task:** Confirm this locally rational quantity. Respond with a single integer."
169
- # 场景 3: 类人 x 完全信息
170
  elif llm_personality == 'human_like' and info_sharing == 'full':
171
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
172
  for name, e_state in all_echelons_state.items():
173
  if name != echelon_state['name']: full_info_str += f"- {name}: Inventory={e_state['inventory']}, Backlog={e_state['backlog']}\n"
174
  return f"**You are a supply chain manager with full visibility across the entire system.**\nYou can see everyone's inventory and the real customer demand. Your goal is to use this information to make a smart, coordinated decision. However, you are still human and might get anxious about your own stock levels.\n{base_info}\n{full_info_str}\n**Your Task:** Look at the full picture, especially the stable end-customer demand. Try to avoid causing the bullwhip effect. However, also consider your own inventory pressure. What quantity should you order this week? Respond with a single integer."
175
- # 场景 4: 类人 x 本地信息
176
  elif llm_personality == 'human_like' and info_sharing == 'local':
177
  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."
178
 
179
- def step_game(human_final_order: int):
180
- """推进一周的游戏进程,并记录详细日志"""
 
181
  state = st.session_state.game_state
182
- week = state['week']
183
- echelons = state['echelons']
184
- human_role = state['human_role']
185
- llm_personality = state['llm_personality']
186
- info_sharing = state['info_sharing']
187
  echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"]
188
  llm_raw_responses = {}
189
 
190
- # --- 游戏流程 ---
191
- # 1. 工厂生产完成 & 2. 各环节接收货物
192
  factory_state = echelons["Factory"]
193
  if state['factory_production_pipeline']: factory_state['inventory'] += state['factory_production_pipeline'].popleft()
194
  for name in ["Retailer", "Wholesaler", "Distributor"]:
195
  if echelons[name]['incoming_shipments']: echelons[name]['inventory'] += echelons[name]['incoming_shipments'].popleft()
196
- # 3. 各环节接收订单
197
  for name in echelon_order:
198
  if name == "Retailer": echelons[name]['incoming_order'] = get_customer_demand(week)
199
  else:
200
  downstream = echelons[name]['downstream_name']
201
  if downstream and echelons[downstream]['order_pipeline']:
202
  echelons[name]['incoming_order'] = echelons[downstream]['order_pipeline'].popleft()
203
- # 4. 满足订单并发货
204
  for name in echelon_order:
205
- e = echelons[name]
206
- demand = e['incoming_order'] + e['backlog']
207
- e['shipment_sent'] = min(e['inventory'], demand)
208
- e['inventory'] -= e['shipment_sent']
209
- e['backlog'] = demand - e['shipment_sent']
210
- # 5. 发货在途
211
  for sender in ["Factory", "Distributor", "Wholesaler"]:
212
  receiver = echelons[sender]['downstream_name']
213
  if receiver: echelons[receiver]['incoming_shipments'].append(echelons[sender]['shipment_sent'])
214
 
215
- # 6. 各环节下订单
216
  for name in echelon_order:
217
  e = echelons[name]
218
  if name == human_role:
219
- order_amount, raw_resp = human_final_order, "HUMAN_INPUT"
220
- st.sidebar.write(f"✔️ 你 ({name}) 的最终订单: {order_amount}")
221
  else:
222
  prompt = get_llm_prompt(e, week, llm_personality, info_sharing, echelons)
223
- order_amount, raw_resp = get_llm_order_decision(prompt, name, week, llm_personality)
224
- st.sidebar.write(f"🤖 AI ({name}) 的订单: {order_amount}")
225
  llm_raw_responses[name] = raw_resp
226
  e['order_placed'] = max(0, order_amount)
227
  if name != "Factory": e['order_pipeline'].append(e['order_placed'])
228
 
229
- # 7. 工厂安排生产
230
  state['factory_production_pipeline'].append(echelons["Factory"]['order_placed'])
231
 
232
- # 8. 更新成本
 
 
233
  for name in echelon_order:
234
  e = echelons[name]
235
- e['weekly_cost'] = (e['inventory'] * HOLDING_COST) + (e['backlog'] * BACKLOG_COST)
236
- e['total_cost'] += e['weekly_cost']
237
-
238
- # 9. 记录详细日志
239
- log_entry = {
240
- 'timestamp': datetime.utcnow().isoformat() + "Z", 'week': week,
241
- 'participant_id': state['participant_id'], 'human_role': human_role,
242
- 'llm_personality': llm_personality, 'info_sharing': info_sharing,
243
- 'customer_demand': get_customer_demand(week),
244
- }
245
- for name in echelon_order:
246
- e = echelons[name]
247
- log_entry[f'{name}.inventory'] = e['inventory']
248
- log_entry[f'{name}.backlog'] = e['backlog']
249
- log_entry[f'{name}.incoming_order'] = e['incoming_order']
250
- log_entry[f'{name}.order_placed'] = e['order_placed']
251
- log_entry[f'{name}.shipment_sent'] = e['shipment_sent']
252
- log_entry[f'{name}.weekly_cost'] = e['weekly_cost']
253
- log_entry[f'{name}.total_cost'] = e['total_cost']
254
  log_entry[f'{name}.llm_raw_response'] = llm_raw_responses.get(name, "")
 
 
 
 
 
255
  state['logs'].append(log_entry)
256
 
257
- # 10. 推进周数
258
  state['week'] += 1
259
- if state['week'] > WEEKS:
260
- state['game_running'] = False
261
 
262
- def plot_results(df: pd.DataFrame, title: str):
263
- """绘制结果图表 (源自代码1)"""
264
- fig, axes = plt.subplots(3, 1, figsize=(12, 16)); fig.suptitle(title, fontsize=16)
 
265
  echelons = ['Retailer', 'Wholesaler', 'Distributor', 'Factory']
266
- # 提取用于绘图的数据
267
  plot_data = []
268
  for _, row in df.iterrows():
269
  for e in echelons:
270
- plot_data.append({
271
- 'week': row['week'], 'echelon': e,
272
  'inventory': row[f'{e}.inventory'], 'order_placed': row[f'{e}.order_placed'],
273
- 'total_cost': row[f'{e}.total_cost']
274
- })
275
  plot_df = pd.DataFrame(plot_data)
276
- # 绘图逻辑 (与之前版本相同)
 
277
  inventory_pivot = plot_df.pivot(index='week', columns='echelon', values='inventory').reindex(columns=echelons)
278
  inventory_pivot.plot(ax=axes[0], kind='line', marker='o', markersize=4); axes[0].set_title('Inventory Levels'); axes[0].grid(True, linestyle='--')
 
 
279
  order_pivot = plot_df.pivot(index='week', columns='echelon', values='order_placed').reindex(columns=echelons)
280
  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()
 
 
281
  total_costs = plot_df.groupby('echelon')['total_cost'].max().reindex(echelons)
282
  total_costs.plot(kind='bar', ax=axes[2], rot=0); axes[2].set_title('Total Cumulative Cost')
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  plt.tight_layout(rect=[0, 0, 1, 0.96]); return fig
284
 
285
- def save_logs_and_upload(state: dict):
286
- """在游戏结束后,保存日志到本地并尝试上传到Hugging Face"""
287
- if not state.get('logs'):
288
- st.warning("没有可保存的日志。")
289
- return
290
 
 
 
 
291
  participant_id = state['participant_id']
292
  df = pd.json_normalize(state['logs'])
293
  fname = LOCAL_LOG_DIR / f"log_{participant_id}_{int(time.time())}.csv"
294
  df.to_csv(fname, index=False)
295
  st.success(f"日志已成功保存到本地: `{fname}`")
296
-
297
- # 提供下载按钮
298
  with open(fname, "rb") as f:
299
  st.download_button("📥 下载日志CSV文件", data=f, file_name=fname.name, mime="text/csv")
300
-
301
- # 尝试上传到Hugging Face
302
  if HF_TOKEN and HF_REPO_ID and hf_api:
303
  with st.spinner("正在上传日志到 Hugging Face Hub..."):
304
  try:
305
- dest_path = f"logs/{fname.name}"
306
- url = hf_api.upload_file(
307
- path_or_fileobj=str(fname),
308
- path_in_repo=dest_path,
309
- repo_id=HF_REPO_ID,
310
- repo_type="dataset",
311
- token=HF_TOKEN
312
- )
313
  st.success(f"✅ 日志已成功上传到 Hugging Face! [查看文件]({url})")
314
  except Exception as e:
315
  st.error(f"上传到 Hugging Face 失败: {e}")
316
- else:
317
- st.info("未配置Hugging Face的 HF_TOKEN 或 HF_REPO_ID, 将跳过上传。")
318
 
319
  # -----------------------------------------------------------------------------
320
  # 4. Streamlit UI 界面
321
  # -----------------------------------------------------------------------------
322
  st.title("🍺 啤酒游戏:人机协作挑战")
323
 
324
- # 检查初始化时是否有错误
325
  if st.session_state.get('initialization_error'):
326
  st.error(st.session_state.initialization_error)
327
  else:
328
- st.markdown("你将扮演供应链中的一个角色,与另外三个由大语言模型(LLM)驱动的AI代理合作。")
329
-
330
  # --- 游戏设置和初始化 ---
331
  if 'game_state' not in st.session_state or not st.session_state.game_state.get('game_running', False):
 
332
  st.header("🎮 开始新游戏")
333
  col1, col2 = st.columns(2)
334
- with col1:
335
- llm_personality = st.selectbox("AI '性格'", ('human_like', 'perfect_rational'), format_func=lambda x: x.replace('_', ' ').title())
336
- with col2:
337
- info_sharing = st.selectbox("信息共享", ('local', 'full'), format_func=lambda x: x.title())
338
  if st.button("🚀 开始游戏", type="primary", disabled=(client is None)):
339
- init_game_state(llm_personality, info_sharing)
340
- st.rerun()
341
 
342
  # --- 游戏主界面 ---
343
  elif 'game_state' in st.session_state and st.session_state.game_state.get('game_running'):
344
  state = st.session_state.game_state
345
- week, human_role, echelons = state['week'], state['human_role'], state['echelons']
 
346
  st.header(f"第 {week} 周 / 共 {WEEKS} 周")
347
  st.subheader(f"你的角色: **{human_role}** | AI模式: **{state['llm_personality'].replace('_', ' ')}** | 信息: **{state['info_sharing']}**")
348
- cols = st.columns(4)
349
- for i, name in enumerate(["Retailer", "Wholesaler", "Distributor", "Factory"]):
350
- with cols[i]:
351
- e, title_icon = echelons[name], "👤" if name == human_role else "🤖"
352
- st.markdown(f"### {title_icon} {name} {'(你)' if name == human_role else '(AI)'}")
353
- st.metric("库存", e['inventory']); st.metric("缺货/积压", e['backlog'])
354
- st.write(f"本周收到订单: **{e['incoming_order']}**")
355
- st.write(f"下周到货: **{list(e['incoming_shipments'])[0] if e['incoming_shipments'] else 0}**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  st.markdown("---")
 
 
357
  st.header("你的决策")
358
  human_echelon_state = echelons[human_role]
359
- prompt_sugg = get_llm_prompt(human_echelon_state, week, state['llm_personality'], state['info_sharing'], echelons)
360
- ai_suggestion, _ = get_llm_order_decision(prompt_sugg, f"{human_role} (Suggestion)", week, state['llm_personality'])
361
- st.info(f"💡 AI建议你 ({human_role}) 本周向上游订购 **{ai_suggestion}** 单位。")
362
- with st.form(key="order_form"):
363
- final_order = st.number_input("请输入你的最终订单数量:", min_value=0, step=1, value=ai_suggestion)
364
- if st.form_submit_button(label=" 提交订单并进入下一周"):
365
- step_game(int(final_order)); st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  st.sidebar.header("游戏信息")
367
- st.sidebar.markdown(f"**游戏ID**: `{state['participant_id']}`")
368
- st.sidebar.markdown(f"**当前周**: {week-1} (已完成)")
369
  if st.sidebar.button("🔄 重置游戏"):
370
  del st.session_state.game_state; st.rerun()
371
 
@@ -374,10 +345,8 @@ else:
374
  st.header("🎉 游戏结束!")
375
  state = st.session_state.game_state
376
  logs_df = pd.json_normalize(state['logs'])
377
- title = f"Beer Game (Human: {state['human_role']})\n(AI: {state['llm_personality'].replace('_', ' ').title()} | Info: {state['info_sharing'].title()})"
378
- fig = plot_results(logs_df, title)
379
  st.pyplot(fig)
380
- # 保存并上传日志
381
  save_logs_and_upload(state)
382
  if st.button("✨ 开始一局新游戏"):
383
  del st.session_state.game_state; st.rerun()
 
1
  # app.py
2
+ # @title 啤酒游戏最终整合版 (v3 - 两阶段决策 + 信息隔离)
3
 
4
  # -----------------------------------------------------------------------------
5
  # 1. 导入必要的库
 
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
  # -----------------------------------------------------------------------------
 
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]
69
 
70
  st.session_state.game_state = {
71
+ 'game_running': True, 'participant_id': participant_id, 'week': 1,
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):
80
  upstream = roles[i + 1] if i + 1 < len(roles) else None
81
  downstream = roles[i - 1] if i - 1 >= 0 else None
 
84
  else: shipping_weeks = SHIPPING_DELAY
85
 
86
  st.session_state.game_state['echelons'][name] = {
87
+ 'name': name, 'inventory': INITIAL_INVENTORY, 'backlog': INITIAL_BACKLOG,
 
88
  'order_pipeline': deque([0] * ORDER_PASSING_DELAY, maxlen=ORDER_PASSING_DELAY),
89
  'incoming_shipments': deque([0] * shipping_weeks, maxlen=shipping_weeks),
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(
101
  model=OPENAI_MODEL,
102
  messages=[
103
  {"role": "system", "content": "You are a supply chain manager playing the Beer Game. Your response must be only an integer number representing your order quantity and nothing else. For example: 8"},
104
  {"role": "user", "content": prompt}
105
  ],
106
+ temperature=temp, max_tokens=10
 
107
  )
108
  raw_text = response.choices[0].message.content.strip()
109
  match = re.search(r'\d+', raw_text)
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
121
  target_inventory_level = (stable_demand * total_lead_time) + safety_stock
122
  inventory_position = (echelon_state['inventory'] - echelon_state['backlog'] + sum(echelon_state['incoming_shipments']) + sum(echelon_state['order_pipeline']))
123
  optimal_order = max(0, int(target_inventory_level - inventory_position))
124
  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.\n* **Optimal Target Inventory Level:** {target_inventory_level} units.\n* **Mathematically Optimal Order:** The optimal order is **{optimal_order} units**.\n**Your Task:** Confirm this optimal quantity. Respond with a single integer."
 
125
  elif llm_personality == 'perfect_rational' and info_sharing == 'local':
126
  safety_stock = 4; anchor_demand = echelon_state['incoming_order']
127
  inventory_correction = safety_stock - (echelon_state['inventory'] - echelon_state['backlog'])
 
129
  calculated_order = anchor_demand + inventory_correction - supply_line
130
  rational_local_order = max(0, int(calculated_order))
131
  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 is {echelon_state['inventory'] - echelon_state['backlog']}. You need to order an extra **{inventory_correction} units** to correct this.\n3. **Account for Supply Line:** You already have **{supply_line} units** in transit or being processed. These should be subtracted from your new order.\n\n**Final Calculation:**\n* Order = (Anchor Demand) + (Inventory Adjustment) - (Supply Line)\n* Order = {anchor_demand} + {inventory_correction} - {supply_line} = **{rational_local_order} units**.\n\n**Your Task:** Confirm this locally rational quantity. Respond with a single integer."
 
132
  elif llm_personality == 'human_like' and info_sharing == 'full':
133
  full_info_str = f"\n**Full Supply Chain Information:**\n- End-Customer Demand this week: {get_customer_demand(week)} units.\n"
134
  for name, e_state in all_echelons_state.items():
135
  if name != echelon_state['name']: full_info_str += f"- {name}: Inventory={e_state['inventory']}, Backlog={e_state['backlog']}\n"
136
  return f"**You are a supply chain manager with full visibility across the entire system.**\nYou can see everyone's inventory and the real customer demand. Your goal is to use this information to make a smart, coordinated decision. However, you are still human and might get anxious about your own stock levels.\n{base_info}\n{full_info_str}\n**Your Task:** Look at the full picture, especially the stable end-customer demand. Try to avoid causing the bullwhip effect. However, also consider your own inventory pressure. What quantity should you order this week? Respond with a single integer."
 
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"]:
153
  if echelons[name]['incoming_shipments']: echelons[name]['inventory'] += echelons[name]['incoming_shipments'].popleft()
 
154
  for name in echelon_order:
155
  if name == "Retailer": echelons[name]['incoming_order'] = get_customer_demand(week)
156
  else:
157
  downstream = echelons[name]['downstream_name']
158
  if downstream and echelons[downstream]['order_pipeline']:
159
  echelons[name]['incoming_order'] = echelons[downstream]['order_pipeline'].popleft()
 
160
  for name in echelon_order:
161
+ e = echelons[name]; demand = e['incoming_order'] + e['backlog']
162
+ e['shipment_sent'] = min(e['inventory'], demand); e['inventory'] -= e['shipment_sent']; e['backlog'] = demand - e['shipment_sent']
 
 
 
 
163
  for sender in ["Factory", "Distributor", "Wholesaler"]:
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:
171
+ order_amount, raw_resp = human_final_order, "HUMAN_FINAL_INPUT"
 
172
  else:
173
  prompt = get_llm_prompt(e, week, llm_personality, info_sharing, echelons)
174
+ order_amount, raw_resp = get_llm_order_decision(prompt, name)
 
175
  llm_raw_responses[name] = raw_resp
176
  e['order_placed'] = max(0, order_amount)
177
  if name != "Factory": e['order_pipeline'].append(e['order_placed'])
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']
187
+ for key in ['inventory', 'backlog', 'incoming_order', 'order_placed', 'shipment_sent', 'weekly_cost', 'total_cost']:
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
+
208
  plot_data = []
209
  for _, row in df.iterrows():
210
  for e in echelons:
211
+ plot_data.append({'week': row['week'], 'echelon': e,
 
212
  'inventory': row[f'{e}.inventory'], 'order_placed': row[f'{e}.order_placed'],
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
 
 
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()