Srishti280992 commited on
Commit
a03e95c
Β·
verified Β·
1 Parent(s): f0aabd8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1029 -4
app.py CHANGED
@@ -1,7 +1,1032 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
1
+ """
2
+ Root-level Gradio app for WorldSmithAI.
3
+
4
+ This file is intended to live directly beside callbacks.py in the repository
5
+ root. It does not assume an app/ package or folder.
6
+
7
+ The UI is intentionally thin:
8
+ - app.py defines the Gradio interface.
9
+ - callbacks.py runs generation, parsing, validation, world construction,
10
+ simulation, visualization, metrics, and narration.
11
+ - The simulation engine remains generic and DSL-driven.
12
+
13
+ Environment variables:
14
+ WORLDSMITHAI_MODEL_ID:
15
+ Optional Hugging Face model id for model-generated DSL.
16
+
17
+ HF_TOKEN:
18
+ Optional Hugging Face token for private or gated models.
19
+
20
+ WORLDSMITHAI_OUTPUT_DIR:
21
+ Optional directory for generated GIFs, MP4s, charts, and images.
22
+
23
+ WORLDSMITHAI_DEFAULT_STEPS:
24
+ Optional default simulation step count.
25
+
26
+ WORLDSMITHAI_MAX_FRAMES:
27
+ Optional maximum animation frame count.
28
+
29
+ WORLDSMITHAI_LOG_LEVEL:
30
+ Optional logging level. Defaults to INFO.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import logging
37
+ import os
38
+ import traceback
39
+ from collections.abc import Mapping
40
+ from dataclasses import dataclass
41
+ from pathlib import Path
42
+ from typing import Any
43
+
44
  import gradio as gr
45
 
46
+ from callbacks import (
47
+ CallbackConfig,
48
+ CallbackResult,
49
+ apply_step_override,
50
+ callback_failure_result,
51
+ generate_world_from_prompt,
52
+ load_example_callback,
53
+ parse_constraints,
54
+ run_worldsmith_pipeline,
55
+ simulate_dsl_pipeline,
56
+ validate_dsl_callback,
57
+ )
58
+ from dsl.validator import validate_world_spec
59
+
60
+
61
+ APP_TITLE = "WorldSmithAI"
62
+ APP_SUBTITLE = "Production-Level Agent-Based World Simulation Framework"
63
+ APP_VERSION = "0.1.0"
64
+
65
+ DEFAULT_PROMPT = (
66
+ "Create a compact research ecosystem where scientists, engineers, reviewers, "
67
+ "and funding agents exchange knowledge, compete for attention, collaborate, "
68
+ "and adapt their goals over time."
69
+ )
70
+
71
+ PROMPT_EXAMPLES = [
72
+ [
73
+ "A medieval civilization with merchants, rulers, artisans, scholars, guards, "
74
+ "taxes, trade, regulations, and resource pressure."
75
+ ],
76
+ [
77
+ "A startup economy with founders, investors, customers, operators, competitors, "
78
+ "market bidding, adoption dynamics, and shifting goals."
79
+ ],
80
+ [
81
+ "A fantasy world with mages, dragons, healers, merchants, mana resources, "
82
+ "alliances, communication, negotiation, and magical infrastructure."
83
+ ],
84
+ [
85
+ "A transport network with hubs, carriers, dispatchers, chargers, passengers, "
86
+ "queues, routes, deliveries, and congestion."
87
+ ],
88
+ [
89
+ "A power grid world with generators, storage nodes, consumers, regulators, "
90
+ "energy resources, subsidies, enforcement, and rebalancing."
91
+ ],
92
+ [
93
+ "A space colony with colonists, engineers, botanists, navigators, miners, "
94
+ "oxygen resources, habitat expansion, and survival goals."
95
+ ],
96
+ ]
97
+
98
+ CUSTOM_CSS = """
99
+ #worldsmith-header {
100
+ padding: 1.1rem 1.25rem;
101
+ border-radius: 1rem;
102
+ background: linear-gradient(135deg, rgba(80, 80, 120, 0.12), rgba(120, 120, 160, 0.08));
103
+ border: 1px solid rgba(120, 120, 160, 0.18);
104
+ }
105
+ .worldsmith-small {
106
+ font-size: 0.92rem;
107
+ opacity: 0.82;
108
+ }
109
+ .worldsmith-status textarea {
110
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
111
+ }
112
+ """
113
+
114
+
115
+ logging.basicConfig(
116
+ level=os.getenv("WORLDSMITHAI_LOG_LEVEL", "INFO").upper(),
117
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
118
+ )
119
+
120
+ logger = logging.getLogger(__name__)
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class AppModelStatus:
125
+ """Status of the optional model client used by the UI."""
126
+
127
+ enabled: bool
128
+ model_id: str | None
129
+ message: str
130
+
131
+ def to_markdown(self) -> str:
132
+ """Return display Markdown for model status."""
133
+
134
+ if self.enabled:
135
+ return f"**Model client:** configured with `{self.model_id}`."
136
+
137
+ return f"**Model client:** not configured. {self.message}"
138
+
139
+
140
+ class OptionalHFInferenceClient:
141
+ """Small adapter around huggingface_hub.InferenceClient.
142
+
143
+ This adapter is optional. If a model id is not configured, the app still
144
+ works through WorldSmithAI's deterministic fallback generator.
145
+ """
146
+
147
+ def __init__(self, model_id: str, token: str | None = None) -> None:
148
+ """Initialize the optional Hugging Face inference adapter."""
149
+
150
+ from huggingface_hub import InferenceClient
151
+
152
+ self.model_id = model_id
153
+ self.client = InferenceClient(model=model_id, token=token)
154
+
155
+ def generate(
156
+ self,
157
+ prompt: str | None = None,
158
+ messages: list[dict[str, str]] | None = None,
159
+ max_new_tokens: int = 2048,
160
+ temperature: float = 0.2,
161
+ **_: Any,
162
+ ) -> str:
163
+ """Generate text using common InferenceClient methods."""
164
+
165
+ if messages:
166
+ chat_text = self._try_chat_completion(
167
+ messages,
168
+ max_new_tokens=max_new_tokens,
169
+ temperature=temperature,
170
+ )
171
+ if chat_text:
172
+ return chat_text
173
+
174
+ if prompt:
175
+ text_generation = self._try_text_generation(
176
+ prompt,
177
+ max_new_tokens=max_new_tokens,
178
+ temperature=temperature,
179
+ )
180
+ if text_generation:
181
+ return text_generation
182
+
183
+ raise RuntimeError("InferenceClient did not return generated text")
184
+
185
+ def _try_chat_completion(
186
+ self,
187
+ messages: list[dict[str, str]],
188
+ *,
189
+ max_new_tokens: int,
190
+ temperature: float,
191
+ ) -> str | None:
192
+ """Try chat-style generation methods."""
193
+
194
+ try:
195
+ chat_completion = getattr(self.client, "chat_completion", None)
196
+ if callable(chat_completion):
197
+ response = chat_completion(
198
+ messages=messages,
199
+ max_tokens=max_new_tokens,
200
+ temperature=temperature,
201
+ )
202
+ return _extract_model_text(response)
203
+ except Exception:
204
+ logger.debug("InferenceClient.chat_completion failed", exc_info=True)
205
+
206
+ try:
207
+ chat = getattr(self.client, "chat", None)
208
+ completions = getattr(chat, "completions", None)
209
+ create = getattr(completions, "create", None)
210
+ if callable(create):
211
+ response = create(
212
+ messages=messages,
213
+ max_tokens=max_new_tokens,
214
+ temperature=temperature,
215
+ )
216
+ return _extract_model_text(response)
217
+ except Exception:
218
+ logger.debug("InferenceClient.chat.completions.create failed", exc_info=True)
219
+
220
+ return None
221
+
222
+ def _try_text_generation(
223
+ self,
224
+ prompt: str,
225
+ *,
226
+ max_new_tokens: int,
227
+ temperature: float,
228
+ ) -> str | None:
229
+ """Try text-generation style methods."""
230
+
231
+ try:
232
+ text_generation = getattr(self.client, "text_generation", None)
233
+ if callable(text_generation):
234
+ response = text_generation(
235
+ prompt,
236
+ max_new_tokens=max_new_tokens,
237
+ temperature=temperature,
238
+ return_full_text=False,
239
+ )
240
+ return _extract_model_text(response)
241
+ except Exception:
242
+ logger.debug("InferenceClient.text_generation failed", exc_info=True)
243
+
244
+ return None
245
+
246
+
247
+ def build_optional_model_client() -> tuple[Any | None, AppModelStatus]:
248
+ """Build the optional model client from environment variables."""
249
+
250
+ model_id = (
251
+ os.getenv("WORLDSMITHAI_MODEL_ID")
252
+ or os.getenv("WORLD_SMITH_MODEL_ID")
253
+ or os.getenv("HF_MODEL_ID")
254
+ )
255
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
256
+
257
+ if not model_id:
258
+ return None, AppModelStatus(
259
+ enabled=False,
260
+ model_id=None,
261
+ message="Set WORLDSMITHAI_MODEL_ID to enable model-generated DSL.",
262
+ )
263
+
264
+ try:
265
+ client = OptionalHFInferenceClient(model_id=model_id, token=token)
266
+ return client, AppModelStatus(
267
+ enabled=True,
268
+ model_id=model_id,
269
+ message="Model client configured.",
270
+ )
271
+ except Exception as exc:
272
+ logger.warning("Optional model client could not be configured: %s", exc)
273
+ return None, AppModelStatus(
274
+ enabled=False,
275
+ model_id=model_id,
276
+ message=(
277
+ f"WORLDSMITHAI_MODEL_ID={model_id} was found, but the client "
278
+ f"could not be initialized: {exc}. The app will use deterministic fallback."
279
+ ),
280
+ )
281
+
282
+
283
+ MODEL_CLIENT, MODEL_STATUS = build_optional_model_client()
284
+
285
+
286
+ def app_callback_config(
287
+ *,
288
+ steps: int | float | str | None,
289
+ animation_format: str,
290
+ ) -> CallbackConfig:
291
+ """Create callback configuration from UI values and environment defaults."""
292
+
293
+ default_steps = _env_int("WORLDSMITHAI_DEFAULT_STEPS", 60)
294
+ max_frames = _env_int("WORLDSMITHAI_MAX_FRAMES", 80)
295
+ output_dir = os.getenv("WORLDSMITHAI_OUTPUT_DIR") or None
296
+
297
+ return CallbackConfig(
298
+ output_dir=output_dir,
299
+ steps=_coerce_optional_int(steps) if steps is not None else default_steps,
300
+ max_animation_frames=max(1, max_frames),
301
+ animation_format=animation_format,
302
+ )
303
+
304
+
305
+ def run_from_prompt_ui(
306
+ prompt: str,
307
+ steps: int | float,
308
+ constraints: str,
309
+ animation_format: str,
310
+ use_configured_model: bool,
311
+ ) -> tuple[Any, ...]:
312
+ """Run the full prompt-to-simulation pipeline for the main UI tab."""
313
+
314
+ client = MODEL_CLIENT if use_configured_model else None
315
+ config = app_callback_config(steps=steps, animation_format=animation_format)
316
+
317
+ try:
318
+ result = run_worldsmith_pipeline(
319
+ prompt=prompt,
320
+ constraints=parse_constraints(constraints),
321
+ client=client,
322
+ config=config,
323
+ )
324
+ return result_to_ui_tuple(result)
325
+ except Exception as exc:
326
+ logger.exception("Prompt simulation failed")
327
+ return failure_to_ui_tuple(exc)
328
+
329
+
330
+ def generate_dsl_only_ui(
331
+ prompt: str,
332
+ constraints: str,
333
+ use_configured_model: bool,
334
+ steps: int | float,
335
+ ) -> tuple[str, str, str]:
336
+ """Generate DSL only, without running simulation."""
337
+
338
+ client = MODEL_CLIENT if use_configured_model else None
339
+ config = app_callback_config(steps=steps, animation_format="gif")
340
+
341
+ try:
342
+ generation = generate_world_from_prompt(
343
+ prompt=prompt,
344
+ constraints=parse_constraints(constraints),
345
+ client=client,
346
+ config=config,
347
+ )
348
+ spec = apply_step_override(generation.spec, _coerce_optional_int(steps))
349
+ report = validate_world_spec(
350
+ spec,
351
+ config=config.resolved_validation_config(),
352
+ )
353
+ status = (
354
+ f"Generated `{spec.id}` with {len(spec.agents)} agent(s), "
355
+ f"{len(spec.resources)} resource(s), and "
356
+ f"{len(spec.behavior_names)} behavior reference(s). "
357
+ f"Validation: {len(report.errors)} error(s), {len(report.warnings)} warning(s)."
358
+ )
359
+ return (
360
+ spec.to_json_string(indent=2, exclude_none=True),
361
+ _safe_json_dumps(report.to_dict()),
362
+ status,
363
+ )
364
+ except Exception as exc:
365
+ logger.exception("DSL generation failed")
366
+ return "", "{}", f"DSL generation failed: {exc}"
367
+
368
+
369
+ def simulate_dsl_ui(
370
+ dsl_json: str,
371
+ steps: int | float,
372
+ animation_format: str,
373
+ use_configured_model_for_narration: bool,
374
+ ) -> tuple[Any, ...]:
375
+ """Run simulation from user-supplied DSL JSON."""
376
+
377
+ client = MODEL_CLIENT if use_configured_model_for_narration else None
378
+ config = app_callback_config(steps=steps, animation_format=animation_format)
379
+
380
+ try:
381
+ result = simulate_dsl_pipeline(
382
+ dsl_json=dsl_json,
383
+ client=client,
384
+ config=config,
385
+ )
386
+ return result_to_ui_tuple(result)
387
+ except Exception as exc:
388
+ logger.exception("DSL simulation failed")
389
+ return failure_to_ui_tuple(exc, world_spec_json=dsl_json)
390
+
391
+
392
+ def validate_dsl_ui(dsl_json: str) -> tuple[str, str]:
393
+ """Validate DSL JSON in the validation tab."""
394
+
395
+ return validate_dsl_callback(dsl_json)
396
+
397
+
398
+ def load_example_ui(example_name: str | None) -> tuple[str, str]:
399
+ """Load an example JSON file into the DSL editor."""
400
+
401
+ if not example_name:
402
+ return "", "Choose an example first."
403
+
404
+ examples = discover_examples()
405
+ path = examples.get(example_name)
406
+ if path is None:
407
+ return "", f"Unknown example: {example_name}"
408
+
409
+ return load_example_callback(path)
410
+
411
+
412
+ def clear_main_outputs() -> tuple[Any, ...]:
413
+ """Clear main-tab outputs."""
414
+
415
+ return (
416
+ "",
417
+ "",
418
+ gr.update(value=None, visible=False),
419
+ gr.update(value=None, visible=False),
420
+ None,
421
+ None,
422
+ None,
423
+ "",
424
+ "",
425
+ "",
426
+ )
427
+
428
+
429
+ def result_to_ui_tuple(result: CallbackResult) -> tuple[Any, ...]:
430
+ """Map CallbackResult to the app output component order."""
431
+
432
+ animation_image, animation_video = animation_path_updates(result.animation_path)
433
+
434
+ return (
435
+ result.world_spec_json,
436
+ result.validation_json,
437
+ animation_image,
438
+ animation_video,
439
+ result.population_chart_path,
440
+ result.resource_chart_path,
441
+ result.final_image_path,
442
+ _markdown_narrative(result.narrative),
443
+ result.metrics_json,
444
+ result.status_message,
445
+ )
446
+
447
+
448
+ def failure_to_ui_tuple(
449
+ error: BaseException,
450
+ *,
451
+ world_spec_json: str = "",
452
+ ) -> tuple[Any, ...]:
453
+ """Map an exception to a safe UI output tuple."""
454
+
455
+ result = callback_failure_result(error, world_spec_json=world_spec_json)
456
+ details = {
457
+ "success": False,
458
+ "error_type": error.__class__.__name__,
459
+ "error": str(error),
460
+ "traceback": traceback.format_exc(),
461
+ "callback_result": result.to_dict(),
462
+ }
463
+
464
+ return (
465
+ result.world_spec_json,
466
+ result.validation_json,
467
+ gr.update(value=None, visible=False),
468
+ gr.update(value=None, visible=False),
469
+ None,
470
+ None,
471
+ None,
472
+ _markdown_narrative(result.narrative),
473
+ _safe_json_dumps(details),
474
+ result.status_message,
475
+ )
476
+
477
+
478
+ def animation_path_updates(path: str | None) -> tuple[Any, Any]:
479
+ """Return Gradio updates for GIF/image and MP4/video animation components."""
480
+
481
+ if not path:
482
+ return gr.update(value=None, visible=False), gr.update(value=None, visible=False)
483
+
484
+ suffix = Path(path).suffix.lower()
485
+
486
+ if suffix in {".mp4", ".webm", ".mov", ".m4v"}:
487
+ return gr.update(value=None, visible=False), gr.update(value=path, visible=True)
488
+
489
+ return gr.update(value=path, visible=True), gr.update(value=None, visible=False)
490
+
491
+
492
+ def discover_examples() -> dict[str, Path]:
493
+ """Discover JSON example files in the root-level examples directory."""
494
+
495
+ examples_dir = Path(__file__).resolve().parent / "examples"
496
+ if not examples_dir.exists():
497
+ return {}
498
+
499
+ return {
500
+ path.stem.replace("_", " ").title(): path
501
+ for path in sorted(examples_dir.glob("*.json"))
502
+ }
503
+
504
+
505
+ def build_demo() -> gr.Blocks:
506
+ """Build and return the Gradio Blocks demo."""
507
+
508
+ examples = discover_examples()
509
+ example_names = list(examples.keys())
510
+ default_steps = _env_int("WORLDSMITHAI_DEFAULT_STEPS", 60)
511
+
512
+ with gr.Blocks(
513
+ title=f"{APP_TITLE} β€” Agent-Based World Simulation",
514
+ css=CUSTOM_CSS,
515
+ analytics_enabled=False,
516
+ ) as demo:
517
+ gr.Markdown(
518
+ f"""
519
+ <div id="worldsmith-header">
520
+
521
+ # {APP_TITLE}
522
+ ### {APP_SUBTITLE}
523
+
524
+ WorldSmithAI converts natural language into a validated JSON DSL, builds a deterministic Python
525
+ agent-based world, simulates emergent behavior, and returns animation, charts, metrics, and narration.
526
+
527
+ <span class="worldsmith-small">
528
+ No species or domains are hardcoded. A farmer, dragon, scientist, vehicle, startup, power node,
529
+ or civilization actor is a generic Agent with state, memory, behaviors, and policy.
530
+ </span>
531
+
532
+ </div>
533
+ """
534
+ )
535
+
536
+ with gr.Accordion("Runtime status", open=False):
537
+ gr.Markdown(MODEL_STATUS.to_markdown())
538
+ gr.Markdown(
539
+ "If no model client is configured, the app still runs with the deterministic fallback DSL generator. "
540
+ "Set WORLDSMITHAI_MODEL_ID in the Space environment to enable model-generated DSL."
541
+ )
542
+
543
+ with gr.Tabs():
544
+ with gr.Tab("Generate + Simulate"):
545
+ with gr.Row():
546
+ with gr.Column(scale=1):
547
+ prompt = gr.Textbox(
548
+ label="World prompt",
549
+ value=DEFAULT_PROMPT,
550
+ lines=8,
551
+ placeholder=(
552
+ "Describe any agent-based world: farm, civilization, research lab, "
553
+ "startup economy, fantasy realm, transport system..."
554
+ ),
555
+ )
556
+ constraints = gr.Textbox(
557
+ label="Optional constraints",
558
+ value='{"max_agents": 6, "demo_size": "small"}',
559
+ lines=4,
560
+ placeholder='Example: {"max_agents": 6, "steps": 80}',
561
+ )
562
+
563
+ with gr.Row():
564
+ steps = gr.Slider(
565
+ minimum=1,
566
+ maximum=250,
567
+ value=default_steps,
568
+ step=1,
569
+ label="Simulation steps",
570
+ )
571
+ animation_format = gr.Dropdown(
572
+ choices=["gif", "mp4"],
573
+ value="gif",
574
+ label="Animation format",
575
+ )
576
+
577
+ use_model = gr.Checkbox(
578
+ label="Use configured model client when available",
579
+ value=MODEL_CLIENT is not None,
580
+ interactive=True,
581
+ )
582
+
583
+ with gr.Row():
584
+ run_button = gr.Button("Generate world + run simulation", variant="primary")
585
+ clear_button = gr.Button("Clear outputs")
586
+
587
+ gr.Examples(
588
+ examples=PROMPT_EXAMPLES,
589
+ inputs=[prompt],
590
+ label="Prompt examples",
591
+ )
592
+
593
+ with gr.Column(scale=1):
594
+ status = gr.Textbox(
595
+ label="Status",
596
+ lines=3,
597
+ interactive=False,
598
+ elem_classes=["worldsmith-status"],
599
+ )
600
+ narrative = gr.Markdown(label="Narrative summary")
601
+
602
+ with gr.Tab("Visual outputs"):
603
+ with gr.Row():
604
+ animation_image = gr.Image(
605
+ label="Simulation animation GIF",
606
+ type="filepath",
607
+ visible=True,
608
+ )
609
+ animation_video = gr.Video(
610
+ label="Simulation animation MP4",
611
+ visible=False,
612
+ )
613
+
614
+ with gr.Row():
615
+ final_image = gr.Image(
616
+ label="Final world state",
617
+ type="filepath",
618
+ )
619
+ population_chart = gr.Image(
620
+ label="Population chart",
621
+ type="filepath",
622
+ )
623
+ resource_chart = gr.Image(
624
+ label="Resource chart",
625
+ type="filepath",
626
+ )
627
+
628
+ with gr.Tab("Generated DSL + diagnostics"):
629
+ world_spec_json = gr.Code(
630
+ label="Generated WorldSpec JSON",
631
+ language="json",
632
+ lines=24,
633
+ )
634
+ validation_json = gr.Code(
635
+ label="Validation report",
636
+ language="json",
637
+ lines=16,
638
+ )
639
+ metrics_json = gr.Code(
640
+ label="Metrics and run diagnostics",
641
+ language="json",
642
+ lines=24,
643
+ )
644
+
645
+ main_outputs = [
646
+ world_spec_json,
647
+ validation_json,
648
+ animation_image,
649
+ animation_video,
650
+ population_chart,
651
+ resource_chart,
652
+ final_image,
653
+ narrative,
654
+ metrics_json,
655
+ status,
656
+ ]
657
+
658
+ run_button.click(
659
+ fn=run_from_prompt_ui,
660
+ inputs=[prompt, steps, constraints, animation_format, use_model],
661
+ outputs=main_outputs,
662
+ show_progress=True,
663
+ )
664
+
665
+ clear_button.click(
666
+ fn=clear_main_outputs,
667
+ inputs=[],
668
+ outputs=main_outputs,
669
+ )
670
+
671
+ with gr.Tab("Generate DSL only"):
672
+ with gr.Row():
673
+ with gr.Column(scale=1):
674
+ dsl_prompt = gr.Textbox(
675
+ label="World prompt",
676
+ value=DEFAULT_PROMPT,
677
+ lines=8,
678
+ )
679
+ dsl_constraints = gr.Textbox(
680
+ label="Optional constraints",
681
+ value='{"max_agents": 6}',
682
+ lines=4,
683
+ )
684
+ dsl_steps = gr.Slider(
685
+ minimum=1,
686
+ maximum=250,
687
+ value=default_steps,
688
+ step=1,
689
+ label="Simulation steps to put in DSL",
690
+ )
691
+ dsl_use_model = gr.Checkbox(
692
+ label="Use configured model client when available",
693
+ value=MODEL_CLIENT is not None,
694
+ )
695
+ generate_dsl_button = gr.Button("Generate DSL", variant="primary")
696
+
697
+ with gr.Column(scale=1):
698
+ generated_dsl_status = gr.Textbox(
699
+ label="Status",
700
+ lines=3,
701
+ interactive=False,
702
+ )
703
+ generated_dsl_validation = gr.Code(
704
+ label="Validation report",
705
+ language="json",
706
+ lines=16,
707
+ )
708
+
709
+ generated_dsl = gr.Code(
710
+ label="Generated WorldSpec JSON",
711
+ language="json",
712
+ lines=28,
713
+ )
714
+
715
+ generate_dsl_button.click(
716
+ fn=generate_dsl_only_ui,
717
+ inputs=[dsl_prompt, dsl_constraints, dsl_use_model, dsl_steps],
718
+ outputs=[generated_dsl, generated_dsl_validation, generated_dsl_status],
719
+ show_progress=True,
720
+ )
721
+
722
+ with gr.Tab("Simulate existing DSL"):
723
+ with gr.Row():
724
+ with gr.Column(scale=1):
725
+ example_dropdown = gr.Dropdown(
726
+ choices=example_names,
727
+ value=example_names[0] if example_names else None,
728
+ label="Load example JSON",
729
+ interactive=bool(example_names),
730
+ )
731
+ load_example_button = gr.Button("Load selected example")
732
+ example_status = gr.Textbox(
733
+ label="Example status",
734
+ lines=2,
735
+ interactive=False,
736
+ )
737
+
738
+ existing_dsl = gr.Code(
739
+ label="WorldSpec JSON",
740
+ language="json",
741
+ lines=28,
742
+ value="",
743
+ )
744
+
745
+ with gr.Row():
746
+ existing_steps = gr.Slider(
747
+ minimum=1,
748
+ maximum=250,
749
+ value=default_steps,
750
+ step=1,
751
+ label="Simulation steps override",
752
+ )
753
+ existing_animation_format = gr.Dropdown(
754
+ choices=["gif", "mp4"],
755
+ value="gif",
756
+ label="Animation format",
757
+ )
758
+
759
+ existing_use_model = gr.Checkbox(
760
+ label="Use configured model client for narration when available",
761
+ value=False,
762
+ )
763
+ simulate_dsl_button = gr.Button("Simulate DSL", variant="primary")
764
+
765
+ with gr.Column(scale=1):
766
+ existing_status = gr.Textbox(
767
+ label="Status",
768
+ lines=3,
769
+ interactive=False,
770
+ )
771
+ existing_narrative = gr.Markdown(label="Narrative summary")
772
+
773
+ with gr.Tab("DSL simulation visuals"):
774
+ with gr.Row():
775
+ existing_animation_image = gr.Image(
776
+ label="Simulation animation GIF",
777
+ type="filepath",
778
+ visible=True,
779
+ )
780
+ existing_animation_video = gr.Video(
781
+ label="Simulation animation MP4",
782
+ visible=False,
783
+ )
784
+
785
+ with gr.Row():
786
+ existing_final_image = gr.Image(
787
+ label="Final world state",
788
+ type="filepath",
789
+ )
790
+ existing_population_chart = gr.Image(
791
+ label="Population chart",
792
+ type="filepath",
793
+ )
794
+ existing_resource_chart = gr.Image(
795
+ label="Resource chart",
796
+ type="filepath",
797
+ )
798
+
799
+ with gr.Tab("DSL simulation diagnostics"):
800
+ existing_validation = gr.Code(
801
+ label="Validation report",
802
+ language="json",
803
+ lines=16,
804
+ )
805
+ existing_metrics = gr.Code(
806
+ label="Metrics and run diagnostics",
807
+ language="json",
808
+ lines=24,
809
+ )
810
+
811
+ load_example_button.click(
812
+ fn=load_example_ui,
813
+ inputs=[example_dropdown],
814
+ outputs=[existing_dsl, example_status],
815
+ )
816
+
817
+ simulate_dsl_button.click(
818
+ fn=simulate_dsl_ui,
819
+ inputs=[
820
+ existing_dsl,
821
+ existing_steps,
822
+ existing_animation_format,
823
+ existing_use_model,
824
+ ],
825
+ outputs=[
826
+ existing_dsl,
827
+ existing_validation,
828
+ existing_animation_image,
829
+ existing_animation_video,
830
+ existing_population_chart,
831
+ existing_resource_chart,
832
+ existing_final_image,
833
+ existing_narrative,
834
+ existing_metrics,
835
+ existing_status,
836
+ ],
837
+ show_progress=True,
838
+ )
839
+
840
+ with gr.Tab("Validate DSL"):
841
+ validation_input = gr.Code(
842
+ label="WorldSpec JSON to validate",
843
+ language="json",
844
+ lines=28,
845
+ )
846
+ validate_button = gr.Button("Validate DSL", variant="primary")
847
+ validation_output = gr.Code(
848
+ label="Validation report",
849
+ language="json",
850
+ lines=24,
851
+ )
852
+ validation_status = gr.Textbox(
853
+ label="Status",
854
+ lines=3,
855
+ interactive=False,
856
+ )
857
+
858
+ validate_button.click(
859
+ fn=validate_dsl_ui,
860
+ inputs=[validation_input],
861
+ outputs=[validation_output, validation_status],
862
+ )
863
+
864
+ with gr.Tab("About"):
865
+ gr.Markdown(
866
+ """
867
+ ## WorldSmithAI architecture
868
+
869
+ WorldSmithAI follows this pipeline:
870
+
871
+ Natural language prompt
872
+ β†’ SLM-generated JSON DSL
873
+ β†’ Pydantic schema validation
874
+ β†’ Semantic DSL validation
875
+ β†’ WorldFactory
876
+ β†’ World / Scheduler / Agents / Policies / Behaviors
877
+ β†’ Metrics / Visualization / Narration
878
+ β†’ Gradio UI
879
+
880
+ ### Important design constraint
881
+
882
+ The simulation engine is deterministic Python. The model generates JSON only, never Python code.
883
+
884
+ ### UI outputs
885
+
886
+ - Animation: GIF by default; MP4 if selected and ffmpeg is available.
887
+ - Population chart: grouped by agent type.
888
+ - Resource chart: grouped by resource type and weighted by amount.
889
+ - Narrative: deterministic by default, optionally model-assisted if a client is configured.
890
+ - Diagnostics: includes DSL validation, factory build report, metric history, artifact paths, and generation mode.
891
+
892
+ ### Suggested root-level Space layout
893
+
894
+ app.py
895
+ callbacks.py
896
+ requirements.txt
897
+ core/
898
+ behaviors/
899
+ policies/
900
+ dsl/
901
+ factory/
902
+ metrics/
903
+ visualization/
904
+ llm/
905
+ examples/
906
+ """
907
+ )
908
+
909
+ return demo
910
+
911
+
912
+ def _extract_model_text(response: Any) -> str | None:
913
+ """Extract text from common model response shapes."""
914
+
915
+ if response is None:
916
+ return None
917
+
918
+ if isinstance(response, str):
919
+ return response.strip()
920
+
921
+ if isinstance(response, bytes):
922
+ return response.decode("utf-8").strip()
923
+
924
+ if isinstance(response, Mapping):
925
+ for key in ("text", "content", "generated_text", "output", "response"):
926
+ value = response.get(key)
927
+ if value is not None:
928
+ return _extract_model_text(value)
929
+
930
+ choices = response.get("choices")
931
+ if isinstance(choices, list) and choices:
932
+ return _extract_model_text(choices[0])
933
+
934
+ message = response.get("message")
935
+ if isinstance(message, Mapping):
936
+ return _extract_model_text(message.get("content"))
937
+
938
+ for attr in ("text", "content", "generated_text", "output", "response"):
939
+ value = getattr(response, attr, None)
940
+ if value is not None:
941
+ return _extract_model_text(value)
942
+
943
+ choices = getattr(response, "choices", None)
944
+ if choices:
945
+ return _extract_model_text(choices[0])
946
+
947
+ message = getattr(response, "message", None)
948
+ if message is not None:
949
+ return _extract_model_text(message)
950
+
951
+ return None
952
+
953
+
954
+ def _coerce_optional_int(value: int | float | str | None) -> int | None:
955
+ """Coerce UI numeric values to optional int."""
956
+
957
+ if value is None or value == "":
958
+ return None
959
+
960
+ return int(float(value))
961
+
962
+
963
+ def _env_int(name: str, default: int) -> int:
964
+ """Read an integer environment variable with a safe default."""
965
+
966
+ raw = os.getenv(name)
967
+ if raw is None or raw == "":
968
+ return default
969
+
970
+ try:
971
+ return int(float(raw))
972
+ except ValueError:
973
+ logger.warning("Invalid integer environment variable %s=%r", name, raw)
974
+ return default
975
+
976
+
977
+ def _safe_json_dumps(value: Any) -> str:
978
+ """Serialize values as pretty JSON for Gradio Code components."""
979
+
980
+ return json.dumps(
981
+ _json_safe(value),
982
+ indent=2,
983
+ sort_keys=True,
984
+ ensure_ascii=False,
985
+ )
986
+
987
+
988
+ def _json_safe(value: Any) -> Any:
989
+ """Return JSON-friendly representation of arbitrary values."""
990
+
991
+ if value is None or isinstance(value, (str, bool)):
992
+ return value
993
+
994
+ if isinstance(value, int) and not isinstance(value, bool):
995
+ return value
996
+
997
+ if isinstance(value, float):
998
+ if value != value or value in {float("inf"), float("-inf")}:
999
+ return None
1000
+ return value
1001
+
1002
+ if isinstance(value, Mapping):
1003
+ return {str(key): _json_safe(nested) for key, nested in value.items()}
1004
+
1005
+ if isinstance(value, (list, tuple)):
1006
+ return [_json_safe(item) for item in value]
1007
+
1008
+ if hasattr(value, "to_dict") and callable(value.to_dict):
1009
+ return _json_safe(value.to_dict())
1010
+
1011
+ if hasattr(value, "model_dump") and callable(value.model_dump):
1012
+ return _json_safe(value.model_dump(mode="json"))
1013
+
1014
+ return str(value)
1015
+
1016
+
1017
+ def _markdown_narrative(text: str) -> str:
1018
+ """Return narrative text formatted for Markdown display."""
1019
+
1020
+ cleaned = str(text or "").strip()
1021
+ if not cleaned:
1022
+ return "No narrative was generated."
1023
+
1024
+ return cleaned
1025
+
1026
+
1027
+ demo = build_demo()
1028
+ app = demo
1029
+
1030
 
1031
+ if __name__ == "__main__":
1032
+ demo.queue().launch()