Download app.py from farquasar/BA: direct link, hf CLI and curl.
- Browser
- Download file 37 kB
-
https://huggingface.co/spaces/farquasar/BA/resolve/main/app.py
- Command line
-
hf download hf://spaces/farquasar/BA/app.py
-
curl -L -o app.py https://huggingface.co/spaces/farquasar/BA/resolve/main/app.py
37 kB
| import os | |
| import json | |
| import time | |
| import random | |
| import threading | |
| from pathlib import Path | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Any, Optional, Tuple | |
| import gradio as gr | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| # OpenAI Python SDK (v1+) | |
| from openai import OpenAI | |
| # ---------------------------- | |
| # Configuration (HF Secrets) | |
| # ---------------------------- | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() | |
| MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo-0125").strip() | |
| TEACHER_PIN = os.getenv("TEACHER_PIN", "1234").strip() | |
| # If you don't want AI at all, set this to "0" in HF Variables | |
| AI_ENABLED_DEFAULT = os.getenv("AI_ENABLED", "1").strip() != "0" | |
| client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None | |
| # ---------------------------- | |
| # Game constants | |
| # ---------------------------- | |
| DATASETS = { | |
| "Sales": {"desc": "Transactions by hour/product (2 years)", "cost": 5}, | |
| "Inventory": {"desc": "Production, waste, stockouts", "cost": 4}, | |
| "Customers": {"desc": "Loyalty behavior, visit frequency", "cost": 5}, | |
| "Staffing": {"desc": "Schedules and service times", "cost": 3}, | |
| "External": {"desc": "Weather + academic calendar", "cost": 2}, | |
| "Feedback": {"desc": "Ratings + text reviews", "cost": 3}, | |
| } | |
| BASE_DIR = Path(__file__).resolve().parent | |
| DATA_DIR = BASE_DIR / "data" | |
| DATASET_FILES = { | |
| "Sales": "sales.csv", | |
| "Inventory": "inventory.csv", | |
| "Customers": "customers.csv", | |
| "Staffing": "staffing.csv", | |
| "External": "external.csv", | |
| "Feedback": "feedback.csv", | |
| } | |
| EVENT_CARDS = [ | |
| ("Exam week", "Demand spikes +25% for 7 days. Morning queue risk increases."), | |
| ("Rainy stretch", "Foot traffic drops ~15%. Afternoon sales become volatile."), | |
| ("Competitor promo", "Nearby café runs 2-for-1 coffees. Price sensitivity increases."), | |
| ("Supplier price increase", "Pastry input costs rise 12%. Margin pressure."), | |
| ("App promo goes viral", "New users surge. More first-time buyers, less predictable demand."), | |
| ] | |
| DECISIONS = [ | |
| ("Reduce pastry production", "Reduce baseline pastry production by 15%."), | |
| ("Afternoon discounts", "Offer 10–25% discount after 4 PM on selected items."), | |
| ("Add morning staff", "Add 1 staff member 8–10 AM to reduce queue time."), | |
| ("Change product mix", "Add 1 vegan option, remove 1 low-selling pastry."), | |
| ("Invest: Demand forecasting", "Build forecasting to reduce stockouts & waste."), | |
| ("Invest: Segmentation", "Cluster customers for targeted offers."), | |
| ("Invest: Sentiment analysis", "Analyze reviews for themes and priorities."), | |
| ("Invest: Dynamic pricing", "Automate discounts based on predicted demand/waste."), | |
| ] | |
| # ---------------------------- | |
| # In-memory store (session-based) | |
| # ---------------------------- | |
| rooms_lock = threading.Lock() | |
| rooms: Dict[str, Any] = {} # room_code -> state dict | |
| def _now_ms() -> int: | |
| return int(time.time() * 1000) | |
| def _new_room_code() -> str: | |
| return f"ROOM-{random.randint(1000, 9999)}" | |
| def _safe_json_loads(text: str) -> Optional[dict]: | |
| try: | |
| # try direct | |
| return json.loads(text) | |
| except Exception: | |
| # try to extract a JSON object from text | |
| start = text.find("{") | |
| end = text.rfind("}") | |
| if start != -1 and end != -1 and end > start: | |
| try: | |
| return json.loads(text[start : end + 1]) | |
| except Exception: | |
| return None | |
| return None | |
| # ---------------------------- | |
| # AI prompts | |
| # ---------------------------- | |
| BUSINESS_CONTEXT = """ | |
| You are the AI Business Assistant for SmartCampus Café (inside a university). | |
| Known situation: | |
| - 1,200–1,800 customers/day; peaks 8–10 AM and 1–3 PM | |
| - Pastry waste about 12% | |
| - Queues during morning peak | |
| - Sales drop after 4 PM | |
| - Competition nearby + delivery apps | |
| Data exists but may need to be requested selectively with a budget. | |
| Your job: | |
| - Provide background about the business when asked. | |
| - Brainstorm approaches, variables, baselines, KPIs, risks. | |
| - Give hints ONLY when students ask for hints. | |
| - Ask clarifying questions when students are vague. | |
| - Do NOT provide final numeric answers or “the best decision”. | |
| - Do NOT invent dataset values. | |
| - Encourage validation, baselines, and limitations. | |
| Keep responses concise and practical. | |
| """.strip() | |
| STUDENT_GAME_BRIEF = """ | |
| ## Business Context | |
| You are the analytics team for **SmartCampus Café**, a university coffee shop with: | |
| - 1,200-1,800 customers per day | |
| - Peak demand at **8-10 AM** and **1-3 PM** | |
| - About **12% pastry waste** | |
| - Long queues during morning rush | |
| - Sales drop after 4 PM | |
| - Pressure from nearby competitors and delivery apps | |
| ## The Core Problem | |
| Your team must make better operating decisions that improve: | |
| - **Profit** (waste, discounts, costs, stockouts) | |
| - **Customer experience** (wait times, availability, satisfaction) | |
| You cannot unlock all data at once, so you need to choose datasets strategically under a budget. | |
| ## What Is Expected From Students | |
| 1. Form a clear business question. | |
| 2. Request datasets with justification (what you need and why). | |
| 3. Use unlocked files/plots to extract evidence (patterns, comparisons, trends). | |
| 4. Propose one decision and justify it with data. | |
| 5. Include risks, limitations, and a KPI/monitoring plan. | |
| Good answers are not just opinions. They connect **decision -> evidence -> expected impact -> risk controls**. | |
| """.strip() | |
| def ai_chat(team_msg: str, room_state: dict, team_name: str) -> str: | |
| if not (AI_ENABLED_DEFAULT and client): | |
| return "AI is disabled (missing OPENAI_API_KEY or AI_ENABLED=0)." | |
| team = room_state["teams"].get(team_name, {}) | |
| unlocked = sorted(list(team.get("unlocked_datasets", []))) | |
| budget_left = team.get("budget_left", 20) | |
| state_summary = { | |
| "round": room_state["round"], | |
| "event": room_state.get("active_event"), | |
| "team": team_name, | |
| "budget_left": budget_left, | |
| "unlocked_datasets": unlocked, | |
| } | |
| # Responses API (OpenAI) | |
| resp = client.responses.create( | |
| model=MODEL, | |
| input=[ | |
| {"role": "system", "content": BUSINESS_CONTEXT}, | |
| { | |
| "role": "user", | |
| "content": f"Game state: {json.dumps(state_summary)}\n\nStudent message: {team_msg}", | |
| }, | |
| ], | |
| max_output_tokens=400, | |
| ) | |
| return resp.output_text.strip() if hasattr(resp, "output_text") else str(resp) | |
| DATASET_GATE_PROMPT = """ | |
| You are evaluating a team's request to unlock a dataset. | |
| Return STRICT JSON only, with keys: | |
| - allow: boolean | |
| - dataset: one of ["Sales","Inventory","Customers","Staffing","External","Feedback"] or null | |
| - reason: short string | |
| - suggestion: short string (how to ask better or what to do next) | |
| Rules: | |
| - Allow only if the student states a clear business question and how the dataset will be used. | |
| - If unclear, deny and ask for a sharper question. | |
| - Do not reveal or invent dataset contents. | |
| """.strip() | |
| def ai_gate_dataset(request_text: str, room_state: dict, team_name: str) -> dict: | |
| if not (AI_ENABLED_DEFAULT and client): | |
| return { | |
| "allow": True, | |
| "dataset": None, | |
| "reason": "AI disabled; defaulting to manual approval.", | |
| "suggestion": "Teacher can approve manually.", | |
| } | |
| team = room_state["teams"].get(team_name, {}) | |
| unlocked = sorted(list(team.get("unlocked_datasets", []))) | |
| budget_left = team.get("budget_left", 20) | |
| payload = { | |
| "round": room_state["round"], | |
| "team": team_name, | |
| "budget_left": budget_left, | |
| "already_unlocked": unlocked, | |
| "request": request_text, | |
| "available_datasets": list(DATASETS.keys()), | |
| } | |
| resp = client.responses.create( | |
| model=MODEL, | |
| input=[ | |
| {"role": "system", "content": DATASET_GATE_PROMPT}, | |
| {"role": "user", "content": json.dumps(payload)}, | |
| ], | |
| max_output_tokens=250, | |
| ) | |
| text = resp.output_text.strip() if hasattr(resp, "output_text") else "" | |
| data = _safe_json_loads(text) | |
| if not data: | |
| return { | |
| "allow": False, | |
| "dataset": None, | |
| "reason": "Could not parse AI response as JSON.", | |
| "suggestion": "Rephrase the request: state business question + intended use.", | |
| } | |
| # sanitize | |
| ds = data.get("dataset") | |
| if ds not in list(DATASETS.keys()): | |
| data["dataset"] = None | |
| data["allow"] = bool(data.get("allow", False)) | |
| data["reason"] = str(data.get("reason", ""))[:220] | |
| data["suggestion"] = str(data.get("suggestion", ""))[:220] | |
| return data | |
| # ---------------------------- | |
| # Game mechanics (simple, deterministic-ish) | |
| # ---------------------------- | |
| def simulate_impact(decision_key: str, room_state: dict, team_name: str) -> Tuple[int, int, str]: | |
| """ | |
| Returns (profit_points_delta, customer_points_delta, note) | |
| Simple rule-based impact; you can later replace with your own simulator. | |
| """ | |
| round_no = room_state["round"] | |
| event = room_state.get("active_event", None) | |
| profit = 0 | |
| cust = 0 | |
| note = "" | |
| if decision_key == "Reduce pastry production": | |
| profit += 3 | |
| cust -= 1 # occasional stockouts risk | |
| note = "Less waste, slight availability risk." | |
| elif decision_key == "Afternoon discounts": | |
| profit += 2 | |
| cust += 1 | |
| note = "Better late-day traffic; margin trade-off." | |
| elif decision_key == "Add morning staff": | |
| profit -= 1 | |
| cust += 3 | |
| note = "Queues improve; labor cost increases." | |
| elif decision_key == "Change product mix": | |
| profit += 1 | |
| cust += 1 | |
| note = "Better fit for preferences; small operational change." | |
| elif decision_key == "Invest: Demand forecasting": | |
| profit += 4 | |
| cust += 1 | |
| note = "Better planning; benefit grows with use." | |
| elif decision_key == "Invest: Segmentation": | |
| profit += 2 | |
| cust += 1 | |
| note = "More targeted promos; needs execution." | |
| elif decision_key == "Invest: Sentiment analysis": | |
| profit += 1 | |
| cust += 2 | |
| note = "Prioritizes improvements; indirect revenue impact." | |
| elif decision_key == "Invest: Dynamic pricing": | |
| profit += 3 | |
| cust += 0 | |
| note = "Reduces waste, boosts revenue; needs guardrails." | |
| # event modifiers | |
| if event and event[0] == "Exam week": | |
| if decision_key in ["Add morning staff", "Invest: Demand forecasting"]: | |
| profit += 1 | |
| cust += 1 | |
| note += " (Great timing for exam week.)" | |
| if event and event[0] == "Rainy stretch": | |
| if decision_key == "Afternoon discounts": | |
| profit += 1 | |
| note += " (Helps counter low traffic.)" | |
| # slight diminishing returns by round | |
| if round_no >= 3: | |
| profit = max(profit - 1, -3) | |
| return profit, cust, note | |
| # ---------------------------- | |
| # Room/state helpers | |
| # ---------------------------- | |
| def create_room(teacher_pin: str, budget_per_team: int = 20) -> Tuple[str, str]: | |
| if teacher_pin.strip() != TEACHER_PIN: | |
| return "", "Incorrect teacher PIN." | |
| code = _new_room_code() | |
| with rooms_lock: | |
| rooms[code] = { | |
| "created_at": _now_ms(), | |
| "round": 1, | |
| "budget_per_team": int(budget_per_team), | |
| "active_event": None, | |
| "teams": {}, # team_name -> state | |
| "log": [], | |
| } | |
| return code, f"Room created: {code}" | |
| def start_free_play(team_name: str, budget_per_team: int = 20) -> Tuple[dict, str]: | |
| team_name = (team_name or "").strip() | |
| if not team_name: | |
| team_name = f"Solo Team {random.randint(10, 99)}" | |
| code = f"FREE-{random.randint(1000, 9999)}" | |
| with rooms_lock: | |
| while code in rooms: | |
| code = f"FREE-{random.randint(1000, 9999)}" | |
| rooms[code] = { | |
| "created_at": _now_ms(), | |
| "round": 1, | |
| "budget_per_team": int(budget_per_team), | |
| "active_event": None, | |
| "teams": { | |
| team_name: { | |
| "joined_at": _now_ms(), | |
| "budget_left": int(budget_per_team), | |
| "unlocked_datasets": set(), | |
| "profit_points": 0, | |
| "customer_points": 0, | |
| "submissions": [], | |
| } | |
| }, | |
| "log": [{"t": _now_ms(), "type": "free_play_start", "team": team_name}], | |
| } | |
| return {"room": code, "team": team_name}, f"Free play started in {code} as {team_name}." | |
| def join_room(room_code: str, team_name: str) -> Tuple[dict, str]: | |
| room_code = room_code.strip().upper() | |
| team_name = team_name.strip() | |
| if not room_code or not team_name: | |
| return {}, "Provide room code and team name." | |
| with rooms_lock: | |
| if room_code not in rooms: | |
| return {}, "Room not found." | |
| r = rooms[room_code] | |
| if team_name not in r["teams"]: | |
| r["teams"][team_name] = { | |
| "joined_at": _now_ms(), | |
| "budget_left": r["budget_per_team"], | |
| "unlocked_datasets": set(), | |
| "profit_points": 0, | |
| "customer_points": 0, | |
| "submissions": [], # list of dicts | |
| } | |
| r["log"].append({"t": _now_ms(), "type": "join", "team": team_name}) | |
| return {"room": room_code, "team": team_name}, f"Joined {room_code} as {team_name}." | |
| def get_room_state(room_code: str) -> Optional[dict]: | |
| with rooms_lock: | |
| return rooms.get(room_code) | |
| def dataset_file_path(dataset_name: str) -> Optional[Path]: | |
| filename = DATASET_FILES.get(dataset_name) | |
| if not filename: | |
| return None | |
| return DATA_DIR / filename | |
| def scoreboard_df(room_state: dict) -> List[List[Any]]: | |
| rows = [] | |
| for team, st in room_state["teams"].items(): | |
| rows.append([ | |
| team, | |
| st.get("budget_left", 0), | |
| st.get("profit_points", 0), | |
| st.get("customer_points", 0), | |
| st.get("profit_points", 0) + st.get("customer_points", 0), | |
| ", ".join(sorted(list(st.get("unlocked_datasets", [])))), | |
| ]) | |
| rows.sort(key=lambda r: r[4], reverse=True) | |
| return rows | |
| # ---------------------------- | |
| # Gradio callbacks | |
| # ---------------------------- | |
| def ui_refresh(session: dict) -> Tuple[str, List[List[Any]], str]: | |
| if not session: | |
| return "Not joined.", [], "" | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return "Room expired or restarted.", [], "" | |
| ev = room_state["active_event"] | |
| ev_text = f"{ev[0]} — {ev[1]}" if ev else "No active event." | |
| header = f"Room: {session['room']} | Team: {session['team']} | Round: {room_state['round']} | Event: {ev_text}" | |
| return header, scoreboard_df(room_state), ev_text | |
| def ui_ai_chat(session: dict, message: str, chat_history: list): | |
| if not session: | |
| return chat_history, "Join a room first." | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return chat_history, "Room expired or restarted." | |
| if not message.strip(): | |
| return chat_history, "" | |
| reply = ai_chat(message.strip(), room_state, session["team"]) | |
| if chat_history is None: | |
| chat_history = [] | |
| chat_history = chat_history + [ | |
| {"role": "user", "content": message.strip()}, | |
| {"role": "assistant", "content": reply}, | |
| ] | |
| return chat_history, "" | |
| def ui_request_dataset(session: dict, request_text: str) -> Tuple[str, List[List[Any]]]: | |
| if not session: | |
| return "Join a room first.", [] | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return "Room expired or restarted.", [] | |
| team_name = session["team"] | |
| gate = ai_gate_dataset(request_text.strip(), room_state, team_name) | |
| # If AI didn't name a dataset, ask user to specify it | |
| if gate.get("allow") and not gate.get("dataset"): | |
| return f"Approved in principle, but dataset not specified. Choose one dataset name. Hint: {gate.get('suggestion','')}", scoreboard_df(room_state) | |
| ds = gate.get("dataset") | |
| if not gate.get("allow") or not ds: | |
| return f"Denied: {gate.get('reason','')} Suggestion: {gate.get('suggestion','')}", scoreboard_df(room_state) | |
| # apply unlock + cost | |
| with rooms_lock: | |
| r = rooms.get(session["room"]) | |
| if not r: | |
| return "Room expired or restarted.", [] | |
| team = r["teams"].get(team_name) | |
| if not team: | |
| return "Team not found.", scoreboard_df(r) | |
| if ds in team["unlocked_datasets"]: | |
| return f"Dataset already unlocked: {ds}", scoreboard_df(r) | |
| cost = DATASETS[ds]["cost"] | |
| if team["budget_left"] < cost: | |
| return f"Not enough budget for {ds} (cost {cost}). Budget left: {team['budget_left']}", scoreboard_df(r) | |
| team["budget_left"] -= cost | |
| team["unlocked_datasets"].add(ds) | |
| r["log"].append({"t": _now_ms(), "type": "unlock", "team": team_name, "dataset": ds, "cost": cost}) | |
| return f"Unlocked dataset: {ds} (cost {DATASETS[ds]['cost']}). {gate.get('reason','')}", scoreboard_df(get_room_state(session["room"])) | |
| def ui_submit_work(session: dict, decision: str, model_card: str, decision_report: str) -> str: | |
| if not session: | |
| return "Join a room first." | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return "Room expired or restarted." | |
| decision = decision.strip() | |
| if not decision: | |
| return "Pick a decision." | |
| # apply deterministic impact | |
| profit_delta, cust_delta, note = simulate_impact(decision, room_state, session["team"]) | |
| submission = { | |
| "t": _now_ms(), | |
| "round": room_state["round"], | |
| "decision": decision, | |
| "model_card": (model_card or "").strip(), | |
| "decision_report": (decision_report or "").strip(), | |
| "profit_delta": profit_delta, | |
| "customer_delta": cust_delta, | |
| "note": note, | |
| } | |
| with rooms_lock: | |
| r = rooms.get(session["room"]) | |
| if not r: | |
| return "Room expired or restarted." | |
| team = r["teams"].get(session["team"]) | |
| if not team: | |
| return "Team not found." | |
| team["profit_points"] += profit_delta | |
| team["customer_points"] += cust_delta | |
| team["submissions"].append(submission) | |
| r["log"].append({"t": _now_ms(), "type": "submit", "team": session["team"], "decision": decision}) | |
| return f"Submitted. Impact: Profit {profit_delta:+d}, Customer {cust_delta:+d}. {note}" | |
| def _word_count(text: str) -> int: | |
| return len([w for w in (text or "").strip().split() if w]) | |
| def _contains_any(text: str, terms: List[str]) -> bool: | |
| t = (text or "").lower() | |
| return any(term in t for term in terms) | |
| def ui_tutor_feedback(session: dict, decision: str, model_card: str, decision_report: str) -> str: | |
| if not session: | |
| return "Join a room first." | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return "Room expired or restarted." | |
| team = room_state["teams"].get(session["team"], {}) | |
| unlocked = sorted(list(team.get("unlocked_datasets", []))) | |
| model_card = (model_card or "").strip() | |
| decision_report = (decision_report or "").strip() | |
| merged_text = f"{model_card}\n{decision_report}".lower() | |
| step_1 = bool(decision and decision.strip()) and _word_count(decision_report) >= 40 | |
| step_2 = len(unlocked) > 0 and _contains_any(merged_text, [d.lower() for d in unlocked]) | |
| step_3 = _contains_any(merged_text, ["baseline", "validation", "metric", "compare", "experiment", "assumption"]) | |
| step_4 = _contains_any(merged_text, ["risk", "limitation", "trade-off", "uncertainty", "bias"]) | |
| step_5 = _contains_any(merged_text, ["next", "plan", "monitor", "kpi", "target", "threshold", "weekly"]) | |
| checks = [ | |
| ("Step 1 - Decision clarity", step_1, "State one concrete action and a report of at least ~40 words."), | |
| ("Step 2 - Evidence from data", step_2, "Reference at least one unlocked dataset and what signal it shows."), | |
| ("Step 3 - Method and validation", step_3, "Add baseline/validation/metric language to show rigor."), | |
| ("Step 4 - Risks and limitations", step_4, "Include at least one downside, uncertainty, or limitation."), | |
| ("Step 5 - Execution plan + KPI", step_5, "Define next step, owner/timing, and KPI to monitor."), | |
| ] | |
| passed = sum(1 for _, ok, _ in checks if ok) | |
| status_lines = [] | |
| for label, ok, hint in checks: | |
| status = "PASS" if ok else "NEEDS WORK" | |
| status_lines.append(f"- **{label}: {status}** \n {hint}") | |
| summary = ( | |
| f"### Tutor Feedback (Step-by-step)\n" | |
| f"Team: `{session['team']}` | Round: `{room_state['round']}` | Score: `{passed}/5`\n\n" | |
| + "\n".join(status_lines) | |
| ) | |
| if passed <= 2: | |
| summary += "\n\n<div class='feedback-box'><b>Tutor next move:</b> Improve Step 2 and Step 3 first. Add specific dataset evidence and one validation metric.</div>" | |
| elif passed == 3: | |
| summary += "\n\n<div class='feedback-box'><b>Tutor next move:</b> Strong draft. Tighten risks and KPI execution details to make it decision-ready.</div>" | |
| else: | |
| summary += "\n\n<div class='feedback-box'><b>Tutor next move:</b> This is close to publishable. Make numbers explicit for KPI targets and timeline.</div>" | |
| return summary | |
| def ui_get_dataset_file(session: dict, dataset_name: str) -> Tuple[str, Optional[str]]: | |
| if not session: | |
| return "Join a room first.", None | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return "Room expired or restarted.", None | |
| team = room_state["teams"].get(session["team"]) | |
| if not team: | |
| return "Team not found.", None | |
| dataset_name = (dataset_name or "").strip() | |
| if dataset_name not in DATASETS: | |
| return "Choose a valid dataset.", None | |
| if dataset_name not in team.get("unlocked_datasets", set()): | |
| return f"Dataset not unlocked yet: {dataset_name}", None | |
| path = dataset_file_path(dataset_name) | |
| if not path or not path.exists(): | |
| return f"File not found for {dataset_name}. Expected: data/{DATASET_FILES.get(dataset_name, '')}", None | |
| return f"File ready: {path.name}", str(path) | |
| def ui_dataset_plot(session: dict, dataset_name: str, plot_type: str): | |
| if not session: | |
| return "Join a room first.", None | |
| room_state = get_room_state(session["room"]) | |
| if not room_state: | |
| return "Room expired or restarted.", None | |
| team = room_state["teams"].get(session["team"]) | |
| if not team: | |
| return "Team not found.", None | |
| dataset_name = (dataset_name or "").strip() | |
| if dataset_name not in DATASETS: | |
| return "Choose a valid dataset.", None | |
| if dataset_name not in team.get("unlocked_datasets", set()): | |
| return f"Dataset not unlocked yet: {dataset_name}", None | |
| path = dataset_file_path(dataset_name) | |
| if not path or not path.exists(): | |
| return f"File not found for {dataset_name}.", None | |
| try: | |
| df = pd.read_csv(path) | |
| except Exception as exc: | |
| return f"Could not load CSV: {exc}", None | |
| fig, ax = plt.subplots(figsize=(8, 4.5)) | |
| try: | |
| if dataset_name == "Sales": | |
| data = df.groupby("hour", as_index=False)["revenue_eur"].sum() | |
| if plot_type == "Line": | |
| ax.plot(data["hour"], data["revenue_eur"], marker="o", color="#2d6a4f") | |
| else: | |
| ax.bar(data["hour"], data["revenue_eur"], color="#2d6a4f") | |
| ax.set_title("Sales: Revenue by Hour") | |
| ax.set_xlabel("Hour") | |
| ax.set_ylabel("Revenue (EUR)") | |
| elif dataset_name == "Inventory": | |
| if plot_type == "Line": | |
| ax.plot(df["item"], df["waste"], marker="o", color="#cc5803") | |
| else: | |
| ax.bar(df["item"], df["waste"], color="#cc5803") | |
| ax.set_title("Inventory: Waste by Item") | |
| ax.set_xlabel("Item") | |
| ax.set_ylabel("Waste") | |
| ax.tick_params(axis="x", rotation=30) | |
| elif dataset_name == "Customers": | |
| if plot_type == "Line": | |
| data = df.groupby("segment", as_index=False)["avg_basket_eur"].mean() | |
| ax.plot(data["segment"], data["avg_basket_eur"], marker="o", color="#3867d6") | |
| else: | |
| data = df.groupby("segment", as_index=False)["avg_basket_eur"].mean() | |
| ax.bar(data["segment"], data["avg_basket_eur"], color="#3867d6") | |
| ax.set_title("Customers: Avg Basket by Segment") | |
| ax.set_xlabel("Segment") | |
| ax.set_ylabel("Avg Basket (EUR)") | |
| elif dataset_name == "Staffing": | |
| data = df.groupby("shift", as_index=False)["avg_service_seconds"].mean() | |
| if plot_type == "Line": | |
| ax.plot(data["shift"], data["avg_service_seconds"], marker="o", color="#6a4c93") | |
| else: | |
| ax.bar(data["shift"], data["avg_service_seconds"], color="#6a4c93") | |
| ax.set_title("Staffing: Avg Service Time by Shift") | |
| ax.set_xlabel("Shift") | |
| ax.set_ylabel("Seconds") | |
| elif dataset_name == "External": | |
| if plot_type == "Line": | |
| ax.plot(df["date"], df["foot_traffic_index"], marker="o", color="#118ab2") | |
| else: | |
| ax.bar(df["date"], df["foot_traffic_index"], color="#118ab2") | |
| ax.set_title("External: Foot Traffic Index by Date") | |
| ax.set_xlabel("Date") | |
| ax.set_ylabel("Foot Traffic Index") | |
| ax.tick_params(axis="x", rotation=30) | |
| elif dataset_name == "Feedback": | |
| data = df.groupby("theme", as_index=False)["rating"].mean() | |
| if plot_type == "Line": | |
| ax.plot(data["theme"], data["rating"], marker="o", color="#2a9d8f") | |
| else: | |
| ax.bar(data["theme"], data["rating"], color="#2a9d8f") | |
| ax.set_title("Feedback: Avg Rating by Theme") | |
| ax.set_xlabel("Theme") | |
| ax.set_ylabel("Average Rating") | |
| ax.tick_params(axis="x", rotation=20) | |
| ax.grid(alpha=0.2) | |
| fig.tight_layout() | |
| return f"Showing {plot_type.lower()} chart for {dataset_name}.", fig | |
| except Exception as exc: | |
| plt.close(fig) | |
| return f"Could not generate plot: {exc}", None | |
| def teacher_next_round(pin: str, room_code: str) -> str: | |
| if pin.strip() != TEACHER_PIN: | |
| return "Incorrect teacher PIN." | |
| room_code = room_code.strip().upper() | |
| with rooms_lock: | |
| if room_code not in rooms: | |
| return "Room not found." | |
| rooms[room_code]["round"] += 1 | |
| rooms[room_code]["log"].append({"t": _now_ms(), "type": "round_advance", "round": rooms[room_code]["round"]}) | |
| return f"Advanced to round {rooms[room_code]['round']}." | |
| def teacher_set_event(pin: str, room_code: str, event_name: str) -> str: | |
| if pin.strip() != TEACHER_PIN: | |
| return "Incorrect teacher PIN." | |
| room_code = room_code.strip().upper() | |
| with rooms_lock: | |
| if room_code not in rooms: | |
| return "Room not found." | |
| if event_name == "None": | |
| rooms[room_code]["active_event"] = None | |
| else: | |
| match = [e for e in EVENT_CARDS if e[0] == event_name] | |
| rooms[room_code]["active_event"] = match[0] if match else None | |
| rooms[room_code]["log"].append({"t": _now_ms(), "type": "event_set", "event": event_name}) | |
| return f"Event set: {event_name}" | |
| def teacher_draw_random_event(pin: str, room_code: str) -> str: | |
| if pin.strip() != TEACHER_PIN: | |
| return "Incorrect teacher PIN." | |
| room_code = room_code.strip().upper() | |
| ev = random.choice(EVENT_CARDS) | |
| with rooms_lock: | |
| if room_code not in rooms: | |
| return "Room not found." | |
| rooms[room_code]["active_event"] = ev | |
| rooms[room_code]["log"].append({"t": _now_ms(), "type": "event_draw", "event": ev[0]}) | |
| return f"Drew event: {ev[0]} — {ev[1]}" | |
| def teacher_room_overview(pin: str, room_code: str) -> str: | |
| if pin.strip() != TEACHER_PIN: | |
| return "Incorrect teacher PIN." | |
| room_code = room_code.strip().upper() | |
| r = get_room_state(room_code) | |
| if not r: | |
| return "Room not found." | |
| lines = [f"Room {room_code} | Round {r['round']} | Teams {len(r['teams'])}"] | |
| ev = r.get("active_event") | |
| lines.append(f"Event: {ev[0]} — {ev[1]}" if ev else "Event: None") | |
| lines.append("") | |
| for team, st in r["teams"].items(): | |
| lines.append(f"- {team}: budget {st['budget_left']}, profit {st['profit_points']}, customer {st['customer_points']}, unlocked {sorted(list(st['unlocked_datasets']))}") | |
| return "\n".join(lines) | |
| # ---------------------------- | |
| # Gradio UI | |
| # ---------------------------- | |
| with gr.Blocks( | |
| title="AI Café Challenge (Session-Based)", | |
| theme=gr.themes.Soft(primary_hue="green", secondary_hue="orange", neutral_hue="stone"), | |
| ) as demo: | |
| gr.Markdown( | |
| """ | |
| <div class="hero"> | |
| <h1>AI Café Challenge</h1> | |
| <p>Run your campus café like a data team: unlock evidence, test decisions, and iterate with tutor feedback.</p> | |
| </div> | |
| """ | |
| ) | |
| gr.Markdown( | |
| """ | |
| ### How to play | |
| 1. Join an existing room, or click **Start Free Play** to explore solo. | |
| 2. Ask the assistant for hints and request datasets with a clear business question. | |
| 3. Use unlocked files and plots to build evidence, then justify your choice with facts (which dataset, what pattern, and why that supports your decision). | |
| 4. Submit a decision with model card + decision report. | |
| 5. Use **Tutor Feedback (Step-by-step)** to improve your work each round. | |
| """ | |
| ) | |
| with gr.Accordion("Business Context, Problem, and Expectations", open=True): | |
| gr.Markdown(STUDENT_GAME_BRIEF) | |
| session = gr.State({}) # per-user session state: {"room":..., "team":...} | |
| with gr.Tab("Student"): | |
| with gr.Row(): | |
| room_in = gr.Textbox(label="Room code", placeholder="ROOM-1234") | |
| team_in = gr.Textbox(label="Team name", placeholder="e.g., Team Orion") | |
| join_btn = gr.Button("Join") | |
| free_play_btn = gr.Button("Start Free Play", variant="primary") | |
| join_status = gr.Markdown("Not joined.", elem_classes=["block-panel"]) | |
| header = gr.Markdown("") | |
| score_table = gr.Dataframe( | |
| headers=["Team", "Budget left", "Profit", "Customer", "Total", "Unlocked datasets"], | |
| datatype=["str", "number", "number", "number", "number", "str"], | |
| row_count=8, | |
| col_count=(6, "fixed"), | |
| interactive=False, | |
| label="Scoreboard", | |
| ) | |
| event_text = gr.Markdown("") | |
| refresh_btn = gr.Button("Refresh scoreboard") | |
| gr.Markdown("## AI Business Assistant (Hints / Background / Brainstorming)") | |
| chatbot = gr.Chatbot(label="Assistant", height=320) | |
| chat_msg = gr.Textbox(label="Message", placeholder="Ask for background, or request a hint, or brainstorm approaches.") | |
| chat_btn = gr.Button("Send") | |
| gr.Markdown("## Request a dataset (AI will approve/deny)") | |
| gr.Markdown( | |
| "Justify the request in 1-2 sentences: **business question + chosen dataset + how you will use it + expected decision impact**." | |
| ) | |
| ds_request = gr.Textbox(label="Dataset request", placeholder='Example: "We want to forecast hourly pastry demand; please unlock Sales to model seasonality and peaks."') | |
| ds_btn = gr.Button("Request dataset") | |
| ds_status = gr.Markdown("", elem_classes=["block-panel"]) | |
| gr.Markdown("## Access unlocked data files") | |
| file_ds_dd = gr.Dropdown(list(DATASETS.keys()), label="Unlocked dataset", value="Sales") | |
| file_btn = gr.Button("Get dataset file") | |
| file_status = gr.Markdown("", elem_classes=["block-panel"]) | |
| data_file = gr.File(label="Dataset file", interactive=False) | |
| gr.Markdown("## Data Plots") | |
| with gr.Row(): | |
| plot_ds_dd = gr.Dropdown(list(DATASETS.keys()), label="Dataset", value="Sales") | |
| plot_type_dd = gr.Dropdown(["Bar", "Line"], label="Plot type", value="Bar") | |
| plot_btn = gr.Button("Show plot", variant="primary") | |
| plot_status = gr.Markdown("", elem_classes=["block-panel"]) | |
| plot_view = gr.Plot(label="Dataset chart") | |
| gr.Markdown("## Submit work (Decision + Model Card + Decision Report)") | |
| decision_dd = gr.Dropdown([d[0] for d in DECISIONS], label="Decision", value=DECISIONS[0][0]) | |
| model_card = gr.Textbox( | |
| label="Model Card (short)", | |
| lines=8, | |
| placeholder="Business objective...\nData used...\nMethod...\nValidation...\nKey insight...\nLimitations...", | |
| ) | |
| decision_report = gr.Textbox( | |
| label="Decision Report (150–200 words)", | |
| lines=6, | |
| placeholder="What action? Evidence? Expected impact? Confidence (Low/Med/High).", | |
| ) | |
| submit_btn = gr.Button("Submit") | |
| submit_status = gr.Markdown("", elem_classes=["block-panel"]) | |
| gr.Markdown("## Tutor Feedback (Step-by-step)") | |
| tutor_btn = gr.Button("Review Draft") | |
| tutor_feedback = gr.Markdown("Tutor feedback will appear here after you review a draft.", elem_classes=["block-panel"]) | |
| def _join(room_code, team_name): | |
| sess, msg = join_room(room_code, team_name) | |
| return sess, msg | |
| def _free_play(team_name): | |
| return start_free_play(team_name) | |
| join_btn.click(_join, [room_in, team_in], [session, join_status]).then( | |
| ui_refresh, [session], [header, score_table, event_text] | |
| ) | |
| free_play_btn.click(_free_play, [team_in], [session, join_status]).then( | |
| ui_refresh, [session], [header, score_table, event_text] | |
| ) | |
| refresh_btn.click(ui_refresh, [session], [header, score_table, event_text]) | |
| chat_btn.click(ui_ai_chat, [session, chat_msg, chatbot], [chatbot, chat_msg]) | |
| ds_btn.click(ui_request_dataset, [session, ds_request], [ds_status, score_table]) | |
| file_btn.click(ui_get_dataset_file, [session, file_ds_dd], [file_status, data_file]) | |
| plot_btn.click(ui_dataset_plot, [session, plot_ds_dd, plot_type_dd], [plot_status, plot_view]) | |
| tutor_btn.click(ui_tutor_feedback, [session, decision_dd, model_card, decision_report], [tutor_feedback]) | |
| submit_btn.click(ui_submit_work, [session, decision_dd, model_card, decision_report], [submit_status]).then( | |
| ui_refresh, [session], [header, score_table, event_text] | |
| ).then( | |
| ui_tutor_feedback, [session, decision_dd, model_card, decision_report], [tutor_feedback] | |
| ) | |
| with gr.Tab("Teacher"): | |
| gr.Markdown("## Teacher Controls (PIN-protected)") | |
| t_pin = gr.Textbox(label="Teacher PIN", type="password", placeholder="Set TEACHER_PIN in HF Secrets") | |
| budget = gr.Number(label="Budget per team", value=20, precision=0) | |
| create_btn = gr.Button("Create new room") | |
| room_out = gr.Textbox(label="Room code (share with students)") | |
| create_msg = gr.Markdown("") | |
| create_btn.click(create_room, [t_pin, budget], [room_out, create_msg]) | |
| gr.Markdown("### Manage existing room") | |
| t_room = gr.Textbox(label="Room code", placeholder="ROOM-1234") | |
| next_round_btn = gr.Button("Next round") | |
| next_round_msg = gr.Markdown("") | |
| next_round_btn.click(teacher_next_round, [t_pin, t_room], [next_round_msg]) | |
| gr.Markdown("### Events") | |
| ev_names = ["None"] + [e[0] for e in EVENT_CARDS] | |
| ev_dd = gr.Dropdown(ev_names, label="Set event", value="None") | |
| set_ev_btn = gr.Button("Set event") | |
| draw_ev_btn = gr.Button("Draw random event") | |
| ev_msg = gr.Markdown("") | |
| set_ev_btn.click(teacher_set_event, [t_pin, t_room, ev_dd], [ev_msg]) | |
| draw_ev_btn.click(teacher_draw_random_event, [t_pin, t_room], [ev_msg]) | |
| gr.Markdown("### Overview") | |
| overview_btn = gr.Button("Show room overview") | |
| overview_txt = gr.Textbox(label="Overview", lines=12) | |
| overview_btn.click(teacher_room_overview, [t_pin, t_room], [overview_txt]) | |
| gr.Markdown("") | |
| # """ | |
| # **Deployment notes** | |
| # - Add `OPENAI_API_KEY` as a Hugging Face **Secret** | |
| # - Optionally set `OPENAI_MODEL` (default: `gpt-3.5-turbo-0125`) | |
| # - Set `TEACHER_PIN` as a **Secret** | |
| # - Set `AI_ENABLED=0` to run without AI | |
| # - Keep dataset files under `data/` for the unlock/download flow | |
| # """ | |
| # ) | |
| if __name__ == "__main__": | |
| demo.launch() | |
| # Some environments cannot access localhost directly (proxy/restricted networks). | |
| # Enable a share link by default, configurable with GRADIO_SHARE=0 to disable. | |
| # use_share = os.getenv("GRADIO_SHARE", "1").strip() != "0" | |
| # demo.launch(share=use_share) | |