Spaces:
Running on Zero
Running on Zero
File size: 10,097 Bytes
d72231c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | """NEMOCITY event-type specs — validation/clamping for the 4 city event types.
Semantics (godseed lineage):
* out-of-range NUMBERS are CLAMPED, never rejected;
* unknown kinds are repaired via SYNONYMS (anything unknown -> house);
* malformed shapes (bad cells, missing geometry) are rejected with a terse
observation string — but the only module that COMPOSES these events is the
engine itself (placement/traffic), so rejections mean engine bugs, not model
noise. The LLM never outputs coordinates.
Pure stdlib. world.py validates through `validate_call`; queue_worker uses
`resolve_kind` / `sanitize_name` / `default_name` on model-supplied fields.
"""
from __future__ import annotations
import re
import zlib
from typing import Any, Optional
from . import constants as C
from .moderation import Moderator
_moderator = Moderator() # wordlist layers only; no judge
TOOL_NAMES: tuple[str, ...] = ("place_building", "lay_road", "apply_fix", "note")
FIX_ACTIONS = ("new_road", "upgrade_avenue")
NOTE_KINDS = ("milestone", "infill", "fix")
_FALLBACK_KIND = "house"
# ----------------------------------------------------------------- kind resolver
def resolve_kind(value: Any) -> str:
"""Map model-supplied kind text to a BUILDINGS key. Never fails: synonyms
repair, substring rescues, anything else becomes a humble house."""
if not isinstance(value, str):
return _FALLBACK_KIND
v = re.sub(r"[\s\-]+", "_", value.strip().lower()).strip("_")
if v in C.BUILDINGS:
return v
if v in C.SYNONYMS:
return C.SYNONYMS[v]
if v.endswith("s") and v[:-1] in C.BUILDINGS:
return v[:-1]
for token, kind in C.SYNONYMS.items():
if token in v:
return kind
for kind in C.BUILDINGS:
if kind in v:
return kind
return _FALLBACK_KIND
# ------------------------------------------------------------------ name helpers
_STOPWORDS = frozenset((
"a", "an", "the", "and", "or", "of", "for", "with", "near", "by", "at",
"in", "on", "to", "please", "build", "make", "add", "put", "want", "like",
"new", "some", "my", "our", "me", "us", "city", "town",
))
def sanitize_name(value: Any, default: str = "") -> str:
"""Printable, wordlist-clean, <=24 chars; falls back to `default`."""
if not isinstance(value, str):
return default
name = "".join(ch for ch in value if ch.isprintable())
name = re.sub(r"\s+", " ", name).strip()[: C.NAME_MAX_LEN].strip()
if not name or not _moderator.check_content(name).allowed:
return default
return name
def default_name(kind: str, petition_text: str, wish_id: str) -> str:
"""Deterministic pleasant default derived from petition words."""
words = [
w for w in re.findall(r"[A-Za-z]+", str(petition_text or ""))
if len(w) > 2 and w.lower() not in _STOPWORDS
and w.lower() not in C.BUILDINGS and w.lower() not in C.SYNONYMS
]
label = kind.replace("_", " ").title()
if words:
pick = words[zlib.crc32(f"{wish_id}:{kind}".encode()) % len(words)].title()
candidate = f"{pick} {label}"[: C.NAME_MAX_LEN].strip()
if _moderator.check_content(candidate).allowed:
return candidate
return f"The {label}"[: C.NAME_MAX_LEN]
def street_name_for(key: str) -> str:
"""Curated street name from a deterministic key (e.g. 'w_000005:1:road')."""
return C.STREET_NAMES[zlib.crc32(str(key).encode()) % len(C.STREET_NAMES)]
# ------------------------------------------------------------------- primitives
def _err(msg: str) -> tuple[None, str]:
return None, f"rejected: {msg}"
def _int_in(value: Any, lo: int, hi: int) -> Optional[int]:
if isinstance(value, bool) or not isinstance(value, (int, float)):
try:
value = float(str(value).strip())
except (ValueError, TypeError):
return None
return int(round(min(max(float(value), lo), hi)))
def _coerce_cell(cell: Any) -> Optional[list[int]]:
if not isinstance(cell, (list, tuple)) or len(cell) < 2:
return None
cx = _int_in(cell[0], C.COORD_MIN, C.COORD_MAX)
cz = _int_in(cell[1], C.COORD_MIN, C.COORD_MAX)
if cx is None or cz is None:
return None
return [cx, cz]
def _coerce_cells(raw: Any, max_cells: int = 256) -> Optional[list[list[int]]]:
if not isinstance(raw, (list, tuple)) or not raw:
return None
out: list[list[int]] = []
seen: set[tuple[int, int]] = set()
for c in list(raw)[:max_cells]:
cell = _coerce_cell(c)
if cell is None:
return None
if (cell[0], cell[1]) in seen:
continue
seen.add((cell[0], cell[1]))
out.append(cell)
return out
def _text(value: Any, max_len: int) -> str:
s = "".join(ch for ch in str(value or "") if ch.isprintable())
return re.sub(r"\s+", " ", s).strip()[:max_len]
# ------------------------------------------------------------------- validators
def _validate_place_building(args: dict):
kind = resolve_kind(args.get("kind"))
spec = C.BUILDINGS[kind]
cx = _int_in(args.get("cx"), C.COORD_MIN, C.COORD_MAX - spec["w"] + 1)
cz = _int_in(args.get("cz"), C.COORD_MIN, C.COORD_MAX - spec["d"] + 1)
if cx is None or cz is None or args.get("cx") is None or args.get("cz") is None:
return _err("place_building needs cx, cz")
lo, hi = spec["floors"]
floors = _int_in(args.get("floors"), lo, hi)
if floors is None:
floors = lo
hue = _int_in(args.get("hue"), 0, 360)
if hue is None:
hue = 35
variant = _int_in(args.get("variant"), 0, 7)
if variant is None:
variant = 0
name = sanitize_name(args.get("name"), default=f"The {kind.replace('_', ' ').title()}")
return {
"kind": kind, "name": name, "cx": cx, "cz": cz,
"w": spec["w"], "d": spec["d"], "floors": floors,
"hue": hue, "variant": variant,
}, None
def _validate_lay_road(args: dict):
cells = _coerce_cells(args.get("cells"))
if cells is None:
return _err("lay_road needs cells [[cx,cz],...]")
klass = str(args.get("klass") or "street").strip().lower()
if klass not in C.ROAD_CLASSES:
klass = "street"
name = sanitize_name(args.get("name"), default="")
out = {"cells": cells, "klass": klass}
if name:
out["name"] = name
return out, None
def _coerce_metrics(raw: Any) -> dict:
out: dict[str, Any] = {}
if isinstance(raw, dict):
for k, v in list(raw.items())[:6]:
key = _text(k, 24)
if not key:
continue
if isinstance(v, bool):
continue
if isinstance(v, (int, float)):
num = round(float(v), 2)
out[key] = int(num) if num.is_integer() else num
elif isinstance(v, str):
out[key] = _text(v, 48)
return out
def _validate_apply_fix(args: dict):
action = str(args.get("action") or "").strip().lower()
if action not in FIX_ACTIONS:
return _err(f"unknown fix action '{_text(args.get('action'), 24)}'")
cells = _coerce_cells(args.get("cells"))
if cells is None:
return _err("apply_fix needs cells [[cx,cz],...]")
klass = str(args.get("klass") or "avenue").strip().lower()
if klass not in C.ROAD_CLASSES:
klass = "avenue"
name = sanitize_name(args.get("name"), default=street_name_for(repr(cells[0])))
diagnosis = _text(args.get("diagnosis"), 200)
return {
"action": action, "cells": cells, "klass": klass, "name": name,
"diagnosis": diagnosis,
"metrics_before": _coerce_metrics(args.get("metrics_before")),
"metrics_predicted": _coerce_metrics(args.get("metrics_predicted")),
}, None
def _validate_note(args: dict):
text = _text(args.get("text"), 140)
if not text:
return _err("note needs text")
if not _moderator.check_content(text).allowed:
return _err("those words may not be posted on the city ledger")
kind = str(args.get("kind") or "milestone").strip().lower()
if kind not in NOTE_KINDS:
kind = "milestone"
return {"text": text, "kind": kind}, None
_VALIDATORS = {
"place_building": _validate_place_building,
"lay_road": _validate_lay_road,
"apply_fix": _validate_apply_fix,
"note": _validate_note,
}
def validate_call(tool: Any, args: Any) -> tuple[Optional[dict], Optional[str]]:
"""Validate one event. Returns (canonical_args, None) or (None, rejection)."""
if not isinstance(tool, str) or tool not in _VALIDATORS:
return _err(f"unknown tool '{_text(tool, 32)}'")
if args is None:
args = {}
if not isinstance(args, dict):
return _err("args must be an object")
return _VALIDATORS[tool](args)
def as_dict() -> dict:
"""Plain-JSON view of the event surface (prompt builders may render this)."""
return {
"place_building": {
"kind": {"type": "enum", "values": list(C.BUILDINGS)},
"name": {"type": "string", "max_len": C.NAME_MAX_LEN},
"cx": {"type": "int", "min": C.COORD_MIN, "max": C.COORD_MAX},
"cz": {"type": "int", "min": C.COORD_MIN, "max": C.COORD_MAX},
"floors": {"type": "int", "note": "clamped to the kind's range"},
"hue": {"type": "int", "min": 0, "max": 360},
},
"lay_road": {
"cells": {"type": "cells", "note": "[[cx,cz],...] engine-routed only"},
"klass": {"type": "enum", "values": list(C.ROAD_CLASSES)},
"name": {"type": "string", "max_len": C.NAME_MAX_LEN},
},
"apply_fix": {
"action": {"type": "enum", "values": list(FIX_ACTIONS)},
"cells": {"type": "cells"},
"klass": {"type": "enum", "values": list(C.ROAD_CLASSES)},
"diagnosis": {"type": "string", "max_len": 200},
},
"note": {
"text": {"type": "string", "max_len": 140},
"kind": {"type": "enum", "values": list(NOTE_KINDS)},
},
}
|