Lilli98 commited on
Commit
9527609
·
verified ·
1 Parent(s): 5fcc22d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -355
app.py CHANGED
@@ -1,368 +1,168 @@
1
  # app.py
2
- """
3
- Beer Game (Streamlit) for Hugging Face Spaces
4
- - Multi-participant support (participant_id)
5
- - Per-participant game state
6
- - Per-participant CSV logs saved server-side
7
- - Optional export to S3 (AWS creds via env/secrets)
8
- - LLM agents use OpenAI gpt-4o-mini
9
- """
10
-
11
  import os
12
- import re
13
  import time
14
- from datetime import datetime
15
- import io
16
  import uuid
 
17
  import pandas as pd
18
  import streamlit as st
19
- from dotenv import load_dotenv
20
-
21
- # Load local .env if present (spaces will inject secrets into env)
22
- load_dotenv()
23
-
24
- # OpenAI
25
  import openai
26
-
27
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
28
- if not OPENAI_API_KEY:
29
- st.warning("OPENAI_API_KEY not found. Set OPENAI_API_KEY secret in the Space settings.")
30
- openai.api_key = OPENAI_API_KEY
31
-
32
- # Optional S3 (boto3)
33
- USE_S3 = False
34
- try:
35
- import boto3
36
-
37
- AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
38
- AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
39
- S3_BUCKET = os.getenv("S3_BUCKET")
40
- S3_REGION = os.getenv("S3_REGION", "us-east-1")
41
-
42
- if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY and S3_BUCKET:
43
- USE_S3 = True
44
- except Exception:
45
- USE_S3 = False
46
-
47
- # ---------------- Config ----------------
48
- ROLES = ["factory", "wholesaler", "distributor", "retailer"]
49
- AGENT_ROLES = ["factory", "wholesaler", "retailer"]
50
- MODEL_NAME = "gpt-4o-mini"
51
-
52
- DEFAULT_INITIAL_INVENTORY = 12
53
- PIPELINE_WEEKS = 2
54
- DEFAULT_DEMAND_PRE = 4
55
- DEMAND_SHOCK_FROM_WEEK = 6
56
- DEMAND_SHOCK_VALUE = 8
57
- DEFAULT_TOTAL_WEEKS = 24
58
-
59
- DATA_DIR = "logs" # local folder to store per-participant csvs
60
-
61
- # Ensure logs dir exists
62
- if not os.path.exists(DATA_DIR):
63
- os.makedirs(DATA_DIR, exist_ok=True)
64
-
65
-
66
- # ---------------- Utilities ----------------
67
- def now_ts():
68
- return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + " UTC"
69
-
70
-
71
- def init_game_state(total_weeks=DEFAULT_TOTAL_WEEKS):
72
- game = {}
73
- for r in ROLES:
74
- game[r] = {
75
- "inventory": DEFAULT_INITIAL_INVENTORY,
76
- "backlog": 0,
77
- "incoming_shipments": [0] * PIPELINE_WEEKS,
78
- "incoming_order": DEFAULT_DEMAND_PRE if r == "retailer" else 0,
79
- "past_orders": [],
80
- "demand_history": [],
81
- }
82
- demand = [DEFAULT_DEMAND_PRE] * (DEMAND_SHOCK_FROM_WEEK - 1) + \
83
- [DEMAND_SHOCK_VALUE] * (total_weeks - (DEMAND_SHOCK_FROM_WEEK - 1))
84
- game["week"] = 1
85
- game["max_weeks"] = total_weeks
86
- game["customer_demand"] = demand
87
- game["logs"] = []
88
- return game
89
-
90
-
91
- def log_row(role, week, order, state, raw, info_sharing_flag, action_by):
92
  return {
93
- "timestamp": now_ts(),
94
- "week": int(week),
95
- "role": role,
96
- "action_by": action_by,
97
- "order_submitted": int(order),
98
- "inventory": int(state.get("inventory", 0)),
99
- "backlog": int(state.get("backlog", 0)),
100
- "incoming_order": int(state.get("incoming_order", 0)),
101
- "incoming_shipment_next_week": int(state.get("incoming_shipments", [0])[0]),
102
- "info_sharing": bool(info_sharing_flag),
103
- "raw_output": str(raw),
104
  }
