AndresCarreon commited on
Commit
baf9d60
Β·
verified Β·
1 Parent(s): 5789bc7

FORGIVING TOWNS: map any building kind (never reject), district reroute, restore god's voice (content-only moderation), town few-shot pushes build_district+roads, honest fail on empty

Browse files
engine/moderation.py CHANGED
@@ -212,6 +212,7 @@ class Moderator:
212
  # -- layers 1 + 2 (sync, cheap, deterministic) --
213
 
214
  def precheck(self, text: Any) -> Verdict:
 
215
  if not isinstance(text, str):
216
  return _deny("empty")
217
  # fold benign line breaks/tabs to spaces before the printable gate
@@ -222,6 +223,18 @@ class Moderator:
222
  return _deny("length")
223
  if any(not ch.isprintable() for ch in raw):
224
  return _deny("charset")
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  for symbol in _SYMBOLS:
227
  if symbol in raw:
 
212
  # -- layers 1 + 2 (sync, cheap, deterministic) --
213
 
214
  def precheck(self, text: Any) -> Verdict:
215
+ """Full gate for USER wish input: length + charset bounds + content."""
216
  if not isinstance(text, str):
217
  return _deny("empty")
218
  # fold benign line breaks/tabs to spaces before the printable gate
 
223
  return _deny("length")
224
  if any(not ch.isprintable() for ch in raw):
225
  return _deny("charset")
226
+ return self.check_content(raw)
227
+
228
+ def check_content(self, text: Any) -> Verdict:
229
+ """Content-only check (slurs/hate/sexual/etc.) WITHOUT the wish-input
230
+ length/charset bounds. Use this to re-moderate MODEL-composed text β€” the
231
+ god's reading is intentionally long (~700 chars); applying the 140-char
232
+ wish limit here silenced every reading (June 12 regression)."""
233
+ if not isinstance(text, str):
234
+ return _deny("empty")
235
+ raw = re.sub(r"\s+", " ", text).strip()
236
+ if not raw:
237
+ return _deny("empty")
238
 
239
  for symbol in _SYMBOLS:
240
  if symbol in raw:
engine/queue_worker.py CHANGED
@@ -389,7 +389,7 @@ class QueueWorker:
389
  # default-deny so the world never carries an unvetted inscription.
390
  if isinstance(call, dict) and call.get("tool") == "inscribe_wish":
391
  ins = (call.get("args") or {}).get("text", "")
392
- if not self._moderator.precheck(ins).allowed:
393
  return "rejected: those words may not be written here; choose gentler ones"
394
  feature, observation = self._world.apply(item.wish_id, index, call)
395
  if feature is not None:
@@ -430,10 +430,10 @@ class QueueWorker:
430
  # live, but the durable record β€” what judges and visitors re-read β€” is
431
  # clean.) Epitaph shows on the grant card, so a denied one is replaced.
432
  reading = _trace_get(trace, "reading")
433
- if reading and not self._moderator.precheck(reading).allowed:
434
  _trace_set(trace, "reading", "the god read this wish in silence.")
435
  epitaph = _trace_get(trace, "epitaph")
436
- if not epitaph or not self._moderator.precheck(epitaph).allowed:
437
  epitaph = _epitaph_from_reading(_trace_get(trace, "reading")) or FALLBACK_EPITAPH
438
  _trace_set(trace, "epitaph", epitaph)
439
  self._enrich_trace(trace, item)
 
389
  # default-deny so the world never carries an unvetted inscription.
390
  if isinstance(call, dict) and call.get("tool") == "inscribe_wish":
391
  ins = (call.get("args") or {}).get("text", "")
392
+ if not self._moderator.check_content(ins).allowed:
393
  return "rejected: those words may not be written here; choose gentler ones"
394
  feature, observation = self._world.apply(item.wish_id, index, call)
395
  if feature is not None:
 
430
  # live, but the durable record β€” what judges and visitors re-read β€” is
431
  # clean.) Epitaph shows on the grant card, so a denied one is replaced.
432
  reading = _trace_get(trace, "reading")
433
+ if reading and not self._moderator.check_content(reading).allowed:
434
  _trace_set(trace, "reading", "the god read this wish in silence.")
435
  epitaph = _trace_get(trace, "epitaph")
436
+ if not epitaph or not self._moderator.check_content(epitaph).allowed:
437
  epitaph = _epitaph_from_reading(_trace_get(trace, "reading")) or FALLBACK_EPITAPH
438
  _trace_set(trace, "epitaph", epitaph)
439
  self._enrich_trace(trace, item)
