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 +13 -0
- engine/queue_worker.py +3 -3
- engine/tools.py +71 -0
- mind/prompts.py +32 -25
- tests/test_engine.py +16 -5
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.
|
| 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.
|
| 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.
|
| 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.
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
You are growing ONE town
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
world
|
|
|
|
|
|
|
| 192 |
|
| 193 |
FEW_SHOT = """An example from long ago β ANOTHER mortal's wish, already granted and
|
| 194 |
-
gone. It shows only the
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 573 |
args, err = validate_call(
|
| 574 |
-
"place_structure",
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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():
|