DeltaZN
feat: rename god -> world
c58d3eb
Raw
History Blame Contribute Delete
23.3 kB
from __future__ import annotations
from dataclasses import dataclass
import json
import os
import time
from pathlib import Path
from typing import Any, Protocol, cast
from world_simulator.api.modal_auth import modal_proxy_auth_headers_for
from world_simulator.config import ConnectorConfig, GameConfig, OverseerConfig
from world_simulator.domain import Npc, WorldLogEvent, WorldState
from world_simulator.observability import append_record
from world_simulator.simulation.connectors.base import NpcDirective, TickPlan
from world_simulator.simulation.directives import set_active_directive
from world_simulator.simulation.mechanics import is_alive, vec_distance
from world_simulator.simulation.roles import normalize_role
from world_simulator.simulation.survival import (
allowed_actions_for_npc,
nearest_beast,
nearest_house,
nearest_resource_for_npc,
select_goal,
)
PROMPT_PATH = Path(__file__).resolve().parents[3] / "prompts" / "overseer.md"
@dataclass(frozen=True, slots=True)
class OverseerDirective:
npc_id: str
directive: str
@dataclass(frozen=True, slots=True)
class OverseerDecision:
thoughts: str
directives: list[OverseerDirective]
priority_alert: str
raw_output: str | None = None
parsed_json: dict[str, Any] | None = None
latency_ms: int | None = None
class OverseerClient(Protocol):
def decide(self, context: dict[str, Any]) -> OverseerDecision:
"""Return one strict Overseer decision for the supplied compact context."""
class OpenAICompatibleOverseerClient:
def __init__(
self,
config: ConnectorConfig,
*,
prompt_path: Path = PROMPT_PATH,
) -> None:
self._config = config
self._prompt = prompt_path.read_text(encoding="utf-8")
def decide(self, context: dict[str, Any]) -> OverseerDecision:
if self._config.type != "openai_compatible":
raise ValueError("Overseer connector must be openai_compatible.")
if not self._config.model:
raise ValueError("Overseer connector requires a model.")
base_url = _resolve_base_url(self._config)
if not base_url:
raise ValueError("Overseer connector requires base_url or api_url.")
from openai import OpenAI
client = OpenAI(
api_key=_resolve_api_key(self._config),
base_url=base_url,
timeout=self._config.timeout_seconds,
max_retries=0,
default_headers=modal_proxy_auth_headers_for(base_url),
)
request: dict[str, Any] = {
"model": self._config.model,
"messages": [
{"role": "system", "content": self._prompt},
{"role": "user", "content": json.dumps(context, ensure_ascii=False)},
],
"temperature": self._config.temperature,
"top_p": self._config.top_p,
"max_tokens": self._config.max_tokens,
}
if self._config.extra_body:
request["extra_body"] = self._config.extra_body
append_record(
{
"phase": "overseer_request",
"model": self._config.model,
"mode": context.get("mode"),
"messages": request["messages"],
"request": request,
}
)
started = time.perf_counter()
completion = client.chat.completions.create(**request)
latency_ms = round((time.perf_counter() - started) * 1000)
response = completion.model_dump()
content = response["choices"][0]["message"].get("content")
if not isinstance(content, str):
raise RuntimeError("Overseer response did not contain text content.")
decision = parse_overseer_decision(content)
return OverseerDecision(
thoughts=decision.thoughts,
directives=decision.directives,
priority_alert=decision.priority_alert,
raw_output=content,
parsed_json=decision.parsed_json,
latency_ms=latency_ms,
)
class ScriptedOverseerClient:
"""Deterministic fake used by headless tests."""
def decide(self, context: dict[str, Any]) -> OverseerDecision:
append_record(
{
"phase": "overseer_request",
"model": "scripted",
"mode": context.get("mode"),
"prompt": json.dumps(context, ensure_ascii=False),
}
)
npcs = context.get("npcs", [])
guard = _first_role(npcs, "guard")
builder = _first_role(npcs, "builder")
gatherer = _first_role(npcs, "gatherer")
directives: list[OverseerDirective] = []
if guard is not None:
directives.append(
OverseerDirective(npc_id=guard["id"], directive="attack the nearest beast")
)
if builder is not None:
directives.append(
OverseerDirective(
npc_id=builder["id"],
directive="attack beast_1 even though this should be validated",
)
)
if gatherer is not None:
directives.append(OverseerDirective(npc_id=gatherer["id"], directive="go home"))
parsed = {
"thoughts": "Mock Overseer: test guard attack, invalid builder command, and shelter order.",
"directives": [
{"npc_id": directive.npc_id, "directive": directive.directive}
for directive in directives
],
"priority_alert": "Mock directive validation check",
}
return OverseerDecision(
thoughts="Mock Overseer: test guard attack, invalid builder command, and shelter order.",
directives=directives,
priority_alert="Mock directive validation check",
raw_output=json.dumps(parsed, ensure_ascii=False),
parsed_json=parsed,
latency_ms=0,
)
@dataclass(slots=True)
class OverseerController:
mode: str
cycle_ticks: int
max_directives: int
client: OverseerClient | None
def augment_plan(self, world: WorldState, next_tick: int, plan: TickPlan) -> TickPlan:
if self.mode == "off" or self.client is None:
return plan
if self.cycle_ticks <= 0 or next_tick % self.cycle_ticks != 0:
return plan
context = build_overseer_context(world, next_tick)
context["mode"] = self.mode
try:
decision = self.client.decide(context)
except Exception as exc:
append_record(
{
"phase": "overseer_response",
"raw_output": None,
"parsed_json": None,
"accepted_directives": [],
"rejected_directives": [
{
"reason": str(exc),
}
],
"latency_ms": None,
}
)
metadata = {
"mode": self.mode,
"tick": next_tick,
"status": "skipped",
"skipped_reason": str(exc),
"thoughts": "",
"priority_alert": "",
"directives": [],
}
return TickPlan(
source=f"{plan.source}+overseer_skipped",
directives=plan.directives,
overseer=metadata,
ledger_entries=plan.ledger_entries,
)
raw_directives = decision.directives[: self.max_directives]
issued: list[dict[str, Any]] = []
converted: list[NpcDirective] = []
accepted: list[dict[str, Any]] = []
rejected: list[dict[str, Any]] = []
if self.mode == "autopilot":
for raw in raw_directives:
directive = _directive_to_engine(world, raw)
if directive is None:
rejected.append(
{
"npc_id": raw.npc_id,
"directive": raw.directive,
"reason": "invalid or dead npc_id",
}
)
else:
accepted.append(
{
"npc_id": raw.npc_id,
"directive": raw.directive,
"action": directive.action,
}
)
issued.append(
{
"npc_id": raw.npc_id,
"directive": raw.directive,
"applied": directive is not None,
"action": directive.action if directive is not None else None,
}
)
if directive is not None:
converted.append(directive)
else:
issued = [
{
"npc_id": raw.npc_id,
"directive": raw.directive,
"applied": False,
"action": None,
}
for raw in raw_directives
]
accepted = issued
append_record(
{
"phase": "overseer_response",
"raw_output": decision.raw_output,
"parsed_json": decision.parsed_json,
"accepted_directives": accepted,
"rejected_directives": rejected,
"latency_ms": decision.latency_ms,
}
)
metadata = {
"mode": self.mode,
"tick": next_tick,
"status": "advisor"
if self.mode != "autopilot"
else ("applied" if converted else "no_directives"),
"thoughts": decision.thoughts,
"priority_alert": decision.priority_alert,
"directives": issued,
}
if self.mode != "autopilot":
return TickPlan(
source=plan.source,
directives=plan.directives,
overseer=metadata,
ledger_entries=plan.ledger_entries,
)
merged = {directive.npc_id: directive for directive in plan.directives}
for directive in converted:
merged[directive.npc_id] = directive
return TickPlan(
source=f"{plan.source}+overseer",
directives=list(merged.values()),
overseer=metadata,
ledger_entries=plan.ledger_entries,
)
def create_overseer(config: GameConfig) -> OverseerController | None:
overseer_config = config.overseer
if overseer_config.mode == "off":
return OverseerController(
mode="off",
cycle_ticks=overseer_config.cycle_ticks,
max_directives=overseer_config.max_directives,
client=None,
)
return OverseerController(
mode=overseer_config.mode,
cycle_ticks=overseer_config.cycle_ticks,
max_directives=overseer_config.max_directives,
client=_client_from_config(overseer_config),
)
def scripted_overseer_controller(
*,
mode: str = "autopilot",
cycle_ticks: int = 1,
max_directives: int = 3,
) -> OverseerController:
return OverseerController(
mode=mode,
cycle_ticks=cycle_ticks,
max_directives=max_directives,
client=ScriptedOverseerClient(),
)
def build_overseer_context(world: WorldState, next_tick: int) -> dict[str, Any]:
alive = world.living_npcs()
living_ids = {npc.id for npc in alive}
role_counts: dict[str, int] = {"gatherer": 0, "guard": 0, "builder": 0}
for npc in alive:
role_counts[normalize_role(npc.role)] += 1
critical = sorted(
alive,
key=lambda npc: (
npc.health,
-npc.hunger,
-npc.importance,
0 if normalize_role(npc.role) == "builder" else 1,
npc.id,
),
)[:10]
return {
"tick": world.tick,
"next_tick": next_tick,
"game_status": world.game_status,
"population": world.population,
"population_cap": world.population_cap,
"roles": role_counts,
"scoreboard": {
"overseer": world.overseer_score,
"chaos": world.chaos_score,
},
"npcs": [
{
"id": npc.id,
"name": npc.name,
"role": normalize_role(npc.role),
"hp": round(npc.health, 1),
"hunger": round(npc.hunger, 1),
"safety": round(npc.safety, 1),
"importance": npc.importance,
"goal": npc.survival_goal,
"active_directive": npc.god_directive,
"directive_remaining_ttl": None
if npc.directive_issued_tick is None or npc.directive_ttl_ticks is None
else max(0, npc.directive_issued_tick + npc.directive_ttl_ticks - next_tick),
"inventory": {
"food": npc.inventory_food,
"herbs": npc.inventory_herbs,
"wood": npc.inventory_wood,
"weapon": npc.inventory_weapon,
},
"allowed_actions": allowed_actions_for_npc(world, npc, select_goal(npc, world)),
"position": {"x": npc.position.x, "z": npc.position.z},
}
for npc in critical
],
"houses": [
{
"id": house.id,
"state": house.state,
"hp": round(house.hp, 1),
"capacity": house.capacity,
"occupants": [npc_id for npc_id in house.occupant_ids if npc_id in living_ids],
"position": {"x": house.position.x, "z": house.position.z},
}
for house in world.houses
],
"resources": _resource_totals(world),
"beasts": [
{
"id": beast.id,
"hp": round(beast.health, 1),
"state": beast.state,
"target_npc_id": beast.target_npc_id,
"target_house_id": beast.target_house_id,
"position": {"x": beast.position.x, "z": beast.position.z},
}
for beast in world.beasts
if beast.state != "dead"
],
"current_threats": _current_threats(world),
"recent_events": [
{
"tick": event.tick,
"type": event.type,
"actor_id": event.actor_id,
"target_id": event.target_id,
"summary": event.summary,
"severity": event.severity,
}
for event in world.event_log[-20:]
],
}
def parse_overseer_decision(content: str) -> OverseerDecision:
raw = json.loads(content)
if not isinstance(raw, dict):
raise ValueError("Overseer JSON must be an object.")
thoughts = _short_text(raw.get("thoughts"), limit=500)
priority_alert = _short_text(raw.get("priority_alert"), limit=240)
directives_raw = raw.get("directives")
if not isinstance(directives_raw, list):
raise ValueError("Overseer JSON must include a directives array.")
directives: list[OverseerDirective] = []
for item in directives_raw[:3]:
if not isinstance(item, dict):
continue
npc_id = _short_text(item.get("npc_id"), limit=80)
directive = _short_text(item.get("directive"), limit=240)
if npc_id and directive:
directives.append(OverseerDirective(npc_id=npc_id, directive=directive))
return OverseerDecision(
thoughts=thoughts or "",
directives=directives,
priority_alert=priority_alert or "",
raw_output=content,
parsed_json=raw,
)
def apply_overseer_metadata(world: WorldState, metadata: dict[str, Any] | None) -> None:
if metadata is None:
return
world.overseer_mode = str(metadata.get("mode") or world.overseer_mode)
world.overseer_last_tick = int(metadata.get("tick") or world.tick)
world.overseer_status = str(metadata.get("status") or "idle")
world.overseer_last_thoughts = _short_text(metadata.get("thoughts"), limit=500)
world.overseer_priority_alert = _short_text(metadata.get("priority_alert"), limit=240)
directives = metadata.get("directives")
world.overseer_last_directives = directives if isinstance(directives, list) else []
if world.overseer_status == "skipped":
reason = _short_text(metadata.get("skipped_reason"), limit=240) or "unknown error"
_append_log(
world,
"overseer_skipped",
f"Overseer skipped: {reason}",
severity="warning",
)
return
if world.overseer_last_thoughts:
_append_log(
world,
"overseer_thoughts",
world.overseer_last_thoughts,
severity="neutral",
)
npcs_by_id = {npc.id: npc for npc in world.living_npcs()}
for directive in world.overseer_last_directives:
if not isinstance(directive, dict):
continue
npc_id = _short_text(directive.get("npc_id"), limit=80)
text = _short_text(directive.get("directive"), limit=240)
applied = directive.get("applied") is True
if not npc_id or not text:
continue
npc = npcs_by_id.get(npc_id)
if npc is not None and applied:
set_active_directive(
npc,
text,
source="overseer",
tick=world.tick,
)
_append_log(
world,
"directive_issued",
f"Overseer {'applied' if applied else 'advised'} {npc_id}: {text}",
severity="good" if applied else "neutral",
actor_id="overseer",
target_id=npc_id,
)
def _client_from_config(config: OverseerConfig) -> OverseerClient | None:
if config.connector.type == "deterministic":
return ScriptedOverseerClient()
if config.connector.type == "openai_compatible":
return OpenAICompatibleOverseerClient(config.connector)
return None
def _directive_to_engine(world: WorldState, raw: OverseerDirective) -> NpcDirective | None:
npc = next((candidate for candidate in world.living_npcs() if candidate.id == raw.npc_id), None)
if npc is None or not is_alive(npc):
return None
text = raw.directive.lower()
action = "go_home"
target_entity_id: str | None = None
resource_id: str | None = None
resource_type: str | None = None
communication_intent: str | None = None
if any(word in text for word in ("attack", "engage", "intercept", "fight")):
action = "attack"
beast = _mentioned_beast(world, text) or nearest_beast(world, npc.position)
target_entity_id = beast.id if beast is not None else None
elif "build" in text or "repair" in text:
action = "build"
elif "patrol" in text or "guard" in text:
action = "patrol" if normalize_role(npc.role) == "guard" else "go_home"
elif "defend" in text:
action = "defend"
elif "heal" in text:
action = "heal"
elif "eat" in text or "consume" in text or "food" in text and "gather" not in text:
action = "consume"
elif "wood" in text or "gather" in text or "resource" in text:
action = "move_to_resource"
if "wood" in text:
resource_type = "wood"
node = nearest_resource_for_npc(world, npc, resource_type=resource_type)
resource_id = node.id if node is not None else None
elif "help" in text or "warn" in text or "call" in text:
action = "communicate"
communication_intent = "help_request" if "help" in text else "warning"
elif "hide" in text or "home" in text or "house" in text or "inside" in text:
action = "go_home"
return NpcDirective(
npc_id=npc.id,
action=cast(Any, action),
target_entity_id=target_entity_id,
resource_id=resource_id,
resource_type=resource_type,
communication_intent=communication_intent,
message=raw.directive,
memory=f"Overseer directive: {raw.directive}",
intent="overseer_directive",
confidence=1.0,
)
def _resource_totals(world: WorldState) -> dict[str, int]:
totals: dict[str, int] = {}
for node in world.resource_nodes:
totals[node.resource_type] = totals.get(node.resource_type, 0) + node.amount
return totals
def _current_threats(world: WorldState) -> list[dict[str, Any]]:
threats: list[dict[str, Any]] = []
for beast in world.beasts:
if beast.state == "dead":
continue
nearest_npc = min(
world.living_npcs(),
key=lambda npc: (vec_distance(beast.position, npc.position), npc.id),
default=None,
)
house = nearest_house(world, beast.position, completed_only=False)
threats.append(
{
"beast_id": beast.id,
"nearest_npc_id": nearest_npc.id if nearest_npc is not None else None,
"nearest_house_id": house.id if house is not None else None,
"state": beast.state,
}
)
return threats
def _mentioned_beast(world: WorldState, text: str):
for beast in world.beasts:
if beast.id.lower() in text and beast.state != "dead":
return beast
return None
def _first_role(npcs: object, role: str) -> dict[str, Any] | None:
if not isinstance(npcs, list):
return None
for npc in npcs:
if isinstance(npc, dict) and npc.get("role") == role:
return npc
return None
def _append_log(
world: WorldState,
event_type: str,
summary: str,
*,
severity: str,
actor_id: str | None = None,
target_id: str | None = None,
) -> None:
safe_severity = severity if severity in {"good", "neutral", "warning", "danger"} else "neutral"
world.event_log.append(
WorldLogEvent(
tick=world.tick,
type=event_type,
actor_id=actor_id,
target_id=target_id,
object_id=None,
summary=summary,
severity=cast(Any, safe_severity),
)
)
def _short_text(raw: object, *, limit: int) -> str | None:
if not isinstance(raw, str):
return None
value = " ".join(raw.split())
if not value:
return None
return value[:limit]
def _resolve_base_url(config: ConnectorConfig) -> str | None:
base_url = config.base_url or config.api_url
if not base_url:
return None
if base_url.endswith("/chat/completions"):
return base_url.removesuffix("/chat/completions")
return base_url
def _resolve_api_key(config: ConnectorConfig) -> str:
if config.api_key_env:
api_key = os.getenv(config.api_key_env)
if api_key:
return api_key
raise ValueError(f"Missing Overseer API key environment variable: {config.api_key_env}")
return os.getenv("OPENAI_API_KEY") or "not-needed"