byte-vortex commited on
Commit
9fa751e
·
verified ·
1 Parent(s): 4268765

Deploy Myco from CI

Browse files
README.md CHANGED
@@ -43,16 +43,27 @@ Design decisions should prioritize:
43
  - Game logic: `game/`
44
  - Mushroom model: `models/`
45
  - Mushroom catalog: `data/mushrooms.json`
 
46
  - Runtime dependency: `requirements.txt`
47
 
 
 
 
 
 
 
 
 
 
48
  ## Local development
49
 
50
  ```bash
51
  pip install -r requirements.txt
 
52
  python app.py
53
  ```
54
 
55
- Then open the local Gradio URL printed in the terminal.
56
 
57
  ## Hugging Face Space
58
 
@@ -66,3 +77,8 @@ No GitHub Actions deployment workflow is required. To deploy manually:
66
  4. The Space should install `gradio==6.16.0` from `requirements.txt` and start from `app.py`.
67
 
68
  The required runtime files are `README.md`, `app.py`, `requirements.txt`, `data/`, `game/`, `models/`, and `ui/`.
 
 
 
 
 
 
43
  - Game logic: `game/`
44
  - Mushroom model: `models/`
45
  - Mushroom catalog: `data/mushrooms.json`
46
+ - Regression tests: `tests/test_myco_core.py`
47
  - Runtime dependency: `requirements.txt`
48
 
49
+ ## Hugging Face AI companion
50
+
51
+ Myco uses the Hugging Face Inference API for both first-discovery reactions and chat replies when a token is configured in the Space. Add these Space variables/secrets:
52
+
53
+ - `HF_TOKEN=<your Hugging Face access token>`
54
+ - Optional: `MYCO_HF_MODEL_ID=meta-llama/Llama-3.1-8B-Instruct`
55
+
56
+ The default model is set in code as `DEFAULT_HF_MODEL_ID = "meta-llama/Llama-3.1-8B-Instruct"`. The app still has a local safety fallback for development and outage protection: if the token is missing or the selected model errors, Myco answers with the built-in cautious companion response instead of crashing.
57
+
58
  ## Local development
59
 
60
  ```bash
61
  pip install -r requirements.txt
62
+ python -m unittest discover -v
63
  python app.py
64
  ```
65
 
66
+ Run tests before launching, then open the local Gradio URL printed in the terminal.
67
 
68
  ## Hugging Face Space
69
 
 
77
  4. The Space should install `gradio==6.16.0` from `requirements.txt` and start from `app.py`.
78
 
79
  The required runtime files are `README.md`, `app.py`, `requirements.txt`, `data/`, `game/`, `models/`, and `ui/`.
80
+
81
+
82
+ ## Optional deploy helper
83
+
84
+ `scripts/deploy_space.py` is optional. It skips safely when `HF_BUILD_SMALL_HACKATHON_TOKEN` is not configured, and uploads to `byte-vortex/Myco` only when that token is present.
app.py CHANGED
@@ -4,6 +4,7 @@ import os
4
 
5
  os.environ.setdefault("GRADIO_SSR_MODE", "False")
6
 
 
7
  import gradio as gr
8
 
9
  from ui.gradio_app import build_app
@@ -14,6 +15,7 @@ demo = build_app()
14
 
15
  if __name__ == "__main__":
16
  demo.launch(
 
17
  theme=gr.themes.Soft(primary_hue="green", secondary_hue="emerald"),
18
  css=APP_CSS,
19
  ssr_mode=False,
 
4
 
5
  os.environ.setdefault("GRADIO_SSR_MODE", "False")
6
 
7
+ from gradio.themes import Soft
8
  import gradio as gr
9
 
10
  from ui.gradio_app import build_app
 
15
 
16
  if __name__ == "__main__":
17
  demo.launch(
18
+ theme=Soft(primary_hue="green", secondary_hue="emerald"),
19
  theme=gr.themes.Soft(primary_hue="green", secondary_hue="emerald"),
20
  css=APP_CSS,
21
  ssr_mode=False,
game/catalog.py CHANGED
@@ -14,4 +14,3 @@ def load_mushrooms() -> tuple[Mushroom, ...]:
14
  """Load the in-repo mushroom catalog."""
15
  raw_mushrooms = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
16
  return tuple(Mushroom.from_dict(item) for item in raw_mushrooms)
17
-
 
14
  """Load the in-repo mushroom catalog."""
15
  raw_mushrooms = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
16
  return tuple(Mushroom.from_dict(item) for item in raw_mushrooms)
 
game/engine.py CHANGED
@@ -95,6 +95,13 @@ def _hf_companion_answer(message, current):
95
  max_tokens=90,
96
  temperature=0.8,
97
  )
 
 
 
 
 
 
 
98
  except Exception:
99
  return None
100
 
@@ -118,6 +125,7 @@ def hf_companion_status():
118
  def _hf_token():
119
  """Return the first configured Hugging Face token without exposing it."""
120
  for env_name in HF_TOKEN_ENV_VARS:
 
121
  token = os.getenv(env_name)
122
  if token:
123
  return token
@@ -138,7 +146,7 @@ def _hf_messages(message, current):
138
  f"Lore: {mushroom.lore}"
139
  )