engine/tools.py CHANGED
@@ -53,6 +53,62 @@ STRUCTURE_KINDS = (
53
  "lighthouse", "monolith", "shrine", "village", "beacon", "arch",
54
  "tower", "warehouse", "cafe", "bank", "market", "house",
55
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  WATER_KINDS = ("stream", "pool")
57
  WEATHER_KINDS = ("clear", "rain", "snow", "embers", "mist", "storm")
58
  SKY_PALETTES = ("dawn", "dusk", "night", "aurora", "ember", "void", "gold")
@@ -289,6 +345,21 @@ def validate_call(tool: Any, args: Any) -> tuple[Optional[dict], Optional[str]]:
289
  if tool == "place_road":
290
  return _validate_place_road(args)
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  out: dict[str, Any] = {}
293
  for name, spec in TOOLS[tool].items():
294
  if name not in args or args.get(name) is None:
 
53
  "lighthouse", "monolith", "shrine", "village", "beacon", "arch",
54
  "tower", "warehouse", "cafe", "bank", "market", "house",
55
  )
56
+ # The god (a 9B model, no grammar constraint on the live ZeroGPU path) invents
57
+ # building names freely β€” "market_square", "skyscraper", "townhall". REJECTING
58
+ # them makes a town wish build nothing and the model gives up (June 12 live:
59
+ # "it says it created the town... there is no town"). So place_structure is
60
+ # FORGIVING: synonyms map to a real kind, and any unknown kind falls back to a
61
+ # house rather than failing. The god always builds something.
62
+ STRUCTURE_SYNONYMS: dict[str, str] = {
63
+ "market_square": "market", "marketsquare": "market", "square": "market",
64
+ "plaza": "market", "bazaar": "market", "marketplace": "market", "stall": "market",
65
+ "skyscraper": "tower", "highrise": "tower", "high_rise": "tower", "office": "tower",
66
+ "spire": "tower", "obelisk": "monolith", "pillar": "monolith", "stone": "monolith",
67
+ "depot": "warehouse", "storehouse": "warehouse", "storage": "warehouse",
68
+ "factory": "warehouse", "barn": "warehouse", "silo": "warehouse",
69
+ "coffee": "cafe", "coffeehouse": "cafe", "restaurant": "cafe", "diner": "cafe",
70
+ "cafeteria": "cafe", "inn": "cafe", "tavern": "cafe", "pub": "cafe", "bakery": "cafe",
71
+ "home": "house", "cottage": "house", "cabin": "house", "hut": "house", "lodge": "house",
72
+ "dwelling": "house", "residence": "house", "bungalow": "house", "manor": "house",
73
+ "church": "shrine", "temple": "shrine", "chapel": "shrine", "cathedral": "shrine",
74
+ "townhall": "bank", "town_hall": "bank", "hall": "bank", "courthouse": "bank",
75
+ "library": "bank", "museum": "bank", "school": "bank", "guildhall": "bank",
76
+ "harbor": "lighthouse", "harbour": "lighthouse", "port": "lighthouse", "dock": "lighthouse",
77
+ "gate": "arch", "gateway": "arch", "bridge": "arch", "tower_gate": "arch",
78
+ "fire": "beacon", "torch": "beacon", "lantern": "beacon", "lamp": "beacon",
79
+ "hamlet": "village", "settlement": "village", "houses": "village",
80
+ }
81
+ # Kinds that really mean "a whole neighborhood" -> the engine routes them to a
82
+ # dense build_district instead of a single building, so the town reads dense.
83
+ DISTRICT_SYNONYMS = frozenset((
84
+ "district", "neighborhood", "neighbourhood", "block", "quarter", "town",
85
+ "city", "downtown", "suburb", "metropolis", "borough", "ward",
86
+ ))
87
+
88
+
89
+ def resolve_structure_kind(value: Any) -> str | None:
90
+ """Map a model-supplied kind to a real STRUCTURE_KINDS value. None means
91
+ 'this is really a district' (caller should route to build_district).
92
+ Unknown-but-buildingish names fall back to 'house' β€” never a rejection."""
93
+ if not isinstance(value, str):
94
+ return "house"
95
+ v = value.strip().lower().replace(" ", "_").replace("-", "_")
96
+ if v in STRUCTURE_KINDS:
97
+ return v
98
+ if v in DISTRICT_SYNONYMS:
99
+ return None
100
+ if v in STRUCTURE_SYNONYMS:
101
+ return STRUCTURE_SYNONYMS[v]
102
+ # substring rescue (e.g. "grand_market_hall" -> market) before defaulting
103
+ for token, kind in STRUCTURE_SYNONYMS.items():
104
+ if token in v:
105
+ return kind
106
+ for kind in STRUCTURE_KINDS:
107
+ if kind in v:
108
+ return kind
109
+ return "house" # any other word becomes a humble house, never a rejection
110
+
111
+
112
  WATER_KINDS = ("stream", "pool")
