Upload 3 files
Browse files
README.md
CHANGED
|
@@ -13,7 +13,7 @@ pinned: false
|
|
| 13 |
|
| 14 |
An AI-involved multiple-choice I Spy game from ZygAI Research.
|
| 15 |
|
| 16 |
-
The app uses `meta-llama/Llama-3.1-8B-Instruct` as the game master.
|
| 17 |
|
| 18 |
## Hugging Face Space Setup
|
| 19 |
|
|
|
|
| 13 |
|
| 14 |
An AI-involved multiple-choice I Spy game from ZygAI Research.
|
| 15 |
|
| 16 |
+
The app uses `meta-llama/Llama-3.1-8B-Instruct` as the game master. When the player starts, Llama generates all 10 clues, answer sets, correct answers, and explanations in one request. The game then plays from that cached set, which keeps the demo cost-effective.
|
| 17 |
|
| 18 |
## Hugging Face Space Setup
|
| 19 |
|
app.py
CHANGED
|
@@ -82,7 +82,7 @@ def get_client() -> InferenceClient | None:
|
|
| 82 |
return InferenceClient(model=MODEL_ID, token=HF_TOKEN)
|
| 83 |
|
| 84 |
|
| 85 |
-
def call_llama(messages: list[dict[str, str]], max_tokens: int =
|
| 86 |
client = get_client()
|
| 87 |
if not client:
|
| 88 |
raise RuntimeError("HF_TOKEN is not configured.")
|
|
@@ -115,7 +115,25 @@ def fallback_round(round_number: int) -> dict[str, Any]:
|
|
| 115 |
return data
|
| 116 |
|
| 117 |
|
| 118 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
try:
|
| 120 |
model_text = call_llama(
|
| 121 |
[
|
|
@@ -123,37 +141,25 @@ def generate_round(round_number: int, previous_answers: list[str]) -> dict[str,
|
|
| 123 |
{
|
| 124 |
"role": "user",
|
| 125 |
"content": (
|
| 126 |
-
f"Create
|
| 127 |
-
"
|
| 128 |
-
"
|
| 129 |
-
"
|
| 130 |
-
f"{', '.join(previous_answers) or 'none'}. "
|
| 131 |
"Return only JSON with this exact shape: "
|
| 132 |
-
'{"clue":"I see something that...","answer":"An umbrella",'
|
| 133 |
'"options":["An umbrella","A chair","A candle","A spoon"],'
|
| 134 |
-
'"explanation":"The umbrella keeps rain off you."}'
|
| 135 |
),
|
| 136 |
},
|
| 137 |
]
|
| 138 |
)
|
| 139 |
data = extract_json(model_text)
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if len(options) != 4 or answer not in options:
|
| 146 |
-
raise ValueError("Round must have four options and include the answer.")
|
| 147 |
-
|
| 148 |
-
random.shuffle(options)
|
| 149 |
-
return {
|
| 150 |
-
"clue": clue,
|
| 151 |
-
"answer": answer,
|
| 152 |
-
"options": options,
|
| 153 |
-
"explanation": explanation or f"The answer is {answer}.",
|
| 154 |
-
}
|
| 155 |
except Exception:
|
| 156 |
-
return fallback_round(round_number)
|
| 157 |
|
| 158 |
|
| 159 |
def option_text(round_data: dict[str, Any], index: int) -> str:
|
|
@@ -179,13 +185,13 @@ def render_question(state: dict[str, Any] | None) -> tuple[str, str, str, str, s
|
|
| 179 |
|
| 180 |
|
| 181 |
def start_game() -> tuple[dict[str, Any], str, str, str, str, str, str, str, str]:
|
| 182 |
-
|
| 183 |
state = {
|
| 184 |
"round_number": 1,
|
| 185 |
"score": 0,
|
| 186 |
"answered": False,
|
| 187 |
-
"
|
| 188 |
-
"round":
|
| 189 |
}
|
| 190 |
return state, *render_question(state), "", gr.update(visible=False)
|
| 191 |
|
|
@@ -226,13 +232,11 @@ def next_round(state: dict[str, Any] | None) -> tuple[dict[str, Any] | None, str
|
|
| 226 |
return state, message, "FINISHED", f"SCORE: {score}", "A", "B", "C", "D", "Start a new game to play again.", gr.update(visible=False)
|
| 227 |
|
| 228 |
round_number = state["round_number"] + 1
|
| 229 |
-
round_data = generate_round(round_number, state["previous_answers"])
|
| 230 |
state.update(
|
| 231 |
{
|
| 232 |
"round_number": round_number,
|
| 233 |
"answered": False,
|
| 234 |
-
"round":
|
| 235 |
-
"previous_answers": state["previous_answers"] + [round_data["answer"]],
|
| 236 |
}
|
| 237 |
)
|
| 238 |
return state, *render_question(state), "", gr.update(visible=False)
|
|
|
|
| 82 |
return InferenceClient(model=MODEL_ID, token=HF_TOKEN)
|
| 83 |
|
| 84 |
|
| 85 |
+
def call_llama(messages: list[dict[str, str]], max_tokens: int = 1800) -> str:
|
| 86 |
client = get_client()
|
| 87 |
if not client:
|
| 88 |
raise RuntimeError("HF_TOKEN is not configured.")
|
|
|
|
| 115 |
return data
|
| 116 |
|
| 117 |
|
| 118 |
+
def validate_round(data: dict[str, Any]) -> dict[str, Any]:
|
| 119 |
+
clue = clean_text(data["clue"])
|
| 120 |
+
answer = clean_text(data["answer"])
|
| 121 |
+
options = [clean_text(option) for option in data["options"]]
|
| 122 |
+
explanation = clean_text(data.get("explanation", ""))
|
| 123 |
+
|
| 124 |
+
if len(options) != 4 or answer not in options:
|
| 125 |
+
raise ValueError("Round must have four options and include the answer.")
|
| 126 |
+
|
| 127 |
+
random.shuffle(options)
|
| 128 |
+
return {
|
| 129 |
+
"clue": clue,
|
| 130 |
+
"answer": answer,
|
| 131 |
+
"options": options,
|
| 132 |
+
"explanation": explanation or f"The answer is {answer}.",
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def generate_game() -> list[dict[str, Any]]:
|
| 137 |
try:
|
| 138 |
model_text = call_llama(
|
| 139 |
[
|
|
|
|
| 141 |
{
|
| 142 |
"role": "user",
|
| 143 |
"content": (
|
| 144 |
+
f"Create all {TOTAL_ROUNDS} rounds for one I See demo game. "
|
| 145 |
+
"Each round needs a short I Spy-style clue, four answer options, "
|
| 146 |
+
"one correct answer that appears exactly in options, and one short explanation. "
|
| 147 |
+
"Use varied everyday objects. Do not repeat correct answers. "
|
|
|
|
| 148 |
"Return only JSON with this exact shape: "
|
| 149 |
+
'{"rounds":[{"clue":"I see something that...","answer":"An umbrella",'
|
| 150 |
'"options":["An umbrella","A chair","A candle","A spoon"],'
|
| 151 |
+
'"explanation":"The umbrella keeps rain off you."}]}'
|
| 152 |
),
|
| 153 |
},
|
| 154 |
]
|
| 155 |
)
|
| 156 |
data = extract_json(model_text)
|
| 157 |
+
rounds = [validate_round(round_data) for round_data in data["rounds"][:TOTAL_ROUNDS]]
|
| 158 |
+
if len(rounds) != TOTAL_ROUNDS:
|
| 159 |
+
raise ValueError("Game must include exactly 10 rounds.")
|
| 160 |
+
return rounds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
except Exception:
|
| 162 |
+
return [fallback_round(round_number) for round_number in range(1, TOTAL_ROUNDS + 1)]
|
| 163 |
|
| 164 |
|
| 165 |
def option_text(round_data: dict[str, Any], index: int) -> str:
|
|
|
|
| 185 |
|
| 186 |
|
| 187 |
def start_game() -> tuple[dict[str, Any], str, str, str, str, str, str, str, str]:
|
| 188 |
+
rounds = generate_game()
|
| 189 |
state = {
|
| 190 |
"round_number": 1,
|
| 191 |
"score": 0,
|
| 192 |
"answered": False,
|
| 193 |
+
"rounds": rounds,
|
| 194 |
+
"round": rounds[0],
|
| 195 |
}
|
| 196 |
return state, *render_question(state), "", gr.update(visible=False)
|
| 197 |
|
|
|
|
| 232 |
return state, message, "FINISHED", f"SCORE: {score}", "A", "B", "C", "D", "Start a new game to play again.", gr.update(visible=False)
|
| 233 |
|
| 234 |
round_number = state["round_number"] + 1
|
|
|
|
| 235 |
state.update(
|
| 236 |
{
|
| 237 |
"round_number": round_number,
|
| 238 |
"answered": False,
|
| 239 |
+
"round": state["rounds"][round_number - 1],
|
|
|
|
| 240 |
}
|
| 241 |
)
|
| 242 |
return state, *render_question(state), "", gr.update(visible=False)
|