Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -71,11 +71,11 @@ else:
|
|
| 71 |
def get_customer_demand(week: int) -> int:
|
| 72 |
return 4 if week <= 4 else 8
|
| 73 |
|
| 74 |
-
# =============== MODIFIED Initialization (
|
| 75 |
def init_game_state(llm_personality: str, info_sharing: str, participant_id: str):
|
| 76 |
roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 77 |
human_role = "Distributor" # Role is fixed
|
| 78 |
-
|
| 79 |
st.session_state.game_state = {
|
| 80 |
'game_running': True,
|
| 81 |
'participant_id': participant_id,
|
|
@@ -86,7 +86,8 @@ def init_game_state(llm_personality: str, info_sharing: str, participant_id: str
|
|
| 86 |
'decision_step': 'initial_order',
|
| 87 |
'human_initial_order': None,
|
| 88 |
'current_ai_suggestion': None, # v4.23 Bugfix: 用于存储AI建议
|
| 89 |
-
'last_week_orders': {name: 0 for name in roles} # v4.21 Logic: 初始化为0
|
|
|
|
| 90 |
}
|
| 91 |
for i, name in enumerate(roles):
|
| 92 |
upstream = roles[i + 1] if i + 1 < len(roles) else None
|
|
@@ -240,7 +241,7 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
|
|
| 240 |
A 'rational' player would account for their {supply_line_desc} (which is {supply_line} units), but you're too busy panicking to trust that.
|
| 241 |
**Your 'Panic' Calculation (Ignoring the Supply Line):**
|
| 242 |
1. **Anchor on Demand:** You just got an order for **{anchor_demand}** units. You'll order *at least* that.
|
| 243 |
-
2. **Correct for Stock:** Your desired 'safe' inventory is {DESIRED_INVENTORY}. Your current net inventory is {net_inventory}. You need to order **{stock_correction}** more units to feel safe again.
|
| 244 |
3. **Ignore Supply Line:** You'll ignore the **{supply_line} units** already in your pipeline.
|
| 245 |
**Final Panic Order:** (Your Incoming Order) + (Your Stock Correction)
|
| 246 |
* Order = {panicky_order_calc} = **{panicky_order} units**.
|
|
@@ -274,7 +275,7 @@ def get_llm_prompt(echelon_state_decision_point: dict, week: int, llm_personalit
|
|
| 274 |
**Your 'Panic' Calculation (Ignoring Supply Line, Averaging Anchors):**
|
| 275 |
1. **Anchor on Conflict:** (Your Incoming Order + End-Customer Demand) / 2
|
| 276 |
* Anchor = ({local_anchor} + {global_anchor}) / 2 = **{anchor_demand}** units.
|
| 277 |
-
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.
|
| 278 |
3. **Ignore *Your* Supply Line:** You'll ignore the **{supply_line} units** in your own pipeline ({supply_line_desc}).
|
| 279 |
**Final Panic Order:** (Conflicted Anchor) + (Your Stock Correction)
|
| 280 |
* Order = {panicky_order_calc} = **{panicky_order} units**.
|
|
@@ -540,6 +541,13 @@ def save_logs_and_upload(state: dict):
|
|
| 540 |
logs_df = pd.json_normalize(state['logs'])
|
| 541 |
safe_participant_id = re.sub(r'[^a-zA-Z0-9_-]', '_', participant_id)
|
| 542 |
fname = LOCAL_LOG_DIR / f"log_{safe_participant_id}_{int(time.time())}.csv"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
for col in logs_df.select_dtypes(include=['object']).columns: logs_df[col] = logs_df[col].astype(str)
|
| 544 |
logs_df.to_csv(fname, index=False)
|
| 545 |
st.success(f"Log successfully saved locally: `{fname}`")
|
|
@@ -580,7 +588,13 @@ def save_logs_and_upload(state: dict):
|
|
| 580 |
'total_chain_cost': float(total_chain_cost), # 新增: 总成本
|
| 581 |
'order_std_dev': float(order_std_dev) if pd.notna(order_std_dev) else 0.0
|
| 582 |
}
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
leaderboard_data = load_leaderboard_data()
|
| 585 |
leaderboard_data[participant_id] = new_entry
|
| 586 |
save_leaderboard_data(leaderboard_data)
|
|
@@ -596,13 +610,17 @@ st.title("🍺 The Beer Game: A Human-AI Collaboration Challenge")
|
|
| 596 |
# --- NEW: Check for Consent ---
|
| 597 |
if 'consent_given' not in st.session_state:
|
| 598 |
st.session_state['consent_given'] = False
|
|
|
|
|
|
|
|
|
|
| 599 |
|
| 600 |
# --- Initialization Check ---
|
| 601 |
if st.session_state.get('initialization_error'):
|
| 602 |
st.error(st.session_state.initialization_error)
|
|
|
|
| 603 |
# --- Consent Form Display Logic ---
|
| 604 |
elif not st.session_state['consent_given']:
|
| 605 |
-
st.header("📝 Participant Consent Form")
|
| 606 |
st.markdown("""
|
| 607 |
**Project Title:** Behavioural Contagion or Rational Alignment? Human Performance in LLM Supply Chains
|
| 608 |
|
|
@@ -626,20 +644,29 @@ elif not st.session_state['consent_given']:
|
|
| 626 |
|
| 627 |
st.markdown("---")
|
| 628 |
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
# --- Game Setup / Main Interface Logic ---
|
| 645 |
else:
|
|
@@ -703,7 +730,13 @@ else:
|
|
| 703 |
state = st.session_state.game_state
|
| 704 |
week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
|
| 705 |
echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
st.header(f"Week {week} / {WEEKS}")
|
|
|
|
| 707 |
st.subheader(f"Your Role: **{human_role}** ({state['participant_id']})")
|
| 708 |
st.markdown("---")
|
| 709 |
st.subheader("Supply Chain Status (Start of Week State)")
|
|
@@ -880,5 +913,8 @@ else:
|
|
| 880 |
st.error(f"Error generating final report: {e}")
|
| 881 |
show_leaderboard_ui()
|
| 882 |
if st.button("✨ Start a New Game"):
|
|
|
|
|
|
|
|
|
|
| 883 |
del st.session_state.game_state
|
| 884 |
st.rerun()
|
|
|
|
| 71 |
def get_customer_demand(week: int) -> int:
|
| 72 |
return 4 if week <= 4 else 8
|
| 73 |
|
| 74 |
+
# =============== MODIFIED Initialization (记录开始时间) ===============
|
| 75 |
def init_game_state(llm_personality: str, info_sharing: str, participant_id: str):
|
| 76 |
roles = ["Retailer", "Wholesaler", "Distributor", "Factory"]
|
| 77 |
human_role = "Distributor" # Role is fixed
|
| 78 |
+
|
| 79 |
st.session_state.game_state = {
|
| 80 |
'game_running': True,
|
| 81 |
'participant_id': participant_id,
|
|
|
|
| 86 |
'decision_step': 'initial_order',
|
| 87 |
'human_initial_order': None,
|
| 88 |
'current_ai_suggestion': None, # v4.23 Bugfix: 用于存储AI建议
|
| 89 |
+
'last_week_orders': {name: 0 for name in roles}, # v4.21 Logic: 初始化为0
|
| 90 |
+
'start_timestamp': datetime.utcnow().isoformat() + "Z", # [修改点 3]: 记录实验开始时间
|
| 91 |
}
|
| 92 |
for i, name in enumerate(roles):
|
| 93 |
upstream = roles[i + 1] if i + 1 < len(roles) else None
|
|
|
|
| 241 |
A 'rational' player would account for their {supply_line_desc} (which is {supply_line} units), but you're too busy panicking to trust that.
|
| 242 |
**Your 'Panic' Calculation (Ignoring the Supply Line):**
|
| 243 |
1. **Anchor on Demand:** You just got an order for **{anchor_demand}** units. You'll order *at least* that.
|
| 244 |
+
2. **Correct for Stock:** Your desired 'safe' inventory is {DESIRED_INVENTORY}. Your current net inventory is {net_inventory}. You need to order an extra **{stock_correction}** more units to feel safe again.
|
| 245 |
3. **Ignore Supply Line:** You'll ignore the **{supply_line} units** already in your pipeline.
|
| 246 |
**Final Panic Order:** (Your Incoming Order) + (Your Stock Correction)
|
| 247 |
* Order = {panicky_order_calc} = **{panicky_order} units**.
|
|
|
|
| 275 |
**Your 'Panic' Calculation (Ignoring Supply Line, Averaging Anchors):**
|
| 276 |
1. **Anchor on Conflict:** (Your Incoming Order + End-Customer Demand) / 2
|
| 277 |
* Anchor = ({local_anchor} + {global_anchor}) / 2 = **{anchor_demand}** units.
|
| 278 |
+
2. **Correct for *Your* Stock:** Your desired 'safe' inventory is {DESIRED_INVENTORY}. Your current net inventory is {net_inventory}. You need to order an extra **{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 |
**Final Panic Order:** (Conflicted Anchor) + (Your Stock Correction)
|
| 281 |
* Order = {panicky_order_calc} = **{panicky_order} units**.
|
|
|
|
| 541 |
logs_df = pd.json_normalize(state['logs'])
|
| 542 |
safe_participant_id = re.sub(r'[^a-zA-Z0-9_-]', '_', participant_id)
|
| 543 |
fname = LOCAL_LOG_DIR / f"log_{safe_participant_id}_{int(time.time())}.csv"
|
| 544 |
+
# [修改点 4]: 在 logs_df 中添加实验结束时间戳和同意时间戳
|
| 545 |
+
logs_df['experiment_end_timestamp'] = datetime.utcnow().isoformat() + "Z"
|
| 546 |
+
if st.session_state.get('consent_timestamp'):
|
| 547 |
+
logs_df['consent_given_timestamp'] = st.session_state['consent_timestamp']
|
| 548 |
+
else:
|
| 549 |
+
logs_df['consent_given_timestamp'] = "N/A"
|
| 550 |
+
|
| 551 |
for col in logs_df.select_dtypes(include=['object']).columns: logs_df[col] = logs_df[col].astype(str)
|
| 552 |
logs_df.to_csv(fname, index=False)
|
| 553 |
st.success(f"Log successfully saved locally: `{fname}`")
|
|
|
|
| 588 |
'total_chain_cost': float(total_chain_cost), # 新增: 总成本
|
| 589 |
'order_std_dev': float(order_std_dev) if pd.notna(order_std_dev) else 0.0
|
| 590 |
}
|
| 591 |
+
|
| 592 |
+
# [修改点 3]: 将实验开始时间戳添加到 Leaderboard entry 中 (仅用于追溯,不参与排名)
|
| 593 |
+
if state.get('start_timestamp'):
|
| 594 |
+
new_entry['start_timestamp'] = state['start_timestamp']
|
| 595 |
+
if st.session_state.get('consent_timestamp'):
|
| 596 |
+
new_entry['consent_timestamp'] = st.session_state['consent_timestamp']
|
| 597 |
+
|
| 598 |
leaderboard_data = load_leaderboard_data()
|
| 599 |
leaderboard_data[participant_id] = new_entry
|
| 600 |
save_leaderboard_data(leaderboard_data)
|
|
|
|
| 610 |
# --- NEW: Check for Consent ---
|
| 611 |
if 'consent_given' not in st.session_state:
|
| 612 |
st.session_state['consent_given'] = False
|
| 613 |
+
# --- NEW: Storage for Consent Timestamp ---
|
| 614 |
+
if 'consent_timestamp' not in st.session_state:
|
| 615 |
+
st.session_state['consent_timestamp'] = None
|
| 616 |
|
| 617 |
# --- Initialization Check ---
|
| 618 |
if st.session_state.get('initialization_error'):
|
| 619 |
st.error(st.session_state.initialization_error)
|
| 620 |
+
|
| 621 |
# --- Consent Form Display Logic ---
|
| 622 |
elif not st.session_state['consent_given']:
|
| 623 |
+
st.header("📝 Participant Consent Form (参与者知情同意书)")
|
| 624 |
st.markdown("""
|
| 625 |
**Project Title:** Behavioural Contagion or Rational Alignment? Human Performance in LLM Supply Chains
|
| 626 |
|
|
|
|
| 644 |
|
| 645 |
st.markdown("---")
|
| 646 |
|
| 647 |
+
# 使用 st.form 确保按钮点击后不会清空 st.radio 的值
|
| 648 |
+
with st.form("consent_form"):
|
| 649 |
+
consent_choice = st.radio(
|
| 650 |
+
"**Do you agree to take part in this study?**",
|
| 651 |
+
('Yes, I agree to participate in this study.', 'No, I do not agree to participate in this study.'),
|
| 652 |
+
index=None,
|
| 653 |
+
key='consent_radio'
|
| 654 |
+
)
|
| 655 |
+
submit_button = st.form_submit_button("Continue / 提交")
|
| 656 |
+
|
| 657 |
+
if submit_button:
|
| 658 |
+
if consent_choice == 'Yes, I agree to participate in this study.':
|
| 659 |
+
# [修正点 2]: 记录时间戳并允许进入
|
| 660 |
+
st.session_state['consent_given'] = True
|
| 661 |
+
st.session_state['consent_timestamp'] = datetime.utcnow().isoformat() + "Z"
|
| 662 |
+
st.rerun()
|
| 663 |
+
elif consent_choice == 'No, I do not agree to participate in this study.':
|
| 664 |
+
# [修正点 1]: 选择 No 后显示感谢信息并停止流程
|
| 665 |
+
st.error("Thank you for your time. Since you declined participation, the experiment will not proceed. You may now close this page.")
|
| 666 |
+
st.session_state['consent_given'] = False # 保持为 False,防止意外进入
|
| 667 |
+
else:
|
| 668 |
+
st.warning("Please select an option to continue.")
|
| 669 |
+
|
| 670 |
|
| 671 |
# --- Game Setup / Main Interface Logic ---
|
| 672 |
else:
|
|
|
|
| 730 |
state = st.session_state.game_state
|
| 731 |
week, human_role, echelons, info_sharing = state['week'], state['human_role'], state['echelons'], state['info_sharing']
|
| 732 |
echelon_order = ["Retailer", "Wholesaler", "Distributor", "Factory"] # Define here for UI
|
| 733 |
+
|
| 734 |
+
# [修改点 1.1]: 移除 st.columns,恢复主内容到左侧,侧边栏不变
|
| 735 |
+
# col_main, col_sidebar_image = st.columns([4, 1]) 移除
|
| 736 |
+
|
| 737 |
+
# with col_main: 移除
|
| 738 |
st.header(f"Week {week} / {WEEKS}")
|
| 739 |
+
# [修改点 2.3]: 移除 subheader 中 AI Mode 和 Information 的提示
|
| 740 |
st.subheader(f"Your Role: **{human_role}** ({state['participant_id']})")
|
| 741 |
st.markdown("---")
|
| 742 |
st.subheader("Supply Chain Status (Start of Week State)")
|
|
|
|
| 913 |
st.error(f"Error generating final report: {e}")
|
| 914 |
show_leaderboard_ui()
|
| 915 |
if st.button("✨ Start a New Game"):
|
| 916 |
+
# 在重置游戏时清除所有与实验相关的时间戳
|
| 917 |
+
if 'consent_timestamp' in st.session_state: del st.session_state['consent_timestamp']
|
| 918 |
+
if 'consent_given' in st.session_state: del st.session_state['consent_given']
|
| 919 |
del st.session_state.game_state
|
| 920 |
st.rerun()
|