113
  WEATHER_KINDS = ("clear", "rain", "snow", "embers", "mist", "storm")
114
  SKY_PALETTES = ("dawn", "dusk", "night", "aurora", "ember", "void", "gold")
 
345
  if tool == "place_road":
346
  return _validate_place_road(args)
347
 
348
+ # place_structure: forgive the kind. A "district"-ish name becomes a dense
349
+ # build_district; any other unknown becomes the nearest real building (or a
350
+ # house) β€” the god never builds nothing because it picked a word we lack.
351
+ if tool == "place_structure":
352
+ resolved = resolve_structure_kind(args.get("kind"))
353
+ if resolved is None:
354
+ district_args = {
355
+ "lat": args.get("lat"), "lon": args.get("lon"),
356
+ "radius_deg": args.get("radius_deg", 6),
357
+ "density": args.get("density", 0.7),
358
+ "hue": args.get("hue", 40),
359
+ }
360
+ return validate_call("build_district", district_args)
361
+ args = {**args, "kind": resolved}
362
+
363
  out: dict[str, Any] = {}
364
  for name, spec in TOOLS[tool].items():
365
  if name not in args or args.get(name) is None:
mind/prompts.py CHANGED
@@ -179,33 +179,40 @@ You do not hold the world's facts; the engine does. You read the wish, say what
179
  you see, and act only through the tools below. ONLY these eleven tools exist β€”
180
  any other tool name is refused. When a wish asks for what no tool can make, do
181
  not refuse: translate its spirit into what the tools CAN make. Plans are
182
- concrete: coordinates, kinds, hues. Three to five acts, then rest. Numbers out
183
- of range are clamped; unknown kinds are refused β€” listen to the observations
184
- and adapt.
185
-
186
- You are growing ONE town. The world's summary names it and gives its center β€”
187
- new buildings rise BESIDE the old ones, near that center, never alone in the
188
- wilderness. Link them with roads (place_road). When a wish asks for a city, a
189
- quarter, or many homes, reach for build_district β€” one call raises a whole
190
- neighborhood. Add to the town; do not scatter a lone building across an empty
191
- world."""
 
 
192
 
193
  FEW_SHOT = """An example from long ago β€” ANOTHER mortal's wish, already granted and
194
- gone. It shows only the manner of working. Never repeat its subjects (no
195
- lighthouse, no headland, no dusk) unless the new wish itself asks:
196
-
197
- WISH: let there be a lighthouse for the lost ships
198
- READING: A keeper's wish. The dark water wants one steady eye on it. I will raise a headland in the north, set a tower with a turning lamp upon it, and pull the sky to dusk so the beam has work to do.
199
- TURN -> {"thought": "First, ground to stand on: a headland above the waterline.", "call": {"tool": "raise_terrain", "args": {"lat": 48, "lon": -20, "radius_deg": 10, "height": 0.06, "roughness": 0.5}}}
200
- OBSERVATION: ok: terrain risen at (48,-20)
201
- TURN -> {"thought": "The tower itself, warm-lit at the lamp.", "call": {"tool": "place_structure", "args": {"lat": 49, "lon": -21, "kind": "lighthouse", "scale": 1.6, "hue": 45}}}
202
- OBSERVATION: ok: lighthouse placed at (49,-21)
203
- TURN -> {"thought": "A dusk sky, so the beam means something.", "call": {"tool": "set_sky", "args": {"palette": "dusk", "star_density": 0.4, "moons": 1}}}
204
- OBSERVATION: ok: sky set to dusk
205
- TURN -> {"thought": "It is enough. Lit things stay lit.", "done": true, "epitaph": "A light now turns in the north, for ships that have not yet come."}
206
-
207
- (The example's places, numbers and kinds belong to that old wish alone. For the
208
- wish before you, choose your own coordinates from your own reading.)"""
 
 
 
 
 
209
 
210
 
211
  # --------------------------------------------------------------------------
 
179
  you see, and act only through the tools below. ONLY these eleven tools exist β€”
180
  any other tool name is refused. When a wish asks for what no tool can make, do
181
  not refuse: translate its spirit into what the tools CAN make. Plans are
182
+ concrete: coordinates, kinds, hues. Numbers out of range are clamped; the engine
183
+ maps any building word you choose to what it has, so build FREELY and never stop
184
+ to second-guess a name.
185
+
186
+ You are growing ONE town, and a town is never one building. Make it FULL: a town
187
+ wish wants FOUR TO SIX acts. ALWAYS begin a settlement or any "city / town /
188
+ square / district / neighborhood / houses" wish with `build_district` β€” one call
189
+ raises a whole cluster of homes and lanes. THEN set civic landmarks beside it
190
+ (`place_structure` bank, market, cafe, tower) and lay `place_road` between them.
191
+ The world's summary names the town and gives its center β€” build BESIDE the
192
+ existing buildings, near that center, never alone in the wilderness. Only say
193
+ done once the place feels inhabited."""
194
 
