godseed / engine /tools.py
AndresCarreon's picture
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
baf9d60 verified
Raw
History Blame Contribute Delete
16 kB
"""GODSEED tool DSL — the single source of truth for the 11-tool surface.
Spec is data (TOOLS); validation is `validate_call`. Semantics per the
architecture contract:
* out-of-range NUMBERS are CLAMPED, never rejected;
* unknown tools / unknown enum values are REJECTED with a terse observation
string the model can react to (e.g. "rejected: unknown kind 'cactus'");
* missing or non-numeric/malformed args are rejected (they cannot be clamped);
* extra args not in the spec are silently dropped.
`place_water` constraints: kind "stream" needs 2..5 path waypoints and takes
no radius; kind "pool" uses path[0] as its center and requires radius_deg.
`place_road` takes a path of 2..6 [lat,lon] waypoints (a paved surface spline).
Pure stdlib. Other engine modules (and the mind's prompt builder) import this.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any, Optional
LAT_RANGE = (-85.0, 85.0)
LON_RANGE = (-180.0, 180.0)
@dataclass(frozen=True)
class NumberSpec:
lo: float
hi: float
integer: bool = False
@dataclass(frozen=True)
class EnumSpec:
values: tuple[str, ...]
@dataclass(frozen=True)
class TextSpec:
max_len: int
@dataclass(frozen=True)
class PathSpec:
max_points: int = 5
FLORA_KINDS = ("trees", "glowgrass", "mushrooms", "vines", "flowers", "reeds")
STRUCTURE_KINDS = (
"lighthouse", "monolith", "shrine", "village", "beacon", "arch",
"tower", "warehouse", "cafe", "bank", "market", "house",
)
# The god (a 9B model, no grammar constraint on the live ZeroGPU path) invents
# building names freely — "market_square", "skyscraper", "townhall". REJECTING
# them makes a town wish build nothing and the model gives up (June 12 live:
# "it says it created the town... there is no town"). So place_structure is
# FORGIVING: synonyms map to a real kind, and any unknown kind falls back to a
# house rather than failing. The god always builds something.
STRUCTURE_SYNONYMS: dict[str, str] = {
"market_square": "market", "marketsquare": "market", "square": "market",
"plaza": "market", "bazaar": "market", "marketplace": "market", "stall": "market",
"skyscraper": "tower", "highrise": "tower", "high_rise": "tower", "office": "tower",
"spire": "tower", "obelisk": "monolith", "pillar": "monolith", "stone": "monolith",
"depot": "warehouse", "storehouse": "warehouse", "storage": "warehouse",
"factory": "warehouse", "barn": "warehouse", "silo": "warehouse",
"coffee": "cafe", "coffeehouse": "cafe", "restaurant": "cafe", "diner": "cafe",
"cafeteria": "cafe", "inn": "cafe", "tavern": "cafe", "pub": "cafe", "bakery": "cafe",
"home": "house", "cottage": "house", "cabin": "house", "hut": "house", "lodge": "house",
"dwelling": "house", "residence": "house", "bungalow": "house", "manor": "house",
"church": "shrine", "temple": "shrine", "chapel": "shrine", "cathedral": "shrine",
"townhall": "bank", "town_hall": "bank", "hall": "bank", "courthouse": "bank",
"library": "bank", "museum": "bank", "school": "bank", "guildhall": "bank",
"harbor": "lighthouse", "harbour": "lighthouse", "port": "lighthouse", "dock": "lighthouse",
"gate": "arch", "gateway": "arch", "bridge": "arch", "tower_gate": "arch",
"fire": "beacon", "torch": "beacon", "lantern": "beacon", "lamp": "beacon",
"hamlet": "village", "settlement": "village", "houses": "village",
}
# Kinds that really mean "a whole neighborhood" -> the engine routes them to a
# dense build_district instead of a single building, so the town reads dense.
DISTRICT_SYNONYMS = frozenset((
"district", "neighborhood", "neighbourhood", "block", "quarter", "town",
"city", "downtown", "suburb", "metropolis", "borough", "ward",
))
def resolve_structure_kind(value: Any) -> str | None:
"""Map a model-supplied kind to a real STRUCTURE_KINDS value. None means
'this is really a district' (caller should route to build_district).
Unknown-but-buildingish names fall back to 'house' — never a rejection."""
if not isinstance(value, str):
return "house"
v = value.strip().lower().replace(" ", "_").replace("-", "_")
if v in STRUCTURE_KINDS:
return v
if v in DISTRICT_SYNONYMS:
return None
if v in STRUCTURE_SYNONYMS:
return STRUCTURE_SYNONYMS[v]
# substring rescue (e.g. "grand_market_hall" -> market) before defaulting
for token, kind in STRUCTURE_SYNONYMS.items():
if token in v:
return kind
for kind in STRUCTURE_KINDS:
if kind in v:
return kind
return "house" # any other word becomes a humble house, never a rejection
WATER_KINDS = ("stream", "pool")
WEATHER_KINDS = ("clear", "rain", "snow", "embers", "mist", "storm")
SKY_PALETTES = ("dawn", "dusk", "night", "aurora", "ember", "void", "gold")
INSCRIBE_STYLES = ("orbit", "stone")
LIFE_KINDS = ("carts", "birds", "fireflies")
TOOLS: dict[str, dict[str, Any]] = {
"raise_terrain": {
"lat": NumberSpec(*LAT_RANGE),
"lon": NumberSpec(*LON_RANGE),
"radius_deg": NumberSpec(2, 45),
"height": NumberSpec(0.01, 0.12),
"roughness": NumberSpec(0, 1),
},
"lower_terrain": {
"lat": NumberSpec(*LAT_RANGE),
"lon": NumberSpec(*LON_RANGE),
"radius_deg": NumberSpec(2, 45),
"depth": NumberSpec(0.01, 0.10),
"roughness": NumberSpec(0, 1),
},
"spawn_flora": {
"lat": NumberSpec(*LAT_RANGE),
"lon": NumberSpec(*LON_RANGE),
"radius_deg": NumberSpec(1, 30),
"kind": EnumSpec(FLORA_KINDS),
"density": NumberSpec(0, 1),
"hue": NumberSpec(0, 360),
},
"place_structure": {
"lat": NumberSpec(*LAT_RANGE),
"lon": NumberSpec(*LON_RANGE),
"kind": EnumSpec(STRUCTURE_KINDS),
"scale": NumberSpec(0.5, 2.0),
"hue": NumberSpec(0, 360),
},
"place_water": {
"kind": EnumSpec(WATER_KINDS),
"path": PathSpec(5),
"radius_deg": NumberSpec(1, 10), # pool only
"hue": NumberSpec(160, 260),
},
"set_weather": {
"kind": EnumSpec(WEATHER_KINDS),
"intensity": NumberSpec(0, 1),
},
"set_sky": {
"palette": EnumSpec(SKY_PALETTES),
"star_density": NumberSpec(0, 1),
"moons": NumberSpec(0, 3, integer=True),
},
"inscribe_wish": {
"text": TextSpec(90),
"style": EnumSpec(INSCRIBE_STYLES),
},
"spawn_life": {
"lat": NumberSpec(*LAT_RANGE),
"lon": NumberSpec(*LON_RANGE),
"radius_deg": NumberSpec(1, 20),
"kind": EnumSpec(LIFE_KINDS),
"count": NumberSpec(1, 12, integer=True),
"hue": NumberSpec(0, 360),
},
"build_district": {
"lat": NumberSpec(*LAT_RANGE),
"lon": NumberSpec(*LON_RANGE),
"radius_deg": NumberSpec(2, 15),
"density": NumberSpec(0, 1),
"hue": NumberSpec(0, 360),
},
"place_road": {
"path": PathSpec(6), # 2..6 [lat,lon] waypoints
},
}
TOOL_NAMES: tuple[str, ...] = tuple(TOOLS.keys())
def _snip(value: Any) -> str:
"""Terse, single-line render of a bad value for observation strings."""
s = str(value)
s = "".join(ch if ch.isprintable() else "?" for ch in s)
return s[:32]
def _coerce_number(name: str, value: Any, spec: NumberSpec):
"""Returns (clamped_value, None) or (None, rejection)."""
if isinstance(value, bool):
return None, f"rejected: bad arg '{name}'"
if isinstance(value, str):
try:
value = float(value.strip())
except (ValueError, AttributeError):
return None, f"rejected: bad arg '{name}'"
if not isinstance(value, (int, float)):
return None, f"rejected: bad arg '{name}'"
v = float(value)
if math.isnan(v):
return None, f"rejected: bad arg '{name}'"
v = min(max(v, float(spec.lo)), float(spec.hi))
if spec.integer:
return int(round(v)), None
return round(v, 4), None
def _coerce_enum(name: str, value: Any, spec: EnumSpec):
if not isinstance(value, str):
return None, f"rejected: unknown {name} '{_snip(value)}'"
v = value.strip().lower()
if v not in spec.values:
return None, f"rejected: unknown {name} '{_snip(value)}'"
return v, None
def _coerce_text(name: str, value: Any, spec: TextSpec):
if not isinstance(value, str):
return None, f"rejected: bad arg '{name}'"
v = "".join(ch for ch in value if ch.isprintable()).strip()
if not v:
return None, f"rejected: empty {name}"
return v[: spec.max_len], None
def _coerce_point(point: Any):
"""A waypoint is [lat, lon] (or {'lat':..,'lon':..}); returns [lat, lon] or None."""
if isinstance(point, dict):
raw = (point.get("lat"), point.get("lon"))
elif isinstance(point, (list, tuple)) and len(point) >= 2:
raw = (point[0], point[1])
else:
return None
lat, err = _coerce_number("lat", raw[0], NumberSpec(*LAT_RANGE))
if err:
return None
lon, err = _coerce_number("lon", raw[1], NumberSpec(*LON_RANGE))
if err:
return None
return [lat, lon]
DEFAULT_WATER_HUE = 190.0
DEFAULT_POOL_RADIUS = 5.0
def _validate_place_water(args: dict):
"""Models fumble water more than any other tool (June 12 live traces), so
this one is deliberately forgiving: lat/lon synthesize a path, a flat
[lat, lon] pair is wrapped, and hue / pool radius take engine defaults."""
spec = TOOLS["place_water"]
kind, err = _coerce_enum("kind", args.get("kind"), spec["kind"])
if err:
return None, err
raw_path = args.get("path")
# lat/lon supplied like every other tool -> a one-point path
if not isinstance(raw_path, (list, tuple)) or not raw_path:
if args.get("lat") is not None and args.get("lon") is not None:
raw_path = [[args.get("lat"), args.get("lon")]]
else:
return None, "rejected: water needs path [[lat,lon],...] or lat+lon"
# a flat [lat, lon] pair instead of [[lat, lon]]
if raw_path and not isinstance(raw_path[0], (list, tuple)):
raw_path = [raw_path]
points = []
for p in list(raw_path)[: spec["path"].max_points]: # extra waypoints clamped off
pt = _coerce_point(p)
if pt is None:
return None, "rejected: bad waypoint in path (each is [lat, lon])"
points.append(pt)
hue_raw = args.get("hue")
if hue_raw is None:
hue = DEFAULT_WATER_HUE
else:
hue, err = _coerce_number("hue", hue_raw, spec["hue"])
if err:
return None, err
if kind == "stream":
if len(points) < 2:
return None, "rejected: stream needs 2..5 waypoints"
return {"kind": "stream", "path": points, "hue": hue}, None
# pool: path[0] is the center; radius defaults rather than dies
radius_raw = args.get("radius_deg")
if radius_raw is None:
radius = DEFAULT_POOL_RADIUS
else:
radius, err = _coerce_number("radius_deg", radius_raw, spec["radius_deg"])
if err:
return None, err
return {"kind": "pool", "path": points[:1], "radius_deg": radius, "hue": hue}, None
def _validate_place_road(args: dict):
"""A road is a paved surface spline through 2..6 [lat,lon] waypoints.
Path handling mirrors place_water (a flat [lat, lon] pair is wrapped; extra
waypoints past the max are clamped off) but a road always needs at least two
points — there is no single-point form.
"""
spec = TOOLS["place_road"]
raw_path = args.get("path")
if not isinstance(raw_path, (list, tuple)) or not raw_path:
return None, "rejected: road needs path [[lat,lon],...] (2..6 points)"
# a flat [lat, lon] pair instead of [[lat, lon]]
if not isinstance(raw_path[0], (list, tuple, dict)):
raw_path = [raw_path]
points = []
for p in list(raw_path)[: spec["path"].max_points]: # extra waypoints clamped off
pt = _coerce_point(p)
if pt is None:
return None, "rejected: bad waypoint in path (each is [lat, lon])"
points.append(pt)
if len(points) < 2:
return None, "rejected: road needs 2..6 waypoints"
return {"path": points}, None
def validate_call(tool: Any, args: Any) -> tuple[Optional[dict], Optional[str]]:
"""Validate one tool call. Returns (canonical_args, None) or (None, rejection).
Canonical args contain exactly the spec'd keys (in spec order), with
numbers clamped + rounded, enums lowercased, text sanitized/truncated.
"""
if not isinstance(tool, str) or tool not in TOOLS:
return None, f"rejected: unknown tool '{_snip(tool)}'"
if args is None:
args = {}
if not isinstance(args, dict):
return None, "rejected: args must be an object"
if tool == "place_water":
return _validate_place_water(args)
if tool == "place_road":
return _validate_place_road(args)
# place_structure: forgive the kind. A "district"-ish name becomes a dense
# build_district; any other unknown becomes the nearest real building (or a
# house) — the god never builds nothing because it picked a word we lack.
if tool == "place_structure":
resolved = resolve_structure_kind(args.get("kind"))
if resolved is None:
district_args = {
"lat": args.get("lat"), "lon": args.get("lon"),
"radius_deg": args.get("radius_deg", 6),
"density": args.get("density", 0.7),
"hue": args.get("hue", 40),
}
return validate_call("build_district", district_args)
args = {**args, "kind": resolved}
out: dict[str, Any] = {}
for name, spec in TOOLS[tool].items():
if name not in args or args.get(name) is None:
return None, f"rejected: missing arg '{name}'"
value = args[name]
if isinstance(spec, NumberSpec):
v, err = _coerce_number(name, value, spec)
elif isinstance(spec, EnumSpec):
v, err = _coerce_enum(name, value, spec)
elif isinstance(spec, TextSpec):
v, err = _coerce_text(name, value, spec)
else: # pragma: no cover — only place_water has PathSpec, handled above
v, err = None, f"rejected: bad arg '{name}'"
if err:
return None, err
out[name] = v
return out, None
# Per-tool minimum path length (place_water's pool form accepts a single
# center point; place_road always needs at least two waypoints).
_PATH_MIN_POINTS: dict[str, int] = {"place_water": 1, "place_road": 2}
def as_dict() -> dict:
"""Plain-JSON view of the tool spec (handy for prompts / grammar builders)."""
out: dict[str, dict] = {}
for tool, argspecs in TOOLS.items():
spec_out: dict[str, dict] = {}
for name, spec in argspecs.items():
if isinstance(spec, NumberSpec):
spec_out[name] = {
"type": "number", "min": spec.lo, "max": spec.hi,
"integer": spec.integer,
}
elif isinstance(spec, EnumSpec):
spec_out[name] = {"type": "enum", "values": list(spec.values)}
elif isinstance(spec, TextSpec):
spec_out[name] = {"type": "string", "max_len": spec.max_len}
elif isinstance(spec, PathSpec):
spec_out[name] = {
"type": "path",
"min_points": _PATH_MIN_POINTS.get(tool, 1),
"max_points": spec.max_points,
"point": "[lat, lon]",
}
out[tool] = spec_out
out["place_water"]["radius_deg"]["note"] = "pool only"
out["place_water"]["path"]["note"] = "stream: 2..5 waypoints; pool: path[0] is center"
out["place_road"]["path"]["note"] = "2..6 waypoints; a paved road laid on the surface"
return out