105
 
106
-
107
- def append_log(game, row):
108
- game["logs"].append(row)
109
-
110
-
111
- def logs_to_df(game):
112
- return pd.DataFrame(game["logs"])
113
-
114
-
115
- # ---------------- OpenAI call ----------------
116
- def call_openai_for_order(role, state, model=MODEL_NAME, max_tokens=40, temperature=0.7):
117
- prompt = (
118
- f"You are the {role} (LLM agent) in a 4-player Beer Game.\n\n"
119
- f"Local state:\n"
120
- f"- Inventory: {state['inventory']}\n"
121
- f"- Backlog: {state['backlog']}\n"
122
- f"- Incoming shipment next week (front): {state['incoming_shipments'][0]}\n"
123
- f"- Incoming order this week: {state['incoming_order']}\n"
124
- f"- Past orders (last 5): {state['past_orders'][-5:]}\n"
125
- f"- Customer demand history (visible): {state.get('demand_history', [])}\n\n"
126
- f"Decide a non-negative integer order quantity to place to your upstream supplier this week.\n"
127
- f"Reply with a single integer only (you may append a brief reason after a dash)."
128
- )
129
- raw = ""
130
  try:
131
- resp = openai.ChatCompletion.create(
132
- model=model,
133
- messages=[
134
- {"role": "system", "content": "You are a beer game decision-making agent."},
135
- {"role": "user", "content": prompt},
136
- ],
137
- max_tokens=max_tokens,
138
- temperature=temperature,
139
- n=1,
140
  )
141
- raw = resp.choices[0].message.get("content", "").strip()
142
  except Exception as e:
143
- raw = f"OPENAI_ERROR: {str(e)}"
144
-
145
- # extract integer
146
- m = re.search(r"(-?\d+)", raw)
147
- order = None
148
- if m:
149
- try:
150
- order = int(m.group(1))
151
- if order < 0:
152
- order = 0
153
- except Exception:
154
- order = None
155
-
156
- if order is None:
157
- target = DEFAULT_INITIAL_INVENTORY + (state.get("incoming_order") or 0)
158
- order = max(0, target - (state.get("inventory") or 0))
159
- raw = (raw + " | PARSE_FALLBACK").strip()
160
-
161
- return int(order), raw
162
-
163
-
164
- # ---------------- Game mechanics ----------------
165
- def advance_week(game, participant_id, human_order_qty, info_sharing_flag, demand_history_length):
166
- week = game["week"]
167
- roles = ROLES
168
-
169
- # Record distributor (human)
170
- d_state = game["distributor"]
171
- d_state["past_orders"].append(int(human_order_qty))
172
- append_log(game, log_row("distributor", week, int(human_order_qty), d_state, "HUMAN", info_sharing_flag, "HUMAN"))
173
-
174
- # Make demand history visible if info sharing enabled
175
- if info_sharing_flag:
176
- for r in ROLES:
177
- game[r]["demand_history"] = game["customer_demand"][max(0, week - demand_history_length): week]
178
-
179
- # LLM agents decide
180
- for role in AGENT_ROLES:
181
- state = game[role]
182
- order_val, raw = call_openai_for_order(role, state)
183
- state["past_orders"].append(int(order_val))
184
- append_log(game, log_row(role, week, int(order_val), state, raw, info_sharing_flag, "LLM"))
185
-
186
- # Demand arrives at retailer this week
187
- demand = game["customer_demand"][week - 1]
188
- game["retailer"]["incoming_order"] = demand
189
-
190
- # Propagate orders downstream
191
- for i in range(len(roles) - 1, 0, -1):
192
- downstream = roles[i]
193
- upstream = roles[i - 1]
194
- latest_downstream_order = game[downstream]["past_orders"][-1] if game[downstream]["past_orders"] else 0
195
- game[upstream]["incoming_order"] = latest_downstream_order
196
-
197
- # Shipments flow (2-week pipeline)
198
- for r in roles:
199
- st_state = game[r]
200
- arriving = st_state["incoming_shipments"].pop(0) if st_state["incoming_shipments"] else 0
201
- if st_state["backlog"] > 0 and arriving > 0:
202
- fulfilled = min(arriving, st_state["backlog"])
203
- st_state["backlog"] -= fulfilled
204
- arriving -= fulfilled
205
- st_state["inventory"] += arriving
206
-
207
- order_to_fill = st_state["incoming_order"] or 0
208
- shipped = min(st_state["inventory"], order_to_fill)
209
- st_state["inventory"] -= shipped
210
- idx = ROLES.index(r)
211
- if idx < len(ROLES) - 1:
212
- downstream_role = ROLES[idx + 1]
213
- game[downstream_role]["incoming_shipments"][-1] += shipped
214
-
215
- if order_to_fill > shipped:
216
- st_state["backlog"] += (order_to_fill - shipped)
217
-
218
- # increment week
219
- game["week"] += 1
220
-
221
-
222
- # ---------------- Persistence ----------------
223
- def local_save_logs(participant_id, game):
224
- df = logs_to_df(game)
225
- fname = os.path.join(DATA_DIR, f"logs_{participant_id}.csv")
226
- df.to_csv(fname, index=False)
227
- return fname
228
-
229
-
230
- def s3_upload_file(local_path, s3_key):
231
- if not USE_S3:
232
- raise RuntimeError("S3 not configured (missing AWS credentials or S3_BUCKET).")
233
- s3 = boto3.client(
234
- "s3",
235
- aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
236
- aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
237
- region_name=os.getenv("S3_REGION", "us-east-1"),
238
- )
239
- s3.upload_file(local_path, os.getenv("S3_BUCKET"), s3_key)
240
- s3_url = f"s3://{os.getenv('S3_BUCKET')}/{s3_key}"
241
- return s3_url
242
-
243
-
244
- # ---------------- Streamlit UI (multi-session) ----------------
245
- st.set_page_config(page_title="Beer Game — Multi-Participant", layout="wide")
246
-
247
- st.title("Beer Game — Distributor (Multi-Participant Sessions)")
248
- st.caption("Deploy on Hugging Face Spaces. LLM agents powered by OpenAI gpt-4o-mini.")
249
-
250
- # Participant ID handling: URL query or input
251
- query_params = st.experimental_get_query_params()
252
- default_pid = query_params.get("participant_id", [None])[0]
253
-
254
- participant_col, info_col = st.columns([2, 1])
255
- with participant_col:
256
- st.markdown("### Participant identification")
257
- pid_input = st.text_input("Enter participant ID (leave blank to auto-generate):", value=default_pid or "")
258
- if pid_input == "":
259
- if "auto_pid" not in st.session_state:
260
- st.session_state["auto_pid"] = str(uuid.uuid4())[:8]
261
- pid_display = st.session_state["auto_pid"]
262
- st.info(f"Auto-generated participant_id: **{pid_display}** — you can copy this or enter your own.")
263
- participant_id = pid_display
264
- else:
265
- participant_id = pid_input.strip()
266
-
267
- with info_col:
268
- st.markdown("### Space secrets & persistence")
269
- if USE_S3:
270
- st.success("S3 export ENABLED")
271
- st.write(f"Bucket: `{os.getenv('S3_BUCKET')}`")
272
- else:
273
- st.info("S3 export not configured. To enable, set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and S3_BUCKET secrets.")
274
-
275
- # Initialize sessions container
276
- if "sessions" not in st.session_state:
277
- st.session_state["sessions"] = {}
278
-
279
- # Create session for participant if not exists
280
- if participant_id not in st.session_state["sessions"]:
281
- st.session_state["sessions"][participant_id] = init_game_state()
282
-
283
- game = st.session_state["sessions"][participant_id]
284
- week = game["week"]
285
-
286
- # Sidebar controls (per participant)
287
- st.sidebar.header("Experiment controls")
288
- st.sidebar.write(f"Participant: **{participant_id}**")
289
- st.sidebar.checkbox("Enable Information Sharing (visible to all roles)", key=f"info_sharing_{participant_id}")
290
- st.sidebar.slider("Demand history visible (weeks)", 0, game["max_weeks"], 2, key=f"demand_history_length_{participant_id}")
291
- st.sidebar.markdown("---")
292
- st.sidebar.write("Model for LLM agents:")
293
- st.sidebar.write(MODEL_NAME)
294
- st.sidebar.markdown("---")
295
- st.sidebar.write("Persistence:")
296
- st.sidebar.write(f"- Local logs stored in `{DATA_DIR}/logs_{participant_id}.csv`")
297
-
298
- # Display local state
299
- st.subheader(f"Participant `{participant_id}` — Week {week} / {game['max_weeks']}")
300
- dist = game["distributor"]
301
- left, right = st.columns([2, 1])
302
-
303
- with left:
304
- st.markdown("#### Your (Distributor) Local State")
305
- st.write(f"- Inventory: **{dist['inventory']}**")
306
- st.write(f"- Backlog: **{dist['backlog']}**")
307
- st.write(f"- Incoming order (this week): **{dist['incoming_order']}**")
308
- st.write(f"- Incoming shipment next week: **{dist['incoming_shipments'][0] if dist['incoming_shipments'] else 0}**")
309
-
310
- if st.session_state.get(f"info_sharing_{participant_id}", False):
311
- hlen = st.session_state.get(f"demand_history_length_{participant_id}", 2)
312
- shown = game["customer_demand"][max(0, week - hlen - 1): week-1] if week > 1 else []
313
- st.markdown("**Customer demand history (visible):**")
314
- st.write(shown)
315
-
316
- st.markdown("---")
317
- order_qty = st.number_input("Place your order to the Wholesaler (units):", min_value=0, step=1, value=dist.get("incoming_order", DEFAULT_DEMAND_PRE), key=f"order_input_{participant_id}")
318
- submit = st.button("Submit Order and Advance Week", key=f"submit_{participant_id}")
319
-
320
- with right:
321
- st.markdown("#### Logs (recent)")
322
- if game["logs"]:
323
- df_tail = logs_to_df(game).tail(10)
324
- st.dataframe(df_tail)
325
- else:
326
- st.write("No actions logged yet.")
327
-
328
- st.markdown("---")
329
- if st.button("Save logs locally now", key=f"save_local_{participant_id}"):
330
- saved_path = local_save_logs(participant_id, game)
331
- st.success(f"Saved local logs to `{saved_path}`")
332
- st.write(f"Filesize: {os.path.getsize(saved_path)} bytes")
333
-
334
- if USE_S3:
335
- if st.button("Upload latest logs to S3", key=f"upload_s3_{participant_id}"):
336
- try:
337
- saved_path = local_save_logs(participant_id, game)
338
- s3_key = f"beer_game_logs/{participant_id}/logs_{participant_id}_{int(time.time())}.csv"
339
- url = s3_upload_file(saved_path, s3_key)
340
- st.success(f"Uploaded to S3: `{url}`")
341
- except Exception as e:
342
- st.error(f"S3 upload failed: {e}")
343
-
344
- # Submit action
345
- if submit:
346
- info_flag = st.session_state.get(f"info_sharing_{participant_id}", False)
347
- demand_history_length = st.session_state.get(f"demand_history_length_{participant_id}", 2)
348
- append_log(game, log_row("info_sharing_toggle", week, int(order_qty), dist, f"info_sharing={info_flag}", info_flag, "HUMAN"))
349
- advance_week(game, participant_id, order_qty, info_flag, demand_history_length)
350
- st.experimental_rerun()
351
-
352
- # Post-game download if finished
353
- if game["week"] > game["max_weeks"]:
354
- st.balloons()
355
- st.success("Game finished for this participant.")
356
- df = logs_to_df(game)
357
- csv_bytes = df.to_csv(index=False).encode("utf-8")
358
- st.download_button("Download final logs CSV", data=csv_bytes, file_name=f"logs_{participant_id}.csv", mime="text/csv")
359
- # save local and optionally upload
360
- local_path = local_save_logs(participant_id, game)
361
- st.write(f"Saved local file: `{local_path}`")
362
- if USE_S3:
363
- try:
364
- s3_key = f"beer_game_logs/{participant_id}/logs_{participant_id}_final_{int(time.time())}.csv"
365
- url = s3_upload_file(local_path, s3_key)
366
- st.success(f"Uploaded final logs to S3: `{url}`")
367
- except Exception as e:
368
- st.error(f"S3 final upload failed: {e}")
 
