Spaces:
Runtime error
Runtime error
| """ | |
| 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__) | |
| 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 tries both chat-completion and text-generation APIs and raises | |
| detailed errors instead of hiding provider/model failures. | |
| """ | |
| def __init__( | |
| self, | |
| model_id: str, | |
| token: str | None = None, | |
| provider: str | None = None, | |
| ) -> None: | |
| """Initialize the optional Hugging Face inference adapter.""" | |
| from huggingface_hub import InferenceClient | |
| self.model_id = model_id.strip().strip('"').strip("'").strip() | |
| self.provider = None if provider is None or not str(provider).strip() else str(provider).strip() | |
| api_key = ( | |
| token | |
| or os.getenv("HF_TOKEN") | |
| or os.getenv("HF_HUB_TOKEN") | |
| or os.getenv("HUGGINGFACEHUB_API_TOKEN") | |
| ) | |
| if not api_key: | |
| raise RuntimeError( | |
| "HF_TOKEN is required for Hugging Face Inference Providers. " | |
| "Create a Hugging Face token with Inference Providers permission " | |
| "and add it as a Space secret named HF_TOKEN." | |
| ) | |
| if self.provider: | |
| self.client = InferenceClient( | |
| model=self.model_id, | |
| provider=self.provider, | |
| api_key=api_key, | |
| ) | |
| else: | |
| self.client = InferenceClient( | |
| model=self.model_id, | |
| api_key=api_key, | |
| ) | |
| 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 Hugging Face chat or text-generation APIs.""" | |
| errors: list[str] = [] | |
| if messages: | |
| text = self._try_chat_completion( | |
| messages, | |
| max_new_tokens=max_new_tokens, | |
| temperature=temperature, | |
| errors=errors, | |
| ) | |
| if text: | |
| return text | |
| text = self._try_openai_compatible_chat( | |
| messages, | |
| max_new_tokens=max_new_tokens, | |
| temperature=temperature, | |
| errors=errors, | |
| ) | |
| if text: | |
| return text | |
| if prompt: | |
| text = self._try_text_generation( | |
| prompt, | |
| max_new_tokens=max_new_tokens, | |
| temperature=temperature, | |
| errors=errors, | |
| ) | |
| if text: | |
| return text | |
| error_text = "\n".join(errors) if errors else "No inference method was attempted." | |
| raise RuntimeError( | |
| "InferenceClient did not return generated text.\n\n" | |
| f"Model id: {self.model_id}\n\n" | |
| f"Attempted methods and errors:\n{error_text}" | |
| ) | |
| def _try_chat_completion( | |
| self, | |
| messages: list[dict[str, str]], | |
| *, | |
| max_new_tokens: int, | |
| temperature: float, | |
| errors: list[str], | |
| ) -> str | None: | |
| """Try InferenceClient.chat_completion().""" | |
| chat_completion = getattr(self.client, "chat_completion", None) | |
| if not callable(chat_completion): | |
| errors.append("chat_completion: method not available on InferenceClient") | |
| return None | |
| try: | |
| response = chat_completion( | |
| messages=messages, | |
| max_tokens=max_new_tokens, | |
| temperature=temperature, | |
| ) | |
| text = _extract_model_text(response) | |
| if text: | |
| return text | |
| errors.append( | |
| "chat_completion: response had no extractable text. " | |
| f"response_type={response.__class__.__name__}, response={_short_repr(response)}" | |
| ) | |
| return None | |
| except Exception as exc: | |
| errors.append(f"chat_completion: {exc.__class__.__name__}: {exc}") | |
| return None | |
| def _try_openai_compatible_chat( | |
| self, | |
| messages: list[dict[str, str]], | |
| *, | |
| max_new_tokens: int, | |
| temperature: float, | |
| errors: list[str], | |
| ) -> str | None: | |
| """Try InferenceClient.chat.completions.create().""" | |
| chat = getattr(self.client, "chat", None) | |
| completions = getattr(chat, "completions", None) | |
| create = getattr(completions, "create", None) | |
| if not callable(create): | |
| errors.append("chat.completions.create: method not available on InferenceClient") | |
| return None | |
| call_variants = ( | |
| { | |
| "model": self.model_id, | |
| "messages": messages, | |
| "max_tokens": max_new_tokens, | |
| "temperature": temperature, | |
| }, | |
| { | |
| "messages": messages, | |
| "max_tokens": max_new_tokens, | |
| "temperature": temperature, | |
| }, | |
| ) | |
| for kwargs in call_variants: | |
| try: | |
| response = create(**kwargs) | |
| text = _extract_model_text(response) | |
| if text: | |
| return text | |
| errors.append( | |
| "chat.completions.create: response had no extractable text. " | |
| f"kwargs_keys={list(kwargs.keys())}, " | |
| f"response_type={response.__class__.__name__}, " | |
| f"response={_short_repr(response)}" | |
| ) | |
| except Exception as exc: | |
| errors.append( | |
| "chat.completions.create: " | |
| f"kwargs_keys={list(kwargs.keys())}, " | |
| f"{exc.__class__.__name__}: {exc}" | |
| ) | |
| return None | |
| def _try_text_generation( | |
| self, | |
| prompt: str, | |
| *, | |
| max_new_tokens: int, | |
| temperature: float, | |
| errors: list[str], | |
| ) -> str | None: | |
| """Try InferenceClient.text_generation().""" | |
| text_generation = getattr(self.client, "text_generation", None) | |
| if not callable(text_generation): | |
| errors.append("text_generation: method not available on InferenceClient") | |
| return None | |
| call_variants = ( | |
| { | |
| "prompt": prompt, | |
| "max_new_tokens": max_new_tokens, | |
| "temperature": temperature, | |
| "return_full_text": False, | |
| }, | |
| { | |
| "prompt": prompt, | |
| "max_new_tokens": max_new_tokens, | |
| "return_full_text": False, | |
| }, | |
| { | |
| "prompt": prompt, | |
| "max_new_tokens": max_new_tokens, | |
| }, | |
| ) | |
| for kwargs in call_variants: | |
| try: | |
| response = text_generation(**kwargs) | |
| text = _extract_model_text(response) | |
| if text: | |
| return text | |
| errors.append( | |
| "text_generation: response had no extractable text. " | |
| f"kwargs_keys={list(kwargs.keys())}, " | |
| f"response_type={response.__class__.__name__}, " | |
| f"response={_short_repr(response)}" | |
| ) | |
| except Exception as exc: | |
| errors.append( | |
| "text_generation: " | |
| f"kwargs_keys={list(kwargs.keys())}, " | |
| f"{exc.__class__.__name__}: {exc}" | |
| ) | |
| return None | |
| def build_optional_model_client() -> tuple[Any | None, AppModelStatus]: | |
| """Build the optional model client from environment variables.""" | |
| raw_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("HF_HUB_TOKEN") | |
| or os.getenv("HUGGINGFACEHUB_API_TOKEN") | |
| ) | |
| provider = ( | |
| os.getenv("WORLDSMITHAI_PROVIDER") | |
| or os.getenv("HF_INFERENCE_PROVIDER") | |
| or None | |
| ) | |
| if not raw_model_id: | |
| return None, AppModelStatus( | |
| enabled=False, | |
| model_id=None, | |
| message="Set WORLDSMITHAI_MODEL_ID to enable model-generated DSL.", | |
| ) | |
| model_id = raw_model_id.strip().strip('"').strip("'").strip() | |
| logger.info( | |
| "WorldSmithAI model configuration: raw_model_id=%r sanitized_model_id=%r", | |
| raw_model_id, | |
| model_id, | |
| ) | |
| if not model_id: | |
| return None, AppModelStatus( | |
| enabled=False, | |
| model_id=None, | |
| message="WORLDSMITHAI_MODEL_ID was set but empty after sanitization.", | |
| ) | |
| try: | |
| client = OptionalHFInferenceClient( | |
| model_id=model_id, | |
| token=token, | |
| provider=provider, | |
| ) | |
| 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 model_health_check_ui() -> str: | |
| """Return a quick model-client health check for the Gradio UI.""" | |
| if MODEL_CLIENT is None: | |
| return ( | |
| "Model client is NOT configured.\n\n" | |
| f"Runtime status: {MODEL_STATUS.message}\n\n" | |
| "Check that WORLDSMITHAI_MODEL_ID is set in Space settings." | |
| ) | |
| provider_label = ( | |
| getattr(MODEL_CLIENT, "provider", None) | |
| or os.getenv("WORLDSMITHAI_PROVIDER") | |
| or os.getenv("HF_INFERENCE_PROVIDER") | |
| or "auto" | |
| ) | |
| try: | |
| response = MODEL_CLIENT.generate( | |
| prompt='Return exactly this JSON object and nothing else: {"ok": true}', | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": "You return compact JSON only.", | |
| }, | |
| { | |
| "role": "user", | |
| "content": 'Return exactly this JSON object and nothing else: {"ok": true}', | |
| }, | |
| ], | |
| max_new_tokens=64, | |
| temperature=0.0, | |
| ) | |
| return ( | |
| "Model client is configured and returned text.\n\n" | |
| f"Model id: {MODEL_STATUS.model_id}\n\n" | |
| f"Provider: {provider_label}\n\n" | |
| f"Response:\n{response}" | |
| ) | |
| except Exception as exc: | |
| logger.exception("Model health check failed") | |
| return ( | |
| "Model client is configured but inference failed.\n\n" | |
| f"Model id: {MODEL_STATUS.model_id}\n\n" | |
| f"Provider: {provider_label}\n\n" | |
| f"Error type: {exc.__class__.__name__}\n" | |
| f"Error:\n{exc}" | |
| ) | |
| 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""" | |
| <div id="worldsmith-header"> | |
| # {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. | |
| <span class="worldsmith-small"> | |
| 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. | |
| </span> | |
| </div> | |
| """ | |
| ) | |
| 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." | |
| ) | |
| model_health_button = gr.Button("Check model connection") | |
| model_health_output = gr.Textbox( | |
| label="Model health check", | |
| lines=8, | |
| interactive=False, | |
| ) | |
| model_health_button.click( | |
| fn=model_health_check_ui, | |
| inputs=[], | |
| outputs=[model_health_output], | |
| ) | |
| 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 _short_repr(value: Any, *, max_chars: int = 1200) -> str: | |
| """Return a bounded repr for diagnostics.""" | |
| text = repr(value) | |
| if len(text) <= max_chars: | |
| return text | |
| return text[: max_chars - 3] + "..." | |
| 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() |