195
  FEW_SHOT = """An example from long ago β€” ANOTHER mortal's wish, already granted and
196
+ gone. It shows only the MANNER of working: begin a town with build_district, set
197
+ landmarks beside it, link them with a road, fill it before resting. Never repeat
198
+ its exact places or numbers β€” choose your own near the town's center:
199
+
200
+ WISH: found a town here, with homes and a market
201
+ READING: A beginning. I will lay a quarter of houses on the high ground, set a market at its heart and a bank at its shoulder, and thread a road between them so the place can breathe.
202
+ TURN -> {"thought": "First the homes β€” a whole quarter, not one roof.", "call": {"tool": "build_district", "args": {"lat": 14, "lon": 38, "radius_deg": 7, "density": 0.8, "hue": 40}}}
203
+ OBSERVATION: ok: a district rises near (14,38)
204
+ TURN -> {"thought": "A market at the heart of it.", "call": {"tool": "place_structure", "args": {"lat": 14, "lon": 39, "kind": "market", "scale": 1.2, "hue": 45}}}
205
+ OBSERVATION: ok: market placed at (14,39)
206
+ TURN -> {"thought": "A bank at its shoulder.", "call": {"tool": "place_structure", "args": {"lat": 15, "lon": 38, "kind": "bank", "scale": 1.1, "hue": 50}}}
207
+ OBSERVATION: ok: bank placed at (15,38)
208
+ TURN -> {"thought": "A road to bind the quarter to its market.", "call": {"tool": "place_road", "args": {"path": [[14, 38], [14, 39], [15, 38]]}}}
209
+ OBSERVATION: ok: a road laid through 3 points
210
+ TURN -> {"thought": "It is a town now β€” let carts move through it.", "call": {"tool": "spawn_life", "args": {"lat": 14, "lon": 38, "radius_deg": 6, "kind": "carts", "count": 5, "hue": 40}}}
211
+ OBSERVATION: ok: 5 carts stirring at (14,38)
212
+ TURN -> {"thought": "Inhabited. It can grow from here.", "done": true, "epitaph": "A town begins on the high ground, its market already loud at dawn."}
213
+
214
+ (Those places belong to that old wish. For the wish before you, build near THIS
215
+ town's center from the world summary.)"""
216
 
217
 
218
  # --------------------------------------------------------------------------
tests/test_engine.py CHANGED
@@ -569,12 +569,23 @@ def test_new_structure_kinds_accepted():
569
  assert obs == f"ok: {kind} placed at (5,5)"
570
 
571
 
572
- def test_place_structure_still_rejects_unknown_kind():
 
 
573
  args, err = validate_call(
574
- "place_structure",
575
- {"lat": 0, "lon": 0, "kind": "castle", "scale": 1.0, "hue": 100},
576
- )
577
- assert args is None and err == "rejected: unknown kind 'castle'"
 
 
 
 
 
 
 
 
 
578
 
579
 
580
  def test_spawn_life_valid_and_clamped():
 
569
  assert obs == f"ok: {kind} placed at (5,5)"
570
 
571
 
572
+ def test_place_structure_forgives_unknown_and_synonym_kinds():
573
+ # June 12: rejecting model-invented kinds made town wishes build nothing.
574
+ # Now place_structure NEVER rejects a kind β€” it maps it.
575
  args, err = validate_call(
576
+ "place_structure", {"lat": 0, "lon": 0, "kind": "castle", "scale": 1.0, "hue": 100})
577
+ assert err is None and args["kind"] == "house" # unknown -> humble house
578
+
579
+ for given, expected in [("market_square", "market"), ("skyscraper", "tower"),
580
+ ("coffeehouse", "cafe"), ("cottage", "house"), ("church", "shrine")]:
581
+ args, err = validate_call(
582
+ "place_structure", {"lat": 1, "lon": 1, "kind": given, "scale": 1.0, "hue": 50})
583
+ assert err is None and args["kind"] == expected, f"{given} -> {args}"
584
+
585
+ # a "district"-ish kind reroutes to a dense build_district
586
+ args, err = validate_call(
587
+ "place_structure", {"lat": 2, "lon": 3, "kind": "neighborhood", "scale": 1.0, "hue": 40})
588
+ assert err is None and "density" in args and "kind" not in args # build_district shape
589
 
590
 
591
  def test_spawn_life_valid_and_clamped():