""" Root-level Gradio app for WorldSmithAI. This file is intended to live directly beside callbacks.py in the repository root. It does not assume an app/ package or folder. The UI is intentionally thin: - app.py defines the Gradio interface. - callbacks.py runs generation, parsing, validation, world construction, simulation, visualization, metrics, and narration. - The simulation engine remains generic and DSL-driven. Environment variables: WORLDSMITHAI_MODEL_ID: Optional Hugging Face model id for model-generated DSL. HF_TOKEN: Optional Hugging Face token for private or gated models. WORLDSMITHAI_OUTPUT_DIR: Optional directory for generated GIFs, MP4s, charts, and images. WORLDSMITHAI_DEFAULT_STEPS: Optional default simulation step count. WORLDSMITHAI_MAX_FRAMES: Optional maximum animation frame count. WORLDSMITHAI_LOG_LEVEL: Optional logging level. Defaults to INFO. """ from __future__ import annotations import json import logging import os import traceback from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path from typing import Any import gradio as gr from callbacks import ( CallbackConfig, CallbackResult, apply_step_override, callback_failure_result, generate_world_from_prompt, load_example_callback, parse_constraints, run_worldsmith_pipeline, simulate_dsl_pipeline, validate_dsl_callback, ) from dsl.validator import validate_world_spec APP_TITLE = "WorldSmithAI" APP_SUBTITLE = "Production-Level Agent-Based World Simulation Framework" APP_VERSION = "0.1.0" DEFAULT_PROMPT = ( "Create a compact research ecosystem where scientists, engineers, reviewers, " "and funding agents exchange knowledge, compete for attention, collaborate, " "and adapt their goals over time." ) PROMPT_EXAMPLES = [ [ "A medieval civilization with merchants, rulers, artisans, scholars, guards, " "taxes, trade, regulations, and resource pressure." ], [ "A startup economy with founders, investors, customers, operators, competitors, " "market bidding, adoption dynamics, and shifting goals." ], [ "A fantasy world with mages, dragons, healers, merchants, mana resources, " "alliances, communication, negotiation, and magical infrastructure." ], [ "A transport network with hubs, carriers, dispatchers, chargers, passengers, " "queues, routes, deliveries, and congestion." ], [ "A power grid world with generators, storage nodes, consumers, regulators, " "energy resources, subsidies, enforcement, and rebalancing." ], [ "A space colony with colonists, engineers, botanists, navigators, miners, " "oxygen resources, habitat expansion, and survival goals." ], ] CUSTOM_CSS = """ #worldsmith-header { padding: 1.1rem 1.25rem; border-radius: 1rem; background: linear-gradient(135deg, rgba(80, 80, 120, 0.12), rgba(120, 120, 160, 0.08)); border: 1px solid rgba(120, 120, 160, 0.18); } .worldsmith-small { font-size: 0.92rem; opacity: 0.82; } .worldsmith-status textarea { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } """ logging.basicConfig( level=os.getenv("WORLDSMITHAI_LOG_LEVEL", "INFO").upper(), format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", ) logger = logging.getLogger(__name__) @dataclass(frozen=True) class AppModelStatus: """Status of the optional model client used by the UI.""" enabled: bool model_id: str | None message: str def to_markdown(self) -> str: """Return display Markdown for model status.""" if self.enabled: return f"**Model client:** configured with `{self.model_id}`." return f"**Model client:** not configured. {self.message}" class OptionalHFInferenceClient: """Small adapter around huggingface_hub.InferenceClient. This adapter is optional. If a model id is not configured, the app still works through WorldSmithAI's deterministic fallback generator. """ def __init__(self, model_id: str, token: str | None = None) -> None: """Initialize the optional Hugging Face inference adapter.""" from huggingface_hub import InferenceClient self.model_id = model_id self.client = InferenceClient(model=model_id, token=token) def generate( self, prompt: str | None = None, messages: list[dict[str, str]] | None = None, max_new_tokens: int = 2048, temperature: float = 0.2, **_: Any, ) -> str: """Generate text using common InferenceClient methods.""" if messages: chat_text = self._try_chat_completion( messages, max_new_tokens=max_new_tokens, temperature=temperature, ) if chat_text: return chat_text if prompt: text_generation = self._try_text_generation( prompt, max_new_tokens=max_new_tokens, temperature=temperature, ) if text_generation: return text_generation raise RuntimeError("InferenceClient did not return generated text") def _try_chat_completion( self, messages: list[dict[str, str]], *, max_new_tokens: int, temperature: float, ) -> str | None: """Try chat-style generation methods.""" try: chat_completion = getattr(self.client, "chat_completion", None) if callable(chat_completion): response = chat_completion( messages=messages, max_tokens=max_new_tokens, temperature=temperature, ) return _extract_model_text(response) except Exception: logger.debug("InferenceClient.chat_completion failed", exc_info=True) try: chat = getattr(self.client, "chat", None) completions = getattr(chat, "completions", None) create = getattr(completions, "create", None) if callable(create): response = create( messages=messages, max_tokens=max_new_tokens, temperature=temperature, ) return _extract_model_text(response) except Exception: logger.debug("InferenceClient.chat.completions.create failed", exc_info=True) return None def _try_text_generation( self, prompt: str, *, max_new_tokens: int, temperature: float, ) -> str | None: """Try text-generation style methods.""" try: text_generation = getattr(self.client, "text_generation", None) if callable(text_generation): response = text_generation( prompt, max_new_tokens=max_new_tokens, temperature=temperature, return_full_text=False, ) return _extract_model_text(response) except Exception: logger.debug("InferenceClient.text_generation failed", exc_info=True) return None def build_optional_model_client() -> tuple[Any | None, AppModelStatus]: """Build the optional model client from environment variables.""" model_id = ( os.getenv("WORLDSMITHAI_MODEL_ID") or os.getenv("WORLD_SMITH_MODEL_ID") or os.getenv("HF_MODEL_ID") ) token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN") if not model_id: return None, AppModelStatus( enabled=False, model_id=None, message="Set WORLDSMITHAI_MODEL_ID to enable model-generated DSL.", ) try: client = OptionalHFInferenceClient(model_id=model_id, token=token) return client, AppModelStatus( enabled=True, model_id=model_id, message="Model client configured.", ) except Exception as exc: logger.warning("Optional model client could not be configured: %s", exc) return None, AppModelStatus( enabled=False, model_id=model_id, message=( f"WORLDSMITHAI_MODEL_ID={model_id} was found, but the client " f"could not be initialized: {exc}. The app will use deterministic fallback." ), ) MODEL_CLIENT, MODEL_STATUS = build_optional_model_client() def app_callback_config( *, steps: int | float | str | None, animation_format: str, ) -> CallbackConfig: """Create callback configuration from UI values and environment defaults.""" default_steps = _env_int("WORLDSMITHAI_DEFAULT_STEPS", 60) max_frames = _env_int("WORLDSMITHAI_MAX_FRAMES", 80) output_dir = os.getenv("WORLDSMITHAI_OUTPUT_DIR") or None return CallbackConfig( output_dir=output_dir, steps=_coerce_optional_int(steps) if steps is not None else default_steps, max_animation_frames=max(1, max_frames), animation_format=animation_format, ) def run_from_prompt_ui( prompt: str, steps: int | float, constraints: str, animation_format: str, use_configured_model: bool, ) -> tuple[Any, ...]: """Run the full prompt-to-simulation pipeline for the main UI tab.""" client = MODEL_CLIENT if use_configured_model else None config = app_callback_config(steps=steps, animation_format=animation_format) try: result = run_worldsmith_pipeline( prompt=prompt, constraints=parse_constraints(constraints), client=client, config=config, ) return result_to_ui_tuple(result) except Exception as exc: logger.exception("Prompt simulation failed") return failure_to_ui_tuple(exc) def generate_dsl_only_ui( prompt: str, constraints: str, use_configured_model: bool, steps: int | float, ) -> tuple[str, str, str]: """Generate DSL only, without running simulation.""" client = MODEL_CLIENT if use_configured_model else None config = app_callback_config(steps=steps, animation_format="gif") try: generation = generate_world_from_prompt( prompt=prompt, constraints=parse_constraints(constraints), client=client, config=config, ) spec = apply_step_override(generation.spec, _coerce_optional_int(steps)) report = validate_world_spec( spec, config=config.resolved_validation_config(), ) status = ( f"Generated `{spec.id}` with {len(spec.agents)} agent(s), " f"{len(spec.resources)} resource(s), and " f"{len(spec.behavior_names)} behavior reference(s). " f"Validation: {len(report.errors)} error(s), {len(report.warnings)} warning(s)." ) return ( spec.to_json_string(indent=2, exclude_none=True), _safe_json_dumps(report.to_dict()), status, ) except Exception as exc: logger.exception("DSL generation failed") return "", "{}", f"DSL generation failed: {exc}" def simulate_dsl_ui( dsl_json: str, steps: int | float, animation_format: str, use_configured_model_for_narration: bool, ) -> tuple[Any, ...]: """Run simulation from user-supplied DSL JSON.""" client = MODEL_CLIENT if use_configured_model_for_narration else None config = app_callback_config(steps=steps, animation_format=animation_format) try: result = simulate_dsl_pipeline( dsl_json=dsl_json, client=client, config=config, ) return result_to_ui_tuple(result) except Exception as exc: logger.exception("DSL simulation failed") return failure_to_ui_tuple(exc, world_spec_json=dsl_json) def validate_dsl_ui(dsl_json: str) -> tuple[str, str]: """Validate DSL JSON in the validation tab.""" return validate_dsl_callback(dsl_json) def load_example_ui(example_name: str | None) -> tuple[str, str]: """Load an example JSON file into the DSL editor.""" if not example_name: return "", "Choose an example first." examples = discover_examples() path = examples.get(example_name) if path is None: return "", f"Unknown example: {example_name}" return load_example_callback(path) def clear_main_outputs() -> tuple[Any, ...]: """Clear main-tab outputs.""" return ( "", "", gr.update(value=None, visible=False), gr.update(value=None, visible=False), None, None, None, "", "", "", ) def result_to_ui_tuple(result: CallbackResult) -> tuple[Any, ...]: """Map CallbackResult to the app output component order.""" animation_image, animation_video = animation_path_updates(result.animation_path) return ( result.world_spec_json, result.validation_json, animation_image, animation_video, result.population_chart_path, result.resource_chart_path, result.final_image_path, _markdown_narrative(result.narrative), result.metrics_json, result.status_message, ) def failure_to_ui_tuple( error: BaseException, *, world_spec_json: str = "", ) -> tuple[Any, ...]: """Map an exception to a safe UI output tuple.""" result = callback_failure_result(error, world_spec_json=world_spec_json) details = { "success": False, "error_type": error.__class__.__name__, "error": str(error), "traceback": traceback.format_exc(), "callback_result": result.to_dict(), } return ( result.world_spec_json, result.validation_json, gr.update(value=None, visible=False), gr.update(value=None, visible=False), None, None, None, _markdown_narrative(result.narrative), _safe_json_dumps(details), result.status_message, ) def animation_path_updates(path: str | None) -> tuple[Any, Any]: """Return Gradio updates for GIF/image and MP4/video animation components.""" if not path: return gr.update(value=None, visible=False), gr.update(value=None, visible=False) suffix = Path(path).suffix.lower() if suffix in {".mp4", ".webm", ".mov", ".m4v"}: return gr.update(value=None, visible=False), gr.update(value=path, visible=True) return gr.update(value=path, visible=True), gr.update(value=None, visible=False) def discover_examples() -> dict[str, Path]: """Discover JSON example files in the root-level examples directory.""" examples_dir = Path(__file__).resolve().parent / "examples" if not examples_dir.exists(): return {} return { path.stem.replace("_", " ").title(): path for path in sorted(examples_dir.glob("*.json")) } def build_demo() -> gr.Blocks: """Build and return the Gradio Blocks demo.""" examples = discover_examples() example_names = list(examples.keys()) default_steps = _env_int("WORLDSMITHAI_DEFAULT_STEPS", 60) with gr.Blocks( title=f"{APP_TITLE} — Agent-Based World Simulation", css=CUSTOM_CSS, analytics_enabled=False, ) as demo: gr.Markdown( f"""
# {APP_TITLE} ### {APP_SUBTITLE} WorldSmithAI converts natural language into a validated JSON DSL, builds a deterministic Python agent-based world, simulates emergent behavior, and returns animation, charts, metrics, and narration. No species or domains are hardcoded. A farmer, dragon, scientist, vehicle, startup, power node, or civilization actor is a generic Agent with state, memory, behaviors, and policy.
""" ) with gr.Accordion("Runtime status", open=False): gr.Markdown(MODEL_STATUS.to_markdown()) gr.Markdown( "If no model client is configured, the app still runs with the deterministic fallback DSL generator. " "Set WORLDSMITHAI_MODEL_ID in the Space environment to enable model-generated DSL." ) with gr.Tabs(): with gr.Tab("Generate + Simulate"): with gr.Row(): with gr.Column(scale=1): prompt = gr.Textbox( label="World prompt", value=DEFAULT_PROMPT, lines=8, placeholder=( "Describe any agent-based world: farm, civilization, research lab, " "startup economy, fantasy realm, transport system..." ), ) constraints = gr.Textbox( label="Optional constraints", value='{"max_agents": 6, "demo_size": "small"}', lines=4, placeholder='Example: {"max_agents": 6, "steps": 80}', ) with gr.Row(): steps = gr.Slider( minimum=1, maximum=250, value=default_steps, step=1, label="Simulation steps", ) animation_format = gr.Dropdown( choices=["gif", "mp4"], value="gif", label="Animation format", ) use_model = gr.Checkbox( label="Use configured model client when available", value=MODEL_CLIENT is not None, interactive=True, ) with gr.Row(): run_button = gr.Button("Generate world + run simulation", variant="primary") clear_button = gr.Button("Clear outputs") gr.Examples( examples=PROMPT_EXAMPLES, inputs=[prompt], label="Prompt examples", ) with gr.Column(scale=1): status = gr.Textbox( label="Status", lines=3, interactive=False, elem_classes=["worldsmith-status"], ) narrative = gr.Markdown(label="Narrative summary") with gr.Tab("Visual outputs"): with gr.Row(): animation_image = gr.Image( label="Simulation animation GIF", type="filepath", visible=True, ) animation_video = gr.Video( label="Simulation animation MP4", visible=False, ) with gr.Row(): final_image = gr.Image( label="Final world state", type="filepath", ) population_chart = gr.Image( label="Population chart", type="filepath", ) resource_chart = gr.Image( label="Resource chart", type="filepath", ) with gr.Tab("Generated DSL + diagnostics"): world_spec_json = gr.Code( label="Generated WorldSpec JSON", language="json", lines=24, ) validation_json = gr.Code( label="Validation report", language="json", lines=16, ) metrics_json = gr.Code( label="Metrics and run diagnostics", language="json", lines=24, ) main_outputs = [ world_spec_json, validation_json, animation_image, animation_video, population_chart, resource_chart, final_image, narrative, metrics_json, status, ] run_button.click( fn=run_from_prompt_ui, inputs=[prompt, steps, constraints, animation_format, use_model], outputs=main_outputs, show_progress=True, ) clear_button.click( fn=clear_main_outputs, inputs=[], outputs=main_outputs, ) with gr.Tab("Generate DSL only"): with gr.Row(): with gr.Column(scale=1): dsl_prompt = gr.Textbox( label="World prompt", value=DEFAULT_PROMPT, lines=8, ) dsl_constraints = gr.Textbox( label="Optional constraints", value='{"max_agents": 6}', lines=4, ) dsl_steps = gr.Slider( minimum=1, maximum=250, value=default_steps, step=1, label="Simulation steps to put in DSL", ) dsl_use_model = gr.Checkbox( label="Use configured model client when available", value=MODEL_CLIENT is not None, ) generate_dsl_button = gr.Button("Generate DSL", variant="primary") with gr.Column(scale=1): generated_dsl_status = gr.Textbox( label="Status", lines=3, interactive=False, ) generated_dsl_validation = gr.Code( label="Validation report", language="json", lines=16, ) generated_dsl = gr.Code( label="Generated WorldSpec JSON", language="json", lines=28, ) generate_dsl_button.click( fn=generate_dsl_only_ui, inputs=[dsl_prompt, dsl_constraints, dsl_use_model, dsl_steps], outputs=[generated_dsl, generated_dsl_validation, generated_dsl_status], show_progress=True, ) with gr.Tab("Simulate existing DSL"): with gr.Row(): with gr.Column(scale=1): example_dropdown = gr.Dropdown( choices=example_names, value=example_names[0] if example_names else None, label="Load example JSON", interactive=bool(example_names), ) load_example_button = gr.Button("Load selected example") example_status = gr.Textbox( label="Example status", lines=2, interactive=False, ) existing_dsl = gr.Code( label="WorldSpec JSON", language="json", lines=28, value="", ) with gr.Row(): existing_steps = gr.Slider( minimum=1, maximum=250, value=default_steps, step=1, label="Simulation steps override", ) existing_animation_format = gr.Dropdown( choices=["gif", "mp4"], value="gif", label="Animation format", ) existing_use_model = gr.Checkbox( label="Use configured model client for narration when available", value=False, ) simulate_dsl_button = gr.Button("Simulate DSL", variant="primary") with gr.Column(scale=1): existing_status = gr.Textbox( label="Status", lines=3, interactive=False, ) existing_narrative = gr.Markdown(label="Narrative summary") with gr.Tab("DSL simulation visuals"): with gr.Row(): existing_animation_image = gr.Image( label="Simulation animation GIF", type="filepath", visible=True, ) existing_animation_video = gr.Video( label="Simulation animation MP4", visible=False, ) with gr.Row(): existing_final_image = gr.Image( label="Final world state", type="filepath", ) existing_population_chart = gr.Image( label="Population chart", type="filepath", ) existing_resource_chart = gr.Image( label="Resource chart", type="filepath", ) with gr.Tab("DSL simulation diagnostics"): existing_validation = gr.Code( label="Validation report", language="json", lines=16, ) existing_metrics = gr.Code( label="Metrics and run diagnostics", language="json", lines=24, ) load_example_button.click( fn=load_example_ui, inputs=[example_dropdown], outputs=[existing_dsl, example_status], ) simulate_dsl_button.click( fn=simulate_dsl_ui, inputs=[ existing_dsl, existing_steps, existing_animation_format, existing_use_model, ], outputs=[ existing_dsl, existing_validation, existing_animation_image, existing_animation_video, existing_population_chart, existing_resource_chart, existing_final_image, existing_narrative, existing_metrics, existing_status, ], show_progress=True, ) with gr.Tab("Validate DSL"): validation_input = gr.Code( label="WorldSpec JSON to validate", language="json", lines=28, ) validate_button = gr.Button("Validate DSL", variant="primary") validation_output = gr.Code( label="Validation report", language="json", lines=24, ) validation_status = gr.Textbox( label="Status", lines=3, interactive=False, ) validate_button.click( fn=validate_dsl_ui, inputs=[validation_input], outputs=[validation_output, validation_status], ) with gr.Tab("About"): gr.Markdown( """ ## WorldSmithAI architecture WorldSmithAI follows this pipeline: Natural language prompt → SLM-generated JSON DSL → Pydantic schema validation → Semantic DSL validation → WorldFactory → World / Scheduler / Agents / Policies / Behaviors → Metrics / Visualization / Narration → Gradio UI ### Important design constraint The simulation engine is deterministic Python. The model generates JSON only, never Python code. ### UI outputs - Animation: GIF by default; MP4 if selected and ffmpeg is available. - Population chart: grouped by agent type. - Resource chart: grouped by resource type and weighted by amount. - Narrative: deterministic by default, optionally model-assisted if a client is configured. - Diagnostics: includes DSL validation, factory build report, metric history, artifact paths, and generation mode. ### Suggested root-level Space layout app.py callbacks.py requirements.txt core/ behaviors/ policies/ dsl/ factory/ metrics/ visualization/ llm/ examples/ """ ) return demo def _extract_model_text(response: Any) -> str | None: """Extract text from common model response shapes.""" if response is None: return None if isinstance(response, str): return response.strip() if isinstance(response, bytes): return response.decode("utf-8").strip() if isinstance(response, Mapping): for key in ("text", "content", "generated_text", "output", "response"): value = response.get(key) if value is not None: return _extract_model_text(value) choices = response.get("choices") if isinstance(choices, list) and choices: return _extract_model_text(choices[0]) message = response.get("message") if isinstance(message, Mapping): return _extract_model_text(message.get("content")) for attr in ("text", "content", "generated_text", "output", "response"): value = getattr(response, attr, None) if value is not None: return _extract_model_text(value) choices = getattr(response, "choices", None) if choices: return _extract_model_text(choices[0]) message = getattr(response, "message", None) if message is not None: return _extract_model_text(message) return None def _coerce_optional_int(value: int | float | str | None) -> int | None: """Coerce UI numeric values to optional int.""" if value is None or value == "": return None return int(float(value)) def _env_int(name: str, default: int) -> int: """Read an integer environment variable with a safe default.""" raw = os.getenv(name) if raw is None or raw == "": return default try: return int(float(raw)) except ValueError: logger.warning("Invalid integer environment variable %s=%r", name, raw) return default def _safe_json_dumps(value: Any) -> str: """Serialize values as pretty JSON for Gradio Code components.""" return json.dumps( _json_safe(value), indent=2, sort_keys=True, ensure_ascii=False, ) def _json_safe(value: Any) -> Any: """Return JSON-friendly representation of arbitrary values.""" if value is None or isinstance(value, (str, bool)): return value if isinstance(value, int) and not isinstance(value, bool): return value if isinstance(value, float): if value != value or value in {float("inf"), float("-inf")}: return None return value if isinstance(value, Mapping): return {str(key): _json_safe(nested) for key, nested in value.items()} if isinstance(value, (list, tuple)): return [_json_safe(item) for item in value] if hasattr(value, "to_dict") and callable(value.to_dict): return _json_safe(value.to_dict()) if hasattr(value, "model_dump") and callable(value.model_dump): return _json_safe(value.model_dump(mode="json")) return str(value) def _markdown_narrative(text: str) -> str: """Return narrative text formatted for Markdown display.""" cleaned = str(text or "").strip() if not cleaned: return "No narrative was generated." return cleaned demo = build_demo() app = demo if __name__ == "__main__": demo.queue().launch()