1
  # app.py
 
 
 
 
 
 
 
 
 
2
  import os
 
3
  import time
 
 
4
  import uuid
5
+ import random
6
  import pandas as pd
7
  import streamlit as st
 
 
 
 
 
 
8
  import openai
9
+ from huggingface_hub import HfApi, HfFolder, Repository
10
+
11
+ # -----------------------------
12
+ # 0. 配置
13
+ # -----------------------------
14
+ openai.api_key = os.getenv("OPENAI_API_KEY")
15
+ HF_TOKEN = os.getenv("HF_TOKEN") # 需要在 Hugging Face Space Secrets 里设置
16
+ HF_REPO_ID = os.getenv("HF_REPO_ID", "your-username/beer-game-logs") # 你要创建的 dataset repo
17
+
18
+ api = HfApi()
19
+
20
+ # -----------------------------
21
+ # 1. 经典 Beer Game 参数
22
+ # -----------------------------
23
+ WEEKS = 36
24
+ TRANSPORT_DELAY = 2
25
+ ORDER_DELAY = 1
26
+
27
+ # 经典客户需求序列:前 4 周恒定 4,之后 8,最后再变动
28
+ def classic_demand():
29
+ demand = []
30
+ for t in range(WEEKS):
31
+ if t < 4:
32
+ demand.append(4)
33
+ elif 4 <= t < 20:
34
+ demand.append(8)
35
+ else:
36
+ demand.append(random.choice([4, 6, 8, 10]))
37
+ return demand
38
+
39
+ CUSTOMER_DEMAND = classic_demand()
40
+
41
+ # -----------------------------
42
+ # 2. 初始化游戏状态
43
+ # -----------------------------
44
+ def init_state():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  return {
46
+ "week": 0,
47
+ "inventory": {"retailer": 12, "distributor": 12, "wholesaler": 12, "factory": 12},
48
+ "backlog": {"retailer": 0, "distributor": 0, "wholesaler": 0, "factory": 0},
49
+ "orders": {"retailer": [], "distributor": [], "wholesaler": [], "factory": []},
50
+ "pipeline": {"retailer": [], "distributor": [], "wholesaler": [], "factory": []},
51
+ "logs": []
 
 
 
 
 
52
  }
