Lilli98 commited on
Commit
a8840e8
·
verified ·
1 Parent(s): 947a6b9

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +368 -0
app.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")