140
 
141
- return (
142
  {
143
  "role": "system",
144
  "content": (
@@ -151,7 +159,7 @@ def _hf_messages(message, current):
151
  "role": "user",
152
  "content": f"{mushroom_context} Player asks: {message}",
153
  },
154
- )
155
 
156
 
157
  companion_reply = myco_reply
 
95
  max_tokens=90,
96
  temperature=0.8,
97
  )
98
+ content = response.choices[0].message.content or ""
99
+ answer = content.strip()
100
+ except (AttributeError, IndexError, TypeError, ValueError):
101
+ return None
102
+ except Exception:
103
+ return None
104
+
105
  except Exception:
106
  return None
107
 
 
125
  def _hf_token():
126
  """Return the first configured Hugging Face token without exposing it."""
127
  for env_name in HF_TOKEN_ENV_VARS:
128
+ token = os.getenv(env_name, "").strip()
129
  token = os.getenv(env_name)
130
  if token:
131
  return token
 
146
  f"Lore: {mushroom.lore}"
147
  )
148
 
149
+ return [
150
  {
151
  "role": "system",
152
  "content": (
 
159
  "role": "user",
160
  "content": f"{mushroom_context} Player asks: {message}",
161
  },
162
+ ]
163
 
164
 
165
  companion_reply = myco_reply
game/state.py CHANGED
@@ -22,4 +22,4 @@ def mushroom_from_state(current: MushroomState) -> Mushroom:
22
 
23
  def collection_contains(collection: CollectionState, mushroom_name: str) -> bool:
24
  """Return whether the MycoDex already has a mushroom by name."""
25
- return any(entry["name"] == mushroom_name for entry in collection)
 
22
 
23
  def collection_contains(collection: CollectionState, mushroom_name: str) -> bool:
24
  """Return whether the MycoDex already has a mushroom by name."""
25
+ return any(entry.get("name") == mushroom_name for entry in collection)
scripts/deploy_space.py CHANGED
@@ -25,7 +25,7 @@ models/**/*.safetensors
25
 
26
  def get_ignore_patterns():
27
  """Return upload ignore patterns as a list for huggingface_hub."""
28
- return IGNORE_PATTERNS_TEXT.split()
29
 
30
 
31
  def get_hf_token():
 
25
 
26
  def get_ignore_patterns():
27
  """Return upload ignore patterns as a list for huggingface_hub."""
28
+ return [str(pattern) for pattern in IGNORE_PATTERNS_TEXT.split()]
29
 
30
 
31
  def get_hf_token():
tests/test_myco_core.py CHANGED
@@ -161,6 +161,27 @@ class MycoCoreTests(unittest.TestCase):
161
  self.assertIn(current["name"], updated_history[-1]["content"])
162
  self.assertIn("unknown", updated_history[-1]["content"].lower())
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  def test_collect_current_prevents_duplicates(self):
165
  _mushroom, current, history = discover_mushroom([])
166
  collection, history = collect_current(current, [], history)
@@ -173,6 +194,8 @@ class MycoCoreTests(unittest.TestCase):
173
  def test_study_and_eat_are_safe(self):
174
  _mushroom, current, history = discover_mushroom([])
175
  studied, study_history = study_current(current, history)
 
 
176
  eat_history = eat_current(studied, study_history)
177
 
178
  self.assertEqual(studied["studied"], "Yes")
 
161
  self.assertIn(current["name"], updated_history[-1]["content"])
162
  self.assertIn("unknown", updated_history[-1]["content"].lower())
163
 
164
+ def test_companion_api_falls_back_on_malformed_response(self):
165
+ _mushroom, current, history = discover_mushroom([])
166
+
167
+ def broken_client(token):
168
+ return SimpleNamespace(
169
+ token=token,
170
+ chat_completion=lambda **kwargs: SimpleNamespace(choices=()),
171
+ )
172
+
173
+ api_env = {
174
+ "HF_TOKEN": " fake-token ",
175
+ "HF_API_TOKEN": "",
176
+ "HF_BUILD_SMALL_HACKATHON_TOKEN": "",
177
+ }
178
+ with patch.dict(os.environ, api_env, clear=False):
179
+ with patch("game.engine.InferenceClient", broken_client):
180
+ prompt, updated_history = companion_reply("What is this?", history, current)
181
+
182
+ self.assertEqual(prompt, "")
183
+ self.assertIn(current["name"], updated_history[-1]["content"])
184
+
185
  def test_collect_current_prevents_duplicates(self):
186
  _mushroom, current, history = discover_mushroom([])
187
  collection, history = collect_current(current, [], history)
 
194
  def test_study_and_eat_are_safe(self):
195
  _mushroom, current, history = discover_mushroom([])
196
  studied, study_history = study_current(current, history)
197
+ self.assertIsNotNone(studied)
198
+ assert studied is not None
199
  eat_history = eat_current(studied, study_history)
200
 
201
  self.assertEqual(studied["studied"], "Yes")
ui/gradio_app.py CHANGED
@@ -1,5 +1,7 @@
1
  """Gradio app composition for Myco."""
2
 
 
 
3
  import gradio as gr
4
 
5
  from game.engine import (
@@ -22,7 +24,7 @@ from ui.renderers import (
22
  world_map_markdown,
23
  )
24
 
25
- EMPTY_COLLECTION = tuple()
26
 
27
  HOME_MARKDOWN = """
28
  ## Welcome to the Myco forest
@@ -106,7 +108,7 @@ def build_app():
106
  """Build the Gradio interface for Hugging Face Spaces."""
107
  with gr.Blocks(title="Myco") as demo:
108
  current_mushroom = gr.State(None)
109
- collection = gr.State(EMPTY_COLLECTION)
110
  gr.HTML(hero_markdown())
111
 
112
  with gr.Tabs():
@@ -133,7 +135,7 @@ def build_app():
133
  study_button = gr.Button("Study")
134
  with gr.Column(scale=3):
135
  chat = gr.Chatbot(
136
- value=welcome_history(),
137
  label="Myco Chat",
138
  height=360,
139
  )
 
1
  """Gradio app composition for Myco."""
2
 
3
+ from typing import Any, cast
4
+
5
  import gradio as gr
6
 
7
  from game.engine import (
 
24
  world_map_markdown,
25
  )
26
 
27
+ EMPTY_COLLECTION = None
28
 
29
  HOME_MARKDOWN = """
30
  ## Welcome to the Myco forest
 
108
  """Build the Gradio interface for Hugging Face Spaces."""
109
  with gr.Blocks(title="Myco") as demo:
110
  current_mushroom = gr.State(None)
111
+ collection = gr.State([])
112
  gr.HTML(hero_markdown())
113
 
114
  with gr.Tabs():
 
135
  study_button = gr.Button("Study")
136
  with gr.Column(scale=3):
137
  chat = gr.Chatbot(
138
+ value=cast(Any, welcome_history()),
139
  label="Myco Chat",
140
  height=360,
141
  )
ui/renderers.py CHANGED
@@ -110,8 +110,6 @@ def mushroom_card(mushroom: Mushroom | None) -> str:
110
  )
111
 
112
 
113
-
114
-
115
  def world_map_markdown(collection: CollectionState | None) -> str:
116
  """Render a game-like world map with unlocks and achievements."""
117
  count = len(_safe_collection(collection))
@@ -161,6 +159,7 @@ def _achievement_badge(label: str, unlocked: bool) -> str:
161
  icon = "🏅" if unlocked else "◇"
162
  return f'<span class="achievement {state}">{icon} {escape(label)}</span>'
163
 
 
164
  def dex_markdown(collection: CollectionState | None) -> str:
165
  """Render collected mushrooms as a compact MycoDex."""
166
  current_collection = _safe_collection(collection)
 
110
  )
111
 
112
 
 
 
113
  def world_map_markdown(collection: CollectionState | None) -> str:
114
  """Render a game-like world map with unlocks and achievements."""
115
  count = len(_safe_collection(collection))
 
159
  icon = "🏅" if unlocked else "◇"
160
  return f'<span class="achievement {state}">{icon} {escape(label)}</span>'
161
 
162
+
163
  def dex_markdown(collection: CollectionState | None) -> str:
164
  """Render collected mushrooms as a compact MycoDex."""
165
  current_collection = _safe_collection(collection)