53
 
54
+ # -----------------------------
55
+ # 3. LLM 决策(OpenAI 调用)
56
+ # -----------------------------
57
+ def llm_decision(role, state):
58
+ prompt = f"""
59
+ You are playing the Beer Game as the {role}.
60
+ Current week: {state['week']}
61
+ Inventory: {state['inventory'][role]}
62
+ Backlog: {state['backlog'][role]}
63
+ Please decide how many units to order from your upstream partner.
64
+ Return only an integer.
65
+ """
 
 
 
 
 
 
 
 
 
 
 
 
66
  try:
67
+ response = openai.chat.completions.create(
68
+ model="gpt-4o-mini",
69
+ messages=[{"role": "user", "content": prompt}],
70
+ max_tokens=10,
 
 
 
 
 
71
  )
72
+ order = int("".join([c for c in response.choices[0].message.content if c.isdigit()]) or 4)
73
  except Exception as e:
74
+ st.error(f"LLM error ({role}): {e}")
75
+ order = 4
76
+ return order
77
+
78
+ # -----------------------------
79
+ # 4. 更新状态
80
+ # -----------------------------
81
+ def update_state(state, distributor_order):
82
+ t = state["week"]
83
+
84
+ # 客户需求作用在零售商
85
+ demand = CUSTOMER_DEMAND[t]
86
+ retailer_inventory = state["inventory"]["retailer"]
87
+ shipped = min(retailer_inventory, demand)
88
+ state["inventory"]["retailer"] -= shipped
89
+ state["backlog"]["retailer"] += demand - shipped
90
+
91
+ # 其他角色按 pipeline 收货
92
+ for role in ["retailer", "distributor", "wholesaler", "factory"]:
93
+ if state["pipeline"][role]:
94
+ arrived = state["pipeline"][role].pop(0)
95
+ state["inventory"][role] += arrived
96
+
97
+ # 下单:零售商由 LLM 控制
98
+ state["orders"]["retailer"].append(llm_decision("retailer", state))
99
+ state["orders"]["distributor"].append(distributor_order)
100
+ state["orders"]["wholesaler"].append(llm_decision("wholesaler", state))
101
+ state["orders"]["factory"].append(llm_decision("factory", state))
102
+
103
+ # 加入运输延迟
104
+ for role in ["retailer", "distributor", "wholesaler", "factory"]:
105
+ order = state["orders"][role][-1]
106
+ state["pipeline"][role].append(0) # 本周发不出
107
+ state["pipeline"][role].append(order) # 延迟两周才到
108
+
109
+ # 记录日志
110
+ log = {
111
+ "week": t,
112
+ "demand": demand,
113
+ "orders": {r: state["orders"][r][-1] for r in state["orders"]},
114
+ "inventory": dict(state["inventory"]),
115
+ "backlog": dict(state["backlog"]),
116
+ "timestamp": time.time(),
117
+ }
118
+ state["logs"].append(log)
119
+
120
+ state["week"] += 1
121
+ return state
122
+
123
+ # -----------------------------
124
+ # 5. 保存日志到 HF Datasets
125
+ # -----------------------------
126
+ def save_logs_to_hf(participant_id, logs):
127
+ df = pd.DataFrame(logs)
128
+ filename = f"logs_{participant_id}.csv"
129
+ df.to_csv(filename, index=False)
130
+
131
+ # 推送到 HF Hub dataset
132
+ repo_url = api.create_repo(repo_id=HF_REPO_ID, token=HF_TOKEN, repo_type="dataset", exist_ok=True)
133
+ repo = Repository(local_dir="hf_logs", clone_from=HF_REPO_ID, use_auth_token=HF_TOKEN)
134
+ os.makedirs("hf_logs", exist_ok=True)
135
+ df.to_csv(f"hf_logs/{filename}", index=False)
136
+ repo.git_add()
137
+ repo.git_commit(f"Add logs for {participant_id}")
138
+ repo.git_push()
139
+
140
+ # -----------------------------
141
+ # 6. Streamlit 界面
142
+ # -----------------------------
143
+ st.set_page_config(page_title="Beer Game Experiment", layout="wide")
144
+ st.title("Beer Game Experiment")
145
+
146
+ if "state" not in st.session_state:
147
+ st.session_state["participant_id"] = str(uuid.uuid4())
148
+ st.session_state["state"] = init_state()
149
+
150
+ state = st.session_state["state"]
151
+
152
+ st.write(f"### Week {state['week']} / {WEEKS}")
153
+ st.write(f"Your Inventory: {state['inventory']['distributor']}, Backlog: {state['backlog']['distributor']}")
154
+
155
+ distributor_order = st.number_input("Enter your order (units):", min_value=0, step=1, value=4)
156
+
157
+ if st.button("Submit Order"):
158
+ st.session_state["state"] = update_state(state, distributor_order)
159
+
160
+ if state["week"] >= WEEKS:
161
+ st.success("Game finished! Saving logs...")
162
+ save_logs_to_hf(st.session_state["participant_id"], state["logs"])
163
+ st.balloons()
164
+ st.stop()
165
+
166
+ # 显示实时日志
167
+ st.write("### Logs (this session)")
168
+ st.dataframe(pd.DataFrame(state["logs"]))