Merge branch 'develop'
Browse files# Conflicts:
# shared/inference_client.py
- projects/tinybard/app.py +12 -35
- shared/inference_client.py +2 -2
projects/tinybard/app.py
CHANGED
|
@@ -191,9 +191,12 @@ def generate_procedural_step(genre: str, step: int, health: int, choice: str = "
|
|
| 191 |
def _parse_messages(genre: str, history: List[Dict[str, str]], next_instruction: str) -> list[Dict[str, str]]:
|
| 192 |
"""Translate internal history into OpenAI-style chat messages."""
|
| 193 |
system = (
|
| 194 |
-
"You are the
|
|
|
|
| 195 |
f"Genre: {genre}. Write in the second person ('You...'). "
|
| 196 |
-
"Keep descriptions
|
|
|
|
|
|
|
| 197 |
"Focus on action, mystery, and choice. Do not offer numbered choices unless asked."
|
| 198 |
)
|
| 199 |
msgs: List[Dict[str, str]] = [{"role": "system", "content": system}]
|
|
@@ -213,9 +216,8 @@ def generate_llm_story(
|
|
| 213 |
max_tokens: int = 180,
|
| 214 |
) -> str:
|
| 215 |
"""Generate story text via HF Inference API (with cooldown)."""
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
return ""
|
| 219 |
try:
|
| 220 |
msgs = _parse_messages(genre, history, next_instruction)
|
| 221 |
result = inference_generate(
|
|
@@ -226,7 +228,6 @@ def generate_llm_story(
|
|
| 226 |
)
|
| 227 |
return result.text
|
| 228 |
except RuntimeError:
|
| 229 |
-
# Cooldown — let caller fall back
|
| 230 |
return ""
|
| 231 |
except Exception as e:
|
| 232 |
log.warning(f"HF Inference error (fallback to procedural): {e}")
|
|
@@ -235,8 +236,9 @@ def generate_llm_story(
|
|
| 235 |
|
| 236 |
def generate_llm_choices(genre: str, story_context: str) -> List[str]:
|
| 237 |
"""Ask the LLM to produce 3 short distinct choices for the player."""
|
| 238 |
-
|
| 239 |
-
|
|
|
|
| 240 |
system = (
|
| 241 |
"You generate 3 short, distinct player choices for an interactive text adventure. "
|
| 242 |
"Output exactly in the format: 1. <choice> | 2. <choice> | 3. <choice>"
|
|
@@ -806,27 +808,12 @@ def create_gradio_app() -> gr.Blocks:
|
|
| 806 |
story, choices, h, s, go, hist = api_start_game(genre)
|
| 807 |
return story, choices, h, s, go, hist, "", gr.update(choices=choices or [], value=None)
|
| 808 |
|
| 809 |
-
# API endpoints (preserved for MCP)
|
| 810 |
-
start_btn.click(
|
| 811 |
-
fn=api_start_game,
|
| 812 |
-
inputs=[genre_input],
|
| 813 |
-
outputs=[story_output, choices_output, health_output, step_output, game_over_output, history_output],
|
| 814 |
-
api_name="start_game",
|
| 815 |
-
)
|
| 816 |
-
|
| 817 |
# UI start game button: also updates choices radio and clears text
|
| 818 |
start_btn.click(
|
| 819 |
fn=handle_start_game,
|
| 820 |
inputs=[genre_input],
|
| 821 |
outputs=[story_output, choices_output, health_output, step_output, game_over_output, history_output, choice_text_input, choice_radio],
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
# API make_choice endpoint (preserved for MCP)
|
| 825 |
-
choice_btn.click(
|
| 826 |
-
fn=api_make_choice,
|
| 827 |
-
inputs=[choice_text_input, genre_input, step_input, health_input, history_input],
|
| 828 |
-
outputs=[story_output, choices_output, health_output, step_output, game_over_output, history_output],
|
| 829 |
-
api_name="make_choice",
|
| 830 |
)
|
| 831 |
|
| 832 |
# UI make choice button: resolves radio/text, updates choices radio
|
|
@@ -834,6 +821,7 @@ def create_gradio_app() -> gr.Blocks:
|
|
| 834 |
fn=handle_make_choice,
|
| 835 |
inputs=[choice_text_input, choice_radio, genre_input, step_input, health_input, history_input],
|
| 836 |
outputs=[story_output, choices_output, health_output, step_output, game_over_output, history_output, choice_text_input, choice_radio],
|
|
|
|
| 837 |
)
|
| 838 |
|
| 839 |
# Save game handler
|
|
@@ -961,14 +949,7 @@ def _run_turn(choice: str, genre: str, step: int, health: int, history: List[Dic
|
|
| 961 |
Returns a dict the frontend can consume directly. Used by both the
|
| 962 |
FastAPI /api/game/* endpoints and the Gradio MCP tools.
|
| 963 |
"""
|
| 964 |
-
# Cooldown short-circuit: if active, the game just uses the procedural
|
| 965 |
-
# engine for this turn. This protects your HF/Modal credit budget.
|
| 966 |
-
in_cooldown = cooldown_active("tinybard")
|
| 967 |
-
|
| 968 |
if step == 0:
|
| 969 |
-
# New game
|
| 970 |
-
if in_cooldown:
|
| 971 |
-
return generate_procedural_step(genre, 0, 100)
|
| 972 |
instruction = "Narrate the beginning of the adventure. What happens first? Do not offer choices yet."
|
| 973 |
story = generate_llm_story(genre, [], instruction)
|
| 974 |
if not story:
|
|
@@ -983,10 +964,6 @@ def _run_turn(choice: str, genre: str, step: int, health: int, history: List[Dic
|
|
| 983 |
"genre": genre,
|
| 984 |
}
|
| 985 |
|
| 986 |
-
# Subsequent turn
|
| 987 |
-
if in_cooldown:
|
| 988 |
-
return generate_procedural_step(genre, step, health, choice)
|
| 989 |
-
|
| 990 |
history.append({"role": "player", "text": choice})
|
| 991 |
health_delta = random.choice([-15, 0, 10])
|
| 992 |
new_health = max(0, min(100, health + health_delta))
|
|
|
|
| 191 |
def _parse_messages(genre: str, history: List[Dict[str, str]], next_instruction: str) -> list[Dict[str, str]]:
|
| 192 |
"""Translate internal history into OpenAI-style chat messages."""
|
| 193 |
system = (
|
| 194 |
+
"You are Nanaboozhoo, the trickster storyteller of Anishinaabe tradition. "
|
| 195 |
+
"You spin interactive text adventures with wit, mischief, and wonder. "
|
| 196 |
f"Genre: {genre}. Write in the second person ('You...'). "
|
| 197 |
+
"Keep descriptions atmospheric but concise (2-3 sentences). "
|
| 198 |
+
"Be unpredictable — every story beat should surprise. "
|
| 199 |
+
"Never repeat the same scene twice. "
|
| 200 |
"Focus on action, mystery, and choice. Do not offer numbered choices unless asked."
|
| 201 |
)
|
| 202 |
msgs: List[Dict[str, str]] = [{"role": "system", "content": system}]
|
|
|
|
| 216 |
max_tokens: int = 180,
|
| 217 |
) -> str:
|
| 218 |
"""Generate story text via HF Inference API (with cooldown)."""
|
| 219 |
+
from shared.inference_client import force_clear_cooldown
|
| 220 |
+
force_clear_cooldown("tinybard")
|
|
|
|
| 221 |
try:
|
| 222 |
msgs = _parse_messages(genre, history, next_instruction)
|
| 223 |
result = inference_generate(
|
|
|
|
| 228 |
)
|
| 229 |
return result.text
|
| 230 |
except RuntimeError:
|
|
|
|
| 231 |
return ""
|
| 232 |
except Exception as e:
|
| 233 |
log.warning(f"HF Inference error (fallback to procedural): {e}")
|
|
|
|
| 236 |
|
| 237 |
def generate_llm_choices(genre: str, story_context: str) -> List[str]:
|
| 238 |
"""Ask the LLM to produce 3 short distinct choices for the player."""
|
| 239 |
+
# Always clear cooldown for choices — they follow a story call in the same turn
|
| 240 |
+
from shared.inference_client import force_clear_cooldown
|
| 241 |
+
force_clear_cooldown("tinybard")
|
| 242 |
system = (
|
| 243 |
"You generate 3 short, distinct player choices for an interactive text adventure. "
|
| 244 |
"Output exactly in the format: 1. <choice> | 2. <choice> | 3. <choice>"
|
|
|
|
| 808 |
story, choices, h, s, go, hist = api_start_game(genre)
|
| 809 |
return story, choices, h, s, go, hist, "", gr.update(choices=choices or [], value=None)
|
| 810 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
# UI start game button: also updates choices radio and clears text
|
| 812 |
start_btn.click(
|
| 813 |
fn=handle_start_game,
|
| 814 |
inputs=[genre_input],
|
| 815 |
outputs=[story_output, choices_output, health_output, step_output, game_over_output, history_output, choice_text_input, choice_radio],
|
| 816 |
+
api_name="start_game",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
)
|
| 818 |
|
| 819 |
# UI make choice button: resolves radio/text, updates choices radio
|
|
|
|
| 821 |
fn=handle_make_choice,
|
| 822 |
inputs=[choice_text_input, choice_radio, genre_input, step_input, health_input, history_input],
|
| 823 |
outputs=[story_output, choices_output, health_output, step_output, game_over_output, history_output, choice_text_input, choice_radio],
|
| 824 |
+
api_name="make_choice",
|
| 825 |
)
|
| 826 |
|
| 827 |
# Save game handler
|
|
|
|
| 949 |
Returns a dict the frontend can consume directly. Used by both the
|
| 950 |
FastAPI /api/game/* endpoints and the Gradio MCP tools.
|
| 951 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
if step == 0:
|
|
|
|
|
|
|
|
|
|
| 953 |
instruction = "Narrate the beginning of the adventure. What happens first? Do not offer choices yet."
|
| 954 |
story = generate_llm_story(genre, [], instruction)
|
| 955 |
if not story:
|
|
|
|
| 964 |
"genre": genre,
|
| 965 |
}
|
| 966 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 967 |
history.append({"role": "player", "text": choice})
|
| 968 |
health_delta = random.choice([-15, 0, 10])
|
| 969 |
new_health = max(0, min(100, health + health_delta))
|
shared/inference_client.py
CHANGED
|
@@ -34,9 +34,9 @@ INFERENCE_MODEL = os.environ.get(
|
|
| 34 |
"meta-llama/Llama-3.2-1B-Instruct", # 1B, free-tier, great prose
|
| 35 |
)
|
| 36 |
|
| 37 |
-
# Provider: "hf-inference" (free serverless), "together", "fal-ai", "replicate"
|
| 38 |
# Free HF inference works for many small models; otherwise use a paid provider.
|
| 39 |
-
INFERENCE_PROVIDER = os.environ.get("INFERENCE_PROVIDER",
|
| 40 |
|
| 41 |
# Token — read from HF Space secrets at runtime.
|
| 42 |
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
|
|
|
|
| 34 |
"meta-llama/Llama-3.2-1B-Instruct", # 1B, free-tier, great prose
|
| 35 |
)
|
| 36 |
|
| 37 |
+
# Provider: "featherless-ai" (supports small instruct models), "hf-inference" (free serverless), "together", "fal-ai", "replicate"
|
| 38 |
# Free HF inference works for many small models; otherwise use a paid provider.
|
| 39 |
+
INFERENCE_PROVIDER = os.environ.get("INFERENCE_PROVIDER", "featherless-ai")
|
| 40 |
|
| 41 |
# Token — read from HF Space secrets at runtime.
|
| 42 |
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
|