Spaces:
Runtime error
Runtime error
| """ | |
| Natural language to WorldSmithAI DSL generation. | |
| This module converts user prompts into ``WorldSpec`` JSON. It is designed for | |
| root-level Hugging Face Spaces ``app.py`` usage and does not assume any | |
| ``app/`` package or folder structure. | |
| The SLM's responsibility is strictly: | |
| Natural language -> JSON DSL | |
| The SLM must not generate Python code. The deterministic Python engine consumes | |
| only validated ``WorldSpec`` objects. | |
| Example root-level app.py usage: | |
| from llm.world_generator import generate_world_spec | |
| from factory.world_factory import WorldFactory | |
| def run(prompt: str): | |
| spec = generate_world_spec(prompt) | |
| world = WorldFactory().create_world(spec) | |
| return spec.to_json_string(), world | |
| Using a custom SLM client: | |
| generator = WorldGenerator(client=my_client) | |
| result = generator.generate("A research ecosystem with competing labs") | |
| print(result.json_text) | |
| Future extensibility: | |
| - Add first-class Hugging Face transformers adapters. | |
| - Add constrained decoding when available. | |
| - Add schema-version migrations. | |
| - Add model-specific prompt profiles. | |
| - Add automatic JSON repair loops. | |
| - Add world-quality self-critique before factory construction. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import inspect | |
| import json | |
| import logging | |
| import re | |
| from collections.abc import Callable, Mapping, Sequence | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| from typing import Any, Protocol, runtime_checkable | |
| from dsl.parser import DSLParseError, ParseResult, WorldDSLParser | |
| from dsl.schema import ( | |
| AgentSpec, | |
| BehaviorSpec, | |
| EventSpec, | |
| PolicySpec, | |
| ResourceSpec, | |
| SimulationSpec, | |
| SpaceSpec, | |
| WorldSpec, | |
| ) | |
| from dsl.validator import ValidationConfig, ValidationReport, ValidationSeverity, validate_world_spec | |
| logger = logging.getLogger(__name__) | |
| DEFAULT_SYSTEM_PROMPT = """ | |
| You are WorldSmithAI's world DSL generator. | |
| Your only job is to convert a user's natural language description into a | |
| WorldSmithAI WorldSpec JSON object. | |
| Critical rules: | |
| - Output JSON only. | |
| - Do not include Markdown fences. | |
| - Do not include explanations. | |
| - Do not generate Python code. | |
| - Do not invent classes such as Sheep, Wolf, Scientist, Dragon, City, or Truck. | |
| - You must only use behavior names that are present in the known behavior list. | |
| - If the user asks for hunting, predation, chasing, fighting, or killing, use attack, seek, flee, move, or raid. | |
| - Do not invent behavior names such as hunt, graze, sleep, mate, patrol, forage, guard, chase, or rest unless they are explicitly listed in the known behavior registry. | |
| - Use generic DSL entities: agents, resources, events, behaviors, policies. | |
| - Agents may have any imaginative type string, but the runtime object is still Agent. | |
| - Behaviors must be referenced by registry name. | |
| - Policies must be referenced by registry type. | |
| - Keep the world small enough for a lightweight Gradio demo. | |
| - Prefer 4 to 8 agents unless the user asks for more. | |
| - Prefer 2D positions when possible. | |
| - Include state, memory, goals, behaviors, and policy for each agent. | |
| - Use deterministic values and avoid randomness in the DSL. | |
| """.strip() | |
| WORLD_DSL_FORMAT_GUIDE = """ | |
| Return one JSON object with this structure: | |
| { | |
| "schema_version": "1.0", | |
| "id": "short_world_id", | |
| "name": "Human readable world name", | |
| "description": "Short description", | |
| "simulation": { | |
| "steps": 50, | |
| "seed": 0, | |
| "scheduler": "sequential", | |
| "activation": "sequential", | |
| "collect_history": true | |
| }, | |
| "space": { | |
| "dimensions": 2, | |
| "bounds": [[0, 10], [0, 10]], | |
| "toroidal": false, | |
| "enforce_bounds": true | |
| }, | |
| "agents": [ | |
| { | |
| "id": "agent_1", | |
| "type": "generic_type", | |
| "position": [1, 1], | |
| "state": {"energy": 10, "credits": 5}, | |
| "memory": {"known_options": {"cooperate": 1.0}}, | |
| "goals": [{"id": "survive", "importance": 1.0, "score": 1.0}], | |
| "behaviors": [ | |
| {"name": "remember", "params": {"category": "observation", "content": {"note": "initial memory"}}}, | |
| {"name": "prioritize", "params": {}}, | |
| {"name": "choose_goal", "params": {}}, | |
| {"name": "communicate", "params": {"message": "status_update", "max_recipients": 1}}, | |
| {"name": "cooperate", "params": {"effort": 1.0}} | |
| ], | |
| "policy": { | |
| "type": "rule_policy", | |
| "params": { | |
| "rules": [ | |
| {"behavior_name": "choose_goal", "score_delta": 3.0}, | |
| {"behavior_name": "communicate", "score_delta": 1.0} | |
| ] | |
| } | |
| }, | |
| "alive": true, | |
| "metadata": {} | |
| } | |
| ], | |
| "resources": [ | |
| { | |
| "id": "resource_1", | |
| "type": "generic_resource", | |
| "amount": 10, | |
| "position": [5, 5], | |
| "regeneration_rate": 0, | |
| "max_amount": 20, | |
| "metadata": {} | |
| } | |
| ], | |
| "events": [], | |
| "metrics": [ | |
| {"name": "diversity", "params": {"collection": "agents", "group_by_path": "type"}} | |
| ], | |
| "metadata": {} | |
| } | |
| Known behavior names available in this project include: | |
| move, wander, seek, flee, | |
| eat, harvest, mine, collect, | |
| buy, sell, exchange, | |
| attack, defend, raid, | |
| study, publish, learn, | |
| cooperate, communicate, share, recommend, negotiate, | |
| route, deliver, ship, queue, charge, | |
| build, repair, expand, demolish, | |
| regulate, tax, subsidize, litigate, approve, enforce, | |
| bid, ask, price, discount, forecast, rebalance, | |
| adopt, switch, imitate, abandon, evaluate, | |
| prioritize, schedule, allocate_time, choose_goal, | |
| remember, forget, reinforce, update_belief. | |
| Behavior mapping guidance: | |
| - "hunt" should usually be represented as "attack" plus optionally "seek". | |
| - "run away" should be represented as "flee". | |
| - "look for" or "track" should be represented as "seek". | |
| - "random movement" should be represented as "wander" or "move". | |
| Known policy types include: | |
| rule_policy, contextual_bandit. | |
| """.strip() | |
| DEFAULT_MINIMAL_EXAMPLE = { | |
| "schema_version": "1.0", | |
| "id": "tiny_research_world", | |
| "name": "Tiny Research World", | |
| "description": "A small generic research ecosystem.", | |
| "simulation": { | |
| "steps": 40, | |
| "seed": 0, | |
| "scheduler": "sequential", | |
| "activation": "sequential", | |
| "collect_history": True, | |
| }, | |
| "space": { | |
| "dimensions": 2, | |
| "bounds": [[0, 10], [0, 10]], | |
| "toroidal": False, | |
| "enforce_bounds": True, | |
| }, | |
| "agents": [ | |
| { | |
| "id": "scientist_1", | |
| "type": "scientist", | |
| "position": [1, 1], | |
| "state": {"energy": 10, "credits": 5, "knowledge": 1}, | |
| "memory": {"known_options": {"study": 1.0, "communicate": 0.5}}, | |
| "goals": [{"id": "discover", "importance": 2.0, "score": 2.0}], | |
| "behaviors": [ | |
| { | |
| "name": "remember", | |
| "params": { | |
| "category": "observation", | |
| "content": {"note": "initial research context"}, | |
| }, | |
| }, | |
| {"name": "prioritize", "params": {}}, | |
| {"name": "choose_goal", "params": {}}, | |
| {"name": "communicate", "params": {"message": "research_update"}}, | |
| {"name": "cooperate", "params": {"goal": "discover", "effort": 1.0}}, | |
| ], | |
| "policy": { | |
| "type": "rule_policy", | |
| "params": { | |
| "rules": [ | |
| {"behavior_name": "choose_goal", "score_delta": 3.0}, | |
| {"behavior_name": "communicate", "score_delta": 2.0}, | |
| {"behavior_name": "cooperate", "score_delta": 1.0}, | |
| ] | |
| }, | |
| }, | |
| "alive": True, | |
| "metadata": {}, | |
| } | |
| ], | |
| "resources": [ | |
| { | |
| "id": "knowledge_pool", | |
| "type": "knowledge", | |
| "amount": 12, | |
| "position": [5, 5], | |
| "regeneration_rate": 0.2, | |
| "max_amount": 20, | |
| "metadata": {}, | |
| } | |
| ], | |
| "events": [], | |
| "metrics": [ | |
| {"name": "diversity", "params": {"collection": "agents", "group_by_path": "type"}}, | |
| {"name": "entropy", "params": {"collection": "agents", "value_path": "type"}}, | |
| {"name": "interestingness", "params": {"collection": "agents", "group_by_path": "type"}}, | |
| ], | |
| "metadata": {"generated_by": "worldsmithai_example"}, | |
| } | |
| class GenerationMode(str, Enum): | |
| """Supported generation modes.""" | |
| MODEL = "model" | |
| FALLBACK = "fallback" | |
| AUTO = "auto" | |
| class ResponseFormat(str, Enum): | |
| """Expected model response format.""" | |
| JSON_OBJECT = "json_object" | |
| TEXT_WITH_JSON = "text_with_json" | |
| class WorldGenerationError(RuntimeError): | |
| """Raised when natural-language-to-DSL generation fails.""" | |
| class ModelClientError(WorldGenerationError): | |
| """Raised when an SLM client cannot be called successfully.""" | |
| class WorldGenerationValidationError(WorldGenerationError): | |
| """Raised when generated DSL fails strict validation.""" | |
| def __init__(self, report: ValidationReport) -> None: | |
| """Initialize the exception with a semantic validation report.""" | |
| self.report = report | |
| super().__init__(report.summary()) | |
| class SupportsGenerate(Protocol): | |
| """Protocol for simple text generation clients.""" | |
| def generate(self, *args: Any, **kwargs: Any) -> Any: | |
| """Generate text from prompt-like arguments.""" | |
| class PromptBundle: | |
| """Prompt payload sent to an SLM client. | |
| The bundle includes both chat-style messages and flattened prompt text so | |
| many different client APIs can be supported from root-level ``app.py``. | |
| """ | |
| system_prompt: str | |
| user_prompt: str | |
| messages: tuple[Mapping[str, str], ...] | |
| metadata: Mapping[str, Any] = field(default_factory=dict) | |
| def text(self) -> str: | |
| """Return a flattened prompt for completion-style models.""" | |
| return f"{self.system_prompt}\n\nUSER REQUEST:\n{self.user_prompt}" | |
| def to_dict(self) -> dict[str, Any]: | |
| """Return a JSON-friendly prompt bundle.""" | |
| return { | |
| "system_prompt": self.system_prompt, | |
| "user_prompt": self.user_prompt, | |
| "messages": [dict(message) for message in self.messages], | |
| "metadata": dict(self.metadata), | |
| } | |
| class WorldGenerationResult: | |
| """Result returned by ``WorldGenerator.generate``. | |
| Attributes: | |
| spec: Validated world specification. | |
| raw_response: Raw text returned by the model, or fallback JSON text. | |
| parse_result: Parser result when available. | |
| validation_report: Optional semantic validation report. | |
| mode: Whether model or deterministic fallback produced the final spec. | |
| prompt_bundle: Prompt used for model generation when available. | |
| metadata: Additional diagnostics useful for Gradio display. | |
| """ | |
| spec: WorldSpec | |
| raw_response: str | |
| parse_result: ParseResult | None = None | |
| validation_report: ValidationReport | None = None | |
| mode: GenerationMode = GenerationMode.MODEL | |
| prompt_bundle: PromptBundle | None = None | |
| metadata: Mapping[str, Any] = field(default_factory=dict) | |
| def json_text(self) -> str: | |
| """Return the generated world spec as formatted JSON.""" | |
| return self.spec.to_json_string(indent=2, exclude_none=True) | |
| def is_semantically_valid(self) -> bool: | |
| """Return whether semantic validation passed or was not run.""" | |
| return self.validation_report is None or self.validation_report.is_valid | |
| def to_dict(self) -> dict[str, Any]: | |
| """Return a JSON-friendly result summary.""" | |
| return { | |
| "world_id": self.spec.id, | |
| "world_name": self.spec.name, | |
| "mode": self.mode.value, | |
| "agent_count": len(self.spec.agents), | |
| "resource_count": len(self.spec.resources), | |
| "event_count": len(self.spec.events), | |
| "behavior_count": len(self.spec.behavior_names), | |
| "is_semantically_valid": self.is_semantically_valid, | |
| "validation_report": None | |
| if self.validation_report is None | |
| else self.validation_report.to_dict(), | |
| "parse_result": None if self.parse_result is None else self.parse_result.to_dict(), | |
| "metadata": copy.deepcopy(dict(self.metadata)), | |
| } | |
| class WorldGenerationConfig: | |
| """Configuration for ``WorldGenerator``. | |
| Defaults are designed for a Gradio demo: | |
| - model output is parsed strictly, | |
| - semantic validation runs but does not raise by default, | |
| - deterministic fallback is enabled, | |
| - generated worlds stay small enough for interactive simulation. | |
| """ | |
| mode: GenerationMode | str = GenerationMode.AUTO | |
| response_format: ResponseFormat | str = ResponseFormat.JSON_OBJECT | |
| system_prompt: str = DEFAULT_SYSTEM_PROMPT | |
| include_format_guide: bool = True | |
| include_example: bool = True | |
| include_json_schema_excerpt: bool = False | |
| default_steps: int = 60 | |
| default_seed: int = 0 | |
| max_agents_hint: int = 8 | |
| max_resources_hint: int = 6 | |
| parser: WorldDSLParser = field(default_factory=WorldDSLParser) | |
| semantic_validation: bool = True | |
| strict_semantic_validation: bool = False | |
| semantic_validation_config: ValidationConfig | None = None | |
| fallback_on_model_error: bool = True | |
| fallback_on_parse_error: bool = True | |
| fallback_on_semantic_error: bool = False | |
| repair_attempts: int = 1 | |
| model_kwargs: Mapping[str, Any] = field(default_factory=dict) | |
| metadata: Mapping[str, Any] = field(default_factory=dict) | |
| def resolved_mode(self) -> GenerationMode: | |
| """Return normalized generation mode.""" | |
| if isinstance(self.mode, GenerationMode): | |
| return self.mode | |
| return GenerationMode(str(self.mode)) | |
| def resolved_response_format(self) -> ResponseFormat: | |
| """Return normalized response format.""" | |
| if isinstance(self.response_format, ResponseFormat): | |
| return self.response_format | |
| return ResponseFormat(str(self.response_format)) | |
| class DeterministicWorldSpecBuilder: | |
| """Build a valid generic fallback ``WorldSpec`` without an SLM. | |
| This fallback exists so the Gradio demo can still run when no SLM client is | |
| configured or when the model returns invalid JSON. | |
| Unlike the earliest fallback version, this builder preserves important | |
| prompt entities when possible. For example, a prompt mentioning sheep, | |
| wolves, a hunter, and grass will produce sheep agents, wolf agents, a | |
| hunter agent, and grass resources instead of generic farm-support roles. | |
| """ | |
| default_steps: int = 60 | |
| default_seed: int = 0 | |
| max_agents: int = 6 | |
| def build( | |
| self, | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| ) -> WorldSpec: | |
| """Build a deterministic world spec from prompt keywords.""" | |
| domain = self._infer_domain(prompt) | |
| max_agents = self._max_agents_from_constraints(constraints) or self.max_agents | |
| max_agents = max(1, int(max_agents)) | |
| roles = self._roles_for_prompt(prompt, domain, max_agents=max_agents) | |
| resources = self._resources_for_prompt(prompt, domain) | |
| world_id = _slugify(f"{domain}_world") | |
| agents = tuple( | |
| self._agent_spec( | |
| index=index, | |
| role=role, | |
| domain=domain, | |
| roles=roles, | |
| ) | |
| for index, role in enumerate(roles) | |
| ) | |
| resource_specs = tuple( | |
| ResourceSpec( | |
| id=self._resource_id(resource_type, index), | |
| type=resource_type, | |
| amount=float(10 + index * 4), | |
| position=self._resource_position_for_index(index), | |
| regeneration_rate=self._resource_regeneration_rate(resource_type), | |
| max_amount=float(30 + index * 5), | |
| metadata={ | |
| "domain": domain, | |
| "generated_by": "deterministic_fallback", | |
| "prompt_entity": resource_type in self._prompt_entity_resource_names(prompt), | |
| }, | |
| ) | |
| for index, resource_type in enumerate(resources) | |
| ) | |
| events = self._events_for_prompt( | |
| prompt=prompt, | |
| domain=domain, | |
| roles=roles, | |
| resources=resources, | |
| ) | |
| return WorldSpec( | |
| schema_version="1.0", | |
| id=world_id, | |
| name=_title_from_domain(domain), | |
| description=self._description(prompt, domain, constraints), | |
| simulation=SimulationSpec( | |
| steps=max(1, int(self.default_steps)), | |
| seed=self.default_seed, | |
| scheduler="sequential", | |
| activation="sequential", | |
| collect_history=True, | |
| ), | |
| space=SpaceSpec( | |
| dimensions=2, | |
| bounds=((0.0, 10.0), (0.0, 10.0)), | |
| toroidal=False, | |
| enforce_bounds=True, | |
| ), | |
| agents=agents, | |
| resources=resource_specs, | |
| events=events, | |
| metrics=( | |
| {"name": "diversity", "params": {"collection": "agents", "group_by_path": "type"}}, | |
| {"name": "entropy", "params": {"collection": "agents", "value_path": "type"}}, | |
| {"name": "stability", "params": {"collection": "agents", "group_by_path": "type"}}, | |
| {"name": "interestingness", "params": {"collection": "agents", "group_by_path": "type"}}, | |
| ), | |
| metadata={ | |
| "generated_by": "deterministic_fallback", | |
| "domain": domain, | |
| "source_prompt": prompt, | |
| "entity_aware_fallback": True, | |
| "constraints": constraints if isinstance(constraints, Mapping) else str(constraints or ""), | |
| }, | |
| ) | |
| def _agent_spec( | |
| self, | |
| *, | |
| index: int, | |
| role: str, | |
| domain: str, | |
| roles: Sequence[str], | |
| ) -> AgentSpec: | |
| """Create one generic fallback agent spec.""" | |
| agent_id = self._agent_id_for_roles(roles, index) | |
| target_agent_id = self._next_agent_id_for_roles(roles, index) | |
| goals = self._goals_for_role(role, domain) | |
| goal_records = [ | |
| { | |
| "id": goal, | |
| "importance": float(len(goals) - goal_index), | |
| "score": float(len(goals) - goal_index), | |
| "description": f"{role} works toward {goal}", | |
| } | |
| for goal_index, goal in enumerate(goals) | |
| ] | |
| x, y = self._position_for_index(index) | |
| role_state = self._state_for_role(role, domain, index) | |
| behaviors = [ | |
| BehaviorSpec( | |
| name="remember", | |
| params={ | |
| "category": "initial_context", | |
| "content": { | |
| "role": role, | |
| "domain": domain, | |
| "note": f"Seed memory for {role} created by entity-aware fallback generator.", | |
| }, | |
| "tags": ["initial", domain, role], | |
| }, | |
| ), | |
| BehaviorSpec( | |
| name="evaluate", | |
| params={ | |
| "category": f"{role}_strategy", | |
| "candidate_options": self._options_for_role(role), | |
| "known_options_path": "memory.known_options", | |
| "adoption_threshold": 0.7, | |
| "write_decision_path": "memory.latest_strategy_decision", | |
| }, | |
| ), | |
| BehaviorSpec( | |
| name="prioritize", | |
| params={ | |
| "source_path": "memory.goals", | |
| "item_attribute_weights": { | |
| "importance": 1.0, | |
| "score": 1.0, | |
| }, | |
| }, | |
| ), | |
| BehaviorSpec( | |
| name="choose_goal", | |
| params={ | |
| "source_path": "memory.latest_priorities.items", | |
| "current_goal_path": "state.current_goal", | |
| }, | |
| ), | |
| BehaviorSpec( | |
| name="communicate", | |
| params={ | |
| "target_agent_id": target_agent_id, | |
| "message": f"{role}_status_update", | |
| "max_recipients": 1, | |
| } | |
| if target_agent_id is not None | |
| else { | |
| "message": f"{role}_status_update", | |
| "max_recipients": 1, | |
| }, | |
| ), | |
| BehaviorSpec( | |
| name="cooperate", | |
| params={ | |
| "target_agent_id": target_agent_id, | |
| "goal": goals[0], | |
| "effort": self._effort_for_role(role), | |
| } | |
| if target_agent_id is not None | |
| else { | |
| "goal": goals[0], | |
| "effort": self._effort_for_role(role), | |
| }, | |
| ), | |
| BehaviorSpec( | |
| name="update_belief", | |
| params={ | |
| "belief_id": f"{role}_situation_belief", | |
| "proposition": self._belief_for_role(role), | |
| "evidence_path": self._evidence_path_for_role(role), | |
| "learning_rate": 0.25, | |
| "write_belief_path": "memory.latest_situation_belief", | |
| }, | |
| ), | |
| ] | |
| if role in {"hunter", "farmer"}: | |
| behaviors.append( | |
| BehaviorSpec( | |
| name="recommend", | |
| params={ | |
| "target_agent_id": target_agent_id, | |
| "recommendation": "protect_flock_and_monitor_wolves", | |
| "confidence": 0.75, | |
| } | |
| if target_agent_id is not None | |
| else { | |
| "recommendation": "protect_flock_and_monitor_wolves", | |
| "confidence": 0.75, | |
| }, | |
| ) | |
| ) | |
| if role == "wolf": | |
| behaviors.append( | |
| BehaviorSpec( | |
| name="share", | |
| params={ | |
| "target_agent_id": target_agent_id, | |
| "content_path": "state.pack_signal", | |
| "share_key": "pack_signal_share", | |
| "relationship_delta": 0.05, | |
| } | |
| if target_agent_id is not None | |
| else { | |
| "content_path": "state.pack_signal", | |
| "share_key": "pack_signal_share", | |
| "relationship_delta": 0.05, | |
| }, | |
| ) | |
| ) | |
| policy_rules = [ | |
| {"behavior_name": "choose_goal", "score_delta": 4.0, "priority_delta": 1.0}, | |
| {"behavior_name": "prioritize", "score_delta": 3.0}, | |
| {"behavior_name": "communicate", "score_delta": 2.0}, | |
| {"behavior_name": "cooperate", "score_delta": 1.5}, | |
| {"behavior_name": "evaluate", "score_delta": 1.0}, | |
| {"behavior_name": "update_belief", "score_delta": 0.8}, | |
| {"behavior_name": "remember", "score_delta": 0.5}, | |
| ] | |
| if role in {"hunter", "farmer"}: | |
| policy_rules.append({"behavior_name": "recommend", "score_delta": 1.2}) | |
| if role == "wolf": | |
| policy_rules.append({"behavior_name": "share", "score_delta": 1.2}) | |
| return AgentSpec( | |
| id=agent_id, | |
| type=role, | |
| position=(x, y), | |
| state=role_state, | |
| memory={ | |
| "goals": goal_records, | |
| "known_options": self._known_options_for_role(role), | |
| "relationships": {}, | |
| "source_role": role, | |
| }, | |
| goals=goal_records, | |
| behaviors=tuple(behaviors), | |
| policy=PolicySpec( | |
| type="rule_policy", | |
| params={ | |
| "selection_strategy": "highest_score", | |
| "rules": policy_rules, | |
| }, | |
| ), | |
| alive=True, | |
| metadata={ | |
| "domain": domain, | |
| "generated_by": "deterministic_fallback", | |
| "prompt_role": role, | |
| }, | |
| ) | |
| def _roles_for_prompt( | |
| self, | |
| prompt: str, | |
| domain: str, | |
| *, | |
| max_agents: int, | |
| ) -> tuple[str, ...]: | |
| """Return role list that preserves important prompt entities.""" | |
| text = prompt.lower() | |
| roles: list[str] = [] | |
| if domain == "farm": | |
| roles.append("farmer") | |
| has_sheep = self._contains_any(text, ("sheep", "lamb", "flock")) | |
| has_wolf = self._contains_any(text, ("wolf", "wolves", "pack")) | |
| has_hunter = self._contains_any(text, ("hunter", "ranger", "shepherd")) | |
| if has_sheep: | |
| roles.append("sheep") | |
| if has_wolf: | |
| roles.append("wolf") | |
| if has_hunter: | |
| roles.append("hunter") | |
| if has_sheep and len(roles) < max_agents: | |
| roles.append("sheep") | |
| if has_wolf and len(roles) < max_agents: | |
| roles.append("wolf") | |
| if not has_sheep and not has_wolf and not has_hunter: | |
| roles.extend(self._roles_for_domain(domain)) | |
| return tuple(roles[:max_agents]) | |
| roles.extend(self._roles_for_domain(domain)) | |
| return tuple(roles[:max_agents]) | |
| def _resources_for_prompt(self, prompt: str, domain: str) -> tuple[str, ...]: | |
| """Return resource types that preserve important prompt entities.""" | |
| text = prompt.lower() | |
| if domain == "farm": | |
| resources: list[str] = [] | |
| if self._contains_any(text, ("grass", "pasture")): | |
| resources.append("grass") | |
| else: | |
| resources.append("food") | |
| if self._contains_any(text, ("sheep", "flock", "lamb")): | |
| resources.append("flock_safety") | |
| resources.append("shelter") | |
| if self._contains_any(text, ("wolf", "wolves", "pack", "hunter")): | |
| resources.append("predator_pressure") | |
| resources.append("water") | |
| if "soil_health" not in resources: | |
| resources.append("soil_health") | |
| return tuple(dict.fromkeys(resources)) | |
| return self._resources_for_domain(domain) | |
| def _events_for_prompt( | |
| self, | |
| *, | |
| prompt: str, | |
| domain: str, | |
| roles: Sequence[str], | |
| resources: Sequence[str], | |
| ) -> tuple[EventSpec, ...]: | |
| """Return domain events that preserve important prompt entities.""" | |
| text = prompt.lower() | |
| events: list[EventSpec] = [] | |
| wolf_ids = [ | |
| self._agent_id_for_roles(roles, index) | |
| for index, role in enumerate(roles) | |
| if role == "wolf" | |
| ] | |
| sheep_ids = [ | |
| self._agent_id_for_roles(roles, index) | |
| for index, role in enumerate(roles) | |
| if role == "sheep" | |
| ] | |
| hunter_ids = [ | |
| self._agent_id_for_roles(roles, index) | |
| for index, role in enumerate(roles) | |
| if role == "hunter" | |
| ] | |
| farmer_ids = [ | |
| self._agent_id_for_roles(roles, index) | |
| for index, role in enumerate(roles) | |
| if role == "farmer" | |
| ] | |
| resource_ids = { | |
| resource_type: self._resource_id(resource_type, index) | |
| for index, resource_type in enumerate(resources) | |
| } | |
| if domain == "farm" and self._contains_any(text, ("wolf", "wolves", "pack")): | |
| events.append( | |
| EventSpec( | |
| id="wolf_pack_enters", | |
| name="wolf_pack_enters", | |
| trigger_step=max(1, self.default_steps // 4), | |
| payload={ | |
| "description": "A pack of wolves enters the farm and increases pressure on the flock.", | |
| "signals": { | |
| "predator_pressure": 0.35, | |
| "flock_fear": 0.25, | |
| }, | |
| }, | |
| enabled=True, | |
| target_agent_ids=tuple(wolf_ids + sheep_ids + farmer_ids), | |
| target_resource_ids=tuple( | |
| resource_id | |
| for key, resource_id in resource_ids.items() | |
| if key in {"predator_pressure", "flock_safety", "grass"} | |
| ), | |
| metadata={ | |
| "generated_by": "entity_aware_fallback", | |
| "narrative_hint": "Predator pressure tests flock safety and farm coordination.", | |
| }, | |
| ) | |
| ) | |
| if domain == "farm" and self._contains_any(text, ("hunter", "ranger")): | |
| events.append( | |
| EventSpec( | |
| id="hunter_enters", | |
| name="hunter_enters", | |
| trigger_step=max(2, self.default_steps // 3), | |
| payload={ | |
| "description": "A hunter enters the farm area and changes the balance between wolves and sheep.", | |
| "signals": { | |
| "protection_pressure": 0.3, | |
| "wolf_caution": 0.2, | |
| }, | |
| }, | |
| enabled=True, | |
| target_agent_ids=tuple(hunter_ids + wolf_ids + farmer_ids + sheep_ids), | |
| target_resource_ids=tuple( | |
| resource_id | |
| for key, resource_id in resource_ids.items() | |
| if key in {"predator_pressure", "flock_safety"} | |
| ), | |
| metadata={ | |
| "generated_by": "entity_aware_fallback", | |
| "narrative_hint": "Hunter arrival adds a stabilizing or disruptive force.", | |
| }, | |
| ) | |
| ) | |
| if not events: | |
| events.append( | |
| EventSpec( | |
| id="midpoint_pressure", | |
| name="midpoint_pressure", | |
| trigger_step=max(1, self.default_steps // 2), | |
| payload={ | |
| "description": "A generic pressure event that can be narrated later.", | |
| "domain": domain, | |
| }, | |
| enabled=True, | |
| metadata={"generated_by": "fallback_builder"}, | |
| ) | |
| ) | |
| return tuple(events) | |
| def _state_for_role(self, role: str, domain: str, index: int) -> dict[str, Any]: | |
| """Return role-specific initial state.""" | |
| base_state: dict[str, Any] = { | |
| "energy": float(max(3, 10 - index)), | |
| "credits": float(5 + index), | |
| "influence": float(index + 1), | |
| "domain_pressure": 0.2, | |
| "inventory": {}, | |
| } | |
| if role == "sheep": | |
| base_state.update( | |
| { | |
| "hunger": 0.4, | |
| "fear": 0.35, | |
| "herd_signal": 0.75, | |
| "grazing_need": 0.8, | |
| "inventory": {"wool": 1}, | |
| } | |
| ) | |
| return base_state | |
| if role == "wolf": | |
| base_state.update( | |
| { | |
| "hunger": 0.85, | |
| "pack_signal": 0.8, | |
| "caution": 0.3, | |
| "predator_intent": 0.9, | |
| "inventory": {}, | |
| } | |
| ) | |
| return base_state | |
| if role == "hunter": | |
| base_state.update( | |
| { | |
| "tracking_skill": 0.8, | |
| "protection_signal": 0.7, | |
| "caution": 0.55, | |
| "inventory": {"tools": 2, "supplies": 2}, | |
| } | |
| ) | |
| return base_state | |
| if role == "farmer": | |
| base_state.update( | |
| { | |
| "flock_care": 0.8, | |
| "grass_management": 0.65, | |
| "protection_signal": 0.5, | |
| "inventory": {"hay": 3, "tools": 2}, | |
| } | |
| ) | |
| return base_state | |
| base_state.update( | |
| { | |
| "role_signal": 0.6, | |
| "inventory": {}, | |
| } | |
| ) | |
| return base_state | |
| def _goals_for_role(self, role: str, domain: str) -> tuple[str, ...]: | |
| """Return role-specific fallback goal ids.""" | |
| if role == "sheep": | |
| return ("graze_on_grass", "stay_with_flock", "avoid_wolves") | |
| if role == "wolf": | |
| return ("coordinate_pack", "seek_food_pressure", "avoid_hunter") | |
| if role == "hunter": | |
| return ("protect_flock", "track_wolves", "avoid_unnecessary_harm") | |
| if role == "farmer": | |
| return ("protect_sheep", "maintain_grass", "coordinate_hunter") | |
| return self._goals_for_domain(domain) | |
| def _known_options_for_role(self, role: str) -> dict[str, Any]: | |
| """Return role-specific known options.""" | |
| if role == "sheep": | |
| return { | |
| "graze": {"score": 1.0, "energy": 0.8}, | |
| "flock_together": {"score": 1.2, "safety": 0.9}, | |
| "avoid_predator": {"score": 1.4, "safety": 1.0}, | |
| } | |
| if role == "wolf": | |
| return { | |
| "pack_coordination": {"score": 1.3, "success": 0.8}, | |
| "cautious_entry": {"score": 1.0, "safety": 0.7}, | |
| "pressure_flock": {"score": 1.2, "food": 0.8}, | |
| } | |
| if role == "hunter": | |
| return { | |
| "track_wolves": {"score": 1.2, "protection": 0.9}, | |
| "coordinate_farmer": {"score": 1.1, "trust": 0.8}, | |
| "guard_pasture": {"score": 1.0, "safety": 0.9}, | |
| } | |
| if role == "farmer": | |
| return { | |
| "protect_flock": {"score": 1.2, "safety": 0.9}, | |
| "maintain_pasture": {"score": 1.0, "grass": 0.8}, | |
| "coordinate_hunter": {"score": 1.1, "protection": 0.8}, | |
| } | |
| return { | |
| "cooperate": 1.0, | |
| "communicate": 0.8, | |
| "evaluate": 0.6, | |
| } | |
| def _options_for_role(self, role: str) -> list[str]: | |
| """Return candidate option ids for evaluation behavior.""" | |
| return list(self._known_options_for_role(role).keys()) | |
| def _belief_for_role(self, role: str) -> str: | |
| """Return a role-specific belief proposition.""" | |
| if role == "sheep": | |
| return "The flock can stay safe while grazing." | |
| if role == "wolf": | |
| return "The pack can coordinate under farm and hunter pressure." | |
| if role == "hunter": | |
| return "The hunter can reduce wolf pressure without destabilizing the farm." | |
| if role == "farmer": | |
| return "The farm can protect sheep while maintaining grass resources." | |
| return "The current situation is manageable." | |
| def _evidence_path_for_role(self, role: str) -> str: | |
| """Return a role-specific evidence path.""" | |
| if role == "sheep": | |
| return "state.herd_signal" | |
| if role == "wolf": | |
| return "state.pack_signal" | |
| if role == "hunter": | |
| return "state.protection_signal" | |
| if role == "farmer": | |
| return "state.flock_care" | |
| return "state.domain_pressure" | |
| def _effort_for_role(self, role: str) -> float: | |
| """Return role-specific cooperation effort.""" | |
| if role in {"hunter", "farmer", "wolf"}: | |
| return 1.0 | |
| if role == "sheep": | |
| return 0.7 | |
| return 0.8 | |
| def _resource_position_for_index(index: int) -> tuple[float, float]: | |
| """Return deterministic in-bounds 2D position for a resource index.""" | |
| positions = ( | |
| (2.0, 7.0), | |
| (4.0, 6.0), | |
| (6.0, 5.0), | |
| (8.0, 4.0), | |
| (2.0, 3.0), | |
| (4.0, 2.0), | |
| (6.0, 7.0), | |
| (8.0, 6.0), | |
| (5.0, 5.0), | |
| (7.0, 3.0), | |
| ) | |
| return positions[index % len(positions)] | |
| def _position_for_index(self, index: int) -> tuple[float, float]: | |
| """Return deterministic 2D position for agent index.""" | |
| positions = ( | |
| (1.0, 1.0), | |
| (3.0, 6.5), | |
| (7.5, 6.5), | |
| (8.5, 2.0), | |
| (4.0, 7.5), | |
| (6.5, 2.5), | |
| (2.0, 4.0), | |
| (8.0, 8.0), | |
| ) | |
| return positions[index % len(positions)] | |
| def _agent_id_for_roles(self, roles: Sequence[str], index: int) -> str: | |
| """Return stable id for an agent in a possibly duplicated role list.""" | |
| role = roles[index] | |
| ordinal = sum(1 for candidate in roles[: index + 1] if candidate == role) | |
| return f"{role}_{ordinal}" | |
| def _next_agent_id_for_roles(self, roles: Sequence[str], index: int) -> str | None: | |
| """Return next cyclic target agent id.""" | |
| if len(roles) <= 1: | |
| return None | |
| next_index = (index + 1) % len(roles) | |
| return self._agent_id_for_roles(roles, next_index) | |
| def _resource_id(self, resource_type: str, index: int) -> str: | |
| """Return stable resource id.""" | |
| return f"{_slugify(resource_type)}_{index + 1}" | |
| def _resource_regeneration_rate(self, resource_type: str) -> float: | |
| """Return resource-specific regeneration rate.""" | |
| if resource_type in {"grass", "food", "water", "soil_health"}: | |
| return 0.2 | |
| if resource_type in {"flock_safety", "predator_pressure"}: | |
| return 0.05 | |
| return 0.0 | |
| def _max_agents_from_constraints( | |
| self, | |
| constraints: Mapping[str, Any] | str | None, | |
| ) -> int | None: | |
| """Extract max_agents from parsed constraints when available.""" | |
| if not isinstance(constraints, Mapping): | |
| return None | |
| value = constraints.get("max_agents") | |
| if value is None: | |
| return None | |
| try: | |
| return max(1, int(value)) | |
| except (TypeError, ValueError): | |
| return None | |
| def _prompt_entity_resource_names(self, prompt: str) -> set[str]: | |
| """Return resource names explicitly suggested by the prompt.""" | |
| text = prompt.lower() | |
| names: set[str] = set() | |
| if self._contains_any(text, ("grass", "pasture")): | |
| names.add("grass") | |
| return names | |
| def _contains_any(text: str, words: Sequence[str]) -> bool: | |
| """Return whether any word-like string appears in text.""" | |
| lowered = text.lower() | |
| return any(word in lowered for word in words) | |
| def _infer_domain(prompt: str) -> str: | |
| """Infer a broad fallback domain from prompt keywords.""" | |
| text = prompt.lower() | |
| keyword_domains = ( | |
| ("farm", ("farm", "crop", "soil", "harvest", "animal", "pasture", "sheep", "wolf", "wolves", "grass")), | |
| ("medieval", ("medieval", "kingdom", "castle", "guild", "village", "empire")), | |
| ("research", ("research", "scientist", "paper", "lab", "study", "knowledge")), | |
| ("startup", ("startup", "founder", "investor", "customer", "market", "product")), | |
| ("social", ("social", "network", "influencer", "community", "friend", "viral")), | |
| ("transport", ("transport", "route", "traffic", "delivery", "ship", "vehicle")), | |
| ("power", ("power", "grid", "energy", "battery", "generator", "electric")), | |
| ("space", ("space", "colony", "planet", "terraform", "asteroid", "orbital")), | |
| ("fantasy", ("fantasy", "dragon", "mana", "mage", "kingdom", "quest")), | |
| ) | |
| for domain, keywords in keyword_domains: | |
| if any(keyword in text for keyword in keywords): | |
| return domain | |
| return "generic" | |
| def _roles_for_domain(domain: str) -> tuple[str, ...]: | |
| """Return fallback agent roles for a domain.""" | |
| roles_by_domain = { | |
| "farm": ("farmer", "pollinator", "soil_guardian", "market_vendor"), | |
| "medieval": ("ruler", "merchant", "artisan", "scholar", "guard"), | |
| "research": ("scientist", "reviewer", "engineer", "curator", "funding_agent"), | |
| "startup": ("founder", "customer", "investor", "operator", "competitor"), | |
| "social": ("creator", "moderator", "member", "recommender", "critic"), | |
| "transport": ("hub", "carrier", "dispatcher", "charger", "passenger"), | |
| "power": ("generator", "storage_node", "consumer", "regulator", "maintainer"), | |
| "space": ("colonist", "engineer", "botanist", "navigator", "miner"), | |
| "fantasy": ("mage", "merchant", "guardian", "dragon", "healer"), | |
| "generic": ("explorer", "builder", "trader", "learner", "coordinator"), | |
| } | |
| return roles_by_domain.get(domain, roles_by_domain["generic"]) | |
| def _resources_for_domain(domain: str) -> tuple[str, ...]: | |
| """Return fallback resource types for a domain.""" | |
| resources_by_domain = { | |
| "farm": ("food", "water", "soil_health"), | |
| "medieval": ("grain", "gold", "craft_materials"), | |
| "research": ("knowledge", "funding", "attention"), | |
| "startup": ("capital", "users", "compute"), | |
| "social": ("attention", "trust", "content"), | |
| "transport": ("capacity", "fuel", "demand"), | |
| "power": ("energy", "storage", "load"), | |
| "space": ("oxygen", "minerals", "habitat_capacity"), | |
| "fantasy": ("mana", "relics", "influence"), | |
| "generic": ("energy", "materials", "knowledge"), | |
| } | |
| return resources_by_domain.get(domain, resources_by_domain["generic"]) | |
| def _goals_for_domain(domain: str) -> tuple[str, ...]: | |
| """Return fallback goal ids for a domain.""" | |
| goals_by_domain = { | |
| "farm": ("sustain_harvest", "share_resources", "adapt_to_weather"), | |
| "medieval": ("maintain_order", "grow_trade", "protect_settlement"), | |
| "research": ("discover", "publish", "collaborate"), | |
| "startup": ("find_users", "improve_product", "raise_capital"), | |
| "social": ("grow_trust", "spread_useful_content", "moderate_conflict"), | |
| "transport": ("deliver_reliably", "reduce_congestion", "balance_capacity"), | |
| "power": ("meet_demand", "prevent_outage", "rebalance_energy"), | |
| "space": ("survive_colony", "expand_habitat", "extract_resources"), | |
| "fantasy": ("protect_realm", "gather_mana", "negotiate_alliances"), | |
| "generic": ("survive", "learn", "cooperate"), | |
| } | |
| return goals_by_domain.get(domain, goals_by_domain["generic"]) | |
| def _description( | |
| prompt: str, | |
| domain: str, | |
| constraints: Mapping[str, Any] | str | None, | |
| ) -> str: | |
| """Build fallback world description.""" | |
| constraint_text = "" | |
| if constraints: | |
| constraint_text = f" Constraints: {constraints}" | |
| return ( | |
| f"A deterministic entity-aware fallback {domain} world generated from the prompt: " | |
| f"{prompt.strip()[:240]}{constraint_text}" | |
| ) | |
| class WorldGenerator: | |
| """Generate ``WorldSpec`` objects from natural language prompts. | |
| The generator is intentionally adapter-neutral. A client can be any object | |
| exposing ``generate``, ``complete``, ``chat``, or ``__call__``. The module | |
| does not import a specific model SDK. | |
| """ | |
| client: Any | None = None | |
| config: WorldGenerationConfig = field(default_factory=WorldGenerationConfig) | |
| def generate( | |
| self, | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> WorldGenerationResult: | |
| """Generate a validated ``WorldSpec`` from natural language. | |
| Args: | |
| prompt: Natural language world description. | |
| constraints: Optional extra constraints from the UI. | |
| examples: Optional few-shot examples as specs, mappings, or strings. | |
| model_kwargs: Optional generation kwargs for the model client. | |
| Returns: | |
| ``WorldGenerationResult``. | |
| Raises: | |
| WorldGenerationError: If generation fails and fallback is disabled. | |
| """ | |
| normalized_prompt = _require_prompt(prompt) | |
| mode = self.config.resolved_mode() | |
| if mode is GenerationMode.FALLBACK or self.client is None: | |
| return self._fallback_result( | |
| normalized_prompt, | |
| constraints=constraints, | |
| reason="fallback_mode" if mode is GenerationMode.FALLBACK else "client_not_configured", | |
| ) | |
| prompt_bundle = self.build_prompt( | |
| normalized_prompt, | |
| constraints=constraints, | |
| examples=examples, | |
| ) | |
| try: | |
| raw_response = self._call_client(prompt_bundle, model_kwargs=model_kwargs) | |
| except Exception as exc: | |
| if self.config.fallback_on_model_error and mode is GenerationMode.AUTO: | |
| logger.warning("Model client failed; using deterministic fallback: %s", exc) | |
| return self._fallback_result( | |
| normalized_prompt, | |
| constraints=constraints, | |
| reason="model_client_error", | |
| error=exc, | |
| prompt_bundle=prompt_bundle, | |
| ) | |
| raise ModelClientError(f"Model client failed: {exc}") from exc | |
| parse_result: ParseResult | None = None | |
| parse_error: BaseException | None = None | |
| current_response = raw_response | |
| for attempt in range(max(1, int(self.config.repair_attempts) + 1)): | |
| try: | |
| parse_result = self.config.parser.parse_result(current_response) | |
| break | |
| except Exception as exc: | |
| parse_error = exc | |
| if attempt >= int(self.config.repair_attempts): | |
| break | |
| current_response = self._repair_response( | |
| prompt_bundle=prompt_bundle, | |
| bad_response=current_response, | |
| error=exc, | |
| model_kwargs=model_kwargs, | |
| ) | |
| if parse_result is None: | |
| if self.config.fallback_on_parse_error and mode is GenerationMode.AUTO: | |
| logger.warning("Generated DSL could not be parsed; using fallback: %s", parse_error) | |
| return self._fallback_result( | |
| normalized_prompt, | |
| constraints=constraints, | |
| reason="parse_error", | |
| error=parse_error, | |
| raw_response=raw_response, | |
| prompt_bundle=prompt_bundle, | |
| ) | |
| raise WorldGenerationError(f"Generated DSL could not be parsed: {parse_error}") from parse_error | |
| validation_report = self._semantic_validation(parse_result.spec) | |
| if validation_report is not None and not validation_report.is_valid: | |
| if self.config.strict_semantic_validation: | |
| if self.config.fallback_on_semantic_error and mode is GenerationMode.AUTO: | |
| return self._fallback_result( | |
| normalized_prompt, | |
| constraints=constraints, | |
| reason="semantic_validation_error", | |
| raw_response=raw_response, | |
| prompt_bundle=prompt_bundle, | |
| ) | |
| raise WorldGenerationValidationError(validation_report) | |
| return WorldGenerationResult( | |
| spec=parse_result.spec, | |
| raw_response=raw_response, | |
| parse_result=parse_result, | |
| validation_report=validation_report, | |
| mode=GenerationMode.MODEL, | |
| prompt_bundle=prompt_bundle, | |
| metadata={ | |
| **copy.deepcopy(dict(self.config.metadata)), | |
| "repair_attempts_used": 0 if current_response == raw_response else 1, | |
| }, | |
| ) | |
| def generate_spec( | |
| self, | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> WorldSpec: | |
| """Generate and return only the ``WorldSpec``.""" | |
| return self.generate( | |
| prompt, | |
| constraints=constraints, | |
| examples=examples, | |
| model_kwargs=model_kwargs, | |
| ).spec | |
| def generate_json( | |
| self, | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> str: | |
| """Generate and return formatted WorldSpec JSON.""" | |
| return self.generate( | |
| prompt, | |
| constraints=constraints, | |
| examples=examples, | |
| model_kwargs=model_kwargs, | |
| ).json_text | |
| def build_prompt( | |
| self, | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| ) -> PromptBundle: | |
| """Build the prompt bundle sent to the model client.""" | |
| sections: list[str] = [] | |
| sections.append(f"World request:\n{prompt.strip()}") | |
| if constraints: | |
| sections.append(f"Additional constraints:\n{_serialize_constraints(constraints)}") | |
| sections.append( | |
| "Return a compact but complete WorldSmithAI WorldSpec JSON object. " | |
| "The JSON must be parseable by Python json.loads." | |
| ) | |
| if self.config.include_format_guide: | |
| sections.append(f"WorldSpec format guide:\n{WORLD_DSL_FORMAT_GUIDE}") | |
| if self.config.include_json_schema_excerpt: | |
| sections.append(f"Schema excerpt:\n{world_spec_schema_excerpt()}") | |
| example_payloads = self._example_payloads(examples) | |
| if self.config.include_example and example_payloads: | |
| sections.append("Example valid WorldSpec JSON:\n" + example_payloads[0]) | |
| sections.append( | |
| "Final answer requirements:\n" | |
| "- JSON object only.\n" | |
| "- No markdown.\n" | |
| "- No comments.\n" | |
| "- No Python code.\n" | |
| "- No prose before or after the JSON." | |
| ) | |
| user_prompt = "\n\n".join(sections) | |
| system_prompt = self.config.system_prompt | |
| return PromptBundle( | |
| system_prompt=system_prompt, | |
| user_prompt=user_prompt, | |
| messages=( | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ), | |
| metadata={ | |
| "response_format": self.config.resolved_response_format().value, | |
| "default_steps": self.config.default_steps, | |
| "max_agents_hint": self.config.max_agents_hint, | |
| "max_resources_hint": self.config.max_resources_hint, | |
| }, | |
| ) | |
| def _example_payloads( | |
| self, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None, | |
| ) -> tuple[str, ...]: | |
| """Return serialized few-shot examples.""" | |
| if examples is None: | |
| return (json.dumps(DEFAULT_MINIMAL_EXAMPLE, indent=2),) | |
| payloads: list[str] = [] | |
| for example in examples: | |
| if isinstance(example, WorldSpec): | |
| payloads.append(example.to_json_string(indent=2, exclude_none=True)) | |
| elif isinstance(example, Mapping): | |
| payloads.append(json.dumps(example, indent=2)) | |
| elif isinstance(example, str): | |
| payloads.append(example.strip()) | |
| return tuple(payload for payload in payloads if payload) | |
| def _call_client( | |
| self, | |
| prompt_bundle: PromptBundle, | |
| *, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> str: | |
| """Call the configured model client and normalize its response to text.""" | |
| if self.client is None: | |
| raise ModelClientError("No model client is configured") | |
| kwargs = { | |
| **copy.deepcopy(dict(self.config.model_kwargs)), | |
| **copy.deepcopy(dict(model_kwargs or {})), | |
| } | |
| callable_obj, call_style = self._resolve_client_callable(self.client) | |
| call_kwargs = { | |
| "prompt": prompt_bundle.text, | |
| "system_prompt": prompt_bundle.system_prompt, | |
| "user_prompt": prompt_bundle.user_prompt, | |
| "messages": [dict(message) for message in prompt_bundle.messages], | |
| "response_format": {"type": "json_object"}, | |
| **kwargs, | |
| } | |
| if call_style == "chat": | |
| call_kwargs.setdefault("prompt", prompt_bundle.user_prompt) | |
| response = _call_with_supported_kwargs(callable_obj, call_kwargs) | |
| return normalize_model_response(response) | |
| def _repair_response( | |
| self, | |
| *, | |
| prompt_bundle: PromptBundle, | |
| bad_response: str, | |
| error: BaseException, | |
| model_kwargs: Mapping[str, Any] | None, | |
| ) -> str: | |
| """Ask the model client to repair malformed JSON.""" | |
| repair_user_prompt = ( | |
| "The previous response was not valid WorldSmithAI JSON.\n\n" | |
| f"Parser error:\n{error}\n\n" | |
| "Previous response:\n" | |
| f"{bad_response[:8000]}\n\n" | |
| "Return only a corrected WorldSpec JSON object. No markdown. No prose." | |
| ) | |
| repair_bundle = PromptBundle( | |
| system_prompt=prompt_bundle.system_prompt, | |
| user_prompt=repair_user_prompt, | |
| messages=( | |
| {"role": "system", "content": prompt_bundle.system_prompt}, | |
| {"role": "user", "content": repair_user_prompt}, | |
| ), | |
| metadata={"repair": True}, | |
| ) | |
| return self._call_client(repair_bundle, model_kwargs=model_kwargs) | |
| def _resolve_client_callable(client: Any) -> tuple[Callable[..., Any], str]: | |
| """Resolve a callable generation method from a model client.""" | |
| for method_name, style in ( | |
| ("generate", "generate"), | |
| ("complete", "complete"), | |
| ("chat", "chat"), | |
| ): | |
| method = getattr(client, method_name, None) | |
| if callable(method): | |
| return method, style | |
| if callable(client): | |
| return client, "callable" | |
| raise ModelClientError( | |
| "Model client must expose generate, complete, chat, or be callable" | |
| ) | |
| def _semantic_validation(self, spec: WorldSpec) -> ValidationReport | None: | |
| """Run optional semantic validation.""" | |
| if not self.config.semantic_validation: | |
| return None | |
| validation_config = self.config.semantic_validation_config | |
| if validation_config is None: | |
| validation_config = ValidationConfig( | |
| require_known_behaviors=True, | |
| require_known_policies=True, | |
| unknown_registry_item_severity=ValidationSeverity.WARNING, | |
| constructor_param_severity=ValidationSeverity.WARNING, | |
| unresolved_reference_severity=ValidationSeverity.WARNING, | |
| ) | |
| return validate_world_spec(spec, config=validation_config) | |
| def _fallback_result( | |
| self, | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None, | |
| reason: str, | |
| error: BaseException | None = None, | |
| raw_response: str | None = None, | |
| prompt_bundle: PromptBundle | None = None, | |
| ) -> WorldGenerationResult: | |
| """Build a deterministic fallback result.""" | |
| builder = DeterministicWorldSpecBuilder( | |
| default_steps=self.config.default_steps, | |
| default_seed=self.config.default_seed, | |
| max_agents=min(max(1, int(self.config.max_agents_hint)), 6), | |
| ) | |
| spec = builder.build(prompt, constraints=constraints) | |
| validation_report = self._semantic_validation(spec) | |
| raw = raw_response or spec.to_json_string(indent=2, exclude_none=True) | |
| return WorldGenerationResult( | |
| spec=spec, | |
| raw_response=raw, | |
| parse_result=None, | |
| validation_report=validation_report, | |
| mode=GenerationMode.FALLBACK, | |
| prompt_bundle=prompt_bundle, | |
| metadata={ | |
| **copy.deepcopy(dict(self.config.metadata)), | |
| "fallback_reason": reason, | |
| "error": None if error is None else f"{error.__class__.__name__}: {error}", | |
| }, | |
| ) | |
| def generate_world_spec( | |
| prompt: str, | |
| *, | |
| client: Any | None = None, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| config: WorldGenerationConfig | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> WorldSpec: | |
| """Convenience function that returns a generated ``WorldSpec``. | |
| This is the simplest function to call from root-level ``app.py``. | |
| """ | |
| generator = WorldGenerator(client=client, config=config or WorldGenerationConfig()) | |
| return generator.generate_spec( | |
| prompt, | |
| constraints=constraints, | |
| examples=examples, | |
| model_kwargs=model_kwargs, | |
| ) | |
| def generate_world_json( | |
| prompt: str, | |
| *, | |
| client: Any | None = None, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| config: WorldGenerationConfig | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> str: | |
| """Convenience function that returns generated WorldSpec JSON.""" | |
| generator = WorldGenerator(client=client, config=config or WorldGenerationConfig()) | |
| return generator.generate_json( | |
| prompt, | |
| constraints=constraints, | |
| examples=examples, | |
| model_kwargs=model_kwargs, | |
| ) | |
| def generate_world_result( | |
| prompt: str, | |
| *, | |
| client: Any | None = None, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| config: WorldGenerationConfig | None = None, | |
| examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None, | |
| model_kwargs: Mapping[str, Any] | None = None, | |
| ) -> WorldGenerationResult: | |
| """Convenience function that returns the full generation result.""" | |
| generator = WorldGenerator(client=client, config=config or WorldGenerationConfig()) | |
| return generator.generate( | |
| prompt, | |
| constraints=constraints, | |
| examples=examples, | |
| model_kwargs=model_kwargs, | |
| ) | |
| def build_world_generation_prompt( | |
| prompt: str, | |
| *, | |
| constraints: Mapping[str, Any] | str | None = None, | |
| config: WorldGenerationConfig | None = None, | |
| ) -> PromptBundle: | |
| """Build the prompt bundle without calling a model.""" | |
| generator = WorldGenerator(client=None, config=config or WorldGenerationConfig()) | |
| return generator.build_prompt(prompt, constraints=constraints) | |
| def normalize_model_response(response: Any) -> str: | |
| """Normalize common model-client response shapes into text. | |
| Supported shapes: | |
| - plain string | |
| - bytes | |
| - mapping with text/content/generated_text/output | |
| - OpenAI-like mapping with choices[0].message.content | |
| - object with text/content/generated_text attributes | |
| """ | |
| if response is None: | |
| raise ModelClientError("Model client returned None") | |
| if isinstance(response, str): | |
| return response.strip() | |
| if isinstance(response, bytes): | |
| return response.decode("utf-8").strip() | |
| if isinstance(response, Mapping): | |
| direct = _first_present( | |
| response, | |
| ("text", "content", "generated_text", "output", "response"), | |
| ) | |
| if direct is not None: | |
| return normalize_model_response(direct) | |
| choices = response.get("choices") | |
| if isinstance(choices, Sequence) and choices: | |
| first_choice = choices[0] | |
| if isinstance(first_choice, Mapping): | |
| message = first_choice.get("message") | |
| if isinstance(message, Mapping) and message.get("content") is not None: | |
| return normalize_model_response(message["content"]) | |
| if first_choice.get("text") is not None: | |
| return normalize_model_response(first_choice["text"]) | |
| if response.get("data") is not None: | |
| return normalize_model_response(response["data"]) | |
| for attribute in ("text", "content", "generated_text", "output", "response"): | |
| value = getattr(response, attribute, None) | |
| if value is not None: | |
| return normalize_model_response(value) | |
| raise ModelClientError( | |
| f"Could not extract text from model response of type {response.__class__.__name__}" | |
| ) | |
| def world_spec_schema_excerpt() -> str: | |
| """Return a compact schema guide suitable for small-model prompts.""" | |
| excerpt = { | |
| "WorldSpec": { | |
| "required": ["id", "name", "agents", "resources", "events"], | |
| "fields": { | |
| "schema_version": "string", | |
| "id": "string", | |
| "name": "string", | |
| "description": "string|null", | |
| "simulation": "SimulationSpec", | |
| "space": "SpaceSpec|null", | |
| "agents": "AgentSpec[]", | |
| "resources": "ResourceSpec[]", | |
| "events": "EventSpec[]", | |
| "metrics": "MetricSpec[]", | |
| "metadata": "object", | |
| }, | |
| }, | |
| "AgentSpec": { | |
| "fields": { | |
| "id": "string", | |
| "type": "string", | |
| "position": "number[]|null", | |
| "state": "object", | |
| "memory": "object", | |
| "goals": "JSON", | |
| "behaviors": "BehaviorSpec[]", | |
| "policy": "PolicySpec|null", | |
| "alive": "boolean", | |
| "metadata": "object", | |
| } | |
| }, | |
| "BehaviorSpec": {"fields": {"name": "string", "params": "object"}}, | |
| "PolicySpec": {"fields": {"type": "string", "params": "object"}}, | |
| "ResourceSpec": { | |
| "fields": { | |
| "id": "string", | |
| "type": "string", | |
| "amount": "number", | |
| "position": "number[]|null", | |
| "regeneration_rate": "number", | |
| "max_amount": "number|null", | |
| "metadata": "object", | |
| } | |
| }, | |
| "EventSpec": { | |
| "fields": { | |
| "id": "string|null", | |
| "name": "string", | |
| "trigger_step": "integer", | |
| "payload": "object", | |
| "enabled": "boolean", | |
| } | |
| }, | |
| } | |
| return json.dumps(excerpt, indent=2) | |
| def extract_json_object_text(text: str) -> str: | |
| """Extract the first balanced JSON object from text. | |
| This is exposed as a utility for app-level debugging. The parser already | |
| uses equivalent extraction internally. | |
| """ | |
| parser = WorldDSLParser() | |
| return parser.extract_json_text(text) | |
| def _call_with_supported_kwargs(callable_obj: Callable[..., Any], kwargs: Mapping[str, Any]) -> Any: | |
| """Call a function using only kwargs supported by its signature. | |
| If the callable accepts ``**kwargs``, all supplied kwargs are passed. | |
| Otherwise unsupported kwargs are filtered out. If no matching keyword is | |
| accepted but a positional prompt appears possible, the flattened prompt is | |
| passed positionally. | |
| """ | |
| try: | |
| signature = inspect.signature(callable_obj) | |
| except (TypeError, ValueError): | |
| return callable_obj(**dict(kwargs)) | |
| parameters = signature.parameters | |
| accepts_var_keyword = any( | |
| parameter.kind is inspect.Parameter.VAR_KEYWORD | |
| for parameter in parameters.values() | |
| ) | |
| if accepts_var_keyword: | |
| return callable_obj(**dict(kwargs)) | |
| accepted = { | |
| key: value | |
| for key, value in kwargs.items() | |
| if key in parameters | |
| } | |
| if accepted: | |
| return callable_obj(**accepted) | |
| positional_params = [ | |
| parameter | |
| for parameter in parameters.values() | |
| if parameter.kind | |
| in { | |
| inspect.Parameter.POSITIONAL_ONLY, | |
| inspect.Parameter.POSITIONAL_OR_KEYWORD, | |
| } | |
| ] | |
| if positional_params and "prompt" in kwargs: | |
| return callable_obj(kwargs["prompt"]) | |
| return callable_obj() | |
| def _first_present(mapping: Mapping[str, Any], keys: Sequence[str]) -> Any: | |
| """Return first present non-null mapping value.""" | |
| for key in keys: | |
| if key in mapping and mapping[key] is not None: | |
| return mapping[key] | |
| return None | |
| def _serialize_constraints(constraints: Mapping[str, Any] | str) -> str: | |
| """Serialize UI constraints for prompt inclusion.""" | |
| if isinstance(constraints, str): | |
| return constraints.strip() | |
| return json.dumps(constraints, indent=2, sort_keys=True) | |
| def _require_prompt(prompt: str) -> str: | |
| """Validate and normalize a user prompt.""" | |
| normalized = str(prompt).strip() | |
| if not normalized: | |
| raise WorldGenerationError("World generation prompt must not be empty") | |
| return normalized | |
| def _slugify(value: str) -> str: | |
| """Return a stable lowercase identifier.""" | |
| lowered = value.lower().strip() | |
| slug = re.sub(r"[^a-z0-9]+", "_", lowered) | |
| slug = re.sub(r"_+", "_", slug).strip("_") | |
| return slug or "world" | |
| def _title_from_domain(domain: str) -> str: | |
| """Return a human-readable fallback world title.""" | |
| words = domain.replace("_", " ").split() | |
| return " ".join(word.capitalize() for word in words) + " World" | |
| __all__ = [ | |
| "DEFAULT_MINIMAL_EXAMPLE", | |
| "DEFAULT_SYSTEM_PROMPT", | |
| "WORLD_DSL_FORMAT_GUIDE", | |
| "DeterministicWorldSpecBuilder", | |
| "GenerationMode", | |
| "ModelClientError", | |
| "PromptBundle", | |
| "ResponseFormat", | |
| "SupportsGenerate", | |
| "WorldGenerationConfig", | |
| "WorldGenerationError", | |
| "WorldGenerationResult", | |
| "WorldGenerationValidationError", | |
| "WorldGenerator", | |
| "build_world_generation_prompt", | |
| "extract_json_object_text", | |
| "generate_world_json", | |
| "generate_world_result", | |
| "generate_world_spec", | |
| "normalize_model_response", | |
| "world_spec_schema_excerpt", | |
| ] |